Ошибка write: bad file descriptor 9 появляется в терминале Linux или Unix-подобной системы в тот момент, когда процесс пытается записать данные в файловый дескриптор, который уже закрыт, никогда не открывался или был открыт только для чтения. Чаще всего её можно увидеть в выводе bash-скриптов, при работе с перенаправлением ввода-вывода, в логах Docker-контейнеров и при вызовах системных утилит вроде curl, ssh или git.
Цифра 9 в сообщении — это номер конкретного дескриптора, с которым произошёл сбой. В Unix-системах каждому открытому файлу, сокету или каналу ядро присваивает целочисленный идентификатор: 0 — стандартный ввод, 1 — стандартный вывод, 2 — поток ошибок, а номера от 3 и выше выделяются файлам и соединениям, которые открывает сама программа. Дескриптор 9 нередко используется в shell-скриптах как «свободный» номер для собственных перенаправлений, поэтому именно он часто фигурирует в тексте ошибки.
Что такое файловый дескриптор и почему он становится «плохим»
Файловый дескриптор — это абстракция ядра операционной системы, через которую процесс обращается к файлам, сокетам, каналам (pipes) и устройствам. Когда программа вызывает системную функцию open() или socket(), ядро возвращает номер дескриптора, и дальнейшие операции чтения и записи выполняются уже через этот номер.
Ошибка EBADF (bad file descriptor) возникает в трёх типовых ситуациях. Первая — запись в дескриптор, который уже был закрыт вызовом close() или конструкцией exec 9>&-. Вторая — попытка писать в дескриптор, открытый в режиме «только чтение». Третья — обращение к номеру, который вообще не был открыт в текущем процессе, например из-за опечатки в скрипте.
Стоит понимать: номера дескрипторов локальны для каждого процесса. Дескриптор 9 в родительской оболочке и дескриптор 9 в дочернем процессе — это разные сущности, если дочерний процесс не унаследовал его явно. Именно потеря наследования при запуске через sudo, nohup или планировщик — частая скрытая причина сбоя.
Типичные сценарии появления ошибки
На практике проблема чаще всего проявляется в нескольких узнаваемых контекстах. Ниже — ситуации, которые стоит проверить в первую очередь.
- 🔧 В bash-скрипте используется конструкция
echo "text" >&9, но дескриптор 9 не был предварительно открыт командойexec 9>файл. - 📜 Логирование в скрипте настроено через перенаправление, а блок
execс открытием дескриптора находится в условной ветке, которая не выполнилась. - 🐳 Приложение в Docker-контейнере пытается писать в сокет или файл, который был закрыт другим потоком — типично для многопоточных программ при гонке состояний.
- 🔌 Скрипт запускается через
sudoилиsu, и новая сессия не наследует дескрипторы, открытые в исходной оболочке. - 🧹 Внешняя утилита или таймаут-обёртка закрывает лишние дескрипторы при запуске дочернего процесса.
Отдельный случай — использование bash-фичи /dev/tcp или /dev/udp для сетевых соединений прямо из скрипта. Здесь дескриптор 9 часто назначают сокету вручную, и если соединение оборвалось по таймауту или удалённая сторона закрыла его, следующая попытка записи даст именно эту ошибку.
Быстрая диагностика: как найти источник проблемы
Прежде чем что-то менять, нужно понять, какой именно процесс и какой дескриптор участвуют в сбое. Начните с просмотра открытых дескрипторов процесса через каталог /proc: для каждого запущенного процесса ядро показывает его дескрипторы в виде символических ссылок.
ls -l /proc/<PID>/fd/
Если в списке нет дескриптора 9, а программа пытается в него писать — причина найдена: дескриптор либо не открывался, либо уже закрыт. Если дескриптор есть, посмотрите, куда указывает ссылка и в каком режиме он открыт — режим виден в каталоге /proc/<PID>/fdinfo/9 в поле flags.
Для отладки скрипта полезно запустить его с трассировкой: bash -x script.sh покажет каждую выполняемую команду, и вы увидите момент, где происходит запись в проблемный дескриптор. Более глубокий уровень — утилита strace, которая перехватывает системные вызовы:
strace -f -e trace=write,close,open,dup2 ./script.sh
В выводе strace ищите строку с write(9, ...) = -1 EBADF — рядом, как правило, видно и предыдущий close(9), который и стал причиной сбоя.
Ошибка bad file descriptor 9 всегда означает одно из трёх: дескриптор не открыт, уже закрыт или открыт не в том режиме. Диагностику начинайте с /proc/
Исправление ошибки в bash-скриптах
Самая частая причина в скриптах — запись в дескриптор до его открытия. Правильный порядок выглядит так: сначала дескриптор открывается через exec, затем в него пишут, а в конце работы закрывают.
exec 9>/var/log/myscript.log
echo "старт скрипта" >&9
... основная логика ...
exec 9>&-
Если дескриптор открывается внутри условного блока или функции, убедитесь, что этот код реально выполняется до первой записи. Также проверьте, что между открытием и записью нет конструкции exec 9>&-, которая могла остаться после рефакторинга.
☑️ Проверка скрипта при ошибке fd 9
⚠️ Внимание: не пытайтесь «исправить» ошибку, просто удаляя строки с записью в дескриптор 9. Если это логирование, вы потеряете важную диагностическую информацию. Сначала восстановите корректное открытие дескриптора.
Ещё один рабочий приём — проверка существования дескриптора перед записью. Конструкция [ -e /proc/self/fd/9 ] позволяет убедиться, что дескриптор жив, и при необходимости перенаправить вывод в запасной файл вместо падения с ошибкой.
Ошибка в Docker, systemd и при запуске через планировщик
В контейнерной и сервисной среде причины те же, но проявляются иначе. Приложение, которое на локальной машине работало без сбоев, в контейнере может получить другой набор открытых дескрипторов, потому что запускающий его процесс-инициализатор не наследует лишние дескрипторы от вашей оболочки.
Для systemd-сервисов важно помнить: юнит стартует в собственном окружении, и дескрипторы, открытые в интерактивной сессии, туда не попадают. Если приложению нужен дополнительный дескриптор, его должен открыть сам процесс либо передать механизм socket activation через systemd.socket.
Почему cron часто «ломает» скрипты с дескрипторами
Задачи cron выполняются в минимальном окружении: без интерактивного терминала, с урезанным PATH и без дескрипторов, открытых в вашей сессии. Если скрипт рассчитывает на дескриптор, открытый вручную перед запуском, под cron он получит EBADF. Решение — открывать все нужные дескрипторы внутри самого скрипта через exec, не полагаясь на внешнее окружение.
При запуске через sudo ситуация похожая: по умолчанию sudo закрывает большинство унаследованных дескрипторов в целях безопасности. Если скрипт должен работать и в интерактивной сессии, и под sudo, открытие дескрипторов нужно вынести внутрь скрипта — тогда поведение станет предсказуемым в любом окружении.
Сделайте скрипт самодостаточным: открывайте все рабочие дескрипторы через exec в начале самого скрипта, а не в вызывающей оболочке. Это устранит зависимость от sudo, cron и systemd.
Ошибка в прикладных программах и многопоточном коде
Если ошибку выдаёт не ваш скрипт, а сторонняя программа, написанная на C, Python, Go или Node.js, вероятная причина — гонка состояний (race condition): один поток закрыл файл или сокет, пока другой ещё пытался в него писать. Такие сбои плавающие: они появляются не всегда и зависят от нагрузки.
Что можно сделать без доступа к исходному коду: обновить программу до актуальной версии (подобные ошибки нередко чинят в релизах), проверить системные лимиты на количество открытых файлов командой ulimit -n и посмотреть, не упирается ли процесс в ограничение. Исчерпание лимита дескрипторов иногда приводит к каскадным ошибкам, где EBADF — лишь следствие.
| Контекст ошибки | Вероятная причина | Первое действие |
|---|---|---|
| Bash-скрипт с >&9 | Дескриптор не открыт через exec | Добавить exec 9>файл до записи |
| Запуск через sudo/cron | Дескрипторы не наследуются | Открывать дескрипторы внутри скрипта |
| Docker-контейнер | Гонка потоков или закрытый сокет | Проверить логи, обновить образ |
| Сторонняя программа | Баг в коде или лимит ulimit | Проверить ulimit -n и версию ПО |
| /dev/tcp соединение | Удалённая сторона закрыла сокет | Переоткрывать соединение перед записью |
В прикладном ПО ошибка EBADF чаще всего симптом гонки состояний или исчерпания лимита открытых файлов — проверяйте ulimit и актуальность версии программы.
Профилактика: как не допустить повторения ошибки
Надёжная защита от подобных сбоев — дисциплина работы с дескрипторами. В скриптах открывайте и закрывайте дескрипторы в предсказуемых местах, избегайте «магических» перенаправлений в глубине условных веток и всегда проверяйте код возврата критичных операций.
- ✅ Добавьте
set -uи проверку кодов возврата — это помогает ловить логические ошибки раньше. - ✅ Используйте ShellCheck для статического анализа скриптов: он находит многие проблемы с перенаправлениями.
- ✅ Для логирования предпочитайте
exec >>файл 2>&1вместо ручных дескрипторов, если нет особой нужды в раздельных потоках. - ✅ В долгоживущих сервисах периодически контролируйте счётчик открытых дескрипторов через
/proc/<PID>/fd.
⚠️ Внимание: не повышайте системный лимит открытых файлов «с запасом» до огромных значений без анализа. Если программа утекает дескрипторами (открывает и не закрывает), высокий лимит лишь отсрочит проблему, а не решит её. Сначала найдите утечку через /proc и lsof.
Полезно также выстроить наблюдаемость: если ошибка появляется в продакшене редко, добавьте в скрипт или обёртку логирование состояния дескрипторов в момент сбоя. Один снимок ls -l /proc/self/fd/ в обработчике ошибки сэкономит часы отладки.
Команда lsof -p
Часто задаваемые вопросы
Почему в ошибке именно дескриптор 9, а не другой?
Номер зависит от того, какой дескриптор использовала программа. В bash-скриптах 9 традиционно выбирают как «безопасный» высокий номер для пользовательских перенаправлений, чтобы не пересекаться с дескрипторами, которые открывают сторонние команды. В других программах на месте 9 может быть любой номер — смысл ошибки от этого не меняется.
Опасна ли эта ошибка для системы?
Сама по себе ошибка EBADF не повреждает данные и систему — это просто отказ ядра выполнить запись. Однако она сигнализирует, что программа потеряла часть вывода: например, кусок лога или данные, которые должны были уйти в файл или сокет. Последствия зависят от того, что именно не записалось.
Можно ли исправить ошибку без правки скрипта?
Иногда да. Если скрипт рассчитывает на внешне открытый дескриптор, можно открыть его перед запуском: exec 9>>/tmp/out.log; ./script.sh. Но это обходной путь — надёжнее исправить сам скрипт, чтобы он не зависел от окружения.
Чем отличается bad file descriptor от broken pipe?
Это разные ошибки. EBADF означает, что дескриптор недействителен в самом процессе — он не открыт или закрыт. EPIPE (broken pipe) возникает, когда дескриптор валиден, но принимающая сторона канала или сокета уже закрыла соединение. Диагностика и исправление у них разные.
Как понять, кто закрыл дескриптор в чужой программе?
Запустите программу под strace -f с фильтром по вызовам close и write, затем найдите в трассировке close для нужного дескриптора перед ошибочным write. Флаг -f важен, потому что закрытие часто происходит в дочернем потоке или процессе.