Процессы, сигналы и зомби

Три вопроса о жизненном цикле процесса, которые отделяют «умею перезапускать сервис» от «понимаю, что при этом происходит».

Вопрос: «Чем kill отличается от kill -9, и почему второй вариант не стоит делать по привычке?»

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

Это вопрос про корректное завершение работы. DevOps каждый день останавливает и катит приложения; если инженер по привычке шлёт -9, он теряет несохранённые данные, оставляет битые блокировки и ломает graceful shutdown. Второй слой — понимает ли кандидат, что сигнал это не «убийство», а асинхронное уведомление, на которое программа может отреагировать.

Развёрнутый ответ

kill без аргументов посылает SIGTERM (15) — вежливую просьбу завершиться. Процесс может её перехватить: закрыть соединения, дописать буферы на диск, снять блокировку, отписаться от очереди — и только потом выйти. Именно на SIGTERM построен graceful shutdown в nginx, PostgreSQL и любом нормальном веб-приложении.

SIGKILL (9) обрабатывает не процесс, а ядро: оно просто снимает процесс с выполнения и освобождает память. Перехватить или проигнорировать SIGKILL невозможно — в этом его сила и его опасность. Данные из буферов не попадут на диск, временные файлы и pid-файлы останутся, дочерние процессы могут осиротеть.

kill 4231          # SIGTERM: попроси завершиться
kill -TERM 4231    # то же самое, явно
kill -HUP 4231     # перечитать конфиг (nginx, sshd)
kill -9 4231       # SIGKILL: последнее средство

# список сигналов
kill -l | head -3

Правильный порядок действий: SIGTERM → подождать разумный таймаут (5–30 секунд) → если процесс жив, SIGKILL. Ровно так делают systemd (TimeoutStopSec), Docker (docker stop ждёт 10 секунд) и Kubernetes (terminationGracePeriodSeconds, по умолчанию 30). Если ваше приложение не успевает закрыться за это время — увеличивайте таймаут, а не переходите сразу к -9.

Полезные сигналы, которые стоит назвать

СигналСмыслПерехватывается
SIGTERM (15)вежливое завершениеда
SIGINT (2)Ctrl+C в терминаледа
SIGHUP (1)обрыв терминала или «перечитай конфиг»да
SIGKILL (9)снять немедленнонет
SIGSTOP (19)заморозить процесснет

Вопрос 2: что такое зомби-процесс и опасен ли он

Когда процесс завершается, ядро не удаляет его запись сразу: оно оставляет код возврата, чтобы родитель мог его прочитать вызовом wait(). Такое состояние называется zombieps — статус Z). Это нормальная промежуточная фаза длиной в миллисекунды.

Проблема начинается, если родитель написан небрежно и никогда не вызывает wait(). Тогда записи копятся. Памяти зомби почти не занимает, но таблица PID конечна: при накоплении десятков тысяч зомби система перестаёт создавать новые процессы, и вы получаете fork: retry: Resource temporarily unavailable.

# найти зомби и их родителей
ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/'

# вывод: PID  PPID STAT COMMAND
#        8123  8100 Z    worker

# kill -9 на зомби бесполезен: он уже мёртв.
# лечится перезапуском РОДИТЕЛЯ (PPID 8100)

Отдельно стоит назвать сироту (orphan): это живой процесс, чей родитель умер. Его усыновляет PID 1, который корректно вызывает wait(). Поэтому сироты, в отличие от зомби, безвредны.

Вопрос 3: почему в контейнере не работает Ctrl+C

Любимый вопрос на стыке Linux и Docker. Процесс с PID 1 имеет особенность: для него не работают сигналы по умолчанию. Обычный процесс без обработчика SIGTERM просто умирает — за него это делает ядро. У PID 1 такого поведения нет: если приложение не установило обработчик SIGTERM явно, сигнал будет проигнорирован.

Отсюда классический симптом: docker stop висит ровно 10 секунд, а потом контейнер убивается жёстко. Вторая частая причина — приложение запущено через шелл (CMD npm start в shell-форме): PID 1 получает /bin/sh, который сигнал не пробрасывает потомку.

# плохо: PID 1 = /bin/sh -c, сигналы не доходят до node
CMD npm start

# хорошо: exec-форма, PID 1 = node, он сам получает SIGTERM
CMD ["node", "server.js"]

# если процесс плодит потомков и не собирает их — включаем init
# docker run --init ...

Типичные ошибки кандидатов

  • «kill -9 надёжнее, поэтому я всегда так» — красный флаг, показывающий, что человек не сталкивался с потерей данных после жёсткого убийства СУБД.
  • Пытаются убить зомби через kill -9. Зомби уже не выполняется, сигналы к нему неприменимы — убирать надо родителя.
  • Путают зомби и сироту: сирота живёт и работает, зомби — только запись в таблице процессов.
  • Не знают про особенность PID 1 и потом неделю ищут, почему поды в Kubernetes всегда выселяются по таймауту.
  • Забывают, что SIGKILL и SIGSTOP невозможно перехватить — на этом строится уточняющий вопрос.

Как ответить кратко (20–30 секунд)

«kill шлёт SIGTERM — процесс может его перехватить и корректно завершиться: дописать буферы, закрыть соединения. kill -9 — это SIGKILL, его обрабатывает ядро, перехватить нельзя, поэтому данные в буферах теряются. Правильная последовательность — SIGTERM, таймаут, и только потом SIGKILL; именно так работают systemd, docker stop и Kubernetes. Отдельно помню, что PID 1 игнорирует сигналы без явного обработчика — из-за этого контейнеры зависают при остановке.»

Проверьте себя
1. Почему нельзя убрать зомби-процесс командой kill -9?
AПотому что у зомби нет прав на получение сигналов
BПотому что зомби уже завершился: осталась только запись в таблице процессов, которую должен забрать родитель через wait()
CПотому что SIGKILL работает только для процессов в состоянии D
DПотому что зомби защищён от сигналов ядром до перезагрузки
2. Контейнер всегда останавливается ровно за 10 секунд по таймауту, хотя приложение умеет graceful shutdown. Наиболее вероятная причина?
AВ контейнере не хватает памяти для завершения
BCMD задан в shell-форме, поэтому PID 1 — это /bin/sh, который не пробрасывает SIGTERM приложению
CDocker всегда использует SIGKILL при docker stop
DУ образа не указан HEALTHCHECK