The design of the unix operating system by Maurice J

Вид материалаРеферат
7.5 Вызов других программ
Подобный материал:
1   ...   23   24   25   26   27   28   29   30   ...   55
значения, в закодированном виде содержащие информацию о причине

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

ся по назначению довольно редко. Ядро передает в соответствующие

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

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

режиме ядра и в режиме задачи и, наконец, освобождает в таблице

процессов место, которое в ней занимал прежде прекративший су-

ществование процесс. Это место будет предоставлено новому процес-

су.

Если процесс, выполняющий функцию wait, имеет потомков, про-

должающих существование, он приостанавливается до получения

ожидаемого сигнала. Ядро не возобновляет по своей инициативе про-

цесс, приостановившийся с помощью функции wait: такой процесс мо-

жет возобновиться только в случае получения сигнала. На все сиг-

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

рассмотренным образом. Реакция процесса на сигнал "гибель потом-

ка" проявляется по-разному в зависимости от обстоятельств:

* По умолчанию (то есть если специально не оговорены никакие

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

торое он вошел с помощью функции wait, и запускает алгоритм

issig для опознания типа поступившего сигнала. Алгоритм issig

(Рисунок 7.7) рассматривает особый случай поступления сигнала

типа "гибель потомка" и возвращает "ложь". Поэтому ядро не

выполняет longjump из функции sleep, а возвращает управление

функции wait. Оно перезапускает функцию wait, находит потом-

ков, прекративших существование (по крайней мере, одного),

освобождает место в таблице процессов, занимаемое этими по-

томками, и выходит из функции wait, возвращая управление про-

цессу, вызвавшему ее.

* Если процессы принимает сигналы данного типа, ядро делает все

необходимые установки для запуска пользовательской функции

обработки сигнала, как и в случае поступления сигнала любого

другого типа.


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

│ алгоритм wait │

│ входная информация: адрес переменной для хранения значения│

│ status, возвращаемого завершающимся │

│ процессом │

│ выходная информация: идентификатор потомка и код возврата │

│ функции exit │

│ { │

│ если (процесс, вызвавший функцию wait, не имеет потом- │

│ ков) │

│ возвратить (ошибку); │

│ │

│ для (;;) /* цикл с внутренним циклом */ │

│ { │

│ если (процесс, вызвавший функцию wait, имеет потом-│

│ ков, прекративших существование) │

│ { │

│ выбрать произвольного потомка; │

│ передать его родителю информацию об использова-│

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

│ освободить в таблице процессов место, занимае- │

│ мое потомком; │

│ возвратить (идентификатор потомка, код возврата│

│ функции exit, вызванной потомком); │

│ } │

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

│ возвратить ошибку; │

│ приостановиться с приоритетом, допускающим прерыва-│

│ ния (до завершения потомка); │

│ } │

│ } │

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


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


* Если процесс игнорирует сигналы данного типа, ядро перезапус-

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

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

оставшихся потомков.

Например, если пользователь запускает программу, приведенную

на Рисунке 7.17, с параметром и без параметра, он получит разные

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

кает программу без параметра (единственный параметр - имя прог-

раммы, то есть argc равно 1). Родительский процесс порождает 15

потомков, которые в конечном итоге завершают свое выполнение с

кодом возврата 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);│

│ } │

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


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

ния сигнала "гибель потомка"


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

сов запись, соответствующую прекратившему существование потомку,

и продолжает выполнение функции 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); │

│ } │

│ } │

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


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

бель потомков"


функции signal, процесс распорядился игнорировать сигналы типа

"гибель потомка", ядро будет очищать записи, соответствующие

прекратившим существование процессам, автоматически. Иначе в ко-

нечном итоге из-за таких процессов может произойти переполнение

таблицы.


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


Системная функция exec дает возможность процессу запускать

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

няемый файл будет располагаться в пространстве памяти процесса.

Содержимое пользовательского контекста после вызова функции ста-

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

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

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

execve(filename,argv,envp)


где filename - имя исполняемого файла, argv - указатель на массив

параметров, которые передаются вызываемой программе, а envp -

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

зываемой программы. Вызов системной функции exec осуществляют

несколько библиотечных функций, таких как execl, execv, execle и

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

строки

main(argc,argv) ,

массив argv является копией одноименного параметра, передаваемого

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

зываемой программы, имеют вид "имя=значение" и содержат полезную

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

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

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


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

│ алгоритм exec │

│ входная информация: (1) имя файла │

│ (2) список параметров │

│ (3) список переменных среды │

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

│ { │

│ получить индекс файла (алгоритм namei); │

│ проверить, является ли файл исполнимым и имеет ли поль- │

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

│ прочитать информацию из заголовков файла и проверить, │

│ является ли он загрузочным модулем; │

│ скопировать параметры, переданные функции, из старого │

│ адресного пространства в системное пространство; │

│ для (каждой области, присоединенной к процессу) │

│ отсоединить все старые области (алгоритм detachreg);│

│ для (каждой области, определенной в загрузочном модуле) │

│ { │

│ выделить новые области (алгоритм allocreg); │

│ присоединить области (алгоритм attachreg); │

│ загрузить область в память по готовности (алгоритм │

│ loadreg); │

│ } │

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

│ ласть стека задачи; │

│ специальная обработка для setuid-программ, трассировка; │

│ проинициализировать область сохранения регистров задачи │

│ (в рамках подготовки к возвращению в режим задачи); │

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

│ } │

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


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


менную environ, которую заводит начальная процедура Си-интерпре-

татора.

На Рисунке 7.19 представлен алгоритм выполнения системной

функции exec. Сначала функция обращается к файлу по алгоритму

namei, проверяя, является ли файл исполнимым и отличным от ката-

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

программу. Затем ядро, считывая заголовок файла, определяет раз-

мещение информации в файле (формат файла).

На Рисунке 7.20 изображен логический формат исполняемого фай-

ла в файловой системе, обычно генерируемый транслятором или заг-

рузчиком. Он разбивается на четыре части:

1. Главный заголовок, содержащий информацию о том, на сколько

разделов делится файл, а также содержащий начальный адрес ис-

полнения процесса и некоторое "магическое число", описывающее

тип исполняемого файла.

2. Заголовки разделов, содержащие информацию, описывающую каждый

раздел в файле: его размер, виртуальные адреса, в которых он

располагается, и др.

3. Разделы, содержащие собственно "данные" файла (например,

текстовые), которые загружаются в адресное пространство про-

цесса.

4. Разделы, содержащие смешанную информацию, такую как таблицы

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

ладки.


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

│ Тип файла │

Главный заголовок │ Количество разделов │

│ Начальное состояние регис-│

│ тров │

+---------------------------+

│ Тип раздела │

Заголовок 1-го раздела │ Размер раздела │

│ Виртуальный адрес │

+---------------------------+

│ Тип раздела │

Заголовок 2-го раздела │ Размер раздела │

│ Виртуальный адрес │

+---------------------------+

│ │

│ │

│ │

+---------------------------+

│ Тип раздела │

Заголовок n-го раздела │ Размер раздела │

│ Виртуальный адрес │

+---------------------------+

Раздел 1 │ Данные (например, текст) │

+---------------------------+

Раздел 2 │ Данные │

+---------------------------+

│ │

│ │

│ │

+---------------------------+

Раздел n │ Данные │

+---------------------------+

│ Другая информация │

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


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


Указанные составляющие с развитием самой системы видоизменя-

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

главный заголовок с полем типа файла.

Тип файла обозначается коротким целым числом (представляется

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

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

характеристики его выполнения. Например, в машине PDP 11/70 опре-

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

что процесс, исполняющий файл, может использовать до 128 Кбайт

памяти вместо 64 Кбайт (**), тем не менее в системах с замещением

страниц тип файла все еще играет существенную роль, в чем нам

предстоит убедиться во время знакомства с главой 9.


---------------------------------------

(**) В PDP 11 "магические числа" имеют значения, соответствующие

командам перехода; при выполнении этих команд в ранних вер-

сиях системы управление передавалось в разные места програм-

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

файла. Эта особенность больше не используется с тех пор, как

система стала разрабатываться на языке Си.


Вернемся к алгоритму. Мы остановились на том, что ядро обра-

тилось к индексу файла и установило, что файл является исполни-

мым. Ядру следовало бы освободить память, занимаемую пользова-

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

подлежащей освобождению, располагаются передаваемые новой прог-

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

пространства в промежуточный буфер на время, пока не будут отве-

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

Поскольку параметрами функции exec выступают пользовательские

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

копирует в системную память адрес строки, а затем саму строку.

Для хранения строки в разных версиях системы могут быть выбраны

различные места. Чаще принято хранить строки в стеке ядра (ло-

кальная структура данных, принадлежащая программе ядра), на не-

распределяемых участках памяти (таких как страницы), которые мож-

но занимать только временно, а также во внешней памяти (на

устройстве выгрузки).

С точки зрения реализации проще всего для копирования пара-

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

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

системой, а также поскольку параметры функции exec могут иметь

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

дами. При рассмотрении других вариантов обычно останавливаются на

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

кам. Если доступ к страницам памяти в системе реализуется доволь-

но просто, строки следует размещать на страницах, поскольку обра-

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

(устройству выгрузки).

После копирования параметров функции exec в системную память

ядро отсоединяет области, ранее присоединенные к процессу, ис-

пользуя алгоритм detachreg. Несколько позже мы еще поговорим о

специальных действиях, выполняемых в отношении областей команд. К

рассматриваемому моменту процесс уже лишен пользовательского кон-

текста и поэтому возникновение в дальнейшем любой ошибки неизбеж-

но будет приводить к завершению процесса по сигналу. Такими ошиб-

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

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

большой размер или использующую области с пересекающимися адреса-

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

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

файла (алгоритмы allocreg, attachreg и loadreg, соответственно).

Область данных процесса изначально поделена на две части: данные,

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

ные, не определенные компилятором ("bss"). Область памяти перво-

начально выделяется для проинициализированных данных. Затем ядро

увеличивает размер области данных для размещения данных типа

"bss" (алгоритм growreg) и обнуляет их значения. Напоследок ядро

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

ранство памяти для хранения параметров функции exec. Если пара-

метры функции размещаются на страницах, те же страницы могут быть

использованы под стек. В противном случае параметры функции раз-

мещаются в стеке задачи.