Процессы и сигналы в Linux
Собрал контейнер двумя способами и замерил остановку. Когда команда запуска написана строкой, docker stop ждал 10190 миллисекунд и добил приложение: код выхода 137. Когда та же команда написана списком, остановка заняла 153 миллисекунды и код выхода 0. Приложение одно и то же, разница в одной строке файла сборки.
Стержень: сигнал доходит не всегда - мешают состояние процесса и особый статус первого процесса в системе, и из этого растут обе классические беды.
// Формулировки: «что такое зомби?», «почему процесс не убивается?», «чем TERM отличается от KILL?»
Зомби: убивать нечего
Сделал настоящего зомби и посмотрел на него глазами ядра. Порождающий процесс создал ребёнка, ребёнок сразу завершился, а родитель не забрал его код возврата. В файле состояния процесса: State: Z (zombie), потоков 1, а строки с занятой памятью нет вовсе - память уже освобождена.
Дальше самое интересное. Отправил ему самый жёсткий сигнал - kill -9. Состояние не изменилось: как был Z, так и остался. Потом родитель вызвал ожидание ребёнка, и запись о процессе исчезла из системы мгновенно.
// Зомби - это не процесс, а строчка в таблице процессов с кодом возврата, которую никто не забрал. Ресурсов он не ест, убивать в нём нечего. Толпа зомби означает не утечку памяти, а баг родителя, который не забирает статусы детей: смотреть надо на него. И не путать с сиротой: сироту (её родитель умер) усыновляет первый процесс системы, и она прекрасно работает дальше.
- зомби (Z)
- запись о завершившемся процессе, чей код возврата не забрал родитель
- сирота
- живой процесс, чей родитель умер; его усыновляет первый процесс системы
TERM просит, KILL убивает
Замер второй. Запустил приложение, которое ставит свой обработчик на сигнал завершения и в нём ничего не делает. Отправил SIGTERM - через восемь десятых секунды процесс жив и работает. Отправил SIGKILL - процесс умер, код завершения показал номер сигнала 9.
Разница принципиальная. SIGTERM - вежливая просьба: приложение может её перехватить, дописать файлы, закрыть соединения, отдать текущие запросы и выйти по-человечески. SIGKILL перехватить нельзя вообще: его обрабатывает само ядро, процесс о нём не узнаёт и ничего доделать не успевает. Поэтому именно на SIGTERM вешают корректное завершение, а SIGKILL - последнее средство.
// Есть и третий случай, когда не проходит даже SIGKILL. Если поток стоит в состоянии D - непрерываемом сне внутри системного вызова, то есть внутри обращения к ядру за услугой вроде чтения с диска или отправки пакета, - ядро не выпускает его туда, где обрабатываются сигналы. Сигнал не теряется, он ждёт возврата из вызова. Лечат тут не силу сигнала, а причину: залипший том, больной диск, отвалившуюся сеть.
- SIGTERM
- просьба завершиться; приложение может её перехватить и прибраться
- состояние D
- непрерываемый сон в системном вызове: сигналы ждут возврата
Первый процесс и остановка контейнера
Теперь замер из начала целиком. Первый процесс системы (у него номер 1) получает от ядра поблажку: сигнал, на который не поставлен обработчик, просто отбрасывается. Иначе случайный сигнал завершения убивал бы всю систему.
В контейнере номер 1 достаётся тому, что вы запустили. Если команда написана строкой, её выполняет оболочка, и номер 1 получает она, а ваше приложение становится её ребёнком. Оболочка сигналы не пробрасывает и обработчика на них не имеет - сигнал завершения улетает в пустоту. Дальше система ждёт положенное время и добивает контейнер жёстким сигналом. Отсюда мои 10190 миллисекунд и код 137, который читается как 128 плюс 9, то есть «снят девятым сигналом».
// Если команду написать списком, оболочки не появляется, приложение само становится первым процессом, само ловит сигнал завершения и корректно выходит - 153 миллисекунды и код 0. Второй способ - флаг запуска с крошечным первым процессом внутри: он пробрасывает сигналы детям и попутно забирает статусы зомби. В моём замере с ним вышло 369 миллисекунд и код 0.
- процесс номер 1
- первый в системе: игнорирует сигналы, на которые нет обработчика
- код 137
- 128 плюс 9: контейнер добит жёстким сигналом после ожидания
Как отвечать: «Процесс не убивается через kill -9. Что происходит?»
Сначала смотрю состояние процесса, потому что за одним симптомом стоят два разных случая. Если состояние Z - убивать нечего: процесс уже завершился, осталась только запись с кодом возврата, которую не забрал родитель. Я это проверял: жёсткий сигнал зомби не меняет вообще ничего, а исчезает он ровно в тот момент, когда родитель забирает статус. Значит, разбираться надо с родителем или перезапускать его. Если состояние D, поток стоит внутри системного вызова, обычно на вводе-выводе, и ядро не выпускает его туда, где обрабатываются сигналы. Сигнал при этом не теряется, он ждёт возврата из вызова. Тут лечат причину: смотрю, на чём именно завис поток и что говорят системные сообщения о дисках, проверяю сетевые монтирования. Сигнал посильнее в обоих случаях не поможет - его просто некому или нечем обработать.
Ответ различает два состояния с одинаковым симптомом и в каждом ведёт к правильному действию, а не к перебору команд.
На чём валятся
- −Добивают зомби сигналом: жёсткий сигнал не изменил его состояние, помог только родитель.
- −Считают, что жёсткий сигнал проходит всегда - в состоянии D он ждёт возврата из системного вызова.
- −Пишут команду запуска контейнера строкой: остановка заняла 10190 мс вместо 153 и закончилась кодом 137.
- −Путают сироту и зомби: сироту усыновляет первый процесс, и она продолжает работать.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 8, остальные разбираются в тренажёре.
- Процесс не реагирует ни на SIGTERM, ни на SIGKILL, в ps состояние D. Что происходит?A)У процесса стоит обработчик SIGKILL и он его перехватываетB)Процесс запущен от root, а сигнал шлётся из-под обычного пользователяC)Это зомби: убивать в нём уже нечегоD)Процесс стоит в системном вызове — сигнал ждёт выхода
показать ответ и разбор
+D)Процесс стоит в системном вызове — сигнал ждёт выхода// разбор: D — uninterruptible sleep: поток застрял в системном вызове (диск, NFS, устройство), и ядро не выпускает его в юзерспейс, где обрабатываются сигналы. SIGKILL не теряется, а ждёт: процесс умрёт, как только вызов вернётся. Лечат причину — отвалившийся сетевой том, больной диск, залипшее устройство, — а не силу сигнала.
- Приложение — PID 1 в контейнере. docker stop ждёт весь таймаут и роняет его кодом 137. Почему?A)docker stop сразу шлёт SIGKILL, SIGTERM он не используетB)Сигнал уходит в namespace, а не процессу, до PID 1 он не доходитC)PID 1 без своего обработчика сигнал просто не увидитD)SIGTERM обработан, но контейнер ждёт закрытия stdout и stderr
показать ответ и разбор
+C)PID 1 без своего обработчика сигнал просто не увидит// разбор: Ядро не применяет к PID 1 действия по умолчанию: сигнал без установленного обработчика отбрасывается — иначе случайный TERM убивал бы init. Приложение (или sh из shell-формы CMD) обработчик обычно не ставит, поэтому docker ждёт grace-период и добивает SIGKILL: отсюда 137 = 128+9 и потерянный graceful shutdown. Лечится exec-формой CMD, флагом --init/tini или обработкой сигнала в коде.
- Родительский процесс упал, а его дочерний демон продолжает работать. Кто теперь его родитель (PPID) в ps?A)Процесс завершится сразу следом за своим родителемB)Останется без родителя и постепенно превратится в зомбиC)Его усыновит init/systemd, PPID станет 1D)Его подхватит ближайший живой процесс из той же группы
показать ответ и разбор
+C)Его усыновит init/systemd, PPID станет 1// разбор: Когда родитель умирает раньше ребёнка, ядро переназначает ребёнку родителем PID 1 (init/systemd) — такой процесс называют сиротой, он спокойно работает дальше. Когда завершится, его код возврата заберёт init, поэтому в зомби он не застрянет. Зомби — обратная ситуация: процесс завершился, а живой родитель не сделал wait().
- На хосте кончилась память, ядро начинает кого-то убивать. Как OOM-killer выбирает жертву и как защитить критичный процесс?A)Наибольший oom_score (по памяти); защита — снизить oom_score_adjB)Убивает самый старый процесс — тот, что дольше всех работает на хостеC)Убивает того, кто последним попросил у ядра ещё памятиD)Убивает процесс с наименьшим PID как самый системный
показать ответ и разбор
+A)Наибольший oom_score (по памяти); защита — снизить oom_score_adj// разбор: OOM-killer считает каждому процессу oom_score — в основном пропорционально занятой памяти, с поправкой oom_score_adj из /proc/PID/oom_score_adj. Убивает наибольший, чтобы одним ударом освободить максимум. Критичный процесс защищают отрицательным oom_score_adj (вплоть до −1000 — «не трогать»), а не надеждой на удачу.
- Сервис, запущенный из ssh-сессии через ./app &, умирает, как только закрываешь терминал. Почему и как правильно?A)Фоновому процессу при выходе из шелла урезают приоритет до нуляB)Шелл при выходе делает kill всем своим дочерним по SIGKILLC)Процесс без управляющего терминала ядро отправляет в стопD)При закрытии терминала процессам сессии прилетает SIGHUP; спасают nohup/setsid или systemd
показать ответ и разбор
+D)При закрытии терминала процессам сессии прилетает SIGHUP; спасают nohup/setsid или systemd// разбор: Закрытие терминала рвёт управляющий терминал сессии, и её процессам ядро шлёт SIGHUP, дефолтная реакция на который — завершиться.
&уводит в фон, но из сессии не выводит. Чтобы процесс пережил логаут: nohup (игнор SIGHUP) с перенаправлением вывода, setsid/новая сессия, disown (убрать из джобов шелла), а на проде — просто отдать systemd, где своя сессия.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.