организовать двустороннюю поочередную связь процесса-родителя и процесса-ребенка через pipe, используя для синхронизации сигналы sigusr1 и sigusr2. |
Организация ввода-вывода в UNIX. Файлы устройств. Аппарат прерываний. Сигналы в UNIX
Возникновение сигнала SIGPIPE при попытке записи в pipe или FIFO, который никто не собирается читать
В материалах семинара 5 (раздел "Особенности поведения вызовов read() и write() для pip'а") при обсуждении работы с pip'ами и FIFO мы говорили, что для них системные вызовы read() и write() имеют определенные особенности поведения. Одной из таких особенностей является получение сигнала SIGPIPE процессом, который пытается записывать информацию в pipe или в FIFO в том случае, когда читать ее уже некому (нет ни одного процесса, который держит соответствующий pipe или FIFO открытым для чтения). Реакция по умолчанию на этот сигнал – прекратить работу процесса. Теперь мы уже можем написать корректную обработку этого сигнала пользователем, например, для элегантного прекращения работы пишущего процесса. Однако для полноты картины необходимо познакомиться с особенностями поведения некоторых системных вызовов при получении процессом сигналов во время их выполнения.
По ходу нашего курса мы представили читателям ряд системных вызовов, которые могут во время выполнения блокировать процесс. К их числу относятся системный вызов open() при открытии FIFO, системные вызовы read() и write() при работе с pip'ами и FIFO, системные вызовы msgsnd() и msgrcv() при работе с очередями сообщений, системный вызов semop() при работе с семафорами и т.д. Что произойдет с процессом, если он, выполняя один из этих системных вызовов, получит какой-либо сигнал? Дальнейшее поведение процесса зависит от установленной для него реакции на этот сигнал.
- Если реакция на полученный сигнал была "игнорировать сигнал " (независимо от того, установлена она по умолчанию или пользователем с помощью системного вызова signal() ), то поведение процесса не изменится.
- Если реакция на полученный сигнал установлена по умолчанию и заключается в прекращении работы процесса, то процесс перейдет в состояние закончил исполнение.
- Если реакция процесса на сигнал заключается в выполнении пользовательской функции, то процесс выполнит эту функцию (если он находился в состоянии ожидание, он попадет в состояние готовность и затем в состояние исполнение ) и вернется из системного вызова с констатацией ошибочной ситуации (некоторые системные вызовы позволяют операционной системе после выполнения обработки сигнала вновь вернуть процесс в состояние ожидания). Отличить такой возврат от действительно ошибочной ситуации можно с помощью значения системной переменной errno, которая в этом случае примет значение EINTR (для вызова write и сигнала SIGPIPE соответствующее значение в порядке исключения будет EPIPE ).
После этого краткого обсуждения становится до конца ясно, как корректно обработать ситуацию "никто не хотел прочитать" для системного вызова write(). Чтобы пришедший сигнал SIGPIPE не завершил работу нашего процесса по умолчанию, мы должны его обработать самостоятельно (функция-обработчик при этом может быть и пустой!). Но этого мало. Поскольку нормальный ход выполнения системного вызова был нарушен сигналом, мы вернемся из него с отрицательным значением, которое свидетельствует об ошибке. Проанализировав значение системной переменной errno на предмет совпадения со значением EPIPE, мы можем отличить возникновение сигнала SIGPIPE от других ошибочных ситуаций (неправильные значения параметров и т.д.) и грациозно продолжить работу программы.
Понятие о надежности сигналов. POSIX функции для работы с сигналами
Основным недостатком системного вызова signal() является его низкая надежность.
Во многих вариантах операционной системы UNIX установленная при его помощи обработка сигнала пользовательской функцией выполняется только один раз, после чего автоматически восстанавливается реакция на сигнал по умолчанию. Для постоянной пользовательской обработки сигнала необходимо каждый раз заново устанавливать реакцию на сигнал прямо внутри функции-обработчика.
В системных вызовах и пользовательских программах могут существовать критические участки, на которых процессу недопустимо отвлекаться на обработку сигналов. Мы можем выставить на этих участках реакцию "игнорировать сигнал " с последующим восстановлением предыдущей реакции, но если сигнал все-таки возникнет на критическом участке, то информация о его возникновении будет безвозвратно потеряна.
Наконец, последний недостаток связан с невозможностью определения количества сигналов одного и того же типа, поступивших процессу, пока он находился в состоянии готовность. Сигналы одного типа в очередь не ставятся! Процесс может узнать о том, что сигнал или сигналы определенного типа были ему переданы, но не может определить их количество. Этот недостаток мы можем проиллюстрировать, слегка изменив программу с асинхронным получением информации о статусе завершившихся процессов, рассмотренную нами ранее в разделе "Изучение особенностей получения терминальных сигналов текущей и фоновой группой процессов ". Пусть в новой программе 13–14-6.c процесс-родитель порождает в цикле пять новых процессов, каждый из которых сразу же завершается со своим собственным кодом, после чего уходит в бесконечный цикл.
/* Программа для иллюстрации ненадежности сигналов */ #include <sys/types.h> #include <unistd.h> #include <wait.h> #include <signal.h> #include <stdio.h> /* Функция my_handler – обработчик сигнала SIGCHLD */ void my_handler(int nsig){ int status; pid_t pid; /* Опрашиваем статус завершившегося процесса и одновременно узнаем его идентификатор */ if((pid = waitpid(-1, &status, 0)) < 0){ /* Если возникла ошибка – сообщаем о ней и продолжаем работу */ printf("Some error on waitpid errno = %d\n", errno); } else { /* Иначе анализируем статус завершившегося процесса */ if ((status & 0xff) == 0) { /* Процесс завершился с явным или неявным вызовом функции exit() */ printf("Process %d was exited with status %d\n", pid, status >> 8); } else if ((status & 0xff00) == 0){ /* Процесс был завершен с помощью сигнала */ printf("Process %d killed by signal %d %s\n", pid, status &0x7f,(status & 0x80) ? "with core file" : "without core file"); } } } int main(void){ pid_t pid; int i; /* Устанавливаем обработчик для сигнала SIGCHLD */ (void) signal(SIGCHLD, my_handler); /* В цикле порождаем 5 процессов-детей */ for (i=0; i < 5; i++) if((pid = fork()) < 0){ printf("Can\'t fork child %d\n", i); exit(1); } else if (pid == 0){ /* Child i – завершается с кодом 200 + i */ exit(200 + i); } /* Продолжение процесса-родителя – уходим на новую итерацию */ } /* Продолжение процесса-родителя – уходим в цикл */ while(1); return 0; }Листинг 13-14.6. Программа (13–14-6.c) для иллюстрации ненадежности сигналов.
Сколько сообщений о статусе завершившихся детей мы ожидаем получить? Пять! А сколько получим? It depends... Откомпилируйте, прогоните и посчитайте.
Последующие версии System V и BSD пытались устранить эти недостатки собственными средствами. Единый способ более надежной обработки сигналов появился с введением POSIX стандарта на системные вызовы UNIX. Набор функций и системных вызовов для работы с сигналами был существенно расширен и построен таким образом, что позволял временно блокировать обработку определенных сигналов, не допуская их потери. Однако проблема, связанная с определением количества пришедших сигналов одного типа, по-прежнему остается актуальной. (Надо отметить, что подобная проблема существует на аппаратном уровне и для внешних прерываний. Процессор зачастую не может определить, какое количество внешних прерываний с одним номером возникло, пока он выполнял очередную команду.)
Рассмотрение POSIX сигналов выходит за рамки нашего курса. Желающие могут самостоятельно просмотреть описания функций и системных вызовов sigemptyset(), sigfillset(), sigaddset(), sigdelset(), sigismember(), sigaction(), sigprocmask(), sigpending(), sigsuspend() в UNIX Manual.
Задача повышенной сложности: модифицируйте обработку сигнала в программе из этого раздела, не применяя POSIX-сигналы, так, чтобы процесс-родитель все-таки сообщал о статусе всех завершившихся процессов-детей.