QNX/UNIX: Анатомия параллелизма

Цилюрик Олег Иванович

Горошко Егор

Зайцев Владимир

Приложение

Организация обмена сообщениями

(Владимир Зайцев)

 

 

Обмен сообщениями (message passing) является основой архитектуры ОС QNX, на которой строится значительная часть служебных функций системы. Несмотря на свою «элементарность», он является удобным (и в силу своей «нативности» чрезвычайно эффективным!) механизмом для непосредственной организации взаимодействия между процессами. Особый же шарм этого механизма заключается в том, что вместе с передачей данных как таковой можно естественным образом (на основе блокировок Send/Receive/Reply) организовать синхронизацию взаимодействующих процессов.

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

Вместе с тем переход от QNX 4 к QNX 6 вызвал изменения в реализации механизма обмена сообщениями и, как следствие, API-функций. Причиной этого стал переход от однопоточных к многопоточным процессам, при этом обмен сообщениями стал осуществляться не между процессами, а между потоками. Соответственно изменился и «адресат» сообщения. В QNX 4 в этом качестве выступал процесс и его можно было однозначно определить по его идентификатору — действительному (при работе на одном узле) или идентификатору виртуального канала («virtual circuit») при межузловых сообщениях. Таким образом, для того чтобы передать сообщение (функцией из семейства Send*()) в адрес некоего сервера, процессу-клиенту достаточно было «знать» этот идентификатор. Получал он его, как правило, либо от «родителя» искомого процесса, либо через сервис глобального пространства имен (qnx_name_attach() и qnx_name_locate()).

Теперь же, в QNX 6, в качестве «адресата» сообщения стал выступать идентификатор соединения (coid), и именно он требуется при вызове функций семейства MsgSend*(). Для создания же соединения с сервером клиенту необходимо «знать» триаду соединения: идентификатор этого процесса-сервера (pid), дескриптор узла (nd), на котором сервер выполняется, и идентификатор созданного сервером канала (chid).

Вторым «возмущением», привнесенным QNX 6 в привычную и сложившуюся технику разработок, явился переход от идентификаторов узлов (nid), которые являлись уникальными в пределах сети и однозначно определяли каждый узел, к дескрипторам узлов (nd), которые уникальны только в пределах каждого данного узла, но не в сети. Уникальность в пределах сети теперь должны обеспечивать символьные имена узлов.

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

 

Организация обмена сообщениями на основе «семейных» процессов

 

Рассмотрим, как можно организовать обмен сообщениями между потоками, принадлежащими процессам, связанным «родственными узами». Для простоты изложения, чтобы в дальнейшем не формулировать «поток, принадлежащий процессу», будем рассматривать однопоточные процессы и говорить (в традициях QNX 4) «обмен сообщениями между процессами».

Итак, пусть некий родительский процесс порождает на другом узле дочерний процесс. Под порождением будем подразумевать «запуск с узла», то есть запуск процесса, выполняемый утилитой on с опцией -f. Для порождения используем функцию spawn():

char* args[] = { "/net/904-3/home/ZZZ/bin/TestChild", NULL};

...

spawn("/home/ZZZ/bin/TestChild", 0, NULL, &InhProc, args, NULL);

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

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

Однако вызов этой функции дочерним процессом приводит к неожиданному (по крайней мере, так было со мной...) на первый взгляд результату: функция возвращает дескриптор узла «с точки зрения» родительского процесса! Иными словами, нулевой дескриптор приписывается не узлу, на котором «живет» дочерний процесс, а узлу с родительским процессом. Другие дескрипторы тоже соотносятся с именами узлов так, как это выполняется на узле с родителем — порожденный процесс, унаследовав от родителя текущую рабочую и корневую директории, тем самым остался, если можно так выразиться, душой на своей исторической родине.

В [4] (глава Дмитрия Алексеева «Утилита on») этот вопрос был достаточно хорошо освещен и было сказано, что для решения проблемы следует перед порождением процесса вызвать функцию chroot() с именем узла, на котором процесс будет порожден (и при необходимости «обратным» вызовом chroot() с именем узла процесса-родителя после вызова spawn()). Это позволяет порожденному процессу обрести новую корневую директорию на том узле, где он выполняется. И тогда вызовы netmgr_strtond() будут возвращать дескрипторы узлов именно с точки зрения того узла, на котором функционирует порожденный процесс.

Далее, если дочерний процесс запущен на удаленном узле, то вызванная им функция getppid() в качестве родительского процесса возвращает совсем даже не идентификатор «фактического» родителя, а идентификатор процесса io-net, что, может быть, формально и верно, но по существу это издевательство (особенно для «мигрантов» с QNX 4, где они получали идентификатор виртуального канала и обращались с ним как с обычным идентификатором процесса). Итак, для того чтобы порожденный процесс знал свое «отчество», проще всего родителю передать свой идентификатор дочернему процессу при его рождении в параметре списка argv[].

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

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

Теперь обсудим проблемы, стоящие перед родительским процессом. Если мы хотим отсылать сообщения с родительского процесса на порожденный, то два из трех членов триады родительский процесс может легко получить: дескриптор узла — с помощью функции netmgr_strtond(), а идентификатор порожденного процесса возвращается функцией spawn(). Но вот с дескриптором канала опять появляется риск «не угадать». Кроме того, если родитель породит дочерний процесс и немедленно после этого попытается подсоединиться к каналу, который должен создать этот процесс, то, вероятнее всего, функция ConnectAttach() вернет -1, поскольку порожденный процесс еще не успеет к тому времени создать канал. Значит, понадобится цикл на N попыток с паузой в ожидании открытия.

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

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

Ниже приводится образец кода, реализующего этот подход. Обратите внимание на значение аргумента index, задаваемое в вызовах функции ConnectAttach() равным _NTO_SIDE_CHANNEL. В примерах из [1], книги, безусловно, основополагающей для любого программиста под QNX 6, для упрощения изложения это значение устанавливается в 0. Однако значение, равное _NTO_SIDE_CHANNEL, гарантирует, что возвращаемое функцией значение идентификатора соединения будет взято не из того же пространства, из которого выделяются файловые дескрипторы; в противном случае возникают проблемы, достаточно определенно обрисованные в описании функции ConnectAttach(), приведенном в технической документации QNX.

 

Пример кода родительского процесса

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

int main(int argc, char **argv) {

 int nid;     // Дескриптор удаленного узла

 int PChanid; // Идентификатор созданного канала

 int CChanid; // Идентификатор канала, созданного

              // порожденным процессом на удаленном узле

 int coid;    // Идентификатор связи с порожденным

              // процессом по созданному им каналу

 int rcvid;   // Идентификатор отправителя полученного

              // сообщения int

 ErrCode;     // Код ошибки

 char *args[] = {

  "/net/904-3/home/ZZZ/BIN/TestChild",

  "pid данного процесса",

  "идентификатор канала",

  NULL

 };

 char BufName[100], Bufpid[12],

  Bufchanid[12], RecBuffer[100];

 char SendBuf[100] = "привет, сынок!";

 pid_t procid, childid;

 struct inheritance Inhproc;

 setlocale(LC_CTYPE, "C-TRADITIONAL");

 if ((PChanid = ChannelCreate(0)) == -1)

  printf("Родитель: странно, но не удалось "

   "создать канал\n");

 else printf("Родитель: канал PChanid = %i создан\n", PChanid);

 strcpy(BufName, "Bed-Test");

 // Передаем порожденному процессу свой pid...

 args[1] = itoa(procid = getpid(), Bufpid, 10);

 // ... и дескриптор канала

 args[2] = itoa(PChanid, Bufchanid, 10);

 InhProc flags = SPAWN_SETND | SPAWN_NOZOMBIE;

 if ((nid = netmgr_strtond(BufName, NULL)) == -1) {

  printf("Родитель, отсутствует %s\n", BufName);

  return(-1);

 } else printf("Родитель: найден узел %s, его nid = %i\n", BufName, nid);

 InhProc nd = nid;

 sprintf(BufName, "/net/Bed-Test/");

 chroot(BufName);

 errno = 0;

 childid = spawn(args[0], 0, NULL, &InhProc, args, NULL);

 ErrCode = errno;

 sprintf(BufName, "/net/904-3/");

 chroot(BufName);

 if (childid- = -1)

  printf("Родитель: не удалось породить процесс,"

   " errno = %i\n", ErrCode);

 else

  printf("Родитель, мой id = %i,"

   "порожденный процесс имеет id = %i\n", procid, childid);

 if ((rcvid = MsgReceive(PChanid, RecBuffer, 100, NULL)) == -1)

  printf("Родитель: от дитятки не удалось"

   " получить сообщение\n");

 else {

  printf("Родитель: от дитятки получено"

   " сообщение:\"%s\"\n", RecBuffer);

  CChanid = atoi(RecBuffer);

  strcpy(RecBuffer, "спасибо, сынок");

  if (MsgReply(rcvid, EOK, RecBuffer, 100) == -1)

   printf("Родитель: почему-то не удалось "

    "ответить сыночку: Ау, где ты?\n");

 }

 if ((coid =

  ConnectAttach(nid, childid, CChanid, _NTO_SIDE_CHANNEL, 0)) == -1) {

  printf("Родитель: странно, но не смог установить"

   " канал связи с ребенком:"

   "nid = %i childid = %i CChanid = %i\n", nid, childid, CChanid);

  return(-1);

 }

 printf("Родитель: установил связь coid = %i с"

  " ребенком\n", coid);

 errno = 0;

 if (MsgSend(coid, SendBuf, 100, SendBuf, 100) == -1)

  printf("Родитель: на MsgSend получил errno = %i\n", errno);

 else

  printf("Родитель, получен отклик на MsgSend()"

   ", \"%s\"\n", SendBuf);

 printf("Родитель: позвольте откланяться\n");

 ChannelDestroy(Pchanid);

 ConnectDetach(CChanid);

 return(0);

}

 

Пример кода порожденного процесса

#include

#include

#include

#include

#include

#include

#include

#include

#include

int main(int argc, char **argv) {

 int nid;      // Дескриптор текущего узла

 int CChanid;  // Идентификатор созданного канала

 int coid;     // Идентификатор связи с родителем

               // по созданному им каналу

 pid_t Parpid; // Идентификатор родительского процесса

 int rcvid;    // Идентификатор отправителя

               // полученного сообщения

 char BufName[100];

 char SendBuf[100], RecBuf[100];

 setlocale(LC_CTYPE, "C-TRADITIONAL");

 if ((CChanid = ChannelCreate(0)) == -1)

  printf("Ребенок: странно, но не удалось создать"

   " канал\n");

 else

  printf("Ребенок: канал CChanid = %i создан\n", CChanid);

 Parpid = atoi(argv[1]);

 printf("Ребенок сообщает: он жив благодаря папане"

  " Parpid = %i\n", Parpid);

 strcpy(BufName, "904-3");

 if ((nid = netmgr_strtond(BufName, NULL)) == -1)

  printf("Ребенок: узел \"%s\" не найден!\n", BufName);

 else

  printf("Ребенок: узел \"%s\" найден, его nid = %i\n", BufName, nid);

 if ((coid =

  ConnectAttach(nid, Parpid, atoi(argv[2]), _NTO_SIDE_CHANNEL, 0)) == -1) {

  printf("Ребенок: странно, но дитя не смогло"

   " установить канал связи с папаней\n");

  return(-1);

 }

 printf("Ребенок: установил связь coid = %i с процессом"

  " Parpid = %i на узле %i\n", coid, Parpid, nid);

 // Вот здесь хорошее место, чтобы выполнить все действия,

 // необходимые для развертывания данного процесса

 itoa(CChanid, SendBuf, 10);

 errno = 0;

 if (MsgSend(coid, SendBuf, 100, SendBuf, 100) == -1)

  printf("Ребенок: на MsgSend() к отцу получил"

   " errno = %i\n", errno);

 else

  printf("Ребенок: на MsgSend() получен отклик"

   " от родителя.\"%s\"\n", SendBuf);

 rcvid = MsgReceive(CChanid, RecBuf, 100, NULL);

 printf("Ребенок: от папани получено сообщение:"

  " \"%s\"\n", RecBuf);

 strcpy(RecBuf, "я здесь, папаня!");

 if (MsgReply(rcvid, EOK, RecBuf, 100) == -1)

  printf("Ребенок: почему-то не удалось ответить"

   " папаше. Ау, где ты?\n");

 printf("Ребенок: дитятко работу закончило\n");

 ChannelDestroy(CChanid);

 ConnectDetach(coid);

 return(0);

}

 

Обмен сообщениями на основе менеджера ресурсов

 

Описанный выше способ построения функционирующей в сети системы процессов может быть реализован далеко не всегда. Зачастую клиенту не известна полная триада, позволяющая ему создать соединение с сервером. Вспомним, что в QNX 4, где для создания связи с другим процессом был необходим его идентификатор, существовала служба пространства имен, обеспечиваемая сервером службы nameloc. Сервер объявлял свое имя в пространстве имен с помощью функции qnx_name_attach(), а затем клиент, вызвав функцию qnx_name_locate(), получал от системы идентификатор сервера, по которому мог далее с ним общаться.

Разработчики QNX 6 настоятельно рекомендуют вместо использования службы имен выполнять сервер в виде менеджера ресурсов, причем настолько настоятельно, что до версии 6.3 аналог этой службы — менеджер службы глобальных имен gns — функционировал только локально. И надо признать, что мощь и изящество менеджера ресурсов являются очень убедительным подкреплением этих рекомендаций.

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

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

Вместе с тем следует учитывать, что менеджер ресурсов поставляется для программиста фактически в готовом виде — как шкаф, то есть уже имеются все отделения, полки, ножки, дверцы, и задача разработчика - лишь заполнить его своим содержимым. Однако «навесить» на него свою полочку уже невозможно. Иными словами, при передаче сообщений с использованием менеджера ресурсов необходимо применять уже имеющиеся средства менеджера ресурсов, благо их вполне достаточно.

Самым очевидным и наиболее простым способом передачи сообщений к серверу является инкапсуляция сообщений в «сообщения управления устройством» — сообщения типа devctl(). Однако этот способ имеет существенный недостаток, заключающийся в том, что при взаимном обмене данными между сервером и клиентом, что является более общим и достаточно частым случаем, мы вынуждены передавать в обоих направлениях буферы одинаковой длины. Это объясняется тем, что функция devctl() имеет только один параметр для размеров обоих буферов. Поэтому в качестве универсального средства передачи сообщений применение этой функции выглядит непривлекательным.

К радости разработчиков, менеджер ресурсов предлагает функцию приватных сообщений io_msg() для сообщений типа _IO_MSG. Менеджер способен их обрабатывать после соответствующей «настройки», заключающейся в подключении диапазона сообщений, интерпретируемых как приватные (допустимые значения должны быть больше 0x1ff — диапазона, резервируемого за системой). При этом сервер в состоянии как сразу «отпустить» Reply-блокированного клиента, так и оставить его в этом состоянии до нужного момента.

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

В остальном все достаточно тривиально. Более подробно о том, как писать менеджеры ресурсов, можно прочитать в главе «Writing a Resource Manager» технической документации QNX, а также в книгах [1] и [4] (глава Олега Цилюрика «Драйверы»).

 

Пример обмена сообщениями с помощью менеджера ресурсов

 

Код файла заголовков

#define NET_OPER "/net/904-3"

#define NET_REG "/net/Bed-Test"

// Максимальная длина обычного стандартного сообщения.

#define MESSIZE_MAX 100

// Максимальная длина инвентаризационного имени процесса

#define PROC_NAME_MAX 100

struct IdLabel_t { // Структура, содержащая,

 int id; // -.инвентаризационную метку процесса

 char name[PROC_NAME_MAX]; // - инвентаризационное имя процесса

} IdLabel[] = {

 /* диапазон выделенный Группе # 1: от 0x5000 до 0x50ff */

 0x5001, "пробный менеджер ресурсов",

 0x5002, "первый тестовый клиент для менеджера ресурсов",

 0x5003, "второй тестовый клиент для менеджера ресурсов",

 0x5004, "третий тестовый клиент для менеджера ресурсов",

 0x50ff, "четвертый тестовый клиент для менеджера ресурсов"

 /* диапазон, выделенный Группе # 2: от 0x5100 до 0x51ff */

 /* диапазон, выделенный Группе # 3: от 0x5200 до 0x52ff */

};

char Anonymous[] = "чуждый процесс";

int ALLNUM_MYPROC = sizeof(IdLabel) /

                    sizeof(IdLabel[0]);

 

Код процесса-клиента

Как было сказано, клиент открывает файл (функция open()), после чего использует MsgSend(), отсылая сообщения и получая ответы.

#include

#include

#include

#include

#include

#include

#include

#include "/home/ZZZ/TESTS/MR/MessTest.h"

int main() {

 int fdRM; // Дескриптор соединения с менеджером ресурсов

 char BufferSend[MESSIZE_MAX], BufferReply[MESSIZE_MAX];

 setlocale(LC_CTYPE, "C-TRADITIONAL");

 if (fdRM = open(strcat(strcpy(BufferSend, NET_REG),

  "/dev/MESSTEST/RM"), O_RDWR)) == -1)) {

  printf("Клиент не нашел имени менеджера!\n");

  fflush(stdout);

  return(-1);

 }

 /* Заполнение заголовка - первых 4-х байт сообщения, содержащего

    инвентаризационную метку данного процесса (описаны в "IRL32.h") */

 ((int *)(BufferSend))[0] = 0x5002;

 /* Заполнение сообщения */

 strcpy(BufferSend + 4, "Так вот ты какой, Менеджер Ресурсов!");

 if (MsgSend(fdRM, BufferSend, 100, BufferReply, 100) == -1)

  printf("Клиенту не удалось передать сообщение\n");

 else

  printf("Клиент передал сообщение и получил <%s>\n", BufferReply);

 fflush(stdout);

 close(fdRM);

 return(0);

}

 

Код процесса-сервера (менеджера ресурсов)

Для запуска сервера на удаленном узле выполните с терминала команду:

# on -f /net/Bed-Test /net/904-3/home/ZZZ/BIN/TestMGR

где Bed-Test — имя удаленного узла, 904-3 — имя локального узла, /home/ZZZ/BIN/TestMGR — путь к исполняемому файлу.

Вначале сервер выполняет действия по своей инициализации, специфические для данного процесса. Если они завершились успешно, т.е. сервер готов обслуживать клиентов, он инициализирует себя как администратор устройства (функции dispatch_create(), memset(&resmgr_attr, ...), iofunc_func_init(), resmgr_attach(), message_attach(), dispatch_context_alloc()), при этом на том узле, где запущен менеджер, появляется файл /dev/MESSTEST/RM. После этого, если все прошло успешно, сервер выходит на бесконечную петлю приема сообщений.

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

Так, в частности, обрабатываются сообщения на открытие ресурса (пересылаемое клиентом при вызове им функции open()), на отсоединение и закрытие ресурса (отсылаются клиентом при вызове им функции close()), на чтение или запись (если клиент вызовет функции read() или write()) и ряд других. Кроме того, разборщику известны сообщения, заголовок которых содержит «инвентаризационную метку», попадающую в диапазон, указанный при вызове функции присоединения приватных сообщений message_attach(). В этом случае сообщение передается для дальнейшей обработки функции-обработчику приватных сообщений (в нашем примере это функция PrivatHandler()).

При рассмотрении функции обработки приватных сообщений PrivatHandler() следует обратить внимание, что, хотя в этой функции и предусмотрено освобождение клиента с Reply-блокировки, она возвращает не _RESMGR_NOREPLY, как можно было бы ожидать, а значение 0, что указывает библиотеке менеджера ресурсов на то, что отвечать Reply-сообщением клиенту уже нет необходимости. Это объясняется тем, что обработчик приватных сообщений сам выполняет Reply-сообщение, и это заложено в нем изначально. В этом состоит важное отличие этого обработчика от всех прочих (взгляните на код обработчика prior_read() в разделе «Менеджеры ресурсов» главы 5).

Еще одна тонкость: при работе с приватными сообщениями в процессе-менеджере необходимо использовать функции диспетчеризации dispatch_*() (dispatch_block(), dispatch_handler() и т.д.), а не функции менеджера ресурсов resmgr_*() (resmgr_block(), resmgr_handler() и т.д.).

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include "/home/ZZZ/TESTS/MR/MessTest.h"

int PrivatHandler(message_context_t *ctp, int code,

 unsigned flags, void* handle);

char* IdLabelParse(int id);

 // Таблица функций связи

static resmgr_connect_funcs_t connect_funcs;

// Таблица функций ввода/вывода

static resmgr_io_funcs_t io_funcs;

// Структура атрибутов устройства

static iofunc_attr_t attr;

main(int args, char **argv) {

 resmgr_attr_t resmgr_attr; // Структура атрибутов менеджера ресурсов

 dispatch_t *dpp;           // Указатель на структуру диспетчеризации,

                            // содержит идентификатор канала.

 dispatch_context_t *ctp;   // Контекстная структура; содержит буфер

                            // сообщений, буфер векторов ввода/вывода

 int id;

 int result;

 char BufferRec[100];

 int rcvid;

 setlocale(LC_CTYPE, "C-TRADITIONAL");

 /* Здесь должны выполняться необходимые действия по инициализации

    конкретного сервера */

 /* Считаем, что все необходимое теперь выполнено... */

 /* Инициализация интерфейса диспетчеризации */

 if ((dpp = dispatch_create()) == NULL) {

  printf("%s: невозможно разместить обработчик"

   " диспетчеризации.\n", argv[0]);

  return EXIT_FAILURE;

 }

 /* В результате по адресу dpp создана структура диспетчеризации */

 /* Инициализация атрибутов менеджера ресурсов */

 memset(&resmgr_attr, 0, sizeof resmgr_attr);

 resmgr_attr.nparts_max = 1;

 resmgr_attr.msg_max_size = MESSIZE_MAX;

 /* Задаем число доступных структур векторов ввода/вывода (IOV) = 1.

    Задаем максимальный размер буфера получения равным MESSIZE_MAX.

    В результате инициализируются атрибуты менеджера ресурсов */

 /* Инициализация функций обработки сообщений */

 iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_funcs,

  _RESMGR_IO_NFUNCS, &io_funcs);

 /* В результате заполняются две таблицы (структуры), задающие функции

    обработки для двух специальных типов сообщений:

    таблица функций связи и таблица функций ввода/вывода.

    В соответствующих местах размещаются принимаемые по умолчанию функции

    iofunc_*_default() ... Своими не заменяем - нет необходимости. */

 /* Инициализация используемой устройством структуры атрибутов */

 iofunc_attr_init(&attr, S_IFNAM | 0666, 0, 0);

 attr.nbytes = MESSIZE_MAX + 1;

 /* В результате инициализируется структура атрибутов,

    используемая устройством;

    S_IFNAM указывает, что тип устройства - Special named file,

    побитовые флаги определяют права доступа,

    число байт в ресурсе задается равным размеру буфера. */

 /* Прикрепление имени устройства */

 if ((id = resmgr_attach(dpp, &resmgr_attr, "/dev/MESSTEST/RM", _FTYPE_ANY,

  0, &connect_funcs, &io_funcs, &attr)) == -1) {

  printf("%s: невозможно прикрепить имя менеджера"

   " ресурсов.\n", argv[0]);

  return EXIT_FAILURE;

 }

 /* Ключевое действие: мы регистрируем на нашем узле имя /dev/MESSTEST/RM

    dpp и resmgr_attr - инициализированные выше структуры;

    /dev/MESSTEST/RM - ассоциированное с устройством имя,

    _FTYPE_ANY - определяет тип открытия устройства (в данном случае

    допускается любой тип запроса открытия); равный нулю флаг

    разборки пути имени файла определяет, что запрос -

    только по имени /dev/MESSTEST/RM,

    &connect_funcs - заданные выше подпрограммы связи;

    &io_funcs - заданные выше подпрограммы ввода/вывода;

    attr - инициализированная выше структура атрибутов устройства.

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

    приватные, с передачей их обработчику для таких сообщений -

    PrivatHandler() */

 if (message_attach(dpp, NULL, 0x5000, 0x5fff, &PrivatHandler, NULL) == -1) {

  printf("невозможно подключить данный "

   "диапазон приватных сообщений\n");

  return(EXIT_FAILURE);

 }

 /* Размещение контекстной структуры */

 ctp = dispatch_context_alloc(dpp);

 /* Размер буфера сообщений, содержащегося а этой структуре, равно как и

    число векторов ввода/вывода, также содержащихся в этой структуре,

    установлены при инициализации структуры атрибутов менеджера ресурсов */

 /* Запуск петли сообщений менеджера ресурсов */

 while(1) {

  // ожидание прихода сообщений

  if ((ctp = dispatch_block(ctp)) == NULL) {

   printf("ошибка блока\n");

   return EXIT_FAILURE;

  }

  printf("Менеджер ресурсов получил сообщение"

   " длиной %i байт\n", ctp->resmgr_context.info.msglen);

  result = dispatch_handler(ctp);

  // сообщение раскодируется, и на основании заданных таблиц функций связи

  // и ввода/вывода вызывается соответствующая функция обработки сообщения

  if (result)

   printf("Менеджер ресурсов не смог обработать"

    " сообщение result = %i\n", result);

 }

}

/********************************************************************

Обработчик приватных сообщений, то есть сообщений, заголовок которых

укладывается в диапазон, указанный при вызове функции message_attach()

********************************************************************/

int PrivatHandler(message_context_t* ctp, int code,

 unsigned flags, void* handle) {

 char Buffer[MESSIZE_MAX];

 printf("получено приватное сообщение тип %x от"

  " \"%s\"\n", code, IdLabelParse(code));

 printf("Вот это сообщение <<%s>>\n",

  (char *)(ctp->msg) + 4);

 strcpy(Buffer, "Клиенту: да, я такой");

 MsgReply(ctp->rcvid, EOK, Buffer, sizeof(Buffer));

 return(0);

}

/********************************************************************

Функция пользовательской библиотеки, определяющая инвентаризационное

имя процесса по его инвентаризационной метке

********************************************************************/

char* IdLabelParse(int id) {

 struct IdLabel_t Inventory;

 int i = 0;

 while (IdLabel[i].id != id && i < ALLNUM_MYPROC) i++;

 if (i == ALLNUM_MYPROC) return Anonymous;

 else return(IdLabel[i].name);

}

 

Использование менеджера службы глобальных имен

 

Начиная с QNX версии 6.3 сервис глобальных имен, обеспечиваемый GNS-менеджером службы (утилитой gns), действует в сети. Используя этот сервис, нет необходимости организовывать программу как полноценный менеджер ресурсов, при этом приложение-сервер может объявлять свою службу, а приложения-клиенты могут отыскивать и использовать службы через QNET-сеть без знания таких частностей, как, например, где эта служба располагается и кто ее обеспечивает. Подробно о сервисе глобальных имен см. в [4].

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

Серверный узел:

# gns -s

Клиентский узел (узлы):

# gns -с

В результате на узлах, где запущены службы глобальных имен, появятся имена /dev/name/global и /dev/name/local. Каждый узел, на котором запущен gns-клиент или сервер, в одной и той же сети имеет один и тот же вид пространства имен на /dev/name/global. Каждый узел имеет локальное пространство имен /dev/name/local, являющееся локальным для данной машины и отличающееся от локального пространства имен на другой машине. (Кстати, помимо имен global и local под /dev/name/ появится еще имя gns_server или gns_local — имя, под которым регистрируется сам GNS-менеджер.)

Существует несколько функций API, относящихся к службе глобальных имен: name_attach(), name_open() и name_close(). Программисты, знакомые с QNX 4, сразу «узнают» в них аналоги известных им функций qnx_name_attach(), qnx_name_open() и qnx_name_close(). Приложения используют эти функции для объявления имени службы, связи со службой и отсоединения от службы.

Итак, чтобы объявить свое имя глобально в сети, приложение-сервер должно на узле, где в режиме сервера функционирует менеджер службы глобальных имен, объявить свою службу, выполнив вызов:

if (!(NameServer = name_attach(NULL, "MyService", NAME_FLAG_ATTACH_GLOBAL)))

 return EXIT_FAILURE;

Флаг NAME_FLAG_ATTACH_GLOBAL указывает, что приложение-сервер объявляет свое имя глобально — в сети. Приложение, которое может подсоединить службу глобально, должно иметь право доступа root. После выполнения этого вызова в директории /dev/name/global появится подсоединенное имя MyService (если бы третий аргумент вызова был установлен в ноль, это имя оказалось бы подсоединенным к /dev/name/local и было бы доступно только локально).

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

• _NTO_CHF_UNBLOCK — доставлять владельцу канала импульс с кодом _PULSE_CODE_UNBLOCK и значением rcvid каждый раз, когда Reply-блокированный клиент попытается разблокироваться (скажем, по получению сигнала или по таймеру);

• _NTO_CHF_DISCONNECT — доставлять владельцу канала импульс с кодом _PULSE_CODE_DISCONNECT, когда от процесса отсоединились все установленные соединения клиента (клиент выполнил name_close() на каждый свой name_open() к имени сервера либо вообще умер);

• _NTO_CHF_COID_DISCONNECT — доставлять владельцу канала импульс с кодом _PULSE_CODE_COIDDEATH и значением coid (идентификатора соединения) для каждого соединения по этому каналу, когда канал закрывается.

Теперь, после создания канала, сервер может становиться на прием сообщений от клиентов:

rcvid = MsgReceive(NameServer->chid, &MsgBuf, sizeof MsgBuf);

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

if (MsgBuf.hdr_type >= _IO_BASE && Buffer.hdr.type <= _IO_MAX) {

 MsgError(rcvid, ENOSYS);

 continue;

}

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

С учетом этих деталей и организован нижеописанный сервер.

 

Код процесса-сервера, использующего службу глобальных имен

#include

#include

#include

#include

/* На сервер могут приходить и импульсы. Как минимум. */

typedef struct _pulse msg_header_t;

/* Структура сообщения состоит из заголовка и буфера наших данных */

typedef struct _MsgBuf {

 msg_header_t hdr;

 char* Buffer;

} MsgBuf_t;

int main() {

 name_attach_t* NameServer;

 MsgBuf_t MsgBuf;

 int rcvid;

 char BufReply[100];

 int flagWork = 1;

 /* Создаем глобальное имя /dev/name/global/MyService */

 if (!(NameServer = name_attach(NULL, "MyService",

  NAME_FLAG_ATTACH_GLOBAL)))

  return EXIT_FAILURE;

 /* Становимся на петлю получения сообщений */

 while (flgWork) {

  if ((rcvid = MsgReceive(NameServer->chid, &MsgBuf,

   sizeof MsgBuf, NULL)) == -1) {

   printf("Ошибка при получении сервером MyService "

    "сообщения от клиента\n");

   fflush(stdout);

   break;

  }

  if (!rcvid) {

   // Получен импульс

   switch(MsgBuf.hdr.code) {

   case _PULSE_CODE_DISCONNECT:

    /* Поскольку для канала установлен флаг _NTO_CHF_DISCONNECT, ядро

       автоматически не освобождает связи, установленные клиентом ранее.

       Сервер должен выполнить это со своей стороны сам, "сознательно"

       удалив маршрут от себя обратно к клиенту */

    ConnectDetach(MsgBuf.hdr.scoid);

    break;

   case _PULSE_CODE_UNBLOCK;

    /* Клиент пытается разблокироваться, не дождавшись ответа по Reply. Надо

       выполнить какие-то действия, чтобы корректно (для себя)

       обработать эту ситуацию, и все-таки отпустить этого клиента - ему

       ведь надо! При этом импульсе в MsgBuf.hdr.value приходит rcvid */

    MsgReply(MsgBuf.hdr.value.sival_int, EAGAIN, NULL, 0);

    break;

   default:

    break;

   }

   continue;

   // вновь уходим на петлю приема сообщений

  }

  /* Полученное сообщение находится в диапазоне системных сообщений

     ввода/вывода. Не обрабатываем. */

  if (MsgBuf.hdr.type >= _IO_BASE && MsgBuf.hdr.type <= _IO_MAX) {

   MsgError(rcvid, ENOSYS);

   continue;

  }

  /* А вот это - сообщение для сервера. Обрабатываем. */

  if (MsgBuf.hdr.type <= 0x50001 || MsgBuf.hdr.type >= 0x500ff) {

   printf("Сервер получил сообщение неизвестно от"

    " кого с меткой %#x\n", MsgBuf.hdr.type);

   strcpy(BufReply, "а кто это???");

  } else {

   printf("Сервер получил сообщение. \"%s\"\n",

    MsgBuf.Buffer);

   strcpy(BufReply, "а, это ты, клиент");

  }

  MsgReply(rcvid, EOK, BufReply, strlen(BufReply) + 1);

 }

 // Конец петли получения сообщений

 /* Отсоединяемся от службы глобальных имен */

 name_detach(NameServer, 0);

 return EXIT_SUCCESS;

}

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

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

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

 

Код процесса-клиента, использующего службу глобальных имен

#include

#include

#include

#include

/* На сервер могут приходить и импульсы. Как минимум. */

typedef struct _pulse msg_header_t;

/* Структура сообщения состоит из заголовка и буфера наших данных */

typedef struct _MsgBuf {

 msg_header_t hdr;

 char* Buffer;

} MsgBuf_t;

int main() {

 MsgBuf_t MsgBuf;

 int fd;

 char BufReply[100];

 if ((fd = name_open("MyService", NAME_FLAG_ATTACH_GLOBAL)) == -1) {

  printf("Клиенту не удалось присоединиться к"

   " сервису\n");

  fflush(stdout);

  return EXIT_FAILURE;

 }

 // Инвентаризационная метка данного клиента

 MsgBuf.hdr.type = 0x50001;

 MsgBuf.hdr.subtype = 0x00;

 strcpy (MsgBuf.Buffer, "Здравствуй, дорогой сервер!");

 if (MsgSend(fd, &MsgBuf, sizeof MsgBuf, BufReply, sizeof BufReply) == -1) {

  printf("Клиент имеет проблемы с передачей сообщений"

   " серверу\n");

  fflush(stdout);

  name_close(fd);

  return EXIT_FAILURE;

 }

 printf("Клиент получил от сервера такой ответ: "

  "\"%s\" \n", BufReply);

 name_close(fd);

 return EXIT_SUCCESS;

}

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

Раньше, когда приложение-клиент использовало вызов функции name_open() для связи с сервером, сервер об этом не знал. Теперь это изменено: серверу фактически отсылается сообщение _IO_CONNECT/_IO_OPEN. Кроме того, изменено приложение-сервер, чтобы иметь возможность обрабатывать приход сообщения _IO_CONNECT.

 

Заключение

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

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