Категории
Самые читаемые
PochitayKnigi » Компьютеры и Интернет » Программное обеспечение » UNIX: разработка сетевых приложений - Уильям Стивенс

UNIX: разработка сетевых приложений - Уильям Стивенс

Читать онлайн UNIX: разработка сетевых приложений - Уильям Стивенс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 226 227 228 229 230 231 232 233 234 ... 263
Перейти на страницу:

Но прежде чем углубляться в детали, исследуем основную структуру этого типа сервера. В листинге 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 startingn", (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) обрабатывает клиентский запрос. Дочерний процесс продолжает выполнение цикла, пока родительский процесс не завершит его.

Реализация 4.4BSD

Если вы никогда ранее не сталкивались с таким типом устройства сервера (несколько процессов, вызывающих функцию accept на одном и том же прослушиваемом сокете), вас, вероятно, удивляет, что это вообще может работать. Пожалуй, здесь уместен краткий экскурс, описывающий реализацию этого механизма в Беркли-ядрах (более подробную информацию вы найдете в [128]).

Родитель сначала создает прослушиваемый сокет, а затем — дочерние процессы. Напомним, что каждый раз при вызове функции fork происходит копирование всех дескрипторов в каждый дочерний процесс. На рис. 30.2 показана организация структур proc (по одной структуре на процесс), одна структура file для прослушиваемого дескриптора и одна структура socket.

Рис. 30.2. Организация структур proc, file и socket

Дескрипторы — это просто индексы массива, содержащегося в структуре proc, который ссылается на структуру file. Одна из целей дублирования дескрипторов в дочерних процессах, осуществляемого функцией fork, заключается в том, чтобы данный дескриптор в дочернем процессе ссылался на ту же структуру file, на которую этот дескриптор ссылается в родительском процессе. Каждая структура file содержит счетчик ссылок, который начинается с единицы, когда открывается первый файл или сокет, и увеличивается на единицу при каждом вызове функции fork и при каждом дублировании дескриптора (с помощью функции dup). В нашем примере с N дочерними процессами счетчик ссылок в структуре file будет содержать значение N+1 (учитывая родительский процесс, у которого по-прежнему открыт прослушиваемый дескриптор, хотя родительский процесс никогда не вызывает функцию accept).

При запуске программы создается N дочерних процессов, каждый из которых может вызывать функцию accept, и все они переводятся родительским процессом в состояние ожидания [128, с. 458]. Когда от клиента прибывает первый запрос на соединение, все N дочерних процессов «просыпаются», так как все они были переведены в состояние ожидания по одному и тому же «каналу ожидания» — полю so_timeo структуры socket, как совместно использующие один и тот же прослушиваемый дескриптор, указывающий на одну и ту же структуру socket. Хотя «проснулись» все N дочерних процессов, только один из них будет связан с клиентом. Остальные N - 1 снова перейдут в состояние ожидания, так как длина очереди клиентских запросов снова станет равна нулю, после того как первый из дочерних процессов займется обработкой поступившего запроса.

Такая ситуация иногда называется thundering herd — более или менее дословный перевод будет звучать как «общая побудка», так как все N процессов должны быть выведены из спящего состояния, хотя нужен всего один процесс, и остальные потом снова «засыпают». Тем не менее этот код работает, хотя и имеет побочный эффект — необходимость «будить» слишком много дочерних процессов каждый раз, когда требуется принять (accept) очередное клиентское соединение. В следующем разделе мы исследуем, как это влияет на производительность в целом.

Эффект наличия слишком большого количества дочерних процессов

В табл. 30.1 (строка 2) указано время (1,8 с), затрачиваемое центральным процессором в случае наличия 15 дочерних процессов, обслуживающих не более 10 клиентов. Мы можем оценить эффект «общей побудки», увеличивая количество дочерних процессов и оставляя то же максимальное значение количества обслуживаемых клиентов (10). Мы не показываем результаты, получаемые при увеличении количества дочерних потоков, потому что они не настолько интересны. Поскольку любое количество дочерних потоков свыше 10 может считаться избыточным, проблема «общей побудки» усугубляется, а затрачиваемое на управление процессами время увеличивается.

ПРИМЕЧАНИЕ

Некоторые ядра Unix снабжены функцией, которая выводит из состояния ожидания только один процесс для обработки одного клиентского запроса [107]. Чаще всего она называется wakeup_one.

Распределение клиентских соединений между дочерними процессами

Следующей темой обсуждения является распределение клиентских соединений между свободными дочерними процессами, блокированными в вызове функции accept. Для получения этой информации мы модифицируем функцию main, размещая в совместно используемой области памяти массив счетчиков, которые представляют собой длинные целые числа (один счетчик на каждый дочерний процесс). Это делается следующим образом:

1 ... 226 227 228 229 230 231 232 233 234 ... 263
Перейти на страницу:
Тут вы можете бесплатно читать книгу UNIX: разработка сетевых приложений - Уильям Стивенс.
Комментарии