Книга: UNIX: разработка сетевых приложений

Различия между функциями wait и waitpid

Различия между функциями wait и waitpid

Теперь мы проиллюстрируем разницу между функциями wait и waitpid, используемыми для сброса завершенных дочерних процессов. Для этого мы изменим код нашего клиента TCP так, как показано в листинге 5.7. Клиент устанавливает пять соединений с сервером, а затем использует первое из них (sockfd[0]) в вызове функции str_cli. Несколько соединений мы устанавливаем для того, чтобы породить от параллельного сервера множество дочерних процессов, как показано на рис. 5.2.


Рис. 5.2. Клиент, установивший пять соединений с одним и тем же параллельным сервером

Листинг 5.7. Клиент TCP, устанавливающий пять соединений с сервером

//tcpcliserv/tcpcli04.c
 1 #include "unp.h"
 2 int
 3 main(int argc, char **argv)
 4 {
 5  int i, sockfd[5];
 6  struct sockaddr_in servaddr;
 7  if (argc != 2)
 8   err_quit("usage: tcpcli <Ipaddress>");
 9  for (i = 0; i < 5; i++) {
10   sockfd[i] = Socket(AF_INET, SOCK_STREAM, 0);
11   bzero(&servaddr, sizeof(servaddr));
12   servaddr.sin_family = AF_INET;
13   servaddr.sin_port = htons(SERV_PORT);
14   Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
15   Connect(sockfd[i], (SA*)&servaddr, sizeof(servaddr));
16  }
17  str_cli(stdin, sockfd[0]); /* эта функция выполняет все необходимые
                               действия для формирования запроса клиента */
18  exit(0);
19 }

Когда клиент завершает работу, все открытые дескрипторы автоматически закрываются ядром (мы не вызываем функцию close, а пользуемся только функцией exit) и все пять соединений завершаются приблизительно в одно и то же время. Это вызывает отправку пяти сегментов FIN, по одному на каждое соединение, что, в свою очередь, вызывает примерно одновременное завершение всех пяти дочерних процессов. Это приводит к доставке пяти сигналов SIGCHLD практически в один и тот же момент, что показано на рис. 5.3.

Доставка множества экземпляров одного и того же сигнала вызывает проблему, к рассмотрению которой мы и приступим.


Рис. 5.3. Клиент завершает работу, закрывая все пять соединений и завершая все пять дочерних процессов

Сначала мы запускаем сервер в фоновом режиме, а затем — новый клиент. Наш сервер, показанный в листинге 5.1, несколько модифицирован — теперь в нем вызывается функция signal для установки обработчика сигнала SIGCHLD, приведенного в листинге 5.6.

linux % tcpserv03 &
[1] 20419
linux % tcpcli04 206.62.226.35
hello мы набираем эту строку
hello и она отражается сервером
^D    мы набираем символ конца файла
child 20426 terminated выводится сервером

Первое, что мы можем заметить, — данные выводит только одна функция printf, хотя мы предполагаем, что все пять дочерних процессов должны завершиться. Если мы выполним программу ps, то увидим, что другие четыре дочерних процесса все еще существуют как зомби.

PID TTY TIME CMD
20419 pts/6 00:00:00 tcpserv03
20421 pts/6 00:00:00 tcpserv03 <defunct>
20422 pts/6 00:00:00 tcpserv03 <defunct>
20423 pts/6 00:00:00 tcpserv03 <defunct>

Установки обработчика сигнала и вызова функции wait из этого обработчика недостаточно для предупреждения появления зомби. Проблема состоит в том, что все пять сигналов генерируются до того, как выполняется обработчик сигнала, и вызывается он только один раз, поскольку сигналы Unix обычно не помещаются в очередь. Более того, эта проблема является недетерминированной. В приведенном примере с клиентом и сервером на одном и том же узле обработчик сигнала выполняется один раз, оставляя четыре зомби. Но если мы запустим клиент и сервер на разных узлах, то обработчик сигналов, скорее всего, выполнится дважды: один раз в результате генерации первого сигнала, а поскольку другие четыре сигнала приходят во время выполнения обработчика, он вызывается повторно только один раз. При этом остаются три зомби. Но иногда в зависимости от точного времени получения сегментов FIN на узле сервера обработчик сигналов может выполниться три или даже четыре раза.

Правильным решением будет вызвать функцию waitpid вместо wait. В листинге 5.8 представлена версия нашей функции sigchld, корректно обрабатывающая сигнал SIGCHLD. Эта версия работает, потому что мы вызываем функцию waitpid в цикле, получая состояние любого из дочерних процессов, которые завершились. Необходимо задать параметр WNOHANG: это указывает функции waitpid, что не нужно блокироваться, если существуют выполняемые дочерние процессы, которые еще не завершились. В листинге 5.6 мы не могли вызвать функцию wait в цикле, поскольку нет возможности предотвратить блокирование функции wait при наличии выполняемых дочерних процессов, которые еще не завершились.

В листинге 5.9 показана окончательная версия нашего сервера. Он корректно обрабатывает возвращение ошибки EINTR из функции accept и устанавливает обработчик сигнала (листинг 5.8), который вызывает функцию waitpid для всех завершенных дочерних процессов.

Листинг 5.8. Окончательная (корректная) версия функции sig_chld, вызывающая функцию waitpid

//tcpcliserv/sigchldwaitpid.c
 1 #include "unp.h"
 2 void
 3 sig_chld(int signo)
 4 {
 5  pid_t pid;
 6  int stat;
 7  while ((pid = waitpid(-1, &stat, WNOHANG)) >0)
 8   printf("child %d terminatedn", pid);
 9  return;
10 }

Листинг 5.9. Окончательная (корректная) версия TCP-сервера, обрабатывающего ошибку EINTR функции accept

//tcpcliserv/tcpserv04.c
 1 #include "unp.h"
 2 int
 3 main(int argc, char **argv)
 4 {
 5  int listenfd, connfd;
 6  pid_t childpid;
 7  socklen_t clilen;
 8  struct sockaddr_in cliaddr, servaddr;
 9  void sig_chld(int);
10  listenfd = Socket(AF_INET, SOCK_STREAM, 0);
11  bzero(&servaddr, sizeof(servaddr));
12  servaddr.sin_family = AF_INET;
13  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
14  servaddr.sin_port = htons(SERV_PORT);
15  Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
16  Listen(listenfd, LISTENQ);
17  Signal(SIGCHLD, sig_chld); /* нужно вызвать waitpid() */
18  for (;;) {
19   clilen = sizeof(cliaddr);
20   if ((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0) {
21    if (errno == EINTR)
22     continue; /* назад к for() */
23    else
24     err_sys("accept error");
25   }
26   if ((childpid = Fork()) == 0) { /* дочерний процесс */
27    Close(listenfd); /* закрываем прослушиваемый сокет */
28    str_echo(connfd); /* обрабатываем запрос */
29    exit(0);
30   }
31   Close(connfd); /* родитель закрывает присоединенный сокет */
32  }
33 }

Целью этого раздела было продемонстрировать три сценария, которые могут встретиться в сетевом программировании.

1. При выполнении функции fork, порождающей дочерние процессы, следует перехватывать сигнал SIGCHLD.

2. При перехватывании сигналов мы должны обрабатывать прерванные системные вызовы.

3. Обработчик сигналов SIGCHLD должен быть создан корректно с использованием функции waitpid, чтобы не допустить появления зомби.

Окончательная версия нашего сервера TCP (см. листинг 5.9) вместе с обработчиком сигналов SIGCHLD в листинге 5.8 обрабатывает все три сценария.

Оглавление книги

Оглавление статьи/книги

Генерация: 1.310. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз