dementiy 06.12.2010 02:08
Coding — Файловая система proc
1. В двух словах о procfs
«procfs – виртуальная файловая система, используемая в UNIX-like операционных системах. procfs позволяет получить доступ к информации о системных процессах из ядра, она необходима для выполнения таких команд как ps, w, top...» - Wikipediaprocfs является псевдофайловой системой, которая хранит и собирает информацию о системе и о процессах в частности. Например, информация о процессоре (процессорах) содержится в файле /proc/cpuinfo и получить ее можно с помощью команды cat:
В том числе 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" представляет зарегистрированную ФС:
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" представляет точку монтирования:
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) — это структура данных, которая хранит информацию о ФС в целом (типе ФС, размере, состоянии и т.д.) и выделяется при монтировании для каждой ФС:
s_list — список суперблоков;
s_type — тип файловой системы, к которой принадлежит суперблок;
s_root — dentry корневой точки монтирования;
s_op — указатель на таблицу операций, связанных с суперблоком:
alloc_inode — выделяет память под inode;
destroy_inode — освобождает память, выделенную под inode;
drop_inode — вызывается, когда счетчик ссылок на inode становится равным нулю;
delete_inode — удаляет inode;
statfs — получение статистики ФС.
Объект файл (file) — представляет открытый файл (создается при открытии файла), связанный с процессом:
fu_list — список открытых файлов;
f_path.dentry — указатель на соответствующую компоненту пути;
f_path.mnt — указатель на точку монтирования;
f_op — указатель на таблицу файловых операций. Файловый операции проводятся над содержимым файла. Названия большинства файловых операций совпадают с названиями соответствующих системных вызовов:
Объект элемент каталога (dentry) — представляет определенный элемент каталога (компонент пути). Основной целью использования dentry является установление связи между именем файла (каталога, FIFO и т.д. - «everything is file») и соответствующим inode:
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):
Рис. 2.3. Абсолютное имя «example»
На рис. 2.1. вы можете видеть directory entry cache (dentry cache), который включается в VFS. Данный кэш предназначен для увеличения скорости поиска inode, связанного с именем файла (имя файла передается в качестве аргумента таким системным вызовам, как open(), stat(), chmod() и т.д.).
Объект файловый индекс (inode) — представляет определенный файл (обычные файлы, директории, FIFO и т.д.):
i_ino – номер индекса;
i_count – счетчик ссылок;
i_mode – права доступа;
i_sb – указатель на суперблок;
i_fop – указатель на таблицу файловых операций. Когда открывается существующий файл, и для него создается структура "struct file", то файловые операции берутся из этого поля;
i_op – указатель на таблицу inode операций. Inode операции производятся над структурами и метаданными, связанными с файлом. Также, как и с файловыми операциями, названия функций совпадают с названиями соответствующих системных вызовов:
Взаимосвязь между описанными объектами показана на рисунке 2.4.
Рис.2.4. Взаимосвязь между объектами VFS
Подведем некоторый итог и обратимся к рисунку 2.1. Справа показаны «приставки» и «окончания». Что они означают? Рассмотрим пример чтения из файла и предположим, что у нас есть следующий код:
1 |
|
Функция read() является библиотечной функцией и принимает три аргумента: дескриптор файла (fd), из которого будет производиться чтение, указатель на буфер (buf), куда будут сохранены данные и сколько информации должно быть прочитано (count). В свою очередь read() является оберткой для системного вызова sys_read():
В системном вызове sys_read() определяется объект file, связанный с файловым дескриптором fd (с помощью функции fget_light()), определяется текущая позиция в файле pos (с помощью функции file_pos_read()) и затем вызывается функция vfs_read():
Здесь возможны два варианта: если в таблице файловых операций определен соответствующий метод, в данном случае чтение из файла, то чтение производится средствами файловой системы, к которой принадлежит этот файл, в противном случае будет вызвана стандартная функция 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():proc_init_inodecache() — создается новый кэш "proc_inode_cache". Кэш представляет собой один или несколько слябов (slabs) и используется ядром Linux для быстрого выделения и освобождения структур ядра, связанных с процессами, файлами и сетевым стеком;
register_filesystem() — регистрируется новая файловая система proc. Регистрация файловой системы фактически является простым добавлением ее в список. Структура "file_system_type" описывает ФС и ее свойства и для procfs выглядит довольно просто:
kern_mount_data() — выделяется суперблок и монтируется в «/proc» (в отличие от «настоящих» ФС, у которых суперблок хранится на диске, у procfs он хранится в памяти). Суперблок выделяется в функции «proc_get_super()», которая, в свою очередь, вызывает функцию «proc_fill_super()» для заполнения основных полей суперблока;
proc_mkdir()/proc_symlink()/proc_xxx_init() — создание записей (каталоги, файлы, символические ссылки) в «/proc».
Остальные записи в «/proc» создаются другими компонентами ядра.
Каждая запись описывается структурой "proc_dir_entry":
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»:
Рис. 3.1. Иерархическое представление структуры ФС экземплярами pde
Также ФС proc имеет и inode-ориентированное представление записей, за счет структуры proc_inode:
1 |
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:Тут возможны два варианта: поиск обычных записей (например /proc/cpuinfo, /proc/fs/ext4/sda/mb_groups и т.д.) и поиск process-specific записей (например /proc/1/cmdline). Так как заранее не известно какого типа ищется запись, то вначале производится поиск обычных записей в функции proc_lookup(), и, если запись не была найдена, производится поиск process-specific записей функцией proc_pid_lookup().
Рассмотри второй вариант — поиск process-specific записи:
К данному моменту был создан inode для /proc/, и установлены операции, которые можно с ним проводить. Как быть с его содержимым? И снова поиск:
1 |
static const struct inode_operations proc_tgid_base_inode_operations = {
|
За дальнейший поиск отвечает proc_tgid_base_lookup(), для чего предназначены другие две функции, вы уже должны понимать (получить/установить атрибуты каталога, например, владельца или права доступа).
proc_tgid_base_lookup() передает всю работу proc_pident_lookup():
Здесь следует обратить внимание на массив tgid_base_stuff. Список содержимого pid-директорий всегда один и тот же и определяется еще на стадии конфигурирования ядра. Массив tgid_base_stuff и определяет этот список:
DIR, REG, INF, LNK, ONE являются макросами, предназначеными для генерации директорий (DIR), файлов (REG, INF, ONE) и ссылок (LNK). Каждый макрос определяет тип записи, имя записи, права и операции.
Вернемся к поиску и функции proc_pident_lookup():
Поиск в ней осуществляется довольно просто — перебором, сравнивая искомое имя с именем из массива tgid_base_stuff. Затем, когда имя найдено, в функции proc_pident_instantiate создается inode, и устанавливаются соответствующие операции.
Суммируя сказанное и опуская детали, рассмотрим следующую команду:
1 |
|
Что происходит? Происходит вызов sys_open(), который является системным вызовом. В нем подготавливаются все необходимые объекты ядра и ищется inode (много функций спустя мы дойдем до описанной ранее функции proc_root_lookup()), связанный с файлом filesystems. Далее, когда все операции завершены, вызывается sys_read() для чтения данных из файла. Ранее было описано, какой путь проделывает функция чтения и мы остановились на том, что вызывается специфичная для ФС функция чтения, если для файла зарегистрирована операция чтения. В данном случае будет вызвана функция proc_reg_read() (reg означает regular, то есть обычный файл) для чтения данных из файла. И в конце концов будет вызван sys_write(), но уже отношения к proc данный вызов иметь не будет, так как все данные записываются в стандартный вывод (stdout).
5. Модули ядра
ФС proc предоставляет удобные интерфейсы для управления записями. Следует отметить, что в реализации самой ФС proc они не используются.Опишем некоторые из функций и их использование:
1 |
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.
1 |
|
Функция remove_proc_entry() удаляет запись и принимает два аргумента: имя удаляемой записи и каталог, из которого она удаляется.
Чтение/запись из файла осуществляется с помощью функций, которые должен разработать пользователь. Указатели на эти функции ставятся в соответствующих полях записи (см. раздел 3, описание структуры proc_dir_entry, поля read_proc/write_proc):
1 |
struct proc_pid_entry *entry;
|
Ниже представлен простой модуль ядра, при загрузке которого создается новая запись в корне ФС proc и удаляется при выгрузке модуля:
Содержимое Makefile:
1 |
obj-m += proc_sample.o
|
Собираем модуль и затем загружаем его:
Больше информации по написанию модулей для ФС proc можно получить на:
http://www.gnugeneration.com/books/linux/2.6.20/procfs-guide/;
http://www.ibm.com/developerworks/ru/library/l-proc/.P.S. Традиционная pdf'ка (пока лучше просто качать, scrib.com надругался над текстом (шрифты уже менял - не помогает); в pdf есть сноски с небольшими пояснениями). Что хотелось бы сказать по поводу самой статьи. Я думаю, что вы заметили - много кода, могу убрать под спойлер, так что говорите, как будет удобнее. Далее, я обещал написать ее к концу ноября, но не успел. Сама статья не является завершенной, это был черновик (я его как мог "причесал" и вот он перед вами). Но продолжать работу над статьей сейчас нет желания. Если кто-то захочет ее детализировать и дополнить, напишите в ЛС, буду рад и подскажу, что еще хотелось бы видеть.
Если заметите очепятки, то в ЛС. Если будут смысловые ошибки, то пишите в комментарии (даже если сомневаетесь, все спокойно обсудим).
le087 06.12.2010 09:45 #
+ 0 -
Круто. Вы сделали большую работу. Спасибо.
В самом начале про dentry нужно написать зачем разделять понятия dentry и inode - у одного inode'а может быть более одного имени (либо ни одного) из-за hard link'ов.
struct file - не обязательно связан с процессом, ибо можно открыть файл один раз, а дальше передавать с пом. fork(2) или UNIX-сокетов в другие процессы; при этом struct file будет единственной. dup2(2) и пр., к слову, не создают новых struct file.
do_sync_read() не просто какая-то функция, а обёртка вокруг fops->aio_read() (попытка вызова асинхронной функции в синхронном режиме). Не реализовав ни одной функции *read() в ФС, глупо надеяться, что каким-то чудесным образом она появится.
SLAB произносится-таки как слэб/слаб, но не сляб :)
У copy_from_user() нужно проверять результат.
struct file - не обязательно связан с процессом, ибо можно открыть файл один раз, а дальше передавать с пом. fork(2) или UNIX-сокетов в другие процессы; при этом struct file будет единственной. dup2(2) и пр., к слову, не создают новых struct file.
do_sync_read() не просто какая-то функция, а обёртка вокруг fops->aio_read() (попытка вызова асинхронной функции в синхронном режиме). Не реализовав ни одной функции *read() в ФС, глупо надеяться, что каким-то чудесным образом она появится.
SLAB произносится-таки как слэб/слаб, но не сляб :)
У copy_from_user() нужно проверять результат.
Спасибо за дополнения. Соглашусь по всем пунктам, хотя про то, что file может быть не связан с процессом я не задумывался. На счет "сляб", так обычно пишут в источниках на русском, поэтому я тоже использовал такое написание, хотя наверное лучше оставлять термин на языке оригинала и писать "slab".
Более того, ни у одного процесса данный файл может не быть в таблице открытых файловых дескрипторов, при этом структура file может существовать - её может использовать какой-то левый драйвер. У структуры есть счётчик ссылок, пока последний драйвер не отпустит структуру, счётчик не обнулится, и структура не будет освобождена.
Вообще ещё в кернел моде можно работать с файлами без таблицы файловых дескрипторов, но за такое бьют молотком по рукам.
Про slab - не знаю, я на русском ничего по ядру не читал. Но по-английски произносится как ?.
Ещё - прежде чем создавать что-либо в /proc, лучше три раза подумать: действительно ли ваща информация напрямую связана с _процессами_? Если это вывод какой-то глобальной информации, то его лучше запихнуть в /sys (если знаете куда именно), либо в /sys/kernel/debug/ (если не знаете).
Вообще ещё в кернел моде можно работать с файлами без таблицы файловых дескрипторов, но за такое бьют молотком по рукам.
Про slab - не знаю, я на русском ничего по ядру не читал. Но по-английски произносится как ?.
Ещё - прежде чем создавать что-либо в /proc, лучше три раза подумать: действительно ли ваща информация напрямую связана с _процессами_? Если это вывод какой-то глобальной информации, то его лучше запихнуть в /sys (если знаете куда именно), либо в /sys/kernel/debug/ (если не знаете).