← Linux: процессы вглубь
12 мин · средне · Урок 4 из 7

Зомби и сироты: reaping

Зомби и сироты: откуда берутся и как убираются (reaping).

Что происходит при завершении процесса

Процесс не пропадает из системы в тот момент, когда его код доработал. Сначала он переходит в завершённое состояние, но запись о нём остаётся в таблице процессов ядра. Эта запись хранит код возврата: то число, которое процесс отдал через exit().

Забрать этот код должен родитель. Он делает это системным вызовом wait() (или waitpid()). Пока родитель не забрал код, запись висит в таблице.

$ echo $?      # код возврата последней завершившейся команды
0

Reaping и откуда берётся зомби

Reaping — это когда родитель вызывает wait() и забирает код возврата потомка. После reaping ядро выкидывает запись из таблицы процессов, и PID освобождается.

Зомби — это завершённый процесс, чей код возврата ещё никто не забрал. В ps у него состояние Z, а в колонке команды часто стоит пометка <defunct>.

потомок вызвал exit(0) запись в таблице держит код возврата родитель вызвал wait запись убрана PID свободен wait не вызвали зомби, состояние Z PID занят PID 1 уберёт
Разница ровно одна: вызвал ли родитель wait. Сироту подхватывает PID 1, поэтому она зомби не становится.
$ ps -eo pid,ppid,stat,comm
  PID  PPID STAT COMMAND
 4821  4820 S    parent
 4822  4821 Z    child <defunct>

Здесь процесс 4822 уже мёртв, но его родитель 4821 пока не сделал wait(). Запись держится, PID 4822 занят.

Зомби держит запись в таблице процессов и номер PID. Память, файлы и стек ядро освободило ещё при завершении. Остаётся только строка в таблице.

Ядро уведомляет родителя о смерти потомка сигналом SIGCHLD. По умолчанию этот сигнал игнорируется, поэтому родитель обязан сам либо ловить SIGCHLD и вызывать wait(), либо периодически опрашивать потомков.

Чем опасны зомби

Один зомби безвреден. Проблема начинается, когда родитель написан с ошибкой и никогда не вызывает wait(). Тогда каждый завершившийся потомок оставляет по записи, и они копятся.

Номера PID — конечный ресурс. Их лимит виден так:

$ cat /proc/sys/kernel/pid_max
4194304

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

Осиротевшие процессы

Осиротевший процесс (orphan) — это живой процесс, чей родитель завершился раньше него. Такой процесс не остаётся без присмотра: ядро переназначает ему нового родителя.

$ ps -eo pid,ppid,comm
  PID  PPID COMMAND
 5310     1 sleep       # был потомком shell, shell закрылся, PPID стал 1

Здесь sleep пережил своего исходного родителя, и его PPID стал 1. Процесс с PID 1 (это init, обычно systemd) усыновляет всех сирот. Когда такой усыновлённый потомок завершится, init получит SIGCHLD, вызовет wait() и уберёт запись. Поэтому сирота под init не превращается в вечный зомби.

PID 1 в контейнере

В контейнере первым процессом (PID 1) запускается не init, а ваше приложение. Роль «убиральщика сирот» переходит к нему. Если приложение не делает wait() для своих потомков, усыновлённые сироты после смерти становятся зомби и копятся внутри контейнера.

# внутри контейнера, приложение как PID 1
$ ps -eo pid,ppid,stat,comm
  PID  PPID STAT COMMAND
    1     0 S    myapp
   42     1 Z    sh <defunct>     # сирота усыновлён myapp, но не reaped

Есть два выхода. Первый: приложение само корректно ловит SIGCHLD и вызывает wait(). Второй: поставить PID 1 маленький init-процесс, который занимается только reaping (tini, или флаг docker run --init).

Ядро с версии 3.4 умеет назначать subreaper. Subreaper — это процесс, который объявляет себя приёмником сирот в своём поддереве вместо PID 1. Тогда сироту усыновляет он, а не глобальный init.

# процесс объявляет себя subreaper для своих потомков
prctl(PR_SET_CHILD_SUBREAPER, 1);

Так работают многие init-обёртки и менеджеры процессов внутри контейнеров.

Частые заблуждения

Миф «зомби ест память» неверен. Память процесса освобождена при завершении, занят только PID и запись в таблице.

Миф «kill -9 уберёт зомби» тоже неверен. Зомби уже мёртв, сигнал ему слать некому:

$ kill -9 4822       # процесс-зомби
$ ps -o pid,stat -p 4822
  PID STAT
 4822 Z              # всё ещё на месте

Убирает зомби только wait() родителя. Если родитель завис и не делает wait(), лечит перезапуск самого родителя: его потомков усыновит init и приберёт за ним.

Полезно помнить пару фактов про сигналы. SIGKILL (9) и SIGSTOP — единственные два сигнала, которые нельзя перехватить, заблокировать или проигнорировать. Действий по умолчанию всего пять: Term (завершить), Ign (игнорировать), Core (завершить с дампом), Stop (приостановить), Cont (продолжить).

Что важно запомнить

Reaping — вызов wait() родителем, который забирает код возврата и освобождает запись в таблице.

Зомби (Z) — завершённый процесс, код которого ещё не забрали. Держит PID и запись в таблице, память не держит.

Уведомление о смерти потомка — SIGCHLD, по умолчанию игнорируется.

Сироту усыновляет init (PID 1) и делает wait(). В контейнере роль PID 1 берёт приложение, поэтому нужен reaping или --init/subreaper (ядро 3.4+).

kill -9 зомби не убирает: он уже мёртв, помогает только wait() или перезапуск родителя.

В следующем уроке — демоны и фоновые процессы: setsid, nohup и почему & не переживает закрытие сессии.

Дрилл · вопросы как на собесе

Проверь себя без подсказок

Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.

01

Что такое процесс-зомби? Как он появляется и как от него избавиться?

LIN-009

Прошли материал до конца? Отметьте урок пройденным.