40. Устройство файловых систем. inode, dentry, superblock
Виртуальная файловая система (VFS) и абстракция
Когда в коде на Go вы вызываете os.Open("/etc/nginx/nginx.conf"), среда исполнения не просто «открывает файл». Она делегирует этот запрос ядру Linux, которому предстоит совершить увлекательное путешествие по графу метаданных в оперативной памяти и на физическом накопителе, чтобы сопоставить текстовую строку пути с реальными блоками данных на диске.
В операционной системе Linux этот путь полностью абстрагирован через VFS (Virtual File System).
flowchart TD
subgraph Userspace["Пользовательское пространство (Go App)"]
GoApp["os.Open(#quot;/etc/nginx/nginx.conf#quot;)"]
end
subgraph VFS_Layer["Ядро Linux: Слой VFS"]
direction TB
VFS["VFS API: open, read, write, stat, unlink"]
DentryCache["Кэш путей: dcache (dentry)"]
InodeCache["Кэш метаданных: icache (inode)"]
SuperBlock["Суперблоки смонтированных ФС (super_block)"]
end
subgraph FS_Drivers["Драйверы конкретных файловых систем"]
EXT4["ext4"]
XFS["xfs"]
Btrfs["btrfs"]
NFS["NFS (сетевая ФС)"]
TmpFS["tmpfs (in-memory)"]
end
GoApp --> VFS
VFS --> DentryCache
VFS --> InodeCache
VFS --> SuperBlock
SuperBlock --> EXT4
SuperBlock --> XFS
SuperBlock --> Btrfs
SuperBlock --> NFS
SuperBlock --> TmpFS
classDef user fill:#1e1b4b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef vfs fill:#0f172a,stroke:#6366f1,stroke-width:2px,color:#f8fafc;
classDef fs fill:#022c22,stroke:#10b981,stroke-width:2px,color:#f8fafc;
class Userspace,GoApp user;
class VFS_Layer,VFS,DentryCache,InodeCache,SuperBlock vfs;
class FS_Drivers,EXT4,XFS,Btrfs,NFS,TmpFS fs;
VFS — это универсальный полиморфный слой ядра, который позволяет вашим программам использовать абсолютно одинаковый системный API (open, read, write, stat) независимо от того, где физически расположены данные: на локальном разделе ext4, корпоративном массиве xfs, снимке btrfs, сетевом диске NFS или виртуальной памяти tmpfs.
Для реализации этой элегантной абстракции ядро Linux оперирует тремя фундаментальными структурами данных в оперативной памяти: super_block, inode и dentry. Понимание их внутреннего устройства критично для диагностики дисковых задержек, эффективной работы с hardlink/symlink и предотвращения скрытых ловушек файловой подсистемы.
Superblock: Паспорт смонтированной файловой системы
Объект superblock (суперблок) — это глобальный реестр метаданных для всей смонтированной файловой системы. Он не привязан к конкретным пользовательским файлам, а описывает параметры, геометрию и правила игры для всего дискового тома.
flowchart LR
subgraph SuperBlockStruct["struct super_block (Паспорт ФС)"]
direction TB
Magic["s_magic: 0xEF53 (Магическое число ext4)"]
BSize["s_blocksize: 4096 байт (Размер блока)"]
Counts["s_inodes_count / s_free_inodes_count"]
Limits["s_maxbytes: Максимальный размер файла"]
Ops["s_op: Таблица функций драйвера (ext4_fill_super, xfs_init)"]
end
classDef sb fill:#1e293b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
class SuperBlockStruct,Magic,BSize,Counts,Limits,Ops sb;
Ключевые поля структуры:
s_magic— уникальная числовая сигнатура файловой системы (например,0xEF53для ext4). Ядро проверяет ее при монтировании, чтобы убедиться, что тип ФС определен верно.s_blocksize— базовый размер блока (обычно 4096 байт). Любые дисковые операции ввода-вывода и выделение пространства выравниваются строго по этому размеру.s_inodes_count,s_free_inodes_count— суммарное и оставшееся количество доступных инодов. Если свободные иноды закончились, диск перестанет принимать новые файлы, даже если на нем свободно 500 ГБ места!s_maxbytes— максимально допустимый размер одного файла (определяется архитектурой адресации блоков).s_op— таблица указателей на системные функции конкретного драйвера файловой системы (ext4_fill_super,xfs_initи т.д.).
Если суперблок поврежден, ядро Linux переводит ФС в защитный режим только для чтения ro (read-only) либо наотрез отказывается монтировать раздел. В коде на Go попытка открыть файл на таком диске вернет ошибку вызова os.Stat (или os.Open) с системным кодом ошибки EBADF или EIO.
Inode: ДНК файла (и почему там нет имени)
inode (index node / information node) — это главная структура метаданных конкретного файла или каталога.
Самое главное открытие для любого программиста, изучающего Unix: имя файла в inode не хранится! Имя файла — это лишь метка на двери, а не сам файл. Это фундаментальное архитектурное отличие философии Unix от систем вроде Windows NTFS или классической FAT.
flowchart TD
subgraph InodeStruct["struct inode (Метаданные файла)"]
direction TB
InoNum["i_ino: Номер инода (#142857)"]
Mode["i_mode: Тип и права (S_IFREG, S_IFDIR, S_IFLNK)"]
Ownership["i_uid / i_gid: Владелец и группа"]
Size["i_size: Размер в байтах | i_blocks: Блоков на диске"]
Timestamps["Временные метки: i_atime, i_mtime, i_ctime"]
Links["i_nlink: Счетчик жестких ссылок (hardlinks)"]
BlockPtrs["i_block[EXT4_DIRECT_BLOCKS] / Дерево экстентов (extents)"]
end
BlockPtrs ==>|"Физические адреса LBA"| DiskBlocks["Блоки данных 4 КБ на накопителе"]
classDef ino fill:#0f172a,stroke:#6366f1,stroke-width:2px,color:#f8fafc;
classDef blk fill:#022c22,stroke:#10b981,stroke-width:2px,color:#f8fafc;
class InodeStruct,InoNum,Mode,Ownership,Size,Timestamps,Links,BlockPtrs ino;
class DiskBlocks blk;
Ключевые поля и нюансы:
i_mode: Разграничивает типы объектов VFS: обычный файл (S_IFREG), каталог (S_IFDIR) или символическая ссылка (S_IFLNK).i_nlink: Количество жестких ссылок (имен в каталогах), которые ссылаются на этот инод. Файл физически удаляется с диска только тогда, когдаi_nlinkпадает до нуля И ни один работающий процесс не держит этот файл открытым.i_mtime(Modification Time): Время последнего изменения содержимого файла (когда выполнялсяwrite).i_ctime(Change Time): Время последнего изменения метаданных файла (права, владелец, количество ссылок). Коварная ловушка: если вы выполнили командуchmod,mtimeне изменится, ноctimeобновится немедленно!
Dentry: Кэш имен и директорий
Если в иноде нет имени файла, то где же оно живет? Имя живет в каталоге, а каталог в Unix — это самый обычный файл специального типа (S_IFDIR), тело которого состоит из списка бинарных записей формата:
[длина имени][имя][inode номер].
Чтобы ядру Linux не приходилось при каждом открытии файла сканировать диск в поисках нужной строки, в памяти создается высокопроизводительный кэш — dentry (directory entry).
dentry связывает между собой имя пути и соответствующий объект inode.
flowchart LR
subgraph DentryCache["Кэш dentry (dcache в оперативной памяти)"]
direction LR
D1["/ (dentry)"] --> D2["etc (dentry)"]
D2 --> D3["nginx (dentry)"]
D3 --> D4["nginx.conf (dentry)"]
end
D4 ==>|"Связка Имя -> Inode"| InodeObj["struct inode #524101"]
classDef dentry fill:#1e1b4b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef ino fill:#022c22,stroke:#10b981,stroke-width:2px,color:#f8fafc;
class DentryCache,D1,D2,D3,D4 dentry;
class InodeObj ino;
Для ускорения доступа ядро использует структуру dcache (directory entry cache). При обращении по пути /home/user/file.txt:
- Ядро проверяет
dentryдля каталога/home. Если запись найдена в хэш-таблице (Cache Hit) — мгновенно переходит к следующему элементу пути. - Если записи нет в памяти (Cache Miss) — ядро читает блок каталога с диска, находит запись с именем
user, создает новый объектdentry, помещает его в dcache и запрашивает соответствующийinode.
Объект dentry остается в памяти до тех пор, пока на него ссылаются процессы или пока ядро не вытеснит его под давлением памяти. При удалении файла через системный вызов unlink (или команду rm) ядро удаляет запись dentry из кэша, но сам inode продолжает жить в памяти до тех пор, пока счетчик жестких ссылок i_nlink не станет равен 0 и счетчик открытых файловых дескрипторов i_count не обнулится!
Разрешение пути: Как ОС находит файл
Когда приложение на Go вызывает os.Open("/etc/nginx/nginx.conf"), под капотом выполняется системный вызов openat или openat2. Процедура разрешения пути выглядит как строгий обход графа:
flowchart TD
A["Go: os.Open(#quot;/etc/nginx/nginx.conf#quot;)"] -->|"syscall openat2"| B["Linux VFS: Path Resolution"]
B --> C{"Проверка dcache"}
C -->|"Hit"| D["Быстрый возврат inode из icache"]
C -->|"Miss"| E["Чтение дискового блока каталога"]
E --> F["Parse корневой записи /"]
F --> G["Parse каталога etc (Проверка права 'x')"]
G --> H["Parse каталога nginx (Проверка права 'x')"]
H --> I["Parse файла nginx.conf"]
I --> J["Возврат inode и валидация прав ('r')"]
J --> K["Выделение struct file и файлового дескриптора"]
classDef app fill:#1e1b4b,stroke:#38bdf8,stroke-width:2px,color:#f8fafc;
classDef vfs fill:#0f172a,stroke:#6366f1,stroke-width:2px,color:#f8fafc;
classDef step fill:#1e293b,stroke:#10b981,stroke-width:2px,color:#f8fafc;
class A app;
class B,C,D,E vfs;
class F,G,H,I,J,K step;
Критический закон Unix: Каждый переход между промежуточными каталогами требует проверки бита права на исполнение — execute (x)! Если у пользователя нет права x на родительский каталог nginx, файл nginx.conf будет абсолютно недоступен, даже если на сам файл выставлены максимальные права на чтение r (права 0666 или 0777).
Влияние на Go-приложения и производительность
В стандартной библиотеке Go взаимодействие с файловой подсистемой сосредоточено в пакете os, который является оберткой над системными вызовами openat2, statx, readlink, open и read.
Понимание механики inode и dentry спасает от классических архитектурных антипаттернов:
- Глубокая вложенность каталогов: Каждый дополнительный уровень слэша
/заставляет ядро выполнять отдельный lookup в dcache. При десятках тысяч параллельных горутин это порождает эффектdcache thrashing(постоянное вымывание кэша имен), взрывной рост задержек и конкуренцию за блокировки каталогов. - Антипаттерн «миллион файлов в одной папке»: Поиск записи в каталоге с миллионом файлов превращается в медленный перебор. При системном вызове
readdirядро считывает десятки мегабайт структуры каталога в память, перегружая CPU и Page Cache. - Hardlink vs Symlink: Системный вызов
os.Linkсоздает новую запись в каталоге (dentry), указывающую на уже существующийinode. Жесткие ссылки не могут указывать на каталоги и не могут пересекать границы файловых систем. Вызовos.Symlinkсоздает полноценный независимый файл (S_IFLNK), хранящий строку пути. Символические ссылки универсальны, но требуют повторного разрешения пути при каждом обращении.
Посмотрим, как в Go получить прямой доступ к структуре inode:
package main
import (
"fmt"
"log"
"os"
"syscall"
"time"
)
func inspectFile(path string) {
// Используем syscall.Stat для прямого доступа к метаданным inode
// без дополнительных аллокаций памяти, которые производит стандартный os.Stat
var stat syscall.Stat_t
if err := syscall.Stat(path, &stat); err != nil {
log.Fatalf("syscall.Stat failed: %v", err)
}
// Оборачиваем syscall.Stat_t в os.FileInfo для совместимости
fileInfo := os.FileInfo(&osStatWrapper{stat})
fmt.Printf("Путь к файлу: %s\n", path)
fmt.Printf("Номер Inode: %d\n", stat.Ino)
fmt.Printf("Hardlinks: %d\n", stat.Nlink)
fmt.Printf("Размер: %d байт\n", stat.Size)
fmt.Printf("Блоков (512B): %d\n", stat.Blocks)
fmt.Printf("Права доступа: %o\n", fileInfo.Mode().Perm())
fmt.Printf("Тип файла: %s\n", fileInfo.Mode().Type())
fmt.Printf("Время mtime: %v\n", time.Unix(stat.Mtim.Sec, int64(stat.Mtim.Nsec)))
fmt.Printf("Время ctime: %v\n", time.Unix(stat.Ctim.Sec, int64(stat.Ctim.Nsec)))
}
type osStatWrapper struct {
syscall.Stat_t
}
func (w *osStatWrapper) Name() string { return "" }
func (w *osStatWrapper) Size() int64 { return w.Stat_t.Size }
// Простое приведение os.FileMode(w.Stat_t.Mode) было бы ошибкой: в stat тип файла лежит в битах S_IFMT
// (S_IFDIR = 0x4000), а в os.FileMode каталог — это бит ModeDir = 1<<31, и IsDir() вернул бы false.
func (w *osStatWrapper) Mode() os.FileMode {
m := os.FileMode(w.Stat_t.Mode & 0777)
switch w.Stat_t.Mode & syscall.S_IFMT {
case syscall.S_IFDIR:
m |= os.ModeDir
case syscall.S_IFLNK:
m |= os.ModeSymlink
}
return m
}
func (w *osStatWrapper) ModTime() time.Time { return time.Unix(w.Stat_t.Mtim.Sec, int64(w.Stat_t.Mtim.Nsec)) }
func (w *osStatWrapper) IsDir() bool { return w.Mode().IsDir() }
func (w *osStatWrapper) Sys() interface{} { return &w.Stat_t }
func main() {
inspectFile("/etc/hostname")
}
Итог
- VFS абстрагирует любые файловые системы через три ключевые структуры ядра:
super_block,inodeиdentry. - Superblock — паспорт смонтированного тома, определяющий размер блока, магическое число и общее количество инодов.
- Inode — паспорта конкретного файла со всеми метаданными, правами и указателями на блоки. Имя файла в иноде принципиально отсутствует.
- Dentry — кэшированная связка имени файла с номером инода, ускоряющая разрешение путей по графу каталогов.
- Для Senior Go-разработчика: Избегайте глубоких путей и раздутых директорий с миллионами файлов во избежание деградации
dcache. Помните, что удаленный файл (unlink/rm) физически освободит место на диске только после того, как все процессы закроют его открытые дескрипторы.
Мы разобрали внутреннюю архитектуру хранения метаданных в файловых системах. Теперь настало время понять, как файловые системы обеспечивают целостность этих структур при катастрофах и сбоях питания. В следующей лекции мы детально исследуем механизм транзакций файловых систем: 41. Журналируемые файловые системы.