MAATRIX / Блог / Что делает ядро на каждом слэше пути, пока ищет ваш файл

Что делает ядро на каждом слэше пути, пока ищет ваш файл

MAATRIX

Когда приложение открывает файл по пути вроде /var/www/app/storage/framework/cache/data/a3/f1/abc123, кажется, что ядро просто «идёт по адресу» и забирает содержимое. На самом деле оно разбирает путь по одному компоненту между слэшами и на каждом шаге отдельно ищет, какому inode соответствует это имя, и имеет ли текущий процесс право туда заглянуть. Чем глубже вложенность каталогов, тем больше таких шагов на каждое открытие файла — и это не абстракция для собеседований, а конкретная причина, почему одни файловые структуры работают быстрее других на одинаковом железе.

Путь — это не строка, а последовательность решений

Файловая система хранит данные не по путям, а по номерам inode. Путь /var/www/app/storage/logs/app.log — это просто человекочитаемая инструкция, как дойти до нужного inode через цепочку каталогов. Ядро не умеет прыгнуть сразу в конец строки: у него нет функции «найди inode по полному пути одним запросом». Вместо этого путь разбирается на компоненты по разделителю /, и дальше идёт пошаговый обход.

Для пути из примера компонентов пять: var, www, app, storage, logs, app.log — то есть шесть шагов, если считать сам файл. На каждом шаге ядро выполняет одну и ту же операцию: взять inode текущего каталога (мы уже в нём находимся), прочитать в нём запись с именем следующего компонента, получить номер inode, который этому имени соответствует, и перейти туда. Потом повторить для следующего компонента. Это называется path walk, и в коде ядра Linux за него отвечает функция link_path_walk (файл fs/namei.c), которая крутит цикл по компонентам пути до тех пор, пока не разберёт весь путь целиком.

Важно, что каждый каталог в Linux сам по себе устроен как файл специального типа: его содержимое — это список пар «имя → номер inode» для всего, что в нём лежит. Открыть файл по имени — значит прочитать этот список (или его индекс) и найти нужную запись. Если каталог маленький, эта операция дешёвая. Если в каталоге лежат сотни тысяч записей, поиск конкретного имени в нём — уже заметная работа, даже если сам файл, до которого мы добираемся, крошечный.

Что именно проверяется на каждом слэше

На каждом компоненте пути происходит не одна проверка, а минимум две — сам поиск и права доступа.

Во-первых, поиск. Ядро запрашивает у файловой системы (ext4, XFS, ZFS — не важно, интерфейс одинаковый) операцию lookup: «в каталоге с inode номер N есть компонент с именем X, каков его inode?». Если данных о структуре каталога нет в памяти, файловая система читает блоки каталога с диска, разбирает их формат (для ext4 это либо линейный список записей, либо HTL-индекс — хешированное дерево для больших каталогов) и находит нужную запись.

Во-вторых, права доступа. Прежде чем спуститься в следующий каталог, ядро проверяет бит x (execute) на текущем каталоге для UID/GID вызывающего процесса — без права на выполнение каталог нельзя *обойти*, даже если у вас есть право на чтение файла внутри него. Отдельно от этого при финальном открытии файла проверяются права на сам файл. То есть если у вас нет права x хотя бы на один промежуточный каталог в цепочке — скажем, /var/www/app доступен, а /var/www/app/storage для вашего пользователя закрыт — вы получите Permission denied на файл внутри, даже если права на сам файл идеальные. Это частая причина «файл же есть, права 644, а всё равно отказ» — проблема на уровне не файла, а одного из родительских каталогов.

Символические ссылки добавляют ядру ещё работы: если один из компонентов пути оказывается симлинком, ядро должно прочитать, куда он указывает, и подставить эту цель в путь обхода, продолжив разбор уже по новой цепочке. Ядро ограничивает глубину рекурсивного разыменования симлинков (исторически 8 уровней вложенности, с защитой от зацикливания через общий лимит числа шагов MAXSYMLINKS), чтобы кольцевые ссылки не повесили процесс в бесконечном цикле.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Почему глубокая вложенность — это не бесплатно

Из всего этого следует прямое правило: чем больше компонентов в пути, тем больше отдельных операций поиска и проверки прав нужно выполнить ядру на каждое обращение к файлу. Путь из трёх уровней (/data/file.txt) и путь из двенадцати уровней (/data/app/env/prod/storage/cache/sessions/2026/08/28/user/file.txt) — это принципиально разное количество работы на *каждое* открытие файла, даже если итоговый файл один и тот же по размеру.

На практике это не значит, что вложенность в 5–6 уровней — катастрофа: с горячим кешем (см. ниже) эта разница почти не ощущается, потому что промежуточные шаги обходятся без обращения к диску. Проблема проявляется там, где:

  • путь действительно очень длинный (десятки уровней — так бывает у автогенерируемых структур хранения, node_modules с глубокой вложенностью зависимостей, кешей вроде storage/framework/cache/data/xx/yy/hash);
  • кеш холодный или его давит нагрузка — тогда каждый уровень means реальное чтение с диска;
  • в одном каталоге на пути лежит огромное число файлов — тогда сам шаг поиска в этом каталоге становится дороже, потому что структура каталога больше и её сложнее пробегать (подробный разбор этой ситуации — в статье про миллион мелких файлов на одном диске).

Здесь стоит разделить два разных источника издержек, которые люди часто путают: издержки от *длины пути* (много компонентов подряд) и издержки от *размера одного каталога* (много файлов в одном месте). Первое увеличивает число шагов path walk. Второе увеличивает стоимость каждого отдельного шага поиска в конкретном каталоге. Оба фактора складываются, и хуже всего — их комбинация: глубокая структура, где один из промежуточных каталогов сам по себе разбух до сотен тысяч записей.

dentry cache: почему второй раз почти всегда быстрее первого

Если бы ядро на каждое открытие файла честно ходило на диск за каждым компонентом пути, файловые операции на любой сколько-нибудь загруженной системе стояли бы колом. От этого спасает dentry cache (dcache) — кеш в оперативной памяти, который хранит уже разрешённые соответствия «имя компонента в конкретном каталоге → его inode» в виде структур dentry (directory entry).

Логика простая: как только ядро один раз прошло путь /var/www/app/storage/logs и нашло, каким inode является каждый компонент, оно кладёт эти пары имя→inode в dcache. При следующем обращении к файлу с тем же префиксом пути (или к любому другому файлу в том же дереве) ядро сначала проверяет dcache, и если запись там есть — использует её напрямую, без единого обращения к файловой системе на диске. Хэш-таблица dcache индексируется по паре (родительский dentry, имя), так что поиск в кеше — это операция с постоянной по сложности стоимостью, а не линейный перебор.

Отсюда прямое следствие: первое обращение к глубокому пути после холодного старта (перезагрузка, только что смонтированный раздел, файл, к которому давно не обращались и его dentry вытеснен из кеша) идёт по полной цепочке до диска на каждом уровне. Повторные обращения к тому же и соседним путям — почти всегда попадание в dcache, и стоимость пути резко падает. Именно поэтому «прогретый» сервер после рестарта приложения ощутимо шустрее по файловым операциям, чем сразу после ребута хоста — и почему первый запрос к статике на холодном контейнере иногда заметно дольше следующих.

У dcache есть и отрицательная сторона: он тоже занимает память, и под давлением памяти ядро вытесняет наименее используемые записи (через механизм shrink slab), точно так же как обычные страницы кеша уходят под нагрузкой — это тот же класс поведения, который разбирается в статье про file cache и грязные страницы. Если рабочий набор путей на сервере огромный (миллионы уникальных файлов, к которым обращаются вперемешку), кеш не может держать всё разом, и часть обращений всё равно упирается в диск даже на «прогретой» системе — просто доля таких промахов ниже, чем при полностью холодном кеше.

Отдельно стоит отрицательный кеш (negative dentry): если процесс запросил файл, которого не существует, ядро тоже кеширует сам факт отсутствия — чтобы повторные проверки на существование не били по диску заново. Это заметно на путях вроде поиска конфигов в нескольких директориях по очереди (типичный паттерн для интерпретаторов и загрузчиков модулей) — большая часть таких проверок на прогретой системе вообще не доходит до файловой системы.

Практические следствия для структуры хранения на сервере

Из механики path walk и dcache вытекает несколько практических правил, которые стоит держать в голове при проектировании структуры каталогов на сервере — будь то приложение, кеш или файловое хранилище.

Не делайте вложенность глубже, чем реально нужно для организации данных. Разбиение на подкаталоги полезно, когда оно решает конкретную задачу — например, шардирование по хешу, чтобы не упереться в производительность одного огромного каталога (об этом — в статье про нехватку inode при формально свободном месте, где как раз миллионы файлов в одном месте создают проблему). Но вложенность ради вложенности — просто лишние компоненты path walk, которые с холодным кешем стоят реальных операций.

Классическая схема двухуровневого шардирования — разумный компромисс. Многие системы хранения (Git, кеши изображений, CDN-заглушки) раскладывают файлы по схеме xx/yy/hash, где xx и yy — первые символы хеша имени. Это ограничивает число записей в каждом отдельном каталоге (обычно до пары тысяч на уровень при разумном общем объёме), сохраняя при этом путь коротким — всего два дополнительных уровня вместо линейного роста одного огромного каталога. Само по себе это выбор глубины 2–3 уровня, а не 10.

Огромный плоский каталог — тоже плохо, просто по другой причине. Если тысячи или миллионы файлов лежат в одном каталоге без разбиения, страдает не длина path walk (она минимальна — путь короткий), а стоимость самого шага поиска внутри этого каталога: структура каталога на диске становится большой, кеш этой структуры в памяти занимает больше места и чаще вытесняется, а листинг (ls, find, обход бэкапом) превращается в заметно более тяжёлую операцию. Это ровно тот случай, который разобран в статье про миллион мелких файлов — стоит заглянуть туда, если у вас уже есть каталог, который разросся неконтролируемо.

На сетевых файловых системах цена промаха мимо кеша выше на порядки. На локальном диске непопадание в dcache означает чтение блока с диска — операция в худшем случае на уровне долей миллисекунды на SSD. На NFS или похожих сетевых ФС каждый непокешированный шаг — это сетевой round-trip до сервера, и цена глубокого пути с холодным кешем растёт ощутимо быстрее. Если приложение упирается в производительность файловых операций именно на сетевом хранилище, вложенность путей стоит пересматривать в первую очередь.

Мониторьте память, выделенную под dentry/inode кеш, а не игнорируйте её как «занятую зря». Команда cat /proc/slabinfo | grep -E 'dentry|inode_cache' покажет, сколько объектов сейчас держит кеш. Резкое падение объёма кеша под давлением памяти на сервере с большим деревом файлов — сигнал, что часть операций, которые раньше обходились без диска, начнёт идти на диск снова. Общая картина «куда уходит память сервера» и почему занятая под кеш память — это нормально, а не утечка, разобрана в статье про page cache и «пропавшую» свободную память.

Права на промежуточные каталоги — частый источник путаницы, а не только производительности. Раз каждый шаг path walk отдельно проверяет бит x на каталоге, при отладке Permission denied полезно проверять права не только на конечный файл, но и на всю цепочку родительских каталогов: namei -l /var/www/app/storage/logs/app.log покажет права на каждом уровне пути сразу, что избавляет от ручного обхода stat по каждому компоненту.

Как это выглядит на практике: одна и та же операция, разная цена

Возьмём пример: сервис логирует в путь с датой в структуре — /var/log/app/2026/08/28/service-a.log. Каждая запись в лог — это в некотором смысле повторное «нахождение» этого файла (открытие файлового дескриптора, если приложение его не держит открытым постоянно). Если приложение открывает и закрывает файл на каждую запись (плохая практика, но встречается), каждое такое открытие — это path walk по пяти-шести компонентам.

Первое обращение в начале нового дня, когда каталог 28 только что создан и ещё не в кеше — идёт с проверкой на диске (или как минимум с созданием новых dentry-записей). Все последующие обращения в течение дня — попадания в dcache, потому что и сам каталог, и записи в нём уже разрешены и лежат в памяти. Разница между «первым файлом дня» и «сотым файлом того же дня» в цене path walk — наглядная иллюстрация того, что кеш делает основную тяжёлую работу невидимой для приложения.

Практический вывод для таких схем: если приложение и так открывает/закрывает файл на каждую операцию, держать открытый дескриптор дольше (а лог-ротацию делать по сигналу, а не пересозданием пути) снимает нагрузку не только на path walk, но и на системные вызовы open/close в целом.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Path walk — это то же самое, что чтение файла с диска?

Нет. Path walk — это только этап поиска, на котором ядро определяет, какому inode соответствует путь, и проверяет права на промежуточные каталоги. После того как inode финального файла найден, начинается отдельный этап — собственно чтение данных файла через page cache или напрямую с диска.

Почему ls в каталоге с миллионом файлов работает медленно, хотя dcache должен помогать?

Dcache ускоряет повторные обращения к уже известным именам. Но ls без параметров запрашивает листинг всего каталога целиком — это отдельная операция (readdir), которая обходит все записи независимо от того, разрешены они уже в dcache или нет, и для миллиона записей это миллион элементов, которые нужно перечислить и (если используется ls -l) для каждого отдельно получить метаданные через stat.

Как проверить, сколько памяти реально занимает dentry cache на сервере?

cat /proc/slabinfo | grep dentry покажет число активных и выделенных объектов dentry и размер одного объекта — умножив, получите примерную занятую память. Точный расчёт зависит от версии ядра и структуры данных, поэтому воспринимайте это как ориентир, а не точную цифру.

Помогает ли SSD вместо HDD против дороговизны глубоких путей?

Помогает, потому что снижает абсолютную цену одного промаха мимо кеша — случайное чтение на SSD на порядки быстрее, чем на вращающемся диске. Но сам принцип — что каждый непокешированный уровень пути стоит отдельного обращения к носителю — остаётся тем же вне зависимости от типа диска. SSD снижает штраф за промах, а не убирает механику path walk как таковую.

Симлинки замедляют разрешение пути сильнее, чем обычные каталоги?

На каждый симлинк добавляется отдельное чтение его содержимого (куда он указывает) и повторный разбор итогового пути с этой подстановкой — то есть да, лишний шаг по сравнению с обычным компонентом-каталогом. С прогретым dcache эта разница почти незаметна, но при холодном кеше и длинных цепочках вложенных симлинков (например, у некоторых схем управления версиями зависимостей) она накапливается.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →