QNX RTP Logo QNX Realtime Platform: Русский Портал QNX
Thursday, 20 Nov 2008 10:59
Меню

Проект OpenNET - все о Unix
Разделение файловых дескрипторов между процессами Print E-mail
Thomas Fletcher, перевод: Андрей Чиликин

В традиционных Unix системах существует редко используемая, но довольно удобная возможность передавать файловый дескриптор между процессами с помощью Unix domain sockets (uds). Если вы не знакомы с понятием uds, то вкратце это можно описать как комбинацию из стандартных каналов (pipe) и сокетов (sockets). Используя uds вы имеете в своем распоряжении все преимущества работы с локальными двунаправленными каналами (скорость межпроцессорного взаимодействия в первую очередь) и при этом вы можете применять все стандартные функции работы с сокетам.

Звучит неплохо, не правда ли? И хотя Unix domain sockets (AF_LOCAL/AF_UNIX) не поддерживаются напрямую в QNX Neutrino 6.1, в богатом выборе других механизмов межпроцессорного взаимодействия отсутствует только возможность передачи файловых дескрипторов между процессами. В данной статье описывается как можно восполнить этот пробел, правда, с небольшими ограничениями.

[Примечание: Unix domain sockets доступны в QNX Neutrino начиная с версии 6.2.0. Но данная статья не потеряла из-за этого своей ценности, так что продолжайте читать!]


Как это работает

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

В монолитной операционной системе, в которой, как правило, существует всего одна центральная виртуальная файловая система (virtual file system - vfs) используемая для поддержки всех других файловых систем, отображение файлового дескриптора одного процесса на файловый дескриптор другого процесса может быть осуществлено довольно просто.

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


Беседа с незнакомцем

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

Графически это можно представить так:

Сценарий таков:

Клиент 1 имеет файловый дескриптор отображенный на Сервер 1 (это может быть, например, файловая система) и коммуникационный канал связи с Клиентом 2 (например, pipe). Клиент 1 хочет, чтобы Клиент 2 использовал тот же самый файловый дескриптор, который есть в распоряжении Клиента 1.

В этом сценарии пакет данных будет представлять собой стандартное сообщение о дублировании файлового дескриптора, которое описано в <sys/iomsg.h>. Сообщение создается Клиентом 1 и содержит все информацию, необходимую Клиенту 2 для установки связи с сервером и получения дубликата файлового дескриптора Клиента 1.

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

/* Создать пакет данных (сообщение), которое будет послано с использованием func() для дублирования файлового дескриптора fdtosend */ int send_fd(int fdtosend, int flags, void *arg, int (*func)(void *arg, void *data, int len)) { int ret; io_dup_t dupmsg; memset(&dupmsg, 0, sizeof(dupmsg)); dupmsg.i.type = _IO_DUP; dupmsg.i.combine_len = sizeof(dupmsg.i); if((ret = ConnectServerInfo(0, fdtosend, &dupmsg.i.info)) == -1) { return ret; } /* Используем thread_id для передачи идентификатора нашего процесса */ dupmsg.i.info.tid = getpid(); printf("SFD: client pid: %d\n" " server nd: 0x%x pid 0x%x chid 0x%x scoid 0x%x coid 0x%x\n", dupmsg.i.info.tid, dupmsg.i.info.nd, dupmsg.i.info.pid, dupmsg.i.info.chid, dupmsg.i.info.scoid, dupmsg.i.info.coid); /* Осуществим отправку данных с помощью переданной нам функции */ if(func) { ret = func(arg, &dupmsg, sizeof(dupmsg)); } else { ret = 0; } return ret; }

В этой функции мы делаем следующее: заполняем структуру io_dup_t и отправляем ее принимающему процессу через коммуникационный канал, заданный пользователем. Мы получаем информацию о канале и сервере для файлового дескриптора, переданного в качестве параметра, используя ConnectServerInfo(). Эта функция возвращает почти все, что нам нужно для io_dup_t. Так как мы не передаем никаких дополнительных данных в этом сообщении, мы можем установить тип и combine_len сообщения в стандартные для _IO_DUB значения.

Когда принимающий процесс получит это сообщение, ему нужно будет знать идентификатор пославшего процесса для того, чтобы правильно сгенерировать _IO_DUP запрос к серверу. Так как в в msg_info есть несколько полей, которые не употребляются для передачи информации о сервере (смотрите <sys/neutrino.h>), мы можем использовать идентификатор потока (tid) для хранения pid Клиента 1.

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

/* Функция получает данные data длиной len, проверяет их на правильность и пытается получить новый файловый дескриптор. */ int receive_fd(void *data, int len) { io_dup_t *pdupmsg = (io_dup_t *)data; pid_t otherpid; int ret, newfd; /* Простейшая проверка на допустимый тип сообщения */ if(len < sizeof(*pdupmsg) || pdupmsg->i.type != _IO_DUP) { errno = EINVAL; return -1; } /* Извлечь значение, используемое нами для передачи pid Клиента 1 */ otherpid = pdupmsg->i.info.tid; pdupmsg->i.info.tid = 0; printf("RFD: other pid: %d\n" " server nd: 0x%x pid 0x%x chid 0x%x scoid 0x%x coid 0x%x\n", otherpid, pdupmsg->i.info.nd, pdupmsg->i.info.pid, pdupmsg->i.info.chid, pdupmsg->i.info.scoid, pdupmsg->i.info.coid); /* Пытаемся установить связь с каналом */ if ((newfd = ConnectAttach(pdupmsg->i.info.nd, pdupmsg->i.info.pid, pdupmsg->i.info.chid, 0, 0)) < 0) { return -1; } /* Делаем вид, что мы Клиент 1 */ pdupmsg->i.info.pid = otherpid; /* Послать сообщение серверу с просьбой продублировать соединение, которое установил Клиент 1. В случае успешного выполнения данного запроса мы будем привязаны к ресурсу сервера. Если запрос завершится ошибкой, то тогда просто отсоединяемся от канала. */ if(MsgSendnc(newfd, &pdupmsg->i, sizeof(pdupmsg->i), 0, 0) == -1) { ConnectDetach_r(newfd); return -1; } return newfd; }

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


Условия соглашения

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

А именно:

  • Работает только между клиентами в группах с одинаковыми uid или от root.
  • Работает только для нормальных файловых дескрипторов, а не для тех, у которых выставлен флаг _NTO_SIDE_CHANNEL.
  • Сервер должен поддерживать сообщение _IO_DUP (поддерживается многими серверами по умолчанию)
  • Хотелось бы завершить данную статью упражнением. Модифицируйте исходный код таким образом, чтобы вместо дублирования файлового дескриптора использовать сообщение _IO_OPENFD для получения файлового дескриптора, открытого другим процессом.

    Полный пример
    ------------

    #include <stdio.h> #include <fcntl.h> #include <errno.h> #include <stdlib.h> #include <unistd.h> #include <mqueue.h> #include <sys/iomsg.h> #include <sys/resmgr.h> #define MY_MQ_NAME "/fdpass" /* Создать пакет данных (сообщение), которое будет послано с использованием func() для дублирования файлового дескриптора fdtosend */ int send_fd(int fdtosend, int flags, void *arg, int (*func)(void *arg, void *data, int len)) { int ret; io_dup_t dupmsg; memset(&dupmsg, 0, sizeof(dupmsg)); dupmsg.i.type = _IO_DUP; dupmsg.i.combine_len = sizeof(dupmsg.i); if((ret = ConnectServerInfo(0, fdtosend, &dupmsg.i.info)) == -1) { return ret; } /* Используем thread_id для передачи идентификатора нашего процесса */ dupmsg.i.info.tid = getpid(); printf("SFD: client pid: %d\n" " server nd: 0x%x pid 0x%x chid 0x%x scoid 0x%x coid 0x%x\n", dupmsg.i.info.tid, dupmsg.i.info.nd, dupmsg.i.info.pid, dupmsg.i.info.chid, dupmsg.i.info.scoid, dupmsg.i.info.coid); /* Осуществим отправку данных с помощью переданной нам функции */ if(func) { ret = func(arg, &dupmsg, sizeof(dupmsg)); } else { ret = 0; } return ret; } /* Функция получает данные data длиной len, проверяет их на правильность и пытается получить новый файловый дескриптор. */ int receive_fd(void *data, int len) { io_dup_t *pdupmsg = (io_dup_t *)data; pid_t otherpid; int ret, newfd; /* Простейшая проверка на допустимый тип сообщения */ if(len < sizeof(*pdupmsg) || pdupmsg->i.type != _IO_DUP) { errno = EINVAL; return -1; } /* Извлечь значение, используемое нами для передачи pid Клиента 1 */ otherpid = pdupmsg->i.info.tid; pdupmsg->i.info.tid = 0; printf("RFD: other pid: %d\n" " server nd: 0x%x pid 0x%x chid 0x%x scoid 0x%x coid 0x%x\n", otherpid, pdupmsg->i.info.nd, pdupmsg->i.info.pid, pdupmsg->i.info.chid, pdupmsg->i.info.scoid, pdupmsg->i.info.coid); /* Пытаемся установить связь с каналом */ if ((newfd = ConnectAttach(pdupmsg->i.info.nd, pdupmsg->i.info.pid, pdupmsg->i.info.chid, 0, 0)) < 0) { return -1; } /* Делаем вид, что мы Клиент 1 */ pdupmsg->i.info.pid = otherpid; /* Послать сообщение серверу с просьбой продублировать соединение, которое установил Клиент 1. В случае успешного выполнения данного запроса мы будем привязаны к ресурсу сервера. Если запрос завершится ошибкой, то тогда просто отсоединяемся от канала. */ if(MsgSendnc(newfd, &pdupmsg->i, sizeof(pdupmsg->i), 0, 0) == -1) { ConnectDetach_r(newfd); return -1; } return newfd; } int mq_write(void *arg, void *data, int len) { return mq_send((int)arg, data, len, 0); } int main(int argc, char **argv) { mqd_t mqfd; int fd, ret, sender; char c; sender = -1; while((ret = getopt(argc, argv, "sr")) != -1) { switch(ret) { case 's': sender = 1; break; case 'r': sender = 0; break; } } if(sender < 0) { printf("Usage %s -s | -r \n", argv[0]); printf("Where:\n" " -s Indicates program should send the fd\n" " -r Indicates program should receive the fd\n"); return 1; } if((mqfd = mq_open(MY_MQ_NAME, O_CREAT | O_RDWR, 0666, NULL)) == -1) { perror("Can't open/create mqueue"); return 1; } if(sender) { /* Если мы – Клиент 1, то открываем fd, который пошлем Клиенту 2 */ if((fd = open("/etc/passwd", O_RDONLY)) == -1) { perror("Can't open file to send"); return 1; } /* Для затравки прочитаем первую строку и заодно сменим текущую позицию файла */ printf("SFD: first line ["); while(read(fd, &c, 1) > 0) { if(c == '\n') { break; } printf("%c", c); } printf("]\n"); /* Отошлем файловый дескриптор Клиенту 2 */ ret = send_fd(fd, 0, (void *)mqfd, mq_write); //Sleep for a bit to ensure the receiver is run /* Подождем немного что бы дать Клиенту 2 отработать */ sleep(10); } else { char *msg; struct mq_attr mqstat; if(mq_getattr(mqfd, &mqstat) == -1) { perror("Can't get mqueue attribute"); return 1; } msg = alloca(mqstat.mq_msgsize); /* Если мы – Клиент 2, то мы получаем fd */ if(!msg || mq_receive(mqfd, msg, mqstat.mq_msgsize, NULL) == -1) { perror("Can't receive message"); return 1; } /* Передать полученные данные для дублирования fd */ if((fd = receive_fd(msg, mqstat.mq_msgsize)) == -1) { perror("Can't create file descriptor"); return 1; } /* Прочитать следующую строку, чтобы убедиться, что мы правильно продублировали fd*/ printf("RFD: next line ["); while(read(fd, &c, 1) > 0) { if(c == '\n') { break; } printf("%c", c); } printf("]\n"); } /* Уберемся за собой перед завершением... */ printf("%s: cleaning up and exiting \n", (sender) ? "SFD" : "RFD"); close(fd); mq_close(mqfd); return 0; }

    Хотите узнать больше?

    Для получения дополнительной информации о Unix domain sockets читайте "UNIX Network Programming, Volume I", by W. Richard Stevens. Также посетите QNX Developers Network на http://www.qnx.com/developer/articles/ и другие веб сайты с различной документацией и статьями, посвященными разработке приложений для QNX.

    Процесс разрешения путей в QNX:
    http://www.qnx.com/developer/articles/nov2000b/

    Оригинал статьи на английском языке:
    http://www.qnx.com/developer/articles/jul2401

    [Вернуться к списку]
    ©   2000-2003 Команда проекта QNX.ORG.RU // QNX.ORG.RU Team
    Авторы проекта: Дмитрий Алексеев [dmi] и Дмитрий Васильев. Техническое сопровождение проекта: Игорь Сорокин [isorokin]. Информационное сопровождение: Дмитрий Алексеев [dmi]
    QNX - зарегистрированная торговая марка QNX Software Systems, Ltd., Canada. Остальные упоминаемые на сайте торговые марки и логотипы являются исключительно собственностью их уважаемых владельцев. Ничьи права не затронуты. Материалы сайта не могут быть скопированы и где-либо использованы в той или иной форме без письменного разрешения разработчиков сайта.
    Powered by Mambo Open Source