Deep Engineering
Средний·Опубликовано·25 МИН

Сигналы, зомби и PID 1: почему контейнер не останавливается по SIGTERM

Разговор идёт лестницей: что такое зомби — кому достаётся сирота — почему контейнер не останавливается сразу — что означает код 137. Первые две ступени держатся на том, что смерть процесса состоит из двух событий; последние две — на правиле ядра, которое почти никогда не называют вслух: у процесса с номером 1 в своём пространстве имён умолчательные действия сигналов не выполняются.

Полное техническое изложение

TL;DR

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

Отсюда главное следствие: образ, у которого первый процесс не обрабатывает SIGTERM, не останавливается по docker stop. Он выжидает весь льготный срок и уходит по SIGKILL. Измерено на льготном сроке в две секунды: 11 мс с обработчиком против 2009 мс без него. Вторая поломка тише: незабранные потомки сами не рассасываются — тысяча завершившихся потомков без wait дала тысячу зомби, то есть 100 % созданных, и ноль после wait.

Дальше — то, что отличает знающего от читавшего. Сирота достаётся не «системе», а первому процессу или ближайшему subreaper: измерено, что родитель внука меняется с настоящего номера на 1 в тот момент, когда его отец умирает. У процесса с номером 1 в своём пространстве имён умолчательные действия сигналов не выполняются, и единственное исключение — SIGKILL и SIGSTOP: из родительского пространства имён они доставляются принудительно и не ловятся ничем, поэтому оркестратор всегда добивается своего, вопрос только в задержке. 137 — это 128 + 9, а 143 — 128 + 15, и складывает эти числа оболочка, а не ядро: 137 значит «убит SIGKILL» и ничего не говорит о том, кто послал. И зомби «висит в памяти» только на словах: адресного пространства процесса уже нет, но ядро сохраняет небольшую запись — PID, статус завершения, данные о потреблённых ресурсах, — и она занимает слот в таблице процессов.

Порог входа
Перед уроком достаточно понимать
  • программу запускают, и она когда-нибудь заканчивается — сама или потому, что её остановили;
  • одна запущенная программа может запустить другую;
  • контейнер кто-то запускает и когда-то просит остановиться — например, при выкатке новой версии.
Заранее знать не нужно
  • что такое зомби, сирота, усыновление и subreaper;
  • что такое пространство имён процессов и чем в нём особенный номер 1;
  • SIGTERM, SIGKILL, диспозиция сигнала, коды выхода 137 и 143.

Что здесь на самом деле спрашивают

Лестница почти всегда одна и та же, и держится она на одном механизме, хотя вопросы выглядят несвязанными:

  1. «Что такое зомби-процесс?» — разминка, на которой отсеиваются те, кто думает, что это зависший процесс.
  2. «Чем он опасен, если самого процесса уже нет?» — здесь начинается содержание.
  3. «Кто становится родителем осиротевшего процесса?» — проверяют, знаете ли вы про усыновление и про subreaper.
  4. «Почему контейнер останавливается не сразу?» — вопрос, на который напрашивается ответ «медленно закрывает соединения»: правдоподобно и мимо механизма.
  5. «Что означает код выхода 137?» — знаете ли вы арифметику и понимаете ли, чего этот код НЕ говорит.
  6. «Зачем в контейнер кладут tini — вопрос, который проверяет, связали ли вы первые пять в одно.

Урок идёт по этой лестнице и идёт в одну сторону: сначала правило, потом его следствия. Все числа получены на одной машине прогоном bench/signals/lifecycle.py и bench/signals/practice.py; номера процессов и миллисекунды у вас будут другие, а состояния, коды выхода и исход SIGTERM для PID 1 совпадут.

База: процесс, его номер и сообщение, которое ему шлют

Прежде чем говорить о зомби и о номере 1, стоит назвать обычными словами четыре вещи, на которых держится весь урок.

Процесс — это запущенная программа. Не файл на диске, а его запуск: одна и та же программа, запущенная дважды, — это два процесса, и живут они независимо друг от друга.

У процесса есть номер. Ядро выдаёт его при запуске, и по этому номеру процесс потом находят: чтобы посмотреть на него, остановить или дождаться его конца.

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

У процессов есть родитель и потомок — тот, кто запустил, и тот, кого запустили. Связь эта не формальная: именно родителю потом достаётся ответ на вопрос, чем кончился потомок.

Из последних двух пунктов прямо следует первая ступень лестницы. Потомок завершился; родитель ещё не спросил, чем всё кончилось; ядру нужно где-то держать ответ на этот будущий вопрос — и оно его держит. Состояние «процесса уже нет, а запись о том, чем он кончился, ещё есть» и называют зомби.

И ещё одно, без чего вторая половина урока читается как магия: у контейнера своё пространство номеров. Процессы внутри нумеруются с единицы, как в только что загрузившейся системе, а снаружи у того же самого процесса номер совсем другой. Один процесс — два номера: внутри 1, снаружи обычный. Тот, кто останавливает контейнер, живёт снаружи и шлёт сигнал по внешнему номеру — а правила к процессу применяются те, которые действуют для номера 1.

Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Дальше — про то, почему один и тот же сигнал по-разному действует на обычный процесс и на процесс с номером 1, кому достаётся потомок, если родитель умер раньше, и откуда в отчёте берётся код 137.

Механизм 1: смерть процесса — это два события, а не одно

Теперь то же самое словами документации — и начнём с того, что не зависит ни от дистрибутива, ни от версии ядра.

контракт языкаПоведение, описанное в wait(2) и pid_namespaces(7). Числа в конце урока сняты на Linux 6.18, но правила ниже старше любой из живых версий ядра.

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

Эта запись и называется зомби. wait(2) говорит о ней прямо:

A child that terminates, but has not been waited for becomes a "zombie". The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child.

перевод

Потомок, который завершился, но не был дождан, становится «зомби». Ядро хранит о нём минимальный набор сведений — PID, статус завершения, данные о потреблённых ресурсах, — чтобы родитель мог позже вызвать wait и получить сведения о потомке.

wait(2)

Отсюда сразу ответ на второй вопрос лестницы. Зомби опасен не тем, что «висит в памяти»: адресного пространства процесса уже нет, а то, что осталось, — минимальный набор сведений, перечисленный в цитате выше. Опасен он тем, что эта запись занимает слот в таблице процессов, а таблица конечна:

As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes.

перевод

Пока зомби не убран из системы через wait, он занимает слот в таблице процессов ядра, и если эта таблица заполнится, создать новые процессы будет невозможно.

wait(2)

Так выглядит утечка, у которой в графике памяти ничего не видно. Приложение работает, память ровная, а fork в какой-то момент перестаёт удаваться — и падает не тот, кто виноват.

Вот это в прогоне. Потомок вызвал _exit(7) и был оставлен без wait:

наблюдение замераbench/signals/lifecycle.py, Linux 6.18.44. Буква состояния, код выхода и номер родителя сироты воспроизводятся у любого читателя на Linux; номера процессов — нет.
1. ZOMBIE: the child is gone, the record is not
-----------------------------------------------
  child finished, status not collected       Z (zombie)
  why the kernel keeps the record            so the parent can read the exit code
  state after wait                           no such process
  exit code the record was held for          7

Четыре строки читаются как история целиком: состояние Z до wait, ничего после wait, и семёрка — то самое, ради чего запись держалась. Убирает зомби не время и не сборщик, а вопрос «чем всё кончилось», заданный родителем.

Механизм 2: у сироты новый родитель, и это не абстракция

Обратный случай: родитель умер раньше потомка. Потомок при этом не умирает и не остаётся без родителя — его усыновляют.

If a parent process terminates, then its "zombie" children (if any) are adopted by init(1), (or by the nearest "subreaper" process as defined through the use of the prctl(2) PR_SET_CHILD_SUBREAPER operation).

перевод

Если родительский процесс завершается, его дети-«зомби» (если они есть) усыновляются init(1) — или ближайшим процессом-«жнецом» (subreaper), назначенным операцией PR_SET_CHILD_SUBREAPER вызова prctl(2).

wait(2)

Прогон показывает сам момент замены. Внук печатает своего родителя дважды — сразу после запуска и после того, как его отец завершился:

2. ORPHAN: the parent died first
--------------------------------
  grandchild: parent right after start    5359
  grandchild: parent after father exits   1

Число 5359 у вас будет другим, единица — та же. И вот что из этого следует для работы: забирать статус сироты будет новый родитель, а не потерянный. Если новый родитель это умеет — зомби не копятся. Если не умеет — копятся все сироты сразу, и это ровно то, что происходит в контейнере, где первым процессом оказалось обычное приложение.

Слово subreaper здесь не для красоты: так называют процесс, который сказал ядру «сироты моего поддерева достаются мне, а не первому процессу». Говорится это вызовом PR_SET_CHILD_SUBREAPER; так устроены менеджеры сессий и системы супервизии. В контейнере такого процесса обычно нет, и сироты идут прямо к PID 1.

Механизм 3: правило PID 1, из которого растёт всё остальное

Теперь главное. У процесса, который в своём пространстве имён получил номер 1, сигналы работают иначе — и это записано в pid_namespaces(7):

Only signals for which the "init" process has established a signal handler can be sent to the "init" process by other members of the PID namespace. This restriction applies even to privileged processes, and prevents other members of the PID namespace from accidentally killing the "init" process.

перевод

Первому процессу пространства имён другие его члены могут послать только те сигналы, для которых он установил обработчик. Ограничение действует даже для привилегированных процессов и не даёт остальным членам пространства имён случайно убить первый процесс.

pid_namespaces(7)

Снаружи — то же самое, и это как раз случай оркестратора: он живёт в родительском пространстве имён и шлёт сигнал вашему первому процессу.

a process in an ancestor namespace can—subject to the usual permission checks described in kill(2)—send signals to the "init" process of a child PID namespace only if the "init" process has established a handler for that signal.

перевод

Процесс в родительском пространстве имён может — с обычными проверками прав из kill(2) — послать сигнал первому процессу дочернего пространства имён только если тот установил обработчик этого сигнала.

pid_namespaces(7)

Сформулируем это словами, которыми стоит отвечать: для PID 1 умолчательные действия сигналов не выполняются. Диспозиция сигнала — это то, что процесс с ним делает: умолчательное действие из таблицы signal(7)SIGTERM там стоит Term — завершиться), игнорирование или обработчик. У обычного процесса без обработчика работает умолчательное действие. У процесса с номером 1 умолчательного действия нет вовсе: обработчик поставлен — сигнал работает, не поставлен — сигнал не делает ничего.

Один и тот же SIGTERM, два процесса, отличающиеся одной строкой:

3. PID 1: one SIGTERM, two outcomes
-----------------------------------
  PID inside the namespace (no handler)      1
  SIGTERM from outside, no handler (PID 5362) ALIVE: the signal did nothing
  PID inside the namespace (with handler)    1
  SIGTERM from outside, with handler (PID 5364) exited
наблюдение замераbench/signals/lifecycle.py, Linux 6.18.44, unshare --fork --pid --mount-proc. Номера процессов у вас будут другие; исход в обеих строках — тот же.

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

Есть исключение, и оно ровно одно:

SIGKILL or SIGSTOP are treated exceptionally: these signals are forcibly delivered when sent from an ancestor PID namespace.

перевод

SIGKILL и SIGSTOP обрабатываются особо: они доставляются принудительно, если посланы из родительского пространства имён.

pid_namespaces(7)

Поэтому «контейнер невозможно остановить» — неверная формулировка. Остановить можно всегда; вопрос в том, ждёт ли оркестратор перед этим весь льготный срок впустую.

Механизм 4: сколько стоит отсутствие обработчика и откуда в отчёте 143 и 137

Остановка контейнера — это три шага: SIGTERM, льготный срок, SIGKILL. Длину срока задаёт тот, кто останавливает, и она называется в его настройках; для урока важно не конкретное число, а то, что срок конечен и после него приходит сигнал, который не ловится. Разница между «остановился за миллисекунды» и «остановился за весь срок» — это ровно наличие обработчика.

Скрипт воспроизводит эту схему со сроком в две секунды:

stop time, SIGTERM handled           11 ms
stop time, no handler                2009 ms
наблюдение замераbench/signals/practice.py, льготный срок в этом прогоне — 2 с. Миллисекунды у вас будут другие: первое число зависит от планировщика, второе — от выбранного срока. Совпадёт то, ради чего замер и сделан: с обработчиком остановка занимает миллисекунды, без него — весь срок целиком.

Второе число — не «медленное завершение». Это весь срок ожидания плюс время, за которое доходит SIGKILL: процесс всё это время жив и здоров, SIGTERM для него не произошёл, а потом приходит SIGKILL, который не ловится.

Дальше арифметика кодов выхода. Ядро отдаёт две разные вещи: код, с которым процесс вышел сам, и номер сигнала, которым его убили. Складывает их тот, кто печатает результат:

When a command terminates on a fatal signal N, bash uses the value of 128+N as the exit status.

перевод

Когда команда завершается по фатальному сигналу N, bash использует в качестве кода выхода значение 128+N.

Bash Reference Manual

Отсюда и таблица, которую спрашивают:

4. EXIT CODES: 128 + signal number
----------------------------------
  exited on its own                          3
  SIGTERM, default disposition               -15 (the shell reports 143)
  SIGKILL                                    -9 (the shell reports 137)

143 — это 128 + 15, SIGTERM. 137 — это 128 + 9, SIGKILL. Минус пятнадцать — то, как отдаёт номер сигнала subprocess: отрицательное число, чтобы его нельзя было спутать с кодом выхода.

И самое полезное здесь — чего код 137 не говорит. Он говорит: процесс убит SIGKILL. Он не говорит, кто послал сигнал. OOM-killer, оркестратор после льготного срока, человек с kill -9 — в этой графе неразличимы, и в логе приложения не будет ничего, потому что SIGKILL не даёт выполнить ни одной строки. Различать их придётся по другим уликам: сообщению ядра, событию оркестратора, времени.

Глубже: зачем в контейнер кладут init

Теперь все части складываются. Ваше приложение стало PID 1, а значит:

  • умолчательные действия сигналов для него не выполняются, и SIGTERM без обработчика не сделает ничего;
  • все сироты в контейнере усыновляются им, и забирать их статусы тоже придётся ему.

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

Отсюда роль, для которой написаны маленькие init-процессы (tini, dumb-init, флаг docker run --init): занять PID 1 и закрыть обе обязанности — пересылать сигналы дочернему процессу и забирать статусы сирот. Правило про умолчательные действия становится заботой такого процесса, а не вашей; что именно умеет конкретный из них, написано в его документации.

Замена ему — не библиотека, а две строки в приложении: поставить обработчик SIGTERM и вызывать wait для потомков. Если приложение не порождает процессов, достаточно первого.

Как отвечать на собеседовании

Короткий ответ: у процесса с номером 1 в своём пространстве имён умолчательные действия сигналов не выполняются, поэтому SIGTERM без обработчика для него не делает ничего — оркестратор ждёт весь льготный срок и добивает SIGKILL, а в отчёте остаётся код 137, то есть 128 + 9. Зомби при этом — не процесс, а запись о его смерти: она держится ради кода выхода и занимает слот в таблице процессов, а убирает её wait, а не время.

Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.

Если интервьюер копает глубже

Хороший ответ отличается двумя вещами. Первая — вы называете правило правилом ядра, а не особенностью Docker, и знаете исключение: SIGKILL и SIGSTOP из родительского пространства имён доставляются принудительно, поэтому остановить можно всегда, вопрос только в задержке. Вторая — вы говорите, чего код 137 не сообщает: кто послал сигнал. Ответ «137 значит OOM» превращает улику в диагноз — а это разные вещи.

Дальше спросят

Спросят дальше

Почему в логе приложения ничего нет, если контейнер «упал»?

Короткий ответ

Потому что SIGKILL не даёт выполнить ни одной строки: обработчика у него не бывает, отложенная запись в лог не случается, буферы не сбрасываются. Тишина в логе — это не потеря сообщений, а признак того, каким сигналом всё кончилось.

Если бы пришёл SIGTERM и обработчик был, в логе была бы запись о начале остановки. Отсюда практическое следствие: наличие такой записи — способ отличить упорядоченную остановку от добивания, и стоит её ставить.

Спросят дальше

Пересылает ли оболочка сигнал, если в ENTRYPOINT написан sh -c "app"?

Короткий ответ

Обычно нет: PID 1 в этом случае — оболочка, а не приложение, и она не обязана пересылать сигналы дочернему процессу. SIGTERM приходит оболочке, у которой для него нет обработчика, а приложение о нём вообще не узнаёт.

Поэтому форму с оболочкой заменяют на форму-массив (ENTRYPOINT ["app"]), где PID 1 становится само приложение, — или ставят перед ним init, который пересылать сигналы умеет.

Спросят дальше

Может ли зомби съесть память, если их накопятся тысячи?

Короткий ответ

Памяти в том смысле, в каком её считают у живого процесса, за зомби нет: адресное пространство освобождено при завершении, а ядро держит лишь небольшую запись — PID, статус завершения, данные о потреблённых ресурсах. Кончится раньше не она, а таблица процессов: wait(2) прямо говорит, что зомби занимает слот, и что it will not be possible to create further processes (создать новые процессы будет невозможно), когда таблица заполнится.

Выглядит это как отказ там, где никто не ждёт: fork перестаёт удаваться у случайного процесса, а не у виноватого. График памяти при этом ровный — и именно поэтому такую утечку долго не находят. Измерено: тысяча завершившихся потомков без wait даёт тысячу зомби, то есть 100 % созданных, и ноль после wait.

Спросят дальше

Что произойдёт, если PID 1 всё-таки завершится?

Короткий ответ

Пространство имён закончится вместе с ним, и это записано там же:

If the "init" process of a PID namespace terminates, the kernel terminates all of the processes in the namespace via a SIGKILL signal.

перевод

Если первый процесс пространства имён PID завершается, ядро завершает все процессы этого пространства сигналом SIGKILL.

pid_namespaces(7)

Отсюда правило, которое иногда формулируют как «в контейнере не должно быть работы после смерти главного процесса» — её и не будет, независимо от того, готова она к завершению или нет.

Практическое следствие: плавное закрытие соединений и дозапись данных обязаны происходить до того, как PID 1 выйдет, а не в каком-нибудь фоновом потоке, который «успеет потом».

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

Утверждение

зомби-процесс висит в памяти и его надо убить

На самом деле

Убивать нечего: процесса уже нет, есть запись о его смерти — PID, статус завершения, потреблённые ресурсы. Адресного пространства за ней нет, а вот слот в таблице процессов занят, и wait(2) прямо говорит, что при заполнении таблицы it will not be possible to create further processes (создать новые процессы будет невозможно). Убирает запись wait родителя, и kill -9 зомби ничего не меняет — сигналы мёртвому процессу не доставляются.

Утверждение

контейнер не останавливается по SIGTERM — это особенность Docker

На самом деле

Это правило ядра, записанное в pid_namespaces(7): первому процессу пространства имён доставляются только те сигналы, для которых он установил обработчик. Docker здесь ни при чём — он лишь запускает ваш процесс первым. Измерено: один и тот же SIGTERM, посланный снаружи, оставляет процесс без обработчика живым и завершает процесс с обработчиком.

Утверждение

код выхода 137 означает нехватку памяти

На самом деле

137 — это 128 + 9, то есть «убит SIGKILL», и кто послал сигнал, код не сообщает. OOM-killer, оркестратор после льготного срока и человек с kill -9 дают одинаковые 137. Диагноз ставится по другим уликам: сообщению ядра, событию оркестратора, времени относительно начала выкатки.

Утверждение

приложению незачем обрабатывать SIGTERM — оркестратор всё равно его остановит

На самом деле

Остановит, но через весь льготный срок и через SIGKILL, у которого нет обработчика: незакрытые соединения оборвутся, недописанное не допишется, в логе не останется ничего. Измерено при льготном сроке в две секунды: 11 мс с обработчиком против 2009 мс без него — и вторая цифра целиком состоит из ожидания.

Утверждение

сироту усыновляет операционная система, и она же убирает зомби

На самом деле

Усыновляет конкретный процесс: init или ближайший subreaper (PR_SET_CHILD_SUBREAPER). Разница практическая: первый процесс обычной системы статусы забирает, а ваше приложение, ставшее PID 1 в контейнере, — нет. Поэтому в контейнере сироты и превращаются в зомби, а в системе — нет.

Утверждение

tini и dumb-init нужны для правильного запуска приложения

На самом деле

Они не про запуск, а про две обязанности PID 1, которых у приложения нет: пересылать сигналы дочернему процессу и забирать статусы сирот. Если приложение делает и то и другое само, init не нужен; если оно не порождает процессов, достаточно обработчика SIGTERM.

Утверждение

SIGKILL можно перехватить, если очень нужно

На самом деле

Нельзя: signal(7) говорит прямо: SIGKILL and SIGSTOP cannot be caught, blocked, or ignored (SIGKILL и SIGSTOP нельзя перехватить, заблокировать или проигнорировать). Именно поэтому оркестратор им и добивает: это единственный сигнал, исход которого не зависит от кода приложения. И по той же причине после него в логе не бывает записей.

Практика

Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.

Практика · что напечатает

Пять наблюдений подряд: состояние незабранного потомка, его код выхода, номер родителя сироты, состояние PID 1 без обработчика через секунду после SIGTERM и код выхода процесса, убитого SIGKILL. Состояния — однобуквенные, из /proc: S — спит, Z — зомби. Что напечатает этот код?
pid = os.fork()
if pid == 0:
  os._exit(7)
time.sleep(0.2)
print(state_of(pid))
print(os.waitstatus_to_exitcode(os.waitpid(pid, 0)[1]))
print(orphan_ppid())
print(pid1_state_after_sigterm(SLEEPER))
killed = subprocess.Popen([sys.executable, "-c", "import time; time.sleep(5)"])
time.sleep(0.2)
killed.send_signal(signal.SIGKILL)
print(128 - killed.wait())

Практика · оцените

Льготный срок — две секунды. Сколько секунд займёт остановка процесса-PID-1, который SIGTERM не обрабатывает?
с

Проверка знаний

Вопрос 1 из 6

Процесс-потомок завершился, родитель не вызвал wait. Сколько памяти занимает потомок?

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

4 ИСТОЧНИКА

  1. wait(2), Linux man-pages 6.7Официальная документация. Что такое зомби и почему ядро держит запись: «A child that terminates, but has not been waited for becomes a "zombie". The kernel maintains a minimal set of information about the zombie process (PID, termination status, resource usage information) in order to allow the parent to later perform a wait to obtain information about the child» (Потомок, который завершился, но не был дождан, становится „зомби“. Ядро хранит о нём минимальный набор сведений — PID, статус завершения, данные о потреблённых ресурсах, — чтобы родитель мог позже вызвать wait и получить сведения о потомке). И чем это кончается: «As long as a zombie is not removed from the system via a wait, it will consume a slot in the kernel process table, and if this table fills, it will not be possible to create further processes» (Пока зомби не убран из системы через wait, он занимает слот в таблице процессов ядра, и если эта таблица заполнится, создать новые процессы будет невозможно). Оттуда же правило про сироту: «If a parent process terminates, then its "zombie" children (if any) are adopted by init(1), (or by the nearest "subreaper" process as defined through the use of the prctl(2) PR_SET_CHILD_SUBREAPER operation)» (Если родительский процесс завершается, его дети-„зомби“ (если они есть) усыновляются init(1) — или ближайшим процессом-„жнецом“ (subreaper), назначенным операцией PR_SET_CHILD_SUBREAPER вызова prctl(2)).https://man7.org/linux/man-pages/man2/wait.2.html
  2. pid_namespaces(7), Linux man-pages 6.7Официальная документация. Правило, из которого растёт весь урок: «Only signals for which the "init" process has established a signal handler can be sent to the "init" process by other members of the PID namespace. This restriction applies even to privileged processes, and prevents other members of the PID namespace from accidentally killing the "init" process» (Первому процессу пространства имён другие его члены могут послать только те сигналы, для которых он установил обработчик. Ограничение действует даже для привилегированных процессов и не даёт остальным членам пространства имён случайно убить первый процесс). И то же для сигнала снаружи: «a process in an ancestor namespace can—subject to the usual permission checks described in kill(2)—send signals to the "init" process of a child PID namespace only if the "init" process has established a handler for that signal» (процесс в родительском пространстве имён может — с обычными проверками прав из kill(2) — послать сигнал первому процессу дочернего пространства имён только если тот установил обработчик этого сигнала). Исключение, которым пользуется оркестратор: «SIGKILL or SIGSTOP are treated exceptionally: these signals are forcibly delivered when sent from an ancestor PID namespace» (SIGKILL и SIGSTOP обрабатываются особо: они доставляются принудительно, если посланы из родительского пространства имён).https://man7.org/linux/man-pages/man7/pid_namespaces.7.html
  3. signal(7), Linux man-pages 6.7Официальная документация. Таблица умолчательных действий, из которой берётся `Term` у SIGTERM, и граница, за которую не выходит ни один обработчик: «SIGKILL and SIGSTOP cannot be caught, blocked, or ignored» (SIGKILL и SIGSTOP нельзя перехватить, заблокировать или проигнорировать). Отсюда же понятие диспозиции сигнала — что процесс с ним делает: умолчательное действие, игнорирование или обработчик.https://man7.org/linux/man-pages/man7/signal.7.html
  4. Bash Reference Manual, раздел Exit StatusОфициальная документация. Откуда берётся соглашение 128+N, по которому печатают код выхода оболочка и отчёты вокруг неё: «When a command terminates on a fatal signal N, bash uses the value of 128+N as the exit status» (Когда команда завершается по фатальному сигналу N, bash использует в качестве кода выхода значение 128+N). Это соглашение оболочки, а не ядра: ядро отдаёт номер сигнала отдельно от кода выхода, и складывает их именно тот, кто печатает результат.https://www.gnu.org/software/bash/manual/bash.html#Exit-Status