write(STDOUT_FILENO, "Parent executing\n", 17);
printf("istack=%d\n", istack);
exit(EXIT_SUCCESS);
}
}
procexec/t_vfork.c
Если не брать во внимание случаи, когда скорость является крайне важным фактором, в новых приложениях следует использовать вызов fork() вместо vfork(). Во-первых, в большинстве современных UNIX-систем он реализован с поддержкой копирования при записи, что делает его почти таким же быстрым, и во-вторых, он позволяет избежать необычного поведения, свойственного вызову vfork() и описанного выше. Сравнение производительности вызовов fork() и vfork() будет продемонстрировано в разделе 28.3.
В стандарте SUSv3 вызов vfork() помечен как устаревший, а в SUSv4 он и вовсе не попал. Его спецификация в SUSv3 оставляет многие подробности неопределенными, позволяя реализовать его в виде вызова fork(). В таком случае семантика, принятая в системах BSD, не сохраняется. В некоторых UNIX-системах (в частности, в Linux вплоть до версии ядра 2.0) функция vfork() всего лишь вызывает fork().
Везде, где используется vfork(), сразу после нее обычно должен следовать вызов exec(). Если exec() заканчивается неудачно, дочерний процесс должен завершить работу с помощью функции _exit() (потомок, созданный функцией vfork(), не должен прибегать к вызову exit(), потому что это приведет к сбросу и закрытию буферов stdio; мы рассмотрим этот момент более подробно в разделе 25.4).
Применение вызова vfork() в других ситуациях (в частности, когда его необычная семантика используется для совместного доступа к памяти и планировании работы процесса) с большой долей вероятности сделает программу непереносимой на другие платформы, особенно если там он реализован просто как вызов fork().
После вызова fork() невозможно точно сказать, какой процесс — родительский или дочерний — первым получит доступ к ЦП (в многопроцессорных системах это может произойти одновременно). Приложения, которые явно или неявно полагаются на определенный порядок выполнения для получения корректных результатов, подвержены ошибкам, связанным с
Эту неопределенность можно продемонстрировать на примере программы, представленной в листинге 24.5. Код программы входит в цикл и использует вызов fork() для создания множества потомков. После каждого такого вызова родитель и потомок выводят сообщение, содержащее номер итерации цикла и строку, которая указывает, родительский это процесс или дочерний. Например, если попросить программу создать всего лишь одного потомка, можно получить следующий вывод:
$ ./fork_whos_on_first 1
0 parent
0 child
С помощью этой программы можно создать большое количество потомков и затем проанализировать результат, чтобы увидеть, кто первым выводит свое сообщение на каждом этапе — родитель или потомок. Анализ результатов на системе Linux/x86-32 2.2.19 при 1 миллионе дочерних процессов показал, что родитель выводит свое сообщение первым во всех случаях, кроме 332 (то есть в 99,97 % всех случаев).
Результаты выполнения программы из листинга 24.5 были проанализированы с помощью скрипта procexec/fork_whos_on_first.count.awk, который можно найти в примерах исходного кода, поставляемых вместе с этой книгой.
Из этих результатов можно было бы предположить, что в системе Linux 2.2.19 после вызова fork() выполнение всегда переходит к родительскому процессу. Те 0,03 % случаев, когда потомок выводит свое сообщение первым, возникают из-за того, что родителю иногда не хватает выделенного ему кванта процессорного времени, чтобы вовремя напечатать сообщение. Иными словами, если бы в этом примере мы полагались на то, что после вызова fork() родитель всегда выполняется первым, в целом все бы шло по плану, однако в одном из 3000 случаев мы бы получали неожиданный результат. Естественно, если бы родителю приходилось выполнять больший объем работы, прежде чем передавать выполнение потомку, вероятность неправильного поведения была бы более высокой. Отладка таких ошибок в масштабных программах может оказаться довольно сложной.
Листинг 24.5. Состязание родителя и потомка за возможность вывести сообщение после вызова fork()
procexec/fork_whos_on_first.c
#include
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
int numChildren, j;
pid_t childPid;
if (argc > 1 && strcmp(argv[1], "-help") == 0)
usageErr("%s [num-children]\n", argv[0]);
numChildren = (argc > 1)? getInt(argv[1], GN_GT_0, "num-children"): 1;
setbuf(stdout, NULL); /* Делаем стандартный вывод небуферизированным */
for (j = 0; j < numChildren; j++) {
switch (childPid = fork()) {
case –1:
errExit("fork");
case 0:
printf("%d child\n", j);
_exit(EXIT_SUCCESS);
default:
printf("%d parent\n", j);