Системный вызов fork: что делает ядро

⏱ 35 мин · 🧩 заданий: 6

fork: один вызов — два возврата

#include <unistd.h>

pid_t pid = fork();
if (pid == -1) {
    perror("fork");           /* потомок не создан */
} else if (pid == 0) {
    printf("я потомок, мой pid=%d, родитель=%d\n", getpid(), getppid());
} else {
    printf("я родитель, создал потомка %d\n", pid);
}

fork создаёт почти точную копию вызвавшего процесса. После вызова работают два процесса, оба продолжают с инструкции после fork. Отличаются они только результатом:

Где Результат fork
родитель PID потомка (> 0)
потомок 0
ошибка −1 в родителе, потомка нет

Порядок выполнения родителя и потомка после fork не определён — решает планировщик.

Что делает ядро при fork

Это вопрос 3(а) экзамена. Ответ стоит строить по шагам:

  1. Вход в ядро и проверки. Системный вызов (в Linux fork → clone → kernel_clone → copy_process). Ядро проверяет лимит процессов пользователя (RLIMIT_NPROC), общее ограничение на число процессов, наличие памяти.
  2. Новый дескриптор. Выделяется task_struct и стек ядра для потомка, в них копируется содержимое дескриптора родителя.
  3. Идентификация. Потомку выделяется новый уникальный PID; родителем назначается вызвавший процесс, потомок добавляется в список его детей и в общий список процессов.
  4. Сброс «личного». Обнуляются статистика использования ресурсов и времени, очищается множество ожидающих сигналов, сбрасываются таймеры; блокировки файлов не наследуются.
  5. Копирование ресурсов: - таблица файловых дескрипторов копируется; дескрипторы указывают на те же структуры открытых файлов, их счётчики ссылок увеличиваются (поэтому позиция в файле общая); - копируются обработчики и маска сигналов, текущий и корневой каталоги, учётные данные (UID/GID), маска umask, лимиты; - копируется сеанс и группа процессов.
  6. Память. Копируется описание адресного пространства (список областей, VMA) и таблицы страниц. Сами страницы не копируются: частные страницы, доступные для записи, у родителя и потомка помечаются «только для чтения» (copy-on-write), счётчики ссылок физических кадров увеличиваются. Страницы MAP_SHARED остаются общими и доступными для записи. Так как родительские таблицы изменились, сбрасываются соответствующие записи TLB.
  7. Регистры потомка. Регистровый контекст — копия родительского, но в регистр результата (rax) записывается 0. Потомок «вернётся» из fork в той же точке.
  8. Запуск. Потомок получает состояние «готов» и ставится в очередь планировщика.
  9. Возврат. Родителю возвращается PID потомка.
до fork после fork (copy-on-write) родитель родитель потомок таблица страниц таблица страниц таблица страниц ┌──────────┐ ┌──────────┐ ┌──────────┐ │ стр.1 RW ├──▶ кадр 7 │ стр.1 RO ├──┐ ┌──┤ стр.1 RO │ │ стр.2 RW ├──▶ кадр 9 │ стр.2 RO ├┐ └──┼─▶ кадр 7 │ └──────────┘ └──────────┘└────┼─▶ кадр 9 │ запись в стр.1 → page fault → ядро копирует кадр 7 в новый кадр, ставит RW тому, кто писал

Как отвечать на экзамене

Назовите четыре блока: (1) новый дескриптор и PID; (2) наследование ресурсов — дескрипторы файлов со счётчиками ссылок, сигналы, каталоги; (3) память — копирование таблиц страниц и copy-on-write; (4) два возвращаемых значения и постановка в очередь. Этого достаточно для полного балла.

Copy-on-write подробнее

Без COW fork процесса с 1 ГиБ памяти копировал бы 1 ГиБ — а потом потомок часто сразу вызывает exec и выбрасывает всё. С COW:

  • fork копирует только таблицы страниц;
  • при первой записи в общую страницу возникает страничный отказ защиты;
  • обработчик видит, что это COW-страница: выделяет новый кадр, копирует 4 КиБ, ставит в таблице писавшего процесса новый кадр с правом записи;
  • если страница уже никем не разделяется (счётчик ссылок = 1), ядро просто снимает запрет записи.

Подсчёт процессов

Главное правило: после fork каждый процесс продолжает выполнение со следующей инструкции, со своей копией всех переменных (включая счётчик цикла).

Код Новых процессов
fork(); fork(); 3 (всего 4)
for (i = 0; i < n; i++) fork(); 2ⁿ − 1
for (...) { if (fork() == 0) break; } n (веер: все — дети исходного)
for (...) { if (fork() > 0) exit(0); } n (цепочка, как в задаче 8)
fork() && fork(); 2 (в потомке первого fork результат 0, && не вычисляет второй)

Переключите в симуляторе режим «родитель завершается» — это структура задачи 8: каждый процесс оставляет одного потомка и выходит.

Задача 8

Корневой процесс создаёт одного потомка (назовём его C0) и ждёт его в wait(NULL). C0 в цикле создаёт C1 и завершается, C1 создаёт C2 и завершается… Всего за цикл создаётся 5 процессов C1…C5. Пишут pid/ppid в память C0…C4 (они родители в своей итерации), а counter++ выполняют C1…C5. Подробный разбор — в модуле «Подготовка к экзамену».

Итог

  • fork возвращает потомку 0, родителю — PID потомка, при ошибке −1.
  • Ядро: проверки → новый task_struct и PID → копирование дескрипторов, сигналов, каталогов → копирование таблиц страниц с COW → регистры потомка с результатом 0 → в очередь готовых.
  • Ожидающие сигналы, блокировки и таймеры не наследуются.
  • Число процессов в циклах считаем деревом: у каждого процесса своя копия переменных.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Расставить по порядку

Расставьте действия ядра при системном вызове fork в правильном порядке:

  1. Скопировать описание адресного пространства и таблицы страниц; доступные для записи страницы пометить «только чтение» для копирования при записи
  2. Вернуть родителю PID потомка
  3. Проверить ограничения: лимит числа процессов пользователя, наличие памяти
  4. Выделить новый дескриптор процесса (task_struct) и стек ядра, скопировать в него данные родителя
  5. Подготовить регистры потомка: копия регистров родителя, но результат вызова = 0
  6. Поставить потомка в очередь готовых процессов
  7. Назначить потомку новый уникальный PID, родителем сделать вызывающий процесс
  8. Скопировать таблицу файловых дескрипторов (увеличив счётчики ссылок открытых файлов), обработчики сигналов, текущий каталог
Задание 2 Ввод ответа

Сколько раз будет напечатано hi?

fork();
fork();
fork();
printf("hi\n");
Задание 3 Ввод ответа

Сколько новых процессов создаст цикл?

for (int i = 0; i < 4; i++)
    fork();
Задание 4 Ввод ответа

Сколько новых процессов создаст цикл из задачи 8 (NUM_PROCS = 5)? В потомке (pid == 0) цикл продолжается, а родитель (pid > 0) записывает свои pid/ppid и вызывает exit(0).

for (int i = 0; i < 5; ++i) {
    pid_t pid = fork();
    if (pid == 0) { mem->counter++; }
    if (pid > 0)  { mem->pid = getpid(); mem->ppid = getppid(); exit(0); }
}
Задание 5 Один вариант ответа

Что такое копирование при записи (copy-on-write) при fork?

Задание 6 Заполнить таблицу

Что потомок получает после fork?

Копию / то же значениеСвоё, новое значениеНе наследуется
PID
PPID
Открытые файловые дескрипторы (и общие позиции в файлах)
Обработчики сигналов и маска сигналов
Ожидающие (pending) сигналы
Содержимое памяти (переменные, буферы stdio)
Отображения MAP_SHARED — общая память с родителем
Блокировки файлов fcntl и таймеры alarm
Отметьте ячейку — «+», пустая — «−».

Войдите, чтобы отмечать пройденные уроки.