Установки обработчика сигнала и вызова функции
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 terminated\n", 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. При перехватывании сигналов мы должны обрабатывать прерванные системные вызовы.