полнение с кодом возврата i, номером процесса в порядке очередности созда-
ния. Ядро, исполняя функцию wait для родителя, находит потомка, прекративше-
го существование, и передает родителю его идентификатор и код возврата функ-
ции exit. При этом заранее не известно, какой из потомков будет обнаружен.
Из текста программы, реализующей системную функцию exit, написанной на языке
Си и включенной в библиотеку стандартных подпрограмм, видно, что программа
запоминает код возврата функции exit в битах 8-15 поля ret_code и возвращает
функции wait идентификатор процесса-потомка. Таким образом, в ret_code хра-
нится значение, равное 256*i, где i - номер потомка, а в ret_val заносится
значение идентификатора потомка.
Если пользователь запускает программу с параметром (то есть argc > 1),
родительский процесс с помощью функции signal делает распоряжение игнориро-
вать сигналы типа "гибель потомка". Предположим, что родительский процесс,
выполняя функцию wait, приостановился еще до того, как его потомок произвел
обращение к функции exit: когда процесс-потомок переходит к выполнению функ-
ции exit, он посылает своему родителю сигнал "гибель потомка"; родительский
процесс возобновляется, поскольку он был приостановлен с приоритетом, допус-
кающим прерывания. Когда так или иначе родительский процесс продолжит свое

+------------------------------------------------------------+
| #include |
| main(argc,argv) |
| int argc; |
| char *argv[]; |
| { |
| int i,ret_val,ret_code; |
| |
| if (argc >= 1) |
| signal(SIGCLD,SIG_IGN); /* игнорировать гибель |
| потомков */ |
| for (i = 0; i < 15; i++) |
| if (fork() == 0) |
| { |
| /* процесс-потомок */ |
| printf("процесс-потомок %x\n",getpid()); |
| exit(i); |
| } |
| ret_val = wait(&ret_code); |
| printf("wait ret_val %x ret_code %x\n",ret_val,ret_code);|
| } |
+------------------------------------------------------------+

Рисунок 7.17. Пример использования функции wait и игнорирова-
ния сигнала "гибель потомка"

202


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

+------------------------------------------------------------+
| #include |
| main(argc,argv) |
| { |
| char buf[256]; |
| |
| if (argc != 1) |
| signal(SIGCLD,SIG_IGN); /* игнорировать гибель |
| потомков */ |
| while (read(0,buf,256)) |
| if (fork() == 0) |
| { |
| /* здесь процесс-потомок обычно выполняет |
| какие-то операции над буфером (buf) */ |
| exit(0); |
| } |
| } |
+------------------------------------------------------------+

Рисунок 7.18. Пример указания причины появления сигнала "ги-
бель потомков"


203

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


    7.5 ВЫЗОВ ДРУГИХ ПРОГРАММ



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

execve(filename,argv,envp)

где filename - имя исполняемого файла, argv - указатель на массив парамет-
ров, которые передаются вызываемой программе, а envp - указатель на массив
параметров, составляющих среду выполнения вызываемой программы. Вызов сис-
темной функции exec осуществляют несколько библиотечных функций, таких как
execl, execv, execle и т.д. В том случае, когда программа использует пара-
метры командной строки

main(argc,argv) ,

+------------------------------------------------------------+
| алгоритм exec |
| входная информация: (1) имя файла |
| (2) список параметров |
| (3) список переменных среды |
| выходная информация: отсутствует |
| { |
| получить индекс файла (алгоритм namei); |
| проверить, является ли файл исполнимым и имеет ли поль- |
| зователь право на его исполнение; |
| прочитать информацию из заголовков файла и проверить, |
| является ли он загрузочным модулем; |
| скопировать параметры, переданные функции, из старого |
| адресного пространства в системное пространство; |
| для (каждой области, присоединенной к процессу) |
| отсоединить все старые области (алгоритм detachreg);|
| для (каждой области, определенной в загрузочном модуле) |
| { |
| выделить новые области (алгоритм allocreg); |
| присоединить области (алгоритм attachreg); |
| загрузить область в память по готовности (алгоритм |
| loadreg); |
| } |
| скопировать параметры, переданные функции, в новую об- |
| ласть стека задачи; |
| специальная обработка для setuid-программ, трассировка; |
| проинициализировать область сохранения регистров задачи |
| (в рамках подготовки к возвращению в режим задачи); |
| освободить индекс файла (алгоритм iput); |
| } |
+------------------------------------------------------------+

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

204

массив argv является копией одноименного параметра, передаваемого функции
exec. Символьные строки, описывающие среду выполнения вызываемой программы,
имеют вид "имя=значение" и содержат полезную для программ информацию, такую
как начальный каталог пользователя и путь поиска исполняемых программ. Про-
цессы могут обращаться к параметрам описания среды выполнения, используя
глобальную пере-
менную environ, которую заводит начальная процедура Си-интерпретатора.
На Рисунке 7.19 представлен алгоритм выполнения системной функции exec.
Сначала функция обращается к файлу по алгоритму namei, проверяя, является ли
файл исполнимым и отличным от каталога, а также проверяя наличие у пользова-
теля права исполнять программу. Затем ядро, считывая заголовок файла, опре-
деляет размещение информации в файле (формат файла).
На Рисунке 7.20 изображен логический формат исполняемого файла в файло-
вой системе, обычно генерируемый транслятором или загрузчиком. Он разбивает-
ся на четыре части:
1. Главный заголовок, содержащий информацию о том, на сколько разделов де-
лится файл, а также содержащий начальный адрес исполнения процесса и не-
которое "магическое число", описывающее тип исполняемого файла.
2. Заголовки разделов, содержащие информацию, описывающую каждый раздел в
файле: его размер, виртуальные адреса, в которых он располагается, и др.
3. Разделы, содержащие собственно "данные" файла (например, текстовые), ко-
торые загружаются в адресное пространство процесса.
4. Разделы, содержащие смешанную информацию, такую как таблицы идентифика-
торов и другие данные, используемые в процессе отладки.

+---------------------------+
| Тип файла |
Главный заголовок | Количество разделов |
| Начальное состояние регис-|
| тров |
+---------------------------+
| Тип раздела |
Заголовок 1-го раздела | Размер раздела |
| Виртуальный адрес |
+---------------------------+
| Тип раздела |
Заголовок 2-го раздела | Размер раздела |
- | Виртуальный адрес |
- +---------------------------+
- | - |
- | - |
- +---------------------------+
- | Тип раздела |
Заголовок n-го раздела | Размер раздела |
| Виртуальный адрес |
+---------------------------+
Раздел 1 | Данные (например, текст) |
+---------------------------+
Раздел 2 | Данные |
- +---------------------------+
- | - |
- | - |
- +---------------------------+
Раздел n | Данные |
+---------------------------+
| Другая информация |
+---------------------------+

Рисунок 7.20. Образ исполняемого файла

205



Указанные составляющие с развитием самой системы видоизменяются, однако
во всех исполняемых файлах обязательно присутствует главный заголовок с по-
лем типа файла.
Тип файла обозначается коротким целым числом (представляется в машине
полусловом), которое идентифицирует файл как загрузочный модуль, давая тем
самым ядру возможность отслеживать динамические характеристики его выполне-
ния. Например, в машине PDP 11/70 определение типа файла как загрузочного
модуля свидетельствует о том, что процесс, исполняющий файл, может использо-
вать до 128 Кбайт памяти вместо 64 Кбайт (**), тем не менее в системах с за-
мещением страниц тип файла все еще играет существенную роль, в чем нам пред-
стоит убедиться во время знакомства с главой 9.
Вернемся к алгоритму. Мы остановились на том, что ядро обратилось к ин-
дексу файла и установило, что файл является исполнимым. Ядру следовало бы
освободить память, занимаемую пользовательским контекстом процесса. Однако,
поскольку в памяти, подлежащей освобождению, располагаются передаваемые но-
вой программе параметры, ядро первым делом копирует их из адресного прост-
ранства в промежуточный буфер на время, пока не будут отведены области для
нового пространства памяти.
Поскольку параметрами функции exec выступают пользовательские адреса
массивов символьных строк, ядро по каждой строке сначала копирует в систем-
ную память адрес строки, а затем саму строку. Для хранения строки в разных
версиях системы могут быть выбраны различные места. Чаще принято хранить
строки в стеке ядра (локальная структура данных, принадлежащая программе яд-
ра), на нераспределяемых участках памяти (таких как страницы), которые можно
занимать только временно, а также во внешней памяти (на устройстве выгруз-
ки).
С точки зрения реализации проще всего для копирования параметров в новый
пользовательский контекст обратиться к стеку ядра. Однако, поскольку размер
стека ядра, как правило, ограничивается системой, а также поскольку парамет-
ры функции exec могут иметь произвольную длину, этот подход следует сочетать
с другими подходами. При рассмотрении других вариантов обычно останавливают-
ся на способе хранения, обеспечивающем наиболее быстрый доступ к строкам.
Если доступ к страницам памяти в системе реализуется довольно просто, строки
следует размещать на страницах, поскольку обращение к оперативной памяти
осуществляется быстрее, чем к внешней (устройству выгрузки).
После копирования параметров функции exec в системную память ядро отсое-
диняет области, ранее присоединенные к процессу, используя алгоритм
detachreg. Несколько позже мы еще поговорим о специальных действиях, выпол-
няемых в отношении областей команд. К рассматриваемому моменту процесс уже
лишен пользовательского контекста и поэтому возникновение в дальнейшем любой
ошибки неизбежно будет приводить к завершению процесса по сигналу. Такими
ошибками могут быть обращение к пространству, не описанному в таблице облас-
тей ядра, попытка загрузить программу, имеющую недопустимо большой размер
или использующую области с пересекающимися адресами, и др. Ядро выделяет и
присоединяет к процессу области команд и данных, загружает в оперативную па-
мять содержимое исполняемого файла (алгоритмы allocreg, attachreg и loadreg,
соответственно). Область данных процесса изначально поделена на две части:


---------------------------------------
(**) В PDP 11 "магические числа" имеют значения, соответствующие командам
перехода; при выполнении этих команд в ранних версиях системы управле-
ние передавалось в разные места программы в зависимости от размера за-
головка и от типа исполняемого файла. Эта особенность больше не исполь-
зуется с тех пор, как система стала разрабатываться на языке Си.



206

данные, инициализация которых была выполнена во время компиляции, и данные,
не определенные компилятором ("bss"). Область памяти первоначально выделяет-
ся для проинициализированных данных. Затем ядро увеличивает размер области
данных для размещения данных типа "bss" (алгоритм growreg) и обнуляет их
значения. Напоследок ядро выделяет и присоединяет к процессу область стека и
отводит пространство памяти для хранения параметров функции exec. Если пара-
метры функции размещаются на страницах, те же страницы могут быть использо-
ваны под стек. В противном случае параметры функции размещаются в стеке за-
дачи.
В пространстве процесса ядро стирает адреса пользовательских функций об-
работки сигналов, поскольку в новом пользовательском контексте они теряют
свое значение. Однако и в новом контексте рекомендации по игнорированию тех
или иных сигналов остаются в силе. Ядро устанавливает в регистрах для режима
задачи значения из сохраненного регистрового контекста, в частности первона-
чальное значение указателя вершины стека (sp) и счетчика команд (pc): перво-
начальное значение счетчика команд было занесено загрузчиком в заголовок
файла. Для setuid-программ и для трассировки процесса ядро предпринимает
особые действия, на которых мы еще остановимся во время рассмотрения глав 8
и 11, соответственно. Наконец, ядро запускает алгоритм iput, освобождая ин-
декс, выделенный по алгоритму namei в самом начале выполнения функции exec.
Алгоритмы namei и iput в функции exec выполняют роль, подобную той, которую
они выполняют при открытии и закрытии файла; состояние файла во время выпол-
нения функции exec похоже на состояние открытого файла, если не принимать во
внимание отсутствие записи о файле в таблице файлов. По выходе из функции
процесс исполняет текст новой программы. Тем не менее, процесс остается тем
же, что и до выполнения функции; его идентификатор не изменился, как не из-
менилось и его место в иерархии процессов. Изменению подвергся только поль-
зовательский контекст процесса.

+-------------------------------------------------------+
| main() |
| { |
| int status; |
| if (fork() == 0) |
| execl("/bin/date","date",0); |
| wait(&status); |
| } |
+-------------------------------------------------------+

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


В качестве примера можно привести программу (Рисунок 7.21), в которой
создается процесс-потомок, запускающий функцию exec. Сразу по завершении
функции fork процесс-родитель и процесс-потомок начинают исполнять независи-
мо друг от друга копии одной и той же программы. К моменту вызова процес-
сом-потомком функции exec в его области команд находятся инструкции этой
программы, в области данных располагаются строки "/bin/date" и "date", а в
стеке - записи, которые будут извлечены по выходе из exec. Ядро ищет файл
"/bin/date" в файловой системе, обнаружив его, узнает, что его может испол-
нить любой пользователь, а также то, что он представляет собой загрузочный
модуль, готовый для исполнения. По условию первым параметром функции exec,
включаемым в список параметров argv, является имя исполняемого файла (пос-
ледняя компонента имени пути поиска файла). Таким образом, процесс имеет
доступ к имени программы на пользовательском уровне, что иногда может ока-
заться полезным (***). Затем ядро копирует строки "/bin/date" и "date" во
внутреннюю структуру хранения и освобождает области команд, данных и стека,
занимаемые процессом. Процессу выделяются новые области команд, данных и
стека, в область команд переписывается командная секция файла "/bin/date", в

207

---------------------------------------
(***) Например, в версии V стандартные программы переименования файла (mv),
копирования файла (cp) и компоновки файла (ln), поскольку исполняют
похожие действия, вызывают один и тот же исполняемый файл. По имени
вызываемой программы процесс узнает, какие действия в настоящий момент
требуются пользователю.

область данных - секция данных файла. Ядро восстанавливает первоначальный
список параметров (в данном случае это строка символов "date") и помещает
его в область стека. Вызвав функцию exec, процесс-потомок прекращает выпол-
нение старой программы и переходит к выполнению программы

"date"; когда программа "date" завершится, процесс-родитель, ожидающий этого
момента, получит код завершения функции exit.
Вплоть до настоящего момента мы предполагали, что команды и данные раз-
мещаются в разных секциях исполняемой программы и, следовательно, в разных
областях текущего процесса. Такое размещение имеет два основных преимущест-
ва: простота организации защиты от несанкционированного доступа и возмож-
ность разделения областей различными процессами. Если бы команды и данные
находились в одной области, система не смогла бы предотвратить затирание ко-
манд, поскольку ей не были бы известны адреса, по которым они располагаются.
Если же команды и данные находятся в разных областях, система имеет возмож-
ность пользоваться механизмами аппаратной защиты области команд процесса.
Когда процесс случайно попытается что-то записать в область, занятую коман-
дами, он получит отказ, порожденный системой защиты и приводящий обычно к
аварийному завершению процесса.

+------------------------------------------------------------+
| #include |
| main() |
| { |
| int i,*ip; |
| extern f(),sigcatch(); |
| |
| ip = (int *)f; /* присвоение переменной ip значения ад-|
| реса функции f */ |
| for (i = 0; i < 20; i++) |
| signal(i,sigcatch); |
| *ip = 1; /* попытка затереть адрес функции f */ |
| printf("после присвоения значения ip\n"); |
| f(); |
| } |
| |
| f() |
| { |
| } |
| |
| sigcatch(n) |
| int n; |
| { |
| printf("принят сигнал %d\n",n); |
| exit(1); |
| } |
+------------------------------------------------------------+

Рисунок 7.22. Пример программы, ведущей запись в область команд

В качестве примера можно привести программу (Рисунок 7.22), которая
присваивает переменной ip значение адреса функции f и затем делает распоря-

208

жение принимать все сигналы. Если программа скомпилирована так, что команды
и данные располагаются в разных областях, процесс, исполняющий программу,
при попытке записать что-то по адресу в ip встретит порожденный системой за-
щиты отказ, поскольку область команд защищена от записи. При работе на
компьютере AT&T 3B20 ядро посылает процессу сигнал SIGBUS, в других системах
возможна посылка других сигналов. Процесс принимает сигнал и завершается, не
дойдя до выполнения команды вывода на печать в процедуре main. Однако, если
программа скомпилирована так, что команды и данные располагаются в одной об-
ласти (в области данных), ядро не поймет, что процесс пытается затереть ад-
рес функции f. Адрес f станет равным 1. Процесс исполнит команду вывода на
печать в процедуре main, но когда запустит функцию f, произойдет ошибка,
связанная с попыткой выполнения запрещенной команды. Ядро пошлет процессу
сигнал SIGILL и процесс завершится.
Расположение команд и данных в разных областях облегчает поиск и предот-
вращение ошибок адресации. Тем не менее, в ранних версиях системы UNIX ко-
манды и данные разрешалось располагать в одной области, поскольку на машинах
PDP размер процесса был сильно ограничен: программы имели меньший размер и
существенно меньшую сегментацию, если команды и данные занимали одну и ту же
область. В последних версиях системы таких строгих ограничений на размер
процесса нет и в дальнейшем возможность загрузки команд и данных в одну об-
ласть компиляторами не будет поддерживаться.
Второе преимущество раздельного хранения команд и данных состоит в воз-
можности совместного использования областей процессами. Если процесс не мо-
жет вести запись в область команд, команды процесса не претерпевают никаких
изменений с того момента, как ядро загрузило их в область команд из команд-
ной секции исполняемого файла. Если один и тот же файл исполняется несколь-
кими процессами, в целях экономии памяти они могут иметь одну область команд
на всех. Таким образом, когда ядро при выполнении функции exec отводит об-
ласть под команды процесса, оно проверяет, имеется ли возможность совместно-
го использования процессами команд исполняемого файла, что определяется "ма-
гическим числом" в заголовке файла. Если да, то с помощью алгоритма xalloc
ядро ищет существующую область с командами файла или назначает новую в слу-
чае ее отсутствия (см. Рисунок 7.23).
Исполняя алгоритм xalloc, ядро просматривает список активных областей в
поисках области с командами файла, индекс которого совпадает с индексом ис-
полняемого файла. В случае ее отсутствия ядро выделяет новую область (алго-
ритм allocreg), присоединяет ее к процессу (алгоритм attachreg), загружает
ее в память (алгоритм loadreg) и защищает от записи (read-only). Последний
шаг предполагает, что при попытке процесса записать что-либо в область ко-
манд будет получен отказ, вызванный системой защиты памяти. В случае обнару-
жения области с командами файла в списке активных областей осуществляется
проверка ее наличия в памяти (она может быть либо загружена в память, либо
выгружена из памяти) и присоединение ее к процессу. В завершение выполнения
алгоритма xalloc ядро снимает с области блокировку, а позднее, следуя алго-
ритму detachreg при выполнении функций exit или exec, уменьшает значение
счетчика областей. В традиционных реализациях системы поддерживается таблица
команд, к которой ядро обращается в случаях, подобных описанному. Таким об-
разом, совокупность областей команд можно рассматривать как новую версию
этой таблицы.
Напомним, что если область при выполнении алгоритма allocreg (Раздел
6.5.2) выделяется впервые, ядро увеличивает значение счетчика ссылок на ин-
декс, ассоциированный с областью, при этом значение счетчика ссылок нами уже