Оглавление

В Linux процесс не может просто исчезнуть бесследно. Когда код завершился, ядро ещё некоторое время хранит запись о процессе, чтобы родитель смог забрать код выхода. Пока родитель это не сделал, процесс висит в состоянии Zombie (defunct).

Зомби не “жрёт” CPU и почти не занимает память, но занимает слот в таблице процессов. Если какой-то родительский процесс долго живёт и массово “забывает” вычищать потомков, зомби накапливаются. В крайних случаях система начинает отказываться создавать новые процессы, и проблема уже выглядит как “всё сломалось”, хотя по факту сломалась логика родителя.

Сироты (orphans) это наоборот живые процессы, которые продолжают работать после смерти родителя. Их усыновляет PID 1. В современных дистрибутивах это обычно systemd. Когда сирота завершится, его статус заберёт PID 1 и запись о процессе исчезнет корректно.

На практике зомби чаще всего всплывают в двух местах: кривые скрипты, которые делают fork пачками и не делают wait, и контейнеры, где PID 1 внутри контейнера не умеет корректно “подметать” завершившиеся процессы.

Как это устроено

Механика крутится вокруг fork, exit и wait. После fork появляется потомок. Когда потомок завершает выполнение, ядро освобождает его ресурсы, закрывает файловые дескрипторы, но оставляет запись о процессе, чтобы родитель мог узнать код завершения.

В момент завершения потомка ядро посылает родителю SIGCHLD. Правильное поведение родителя: обработать этот сигнал и вызвать wait или waitpid. Как только родитель забрал статус, ядро удаляет запись о процессе окончательно.

Почему kill -9 не помогает зомби. У зомби уже нет исполняемого контекста, ему нечего “убивать”. Сигналы не работают, потому что процесс уже мёртв, осталась только запись о нём. Лечится не убийством зомби, а тем, что родитель должен сделать wait. Или, если родитель сломан и ждать не будет, родителя приходится завершать. Тогда зомби станет сиротой, и PID 1 аккуратно заберёт его статус.

Отдельный нюанс про контейнеры. Внутри контейнера тоже есть PID 1. Если туда запихнуть приложение, которое не умеет быть init и не делает wait за своими детьми, зомби начнут копиться внутри контейнера. Поэтому в Docker часто используют tini или режим —init, чтобы внутри был простой “подметальщик” процессов.

Как посмотреть это на живой системе

  • Выведи зомби через ps по статусу Z. Это самый прямой способ.

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

  • Если хочешь “по-человечески”, посмотри дерево процессов и отметки defunct.

pstree -ap | grep -i defunct

  • Посмотри лимит PID. Он бывает разным, поэтому лучше не угадывать, а смотреть.

cat /proc/sys/kernel/pid_max

  • Для конкретного PID проверь его статус и родителя. Зомби обычно имеет STAT с Z.

ps -o pid,ppid,stat,cmd -p 12345

В последней команде замени 12345 на PID интересующего процесса. Родитель это PPID. Если зомби не исчезает, значит родитель жив и не делает wait.

Типовые проблемы и симптомы

Главный симптом это медленное накопление процессов в статусе Z при стабильной нагрузке. Обычно это не “особенность Linux”, а конкретный баг в родителе или в том, как сервис запускает дочерние процессы.

  • В top или htop растёт счётчик zombie.
  • Периодически начинают всплывать ошибки про невозможность создать процесс, хотя памяти и CPU достаточно.
  • Сервисы странно ведут себя, потому что не могут запустить внешние утилиты или подпроцессы.

В контейнерах частая причина это PID 1 внутри контейнера, который не делает reaping. Если внутри контейнера запускается shell-скрипт, который делает fork и не вызывает wait, зомби гарантированы.

Мини-лабораторка

Цель лабораторки: создать зомби намеренно и увидеть его в ps. Мы сделаем это на Python: потомок завершится сразу, родитель подождёт 60 секунд и не будет вызывать wait. Потомок в это время будет зомби.

  • Создай файл zombie.py одной командой. Она длинная, зато копируется без сюрпризов: строки в файле будут правильные.

printf '%s\n' 'import os' 'import time' '' 'pid = os.fork()' '' 'if pid > 0:' ' print(f"Родитель PID {os.getpid()}, потомок PID {pid}")' ' print("Родитель спит 60 секунд. В другом терминале найди потомка в статусе Z.")' ' time.sleep(60)' 'else:' ' print("Потомок завершается и становится зомби...")' ' os._exit(0)' > zombie.py

  • Запусти скрипт. Он выведет PID родителя и потомка.

python3 zombie.py

  • Во втором терминале найди зомби по статусу Z и убедись, что он существует, пока родитель спит.

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

  • Если хочешь проверить точечно, подставь PID потомка из вывода Python и посмотри его STAT и PPID.

ps -o pid,ppid,stat,cmd -p 12345

  • Подожди, пока родитель завершится сам через 60 секунд. После этого зомби должен исчезнуть, потому что PID 1 заберёт его статус.

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

Если тебе нужно “почувствовать” механику жёстче, можно завершить родителя раньше командой kill по PID родителя из вывода скрипта. Это лабораторка, на проде так делать не надо.

Разбираем жизненный цикл процессов в Linux: почему возникают зомби (defunct), кто такие сироты и как PID 1 (systemd) управляет их усыновлением

Источники и куда копать дальше

Что попробовать руками

  • Посмотри, сколько зомби сейчас в системе, если они есть.

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

  • Посмотри дерево процессов и убедись, кто у тебя PID 1.

ps -p 1 -o pid,comm,args

  • Найди сирот, у которых родитель PID 1. Это нормально, но полезно видеть картину.

ps -eo ppid,pid,stat,cmd | awk '$1 == 1 {print}'

Метаописание: почему в Linux появляются зомби-процессы, кто такие сироты, какие системные вызовы за это отвечают и как диагностировать проблему на живой системе. Плюс лабораторка, которую можно повторить без танцев с форматированием.