Ранние реализации BSD входили в число операционных систем, в которых вызов fork() выполнял полноценное дублирование данных, кучи и стека родителя. Как уже было замечено выше, это довольно расточительно, особенно если за вызовом fork() следует функция exec(). В связи с этим в более поздних версиях BSD-систем появился системный вызов vfork(), который отличается гораздо большей эффективностью, хотя и имеет немного другую (на самом деле довольно странную) семантику. В современных системах UNIX вызов fork() выполняет копирование при записи, что значительно более экономно по сравнению со старыми реализациями; благодаря этому необходимость в функции vfork() во многом отпала. Тем не менее в случае, если вам нужна самая быстрая операция создания дочерних процессов, операционная система Linux (как и многие другие реализации UNIX) тоже поддерживает вызов vfork() с семантикой, принятой в BSD-системах. Но, поскольку эта необычная семантика может привести к неочевидным программным ошибкам, vfork() рекомендуется избегать; исключение составляют те редкие случаи, когда он обеспечивает существенный прирост производительности.
Как и fork(), вызов vfork() применяется вызывающим процессом для создания нового потомка. Однако он специально был спроектирован так, чтобы его можно было использовать в программах, в которых дочерний процесс сразу же делает вызов exec().
#include
pid_t vfork(void);
В родителе: возвращает идентификатор потомка при успешном завершении или –1 при ошибке; в успешно созданном потомке всегда возвращает 0
Функция vfork() имеет два отличия от системного вызова fork(), которые делают ее более эффективной.
• Страницы виртуальной памяти или таблицы страниц не дублируются для дочернего процесса. Вместо этого потомок и родитель делят память до тех пор, пока один из них не выполнит успешный вызов exec() или _exit(), чтобы завершить работу.
• Выполнение родительского процесса приостанавливается до тех пор, пока потомок не вызовет exec() или _exit().
Эти два свойства имеют важные последствия. Поскольку потомок использует память родителя, любые изменения, вносимые им в сегменты данных, кучи или стека, будут доступны самому родительскому процессу, как только он возобновит работу. Более того, выполнение потомком функции между вызовами vfork() и exec() (или _exit()) тоже коснется родителя. Это похоже на пример, описанный в разделе 6.8, когда вызов longjmp() выполняет переход внутрь функции, которая уже вернула управление (завершилась). Чаще всего это приводит к ошибке сегментации (SIGSEGV).
Есть несколько вещей, которые потомок может делать между вызовами vfork() и exec(), не затрагивая при этом своего родителя. К таковым относится операция открытия дескриптора файла (не путать с потоками stdio (библиотеки)). Поскольку таблица файловых дескрипторов каждого процесса хранится в пространстве ядра (см. раздел 5.4) и дублируется во время вызова vfork(), родитель не будет знать об операциях с дескрипторами файлов, выполняемых потомком.
SUSv3 гласит, что поведение программы является неопределенным, если она: а) изменяет любые данные, кроме переменной типа pid_t, применяемой для хранения значения, возвращаемого вызовом vfork(); б) возвращается из функции, в которой произошел вызов vfork(); в) вызывает любую другую функцию до успешного выполнения _exit() или exec().
При рассмотрении системного вызова clone() в разделе 28.2 мы увидим, что потомок, созданный с помощью функций fork() или vfork(), получает собственные копии некоторых других атрибутов процесса.
Семантика функции vfork() указывает на то, что после ее вызова потомок гарантированно получит ресурсы центрального процессора раньше родителя. В разделе 24.2 отмечалось, что вызов fork() не дает такой гарантии, поэтому после него выполнение может перейти как к родительскому, так и к дочернему процессу.
В листинге 24.4 продемонстрированы обе семантические особенности, которые отличают вызов vfork() от fork(): потомок получает доступ к памяти родителя, а родитель приостанавливает работу, пока потомок не завершит выполнение или не вызовет функцию exec(). Запустив эту программу, мы увидим следующий вывод:
$ ./t_vfork
Child executing
Parent executing
istack=666
Последняя строчка вывода показывает изменение, внесенное потомком в переменную istack, которая принадлежит родителю.
Листинг 24.4. Использование вызова vfork()
procexec/t_vfork.c
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int istack = 222;
switch (vfork()) {
case –1:
errExit("vfork");
case 0: /* Первым выполняется потомок (в пространстве памяти родителя) */
sleep(3); /* Даже если процесс остановится на некоторое время,
родитель все равно не продолжит работу */
write(STDOUT_FILENO, "Child executing\n", 16);
istack *= 3; /* Это изменение будет доступно родителю */
_exit(EXIT_SUCCESS);
default: /* Родитель блокируется, пока существует его потомок */