Функции драйверов
Прежде всего, драйвер должен иметь функции, вызываемые
ядром при загрузке и выгрузке модуля и при подключении модуля к конкретным
устройствам. Например, в Sun Solans это перечисленные функции.
- int _init(void) — инициализация
драйвера. Эта функция вызывается при загрузке модуля. Драйвер должен
зарезервировать все необходимые ему системные ресурсы и проинициализировать
собственные глобальные переменные. Инициализация устройства на этом
этапе не происходит.
- int probe (dev_info_t *dip) —
проверить наличие устройства в системе. Во многих системах эта функция
реализуется не самим драйвером, а специальным модулем-"сниффером"
(sniffer — дословно, "нюхач"), используемым программой автоконфигурации.
- int attach (dev_info_t * dip, ddi_attach_cmd_t
crtid) — инициализация копии драйвера, управляющей конкретным
устройством. Эту функцию можно рассматривать как аналог конструктора
объекта в объектно-ориентированном программировании. Если в системе
присутствует несколько устройств, управляемых одним драйвером, некоторые
ОС загружают несколько копий кода драйвера, но в системах семейства
Unix функция attach просто вызывается многократно.
Каждая из инициализированных копий драйвера имеет собственный блок локальных
переменных, в которых хранятся переменные состояния устройства. При вызове
attach драйвер должен прочитать конфигурационный
файл, где записаны параметры устройства (номенклатура этих параметров
зависит от устройства и от драйвера), разместить и проинициализировать
блок переменных состояния, зарегистрировать обработчики прерываний, проинициализировать
само устройство и, наконец, зарегистрировать устройство как доступное
для пользовательских программ, создав для него минорную
запись (minor node). В ряде случаев драйвер создает для одного
устройства несколько таких записей.
Например, каждый жесткий диск в Unix SVR4 должен иметь 16 записей — по
две (далее мы поймем, для чего они нужны) для каждого из восьми допустимых
слайсов (логических разделов, см. разд.
Загрузка самой ОС) диска. Другой пример: в большинстве систем семейства
Unix лентопротяжные устройства имеют две минорные записи. Одно из этих
устройств при открытии перематывает ленту к началу, другое не перематывает.
В действительности оба устройства управляются одним и тем же драйвером,
который определяет текущий режим работы в зависимости от указанной минорной
записи.
Современные Unix системы, в частности Solaris, используют отложенную инициализацию,
когда для многих устройств attach вызывается
только при первой попытке доступа пользовательской программы к устройству.
- int detach(dev_info_t *dip, ddi_detach_cmd_t
cmd) — аналог деструктора объекта в ООП. Впрочем, в отличие от
деструктора, эта операция не безусловна — если не удается нормально
завершить обрабатываемые в данный момент операции над устройством, драйвер
может и даже обязан отказаться деинициализироваться. При деингщиализации
драйвер должен освободить все системные ресурсы, которые он занял при
инициализации и в процессе работы (в том числе и уничтожить минорную
запись) и может, если это необходимо, произвести какие-то операции над
устройством, например, выключить приемопередатчик, запарковать головки
чтения-записи и т. д. После того, как все устройства, управляемые драйвером,
успешно деинициализированы, система может его выгрузить.
- int _fini (void) — функция,
вызываемая системой перед выгрузкой дуля. Драйвер обязан освободить
все ресурсы, которые он занял на этапе инициализации модуля, а также
все ресурсы, занятые им во время работы на уровне модуля (не привязанные
к конкретному управляемому устройству).
После того, как драйвер проинициализировался и зарегистрироват
мино ную запись, пользовательские программы могут начинать обращаться
к не му и к управляемым им устройствам. Понятно, что обеспечить единый
ин терфейс к разнообразным категориям устройств, перечисленным в главе
9 по меньшей мере сложно. Наиболее радикально подошли к этой проблеме
разработчики системы UNIX, разделившие все устройства на два класса-блочные
(высокоскоростные устройства памяти с произвольным доступом в первую очередь,
дисковые устройства) и последовательные или символьные устройства (всё
остальное) (в действительности, у современных систем семейства Unix типов
драйверов несколько больше, но об этом далее).
Над последовательными устройствами определен следующий набор операций,
которые могут осуществляться прикладной программой (в простых случаях
эти операции непосредственно транслируются в вызовы функций драйвера).
- int open (char * fnarne, int flags,
mode_t mode) — Процедура открытия
устройства. В некоторых случаях она может содержать и дополнительные
шаги инициализации устройства — например, для лентопротяжек эта процедура
может включать в себя перемотку ленты к началу. Функция возвращает целочисленный
идентификатор-"ручку" (handle), часто называемый также дескриптором
файла, который используется программой при всех последующих обращениях
к устройству.
- int readfint handle, char * where,
size_t how_much) — чтение данных с устройства. Если устройство
приспособлено только для вывода (например, принтер), эта функция может
быть не определена.
- int write (int handle, char * what,
size_t how_much) — запись данных
на устройство. Если устройство приспособлено только для ввода, (например,
перфоленточный ввод или мышь), эта функция также может быть не определена.
- void dose (int handle) —
процедура закрытия (освобождения) устройства.
- int ioctitint handle, int cmd, ...)
— процедура задания специальной команды, которая не может быть
сведена к операциям чтения и записи. Набор таких команд зависит от устройства.
Например, для растровых графических устройств могут быть определены
операции установки видеорежима; для последовательных портов RS232 это
могут быть команд^ установки скорости, количества битов, обработки бита
четности и т. д., для дисководов — команды форматирования носителя.
- off r lseek<int handle, off_t
offset, int whence), long seek — команда перемещения головки
чтения/записи к заданной позиции. Драйверы устройств, не являющихся
устройствами памяти, например модема или Принтера, как правило, не поддерживают
эту функцию.
Слово long в названии функции появилось по историческим
причинам: версиях Unix для 16-разрядных машин индекс позиции не мог обозначать-я
словом, потому что это ограничивало бы логическую длину устройства недопустимо
малым значением 65334 байт. Поэтому необходимо было использовать двойное
слово, что соответствовало типу long языка С. Современные системы используют
64-разрядный off_t.
- caddr_t rranap (caddr_t addr, size_t
len, int prot, int flags, int handle, off_t offset) memory map
— отображение устройства в адресное пространство процесса. Параметр
prot задает права доступа к отображенному
участку: на чтение, на запись и на исполнение. Отображение может происходить
на заданный виртуальный адрес, или же система может выбирать адрес для
отображения сама.
Эта функция отсутствовала в старых версиях системы,
но большинство современных систем семейства (BSD 4.4, ряд наследников
BSD 4.3, SVR4 и Linux) поддерживают ее.
Речь идет об отображении в память данных, хранящихся на устройстве. Для
устройств ввода-вывода, например, для принтера или терминала, эту функцию
невозможно реализовать разумным образом. Напротив, для лент и других последовательных
устройств памяти, поддерживающих функцию Iseek,
отображение может быть реализовано с использованием аппаратных средств
виртуализации памяти и операции read и write.
Необходимость специальной функции отображения появляется у драйверов устройств,
использующих большие объемы памяти, отображенной в адресное пространство
системной шины, например, для растровых видеоадаптеров, некоторых звуковых
устройств или страниц общей памяти (backpane memory — двухпортовой памяти,
используемой как высокоскоростной канал обмена данными в многопроцессорных
системах).
Механизм отображения доступных прикладной программе системных вызовов
в функции драйвера относительно сложен. Этот механизм должен включать
в себя следующее.
- Изменение способа идентификации устройства. "Ручка"
представляет собой специфичный для пользовательского процесса номер,
в то время как к драйверу могут обращаться разные процессы. В системах
семейства Unix для идентификации устройства используется упомянутая
выше минорная запись, которая должна содержать указатель на блок переменных
состояния устройства.
- Передачу или отображение данных из пользовательского
адресного пространства в системное и обратно.
- Взаимодействие потоков пользовательского процесса (а
в общем случае -- нескольких пользовательских процессов, одновременно
использующих устройство) с потоками драйвера.
Способы, которыми эти вопросы решаются в современных
операционных системах, обсуждаются в последующих разделах. А пока что
мы подробнее обсудим, какие именно операции над устройством следует определить
и почему.
Видно, что предлагаемый системами семейства Unix набор операций рассматривает
устройство как неструктурированный поток байтов (или, ддя устройств ввода-вывода,
два разнонаправленных потока — для ввода и для вывода). Такое рассмотрение
естественно для устройств алфавитно-цифрового ввода-вывода и простых запоминающих
устройств, например магнитных лент, однако далеко не столь естественно
для более сложных устройств.
Стандартный ответ Unix-культуры в этом случае таков: любая, сколь угодно
сложная структура данных может быть сериализована — преобразована в последовательный
поток байтов. Например, изображение может быть превращено в последовательный
поток байтов в виде растровой битовой карты или последовательности описаний
графических примитивов — линий, прямоугольников и пр. Примерами такой
сериализации для изображений могут являться язык PostScript [partners.adobe.com]
и протокол распределенной оконной системы X Window [www.x.org]
(оба протокола поддерживают как растровые образы, так и довольно богатые
наборы векторных примитивов).
Нередки, впрочем, ситуации, когда нам интересна не только структура поступающих
данных, но и время их поступления (в предыдущей главе мы предложили классифицировать
устройства, которые могут быть использованы подобным образом, как генераторы
событий) — это бывает в приложениях реального времени, а также в задачах,
которые сейчас стало модно называть "задачами мягкого реального времени"
— мультимедийных программах, генерирующих поток звука, синхронизованного
с изображением, и, особенно, в компьютерных играх.
Для работы с таким устройством прикладная программа, так или иначе, должна
зарегистрировать обработчик поступающих от устройства событии. В системах
Unix такая регистрация состоит в открытии устройства для чтения, а ожидание
события заключается в выполнении над этим устройством операции чтения.
Для последовательных устройств ввода операция чтений разблокируется, когда
с устройства поступят хоть какие-то данные (а не тогда, когда будет заполнен
весь буфер), поэтому, если пришло только одно событие, мы его не пропустим.
Драйверы многих устройств, способных работать в качестве генераторов событий,
имеют команды ioctl, позволяющие более тонко
управлять условием разблокирования функции read.
Для того чтобы во время ожидания события от генератора, заниматься еще
какой-то полезной работой, предлагается либо выделить ожидающий события
вызов read в отдельную нить, либо пользоваться
системными вызовами lect и poll,
позволяющими ожидать событий на нескольких устройствах (а также средствах
межпроцессного взаимодействия) одновременно.
Другие ОС предоставляют для работы с устройствами-генераторами событий
более сложные механизмы, зачастую основанные на callback
(дословно — "вызов назад"; механизм взаимодействия подсистем,
когда подсистема, запрашивающая сервис, передает обслуживающей подсистеме
указатель на функцию, которую необходимо вызвать при наступлении определенного
события).
Работа с генераторами событий требует решения еще одной задачи — хранения
поступающих событий в периоды, когда пользовательская программа их не
успевает обрабатывать. Необходимость относительно сложных схем работы
с требуемыми для этого буферами вынудила разработчиков Unix System V Release
3 ввести еще один тип драйверов — потоковые
(STREAMS) [docs.sun.com 805-7478-10]. Для
прикладной программы потоковый драйвер не отличается от обычного символьного
устройства, но отличий с точки зрения системы довольно много. Некоторые
из этих отличий будут рассматриваться далее.
Unix System V Release 3 (SCO Open Desktop, SCO OpenServer), Release (SCO
UnixWare, SGI Irix, Sun Solaris) и системы, испытавшие влияние OSF Unix
(IBM AIX, HP/UX) используют потоковые драйверы для реализации таких важных
псевдоустройств, как трубы и сокеты TCP/IP. Кроме того, потоковыми в этих
системах являются драйверы сетевых адаптеров и терминальных устройств.
С другой стороны, в OS/2 и Windows NT/2000/XP существуют обширные номенклатуры
типов драйверов с различными наборами функций. Так, в OS/2 используются
драйверы физических устройств следующих типов:
- простые драйверы последовательных устройств ввода-вывода,
аналогичные драйверам символьных устройств в Unix;
- Драйверы запоминающих устройств прямого доступа, аналогичные
драйверам блочных устройств в Unix;
- Драйверы видеоадаптеров, используемые графической оконной
системой Presentation Manager (PM);
- Драйверы позиционных устройств ввода (мышей и др.),
также используемые РМ;
- Драйверы принтеров и других устройств вывода твердой
копии;
- Драйверы звуковых устройств, используемые подсистемой
"мультимедиа" MMOS/2;
- драйверы сетевых адаптеров стандарта NDIS, используемые
сетевк программным обеспечением фирм IBM и Microsoft;
- драйверы сетевых адаптеров стандарта ODI, используемые
программным обеспечением фирмы Novell;
- DMD (Device Manager Driver - - драйвер-менеджер класса
устройств (в разд. 10.2 мы подробнее разберемся с назначением драйверов
этог типа);
- различного рода "фильтры", например,
ODINSUP.SYS — преобразоватеи ODI-интерфейса в NDIS.
|