Анализ обмена сообщениями

We use cookies. Read the Privacy and Cookie Policy

Анализ обмена сообщениями

В 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 дескриптор файла в действительности является идентификатором соединения!

Трансляция сообщений в стандартные вызовы POSIX на основе файловых дескрипторов

В случае CLID-сервера это не вариант. Не существует стандартного POSIX-вызова на основе файлового дескриптора, который мог бы «добавить к администратору ресурса CLID пару NPA/NXX». Однако, существует стандартный механизм devctl(), так что если ваши отношения клиент/сервер требуют такой формы, смотрите ниже.

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

Хорошо, но ведь не каждое взаимодействие клиент/сервер сводится к блочной передаче данных (тот же сервер CLID — тому пример).

Трансляция сообщений в вызовы devctl() или _IO_MSG

Итак, возник вопрос — как выполнять операции управления? Самый простой способ состоит в применении 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, зарезервированным специально для вас — никто, кроме вас, не должен использовать это номер. Посмотрите файл <sys/iomgr.h>, там представлены используемые в настоящее время диапазоны. В нашем вышепредставленном мы могли предположить, что 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 в качестве индекса к этой таблице, выбрали бы из таблицы соответствующее сообщение и «притворились», что получено именно оно.