это программа, которая способна использовать ЦП). Для выполнения выбирается
процесс с наивысшим приоритетом.



fig: i/readyq.gif




В очереди шесть процессов (A-F), готовых к выполнению и
находящихся в состоянии READY. Остальные процессы (G-Z) блокированы.
В данный момент выполняется процесс A. Процессы A, B и C имеют
наивысший приоритет, поэтому будут разделять центральный процессор в
соответствии с алгоритмом диспетчеризации для выполняемого процесса.






Приоритеты, присваиваемые процессам, находятся в диапазоне от 0
(наименьший) до 31 (наивысший). Уровень приоритета по умолчанию для
создаваемого процесса наследуется от его родителя; для приложений,
запускаемых командным процессором, приоритет обычно равен 10.




Если вы хотите:
Используйте функцию языка СИ:
Определить приоритет процесса
getprio()
Установить приоритет для процесса
setprio()


Методы диспетчеризации

Чтобы удовлетворить потребность различных приложений, QNX
предлагает три метода диспетчеризации:

  • FIFO;

  • карусель;

  • адаптивный.


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

Помните, что эти методы диспетчеризации применимы, только когда
два или более процесса с одинаковым приоритетом находятся в состоянии
READY (т.е. эти процессы непосредственно конкурируют друг с другом).
Если процесс с более высоким приоритетом переходит в состояние READY,
то он немедленно вытесняет все процессы с более низким приоритетом.

На следующей диаграмме три процесса с одинаковым приоритетом
находятся в состоянии READY. Если процесс А блокируется, то
выполняется процесс B.



fig: i/ablocks.gif




Процесс A блокируется, процесс B выполняется.





Метод диспетчеризации процесса наследуется от его родительского
процесса, однако, затем этот метод может быть изменен.




Если вы хотите:
Используйте функцию языка Си:
Определить метод диспетчеризации для процесса
getscheduler()
Установить метод диспетчеризации для процесса
setscheduler()


FIFO диспетчеризация


При FIFO диспетчеризации процесс продолжает выполнение пока не
наступит момент, когда он:

  • добровольно уступает управление (т.е. выполняет
    любой вызов ядра);
  • вытесняется процессом с более высоким приоритетом.



fig: i/method1.gif




FIFO диспетчеризация. Процесс А выполняется до тех пор, пока не блокируется.





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


Карусельная диспетчеризация


При карусельной диспетчеризации процесс продолжает выполнение,
пока не наступит момент, когда он:

  • добровольно уступает управление (т.е. блокируется);
  • вытесняется процессом с более высоким приоритетом;
  • использовал свой квант времени (timeslice).



fig: i/method2.gif




Карусельная диспетчеризация. Процесс А выполняется до тех пор,
пока он не использовал свой квант времени; затем выполняется
следующий процесс, находящийся в состоянии READY (процесс B).





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







Note: За исключением квантования времени, карусельная диспетчеризация
идентична FIFO-диспетчеризации.





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


При адаптивной диспетчеризации процесс ведет себя следующим образом:

  • Если процесс использовал свой квант времени (т.е. он не
    блокировался), то его приоритет уменьшается на 1. Это получило
    название снижение приоритета (priority decay). Учтите, что
    "пониженный" процесс не будет продолжать "снижаться", даже если он
    использовал еще один квант времени и не блокировался - он снизится
    только на один уровень ниже своего исходного приоритета.

  • Если процесс блокируется, то ему возвращается первоначальное
    значение приоритета.



fig: i/method3.gif




Адаптивная диспетчеризация. Процесс А использовал свой квант
времени; его приоритет снизился на 1. Выполняется следующий процесс в
состоянии READY (процесс B).





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

Адаптивная диспетчеризация является методом диспетчеризации,
использующимся по умолчанию для программ, запускаемых командным
интерпретатором.


Клиент-управляемый приоритет


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

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

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

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

Чтобы решить эту дилемму, сервер может поставить свой приоритет в
зависимость от приоритета того клиента, чей запрос он выполняет. Когда
сервер получает сообщение, приоритет будет установлен таким же, как у
клиента. Обратите внимание, что изменился только приоритет сервера -
его алгоритм диспетчеризации остается неизменным. Если во время
выполнения запроса сервер получает другое сообщение, и приоритет
нового клиента выше, чем у сервера, то приоритет сервера повышается.
Фактически, новый клиент "заряжает" сервер до своего уровня
приоритета, позволяя ему закончить выполнение текущего запроса и
приступить к обработке запроса нового клиента. Если этого не делать,
то фактически приоритет нового клиента понизится, пока он блокирован
на сервере с низким приоритетом.

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

Чтобы установить клиент-управляемый приоритет, используйте
функцию Си qnx_pflags() следующим образом:



qnx_pflags(~0, _PPF_PRIORITY_FLOAT
| _PPF_PRIORITY_REC, 0, 0);



Несколько слов о реальном времени


Как бы мы этого не хотели, компьютеры не являются бесконечно
быстрыми. Для системы реального времени жизненно важно, чтобы такты
работы ЦП не расходовались зря. Также очень важно свести к минимуму
время, которое проходит с момента наступления внешнего события до
начала выполнения кода программы, ответственной за обработку данного
события. Это время называется задержкой.

В QNX системе встречается несколько видов задержек.

Задержка прерывания

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

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



fig: i/intlat.gif




Обработчик прерывания просто завершается.





Задержка прерывания (Til) на приведенной выше диаграмме отражает
минимальную задержку - случай, когда прерывания были полностью
разрешены в момент, когда произошло прерывание. В худшем случае задержка
прерывания составит это время плюс наибольшее время, в течение
которого QNX или выполняющийся процесс запрещает прерывания ЦП.


Til для различных процессоров


Следующая таблица содержит типичные значения задержки прерывания
(TqqqЏ1ilqqqЏ0) для разных процессоров:










Задержка прерывания (Til): Процессор:
3.3 микросекунды166 МГц Pentium
4.4 микросекунды 100 МГц Pentium
5.6 микросекунды 100 МГц 486DX4
22.5 микросекунды 33 МГц 386EX


Задержка диспетчеризации

В некоторых случаях обработчик аппаратного прерывания низкого
уровня должен передать управление процессу более высокого уровня. В
этом случае обработчик прерывания перед выполнением команды "возврат"
запускает прокси. Здесь имеет место второй вид задержки - задержка
диспетчеризации
, - с которой также надо считаться.

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



fig: i/schedlat.gif




Обработчик прерывания завершает работу и запускает прокси.





Важно отметить, что обработка большинства прерываний завершается
без запуска прокси. В большинстве случаев обработчик прерывания сам
может выполнить все необходимые действия. Запуск прокси, чтобы "разбудить"
драйвер, процесс более высокого уровня, происходит только
при наступлении важного события. Например, обработчик прерывания для
драйвера последовательного порта при каждом прерывании "регистр передачи
свободен" будет передавать один байт данных и запустит процесс более
высокого уровня (Dev) только тогда, когда выходной буфер, наконец,
опустеет.


Tsl для различных процессоров


Следующая таблица содержит типичные значения задержки
диспетчеризации (Tsl) для разных процессоров:







Задержка диспетчеризации (Tsl): Процессор:
4.7 микросекунды166 МГц Pentium
6.7 микросекунды100 МГц Pentium
11.1 микросекунды100 МГц 486DX4
74.2 микросекунды33 МГц 386EX


Стек прерываний

Так как архитектура микрокомпьютеров позволяет назначать
приоритеты аппаратным прерываниям, то прерывания с более высоким
приоритетом могут вытеснять прерывания с меньшим приоритетом.

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



fig: i/stackint.gif




Выполняется процесс A. Прерывание IRQx вызывает выполнение
обработчика прерывания Intx, который вытесняется IRQy
и его обработчиком Inty. Inty запускает прокси,
вызывающее выполнение процесса B, а Intx запускает прокси,
вызывающее выполнение процесса C.






<!-- 2 -->



<!-- 3 -->

Менеджер процессов



Эта глава охватывает следующие темы:



Введение



Обязанности Менеджера процессов

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

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


Примитивы создания процессов

QNX поддерживают три примитива создания процесса:

  • fork();

  • exec();
  • spawn().

Примитивы fork() и exec() определены стандартом POSIX,
а примитив spawn() реализован только в QNX.

Примитив fork()


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


Примитив exec()


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


Примитив spawn()


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


Наследование

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






















Наследуемый параметр fork()exec()spawn()
Идентификатор процессанетданет
Открытые файлыдапо выбору*по выбору
Блокировка файловнетданет
Ожидающие сигналынетданет
Маска сигналовдапо выборупо выбору
Игнорируемые сигналыдапо выборупо выбору
Обработчики сигналовданетнет
Переменные окружениядапо выборупо выбору
Идентификатор сеансададапо выбору
Группа процессададапо выбору
Реальные UID, GIDдадада
Эффективные UID, GIDдапо выборупо выбору
Текущий рабочий каталогдапо выборупо выбору
Маска создания файловдадада
Приоритетдапо выборупо выбору
Алгоритм диспетчеризациидапо выборупо выбору
Виртуальные каналынетнетнет
Символьные именанетнетнет
таймеры реального временинетнетнет

*по выбору: вызывающий процесс может по необходимости выбрать -
да или нет.




Жизненный цикл процесса


Процесс проходит через четыре стадии:



  1. Создание.
  2. Загрузка.
  3. Выполнение.
  4. Завершение.



Создание

Создание нового процесса состоит из присвоения новому процессу
идентификатора (ID) процесса и подготовки информации, которая
определяет окружение нового процесса. Большая часть этой
информации наследуется от родительского процесса (смотри раздел
"Наследование").


Загрузка

Загрузка образа процесса производится нитью загрузчика.
Код загрузчика находится в Менеджере процессов, но нить выполняется под ID
нового процесса. Это позволяет Менеджеру процессов обрабатывать и другие
запросы во время загрузки программы.


Выполнение

После того, как код программы загружен, процесс готов к
выполнению; он начинает конкурировать с остальными процессами за
ресурсы ЦП. Заметьте, что все процессы выполняются параллельно со
своими родителями. Кроме того, смерть родительского процесса не
означает автоматическую смерть его дочерних процессов.


Завершение

Процесс завершается одним из двух способов:

  • процесс получает сигнал, который вызывает завершение процесса;

  • процесс вызывает функцию Си exit(), либо в явном виде,
    либо в качестве действия по умолчанию при возврате из функции main().


Завершение включает две стадии:


  1. Выполняется нить завершения в Менеджере процессов. Этот
    "заслуживающий доверия" код расположен в Менеджере процессов, но нить
    выполняется с ID завершающегося процесса. Эта нить закрывает все
    открытые файловые дескрипторы и освобождает следующие ресурсы:

    • все виртуальные каналы, принадлежащие процессу;
    • всю память, выделенную процессу;
    • все символьные имена;
    • любые старшие номера устройств (только менеджеры ввода/вывода);
    • любые обработчики прерывания;
    • любые прокси;
    • любые таймеры.

  2. После того, как нить завершения выполнена, извещение о
    завершении процесса посылается родительскому процессу (эта фаза
    выполняется внутри Менеджера процессов).

    Если родительский процесс не вызвал wait() или
    waitpid(), то дочерний процесс становится "зомби" и не будет
    завершен, пока родительский процесс не вызовет wait() или не
    завершит выполнение. (Если вы не хотите, чтобы процесс ждал смерти
    дочерних процессов, вы должны либо установить _SPAWN_NOZOMBIE флаг
    функциями qnx_spawn() или qnx_spawn_options(), либо установить
    действие SIG_IGN для сигнала SIGCHLD посредством функции signal().
    Таким образом, дочерние процессы не будут становиться зомби после
    смерти.)

    Родительский процесс может ждать смерти дочернего процесса,
    запущенного на удаленном узле. Если родитель процесса-зомби умирает,
    то зомби освобождается.



Если запущена утилита dumper и процесс завершается в
результате получения сигнала, то генерируется дамп памяти. Вы можете
затем исследовать его с помощью отладчика.



Состояния процесса


Процесс всегда находится в одном из следующих состояний:

  1. READY (готов) - процесс способен использовать ЦП (т.е. он не
    ждет наступления какого-либо события).

  2. BLOCKED (блокирован) - процесс в одном из следующих блокированных
    состояний:

    • SEND-блокирован;


    • RECEIVE-блокирован;


    • REPLY-блокирован;


    • SIGNAL-блокирован;


    • SEMAPHORE-блокирован.



  3. HELD (приостановлен) - процесс получил сигнал SIGSTOP.
    В этом состоянии процесс не имеет права использовать ЦП; выйти
    из состояния HELD процесс может только в результате получения
    сигнала SIGCONT, или завершив свою работу после получения другого сигнала.

  4. WAIT (блокирован) - процесс выполнил вызов функции wait()
    или waitpid() для ожидания сообщения о завершении выполнения
    дочернего процесса.

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







Note: Для получения более полной информации о блокированном состоянии
обратитесь к главе "Микроядро".






fig: i/allstate.gif




Возможные состояния процесса в QNX.





Показаны следующие переходы:


  1. Процесс посылает сообщение.
  2. Процесс-адресат получает сообщение.
  3. Процесс-адресат отвечает на сообщение.
  4. Процесс ожидает сообщения.
  5. Процесс получает сообщение.
  6. Сигнал разблокирует процесс.
  7. Сигнал пытается разблокировать процесс; адресат ранее запросил
    извещение о сигнале путем посылки сообщения.
  8. Процесс-адресат получает сообщение о сигнале.
  9. Процесс ожидает смерти дочернего процесса.
  10. Дочерний процесс умирает, либо сигнал разблокирует процесс.
  11. Процесс получает SIGSTOP.
  12. Процесс получает SIGCONT.
  13. Процесс умирает.
  14. Родительский процесс вызывает функцию wait() или
    waitpid(), завершает ее выполнение, либо ранее уже завершил выполнение.
  15. Процесс вызывает функцию semwait() для семафора с
    неположительным значением.
  16. Другой процесс вызывает функцию sempost(), или приходит
    немаскированный сигнал.


Определение состояний процесса

Чтобы определить состояние конкретного процесса из командного
интерпретатора (Shell), используйте утилиты ps и sin
(внутри приложений используйте функцию qnx_psinfo()).

Чтобы определить состояние операционной системы в целом из
командного интерпретатора (Shell), используйте утилиту sin (внутри
приложений используйте функцию qnx_osinfo()).

Утилита ps определена стандартом POSIX; ее использование в
командных файлах может быть переносимым в другие системы. Утилита
sin, напротив, уникальна для QNX; она дает вам полезную информацию,
специфическую для QNX, которую вы не можете получить, используя
утилиту ps.



Символьные имена процессов


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

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

В QNX процессы могут присваивать себе символьные имена. В
контексте одного узла процесса могут зарегистрировать эти имена у
Менеджера процессов на том узле, на котором они выполняются. Остальные
процессы могут затем запросить у Менеджера процессов идентификатор
процесса, ассоциированный с определенным именем.

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







qnx/fsys локальное имя
company/xyz локальное имя
/company/xyz глобальное имя







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




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

В каждый момент времени в сети могут быть запущены до десяти