ание. В программе, приведенной на Рисунке 7.15, процесс создает новый процесс, который печатает свой код идентификации и вызывает системную функцию pause, приостанавливаясь до получения сигнала. Процесс-родитель печатает PID своего потомка и завершается, возвращая только что выведенное значение через пара- метр status. Если бы вызов функции exit отсутствовал, начальная процедура сделала бы его по выходе процесса из функции main. Порожденный процесс про- должает ожидать получения сигнала, даже если его родитель уже завершился. 7.4 ОЖИДАНИЕ ЗАВЕРШЕНИЯ ВЫПОЛНЕНИЯ ПРОЦЕССА Процесс может синхронизировать продолжение своего выполнения с моментом завершения потомка, если воспользуется системной функцией wait. Синтаксис вызова функции: +------------------------------------------------------------+ | main() | | { | | int child; | | | | if ((child = fork()) == 0) | | { | | printf("PID потомка %d\n",getpid()); | | pause(); /* приостанов выполнения до получения | | сигнала */ | | } | | /* родитель */ | | printf("PID потомка %d\n",child); | | exit(child); | | } | +------------------------------------------------------------+ Рисунок 7.15. Пример использования функции exit pid = wait(stat_addr); где pid - значение кода идентификации (PID) прекратившего свое существование потомка, stat_addr - адрес переменной целого типа, в которую будет помещено возвращаемое функцией exit значение, в пространстве задачи. Алгоритм функции wait приведен на Рисунке 7.16. Ядро ведет поиск потом- ков процесса, прекративших существование, и в случае их отсутствия возвраща- ет ошибку. Если потомок, прекративший существование, обнаружен, ядро переда- ет его код идентификации и значение, возвращаемое через параметр функции exit, процессу, вызвавшему функцию wait. Таким образом, через параметр функ- 200 ции exit (status) завершающийся процесс может передавать различные значения, в закодированном виде содержащие информацию о причине завершения процесса, однако на практике этот параметр используется по назначению довольно редко. Ядро передает в соответствующие поля, принадлежащие пространству родитель- ского процесса, накопленные значения продолжительности исполнения процес- са-потомка в режиме ядра и в режиме задачи и, наконец, освобождает в таблице процессов место, которое в ней занимал прежде прекративший существование процесс. Это место будет предоставлено новому процессу. Если процесс, выполняющий функцию wait, имеет потомков, продолжающих су- ществование, он приостанавливается до получения ожидаемого сигнала. Ядро не возобновляет по своей инициативе процесс, приостановившийся с помощью функ- ции wait: такой процесс может возобновиться только в случае получения сигна- ла. На все сигналы, кроме сигнала "гибель потомка", процесс реагирует ранее рассмотренным образом. Реакция процесса на сигнал "гибель потомка" проявля- ется по-разному в зависимости от обстоятельств: * По умолчанию (то есть если специально не оговорены никакие другие дейст- вия) процесс выходит из состояния останова, в которое он вошел с помощью функции wait, и запускает алгоритм issig для опознания типа поступившего сигнала. Алгоритм issig (Рисунок 7.7) рассматривает особый случай пос- тупления сигнала типа "гибель потомка" и возвращает "ложь". Поэтому ядро не выполняет longjump из функции sleep, а возвращает управление функции wait. Оно перезапускает функцию wait, находит потомков, прекративших су- ществование (по крайней мере, одного), освобождает место в таблице про- цессов, занимаемое этими потомками, и выходит из функции wait, возвращая +------------------------------------------------------------+ | алгоритм wait | | входная информация: адрес переменной для хранения значения| | status, возвращаемого завершающимся | | процессом | | выходная информация: идентификатор потомка и код возврата | | функции exit | | { | | если (процесс, вызвавший функцию wait, не имеет потом- | | ков) | | возвратить (ошибку); | | | | для (;;) /* цикл с внутренним циклом */ | | { | | если (процесс, вызвавший функцию wait, имеет потом-| | ков, прекративших существование) | | { | | выбрать произвольного потомка; | | передать его родителю информацию об использова-| | нии потомком ресурсов центрального процессора;| | освободить в таблице процессов место, занимае- | | мое потомком; | | возвратить (идентификатор потомка, код возврата| | функции exit, вызванной потомком); | | } | | если (у процесса нет потомков) | | возвратить ошибку; | | приостановиться с приоритетом, допускающим прерыва-| | ния (до завершения потомка); | | } | | } | +------------------------------------------------------------+ Рисунок 7.16. Алгоритм функции wait 201 управление процессу, вызвавшему ее. * Если процессы принимает сигналы данного типа, ядро делает все необходи- мые установки для запуска пользовательской функции обработки сигнала, как и в случае поступления сигнала любого другого типа. * Если процесс игнорирует сигналы данного типа, ядро перезапускает функцию wait, освобождает в таблице процессов место, занимаемое потомками, прек- ратившими существование, и исследует оставшихся потомков. Например, если пользователь запускает программу, приведенную на Рисунке 7.17, с параметром и без параметра, он получит разные результаты. Сначала рассмотрим случай, когда пользователь запускает программу без параметра (единственный параметр - имя программы, то есть argc равно 1). Родительский процесс порождает 15 потомков, которые в конечном итоге завершают свое вы- полнение с кодом возврата i, номером процесса в порядке очередности созда- ния. Ядро, исполняя функцию wait для родителя, находит потомка, прекративше- го существование, и передает родителю его идентификатор и код возврата функ- ции exit. При этом заранее не известно, какой из потомков будет обнаружен. Из текста программы, реализующей системную функцию exit, написанной на языке Си и включенной в библиотеку стандартных подпрограмм, видно, что программа запоминает код возврата функции exit в битах 8-15 поля ret_code и возвращает функции wait идентификатор процесса-потомка. Таким образом, в ret_code хра- нится значение, равное 256*i, где i - номер потомка, а в ret_val заносится значение идентификатора потомка. Если пользователь запускает программу с параметром (то есть argc > 1), родительский процесс с помощью функции signal делает распоряжение игнориро- вать сигналы типа "гибель потомка". Предположим, что родительский процесс, выполняя функцию wait, приостановился еще до того, как его потомок произвел обращение к функции exit: когда процесс-потомок переходит к выполнению функ- ции exit, он посылает своему родителю сигнал "гибель потомка"; родительский процесс возобновляется, поскольку он был приостановлен с приоритетом, допус- кающим прерывания. Когда так или иначе родительский процесс продолжит свое +------------------------------------------------------------+ | #include | | main(argc,argv) | | int argc; | | char *argv[]; | | { | | int i,ret_val,ret_code; | | | | if (argc >= 1) | | signal(SIGCLD,SIG_IGN); /* игнорировать гибель | | потомков */ | | for (i = 0; i < 15; i++) | | if (fork() == 0) | | { | | /* процесс-потомок */ | | printf("процесс-потомок %x\n",getpid()); | | exit(i); | | } | | ret_val = wait(&ret_code); | | printf("wait ret_val %x ret_code %x\n",ret_val,ret_code);| | } | +------------------------------------------------------------+ Рисунок 7.17. Пример использования функции wait и игнорирова- ния сигнала "гибель потомка" 202 выполнение, он обнаружит, что сигнал сообщал о "гибели" потомка; однако, поскольку он игнорирует сигналы этого типа и не обрабатывает их, ядро удаляет из таблицы процессов запись, соответствующую прекратившему существование потомку, и продолжает выполнение функции wait так, словно сигнала и не было. Ядро выполняет эти действия вся- кий раз, когда родительский процесс получает сигнал типа "гибель потомка", до тех пор, пока цикл выполнения функции wait не будет завершен и пока не будет установлено, что у процесса больше потомков нет. Тогда функция wait возвращает значение, равное -1. Разница между двумя способами запуска прог- раммы состоит в том, что в первом случае процесс-родитель ждет завершения любого из потомков, в то время как во втором случае он ждет, пока завершатся все его потомки. В ранних версиях системы UNIX функции exit и wait не использовали и не рассматривали сигнал типа "гибель потомка". Вместо посылки сигнала функция exit возобновляла выполнение родительского процесса. Если родительский про- цесс при выполнении функции wait приостановился, он возобновляется, находит потомка, прекратившего существование, и возвращает управление. В противном случае возобновления не происходит; процесс-родитель обнаружит "погибшего" потомка при следующем обращении к функции wait. Точно так же и процесс на- чальной загрузки (init) может приостановиться, используя функцию wait, и за- вершающиеся по exit процессы будут возобновлять его, если он имеет усынов- ленных потомков, прекращающих существование. В такой реализации функций exit и wait имеется одна нерешенная проблема, связанная с тем, что процессы, прекратившие существование, нельзя убирать из системы до тех пор, пока их родитель не исполнит функцию wait. Если процесс создал множество потомков, но так и не исполнил функцию wait, может произой- ти переполнение таблицы процессов из-за наличия потомков, прекративших су- ществование с помощью функции exit. В качестве примера рассмотрим текст программы планировщика процессов, приведенный на Рисунке 7.18. Процесс про- изводит считывание данных из файла стандартного ввода до тех пор, пока не будет обнаружен конец файла, создавая при каждом исполнении функции read но- вого потомка. Однако, процесс-родитель не дожидается завершения каждого по- томка, поскольку он стремится запускать процессы на выполнение как можно быстрее, тем более, что может пройти довольно много времени, прежде чем про- цесс-потомок завершит свое выполнение. Если, обратившись к +------------------------------------------------------------+ | #include | | main(argc,argv) | | { | | char buf[256]; | | | | if (argc != 1) | | signal(SIGCLD,SIG_IGN); /* игнорировать гибель | | потомков */ | | while (read(0,buf,256)) | | if (fork() == 0) | | { | | /* здесь процесс-потомок обычно выполняет | | какие-то операции над буфером (buf) */ | | exit(0); | | } | | } | +------------------------------------------------------------+ Рисунок 7.18. Пример указания причины появления сигнала "ги- бель потомков" 203 функции signal, процесс распорядился игнорировать сигналы типа "гибель по- томка", ядро будет очищать записи, соответствующие прекратившим существова- ние процессам, автоматически. Иначе в конечном итоге из-за таких процессов может произойти переполнение таблицы. 7.5 ВЫЗОВ ДРУГИХ ПРОГРАММ Системная функция exec дает возможность процессу запускать другую прог- рамму, при этом соответствующий этой программе исполняемый файл будет распо- лагаться в пространстве памяти процесса. Содержимое пользовательского кон- текста после вызова функции становится недоступным, за исключением передава- емых функции параметров, которые переписываются ядром из старого адресного пространства в новое. Синтаксис вызова функции: execve(filename,argv,envp) где filename - имя исполняемого файла, argv - указатель на массив парамет- ров, которые передаются вызываемой программе, а envp - указатель на массив параметров, составляющих среду выполнения вызываемой программы. Вызов сис- темной функции exec осуществляют несколько библиотечных функций, таких как execl, execv, execle и т.д. В том случае, когда программа использует пара- метры командной строки main(argc,argv) , +------------------------------------------------------------+ | алгоритм exec | | входная информация: (1) имя файла | | (2) список параметров | | (3) список переменных среды | | выходная информация: отсутствует | | { | | получить индекс файла (алгоритм namei); | | проверить, является ли файл исполнимым и имеет ли поль- | | зователь право на его исполнение; | | прочитать информацию из заголовков файла и проверить, | | является ли он загрузочным модулем; | | скопировать параметры, переданные функции, из старого | | адресного пространства в системное пространство; | | для (каждой области, присоединенной к процессу) | | отсоединить все старые области (алгоритм detachreg);| | для (каждой области, определенной в загрузочном модуле) | | { | | выделить новые области (алгоритм allocreg); | | присоединить области (алгоритм attachreg); | | загрузить область в память по готовности (алгоритм | | loadreg); | | } | | скопировать параметры, переданные функции, в новую об- | | ласть стека задачи; | | специальная обработка для setuid-программ, трассировка; | | проинициализировать область сохранения регистров задачи | | (в рамках подготовки к возвращению в режим задачи); | | освободить индекс файла (алгоритм iput); | | } | +------------------------------------------------------------+ Рисунок 7.19. Алгоритм функции exec 204 массив argv является копией одноименного параметра, передаваемого функции exec. Символьные строки, описывающие среду выполнения вызываемой программы, имеют вид "имя=значение" и содержат полезную для программ информацию, такую как начальный каталог пользователя и путь поиска исполняемых программ. Про- цессы могут обращаться к параметрам описания среды выполнения, используя глобальную пере- менную environ, которую заводит начальная процедура Си-интерпретатора. На Рисунке 7.19 представлен алгоритм выполнения системной функции exec. Сначала функция обращается к файлу по алгоритму namei, проверяя, является ли файл исполнимым и отличным от каталога, а также проверяя наличие у пользова- теля права исполнять программу. Затем ядро, считывая заголовок файла, опре- деляет размещение информации в файле (формат файла). На Рисунке 7.20 изображен логический формат исполняемого файла в файло- вой системе, обычно генерируемый транслятором или загрузчиком. Он разбивает- ся на четыре части: 1. Главный заголовок, содержащий информацию о том, на сколько разделов де- лится файл, а также содержащий начальный адрес исполнения процесса и не- которое "магическое число", описывающее тип исполняемого файла. 2. Заголовки разделов, содержащие информацию, описывающую каждый раздел в файле: его размер, виртуальные адреса, в которых он располагается, и др. 3. Разделы, содержащие собственно "данные" файла (например, текстовые), ко- торые загружаются в адресное пространство процесса. 4. Разделы, содержащие смешанную информацию, такую как таблицы идентифика- торов и другие данные, используемые в процессе отладки. +---------------------------+ | Тип файла | Главный заголовок | Количество разделов | | Начальное состояние регис-| | тров | +---------------------------+ | Тип раздела | Заголовок 1-го раздела | Размер раздела | | Виртуальный адрес | +---------------------------+ | Тип раздела | Заголовок 2-го раздела | Размер раздела | - | Виртуальный адрес | - +---------------------------+ - | - | - | - | - +---------------------------+ - | Тип раздела | Заголовок n-го раздела | Размер раздела | | Виртуальный адрес | +---------------------------+ Раздел 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"; | | снять с области блокировку; | | } | +-----------------------------------------