Поделиться в Facebook Поделиться ВКонтакте Поделиться в LinkedIn Опубликовать в Twitter

Микроядро

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

Введение

Микроядро QNX отвечает за выполнение следующих функций:

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

Внутри микроядра QNX

Внутри микроядра QNX

Связь между процессами

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

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

IPC посредством сообщений

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

Примитивы передачи сообщений

Для непосредственной связи друг с другом взаимодействующие процессы используют следующие функции языка программирования Си:

Функция языка Си:Назначение:
Send()посылка сообщений
Receive()получение сообщений
Reply()ответ процессу, пославшему сообщение

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

Следует заметить, однако, что далеко не всегда возникает необходимость использовать функции Send(), Receive() и Reply() в явном виде. Библиотека функций языка Си в QNX построена на основе использования сообщений - в результате, когда процесс использует стандартные механизмы передачи данных (такие, как, например, программный канал - pipe), он косвенным образом использует передачу сообщений.


Процесс A посылает сообщение процессу B, который получает его,обрабатывает и посылает ответ

Процесс A посылает сообщение процессу B, который получает его,обрабатывает и посылает ответ

На рисунке изображена последовательность событий, имеющих место, когда два процесса, процесс A и процесс B, используют функции Send(), Receive() и Reply() для связи друг с другом:

  1. Процесс A посылает сообщение процессу B, вызвав функцию Send(), которая передает соответствующий запрос ядру. В этот момент времени процесс A переходит в SEND-блокированное состояние и остается в этом состоянии до тех пор, пока процесс B не вызовет функцию Receive() для получения сообщения.
  2. Процесс B вызывает Receive() и получает сообщение от процесса A. При этом состояние процесса A изменяется на REPLY-блокирован. Процесс B при вызове функции Receive() в данном случае не блокируется, т.к. к этому моменту его уже ожидало сообщение от процесса A.

Заметьте, что если бы процесс B вызвал Receive() до того, как ему было послано сообщение, то он бы попал в состояние RECEIVE-блокирован до получения сообщения. В этом случае процесс-отправитель сообщения немедленно после посылки сообщения попал бы в состояние REPLY-блокирован.

  1. Процесс B выполняет обработку полученного от процесса A сообщения и затем вызывает функцию Reply(). Ответное сообщение передается процессу A, который переходит в состояние готовности к выполнению. Вызов Reply() не блокирует процесс B, который также готов к выполнению. Какой из этих процессов будет выполняться, зависит от их приоритетов.

Синхронизация процессов

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

Давайте снова рассмотрим приведенный выше рисунок. После того как процесс A вызвал функцию Send(), он не может продолжать выполнение до тех пор, пока не получит ответ на посланное сообщение. Это гарантирует, что выполняемая процессом B по запросу процесса A обработка данных будет завершена прежде, чем процесс A продолжит выполнение. Более того, после вызова процессом B запроса на получение данных Receive(), он не может продолжать выполнение до тех пор, пока не получит следующее сообщение.


Note: Более подробно диспетчеризация процессов в QNX рассматривается в разделе "Диспетчеризация процессов" далее в этой главе.

Блокированные состояния

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

Возможные блокированные состояния процессов приведены в следующей таблице:

Если процесс выдал:То процесс:
Запрос Send(), и отправленное им сообщение еще не получено процессом-получателемSEND-блокирован
Запрос Send(), и отправленное им сообщение получено процессом-получателем, но ответ еще не выданREPLY-блокирован
Запрос Receive(), но еще не получил сообщениеRECEIVE-блокирован

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

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


Note: Для получения информации обо всех возможных состояниях процесса смотри главу "Менеджер процессов".

Использование Send(), Receive() и Reply()

Давайте теперь более подробно рассмотрим вызовы функций Send(), Receive() и Reply(). Воспользуемся рассмотренным выше примером передачи сообщения от процесса A к процессу B.

Функция Send()

Предположим, что процесс А выдает запрос на передачу сообщения процессу В. Это выполняется посредством вызова функции Send():

Send( ''pid'', ''smsg'', ''rmsg'', ''smsg_len'', ''rmsg_len'' );

При вызове функции Send() используются следующие аргументы:

pid
идентификатор процесса (process ID), которому предназначается сообщение (т.е. процесса B); этот идентификатор используется для обращения к процессу со стороны операционной системы и других процессов;
smsg
буфер сообщения (т.е. сообщение, подлежащее посылке)
rmsg
буфер ответа (будет содержать ответ от процесса B)
smsg_len
длина посылаемого сообщения в байтах
rmsg_len
максимальная длина ответа, который может принять процесс A, в байтах

Обратите внимание, что будет передано не более smsg_len байт и не более чем rmsg_len байт будет получено в качестве ответа - это гарантирует, что не произойдет случайного переполнения буферов.

Функция Receive()

Вызвав запрос Receive(), процесс B может получить сообщение, направленное ему процессом A:

''pid'' = Receive( 0, ''msg'', ''msg_len'' )

Вызов функции Receive() содержит следующие аргументы:

pid
идентификатор процесса, который послал сообщение (т.е. процесс A)
0
(ноль) означает, что процесс B желает принять сообщение от любого процесса
msg
буфер, куда будет помещено принимаемое сообщение
msg_len
максимальный размер данных, которые будут помещены в буфер приема, в байтах

Если значения аргументов smsg_len в вызове функции Send() и msg_len в вызове функции Receive() отличаются, друг от друга, то наименьшее из них определяет размер данных, которые будут переданы.

Функция Reply()

После успешного получения сообщения от процесса A, процесс B должен ответить процессу A, вызвав функцию Reply():

Reply( ''pid'', ''reply'', ''reply_len'' );

Вызов функции Reply() содержит следующие аргументы:

pid
идентификатор процесса, которому предназначается ответ (т.е. процесс A)
reply
буфер, содержащий ответ
reply_len
длина данных, передаваемых в качестве ответа, в байтах

Если значения аргументов reply_len при вызове функции Reply() и rmsg_len при вызове функции Send() отличаются друг от друга, то наименьшее из них определяет размер передаваемых данных.

Reply-управляемый обмен сообщениями

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

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

Дополнительные сведения

При разработке программ, использующих передачу сообщений, необходимо иметь в виду следующее:

  • сообщение сохраняется в теле посылающего процесса до тех пор, пока принимающий процесс не будет готов получить его. Сообщение не копируется в Микроядро. Это безопасно, т.к. посылающий процесс SEND-блокирован и не может неумышленно изменить содержимое сообщения
  • при вызове функции Reply() данные копируются из отвечающего процесса в REPLY-блокированный процесс как одна неразрывная операция. Вызов функции Reply() не блокирует процесс - REPLY-блокированный процесс перестает быть блокированным после того, как в него скопированы данные;
  • при посылке сообщения процессу не требуется знать состояние того процесса, которому это сообщение адресовано. Если получатель не готов к приему сообщения в момент его отправки, то посылающий сообщение процесс просто становится SEND-блокирован;
  • если это необходимо, то процесс может посылать сообщение нулевой длины, ответ нулевой длины, либо и то и другое;
  • с точки зрения разработчика, использование Send() для передачи процессу-серверу запроса на получение какой-либо услуги равнозначно вызову библиотечной подпрограммы, предоставляющей ту же самую услугу. В обоих случаях ваша программа сначала подготавливает определенные данные, а затем вызывает либо функцию Send(), либо библиотечную подпрограмму, после чего ожидает, когда они закончат выполнение. В обоих случаях код, реализующий запрашиваемую услугу, выполняется между двумя четко определенными точками - Receive() и Reply() для процесса-сервера, входом в подпрограмму и командой return для вызова библиотечной подпрограммы. Когда вызываемая сервисная программа завершена, ваша программа "знает", куда помещены результаты, и может приступить к проверке возвращенного кода ошибки, обработке результатов и так далее.

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

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

Сервер получил сообщение от клиента A и клиента B (но еще не ответил им). Сообщения от клиентов C, D, E еще не получены

Сервер получил сообщение от клиента A и клиента B (но еще не ответил им). Сообщения от клиентов C, D, E еще не получены

Дополнительные возможности

QNX также предоставляет следующие дополнительные возможности по передаче сообщений:

  • условный прием сообщений;
  • чтение или запись части сообщений;
  • составные сообщения (сообщения, состоящие из нескольких частей).

Условный прием сообщений

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

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


Note: Следует по возможности избегать использования функции Creceive(), т.к. она позволяет процессу непрерывно выполняться с неизменяющимся уровнем приоритета.

Чтение или запись части сообщения

Иногда желательно читать или записывать сообщения по частям с тем, чтобы использовать уже выделенный для сообщения буфер вместо выделения отдельного рабочего буфера.

Например, менеджер ввода/вывода может принимать данные в виде сообщений, которые состоят из заголовка фиксированной длины и следующих за ним данных переменной длины. В заголовке указывается размер данных (от 0 до 64 Кбайт). В этом случае менеджер ввода/вывода может сначала принять только заголовок сообщения, а затем использовать функцию Readmsg() для чтения данных переменной длины непосредственно в соответствующий буфер вывода. Если размер данных превышает размер буфера, то менеджер может неоднократно вызывать функцию Readmsg() по мере освобождения буфера вывода. Аналогичным образом, функция Writemsg() может быть использована для поэтапного копирования данных в выделенный для ответного сообщения буфер уменьшая, таким образом, потребность менеджера ввода/вывода в выделении внутренних буферов.

Составные сообщения

До сих пор мы рассматривали сообщения как непрерывную последовательность байт. Однако сообщения часто состоят из двух или более отдельных частей. Например, сообщение может иметь заголовок фиксированной длины, за которым следуют данные переменной длины. Для того чтобы избежать копирования частей такого сообщения во временные промежуточные буферы при передаче или приеме, может быть использовано составное сообщение, состоящее из двух или более отдельных буферов сообщений. Именно благодаря этой возможности менеджеры ввода/вывода QNX, такие как Dev и Fsys, достигают своей высокой производительности.

Следующие функции позволяют обрабатывать составные сообщения:

  • Creceivemx()
  • Readmsgmx();
  • Receivemx();
  • Replymx();
  • Sendmx();
  • Writemsgmx().

Составные сообщения могут быть описаны с помощью специальной mx структуры. Микроядро объединяет части такого сообщения в единый непрерывный поток данных

Составные сообщения могут быть описаны с помощью специальной mx структуры. Микроядро объединяет части такого сообщения в единый непрерывный поток данных

Зарезервированные коды сообщений

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

Зарезервированный диапазон:Описание:
0x0000 - 0x0000Сообщения Менеджера процессов
0x0100 - 0x01FFСообщения ввода/вывода (общие для всех серверов ввода/вывода)
0x0200 - 0x02FFСообщения Менеджера файловой системы
0x0300 - 0x03FFСообщения Менеджера устройств
0x0400 - 0x04FFСообщения Менеджера сети
0x0500 - 0x0FFFЗарезервированы для будущих системных процессов QNX

IPC посредством прокси

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

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

Вот некоторые примеры использования прокси:

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

Для создания прокси используется функция языка Си qnx_proxy_attach(). Любой другой процесс или обработчик прерывания, которому известен идентификатор прокси, может воспользоваться функцией языка Си Trigger() для того, чтобы заставить прокси передать заранее заданное сообщение. Запрос Trigger() обрабатывается Микроядром.

Прокси может быть "запущено" неоднократно - каждый раз при этом оно посылает сообщение. Прокси может накапливать очередь длиной до 65535 сообщений.


Процесс-клиент запускает прокси 3 раза,  в результате чего сервер получает 3 консервированных сообщения от прокси

Процесс-клиент запускает прокси 3 раза, в результате чего сервер получает 3 "консервированных" сообщения от прокси

IPC посредством сигналов

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

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

Как породить сигнал

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

Если вы хотите:Используйте:
Породить сигнал из командной строкиУтилиты: kill и slay
Породить сигнал внутри процессаФункции Си: kill() и raise()

Получение сигналов

Процесс может принять сигнал одним из трех способов, в зависимости от того, как в процессе определена обработка сигналов:

  • если процесс не определил никаких специальных мер по обработке сигнала, то выполняется предусмотренное для сигнала действие по умолчанию - обычно таким действием по умолчанию является завершение работы процесса;
  • процесс может игнорировать сигнал. Если процесс игнорирует сигнал, то сигнал не оказывает на процесс никакого воздействия (учтите, что SIGCONT, SIGSTOP и SIGKILL не могут быть игнорированы в обычных обстоятельствах);
  • процесс может предусмотреть обработчик сигнала - функцию, которая будет вызываться при приеме сигнала. Если процесс содержит обработчик для какого-либо сигнала, говорят, что процесс может "поймать" этот сигнал. Любой процесс, который "ловит" сигнал, фактически получает особый вид программного прерывания. Никакие данные с сигналом не передаются.

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

Перечень сигналов

Сигнал:Описание:
SIGABRTСигнал ненормального завершения, порождается функцией abort()
SIGALRMСигнал тайм-аута, порождается функцией alarm()
SIGBUSУказывает на ошибку контроля четности оперативной памяти (особая для QNX интерпретация). Если во время выполнения обработчика данного сигнала произойдет вторая такая ошибка, то процесс будет завершен
SIGCHLDЗавершение порожденного процесса. Действие по умолчанию - игнорировать сигнал
SIGCONTЕсли процесс в состоянии HELD, то продолжить выполнение. Действие по умолчанию - игнорировать сигнал, если процесс не в состоянии HELD
SIGDEVГенерируется, когда в Менеджере устройств происходит важное и запрошенное событие
SIGFPEОшибочная арифметическая операция (целочисленная или с плавающей запятой), например, деление на ноль или операция, вызвавшая переполнение. Если во время выполнения обработчика данного сигнала произойдет вторая такая ошибка, то процесс будет завершен
SIGHUPГибель процесса, который был ведущим сеанса, или зависание управляющего терминала
SIGILLОбнаружение недопустимой аппаратной команды. Если во время выполнения обработчика данного сигнала произойдет вторая такая ошибка, то процесс будет завершен
SIGINTИнтерактивный сигнал "внимание" (Break)
SIGKILLСигнал завершения - должен быть использован только в экстренных ситуациях. Этот сигнал не может быть "пойман" или игнорирован. Сервер может защитить себя от этого сигнала посредством функции языка Си qnx_pflags(). Для этого сервер должен иметь статус привилегированного пользователя
SIGPIPEПопытка записи в программный канал, который не открыт для чтения
SIGPWRПерезапуск компьютера в результате нажатия CtrlAltShift-Del или вызова утилиты shutdown
SIGQUITИнтерактивный сигнал завершения
SIGSEGVОбнаружение недопустимой ссылки на оперативную память. Если во время выполнения обработчика данного сигнала произойдет вторая такая ошибка, то процесс будет завершен
SIGSTOPСигнал приостановки выполнения (HOLD) процесса. Действие по умолчанию - приостановить процесс. Сервер может защитить себя от этого сигнала посредством функции языка Си qnx_pflags(). Для этого сервер должен иметь статус привилегированного пользователя
SIGTERMСигнал завершения
SIGTSTPНе поддерживается QNX
SIGTTINНе поддерживается QNX
SIGTTOUНе поддерживается QNX
SIGUSR1Зарезервирован как определяемый приложением сигнал 1
SIGUSR2Зарезервирован как определяемый приложением сигнал 2
SIGWINCHИзменился размер окна

Управление обработкой сигналов

Чтобы задать желаемый способ обработки для каждого из сигналов, вы можете использовать функции языка Си signal() стандарта ANSI или sigaction() стандарта POSIX.

Функция sigaction() предоставляет большие возможности по управлению обработкой сигналов.

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

Обработчики сигналов

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

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

Если в вашей программе не предусмотрен возврат из обработчика сигнала, то могут быть использованы функции siglongjmp() либо longjmp() языка Си, однако, функция siglongjmp() предпочтительнее. При использовании функции longjmp() сигнал остается блокированным.

Блокирование сигналов

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

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


Note: Реализация обработчиков сигналов в некоторых UNIX системах отличается тем, что при получении сигналов они не блокируют его, а устанавливают действие по умолчанию. В результате некоторые UNIX приложения вызывают функцию signal() изнутри обработчика сигналов, чтобы подготовить обработчик к следующему вызову. Такой способ имеет два недостатка. Во-первых, если следующий сигнал поступает, когда ваша программа уже выполняет обработчик сигнала, но еще не вызвала функцию signal(), то программа может быть завершена. Во-вторых, если сигнал поступает сразу после вызова функции signal() в обработчике, то возможен рекурсивный вход в обработчик сигнала. QNX поддерживает блокирование сигналов и, таким образом, не страдает ни от одного из этих недостатков. Вам не нужно вызывать функцию signal() изнутри обработчика сигналов. Если вы покидаете обработчик через дальний переход (long jump), вам следует использовать функцию siglongjmp().

Сигналы и сообщения

Существует важное взаимодействие между сигналами и сообщениями. Если ваш процесс SEND-блокирован или RECEIVE-блокирован в момент получения сигнала, и вы предусмотрели обработчик сигнала, происходят следующие действия:

  1. Процесс разблокируется;
  2. Выполняется обработка сигнала;
  3. Функция Send() или Receive() возвратит код ошибки.

Если процесс был SEND-блокирован в момент получения сигнала, то проблемы не возникает, потому что получатель еще не получил сообщение. Но если процесс был RECEIVE-блокирован, то вы не узнаете, было ли обработано посланное им сообщение и, следовательно, не будете знать, следует ли повторять Send().

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

  • закончить выполнение запроса от клиента нормальным образом: клиент (процесс-отправитель) будет уведомлен, что его сообщение было обработано должным образом

или

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

Когда сервер отвечает процессу, который был SIGNAL-блокирован, то сигнал выдается непосредственно после возврата из функции Send(), вызванной клиентом (процессом-отправителем).

IPC в сети

Виртуальные каналы

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

Такая замечательная степень прозрачности становится возможна благодаря виртуальным каналам (от английского virtual circuit, сокращенно VC). VC - это пути, которые Менеджер сети предоставляет для передачи сообщений, прокси и сигналов по сети. VC способствуют эффективному использованию QNX-сети в целом по нескольким причинам:

  1. Когда создается VC, он получает возможность обрабатывать сообщения вплоть до определенного размера; это означает, что вы можете предварительно выделить ресурс для обработки сообщения. Тем не менее, если вам необходимо послать сообщение, превышающее заданный максимальный размер, VC автоматически увеличивает размер буфера.
  2. Если два процесса, располагающиеся на различных узлах сети, используют для связи друг с другом более чем один VC, то такие VC будут разделяемыми, так как между процессами будет реально существовать только один виртуальный канал. Такая ситуация обычно имеет место, когда процесс использует несколько файлов на удаленной файловой системе.
  3. Если процесс присоединяется к существующему разделяемому VC и запрашивает буфер большего размера, чем тот, который используется, то размер буфера автоматически увеличивается.
  4. Когда процесс завершает выполнение, все связанные с ним VC автоматически освобождаются.

Виртуальные процессы

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

Например, на следующем рисунке виртуальный канал соединяет PID 1 и PID 2. На узле 20 - где находится PID 1 - VID представляет PID 2. На узле 40 - где находится PID 2 - VID представляет PID 1. И PID 1 и PID 2 могут обращаться к VID на своем узле так же, как к любому другому локальному процессу, посылая сообщения, принимая сообщения, поставляя сигналы, ожидая и т.д. Так, например, PID 1 может послать сообщение VID на своем конце, и этот VID передаст сообщение по сети к VID, представляющему PID 1 на другом конце. Этот VID затем отправит сообщение к PID 2.


Связь по сети реализуется посредством виртуальных каналов. Когда PID 1 посылает данные VID 2, посланный запрос передается по виртуальному каналу, в результате VID 1 направляет данные PID 2

Связь по сети реализуется посредством виртуальных каналов.Когда PID 1 посылает данные VID 2, посланный запрос передается по виртуальному каналу, в результате VID 1 направляет данные PID 2

Каждый VID поддерживает соединение, которое имеет следующие атрибуты:

  • локальный pid;
  • удаленный pid;
  • удаленный nid (ID узла);
  • удаленный vid.

Возможно, вам не придется часто иметь дело непосредственно с VC. Так, например, когда приложение хочет получить доступ к ресурсу ввода/вывода на другом узле сети, то VC создается библиотечной функцией open() от имени приложения. Приложение не принимает непосредственного участия в создании или использовании VC. Аналогичным образом, когда приложение определяет местонахождение сервера с помощью функции qnx_name_locate(), то VC автоматически создается от имени приложения. Для приложения VC просто является PID сервера.

Для более подробной информации о функции qnx_name_locate() смотрите описание "Символьные имена процессов" в главе "Менеджер процессов".

Виртуальные прокси

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

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

Виртуальные прокси создаются функцией qnx_proxy_rem_attach(), которой в качестве аргументов передаются узел (nid_t) и прокси (pid_t).


"

На удаленном узле создается виртуальный прокси, который ссылается на локальный прокси.

Имейте в виду, что на вызывающем узле виртуальный канал создается автоматически посредством функции qnx_proxy_rem_attach().

Отключение виртуальных каналов

Процесс может утратить возможность связи по установленному VC в результате различных причин:

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

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

Менеджер процессов на каждом узле проверяет целостность VC на своем узле. Он делает это следующим образом:

  1. Каждый раз, когда происходит успешная передача по VC, обновляется временная метка, связанная с данным VC, в соответствии со временем последней операции.
  2. Через определенные промежутки времени Менеджер процессов проверяет каждый VC. Если не было зафиксировано никаких операций, то Менеджер процессов посылает проверочный пакет Менеджеру процессов на другом конце VC.
  3. Если ответ не получен или выявлен сбой, то VC помечается как неисправный. Затем делается определенное количество попыток восстановить соединение.
  4. Если эти попытки не дают результата, то VC демонтируется; все процессы, блокированные VC, переходят в состояние READY (готов). Процесс получает код ошибки, возвращаемой ранее вызванной функцией связи.

Для установки параметров, связанных с данной проверкой целостности VC, используйте утилиту netpoll.

IPC посредством семафоров

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

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

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

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

Когда принимаются решения по диспетчеризации

Планировщик Микроядра принимает решение по диспетчеризации:

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

Приоритеты процессов

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


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

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

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

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

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

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

  • FIFO;
  • карусель;
  • адаптивный.

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


Выполняется процесс A.  Прерывание IRQ<em>x</em> вызывает выполнение обработчика прерывания Int<em>x</em>, который вытесняется IRQ<em>y</em> и его обработчиком Int<em>y</em>.  Int<em>y</em> запускает прокси, вызывающее выполнение процесса B, а Int<em>x</em> запускает прокси, вызывающее выполнение процесса C

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

<< Концепция QNX| Оглавление |Менеджер процессов >>




Задать вопрос on-line Обсудить на форуме Написать электронное письмо