Виртуальная файловая система VFS

Виртуальная файловая система VFS

Для организации доступа к разнообразным файловым системам (ФС) в Unix используется промежуточный слой абстракции - виртуальная файловая система (VFS).

С точки зрения программиста VFS организована как интерфейс или абстрактный класс в объектно ориентированном языке программирования типа C++.

VFS объявляет API доступа к файловой системе, а реализацию этого API отдаёт на откуп драйверам конкретных ФС, которые можно рассматривать, как производные классы, наследующие интерфейс VFS.

Виртуальные методы VFS

Каждый драйвер ФС должен реализовать вызовы для работы с файлами, inode и с ФС в целом, описанные в заголовочном файле ядра linux/fs.h. При монтировании ФС соответствующие структуры заполняются указателями на соответствующие реализации в драйвере.

Если какая-нибудь функция отсутствует в драйвере, то указатель ссылается на функцию заглушку, которая возвращает ошибку "не реализовано" - ENOSYS.

struct file_operations {
    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 *);
...
struct inode_operations {
    struct dentry * (*lookup) (struct inode *,struct dentry *);
    int (*create) (struct inode *,struct dentry *,int);
    int (*link) (struct dentry *,struct inode *,struct dentry *);
    int (*symlink) (struct inode *,struct dentry *,const char *);
    int (*mkdir) (struct inode *,struct dentry *,int);
...
struct super_operations {
    int (*  ) (struct super_block *, int *, char *);
    void (*umount_begin) (struct super_block *);
...

Для файловой системы ext2fs объявление соответствующих функций выглядит так:

extern struct dentry *ext2_lookup (struct inode *, struct dentry *);
extern int ext2_create (struct inode *,struct dentry *,int);
extern int ext2_mkdir (struct inode *,struct dentry *,int);
extern int ext2_rmdir (struct inode *,struct dentry *);
extern int ext2_unlink (struct inode *,struct dentry *);
...

Вызовы remount_fs() и umount_begin() можно использовать как конструктор и деструктор класса, которые вызываются в момент монтирования файловой системы и в момент размонтирования.

Виртуальный inode

Кроме виртуальных функций VFS описывает обобщённые структуры superblock, dentry (directory entry запись в каталоге),inode (в некоторых ОС называется vnode). Эти структуры содержит все основные структуры данных суперблока ФС, каталогов и inode из классической ФС Unix. Кроме того структура file содержит информацию, необходимую для работы с открытыми файлами Поскольку VFS является интерфейсом, то перечисленные структуры не содержат технических подробностей, таких как информации о размещении блоков данных файла или IP адреса сервера сетевой ФС. Для хранения деталей реализации, драйверу ФС в каждой из структур предоставляется дополнительное поле для хранения указателя на специфические для ФС структуры данных. В inode в Linux это поле выглядит так:

union {
  struct pipe_inode_info        pipe_i;
  struct minix_inode_info       minix_i;
  struct ext2_inode_info        ext2_i;
...
  void              *generic_ip;
} u;

На основе перечисленных

Драйвер ФС должен уметь конвертировать атрибуты файла, фактически хранящиеся в ФС, в поля inode. Например, драйвер NTFS должен уметь преобразовывать SID пользователя Windows в UID пользователя Unix и наоборот.

Если ФС не позволяет хранить необходимые атрибуты inode, то при монтировании драйверу можно передать некоторые дополнительные параметры, содержащие фиксированные значения для этих атрибутов. Драйвер FAT позволяет задать:

  • uid, gid - владелец и группа для всех файлов
  • dmask, fmask - маски прав доступа для файлов и каталогов, которые вычитается из rwxrwxrwx.
  • codepage, iocharset - кодовые таблицы для преобразования имён файлов на национальных алфавитов в UTF-8 или иную, используемую в Unix кодировку.
  • tz=UTC - указание, что метки времени в ФС хранятся в UTC. DOS и Windows хранят метки времени в локальном времени часового пояса.

Procfs /proc

Благодаря VFS в Unix возможно представление в виде ФС любых иерархических структур данных. Самый известный пример, это файловая система Procfs , которая отображает в виде дерева каталогов внутренние структуры ядра. Чаще всего, она смонтирована в каталог /proc, но может быть смонтирована и в другой каталог или не смонтирована вовсе.

В каталоге /proc в Linux присутствуют, по сути, два дерева ФС. В основном дереве, каждый каталог имеет числовое имя и соответствует процессу, с соответствующим PID. Файлы в этих каталогах соответствуют структурам данных, связанных с процессом. Каталог /proc/self в Linux является символической ссылкой, указывающей на каталог процесса, который к ней обратился. Например, cat /proc/self/cmdline покажет аргументы запуска cat т.е. cat /proc/self/cmdline, а ls -l /proc/self/exe покажет ссылку на исполняемый файл ls - /proc/self/exe -> /usr/bin/ls.

В дереве /proc/sys отображаются внутренние переменные ядра. Операции чтения/записи в каталоге /proc/sys позволяют настраивать такие параметры ядра как маршрутизация - /proc/sys/net/ipv4/ip_forward или максимальный объём разделяемой между процессами памяти /proc/sys/kernel/shmmax. В исторических Unix, таких как Solaris 5 такие настройки делались через отладчик, который подключался к ядру как к программе и менял значения переменных.

В последних версиях ядра Linux прослеживается тенденция вынесения доступа к новым переменным в отдельную ФС Sysfs, которая монтируется в каталог /sys.

ФС вне ядра - fuse

В Linux (а в последнее время и в Windows) есть возможность зарегистрировать в VFS свой драйвер ФС без написания кода в ядре. Адаптер fuse транслирует вызовы из ядра в обычный пользовательский процесс. Благодаря этому механизму возможно написание драйверов, написанных на любом языке программирования: C++, python, Java и т.д. Главное, написать соответствующий набор функций и через API fuse зарегистрировать их в ядре.

Известные примеры: sshfs - монтирование дисков через sftp, NTFS-3G - драйвер NTFS через fuse, различные ФС на основе баз данных.

Разбор имён файлов в VFS

Файловые системы (ФС) в Unix доступны через промежуточный слой абстракции - виртуальную файловую систему (VFS). VFS организована в виде дерева, в котором узлы ветвления это всегда каталоги, а листья - любые допустимые в VFS объекты (файлы, каталоги, символические ссылки, сокеты, и т.д.). Чтобы подключить в VFS новый носитель данных, выполняется операция монтирования, которая сопоставляет одному из каталогов, уже присутствующему в дереве, ссылку на корневой каталог в ФС на носителе. Доступ к ранее существовавшему содержимому каталога до отмены операции монтирования прекращается. Говорят, что в результате монтирования носитель становится смонтированным в каталог. Корневой каталог всей VFS также должен быть смонтирован на корневой каталог какой-либо конкретной ФС.

Имена объектов в VFS - это байтовые строки с завершающим нулевым байтом. Интерпретацией кодировки символов VFS не занимается. При записи путей символ "слэш" "/" используется как разделитель каталогов и потому не может быть использован в именах объектов VFS. Кроме слэша внутри имён объектов не может присутствовать нулевой байт '\0'. Длина имени не должна превышать NAME_MAX (определена в файле linux/limits.h как 255 байт). В остальном в VFS ограничений на имена нет, но такие ограничения могут быть обусловлены реализацией конкретной файловой системы.

В каждом каталоге существует два зарезервированных имени "." и "..", обозначающие текущий и вышележащий каталоги соответственно. Если ФС на носителе не поддерживает такие имена, то они должны быть сэмулированы драйвером. При выполнении монтирования значение "." берётся из корневого каталога на носителе, а ".." из того каталога, в который было произведено монтирование. В корневом каталоге (см. ниже) ".." ссылается на сам каталог как и ".".

Разбор пути

С каждым процессом в Unix связано два каталога:

  • текущий рабочий (current work directory, cwd)
  • корневой (root directory, rtd)

Эти каталоги используются при разборе путей в VFS теми системными вызовами, которые получают пути в качестве аргументов (open("path", ...), unlink("path") и т.п.). Если путь начинается с символа "слэш" "/" то он называется абсолютным и отсчитывается от корневого каталога процесса, а если начинается с любого другого символа, то называется относительным и отсчитывается от текущего рабочего каталога.

Разбор пути идет по следующим правилам (man path_resolution):

  1. Если длина пути превышает PATH_MAX (4096 байт вместе с завершающим нулевым байтом - определено в linux/limits.h)то возвращается ошибка ENAMETOOLONG. Такая же ошибка может появиться в дальнейшем при рекурсивном разборе символических ссылок.
  2. В зависимости от первого символа разбор пути начинается с корневого или с текущего рабочего каталога;
  3. В текущем разбираемом каталоге ищется имя следующего компонента пути. Если на чтение каталога не хватает прав, то возвращается ошибка EACCES; если имя не найдено, то возвращается ошибка ENOENT
  4. Если найденное имя является символической ссылкой, то её содержимое разбирается в рекурсивном вызове. Внутри рекурсии увеличивается счётчик вложенности. Если счётчик вложенности при разборе символических ссылок превышает максимальное допустимое число, то разбор останавливается и возвращается ошибка ELOOP. Если разбор символической ссылки вернул каталог, то он делается текущим для разбора. Разбор продолжается с п.2.
  5. Если найденное имя является каталогом, то проверяется, не используется ли этот каталог как точка монтирования. Если нет, то он устанавливается текущим для разбора. Если каталог используется для монтирования, то текущим для разбора устанавливается корневой каталог смонтированной ФС. Разбор продолжается с п.2. Если найденное имя это каталог ".." то, по правилам формирования этого имени, в корневом каталоге он сошлётся снова на корневой каталог, а в каталоге, используемым для монтирования, прозрачно позволит перейти из одной ФС в другую
  6. Если найденное имя последнее в пути, то соответствующий объект обрабатывается системным вызовом по своим правилам. Если найденное имя не последнее в пути и, при этом, указывает не на каталог и не на символическую ссылку, то выдаётся ошибка ENOTDIR

Текущий и корневой каталоги процесса

Смена текущего каталога выполняется вызовом chdir(const char *dir) или fchdir(int fd). Второй способ требует предварительно получить файловый дескриптор, ссылающийся на каталог.

Смена корневого каталога процесса выполняется вызовом chroot(const char *dir). В новом корневом каталоге имя ".." указывает на него самого, что не позволяет с помощью абсолютных путей подняться по дереву выше корневого каталога процесса. После выполнения этого вызова вся файловая система "выше" указанного корневого каталога становится невидимой для последующих системных вызовов, но открытые файлы за пределами видимости остаются доступными. Более того, текущий каталог может оказаться за пределами корневого, так что при практическом применении, например для ограничения доступа процесса к ФС, надо сочетать chroot() и chdir().

Chroot в shell

Чтобы запустить программу с переопределённым корневым каталогом используется программа

chroot NEWROOT [COMMAND [ARG]...]

Если команда COMMAND не указана, то выполняется shell, указанный в файле passwd для текущего пользователя.

Внутри chroot выполняются следующая последовательность вызовов:

chroot("NEWROOT");
chdir("/");
execve("COMMAND", ...);

Последний вызов означает, что исполняемый файл COMMAND и динамические библиотеки необходимые для его выполнения должны находиться внутри нового корневого каталога.

Если мы хотим запереть пользователя внутри chroot нам необходимо скопировать вовнутрь /bin/bash, /lib/libc.so, /etc/passwd, /dev/tty и ещё ряд важных файлов, полный состав которых зависит от версии ОС и набора решаемых в "тюрьме" задач.

Структуры VFS, описывающие открытый файл в Linux

Сокращенный вариант структур данных в ядре Linux 2.2.26. Структуры описаны в include/linux/{sched.h,fs.h,dcache.h}.

ФД -файловый дескриптор, ФС - файловая система

Данные о процессе в ядре

struct task_struct {
...
  struct files_struct *files; //информация об открытых файлах процесса
...
}

Информация об открытых файлах процесса

struct files_struct {
  atomic_t count; //количество открытых файлов
  int max_fds; //максимальное доступное количество ФД
  int max_fdset;  //максимальный номер использованного ФД
  int next_fd;  //минимальный свободный номер ФД
  fd_set *close_on_exec; //битовая карта ФД, которые необходимо закрыть при вызове exec()
  fd_set *open_fds;  //битовая карта открытых ФД
  struct file * fd_array[NR_OPEN_DEFAULT]; //массив указателей на открытые файлы
...
};

Свойства открытого в процессе файла

struct file {
  struct dentry *f_dentry; //ссылка на запись в каталоге (имя и inode файла)
  struct file_operations *f_op; //указатели на функции драйвера ФС для работы с файлом
  atomic_long_t f_count; //количество открытых файловых дескрипторов процесса, указывающих на файл
  unsigned int f_flags; //флаги, указанные при открытии
  loff_t f_pos; //позиция головки ввода/вывода
  fmode_t f_mode; //права доступа в момент открытия
  struct fown_struct  f_owner; //uid,gid файла в момент открытия
...
  void *private_data; //место для хранения специфичных для драйвера данных
};

Запись в каталоге

struct dentry {
  struct inode *d_inode; 
  struct qstr d_name;
...
}

Inode

struct inode {
...
  kdev_t i_dev; //номер устройства, на котором расположен inode
  unsigned long i_ino; //номер inode
  umode_t i_mode;//права доступа
  nlink_t i_nlink; //число имён
  uid_t i_uid;
  gid_t i_gid;
...
  struct super_block *i_sb; // ссылка на суперблок ФС
...
  unsigned int i_count; //общее число ФД, ссылающихся на этот файл
  struct file_lock * i_flock; //блокировки файла
...
}

Указатели на операции с файлом в драйвере ФС

struct file_operations { 
  int (*open) (struct inode *, struct file *);
  loff_t (*llseek) (struct file *, loff_t, int);
  ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  int (*flush) (struct file *);
  int (*release) (struct inode *, struct file *);
...
}
Прикрепленный файлРазмер
Иконка изображения fd_tables.png10.19 КБ