ГЛАВА 7. УПРАВЛЕНИЕ ПРОЦЕССАМИ В предыдущей главе был рассмотрен контекст процесса и описаны алгоритмы для работы с ним; в данной главе речь пойдет об использовании и реализации системных функций, управляющих контекстом процесса. Системная функция fork создает новый процесс, функция exit завершает выполнение процесса, а wait дает возможность родительскому процессу синхронизировать свое продолжение с завершением порожденного процесса. Об асинхронных событиях процессы информи- руются при помощи сигналов. Поскольку ядро синхронизирует выполнение функций exit и wait при помощи сигналов, описание механизма сигналов предваряет со- бой рассмотрение функций exit и wait. Системная функция exec дает процессу возможность запускать "новую" программу, накладывая ее адресное пространство на исполняемый образ файла. Системная функция brk позволяет динамически вы- делять дополнительную память; теми же самыми средствами ядро динамически на- ращивает стек задачи, выделяя в случае необходимости дополнительное прост- ранство. В заключительной части главы дается краткое описание основных групп операций командного процессора shell и начального процесса init. На Рисунке 7.1 показана взаимосвязь между системными функциями, рассмат- риваемыми в данной главе, с одной стороны, и алгоритмами, описанными в пре- дыдущей главе, с другой. Почти во всех функциях используются алгоритмы sleep и wakeup, отсутствующие на рисунке. Функция exec, кроме того, взаимодейству- ет с алгоритмами работы с файловой системой, речь о которых шла в главах 4 и 5. +-----------------------------+---------------------+------------+ | Системные функции, имеющие | Системные функции, | Функции | | ющие дело с управлением па- | связанные с синхро- | смешанного | | мятью | низацией | типа | +-------+-------+-------+-----+--+----+------+----+-+-----+------+ | fork | exec | brk | exit |wait|signal|kill|setrgrр|setuid| +-------+-------+-------+--------+----+------+----+-------+------+ |dupreg |detach-|growreg| detach-| | |attach-| reg | | reg | | | reg |alloc- | | | | | | reg | | | | | |attach-| | | | | | reg | | | | | |growreg| | | | | |loadreg| | | | | |mapreg | | | | +-------+-------+-------+--------+-------------------------------+ Рисунок 7.1. Системные функции управления процессом и их связь с другими алгоритмами 7.1 СОЗДАНИЕ ПРОЦЕССА Единственным способом создания пользователем нового процесса в операци- онной системе UNIX является выполнение системной функции fork. Процесс, вы- зывающий функцию fork, называется родительским (процесс-родитель), вновь создаваемый процесс называется порожденным (процесс-потомок). Синтаксис вы- зова функции fork: 179 pid = fork(); В результате выполнения функции fork пользовательский контекст и того, и другого процессов совпадает во всем, кроме возвращаемого значения переменной pid. Для родительского процесса в pid возвращается идентификатор порожденно- го процесса, для порожденного - pid имеет нулевое значение. Нулевой процесс, возникающий внутри ядра при загрузке системы, является единственным процес- сом, не создаваемым с помощью функции fork. В ходе выполнения функции ядро производит следующую последовательность действий: 1. Отводит место в таблице процессов под новый процесс. 2. Присваивает порождаемому процессу уникальный код идентификации. 3. Делает логическую копию контекста родительского процесса. Поскольку те или иные составляющие процесса, такие как область команд, могут разде- ляться другими процессами, ядро может иногда вместо копирования области в новый физический участок памяти просто увеличить значение счетчика ссылок на область. 4. Увеличивает значения счетчика числа файлов, связанных с процессом, как в таблице файлов, так и в таблице индексов. 5. Возвращает родительскому процессу код идентификации порожденного процес- са, а порожденному процессу - нулевое значение. Реализацию системной функции fork, пожалуй, нельзя назвать тривиальной, так как порожденный процесс начинает свое выполнение, возникая как бы из воздуха. Алгоритм реализации функции для систем с замещением страниц по зап- росу и для систем с подкачкой процессов имеет лишь незначительные различия; все изложенное ниже в отношении этого алгоритма касается в первую очередь традиционных систем с подкачкой процессов, но с непременным акцентированием внимания на тех моментах, которые в системах с замещением страниц по запросу реализуются иначе. Кроме того, конечно, предполагается, что в системе имеет- ся свободная оперативная память, достаточная для размещения порожденного процесса. В главе 9 будет отдельно рассмотрен случай, когда для порожденного процесса не хватает памяти, и там же будут даны разъяснения относительно ре- ализации алгоритма fork в системах с замещением страниц. На Рисунке 7.2 приведен алгоритм создания процесса. Сначала ядро должно удостовериться в том, что для успешного выполнения алгоритма fork есть все необходимые ресурсы. В системе с подкачкой процессов для размещения порожда- емого процесса требуется место либо в памяти, либо на диске; в системе с за- мещением страниц следует выделить память для вспомогательных таблиц (в част- ности, таблиц страниц). Если свободных ресурсов нет, алгоритм fork заверша- ется неудачно. Ядро ищет место в таблице процессов для конструирования кон- текста порождаемого процесса и проверяет, не превысил ли пользователь, вы- полняющий fork, ограничение на максимально-допустимое количество параллельно запущенных процессов. Ядро также подбирает для нового процесса уникальный идентификатор, значение которого превышает на единицу максимальный из сущес- твующих идентификаторов. Если предлагаемый идентификатор уже присвоен друго- му процессу, ядро берет идентификатор, следующий по порядку. Как только бу- дет достигнуто максимально-допустимое значение, отсчет идентификаторов опять начнется с 0. Поскольку большинство процессов имеет короткое время жизни, при переходе к началу отсчета значительная часть идентификаторов оказывается свободной. На количество одновременно выполняющихся процессов накладывается ограни- чение (конфигурируемое), отсюда ни один из пользователей не может занимать в таблице процессов слишком много места, мешая тем самым другим пользователям создавать новые процессы. Кроме того, простым пользователям не разрешается создавать процесс, занимающий последнее свободное место в таблице процессов, в противном случае система зашла бы в тупик. Другими словами, поскольку в таблице процессов нет свободного места, то ядро не может гарантировать, что все существующие процессы завершатся естественным образом, поэтому новые 180 +------------------------------------------------------------+ | алгоритм fork | | входная информация: отсутствует | | выходная информация: для родительского процесса - идентифи-| | катор (PID) порожденного процесса | | для порожденного процесса - 0 | | { | | проверить доступность ресурсов ядра; | | получить свободное место в таблице процессов и уникаль- | | ный код идентификации (PID); | | проверить, не запустил ли пользователь слишком много | | процессов; | | сделать пометку о том, что порождаемый процесс находится| | в состоянии "создания"; | | скопировать информацию в таблице процессов из записи, | | соответствующей родительскому процессу, в запись, соот-| | ветствующую порожденному процессу; | | увеличить значения счетчиков ссылок на текущий каталог и| | на корневой каталог (если он был изменен); | | увеличить значение счетчика открытий файла в таблице | | файлов; | | сделать копию контекста родительского процесса (адресное| | пространство, команды, данные, стек) в памяти; | | поместить в стек фиктивный уровень системного контекста | | над уровнем системного контекста, соответствующим по- | | рожденному процессу; | | фиктивный контекстный уровень содержит информацию, | | необходимую порожденному процессу для того, чтобы | | знать все о себе и будучи выбранным для исполнения | | запускаться с этого места; | | если (в данный момент выполняется родительский процесс) | | { | | перевести порожденный процесс в состояние "готовности| | к выполнению"; | | возвратить (идентификатор порожденного процесса); | | /* из системы пользователю */ | | } | | в противном случае /* выполняется порожденный | | процесс */ | | { | | записать начальные значения в поля синхронизации ад- | | ресного пространства процесса; | | возвратить (0); /* пользователю */ | | } | | } | +------------------------------------------------------------+ Рисунок 7.2. Алгоритм fork процессы создаваться не будут. С другой стороны, суперпользователю нужно дать возможность исполнять столько процессов, сколько ему потребуется, ко- нечно, учитывая размер таблицы процессов, при этом процесс, исполняемый су- перпользователем, может занять в таблице и последнее свободное место. Пред- полагается, что суперпользователь может прибегать к решительным мерам и за- пускать процесс, побуждающий остальные процессы к завершению, если это вызы- вается необходимостью (см. раздел 7.2.3, где говорится о системной функции kill). Затем ядро присваивает начальные значения различным полям записи таблицы 181 процессов, соответствующей порожденному процессу, копируя в них значения по- лей из записи родительского процесса. Например, порожденный процесс "насле- дует" у родительского процесса коды идентификации пользователя (реальный и тот, под которым исполняется процесс), группу процессов, управляемую роди- тельским процессом, а также значение, заданное родительским процессом в фун- кции nice и используемое при вычислении приоритета планирования. В следующих разделах мы поговорим о назначении этих полей. Ядро передает значение поля идентификатора родительского процесса в запись порожденного, включая послед- ний в древовидную структуру процессов, и присваивает начальные значения раз- личным параметрам планирования, таким как приоритет планирования, использо- вание ресурсов центрального процессора и другие значения полей синхрониза- ции. Начальным состоянием процесса является состояние "создания" (см. Рису- нок 6.1). После того ядро устанавливает значения счетчиков ссылок на файлы, с ко- торыми автоматически связывается порождаемый процесс. Во-первых, порожденный процесс размещается в текущем каталоге родительского процесса. Число процес- сов, обращающихся в данный момент к каталогу, увеличивается на 1 и, соответ- ственно, увеличивается значение счетчика ссылок на его индекс. Во-вторых, если родительский процесс или один из его предков уже выполнял смену корне- вого каталога с помощью функции chroot, порожденный процесс наследует и но- вый корень с соответствующим увеличением значения счетчика ссылок на индекс корня. Наконец, ядро просматривает таблицу пользовательских дескрипторов для родительского процесса в поисках открытых файлов, известных процессу, и уве- личивает значение счетчика ссылок, ассоциированного с каждым из открытых файлов, в глобальной таблице файлов. Порожденный процесс не просто наследует права доступа к открытым файлам, но и разделяет доступ к файлам с родитель- ским процессом, так как оба процесса обращаются в таблице файлов к одним и тем же записям. Действие fork в отношении открытых файлов подобно действию алгоритма dup: новая запись в таблице пользовательских дескрипторов файла указывает на запись в глобальной таблице файлов, соответствующую открытому файлу. Для dup, однако, записи в таблице пользовательских дескрипторов файла относятся к одному процессу; для fork - к разным процессам. После завершения всех этих действий ядро готово к созданию для порожден- ного процесса пользовательского контекста. Ядро выделяет память для адресно- го пространства процесса, его областей и таблиц страниц, создает с помощью алгоритма dupreg копии всех областей родительского процесса и присоединяет с помощью алгоритма attachreg каждую область к порожденному процессу. В систе- ме с подкачкой процессов ядро копирует содержимое областей, не являющихся областями разделяемой памяти, в новую зону оперативной памяти. Вспомним из раздела 6.2.4 о том, что в пространстве процесса хранится указатель на соот- ветствующую запись в таблице процессов. За исключением этого поля, во всем остальном содержимое адресного пространства порожденного процесса в начале совпадает с содержимым пространства родительского процесса, но может расхо- диться после завершения алгоритма fork. Родительский процесс, например, пос- ле выполнения fork может открыть новый файл, к которому порожденный процесс уже не получит доступ автоматически. Итак, ядро завершило создание статической части контекста порожденного процесса; теперь оно приступает к созданию динамической части. Ядро копирует в нее первый контекстный уровень родительского процесса, включающий в себя сохраненный регистровый контекст задачи и стек ядра в момент вызова функции fork. Если в данной реализации стек ядра является частью пространства про- цесса, ядро в момент создания пространства порожденного процесса автомати- чески создает и системный стек для него. В противном случае родительскому процессу придется скопировать в пространство памяти, ассоциированное с по- рожденным процессом, свой системный стек. В любом случае стек ядра для по- рожденного процесса совпадает с системным стеком его родителя. Далее ядро создает для порожденного процесса фиктивный контекстный уровень (2), в кото- ром содержится сохраненный регистровый контекст из первого контекстного уровня. Значения счетчика команд (регистр PC) и других регистров, сохраняе- 182 мые в регистровом контексте, устанавливаются таким образом, чтобы с их по- мощью можно было "восстанавливать" контекст порожденного процесса, пусть да- же последний еще ни разу не исполнялся, и чтобы этот процесс при запуске всегда помнил о том, что он порожденный. Например, если программа ядра про- веряет значение, хранящееся в регистре 0, для того, чтобы выяснить, является ли данный процесс родительским или же порожденным, то это значение переписы- вается в регистровый контекст порожденного процесса, сохраненный в составе первого уровня. Механизм сохранения используется тот же, что и при переклю- чении контекста (см. предыдущую главу). Родительский процесс +---------------------------------------------+ Таблица | +---------+ Частная Адресное простран- | файлов | | Область | таблица ство процесса | +---------+ | | данных | областей +------------------+| | - | | +---------+ процесса | Открытые файлы --||-- + | - | | | +------+ | || - | - | | -- - - + | + +| Текущий каталог -||+ | +---------+ | - +------+ -| ||- -- -| | | +---------+ + + | || Измененный корень||| | | | | | Стек | +------+ -+------------------+|- - +---------+ | | задачи + - + | |+------------------+|| | | - | | +---------+ +------+ -| - ||- - | - | | || - ||| | | - | | -| - ||- - +---------+ | -- - - - - - - - -+| Стек ядра ||| + - + | | | +------------------+|- - | | +---------------------------------------------+| | +---------+ - - - | - | +----+----+ | | | - | |Разделяе-| - - | - | | мая | | | +---------+ | область | - -- -| | | команд | | | | | +----+----+ - - +---------+ - | | +---------+ + - - - - - - - - + - - +---------------------------------------------++ -|+ Таблица | +---------+ Частная | Адресное простран- | -- файлов | | Область | таблица - ство процесса | || +---------+ | | данных | областей |+------------------+| -- | - | | +---------+ процесса -| Открытые файлы --||-- +| | - | | | +------+ || || - | - | | -- - - + | +--| Текущий каталог -||+ | +---------+ | - +------+ | ||- -- + | | +---------+ + + | | Измененный корень||+ - - -| | | | Стек | +------+ +------------------+| +---------+ | | задачи + - + | +------------------+| | - | | +---------+ +------+ | - || | - | | | - || +---------+ | | Стек ядра || | | | +------------------+| | | +---------------------------------------------+ +---------+ Порожденный процесс | - | | - | +---------+ Рисунок 7.3. Создание контекста нового процесса при выполне- нии функции fork 183 Если контекст порожденного процесса готов, родительский процесс заверша- ет свою роль в выполнении алгоритма fork, переводя порожденный процесс в состояние "готовности к запуску, находясь в памяти" и возвращая пользователю его идентификатор. Затем, используя обычный алгоритм планирования, ядро вы- бирает порожденный процесс для исполнения и тот "доигрывает" свою роль в ал- горитме fork. Контекст порожденного процесса был задан родительским процес- сом; с точки зрения ядра кажется, что порожденный процесс возобновляется после приостанова в ожидании ресурса. Порожденный процесс при выполнении функции fork реализует ту часть программы, на которую указывает счетчик ко- манд, восстанавливаемый ядром из сохраненного на уровне 2 регистрового кон- текста, и по выходе из функции возвращает нулевое значение. На Рисунке 7.3 представлена логическая схема взаимодействия родительско- го и порожденного процессов с другими структурами данных ядра сразу после завершения системной функции fork. Итак, оба процесса совместно пользуются файлами, которые были открыты родительским процессом к моменту исполнения функции fork, при этом значение счетчика ссылок на каждый из этих файлов в таблице файлов на единицу больше, чем до вызова функции. Порожденный процесс имеет те же, что и родительский процесс, текущий и корневой каталоги, значе- ние же счетчика ссылок на индекс каждого из этих каталогов так же становится на единицу больше, чем до вызова функции. Содержимое областей команд, данных и стека (задачи) у обоих процессов совпадает; по типу области и версии сис- темной реализации можно установить, могут ли процессы разделять саму область команд в физических адресах. Рассмотрим приведенную на Рисунке 7.4 программу, которая представляет собой пример разделения доступа к файлу при исполнении функции fork. Пользо- вателю следует передавать этой программе два параметра - имя существующего файла и имя создаваемого файла. Процесс открывает существующий файл, создает новый файл и - при условии отсутствия ошибок - порождает новый процесс. Внутри программы ядро делает копию контек- ста родительского процесса для порожденного, при этом родительский процесс исполняется в одном адресном пространстве, а порожденный - в другом. Каждый из процессов может работать со своими собственными копиями глобальных пере- менных fdrd, fdwt и c, а также со своими собственными копиями стековых пере- менных argc и argv, но ни один из них не может обращаться к переменным дру- гого процесса. Тем не менее, при выполнении функции fork ядро делает копию адресного пространства первого процесса для второго, и порожденный процесс, таким образом, наследует доступ к файлам родительского (то есть к файлам, им ранее открытым и созданным) с правом использования тех же самых деск- рипторов. Родительский и порожденный процессы независимо друг от друга, конечно, вызывают функцию rdwrt и в цикле считывают по одному байту информацию из ис- ходного файла и переписывают ее в файл вывода. Функция rdwrt возвращает уп- равление, когда при считывании обнаруживается конец файла. Ядро перед тем уже увеличило значения счетчиков ссылок на исходный и результирующий файлы в таблице файлов, и дескрипторы, используемые в обоих процессах, адресуют к одним и тем же строкам в таблице. Таким образом, дескрипторы fdrd в том и в другом процессах указывают на запись в таблице файлов, соответствующую ис- ходному файлу, а дескрипторы, подставляемые в качестве fdwt, - на запись, соответствующую результирующему файлу (файлу вывода). Поэтому оба процесса никогда не обратятся вместе на чтение или запись к одному и тому же адресу, вычисляемому с помощью смещения внутри файла, поскольку ядро смещает внутри- файловые указатели после каждой операции чтения или записи. Несмотря на то, что, казалось бы, из-за того, что процессы распределяют между собой рабочую нагрузку, они копируют исходный файл в два раза быстрее, содержимое резуль- тирующего файла зависит от очередности, в которой ядро запускает процессы. Если ядро запускает процессы так, что они исполняют системные функции попе- ременно (чередуя и спаренные вызовы функций read-write), содержимое резуль- 184 +------------------------------------------------------------+ | #include | | int fdrd, fdwt; | | char c; | | | | main(argc, argv) | | int argc; | | char *argv[]; | | { | | if (argc != 3) | | exit(1); | | if ((fdrd = open(argv[1],O_RDONLY)) == -1) | | exit(1); | | if ((fdwt = creat(argv[2],0666)) == -1) | | exit(1); | | | | fork(); | | /* оба процесса исполняют одну и ту же программу */ | | rdwrt(); | | exit(0); | | } | | | | rdwrt(); | | { | | for(;;) | | { | | if (read(fdrd,&c,1) != 1) | | return; | | write(fdwt,&c,1); | | } | | } | +------------------------------------------------------------+ Рисунок 7.4. Программа, в которой родительский и порожденный процессы разделяют доступ к файлу тирующего файла будет совпадать с содержимым исходного файла. Рассмотрим, однако, случай, когда процессы собираются считать из исходного файла после- довательность из двух символов "ab". Предположим, что родительский процесс считал символ "a", но не успел записать его, так как ядро переключилось на контекст порожденного процесса. Если порожденный процесс считывает символ "b" и записывает его в результирующий файл до возобновления родительского процесса, строка "ab" в результирующем файле будет иметь вид "ba". Ядро не гарантирует согласование темпов выполнения процессов. Теперь перейдем к программе, представленной на Рисунке 7.5, в которой процесс-потомок наследует от своего родителя файловые дескрипторы 0 и 1 (со- ответствующие стандартному вводу и стандартному выводу). При каждом выполне- нии системной функции pipe производится назначение двух файловых дескрипто- ров в массивах to_par и to_chil. Процесс вызывает функцию fork и делает ко- пию своего контекста: каждый из процессов имеет доступ только к своим собст- венным данным, так же как и в предыдущем примере. Родительский процесс зак- рывает файл стандартного вывода (дескриптор 1) и дублирует дескриптор запи- си, возвращаемый в канал to_chil. Поскольку первое свободное место в таблице дескрипторов родительского процесса образовалось в результате только что вы- полненной операции закрытия (close) файла вывода, ядро переписывает туда дескриптор записи в канал и этот дескриптор становится дескриптором файла стандартного вывода для to_chil. Те же самые действия родительский процесс выполняет в отношении дескриптора файла стандартного ввода, заменяя его дес- 185 криптором чтения из канала to_par. И порожденный процесс закрывает файл стандартного ввода (дескриптор 0) и так же дублирует дескриптор чтения из канала to_chil. Поскольку первое свободное место в таблице дескрипторов фай- лов прежде было занято файлом стандартного ввода, его дескриптором становит- ся дескриптор чтения из канала to_chil. Аналогичные действия выполняются и в отношении дескриптора файла стандартного вывода, заменяя его дескриптором записи в канал to_par. И тот, и другой процессы закрывают файлы, дескрипторы +------------------------------------------------------------+ | #include | | char string[] = "hello world"; | | main() | | { | | int count,i; | | int to_par[2],to_chil[2]; /* для каналов родителя и | | потомка */ | | char buf[256]; | | pipe(to_par); | | pipe(to_chil); | | if (fork() == 0) | | { | | /* выполнение порожденного процесса */ | | close(0); /* закрытие прежнего стандартного ввода */ | | dup(to_chil[0]); /* дублирование дескриптора чтения | | из канала в позицию стандартного | | ввода */ | | close(1); /* закрытие прежнего стандартного вывода */| | dup(to_par[0]); /* дублирование дескриптора записи | | в канал в позицию стандартного | | вывода */ | | close(to_par[1]); /* закрытие ненужных дескрипторов | | close(to_chil[0]); канала */ | | close(to_par[0]); | | close(to_chil[1]); | | for (;;) | | { | | if ((count = read(0,buf,sizeof(buf))) == 0) | | exit(); | | write(1,buf,count); | | } | | } | | /* выполнение родительского процесса */ | | close(1); /* перенастройка стандартного ввода-вывода */| | dup(to_chil[1]); | | close(0); | | dup(to_par[0]); | | close(to_chil[1]); | | close(to_par[0]); | | close(to_chil[0]); | | close(to_par[1]); | | for (i = 0; i < 15; i++) | | { | | write(1,string,strlen(string)); | | read(0,buf,sizeof(buf)); | | } | | } | +------------------------------------------------------------+ Рисунок 7.5. Использование функций pipe, dup и fork 186 которых возвратила функция pipe - хорошая традиция, в чем нам еще предстоит убедиться. В результате, когда родительский процесс переписывает данные в стандартный вывод, запись ведется в канал to_chil и данные поступают к по- рожденному процессу, который считывает их через свой стандартный ввод. Когда же порожденный процесс пишет данные в стандартный вывод, запись ведется в канал to_par и данные поступают к родительскому процессу, считывающему их через свой стандартный ввод. Так через два канала оба процесса обмениваются сообщениями. Результаты этой программы не зависят от того, в какой очередности про- цессы выполняют свои действия. Таким образом, нет никакой разницы, возвраща- ется ли управление родительскому процессу из функции fork раньше или позже, чем порожденному процессу. И так же безразличен порядок, в котором процессы вызывают системные функции перед тем, как войти в свой собственный цикл, ибо они используют идентичные структуры ядра. Если процесс-потомок исполняет функцию read раньше, чем его родитель выполнит write, он будет приостановлен до тех пор, пока родительский процесс не произведет запись в канал и тем са- мым не возобновит выполнение потомка. Если родительский процесс записывает в канал до того, как его потомок приступит к чтению из канала, первый процесс не сможет в свою очередь считать данные из стандартного ввода, пока второй процесс не прочитает все из своего стандартного ввода и не произведет запись данных в стандартный вывод. С этого места порядок работы жестко фиксирован: каждый процесс завершает выполнение функций read и write и не может выпол- нить следующую операцию read до тех пор, пока другой процесс не выполнит па- ру read-write. Родитель- ский процесс после 15 итераций завершает работу; порожденный процесс натал- кивается на конец файла ("end-of-file"), поскольку канал не связан больше ни с одним из записывающих процессов, и тоже завершает работу. Если порожденный процесс попытается произвести запись в канал после завершения родительского процесса, он получит сигнал о том, что канал не связан ни с одним из процес- сов чтения. Мы упомянули о том, что хорошей традицией в программировании является закрытие ненужных файловых дескрипторов. В пользу этого говорят три довода. Во-первых, дескрипторы файлов постоянно находятся под контролем системы, ко- торая накладывает ограничение на их количество. Во-вторых, во время исполне- ния порожденного процесса присвоение дескрипторов в новом контексте сохраня- ется (в чем мы еще убедимся). Закрытие ненужных файлов до запуска процесса открывает перед программами возможность исполнения в "стерильных" условиях, свободных от любых неожиданностей, имея открытыми только файлы стандартного ввода-вывода и ошибок. Наконец, функция read для канала возвращает признак конца файла только в том случае, если канал не был открыт для записи ни од- ним из процессов. Если считывающий процесс будет держать дескриптор записи в канал открытым, он никогда не узнает, закрыл ли записывающий процесс работу на своем конце канала или нет. Вышеприведенная программа не работала бы над- лежащим образом, если бы перед входом в цикл выполнения процессом-потомком не были закрыты дескрипторы записи в канал. 7.2 СИГНАЛЫ Сигналы сообщают процессам о возникновении асинхронных событий. Посылка сигналов производится процессами - друг другу, с помощью функции kill, - или ядром. В версии V (вторая редакция) системы UNIX существуют 19 различных сигналов, которые можно классифицировать следующим образом: * Сигналы, посылаемые в случае завершения выполнения процесса, то есть тогда, когда процесс выполняет функцию exit или функцию signal с пара- метром death of child (гибель потомка); * Сигналы, посылаемые в случае возникновения вызываемых процессом особых ситуаций, таких как обращение к адресу, находящемуся за пределами вирту- 187 ального адресного пространства процесса, или попытка записи в область памяти, открытую только для чтения (например, текст программы), или по- пытка исполнения привилегированной команды, а также различные аппаратные ошибки; * Сигналы, посылаемые во время выполнения системной функции при возникно- вении неисправимых ошибок, таких как исчерпание системных ресурсов во время выполнения функции exec после освобождения исходного адресного пространства (см. раздел 7.5); * Сигналы, причиной которых служит возникновение во время выполнения сис- темной функции совершенно неожиданных ошибок, таких как обращение к не- существующей системной функции (процесс передал номер системной функции, который не соответствует ни одной из имеющихся функций), запись в канал, не связанный ни с одним из процессов чтения, а также использование недо- пустимого значения в параметре "reference" системной функции lseek. Ка- залось бы, более логично в таких случаях вместо посылки сигнала возвра- щать код ошибки, однако с практической точки зрения для аварийного за- вершения процессов, в которых возникают подобные ошибки, более предпоч- тительным является именно использование сигналов (*); * Сигналы, посылаемые процессу, который выполняется в режиме задачи, нап- ример, сигнал тревоги (alarm), посылаемый по истечении определенного пе- риода времени, или произвольные сигналы, которыми обмениваются процессы, использующие функцию kill; * Сигналы, связанные с терминальным взаимодействием, например, с "зависа- нием" терминала (когда сигнал-носитель на терминальной линии прекращает- ся по любой причине) или с нажатием клавиш "break" и "delete" на клавиа- туре терминала; * Сигналы, с помощью которых производится трассировка выполнения процесса. Условия применения сигналов каждой группы будут рассмотрены в этой и последующих главах. Концепция сигналов имеет несколько аспектов, связанных с тем, каким об- разом ядро посылает сигнал процессу, каким образом процесс обрабатывает сиг- нал и управляет реакцией на него. Посылая сигнал процессу, ядро устанавлива- ет в единицу разряд в поле сигнала записи таблицы процессов, соответствующий типу сигнала. Если процесс находится в состоянии приостанова с приоритетом, допускающим прерывания, ядро возобновит его выполнение. На этом роль отпра- вителя сигнала (процесса или ядра) исчерпывается. Процесс может запоминать сигналы различных типов, но не имеет возможности запоминать количество полу- чаемых сигналов каждого типа. Например, если процесс получает сигнал о "за- висании" или об удалении процесса из системы, он устанавливает в единицу со- ответствующие разряды в поле сигналов таблицы процессов, но не может ска- зать, сколько экземпляров сигнала каждого типа он получил. Ядро проверяет получение сигнала, когда процесс собирается перейти из режима ядра в режим задачи, а также когда он переходит в состояние приоста- нова или выходит из этого состояния с достаточно низким приоритетом планиро- вания (см. Рисунок 7.6). Ядро обрабатывает сигналы только тогда, когда про- цесс возвращается из режима ядра в режим задачи. Таким образом, сигнал не оказывает немедленного воздействия на поведение процесса, исполняемого в ре- жиме ядра. Если процесс исполняется в режиме задачи, а ядро тем временем об- рабатывает прерывание, послужившее поводом для посылки процессу сигнала, яд- ро распознает и обработает сигнал по выходе из прерывания. Таким образом, процесс не будет исполняться в режиме задачи, пока какие-то сигналы остаются необработанными. На Рисунке 7.7 представлен алгоритм, с помощью которого ядро определяет, --------------------------------------- (*) Использование сигналов в некоторых обстоятельствах позволяет обнаружить ошибки при выполнении программ, не проверяющих код завершения вызываемых системных функций (сообщил Д.Ричи). 188 Выполняется в режиме задачи +-------+ | | Проверка | 1 | и Вызов функ- | | + - обработка ции, преры- ++------+ -+ - сигналов вание | ^ ^- -+- Преры- +-----+ +-------+ |- -|- - + вание, | | | +-------+ +---+ Возврат в возврат| | | | Возврат | режим задачи из пре-| | | | | рыва-| v v | Выполняет- | +-------+ ния | +------++ся в режи- ++------+ | | +-->| |ме ядра | | | 9 |<-----------+ 2 +------------>| 7 | | | Выход | | Резервирует-| | +-------+ ++------+ ся +-------+ Прекращение | ^ - Зарезер- существования |- - -|- - - - - - - - + - вирован | |- - - - - - - + + -- -+ +---------------+ +------+ -------- Проверка | Приостанов Запуск | - + - - - сигналов v | - При-+-------+ +-+-----+ Готов к ос- | | Возобновление | | з