Что происходит с процессом между kill и его реальной смертью
Команда kill -9 кажется мгновенной: нажал — процесс исчез. На самом деле между отправкой сигнала и реальным освобождением ресурсов проходит целая последовательность шагов, и на каждом что-то может пойти не так — от зависшего процесса, который «не хочет» умирать, до повреждённых данных на диске из-за резкого обрыва записи. Разберём по шагам, что на самом деле происходит с процессом от kill до его исчезновения из таблицы процессов ядра — и как использовать это знание, чтобы деплой и рестарт сервисов на вашем VPS не роняли данные.
Содержание
- Что на самом деле происходит, когда вы пишете kill
- SIGTERM: сигнал, который процесс может поймать
- Что происходит в эти секунды: graceful shutdown изнутри
- Зомби: короткое пограничное состояние между исчезновением и смертью
- SIGKILL: когда терпение заканчивается
- Почему резкий kill -9 опасен для баз данных и файлов
- Как это устроено в systemd и Docker на практике
Что на самом деле происходит, когда вы пишете kill
kill — это не «убить», а «отправить сигнал». Команда kill -TERM 1234 не завершает процесс с PID 1234 сама по себе — она просит ядро доставить этому процессу извещение о том, что его просят завершиться. Дальше решение остаётся за самим процессом (если сигнал вообще можно перехватить) или за ядром (если нет).
В Linux у каждого сигнала есть номер и имя. Самые важные для повседневной работы:
| Сигнал | Номер | Можно перехватить? | Смысл |
|---|---|---|---|
| SIGHUP | 1 | да | «конфиг мог измениться», традиционно — перечитать настройки |
| SIGINT | 2 | да | Ctrl+C из терминала |
| SIGTERM | 15 | да | вежливая просьба завершиться (это шлёт kill по умолчанию) |
| SIGKILL | 9 | нет | безусловное уничтожение силами ядра |
| SIGSTOP | 19 | нет | приостановить выполнение |
Ключевое отличие: SIGTERM — это именно просьба. Процесс может её проигнорировать, обработать по-своему или отреагировать штатным образом (по умолчанию — завершиться). SIGKILL никакой альтернативы не оставляет: ядро не доставляет его в пользовательский код процесса вообще, оно напрямую убирает процесс из планировщика.
Если вы вводите просто kill 1234 без флага — уйдёт SIGTERM. Это осознанный выбор разработчиков утилиты: дать процессу шанс прибраться за собой, прежде чем переходить к силовым методам.
SIGTERM: сигнал, который процесс может поймать
Когда ядро доставляет SIGTERM, у процесса есть три варианта поведения:
- Default action — если обработчик не установлен, происходит действие по умолчанию: для SIGTERM это немедленное завершение.
- Custom handler — приложение зарегистрировало функцию, которая выполнится при получении сигнала. Именно здесь живёт вся логика graceful shutdown.
- Ignore — процесс явно попросил ядро игнорировать этот сигнал. Для SIGTERM это разрешено, для SIGKILL — нет.
Пример обработчика на Python:
import signal
import sys
def handle_sigterm(signum, frame):
print("Получен SIGTERM, начинаю грациозное завершение")
# закрыть соединения с БД, доотдать текущие запросы, сохранить состояние
sys.exit(0)
signal.signal(signal.SIGTERM, handle_sigterm)
И на Node.js:
process.on('SIGTERM', () => {
console.log('Получен SIGTERM, закрываю сервер');
server.close(() => {
db.end();
process.exit(0);
});
});
Важный нюанс: обработчик сигнала может прервать процесс буквально в любой точке кода. Поэтому в нём принято не городить сложную логику напрямую, а выставлять флаг («пора закругляться») и дать основному циклу событий доработать текущую итерацию корректно, либо, как в примерах выше, вызывать заранее подготовленные функции остановки сервера.
Здесь же кроется частая практическая проблема: если ваш процесс — это PID 1 внутри контейнера, а он не настроен на пересылку сигналов дочерним процессам, SIGTERM может просто не дойти до реального приложения. Мы разбирали похожий случай, когда PID 1 не пересылал сигналы и это роняло запросы при деплое — типичная ловушка для самодельных shell-скриптов в качестве ENTRYPOINT.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверЧто происходит в эти секунды: graceful shutdown изнутри
«Грациозное завершение» — это не абстрактная вежливость, а конкретная последовательность действий, которую должно выполнить приложение, получив сигнал на остановку:
- Перестать принимать новую работу. HTTP-сервер прекращает слушать порт или, чаще, продолжает слушать, но начинает отвечать
503/Connection: closeна новые подключения, чтобы балансировщик их не направлял сюда. - Довести до конца то, что уже начато. Активные запросы, открытые транзакции, задачи из очереди — им дают время закончиться штатно, а не обрываются на полуслове.
- Закрыть внешние соединения аккуратно. Пул соединений к базе данных, к Redis, к очереди сообщений — закрываются явно, а не бросаются на произвол сборщика мусора при завершении процесса.
- Сбросить буферы на диск. Если приложение что-то держит в памяти и должно было это записать (логи, промежуточные файлы, кэш) — самое время это сделать, пока есть возможность выполнять код.
- Сообщить оркестратору, что можно продолжать. Завершиться с кодом 0, чтобы systemd/Docker/Kubernetes знали: остановка прошла штатно, а не аварийно.
У этой последовательности всегда есть бюджет времени: если процесс не уложился в отведённое окно, за него решает уже не он сам. Именно поэтому у systemd есть TimeoutStopSec, у Docker — stop_grace_period (по умолчанию 10 секунд), у Kubernetes — terminationGracePeriodSeconds. Пока окно не истекло, обработчик может делать свою работу сколько угодно аккуратно. Как только оно истекло — на сцену выходит SIGKILL, и все несохранённые намерения приложения перестают иметь значение.
Сигналы доставляются мгновенно, а вот сколько времени займёт реакция на них — целиком зависит от логики приложения и от того, что творится с диском и сетью в этот момент. Мы отдельно разбирали, как отличаются по смыслу сигналы, которые процесс успевает обработать, до того как всё окончательно посыплется — цепочка событий перед падением сервиса часто состоит не из одного тревожного знака, а из нескольких сигналов подряд.
Зомби: короткое пограничное состояние между исчезновением и смертью
Здесь важно разделить два разных момента: когда процесс перестаёт выполнять код, и когда он окончательно исчезает из таблицы процессов ядра. Это не одно и то же, и разрыв между ними — то самое состояние «зомби».
Когда процесс завершается (получил SIGTERM/SIGKILL, вызвал exit(), дошёл до конца main), ядро не удаляет его запись немедленно. Оно:
- освобождает почти все ресурсы процесса — память, открытые файловые дескрипторы, стек;
- но оставляет в таблице процессов минимальную запись — PID, код завершения, статистику использования ресурсов;
- переводит процесс в состояние
Z(zombie / defunct), которое видно вps auxкак<defunct>.
Эта запись хранится специально: родительский процесс должен забрать код завершения через системный вызов wait() или waitpid(). Пока родитель не «собрал» (reap) завершённый дочерний процесс, тот висит в состоянии зомби. Обычно это занимает доли секунды — родитель почти сразу вызывает wait(), как только получает SIGCHLD (сигнал «один из твоих детей завершился»).
Проблема начинается, если родительский процесс не вызывает wait() — например, из-за бага, из-за того, что сам завис, или потому что это самописный shell-скрипт, который вообще не обрабатывает завершение дочерних процессов. Тогда зомби накапливаются. Важный нюанс: зомби-процесс не занимает память и не потребляет процессорное время, это просто запись в таблице. Но у таблицы процессов есть предел (pid_max), и при достаточном количестве зомби вы упрётесь именно в него — новые процессы не смогут получить PID.
Если родитель завершается сам, не собрав своих детей, ядро переусыновляет их зомби-записи процессу с PID 1, а тот обязан регулярно вызывать wait(), чтобы зомби не копились бесконечно. Поэтому то, что запущено как PID 1 в контейнере, должно уметь корректно «пожинать» дочерние процессы — обычные init-системы (systemd, tini, dumb-init) делают это из коробки, а голый CMD ["node", "app.js"] без обёртки — не всегда.
SIGKILL: когда терпение заканчивается
SIGKILL (номер 9) устроен принципиально иначе, чем SIGTERM. Он не доставляется в пользовательский код процесса вообще — ядро не даёт процессу шанса ни перехватить его, ни проигнорировать. Вместо этого ядро напрямую:
- убирает процесс из очереди планировщика;
- освобождает страницы памяти без участия кода приложения;
- закрывает файловые дескрипторы на уровне ядра, а не через логику приложения.
Разница на практике огромна. При SIGTERM приложение само решает, в каком порядке закрывать соединения и сбрасывать буферы. При SIGKILL всё, что приложение держало «в уме» и ещё не записало на диск или не отправило по сети, просто исчезает вместе с процессом — без возможности что-либо доделать.
kill -9 — не универсальное зло, это последний рубеж на случай, когда процесс не реагирует на SIGTERM (завис, заблокирован, зациклился) и его действительно нужно остановить любой ценой. Проблема в том, что многие используют kill -9 (или docker kill, или systemctl kill -s KILL) как рефлекторную первую реакцию на «что-то не отвечает», хотя правильный порядок — сначала SIGTERM и разумное ожидание, и только потом, если процесс не завершился, SIGKILL.
Отдельный частный случай — когда SIGKILL присылает не человек, а сам ядерный механизм: OOM killer, когда системе критически не хватает памяти, выбирает жертву и убивает её точно так же безусловно. Мы подробно разбирали как ядро выбирает жертву для OOM killer — механика в момент убийства процесса там та же самая, что и при ручном kill -9, разница только в том, кто инициирует сигнал.
Почему резкий kill -9 опасен для баз данных и файлов
Здесь стоит быть аккуратным с формулировками: SIGKILL сам по себе не «портит» файлы на диске в том смысле, что не пишет в них мусор. Опасность в другом — в том, что операция, которая была в процессе выполнения, обрывается на середине, и то, что должно было произойти атомарно, атомарным не оказывается.
Качественно это выглядит так:
- Незавершённая транзакция. Если СУБД в момент сигнала находится посреди записи нескольких взаимосвязанных изменений, резкий обрыв может оставить данные в промежуточном состоянии на диске. Серьёзные СУБД (PostgreSQL, MySQL/InnoDB) защищаются от этого журналом упреждающей записи (WAL / redo log): при следующем старте база проигрывает или откатывает незавершённые транзакции по журналу. Поэтому «повреждение данных» от
kill -9для них — не типичный сценарий, а скорее исключение. - Файлы без журнала транзакций в самом приложении. Если оно пишет файл в несколько шагов без атомарных операций (например, построчно дописывает JSON вместо записи во временный файл с последующим переименованием), обрыв ровно посередине оставит файл в невалидном виде — и здесь уже никакой WAL не поможет, потому что его просто нет.
- Долгое восстановление после старта. Даже когда журнал спасает данные, crash recovery при следующем запуске СУБД занимает время — это ожидаемо, но добавляет простой, которого не было бы при штатной остановке.
- Клиенты в подвешенном состоянии. Соединения к резко убитому процессу не получают явного
FIN— им нужно ждать таймаута TCP вместо мгновенного и явного разрыва при штатном закрытии сокета.
Практический вывод простой: SIGTERM даёт приложению шанс превратить потенциально «недоатомарную» операцию в атомарную — либо закончить её, либо откатить, — прежде чем процесс исчезнет. SIGKILL этого шанса не даёт вообще. Поэтому правило для эксплуатации на VPS звучит так: kill -9 и docker kill — это инструмент для случая «процесс не реагирует на вежливую просьбу», а не стандартный способ остановки сервиса.
Как это устроено в systemd и Docker на практике
На уровне конкретных инструментов эта же логика реализована через настраиваемые тайм-ауты и порядок сигналов.
systemd. Команда systemctl stop myservice по умолчанию отправляет SIGTERM главному процессу юнита и ждёт TimeoutStopSec (по умолчанию обычно 90 секунд) перед тем, как прислать SIGKILL. Настраивается в unit-файле:
[Service]
ExecStart=/usr/bin/myapp
KillMode=mixed
TimeoutStopSec=30
KillMode=mixed означает: сначала SIGTERM только главному процессу, затем, если он не завершился, SIGKILL уже всей control group — полезно, если приложение порождает воркеров, которые сами не подписаны на сигналы от systemd. Мы отдельно разбирали, как устроен запуск сервисов через systemd при загрузке — та же логика unit-файлов действует и при остановке, просто в обратном порядке.
Docker. docker stop шлёт SIGTERM процессу с PID 1 внутри контейнера и ждёт stop_grace_period (10 секунд по умолчанию, настраивается флагом -t или в docker-compose):
services:
app:
image: myapp:latest
stop_grace_period: 30s
stop_signal: SIGTERM
Если за это время контейнер не остановился — Docker отправляет SIGKILL. Здесь проявляется упомянутая выше проблема PID 1: если точка входа — sh -c "node app.js", SIGTERM получает sh, а не сам Node-процесс, и оболочка может просто завершиться сама, оставив дочерний процесс сиротой. Решается явным exec в скрипте-обёртке, использованием tini/dumb-init как init-процесса контейнера, либо запуском команды напрямую в exec-форме (CMD ["node", "app.js"]) без промежуточной оболочки.
PostgreSQL. Отдельно стоит знать про три режима штатной остановки, доступные через pg_ctl stop -m <mode>:
pg_ctl stop -m smart # ждать отключения всех клиентов, новых не пускать
pg_ctl stop -m fast # разорвать соединения сразу, но откатить активные транзакции штатно
pg_ctl stop -m immediate # аварийная остановка, как SIGKILL — при следующем старте будет crash recovery
Именно режим immediate эквивалентен по последствиям резкому kill -9: он пропускает штатный контрольный чекпоинт, и это надёжно, но требует последующего восстановления по WAL. Режимы smart и fast — это ровно то graceful shutdown, о котором шла речь выше, только реализованное внутри конкретной СУБД.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Чем kill -15 отличается от простого kill?
Ничем — SIGTERM (номер 15) это сигнал по умолчанию, если флаг не указан. kill 1234, kill -15 1234 и kill -TERM 1234 — одна и та же команда.
Можно ли перехватить SIGKILL, чтобы сделать красивое завершение?
Нет. SIGKILL и SIGSTOP нельзя ни поймать, ни заблокировать, ни проигнорировать — это осознанное решение архитектуры Unix, гарантированный способ остановить процесс, который сам этого не хочет.
Почему в ps aux висит процесс в статусе Z, а kill -9 на него не действует?
Зомби уже не выполняется — у него нет кода, которому можно доставить сигнал, это запись в таблице, ожидающая, что родитель вызовет wait(). Убить нечего; проблему нужно решать на стороне родителя, а если тот сам завис — перезапуском родителя.
Сколько времени безопасно давать процессу на graceful shutdown?
Универсального числа нет — ориентируйтесь на самый долгий разумный запрос или транзакцию, которые приложение обязано доводить до конца, и закладывайте окно с запасом, а не берите значение по умолчанию не глядя.
Как проверить, что контейнер правильно обрабатывает SIGTERM?
Запустите docker stop <container> с логированием на стороне приложения и посмотрите, появляется ли лог о начале graceful shutdown до остановки. Если он появляется только перед принудительным убийством по истечении stop_grace_period — сигнал до приложения не доходит.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →