Learning Platform
Глоссарий Troubleshooting
Урок 10.02 · 22 мин
Начальный
FilesystemsInodesLinuxext4

Inodes — метаданные файлов и жизнь без имени

В Unix-системах есть фундаментальное архитектурное решение, которое долго удивляет новичков: файл и его имя — разные вещи. Имя — это просто запись в директории, которая ссылается на inode. Один inode может иметь много имён (hard links). Имя можно удалить — inode останется жив, пока на него есть ссылки или процессы держат открытый файл. Это объясняет десятки практических ситуаций — от «удалил лог, а место не освободилось» до «программа продолжает писать в файл, который я удалил».

Inode (index node) — структура данных, описывающая всё про файл, КРОМЕ имени: размер, владелец, права, времена, и указатели на блоки данных. У каждого файла в ФС ровно один inode. Имя живёт в директории — запись «имя -> inode номер».

В этом уроке: что физически лежит в inode, как файл «живёт без имени», что такое inode exhaustion и почему он — реальный production-баг, и почему lsof | grep deleted спасает от потерянных гигабайт.


Что хранится в inode

В ext4 (типичная Linux-ФС) inode занимает 256 байт. В нём:

Содержимое inode (ext4)
i_mode (file type + perms)2 байта. Старшие 4 бита -- тип файла (regular, directory, symlink, block, char, fifo, socket). Младшие 12 бит -- rwx-permissions + setuid/setgid/sticky
i_uid, i_gidUID и GID владельца. По умолчанию совпадают с создающим процессом. Используются для проверки прав доступа
i_sizeРазмер файла в байтах. Для regular files -- кол-во полезных данных. Для директорий -- размер записей. Для symlinks -- длина пути назначения
i_atime, i_mtime, i_ctimeВремена: atime -- последнее чтение (часто отключают через noatime для performance), mtime -- последнее изменение содержимого, ctime -- последнее изменение метаданных (chmod, chown тоже триггерят ctime)
i_links_countКоличество hard links на этот inode. Изначально 1 при создании. Каждый ln file file2 увеличивает. Когда падает до 0 и нет открытых fd -- inode освобождается
i_blocksКоличество блоков (по 512 байт), которые занимает файл на диске. Может быть меньше, чем i_size/512 (sparse files) или больше (preallocated)
i_block[15] (data blocks)Указатели на блоки данных. В ext2/3 -- 12 direct + indirect + double + triple indirect. В ext4 -- extents (диапазоны блоков), гораздо эффективнее для больших файлов
i_flagsФлаги: immutable (нельзя менять даже root-у пока флаг стоит), append-only, no_dump, sync (синхронная запись каждой операции). Управляются через chattr
extended attributesПроизвольные key-value атрибуты. Используются для ACL, SELinux labels, application metadata. Видны через getfattr

Обратите внимание: имени файла в inode НЕТ. Это ключевое решение Unix. Имя живёт в родительской директории, которая сама файл (типа directory) с записями «имя -> inode_number».

Посмотрим на реальный inode:

# Создадим файл и посмотрим его inode:
echo "Hello world" > /tmp/test.txt
stat /tmp/test.txt
# File: /tmp/test.txt
# Size: 12          Blocks: 8          IO Block: 4096   regular file
# Device: 802h/2050d  Inode: 786432      Links: 1
# Access: (0644/-rw-r--r--)  Uid: ( 1000/   myuser)   Gid: ( 1000/   myuser)
# Access: 2026-05-18 10:00:00.123456789 +0000
# Modify: 2026-05-18 10:00:00.123456789 +0000
# Change: 2026-05-18 10:00:00.123456789 +0000

# Видно: inode 786432, size 12, links 1, права 644, owner myuser

Ключевые поля — именно те, что в inode. Имени /tmp/test.txt нет среди этих данных — оно живёт в записи директории /tmp.


Имя живёт в директории

Директория — это специальный тип файла, содержимое которого — таблица записей «имя -> inode_number».

# Создадим файлы и посмотрим директорию:
mkdir /tmp/demo
touch /tmp/demo/a.txt /tmp/demo/b.txt /tmp/demo/c.txt

# Показать inode номера:
ls -i /tmp/demo/
# 786433 a.txt  786434 b.txt  786435 c.txt

# Размер директории = сумма размеров записей:
stat /tmp/demo/
# Size: 4096  -- директория занимает один блок (минимум 4 КБ)
# В нём ~ 24-32 байта на запись

Запись в директории состоит из:

  • inode_number (4 байта в ext4).
  • rec_len — длина записи (2 байта).
  • name_len — длина имени (1 байт).
  • file_type — тип файла (1 байт) — хранится здесь для оптимизации readdir.
  • name — собственно имя, до 255 байт.

Когда вы делаете ls /tmp/demo, kernel читает блок директории, парсит записи, для каждого имени берёт inode, читает его, складывает в результат.

rm a.txt — это:

  1. Удалить запись «a.txt -> 786433» из директории.
  2. Уменьшить i_links_count в inode 786433 на 1.
  3. Если i_links_count = 0 и никто не держит fd — освободить inode + блоки данных.

Заметьте: на втором шаге, если кто-то держит файл открытым — inode жив, данные на месте. Это и есть «жизнь без имени».


Файл удалён, но процесс держит его открытым

Классический сценарий: огромный лог-файл, rm его, чтобы освободить диск — а место не освободилось. df показывает тот же used, du не находит большой файл. Что происходит?

# Демонстрация:
# Терминал 1: программа пишет в лог:
exec 9> /tmp/big.log
for i in {1..1000000}; do
    echo "Line $i log entry" >&9
done &
LOGGER=$!

# Через секунду удаляем:
sleep 1
rm /tmp/big.log
ls /tmp/big.log
# ls: cannot access '/tmp/big.log': No such file or directory

# Но процесс продолжает писать!
# Disk usage не упал:
df /tmp

# Кто держит deleted-файлы:
lsof +L1 | grep deleted
# bash  12345  myuser  9w  REG   8,2  123456789  786500  /tmp/big.log (deleted)
#                          ^^^                          ^^^^^^^^^^^^^^^^
#                          write fd 9                    помечен deleted

# Файл живёт пока fd 9 открыт. Чтобы освободить место:
# 1) Сказать процессу закрыть fd (kill / signal / API)
# 2) Или: восстановить файл через /proc:
ls -la /proc/12345/fd/9
# lr-x------ 1 myuser myuser 64 May 18 10:00 /proc/12345/fd/9 -> /tmp/big.log (deleted)

# Можно даже спасти данные:
cp /proc/12345/fd/9 /tmp/recovered.log

# Только когда процесс закроет fd (exit, kill, close()) -- место освободится
kill $LOGGER

lsof +L1 — стандартная команда для поиска deleted-but-still-open файлов. На production-сервере с jumbo логами это первый подозреваемый при нехватке места.

WARNING

В production-сервисах часто пишут: `rm /var/log/app.log` -> диск не освободился -> поверили df но не lsof -> в панике рестартили сервис. Правильно: 1) запустить logrotate с copytruncate (truncate активный файл, не unlink), 2) или дать процессу сигнал на reopen логов (SIGHUP), 3) или использовать journald, который сам управляет ротацией.


Inode exhaustion — невидимая ловушка

В ext4 количество inode-ов фиксируется при создании ФС. Один inode на каждый файл. Если файлов много (хотя они маленькие) — можно исчерпать inode-ы при свободном дисковом пространстве.

# Использование disk space:
df -h /
# Filesystem  Size  Used Avail Use% Mounted on
# /dev/...    500G  100G  400G  20% /

# Использование inodes (отдельная статистика!):
df -i /
# Filesystem    Inodes  IUsed  IFree IUse% Mounted on
# /dev/...    32000000  20000  31980000   1% /

# Если IUse% близко к 100% -- проблема!
# touch new_file -> "No space left on device"
# Хотя диск свободен на 80%

# Соотношение inodes/space задаётся при mkfs:
# По умолчанию ext4: ~1 inode на 16 КБ диска
# Можно явно: mkfs.ext4 -i 4096 (один inode на 4 КБ -- больше inodes)

Типичные сценарии inode exhaustion:

  • mail spool с миллионами мелких писем.
  • cache directory с миллиардами маленьких файлов (по одному на ключ).
  • node_modules на dev-машине разработчика.
  • Docker layer cache с тысячами слоёв.
# Найти, кто жрёт inodes:
sudo find / -xdev -type f 2>/dev/null | awk -F/ '{print $2"/"$3}' | sort | uniq -c | sort -rn | head -20
# 5234567 var/cache
# 1234567 home/user/project/node_modules
# ...

# Альтернатива -- du --inodes (если есть):
du --inodes -d 2 / 2>/dev/null | sort -rn | head -20

Лекарство: чистка, или пересоздание ФС с большим количеством inodes (mkfs.ext4 -N 128000000), или переход на XFS (динамически выделяет inodes).


Расширенные атрибуты (xattr)

inode хранит обычные поля, но также может ссылаться на extended attributes — произвольные key-value пары:

# Установить xattr:
setfattr -n user.author -v "myname" /tmp/test.txt

# Прочитать все xattr:
getfattr -d /tmp/test.txt
# file: tmp/test.txt
# user.author="myname"

# Системные xattr (требуют root):
# security.selinux  -- SELinux label
# system.posix_acl_access -- POSIX ACL
# trusted.*  -- root-only metadata

# Видеть в ls -l (флаг + после прав, например -rw-r--r--+):
getfacl /tmp/test.txt

Xattr используются:

  • SELinux / AppArmor — security labels.
  • ACL — расширенные права доступа.
  • md5sum cache в некоторых тулзах.
  • Docker — metadata о слоях.

Inode = ключевая абстракция

Если запомнить из VFS одну вещь — запомните inode. Это и есть «настоящий файл». Имя — ярлык. Path — последовательность ярлыков. Понимание этого сразу делает прозрачными:

  • Hard links: одно содержимое, несколько имён. Все имена равноправны.
  • Symbolic links: ссылка-указатель на имя (а не на inode), может быть битой.
  • rm на открытом файле: убираем имя, inode жив.
  • rename: меняется запись в директории, inode тот же.
  • Move within filesystem: переименование между директориями, inode сохраняется (быстро).
  • Move across filesystems: copy + remove, новый inode в новой ФС (медленно).
  • df vs du: df смотрит на ФС (включая deleted-open), du — только видимые файлы.
df и du на практике: диагностика места на диске

Попробуй сам

Эксперименты с inode:

# Создать файл, посмотреть inode:
echo "data" > /tmp/file.txt
INODE=$(stat -c '%i' /tmp/file.txt)
echo "Inode: $INODE"

# Создать hard link:
ln /tmp/file.txt /tmp/link.txt
stat -c '%i Links: %h' /tmp/link.txt
# 786432 Links: 2  <-- тот же inode

# Удалить оригинал:
rm /tmp/file.txt
cat /tmp/link.txt
# data  <-- по-прежнему доступно через link.txt!

# Удалить последнюю ссылку:
rm /tmp/link.txt
# Теперь inode 786432 действительно освобождён

# Хитрость: восстановление случайно удалённого файла
# (если процесс ещё держит его открытым)
python3 -c "
import time
f = open('/tmp/important.txt', 'w')
f.write('critical data\n')
f.flush()
print('PID:', __import__('os').getpid(), 'FD:', f.fileno())
time.sleep(60)
" &
PYPID=$!
sleep 1

# 'Случайно' удалили:
rm /tmp/important.txt
ls /tmp/important.txt  # No such file

# Но процесс ещё держит! Lsof покажет:
lsof -p $PYPID | grep important

# Восстановить:
cp /proc/$PYPID/fd/3 /tmp/recovered.txt
cat /tmp/recovered.txt
# critical data

kill $PYPID

Демонстрация inode exhaustion:

# СОЗДАТЬ ТЕСТОВУЮ ФС (не на основной системе!):
dd if=/dev/zero of=/tmp/test_fs.img bs=1M count=100
mkfs.ext4 -N 1000 /tmp/test_fs.img  # всего 1000 inodes
sudo mkdir -p /mnt/test
sudo mount /tmp/test_fs.img /mnt/test

df -i /mnt/test
# IUse только 11, IFree около 989

# Заполняем мелкими файлами:
sudo bash -c 'for i in {1..1000}; do touch /mnt/test/f$i; done' 2>&1 | tail -3
# touch: cannot touch ... No space left on device  <-- inode exhaustion!

df -h /mnt/test    # disk space почти свободен
df -i /mnt/test    # inodes исчерпаны

# Cleanup:
sudo umount /mnt/test
rm /tmp/test_fs.img

Проверка знанийKnowledge check
Production-сервер выдаёт 'No space left on device' при попытке создать файл. df -h показывает 70% used (диск не полон). Какие гипотезы и план действий?
ОтветAnswer
Несколько возможных причин: 1) Inode exhaustion -- проверить df -i. Если IUse% близко к 100% -- ext4 ФС исчерпала inodes при свободном диске. Лекарство: найти кто жрёт (find / -xdev -type f | awk -F/ '{print $2}' | sort | uniq -c | sort -rn), удалить лишнее, или мигрировать на XFS (динамические inodes). 2) Deleted-but-open files -- огромный лог удалён через rm, но процесс держит fd. df смотрит на ФС (видит занятые блоки), du не находит файл. Проверить через lsof +L1 | grep deleted. Решение -- закрыть процесс (kill, systemctl restart) или truncate через /proc/PID/fd/N. 3) Reserved blocks для root -- ext4 резервирует 5% места для root. У не-root юзера может закончиться место при df 95% used. Уменьшить через tune2fs -m 1 /dev/sdX. 4) Один файл переполнен -- проверить ulimit -f (file size limit) для процесса. 5) Quota исчерпана -- если используются user/group quotas (quota -u user). 6) Filesystem corruption -- редко, но fsck выявит. План: a) df -h + df -i. b) lsof +L1 | grep deleted. c) find largest dirs: du -h --max-depth=2 / 2>/dev/null | sort -rh | head. d) проверить /var/log -- частый виновник.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что хранится в inode, а что -- нет?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5