сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Linux и bash

Процессы и сигналы в 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, остальные разбираются в тренажёре.

  1. #dvo_linux_processes1 / 5
    Процесс не реагирует ни на SIGTERM, ни на SIGKILL, в ps состояние D. Что происходит?
    A)У процесса стоит обработчик SIGKILL и он его перехватывает
    B)Процесс запущен от root, а сигнал шлётся из-под обычного пользователя
    C)Это зомби: убивать в нём уже нечего
    D)Процесс стоит в системном вызове — сигнал ждёт выхода
    показать ответ и разбор
    +D)Процесс стоит в системном вызове — сигнал ждёт выхода

    // разбор: D — uninterruptible sleep: поток застрял в системном вызове (диск, NFS, устройство), и ядро не выпускает его в юзерспейс, где обрабатываются сигналы. SIGKILL не теряется, а ждёт: процесс умрёт, как только вызов вернётся. Лечат причину — отвалившийся сетевой том, больной диск, залипшее устройство, — а не силу сигнала.

  2. #dvo_linux_processes2 / 5
    Приложение — 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 или обработкой сигнала в коде.

  3. #dvo_linux_processes3 / 5
    Родительский процесс упал, а его дочерний демон продолжает работать. Кто теперь его родитель (PPID) в ps?
    A)Процесс завершится сразу следом за своим родителем
    B)Останется без родителя и постепенно превратится в зомби
    C)Его усыновит init/systemd, PPID станет 1
    D)Его подхватит ближайший живой процесс из той же группы
    показать ответ и разбор
    +C)Его усыновит init/systemd, PPID станет 1

    // разбор: Когда родитель умирает раньше ребёнка, ядро переназначает ребёнку родителем PID 1 (init/systemd) — такой процесс называют сиротой, он спокойно работает дальше. Когда завершится, его код возврата заберёт init, поэтому в зомби он не застрянет. Зомби — обратная ситуация: процесс завершился, а живой родитель не сделал wait().

  4. #dvo_linux_processes4 / 5
    На хосте кончилась память, ядро начинает кого-то убивать. Как OOM-killer выбирает жертву и как защитить критичный процесс?
    A)Наибольший oom_score (по памяти); защита — снизить oom_score_adj
    B)Убивает самый старый процесс — тот, что дольше всех работает на хосте
    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 — «не трогать»), а не надеждой на удачу.

  5. #dvo_linux_processes5 / 5
    Сервис, запущенный из ssh-сессии через ./app &, умирает, как только закрываешь терминал. Почему и как правильно?
    A)Фоновому процессу при выходе из шелла урезают приоритет до нуля
    B)Шелл при выходе делает kill всем своим дочерним по SIGKILL
    C)Процесс без управляющего терминала ядро отправляет в стоп
    D)При закрытии терминала процессам сессии прилетает SIGHUP; спасают nohup/setsid или systemd
    показать ответ и разбор
    +D)При закрытии терминала процессам сессии прилетает SIGHUP; спасают nohup/setsid или systemd

    // разбор: Закрытие терминала рвёт управляющий терминал сессии, и её процессам ядро шлёт SIGHUP, дефолтная реакция на который — завершиться. & уводит в фон, но из сессии не выводит. Чтобы процесс пережил логаут: nohup (игнор SIGHUP) с перенаправлением вывода, setsid/новая сессия, disown (убрать из джобов шелла), а на проде — просто отдать systemd, где своя сессия.

дальше

Теорию прочитали. Навык ставится повторением

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