← Linux: процессы вглубь
12 мин · просто · Урок 1 из 7

Жизненный цикл: fork, exec, exit

Как рождается процесс: fork, exec, exit, дерево процессов.

Как рождается процесс

Процесс не собирается с нуля. Ядро создаёт новый процесс, копируя уже существующий. Системный вызов fork делает дубликат вызвавшего процесса: у копии новый PID, но тот же образ памяти, те же открытые файлы и та же текущая директория. После fork их два, и оба продолжают выполняться со следующей строки после вызова.

Отличить родителя от потомка просто: fork возвращает разное значение. Родителю приходит PID потомка, потомку ноль.

pid = fork();          # родитель и потомок оба тут
# у родителя pid = 4210 (PID потомка)
# у потомка  pid = 0

exec заменяет образ

Дубликат родителя редко нужен сам по себе. Обычно потомок хочет запустить другую программу. Для этого он вызывает exec — семейство системных вызовов, которое заменяет образ текущего процесса на новую программу. PID при этом не меняется, меняется содержимое: код, данные, стек.

# внутри потомка после fork
exec("/bin/ls", ...);  # процесс перестаёт быть копией shell
                       # и становится ls, PID тот же

Отсюда модель fork+exec. Shell форкается, а в потомке делает exec нужной команды. Родительский shell при этом остаётся жив и ждёт, пока потомок отработает.

bash PID 3187 fork bash ждёт wait, PID 3187 копия bash PID 4210, fork вернул 0 exec ls PID тот же, образ новый exit 0 $? = 0 код возврата прочитан
PID у потомка новый, а при exec он не меняется: меняется только содержимое процесса.
$ ls        # bash форкается, потомок делает exec("ls"), bash ждёт
file1  file2
$           # потомок завершился, управление вернулось в bash

PID, PPID и дерево

У процесса два номера, которые задают его место в системе. PID — уникальный номер самого процесса. PPID — PID его родителя. По этим связям процессы выстраиваются в дерево: терминал породил shell, shell породил команду.

$ echo $$      # PID текущего shell
3187
$ bash         # запускаем вложенный shell
$ echo $$      # у него свой PID
4055
$ echo $PPID   # а его родитель — прежний shell
3187

Дерево целиком показывает pstree. В корне сидит PID 1 (обычно systemd), от него ветвятся все остальные.

$ pstree -p
systemd(1)---sshd(880)---sshd(3140)---bash(3187)---pstree(4102)

exit и код возврата

Процесс завершается вызовом exit с целым числом, которое называют кодом возврата. Ноль означает успех, любое ненулевое значение указывает на ошибку. Родитель читает этот код и решает, что делать дальше.

В shell код последней команды лежит в переменной $?.

$ ls /etc/hostname
/etc/hostname
$ echo $?      # успех
0
$ ls /нет-такого
ls: cannot access '/нет-такого': No such file or directory
$ echo $?      # ошибка
2

Сигналы завершения и осиротевшие процессы

Завершить процесс можно и сигналом. Два сигнала процесс не в силах ни перехватить, ни заблокировать, ни проигнорировать: SIGKILL и SIGSTOP. Это единственная такая пара, для всех остальных обработчик поставить можно.

У каждого сигнала есть действие по умолчанию, всего их пять: завершить процесс (Term), проигнорировать (Ign), завершить со снимком памяти для отладки (Core), остановить (Stop), продолжить остановленный (Cont). SIGCHLD, которым ядро сообщает родителю о смерти потомка, по умолчанию попадает в Ign.

Когда потомок завершился, а родитель ещё не прочитал его код через wait(), остаётся зомби. Он держит запись в таблице процессов, то есть занятый номер PID, но не память. Как только родитель делает wait(), запись убирается.

$ ps aux | grep defunct
deploy 4310 0.0 0.0 0 0 Z 10:20 0:00 [worker] <defunct>   # зомби

Если родитель умирает раньше потомка, потомок становится осиротевшим. Такого сироту усыновляет процесс с PID 1 (обычно init/systemd), и он же вызывает wait(), забирая код возврата. С ядра 3.4 роль усыновителя можно назначить и обычному процессу через subreaper, тогда сироты уходят к нему, а не к PID 1.

Ловушки

Что важно запомнить

fork делает копию родителя: новый PID, тот же образ памяти и файлы.

exec заменяет образ на другую программу, PID не меняется. Отсюда модель fork+exec: shell форкается, потомок делает exec.

PID — номер процесса, PPID — номер родителя. pstree рисует дерево от PID 1.

exit отдаёт код возврата: 0 — успех, ненулевой — ошибка. Смотри $?.

SIGKILL и SIGSTOP нельзя перехватить или проигнорировать. Зомби держит номер PID, сироту усыновляет PID 1.

В следующем уроке — состояния процесса и планировщик: что значат R, S, D и как ядро раздаёт им процессор.

Дрилл · вопросы как на собесе

Проверь себя без подсказок

Сформулируй ответ вслух или письменно, потом раскрой и честно отметь: знал или нет.

01

В чём разница между fork и exec? Что копируется, а что заменяется?

LIN-132
02

Что такое SIGCHLD? В какой ситуации процесс может его получить?

LIN-044

Прошли материал до конца? Отметьте урок пройденным.