Внутреннее устройство файловых систем¶
Файловая система превращает байты на блочном устройстве в иерархию каталогов и файлов с метаданными. От её внутренней структуры зависит почти всё: насколько быстро открывается файл в каталоге с миллионом записей, как поведёт себя система при внезапном отключении питания, сколько накладных расходов на маленькие файлы, поддерживается ли snapshot. Эта статья разбирает устройство ext4 в подробностях и кратко сравнивает его с btrfs, XFS, ZFS и tmpfs.
ext4: layout на диске¶
ext4 делит раздел на блочные группы (block groups). Каждая группа содержит копию суперблока (для устойчивости), дескрипторы групп, битмапы, таблицу inode и сами блоки данных. Идея — локализация: метаданные файла и его данные лежат в одной группе, чтобы головка диска (или контроллер SSD) меньше двигалась.
Раздел / partition
┌──────────┬───────────────────────────────────────────────────────┐
│ boot │ ext4 filesystem │
│ sector │ │
└──────────┴───────────────────────────────────────────────────────┘
│
▼
┌─────────────┬─────────────┬─────────────┬───────┬─────────────┐
│ block │ block │ block │ ... │ journal │
│ group 0 │ group 1 │ group 2 │ │ (особый │
│ │ │ │ │ inode #8) │
└──────┬──────┴─────────────┴─────────────┴───────┴─────────────┘
│
▼ одна block group (обычно 32768 блоков по 4 KB = 128 MB)
┌─────────────┬──────────────┬───────────────┬────────────┬─────────────┐
│ superblock │ group desc. │ block bitmap │ inode │ data blocks │
│ (копия) │ table │ + inode bitmap│ table │ │
│ опционально│ (копия) │ (по 1 блоку) │ (N блоков) │ (остальное) │
└─────────────┴──────────────┴───────────────┴────────────┴─────────────┘
Что лежит где:
| Структура | Что хранит |
|---|---|
| Superblock | глобальные параметры ФС: размер блока, число inode, версия, флаги, UUID, mount count |
| Group descriptor | для каждой группы: адреса bitmap'ов и inode table, число свободных блоков/inode |
| Block bitmap | один бит на блок данных в группе: занят или свободен |
| Inode bitmap | один бит на inode в группе |
| Inode table | массив inode-структур (по 256 байт каждая в ext4 по умолчанию) |
| Data blocks | сами блоки данных файлов и каталогов |
| Journal | отдельный inode (по умолчанию #8), содержащий журнал изменений |
Sparse superblock (default) — копии superblock и group descriptor table лежат не во всех группах, а только в номерах, кратных степеням 3, 5, 7 (плюс 0 и 1). Это экономит место на больших разделах.
Resize позволяет расширять ФС онлайн: ext4 хранит в superblock резерв блочных групп (s_reserved_gdt_blocks), куда
можно добавить новые group descriptors без переноса данных.
Inode: метаданные файла¶
Inode (index node) — структура размером 256 байт (в ext4; 128 в ext2/3), содержащая всё о файле, кроме имени:
┌──────────────────────────────────────────┐
│ st_mode (тип + права) │ 2 байта
│ st_uid, st_gid │ 4 байта
│ st_size (размер в байтах) │ 8 байт (нижние и верхние 32 бита)
│ st_atime, st_mtime, st_ctime, st_crtime │ каждый 8 байт (наносекунды в ext4)
│ st_nlink │ 2 байта
│ st_blocks │ 4 байта (в единицах 512 B)
│ i_block[15] │ 60 байт: extent header + extents
│ │ или (ext2/3) указатели на блоки
│ flags, generation, ACL, ... │
│ extended inode space (xattrs) │ до конца 256 байт
└──────────────────────────────────────────┘
Имя файла в inode не хранится. Оно живёт в directory entry в каталоге, где этот файл лежит. Несколько имён,
указывающих на один inode — это hard links (см. hard_and_symbolic_links.md).
Адресация данных: extents против block mapping¶
В ext2/ext3 inode хранил прямые указатели на блоки данных:
ext3 inode (15 указателей по 4 байта):
┌───────────────┐
│ block[0..11] │ ─▶ 12 прямых блоков (до 48 KB при 4 KB блоке)
│ block[12] │ ─▶ indirect block ─▶ 1024 блока (до 4 MB)
│ block[13] │ ─▶ double-indirect ─▶ 1024² блоков (до 4 GB)
│ block[14] │ ─▶ triple-indirect ─▶ 1024³ блоков (до 4 TB)
└───────────────┘
Большой файл на ext3 требовал трёх обращений к indirect-блокам прежде, чем получить адрес данных. И каждый блок данных описывался индивидуально — для 1-гигабайтного файла нужно 256K записей по 4 байта в indirect-цепочке.
ext4 заменил это на extents — записи вида (логический блок → физический блок, длина):
Extent описывает непрерывный диапазон:
логический блок 0..1023 → физический блок 50000..51023 (1024 блока подряд)
логический блок 1024..1535 → физический блок 60000..60511 (512 блоков подряд)
...
Для непрерывного файла в 4 GB достаточно одного extent (max длина — 32768 блоков, ~128 MB) вместо миллиона указателей.
В inode помещается extent tree — небольшое B+ tree, корень которого живёт в 60 байтах i_block[]:
i_block[60 bytes]
┌─────────────────────────────────────┐
│ ext4_extent_header │ 12 байт
│ eh_magic = 0xF30A │
│ eh_entries (число записей ниже) │
│ eh_depth (0 = лист, >0 = узел) │
├─────────────────────────────────────┤
│ ext4_extent / ext4_extent_idx [0] │ 12 байт
│ ext4_extent / ext4_extent_idx [1] │ 12 байт
│ ext4_extent / ext4_extent_idx [2] │ 12 байт
│ ext4_extent / ext4_extent_idx [3] │ 12 байт
└─────────────────────────────────────┘
│
▼ если eh_depth > 0
┌─────────────────────┐
│ внешний extent │ (отдельный блок ФС, не в inode)
│ block: header + │
│ больше записей │
└─────────────────────┘
ext4_extent_header — 12 байт, остаются 48 байт под четыре 12-байтовые записи. Если файл фрагментирован сильнее —
выделяется отдельный блок под extent tree, и в inode остаётся ext4_extent_idx (индексные записи).
Глубина дерева редко превышает 5 — этого хватает на терабайтные файлы. Каждый узел B+ tree содержит до сотен extent'ов.
htree: быстрый lookup в каталогах¶
Классическая ext2 хранила каталог как линейный список directory entries. Поиск файла в каталоге с миллионом записей требовал линейного сканирования в среднем 500K записей — десятки секунд на HDD.
ext3 ввёл, а ext4 унаследовал, htree (hash tree) — хешированный индекс поверх линейного хранилища:
Каталог с htree:
┌─────────────────────────────────────────────────┐
│ block 0: "." | ".." | htree root (hash index) │
├─────────────────────────────────────────────────┤
│ block 1: записи с hash 0x00000000..0x3FFFFFFF │
│ block 2: записи с hash 0x40000000..0x7FFFFFFF │
│ block 3: записи с hash 0x80000000..0xBFFFFFFF │
│ block 4: записи с hash 0xC0000000..0xFFFFFFFF │
└─────────────────────────────────────────────────┘
Lookup "file.txt":
1. hash("file.txt") = 0x5A12E7D0
2. По htree корню в block 0 находим: hash попадает в block 2
3. Внутри block 2 линейный поиск (но это уже 1 блок, а не весь каталог)
Хеш-функция (Half MD4 или Tea) выбирается при создании ФС. htree активируется автоматически, когда каталог не помещается
в один блок. С точки зрения userspace ничего не меняется — readdir(3) возвращает все записи как раньше.
Включение htree контролируется флагом dir_index суперблока. Проверить:
Delayed allocation¶
ext4 не выделяет блоки на диске сразу при write(2). Вместо этого данные кладутся в page cache как dirty, а блоки на
диске резервируются только при writeback (асинхронно, через pdflush/writeback поток, или принудительно через
fsync):
Без delayed allocation (ext3):
write(fd, buf, 4096)
↓
выделить блок на диске СЕЙЧАС ──▶ записать в page cache как dirty
↓
writeback позже сольёт грязные страницы на диск
С delayed allocation (ext4):
write(fd, buf, 4096)
↓
только пометить страницу как dirty (без выделения блока)
↓
writeback СЕЙЧАС: видит все dirty страницы файла сразу,
выделяет ОДИН непрерывный extent на всю запись
Что это даёт:
- Меньше фрагментации. Файлу, который растёт на 1 KB за раз, без delayed allocation выделяли бы по 1 KB-блоку в разных местах. С delayed allocation writeback видит уже 100 MB dirty данных и выделяет одну непрерывную область.
- Меньше работы. Если файл создан и удалён до writeback'а, блоки на диске вообще не выделялись.
- Лучше extents. Один большой extent вместо десятков мелких.
Цена — известная проблема: после write без fsync данных нет на диске. Если процесс или система упадёт между
write и writeback'ом, файл будет нулевой длины или с нулями внутри. Это поведение ext4 (как и любой ФС с writeback
cache) и оно по стандарту POSIX корректно — но иногда удивляет пользователей.
Журналирование¶
Если система упала во время записи метаданных, ФС может остаться в несогласованном состоянии: inode уже указывает на
блок, который ещё не выделен в bitmap, или наоборот. Без журнала восстановление требует полного fsck, который на
терабайтном разделе занимает часы.
ext4 решает это журналом: перед изменением метаданных запись о намерении пишется в специальный циркулярный буфер (journal). При сбое ядро при mount'е проигрывает журнал заново — все незавершённые транзакции либо доводятся до конца, либо откатываются.
Журнал — это отдельный inode (по умолчанию #8) размером обычно 128 MB. Управляется подсистемой JBD2 (Journaling Block Device 2).
Три режима журналирования:
| Режим | Что в журнал | Скорость | Безопасность |
|---|---|---|---|
data=writeback |
только метаданные | максимум | минимум |
data=ordered |
только метаданные, но порядок строгий | компромисс | хорошая |
data=journal |
метаданные и данные | минимум | максимум |
data=writeback¶
В журнал идут только метаданные (inode, bitmap, group descriptor). Данные пишутся в любой момент — до или после commit метаданных.
Транзакция:
┌──────────────────────┐
│ data на диск (1) │ ↓ может произойти позже метаданных
│ ИЛИ │
│ metadata в journal │ ↓
│ commit │ ↓
│ metadata на диск │
└──────────────────────┘
Опасность: после сбоя метаданные говорят «файл длиной 4 KB, блок 50000», но данные ещё не успели туда записаться. В блоке может оказаться мусор от прежнего файла.
data=ordered (default)¶
Данные пишутся до commit'а метаданных. В журнал идут только метаданные, но JBD2 гарантирует порядок: блок данных физически лёг на диск раньше, чем закоммитятся метаданные, ссылающиеся на этот блок.
Транзакция:
┌──────────────────────┐
│ data на диск │ ── ждём, пока не запишется
│ ↓ │
│ metadata в journal │
│ commit │
│ ↓ │
│ metadata на диск │
└──────────────────────┘
При сбое: либо транзакция вся применилась, либо вся откатилась. Мусора в новых блоках не будет. По умолчанию ext4 работает именно так.
data=journal¶
В журнал идёт всё: и метаданные, и данные. Каждый блок пишется дважды — сначала в журнал, потом в финальное место.
Транзакция:
┌──────────────────────┐
│ data в journal │
│ metadata в journal │
│ commit │
│ ─ ─ ─ ─ ─ ─ │
│ data на диск │ ← из journal в финальное место
│ metadata на диск │
└──────────────────────┘
Самый безопасный, самый медленный. Полезен для критичных приложений, где двойная запись окупается стабильностью.
Парадокс: некоторые синхронные workload'ы с data=journal оказываются быстрее, чем с ordered, потому что обе записи
журнала идут последовательно (быстрая запись на HDD), а потом writeback метаданных и данных может слиться оптимально.
Сменить режим — опция монтирования:
Другие файловые системы¶
btrfs¶
Copy-on-Write (CoW) на уровне ФС: блок никогда не перезаписывается — изменённая копия пишется в новое место, и
ссылки на неё обновляются вверх по дереву. Это даёт практически бесплатные snapshots (моментальные снимки), clone
файла (без копирования), checksum'ы на всё (включая данные), встроенный RAID 0/1/10/5/6, сжатие, дедупликацию.
CoW при изменении блока B файла:
старая версия новая версия
┌──────┐ ┌──────┐
│ root │ │ root'│ ← новый корень
└───┬──┘ └───┬──┘
│ │
┌───┴────┐ ┌───┴────┐
│ A │ B │ │ A │ B' │ ← новая копия B
└────────┘ └────────┘
│ │ │ │
▼ ▼ ▼ ▼
data data данные новый блок
Цена — фрагментация (CoW «размазывает» файл по диску с каждой правкой) и проблемы с большими long-lived базами данных
(сильно деградируют без chattr +C — отключение CoW для конкретных файлов).
XFS¶
Изначально разработана SGI для серверов и больших файлов. Использует allocation groups (аналог блочных групп ext4, но сильно крупнее — по умолчанию 1 GB и больше) и B+ trees для всех структур: extents, inodes, free space.
XFS отлично масштабируется на терабайтные и петабайтные тома, гонит на параллельных нагрузках (каждая allocation group
может обрабатываться независимо). Не поддерживает уменьшение раздела онлайн — только увеличение. Журналирует только
метаданные (аналог data=writeback).
ZFS¶
Файловая система от Sun, портирована в Linux как OpenZFS. Не входит в mainline ядро по лицензионным причинам (CDDL несовместима с GPL). CoW + integrated volume management: ZFS управляет дисками сама, без LVM.
Особенности:
- Pools объединяют несколько устройств; на пул заводятся datasets (аналог разделов).
- Snapshots через CoW, бесплатно по записи.
- Checksums на каждый блок (SHA-256 или fletcher4), автоматическое восстановление при scrub.
- L2ARC, ZIL — кеш чтения на SSD, отдельный device для журнала.
- RAID-Z — улучшенный RAID 5/6 без write hole.
Цена — память. ZFS любит RAM, рекомендуют 1 GB на TB пула.
tmpfs¶
Файловая система в RAM. Не имеет блочного устройства — данные хранятся в page cache, выгружаются в swap при нехватке памяти. Размер задаётся при монтировании:
Используется для /dev/shm (shared memory), /run, /tmp в многих дистрибутивах. Скорость — на пределе скорости RAM,
никаких syscall'ов до диска. Содержимое исчезает при перезагрузке.
Сравнительная таблица¶
| ФС | Журнал | CoW | Snapshots | Extents | Checksums data | В mainline |
|---|---|---|---|---|---|---|
| ext2 | нет | нет | нет | нет (blocks) | нет | да |
| ext3 | да | нет | нет | нет (blocks) | нет | да |
| ext4 | да | нет | нет | да | metadata only | да |
| XFS | да | нет | нет | да | metadata only | да |
| btrfs | нет (CoW) | да | да | да | да | да |
| ZFS | нет (CoW) | да | да | да | да | нет |
| tmpfs | — | — | — | — | — | да |
Диагностика ext4¶
Состояние и параметры:
tune2fs -l /dev/sda1 # superblock: размер, кол-во inode, флаги, mount count
dumpe2fs /dev/sda1 | less # полный дамп: group descriptors, bitmaps
debugfs /dev/sda1 # интерактивный debug (требует root)
Внутри debugfs:
debugfs: stat <inode> # показать структуру inode
debugfs: ls /etc # содержимое каталога
debugfs: dump <inode> /tmp/out # извлечь содержимое файла
debugfs: icheck <block> # какой inode владеет этим блоком
Фрагментация:
e4defrag -c /home/user # отчёт по фрагментации
e4defrag /home/user # дефрагментация онлайн
filefrag -v file.bin # показать extents конкретного файла
Проверка целостности:
Связанные темы¶
- Основы файловых систем — VFS, inode, суперблок, базовая модель
- Linux block layer — куда уходят writeback из page cache
- Жёсткие и символические ссылки — directory entries и
st_nlink - Права доступа и атрибуты файлов — биты в
i_mode,chattrфлаги ext4
Источники¶
- ext4 wiki — kernel.org
- "The Linux Programming Interface" — Michael Kerrisk, главы про ФС
- XFS Algorithms & Data Structures — sandeen.net
- btrfs design notes
- OpenZFS documentation
man 5 ext4,man 8 tune2fs,man 8 debugfs,man 8 e4defrag