Почему lsof в Linux видит почти всё
Главная философия Unix - "всё есть файл". Обычный файл на диске - файл. Каталог - файл. Сетевой сокет, через который веб-сервер слушает порт 443 - тоже файл (точнее, файловый дескриптор). Пайп между процессами, символьное устройство /dev/null, загруженная в память разделяемая библиотека, eventfd, inotify-наблюдатель - всё это для ядра однотипные объекты, на которые процесс ссылается через файловый дескриптор (file descriptor, сокращённо fd).
Дескриптор - это просто целое неотрицательное число, индекс в таблице открытых файлов конкретного процесса. Когда программа делает системный вызов open(), socket() или accept(), ядро возвращает ей наименьший свободный номер: 3, 4, 5 и так далее (0, 1, 2 заняты под stdin, stdout, stderr). Дальше процесс работает с объектом по этому номеру и обязан вызвать close(), когда закончил. Забыл закрыть - дескриптор "течёт".
lsof проходит по всем процессам и показывает, какие объекты каждый из них держит открытыми. Под капотом она читает псевдо-файловую систему /proc: в каталоге /proc/PID/fd лежат символические ссылки на всё, что открыл процесс с этим PID. lsof собирает это в удобную таблицу. Глянуть можно и без lsof, голыми руками:
Код: Выделить всё
ls -l /proc/$$/fd
# lrwx------ 1 user user 64 ... 0 -> /dev/pts/0
# lrwx------ 1 user user 64 ... 1 -> /dev/pts/0
# lrwx------ 1 user user 64 ... 2 -> /dev/pts/0

Читаем вывод lsof по колонкам
Самый частый запуск - посмотреть, что открыл конкретный процесс по его PID:
Код: Выделить всё
sudo lsof -p 774
Код: Выделить всё
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 774 root cwd DIR 259,2 4096 2 /
nginx 774 root rtd DIR 259,2 4096 2 /
nginx 774 root txt REG 259,2 1264424 131841 /usr/sbin/nginx
nginx 774 root mem REG 259,2 2030928 131720 /usr/lib/x86_64-linux-gnu/libc.so.6
nginx 774 root 0u CHR 1,3 0t0 6 /dev/null
nginx 774 root 6u IPv4 28931 0t0 TCP *:443 (LISTEN)
nginx 774 root 9w REG 259,2 51234 131900 /var/log/nginx/access.log
- COMMAND - имя процесса (по умолчанию обрезано до 9 символов; флаг +c 0 или +c 15 расширяет ширину, если имя длинное).
- PID - его номер. По нему ты находишь и при необходимости прибиваешь процесс.
- USER - от какого пользователя процесс запущен.
- FD - тот самый дескриптор. Тут либо число с буквой режима (0u, 6u, 9w), либо специальное слово. Буквы: r - открыт на чтение, w - на запись, u - и чтение, и запись. Спецслова: cwd - текущий рабочий каталог процесса, rtd - корневой каталог (root), txt - исполняемый код самой программы (text), mem - файл, отображённый в память (обычно библиотеки через mmap). Если видишь DEL - файл удалён, но mmap-отображение ещё держится в памяти (запомни, к этому вернёмся).
- TYPE - тип объекта: REG - обычный файл, DIR - каталог, CHR - символьное устройство, IPv4/IPv6 - сетевой сокет, unix - локальный сокет, FIFO - именованный пайп, a_inode - анонимный inode (epoll, eventfd, inotify).
- DEVICE - номера устройства (мажор, минор) или адрес объекта в ядре.
- SIZE/OFF - размер файла либо текущее смещение чтения/записи (0t0 значит "смещение 0", запись 0t показывает десятичное смещение).
- NODE - номер inode для файла, либо протокол (TCP/UDP) для сокета.
- NAME - путь к файлу или описание соединения, например *:443 (LISTEN).
Практика: кто держит файл и кто держит порт
Кто держит файл в Linux. Классика - не можешь размонтировать или удалить раздел, потому что файл "занят". Передаём lsof путь:
Код: Выделить всё
sudo lsof /var/log/nginx/access.log
Код: Выделить всё
sudo lsof +D /var/log/nginx # рекурсивно по всему дереву, полно, но медленно
sudo lsof +d /var/log/nginx # только сам каталог, без спуска вглубь
sudo lsof /mnt/disk # быстрая проверка точки монтирования целиком
Кто держит порт. Сервис ругается "address already in use". Узнаём владельца порта:
Код: Выделить всё
sudo lsof -i :443 -nP
# COMMAND PID USER FD TYPE DEVICE NODE NAME
# nginx 774 root 6u IPv4 28931 TCP *:443 (LISTEN)
Важная оговорка на 2026: для сетевых вопросов lsof давно не первый выбор. Современный штатный инструмент - ss из пакета iproute2: ss -tlnp покажет все слушающие TCP-сокеты с процессами быстрее и точнее, чем lsof, и без обхода всех /proc. lsof -i удобен, когда нужно одной командой увидеть и сетевые сокеты, и файлы процесса вместе. Держи в голове обе: ss -tulpn для сети, lsof -p PID для полной картины процесса.
Утечка дескрипторов и ошибка too many open files
Теперь та самая прод-ошибка. У каждого процесса есть лимит на число открытых дескрипторов. Упёрся в него - получаешь в логах "Too many open files" (errno EMFILE), и приложение начинает отбрасывать соединения и не может открывать файлы. Смотрим лимит:
Код: Выделить всё
ulimit -n # мягкий лимит текущей сессии
cat /proc/774/limits | grep "open files"
# Max open files 1024 524288 files
Код: Выделить всё
ls /proc/774/fd | wc -l # быстрый способ, прямо из /proc
sudo lsof -p 774 | wc -l # то же через lsof (чуть больше из-за cwd/rtd/txt/mem)
Код: Выделить всё
sudo lsof -p 774 | awk '{print $5}' | sort | uniq -c | sort -rn
# 8901 IPv4
# 120 REG
# ...
Удалённый, но открытый файл - почему df и du расходятся
Третья загадка, и она же самая частая причина внезапно "забитого" диска. В Linux, если процесс держит файл открытым, а ты его rm-нул, место не освобождается до закрытия дескриптора. Файл исчезает из каталога, du его не видит (ему нечего обходить - имени больше нет), но блоки на диске заняты, и df это честно показывает. Найти таких "призраков" умеет lsof:
Код: Выделить всё
sudo lsof +L1
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
# java 2210 app 35w REG 259,2 9.2G 0 4521 /var/log/app.log (deleted)
- Самое чистое - корректно перезапустить или сигнализировать процессу переоткрыть файлы (для логов часто помогает logrotate с copytruncate или kill -HUP/-USR1, если сервис умеет переоткрывать лог).
- Если ронять прод нельзя и файл - именно лог в режиме append, можно обнулить его через дескриптор, не убивая процесс:
Код: Выделить всё
sudo truncate -s 0 /proc/2210/fd/35
# или эквивалент через перенаправление:
sudo sh -c ': > /proc/2210/fd/35'
lsfd - современная замена lsof (актуально на 2026)
С версии util-linux 2.38 в дистрибутивах появился lsfd (list file descriptors) - Linux-специфичная замена lsof. Он не drop-in (другой CLI и формат), зато заточен ровно под Linux: понимает cgroups, сетевые namespace, отдаёт JSON, и колонки задаёшь сам. Базовый запуск:
Код: Выделить всё
lsfd # все дескрипторы системы
lsfd -p 774 # как lsof -p
lsfd --list-columns # все доступные колонки
lsfd -Q 'TYPE == "IPv4"' -o PID,COMMAND,NAME # фильтр-запрос
lsfd -J -o PID,FD,NAME # JSON для скриптов
Типичные грабли и заблуждения
- Запуск без sudo. lsof покажет только твои процессы и молча умолчит о чужих - отсюда классика "lsof -i :443 ничего не выводит, хотя порт занят". Чужие порты и файлы root видны только под sudo.
- Пустой вывод lsof -i при занятом порте бывает и из-за сетевых namespace: сокет живёт внутри контейнера (Docker, Podman, systemd-nspawn). Тогда смотри изнутри неймспейса (nsenter -t PID -n ss -tlnp) или используй lsof/ss с правами на хосте.
- +D на большом каталоге может работать минуты: он обходит дерево рекурсивно и stat-ит каждый файл. Для точки монтирования хватит lsof /mnt/disk.
- Имя в COMMAND обрезано до 9 символов - не пугайся куцых названий, ориентируйся на PID или добавь +c 0.
- ulimit -n меняет лимит только для текущей оболочки и её потомков. На уже запущенный сервис он не влияет - правь LimitNOFILE= в unit-файле и делай systemctl daemon-reload + рестарт.
- Заблуждение "удалил большой файл - место сразу вернулось". Нет: пока хоть один процесс держит fd, блоки заняты. Проверяй lsof +L1.
- Выполни ls -l /proc/$$/fd и найди дескрипторы 0, 1, 2 - это твой терминал.
- Запусти sleep 600 & запиши PID, в другом окне sudo lsof -p PID - посмотри cwd, rtd, txt.
- Сделай так: echo test > /tmp/test.log; tail -f /tmp/test.log & ; rm /tmp/test.log; потом sudo lsof +L1 | grep test - найди (deleted) файл, посмотри NLINK 0.
- Освободи его, не убивая tail: найди fd через ls -l /proc/PID/fd и сделай sudo truncate -s 0 /proc/PID/fd/N (для append-лога безопасно).
- Проверь, кто держит порт: sudo lsof -i :22 -nP, а затем сравни с sudo ss -tlnp 'sport = :22'.
- Если есть util-linux 2.38+: повтори lsof -p PID через lsfd -p PID, сравни вывод.
- Что означают значения cwd, txt, mem и DEL в колонке FD?
- Каким одним флагом lsof найти удалённые, но всё ещё открытые файлы, съевшие место, и какое значение NLINK у них?
- Как узнать лимит дескрипторов процесса и сколько он занял сейчас, и почему ulimit -n в твоей оболочке тут может врать?
- Чем lsof -i :443 удобнее и в чём проигрывает команде ss -tlnp? И что такое lsfd?
lsof - твой рентген файловых дескрипторов. Колонки FD и NAME - главные: по ним видно, держит процесс файл, сокет или библиотеку. lsof файл, lsof +D и fuser отвечают "кто держит файл linux" перед размонтированием. lsof -i :порт -nP находит владельца порта, но для сети на 2026 первым бери ss -tlnp. ulimit/limits плюс счёт через /proc/PID/fd ловят утечку и ошибку too many open files. lsof +L1 - способ увидеть удалённые открытые файлы (NLINK 0), из-за которых расходятся df и du; чинить рестартом, а truncate-через-fd только для append-логов. А в свежих системах присматривайся к lsfd - штатной Linux-замене lsof с фильтрами и JSON.