Системный вызов fork: что делает ядро
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(а) экзамена. Ответ стоит строить по шагам:
- Вход в ядро и проверки. Системный вызов (в Linux
fork→clone→kernel_clone→copy_process). Ядро проверяет лимит процессов пользователя (RLIMIT_NPROC), общее ограничение на число процессов, наличие памяти. - Новый дескриптор. Выделяется
task_structи стек ядра для потомка, в них копируется содержимое дескриптора родителя. - Идентификация. Потомку выделяется новый уникальный PID; родителем назначается вызвавший процесс, потомок добавляется в список его детей и в общий список процессов.
- Сброс «личного». Обнуляются статистика использования ресурсов и времени, очищается множество ожидающих сигналов, сбрасываются таймеры; блокировки файлов не наследуются.
- Копирование ресурсов:
- таблица файловых дескрипторов копируется; дескрипторы указывают на те же структуры открытых файлов, их счётчики ссылок увеличиваются (поэтому позиция в файле общая);
- копируются обработчики и маска сигналов, текущий и корневой каталоги, учётные данные (UID/GID), маска
umask, лимиты; - копируется сеанс и группа процессов. - Память. Копируется описание адресного пространства (список областей, VMA) и таблицы страниц. Сами страницы не копируются: частные страницы, доступные для записи, у родителя и потомка помечаются «только для чтения» (copy-on-write), счётчики ссылок физических кадров увеличиваются. Страницы
MAP_SHAREDостаются общими и доступными для записи. Так как родительские таблицы изменились, сбрасываются соответствующие записи TLB. - Регистры потомка. Регистровый контекст — копия родительского, но в регистр результата (
rax) записывается 0. Потомок «вернётся» изforkв той же точке. - Запуск. Потомок получает состояние «готов» и ставится в очередь планировщика.
- Возврат. Родителю возвращается PID потомка.
Как отвечать на экзамене
Назовите четыре блока: (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
Расставьте действия ядра при системном вызове fork в правильном порядке:
- Скопировать описание адресного пространства и таблицы страниц; доступные для записи страницы пометить «только чтение» для копирования при записи
- Вернуть родителю PID потомка
- Проверить ограничения: лимит числа процессов пользователя, наличие памяти
- Выделить новый дескриптор процесса (task_struct) и стек ядра, скопировать в него данные родителя
- Подготовить регистры потомка: копия регистров родителя, но результат вызова = 0
- Поставить потомка в очередь готовых процессов
- Назначить потомку новый уникальный PID, родителем сделать вызывающий процесс
- Скопировать таблицу файловых дескрипторов (увеличив счётчики ссылок открытых файлов), обработчики сигналов, текущий каталог
Сколько раз будет напечатано hi?
fork();
fork();
fork();
printf("hi\n");
Сколько новых процессов создаст цикл?
for (int i = 0; i < 4; i++)
fork();
Сколько новых процессов создаст цикл из задачи 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); }
}
Что такое копирование при записи (copy-on-write) при fork?
Что потомок получает после fork?
| Копию / то же значение | Своё, новое значение | Не наследуется | |
|---|---|---|---|
| PID | |||
| PPID | |||
| Открытые файловые дескрипторы (и общие позиции в файлах) | |||
| Обработчики сигналов и маска сигналов | |||
| Ожидающие (pending) сигналы | |||
| Содержимое памяти (переменные, буферы stdio) | |||
| Отображения MAP_SHARED — общая память с родителем | |||
| Блокировки файлов fcntl и таймеры alarm |
Войдите, чтобы отмечать пройденные уроки.