26.3. Сигнал SIGCHLD

Принудительное завершение дочернего процесса — это событие, которое происходит асинхронно. Родитель не может предвидеть момент его возникновения (даже если он сам шлет потомку сигнал SIGKILL, точное время завершения зависит от того, когда потомок получит доступ к ЦП). Мы уже знаем, что для предотвращения накопления потомков-«зомби» родитель должен использовать вызов wait() (или аналогичный ему), и сделать это можно двумя способами.

• Родитель может вызвать wait() или waitpid(), не указывая флаг WNOHANG. В этом случае вызов блокируется, если потомок все еще не завершил работу.

• Родитель может периодически выполнять неблокирующую проверку (опрос) завершившихся потомков с помощью вызова waitpid(), указывая флаг WNOHANG.

Оба этих метода могут оказаться неудобными. С одной стороны, мы не хотим блокировать родителя, пока тот ждет завершения работы своего потомка. С другой — постоянное выполнение неблокирующих вызовов waitpid() расходует процессорное время и усложняет структуру приложения. Чтобы обойти эту проблему, мы можем воспользоваться обработчиком сигнала SIGCHLD.

26.3.1. Установка обработчика сигнала SIGCHLD

Сигнал SIGCHLD передается родительскому процессу всякий раз, когда один из его потомков завершает работу. По умолчанию он игнорируется, но мы можем его перехватить, установив соответствующий обработчик. Внутри этого обработчика можно сделать вызов wait() (или аналогичный ему), чтобы убрать дочерний процесс-«зомби». Однако у этого подхода есть небольшая тонкость.

В разделах 20.10 и 20.12 мы видели, что при вызове обработчика сигнал, который этот вызов спровоцировал, временно блокируется (если при вызове sigaction() не был указан флаг SA_NODEFER); кроме того, стандартные сигналы, одним из которых и является SIGCHLD, не ставятся в очередь. Следовательно, если во время того, как обработчик SIGCHLD выполняется для уже завершенного потомка, еще несколько потомков успевают завершиться, сигнал SIGCHLD хоть и будет сгенерирован два раза, но дойдет до родителя в единственном экземпляре. В результате, если родительский обработчик SIGCHLD при каждом выполнении делает только один вызов wait(), он может пропустить некоторые дочерние процессы-«зомби».

Для решения этой проблемы внутри обработчика SIGCHLD можно поместить цикл и вызывать в нем waitpid() с флагом WNOHANG, пока больше не останется завершенных потомков. Тело обработчика SIGCHLD часто состоит из следующего кода, который просто утилизирует завершенных потомков без проверки их статуса:

while (waitpid(–1, NULL, WNOHANG) > 0)

continue;

Цикл, представленный выше, продолжается до тех пор, пока waitpid() не вернет либо 0, что говорит о том, что потомков-«зомби» больше не осталось, либо –1, что свидетельствует об ошибке (вероятно, со статусом ECHILD, который указывает на отсутствие дочерних процессов).

Проблемы проектирования обработчиков SIGCHLD

Представьте, что на момент установки обработчика SIGCHLD у процесса уже есть завершенный потомок. Должно ли ядро немедленно сгенерировать сигнал SIGCHLD для родителя? Стандарт SUSv3 оставляет этот вопрос без ответа. Одни системы, в основном на базе System V, генерируют этот сигнал, а другие, включая Linux, — нет. Переносимое приложение может нивелировать эти отличия, установив обработчик SIGCHLD до создания каких-либо потомков (обычно это наиболее очевидное решение).

Еще одним моментом, на который стоит обратить внимание, является реентерабельность. В подразделе 21.1.2 отмечалось, что использование системного вызова (например, waitpid()) внутри обработчика сигнала может изменить значение глобальной переменной errno. Такое изменение может помешать главной программе задать это значение (в качестве примера см. обсуждение вызова getpriority() в разделе 35.1) или проверить его после неудачного системного вызова. По этой причине иногда требуется, чтобы обработчик SIGCHLD в самом начале сохранял errno в локальной переменной, и затем восстанавливал ее значение перед самым возвращением. Пример этого подхода показан в листинге 26.5.

Пример программы

В листинге 26.5 приводится пример более сложного обработчика SIGCHLD. Этот обработчик выводит идентификатор процесса и статус ожидания каждого освобожденного потомка . Чтобы продемонстрировать, что во время выполнения обработчика сигналы SIGCHLD не ставятся в очередь, мы искусственно продлеваем время работы обработчика, делая вызов sleep() . Главная программа создает по одному дочернему процессу на каждый (целочисленный) аргумент командной строки . Все эти потомки приостанавливаются на время, заданное в соответствующем аргументе (в секундах), после чего завершаются . В листинге сессии данной программы, приведенном ниже, видно, что сигнал SIGCHLD поступает родителю всего два раза, хотя завершено было три потомка:

$ ./multi_SIGCHLD 1 2 4

16:45:18 Child 1 (PID=17767) exiting

Перейти на страницу:

Похожие книги