Во многих приложениях создание нескольких процессов может быть удобным способом разделения задач. Например, сетевой сервер может следить за входящими клиентскими запросами, обрабатывая каждый из них в отдельном дочернем процессе; при этом процесс сервера не прекращает отслеживать дальнейшие подключения со стороны клиента. Разделение задач таким способом упрощает проектирование приложений. Это также улучшает параллелизм (то есть одновременно можно обрабатывать больше задач или запросов).
Системный вызов fork() создает новый процесс
#include
pid_t fork(void);
В родителе: возвращает идентификатор потомка при успешном завершении или –1 при ошибке; в успешно созданном потомке всегда возвращает 0.
Ключевым моментом в понимании вызова fork() является тот факт, что после завершения его работы мы получаем два процесса, каждый из которых продолжает выполнение с момента возврата из этого вызова.
Рис. 24.1.
Оба процесса выполняют один и тот же программный код, но обладают разными копиями сегментов стека, данных и кучи. Сегменты потомка вначале полностью дублируют соответствующие части памяти своего родителя. Но после завершения вызова fork() каждый из процессов может самостоятельно изменять переменные в своих сегментах, не влияя на другой процесс.
Распознавать процессы внутри кода программы можно с помощью значения, возвращенного функцией fork(). В случае с родителем это значение равно идентификатору только что созданного потомка. Это полезно, поскольку родитель может создать несколько дочерних процессов, которые ему придется отслеживать (с помощью вызова wait() или одной из его разновидностей). В случае с потомком возвращается 0. При необходимости дочерний процесс может получить свой собственный идентификатор или идентификатор своего родителя, используя функции getpid() и соответственно getppid().
Если новый процесс не удается создать, вызов fork() возвращает –1. Одной из причин этого может быть превышение пользователем или всей системой в целом ограничения на определенный вид ресурсов (RLIMIT_NPROC, описанное в разделе 36.3), а именно на количество создаваемых процессов.
Иногда вызов fork() используют следующим образом:
pid_t childPid; /* Используется в родителе после успешного вызова fork()
для записи идентификатора потомка */
switch (childPid = fork()) {
case –1: /* Вызов fork() завершился неудачей */
/* Обработка ошибки */
case 0: /* Ветка для потомка после успешного вызова fork() */
/* Выполнение действий, связанных с потомком */
default: /* Ветка для родителя, после успешного вызова fork() */
/* Выполнение действий, связанных с родителем */
}
Следует понимать, что после вызова fork() невозможно сказать, какой из двух процессов первым получит от планировщика ресурсы ЦП. В плохо написанных программах такая неопределенность может привести к ошибке, известной под названием
Использование функции fork() продемонстрировано в листинге 24.1. Описанная в нем программа создает дочерний процесс, который изменяет унаследованные им в результате вызова fork() глобальные и автоматические переменные.
С помощью функции sleep() (в нашем коде она вызывается родителем) программа позволяет потомку получить ресурсы процессора раньше родителя; благодаря этому потомок успевает выполнить свою работу и завершиться до того, как выполнение продолжит родительский процесс. Такое использование вызова sleep() не дает никаких гарантий; более надежный подход будет рассмотрен в разделе 24.5.
Запустив программу, описанную в листинге 24.1, вы увидите следующий вывод:
$ ./t_fork
PID=28557 (child) idata=333 istack=666
PID=28556 (parent) idata=111 istack=222
Вышеприведенный вывод показывает, что во время вызова fork() дочерний процесс получает собственную копию сегментов стека и данных и может изменять переменные в этих сегментах, не влияя на своего родителя.
Листинг 24.1. Использование функции fork()
procexec/t_fork.c
#include "tlpi_hdr.h"
static int idata = 111; /* Выделена в сегменте данных */
int
main(int argc, char *argv[])
{
int istack = 222; /* Выделена в сегменте стека */
pid_t childPid;
switch (childPid = fork()) {
case –1:
errExit("fork");
case 0:
idata *= 3;
istack *= 3;
break;
default:
sleep(3); /* Даем потомку шанс выполниться */
break;
}
/* Здесь выполняются и потомок и родитель */
printf("PID=%ld %s idata=%d istack=%d\n", (long) getpid(),
(childPid == 0)? "(child) ": "(parent)", idata, istack);
exit(EXIT_SUCCESS);
}
procexec/t_fork.c
24.2.1. Совместный доступ к файлу родителя и потомка