Уильям Стивенс - UNIX: разработка сетевых приложений
bsdi % udpcli01 192.168.42.255 < 2000line
sendto error: Message too long
ПРИМЕЧАНИЕЭто ограничение реализовано в AIX, FreeBSD и MacOS. Linux, Solaris и HP-UX фрагментируют дейтаграммы, отправленные на широковещательный адрес. Однако в целях переносимости приложение, которому нужно сделать широковещательный запрос, должно определять MTU для интерфейса, через который будет отправлено сообщение, при помощи параметра SIOCGIPMTU функции ioctl, после чего вычесть размер заголовков IP и транспортного протокола. Альтернативный подход: выбрать типичное значение MTU (например, 1500 для Ethernet) и использовать его в качестве константы.
20.5. Ситуация гонок
Ситуация гонок (race condition) обычно возникает, когда множество процессов получают доступ к общим для них данным, но корректность результата зависит от порядка выполнения процессов. Поскольку порядок выполнения процессов в типичных системах Unix зависит от множества факторов, которые могут меняться от запуска к запуску, иногда результат корректен, а иногда — нет. Наиболее сложным для отладки типом гонок является такой, когда результат получается некорректным только изредка. Более подробно о ситуациях гонок мы поговорим в главе 26, когда будем обсуждать взаимные исключения (mutex) и условные переменные (condition variables). При программировании потоков всегда возникают проблемы с ситуациями гонок, поскольку значительное количество данных является общим для всех потоков (например, все глобальные переменные).
Ситуации гонок другого типа часто возникают при работе с сигналами. Проблемы возникают, потому что сигнал, как правило, может быть доставлен в любой момент во время выполнения нашей программы. POSIX позволяет нам блокировать доставку сигнала, но при выполнении операций ввода-вывода это часто не дает эффекта.
Чтобы понять эту проблему, рассмотрим пример. Ситуация гонок возникает при выполнении программы из листинга 20.1. Потратьте несколько минут и посмотрите, сможете ли вы ее обнаружить. (Подсказка: в каком месте программы мы можем находиться, когда доставляется сигнал?) Вы можете также инициировать ситуацию гонок следующим образом: изменить аргумент функции alarm с 5 на 1 и добавить вызов sleep(1) сразу же после printf.
Когда мы после внесения этих изменений наберем первую строку ввода, эта строка будет отправлена как широковещательное сообщение, а мы установим аргумент функции 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 заблокируется навсегда. Мы ограничили размер окна, но проблема осталась.
Вариантом решения может быть установка глобального флага при доставке сигнала его обработчиком:
recvfrom_alarm(int signo) {
had_alarm = 1;
return;
}
Флаг сбрасывается в 0 каждый раз, когда вызывается функция alarm. Наша функция dg_cli проверяет этот флаг перед вызовом функции recvfrom и не вызывает ее, если флаг ненулевой.
for (;;) {
len = servlen;
Sigprocmask(SIG_UNBLOCK, &sigset_alrm, NULL);
if (had_alarm == 1)
break;
n = recvfrom(sockfd, recvline, MAXLINE, 0, preply_addr, &len);
Если сигнал был сгенерирован во время его блокирования (после предыдущего возвращения из функции recvfrom), то после разблокирования в этой части кода он будет доставлен перед завершением функции sigprocmask, устанавливающей наш флаг. Однако между проверкой флага и вызовом функции recvfrom существует промежуток времени, в течение которого сигнал может быть сгенерирован и доставлен, и если это произойдет, вызов функции recvfrom заблокируется навсегда (разумеется, мы считаем при этом, что не приходит никаких дополнительных ответов).
Блокирование и разблокирование сигнала с помощью функции pselect
Одним из корректных решений будет использование функции pselect (см. раздел 6.9), как показано в листинге 20.3.
Листинг 20.3. Блокирование и разблокирование сигналов с помощью функции pselect
//bcast/dgclibcast4.с
1 #include "unp.h"
2 static void recvfrom_alarm(int);