The design of the unix operating system by Maurice J

Вид материалаРеферат
7.3 Завершение выполнения процесса
7.4 Ожидание завершения выполнения процесса
Подобный материал:
1   ...   22   23   24   25   26   27   28   29   ...   55

│ 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 (;;) │

│ ; │

│ } │

L-------------------------------------------------------------


Рисунок 7.12. Программа, демонстрирующая возникновение сопер-

ничества между процессами в ходе обработки сиг-

налов


исполнять циклический набор команд.

Однако, возможна и следующая очередность наступления событий:

1. Порожденный процесс посылает родительскому процессу сигнал о

прерывании.

2. Родительский процесс принимает сигнал и вызывает функцию об-

работки сигнала, но резервируется ядром, которое производит

переключение контекста до того, как функция signal будет выз-

вана повторно.

3. Снова запускается порожденный процесс, который посылает роди-

тельскому процессу еще один сигнал о прерывании.

4. Родительский процесс получает второй сигнал о прерывании, но

перед тем он не успел сделать никаких распоряжений относи-

тельно способа обработки сигнала. Когда выполнение родитель-

ского процесса будет возобновлено, он завершится.

В программе описывается именно такое поведение процессов,

поскольку вызов родительским процессом функции nice приводит к

тому, что ядро будет чаще запускать на выполнение порожденный

процесс.

По словам Ричи (эти сведения были получены в частной беседе),

сигналы были задуманы как события, которые могут быть как фаталь-

ными, так и проходящими незаметно, которые не всегда обрабатыва-

ются, поэтому в ранних версиях системы конкуренция процессов,

связанная с посылкой сигналов, не фиксировалась. Тем не менее,

она представляет серьезную проблему в тех программах, где осу-

ществляется прием сигналов. Эта проблема была бы устранена, если

бы поле описания сигнала не очищалось по его получении. Однако,

такое решение породило бы новую проблему: если поступающий сигнал

принимается, а поле очищено, вложенные обращения к функции обра-

ботки сигнала могут переполнить стек. С другой стороны, ядро мог-

ло бы сбросить значение функции обработки сигнала, тем самым де-

лая распоряжение игнорировать сигналы данного типа до тех пор,

пока пользователь вновь не укажет, что нужно делать по получении

подобных сигналов. Такое решение предполагает потерю информации,

так как процесс не в состоянии узнать, сколько сигналов им было

получено. Однако, информации при этом теряется не больше, чем в

том случае, когда процесс получает большое количество сигналов

одного типа до того, как получает возможность их обработать. В

системе BSD, наконец, процесс имеет возможность блокировать полу-

чение сигналов и снимать блокировку при новом обращении к систем-

ной функции; когда процесс снимает блокировку сигналов, ядро по-

сылает процессу все сигналы, отложенные (повисшие) с момента ус-

тановки блокировки. Когда процесс получает сигнал, ядро автомати-

чески блокирует получение следующего сигнала до тех пор, пока

функция обработки сигнала не закончит работу. В этих действиях

ядра наблюдается аналогия с тем, как ядро реагирует на аппаратные

прерывания: оно блокирует появление новых прерываний на время об-

работки предыдущих.

Второе несоответствие в обработке сигналов связано с приемом

сигналов, поступающих во время исполнения системной функции, ког-

да процесс приостановлен с допускающим прерывания приоритетом.

Сигнал побуждает процесс выйти из приостанова (с помощью

longjump), вернуться в режим задачи и вызвать функцию обработки

сигнала. Когда функция обработки сигнала завершает работу, проис-

ходит то, что процесс выходит из системной функции с ошибкой, со-

общающей о прерывании ее выполнения. Узнав об ошибке, пользова-

тель запускает системную функцию повторно, однако более удобно

было бы, если бы это действие автоматически выполнялось ядром,

как в системе BSD.

Третье несоответствие проявляется в том случае, когда процесс

игнорирует поступивший сигнал. Если сигнал поступает в то время,

когда процесс находится в состоянии приостанова с допускающим

прерывания приоритетом, процесс возобновляется, но не выполняет

longjump. Другими словами, ядро узнает о том, что процесс проиг-

норировал поступивший сигнал только после возобновления его вы-

полнения. Логичнее было бы оставить процесс в состоянии приоста-

нова. Однако, в момент посылки сигнала к пространству процесса, в

котором ядро хранит адрес функции обработки сигнала, может от-

сутствовать доступ. Эта проблема может быть решена путем запоми-

нания адреса функции обработки сигнала в записи таблицы процес-

сов, обращаясь к которой, ядро получало бы возможность решать

вопрос о необходимости возобновления процесса по получении сигна-

ла. С другой стороны, процесс может немедленно вернуться в состо-

яние приостанова (по алгоритму sleep), если обнаружит, что в его

возобновлении не было необходимости. Однако, пользовательские

процессы не имеют возможности осознавать собственное возобновле-

ние, поскольку ядро располагает точку входа в алгоритм sleep

внутри цикла с условием продолжения (см. главу 2), переводя про-

цесс вновь в состояние приостанова, если ожидаемое процессом со-

бытие в действительности не имело места.

Ко всему сказанному выше следует добавить, что ядро обрабаты-

вает сигналы типа "гибель потомка" не так, как другие сигналы. В

частности, когда процесс узнает о получении сигнала "гибель по-

томка", он выключает индикацию сигнала в соответствующем поле за-

писи таблицы процессов и по умолчанию действует так, словно ника-

кого сигнала и не поступало. Назначение сигнала "гибель потомка"

состоит в возобновлении выполнения процесса, приостановленного с

допускающим прерывания приоритетом. Если процесс принимает такой

сигнал, он, как и во всех остальных случаях, запускает функцию

обработки сигнала. Действия, предпринимаемые ядром в том случае,

когда процесс игнорирует поступивший сигнал этого типа, будут

описаны в разделе 7.4. Наконец, когда процесс вызвал функцию

signal с параметром "гибель потомка" (death of child), ядро посы-

лает ему соответствующий сигнал, если он имеет потомков, прекра-

тивших существование. В разделе 7.4 на этом моменте мы остановим-

ся более подробно.


G7.2.2 Группы процессовH


Несмотря на то, что в системе 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 завершается неудачно.


-------------------------------------------------------------┐

│ #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); │

│ } │

L-------------------------------------------------------------


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


В программе, приведенной на Рисунке 7.13, главный процесс

сбрасывает установленное ранее значение номера группы и порождает

10 новых процессов. При рождении каждый процесс-потомок наследует

номер группы процессов своего родителя, однако, процессы, создан-

ные в нечетных итерациях цикла, сбрасывают это значение. Систем-

ные функции getpid и getpgrp возвращают значения кода идентифика-

ции выполняемого процесса и номера группы, в которую он входит, а

функция pause приостанавливает выполнение процесса до момента по-

лучения сигнала. В конечном итоге родительский процесс запускает

функцию kill и посылает сигнал о прерывании всем процессам, вхо-

дящим в одну с ним группу. Ядро посылает сигнал пяти "четным"

процессам, не сбросившим унаследованное значение номера группы,

при этом пять "нечетных" процессов продолжают свое выполнение.


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


В системе UNIX процесс завершает свое выполнение, запуская

системную функцию exit. После этого процесс переходит в состояние

"прекращения существования" (см. Рисунок 6.1), освобождает ресур-

сы и ликвидирует свой контекст. Синтаксис вызова функции:

exit(status);

где status - значение, возвращаемое функцией родительскому про-

цессу. Процессы могут вызывать функцию exit как в явном, так и в

неявном виде (по окончании выполнения программы: начальная проце-

дура (startup), компонуемая со всеми программами на языке Си, вы-

зывает функцию exit на выходе программы из функции main, являю-

щейся общей точкой входа для всех программ). С другой стороны,

ядро может вызывать функцию exit по своей инициативе, если про-

цесс не принял посланный ему сигнал (об этом мы уже говорили вы-

ше). В этом случае значение параметра status равно номеру сигна-

ла.

Система не накладывает никакого ограничения на продолжитель-

ность выполнения процесса, и зачастую процессы существуют в тече-

ние довольно длительного времени. Нулевой процесс (программа под-

качки) и процесс 1 (init), к примеру, существуют на протяжении

всего времени жизни системы. Продолжительными процессами являются

также getty-процессы, контролирующие работу терминальной линии,

ожидая регистрации пользователей, и процессы общего назначения,

выполняемые под руководством администратора.


-------------------------------------------------------------┐

│ алгоритм exit │

│ входная информация: код, возвращаемый родительскому про- │

│ цессу │

│ выходная информация: отсутствует │

│ { │

│ игнорировать все сигналы; │

│ если (процесс возглавляет группу процессов, ассоцииро- │

│ ванную с операторским терминалом) │

│ { │

│ послать всем процессам, входящим в группу, сигнал о │

│ "зависании"; │

│ сбросить в ноль код группы процессов; │

│ } │

│ закрыть все открытые файлы (внутренняя модификация алго-│

│ ритма close); │

│ освободить текущий каталог (алгоритм iput); │

│ освободить области и память, ассоциированную с процессом│

│ (алгоритм freereg); │

│ создать запись с учетной информацией; │

│ прекратить существование процесса (перевести его в соот-│

│ ветствующее состояние); │

│ назначить всем процессам-потомкам в качестве родителя │

│ процесс init (1); │

│ если кто-либо из потомков прекратил существование, │

│ послать процессу init сигнал "гибель потомка"; │

│ послать сигнал "гибель потомка" родителю данного процес-│

│ са; │

│ переключить контекст; │

│ } │

L-------------------------------------------------------------


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


На Рисунке 7.14 приведен алгоритм функции exit. Сначала ядро

отменяет обработку всех сигналов, посылаемых процессу, поскольку

ее продолжение становится бессмысленным. Если процесс, вызывающий

функцию exit, возглавляет группу процессов, ассоциированную с

операторским терминалом (см. раздел 10.3.5), ядро делает предпо-

ложение о том, что пользователь прекращает работу, и посылает

всем процессам в группе сигнал о "зависании". Таким образом, если

пользователь в регистрационном shell'е нажмет последовательность

клавиш, означающую "конец файла" (Ctrl-d), при этом с терминалом

остались связанными некоторые из существующих процессов, процесс,

выполняющий функцию exit, пошлет им всем сигнал о "зависании".

Кроме того, ядро сбрасывает в ноль значение кода группы процессов

для всех процессов, входящих в данную группу, поскольку не исклю-

чена возможность того, что позднее текущий код идентификации про-

цесса (процесса, который вызвал функцию exit) будет присвоен дру-

гому процессу и тогда последний возглавит группу с указанным

кодом. Процессы, входившие в старую группу, в новую группу вхо-

дить не будут. После этого ядро просматривает дескрипторы откры-

тых файлов, закрывает каждый из этих файлов по алгоритму close и

освобождает по алгоритму iput индексы текущего каталога и корня

(если он изменялся).

Наконец, ядро освобождает всю выделенную задаче память вместе

с соответствующими областями (по алгоритму detachreg) и переводит

процесс в состояние прекращения существования. Ядро сохраняет в

таблице процессов код возврата функции exit (status), а также

суммарное время исполнения процесса и его потомков в режиме ядра

и режиме задачи. В разделе 7.4 при рассмотрении функции wait бу-

дет показано, каким образом процесс получает информацию о времени

выполнения своих потомков. Ядро также создает в глобальном учет-

ном файле запись, которая содержит различную статистическую ин-

формацию о выполнении процесса, такую как код идентификации поль-

зователя, использование ресурсов центрального процессора и

памяти, объем потоков ввода-вывода, связанных с процессом. Поль-

зовательские программы могут в любой момент обратиться к учетному

файлу за статистическими данными, представляющими интерес с точки

зрения слежения за функционированием системы и организации расче-

тов с пользователями. Ядро удаляет процесс из дерева процессов, а

его потомков передает процессу 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); │

│ } │

L-------------------------------------------------------------


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


pid = wait(stat_addr);

где pid - значение кода идентификации (PID) прекратившего свое

существование потомка, stat_addr - адрес переменной целого типа,

в которую будет помещено возвращаемое функцией exit значение, в

пространстве задачи.

Алгоритм функции wait приведен на Рисунке 7.16. Ядро ведет

поиск потомков процесса, прекративших существование, и в случае

их отсутствия возвращает ошибку. Если потомок, прекративший су-

ществование, обнаружен, ядро передает его код идентификации и

значение, возвращаемое через параметр функции exit, процессу,

вызвавшему функцию wait. Таким образом, через параметр функции

exit (status) завершающийся процесс может передавать различные