Как ядро видит составное сообщение.
Ядро просто прозрачно копирует данные из каждой части вектора 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(), а последние три элемента аналогичны трехэлементному вектору из второго примера — они определяют место, куда мы хотим записать данные.
Можно представить, как мы ответили бы на запрос чтения:
1. Найти элементы кэша, которые соответствуют запрашиваемым данным.
2. Заполнить вектора IOV ссылками на них.
3. Применить функцию MsgWritev() (или MsgReplyv()) для передачи данных клиенту.
Отметим, что если данные начинаются не непосредственно с начала блока кэша (или другой структуры данных), то в этом нет никакой проблемы. Просто сместите первый вектор IOV на точку начала данных и соответственно откорректируйте поле размера.
Как насчет других версий?
Все функции обмена сообщениями, кроме функций семейства MsgSend*(), имеют одинаковую общую форму: если имя функции имеет суффикс «v», значит, она принимает в качестве аргументов вектор IOV и число его частей; в противном случае, она принимает указатель и длину.
Семейство MsgSend*() содержит четыре основных варианта реализации функций с точки зрения буферов источника и адресата, плюс два варианта собственно системного вызова — итого восемь.
В нижеприведенной таблице сведены данные о вариантах функций семейства MsgSend*().
Функция Буфер передачи Буфер приема
MsgSend() линейный линейный
MsgSendnc() линейный линейный
MsgSendsv() линейный IOV
MsgSendsvnc() линейный IOV
MsgSendvs() IOV линейный
MsgSendvsnc() IOV линейный
MsgSendv() IOV IOV
MsgSendvnc() IOV IOV
Под линейным буфером я подразумеваю, что передается единый буфер типа void* вместе с его длиной. Это легко запомнить: суффикс «v» означает «вектор», и он находится на том же самом месте, что и соответствующий параметр — первым или вторым, в зависимости от того, какой буфер — передачи или приема — объявляется векторным.
Хмм. Получается, что функции MsgSendsv() и MsgSendsvnc() идентичны? Да, по части параметров именно так оно и есть. Различие заключается в том, является функция точкой завершения (cancellation point) или нет. Версии с суффиксом «nc» («no cancellation» — прим. ред.) не являются точками завершения, в то время как версии без этого суффикса — являются. (Дополнительную информацию относительно точек завершения и завершаемости (cancelability) вообще можно найти в справочном руководстве по Си-библиотеке в главе, посвященной pthread_cancel().)
Реализация
Вероятно, вы уже подозревали, что все варианты функций MsgRead(), MsgReceive(), MsgSend() и функций MsgWrite() тесно связаны между собой. (Единственное исключение — функция MsgReceivePulse(); мы ее вкратце рассмотрим.)
Которые из этих функций следует применять? В общем-то вопрос этот является чисто философским. Что до меня лично, то я предпочитаю комбинировать.
Если мы посылаем или принимаем только одноэлементные сообщения, то зачем нам все эти проблемы с настройкой векторов IOV?
Накладные расходы (кстати, незначительные) по загрузке процессора обычно не зависят от того, настраиваете ли вы все сами или оставляете это ядру или библиотеке. Подход с использованием одноэлементных сообщений избавляет ядро от необходимости манипуляций с адресным пространством и поэтому работает несколько быстрее.
Следует ли вам применять функции, использующие IOV? Конечно! Используйте их всегда, когда вам приходится самостоятельно программировать обмен многоэлементными сообщениями. Никогда непосредственно не копируйте данные при передаче многоэлементных сообщений, даже если для этого потребуется всего несколько строк программы. Это перегрузит систему попытками минимизировать число реальных операций копирования данных туда-сюда; передача указателей происходит намного быстрее, чем копирование данных из буфера в буфер.
Сообщения типа «импульс» (pulse)
Все сообщения, которые мы обсуждали до настоящего времени, блокируют клиента. Как только клиент вызывает функцию MsgSend(), для него наступает тихий час. Клиент отдыхает до тех пор, пока сервер не ответит на сообщение.
Однако есть ситуации, где отправитель сообщения не может себе позволить блокироваться. Мы рассмотрим некоторые из них в главах «Прерывания» и «Часы, таймеры и периодические уведомления», а сейчас мы должны понять концепцию данной проблемы.