Что такое файловый дескриптор — и почему Too many open files бывает без файлов

Сервер падает с ошибкой Too many open files. Ты открываешь код — а он вообще не работает с файлами. Только принимает запросы.
Смотри, в чём хитрость. Для системы «файл» — это почти всё. Сетевое соединение тоже. И лимит у них один на всех.
Что такое файловый дескриптор?
Файловый дескриптор — это номерок, который система выдаёт программе за всё, что та открыла.
Представь гардероб. Ты сдаёшь куртку и получаешь номерок. Дальше ты показываешь только номерок, а куртку ищет гардеробщик. Программа делает то же самое. Она говорит: «открой файл», система отвечает: «держи номер 3». Потом программа просит: «прочитай из номера 3».
Номерок получают:
- обычные файлы;
- сетевые соединения — каждый посетитель твоего сайта;
- трубы между командами — та самая
|; - сам терминал.
А теперь про крючки. Их у программы конечное число. Обычно 1024 на Linux и 256 в терминале на macOS. Посмотреть свой лимит можно так:
ulimit -n
Закончились крючки — следующий посетитель получает ошибку EMFILE. Это и есть Too many open files.
Как это работает
Первые три номера заняты ещё до старта программы:
- 0 — stdin, откуда программа читает ввод;
- 1 — stdout, куда пишет результат;
- 2 — stderr, куда пишет ошибки.
Поэтому первый файл, который ты откроешь, получит номер 3. Проверь сам:
python3 -c "import os; print(os.open('/etc/hostname', os.O_RDONLY))"
# 3
И это объясняет странную запись 2>&1. Там нет никакой магии: «отправь поток номер 2 туда же, куда идёт номер 1». Подробнее — в статье что такое stdout и stderr.
Ещё одно правило: система всегда выдаёт наименьший свободный номер. Закрыл номер 0 — следующий файл получит 0.
Почему лимит кончается без единого файла
Я проверил на Python. Поставил лимит 64 и открывал сетевые сокеты, ни одного файла:
ulimit -n 64
python3 -c "
import socket
s = []
while True:
s.append(socket.socket())
"
# OSError: [Errno 24] Too many open files
Упало на 61-м сокете. Три номера уже были заняты: 0, 1 и 2.
На практике один посетитель редко стоит одного номерка. Сервер держит соединение с браузером, соединение с базой и иногда открытый файл. Это уже три крючка. Поэтому лимит 1024 кончается заметно раньше тысячи посетителей.
Вторая частая ловушка — код, который легко напишет и ИИ. Прочитать 3000 файлов «параллельно»:
await Promise.all(names.map((n) => readFile(n)));
С лимитом 256 это падает с EMFILE: too many open files. Все файлы открываются разом, и крючков не хватает. ИИ тут не виноват: он просто написал самую короткую версию.
Чинится ограничением: пусть работает, скажем, 50 читателей сразу.
const out = [];
let next = 0;
async function worker() {
while (next < names.length) {
const i = next++;
out[i] = await readFile(names[i]);
}
}
await Promise.all(Array.from({ length: 50 }, worker));
Те же 3000 файлов с тем же лимитом прошли без ошибки. Цена — чуть больше кода, зато программа не падает.
Как найти утечку
Если программа падает не сразу, а через день работы — это почти всегда утечка. Что-то открывается и не закрывается.
- Узнай номер процесса:
pgrep -f nodeилиpgrep -f python. - Посчитай его дескрипторы:
ls /proc/PID/fd | wc -l. На macOS —lsof -p PID | wc -l. - Подожди минуту и посчитай ещё раз. Число растёт и не падает — утечка.
- Посмотри, что именно копится:
lsof -p PID | tail -20.
Открытых сокетов сотни? Ищи соединение, которое не закрывается. Открытых файлов сотни? Ищи open без close.
Самый надёжный приём — не закрывать руками, а отдать это языку:
with open("data.txt") as f:
text = f.read()
# здесь файл уже закрыт, даже если внутри случилась ошибка
Помогает ли просто поднять ulimit?
Только если твоей программе честно нужно много соединений. Утечку это не вылечит, а лишь отложит падение. Сначала посмотри, растёт ли число дескрипторов.
А как в Windows?
Идея та же, название другое — handle. Ошибка EMFILE в Node там тоже встречается.
Почему удалённый файл продолжает занимать место?
Потому что программа ещё держит номерок. Система сотрёт данные только после закрытия. Как это выглядит на практике — в статье почему No space left on device.
Если терминал пока кажется чёрным ящиком, начни с него. Номерки станут понятнее сами собой.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.





