Skip Return Title Contents

ОРГАНИЗАЦИЯ ПРОЦЕССОВ OS UNIX

Понятия ядра и процесса

Резидентная в RAM часть OS UNIX называется ядром. Программа ядра оформлена как выполняемый файл, который хранится в корневом каталоге корневой файловой системы OS UNIX и имеет имя, однозначно выделяющее его среди других файлов. Ядро считывается в RAM программой начальной загрузки, начиная с нулевого адреса. После загрузки ядро постоянно находится в RAM, обеспечивая управление подсистемами OS UNIX посредством соответствующих ядерных подпрограмм, системных таблиц и буферов.

Все работы вне ядра оформлены в виде процессов выполнения системных и прикладных программ. Под процессом понимается единица вычислительной работы, потребляющая ресурсы, предоставляемые ядром, для обработки системных и прикладных программ, которые оформлены как командные или выполняемые файлы на внешнем устройстве. Процесс и выполняемая им программа - принципиально различные категории. Программа - статическое понятие, процесс - динамическая категория, отражающая ход выполнения программы. Это различие принципиально, т.к. в ходе выполнения одного и того же процесса может неоднократно происходить замена программы.

Процессы могут взаимодействовать между собой посредством программных каналов или сигналов и обращаться к подпрограммам ядра через системные вызовы. Ядро поддерживает иерархию процессов, предоставляет необходимое адресное пространство для их выполнения, обеспечивает диспечеризацию и внутреннюю синхронизацию процессов, осуществляет переключение фазы выполнения процесса, реализует механизм своппинга и пейджинга процессов.



Адресное пространство процесса и ядра

Каждый процесс выполняется в собственном виртуальном пространстве адресов. Размер виртуального адресного пространства фиксирован, но может отличаться в реализациях OS UNIX на различных платформах. Совокупность участков RAM, отображаемых виртуальными адресами составляет образ процесса.

Образ процесса составляют следующие сегменты: процедурный сегмент (text), сегмент инициализированных данных (data), сегмент неинициализированных данных (bss) и стек (stack). Расположение сегментов в виртуальном адресном пространстве процесса иллюстрирует следующий рисунок.


stack Свободные
виртуальные
адреса
basic
static
storage
data text

Рис. Структура виртуального адресного пространства процесса

Расширение ->
стека
    <- Расширение сегментов данных 0

Начальные размеры сегментов образа процесса определяет заголовок файла программы процесса. Сегменты text и data загружаются из файла программы процесса. Сегменты bss и stack создаются в образе процесса и не занимают места в файле программы процесса, так как, по умолчанию, начальные значения составляющих их данных равны 0.

Сегмент text
содержит машинные инструкции и константы. Его содержание не изменяется при выполнения процесса.
Сегмент data
содержит внешние и статические переменные, инициализированные при компиляции программы процесса.
Сегмент bss
содержит внешние и статические переменные, неинициализированные при компиляции программы процесса.
Сегмент stack
используется для хранения значений автоматических переменных и параметров функций, а также для организации рекурсивных вызовов функций.

Между нижней границей стека и сегментов данных обычно существует адресный зазор, который может быть использован для расширения стека и сегментов данных при выполнении процесса, пока не исчерпано пространство свободных адресов. Изменение размера сегментов данных реализуется системными вызовами динамического распределения памяти brk и sbrk.

Хотя ядро не является самостоятельной вычислительной единицей, однако, программа ядра также как программы процессов хранится в выполняемом файле формата a.out. Резидентный образ ядра располагается в RAM, начиная с нулевого адреса, и состоит из набора сегментов, который идентичен образу процесса. В частности, процедурный сегмент ядра составляют коды подпрограмм управления процессами, файлами и вводом-выводом, а также драйверы периферийных устройств. Сегмент данных ядра составляют системные таблицы и переменные, используемые подпрограммами процедурного сегмента, а также область отображения контекста текущего процесса. Стек ядра используется для размещения параметров ядерных функций и системных вызовов. Образу ядра соответствует виртуальное адресное пространство, которое по структуре аналогично виртуальному адресному пространству процесса.

Образы процессов и ядра не пересекаются в RAM. Также не пересекаются образы процессов, выполняющих различные программы. Образы процессов, которые выполняют одинаковую программу, могут разделять, т.е. совместно использовать, общий процедурный сегмент. Является процедурный сегмент разделяемым или нет определяет магическое число в заголовке файла программы процесса, которое может принимать одно из 3-х значений: 0407, 0410 и 0411 (0413).

Если магическое число в заголовке файла программы процесса равно 0407, то процедурный сегмент не разделяется процессами, которые одновременно выполняют данную программу, хотя они имеют абсолютно идентичные процедурные сегменты. Если магическое число процесса равно 0410, то процедурный сегмент разделяется всеми процессами, которые выполняют эту программу. Разделяемый процедурный сегмент часто называют сегментом чистого кода, а программу, которая его содержит - повторно входимой или реентерабельной. Процедурный сегмент реентерабельной программы может быть повторно выполнен любыми процессами без повторной загрузки в RAM. Если магическое число в файле программы процесса равно 0411 (0413), то процедурный сегмент также является разделяемым, но располагается в отдельном виртуальном адресном пространстве. Использование разделяемых процедурных сегментов позволяет эффективно экономить ресурсы RAM при обработке идентичных программ.



Фазы выполнения процесса

В каждый момент выполнения процесс может находиться фазе "пользователь" или в фазе "система". В фазе "пользователь" выполняются инструкции из адресного пространства процесса, в фазе "система" - инструкции ядра. Текущая фаза отслеживается в регистре PS. Выполнение процесса в фазе "пользователь" может быть прервано для перехода в фазу "система" по одной из следующих 3-х причин:

После отработки любой из указанных причин прерывания следует возврат в фазу "пользователь".

Прерывание системным вызовом сознательно инициируется процессом в фазе "пользователь" для обращения к необходимым подпрограммам из адресного пространства ядра. Внутренние прерывания (ловушки) связаны с обработкой подпрограммами ядра аварийных ситуаций, которые возникают синхронно при нарушении границ сегментов в адресном пространстве процесса, при ошибке косвенной адресации в программе процесса, при десятичном переполнении или при делении на ноль и при переполнении стека. Результатом обработки является посылка процессу соответствующего сигнала, который стандартно вызывает аварийное завершение процесса с фиксацией дампа памяти в файле core. Внешние прерывания связаны с активизацией работы периферийных устройств, контроллеры которых имеют более высокий уровень приоритета, чем процессор, выполняющий инструкции программы процесса в фазе "пользователь". Внешние прерывания возникают вне зависимости от хода выполнения программы процесса, т.е. асинхронно. При этом, управление передается соответствующей подпрограмме обработки внешнего прерывания из адресного пространства ядра. Во всех перечисленых случаях, процессор переключается на выполнение подпрограмм из адресного пространства ядра, которое доступно процессу только в фазе "система". При переходе процесса в фазу "система" из фазы "пользователь" выполняются следующие действия.


Контекст процесса

Системные данные, используемые при выполнении процесса (когда его образ загружен в RAM), составляют контекст процесса. Контекст процесса формально описан структурой struct user в файле /usr/include/sys/user.h. Эта структура включает следующие основные поля:

Область контекста процесса имеет фиксированный размер и не входит в виртуальное адресное пространство процесса. В пространстве физических адресов контекст процесса располагается перед процедурным сегментом или перед сегментом данных в случае разделяемого процедурного сегмента.

Контекст текущего процесса отображается в область текущего процесса виртуального адресного пространства ядра. Адрес этой области сохраняет системная переменная u, через которую поля контекста текущего процесса доступны доступны подпрограммам ядра. Доступ к полям контекста процесса на уровне пользователя реализован через системные вызовы ядра, например, chdir, chroot, signal, ulimit.



Идентификатор процесса

Каждый процесс имеет уникальный целочисленный номер, который называется идентификатором процесса. Он позволяет однозначно указать нужный процесс в командах пользователя или системных вызовах. Идентификаторы процессов сохраняют свою уникальность от момента образования процесса до очередной перезагрузки OS UNIX. Уникальность идентификаторов гарантируется ядром.

Идентификаторы процессов динамически назначаются ядром из диапазона от 1 до 30000 в порядке образования процессов. Идентификатор процесса никак не связан с именем файла программы его выполнения. Процессы не именют предопределенных идентификаторов. Исключение составляют диспетчерский процесс (swapper) с идентификатором 0 и процесс инициализации (init) с идентификатором 1, которые образуются первыми при загрузке OS UNIX. Процесс имеет возможность узнать свой идентификатор системным вызовом getpid и идентификатор процесса-предка - системным вызовом getppid.

Кроме индивидуальной идентификации процессов в OS UNIX предусмотрена идентификация групп процессов. Группу процессов образует множество процессов, запущенных с одного терминала в течении одного сеанса. Все процессы группы имеют одинаковый идентификатор, который совпадает с идентификатором лидера группы. Лидером группы обычно является процесс интепретатора команд, который открывает сеанс работы с терминалом - так называемый login shell. Идентификатор группы процесса позволяет узнать системный вызов getpgrp.

Личные и групповые идентификаторы процессов могут быть использованы для персонифицированной и распределенной посылки сигналов системным вызовом kill или для порождения уникальных имен временных файлов.



Дескриптор процесса

Системные данные, используемые ядром в течении времени жизни процесса, составляют дескриптор процесса. Дескриптор процесса резервируется ядром при образовании процесса и освобождается при его завершении. Дескриптор процесса формально описан структурой struct proc в заголовочном файле /usr/include/sys/proc.h. Основные поля структуры struct proc могут быть классифицированы по характеру данных следующим образом.

Множество дескрипторов процессов объединяет таблица процессов. Таблица процессов расположена в сегменте данных ядра по виртуальному адресу, заданному системной переменной practive. Она содержит адрес первого дескриптора в таблице процессов. Остальные дескрипторы в таблице процессов связаны в линейный список ссылочными полями p_next структуры struct proc. Списковая организация таблицы процессов позволяет гибко манипулировать ее элементами без изменения их адресов, а именно, удалять из таблицы дескриптор завершившегося процесса и добавлять в таблицу дескриптор нового процесса, а также обеспечивает возможность просмотра таблицы для поиска нужного элемента. Соответствие дескрипторов таблицы процессов конкретным процессам обеспечивают поля p_pid, содержащие идентификаторы процессов.

Постоянство адресов дескрипторов в таблице процессов используется для реализации прямого доступа к любому из них через поле u_procp структуры struct user контекста процесса. Для текущего процесса, контекст которого отображен в адресное пространство ядра, поле u_procp доступно через системную переменную "u". Например, доступ к полю приоритета p_pri дескриптора текущего процесса может быть реализован такой конструкцией на C-коде: u->u_procp->p_pri. Обратную связь с контекстом процесса обеспечивает поле p_ubptbl дескриптора процесса. В более общем случае дескриптор любого процесса может быть найден путем последовательного просмотра таблицы процессов, начиная с адреса practive через ссылочные поля p_next, с использованием в качестве ключа поиска идентификатора процесса в поле p_pid.

Непосредственный доступ к полям структуры struct proc дескриптора процесса возможен только в фазе "система". Oн может быть реализован системными вызовами, такими как nice, getpid, getppid, getpgrp, getpuid, getpeuid, getgid, getegid, а также ядерными функциями, которые реализуют механизмы синхронизации, диспечеризации, управления памятью и иерархией процессов. Информацию о текущем состоянии таблицы процессов позволяет получить команда ps в длинном формате (с ключом -l).

Максимальный размер таблицы процессов задается при генерации OS UNIX. Это означает, что при работе OS UNIX может быть выделено фиксированное число дескрипторов, которое ограничивает число существующих процессов в каждый момент времени. Когда предельный размер таблицы процессов будет исчерпан, образование нового процесса не будет иметь успеха и станет возможным только после завершения каких-либо процессов и освобождения некоторого числа дескрипторов в таблице процессов, соответственно.



Управление иерархией процессов

Множество процессов, существующих в каждый момент, образуют единую иерархическую структуру, где они связаны отношениями потомок-предок. Каждый новый процесс может быть образован (порожден) только одним из существующих процессов, который становится его предком. По отношению к предку порожденый процесс будет считаться потомком. Иного способа образования новых процессов и иных отношений между процессами в OS UNIX не предусмотрено. Следует отметить, что любой процесс-предок может иметь более одного потомка.

При порождении нового процесса его образ строится путем копирования образа предка в свободное постранство адресов RAM. Конкретно, копируются стек и сегменты данных. Процедурный сегмент копируется, когда он не является разделяемым сегментом, который уже загружен в RAM. Кроме того, потомок наследует контекст предка. В частности, через контекст потомку передаются таблицы файлов и сигналов, а также текущий каталог предка. Таким образом, после порождения потомок является точной копией предка. В этот момент они различаются только личными идентификаторами и идентификаторами предков в таблице процессов. В дальнейшем тождество предка и потомка нарушается за счет индивидуального изменения их сегментов и контекстов. Например, процесс-потомок может распределять дополнительную память, расширяя сегмент данных, изменять содержание сегмента данных путем обработки его переменных, модифицировать таблицу открытых файлов в своем контексте, открывая и закрывая файлы, переопределить обработку сигналов в таблице сигналов своего контекста.

Для порождения потомка процесс-предок использует системный вызов fork, который возвращает идентификатор потомка предку и 0 - потомку. Такой алгоритм возврата позволяет легко различать инструкции предка и потомка в тексте программы процесса. При аварийном завершении fork возвращает отрицательный код, например, когда превышено ограничение по общему числу процессов или числу процессов одного пользователя. Следующий фрагмент С-кода демонстрирует применение системного вызова fork для порождения нового процесса.


int pid; /* идентификатор процесса-потомка */

/* инструкции процесса-предка до развилки */

...........................................

/* развилка процессов */

switch(pid= fork()) {
case -1:/* обработка ошибки порождения процесса */
 .........................................
 break;
case  0:/* инструкции процесса-потомка */
 .........................................
 break;
default:/* инструкции процесса-предка после развилки */
 .........................................
 break;
} /* switch */

При успешном завершении системного вызова fork процессы потомка и предка равноправно существуют в системе. Они могут функционировать параллельно, конкурируя за ресурсы на основе своих приоритетов, или выполнение предка может быть приостановлено до завершения потомка системным вызовом wait. Системный вызов wait возвращает предку идентификатор потомка, который завершился первым после последнего обращения к wait. Если предок имеет несколько потомком, то чтобы узнать о завершении каждого из них, нужно реализовать соответствующее число системных вызовов wait с проверкой их возвратов. Когда процесс не имеет потомков, wait возвращает код (-1). Следующий пример С-кода иллюстрирует применение системного вызова wait при ожидании завершения 2-х потомков.

int pid1, pid2; /* идентификаторы процессов-потомков */
int status;/* статус завершения процесса-потомка */
int ret = 0;/* текущий возврат системного вызова wait */

/* инструкции порождения процессов-потомков */

..............................................

/* цикл ожидания звершения потомков в процессе-предке */

 
while((ret = wait(&status)) != (-1)) {
  
/* обработка завершения 1-го потомка */
  
if(ret == pid1)
  
 .....................................
  
/* обработка завершения 2-го потомка */
  
if(ret == pid2)
  
 .....................................
  
} /*while */

Каждый процесс процесс может завершиться по сигналу, который генерирует другой процесс, ядро или аппаратура, а также по собственной инициативе, используя системный вызов exit. Системный вызов exit освобождает память, динамически распределенную процессом, закрывает открытые процессом файлы и осуществляет очистку буферов системного пула, которые использованны для ввода-вывода в эти файлы. Статус завершения процесса может быть передан exit как целочисленный аргумент и будет известен потомку, если последний выполняет системный вызов wait.

Для анализа причины завершения потомка системный вызов wait обеспечивает возврат слова статуса завершения потомка. Адрес слова статуса завершения потомка передается в wait как параметр. Байт слова статуса с маской FF00 содержит пользовательский код завершения потомка, объявленный им через аргумент системного вызова exit. Байт слова статуса с маской 00FF, если он не равен 0, содержит системный код завершения потомка по сигналу. Его младшие 7 бит сохраняют номер сигнала, по которому завершился потомок. Ненулевой старший бит системного кода завершения индицирует образование файла core, содержащего дамп памяти на момент завершения потомка.

Следует отметить, что в OS UNIX приняты определенные соглашения, которые позволяют поддерживать иерархию процессов в нестандартных ситуациях. Две такие ситуации являются наиболее распространеннными.

Процесс-предок завершается раньше своих потомков. В этом случае его потомки автоматически становятся потомками процесса инициализации init с идентификатором 1.

Процесс-потомок завершается раньше 1-го предполагаемого обращения предка к системному вызову wait. Чтобы такая ситуация не стала тупиковой из-за бесконечного ожидания предком завершения уже несуществующего потомка введено состояние промежуточного завершения или состояние "зомби". В состоянии "зомби" процесс не имеет образа в RAM, но информация о нем сохраняется в таблице процессов для корректной реализации предком системного вызова wait.

Дополнительно к рассмотренным средствам управления иерархией процессов, OS UNIX предоставляет возможность замены программы выполнения процесса с помощью системных вызовов execl, execv, execle и execve. При реализации системных вызовов семейства exec образ процесса заменяется сегментами специфицированного файла новой программы его выполнения. При этом новый процесс не создается, управление новому образу передается в рамках существующего процесса с тем же идентификатором. При этом контекст процесса сохраняется без изменений, исключая таблицу сигналов, в которой все установки нестандартной обработки сигналов будут игнорированы. При корректном завершении любого системного вызова семейства exec возврат в старый образ невозможен. Поэтому обычно они применяются в сочетании с порождением промежуточного потомка системным вызовом fork, исключительно для последующей замены программы его выполнения. Комбинация fork-exec позволяет продолжить старую программу выполнения предка после завершения потомка по новой программе, заданной системным вызовом семейства exec. Следующий фрагмент С-кода демонстрирует наиболее распространенную схему применения системного вызова execl из семейства exec для замены текущей программы выполнение процесса на программу обработки команды ls в длинном формате вывода (с ключем -l).

int pid;  /* идентификатор процесса-потомка */
int status = 0;  /* статус завершения процесса-потомка */
 
/* развилка процессов */
 
switch(pid  = fork()) {
case -1:  /* ошибка порождения процесса-потомка */
 break;
case  0: /* замена программы выполнения потомка */
 execl("/bin/ls", "ls", "-l", (char *) 0);
 exit(1);
default/* ожидание предком завершения потомка */
 while(wait(&status) != pid);
 break;
} /* switch */

Рассмотренные средства управления иерархией процессов используются при инициализации OS UNIX и для интерпретации команд в сеансах работы пользователей в монопольном или многопользовательском режимах.

 

Внутренняя синхронизация процессов

Внутренняя синхронизация процессов основана на использовании аппарата событий для изменения состояний процессов в фазе "система". Состояние процесса отражает поле p_stat структуры struct proc дескритора процесса. Процесс может находиться в одном из следующих состояний в соответствии со значением поля p_stat:

SONPROC  - процесс выполняется, т.е. его инструкции обрабатываются процессором;
SRUN - процесс готов к выполнению, но не обладает приоритетом, достаночным для использования ресурсов процессора;
SSLEEP - процесс ожидает некоторое событие, до наступления которого он не претендует на ресурсы процессора;
SZOMB - состояние промежуточного завершения, когда процесс не имеет образа в RAM, но сохраняет дескриптор в таблице процессов.

Взаимосвязь состояний процесса средствами внутренней синхронизации иллюстрирует следующая схема.


Рис. Схема внутренней синхронизации процессов
 
 



 SRUN 



 
 

switch in
 



 SONPROC 



 
 
  ================================>  
 pswtch 
 

 SZOMB 

 
fork <================================ exit
========> switch out =======>
    
  <==========
 

 SSLEEP 


<==========  
  wakeup sleep  


В каждый момент времени только один процесс может являться текущим, т.е. использовать ресурсы процессора и находиться в состоянии SONPROC. Другие процессы, обработка которых не блокирована ожиданием событий, находятся в состоянии SRUN. Их дескрипторы образуют очередь дипетчера dispq и связаны в таблице процессов посредством ссылочных полей p_link структуры struct proc. Механизм синхронизации обеспечивает переключение текущего процесса в очередь диспетчера (switch out), когда он уступает процессор другому процессу, который, соответственно, включается (switch in) на выполнение. Переключение процессов между состояниями SRUN и SONPROC обеспечивает системная функция pswtch на основе сравнения приоритетов. Процесс остается текущим пока его приоритет выше, чем у любого процесса в очереди диспетчера. Планирование приоритетов процессов, претендующих на ресурсы процессора, обеспечивает механизм диспечеризации процессов.

Для управления переключением процессов ядро использует системные флаги вытеснения runrun и kprunrun. Флаг runrun уведомляет системную функцию pswtch, что она должна быть вызвана как только переключаемый процесс перейдет в фазу "пользователь". Блокировка переключения в фазе "система" необходима для гарантии целостности системных данных. Флаг kprunrun позволяет реализовать переключение в фазе "система", когда код текущего процесса достигает достигает некоторых разрешенных точек вытеснения в адресном пространстве ядра.

Обычно при работе OS UNIX число "спящих" процессов в состоянии SSLEEP превышает число готовых к выполнению процессов в состоянии SRUN. Это объяняется наличием большого числа событий в реальной практике работы OS UNIX. Наиболее распространенная причина погружения процесса в "сон" связана с отработкой внешних прерываний от периферийных устройств. В частности, выполнение операций ввода-вывода не может быть реализовано параллельно с обработкой процессором кода программы процесса. Более высокий уровень приоритета конроллеров периферийных устройств, чем у процессора при выполнении программы процесса, заставляет последний переключаться на обработку внешних прерываний, переводя текущий процесс в состояние "сна" до завершения отработки прерывания. Поэтому увеличение числа "спящих" процессов характерно при интенсивном обмене с периферией, когда большое число процессов ожидает в состоянии SSLEEP наступления событий, связанных с завершением операций ввода-вывода, чтобы продолжить процессорную обработку своих данных. Кроме приоритетной обработки внешних прерываний события могут быть вызваны программными причинами. Например, выполнение процесса-предка может быть приостановлено системным вызовом wait до завершения процесса-потомка. В другом случае процесс "засыпает" при попытке чтения из пустого struct proc дескриптора процесса, когда он находится в состоянии SSLEEP. Более конкретно, идентификатор события есть адрес определенной системной таблицы или элемента таблицы, связанной с ожидаемым ресурсом. Например, если процесс-предок ожидает завершения потомка, то идентификатор события, с которым будет связано его пробуждение, есть адрес дескриптора процесса-потомка.

Для погружения процесса в состояние "сна" используется системная функция sleep, которой в качестве аргументов передаются идентификатор события и приоритет процесса после пробуждения. После выполнения функции sleep поле состояния дескриптора текущего процесса принимает значение SSLEEP, в поле p_wchan заносится идентификатор события, в поле приоритета p_pri фиксируется величина приоритета после пробуждения, а сам дескриптор включается в очередь дескрипторов "спящих" процессов sleepq. Очередь "спящих" процессов sleepq имеет организацию, аналогичную очереди диспетчера dispq. При погружении в "сон" текущего процесса процессор переключается на обслуживание самого приоритетного процесса из очереди диспетчера или обработку внешнего прерывания.

Для пробуждения процесса из состояния "сна", при наступления ожидаемого события, выполняется системная функция wakeup, аргументом которой является идентификатор объявленного события. При вызове функции wakeup все дескрипторы очереди "спящих" процессов sleepq, у которых значение поля p_wchan совпадает с аргументом функции wakeup, перемещаются в очередь диспетчера dispq для готовых к выполнению процессов. Если приоритет "разбуженного" процесса выше, чем у текущего, то он включается на обслуживание процессором по алгоритму, рассмотренному выше. Если в очереди "спящих" процессов нет процессов с идентификатором события, объявленным функцией wakeup, то "пробуждения" процессов не происходит.

Последнее из состояний процесса, достижимых внутренней синхронизацией, есть состояние промежуточного завершения текущего процесса - SZOMB (состояние "зомби"). Состояние "зомби" имеет место, если процесс-потомок завершается по системному вызову exit или по сигналу до планируемой реализации системного вызова wait в процессе-предке. При этом образ завершившегося процесса освобождает адресное пространство, но его дескриптор временно сохраняется в таблице процессов, чтобы обеспечить корректную обработку системного вызова wait в процессе-предке.

В заключении следует отметить, что рассмотренный механизм внутренней синхронизации обслуживает процессы в фазе "система". Для синхронизации процессов в фазе "пользователь" используется аппарат сигналов OS UNIX.



Диспечеризация процессов

Диспечеризация процессов в OS UNIX реализует политику разделения времени процессора среди всех процессов, функционирующих в системе, на основе их приоритетов. Приоритеты процессов выражаются целыми числами. Их значения сохраняются в поле p_pri структуры struct proc дескриптора процесса. Во всех версиях OS UNIX, кроме System V Release 4 (SVR4), принято, что приоритет процесса тем выше, чем меньше его численное значение (в SVR4 - наоборот). Процесс выбирается для обработки процессором из очереди диспетчера dispq на основе их сравнения приоритетов. Методы назначения приоритета различны в фазе "система" и в фазе "пользователь".

В фазе "система" приоритет процесса задается системной функцией sleep, которая вызывается для погружении процесса в состояние ожидания события при внутренней синхронизации процессов. При "пробуждении" процесса после наступления ожидаемого события системной функцией wakeup, процесс сохраняет установленный приоритет, пока он находится в фазе "система". Величина приоритета устанавливается в зависимости от события, ожидаемого процессом при погружении в состояния "сна", по глобальным параметрам диспетчера разделения времени как показано в следующей таблице.


Таблица системных приоритетов

Глобальный параметр

Системный приоритет Событие, ожидаемое процессом
PSWP0Своппинг
PRIBIO20Блоко-ориенированный ввод-вывод
PZERO25Уровень пробуждения сигналом
PPIPE26Пустой/полный программный канал
TTIPRI28Ввод с терминала
TTOPRI29Вывод на терминал
PWAIT30Вывод на терминал
PSLEEP39Пауза в ожидании сигнала


Глобальные параметры, определяющие значения системных приоритетов, задаются при генерации OS UNIX и подобраны таким образом, чтобы поддерживать равновесие в режиме разделения времени процессора. Глобальный параметр PZERO определяет границу между низкими и высокими приоритетами в фазе "система". Системный приоритет со значением больше PZERO считается низким, не превышающий PZERO - высоким. Процессы, погруженные в состояние "сна" с высоким приоритетом, не могут быть выведены из этого состояния каким-либо сигналом. Процессы в состоянии "сна" с низким приоритетом могут быть "разбужены" сигналом. Текущий и готовые к выполнению процессы сохраняют фиксированный приоритет, пока они находятся в фазе "система". После перехода в фазу "пользователь" приоритет процесса будет переопределен.

В фазе "пользователь" приоритет процесса имеет 2 составляющие - пользовательскую и системную. Значения этих составляющих задают поля дескриптора процесса p_nice и p_cpu, соответственно. Полное значение приоритета в фазе "пользователь" устанавливается на основе определенной комбинации его составляющих в полях p_nice и p_cpu.

Начальное значение пользовательской составляющей приоритета определяется константой NZERO, обычно равной 20. Пользовательская составляющая может быть изменена системным вызовом nice. Его аргумент определяет величину модификации поля p_nice и может задаваться в пределах от 0 до NZERO для непривилигированного процесса или в пределах от (-NZERO) до (+NZERO) для привилигированного процесса. Поскольку значение пользовательской составляющей приоритета наследуется при порождении процесса, то непривилигированный процесс может только уменьшить приоритет, полученный от предка. Привилигированный потомок может как уменьшить, так и увеличить свой приоритет относительно предка.

Начальное значение системной составляющей приоритета (p_cpu) в фазе "пользователь" равно 0. Ее изменение зависит от времени использования процессора, т.е. времени, пока процесс остается текущим, работая в фазе "пользователь". Для формирования системной составляющей приоритета используются прерывания от аппаратного таймера. При частоте сети питания 50 Гц прерывания от таймера генерируются 50 раз в секунду. Каждое таймерное прерывание увеличивает значение поля p_cpu, т.е. уменьшает системную составляющую приоритета на 1.

Результирующий приоритет процесса в фазе "пользователь" определяется по формуле:

p_pri = (p_nice - NZERO) + (p_cpu/16) + PUSER

Разность (p_nice - NZERO) учитывает модификацию пользовательской составляющей приоритета системным вызовом nice. Отношение (p_cpu/16) учитывает усредненное значение системной составляющей приоритета. Усреднение необходимо, так как неизвестно, какую часть таймерного интервала проработал процесс на момент очередного прерывания от таймера. Кроме того, нежелательно частое переключение процессора между процессами с близкими значениями приоритетов. В частности, для равноприоритетных процессов в фазе "пользователь" выбранная величина усреднения (1/16) гарантирует переключение процессора не ранее, чем через 16 таймерных интервалов (320 мс), когда отношение (p_cpu/16) увеличится на 1. Константа PUSER, по умолчанию равная 50, вводится для принудительного увеличения приоритета в фазе "пользователь" по отношению к приоритету в фазе "система". Это сделано с целью стимулировать переключение процессора на выполнение процессов в фазе "система", в частности, для ускорения доступа "разбуженных" процессов к ресурсам процессора.

Для вычисления приоритета используется системная функция setpri, которой в качестве аргумента передается дескриптор процесса. Приоритет процесса, вычисленный функцией setpri, сравнивается с приоритетом текущего процесса, который сохраняет системная переменная curpri. Если приоритет какого-либо процесса из очереди диспетчера выше приоритета текущего, реализуется переключение процессора на более приоритетный процесс по схеме внутренней синхронизации процессов.



Своппинг и пейджинг процессов

Ограниченный объем RAM в общем случае не позволяет разместить в ней множество образов существующих процессов. Поэтому образы пассивных процессов, для размещения которых нет места в RAM, располагаются в специально отведенной области внешней памяти, называемой областью своппинга. Образы активных процессов располагаются в RAM. Перекачку (своппинг) образов процессов между оперативной и внешней памятью, обеспечивает диспетчерский процесс (планировщик своппинга) swapper (или sched для SUN OS) с идентификатором 0. Он осуществляет загрузку в RAM образов активных процессов и выгрузку в область своппинга образов пассивных процессов. Своппинг происходит при практической реализации механизмов синхронизации, диспечеризации и управлении иерархией процессов. Например, когда недостаточно места для размещения в RAM образа порожденного процесса-потомка, образ его предка может быть выгружен (откачен) в область своппинга, чтобы освободить место под образ потомка. В другом случае, образ "разбуженного" процесса с высоким приоритетом может быть загружен (подкачен) из области своппинга в RAM для реализации процессорной обработки. В более общем случае, образ любого активизированного процесса может быть загружен (подкачен) из области своппинга в RAM при наличии необходимости и свободной памяти, также как образ любого пассивного процесса может выгружен (откачен) из RAM в область своппинга, чтобы освободить ресурсы памяти для более приоритетного процесса.

Диспетчерский процесс swapper, реализуя механизм своппинга должен решать 2 задачи по определению процессов-претендентов для откачки из и подкачки в RAM. Для решения этих задач диспетчерский процесс, не имея фазы "пользователь", осуществляет бесконечный цикл в фазе "система". Цикл начинается с просмотра таблицы процессов для поиска процесса - претендента на загрузку или расширение своего образа в RAM. Если претендентов нет, диспетчер переходит в состояние ожидания их появления, используя системную функцию sleep, которая назначает ему наивысший приоритет PSWP после пробуждения. Диспетчер "пробуждается" системной функцией wakeup при появлении претендента на загрузку в RAM.

Если в RAM недостаточно свободного места для подкачки образа процесса - претендента на загрузку, диспетчер выгружает (откачивает) в область своппинга необходимое число пассивных процессов. Кандидаты на откачку определяются прежде всего среди "спящих" процессов с низким приоритетом (ниже PZERO), которые ожидают завершения сравнительно медленных операций ввода-вывода. Если таких процессов несколько, выбирается процесс с максимальным образом в RAM.

Чтобы обеспечить однозначность выбора процесса для загрузки в RAM или для выгрузки в область своппинга при прочих равных условия применяется временной критерий оценки. С этой целью в поле p_time структуры struct proc дескриптора любого процесса фиксируется время непрерывного пребывания в RAM или в области своппинга, соответственно тому, где находится образ процесса. Для своппинга выбирается процесс с максимальным значением в поле p_time.

В современных версиях OS UNIX применяется альтернативный своппингу механизм, который использует страничную организацию памяти и называется пейджинг (paging). Пейджинг реализует перемещение активных страниц образов процессов из области своппинга в RAM, называемое подкачкой страниц по требованию, а также обратное перемещение пассивных страниц из RAM в область своппинга, называемое изъятием страниц. Схема реализации пейджинга подобна своппингу, за исключением того, что перемещению между RAM и областью своппинга подвергается не целиком образ процесса, а его отдельные страницы. При этом в RAM должны присутствовать только активные страницы образа процесса, к которым происходит частое обращение. Пассивные страницы, к которым нет обращений, могут быть откачены в область своппинга. Таким образом, при пейджинге загрузка в RAM полного образа процесса не является обязательной. Последнее обстоятельство обуславливает ряд преимуществ пейджинговой схемы по сравнению с традиционным механизмом своппинга, из которых наиболее существенными являются следующие.

Отсутствие ограничений на размер образа процесса по объему физической памяти. Связано с тем, что в RAM размещаются только активные страницы процесса, а не полные сегменты его образа.

Ускоренный старт процесса. Становится возможным, поскольку загрузка сравнительно небольшого числа активных страниц может быть реализована быстрее, чем загрузка всех сегментов процесса.

Экономия оперативной памяти. Поскольку для работы процесса необходима только загрузка активных страниц, память не расходуется на хранение редко используемых кодов и данных.

Увеличение производительности. Становится ощутимым при параллельном выполнении больших процессов, которые часто обращаются к небольшому подмножеству своих активных страниц.

Недостатком схемы пейджинга являются накладные расходы по обработке так называемых отказов страниц, которые связаны с поиском нужной страницы, если она не загружена в RAM. Кроме того, обработка отказов страниц требует реализации операций ввода-вывода, прерывающих работу процессора, для подкачки нужной страницы в RAM из области своппинга. Поэтому частые отказы страниц увеличивают время выполнения процесса. В идеальном случае процесс работает с небольшим числом активных страниц, резидентных в RAM, когда отказы страниц с последующей подкачкой нужных страниц по требованию происходят редко.

Для размещения страниц в RAM при подкачке их по требованию используется пул свободных страниц. Определенный уровень наличия свободных страниц поддерживает процедура изъятия страниц, которая откачивает содержимое пассивных страниц в область своппинга и возвращает изъятые страницы в пул свободных страниц. Сохранение содержимого изъятой страницы в области своппинга происходит, если содержание страницы было модифицировано.

Процедуры подкачки страниц по требованию и изъятия страниц являются основными в механизме пейджинга. Их реализует следящий процесс pageout (pagedaemon в SUN OS), называемый демоном страниц. Также как и диспетчерский процесс swapper, управляющий своппингом, демон страниц это один из начальных процессов OS UNIX. Он имеет идентификатор 2. Демон страниц следит за возрастом страниц и определяет какие пассивные страницы следует отобрать у процессов, чтобы вернуть их в пул свободных страниц. Он также обеспечивает подкачку страниц по требованию в случае отказов страниц. Демон страниц активизируется, когда начинает ощущаться недостаток свободной памяти или для обработки отказов страниц.

В общем случае демон страниц успешно решает все проблемы по управлению памятью с помощью своих базовых процедур подкачки по требованию и изъятия страниц. Однако, могут возникнуть критические ситуации, когда демон страниц не может предоставить необходимое число свободных страниц для удовлетворения запросов процессов к RAM и уровень свободной памяти опускается ниже определенного предела, заданнного системным параметром GPGSLO. В этом случае инициатива передается диспетчерскому процессу, который планирует и реализует своппинг целых процессов. Своппинг подразумевает возвращение в пул свободных страниц всех страниц образа процесса, независимо от их возраста. Реализация своппинга продолжается, пока не восстановлен допустимый уровень свободной памяти. После этого инициатива по управлению памятью возвращается демону страниц.


More Less Title Contents

Hosted by uCoz