PID 1 и init
PID 1 — корень дерева процессов. Его нельзя убить SIGKILL — если он умрёт, ядро запаникует. Его задачи: пожинание осиротевших процессов, супервизия сервисов, приведение системы к целевому состоянию. systemd заменил SysV init, сделав зависимости явными и параллельными.
Ты выполняешь kill -9 1 на Linux-машине. Ничего не происходит. Пробуешь снова. По-прежнему ничего. Это не баг с правами — PID 1 по дизайну неуязвим к SIGKILL. Ядро это гарантирует: если PID 1 умрёт, пути для восстановления нет, поэтому ядро просто отказывается доставлять сигнал. Понимание того, почему PID 1 особенный — и чем он занимается — объясняет всё: от зомби-процессов до того, почему твой сервис не перезапустился после сбоя.
После этого урока ты сможешь объяснить, что делает PID 1 и почему он особенный, описать как systemd отличается от SysV init по модели запуска и выражению зависимостей, понять пожинание осиротевших процессов, и читать вывод systemctl status для понимания дерева процессов.
PID 1 — предок каждого процесса в системе. Когда ядро завершает собственную инициализацию и выполняет /sbin/init, возникший процесс получает PID 1. Каждый другой процесс — это либо прямой потомок PID 1, либо его наследник. Дерево процессов укоренено здесь.
# Показать полное дерево процессов, начиная с PID 1
pstree -p 1
# Увидеть идентичность PID 1
ps -p 1 -o pid,comm,argsНа современных Debian/Ubuntu: 1 systemd /lib/systemd/systemd. На старых системах или в контейнерах с минимальным init: 1 init /sbin/init или даже 1 bash (если запущен с init=/bin/bash).
PID 1 пожинает осиротевшие зомби-процессы — и если не делает это, происходит утечка памяти. Когда процесс завершается, ядро хранит маленькую запись о нём (статус завершения), пока родитель не вызовет wait() для её получения. Эта запись называется зомби. Обычно родитель собирает её сразу. Но если родитель умирает раньше потомка, потомок становится сиротой и получает нового родителя — PID 1. Теперь PID 1 обязан вызвать wait() на нём.
Если PID 1 не вызывает wait() — потому что это наивная оболочка или плохо написанный init — сироты накапливаются как зомби. В конечном счёте таблица PID заполняется и новые процессы создать невозможно. Это реальный режим отказа в Docker-контейнерах, использующих произвольный CMD как PID 1 без правильной пересылки сигналов и пожинания.
# Подсчёт зомби-процессов — ненулевое значение означает, что PID 1 не жнёт
ps aux | awk '$8=="Z"' | wc -lPID 1 неуязвим к SIGKILL — по дизайну ядра, а не по привилегиям. Обычно SIGKILL (сигнал 9) не может быть перехвачен, заблокирован или проигнорирован никаким процессом. Это безусловный сигнал завершения. Единственное исключение: ядро никогда не доставляет SIGKILL на PID 1. Обоснование простое — если PID 1 умрёт, ядро всё равно не сможет продолжать работу и запаникует. Поэтому ядро защищает PID 1 превентивно.
# Это ничего не делает (даже от root)
sudo kill -9 1
# PID 1 всё ещё может получить SIGTERM (systemd обрабатывает его корректно)
sudo kill -15 1 # systemd интерпретирует это как "запустить kbrequest.target"Контейнеры — заметное исключение: внутри Docker-контейнера PID 1 можно убить SIGKILL с хоста (через docker kill), потому что ядро хоста рассматривает init контейнера как обычный процесс.
SysV init: последовательные скрипты, нет модели зависимостей. Предшественником systemd был SysV init. Его модель: пронумерованные уровни выполнения (0–6), и для каждого уровня набор shell-скриптов в /etc/rc<N>.d/, запускающих или останавливающих сервисы. Скрипты выполнялись последовательно — один за другим — что означало 60-секундную загрузку даже когда большинство сервисов не зависели друг от друга. Зависимости были неявными: порядок скриптов был единственным механизмом координации. Упавший сервис мог заблокировать всё последующее.
# Управление сервисами в стиле SysV (работает как шим совместимости на Ubuntu)
sudo service nginx start
sudo service nginx statussystemd: параллельная активация с явным графом зависимостей. systemd заменил SysV init, моделируя всё как юниты с объявленными зависимостями. Когда запрашивается multi-user.target, systemd обходит граф зависимостей и параллельно запускает все юниты, чьи зависимости удовлетворены. Сервис, которому нужна только сеть, может запускаться одновременно с сервисом, которому нужен только монтируемый раздел — они не блокируют друг друга.
# Посмотреть, что systemd активировал при последней загрузке и когда
systemd-analyze plot > boot.svg # генерирует визуальную временную шкалу
# Проверить граф зависимостей для юнита
systemctl list-dependencies sshd.service
# Проверить здоровье PID 1 (systemd)
systemctl is-system-runningКлючевое понимание: systemd превратил неявное упорядочение (номера скриптов) в явный граф (After=, Requires=, Wants=). Вот почему загрузка с systemd быстрее и почему упавший необязательный сервис не останавливает остальную загрузку.
Отладка накопления зомби в контейнере.
Docker-контейнер с долгим временем работы начал накапливать зомби-процессы (состояние Z в ps). Контейнер использовал Node.js-приложение напрямую как CMD — то есть процесс Node был PID 1. Node не вызывает wait() на неожиданных дочерних процессах из обработчиков сигналов или подпроцессов нативных add-on.
Диагностика:
# Внутри контейнера проверяем зомби
ps aux | awk '$8=="Z"'
# Смотрим родителя зомби (должен быть PID 1)
ps -eo pid,ppid,stat,comm | grep ZВарианты исправления:
# Вариант 1: использовать tini как PID 1 (минимальный init, только жнёт)
# В Dockerfile:
# ENTRYPOINT ["/usr/bin/tini", "--"]
# CMD ["node", "server.js"]
# Вариант 2: встроенный init Docker
docker run --init myimage node server.jsВывод: любой процесс, используемый как PID 1 в контейнере, должен реализовывать пожинание сирот. Настоящий init (systemd, tini, s6) делает это автоматически. Прикладные процессы обычно нет.
▸Почему это работает
macOS использует launchd как PID 1 — это одновременно init-система и менеджер сервисов, эквивалент объединения init + socket activation + супервизия сервисов systemd в одном демоне. SysV init на macOS отсутствует. Обязанность пожинания зомби та же: launchd жнёт сирот. Концептуальная модель (PID 1 как корень дерева процессов, усыновление сирот, супервизия сервисов) универсальна для Unix-подобных систем.
▸Частая ошибка
Распространённое заблуждение: «systemd раздутый и делает слишком много». Практический контраргумент: SysV init делал те же вещи — управление сервисами, обработку событий устройств, запуск getty — но через сотни shell-скриптов без модели зависимостей и логики перезапуска. systemd консолидировал их за единым интерфейсом (systemctl, unit-файлы, journalctl). Нравится тебе этот дизайн или нет — понимание его модели делает тебя быстрее в любой операционной задаче.
Контейнер запускает Python-сервер напрямую как PID 1. Через несколько часов новые запросы зависают, а ps показывает десятки процессов в состоянии Z. Какова наиболее вероятная причина?
PID 1 — корень дерева процессов, выполняемый ядром после того, как initramfs переключается на реальный корень. У него две гарантии ядра: он является родителем всех осиротевших процессов (которых должен пожинать через wait(), иначе накапливаются зомби), и он неуязвим к SIGKILL (его смерть вызвала бы панику ядра). SysV init выполнял нумерованные shell-скрипты последовательно с неявным упорядочением. systemd заменил его явным графом зависимостей, обеспечивающим параллельную активацию — более быструю загрузку без каскадных остановок. В контейнерах любой процесс как PID 1 должен реализовывать пожинание сирот; используй tini или --init, если приложение этого не делает.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.