Создание процессов: 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дешёвым: страницы копируются только при записи.