один и тот же read() работает одинаково на ext4, tmpfs и NFS — вот почемуthe same read() behaves identically on ext4, tmpfs, and NFS — here's why

VFS: общий интерфейс для любой файловой системыVFS: a common interface for any filesystem

Ядро само по себе не знает, что такое ext4 или Btrfs. Оно знает четыре абстрактных объекта — superblock, inode, dentry, file — и таблицы указателей на функции внутри них. Конкретная файловая система заполняет эти таблицы своими реализациями; всё, что находится выше VFS (системные вызовы open/read/write, дерево каталогов, права доступа), работает одинаково независимо от того, что физически лежит на диске.The kernel itself doesn't know what ext4 or Btrfs is. It knows four abstract objects — superblock, inode, dentry, file — and tables of function pointers inside them. A specific filesystem fills those tables with its own implementations; everything above VFS (the open/read/write syscalls, the directory tree, permission checks) works identically regardless of what's physically on disk.

Что этоWhat it is

VFS (Virtual File System) — не файловая система в обычном смысле, а слой абстракции внутри ядра, через который проходит любой доступ к файлам. Когда приложение вызывает open(), read() или mkdir(), эти вызовы попадают не напрямую в код ext4 или Btrfs, а в общий код VFS, который определяет, какая именно файловая система отвечает за нужный путь, и вызывает её конкретную реализацию через таблицу операций. Это тот же архитектурный приём, что и в объектно-ориентированном полиморфизме — только вместо vtable компилятора здесь обычные C-структуры с указателями на функции, заполняемые модулем файловой системы при инициализации.VFS (Virtual File System) isn't a filesystem in the usual sense — it's an abstraction layer inside the kernel that every file access passes through. When an application calls open(), read(), or mkdir(), those calls don't go directly into ext4 or Btrfs code — they go into common VFS code, which figures out which filesystem actually owns the requested path and calls its specific implementation through an operations table. It's the same architectural trick as object-oriented polymorphism — except instead of a compiler-generated vtable, it's plain C structs with function pointers, filled in by the filesystem module at initialization.

объектobjectчто представляетwhat it representsодна структура — сколько экземпляровone struct — how many instances
superblockсмонтированная файловая система целиком: параметры, размер блока, указатель на корневой inodean entire mounted filesystem: parameters, block size, a pointer to the root inodeодин на каждую точку монтированияone per mount point
inodeодин файл, каталог или символическую ссылку: метаданные (размер, права, владелец, время изменения) и то, где физически лежат данныеone file, directory, or symlink: metadata (size, permissions, owner, mtime) and where the data physically livesодин на каждый уникальный файл в файловой системеone per unique file in the filesystem
dentryодин компонент пути (имя ↔ inode) и связь с родительским каталогом; в отличие от inode существует только в памяти как элемент кэша поискаone path component (name ↔ inode) and a link to the parent directory; unlike an inode, it exists only in memory, as a lookup-cache entryодин на каждый когда-либо разрешённый компонент пути (пока не вытеснен из кэша)one per every path component ever resolved (until evicted from the cache)
fileодин открытый дескриptor: текущая позиция чтения/записи, режим открытия, флаги — состояние конкретного open(), а не самого файлаone open descriptor: current read/write offset, open mode, flags — the state of a specific open() call, not of the file itselfодин на каждый вызов open() (два процесса, открывшие один файл, получают два разных file, но один и тот же inode)one per open() call (two processes opening the same file get two different file objects but the same inode)

Как это устроено внутриHow it's built internally

Связь между четырьмя объектами — не плоский список, а граф, который строится по мере того, как ядро разрешает путь компонент за компонентом:The relationship between the four objects isn't a flat list — it's a graph, built as the kernel resolves a path component by component:

super_block одна на mount s_root dentry "/" d_subdirs → дети dentry "etc" d_parent ↑, d_subdirs → dentry "passwd" кэш поиска (dcache) d_inode inode i_op, i_fop, размер, права file создан на open(): f_pos, f_op открывает конкретный процесс, хранит позицию чтения/записи i_op / i_fop указывают на реализацию конкретной ФС (ext4_file_operations, ...)
superblock хранит указатель на корневой dentry; dentry-объекты образуют дерево путей и указывают на свой inode; inode хранит операции конкретной ФС (i_op/i_fop); file создаётся отдельно на каждый open() и хранит позицию чтения — сам объект «файл» с точки зрения VFS размазан по трём разным структурам с разным временем жизни.the superblock holds a pointer to the root dentry; dentry objects form the path tree and each points to its inode; the inode holds a specific filesystem's operations (i_op/i_fop); a file object is created separately on every open() and holds the read/write offset — the "file" as VFS sees it is spread across three different structs with three different lifetimes.

Ключевой механизм — таблицы операций. Каждый объект несёт указатель на структуру функций-указателей, и именно эти таблицы заполняет модуль конкретной ФС при регистрации:The key mechanism is operations tables. Every object carries a pointer to a struct of function pointers, and it's exactly these tables that a specific filesystem's module fills in when it registers:

// super_operations — операции над самой смонтированной ФС
struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_inode)(struct inode *);
    int (*write_inode)(struct inode *, struct writeback_control *);
    void (*put_super)(struct super_block *);
    int (*statfs)(struct dentry *, struct kstatfs *);
};

// inode_operations — операции над деревом каталогов (создание/поиск/удаление)
struct inode_operations {
    struct dentry *(*lookup)(struct inode *, struct dentry *, unsigned int);
    int (*create)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t, bool);
    int (*mkdir)(struct mnt_idmap *, struct inode *, struct dentry *, umode_t);
    int (*unlink)(struct inode *, struct dentry *);
};

// file_operations — операции над уже открытым файлом (I/O)
struct file_operations {
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*open)(struct inode *, struct file *);
    int (*fsync)(struct file *, loff_t, loff_t, int datasync);
};

Когда приложение вызывает read(fd, buf, count), ядро находит по дескриптору fd объект file, берёт из него f_op — указатель на таблицу file_operations, заполненную ext4 (или Btrfs, или tmpfs) при монтировании, — и вызывает f_op->read(...). Код системного вызова read() в VFS ни разу не упоминает ext4 напрямую: он просто разыменовывает указатель на функцию, которая может вести куда угодно, вплоть до сетевого запроса в случае NFS.When an application calls read(fd, buf, count), the kernel looks up the file object by descriptor fd, takes f_op from it — a pointer to the file_operations table filled in by ext4 (or Btrfs, or tmpfs) at mount time — and calls f_op->read(...). The VFS read() syscall code never mentions ext4 by name: it just dereferences a function pointer that can lead anywhere, up to and including a network request in NFS's case.

Второй ключевой механизм — кэши, без которых VFS был бы слишком медленным для практического использования:The second key mechanism is the caches, without which VFS would be too slow for practical use:

open("/etc/passwd") dentry cache (dcache) RCU lock-free lookup путь miss inode_op->lookup код конкретной ФС hit — без обращения к диску вообще page cache данные + метаданные физический блок на диске
Dentry cache избавляет от повторного разбора пути по компонентам; если путь уже разрешался — inode находится без единого обращения к диску. Page cache кэширует уже сами данные и метаданные, независимо от файловой системы: физический диск читается только при промахе обоих кэшей.The dentry cache avoids re-parsing a path component by component; if the path was already resolved, the inode is found with zero disk access. The page cache caches the actual data and metadata, independent of the filesystem: the physical disk is read only on a miss in both caches.

Оба кэша работают на уровне VFS, а не отдельно для каждой файловой системы — это значит, что горячий путь (повторное открытие того же файла, повторное чтение того же блока) почти никогда не касается кода ext4 или Btrfs напрямую и уж тем более физического диска. С Linux 2.6.38 большинство попаданий в dcache идут по RCU-пути без единой блокировки — конкурентные lookup'ы не сериализуются спинлоком на общем (кэш-попадание) пути.Both caches operate at the VFS level, not separately per filesystem — meaning the hot path (reopening the same file, rereading the same block) almost never touches ext4 or Btrfs code directly, let alone the physical disk. Since Linux 2.6.38, most dcache hits go through an RCU path with no locking at all — concurrent lookups aren't serialized by a spinlock on the common (cache-hit) path.

Как это видно на практикеHow it's visible in practice

командаcommandчто показываетwhat it shows
mountсписок активных superblock-ов: каждая строка — одна смонтированная файловая система своего типа (ext4, tmpfs, overlay, proc — все через один и тот же VFS)the list of active superblocks: each line is one mounted filesystem of its own type (ext4, tmpfs, overlay, proc — all through the same VFS)
stat file.txtполя непосредственно из inode: номер inode, размер, права, время последнего изменения — метаданные, которые существуют независимо от того, под каким именем (dentry) файл сейчас доступенfields straight out of the inode: inode number, size, permissions, mtime — metadata that exists independent of whatever name (dentry) the file is currently accessible under
cat /proc/sys/fs/dentry-stateтекущий размер dentry cache и число «неиспользуемых» (unused, но ещё не вытесненных) записей — прямое наблюдение за кэшем поиска путейthe current dentry cache size and the number of "unused" (not yet evicted) entries — a direct look at the path-lookup cache
free -hстрока «buff/cache» включает page cache — данные файлов, которые ядро держит в памяти вне зависимости от того, на какой ФС они лежат; именно поэтому свободная память в Linux «съедается» кэшем и это нормальноthe "buff/cache" line includes the page cache — file data the kernel keeps in memory regardless of which filesystem it lives on; this is why free memory on Linux gets "eaten" by cache, and that's expected
ln file.txt hardlink.txtсоздаёт второй dentry, указывающий на тот же inode — ls -i покажет одинаковый номер inode у обоих имён; наглядная демонстрация того, что имя (dentry) и файл (inode) — разные объектыcreates a second dentry pointing at the same inode — ls -i shows the same inode number for both names; a concrete demonstration that a name (dentry) and a file (inode) are different objects

Грабли и мифыGotchas & myths

«файл» в POSIX — не один объект, а как минимум триa "file" in POSIX isn't one object — it's at least three

Путь (dentry), метаданные и данные (inode) и открытый дескриптор (file) — три разных структуры с разным временем жизни. Файл можно удалить (unlink, убирает dentry), пока процесс держит его открытым (file и inode продолжают существовать, пока не закрыт последний дескриптор) — отсюда классический трюк создания «невидимого» временного файла: открыть, сразу unlink, писать и читать через уже полученный файловый дескриптор.the path (dentry), the metadata and data (inode), and the open descriptor (file) are three different structs with three different lifetimes. A file can be deleted (unlink, which removes the dentry) while a process still has it open (the file and inode keep existing until the last descriptor is closed) — hence the classic trick of creating an "invisible" temp file: open it, unlink immediately, then keep reading/writing through the already-obtained file descriptor.

page cache нельзя «выключить», и обычно не нужноthe page cache can't be "turned off", and usually shouldn't be

Свободная память, которую показывает free как «использованную» под buff/cache, доступна для вытеснения в любой момент, когда приложениям реально нужна память — ядро отдаёт её первой. Ручной сброс через echo 3 > /proc/sys/vm/drop_caches используется только для тестов производительности (замер «холодного» чтения) — в проде он не нужен и временно снижает производительность до повторного прогрева кэша.the memory free reports as "used" under buff/cache is available for eviction the moment applications actually need it — the kernel gives it up first. A manual reset via echo 3 > /proc/sys/vm/drop_caches is only for performance testing (measuring "cold" reads) — it's not needed in production and temporarily hurts performance until the cache warms back up.

не любая ФС имеет физический superblock на дискеnot every filesystem has a physical on-disk superblock

VFS-объект super_block обязателен для любой смонтированной файловой системы, но для псевдо-ФС вроде proc, sysfs или tmpfs он существует только в памяти и генерируется на лету — это не мешает им полноценно участвовать в том же дереве каталогов и тех же системных вызовах, что и ext4 на реальном диске.the VFS super_block object is mandatory for any mounted filesystem, but for pseudo-filesystems like proc, sysfs, or tmpfs it exists only in memory and is generated on the fly — that doesn't stop them from fully participating in the same directory tree and the same syscalls as ext4 on a real disk.

ТерминыTerms

superblock
VFS-объект, представляющий одну смонтированную файловую систему целиком — параметры, размер блока, указатель на корневой dentry; у реальной ФС обычно есть его физическая копия на диске.the VFS object representing one entire mounted filesystem — parameters, block size, a pointer to the root dentry; a real filesystem usually has a physical copy of it on disk.
inode
структура, представляющая один файл, каталог или символическую ссылку: метаданные (размер, права, владелец, время) и указание на то, где физически лежат данные. У файла может быть несколько имён (dentry, hardlink), но inode — один.the struct representing one file, directory, or symlink: metadata (size, permissions, owner, timestamps) and where the data physically lives. A file can have several names (dentries, hardlinks), but only one inode.
dentry
объект, связывающий один компонент пути с его inode; существует только в памяти как элемент dentry cache — на диске в таком виде не хранится, восстанавливается через lookup конкретной ФС при необходимости.an object linking one path component to its inode; exists only in memory as a dentry cache entry — not stored on disk in this form, rebuilt via a specific filesystem's lookup when needed.
dentry cache (dcache)
кэш VFS, хранящий уже разрешённые компоненты пути — избавляет от повторного обращения к конкретной файловой системе (и тем более к диску) при повторном разрешении того же пути; с 2.6.38 попадания в кэш идут по RCU-пути без блокировок.the VFS cache holding already-resolved path components — avoids re-querying a specific filesystem (let alone the disk) when the same path is resolved again; since 2.6.38, cache hits go through a lock-free RCU path.
page cache
общий для всех файловых систем кэш страниц данных и метаданных в оперативной памяти; именно из-за него «свободная» память в Linux почти всегда занята под buff/cache и мгновенно освобождается при реальной потребности.a cache of data and metadata pages shared across all filesystems; the reason "free" memory on Linux is almost always occupied by buff/cache, and instantly released when actually needed.
file_operations / inode_operations / super_operations
три таблицы указателей на функции, через которые конкретная файловая система подставляет свою реализацию: I/O над уже открытым файлом, операции над деревом каталогов, операции над самой смонтированной ФС соответственно.three tables of function pointers through which a specific filesystem plugs in its implementation: I/O on an already-open file, operations on the directory tree, and operations on the mounted filesystem itself, respectively.
mount
операция, присоединяющая корень одной файловой системы к некоторому каталогу (точке монтирования) другой — с точки зрения VFS это создание нового superblock и подшивка его dentry-дерева в общее глобальное дерево путей.the operation attaching the root of one filesystem to some directory (the mount point) of another — from VFS's point of view, this creates a new superblock and grafts its dentry tree into the overall global path tree.