Почему в Linux появляются зомби-процессы и как найти их родителя

8 февраля 2026 года разбираем жизненный цикл процессов Linux по руководству wait(2): почему завершившаяся программа остаётся в списке процессов и чем зомби отличается от сироты. Надпись <defunct> в выводе ps означает не зависшую программу, а процесс, который уже завершился, но чей статус ещё не забрал родитель.

Это продолжение рубрики «Linux под капотом». О том, откуда вообще берётся первый пользовательский процесс, рассказано в разборе загрузки Linux, а о его работе с сервисами в статье про SysV init и systemd. Здесь нас интересует завершение процессов, а не порядок запуска системы.

Почему после завершения остаётся зомби

После fork() у родителя появляется дочерний процесс. Когда потомок заканчивает работу, ядро освобождает основную часть его ресурсов, но сохраняет PID, статус завершения и сведения об использовании ресурсов. Они нужны родителю, чтобы узнать результат работы. Вызовы семейства wait(), например waitpid(), позволяют получить этот результат и освободить оставшуюся запись.

До этого момента потомок находится в состоянии Z. Сам он уже не исполняет инструкции и не создаёт новую нагрузку на CPU. Небольшая запись в ядре всё же остаётся, поэтому массовое накопление зомби способно помешать созданию новых процессов. Один кратковременно появившийся зомби ещё не говорит о поломке: родитель мог просто не успеть его обработать.

Обычно родитель получает SIGCHLD, но сам по себе сигнал не заменяет получение статуса. Не обязательно писать отдельный обработчик сигнала: программа может вызывать waitpid() из своего основного цикла. При явной установке SIGCHLD в SIG_IGN или использовании SA_NOCLDWAIT завершившиеся потомки вообще не становятся зомби. Это отдельное поведение, не равнозначное обработке сигнала по умолчанию.

Сирота не обязательно попадает к systemd

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

Упрощённое правило «всех усыновляет PID 1» имеет важное исключение. Linux передаёт потомка ближайшему живому предку, помеченному как subreaper. Если такого предка нет, эту роль выполняет init соответствующего пространства PID. На обычном хосте им часто оказывается systemd, но Linux не требует, чтобы PID 1 был именно systemd.

Поэтому поле PPID=1 не доказывает, что процесс осиротел: он мог изначально быть запущен первым процессом. А после усыновления subreaper значение PPID может быть вовсе не равно единице. Механизм описан в PR_SET_CHILD_SUBREAPER.

Как найти зомби и его родителя

В procps-ng удобно вывести PID, родительский PID, состояние и команду, затем выбрать строки, у которых состояние начинается с Z:

ps -eo pid,ppid,stat,cmd | awk '$3 ~ /^Z/ {print}'

Пустой вывод означает, что в момент снимка среди доступных процессов зомби не было. Чтобы посчитать их количество, достаточно следующей команды:

ps -eo stat= | awk '$1 ~ /^Z/ {c++} END {print c+0}'

Для конкретного потомка подставьте его PID вместо 12345. Затем так же посмотрите процесс с номером из колонки PPID. Дерево поможет понять, как он связан с остальными сервисами:

ps -o pid,ppid,stat,cmd -p 12345
ps -eo pid,ppid,stat,cmd --forest
ps -p 1 -o pid,comm,args

Не путайте Z с D. Последнее обозначает непрерываемое ожидание, обычно связанное с вводом-выводом: такой процесс ещё не завершился. А поиск слова defunct в выводе pstree зависит от способа отображения имён; состояние в ps надёжнее для этой проверки.

Безопасная лабораторная работа на Python

Пример предназначен для Linux с Python 3, в отдельном обычном терминале. Он создаёт ровно одного потомка. Родитель делает паузу на 30 секунд, позволяя увидеть состояние Z, а затем сам вызывает waitpid(). Завершать родителя ради уборки зомби здесь не требуется.

python3 - <<'PYTHON'
import os
import time

pid = os.fork()
if pid == 0:
    os._exit(0)

print(f'Родитель: {os.getpid()}, потомок: {pid}', flush=True)
try:
    time.sleep(30)
finally:
    waited_pid, status = os.waitpid(pid, 0)
    print(f'Получен статус потомка {waited_pid}: {os.waitstatus_to_exitcode(status)}')
PYTHON

Пока идёт пауза, откройте второй терминал и выполните ps -o pid,ppid,stat,cmd -p НОМЕР_ПОТОМКА, подставив напечатанный PID. После получения статуса повторите команду: запись потомка должна исчезнуть. В этом примере результат не зависит от того, умеет ли PID 1 вашего окружения убирать чужих потомков.

Что делать на сервере и в контейнере

Команда kill -9 не уберёт зомби: исполняющийся процесс уже закончился. Исправлять нужно родителя, который не получает статусы потомков. Сначала определите его назначение, посмотрите журнал и проверьте, продолжает ли расти число записей Z. Перезапускать сервис имеет смысл только с пониманием последствий для запросов и фоновых задач. Убивать неизвестный родительский процесс на рабочем сервере опасно.

В контейнере есть собственный PID 1, и им часто становится само приложение, а не полноценная система инициализации. В документации Docker для приложений, которым нужна помощь с управлением дочерними процессами, предусмотрен параметр --init. Он добавляет небольшой init-процесс. Это полезно для усыновлённых потомков, но не исправляет живого родителя внутри приложения, который сам не вызывает wait для своих детей.

Практический ориентир простой: ищите состояние Z, определяйте PPID и разбирайтесь, почему родитель не забрал результат. Зомби не нужно «добивать» сигналом, а сироту не нужно автоматически считать неисправностью. Важны устойчивое накопление завершившихся потомков и поведение программы, которая ими управляет.

Источники и ссылки


Комментарии загружаются…