Роб Кёртен - Введение в QNX/Neutrino 2. Руководство по программированию приложений реального времени в QNX Realtime Platform
rcvid = MsgReceive(chid, &header, sizeof(header), NULL);
switch (header.type) {
...
case _IO_WRITE:
number_of_bytes = header.io_write.nbytes;
...
Теперь сервер fs-qnx4 знает, что в адресном пространстве клиента находится 4Кб данных (сообщение известило его об этом через элемент структуры nbytes), и что эти данные надо передать в буфер кэша. Теперь сервер fs-qnx4 может сделать так:
MsgRead(rcvid, cache_buffer[index].data,
cache_buffer[index].size, sizeof(header.io_write));
Обратите внимание, что операции приема сообщения задано смещение sizeof(header.io_write) — это сделано для того, чтобы пропустить заголовок, добавленный клиентской библиотекой. Мы предполагаем здесь, что cache_buffer[index].size (размер буфера кэша) равен 4096 (или более) байт.
Для записи данных в адресное пространство клиента есть аналогичная функция:
#include <sys/neutrino.h>
int MsgWrite(int rcvid, const void *msg, int nbytes,
int offset);
Применение функции MsgWrite() позволяет серверу записать данные в адресное пространство клиента, начиная со смещения offset байт от начала указанного клиентом приемного буфера. Эта функция наиболее полезна в случаях, где сервер ограничен в ресурсах, а клиент желает получить от него значительное количество информации.
Например, в системе сбора данных клиент может выделить 4-мегабайтный буфер и приказать драйверу собрать 4 мегабайта данных. Драйверу вовсе не обязательно держать под боком здоровенный буфер просто так, на случай если кто-то вдруг неожиданно запросит передачу большого массива данных.
Драйвер может иметь буфер размером 128Кб для обмена с аппаратурой посредством DMA, а сообщение пересылать в адресное пространство клиента по частям, используя функцию MsgWrite() (разумеется, каждый раз увеличивая смещение на 128Кб). Когда будет передан последний фрагмент, можно будет вызывать MsgReply().
Передача нескольких фрагментов сообщения с помощью функции MsgWrite()
Отметим, что функция MsgWrite() позволяет вам записать различные компоненты данных в различные места, а затем либо просто разбудить клиента вызовом MsgReply():
MsgReply(rcvid, EOK, NULL, 0);
либо сделать это после записи заголовка в начало клиентского буфера:
MsgReply(rcvid, EOK, &header, sizeof(header));
Это довольно изящный трюк для записи неизвестного количества данных, когда вы узнаете, сколько данных нужно было записать, только когда запись уже закончена. Главное — если вы будете использовать второй метод, с записью заголовка после записи данных, не забудьте зарезервировать место под заголовок в начале клиентского буфера!
Составные сообщения
До сих пор мы демонстрировали только обмен сообщениями, когда данные передаются из одного буфера в адресном пространстве клиента в другой буфер в адресном пространстве сервера (и наоборот — в случае ответа на сообщение).
При том, что данный подход вполне приемлем для большинства приложений, его применение далеко не всегда эффективно. Вспомните: наша функция write() из Си-библиотеки берет переданный ей буфер и добавляет в его начало небольшой заголовок. Используя то, что мы уже изучили ранее, вы могли бы ожидать, что реализация write() в Си-библиотеке может выглядеть примерно так (это не реальный код!):
ssize_t write(int fd, const void *buf, size_t nbytes) {
char *newbuf;
io_write_t *wptr;
int nwritten;
newbuf = malloc(nbytes + sizeof(io_write_t));
// Заполнить write_header
wptr = (io_write_t*)newbuf;
wptr->type = _IO_WRITE;
wptr->nbytes = nbytes;
// Сохранить данные от клиента
memcpy(newbuf + sizeof(io_write_t), buf, nbytes);
// Отправить сообщение серверу
nwritten =
MsgSend(fd, newbuf, nbytes + sizeof(io_write_t),
newbuf, sizeof(io_write_t));
free(newbuf);
return(nwritten);
}
Понимаете, что произошло? Несколько неприятных вещей:
• Функция write() теперь должна быть способна выделить память под буфер достаточно большого размера как для данных клиента (которые могут быть довольно значительными по объему), так и для заголовка. Размер заголовка не имеет значения — в этом случае он был равен 12 байтам.
• Мы были должны скопировать данные дважды: в первый раз — при использовании функции memcpy(), и затем еще раз, снова — уже при осуществлении передачи сообщения.
• Мы должны были предусмотреть указатель на тип io_write_t и установить его на начало буфера, вместо использования обычных механизмов доступа (впрочем, это незначительный недостаток).
Поскольку ядро намерено копировать данные в любом случае, было бы хорошо, если бы мы смогли сообщить ему о том, что одна часть данных (заголовок) фиксирована по некоторому адресу, а другая часть (собственно данные) фиксирована где- нибудь еще, без необходимости самим вручную собирать буферы из частей и копировать данные.
На наше счастье, в QNX/Neutrino реализован механизм, который позволяет нам сделать именно так! Механизм этот называется IOV (i/o vector), или «вектор ввода/вывода».
Давайте для начала рассмотрим некоторую программу, а затем обсудим, что происходит с применением такого вектора.
#include <sys/neutrino.h>
ssize_t write(int fd, const void *buf, size_t nbytes) {
io_write_t whdr;
iov_t iov[2];
// Установить IOV на обе части:
SETIOV(iov + 0, &whdr, sizeof(whdr));
SETIOV(iov + 1, buf, nbytes);
// Заполнить io_write_t
whdr.type = _IO_WRITE;
whdr.nbytes = nbytes;
// Отправить сообщение серверу
return (MsgSendv(coid, iov, 2, iov, 1));
}
Прежде всего, обратите внимание на то, что не применяется никакой функции malloc() и никакой функции memcpy(). Затем обратим внимание на тип применяемого вектора IOV — iov_t. Это структура, которая содержит два элемента — адрес и длину. Мы определили массив из двух таких структур и назвали его iov.
Определение типа вектора iov_t содержится в <sys/neutrino.h> и выглядит так:
typedef struct iovec {
void *iov_base;
size_t iov_len;
} iov_t;
Мы заполняем в этой структуре пары «адрес — длина» для заголовка операции записи (первая часть) и для данных клиента (вторая часть). Существует удобная макрокоманда, SETIOV(), которая выполняет за нас необходимые присвоения. Она формально определена следующим образом:
#include <sys/neutrino.h>
#define SETIOV(_iov, _addr, _len)
((_iov)->iov_base = (void *)(_addr),
(_iov)->iov_len = (_len))
Макрос SETIOV() принимает вектор iov_t, а также адрес и данные о длине, которые подлежат записи в вектор IOV.
Также отметим, что как только мы создаем IOV для указания на заголовок, мы сможем выделить стек для заголовка без использования malloc(). Это может быть и хорошо, и плохо — это хорошо, когда заголовок невелик, потому что вы хотите исключить головные боли, связанные с динамическим распределением памяти, но это может быть плохо, когда заголовок очень велик, потому что тогда он займет слишком много стекового пространства. Впрочем, заголовки обычно невелики.
В любом случае, вся важная работа выполняется функцией MsgSendv(), которая принимает почти те же самые аргументы, что и функция MsgSend(), которую мы использовали в предыдущем примере:
#include <sys/neutrino.h>
int MsgSendv(int coid, const iov_t *siov, int sparts,
const iov_t *riov, int rparts);
Давайте посмотрим на ее аргументы:
coid Идентификатор соединения, по которому мы передаем — как и при использовании функции MsgSend(). sparts и rparts Число пересылаемых и принимаемых частей, указанных параметрами вектора iov_t; в нашем примере мы присваиваем аргументу sparts значение 2, указывая этим, что пересылаем сообщение из двух частей, а аргументу rparts — значение 1, указывая этим, что мы принимаем ответ из одной части. siov и riov Эти массивы значений типа iov_t указывают на пары «адрес — длина», которые мы желаем переслать. В вышеупомянутом примере мы выделяем siov из двух частей, указывая ими на заголовок и данные клиента, и riov из одной части, указывая им только на заголовок.Как ядро видит составное сообщение.