Полная версия

Главная arrow Информатика arrow Введение в инфокоммуникационные технологии

  • Увеличить шрифт
  • Уменьшить шрифт


<<   СОДЕРЖАНИЕ ПОСМОТРЕТЬ ОРИГИНАЛ   >>

Процессы в ОС и1Ч1Х: системные вызовы, иерархия процессов

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

Реализация системных вызовов должна удовлетворять следующим требованиям:

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

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

Для обеспечения высокой скорости полезно использовать векторные свойства системы программных прерываний, имеющиеся во многих процессорах, т.е. закрепить за каждым системным вызовом определенное значение вектора. Приложение при таком способе вызова непосредственно указывает в аргументе запроса значение вектора, после чего управление немедленно передается требуемой процедуре ОС (рис. 3.5, а). Однако этот децентрализованный способ передачи управления привязан к особенностям аппаратной платформы, а также не позволяет ОС легко модифицировать набор системных вызовов и контролировать их использование. Например, в процессоре Pentium количество системных вызовов определяется количеством векторов прерываний, выделенных для этой цели из общего пула в 256 элементов (часть которых используется под аппаратные прерывания и обработку исключений). Добавление нового системного вызова требует от системного программиста тщательного поиска свободного элемента в таблице прерываний, которого к тому же на каком-то этапе развития ОС может и не оказаться.

В большинстве ОС системные вызовы обслуживаются по централизованной схеме, основанной на существовании диспетчера системных вызовов (рис. 3.5, б). При любом системном вызове приложение выполняет программное прерывание с определенным и единственным номером вектора. Например, ОС Linux использует для системных вызовов команду INT 80h, а ОС Windows NT (при работе на платформе Pentium) — INT 2Eh. Перед выполнением программного прерывания приложение тем или иным способом передает операционной системе номер системного вызова, который является индексом в таблице адресов процедур ОС, реализующих системные вызовы (таблица sysent на рис. 3.5, б). Способ передачи зависит от реализации. Например, номер можно поместить в определенный регистр общего назначения процессора или передать через стек (в этом случае после прерывания и перехода в привилегированный режим их нужно будет скопировать в системный стек из пользовательского, это действие в некоторых процессорах автоматизировано). Также некоторым способом передаются аргументы системного вызова, они могут как помещаться в регистры общего назначения, так и передаваться через стек

Децентрализованная (а) и централизованная (б) схемы обработки

Рис. 3.5. Децентрализованная (а) и централизованная (б) схемы обработки

системных вызовов

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

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

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

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

Для приложения системный вызов внешне ничем не отличается от вызова обычной библиотечной функции языка С, связанной (динамически или статически) с объектным кодом приложения и выполняющейся в пользовательском режиме. И такая ситуация действительно имеет место — для всех системных вызовов в библиотеках, предоставляемых компилятором С, имеются так называемые «заглушки» (в англоязычном варианте используется термин stub — остаток, огрызок). Каждая заглушка оформлена как С-функция, при этом она содержит несколько ассемблерных строк, нужных для выполнения инструкции программного прерывания. Таким образом, пользовательская программа вызывает заглушку, а та в свою очередь вызывает процедуру ОС.

Для ускорения некоторых достаточно простых системных вызовов, которым к тому же не нужен привилегированный режим, требуемая работа полностью выполняется библиотечной функцией, которую несправедливо называть в данном случае заглушкой. Точнее, такая функция не является системным вызовом, а представляет собой «чистую» библиотечную функцию, выполняющую всю свою работу в пользовательском режиме в виртуальном адресном пространстве процесса, но прикладной программист может об этом и не знать — для него системные вызовы и библиотечные функции выглядят единообразно. Прикладной программист имеет дело с набором функций прикладного программного интерфейса — API (например, Win32 или POSIX), состоящего и библиотечных функций, часть которых пользуется для завершения работы системными вызовами, а часть нет.

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

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

Асинхронный системный вызов не приводит к переводу процесса в режим ожидания после выполнения некоторых начальных системных действий, например запуска операции вывода/вывода, управление возвращается прикладному процессу (рис. 3.6, б).

Синхронные (а) и асинхронные (б) системные вызовы

Рис. 3.6. Синхронные (а) и асинхронные (б) системные вызовы

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

Блок управления процессом

Для того чтобы ОС могла управлять процессами, она должна располагать всей необходимой для этого информацией. С этой целью на каждый процесс заводится специальная информационная структура, содержащая определенную важную информацию о процессе и называемая дескриптором процесса (блоком управления процессом — Process Control Block, PCB). В общем случае дескриптор процесса содержит следующую информацию:

  • • идентификатор процесса (так называемый PID — Process IDentificator);
  • • тип (или класс) процесса, который определяет для супервизора некоторые правила предоставления ресурсов;
  • • приоритет процесса, в соответствии с которым супервизор предоставляет ресурсы. В рамках одного класса процессов в первую очередь обслуживаются более приоритетные процессы;
  • • переменную состояния, которая определяет, в каком состоянии находится процесс (готов к работе, в состоянии выполнения, ожидание устройства ввода/вывода и т.д.);
  • • защищенную область памяти (или адрес такой зоны), в которой хранятся текущие значения регистров процессора, если процесс прерывается, не закончив работы. Эта информация называется контекстом задачи (процесса);
  • • информацию о ресурсах, которыми процесс владеет и/или имеет право пользоваться (указатели на открытые файлы, информация о незавершенных операциях ввода/вывода и т. п.);
  • • место (или его адрес) для организации общения с другими процессами;
  • • параметры времени запуска (момент времени, когда процесс должен активизироваться, и периодичность этой процедуры).

Таким образом, в блоке управления процессом ОС может сосредоточить всю ключевую информацию о процессе.

Блоки управления процессами, как правило, постоянно располагаются в оперативной памяти с целью ускорить работу супервизора, который организует их в списки (очереди) и отображает изменение состояния процесса перемещением соответствующего описателя из одного списка в другой. Для каждого состояния (за исключением состояния выполнения для однопроцессорной системы) ОС ведет соответствующий список задач, находящихся в этом состоянии. Однако для состояния ожидания может быть не один список, а столько, сколько различных видов ресурсов могут вызывать состояние ожидания. Например, состояний ожидания завершения операции ввода/вывода может быть столько, сколько устройств ввода/вывода имеется в системе. Поскольку ОС должна иметь возможность быстро выполнять операции с различными РСВ, во многих вычислительных машинах предусматривается специальный аппаратный регистр, который всегда указывает на РСВ текущего выполняемого процесса. Зачастую имеются также аппаратно-реализованные команды, которые обеспечивают быструю загрузку информации состояния в РСВ и последующее быстрое восстановление этой информации.

Процессы в ОС UNIX. Все построение ОС UNIX основано на использовании концепции процессов. Контекст процесса складывается из пользовательского контекста и контекста ядра, как изображено на рис. 3.7.

Под пользовательским контекстом процесса понимают код и данные, расположенные в адресном пространстве процесса. Все данные подразделяются:

  • • на инициализируемые неизменяемые данные (например, константы);
  • • инициализируемые изменяемые данные (все переменные, начальные значения которых присваиваются на этапе компиляции);
  • • ^инициализируемые изменяемые данные (все статические переменные, которым не присвоены начальные значения на этапе компиляции);
Контекст процесса в UNIX

Рис. 3.7. Контекст процесса в UNIX

  • • стек пользователя;
  • • данные, расположенные в динамически выделяемой памяти (например, с помощью стандартных библиотечных С функций mallocO, callocO, reallocO).

Исполняемый код и инициализируемые данные составляют содержимое файла программы, который исполняется в контексте процесса. Пользовательский стек применяется при работе процесса в пользовательском режиме (user-mode).

Под понятием «контекст ядра» объединяются системный контекст и регистровый контекст. Выделим в контексте ядра стек ядра, который используется при работе процесса в режиме ядра (kernel mode), и данные ядра, хранящиеся в структурах, являющихся аналогом блока управления процессом — РСВ. В данные ядра входят: идентификатор пользователя — UID, групповой идентификатор пользователя — G1D, идентификатор процесса — P1D, идентификатор родительского процесса — PP1D.

Идентификация процесса. Каждый процесс в ОС получает уникальный идентификационный номер — P1D (process identificator).

При создании нового процесса ОС пытается присвоить ему свободный номер больший, чем у процесса, созданного перед ним. Если таких свободных номеров не оказывается (например, достигнут максимально возможный номер для процесса), то ОС выбирает минимальный номер из всех свободных номеров. В ОС Linux присвоение идентификационных номеров процессов начинается с номера 0, который получает процесс kernel при старте ОС. Этот номер впоследствии не может быть присвоен никакому другому процессу. Максимально возможное значение для номера процесса в Linux на базе 32-разрядных процессоров Intel составляет 231-1.

Состояния процесса. Краткая диаграмма состояний. Модель состояний процессов в ОС UNIX представляет собой детализацию модели состояний. Диаграмма состояний процессов в ОС UNIX изображена на рис. 3.8.

Состояние процесса «исполнение» расщепилось на два состояния: «исполнение в режиме ядра» и «исполнение в режиме пользователя». В состоянии «исполнение в режиме пользователя» процесс выполняет прикладные инструкции пользователя. В состоянии «исполнение в режиме ядра» выполняются инструкции ядра ОС в контексте текущего процесса (например, при обработке сис-

Краткая диаграмма состояний процесса в UNIX темного вызова или прерывания)

Рис. 3.8. Краткая диаграмма состояний процесса в UNIX темного вызова или прерывания). Из состояния «исполнение в режиме пользователя» процесс не может непосредственно перейти в состояния «ожидание», «готовность» и «закончил исполнение». Такие переходы возможны только через промежуточное состояние «исполняется в режиме ядра». Также запрещен прямой переход из состояния «готовность» в состояние «исполнение в режиме пользователя».

Приведенная выше диаграмма состояний процессов в UNIX не является полной. Она показывает только состояния, для понимания которых достаточно уже полученных знаний.

Иерархия процессов. В ОС UNIX все процессы, кроме одного, создающегося при старте ОС, могут быть порождены только какими-либо другими процессами. В качестве прародителя всех остальных процессов в подобных UNIX системах могут выступать процессы с номерами 1 или 0. В ОС Linux таким родоначальником, существующим только при загрузке системы, является процесс kernel с идентификатором 0.

Таким образом, все процессы в UNIX связаны отношениями «процесс-родитель — процесс-ребенок» и образуют генеалогическое дерево процессов. Для сохранения целостности генеалогического дерева в ситуациях, когда процесс-родитель завершает свою работу до завершения выполнения процесса-ребенка, идентификатор родительского процесса в данных ядра процесса-ребенка (PPID — parent process identificator) изменяет свое значение на значение 1, соответствующее идентификатору процесса init, время жизни которого определяет время функционирования ОС. Тем самым процесс init как бы усыновляет осиротевшие процессы. Наверное, логичнее было бы заменять PPID не на значение 1, а на значение идентификатора ближайшего существующего процесса- прародителя умершего процесса-родителя, но в UNIX почему-то такая схема не была реализована.

Системные вызовы де1рр1с!() и де1ртс1()

Данные ядра, находящиеся в контексте ядра процесса, не могут быть прочитаны процессом непосредственно. Для получения информации о них процесс должен совершить соответствующий системный вызов. Значение идентификатора текущего процесса может быть получено с помощью системного вызова §е1р1с1(), а значение идентификатора родительского процесса для текущего процесса — с помощью системного вызова getppid(). Прототипы этих системных вызовов и соответствующие типы данных описаны в системных файлах и . Системные вызовы не имеют параметров и возвращают идентификатор текущего процесса и идентификатор родительского процесса соответственно.

Прототипы системных вызовов

#include ttinclude pid_t getpid(void); pid_t getppid(void);

Описание системных вызовов

Системный вызов getpid возвращает идентификатор текущего процесса. Системный вызов getppid возвращает идентификатор процесса-родителя для текущего процесса.

Тип данных pid_t является синонимом для одного из целочисленных типов языка С.

Создание процесса в UNIX. Системный вызов fork()

В ОС UNIX новый процесс может быть порожден единственным способом — с помощью системного вызова fork(). При этом вновь созданный процесс будет являться практически полной копией родительского процесса. У порожденного процесса по сравнению с родительским процессом (на уровне уже полученных знаний) изменяются значения следующих параметров:

  • • идентификатор процесса — PID;
  • • идентификатор родительского процесса — РРШ. Дополнительно может измениться поведение порожденного

процесса по отношению к некоторым сигналам.

Системный вызов для порождения нового процесса

Прототип системного вызова

#include ttinclude pid_t fork(void);

Описание системного вызова

Системный вызов fork служит для создания нового процесса в операционной системе UNIX. Процесс, который инициировал системный вызов fork, принято называть родительским процессом (parent process). Вновь порожденный процесс принято называть процессом-ребенком (child process). Процесс-ребенок является почти полной копией родительского процесса. У порожденного процесса по сравнению с родительским изменяются значения следующих параметров:

  • • идентификатор процесса;
  • • идентификатор родительского процесса;
  • • время, оставшееся до получения сигнала SIGALRM;
  • • сигналы, ожидавшие доставки родительскому процессу, не будут доставляться порожденному процессу.

При однократном системном вызове возврат из него может произойти дважды: один раз в родительском процессе, а второй раз в порожденном процессе. Если создание нового процесса произошло успешно, то в порожденном процессе системный вызов вернет значение 0, а в родительском процессе — положительное значение, равное идентификатору процесса-ребенка. Если создать новый процесс не удалось, то системный вызов вернет в инициировавший его процесс отрицательное значение.

Системный вызов fork является единственным способом породить новый процесс после инициализации ОС UNIX.

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

Завершение процесса. Функция ех1Ч()

Существует два способа корректного завершения процесса в программах, написанных на языке С. Первый способ мы использовали до сих пор: процесс корректно завершался по достижении конца функции main() или при выполнении оператора return в функции main(), второй способ применяется при необходимости завершить процесс в каком-либо другом месте программы. Для этого используется функция exit () из стандартной библиотеки функций для языка С. При выполнении этой функции происходит сброс всех частично заполненных буферов ввода/вы- вода с закрытием соответствующих потоков, после чего инициируется системный вызов прекращения работы процесса и перевода его в состояние «закончил исполнение».

Возврата из функции в текущий процесс не происходит, и функция ничего не возвращает.

Значение параметра функции exitQ — кода завершения процесса — передается ядру ОС и может быть затем получено процессом, породившим завершившийся процесс. На самом деле при достижении конца функции main() также неявно вызывается эта функция со значением параметра 0.

Функция для нормального завершения процесса

Прототип функции

«include void exit(int status);

Описание функции

Функция exit служит для нормального завершения процесса. При выполнении этой функции происходит сброс всех частично заполненных буферов ввода/вывода с закрытием соответствующих потоков (файлов, pipe, FIFO, сокетов), после чего инициируется системный вызов прекращения работы процесса и перевода его в состояние закончил исполнение.

Возврата из функции в текущий процесс не происходит, и функция ничего не возвращает.

Значение параметра status — кода завершения процесса — передается ядру ОС и может быть затем получено процессом, породившим завершившийся процесс. При этом используются только младшие 8 бит параметра, так что для кода завершения допустимы значения от 0 до 255. По соглашению, код завершения 0 означает безошибочное завершение процесса.

Если процесс завершает свою работу раньше, чем его родитель, и родитель явно не указал, что он не хочет получать информацию о статусе завершения порожденного процесса, то завершившийся процесс не исчезает из системы окончательно, а остается в состоянии «закончил исполнение» либо до завершения процесса-родителя, либо до того момента, когда родитель получит эту информацию. Процессы, находящиеся в состоянии «закончил исполнение», в ОС UNIX принято называть процессами-зомби (zombie, defunct).

 
<<   СОДЕРЖАНИЕ ПОСМОТРЕТЬ ОРИГИНАЛ   >>