Наши партнеры








Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

Вперед Назад Содержание

3. Virtual Filesystem (VFS)

3.1 Кеш Inode и взаимодействие с Dcache

Для поддержки различных файловых систем 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 представляет из себя:

  1. Глобальный хеш-массив 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().
  2. Глобальный список inode_in_use, который содержит допустимые inodes (i_count>0 и i_nlink>0). Inodes вновь созданные вызовом функций get_empty_inode() и get_new_inode() добавляются в список inode_in_use
  3. Глобальный список inode_unused, который содержит допустимые inode с i_count = 0.
  4. Список для каждого суперблока (sb->s_dirty) , который содержит inodes с i_count>0, i_nlink>0 и i_state & I_DIRTY. Когда inode помечается как "грязный" (здесь и далее под термином "грязный" подразумевается "измененный" прим. перев.), он добавляется к списку sb->s_dirty при условии, что он (inode) хеширован. Поддержка такого списка позволяет уменьшить накладные расходы на синхронизацию.
  5. Inode cache суть есть - SLAB cache, который называется 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(), которая разбита на две части:

  1. open_namei(): заполняет структуру nameidata, содержащую структуры dentry и vfsmount.
  2. 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), эта функция:

  1. Пытается найти inode в хеш-таблице по номерам суперблока и inode. Поиск выполняется под блокировкой inode_lock. Если inode найден, то увеличивается его счетчик ссылок (i_count); если счетчик перед инкрементом был равен нулю и inode не "грязный", то он удаляется из любого списка (inode->i_list), в котором он находится (это конечно же список inode_unused) и вставляется в список inode_in_use; в завершение, уменьшается счетчик inodes_stat.nr_unused.
  2. Если inode на текущий момент заблокирован, то выполняется ожидание до тех пор, пока inode не будет разблокирован, таким образом, iget4() гарантирует возврат незаблокированного inode.
  3. Если поиск по хеш-таблице не увенчался успехом, то вызывается функция get_new_inode(), которой передается указатель на место в хеш-таблице, куда должен быть вставлен inode.
  4. 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) подробнее:

  1. Если входной параметр NULL, то абсолютно ничего не делается и управление возвращается обратно.
  2. Если входнй параметр определен, то вызывается специфичный для файловой системы метод sb->s_op->put_inode() без захвата блокировки (так что он может быть блокирован).
  3. Устанавливается блокировка (spinlock) и уменьшается i_count. Если это была не последняя ссылка, то просто проверяется - поместится ли количество ссылок в 32-битное поле и если нет - то выводится предупреждение. Отмечу, что поскольку вызов производится под блокировкой inode_lock, то для вывода предупреждения используется функция printk(), которая никогда не блокируется, поэтому ее можно вызывать абсолютно из любого контекста исполнения (даже из обработчика прерываний!).
  4. Если ссылка была последней, то выполняются дополнительные действия.

Дополнительные действия, выполняемые по закрытию в случае последней ссылки функцией iput(), достаточно сложны, поэтому они рассматриваются отдельно:

  1. Если 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).
  2. Если i_nlink != 0, то проверяется - есть ли другие inode с тем же самым хеш-ключом (in the same hash bucket) и если нет, и inode не "грязный", то он удаляется из своего списка типа, вставляется в список inode_unused, увеличивая inodes_stat.nr_unused. Если имеются inodes с тем же самым хеш-ключом, то inode удаляется из списка типа и добавляется к списку inode_unused. Если это анонимный inode (NetApp .snapshot) то он удаляется из списка типа и очищается/удаляется полностью.

3.2 Регистрация/Дерегистрация файловых систем.

Ядро Linux предоставляет механизм, минимизирующий усилия разработчиков по написанию новых файловых систем. Исторически сложилось так, что:

  1. В мире широко используются различные операционные системы и чтобы люди не потеряли деньги, затреченные на покупку легального программного обеспечения, Linux должен был предоставить поддержку большого количества файловых систем, большинство из которых реализовано исключительно для совместимости.
  2. Интерфейс для новых файловых систем должен был быть очень простым, чтобы разработчики могли легко перепроектировать существующие файловые системы в их версии "ТОЛЬКО ДЛЯ ЧТЕНИЯ". Linux значительно облегчает создание таких версий, 95% работы над созданием новой файловой системы заключается в добавлении поддержки записи. Вот конкретный пример, я написал файловую систему BFS в версии "ТОЛЬКО ДЛЯ ЧТЕНИЯ" всего за 10 часов, однако мне потребовалось несколько недель, чтобы добавить в нее поддержку записи (и даже сегодня некоторые пуристы говорят о ее незавершенности, поскольку в ней "не реализована поддержка компактификации"). .
  3. Интерфейс VFS является экспортируемым и поэтому все файловые системы в 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;
};


Поля структуры:

  • name: Удобочитаемое название файловой системы, которое выводится в файле /proc/filesystems и используется как ключ для поиска файловой системы по имени; это же имя используется как аргумент в вызове mount(2) и должно быть уникальным. Для модулей имя указывает на адресное пространство модуля так, что в случае, когда модуль уже выгружен, но файловая система еще остается зарегистрированной, то команда cat /proc/filesystems может вызвать oops.
  • fs_flags: один или более флагов (объединенных по OR): FS_REQUIRES_DEV для файловых систем, которые могут быть смонтированы только с блочных устройств, FS_SINGLE для файловых систем, имеющих только один суперблок, FS_NOMOUNT для файловых систем которые не могут быть смонтированы из пользовательского пространства системным вызовом mount(2), однако такие файловые системы могут быть смонтированы ядром через вызов kern_mount(), например pipefs.
  • read_super: указатель на функцию, которая считывает суперблок в процессе монтирования. Эта функция должна быть определена обязательно. В случае ее отсутствия, операция монтирования (независимо от того - из пользовательского ли пространства или из ядра выполняется монтирование) всегда будет терпеть неудачу, а в случае установленного флага FS_SINGLE попытка монтирования будет приводить к Oops в get_sb_single(), при попытке получить ссылку fs_type->kern_mnt->mnt_sb (в то время как fs_type->kern_mnt = NULL).
  • owner: указатель на модуль реализации файловой системы. Если файловая система связана в ядро статически, то этот указатель содержит NULL. Нет необходимости устанавливать его вручную, так как макрос THIS_MODULE делает это автоматически.
  • kern_mnt: только для файловых систем, имеющих флаг FS_SINGLE. Устанавливается kern_mount() (TODO: вызов kern_mount() должен отвергать монтирование файловых систем если флаг FS_SINGLE не установлен).
  • next: поле связи в односвязном списке file_systems (см. fs/super.c). Список защищается "read-write" блокировкой file_systems_lock и модифицируется функциями register/unregister_filesystem().

Функция read_super() заполняет поля суперблока, выделяет память под корневой inode и инициализирует специфичную информацию, связанную с монтируемым экземпляром файловой системы. Как правило read_super():

  1. Считывает суперблок с устройства, определяемого аргументом sb->s_dev, используя функцию bread(). Если предполагается чтение дополнительных блоков с метаданными, то имеет смысл воспользоваться функцией breada(), чтобы прочитать дополнительные блоки асинхронно.
  2. Суперблок проверяется на корректность по "магическим" последовательностям и другим признакам.
  3. Инициализируется указатель sb->s_op на структуру struct super_block_operations. Эта структура содержит указатели на функции, специфичные для файловой системы, такие как "read inode", "delete inode" и пр.
  4. Выделяет память под корневой inode и dentry вызовом функции d_alloc_root().
  5. Если файловая система монтируется не как "ТОЛЬКО ДЛЯ ЧТЕНИЯ", то в sb->s_dirt записывается 1 и буфер, содержащий суперблок, помечается как "грязный" (TODO: зачем это делается? Я сделал так в BFS потому, что в MINIX делается то же самое).

3.3 Управление файловыми дескрипторами

В 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 на случай поиска первого неиспользуемого дескриптора при следующем открытии файла.

3.4 Управление файловой структурой

Структура 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:

  1. f_list: поле связи с одним (и только одним) из списков:
    а) 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.
  2. f_dentry: dentry файла. Создается в процессе поиска nameidata в open_namei() (или точнее в path_walk()), но в действительности поле file->f_dentry заполняется в dentry_open().
  3. f_vfsmnt: указатель на структуру vfsmount файловой системы, содержащей файл. Заполняется функцией dentry_open() и является частью nameidata, поиск которой производится в open_namei() (или точнее в path_init()).
  4. f_op: указатель на список file_operations, который содержит адреса методов для работы с файлом. Копируется из inode->i_fop методом s_op->read_inode(), вызываемым в процессе поиска nameidata. Более подробно на списке file_operations мы остановимся ниже в этом разделе.
  5. f_count: счетчик ссылок, изменяется в get_file/put_filp/fput.
  6. f_flags: флаги O_XXX системного вызова open(2), копируются функцией dentry_open() (с небольшими изменениями в filp_open()), при чем флаги O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC сбрасываются, поскольку они не могут модифицироваться по параметру F_SETFL (или F_GETFL) в системном вызове fcntl(2).
  7. f_mode: комбинация флагов состояния, устанавливается в dentry_open(). Флаги режимов доступа для чтения и записи выведены в отдельные биты, чтобы облегчить контроль состояния: (f_mode & FMODE_WRITE) и (f_mode & FMODE_READ).
  8. f_pos: текущая позиция чтения/записи в файле. Для архитектуры i386 имеет тип long long, т.е. 64 бита.
  9. f_reada, f_ramax, f_raend, f_ralen, f_rawin: поддержка опережающего чтения (readahead) слишком сложна, чтобы обсуждаться простыми смертными ;)
  10. f_owner: владелец файла, который будет получать I/O уведомления посредством механизма SIGIO (см. fs/fcntl.c:kill_fasync()).
  11. f_uid, f_gid - user id и group id процесса, открывшего файл, заполняются во время создания структуры в get_empty_filp(). Если файл является сокетом, то эти поля могут быть использованы в ipv4 netfilter.
  12. f_error: используется клиентом NFS для возврата ошибки записи. Поле устанавливается в fs/nfs/file.c и проверяется в mm/filemap.c:generic_file_write().
  13. f_version - механизм контроля версий, служит для синхронизации с кэшем. Увеличивается на единицу (используя глобальный event) всякий раз, когда изменяется f_pos.
  14. private_data: скрытая информация о файле, может использоваться файловой системой (например coda хранит здесь удостоверения) или драйверами устройств. Драйверы устройств (при наличии devfs) могут использовать это поле для различения нескольких экземпляров вместо классического анализа младшего номера версии в 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 *);
};


  1. owner: указатель на модуль рассматриваемой подсистемы. Это поле устанавливается только драйверами устройств, файловая система может игнорировать его, поскольку счетчик ссылок модуля файловой системы изменяется во время монтирования/демонтирования, в то время как для драйверов это должно делаться во время открытия/закрытия устройства.
  2. llseek: реализация системного вызова lseek(2). Обычно опускается и используется fs/read_write.c:default_llseek(). (TODO: Принудительно устанавливать это поле в NULL, тем самым сэкономится лишний if() в llseek())
  3. read: реализация системного вызова read(2). Файловые системы могут использовать mm/filemap.c:generic_file_read() для обычных файлов и fs/read_write.c:generic_read_dir() (которая просто возвращает -EISDIR) для каталогов.
  4. write: реализация системного вызова write(2). Файловые системы могут использовать mm/filemap.c:generic_file_write() для обычных файлов и игнорировать его для каталогов.
  5. readdir: используется файловой системой. Реализует системные вызовы readdir(2) и getdents(2) для каталогов и игнорируется для обычных файлов.
  6. poll: реализация системных вызовов poll(2) и select(2)
  7. ioctl: реализация специфичного для драйвера или для файловой системы метода ioctl ( управление вводом/выводом). Обратите внимание: общие методы ioctl типа FIBMAP, FIGETBSZ, FIONREAD реализуются на более высоком уровне, поэтому они никогда не пользуются методом f_op->ioctl().
  8. mmap: реализация системного вызова mmap(2). Файловая система может использовать generic_file_mmap для обычных файлов и игнорировать это поле для каталогов.
  9. open: вызывается во время выполнения open(2) функцией dentry_open(). Редко используется файловыми системами, например coda пытается кэшировать файл во время открытия.
  10. flush: вызывается при каждом вызове close(2) для заданного файла, не обязательно в последнем (см. метод release() ниже). Единственная файловая система, которая вызывает этот метод - это NFS клиент, которая "выталкивает" все измененные страницы. Примечательно, что этот метод может завершаться с кодом ошибки, который передается обратно в пространство пользователя, откуда делался системный вызов close(2).
  11. release: метод вызывается в последнем вызове close(2) для заданного файла, т.е. когда file->f_count станет равным нулю. Хотя и возвращает целое (int) значение, но VFS игнорирует его (см. code>fs/file_table.c:__fput()).
  12. fsync: преобразуется в системные вызовы fsync(2)/fdatasync(2), причем последний аргумент определяет сам вызов - fsync или fdatasync. Не выполняет почти никаких действий за исключением преобразования файлового дескриптора в файловую структуру (file = fget(fd)) и сброса/установки семафора inode->i_sem. Файловая система Ext2, на сегодняшний день, игнорирует последний аргумент, передаваемый методу и выполняет одни и те же действия как для fsync(2) так и для fdatasync(2).
  13. fasync: этот метод вызывается при изменении file->f_flags & FASYNC.
  14. lock: специфичная для файловой системы часть механизма блокировки области файла POSIX fcntl(2). Единственная неувязка состоит в том, что этот метод вызывается перед независимой от типа файловой системы posix_lock_file(), если метод завершается успешно, а стандартный POSIX код блокировки терпит неудачу, то блокировка не будет снята на зависимом от типа файловой системы уровне..
  15. readv: реализация системного вызова readv(2).
  16. writev: реализация системного вызова writev(2).

3.5 Управление Суперблоком и точкой монтирования

В 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:

  1. s_list: двусвязный список всех активных суперблоков; Заметьте, что я не говорю "всех смонтированных файловых систем", потому что в Linux всем смонтированным экземплярам файловой системы соответствует единственный суперблок.
  2. s_dev: предназначено для файловых систем, требующих наличие блочного устройства, т.е. для файловых систем, зарегистрированных с флагом 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 анонимными файловыми системами.
  3. s_blocksize, s_blocksize_bits: Размер блока и количество бит, необходимое для хранения размера блока (log2(blocksize)).
  4. s_lock: индикатор блокировки суперблока функциями lock_super()/unlock_super().
  5. s_dirt: устанавливается при внесении изменений в суперблок и сбрасывается при записи его обратно на диск.
  6. s_type: указатель на структуру struct file_system_type, соответствующую файловой системе. Метод файловой системы read_super() не должен устанавливать это поле, так как это поле устанавливается VFS в функции fs/super.c:read_super(), в случае успешного вызова метода read_super() конкретной файловой, и сбрасывется в NULL в противном случае.
  7. s_op: указатель на структуру (список) super_operations, которая содержит специфичные для заданной файловой системы методы, такие как чтение/запись inode и пр. Корректное заполнение этой структуры - задача метода файловой системы read_super().
  8. dq_op: операции по дисковому квотированию.
  9. s_flags: флаги суперблока.
  10. s_magic: "магическое" число файловой системы. Используется файловой системой minix для различения разных вариантов ее.
  11. s_root: dentry корня файловой системы. Метод read_super() считывает корневой inode с диска и передает его в d_alloc_root(), который выделяет память под dentry и заполняет ее. Некоторые файловые системы используют иное обозначение корня, нежели "/", поэтому используется более общая функция d_alloc() для образования полного имени, например pipefs использует "pipe:" для обозначения своего корня.
  12. s_wait: очередь ожидания, в которой нахдятся процессы, ожидающие снятия блокировки с суперблока.
  13. s_dirty: список всех "грязных" (измененных) inodes. Напомню, что если inode изменился (т.е. inode->i_state & I_DIRTY), то этот список связуется через inode->i_list.
  14. s_files: список всех открытых файлов в данном суперблоке. Полезен при принятии решения о перемонтировании файловой системы в режиме "только для чтения", см. fs/file_table.c:fs_may_remount_ro(), которая просматривает список sb->s_files и отвергает возможность перемонтирования если имеется хотя бы один файл, открытый "на запись" (file->f_mode & FMODE_WRITE) или ожидающий удаления (inode->i_nlink == 0).
  15. s_bdev: для случая FS_REQUIRES_DEV указывает на структуру block_device, описывающую блочное устройство, с которого смонтирована файловая система.
  16. s_mounts: список всех структур vfsmount для каждого смонтированного экземпляра данного суперблока.
  17. s_dquot: используется при квотировании диска.

Методы управления суперблоком перечисляются в структуре 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 *);
};


  1. read_inode: операция чтения inode из файловой системы. Вызывается только в 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()).
  2. write_inode: операция записи inode на диск. так же как и read_inode() отыскивает нужный дисковый блок и вызывает функцию буферного кэша mark_buffer_dirty(bh). Этот метод вызывается для "грязных" inode (которые были помечены вызовом mark_inode_dirty()) при возникновении необходимости синхронизации как отдельно взятого inode, так и файловой системы в целом.
  3. put_inode: вызывается всякий раз при уменьшении счетчика ссылок.
  4. delete_inode: вызывается всякий раз, когда inode->i_count и inode->i_nlink достигают нулевого значения. Файловая система удаляет дисковую копию inode и вызывает clear_inode() для VFS inode, чтобы "прекратить его существование окончательно".
  5. put_super: вызывается на последней стадии работы системного вызова umount(2), чтобы уведомить файловую систему о том, что любая приватная информация, удерживаемая ею, должна быть освобождена. Обычно это brelse() блока, содержащего суперблок, и kfree() для освобождения всех ранее размещенных блоков, inodes и т.п.
  6. write_super: вызывается в случае необходимости записать суперблок на диск. Должен отыскать блок, содержащий суперблок, (обычно хранится в области sb-private) и вызвать mark_buffer_dirty(bh). А так же должен сбросить флаг sb->s_dirt flag.
  7. statfs: реализация системного вызова fstatfs(2)/statfs(2). Заметьте, что указатель на struct statfs, передаваемый в качестве аргумента, является указателем пространства ядра а не пользовательского пространства, поэтому не следует выполнять каких либо операций ввода-вывода в пользовательском пространстве. В случае отсутствия этого метода вызов statfs(2) будет возвращвть код ошибки ENOSYS.
  8. remount_fs: вызывается всякий раз при перемонтировании файловой системы.
  9. clear_inode: вызывается из функции clear_inode() уровня VFS. Файловая система должна освободить приватную информацию в структуре inode (присоединенную через поле generic_ip).
  10. umount_begin: вызывается в случае принудительно размонтирования для уведомления файловой системы заранее, чтобы убедиться, что она не занята. В настоящее время используется только NFS. Этот метод не имеет никакого отношения к идее поддержки принудительного размонтирования на уровне VFS.

Теперь рассмотрим последовательность действий, выполняемых при монтировании дисковой (FS_REQUIRES_DEV) файловой системы. Реализация системного вызова mount(2) находится в fs/super.c:sys_mount(), которая по сути является лишь оберткой, которая передает опции монтирования, тип файловой системы и название устройства в функцию do_mount().

  1. В случае необходимости, загружается модуль драйвера файловой системы и увеличивается счетчик ссылок на этот модуль. Примечательно, что в процессе монтирования счетчик ссылок на модуль файловой системы увеличивается дважды - один раз в do_mount(), вызываемой из get_fs_type(), и один раз в get_sb_dev(), вызываемой из get_filesystem(), если read_super() выполнилась успешно. Первое увеличение предотвращает выгрузку модуля пока выполняется метод read_super() и второе увеличение указывает на то, что модуль используется смонтированным экземпляром. Вполне понятно, что перед завершением do_mount() уменьшает счетчик ссылок на единицу, таким образом суммарное приращение счетчика составляет единицу после каждого монтирования.
  2. Для нашего случая выражение fs_type->fs_flags & FS_REQUIRES_DEV истинно, поэтому далее инициализируется суперблок, вызовом get_sb_bdev(), который получает ссылку на блочное устройство и вызывом метода read_super() заполняет поля суперблока. Если все прошло гладко, то структура super_block считается инициализированной и мы получаем дополнительно ссылку на модуль файловой системы и ссылку на основное блочное устройство.
  3. В памяти размещается новая структура vfsmount и "прицепляется" к списку sb->s_mounts и к глобальному списку vfsmntlist. С помощью поля mnt_instances структуры vfsmount можно найти все смонтированные экземпляры файловой системы для одного и того же суперблока. С помощью списка mnt_list можно отыскать все смонтированные экземпляры файловых систем для всех суперблоков в системе. Поле mnt_sb указывает на данный суперблок, а mnt_root получает новую ссылку на sb->s_root dentry.

3.6 Пример виртуальной файловой системы: pipefs

В качестве простого примера файловой системы в 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():

  1. Создается новое неименованное (anonymous) устройство установкой бита в unnamed_dev_in_use; если в этом массиве не окажется свободного бита, то kern_mount() вернется с ошибкой EMFILE.
  2. Посредством get_empty_super() создается новая структура суперблока. Функция get_empty_super() проходит по списку суперблоков super_block в поисках свободного места, т.е. s->s_dev == 0. Если такового не обнаружилось, то резервируется память вызовом kmalloc(), с приоритетом GFP_USER. В get_empty_super() проверяется превышение максимально возможного количества суперблоков, так что в случае появления сбоев, при монтировании pipefs, можно попробовать подкорректировать /proc/sys/fs/super-max.
  3. Вызывается метод pipe_fs_type->read_super() (т.е. pipefs_read_super()), который размещает корневой inode и dentry sb->s_root, а также записывает адрес &pipefs_ops в sb->s_op.
  4. Затем вызывается add_vfsmnt(NULL, sb->s_root, "none"), которая размещает в памяти новую структуру vfsmount и включает ее в список vfsmntlist и sb->s_mounts.
  5. В 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, то же произойдет и при попытке чтения с конца канала, открытого на запись.

3.7 Пример дисковой файловой системы: BFS

В качестве примера дисковой файловой системы рассмотрим 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(), который выполняет следующие действия:

  1. set_blocksize(s->s_dev, BFS_BSIZE): поскольку предполагается взаимодействие с уровнем блочного устройства через буферный кэш, следует выполнить некоторые действия, а именно указать размер блока и сообщить о нем VFS через поля s->s_blocksize и s->s_blocksize_bits.
  2. bh = bread(dev, 0, BFS_BSIZE): читается нулевой блок с устройства s->s_dev. Этот блок является суперблоком файловой системы.
  3. Суперблок проверяется на наличие сигнатуры ("магической" последовательности) BFS_MAGIC, если все в порядке, то он сохраняется в поле s->su_sbh (на самом деле это s->u.bfs_sb.si_sbh).
  4. Далее создается новая битовая карта inode вызовом kmalloc(GFP_KERNEL) и все биты в ней сбрасываются в 0, за исключением двух первых, которые указывают на то, что 0-й и 1-й inode никогда не должны распределяться. Inode с номером 2 является корневым, установка соответствующего ему бита производится несколькими строками ниже, в любом случае файловая система должна получить корневой inode во время монтирования!
  5. Инициализируется s->s_op, и уже после этого можно вызвать iget(), которая обратится к s_op->read_inode(). Она отыщет блок, который содержит заданный (по inode->i_ino и inode->i_dev) inode и прочитает его. Если при запросе корневого inode произойдет ошибка, то память, занимаемая битовой картой inode, будет освобождена, буфер суперблока возвратится в буферный кэш и в качестве результата будет возвращен "пустой" указатель - NULL. Если корневой inode был успешно прочитан, то далее размещается dentry с именем / и связывается с этим inode.
  6. После этого последовательно считываются все inode в файловой системе и устанвливаются соответствующие им биты в битовой карте, а так же подсчитываются некоторые внутренние параметры, такие как смещение последнего inode и начало/конец блоков последнего файла. Все прочитанные inode возвращаются обратно в кэш inode вызовом iput() - ссылка на них не удерживается дольше, чем это необходимо.
  7. Если файловая система была смонтирована как "read/write", то буфер суперблока помечается как "грязный" (измененный прим. перев.) и устанавливается флаг s->s_dirt (TODO: Для чего? Первоначально я сделал это потому, что это делалось в minix_read_super(), но ни minix ни BFS кажется не изменяют суперблок в read_super()).
  8. Все складывается удачно, так что далее функция возвращает инициализированный суперблок уровню VFS, т.е. 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():

  1. Имена из пользовательского пространства копируются в пространство ядра функцией getname(), которая выполняет проверку на наличие ошибок.
  2. Эти имена преобразуются в nameidata с помощью path_init()/path_walk(). Результат сохраняется в структурах old_nd и nd
  3. Если old_nd.mnt != nd.mnt, то возвращается "cross-device link" EXDEV - невозможно установить ссылку между файловыми системами, в Linux это означает невозможность установить ссылку между смонтированными экземплярами одной файловой системы (или, особенно, между различными файловыми системами).
  4. Для nd создается новый dentry вызовом lookup_create() .
  5. Вызывается универсальная функция vfs_link(), которая проверяет возможность создания новой ссылки по заданному пути и вызывает метод dir->i_op->link(), который приводит нас в fs/bfs/dir.c:bfs_link().
  6. Внутри bfs_link(), производится проверка - не делается ли попытка создать жесткую ссылку на директорию и если это так, то возвращается код ошибки EPERM. Это как стандарт (ext2).
  7. Предпринимается попытка добавить новую ссылку в заданную директорию вызовом вспомогательной функции bfs_add_entry(), которая отыскивает неиспользуемый слот (de->ino == 0) и если находит, то записывает пару имя/inode в соответствующий блок и помечает его как "грязный".
  8. Если ссылка была добавлена, то далее ошибки возникнуть уже не может, поэтому увеличивается inode->i_nlink, обновляется inode->i_ctime и inode помечается как "грязный" твк же как и inode приписанный новому dentry.

Другие родственные операции над inode, подобные unlink()/rename(), выполняются аналогичным образом, так что не имеет большого смысла рассматривать их в деталях.

3.8 Домены исполнения и двоичные форматы

Linux поддерживает загрузку пользовательских приложений с диска. Самое интересное, что приложения могут храниться на диске в самых разных форматах и реакция Linux на системные вызовы из программ тоже может быть различной (такое поведение является нормой в Linux) как того требует эмуляция форматов, принятых в других UNIX системах (COFF и т.п.), а так же эмуляция поведения системных вызовов (Solaris, UnixWare и т.п.). Это как раз то, для чего служит поддержка доменов исполнения и двоичных форматов.

Каждая задача в Linux хранит свою "индивидуальность" (personality) в task_struct (p->personality). В настоящее время существует (либо официально в ядре, либо в виде "заплат") поддержка FreeBSD, Solaris, UnixWare, OpenServer и многих других популярных операционных систем. Значение current->personality делится на две части:

  1. старшие три байта - эмуляция "ошибок": STICKY_TIMEOUTS, WHOLE_SECONDS и т.п.
  2. младший байт - соответствующая "индивидуальность" (personality), уникальное число.

Изменяя значение 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.


Вперед Назад Содержание