В данном приложении мы рассмотрим предыдущую операционную систему разработки QSSL, QNX4, и сравним ее с QNX/Neutrino. Данное приложение будет вам интересно главным образом в том случае, если вы уже являетесь пользователем QNX4 и хотите узнать.
• что такого замечательного в QNX/Neutrino?
• какие сложности связаны с переносом программного обеспечения в QNX/Neutrino?
а также если вы разрабатываете (или портируете) программное обеспечение для обеих операционных систем.
Давайте начнем со общих черт этих двух операционных систем:
• архитектура на основе обмена сообщениями;
• распределенный обмен сообщениями в сети;
• реальное время;
• микроядерная архитектура;
• защита памяти на уровне процессов;
• POSIX-совместимость;
• относительно простая модель «драйвера устройства»;
• встраиваемость.
Заметьте, что хоть часть вышеперечисленных базовых свойств действительно подобны в этих двух ОС, в целом QNX/Neutrino обеспечивает более расширенную поддержку. Например, поддержки POSIX в QNX/Neutrino больше, чем в QNX4, — просто потому что многие из стандартов этой серии на момент выхода QNX4 были еще только в стадии разработки. И хотя в процессе разработки QNX/Neutrino в состоянии разработки находилась гораздо меньшая их часть, постоянно появляются новые. Эта бесконечная игра в догонялки.
Теперь, когда вы выяснили, что общего между этими двумя ОС, давайте посмотрим, каковы преимущества QNX/Neutrino перед QNX4:
• большее число поддерживаемых стандартов POSIX;
• лучше встраивается;
• ядро лучше конфигурируется для других аппаратных платформ;
• поддержка многопоточности;
• более простая модель драйвера устройства;
• переносимая архитектура (в настоящее поддерживаются, кроме x86, процессоры MIPS и PPC) (а теперь уже и ARM, StrongARM и SuperH-4 — прим. ред.),
• поддерживает SMP;
• лучше документирована.
При том, что некоторые из этих усовершенствований «вне сравнения», поскольку аналогов для них в QNX4 нет, а значит, нет и проблем совместимости (например, многопоточность POSIX не поддерживалась в QNX4), некоторые из проблем потребовали внесения в ОС кардинальных изменений. Сначала я вкратце упомяну, какие классы изменений были необходимы, а затем мы подробно рассмотрим возникшие в связи с этим проблемы совместимости и предложения о том, как переносить программы в QNX/Neutrino (или заставить программу работать в обеих ОС).
В QNX/Neutrino коренным образом пересмотрена стратегия встраивания. В первоначальном варианте QNX4 встраиваемость обеспечивалась только частично. Затем появилась QNX/Neutrino, которая с самого начала разрабатывалась как встраиваемая. В качестве премии, QNX4 подверглась некоторой модернизации на основе опыта, полученного с QNX/Neutrino, и теперь является в гораздо более значительной степени встраиваемой, чем ранее. Как бы там ни было, по части встраиваемости QNX/Neutrino и QNX4 отличаются, как день и ночь. QNX4 не содержит никакой реальной поддержки таких вещей как:
• исходящие вызовы ядра (kernel callouts) (прерывание, таймер);
• настройка стартового кода;
• образная файловая система.
а вот в QNX/Neutrino все это есть. Подробное описание методик встраивания QNX/Neutrino приведено в книге «Построение встраиваемых систем», входящей в комплект документации.
В QNX4 есть функция, называемая tfork(), которая позволяет вам организовать «многопоточность», создавая процесс с сегментами кода и данных, отображенными в то же адресное пространство, что и у родительского. Создание процесса и потом приведение его к «потокоподобному» виду дает иллюзию создания потока. И хотя в системе обновлений QSSL содержится библиотека поддержки потоков для QNX4, само ядро потоки непосредственно не поддерживает.
В QNX/Neutrino для организации многопоточности применяется POSIX-модель «рthread». Это означает, что там вы увидите (и уже видели в данной книге) знакомые вызовы функций типа pthread_create(), pthread_mutex_lock(), и т.п.
При том что воздействие потоков на обмен сообщениями может показаться минимальным, использование потоков привело к фундаментальному изменению реализации механизма обмена сообщениями (не самой концепции SEND/RECEIVE/REPLY, а именно ее реализации).
В QNX4 сообщения адресовались идентификатору процесса. Чтобы отправить сообщение, нужно было просто найти идентификатор процесса-адресата и выполнить Send(). Чтобы сервер мог принять сообщение в QNX4, он должен был вызвать Receive(). Это блокировало его до прибытия сообщения. Затем сервер отвечал с помощью функции Reply().
В QNX/Neutrino обмен сообщениями устроен так же, только при этом используются другие имена функций. Что изменилось, так это сам механизм. Теперь, прежде чем вызывать стандартные функции обмена сообщениями, клиент должен сперва создать соединение с сервером. А сервер, прежде чем он сможет выполнять стандартные функции обмена сообщениями, должен сначала создать канал.
Заметьте, что существовавшая в QNX4 функция Creceive(), которая выполняла неблокирующий вызов Receive(), в QNX/Neutrino отсутствует. Мы вообще не одобряем такие «опрашивающие» функции, особенно когда можно запустить поток; впрочем, если вы настаиваете на выполнении неблокирующего вызова MsgReceive(), посмотрите главу «Часы, таймеры и периодические уведомления», раздел «Тайм-ауты ядра». Вот соответствующий пример кода в качестве краткого пояснения:
TimerTimeout(CLOCK_REALTIME, _NTO_TIMEOUT_RECEIVE,
NULL, NULL, NULL);
rcvid = MsgReceive(...
В QNX4 было нечто по имени «прокси». Прокси лучше всего описывается как «законсервированное» (т.е. неизменяемое) сообщение, которое можно отослать владельцу этой (почему-то исторически сложилось, что в русском языке термин «прокси» женского рода; возможно, из-за распространенного сленгового произношения «прокся» #:о) — прим. ред.) прокси от имени процесса или ядра (например, по срабатыванию таймера или из обработчика прерывания). Прокси является неблокирующей для отправителя и принимается точно также, как и любое другое сообщение. Распознать сообщение прокси (то есть отличить его от обычного сообщения) можно было либо проанализировав его содержимое (не самый надежный способ, поскольку процесс тоже может передать что-нибудь с подобным содержимым), либо проверив идентификатор процесса, от которого оно было получено. Если идентификатор процесса совпадает с идентификатором прокси — значит, это прокси, потому что идентификаторы процессов и прокси берутся из того же самого пула номеров и не пересекаются.
(На самом деле, документация по QNX4 вносит в понятие «прокси» страшную путаницу, которая требует пояснения. Если бы прокси являлась именно сообщением, как это записано в определении прокси, обороты типа «получить сообщение от прокси» были бы лишены смысла. Само по себе слово «proxy» переводится как «промежуточный агент». Если внимательно прочитать главу «IPC via proxies» книги «QNX4 System Architecture», становится ясно, что прокси — это не само сообщение, а специализированный «квазипроцесс», не обладающий ресурсами, но имеющий идентификатор (который, разумеется, по понятным причинам не перекрывается с другими идентификаторами процессов) и способный к обмену специализированными сообщениями. Применяя к прокси с определенным идентификатором функцию Trigger(), процесс в QNX4 фактически делает этому квазипроцессу специализированный Send(), в ответ на который мгновенно следует Reply(), поэтому отправитель (вызвавший Trigger()) и не блокируется. Затем прокси отправляет своему процессу-владельцу свое предопределенное сообщение; если владелец прокси в этот момент не является RECEIVE-блокированным, сообщение становится в очередь. Таким образом, правильнее было бы говорить «переключить прокси» вместо «отправить прокси» (о вызове Trigger()) и «получить сообщение прокси» вместо «получить прокси» — прим. ред.).
QNX/Neutrino расширяет концепцию прокси введением «импульсов». Импульсы по-прежнему являются неблокирующими сообщениями, и их по-прежнему можно переслать либо от потока к потоку, либо от служебной функции ядра (например, от того же таймера или обработчика прерывания) к потоку. Различие состоит в том, что в то время как прокси имели фиксированное содержимое, импульсы имеют фиксированную длину, но содержимое их может быть изменено отправителем в любой момент. Например, обработчик прерываний (ISR) может сохранить в импульсе какие-либо данные и затем отправить этот импульс потоку.
В QNX4 некоторые сервисы были наделены способностью послать как сигнал, так и прокси, в то время как другие сервисы были наделены способностью послать либо только одно, либо только другое. Мало того, обычно это выполнялось несколькими различными способами. Например, чтобы доставить сигнал, вы должны были применить функцию kill(). Чтобы доставить прокси или сигнал по срабатыванию таймера, вы должны были использовать отрицательный номер сигнала (для указания на то, что это прокси) или положительный номер сигнала (для указания на то, что это сигнал). И, наконец, обработчик прерываний мог доставлять только прокси.
В QNX/Neutrino все это было абстрагировано в расширение POSIX-структуры
struct sigevent
. Все, что использует и возвращает struct sigevent
, может использовать как сигнал, так и импульс.
На самом деле, функциональность
struct sigevent
была расширена вплоть до возможности создания потока! Мы говорили об этом в Главе «Часы, таймеры и периодические уведомления» в разделе «Уведомление созданием потока».
В самых ранних версиях QNX (семейство QNX2) программирование драйверов устройств было чем-то из области черной магии. В QNX4 это тоже поначалу была загадочная вещь, но затем, в конце концов, появилось несколько примеров программ. В QNX/Neutrino этому вопросу посвящены книги и учебные курсы. И, как оказалось, модели драйверов в QNX/ Neutrino и QNX4 на архитектурном уровне достаточно сходны. В то время как в QNX4 была кромешная путаница вокруг того, что такое «функции установления соединения» и что такое «функции ввода-вывода», в QNX/Neutrino все четко разделено. Также под QNX4 вы (разработчик драйвера устройства) должны были выполнять большую часть работы (программирование основного цикла обработки сообщений, сопоставление контекста каждому сообщению ввода-вывода, и т.д.) самостоятельно. В QNX/Neutrino все это в значительной степени упрощено введением библиотеки администратора ресурсов.
Одно из самых существенных отличий QNX/Neutrino от QNX4 в отношении встраиваемости состоит в том, что QNX/Neutrino поддерживает процессоры MIPS и PPC (Power PC). QNX4 изначально была «дома» на IBM PC с их BIOS и «очень» стандартным набором аппаратных средств, QNX/Neutrino же одинаково по-домашнему чувствует себя на разных платформах с BIOS (или монитором ПЗУ) или без нее, а также на нестандартной аппаратуре, комплектация которой выбирается изготовителем (и часто без учета требований ОС). Это означает, что ядро QNX/Neutrino должно было обеспечивать поддержку исходящих вызовов (callouts), чтобы вы могли, например, задать свой тип контроллера прерываний и работать на этих аппаратных средствах без необходимости приобретения лицензии на исходный текст операционной системы.
Другая группа изменений, которые вы заметите при переносе приложений из QNX4 в QNX/Neutrino, особенно на другие платформы, касается того, что процессоры MIPS и PPC уж больно вычурны по части выравнивания. Вы не можете обращаться к N-байтовому объекту иначе чем по адресу, кратному N. При работе на x86 (с выключенным флагом выравнивания) вы бы волей-неволей обратились к памяти. Изменив вашу программу так, чтобы все структуры были правильно выровнены (для не-x86 процессоров), вы также заметите, что ваша программа после этого на x86 станет выполняться быстрее, потому что x86 быстрее обращается к выровненным данным.
Другое, что так часто не дает людям жить — это порядок следования байт, прямой (big-endian) или обратный (little-endian). (Кому интересна этимология этих терминов, загляните в английский оригинал «Путешествий Гулливера» Джонатана Свифта :-) — прим. ред.). У процессора x86 возможен только один порядок следования байт, и он обратный. Процессоры MIPS и PPC допускают оба порядка следования байт, т. е. могут работать как с прямым, так и с обратным. Кроме того, процессоры MIPS и PPC являются процессорами типа RISC (с сокращенным набором команд), что означает, что некоторые операции типа
|=
в Си (установка бита) могут и не быть атомарными. Это иногда приводит к жутким последствиям! Список вспомогательных функций, которые обеспечивают атомарность выполняемой операции, приведен в файле
.
Существующие версии QNX4 работают только на однопроцессорных системах, в то время как QNX/Neutrino уже на момент публикации первого издания этой книги обеспечивала поддержку SMP по меньшей мере на архитектуре x86. SMP дает значительные преимущества, особенно в многопоточной ОС, но это одновременно и гораздо более серьезный пистолет для простреливания ноги (кто любопытен, поищите в Интернет по ключевой фразе «shoot yourself in the foot» — прим. ред.).
Например, в однопроцессорной машине обработчик прерывания (ISR) может вытеснить поток, но наоборот никогда не бывает. В однопроцессорной машине бывает полезно представить себе, что потоки якобы выполняются одновременно, хотя в действительности это не так.
В блоке SMP поток и обработчик прерывания могут работать одновременно, да и несколько потоков тоже могут работать одновременно. Так что SMP — это не только превосходная рабочая станция, но и неплохое средство тестирования программного обеспечения — если вы сделали какие бы то ни было «плохие» предположения о защите в многопоточной среде, в SMP-системе они однажды обязательно выплывут.
Для иллюстрации того, насколько это верно, один пример. Одна из ошибок в ранней внутренней версии поддержки SMP проявлялась в «окне» длиной в один машинный цикл! То, что для однопроцессорной машины было запрограммировано как атомарная операция «чтение/модификация/запись», в SMP-блоке стало допускать вмешательство в ход операции второго процессора, выполняющего «сравнение/обмен».
Давайте теперь взглянем на все «сверху». Здесь мы рассмотрим:
• обмен сообщениями и систему «клиент/сервер»;
• обработчики прерываний (ISR).
В QNX4 клиент мог найти сервер двумя способами:
• используя глобальное пространство имен;
• выполнение open() в отношении администратора ввода/вывода.
Если взаимоотношения «клиент/сервер, которые вы переносите, базируются на глобальном пространстве имен, тогда клиент использует:
qnx_name_locate()
а сервер регистрирует свое имя при помощи:
qnx_name_attach()
В этом случае у вас есть два выбора. Вы можете либо попробовать сохранить вариант с глобальными именами, либо модифицировать клиента и сервер так, чтобы они работали подобно стандартному администратору ресурсов.
Я рекомендую вам последний вариант, поскольку именно этот вариант характерен для QNX/Neutrino — сводить все к администраторам ресурсов, а не пытаться навесить кусок администратора ресурсов на службу глобальных имен.
Модификация будет достаточно проста. Скорее всего, клиентская сторона вызывает функцию, либо возвращающую идентификатор серверного процесса, либо создающую виртуальный канал («VC» — Virtual Circuit) от клиентского узла к удаленному узлу сервера. В обоих случаях как идентификатор процесса, так и идентификатор виртуального канала к удаленному процессу определяются при помощи qnx_name_locate(). «Магическим амулетом», связывающим клиента с сервером, здесь является специальная разновидность идентификатора процесса (мы считаем идентификатор виртуального канала идентификатором процесса, поскольку он берется из того же пула номеров и со всех точек зрения выглядит как идентификатор процесса).
Преодолеть основное различие можно было бы, возвращая вместо идентификатора процесса идентификатор соединения. Поскольку клиент в QNX4, вероятно, не анализирует идентификаторы процессов (да и зачем? Так, просто число), вы могли бы «обмануть» его, применив к «глобальному имени» функцию open(). В этом случае, однако, глобальное имя должно было бы быть точкой монтирования, зарегистрированной администратором ресурса в качестве своего «идентификатора». Вот, например, типовой пример клиента QNX4, взятый из моей серверной библиотеки CLID:
/*
* CLID_Attach(ServerName)
*
* Эта подпрограмма отвечает за установление соединения
* с сервером CLID.
*
* Возвращает PID сервера CLID или идентификатор
* виртуального канала к нему.
*/
// Сюда запишется имя - для других библиотечных вызовов
static char CLID_serverName(MAX_CLID_SERVER_NAME + 1);
// Сюда запишется идентификатор сервера
CLID static int clid_pid = -1;
int CLID_Attach(char *serverName) {
if (ServerName == NULL) {
sprintf(CLID_serverName, "/PARSE/CLID");
} else {
strcpy(CLID_serverName, serverName);
}
clid_pid = qnx_name_locate(0, CLID_serverName,
sizeof(CLID_ServerIPC), NULL);
if (clid_pid != -1) {
CLID_IPC(CLID_MsgAttach); // Послать сообщение ATTACH
return (clid_pid);
}
return (-1);
}
Вы могли бы изменить это на следующее:
/*
* CLID_Attach(serverName), версия для QNX/Neutrino
*/
int CLID_Attach(char *serverName) {
if (ServerName == NULL) {
sprintf(CLID_serverName, "/PARSE/CLID");
} else {
strcpy(CLID_serverName, serverName);
}
return (clid_pid = open(CLID_serverName, O_RDWR));
}
И клиент ничего бы не заметил.
Два замечания по реализации. В качестве зарегистрированного префикса администратора ресурса я просто оставил имя по умолчанию («
/PARSE/CLID
»). Вероятно, лучше было бы взять имя «/dev/clid
», но насколько вы хотите следовать POSIX — это ваше личное дело. В любом случае, это незначительное изменение, и оно мало связано с тем, что здесь обсуждается.
Второе замечание касается того, что я по-прежнему назвал дескриптор файла clid_pid, хотя реально ему бы теперь следовало называться clid_fd. Это, опять же, вопрос стиля и касается только того, сколько различий вы хотите иметь между версиями кода для QNX4 и QNX/Neutrino.
В любом случае, того чтобы данная программа была переносима в обе ОС, вам придется выделить код соединения с сервером в отдельную функцию — как я это сделал выше с функцией CLID_Attach().
В какой-то момент клиент должен будет выполнить собственно операцию отправки сообщения. Здесь все становится несколько сложнее. Поскольку отношения клиент/сервер не основаны на отношениях с администраторами ввода/вывода, клиент обычно создает «нестандартные» сообщения. Снова пример из CLID-библиотеки («незащищенный» клиентский вызов здесь — CLID_AddSingleNPANXX(), я также включил функции checkAttach() и CLID_IPC() для того, чтобы продемонстрировать фактическую передачу сообщений и логику проверок):
/*
* CLID_AddSingleNPANXX(npa, nxx)
*/
int CLID_AddSingleNPANXX(int npa, int nxx) {
checkAttach();
CLID_IPCData.npa = npa;
CLID_IPCData.nxx = nxx;
CLID_IPC(CLID_MsgAddSingleNPANXX);
return (CLID_IPCData.returnValue);
}
/*
* CLID_IPC(номер_сообщения_IPC)
*
* Эта подпрограмма вызывает сервер с глобальным буфером
* CLID_IPCData и заносит в него номер сообщения,
* переданный ей в качестве аргумента.
*
* Если сервера нет, эта подпрограмма установит
* поле returnValue в CLID_NoServer. Остальные
* поля остаются как есть.
*/
void CLID_IPC(int IPCMessage) {
if (clid_pid == -1) {
CLID_IPCData.returnValue = CLID_NoServer;
return;
}
CLID_IPCData.serverFunction = IPCMessage;
CLID_IPCData.type = 0x8001;
CLID_IPCData.subtype = 0;
if (Send(clid_pid, &CLID_IPCData, &CLID_IPCData,
sizeof(CLID_IPCData), sizeof(CLID_IPCData))) {
CLID_IPCData.returnValue = CLID_IPCError;
return;
}
}
void checkAttach() {
if (clid_pid == -1) {
CLID_Attach(NULL);
}
}
Как вы видите, функция checkAttach() применяется для проверки существования соединения с сервером CLID. Если бы соединения не было, это было бы подобно запросу read() по несуществующему дескриптору файла. В моем варианте программы функция checkAttach() создает соединение автоматически. Это как если бы функция read() определила, что дескриптор файла некорректен, и сама создала бы корректный. Еще один вопрос стиля.
Обмен специализированными сообщениями происходит в функции CLID_IPC(). Она берет значение глобальной переменной CLID_IPCData и пробует переслать его серверу, используя функцию QNX4 Send().
Специализированные сообщения могут быть обработаны о из двух способов. Можно:
1. транслировать их в стандартные вызовы функций POSIX основанные на файловых дескрипторах;
2. инкапсулировать их в сообщение типа devctl(), либо в специализированное сообщение, используя тип _IO_MSG.
В обоих случаях вы перестраиваете клиента на обмен сообщениями стандартными для администраторов ресурсов средствами. Как? У вас нет файлового дескриптора? Есть только идентификатор соединения? Или наоборот? Ну, это как раз не проблема. В QNX/Neutrino дескриптор файла в действительности является идентификатором соединения!
В случае CLID-сервера это не вариант. Не существует стандартного POSIX-вызова на основе файлового дескриптора, который мог бы «добавить к администратору ресурса CLID пару NPA/NXX». Однако, существует стандартный механизм devctl(), так что если ваши отношения клиент/сервер требуют такой формы, смотрите ниже.
Прежде чем броситься реализовывать этот подход (трансляцию в стандартные сообщения на основе файловых дескрипторов), давайте остановимся и подумаем, где это может оказаться полезным. В аудиодрайвере QNX4 вы могли бы использовать нестандартные сообщения для передачи аудиоданных администратору и от него. При ближайшем рассмотрении здесь, для задачи блочной передачи данных, вероятно, наиболее бы подошли функции read() и write(). Установку частоты оцифровки, с другой стороны, можно было бы гораздо удачнее реализовать с применением функции devctl().
Хорошо, но ведь не каждое взаимодействие клиент/сервер сводится к блочной передаче данных (тот же сервер CLID — тому пример).
Итак, возник вопрос — как выполнять операции управления? Самый простой способ состоит в применении POSIX-вызова devctl(). Наш пример из библиотеки CLID примет вид:
/*
* CLID_AddSingleNPANXX(npa, nxx)
*/
int CLID_AddSingleNPANXX(int npa, int nxx) {
struct clid_addnpanxx_t msg;
checkAttach(); // Оставить или нет — дело вкуса
msg.npa = npa;
msg.nxx = nxx;
return
(devctl(clid_pid, DCMD_CLID_ADD_NPANXX,
&msg, sizeof(msg), NULL));
}
Как видите, операция относительно безболезненная. (Для тех, кто не любит devctl() за то, что приходится отправлять в обе стороны блоки данных одного и того же размера, см. ниже обсуждение сообщений _IO_MSG.). Опять же, если вы пишете программу, которая должна работать в обеих операционных системах, вам следует выделить функцию обмена сообщениями в отдельный библиотечный модуль и предоставить несколько вариантов реализации, в зависимости от применяемой операционной системы.
Реально мы убили двух зайцев:
1. отказались от глобальной переменной и стали собирать сообщения на основе стековой переменной — это делает нашу программу безопасной в многопоточной среде (thread- safe);
2. передали структуру данных нужного размера вместо структуры данных максимального размера, как мы это делали в предыдущем примере с QNX4.
Заметьте, что нам пришлось определить константу DCMD_CLID_ADD_NPANXX — в принципе, мы могли бы для этих же целей применить константу CLID_MsgAddSingleNPANXX (сделав соответствующее изменение в заголовочном файле), но я просто хотел подчеркнуть тот факт, что эти две константы не являются одинаковыми.
Второй убитый заяц заключался в том, что мы передали «структуру данных нужного размера». На самом деле, мы тут немножко приврали. Обратите внимание на то, что функция devctl() имеет только один параметр размера (четвертый, который мы установили в
sizeof(msg)
). Как на самом деле происходит пересылка данных? Второй параметр функции devctl() содержит команду для устройства (поэтому и «DCMD»). Двумя старшими битами команды кодируется направление, которое может быть одним из четырех:
1. «00» — передачи данных нет;
2. «01» — передача от драйвера клиенту;
3. «10» — передача от клиента драйверу;
4. «11» — двунаправленная передача.
Если вы не передаете данные (то есть достаточно просто команды) или передаете их в одном направлении, то применение функции devctl() — прекрасный выбор. Интересен тот вариант, когда вы передаете данные в обоих направлениях. Интересен он тем, что, поскольку у функции devctl() только один параметр размера, обе пересылки данных (как драйверу, так и от драйвера) передадут весь буфер данных целиком! Это хорошо в том частном случае, когда размеры буферов «ввода» и «вывода» одинаковы, но представьте себе, что буфер принимаемых драйвером данных имеет размер в несколько байт, а буфер передаваемых данных гораздо больше. Поскольку у нас есть только один параметр размера, мы вынуждены будем каждый раз передавать драйверу полный буфер данных, хотя требовалось передать всего несколько байт!
Эта проблема может быть решена применением «своих собственных» сообщений на основе общего механизма управляющих последовательностей, поддерживаемого в сообщениях типа _IO_MSG.
Сообщение типа _IO_MSG было предусмотрено для того, чтобы дать вам возможность вводить свои собственные типы сообщений, не конфликтуя при этом со «стандартными» типами сообщений администраторов ресурсов, поскольку для администраторов ресурсов сам тип сообщения _IO_MSG уже является «стандартным».
Первое, что вы должны сделать при использовании сообщений типа _IO_MSG — это определить ваши «специальные» сообщения. В этом примере мы определим два таких типа и последуем стандартной модели сообщений администратора ресурсов: один тип будет сообщением ввода, другой — вывода.
typedef struct {
int data_rate;
int more_stuff;
} my_input_xyz_t;
typedef struct {
int old_data_rate;
int new_data_rate;
int more_stuff;
} my_output_xyz_t;
typedef union {
my_input_xyz_t i;
my_output_xyz_t o;
} my_message_xyz_t;
Здесь мы определили новый тип — объединение (union) из сообщений ввода и вывода — и назвали этот тип
my_message_xyz_t
. Закономерность в имени идентификатора заключается в том, что это сообщение относится к службе «xyz
» какова бы она ни была. Сообщение ввода имеет тип my_input_xyz_t
, а сообщение вывода — my_output_xyz_t
. Отметьте, что и «ввод», и «вывод» определяются с позиции администратора ресурса: «ввод» — это данные, поступающие в администратор ресурса, а «вывод» — это данные, поступающие из него (обратно клиенту).
Нам надо придумать какой-то вызов API для клиента — мы, конечно, можем принудить клиента «вручную» заполнять структуры
my_input_xyz_t
и my_output_xyz_t
, но я не рекомендовал бы так делать по той причине, что API призван «отвязать» реализацию передаваемого сообщения от функциональности. Давайте предположим, что API клиента у нас такой:
int adjust_xyz(int *data_rate, int *оdata_rate,
int *more_stuff);
Теперь мы имеем хорошо документированную функцию adjust_xyz(), которая выполняет нечто полезное для клиента. Заметьте, что для передачи данных мы использовали указатели на целые числа — это просто пример реализации. Вот текст функции adjust_xyz():
int adjust_xyz(int *dr, int *odr, int *ms) {
my_message_xyz_t msg;
int sts;
msg.i.data_rate = *dr;
msg.i.more_stuff = *ms;
sts =
io_msg(global_fd, COMMAND_XYZ, &msg, sizeof(msg.i),
sizeof(msg.o));
if (sts == EOK) {
*odr = msg.o.old_data_rate;
*ms = msg.o.more_stuff;
}
return (sts);
}
Это пример применения функции io_msg() (ее мы скоро опишем — это не стандартный библиотечный вызов!). Функция io_msg() колдует над сборкой сообщения _IO_MSG. Чтобы уйти от проблемы функции devctl() с наличием только одного параметра размера, мы дали функции io_msg() два таких параметра: один — для ввода (
sizeof(msg.i)
), другой — для вывода (sizeof(msg.о)
). Заметьте, что мы обновляем значения *odr и *ms только в том случае, когда функция io_msg() возвращает EOK. Это обычный прием, и здесь он полезен потому, что передаваемые аргументы не изменятся, если команда не завершится успешно. (Это предохраняет клиентскую программу от необходимости держать копию передаваемых данных на случай несрабатывания функции.)
Последнее, что я сделал в функции adjust_xyz(), — это зависимость от переменной global_fd, содержащей дескриптор файла администратора ресурса. Есть, опять же, множество способов обработки этого:
• Скрыть дескриптор файла внутри функции io_msg() (это было бы полезно, если бы вы пожелали избавиться от необходимости передавать дескриптор файла с каждым вызовом; это хорошо в случаях, когда вы собираетесь обмениваться сообщениями только с одним администратором ресурса, и не подходит в качестве универсального решения).
• Передавать от клиента дескриптор файла каждому вызову функции библиотеки API (полезно, если клиент хочет разговаривать с администратором ресурса еще и другими способами, например, стандартными POSIX-вызовами на основе файловых дескрипторов типа read(), или если клиент должен уметь общаться с несколькими администраторами ресурсов).
Вот текст функции io_msg():
int io_msg(int fd, int cmd, void *msg, int isize,
int osize) {
io_msg_t io_message;
iov_t rx_iov[2];
iov_t tx_iov[2];
int sts;
// set up the transmit IOV
SETIOV(tx_iov + 0, &io_msg.o, sizeof(io_msg.o));
SETIOV(tx_iov + 1, msg, osize);
// set up the receive IOV
SETIOV(rx_iov + 0, &io_msg.i, sizeof(io_msg.i));
SETIOV(r.x_iov + 1, msg, isize);
// set up the _IO_MSG itself
memset(&io_message, 0, sizeof(io_message));
io_message.type = _IO_MSG;
io_message.mgrid = cmd;
return (MsgSendv(fd, tx_iov, 2, rx_iov, 2));
}
Отметьте несколько вещей.
В функции io_msg() для «инкапсуляции» специального сообщения (передаваемого в параметре «msg») в структуру io_message использован двухкомпонентный вектор ввода-вывода IOV.
Структура io_message была предварительно обнулена, и в ней был задан тип сообщения (_IO_MSG), а также инициализировано поле cmd (это будет использовано администратором ресурса для определения типа посылаемого сообщения).
В качестве кода завершения функции io_msg() использовался непосредственно код завершения MsgSendv().
Единственная «забавная» вещь, которую мы тут сделали, касается поля mgrid. QSSL резервирует для данного поля диапазон значений со специальным поддиапазоном для «неофициальных» драйверов. Этот поддиапазон ограничен значениями от _IOMGR_PRIVATE_BASE до IOMGR_PRIVATE_MAX соответственно. Если вы разрабатываете глубоко встраиваемую систему и хотите быть уверены, что ваш администратор ресурса не получит никаких неподходящих сообщений, то смело можете использовать значения из этого специального диапазона. С другой стороны, если вы разрабатываете в большей степени «настольную» или «обычную» систему, вы можете захотеть точно проконтролировать, будут ли вашему администратору ресурса приходит несоответствующие сообщения или нет. В этом случае вам нужно будет обратиться в QSSL за значением mgrid, зарезервированным специально для вас — никто, кроме вас, не должен использовать это номер. Посмотрите файл
, там представлены используемые в настоящее время диапазоны. В нашем вышепредставленном мы могли предположить, что COMMAND_XYZ базирована на _IOMGR_PRIVATE_BASE:
#define COMMAND_XYZ (_IOMGR_PRIVATE_BASE + 0x0007)
или QSSL назначила нам специальный поддиапазон:
#define COMMAND_XYZ ( IOMGR_ACME_CORP + 0x0007)
А что если клиент, которого вы переносите, использует администратор ввода/вывода? Как адаптировать его для QNX/ Neutrino? Ответ прост: мы уже это сделали. Установив интерфейс на основе файловых дескрипторов, мы уже используем администратор ресурса. В QNX/Neutrino вам почти никогда не придется использовать интерфейс «сырых» сообщений. Почему?
1. Вам пришлось бы самим беспокоиться о сообщении _IO_CONNECT, поступившим с клиентским вызовом open(), или искать способ поиска администратор ресурса, альтернативный использованию open().
2. Вам пришлось бы искать способ сопоставить клиенту конкретный контекстный блок в администраторе ресурса. Это, конечно, не ракетная техника, но поработать придется.
3. Вам придется инкапсулировать все ваши сообщения вручную вместо использования стандартных POSIX-функций, которые бы сделали эту работу за вас.
4. Ваш администратор ресурса не будет работать с приложениями на основе stdin/stdout. В примере с аудиодрайвером вы не смогли бы просто так выполнить
mp3_decode spud.mp3 >/dev/audio
, потому что open(), скорее всего, не сработала бы (а если бы и сработала, то не сработала бы write(), и так далее).
В QNX4 единственным способом передачи неблокирующего сообщения было создание прокси — это делалось с помощью функции qnx_proxy_attach(). Эта функция возвращает идентификатор прокси (proxy ID), (он выбирается из того же самого пространства номеров, что и идентификаторы процессов), к которому вы затем можете применить функцию Trigger() или возвратить его из функции обработки прерывания (см. ниже).
В QNX/Neutrino вы бы вместо этого настроили структуру
struct sigevent
на генерацию «импульса», а потом либо использовали бы функцию MsgDeliverEvent() для доставки события, либо привязали бы событие к таймеру или обработчику прерывания.
Обычный прием распознавания прокси-сообщений QNX4 (полученных с помощью Receive() или Creceive()) — сравнить идентификатор процесса, возвращенный функцией приема сообщения, с ожидаемым идентификатором прокси. Если совпадают — значит, это прокси. Как вариант, идентификатор процесса можно было игнорировать и обрабатывать сообщение как «стандартное». К сожалению, это несколько усложняет проблему переноса программ.
Если вы сравниваете полученный от функции приема идентификатор процесса со списком ожидаемых идентификаторов прокси, обычно вы игнорируете содержимое прокси. В конце концов, коль скоро содержимое прокси нельзя изменить после ее создания, какой прок с анализа сообщения, о котором вы уже знаете, что это одна из ваших прокси? Вы можете возразить, что это для удобства — помещаем в прокси нужные сообщения, а затем обрабатываем все сообщения одним стандартным декодером. Если это ваш случай, см. ниже «Анализ прокси по содержимому».
Поэтому, в QNX4 ваш код выглядел бы примерно так:
pid = Receive(0, &msg, sizeof(msg));
if (pid == proxyPidTimer) {
// Сработал наш таймер, сделать что-нибудь
} else if (pid == proxyPidISR) {
// Сработал наш ISR, сделать что-нибудь
} else {
// Не наша прокси — возможно, обычное
// клиентское сообщение. Сделать что-нибудь.
}
В QNX/Neutrino он заменился бы на следующий:
rcvid = MsgReceive(chid, &msg, sizeof(msg), NULL);
if (rcvid == 0) { // 0 значит, что это импульс
switch (msg.pulse.code) {
case MyCodeTimer:
// Сработал наш таймер, сделать что-нибудь
break;
case MyCodeISR:
// Сработал наш ISR, сделать что-нибудь
break;
default:
// Неизвестный код импульса
break;
}
} else {
// rcvid - не нуль, значит, это обычное
// клиентское сообщение. Сделать что-нибудь.
}
Отметим, что это пример для случая, когда вы обрабатываете сообщения самостоятельно. Но поскольку мы рекомендуем использовать библиотеку администратора ресурсов, на самом деле ваша программа выглядела бы примерно так:
int main(int argc, char **argv) {
...
// Выполнить обычные инициализации
pulse_attach(dpp, 0, MyCodeTimer, my_timer_pulse_handler,
NULL);
pulse_attach(dpp, 0, MyCodeISR, my_isr_pulse_handler,
NULL);
...
}
На этот раз мы предписываем библиотеке администратора ресурсов ввести две проверки из предыдущего примера в основной цикл приема сообщений и вызывать две наши функции обработки (my_timer_pulse_handler() и my_isr_pulse_handler()) всякий раз, когда обнаруживаются нужные коды. Гораздо проще.
Если вы анализируете содержимое прокси (фактически игнорируя, что это прокси, и обрабатывая их как сообщения), то вы автоматически имеете дело с тем, что в QNX4 на прокси ответить нельзя. В QNX/Neutrino ответить на импульс тоже нельзя. Это означает, что у вас уже есть код, который либо анализирует идентификатор, возвращаемый функцией приема, и определяет, что это прокси, и отвечать не надо, либо смотрит на содержимое сообщения и по нему определяет, надо отвечать на это сообщение или нет.
К сожалению в QNX/Neutrino произвольные данные в импульс не запихнешь. Импульс имеет четко определенную структуру, и обойти это нельзя. Умным решением здесь было бы «имитировать» сообщение от прокси при помощи импульса и таблицы. Таблица содержала бы сообщения, которые раньше передавались посредством прокси. Получив импульс, вы использовали бы поле value в качестве индекса к этой таблице, выбрали бы из таблицы соответствующее сообщение и «притворились», что получено именно оно.
Обработчики прерываний в QNX4 могли либо возвратить идентификатор прокси (указывая этим, что надо переключить прокси и таким образом уведомить ее владельца о прерывании), либо возвратить нуль (что означало бы, что в дальнейшем ничего делать не требуется). В QNX/Neutrino механизм почти идентичен — за исключением того, что вместо возвращения идентификатора прокси вы возвращаете указатель на
struct sigevent
. Генерируемое событие может быть либо импульсом (ближайший аналог прокси), либо сигналом, либо созданием потока — как выберете, так и будет.
Также в QNX4 вы обязаны были иметь обработчик прерывания — даже в том случае, если он должен был только возвратить идентификатор прокси. В QNX/Neutrino вы можете привязать
struct sigevent
к вектору прерывания, используя InterruptAttachEvent(), и это событие будет генерироваться при каждой активизации данного вектора.
Перенос приложений из QNX4 в QNX/Neutrino или поддержка программы, функционирующей в обеих операционных системах, — это возможно, если придерживаться следующих правил:
• абстрагировать, абстрагировать и еще раз абстрагировать;
• развязывать, развязывать и еще раз развязывать.
Ключ в том, чтобы отказаться от использования конкретного «дескриптора», который «связывал» бы клиента с сервером, и не привязываться к конкретному механизму обнаружения сервера. Если вы абстрагируете операции обнаружения сервера и установления соединения в отдельный набор функций, то вы сможете производить условную компиляцию для любой платформы, на которую вы пожелаете перенести ваш код.
Аналогичные рассуждения применимы и к транспортировке сообщений — всегда абстрагируйте клиентский API от «понимания» того, как сообщение доставляются от клиента к серверу, в некоторый универсальный API, который уже бы базировался на конкретном транспортном API. Затем этот конкретный транспортный API можно будет условно откомпилировать для любой платформы.
Перенос сервера из QNX4 в QNX/Neutrino является более трудной задачей вследствие того, что QNX4-серверы были сделаны «вручную» и никогда не следовали четкой структуре подобно тому, как это принято в библиотеке администраторов ресурсов в QNX/Neutrino. Впрочем, в общем случае, если вы переносите что-то сильно аппаратно-зависимое (например, аудиодрайвер или блок-ориентированный дисковый драйвер), основная часть портируемого кода никак не связна с операционной системой, и ой как связана с собственно аппаратурой. Подход, который я обычно использовал для таких случаев, состоит в том, чтобы запрограммировать каркас «драйвера» и обеспечить набор четко определенных аппаратно-зависимых функций. Каркас будет отличаться для разных операционных систем, но аппаратно-зависимые функции получатся на удивление переносимыми.