Чтение онлайн

ЖАНРЫ

Введение в QNX/Neutrino 2. Руководство по программированию приложений реального времени в QNX Realtime Platform

Кёртен Роб

Шрифт:

• Мы были должны скопировать данные дважды: в первый раз — при использовании функции 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 из одной части, указывая им только на заголовок.

Как ядро видит составное сообщение.

Ядро просто прозрачно копирует данные из каждой части вектора IOV из адресного пространства клиента в адресное пространство сервера (и обратно, при ответе на сообщение). Фактически, при этом ядро выполняет операцию фрагментации/дефрагментации сообщения (scatter/gather).

Несколько моментов, которые необходимо запомнить:

• Число фрагментов ограничено значением 231 (больше, чем вам придется использовать!); число 2 в нашем примере — типовое значение.

• Ядро просто копирует данные, указанные вектором IOV, из одного адресного пространства в другое.

• Вектор-источник и вектор-приемник не должны совпадать.

Почему последний пункт так важен? Для того чтобы ответить, рассмотрим все подробнее. Со стороны клиента, скажем, мы выдали:

write(fd, buf, 12000);

в результате чего был создан вектор IOV из двух частей:

• заголовок (12 байт);

• данные (12000 байт);

На стороне сервера (скажем, сервера файловой системы

fs-qnx4
) мы имеем блоки памяти кэша до 4Кб каждый, и мы хотели бы эффективно принять сообщение непосредственно в эти блоки. В идеале мы бы написали что-то типа:

// Настроить структуру IOV для приема:

SETIOV(iov + 0, &header, sizeof(header.io_write));

SETIOV(iov + 1, &cache_buffer[37], 4096);

SETIOV(iov + 2, &cache_buffer[16], 4096);

SETIOV(iov + 3, &cache_buffer[22], 4096);

rcvid = MsgReceivev(chid, iov, 4, NULL);

Эта программа делает в значительной степени то, что вы и предполагаете: она задает вектор IOV из 4 частей, первая из которых указывает на заголовок, а следующие три части — на блоки кэш-памяти с номерами 37, 16 и 22. (Предположим, что именно эти блоки случайно оказались доступными в данный момент.) Ниже это иллюстрируется графически.

Распределение непрерывных данных по отдельным буферам.

Затем осуществляется вызов функции MsgReceivev, и ей указывается, что мы намерены принять сообщение по указанному каналу (параметр chid), и что вектор IOV для этой операции состоит из 4 частей.

(Кроме возможности работать с векторами IOV, функция MsgReceivev действует аналогично функции MsgReceive.)

Опа! Мы сделали ту же самую ошибку, которую уже делали к раньше, когда знакомились с функцией MsgReceive. Как мы узнаем, сообщение какого типа мы собираемся принять и сколько в нем данных, пока не примем все сообщение целиком?

Мы сможем решить эту проблему тем же способом, что и прежде:

rcvid = MsgReceive(chid, &header, sizeof(header), NULL);

switch (header.message_type) {

 ...

case _IO_WRITE:

 number_of_bytes = header.io_write.nbytes;

 // Выделить / найти элемент кэша

 // Заполнить элементами кэша 3-элементный IOV

 MsgReadv(rcvid, iov, 3, sizeof(header.io_write));

Поделиться с друзьями: