2.1. Система классов DriverWorks.
Возможно, идея писать драйвера объектно-ориентированными и кажется на первый взгляд нелогичной. Но при более близком знакомстве с DriverStudio и с драйверами в общем, оказывается, что это не так уж страшно и довольно удобно. Объектная модель DriverWorks отражает архитектуру WDM и представляет собой систему классов, построенную на системных вызовах. Цель DriverWorks – с одной стороны, оставаться на достаточно низком уровне программирования, чтобы эффективно писать драйвера, а с другой – упростить и упорядочить процесс разработки драйверов режима ядра.
В соответствии с идеологией DriverWorks драйвер представляется, как набор объектов. Эта же идея присутствует и в "чистой" архитектуре WDM, но DriverWorks упорядочивает эти объекты и представляет их экземплярами классов. Классы DriverWorks также несколько упрощают код драйвера по сравнению с DDK, делают его более компактным и доступным для понимания. Часто повторяющиеся, рутинные фрагменты кода драйвера спрятаны внутри методов класса. И то, что при использовании пакета DDK занимало несколько строк в программе, теперь можно вполне заменить вызовом одного единственного метода.
Также в DriverWorks предложено несколько полезных классов: например класс KFile — доступ к файлам или классы динамических списков и массивов.
В общем, сама идея DriverWorks напоминает Visual C++ и библиотеку MFC. MFC представляет из себя некую прослойку, которая отделяет программиста от жутковатых функций API и позволяет создавать объектно-ориентированные проекты, при этом оставаясь на достаточно низком уровне программирования.
Впрочем, в системе классов DriverWorks есть одна особенность: иерархия классов практически отсутствует. Это вполне естественно: в системе классов DriverWorks присутствуют самые различные классы — классы, представляющие собой ресурсы устройства (линии ПДП, прерываний, областей памяти, портов ввода-вывода), сами устройства, классы для взаимодействия с реестром, файлами и т.п. Еще одним аргументом в пользу отсутствия наследования является то, что разветвленная иерархия классов может снизить быстродействие программы. Для драйвера, это, конечно, неприемлемо.
В основе архитектуры DriverWorks лежит несколько основных классов.
Объект драйвера (Driver Object).
Объект драйвера является экземпляром класса KDriver. Он представляет драйвер в целом как некую абстракцию. Для объекта драйвера абсолютно все равно, каким оборудованием он управляет, объект драйвера об этом по настоящему никогда не задумывается. Его задача — обеспечить интерфейс драйвера с ОС: загрузка и инициализация драйвера, выгрузка и т.п. А управление аппаратурой возлагается на другие объекты драйвера, в частности, на объект устройства.
Когда ОС загружает драйвер, то она создает для него соответствующий объект драйвера. Компонент ядра операционной системы — диспетчер ввода-вывода (I/O Manager) — использует объекты драйверов для управления устройствами. Каждый драйвер отвечает за управление одним или несколькими объектами устройств. Запрос на операцию ввода-вывода (I/O request), посланный приложением пользователя, поступает к диспетчеру ввода-вывода.
Диспетчер ввода-вывода определяет, какой именно объект драйвера отвечает за соответствующий объект устройства, и перенаправляет ему запрос. Кроме управления объектами устройств, объект драйвера имеет дополнительные методы, отвечающие за инициализацию и завершение работы драйвера. Программист создает свой подкласс класса KDriver для взаимодействия с системой. Он обязательно должен содержать метод DriverEntry — функцию, вызываемую при инициализации драйвера.
В отличие от обычного не-WDM драйвера, процедура инициализации WDM-драйвера выполняет весьма ограниченное число функций: в основном это загрузка некоторых внутренних переменных на основе данных реестра. WDM-драйвер не инициализирует ресурсы устройства при старте в вызове EntryPoint. Для этого существует объект устройства.
WDM-драйвер экспортирует метод AddDevice, который вызывается системой, если обнаружено устройство, поддерживаемое данным драйвером. Этот метод отвечает за создание объектов устройств, соответствующих системным физическим объектам устройств (Physical Device Object, PDO).
Объект драйвера может содержать дополнительные методы для реинициализации драйвера на более поздних стадиях загрузки системы. Такой подход необходим, если в системе присутствует несколько драйверов, критичных к порядку их загрузки. Впрочем, такие проблемы встречаются нечасто.
В принципе, хорошо спроектированный драйвер должен экспортировать метод Unload, который вызывается при выгрузке драйвера. Но такие случаи встречаются довольно редко.
Класс KRegistryKey
Как было упомянуто выше, драйвер обращается к системному реестру при инициализации. Системный реестр (registry) — системная база данных, организованная в виде дерева, похожего на дерево каталогов. Каждую ветвь этого дерева (реестра) называют разделом (key), каждый лист – параметром (value). Данные, хранящиеся в реестре, могут быть разных типов: целое (integer), строка, набор байтов.
Система позволяет каждому драйверу хранить данные в реестре. Эти данные используются драйверами при старте и инициализации. Обычно драйвер хранит данные в разделе HKLM\SYSTEM\CurrentControlSet\Services\<имя драйвера>\Parameters\.
В DriverWorks есть класс KRegistryKey, который облегчает доступ к параметрам реестра. Он имеет методы для чтения (QueryValue), записи (WriteValue), удаления (Delete) значений ключей реестра. При вызове конструктора KRegistryKey сразу указывается ключ, с которым будет связан создаваемый объект. Далее можно изменить ключ при помощи метода Reconstruct.
Объект запроса на ввод-вывод (I/O Request Object)
Объекты запроса на ввод-вывод, более известные, как пакеты запроса на ввод-вывод (I/O request packet, IRP — так мы и будем их называть в дальнейшем), предназначены для управления драйверами режима ядра.
Физически IRP представляет собой весьма сложную структуру данных, содержащую множество полей, таких как код статуса, указатель на буфер пользователя, указатель на IRP драйвера более высокого уровня, различные флаги и т.п. Многие из этих полей не используется драйверами режима ядра, но необходимы для того, чтобы IRP был функционально полным инструментом управления драйверами. Т.е. при помощи IRP можно управлять любым типом драйвера. При желании увидеть структуру IRP во всем ее великолепии — см. Win2000 DDK.
Обмен информацией и управление драйверами при помощи IRP выглядит приблизительно следующим образом: когда приложение пользователя посылает данные или пытается получить данные из устройства, диспетчер ввода-вывода формирует IRP и отправляет его драйверу, отвечающему за данное устройство. Объект драйвера получает этот IRP и перенаправляет его одному из своих объектов устройств. Объект устройства, получив пакет, может либо начать его обработку немедленно, либо поставить его в очередь, чтобы обработать этот пакет позже. Что именно сделает объект устройства, зависит от того, какой пришел IRP, от состояния объекта устройства и от состояния самого устройства. После того, как пакет будет обработан, объект устройства пошлет IRP с информацией о результате операции обратно диспетчеру ввода-вывода.
Каждый IRP описывает операцию В/В, которая может быть выполнена устройством. Для того, чтобы драйвер смог получить информацию о том, какая именно операция должна быть выполнена, IRP содержит целый набор атрибутов: старший и младший коды функции, код статуса и различные параметры: число байт, которые должны быть прочитаны, смещение и т.п. За время своего существования IRP может проходить несколько уровней иерархии драйверов устройств в системе. Поэтому в пакете резервируется место для сохранения данных и параметров, необходимых для следующего драйвера в иерархии — так называемый "стек IRP", "IRP stack location". Когда объект устройства обрабатывает запрос, то он имеет доступ только к тем участкам стека, которые предназначены для использования им или устройством более низкого уровня, которому будет перенаправлен IRP.
Рис. 3 – Интерфейс с драйвером при помощи IRP
IRP могут создаваться как диспетчером В/В, так и самими драйверами. Чаще всего это происходит при выполнении функций CreateFile, CloseFile, ReadFile, WriteFile и DeviceControl.
IRP может быть уничтожен, если необходимо отменить операцию В/В, например, при закрытии приложения. Объект IRP содержит указатель на функцию, вызываемую при уничтожении пакета.
Объект устройства (Device Object).
Объекты устройств являются экземплярами класса KDevice или KPnpDevice. Эти классы являются краеугольными камнями архитектуры DriverWorks: они представляют собой как бы программный образ тех устройств, которые присутствуют в системе. Именно объекты устройств обеспечивают управление и обмен данными с внешними устройствами, управление их ресурсами — линиями прерываний, каналами ПДП, диапазонами адресов памяти, портами В/В и т.п. Когда выполняется системный вызов типа CreateFile, ReadFile, WriteFile, диспетчер В/В посылает IRP соответствующему драйверу. Но сам драйвер, вернее объект драйвера, не выполняет никаких операций по обработке этого пакета — он просто передает его объекту устройства и забывает о самом существовании этого IRP. Это естественно, ведь управление физическим устройством — не его задача, это дело соответствующего объекта устройства.
Класс KDevice является суперклассом для всех классов устройств. Но на практике он сейчас почти не используется. Чаще используют его потомка – класс KPnpDevice. Этот класс предназначен для управления PnP-устройствами, т.е. устройствами, которые конфигурируется системой. В данный момент практически все устройства являются PnP-устройствами. Появление таких устройств здорово облегчило жизнь разработчикам драйверов: использовать KPnpDevice намного проще, а часто и безопаснее, чем KDevice. Еще бы, ведь в данном случае все проблемы конфигурирования и инициализации ресурсов оборудования ложатся на широкие плечи системы.
Любой объект устройства содержит стандартные методы обработки запросов на чтение, запись и управление устройством (device control). Эти методы вызываются при вызове соответствующих функций API ReadFile(), WriteFile(), DeviceControl().
Каждый драйвер содержит минимум один объект устройства. Как было упомянуто выше, объект устройства является экземпляром класса, порожденного программистом от класса KDevice или KPnpDevice. Для придания функциональности объекту устройства разработчик драйвера может переопределять виртуальные методы суперкласса, включая те методы, которые обрабатывают различные типы IRP, а также добавлять свои свойства и методы в класс. В процессе разработки можно использовать как иерархию классов DriverStudio, так и функции DDK. Впрочем, для большинства задач без использования DDK вполне можно обойтись.
Естественно, делать все это надо с осторожностью. Вызов некотроых методов является обязательным. Переопределение виртуальных методов также требует осторожности: многие из них содержат код, который обязательно должен быть выполнен. Если его удалить, то драйвер будет работать неправильно (если будет работать вообще). В результате, система скорее всего, зависнет.
Все вышесказанное, конечно, может быть очень интересным, но остается открытым вопрос: так как же все-таки драйвер, вернее объект устройства, управляет аппаратурой? Официально провозглашается, что Win2000 — переносимая система. Т.е., если она хорошо работает на архитектуре Intel, то она также может быть перенесена и на другие системы, например Alpha. Для того, чтобы система с минимальными исправлениями могла работать на компьютерах с другой архитектурой, и был введен HAL — уровень абстракции аппаратуры. Он действительно абстрагирует драйвера и большую часть кода ядра ОС от того, как именно построен компьютер. Теперь разработчику драйвера становится абсолютно все равно, как на данном компьютере реализован контроллер прерываний или контроллер прямого доступа к памяти – все ресурсы аппаратуры также представлены объектами. Это диапазоны адресов памяти и портов В/В устройства, линии прерываний и ПДП. Все они могут быть реализованы в различных архитектурах по разному, но общие принципы их работы остаются одними и теми же. Соответственно, и интерфейс классов, реализующих управление этими ресурсами, остается одинаковым. Нам, как программистам, теперь не нужно знать тонкости работы аппаратной части на этом компьютере — это задача HAL и тех людей, которые переносили систему.
На практике это означает то, что если устройство, например, имеет диапазон адресов памяти и линию запроса на прерывание, то класс устройства будет содержать два свойства (данные). Одно из них — экземпляр класса KMemoryRange, который будет реализовывать управление памятью устройства, а другое — экземпляр класса KInterrupt, который управлет линией запроса на прерывание, и всем, что с ней связано. Если устройство будет иметь несколько областей адресов памяти, то, соответственно, класс устройства будет содержать несколько экземпляров класса KMemoryRange.
Другим способом управления устройствами является наличие устройств нижнего уровня (Lower devices). Как уже было отмечено, особенностью архитектуры WDM является наличие стека драйверов, когда драйвера могут обмениваться IRP-пакетами между собой. Данную ситуацию легче объяснить при помощи рисунка:
Рис. 4 — стек устройств
На рис. 4 изображен стек устройств, состоящий из трех объектов устройств. Устройство 1 — самое первое (верхнее) в стеке, устройство 3 — самое последнее (нижнее) в стеке. Тогда по отношению к устройству 1 устройство 2 будет устройством нижнего уровня. Устройства верхнего уровня для устройства 1 нет. Устройство 2 имеет и устройство верхнего уровня (устройство 1) и устройство нижнего уровня (устройство 3). Для устройства 3 есть только устройство верхнего уровня (устройство 2), устройства нижнего уровня у него нет, устройство 3 напрямую контролирует оборудование.
Такой метод управления оборудованием, когда в системе присутствует не один драйвер, а целая цепочка драйверов, может иметь свои преимущества. Предположим, наше физическое устройство — это клавиатура, подключенная к USB–порту. Тогда объект устройства 3 — драйвер USB–порта. Устройство 2 выполняет действия, специфичные именно для данного типа клавиатур: читает данные из портов ввода-вывода клавиатуры, "висит" на прерывании, выполняет дополнительные функции (например, если у нас мультимедийная клавиатура). Он передает коды нажатых клавиш устройству 1. Устройство 1 не зависит от того, какой именно тип клавиатуры подключен к компьютеру. Оно реализует очередь кодов нажатых клавиш; реакцию на клавиши CapsLock, Shift и т.п.
Если в данном случае у компьютера поменяется клавиатура, то необходимо установить только новый драйвер 2. Если клавиатура переключится на другой порт, то устройство 2 будет общаться не с устройством 3, а с каким-то другим устройством. В таком случае система становится более гибкой, легкой в проектировании, более надежной и простой в использовании. И пользователю, и приложениям становится абсолютно все равно, какой тип клавиатуры установлен на компьютере.
В нашем примере получается, что и устройство 1, и устройство 2 управляет оборудованием – клавиатурой. Но они делают это не напрямую, а посылая IRP устройству 3. Для того, чтобы наш объект устройства мог передавать IRP–пакеты другим объектам устройств, введен класс устройств нижнего уровня (KLowerDevice, KPnpLowerDevice). Естественно, для этого устройство должно знать, как управлять устройством нижнего уровня при помощи IRP.
Впрочем, подбная ситуация имеет место практически во всех современных ОС. Только в других системах это выражено менее ярко и не декларируется, как "официальная идеология".
Затрагивая тему управления аппаратурой, нельзя не упомянуть еще об одном способе управления устройствами. Иногда нет возможности использовать классы DriverWorks или функции DDK. Например, необходимо обратится непосредственно к портам ввода-вывода компьютера, в частности, к портам управления принтером. Напрямую сделать это из приложения пользователя, работающего под Win2000, невозможно. Все пользовательские программы работают в непривилегированном кольце защиты 3 и не могут выполнять ассемблерные команды типа inp / outp. Но драйвер работает в кольце защиты 0 и, фактически, может делать все что угодно. В этом случае следует переопределить методы класса устройства, например ReadFile(), WriteFile(), DeviceControl() – добавить туда ассемблерные вставки или код на С, выполняющий то, что нам необходимо сделать (чаще всего это обращение к портам ввода-вывода). Впрочем, любое обращение к портам ввода-вывода компьютера напрямую может оказаться опасным. Если программист допустит ошибку или неточность в манипуляциях с параллельным портом, то это, скорее всего, пройдет бесследно для системы. Но если он ошибется при обращении к портам управления таймером, винчестером или другими жизненно важными устройствами компьютера, то в лучшем случае система зависнет.
Объекты очередей и буферизация запросов.
Сколько операций может параллельно выполнять наше физическое устройство? Естественно, это определяется самой природой этого устройства. Многие виды оборудования могут одновременно делать что-то одно. Например, параллельный порт не может передать два байта за один раз при всем нашем желании, ведь физически это один канал передачи. Но ведь IRP–пакеты могут приходить в любое время! Поэтому большинство объектов устройств должны содержать какой-либо механизм для буферизации и упорядочивания (serialization) запросов, т.к. зачастую только один запрос может быть обработан в единицу времени. Самым простым и в то же время эффективным методом такой буферизации является очередь.
Объекты, внедренные в объект устройства, представлены в классе KDeviceQueue. Его методы не только реализуют манипуляцию с очередью, но и решают более интеллектуальные задачи. Например, есть метод, смысл которого может быть описан таким образом: "Если устройство сейчас обрабатывает запрос и занято, то помести новый запрос в очередь, иначе немедленно начни его обработку". Подобные методы сильно облегчают задачу буферизации запросов для объекта устройства. Но возможна и другая ситуация: устройство может одновременно обрабатывать запросы разного вида. К примеру, наше устройство – это дуплексный канал связи. Оно одновременно может и принимать, и передавать информацию. Если мы будем использовать для буферизации всех одну очередь, то такой подход является неэффективным. Поэтому система позволяет объектам устройств создавать дополнительные объекты очередей. Они реализованы в классе KDriverManagedQueue.
Рис. 5 — объект очереди, внедренный в объект драйвера.
Эти классы имеют методы, сходные с методами класса KDeviceQueue. Впрочем, ситуации, когда следует применять более одной очереди для буферизации запросов, встречаются не так уж и часто.
Обработка запросов на прерывание при помощи DriverWorks
В контексте данного руководства будем считать, что прерывание (Interrupt) – асинхронный аппаратный сигнал, который обычно возникает, когда периферийному устройству необходимы ресурсы процессора. "Асинхронный" означает то, что прерывание возникает в произвольные моменты времени (если вообще возникает). Прерывание заставляет процессор прервать выполнение программы, сохранить свое состояние, обработать поступивший запрос (вызывается процедура обработки прерывания, Interrupt Service Routine, ISR) и возобновить выполнение прерванной программы. При этом останавливаются все остальные процессы и потоки ОС вне зависимости от их приоритета.
Для того, чтобы удовлетворять разнообразным требованиям, возникающим при работе разнообразных устройств и программ на различных типах компьютеров, ОС предлагает концепцию уровня запроса на прерывание (Interrupt Request Level), IRQL. Всего существует 32 IRQL для данного процессора, пронумерованных от 0 до 31. При этом 0 — самый низкий приоритет, 31 — самый высокий.
31 |
Сбой работы шины |
29 |
Сбой в цепи питания |
28 |
Запрос от другого процессора (в многопроцессорной системе) |
|
Прерывания, доступные устройствам В/В |
2 |
Выполнение DPC |
1 |
Исключение защиты (page fault) |
0 |
Passive level |
Табл. 1 – уровни IRQL.
Для катастрофических событий ОС резервирует самые приоритетные прерывания (31–29). Для программных прерываний — прерывания с самым низким приоритетом (2–1). PassiveLevel — обычный режим работы драйвера. IRQL, предоставляемые для работы системных устройств, находятся где-то посредине нумерации уровней. О том, как эти прерывания сопрягаются с архитектурой компьютера, заботится HAL.
Естественно, в любой момент процессор может обрабатывать только один запрос на прерывание. Обработка поступившего прерывания прервется только в том случае, если поступит прерывание с более высоким приоритетом.
При проектировании процедуры обработки прерывания следует минимизировать время, которое будет затрачено на обработку прерывания. Иначе процессор будет чересчур долго обрабатывать прерывание и ни один процесс не сможет возобновить свою работу. Когда вызывается ISR первое, что она должна сделать сообщить оборудованию, что запрос на прерывание получен и обработан. После этого можно завершать обработку прерывания. Но как тогда обработать данные, поступившие от устройства, если мы сразу же завершим обработку прерывания? Для этого введен механизм вызова отложенных процедур (Deferred Procedure Call, DPC). Перед завершением работы ISR следует вызвать отложенную процедуру (DPC). DPC начнет выполнятся, как только процессор освободится от обработки прерываний. DriverWorks предоставляет класс KDeferredCall, в котором инкапсулируются данные и методы, необходимые для использования механизма DPC.
DriverWorks инкапсулирует все функции, необходимые для обработки прерываний, в классе KInterrupt. Экземпляр класса KInterrupt должен быть создан, как свойство в классе устройства. Пусть в нашем случае класс устройства называется MyDevice, объект класса KInterrupt – m_TheInterrupt. Далее в классе устройства описывается функция ISR:
BOOLEAN MyDevice::TheIsr(void);
Далее, в методе OnStartDevice следует добавить код для привязки ISR к устройству:
status = m_TheInterrupt.InitializeAndConnect(pAssignedResource,Isr,Context,0,FALSE);
где Context — значение без типа (void), передаваемое ISR.
Isr — адрес ISR, процедуры обработки прерываний.
Теперь осталось только добавить в конструктор следующий код:
VOID MyDevice::MyDevice(void) {
. . .
status = m_TheInterrupt.InitializeAndConnect(pAssignedResource, LinkTo(Isr), this, 0, FALSE );
. . .
}
Для отключения ISR следует вызвать метод Disconnect().
Естественно, данное описание не претендует быть полным описанием такой важной темы, как обработка прерываний и связанные с ней проблемы. Но в примере драйвера, описываемом ниже, отсутствует реакция на прерывания, а не упомянуть о них нельзя. Для более подробного обзора темы прерываний и DPC следует обратиться к документации DriverWorks или DDK.
Объекты для управления оборудованием
Как было упомянуто выше, объект устройства управляет работой устройства при помощи специальных объектов, управляющих работой оборудования – портами В/В, прерываниями, памятью, контроллерами ПДП. Драйвер создает эти объекты для представления физических параметров устройства.
Большинство периферийных устройств находятся на шинах компьютера. В современном компьютере есть несколько шин. Обычно процессор, внешняя кэш-память, и оперативная память находятся на высокоскоростной шине, архитектура которой специфична для данного типа процессора. Шина процессора соединена мостом со стандартной скоростной шиной, на которой находятся контроллеры дисплея, некоторые скоростные устройства. Архитектура этой шины может быть процессоро-независимой. Пример такой шины — PCI. Эта шина также может быть соединена мостом со вторичной локальной шиной, часто более медленной. На ней могут находиться контроллеры дисковых накопителей, сетевых адаптеров и т.п.
Периферийные устройства обычно имеют "на борту" регистры и диапазоны адресов памяти, при помощи которых реализуется интерфейс устройства с системой. Но добраться до них не так просто: процессор ведь физически использует другие механизмы для обращения к своим "родным" портам ввода-вывода и оперативной памяти. Для того, чтобы обратится к памяти и портам устройства, находящегося на локальной шине, процессор должен выполнить отображение (mapping) адресного пространства процессора и той шины, где находится наше устройство. В результате этой операции к участку памяти, физически находящийся в устройтсве, можно обращаться, как к участку оперативной памяти процессора. При таком обращении процессор переадресует запрос локальной шине. Но тут следует вспомнить об особенностях архитектуры Windows (да и практически любой современной ОС): ведь система поддерживает механизм виртуальной памяти! Пользовательские приложения теперь работают в своем адресном пространстве, а система, в том числе и драйвера, — в своем. Куда же будет отображена память устройства?
Ответ прост. Можно отобразить диапазон адресов устройства как на адресное пространство системы, так и на адресное пространство пользовательского процесса. Соответственно различаться будет и способ доступа к памяти устройства из приложения пользователя: в первом случае буфер с данными для записи или чтения будет передаваться драйверу из приложения, а в драйвере эти данные будут пересылаться устройству. Во втором случае приложение будет писать и читать данные в выделенный ему участок памяти, который находится в адресном пространстве процесса. Какой механизм выбрать — дело разработчика драйвера.
Объекты, представляющие адресное пространство периферийных устройств, представлены классами KPeripherialAdress, KIoRange, KMemoryRange, KIoregister, KMemoryRegister. KPeripherialAdress является базовым классом для большинства остальных классов управления диапазонами памяти и портов ввода-вывода. Сам класс KperipherialAdress в основном, не используется. Используются, в основном, следующие его подклассы:
• KIoRange — диапазон адресов ввода-вывода. Данный класс отображает диапазон адресов портов В/В из адресного пространства какой-либо из шин в адресное пространство процессора. При использовании класса KIoRange можно читать и записывать в порты 8, 16, и 32-битные значения.
• KIoRegister является альтернативным путем доступа к портам ввода-вывода. В виде экземпляра KIoRegister может быть пердставлен отдельный порт-ввода вывода в диапазоне адресов. Фактически, KIoRange — это несколько экземпляров класса KIoRegister, объединенных в массив. Создать экземпляр KioRegister можно, используя как стандартный конструктор, так и используя оператор [] класса KIoRange, например:
KIoRange m_range;
…
KIoRegister m_reg = m_range[6];
…
Применение KIoRegister упрощет процесс программирования и улучшает читабельность программы.
• KMemoryRange используется для отображения диапазона адресов памяти из адресного пространства шины в адресное пространство процессора (адресное пространство системы). После того, как память будет отображена, драйвер должен использовать методы доступа к памяти, позволяющие оперировать 8, 16 и 32– битными значениями.
• KMemoryRegister аналогичен KIoRegister, за исключением того, что в данном случае он представляет из себя отдельную ячейку памяти в адресном пространстве устройства.
• KMemoryToProcessMap используется для отображения диапазона адресов памяти шины в адресное пространство пользовательского процесса. Это может оказаться очень удобным: пользователь может напрямую общаться с памятью устроства в программе, как с обычным буфером. Впрочем, такое отображение следует применять с большой осторожностью: возможна ситуация, когда пользователь запустит несколько экземпляров программы, и все они начнут работать с памятью устройства одновременно. Вряд ли стоит объяснять, к чему это может привести.
Стоит отметить, что немалая часть устройств могут общаються со своей памятью только словами. Длина слова зависит от устройства, и может колебаться в широких пределах. Обычно для PCI-устройств — 32 бит.
В документации настоятельно рекомендуется использовать только эти классы для управления оборудованием. Это связано с возможной переносимостью драйвера на другие платформы. При использовании этих классов, которые, в свою очередь, используют функции DDK для доступа к оборудованию, процесс портирования пройдет безболезненно, т.к. для доступа к устройству будет использован HAL. Если же программист будет пытаться управлять устройствами самостоятельно, то драйвер придется переписывать при переносе на другую платформу.
Есть еще одна причина, по которой стоит использовать эти классы: ведь с ними разрабатывать драйвер намного проще!
Объекты синхронизации
Как и все Windows–программы, драйвера являются частью многозадачной операционной системы, в которой выполняется множество процессов и потоков. Драйвер, как и программа, также может содержать несколько потоков. При этом, естественно, возникает проблема синхронизации работы этих потоков, совместного доступа к данным и т.п. Особенно актуальной эта проблема становится в многопроцессорной системе. Windows 2000 предназначается для работы в многопроцессорных системах, и если пренебречь синхронизацией при разработке драйвера, то это может повлечь за собой неприятные последствия.
Для решения задач синхронизации WDM (и, соответственно, DriverWorks) предлагает различные средства. Простейшим из объектов синхронизации является защелка (Spin Lock), представленная классом KSpinLock. Принцип действия защелки очень прост: чтобы запретить любому другому потоку в системе доступ к данным, нужно вызывать метод Lock защелки. Любой поток, пытающийся получить доступ к заблокированным данным, уснет. Чтобы снять блокировку, нужно вызвать метод Unlock.
Класс диспетчера KDispatcherObject является суперклассом для нескольких важных классов синхронизации. Эти классы управляют планировщиком Windows и позволяют синхронизировать как работу драйверов, так и работу приложения пользователя и драйвера. Все классы, порожденные от KDispatcherObject, имеют два важных отличия:
• С объектом диспетчера связана логическая переменная–флажок, который может находиться в двух состояниях: сигнализировать (TRUE) и молчать (FALSE).
• Если поток вызовет метод Wait диспетчера, он приостановится до тех пор, пока диспетчер не перейдет в состояние "сигнализирует".
При работе с объектами диспетчера и его подклассов следует иметь в виду, что нельзя блокировать поток при обработке прерывания. Последствия будут фатальными.
Подклассы класса KDispatcherObject:
KEvent — используется для синхронизации работы потоков. Kevent почти не отличается от объекта диспетчера.
KSemaphore инкапсулирует системный объект семафора. Семафор отличается от объекта события тем, что имеет счетчик. Семафор сигнализирует в том случае, если счетчик больше нуля. Семафоры могут быть полезны, например, при управлении несколькими пото– ками.
KTimer — таймер. При создании таймера его флажок находится в состоянии "молчит". Временной интервал таймера задается функцией Set с точностью до 100 нс. На практике таймер устойчиво работает с временем ожидания >= 10 мс. Когда пройдет указанный промежуток времени, таймер перейдет в состояние "сигнализирует". Подклассом Ktimer является класс KTimedCallBack. В нем по истечении промежутка времени выполняется вызов отложенной процедуры (DPC).
KSystemThread позволяет создать новый поток в драйвере. Потоки в драйвере используются в разных целях. В основном это — поллинг медленных устройств и работа на многопроцессорных системах. Для запуска потока следует создать функцию, которая станет функцией потока и вызвать метод Start. Для уничтожения потока — метод Terminate. При работе с потоками можно использовать все упомянутые выше классы синхронизации.
Дополнительные классы.
DriverWorks предоставляет дополнительные классы для нужд программиста. Это классы очередей, списков, стеков; классы файлов и Unicode–строк; классы синхронизации.
Списки представлены класами KList, KInterlockedList, KInterruptSafeList. Они представляют шаблоны двунаправленных списков и стандартные методы для вставки, удаления и добавления элементов. Различаются эти классы методами синхронизации. KList не содержит никаких методов синхронизации и защиты данных. KInterLockedList использует защелки (spin locks) для защиты внутренних связей в списке. KInterruptSafeList использует присоединенный объект прерывания для защиты связей. По аналогичному принципу работают шаблоны классов FIFO (стек): KFifo, KLockableFifo, KInterruptSafeFifo. Класс KFile инкапсулирует методы для работы с файлами. Этот класс позволяет читать и записывать данные в файл а также изменять атрибуты файлов. Для представления Unicode – строк используется класс KUstring. Методы данного класса позволяют выполнять сравнение, конкатенацию, доступ к символам строки и разнообразные преобразования типа.
Связь драйвера с приложением пользователя
Также остается неясным еще один вопрос, связанный с драйверами: как именно с нашим объектом устройства может связаться приложение или другой драйвер? Большинство из устройств в системе именованы, хотя теоретически допускается существование неименованных (anonymous) устройств. Связь с устройством можно установить двумя методами:
• при помощи GUID.
• при помощи символической ссылки.
1. GUID (Globally Unique Identifier, глобально уникальный идентификатор) — 16-байтное уникальное число. GUID используются для идентификации в системе драйверов, СОМ-объектов и т.п. В идеале, во всем мире не может быть двух одинаковых GUID, поэтому GUID может быть абсолютно уникальным идентификатором драйвера. GUID генерируется на основе текущей даты, времени и номера сетевого адаптера, если такой присутствует, и обычно указывается в заголовочном файле класса устройства и программы, которая хочет связаться с ним приблизительно таким образом:
#define MyDevice_CLASS_GUID \
{ 0xff779f4c, 0x8b57, 0x4a65, { 0x85, 0xc4, 0xc8, 0xad, 0x7a, 0x56, 0x64, 0xa6 } }
2. Символическая ссылка (symbloic link) похожа на путь к файлу и в тексте программы имеет вид:
char *sLinkName = "\\\\.\\MyDevice";
Если отбросить лишние символы бэкслэша, необходимые для соблюдения синтаксиса С++, то символическая ссылка оказывается строкой \\.\MyDevice. Чтобы понять принцип работы символической ссылки, следует знать, что в ОС есть системный каталог различных объектов, которые присутствуют в системе: драйверов, устройств, объектов событий, семафоров и т.п. Символическая ссылка – специфический тип объекта, который обеспечивает доступ к другим системным объектам. Специальный подкаталог системного каталога зарезервирован для символических ссылок на другие объекты ОС. Программа пользователя может обратиться к этим символическим ссылкам при помощи функций API.
Как же следует проектировать интерфейс с драйвером? Следует использовать GUID или символическую ссылку?
Идентификация драйвера при помощи GUID считается более правильной. Как было упомянуто выше, специальные алгоритмы гарантируют то, что GUID будет действительно уникальным. А кто мешает разработчику, находящемуся на другом конце света, также создать устройство с той же ссылкой на него \\.\MyDevice? Вообще-то, никто. Но с другой стороны, с написанной на понятном английском языке ссылкой гораздо проще обращаться, особенно на этапе разработки драйвера, чем с длинным и непонятным GUID. Так что, вероятно, на этапе разработки и отладки драйвера для интерфейса драйвера с приложением лучше использовать символическую ссылку, а для коммерческой версии драйвера — GUID.