Ядро Linux: файловая подсистема ядра. VFS
Некоторые объекты VFS
Вместо введения
Ну вот наконец и я разродился новой статейкой (ох и понравилось же мне
статьи писать Wink ) К сожалению я управился с ней не так быстро, как
планировал. Какое-то время ушло на раскопки в недрах кода (в
экспериментах использовался исходный код ядра версии 2.4.23), затем
некоторое время пришлось затратить на правку уже написанного. Итого
более чем полмесяца! Объём материала тоже оказался больше, чем я мог
себе представить. Пришлось разбить всё это дело на несколько кусков.
Это самый первый.
Так вышло, что мне пришлось начать изучать работу файловой подсистемы
ядра. Это связано с работой над одним проектом. К сожалению, я нигде
не смог найти удовлетворительного описания работы ядра с файловыми
системами. Максимум, что удалось откопать на бескрайних просторах сети
- это довольно таки куцее описание VFS. Но детального описания
алгоритмов судя по всему нет. Это прискорбное обстоятельство заставило
меня зарыться на долгое время в исходники ядра. Эта статья является
результатом моих исследований в этой области.
К сожалению, не все участки кода, ответственного за работу с файловыми
системами документированы. Строго говоря, комментариев там вообще не
густо. Поэтому кое-что мне пришлось додумывать самому. Разобраться в
нескольких тысячах строках кода, где ясность приносится в жертву
эффективности, не так уж и просто, особенно если этот код написан не
тобой.
Эта статья едва ли может послужить хорошим описанием файловой
подсистемы ядра по вышеупомянутым причинам. Я бы сказал, что это лишь
попытка документирования работы этой части ядра.
Заранее прошу извинить мне некоторые возможные неточности в описании.
Надеюсь, что ваши комментарии помогут мне (и возможно кому-то ещё, кто
также интересуется этим предметом) лучше понять устройство и принцип
работы файловой подсистемы ядра.
По большому счёту это описание именно VFS и алгоритмов, которые ею
сипользуются. Как известно, одна из важнейших возможностей Linux - это
поддержка довольно таки большого числа файловых систем. Это делает
Linux более гибкой ОС. Благодаря этому использование Linux в паре с
другой ОС на одной и той же машине становится более приятным. Ядро
Linux поддерживает такие файловые системы, как ReiserFS, ext2/3, XFS,
NTFS, UFS, (к моему личному сожалению пока ещё нет полноценной
поддержки UFS2), AFFS, HPFS, SYSV, ISO9660, UDF, MS-DOS FAT, FAT*,
UMSDOS, Minix, SMB и проч. Ну как, впечатляет? Думаю, мало кто
пользовался всеми этими файловыми системами, хотя это вовсе на значит,
что их поддержка ненужна. Более того, этот список, судя по всему,
будет пополнен ещё не один раз!
В Linux, как и в Unix, доступ к содержимому носителей осуществляется
вовсе не по их именам (как в Win или MS-DOS). Все файловые системы
представлены как одно целое, в виде дерева каталогов и файлов. Это
дерево может обрастать новыми ветвями по мере монтирования новых
файловых систем. Каждая файловая система монтируется к каталогу (так
называемая точка монтирования - mount point). Интересная особенность:
подмонтировав файловую систему к каталогу /х, вы обнаружите, что файлы
которые в нём были до монтирования, исчезли. Вместо них в каталоге /х
находится то, что содержится на подмонтированной файловой системе. Но
повода впадать в панику нет. Просто прежнее содержимое каталога /х
стало временно недоступно. Как только вы размонтируете файловую
систему, подмонтированную ранее на /х, вы снова увидите свои
драгоценные файлы.
При разработке VFS была поставлена задача, сделать работу с этим
интерфейсом не только прозрачной, но и достаточно эффективной. Дело в
том, что VFS должна своевременно обрабатывать запросы от
пользовательских приложений, при этом ей необходимо следить за
корректностью и синхронизацией уже имеющихся данных, подкачивать новые
и т.п. и т.д.. В общем если бы VFS была одушевлённым существом, лично
я бы ей не позавидовал - черезчур уж это всё хлопотно :-) Для того,
чтобы оптимизировать работу VFS, применяется куча (пожалуй даже сверх
меры Wink ) всяких фишек типа списков, хэшей, кэшей и прочего добра.
Вот про это всё я и хотел бы расказать. Правда не знаю наперёд, как
это у меня получится. Но надеюсь, получится...
Для начала несколько слов о монтировании
Всем пользователям *nix-систем наверняка знакома процедура
монтирования внешних носителей или дисковых разделов, например. Ну
естественно, куда ж без неё! :-) Иногда, правда, это несколько
утомляет. А у вас никогда не возникал вопрос, зачем так усложнять
жизнь? Если да, то надеюсь эта статья в какой-то мере поможет вам что
к чему и разобраться в принципах работы ядра с файловыми системами
(далее для краткости просто ФС).
Когда вы говорите mount -t vfat /dev/hdXY /zzzz происходит
приблизительно следующее:
1. Внутри mount вызывает mount_one() (именно эта функция должна быть
задействована в нашем случае), которая в свою очередь вызывает...
2. try_mount_one(). try_mount_one() с помощью
check_special_mountprog() пытается выяснить, нет ли специальной
реализации утилиты mount для монтирования носителей с данной ФС (в
нашем случае она называлась бы mount.vfat или mount.xxx, где xxx -
название ФС, используемой на монтируемом носителе). Если таковая не
найдена то вызывается
3. guess_fstype_and_mount(), которая формирует аргументы вызова для
системного вызова mount()
4. guess_fstype_from_superblock() - эта функция была бы вызвана, если
бы мы не задали тип ФС явным образом. В этом случае
guess_fstype_from_superblock() попыталась бы определить ФС, прочитав
информацию из суперблока (описатель ФС, располагающийся в самом начале
раздела. Хотя, справедливости ради стоит сказать, что в некоторых ФС
суперблок располагается не в первых секторах. Так обстоит дело с
ReiserFS, где первые 64 Кб на разделе не содержат какой-либо нформации
о самой ФС. В ReiserFS они зарезервированы под boot-loader)
5. do_mount_syscall() делает системный вызов mount() :-)
(Можете посмотреть исходники mount из util-linux, или придётся
поверить мне на слово :-) )
В итоге, если при вызове вы не перепутали /dev/hdXY с /dev/ttyN (на
этом устройстве нет никакой ФС, это вообще не блочное устройство :-)
), и если /dev/hdXY - это дейтсвительно раздел, отформатированный под
FAT, то он будет смонтирован на /zzzz. mount зарегистрирует факт
монтирования носителя в файле mtab.
Проще говоря, вы сможете получить доступ к файлам, находящимся на этом
разделе через каталог /zzzz. Вполне естественно объединить все файлы
на всех логических/физических носителях, которые есть на вашей машине
в одно дерево каталогов! Не так ли?
Mutti, woher denn die Kinder kommen? (Мама, а откуда берутся дети?
:-) )
На примере системной утилиты mount я попытался дать общее
представление о работе с ФС в *nix. Как видно из описания её работы,
данного выше, всю работу проделывает вызов mount(). Утилита mount
выполныяет кое-какие дополнительные проверки, и, так сказать,
подготавливает почву для вызова mount(). Но это пока ещё user-space.
Наша же цель, узнать что происходит дальше, в самом ядре. Уже
догадываетесь, почему я так озаглавил этот пункт? :-)
Монтированием занимается системный вызов sys_mount() в fs/namespace.c.
Естественно, sys_mount() это далеко не всё. А как вы хотели, чтобы всё
так просто было? Ну позвольте, это же ядро, так что простота по
статусу не положена!
Прежде чем sys_mount() сделает нечто полезное, должны быть удачно
выполнены следующие вызовы (в порядке убывания уровня): do_mount(),
do_add_mount() и, наконец, do_kern_mount().
Все операции с файлами на подмонтированных ФС осуществляются через
интерфейс, именуемый VFS (Virtual File System) - виртуальная ФС. VFS -
это абстрактный уровень, обеспечивающий прозрачную работу с ФС,
поддерживаемыми ядром. Само собой разумеется, если для каждой такой ФС
реализовать отдельные вызовы, то на эффективности работы ядра это
скажется далеко не лучшим образом. И вообще, если бы это было так, то
жизнь программиста на этой платформе превратилась бы в ад. Эту
проблему и призвана была решить VFS.
На уровне VFS реализованы основные вызовы файлового API Unix - open(),
stat(), chmod() и т.д.. Аргумент pathname, передаваемый этим функциям
при вызове, сипользуется VFS для поиска вхождения dentry для данного
файла в кэше каталогов (dentry cache или попросту "dcache"). Структура
dentry [include/linux/dcache.h] имеет следующий вид:
struct dentry {
atomic_t d_count;
unsigned int d_flags;
struct inode * d_inode; /* Where the name belongs to - NULL is negative */
struct dentry * d_parent; /* parent directory */
struct list_head d_hash; /* lookup hash list */
struct list_head d_lru; /* d_count = 0 LRU list */
struct list_head d_child; /* child of parent list */
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
int d_mounted;
struct qstr d_name;
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block * d_sb; /* The root of the dentry tree */
unsigned long d_vfs_flags;
void * d_fsdata; /* fs-specific data */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
};
Собственно, она описывает не только каталоги но и всё то, что на диске
может быть описано с помощью inode. Как
видно из приведённого выше описания этой структуры, она содержит
указатель на inode данного файла - поле d_inode.
inode обитают на диске. Они могут описывать обычные файлы, каталоги,
каналы и проч.. При необходимости копия inode создаётся в памяти. Если
она была изменена, то содержимое дискового и "виртуального" inode
синхронизируется. Старое содержимое inode на диске заменятеся теми
данными, которые находятся в памяти. VFS склонна рассматривать копии
inode в памяти как свои, а не как inode какой-либо другой системы.
Благодаря этому VFS работает с разными ФС так, как если бы они вовсе
не были разными
d_parent - как не сложно догадаться, содержит указатель на
родительский dentry (каталог).
d_name - "истинное" имя нашего dentry (имя файла/каталога на диске).
Это поле имеет тип struct qstr - структура, описывающая простую
текстовую строку. Однако эта структура содержит кое-какие
дополнительные данные о строке: длина (len) и хэш-код (hash). struct
qstr имеет следующий вид:
struct qstr {
const unsigned char * name;
unsigned int len;
unsigned int hash;
};
В общем, с этим, думаю, всё ясно :-)
dcache инициализируется в функции vfs_caches_init() с помощью
dcache_init() [обе находятся в fs/dcache.c]. vfs_caches_init()
вызывается из функции start_kernel() [init/main.c]. Как видно из кода,
находящегося в fs/dcache.c, переменная dentry_cache содержит этот
самый кэш (область памяти, в которой хранятся записи dentry). С
помощью d_alloc() происходит создание новой записи типа dentry в кэше.
dentry_cache объявлена как static, т.е. доступ к ней можно получить
только в пределах кода в файле dcache.c. Соответвенно весь код,
работающий с кэшем, находится именно здесь. Далее я попытаюсь описать
работу с dentry-кэшем более подробно.
d_sb - Указатель на структуру-описатель суперблока. Суперблок содержит
информацию о смонтированной ФС. Формат struct super_block
[include/linux/fs.h]:
struct super_block {
struct list_head s_list; /* Keep this first */
kdev_t s_dev;
unsigned long s_blocksize;
unsigned char s_blocksize_bits;
unsigned char s_dirt;
unsigned long long s_maxbytes; /* Max file size */
struct file_system_type *s_type;
struct super_operations *s_op;
struct dquot_operations *dq_op;
struct quotactl_ops *s_qcop;
unsigned long s_flags;
unsigned long s_magic;
struct dentry *s_root;
struct rw_semaphore s_umount;
struct semaphore s_lock;
int s_count;
atomic_t s_active;
struct list_head s_dirty; /* dirty inodes */
struct list_head s_locked_inodes;/* inodes being synced */
struct list_head s_files;
struct block_device *s_bdev;
struct list_head s_instances;
struct quota_info s_dquot; /* Diskquota specific options */
union {
struct minix_sb_info minix_sb;
......................................................
void *generic_sbp;
} u;
/*
* The next field is for VFS *only*. No filesystems have any business
* even looking at it. You had been warned.
*/
struct semaphore s_vfs_rename_sem; /* Kludge */
/* The next field is used by knfsd when converting a (inode number based)
* file handle into a dentry. As it builds a path in the dcache tree from
* the bottom up, there may for a time be a subpath of dentrys which is not
* connected to the main tree. This semaphore ensure that there is only ever
* one such free path per filesystem. Note that unconnected files (or other
* non-directories) are allowed, but not unconnected diretories.
*/
struct semaphore s_nfsd_free_path_sem;
};
Значение некоторых полей этой структуры вполне ясно: s_dev -
устройтство, на котором находится ФС и, соответсвенно суперблок,
описывающий её; s_blocksize - размер блока на носителе; s_maxbytes -
максимально возможный размер файла, допустимый в рамках данной ФС;
*s_type - указатель на структуру описателя ФС, s_type содержит метод
read_super, с помощью его происходит чтение суперблока ФС (далее о
структуре file_system_type и её роли в VFS будет расказано подробнее);
*s_op - содержит указатели на методы работы с суперблоком и образом
inode в памяти (захватить память, удалить выделенный под него блок
памяти и проч); *dq_op и *s_qcop - используются для работы с дисковыми
квотами. Здесь (во всяком случае сейчас) я квоты рассматривать не
буду, поэтому их описание опущу. *s_root - указывает на dentry для
корневого каталога на диске с данной ФС. struct rw_semaphore s_umount
и struct semaphore s_lock обеспечивают блокировку данных. Это
необходимо для того, чтобы обеспечивать корректную работу разных
участков кода с этим кодом (например чтобы избежать некоторых
коллизий, которые могут возникнуть при работе в мултизадачной среде.
Если ядро производит какие-либо операции на конкретной ФС, то она (ФС)
не может быть размонтирована до тех пор, пока ядро не завершит работу
с ФС). То же касается и поля s_locked_inodes с той лишь разницей, что
это список (struct list_head) заблокированных узлов (inode). s_files -
список файлов, принадлежащих данному суперблоку. Эта переменная может
быть использована, например, в роцессе размонтирования ФС. Если в
списке есть по крайней мере один файл, открытый для записи, ядро не
размонтирует ФС. s_dirty - список узлов (inode), которые в ближайшее
время должны быть записаны на диск (синхронизированы). s_dirt: если
этот флаг установлен (не 0), значит содержимое суперблока на диске и
копия суперблока в памяти должны быть приведены в соответствие
(синхронизированы). Значение этого флага используется, например, в
функции sync_supers() [fs/super.c]. s_flags - флаги специфичные для
данной ФС. s_umount - определяет (по обстоятельствам) можно ли
размонтирвать ФС.
В целом, как видно из кода ядра, а в особенности из кода функций, в
ядре Linux очень активно используюся переменные типа semaphore,
rw_semaphore и atomic_t. Дело в том, что ядро должно обеспечивать
стабильную работу не только на UniProcessor-системах, но и на SMP
(Symmetrical MultiProcessor - симметричные мултипроцессорные) машинах.
В частности если вы обратите внимание на файл include/asm/atomic.h, то
увидите, что все функции используют в ассемблерном коде так называемый
LOCK_PREFIX (иногда просто LOCK). Если при конфигурировании ядра вы
указали, что ядро должно быть использовано на SMP-системе, то значение
LOCK_PREFIX определяется как lock (на Intel машинах это ассемблерная
инструкция, предписывающая захват системной шины одним процессором).
Таким образом, грубо говоря, код, выполняемый на одном физическом
процессоре не сможет изменить или повлиять на данные ядра, код
которого выполняется на другом физическом CPU. Все функции в файле
include/asm/atomic.h объявлены как inline, поэтому код функций
прилагается сразу Wink semaphore и rw_semaphore также необходимы для
блокировки критичных данных, однако их ни в коем случае не следует
путать с семафорами, которые используются в IPC (InterProcess
Connectivity) на Linux-системах.
Итак, это было довольно таки отрывочное описание наиболее важных
структур данных, используемых ядром Linux при работе с ФС. За кадром
остались не менее важные структуры, такие как inode, file, file_lock.
Но до них мы доберёмся чуть-чуть позже. В следующей статье я попытаюсь
описать принципиальное строение Unix-ФС, ведь все "родные" для Linux
ФС строятся на этих принципах. Здесь то мы и рассмотрим inode и
некоторые другие явления из мира Unix более подробно...
С уважением, Afi.
Общие принципы построения ФС в Unix
(бусидо - путь самурая или как это делают в Unix :-) )
Прошу простить мне такие огромные разбежки в написании статей. К
сожалению, по обстоятельствам от меня не зависящим, я вынужден был
существенно сбавить темпы моих исследований. Сессия, до этого ещё и
подготовка к ней, отнимает невероятно много времени. Надеюсь, меня не
будут забрасывать помидорами и тухлыми яйцами и признаюсь честно,
специальность, по которой я учусь в институте не имеет никакого
отношения ни к Linux, ни к ИТ, ни к технике вообще. Именно поэтому мне
так трудно сочетать моё самое большое увлечение - изучение Unix/Linux
и сессию в институте... По тем же причинам, эта статья получилась не
такой фундаментальной как мне хотелось бы. Надеюсь, в будущем мне
удастся исправить это досадное упущение...
В первой части статьи расказывалось о некоторых структурах, с помощью
которых ядро работает с ФС. Там, в частности, фигурировали такие
понятия, как inode, superblock, и пр. Моей ошибкой, возможно, было то,
что я начал не совсем оттуда, откуда стоило бы. Возможно, конечно,
многие и так знают кое-что о строении Unix'овых ФС. Тем не менее
предлагаю ещё раз пройтись на этот счёт Wink . Учитывая то
обстоятельство, что в основе всех Unix-файловых систем лежат одни и те
же логические понятия, почти всё, о чём пойдёт речь здесь, в равной
степени может быть применено и к ext2/3, и к ReiserFS, и к UFS2 (быть
может кто-то, прочитав всё это, напишет и о UFS2 на русском - во
всяком случае было бы неплохо... Wink )
Файлы
Итак, поехали! Для начала: каждый файл в Unix описывается с помощью
уникального inode (их, кстати, ещё называют индексами, далее я тоже
буду называть inode индексом, лень знаете ли постоянно переключать
раскладки :-) ) В индексе содержится информация определённого рода
(права доступа, размер, имя владельца, и т.д.). Как было сказано выше,
индексы живут на диске, но при работе с ними ядро создаёт их копии в
памяти. Если быть более точным, то индекс имеет следующие поля:
имя (идентификатор) владельца файла. Unix - многопользовательская
система, поэтому такая информация должна быть представлена в любом
индексе. Для тех ФС, где просто напросто нет таких понятий, как
владелец файла, права доступа и проч. (классический пример такой
примитивной ФС - FAT'ы всех мастей), ядро по умолчания присваивает
право на вледение файлом тому пользователю, который подмонтировал ФС.
тип файла. Индекс может описывать обычный файл (так называемые regular
files), каталог, специальный файл устройства, канал (pipe) или,
например, сокет домена Unix (это те, которые создаются приблизительно
так: socket (AF_UNIX, SOCK_STREAM, TCP) см. man socket, man protocols,
man getprotoent).
права доступа для владельца, группы и других пользователей, которые не
входят в группу владельца. Обычно они представляются ввиде триплета
цифр в восьмеричной системе счисления, или в более удобоваримой форме,
напримерв листинге ls с помощью букв "r", "w" и "x" (rwxr-x---, т.е.
владелец имеет право на запись чтение и выполнение файла, остальные
участники группы только на исполнение и на чтение, все прочие
пользователи полностью бесправны в отношении данного файла). Занятный
нюанс - каталоги, которые описываются таким же образом, конечно же не
могут быть "выполнены". Для них право на выполнение интерпретируется
как право на поиск файла в каталоге. Все файлы с установленным битом
"х" могут быть исполнены, в системе Unix файл вовсе не обязан иметь
расширение *.exe, *.com, или *.bat, чтобы считаться исполняемым. На
мой взгляд, в этом есть своя прелесть Wink Эксперимента ради можете
попробовать установить бит "х" для обычного текстового, не
скриптового, файла - не бойтесь, система (как это могло бы быть в
MS-DOS, например) не грохнется из-за недопустимой инструкции.
Загрузчик исполняемых образов в ядре Linux в состоянии определить, что
файл на самом деле не может быть выполнен.
дата и время создания файла, дата и время последнего изменения, дата и
время последнего обращения к файлу, дата и время последнего изменения
индекса.
число ссылок на файл. Если оно в какой-то момент времени становится
равным нулю, это значит, что на данный файл больше никто и ничто не
ссылается, значит он больше никому не нужен и его можно удалить Sad .
В Unix файл на самом деле может иметь несколько имён или говоря иначе,
ссылок (link). Изначально на любой файл при его создании есть по
крайней мере одна ссылка - это его имя (ср. этимологию названия вызова
unlink() - удалить файл).
список блоков на диске. В блоках, перечисленных в этом списке,
хранится реальная информация файла - данные (текст, бинарный код или
что-нить ещё).
размер файла. Обычно, данные адресуются с помощью смещения в байтах
относительно начала файла, со смещения, равного 0. Т. о. размер файла
в байтах на единицу больше максимального смещения.
Индекс не содержит имени файла. Одно занятное замечание относительно
поля #4: дело в том, что изменение индекса файла ещё не означает, что
были внесены изменения в сам файл. Именно поэтому дата изменения
индекса и файла идут по отдельности. Изменение файла происходит тогда,
когда в файл были добавлены новые данные, либо какой то их фрагмент
был изменён и проч. Изменение индекса происходит тогда, когда Вы,
например переименовываете файл, меняете права доступа, хотя само по
себе содержимое файла при этом конечно же не меняется. Т.о., дата
изменения индекса вовсе не означает, что содержимое файла было
изменено тогда же, когда и индекс. Но когда Вы всё же изменяете
содержимое файла, то автоматически корректируется и индекс.
Заметьте, в структурах ядра для работы с индексами есть поля,
ответственные за блокировки, но в индексе на диске этих полей конечно
нет, ведь нет никакого смысла "блокировать" индекс, находящийся на
диске. Это в некотором роде runtime-данные, используемые только внутри
ядра и не имеющие никакого отношения к тому, что реально хранится в
индексе на диске.
Копия индекса, находящаяся в памяти, содержит и дополнительные поля,
которых нет в дисковом индексе:
состояние блокировки.
логический номер устройства, содержащего файловую систему, с которой
мы работаем в данный момент.
номер индекса. Индексы на диске хранятся в линейном массиве, ядро
идентифицирует номер дискового индекса по его местоположению в
массиве. В дисковом индексе это поле не нужно.
ссылки на другие индексы, находящиеся в памяти. Ядро связывает индексы
в хэш-очереди. Кроме того, ядро ведёт список свободных индексов.
счётчик ссылок, который показывает, сколько существует активных
экземпляров данного файла (читай, сколько экземпляров файла открыто).
Для ядра Linux структура, описывающая индекс, выглядит так:
Сама по себе эта структура описывает некий абстрактный индекс. Этот
индекс имеет удобное для VFS представление. Реальные индексы
описываются в объединии "u". Если Вы напишите свою файловую систему, и
захотите встроить её поддержку в ядро Linux, то не забудьте поместить
описатель индекса для своей ФС в это объединение Wink .
С уважением, Akira (former Afi).
1313 Прочтений • [VFS - файловая подсистема ядра Linux (linux fs kernel inode)] [08.05.2012] [Комментариев: 0]