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));
Здесь мы вызываем «предварительную» MsgReceive() (отметьте, что тут мы не используем ее векторную форму, поскольку для сообщения, состоящего из одной части, в ней просто нет необходимости), определяем тип сообщения и затем продолжаем считывать данные из адресного пространства клиента (начиная со смещения sizeof(header.io_write)
) в кэш-буферы, определенные трехэлементным вектором IOV.
Обратите внимание, что мы перешли от вектора IOV, состоящего из 4 частей (как в первом примере), к вектору IOV из 3 частей. Дело в том, что в первом примере первый из четырех элементов вектора IOV отводился под заголовок, который на этот раз мы считали непосредственно при помощи функции MsgReceive(), а последние три элемента аналогичны трехэлементному вектору из второго примера — они определяют место, куда мы хотим записать данные.