Мы говорим, что имеет место «абстрагирование» сервиса, потому что клиентская программа (та, которая непосредственно использует сервис) не знает (да и незачем ей) о микросхеме 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.