В 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 родителя из вывода скрипта. Это лабораторка, на проде так делать не надо.
Источники и куда копать дальше
- wait(2)
- waitpid(2)
- fork(2)
- exit(2)
- signal(7)
- proc(5)
- tini
- LWN: re-parenting и subreaper (контекстная статья)
Что попробовать руками
- Посмотри, сколько зомби сейчас в системе, если они есть.
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 появляются зомби-процессы, кто такие сироты, какие системные вызовы за это отвечают и как диагностировать проблему на живой системе. Плюс лабораторка, которую можно повторить без танцев с форматированием.