30.6. Сервер TCP с предварительным порождением процессов без блокировки для вызова accept

30.6. Сервер TCP с предварительным порождением процессов без блокировки для вызова accept

В первом из рассматриваемых нами «усовершенствованных» серверов используется технология, называемая предварительным созданием процессов (preforking). Вместо того чтобы вызывать функцию fork каждый раз при поступлении очередного запроса от клиента, сервер создает при запуске некоторое количество дочерних процессов, и впоследствии они обслуживают клиентские запросы по мере установления соединений с клиентами. На рис. 30.1 показан сценарий, при котором родитель предварительно создал N дочерних процессов, и в настоящий момент имеется два соединения с клиентами.

Рис. 30.1. Предварительное создание дочерних процессов сервером

Преимущество этой технологии заключается в том, что обслуживание нового клиента не требует вызова функции fork родительским процессом, тем самым стоимость этого обслуживания понижается. Недостатком же является необходимость угадать, сколько дочерних процессов нужно создать при запуске. Если в некоторый момент времени количество имеющихся дочерних процессов будет равно количеству обслуживаемых клиентов, то дополнительные клиентские запросы будут игнорироваться до того момента, когда освободится какой-либо дочерний процесс. Но, как сказано в разделе 4.5, клиентские запросы в такой ситуации игнорируются не полностью. Для каждого из этих дополнительных клиентов ядро выполнит трехэтапное рукопожатие (при этом общее количество соединений не может превышать значения аргумента backlog функции listen), и при вызове функции accept установленные соединения будут переданы серверу. При этом, однако, приложение-клиент может заметить некоторое ухудшение в скорости получения ответа, так как, хотя функция connect может быть выполнена сразу же, запрос может не поступать на обработку еще некоторое время.

За счет некоторого дополнительного усложнения кода всегда можно добиться того, что сервер справится со всеми клиентскими запросами. От родительского процесса требуется постоянно отслеживать количество свободных дочерних процессов, и если это количество падает ниже некоторого минимального предела, родитель должен вызвать функцию fork и создать недостающее количество дочерних процессов. Аналогично, если количество свободных дочерних процессов превосходит некоторую максимальную величину, некоторые из этих процессов могут быть завершены родителем, так как излишнее количество свободных дочерних процессов тоже отрицательно влияет на производительность (об этом мы поговорим чуть позже).

Но прежде чем углубляться в детали, исследуем основную структуру этого типа сервера. В листинге 30.6 показана функция main для первой версии нашего сервера с предварительным порождением дочерних процессов.

Листинг 30.6. Функция main сервера с предварительным порождением дочерних процессов

//server/serv02.c

 1 #include "unp.h"

 2 static int nchildren;

 3 static pid_t *pids;

 4 int

 5 main(int argc, char **argv)

 6 {

 7  int listenfd, i;

 8  socklen_t addrlen;

 9  void sig_int(int);

10  pid_t child_make(int, int, int);

11  if (argc == 3)

12   listenfd = Tcp_listen(NULL, argv[1], &addrlen);

13  else if (argc == 4)

14   listenfd = Tcp_listen(argv[1], argv[2], &addrlen);

15  else

16   err_quit("usage: serv02 [ <host> ] <port#> <#children>");

17  nchildren = atoi(argv[argc - 1]);

18  pids = Calloc(nchildren, sizeof(pid_t));

19  for (i = 0; i < nchildren; i++)

20   pids[i] = child_make(i, listenfd, addrlen); /* возвращение родительского процесса */

21  Signal (SIGINT, sig_int);

22  for (;;)

23   pause(); /* дочерние процессы завершились */

24 }

11-18 Дополнительный аргумент командной строки указывает, сколько требуется создать дочерних процессов. В памяти выделяется место для размещения массива, в который записываются идентификаторы дочерних процессов, используемые функцией main при окончании работы программы для завершения этих процессов.

19-20 Каждый дочерний процесс создается функцией child_make, которую мы показываем в листинге 30.8.

Код обработчика сигнала SIGINT, представленный в листинге 30.7, отличается от кода, приведенного в листинге 30.3.

Листинг 30.7. Обработчик сигнала SIGINT

//server/serv02.c

25 void

26 sig_int(int signo)

27 {

28  int i;

29  void pr_cpu_time(void);

30  /* завершаем все дочерние процессы */

31  for (i = 0; i < nchildren; i++)

32   kill(pids[i], SIGTERM);

33  while (wait(NULL) > 0) /* ждем завершения всех дочерних процессов */

34   ;

35  if (errno != ECHILD)

36   err_sys("wait error");

37  pr_cpu_time();

38  exit(0);

39 }

30-34 Функция getrusage сообщает об использовании ресурсов всеми дочерними процессами, завершившими свое выполнение, поэтому мы должны завершить все дочерние процессы к моменту вызова функции pr_cpu_time. Для этого дочерним процессам посылается сигнал SIGTERM, после чего мы вызываем функцию wait и ждем завершения выполнения дочерних процессов.

В листинге 30.8 показана функция child_make, вызываемая из функции main для порождения очередного дочернего процесса.

Листинг 30.8. Функция child_make: создание очередного дочернего процесса

//server/child02.c

 1 #include "unp.h"

 2 pid_t

 3 child_make(int i, int listenfd, int addrlen)

 4 {

 5 pid_t pid;

 6 void child_main(int, int, int);

 7 if ( (pid = Fork()) > 0)

 8 return (pid); /* родительский процесс */

 9 child_main(i, listenfd, addrlen); /* никогда не завершается */

10 }

7-9 Функция fork создает очередной дочерний процесс и возвращает родителю идентификатор дочернего процесса. Дочерний процесс вызывает функцию child_main, показанную в листинге 30.9, которая представляет собой бесконечный цикл.

Листинг 30.9. Функция child_main: бесконечный цикл, выполняемый каждым дочерним процессом

//server/child02.c

11 void

12 child_main(int i, int listenfd, int addrlen)

13 {

14  int connfd;

15  void web_child(int);

16  socklen_t clilen;

17  struct sockaddr *cliaddr;

18  cliaddr = Malloc(addrlen);

19  printf("child %ld starting ", (long)getpid());

20  for (;;) {

21   clilen = addrlen;

22   connfd = Accept(listenfd, cliaddr, &clilen);

23   web_child(connfd); /* обработка запроса */

24   Close(connfd);

25  }

26 }

20-25 Каждый дочерний процесс вызывает функцию accept, и когда она завершается, функция web_child (см. листинг 30.5) обрабатывает клиентский запрос. Дочерний процесс продолжает выполнение цикла, пока родительский процесс не завершит его.

Данный текст является ознакомительным фрагментом.