Глава 5 Администраторы ресурсов

Что такое администратор ресурсов?

В данной главе мы рассмотрим все, что вы должны знать для самостоятельного написания администратора ресурса.

Администратор ресурса — это просто программа с рядом четко определенных характеристик. Эта программа по-разному называется в различных операционных системах — «драйвер», «устройство», «драйвер устройства», «администратор ввода/вывода», «файловая система», и т.п. Однако, во всех случаях предназначение этой программы (мы будем называть ее просто «администратором ресурса») заключается в том, чтобы предоставить абстрактную форму некоего сервиса.

Также, поскольку QNX/Neutrino является POSIX-совместимой ОС, основу предоставляемой абстракции составляют спецификации POSIX.

Примеры администраторов ресурсов

Прежде чем уйти в тонкости проблемы, давайте проанализируем пару примеров и увидим, как в них «абстрагируются» сервисы. Рассмотрим реальный аппаратный блок (последовательный порт) и кое-что более абстрактное (файловую систему).

Последовательный порт

В типовой системе обычно существует какой-нибудь способ программирования обмена информацией по последовательному интерфейсу типа RS-232. Этот интерфейс составляют ряд аппаратных устройств, включая микросхему UART (Universal Asynchronous Receiver Transmitter — универсальный асинхронный приемопередатчик), которая умеет преобразовывать параллельные данные от центрального процессора в последовательный поток и обратно.

В этом случае сервисом, предоставляемым соответствующим администратором ресурса, будет возможность передачи и приема символьных данных через последовательный порт.

Мы говорим, что имеет место «абстрагирование» сервиса, потому что клиентская программа (та, которая непосредственно использует сервис) не знает (да и незачем ей) о микросхеме UART и ее реализации. Все, что знает клиентская программа, — что для передачи символа она должна вызвать функцию fprintf() а для приема символов — функцию fgets(). Обратите внимание, что взаимодействия с последовательным портом мы использовали стандартные функции POSIX.

Файловая система

В качестве другого примера администратора ресурса рассмотрим файловую систему. Она состоит из ряда взаимодействующих модулей: собственно файловой системы, драйвера блочного ввода/вывода и дискового драйвера.

Предлагаемый здесь сервис состоит в способности считывать и записывать символы на некоторый носитель. Абстракция здесь та же самая, что и в предыдущем примере с последовательным портом — клиентская программа по-прежнему может использовать те же самые вызовы функций (например, fprintf() и fgets()) для доступа к носителю. Фактически, клиент действительно не знает или даже не должен знать, с каким конкретно администратором ресурсов он имеет дело.

Характеристики администраторов ресурсов

Как мы увидели в приведенных выше примерах, ключом к универсальности администраторов ресурсов является возможность использования стандартных функций POSIX — мы ведь не использовали никакие «специальные» функции, когда общались с последовательным портом. А если вам понадобится сделать нечто «особенное», характерное только для применяемого вами устройства? Например, операция настройки скорости обмена по последовательному порту в бодах специфична для администратора последовательного порта, но абсолютно бессмысленна для администратора файловой системы. Аналогично, операция по позиционированию в файле с помощью функции lseek() имеет смысл для файловой системы, но является несодержательной для последовательного порта. В POSIX эта проблема решается просто. Некоторые функции — например, lseek() — при попытке применить их к устройству, которое их не поддерживает, просто возвращают код ошибки. Для реализации функций, специфичных для каждого устройства, в POSIX предусмотрена функция управления устройствами, devctl(). Если устройство не понимает команду, передаваемую ему посредством devctl(), оно просто возвращают код ошибки, аналогично устройствам, которые не понимают функцию lseek().

Поскольку мы уже упомянули функции lseek() и devctl() как общеупотребительные, следует заметить, что администраторы ресурсов обычно поддерживают весь спектр функций, работающих с дескрипторами файлов (или

FILE* stream
).

Это естественно приводит нас к выводу о том, что администраторы ресурсов будут работать почти исключительно с вызовами дескриптор-ориентированных функций. Поскольку QNX/Neutrino — операционная система, организованная на основе обмена сообщениями, из этого следует, что вызовы POSIX-функций транслируются в сообщения, которые затем пересылаются администраторам ресурсов.

Именно эта трансляция вызовов POSIX в сообщения позволяет нам отвязать клиентуру от администраторов ресурсов. Все, что должен уметь делать администратор ресурса, — это обрабатывать ряд строго определенных сообщений. Все, что должен уметь делать клиент, — это генерировать эти самые строго определенные сообщения, которые администратор ресурса ожидает принимать и обрабатывать.

Поскольку взаимодействие между клиентурой и администраторами ресурсов основано на обмене сообщениями, имеет смысл делать этот «передаточный уровень» как можно «тоньше». Например, когда клиент выполняет функцию open() и получает в ответ дескриптор файла, этот дескриптор фактически является идентификатором соединения! Данный идентификатор соединения (он же дескриптор файла) используется затем функциями клиентской Си-библиотеки (например, функцией read()) при создании и отправке сообщения для администратора ресурсов.

Взгляд со стороны клиента

Мы уже намекнули, что ожидает клиент. Он ожидает интерфейс на основе файловых дескрипторов с применением стандартных функций POSIX.

В действительности «под колпаком» происходит еще кое-что.

Например, как в действительности клиент соединяется с соответствующим администратором ресурса? Что происходит в случае объединённых файловых систем (когда несколько файловых систем ответственны за то же самое пространство имен)? Как обрабатываются каталоги?

Поиск сервера

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

Реализация функции open() в Си-библиотеке создает сообщение, которое затем пересылается администратору процессов (

procnto
).

Администратор процессов отвечает за поддержание информации о пространстве имен путей. Данная информация представляет собой древовидную структуру имен путей, с которой связаны дескрипторы узлов (node descriptors), идентификаторы процессов (process IDs), идентификаторы каналов (channel IDs) и обработчики (handles):

Пространство имен путей в QNX/Neutrino.

Отметьте, что на представленном выше рисунке и в последующих описаниях для обозначения администратора ресурса, который реализует файловую систему QNX4, я использовал имя

fsys-qnx4
. В действительности все немного сложнее, потому что драйверы файловой системы представляют собой группы связанных между собой динамических библиотек (DLL), так что никакой программы с именем
fsys-qnx4
на самом деле не существует; мы просто используем это имя в качестве «заполнителя» для компонента файловой системы.

Давайте предположим, что клиент вызывает функцию open():

fd = open("/dev/ser1", O_WRONLY);

Реализация функции open() в клиентской Си-библиотеке создает сообщение и пересылает его администратору процессов. Это сообщение гласит: «Хочу открыть

/dev/ser1
. К кому мне обратиться по этому вопросу?»

Первая стадия разрешения имени.

Администратор процессов принимает запрос и просматривает дерево имен на предмет соответствия (давайте предположим здесь, что нам необходимо точное соответствие). Имя пути «

/dev/ser1
» вполне подойдет, и администратор процессов может ответить клиенту: «Нашел
/dev/ser1
. За обработку отвечает канал 1 процесса 44 на узле 0, спроси его!»

Не забывайте: мы все еще в клиентском коде open()!

Функция open() создает другое сообщение и соединяется с указанным процессом (PID 44) на указанном узле (NID 0 означает локальный узел) по заданному каналу (CHID 1), помещая обработчик (handle) непосредственно в сообщение. Это воистину «сообщение установки соединения» — то самое сообщение которое клиентская функция open() использует для установления связи с администратором ресурса (3 стадия на рисунке ниже) Когда администратор ресурса получает сообщение установки соединения, он анализирует его и проверяет на корректность. Например, вы могли бы попытаться применить операцию записи к администратору ресурса, который реализует файловую систему с доступом только для чтения — в этом случае вы бы получили обратно признак ошибки (в данном случае — EROFS). В нашем примере, однако, администратор последовательного порта смотрит на запрос (мы указали там O_WRONLY, что для последовательного порта абсолютно кошерно) и отвечает признаком EOK (4 стадия на рисунке ниже).

Сообщение _IO_CONNECT.

Затем, наконец, клиентская функция open() возвращает клиенту корректный дескриптор файла.

На самом деле этот дескриптор файла представляет собой идентификатор соединения, который мы только что использовали для отправки сообщения администратору ресурса! Если бы администратор ресурса не ответил признаком EOK, мы бы сообщили клиенту, что произошла ошибка (open() возвратила бы -1 и установила код ошибки в errno).

Поиск администратора процессов

Теперь, когда мы знаем основные этапы поиска конкретного администратора ресурса, осталось раскрыть тайну поиска администратора процесса, с которого все начинается. На самом деле все очень просто. По определению, администратору процессов соответствует дескриптор узла 0 (то есть текущий узел), идентификатором процесса 1 и идентификатор канала 1. Так что администратор процессов всегда идентифицируется триплетом ND/PID/CHID, равным 0/1/1.

Обработка каталогов

Пример, рассмотренный выше, относился к администратору последовательного порта. Мы также высказывали предположение, что хотим точного соответствия имен путей при поиске по дереву. Это предположение справедливо только наполовину — все соответствия имен путей, о которых мы будем говорить в этой главе, основаны на полном соответствии компонента имени пути, но вовсе не обязательно имени пути целиком. Давайте вкратце это поясним.

Предположим, у меня есть код, который делает следующее:

fp = fopen("/etc/passwd", "r");

Напомним, что функция fopen() в конечном счете вызывает функцию open(), так что реально мы имеем функцию open(), запрашивающую имя пути

/etc/passwd
. Но такого имени на рисунке нет:

Пространство имен путей в QNX/Neutrino.

Однако, из рисунка видно, что модуль

fs-qnx4
зарегистрировал свою тройку ND/PID/CHID для имени пути «
/
». Хоть это и не показано на рисунке, файловая система
fs-qnx4
зарегистрировалась как «администратор каталога», сказав администратору процессов, что будет отвечать за «
/
» и все то, что расположено «ниже». «Администраторы устройств» (например, администратор последовательного порта) так не делают. Установив флаг каталога,
fs-qnx4
получает возможность обработать запрос для имени пути «
/etc/passwd
», потому что это имя начинается с «
/
», а значит, есть совпадение!

А что произошло бы, если бы мы попытались сделать так?

fd = open("/dev/ser1/9600.8.1.n", O_WRONLY);

Ну, поскольку у администратора последовательного порта не установлен флаг каталога, администратор процессов увидит эта и скажет: «Опаньки, извините,

/dev/ser1
— не каталог. В обработке отказано». Запрос прямо здесь и заканчивается — администратор процессов даже не возвращает функции open() четверку ND/PID/CHID/handle.

Из параметров функции open() в примере выше видно, что может показаться заманчивой идеей позволить некоторым «традиционным» устройствам открываться с дополнительными параметрами, указываемыми после «обычного» имени. Однако, эмпирическое правило здесь такое: если это пройдет на совещании по организации проекта, тогда вперед. Некоторые из моих студентов, услышав это от меня, заявляют: «Так я и есть сам себе комитет по проектным решениям!» На что я обычно отвечаю. «Пистолет у вас есть. Прострелите себе ногу. :-)»

Объединенные файловые системы

Взгляните повнимательнее на уже знакомый нам рисунок.

Пространство имен путей в QNX/Neutrino.

Обратите внимание, что ответственными за префикс «

/
» объявили себя как файловая система
fs-qnx4
, так и администратор процессов. Это нормально, и беспокоиться тут не о чем. Мало того, иногда это оказывается очень даже неплохой идеей. Рассмотрим один такой случай.

Файловые системы с перекрытием

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

fs-cache
) и поместить ее поверх сетевой файловой системы. Вот как это будет смотреться с позиции клиента:

Обе файловые системы,

fs-nfs
(сетевая файловая система) и ваша кэшированная файловая система (
fs-cache
) регистрируются под одним и тем же префиксом, «
/nfs
» уже упомянули выше, в QNX/Neutrino это нормально и абсолютно законно.

Предположим, что ваша система только что стартовала, и в вашей кэшированной файловой системе еще ничего нет. Клиентская программа пробует открыть какой-нибудь файл — скажем

/nfs/home/rk/abc.txt
. Ваша кэшированная файловая система находится «перед» сетевой файловой системой (я потом покажу вам, как это сделать, когда мы будем обсуждать реализацию администратора ресурса).

Клиентский вызов open() выполняет свои обычные действия:

1. Спрашивает администратор процессов: «К кому обратиться по поводу файла

/nfs/home/rk/abc.txt

2. Получает ответ от администратора процессов: «Поговори сначала с

fs-cache
, а потом с
fs-nfs
».

Обратите внимание, что здесь администратор процессов возвращает две четверки ND/PID/CHID/handle — одну для файловой системы

fs-cach
e и одну для файловой системы
fs-nfs
. Это критично.

Далее функция open() делает следующее:

1. Направляет сообщение файловой системе

fs-cache
. «Я бы хотел открыть файл
/nfs/home/rk/abc.txt
на чтение, пожалуйста.»

2. Получает ответ от файловой системы

fs-cache
: «Сожалею, но я никогда о таком не слышала.»

Здесь становится ясно, что с администратором файловой системы

fs-cache
клиентской функции open() не повезло. Файл не существует! Однако, вызов open() знает, что он получил список из двух четверок ND/PID/CHID/handle, и поэтому пробует второй вариант:

1. Направляет сообщение файловой системе

fs-nfs
: «Я бы хотел открыть файл
/nfs/home/rk/abc.txt
на чтение, пожалуйста.»

2. От файловой системы приходит ответ: «Запросто, никаких проблем!»

Теперь, после того как у функции open() есть EOK («никаких проблем»), она возвращает дескриптор файла. Все дальнейшие операции клиент выполняет непосредственно с администратором сетевой файловой системы

fs-nfs
.

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

Так когда же вступает в игру наша кеширующая файловая система

fs-cache
? Ну, допустим, пользователь закончил считывание файла (файл теперь загружен в текстовый редактор). Когда файл понадобится сохранить, произойдет та же самая последовательность действий, но возникнет один любопытный поворот:

1. Сообщение администратору процессов: «С кем я должен переговорить насчет файла

/nfs/home/rk/abc.txt

2. Ответ администратора процессов: «Поговори сначала с

fs-cache
, а затем с
fs-nfs
».

3. Сообщение

fs-cache
: «Мне хотелось бы открыть файл
/nfs/home/rk/abc.txt
на запись, пожалуйста».

4. Ответ от

fs-cache
: «Запросто, нет проблем».

Обратите внимание на то, что на 3 этапе мы открыли файл на запись, а не на чтение, как в первый раз. Поэтому не удивительно, что

fs-cache
на этот раз разрешает эту операцию (этап 4).

Еще более интересные события происходят, когда мы повторно пытаемся прочитать этот файл:

1. Сообщение администратору процессов: «С кем я должен переговорить насчет файла

/nfs/home/rk/abc.txt

2. Ответ администратора процессов: «Поговори сначала с

fs-cache
, а затем с
fs-nfs
».

3. Сообщение

fs-cache
: «Мне хотелось бы открыть файл
/nfs/home/rk/abc.txt
на чтение, пожалуйста».

4. Ответ от

fs-cache
: «Запросто, нет проблем».

Да-да, на этот раз

fs-cache
обработала запрос на чтение!

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

Объединенные файловые системы (UFS — Unioned File Systems) и объединенные точки монтирования (UMP — Unioned Mount Points)

Дабы не путать понятия, сделаем небольшой экскурс в терминологию. Основное различие между объединенной файловой системой (UFS) и объединенной точкой монтирования (UMP) заключается в том, что UFS ориентирована на файлы, а UMP — на точки монтирования. В вышеупомянутой кеширующей файловой системе у нас была UFS, потому что оба администратора могли получить доступ к файлу вне зависимости от глубины его размещения файла в дереве каталогов. Давайте для примера рассмотрим другой администратор ресурса (назовем его «

foobar
»), отвечающий за путь «
/nfs/other
». В UFS-системе процесс
fs-cache
был бы способен кэшировать файлы и оттуда тоже, присоединившись к «
/nfs
». В случае с UMP, что принято в QNX/Neutrino по умолчанию, поскольку там все основано на соответствии самого длинного префикса, запросы смог бы обрабатывать только администратор
foobar
.

Резюме о клиенте

На этом с клиентом все. Перечислим ключевые моменты, которые следует запомнить:

• Клиент обычно налаживает связь с администратором ресурса с помощью вызова open() (или fopen()).

• После того как запрос клиента разрешился в конкретный администратор ресурса, мы его больше не меняем.

• Все дальнейшие клиентские сообщения в этом сеансе основываются на дескрипторах файлов (или

FILE* stream
) — например, read(), lseek(), fgets(), и т.п.

• Сеанс прекращается, когда клиент закрывает дескриптор файла или поток (или завершается по какой-либо причине).

Все вызовы, основанные на дескрипторах файлов, транслируются в сообщения.

Взгляд со стороны администратора ресурсов

Давайте теперь посмотрим на вещи с позиции администратора ресурса. Перво-наперво администратор ресурса должен сообщить администратору процессов, что он берет на себя ответственность за некоторую часть пространства имен путей (то есть зарегистрироваться). Затем он должен принимать сообщения от клиентуры и их обрабатывать. Очевидно, не так тут все просто.

Давайте кратко рассмотрим функции, реализуемые администраторами ресурсов, а затем уже углубимся в детали.

Регистрация префикса

Администратор ресурса должен сообщить администратору процессов, что одно или более имя пути теперь является его доменом ответственности — иными словами, что он готов обрабатывать клиентские запросы, относящиеся к этим именам путей.

Администратор последовательного порта способен обрабатывать (допустим, так) четыре последовательных порта. В этом случае он должен зарегистрировать у администратора процесса четыре различных имени пути:

/dev/ser1
,
/dev/ser2
,
/dev/ser3
и
/dev/ser4
. В результате этого в дереве имен путей у администратора процессов появятся еще четыре элемента, по одному на каждый из последовательных портов. Четыре элемента — это неплохо. А что если бы администратор последовательного порта обрабатывал, например, новомодную мультипортовую плату на 256 портов? Регистрация 256 отдельных префиксов (то есть от
/dev/ser1
до
/dev/ser256
) привела бы к появлению в дереве имен путей администратора процессов 256 различных элементов! Администратор процессов не оптимизирован для поиска по этому дереву — он предполагает, что элементы в нем, конечно, есть, но не сотни же там этих элементов.

Как правило, не следует регистрировать больше чем несколько дюжин отдельных префиксов, потому что поиск по дереву является линейным. Число 256 портов определенно больше. В таких случаях что мультипортовый администратор ресурсов должен сделать, так это зарегистрировать каталогоподобный префикс, например,

/dev/multiport
. Это займет только один элемент в дереве имен путей. Клиент открывает последовательный порт, скажем, порт
57
:

fp = fopen("/dev/multiport/57", "w");

Администратор процессов разрешает это в четверку ND/PID/CHID/handle для мультипортового администратора; решать, насколько корректен при этом остаток имени («57») — это уже дело самого администратора ресурса. В этом примере, предположив, что часть имени пути после точки монтирования хранится в переменной path, администратор ресурса мог бы выполнить проверку очень простым способом:

devnum = atoi(path);

if ((devnum <= 0) || (devnum >= 256)) {

 // Неправильный номер устройства

} else {

 // Правильный номер устройства

}

Этот будет однозначно быстрее, чем поиск, выполняемый администратором процессов, — хотя бы потому что администратор процессов по сути своей намного более универсален, чем наш администратор ресурса.

Обработка сообщений

Как только мы зарегистрировали один или более префиксов, мы должны быть готовы принимать сообщения от клиентов. Это делается «обычным» способом с помощью функции MsgReceive(). Существуют менее 30 четко определенных типов сообщений, которые администратор ресурса должен быть способен обработать. Однако, для упрощения обсуждения и реализации их условно делят на две группы:

Сообщения установления соединения (connect messages)

Всегда содержат имя пути; либо являются однократными, либо устанавливают контекст для последующих сообщений ввода/вывода.

Сообщения ввода/вывода (I/O messages)

Всегда базируются на сообщениях установления соединения; выполняют всю последующую работу.

Сообщения установления соединения

Сообщения установления соединения всегда содержат имя пути. Прекрасным примером функции, генерирующей сообщение установления соединения, является уже не раз упомянутая нами функция open(). В этом случае обработчик (handle) сообщения установления соединении устанавливает контекст для последующих сообщений ввода/вывода. (В конце-то концов, после open() мы все-таки собираемся делать что-то наподобие read().).

Примером «однократного» сообщения установления соединения может быть сообщение, сгенерированное в результате вызова rename(). Здесь никакого контекста не предполагается — обработчик в администраторе ресурса изменяет имя указанного файла на новое, и все.

Сообщения ввода/вывода

Сообщения ввода/вывода возникают только после соответствующего сообщения установления соединения и ссылаются на установленный им контекст. Как уже упоминалось ранее при обсуждении сообщений установления соединения, идеальный пример — вызов функции open(), за которым следует read().

На самом деле групп сообщений три

Кроме сообщений об установлении соединения и сообщений ввода/вывода, есть еще и «другие» сообщения, которые администратор ресурсов может принимать и обрабатывать. Но поскольку они не являются в полной мере «административными», покамест отложим их обсуждение и вернемся к ним позже.

Библиотека администратора ресурсов

Прежде чем лезть в глубины организации администраторов ресурсов, познакомимся сначала с библиотекой администратора ресурсов, разработанной QSSL. Отметим, что в действительности эта «библиотека» состоит из нескольких четко различимых частей:

• функции пула потоков (мы обсуждали их в главе «Процессы и потоки», в параграфе «Пулы потоков»);

• интерфейс диспетчеризации;

• функции администратора ресурсов;

• вспомогательные функции POSIX-библиотеки.

При том, что можно было бы, конечно, писать администраторы ресурсов «с нуля» (как это делалось в QNX4), эта овчинка часто не стоит такой выделки.

Просто для демонстрации практичности библиотечного подхода — вот код однопоточной версии администратора «

/dev/null
»:

/*

 * resmgr1.c

 *

 * /dev/null на основе библиотеки администратора ресурсов

*/

#include 

#include 

#include 

#include 

#include 


int main(int argc, char **argv) {

 dispatch_t       *dpp;

 resmgr_attr_t      resmgr_attr;

 resmgr_context_t    *ctp;

 resmgr_connect_funcs_t connect_func;

 resmgr_io_funcs_t    io_func;

 iofunc_attr_t     attr;

 // Создать структуру диспетчеризации

 if ((dpp = dispatch_create()) == NULL) {

  perror("Ошибка dispatch_create\n");

  exit(EXIT_FAILURE);

 }

 // Инициализировать структуры данных

 memset(&resmgr_attr, 0, sizeof(resmgr_attr));

 resmgr_attr.nparts_max = 1;

 resmgr_attr.msg_max_size = 2048;

 // Назначить вызовам обработчики по умолчанию

 iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_func,

  _RESMGR_IO_NFUNCS, &io_func);

 iofunc_attr_init(&attr, S_IFNAM | 0666, 0, 0);

 // Зарегистрировать префикс в пространстве имен путей

 if (resmgr_attach(dpp, &resmgr_attr,

 "/dev/mynull", _FTYPE_ANY,

  0, &connect_func, &io_func, &attr) == -1) {

 perror("Ошибка resmgr_attach\n");

  exit(EXIT_FAILURE);

 }

 ctp = resmgr_context_alloc(dpp);

 // Ждать сообщений в вечном цикле

 while (1) (

  if ((ctp = resmgr_block(ctp)) == NULL) {

  perror("Ошибка resmgr_block\n");

  exit(EXIT_FAILURE);

  }

  resmgr_handler(ctp);

 }

}

И все! Полнофункциональный администратор ресурса

/dev/null
реализуется всего несколькими вызовами функций!

Если бы пришлось писать аналогичный по функциональности администратор (то есть с поддержкой функций stat(), chown(), chmod(), и т.д.) «с нуля», то вам пришлось бы перелопатить сотни, если не тысячи строк Си-кода.

Реально все это за вас делает библиотека

Как вариант начального знакомства с библиотекой, давайте посмотрим, что делают вызовы, использованные в администраторе ресурсов

/dev/null
:

dispatch _create()

Создает структуру диспетчеризации; она будет использоваться для блокирования по приему сообщения.

iofunc_attr_init()

Инициализирует используемую устройством атрибутную запись. Мы обсудим атрибутные записи в подробностях несколько позже, а вкратце так: атрибутная запись содержит информацию об устройстве, и на каждое имя устройства имеется по одной атрибутной записи.

iofunc_func_init()

Инициализирует две структуры данных, cfuncs и ifuncs, которые содержат соответственно указатели на функции установления соединения и функции ввода/вывода. Это, пожалуй, самый «магический» вызов, поскольку именно он назначает подпрограммы обработки сообщений, привязывая их к структурам данных. Заметьте, что никакого кода обработки сообщений установления соединения или сообщений ввода/вывода, генерируемых функциями read(), stat() или им подобными, в администраторе нет. Дело в том, что библиотека содержит для всех сообщений готовые POSIX-обработчики по умолчанию, и как раз функция iofunc_func_init()-то и привязывает их к двум передаваемым ей таблицам.

resmgr_attach()

Создает канал, который администратор ресурса будет использовать для приема сообщений, и говорит администратору процессов, что мы намерены отвечать за «

/dev/null
». Параметров тут много, но к этой головной боли мы вернемся несколько позже. Сейчас же важно отметить, что именно здесь связываются воедино дескриптор диспетчера (dpp), имя пути (строка «
/dev/null
») и обработчики функций установления соединения (cfuncs) и ввода/вывода (ifuncs).

resmgr_context_alloc()

Выделяет внутренний контекстный блок администратора ресурса. Мы рассмотрим этот блок в подробностях несколько позже, а вкратце — он содержит информацию, относящуюся к обрабатываемому сообщению.

resmgr_block()

Это блокирующий вызов администратора ресурса — функция, с помощью которой мы ожидаем сообщение от клиента.

resmgr_handler()

После того как сообщение от клиента получено, для его обработки вызывается эта функция.

За кулисами библиотеки

Вы уже видели, что наша программа ответственна за предоставление основного рабочего цикла приема сообщений:

while (1) {

 // Здесь ждем сообщения

 if ((ctp = resmgr_block(ctp)) == NULL) {

  perror("Unable to resmgr_block\n");

  exit(EXIT_FAILURE);

 }

 // Обработать сообщение

 resmgr_handler(ctp);

}

Это очень удобно, поскольку позволяет вам помещать точки останова на принимающей функции и перехватывать сообщения (например, с помощью отладчика) в процессе работы.

Библиотека осуществляет все магические манипуляции внутри функции resmgr_handler(), потому что это как раз то самое место, где сообщение анализируется и обрабатывается в соответствии с таблицами функций установления соединения и ввода/вывода, о которых мы уже говорили ранее.

В действительности, библиотека состоит из двух взаимодействующих уровней — базового, который обеспечивает «сырую» функциональность администратора ресурсов, и уровня POSIX, который содержит вспомогательные функции POSIX и обработчики по умолчанию. Сейчас мы кратко обрисуем эти два уровня, а затем в разделе «Структура администратора ресурсов» рассмотрим все в подробностях.

Базовый уровень

Самый нижний, базовый, уровень состоит из функций, имена которых начинаются с «resmgr_». Этот класс функций относится к низкоуровневым механизмам функционирования администратора ресурсов.

Здесь я только кратко приведу описание этих функций, которые являются доступными и которые мы будем использовать. Затем я отправлю вас к изучению QSSL документации для получения более подробных сведений об этих функциях.

К функциям базового уровня относятся:

resmgr_msgreadv() и resmgr_msgread()

Считывают данных из адресного пространства клиента при помощи обмена сообщениями.

resmgr_msgwritev() и resmgr_msgwrite()

Записывают данные в адресное пространство клиента при помощи обмена сообщениями.

resmgr_open_bind()

Связывает контекст с запросом на установление соединения, поступившим от соответствующей клиентской функции. Этот контекст далее будет использоваться функциями ввода/вывода.

resmgr_attach()

Создает канал и связывает воедино имя пути, дескриптор диспетчера, функции установления соединения, функции ввода/вывода и другие параметры. Посылает сообщение администратору процессов для регистрации имени пути (префикса).

resmgr_detach()

Противоположна функции resmgr_attach(). Уничтожает связь между именем пути и администратором ресурса.

pulse_attach()

Связывает код импульса с функцией. Поскольку приема сообщений реализуется библиотекой, это удобный способ «перехватывать управление» для обработки импульсов.

pulse_detach()

Отвязывает код импульса от функции.

В дополнение к функциям, перечисленным выше, есть также множество функций, посвященных интерфейсу диспетчеризации.

Одна функция из вышеупомянутого списка заслуживает особого упоминания — функция resmgr_open_bind(). Данная функция, когда приходит сообщение установления соединения (обычно это происходит в результате клиентского вызова open() или fopen()), создает некую контекстную информацию, чтобы она была готов к моменту прихода сообщения ввода/вывода. Почему ее не быт администраторе

/dev/null
? Потому что POSIX-функции обработки сообщений, принятые по умолчанию, сами вызывают для нас эту функцию. Если бы мы обрабатывали все сообщения самостоятельно, нам, конечно, пришлось бы вызвать данную функцию.

Функция resmgr_open_bind() не только формирует контекстный блок для последующих сообщений ввода/вывода, но также инициализирует и другие структуры данных, используемые непосредственно библиотекой администратора ресурсов.

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

Уровень POSIX

Второй уровень библиотеки администратора ресурсов является POSIX-уровнем. Как и в случае с базовым уровнем, вы могли бы написать администратор ресурсов и без использования этих функций, но это отняло бы уйму трудов! Прежде чем обсуждать функции уровня POSIX в подробностях, мы должны рассмотреть ряд структур данных базового уровня, приходящие от клиентуры сообщения, а также общую структуру и сферу ответственности администратора ресурсов.

Написание администратора ресурсов

Теперь, когда мы знаем основы, — как выглядит мир глазами клиента, в каком цвете видит все администратор ресурсов, и что из себя представляют оба уровня библиотеки — пришло время сконцентрироваться на деталях.

В этом разделе мы рассмотрим следующие темы:

• структуры данных;

• структуру администратора ресурсов;

• структуры данных POSIX-уровня;

• подпрограммы обработки сообщений;

• и, конечно, множество примеров.

Постарайтесь запомнить приведенную ниже «большую картинку» — на ней изображено практически все, что имеет отношение к администратору ресурсов:

Архитектура администратора ресурсов — общая схема.

Структуры данных

Первое, в чем следует разобраться, — это структуры данных, которые управляют работой библиотеки:

• управляющая структура

resmgr_attr_t

• таблица функций установления соединения

resmgr_connect_funcs_t

• таблица функций ввода-вывода

resmgr_io_funcs_t
и еще одна внутренняя структура данных библиотеки:

• внутренний блок контекста

resmgr_context_t

Позже мы рассмотрим такие типы данных как блок открытого контекста (OCB), атрибутную запись (attributes structure) и запись точки монтирования (mount structure), которые используются POSIX-уровнем библиотеки.

Управляющая структура
resmgr_attr_t

Управляющая структура (типа

resmgr_attr_t
) передается функции resmgr_start(), которая несколько ее элементов и отвечает за основной цикл приема сообщений.

Управляющая структура (взято из

) содержит следующее:

typedef struct _resmgr_attr {

 unsigned flags;

 unsigned nparts_max;

 unsigned msg_max_size;

 int    (*other_func)(resmgr_context_t *ctp, void *msg);

} resmgr_attr_t;

Обработчик сообщений
other_func

Вообще говоря, использования этого элемента следует избегать. Этот элемент, если не равен NULL, указывает на подпрограмму, которая должна быть вызвана, если принято сообщение, не распознанное библиотекой. Хоть это и можно было бы использовать для реализации «нестандартных» сообщений, но это нехорошая практика (применяйте либо обработчики _IO_DEVCTL, либо _IO_MSG — см. ниже). Если вы хотите обрабатывать входящие импульсы, рекомендую для этого применять функцию pulse_attach().

Так что оставьте у этого элемента значение NULL.

Параметры, задающие размеры структур данных

Эти два параметра используются для управления размерами областей памяти, используемых при обмене сообщениями.

Параметр nparts_max управляет размером динамически выделяемого вектора ввода/вывода (элемент iov в структуре типа

resmgr_context_t
— контекстном блоке библиотеки администратора ресурсов, см. ниже). Обычно этот параметр подстраивают, когда некоторые из функций-обработчиков возвращают более чем одноэлементный вектор ввода-вывода (IOV). Отметим, что этот параметр применяется только к исходящим сообщениям на поступающие сообщения не влияет.

Параметр msg_max_size управляет размером буферного пространства, которое библиотека администратора ресурсов должна выделить под входящее сообщение. Библиотека администратора ресурсов установит этот параметр в значение как минимум соответствующее наибольшему заголовку принимаемого сообщения. Это гарантирует, что когда будет вызвана функция- обработчик, ей будет передан полный заголовок сообщения. Отметим, однако, что присутствие в буфере следующих за заголовком данных (если таковые имеются) не гарантируется, даже если параметр msg_max_size задан достаточно большим. (Размеры буферов обсуждаются в параграфе «Внутренний контекстный блок

resmgr_context_t
», см. ниже).

Параметр
flags

Этот параметр дает библиотеке администратора ресурсов дополнительную информацию. В нашем случае мы передадим просто нуль (0). Другие значения этого параметра можно найти в справочном руководстве по Си-библиотеке, в разделе, посвященном функции resmgr_attach().

Таблица функций установления соединения
resmgr_connect_funcs_t

Когда библиотека администратора ресурсов принимает сообщение, она проверяет тип сообщения и смотрит, что можно сделать. Базовый уровень библиотеки содержит две таблицы, которые определяют это поведение. Это таблица типа

resmgr_connect_funcs_t
, которая содержит список обработчиков сообщений установления соединения, а также таблица типа
геsmgr_io_funсs_t
, которая содержит аналогичный список обработчиков сообщений ввода/вывода — ее мы рассмотрим несколько позже.

Когда придет время заполнить таблицы функций установления соединения и ввода/вывода, рекомендуется сначала воспользоваться функцией iofunc_func_init(), чтобы инициализировать таблицы функциями по умолчанию, определенными на уровне POSIX. Тогда, если вам потребуется заменить обработчик какого-либо сообщения, вы просто подставляете вместо POSIX-обработчика по умолчанию свою собственную функцию. Мы увидим это в разделе «Подстановка своих собственных функций». А сейчас давайте рассмотрим собственно таблицу функций установления соединения (взято из

):

typedef struct _resmgr_connect_funcs {

 unsigned nfuncs;

 int (*open)(ctp, io_open_t *msg, handle, void *extra);

 int (*unlink)(ctp, io_unlink_t *msg, handle,

  void *reserved);

 int (*rename)(ctp, io_rename_t *msg, handle,

  io_rename_extra_t *extra);

 int (*mknod)(ctp, io_mknod_t *msg, handle,

 void *reserved);

 int (*readlink)(ctp, io_readlink_t *msg, handle,

  void *reserved);

 int (*link)(ctp, io_link_t *msg, handle,

  io_link_extra_t *extra);

 int (*unblock)(ctp, io_pulse_t *msg, handle,

  void *reserved);

 int (*mount) (ctp, io_mount_t *msg, handle,

  io_mount_extra_t *extra);

} resmgr_connect_funcs_t;

Заметьте, что я сократил прототипы, опустив тип первого параметра, ctp (

resmgr_context_t*
), и третьего, handle (
RESMGR_HANDLE_T
).

Полный прототип для, например, функции open() в действительности имеет вид:

int (*open)(resmgr_context_t *ctp, io_open_t *msg,

 RESMGR_HANDLE_T *handle, void *extra);

Первый элемент структуры (nfuncs) указывает, насколько она велика (то есть сколько в ней содержится элементов). Для приведенной выше структуры он должен быть равен 8, поскольку элементов в ней восемь (от open() до mount()). Этот элемент нужен главным образом для того, чтобы позволить QSSL обновлять данную библиотеку без каких бы то ни было вредных последствий для вашего кода. Предположим, к примеру, что вы компилировали свой код со значением 8, а затем QSSL обновила библиотеку, и параметр стал равен 9. Библиотека могла бы себе сказать: «Ага! Пользователь библиотеки был скомпилирован с расчетом на 8 функций, а их у нас теперь 9. Надо бы назначить 9-й функции обработчик по умолчанию.» Текущее значение параметра nfuncs хранится в заголовочном файле

в виде именованной константы _RESMGR_CONNECT_NFUNCS. Используйте эту константу при заполнении таблицы функций установления соединения вручную (хотя лучше всего применять для этого функцию iofunc_func_init()).

Отметим, что формат у всех прототипов один и тот же. Первый параметр, ctp, указывает на структуру

resmgr_context_t
. Это внутренний контекстный блок, используемый библиотекой администратора ресурсов и который изменять не следует (за исключением одного поля, к обсуждению которого мы еще вернемся).

Второй параметр всегда указывает на сообщение. Поскольку функции в таблице предназначены для обработки различных типов сообщений, тип второго параметра в прототипе соответствует типу сообщения, которое данная функция должна обрабатывать.

Третий параметр — структура типа

RESMGR_HANDLE_T
, называемая дескриптором (handle). Она используется для идентификации устройства, которому предназначалось сообщение. Мы тоже рассмотрим ее позже, когда будем говорить об атрибутной записи.

И, наконец, последний параметр является «резервным», или «дополнительным», и используется для функций, которым необходимы какие-либо дополнительные данные. Мы продемонстрируем применение параметра extra по назначению в обсуждении функций-обработчиков сообщений.

Таблица функций ввода/вывода
resmgr_io_funcs_t

Таблица функций ввода/вывода подобна таблице функций установления соединения. Вот она (взято из

):

typedef struct _resmgr_io_funcs {

 unsigned nfuncs;

 int (*read)(ctp, io_read_t *msg, ocb);

 int (*write)(ctp, io_write_t *msg, ocb);

 int (*close_ocb)(ctp, void *reserved, ocb);

 int (*stat)(ctp, io_stat_t *msg, ocb);

 int (*notify)(ctp, io_notify_t *msg, ocb);

 int (*devctl)(ctp, io_devctl_t *msg, ocb);

 int (*unblock)(ctp, io_pulse_t *msg, ocb);

 int (*pathconf)(ctp, io_pathconf_t *msg, ocb);

 int (*lseek)(ctp, io_lseek_t *msg, ocb);

 int (*chmod)(ctp, io_chmod_t *msg, ocb);

 int (*chown)(ctp, io_chown_t *msg, ocb);

 int (*utime)(ctp, io_utime_t *msg, ocb);

 int (*openfd)(ctp, io_openfd_t *msg, ocb);

 int (*fdinfo)(ctp, io_fdinfo_t *msg, ocb);

 int (*lock)(ctp, io_lock_t *msg, ocb);

 int (*space)(ctp, io_space_t *msg, ocb);

 int (*shutdown)(ctp, io_shutdown_t *msg, ocb);

 int (*mmap)(ctp, io_mmap_t *msg, ocb);

 int (*msg)(ctp, io_msg_t *msg, ocb);

 int (*umount)(ctp, void *msg, ocb);

 int (*dup)(ctp, io_dup_t *msg, ocb);

 int (*close_dup)(ctp, io_close_t *msg, ocb);

 int (*lock_ocb)(ctp, void *reserved, ocb);

 int (*unlock_ocb)(ctp, void *reserved, ocb);

 int (*sync)(ctp, io_sync_t *msg, ocb);

} resmgr_io_funcs_t;

В этой структуре я тоже сократил прототипы, опустив тип элемента ctp (

resmgr_context_t*
) и тип последнего элемента, ocb (
RESMGR_OCB_T*
). Полный прототип, например, для функции read() в действительности имеет вид:

int (*read)(resmgr_context_t *ctp, io_read_t *msg, RESMGR_OCB_T *ocb);

Самый первый элемент структуры (nfuncs) указывает, насколько она велика (то есть сколько элементов она содержит). Текущее значение этого элемента содержится в константе _RESMGR_IO_NFUNCS.

Отметим, что списки параметров в таблице функций ввода/вывода также довольно однообразны. Первый параметр — ctp, второй параметр — msg, как и у обработчиков из таблицы функций установления соединения.

Третий параметр, однако, отличается. Этот параметр называется ocb, что расшифровывается как «Open Context Block» — «блок открытого контекста». Этот блок содержит контекст, созданный обработчиком сообщения установления соединения (например, в результате клиентского запроса open()) и доступный функциям ввода/вывода.

Как уже упоминалось ранее, когда придет время заполнять таблицы функций, рекомендуется пользоваться для этого функцией iofunc_func_init(), чтобы сначала загрузить таблицы POSIX-обработчиками по умолчанию. Если же вам будет нужно переопределить обработчики сообщений определенного типа, вы сможете просто заменить POSIX-обработчики по умолчанию на свои собственные. Мы рассмотрим это в разделе «Подстановка своих собственных функций».

Внутренний контекстный блок
resmgr_context_t

И, наконец, еще одна структура данных используется базовым уровнем библиотеки, чтобы отслеживать кое-какую информацию для себя. Вам не следует изменять содержимое этой структуры, за исключением одного элемента — вектора ввода/вывода iov.

Вот эта структура данных (взято из

):

typedef struct _resmgr_context {

 int         rcvid;

 struct _msg_info   info;

 resmgr_iomsgs_t   *msg;

 struct _resmgr_ctrl *ctrl;

 int         id;

 int         status;

 int         offset;

 int         size;

 iov_t        iov[1];

} resmgr_context_t;

Как и в случае с другими структурами данных, я позволил себе опустить зарезервированные поля.

Давайте взглянем на ее содержимое.

rcvid Идентификатор отправителя, полученный от MsgReceivev(). Указывает, кому вы должны ответить (если вы намерены отвечать самостоятельно).
info Содержит информационную структуру, возвращаемую функцией MsgReceivev() в основном цикле приема сообщений библиотеки администратора ресурсов. Полезна для получения информации о клиенте, включая дескриптор узла, идентификатор процесса (PID), идентификатор потока и т.д. Подробнее см. документацию по функции MsgReceivev().
msg Указатель на объединение (union) всех возможных типов сообщений. Практически бесполезен, потому что каждая из ваших функций-обработчиков получает соответствующий элемент объединения вторым параметром.
ctrl Указатель на управляющую структуру, которую вы передали в самом начале. Опять же, для вас этот параметр не очень полезен, но зато полезен для библиотеки администратора ресурсов.
id Идентификатор точки монтирования, которой предназначалось сообщение. Когда вы вызывали resmgr_attach(), она вернула вам небольшой целочисленный идентификатор. Это и есть значение id. Отметим, что вы вероятнее всего никогда не будете использовать этот параметр самостоятельно, а будете полагаться вместо этого на атрибутную запись, передаваемую вам обработчиком io_open().
status Сюда ваша функция-обработчик помещает результат выполнения операции. Отметим, что вы должны всегда использовать макрос _RESMGR_STATUS() для заполнения этого поля. Например, если вы обрабатываете сообщение установления соединения от функции open(), причем ваш администратор ресурса предоставляет доступ «только для чтения», а клиент хотел открыть ресурс на запись, вы возвратите клиенту через errno код EROFS при помощи (обычно)
_RESMGR_STATUS(ctp, EROFS)
.
offset Текущее смещение (в байтах) в клиентском буфере сообщений. Имеет смысл для базового уровня библиотеки только при чтении составных сообщений функцией resmgr_msgreadv().
size Этот параметр говорит, сколько байт в буфере сообщения, переданном вашей функции-обработчику, являются достоверными. Это важная цифра, поскольку она указывает на то, требуется ли читать дополнительные данные от клиента (например, если не все данные были считаны базовым уровнем библиотеки), и надо ли выделить память для ответа клиенту (например, для ответа на запрос read()). (Отметим, что в версии 2.00 есть ошибка, из-за которой это поле не заполняется в случае несоставного сообщения установления соединения. Все остальные сообщения обрабатываются корректно. Обходной путь здесь (и только здесь!) заключается в использовании параметра msglen структуры info.)
iov Таблица векторов ввода/вывода, в которую вы можете записывать возвращаемые значения, если это необходимо. Например, когда клиент вызывает read(), и у вас вызывается соответствующий обработчик read(), вам может потребоваться возвратить клиенту данные. Можно задать эти данные при помощи массива iov и возвратить что-нибудь типа
_RESMGR_NPARTS(2)
, указав тем самым (в нашем случае), векторы
iov[0]
и
iov[1]
содержат данные для клиента. Заметьте, что массив iov определен как одноэлементный. Однако, заметьте также, что он очень удобно расположен в конце структуры. Фактическое число элементов в массиве iov определяете вы сами, когда присваиваете значение полю nparts_max вышеупомянутой управляющей структуры (см. параграф «Управляющая структура
resmgr_attr_t
»).

Структура администратора ресурсов

Теперь, когда мы имеем представление о структурах данных, мы можем обсудить взаимодействие между компонентами, которые вам предстоит написать, чтобы ваш администратор ресурсов мог что-нибудь реально сделать.

Мы рассмотрим:

• Функцию resmgr_attach() и ее параметры;

• Подстановку своих собственных функций;

• Общую схему работы администратора ресурсов;

• Сообщения, которые должны бы быть сообщениями установления соединения, но таковыми не являются;

• Составные сообщения.

Функция
resmgr_attach()
и ее параметры

Как вы уже видели в приведенном выше примере с администратором

/dev/null
, первое, что вы должны сделать — это зарегистрировать у администратора процессов свою точку монтирования. Это делается с помощью функции resmgr_attach(), которая имеет следующий прототип:

int resmgr_attach(void *dpp, resmgr_attr_t *resmgr_attr,

 const char *path, enum _file_type file_type,

 unsigned flags,

 const resmgr_connect_funcs_t *connect_funcs,

 const resmgr_io_funcs_t *io_funcs,

 RESMGR_HANDLE_T *handle);

Давайте исследуем по порядку ее аргументы и посмотрим, как они применяются.

dpp Дескриптор диспетчера (dispatch handle). Обеспечивает интерфейсу диспетчеризации возможность управлять приемом сообщений для вашего администратора ресурсов.
resmgr_attr Управляет характеристиками администратора ресурсов, как обсуждалось ранее.
path Точка монтирования, которую вы регистрируете. Если вы регистрируете дискретную точку монтирования (как, например, в случае с
/dev/null
или
/dev/ser1
), клиент должен указывать ее точно, без каких бы то ни было дополнительных компонентов имени пути в ее конце. Если вы регистрируете каталоговую точку монтирования (как было бы, например, в случае с сетевой файловой системой, монтируемой как
/nfs
), то соответствие тоже должно быть точным, но с той оговоркой, что в этом случае продолжение имени пути допускается; то, что идет после точки монтирования, будет передано функции установления соединения (например, имя пути
/nfs/etc/passwd
даст совпадение с точкой монтирования сетевой файловой системой, а «остатком» будет
etc/passwd
). (Эта особенность, кстати, может пригодиться и там, где на первый взгляд логичнее было бы регистрировать дискретную точку монтирования — см. параграф «Регистрация префикса» раздела «Взгляд со стороны администратора ресурсов» — прим. ред.)
file_type Класс администратора ресурсов. См. ниже.
flags Дополнительные флаги, управляющие поведением вашего администратора ресурсов. Эти флаги выбираются из множества _RESMGR_FLAG_BEFORE, _RESMGR_FLAG_AFTER, _RESMGR_FLAG_DIR и константы 0. Флаги «BEFORE» (букв, «перед») и «AFTER» (букв, «после») указывают на то, что ваш администратор ресурсов хочет зарегистрироваться на данной точке монтирования перед или, соответственно, после других. Эти два флага могут быть полезны, если надо реализовать объединенные файловые системы. Мы вскоре вернемся к взаимосвязи этих флагов. Флаг «DIR.» («каталог») указывает на то, что ваш администратор ресурса хочет обслуживать указанную точку монтирования и все, что находится ниже ее — этот стиль характерен для администратора файловой системы, в противоположность администратору ресурсов, регистрирующему дискретную точку монтирования.
connect_funcs и io_funcs Эти параметры являются просто списком функций установления соединения и функций ввода/вывода, которые вы хотите привязать к точке монтирования.
handle Это «расширяемая» структура (также известная как «атрибутная запись»), описывающая монтируемый ресурс. Например, в случае последовательного порта вы могли бы расширить стандартную атрибутную запись POSIX-уровня информацией о базовом адресе последовательного порта, скорости обмена в бодах, и т.д.

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

Когда вы определились с точкой монтирования и хотите ее зарегистрировать, вы должны сообщить администратору процессов, хочет ли ваш администратор ресурсов обрабатывать запросы от кого попало или только от клиентуры, которая помечает свои сообщения установления соединения специальными метками. Например, рассмотрим драйвер очередей сообщений POSIX (mqueue). Ему совершенно ни к чему «обычные» вызовы open() от старых добрых клиентов — он просто не будет знать, что с ними делать. Он примет сообщения только от тех клиентов, которые используют POSIX-вызовы mq_open(), mq_receive(), и т.п. Чтобы не позволять администратору процессов даже перенаправлять «обычные» запросы администратору очередей mqueue, у этого администратора в параметре параметр file_type задается значение _TYPE_MQUEUE. Это означает, что когда клиент пытается с помощью администратора процессов выполнить разрешение имени, при этом явно не определив, что хочет поговорить с администратором ресурсов, зарегистрированным как _FTYPE_MQUEUE, администратор процессов не будет даже рассматривать администратор mqueue как возможный вариант.

Если только вы не делаете что-либо уж очень специфичное, вам лучше всего подойдет значение file_type, равное _FTYPE_ANY, означающее, что ваш администратор ресурсов готов обработать запрос от любого клиента. Полный список именованных констант _FTYPE_* приведен в файле

.

Что касательно флагов «BEFORE» и «AFTER», тут все становится интереснее. Вы можете задать либо один из этих флагов, либо константу 0.

Давайте посмотрим, как это работает. Стартуют несколько администраторов ресурсов, в указанном в таблице порядке. В таблице также приведены флаги, указанные каждым из них в параметре flags. Взгляните на получившуюся очередность.

Администратор Флаг Очередность
1 _RESMGR_BEFORE 1
2 _RESMGR_AFTER 1, 2
3 0 1, 3 ,2
4 _RESMGR_BEFORE 1, 4, 3, 2
5 _RESMGR_AFTER 1, 4, 3, 5, 2
6 0 1, 4, 6, 3, 5, 2

Из таблицы видно, что первый администратор ресурса, явно определивший флаг, далее не сдвигается со своей позиции.(См. таблицу: администратор ресурсов № 1 был первым определившим флаг «BEFORE»; кто бы теперь ни зарегистрировался, он так и останется первым в списке. Аналогично, администратор ресурсов № 2 был первым определившим флаг «AFTER» — и снова, независимо от того, кто еще будет регистрироваться после него, он всегда остается в списке последним.) Если не определен никакой флаг, это действует как флаг «между». Когда стартует администратор ресурсов № 3 (указав нулевой флаг), он помещается в середину очереди. Как и в случае с флагами «BEFORE» и «AFTER», здесь имеет место упорядочивание, в результате чего все вновь регистрирующиеся «средние» администраторы ресурсов располагаются перед уже зарегистрированными «средними».

Однако, в действительности, только в очень редких случаях вам придется регистрировать более одного, и в еще меньшем числе случаев — более двух администраторов ресурсов при той же самой точке монтирования. Полезный совет: обеспечьте возможность установки флагов непосредственно в командной строке администратора ресурса, чтобы конечный пользователь вашего администратора ресурса мог сам задать, например, флаг «BEFORE» опцией

-b
, флаг «AFTER» — опцией
, а нулевой флаг («между») был бы, скажем, установкой по умолчанию.

Имейте в виду, что данное обсуждение применимо только для администраторов ресурсов, регистрируемых при одной и той же точке монтирования. Монтирование «

/nfs
» с флагом «BEFORE» и «
/disk2
» с флагом «AFTER» не будет иметь никакого взаимного влияния. Однако, если вы затем будете монтировать еще одну «
/nfs
» или «
/disk2
», вот тогда эти флаги и проявят себя.

И наконец, функция resmgr_attach() в случае успешного завершения возвращает дескриптор (handle) в виде небольшого целого числа (или -1 при неудаче). Этот дескриптор можно затем применить для того, чтобы убрать данное имя пути из внутренней таблицы имен путей администратора процессов.

Подстановка своих собственных функций

Проектируя свой самый первый администратор ресурсов вы, скорее всего, захотите действовать постепенно. Было бы очень досадно написать несколько тысяч строк кода только для того, чтобы понять, что в самом начале была допущена фундаментальная ошибка, и теперь придется либо наспех затыкать дырки (э-э, я хотел сказать — вносить коррективы), либо выкинуть все это и начать заново.

Чтобы все работало как надо, рекомендуемым подходом здесь является использование функции-инициализатора iofunc_func_init() из уровня POSIX, чтобы заполнить таблицы функций установления соединения и функций ввода/вывода заданными по умолчанию функциями POSIX-уровня. Это значит; что вы можете фактически написать каркас вашего администратора ресурсов, как мы уже делали выше, с помощью всего нескольких вызовов.

Какую функцию запрограммировать первой — это будет зависеть от того, какой администратор ресурсов вы пишете. Если это администратор файловой системы, отвечающий за точку монтирования и все, что под ней, то вам, скорее всего, лучше всего начать с функции io_open(). С другой стороны, если вы пишете администратор ресурса с дискретной точкой монтирования, который выполняет «традиционные» операции ввода/вывода (то есть вы будете общаться с ним преимущественно вызовами типа read() и write()), то лучшей стартовой позицией для вас были бы функции io_read() и/или io_write(). Если же вы пишете администратор ресурса с дискретной точкой монтирования, но вместо «традиционных» операций ввода/вывода основу его функциональности составляют вызовы типа devctl() или ioctl(), то правильнее было бы начать с io_devctl().

Независимо от того, с чего вы начинаете, вам нужно будет удостовериться в том, что ваши функции вызываются так, как вы предполагаете. В данном ключе функции-обработчики POSIX- уровня по умолчанию обладают очень полезным свойством — их можно помещать непосредственно в таблицы функций установления соединения и таблицы функций ввода/вывода.

Это означает, что если вы захотите что-то дополнительно проконтролировать, просто добавьте дополнительный диагностический вызов printf(), чтобы он сказал что-то типа «Я тут!», а затем делайте «то, что надо сделать» — все очень просто.

Вот фрагмент администратора ресурсов, который перехватывает функцию io_open():

// Упреждающая декларация

int io_open(resmgr_context_t*, io_open_t*,

 RESMGR_HANDLE_T*, void*);


int main() {

 // Все как в примере /dev/null,

 // кроме следующего за этой строкой:

 iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &cfuncs,

  _RESMGR_IO_NFUNCS, &ifuncs);

 // Добавьте это для перехвата управления:

 cfuncs.open = io_open;

Если вы описали функцию io_open() корректно, как в этом примере кода, то вы можете вызывать функцию, заданную по умолчанию, из вашей собственной!

int io_open(resmgr_context_t *ctp, io_open_t *msg,

 RESMGR_HANDLE_T *handle, void *extra) {

 printf("Мы в io_open!\n");

 return (iofunc_open_default(ctp, msg, handle, extra));

}

Таким образом, вы по-прежнему применяете POSIX-обработчик по умолчанию iofunc_open_default(), но заодно перехватываете управление для вызова printf().

Очевидно, что вы могли бы выполнить аналогичные действия для функций io_read(), io_write(), io_devctl() и любых других, для которых есть обработчики POSIX-уровня по умолчанию. Идея, кстати, действительно отличная, потому что такой подход показывает вам, что клиент вызывает ваш администратор ресурса именно так, как вы предполагаете.

Общая схема работы администратора ресурсов

Как мы уже намекнули выше в разделах, посвященных краткому рассмотрению клиента и администратора ресурсов, последовательность действий начинается на клиентской стороне с вызова open(). Он транслируется в сообщение установления соединения, которое принимается и обрабатывается функцией администратора ресурсов io_open().

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

Но на самом деле роль функции io_open() гораздо значительнее. Она отвечает не только за проверку, может клиент открыть ресурс или нет, но также за следующее:

• инициализацию внутренних параметров библиотеки;

• привязку к запросу контекстного блока;

• привязку к контекстному блоку атрибутной записи.

Первые две операции выполняются с помощью функции базового уровня resmgr_open_bind(), а привязка атрибутной записи сводится к простому присваиванию.

Будучи однажды вызвана, io_open() выпадает из рассмотрения. Клиент может либо прислать сообщение ввода/вывода, либо нет, но в любом случае должен будет однажды завершить «сеанс связи» с помощью сообщения, соответствующего функции close(). Заметьте, что если клиента вдруг постигает внезапная смерть (например, он получает SIGSEGV, или выходит из строя узел, на котором он работает), операционная система автоматически синтезирует сообщение close(), чтобы администратор ресурсов смог корректно завершить сессию. Поэтому вы гарантированно получите сообщение close()!

Сообщения, которые должны быть сообщениями установления соединения, но таковыми не являются

Тут есть один интересный момент, который вы, может быть, для себя уже отметили. Прототип клиентской функции chown() имеет вид:

int chown(const char *path, uid_t owner, gid_t group);

Вспомните: сообщение об установлении соединения всегда содержит имя пути и является либо однократным, либо устанавливает контекст для дальнейших сообщений ввода/ вывода.

Так почему же сообщение, соответствующее клиентской функции chown(), не является сообщением установления соединения? К чему здесь сообщение ввода/вывода, когда в прототипе даже дескриптора файла нет?!

Ответ простой — чтобы облегчить вам жизнь.

Представьте себе, что было бы, если бы функции типа chown(), chmod(), stat() и им подобные требовали от администратора ресурсов, чтобы он сначала анализировал имя пути, а затем уже выполнял нужные действия. (Именно так, кстати, все реализовано в QNX4.) Типичные проблемы этого подхода:

• Каждой функции приходится вызывать процедуру поиска.

• Для функций, у которых есть также версия, ориентированная на файловый дескриптор, драйвер должен обеспечить две отдельные точки входа: одну для версии с именем пути, и еще одну — версии с дескриптором файла.

В QNX/Neutrino же происходит следующее. Клиент создает составное сообщение — реально это одно сообщение, но оно включает в себя несколько сообщений администратору ресурсов. Без составных сообщений мы могли бы смоделировать функцию chown() чем-то таким:

int chown(const char *path, uid_t owner, gid_t group) {

 int fd, sts;

 if ((fd = open(path, O_RDWR)) == -1) {

  return (-1);

 }

 sts = fchown(fd, owner, group);

 close(fd);

 return (sts);

}

где функция fchown() — это версия функции chown(), ориентированная на файловые дескрипторы. Проблема здесь в том, что мы в этом случае используем три вызова функций (а значит, и три отдельных транзакции передачи сообщений) и привносим дополнительные накладные расходы применением функций open() и close() на стороне клиента.

При использовании составных сообщений в QNX/Neutrino непосредственно клиентским вызовом chown() создается одиночное сообщение, выглядящее примерно так:

Составное сообщение.

Сообщение состоит из двух частей. Первая часть посвящена установлению соединения (подобно сообщению, которое сгенерировала бы функция open()), вторая — вводу/выводу (эквивалент сообщения, генерируемого функцией fchown()). Никакого эквивалента функции close() здесь нет, поскольку мы выбрали сообщение типа _IO_CONNECT_COMBINE_CLOSE, которое гласит: «Открой указанное имя пути, используй полученный дескриптор файла для обработки остальной части сообщения, а когда закончишь дела или столкнешься с ошибкой, закрой дескриптор».

Написанный вами администратор ресурса даже не заметит, вызвал ли клиент функцию chown() или сначала сделал open(), а потом вызвал fchown() и далее close(). Все это скрыто базовым уровнем библиотеки.

Составные сообщения

Как выясняется, концепция составных сообщений полезна не только для экономии ресурсов вследствие уменьшения числа сообщений (как в случае с chown(), см. выше). Она также критически важна для обеспечения атомарности операций.

Предположим, что в клиентском процессе есть два или более потоков, работающих с одним дескриптором файла. Один из потоков в клиенте вызывает функцию lseek(), за которой следует read(). Все так, как мы и предполагаем. А вот если другой клиента попробует выполнить ту же самую последовательность операций с тем же самым дескриптором файла, вот тут у нас начнутся проблемы. Поскольку функции lseek() и read() друг о друге ничего не знают, то возможно, например, что первый поток выполнит lseek(), а затем будет вытеснен вторым потоком. Второй поток выполнит свои lseek() и read(), после чего освободит процессор. Проблема здесь состоит в том, что поскольку эти два потока разделяют один и тот же дескриптор файла, у первого потока теперь получается неправильное смещение lseek(), поскольку оно было изменено функциями lseek() и read() второго потока! Эта проблема проявляется также с дескрипторами файлов, которые дублируются (dup()) между процессами, не говоря уже о сети.

Очевидным решением здесь является заключение lseek() и read() в пределы действия мутекса — когда первый поток захватит мутекс, мы будем знать, что он имеет эксклюзивный доступ к дескриптору. Второй поток должен будет ждать освобождения мутекса, прежде чем он сможет творить свое безобразие с позиционированием дескриптора.

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

Давайте взглянем на библиотечный вызов readblock() (из

):

int readblock(int fd, size_t blksize, unsigned block,

 int numblks, void *buff);

(Функция writeblock() описывается аналогично.)

Вы можете вообразить для функции readblock() довольно «простенькую» реализацию:

int readblock(int fd, size_t blksize, unsigned block,

 int numblks, void *buff) {

 lseek(fd, blksize * block, SEEK_SET); // Идем к блоку

 read(fd, buff, blksize * numblks);

}

Очевидно, что от такой реализации в многопоточной среде толку мало. Нам нужно будет как минимум добавить использование мутекса:

int readblock(int fd, size_t blksize, unsigned block,

 int numblks, void *buff) {

 pthread_mutex_lock(&block_mutex);

 lseek(fd, blksize * block, SEEK_SET); // Идем к блоку

 read(fd, buff, blksize * numblks);

 pthread_mutex_unlock(&block_mutex);

}

(Мы здесь предполагаем, что мутекс уже инициализирован.)

Этот код по-прежнему уязвим для «незащищенного» доступа — если некий поток вызовет lseek() на этом файловом дескрипторе без предварительной попытки захвата мутекса, вот у нас уже и ошибка.

Решение проблемы заключается в использовании составного сообщения, аналогично вышеописанному случаю с функцией chown(). В данном случае библиотечная реализация функции readblock() помещает обе операции — lseek() и read() — в единое сообщение и посылает это сообщение администратору ресурсов:

Составное сообщение для функции readblock().

Это работает, потому что передача сообщения является атомарной операцией. С точки зрения клиента, сообщение уходит либо целиком, либо не уходит вообще. Поэтому, вмешательство «незащищенной» функции lseek() становится несущественным — когда администратор ресурсов принимает сообщение с запросом readblock(), он делает это за один прием. (Очевидно, что в этом случае пострадает сама «незащищенная» lseek(), поскольку после отработки readblock() смещение на этом дескрипторе файла будет отличаться от того, которое она хотела установить.)

А как насчет самого администратора ресурсов? Как он обрабатывает операцию readblock() за один прием? Мы вскоре рассмотрим это, когда будем обсуждать операции, выполняемые для каждого компонента составных сообщений.

Структуры данных уровня POSIX

К подпрограммам POSIX-уровня относятся три структуры данных. Отметьте, что пока речь идет о базовом уровне, вы можете использовать любые структуры данных, которые пожелаете; соответствия определенной структуре и содержанию требует именно уровень POSIX. Однако, преимущества, которые предоставляет POSIX-уровень, с лихвой окупают вносимые ограничения. Как мы увидим далее, вы также сможете дополнять эти структуры вашим собственным содержанием.

На рисунке приведены эти три структуры данных для случая, когда несколько клиентов используют администратора ресурсов, объявивший два устройства:

Структуры данных — общая схема.

Структурами данных являются:

iofunc_ocb_t
— OCB (блок открытого контекста)

Содержит информацию по каждому дескриптору файла.

iofunc_attr_t
— атрибутная запись

Содержит информацию по каждому устройству.

iofunc_mount_t
— запись точки монтирования

Содержит информацию по каждой точке монтирования.

Когда мы обсуждали таблицы функций установления соединения и ввода/вывода, мы уже видели блоки открытого контекста и атрибутные записи — в таблицах функций ввода/вывода OCB был последним передаваемым параметром. Атрибутная запись передавалась как параметр handle (третий по счету) в функциях установления соединения. Запись точки монтирования обычно представляет собой глобальную структуру и привязывается к атрибутной записи «вручную» (в инициализационном коде, написанном вами для вашего администратора ресурса).

Структура блока открытого контекста (OCB)
iofunc_ocb_t

Структура блока открытого контекста (OCB) содержит информацию по каждому дескриптору файла. Это означает, что когда клиент выполняет вызов open() и получает в ответ дескриптор файла (в противоположность коду ошибки), администратор ресурсов создает OCB и связывает его с данным клиентом. Этот OCB будет существовать до тех пор, пока клиент держит данный дескриптор файла открытым. В действительности, OCB и дескриптор файла — всегда согласованная пара. По каждому сообщению ввода/вывода от клиента библиотека администратора ресурсов автоматически ищет нужный OCB и вместе с сообщением передает его нужной функции из таблицы функций ввода/вывода. Это ответ на вопрос, зачем всем функциям ввода/вывода параметр ocb. В конце концов клиент закроет дескриптор файла (применив close()), что заставит администратор ресурса отвязать OCB от дескриптора файла и от клиента. Заметьте, что клиентская функция dup() просто увеличивает счетчик связей. В этом случае OCB отделяется от дескриптора файла и от клиента только тогда, когда значение счетчика связей достигнет нуля (то есть когда число вызовов close() будет соответствовать числу open() и dup()).

Как вы, наверное, догадываетесь, в OCB содержатся важные вещи по каждому открытию ресурса и по каждому дескриптору файла. Вот его содержание (взято из

):

typedef struct _iofunc_ocb {

 IOFUNC_ATTR_T *attr;

 int32_t    ioflag;

 CM НИЖЕ!!!   offset;

 uint16_t    sflag;

 uint16_t    flags;

} iofunc_ocb_t;

Проигнорируем пока комментарий относительно поля offset; мы вернемся к этому вопросу сразу же после данного обсуждения.

Поля структуры

iofunc_ocb_t
:

attr Указатель на атрибутную запись, связанную с данным блоком OCB. В функциях ввода/вывода вы будете встречать устоявшуюся идиому «
ocb->attr
»; она используется для получения доступа к элементам атрибутной записи.
ioflag Режим открытия, то есть как был открыт ресурс (например, «только для чтения»). Заметьте, что поле ioflag содержит режим открытия (который был передан клиентской функции open()) плюс единица. Например, режим открытия O_RDONLY (значение 0) появится в поле ioflag, как значение, равное единице (1) (константа _READ из
). Это позволяет трактовать два младших бита поля ioflag как флаги разрешения чтения и записи (
ioflag & _READ
указывает на право доступа по чтению;
ioflag & _WRITE
— по записи).
offset Текущее смещение lseek() в данном ресурсе.
sflag Флаг разделяемого использования (см.
), используемый с клиентской функцией вызова sopen(). Возможны значения SH_COMPAT, SH_DENYRW, SH_DENYWR, SH_DENYRD, и SH_DENYN
flags Системные флаги. В настоящее время поддерживаются два флага: IOFUNC_OCB_PRIVILEGED, указывающий на то, что этот OCB был создан в результате сообщения установления соединения от привилегированного процесса, и IOFUNC_OCB_MMAP, указывающий, используется ли этот OCB функцией mmap() на стороне клиента. На настоящий момент никаких других флагов не определено. Вы можете использовать биты, заданные в IOFUNC_OCB_FLAGS_PRIVATE, по своему собственному усмотрению.

Если вы хотите наряду со «стандартным» OCB сохранить какие-либо дополнительные данные, то будьте покойны — OCB можно «расширять». Мы обсудим это в разделе «Дополнительно».

Это странное поле offset

Поле offset, скажем так, как минимум любопытно. Посмотрите в

, как оно реализовано. В зависимости от того, какие у вас заданы флаги препроцессора, вы можете получить одну из шести (!) возможных раскладок поля offset. Но не беспокойтесь особо по поводу реализации — реально есть смысл рассматривать только два случая, в зависимости от того, хотите вы поддерживать 64-разрядные смещения или нет:

• если да, то поле offset 64-разрядное;

• если нет (у вас 32-разрядные целые), то поле offset — это младшие 32 бита; старшие 32 бита хранятся в поле offset_hi.

Для наших целей, если речь не идет о явном противопоставлении 32- и 64-разрядных значений, мы будем предполагать, что все смещения являются 64-разрядными (типа

off_t
), и платформа знает, что делать с 64-разрядными числами.

Атрибутная запись
iofunc_attr_t

В то время как OCB был определен как структура данных по каждому дескриптору файла, атрибутная запись является структурой данных по каждому устройству. Вы видели, что стандартный OCB типа

iofunc_ocb_t
имеет элемент, называемый attr, который представляет собой указатель на атрибутную запись. Это сделано для того, чтобы у OCB был доступ к информации об устройстве. Давайте посмотрим на атрибутную запись (взято из
):

typedef struct _iofunc_attr {

 IOFUNC_MOUNT_T      *mount;

 uint32_t         flags;

 int32_t          lock_tid;

 uint16_t         lock_count;

 uint16_t         count;

 uint16_t         rcount;

 uint16_t         wcount;

 uint16_t         rlocks;

 uint16_t         wlocks;

 struct _iofunc_mmap_list *mmap_list;

 struct _iofunc_lock_list *lock_list;

 void           *list;

 uint32_t         list_size;

 СМ_НИЖЕ!!!        nbytes;

 СМ_НИЖЕ!!!        inode;

 uid_t           uid;

 gid_t            gid;

 time_t          mtime;

 time_t          atime;

 time_t          ctime;

 mode_t          mode;

 nlink_t          nlink;

 dev_t          rdev;

} iofunc_attr_t;

У полей nbytes и inode такой же набор директив условной компиляции, что и у поля offset в OCB (см. параграф «Это странное поле offset»).

Заметьте, что некоторые из полей атрибутной записи полезны только для вспомогательных функций POSIX.

Давайте рассмотрим поля в индивидуальном порядке:

mount Указатель на необязательную запись точки монтирования (типа
iofunc_mount_t
). Он применяется аналогично указателю на атрибутную запись, входящему в состав в OCB, за исключением того, что здесь это поле может принимать NULL — в этом случае для записи точки монтирования применяются установки по умолчанию (см. ниже параграф «Запись точки монтирования
iofunc_mount_t
»). Как уже было упомянуто, запись точки монтирования привязывается к атрибутной записи «вручную» в инициализационном коде вашего администратора ресурсов.
flags Содержит флаги, которые описывают состояние других полей атрибутной записи. Мы вскоре к ним вернемся.
lock_tid Для предупреждения проблем синхронизации доступ множества потоков к одной и той же атрибутной записи должен быть взаимно исключающим. Поле lock_tid содержит идентификатор потока (thread ID), которым данная атрибутная запись блокирована в настоящий момент.
lock_count Указывает, сколько потоков пытаются использовать данную атрибутную запись. Нулевое значение указывает на то, что структура не заблокирована. Значение, большее нуля (единица или более) указывает на то, что данную структуру используют один или более потоков.
count Указывает на число OCB, которые по какой-либо причине открыли эту атрибутную запись. Например, если у одного клиента есть OCB, открытый на чтение, у другого — другой OCB, открытый на чтение/запись, и оба эти OCB указывают на одну и ту же атрибутную запись, то значение count для нее должно быть равно 2. Это будет указывать на то, что данный ресурс открыт двумя клиентами.
rcount Число читателей. В примере, приведенном для count, rcount будет также иметь значение 2, потому что ресурс открыт на чтение двумя клиентами.
wcount Число писателей. В примере, приведенном для count, wcount будет иметь значение 1, потому что ресурс открыт на чтение только одним клиентом.
rlocks Показывает число OCB, наложивших на данный ресурс блокировки по чтению. Если значение этого поля равно нулю, это означает, что никаких блокировок по чтению нет, но могут быть блокировки по записи.
wlocks Аналогично rlocks, только для блокировок по записи.
mmap_list Для внутреннего использования POSIX-функцией iofunc_mmap_default().
lock_list Для внутреннего использования POSIX-функцией iofunc_lock_default().
list Зарезервировано.
list_size Размер области, зарезервированной под поле list.
nbytes Размер ресурса в байтах. Например, если ресурс описывает конкретный файл, и этот файл имеет размер 7756 байт, то поле nbytes будет содержать значение 7756.
inode Содержит порядковый номер файла или ресурса; он должен быть уникален для каждой точки монтирования. Значение поля inode никогда не должно быть нулевым, потому что нуль указывает на неиспользуемый файл.
uid Идентификатор пользователя владельца данного ресурса.
gid Идентификатор группы владельца данного ресурса.
mtime Время последней модификации файла, обновленное или как минимум ставшее недействительным вследствие обработки клиентской функции write().
atime Время последнего доступа к файлу, обновленное или как минимум ставшее недействительным вследствие обработки клиентской функции read(), возвратившей ненулевое количество прочитанных байт.
ctime Время последнего изменения файла, обновленное или как минимум ставшее недействительным вследствие обработки клиентских функций write(), chown() или chmod().
mode Режим доступа к файлу. Содержит стандартные значения S_* из
(например, S_IFCHR), или восьмеричные значения (например, 0664), указывающие на режим доступа для владельца объекта (owner), группы (group) и всех остальных (other).
nlink Число связей (линков) файла, возвращаемое клиентским вызовом stat().
rdev Для специальных символьных устройств это поле состоит из старшего (major) и младшего (minor) кодов устройства (10 младших бит — младший код, старшие 6 бит — старший). Для устройств другого типа это поле содержит номер устройства (подробности см. ниже в параграфе «О номерах устройств, индексных дескрипторах и нашем друге rdev»).

Как и в случае с OCB, вы можете расширять «стандартную» атрибутную запись вашими собственными данными — см. раздел «Дополнительно».

Запись точки монтирования
iofunc_mount_t

Запись точки монтирования содержит информацию, общую для нескольких атрибутных записей.

Вот содержимое записи точки монтирования (взято из

):

typedef struct _iofunc_mount {

 uint32_t flags;

 uint32_t conf;

 dev_t     dev;

 int32_t     blocksize;

 iofunc_funcs_t *funcs;

} iofunc_mount_t;

Поле flags содержит только один флаг — IOFUNC_MOUNT_32BIT. Этот флаг указывает на то, что параметр offset в OCB и параметры nbytes и inode в атрибутной записи являются 32-разрядными. Заметьте, что вы можете определять ваши собственные флаги в поле flags, используя биты, определенные константой IOFUNC_MOUNT_FLAGS_PRIVATE.

Поле conf содержит следующие флаги:

IOFUNC_PC_CHOWN_RESTRICTED

Указывает, что файловая система является «chown-ограниченной», то есть никто, кроме суперпользователя (root), не может применять к файлам операцию chown().

IOFUNC_PC_NO_TRUNC

Указывает на то, что файловая система не выполняет усечение имен.

IOFUNC_PC_SYNC_IO

Указывает на то, что файловая система поддерживает синхронные операции ввода-вывода.

IOFUNC_PC_LINK_DIR

Указывает на то, что допускается создание/уничтожение связей (linking/unilnking) для каталогов.

Поле dev содержит номер устройства и описывается ниже в параграфе «О номерах устройств, индексных дескрипторах и нашем друге rdev».

Поле blocksize описывает типовой для данного устройства размер блока в байтах. Например, для дисковых устройств типовым значением будет 512.

И наконец, поле funcs указывает на следующую структуру (взято из ):

typedef struct _iofunc_funcs {

 unsigned nfuncs;

 IOFUNC_OCB_T *(*ocb_calloc)(

 resmgr_context_t *ctp, IOFUNC_ATTR_T *attr);

 void (*ocb_free)(IOFUNC_OCB_T *ocb);

} iofunc_funcs_t;

Как и в таблицах функций установления соединения и ввода/вывода, поле nfuncs должен содержать текущий размер таблицы. Используйте для этого константу _IOFUNC_NFUNCS.

Указатели на функции ocb_calloc и ocb_free могут быть заполнены адресами функций, которые следует вызывать всякий раз при создании и уничтожении OCB. Зачем вам могут понадобиться эти функции — мы обсудим это чуть позже, когда будем говорить о расширении OCB.

О номерах устройств, индексных дескрипторах и нашем друге rdev

Запись точки монтирования содержит поле с именем dev. Атрибутная запись содержит два поля: inode и rdev. Давайте рассмотрим их взаимосвязь на примере традиционной дисковой файловой системы. Файловая система монтируется на блок- ориентированном устройстве (которое представляет собой весь диск целиком). Это блок-ориентированное устройство может называться, скажем,

/dev/hd0
(первый жесткий диск в системе). На этом диске может быть несколько разделов, один из которых вполне может называться
/dev/hd0t77
(первый раздел файловой системы QNX на этом конкретном устройстве). И, наконец, на этом разделе может находиться произвольное число файлов, один из которых может иметь имя
/hd/spud.txt
.

Поле dev («device number» — «номер устройства») содержит число, уникальное для узла, на котором зарегистрирован данный администратор ресурсов. Поле rdev — значение dev для корневого устройства (root device). И, наконец, поле inode — порядковый номер файла.

Попробуем соотнести это с нашим примером дисковой системы. В приведенной ниже таблице приведен ряд чисел; взглянем на таблицу, а потом посмотрим, откуда появились эти номера и как они соотносятся.

Устройство dev inode rdev
/dev/hd0 6 2 1
/dev/hd0t77 1 12 77
/hd/spud.txt 77 47343 -

Для «сырого» блок-ориентированного устройства

/dev/hd0
значения dev и inode назначил администратор процессов (значения 6 и 2 в таблице, см. выше). Администратор ресурсов при старте получил для устройства уникальное значение rdev (число 1).

Для раздела

/dev/hd0t77
значение dev взялось из значения rdev «сырого» блок-ориентированного устройства (та самая 1). Значение inode было выбрано администратором ресурсов как уникальное в пределах rdev. Так появилась цифра 12. Наконец, значение rdev было также выбрано администратором ресурсов — здесь разработчик администратора выбрал значение 77, потому что оно соответствует типу раздела.

И наконец, для файла

/hd/spud.txt
, значение dev (77) было взято из значения rdev для раздела. Значение inode было выбрано администратором ресурса (в случае файла номер выбирается так, чтобы соответствовать некоторому внутреннему представлению файла — конкретное число не имеет значения, лишь бы оно было ненулевым и уникальным в пределах rdev). Отсюда число 47343. Для файла поле rdev не имеет значения.

Функции-обработчики

Не все вызовы обработчиков соответствуют клиентским сообщениям. Некоторые из них синтезируются ядром, а некоторые — библиотекой.

Я организовал этот раздел следующим образом:

• общие замечания;

• замечания о функциях установления соединения;

• алфавитный список сообщений установления соединения и ввода/вывода.

Общие замечания

Каждой функции-обработчику передается внутренний контекстный блок (параметр ctp), который следует рассматривать как «только для чтения», за исключением поля iov. Как это уже упоминалось в параграфе «Внутренний контекстный блок

resmgr_context_t
», этот контекстный блок содержит несколько интересных вещей. Также, каждой функции передается указатель на сообщение (аргумент msg). Вы будете активно использовать этот указатель, поскольку он содержит параметры, которыми его для вас заполнил клиентский библиотечный вызов. Функция, которую вы пишете, должна возвращать некоторое значение (все функции описаны как возвращающие
int
).

Значения выбираются из следующего списка:

_RESMGR_NOREPLY

Указывает библиотеке администратора ресурсов, что она не должна выполнять MsgReplyv() — в предположении, что вы либо уже сделали это самостоятельно в вашей функции-обработчике, либо собираетесь сделать это несколько позже.

_RESMGR_NPARTS(n)

Указывает библиотеке администратора ресурсов при выполнении MsgReplyv() возвратить n-элементный вектор ввода/вывода (он располагается в

ctp->iov
). Ваша функция ответственна за заполнение поля iov структуры ctp и возврат _RESMGR_NPARTS с корректным числом элементов.

Память под поле iov структуры ctp выделяется динамически, и ее должно быть достаточно, чтобы вместить столько число элементов массива, сколько вы записываете в iov! Детали о настройке поля nparts_max см. выше в разделе «Управляющая структура

resmgr_attr_t
».

_RESMGR_DEFAULT

Это говорит библиотеке администратора ресурсов выполнить низкоуровневую функцию по умолчанию (это другое семейство функций; не путайте их с iofunc_*_default()!). Это возвращаемое значение вам вряд ли когда-нибудь пригодится. В общем случае оно заставляет библиотеку администратора ресурсов возвратить клиенту значение errno, равное ENOSYS, что означает «функция не поддерживается».

_RESMGR_ERRNO(errno)

(Устаревшее.) Данное возвращаемое значение использовалось для «инкапсуляции» значения errno в возвращаемое сообщением значение. Например, если бы клиент выдал запрос open() (по записи — прим. ред.) устройству, доступному только для чтения, корректно было бы возвратить код ошибки EROFS. Поскольку данный способ сделать это считается устаревшим, вы можете возвратить код ошибки непосредственно (например, при помощи

return (EROFS);
вместо громоздкого
_RESMGR_ERRNO(EROFS);
).

_RESMGR_PTR(ctp, addr, len)

Это макрос для удобства. Он берет указатель на контекст ctp и заполняет его первый элемент IOV адресом addr и длиной len, а затем возвращает библиотеке эквивалент

_RESMGR_NPARTS(1)
. Это может быть полезно для функций, возвращающих одноэлементные IOV.

Блокировки, разблокировки и обработка составных сообщений

Мы видели клиентский взгляд на составные сообщения, когда рассматривали функцию readblock() (в параграфе «Составные сообщения»). Клиент мог атомарно создать сообщение, которое содержало бы несколько «подсообщений» администратору ресурсов — в нашем примере это были сообщения, соответствующие функциям lseek() и read(). С точки зрения клиента две (или более) функций были как минимум атомарно переданы (и, вследствие самой сути обмена сообщениями, будут атомарно приняты администратором ресурсов). О чем мы еще не говорили, так это о том, как мы сможем гарантированно обеспечить атомарность обработки этих сообщений.

Данные рассуждения применимы не только к составным сообщениям, но и ко всем сообщениям, принимаемым библиотекой администратора ресурсов. Первое, что делает библиотека администратора ресурсов, — она блокирует атрибутную запись, соответствующую ресурсу, используемому полученным сообщением. Затем она обрабатывает одно или более «подсообщений», содержащихся в полученном сообщении. Затем она снова разблокирует атрибутную запись.

Это гарантирует, что поступающие сообщения обрабатываются атомарно, поскольку никакой другой поток администратора ресурсов (в случае многопоточного администратора, конечно) не может «влезть» и изменить ресурс, пока наш поток этот ресурс использует. Без блокировок два клиентских потока могли бы оба выдать то, что, по их мнению, являлось бы атомарным составным сообщением (скажем, пару «lseek()read()»). Поскольку администратор ресурсов мог выделить на обработку этих сообщений два различных потока, эти потоки могли бы в произвольном порядке вытеснять друг друга, и их lseek() могли бы друг другу помешать. Блокировки же позволяют это предотвратить, потому что каждое сообщение, получающее доступ к ресурсу, обрабатывается целиком и атомарно.

Блокировка и разблокировка ресурса выполняются вспомогательными функциями по умолчанию (iofunc_lock_ocb_default() и iofunc_unlock_ocb_default()), которые размещаются в таблице функций ввода/вывода в полях lock_ocb и unlock_ocb соответственно. Вы можете, конечно, переназначить эти функции, если хотите выполнить в процессе блокировки и разблокировки какие-либо дополнительные действия.

Заметьте, что ресурс разблокируется перед вызовом io_close(). Это необходимо, поскольку функция io_close() освободит OCB, что автоматически сделает недействительным указатель на атрибутную запись, а блокировка хранится именно там!

Замечания о функциях установления соединения

Перед тем как углубиться в отдельные сообщения, однако, есть смысл подчеркнуть, что для всех функции установления соединения структура сообщений идентична (взято из

, с небольшими изменениями):

struct _io_connect {

 // Для внутреннего использования

 uint16_t type;

 uint16_t subtype;

 uint32_t file_type;

 uint16_t reply_max;

 uint16_t entry_max;

 uint32_t key;

 uint32_t handle;

 uint32_t ioflag;

 uint32_t mode;

 uint16_t sflag;

 uint16_t access;

 uint16_t zero;

 uint8_t  eflag;

 // Для конечного пользователя

 uint16_t path_len;

 uint8_t  extra_type;

 uint16_t extra_len;

 char path[1];

};

Вы заметите, что я разделил структуру

struct _io_connect
на две части, часть «Для внутреннего использования» и часть «Для конечного пользователя».

Поля для внутреннего использования

Первая часть состоит из полей, которые библиотека администратора ресурсов использует для:

• определения типа сообщения, полученного от клиента;

• проверки сообщения на достоверность (не является ли оно дезинформацией);

• отслеживания режима доступа (используется вспомогательными функциями).

Для простоты я бы рекомендовал вам всегда применять вспомогательные функции (из семейства iofunc_*_default()) во всех функциях установления соединения. Эти функции возвратят вам признак успешного/неудачного завершения, после чего вы сможете использовать в функции установления соединения «поля для конечного пользователя».

Поля для конечного пользователя

Вторая половина полей непосредственно относится к вашей реализации функции установления соединения:

path_len и path

Имя пути (и его длина), которые являются операндом (то есть это имя пути, над которым производится действие).

extra_type и extra_len

Дополнительные параметры (имена путей, например), соответствующее данной функции установления соединения.

Чтобы получить представление о том, как поле path используется в качестве «имени пути, над которым производится действие», давайте рассмотрим что-нибудь типа функции rename(). Эта функция принимает два имени пути: «изначальное» имя пути и «новое» имя пути. Изначальное имя пути передается в path, потому что именно над ним производится операция (это имя файла, подлежащего переименованию). Новое имя пути — аргумент данной операции. Вы увидите, что параметр extra, который передается функции установления соединения, как раз содержит указатель на аргумент операции, в данном случае — на новое имя пути.

(В принятой реализации новое имя пути располагается в памяти непосредственно следом за изначальным (на которое указывает path) с учетом выравнивания, но вам делать на этот счет ничего не надо — параметр extra уже содержит правильный указатель.)

Алфавитный список функций установления соединения и ввода/вывода

В данном разделе в алфавитном порядке приведен список точек входа в функции установления соединения и ввода/вывода, которые вы можете заполнять самостоятельно (эти две таблицы затем передаются функции pathname_attach()). Помните, что если вы просто вызываете функцию iofunc_func_init(), все эти точки входа будут заполнены соответствующими значениями по умолчанию; вам следует переопределять конкретные точки входа только в том случае, если вы собираетесь обрабатывать данное конкретное сообщение самостоятельно. Ниже, в разделе «Примеры», вы увидите несколько примеров общеупотребительных функций.

Поначалу все это может показаться излишне запутанным, но отметим, что на самом деле существуют два разблокирующих вызова — один для функций установления соединения и один для функций ввода/вывода. Все вполне корректно, поскольку отражает то, когда происходит разблокирование. Вариант для функций установления соединения применяется тогда, когда ядро разблокирует клиента немедленно после отправки им сообщения установления соединения. Вариант для функций ввода/вывода применяется тогда, когда ядро разблокирует клиента немедленно после отправки им сообщения ввода/вывода.

Чтобы не путать точки входа в функции-обработчики сообщений с вызовами клиентской Си-библиотеки (например, open()), к именам всех приведенных здесь функций добавлен префикс «io_». Например, обработчик функции установления соединения open() будет называться io_open().

io_chmod()

int io_chmod(resmgr_context_t *ctp, io_chmod_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_chmod_default()

Вспомогательные функции: iofunc_chmod()

Клиентская функция: chmod(), fchmod()

Сообщения: _IO_CHMOD

Структура данных:

struct _io_chmod {

 uint16_t type;

 uint16_t combine_len;

 mode_t  mode;

};


typedef union {

 struct _io_chmod i;

} io_chmod_t;

Описание: Отвечает за изменение режима доступа к ресурсу, указанному в переданном ей параметре ocb, в значение, содержащееся в поле сообщения mode.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_chown()

int io_chown(resmgr_context_t *ctp, io_chown_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_chown_default()

Вспомогательные функции: iofunc_chown()

Клиентская функция: chown(), fchown()

Сообщения: _IO_CHOWN

Структура данных:

struct _io_chown {

 uint16_t type;

 uint16_t combine_len;

 int32_t  gid;

 int32_t  uid;

};


typedef union {

 struct _io_chown i;

} io_chown_t;

Описание: Ответственна за изменение полей идентификатора пользователя и группы для ресурса, указанному в переданном ей параметре ocb, соответственно в значения uid и gid. Отметим, что чтобы узнать, позволяет ли данная файловая система выполнять chown() кому-либо, кроме суперпользователя (root), надо проверить запись точки монтирования на предмет флага IOFUNC_PC_CHOWN_RESTRICTED, а также поле flags в OCB.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_close_dup()

int io_close_dup(resmgr_context_t *ctp, io_close_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_close_dup_default()

Вспомогательные функции: iofunc_close_dup()

Клиентская функция. close(), fclose()

Сообщения: _IO_CLOSE_DUP

Структура данных:

struct _io_close {

 uint16_t type;

 uint16_t combine_len;

};


typedef union {

 struct _io_close i;

} io_close_t;

Описание: Это реальный обработчик клиентских вызовов close() и fclose(). Отметим, что вам почти никогда не придется переназначать эту функцию; оставляйте в таблице функций ввода/вывода значение iofunc_close_dup_default(). Причиной этому служит то, что базовый уровень библиотеки отслеживает число сообщений open(), dup() и close(), выданных по каждому OCB, и синтезирует вызов io_close_ocb() (см. ниже) после получения для данного OCB последнего сообщения close(). Отметим, что идентификаторы отправителей, расположенные в

ctp->rcvid
, могут и не совпадать с переданными функции io_open(); однако, совпадение по меньшей мере одного идентификатора гарантируется. «Лишние» идентификаторы отправителей являются результатом (возможно, внутренних) вызовов типа dup().

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_close_ocb()

int io_close_ocb(resmgr_context_t *ctp, void* reserved,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода (синтезируется библиотекой)

Обработчик по умолчанию: iofunc_close_default()

Вспомогательные функции: Нет

Клиентская функция: Нет (синтезируется библиотекой)

Сообщения: Нет (синтезируется библиотекой)

Структура данных:

// Синтезируется библиотекой

struct _io_close {

 uint16_t type;

 uint16_t combine_len;

};


typedef union {

 struct _io_close i;

} io_close_t;

Описание: Это функция, которая синтезируется базовым уровнем библиотеки, когда для некоего OCB получено последнее сообщение close(). Это то самое место, где вам следует «подчистить» все перед уничтожением OCB. Отметим, что идентификатор отправителя в

ctp->rcvid
есть нуль (0), потому что данная функция синтезируется библиотекой и не обязательно соответствует какому-либо конкретному сообщению.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_devctl()

int io_devctl(resmgr_context_t *ctp, io_devctl_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_devctl_default()

Вспомогательные функции: iofunc_devctl()

Клиентская функция: devctl(), ioctl()

Сообщения: _IO_DEVCTL

Структура данных:

struct _io_devctl {

 uint16_t type;

 uint16_t combine_len;

 int32_t  dcmd;

 int32_t  nbytes;

 int32_t  zero;

};


struct _io_devctl_reply {

 uint32_t zero;

 int32_t  ret_val;

 int32_t  nbytes;

 int32_t  zero2;

};


typedef union {

 struct _io_devctl    i;

 struct _io_devctl_reply o;

} io_devctl_t;

Описание: Выполняет над устройством операцию ввода/вывода, переданную от клиентской функции devctl() в параметре dcmd. Клиент кодирует направление передачи данных двумя старшими разрядами dcmd, указывая этим, как функция devctl() должна передавать данные (поле «to» соответствует биту _POSIX_DEVDIR_TO, поле «from» — биту _POSIX_DEVDIR_FROM):

Поле «to» Поле «from» Значение
0 0 Передачи данных нет
0 1 Передача от драйвера клиенту
1 0 Передача от клиента драйверу
1 1 Двунаправленная передача

В случае, когда передачи данных нет, предполагается, что драйвер просто выполняет команду, заданную в dcmd. В случае передачи данных предполагается, что драйвер передает данные клиенту и/или обратно, используя вспомогательные функции resmgr_msgreadv() и resmgr_msgwritev(). Клиент указывает размер передачи в поле nbytes; драйвер должен установить число передаваемых байт в поле nbytes исходящей структуры.

Отметим, что структуры данных, предназначенные для ввода и вывода, дополнены нулями, чтобы быть выровненными друг относительно друга. Это означает, что неявная область данных начинается в этих структурах с того же самого адреса.

Если вы используете вспомогательную функцию iofunc_devctl(), то имейте в виду, что если она не сможет сделать что-либо с сообщением devctl(), она возвратит вам константу _RESMGR_DEFAULT. Эта сделано для отделения корректных значений errno от возвращаемого признака «нераспознанная команда». Получив _RESMGR_DEFAULT, базовый уровень библиотеки ответит установкой errno в значение ENOSYS, которое будет транслировано клиентской библиотечной функцией devctl() в значение ENOTTY, «корректное» с точки зрения POSIX.

Проверка режима открытия и сопоставление его с выполняемой операцией лежит всецело на совести вашей функции — ни в клиентской devctl(), ни в библиотеке администратора ресурсов никаких проверок не выполняется. Например, можно открыть администратор ресурса в режиме «только для чтения», а затем выдать ему посредством devctl() команду «отформатировать жесткий диск» (которая, в общем, является весьма нехилой операцией записи). Так вот, с точки зрения администратора было бы весьма предусмотрительно до выполнения такой операции сначала проверить режим открытия ресурса.

Отметим, что диапазон доступных пользователю значений dcmd ограничен (значения от 0x0000 до 0x0FFF включительно зарезервированы QSSL). Другие значения можно смело использовать — см. заголовочные файлы с именами

.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS, и буфер приема (с ответными данными, если надо).

Для примера см. ниже параграф «Простой пример функции io_devctl()».

io_dup()

int io_dup(resmgr_context_t *ctp, io_dup_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: NULL (обрабатывается базовым уровнем)

Вспомогательные функции: Нет

Клиентская функция: dup(), dup2(), fcntl(), fork(), spawn*(), fork()

Сообщения: _IO_DUP

Структура данных:

struct _io_dup {

 uint16_t     type;

 uint16_t     combine_len;

 struct _msg_info info;

 uint32_t     reserved;

 uint32_t     key;

};


typedef union {

 struct _io_dup i;

} io_dup_t;

Описание: Это обработчик сообщений dup(). Как и в случае с io_close_dup(), вы вряд ли будете обрабатывать это сообщение самостоятельно. За вас это сделает базовый уровень библиотеки.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_fdinfo()

int io_fdinfo(resmgr_context_t *ctp, io_fdinfo_t *msg,

 RESMGR_OCB_T *ocb)

Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода при помощи iofunc_func_init() и не изменять данную точку входа.

io_link()

int io_link(resmgr_context_t *ctp, io_link_t *msg,

 RESMGR_HANDLE_T* handle, io_link_extra_t* extra)

Классификация: Функция установления соединения

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_link()

Клиентская функция: link()

Сообщения: _IO_CONNECT, подтип IO_CONNECT_LINK

Структура данных:

struct _io_connect {

 // Внутренние поля (как описано выше)

 uint16_t path_len;

 uint8_t  extra_type;

 uint16_t extra_len;

 char   path[1];

};


struct _io_connect_link_reply {

 uint32_t reserved1[2];

 uint8_t  eflag;

 uint8_t  reserved2[3];

 uint32_t umask;

 uint16_t nentries;

 uint16_t path_len;

};


typedef union {

 struct _io_connect       connect;

 struct _io_connect_link_reply link_reply;

} io_link_t;


typedef union _io_link_extra {

 struct _msg_info       info;

 void             *ocb;

 char             path[1];

 struct _io_resmgr_link_extra resmgr;

} io_link_extra_t;

Описание: Создает новую связь (линк) с именем, заданным в поле path структуры msg, к уже существующему имени пути, указанному в поле path параметра extra (переданного вашей функции). Для удобства поле ocb параметра extra содержит указатель на OCB существующего имени пути.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_lock()

int io_lock(resmgr_context_t *ctp, io_lock_t *msg,

 RESMGR_OCB_T *ocb)

Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя iofunc_func_init(), и не изменять данную точку входа.

io_lock_ocb()

int io_lock_ocb(resmgr_context_t *ctp, void *reserved,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода (синтезируется библиотекой)

Обработчик по умолчанию: iofunc_lock_ocb_default()

Вспомогательные функции: Нет

Клиентская функция: Все

Сообщения: Нет (синтезируются библиотекой)

Структура данных: Нет

Описание: Эта функция отвечает за блокировку атрибутной записи, на которую указывает OCB. Это сделано для того, чтобы гарантировать одновременный доступ не более одного потока как к самому OCB, так и к соответствующей атрибутной записи. Функции блокировки (и соответствующие функции разблокировки) синтезируются библиотекой администратора ресурсов до начала обработки сообщения и после ее завершения соответственно. Более подробно это описано выше в параграфе «Составные сообщения». Вы почти никогда не будете использовать этот вызов самостоятельно; вместо этого используйте функцию POSIX-уровня по умолчанию.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_lseek()

int io_lseek(resmgr_context_t *ctp, io_lseek_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_lseek_default()

Вспомогательные функции: iofunc_lseek()

Клиентская функции: lseek(), fseek(), rewinddir()

Сообщения: _IO_LSEEK

Структура данных:

struct _io_lseek {

 uint16_t type;

 uint16_t combine_len;

 short   whence;

 uint16_t zero;

 uint64_t offset;

};


typedef union {

 struct _io_lseek i;

 uint64_t     o;

} io_lseek_t;

Описание: Обрабатывает клиентскую функцию lseek(). Отметьте, что администратору ресурса, который обрабатывает каталоги, придется также интерпретировать сообщение _IO_LSEEK для операций с каталогами. Параметры whence и offset передаются от клиентской функции lseek(). После интерпретации параметров whence и offset клиентского сообщения подпрограмма должна скорректировать у OCB параметр offset и затем возвратить новое значение offset или возвратить признак ошибки.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS, а также (не обязательно) текущее смещение.

io_mknod()

int io_mknod(resmgr_context_t *ctp, io_mknod_t *msg,

 RESMGR_HANDLE_T *handle, void *reserved)

Классификация: Функция установления соединения

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_mknod()

Клиентская функция: mknod(), mkdir(), mkfifo()

Сообщения: _IO_CONNECT, подтип _IO_CONNECT_MKNOD

Структура данных:

struct _io_connect {

 // Внутренние поля (как описано выше)

 uint16_t path_len;

 uint8_t  extra_type;

 uint16_t extra_len;

 char   path[1];

};


struct _io_connect_link_reply {

 uint32_t reserved1[2];

 uint8_t  eflag;

 uint8_t  reserved2[3];

 uint32_t umask;

 uint16_t nentries;

 uint16_t path_len;

};


typedef union {

 struct _io_connect       connect;

 struct _io_connect_link_reply link_reply;

} io_mknod_t;

Описание: Создает новую точку входа в файловую систему. Сообщение выдается для создания файла с именем, указанным в path, и типом, закодированным в поле mode (оно из «внутренних полей» структуры

struct _io_connect
и здесь не показано).

Реально это используется только для клиентских функций mkfifo() и mkdir().

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_mmap()

int io_mmap(resmgr_context_t *ctp, io_mmap_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_mmap_default()

Вспомогательные функции: iofunc_mmap()

Клиентская функция: mmap(), munmap(), mmap_device_io(), mmap_device_memory()

Сообщения: _IO_MMAP

Структура данных:

struct _io_mmap {

 uint16_t     type;

 uint16_t     combine_len;

 uint32_t     prot;

 uint64_t     offset;

 struct _msg_info info;

 uint32_t     zero[6];

};


struct _io_mmap_reply {

 uint32_t zero;

 uint32_t flags;

 uint64_t offset;

 int32_t  coid;

 int32_t  fd;

};


typedef union {

struct _io_mmap    i;

struct _io_mmap_reply o;

} io_mmap_t;

Описание: Позволяет администратору процессов применять к файлам вашего администратора ресурсов операцию mmap(). В общем случае самостоятельно программировать эту функцию не следует (используйте обработчик по умолчанию, предоставляемый iofunc_func_init()), если только вы не хотите ее преднамеренно отключить (например, драйвер последовательного порта мог бы запросто возвратить ENOSYS, поэтому для него эта операция не имеет никакого смысла).

Данную функцию администратора ресурсов может вызвать только администратор процессов

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

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_mount()

int io_mount(resmgr_context_t *ctp, io_mount_t *msg,

 RESMGR_HANDLE_T* handle, io_mount_extra_t* extra)

Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя iofunc_func_init(), и не изменять данную точку входа.

io_msg()

int io_msg(resmgr_context_t *ctp, io_msg_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: Нет

Вспомогательные функции: Нет

Клиентская функция: Нет (создается «вручную» и передается посредством MsgSend())

Сообщения: _IO_MSG

Структура данных:

struct _io_msg {

 uint16_t type;

 uint16_t combine_len;

 uint16_t mgrid;

 uint16_t subtype;

};


typedef union {

 struct _io_msg i;

} io_msg_t;

Описание: Интерфейс _IO_MSG является более общей, но менее переносимой вариацией на тему ioctl() и devctl(). Поле mgrid идентифицирует конкретный администратор — вы не должны выполнять никаких действий по запросам, не соответствующим идентификатору вашего администратора. Поле subtype фактически задает команду, которую клиент хочет выполнить. Любые неявно передаваемые данные следуют за входной структурой. Данные, возвращаемые клиенту, передаются сами по себе; код завершения возвращается через макрос _RESMGR_STATUS. Уникальный «идентификатор администратора» (manager ID) вы можете получить в QSSL.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_notify()

int io_notify(resmgr_context_t *ctp, io_notify_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_notify(), iofunc_notify_remove(), iofunc_notify_trigger()

Клиентская функция: select(), ionotify()

Сообщения: _IO_NOTIFY

Структура данных:

struct _io_notify {

 uint16_t     type;

 uint16_t     combine_len;

 int32_t     action;

 int32_t     flags;

 struct sigevent event;

};


struct _io_notify_reply {

 uint32_t zero;

 uint32_t flags;

};


typedef union {

 struct _io_notify    i;

 struct _io_notify_reply o;

} io_notify_t;

Описание: Данный обработчик отвечает за установку, опрос или удаление обработчика уведомлений. Параметры action (действие) и flags (флаги) определяют тип операции уведомления и условия; параметр event (событие) является структурой типа

struct sigevent
, которая определяет событие уведомления (если оно есть), которое клиент хочет получить. Событие event клиенту доставляется функцией MsgDeliverEvent() или функцией iofunc_notify_trigger().

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS; флаги возвращается ответным сообщением.

io_open()

int io_open(resmgr_context_t *ctp, io_open_t *msg,

 RESMGR_HANDLE_T *handle, void *extra)

Классификация: Функция установления соединения

Обработчик по умолчанию: iofunc_open_default()

Вспомогательные функции: iofunc_open(), iofunc_ocb_attach()

Клиентская функция: open(), fopen(), sopen() и др.

Сообщения: _IO_CONNECT, подтипы _IO_CONNECT_COMBINE, _IO_CONNECT_COMBINE_CLOSE и _IO_CONNECT_OPEN.

Структура данных:

struct _io_connect {

 // Внутренние поля (как описано выше)

 uint16_t path_len;

 uint8_t  extra_type;

 uint16_t extra_len;

 char   path[1];

};


struct _io_connect_link_reply {

 uint32_t reserved1[2];

 uint8_t  eflag;

 uint8_t  reserved2[3];

 uint32_t umask;

 uint16_t nentries;

 uint16_t path_len;

};


typedef union {

 struct _io_connect      connect;

 struct _io_connect_link_reply link_reply;

} io_open_t;

Описание: Это основная точка входа в администратор ресурсов. Она выполняет проверку, действительно ли клиент имеет соответствующие права на открытие файла, привязывает OCB к внутренним структурам библиотеки (посредством функций resmgr_bind_ocb() или iofunc_ocb_attach()) и возвращает errno. Отметим, что для данной функции релевантны не все поля структур ввода и вывода.

Возвращает: Код завершения, при помощи вспомогательного макроса _IO_SET_CONNECT_RET.

io_openfd()

int io_openfd(resmgx_context_t *ctp, io_openfd_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_openfd_default()

Вспомогательные функции: iofunc_openfd()

Клиентская функция: openfd()

Сообщения: _IO_OPENFD

Структура данных:

struct _io_openfd {

 uint16_t     type;

 uint16_t     combine_len;

 uint32_t     ioflag;

 uint16_t     sflag;

 uint16_t     reserved1;

 struct _msg_info info;

 uint32_t     reserved2;

 uint32_t     key;

};


typedef union {

 struct _io_openfd i;

} io_openfd_t;

Описание: Данная функция аналогична предоставляемому обработчику io_open() — за исключением того, что вместо имени пути передается дескриптор уже открытого файла (в силу передачи вам параметра ocb в вызове функции).

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_pathconf()

int io_pathconf(resmgr_context_t *ctp, io_pathconf_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_pathconf_default()

Вспомогательные функции: iofunc_pathconf()

Клиентская функция: fpathconf(), pathconf()

Сообщения: IO_PATHCONF

Структура данных:

struct _io_pathconf {

 uint16_t type;

 uint16_t combine_len;

 short  name;

 uint16_t zero;

};


typedef union {

 struct _io_pathconf i;

] io_pathconf_t;

Описание: Обработчик этого сообщения отвечает за возврат значения настраиваемого параметра name для ресурса, связанного с данным OCB. Используйте функцию по умолчанию и расширьте ее дополнительными вариантами элемента name, соответствующими вашему устройству.

Возвращает: Код завершения, при помощи вспомогательного макроса _IO_SET_PATHCONF_VALUE; флаги возвращаются в ответном сообщении.

io_read()

int io_read(resmgr_context_t *ctp, io_read_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_read_default()

Вспомогательные функции: iofunc_read(), iofunc_read_verify()

Клиентская функция: read(), readdir()

Сообщение: IO_READ

Структура данных:

struct _io_read {

 uint16_t type;

 uint16_t combine_len;

 int32_t  nbytes;

 uint32_t xtype;

};


typedef union {

 struct _io_read i;

} io_read_t;

Описание: Отвечает за чтение данных из ресурса. Клиент задает число байт, которое он готов прочитать, в элементе nbytes. Вы возвращаете данные, увеличиваете смещение в OCB и обновляете соответствующие поля с информацией о временах доступа.

Отметим, что элемент xtype может устанавливать для отдельных сообщений флаг переопределения, поэтому его надо проверять. Если вы не поддерживаете никаких расширенных флагов переопределения, вы должны возвратить EINVAL. Далее, в примерах функций io_read() и io_write(), мы рассмотрим обработку одного очень важного (и очень непростого!) флага переопределения, называемого _IO_XTYPE_OFFSET.

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

struct dirent
. За дополнительной информацией по возврату элементов каталога см. пример в параграфе «Возврат элементов каталога» раздела «Дополнительно».

Чтобы удостовериться, что файл был открыт в режиме, совместимом с операцией чтения, надо вызвать вспомогательную функцию iofunc_read_verify(). Также, следует вызвать функцию iofunc_sync_verify(), чтобы проверить, надо ли синхронизировать данные с носителем.

Возвращает: Число считанных байтов или код завершения, при помощи вспомогательного макроса _IO_SET_READ_NBYTES, а также собственно данные — ответным сообщением.

В качестве примера с возвратом только данных см. ниже раздел «Простой пример функции io_read()». Более сложный пример с одновременным возвратом как данных, так и элементов каталогов, см. в параграфе «Возврат элементов каталога» раздела «Дополнительно».

io_readlink()

int io_readlink(resmgr_context_t *ctp, io_readlink_t *msg,

 RESMGR_HANDLE_T *handle, void* reserved)

Классификация: Функция установления соединения

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_readlink()

Клиентская функция: readlink()

Сообщения: IO_CONNECT, подтип IO_CONNECT_READLINK

Структура данных:

struct _io_connect {

 // Внутренние поля (как описано выше)

 uint16_t path_len;

 uint8_t  extra_type;

 uint16_t extra_len;

 char   path[1];

};


struct _io_connect_link_reply {

 uint32_t reserved1[2];

 uint8_t  eflag;

 uint8_t  reserved2[3];

 uint32_t umask;

 uint16_t nentries;

 uint16_t path_len;

};


typedef union {

 struct _io_connect       connect;

 struct _io_connect_link_reply link_reply;

} io_open_t;

Описание: Отвечает за чтение содержимого символьной связи (линка), как определено полем path входной структуры. Возвращаемые байты представляют собой содержимое символьной связи; возвращаемый код состояния представляет собой число байт в ответе. Допустимый возврат должен быть сделано только для символьной связи. Все другие доступы должны возвратить код ошибки.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS, и данные в ответном сообщении.

io_rename()

int io_rename(resmgr_context_t *ctp, io_rename_t *msg,

 RESMGR_HANDLE_T *handle, io_rename_extra_t* extra)

Классификация: Функция установления соединения

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_rename()

Клиентская функция: rename()

Сообщение: _IO_CONNECT, подтип _IO_CONNECT_RENAME

Структура данных:

struct _io_connect {

 // internal fields (as described above)

 uint16_t path_len;

 uint8_t  extra_type;

 uint16_t extra_len;

 char   path[1];

};


struct _io_connect_link_reply {

 uint32_t reserved1[2];

 uint8_t  eflag;

 uint8_t  reserved2[3];

 uint32_t umask;

 uint16_t nentries;

 uint16_t path_len;

};


typedef union _io_rename_extra {

 char path[1];

} io_rename_extra_t;


typedef union {

 struct _io_connect       connect;

 struct _io_connect_link_reply link_reply;

} io_rename_t;

Описание: Выполняет операцию переименования, получив на вход первоначальное имя в элементе path и новое имя в поле path переданного параметра extra. Замечание по реализации: для первоначального имени задается имя пути (а не OCB) — это делается специально для случая переименования файла, который является жесткой связью к другому файлу. Если бы был задан OCB, две (или более) жестких связей к одному и тому же файлу различить было бы нельзя.

Данная функция будет вызываться только для тех двух имен файлов, которые принадлежат одной и той же файловой системе (то есть одному и тому же устройству). Поэтому в проверке случаев, в которых надо было бы возвращать EXDEV, нет никакой необходимости. Это ничуть не мешает вам возвращать EXDEV — например, если вы не хотите выполнять rename() самостоятельно (например, операция переименования из одного каталога в другой может оказаться очень сложной). В случае возврата EXDEV командно-строковая утилита

mv
выполнит сначала
cp
, а потом
rm
(библиотечная функция rename() этого не сделает — она просто установит errno в EXDEV).

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

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_shutdown()

int io_shutdown(resmgr_context_t *ctp, io_shutdown_t *msg,

 RESMGR_OCB_T *ocb)

Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя iofunc_func_init(), и не изменять данную точку входа.

io_space()

int io_space(resmgr_context_t *ctp, io_space_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_space_verify()

Клиентская функция: chsize(), fcntl(), ftruncate(), ltrunc()

Сообщение _IO_SPACE

Структура данных:

struct _io_space {

 uint16_t type;

 uint16_t combine_len;

 uint16_t subtype;

 short   whence;

 uint64_t start;

 uint64_t len;

};


typedef union {

 struct _io_space i;

 uint64_t     o;

} io_space_t;

Описание: Эта функция применяется для выделения или освобождения занимаемого ресурсом пространства. Параметр subtype («подтип») указывает на то, следует ли это пространство выделить (если равен F_ALLOCSP) или освободить (если равен F_FREESP). Комбинация параметров whence («откуда») и start («начало») указывает, где следует начать выделение/ освобождение; элемент len указывает размер операции.

Возвращает: Число байтов (размер ресурса), посредством вспомогательного макроса _RESMGR_STATUS.

io_stat()

int io_stat(resmgr_context_t *ctp, io_stat_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_stat_default()

Вспомогательные функции: iofunc_stat()

Клиентская функция: stat(), lstat(), fstat()

Сообщения: _IO_STAT

Структура данных:

struct _io_stat {

 uint16_t type;

 uint16_t combine_len;

 uint32_t zero;

};


typedef union (

 struct _io_stat i;

 struct stat   o;

} io_stat_t;

Описание: Обрабатывает сообщение, запрашивающее информацию о ресурсе, связанном с переданным OCB. Заметьте, что атрибутная запись содержит всю информацию, необходимую для выполнения запроса stat(). Вспомогательная функция iofunc_stat() заполняет структуру

struct stat
, базированную на атрибутной записи. Эта вспомогательная функция также изменяет сохраненные элементы dev/rdev так, чтобы они были уникальны с точки зрения единичного узла (это используется для выполнения вызовов stat() в отношении файлов по сети). Писать этот обработчик самостоятельно особого смысла нет.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS, и структуру

struct stat
— в ответном сообщении.

io_sync()

int io_sync(resmgr_context_t *ctp, io_sync_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_sync_default()

Вспомогательные функции: iofunc_sync_verify(), iofunc_sync()

Клиентская функция: fsync(), fdatasync()

Сообщения: _IO_SYNC

Структура данных:

struct _io_sync {

 uint16_t type;

 uint16_t сombine_len;

 uint32_t flag;

};


typedef union {

 struct _io_sync i;

} io_sync_t;

Описание: Это точка входа команды flush (синхронизация носителя информации с буферами — например, диска с дисковым кэшем — прим. ред.). Вспомогательная функция iofunc_sync() принимает поле flag входного сообщения и возвращает одно из следующих значений, которые указывают, какие действия ваш администратор ресурсов должен выполнить:

• 0 — не делать ничего;

• O_SYNC — все, что связано с файлом (включая содержимое файла, элементы каталогов, индексные дескрипторы (inodes), и т.д.) должно присутствовать на носителе и должно быть восстанавливаемым с него.

• O_DSYNC — присутствовать на носителе и быть восстанавливаемыми с него должны только данные файла.

Отметим, что эта функция будет вызываться только в том случае, если вы согласились поддерживать функцию

sync
, установив соответствующий флаг в записи точки монтирования.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_umount()

int io_umount(resmgr_context_t *ctp, void *msg,

 RESMGR_OCB_T *ocb)

Данная функция зарезервирована QSSL для будущего использования. Вам следует инициализировать таблицу функций ввода/вывода, используя iofunc_func_init(), и не изменять данную точку входа.

io_unblock() [установление соединения]

int io_unblock(resmgr_context_t *ctp, io_pulse_t *msg,

 RESMGR_HANDLE_T *handle, void* reserved)

Классификация: Функция установления соединения (синтезируется ядром и библиотекой)

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_unblock()

Клиентская функция: Нет (вызывается ядром вследствие сигнала или тайм-аута)

Сообщения: Нет (синтезируется библиотекой)

Структура данных: (см. вариант io_unblock() для ввода/вывода, ниже)

Описание: Это версия разблокирующего вызова в форме сообщения установления соединения, синтезируемая библиотекой в ответ на импульс от ядра, возникший в результате желания клиента разблокироваться на этапе установления соединения. См. вариант этой функции для ввода/вывода ниже.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

См. подробное обсуждение стратегий разблокирования в главе «Обмен сообщениями», параграф «Применение флага _NTO_MI_UNBLOCK_REQ».

io_unblock() [ввод/вывод]

int io_unblock(resmgr_context_t *ctp, io_pulse_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода (синтезируется ядром и библиотекой)

Обработчик по умолчанию: iofunc_unblock_default()

Вспомогательные функции: iofunc_unblock()

Клиентская функция: Нет (реакция ядра на сигнал или тайм-аут)

Сообщения: Нет (синтезируется библиотекой)

Структура данных: указательна структуру, содержащую прерываемое сообщение

Описание: Это версия разблокирующего вызова для сообщения ввода/вывода, синтезируемая библиотекой в результате импульса от ядра, возникшего вследствие попытки клиента разблокироваться на этапе ввода/вывода. Обработчик io_unblock() для фазы установления соединения почти аналогичен (см. предыдущий параграф).

Общим для обеих обработчиков разблокировки (как для функций установления соединения, так и для функций ввода/вывода) является желание клиента разблокироваться с разрешения администратора ресурсов. Администратор ресурсов обязан ответить на клиентское сообщение, чтобы разблокировать клиента. (Мы это обсуждали в главе «Обмен сообщениями», когда говорили о флагах функции ChannelCreate() — в частности, о флаге _NTO_CHF_UNBLOCK).

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

Подробное обсуждение стратегий разблокирования см. в разделе «Применение флага _NTO_MI_UNBLOCK_REQ» в главе «Обмен сообщениями».

io_unlink()

int io_unlink(resmgr_context_t *ctp, io_unlink_t *msg,

 RESMGR_HANDLE_T* handle, void* reserved)

Классификация: Функция установления соединения

Обработчик по умолчанию: Нет

Вспомогательные функции: iofunc_unlink()

Клиентская функция: unlink()

Сообщение: _IO_CONNECT, подтип _IO_CONNECT_UNLINK

Структура данных:

struct _io_connect {

 // Внутренние поля (как описано выше)

 uint16_t path_len;

 uint8_t  extra_type;

 uint16_t extra_len;

 char   path[1];

};


struct _io_connect_link_reply {

 uint32_t reserved1[2];

 uint8_t  eflag;

 uint8_t  reserved2[3];

 uint32_t umask;

 uint16_t nentries;

 uint16_t path_len;

};


typedef union {

 struct _io_connect       connect;

 struct _io_connect_link_reply link_reply;

} io_unlink_t;

Описание: Отвечает за уничтожение связей (unlinking) файла, имя пути которого передается в поле path структуры входящего сообщения.

Возвращает: Состояние по применению вспомогательного макроса _RESMGR_STATUS.

io_unlock_ocb()

int io_unlock_ocb(resmgr_context_t *ctp, void *reserved,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода (синтезируется библиотекой)

Обработчик по умолчанию: iofunc_unlock_ocb_default()

Вспомогательные функции: Нет

Клиентская функция: Все

Сообщения: Нет (синтезируется библиотекой)

Структура данных: Нет

Описание: Действует противоположно вышеописанной функции io_lock_ocb(), т.е. отвечает за разблокирование атрибутной записи, на которую указывает OCB. Эта операция освобождает атрибутную запись, чтобы другие администратора ресурсов могли с ней работать. Подробности см. выше в разделе «Составные сообщения».

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_utime()

int io_utime(resmgr_context_t *ctp, io_utime_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_utime_default()

Вспомогательные функции: iofunc_utime()

Клиентская функция: utime()

Сообщения: _IO_UTIME

Структура данных:

struct _io_utime {

 uint16_t    type;

 uint16_t    combine_len;

 int32_t     cur_flag;

 struct utimbuf times;

};


typedef union {

 struct _io_utime i;

} io_utime_t;

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

IOFUNC_ATTR_*
в атрибутной записи. Вам почти никогда не придется самостоятельно использовать этот обработчик; вместо этого вы будете использовать вспомогательную функцию POSIX-уровня.

Возвращает: Код завершения, при помощи вспомогательного макроса _RESMGR_STATUS.

io_write()

int io_write(resmgr_context_t *ctp, io_write_t *msg,

 RESMGR_OCB_T *ocb)

Классификация: Функция ввода/вывода

Обработчик по умолчанию: iofunc_write_default()

Вспомогательные функции: iofunc_write_verify()

Клиентская функция: write(), fwrite(), и т.п.

Сообщения: _IO_WRITE

Структура данных:

struct _io_write {

 uint16_t type;

 uint16_t combine_len;

 int32_t  nbytes;

 uint32_t xtype;

};


typedef union {

 struct _io_write i;

} io_write_t;

Описание: Данный обработчик отвечает за получение данных, которые клиент записал в администратор ресурсов. Обработчику передается число байт, которые клиент пытается записать, в элементе nbytes; данные неявно следуют за входной структурой (если параметр xtype не установлен в _IO_XTYPE_OFFSET; см. ниже параграф «Простой пример функции io_write()»!). Согласно реализации, потребуется повторное считывание от клиента части сообщения с данными при помощи функции resmgr_msgreadv() или ей эквивалентной. Код завершения дает число байт, фактически записанных, либо устанавливает признак ошибки в errno.

Отметьте, что чтобы удостовериться, что файл был открыт в режиме, совместимом с записью, следует вызвать вспомогательную функцию iofunc_write_verify(). Также следует вызывать функцию iofunc_sync_verify() для проверки необходимости синхронизации данных с носителем.

Возвращает: Код завершения, при помощи вспомогательного макроса _IO_SET_WRITE_NBYTES.

Пример см. ниже в параграфе «Простой пример функции io_write()».

Примеры

Этот раздел — своего рода «кулинарная книга» для программистов. Здесь я приведу ряд готовых примеров, которые вы сможете непосредственно использовать в качестве базиса для ваших проектов. Это не совсем готовые администраторы ресурсов — вы должны будете дополнить их функциями работы с пулами потоков и «каркасом» диспетчеризации (о котором речь ниже), а также удостовериться, что ваши версии функций- обработчиков помещаются в соответствующие таблицы функций после вызова iofunc_func_init(), чтобы корректно переопределить значения по умолчанию!

Я начну с ряда простых примеров, демонстрирующих базовые функциональные возможности обработчиков различных сообщений, таких как:

io_read()

io_write()

io_devctl() (без передачи данных)

io_devctl() (с передачей данных)

Затем, в разделе «Дополнительно», мы рассмотрим обработчик io_read(), который обеспечивает возврат элементов каталога.

Базовый каркас администратора ресурсов

Приведенный ниже пример можно использовать в качестве шаблона для многопоточного администратора ресурсов. (Шаблон однопоточного администратора ресурсов мы уже рассматривали — это было в разделе «Библиотека администратора ресурсов», когда мы обсуждали администратор

/dev/null
).

#include 

#include 

#include 

#include 

#include 


static resmgr_connect_funcs_t connect_func;

static resmgr_io_funcs_t io_func;

static iofunc_attr_t attr;


main(int argc, char **argv) {

 thread_pool_attr_t pool_attr;

 thread_pool_t   *tpp;

 dispatch_t     *dpp;

 resmgr_attr_t    resmgr_attr;

 resmgr_context_t  *ctp;

 int         id;

 if ((dpp = dispatch_create()) == NULL) {

  fprintf(stderr,

  "%s: Ошибка выделения контекста диспетчеризации\n",

   argv[0]);

  return (EXIT_FAILURE);

 }

 memset(&pool_attr, 0, sizeof(pool_attr));

 pool_attr.handle = dpp;

 pool_attr.context_alloc = resmgr_context_alloc;

 pool_attr.block_func = resmgr_block;

 pool_attr.handler_func = resmgr_handler;

 pool_attr.context_free = resmgr_context_free;


 // 1) Настроить пул потоков

 pool_attr.lo_water = 2;

 pool_attr.hi_water = 4;

 pool_attr.increment = 1;

 pool_attr.maximum = 50;

 if ((tpp =

  thread_pool_create(&pool_attr, POOL_FLAG_EXIT_SELF))

   == NULL) {

  fprintf(stderr,

   "%s: Ошибка инициализации пула потоков\n",

  argv[0]);

  return (EXIT_FAILURE);

 }

 iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_func,

  _RESMGR_IO_NFUNCS, &io_func);

 iofunc_attr_init(&attr, S_IFNAM | 0777, 0, 0);


 // 2) Переопределить функции установления соединения

 // и функции ввода/вывода, как надо

 memset(&resmgr_attr, 0, sizeof(resmgr_attr));

 resmgr_attr.nparts_max = 1;

 resmgr_attr.msg_max_size = 2048;


 // 3) Замените «/dev/whatever» на нужный префикс

 if ((id =

  resmgr_attach(dpp, &resmgr_attr, "/dev/whatever",

   _FTYPE_ANY,

  0, &connect_func, &io_func, &attr)) == -1) {

  fprintf(stderr, "%s: Ошибка регистрации префикса\n",

   argv[0]);

  return (EXIT_FAILURE);

 }

 // Отсюда возврата не будет

 thread_pool_start(tpp);

}

Дополнительную информацию об интерфейсе диспетчеризации (т.е., о функции dispatch_create()), см. в справочном руководстве по Си-библиотеке (С Library Reference).

Этап 1

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

/dev/null
. Как только базовая функциональность у вас заработает, вы сможете затем добавить многопоточность. Вам нужно будет задать параметры lo_water, hi_water, increment и maximum структуры pool_attr, как это было описано в главе «Процессы и потоки» в обсуждениях функций пула потоков.

Этап 2

Здесь мы дополняем администратор ресурсов нашими функциями. Эти функции представляют собой функции- обработчики сообщений, о которых мы только что говорили (например, io_read(), io_devctl(), и т.п.). Например, чтобы добавить свой собственный обработчиком для сообщения _IO_READ, указывающий на функцию my_io_read(), мы должны были бы добавить в программу такую строчку:

io_func.io_read = my_io_read;

Это переопределит элемент таблицы, инициализированный вызовом iofunc_func_init() и содержавший функцию POSIX-уровня по умолчанию, заменив его указателем на вашу функцию my_io_read().

Этап 3

Вы, вероятно, не захотите, чтобы ваш администратор ресурсов назывался

/dev/whatever
(букв. — «
/dev/абы_что
» — прим. ред.), так что вам придется выбрать для него соответствующее имя. Отметим, что привязка атрибутной записи (параметр attr) к регистрируемому префиксу осуществляется вызовом resmgr_attach() — если бы нам было надо, чтобы наш администратор обрабатывал несколько устройств, нам пришлось бы вызывать resmgr_attach() несколько раз, каждый раз с новой атрибутной записью, чтобы на этапе выполнения можно было отличить зарегистрированные префиксы друг от друга.

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

Чтобы проиллюстрировать, как ваш администратор ресурса мог бы возвращать клиенту данные, рассмотрим простейший администратор ресурса, который всегда возвращает одну и ту же строковую константу «

Здравствуй, мир!\n
». Даже в таком простом случае необходимо учесть ряд моментов, как-то:

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

• обработка EOF;

• поддерживание контекстной информации (индекс lseek());

• обновление POSIX-информации stat().

Учет размеров областей данных

В нашем случае администратор ресурсов возвращает фиксированную строку длиной в 17 байт, то есть размер доступных данных точно известен и постоянен. Эти аналогично случаю с дисковым файлом, доступным только для чтения и содержащим рассматриваемую строку. Единственное реальное отличие состоит в том, что этот «файл» обеспечивается в нашей программе строкой:

char *data_string = "Здравствуй, мир!\n";

С другой стороны, клиент может запросить чтение любого объема данных — один байт, 17 байт или более. Это должно отразиться на характеристиках вашей реализации io_read() ее умением согласовывать размер запрашиваемых клиентом данных с размером данных, имеющихся в наличии.

Обработка EOF

Особым случаем согласования размеров областей данных является обработка EOF для строки фиксированной длины. Как только клиент считал заключительный символ «

\n
», дальнейшие его попытки считать данные должны возвращать EOF.

Поддерживание контекстной информации

И «учет размеров областей данных», и «обработка EOF» требуют, чтобы в OCB, передаваемом вашей функции io_read(), поддерживалась контекстная информация — в частности, поле offset.

Обновление информации POSIX

И еще одно заключительное соображение: при чтении данных из ресурса должна обновляться POSIX-переменная времени доступа atime («access time» — «время доступа»). Это делается для того, чтобы клиентская функция stat() могла обнаружить, что к устройству кто-то обращался.

Собственно код

Ниже приведена программа, в которой учтены все вышеперечисленные моменты. Ниже мы ее последовательно проанализируем.

/*

 * io.read1.c

*/


#include 

#include 

#include 

#include 


// наша строка с данными

char* data_string = "Здравствуй, мир!\n";


int io_read(resmgr_context_t* ctp, io_read_t* msg,

 iofunc_ocb_t* ocb) {

 int sts;

 int nbytes;

 int nleft;

 int off;

 int xtype;

 struct _xtype_offset* xoffset;


 // 1) Проверить, открыто ли устройство на чтение

 if ((sts ==

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

  return sts;

 }


 // 2) проверить и обработать переопределение XTYPE

 xtype = msg->i.xtype & _IO_XTYPE_MASK;

 if (xtype == _IO_XTYPE_OFFSET) {

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

  off = xoffset->offset;

 } else if (xtype = _IO_XTYPE_NONE) {

  off = ocb->offset;

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

  return ENOSYS;

 }


 // 3) Сколько байт осталось?

 nleft = ocb->attr->nbytes – off;


 // 4) Сколько байт мы можем отдать клиенту?

 nbytes = min(nleft, msg->i.nbytes);


 // 5) Если возвращаем данные, отдать их клиенту

 if (nbytes) {

  MsgReply(ctp->rcvid, nbytes, data_string+off, nbytes);

  // 6) Установить значение "atime" для POSIX stat()

  ocb->attr->flags |=

  IOFUNC_ATTR_ATIME | IOFUNC_ATTR_DIRTY_TIME;

  // 7) Если индекс lseek() не равен _IO_XTYPE_OFFSET,

  // увеличить его на число считанных байт

  if (xtype == _IO_XTYPE_NONE) {

  ocb->offset += nbytes;

  }

 } else {

 // 8) Не возвращаем данные, просто разблокировать клиента

 MsgReply(ctp->rcvid, EOK, null, 0);

 }


 // 9) Сказать библиотеке, что мы уже ответили сами

 return _RESMGR_NOREPLY;

}

Этап 1

Здесь мы убедились, что клиентский вызов open() действительно запросил открытие устройства на чтение. Если бы клиент открыл устройство только на запись, а затем попытался выполнить чтение, это следовало бы расценивать как ошибку. В этом случае вспомогательная функция iofunc_read_verify() возвратила бы нам (затем мы — библиотеке, а библиотека — клиенту) EBADF, а не EOK.

Этап 2

Здесь мы проверили, указал ли клиент индивидуальное для данного сообщения переопределение типа (xtype-override) (например, потому что если мы открыли устройство в неблокирующем режиме, то это указало бы, что для данного конкретного запроса мы хотим задать блокирующее поведение). Отметим, что блокирующий аспект переопределенияа типа может быть отражён в последнем параметре функции iofunc_read_verify(), однако, поскольку мы приводим здесь упрощенный пример, мы передаем NULL, указывая этим, что этот вопрос нас не волнует.

Более важно, однако, посмотреть, как обрабатываются конкретные модификаторы xtype. Очень интересен, например, модификатор _IO_XTYPE_OFFSET, который, если присутствует, указывает на то, что принятое от клиента сообщение содержит смещение, и что операция чтения не должна изменять «текущую позицию файла» для данного файлового дескриптора (так делает, например, функция pread()). Если модификатор _IO_XTYPE_OFFSET не указан, то операция чтения может смело модифицировать «текущую позицию файла». Мы используем переменную хtype для сохранения xtype, содержавшегося в принятом сообщении, и переменную off для представления текущего смещения, которое мы должны будем использовать при обработке. Далее, на этапе 7, вы увидите еще кое-какие действия по обработке модификатора _IO_XTYPE_OFFSET.

Если присутствует иное переопределение xtype, чем _IO_XTYPE_OFFSET (и это не пустая команда _IO_XTYPE_NONE), мы отказываемся обрабатывать запрос и возвращаем ENOSYS. Это просто означает, что мы не знаем, как обрабатывать такую ситуацию, и поэтому возвращаем клиенту признак ошибки.

Этапы 3 и 4

Чтобы вычислить, сколько байт мы можем реально возвратить клиенту, мы выполняем этапы 3 и 4, в которых выясняется, сколько байт доступно у устройства (разность между полным объемом устройства, полученным из

ocb->attr->nbytes
, и текущим смещением в устройстве). Узнав, сколько байт осталось, мы выбираем наименьшее значение между размером этого остатка и количеством байт, которые клиент хочет прочитать. Например, у нас может остаться семь байт, а клиент захочет прочитать только два. В этом случае мы возвратим клиенту только два байта. И наоборот, если клиент захочет прочитать 4096 байт, а у нас осталось только семь, мы сможем возвратить ему только семь байт.

Этап 5

Теперь, вычислив, сколько байт мы намерены возвратить клиенту, нам нужно сделать ряд вещей в зависимости от того, возвращаем мы данные или нет. Если да, то мы просто отвечаем клиенту с данными сразу после проверки на этапе 5. Обратите внимание, что для возврата данных с корректного смещения мы используем

data_string + off
(смещение off вычисляется в зависимости от наличия переопределения типа). Отметьте также второй параметр функции MsgReply() — в документации он упоминается как «status» («код завершения»), но в этом случае мы используем его для возврата числа байт. Мы делаем так, потому что реализация клиентской функции read() знает, что значение, возвращаемое ее функцией MsgSendv() (а это, кстати, как раз и есть параметр status функции MsgReply()) представляет собой число реально прочитанных байт — это общеизвестное соглашение.

Этап 6

Поскольку мы возвращаем данные от устройства, мы знаем, что к устройству производился доступ. Мы устанавливаем биты IOFUNC_ATTR_ATIME и IOFUNC_ATTR_DIRTY_TIME в поле flags атрибутной записи. Это служит напоминанием для функции io_stat() о том, что время доступа стало недействительным, и перед выполнением ответа его следует скорректировать по системным часам. Если бы нам очень хотелось, мы могли бы записать текущее время в поле atime атрибутной записи и сбросить флаг IOFUNC_ATTR_DIRTY_TIME; однако, это было бы не очень-то эффективно, поскольку мы предполагаем получить от клиента значительно большее запросов типа read(), чем запросов типа stat(). Впрочем, ваши условия могут диктовать иначе.

Так какое же время видит клиент, когда он вызывает-таки функцию stat()? Функция iofunc_stat_default(), предоставляемая библиотекой администратора ресурсов, посмотрит на поле flags атрибутной записи, чтобы проверить, являются времена доступа (поля atime, ctime и mtime) корректными или нет. Если нет (как это было бы после вызова io_read() с возвратом данных), iofunc_stat_default() устанавливает нужные из них в значение текущего времени.

Этап 7

Теперь мы увеличиваем смещение lseek() на число возвращенных клиенту байт, но делаем это только в том случае, если не обрабатываем модификатор _IO_XTYPE_OFFSET. Это гарантирует, что в случае отсутствия флага _IO_XTYPE_OFFSET, если клиент вызовет функцию lseek() для определения текущей позиции, или (более важный случай) если клиент вызовет read() для чтения еще нескольких байт, смещение в ресурсе будет корректным. Если _IO_XTYPE_OFFSET установлен, мы оставляем содержащееся в ocb смещение в покое.

Этап 8

Сопоставьте этот этап с этапом 6. Здесь мы только разблокируем клиента и не выполняем больше никаких действий. Также обратите внимание, что функции MsgReply() не передается никакой области данных, потому что в этом случае данные мы не возвращаем.

Этап 9

И наконец, на этапе 9 мы выполняем действия, не зависящие от того, возвращаем мы данные клиенту или нет. Поскольку мы уже сами разблокировали клиента при помощи MsgReply(), мы, конечно же, не хотим, чтобы это попыталась сделать еще и библиотека администратора ресурсов. Поэтому мы сообщаем ей, что мы уже сделали это сами, возвратом _RESMGR_NOREPLY.

Эффективное применение других функций обмена сообщениями

Как вы помните из главы «Обмен сообщениями», мы упоминали еще несколько функций обмена сообщениями, а именно — функции MsgWrite(), MsgWritev() и MsgReplyv(). Повод, в связи с которым я снова упоминаю здесь эти функции, состоит в том, что ваша функция io_read() может быть превосходным местом для их применения. В простом примере, показанном выше, мы возвращали непрерывный массив данных из постоянного места в памяти. В реальной же жизни вам может понадобиться возвратить, скажем, множество фрагментов данных из различных выделенных вами буферов. Классическим примером такого случая является циклический буфер, который часто применяется, например, в драйверах последовательных устройств. Часть данных может быть размещена в конце буфера, другая часть — в начале. В этом случае для возврата обеих частей данных вам понадобилось бы передать MsgReplyv() двухэлементный вектор ввода/вывода (IOV), где первый элемент содержал бы адрес (и длину) «нижней» части данных, а второй — адрес (и длину) «верхней» части. Или же, если вы ожидаете прибытия данных частями, вы могли бы вместо этого использовать функции MsgWrite() или MsgWritev() для записи данных в адресное пространство клиента по мере их поступления, а затем выдать заключительный вызов MsgReply() или MsgReplyv(), чтобы разблокировать клиента. Как мы уже показали выше, функция MsgReply() может и не передавать никаких данных— вы можете использовать ее просто для того, чтобы разблокировать клиента.

Простой пример функции 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 

#include 

#include 

#include 

#include 


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() возвратит число байт, записанное нашим устройством.

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

Клиентский вызов devctl() формально определен так:

#include 

#include 

#include 


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 Переменная для дополнительной информации, установку которой может выполнить администратор ресурса.

Двумя старшими разрядами команды dcmd кодируется направление обмена данными, если он вообще имеет место. Подробности см. выше в описании функций ввода/вывода (параграф «io_devctl()»).

Когда администратор ресурсов принимает сообщение _IO_DEVCTL, оно обрабатывается вашей функцией io_devctl(). Ниже представлен очень простой пример, который предполагается использовать для настройки каналов и частоты дискретизации для аудиоустройства, как упоминалось выше.

/*

 * io_devctl1.c

*/

#include 

#include 

#include 

#include 

#include 

#include 


#define DCMD_AUDIO_SET_CHANNEL_MONO   1

#define DCMD_AUDIO_SET_CHANNEL_STEREO  2

#define DCMD_AUDIO_SET_SAMPLE_RATE_CD  3

#define DCMD_AUDIO_SET_SAMPLE_RATE_DAT 4


int io_devctl(resmgr_context_t *ctp, io_devctl_t *msg,

 iofunc_ocb_t *ocb) {

 int sts;


 // 1) Проверить, не является ли это обычным

 // POSIX-совместимым devctl()

 if ((sts =

  iofunc_devctl_default(ctp, msg, ocb)) !=

   _RESMGR_DEFAULT) {

  return (sts);

 }


 // 2) Узнать, что за команда, и отработать ее

 switch (msg->i.dcmd) {

 case DCMD_AUDIO_SET_CHANNEL_MONO:

  audio_set_nchannels(1);

  break;

 case DCMD_AUDIO_SET_CHANNEL_STEREO:

  audio_set_nchannels(2);

  break;

 case DCMD_AUDIO_SET_SAMPLE_RATE_CD:

  audio_set_samplerate(44100);

  break;

 case DCMD_AUDIO_SET_SAMPLE_RATE_DAT:

  audio_set_samplerate(48000);

  break;

 // 3) Если мы не знаем такой команды, отвергнуть ее

 default:

  return (ENOSYS);

 }


 // 4) Сказать клиенту, что все отработано

 memset(imsg->о, 0, sizeof(msg->о));

 SETIOV(ctp->iov, &msg->o, sizeof(msg->o));

 return (_RESMGR_NPARTS(1));

}

Этап 1

На первом этапе мы снова видим применение вспомогательной функции, на этот раз — функции iofunc_devctl_default(), которая используется для выполнения всей обработки по умолчанию для devctl(). Если вы не поставляете свою версию io_devctl(), а только инициализируете таблицы функций ввода/вывода и установления соединения при помощи iofunc_func_init(), будет вызвана именно iofunc_devctl_default(). Мы включаем ее в нашу версию io_devctl(), потому что мы хотим, чтобы она обработала для нас все стандартные POSIX-варианты вызова devctl(). Затем мы проверяем возвращаемое значение; если это не _RESMGR_DEFAULT, значит, функция iofunc_devctl_default() «обработала» запрос, и нам остается только возвратить это значение, выдав его за «наше».

Если возвращенное значение является константой _RESMGR_DEFAULT, это говорит нам, что вспомогательная функция не обработала запрос, и что мы должны выяснить, является ли он одним из «наших».

Этап 2

Эта проверка выполняется на этапе 2 при помощи инструкции

switch
/
case
. Мы просто проверяем значение dcmd, которое клиентский код указал во втором параметре функции devctl(), на предмет совпадения с какой-нибудь из «наших» команд. Обратите внимание, что для выполнения фактической «работы» для клиента мы вызываем фиктивные функции audio_set_nchannels() и audio_set_samplerate(). Здесь важно отметить, что мы преднамеренно избегаем обсуждения области данных функции devctl(). Вы можете подумать: «А что если я хочу установить частоту дискретизации в некое значение n? Как это сделать?» На этот вопрос мы ответим в следующем примере io_devctl(), который представлен ниже.

Этап 3

Этот этап — дань концепции защитного программирования. Мы возвращаем код ошибки ENOSYS, чтобы сообщить клиенту, что мы не распознали его запрос.

Этап 4

Наконец, мы обнуляем возвратную структуру и устанавливаем на нее одноэлементный вектор ввода/вывода. Затем мы возвращаем библиотеке администратора ресурсов единицу (1) через макрос _RESMGR_NPARTS(), сообщая ей тем самым, что мы возвращаем одноэлементный вектор ввода/вывода. Это и будет возвращено клиенту. Как вариант, мы могли бы применить макрос _RESMGR_PTR():

// Вместо этого

// 4) Сказать клиенту, что это сработало

memset(imsg->о, 0, sizeof(msg->о));

SETIOV(&ctp->iov, &msg->о, sizeof(msg->o));

return (_RESMGR_NPARTS(1));


//Мы могли бы сделать так:

// 4) Сказать клиенту, что это сработало

memset(imsg->о, 0, sizeof(msg->о));

return (_RESMGR_PTR(ctp, imsg->o, sizeof(msg->o)));

Причиной тому, что мы здесь обнулили возвращаемую структуру (вспомните, в примерах io_read() и io_write() мы этого не делали) является то, что в данном случае возвращаемая структура имеет реальное содержимое! (В случае с io_read() мы возвращали только собственно данные и число считанных байт — никакой «возвращаемой структуры» не было; в случае же с io_write() единственным возвращаемым значением было число записанных байт.)

Пример функции io_devctl(), имеющей дело с данными

В предыдущем примере io_devctl() мы подняли вопрос о том, как устанавливать произвольные значения частоты дискретизации, Очевидно, создание большого количества констант DCMD_AUDIO_SET_SAMPLE_RATE_* было бы не самым оптимальным решением — у нас бы просто не хватило разрядности поля dcmd.

С клиентской стороны мы будем использовать указатель на частоту дискретизации, dev_data_ptr, которую мы просто передадим как целое, поэтому поле nbytes будет содержать число байт в целом числе (для 32-разрядных машин это 4). Будем предполагать, что для этих целей определена константа DCMD_AUDIO_SET_SAMPLE_RATE.

Также неплохо было бы уметь считывать текущее значение частоты дискретизации. Для этого мы также будем использовать переменные dev_data_ptr и nbytes, как описано выше, но в обратном направлении — администратор ресурсов запишет nbytes данных в ячейку памяти, на которую указывает dev_data_ptr, вместо чтения данных из этой ячейки. Предположим, что для этого определена константа DCMD_AUDIO_GET_SAMPLE_RATE.

Давайте посмотрим, что в таком случае происходит в функции io_devctl() администратора ресурсов (того, что обсуждалось в предыдущем примере, мы здесь касаться не будем):

/*

 * io_devctl2.с

*/

#include 

#include 

#include 

#include 

#include 

#include 

#include 


#define DCMD_AUDIO_SET_SAMPLE_RATE 1

#define DCMD_AUDIO_GET_SAMPLE_RATE 2


int io_devctl(resmgr_context_t *ctp, io_devctl_t *msg,

 iofunc_ocb_t *ocb) {

 int  sts;

 void *data;

 int  nbytes;

 if ((sts =

  iofunc_devctl_default(ctp, msg, ocb)) !=

   _RESMGR_DEFAULT) {

  return (sts);

 }


 // 1) Установить указатель на область данных сообщения

 data = _DEVCTL_DATA(msg);


 // 2) Установить число возвращаемых байт в 0

 nbytes = 0;

 // Проверить все команды; покажем только те, которые нам

 // здесь интересны

 switch (msg->i.dcmd) {

  ...


 // 3) Обработать команду SET

 case DCMD_AUDIO_SET_SAMPLE_RATE:

  audio_set_samplerate(*(int*)data);

  break;

 // 4) Обработать команду GET

 case DCMD_AUDIO_GET_SAMPLE_RATE:

  *(int*)data = audio_get_samplerate();

  nbytes = sizeof(int);

  break;

  ...

 }


 // 5) Возвратить данные (если есть) клиенту

 memset(&msg->о, 0, sizeof(msg->о));

 msg->о.nbytes = nbytes;

 SETIOV(ctp->iov, &msg->o, sizeof(msg->o) + nbytes);

 return (_RESMGR_NPARTS(1));

}

Этап 1

В «шапке» мы декларировали указатель типа

void*
по имени data («данные»), которые мы будем использовать в качестве универсального указателя на область данных. Если вы обратитесь к приведенному выше описанию io_devctl(), то вы увидите, что структура данных состоит из объединения заголовков входной и выходной структур, за которым неявно следует область данных. На этапе 1 указатель на эту область данных возвращается макросом _DEVCTL_DATA().

Этап 2

Здесь мы должны указать, сколько байт мы собираемся возвратить клиенту. Я для удобства обнулил переменную nbytes перед выполнением каких-либо действий — теперь мне не придется принудительно обнулять ее в каждой ветви

switch
/
case
.

Этап 3

Пришло время для команды «set» («установить»). Мы вызываем фиктивную функцию audio_set_samplerate() и передаем ей значение частоты дискретизации, полученное разыменованием указателя data (который мы коварно выставили указателем на целое число... нет, никакого коварства, обычный для Си прием приведения типов). Это ключевой механизм, потому что это и есть наш способ «интерпретации» области данных (клиентского указателя dev_data_ptr) в соответствии с командой. В более сложном случае вы, наверное, выполнили бы приведение его типа к какой-нибудь структуре побольше вместо простого целого числа. Очевидно, что описания этой структуры на стороне как клиента, так и администратора ресурсов, должны быть идентичными, поэтому лучшим местом для описания такой структуры является заголовочный файл, в котором хранятся ваши командные константы DCMD_*

Этап 4

Обработка команды «get» («получить») на этапе 4 во многом аналогична (по части приведения типов), кроме того, что на этот раз мы записываем данные в структуру вместо считывания из нее. Заметьте, что мы также присваиваем переменной nbytes число байт, которые мы хотим возвратить клиенту. В случае более сложного доступа к данным вы должны были бы возвратить размер области данных (т.е. если бы эта область была бы структурой, вам нужно было бы возвратить ее размер).

Этап 5

Наконец, для возврата данных клиенту мы должны вспомнить, что клиент ожидает не только возвращаемые данные (если таковые имеются), но и заголовочную структуру, за которой идут эти данные. Поэтому на этом этапе мы обнуляем заголовочную структуру и устанавливаем число байт (поле nbytes) в число байт, которые мы намерены возвратить (вспомните, мы обнуляли это значение ранее). Затем мы создаем одноэлементный вектор ввода/ вывода с указателем на заголовок и расширяем размер заголовка на число возвращаемых байт. В конце мы просто сообщаем библиотеке администратора ресурсов, что мы возвращаем клиенту одноэлементный вектор ввода/вывода.

Важное замечание

Вспомните рассуждения про следующую за заголовком область данных из примера io_write(), приведенного выше. Мы утверждали, что байты, расположенные сразу после заголовка, могут как быть полноценными, так и нет (то есть возможны случаи, когда область данных со стороны клиента была считана лишь частично) — в зависимости от того, сколько данных считала библиотека администратора ресурсов. Затем мы говорили о том, что было бы неэффективно пытаться «сэкономить» лишнюю операцию обмена сообщениями и «повторно использовать» область данных. Однако, в случае с devctl() все обстоит несколько иначе, особенно если количество передаваемых данных достаточно невелико (как было и в наших примерах). Здесь у нас есть неплохой шанс того, что данные от клиента были-таки считаны в область данных целиком, и тогда повторное их считывание будет напрасной тратой сил. Узнать, сколько у вас доступно пространства, очень просто: поле size («размер») структуры ctp содержит число байт, доступных для вас, начиная с параметра msg. Размер доступной области данных, расположенной за буфером сообщений, вычисляется как разность между размером буфера сообщений и полем size структуры ctp:

data_area_size = ctp->size - sizeof(*msg);

Отметим, что этот размер будет действителен также и в случае возврата данных клиенту (как при команде DCMD_AUDIO_GET_SAMPLE_RATE).

Для всего, что превосходит по размеру выделенную область, вам придется получать данные от клиента так же, как мы это делали в примере с io_write() (см. выше), а также выделить буфер для возврата данных клиенту.

Дополнительно

Теперь, после того как мы овладели «основами» построения администраторов ресурсов, пришло время рассмотреть более сложные вопросы. К ним относятся:

• расширение OCB;

• расширение атрибутной записи;

• блокирование в пределах администратора ресурсов;

• возврат элементов каталога.

Расширение OCB

В ряде случаев у вас может возникнуть необходимость расширения OCB. Процедура эта является относительно безболезненной. Обычно OCB расширяют дополнительными флагами, характеризующими каждый конкретный open(). Один такой флаг можно было бы использовать с обработчиком io_unblock() для кэширования значения флага ядра _NTO_MI_UNBLOCK_REQ (подробнее см. параграф «Применение флага _NTO_MI_UNBLOCK_REQ» в главе «Обмен сообщениями»).

Для расширения блока OCB вам нужно будет обеспечить две дополнительных функции: одну для выделения OCB, и одну — для его освобождения. Затем вы должны будете привязать эти две функции к записи точки монтирования. (Да-да, совершенно верно — вам понадобится запись точки монтирования, даже если только для этого.) И наконец, вы должны будете определить ваш собственный тип OCB, чтобы все прототипы в программе были корректны.

Давайте рассмотрим сначала описание типа OCB, а затем уже поглядим, как переопределяются функции:

#define IOFUNC_OCB_T struct my_ocb

#include 

Это сообщает включаемому файлу

, что именованная константа IOFUNC_OCB_T теперь указывает на вашу новую усовершенствованную структуру OCB.

Очень важно иметь в виду, что ваш «расширенный» OCB должен содержать «стандартный» OCB в качестве своего первого элемента! Это так, потому что вспомогательная библиотека POSIX везде передает указатель на то, что она считает стандартным OCB — о вашем расширенном OCB ей ничего не известно, так что первый элемент данных, расположенный по этому указателю, должен соответствовать стандартному OCB.

Вот наш расширенный OCB:

typedef struct my_ocb {

 iofunc_ocb_t normal_ocb;

 int      my_extra_flags;

 ...

} my_ocb_t;

А вот код, иллюстрирующий, как переопределяются функции выделения и освобождения OCB в записи точки монтирования:

// Декларации

iofunc_mount_t mount;

iofunc_funcs_t mount_funcs;


// Задать в записи точки монтирования

// наши функции выделения/освобождения

// _IOFUNC_NFUNCS взята из .h-файла

mount_funcs.nfuncs = _IOFUNC_NFUNCS;


// Новая функция выделения OCB

mount_funcs.ocb_calloc = my_ocb_calloc;


// Новая функция освобождения OCB

mount_funcs.ocb_free = my_ocb_free;


// Настроить запись точки монтирования

memset(&mount, 0, sizeof(mount));

После этого остается только привязать запись точки монтирования к атрибутной записи:

...

attr.mount = &mount;

Функции my_ocb_calloc() и my_ocb_free() отвечают за выделение обнуленного расширенного OCB и освобождения OCB, соответственно. Вот их прототипы:

IOFUNC_OCB_T* my_ocb_calloc(resmgr_context_t *ctp,

 IOFUNC_ATTR_T *attr);


void my_ocb_free(IOFUNC_OCB_T *ocb);

Это означает, что функции my_ocb_calloc() передаются одновременно и внутренний контекст администратора ресурсов, и атрибутная запись. Функция отвечает за возврат обнуленного OCB. Функция my_ocb_free() получает OCB и отвечает за освобождение выделенной под него памяти.

Для этих двух функций имеются два интересных применения (которые ничем не связаны с выполнением расширения блока OCB):

• контроль распределения/освобождения блока OCB;

• обеспечение более эффективного распределения/ освобождения

Контроль за OCB

В этом случае вы можете просто «подключиться» к функциям распределения/освобождения и контролировать использование OCB (например, вам может быть необходимо ограничить суммарное количество OCB). Это может оказаться полезным, если вы не перехватываете функцию io_open(), но создание (и, возможно, удаление) OCB все-таки хотите контролировать.

Более эффективное распределение

Другое применение для переопределения встроенных библиотечных функций распределения/освобождения OCB может заключаться в том, что вы можете захотеть хранить OCB в свободном списке вместо использования библиотечных calloc() и free(). Если вы распределяете и освобождаете OCB с большой частотой, это может оказаться более эффективно.

Расширение атрибутной записи

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

Расширять атрибутную запись намного проще, чем OCB, потому что атрибутные записи в любом случае распределяются и освобождаются вашим кодом.

Вам нужно будет выполнить тот же трюк с переопределением атрибутной записи в заголовочных файлах, как мы это делали ранее при расширении OCB:

#define IOFUNC_ATTR_T struct my_attr

#include 

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

Блокирование в пределах администратора ресурсов

До настоящего момента мы избегали разговоров о возможности блокирования в пределах администратора ресурсов. Мы предполагали, что у нас есть функция-обработчик (например, io_read()), и что данные будут доступны немедленно. А что если нам придется блокироваться в ожидании данных? Например, выполнение read() применительно к последовательному порту может потребовать блокирования до приема символа. Очевидно, что мы не можем предсказать, сколько может продолжаться такое ожидание.

Блокирование в пределах администратора ресурсов базируется на тех же самых принципах, которые мы обсуждали в главе «Обмен сообщениями» — в конце концов, администратор ресурса фактически является сервером, который обрабатывает рад четко определенных сообщений. Когда прибывает сообщение, соответствующее клиентскому запросу read(), оно прибывает вместе с идентификатором отправителя (receive ID), и клиент блокируется. Если у администратора ресурсов есть данные, он просто возвращает их клиенту, как мы уже видели в различных приведенных ранее примерах. Однако, если данные недоступны, администратор ресурсов должен будет удерживать этого клиента в заблокированном состоянии (конечно, если клиент для этой операции определил блокирующий режим), чтобы иметь возможность продолжить обработку других сообщений. Реально это означает, что поток администратора ресурсов, который принял сообщение от клиента, не должен блокироваться в ожидании данных — в противном случае это может закончиться для администратора ресурсов огромным числом заблокированных потоков, каждый из которых ожидал бы данные от некоего устройства.

Правильным решением для этой проблемы является сохранение идентификатора отправителя полученного сообщения в какой-нибудь очереди и возврат из вашего обработчика константы _RESMGR._NOREPLY. Это укажет библиотеке администратора ресурсов, что обработка сообщения закончена, но клиента пока разблокировать не надо.

Несколько позже, по готовности данных, вы сможете выбрать идентификатор отправителя нужного клиента и сконструировать ответное сообщение с данными. После этого можно отвечать клиенту.

Вы могли бы также расширить эту концепцию добавлением тайм-аутов, как мы это делали в главе «Часы, таймеры и периодические уведомления» (параграф «Поддерживаемые сервером тайм-ауты»). Если говорить вкратце, там по истечении некоторого интервала времени клиентский запрос считался «просроченным», и сервер отвечал по сохраненному по идентификатору отправителя неким сообщением об ошибке.

Возврат элементов каталога

В приведенном ранее примере функции io_read() мы уже видели, как происходит возврат данных. Как было упомянуто в описании функции io_read() (в разделе «Алфавитный список функций установления соединения и ввода-вывода»), io_read() можно возвращать и элементы каталога тоже. Поскольку это может понадобиться далеко не всем, я решил рассмотреть этот вопрос здесь отдельно.

Прежде всего давайте посмотрим, почему и когда вашей io_read() могло бы понадобиться возвращать элементы каталога, а не «сырые» данные.

Если вы дискретно объявляете элементы в пространстве имени путей, и эти элементы не помечены флагом _RESMGR_FLAG_DIR, тогда вам не придется возвращать элементы каталога из функции io_read(). Если рассматривать это как «файловую систему», то ваш объект будет «файлом». Если же, с другой стороны, вы указываете _RESMGR_FLAG_DIR, то будет создан объект типа «каталог». Никто, кроме вас, не знает ничего о содержимом этого каталога, поэтому вы должны будете предоставить эти данные. Это и есть ответ на вопрос, почему функции io_read() может понадобиться возвращать элементы каталогов.

Вообще говоря…

Вообще говоря, возврат элементов каталога — это почти то же самое, что и возврат «сырых» данных, за исключением того, что:

• вы должны возвратить целое число структур типа

struct dirent
;

• эти структуры

struct dirent
должны быть заполнены.

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

struct dirent
. Если восемь структур не вписываются в выделенное пространство, то вы должны будете возвратить только семь элементов.

Второй пункт достаточно очевиден. Он упомянут здесь только потому, что заполнение структуры

struct dirent
может быть несколько «хитрее», чем «сырой» подход к данным в случае с «обычной» io_read().

Структура
struct dirent
и ее друзья

Давайте взглянем на структуру

struct dirent
, поскольку это именно то, что возвращает функция io_read() в случае чтения каталога. Мы также вкратце рассмотрим клиентские вызовы, имеющие дело с элементами каталогов, поскольку у них со структурой
struct dirent
существует ряд интересных взаимоотношений.

Чтобы работать с каталогами, клиент использует функции closedir(), opendir(), readdir(), rewinddir(), seekdir() и telldir().

Обратите внимание на сходство с «нормальными» функций для файлов (и совпадение применяемых типов сообщений):

Функция для работы с каталогами Функция для работы с файлами Сообщение
closedir() close() IO_CLOSE_DUP
opendir() open() _IO_CONNECT
readdir() read() _IO_READ
rewinddir() lseek() _IO_LSEEK
seekdir() lseek() _IO_LSEEK
telldir() tell() _IO_LSEEK

Если мы на мгновение вообразим, что функции opendir() и closedir() будут обработаны для нас автоматически, мы сможем сконцентрироваться только на сообщениях типа _IO_READ и _IO_LSEEK и на соответствующих им функциях.

Смещения

Сообщение _IO_LSEEK и соответствующая ему функция применяются для «поиска» (или «перемещения») в пределах файла. С каталогом происходит та же история. Вы можете переместиться к «первому» элементу каталога (как явно задав смещение функции seekdir(), так и вызвав функцию rewinddir()) переместиться к любому произвольному элементу (используя функцию seekdir()), или же узнать свою текущую позицию в списке элементов каталога (вызвав функцию telldir()).

Однако, «хитрость» при работе с каталогами состоит в том, что смещения задаете вы сами, и управление ими тоже всецело лежит на вас. Это означает, что вы можете назначать смещения элементов в каталоге равными как «0», «2» и так далее, так и «0», «64», «128» и так далее. Единственно, что здесь необходимо предусмотреть — чтобы обработчики io_lseek() и io_read() одинаково трактовали эти смещения.

В приведенном ниже примере мы предположим, что используется простой подход с номерами «0», «1», «2», и т.д. (Вы могли бы использовать «0», «64», «128», и т.д., если бы эти числа соответствовали, например, неким смещениям на носителе. Выбор за вами!)

Содержимое

Ну вот, остается «просто» заполнить

struct dirent
«содержимым» нашего каталога. Структура
struct dirent
выглядит так (взято из
):

struct dirent {

 ino_t   d_ino;

 off_t   d_offset;

 uint16_t d_reclen;

 uint16_t d_namelen;

 char   d_name[1];

};

Коротко о ее полях:

d_ino «Индексный дескриптор» («inode») — уникальный для точки монтирования порядковый номер, который не может быть нулевым (нуль указывал бы на то, что элемент, соответствующий данному индексному дескриптору, является свободным/пустым).
d_offset Смещение в каталоге, о котором мы только что говорили. В нашем примере это будут обычные числа типа «0», «1», «2», и т.д.
d_reclen Размер структуры
struct dirent
целиком, включая любые добавляемые в нее расширения. Заполнители для выравнивания при вычислении размера учитываются.
d_namelen Число символов в поле d_name, не включая признак конца строки NULL.
d_name Имя элемента каталога, которое должно завершаться признаком конца строки — NULL.

При возврате структур типа

struct dirent
код возврата, который передается клиенту, представляет собой число возвращенных байт.

Пример

Давайте для примера создадим администратора каталогового ресурса

/dev/atoz
. Этот администратор зарегистрирует «файлы» с именами от
/dev/atoz/a
до
/dev/atoz/z
, чтобы команда cat, примененная к любому из них, возвращала соответствующие их именам заглавные буквы. Чтобы понять, как это должно работать, вот пример командно-строковой сессии:

# cd /dev


# ls

atoz  null  ptyp2 socket ttyp0 ttyp3

enet0 ptyp0 ptyp3 text  ttyp1 zero

mem  ptyp1 shmem tty   ttyp2


# ls -ld atoz

dr-xr-xr-x 1 root 0 26 Sep 05 07:59 atoz


# cd atoz

# ls

a  e  i  m  q  u  y

b  f  j  n  r  v  z

c  g  k  o  s  w

d  h  l  p  t  x


# ls -l e

-r--r--r-x 1 root 0 1 Sep 05 07:59 e


# cat m

M# cat q

Q#

Приведенный пример показывает, что в каталоге

/dev
есть каталог
atoz
, и что к нему можно применить команду
ls
и выполнить в него
cd
. Данный каталог
/dev/atoz
имеет размер «26» — мы так задали в нашей программе. Сменив текущий каталог на
atoz
и выполнив еще одну
ls
, получаем его содержимое — файлы с именами от
а
до
z
. Выполнив
ls
для отдельного файла — в данном случае для файла
e
— мы видим, что файл доступен по чтению всем (часть «
-r--r--r--
») и имеет размер, равный 1 байту. Наконец, выполнение нескольких
cat
показывает, что файлы действительно имеют заявленное содержимое. (Отметим, что поскольку файлы содержат только один байт и не содержат символа новой строки, после вывода символа строка не переводится, и приглашение командного интерпретатора оказывается на той же самой строке, что и вывод
cat
.)

Теперь, когда мы знаем нужные характеристики, давайте посмотрим на код. Он разбит на следующие функции:

main() и декларации

Основная функция; здесь мы все инициализируем и запускаем наш администратор ресурса.

my_open()

Обработчик сообщения _IO_CONNECT.

my_read()

Обработчик сообщения _IO_READ.

my_read_dir() и my_read_file()

Выполняют фактическую работу функции my_read().

dirent_size() и dirent_fill()

Сервисные функции для работы со структурой

struct diren
t.

Заметьте, что при том, что разбит на короткие секции, перемежаемые текстовыми пояснениями, архив с полным исходным текстом в виде единого файла можно взять на веб-сайте компании PARSE Software Devices (

http://www.parse.com/
), он называется
atoz.c.

main() и декларации

Первый приведенный раздел программы представляет собой функцию main() и ряд деклараций. Для удобства объявлен макрос ALIGN(), который используется функциями dirent_size() и dirent_fill() для выравнивания.

Массив atoz_attrs содержит атрибутные записи, используемые в этом примере для «файлов». Мы объявляем массив из NUM_ENTS элементов, потому что у нас есть NUM_ENTS (то есть 26) файлов — от «

а
» до «
z
». Атрибутная запись, применяемая непосредственно для каталога (то есть для
/dev/atoz
, объявлена в теле функции main() и называется просто attr. Обратите внимание на различное содержимое у этих двух типов атрибутных записей:

Файловая атрибутная запись:

Маркируется как обычный файл (константа S_IFREG) с режимом доступа 0444 (это означает, что доступ по чтению имеет каждый, но доступа по записи нет ни у кого). Размер равен «1» — файл содержит только один байт, а именно прописную букву, соответствующую своему имени. Индексные дескрипторы (inodes) этих файлов пронумерованы от «1» до «26» включительно (было бы удобнее взять числа от «0» до «25», но цифра «0» зарезервирована).

Каталоговая атрибутная запись:

Маркируется как файл типа «каталог» (константа S_IFDIR) с режимом доступа 0555 (это означает, что доступ по чтению и поиску имеет каждый, но доступа по записи нет ни у кого). Размер определен как «26» — это просто число, взятое по количеству элементов в каталоге. Индексный дескриптор выбран равным «27» — это число заведомо не используется больше ни в одной атрибутной записи.

Обратите внимание, что мы переопределили только поле open структуры connect_func и поле read структуры io_func. Для всех остальных полей сохранены POSIX-значения по умолчанию.

Наконец, обратите внимание, как мы создали имя

/dev/atoz
, используя resmgr_attach(). Наиболее важным здесь является то, что мы применили флаг _RESMGR_FLAG_DIR, который сообщает администратору процессов, что он может разрешать запросы на эту точку монтирования и ниже.

/*

 * atoz.с

 *

 * /dev/atoz с использованием библиотеки администратора ресурсов

*/

#include 

#include 

#include 

#include 

#include 

#include 

#include 

#include 


#define ALIGN(x) (((x) +3) & ~3)

#define NUM_ENTS 26


static iofunc_attr_t atoz_attrs[NUM_ENTS];


int main (int argc, char **argv) {

 dispatch_t *dpp;

 resmgr_attr_t resmgr_attr;

 resmgr_context_t *ctp;

 resmgr_connect_funcs_t connect_func;

 resmgr_io_funcs_t io_func;

 iofunc_attr_t attr;

 int i;


 // Создать структуру диспетчеризации

 if ((dpp = dispatch_create()) == NULL) {

  perror("Ошибка dispatch_create\n");

  exit(EXIT_FAILURE);

 }


 // Инициализировать структуры данных

 memset(&resmgr_attr, 0, sizeof(resmgr_attr));

 resmgr_attr.nparts_max = 1;

 resmgr_attr.msg_max_size = 2048;


 // Назначить обработчики по умолчанию

 iofunc_func_init(_RESMGR_CONNECT_NFUNCS, &connect_func,

  _RESMGR_IO_NFUNCS, &io_func);


 // Создать и инициализировать атрибутную запись для

 // каталога...

 iofunc_attr_init(&attr, S_IFDIR | 0555, 0, 0);

 attr.inode = NUM_ENTS + 1; // 1-26 зарезервированы для

               // файлов от «a» до «z»

 attr.nbytes = NUM_ENTS; // 26 элементов в каталоге


 // ...и для имен от «a» до «z»

 for (i = 0; i < NUM_ENTS; i++) {

  iofunc_attr_init(&atoz_attrs[i], S_IFREG | 0444, 0, 0);

  atoz_attrs[i].inode = i + 1;

  atoz_attrs[i].nbytes = 1;

 }

 // Добавить наши функции; нам интересны только io_open

 // и io_read

 connect_func.open = my_open;

 io_func.read = my_read;


 // Зарегистрировать префикс

 if (resmgr_attach(dpp, &resmgr_attr, "/dev/atoz",

  _FTYPE_ANY, _RESMGR_FLAG_DIR, &connect_func,

 &io_func, &attr) == -1) {

 perror("Ошибка resmgr_attach\n");

 exit(EXIT_FAILURE);

 }


 // Выделить контекст

 ctp = resmgr_context_alloc(dpp);


 // Ждать сообщений в вечном цикле

 while (1) {

  if ((ctp = resmgr_block(ctp)) == NULL) {

  perror("Ошибка resmgr_block\n");

  exit(EXIT_FAILURE);

  }

  resmgr_handler(ctp);

 }

}

my_open()

При том, что функция my_open() очень невелика, в ней есть ряд критических мест. Обратите внимание, как мы принимаем решение о том, был ли ресурс открыт как «файл» или как «каталог», на основе только длины имени пути. Мы можем себе это позволить, потому что знаем, что других каталогов, кроме основного, в этом администраторе ресурсов нет. Если вы захотите расположить ниже вашей точки монтирования дополнительные каталоги, вам придется применить более сложный механизм анализа поля path структуры msg. В нашем простом примере, если в имени пути ничего нет, то мы знаем, что это каталог. Также обратите внимание на чрезвычайно упрощенную проверку корректности имени пути: мы просто проверяем, что у нас действительно только один символ, и что он лежит в диапазоне от «a» до «z» включительно. Опять же, в случае более сложного администратора ресурсов вам пришлось бы выполнять синтаксический анализа имени, следующего за зарегистрированной точкой монтирования.

Теперь о наиболее важной особенности. Обратите внимание, как мы использовали для выполнения всей нашей работы функции POSIX-уровня по умолчанию! Функция iofunc_open_default() обычно размещается в таблице функций установления соединения в той же самой ячейке, которую сейчас занимает наша функция my_open(). Это означает, что они принимают одинаковый набор аргументов!

Все, что мы должны сделать — это решить, какую атрибутную запись мы хотим связать с OCB, создаваемым функцией по умолчанию: либо каталоговую (в этом случае мы передаем attr), либо одну из 26 имеющихся у нас файловых (тогда мы передаем соответствующий элемент atoz_attrs). Это ключевой момент, поскольку обработчик, который вы помещаете в слот open в таблице функций установления соединения, действует как «швейцар» по отношению ко всем последующим запросам к вашему администратору ресурса.

static int my_open(resmgr_context_t *ctp, io_open_t *msg,

 iofunc_attr_t *attr, void *extra) {

 if (msg->connect.path[0] == 0) {

  // Каталог (/dev/atoz)

  return (iofunc_open_default(ctp, msg, attr, extra));

 } else if (msg->connect.path[1] == 0 &&

  (msg->connect.path[0] >= 'a' &&

  msg->connect.path[0] <= 'z')) { // Файл (/dev/atoz/[a-z])

  return

  (iofunc_open_default(ctp, msg, atoz_attrs +

   msg->connect.path[0] - 'a', extra));

 } else {

  return (ENOENT);

 }

}

my_read()

В функции my_read(), чтобы решить, какие операции надо выполнить, мы анализируем поле mode атрибутной записи. Если макрос S_ISDIR() говорит, что это каталог, мы вызываем функцию my_read_dir(); если макрос S_ISREG() говорит, что это файл, мы вызываем функцию my_read_file(). (Отметим, что если мы не можем разобрать, что это такое, мы возвращаем клиенту EBADF, указывая ему этим, что что-то здесь не так.)

Приведенный ниже код ничего не знает о наших специальных устройствах, да ему, собственно, и дела до них нет — он просто принимает решение на основе стандартных общеизвестных данных.

static int my_read(resmgr_context_t *ctp, io_read_t *msg,

 iofunc_ocb_t *ocb) {

 int sts;


 // Использовать вспомогательную функцию для проверки

 // корректности

 if ((sts =

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

  return (sts);

 }


 // Решить, надо ли читать «файл» или «каталог»

 if (S_ISDIR(ocb->attr->mode)) {

  return (my_read_dir(ctp, msg, ocb));

 } else if (S_ISREG(ocb->attr->mode)) {

  return (my_read_file(ctp, msg, ocb));

 } else {

  return (EBADF);

 }

}

my_read_dir()

Вот тут-то все веселье и начинается. С точки зрения высокого уровня, мы выделяем буфер (он называется reply_msg), в котором собираемся разместить результат операции. Затем мы используем dp, чтобы «прогуляться» по буферу, заполняя его по ходу дела элементами

struct dirent
. Вспомогательная подпрограмма dirent_size() применяется, чтобы определить, есть ли у нас место в буфере для еще одного элемента. Вспомогательная подпрограмма dirent_fill() кладет элемент в буфер. (Отметим, что эти подпрограммы не входят в библиотеку администратора ресурсов; мы обсудим их ниже.)

На первый взгляд этот код может показаться неэффективным; мы используем функцию sprintf() для создания двухбайтового имени файла (символ имени файла и признак конца строки NULL) в буфере длиной _POSIX_PATH_MAX (то есть 256) байт. Это делается для того, чтобы сделать код по возможности универсальным.

Наконец, обратите внимание, что мы используем поле offset в OCB для указания, для какого конкретного файла мы в данный момент генерируем структуру

struct dirent
. Это означает, что мы также должны корректировать поле offset всякий раз, когда возвращаем данные.

Возврат данных клиенту осуществляется «обычным» способом при помощи функции MsgReply(). Заметьте, что поле состояния (status) функции MsgReply() используется для указания числа отправленных клиенту байт.

static int my_read_dir(resmgr_context_t *ctp,

 io_read_t *msg, iofunc_ocb_t *ocb) {

 int nbytes;

 int nleft;

 struct dirent *dp;

 char *reply_msg;

 char fname[_POSIX_PATH_MAX];

 // Выделить буфер для ответа

 reply_msg = calloc(1, msg->i.nbytes);

 if (reply_msg == NULL) {

 return (ENOMEM);

 }


 // Назначить выходной буфер

 dp = (struct dirent *)reply_msg;


 // Осталось «nleft» байт

 nleft = msg->i.nbytes;

 while (ocb->offset < NUM_ENTS) {

  // Создать имя файла

  sprintf(fname, "%с", ocb->offset + "a");

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

  nbytes = dirent_size(fname);

  // Есть место?

  if (nleft - nbytes >= 0) {

  // Заполнить элемент каталога и увеличить указатель

  dp =

   dirent_fill(dp, ocb->offset + 1, ocb->offset, fname);

  // Увеличить смещение OCB

  ocb->offset++;

  // Учесть, сколько байт мы использовали

  nleft -= nbytes;

  } else {

  // Места больше нет, остановиться

  break;

  }

 }


 // Возвращаемся обратно к клиенту

 MsgReply(ctp->rcvid, (char*)dp - reply_msg, reply_msg,

  (char*)dp — reply_msg);


 // Освободить буфер

 free(reply_msg);

 // Сказать библиотеке, что мы уже ответили сами

 return (_RESMGR_NOREPLY);

}

my_read_file()

В функции my_read_file() мы видим код, почти аналогичный простому примеру функции чтения, который приведен выше в данном разделе. Единственная странная вещь, которую мы здесь делаем — поскольку мы знаем, что возвращается всегда только один байт данных, значит, если параметр nbytes не равен нулю, то он должен быть равен единице (и ничему другому). Таким образом, мы можем создавать данные, подлежащие возврату, непосредственным заполнением символьной переменной string. Обратите внимание, как мы используем поле inode атрибутной записи для определения, какие данные возвращать. Это обычный прием для администраторов, обслуживающих несколько ресурсов. Дополнительным трюком было бы расширить атрибутную запись (мы говорили об этом в разделе «Расширение атрибутной записи») и хранить непосредственно в ней либо сами данные, либо указатель на них.

static int my_read_file(resmgr_context_t *ctp,

 io_read_t *msg, iofunc_ocb_t *ocb) {

 int nbytes;

 int nleft;

 char string;


 // Тут нет никаких xtype...

 if ((msg->i.xtype & _IO_XTYPE_MASK) != _IO_XTYPE_NONE) (

  return (ENOSYS);

 }


 // Выяснить, сколько байт осталось...

 nleft = ocb->attr->nbytes — ocb->offset;


 // ...и сколько мы можем возвратить клиенту

 nbytes = min(nleft, msg->i.nbytes);

 if (nbytes) {

  // Создать ответную строку

  string = ocb->attr->inode - 1 + "A";

  // Возвратить ее клиенту

  MsgReply(ctp->rcvid, nbytes, &string + ocb->offset,

  nbytes);

  // Обновить флаги и смещение

  ocb->attr->flags |=

  IOFUNC_ATTR_ATIME | IOFUNC_ATTR_DIRTY_TIME;

  ocb->offset += nbytes;

 } else {

  // Возвращать нечего, индицировать конец файла

  MsgReply(ctp->rcvid, EOK, NULL, 0);

 }


 // Уже ответили сами

 return (_RESMGR_NOREPLY);

}

dirent_size()

Вспомогательная подпрограмма dirent_size() просто вычисляет число байт, необходимое для структуры

struct dirent
, с учетом ограничений по выравниванию. Опять же, для нашего простого администратора ресурсов здесь имеет место небольшое искусственное упрощение, потому что мы точно знаем, каков размер каждого элемента каталога — все имена файлов имеют длину ровно один байт. Однако, как бы там ни было, это все равно полезная служебная подпрограмма.

int dirent_size(char *fname) {

return (ALIGN(sizeof(struct dirent) - 4 + strlen(fname)));

}

dirent_fill()

И, наконец, вспомогательная подпрограмма dirent_fill() применяется для помещения передаваемых ей значений (а именно — полей inode, offset и fname) в также передаваемый ей элемент каталога. В порядке щедрости она также возвращает указатель на адрес (с учетом выравнивания), с которого должен начинаться следующий элемент каталога.

struct dirent* dirent_fill(struct dirent *dp, int inode,

 int offset, char *fname) {

 dp->d_ino = inode;

 dp->d_offset = offset;

 strcpy(dp->d_name, fname);

 dp->d_namelen = strlen(dp->d_name);

 dp->d_reclen =

 ALIGN(sizeof(struct dirent) - 4 + dp->d_namelen);

 return ((struct dirent*)((char*)dp + dp->d_reclen));

}

Резюме

Написание администратора ресурсов — наиболее сложная задача из описанных в этой книге.

Администратор ресурсов — это сервер, который воспринимает ряд четко определенных сообщений. Эти сообщения можно подразделить на две категории:

Сообщения установления соединения

Относятся к операциям с именами путей, с их помощью можно устанавливать контекст для дальнейшей работы.

Сообщения ввода/вывода

Всегда следуют за сообщениями установления соединения и указывают, какую реальную работу нужно сделать для клиента (например, stat()).

Действия администратора ресурса управляются функциями пула потоков (мы обсуждали их в главе «Процессы и потоки») и функциями интерфейса диспетчеризации.

QSSL в библиотеке администратора ресурсов предоставляет ряд вспомогательных POSIX-функций, которые выполняют львиную долю работы по обслуживанию клиентских сообщений установления соединения и ввода/вывода, поступающих в администратор ресурсов.

Существует ряд структур данных, относящихся к клиентам и объявляемым администраторами ресурсов устройствам, которые необходимо принимать во внимание:

OCB (блок открытого контекста)

Выделяется при каждом «открытии» ресурса; содержат контекст для клиента (например, текущее смещение lseek()).

Атрибутная запись

Выделяется для каждого устройства; содержит информацию об устройстве (например, размер устройства, режимы доступа, и т.д.).

Запись точки монтирования

Распределяется на базисе по каждому администратору ресурса и содержит полную информацию о характеристиках администратора ресурса.

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

И наконец, вы наделяете администратор ресурса необходимой вам функциональностью, переопределяя некоторые из вызовов в таблицах функций установления соединения и функций ввода/вывода.

Загрузка...