The design of the unix operating system by Maurice J

Вид материалаРеферат
G7.6 код идентификации пользователя процессаh
7.7 Изменение размера процесса
7.8 Командный процессор shell
Подобный материал:
1   ...   25   26   27   28   29   30   31   32   ...   55

который инициирует запуск shell'а (с помощью функции exec) и ис-

полняет команды файла "script". Если процесс запускает самого се-

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

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

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

Иначе говоря, ядро не может, не снимая блокировки со "старой" об-

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

на самом деле это одна и та же область. Вместо этого ядро просто

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

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

Обычно процессы вызывают функцию exec после функции fork; та-

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

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

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

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

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

программу и исполняла ее под видом нового процесса ? Ричи выска-

зал предположение, что возникновение fork и exec как отдельных

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

функция fork была добавлена к уже существующему образу ядра сис-

темы (см. [Ritchie 84a], стр.1584). Однако, разделение fork и

exec важно и с функциональной точки зрения, поскольку в этом слу-

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

ввода-вывода независимо, повышая тем самым "элегантность" исполь-

зования каналов. Пример, показывающий использование этой возмож-

ности, приводится в разделе 7.8.


G7.6 КОД ИДЕНТИФИКАЦИИ ПОЛЬЗОВАТЕЛЯ ПРОЦЕССАH


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

ля, не зависящих от кода идентификации процесса: реальный (дейс-

твительный) код идентификации пользователя и исполнительный код

или 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" выводит-

ся следующая информация:

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

│ } │

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


Рисунок 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

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

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

ранее выделенного адресного пространства; когда процесс попытает-

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

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


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

│ алгоритм brk │

│ входная информация: новый адрес верхней границы области │

│ данных │

│ выходная информация: старый адрес верхней границы области │

│ данных │

│ { │

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

│ если (размер области увеличивается) │

│ если (новый размер области имеет недопустимое зна-│

│ чение) │

│ { │

│ снять блокировку с области; │

│ вернуть (ошибку); │

│ } │

│ изменить размер области (алгоритм growreg); │

│ обнулить содержимое присоединяемого пространства; │

│ снять блокировку с области данных; │

│ } │

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


Рисунок 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, прежде чем сможет продолжить выполнение ос-

новной программы. Таким образом, процесс может иногда выходить за

официальную верхнюю границу области данных, хотя это и нежела-

тельный момент в практике программирования.

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

вает его размер, выполняя алгоритм, похожий на алгоритм функции

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

│ } │

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

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

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

│ │

│ │

│ │

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


Рисунок 7.27. Пример программы, использующей функцию brk, и

результаты ее контрольного прогона


этот стек может переполниться. Переполнение стека приводит к

ошибке адресации, свидетельствующей о попытке процесса обратиться

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

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

щее значение указателя вершины стека с размером области стека.

При расширении области стека ядро использует точно такой же меха-

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


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

│ /* чтение командной строки до символа конца файла */ │

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

│ │

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


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


имеет область стека необходимого для продолжения работы размера.


7.8 КОМАНДНЫЙ ПРОЦЕССОР SHELL


Теперь у нас есть достаточно материала, чтобы перейти к объ-

яснению принципов работы командного процессора shell. Сам команд-

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

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

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

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

ронное выполнение процессов, переназначение вывода и использова-

ние каналов.

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

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

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

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

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

главу 10). Если shell узнает во введенной строке конструкцию

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

while и т.п.), он исполняет команду своими силами, не прибегая к

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

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


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

│ if (fork() == 0) │

│ { │

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

│ close(stdout); │

│ dup(fildes[1]); │

│ close(fildes[1]); │