Простой пример функции io_write()

Простой пример функции io_write()

Это был простой пример функции io_read(); давайте теперь перейдем к функции io_write(). Основной камень преткновения, связанный с io_write(), — получить доступ к данным. Поскольку библиотека администратора ресурсов считывает лишь незначительную часть сообщения от клиента, переданные клиентом данные (они идут сразу после заголовка _IO_WRITE) могут быть приняты функцией io_write() только частично. Простой пример — представьте себе клиента, записывающего один мегабайт. Библиотекой администратора ресурсов будут считаны только заголовок сообщения и несколько байт данных. Остальная часть мегабайта остается по-прежнему доступной на клиентской стороне — администратор ресурсов при желании может к ней обращаться.

Реально рассмотрения заслуживают только два случая:

• все содержимое сообщения клиентской функции write() было считано библиотекой администратора ресурсов полностью; или

• этого не произошло.

Судьбоносное решение, однако, состоит в ответе на следующий вопрос: «Какие проблемы сопряжены с попыткой сохранить полученную с первым сообщением часть данных?» Ответ такой: овчинка не стоит выделки. Тому есть ряд причин:

• обмен сообщениями (операции копирования на уровне ядра) выполняется очень быстро;

• проверка, получены ли данные целиком или частично, влечет определенные накладные расходы;

• дополнительные накладные расходы связаны с попыткой «сохранения» первой, уже прибывшей, части данных, в свете того факта, что ожидаются дополнительные.

По-моему, первые два пункта говорят сами за себя. Третий же пункт заслуживает пояснения. Давайте предположим, что клиент переслал большую порцию данных, и мы приняли-таки решение о том, что было бы неплохо попробовать сохранить ту часть данных, которая уже получена. К сожалению, эта часть оказалось очень небольшой. Это означает, что вместо одного непрерывного массива байт у нас теперь будет большой кусок и маленький «довесок». Иными словами, только ради этого маленького кусочка нам придется значительно усложнять работу с данными, что может неприятно сказаться на эффективности кода. Это потенциальная головная боль, не делайте так!

Реальным ответом на поставленный вопрос будет просто заново считать данные в заранее подготовленные буферы. В нашем простом примере функции io_write() я буду просто каждый раз выделять буфер при помощи malloc(), считывать в него данные, а затем освобождать его функцией free(). Разумеется, есть и куда более эффективные способы выделения буферов и управления ими!

Еще один тонкий момент в этом примере функции io_write() — это обработка модификатора _IO_XTYPE_OFFSET (и связанных с этим данных; здесь она выполняется несколько иначе, чем в примере io_read()).

/*

 * io_write1.c

*/

#include <stdio.h>

#include <stdlib.h>

#include <errno.h>

#include <sys/neutrino.h>

#include <sys/iofunc.h>

void process_data(int offset, void *buffer, int nbytes) {

 // Сделать что-нибудь с данными

}

int io_write(resmgr_context_t *ctp, io_write_t *msg,

 iofunc_ocb_t *ocb) {

 int sts;

 int nbytes;

 int off;

 int start_data_offset;

 int xtype;

 char *buffer;

 struct _xtype_offset *xoffset;

 // Проверить, открыто ли устройство на запись

 if ((sts =

  iofunc_write_verify(ctp, msg, ocb, NULL)) != EOK) {

  return (sts);

 }

 // 1) Проверить и обработать переопределение

 XTYPE xtype = msg->i.xtype & _IO_XTYPE_MASK;

 if (xtype == _IO_XTYPE_OFFSET) {

  xoffset = (struct _xtype_offset*)(&msg->i + 1);

  start_data_offset = sizeof(msg->i) + sizeof(*xoffset);

  off = xoffset->offset;

 } else if (xtype == _IO_XTYPE_NONE) {

  off = ocb->offset;

  start_data_offset = sizeof(msg->i);

 } else {

  // Неизвестный тип; игнорировать

  return (ENOSYS);

 }

 // 2) Выделить достаточно большой буфер для данных

 nbytes = msg->i.nbytes;

 if ((buffer = malloc(nbytes)) == NULL) {

  return (ENOMEM);

 }

 // 3) Считать данные от клиента (возможно, повторно)

 if (resmgr_msgread(ctp, buffer, nbytes,

  start_data_offset) == -1) {

  free(buffer);

  return (errno);

 }

 // 4) Сделать что-нибудь с данными

 process_data(off, buffer, nbytes);

 // 5) Освободить память буфера

 free(buffer);

 // 6) Установить, сколько байт должна возвращать

 // клиентская функция «write»

 _IO_SET_WRITE_NBYTES(ctp, nbytes);

 // 7) Если данные записаны, обновить структуры

 // данных POSIX и смещение OCB

 if (nbytes) {

  ocb->attr->flags |=

   IOFUNC_ATTR_MTIME | IОFUNC_ATTR_DIRTY_TIME;

  if (xtype == _IO_XTYPE_NONE) {

   ocb->offset += nbytes;

  }

 }

 // 8) Пусть библиотека сама ответит, что все в порядке

 return (EOK);

}

Как вы видите, некоторые начальные действия идентичны таковым из примера функции io_read() — функция iofunc_write_verify() аналогична функции iofunc_read_verify(), и проверка переопределения xtype выполняется точно также.

Этап 1

Здесь мы выполняем обработку переопределения xtype, в значительной степени аналогичную примеру с io_read() — за исключением того, что смещение не сохраняется в поле структуры входящего сообщения. Причина этого состоит в том, что обычной практикой для определения начального адреса поступающих от клиента данных является использование размера структуры входящего сообщения. Мы предпринимаем дополнительные усилия, чтобы удостовериться, что смещение начала данных (doffset) в коде обработки xtype является корректным.

Этап 2

Здесь мы выделяем буфер, достаточный для размещения в нем данных. Число байт, которые клиент собирается записать, представлено нам в поле nbytes объединения msg, оно заполняется автоматически Си-библиотекой клиента в коде функции write(). Отметим, что у нас недостаточно памяти для обработки запроса malloc(), мы возвращаем клиенту ENOMEM, чтобы он знал, почему его запрос потерпел неудачу.

Этап 3

Здесь мы применяем вспомогательную функцию resmgr_msgread() для считывания всего объема данных от клиента непосредственно в только что выделенный для этого буфер. В большинстве случаев здесь вполне сошла бы функция MsgRead(), но в случаях, когда сообщение является частью составного сообщения, функция resmgr_msgread() выполни для нас еще и соответствующие «магические» действия (о том, почему это надо, см. раздел «Составные сообщения»). Параметры функции resmgr_msgread() довольно очевидны: мы передаем ей указатель на внутренний контекстный блок (ctp), буфер, в который мы хотим поместить данные (buffer), и число байт, которые мы хотим считать (поле nbytes объединения msg). Последний параметр — это смещение, которое мы вычислили ранее, на этапе 1. Смещение реально позволяет пропустить заголовок, помещенный туда функцией write() клиентской Си-библиотеки, и сразу перейти к данным. Здесь возникает два интересных момента:

• мы могли бы взять произвольное смещение, чтобы считывать любые фрагменты данных в любом порядке, как нам заблагорассудится;

• мы могли бы использовать функцию resmgr_msgreadv() (обратите внимание на символ «V» в названии) для считывания данных от клиента в вектор ввода/вывода, возможно, описывающий несколько разных буферов, подобно тому, как мы делали с буферами кэша при обсуждении файловых систем в главе «Обмен сообщениями».

Этап 4

Здесь вы можете делать с данными все, что вашей душе угодно — я, например, вызвал некую условную функцию process_data() и передал ей буфер и его размер.

Этап 5

Критически важный этап! Упустить его из виду очень просто, но это неизбежно приведет к «утечкам памяти». Обратите также внимание, как мы позаботились об освобождении памяти в случае неудачи на этапе 3.

Этап 6

Здесь мы используем макрос _IO_SET_WRITE_NBYTES() для сохранения числа записанных байт, которое затем будет передано назад клиенту в качестве значения, возвращаемого функцией write(). Важно отметить, что вы должны возвратить фактическое число байт! От этого зависит судьба клиента.

Этап 7

Пришло время навести лоск для функций stat(), lseek() и последующих write(), аналогично тому, как мы это делали в нашей io_read() (и опять мы изменяем смещение в ocb только в том случае, если это не сообщение типа _IO_XTYPE_OFFSET). Однако, поскольку мы выполняем запись в устройство, мы используем при этом константу IOFUNC_ATTR_MTIME вместо константы IOFUNC_ATTR_ATIME. Флаг MTIME означает «время модификации» (modification time) — функция write() определенно его изменяет.

Этап 8

Последний этап прост: мы возвращаем константу EOK, которая сообщает библиотеке администратора ресурсов, что она должна ответить клиенту. Здесь наша работа закончена. Библиотека администратора ресурсов использует в ответе данные о числе записанных байт, которые мы сохранили с помощью макроса IO_SET_WRITE_NBYTES(), и разблокирует клиента. Клиентская функция write() возвратит число байт, записанное нашим устройством.