The design of the unix operating system by Maurice J

Вид материалаРеферат
7.9 Загрузка системы и начальный процесс
G7.11 упражненияh
Подобный материал:
1   ...   26   27   28   29   30   31   32   33   ...   55

│ close(fildes[0]); │

│ /* стандартный вывод направляется в ка- │

│ нал */ │

│ /* команду исполняет порожденный про- │

│ цесс */ │

│ execlp(command1,command1,0); │

│ } │

│ /* вторая компонента командной строки */ │

│ close(stdin); │

│ dup(fildes[0]); │

│ close(fildes[0]); │

│ close(fildes[1]); │

│ /* стандартный ввод будет производиться из│

│ канала */ │

│ } │

│ execve(command2,command2,0); │

│ } │

│ /* с этого места продолжается выполнение родительского │

│ * процесса... │

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

│ * если это вытекает из введенной строки │

│ * / │

│ if (amper == 0) │

│ retid = wait(&status); │

│ } │

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


Рисунок 7.28. Основной цикл программы shell (продолжение)


Командные строки простейшего вида содержат имя программы и

несколько параметров, например:

who

grep -n include *.c

ls -l

Shell "ветвится" (fork) и порождает новый процесс, который и за-

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

Родительский процесс (shell) дожидается завершения потомка и пов-

торяет цикл считывания следующей команды.

Если процесс запускается асинхронно (на фоне основной прог-

раммы), как в следующем примере

nroff -mm bigdocument &

shell анализирует наличие символа амперсанд (&) и заносит резуль-

тат проверки во внутреннюю переменную amper. В конце основного

цикла shell обращается к этой переменной и, если обнаруживает в

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

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


Из рисунка видно, что процесс-потомок по завершении функции

fork получает доступ к командной строке, принятой shell'ом. Для

того, чтобы переадресовать стандартный вывод в файл, как в следу-

ющем примере

nroff -mm bigdocument > output

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

ке именем; если файл не удается создать (например, не разрешен

доступ к каталогу), процесс-потомок тут же завершается. В против-

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

вода и переназначает с помощью функции dup дескриптор этого файла

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

сохраняется для запускаемой программы. Подобным же образом shell

переназначает и стандартный ввод и стандартный вывод ошибок.


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

│ │

│ Shell │

│ │

L-----T------ wait



│ │

------+-----┐ exit

│ │

│ wc │

│ │

L-----T------ read



│ │

------+-----┐ write

│ │

│ ls - l │

│ │

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


Рисунок 7.29. Взаимосвязь между процессами, исполняющими ко-

мандную строку ls -l│wc


Из приведенного текста программы видно, как shell обрабатыва-

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

ная строка имеет вид:

ls -l│wc

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

томок создает канал. Затем процесс-потомок создает свое

ответвление; он и его потомок обрабатывают по одной компоненте

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

строки (ls): он собирается вести запись в канал, поэтому он зак-

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

каналу и закрывает старый дескриптор записи в канал, в котором (в

дескрипторе) уже нет необходимости. Родитель (wc) "внучатого"

процесса (ls) является потомком основного процесса, реализующего

программу shell'а (см. Рисунок 7.29). Этот процесс (wc) закрывает

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

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

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

исполняется вторая компонента командной строки. Оба порожденных

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

поступает на вход другого. Тем временем основной процесс дожида-

ется завершения своего потомка (wc), после чего продолжает свою

обычную работу: по завершении процесса, выполняющего команду wc,

вся командная строка является обработанной. Shell возвращается в

цикл и считывает следующую командную строку.


7.9 ЗАГРУЗКА СИСТЕМЫ И НАЧАЛЬНЫЙ ПРОЦЕСС


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

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

На разных машинах эта процедура имеет свои особенности, однако во

всех случаях она реализует одну и ту же цель: загрузить копию

операционной системы в основную память машины и запустить ее на

исполнение. Обычно процедура начальной загрузки включает в себя

несколько этапов. Переключением клавиш на пульте машины админист-

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

рузки, а может, нажав только одну клавишу, дать команду машине

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

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

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

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

ной загрузки (нулевого блока). Программа, содержащаяся в этом

блоке, загружает из файловой системы ядро ОС (например, из файла

с именем "/unix" или с другим именем, указанным администратором).

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

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

start, Рисунок 7.30).

Ядро инициализирует свои внутренние структуры данных. Среди

прочих структур ядро создает связные списки свободных буферов и

индексов, хеш-очереди для буферов и индексов, инициализирует

структуры областей, точки входа в таблицы страниц и т.д. По окон-

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

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

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

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

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

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

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

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

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

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

своему адресному пространству. Он увеличивает размер области до

надлежащей величины и переписывает программу загрузки из адресно-

го пространства ядра в новую область: эта программа теперь будет

определять контекст процесса 1. Затем процесс 1 сохраняет регист-

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

дачи и исполняет только что переписанную программу. В отличие от

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

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

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

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

файла "/etc/init". Обычно процесс 1 именуется процессом init,

поскольку он отвечает за инициализацию новых процессов.

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

помощью функции exec, в адресное пространство процесса 1 ? Он мог

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


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

│ алгоритм start /* процедура начальной загрузки системы */│

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

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

│ { │

│ проинициализировать все структуры данных ядра; │

│ псевдо-монтирование корня; │

│ сформировать среду выполнения процесса 0; │

│ создать процесс 1; │

│ { │

│ /* процесс 1 */ │

│ выделить область; │

│ подключить область к адресному пространству процесса│

│ init; │

│ увеличить размер области для копирования в нее ис- │

│ полняемого кода; │

│ скопировать из пространства ядра в адресное прост- │

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

│ сом; │

│ изменить режим выполнения: вернуться из режима ядра │

│ в режим задачи; │

│ /* процесс init далее выполняется самостоятельно -- │

│ * в результате выхода в режим задачи, │


│ * init исполняет файл "/etc/init" и становится │

│ * "обычным" пользовательским процессом, производя- │

│ * щим обращения к системным функциям │

│ */ │

│ } │

│ /* продолжение нулевого процесса */ │

│ породить процессы ядра; │

│ /* нулевой процесс запускает программу подкачки, управ- │

│ * ляющую распределением адресного пространства процес- │

│ * сов между основной памятью и устройствами выгрузки. │

│ * Это бесконечный цикл; нулевой процесс обычно приоста-│

│ * навливает свою работу, если необходимости в нем боль-│

│ * ше нет. │

│ */ │

│ исполнить программу, реализующую алгоритм подкачки; │

│ } │

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


Рисунок 7.30. Алгоритм загрузки системы


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

труднее реализовать, ибо в этом случае функции exec пришлось бы

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

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

init, усложнила бы программу реализации функции exec и отрица-

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

случаях.

Процесс init (Рисунок 7.31) выступает диспетчером процессов,

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

зователю регистрироваться в системе. Инструкции о том, какие про-

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

"/etc/inittab". Строки файла включают в себя идентификатор состо-

яния "id" (однопользовательский режим, многопользовательский и т.

д.), предпринимаемое действие (см. упражнение 7.43) и специфика-

цию программы, реализующей это действие (см. Рисунок 7.32). Про-

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

жит идентификатор состояния, соответствующего тому состоянию, в

котором находится процесс, и создает процесс, исполняющий прог-

рамму с указанной спецификацией. Например, при запуске в много-

пользовательском режиме (состояние 2) процесс init обычно порож-


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

│ алгоритм init /* процесс init, в системе именуемый │

│ "процесс 1" */ │

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

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

│ { │

│ fd = open("/etc/inittab",O_RDONLY); │

│ while (line_read(fd,buffer)) │

│ { │

│ /* читать каждую строку файлу */ │

│ if (invoked state != buffer state) │

│ continue; /* остаться в цикле while */ │

│ /* найден идентификатор соответствующего состояния │

│ */ │

│ if (fork() == 0) │

│ { │

│ execl("процесс указан в буфере"); │

│ exit(); │

│ } │

│ /* процесс init не дожидается завершения потомка */ │

│ /* возврат в цикл while */ │

│ } │

│ │

│ while ((id = wait((int*) 0)) != -1) │

│ { │

│ /* проверка существования потомка; │

│ * если потомок прекратил существование, рассматри- │

│ * вается возможность его перезапуска */ │

│ /* в противном случае, основной процесс просто про- │

│ * должает работу */ │

│ } │


│ } │

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


Рисунок 7.31. Алгоритм выполнения процесса init


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

│ Формат: идентификатор, состояние, действие, спецификация │

│ процесса │

│ Поля разделены между собой двоеточиями │

│ Комментарии в конце строки начинаются с символа '#' │

│ │

│ co::respawn:/etc/getty console console #Консоль в машзале│

│ 46:2:respawn:/etc/getty -t 60 tty46 4800H #комментарии │

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


Рисунок 7.32. Фрагмент файла inittab


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

линий, входящих в состав системы. Если регистрация пользователя

прошла успешно, getty-процесс, пройдя через процедуру login, за-

пускает на исполнение регистрационный shell (см. главу 10). Тем

временем процесс init находится в состоянии ожидания (wait), наб-

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

чатых" процессов, оставшихся "сиротами" после гибели своих роди-

телей.

Процессы в системе UNIX могут быть либо пользовательскими,

либо управляющими, либо системными. Большинство из них составляют

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

миналы. Управляющие процессы не связаны с конкретными пользовате-

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

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

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

печати и т.д. Процесс init может порождать управляющие процессы,

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

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

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

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

вызова соответствующих системных функций.

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

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

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

ки. Системные процессы похожи на управляющие процессы тем, что

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

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

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

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

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

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

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

придется еще раз перекомпилировать ядро.


7.10 ВЫВОДЫ


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

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

нием процесса. Системная функция fork создает новый процесс, ко-

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

родительскому процессу. Особенность реализации функции fork сос-

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

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

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

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

ка. Все процессы завершают свое выполнение вызовом функции exit,

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

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

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

томка, используя системную функцию wait. Системная функция exec

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

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

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

сом, и назначает процессу новые области в соответствии с потреб-

ностями исполняемого файла. Совместное использование областей ко-

манд и наличие режима "sticky-bit" дают возможность более рацио-

нально использовать память и экономить время, затрачиваемое на

подготовку к запуску программ. Простым пользователям предоставля-

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

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

ции setuid и setuid-программ. С помощью функции brk процесс может

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

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

При получении сигнала производится обращение к специальной функ-

ции обработки сигнала с внесением соответствующих изменений в

стек задачи и в сохраненный регистровый контекст задачи. Процессы

могут сами посылать сигналы, используя системную функцию kill,

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

ных группе процессов, прибегая к услугам функции setpgrp.

Командный процессор shell и процесс начальной загрузки init

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

набор операций, в других системах обычно выполняемых ядром. Shell

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

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

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

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

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

частности, управляющие работой пользователя за терминалом. Когда

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


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

ции файла "/etc/inittab".


G7.11 УПРАЖНЕНИЯH


1. Запустите с терминала программу, приведенную на Рисунке

7.33. Переадресуйте стандартный вывод данных в файл и срав-

ните результаты между собой.

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

│ main() │

│ { │

│ printf("hello\n"); │

│ if (fork() == 0) │

│ printf("world\n"); │

│ } │

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


Рисунок 7.33. Пример модуля, содержащего вызов

функции fork и обращение к стандарт-

ному выводу


2. Разберитесь в механизме работы программы, приведенной на Ри-

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

мы на Рисунке 7.4.

3. Еще раз обратимся к программе, приведенной на Рисунке 7.5 и

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

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

ся вести обмен сообщениями, используя один канал ?

4. Возможна ли потеря информации в случае, когда процесс полу-

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

возможность отреагировать на них надлежащим образом ? (Расс-

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

ченных сигналов о прерывании.) Есть ли необходимость в реше-

нии этой проблемы ?

5. Опишите механизм работы системной функции kill.

6. Процесс в программе на Рисунке 7.35 принимает сигналы типа

"гибель потомка" и устанавливает функцию обработки сигналов

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

мы ?

7. Когда процесс получает сигналы определенного типа и не обра-

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

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

текущем каталоге процесса файл с именем "core" и копирует в