UNIX: разработка сетевых приложений
Доставка множества экземпляров одного и того же сигнала вызывает проблему, к рассмотрению которой мы и приступим.
Рис. 5.3. Клиент завершает работу, закрывая все пять соединений и завершая все пять дочерних процессов
Сначала мы запускаем сервер в фоновом режиме, а затем — новый клиент. Наш сервер, показанный в листинге 5.1, несколько модифицирован — теперь в нем вызывается функция
для установки обработчика сигналаsignal, приведенного в листинге 5.6.SIGCHLDlinux % <b>tcpserv03 &</b>[1] 20419linux % <b>tcpcli04 206.62.226.35</b><b>hello </b><i>мы набираем эту строку</i>hello <i>и она отражается сервером</i><b>^D</b> <i>мы набираем символ конца файла</i>child 20426 terminated <i>выводится сервером</i>Первое, что мы можем заметить, — данные выводит только одна функция
, хотя мы предполагаем, что все пять дочерних процессов должны завершиться. Если мы выполним программуprintf, то увидим, что другие четыре дочерних процесса все еще существуют как зомби.psPID TTY TIME CMD20419 pts/6 00:00:00 tcpserv0320421 pts/6 00:00:00 tcpserv03 <defunct>20422 pts/6 00:00:00 tcpserv03 <defunct>20423 pts/6 00:00:00 tcpserv03 <defunct>Установки обработчика сигнала и вызова функции
из этого обработчика недостаточно для предупреждения появления зомби. Проблема состоит в том, что все пять сигналов генерируются до того, как выполняется обработчик сигнала, и вызывается он только один раз, поскольку сигналы Unix обычно не помещаются в очередь. Более того, эта проблема является недетерминированной. В приведенном примере с клиентом и сервером на одном и том же узле обработчик сигнала выполняется один раз, оставляя четыре зомби. Но если мы запустим клиент и сервер на разных узлах, то обработчик сигналов, скорее всего, выполнится дважды: один раз в результате генерации первого сигнала, а поскольку другие четыре сигнала приходят во время выполнения обработчика, он вызывается повторно только один раз. При этом остаются три зомби. Но иногда в зависимости от точного времени получения сегментов FIN на узле сервера обработчик сигналов может выполниться три или даже четыре раза.waitПравильным решением будет вызвать функцию
вместоwaitpid. В листинге 5.8 представлена версия нашей функцииwait, корректно обрабатывающая сигналsigchld. Эта версия работает, потому что мы вызываем функциюSIGCHLDв цикле, получая состояние любого из дочерних процессов, которые завершились. Необходимо задать параметрwaitpid: это указывает функцииWNOHANG, что не нужно блокироваться, если существуют выполняемые дочерние процессы, которые еще не завершились. В листинге 5.6 мы не могли вызвать функциюwaitpidв цикле, поскольку нет возможности предотвратить блокирование функцииwaitпри наличии выполняемых дочерних процессов, которые еще не завершились.waitВ листинге 5.9 показана окончательная версия нашего сервера. Он корректно обрабатывает возвращение ошибки
из функцииEINTRи устанавливает обработчик сигнала (листинг 5.8), который вызывает функциюacceptдля всех завершенных дочерних процессов.waitpidЛистинг 5.8. Окончательная (корректная) версия функции sig_chld, вызывающая функцию waitpid
//tcpcliserv/sigchldwaitpid.c1 #include "unp.h"2 void3 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.c1 #include "unp.h"2 int3 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 else24 err_sys("accept error");25 }26 if ((childpid = Fork()) == 0) { /* дочерний процесс */
