Coding — Файловая система proc
1. В двух словах о procfs
«procfs – виртуальная файловая система, используемая в UNIX-like операционных системах. procfs позволяет получить доступ к информации о системных процессах из ядра, она необходима для выполнения таких команд как ps, w, top...» - Wikipediaprocfs является псевдофайловой системой, которая хранит и собирает информацию о системе и о процессах в частности. Например, информация о процессоре (процессорах) содержится в файле /proc/cpuinfo и получить ее можно с помощью команды cat:
1 2 3 4 5 6 7 8 9 10 |
$ cat /proc/cpuinfo |
В том числе procfs позволяет менять поведение ядра без необходимости заново его собирать, загружать модули или перезагружать систему.
Более подробно о назначении файлов и каталогов procfs и о том, какую информацию они предоставляют, вы можете узнать на http://welinux.ru/post/4487. Также есть замечательное руководство "Red Hat Enterprise Linux 6. Deployment Guide" (ссылка на pdf), где содержимому /proc (Раздел 19) уделяется большое внимание.
2. Структуры VFS
Прежде чем перейти к файловой системе proc, вкратце рассмотрим основные структуры (объекты) VFS.Virtual File System или Virtual Filesystem Switch (VFS) представляет собой "слой" между приложениями пространства пользователя и реализацией файловых систем, другими словами, VFS предоставляет единый интерфейс для доступа к различным ФС:
Рис. 2.1. Взаимодействие с VFS
Это означает, что мы имеем возможность использовать одни и те же системные вызовы для разных файловых систем.
Для VFS есть несколько основных объектов, которые можно условно разделить на два типа: объекты, описывающие файловую систему, и объекты, описывающие элементы файловой системы.
Первый тип объектов:
- Структура "file_system_type" представляет зарегистрированную ФС:
1
2
3
4
5
6
7
8
9
10
11
12include/linux/fs.h:
struct file_system_type {
const char *name;
int fs_flags;
int (*get_sb) (struct file_system_type *, int,
const char *, void *, struct vfsmount *);
void (*kill_sb) (struct super_block *);
struct module *owner;
struct file_system_type * next;
struct list_head fs_supers;
...
};
- name – имя файловой системы;
- fs_flags – флаги ФС. Например, флаг FS_REQUIRES_DEV указывает на то, что ФС не является "псевдо" (в файле /proc/filesystems указывается "nodev", если данный флаг не был выставлен). Все флаги определены в файле include/linux/fs.h;
- get_sb – указатель на функцию выделения суперблока;
- kill_sb – указатель на функцию освобождения суперблока;
- owner – если ФС была собрана как загружаемый модуль - указатель на модуль, отвечающий за данную ФС. В противном случае, если ФС встроена в ядро, указывается NULL;
- next – указатель на следующую файловую систему. Все зарегистрированные ФС образуют список, на вершину которого указывает глобальная переменная "file_systems" (см. рис. 2.2, полный список зарегистрированных ФС можно получить в файле /proc/filesystems);
- fs_supers – список суперблоков для файловых систем данного типа.
Рис. 2.2. Список зарегистрированных файловых систем - Структура "vfsmount" представляет точку монтирования:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15include/linux/mount.h:
struct vfsmount {
struct list_head mnt_hash;
struct vfsmount *mnt_parent; /* fs we are mounted on */
struct dentry *mnt_mountpoint; /* dentry of mountpoint */
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
...
int mnt_flags;
const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
struct list_head mnt_list;
...
int mnt_id; /* mount identifier */
...
};
- mnt_parent — родительская точка монтирования;
- mnt_mountpoint — dentry (имя) для точки монтирования;
- mnt_root — dentry (имя) для корневой точки монтирования (обычно «/»);
- mnt_sb — указатель на связанный суперблок;
- mnt_flags — флаги, с которыми монтируется ФС. Например, флаг MS_RDONLY указывает, что ФС будет смонтирована только для чтения. Все флаги определены в файле include/linux/fs.h;
- mnt_devname — имя устройства;
- mnt_list — список всех точек монтирования (mounts). Данный список можно просмотреть в файле /proc/mounts;
- mnt_id — идентификатор точки монтирования.
Второй тип объектов:
- Объект суперблок (superblock) — это структура данных, которая хранит информацию о ФС в целом (типе ФС, размере, состоянии и т.д.) и выделяется при монтировании для каждой ФС:
1
2
3
4
5
6
7
8
9
10include/linux/fs.h:
struct super_block {
struct list_head s_list; /* Keep this first */
...
struct file_system_type *s_type;
const struct super_operations *s_op;
...
struct dentry *s_root;
...
};
- s_list — список суперблоков;
- s_type — тип файловой системы, к которой принадлежит суперблок;
- s_root — dentry корневой точки монтирования;
- s_op — указатель на таблицу операций, связанных с суперблоком:
1
2
3
4
5
6
7
8
9
10struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
...
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
...
int (*statfs) (struct dentry *, struct kstatfs *);
...
};
- alloc_inode — выделяет память под inode;
- destroy_inode — освобождает память, выделенную под inode;
- drop_inode — вызывается, когда счетчик ссылок на inode становится равным нулю;
- delete_inode — удаляет inode;
- statfs — получение статистики ФС.
- Объект файл (file) — представляет открытый файл (создается при открытии файла), связанный с процессом:
1
2
3
4
5
6
7
8
9
10
11
12include/linux/fs.h:
struct file {
union {
struct list_head fu_list;
...
} f_u;
struct path f_path;
#define f_dentry f_path.dentry
#define f_vfsmnt f_path.mnt
const struct file_operations *f_op;
...
};
- fu_list — список открытых файлов;
- f_path.dentry — указатель на соответствующую компоненту пути;
- f_path.mnt — указатель на точку монтирования;
- f_op — указатель на таблицу файловых операций. Файловый операции проводятся над содержимым файла. Названия большинства файловых операций совпадают с названиями соответствующих системных вызовов:
1
2
3
4
5
6
7
8
9
10
11
12struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
...
int (*readdir) (struct file *, void *, filldir_t);
...
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
...
};
- Объект элемент каталога (dentry) — представляет определенный элемент каталога (компонент пути). Основной целью использования dentry является установление связи между именем файла (каталога, FIFO и т.д. - «everything is file») и соответствующим inode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19include/linux/dcache.h:
struct dentry {
...
struct inode *d_inode; /* Where the name belongs to - NULL is negative */
...
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
...
union {
struct list_head d_child; /* child of parent list */
...
} d_u;
struct list_head d_subdirs; /* our children */
...
const struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
...
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
- d_inode — указатель на связанный inode;
- d_parent — указатель на dentry родителя;
- d_name — структура qstr содержит имя файла, его длину и хэш-сумму. Хранится не абсолютное имя, а только последняя его часть. Например, если абсолютное имя «/home/user/staff/example», то поле name структуры qstr содержит только «example», т.к. остальные компоненты пути можно получить из родительского dentry (см. рис. 2.3);
- d_child — список подкаталогов dentry родителя;
- d_subdirs — список подкаталогов данного dentry;
- d_sb — указатель на связанный суперблок;
- d_iname — содержит короткое имя файла;
- d_op — указатель на таблицу dentry операций (большинство файловых систем не реализуют эти функции, а используют реализацию, предоставляемую VFS):
1
2
3
4
5
6
7
8
9
10include/linux/dcache.h:
struct dentry_operations {
int (*d_revalidate)(struct dentry *, struct nameidata *);
int (*d_hash) (struct dentry *, struct qstr *);
int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
int (*d_delete)(struct dentry *);
void (*d_release)(struct dentry *);
void (*d_iput)(struct dentry *, struct inode *);
char *(*d_dname)(struct dentry *, char *, int);
};
Рис. 2.3. Абсолютное имя «example»
На рис. 2.1. вы можете видеть directory entry cache (dentry cache), который включается в VFS. Данный кэш предназначен для увеличения скорости поиска inode, связанного с именем файла (имя файла передается в качестве аргумента таким системным вызовам, как open(), stat(), chmod() и т.д.). - Объект файловый индекс (inode) — представляет определенный файл (обычные файлы, директории, FIFO и т.д.):
1
2
3
4
5
6
7
8
9
10
11
12include/linux/fs.h:
struct inode {
...
unsigned long i_ino;
atomic_t i_count;
umode_t i_mode
...
const struct inode_operations *i_op;
const struct file_operations *i_fop; /*former ->i_op->default_file_ops */
struct super_block *i_sb;
...
};
- i_ino – номер индекса;
- i_count – счетчик ссылок;
- i_mode – права доступа;
- i_sb – указатель на суперблок;
- i_fop – указатель на таблицу файловых операций. Когда открывается существующий файл, и для него создается структура "struct file", то файловые операции берутся из этого поля;
- i_op – указатель на таблицу inode операций. Inode операции производятся над структурами и метаданными, связанными с файлом. Также, как и с файловыми операциями, названия функций совпадают с названиями соответствующих системных вызовов:
1
2
3
4
5
6
7
8
9
10
11
12
13
14include/linux/fs.h:
struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
struct dentry * (*lookup) (struct inode *,struct dentry *,
struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry *);
...
int (*mkdir) (struct inode *,struct dentry *,int);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,int,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
...
};
Взаимосвязь между описанными объектами показана на рисунке 2.4.
Рис.2.4. Взаимосвязь между объектами VFS
Подведем некоторый итог и обратимся к рисунку 2.1. Справа показаны «приставки» и «окончания». Что они означают? Рассмотрим пример чтения из файла и предположим, что у нас есть следующий код:
1 2 3 |
... |
Функция read() является библиотечной функцией и принимает три аргумента: дескриптор файла (fd), из которого будет производиться чтение, указатель на буфер (buf), куда будут сохранены данные и сколько информации должно быть прочитано (count). В свою очередь read() является оберткой для системного вызова sys_read():
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fs/read_write.c: |
В системном вызове sys_read() определяется объект file, связанный с файловым дескриптором fd (с помощью функции fget_light()), определяется текущая позиция в файле pos (с помощью функции file_pos_read()) и затем вызывается функция vfs_read():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
fs/read_write.c: |
Здесь возможны два варианта: если в таблице файловых операций определен соответствующий метод, в данном случае чтение из файла, то чтение производится средствами файловой системы, к которой принадлежит этот файл, в противном случае будет вызвана стандартная функция VFS.
Дополнительную информацию о VFS можно получить в документации к ядру Documentation/fs/vfs.txt, а также по следующим ссылкам:
- http://www.ibm.com/developerworks/ru/library/l-linux-filesystem/index.html;
- http://www.ibm.com/developerworks/library/l-virtual-filesystem-switch/;
- http://www.opennet.ru/base/sys/linux_vfs.txt.html;
- http://tldp.org/LDP/khg/HyperNews/get/fs/vfstour.html.
3. Инициализация procfs и ее структуры
Инициализация файловой системы proc происходит еще на стадии загрузки ядра (про загрузку ядра можно прочитать на http://welinux.ru/post/4453). В функции start_kernel(), при условии что ядро было собрано с опцией «CONFIG_PROC_FS=y» (данная опция включена по умолчанию), вызывается proc_init_root():1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
fs/proc/root.c: |
proc_init_inodecache() — создается новый кэш "proc_inode_cache". Кэш представляет собой один или несколько слябов (slabs) и используется ядром Linux для быстрого выделения и освобождения структур ядра, связанных с процессами, файлами и сетевым стеком;
register_filesystem() — регистрируется новая файловая система proc. Регистрация файловой системы фактически является простым добавлением ее в список. Структура "file_system_type" описывает ФС и ее свойства и для procfs выглядит довольно просто:
1 2 3 4 5 6 |
fs/proc/root.c: |
kern_mount_data() — выделяется суперблок и монтируется в «/proc» (в отличие от «настоящих» ФС, у которых суперблок хранится на диске, у procfs он хранится в памяти). Суперблок выделяется в функции «proc_get_super()», которая, в свою очередь, вызывает функцию «proc_fill_super()» для заполнения основных полей суперблока;
proc_mkdir()/proc_symlink()/proc_xxx_init() — создание записей (каталоги, файлы, символические ссылки) в «/proc».
Остальные записи в «/proc» создаются другими компонентами ядра.
Каждая запись описывается структурой "proc_dir_entry":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
include/linux/proc_fs.h: |
- low_ino — идентификатор записи (по аналогии с inode);
- name – указатель на строку, в которой хранится имя файла;
- namelen – содержит длину имени файла;
- mode – содержит права доступа к файлу (стандартные владелец/группа/ остальные);
- nlink – количество подкаталогов и символических ссылок в данном каталоге;
- uid/gid – определяют идентификатор пользователя и идентификатор группы, соответственно;
- size – размер файла в байтах;
- proc_iops/proc_fops – указатели на структуры inode_operations и file_operations, они содержат операции, которые могут проводиться над inode и файлами, соответственно;
- next/parent/subdir — указатели на следующую запись, родителя и подкаталог, соответственно;
- data — используется для хранения дополнительных данных (например, для хранения целевого пути (target path) в случае ссылки);
- read_proc/write_proc – указатели на функции для чтения/записи данных из/в пространства ядра;
- count – счетчик использования.
За счет элементов nlink, next, parent и subdir структура файловой системы получает иерархическое представление (см. рис. 3.1). Корневая запись (root entry) представлена отдельной статической структурой «proc_dir_entry» и называется «proc_root»:
1 2 3 4 5 6 7 8 9 10 11 12 |
fs/proc/root.c: |
Рис. 3.1. Иерархическое представление структуры ФС экземплярами pde
Также ФС proc имеет и inode-ориентированное представление записей, за счет структуры proc_inode:
1 2 3 4 5 6 7 8 9 |
include/linux/proc_fs.h: |
- pid — указатель на структуру pid, связанную с процессом (struct pid является внутренним представлением ядра о идентификаторе процесса);
- fd — файловый дескриптор, для которого представлена информация в файле /proc//fd;
- op — в объединении находятся операции proc_get_link, proc_read и proc_show для получения process-specific информации;
- pde — указатель на соответствующую структуру «proc_dir_entry»;
- vfs_inode — inode, используемый уровнем VFS. Таким образом, перед каждым inode, связанным с ФС proc, есть дополнительные данные в памяти.
4. Поиск записей
Поиск записей начинается с корневой записи proc_root:1 2 3 4 5 6 7 8 9 10 |
fs/proc/root.c: |
Тут возможны два варианта: поиск обычных записей (например /proc/cpuinfo, /proc/fs/ext4/sda/mb_groups и т.д.) и поиск process-specific записей (например /proc/1/cmdline). Так как заранее не известно какого типа ищется запись, то вначале производится поиск обычных записей в функции proc_lookup(), и, если запись не была найдена, производится поиск process-specific записей функцией proc_pid_lookup().
Рассмотри второй вариант — поиск process-specific записи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
fs/proc/base.c: |
К данному моменту был создан inode для /proc/, и установлены операции, которые можно с ним проводить. Как быть с его содержимым? И снова поиск:
1 2 3 4 5 |
static const struct inode_operations proc_tgid_base_inode_operations = { |
За дальнейший поиск отвечает proc_tgid_base_lookup(), для чего предназначены другие две функции, вы уже должны понимать (получить/установить атрибуты каталога, например, владельца или права доступа).
proc_tgid_base_lookup() передает всю работу proc_pident_lookup():
1 2 3 4 5 |
static struct dentry *proc_tgid_base_lookup(struct inode *dir, |
Здесь следует обратить внимание на массив tgid_base_stuff. Список содержимого pid-директорий всегда один и тот же и определяется еще на стадии конфигурирования ядра. Массив tgid_base_stuff и определяет этот список:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
fs/proc/base.c: |
DIR, REG, INF, LNK, ONE являются макросами, предназначеными для генерации директорий (DIR), файлов (REG, INF, ONE) и ссылок (LNK). Каждый макрос определяет тип записи, имя записи, права и операции.
Вернемся к поиску и функции proc_pident_lookup():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static struct dentry *proc_pident_lookup(struct inode *dir, |
Поиск в ней осуществляется довольно просто — перебором, сравнивая искомое имя с именем из массива tgid_base_stuff. Затем, когда имя найдено, в функции proc_pident_instantiate создается inode, и устанавливаются соответствующие операции.
Суммируя сказанное и опуская детали, рассмотрим следующую команду:
$ cat /proc/filesystems
Что происходит? Происходит вызов sys_open(), который является системным вызовом. В нем подготавливаются все необходимые объекты ядра и ищется inode (много функций спустя мы дойдем до описанной ранее функции proc_root_lookup()), связанный с файлом filesystems. Далее, когда все операции завершены, вызывается sys_read() для чтения данных из файла. Ранее было описано, какой путь проделывает функция чтения и мы остановились на том, что вызывается специфичная для ФС функция чтения, если для файла зарегистрирована операция чтения. В данном случае будет вызвана функция proc_reg_read() (reg означает regular, то есть обычный файл) для чтения данных из файла. И в конце концов будет вызван sys_write(), но уже отношения к proc данный вызов иметь не будет, так как все данные записываются в стандартный вывод (stdout).
5. Модули ядра
ФС proc предоставляет удобные интерфейсы для управления записями. Следует отметить, что в реализации самой ФС proc они не используются.Опишем некоторые из функций и их использование:
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode, struct proc_pid_entry *parent);
Создание новой записи (обычного файла). Функция возвращает указатель на созданную запись в случае успеха, в противном случае NULL. create_proc_entry() принимает три аргумента:
name — имя записи;
mode — права доступа к файлу (владелец/группа/остальные);
parent — указатель на запись-родителя. Если новая запись создается в корне ФС proc, то следует указывать NULL.
void remove_proc_entry(const char *name, struct proc_pid_entry *parent);
Функция remove_proc_entry() удаляет запись и принимает два аргумента: имя удаляемой записи и каталог, из которого она удаляется.
Чтение/запись из файла осуществляется с помощью функций, которые должен разработать пользователь. Указатели на эти функции ставятся в соответствующих полях записи (см. раздел 3, описание структуры proc_dir_entry, поля read_proc/write_proc):
1 2 3 4 |
struct proc_pid_entry *entry; |
Ниже представлен простой модуль ядра, при загрузке которого создается новая запись в корне ФС proc и удаляется при выгрузке модуля:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
/* |
Содержимое Makefile:
1 2 3 4 5 |
obj-m += proc_sample.o |
Собираем модуль и затем загружаем его:
1 2 3 4 5 6 7 8 9 10 11 |
# make |
Больше информации по написанию модулей для ФС proc можно получить на:
- http://www.gnugeneration.com/books/linux/2.6.20/procfs-guide/;
- http://www.ibm.com/developerworks/ru/library/l-proc/.
Если заметите очепятки, то в ЛС. Если будут смысловые ошибки, то пишите в комментарии (даже если сомневаетесь, все спокойно обсудим).