для наших обработчиков сигналов (см. листинг 5.6), даже если функция ничего не возвращает (
void
), чтобы этот оператор напоминал нам о возможности прерывания системного вызова при возврате из обработчика.
Обработка прерванных системных вызовов
Термином медленный системный вызов( slow system call), введенным при описании функции
accept
, мы будем обозначать любой системный вызов, который может быть заблокирован навсегда. Такой системный вызов может никогда не завершиться. В эту категорию попадает большинство сетевых функций. Например, нет никакой
гарантии, что вызов функции
accept
сервером когда-нибудь будет завершен, если нет клиентов, которые соединятся с сервером. Аналогично, вызов нашим сервером функции
read
(из
readline
) в листинге 5.2 никогда не возвратит управление, если клиент никогда не пошлет серверу строку для отражения. Другие примеры медленных системных вызовов — чтение и запись в случае программных каналов и терминальных устройств. Важным исключением является дисковый ввод-вывод, который обычно завершается возвращением управления вызвавшему процессу (в предположении, что не происходит фатальных аппаратных ошибок).
Основное применяемое здесь правило связано с тем, что когда процесс, блокированный в медленном системном вызове, перехватывает сигнал, а затем обработчик сигналов завершает работу, системный вызов можетвозвратить ошибку
EINTR
. Некоторыеядра автоматически перезапускают некоторыепрерванные системные вызовы. Для обеспечения переносимости программ, перехватывающих сигналы (большинство параллельных серверов перехватывает сигналы SIGCHLD), следует учесть, что медленный системный вызов может возвратить ошибку EINTR. Проблемы переносимости связаны с написанными выше словами « могут» и « некоторые» и тем фактом, что поддержка флага POSIX
SA_RESTART
не является обязательной. Даже если реализация поддерживает флаг
SA_RESTART
, не все прерванные системные вызовы могут автоматически перезапуститься. Например, большинство реализаций, происходящих от Беркли, никогда автоматически не перезапускают функцию
select
, а некоторые из этих реализаций никогда не перезапускают функции
accept
и
recvfrom
.
Чтобы обработать прерванный вызов функции
accept
, мы изменяем вызов функции
accept
, приведенной в листинге 5.1, в начале цикла
for
следующим образом:
for (;;) {
clilen = sizeof(cliaddr);
if ((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0) {
if (errno == EINTR)
continue; /* назад в for */
else
err_sys("accept error");
}
Обратите внимание, что мы вызываем функцию
accept
, а не функцию-обертку
Accept
, поскольку мы должны обработать неудачное выполнение функции самостоятельно.
В этой части кода мы сами перезапускаем прерванный системный вызов. Это допустимо для функции
accept
и таких функций, как
read
,
write
,
select
и
open
. Но есть функция, которую мы не можем перезапустить самостоятельно, — это функция
connect
. Если она возвращает ошибку
EINTR
, мы не можем снова вызвать ее, поскольку в этом случае немедленно возвратится еще одна ошибка. Когда функция connect прерывается перехваченным сигналом и не перезапускается автоматически, нужно вызвать функцию
select
, чтобы дождаться завершения соединения (см. раздел 16.3).
5.10. Функции wait и waitpid
В листинге 5.7 мы вызываем функцию
wait
для обработки завершенного дочернего процесса.
#include <sys/wait.h>
pid_t wait(int * statloc);
pid_t waitpid(pid_t pid, int * statloc, int options);
Обе
функции возвращают ID процесса в случае успешного выполнения, -1 в случае ошибки
Обе функции, и
wait
, и
waitpid
, возвращают два значения. Возвращаемое значение каждой из этих функций — это идентификатор завершенного дочернего процесса, а через указатель
statloc
передается статус завершения дочернего процесса (целое число). Для проверки статуса завершения можно вызвать три макроса, которые сообщают нам, что произошло с дочерним процессом: дочерний процесс завершен нормально, уничтожен сигналом или только приостановлен программой управления заданиями (job-control). Дополнительные макросы позволяют получить состояние выхода дочернего процесса, а также значение сигнала, уничтожившего или остановившего процесс. В листинге 15.8 мы используем макроопределения
WIFEXITED
и
WEXITSTATUS
.
Если у процесса, вызывающего функцию
wait
, нет завершенных дочерних процессов, но есть один или несколько выполняющихся, функция
wait
блокируется до тех пор, пока первый из дочерних процессов не завершится.
Функция
waitpid
предоставляет более гибкие возможности выбора ожидаемого процесса и его блокирования. Прежде всего, в аргументе
pid
задается идентификатор процесса, который мы будем ожидать. Значение -1 говорит о том, что нужно дождаться завершения первого дочернего процесса. (Существуют и другие значения идентификаторов процесса, но здесь они нам не понадобятся.) Аргумент
options
позволяет задавать дополнительные параметры. Наиболее общеупотребительным является параметр
WNOHANG
: он сообщает ядру, что не нужно выполнять блокирование, если нет завершенных дочерних процессов.
Различия между функциями wait и waitpid
Теперь мы проиллюстрируем разницу между функциями
wait
и
waitpid
, используемыми для сброса завершенных дочерних процессов. Для этого мы изменим код нашего клиента TCP так, как показано в листинге 5.7. Клиент устанавливает пять соединений с сервером, а затем использует первое из них (
sockfd[0]
) в вызове функции
str_cli
. Несколько соединений мы устанавливаем для того, чтобы породить от параллельного сервера множество дочерних процессов, как показано на рис. 5.2.
Рис. 5.2. Клиент, установивший пять соединений с одним и тем же параллельным сервером
Листинг 5.7. Клиент TCP, устанавливающий пять соединений с сервером