Для поддержки различных файловых систем Linux предоставляет специальный интерфейс уровня ядра, который называется VFS (Virtual Filesystem Switch). Он подобен интерфейсу vnode/vfs, имеющемуся в производных от SVR4 (изначально пришедшему из BSD и реализаций Sun)
Реализация inode cache для Linux находится в единственном файле
fs/inode.c
, длиной в 977 строк (Следует понимать,
что размер файла может колебаться от версии к версии, так например
в ядре 2.4.18, длина этого файла составляет 1323 строки
прим. перев.). Самое интересное, что за
последние 5 - 7 лет этот файл претерпел незначительные изменения, в
нем до сих пор можно найти участки кода, дошедшие до наших дней с
версии, скажем, 1.3.42
Inode cache в Linux представляет из себя:
inode_hashtable
, в котором
каждый inode хешируется по значению указателя на суперблок и
32-битному номеру inode. При отсутсвии суперблока
(inode->i_sb == NULL
), вместо хеш-массива inode
добавляется к двусвязному списку anon_hash_chain
.
Примером таких анонимных inodes могут служить сокеты, созданные
вызовом функции net/socket.c:sock_alloc()
, которая
вызывает fs/inode.c:get_empty_inode()
.inode_in_use
, который содержит
допустимые inodes (i_count>0
и
i_nlink>0
). Inodes вновь созданные вызовом
функций get_empty_inode()
и
get_new_inode()
добавляются в список
inode_in_use
inode_unused
, который содержит
допустимые inode с i_count = 0
.sb->s_dirty
) ,
который содержит inodes с i_count>0
,
i_nlink>0
и i_state & I_DIRTY
.
Когда inode помечается как "грязный" (здесь и далее
под термином "грязный" подразумевается
"измененный" прим. перев.), он
добавляется к списку sb->s_dirty
при условии, что
он (inode) хеширован. Поддержка такого списка позволяет уменьшить
накладные расходы на синхронизацию.inode_cachep
. Объекты inode могут создаваться и
освобождаться, вставляться и изыматься из SLAB cacheЧерез поле inode->i_list
с inode вставляется в
список определенного типа, через поле inode->i_hash
- в хеш-массив. Каждый inode может входить в хеш-массив и в один и
только в один список типа (in_use, unused или dirty).
Списки эти защищаются блокировкой (spinlock)
inode_lock
.
Подсистема inode cache инициализируется при вызове функции
inode_init()
из
init/main.c:start_kernel()
. Эта функция имеет один
входной параметр - число страниц физической памяти в системе. В
соответсвии с этим параметром inode cache конфигуририруется под
существующий объем памяти, т.е. при большем объеме памяти создается
больший хеш-массив.
Единственная информация о inode cache, доступная пользователю -
это количество неиспользованных inodes из
inodes_stat.nr_unused
. Получить ее можно из файлов
/proc/sys/fs/inode-nr
и
/proc/sys/fs/inode-state
.
Можно исследовать один из списков с помощью gdb:
(gdb) printf "%d\n", (unsigned long)(&((struct inode *)0)->i_list) 8 (gdb) p inode_unused $34 = 0xdfa992a8 (gdb) p (struct list_head)inode_unused $35 = {next = 0xdfa992a8, prev = 0xdfcdd5a8} (gdb) p ((struct list_head)inode_unused).prev $36 = (struct list_head *) 0xdfcdd5a8 (gdb) p (((struct list_head)inode_unused).prev)->prev $37 = (struct list_head *) 0xdfb5a2e8 (gdb) set $i = (struct inode *)0xdfb5a2e0 (gdb) p $i->i_ino $38 = 0x3bec7 (gdb) p $i->i_count $39 = {counter = 0x0}
Заметьте, что от адреса 0xdfb5a2e8 отнимается число 8, чтобы
получить адрес struct inode
(0xdfb5a2e0), согласно
определению макроса list_entry()
из
include/linux/list.h
.
Для более точного понимания принципа работы inode cache, давайте рассмотрим цикл жизни обычного файла в файловой системе ext2 с момента его открытия и до закрытия.
fd = open("file", O_RDONLY); close(fd);
Системный вызов open(2) реализован в виде
функции fs/open.c:sys_open
, но основную работу
выполняет функция fs/open.c:filp_open()
, которая
разбита на две части:
open_namei()
: заполняет структуру nameidata,
содержащую структуры dentry и vfsmount.dentry_open()
: с учетом dentry и vfsmount,
размещает новую struct file
и связывает их между
собой; вызывает метод f_op->open()
который был
установлен в inode->i_fop
при чтении inode в
open_namei()
(поставляет inode через
dentry->d_inode
).Функция open_namei()
взаимодействует с dentry cache
через path_walk()
, которая, в свою очередь, вызывает
real_lookup()
, откуда вызывается метод
inode_operations->lookup()
. Назначение последнего -
найти вход в родительский каталог и получить соответствующий inode
вызовом iget(sb, ino)
При считывании inode, значение
dentry присваивается посредством d_add(dentry, inode)
.
Следует отметить, что для UNIX-подобных файловых систем,
поддерживающих концепцию дискового номера inode, в ходе выполнения
метода lookup()
. производится преобразование порядка
следования байт числа (endianness) в формат CPU, например, если
номер inode хранится в 32-битном формате с обратным порядком
следования байт (little-endian), то выполняются следующие
действия:
(Считаю своим долгом подробнее остановиться на понятии
endianness. Под этим термином понимается порядок
хранения байт в машинном слове (или двойном слове). Порядок может
быть "прямым" (т.е. 32-битное число хранится так
0x12345678) и тогда говорят "big
endianness" (на отечественном жаргоне это звучит как
"большой конец", т.е. младший байт лежит в старшем
адресе) или "обратным" (т.е. 32-битное число хранится так
0x78563412 - такой порядок следования байт принят в архитектуре
Intel x86) и тогда говорят "little
endianness" (на отечественном жаргоне это звучит как
"маленький конец", т.е. младший байт лежит в младшем
адресе). прим. перев.)
unsigned long ino = le32_to_cpu(de->inode); inode = iget(sb, ino); d_add(dentry, inode);
Таким образом, при открытии файла вызывается iget(sb,
ino)
, которая, фактически, называется iget4(sb, ino,
NULL, NULL)
, эта функция:
inode_lock
.
Если inode найден, то увеличивается его счетчик ссылок
(i_count
); если счетчик перед инкрементом был равен
нулю и inode не "грязный", то он удаляется из любого
списка (inode->i_list
), в котором он находится
(это конечно же список inode_unused
) и вставляется в
список inode_in_use
; в завершение, уменьшается
счетчик inodes_stat.nr_unused
.iget4()
гарантирует возврат
незаблокированного inode.get_new_inode()
, которой передается
указатель на место в хеш-таблице, куда должен быть вставлен
inode.get_new_inode()
распределяет память под новый
inode в SLAB кэше inode_cachep
, но эта операция
может устанавливать блокировку (в случае
GFP_KERNEL
), поэтому освобождается блокировка
inode_lock
. Поскольку блокировка была сброшена то
производится повторный поиск в хеш-таблице, и если на этот раз
inode найден, то он возвращается в качестве результата (при этом
счетчик ссылок увеличивается вызовом __iget
), а
новый, только что распределенный inode уничтожается. Если же
inode не найден в хеш-таблице, то вновь созданный inode
инициализируется необходимыми значениями и вызывается метод
sb->s_op->read_inode()
, чтобы инициализировать
остальную часть inode Во время чтения метдом
s_op->read_inode()
, inode блокируется
(i_state = I_LOCK
), после возврата из
s_op->read_inode()
блокировка снимается и
активируются все ожидающие его процессы.Теперь рассмотрим действия, производимые при закрытии файлового
дескриптора. Системный вызов close(2) реализуется
функцией fs/open.c:sys_close()
, которая вызывает
do_close(fd, 1)
. Функция do_close(fd, 1)
записывает NULL на место дескриптора файла в таблице дескрипторов
процесса и вызывает функцию filp_close()
, которая и
выполняет большую часть действий. Вызывает интерес функция
fput()
, которая проверяет была ли это последняя ссылка
на файл и если да, то через fs/file_table.c:_fput()
вызывается __fput()
, которая взаимодействует с dcache
(и таким образом с inode cache - не забывайте, что dcache является
"хозяином" inode cache!). Функция
fs/dcache.c:dput()
вызывает
dentry_iput()
, которая приводит нас обратно в inode
cache через iput(inode)
. Разберем
fs/inode.c:iput(inode)
подробнее:
sb->s_op->put_inode()
без захвата блокировки (так что он может быть блокирован).i_count
. Если это была не последняя ссылка, то
просто проверяется - поместится ли количество ссылок в 32-битное
поле и если нет - то выводится предупреждение. Отмечу, что
поскольку вызов производится под блокировкой
inode_lock
, то для вывода предупреждения
используется функция printk()
, которая никогда не
блокируется, поэтому ее можно вызывать абсолютно из любого
контекста исполнения (даже из обработчика прерываний!).Дополнительные действия, выполняемые по закрытию в случае
последней ссылки функцией iput()
, достаточно сложны,
поэтому они рассматриваются отдельно:
i_nlink == 0
(например файл был удален,
пока мы держали его открытым), то inode удаляется из хеш-таблицы
и из своего списка. Если имеются какие-либо страницы в кеше
страниц, связанные с данным inode, то они удаляются посредством
truncate_all_inode_pages(&inode->i_data)
.
Затем, если определен, то вызывается специфичный для файловой
системы метод s_op->delete_inode()
, который
обычно удаляет дисковую копию inode. В случае отсутствия
зарегистрированного метода s_op->delete_inode()
(например ramfs), то вызывается clear_inode(inode)
,
откуда производится вызов s_op->clear_inode()
,
если этот метод зарегистрирован и inode соответствует блочному
устройству. Счетчик ссылок на это устройство уменьшается вызовом
bdput(inode->i_bdev)
.i_nlink != 0
, то проверяется - есть ли
другие inode с тем же самым хеш-ключом (in the same hash bucket)
и если нет, и inode не "грязный", то он удаляется из
своего списка типа, вставляется в список
inode_unused
, увеличивая
inodes_stat.nr_unused
. Если имеются inodes с тем же
самым хеш-ключом, то inode удаляется из списка типа и добавляется
к списку inode_unused
. Если это анонимный inode
(NetApp .snapshot) то он удаляется из списка типа и
очищается/удаляется полностью.Ядро Linux предоставляет механизм, минимизирующий усилия разработчиков по написанию новых файловых систем. Исторически сложилось так, что:
Рассмотрим порядок добавления новой файловой системы в Linux.
Код, реализующий файловую систему, может быть выполнен либо в виде
динамически подгружаемого модуля, либо может быть статически связан
с ядром. Все, что требуется сделать - это заполнить struct
file_system_type
и зарегистрировать файловую систему в VFS с
помощью функции register_filesystem()
, как показано
ниже (пример взят из fs/bfs/inode.c
):
#include <linux/module.h> #include <linux/init.h> static struct super_block *bfs_read_super(struct super_block *, void *, int); static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super); static int __init init_bfs_fs(void) { return register_filesystem(&bfs_fs_type); } static void __exit exit_bfs_fs(void) { unregister_filesystem(&bfs_fs_type); } module_init(init_bfs_fs) module_exit(exit_bfs_fs)
Макросы module_init()/module_exit()
, в случае,
когда BFS компилируется как модуль, преобразуют функции
init_bfs_fs()
и exit_bfs_fs()
в
init_module()
и cleanup_module()
соответственно. Если BFS компилируется статически, то код
exit_bfs_fs()
исчезает, поскольку необходимость в нем
отпадает.
Структура struct file_system_type
объявлена в
include/linux/fs.h
:
struct file_system_type { const char *name; int fs_flags; struct super_block *(*read_super) (struct super_block *, void *, int); struct module *owner; struct vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */ struct file_system_type * next; };
Поля структуры:
/proc/filesystems
и используется как ключ для поиска файловой системы по имени; это
же имя используется как аргумент в вызове
mount(2) и должно быть уникальным. Для модулей
имя указывает на адресное пространство модуля так, что в случае,
когда модуль уже выгружен, но файловая система еще остается
зарегистрированной, то команда cat
/proc/filesystems может вызвать oops.FS_REQUIRES_DEV
для файловых
систем, которые могут быть смонтированы только с блочных
устройств, FS_SINGLE
для файловых систем, имеющих
только один суперблок, FS_NOMOUNT
для файловых
систем которые не могут быть смонтированы из пользовательского
пространства системным вызовом mount(2), однако
такие файловые системы могут быть смонтированы ядром через вызов
kern_mount()
, например pipefs.FS_SINGLE
попытка монтирования будет приводить к
Oops в get_sb_single()
, при попытке получить ссылку
fs_type->kern_mnt->mnt_sb
(в то время как
fs_type->kern_mnt = NULL
).THIS_MODULE
делает это автоматически.FS_SINGLE
. Устанавливается
kern_mount()
(TODO: вызов kern_mount()
должен отвергать монтирование файловых систем если флаг
FS_SINGLE
не установлен).file_systems
(см. fs/super.c
). Список
защищается "read-write" блокировкой
file_systems_lock
и модифицируется функциями
register/unregister_filesystem()
.Функция read_super()
заполняет поля суперблока,
выделяет память под корневой inode и инициализирует специфичную
информацию, связанную с монтируемым экземпляром файловой системы.
Как правило read_super()
:
sb->s_dev
, используя функцию
bread()
. Если предполагается чтение дополнительных
блоков с метаданными, то имеет смысл воспользоваться функцией
breada()
, чтобы прочитать дополнительные блоки
асинхронно.sb->s_op
на
структуру struct super_block_operations
. Эта
структура содержит указатели на функции, специфичные для файловой
системы, такие как "read inode", "delete
inode" и пр.d_alloc_root()
.sb->s_dirt
записывается 1 и
буфер, содержащий суперблок, помечается как "грязный"
(TODO: зачем это делается? Я сделал так в BFS потому, что в MINIX
делается то же самое).В Linux между пользовательским файловым дескриптором и
структурой inode в ядре, существует несколько уровней косвенных
ссылок. Когда процесс открывает файл системным вызовом
open(2), ядро возвращает положительное малое целое
число, которое затем используется в операциях ввода/вывода над
заданным файлом. Это целое число является индексом в массиве
указателей на struct file
. Каждая struct
file
содержит указатель на dentry
file->f_dentry
. Каждая dentry имеет указатель на
inode dentry->d_inode
.
Каждая задача содержит поле tsk->files
которое
указывает на struct files_struct
, определенную в
include/linux/sched.h
:
/* * Структура таблицы открытых файлов */ struct files_struct { atomic_t count; rwlock_t file_lock; int max_fds; int max_fdset; int next_fd; struct file ** fd; /* массив дескрипторов */ fd_set *close_on_exec; fd_set *open_fds; fd_set close_on_exec_init; fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; };
Поле file->count
- это счетчик ссылок,
увеличивается в get_file()
(обычно вызывается из
fget()
) и уменьшается в fput()
и в
put_filp()
.Различие между fput()
и
put_filp()
состоит в том, что fput()
выполняет больший объем работы, необходимый для регулярных файлов,
т.е. освобождение блокировок, освобождение dentry и пр., в то время
как put_filp()
работает только с таблицей файловых
структур, т.е. уменьшает счетчик, удаляет файл из
anon_list
и добавляет его в free_list
,
под блокировкой files_lock
.
Таблица tsk->files
может использоваться
совместно родителем и потомком, если потомок был создан системным
вызовом clone()
с флагом CLONE_FILES
. В
качестве примера можно привести
kernel/fork.c:copy_files()
(вызывается из
do_fork()
), где только лишь увеличивается счетчик
ссылок file->count
. вместо обычного (для
классического fork(2) в UNIX) копирования таблицы
дескрипторов.
При открытии файла в памяти размещается новая файловая
структура, которая устанавливается в слот
current->files->fd[fd]
и взводится бит
fd
в current->files->open_fds
.
Действия эти выполняются под защитой от записи read-write
блокировкой current->files->file_lock
. При
закрытии дескриптора сбрасывается бит fd
в
current->files->open_fds
, а поле
current->files->next_fd
устанавливается равным
fd
на случай поиска первого неиспользуемого
дескриптора при следующем открытии файла.
Структура file объявлена в include/linux/fs.h
:
struct fown_struct { int pid; /* pid или -pgrp процесса, которому должен передаваться SIGIO */ uid_t uid, euid; /* uid/euid процесса-владельца */ int signum; /* posix.1b rt signal to be delivered on IO */ }; struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error; unsigned long f_version; /* требуется для драйвера tty, а возможно и для других */ void *private_data; };
Остановимся подробнее на полях struct file
:
sb->s_files
- список всех открытых файлов в
данной файловой системе, если соответствующий inode не является
анонимным, то dentry_open()
(вызываемая из
filp_open()
) вставляет файл в этот список;fs/file_table.c:free_list
- список
неиспользуемых структур;fs/file_table.c:anon_list
- в этот список
включаются структуры, создаваемые в
get_empty_filp()
.files_lock
.open_namei()
(или точнее в
path_walk()
), но в действительности поле
file->f_dentry
заполняется в
dentry_open()
.vfsmount
файловой системы, содержащей файл.
Заполняется функцией dentry_open()
и является частью
nameidata, поиск которой производится в open_namei()
(или точнее в path_init()
).file_operations
, который содержит адреса методов для
работы с файлом. Копируется из inode->i_fop
методом s_op->read_inode()
, вызываемым в процессе
поиска nameidata. Более подробно на списке
file_operations
мы остановимся ниже в этом
разделе.get_file/put_filp/fput
.O_XXX
системного
вызова open(2), копируются функцией
dentry_open()
(с небольшими изменениями в
filp_open()
), при чем флаги O_CREAT
,
O_EXCL
, O_NOCTTY
, O_TRUNC
сбрасываются, поскольку они не могут модифицироваться по
параметру F_SETFL
(или F_GETFL
) в
системном вызове fcntl(2).dentry_open()
. Флаги режимов
доступа для чтения и записи выведены в отдельные биты, чтобы
облегчить контроль состояния: (f_mode &
FMODE_WRITE)
и (f_mode &
FMODE_READ)
.long long
,
т.е. 64 бита.SIGIO
(см. fs/fcntl.c:kill_fasync()
).get_empty_filp()
. Если файл является сокетом, то эти
поля могут быть использованы в ipv4 netfilter.fs/nfs/file.c
и проверяется в
mm/filemap.c:generic_file_write()
.event
) всякий раз, когда изменяется
f_pos
.file->f_dentry->d_inode->i_rdev
.Перейдем к рассмотрению списка методов управления файлом
file_operations
. Позволю себе напомнить, что он
копируется из inode->i_fop
методом
s_op->read_inode()
. Структура (список методов)
объявлена в include/linux/fs.h
:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); };
fs/read_write.c:default_llseek()
. (TODO:
Принудительно устанавливать это поле в NULL, тем самым
сэкономится лишний if()
в
llseek()
)read(2)
. Файловые системы могут использовать
mm/filemap.c:generic_file_read()
для обычных файлов
и fs/read_write.c:generic_read_dir()
(которая просто
возвращает -EISDIR
) для каталогов.mm/filemap.c:generic_file_write()
для обычных файлов
и игнорировать его для каталогов.FIBMAP
, FIGETBSZ
, FIONREAD
реализуются на более высоком уровне, поэтому они никогда не
пользуются методом f_op->ioctl()
.dentry_open()
.
Редко используется файловыми системами, например coda пытается
кэшировать файл во время открытия.release()
ниже). Единственная
файловая система, которая вызывает этот метод - это NFS клиент,
которая "выталкивает" все измененные страницы.
Примечательно, что этот метод может завершаться с кодом ошибки,
который передается обратно в пространство пользователя, откуда
делался системный вызов close(2).file->f_count
станет равным нулю. Хотя и
возвращает целое (int) значение, но VFS игнорирует его (см.
code>fs/file_table.c:__fput()).file = fget(fd)
) и
сброса/установки семафора inode->i_sem
. Файловая
система Ext2, на сегодняшний день, игнорирует последний аргумент,
передаваемый методу и выполняет одни и те же действия как для
fsync(2) так и для
fdatasync(2).file->f_flags & FASYNC
.posix_lock_file()
, если метод завершается
успешно, а стандартный POSIX код блокировки терпит неудачу, то
блокировка не будет снята на зависимом от типа файловой системы
уровне..В Linux, информация о смонтированных файловых системах хранится
в двух различных структурах - super_block
и
vfsmount
. Сделано это для того, чтобы имелась
возможность смонтировать одну и ту же файловую систему к нескольким
точкам монтирования одновременно, это означает, что одна и та же
структура super_block
может соответствовать нескольким
структурам vfsmount
.
В первую очередь рассмотрим структуру struct
super_block
, объявленную в
include/linux/fs.h
:
struct super_block { struct list_head s_list; /* Хранится первым */ kdev_t s_dev; unsigned long s_blocksize; unsigned char s_blocksize_bits; unsigned char s_lock; unsigned char s_dirt; struct file_system_type *s_type; struct super_operations *s_op; struct dquot_operations *dq_op; unsigned long s_flags; unsigned long s_magic; struct dentry *s_root; wait_queue_head_t s_wait; struct list_head s_dirty; /* "грязные" inodes */ struct list_head s_files; struct block_device *s_bdev; struct list_head s_mounts; /* vfsmount(s) of this one */ struct quota_mount_options s_dquot; /* параметры для Diskquota */ union { struct minix_sb_info minix_sb; struct ext2_sb_info ext2_sb; ..... Информация sb-private, необходимая для всех файловых систем ... void *generic_sbp; } u; /* * Следующее поле предназначено *только* для VFS. * Ни одна файловая система не должна изменять его, * даже если она обращается к этому полю. * Вас предупредили. */ struct semaphore s_vfs_rename_sem; /* Kludge */ /* Следующее поле используется демоном knfsd для преобразования(inode number based) * file handle в dentry. Поскольку путь в дереве dcache строится снизу вверх * то в течение некоторого времени путь является неполным, никак не связанным * с главным деревом. Этот семафор гарантирует существование единственного * такого свободного пути в файловой системе. * Заметьте, что такие "несвязанные" файлы допустимы * но не каталоги. */ struct semaphore s_nfsd_free_path_sem; };
Более подробно о полях структуры super_block
:
FS_REQUIRES_DEV
, это
поле представляет собой копию i_dev
блочного
устройства. Для других файловых систем (называемых анонимными)
представляет собой целое число MKDEV(UNNAMED_MAJOR,
i)
, где i
принадлежит диапазону от 0 до 255
включительно и является порядковым номером первого
неустановленного бита в массиве unnamed_dev_in_use
.
Смотрите
fs/super.c:get_unnamed_dev()/put_unnamed_dev()
.
Неоднократно предлагалось отказаться от использования поля
s_dev
анонимными файловыми системами.lock_super()/unlock_super()
.struct
file_system_type
, соответствующую файловой системе. Метод
файловой системы read_super()
не должен
устанавливать это поле, так как это поле устанавливается VFS в
функции fs/super.c:read_super()
, в случае успешного
вызова метода read_super()
конкретной файловой, и
сбрасывется в NULL в противном случае.super_operations
, которая содержит специфичные для
заданной файловой системы методы, такие как чтение/запись inode и
пр. Корректное заполнение этой структуры - задача метода файловой
системы read_super()
.read_super()
считывает корневой inode с диска и
передает его в d_alloc_root()
, который выделяет
память под dentry и заполняет ее. Некоторые файловые системы
используют иное обозначение корня, нежели "/", поэтому
используется более общая функция d_alloc()
для
образования полного имени, например pipefs использует
"pipe:" для обозначения своего корня.inode->i_state & I_DIRTY
), то этот список
связуется через inode->i_list
.fs/file_table.c:fs_may_remount_ro()
, которая
просматривает список sb->s_files
и отвергает
возможность перемонтирования если имеется хотя бы один файл,
открытый "на запись" (file->f_mode &
FMODE_WRITE
) или ожидающий удаления
(inode->i_nlink == 0
).FS_REQUIRES_DEV
указывает на структуру block_device,
описывающую блочное устройство, с которого смонтирована файловая
система.vfsmount
для каждого смонтированного экземпляра
данного суперблока.Методы управления суперблоком перечисляются в структуре
super_operations
, объявленной в
include/linux/fs.h
:
struct super_operations { void (*read_inode) (struct inode *); void (*write_inode) (struct inode *, int); void (*put_inode) (struct inode *); void (*delete_inode) (struct inode *); void (*put_super) (struct super_block *); void (*write_super) (struct super_block *); int (*statfs) (struct super_block *, struct statfs *); int (*remount_fs) (struct super_block *, int *, char *); void (*clear_inode) (struct inode *); void (*umount_begin) (struct super_block *); };
fs/inode.c:get_new_inode()
из iget4()
(и следовательно из iget()
). Если файловая система
предполагает вызов iget()
то метод
read_inode()
должен быть реализован, в противном
случае get_new_inode()
будет приводить к
"впадению в панику" (panic). Во время чтения inode
заблокирован (inode->i_state = I_LOCK
). Когда
функция возвращает управление, все процессы из очереди
inode->i_wait
пробуждаются. В задачу метода
read_inode()
входит обнаружение дискового блока,
который содержит заданный inode и с помощью функйии буферного
кэша bread()
прочитать его и инициализировать
различные поля в структуре inode, например
inode->i_op
и inode->i_fop
, чтобы
уровень VFS "знал" какие операции над inode и
соответствующим ему файлом, считаются допустимыми. Имеются
файловые системы, в которых метод read_inode()
не
реализован - это ramfs и pipefs. Так ramfs имеет свою собственную
функцию генерации inode (ramfs_get_inode()
).read_inode()
отыскивает нужный дисковый
блок и вызывает функцию буферного кэша
mark_buffer_dirty(bh)
. Этот метод вызывается для
"грязных" inode (которые были помечены вызовом
mark_inode_dirty()
) при возникновении необходимости
синхронизации как отдельно взятого inode, так и файловой системы
в целом.inode->i_count
и inode->i_nlink
достигают нулевого значения. Файловая система удаляет дисковую
копию inode и вызывает clear_inode()
для VFS inode,
чтобы "прекратить его существование окончательно".brelse()
блока, содержащего суперблок, и
kfree()
для освобождения всех ранее размещенных
блоков, inodes и т.п.sb-private
) и вызвать
mark_buffer_dirty(bh)
. А так же должен сбросить флаг
sb->s_dirt
flag.struct statfs
, передаваемый в качестве аргумента,
является указателем пространства ядра а не пользовательского
пространства, поэтому не следует выполнять каких либо операций
ввода-вывода в пользовательском пространстве. В случае отсутствия
этого метода вызов statfs(2)
будет возвращвть код
ошибки ENOSYS
.clear_inode()
уровня VFS. Файловая система должна
освободить приватную информацию в структуре inode (присоединенную
через поле generic_ip
).Теперь рассмотрим последовательность действий, выполняемых при
монтировании дисковой (FS_REQUIRES_DEV
) файловой
системы. Реализация системного вызова mount(2)
находится в fs/super.c:sys_mount()
, которая по сути
является лишь оберткой, которая передает опции монтирования, тип
файловой системы и название устройства в функцию
do_mount()
.
do_mount()
, вызываемой из
get_fs_type()
, и один раз в
get_sb_dev()
, вызываемой из
get_filesystem()
, если read_super()
выполнилась успешно. Первое увеличение предотвращает выгрузку
модуля пока выполняется метод read_super()
и второе
увеличение указывает на то, что модуль используется
смонтированным экземпляром. Вполне понятно, что перед завершением
do_mount()
уменьшает счетчик ссылок на единицу,
таким образом суммарное приращение счетчика составляет единицу
после каждого монтирования.fs_type->fs_flags &
FS_REQUIRES_DEV
истинно, поэтому далее инициализируется
суперблок, вызовом get_sb_bdev()
, который получает
ссылку на блочное устройство и вызывом метода
read_super()
заполняет поля суперблока. Если все
прошло гладко, то структура super_block
считается
инициализированной и мы получаем дополнительно ссылку на модуль
файловой системы и ссылку на основное блочное устройство.vfsmount
и
"прицепляется" к списку sb->s_mounts
и
к глобальному списку vfsmntlist
. С помощью поля
mnt_instances
структуры vfsmount
можно
найти все смонтированные экземпляры файловой системы для одного и
того же суперблока. С помощью списка mnt_list
можно
отыскать все смонтированные экземпляры файловых систем для всех
суперблоков в системе. Поле mnt_sb
указывает на
данный суперблок, а mnt_root
получает новую ссылку
на sb->s_root
dentry.В качестве простого примера файловой системы в Linux рассмотрим
pipefs, которая не требует наличия блочного устройства для своего
монтирования. Реализация pipefs находится в
fs/pipe.c
.
static DECLARE_FSTYPE(pipe_fs_type, "pipefs", pipefs_read_super, FS_NOMOUNT|FS_SINGLE); static int __init init_pipe_fs(void) { int err = register_filesystem(&pipe_fs_type); if (!err) { pipe_mnt = kern_mount(&pipe_fs_type); err = PTR_ERR(pipe_mnt); if (!IS_ERR(pipe_mnt)) err = 0; } return err; } static void __exit exit_pipe_fs(void) { unregister_filesystem(&pipe_fs_type); kern_umount(pipe_mnt); } module_init(init_pipe_fs) module_exit(exit_pipe_fs)
Файловая система принадлежит к типу
FS_NOMOUNT|FS_SINGLE
это означает, что она не может
быть смонтирована из пространства пользователя и в системе может
иметься только один суперблок этой файловой системы. Флаг
FS_SINGLE
означает также что она должна монтироваться
через kern_mount()
после того как будет выполнена
регистрация вызовом register_filesystem()
, что
собственно и выполняется функцией init_pipe_fs()
.
Единственная неприятность - если kern_mount()
завершится с ошибкой (например когда kmalloc()
,
вызываемый из add_vfsmnt()
не сможет распределить
память), то файловая система окажется зарегистрированной но модуль
не будет инициализирован. Тогда команда cat
/proc/filesystems повлечет за собой Oops. (передал Линусу
"заплату", хотя это фактически не является ошибкой,
поскольку на сегодняшний день pipefs не может быть скомпилирована
как модуль, но в будущем вполне может быть добавлена взможность
вынесения pipefs в модуль).
В результате register_filesystem()
,
pipe_fs_type
добавляется к списку
file_systems
, который содержится в
/proc/filesystems
. Прочитав его, вы обнаружите
"pipefs" с флагом "nodev", указывающим на то,
что флаг FS_REQUIRES_DEV
не был установлен. Следовало
бы расширить формат файла /proc/filesystems
с тем,
чтобы включить в него поддержку всех новых FS_
флагов
(и я написал такую "заплату"), но это невозможно,
поскольку такое изменение может отрицательно сказаться на
пользовательских приложениях, которые используют этот файл.
Несмотря на то, что интерфейсы ядра изменяются чуть ли не
ежеминутно, тем не менее когда вопрос касается пространства
пользователя, Linux превращается в очень консервативную
операционную систему, которая позволяет использование программ в
течение длительного времени без необходимости их
перекомпиляции.
В результате выполнения kern_mount()
:
unnamed_dev_in_use
; если в этом
массиве не окажется свободного бита, то kern_mount()
вернется с ошибкой EMFILE
.get_empty_super()
создается новая
структура суперблока. Функция get_empty_super()
проходит по списку суперблоков super_block
в поисках
свободного места, т.е. s->s_dev == 0
. Если
такового не обнаружилось, то резервируется память вызовом
kmalloc()
, с приоритетом GFP_USER
. В
get_empty_super()
проверяется превышение максимально
возможного количества суперблоков, так что в случае появления
сбоев, при монтировании pipefs, можно попробовать
подкорректировать /proc/sys/fs/super-max
.pipe_fs_type->read_super()
(т.е. pipefs_read_super()
), который размещает
корневой inode и dentry sb->s_root
, а также
записывает адрес &pipefs_ops
в
sb->s_op
.add_vfsmnt(NULL, sb->s_root,
"none")
, которая размещает в памяти новую
структуру vfsmount
и включает ее в список
vfsmntlist
и sb->s_mounts
.pipe_fs_type->kern_mnt
заносится адрес
новой структуры vfsmount
и он же и возвращается в
качестве результата. Причина, по которой возвращаемое значение
является указателем на vfsmount
состоит в том, что
даже не смотря на флаг FS_SINGLE
, файловая система
может быть смонтирована несколько раз, вот только их
mnt->mnt_sb
будут указывать в одно и то же
место.После того как файловая система зарегистрирована и смонтирована,
с ней можно работать. Точкой входа в файловую систему pipefs
является системный вызов pipe(2), реализованный
платформо-зависимой функцией sys_pipe()
, которая в
свою очередь передает управление платформо-независимой функции
fs/pipe.c:do_pipe()
. Взаимодействие
do_pipe()
с pipefs начинается с размещения нового
inode вызовом get_pipe_inode()
. В поле
inode->i_sb
этого inode заносится указатель на
суперблок pipe_mnt->mnt_sb
, в список
i_fop
файловых операций заносится
rdwr_pipe_fops
, а число "читателей" и
"писателей" (содержится в inode->i_pipe
)
устанавливается равным 1. Причина, по которой имеется отдельное
поле i_pipe
, вместо хранения этой информации в
приватной области fs-private
, заключается в том, что
каналы (pipes) и FIFO (именованные каналы) совместно используют
один и тот же код, а FIFO могут существовать и в другой файловой
системе, которые используют другие способы доступа в пределах этого
объединения (fs-private
) и могут работать, что
называется "на удачу". Так, в ядре 2.2.x, все перестает
работать, стоит только слегка изменить порядок следования полей в
inode.
Каждый системный вызов pipe(2) увеличивает
счетчик ссылок в структуре pipe_mnt
.
В Linux каналы (pipes) не являются симметричными, т.е. с каждого
конца канал имеет различный набор файловых операций
file->f_op
- read_pipe_fops
и
write_pipe_fops
. При попытке записи со стороны канала,
открытого на чтение, будет возвращена ошибка EBADF
, то
же произойдет и при попытке чтения с конца канала, открытого на
запись.
В качестве примера дисковой файловой системы рассмотрим BFS.
Преамбула модуля BFS в файле fs/bfs/inode.c
:
static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super); static int __init init_bfs_fs(void) { return register_filesystem(&bfs_fs_type); } static void __exit exit_bfs_fs(void) { unregister_filesystem(&bfs_fs_type); } module_init(init_bfs_fs) module_exit(exit_bfs_fs)
Макрокоманда DECLARE_FSTYPE_DEV()
взводит флаг
FS_REQUIRES_DEV
в fs_type->flags
, это
означает, что BFS может быть смонтирована только с реального
блочного устройства.
Функция инициализации модуля регистрирует файловую систему в VFS, а функция завершения работы модуля - дерегистрирует ее (эта функция компилируется только когда поддержка BFS включена в ядро в виде модуля).
После регистрации файловой системы, она становится доступной для
монтирования, в процессе монтирования вызывается метод
fs_type->read_super()
, который выполняет следующие
действия:
set_blocksize(s->s_dev, BFS_BSIZE)
: поскольку
предполагается взаимодействие с уровнем блочного устройства через
буферный кэш, следует выполнить некоторые действия, а именно
указать размер блока и сообщить о нем VFS через поля
s->s_blocksize
и
s->s_blocksize_bits
.bh = bread(dev, 0, BFS_BSIZE)
: читается нулевой
блок с устройства s->s_dev
. Этот блок является
суперблоком файловой системы.BFS_MAGIC
, если все в порядке, то он сохраняется в
поле s->su_sbh
(на самом деле это
s->u.bfs_sb.si_sbh
).kmalloc(GFP_KERNEL)
и все биты в ней сбрасываются в
0, за исключением двух первых, которые указывают на то, что 0-й и
1-й inode никогда не должны распределяться. Inode с номером 2
является корневым, установка соответствующего ему бита
производится несколькими строками ниже, в любом случае файловая
система должна получить корневой inode во время
монтирования!s->s_op
, и уже после этого
можно вызвать iget()
, которая обратится к
s_op->read_inode()
. Она отыщет блок, который
содержит заданный (по inode->i_ino
и
inode->i_dev
) inode и прочитает его. Если при
запросе корневого inode произойдет ошибка, то память, занимаемая
битовой картой inode, будет освобождена, буфер суперблока
возвратится в буферный кэш и в качестве результата будет
возвращен "пустой" указатель - NULL. Если корневой
inode был успешно прочитан, то далее размещается dentry с именем
/
и связывается с этим inode.iput()
- ссылка на них не удерживается дольше, чем
это необходимо.s->s_dirt
(TODO: Для чего? Первоначально я сделал
это потому, что это делалось в minix_read_super()
,
но ни minix ни BFS кажется не изменяют суперблок в
read_super()
).fs/super.c:read_super()
.После успешного завершения работы функции
read_super()
VFS получает ссылку на модуль файловой
системы через вызов get_filesystem(fs_type)
в
fs/super.c:get_sb_bdev()
и ссылку на блочное
устройство.
Рассмотрим, что происходит при выполнении опреаций ввода/вывода
над файловой системой. Мы уже знаем, что inode читается функцией
iget()
и что они освобождаются вызовом
iput()
. Чтение inode приводит, кроме всего прочего, к
установке полей inode->i_op
и
inode->i_fop
; открытие файла вызывает копирование
inode->i_fop
в file->f_op
.
Рассмотрим последовательность действий системного вызова
link(2). Реализация системного вызова находится в
fs/namei.c:sys_link()
:
getname()
, которая
выполняет проверку на наличие ошибок.path_init()/path_walk()
. Результат сохраняется в
структурах old_nd
и nd
old_nd.mnt != nd.mnt
, то возвращается
"cross-device link" EXDEV
- невозможно
установить ссылку между файловыми системами, в Linux это означает
невозможность установить ссылку между смонтированными
экземплярами одной файловой системы (или, особенно, между
различными файловыми системами).nd
создается новый dentry вызовом
lookup_create()
.vfs_link()
,
которая проверяет возможность создания новой ссылки по заданному
пути и вызывает метод dir->i_op->link()
,
который приводит нас в fs/bfs/dir.c:bfs_link()
.bfs_link()
, производится проверка - не
делается ли попытка создать жесткую ссылку на директорию и если
это так, то возвращается код ошибки EPERM
. Это как
стандарт (ext2).bfs_add_entry()
, которая отыскивает неиспользуемый
слот (de->ino == 0
) и если находит, то записывает
пару имя/inode в соответствующий блок и помечает его как
"грязный".inode->i_nlink
,
обновляется inode->i_ctime
и inode помечается как
"грязный" твк же как и inode приписанный новому
dentry.Другие родственные операции над inode, подобные
unlink()/rename()
, выполняются аналогичным образом,
так что не имеет большого смысла рассматривать их в деталях.
Linux поддерживает загрузку пользовательских приложений с диска. Самое интересное, что приложения могут храниться на диске в самых разных форматах и реакция Linux на системные вызовы из программ тоже может быть различной (такое поведение является нормой в Linux) как того требует эмуляция форматов, принятых в других UNIX системах (COFF и т.п.), а так же эмуляция поведения системных вызовов (Solaris, UnixWare и т.п.). Это как раз то, для чего служит поддержка доменов исполнения и двоичных форматов.
Каждая задача в Linux хранит свою "индивидуальность"
(personality) в task_struct
(p->personality
). В настоящее время существует
(либо официально в ядре, либо в виде "заплат") поддержка
FreeBSD, Solaris, UnixWare, OpenServer и многих других популярных
операционных систем. Значение current->personality
делится на две части:
STICKY_TIMEOUTS
, WHOLE_SECONDS
и
т.п.Изменяя значение personality можно добиться изменения способа
исполнения некоторых системных вызовов, например: добавление
STICKY_TIMEOUT
в current->personality
приведет к тому, что последний аргумент (timeout), передаваемый в
select(2) останется неизменным после возврата, в
то время как в Linux в этом аргументе принято возвращать
неиспользованное время. Некоторые программы полагаются на
соответствующее поведение операционных систем (не Linux) и поэтому
Linux предоставляет возможность эмуляции "ошибок" в
случае, когда исходный код программы не доступен и такого рода
поведение программ не может быть исправлено.
Домен исполнения - это непрерывный диапазон "индивидуальностей", реализованных в одном модуле. Обычно один домен исполнения соответствует одной "индивидуальности", но иногда оказывается возможным реализовать "близкие индивидуальности" в одном модуле без большого количества условий.
Реализация доменов исполнения находится в
kernel/exec_domain.c
и была полностью переписана, по
сравнению с ядром 2.2.x. Список доменов исполнения и диапазоны
"индивидуальностей", поддерживаемых доменами, можно найти
в файле /proc/execdomains
. Домены исполнения могут
быть реализованы в виде подгружаемых модулей, кроме одного -
PER_LINUX
.
Интерфейс с пользователем осуществляется через системный вызов
personality(2), который изменяет
"индивидуальность" текущего процесса или возвращает
значение current->personality
, если в качестве
аргумента передать значение несуществующей
"индивидуальности" 0xffffffff. Очевидно, что поведение
самого этого системного вызова не зависит от
"индивидуальности" вызывающего процесса.
Действия по регистрации/дерегистрации доменов исполнения в ядре выполняются двумя функциями:
int register_exec_domain(struct exec_domain *)
:
регистрирует домен исполнения, добавляя его в односвязный список
exec_domains
под защитой от записи read-write
блокировкой exec_domains_lock
. Возвращает 0 в случае
успеха и ненулевое в случае неудачи.int unregister_exec_domain(struct exec_domain
*)
: дерегистрирует домен исполнения, удаляя его из списка
exec_domains
, опять же под read-write блокировкой
exec_domains_lock
полученной в режиме защиты от
записи. Возвращает 0 в случае успеха.Причина, по которой блокировка exec_domains_lock
имеет тип read-write, состоит в том, что только запросы на
регистрацию и дерегистрацию модифицируют список доменов, в то время
как команда cat /proc/filesystems вызывает
fs/exec_domain.c:get_exec_domain_list()
, которой
достаточен доступ к списку в режиме "только для чтения".
Регистрация нового домена определяет "обработчик lcall7"
и карту преобразования номеров сигналов. "Заплата" ABI
расширяет концепцию доменов исполнения, включая дополнительную
информацию (такую как опции сокетов, типы сокетов, семейство
адресов и таблицы errno (коды ошибок)).
Обработка двоичных форматов реализована похожим образом, т.е. в
виде односвязного списка форматов и определена в
fs/exec.c
. Список защищается read-write блокировкой
binfmt_lock
. Как и exec_domains_lock
,
блокировка binfmt_lock
, в большинстве случаев, берется
"только на чтение" за исключением
регистрации/дерегистрации двоичного формата. Регистрация нового
двоичного формата расширяет системный вызов
execve(2) новыми функциями
load_binary()/load_shlib()
так же как и
core_dump()
. Метод load_shlib()
используется только в устаревшем системном вызове
uselib(2), в то время как метод
load_binary()
вызывается функцией
search_binary_handler()
из do_execve()
,
который и является реализацией системного вызова
execve(2).
"Индивидуальность" процесса определяется во время
загрузки двоичного формата соответствующим методом
load_binary()
с использованием некоторых эвристик.
Например, формат UnixWare7 при создании помечается утилитой
elfmark(1), которая заносит "магическую"
последовательность 0x314B4455 в поле e_flags
ELF-заголовка. Эта последовательность затем определяется во время
загрузки приложения и в результате
current->personality
принимает значение PER_UW7.
Если эта эвристика не подходит, то более универсальная обрабатывает
пути интерпретатора ELF, подобно /usr/lib/ld.so.1
или
/usr/lib/libc.so.1
для указания используемого формата
SVR4 и personality принимает значение PER_SVR4. Можно написать
небольшую утилиту, которая использовала бы возможности
ptrace(2) Linux для пошагового прохождения по коду
и принудительно запускать программы в любой
"индивидуальности".
Поскольку "индивидуальность" (а следовательно и
current->exec_domain
) известна, то и системные
вызовы обрабатываются соответственно. Предположим, что процесс
производит системный вызов через шлюз lcall7. Такой вызов передает
управление в точку ENTRY(lcall7)
в файле
arch/i386/kernel/entry.S
, поскольку она задается в
arch/i386/kernel/traps.c:trap_init()
. После
преобразования размещения стека, entry.S:lcall7
получает указатель на exec_domain
из
current
и смещение обработчика lcall7 внутри
exec_domain
(которое жестко задано числом 4 в
ассемблерном коде, так что вы не сможете изменить смещение поля
handler
в C-объявлении struct
exec_domain
) и переходит на него. Так на C это выглядело бы
как:
static void UW7_lcall7(int segment, struct pt_regs * regs) { abi_dispatch(regs, &uw7_funcs[regs->eax & 0xff], 1); }
где abi_dispatch()
- это обертка вокруг таблицы
указателей на функции, реализующих системные вызовы для personality
uw7_funcs
.