Страница:
было увеличено в самом начале выполнения функции exec (алгоритм namei). Пос-
кольку ядро уменьшает значение счетчика только один раз в завершение выпол-
нения функции exec (по алгоритму iput), значение счетчика ссылок на индекс
файла, ассоциированного с разделяемой областью команд и исполняемого в нас-
тоящий момент, равно по меньшей мере 1. Поэтому когда процесс разрывает
связь с файлом (функция unlink), содержимое файла остается нетронутым (не
претерпевает изменений). После загрузки в память сам файл ядру становится
209
ненужен, ядро интересует только указатель на копию индекса файла в памяти,
содержащийся в таблице областей; этот указатель и будет идентифицировать
+------------------------------------------------------------+
| алгоритм xalloc /* выделение и инициализация области |
| команд */ |
| входная информация: индекс исполняемого файла |
| выходная информация: отсутствует |
| { |
| если (исполняемый файл не имеет отдельной области команд)|
| вернуть управление; |
| если (уже имеется область команд, ассоциированная с ин- |
| дексом исполняемого файла) |
| { |
| /* область команд уже существует ... подключиться к |
| ней */ |
| заблокировать область; |
| выполнить пока (содержимое области еще не доступно) |
| { |
| /* операции над счетчиком ссылок, предохраняющие |
| от глобального удаления области |
| */ |
| увеличить значение счетчика ссылок на область; |
| снять с области блокировку; |
| приостановиться (пока содержимое области не станет|
| доступным); |
| заблокировать область; |
| уменьшить значение счетчика ссылок на область; |
| } |
| присоединить область к процессу (алгоритм attachreg);|
| снять с области блокировку; |
| вернуть управление; |
| } |
| /* интересующая нас область команд не существует -- соз- |
| дать новую */ |
| выделить область команд (алгоритм allocreg); /* область |
| заблоки- |
| рована */|
| если (область помечена как "неотъемлемая") |
| отключить соответствующий флаг; |
| подключить область к виртуальному адресу, указанному в |
| заголовке файла (алгоритм attachreg); |
| если (файл имеет специальный формат для системы с замеще-|
| нием страниц) |
| /* этот случай будет рассмотрен в главе 9 */ |
| в противном случае /* файл не имеет специального фор-|
| мата */ |
| считать команды из файла в область (алгоритм |
| loadreg); |
| изменить режим защиты области в записи частной таблицы |
| областей процесса на "read-only"; |
| снять с области блокировку; |
| } |
+------------------------------------------------------------+
Рисунок 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.
Ядро связывает с процессом два кода идентификации пользователя, не зави-
сящих от кода идентификации процесса: реальный (действительный) код иденти-
фикации пользователя и исполнительный код или 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,
и предоставляет права собственности и доступа к каталогу истинному пользова-
телю процесса.
С помощью системной функции 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); |
кольку ядро уменьшает значение счетчика только один раз в завершение выпол-
нения функции exec (по алгоритму iput), значение счетчика ссылок на индекс
файла, ассоциированного с разделяемой областью команд и исполняемого в нас-
тоящий момент, равно по меньшей мере 1. Поэтому когда процесс разрывает
связь с файлом (функция unlink), содержимое файла остается нетронутым (не
претерпевает изменений). После загрузки в память сам файл ядру становится
209
ненужен, ядро интересует только указатель на копию индекса файла в памяти,
содержащийся в таблице областей; этот указатель и будет идентифицировать
+------------------------------------------------------------+
| алгоритм xalloc /* выделение и инициализация области |
| команд */ |
| входная информация: индекс исполняемого файла |
| выходная информация: отсутствует |
| { |
| если (исполняемый файл не имеет отдельной области команд)|
| вернуть управление; |
| если (уже имеется область команд, ассоциированная с ин- |
| дексом исполняемого файла) |
| { |
| /* область команд уже существует ... подключиться к |
| ней */ |
| заблокировать область; |
| выполнить пока (содержимое области еще не доступно) |
| { |
| /* операции над счетчиком ссылок, предохраняющие |
| от глобального удаления области |
| */ |
| увеличить значение счетчика ссылок на область; |
| снять с области блокировку; |
| приостановиться (пока содержимое области не станет|
| доступным); |
| заблокировать область; |
| уменьшить значение счетчика ссылок на область; |
| } |
| присоединить область к процессу (алгоритм attachreg);|
| снять с области блокировку; |
| вернуть управление; |
| } |
| /* интересующая нас область команд не существует -- соз- |
| дать новую */ |
| выделить область команд (алгоритм allocreg); /* область |
| заблоки- |
| рована */|
| если (область помечена как "неотъемлемая") |
| отключить соответствующий флаг; |
| подключить область к виртуальному адресу, указанному в |
| заголовке файла (алгоритм attachreg); |
| если (файл имеет специальный формат для системы с замеще-|
| нием страниц) |
| /* этот случай будет рассмотрен в главе 9 */ |
| в противном случае /* файл не имеет специального фор-|
| мата */ |
| считать команды из файла в область (алгоритм |
| loadreg); |
| изменить режим защиты области в записи частной таблицы |
| областей процесса на "read-only"; |
| снять с области блокировку; |
| } |
+------------------------------------------------------------+
Рисунок 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.
Ядро связывает с процессом два кода идентификации пользователя, не зави-
сящих от кода идентификации процесса: реальный (действительный) код иденти-
фикации пользователя и исполнительный код или 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,
и предоставляет права собственности и доступа к каталогу истинному пользова-
телю процесса.
С помощью системной функции 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); |