Зомби и сироты: reaping
Зомби и сироты: откуда берутся и как убираются (reaping).
Что происходит при завершении процесса
Процесс не пропадает из системы в тот момент, когда его код доработал.
Сначала он переходит в завершённое состояние, но запись о нём остаётся в
таблице процессов ядра. Эта запись хранит код возврата: то число, которое
процесс отдал через exit().
Забрать этот код должен родитель. Он делает это системным вызовом wait()
(или waitpid()). Пока родитель не забрал код, запись висит в таблице.
$ echo $? # код возврата последней завершившейся команды
0
Reaping и откуда берётся зомби
Reaping — это когда родитель вызывает wait() и забирает код возврата
потомка. После reaping ядро выкидывает запись из таблицы процессов, и PID
освобождается.
Зомби — это завершённый процесс, чей код возврата ещё никто не забрал. В
ps у него состояние Z, а в колонке команды часто стоит пометка
<defunct>.
$ 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 и почему & не переживает закрытие сессии.
Дрилл · вопросы как на собесе
Проверь себя без подсказок
Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.
Что такое процесс-зомби? Как он появляется и как от него избавиться?
LIN-009Прошли материал до конца? Отметьте урок пройденным.
Прогресс сохранён в этом браузере
Почистишь историю или откроешь с телефона, и отметки пропадут. Оставь почту, они переедут в аккаунт. Пароль придумывать не нужно.
Готово. теперь в аккаунте: прогресс виден с любого устройства. Мой прогресс