Независимо от того, с чего вы начинаете, вам нужно будет удостовериться в том, что ваши функции вызываются так, как вы предполагаете. В данном ключе функции-обработчики 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() создается одиночное сообщение, выглядящее примерно так:
Составное сообщение.