Сигналы, зомби и 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.
Что здесь на самом деле спрашивают
Лестница почти всегда одна и та же, и держится она на одном механизме, хотя вопросы выглядят несвязанными:
- «Что такое зомби-процесс?» — разминка, на которой отсеиваются те, кто думает, что это зависший процесс.
- «Чем он опасен, если самого процесса уже нет?» — здесь начинается содержание.
- «Кто становится родителем осиротевшего процесса?» — проверяют, знаете
ли вы про усыновление и про
subreaper. - «Почему контейнер останавливается не сразу?» — вопрос, на который напрашивается ответ «медленно закрывает соединения»: правдоподобно и мимо механизма.
- «Что означает код выхода 137?» — знаете ли вы арифметику и понимаете ли, чего этот код НЕ говорит.
- «Зачем в контейнер кладут
tini?» — вопрос, который проверяет, связали ли вы первые пять в одно.
Урок идёт по этой лестнице и идёт в одну сторону: сначала правило, потом его
следствия. Все числа получены на одной машине прогоном
bench/signals/lifecycle.py и bench/signals/practice.py; номера процессов и
миллисекунды у вас будут другие, а состояния, коды выхода и исход SIGTERM для
PID 1 совпадут.
База: процесс, его номер и сообщение, которое ему шлют
Прежде чем говорить о зомби и о номере 1, стоит назвать обычными словами четыре вещи, на которых держится весь урок.
Процесс — это запущенная программа. Не файл на диске, а его запуск: одна и та же программа, запущенная дважды, — это два процесса, и живут они независимо друг от друга.
У процесса есть номер. Ядро выдаёт его при запуске, и по этому номеру процесс потом находят: чтобы посмотреть на него, остановить или дождаться его конца.
Сигнал — это короткое сообщение процессу. Посылает его ядро или другой процесс, и содержания в нём ровно столько, сколько в самом номере сигнала: «завершись по-хорошему», «прекрати немедленно», «приостановись». Ни текста, ни ответа: сигнал либо что-то с процессом сделал, либо не сделал ничего.
У процессов есть родитель и потомок — тот, кто запустил, и тот, кого запустили. Связь эта не формальная: именно родителю потом достаётся ответ на вопрос, чем кончился потомок.
Из последних двух пунктов прямо следует первая ступень лестницы. Потомок завершился; родитель ещё не спросил, чем всё кончилось; ядру нужно где-то держать ответ на этот будущий вопрос — и оно его держит. Состояние «процесса уже нет, а запись о том, чем он кончился, ещё есть» и называют зомби.
И ещё одно, без чего вторая половина урока читается как магия: у контейнера своё пространство номеров. Процессы внутри нумеруются с единицы, как в только что загрузившейся системе, а снаружи у того же самого процесса номер совсем другой. Один процесс — два номера: внутри 1, снаружи обычный. Тот, кто останавливает контейнер, живёт снаружи и шлёт сигнал по внешнему номеру — а правила к процессу применяются те, которые действуют для номера 1.
Этого уже достаточно, чтобы ответить на базовый вопрос собеседования. Дальше — про то, почему один и тот же сигнал по-разному действует на обычный процесс и на процесс с номером 1, кому достаётся потомок, если родитель умер раньше, и откуда в отчёте берётся код 137.
Механизм 1: смерть процесса — это два события, а не одно
Теперь то же самое словами документации — и начнём с того, что не зависит ни от дистрибутива, ни от версии ядра.
Процесс заканчивается дважды. Сначала он перестаёт выполняться — вызывает
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 и получить сведения о потомке.
Отсюда сразу ответ на второй вопрос лестницы. Зомби опасен не тем, что «висит в памяти»: адресного пространства процесса уже нет, а то, что осталось, — минимальный набор сведений, перечисленный в цитате выше. Опасен он тем, что эта запись занимает слот в таблице процессов, а таблица конечна:
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, он занимает слот в таблице процессов ядра, и если эта таблица заполнится, создать новые процессы будет невозможно.
Так выглядит утечка, у которой в графике памяти ничего не видно. Приложение
работает, память ровная, а fork в какой-то момент перестаёт удаваться —
и падает не тот, кто виноват.
Вот это в прогоне. Потомок вызвал _exit(7) и был оставлен без wait:
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).
Прогон показывает сам момент замены. Внук печатает своего родителя дважды — сразу после запуска и после того, как его отец завершился:
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.
Первому процессу пространства имён другие его члены могут послать только те сигналы, для которых он установил обработчик. Ограничение действует даже для привилегированных процессов и не даёт остальным членам пространства имён случайно убить первый процесс.
Снаружи — то же самое, и это как раз случай оркестратора: он живёт в родительском пространстве имён и шлёт сигнал вашему первому процессу.
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 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
Обратите внимание, что сигнал шлётся снаружи и по внешнему номеру (5362), а внутри пространства имён у этого процесса номер 1. Так же поступает и оркестратор: он не заходит внутрь контейнера, он посылает сигнал процессу, который снаружи выглядит обычным.
Есть исключение, и оно ровно одно:
SIGKILL or SIGSTOP are treated exceptionally: these signals are forcibly
delivered when sent from an ancestor PID namespace.
SIGKILL и SIGSTOP обрабатываются особо: они доставляются принудительно, если посланы из родительского пространства имён.
Поэтому «контейнер невозможно остановить» — неверная формулировка. Остановить можно всегда; вопрос в том, ждёт ли оркестратор перед этим весь льготный срок впустую.
Механизм 4: сколько стоит отсутствие обработчика и откуда в отчёте 143 и 137
Остановка контейнера — это три шага: SIGTERM, льготный срок, SIGKILL.
Длину срока задаёт тот, кто останавливает, и она называется в его настройках;
для урока важно не конкретное число, а то, что срок конечен и после него
приходит сигнал, который не ловится. Разница между «остановился за
миллисекунды» и «остановился за весь срок» — это ровно наличие обработчика.
Скрипт воспроизводит эту схему со сроком в две секунды:
stop time, SIGTERM handled 11 ms
stop time, no handler 2009 ms
Второе число — не «медленное завершение». Это весь срок ожидания плюс время,
за которое доходит 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.
Отсюда и таблица, которую спрашивают:
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 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 = 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())
Практика · оцените
Проверка знаний
Процесс-потомок завершился, родитель не вызвал wait. Сколько памяти занимает потомок?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- Смерть процесса — это два события, а не одно. Сначала процесс перестаёт выполняться, потом кто-то забирает его статус; между этими двумя событиями от процесса остаётся запись, и её называют зомби. А процесс, оказавшийся в контейнере первым, ядро оберегает: сигнал, для которого у него нет обработчика, не делает с ним ничего. Это правило ядра, а не особенность Docker.
- Отсюда главное следствие: образ, у которого первый процесс не обрабатывает
SIGTERM, не останавливается поdocker stop. Он выжидает весь льготный срок и уходит поSIGKILL. Измерено на льготном сроке в две секунды: 11 мс с обработчиком против 2009 мс без него. Вторая поломка тише: незабранные потомки сами не рассасываются — тысяча завершившихся потомков безwaitдала тысячу зомби, то есть 100 % созданных, и ноль послеwait. - Дальше — то, что отличает знающего от читавшего. Сирота достаётся не «системе», а первому процессу или ближайшему
subreaper: измерено, что родитель внука меняется с настоящего номера на1в тот момент, когда его отец умирает. У процесса с номером 1 в своём пространстве имён умолчательные действия сигналов не выполняются, и единственное исключение —SIGKILLиSIGSTOP: из родительского пространства имён они доставляются принудительно и не ловятся ничем, поэтому оркестратор всегда добивается своего, вопрос только в задержке. 137 — это 128 + 9, а 143 — 128 + 15, и складывает эти числа оболочка, а не ядро: 137 значит «убитSIGKILL» и ничего не говорит о том, кто послал. И зомби «висит в памяти» только на словах: адресного пространства процесса уже нет, но ядро сохраняет небольшую запись — PID, статус завершения, данные о потреблённых ресурсах, — и она занимает слот в таблице процессов.
На самом деле
- Убивать нечего: процесса уже нет, есть запись о его смерти — PID, статус завершения, потреблённые ресурсы. Адресного пространства за ней нет, а вот слот в таблице процессов занят, и
wait(2)прямо говорит, что при заполнении таблицы it will not be possible to create further processes. Убирает записьwaitродителя, иkill -9зомби ничего не меняет — сигналы мёртвому процессу не доставляются. - Это правило ядра, записанное в
pid_namespaces(7): первому процессу пространства имён доставляются только те сигналы, для которых он установил обработчик. Docker здесь ни при чём — он лишь запускает ваш процесс первым. Измерено: один и тот жеSIGTERM, посланный снаружи, оставляет процесс без обработчика живым и завершает процесс с обработчиком. - 137 — это 128 + 9, то есть «убит
SIGKILL», и кто послал сигнал, код не сообщает. OOM-killer, оркестратор после льготного срока и человек сkill -9дают одинаковые 137. Диагноз ставится по другим уликам: сообщению ядра, событию оркестратора, времени относительно начала выкатки. - Остановит, но через весь льготный срок и через
SIGKILL, у которого нет обработчика: незакрытые соединения оборвутся, недописанное не допишется, в логе не останется ничего. Измерено при льготном сроке в две секунды: 11 мс с обработчиком против 2009 мс без него — и вторая цифра целиком состоит из ожидания. - Усыновляет конкретный процесс:
initили ближайшийsubreaper(PR_SET_CHILD_SUBREAPER). Разница практическая: первый процесс обычной системы статусы забирает, а ваше приложение, ставшее PID 1 в контейнере, — нет. Поэтому в контейнере сироты и превращаются в зомби, а в системе — нет. - Они не про запуск, а про две обязанности PID 1, которых у приложения нет: пересылать сигналы дочернему процессу и забирать статусы сирот. Если приложение делает и то и другое само, init не нужен; если оно не порождает процессов, достаточно обработчика
SIGTERM. - Нельзя:
signal(7)говорит прямо: SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. Именно поэтому оркестратор им и добивает: это единственный сигнал, исход которого не зависит от кода приложения. И по той же причине после него в логе не бывает записей.
Что разобрано
- Что здесь на самом деле спрашивают
- База: процесс, его номер и сообщение, которое ему шлют
- Механизм 1: смерть процесса — это два события, а не одно
- Механизм 2: у сироты новый родитель, и это не абстракция
- Механизм 3: правило PID 1, из которого растёт всё остальное
- Механизм 4: сколько стоит отсутствие обработчика и откуда в отчёте 143 и 137
- Глубже: зачем в контейнер кладут init
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
4 ИСТОЧНИКА
- 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
- 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
- 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
- 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