Продолжаем создавать модуль ядра в Линукс на примере виртуальной файловой системы.

Часть 1: Описание задачи, Модуль ядра

Часть 2: Модуль ядра, Регистрация файловой системы

Часть 3: Inode, Lookup

Что в результате получилось можно увидеть по ссылкам: демо-видео, код.

Столпы файловой системы

Файловая система (фс) в Linux стоит на 3-х столпах: superblock (суперблок), inode, dentry.

суперблок - обычно это общий описатель одной точки монтирования файловой системы. Суперблок создаётся самим Linux-ом и далее идёт заполнение уже в вашем модуле. Подробнее этот процесс описан в предыдущей статье.

inode - это описатель некоторой сущности в файловой системе: файла, директории и т.п. У каждой сущности есть номер, который уникален в пределах точки монтирования (точнее: уникальна пара суперблок - номер inode). И ещё inode не хранит имя сущности - для этого есть dentry.

dentry - это по сути узлы в иерархии или дереве файловой системы. Они упорядочивают сущности в файловой системе (inode-ы) и позволяют работать с ними, как с деревом объектов. Тут стоит отметить, что сами dentry организованы в иерархию и хранятся в неком "кэше", которым управляет сама Linux. Такое отделение иерархии (dentry) от сущностей (inode) и плюс кэширование способствует ускорению работы файловой системы, т.к. нет необходимости по каждому запросу дёргать ФС - с чем-то Linux разбирается сам (и да, это доставляет проблем для не-иерархических структур). Кроме ускорения такое разделение позволяет одному файлу существовать в нескольких местах иерархии, причём под разными именами - т.е. реализует механизм жёстких ссылок. С позиции нашей тэговой файловой системы такая структура является скорее ограничением, под которое придётся подстраиваться: ведь организация тэгов - она явно не иерархическая; симлинки существуют в нескольких тэгах-директориях, но жёсткие ссылки здесь не подойдут, так как будут проблемы с удалением файлов. Поломать же кэш dentry и подкрутить его под свои задачи - задача явно неблагодарная и бесперспективная.

Итак, суперблок мы рассмотрели ранее, теперь посмотрим на inode.

Inode

inode - это структура с большим количеством полей. Полей действительно немало (50+), их количество и их состав зависят от версии ядра, макроcов препроцессора (например, см. CONFIG_FS_ENCRYPTION), даже их порядок может меняться (см. аннотацию __randomize_layout). Частью полей пользуется Linux и лезть туда не обязательно. Из оставшихся полей нам тоже нужны не все - только важные. В нашем случае это следующие поля:

  • i_mode - хранит тип сущности и права на доступ;

  • i_ino - номер файловой сущности;

  • i_uid, i_gid - идентифицируют владельца сущности. Выставляем в 0 (немного облегчим себе жизнь);

  • i_atime, i_ctime, i_mtime - временные метки, связанные с сущностью: время последнего доступа, создания и модификации;

  • i_size - размер, занимаемый файловой сущностью. Можно указать любое, для простоты ставим 1;

  • i_nlink - количество жёстких ссылок на сущность;

  • i_op - операции над узлом;

  • i_fop - операции над файлом.

Пройдёмся по полям подробнее:

i_mode хранит тип сущности и права на доступ. Само значение представляем собой микс значений. Прежде всего задаётся тип сущности. Их есть несколько, но мы используем два: S_IFDIR - директория; S_IFLNK - символьная ссылка (далее симлинк). Помимо типа задаются права на объект (выполнение, запись, чтение) для различных категорий субъектов (пользователя, группы и всех прочих) и специальные биты наследования/ограничения (подробнее см. S_ISVTX). Конечно программист волен выставлять любые значения, однако лучше разрешить чтение для всех групп пользователей; выполнение для директорий; запись для директорий тоже будет полезной (так как добавление и удаление файлов - это очень полезная функция). В общем, самый простой вариант для большинства сущностей сделать права 0777 (восьмеричное число) или, по-правильному S_IRWXU | S_IRWXG | S_IRWXO.

i_ino содержит номер представляемой сущности. И это не совсем индекс, т.к. значение 0 очень плохо переваривается файловой системой (считается ошибочным), да и значения идут не подряд. В любом случае, наличие номера у файловой сущности есть обязательное условие со стороны Linux-а (возможно, это временно - см. немного рассуждений ниже) и важно, чтобы это число было уникальным; для пользователя важно, чтобы количество этих чисел было минимальное, так как раздувание количества нод ест память в кэшах и замедляет поиск. Поэтому в тэговой фс используется двойной подход: для симлинков и структурных папок (only-files, tags и т.д.) используются используются фиксированные номера; для директорий-тэгов используются динамические номера: каждый новый запрос использует следующий номер.

ещё немного рассуждений про номер сущности/ноды

Изучая код Linux-а (помним: версия ядра 5.10) складывается впечатление, что происходит постепенный отход от номера ноды, его ценность снижается.

Например, если почитать более ранние статьи по работе с файловой системой, то можно подумать, что номер ноды это "нить Ариадны" в сумраке нагромождённых байтов: он и на файлы указывает; и выстраивает иерархию, задавая директории "." и ".."; и в информации о файле его выдают - в общем очень важной число.

Сейчас в коде ФС номер ноды становится "просто полезным числом": если придумаешь, как им пользоваться - то пользуйся; если не придумал, как пользоваться - то ... функции, которые вызывает линукс для работы виртуальной ФС, получают на вход dentry и inode (т.е. не только номер, хотя он там тоже запрятан). А далее: в dentry можно подпихнуть свои пользовательские данные или заставить линукс подменить dentry своим. Ноду вообще создаёшь сам и можешь туда натолкать своих данных. И как результат: программист, для примера, может при создании dentry и inode привязываться не к номеру, а затолкать туда структуру из десятка чисел, или вообще какой-нибудь url.

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

cd .

всё равно отлично работает. И, очевидно, никто не запрашивает у файловой системы информацию об этой директории - Linux сам всё знает.

Прим.: естественно, "всё отлично работает" относится к штатным средствам Linux-а. Если же некоторый файловый менеджер пытается в двух первых записях о содержимом директории найти имена точка и две-точки, то у него могут быть проблемы.

i_atime, i_ctime, i_mtime содержат временные метки и лучше им присвоить реальные осмысленные значения, так как файловые менеджеры обожают выводить время для файлов или делать сортировку по времени. Чтобы не завязываться на хранение времён используется текущее время. Для его установки используем функцию ядра current_time, которая принимает inode как аргумент. Необходимость использования inode связана с тем, что условная файловая система может иметь ограничения на временные метки (например, время задаётся с точностью до десятков миллисекунд или некоторые даты вообще не могут быть представлены), поэтому функция использует настройки дискретности и минимального/максимального времени, получаемые из суперблока (поля s_time_gran, s_time_min, s_time_max), до которого добирается через inode.

i_nlink это количество жёстких ссылок на файловую сущность и с этим значением есть пара тонкостей. Во-первых, для директорий количество ссылок выставляется в 2. Для этого используется функция set_nlink, т.к. ставить значение вручную не есть правильно. С симлинками же вообще лёгкий бардак: с одной стороны отдельный симлинк присутствует во многих директориях-тэгах; с другой стороны удаление симлинка из тэга не уменьшает количества ссылок, т.к. этот файл появляется в no-тэге; с третьей стороны есть место (директория only-files), где файл явно удаляется вне зависимости от количества ссылок. В общем, это одно из мест, где файловую иерархию необходимо натянуть на глобус, как сову на тэги. Поэтому для симлинков используем значение 1 и вручную управляем их удалением.

i_op - это операции над inode как узлом/нодой, т.е. без копания во внутреннем содержимом. Операций вообще много (больше 20), однако в современных версиях ядра используется очень грамотный подход, когда если операция не нужна, то её можно не объявлять и всё будет работать правильно. Поэтому обозначим только те операции, которые используются в тэговой фс:

  • lookup - функция поиска внутри директории сущности по имени;

  • symlink - создание симлинка в директории;

  • unlink - удаление симлинка из директории;

  • get_link - получение целевой ссылки из симлинка;

  • mkdir - создаём новую директорию;

  • rmdir - удаляем директорию.

Отмечу также, что эти функции не требуются для всех inode-ов: где-то нужно создавать директории (внутри директорий-тэгов), где-то этого делать нельзя (внутри директории only-files). Поэтому для разных нодов заявляем только те функции, которые ей нужны.

i_fop содержит файловые операции над сущностью, т.е. разборки с внутренним содержимым. Здесь операций также много и нам потребуется только часть:

  • open - открытие как файла;

  • release - закрытие файла;

  • llseek - перемещение по файлу;

  • iterate_shared - получение содержимого директории.

Также, как и для i_op, здесь можно операцию на заявлять, если она не требуется.

С содержимым цифровых полей разобрались, переходим к операциям над i_node и начнём с одной из важных операций - это lookup.

Lookup

Метод lookup вызывается Linux-ом, когда требуется получить inode, соответствующий заданному имени в указанной директории. Основная тонкость этой функции в том, что Linux начинает запрашивать ноды на имена даже не прочитав содержимое директории. Например, в моём случае сразу же идёт поиск файлов .Trash и .Trash-1000. В ФС таких файлов нет, никто про них ничего не говорил, однако поиск делается. Поэтому в функции lookup самое основное это определить, есть ли вообще заданное имя в директории. И если такого имени нет, то спокойно без ошибок сказать Linux-у: нет таких. Ключевое слово здесь: без ошибок, потому что если функция вернёт ошибку, то Linux будет думать, что случилось что-то непоправимое и такой файловой системой пользоваться нельзя.

В упрощённом виде реализация lookup выглядит так:

// Создадим inode
struct inode* tagfs_create_inode(struct super_block* sb, umode_t mode,
    size_t index) {
  struct inode* n = new_inode(sb);
  if (!n) {
    return NULL;
  }

  n->i_mode = mode;
  n->i_uid.val = n->i_gid.val = 0;
  n->i_blocks = 0;
  n->i_size = kInodeSize;
  n->i_ino = index;
  n->i_atime = n->i_ctime = n->i_mtime = current_time(n);
  switch (mode & S_IFMT) {
    case S_IFDIR:
      set_nlink(n, 2);
      break;
    case S_IFLNK:
      break;
  }

  return n;
}


// Привязываем dentry к симлинку
struct inode* tagfs_fills_dentry_by_linkfile_inode(struct super_block* sb,
    struct dentry* owner_de, size_t file_index) {
  struct inode* inode;

  inode = tagfs_create_inode(sb, S_IFLNK | 0777, file_index);
  if (!inode) { return NULL; }

  inode->i_op = &linkfile_iops;
  inode->i_fop = &linkfile_fops;
  d_add(owner_de, inode);
  return inode;
}


struct dentry* tagfs_allfiles_dir_lookup(struct inode* dir, struct dentry *de,
    unsigned int flags) {
  size_t ino;

  ...
  ino = tagfs_get_fileino_by_name(stor, de->d_name, NULL);
  if (ino == kNotFoundIno) {
    ...
    d_add(de, NULL);
    return NULL;
  }

  inode = tagfs_fills_dentry_by_linkfile_inode(sb, de, ino + kFSRealFilesStartIno);
  if (!inode) { return ERR_PTR(-ENOMEM); }
  return NULL;
}


const struct inode_operations tagfs_allfiles_dir_inode_ops = {
  .lookup = tagfs_allfiles_dir_lookup,
  ...
};

Разбор кода

Ещё до вызова обработчика lookup, при создании inode родительской директории (или корневой директории для всей ФС) для неё выставляются операции в tagfs_allfiles_dir_inode_ops. И когда Linux захочет найти файл в этой директории он вызовет функцию из поля lookup - в нашем случае это tagfs_allfiles_dir_lookup. При вызове функции в неё передаётся inode dir, в котором ищется имя (т.е. родительский каталог), и dentry de, к которому нужно привязать inode для найденного файла. Передаваемый dentry содержит только имя, остальное всё пусто. И функция-обработчик lookup должна по этому имени создать ноду, соответствующую объекту в файловой системе.

А ещё в lookup передаётся флаг

Вообще этот параметр не используется чуть реже, чем всегда. И по содержанию он скорее похож на костыль: информирует, для какой операции делался lookup. Т.е., например, программист может написать дополнительные условия: если мы пытаемся открыть файл, то он существует (имя есть); если же мы пытается создать файл (с тем же именем), то такого файла нет. В общем, что-то такое ... эклектичное. Кому хочется поизучать подробнее, можно поискать константу LOOKUP_CREATE и всё вокруг неё.

Функция tagfs_allfiles_dir_lookup запрашивает поиск по имени в хранилище (фактически это поиск в структурированном файле; тема хранилища будет разобрана позднее). Если симлинк не найден, то возвращается kNotFoundIno и (вспоминая ключевое слово "без ошибок") к dentry привязывается указатель на NULL: то есть как бы всё хорошо и файловая сущность найдена, только она NULL:

d_add(de, NULL);

Если же файл найден, то для него создаётся новый inode в функции tagfs_create_inode, поля заполняются значениями (как было описано выше), выставляются операции для этой ноды. И делается привязка к dentry:

inode->i_op = &linkfile_iops;
inode->i_fop = &linkfile_fops;
d_add(owner_de, inode);

Отмечу, что в обработчике lookup-а используется функция привязки d_add, которая помимо собственно связывания dentry и inode ещё добавляет ноду в кэш. Такой механизм является обязательным (ориг: This method must call d_add() to insert ...), хотя есть и другой механизм связывания - функция d_instantiate. Подробнее можно прочесть в Linux Filesystems Documentation.

Результатом вызова функции будет либо NULL если все привязки выполнены правильно (и нужно стремиться возвращать NULL), либо ошибка (когда уже ничего не работает):

return ERR_PTR(-ENOMEM);
А ещё можно вернуть ненулевой указатель

Если функция-обработчик lookup вернёт валидный ненулевой указатель, то Linux забудет про свой dentry, который передавала как аргумент, и будет использовать тот, который вернула функция. Возможно (судя по коду ядра и комментариям) это позволяет создавать в фс свою внутреннюю иерархию и "транслировать" её в Linux. Не совсем понятно зачем? Может для экономии памяти. Возможно, для ускорения поиска, например, когда у вас собственный менеджер памяти и все структуры ищутся быстрее быстрого, если они "ваши". В целом, похоже на какой-то рудимент, который очень редко используется. За возможным использованием и подробностями можно поискать применение функции d_splice_alias.

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


Заставка является изображением, сгенерированным нейросетью.

Комментарии (2)


  1. NeoCode
    12.10.2023 06:18

    Кстати, а есть в линуксовых ФС аналог ADS?


    1. Apoheliy Автор
      12.10.2023 06:18
      +1

      В linux-е есть xattr - для хранения параметров может подойти, для хранения стримов данных уже сомнительно.

      Также, если вы делаете свою файловую систему, отдельные потоки представлять как файлы. Что-то наподобие директорий процессов в /proc.