| Размер раздела | | Виртуальный адрес | +---------------------------+ Раздел 1 | Данные (например, текст) | +---------------------------+ Раздел 2 | Данные | - +---------------------------+ - | - | - | - | - +---------------------------+ Раздел n | Данные | +---------------------------+ | Другая информация | +---------------------------+ Рисунок 7.20. Образ исполняемого файла 205 Указанные составляющие с развитием самой системы видоизменяются, однако во всех исполняемых файлах обязательно присутствует главный заголовок с по- лем типа файла. Тип файла обозначается коротким целым числом (представляется в машине полусловом), которое идентифицирует файл как загрузочный модуль, давая тем самым ядру возможность отслеживать динамические характеристики его выполне- ния. Например, в машине PDP 11/70 определение типа файла как загрузочного модуля свидетельствует о том, что процесс, исполняющий файл, может использо- вать до 128 Кбайт памяти вместо 64 Кбайт (**), тем не менее в системах с за- мещением страниц тип файла все еще играет существенную роль, в чем нам пред- стоит убедиться во время знакомства с главой 9. Вернемся к алгоритму. Мы остановились на том, что ядро обратилось к ин- дексу файла и установило, что файл является исполнимым. Ядру следовало бы освободить память, занимаемую пользовательским контекстом процесса. Однако, поскольку в памяти, подлежащей освобождению, располагаются передаваемые но- вой программе параметры, ядро первым делом копирует их из адресного прост- ранства в промежуточный буфер на время, пока не будут отведены области для нового пространства памяти. Поскольку параметрами функции exec выступают пользовательские адреса массивов символьных строк, ядро по каждой строке сначала копирует в систем- ную память адрес строки, а затем саму строку. Для хранения строки в разных версиях системы могут быть выбраны различные места. Чаще принято хранить строки в стеке ядра (локальная структура данных, принадлежащая программе яд- ра), на нераспределяемых участках памяти (таких как страницы), которые можно занимать только временно, а также во внешней памяти (на устройстве выгруз- ки). С точки зрения реализации проще всего для копирования параметров в новый пользовательский контекст обратиться к стеку ядра. Однако, поскольку размер стека ядра, как правило, ограничивается системой, а также поскольку парамет- ры функции exec могут иметь произвольную длину, этот подход следует сочетать с другими подходами. При рассмотрении других вариантов обычно останавливают- ся на способе хранения, обеспечивающем наиболее быстрый доступ к строкам. Если доступ к страницам памяти в системе реализуется довольно просто, строки следует размещать на страницах, поскольку обращение к оперативной памяти осуществляется быстрее, чем к внешней (устройству выгрузки). После копирования параметров функции exec в системную память ядро отсое- диняет области, ранее присоединенные к процессу, используя алгоритм detachreg. Несколько позже мы еще поговорим о специальных действиях, выпол- няемых в отношении областей команд. К рассматриваемому моменту процесс уже лишен пользовательского контекста и поэтому возникновение в дальнейшем любой ошибки неизбежно будет приводить к завершению процесса по сигналу. Такими ошибками могут быть обращение к пространству, не описанному в таблице облас- тей ядра, попытка загрузить программу, имеющую недопустимо большой размер или использующую области с пересекающимися адресами, и др. Ядро выделяет и присоединяет к процессу области команд и данных, загружает в оперативную па- мять содержимое исполняемого файла (алгоритмы allocreg, attachreg и loadreg, соответственно). Область данных процесса изначально поделена на две части: --------------------------------------- (**) В PDP 11 "магические числа" имеют значения, соответствующие командам перехода; при выполнении этих команд в ранних версиях системы управле- ние передавалось в разные места программы в зависимости от размера за- головка и от типа исполняемого файла. Эта особенность больше не исполь- зуется с тех пор, как система стала разрабатываться на языке Си. 206 данные, инициализация которых была выполнена во время компиляции, и данные, не определенные компилятором ("bss"). Область памяти первоначально выделяет- ся для проинициализированных данных. Затем ядро увеличивает размер области данных для размещения данных типа "bss" (алгоритм growreg) и обнуляет их значения. Напоследок ядро выделяет и присоединяет к процессу область стека и отводит пространство памяти для хранения параметров функции exec. Если пара- метры функции размещаются на страницах, те же страницы могут быть использо- ваны под стек. В противном случае параметры функции размещаются в стеке за- дачи. В пространстве процесса ядро стирает адреса пользовательских функций об- работки сигналов, поскольку в новом пользовательском контексте они теряют свое значение. Однако и в новом контексте рекомендации по игнорированию тех или иных сигналов остаются в силе. Ядро устанавливает в регистрах для режима задачи значения из сохраненного регистрового контекста, в частности первона- чальное значение указателя вершины стека (sp) и счетчика команд (pc): перво- начальное значение счетчика команд было занесено загрузчиком в заголовок файла. Для setuid-программ и для трассировки процесса ядро предпринимает особые действия, на которых мы еще остановимся во время рассмотрения глав 8 и 11, соответственно. Наконец, ядро запускает алгоритм iput, освобождая ин- декс, выделенный по алгоритму namei в самом начале выполнения функции exec. Алгоритмы namei и iput в функции exec выполняют роль, подобную той, которую они выполняют при открытии и закрытии файла; состояние файла во время выпол- нения функции exec похоже на состояние открытого файла, если не принимать во внимание отсутствие записи о файле в таблице файлов. По выходе из функции процесс исполняет текст новой программы. Тем не менее, процесс остается тем же, что и до выполнения функции; его идентификатор не изменился, как не из- менилось и его место в иерархии процессов. Изменению подвергся только поль- зовательский контекст процесса. +-------------------------------------------------------+ | main() | | { | | int status; | | if (fork() == 0) | | execl("/bin/date","date",0); | | wait(&status); | | } | +-------------------------------------------------------+ Рисунок 7.21. Пример использования функции exec В качестве примера можно привести программу (Рисунок 7.21), в которой создается процесс-потомок, запускающий функцию exec. Сразу по завершении функции fork процесс-родитель и процесс-потомок начинают исполнять независи- мо друг от друга копии одной и той же программы. К моменту вызова процес- сом-потомком функции exec в его области команд находятся инструкции этой программы, в области данных располагаются строки "/bin/date" и "date", а в стеке - записи, которые будут извлечены по выходе из exec. Ядро ищет файл "/bin/date" в файловой системе, обнаружив его, узнает, что его может испол- нить любой пользователь, а также то, что он представляет собой загрузочный модуль, готовый для исполнения. По условию первым параметром функции exec, включаемым в список параметров argv, является имя исполняемого файла (пос- ледняя компонента имени пути поиска файла). Таким образом, процесс имеет доступ к имени программы на пользовательском уровне, что иногда может ока- заться полезным (***). Затем ядро копирует строки "/bin/date" и "date" во внутреннюю структуру хранения и освобождает области команд, данных и стека, занимаемые процессом. Процессу выделяются новые области команд, данных и стека, в область команд переписывается командная секция файла "/bin/date", в 207 --------------------------------------- (***) Например, в версии V стандартные программы переименования файла (mv), копирования файла (cp) и компоновки файла (ln), поскольку исполняют похожие действия, вызывают один и тот же исполняемый файл. По имени вызываемой программы процесс узнает, какие действия в настоящий момент требуются пользователю. область данных - секция данных файла. Ядро восстанавливает первоначальный список параметров (в данном случае это строка символов "date") и помещает его в область стека. Вызвав функцию exec, процесс-потомок прекращает выпол- нение старой программы и переходит к выполнению программы "date"; когда программа "date" завершится, процесс-родитель, ожидающий этого момента, получит код завершения функции exit. Вплоть до настоящего момента мы предполагали, что команды и данные раз- мещаются в разных секциях исполняемой программы и, следовательно, в разных областях текущего процесса. Такое размещение имеет два основных преимущест- ва: простота организации защиты от несанкционированного доступа и возмож- ность разделения областей различными процессами. Если бы команды и данные находились в одной области, система не смогла бы предотвратить затирание ко- манд, поскольку ей не были бы известны адреса, по которым они располагаются. Если же команды и данные находятся в разных областях, система имеет возмож- ность пользоваться механизмами аппаратной защиты области команд процесса. Когда процесс случайно попытается что-то записать в область, занятую коман- дами, он получит отказ, порожденный системой защиты и приводящий обычно к аварийному завершению процесса. +------------------------------------------------------------+ | #include | | main() | | { | | int i,*ip; | | extern f(),sigcatch(); | | | | ip = (int *)f; /* присвоение переменной ip значения ад-| | реса функции f */ | | for (i = 0; i < 20; i++) | | signal(i,sigcatch); | | *ip = 1; /* попытка затереть адрес функции f */ | | printf("после присвоения значения ip\n"); | | f(); | | } | | | | f() | | { | | } | | | | sigcatch(n) | | int n; | | { | | printf("принят сигнал %d\n",n); | | exit(1); | | } | +------------------------------------------------------------+ Рисунок 7.22. Пример программы, ведущей запись в область команд В качестве примера можно привести программу (Рисунок 7.22), которая присваивает переменной ip значение адреса функции f и затем делает распоря- 208 жение принимать все сигналы. Если программа скомпилирована так, что команды и данные располагаются в разных областях, процесс, исполняющий программу, при попытке записать что-то по адресу в ip встретит порожденный системой за- щиты отказ, поскольку область команд защищена от записи. При работе на компьютере AT&T 3B20 ядро посылает процессу сигнал SIGBUS, в других системах возможна посылка других сигналов. Процесс принимает сигнал и завершается, не дойдя до выполнения команды вывода на печать в процедуре main. Однако, если программа скомпилирована так, что команды и данные располагаются в одной об- ласти (в области данных), ядро не поймет, что процесс пытается затереть ад- рес функции f. Адрес f станет равным 1. Процесс исполнит команду вывода на печать в процедуре main, но когда запустит функцию f, произойдет ошибка, связанная с попыткой выполнения запрещенной команды. Ядро пошлет процессу сигнал SIGILL и процесс завершится. Расположение команд и данных в разных областях облегчает поиск и предот- вращение ошибок адресации. Тем не менее, в ранних версиях системы UNIX ко- манды и данные разрешалось располагать в одной области, поскольку на машинах PDP размер процесса был сильно ограничен: программы имели меньший размер и существенно меньшую сегментацию, если команды и данные занимали одну и ту же область. В последних версиях системы таких строгих ограничений на размер процесса нет и в дальнейшем возможность загрузки команд и данных в одну об- ласть компиляторами не будет поддерживаться. Второе преимущество раздельного хранения команд и данных состоит в воз- можности совместного использования областей процессами. Если процесс не мо- жет вести запись в область команд, команды процесса не претерпевают никаких изменений с того момента, как ядро загрузило их в область команд из команд- ной секции исполняемого файла. Если один и тот же файл исполняется несколь- кими процессами, в целях экономии памяти они могут иметь одну область команд на всех. Таким образом, когда ядро при выполнении функции exec отводит об- ласть под команды процесса, оно проверяет, имеется ли возможность совместно- го использования процессами команд исполняемого файла, что определяется "ма- гическим числом" в заголовке файла. Если да, то с помощью алгоритма xalloc ядро ищет существующую область с командами файла или назначает новую в слу- чае ее отсутствия (см. Рисунок 7.23). Исполняя алгоритм xalloc, ядро просматривает список активных областей в поисках области с командами файла, индекс которого совпадает с индексом ис- полняемого файла. В случае ее отсутствия ядро выделяет новую область (алго- ритм allocreg), присоединяет ее к процессу (алгоритм attachreg), загружает ее в память (алгоритм loadreg) и защищает от записи (read-only). Последний шаг предполагает, что при попытке процесса записать что-либо в область ко- манд будет получен отказ, вызванный системой защиты памяти. В случае обнару- жения области с командами файла в списке активных областей осуществляется проверка ее наличия в памяти (она может быть либо загружена в память, либо выгружена из памяти) и присоединение ее к процессу. В завершение выполнения алгоритма xalloc ядро снимает с области блокировку, а позднее, следуя алго- ритму detachreg при выполнении функций exit или exec, уменьшает значение счетчика областей. В традиционных реализациях системы поддерживается таблица команд, к которой ядро обращается в случаях, подобных описанному. Таким об- разом, совокупность областей команд можно рассматривать как новую версию этой таблицы. Напомним, что если область при выполнении алгоритма allocreg (Раздел 6.5.2) выделяется впервые, ядро увеличивает значение счетчика ссылок на ин- декс, ассоциированный с областью, при этом значение счетчика ссылок нами уже было увеличено в самом начале выполнения функции 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. 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. Алгоритм загрузки системы 221 ра. Порожденный нулевым новый процесс, процесс 1, запускается в том же режи- ме и создает свой пользовательский контекст, формируя область данных и при- соединяя ее к своему адресному пространству. Он увеличивает размер области до надлежащей величины и переписывает программу загрузки из адресного прост- ранства ядра в новую область: эта программа теперь будет определять контекст процесса 1. Затем процесс 1 сохраняет регистровый контекст задачи, "возвра- щается" из режима ядра в режим задачи и исполняет только что переписанную программу. В отличие от нулевого процесса, который является процессом сис- темного уровня, выполняющимся в режиме ядра, процесс 1 относится к пользова- тельскому уровню. Код, исполняемый процессом 1, включает в себя вызов сис- темной функции exec, запускающей на выполнение программу из файла "/etc/init". Обычно процесс 1 именуется процессом init, поскольку он отвеча- ет за инициализацию новых процессов. Казалось бы, зачем ядру копировать программу, запускаемую с помощью фун- кции exec, в адресное пространство процесса 1 ? Он мог бы обратиться к внут- реннему варианту функции прямо из ядра, одна- ко, по сравнению с уже описанным алгоритмом это было бы гораздо труднее реа- лизовать, ибо в этом случае функции exec пришлось бы производить анализ имен файлов в пространстве ядра, а не в пространстве задачи. Подобная деталь, требующаяся только для процесса init, усложнила бы программу реализации фун- кции exec и отрицательно отразилась бы на скорости выполнения функции в бо- лее общих случаях. Процесс init (Рисунок 7.31) выступает диспетчером процессов, который по- рождает процессы, среди всего прочего позволяющие пользователю регистриро- ваться в системе. Инструкции о том, какие процессы нужно создать, считывают- ся процессом init из файла "/etc/inittab". Строки файла включают в себя идентификатор состояния "id" (однопользовательский режим, многопользователь- ский и т. д.), предпринимаемое действие (см. упражнение 7.43) и спецификацию программы, реализующей это действие (см. Рисунок 7.32). Процесс init прос- матривает строки файла до тех пор, пока не обнаружит идентификатор состоя- ния, соответствующего тому состоянию, в котором находится процесс, и создает процесс, исполняющий программу с указанной спецификацией. Например, при за- пуске в многопользовательском режиме (состояние 2) процесс init обычно по- рождает getty-процессы, управляющие функционированием терминальных линий, входящих в состав системы. Если регистрация пользователя прошла успешно, getty-процесс, пройдя через процедуру login, запускает на исполнение регист- рационный shell (см. главу 10). Тем временем процесс init находится в состо- янии ожидания (wait), наблюдая за прекращением существования своих потомков, а также "внучатых" процессов, оставшихся "сиротами" после гибели своих роди- телей. Процессы в системе UNIX могут быть либо пользовательскими, либо управля- ющими, либо системными. Большинство из них составляют пользовательские про- цессы, связанные с пользователями через терминалы. Управляющие процессы не связаны с конкретными пользователями, они выполняют широкий спектр системных функций, таких как администрирование и управление сетями, различные периоди- ческие операции, буферизация данных для вывода на устройство построчной пе- чати и т.д. Процесс init может порождать управляющие процессы, которые будут существовать на протяжении всего времени жизни системы, в различных случаях они могут быть созданы самими пользователями. Они похожи на пользовательские процессы тем, что они исполняются в режиме задачи и прибегают к услугам сис- темы путем вызова соответствующих системных функций. Системные процессы выполняются исключительно в режиме ядра. Они могут порождаться нулевым процессом (например, процесс замещения страниц vhand), который затем становится процессом подкачки. Системные процессы похожи на управляющие процессы тем, что они выполняют системные функции, при этом они обладают большими возможностями приоритетного выполнения, поскольку лежащие в их основе программные коды являются составной частью ядра. Они могут обра- щаться к структурам данных и алгоритмам ядра, не прибегая к вызову системных функций, отсюда вытекает их исключительность. Однако, они не обладают такой 222 +------------------------------------------------------------+ | алгоритм init /* процесс init, в системе именуемый | | "процесс 1" */ | | входная информация: отсутствует | | выходная информация: отсутствует | | { | | fd = open("/etc/inittab",O_RDONLY); | | while (line_read(fd,buffer)) | | { | | /* читать каждую строку файлу */ | | if (invoked state != buffer state) | | continue; /* остаться в цикле while */ | | /* найден идентификатор соответствующего состояния | | */ | | if (fork() == 0) | | { | | execl("процесс указан в буфере"); | | exit(); | | } | | /* процесс init не дожидается завершения потомка */ | | /* возврат в цикл while */ | | } | | | | while ((id = wait((int*) 0)) != -1) | | { | | /* проверка существования потомка; | | * если потомок прекратил существование, рассматри- | | * вается возможность его перезапуска */ | | /* в противном случае, основной процесс просто про- | | * должает работу */ | | } | | } | +------------------------------------------------------------+ Рисунок 7.31. Алгоритм выполнения процесса init +------------------------------------------------------------+ | Формат: идентификатор, состояние, действие, спецификация | | процесса | | Поля разделены между собой двоеточиями | | Комментарии в конце строки начинаются с символа '#' | | | | co::respawn:/etc/getty console console #Консоль в машзале| | 46:2:respawn:/etc/getty -t 60 tty46 4800H #комментарии | +------------------------------------------------------------+ Рисунок 7.32. Фрагмент файла inittab же гибкостью, как управляющие процессы, поскольку для того, чтобы внести из- менения в их программы, придется еще раз перекомпилировать ядро. 7.10 ВЫВОДЫ В данной главе были рассмотрены системные функции, предназначенные для работы с контекстом процесса и для управления выполнением процесса. Систем- ная функция fork создает новый процесс, копируя для него содержимое всех об- ластей, подключенных к родительскому процессу. Особенность реализации функ- ции fork состоит в том, что она выполняет инициализацию сохраненного регист- 223 рового контекста порожденного процесса, таким образом этот процесс начинает выполняться, не дожидаясь завершения функции, и уже в теле функции начинает осознавать свою предназначение как потомка. Все процессы завершают свое вы- полнение вызовом функции exit, которая отсоединяет области процесса и посы- лает его родителю сигнал "гибель потомка". Процесс-родитель может совместить момент продолжения своего выполнения с моментом завершения процесса-потомка, используя системную функцию wait. Системная функция exec дает процессу воз- можность запускать на выполнение другие программы, накладывая содержимое ис- полняемого файла на свое адресное пространство. Ядро отсоединяет области, ранее занимаемые процессом, и назначает процессу новые области в соответст- вии с потребностями исполняемого файла. Совместное использование областей команд и наличие режима "sticky-bit" дают возможность более рационально ис- пользовать память и экономить время, затрачиваемое на подготовку к запуску программ. Простым пользователям предоставляется возможность получать приви- легии других пользователей, даже суперпользователя, благодаря обращению к услугам системной функции setuid и setuid-программ. С помощью функции brk процесс может изменять размер своей области данных. Функция signal дает про- цессам возможность управлять своей реакцией на поступающие сигналы. При по- лучении сигнала производится обращение к специальной функции обработки сиг- нала с внесением соответствующих изменений в стек задачи и в сохраненный ре- гистровый контекст задачи. Процессы могут сами посылать сигналы, используя системную функцию kill, они могут также контролировать получение сигналов, предназначенных группе процессов, прибегая к услугам функции setpgrp. Командный процессор shell и процесс начальной загрузки init используют стандартные обращения к системным функциям, производя набор операций, в дру- гих системах обычно выполняемых ядром. Shell интерпретирует команды пользо- вателя, переназначает стандартные файлы ввода-вывода данных и выдачи ошибок, порождает процессы, организует каналы между порожденными процессами, синхро- низирует свое выполнение с этими процессами и формирует коды, возвращаемые командами. Процесс init тоже порождает различные процессы, в частности, уп- равляющие работой пользователя за терминалом. Когда такой процесс завершает- ся, init может породить для выполнения той же самой функции еще один про- цесс, если это вытекает из информации файла "/etc/inittab". 7.11 УПРАЖНЕНИЯ 1. Запустите с терминала программу, приведенную на Рисунке 7.33. Переадре- суйте стандартный вывод данных в файл и сравните результаты между со- бой. +------------------------------------+ | main() | | { | | printf("hello\n"); | | if (fork() == 0) | | printf("world\n"); | | } | +------------------------------------+ Рисунок 7.33. Пример модуля, содержащего вызов функции fork и обра- щение к стандартному выводу 2. Разберитесь в механизме работы программы, приведенной на Рисунке 7.34, и сравните ее результаты с результатами программы на Рисунке 7.4. 3. Еще раз обратимся к программе, приведенной на Рисунке 7.5 и показываю- щей, как два процесса обмениваются сообщениями, используя спаренные ка- налы. Что произойдет, если они попытаются вести обмен сообщениями, ис- пользуя один канал ? 4. Возможна ли потеря информации в случае, когда процесс получает несколь- 224 ко сигналов прежде чем ему предоставляется возможность отреагировать на них надлежащим образом ? (Рассмотрите случай, когда процесс подсчитыва- ет количество полученных сигналов о прерывании.) Есть ли необходимость в решении этой проблемы ? 5. Опишите механизм работы системной функции kill. 6. Процесс в программе на Рисунке 7.35 принимает сигналы типа "гибель по- томка" и устанавливает функцию обработки сигналов в исходное состояние. Что происходит при выполнении программы ? 7. Когда процесс получает сигналы определенного типа и не обрабатывает их, ядро дампирует образ процесса в том виде, который был у него в момент получения сигнала. Ядро создает в текущем каталоге процесса файл с име- нем "core" и копирует в него пространство процесса, области команд, данных и стека. Впоследствии пользователь может тщательно изучить дамп образа процесса с помощью стандартных средств отладки. Опишите алго- ритм, которому на Ваш взгляд должно следовать ядро в процессе создания файла "core". Что нужно предпринять в том случае, если в текущем ката- логе файл с таким именем уже существует ? Как должно вести себя ядро, когда в одном и том же каталоге дампируют свои образы сразу несколько процессов? 8. Еще раз обратимся к программе (Рисунок 7.12), описывающей, как один процесс забрасывает другой процесс сигналами, которые принимаются их адресатом. Подумайте, что произошло бы в том случае, если бы алгоритм обработки сигналов был переработан в любом из следующих направлений: +------------------------------------------------------------+ | #include | | int fdrd,fdwt; | | char c; | | | | main(argc,argv) | | int argc; | | char *argv[]; | | { | | if (argc != 3) | | exit(1); | | fork(); | | | | if ((fdrd = open(argv[1],O_RDONLY)) == -1) | | exit(1); | | if (((fdwt = creat(argv[2],0666)) == -1) && | | ((fdwt = open(argv[2],O_WRONLY)) == -1)) | | exit(1); | | rdwrt(); | | } | | rdwrt() | | { | | for (;;) | | { | | if (read(fdrd,&c,1) != 1) | | return; | | write(fdwt,&c,1); | | } | | } | +------------------------------------------------------------+ Рисунок 7.34. Пример программы, в которой процесс-родитель и процесс-потомок не разделяют доступ к файлу * ядро не заменяет функцию обработки сигналов до тех пор, пока пользо- ватель явно не потребует этого; 225 * ядро заставляет процесс игнорировать сигналы до тех пор, пока пользо- ватель не обратится к функции signal вновь. 9. Переработайте алгоритм обработки сигналов так, чтобы ядро автоматически перенастраивало процесс на игнорирование всех последующих поступлений сигналов по возвращении из функции, обрабатывающей их. Каким образом ядро может узнать о завершении функции обработки сигналов, выполняющей- ся в режиме задачи ? Такого рода перенастройка приблизила бы нас к трактовке сигналов в системе BSD. *10. Если процесс получает сигнал, находясь в состоянии приостанова во время выполнения системной функции с допускающим прерывания приоритетом, он выходит из функции по алгоритму longjump. Ядро производит необходимые установки для запуска функции обработки сигнала; когда процесс выйдет из функции обработки сигнала, в версии V это будет выглядеть так, слов- но он вернулся из системной функции с признаком ошибки (как бы прервав свое выполнение). В системе BSD системная функция в этом случае автома- тически перезапускается. Каким образом можно реализовать этот момент в нашей системе? +------------------------------------------------------------+ | #include | | main() | | { | | extern catcher(); | | | | signal(SIGCLD,catcher); | | if (fork() == 0) | | exit(); | | /* пауза до момента получения сигнала */ | | pause(); | | } | | | | catcher() | | { | | printf("процесс-родитель получил сигнал\n"); | | signal(SIGCLD,catcher); | | } | +------------------------------------------------------------+ Рисунок 7.35. Программа, в которой процесс принимает сигналы типа "гибель потомка" 11. В традиционной реализации команды mkdir для создания новой вершины в дереве каталогов используется системная функция mknod, после чего дваж- ды вызывается системная функция link, привязывающая точки входа в ката- лог с именами "." и ".." к новой вершине и к ее родительскому каталогу. Без этих трех операций каталог не будет иметь надлежащий формат. Что произойдет, если во время исполнения команды mkdir процесс получит сиг- нал ? Что если при этом будет получен сигнал SIGKILL, который процесс не распознает ? Эту же проблему рассмотрите применительно к реализации системной функции mkdir. 12. Процесс проверяет наличие сигналов в моменты перехода в состояние при- останова и выхода из него (если в состоянии приостанова процесс нахо- дился с приоритетом, допускающим прерывания), а также в момент перехода в режим задачи из режима ядра по завершении исполнения системной функ- ции или после обработки прерывания. Почему процесс не проверяет наличие сигналов в момент обращения к системной функции ? *13. Предположим, что после исполнения системной функции процесс готовится к возвращению в режим задачи и не обнаруживает ни одного необработанного сигнала. Сразу после этого ядро обрабатывает прерывание и посылает про- 226 цессу сигнал. (Например, пользователем была нажата клавиша "break".) Что делает процесс после того, как ядро завершает обработку прерывания? *14. Если процессу одновременно посылается несколько сигналов, ядро обраба- тывает их в том порядке, в каком они перечислены в описании. Существуют три способа реагирования на получение сигнала - прием сигналов, завер- шение выполнения со сбросом на внешний носитель (дампированием) образа процесса в памяти и завершение выполнения без дампирования. Можно ли указать наилучший порядок обработки одновременно поступающих сигналов ? Например, если процесс получает сигнал о выходе (вызывающий дампирова- ние образа процесса в памяти) и сигнал о прерывании (выход без дампиро- вания), то какой из этих сигналов имело бы смысл обработать первым ? 15. Запомните новую системную функцию newpgrp(pid,ngrp); которая включает процесс с идентификатором pid в группу процессов с но- мером ngrp (устанавливает для процесса новую группу). Подумайте, для каких целей она может использоваться и какие опасности таит в себе ее вызов. 16. Прокомментируйте следующее утверждение: по алгоритму wait процесс может приостановиться до наступления какого-либо события и это не отразилось бы на работе всей системы. 17. Рассмотрим новую системную функцию nowait(pid); где pid - идентификатор процесса, являющегося потомком того процесса, который вызывает функцию. Вызывая функцию, процесс тем самым сообщает ядру о том, что он не собирается дожидаться завершения выполнения свое- го потомка, поэтому ядро может по окончании существования потомка сразу же очистить занимаемое им место в таблице процессов. Каким образом это реализуется на практике ? Оцените достоинства новой функции и сравните ее использование с использованием сигналов типа "гибель потомка". 18. Загрузчик модулей на Си автоматически подключает к основному модулю на- чальную процедуру (startup), которая вызывает функцию main, принадлежа- щую программе пользователя. Если в пользовательской программе отсутст- вует вызов функции exit, процедура startup сама вызывает эту функцию при выходе из функции main. Что произошло бы в том случае, если бы и в процедуре startup отсутствовал вызов функции exit (из-за ошибки загруз- чика) ? 19. Какую информацию получит процесс, выполняющий функцию wait, если его потомок запустит функцию exit без параметра ? Имеется в виду, что про- цесс-потомок вызовет функцию в формате exit() вместо exit(n). Если программист постоянно использует вызов функции exit без параметра, то насколько предсказуемо значение, ожидаемое функцией wait ? Докажите свой ответ. 20. Объясните, что произойдет, если процесс, исполняющий программу на Ри- сунке 7.36 запустит с помощью функции exec самого себя. Как в таком случае ядро сможет избежать возникновения тупиковых ситуаций, связанных с блокировкой индексов ? +----------------------------------+ | main(argc,argv) | | int argc; | | char *argv[]; | | { | | execl(argv[0],argv[0],0); | | } | +----------------------------------+ Рисунок 7.36 21. По условию первым аргументом функции exec является имя (последняя ком- понента имени пути поиска) исполняемого процессом файла. Что произойдет в результате выполнения программы, приведенной на Рисунке 7.37 ? Каков будет эффект, если в качестве файла "a.out" выступит загрузочный мо- 227 дуль, полученный в результате трансляции программы, приведенной на Ри- сунке 7.36 ? 22. Предположим, что в языке Си поддерживается новый тип данных "read-only" (только для чтения), причем процесс, пытающийся записать информацию в поле с этим типом, получает отказ системы защиты. Опишите реализацию этого момента. (Намек: сравните это понятие с понятием "разделяемая об- ласть команд".) В какие из алгоритмов ядра потребуется внести изменения ? Какие еще объекты могут быть реализованы аналогичным с областью обра- зом ? 23. Какие изменения имеют место в алгоритмах open, chmod, unlink и unmount при работе с файлами, для которых установлен режим "sticky-bit" ? Какие действия, например, следует предпринять в отношении такого файла ядру, когда с файлом разрывается связь ? 24. Суперпользователь является единственным пользователем, имеющим право на запись в файл паролей "/etc/passwd", благодаря чему содержимое файла предохраняется от умышленной или случайной порчи. Программа passwd дает пользователям возможность изменять свой собственный пароль, защищая от изменений чужие записи. Каким образом она работает ? +-----------------------------------------------------+ | main() | | { | | if (fork() == 0) | | { | | execl("a.out",0); | | printf("неудачное завершение функции exec\n");| | } | | } | +-----------------------------------------------------+ Рисунок 7.37 *25. Поясните, какая угроза безопасности хранения данных возникает, если setuid-программа не защищена от записи. 26. Выполните следующую последовательность команд, в которой "a. out" - имя исполняемого файла: +-----------------------------------------------------+ | main() | | { | | char *endpt; | | char *sbrk(); | | int brk(); | | | | endpt = sbrk(0); | | printf("endpt = %ud после sbrk\n", (int) endpt); | | | | while (endpt--) | | { | | if (brk(endpt) == -1) | | { | | printf("brk с параметром %ud завершилась | | неудачно\n",endpt); | | exit(); | | } | | } | | } | +-----------------------------------------------------+ Рисунок 7.38 228 chmod 4777 a.out chown root a.out Команда chmod "включает" бит setuid (4 в 4777); пользователь "root" традиционно является суперпользователем. Может ли в результате выполне- ния этой последовательности произойти нарушение защиты информации ? 27. Что произойдет в процессе выполнения программы, представленной на Ри- сунке 7.38 ? Поясните свой ответ. 28. Библиотечная подпрограмма malloc увеличивает область данных процесса с помощью функции brk, а подпрограмма free освобождает память, выделенную подпрограммой malloc. Синтаксис вызова подпрограмм: ptr = malloc(size); free(ptr); где size - целое число без знака, обозначающее количество выделяемых байт памяти, а ptr - символьная ссылка на вновь выделенное пространст- во. Прежде чем появиться в качестве параметра в вызове подпрограммы free, указатель ptr должен быть возвращен подпрограммой malloc. Выпол- ните эти подпрограммы. 29. Что произойдет в процессе выполнения программы, представленной на Ри- сунке 7.39 ? Сравните результаты выполнения этой программы с результа- тами, предусмотренными в системном описании. +-----------------------------------------------------+ | main() | | { | | int i; | | char *cp; | | extern char *sbrk(); | | | | cp = sbrk(10); | | for (i = 0; i < 10; i++) | | *cp++ = 'a' + i; | | sbrk(-10); | | cp = sbrk(10); | | for (i = 0; i < 10; i++) | | printf("char %d = '%c'\n",i,*cp++); | | } | +-----------------------------------------------------+ Рисунок 7.39. Пример программы, использующей подпрограмму sbrk 30. Каким образом командный процессор shell узнает о том, что файл исполня- емый, когда для выполнения команды создает новый процесс ? Если файл исполняемый, то как узнать, создан ли он в результате трансляции исход- ной программы или же представляет собой набор команд языка shell ? В каком порядке следует выполнять проверку указанных условий ? 31. В командном языке shell символы ">>" используются для направления выво- да данных в файл с указанной спецификацией, например, команда: run >> outfile открывает файл с именем "outfile" (а в случае отсутствия файла с таким именем создает его) и записывает в него данные. Напишите прог- рамму, в которой используется эта команда. 32. Процессор командного языка shell проверяет код, возвращаемый функцией exit, воспринимая нулевое значение как "истину", а любое другое значе- ние как "ложь" (обратите внимание на несогласованность с языком Си). Предположим, что файл, исполняющий программу на Рисунке 7.40, имеет имя "truth". Поясните, что произойдет, когда shell будет исполнять следую- щий набор команд: while truth 229 +------------------+ | main() | | { | | exit(0); | | } | +------------------+ Рисунок 7.40 do truth & done 33. Вопрос по Рисунку 7.29: В связи с чем возникает необходимость в созда- нии процессов для конвейерной обработки двухкомпонентной команды в ука- занном порядке ? 34. Напишите более общую программу работы основного цикла процессора shell в части обработки каналов. Имеется в виду, что программа должна уметь обрабатывать случайное число каналов, указанных в командной строке. 35. Переменная среды PATH описывает порядок, в котором shell'у следует просматривать каталоги в поисках исполняемых файлов. В библиотечных функциях execlp и execvp перечисленные в PATH каталоги присоединяются к именам файлов, кроме тех, которые начинаются с символа "/". Выполните эти функции. *36. Для того, чтобы shell в поисках исполняемых файлов не обращался к теку- щему каталогу, суперпользователь должен задать переменную среды PATH. Какая угроза безопасности хранения данных может возникнуть, если shell попытается исполнить файлы из текущего каталога ? 37. Каким образом shell обрабатывает команду cd (создать каталог) ? Какие действия предпринимает shell в процессе обработки следующей командной строки: cd pathname & ? 38. Когда пользователь нажимает на клавиатуре терминала клавиши "delete" или "break", всем процессам, входящим в группу регистрационного shell'а, терминальный драйвер посылает сигнал о прерывании. Пользова- тель может иметь намерение остановить все процессы, порожденные shell'ом, без выхода из системы. Какие усовершенствования в связи с этим следует произвести в теле основного цикла программы shell (Рисунок 7.28) ? 39. С помощью команды nohup command_line пользователь может отменить действие сигналов о "зависании" и о завер- шении (quit) в отношении процессов, реализующих командную строку (command_line). Как эта команда будет обрабатываться в основном цикле программы shell ? 40. Рассмотрим набор команд языка shell: nroff -mm bigfile1 > big1out & nroff -mm bigfile2 > big2out и вновь обратимся к основному циклу программы shell (Рисунок 7.28). Что произойдет, если выполнение первой команды nroff завершится раньше вто- рой ? Какие изменения следует внести в основной цикл программы shell на этот случай ? 41. Часто во время выполнения из shell'а непротестированных программ появ- ляется сообщение об ошибке следующего вида: "Bus error - core dumped" (Ошибка в магистрали - содержимое памяти сброшено на внешний носитель). Очевидно, что в программе выполняются какие-то недопустимые действия; откуда shell узнает о том, что ему нужно вывести сообщение об ошибке ? 42. Процессом 1 в системе может выступать только процесс init. Тем не ме- нее, запустив процесс init, администратор системы может тем самым изме- нить состояние системы. Например, при загрузке система может войти в однопользовательский режим, означающий, что в системе активен только консольный терминал. Для того, чтобы перевести процесс init в состояние 230 2 (многопользовательский режим), администратор системы вводит с консоли команду init 2 . Консольный shell порождает свое ответвление и запускает init. Что имело бы место в системе в том случае, если бы активен был только один про- цесс init ? 43. Формат записей в файле "/etc/inittab" допускает задание действия, свя- занного с каждым порождаемым процессом. Например, с getty-процессом связано действие "respawn" (возрождение), означающее, что процесс init должен возрождать getty-процесс, если последний прекращает существова- ние. На практике, когда пользователь выходит из системы процесс init порождает новый getty-процесс, чтобы другой пользователь мог получить доступ к временно бездействующей терминальной линии. Каким образом это делает процесс init ? 44. Некоторые из алгоритмов ядра прибегают к просмотру таблицы процессов. Время поиска данных можно сократить, если использовать указатели на: родителя процесса, любого из потомков, другой процесс, имеющий того же родителя. Процесс обнаруживает всех своих потомков, следуя сначала за указателем на любого из потомков, а затем используя указатели на другие процессы, имеющие того же родителя (циклы недопустимы). Какие из алго- ритмов выиграют от этого ? Какие из алгоритмов нужно оставить без изме- 231