сунке 7.12. Процесс обращается к системной функции signal для того, чтобы
дать указание принимать сигналы о прерываниях и исполнять по их получении
функцию sigcatcher. Затем он порождает новый процесс, запускает системную
функцию nice, позволяющую сделать приоритет запуска процесса-родителя ниже
приоритета его потомка (см. главу 8), и входит в бесконечный цикл. Порожден-
ный процесс задерживает свое выполнение на 5 секунд, чтобы дать родительско-
му процессу время исполнить системную функцию nice и снизить свой приоритет.
После этого порожденный процесс входит в цикл, в каждой итерации которого он
посылает родительскому процессу сигнал о прерывании (посредством обращения к
функции kill). Если в результате ошибки, например, из-за того, что родитель-
ский процесс больше не существует, kill завершается, то завершается и порож-
денный процесс. Вся идея состоит в том, что родительскому процессу следует
запускать функцию обработки сигнала при каждом получении сигнала о прерыва-
нии. Функция обработки сигнала выводит сообщение и снова обращается к функ-
ции signal при очередном появлении сигнала о прерывании, родительский же
процесс продолжает



194

+------------------------------------------------------------+
| #include |
| sigcatcher() |
| { |
| printf("PID %d принял сигнал\n",getpid()); /* печать |
| PID */ |
| signal(SIGINT,sigcatcher); |
| } |
| |
| main() |
| { |
| int ppid; |
| |
| signal(SIGINT,sigcatcher); |
| |
| if (fork() == 0) |
| { |
| /* дать процессам время для выполнения установок */ |
| sleep(5); /* библиотечная функция приостанова на|
| 5 секунд */ |
| ppid = getppid(); /* получить идентификатор родите- |
| ля */ |
| for (;;) |
| if (kill(ppid,SIGINT) == -1) |
| exit(); |
| } |
| |
| /* чем ниже приоритет, тем выше шансы возникновения кон-|
| куренции */ |
| nice(10); |
| for (;;) |
| ; |
| } |
+------------------------------------------------------------+

Рисунок 7.12. Программа, демонстрирующая возникновение соперничества
между процессами в ходе обработки сигналов

исполнять циклический набор команд.
Однако, возможна и следующая очередность наступления событий:
1. Порожденный процесс посылает родительскому процессу сигнал о прерывании.
2. Родительский процесс принимает сигнал и вызывает функцию обработки сиг-
нала, но резервируется ядром, которое производит переключение контекста
до того, как функция signal будет вызвана повторно.
3. Снова запускается порожденный процесс, который посылает родительскому
процессу еще один сигнал о прерывании.
4. Родительский процесс получает второй сигнал о прерывании, но перед тем
он не успел сделать никаких распоряжений относительно способа обработки
сигнала. Когда выполнение родительского процесса будет возобновлено, он
завершится.
В программе описывается именно такое поведение процессов, поскольку вы-
зов родительским процессом функции nice приводит к тому, что ядро будет чаще
запускать на выполнение порожденный процесс.
По словам Ричи (эти сведения были получены в частной беседе), сигналы
были задуманы как события, которые могут быть как фатальными, так и проходя-
щими незаметно, которые не всегда обрабатываются, поэтому в ранних версиях
системы конкуренция процессов, связанная с посылкой сигналов, не фиксирова-
лась. Тем не менее, она представляет серьезную проблему в тех программах,
где осуществляется прием сигналов. Эта проблема была бы устранена, если бы

195

поле описания сигнала не очищалось по его получении. Однако, такое решение
породило бы новую проблему: если поступающий сигнал принимается, а поле очи-
щено, вложенные обращения к функции обработки сигнала могут переполнить
стек. С другой стороны, ядро могло бы сбросить значение функции обработки
сигнала, тем самым делая распоряжение игнорировать сигналы данного типа до
тех пор, пока пользователь вновь не укажет, что нужно делать по получении
подобных сигналов. Такое решение предполагает потерю информации, так как
процесс не в состоянии узнать, сколько сигналов им было получено. Однако,
информации при этом теряется не больше, чем в том случае, когда процесс по-
лучает большое количество сигналов одного типа до того, как получает возмож-
ность их обработать. В системе BSD, наконец, процесс имеет возможность бло-
кировать получение сигналов и снимать блокировку при новом обращении к сис-
темной функции; когда процесс снимает блокировку сигналов, ядро посылает
процессу все сигналы, отложенные (повисшие) с момента установки блокировки.
Когда процесс получает сигнал, ядро автоматически блокирует получение следу-
ющего сигнала до тех пор, пока функция обработки сигнала не закончит работу.
В этих действиях ядра наблюдается аналогия с тем, как ядро реагирует на ап-
паратные прерывания: оно блокирует появление новых прерываний на время обра-
ботки предыдущих.
Второе несоответствие в обработке сигналов связано с приемом сигналов,
поступающих во время исполнения системной функции, когда процесс приостанов-
лен с допускающим прерывания приоритетом. Сигнал побуждает процесс выйти из
приостанова (с помощью longjump), вернуться в режим задачи и вызвать функцию
обработки сигнала. Когда функция обработки сигнала завершает работу, проис-
ходит то, что процесс выходит из системной функции с ошибкой, сообщающей о
прерывании ее выполнения. Узнав об ошибке, пользователь запускает системную
функцию повторно, однако более удобно было бы, если бы это действие автома-
тически выполнялось ядром, как в системе BSD.
Третье несоответствие проявляется в том случае, когда процесс игнорирует
поступивший сигнал. Если сигнал поступает в то время, когда процесс находит-
ся в состоянии приостанова с допускающим прерывания приоритетом, процесс во-
зобновляется, но не выполняет longjump. Другими словами, ядро узнает о том,
что процесс проигнорировал поступивший сигнал только после возобновления его
выполнения. Логичнее было бы оставить процесс в состоянии приостанова. Одна-
ко, в момент посылки сигнала к пространству процесса, в котором ядро хранит
адрес функции обработки сигнала, может отсутствовать доступ. Эта проблема
может быть решена путем запоминания адреса функции обработки сигнала в запи-
си таблицы процессов, обращаясь к которой, ядро получало бы возможность ре-
шать вопрос о необходимости возобновления процесса по получении сигнала. С
другой стороны, процесс может немедленно вернуться в состояние приостанова
(по алгоритму sleep), если обнаружит, что в его возобновлении не было необ-
ходимости. Однако, пользовательские процессы не имеют возможности осознавать
собственное возобновление, поскольку ядро располагает точку входа в алгоритм
sleep внутри цикла с условием продолжения (см. главу 2), переводя процесс
вновь в состояние приостанова, если ожидаемое процессом событие в действи-
тельности не имело места.
Ко всему сказанному выше следует добавить, что ядро обрабатывает сигналы
типа "гибель потомка" не так, как другие сигналы. В частности, когда процесс
узнает о получении сигнала "гибель потомка", он выключает индикацию сигнала
в соответствующем поле записи таблицы процессов и по умолчанию действует
так, словно никакого сигнала и не поступало. Назначение сигнала "гибель по-
томка" состоит в возобновлении выполнения процесса, приостановленного с до-
пускающим прерывания приоритетом. Если процесс принимает такой сигнал, он,
как и во всех остальных случаях, запускает функцию обработки сигнала. Дейст-
вия, предпринимаемые ядром в том случае, когда процесс игнорирует поступив-
ший сигнал этого типа, будут описаны в разделе 7.4. Наконец, когда процесс
вызвал функцию signal с параметром "гибель потомка" (death of child), ядро
посылает ему соответствующий сигнал, если он имеет потомков, прекративших
существование. В разделе 7.4 на этом моменте мы остановимся более подробно.

196


    7.2.2 Группы процессов



Несмотря на то, что в системе UNIX процессы идентифицируются уникальным
кодом (PID), системе иногда приходится использовать для идентификации про-
цессов номер "группы", в которую они входят. Например, процессы, имеющие об-
щего предка в лице регистрационного shell'а, взаимосвязаны, и поэтому когда
пользователь нажимает клавиши "delete" или "break", или когда терминальная
линия "зависает", все эти процессы получают соответствующие сигналы. Ядро
использует код группы процессов для идентификации группы взаимосвязанных
процессов, которые при наступлении определенных событий должны получать об-
щий сигнал. Код группы запоминается в таблице процессов; процессы из одной
группы имеют один и тот же код группы.
Для того, чтобы присвоить коду группы процессов начальное значение, при-
равняв его коду идентификации процесса, следует воспользоваться системной
функцией setpgrp. Синтаксис вызова функции:
grp = setpgrp();
где grp - новый код группы процессов. При выполнении функции fork про-
цесс-потомок наследует код группы своего родителя. Использование функции
setpgrp при назначении для процесса операторского терминала имеет важные
особенности, на которые стоит обратить внимание (см. раздел 10.3.5).


    7.2.3 Посылка сигналов процессами



Для посылки сигналов процессы используют системную функцию kill. Синтак-
сис вызова функции:
kill(pid,signum)
где в pid указывается адресат посылаемого сигнала (область действия сигна-
ла), а в signum - номер посылаемого сигнала. Связь между значением pid и со-
вокупностью выполняющихся процессов следующая:
* Если pid - положительное целое число, ядро посылает сигнал процессу с
идентификатором pid.
* Если значение pid равно 0, сигнал посылается всем процессам, входящим в
одну группу с процессом, вызвавшим функцию kill.
* Если значение pid равно -1, сигнал посылается всем процессам, у которых
реальный код идентификации пользователя совпадает с тем, под которым ис-
полняется процесс, вызвавший функцию kill (об этих кодах более подробно
см. в разделе 7.6). Если процесс, пославший сигнал, исполняется под ко-
дом идентификации суперпользователя, сигнал рассылается всем процессам,
кроме процессов с идентификаторами 0 и 1.
* Если pid - отрицательное целое число, но не -1, сигнал посылается всем
процессам, входящим в группу с номером, равным абсолютному значению pid.

Во всех случаях, если процесс, пославший сигнал, исполняется под кодом
идентификации пользователя, не являющегося суперпользователем, или если коды
идентификации пользователя (реальный и исполнительный) у этого процесса не
совпадают с соответствующими кодами процесса, принимающего сигнал, kill за-
вершается неудачно.
В программе, приведенной на Рисунке 7.13, главный процесс сбрасывает ус-
тановленное ранее значение номера группы и порождает 10 новых процессов. При
рождении каждый процесс-потомок наследует номер группы процессов своего ро-
дителя, однако, процессы, созданные в нечетных итерациях цикла, сбрасывают
это значение. Системные функции getpid и getpgrp возвращают значения кода
идентификации выполняемого процесса и номера группы, в которую он входит, а
функция pause приостанавливает выполнение процесса до момента получения сиг-
нала. В конечном итоге родительский процесс запускает функцию kill и посыла-
ет сигнал о прерывании всем процессам, входящим в одну с ним группу. Ядро


197

+------------------------------------------------------------+
| #include |
| main() |
| { |
| register int i; |
| |
| setpgrp(); |
| for (i = 0; i < 10; i++) |
| { |
| if (fork() == 0) |
| { |
| /* порожденный процесс */ |
| if (i & 1) |
| setpgrp(); |
| printf("pid = %d pgrp = %d\n",getpid(),getpgrp());|
| pause(); /* системная функция приостанова вы- |
| полнения */ |
| } |
| } |
| kill(0,SIGINT); |
| } |
+------------------------------------------------------------+

Рисунок 7.13. Пример использования функции setpgrp


посылает сигнал пяти "четным" процессам, не сбросившим унаследованное значе-
ние номера группы, при этом пять "нечетных" процессов продолжают свое выпол-
нение.


    7.3 ЗАВЕРШЕНИЕ ВЫПОЛНЕНИЯ ПРОЦЕССА



В системе UNIX процесс завершает свое выполнение, запуская системную
функцию exit. После этого процесс переходит в состояние "прекращения сущест-
вования" (см. Рисунок 6.1), освобождает ресурсы и ликвидирует свой контекст.
Синтаксис вызова функции:

exit(status);

где status - значение, возвращаемое функцией родительскому процессу. Процес-
сы могут вызывать функцию exit как в явном, так и в неявном виде (по оконча-
нии выполнения программы: начальная процедура (startup), компонуемая со все-
ми программами на языке Си, вызывает функцию exit на выходе программы из
функции main, являющейся общей точкой входа для всех программ). С другой
стороны, ядро может вызывать функцию exit по своей инициативе, если процесс
не принял посланный ему сигнал (об этом мы уже говорили выше). В этом случае
значение параметра status равно номеру сигнала.
Система не накладывает никакого ограничения на продолжительность выпол-
нения процесса, и зачастую процессы существуют в течение довольно длительно-
го времени. Нулевой процесс (программа подкачки) и процесс 1 (init), к при-
меру, существуют на протяжении всего времени жизни системы. Продолжительными
процессами являются также getty-процессы, контролирующие работу терминальной
линии, ожидая регистрации пользователей, и процессы общего назначения, вы-
полняемые под руководством администратора.
На Рисунке 7.14 приведен алгоритм функции exit. Сначала ядро отменяет
обработку всех сигналов, посылаемых процессу, поскольку ее продолжение ста-
новится бессмысленным. Если процесс, вызывающий функцию exit, возглавляет


198

+------------------------------------------------------------+
| алгоритм exit |
| входная информация: код, возвращаемый родительскому про- |
| цессу |
| выходная информация: отсутствует |
| { |
| игнорировать все сигналы; |
| если (процесс возглавляет группу процессов, ассоцииро- |
| ванную с операторским терминалом) |
| { |
| послать всем процессам, входящим в группу, сигнал о |
| "зависании"; |
| сбросить в ноль код группы процессов; |
| } |
| закрыть все открытые файлы (внутренняя модификация алго-|
| ритма close); |
| освободить текущий каталог (алгоритм iput); |
| освободить области и память, ассоциированную с процессом|
| (алгоритм freereg); |
| создать запись с учетной информацией; |
| прекратить существование процесса (перевести его в соот-|
| ветствующее состояние); |
| назначить всем процессам-потомкам в качестве родителя |
| процесс init (1); |
| если кто-либо из потомков прекратил существование, |
| послать процессу init сигнал "гибель потомка"; |
| послать сигнал "гибель потомка" родителю данного процес-|
| са; |
| переключить контекст; |
| } |
+------------------------------------------------------------+

Рисунок 7.14. Алгоритм функции exit

группу процессов, ассоциированную с операторским терминалом (см. раздел
10.3.5), ядро делает предположение о том, что пользователь прекращает рабо-
ту, и посылает всем процессам в группе сигнал о "зависании". Таким образом,
если пользователь в регистрационном shell'е нажмет последовательность кла-
виш, означающую "конец файла" (Ctrl-d), при этом с терминалом остались свя-
занными некоторые из существующих процессов, процесс, выполняющий функцию
exit, пошлет им всем сигнал о "зависании". Кроме того, ядро сбрасывает в
ноль значение кода группы процессов для всех процессов, входящих в данную
группу, поскольку не исключена возможность того, что позднее текущий код
идентификации процесса (процесса, который вызвал функцию exit) будет присво-
ен другому процессу и тогда последний возглавит группу с указанным кодом.
Процессы, входившие в старую группу, в новую группу входить не будут. После
этого ядро просматривает дескрипторы открытых файлов, закрывает каждый из
этих файлов по алгоритму close и освобождает по алгоритму iput индексы теку-
щего каталога и корня (если он изменялся).
Наконец, ядро освобождает всю выделенную задаче память вместе с соответ-
ствующими областями (по алгоритму detachreg) и переводит процесс в состояние
прекращения существования. Ядро сохраняет в таблице процессов код возврата
функции exit (status), а также суммарное время исполнения процесса и его по-
томков в режиме ядра и режиме задачи. В разделе 7.4 при рассмотрении функции
wait будет показано, каким образом процесс получает информацию о времени вы-
полнения своих потомков. Ядро также создает в глобальном учетном файле за-
пись, которая содержит различную статистическую информацию о выполнении про-
цесса, такую как код идентификации пользователя, использование ресурсов цен-
трального процессора и памяти, объем потоков ввода-вывода, связанных с про-

199

цессом. Пользовательские программы могут в любой момент обратиться к учетно-
му файлу за статистическими данными, представляющими интерес с точки зрения
слежения за функционированием системы и организации расчетов с пользователя-
ми. Ядро удаляет процесс из дерева процессов, а его потомков передает про-
цессу 1 (init). Таким образом, процесс 1 становится законным родителем всех
продолжающих существование потомков завершающегося процесса. Если кто-либо
из потомков прекращает существование, завершающийся процесс посылает процес-
су init сигнал "гибель потомка" для того, чтобы процесс начальной загрузки
мог удалить запись о потомке из таблицы процессов (см. раздел 7.9); кроме
того, завершающийся процесс посылает этот сигнал своему родителю. В типичной
ситуации родительский процесс синхронизирует свое выполнение с завершающимся
потомком с помощью системной функции wait. Прекращая существование, процесс
переключает контекст и ядро может теперь выбирать для исполнения следующий
процесс; ядро с этих пор уже не будет исполнять процесс, прекративший сущес-
твование.
В программе, приведенной на Рисунке 7.15, процесс создает новый процесс,
который печатает свой код идентификации и вызывает системную функцию pause,
приостанавливаясь до получения сигнала. Процесс-родитель печатает PID своего
потомка и завершается, возвращая только что выведенное значение через пара-
метр status. Если бы вызов функции exit отсутствовал, начальная процедура
сделала бы его по выходе процесса из функции main. Порожденный процесс про-
должает ожидать получения сигнала, даже если его родитель уже завершился.


    7.4 ОЖИДАНИЕ ЗАВЕРШЕНИЯ ВЫПОЛНЕНИЯ ПРОЦЕССА



Процесс может синхронизировать продолжение своего выполнения с моментом
завершения потомка, если воспользуется системной функцией wait. Синтаксис
вызова функции:

+------------------------------------------------------------+
| main() |
| { |
| int child; |
| |
| if ((child = fork()) == 0) |
| { |
| printf("PID потомка %d\n",getpid()); |
| pause(); /* приостанов выполнения до получения |
| сигнала */ |
| } |
| /* родитель */ |
| printf("PID потомка %d\n",child); |
| exit(child); |
| } |
+------------------------------------------------------------+

Рисунок 7.15. Пример использования функции exit


pid = wait(stat_addr);
где pid - значение кода идентификации (PID) прекратившего свое существование
потомка, stat_addr - адрес переменной целого типа, в которую будет помещено
возвращаемое функцией exit значение, в пространстве задачи.
Алгоритм функции wait приведен на Рисунке 7.16. Ядро ведет поиск потом-
ков процесса, прекративших существование, и в случае их отсутствия возвраща-
ет ошибку. Если потомок, прекративший существование, обнаружен, ядро переда-
ет его код идентификации и значение, возвращаемое через параметр функции
exit, процессу, вызвавшему функцию wait. Таким образом, через параметр функ-

200

ции exit (status) завершающийся процесс может передавать различные значения,
в закодированном виде содержащие информацию о причине завершения процесса,
однако на практике этот параметр используется по назначению довольно редко.
Ядро передает в соответствующие поля, принадлежащие пространству родитель-
ского процесса, накопленные значения продолжительности исполнения процес-
са-потомка в режиме ядра и в режиме задачи и, наконец, освобождает в таблице
процессов место, которое в ней занимал прежде прекративший существование
процесс. Это место будет предоставлено новому процессу.
Если процесс, выполняющий функцию wait, имеет потомков, продолжающих су-
ществование, он приостанавливается до получения ожидаемого сигнала. Ядро не
возобновляет по своей инициативе процесс, приостановившийся с помощью функ-
ции wait: такой процесс может возобновиться только в случае получения сигна-
ла. На все сигналы, кроме сигнала "гибель потомка", процесс реагирует ранее
рассмотренным образом. Реакция процесса на сигнал "гибель потомка" проявля-
ется по-разному в зависимости от обстоятельств:
* По умолчанию (то есть если специально не оговорены никакие другие дейст-
вия) процесс выходит из состояния останова, в которое он вошел с помощью
функции wait, и запускает алгоритм issig для опознания типа поступившего
сигнала. Алгоритм issig (Рисунок 7.7) рассматривает особый случай пос-
тупления сигнала типа "гибель потомка" и возвращает "ложь". Поэтому ядро
не выполняет longjump из функции sleep, а возвращает управление функции
wait. Оно перезапускает функцию wait, находит потомков, прекративших су-
ществование (по крайней мере, одного), освобождает место в таблице про-
цессов, занимаемое этими потомками, и выходит из функции wait, возвращая

+------------------------------------------------------------+
| алгоритм wait |
| входная информация: адрес переменной для хранения значения|
| status, возвращаемого завершающимся |
| процессом |
| выходная информация: идентификатор потомка и код возврата |
| функции exit |
| { |
| если (процесс, вызвавший функцию wait, не имеет потом- |
| ков) |
| возвратить (ошибку); |
| |
| для (;;) /* цикл с внутренним циклом */ |
| { |
| если (процесс, вызвавший функцию wait, имеет потом-|
| ков, прекративших существование) |
| { |
| выбрать произвольного потомка; |
| передать его родителю информацию об использова-|
| нии потомком ресурсов центрального процессора;|
| освободить в таблице процессов место, занимае- |
| мое потомком; |
| возвратить (идентификатор потомка, код возврата|
| функции exit, вызванной потомком); |
| } |
| если (у процесса нет потомков) |
| возвратить ошибку; |
| приостановиться с приоритетом, допускающим прерыва-|
| ния (до завершения потомка); |
| } |
| } |
+------------------------------------------------------------+

Рисунок 7.16. Алгоритм функции wait

201



управление процессу, вызвавшему ее.
* Если процессы принимает сигналы данного типа, ядро делает все необходи-
мые установки для запуска пользовательской функции обработки сигнала,
как и в случае поступления сигнала любого другого типа.
* Если процесс игнорирует сигналы данного типа, ядро перезапускает функцию
wait, освобождает в таблице процессов место, занимаемое потомками, прек-
ратившими существование, и исследует оставшихся потомков.

Например, если пользователь запускает программу, приведенную на Рисунке
7.17, с параметром и без параметра, он получит разные результаты. Сначала
рассмотрим случай, когда пользователь запускает программу без параметра
(единственный параметр - имя программы, то есть argc равно 1). Родительский
процесс порождает 15 потомков, которые в конечном итоге завершают свое вы-