Когда мы после внесения этих изменений наберем первую строку ввода, эта строка будет отправлена как широковещательное сообщение, а мы установим аргумент функции alarm равным 1 с. Мы блокируемся в вызове функции recvfrom, а затем для нашего сокета приходит первый ответ, вероятно, в течение нескольких миллисекунд. Ответ возвращается функцией recvfrom, но затем мы входим в спящее состояние на одну секунду. Принимаются остальные ответы и помещаются в приемный буфер сокета. Но пока мы находимся в спящем состоянии, время таймера alarm истекает и генерируется сигнал SIGALRM. При этом вызывается наш обработчик сигнала, затем он возвращает управление и прерывает функцию sleep, в которой мы блокированы. Далее мы повторяем цикл и читаем установленные в очередь ответы с паузой в одну секунду каждый раз, когда выводится ответ. Прочитав все ответы, мы снова блокируемся в вызове функции recvfrom, однако таймер уже не работает. Мы окажемся навсегда заблокированы в вызове функции recvfrom. Фундаментальная проблема здесь в том, что наша цель — обеспечить прерывание блокирования в функции recvfrom обработчиком сигнала, однако сигнал может быть доставлен в любое время, и наша программа в момент доставки сигнала может находиться в любом месте бесконечного цикла for.
Теперь мы проанализируем четыре различных варианта решения этой проблемы: одно некорректное и три различных корректных решения.
Блокирование и разблокирование сигналаНаше первое (некорректное) решение снижает вероятность появления ошибки, блокируя сигнал и предотвращая его доставку, пока наша программа выполняет оставшуюся часть цикла for. Эта версия представлена в листинге 20.2.
Листинг 20.2. Блокирование сигналов при выполнении в цикле for (некорректное решение)
//bcast/dgclibcast3.c
1 #include "unp.h"
2 static void recvfrom_alarm(int);
3 void
4 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
5 {
6 int n;
7 const int on = 1;
8 char sendline[MAXLINE], recvline[MAXLINE + 1];
9 sigset_t sigset_alrm;
10 socklen_t len;
11 struct sockaddr *preply_addr;
12 preply_addr = Malloc(servlen);
13 Setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
14 Sigemptyset(&sigset_alrm);
15 Sigaddset(&sigset_alrm, SIGALRM);
16 Signal(SIGALRM, recvfrom_alarm);
17 while (Fgets(sendline, MAXLINE, fp) != NULL) {
18 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
19 alarm(5);
20 for (;;) {
21 len = servlen;
22 Sigprocmask(SIG_UNBLOCK, &sigset_alrm, NULL);
23 n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
24 Sigprocmask(SIG_BLOCK, &sigset_alrm, NULL);
25 if (n < 0) {
26 if (errno == EINTR)
27 break; /* окончание ожидания ответа */
28 else
29 err_sys("recvfrom error");
30 } else {
31 recvline[n] = 0; /* завершающий нуль */
32 printf("from %s: %s",
33 Sock_ntop_host(preply_addr, len), recvline);
34 }
35 }
36 }
37 free(preply_addr);
38 }
39 static void
40 recvfrom_alarm(int signo)
41 {
42 return; /* выход из recvfrom() */
43 }
Объявление набора сигналов и инициализация14-15 Мы объявляем набор сигналов, инициализируем его как пустой набор (sigemptyset) и включаем бит, соответствующий сигналу SIGALRM (sigaddset).
Разблокирование и блокирование сигнала21-24 Перед вызовом функции recvfrom мы разблокируем сигнал (с тем, чтобы он мог быть доставлен, пока наша программа блокирована), а затем блокируем его, как только завершается функция recvfrom. Если сигнал генерируется (истекает время таймера), когда сигнал блокирован, то ядро запоминает этот факт, но доставить сигнал (то есть вызвать наш обработчик) не может, пока сигнал не будет разблокирован. В этом состоит принципиальная разница между генерацией сигнала и его доставкой. В главе 10 [110] предоставлена более подробная информация обо всех аспектах обработки сигналов POSIX.
Если мы откомпилируем и запустим эту программу, нам будет казаться, что она работает нормально, но все программы, порождающие ситуацию гонок, большую часть времени работают без каких-либо проблем! Проблема остается: разблокирование сигнала, вызов функции recvfrom и блокирование сигнала — все эти действия являются независимыми системными вызовами. Будем считать, что функция recvfrom возвращает последний ответ на нашу дейтаграмму, а сигнал доставляется между вызовом функции recvfrom и блокированием сигнала. Следующий вызов функции recvfrom заблокируется навсегда. Мы ограничили размер окна, но проблема осталась.
Вариантом решения может быть установка глобального флага при доставке сигнала его обработчиком: