Выбрать главу

Давайте рассмотрим пример.

Пример отправки сообщения серверу fs-qnx4 с непрерывным представлением данных.

Итак, клиент решил переслать файловой системе 4Кб данных. (Отметьте для себя, что Си-библиотека добавила к сообщению перед данными небольшой заголовок — чтобы потом можно было узнать, к какому типу принадлежал этот запрос. Мы еще вернемся к этому вопросу, когда будем говорить о составных сообщениях, а также — еще более детально — когда будем анализировать работу администраторов ресурсов.) Файловая система считывает только те данные (заголовок), которые будут ей необходимы для того, чтобы выяснить тип принятого сообщения:

// Часть заголовков, вымышлены для примера

struct _io_write {

 uint16_t type;

 uint16_t combine_len;

 int32_t nbytes;

 uint32_t xtype;

};

typedef union {

 uint16_t type;

 struct _io_read io_read;

 struct _io_write io_write;

 ...

} header_t;

header_t header; // Объявить заголовок

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));