| } |
+-------------------------------------------------------+
+-------------------------------------------+
| original brk value 140924 |
| caught sig 11 1th call at addr 141312 |
| caught sig 11 2th call at addr 141312 |
| caught sig 11 3th call at addr 143360 |
| ...(тот же адрес печатается до 10-го |
| вызова подпрограммы sbrk) |
| caught sig 11 10th call at addr 143360 |
| caught sig 11 11th call at addr 145408 |
| ...(тот же адрес печатается до 18-го |
| вызова подпрограммы sbrk) |
| caught sig 11 18th call at addr 145408 |
| caught sig 11 19th call at addr 145408 |
| - |
| - |
+-------------------------------------------+

Рисунок 7.27. Пример программы, использующей функцию brk, и
результаты ее контрольного прогона


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

217

+------------------------------------------------------------+
| /* чтение командной строки до символа конца файла */ |
| while (read(stdin,buffer,numchars)) |
| { |
| /* синтаксический разбор командной строки */ |
| if (/* командная строка содержит & */) |
| amper = 1; |
| else |
| amper = 0; |
| /* для команд, не являющихся конструкциями командного |
| языка shell */ |
| if (fork() == 0) |
| { |
| /* переадресация ввода-вывода ? */ |
| if (/* переадресация вывода */) |
| { |
| fd = creat(newfile,fmask); |
| close(stdout); |
| dup(fd); |
| close(fd); |
| /* stdout теперь переадресован */ |
| } |
| if (/* используются каналы */) |
| { |
| pipe(fildes); |
| |
+------------------------------------------------------------+

Рисунок 7.28. Основной цикл программы shell


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


    7.8 КОМАНДНЫЙ ПРОЦЕССОР SHELL



Теперь у нас есть достаточно материала, чтобы перейти к объяснению прин-
ципов работы командного процессора shell. Сам командный процессор намного
сложнее, чем то, что мы о нем здесь будем излагать, однако взаимодействие
процессов мы уже можем рассмотреть на примере реальной программы. На Рисунке
7.28 приведен фрагмент основного цикла программы shell, демонстрирующий
асинхронное выполнение процессов, переназначение вывода и использование ка-
налов.
Shell считывает командную строку из файла стандартного ввода и интерпре-
тирует ее в соответствии с установленным набором правил. Дескрипторы файлов
стандартного ввода и стандартного вывода, используемые регистрационным
shell'ом, как правило, указывают на терминал, с которого пользователь регис-
трируется в системе (см. главу 10). Если shell узнает во введенной строке
конструкцию собственного командного языка (например, одну из команд cd, for,
while и т.п.), он исполняет команду своими силами, не прибегая к созданию
новых процессов; в противном случае команда интерпретируется как имя испол-
няемого файла.
Командные строки простейшего вида содержат имя программы и несколько па-
раметров, например:

who
grep -n include *.c
ls -l


218

+------------------------------------------------------------+
| if (fork() == 0) |
| { |
| /* первая компонента командной строки */|
| close(stdout); |
| dup(fildes[1]); |
| close(fildes[1]); |
| close(fildes[0]); |
| /* стандартный вывод направляется в ка- |
| нал */ |
| /* команду исполняет порожденный про- |
| цесс */ |
| execlp(command1,command1,0); |
| } |
| /* вторая компонента командной строки */ |
| close(stdin); |
| dup(fildes[0]); |
| close(fildes[0]); |
| close(fildes[1]); |
| /* стандартный ввод будет производиться из|
| канала */ |
| } |
| execve(command2,command2,0); |
| } |
| /* с этого места продолжается выполнение родительского |
| * процесса... |
| * процесс-родитель ждет завершения выполнения потомка,|
| * если это вытекает из введенной строки |
| * / |
| if (amper == 0) |
| retid = wait(&status); |
| } |
+------------------------------------------------------------+

Рисунок 7.28. Основной цикл программы shell (продолжение)



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

nroff -mm bigdocument &

shell анализирует наличие символа амперсанд (&) и заносит результат проверки
во внутреннюю переменную amper. В конце основного цикла shell обращается к
этой переменной и, если обнаруживает в ней признак наличия символа, не вы-
полняет функцию wait, а тут же повторяет цикл считывания следующей команды.

Из рисунка видно, что процесс-потомок по завершении функции fork получа-
ет доступ к командной строке, принятой shell'ом. Для того, чтобы переадресо-
вать стандартный вывод в файл, как в следующем примере

nroff -mm bigdocument > output

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

219

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

+-----------+
| Shell |
+-----+-----+ wait
| ^
| |
+-----+-----+ exit
| wc |
+-----+-----+ read
| ^
| |
+-----+-----+ write
| ls - l |
+-----------+

Рисунок 7.29. Взаимосвязь между процессами, исполняющими ко-
мандную строку ls -l|wc


Из приведенного текста программы видно, как shell обрабатывает командную
строку, используя один канал. Допустим, что командная строка имеет вид:
ls -l|wc
После создания родительским процессом нового процесса процесс-потомок созда-
ет канал. Затем процесс-потомок создает свое ответвление; он и его потомок
обрабатывают по одной компоненте командной строки. "Внучатый" процесс испол-
няет первую компоненту строки (ls): он собирается вести запись в канал, поэ-
тому он закрывает старый файл стандартного вывода, передает его дескриптор
каналу и закрывает старый дескриптор записи в канал, в котором (в дескрипто-
ре) уже нет необходимости. Родитель (wc) "внучатого" процесса (ls) является
потомком основного процесса, реализующего программу shell'а (см. Рисунок
7.29). Этот процесс (wc) закрывает свой файл стандартного ввода и передает
его дескриптор каналу, в результате чего канал становится файлом стандартно-
го ввода. Затем закрывается старый и уже не нужный дескриптор чтения из ка-
нала и исполняется вторая компонента командной строки. Оба порожденных про-
цесса выполняются асинхронно, причем выход одного процесса поступает на вход
другого. Тем временем основной процесс дожидается завершения своего потомка
(wc), после чего продолжает свою обычную работу: по завершении процесса, вы-
полняющего команду wc, вся командная строка является обработанной. Shell
возвращается в цикл и считывает следующую командную строку.


    7.9 ЗАГРУЗКА СИСТЕМЫ И НАЧАЛЬНЫЙ ПРОЦЕСС



Для того, чтобы перевести систему из неактивное состояние в активное,
администратор выполняет процедуру "начальной загрузки". На разных машинах
эта процедура имеет свои особенности, однако во всех случаях она реализует
одну и ту же цель: загрузить копию операционной системы в основную память
машины и запустить ее на исполнение. Обычно процедура начальной загрузки
включает в себя несколько этапов. Переключением клавиш на пульте машины ад-
министратор может указать адрес специальной программы аппаратной загрузки, а
может, нажав только одну клавишу, дать команду машине запустить процедуру
загрузки, исполненную в виде микропрограммы. Эта программа может состоять из
нескольких команд, подготавливающих запуск другой программы. В системе UNIX

220

процедура начальной загрузки заканчивается считыванием с диска в память бло-
ка начальной загрузки (нулевого блока). Программа, содержащаяся в этом бло-
ке, загружает из файловой системы ядро ОС (например, из файла с именем
"/unix" или с другим именем, указанным администратором). После загрузки ядра
системы в память управление передается по стартовому адресу ядра и ядро за-
пускается на выполнение (алгоритм start, Рисунок 7.30).
Ядро инициализирует свои внутренние структуры данных. Среди прочих
структур ядро создает связные списки свободных буферов и индексов, хеш-оче-
реди для буферов и индексов, инициализирует структуры областей, точки входа
в таблицы страниц и т.д. По окончании этой фазы ядро монтирует корневую фай-
ловую систему и формирует среду выполнения нулевого процесса, среди всего
прочего создавая пространство процесса, инициализируя нулевую точку входа в
таблице процесса и делая корневой каталог текущим для процесса.
Когда формирование среды выполнения процесса заканчивается, система ис-
полняется уже в виде нулевого процесса. Нулевой процесс "ветвится", запуская
алгоритм fork прямо из ядра, поскольку сам процесс исполняется в режиме яд-

+------------------------------------------------------------+
| алгоритм start /* процедура начальной загрузки системы */|
| входная информация: отсутствует |
| выходная информация: отсутствует |
| { |
| проинициализировать все структуры данных ядра; |
| псевдо-монтирование корня; |
| сформировать среду выполнения процесса 0; |
| создать процесс 1; |
| { |
| /* процесс 1 */ |
| выделить область; |
| подключить область к адресному пространству процесса|
| init; |
| увеличить размер области для копирования в нее ис- |
| полняемого кода; |
| скопировать из пространства ядра в адресное прост- |
| ранство процесса код программы, исполняемой процес-|
| сом; |
| изменить режим выполнения: вернуться из режима ядра |
| в режим задачи; |
| /* процесс init далее выполняется самостоятельно -- |
| * в результате выхода в режим задачи, |
| * init исполняет файл "/etc/init" и становится |
| * "обычным" пользовательским процессом, производя- |
| * щим обращения к системным функциям |
| */ |
| } |
| /* продолжение нулевого процесса */ |
| породить процессы ядра; |
| /* нулевой процесс запускает программу подкачки, управ- |
| * ляющую распределением адресного пространства процес- |
| * сов между основной памятью и устройствами выгрузки. |
| * Это бесконечный цикл; нулевой процесс обычно приоста-|
| * навливает свою работу, если необходимости в нем боль-|
| * ше нет. |
| */ |
| исполнить программу, реализующую алгоритм подкачки; |
| } |
+------------------------------------------------------------+

Рисунок 7.30. Алгоритм загрузки системы

221

ра. Порожденный нулевым новый процесс, процесс 1, запускается в том же режи-
ме и создает свой пользовательский контекст, формируя область данных и при-
соединяя ее к своему адресному пространству. Он увеличивает размер области
до надлежащей величины и переписывает программу загрузки из адресного прост-
ранства ядра в новую область: эта программа теперь будет определять контекст
процесса 1. Затем процесс 1 сохраняет регистровый контекст задачи, "возвра-
щается" из режима ядра в режим задачи и исполняет только что переписанную
программу. В отличие от нулевого процесса, который является процессом сис-
темного уровня, выполняющимся в режиме ядра, процесс 1 относится к пользова-
тельскому уровню. Код, исполняемый процессом 1, включает в себя вызов сис-
темной функции exec, запускающей на выполнение программу из файла
"/etc/init". Обычно процесс 1 именуется процессом init, поскольку он отвеча-
ет за инициализацию новых процессов.
Казалось бы, зачем ядру копировать программу, запускаемую с помощью фун-
кции exec, в адресное пространство процесса 1 ? Он мог бы обратиться к внут-
реннему варианту функции прямо из ядра, одна-
ко, по сравнению с уже описанным алгоритмом это было бы гораздо труднее реа-
лизовать, ибо в этом случае функции exec пришлось бы производить анализ имен
файлов в пространстве ядра, а не в пространстве задачи. Подобная деталь,
требующаяся только для процесса init, усложнила бы программу реализации фун-
кции exec и отрицательно отразилась бы на скорости выполнения функции в бо-
лее общих случаях.
Процесс init (Рисунок 7.31) выступает диспетчером процессов, который по-
рождает процессы, среди всего прочего позволяющие пользователю регистриро-
ваться в системе. Инструкции о том, какие процессы нужно создать, считывают-
ся процессом init из файла "/etc/inittab". Строки файла включают в себя
идентификатор состояния "id" (однопользовательский режим, многопользователь-
ский и т. д.), предпринимаемое действие (см. упражнение 7.43) и спецификацию
программы, реализующей это действие (см. Рисунок 7.32). Процесс init прос-
матривает строки файла до тех пор, пока не обнаружит идентификатор состоя-
ния, соответствующего тому состоянию, в котором находится процесс, и создает
процесс, исполняющий программу с указанной спецификацией. Например, при за-
пуске в многопользовательском режиме (состояние 2) процесс init обычно по-
рождает getty-процессы, управляющие функционированием терминальных линий,
входящих в состав системы. Если регистрация пользователя прошла успешно,
getty-процесс, пройдя через процедуру login, запускает на исполнение регист-
рационный shell (см. главу 10). Тем временем процесс init находится в состо-
янии ожидания (wait), наблюдая за прекращением существования своих потомков,
а также "внучатых" процессов, оставшихся "сиротами" после гибели своих роди-
телей.
Процессы в системе UNIX могут быть либо пользовательскими, либо управля-
ющими, либо системными. Большинство из них составляют пользовательские про-
цессы, связанные с пользователями через терминалы. Управляющие процессы не
связаны с конкретными пользователями, они выполняют широкий спектр системных
функций, таких как администрирование и управление сетями, различные периоди-
ческие операции, буферизация данных для вывода на устройство построчной пе-
чати и т.д. Процесс init может порождать управляющие процессы, которые будут
существовать на протяжении всего времени жизни системы, в различных случаях
они могут быть созданы самими пользователями. Они похожи на пользовательские
процессы тем, что они исполняются в режиме задачи и прибегают к услугам сис-
темы путем вызова соответствующих системных функций.
Системные процессы выполняются исключительно в режиме ядра. Они могут
порождаться нулевым процессом (например, процесс замещения страниц vhand),
который затем становится процессом подкачки. Системные процессы похожи на
управляющие процессы тем, что они выполняют системные функции, при этом они
обладают большими возможностями приоритетного выполнения, поскольку лежащие
в их основе программные коды являются составной частью ядра. Они могут обра-
щаться к структурам данных и алгоритмам ядра, не прибегая к вызову системных
функций, отсюда вытекает их исключительность. Однако, они не обладают такой

222

+------------------------------------------------------------+
| алгоритм init /* процесс init, в системе именуемый |
| "процесс 1" */ |
| входная информация: отсутствует |
| выходная информация: отсутствует |
| { |
| fd = open("/etc/inittab",O_RDONLY); |
| while (line_read(fd,buffer)) |
| { |
| /* читать каждую строку файлу */ |
| if (invoked state != buffer state) |
| continue; /* остаться в цикле while */ |
| /* найден идентификатор соответствующего состояния |
| */ |
| if (fork() == 0) |
| { |
| execl("процесс указан в буфере"); |
| exit(); |
| } |
| /* процесс init не дожидается завершения потомка */ |
| /* возврат в цикл while */ |
| } |
| |
| while ((id = wait((int*) 0)) != -1) |
| { |
| /* проверка существования потомка; |
| * если потомок прекратил существование, рассматри- |
| * вается возможность его перезапуска */ |
| /* в противном случае, основной процесс просто про- |
| * должает работу */ |
| } |
| } |
+------------------------------------------------------------+

Рисунок 7.31. Алгоритм выполнения процесса init

+------------------------------------------------------------+
| Формат: идентификатор, состояние, действие, спецификация |
| процесса |
| Поля разделены между собой двоеточиями |
| Комментарии в конце строки начинаются с символа '#' |
| |
| co::respawn:/etc/getty console console #Консоль в машзале|
| 46:2:respawn:/etc/getty -t 60 tty46 4800H #комментарии |
+------------------------------------------------------------+

Рисунок 7.32. Фрагмент файла inittab

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


    7.10 ВЫВОДЫ



В данной главе были рассмотрены системные функции, предназначенные для
работы с контекстом процесса и для управления выполнением процесса. Систем-
ная функция fork создает новый процесс, копируя для него содержимое всех об-
ластей, подключенных к родительскому процессу. Особенность реализации функ-
ции fork состоит в том, что она выполняет инициализацию сохраненного регист-

223

рового контекста порожденного процесса, таким образом этот процесс начинает
выполняться, не дожидаясь завершения функции, и уже в теле функции начинает
осознавать свою предназначение как потомка. Все процессы завершают свое вы-
полнение вызовом функции exit, которая отсоединяет области процесса и посы-
лает его родителю сигнал "гибель потомка". Процесс-родитель может совместить
момент продолжения своего выполнения с моментом завершения процесса-потомка,
используя системную функцию wait. Системная функция exec дает процессу воз-
можность запускать на выполнение другие программы, накладывая содержимое ис-
полняемого файла на свое адресное пространство. Ядро отсоединяет области,
ранее занимаемые процессом, и назначает процессу новые области в соответст-
вии с потребностями исполняемого файла. Совместное использование областей
команд и наличие режима "sticky-bit" дают возможность более рационально ис-
пользовать память и экономить время, затрачиваемое на подготовку к запуску
программ. Простым пользователям предоставляется возможность получать приви-
легии других пользователей, даже суперпользователя, благодаря обращению к
услугам системной функции setuid и setuid-программ. С помощью функции brk
процесс может изменять размер своей области данных. Функция signal дает про-
цессам возможность управлять своей реакцией на поступающие сигналы. При по-
лучении сигнала производится обращение к специальной функции обработки сиг-
нала с внесением соответствующих изменений в стек задачи и в сохраненный ре-
гистровый контекст задачи. Процессы могут сами посылать сигналы, используя
системную функцию kill, они могут также контролировать получение сигналов,
предназначенных группе процессов, прибегая к услугам функции setpgrp.
Командный процессор shell и процесс начальной загрузки init используют
стандартные обращения к системным функциям, производя набор операций, в дру-
гих системах обычно выполняемых ядром. Shell интерпретирует команды пользо-
вателя, переназначает стандартные файлы ввода-вывода данных и выдачи ошибок,
порождает процессы, организует каналы между порожденными процессами, синхро-
низирует свое выполнение с этими процессами и формирует коды, возвращаемые
командами. Процесс init тоже порождает различные процессы, в частности, уп-
равляющие работой пользователя за терминалом. Когда такой процесс завершает-
ся, init может породить для выполнения той же самой функции еще один про-
цесс, если это вытекает из информации файла "/etc/inittab".


    7.11 УПРАЖНЕНИЯ



1. Запустите с терминала программу, приведенную на Рисунке 7.33. Переадре-
суйте стандартный вывод данных в файл и сравните результаты между со-
бой.
+------------------------------------+
| main() |
| { |
| printf("hello\n"); |
| if (fork() == 0) |
| printf("world\n"); |
| } |
+------------------------------------+

Рисунок 7.33. Пример модуля, содержащего вызов функции fork и обра-
щение к стандартному выводу

2. Разберитесь в механизме работы программы, приведенной на Рисунке 7.34,
и сравните ее результаты с результатами программы на Рисунке 7.4.
3. Еще раз обратимся к программе, приведенной на Рисунке 7.5 и показываю-
щей, как два процесса обмениваются сообщениями, используя спаренные ка-
налы. Что произойдет, если они попытаются вести обмен сообщениями, ис-
пользуя один канал ?
4. Возможна ли потеря информации в случае, когда процесс получает несколь-

224

ко сигналов прежде чем ему предоставляется возможность отреагировать на
них надлежащим образом ? (Рассмотрите случай, когда процесс подсчитыва-
ет количество полученных сигналов о прерывании.) Есть ли необходимость
в решении этой проблемы ?
5. Опишите механизм работы системной функции kill.
6. Процесс в программе на Рисунке 7.35 принимает сигналы типа "гибель по-
томка" и устанавливает функцию обработки сигналов в исходное состояние.
Что происходит при выполнении программы ?
7. Когда процесс получает сигналы определенного типа и не обрабатывает их,
ядро дампирует образ процесса в том виде, который был у него в момент
получения сигнала. Ядро создает в текущем каталоге процесса файл с име-
нем "core" и копирует в него пространство процесса, области команд,
данных и стека. Впоследствии пользователь может тщательно изучить дамп
образа процесса с помощью стандартных средств отладки. Опишите алго-
ритм, которому на Ваш взгляд должно следовать ядро в процессе создания
файла "core". Что нужно предпринять в том случае, если в текущем ката-
логе файл с таким именем уже существует ? Как должно вести себя ядро,
когда в одном и том же каталоге дампируют свои образы сразу несколько
процессов?
8. Еще раз обратимся к программе (Рисунок 7.12), описывающей, как один
процесс забрасывает другой процесс сигналами, которые принимаются их
адресатом. Подумайте, что произошло бы в том случае, если бы алгоритм
обработки сигналов был переработан в любом из следующих направлений:

+------------------------------------------------------------+
| #include |
| int fdrd,fdwt; |
| char c; |
| |
| main(argc,argv) |
| int argc; |
| char *argv[]; |
| { |
| if (argc != 3) |
| exit(1); |
| fork(); |
| |
| if ((fdrd = open(argv[1],O_RDONLY)) == -1) |
| exit(1); |
| if (((fdwt = creat(argv[2],0666)) == -1) && |
| ((fdwt = open(argv[2],O_WRONLY)) == -1)) |
| exit(1); |
| rdwrt(); |
| } |
| rdwrt() |
| { |
| for (;;) |
| { |
| if (read(fdrd,&c,1) != 1) |
| return; |
| write(fdwt,&c,1); |
| } |