-------------------+ Рисунок 7.23. Алгоритм выделения областей команд файл, связанный с областью. Если бы значение счетчика ссылок стало равным 0, 210 ядро могло бы передать копию индекса в памяти другому файлу, тем самым делая сомнительным значение указателя на индекс в записи таблицы областей: если бы пользователю пришлось исполнить новый файл, используя функцию exec, ядро по ошибке связало бы его с областью команд старого файла. Эта проблема устраня- ется благодаря тому, что ядро при выполнении алгоритма allocreg увеличивает значение счетчика ссылок на индекс, предупреждая тем самым переназначение индекса в памяти другому файлу. Когда процесс во время выполнения функций exit или exec отсоединяет область команд, ядро уменьшает значение счетчика ссылок на индекс (по алгоритму freereg), если только связь индекса с об- ластью не помечена как "неотъемлемая". Таблица индексов Таблица областей +----------------+ что могло бы прои- +----------------+ | - | зойти, если бы счет- | - | | - | чик ссылок на индекс | - | | - | файла /bin/date был | - | | - | равен 0 +----------------+ | - | | область команд | | - | -- - - - - -|- для файла | | - | | | /bin/who | +----------------+ - +----------------+ | копия индекса -|- - - - - -+ | - | | файла /bin/date| | - | | в памяти <+-----------+ | - | +----------------+ | +----------------+ | - | | | область команд | | - | +-----------+- для файла | | - | указатель на| /bin/date | | - | копию индек-+----------------+ | - | са в памяти | - | | - | | - | +----------------+ +----------------+ Рисунок 7.24. Взаимосвязь между таблицей индексов и таблицей областей в случае совместного использования процессами одной области команд Рассмотрим в качестве примера ситуацию, приведенную на Рисунке 7.21, где показана взаимосвязь между структурами данных в процессе выполнения функции exec по отношению к файлу "/bin/date" при условии расположения команд и дан- ных файла в разных областях. Когда процесс исполняет файл "/bin/date" первый раз, ядро назначает для команд файла точку входа в таблице областей (Рисунок 7.24) и по завершении выполнения функции exec оставляет счетчик ссылок на индекс равным 1. Когда файл "/bin/date" завершается, ядро запускает алгорит- мы detachreg и freereg, сбрасывая значение счетчика ссылок в 0. Однако, если ядро в первом случае не увеличило значение счетчика, оно по завершении функ- ции exec останется равным 0 и индекс на всем протяжении выполнения процесса будет находиться в списке свободных индексов. Предположим, что в это время свободный индекс понадобился процессу, запустившему с помощью функции exec файл "/bin/who", тогда ядро может выделить этому процессу индекс, ранее при- надлежавший файлу "/ bin/date". Просматривая таблицу областей в поисках ин- декса файла "/bin/who", ядро вместо него выбрало бы индекс файла "/bin/date". Считая, что область содержит команды файла "/bin/who", ядро ис- полнило бы совсем не ту программу. Поэтому значение счетчика ссылок на ин- декс активного файла, связанного с разделяемой областью команд, должно быть не меньше единицы, чтобы ядро не могло переназначить индекс другому файлу. Возможность совместного использования различными процессами одних и тех же областей команд позволяет экономить время, затрачиваемое на запуск прог- раммы с помощью функции exec. Администраторы системы могут с помощью систем- 211 ной функции (и команды) chmod устанавливать для часто исполняемых файлов ре- жим "sticky-bit", сущность которого заключается в следующем. Когда процесс исполняет файл, для которого установлен режим "sticky-bit", ядро не освобож- дает область памяти, отведенную под команды файла, отсоединяя область от процесса во время выполнения функций exit или exec, даже если значение счет- чика ссылок на индекс становится равным 0. Ядро оставляет область команд в первоначальном виде, при этом значение счетчика ссылок на индекс равно 1, пусть даже область не подключена больше ни к одному из процессов. Если же файл будет еще раз запущен на выполнение (уже другим процессом), ядро в таб- лице областей обнаружит запись, соответствующую области с командами файла. Процесс затратит на запуск файла меньше времени, так как ему не придется чи- тать команды из файловой системы. Если команды файла все еще находятся в па- мяти, в их перемещении не будет необходимости; если же команды выгружены во внешнюю память, будет гораздо быстрее загрузить их из внешней памяти, чем из файловой системы (см. об этом в главе 9). Ядро удаляет из таблицы областей записи, соответствующие областям с ко- мандами файла, для которого установлен режим "sticky-bit" (иными словами, когда область помечена как "неотъемлемая" часть файла или процесса), в сле- дующих случаях: 1. Если процесс открыл файл для записи, в результате соответствующих опера- ций содержимое файла изменится, при этом будет затронуто и содержимое области. 2. Если процесс изменил права доступа к файлу (chmod), отменив режим "sticky-bit", файл не должен оставаться в таблице областей. 3. Если процесс разорвал связь с файлом (unlink), он не сможет больше ис- полнять этот файл, поскольку у файла не будет точки входа в файловую систему; следовательно, и все остальные процессы не будут иметь доступа к записи в таблице областей, соответствующей файлу. Поскольку область с командами файла больше не используется, ядро может освободить ее вместе с остальными ресурсами, занимаемыми файлом. 4. Если процесс демонтирует файловую систему, файл перестает быть доступным и ни один из процессов не может его исполнить. В остальном - все как в предыдущем случае. 5. Если ядро использовало уже все пространство внешней памяти, отведенное под выгрузку задач, оно пытается освободить часть памяти за счет облас- тей, имеющих пометку "sticky-bit", но не используемых в настоящий мо- мент. Несмотря на то, что эти области могут вскоре понадобиться другим процессам, потребности ядра являются более срочными. В первых двух случаях область команд с пометкой "sticky-bit" должна быть освобождена, поскольку она больше не отражает текущее состояние файла. В ос- тальных случаях это делается из практических соображений. Конечно же ядро освобождает область только при том условии, что она не используется ни одним из выполняющихся процессов (счетчик ссылок на нее имеет нулевое значение); в противном случае это привело бы к аварийному завершению выполнения системных функций open, unlink и umount (случаи 1, 3 и 4, соответственно). Если процесс запускает с помощью функции exec самого себя, алгоритм вы- полнения функции несколько усложняется. По команде sh script командный процессор shell порождает новый процесс (новую ветвь), который инициирует запуск shell'а (с помощью функции exec) и исполняет команды файла "script". Если процесс запускает самого себя и при этом его область команд допускает совместное использование, ядру придется следить за тем, чтобы при обращении ветвей процесса к индексам и областям не возникали взаимные блоки- ровки. Иначе говоря, ядро не может, не снимая блокировки со "старой" области команд, попытаться заблокировать "новую" область, поскольку на самом деле это одна и та же область. Вместо этого ядро просто оставляет "старую" об- 212 ласть команд присоединенной к процессу, так как в любом случае ей предстоит повторное использование. Обычно процессы вызывают функцию exec после функции fork; таким образом, во время выполнения функции fork процесс-потомок копирует адресное простран- ство своего родителя, но сбрасывает его во время выполнения функции exec и по сравнению с родителем исполняет образ уже другой программы. Не было бы более естественным объединить две системные функции в одну, которая бы заг- ружала программу и исполняла ее под видом нового процесса ? Ричи высказал предположение, что возникновение fork и exec как отдельных системных функций обязано тому, что при создании системы UNIX функция fork была добавлена к уже существующему образу ядра системы (см. [Ritchie 84a], стр.1584). Однако, разделение fork и exec важно и с функциональной точки зрения, поскольку в этом случае процессы могут работать с дескрипторами файлов стандартного вво- да-вывода независимо, повышая тем самым "элегантность" использования кана- лов. Пример, показывающий использование этой возможности, приводится в раз- деле 7.8. 7.6 КОД ИДЕНТИФИКАЦИИ ПОЛЬЗОВАТЕЛЯ ПРОЦЕССА Ядро связывает с процессом два кода идентификации пользователя, не зави- сящих от кода идентификации процесса: реальный (действительный) код иденти- фикации пользователя и исполнительный код или setuid (от "set user ID" - ус- тановить код идентификации пользователя, под которым процесс будет испол- няться). Реальный код идентифицирует пользователя, несущего ответственность за выполняющийся процесс. Исполнительный код используется для установки прав собственности на вновь создаваемые файлы, для проверки прав доступа к файлу и разрешения на посылку сигналов процессам через функцию kill. Процессы мо- гут изменять исполнительный код, запуская с помощью функции exec программу setuid или запуская функцию setuid в явном виде. Программа setuid представляет собой исполняемый файл, имеющий в поле ре- жима доступа установленный бит setuid. Когда процесс запускает программу setuid на выполнение, ядро записывает в поля, содержащие реальные коды иден- тификации, в таблице процессов и в пространстве процесса код идентификации владельца файла. Чтобы как-то различать эти поля, назовем одно из них, кото- рое хранится в таблице процессов, сохраненным кодом идентификации пользова- теля. Рассмотрим пример, иллюстрирующий разницу в содержимом этих полей. Синтаксис вызова системной функции setuid: setuid(uid) где uid - новый код идентификации пользователя. Результат выполнения функции зависит от текущего значения реального кода идентификации. Если реальный код идентификации пользователя процесса, вызывающего функцию, указывает на су- перпользователя, ядро записывает значение uid в поля, хранящие реальный и исполнительный коды идентификации, в таблице процессов и в пространстве про- цесса. Если это не так, ядро записывает uid в качестве значения исполнитель- ного кода идентификации в пространстве процесса и то только в том случае, если значение uid равно значению реального кода или значению сохраненного кода. В противном случае функция возвращает вызывающему процессу ошибку. Процесс наследует реальный и исполнительный коды идентификации у своего ро- дителя (в результате выполнения функции fork) и сохраняет их значения после вызова функции exec. На Рисунке 7.25 приведена программа, демонстрирующая использование функ- ции setuid. Предположим, что исполняемый файл, полученный в результате тран- сляции исходного текста программы, имеет владельца с именем "maury" (код идентификации 8319) и установленный бит setuid; право его исполнения предос- тавлено всем пользователям. Допустим также, что пользователи "mjb" (код идентификации 5088) и "maury" являются владельцами файлов с теми же именами, каждый из которых доступен только для чтения и только своему владельцу. Во время исполнения программы пользователю "mjb" выводится следующая информа- 213 ция: uid 5088 euid 8319 fdmjb -1 fdmaury 3 after setuid(5088): uid 5088 euid 5088 fdmjb 4 fdmaury -1 after setuid(8319): uid 5088 euid 8319 Системные функции getuid и geteuid возвращают значения реального и исполни- тельного кодов идентификации пользователей процесса, для +------------------------------------------------------------+ | #include | | main() | | { | | int uid,euid,fdmjb,fdmaury; | | | | uid = getuid(); /* получить реальный UID */ | | euid = geteuid(); /* получить исполнительный UID */| | printf("uid %d euid %d\n",uid,euid); | | | | fdmjb = open("mjb",O_RDONLY); | | fdmaury = open("maury",O_RDONLY); | | printf("fdmjb %d fdmaury %d\n",fdmjb,fdmaury); | | | | setuid(uid); | | printf("after setuid(%d): uid %d euid %d\n",uid, | | getuid(),geteuid()); | | | | fdmjb = open("mjb",O_RDONLY); | | fdmaury = open("maury",O_RDONLY); | | printf("fdmjb %d fdmaury %d\n",fdmjb,fdmaury); | | | | setuid(uid); | | printf("after setuid(%d): uid %d euid %d\n",euid, | | getuid(),geteuid()); | | } | +------------------------------------------------------------+ Рисунок 7.25. Пример выполнения программы setuid пользователя "mjb" это, соответственно, 5088 и 8319. Поэтому процесс не мо- жет открыть файл "mjb" (ибо он имеет исполнительный код идентификации поль- зователя (8319), не разрешающий производить чтение файла), но может открыть файл "maury". После вызова функции setuid, в результате выполнения которой в поле исполнительного кода идентификации пользователя ("mjb") заносится зна- чение реального кода идентификации, на печать выводятся значения и того, и другого кода идентификации пользователя "mjb": оба равны 5088. Теперь про- цесс может открыть файл "mjb", поскольку он исполняется под кодом идентифи- кации пользователя, имеющего право на чтение из файла, но не может открыть файл "maury". Наконец, после занесения в поле исполнительного кода идентифи- кации значения, сохраненного функцией setuid (8319), на печать снова выво- дятся значения 5088 и 8319. Мы показали, таким образом, как с помощью прог- раммы setuid процесс может изменять значение кода идентификации пользовате- ля, под которым он исполняется. Во время выполнения программы пользователем "maury" на печать выводится следующая информация: uid 8319 euid 8319 fdmjb -1 fdmaury 3 after setuid(8319): uid 8319 euid 8319 fdmjb -1 fdmaury 4 214 after setuid(8319): uid 8319 euid 8319 Реальный и исполнительный коды идентификации пользователя во время выполне- ния программы остаются равны 8319: процесс может открыть файл "maury", но не может открыть файл "mjb". Исполнительный код, хранящийся в пространстве про- цесса, занесен туда в результате последнего исполнения функции или программы setuid; только его значением определяются права доступа процесса к файлу. С помощью функции setuid исполнительному коду может быть присвоено значение сохраненного кода (из таблицы процессов), т.е. то значение, которое исполни- тельный код имел в самом начале. Примером программы, использующей вызов системной функции setuid, может служить программа регистрации пользователей в системе (login). Параметром функции setuid при этом является код идентификации суперпользователя, таким образом, программа login исполняется под кодом суперпользователя из корня системы. Она запрашивает у пользователя различную информацию, например, имя и пароль, и если эта информация принимается системой, программа запускает функцию setuid, чтобы установить значения реального и исполнительного кодов идентификации в соответствии с информацией, поступившей от пользователя (при этом используются данные файла "/etc/passwd"). В заключение программа login инициирует запуск командного процессора shell, который будет исполняться под указанными пользовательскими кодами идентификации. Примером setuid-программы является программа, реализующая команду mkdir. В разделе 5.8 уже говорилось о том, что создать каталог может только про- цесс, выполняющийся под управлением суперпользователя. Для того, чтобы пре- доставить возможность создания каталогов простым пользователям, команда mkdir была выполнена в виде setuid-программы, принадлежащей корню системы и имеющей права суперпользователя. На время исполнения команды mkdir процесс получает права суперпользователя, создает каталог, используя функцию mknod, и предоставляет права собственности и доступа к каталогу истинному пользова- телю процесса. 7.7 ИЗМЕНЕНИЕ РАЗМЕРА ПРОЦЕССА С помощью системной функции brk процесс может увеличивать и уменьшать размер области данных. Синтаксис вызова функции: brk(endds); где endds - старший виртуальный адрес области данных процесса (адрес верхней границы). С другой стороны, пользователь может обратиться к функции следую- щим образом: oldendds = sbrk(increment); где oldendds - текущий адрес верхней границы области, increment - число байт, на которое изменяется значение oldendds в результате выполнения функ- ции. Sbrk - это имя стандартной библиотечной подпрограммы на Си, вызывающей функцию brk. Если размер области данных процесса в результате выполнения функции увеличивается, вновь выделяемое пространство имеет виртуальные адре- са, смежные с адресами увеличиваемой области; таким образом, виртуальное ад- ресное пространство процесса расширяется. При этом ядро проверяет, не превы- шает ли новый размер процесса максимально-допустимое значение, принятое для него в системе, а также не накладывается ли новая область данных процесса на виртуальное адресное пространство, отведенное ранее для других целей (Рису- нок 7.26). Если все в порядке, ядро запускает алгоритм growreg, присоединяя к области данных внешнюю память (например, таблицы страниц) и увеличивая значение поля, описывающего размер процесса. В системе с замещением страниц ядро также отводит под новую область пространство основной памяти и обнуляет его содержимое; если свободной памяти нет, ядро освобождает память путем выгрузки процесса (более подробно об этом мы поговорим в главе 9). Если с помощью функции brk процесс уменьшает размер области данных, ядро освобожда- ет часть ранее выделенного адресного пространства; когда процесс попытается обратиться к данным по виртуальным адресам, принадлежащим освобожденному 215 пространству, он столкнется с ошибкой адресации. +------------------------------------------------------------+ | алгоритм brk | | входная информация: новый адрес верхней границы области | | данных | | выходная информация: старый адрес верхней границы области | | данных | | { | | заблокировать область данных процесса; | | если (размер области увеличивается) | | если (новый размер области имеет недопустимое зна-| | чение) | | { | | снять блокировку с области; | | вернуть (ошибку); | | } | | изменить размер области (алгоритм growreg); | | обнулить содержимое присоединяемого пространства; | | снять блокировку с области данных; | | } | +------------------------------------------------------------+ Рисунок 7.26. Алгоритм выполнения функции brk На Рисунке 7.27 приведен пример программы, использующей функцию brk, и выходные данные, полученные в результате ее прогона на машине AT&T 3B20. Вызвав функцию signal и распорядившись принимать сигналы о нарушении сегмен- тации (segmentation violation), процесс обращается к подпрограмме sbrk и вы- водит на печать первоначальное значение адреса верхней границы области дан- ных. Затем в цикле, используя счетчик символов, процесс заполняет область данных до тех пор, пока не обратится к адресу, расположенному за пределами области, тем самым давая повод для сигнала о нарушении сегментации. Получив сигнал, функция обработки сигнала вызывает подпрограмму sbrk для того, чтобы присоединить к области дополнительно 256 байт памяти; процесс продолжается с точки прерывания, заполняя информацией вновь выделенное пространство памяти и т.д. На машинах со страничной организацией памяти, таких как 3B20, наблю- дается интересный феномен. Страница является наименьшей единицей памяти, с которой работают механизмы аппаратной защиты, поэтому аппаратные средства не в состоянии установить ошибку в граничной ситуации, когда процесс пытается записать информацию по адресам, превышающим верхнюю границу области данных, но принадлежащим т.н. "полулегальной" странице (странице, не полностью заня- той областью данных процесса). Это видно из результатов выполнения програм- мы, выведенных на печать (Рисунок 7.27): первый раз подпрограмма sbrk возв- ращает значение 140924, то есть адрес, не дотягивающий 388 байт до конца страницы, которая на машине 3B20 имеет размер 2 Кбайта. Однако процесс полу- чит ошибку только в том случае, если обратится к следующей странице памяти, то есть к любому адресу, начиная с 141312. Функция обработки сигнала прибав- ляет к адресу верхней границы области 256, делая его равным 141180 и, таким образом, оставляя его в пределах текущей страницы. Следовательно, процесс тут же снова получит ошибку, выдав на печать адрес 141312. Исполнив подпрог- рамму sbrk еще раз, ядро выделяет под данные процесса новую страницу памяти, так что процесс получает возможность адресовать дополнительно 2 Кбайта памя- ти, до адреса 143360, даже если верхняя граница области располагается ниже. Получив ошибку, процесс должен будет восемь раз обратиться к подпрограмме sbrk, прежде чем сможет продолжить выполнение основной программы. Таким об- разом, процесс может иногда выходить за официальную верхнюю границу области данных, хотя это и нежелательный момент в практике программирования. 216 Когда стек задачи переполняется, ядро автоматически увеличивает его раз- мер, выполняя алгоритм, похожий на алгоритм функции brk. Первоначально стек задачи имеет размер, достаточный для хранения параметров функции exec, одна- ко при выполнении процесса +-------------------------------------------------------+ | #include | | char *cp; | | int callno; | | | | main() | | { | | char *sbrk(); | | extern catcher(); | | | | signal(SIGSEGV,catcher); | | cp = sbrk(0); | | printf("original brk value %u\n",cp); | | for (;;) | | *cp++ = 1; | | } | | | | catcher(signo); | | int signo; | | { | | callno++; | | printf("caught sig %d %dth call at addr %u\n", | | signo,callno,cp); | | sbrk(256); | | signal(SIGSEGV,catcher); | | } | +-------------------------------------------------------+ +-------------------------------------------+ | original brk value 140924 | | caught sig 11 1th call at addr 141312 | | caught sig 11 2th call at addr 141312 | | caught sig 11 3th call at addr 143360 | | ...(тот же адрес печатается до 10-го | | вызова подпрограммы sbrk) | | caught sig 11 10th call at addr 143360 | | caught sig 11 11th call at addr 145408 | | ...(тот же адрес печатается до 18-го | | вызова подпрограммы sbrk) | | caught sig 11 18th call at addr 145408 | | caught sig 11 19th call at addr 145408 | | - | | - | +-------------------------------------------+ Рисунок 7.27. Пример программы, использующей функцию brk, и результаты ее контрольного прогона этот стек может переполниться. Переполнение стека приводит к ошибке адреса- ции, свидетельствующей о попытке процесса обратиться к ячейке памяти за пре- делами отведенного адресного пространства. Ядро устанавливает причину воз- никновения ошибки, сравнивая текущее значение указателя вершины стека с раз- мером области стека. При расширении области стека ядро использует точно та- кой же механизм, что и для области данных. На выходе из прерывания процесс 217 +------------------------------------------------------------+ | /* чтение командной строки до символа конца файла */ | | while (read(stdin,buffer,numchars)) | | { | | /* синтаксический разбор командной строки */ | | if (/* командная строка содержит & */) | | amper = 1; | | else | | amper = 0; | | /* для команд, не являющихся конструкциями командного | | языка shell */ | | if (fork() == 0) | | { | | /* переадресация ввода-вывода ? */ | | if (/* переадресация вывода */) | | { | | fd = creat(newfile,fmask); | | close(stdout); | | dup(fd); | | close(fd); | | /* stdout теперь переадресован */ | | } | | if (/* используются каналы */) | | { | | pipe(fildes); | | | +------------------------------------------------------------+ Рисунок 7.28. Основной цикл программы shell имеет область стека необходимого для продолжения работы размера. 7.8 КОМАНДНЫЙ ПРОЦЕССОР SHELL Теперь у нас есть достаточно материала, чтобы перейти к объяснению прин- ципов работы командного процессора shell. Сам командный процессор намного сложнее, чем то, что мы о нем здесь будем излагать, однако взаимодействие процессов мы уже можем рассмотреть на примере реальной программы. На Рисунке 7.28 приведен фрагмент основного цикла программы shell, демонстрирующий асинхронное выполнение процессов, переназначение вывода и использование ка- налов. Shell считывает командную строку из файла стандартного ввода и интерпре- тирует ее в соответствии с установленным набором правил. Дескрипторы файлов стандартного ввода и стандартного вывода, используемые регистрационным shell'ом, как правило, указывают на терминал, с которого пользователь регис- трируется в системе (см. главу 10). Если shell узнает во введенной строке конструкцию собственного командного языка (например, одну из команд cd, for, while и т.п.), он исполняет команду своими силами, не прибегая к созданию новых процессов; в противном случае команда интерпретируется как имя испол- няемого файла. Командные строки простейшего вида содержат имя программы и несколько па- раметров, например: who grep -n include *.c ls -l 218 +------------------------------------------------------------+ | if (fork() == 0) | | { | | /* первая компонента командной строки */| | close(stdout); | | dup(fildes[1]); | | close(fildes[1]); | | 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); | | } | +------------------------------------------------------------+ Рисунок 7.28. Основной цикл программы shell (продолжение) Shell "ветвится" (fork) и порождает новый процесс, который и запускает прог- рамму, указанную пользователем в командной строке. Родительский процесс (shell) дожидается завершения потомка и повторяет цикл считывания следующей команды. Если процесс запускается асинхронно (на фоне основной программы), как в следующем примере nroff -mm bigdocument & shell анализирует наличие символа амперсанд (&) и заносит результат проверки во внутреннюю переменную amper. В конце основного цикла shell обращается к этой переменной и, если обнаруживает в ней признак наличия символа, не вы- полняет функцию wait, а тут же повторяет цикл считывания следующей команды. Из рисунка видно, что процесс-потомок по завершении функции fork получа- ет доступ к командной строке, принятой shell'ом. Для того, чтобы переадресо- вать стандартный вывод в файл, как в следующем примере nroff -mm bigdocument > output процесс-потомок создает файл вывода с указанным в командной строке именем; 219 если файл не удается создать (например, не разрешен доступ к каталогу), про- цесс-потомок тут же завершается. В противном случае процесс-потомок закрыва- ет старый файл стандартного вывода и переназначает с помощью функции dup дескриптор этого файла новому файлу. Старый дескриптор созданного файла зак- рывается и сохраняется для запускаемой программы. Подобным же образом shell переназначает и стандартный ввод и стандартный вывод ошибок. +-----------+ | Shell | +-----+-----+ wait | ^ | | +-----+-----+ exit | wc | +-----+-----+ read | ^ | | +-----+-----+ write | ls - l | +-----------+ Рисунок 7.29. Взаимосвязь между процессами, исполняющими ко- мандную строку ls -l|wc Из приведенного текста программы видно, как shell обрабатывает командную строку, используя один канал. Допустим, что командная строка имеет вид: ls -l|wc После создания родительским процессом нового процесса процесс-потомок созда- ет канал. Затем процесс-потомок создает свое ответвление; он и его потомок обрабатывают по одной компоненте командной строки. "Внучатый" процесс испол- няет первую компоненту строки (ls): он собирается вести запись в канал, поэ- тому он закрывает старый файл стандартного вывода, передает его дескриптор каналу и закрывает старый дескриптор записи в канал, в котором (в дескрипто- ре) уже нет необходимости. Родитель (wc) "внучатого" процесса (ls) является потомком основного процесса, реализующего программу shell'а (см. Рисунок 7.29). Этот процесс (wc) закрывает свой файл стандартного ввода и передает его дескриптор каналу, в результате чего канал становится файлом стандартно- го ввода. Затем закрывается старый и уже не нужный дескриптор чтения из ка- нала и исполняется вторая компонента командной строки. Оба порожденных про- цесса выполняются асинхронно, причем выход одного процесса поступает на вход другого. Тем временем основной процесс дожидается завершения своего потомка (wc), после чего продолжает свою обычную работу: по завершении процесса, вы- полняющего команду wc, вся командная строка является обработанной. Shell возвращается в цикл и считывает следующую командную строку. 7.9 ЗАГРУЗКА СИСТЕМЫ И НАЧАЛЬНЫЙ ПРОЦЕСС Для того, чтобы перевести систему из неактивное состояние в активное, администратор выполняет процедуру "начальной загрузки". На разных машинах эта процедура имеет свои особенности, однако во всех случаях она реализует одну и ту же цель: загрузить копию операционной системы в основную память машины и запустить ее на исполнение. Обычно процедура начальной загрузки включает в себя несколько этапов. Переключением клавиш на пульте машины ад- министратор может указать адрес специальной программы аппаратной загрузки, а может, нажав только одну клавишу, дать команду машине запустить процедуру загрузки, исполненную в виде микропрограммы. Эта программа может состоять из нескольких команд, подготавливающих запуск другой программы. В системе UNIX 220 процедура начальной загрузки заканчивается считыванием с диска в память бло- ка начальной загрузки (нулевого блока). Программа, содержащаяся в этом бло- ке, загружает из файловой системы ядро ОС (например, из файла с именем "/unix" или с другим именем, указанным администратором). После загрузки ядра системы в память управление передается по стартовому адресу ядра и ядро за- пускается на выполнение (алгоритм start, Рисунок 7.30). Ядро инициализирует свои внутренние структуры данных. Среди прочих структур ядро создает связные списки свободных буферов и индексов, хеш-оче- реди для буферов и индексов, инициализирует структуры областей, точки входа в таблицы страниц и т.д. По окончании этой фазы ядро монтирует корневую фай- ловую систему и формирует среду выполнения нулевого процесса, среди всего прочего создавая пространство процесса, инициализируя нулевую точку входа в таблице процесса и делая корневой каталог текущим для процесса. Когда формирование среды выполнения процесса заканчивается, система ис- полняется уже в виде нулевого процесса. Нулевой процесс "ветвится", запуская алгоритм fork прямо из ядра, поскольку сам процесс исполняется в режиме яд- +------------------------------------------------------------+ | алгоритм start /* процедура начальной загрузки системы */| | входная информация: отсутствует | | выходная информация: отсутствует | | { | | проинициализировать все структуры данных ядра; | | псевдо-монтирование корня; | | сформировать среду выполнения процесса 0; | | создать процесс 1; | | { | | /* процесс 1 */ | | выделить область; | | подключить область к адресному пространству процесса| | init; | | увеличить размер области для копирования в нее ис- | | полняемого кода; | | скопировать из пространства ядра в адресное прост- | | ранство процесса код программы, исполняемой процес-| | сом; | | изменить режим выполнения: вернуться из режима ядра | | в режим задачи; | | /* процесс init далее выполняется самостоятельно -- | | * в результате выхода в режим задачи, | | * init исполняет файл "/etc/init" и становится | | * "обычным" пользовательским процессом, производя- | | * щим обращения к системным функциям | | */ | | } | | /* продолжение нулевого процесса */ | | породить процессы ядра; | | /* нулевой процесс запускает программу подкачки, управ- | | * ляющую распределением адресного пространства процес- | | * сов между основной памятью и устройствами выгрузки. | | * Это бесконечный цикл; нулевой процесс обычно приоста-| | * навливает свою работу, если необходимости в нем боль-| | * ше нет. | | */ | | исполнить программу, реализующую алгоритм подкачки; | | } | +------------------------------------------------------------+ Рисунок 7.30. Алгор