The design of the unix operating system by Maurice J

Вид материалаРеферат
Подобный материал:
1   ...   20   21   22   23   24   25   26   27   ...   55

После завершения всех этих действий ядро готово к созданию

для порожденного процесса пользовательского контекста. Ядро выде-

ляет память для адресного пространства процесса, его областей и

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

ластей родительского процесса и присоединяет с помощью алгоритма

attachreg каждую область к порожденному процессу. В системе с

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

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

мяти. Вспомним из раздела 6.2.4 о том, что в пространстве процес-

са хранится указатель на соответствующую запись в таблице процес-

сов. За исключением этого поля, во всем остальном содержимое ад-

ресного пространства порожденного процесса в начале совпадает с

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

диться после завершения алгоритма fork. Родительский процесс,

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

рому порожденный процесс уже не получит доступ автоматически.

Итак, ядро завершило создание статической части контекста по-

рожденного процесса; теперь оно приступает к созданию динамичес-

кой части. Ядро копирует в нее первый контекстный уровень роди-

тельского процесса, включающий в себя сохраненный регистровый

контекст задачи и стек ядра в момент вызова функции fork. Если в

данной реализации стек ядра является частью пространства процес-

са, ядро в момент создания пространства порожденного процесса ав-

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

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

ти, ассоциированное с порожденным процессом, свой системный стек.

В любом случае стек ядра для порожденного процесса совпадает с

системным стеком его родителя. Далее ядро создает для порожденно-

го процесса фиктивный контекстный уровень (2), в котором содер-

жится сохраненный регистровый контекст из первого контекстного

уровня. Значения счетчика команд (регистр PC) и других регистров,

сохраняемые в регистровом контексте, устанавливаются таким обра-

зом, чтобы с их помощью можно было "восстанавливать" контекст по-

рожденного процесса, пусть даже последний еще ни разу не испол-

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

он порожденный. Например, если программа ядра проверяет значение,

хранящееся в регистре 0, для того, чтобы выяснить, является ли

данный процесс родительским или же порожденным, то это значение

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

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


ется тот же, что и при переключении контекста (см. предыдущую

главу).

Если контекст порожденного процесса готов, родительский про-

цесс завершает свою роль в выполнении алгоритма fork, переводя

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

памяти" и возвращая пользователю его идентификатор. Затем, ис-

пользуя обычный алгоритм планирования, ядро выбирает порожденный

процесс для исполнения и тот "доигрывает" свою роль в алгоритме

fork. Контекст порожденного процесса был задан родительским про-

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

зобновляется после приостанова в ожидании ресурса. Порожденный

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

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

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

функции возвращает нулевое значение.

На Рисунке 7.3 представлена логическая схема взаимодействия

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

ных ядра сразу после завершения системной функции fork. Итак, оба

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

тельским процессом к моменту исполнения функции fork, при этом

значение счетчика ссылок на каждый из этих файлов в таблице фай-

лов на единицу больше, чем до вызова функции. Порожденный процесс

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

логи, значение же счетчика ссылок на индекс каждого из этих ката-

логов так же становится на единицу больше, чем до вызова функции.

Содержимое областей команд, данных и стека (задачи) у обоих про-

цессов совпадает; по типу области и версии системной реализации

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

в физических адресах.

Рассмотрим приведенную на Рисунке 7.4 программу, которая

представляет собой пример разделения доступа к файлу при исполне-

нии функции fork. Пользователю следует передавать этой программе


Родительский процесс

----------------------------------------------┐ Таблица

│ ----------┐ Частная Адресное простран- │ файлов

│ │ Область │ таблица ство процесса │ ----------┐

│ │ данных │ областей -------------------┐│ │ │

│ L---------- процесса │ Открытые файлы -││- ┐ │ │

│ │ -------┐ │ ││ │ │

│ - - - ┐ │ + ┐│ Текущий каталог -││┐ │ +---------+

│ +------+ │ ││ - -│ │

│ ----------┐ L + │ ││ Измененный корень│││ │ │ │

│ │ Стек │ +------+ L-------------------│ +---------+

│ │ задачи + - + │ │-------------------┐││ │ │ │

│ L---------- L------- │ ││ │ │

│ ││ │││ │ │ │

│ │ ││ +---------+

│ - - - - - - - - --│ Стек ядра │││ + - + │

│ │ L-------------------│ │ │

L----------------------------------------------│ │ +---------+

│ │

-----+----┐ │ │ │ │

│Разделяе-│ │ │

│ мая │ │ │ +---------+

│ область │ - -│ │

│ команд │ │ │ │ │

L----T----- +---------+

│ │ L----------

L - - - - - - - - ┐

----------------------------------------------┐L -│┐ Таблица

│ ----------┐ Частная │ Адресное простран- │ файлов

│ │ Область │ таблица ство процесса │ ││ ----------┐

│ │ данных │ областей │-------------------┐│ │ │

│ L---------- процесса │ Открытые файлы -││- -│ │ │

│ │ -------┐ ││ ││ │ │

│ - - - ┐ │ +-│ Текущий каталог -││┐ │ +---------+

│ +------+ │ ││ - + │

│ ----------┐ L + │ │ Измененный корень││L - - -│ │

│ │ Стек │ +------+ L-------------------│ +---------+

│ │ задачи + - + │ -------------------┐│ │ │

│ L---------- L------- │ ││ │ │

│ │ ││ │ │

│ │ ││ +---------+

│ │ Стек ядра ││ │ │

│ L-------------------│ │ │

L---------------------------------------------- +---------+

Порожденный процесс │ │

│ │

│ │

L----------


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

нии функции fork


два параметра - имя существующего файла и имя создаваемого файла.

Процесс открывает существующий файл, создает новый файл и - при

условии отсутствия ошибок - порождает новый процесс. Внутри прог-

раммы ядро делает копию контекста родительского процесса для по-

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

ресном пространстве, а порожденный - в другом. Каждый из процес-

сов может работать со своими собственными копиями глобальных пе-

ременных fdrd, fdwt и c, а также со своими собственными копиями

стековых переменных argc и argv, но ни один из них не может обра-

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

нии функции fork ядро делает копию адресного пространства первого

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

ледует доступ к файлам родительского (то есть к файлам, им ранее

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


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

│ #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); │

│ } │

│ } │

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


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

процессы разделяют доступ к файлу


рипторов.

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

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

байту информацию из исходного файла и переписывают ее в файл вы-

вода. Функция rdwrt возвращает управление, когда при считывании

обнаруживается конец файла. Ядро перед тем уже увеличило значения

счетчиков ссылок на исходный и результирующий файлы в таблице

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

одним и тем же строкам в таблице. Таким образом, дескрипторы fdrd

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

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

качестве fdwt, - на запись, соответствующую результирующему файлу

(файлу вывода). Поэтому оба процесса никогда не обратятся вместе

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

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

ловые указатели после каждой операции чтения или записи. Несмотря

на то, что, казалось бы, из-за того, что процессы распределяют

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

раза быстрее, содержимое результирующего файла зависит от очеред-

ности, в которой ядро запускает процессы. Если ядро запускает

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

(чередуя и спаренные вызовы функций read-write), содержимое ре-

зультирующего файла будет совпадать с содержимым исходного файла.

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

исходного файла последовательность из двух символов "ab". Предпо-

ложим, что родительский процесс считал символ "a", но не успел

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

процесса. Если порожденный процесс считывает символ "b" и записы-

вает его в результирующий файл до возобновления родительского

процесса, строка "ab" в результирующем файле будет иметь вид

"ba". Ядро не гарантирует согласование темпов выполнения процес-

сов.

Теперь перейдем к программе, представленной на Рисунке 7.5, в

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

дескрипторы 0 и 1 (соответствующие стандартному вводу и стандарт-

ному выводу). При каждом выполнении системной функции pipe произ-

водится назначение двух файловых дескрипторов в массивах to_par и

to_chil. Процесс вызывает функцию fork и делает копию своего кон-

текста: каждый из процессов имеет доступ только к своим собствен-

ным данным, так же как и в предыдущем примере. Родительский про-

цесс закрывает файл стандартного вывода (дескриптор 1) и дублиру-

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

первое свободное место в таблице дескрипторов родительского про-

цесса образовалось в результате только что выполненной операции

закрытия (close) файла вывода, ядро переписывает туда дескриптор

записи в канал и этот дескриптор становится дескриптором файла

стандартного вывода для to_chil. Те же самые действия родитель-

ский процесс выполняет в отношении дескриптора файла стандартного

ввода, заменяя его дескриптором чтения из канала to_par. И порож-

денный процесс закрывает файл стандартного ввода (дескриптор 0) и

так же дублирует дескриптор чтения из канала to_chil. Поскольку

первое свободное место в таблице дескрипторов файлов прежде было

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

дескриптор чтения из канала to_chil. Аналогичные действия выпол-

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

няя его дескриптором записи в канал to_par. И тот, и другой про-

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

pipe - хорошая традиция, в чем нам еще предстоит убедиться. В ре-

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

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

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

дартный ввод. Когда же порожденный процесс пишет данные в стан-

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

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

ввод. Так через два канала оба процесса обмениваются сообщениями.

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

ности процессы выполняют свои действия. Таким образом, нет ника-

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

функции fork раньше или позже, чем порожденному процессу. И так

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

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

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

ет функцию read раньше, чем его родитель выполнит write, он будет

приостановлен до тех пор, пока родительский процесс не произведет

запись в канал и тем самым не возобновит выполнение потомка. Если

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

приступит к чтению из канала, первый процесс не сможет в свою

очередь считать данные из стандартного ввода, пока второй процесс

не прочитает все из своего стандартного ввода и не произведет за-

пись данных в стандартный вывод. С этого места порядок работы

жестко фиксирован: каждый процесс завершает выполнение функций

read и write и не может выполнить следующую операцию read до тех

пор, пока другой процесс не выполнит пару read-write. Родитель-


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

│ #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]); /* дублирование дескриптора записи │

│ в канал в позицию стандартного │

│ вывода */ │

│ 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)); │

│ } │

│ } │

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


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


ский процесс после 15 итераций завершает работу; порожденный про-

цесс наталкивается на конец файла ("end-of-file"), поскольку ка-

нал не связан больше ни с одним из записывающих процессов, и тоже

завершает работу. Если порожденный процесс попытается произвести

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

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

чтения.

Мы упомянули о том, что хорошей традицией в программировании

является закрытие ненужных файловых дескрипторов. В пользу этого

говорят три довода. Во-первых, дескрипторы файлов постоянно нахо-

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

их количество. Во-вторых, во время исполнения порожденного про-

цесса присвоение дескрипторов в новом контексте сохраняется (в

чем мы еще убедимся). Закрытие ненужных файлов до запуска процес-

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

ных" условиях, свободных от любых неожиданностей, имея открытыми

только файлы стандартного ввода-вывода и ошибок. Наконец, функция

read для канала возвращает признак конца файла только в том слу-

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

Если считывающий процесс будет держать дескриптор записи в канал

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

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

не работала бы надлежащим образом, если бы перед входом в цикл

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

в канал.


7.2 СИГНАЛЫ


Сигналы сообщают процессам о возникновении асинхронных собы-

тий. Посылка сигналов производится процессами - друг другу, с по-

мощью функции kill, - или ядром. В версии V (вторая редакция)

системы UNIX существуют 19 различных сигналов, которые можно

классифицировать следующим образом:

* Сигналы, посылаемые в случае завершения выполнения процесса,

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

цию signal с параметром death of child (гибель потомка);

* Сигналы, посылаемые в случае возникновения вызываемых процес-

сом особых ситуаций, таких как обращение к адресу, находяще-

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

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

чтения (например, текст программы), или попытка исполнения

привилегированной команды, а также различные аппаратные ошиб-

ки;

* Сигналы, посылаемые во время выполнения системной функции при

возникновении неисправимых ошибок, таких как исчерпание сис-

темных ресурсов во время выполнения функции exec после осво-

бождения исходного адресного пространства (см. раздел 7.5);

* Сигналы, причиной которых служит возникновение во время вы-

полнения системной функции совершенно неожиданных ошибок,

таких как обращение к несуществующей системной функции (про-

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

ет ни одной из имеющихся функций), запись в канал, не

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

ние недопустимого значения в параметре "reference" системной

функции lseek. Казалось бы, более логично в таких случаях

вместо посылки сигнала возвращать код ошибки, однако с прак-

тической точки зрения для аварийного завершения процессов, в

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

ляется именно использование сигналов (*);


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

(*) Использование сигналов в некоторых обстоятельствах позволяет

обнаружить ошибки при выполнении программ, не проверяющих код

завершения вызываемых системных функций (сообщил Д.Ричи).