Файловая система Linux: inode и ссылки
Замер на живом разделе. Создал файл на 300 мегабайт - занятое место выросло с 28751 до 29051 мегабайта. Оставил процесс с открытым файлом и удалил файл командой rm. После удаления du по каталогу показывает 1 мегабайт, а df по разделу - те же 29051, ни мегабайта не вернулось. Как только я остановил процесс, стало 28751.
Стержень: имя - это ссылка на файл, а не сам файл; данные живут, пока на них есть ссылка или открытый дескриптор. И место на разделе состоит из двух независимых ресурсов.
// Формулировки: «чем жёсткая ссылка отличается от символической?», «df показывает полный диск, du - нет», «место есть, а файл не создаётся»
Имя, номер файла и две ссылки
Файл в Linux устроен так: есть запись с метаданными и указателями на блоки данных, у неё свой номер, а имя в каталоге - всего лишь ссылка на этот номер. Проверил командой stat: у файла original.txt номер 4484904 и счётчик ссылок 2, у сделанной на него жёсткой ссылки hard.txt - тот же номер 4484904 и тот же счётчик. Это буквально одно и то же, просто под двумя именами.
Символическая ссылка устроена иначе: это отдельный файл со своим номером (у меня 4484905), внутри которого лежит текст пути. Размер у неё 12 байт - ровно столько букв в слове original.txt.
// Дальше я удалил original.txt. Через жёсткую ссылку данные читаются как ни в чём не бывало, счётчик ссылок стал 1. Через символическую - ошибка «нет такого файла»: путь, на который она указывала, исчез. Отсюда и разница в применении: символическая переживает переезд на другой раздел и умеет указывать на каталог, а жёсткая не умеет ни того, ни другого, зато держит данные живыми.
- номер файла (inode)
- запись с метаданными и указателями на блоки; имя - лишь ссылка на неё
- счётчик ссылок
- сколько имён указывает на файл; блоки освобождаются при нуле
Удалили файл, а место не вернулось
Вернусь к замеру из начала - это самый частый ночной инцидент. Блоки данных освобождаются при двух условиях сразу: имён на файл не осталось И его не держит открытым ни один процесс. Дескриптор - это открытая ссылка процесса на файл, и она привязана к самому файлу, а не к имени.
Поэтому картина выходит такая: логи «почистили» командой rm, имя из дерева исчезло - и du, который ходит по именам, показывает пусто. А сервис держит дескриптор и продолжает писать в файл, у которого больше нет имени: df, который считает занятые блоки, показывает переполнение. Расхождение этих двух команд и есть диагноз.
// Найти виновника просто. В списке открытых файлов процесса есть пометка о удалении - у меня было видно строку вида 3 -> /data/big.log (deleted). Лечится перезапуском сервиса или командой переоткрыть лог, но никак не повторным rm. В моём замере после остановки процесса место вернулось ровно те 300 мегабайт, что он держал.
- дескриптор
- открытая ссылка процесса на файл; держит блоки занятыми даже без имени
- df против du
- занятые блоки раздела против суммы по именам; расхождение - удалённый открытый файл
Байты и записи о файлах - разные ресурсы
Второй замер, и он удивляет всех. Сделал раздел на 64 мегабайта, но всего с 40 записями о файлах. Создал 39 пустых файлов - на сороковом система ответила «No space left on device», то есть «место кончилось». А df по месту в этот момент показывает: занято 0 из 64 мегабайт, свободно всё. И только df по записям объясняет: занято 40 из 40.
Дело в том, что у файловой системы два независимых ресурса: блоки для данных и таблица записей о файлах, по одной на файл. На распространённой файловой системе ext4 их число фиксируется при создании раздела и потом не растёт. Миллионы мелких файлов - кэши, сессии, письма - выедают таблицу, и запись падает с ошибкой «нет места» при свободных гигабайтах.
// Поэтому при любом «нет места» проверяют оба ресурса, а не один. Лечение - удалить мелочь или пересоздать раздел с другим соотношением. На файловой системе XFS записи выделяются по мере надобности, поэтому там в это упираются реже. И заодно держите в голове права: маска новых файлов обычно такая, что файлы создаются с правами 644, каталоги 755; особый бит на общем каталоге запрещает удалять чужие файлы.
- таблица записей о файлах
- по одной записи на файл; на ext4 её размер фиксирован при создании раздела
- «нет места» при свободном диске
- кончились записи о файлах, а не байты - проверяется отдельной командой
Как отвечать: «df показывает переполнение, du - нет. Куда делось место?»
Почти наверняка это удалённый файл, который держит открытым живой процесс. Имени в дереве уже нет, поэтому du его не видит - он ходит по именам. А блоки заняты, и df это честно показывает, потому что считает занятое на разделе. Я это воспроизводил: создал файл на триста мегабайт, оставил процесс с открытым дескриптором, удалил файл - du по каталогу показал один мегабайт, а df не сдвинулся ни на мегабайт. Как только процесс остановили, ровно триста мегабайт вернулись. Типичный сценарий в проде - логи удалили командой rm, а сервис продолжает в них писать. Ищу виновника по списку открытых файлов, там у таких записей стоит пометка об удалении, дальше перезапускаю сервис или заставляю его переоткрыть лог. И обязательно проверяю второй ресурс - записи о файлах: бывает, что место кончилось не в байтах, а в них, и тогда df по месту показывает свободный диск.
Ответ объясняет механику через два разных счётчика, подкрепляет её собственным замером и добавляет вторую частую причину.
На чём валятся
- −Пытаются вернуть место повторным rm, хотя файл держит открытый дескриптор процесса.
- −Ищут причину «нет места» только в байтах: на моём разделе было свободно 64 МБ и ноль записей о файлах.
- −Считают символическую ссылку копией данных - после удаления цели остаётся битая ссылка.
- −Делают жёсткую ссылку на каталог или через границу разделов и удивляются отказу.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 8, остальные разбираются в тренажёре.
- df показывает раздел забитым, а du по всем каталогам даёт вдвое меньше. Куда делось место?A)du не считает скрытые файлы и каталоги с точкойB)Место держит удалённый файл, открытый живым процессомC)df приплюсовывает зарезервированные под root процентыD)Кончились inode, и df показывает это как занятое место
показать ответ и разбор
+B)Место держит удалённый файл, открытый живым процессом// разбор: Пока процесс держит открытым дескриптор, ядро не освобождает блоки удалённого файла: имени в дереве нет (du не видит), а место занято (df видит). Классика с логами, которые «почистили» через rm, пока сервис пишет в тот же fd. Ищут через lsof +L1 или ls -l /proc/PID/fd | grep deleted, лечат перезапуском сервиса либо переоткрытием файла.
- df -h показывает 60% занято, но touch падает с No space left on device. Что проверишь первым?A)Квоту пользователя на этом разделеB)Резерв под root на файловой системеC)Свободные inode через df -iD)Права на каталог и контекст SELinux
показать ответ и разбор
+C)Свободные inode через df -i// разбор: Место на ФС — два независимых ресурса: блоки данных и inode, по одному на файл. Миллионы мелких файлов (сессии, кэш, очереди) выедают таблицу inode при свободных байтах, и запись падает с ENOSPC. На ext4 число inode задаётся при mkfs и на живой ФС не растёт: чистят мелочь или пересоздают раздел с другим соотношением.
- У пользователя есть права rw на файл, но rm выдаёт Permission denied. Файл лежит в чужом каталоге. Почему?A)На файле стоит атрибут immutable, выставленный через chattr +iB)Удаление имени зависит от прав на КАТАЛОГ (w+x), а не на файлC)Файл в этот момент держит открытым какой-то другой процессD)Чужие файлы обычному пользователю не отдают, нужен root
показать ответ и разбор
+B)Удаление имени зависит от прав на КАТАЛОГ (w+x), а не на файл// разбор: Имя файла хранится в каталоге, поэтому создание, удаление и переименование — это запись в КАТАЛОГ, и для них нужны права w+x на каталог, а не на файл. Права на файл (rw) управляют его содержимым, но не тем, можно ли отвязать имя. Отсюда же смысл липкого бита на /tmp: там всем rwx, но t-бит запрещает удалять чужие файлы.
- ls -lh показывает файл на 10G, а du -h на нём — 20M. Диск не переполнен. Что это за файл?A)Файл прозрачно сжимается на лету самой файловой системойB)ls врёт из-за кэша метаданных, реальный размер и есть 20MC)Разреженный файл: записаны только блоки с данными, дыры места не занимаютD)Это символическая ссылка на большой файл в другом месте
показать ответ и разбор
+C)Разреженный файл: записаны только блоки с данными, дыры места не занимают// разбор: Разреженный файл (sparse) хранит только реально записанные блоки, а «дыры» между ними места не занимают — читаются как нули. Отсюда расхождение: ls -l показывает логический размер (до последнего байта), du — физически занятые блоки. Так устроены образы дисков, qcow2, файлы БД. Осторожно при копировании: без поддержки sparse (cp --sparse=always, tar -S) дыры «раздуются» в реальные нули.
- df показывает 100% занято, приложение падает с No space left, но из-под root touch в том же разделе проходит. Почему?A)В ext4 по умолчанию 5% блоков зарезервировано под rootB)root на самом деле пишет в tmpfs в памяти, а не на этот дискC)У приложения включена дисковая квота, которой нет у rootD)root пишет мимо файловой системы прямо в блочное устройство
показать ответ и разбор
+A)В ext4 по умолчанию 5% блоков зарезервировано под root// разбор: ext2/3/4 по умолчанию резервируют 5% блоков под uid 0 — чтобы при переполнении не легли системные сервисы и root мог разгрести. Поэтому обычный процесс упирается в «диск полон» на ~95%, а root ещё пишет. На больших разделах под данные эти 5% — десятки гигабайт впустую, их уменьшают через tune2fs -m 1. Если и root не может писать — тогда уже реально ноль байт или кончились inode.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.