Процессы и сигналы
| Идентификатор | PID; у родителя — PPID |
|---|---|
| Создание | fork() и execve() |
| Первый процесс | init/systemd (PID 1) |
| Состояния | R (выполняется), S (спит), D (ожидает ввода-вывода), T (остановлен), Z (зомби) |
| Частые сигналы | SIGTERM (15), SIGKILL (9), SIGINT (2), SIGHUP (1), SIGCHLD |
| Инструменты | ps, top, htop, pgrep, kill, pkill, /proc |
Процесс — экземпляр выполняющейся программы: код, загруженный в память, вместе с данными, открытыми файлами, переменными окружения и правами пользователя. У каждого процесса в Linux есть уникальный номер — PID — и родитель (PPID). Сигнал — короткое асинхронное уведомление, которое ядро или другой процесс посылает процессу: «завершись», «перечитай конфигурацию», «ты обратился к чужой памяти», «твой дочерний процесс закончил работу».
Понимание процессов и сигналов нужно, чтобы корректно останавливать службы, искать зависшие программы, писать скрипты и правильно завершать приложения в Docker и Kubernetes.
Жизнь процесса
Новые процессы в Unix создаются двумя системными вызовами: fork() создаёт копию текущего процесса, а execve() заменяет программу в этой копии на другую.[2] Так оболочка запускает каждую команду: делает fork, в дочернем процессе вызывает exec нужной программы, а сама ждёт её завершения через wait.
Все процессы образуют дерево с корнем в процессе с PID 1 — сегодня это обычно systemd. Завершившись, процесс возвращает код выхода (0 — успех) и становится зомби — записью в таблице процессов, ожидающей, пока родитель прочитает код выхода. Если родитель умер раньше, «сирот» усыновляет PID 1 и сам их «хоронит».
| Состояние | Обозначение | Смысл |
|---|---|---|
| Выполняется | R | работает или готов к работе |
| Спит | S | ждёт события (сеть, таймер, ввод) |
| Непрерываемый сон | D | ждёт ввода-вывода, не реагирует даже на SIGKILL |
| Остановлен | T | приостановлен (Ctrl+Z, отладчик) |
| Зомби | Z | завершён, родитель не забрал код выхода |
Просмотр процессов
ps aux # все процессы: пользователь, PID, CPU, память, команда
ps -ef --forest # дерево процессов
pgrep -a nginx # найти по имени
top # интерактивно; htop — удобнее
cat /proc/1234/status # всё о процессе из виртуальной ФС /proc
ls -l /proc/1234/fd # открытые файлы и сокеты
lsof -i :8080 # какой процесс занял порт
Каталог /proc — окно в ядро: для каждого процесса есть каталог с его командной строкой, окружением, картой памяти и открытыми файлами.[3][4]
Сигналы
| Сигнал | Номер | Действие по умолчанию | Когда приходит |
|---|---|---|---|
| SIGHUP | 1 | завершение | закрыт терминал; демоны часто перечитывают конфигурацию |
| SIGINT | 2 | завершение | Ctrl+C в терминале |
| SIGQUIT | 3 | завершение с дампом | Ctrl+\ |
| SIGKILL | 9 | немедленное завершение | kill -9; нельзя перехватить |
| SIGSEGV | 11 | завершение с дампом | обращение к недопустимой памяти |
| SIGPIPE | 13 | завершение | запись в закрытый канал |
| SIGTERM | 15 | завершение | вежливая просьба завершиться — kill по умолчанию |
| SIGCHLD | 17 | игнорируется | завершился дочерний процесс |
| SIGSTOP, SIGCONT | 19, 18 | остановка, продолжение | приостановить и возобновить; SIGSTOP нельзя перехватить |
Процесс может перехватить большинство сигналов и обработать их по-своему — например, по SIGTERM закрыть соединения и сохранить данные. SIGKILL и SIGSTOP перехватить нельзя: их обрабатывает ядро.[1]
kill 1234 # SIGTERM — попросить завершиться
kill -HUP $(pgrep nginx) # перечитать конфигурацию
kill -9 1234 # SIGKILL — крайняя мера
pkill -f "python worker.py" # по шаблону командной строки
Корректное завершение
Правильный порядок остановки — сначала SIGTERM, дать процессу время закончить работу и только затем SIGKILL. Так поступают systemd (TimeoutStopSec), docker stop (по умолчанию 10 секунд)[5] и Kubernetes (terminationGracePeriodSeconds). Приложение должно обрабатывать SIGTERM: перестать принимать новые запросы, дождаться текущих, закрыть соединения с базой.
import signal, sys
def shutdown(signum, frame):
print("получен SIGTERM, завершаемся")
server.stop(grace=10)
sys.exit(0)
signal.signal(signal.SIGTERM, shutdown)
В Bash обработчик ставится командой trap 'cleanup' TERM INT EXIT.
Процессы в контейнерах
В контейнере приложение часто становится PID 1, а у PID 1 особые правила: ядро не применяет к нему действия сигналов по умолчанию, поэтому процесс, не установивший обработчик SIGTERM, просто его проигнорирует, и контейнер будет убит по таймауту. Кроме того, PID 1 обязан собирать зомби. Решения — запускать команду в exec-форме (CMD ["python", "app.py"], а не через sh -c, см. Dockerfile), обрабатывать сигналы в приложении или использовать маленький init вроде tini (docker run --init).[6]
Приоритеты и ресурсы
Приоритет планирования задаётся значением nice от −20 (высший) до 19: nice -n 10 ./backup.sh, renice. Ограничения ресурсов — ulimit (число открытых файлов, размер стека) и cgroups (память, процессор; см. Namespaces и cgroups). Когда память кончается, ядро запускает OOM killer, который завершает процесс с наибольшим «счётом» потребления — частая причина внезапной гибели баз данных и Java-приложений.