Думаю, многие из нас, желавших освоить DevOps или углубиться в азы архитектуры всеми любимого Linux сталкивались с такими понятиями как Inode, файловые дескрипторы, виртуальная файловая система, какие-то там суперблоки и многое-многое другое.
Всё это, как я считаю, можно объединить двумя словами: "Архитектура VFS", которую мы сегодня и разберём. Перед тем как начать, я думаю, что нам стоит определиться, что КОНКРЕТНО мы будем изучать:
Что такое файловые системы, какими они бывают и зачем они нужны.
Что такое VFS, зачем она нужна и как она работает.
Что такое файловые дескрипторы (FD), зачем они нужны и как они работают.
Операторы перенаправления потоков.
Зачем вообще это нужно?
Я писал эту статью, основываясь на своих личных заметках, которые делал, пока сам изучал блок по линуксу. Выложить я их решил по совету своего знакомого, порекомендовавшего мне метод "учи других тому, что учишь сам".
Теперь о практической пользе. Если вы, как и я, метите в DevOps(а именно для них эта статья и написана по принципу), то эта информация вам нужна, как минимум, для того, чтобы пройти собеседование - на тему траблшутинга часто могут задавать вопросы в духе "удалил файл, но процесс всё равно продолжает его читать".
Важно!
Эта статья НЕ ДЛЯ АБСОЛЮТНЫХ НОВИЧКОВ! Также отмечу, что эта статья даёт больше поверхностно-практический обзор, чем глубокий.
1. Файловые системы
Файловая система - это порядок, определяющий способ организации, хранения и именования данных на физических носителях(флешки, диски и т.д).
Конкретная файловая система определяет размер имён файлов, максимальный возможный раздел файла и раздела, а также набор атрибутов файла.
Какие задачи решает ФС:
Именование - превратить "блок n1828" на физ-диске в читаемые
/etc/nginx/nginx.conf.Организация - дерево из каталогов (вспомните FHS), чтобы миллионы файлов не лежали в одной куче.
Метаданные - размер файла, его права, владелец, время создания/изменения, тип(текстовый или видео). По факту - это и есть inode ИЛИ его аналоги(с которыми вы, я уверен, вряд ли столкнётесь).
Связка имени и данных - ФС связывает имя файла с inode, а inode - с блоками данных (и есть содержимое нашего файла) на носителе.
Предоставление интерфейса работы с файлами для других приложений. Каждая ФС имеет одинаковый набор функций, но разную реализацию.
Поддержание целостности ФС во время сбоёв питания или аппаратных ошибок.
Хорошо, с этим определились. Но давайте слегка дополним наше первое определение.
Файловая система - это конкретный формат хранения данных на носителе + драйвер в ядре, который этот формат понимает.
У каждой ФС есть:
Суперблок - это методанные всей ФС: размер, размер блока, количество inode. Именно его VFS(о нём позже) и читает.
Таблица inode - в ней, как я и говорил ранее, лежат метаданные файла.
Блоки данных - это уже содержимое нашего файла (текст или видео)
Структуры каталогов - как человеко-читаемые имена файлов связаны с inode
Набор операций для работы со своими объектами(условно, inodes) и уникальной для каждой ФС реализации.
Структуры свободного пространства - это служебные данные внутри ФС, которые отвечают на вопрос: «какие блоки (и inode) на носителе свободны, а какие заняты?». Без них ФС не смогла бы выделить место под новый файл, не зная, куда писать.
Механизмы обеспечения целостности (consistency) - это журнал у ext4 и CoW-дерево у btrfs. Роль у них одна - гарантировать, что ФС останется КОНСИСТЕНТНОЙ (целостностой) после сбоя (питание, kernel panic или отключение диска).
Виды файловых систем
Файловых систем существует достаточно много - все они организовывают файлы по-своему, и все они используются для ОПРЕДЕЛЁННЫХ СЛУЧАЕВ.
Знать все существующие нам необязательно, достаточно знать несколько. Углубляться в архитектурные особенности каждой конкретной ФС мы не будем. Также я не буду подробно рассказывать о преимуществах и недостатках каждой ФС - это тема уже для другой огромной статьи. Нам сейчас нужно всего лишь понять, что ФСки бывают разные.
Но для начала давайте определимся, что такое журналируемая ФС:
Журналируемая файловая система — это ФС, которая перед внесением изменений в основную область данных сначала записывает информацию об этих изменениях в специальный «журнал» (journal). Если в процессе произойдёт сбой, вы сможете восстановить ход действий по журналу. Учтите, что по умолчанию журналируются только метаданные, а не сами данные файла. Журнал, в первую очередь, спасает саму ФС от поломки.
ext4 - это журналируемая, стандартная и наиболее универсальная ФС для большинства дистрибутивов. Её основные преимущества - это отличная совместимость с ext прошлых версий, а также с самыми разными инструментами, поддержка уменьшения размера (например, с 20гб до 10гб, но там есть нюансы). Помимо этого она очень стабильная и работает без неприятных сюрпризов.
XFS - высокопроизводительная и журналируемая ФС, которая является стандартом для серверов (стоит по дефолту в RHEL). Она демонстрирует превосходную производительность при работе с большими файлами и высоких нагрузках. Главный недостаток - невозможность уменьшения размера.
Btrfs - это ФС, использующая механизм CoW (Copy-On-Write), что означает, что при изменении файла данные НЕ перезаписываются на месте: сначала создаётся их новая копия. Главные преимущества - это эффективные снапшоты, которые почти не занимают места, прозрачное сжатие данных и встроенная поддержка RAID, что делает её удобной для создания резервных копий и откатов системы. Главный недостаток - она может быть медленнее в ситуациях с интенсивной записью на диск из-за накладных расходов CoW.
ZFS - я бы сказал, что это больше платформа для хранения данных, чем просто ФС. Она объединяет в себе функции ФС и менеджера томов (как LVM). Ее сильнейшая сторона — «самовосстановление»: ZFS хранит контрольные суммы для каждого блока данных, поэтому она может обнаружить «тихое» повреждение файла и, если есть резервная копия на другом диске, автоматически исправить его. Кроме того, она поддерживает практически бесконечное количество мгновенных снапшотов и эффективное сжатие. Однако за эту мощь приходится платить: ZFS очень требовательна к оперативной памяти (ей нужно много RAM для кэша).
NFS (Network File System) - это стандарт для обмена файлами между Linux/Unix системами. По факту - это распределённая ФС, которая позволяет клиентам монтировать удалённые каталоги по сети на единый сервер и работать с ними так, как если бы они были локальными. NFS - это отдельная тема, и раскрывать его в рамках этой статьи я не буду.
tmpfs - это ФС, которая хранит все свои файлы в оперативной памяти (и, при необходимости, разделе подкачки), а не на жёстком диске. Она идеально подходит для временных файлов, которые не нужно хранить долго, но к которым требуется очень быстрый доступ. В Linux она обычно используется для /tmp, /var/tmp и /dev/shm. Главное преимущество здесь - потрясающая скорость работы. А также после перезагрузки все данные из tmpfs автоматически исчезают.
FAT32 / vfat: Обязательна для загрузочного раздела EFI System Partition (ESP). Прошивка UEFI умеет читать только FAT, поэтому этот раздел должен быть в vfat, независимо от того, какая ФС стоит на корне.
VFS - Виртуальная файловая система
Почему появился VFS?
Начнём с того, что VFS появилась не просто так, а как средство для решения проблемы.
А чтобы понять суть антивируса, нужно увидеть саму заразу: в 1980х мир операционных систем стремительно разрастался, а вместе с ним рос и настоящий кошмар совместимости.
Представьте, что вы - программист того времени, который пишет свой текстовый редактор. На рынке существует несколько видов накопителей и файловых систем. И это порождало следующие проблемы:
Усложнение кода. Просто чтобы сохранить файл, наша программа должна была выяснять, на какую ФС шлётся файл. Если это локальный диск с родным UFS, то нам нужно сделать это способом A. Если пользователь хочет сохранить файл в папку на базе NFS, то нам нужен радикально иной способ B. Итого у нас банальная функция
save_file()превращалась в гигантский клубок изif (is_local_ufs) { ... } else if (is_remote_nfs) { ... }, где под каждую ФС писался свой уникальный низкоуровневый код.Каждый был сам за себя. Операционная система НЕ давала какого-либо универсального интерфейса для работы с файлами. У локальной UFS и сетевой NFS были совершенно разные структуры в ядре. Не существовало единого системного вызова, который мог бы прозрачно обработать обе файловые системы.
Ад при масштабировании ФС. Представьте, что ваша программа написана, оттестирована и уже стала продаваться. Как вдруг выходит новая файловая система - например, FAT. Для нашего приложения это инопланетянин, у которого совсем другая структура каталогов и таблица распределения файлов. Чтобы программа просто "увидела" дискету с этой ФС, вам нужно брать её исходный код, дописывать туда очередной
else if (is_fat), заново компилировать приложение и накатывать патч.
Разработчики тонули в дублировании кода. Вместо того, чтобы развивать логику программы, приходилось писать бесконечные переводчики под каждую новую ФС. И нам в то время критически не хватало прослойки, которая скрыла бы всё это безобразие под капотом...
Вот тут и появился VFS, чтобы раз и навсегда решить эту проблему.
Что такое VFS и какие проблемы он решает?
VFS (Virtual File System) - это программный слой абстракции, объединяющий различные типы файловых систем под единым интерфейсом.
Звучит завуалированно, правда? Давайте разберём получше, как именно VFS решает нашу проблему.
Полная изоляция программы от деталей реализации. Программе больше не нужно знать, как устроена ФС. Будь то ext4 или FAT32 - для приложения это просто стандартный поток байт.
Обеспечение расширяемости. VFS сделала поддержку ФС модульной структурой. Если завтра вы придумаете новую ФС
MyCoolFs, вам не придётся переписывать свои программы. Вы просто пишете драйвер под стандарты VFS, "втыкаете" его, и система начинает с ним работать.Создание единого пространства. VFS позволяет бесшовно склеивать разные ФС. Например, вы можете примонтировать (о монтировании поговорим в другой статье) флешку с exFAT в папку внутри локальной ext4, а внутрь - ещё и сетевую папку NFS. Для юзера и программ это будет выглядеть как единое дерево файлов.
Как VFS работает? Или просто его функции
Чтобы решать все эти задачи, VFS должен обладать определёными функциями. Когда программа вызывает команду для открытия файла, VFS перенаправляет её (команду) нужному адресату. Сами функции VFS можно разделить на три ключевые группы:
Трансляция системных вызовов. Когда команда делает обычный системный вызов
read, запрос сначала прилетает в VFS. VFS смотрит на дескриптор нашего файла и понимает: "Ага, этот файлик лежит на разделе с ext4" - и мгновенно перенаправляет вызов в специфическую функцию драйвера ext4.Введение единого стандарта для ФС. VFS вводит универсальные структуры данных, которые обязана понимать ЛЮБАЯ ФС:
Суперблок (Superblock): содержит общую информацию о примонтированной системе.
Индексный дескриптор (Inode): описывает конкретный файл (размер, права доступа, блоки данных, НО НЕ ИМЯ)
Запись каталога (Dentry, Directory Entry): связывает человекочитаемое имя файла с его inode, отвечает за логику путей (/home/user228/Downloads)
Объект открытого файла: хранит состояние взаимодействия процесса с файлом (например, текущую позицию, где именно программа сейчас читает или пишет данные).
Кэширование. Т.к VFS находится, грубо говоря, между приложениями и ФС, он берёт на себя оптимизацию. Если конкретно, то:
Он управляет Dentry Cache (сокращённо dcache - хранит в RAM соответствие между путями/именами файлов и их inode), чтобы ядру не приходилось парсить весь диск.
Также у VFS есть Page Cache (хранит в RAM недавно считанные с диска или подготовленные для записи блоки данных).

FD - Файловые дескрипторы
Файловый дескриптор - это целочисленный идентификатор, который ОС присваивает открытому файлу, сокету, пайпу, устройству или другому устройству ввода-вывода. Через этот дескриптор процесс взаимодействует с объектом: читает из него данные, записывает.
Дальше для сокращения я вместо "файловый дескриптор" буду использовать FD.
Пример:
Наш процесс хочет прочитать файл text.txt. Для этого он делает syscall read() и обращается к ядру. А ядро выделяет ему файловый дескриптор (например, 3). Дальше процесс работает работает только с этим номером, и чтение файла происходит через read(3, ...). Ядро само знает, какой файл стоит за номером 3, и безопасно отдаёт процессу данные.
То есть, дескриптор служит "ссылкой", через которую процесс может выполнять операции с файлами (напомню, в Linux всё является файлом, включая сетевые соединения).
Крайне важные особенности FD
Во-первых, КАЖДЫЙ ПРОЦЕСС имеет СВОЮ СОБСТВЕННУЮ таблицу дескрипторов. Даже если два процесса откроют один и тот же файл, у каждого будет свой уникальный дескриптор.
Во-вторых, существуют СТАНДАРТНЫЕ ДЕСКРИПТОРЫ. Это три заранее зарезервированных числа:
0 — стандартный ввод (stdin).
1 — стандартный вывод (stdout).
2 — стандартный поток ошибок (stderr)
В-третьих, ЛИМИТЫ И ОГРАНИЧЕНИЯ. Каждый раз, когда ваш Nginx или высоконагруженный бэкенд начинает сыпать ошибками Too many open files — знайте, что это 'подарочек' именно от лимитов дескрипторов.
Существует два уровня ограничений:
Лимит процесса (ulimit): Настраивается через
ulimit -n(или в/etc/security/limits.conf). Если процесс упирается в него, он получает ошибкуEMFILE(Too many open files)Глобальный лимит системы: Исторически за него отвечал параметр
/proc/sys/file-max. При его достижении ядро выбрасывало ошибкуENFILE. В современных версиях ядра линукса этот лимит стал более гибким, но понимать разницу между «закончились лимиты у приложения» и «закончились лимиты у ОС» нужно обязательно.
Иногда достижение лимита происходит из-за утечки дескрипторов, когда процесс не закрывает дескриптор вовремя, но накапливает другие. За закрытие дескриптора отвечает системный вызов close().
Ключевые команды для работы с дескрипторами / Практическая часть
Это всё, конечно, хорошо. Но вот более практический вопрос: "А зачем нам, как DevOps-инженеру, это нужно? С чём мы столкнёмся?"
Зачем нам это нужно? Если кратко, то в основном - для траблшутинга и масштабируемости. Нам они нужны для поддержания стабильности и масштабируемости инфраструктуры: правильная настройка лимитов FD защищает высоконагруженные сервисы (условный Nginx или БД) от падения с ошибкой Too many open files. А анализ открытых дескрипторов позволяет быстро находить утечки ресурсов и зависшие процессы.
Из самых часто встречающихся проблем можно выделить: исчерпания лимитов, утечки дескрипторов, проблема "удалил файл через rm, но процесс продолжает в него записывать", исчерпание системного лимита(когда лимиты заканчиваются на уровне ОС),
Для работы с дескрипторами нам помогут утилиты lsof, ls, ulimit, а также встроенные в bash инструменты перенаправления ввода-вывода.
Справка:
lsof- это утилита, предназначенная для вывода информации об открытых файлах и процессах, которые их используют.
Рекомендуемое чтиво: https://habr.com/ru/companies/ruvds/articles/337934/
Применений у этой команды в контексте в контексте диагностики тысяча и одна, на практике нам, скорее всего, часто придётся писать с ними скрипты.
Вот, в каком формате у lsof происходит вывод:
COMMAND PID TID USER FD TYPE DEVICE SIZE/OFF NODE NAME systemd 1 root cwd DIR 254,1 4096 2 / systemd 1 root rtd DIR 254,1 4096 2 / sshd 1420 root 3u IPv4 32451 0t0 TCP *:ssh (LISTEN)
Расшифровка основных колонок:
COMMAND: Имя процесса (может усекаться).
PID: Идентификатор процесса (Process ID).
TID: Идентификатор потока (Thread ID), если применимо.
USER: Имя пользователя, запустившего процесс.
FD: Это самый важный столбец, обычно состоящий из «числа» и «режима доступа» (например,
1w,2u), а также может быть специальным значением:Число: представляет собой реальный файловый дескриптор.
0,1,2обычно означают стандартный ввод, стандартный вывод, стандартный вывод ошибок.Режим доступа:
rозначает только чтение,w- только запись,u- чтение и запись. Это означает, в каком режиме был открыт файл, а не то, какие права есть у ВАШЕГО юзера(использовавшего lsof есть).Специальные значения:
cwd- текущий рабочий каталог,txt— исполняемый файл или библиотека программы,mem— отображённый в памяти файл,rtd— корневой каталог. Я об этих полях расскажу чуть позже, на примере конкретного процесса.
TYPE: Тип файла (
DIR- каталог,REG- обычный файл,IPv4/IPv6- сетевой сокет).DEVICE: Номер устройства (мажорный и минорный).
SIZE/OFF: Размер файла или смещение внутри него.
NODE: Номер иноды (inode) на диске.
NAME: Полный путь к файлу, точка монтирования или параметры сетевого соединения (IP и порты).
Основные команды
lsof - вывести список вообще всех открытых файлов в системе в данный момент.
lsof -p <PID> - показывает вообще все задействованные файлы процесса: cwd (каталог), txt (бинарник), mem (библиотеки в ОЗУ) и числовые FD. Технически mem, txt НЕ ЯВЛЯЮТСЯ ФАЙЛОВЫМИ ДЕСКРИПТОРАМИ. Они представляют с собой ссылки на специальные объекты состояния процесса в папке /proc/PID (например, /proc/<PID>/cwd, /proc/<PID>/root, /proc/<PID>/exe), а не открытые программой через open() дескрипторы.
lsof -i - показать процессы, связанные с сетевыми сокетами.
lsof -i :<port> - найти процесс, который держит конкретный сетевой порт (например, lsof -i :8080)
lsof -u <user> - фильтрация открытых дескрипторов для конкретного пользователя.
lsof +D /path/to/dir - показывает все процессы, которые используют файлы внутри указанной директории.
lsof | grep deleted - классика. Показывает файлы, которые якобы были удалены с диска, но продолжают удерживаться процессами и занимать место на файловой системе.
ls -la /proc/<PID>/fd - Аналог lsof -p. В этом каталоге для каждого открытого FD создаётся символическая ссылка. В отличие от lsof -P, выводи исключительн открытые FD без mem, cwd, txt, rtd.
cat /proc/<PID>/fdinfo/<FD> - получение подробной информации о состоянии дескриптора (например, текущей позиции указателя чтения/записи или флагов).
ulimit -n - показывает максимальное количество открытых файловых дескрипторов для текущей сессии оболочки (shell) и порождённые ею процессы.
ulimit -Hn / ulimit -Sn - просмотр жесткого и мягкоголимитов.
cat /proc/sys/fs/file-max - просмотр системного лимита на максимальное число открытых файлов во всей ОС.
cat /proc/sys/fs/file-nr - статистика использования файловых дескрипторов ядром (всего выделено, используется, максимум).
Мягкие (soft) и жёсткие (hard) лимиты
Мягкий лимит - это фактическое рабочее ограничение, которое ядро ОС проверяет при попытке открыть новый файл. Обычный юзер или процесс МОЖЕТ ВРЕМЕННО ПРЕВЫСИТЬ мягкий лимит.. Значение мягкого лимита можно самостоятельно повышать или понижать изнутри процесса (или через команду ulimit), но только в пределах установленного жесткого лимита
Жёсткие лимиты - это абсолютный потолок (максимальное значение), выше которого мягкий лимит поднят быть не может. Его нельзя обойти или превысить ни при каких условиях для обычного пользователя. Повысить жесткий лимит имеет право только суперпользователь.
Флаги
Когда процесс открывает файл с помощью системного вызова open(), ядро не только даёт ему порядковый FD, но и привязывает к этому дескриптору определённый набор флагов.
Эти флаги определяют, как именно процесс будет взаимодействовать с файлом.
Стоит также упомянуть, что под понятием «флаги дескриптора» на самом деле объединяет две разные сущности, которые хранятся в разных местах VFS: флаги самого дескриптора (FD Flags) и флаги состояния открытого файла (File Status Flags).
Чем они отличаются?
Флаги дескриптора (например,
CLOEXEC) — отвечают за жизнь самого номера FD при наследовании процессами. Хранятся в таблице процесса.Флаги состояния (например,
O_NONBLOCK,O_APPEND) — отвечают за поведение ввода-вывода (блокирующий/неблокирующий, чтение/дозапись). Хранятся в системной таблице VFS.
Зачем нам это знать?
На практике, как я понял, опрашивая знакомых, DevOps редко сталкивается с флагами FD в повседневной работе. Я решил про них написать больше для того, чтобы мы просто знали, что ЭТО существует.
Конкретные флаги:
Флаги файловых дескрипторов (FD Flags):
FD_CLOEXEC(Close-on-exec) — единственный стандартизированный флаг дескриптора. Инструктирует ядро автоматически закрыть этот числовой дескриптор в дочернем процессе сразу после выполнения системного вызоваexecve().
Флаги состояния открытого файла (File Status Flags):
Делятся на разные группы: режимы доступа, операционные флаги, флаги создания. Но я перечислю пять из всех:O_NONBLOCK(Неблокирующий ввод-вывод). Переводит дескриптор в режим «не жди». Если программа пытается прочитать данные из сетевого сокета, а их там нет, ядро не усыпляет процесс, а мгновенно возвращает управление с ошибкой.O_APPEND(Режим дозаписи). Заставляет ядро перед каждой операцией записи принудительно сдвигать указатель текущей позиции в самый конец файла.O_DIRECT(Прямой ввод-вывод). Дает команду ядру Linux выполнять чтение и запись напрямую с физического диска в буфер приложения в обход системного Page Cache.O_SYNC/O_DSYNC(Синхронная запись). локирует выполнение программы при вызовеwrite()до тех пор, пока данные физически не будут записаны на жесткий диск или SSD. По умолчанию Linux ради скорости сначала пишет всё в оперативную память (кэш), а на диск сбрасывает позже.O_SYNCотменяет это поведение.O_NOATIME(Запрет обновления времени доступа). Указывает ядру не обновлять метаданные файла в полеatime(access time) при его чтении. По умолчанию Linux при каждом просмотре файла делает микрозапись на диск, отмечая «время последнего чтения». Флаг позволяет избавиться от миллионов лишних и бесполезных мелких операций записи на диск, снижая нагрузку на систему.
Подробно углубляться в практическую работу с флагами я не стану, потому что статья тогда ужасно растянется. Но упомяну, что для этого используются утилиты fcntl и flock . Вот самый простой пример использования fcntl (сгенерировано через Gemini, как пример):
# Открываем файл на дескрипторе 3 и ставим эксклюзивную блокировку exec 3> /var/run/myscript.lock fcntl 3 F_SETLK F_WRLCK # Если блокировка успешна, выполняем код echo "Выполняются критические операции..." # Освобождаем блокировку и закрываем дескриптор fcntl 3 F_SETLK F_UNLCK exec 3>&-
Подводные камни, связанные с FD
Ловушка мягкого и жёсткого лимитов. Если вы просто выполните
ulimit -n 65535в терминале перед запуском приложения, этот лимит применится только к текущей сессии командной строки и ее дочерним процессам. Если ваш сервис управляется черезsystemd, он проигнорирует эти настройки. Дляsystemd-сервисовлимиты нужно жестко прописывать в их unit-файлах (параметрLimitNOFILE=65535).Иллюзия бесконечности системного лимита. Параметр
fs.file-max(редактируется через файл/proc/sys/fs/file-max) определяет глобальный лимит на всю ОС. Даже на мощных серверах под нагрузкой из миллионов сокетов дефолтных больших значений от ядра может не хватить. Как следствие - сервер ляжет.Утечка FD в контейнерах. Всякий контейнер имеет свой изолированный корень
rtd. Если приложение внутри контейнера "потечёт" по дескрипторам, оно может исчерпать лимиты не только внутри себя, но и забрать доступные ресурсы у всей хост-системы, если на уровне демона контейнеризации или рантайма не были выставлены четкие ограничения на количество открытых файлов.Скрытая утечка дискового пространства через FD. Процесс держит файл открытым, но администратор удаляет его через
rm. В итоге имя файла исчезает из структуры каталогов, но место на диске не освобождается. В таких случаях помогает только перезапуск процесса, удерживающего FD, либо очистка файла «на лету» через перенаправление (> /proc/PID/fd/FD_NUMBER), если процесс нельзя останавливать.
Пояснение про txt и cwd на конкретном примере
Если вы поняли, о чём идёт речь, можете идти к пункту про перенаправление потоков. Я, при изучении материала для статьи, сразу не понял, поэтому решил всё разобрать на конкретном примере.
Представьте, что вы запустили простую команду:
cd /home/devops/project python3 app.py
В другом терминале выполняем(чтобы без лишних танцев с бубном получить PID процессора):
lsof -p $(pgrep -f app.py)
И видим примерно такой вывод (упрощённо):
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME python3 4242 devops cwd DIR 8,1 4096 123456 /home/devops/project python3 4242 devops rtd DIR 8,1 4096 2 / python3 4242 devops txt REG 8,1 5763456 234567 /usr/bin/python3.11 python3 4242 devops mem REG 8,1 2216304 345678 /usr/lib/x86_64-linux-gnu/libc.so.6 python3 4242 devops mem REG 8,1 108960 456789 /usr/lib/x86_64-linux-gnu/libz.so.1.2.13 python3 4242 devops mem REG 8,1 1847560 567890 /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so python3 4242 devops 0r CHR 1,3 0t0 1024 /dev/null python3 4242 devops 1w REG 8,1 1024 456789 /home/devops/project/app.log python3 4242 devops 2w REG 8,1 2048 567890 /home/devops/project/error.log python3 4242 devops 3u IPv4 12345 0t0 TCP *:8080 (LISTEN) python3 4242 devops 4r REG 8,1 8192 678901 /home/devops/project/config.yaml
Что такое cwd?
python3 4242 devops cwd DIR ... /home/devops/project
Это текущий рабочий каталог процесса. Тот самый, куда смотрит относительный путь. Если внутри app.py написано open("config.yaml") - файл будет искаться именно в /home/devops/project/config.yaml, а не где-то ещё. Также в этом поле указан тип DIR, потому что это каталог.
Что такое txt?
python3 4242 devops txt REG ... /usr/bin/python3.11
Это исполняемый файл программы, который ядро отобразило в память и выполняет.
Тип REG означает, что это обычный файл: текстовый файл, исполняемый файл или лог-файл.
Обратите внимание: txt указывает на интерпретатор /usr/bin/python3.11, а не на ваш app.py. Потому что с точки зрения ядра выполняется именно бинарник Python, а app.py — это данные, которые он читает (и они появятся в списке как обычный FD или mem).
Что такое mem?
python3 4242 devops mem REG ... /usr/lib/x86_64-linux-gnu/libc.so.6 python3 4242 devops mem REG ... /usr/lib/x86_64-linux-gnu/libz.so.1.2.13 python3 4242 devops mem REG ... /usr/lib/python3.11/lib-dynload/_ssl.cpython-311-x86_64-linux-gnu.so
mem обозначает файл, который загружен в память процесса через mmap() (memory-mapper file). Чаще всего в качестве такого файла выступает разделяемая библиотека (.so в Linux) или часть исполняемого кода, которые процесс подгрузил в свое адресное пространство.
Что такое rtd?
python3 4242 devops rtd DIR ... /
rtd (root directory) - это корневой каталог, который процесс считает своим /. У обычного процесса это просто /. Но если процесс запущен в, например, контейнере (Docker), rtd укажет на другой каталог - например, /var/lib/docker/overlay2/.../merged или /srv/chroot/debian.
Метафорически, это "якорь" для разрешения абсолютных путей: когда процесс открывает /etc/nginx/nginx.conf, ядро ищет его относительно rtd.
Перенаправление потоков
Было бы вопиющим безобразим рассказать о файловых дескрипторах, не упомянув при этом перенаправление потоков.
В Linux действует принцип: "Всё есть файл", включая наши устройства, такие как клавиатура и экран. А процессы, в свою очередь, для работы с файлами используют файловые дескрипторы. Для каждого процесса по умолчанию зарегистрировано три таких:
0 - stdin - ввод, привязанный к клавиатуре.
1 - stdout - вывод, привязанный к экрану.
2 - stderr - вывод ошибок, также привязанный к экрану.
Зачем stdout и stderr разделили?
Разделение было необходимо, чтобы отделить обычный результат работы программы от служебных сообщений и ошибок, которые должны улетать в лог-файлы.
По умолчанию эти два потока выводятся на один экран (в терминал), но на уровне ОС это два разных файловых дескриптора, вывод которых можно разделить в отдельные файлы или передать другим командам.
Перенаправление вывода с перезаписью файла:
>Оператор сообщает шеллу закрыть дескриптор 1 для этой команды и перенаправить его в указанный файл. Перед выполнением самой команды шелл полностью очищает целевой файл или создаёт его, если того не существовало.
Формат:
[command] > [file]Пример:
echo "Hello, world" > text.txt(файл сотрется, и в нём останется только "Hellom world!")Перенаправление вывода БЕЗ перезаписи файла:
>>Механика аналогична
>, но с ключевым отличием: файл открывается в режиме дозаписи (append). Иными словами, вывод команды перемещается в конец файла.Формат:
[command] >> [file]Пример:
date >> log.txt(добавит текущую дату новой строкой в конец файла).Чтение из файла:
<Этот оператор меняет источник для дескриптора
0. Вместо ожидания ввода от пользователя с клавитуры, команда начинает читать данные из файла с первой строки до символа конца файла (EOF).Интересный факт: вместо простого
<можно использовать0<. Разницы между ними нет, просто во втором случае мы явно перенаправляем содержимое файла вstdin.Формат:
[command] < [file]Пример:
read line < text.txt(считает первую строку из файла в переменнуюline).Перенаправление ошибок:
2>и2>>Явное указание цифры
2перед оператором отделяет поток ошибок от стандартного вывода. Иными словами, мы можем сделать так, чтобы сообщения об ошибках выводились в файлerrors.log, а на экране терминала появлялся обычный вывод.Формат для
>:[command] 2> [file]Формат для
>>:[command] 2>> [file]Объединение потоков:
2>&1Что это? Этот синтаксис буквально означает: "Направь дескриптор 2 (stderr) туда же, куда в данный момент укзаывает дескриптор 1 (stdout)". Знак амперсанда (&) здесь критичен - он указывает оболочке, что
1- это дескриптор потока, а не имя файла с названием "1".ВАЖЕН ПОРЯДОК! Пример:
Команда
command > file.txt 2>&1сработает корректно (сначала1направляется в файл через> file.txt, а затем2направляется туда же через2>&1). Но если написатьcommand 2>&1 > file.txt, то ошибки уйдут в терминал (куда изначально смотрел1), а в файл запишется только чистыйstdout.Формат:
[command] > [file] 2>&1Объединение потоков через
&>Это упрощённый синтаксис, добавленный в шелл для замены громоздкого
> [file] 2>&1. Делает он то же, что и его предшественник: одновременно перенаправляет и дескриптор1, и дескриптор2в один целевой файл.Используется в случаях, когда нужно, чтобы весь лог команды был в одном месте и не имело значения, возникла ошибка или команда отработала успешно.
Конвейеры (пайпы, пайплайнинг):
|Пайпы - это механизм межпроцессного взаимодействия, который позволяет передавать
stdoutодной команды напрямую вstdinдругой. Упомяну, что данные передаются потоково, без промежуточного сохранения на жёсткий диск.По умолчанию пайпы передают только
stdout, но вместе с основным потоком можно передать также иstderr.Формат для
stdout:[command №1] | [command №2]Формат для
stderr:[command №1] |& [command №2]Пример:
ls -la /etc/hosts /not/exists/folder |& grep "No Such"Разветвление потока через
teetee- это утилита, читающая данные из стандартного потока ввода (stdin) и одновременно отправляющая их в два места: в стандартный поток вывода (stdout, то есть на экран терминала) и в один или несколько файлов. Всегда используется вместе с пайпами.Формат:
[command] | tee [опции] [файл]Пример:
ls -l | tee output.txt(список файлов отобразится в терминале и запишется в файлoutput.txt).
Коротко о epoll
Так как мы выяснили, что в Linux всё является файлом (включая сетевые сокеты), а процессы общаются с ними через файловые дескрипторы (FD), возникает логичный вопрос:
А как серверные приложения (вроде Nginx) умудряются одновременно обрабатывать 50 000 открытых сетевых соединений?
Если процесс будет опрашивать каждый дескриптор по очереди в цикле, система просто "ляжет"(в теории) от нагрузки на процессор. Для решения этой проблемы в ядре Linux и существует epoll (event poll) - высокоэффективный механизм мониторинга событий ввода-вывода.
Хорошо, а нам это зачем? А затем, что понимание epoll критически важно, так как именно эта технология лежит в основе масштабируемости и производительности большинства современных серверных приложений.
К сожалению, сам я с этой технологией не работал и услышал о ней от знакомого буквально вчера, когда спрашивал его мнение насчёт статьи. Так что уверенно что-то сказать про epoll, дать какие-либо советы или факты я не могу.
И тем не менее я с ней разберусь и, возможно, в будущем напишу про неё отдельную статью. Если вы в теме, то буду очень благодарен за советы или комментарии.
Послесловие
Статья получилась очень объёмной, но я постарался уместить только самую нужную для практики информацию. Глубоких подробностей, таких как работа с флагами дескрипторов, я не затронул,как и работу с epoll.
В следующей статье мы поговорим об айнодах, а также составим полноценную схематическую карту того, что происходит когда мы открываем файл через cat или пытаемся записать туда данные. от системного вызова open до закрытия файлового дескриптора.
Источники, по которым я работал
Нейросеть Gemini - консультация
Нейросеть DeepSeek - консультация, редактирование


