| вывода */ |
| close(to_par[1]); /* закрытие ненужных дескрипторов |
| close(to_chil[0]); канала */ |
| close(to_par[0]); |
| close(to_chil[1]); |
| for (;;) |
| { |
| if ((count = read(0,buf,sizeof(buf))) == 0) |
| exit(); |
| write(1,buf,count); |
| } |
| } |
| /* выполнение родительского процесса */ |
| close(1); /* перенастройка стандартного ввода-вывода */|
| dup(to_chil[1]); |
| close(0); |
| dup(to_par[0]); |
| close(to_chil[1]); |
| close(to_par[0]); |
| close(to_chil[0]); |
| close(to_par[1]); |
| for (i = 0; i < 15; i++) |
| { |
| write(1,string,strlen(string)); |
| read(0,buf,sizeof(buf)); |
| } |
| } |
+------------------------------------------------------------+

Рисунок 7.5. Использование функций pipe, dup и fork

186


которых возвратила функция pipe - хорошая традиция, в чем нам еще предстоит
убедиться. В результате, когда родительский процесс переписывает данные в
стандартный вывод, запись ведется в канал to_chil и данные поступают к по-
рожденному процессу, который считывает их через свой стандартный ввод. Когда
же порожденный процесс пишет данные в стандартный вывод, запись ведется в
канал to_par и данные поступают к родительскому процессу, считывающему их
через свой стандартный ввод. Так через два канала оба процесса обмениваются
сообщениями.
Результаты этой программы не зависят от того, в какой очередности про-
цессы выполняют свои действия. Таким образом, нет никакой разницы, возвраща-
ется ли управление родительскому процессу из функции fork раньше или позже,
чем порожденному процессу. И так же безразличен порядок, в котором процессы
вызывают системные функции перед тем, как войти в свой собственный цикл, ибо
они используют идентичные структуры ядра. Если процесс-потомок исполняет
функцию read раньше, чем его родитель выполнит write, он будет приостановлен
до тех пор, пока родительский процесс не произведет запись в канал и тем са-
мым не возобновит выполнение потомка. Если родительский процесс записывает в
канал до того, как его потомок приступит к чтению из канала, первый процесс
не сможет в свою очередь считать данные из стандартного ввода, пока второй
процесс не прочитает все из своего стандартного ввода и не произведет запись
данных в стандартный вывод. С этого места порядок работы жестко фиксирован:
каждый процесс завершает выполнение функций read и write и не может выпол-
нить следующую операцию read до тех пор, пока другой процесс не выполнит па-
ру read-write. Родитель-
ский процесс после 15 итераций завершает работу; порожденный процесс натал-
кивается на конец файла ("end-of-file"), поскольку канал не связан больше ни
с одним из записывающих процессов, и тоже завершает работу. Если порожденный
процесс попытается произвести запись в канал после завершения родительского
процесса, он получит сигнал о том, что канал не связан ни с одним из процес-
сов чтения.
Мы упомянули о том, что хорошей традицией в программировании является
закрытие ненужных файловых дескрипторов. В пользу этого говорят три довода.
Во-первых, дескрипторы файлов постоянно находятся под контролем системы, ко-
торая накладывает ограничение на их количество. Во-вторых, во время исполне-
ния порожденного процесса присвоение дескрипторов в новом контексте сохраня-
ется (в чем мы еще убедимся). Закрытие ненужных файлов до запуска процесса
открывает перед программами возможность исполнения в "стерильных" условиях,
свободных от любых неожиданностей, имея открытыми только файлы стандартного
ввода-вывода и ошибок. Наконец, функция read для канала возвращает признак
конца файла только в том случае, если канал не был открыт для записи ни од-
ним из процессов. Если считывающий процесс будет держать дескриптор записи в
канал открытым, он никогда не узнает, закрыл ли записывающий процесс работу
на своем конце канала или нет. Вышеприведенная программа не работала бы над-
лежащим образом, если бы перед входом в цикл выполнения процессом-потомком
не были закрыты дескрипторы записи в канал.


7.2 СИГНАЛЫ

Сигналы сообщают процессам о возникновении асинхронных событий. Посылка
сигналов производится процессами - друг другу, с помощью функции kill, - или
ядром. В версии V (вторая редакция) системы UNIX существуют 19 различных
сигналов, которые можно классифицировать следующим образом:
* Сигналы, посылаемые в случае завершения выполнения процесса, то есть
тогда, когда процесс выполняет функцию exit или функцию signal с пара-
метром death of child (гибель потомка);
* Сигналы, посылаемые в случае возникновения вызываемых процессом особых
ситуаций, таких как обращение к адресу, находящемуся за пределами вирту-

187

ального адресного пространства процесса, или попытка записи в область
памяти, открытую только для чтения (например, текст программы), или по-
пытка исполнения привилегированной команды, а также различные аппаратные
ошибки;
* Сигналы, посылаемые во время выполнения системной функции при возникно-
вении неисправимых ошибок, таких как исчерпание системных ресурсов во
время выполнения функции exec после освобождения исходного адресного
пространства (см. раздел 7.5);
* Сигналы, причиной которых служит возникновение во время выполнения сис-
темной функции совершенно неожиданных ошибок, таких как обращение к не-
существующей системной функции (процесс передал номер системной функции,
который не соответствует ни одной из имеющихся функций), запись в канал,
не связанный ни с одним из процессов чтения, а также использование недо-
пустимого значения в параметре "reference" системной функции lseek. Ка-
залось бы, более логично в таких случаях вместо посылки сигнала возвра-
щать код ошибки, однако с практической точки зрения для аварийного за-
вершения процессов, в которых возникают подобные ошибки, более предпоч-
тительным является именно использование сигналов (*);
* Сигналы, посылаемые процессу, который выполняется в режиме задачи, нап-
ример, сигнал тревоги (alarm), посылаемый по истечении определенного пе-
риода времени, или произвольные сигналы, которыми обмениваются процессы,
использующие функцию kill;
* Сигналы, связанные с терминальным взаимодействием, например, с "зависа-
нием" терминала (когда сигнал-носитель на терминальной линии прекращает-
ся по любой причине) или с нажатием клавиш "break" и "delete" на клавиа-
туре терминала;
* Сигналы, с помощью которых производится трассировка выполнения процесса.
Условия применения сигналов каждой группы будут рассмотрены в этой и
последующих главах.
Концепция сигналов имеет несколько аспектов, связанных с тем, каким об-
разом ядро посылает сигнал процессу, каким образом процесс обрабатывает сиг-
нал и управляет реакцией на него. Посылая сигнал процессу, ядро устанавлива-
ет в единицу разряд в поле сигнала записи таблицы процессов, соответствующий
типу сигнала. Если процесс находится в состоянии приостанова с приоритетом,
допускающим прерывания, ядро возобновит его выполнение. На этом роль отпра-
вителя сигнала (процесса или ядра) исчерпывается. Процесс может запоминать
сигналы различных типов, но не имеет возможности запоминать количество полу-
чаемых сигналов каждого типа. Например, если процесс получает сигнал о "за-
висании" или об удалении процесса из системы, он устанавливает в единицу со-
ответствующие разряды в поле сигналов таблицы процессов, но не может ска-
зать, сколько экземпляров сигнала каждого типа он получил.
Ядро проверяет получение сигнала, когда процесс собирается перейти из
режима ядра в режим задачи, а также когда он переходит в состояние приоста-
нова или выходит из этого состояния с достаточно низким приоритетом планиро-
вания (см. Рисунок 7.6). Ядро обрабатывает сигналы только тогда, когда про-
цесс возвращается из режима ядра в режим задачи. Таким образом, сигнал не
оказывает немедленного воздействия на поведение процесса, исполняемого в ре-
жиме ядра. Если процесс исполняется в режиме задачи, а ядро тем временем об-
рабатывает прерывание, послужившее поводом для посылки процессу сигнала, яд-
ро распознает и обработает сигнал по выходе из прерывания. Таким образом,
процесс не будет исполняться в режиме задачи, пока какие-то сигналы остаются
необработанными.
На Рисунке 7.7 представлен алгоритм, с помощью которого ядро определяет,

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


188


Выполняется в
режиме задачи
+-------+
| | Проверка
| 1 | и
Вызов функ- | | + - обработка
ции, преры- ++------+ -+ - сигналов
вание | ^ ^- -+-
Преры- +-----+ +-------+ |- -|- - +
вание, | | | +-------+ +---+ Возврат в
возврат| | | | Возврат | режим задачи
из пре-| | | | |
рыва-| v v | Выполняет- |
+-------+ ния | +------++ся в режи- ++------+
| | +-->| |ме ядра | |
| 9 |<-----------+ 2 +------------>| 7 |
| | Выход | | Резервирует-| |
+-------+ ++------+ ся +-------+
Прекращение | ^ - Зарезер-
существования |- - -|- - - - - - - - + - вирован
| |- - - - - - - + + -- -+
+---------------+ +------+ -------- Проверка
| Приостанов Запуск | - + - - - сигналов
v | -
При-+-------+ +-+-----+ Готов к
ос- | | Возобновление | | запуску
та- | 4 +----------------------->| 3 | в памяти
нов-| | | |
лен +---+---+ ++------+
в па- | | ^ ^
мяти | | | | Достаточно
| | | | памяти
| | | +---+
| Вы- Вы- | | |
| грузка грузка | | | Создан
| | |За- ++------+
| | |груз-| | fork
| | |ка | 8 |<-----
| | | | |
| | | ++------+
| | | |
| | | | Недоста-
| | | +---+ точно
| | | | памяти
| | | | (только система
| | | | подкачки)
v v | v
+-------+ +---+---+
| | Возобновление | |
| 6 +----------------------->| 5 |
| | | |
+-------+ +-------+
Приостановлен, Готов к запуску,
выгружен выгружен

Рисунок 7.6. Диаграмма переходов процесса из состояние в состояние с
указанием моментов проверки и обработки сигналов


189

получил ли процесс сигнал или нет. Условия, в которых формируются сигналы
типа "гибель потомка", будут рассмотрены позже. Мы также увидим, что процесс
может игнорировать отдельные сигналы, если воспользуется функцией signal. В
алгоритме issig ядро просто гасит индикацию тех сигналов, на которые процесс
не желает обращать внимание, и привлекает внимание процесса ко всем осталь-
ным сигналам.
+------------------------------------------------------------+
| алгоритм issig /* проверка получения сигналов */ |
| входная информация: отсутствует |
| выходная информация: "истина", если процесс получил сигна- |
| лы, которые его интересуют |
| "ложь" - в противном случае |
| { |
| выполнить пока (поле в записи таблицы процессов, содер- |
| жащее индикацию о получении сигнала, хранит ненулевое |
| значение) |
| { |
| найти номер сигнала, посланного процессу; |
| если (сигнал типа "гибель потомка") |
| { |
| если (сигналы данного типа игнорируются) |
| освободить записи таблицы процессов, которые |
| соответствуют потомкам, прекратившим существо-|
| вание; |
| в противном случае если (сигналы данного типа при-|
| нимаются) |
| возвратить (истину); |
| } |
| в противном случае если (сигнал не игнорируется) |
| возвратить (истину); |
| сбросить (погасить) сигнальный разряд, установленный |
| в соответствующем поле таблицы процессов, хранящем |
| индикацию получения сигнала; |
| } |
| возвратить (ложь); |
| } |
+------------------------------------------------------------+

Рисунок 7.7. Алгоритм опознания сигналов



    7.2.1 Обработка сигналов



Ядро обрабатывает сигналы в контексте того процесса, который получает
их, поэтому чтобы обработать сигналы, нужно запустить процесс. Существует
три способа обработки сигналов: процесс завершается по получении сигнала, не
обращает внимание на сигнал или выполняет особую (пользовательскую) функцию
по его получении. Реакцией по умолчанию со стороны процесса, исполняемого в
режиме ядра, является вызов функции exit, однако с помощью функции signal
процесс может указать другие специальные действия, принимаемые по получении
тех или иных сигналов.
Синтаксис вызова системной функции signal:
oldfunction = signal(signum,function);
где signum - номер сигнала, при получении которого будет выполнено действие,
связанное с запуском пользовательской функции, function - адрес функции,
oldfunction - возвращаемое функцией значение. Вместо адреса функции процесс
может передавать вызываемой процедуре signal числа 1 и 0: если function = 1,
процесс будет игнорировать все последующие поступления сигнала с номером

190

signum (особый случай, связанный с игнорированием сигнала "гибель потомка",
рассматривается в разделе 7.4), если = 0 (значение по умолчанию), процесс по
получении сигнала в режиме ядра завершается. В пространстве процесса поддер-
живается массив полей для обработки сигналов, по одному полю на каждый опре-
деленный в системе сигнал. В поле, соответствующем сигналу с указанным номе-
ром, ядро сохраняет адрес пользовательской функции, вызываемой по получении
сигнала процессом. Способ обработки сигналов одного типа не влияет на обра-
ботку сигналов других типов.

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

Рисунок 7.8. Алгоритм обработки сигналов


Обрабатывая сигнал (Рисунок 7.8), ядро определяет тип сигнала и очищает
(гасит) разряд в записи таблицы процессов, соответствующий данному типу сиг-
нала и установленный в момент получения сигнала процессом. Если функции об-
работки сигнала присвоено значение по умолчанию, ядро в отдельных случаях
перед завершением процесса сбрасывает на внешний носитель (дампирует) образ
процесса в памяти (см. упражнение 7.7). Дампирование удобно для программис-

191

тов тем, что позволяет установить причину завершения процесса и посредством
этого вести отладку программ. Ядро дампирует состояние памяти при поступле-
нии сигналов, которые сообщают о каких-нибудь ошибках в выполнении процес-
сов, как например, попытка исполнения запрещенной команды или обращение к
адресу, находящемуся за пределами виртуального адресного пространства про-
цесса. Ядро не дампирует состояние памяти, если сигнал не связан с программ-
ной ошибкой. Например, прерывание, вызванное нажатием клавиш "delete" или
"break" на терминале, имеет своим результатом посылку сигнала, который сооб-
щает о том, что пользователь хочет раньше времени завершить процесс, в то
время как сигнал о "зависании" является свидетельством нарушения связи с ре-
гистрационным терминалом. Эти сигналы не связаны с ошибками в протекании
процесса. Сигнал о выходе (quit), однако, вызывает сброс состояния памяти,
несмотря на то, что он возникает за пределами выполняемого процесса. Этот
сигнал, обычно вызываемый одновременным нажатием клавиш , дает прог-
раммисту возможность получать дамп состояния памяти в любой момент после за-
пуска процесса, что бывает необходимо, если процесс попадает в бесконечный
цикл выполнения одних и тех же команд (зацикливается).
Если процесс получает сигнал, на который было решено не обращать внима-
ние, выполнение процесса продолжается так, словно сигнала и не было. Пос-
кольку ядро не сбрасывает значение соответствующего поля, свидетельствующего
о необходимости игнорирования сигнала данного типа, то когда сигнал поступит
вновь, процесс опять не обратит на него внимание. Если процесс получает сиг-
нал, реагирование на который было признано необходимым, сразу по возвращении
процесса в режим задачи выполняется заранее условленное действие, однако
прежде чем перевести процесс в режим задачи, ядро еще должно предпринять
следующие шаги:
1. Ядро обращается к сохраненному регистровому контексту задачи и выбирает
значения счетчика команд и указателя вершины стека, которые будут возв-
ращены пользовательскому процессу.
2. Сбрасывает в пространстве процесса прежнее значение поля функции обра-
ботки сигнала и присваивает ему значение по умолчанию.
3. Создает новую запись в стеке задачи, в которую, при необходимости выде-
ляя дополнительную память, переписывает значения счетчика команд и ука-
зателя вершины стека, выбранные ранее из сохраненного регистрового кон-
текста задачи. Стек задачи будет выглядеть так, как будто процесс произ-
вел обращение к пользовательской функции (обработки сигнала) в той точ-
ке, где он вызывал системную функцию или где ядро прервало его выполне-
ние (перед опознанием сигнала).
4. Вносит изменения в сохраненный регистровый контекст задачи: устанавлива-
ет значение счетчика команд равным адресу функции обработки сигнала, а
значение указателя вершины стека равным глубине стека задачи.

Таким образом, по возвращении из режима ядра в режим задачи процесс
приступит к выполнению функции обработки сигнала; после ее завершения управ-
ление будет передано на то место в программе пользователя, где было произве-
дено обращение к системной функции или произошло прерывание, тем самым как
бы имитируется выход из системной функции или прерывания.
В качестве примера можно привести программу (Рисунок 7.9), которая при-
нимает сигналы о прерывании (SIGINT) и сама посылает их (в результате выпол-
нения функции kill). На Рисунке 7.10 представлены фрагменты программного ко-
да, полученные в результате дисассемблирования загрузочного модуля в опера-
ционной среде VAX 11/780. При выполнении процесса обращение к библиотечной
процедуре kill имеет адрес (шестнадцатиричный) ee; эта процедура в свою оче-
редь, прежде чем вызвать системную функцию kill, исполняет команду chmk (пе-
ревести процесс в режим ядра) по адресу 10a. Адрес возврата из системной
функции - 10c. Во время исполнения системной функции ядро посылает процессу
сигнал о прерывании. Ядро обращает внимание на этот сигнал тогда, когда про-
цесс собирается вернуться в режим задачи, выбирая из сохраненного регистро-
вого контекста адрес возврата 10c и помещая его в стек задачи. При этом ад-

192

рес функции обработки сигнала, 104, ядро помещает в сохраненный регистровый
контекст задачи. На Рисунке 7.11 показаны различные состояния стека задачи и
сохраненного регистрового контекста.
В рассмотренном алгоритме обработки сигналов имеются некоторые несоот-
ветствия. Первое из них и наиболее важное связано с очисткой перед возвраще-
нием процесса в режим задачи того поля в пространстве процесса, которое со-
держит адрес пользовательской функции обработки сигнала. Если процессу снова
понадобится обработать сигнал, ему опять придется прибегнуть к помощи сис-
темной функции signal. При этом могут возникнуть нежелательные последс-

+-------------------------------------------+
| #include |
| main() |
| { |
| extern catcher(); |
| signal(SIGINT,catcher); |
| kill(0,SIGINT); |
| } |
| |
| catcher() |
| { |
| } |
+-------------------------------------------+

Рисунок 7.9. Исходный текст программы приема сигналов

+--------------------------------------------------------+
| **** VAX DISASSEMBLER **** |
| |
| _main() |
| e4: |
| e6: pushab Ox18(pc) |
| ec: pushl $Ox2 |
| # в следующей строке вызывается функция signal |
| ee: calls $Ox2,Ox23(pc) |
| f5: pushl $Ox2 |
| f7: clrl -(sp) |
| # в следующей строке вызывается библиотечная процеду-|
| ра kill |
| f9: calls $Ox2,Ox8(pc) |
| 100: ret |
| 101: halt |
| 102: halt |
| 103: halt |
| _catcher() |
| 104: |
| 106: ret |
| 107: halt |
| _kill() |
| 108: |
| # в следующей строке вызывается внутреннее прерывание|
| операционной системы |
| 10a: chmk $Ox25 |
| 10c: bgequ Ox6 |
| 10e: jmp Ox14(pc) |
| 114: clrl r0 |
| 116: ret |
+--------------------------------------------------------+
Рисунок 7.10. Результат дисассемблирования программы приема сигналов

193




До После
| | | |
| | +-->+--------------------+
| | Вершина | | Новая запись с вы- |
| | +-- стека --+ | зовом функции |
| | | задачи | |
| | | ---->|Адрес возврата (10c)|
+--------------------+<--+ - +--------------------+
| Стек задачи | - | Стек задачи |
| до | - | до |
| получения сигнала | - | получения сигнала |
+--------------------+ - +--------------------+
Стек задачи - Стек задачи
-
+--------------------+ - +--------------------+
| Адрес возврата | - | Адрес возврата |
| в процессе (10c) -|---------------- | в процессе (104) |
+--------------------+ +--------------------+
| Сохраненный регист-| | Сохраненный регист-|
| ровый контекст за- | | ровый контекст за- |
| дачи | | дачи |
+--------------------+ +--------------------+
Системный контекстный Системный контекстный
уровень 1 уровень 1
Область сохранения Область сохранения
регистров регистров

Рисунок 7.11. Стек задачи и область сохранения структур ядра
до и после получения сигнала


твия: например, могут создасться условия для конкуренции, если второй раз
сигнал поступит до того, как процесс получит возможность запустить системную
функцию. Поскольку процесс выполняется в режиме задачи, ядру следовало бы
произвести переключение контекста, чтобы увеличить тем самым шансы процесса
на получение сигнала до момента сброса значения поля функции обработки сиг-
нала.
Эту ситуацию можно разобрать на примере программы, представленной на Ри-