// 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 выполняется точно также.
Здесь мы выполняем обработку переопределения xtype, в значительной степени аналогичную примеру с io_read() — за исключением того, что смещение не сохраняется в поле структуры входящего сообщения. Причина этого состоит в том, что обычной практикой для определения начального адреса поступающих от клиента данных является использование размера структуры входящего сообщения. Мы предпринимаем дополнительные усилия, чтобы удостовериться, что смещение начала данных (doffset) в коде обработки xtype является корректным.
Здесь мы выделяем буфер, достаточный для размещения в нем данных. Число байт, которые клиент собирается записать, представлено нам в поле nbytes объединения msg, оно заполняется автоматически Си-библиотекой клиента в коде функции write(). Отметим, что у нас недостаточно памяти для обработки запроса malloc(), мы возвращаем клиенту ENOMEM, чтобы он знал, почему его запрос потерпел неудачу.
Здесь мы применяем вспомогательную функцию resmgr_msgread() для считывания всего объема данных от клиента непосредственно в только что выделенный для этого буфер. В большинстве случаев здесь вполне сошла бы функция MsgRead(), но в случаях, когда сообщение является частью составного сообщения, функция resmgr_msgread() выполни для нас еще и соответствующие «магические» действия (о том, почему это надо, см. раздел «Составные сообщения»). Параметры функции resmgr_msgread() довольно очевидны: мы передаем ей указатель на внутренний контекстный блок (ctp), буфер, в который мы хотим поместить данные (buffer), и число байт, которые мы хотим считать (поле nbytes объединения msg). Последний параметр — это смещение, которое мы вычислили ранее, на этапе 1. Смещение реально позволяет пропустить заголовок, помещенный туда функцией write() клиентской Си-библиотеки, и сразу перейти к данным. Здесь возникает два интересных момента:
• мы могли бы взять произвольное смещение, чтобы считывать любые фрагменты данных в любом порядке, как нам заблагорассудится;
• мы могли бы использовать функцию resmgr_msgreadv() (обратите внимание на символ «V» в названии) для считывания данных от клиента в вектор ввода/вывода, возможно, описывающий несколько разных буферов, подобно тому, как мы делали с буферами кэша при обсуждении файловых систем в главе «Обмен сообщениями».
Здесь вы можете делать с данными все, что вашей душе угодно — я, например, вызвал некую условную функцию process_data() и передал ей буфер и его размер.
Критически важный этап! Упустить его из виду очень просто, но это неизбежно приведет к «утечкам памяти». Обратите также внимание, как мы позаботились об освобождении памяти в случае неудачи на этапе 3.
Здесь мы используем макрос _IO_SET_WRITE_NBYTES() для сохранения числа записанных байт, которое затем будет передано назад клиенту в качестве значения, возвращаемого функцией write(). Важно отметить, что вы должны возвратить фактическое число байт! От этого зависит судьба клиента.
Пришло время навести лоск для функций stat(), lseek() и последующих write(), аналогично тому, как мы это делали в нашей io_read() (и опять мы изменяем смещение в ocb только в том случае, если это не сообщение типа _IO_XTYPE_OFFSET). Однако, поскольку мы выполняем запись в устройство, мы используем при этом константу IOFUNC_ATTR_MTIME вместо константы IOFUNC_ATTR_ATIME. Флаг MTIME означает «время модификации» (modification time) — функция write() определенно его изменяет.
Последний этап прост: мы возвращаем константу EOK, которая сообщает библиотеке администратора ресурсов, что она должна ответить клиенту. Здесь наша работа закончена. Библиотека администратора ресурсов использует в ответе данные о числе записанных байт, которые мы сохранили с помощью макроса IO_SET_WRITE_NBYTES(), и разблокирует клиента. Клиентская функция write() возвратит число байт, записанное нашим устройством.
Простой пример функции io_devctl()
Клиентский вызов devctl() формально определен так:
#include <sys/types.h>
#include <unistd.h>
#include <devctl.h>
int devctl(int fd, int dcmd, void *dev_data_ptr,
size_t nbytes, int *dev_info_ptr);
Прежде чем рассматривать эту функцию с позиций администратора ресурсов, надо сначала понять, что это за зверь. Функция devctl() применяется для «нестандартных» и «управляющих» операций. Например, вы можете записывать данные в звуковую плату (реальные оцифрованные звуковые фрагменты, которые звуковая плата должна будет конвертировать в аналоговый аудиосигнал) и принять решение об изменении числа каналов от одного (моно) до двух (стерео) или об изменении частоты дискретизации данных от стандарта CD (44.1 кГц) к стандарту DAT (48 кГц). Такие вещи было бы правильно делать при помощи функции devctl(). При написании администратора ресурсов вы можете решить, что вам вообще не нужны никакие devctl(), и что всю необходимую функциональность можно свести к стандартным функциям read() и write(). С другой стороны, вы можете захотеть использовать как вызовы devctl() наряду с вызовами read() и write(), так и только devctl() — это будет зависеть от вашего устройства.
Функция devctl() принимает 5 аргументов:
fd | Дескриптор файла администратора ресурсов, которому вы посылаете команду devctl(). |
dcmd | Собственно команда — комбинация из двух разрядов направления обмена данными и 30 разрядов команды (см. ниже). |
dev_data_ptr | Указатель на область данных, которые передаются, принимаются или и то, и другое. |
nbytes | Размер области данных, на которую указывает dev_data_ptr. |
dev_info_ptr | Переменная для дополнительной информации, установку которой может выполнить администратор ресурса. |