UNIX: разработка сетевых приложений
Signal(SIGCHLD, sig_chld);в листинге 5.1, после вызова функции
. (Необходимо сделать это до вызова функцииlistenдля первого дочернего процесса, причем только один раз.) Затем мы определяем обработчик сигнала — функциюfork, представленную в листинге 5.6.sig_chldЛистинг 5.6. Версия обработчика сигнала SIGCHLD, вызывающая функцию wait (усовершенствованная версия находится в листинге 5.8)
//tcpcliserv/sigchldwait.с1 #include "unp.h"2 void3 sig_chld(int signo)4 {5 pid_t pid;6 int stat;7 pid = wait(&stat);8 printf("child terrmnated\n", pid);9 return;10 }ВНИМАНИЕВ обработчике сигналов не рекомендуется вызов стандартных функций ввода-вывода, таких как printf, по причинам, изложенным в разделе 11.18. В данном случае мы вызываем функцию printf как средство диагностики, чтобы увидеть, когда завершается дочерний процесс.
В системах System V и Unix 98 дочерний процесс не становится зомби, если процесс задает действие SIG_IGN для SIGCHLD. К сожалению, это верно только для System V и Unix 98. В POSIX прямо сказано, что такое поведение этим стандартом не предусмотрено. Переносимый способ обработки зомби состоит в том, чтобы перехватывать сигнал SIGCHLD и вызывать функцию wait или waitpid.
Если мы откомпилируем в Solaris 9 программу, представленную в листинге 5.1, вызывая функцию
с нашим обработчикомSignal, и будем использовать функциюsig_chldиз системной библиотеки (вместо нашей версии, показанной в листинге 5.5), то получим следующее:signalsolaris % <b>tcpserv02 &</b> <i>запускаем сервер в фоновом режиме</i>[2] 16939solaris % <b>tcpcli01 127.0.0.1</b> <i>затем клиент</i><b>hi there </b><i>набираем эту строку</i>hi there <i>и она отражается сервером</i><b>^D </b><i>вводим символ конца файла</i>child 16942 terminated <i>функция printf из обработчика сигнала выводит эту строку</i>accept error: Interrupted system call <i>но функция main преждевременно прекращает выполнение</i>Последовательность шагов в этом примере такова:
1. Мы завершаем работу клиента, вводя символ EOF. TCP клиента посылает сегмент FIN серверу, и сервер отвечает сегментом ACK.
2. Получение сегмента FIN доставляет EOF ожидающей функции
дочернего процесса. Дочерний процесс завершается.readline3. Родительский процесс блокирован в вызове функции
, когда доставляется сигналaccept. ФункцияSIGCHLD(наш обработчик сигнала) выполняется, функцияsig_chldполучает PID дочернего процесса и статус завершения, после чего из обработчика сигнала вызывается функцияwait. Обработчик сигнала возвращает управление.printf4. Поскольку сигнал был перехвачен родительским процессом, в то время как родительский процесс был блокирован в медленном (см. ниже) системном вызове (функция
), ядро заставляет функциюacceptвозвратить ошибкуaccept(прерванный системный вызов). Родительский процесс не обрабатывает эту ошибку корректно (см. листинг 5.1), поэтому функцияEINTRпреждевременно завершается.mainЦель данного примера — показать, что при написании сетевых программ, перехватывающих сигналы, необходимо получать информацию о прерванных системных вызовах и обрабатывать их. В этом специфичном для Solaris 2.5 примере функция
из стандартной библиотеки С не осуществляет автоматический перезапуск прерванного вызова, то есть флагsignal, установленный нами в листинге 5.5, не устанавливается функцией signal из системной библиотеки. Некоторые другие системы автоматически перезапускают прерванный системный вызов. Если мы запустим тот же пример в 4.4BSD, используя ее библиотечную версию функцииSA_RESTART, ядро перезапустит прерванный системный вызов и функцияsignalне возвратит ошибки. Одна из причин, по которой мы определяем нашу собственную версию функцииacceptи используем ее далее, — решение этой потенциальной проблемы, возникающей в различных операционных системах (см. листинг 5.5).signalКроме того, мы всегда программируем явную функцию
для наших обработчиков сигналов (см. листинг 5.6), даже если функция ничего не возвращает (return), чтобы этот оператор напоминал нам о возможности прерывания системного вызова при возврате из обработчика.voidОбработка прерванных системных вызовов
Термином медленный системный вызов (slow system call), введенным при описании функции
, мы будем обозначать любой системный вызов, который может быть заблокирован навсегда. Такой системный вызов может никогда не завершиться. В эту категорию попадает большинство сетевых функций. Например, нет никакой гарантии, что вызов функцииacceptсервером когда-нибудь будет завершен, если нет клиентов, которые соединятся с сервером. Аналогично, вызов нашим сервером функцииaccept(изread) в листинге 5.2 никогда не возвратит управление, если клиент никогда не пошлет серверу строку для отражения. Другие примеры медленных системных вызовов — чтение и запись в случае программных каналов и терминальных устройств. Важным исключением является дисковый ввод-вывод, который обычно завершается возвращением управления вызвавшему процессу (в предположении, что не происходит фатальных аппаратных ошибок).readlineОсновное применяемое здесь правило связано с тем, что когда процесс, блокированный в медленном системном вызове, перехватывает сигнал, а затем обработчик сигналов завершает работу, системный вызов может возвратить ошибку
. Некоторые ядра автоматически перезапускают некоторые прерванные системные вызовы. Для обеспечения переносимости программ, перехватывающих сигналы (большинство параллельных серверов перехватывает сигналы SIGCHLD), следует учесть, что медленный системный вызов может возвратить ошибку EINTR. Проблемы переносимости связаны с написанными выше словами «могут» и «некоторые» и тем фактом, что поддержка флага POSIXEINTRне является обязательной. Даже если реализация поддерживает флагSA_RESTART, не все прерванные системные вызовы могут автоматически перезапуститься. Например, большинство реализаций, происходящих от Беркли, никогда автоматически не перезапускают функциюSA_RESTART, а некоторые из этих реализаций никогда не перезапускают функцииselectиaccept.recvfrom