Диски, разделы и файловые системы
В теме 2 было показано, что в Linux существует одно дерево каталогов с корнем /, что имя файла хранится в каталоге и указывает на inode, а команды du и df по-разному отвечают на вопрос о занятом месте. Там же мимоходом упоминалось монтирование: диск или флешка «подключается» к дереву в некотором каталоге. Эта тема отвечает на вопрос, откуда берётся само дерево: как физический или виртуальный диск превращается в каталоги и файлы, где хранятся inode, почему место на диске может «закончиться» при свободных гигабайтах и почему одна неверная строка в файле /etc/fstab способна остановить загрузку системы.
Изложение идёт снизу вверх, по уровням, через которые проходит каждый байт файла. Сначала рассматриваются блочные устройства и таблицы разделов MBR и GPT. Затем — файловые системы: что они хранят, как устроена ext4 и чем метка отличается от UUID. Далее разбирается монтирование и его опции, автоматическое монтирование через /etc/fstab и проверка этого файла до перезагрузки. Последние разделы посвящены проверке и восстановлению файловой системы, учёту места (df, du, inode, зарезервированные блоки), подкачке и обзору LVM, шифрования и RAID, а также особенностям дисков в WSL2.
Работать с настоящими дисками рискованно: ошибка в имени устройства уничтожает данные. Поэтому все эксперименты темы выполняются на файле-образе — обычном файле, который ядро умеет представить как диск. Примеры выполнялись в Ubuntu 22.04 под WSL2 без прав root; команды, для которых root необходим (losetup, mount блочного устройства, swapon, правка /etc/fstab), записаны с sudo, а их вывод помечен как пример вывода. Целевая среда курса — Ubuntu 24.04; отличия версий оговариваются там, где они видны.
Блочные устройства и таблицы разделов
Уровни хранения: устройство, разделы, файловая система, монтирование
Блочное устройство (англ. block device) — устройство хранения, к которому ядро обращается порциями фиксированного размера и в произвольном порядке: можно прочитать сектор 0, затем сектор 500 000, затем снова сектор 7. Минимальная адресуемая порция называется сектором (англ. sector), исторически это 512 байт. Для программ блочное устройство выглядит как очень длинный массив байт без какой-либо структуры: в нём нет ни файлов, ни каталогов. Жёсткий диск, SSD, флешка, виртуальный диск ВМ и файл-образ, подключённый через loop-устройство, — всё это блочные устройства.
Чтобы на таком массиве появились файлы, над ним надстраиваются ещё несколько уровней (рисунок). Таблица разделов (англ. partition table) делит устройство на непрерывные участки — разделы (англ. partition); каждый раздел ядро показывает как отдельное блочное устройство. Файловая система (англ. file system, ФС) — структура данных, записанная внутрь раздела: она знает, где лежат inode, каталоги и блоки данных, какие блоки свободны. Наконец, монтирование присоединяет корень файловой системы к одному из каталогов общего дерева. Уровни независимы: таблица разделов ничего не знает о файловой системе внутри раздела, а файловая система — о том, на каком смещении от начала диска она лежит.
Различие уровней объясняет многие ошибки новичков. Команда mkfs создаёт файловую систему, а не раздел; если применить её к целому диску /dev/sdb вместо раздела /dev/sdb1, таблица разделов будет затёрта. Команда sfdisk меняет таблицу, но не трогает данные внутри разделов. Таблица разделов вообще не обязательна: файловую систему можно создать прямо на целом устройстве, и, как будет видно ниже, именно так устроен корневой диск WSL2.
Устройства представлены файлами в каталоге /dev, о котором шла речь в теме 2. Первая буква в выводе ls -l для них — b (блочное устройство), а вместо размера выводятся два числа: старший (англ. major) и младший (англ. minor) номера. Старший номер указывает драйвер, младший — конкретное устройство этого драйвера:
ls -l /dev/sdd /dev/loop0 /dev/loop-control
crw------- 1 root root 10, 237 Oct 18 13:33 /dev/loop-control
brw------- 1 root root 7, 0 Oct 18 13:33 /dev/loop0
brw------- 1 root root 8, 48 Oct 18 13:33 /dev/sdd
Номер 8 принадлежит драйверу SCSI-дисков sd, номер 7 — драйверу loop-устройств. Файл /dev/loop-control символьный (буква c): через него программа losetup просит ядро выделить свободное loop-устройство. Права rw------- и владелец root показывают, почему обычный пользователь не может читать диск напрямую: иначе он прочитал бы чужие файлы в обход прав каталогов из темы 4.
Имена устройств подчиняются соглашениям, которые полезно узнавать с первого взгляда.
| Имя | Что это | Разделы |
|---|---|---|
/dev/sda, /dev/sdb | диски SATA, SAS, USB (драйвер sd), буквы по порядку обнаружения | /dev/sda1, /dev/sda2 |
/dev/nvme0n1 | NVMe-SSD: контроллер 0, пространство имён 1 | /dev/nvme0n1p1 (буква p перед номером) |
/dev/vda | виртуальный диск virtio в ВМ KVM/QEMU | /dev/vda1 |
/dev/mmcblk0 | SD-карта, eMMC | /dev/mmcblk0p1 |
/dev/loop0 | loop-устройство: файл, представленный как диск | /dev/loop0p1 при сканировании разделов |
/dev/sr0 | оптический привод | — |
Правило с буквой p простое: если имя устройства заканчивается цифрой, перед номером раздела ставится p, иначе nvme0n11 нельзя было бы отличить от одиннадцатого раздела. Подробное описание устройств и драйверов дано в справочнике Немета и соавторов 2 и на страницах руководства sd(4) и loop(4) 3.
Как увидеть устройства: lsblk, /proc/partitions, blkid
Главная команда обзора — lsblk (англ. list block devices). Она строит дерево «устройство — разделы — точки монтирования» по сведениям ядра из /sys и не требует прав root. Вот её вывод в WSL2:
lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 388.6M 1 disk
sdb 8:16 0 186M 1 disk
sdc 8:32 0 2G 0 disk [SWAP]
sdd 8:48 0 1T 0 disk /mnt/wslg/distro
/
Колонки означают: NAME — имя в /dev; MAJ:MIN — номера устройства; RM — съёмное ли устройство; SIZE — размер; RO — только для чтения (1) или нет (0); TYPE — тип (disk, part, loop, lvm, crypt, rom); MOUNTPOINTS — куда смонтировано. Здесь видно характерное для WSL2 устройство: четыре виртуальных диска без единого раздела. Диски sda и sdb — служебные образы WSL, подключённые только для чтения; sdc — файл подкачки (отметка [SWAP]); sdd размером 1 ТиБ — это файл ext4.vhdx на диске Windows, и файловая система ext4 создана на нём целиком, без таблицы разделов. Один и тот же sdd смонтирован дважды: как корень / и как /mnt/wslg/distro (это допустимо, монтирование не копирует данные). На физической машине с Ubuntu 24.04 вывод выглядит иначе — это пример вывода:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 4K 1 loop /snap/bare/5
loop1 7:1 0 74.2M 1 loop /snap/core22/1564
nvme0n1 259:0 0 476.9G 0 disk
├─nvme0n1p1 259:1 0 1G 0 part /boot/efi
└─nvme0n1p2 259:2 0 475.9G 0 part /
Строки loop принадлежат пакетам snap (тема 9): каждый snap-пакет — это файл-образ со сжатой файловой системой squashfs, смонтированный через loop-устройство. Поэтому в Ubuntu первые номера loop обычно заняты, и при подключении своего образа ядро выдаст, например, /dev/loop8.
Набор колонок задаётся опцией -o, а -f показывает сведения о файловых системах: тип, версию, метку, UUID, свободное место. Опция -o полезна в скриптах, потому что фиксирует формат вывода:
lsblk -o NAME,MAJ:MIN,SIZE,TYPE,RO,MOUNTPOINTS
lsblk -f
NAME MAJ:MIN SIZE TYPE RO MOUNTPOINTS
sda 8:0 388.6M disk 1
sdb 8:16 186M disk 1
sdc 8:32 2G disk 0 [SWAP]
sdd 8:48 1T disk 0 /mnt/wslg/distro
/
NAME FSTYPE FSVER LABEL UUID FSAVAIL FSUSE% MOUNTPOINTS
sda
sdb
sdc [SWAP]
sdd 784.4G 17% /mnt/wslg/distro
/
Колонки FSTYPE, LABEL и UUID в этой среде пусты, и это поучительно. lsblk берёт их не из ядра, а из базы udev — службы, которая при появлении устройства читает его заголовок и создаёт ссылки вида /dev/disk/by-uuid/.... В WSL2 без systemd udev не работает, каталога /dev/disk нет, а прочитать заголовок диска сам обычный пользователь не может (права rw-------). В Ubuntu 24.04 на ВМ или физической машине, а также в WSL2 с включённым systemd колонки заполнены. Если они пусты, помогает sudo lsblk -f или sudo blkid.
Самый первичный источник — файл /proc/partitions: его формирует ядро, и в нём перечислены все блочные устройства с размером в блоках по 1 КиБ.
grep -v ram /proc/partitions
major minor #blocks name
8 0 397940 sda
8 16 190508 sdb
8 32 2097156 sdc
8 48 1073741824 sdd
Число 1 073 741 824 блока по 1 КиБ — ровно 1 ТиБ. Команда blkid читает заголовки устройств и выводит найденные сигнатуры: тип ФС, метку, UUID. Ей нужны права на чтение устройства, но с файлом-образом она работает и без root — это будет использовано ниже.
Таблицы разделов MBR и GPT
Таблица разделов — небольшая структура в начале диска, которая перечисляет разделы: где каждый начинается, сколько секторов занимает и какого он типа. Существуют два формата.
MBR (англ. Master Boot Record, также «DOS-разметка») появился в IBM PC в 1983 году. Таблица занимает 64 байта в самом первом секторе диска, со смещения 446, и содержит ровно четыре записи по 16 байт; последние два байта сектора — сигнатура 55 AA. Номер начального сектора и длина хранятся в 32-битных полях, поэтому при секторе 512 байт MBR не может адресовать больше 2³² × 512 байт = 2 ТиБ. Ограничение в четыре раздела обходили так: один из четырёх объявлялся расширенным (англ. extended), а внутри него цепочкой размещались логические разделы (/dev/sda5 и далее). Тип раздела в MBR — один байт: 83 — Linux, 82 — Linux swap, 07 — NTFS, ef — системный раздел EFI.
GPT (англ. GUID Partition Table) — часть стандарта UEFI. Заголовок лежит во втором секторе (LBA 1) и начинается с сигнатуры EFI PART, за ним следуют 128 записей по 128 байт (ещё 32 сектора). Номера секторов 64-битные, так что ограничение размера диска практически снято. Каждая запись содержит GUID типа раздела, уникальный идентификатор самого раздела (PARTUUID), имя до 36 символов и атрибуты. Копия заголовка и таблицы записана в последних 33 секторах диска: если начало диска повреждено, таблица восстанавливается из копии. В первом секторе GPT-диска оставлен «защитный» MBR (англ. protective MBR) с одним разделом типа ee, который занимает весь диск. Его задача — не дать старым программам, понимающим только MBR, посчитать диск пустым и предложить его разметить.
| Свойство | MBR | GPT |
|---|---|---|
| где хранится | сектор 0 | сектора 1–33 и копия в конце диска |
| максимум разделов | 4 основных (больше — через расширенный) | 128 по умолчанию |
| максимальный размер диска | 2 ТиБ при секторе 512 байт | 8 ЗиБ, практически без ограничения |
| тип раздела | 1 байт: 83, 82, ef | GUID: 0FC63DAF-… (Linux), C12A7328-… (EFI) |
| имя и идентификатор раздела | нет (есть 4-байтовый идентификатор диска) | имя и PARTUUID у каждого раздела |
| защита от повреждения | нет | копия таблицы и контрольные суммы CRC32 |
| загрузка | BIOS (Legacy) | UEFI; BIOS — через загрузчик с особым разделом |
Новые установки Ubuntu на компьютеры с UEFI используют GPT. MBR встречается на старых машинах, флешках и в некоторых облачных образах.
Разметка файла-образа: sfdisk, fdisk, parted
Создадим «диск» размером 256 МиБ. Команда truncate из темы 2 создаёт разреженный файл: размер есть, а блоков на диске почти не выделено.
mkdir -p ~/lab-10 && cd ~/lab-10
truncate -s 256M disk.img
ls -lhs disk.img
0 -rw-r--r-- 1 student student 256M Oct 18 19:47 disk.img
Первая колонка 0 — занятое место, опция -s команды ls. Разметим образ программой sfdisk — неинтерактивным вариантом fdisk, который читает описание разделов со стандартного ввода. Это удобно: описание можно сохранить в файл и повторить. Работа с образом не требует root, потому что это обычный файл, принадлежащий пользователю.
cd ~/lab-10
cat > layout.sfdisk <<'EOF'
label: gpt
size=160MiB, type=linux, name=data
type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, name=share
EOF
sfdisk disk.img < layout.sfdisk
Checking that no-one is using this disk right now ... OK
Disk disk.img: 256 MiB, 268435456 bytes, 524288 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
>>> Script header accepted.
>>> Created a new GPT disklabel (GUID: 48368858-F186-AA49-804A-FEA8002EDB2D).
disk.img1: Created a new partition 1 of type 'Linux filesystem' and of size 160 MiB.
disk.img2: Created a new partition 2 of type 'Microsoft basic data' and of size 95 MiB.
disk.img3: Done.
New situation:
Disklabel type: gpt
Disk identifier: 48368858-F186-AA49-804A-FEA8002EDB2D
Device Start End Sectors Size Type
disk.img1 2048 329727 327680 160M Linux filesystem
disk.img2 329728 524254 194527 95M Microsoft basic data
The partition table has been altered.
Syncing disks.
Строка label: gpt выбирает формат таблицы. Каждая следующая строка описывает раздел: у первого задан размер 160 МиБ, тип linux (сокращение для GUID 0FC63DAF-8483-4772-8E79-3D69D8477DE4) и имя data; у второго размер не указан, поэтому он занимает весь остаток, а тип — «Microsoft basic data», под которым Windows и Linux ожидают FAT, exFAT и NTFS. В таблице New situation колонка Start показывает первый сектор раздела, End — последний, Sectors — длину. Первый раздел начинается не с сектора 34, где закончилась таблица, а с сектора 2048. Это выравнивание (англ. alignment): 2048 × 512 байт = 1 МиБ. Физические сектора современных дисков имеют размер 4 КиБ, страницы SSD и полосы RAID — ещё больше; если начало раздела не кратно им, каждая запись блока ФС задевает два физических блока, и скорость падает. Граница в 1 МиБ кратна всем распространённым размерам, поэтому её используют все современные программы разметки. Второй раздел заканчивается сектором 524 254, а не 524 287: последние 33 сектора заняты копией GPT.
Просмотреть таблицу без изменений можно тремя программами. Вывод fdisk -l уже встречался выше; sfdisk -d выводит таблицу в том самом формате, который sfdisk принимает на вход, — это готовая резервная копия разметки:
sfdisk -d disk.img
label: gpt
label-id: 48368858-F186-AA49-804A-FEA8002EDB2D
device: disk.img
unit: sectors
first-lba: 2048
last-lba: 524254
sector-size: 512
disk.img1 : start= 2048, size= 327680, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, uuid=DE64D989-EDA6-8C46-A1A4-D466A9913AA5, name="data"
disk.img2 : start= 329728, size= 194527, type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7, uuid=7A082637-E563-FA40-8F52-D6F0601DAE7B, name="share"
Поле uuid= у каждого раздела — это PARTUUID, идентификатор раздела в таблице GPT; его не следует путать с UUID файловой системы, который появится после mkfs. Программа parted показывает ту же таблицу в мебибайтах и уже распознаёт файловые системы внутри разделов (вывод ниже получен после того, как разделы были отформатированы):
parted -s disk.img unit MiB print
Model: (file)
Disk /home/student/lab-10/disk.img: 256MiB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:
Number Start End Size File system Name Flags
1 1.00MiB 161MiB 160MiB ext4 data
2 161MiB 256MiB 95.0MiB fat16 share msftdata
Наконец, таблицу можно увидеть «глазами» — побайтово, командой xxd из темы 3. В секторе 0 по смещению 446 (0x1be) лежит единственная запись защитного MBR с типом ee, в конце сектора — 55aa; сектор 1 начинается сигнатурой EFI PART:
xxd -s 446 -l 66 disk.img
xxd -s 512 -l 16 disk.img
000001be: 0000 0200 eeff ffff 0100 0000 ffff 0700 ................
000001ce: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000001de: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000001ee: 0000 0000 0000 0000 0000 0000 0000 0000 ................
000001fe: 55aa U.
00000200: 4546 4920 5041 5254 0000 0100 5c00 0000 EFI PART....\...
Байт ee на пятой позиции записи — тип раздела. Байты 0100 0000 — начальный сектор 1 (порядок байт младшим вперёд), ffff 0700 — длина 0x7ffff = 524 287 секторов, то есть весь диск за вычетом сектора 0. Интерактивная программа fdisk (без -l) позволяет сделать то же, что sfdisk, через меню; parted и графическая gparted умеют, кроме того, изменять размер разделов. Изменение таблицы на диске, с которого работает система, — операция, после которой ядро может не увидеть новые разделы до перезагрузки или до команды partprobe; на образе этой проблемы нет.
Файловые системы
Задача файловой системы и выбор типа
Раздел — по-прежнему массив байт. Файловая система превращает его в дерево каталогов: решает, где хранить inode и данные каждого файла, ведёт учёт свободных блоков, обеспечивает права доступа и времена, а после сбоя питания позволяет быстро вернуться к согласованному состоянию. Идея восходит к UNIX: в статье Ритчи и Томпсона 1974 года файловая система уже описана как набор inode («i-list») и блоков, а съёмные тома присоединяются к дереву системным вызовом mount 8. Linux поддерживает десятки файловых систем; список тех, что ядро знает сейчас, находится в /proc/filesystems (отметка nodev означает, что ФС не требует блочного устройства):
grep -v nodev /proc/filesystems
ext3
ext4
ext2
squashfs
vfat
fuseblk
udf
xfs
Выбор ФС определяется задачей. В таблице перечислены типы, которые встречаются чаще всего.
| ФС | Где применяется | Особенности |
|---|---|---|
| ext4 | корневой раздел Ubuntu и Debian, «по умолчанию» | журнал, зрелые инструменты e2fsprogs, число inode задаётся при создании; можно увеличить и уменьшить (уменьшить — только размонтированной) |
| xfs | серверы с большими файлами, RHEL | хорошо масштабируется, inode выделяются динамически; уменьшить размер нельзя |
| btrfs | Fedora, openSUSE, NAS | копирование при записи, снимки, контрольные суммы данных, встроенный RAID |
| vfat (FAT32) | флешки, системный раздел EFI, обмен с Windows и камерами | нет прав и владельцев Unix, файл не больше 4 ГиБ − 1 байт |
| exfat | большие флешки и SD-карты | файлы больше 4 ГиБ, прав Unix тоже нет |
| ntfs | диски Windows | в Linux — через драйвер ntfs3 или FUSE |
| tmpfs | /run, /dev/shm | живёт в оперативной памяти, исчезает при перезагрузке |
| squashfs | snap-пакеты, установочные образы | сжатая, только для чтения |
Отдельный класс — файловые системы FUSE (англ. Filesystem in Userspace): код ФС работает не в ядре, а в обычной программе, а ядро пересылает ей запросы. Так работает sshfs из темы 11 и fuse2fs, который будет использован ниже, чтобы монтировать ext4-образ без root. В выводе findmnt такие ФС имеют тип вида fuse.ext4 или fuse.sshfs.
Устройство ext4: суперблок, группы блоков, таблица inode, журнал
Создадим ext4 в файле размером 160 МиБ — того же размера, что первый раздел образа. Вывод mkfs.ext4 не зависит от того, что именно форматируется — раздел, loop-устройство или файл, поэтому его удобно изучать на файле. Опция -L задаёт метку.
cd ~/lab-10
truncate -s 160M data.img
mkfs.ext4 -L data data.img
mke2fs 1.46.5 (30-Dec-2021)
Discarding device blocks: done
Creating filesystem with 40960 4k blocks and 40960 inodes
Filesystem UUID: 4ce5796f-8d28-4f4a-8856-b7af0935f9d2
Superblock backups stored on blocks:
32768
Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done
Программа сообщила главное. Раздел разбит на 40 960 блоков по 4 КиБ: блок (англ. block) — единица выделения места в ФС, файл всегда занимает целое число блоков. Создано 40 960 inode: по умолчанию mke2fs выделяет один inode на каждые 16 КиБ, а для маленьких ФС (до 512 МиБ) — на каждые 4 КиБ, что задано в /etc/mke2fs.conf. Это число определяет, сколько файлов вообще можно создать, и у ext4 его нельзя изменить отдельно: оно меняется только вместе с размером ФС при resize2fs, когда добавляются или убираются группы блоков со своими таблицами inode. Сгенерирован UUID. Копия суперблока записана в блок 32 768. Создан журнал из 4096 блоков (16 МиБ).
Устройство ext4 показано на рисунке. Пространство делится на группы блоков (англ. block groups) по 32 768 блоков (128 МиБ при блоке 4 КиБ); у нашей ФС две группы, вторая неполная. В начале лежат суперблок, дескрипторы групп, битовые карты занятости блоков и inode, таблица inode, затем журнал и блоки данных.
Суперблок (англ. superblock) — паспорт файловой системы: размер блока, число блоков и inode, сколько из них свободно, UUID, метка, состояние, время последней проверки. Он начинается со смещения 1024 байта от начала раздела и опознаётся по «магическому числу» 0xEF53. Без суперблока ФС нельзя открыть, поэтому его копии хранятся в начале некоторых групп. Основные поля выводят dumpe2fs -h и равнозначная ей tune2fs -l 3:
dumpe2fs -h data.img
dumpe2fs 1.46.5 (30-Dec-2021)
Filesystem volume name: data
Last mounted on: <not available>
Filesystem UUID: 4ce5796f-8d28-4f4a-8856-b7af0935f9d2
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 40960
Block count: 40960
Reserved block count: 2048
Overhead clusters: 6702
Free blocks: 34252
Free inodes: 40949
First block: 0
Block size: 4096
...
Inodes per group: 20480
Inode blocks per group: 1280
...
Reserved blocks uid: 0 (user root)
Reserved blocks gid: 0 (group root)
First inode: 11
Inode size: 256
...
Journal inode: 8
...
Total journal size: 16M
Вывод сокращён. Разберём поля, которые понадобятся дальше. Filesystem features — включённые возможности: has_journal — есть журнал, extent — файлы описываются экстентами, metadata_csum — метаданные защищены контрольными суммами, flex_bg — служебные структуры нескольких групп собраны вместе. В Ubuntu 24.04 (e2fsprogs 1.47) набор по умолчанию тот же: появившиеся в этой версии возможности orphan_file и metadata_csum_seed Debian и Ubuntu по умолчанию не включают, чтобы ФС оставалась читаемой загрузчиком GRUB. Inode count и Free inodes: из 40 960 inode занято 11 — первые десять зарезервированы для служебных целей (First inode: 11), одиннадцатый занят каталогом lost+found. Reserved block count: 2048 — ровно 5 % от 40 960: эти блоки может занимать только root, о них пойдёт речь в разделе о месте на диске. Overhead clusters: 6702 — блоки под служебные структуры (таблицу inode, битовые карты, журнал), недоступные для данных. Inode size: 256 — каждый inode занимает 256 байт; 20 480 inode группы занимают 20 480 × 256 / 4096 = 1280 блоков, что совпадает со строкой Inode blocks per group.
Полный вывод dumpe2fs без -h перечисляет группы блоков, и именно из него взяты номера на рисунке:
dumpe2fs data.img 2>/dev/null | sed -n '/^Group 0/,$p'
Group 0: (Blocks 0-32767) csum 0x2779 [ITABLE_ZEROED]
Primary superblock at 0, Group descriptors at 1-1
Reserved GDT blocks at 2-20
Block bitmap at 21 (+21), csum 0x366f4561
Inode bitmap at 23 (+23), csum 0x93fe98af
Inode table at 25-1304 (+25)
26081 free blocks, 20469 free inodes, 2 directories, 20469 unused inodes
Free blocks: 6687-32767
Free inodes: 12-20480
Group 1: (Blocks 32768-40959) csum 0x921f [INODE_UNINIT, ITABLE_ZEROED]
Backup superblock at 32768, Group descriptors at 32769-32769
Reserved GDT blocks at 32770-32788
Block bitmap at 22 (bg #0 + 22), csum 0x8408f7c5
Inode bitmap at 24 (bg #0 + 24), csum 0x00000000
Inode table at 1305-2584 (bg #0 + 1305)
8171 free blocks, 20480 free inodes, 0 directories, 20480 unused inodes
Free blocks: 32789-40959
Free inodes: 20481-40960
Битовая карта (англ. bitmap) — массив битов, по одному на блок или inode: 1 — занят, 0 — свободен. Чтобы найти свободный блок, ФС ищет нулевой бит в карте, а не просматривает весь диск. Запись Inode table at 1305-2584 (bg #0 + 1305) показывает действие flex_bg: таблица inode группы 1 физически лежит в группе 0, рядом с таблицей группы 0. Так служебные данные собраны в одном месте, и головке жёсткого диска не нужно метаться между группами. Reserved GDT blocks — запас под дескрипторы новых групп на случай, если ФС будет увеличена командой resize2fs.
Теперь свяжем это с inode из темы 2. Там говорилось, что inode хранит метаданные и «адреса блоков». Посмотрим на настоящий inode. Утилита debugfs — отладчик ext2/3/4, который читает и (с опцией -w) изменяет структуры ФС напрямую, без монтирования. Запишем в ФС два файла (как смонтировать образ без root, будет показано в разделе о монтировании) и спросим debugfs о содержимом корневого каталога и об одном из файлов:
cd ~/lab-10
debugfs -R 'ls -l /' data.img
debugfs -R 'stat /blob.bin' data.img
debugfs 1.46.5 (30-Dec-2021)
2 40755 (2) 1000 1000 4096 18-Oct-2026 19:50 .
2 40755 (2) 1000 1000 4096 18-Oct-2026 19:50 ..
11 40700 (2) 0 0 16384 18-Oct-2026 19:50 lost+found
12 100644 (1) 1000 1000 26 18-Oct-2026 19:50 hello.txt
13 100644 (1) 1000 1000 1048576 18-Oct-2026 19:50 blob.bin
debugfs 1.46.5 (30-Dec-2021)
Inode: 13 Type: regular Mode: 0644 Flags: 0x80000
Generation: 2020918291 Version: 0x00000000:00000100
User: 1000 Group: 1000 Project: 0 Size: 1048576
File ACL: 0
Links: 1 Blockcount: 2048
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x6ad4f8d8:36f59400 -- Sun Oct 18 19:50:32 2026
atime: 0x6ad4f8d8:28e79850 -- Sun Oct 18 19:50:32 2026
mtime: 0x6ad4f8d8:36f59400 -- Sun Oct 18 19:50:32 2026
crtime: 0x6ad4f8d8:28e79850 -- Sun Oct 18 19:50:32 2026
Size of extra inode fields: 32
Inode checksum: 0x293d867c
EXTENTS:
(0-255):6688-6943
Корневой каталог ФС всегда имеет inode 2, поэтому у . и .. в корне номер 2. Каталог — это файл с записями «номер inode — имя» (первая и последняя колонки). Inode 13 хранит тип, права 0644, владельца 1000, размер, число ссылок Links: 1 и четыре времени, включая время создания crtime, которого нет в классическом выводе stat. Последняя строка — экстент (англ. extent): логические блоки файла 0–255 лежат в физических блоках 6688–6943, подряд. Вместо перечисления 256 номеров блоков ext4 хранит одну запись «начало и длина»; для большого нефрагментированного файла это экономит и место, и время. Blockcount: 2048 — те же 1 МиБ в единицах по 512 байт, как поле Blocks у stat из темы 2. Журнал также является файлом с inode 8, только без имени в каталоге: debugfs -R 'stat <8>' data.img показывает его экстент (0-4095):2591-6686.
Журнал и согласованность метаданных
Создание одного файла меняет несколько структур в разных местах диска: битовую карту inode, сам inode, запись в каталоге, битовую карту блоков, счётчики в дескрипторе группы. Если питание пропадёт посередине, ФС окажется в противоречивом состоянии: например, inode помечен занятым, но ни один каталог на него не ссылается, или блок принадлежит двум файлам. Раньше после каждого сбоя запускалась полная проверка fsck, которая обходила все inode и на больших дисках занимала часы.
Журнал (англ. journal) решает эту задачу так же, как журнал транзакций в базе данных. Прежде чем изменить структуры на их постоянных местах, ext4 записывает описание всей группы изменений (транзакции) в журнал и отмечает её завершённой. Только после этого изменения переносятся на место. При сбое возможны два случая: транзакция не была завершена в журнале — её просто отбрасывают, и ФС остаётся в прежнем согласованном состоянии; транзакция завершена — при следующем монтировании её «проигрывают» (англ. replay) заново. В обоих случаях проверка занимает секунды.
По умолчанию ext4 журналирует только метаданные, а данные файлов записывает до фиксации транзакции метаданных — режим data=ordered. В WSL2 он передан при монтировании явно и потому виден в опциях корневой ФС в /proc/mounts (вывод приведён ниже в разделе о монтировании); на обычной системе режим по умолчанию в списке опций не выводится. Режим гарантирует, что после сбоя inode не будет указывать на блоки с «мусором» от чужих удалённых файлов, но не гарантирует, что в файле окажутся последние записанные данные: то, что программа не успела сбросить на диск вызовом fsync, может пропасть 6. Журнал — не резервная копия: он защищает структуру ФС, а не содержимое файлов от удаления; резервное копирование разбирается в теме 16.
Сравнительное устройство файловых систем: ext4 против NTFS
Архитектурные различия между семейством Unix/Linux и Windows наиболее наглядно проявляются в структуре их основных файловых систем — ext4 и NTFS (New Technology File System) 9:
- Концепция метаданных и Master File Table (MFT):
- В ext4 (POSIX): существует строгое разделение структур. Инода (inode фиксированного размера, обычно 256 байт) хранит числовой идентификатор, тип, атрибуты прав доступа (
rwx), временные метки и ссылки на блоки/экстенты данных. Имя файла не является атрибутом inode: каталоги представляют собой отдельные файлы со списком записей вида «имя — номер inode». Это позволяет создавать жёсткие ссылки (hard links), когда несколько имён в разных каталогах ссылаются на один и тот же номер inode. - В NTFS (Windows NT): используется концепция, схожая с реляционной базой данных. Все метаданные и структуры тома хранятся в специальном индексном файле — главной таблице файлов (Master File Table,
$MFT) 9. Каждая запись MFT имеет фиксированный размер (как правило, 1024 байта) и представляет собой последовательность независимых типизированных атрибутов переменной длины.
- В ext4 (POSIX): существует строгое разделение структур. Инода (inode фиксированного размера, обычно 256 байт) хранит числовой идентификатор, тип, атрибуты прав доступа (
- Типизированные атрибуты и резидентные данные (Resident Data):
- Вся информация о файле в NTFS разбита на атрибуты со стандартными идентификаторами:
$STANDARD_INFORMATION(0x10): стандартные временные метки создания, модификации и доступа (MACB с точностью до 100 наносекунд), флаги DOS (Read-Only, Hidden, System, Archive);$FILE_NAME(0x30): имя файла в кодировке UTF-16, размер и ссылка на запись родительского каталога (у файла может быть несколько атрибутов$FILE_NAME, например длинное имя и короткое имя 8.3);$SECURITY_DESCRIPTOR(0x50): списки управления доступом (ACL) и идентификаторы безопасности (SID) владельца;$DATA(0x80): собственно полезное содержимое файла.
- Резидентные файлы: если размер файла невелик (обычно до ~600–700 байт), его содержимое помещается непосредственно внутрь записи MFT в тело атрибута
$DATA. Для такого файла дисковые кластеры вообще не выделяются! Это исключает фрагментацию и обеспечивает мгновенный доступ к содержимому за одно дисковое чтение записи MFT. - Нерезидентные файлы: когда размер файла превышает свободное место в записи MFT, атрибут
$DATAпереводится в режим non-resident. Вместо сырых данных в атрибуте сохраняется список цепочек кластеров диска — data runs (смещение первого кластера и количество кластеров), что концептуально эквивалентно экстентам ext4.
- Вся информация о файле в NTFS разбита на атрибуты со стандартными идентификаторами:
- Множественные потоки данных (Data Streams):
- В ext4 каждый файл представляет собой один линейный байтовый поток. Любые метаданные за пределами стандартного заголовка сохраняются в расширенных атрибутах
xattr(ограниченных размером блока ФС, обычно 4 КиБ). - В NTFS файл может содержать произвольное количество независимых потоков данных — альтернативных потоков данных (Alternate Data Streams, ADS) 9. Основной безымянный поток обозначается как
file.txt::$DATA, а дополнительный именованный поток — какfile.txt:stream_name:$DATA. Каждый поток может быть нерезидентным и хранить гигабайты информации, оставаясь невидимым для классических утилит командной строки без специальных ключей (например,dir /R).
- В ext4 каждый файл представляет собой один линейный байтовый поток. Любые метаданные за пределами стандартного заголовка сохраняются в расширенных атрибутах
- Механизмы журналирования и отслеживания транзакций:
- В ext4: за согласованность отвечает модуль ядра
jbd2(Journaling Block Device 2). Он журналирует блоки ФС (метаданные по умолчанию в режимеordered) в кольцевом буфере журнала (inode 8). - В NTFS: применяется двухзвенная модель:
- Служебный журнал транзакций
$LogFileреализует протокол Write-Ahead Logging по алгоритму ARIES (redo/undo операции) для гарантированного отката незавершённых системных транзакций при сбое питания. - Журнал изменений номеров обновления USN (
$Extend\$UsnJrnl— Update Sequence Number Journal) фиксирует высокоуровневые факты создания, удаления и переименования файлов, позволяя поисковым индексаторам, антивирусным сканерам и службам резервного копирования мгновенно узнавать об изменениях на томе без полного обхода дерева каталогов.
- Служебный журнал транзакций
- В ext4: за согласованность отвечает модуль ядра
| Характеристика | ext4 (Linux) | NTFS (Windows NT) |
|---|---|---|
| Базовая единица метаданных | Inode (обычно 256 байт) | Запись Master File Table ($MFT, 1024 байта) |
| Хранение имени файла | В записях блоков каталогов | Внутри атрибута $FILE_NAME в записи MFT |
| Мелкие файлы (< 600 байт) | Выделяется блок (4 КиБ) (inline_data опциональна) | Резидентные данные прямо в записи $DATA в MFT |
| Адресация блоков данных | Дерево экстентов (Htree/Extents) | Цепочки кластеров (Data Runs) |
| Потоки данных | Один поток + расширенные атрибуты xattr | Неограниченное число потоков (ADS) |
| Журналирование метаданных | Кольцевой журнал транзакций jbd2 | Журнал транзакций $LogFile (алгоритм ARIES) |
| Журнал событий для приложений | Сторонний аудит (auditd, fanotify, inotify) | Встроенный журнал изменений USN ($UsnJrnl) |
Сравнительная организация структур метаданных ext4 и NTFS наглядно показана на рисунке ниже.
Метка, UUID и имя устройства
У одного и того же раздела есть несколько имён, и важно понимать, какое из них стабильно. Отформатируем разделы внутри образа (в лабораторной работе это делается через loop-устройство, здесь результат показан командой blkid с опцией -O — смещением раздела от начала образа в байтах):
cd ~/lab-10
blkid -p -O $((2048*512)) disk.img
blkid -p -O $((329728*512)) disk.img
blkid disk.img
disk.img: LABEL="data" UUID="2485c78f-dbb9-4283-b32d-37b47e459c51" VERSION="1.0" BLOCK_SIZE="4096" TYPE="ext4" USAGE="filesystem"
disk.img: SEC_TYPE="msdos" LABEL_FATBOOT="SHARE" LABEL="SHARE" UUID="8BF5-92F6" VERSION="FAT16" BLOCK_SIZE="512" TYPE="vfat" USAGE="filesystem"
disk.img: PTUUID="48368858-f186-aa49-804a-fea8002edb2d" PTTYPE="gpt"
Сами файловые системы созданы прямо в образе без loop-устройства: mkfs.ext4 принимает смещение раздела опцией -E offset=1048576 и размер последним аргументом, mkfs.vfat — опцией --offset 329728 (в секторах); в лабораторной работе удобнее и нагляднее форматировать разделы loop-устройства. Без смещения blkid видит только таблицу разделов (PTTYPE="gpt" и её идентификатор PTUUID). Внутри разделов найдены ext4 с меткой data и vfat с меткой SHARE. Заметим разницу в форматах UUID: у ext4 это 128-битное число из 32 шестнадцатеричных цифр, у FAT — 32-битный серийный номер тома вида 8BF5-92F6.
Имеется четыре способа сослаться на ФС, и в таблице они сравниваются.
| Способ | Пример | Откуда берётся | Когда меняется |
|---|---|---|---|
| имя устройства | /dev/sdb1 | порядок обнаружения дисков ядром | при добавлении диска, смене порта, другом порядке загрузки драйверов |
| UUID файловой системы | UUID=2485c78f-… | генерируется mkfs | только при новом mkfs или tune2fs -U |
| метка ФС | LABEL=data | задаётся mkfs -L или tune2fs -L | по желанию администратора; может совпасть у двух дисков |
| PARTUUID | PARTUUID=de64d989-… | генерируется при создании раздела в GPT | при пересоздании таблицы разделов |
Имя устройства — самое ненадёжное. Буквы sda, sdb раздаются в порядке, в котором драйверы обнаружили диски; подключение флешки, второго диска или перенос диска на другой порт SATA меняет порядок. Если в настройке записано /dev/sdb1, после такой перемены на /dev/sdb1 может оказаться совсем другая ФС — и в неё начнут писать данные. UUID хранится внутри самой ФС и «переезжает» вместе с ней. Метка удобна для человека (LABEL=backup), но её уникальность никто не проверяет: две флешки с меткой USB неразличимы. Поэтому в /etc/fstab рекомендуется UUID. Служба udev создаёт для каждого способа каталог символических ссылок: /dev/disk/by-uuid/, /dev/disk/by-label/, /dev/disk/by-partuuid/, /dev/disk/by-id/, — и ls -l /dev/disk/by-uuid показывает, на какое устройство сейчас указывает каждый UUID.
Метку и UUID ext4 меняет tune2fs: tune2fs -L archive data.img задаёт новую метку, tune2fs -U random — новый UUID. Последнее нужно, например, после побайтового копирования раздела: две ФС с одинаковым UUID на одной машине — источник путаницы, ведь система не знает, какую из них монтировать.
Монтирование
Точка монтирования и команды mount, umount, findmnt
Монтирование (англ. mounting) — присоединение корневого каталога файловой системы к существующему каталогу общего дерева. Этот каталог называется точкой монтирования (англ. mount point). После монтирования путь /mnt/lab10/data ведёт уже не в каталог корневой ФС, а в корень (inode 2) другой ФС. Каталоги для временного монтирования администратором по стандарту FHS — /mnt, для съёмных носителей — /media 7. Монтирование блочного устройства требует root, так как меняет дерево для всех пользователей. Доступное введение в монтирование носителей и команды mount, umount, fsck есть в книге Шоттса, в главе о носителях данных 1.
Общий вид команды: mount [-t тип] [-o опции] источник точка. В лабораторной работе образ подключается к loop-устройству и монтируется так; это пример вывода, номер устройства у читателя будет другим:
cd ~/lab-10
sudo losetup -fP --show disk.img
sudo mkdir -p /mnt/lab10/data /mnt/lab10/share
sudo mount /dev/loop3p1 /mnt/lab10/data
sudo mount -o uid=$(id -u),gid=$(id -g),umask=022 /dev/loop3p2 /mnt/lab10/share
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS /mnt/lab10/data /mnt/lab10/share
/dev/loop3
TARGET SOURCE FSTYPE OPTIONS
/mnt/lab10/data /dev/loop3p1 ext4 rw,relatime
TARGET SOURCE FSTYPE OPTIONS
/mnt/lab10/share /dev/loop3p2 vfat rw,relatime,uid=1000,gid=1000,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro
Опция -f команды losetup берёт первое свободное loop-устройство, -P просит ядро прочитать таблицу разделов образа (появляются loop3p1 и loop3p2), --show печатает имя выделенного устройства. Тип ФС mount определяет сам по сигнатуре, поэтому -t можно не указывать. Для vfat заданы uid, gid и umask: в FAT нет владельцев и прав Unix, и драйвер «назначает» их всем файлам сразу, иначе все файлы принадлежали бы root. Если источник — обычный файл с ФС без таблицы разделов, mount сам создаёт loop-устройство: sudo mount data.img /mnt/x равносильно sudo mount -o loop data.img /mnt/x.
Команда findmnt выводит смонтированные ФС деревом и отвечает на вопрос «на какой ФС лежит этот путь». Без аргументов она печатает всё дерево; опция -t фильтрует по типу, --target находит ФС, содержащую указанный путь:
findmnt -t ext4,9p
findmnt --target /home
TARGET SOURCE FSTYPE OPTIONS
/ /dev/sdd ext4 rw,relatime,discard,errors=remount-ro,data=ordered
├─/usr/lib/wsl/drivers drivers 9p ro,nosuid,nodev,noatime,aname=drivers;fmask=222;dmask=222,cache=5,access=client,msize=65536,trans=fd,rfd=8,wfd=8
├─/mnt/wslg/distro /dev/sdd ext4 ro,relatime,discard,errors=remount-ro,data=ordered
├─/mnt/c C:\ 9p rw,noatime,aname=drvfs;path=C:\;uid=1000;gid=1000;symlinkroot=/mnt/,cache=5,access=client,msize=65536,trans=fd,rfd=5,wfd=5
└─/mnt/d D:\ 9p rw,noatime,aname=drvfs;path=D:\;uid=1000;gid=1000;symlinkroot=/mnt/,cache=5,access=client,msize=65536,trans=fd,rfd=5,wfd=5
TARGET SOURCE FSTYPE OPTIONS
/ /dev/sdd ext4 rw,relatime,discard,errors=remount-ro,data=ordered
Источник сведений для findmnt — файл /proc/self/mounts, который ядро формирует для каждого процесса; /proc/mounts и /etc/mtab — ссылки на него. Формат строки — шесть полей, как в /etc/fstab, о котором речь ниже:
grep -E ' / | /mnt/c ' /proc/mounts
/dev/sdd / ext4 rw,relatime,discard,errors=remount-ro,data=ordered 0 0
C:\134 /mnt/c 9p rw,noatime,aname=drvfs;path=C:\;uid=1000;gid=1000;symlinkroot=/mnt/,cache=5,access=client,msize=65536,trans=fd,rfd=5,wfd=5 0 0
Пробелы и обратные косые черты в именах записываются в /proc/mounts восьмеричными кодами (\134 — это \), чтобы не ломать разбиение строки на поля. Опция errors=remount-ro корневой ФС означает: при обнаружении ошибки в структурах ФС ядро переведёт её в режим только для чтения, чтобы не усугубить повреждение. Отключение выполняет umount — с точкой монтирования или устройством в качестве аргумента (sudo umount /mnt/lab10/data). Перед отключением ядро сбрасывает на диск все изменения из кэша, поэтому извлекать флешку без umount нельзя.
Без root смонтировать ext4-образ позволяет fuse2fs (пакет fuse2fs): код ext4 работает в пользовательском процессе, а ядро через FUSE передаёт ему запросы. Корневой каталог новой ФС по умолчанию принадлежит root, и обычный пользователь не сможет в нём писать; это решает опция mkfs.ext4 -E root_owner=UID:GID:
cd ~/lab-10
mkfs.ext4 -q -L data -E root_owner=$(id -u):$(id -g) data.img
mkdir -p mnt/data
fuse2fs data.img mnt/data
findmnt mnt/data
df -hT mnt/data
TARGET SOURCE FSTYPE OPTIONS
/home/student/lab-10/mnt/data data.img fuse.ext4 rw,nosuid,nodev,relatime,user_id=1000,group_id=1000
Filesystem Type Size Used Avail Use% Mounted on
data.img fuse.ext4 150M 17M 126M 12% /home/student/lab-10/mnt/data
Отключается такая ФС командой fusermount -u mnt/data. Здесь есть ловушка, найденная при подготовке темы: fusermount -u возвращает управление раньше, чем процесс fuse2fs дописал суперблок. Если сразу после неё запустить tune2fs или e2fsck на том же образе, две программы одновременно пишут в один файл, и суперблок портится (Superblock checksum does not match superblock). Перед работой с образом нужно убедиться, что процесс завершился: pgrep -x fuse2fs не должен ничего выводить. Числа df под fuse2fs немного отличаются от монтирования ядром: fuse2fs относит журнал к занятому месту (Used 17M — это в основном журнал 16 МиБ), а ядро исключает его из размера ФС.
Опции монтирования
Опции передаются через -o списком через запятую. Часть опций понимает сам VFS — общий слой ядра для всех ФС, — часть относится к конкретному драйверу. В таблице приведены самые употребительные.
| Опция | Назначение |
|---|---|
ro, rw | только чтение или чтение и запись |
noexec | запрет запуска программ с этой ФС |
nosuid | игнорировать биты setuid и setgid (тема 4) |
nodev | не считать файлы устройств устройствами |
noatime, relatime | не обновлять время доступа вовсе или обновлять, только если оно старше времени изменения или старше суток (по умолчанию relatime) |
defaults | набор rw,suid,dev,exec,auto,nouser,async для fstab |
noauto | не монтировать по mount -a и при загрузке |
nofail | не считать ошибкой отсутствие устройства при загрузке (fstab) |
uid=, gid=, umask= | владелец и права для ФС без прав Unix (vfat, exfat) |
errors=remount-ro | при ошибке ФС перейти в режим только для чтения (ext4) |
remount | изменить опции уже смонтированной ФС без отключения |
Опции noexec, nosuid и nodev — средство защиты. Каталоги, куда может писать любой пользователь, — /tmp, /dev/shm, флешки — часто монтируют с ними, чтобы злоумышленник не мог положить туда программу с битом setuid или файл устройства, дающий доступ к диску. Пример виден в этой же среде: findmnt -t tmpfs показывает /run/lock с опциями rw,nosuid,nodev,noexec,noatime. Однако noexec — не абсолютный барьер. Проверим это на tmpfs.
Все примеры с tmpfs в этом и следующих разделах (noexec, скрытый файл, «target is busy», удалённый открытый файл) выполнены без sudo — в отдельном пространстве имён unshare -rm (тема 12), где обычный пользователь может монтировать tmpfs; имя пользователя в выводе заменено на student. У читателя, выполняющего команды через sudo, вывод будет отличаться в мелочах: в опциях tmpfs не будет uid=1000,gid=1000, зато в Ubuntu 24.04 обычно добавится inode64; родительский каталог .. в ls -la будет принадлежать student, а не root; номера PID и устройства (0,71) будут другими.
cd ~/lab-10 && mkdir -p mp
sudo mount -t tmpfs -o size=8M,noexec tmp1 mp
printf '#!/bin/sh\necho привет\n' > mp/hello.sh
chmod +x mp/hello.sh
./mp/hello.sh; echo "код: $?"
sh mp/hello.sh; echo "код: $?"
findmnt -o TARGET,FSTYPE,OPTIONS mp
sudo umount mp
bash: ./mp/hello.sh: Permission denied
код: 126
привет
код: 0
TARGET FSTYPE OPTIONS
/home/student/lab-10/mp tmpfs rw,noexec,relatime,size=8192k,uid=1000,gid=1000
Право x у файла есть, но ядро отказывает в exec с кодом 126, знакомым по теме 5. При этом sh mp/hello.sh работает: запускается /bin/sh с корневой ФС, а скрипт для него — просто читаемый файл. Значит, noexec защищает от случайного или автоматического запуска, а против интерпретаторов бесполезен.
Опция noatime ускоряет работу: при каждом чтении файла ядру не приходится записывать в inode новое время доступа. Компромисс relatime обновляет его, только если оно старше времени изменения или прошли сутки; так продолжают работать программы, которым важно, читался ли файл после изменения (например, почтовые клиенты).
Файлы, скрытые под точкой монтирования
Точка монтирования — обычный каталог, и в нём могут лежать файлы. Что с ними происходит при монтировании? Они не удаляются и не перемещаются: путь просто начинает вести в другой корень (рисунок). Как только ФС отключена, старое содержимое снова видно.
Проверим это на tmpfs:
cd ~/lab-10
echo 'я лежу в каталоге mp на диске' > mp/hidden.txt
ls mp
sudo mount -t tmpfs -o size=16M demo mp
ls -la mp
sudo umount mp
ls mp
hidden.txt
total 4
drwxrwxrwt 2 root root 40 Oct 18 19:51 .
drwxr-xr-x 4 root root 4096 Oct 18 19:51 ..
hidden.txt
После mount каталог mp пуст: в нём корень новой tmpfs с правами drwxrwxrwt (по умолчанию tmpfs создаётся с правами 1777, как /tmp). После umount файл hidden.txt на месте. Это частая причина загадок. Если флешка не смонтировалась, а программа записала данные в /media/backup, данные попали на корневую ФС; при следующем успешном монтировании они «исчезнут», но место на корневом разделе останется занятым, и du его не найдёт. Увидеть скрытое содержимое без отключения ФС можно, смонтировав родительскую ФС ещё раз в другое место опцией --bind (связанное монтирование, англ. bind mount, делает одно поддерево видимым в двух местах):
cd ~/lab-10 && mkdir -p peek
sudo mount -t tmpfs -o size=16M demo mp
sudo mount --bind ~/lab-10 peek
ls peek/mp
sudo umount peek mp
hidden.txt
Через peek виден каталог mp корневой ФС «под» tmpfs. Тот же приём применяют, чтобы найти пропавшее место на корневом разделе: sudo mount --bind / /mnt/root и затем du по /mnt/root покажет всё, что скрыто под точками монтирования.
«target is busy»: кто держит файловую систему
Ядро не позволит отключить ФС, если ею пользуется хотя бы один процесс: у него открыт файл на этой ФС, или его текущий каталог (тема 7) находится внутри неё. Иначе процесс остался бы с дескриптором, указывающим «в никуда». Воспроизведём ситуацию: процесс sleep запускается с текущим каталогом внутри точки монтирования.
cd ~/lab-10
sudo mount -t tmpfs -o size=16M demo mp
(cd mp && exec sleep 300) &
sudo umount mp; echo "код umount: $?"
umount: /home/student/lab-10/mp: target is busy.
код umount: 32
Найти виновника помогают две программы. fuser -vm путь перечисляет процессы, использующие ФС, на которой лежит путь; буква в колонке ACCESS показывает способ: c — текущий каталог, f — открытый файл, e — исполняемый файл, r — корневой каталог процесса, m — отображённый в память файл. lsof путь выводит открытые файлы и каталоги с номерами дескрипторов (колонка FD: cwd — текущий каталог, 3r — дескриптор 3 открыт на чтение).
fuser -vm ~/lab-10/mp
lsof ~/lab-10/mp
USER PID ACCESS COMMAND
/home/student/lab-10/mp: root kernel mount /home/student/lab-10/mp
student 17276 ..c.. sleep
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sleep 17276 student cwd DIR 0,71 40 1 mp
Оба вывода указывают на процесс 17276 с текущим каталогом внутри ФС. Правильный порядок действий: понять, что это за процесс, и завершить его штатно (kill с сигналом TERM, тема 7) или, если это своя оболочка, выполнить cd в другой каталог. В примере достаточно kill 17276, после чего sudo umount mp завершается с кодом 0. Самая частая причина в учебной работе — собственная оболочка, оставшаяся в /mnt/lab10/data после cd. Существуют и силовые варианты: fuser -km путь посылает SIGKILL всем процессам, использующим ФС, а umount -l (англ. lazy) немедленно убирает ФС из дерева, но отключает её по-настоящему лишь тогда, когда последний процесс её отпустит. Оба приёма опасны: первый убивает процессы без возможности сохранить данные, второй скрывает проблему — устройство остаётся занятым, и извлечённая «отключённая» флешка может оказаться недописанной.
Автоматическое монтирование: /etc/fstab
Шесть полей строки fstab
Монтирование командой mount действует до перезагрузки. Чтобы ФС подключалась при загрузке, её описывают в файле /etc/fstab (англ. file systems table). Каждая непустая строка, не начинающаяся с #, — одна ФС из шести полей, разделённых пробелами или табуляциями; формат описан в fstab(5) 3. Так выглядит файл, созданный установщиком Ubuntu 24.04 на компьютере с UEFI (пример вывода; UUID у каждой установки свои):
# /etc/fstab: static file system information.
#
# Use 'blkid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point> <type> <options> <dump> <pass>
# / was on /dev/nvme0n1p2 during curtin installation
/dev/disk/by-uuid/0c9d1e57-3b2a-4f4e-9f61-7f0b5a2c1d84 / ext4 defaults 0 1
# /boot/efi was on /dev/nvme0n1p1 during curtin installation
/dev/disk/by-uuid/6A2C-91F3 /boot/efi vfat defaults 0 1
/swap.img none swap sw 0 0
Поля по порядку:
- Что монтировать:
UUID=…,LABEL=…,PARTUUID=…, путь к устройству (/dev/disk/by-uuid/…— та же ссылка на UUID через udev) или, для виртуальных ФС, произвольное имя (tmpfs). - Куда: абсолютный путь к существующему каталогу; для подкачки —
none. - Тип ФС:
ext4,vfat,tmpfs,swap;auto— определить по сигнатуре. - Опции через запятую, без пробелов;
defaults, если особых нет. - dump: флаг для древней программы резервного копирования
dump; сегодня всегда0. - pass: порядок проверки
fsckпри загрузке:1— корневая ФС,2— остальные дисковые ФС после корня,0— не проверять (tmpfs, vfat-флешки, сетевые ФС, подкачка). Установщик Ubuntu, как видно в примере выше, ставит1и для/boot/efi; это работает (такая ФС проверяется в первой очереди вместе с корнем), ноfstab(5)рекомендует для некорневых ФС значение2.
Учебная строка для раздела образа выглядит так:
UUID=2485c78f-dbb9-4283-b32d-37b47e459c51 /mnt/lab10/data ext4 defaults,noatime,nofail 0 2
Пробелы внутри поля недопустимы: точку монтирования /mnt/my disk записывают как /mnt/my\040disk — тот же восьмеричный код, что и в /proc/mounts. Пятое и шестое поля можно опустить (будут нули), но лучше писать их явно.
Почему UUID и что делает nofail
Представим сервер с системным диском /dev/sda и диском для резервных копий /dev/sdb1, записанным в fstab по имени. Администратор подключил ещё один диск, и после перезагрузки порядок обнаружения изменился: новый диск стал /dev/sdb, а диск копий — /dev/sdc. По строке /dev/sdb1 /backup ext4 defaults 0 2 система смонтирует в /backup чужую ФС или не найдёт её вовсе. С записью UUID=… ФС будет найдена по идентификатору внутри неё, на каком бы устройстве она ни оказалась. Поэтому установщик Ubuntu и пишет /dev/disk/by-uuid/….
Теперь о том, что происходит, если ФС из fstab не найдена при загрузке. В Ubuntu fstab читает systemd: генератор systemd-fstab-generator превращает каждую строку в юнит типа .mount (юниты — тема 9), имя которого получается из пути: /mnt/lab10/data → mnt-lab10-data.mount. Все такие юниты по умолчанию требуются целью local-fs.target, а она — нормальной загрузкой. Если устройство с нужным UUID не появилось, systemd ждёт его 90 секунд (сообщение A start job is running for /dev/disk/by-uuid/…), затем юнит завершается ошибкой, local-fs.target не достигается, и система переходит в аварийный режим (англ. emergency mode) 4. На экране — пример вывода:
You are in emergency mode. After logging in, type "journalctl -xb" to view
system logs, "systemctl reboot" to reboot, "systemctl default" or "exit"
to boot into default mode.
Cannot open access to console, the root account is locked.
See sulogin(8) man page for more details.
Press Enter to continue.
Последние строки особенно неприятны: в Ubuntu у root нет пароля (тема 4), поэтому войти в аварийную консоль нельзя, и нажатие Enter возвращает к тому же сообщению. Выход — загрузиться иначе: в меню GRUB нажать e и добавить к строке ядра fstab=no (генератор проигнорирует fstab, это описано в systemd-fstab-generator(8)) или init=/bin/bash, затем перемонтировать корень на запись (mount -o remount,rw /) и исправить файл. На ВМ можно также подключить её диск к другой машине. На удалённом сервере без консоли ошибка в fstab означает поездку в серверную.
Опция nofail меняет «требуется» на «желательно»: если устройства нет, юнит не запускается, но загрузка продолжается. Её нужно ставить на все ФС, без которых система способна работать: съёмные и дополнительные диски, разделы для резервных копий, сетевые ресурсы. Опция x-systemd.device-timeout=10s сокращает ожидание устройства, а x-systemd.automount вообще не монтирует ФС при загрузке: systemd создаёт на её месте «ловушку» (юнит .automount) и монтирует ФС при первом обращении к каталогу — удобно для редко используемых и сетевых дисков. Опции с префиксом x-systemd. понимает только systemd; mount их игнорирует.
Юниты, созданные из fstab, видны командой systemctl list-units -t mount (пример вывода):
UNIT LOAD ACTIVE SUB DESCRIPTION
-.mount loaded active mounted Root Mount
boot-efi.mount loaded active mounted /boot/efi
mnt-lab10-data.mount loaded active mounted /mnt/lab10/data
После правки fstab генератор нужно перезапустить командой sudo systemctl daemon-reload, иначе systemd продолжит пользоваться старой версией; mount в Ubuntu 24.04 напоминает об этом сообщением mount: (hint) your fstab has been modified, but systemd still uses the old version; use 'systemctl daemon-reload' to reload. Юнит .mount можно написать и вручную в /etc/systemd/system, но для обычных дисков fstab проще и привычнее.
Проверка fstab до перезагрузки
Правило: после каждой правки fstab — проверка, и только потом перезагрузка. Порядок такой.
- Резервная копия:
sudo cp /etc/fstab /etc/fstab.bak. - Статическая проверка:
sudo findmnt --verifyразбирает fstab и для каждой строки проверяет, существует ли точка монтирования, найдено ли устройство по UUID или метке, известен ли ядру тип ФС и совпадает ли он с типом на устройстве. Опция--tab-file файлпроверяет произвольный файл, а не/etc/fstab,--verboseвыводит и успешные проверки. - Пробное монтирование:
sudo systemctl daemon-reload, затемsudo mount -aмонтирует всё, что описано в fstab, кромеnoautoи уже смонтированного. Если команда завершилась без сообщений, аfindmntпоказывает ФС на месте, строка работает.
Посмотрим, что ловит findmnt --verify. Ниже проверяется учебный файл с несколькими ошибками; источниками служат файлы-образы, что позволяет запустить проверку без root (отсюда предупреждения non-bind mount source ... is a directory or regular file, которых для настоящих устройств не будет):
cd ~/lab-10
cat test.fstab
findmnt --verify --verbose --tab-file test.fstab
echo "код: $?"
/home/student/lab-10/data.img /home/student/lab-10/mp ext4 defaults,nofail 0 2
/home/student/lab-10/data.img /home/student/lab-10/mp ext5 defaults 0 2
/home/student/lab-10/share.img /home/student/lab-10/mp ext4 defaults 0 2
/home/student/lab-10/data.img home/student/lab-10/mp ext4 default,noatime 0 2
0 parse errors, 3 errors, 8 warnings
/home/student/lab-10/mp
[W] target specified more than once
[W] target specified more than once
[ ] target exists
[ ] userspace options: nofail
[W] non-bind mount source /home/student/lab-10/data.img is a directory or regular file
[ ] FS type is ext4
/home/student/lab-10/mp
[W] target specified more than once
[ ] target exists
[W] non-bind mount source /home/student/lab-10/data.img is a directory or regular file
[W] ext5 seems unsupported by the current kernel
[E] ext5 does not match with on-disk ext4
/home/student/lab-10/mp
[ ] target exists
[W] non-bind mount source /home/student/lab-10/share.img is a directory or regular file
[E] ext4 does not match with on-disk vfat
home/student/lab-10/mp
[E] unreachable on boot required target: No such file or directory
[ ] VFS options: noatime
[ ] FS options: default
[W] non-bind mount source /home/student/lab-10/data.img is a directory or regular file
[ ] FS type is ext4
код: 1
Отметка [E] — ошибка, [W] — предупреждение, пустые скобки — успешная проверка. Найдены: несуществующий тип ext5, несовпадение типа ext4 с настоящей vfat на устройстве, относительный путь точки монтирования (ему соответствует несуществующий каталог). Код возврата 1 позволяет использовать проверку в скриптах. Для строк, где устройство не найдено по UUID, выводится [E] unreachable on boot required source: UUID=… — это самая опасная ошибка, ведущая к аварийному режиму, если у строки нет nofail.
Но не менее важно, чего findmnt --verify не заметил. Опечатка default вместо defaults отнесена к «FS options» — опциям, которые передаются драйверу ФС; findmnt не знает, какие опции понимает драйвер ext4, и считает строку корректной. Драйвер же откажется монтировать с неизвестной опцией, и без nofail загрузка остановится. Текст ошибки зависит от версии mount: util-linux 2.37 и 2.39 (Ubuntu 22.04 и 24.04) пишет общее wrong fs type, bad option, bad superblock on …, а более новые версии, работающие через новый интерфейс монтирования ядра, — сообщение вида fsconfig system call failed: ext4: Unknown parameter 'default'. В первую очередь стоит читать сообщение самого mount; если оно общее, причину уточняет журнал ядра (sudo dmesg | tail), где драйвер ext4 называет неизвестную опцию. Такие ошибки находит только пробное sudo mount -a. Поэтому проверка состоит из двух шагов, и ни один не заменяет другой.
В WSL2 последствия ошибки мягче: fstab обрабатывается при запуске дистрибутива (параметр mountFsTab в секции [automount] файла /etc/wsl.conf, по умолчанию включён), а при включённом systemd — ещё и генератором юнитов; ошибка проявится сообщениями при старте, а не аварийной консолью 5. Однако привычка проверять fstab до перезагрузки нужна именно для серверов и ВМ, где цена ошибки высока.
Проверка, восстановление и учёт места
fsck и e2fsck: только на размонтированной ФС
Программа fsck (англ. file system check) — общая оболочка, которая по типу ФС вызывает конкретную программу: для ext2/3/4 — e2fsck, для vfat — fsck.vfat. Она проходит по всем структурам ФС и сверяет их между собой: каждый занятый в битовой карте блок должен принадлежать ровно одному inode, каждый inode с ненулевым числом ссылок — иметь столько же записей в каталогах, и так далее. Главное правило: проверять можно только размонтированную ФС (или смонтированную только для чтения). Смонтированная ФС меняется ядром прямо во время проверки, и «исправления» e2fsck на основе устаревшей картины разрушат её. e2fsck сам проверяет это и отказывается работать (пример вывода: /dev/loop3p1 is mounted. и e2fsck: Cannot continue, aborting.). Корневую ФС проверяют при загрузке до монтирования на запись — для этого служит шестое поле fstab.
Проверим чистую ФС. Опция -f заставляет проверить даже ФС с состоянием clean, -n отвечает «нет» на все предложения исправить — безопасный режим «только посмотреть»:
cd ~/lab-10
e2fsck -fn data.img; echo "код: $?"
e2fsck 1.46.5 (30-Dec-2021)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
data: 13/40960 files (0.0% non-contiguous), 6965/40960 blocks
код: 0
Пять проходов — это пять уровней согласованности: inode и их блоки; записи каталогов; связность дерева (каждый каталог достижим из корня); счётчики ссылок; итоговые счётчики групп и суперблока. Итоговая строка: занято 13 из 40 960 inode и 6965 блоков. Код возврата e2fsck — битовая маска, его стоит проверять в скриптах.
| Код | Смысл |
|---|---|
| 0 | ошибок нет |
| 1 | ошибки найдены и исправлены |
| 2 | исправлены, нужна перезагрузка (проверялась смонтированная корневая ФС) |
| 4 | ошибки остались неисправленными |
| 8 | ошибка выполнения (например, ФС не открылась) |
| 16 | ошибка использования: неверные опции командной строки |
| 32 | проверка прервана пользователем |
Теперь создадим повреждение. Командой debugfs -w удалим запись каталога hello.txt, не освободив inode, — именно так выглядит файл, «потерявший имя» после сбоя. Работаем с копией:
cd ~/lab-10
cp data.img broken.img
debugfs -w -R 'unlink /hello.txt' broken.img
e2fsck -fy broken.img; echo "код: $?"
e2fsck 1.46.5 (30-Dec-2021)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Unattached inode 12
Connect to /lost+found? yes
Inode 12 ref count is 2, should be 1. Fix? yes
Pass 5: Checking group summary information
data: ***** FILE SYSTEM WAS MODIFIED *****
data: 13/40960 files (0.0% non-contiguous), 6965/40960 blocks
код: 1
На проходе 4 найден inode 12, на который не ссылается ни один каталог. Удалить его — значит потерять данные, поэтому e2fsck по умолчанию «подвешивает» его в каталог lost+found с именем по номеру inode; затем исправляется счётчик ссылок. Вот зачем mkfs создаёт lost+found заранее: в момент восстановления выделять новые блоки под каталог рискованно. Имя файла утеряно навсегда, но содержимое на месте:
debugfs -R 'cat /lost+found/#12' broken.img
debugfs 1.46.5 (30-Dec-2021)
Северный узел
Опция -y отвечает «да» на все вопросы; -p (англ. preen) исправляет только безопасные ошибки без вопросов — так fsck работает при загрузке.
Резервный суперблок
Что если испорчен сам суперблок? Затрём его нулями (1024 байта со смещения 1024) в копии образа:
cd ~/lab-10
cp data.img nosb.img
dd if=/dev/zero of=nosb.img bs=1024 seek=1 count=1 conv=notrunc status=none
dumpe2fs -h nosb.img
dumpe2fs 1.46.5 (30-Dec-2021)
dumpe2fs: Bad magic number in super-block while trying to open nosb.img
Couldn't find valid filesystem superblock.
Опция conv=notrunc команды dd важна: без неё dd обрезал бы файл до записанной длины. Номера резервных суперблоков сообщила mkfs.ext4 при создании (Superblock backups stored on blocks: 32768). Если вывод не сохранён, их можно узнать командой mke2fs -n с теми же параметрами: ключ -n означает «ничего не записывать, только показать». Это надёжно, лишь если параметры (размер, размер блока) совпадают с исходными; и, конечно, без -n та же команда уничтожит ФС. Сам e2fsck при испорченном основном суперблоке автоматически пробует резервные (сообщение Superblock invalid, trying backup blocks...) и обычно находит их без подсказки; явно указывать резервный суперблок ключом -b нужно, если автоматический поиск не сработал — например, при нестандартном размере блока или повреждении всей первой группы. Покажем явный вариант: -b — номер блока суперблока, -B — размер блока.
e2fsck -fy -b 32768 -B 4096 nosb.img; echo "код: $?"
e2fsck 1.46.5 (30-Dec-2021)
Pass 1: Checking inodes, blocks, and sizes
Pass 2: Checking directory structure
Pass 3: Checking directory connectivity
Pass 4: Checking reference counts
Pass 5: Checking group summary information
Free blocks count wrong for group #0 (26081, counted=25824).
Fix? yes
Free blocks count wrong (34252, counted=33995).
Fix? yes
Free inodes count wrong for group #0 (20469, counted=20467).
Fix? yes
Free inodes count wrong (40949, counted=40947).
Fix? yes
Padding at end of inode bitmap is not set. Fix? yes
Block bitmap differences: Group 0 block bitmap does not match checksum.
FIXED.
data: ***** FILE SYSTEM WAS MODIFIED *****
data: 13/40960 files (0.0% non-contiguous), 6965/40960 blocks
код: 1
Сообщения о «неверных счётчиках» объяснимы: резервный суперблок записан при mkfs и не обновляется при каждой записи, поэтому в нём 40 949 свободных inode — столько было до появления hello.txt и blob.bin. e2fsck пересчитал реальные значения (40 947) по битовым картам и записал исправленный основной суперблок. Повторная проверка e2fsck -fn nosb.img проходит без замечаний. Важный вывод: резервные суперблоки спасают структуру ФС, а не данные, и e2fsck -y на серьёзно повреждённом диске может «исправить» его до потери файлов. С ценным диском сначала снимают побайтовую копию (dd или ddrescue) и экспериментируют с ней.
df и du: два способа считать место
В теме 2 du и df использовались как два способа узнать занятое место. Теперь ясно, почему они могут расходиться. du (англ. disk usage) обходит дерево каталогов и суммирует блоки файлов, до которых ведут имена. df (англ. disk free) не смотрит на файлы вовсе: он спрашивает у ФС счётчики из суперблока через системный вызов statfs 6. Расхождение возникает в трёх случаях: файлы скрыты под точкой монтирования (рассмотрено выше), место занято служебными структурами и резервом (рассмотрено ниже) и — самый частый на практике — файл удалён, но ещё открыт.
Напомним тему 2: файл удаляется, когда счётчик ссылок становится нулевым. Точнее, ядро освобождает inode и блоки, когда выполнены два условия: нет ни одного имени и ни один процесс не держит файл открытым. Команда rm убирает только имя. Если файл журнала держит открытым работающая служба, после rm место не освободится, пока служба не закроет дескриптор (рисунок).
Воспроизведём ситуацию на tmpfs размером 64 МиБ:
cd ~/lab-10
sudo mount -t tmpfs -o size=64M logs mp
head -c 40M /dev/zero > mp/app.log
tail -f mp/app.log > /dev/null &
pid=$!
rm mp/app.log
df -h mp; du -sh mp
ls -l /proc/$pid/fd/3
lsof -nP +L1 mp
Filesystem Size Used Avail Use% Mounted on
logs 64M 40M 24M 63% /home/student/lab-10/mp
0 mp
lr-x------ 1 student student 64 Oct 18 20:03 /proc/11994/fd/3 -> /home/student/lab-10/mp/app.log (deleted)
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
tail 11994 student 3r REG 0,70 41943040 0 2 /home/student/lab-10/mp/app.log (deleted)
df показывает 40 МиБ занятыми, du — ноль. В /proc/PID/fd (тема 7) дескриптор 3 процесса tail по-прежнему указывает на файл с пометкой (deleted). Опция +L1 команды lsof выбирает открытые файлы с числом ссылок меньше одного, то есть именно удалённые; колонка NLINK равна 0, SIZE/OFF — 41 943 040 байт. Так за минуту находится «пропавшее» место на сервере.
Освободить место можно двумя путями. Правильный — заставить процесс закрыть файл: перезапустить службу (systemctl restart, тема 9) или послать ей сигнал, по которому она переоткрывает журналы. Аварийный — обрезать файл через ссылку в /proc, не трогая процесс: : > /proc/11994/fd/3. Перенаправление > (тема 5) открывает файл с обнулением длины, и блоки освобождаются сразу:
: > /proc/$pid/fd/3
df -h mp
Filesystem Size Used Avail Use% Mounted on
logs 64M 0 64M 0% /home/student/lab-10/mp
Эксперимент завершается командами kill $pid и sudo umount mp. Вывод из этой истории: журнал работающей службы не удаляют командой rm, а очищают (truncate -s 0 файл или : > файл) либо поручают ротацию logrotate, который после переименования журнала сигналом просит службу открыть новый файл.
Исчерпание inode при свободном месте
Как было сказано выше, число inode в ext4 задаётся при mkfs и не меняется отдельно от размера ФС. Каждый файл, каталог и символическая ссылка занимают inode, даже если они пустые. Значит, ФС с миллионами крошечных файлов (кэш сессий веб-приложения, очередь почты, миниатюры картинок) может исчерпать inode задолго до места. Проверим на образе 32 МиБ, созданном с обычной для больших ФС плотностью — один inode на 16 КиБ (опция -i), и смонтированном через fuse2fs:
cd ~/lab-10
truncate -s 32M tiny.img
mkfs.ext4 -q -i 16384 -L tiny -E root_owner=$(id -u):$(id -g) tiny.img
mkdir -p mnt/tiny && fuse2fs tiny.img mnt/tiny
df -h mnt/tiny; df -i mnt/tiny
for i in $(seq 1 3000); do : > mnt/tiny/f$i || { echo "остановились на файле $i"; break; }; done
Filesystem Size Used Avail Use% Mounted on
tiny.img 32M 4.1M 26M 14% /home/student/lab-10/mnt/tiny
Filesystem Inodes IUsed IFree IUse% Mounted on
tiny.img 2048 11 2037 1% /home/student/lab-10/mnt/tiny
bash: mnt/tiny/f2038: No space left on device
остановились на файле 2038
Опция -i команды df показывает inode вместо блоков: Inodes — всего, IUsed — занято, IFree — свободно. 32 МиБ / 16 КиБ = 2048 inode, 11 уже заняты служебными, поэтому создалось ровно 2037 файлов, и на 2038-м ядро вернуло ошибку ENOSPC с тем же текстом «No space left on device», что и при нехватке места. Посмотрим на ФС после этого:
df -h mnt/tiny; df -i mnt/tiny
echo test > mnt/tiny/one-more.txt; echo "код: $?"
echo data >> mnt/tiny/f1; echo "код дописывания: $?"
Filesystem Size Used Avail Use% Mounted on
tiny.img 32M 4.1M 26M 14% /home/student/lab-10/mnt/tiny
Filesystem Inodes IUsed IFree IUse% Mounted on
tiny.img 2048 2048 0 100% /home/student/lab-10/mnt/tiny
bash: mnt/tiny/one-more.txt: No space left on device
код: 1
код дописывания: 0
Числа Size и Used получены под fuse2fs: при монтировании ядром (sudo mount tiny.img) Used будет почти нулевым, а Size — около 28 МиБ, потому что ядро исключает журнал и служебные структуры из размера ФС; 4,1 МиБ у fuse2fs — это в основном журнал. В обоих случаях ФС почти пуста, но создать файл нельзя. Дописать в существующий файл можно: inode для этого не нужен. Отсюда правило диагностики: при сообщении «No space left on device» смотреть и df -h, и df -i. Найти каталог с миллионами файлов помогает du --inodes -x / | sort -n | tail (опция --inodes считает inode вместо блоков, -x не переходит на другие ФС). Решения: удалить ненужные мелкие файлы, перенести их на другую ФС или пересоздать ФС с большим числом inode (mkfs.ext4 -i 4096 или -N число; опция -T news или -T small выбирает готовый профиль из /etc/mke2fs.conf). У xfs и btrfs inode выделяются динамически, и эта проблема у них почти не возникает.
Для сравнения — корневая ФС WSL2:
df -i /
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sdd 67108864 718684 66390180 2% /
67 миллионов inode на 1 ТиБ — это и есть один inode на 16 КиБ.
Зарезервированные блоки ext4
Сравним столбцы df -h / для корневой ФС WSL2: Size 1007G, Used 172G, Avail 785G. Сумма Used и Avail — 957 ГиБ, а не 1007. Пропавшие 50 ГиБ — это зарезервированные блоки (англ. reserved blocks): по умолчанию mkfs.ext4 отдаёт 5 % блоков в распоряжение root (Reserved blocks uid: 0 (user root) в выводе dumpe2fs). Обычные пользователи не могут их занять, и для них ФС «заполнена», когда Avail дошёл до нуля. Причины две. Во-первых, системные службы, работающие от root (журналы, systemd-journald, sshd), продолжают работать на «полном» диске, и администратор может войти и навести порядок. Во-вторых, ext4 при почти полном диске сильно фрагментирует файлы, а запас снижает фрагментацию.
Тот же расчёт по точным числам в блоках по 4 КиБ:
df -B4K /
Filesystem 4K-blocks Used Available Use% Mounted on
/dev/sdd 263940717 44926775 205588074 18% /
263 940 717 − 44 926 775 − 205 588 074 = 13 425 868 блоков, около 51,2 ГиБ. Пять процентов от 268 435 456 блоков ФС размером ровно 1 ТиБ — 13 421 772; ещё 4096 блоков ядро держит в собственном резерве под служебные операции: 2 % размера ФС, но не более 4096 блоков. На маленьких ФС этот резерв тоже заметен: для раздела 160 МиБ это 819 блоков, и при монтировании ядром разница «всего − занято − доступно» будет равна 2048 + 819. Заметим и способ вычисления столбца Use%: он вычисляется как Used / (Used + Avail), а не Used / Size, то есть показывает заполненность «пользовательской» части. Поэтому df может показывать 100 %, когда у root ещё есть место, — и root действительно может продолжать запись.
На учебной ФС эффект виден так же. Под fuse2fs в блоках по 4 КиБ: всего 38 395 (ФС минус служебные структуры), занято 4400, доступно 31 947; 38 395 − 4400 − 31 947 = 2048 — ровно Reserved block count. Для диска с данными (не системного) 5 % от многотерабайтного тома — много. Резерв меняет tune2fs -m (процент) или -r (число блоков):
cd ~/lab-10
tune2fs -m 1 data.img
tune2fs 1.46.5 (30-Dec-2021)
Setting reserved blocks percentage to 1% (409 blocks)
После этого df -h для той же ФС показывает Avail 132M вместо 125M. На корневой ФС резерв уменьшать не стоит, а на разделе архива или медиатеки 1 % или даже 0 — разумный выбор. tune2fs -m работает и на смонтированной ФС.
Подкачка, тома и диски в WSL2
Подкачка: раздел и файл
Подкачка (англ. swap) — область на диске, куда ядро вытесняет редко используемые страницы оперативной памяти, когда её не хватает. Подкачка не делает диск оперативной памятью: обращение к вытесненной странице в тысячи раз медленнее. Её смысл в другом — освободить память, занятую давно не нужными данными (например, частью программы, которая выполнилась при старте), под кэш файлов и активные процессы, и дать системе запас прочности перед тем, как механизм OOM (англ. out of memory) начнёт завершать процессы. Подкачка бывает разделом (тип раздела 82 в MBR, «Linux swap» в GPT) или файлом; современные установки Ubuntu используют файл /swap.img, потому что его размер легко изменить.
Текущее состояние показывают swapon --show, /proc/swaps и free -h (тема 7):
swapon --show
cat /proc/sys/vm/swappiness
NAME TYPE SIZE USED PRIO
/dev/sdc partition 2G 369.5M -2
60
В WSL2 подкачка — отдельный виртуальный диск sdc размером 2 ГиБ. Число 60 — параметр vm.swappiness (от 0 до 200): насколько охотно ядро вытесняет страницы программ в подкачку по сравнению с освобождением кэша файлов. Меньшее значение заставляет дольше держать программы в памяти; изменяется оно командой sudo sysctl vm.swappiness=10 до перезагрузки или файлом в /etc/sysctl.d/ насовсем 3.
Файл подкачки создаётся в три шага. Файл должен быть без «дыр», поэтому его создают fallocate или dd, а не truncate, и закрывают от чтения другими пользователями (в подкачке могут оказаться пароли из памяти):
cd ~/lab-10
fallocate -l 64M swapfile
chmod 600 swapfile
mkswap swapfile
Setting up swapspace version 1, size = 64 MiB (67104768 bytes)
no label, UUID=26c0d27f-8801-4fb7-9a34-38a89ccdcf87
mkswap записывает в начало файла заголовок с сигнатурой и UUID (размер на 4 КиБ меньше — эту страницу занимает заголовок). Включение требует root; без него результат такой:
swapon swapfile
swapon: /home/student/lab-10/swapfile: insecure file owner 1000, 0 (root) suggested.
swapon: /home/student/lab-10/swapfile: swapon failed: Operation not permitted
Первая строка — предупреждение: файл подкачки должен принадлежать root, иначе его владелец сможет читать чужие страницы памяти. В учебной работе файл создаётся через sudo, включается sudo swapon swapfile и отключается sudo swapoff swapfile. Строка в fstab для постоянной подкачки: /swapfile none swap sw 0 0.
LVM, шифрование и RAID: обзор
Разделы GPT жёстко привязаны к диску: раздел нельзя растянуть на второй диск, а увеличить его можно, только если сразу за ним есть свободное место. LVM (англ. Logical Volume Manager) добавляет между разделами и ФС ещё один уровень. Физические тома (PV, англ. physical volume) — разделы или целые диски — объединяются в группу томов (VG, volume group), общий «пул» места. Из пула нарезаются логические тома (LV, logical volume), которые выглядят как обычные блочные устройства (/dev/vg0/data или /dev/mapper/vg0-data) и на которых создаются ФС. Логический том можно увеличить на лету, добавив в группу новый диск, а также сделать его снимок. Установщик Ubuntu Server по умолчанию использует LVM. Пример вывода для группы из двух loop-устройств (углублённая часть лабораторной работы):
sudo pvcreate /dev/loop4 /dev/loop5
sudo vgcreate labvg /dev/loop4 /dev/loop5
sudo lvcreate -n lvdata -L 150M labvg
sudo lvs
Physical volume "/dev/loop4" successfully created.
Physical volume "/dev/loop5" successfully created.
Volume group "labvg" successfully created
Rounding up size to full physical extent 152.00 MiB
Logical volume "lvdata" created.
LV VG Attr LSize Pool Origin Data% Meta% Move Log Cpy%Sync Convert
lvdata labvg -wi-a----- 152.00m
Увеличение тома вместе с ФС выполняет одна команда sudo lvextend -r -L +50M labvg/lvdata: опция -r после расширения тома вызывает resize2fs. Подробности — в справочнике Немета 2 и на страницах lvm(8), lvextend(8) 3.
Шифрование диска в Linux выполняет LUKS (англ. Linux Unified Key Setup) через подсистему ядра dm-crypt. Команда cryptsetup luksFormat записывает в раздел заголовок с зашифрованным ключом, cryptsetup open после ввода пароля создаёт устройство /dev/mapper/имя, на котором лежит обычная ФС; всё, что попадает на диск, зашифровано. Это защита на случай кражи ноутбука или выброшенного диска, а не от программ, работающих в уже загруженной системе. Установщик Ubuntu предлагает LUKS вместе с LVM.
RAID (англ. Redundant Array of Independent Disks) объединяет несколько дисков ради надёжности или скорости: RAID 1 — зеркало, данные пишутся на два диска сразу; RAID 5 и 6 — чередование с контрольной информацией, переживающее отказ одного или двух дисков; RAID 0 — чередование без избыточности, только ради скорости. В Linux программный RAID собирает mdadm (устройства /dev/md0), а btrfs и ZFS умеют делать его сами. RAID защищает от отказа диска, но не от ошибочного rm: удаление мгновенно «зеркалируется» на все диски, поэтому резервное копирование (тема 16) он не заменяет.
Диски в WSL2
WSL2 — настоящий Linux в лёгкой виртуальной машине, и его диски — виртуальные. Корневая ФС дистрибутива хранится в Windows в файле ext4.vhdx (в каталоге пакета дистрибутива внутри %LOCALAPPDATA%). Внутри Linux этот файл виден как целый диск /dev/sdd с ext4 без таблицы разделов; его максимальный размер по умолчанию — 1 ТиБ, а реальный размер файла на диске Windows растёт по мере заполнения (как разреженный файл из темы 2). Удаление файлов в Linux не уменьшает ext4.vhdx автоматически; документация Microsoft описывает, как сжать или увеличить виртуальный диск 5.
Диски Windows видны в /mnt/c, /mnt/d и монтируются по протоколу 9p через драйвер drvfs — это видно в выводе findmnt выше: тип 9p, источник C:\, опции aname=drvfs;path=C:\;uid=1000;gid=1000. Каждое обращение к файлу проходит через границу ВМ и сервер файлов на стороне Windows, поэтому операции с множеством мелких файлов там медленнее, чем в домашнем каталоге Linux (тема 2). Права Unix на /mnt/c эмулируются опциями uid, gid и metadata — так же, как опции uid и umask у vfat.
Подкачка WSL2 — ещё один виртуальный диск (sdc); её размер задаётся в файле %UserProfile%\.wslconfig на стороне Windows, параметрами swap и swapFile секции [wsl2], по умолчанию — четверть объёма памяти, округлённая до гигабайта. Здесь 7,7 ГиБ памяти дают 2 ГиБ подкачки.
Для этой темы важно ещё три обстоятельства. Loop-устройства в ядре WSL2 есть (/dev/loop-control существует), и с sudo вся лабораторная работа выполняется так же, как в ВМ. Без включённого systemd в WSL2 не работает udev: нет /dev/disk/by-uuid, а lsblk -f без sudo показывает пустые колонки — сведения нужно брать из sudo blkid. Наконец, физический диск или его раздел можно подключить в WSL2 целиком командой wsl --mount в PowerShell от администратора; это нужно, например, чтобы прочитать диск с ext4 на компьютере с Windows.
Безопасность файловых систем и криминалистический анализ
Атрибуты неизменяемости: chattr и lsattr
В операционных системах Linux стандартных прав DAC (rwx) недостаточно для защиты критических файлов от привилегированного вредоносного ПО (программ-вымогателей / ransomware). Если нарушитель получает права root, он может стереть или зашифровать любые файлы.
Для защиты ключевых конфигураций и журналов файловые системы семейства ext4, xfs и btrfs поддерживают расширенные атрибуты ядра, управляемые утилитами chattr и lsattr 36:
- Флаг неизменяемости
+i(immutable):
Файл с флагомsudo chattr +i /etc/resolv.conf sudo chattr +i /bin/login+iневозможно модифицировать, удалить, переименовать или создать на него жёсткую ссылку. Операция записи блокируется ядром с ошибкойOperation not permittedдаже для суперпользователяroot. Чтобы изменить файл, администратор обязан сначала явно снять атрибут командойsudo chattr -i. - Флаг добавления в конец
+a(append-only):
Файл разрешено открывать только для добавления новых строк в конец (режимsudo chattr +a /var/log/secure.logO_APPEND). Перезаписать существующие строки, очистить или удалить файл невозможно. Этот флаг защищает системные журналы от затирания следов атакующим. - Просмотр установленных флагов выполняется утилитой
lsattr:lsattr -d /etc/resolv.conf # ----i---------e------- /etc/resolv.conf
Расширенные атрибуты (xattr) и скрытые потоки данных
Помимо стандартных метаданных inode, современные файловые системы поддерживают расширенные атрибуты (Extended Attributes, xattr) — пары «имя-значение», привязанные к файлу 6. Они делятся на четыре пространства имён:
user: произвольные пользовательские метки (управляются командамиgetfattrиsetfattr);security: метки мандатного контроля доступа SELinux (security.selinux), AppArmor и подсистемы контроля целостности IMA (Integrity Measurement Architecture);system: списки контроля доступа POSIX ACL;trusted: служебные метаданные процессов пространства пользователя с правамиCAP_SYS_ADMIN.
Злоумышленники нередко используют расширенные атрибуты для скрытого хранения полезной нагрузки или конфигураций C2. В файловой системе NTFS (Windows) аналогичный и ещё более опасный механизм называется альтернативными потоками данных (Alternate Data Streams, ADS) 9. В NTFS любой файл может содержать несколько независимых потоков данных с синтаксисом файл.txt:поток. Например, команда type malware.exe > test.txt:hidden.exe записывает исполняемый файл внутрь текстового файла test.txt. При этом проводник Windows и стандартная команда dir показывают нулевой или исходный размер test.txt, скрывая наличие сотен килобайт вредоносного исполняемого кода, который затем может быть вызван через WMIC или системные утилиты.
Создание криминалистических образов дисков (Forensic Imaging)
При расследовании компьютерных инцидентов (Digital Forensics) золотое правило криминалистики гласит: любые исследования проводятся только на побитовой копии, оригинальный носитель остаётся неизменным 9.
- Аппаратные блокираторы записи (Write Blockers): физический диск жертвы подключается к рабочей станции эксперта исключительно через контроллер с аппаратной блокировкой сигналов записи на шине SATA/NVMe.
- Побитовое копирование (Bit-stream Image): утилита копирует все секторы накопителя подряд (включая неразмеченное пространство, удалённые файлы и скрытые сектора):
Параметрsudo dd if=/dev/sdb of=/evidence/disk_image.raw bs=64K conv=noerror,sync status=progressconv=noerror,syncкритически важен: при обнаружении сбойных секторов (bad blocks)ddне прерывает работу, а заполняет пропущенные блоки нулями, сохраняя точное смещение адресов всех последующих секторов диска. В профессиональной криминалистике применяются специализированные утилитыdc3ddиdcfldd, вычисляющие хеш на лету. - Хеширование и Chain of Custody: до начала копирования и сразу после него вычисляется криптографический хеш образа (SHA-256):
Совпадение хешей фиксируется в протоколе изъятия доказательств (англ. Chain of Custody) и юридически подтверждает, что в процессе снятия копии ни один байт оригинального диска не был изменён.sha256sum /dev/sdb > /evidence/sdb.sha256 sha256sum /evidence/disk_image.raw > /evidence/image.sha256
Итоги темы
Хранение данных в Linux устроено уровнями. Блочное устройство — массив секторов без структуры; таблица разделов MBR или GPT делит его на разделы, причём GPT хранит копию себя в конце диска и снимает ограничения MBR в 2 ТиБ и четыре раздела. Файловая система превращает раздел в дерево каталогов: ext4 хранит суперблок с копиями, группы блоков с битовыми картами, таблицу inode фиксированного размера, журнал и блоки данных, описываемые экстентами. Монтирование присоединяет корень ФС к каталогу общего дерева, скрывая на время содержимое точки монтирования, а занятую процессами ФС ядро отключить не позволит — виновников находят fuser и lsof.
Для постоянного монтирования служит /etc/fstab. Устройство в нём указывают по UUID, который хранится в самой ФС и не зависит от порядка обнаружения дисков; несистемные ФС помечают nofail, иначе отсутствие диска при загрузке приводит к аварийному режиму. Проверка выполняется в два шага: findmnt --verify ловит неверные пути, типы и недоступные UUID, а пробное mount -a — опечатки в опциях, которых findmnt не видит. Проверять ФС e2fsck можно только размонтированной; потерянные файлы оказываются в lost+found, а испорченный суперблок восстанавливается с резервной копии.
Место на диске считают по-разному: du суммирует файлы, до которых ведут имена, а df читает счётчики ФС. Поэтому удалённый, но открытый файл занимает место, невидимое для du, — его находит lsof +L1. ФС ext4 может исчерпать inode при свободных гигабайтах (df -i), а 5 % её блоков по умолчанию зарезервированы для root (tune2fs -m). Подкачка, LVM, LUKS и RAID добавляют к этой картине новые уровни, но не меняют её логики. Следующая тема переходит к сети и SSH; там встретится sshfs — файловая система FUSE, которая монтирует удалённый каталог по SSH так же, как fuse2fs монтировал образ.
Литература
- Shotts W. The Linux Command Line: A Complete Introduction. 2nd ed. — No Starch Press, 2019. Открытая версия: https://linuxcommand.org/tlcl.php
- Nemeth E., Snyder G., Hein T. R., Whaley B., Mackin D. UNIX and Linux System Administration Handbook. 5th ed. — Addison-Wesley, 2017.
- The Linux man-pages project. — https://man7.org/linux/man-pages/
- systemd. Документация systemd.unit, systemd.service, systemd.timer, journalctl. — https://www.freedesktop.org/software/systemd/man/
- Microsoft. Документация WSL. — https://learn.microsoft.com/windows/wsl/
- Kerrisk M. The Linux Programming Interface. — No Starch Press, 2010.
- Linux Foundation. Filesystem Hierarchy Standard 3.0. — 2015. https://refspecs.linuxfoundation.org/FHS_3.0/fhs-3.0.html
- Ritchie D. M., Thompson K. The UNIX Time-Sharing System. — Communications of the ACM, 1974, Vol. 17, No. 7, С. 365–375, DOI: 10.1145/361011.361061.
- Carrier B. File System Forensic Analysis. — Addison-Wesley, 2005.