Есть вещи, которые работают настолько незаметно, что их существование вообще не приходит в голову. Команда ls выдаёт список файлов на USB-флешке с FAT32 точно так же, как на сетевом NFS-томе или на системном разделе ext4. Никаких специальных ключей, никакой смены синтаксиса. Просто ls и список. За этой простотой прячется один из самых элегантных архитектурных решений в ядре Linux: Virtual File System, или VFS.
Понять VFS значит понять, как ядро умудряется говорить с десятками принципиально разных файловых систем через один и тот же интерфейс. И почему добавить поддержку новой файловой системы в Linux куда проще, чем кажется.
Зачем вообще понадобился слой абстракции
Если задуматься, файловые системы устроены принципиально по-разному. FAT32 не знает понятия "владелец файла" и хранит каталог как плоскую таблицу. ext4 ведёт журнал транзакций и держит метаданные в отдельной области inode. NFS вообще не работает с дисковыми блоками: она отправляет сетевые запросы удалённому серверу. Как одна команда read() должна работать со всем этим зоопарком?
Без промежуточного слоя ядро было бы обречено знать о каждой файловой системе всё. Разработчики ядра оказались бы заложниками бесконечных условий вида "если это FAT32, делай так; если это ext4, делай иначе". Каждое изменение в одной файловой системе рисковало сломать что-то в другой. Именно эту проблему и решает VFS.
Первая реализация виртуальной файловой системы появилась в SunOS 2.0 в 1985 году. Тогда Sun Microsystems создала её ровно с одной целью: дать системным вызовам Unix прозрачный доступ одновременно к локальной файловой системе UFS и к сетевой NFS. Идея оказалась настолько удачной, что перекочевала в System V Release 4, а затем и в Linux. С тех пор механизм вырос, оброс деталями, но суть осталась прежней: один универсальный контракт между ядром и любой файловой системой, которая хочет с ним работать.
Как системный вызов попадает к нужному драйверу
Когда процесс вызывает open("/mnt/usb/photo.jpg", O_RDONLY), он понятия не имеет, какая файловая система смонтирована в /mnt/usb. Это не его забота. Системный вызов уходит в ядро, где его подхватывает VFS.
VFS смотрит на путь, находит точку монтирования, выясняет, какой тип файловой системы там зарегистрирован, и делегирует запрос соответствующему драйверу. FAT32? Вызывается реализация open() из драйвера vfat. NFS? Запрос уйдёт в сетевой стек. ext4? Обратится к блочному устройству. Сам процесс при этом получает обычный файловый дескриптор и дальше работает с ним через те же универсальные read() и write(), не зная и не желая знать, что происходит под капотом.
Технически это реализовано через таблицы указателей на функции. Каждая файловая система при регистрации в ядре заполняет структуру file_operations: поле read указывает на свою реализацию чтения, поле write на свою запись, и так по всему списку операций. VFS не знает деталей. Он знает только, что у него есть указатель на функцию, и вызывает его. Это классический полиморфизм, реализованный на чистом C через указатели на функции.
Четыре ключевых объекта внутри VFS
VFS оперирует четырьмя фундаментальными структурами данных, каждая из которых отвечает за свой уровень абстракции.
Суперблок (struct super_block) описывает смонтированную файловую систему целиком. Это паспорт конкретного тома: размер блока, максимальный размер файла, указатель на таблицу операций над файловой системой (super_operations), ссылка на корневой dentry. Когда файловая система монтируется, её драйвер читает суперблок с диска и заполняет эту структуру. Для NFS суперблок не читается с диска, а формируется из параметров подключения к серверу.
Inode (struct inode) описывает конкретный файл или каталог. В inode живут права доступа, владелец, размер, временные метки, тип файла и, главное, указатель i_op на таблицу операций над этим файлом (inode_operations). Важная деталь: inode не содержит имени файла. Один inode может соответствовать нескольким именам. Это и есть механизм жёстких ссылок (hardlinks). FAT32, у которой нет концепции inode на диске, создаёт их динамически в памяти при монтировании, чтобы соответствовать ожиданиям VFS.
Dentry (struct dentry) связывает имя файла с его inode. Это запись в кэше каталогов. Путь /home/user/docs/report.txt порождает четыре объекта dentry: для /, для home, для user, для docs и для report.txt. Dentry существует только в памяти, на диск не пишется. Его единственная задача ускорить разрешение путей. Ядро держит активный dentry-кэш (dcache) в оперативной памяти, и именно поэтому повторные обращения к одному и тому же файлу работают значительно быстрее первого. Алгоритм поиска через dcache аналогичен DNS-кэшу: нашёл в кэше не ходи дальше, иди сразу к inode.
Файловый объект (struct file) описывает открытый файл с точки зрения конкретного процесса. Если два процесса открыли один и тот же файл, у каждого будет свой struct file со своим курсором позиции чтения, но оба будут ссылаться на один inode. Именно здесь хранится таблица file_operations с указателями на read, write, lseek и прочие операции, которые процесс вызывает через файловый дескриптор.
Все четыре объекта реализованы как C-структуры с таблицами указателей на функции. Драйвер каждой файловой системы предоставляет собственные реализации этих функций. Если какая-то операция не поддерживается, указатель на неё выставляется в NULL или на заглушку, возвращающую ENOSYS.
Псевдофайловые системы и философия "всё есть файл"
Один из самых изящных побочных эффектов архитектуры VFS состоит в том, что через неё можно представить не только данные на диске, но и абсолютно любую иерархическую информацию.
/proc смонтирована как procfs и не имеет никакого диска за собой. Каждый раз, когда процесс читает /proc/meminfo, ядро генерирует содержимое этого файла прямо в момент чтения, собирая актуальные данные из внутренних структур. Это снимок состояния системы, который никогда не устаревает. Аналогично работает и /proc/PID/ для каждого процесса: файлы внутри этого каталога не существуют на диске, они рождаются при каждом обращении.
/sys монтируется как sysfs и предоставляет доступ к объектам ядра kobject. Каждый файл в sysfs описывает ровно одно свойство устройства или драйвера. Запись в /sys/class/backlight/intel_backlight/brightness буквально меняет яркость экрана ноутбука, а чтение говорит, какая яркость выставлена сейчас. Это не магия и не хак. Это VFS, работающая так, как задумана.
tmpfs представляет файловую систему, живущую полностью в оперативной памяти. Каталог /tmp на большинстве современных дистрибутивов смонтирован именно как tmpfs. Скорость чтения и записи ограничена только пропускной способностью RAM, а не дискового контроллера. Платой за скорость служит эфемерность: после перезагрузки содержимое исчезает.
Все эти системы регистрируются в VFS по одному контракту. Ядро не делает между ними различий. Оно лишь вызывает функции из таблицы операций и получает результат.
Как VFS кэширует метаданные и зачем это важно
Работа с диском медленная. Даже на SSD латентность чтения на порядки выше латентности обращения к оперативной памяти. VFS давно это осознал и строит многоуровневое кэширование поверх драйверов файловых систем.
Dentry-кэш (dcache) держит в памяти уже разрешённые пути к файлам. Запросить одно и то же имя файла дважды: второй раз будет несравнимо быстрее первого. Inode-кэш хранит метаданные файлов, к которым недавно обращались. Страничный кэш (page cache) держит содержимое файлов постранично: если страница файла уже была прочитана с диска, следующее чтение той же страницы пойдёт из памяти.
Для управления давлением на эти кэши ядро использует переменную vm.vfs_cache_pressure из /proc/sys/vm/vfs_cache_pressure. При значении 100 ядро агрессивно освобождает кэш при нехватке памяти. При значении 0 кэш не сбрасывается никогда, но тогда система рискует остаться без свободной памяти на активно нагруженных серверах.
Когда inode помечается как "грязный" (dirty), то есть изменённый в памяти, но ещё не записанный на диск, VFS ставит его в очередь на запись. Фоновый поток ядра pdflush (в современных ядрах kworker/flush) периодически сбрасывает грязные страницы на диск. Это и есть writeback: механизм, позволяющий write() возвращать управление немедленно, не ожидая физической записи.
FUSE и файловые системы в пространстве пользователя
Классическая схема требует, чтобы драйвер файловой системы работал внутри ядра. Это означает привилегированный код, сложную разработку и потенциальный kernel panic при ошибке. FUSE (Filesystem in Userspace) разрывает эту цепочку.
Через FUSE файловая система реализуется как обычная программа в пространстве пользователя. VFS при обращении к ней не вызывает функцию из ядерного модуля, а пересылает запрос в эту программу через специальный канал. Программа обрабатывает его и возвращает ответ обратно в ядро. Накладные расходы на это переключение контекста есть, но зато разработчик пишет обычный код на C, Go или Python без страха уронить систему.
На FUSE построены десятки практичных инструментов: SSHFS монтирует удалённую директорию по SSH как локальную, EncFS шифрует файлы прозрачно для приложений, s3fs монтирует bucket облачного хранилища как обычную папку. Все они прозрачно интегрируются в дерево файловой системы Linux через тот же VFS, не требуя ни привилегий суперпользователя, ни модификаций ядра.
Почему архитектура VFS оказалась живучей
Прошло сорок лет с момента появления первой реализации виртуальной файловой системы в SunOS. Linux поддерживает сегодня десятки файловых систем одновременно. Блочные ext4 и btrfs, сетевые NFS и CIFS, псевдофайловые proc и sysfs, пользовательские через FUSE. Все они уживаются в едином дереве каталогов через один и тот же контракт с VFS.
Архитектурная ставка на полиморфизм через указатели на функции оказалась правильной. Чтобы добавить новую файловую систему, не нужно трогать ядро. Достаточно реализовать нужный набор операций и зарегистрироваться. VFS возьмёт на себя кэширование, управление памятью, очередь сброса на диск и весь остальной инфраструктурный код.
Есть в этом что-то поучительное. Хорошая абстракция не та, которая знает обо всём. Это та, которая задаёт правильный контракт и доверяет конкретным реализациям делать свою работу. VFS так и работает уже несколько десятилетий: не спрашивает, как устроена файловая система внутри, а только требует отвечать на нужные вопросы нужным образом. Это и есть причина, по которой ls одинаково работает везде.