Жизненный цикл процесса
Каждый процесс Linux рождается через fork() (клон родителя), затем exec() (замена образа). Родитель должен вызвать wait() чтобы подобрать дочерний; дочерний, чей родитель не вызывает wait(), становится зомби. Осиротевшие процессы усыновляет PID 1.
Ты уже использовал ps aux для просмотра процессов и kill для их остановки. Но откуда берётся процесс? Каждый процесс в системе — от sshd до nginx до оболочки — рождается одинаково: ядро клонирует существующий процесс, а затем заменяет его программный образ. Понимание этого двухшагового рождения (fork + exec) объясняет, почему появляются зомби-процессы, почему осиротевшие попадают к PID 1, и что означают буквы состояния в выводе ps. А ещё это превью механизма, на котором контейнеры создают изолированные деревья процессов.
После этого урока ты сможешь объяснить жизненный цикл fork/exec/wait, читать флаги состояния в ps, объяснить что такое зомби и почему он сам по себе не опасен, объяснить что такое осиротевший процесс и куда он попадает, и понять почему PID-пространство имён даёт контейнеру собственный PID 1.
Каждый новый процесс рождается через fork() — клон родителя.
fork() — это системный вызов, создающий точную копию вызывающего процесса. Дочерний процесс получает:
- Новый PID
- Копию памяти родителя через механизм copy-on-write
- Копии всех открытых файловых дескрипторов
- Тот же счётчик инструкций (оба процесса возобновляются после
fork())
fork() возвращается дважды: в родителе — PID дочернего, в дочернем — 0. Так оба процесса знают свою роль.
# Наблюдаем fork в действии: оболочка делает fork для каждой команды
echo $$ # PID текущей оболочки, например 1234
# Оболочка делает fork, затем дочерний exec'ит 'ls'
ls /tmp
# После завершения ls управление возвращается родителю (bash)
# Дочерний процесс больше не существуетКлючевой момент: дочерний не пустой — он клон. Поэтому создание дочернего процесса дёшево: копируются только страницы, которые реально записываются (copy-on-write).
exec() заменяет программный образ дочернего на новый.
После fork() дочерний обычно вызывает один из exec() (execve, execvp и др.). Это заменяет память, код и стек дочернего новой программой. Файловые дескрипторы наследуются, если не помечены close-on-exec.
# strace показывает последовательность fork+exec для любой команды
strace -e trace=execve ls /tmp 2>&1 | head -5
# execve("/usr/bin/ls", ["ls", "/tmp"], 0x... /* env */) = 0
# Ядро заменяет образ процесса на /usr/bin/ls
# Для команды оболочки:
strace -e trace=clone,execve bash -c 'echo hello' 2>&1 | grep -E 'clone|execve'
# clone(...) = <child-pid> -- это fork()
# execve("/usr/bin/bash", [...]) -- дочерний exec'ит bash с -c echo helloОболочка делает это тысячи раз в день. Каждый этап конвейера, каждый подпроцесс, каждая подстановка $(command) — это fork, за которым следует exec.
Состояния процессов: что на самом деле означает колонка STAT в ps.
Каждый процесс в любой момент находится в одном из состояний планировщика:
ps aux
# USER PID %CPU %MEM VSZ RSS TTY STAT ...
# root 1 0.0 0.2 22596 9320 ? Ss ... ← systemd: спит, лидер сессии
# www-data 987 0.1 1.1 123456 44000 ? S ... ← nginx worker: прерываемый сон
# root 123 0.0 0.0 0 0 ? D ... ← непрерываемое ожидание I/O
# root 3456 0.0 0.0 0 0 ? Z ... ← зомби: завершился, не подобран
# Значения букв STAT:
# R Running — выполняется или готов к выполнению
# S Interruptible sleep — ждёт события; может быть разбужен сигналом
# D Uninterruptible sleep — ждёт I/O; НЕЛЬЗЯ убить, даже SIGKILL
# Z Zombie — завершился, но родитель ещё не вызвал wait()
# T Stopped — остановлен (SIGSTOP, Ctrl-Z, или трассируется отладчиком)
# + Процесс переднего плана
# s Лидер сессии
# l МногопоточныйСостояние D удивляет операторов: процесс в D-состоянии не реагирует на kill -9. Он находится внутри операции ядра (обычно I/O) и ядро не прерывает его. Если процесс застрял в D на минуты — проблема в I/O-подсистеме (зависший NFS-mount, умирающий диск), а не в самом процессе.
wait(): родитель должен подобрать дочернего, иначе тот станет зомби.
Когда процесс завершается, ядро сохраняет небольшую запись (код выхода, PID, использование ресурсов) до тех пор, пока родитель не вызовет wait() или waitpid(). Дочерний, который завершился, но не подобран — это зомби (состояние Z).
# Зомби в ps выглядят так:
# 1234 Z+ 0:00 [defunct]
# Сколько зомби у тебя сейчас?
ps aux | awk '$8 ~ /Z/ { print $0 }'
# Зомби сами по себе НЕ опасны:
# Они занимают PID-слот и небольшую структуру в ядре — ничего больше.
# Никакого CPU, памяти, файловых дескрипторов.
# Зомби становятся проблемой при их тысячах:
# PID — конечный ресурс (cat /proc/sys/kernel/pid_max)
# Родитель с багом, который никогда не вызывает wait(), накапливает зомби.
# Исправление: починить родителя (или перезапустить), не убивать зомби.
# Найти родителя зомби:
ps -o ppid= -p 1234 # выводит PID родителяОсиротевшие процессы и PID-пространства имён.
Если родитель завершился, пока его дети ещё работают, дети становятся осиротевшими. Ядро немедленно усыновляет их PID 1 (systemd). PID 1 непрерывно вызывает wait() и сразу подбирает осиротевших зомби.
# Демонстрация усыновления:
bash -c 'sleep 300 & echo "child PID: $!"' &
# Убиваем родительский bash — sleep продолжает работать
sleep 1
pstree -p | grep sleep
# sleep теперь дочерний PID 1 (systemd), а не исходного bash
# Превью PID-пространства имён (мост к контейнерам):
# Контейнер — это (среди прочего) новое PID-пространство имён.
# Внутри него процессы видят PID начиная с 1.
# Первый процесс в пространстве становится его PID 1.
# Если он завершится не подобрав детей — зомби накапливаются.
# Поэтому PID 1 контейнера должен обрабатывать сигналы и вызывать wait() —
# или использовать tiny init.
unshare --pid --fork --mount-proc bash
# Внутри: ps показывает PID 1 = bash, PID 2 = psДиагностика сервиса, накапливающего зомби-детей.
Ситуация: у Node.js-сервера десятки [defunct] процессов в ps aux. Количество PID растёт. Полный диагностический поток:
# Шаг 1: подтвердить зомби и найти виновного родителя
ps aux | awk '$8 ~ /Z/'
# node 4201 0.0 0.0 0 0 ? Z [node]
# node 4202 0.0 0.0 0 0 ? Z [node]
# ... (десятки)
# Получить родителя зомби PID 4201:
ps -o ppid= -p 4201
# 3890
# Шаг 2: что такое PID 3890?
ps -p 3890 -o pid,comm,args
# 3890 node node /app/server.js
# Шаг 3: варианты решения:
# а) Починить приложение: обеспечить обработку события 'exit'/'close'
# для каждого дочернего процесса в child_process Node.js
# б) Перезапустить процесс Node: все его зомби-дети будут усыновлены
# PID 1 и немедленно подобраны
# в) Для стороннего сервиса: открыть баг-репорт.
# Накопление зомби — всегда баг родителя, не самих зомби.
# Шаг 4: проверить запас PID
cat /proc/sys/kernel/pid_max # обычно 4194304
ps aux | wc -l # текущее количество процессов▸Почему это работает
На macOS модель fork/exec та же (это POSIX), но ps использует BSD-флаги. Колонка STAT показывает Z для зомби одинаково. Важное отличие macOS: launchd играет роль PID 1 и сборщика зомби. Псевдофайловой системы /proc нет — информация о процессах доступна через вызовы sysctl() и инструменты lsof, dtrace, Activity Monitor. Концепции fork/exec/wait, зомби и осиротевших — универсальны; различается только инструментарий.
▸Частая ошибка
kill -9 зомби не делает ничего. Зомби уже мёртв — это просто запись в ядре, ожидающая wait(). SIGKILL адресован живому процессу. У зомби нет работающего кода, нет обработчика сигналов, некому получить сигнал. Единственное, что может убрать зомби — это родитель (вызвав wait()) или PID 1 (если родитель умрёт и осиротит зомби, а PID 1 подберёт их). Хочешь убрать зомби немедленно — убей родителя, а не зомби.
Процесс показывает состояние 'D' в колонке STAT ps aux уже 10 минут. Что это означает и остановит ли его kill -9?
Каждый процесс Linux рождается через fork() (ядро клонирует родителя) с последующим exec() (дочерний заменяет свой программный образ). Состояния в ps: R (выполняется), S (прерываемый сон), D (непрерываемый I/O-сон — иммунитет к SIGKILL), Z (зомби — завершился, не подобран), T (остановлен). Зомби — уже завершившийся дочерний, чей родитель ещё не вызвал wait(); зомби занимают PID-слот, но не CPU и память, и их нельзя убить, ведь они уже мертвы. Осиротевший — работающий дочерний, чей родитель завершился; ядро немедленно усыновляет осиротевших PID 1, который непрерывно их подбирает. PID-пространства имён расширяют эту модель: первый процесс контейнера становится PID 1 внутри своего пространства и отвечает за подбор детей — вот почему контейнеры нуждаются в правильном init.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.