Создание процессов: fork и exec

Как из одного процесса появляется другой — на примере классической модели Unix fork/exec.

fork создаёт новый процесс — почти точную копию текущего; exec заменяет код процесса другой программой. Вместе они — классический способ запуска программ в Unix.

Дерево процессов

Процессы не возникают из ниоткуда: каждый процесс кем-то порождён. Так образуется дерево, где у каждого процесса есть родитель. На вершине — первый процесс системы (в Linux это init или systemd, PID 1). Когда вы запускаете команду в терминале, оболочка порождает дочерний процесс.

fork: «раздвоение» процесса

Системный вызов fork() создаёт копию текущего процесса. После него существуют два почти одинаковых процесса — родитель и потомок. Хитрость в том, что fork возвращает два разных значения:

  • в процессе-родителе — PID потомка (число больше нуля);
  • в процессе-потомке — ноль.

По этому значению код понимает, в каком из двух процессов он сейчас выполняется.

pid_t pid = fork();

if (pid > 0) {
    // здесь выполняется РОДИТЕЛЬ (pid = PID потомка)
} else if (pid == 0) {
    // здесь выполняется ПОТОМОК
} else {
    // fork не удался (вернул -1)
}

exec: «стать другой программой»

Часто потомок сразу заменяет себя другой программой через exec. Этот вызов не создаёт новый процесс — он берёт текущий и загружает в него новый код. Старый код исчезает. Поэтому типичный запуск программы — это fork (создать потомка), а в потомке exec (стать нужной программой):

pid_t pid = fork();
if (pid == 0) {
    // потомок превращается в команду ls
    execlp("ls", "ls", "-l", NULL);
    // сюда код не дойдёт, если exec удался
}
// родитель ждёт потомка
wait(NULL);

Copy-on-write: умное копирование

Копировать всю память при каждом fork было бы расточительно, особенно если потомок тут же сделает exec и выбросит копию. Поэтому используют copy-on-write (COW): сразу после fork родитель и потомок делят одни и те же страницы памяти. Реальная копия страницы создаётся только тогда, когда кто-то из них в неё пишет. Если потомок сделал exec и ничего не записал — копирования вообще не было. Это сильно ускоряет fork.

Симуляция дерева процессов

Смоделируем порождение процессов и посчитаем глубину дерева и общее число процессов после серии fork.

next_pid = 1
processes = {}  # pid -> parent_pid

def fork(parent):
    global next_pid
    next_pid += 1
    processes[next_pid] = parent
    return next_pid

# init
processes[1] = None
shell = fork(1)        # оболочка
p_ls = fork(shell)     # ls
p_grep = fork(shell)   # grep
p_child = fork(p_grep) # потомок grep

print("Дерево (pid -> родитель):")
for pid, parent in processes.items():
    print(f"  PID {pid} <- родитель {parent}")

def depth(pid):
    d = 0
    while processes[pid] is not None:
        pid = processes[pid]
        d += 1
    return d

print(f"Всего процессов: {len(processes)}")
print(f"Глубина процесса PID {p_child}: {depth(p_child)}")

Вывод:

Дерево (pid -> родитель):
  PID 1 <- родитель None
  PID 2 <- родитель 1
  PID 3 <- родитель 2
  PID 4 <- родитель 2
  PID 5 <- родитель 4
Всего процессов: 5
Глубина процесса PID 5: 3

Команды для наблюдения

ps -ef        # список процессов с PID и PPID (родителем)
pstree        # дерево процессов наглядно
ps aux        # процессы с использованием CPU и памяти

Итог

  • Процессы образуют дерево; у каждого есть родитель, на вершине — init/systemd.
  • fork создаёт копию процесса и возвращает разные значения родителю и потомку.
  • exec заменяет код текущего процесса другой программой.
  • Типичный запуск программы — fork, затем exec в потомке.
  • Copy-on-write делает fork дешёвым: страницы копируются только при записи.
Проверьте себя
1. Что возвращает fork() в процессе-потомке?
APID родителя
BНоль
CPID потомка
D-1
2. Что делает системный вызов exec?
AСоздаёт новый процесс-копию
BЗаменяет код текущего процесса другой программой
CЗавершает процесс
DСоздаёт новый поток
3. Зачем используется copy-on-write при fork?
AЧтобы потомок не видел память родителя
BЧтобы не копировать всю память сразу — страницы дублируются только при записи
CЧтобы ускорить exec за счёт шифрования
DЧтобы потомок и родитель всегда оставались полностью раздельными
Поддержать проект