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 terminated\n", pid);
9 return;
10 }
Листинг 5.9. Окончательная (корректная) версия TCP-сервера, обрабатывающего ошибку EINTR функции accept