В чем разница между системными вызовами fork() и vfork() в Linux?

«В чем разница между системными вызовами fork() и vfork() в Linux?» — вопрос из категории Linux, который задают на 23% собеседований Devops Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Оба вызова используются для создания нового процесса, но с фундаментально разной семантикой и оптимизациями.

fork() — создает почти полную копию родительского процесса (копирует адресное пространство, таблицу файловых дескрипторов и т.д.). Для оптимизации используется механизм Copy-On-Write (COW): физические страницы памяти дублируются только при попытке записи в них. Это стандартный и безопасный способ создания процессов.

vfork() — создает новый процесс, но не копирует адресное пространство родителя. Дочерний процесс выполняется в том же адресном пространстве, что и родитель, и родительский процесс приостанавливается, пока дочерний не вызовет exec() или _exit(). Это историческая оптимизация для систем, где fork() с полным копированием был очень дорогим.

Ключевые различия:

Аспект fork() vfork()
Адресное пространство Копируется (с COW). Разделяется с родителем.
Выполнение родителя Продолжается параллельно с дочерним. Блокируется до завершения дочернего.
Безопасность Безопасен. Изменения в памяти одного процесса не влияют на другой. Опасен. Запись в память дочерним процессом может повредить состояние родителя.
Производительность Быстрее, чем полное копирование (благодаря COW), но все равно имеет накладные расходы. Чрезвычайно быстр, так как не требует копирования таблиц страниц.
Типичное применение Общий случай создания процессов (например, в веб-серверах, демонах). Строго для сценария vfork()exec(), когда нужно максимально быстро запустить новую программу.

Пример кода, демонстрирующий правильное использование vfork():

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main() {
    pid_t pid = vfork(); // Используем vfork

    if (pid == 0) {
        // Дочерний процесс: Родитель заблокирован.
        // НИКОГДА не модифицируйте глобальные/локальные переменные здесь!
        // Сразу выполняем новую программу.
        execlp("/bin/ls", "ls", "-l", NULL);

        // Если execlp fails, нужно использовать _exit(), а не exit()!
        // exit() сбрасывает буферы stdio и может сломать состояние родителя.
        _exit(EXIT_FAILURE);
    } else if (pid > 0) {
        // Родительский процесс: продолжает работу только после exec/_exit в дочернем.
        printf("Parent process resumed. Child PID: %dn", pid);
    } else {
        perror("vfork failed");
    }
    return 0;
}

Современный контекст: В современных ядрах Linux fork() с COW настолько оптимизирован, что vfork() практически утратил свое преимущество в скорости для большинства задач. Фактически, во многих реализациях vfork() — это просто fork() с гарантией, что родитель будет заблокирован. Я использую fork() для всех стандартных случаев, а vfork() рассматриваю как устаревшую, потенциально опасную конструкцию, которую стоит применять только в очень специфичных embedded-системах или при написании низкоуровневого системного ПО, где каждый цикл процессора на счету.