На количество одновременно выполняющихся процессов накладывается ограничение (конфигурируемое), отсюда ни один из пользователей не может занимать в таблице процессов слишком много места, мешая тем самым другим пользователям создавать новые процессы. Кроме того, простым пользователям не разрешается создавать процесс, занимающий последнее свободное место в таблице процессов, в противном случае система зашла бы в тупик. Другими словами, поскольку в таблице процессов нет свободного места, то ядро не может гарантировать, что все существующие процессы завершатся естественным образом, поэтому новые процессы создаваться не будут. С другой стороны, суперпользователю нужно дать возможность исполнять столько процессов, сколько ему потребуется, конечно, учитывая размер таблицы процессов, при этом процесс, исполняемый суперпользователем, может занять в таблице и последнее свободное место. Предполагается, что суперпользователь может прибегать к решительным мерам и запускать процесс, побуждающий остальные процессы к завершению, если это вызывается необходимостью (см. раздел 7.2.3, где говорится о системной функции kill).
алгоритм fork
входная информация: отсутствует
выходная информация:
для родительского процесса — идентификатор (PID) порожденного процесса
для порожденного процесса — 0
{
проверить доступность ресурсов ядра;
получить свободное место в таблице процессов и уникальный код идентификации (PID);
проверить, не запустил ли пользователь слишком много процессов;
сделать пометку о том, что порождаемый процесс находится в состоянии «создания»;
скопировать информацию в таблице процессов из записи, соответствующей родительскому процессу, в запись, соответствующую порожденному процессу;
увеличить значения счетчиков ссылок на текущий каталог и на корневой каталог (если он был изменен);
увеличить значение счетчика открытий файла в таблице файлов;
сделать копию контекста родительского процесса (адресное пространство, команды, данные, стек) в памяти;
поместить в стек фиктивный уровень системного контекста над уровнем системного контекста, соответствующим порожденному процессу;
/* фиктивный контекстный уровень содержит информацию, необходимую порожденному процессу для того, чтобы знать все о себе и будучи выбранным для исполнения запускаться с этого места; */
if (в данный момент выполняется родительский процесс)
{
перевести порожденный процесс в состояние «готовности к выполнению»;
return (идентификатор порожденного процесса);
/* из системы пользователю */
}
else { /* выполняется порожденный процесс */
записать начальные значения в поля синхронизации адресного пространства процесса;
return (0); /* пользователю */
}
}
Рисунок 7.2. Алгоритм fork
Затем ядро присваивает начальные значения различным полям записи таблицы процессов, соответствующей порожденному процессу, копируя в них значения полей из записи родительского процесса. Например, порожденный процесс «наследует» у родительского процесса коды идентификации пользователя (реальный и тот, под которым исполняется процесс), группу процессов, управляемую родительским процессом, а также значение, заданное родительским процессом в функции nice и используемое при вычислении приоритета планирования. В следующих разделах мы поговорим о назначении этих полей. Ядро передает значение поля идентификатора родительского процесса в запись порожденного, включая последний в древовидную структуру процессов, и присваивает начальные значения различным параметрам планирования, таким как приоритет планирования, использование ресурсов центрального процессора и другие значения полей синхронизации. Начальным состоянием процесса является состояние «создания» (см. Рисунок 6.1).
После того ядро устанавливает значения счетчиков ссылок на файлы, с которыми автоматически связывается порождаемый процесс. Во-первых, порожденный процесс размещается в текущем каталоге родительского процесса. Число процессов, обращающихся в данный момент к каталогу, увеличивается на 1 и, соответственно, увеличивается значение счетчика ссылок на его индекс. Во-вторых, если родительский процесс или один из его предков уже выполнял смену корневого каталога с помощью функции chroot, порожденный процесс наследует и новый корень с соответствующим увеличением значения счетчика ссылок на индекс корня. Наконец, ядро просматривает таблицу пользовательских дескрипторов для родительского процесса в поисках открытых файлов, известных процессу, и увеличивает значение счетчика ссылок, ассоциированного с каждым из открытых файлов, в глобальной таблице файлов. Порожденный процесс не просто наследует права доступа к открытым файлам, но и разделяет доступ к файлам с родительским процессом, так как оба процесса обращаются в таблице файлов к одним и тем же записям. Действие fork в отношении открытых файлов подобно действию алгоритма dup: новая запись в таблице пользовательских дескрипторов файла указывает на запись в глобальной таблице файлов, соответствующую открытому файлу. Для dup, однако, записи в таблице пользовательских дескрипторов файла относятся к одному процессу; для fork — к разным процессам.
После завершения всех этих действий ядро готово к созданию для порожденного процесса пользовательского контекста. Ядро выделяет память для адресного пространства процесса, его областей и таблиц страниц, создает с помощью алгоритма dupreg копии всех областей родительского процесса и присоединяет с помощью алгоритма attachreg каждую область к порожденному процессу. В системе с подкачкой процессов ядро копирует содержимое областей, не являющихся областями разделяемой памяти, в новую зону оперативной памяти. Вспомним из раздела 6.2.4 о том, что в пространстве процесса хранится указатель на соответствующую запись в таблице процессов. За исключением этого поля, во всем остальном содержимое адресного пространства порожденного процесса в начале совпадает с содержимым пространства родительского процесса, но может расходиться после завершения алгоритма fork. Родительский процесс, например, после выполнения fork может открыть новый файл, к которому порожденный процесс уже не получит доступ автоматически.
Итак, ядро завершило создание статической части контекста порожденного процесса; теперь оно приступает к созданию динамической части. Ядро копирует в нее первый контекстный уровень родительского процесса, включающий в себя сохраненный регистровый контекст задачи и стек ядра в момент вызова функции fork. Если в данной реализации стек ядра является частью пространства процесса, ядро в момент создания пространства порожденного процесса автоматически создает и системный стек для него. В противном случае родительскому процессу придется скопировать в пространство памяти, ассоциированное с порожденным процессом, свой системный стек. В любом случае стек ядра для порожденного процесса совпадает с системным стеком его родителя. Далее ядро создает для порожденного процесса фиктивный контекстный уровень (2), в котором содержится сохраненный регистровый контекст из первого контекстного уровня. Значения счетчика команд (регистр PC) и других регистров, сохраняемые в регистровом контексте, устанавливаются таким образом, чтобы с их помощью можно было «восстанавливать» контекст порожденного процесса, пусть даже последний еще ни разу не исполнялся, и чтобы этот процесс при запуске всегда помнил о том, что он порожденный. Например, если программа ядра проверяет значение, хранящееся в регистре 0, для того, чтобы выяснить, является ли данный процесс родительским или же порожденным, то это значение переписывается в регистровый контекст порожденного процесса, сохраненный в составе первого уровня. Механизм сохранения используется тот же, что и при переключении контекста (см. предыдущую главу).