Pull to refresh

ОСРВ QNX: Межзадачное взаимодействие

Reading time10 min
Views26K
Продолжение цикла заметок об операционной системе реального времени QNX. В этот раз я хотел бы рассказать о межзадачном взаимодействии в QNX Neutrino (мы будем рассматривать QNX 6.5.0). В ОСРВ существует широкий набор механизмов межзадачного взаимодействия — от специфичного для QNX обмена сообщениями до знакомых разработчикам UNIX и POSIX сигналов и разделяемой памяти. И хотя большая часть заметки будет посвящена обмену сообщениями, но особенности использования сигналов, сообщений POSIX и разделяемой памяти будут также описаны. А дочитавшие до конца получат две плюшки к чаю.

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

Формы межзадачного взаимодействия ОСРВ QNX


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

Таблица 1. Формы межзадачного взаимодействия.
Механизм Область реализации
Обмен сообщениями микроядро
Сигналы микроядро
Разделяемая память администратор процессов procnto
Очереди сообщений POSIX менеджер mqueue
Неименованные (pipe) и именованные (FIFO) программные каналы менеджер pipe

В ОСРВ QNX 6.5.0 появилась ещё одна форма межзадачного взаимодействия — Persistent Publish/Subscribe (PPS). Это довольно интересная технология, о которой я постараюсь написать в другой раз. Заинтересовавшиеся могут прочитать перевод раздела PPS из «Системной архитектуры QNX Neutruno».

Обмен сообщениями


Это синхронный механизм межзадачного взаимодействия реализованный в микроядре QNX Neutrino. При разработке ОСРВ такая форма межзадачного взаимодействия не случайно была выбрана в качестве основной. Во-первых, сам механизм достаточно простой. Во-вторых, синхронность передачи и приёма сообщений облегчает отладку. В-третьих, тестирования высокоуровневых форм взаимодействия (например, программных каналов), основанных на обмене сообщениями в QNX Neutrino и реализованных в монолитном ядре, выявили приблизительно одинаковые характеристики производительности.

Механизм обмена сообщениями в QNX также называют SRR-механизм, по первым буквам трёх основных функций, применяемых при обмене сообщениями1. MsgSend() служит для отправки сообщения, MsgReceive() — для приёма сообщения, и MsgReply() — для передачи ответа вызывающей стороне. Сначала рассмотрим каждую функцию по-отдельности, чтобы понять, как они работают, а затем объединим их в одном примере. Все аргументы функций пока не будут приводиться намеренно, чтобы не отвлекаться и сначала понять сам принцип работы.

MsgSend() служит для передачи сообщения от клиента к серверу и получения ответа. Сами понятия клиента и сервера здесь достаточно условны, т.к. одна и та же программа может быть сервером для одних задачи и, в тоже самое время, клиентом других. Например, сервер базы данных является сервером для клиентов БД2. И в тоже самое время сервер БД будет клиентом для менеджера файловой системы. При вызове функции MsgSend() клиент блокируется в одном из двух состояний: SEND или REPLY. Состояние SEND означает, что клиент отправил сообщение, а сервер его ещё не принял. После того, как сервер принимает сообщение, клиент переходит в состояние REPLY. Когда сервер вернёт сообщение с ответом, клиент разблокируется.

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

MsgReply() используется для передачи сообщения с ответом клиенту3. При вызове функции MsgReply() блокировка не происходит, т.е. сервер будет продолжать работать дальше. Это сделано потому, что клиент уже находится в заблокированном состоянии (REPLY) и не требуется какая-либо дополнительная синхронизация.

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

Микроядру QNX Neutrino нет никакого дела до содержимого передаваемого сообщения. У сообщений нет какого-то формата. Сообщение имеет смысл только для клиента и сервера. Микроядро только копирует сообщение (т.е. просто буфер данных) из адресного пространства клиента, в адресное пространство сервера (и наоборот при ответе) и нет никакого промежуточного буфера для хранения сообщений. А значит и нет промежуточного копирования, т.к. микроядро копирует данные напрямую из памяти клиента в память сервера (и наоборот при ответе). Как следствие, увеличивается быстродействие механизма передачи сообщений.

Для синхронизации передачи, приёма и ответа на сообщение микроядро блокирует потоки, участвующие в обмене сообщениями в одном из трёх состояний: SEND, RECIEVE и REPLY. Клиент блокируется в состоянии SEND, пока сервер не примет его сообщение. Сервер блокируется в состоянии RECEIVE, если ни один из клиентов не передал ему сообщение. После приёма сообщения сервером, он разблокируется, а клиент переходит в блокированное состояние REPLY. После того, как сервер возвращает ответ клиенту, последний разблокируется. Вот и всё.

Итак, мы выяснили как работает механизм обмена сообщениями в QNX. Рис. 1 просто иллюстрирует вышесказанное.


Рис. 1. Обмен сообщениями в QNX Neutrino.

Как клиент находит сервер?


Если вам стал понятен принцип работы механизма обмена сообщениями в ОСРВ QNX, то можно двигаться дальше. Наверное, у вас должен появиться один вопрос, не решив который, вы не сможете обмениваться сообщениями в QNX. Как же клиент находит сервер?

Сообщения не передаются напрямую между потоками. Вместо этого используется каналы и соединения. Сервер создаёт канал с помощью функции ChannelCreate(). Теперь наконец-то сервер может с чистой совестью вызывать MsgReceive() и MsgReply(). Кусочек кода ниже иллюстрирует работу сервера:

chid = ChannelCreate( flags );
/* Не забываем проверять chid на -1 */

for (;;)
{
    rid = MsgReceive( chid, &msg, sizeof( msg ), NULL );
    /* Не забываем проверять rid на -1 */

    switch ( msg.type )
    {
        /* Обрабатываем сообщение */
    }

    MsgReply( rid, EOK, NULL, 0 );
    /* Тут тоже стоит проверять, как отработала функция */
}

В свою очередь, клиент создаёт соединение к каналу сервера используя ConnectAttach(), а потом уже вызывает MsgSend(). Код клиента совсем простой:

coid = ConnectAttach( nd, pid, chid, _NTO_SIDE_CHANNEL, 0 );
/* Не забываем проверять coid на -1 */

/* Готовим сообщение */

MsgSend( coid, smsg, sizeof( smsg ), rmsg, sizeof( rmsg ) );
/* Тут тоже стоит проверять, как отработала функция */

/* Обрабатываем ответ */

Теперь остаётся последний вопрос. Откуда клиент узнаёт параметры сервера: nd, pid, chid? Эти параметры представляют собой адрес сервера или даже номер телефона с кодом города и добавочным номером. Половина ответа на этот вопрос заключается в том, что сам сервер знает все эти параметры. Но как сервер может сообщить их клиенту?

Существуют разные способы получить эту информацию от сервера. Можно использовать .pid файлы или глобальные переменные. Но правильный способ для небольших приложений это использование функции name_attach() в сервере, и name_open() в клиенте. Ещё более правильный способ это реализация сервера в виде менеджера ресурсов4, когда он отвечает за элемент пространства имён.

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


Как уже говорилось, одним из главных достоинств механизма обмена сообщениями в QNX Neutrino является его высокая производительность. Это достигается тем, что отсутствует промежуточное копирование данных, т.е. сообщение копируется напрямую из области памяти клиента в память сервера. Зачастую бывает так, что данные сообщения находятся в разных местах. Типичный случай, когда данные представляют собой сырой буфер принятый от аппаратуры и структуру с информацией о данных (заголовок сообщения). Сами сырые данные могут располагаться в кольцевом буфере. Неужели в этом случае надо предварительно подготовить отдельный буфер и скопировать туда заголовок и данные из кольцевого буфера? Не будет ли это лишним копированием? Этого лишнего копирования данных перед отправкой сообщения можно избежать в ОСРВ QNX, если использовать составные сообщения.

Для формирования составных сообщений в QNX Neutrino надо объявить массив типа iov_t, с количеством элементов равным (или большим) количеству сообщений, и проинициализировать каждый элемент при помощи макроса SETIOV(), т.е. указать адрес и размер каждого буфера. Рис. 2 иллюстрирует принцип работы составных сообщений.


Рис. 2. Пример составного сообщения.

Для работы с составными сообщениями используются уже знакомые функции MsgReceive() и MsgReply(), но с окончанием v, т.е. MsgReceivev() и MsgReplyv(). Поскольку функция отправки сообщения MsgSend() ещё и принимает результат, то она обрастает целым семейством: MsgSendv(), MsgSendsv() и MsgSendvs(). Теперь микроядро сделает за нас всю лишнюю работу, и никакого дополнительного копирования и буфера с целым сообщением. Вот это мне нравится!

Импульсы


Иногда требуется только сообщить другому потоку, что что-то произошло, а ответ при этом не требуется. Значит не нужно и блокирование на MsgSend(). В этом случае на помощь приходит функция MsgSendPulse(). Импульс содержит 8 бит кода и 32 бита данных. Очень часто импульсы используются в обработчиках прерываний. Для импульсов используются очереди, т.е. импульсы не потеряются, если поток какое-то время не принимал их. Но будьте готовы рано или поздно получить ошибку EAGAIN, если вы посылаете импульсы потоку, который их не успевает вычитывать.

Сигналы


ОСРВ QNX Neutrino поддерживает механизм сигналов, который должен быть знаком разработчикам UNIX. Поддерживаются как стандартные сигналы POSIX, так и сигналы реального времени POSIX. Для работы с обоими типами сигналов используется один и тот же код микроядра. В следствии чего само микроядро становится компактнее, а сигналы POSIX (по запросу приложения) могут ставиться в очередь, как и их коллеги из группы сигналов реального времени POSIX. Помимо прочего, QNX Neutrino расширяет стандарт POSIX и позволяет посылать сигналы определённому потоку. А это иногда бывает очень полезно.

Кстати, сигналы реального времени POSIX содержат 8 бит кода и 32 бита данных. Ничего не напоминает? Точно, тот же самый код, который реализует механизм сигналов, используется и при передаче импульсов. Удобно и надёжно.

Для работы с сигналами используются знакомые функции kill(), sigaction(), sigprocmask() и др., а также более интересные, например, pthread_kill() и pthread_sigmask().

Программные каналы


Программные каналы должны быть знакомы пользователям и разработчикам UNIX. В QNX все знакомые команды, функции и приёмы тоже есть. Например, вот так создаётся неименованный программный канал (pipe) в командной строке:

# ls -l | less

А чтобы создать именованный канал (FIFO), надо воспользоваться командой mkfifo или функцией mkfifo().

Есть только одна особенность. Для того, чтобы в QNX Neutrino можно было работать с программными каналами, необходимо запустить менеджер pipe.

Очереди сообщений POSIX


Очереди сообщений похожи на именованные программные каналы (FIFO), но являются более сложным механизмом, т.к. поддерживают приоритеты сообщений. Чтобы работать с очередями сообщений POSIX в QNX Neutrino, необходимо запустить менеджер mqueue.

Обратите внимание, что очереди сообщений в ОСРВ QNX могут содержать более одного символа косой черты '/', а значит можно создавать каталоги. Таким образом расширяется стандарт POSIX, который требует, чтобы имя очереди начиналось с косой черты и больше не содержало этого символа. Достаточно удобная особенность, т.к. можно сгруппировать очереди одного программного комплекса или одной фирмы в одном каталоге.

Разделяемая память


Работа с разделяемой памятью в QNX Neutrino ведётся также как и в других UNIX системах. Поскольку механизм разделяемой памяти реализован в администраторе процессов procnto, который также содержит и микроядро, то дополнительно запускать ничего не нужно. Подробнее можно почитать в документации на функции shm_open() и mmap().

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

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

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

Обещанные плюшки


Раз уж я обещал, что будут плюшки, то они будут. Я вовсе не забыл, и мне не жалко. Первая плюшка заключается в том, что при отправке сообщения можно использовать один и тот же буфер для самого сообщения и ответа на него. Я бы даже сказал, что это довольно часто делают. Чтобы было удобнее все структуры, описывающие различные сообщения и ответы на них группируются в одно объединение (union). Обычно отправляемое сообщение не требуется клиенту после получения ответа от сервера. Таким образом можно сэкономить на буфере.

Вторая плюшка заключается в том, что файловый дескриптор POSIX в QNX Neutrino это тоже самое, что и соединение (для отправки сообщений). А это значит, что функции, использующие файловые дескрипторы (write(), read() и т.п.) это просто обёртки с незначительными накладными расходами преобразующие свои аргументы в сообщения к серверу. Достаточно серьёзная оптимизация. Так что, если хотите разрабатывать системное ПО для QNX, то учитесь писать менеджер ресурсов.

Список литературы

  1. Операционная система реального времени QNX Neutrino 6.3. Системная архитектура. ISBN 5-94157-827-X
  2. Операционная система реального времени QNX Neutrino 6.3. Руководство пользователя. ISBN 978-5-9775-0370-9
  3. Роб Кртен, «Введение в QNX Neutrino 2. Руководство для разработчиков приложений реального времени», 2-е издание. ISBN 978-5-9775-0681-6


1 Префикс Msg в названии функций MsgSend() и других появился только в QNX Neutrino, а в QNX4 эти функции назывались просто Send(), Receive() и Reply(). Отсюда и название SRR.

2 В примере сервер и клиенты БД не используют функции обмена сообщениями напрямую, но мы то уже знаем, что в QNX под каждым read(), write(), sendto() и др. спрятан механизм SRR.

3 Если сервер должен вернуть только код ошибки, например, в том случае, если сообщение не поддерживается, то удобнее использовать функцию MsgError().

4 Описание менеджеров ресурсов выходит за рамки этой заметки. Вполне возможно, я напишу об этом как-нибудь в другой раз.
Tags:
Hubs:
+44
Comments6

Articles

Change theme settings