один и тот же read() работает одинаково на ext4, tmpfs и NFS — вот почемуthe same read() behaves identically on ext4, tmpfs, and NFS — here's why
Ядро само по себе не знает, что такое 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.
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) |
Связь между четырьмя объектами — не плоский список, а граф, который строится по мере того, как ядро разрешает путь компонент за компонентом: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:
Ключевой механизм — таблицы операций. Каждый объект несёт указатель на структуру функций-указателей, и именно эти таблицы заполняет модуль конкретной ФС при регистрации: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:
Оба кэша работают на уровне 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.
| команда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 |
Путь (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.
Свободная память, которую показывает 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.
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.