The design of the unix operating system by Maurice J
Вид материала | Реферат |
- Лекция 10. Файловые системы Unix, 116.79kb.
- Уровни рассмотрения, 314.07kb.
- Курс по операционным системам (на примере ос windows) Основан на учебном курсе Windows, 29.21kb.
- Выполнил ученик 11 «А» класса, 443.51kb.
- Ос лекция 1 (2-й семестр – временно), 101.4kb.
- Operating System, 7686.97kb.
- Unix-подобные операционные системы, характеристики, особенности, разновидности, 40.63kb.
- 1. ms sql server. Общие сведения, 66.03kb.
- Shanti ananda maurice, 89.84kb.
- Методические материалы, 3002.45kb.
После завершения всех этих действий ядро готово к созданию
для порожденного процесса пользовательского контекста. Ядро выде-
ляет память для адресного пространства процесса, его областей и
таблиц страниц, создает с помощью алгоритма 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. Казалось бы, более логично в таких случаях
вместо посылки сигнала возвращать код ошибки, однако с прак-
тической точки зрения для аварийного завершения процессов, в
которых возникают подобные ошибки, более предпочтительным яв-
ляется именно использование сигналов (*);
---------------------------------------
(*) Использование сигналов в некоторых обстоятельствах позволяет
обнаружить ошибки при выполнении программ, не проверяющих код
завершения вызываемых системных функций (сообщил Д.Ричи).