В предыдущей главе был рассмотрен контекст процесса и описаны алгоритмы
для работы с ним; в данной главе речь пойдет об использовании и реализации
системных функций, управляющих контекстом процесса. Системная функция fork
создает новый процесс, функция exit завершает выполнение процесса, а wait
дает возможность родительскому процессу синхронизировать свое продолжение с
завершением порожденного процесса. Об асинхронных событиях процессы информи-
руются при помощи сигналов. Поскольку ядро синхронизирует выполнение функций
exit и wait при помощи сигналов, описание механизма сигналов предваряет со-
бой рассмотрение функций exit и wait. Системная функция exec дает процессу
возможность запускать "новую" программу, накладывая ее адресное пространство
на исполняемый образ файла. Системная функция brk позволяет динамически вы-
делять дополнительную память; теми же самыми средствами ядро динамически на-
ращивает стек задачи, выделяя в случае необходимости дополнительное прост-
ранство. В заключительной части главы дается краткое описание основных групп
операций командного процессора shell и начального процесса init.
На Рисунке 7.1 показана взаимосвязь между системными функциями, рассмат-
риваемыми в данной главе, с одной стороны, и алгоритмами, описанными в пре-
дыдущей главе, с другой. Почти во всех функциях используются алгоритмы sleep
и wakeup, отсутствующие на рисунке. Функция exec, кроме того, взаимодейству-
ет с алгоритмами работы с файловой системой, речь о которых шла в главах 4 и
5.

+-----------------------------+---------------------+------------+
| Системные функции, имеющие | Системные функции, | Функции |
| ющие дело с управлением па- | связанные с синхро- | смешанного |
| мятью | низацией | типа |
+-------+-------+-------+-----+--+----+------+----+-+-----+------+
| fork | exec | brk | exit |wait|signal|kill|setrgrр|setuid|
+-------+-------+-------+--------+----+------+----+-------+------+
|dupreg |detach-|growreg| detach-| |
|attach-| reg | | reg | |
| reg |alloc- | | | |
| | reg | | | |
| |attach-| | | |
| | reg | | | |
| |growreg| | | |
| |loadreg| | | |
| |mapreg | | | |
+-------+-------+-------+--------+-------------------------------+

Рисунок 7.1. Системные функции управления процессом и их
связь с другими алгоритмами



    7.1 СОЗДАНИЕ ПРОЦЕССА



Единственным способом создания пользователем нового процесса в операци-
онной системе UNIX является выполнение системной функции fork. Процесс, вы-
зывающий функцию fork, называется родительским (процесс-родитель), вновь
создаваемый процесс называется порожденным (процесс-потомок). Синтаксис вы-
зова функции fork:

179

pid = fork();
В результате выполнения функции fork пользовательский контекст и того, и
другого процессов совпадает во всем, кроме возвращаемого значения переменной
pid. Для родительского процесса в pid возвращается идентификатор порожденно-
го процесса, для порожденного - pid имеет нулевое значение. Нулевой процесс,
возникающий внутри ядра при загрузке системы, является единственным процес-
сом, не создаваемым с помощью функции fork.
В ходе выполнения функции ядро производит следующую последовательность
действий:
1. Отводит место в таблице процессов под новый процесс.
2. Присваивает порождаемому процессу уникальный код идентификации.
3. Делает логическую копию контекста родительского процесса. Поскольку те
или иные составляющие процесса, такие как область команд, могут разде-
ляться другими процессами, ядро может иногда вместо копирования области
в новый физический участок памяти просто увеличить значение счетчика
ссылок на область.
4. Увеличивает значения счетчика числа файлов, связанных с процессом, как в
таблице файлов, так и в таблице индексов.
5. Возвращает родительскому процессу код идентификации порожденного процес-
са, а порожденному процессу - нулевое значение.

Реализацию системной функции fork, пожалуй, нельзя назвать тривиальной,
так как порожденный процесс начинает свое выполнение, возникая как бы из
воздуха. Алгоритм реализации функции для систем с замещением страниц по зап-
росу и для систем с подкачкой процессов имеет лишь незначительные различия;
все изложенное ниже в отношении этого алгоритма касается в первую очередь
традиционных систем с подкачкой процессов, но с непременным акцентированием
внимания на тех моментах, которые в системах с замещением страниц по запросу
реализуются иначе. Кроме того, конечно, предполагается, что в системе имеет-
ся свободная оперативная память, достаточная для размещения порожденного
процесса. В главе 9 будет отдельно рассмотрен случай, когда для порожденного
процесса не хватает памяти, и там же будут даны разъяснения относительно ре-
ализации алгоритма fork в системах с замещением страниц.
На Рисунке 7.2 приведен алгоритм создания процесса. Сначала ядро должно
удостовериться в том, что для успешного выполнения алгоритма fork есть все
необходимые ресурсы. В системе с подкачкой процессов для размещения порожда-
емого процесса требуется место либо в памяти, либо на диске; в системе с за-
мещением страниц следует выделить память для вспомогательных таблиц (в част-
ности, таблиц страниц). Если свободных ресурсов нет, алгоритм fork заверша-
ется неудачно. Ядро ищет место в таблице процессов для конструирования кон-
текста порождаемого процесса и проверяет, не превысил ли пользователь, вы-
полняющий fork, ограничение на максимально-допустимое количество параллельно
запущенных процессов. Ядро также подбирает для нового процесса уникальный
идентификатор, значение которого превышает на единицу максимальный из сущес-
твующих идентификаторов. Если предлагаемый идентификатор уже присвоен друго-
му процессу, ядро берет идентификатор, следующий по порядку. Как только бу-
дет достигнуто максимально-допустимое значение, отсчет идентификаторов опять
начнется с 0. Поскольку большинство процессов имеет короткое время жизни,
при переходе к началу отсчета значительная часть идентификаторов оказывается
свободной.
На количество одновременно выполняющихся процессов накладывается ограни-
чение (конфигурируемое), отсюда ни один из пользователей не может занимать в
таблице процессов слишком много места, мешая тем самым другим пользователям
создавать новые процессы. Кроме того, простым пользователям не разрешается
создавать процесс, занимающий последнее свободное место в таблице процессов,
в противном случае система зашла бы в тупик. Другими словами, поскольку в
таблице процессов нет свободного места, то ядро не может гарантировать, что
все существующие процессы завершатся естественным образом, поэтому новые


180

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

Рисунок 7.2. Алгоритм fork


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

181

процессов, соответствующей порожденному процессу, копируя в них значения по-
лей из записи родительского процесса. Например, порожденный процесс "насле-
дует" у родительского процесса коды идентификации пользователя (реальный и
тот, под которым исполняется процесс), группу процессов, управляемую роди-
тельским процессом, а также значение, заданное родительским процессом в фун-
кции nice и используемое при вычислении приоритета планирования. В следующих
разделах мы поговорим о назначении этих полей. Ядро передает значение поля
идентификатора родительского процесса в запись порожденного, включая послед-
ний в древовидную структуру процессов, и присваивает начальные значения раз-
личным параметрам планирования, таким как приоритет планирования, использо-
вание ресурсов центрального процессора и другие значения полей синхрониза-
ции. Начальным состоянием процесса является состояние "создания" (см. Рису-
нок 6.1).
После того ядро устанавливает значения счетчиков ссылок на файлы, с ко-
торыми автоматически связывается порождаемый процесс. Во-первых, порожденный
процесс размещается в текущем каталоге родительского процесса. Число процес-
сов, обращающихся в данный момент к каталогу, увеличивается на 1 и, соответ-
ственно, увеличивается значение счетчика ссылок на его индекс. Во-вторых,
если родительский процесс или один из его предков уже выполнял смену корне-
вого каталога с помощью функции chroot, порожденный процесс наследует и но-
вый корень с соответствующим увеличением значения счетчика ссылок на индекс
корня. Наконец, ядро просматривает таблицу пользовательских дескрипторов для
родительского процесса в поисках открытых файлов, известных процессу, и уве-
личивает значение счетчика ссылок, ассоциированного с каждым из открытых
файлов, в глобальной таблице файлов. Порожденный процесс не просто наследует
права доступа к открытым файлам, но и разделяет доступ к файлам с родитель-
ским процессом, так как оба процесса обращаются в таблице файлов к одним и
тем же записям. Действие fork в отношении открытых файлов подобно действию
алгоритма dup: новая запись в таблице пользовательских дескрипторов файла
указывает на запись в глобальной таблице файлов, соответствующую открытому
файлу. Для dup, однако, записи в таблице пользовательских дескрипторов файла
относятся к одному процессу; для fork - к разным процессам.
После завершения всех этих действий ядро готово к созданию для порожден-
ного процесса пользовательского контекста. Ядро выделяет память для адресно-
го пространства процесса, его областей и таблиц страниц, создает с помощью
алгоритма dupreg копии всех областей родительского процесса и присоединяет с
помощью алгоритма attachreg каждую область к порожденному процессу. В систе-
ме с подкачкой процессов ядро копирует содержимое областей, не являющихся
областями разделяемой памяти, в новую зону оперативной памяти. Вспомним из
раздела 6.2.4 о том, что в пространстве процесса хранится указатель на соот-
ветствующую запись в таблице процессов. За исключением этого поля, во всем
остальном содержимое адресного пространства порожденного процесса в начале
совпадает с содержимым пространства родительского процесса, но может расхо-
диться после завершения алгоритма fork. Родительский процесс, например, пос-
ле выполнения fork может открыть новый файл, к которому порожденный процесс
уже не получит доступ автоматически.
Итак, ядро завершило создание статической части контекста порожденного
процесса; теперь оно приступает к созданию динамической части. Ядро копирует
в нее первый контекстный уровень родительского процесса, включающий в себя
сохраненный регистровый контекст задачи и стек ядра в момент вызова функции
fork. Если в данной реализации стек ядра является частью пространства про-
цесса, ядро в момент создания пространства порожденного процесса автомати-
чески создает и системный стек для него. В противном случае родительскому
процессу придется скопировать в пространство памяти, ассоциированное с по-
рожденным процессом, свой системный стек. В любом случае стек ядра для по-
рожденного процесса совпадает с системным стеком его родителя. Далее ядро
создает для порожденного процесса фиктивный контекстный уровень (2), в кото-
ром содержится сохраненный регистровый контекст из первого контекстного
уровня. Значения счетчика команд (регистр PC) и других регистров, сохраняе-

182

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

Родительский процесс
+---------------------------------------------+ Таблица
| +---------+ Частная Адресное простран- | файлов
| | Область | таблица ство процесса | +---------+
| | данных | областей +------------------+| | - |
| +---------+ процесса | Открытые файлы --||-- + | - |
| | +------+ | || - | - |
| -- - - + | + +| Текущий каталог -||+ | +---------+
| - +------+ -| ||- -- -| |
| +---------+ + + | || Измененный корень||| | | |
| | Стек | +------+ -+------------------+|- - +---------+
| | задачи + - + | |+------------------+|| | | - |
| +---------+ +------+ -| - ||- - | - |
| || - ||| | | - |
| -| - ||- - +---------+
| -- - - - - - - - -+| Стек ядра ||| + - + |
| | +------------------+|- - | |
+---------------------------------------------+| | +---------+
- - - | - |
+----+----+ | | | - |
|Разделяе-| - - | - |
| мая | | | +---------+
| область | - -- -| |
| команд | | | | |
+----+----+ - - +---------+
- | | +---------+
+ - - - - - - - - + - -
+---------------------------------------------++ -|+ Таблица
| +---------+ Частная | Адресное простран- | -- файлов
| | Область | таблица - ство процесса | || +---------+
| | данных | областей |+------------------+| -- | - |
| +---------+ процесса -| Открытые файлы --||-- +| | - |
| | +------+ || || - | - |
| -- - - + | +--| Текущий каталог -||+ | +---------+
| - +------+ | ||- -- + |
| +---------+ + + | | Измененный корень||+ - - -| |
| | Стек | +------+ +------------------+| +---------+
| | задачи + - + | +------------------+| | - |
| +---------+ +------+ | - || | - |
| | - || +---------+
| | Стек ядра || | |
| +------------------+| | |
+---------------------------------------------+ +---------+
Порожденный процесс | - |
| - |
+---------+

Рисунок 7.3. Создание контекста нового процесса при выполне-
нии функции fork

183


Если контекст порожденного процесса готов, родительский процесс заверша-
ет свою роль в выполнении алгоритма fork, переводя порожденный процесс в
состояние "готовности к запуску, находясь в памяти" и возвращая пользователю
его идентификатор. Затем, используя обычный алгоритм планирования, ядро вы-
бирает порожденный процесс для исполнения и тот "доигрывает" свою роль в ал-
горитме fork. Контекст порожденного процесса был задан родительским процес-
сом; с точки зрения ядра кажется, что порожденный процесс возобновляется
после приостанова в ожидании ресурса. Порожденный процесс при выполнении
функции fork реализует ту часть программы, на которую указывает счетчик ко-
манд, восстанавливаемый ядром из сохраненного на уровне 2 регистрового кон-
текста, и по выходе из функции возвращает нулевое значение.
На Рисунке 7.3 представлена логическая схема взаимодействия родительско-
го и порожденного процессов с другими структурами данных ядра сразу после
завершения системной функции fork. Итак, оба процесса совместно пользуются
файлами, которые были открыты родительским процессом к моменту исполнения
функции fork, при этом значение счетчика ссылок на каждый из этих файлов в
таблице файлов на единицу больше, чем до вызова функции. Порожденный процесс
имеет те же, что и родительский процесс, текущий и корневой каталоги, значе-
ние же счетчика ссылок на индекс каждого из этих каталогов так же становится
на единицу больше, чем до вызова функции. Содержимое областей команд, данных
и стека (задачи) у обоих процессов совпадает; по типу области и версии сис-
темной реализации можно установить, могут ли процессы разделять саму область
команд в физических адресах.
Рассмотрим приведенную на Рисунке 7.4 программу, которая представляет
собой пример разделения доступа к файлу при исполнении функции fork. Пользо-
вателю следует передавать этой программе
два параметра - имя существующего файла и имя создаваемого файла. Процесс
открывает существующий файл, создает новый файл и - при условии отсутствия
ошибок - порождает новый процесс. Внутри программы ядро делает копию контек-
ста родительского процесса для порожденного, при этом родительский процесс
исполняется в одном адресном пространстве, а порожденный - в другом. Каждый
из процессов может работать со своими собственными копиями глобальных пере-
менных fdrd, fdwt и c, а также со своими собственными копиями стековых пере-
менных argc и argv, но ни один из них не может обращаться к переменным дру-
гого процесса. Тем не менее, при выполнении функции fork ядро делает копию
адресного пространства первого процесса для второго, и порожденный процесс,
таким образом, наследует доступ к файлам родительского (то есть к файлам, им
ранее открытым и созданным) с правом использования тех же самых деск-
рипторов.
Родительский и порожденный процессы независимо друг от друга, конечно,
вызывают функцию rdwrt и в цикле считывают по одному байту информацию из ис-
ходного файла и переписывают ее в файл вывода. Функция rdwrt возвращает уп-
равление, когда при считывании обнаруживается конец файла. Ядро перед тем
уже увеличило значения счетчиков ссылок на исходный и результирующий файлы в
таблице файлов, и дескрипторы, используемые в обоих процессах, адресуют к
одним и тем же строкам в таблице. Таким образом, дескрипторы fdrd в том и в
другом процессах указывают на запись в таблице файлов, соответствующую ис-
ходному файлу, а дескрипторы, подставляемые в качестве fdwt, - на запись,
соответствующую результирующему файлу (файлу вывода). Поэтому оба процесса
никогда не обратятся вместе на чтение или запись к одному и тому же адресу,
вычисляемому с помощью смещения внутри файла, поскольку ядро смещает внутри-
файловые указатели после каждой операции чтения или записи. Несмотря на то,
что, казалось бы, из-за того, что процессы распределяют между собой рабочую
нагрузку, они копируют исходный файл в два раза быстрее, содержимое резуль-
тирующего файла зависит от очередности, в которой ядро запускает процессы.
Если ядро запускает процессы так, что они исполняют системные функции попе-
ременно (чередуя и спаренные вызовы функций read-write), содержимое резуль-


184

+------------------------------------------------------------+
| #include |
| int fdrd, fdwt; |
| char c; |
| |
| main(argc, argv) |
| int argc; |
| char *argv[]; |
| { |
| if (argc != 3) |
| exit(1); |
| if ((fdrd = open(argv[1],O_RDONLY)) == -1) |
| exit(1); |
| if ((fdwt = creat(argv[2],0666)) == -1) |
| exit(1); |
| |
| fork(); |
| /* оба процесса исполняют одну и ту же программу */ |
| rdwrt(); |
| exit(0); |
| } |
| |
| rdwrt(); |
| { |
| for(;;) |
| { |
| if (read(fdrd,&c,1) != 1) |
| return; |
| write(fdwt,&c,1); |
| } |
| } |
+------------------------------------------------------------+

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


тирующего файла будет совпадать с содержимым исходного файла. Рассмотрим,
однако, случай, когда процессы собираются считать из исходного файла после-
довательность из двух символов "ab". Предположим, что родительский процесс
считал символ "a", но не успел записать его, так как ядро переключилось на
контекст порожденного процесса. Если порожденный процесс считывает символ
"b" и записывает его в результирующий файл до возобновления родительского
процесса, строка "ab" в результирующем файле будет иметь вид "ba". Ядро не
гарантирует согласование темпов выполнения процессов.
Теперь перейдем к программе, представленной на Рисунке 7.5, в которой
процесс-потомок наследует от своего родителя файловые дескрипторы 0 и 1 (со-
ответствующие стандартному вводу и стандартному выводу). При каждом выполне-
нии системной функции pipe производится назначение двух файловых дескрипто-
ров в массивах to_par и to_chil. Процесс вызывает функцию fork и делает ко-
пию своего контекста: каждый из процессов имеет доступ только к своим собст-
венным данным, так же как и в предыдущем примере. Родительский процесс зак-
рывает файл стандартного вывода (дескриптор 1) и дублирует дескриптор запи-
си, возвращаемый в канал to_chil. Поскольку первое свободное место в таблице
дескрипторов родительского процесса образовалось в результате только что вы-
полненной операции закрытия (close) файла вывода, ядро переписывает туда
дескриптор записи в канал и этот дескриптор становится дескриптором файла
стандартного вывода для to_chil. Те же самые действия родительский процесс
выполняет в отношении дескриптора файла стандартного ввода, заменяя его дес-

185

криптором чтения из канала to_par. И порожденный процесс закрывает файл
стандартного ввода (дескриптор 0) и так же дублирует дескриптор чтения из
канала to_chil. Поскольку первое свободное место в таблице дескрипторов фай-
лов прежде было занято файлом стандартного ввода, его дескриптором становит-
ся дескриптор чтения из канала to_chil. Аналогичные действия выполняются и в
отношении дескриптора файла стандартного вывода, заменяя его дескриптором
записи в канал to_par. И тот, и другой процессы закрывают файлы, дескрипторы

+------------------------------------------------------------+
| #include |
| char string[] = "hello world"; |
| main() |
| { |
| int count,i; |
| int to_par[2],to_chil[2]; /* для каналов родителя и |
| потомка */ |
| char buf[256]; |
| pipe(to_par); |
| pipe(to_chil); |
| if (fork() == 0) |
| { |
| /* выполнение порожденного процесса */ |
| close(0); /* закрытие прежнего стандартного ввода */ |
| dup(to_chil[0]); /* дублирование дескриптора чтения |
| из канала в позицию стандартного |
| ввода */ |
| close(1); /* закрытие прежнего стандартного вывода */|
| dup(to_par[0]); /* дублирование дескриптора записи |
| в канал в позицию стандартного |