UNIX: разработка сетевых приложений - Уильям Стивенс
Шрифт:
Интервал:
Закладка:
57 for (i = 0; i < nchildren; i++) {
58 if (FD_ISSET(cptr[i].child_pipefd, &rset)) {
59 if ((n = Read(cptr[i].child_pipefd, &rc, 1)) == 0)
60 err_quit("child %d terminated unexpectedly", i);
61 cptr[i].child_status = 0;
62 navail++;
63 if (--nsel == 0)
64 break; /* с результатами select() закончено */
65 }
66 }
67 }
68 }
Отключение прослушиваемого сокета в случае отсутствия свободных дочерних процессов36-37 Счетчик navail отслеживает количество свободных дочерних процессов. Если его значение становится равным нулю, прослушиваемый сокет в наборе дескрипторов функции select выключается. Это предотвращает прием нового соединения в тот момент, когда нет ни одного свободного дочернего процесса. Ядро по- прежнему устанавливает эти соединения в очередь, пока их количество не превысит значения аргумента backlog функции listen, заданного для прослушиваемого сокета, но мы не хотим их принимать, пока у нас не появится свободный дочерний процесс, готовый обрабатывать клиентский запрос.
Прием нового соединения39-55 Если прослушиваемый сокет готов для считывания, можно принимать (accept) новое соединение. Мы находим первый свободный дочерний процесс и передаем ему присоединенный сокет с помощью функции write_fd, приведенной в листинге 15.11. Вместе с дескриптором мы передаем 1 байт, но получатель не интересуется содержимым этого байта. Родитель закрывает присоединенный сокет.
Мы всегда начинаем поиск свободного дочернего процесса с первого элемента массива структур Child. Это означает, что новое соединение для обработки поступившего клиентского запроса всегда получает первый элемент этого массива. Этот факт мы проверим при рассмотрении табл. 30.2 и значения счетчика child_count после завершения работы сервера. Если мы не хотим оказывать такое предпочтение первому элементу массива, мы можем запомнить, какой дочерний процесс получил последнее клиентское соединение, и каждый раз начинать поиск свободного дочернего процесса со следующего за ним, а по достижении конца массива переходить снова к первому элементу. В этом нет особого смысла (на самом деле все равно, какой дочерний процесс обрабатывает очередное соединение, если имеется несколько свободных дочерних процессов), если только планировочный алгоритм операционной системы не накладывает санкций на процессы, которые требуют относительно больших временных затрат центрального процессора. Более равномерное распределение загрузки между всеми дочерними процессами приведет к выравниванию времен, затраченных на их выполнение.
Обработка вновь освободившихся дочерних процессов56-66 Когда дочерний процесс заканчивает обработку клиентского запроса, наша функция child_main записывает один байт в канал для родительского процесса. Тем самым родительский конец канала становится доступным для чтения. Упомянутый байт считывается (но его значение при этом игнорируется), а дочерний процесс помечается как свободный. Если же дочерний процесс завершит свое выполнение неожиданно, его конец канала будет закрыт, а операция чтения (read) возвратит нулевое значение. Это значение перехватывается и дочерний процесс завершается, но более удачным решением было бы записать ошибку и создать новый дочерний процесс для замены завершенного.
Функция child_main показана в листинге 30.19.
Листинг 30.19. Функция child_main: передача дескриптора в сервере с предварительным порождением дочерних процессов
//server/child05.c
23 void
24 child_main(int i, int listenfd, int addrlen)
25 {
26 char c;
27 int connfd;
28 ssize_t n;
29 void web_child(int);
30 printf("child %ld startingn", (long)getpid());
31 for (;;) {
32 if ((n = Read_fd(STDERR_FILENO, &c, 1, &connfd)) == 0)
33 err_quit("read_fd returned 0");
34 if (connfd < 0)
35 err_quit("no descriptor from read_fd");
36 web_child(connfd); /* обработка запроса */
37 Close(connfd);
38 Write(STDERR_FILENO, "", 1); /* сообщаем родительскому процессу
о том, что дочерний освободился */
39 }
40 }
Ожидание дескриптора от родительского процесса32-33 Эта функция отличается от аналогичных функций из двух предыдущих разделов, так как дочерний процесс не вызывает более функцию accept. Вместо этого дочерний процесс блокируется в вызове функции read_fd, ожидая, когда родительский процесс передаст ему дескриптор присоединенного сокета.
Сообщение родительскому процессу о готовности дочернего к приему новых запросов38 Закончив обработку очередного клиентского запроса, мы записываем (write) 1 байт в канал, чтобы сообщить, что данный дочерний процесс освободился.
В табл. 30.1 при сравнении строк 4 и 5 мы видим, что данный сервер медленнее, чем версия, рассмотренная нами в предыдущем разделе, которая использовала блокировку потоками взаимного исключения. Передача дескриптора по каналу от родительского процесса к дочернему и запись одного байта в канал для сообщения родительскому процессу о завершении обработки клиентского запроса занимает больше времени, чем блокирование и разблокирование взаимного исключения или файла.
В табл. 30.2 показаны значения счетчиков child_count из структуры Child, которые выводятся обработчиком сигнала SIGINT по завершении работы сервера. Дочерние процессы, расположенные ближе к началу массива, обрабатывают большее количество клиентских запросов, как было указано при обсуждении листинга 30.18.
30.10. Параллельный сервер TCP: один поток для каждого клиента
Предыдущие пять разделов были посвящены рассмотрению серверов, в которых для обработки клиентских запросов используются дочерние процессы, либо заранее порождаемые с помощью функции fork, либо требующие вызова этой функции для каждого вновь поступившего клиентского запроса. Если же сервер поддерживает потоки, мы можем применить потоки вместо дочерних процессов.
Наша первая версия сервера с использованием потоков показана в листинге 30.20. Это модификация листинга 30.2: в ней создается один поток для каждого клиента вместо одного дочернего процесса для каждого клиента. Эта версия во многом похожа на сервер, представленный в листинге 26.2.
Листинг 30.20. Функция main для сервера TCP, использующего потоки
//server/serv06.c
1 #include "unpthread.h"
2 int
3 main(int argc, char **argv)
4 {
5 int listenfd, connfd;
6 void sig_int(int);
7 void *doit(void*);
8 pthread_t tid;
9 socklen_t clilen, addrlen;
10 struct sockaddr *cliaddr;
11 if (argc == 2)
12 listenfd = Tcp_listen(NULL, argv[1], &addrlen);
13 else if (argc == 3)
14 listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
15 else
16 err_quit("usage: serv06 [ <host> ] <port#>");
17 cliaddr = Malloc(addrlen);
18 Signal (SIGINT, sig_int);
19 for (;;) {
20 clilen = addrlen;
21 connfd = Accept(listenfd, cliaddr, &clilen);
22 Pthread_create(&tid, NULL, &doit, (void*)connfd);
23 }
24 }
25 void*
26 doit(void *arg)
27 {
28 void web_child(int);
29 Pthread_detach(pthread_self());
30 web_child((int)arg);
31 Close((int)arg);
32 return (NULL);
33 }
Цикл основного потока19-23 Основной поток блокируется в вызове функции accept, и каждый раз, когда прибывает новое клиентское соединение, функцией pthread_create создается новый поток. Функция, выполняемая новым потоком, — это функция doit, а ее аргументом является присоединенный сокет.
Функция прочих потоков25-33 Функция doit выполняется как отсоединенный (detached) поток, потому что основному потоку не требуется ждать ее завершения. Doit вызывает функцию web_child (см. листинг 30.5). Когда эта функция возвращает управление, присоединенный сокет закрывается.
Из табл. 30.1 мы видим, что эта простая версия с использованием потоков является более быстродействующей, чем даже самая быстрая из версий с предварительным порождением процессов. Кроме того, эта версия, в которой каждый клиент обслуживается одним потоком, во много раз быстрее версии, в которой каждый клиент обслуживается специально созданным для него дочерним процессом (первая строка табл. 30.1).
ПРИМЕЧАНИЕВ разделе 26.5 мы упомянули о трех вариантах преобразования функции, которая не является безопасной в многопоточной среде, в функцию, обеспечивающую требуемую безопасность. Функция web_child вызывает функцию readline, и версия, показанная в листинге 3.12, не является безопасной в многопоточной среде. На примере, приведенном в листинге 30.20, были испробованы вторая и третья альтернативы из раздела 26.5. Увеличение быстродействия при переходе от альтернативы 3 к альтернативе 2 составило менее одного процента, вероятно, потому, что функция readline использовалась лишь для считывания значения счетчика (5 символов) от клиента. Поэтому в данной главе для простоты мы использовали более медленную версию из листинга 3.11 для сервера с предварительным порождением потоков.