Текст
                    Уолтер Они
ДЛЯ ПРОФЕССИОНАЛОВ
ИСПОЛЬЗОВАНИЕ
MICROSOFT WINDOWS DRIVER MODEL
Microsoft
ПИТЕР'

АН 09

НЕТ СТРАНИЦЫ
НЕТ СТРАНИЦЫ
НЕТ СТРАНИЦЫ
Краткое содержание
Благодарности.....................................................15
Введение..........................................................16
Глава	1.	В начале работы над проектом драйвера ................26
Глава	2.	Базовая структура драйвера WDM........................43
Глава	3.	Основные приемы программирования......................99
Глава	4.	Синхронизация........................................163
Глава	5.	Пакеты запросов ввода/вывода.........................213
Глава 6. Поддержка Plug and Play для функциональных драйверов . . 294
Глава 7. Чтение и запись данных................................342
Глава 8. Управление питанием...................................398
Глава 9. Управляющие операции ввода/вывода....................445
Глава 10. WMI.................................................470
Глава 11. Контроллеры и многофункциональные устройства........495
Глава 12. USB.................................................517
Глава 13. Устройства взаимодействия с пользователем...........587
Глава 14. Специализированные темы.............................617
Глава 15. Распространение драйверов устройств.................639
Глава 16. Фильтрующие драйверы................................709
Приложение А. Решение проблем несовместимости между платформами 736
Приложение Б. Мастер WDMWIZ.AWX...............................746
Алфавитный указатель..........................................756
Содержание
Благодарности.......................................................15
Введение............................................................16
Для кого написана эта книга......................................16
Структура книги..................................................17
Безопасность и надежность драйверов..............................19
Файлы примеров...................................................20
О компакт-диске................................................21
Как создавались примеры........................................22
Построение примеров..........................................  22
Обновления примеров............................................23
GENERIC.SYS......................................................23
Системные требования.............................................24
Об ошибках.......................................................25
Другие ресурсы...................................................25
От издательства..................................................25
Глава 1. В начале работы над проектом драйвера....................  26
Краткая история драйверов устройств..............................26
Обзор операционных систем .......................................30
Обзор архитектуры Windows ХР.................................  30
Обзор архитектуры Windows 98/Windows Me........................32
Какой драйвер вам нужен?.........................................35
Драйверы WDM...................................................37
Фильтрующие драйверы WDM.......................................38
Монолитные функциональные драйверы WDM.........................38
Другие типы драйверов..........................................39
Управление проектом и контрольный список.........................40
Глава 2. Базовая структура драйвера WDM.............................43
Как работают драйверы............................................43
Как работают приложения........................................44
Драйверы устройств.............................................46
Поиски загрузка драйверов .......................................49
Иерархия устройств и драйверов.................................50
Содержание
7
Устройства Plug and Play............................................52
Наследные устройства................................................55
Рекурсивное перечисление............................................56
Порядок загрузки драйверов..........................................58
Две основные структуры данных..........................................63
Объекты драйверов...................................................65
Объекты устройств...................................................68
Функция DriverEntry....................................................71
Обзор DriverEntry...................................................72
DriverUnload........................................................75
Функция Add Device.....................................................75
Создание объекта устройства.........................................76
Имена устройств.....................................................78
Другая глобальная инициализация устройств...........................91
Общая картина.......................................................97
Проблемы совместимости с Windows 98/Ме.................................98
Различия в вызове DriverEntry.......................................98
DriverUnload .....................................................  98
Каталог \GLOBAL??...................................................98
Нереализованные типы устройств......................................98
Глава 3. Основные приемы программирования.................................99
Среда программирования в режиме ядра...................................99
Стандартные функции библиотеки времени выполнения...................101
Предупреждение о побочных эффектах..................................101
Обработка ошибок....................................................  102
Коды состояния.....................................................103
Структурированная обработка исключений..............................105
Фатальные сбои......................................................115
Управление памятью ................................................  .	116
Адресные пространства пользовательского режима и режима ядра........117
О размере страницы.................................................118
Выделение памяти в куче............................................124
Связанные списки...................................................130
Резервные списки...................................................136
Работа со строками..................................................  140
Другие методы программирования .......................................143
Работа с реестром..................................................143
Работа с файлами...................................................152
Вещественные вычисления............................................155
Упрощение отладки..................................................156
Проблемы совместимости с Windows 98/Ме................................161
Файловые операции ввода/вывода.....................................161
Вещественные вычисления............................................162
8
Содержание
Глава 4. Синхронизация..................................................163
Основные проблемы синхронизации......................................164
Уровень запроса прерываний (IRQL)....................................166
IRQL в действии...................................................169
IRQL и приоритеты потоков.........................................169
IRQL и перемещение памяти.........................................170
Косвенное управление IRQL.........................................171
Прямое управление IRQL............................................172
Спин-блокировки......................................................173
Несколько фактов о спин-блокировках...............................174
Работа со спин-блокировками ....................................  176
Спин-блокировки с очередями.......................................177
Синхронизационные объекты ядра.......................................178
Как и когда блокировать...........................................179
Ожидание одного объекта синхронизации ............................180
Ожидание нескольких объектов синхронизации........................183
События ядра......................................................184
Семафоры ядра.....................................................188
Мьютексы ядра.....................................................189
Таймеры ядра......................................................191
Использование потоков для синхронизации...........................197
Сигналы потоков и АРС........................................\	. 198
Другие синхронизационные примитивы ядра..............................201
Объекты быстрых мьютексов.........................................201
Атомарные вычисления .............................................204
Атомарная работа со списками......................................209
Проблемы совместимости с Windows 98/Ме...............................212
Глава 5. Пакеты запросов ввода/вывода ..................................213
Структуры данных.....................................................213
Структура IRP.....................................................213
Стек ввода/вывода.................................................217
«Стандартная модель» обработки IRP...................................219
Передача пакета диспетчерской функции.............................223
Обязанности диспетчерской функции.................................227
Функция Startlo...................................................234
Обработчик прерывания (ISR).......................................235
Функция DPC.......................................................236
Функции завершения...................................................236
Очереди запросов ввода/вывода........................................248
Объект DEVQUEUE...................................................252
Использование защищенных очередей.................................255
Отмена запросов ввода/вывода.........................................260
Если бы не многозадачность...........................................261
Синхронизация отмены..............................................261
Подробнее об отмене IRP...........................................262
Содержание
9
Как работает отмена в DEVQUEUE...................................263
Отмена пакетов IRP, созданных или обрабатываемых в вашем коде ..... 270
Обработка IRP_MJ_CLEANUP.........................................278
Зачистка с использованием DEVQUEUE...............................279
Зачистка в защищенных очередях ..................................281
Восемь сценариев обработки IRP......................................282
Сценарий 1 —Передача вниз с функцией завершения  ................282
Сценарий 2 — Передача вниз без функции завершения................283
Сценарий 3 — Завершение в диспетчерской функции..................284
Сценарий 4 — Постановка в очередь для последующей обработки......285
Сценарий 5 —Создание асинхронных IRP.............................287
Сценарий 6 — Создание синхронных IRP.............................289
Сценарий 7 — Синхронная передача вниз............................291
Сценарий 8 — Синхронная обработка асинхронных IRP................292
Глава 6. Поддержка Plug and Play для функциональных драйверов .
Диспетчерская функция IRP_MJ_PNP................
Запуск и остановка устройства...................
IRP_MN_START_DEVICE..........................
IRP_MN_STOP_DEVICE...........................
IRP_MN_REMOVE_DEVICE.........................
IRP_MN_SURPRISE_REMOVAL .....................
Управление переходами состояний РпР.............
Запуск устройства............................
Возможна ли остановка устройства?............
Во время остановки устройства................
Можно ли удалить устройство?.................
Синхронизация удаления.......................
Зачем нужна эта @#$! блокировка??............
Как DEVQUEUE работает с РпР..................
Другие конфигурационные функции.................
Фильтрация требований к ресурсам.............
Оповещения об использовании устройства..........
Оповещения РпР..................................
Оповещения служб Windows ХР.....................
Проблемы совместимости с Windows 98/Ме.............
Непредвиденное удаление.........................
Оповещения РпР..................................
Блокировка удаления ............................
Глава 7. Чтение и запись данных....................
Настройка конфигурации устройства...............
Адресация буфера данных.........................
Выбор метода буферизации.....................
Порты и регистры................................
Ресурсы портов..........................  .
Ресурсы памяти...............................
. 294
. 297 . 298 . 300 . 302 . 303 . 304 . 305 . 307 . 308 . 310 . 311 . 312 . 316 . 320 . 324 . 324 . 326 . 329 . 335 . 340 . 340 . 341 . 341
. 342
. 342 . 345 . 346 . 351 . 353 . 355
10
Содержание
Обработка прерываний.................................................356
Настройка прерывания .............................................  356
Обработка прерываний...............................................358
Отложенные вызовы процедур (DPC)...................................361
Простое устройство, управляемое прерываниями.......................365
DMA..................................................................372
Выполнение пересылки DMA...........................................376
Использование общего буфера........................................391
Простое устройство, управляющее шиной..............................394
Проблемы совместимости с Windows 98/Ме...............................396
Глава 8. Управление питанием.........................................  398
Модель управления питанием в WDM.....................................399
Роли драйверов WDM.................................................399
Питание устройств и состояния энергопотребления системы............400
Переходы между состояниями питания.................................401
Обработка запросов IRP_MJ_POWER....................................402
Управление переходами................................................406
Необходимая инфраструктура.........................................408
Исходное разделение запросов.......................................408
Системные IRP, повышающие энергопотребление........................409
Системные IRP, снижающие энергопотребление ........................417
IRP устройства.....................................................418
Другие аспекты управления питанием..................................  429
Флаги, устанавливаемые функцией AddDevice..........................429
Функция пробуждения устройства.....................................430
Отключение питания при бездействии.................................437
Оптимизация смены состояний........................................441
Проблемы совместимости с Windows 98/Ме...............................442
О важности DO_POWER_PAGABLE........................................442
Завершение IRP управления питанием.................................443
Запрос IRP устройств...............................................443
PoCallDriver.......................................................444
Глава 9. Управляющие операции ввода/вывода.............................445
Функция API DeviceloControl..........................................445
Синхронные и асинхронные вызовы DeviceloControl...................  447
Определение управляющих кодов ввода/вывода.........................449
Обработка IRP_MJ_DEVICE_CONTROL......................................450
METHODJ3UFFERED....................................................453
Методы DIRECT......................................................454
METHOD_NEITHER.....................................................455
Проектирование надежного и безопасного интерфейса IOCTL............457
Внутренние управляющие операции ввода/вывода.........................458
Оповещение приложений о событиях.....................................461
Применение общего события для оповещения...........................462
Применение приостановки IOCTL для оповещения......................463
Проблемы совместимости с Windows 98/Ме . ............................468
Содержание
11
Глава 10. WMI........................................................470
Основные концепции WMI............................................471
Пример схемы....................................................472
Соответствие между классами WMI и структурами С.................473
Драйверы WDM и WMI................................................474
Обработка IRP с использованием WMILIB...........................476
Расширенные возможности.........................................484
Проблемы совместимости с Windows 98/Ме............................494
Глава 11. Контроллеры и многофункциональные устройства...............495
Общая архитектура.................................................496
Объекты дочерних устройств......................................496
Обработка запросов РпР............................................498
Передача информации о дочерних устройствах РпР Manager......... 500
Обработка запросов РпР в роли PDO...............................501
Обработка удаления устройств....................................505
Обработка IRP_MN_QUERY_ID.......................................506
Обработка запросов IRPJ4N_QUERY_DEVICE_RELATIONS................507
Обработка запроса IRP_MN_QUERYJNTERFACE ........................508
Обработка запросов управления питанием............................512
Завершение......................................................513
Успех...........................................................513
Обработка запроса IRP_MN_WAIT_WAKE..............................514
Работа с ресурсами дочерних устройств.............................516
Глава 12. USB....................................................    517
Программная архитектура...........................................518
Иерархия устройств..............................................518
Высокоскоростные, полноскоростные и низкоскоростные устройства .... 519
Питание.........................................................520
Как организовано устройство?....................................521
Передача информации.............................................523
Упаковка информации.............................................525
Дескрипторы.....................................................535
Работа с драйвером шины...........................................543
Инициирование запросов..........................................543
Управление каналами массовой передачи...........................555
Управление прерывающими каналами................................563
Управляющие запросы.............................................564
Управление изохронными каналами ................................567
Управление питанием при бездействии для устройств USB...........583
Глава 13. Устройства взаимодействия с пользователем ......... 587
Драйверы HID-устройств............................................588
Отчеты и дескрипторы отчетов......................................588
Пример дескриптора клавиатуры...................................589
Дескриптор HIDFAKE..............................................592
12
Содержание
Минидрайверы HIDCLASS..................................................
DriverEntry..........................................................
Функции обратного вызова в драйверах.................................
Внутренний интерфейс IOCTL........................................602
Проблемы совместимости с Windows 98/Ме......................... ....	615
Обработка IRP_MN_QUERY_ID.........................................615
Джойстики.........................................................616
Глава 14. Специализированные темы......................................617
Журналы ошибок......................................................617
Создание пакета регистрации ошибок..................................619
Создание файла сообщений..........................................621
Системные потоки....................................................625
Создание и завершение системного потока...........................626
Опрос устройств в системном потоке................................628
Рабочие элементы....................................................631
Сторожевые таймеры..................................................633
Проблемы совместимости с Windows 98/Ме..............................637
Журналы ошибок....................................................637
Ожидание завершения системных потоков.............................637
Рабочие элементы..................................................637
Глава 15. Распространение драйверов устройств..........................639
Роль реестра .......................................................639
Раздел оборудования (экземпляра)..................................641
Раздел класса.....................................................643
Раздел драйвера.................................................  644
Раздел службы.....................................................645
Работа с реестром из программы....................................646
Свойства объекта устройства.......................................649
INF-файл............................................................650
Секции установки..................................................654
Секция Services.................................................  657
Заполнение реестра................................................658
Настройки безопасности............................................663
Строки и локализация..............................................664
Идентификаторы устройств..........................................665
Ранжирование драйверов ...........................................671
Инструменты для работы с INF-файлами..............................673
Определение класса устройств........................................675
Поставщик страниц свойств ........................................677
Настройка процесса установки........................................681
Основные и вспомогательные установочные DLL.......................682
Предварительная установка файлов драйверов........................688
Дополнительное программное обеспечение............................689
Программная установка драйвера ...................................690
Параметр RunOnce..................................................690
Запуск приложения.................................................691
Содержание
13
WHQL...............................................................692
Проведение тестов НСТ...........................................692
Передача пакета с драйвером.....................................699
Проблемы совместимости с Windows 98/Ме.............................705
Поставщики страниц свойств......................................705
Основные и вспомогательные установочные DLL.....................705
Предварительная установка пакетов драйверов.....................706
Цифровые подписи................................................706
Программная установка драйверов.................................706
CONFIGMGAPI.....................................................706
О несовместимости INF-файлов....................................706
Работа с реестром...............................................707
Получение свойств устройств.....................................708
Глава 16. Фильтрующие драйверы........................................709
Роль фильтрующего драйвера.........................................709
Верхние фильтрующие драйверы....................................709
Нижние фильтрующие драйверы.....................................714
Механика работы фильтрующего драйвера..............................714
Функция DriverEntry.............................................715
Функция AddDevice...............................................716
Функция DispatchAny.............................................718
Установка фильтрующего драйвера....................................722
Установка фильтра класса........................................723
Установка фильтра устройства с функциональным драйвером.........725
Реальные примеры ..................................................726
Нижний фильтр для отслеживания трафика..........................726
Именованные фильтры.............................................727
Фильтры шин.....................................................730
Фильтры мыши и клавиатуры.......................................731
Фильтрация для других устройств HID.............................733
Проблемы совместимости с Windows 98/Ме.............................734
Фильтры WDM для драйверов VxD...................................734
Сокращенная запись в INF-файле..................................735
Фильтрующие драйверы классов....................................735
Приложение А. Решение проблем несовместимости между платформами 736
Определение версии операционной системы..............................736
Динамическая компоновка..............................................737
Проверка совместимости платформ......................................738
Определение заглушек для функций режима ядра в Win98/Me..............740
Совместимость версий..............................................741
Функции-заглушки..................................................742
Использование WDMSTUB.............................................744
Взаимодействие между WDMSTUB и WDMCHECK...........................744
Специальное замечание о лицензировании............................745
14
Содержание
Приложение Б. Мастер WDMWIZ.AWX.....................................746
Основная информация о драйвере...................................746
Коды DeviceloControl.............................................749
Ресурсы ввода/вывода.............................................750
Конечные точки USB...............................................751
Поддержка WMI....................................................752
Параметры INF-файла..............................................754
Что дальше?......................................................755
Алфавитный указатель................................................756
Благодарности
В работе над книгой мне помогало множество людей. В самом начале проекта Энн Хэмилтон (Anne Hamilton), ответственный редактор Microsoft Press, решила, что требуется издание новой версии книги. Редактор Джулиана Алдус (Juliana Aldous) работала над проектом на всем пути его превращения в готовый продукт, который вы сейчас держите в руках. Под ее руководством работали Дик Браун (Dick Brown), Джим Фукс (Jim Fuchs), Шон Пек (Shawn Peck), Роб Нанс (Rob Nance), Салли Стикни (Sally Stickney), Пола Горелик (Paula Gorelick), Элизабет Хэнсфорд (Elizabeth Hansford) и Джули Кавабата (Julie Kawabata). Благодаря им текст в книге выверен с точки зрения орфографии, подписи к рисункам нормально читаются и понятны, а элементы алфавитного указателя соответствуют тексту.
Марк Рейниг (Marc Reinig) и доктор Лоуренс М. Шен (Lawrence М. Schoen) предоставили ценную помощь по лингвистическому и типографическому вопросам в разделе «Работа со строками» главы 3.
Майк Трикер (Mike Tricker) из компании Microsoft заслуживает особой благодарности за поддержку моего запроса на получение лицензии к исходным кодам — как, впрочем, и Брэд Карпентер (Brad Carpenter) за общую поддержку проекта пересмотра.
Эльяс Якуб (Eliyas Yakub) взял на себя все хлопоты по получению технических рецензий на содержимое книги и по упрощению доступа ко всем ресурсам Microsoft. Немало разработчиков и руководителей выкроили время из своих заполненных графиков и позаботились о том, чтобы материал книги был как можно более точным; перечислю лишь некоторых из них (в произвольном порядке) — Эдриан Уани (Adrian Oney) — нет, он мне не родственник, но проявляет особый интерес к книге с его фамилией на обложке; Аллен Маршалл (Allen Marshall), Скотт Джонсон (Scott Johnson), Мартин Борве (Martin Borve), Джин Валентайн (Jean Valentine), Дорон Холан (Doron Holan), Рэнди Олл (Randy Aull), Джейк Ошинс (Jake Oshins), Нейл Клифт (Neil Clift), Нараянан Ганапатхи (Narayanan Ganapathy), Фред Бесанья (Fred Bhesania), Гордан Лейси (Gordan Lacey), Алан Уорвик (Alan Warwick), Боб Фрут (Bob Fruth) и Скотт Херрболдт (Scott Herboldt).
Наконец, моя жена Марти обеспечивала поощрение и поддержку на протяжении всей работы над проектом.
Введение
В этой книге объясняется, как пишутся драйверы устройств для последних представителей семейства операционных систем Microsoft Windows на основе модели WDM (Windows Driver Model). Во введении я расскажу, для кого написана эта книга, как организован материал и как наиболее эффективно работать с книгой. Также здесь приводится информация об ошибках и о других ресурсах, которые могут вам пригодиться при изучении программирования драйверов. Немного забегая вперед, скажу, что в главе 1 описаны некоторые внутренние механизмы двух основных ветвей семейства Windows, а также в ней объясняется, что такое драйверы устройств WDM и их место в архитектуре Windows.
Для кого написана эта книга
Я писал книгу для опытных программистов, причем от читателя вовсе не требуются какие-либо познания в области написания драйверов устройств для операционной системы Windows. Книга написана для тех, кто хочет этому научиться. Чтобы успешно освоить программирование драйверов устройств, необходимо очень хорошо владеть языком программирования С, потому что драйверы WDM пишутся именно на этом языке. Также потребуется исключительное умение ориентироваться в неоднозначных ситуациях и проводить инженерный анализ компонентов операционной системы, потому что из-за неполной или неточной информации часто придется пользоваться методом проб и ошибок.
Написание драйвера WDM имеет много общего с написанием драйвера режима ядра для Windows NT 4.0. Задача немного упрощается тем, что вам не придется обнаруживать и настраивать оборудование. По иронии судьбы, правильно обращаться с Plug and Play и с управлением питанием оказывается дьявольски сложно. Если вы писали драйверы режима ядра для Windows NT, у вас не будет проблем с чтением этой книги. Заодно вам пригодятся готовые фрагменты кода, которые вы сможете вставлять в собственные программы в этих дьявольски сложных областях.
Написание драйвера WDM не имеет ничего общего с написанием драйверов виртуальных устройств (VxD) для системы Windows 3.0 и ее потомков, а также драйверов UNIX и драйверов реального режима для MS-DOS. Если весь ваш предыдущий опыт сосредоточен в этих областях, придется основательно потрудиться для изучения новой технологии. Тем не менее, я все же считаю, что
Структура книги
17
программировать драйверы WDM проще, чем остальные разновидности, потому что в этой области действует больше правил и вам приходится реже выбирать между малопонятными альтернативами. Конечно, чтобы извлечь пользу из этого факта, необходимо сначала узнать эти правила.
Если у вас уже имеется экземпляр первого издания книги и вы интересуетесь, стоит ли покупать обновленное издание, приведу немного информации, чтобы вам было проще решать. Количество изменений в области разработки драйверов для Windows ХР и Windows Me по сравнению с Windows 2000 и Windows 98 соответственно не так уж велико. Главной причиной для выпуска обновленного издания стало множество изменений, накопившихся на моей веб-странице обновлений/ошибок. Конечно, в этом издании рассматриваются некоторые новые «примочки», появившиеся в Windows ХР. Кроме того, здесь приводятся более четкие рекомендации по поводу написания мощных, безопасных драйверов. Наконец, говоря начистоту, некоторые вещи здесь попросту объясняются лучше, чем в первом издании.
Информация, содержащаяся в первой главе, пригодится руководителям проектов и всем, кому приходится планировать аппаратные проекты. Крайпе неприятно в самом конце цикла разработки нового устройства вспомнить, что для него потребуется драйвер. В отдельных случаях удается найти обобщенный драйвер, работающий с новым устройством. Тем не менее, чаще таких драйверов не существует, и их приходится писать самостоятельно. Надеюсь, в первой главе мне удастся убедить всех руководителей, что написать драйвер довольно трудно и приступать к решению этой задачи лучше раньше, чем позже. А когда вы закончите читать эту главу, отдайте книгу человеку, который будет стоять у штурвала... и купите еще несколько экземпляров (как говорил один мой друг из колледжа, лишних книг не бывает — их всегда можно подложить под ножки стульев в столовой).
Структура книги
После многолетнего проведения семинаров по программированию я начал понимать, что изучение чего-то нового может идти по принципиально разным путям. Одни ученики предпочитают получить солидную теоретическую основу, а затем изучать, как теория применяется к практическим проблемам. Другие предпочитают начать с практики и только потом переходить к общей теории. Я называю первый подход дедуктивным, а второй — индуктивным. Лично мне ближе индуктивный метод, поэтому структура материала в книге соответствует именно этому пути обучения.
Моя цель — научить читателя писать драйверы устройств. В общих чертах, я хочу заложить минимальную базу, необходимую для написания реального драйвера, а затем переходить к более специализированным темам. Однако «минимальная база» получилась довольно обширной — в книге она занимает целых семь глав. После главы 7 следует материал важный, но не всегда напрямую относящийся к задаче написания работоспособного драйвера.
18
Введение
Как уже упоминалось ранее, в главе 1 «В начале работы над проектом драйвера» описаны драйверы устройств WDM и их место в самой системе Windows. Попутно я расскажу о том, как возникло текущее состояние дел в операционных системах и технологиях драйверов. Также в этой главе объясняется, как выбрать нужную разновидность драйвера, приводятся общие сведения и список вопросов специально для руководителей проектов, а также обсуждаются вопросы совместим ости на двоичном уровне.
В главе 2 «Базовая структура драйвера WDM» рассматриваются базовые с труктуры данных, применяемые в Windows 2000 для управления устройствами ввода/вывода, и основные связи драйвера с этими структурами данных. Мы обсудим объект драйвера и объект устройства, а также поговорим о том, как написать две функции, DriverEntry и AddDevice, входящие в любой пакет' драйвера WDM.
В главе 3 «Основные приемы программирования» описаны важнейшие сервисные функции, вызываемые для выполнения повседневных программных задач. В частности, в этой главе рассматриваются обработка ошибок, управление памятью и другие задачи такого рода.
В главе 4 «Синхронизация» обсуждается тема синхронизации доступа драйвера к общим данным в многозадачном, многопроцессорном мире Windows ХР. Читатель получит подробную информацию об уровнях запросов прерываний (IRQL) и различных примитивах синхронизации, предоставляемых операционной системой.
Глава 5 «Пакеты запросов ввода/вывода» открывает тему программирования ввода/вывода — несомненно, главную тему книги. Я объясню, откуда поступают пакеты запросов ввода/вывода, и в общих чертах расскажу, что с ними делают драйверы в так называемой «стандартной модели» обработки IRP. Также мы обсудим запутанную тему очередей IRP и отмены, для которой особенно важно хорошее понимание проблем синхронизации.
Глава 6 «Поддержка Plug and Play для функциональных драйверов» посвящена всего одному типу пакетов ввода/вывода, а именно IRP_MJ_PNP. Компонент операционной системы Plug and Play Manager посылает этот пакет для передачи информации о конфигурации устройств и оповещении о важных событиях в их жизни.
Только в главе 7 «Чтение и запись данных» мы наконец-то доберемся до написания кода драйвера, выполняющего операции ввода/вывода. Читатель узнает, как получить информацию о конфигурации от РпР Manager и как на основании этой информации подготовить драйвер к «содержательным» пакетам IRP, осуществляющим чтение и запись данных. Я представлю два простых примера драйверов: для устройства РЮ и для устройства DMA, управляющего передачей данных по шине.
В главе 8 «Управление питанием» описано участие драйвера в схеме управления энергопотреблением. Вероятно, вы, как и я, решите, что тема управления питанием чересчур усложнена. К сожалению, ваш драйвер должен участвовать в системных протоколах управления питанием, иначе он нарушит устойчивость
Безопасность и надежность драйверов
19
работы системы в целом. К счастью, в сообществе программирования драйверов уже сформировалась хорошая традиция копирования вставки, которая вас спасет.
Глава 9 «Управляющие операции ввода/вывода» содержит описание важного механизма «внеполосного» взаимодействия приложений и других драйверов с вашим драйвером.
Глава 10 «WMI» посвящена схемам управления компьютерами масштаба предприятия, в которых может (и должен) участвовать ваш драйвер. Я объясню, как предоставить данные по статистике и производительности для приложений-мониторов, как следует реагировать на стандартное управление WMI и как оповещать управляющие приложения о происходящих важных событиях.
Б главе И «Контроллеры и многофункциональные устройства» речь пойдет о том, как написать драйвер для устройства, совмещающего несколько разных функций или несколько экземпляров одной функции.
В главе 12 «USB» рассматривается написание драйверов для устройств USB.
Глава 13 «Устройства взаимодействия с пользователем» объясняет, как пишутся драйверы для этого важного класса устройств (клавиатуры, мыши, джойстики и т. д.).
В главе 14 «Специализированные темы» описаны системные программные потоки, рабочие элементы, ведение протоколов ошибок и другие специализированные темы.
Глава 15 «Распространение драйверов устройств» рассказывает, как организовать установку драйвера в системе конечного пользователя. Читатель познакомится с основами написания INF-файлов, управляющих процессом установки, а также узнает ряд интересных и полезных манипуляций с системным реестром.
В главе 16 «Фильтрующие драйверы» обсуждается, в каких ситуациях уместно применять фильтрующие драйверы, как происходят их сборка и установка.
В приложении А «Решение проблем несовместимости между платформами» объясняется, как определить, под управлением какой операционной системы работает компьютер, и как построить драйвер, совместимый на двоичном уровне.
В приложении Б «Мастер WDMWIZ.AWX» описана процедура использования мастера Visual C++, написанного мной для построения драйверов. WDMWIZ.AWX не претендует на роль заменителя коммерческих пакетов. В частности, это означает, что он не так уж прост в использовании и без чтения документации вам не обойтись.
Безопасность и надежность драйверов
Мы отвечаем за безопасность и надежность своих программ. Тс из нас, кто занимается разработкой драйверов, несут особую ответственность, потому что наш код работает в доверенном пространстве ядра. Когда в коде драйвера происходит сбой, он обычно парализует работу всей системы. Если в коде драйвера остается лазейка, хакер может проникнуть через нее и взять под контроль всю
20
Введение
систему, а возможно, и все обслуживаемое ею предприятие. Всем нам следует воспринимать эту проблему серьезно, иначе это может обернуться вполне реальными экономическими и физическими потерями.
Поскольку эта тема играет очень важную роль в программировании драйве-=| ров, в этом издании места, особенно важные для безопасности и надежности драйверов, будут помечаться специальным значком.
Компонент операционной системы Driver Verifier выполняет различные проверки драйверов — если его об этом попросить. WHQL (Windows Hardware Quality Laboratory) запускает ваш драйвер со всевозможными активными тестами Driver Verifier, но вы можете перехитрить ее и запустить Driver Verifier сразу же после того, как драйвер обретет минимальную функциональность. При lJ обсуждении того, чем Driver Verifier может помочь в отладке драйвера, я буду использовать этот значок.
Файлы примеров
Файлы примеров для книги размещены на сайте Microsoft Press по адресу http://www.microsoft.com/mspress/books/6262.asp. Ссылка Companion Content открывает страницу загрузки примеров. Кроме того, файлы также имеются на компакт-диске.
К книге прилагается огромное количество примеров драйверов и тестовых программ. Работая над каждым примером, я стремился продемонстрировать конкретный аспект или прием, упоминаемый в тексте. Таким образом, примеры получились «игрушечными», и их нельзя перепродать, изменив несколько строк кода. Я намеренно выбрал этот подход. За многие годы я заметил, что авторы книг по программированию склонны создавать примеры, демонстрирующие их умение справляться с нарастающими сложностями, а не примеры, которые учат новичков решать простейшие проблемы. С вами я так не поступлю. В главах 7 и 12 встречаются примеры драйверов, работающие с «настоящим» оборудованием, а именно с макетными платами от производителей чипсетов PCI и USB. Все остальные драйверы предназначены для несуществующего оборудования.
Практически в каждом случае я строил простую тестовую программу пользовательского режима, позволяющую поэкспериментировать с работой драйвера. Тестовые программы получились совсем крошечными: они состоят из нескольких строк кода и демонстрируют одно конкретное обстоятельство, для которого создавался этот пример драйвера. И снова я подумал, что будет лучше предоставить вам простой способ поэкспериментировать с кодом драйвера, вместо того чтобы демонстрировать все известные мне фокусы программирования в MFC.
Вы можете использовать все примеры кода в книге в своих проектах, не выплачивая мне или кому-либо другому авторского вознаграждения (конечно, при этом вы обязаны ознакомиться с подробным лицензионным соглашением
Файлы примеров
21
в конце книги — предыдущая фраза никоим образом не отменяет соглашения). Пожалуйста, не распространяйте среди своих клиентов GENERIC.SYS или драйвер, вызывающий функции из GENERIC.SYS. Справочный файл GENERIC.CHM в прилагаемых материалах содержит инструкции по поводу того, как переименовать GENERIC во что-нибудь менее... общее. Предполагается, что читатели будут распространять модули WDMSTUB.SYS и AutoLaunch.exe, но перед этим я попрошу выполнить лицензионное соглашение без отчислений с продаж. Просто напишите мне по адресу walteroney@oneysoft.com, и я объясню, что нужно делать. В сущности, лицензионное соглашение обязывает вас поставлять только новейшую версию этих компонентов вместе с установочной программой, которая предотвратит использование устаревших версий на компьютере конечного пользователя.
О компакт-диске
Компакт-диск, прилагаемый к книге, содержит полный исходный код и исполняемые файлы всех примеров. Чтобы получить доступ к этим файлам, вставьте диск в дисковод CD-ROM вашего компьютера и выберите нужный пункт в появившемся меню. Если функция автозапуска отключена в вашей системе (если после вставки диска меню не появляется на экране), запустите файл StartCD.exe из корневой папки диска. Для установки файлов примеров на жесткий диск потребуется примерно 50 Мбайт дискового пространства.
На компакт-диске также находятся некоторые вспомогательные программы, которые могут пригодиться в вашей работе. Откройте файл WDMBOOK.HTM в браузере — вы найдете в нем перечень примеров и объяснения по поводу использования этих утилит.
Программа установки дает возможность установить все примеры на жесткий диск либо оставить их на компакт-диске. Тем не менее, она не будет устанавливать какие-либо компоненты уровня ядра в вашей системе. Программа спросит вашего разрешения на создание переменных окружения (environment) в системе. Эти переменные используются при построении примеров, а их значения вступают в силу немедленно в Windows ХР или при следующей загрузке системы в Windows 98/Ме.
Если на компьютере установлены как Windows ХР, так и Windows 98/Ме, я рекомендую выполнить полную установку в обеих системах, чтобы в них были внесены необходимые изменения в реестр и настроено окружение. Запустите программу установки из созданного каталога примеров, чтобы избежать лишнего копирования. Указывать разные приемные каталоги для двух установок не обязательно (да и нежелательно).
К каждому примеру прилагается HTML-файл, в котором (очень кратко) объясняется, что делает этот пример, как его построить и тестировать. Я рекомендую читать эти файлы перед тем, как пытаться устанавливать примеры, потому что установка некоторых из них сопровождается специфическими требованиями. После установки драйвера в Диспетчере устройств появляется новая
22
Введение
страница свойств; на ней имеется кнопка для просмотра того же HTML-файла, как показано на следующем рисунке.
II1SI
Genera! Sample Information Driver Resource-
Fake PIO Sample
T hi?- device is one of the sample programs accompanying "Programming the Microsoft Windows Driver Model' by Walter Oney (Mscrosoft Press 2d ed.
Additional infomatron on usage or testing may be available by pressing the "More Info" button below
h Morejnfo.
OK
Cancel
Как создавались примеры
Мои примеры драйверов выглядят так, словно они создавались по одному шаблону, и на то есть веская причина: так оно и было. Когда мне потребовалось создать столько похожих примеров, я решил написать пользовательского мастера (wizard). Функциональность мастеров в Microsoft Visual C++ версии 6.0 практически готова к построению проекта драйвера WDM, поэтому я решил ею воспользоваться. Мастер называется WDMWIZ.AWX; файл находится в прилагаемых материалах. Описание работы с мастером приводится в приложении Б. При желании вы можете использовать его для конструирования заготовок своих собственных драйверов. Однако помните, что мастер не соответствует стандартам коммерческого продукта — он всего лишь помогает научиться писать драйверы и нс претендует па конкуренцию с коммерческими пакетами (и тем более пе может их заменить). Также следует учитывать, что некоторые параметры проекта приходится менять вручную, потому мастер делает только почти все необходимое. За дополнительной информацией обращайтесь к файлу WDMBOOK.HTM, расположенному в корневом каталоге прилагаемого компакт-диска.
Построение примеров
На мой взгляд, для работы над проектами драйверов интец)ированная среда разработки Microsoft Visual Studio 6.0 значительно превосходит все остальные среды. Если вы разделяете мое предпочтение, вам будет проще работать с примерами.
GENERIC.SYS
23
В файле WDMBOOK.HTM из прилагаемых материалов содержатся подробные инструкции ио поводу настройки среды. Я намеренно не повторяю их в книге, потому что в будущем они могуч измениться. К каждому примеру также прилагается стандартный файл SOURCES, предназначенный для среды DDK (Driver Development Kit), если вы предпочитаете этот вариант.
Обновления примеров
На моем веб-сайте http://www.oneysoft.com имеется страница с обновлениями примеров. За три года, прошедшие с момента публикации первого издания, я выпустил более дюжины обновлений, содержащих исправления ошибок и новые примеры. Если вы установите мои примеры, я рекомендую также устанавливать все новые обновления сразу же после их выхода.
Чтобы получать оповещения о выходе новых обновлений, заполните простую форму для включения в список рассылки. Кстати говоря, подписчикам первого издания перерегистрироваться не нужно: они включаются в список по умолчанию.
GENERIC.SYS
Драйвер WDM содержит большой объем стандартного кода, обеспечивающего поддержку Plug and Play и управления питанием. Этот код длинен. Он скучен. В нем ле!ко ошибиться. Во всех моих примерах используется DLL-библиотека режима ядра с именем GENERIC.SYS. Проекты, построенные мастером WDMWIZ.AWX, могут использовать GENERIC.SYS, но могут и не использовать - все зависит от вашего решения. В файле GENERIC.CHM, находящемся в прилагаемых материалах, перечислены вспомогательные функции, экспортируемые GENERIC.SYS (на тот случай, если вы захотите использовать их самостоятельно).
Впрочем, у использования библиотеки GENERIC.SYS есть и оборотная сторона: библиотека затрудняет понимание некоторых критических вещей, происходящих в драйвере. Все драйверы, использующие библиотеку GENERIC, перепоручают ей всю обработку IRP_MJ_PNP (см. главу 6) и IRP_MJ_POWER (см. главу 8), а последняя осуществляет обратный вызов функций драйверов для обработки деталей. В следующей таблице перечислены важнейшие функции обратного вызова.
Тип IRP	Функция обратного вызова	Назначение
IRP_MJ_PNP	StartDevice	Инициализация устройства (отображение регистров памяти, подключение прерываний и т. д.)
	Stop Device	Остановка устройства и освобождение ресурсов ввода/вывода (отключение отображения регистров памяти, отключение прерываний и т. д.)
	RemoveDevice	Отмена действий, выполняемых в AddDevice (отсоединение от объекта устройства нижнего уровня, удаление объекта устройства и т. д.)
продолжение
24
Введение
(продолжение)		
Тип IRP	Функция обратного вызова	Назначение
	OkayToStop	(He обязательно) Проверка возможности остановки устройства (используется при обработке IRP_MN_ QUERY_STOP_PROCESSING)
	OkayToRemove	(Не обязательно) Проверка возможности извлечения устройства (используется при обработке IRP_MN_ QUERY.REMOVE.DEVICE)
	FlushPendinglo	(Не обязательно) Выполнение всех действий, необходимых для принудительного завершения незавершенных операций в ближайшем будущем
IRP_MJ_POWER	QueryPower	(Не обязательно) Проверка допустимости предложенных изменений в питании устройства (используется при обработке IRP_MN_QUERY_POWER)
	SaveDeviceContext	(Не обязательно) Сохранение контекста устройства, который будет потерян в период низкого энергопотребления
	RestoreDeviceContext	(Не обязательно) Восстановление контекста устройства после периода низкого энергопотребления
	GetDevicePowerState	Получение информации о питании устройства для заданного состояния энергопотребления системы
Системные требования
Для запуска программ с прилагаемого компакт-диска потребуется компьютер с Windows 98 Second Edition, Windows Me, Windows 2000, Windows ХР или более поздней версией Windows. Некоторые примеры требуют наличия порта USB и пакета разработки EZ-USB от Cypress Semiconductors. Для двух примеров потребуются слот расширения ISA и макетная плата S5933-DK (или ее аналог) от Applied Micro Circuits Corporation.
Для построения примеров также необходим определенный набор программ, который может изменяться со временем в результате выпуска обновлений. Файл WDMBOOK.HTM с перечнем требований обновляется по мере изменения требований. На момент публикации книги вам потребуется следующее:
О Microsoft Windows .NET DDK;
О Microsoft Visual Studio 6.0. Подойдет любое издание; неважно, устанавливали ли вы какие-либо обновления. При построении примеров из Visual Studio будет использоваться только интегрированная среда разработки. Компилятор и другие служебные программы сборки беру гея из DDK;
О только для одного примера (PNPMON) - Windows 98 DDK.
Если сборка и тестирование осуществляются только в среде Windows 98 или Windows Me, вам также потребуется копия Windows DDK для платформы, пред
От издательства
25
шествующей .NET. Компания Microsoft не разрешила мне распространять версию компилятора ресурсов, работающего в Windows 98/Windows Me, и кроссплат-форменную версию USBD.LIB. Достаньте их, где сможете найти, пока Microsoft не прекратила поддержку более ранних версий DDK. Помните, что драйверы, построенные в Windows 98/Ме, могут не работать в Windows 2000 и более поздних платформах из-за ошибки вычисления контрольной суммы во вспомогательной DLL-библиотеке.
Об ошибках
Несмотря на героическое внимание к деталям, проявленное мною и редакторами из Microsoft Press, некоторые ошибки все же просочились из рукописи в первое издание книги. Я просмотрел несколько технических моментов, споткнулся на других и узнал о третьих уже тогда, когда книга находилась в печати. Как бы то ни было, веб-страница ошибок/обновлений разрослась почти до 30 печатных страниц. Мое желание «обнулить счетчики» стало одной из главных причин для выхода обновленного издания.
Увы, и это издание наверняка не обойдется без ошибок и обновлений. Я буду продолжать их публикацию на сайте http://www.oneysoft.com в течение по крайней мере ближайшей пары лет. Я рекомендую почаще заглядывать на сайт, чтобы быть в курсе дел. И пожалуйста, присылайте свои вопрос ы и комментарии, чтобы я мог исправить как можно больше ошибок.
Другие ресурсы
Эта книга не должна быть единс твенным ис точником информации, используемым для изучения программирования драйверов. В ней особо выделены те аспекты, которые кажутся мне важным, однако вам может потребоваться информация, отсутствующая в книге; а может быть, вы предпочитаете другую методику обучения. Общие' принципы работы операционной системы рассматриваются лишь в том объеме, который, на мой взгляд, необходим для эффективного программирования драйверов. Если вы принадлежите к числу сторонников дедуктивного метода или просто хотите получить больше теоретической информации, обращайтесь к дополнительным ресурсам.
От издательства
Ваши замечания, предложения, вопросы отправляйте по адресу электронной почты comp@piter.com (издательство «Питер», редакция технической литературы).
Мы будем рады узнать ваше мнение!
На веб-сайте издательства http://www.piter.com вы найдете подробную информацию о наших книгах.
1В начале работы над проектом драйвера
В этой главе в общих чертах описан процесс создания драйвера. Мой опыт работы с персональными компьютерами начался с середины 1980-х годов, когда IBM выпустила свой персональный компьютер (PC) с операционной системой MS-DOS. Последствия многих решений, принятых IBM и Microsoft в те времена, продолжают ощущаться до сих пор. Соответственно, небольшая историческая справка поможет вам лучше понять, как программируются драйверы устройств.
Драйверы WDM (Windows Driver Model) работают в двух принципиально разных линейках операционных сред; в этой главе приводится общий обзор архитектуры этих сред. Windows ХР, как и Windows 2000 и более ранние версии Windows NT, обеспечивает формальную структуру, в которой драйверы играют четко определенные роли по выполнению операций ввода/вывода по поручению других драйверов и приложений. В Windows Me, как и в предшествующих операционных системах Windows 9х и Windows 3.x, используется более свободная архитектура, в которой драйверы играют несколько ролей.
Работа над любым проектом драйвера начинается с выбора типа драйвера (если вам действительно необходимо его писать). В этой главе будут описаны различные классы устройств, причем особое внимание будет уделено информации, которая поможет вам в принятии этого решения.
В завершение главы приводится список, который поможет лучше представить основные этапы работы над проектом.
Краткая история драйверов устройств
Первые модели PC работали на процессорных чипах Intel, способных адресовать до 640 Кбайт «реальной» памяти, называемой так потому, что она существовала в виде настоящих чипов памяти, напрямую адресуемых процессором с использованием 20-разрядного физического адреса. Сам процессор работал только в реальном режиме, в котором процессор объединяет информацию из двух 16-разрядных регистров и формирует 20-разрядный адрес для каждой команды, содержащей ссылку на память. В архитектуре компьютера были предусмотрены слоты расширения, в которых отважные пользователи могли устанавливать карты, приобретенные отдельно от самого компьютера. Сами карты обычно снабжались
Краткая история драйверов устройств
27
инструкциями относительно того, как следует настраивать DIP-переключатели (позднее — перемычки) для внесения изменений в конфигурацию ввода/вывода. Чтобы предотвратить конфликты ресурсов, пользователю приходилось хранить список всех назначений портов ввода/вывода и прерываний. В MS-DOS использовалась схема, основанная на файле CONFIG.SYS и позволявшая операционной системе загружать драйверы реального режима для исходного оборудования и карт расширения. Все драйверы писались исключительно на ассемблере и в большей или меньшей степени зависели от команды INT для взаимодействия с BIOS и системными функциями самой MS-DOS. Прикладные программисты волей-неволей были вынуждены учиться программировать операции с видеоадаптером, клавиатурой и мышью напрямую, потому что поддержка этих устройств в MS-DOS и BIOS оставляла желать лучшего.
Позднее компания IBM выпустила АТ — новый класс персональных компьютеров на базе процессора Intel 80286. В процессоре 286 появился защищенный режим работы, в котором программы могли адресовать до 16 Мбайт основной и дополнительной (extended) памяти с использованием 24-разрядного адреса сегмента (задаваемого косвенно селектором сегмента, хранящимся в 16-разряд-ном сегментном регистре) и 16-разрядного смещения. Сама по себе MS-DOS оставалась операционной системой реального режима, поэтому некоторые производители программного обеспечения создали расширители DOS (DOS extenders) — продукты, позволявшие программистам перевести приложения реального режима в защищенный режим и получить доступ ко всей памяти, постепенно появляющейся на рынке. Поскольку компьютером по-прежнему управляла MS-DOS, технология драйверов на этой стадии не развивалась.
Радикальные изменения в технологии PC (по крайней мере, на мой взгляд) произошли тогда, когда компания Intel выпустила процессорный чип 80386. Он позволял программам использовать до 4 Гбайт виртуальной памяти при помощи таблицы страниц, а также легко работать с 32-разрядными числами при математических вычислениях и адресации. На рынке производителей программного обеспечения произошел всплеск активности — разработчики компиляторов и расширителей DOS торопились удовлетворить спрос постоянно расширяющегося круга больших приложений, предъявлявших высокие требования к памяти и скорости процессора. Драйверы устройств оставались 16-разрядными программами реального режима, написанными на ассемблере и установленными через CONFIG.SYS, а пользователям по-прежнему приходилось настраивать карты вручную.
Последующие достижения в технологиях процессорных чинов относились в основном к быстродействию и объемам памяти. На момент написания этой главы компьютеры с тактовой частотой более 1 ГГц, 50-60 гигабайтным жестким диском и 512 (и более) Мбайт памяти стали вполне обыденным товаром, который может себе позволить рядовой потребитель.
Параллельно с эволюцией платформы происходила революция в области технологии операционных систем. Большинство пользователей, и даже разработчики системных программ, предпочитают работать на компьютере в графическом, а не в символьном режиме. Компания Microsoft запоздала с приходом
28
Глава 1. В начале работы над проектом драйвера
в сектор графических операционных систем (ее опередила компания Apple со своим первым Macintosh), однако ей удалось завоевать этот рынок своими операционными системами семейства Windows. Поначалу Windows была обычной графической оболочкой для MS-DOS реального режима. Со временем появился набор Windows-драйверов для основного оборудования, включая экран, клавиатуру и мышь. Драйверы представляли собой исполняемые файлы с расширением .DRV и писались в основном на ассемблере.
С появлением компьютеров класса АТ компания Microsoft разработала версию Windows для защищенного режима. Драйверы реального режима .DRV также были переработаны в защищенный режим. Однако все оборудование, кроме стандартных устройств Windows (экран, клавиатура, мышь), продолжало обслуживаться драйверами MS-DOS реального режима.
Наконец, спустя некоторое время после широкого распространения PC с процессорами 386 была выпущена Windows 3.0. «Расширенный» режим работы этой системы в полной мере использовал возможности виртуальной памяти. Впрочем, даже после этого каждому новому устройству требовался драйвер реального режима. Но теперь возникла большая проблема: для поддержки многозадачности приложений MS-DOS (что было необходимо для принятия Windows конечными пользователями) Microsoft заложила в операционную систему концепцию виртуальной машины. Каждое приложение MS-DOS (а также сама графическая среда Windows) работало в отдельной виртуальной машине. Однако приложения MS-DOS пытались напрямую работать с оборудованием, выдавая команды IN и OUT, осуществляя чтение и запись в памяти устройств и обрабатывая прерывания. Более того, два и более приложения, поочередно использующие процессорное время, могли отдавать оборудованию несогласованные приказы. Конечно, это должно было неизбежно привести к конфликтам использования экрана, клавиатуры и мыши.
Чтобы разные приложения могли совместно работать с физическим оборудованием, компания Microsoft ввела концепцию драйвера виртуального устройства, основной функцией которого была «виртуализация» оборудования. Такие драйверы обычно сокращенно назывались VxD, потому что имена большинства из них строились по шаблону VxD.386, где х — тип устройства, которым они управляли. При помощи этой концепции Windows 3.0 создавала впечатление, что каждая виртуальная машина оснащена собственным набором экземпляров многих аппаратных устройств. Но сами устройства в большинстве случаев продолжали обслуживаться драйверами реального режима MS-DOS. Драйверы VxD выполняли функции посредника для работы приложения с оборудованием: для этого они сначала перехватывали обращения приложения к оборудованию и на короткое время переключали процессы в особую разновидность реального режима — режим виртуального 8086 — для выполнения кода драйвера MS-DOS.
Откровенно говоря, переключение режимов для запуска драйверов реального режима было трюком, у которого было только одно преимущество: он обеспечивал относительно плавный рост аппаратной платформы и операционной системы. Windows 3.0 содержала много ошибок, истинной причиной которых была имен
Краткая история драйверов устройств
29
но эта особенность архитектуры. Ответом Microsoft должна была стать система OS/2, которую опа разрабатывала в гармонии (в понимании этого слова, принятом в XX веке) с IBM.
Версия OS 2 от Microsoft превратилась в Windows NT, первый выпуск которой состоялся в начале 1990-х, вскоре после выхода Windows 3.1. Microsoft изначально строила Windows NT с намерением превратить ее в падежную и безопасную платформу для работы Windows-приложений. Драйверы для Windows NT использовали принципиально новую технологию режима ядра, которая не имела практически ничего общего с двумя другими драйверными технологиями, модными на тот день. Драйверы Windows NT программировались почти исключительно на языке С, чтобы их можно было компилировать для новых процессорных архитектур без изменения исходных кодов.
Во времена Windows 3.0 произошло еще одно событие, которое имеет для нас сегодня важные последствия. Windows 3.0 формально разделила мир программного обеспечения на программы пользовательского режима и режима ядра. К первой категории относятся все приложения и игры, ради которых люди и покупают компьютеры; однако нельзя быть уверенным в том, что такие программы будут надежно (или хотя бы добросовестно) работать с оборудованием и другими программами. К категории режима ядра относятся сама операционная система и все драйверы устройств, написанные такими людьми, как вы и я. Программы режима ядра пользуются полным доверием и могут работать с любыми системными ресурсами так, как считают нужным. Хотя Windows 3.0 разделила программы по режиму работы, ни одна версия Windows этой линейки (даже Windows Me) не реализовала защиту памяти с целью создания защищенной системы. Безопасность относится к сфере деятельности Windows NT и ее потомков; в этих системах программам пользовательского режима запрещено получение информации и изменение ресурсов, находящихся под управлением и в ведении ядра.
На самом деле вычислительная мощность оборудования лишь недавно достигла той точки, когда Windows NT может нормально работать на среднем PC. Из-за этого Microsoft приходилось поддерживать предыдущую линейку Windows в жизнеспособном состоянии. Windows 3.0 выросла в 3.1, 3.11 и 95. Начиная с Windows 95, если вам требовалось написать драйвер устройства, вы писали нечто под названием VxD; это «нечто» в дейс твительности представляло собой 32-разрядный драйвер защищенного режима. Кроме того, начиная с Windows 95 пользователи могли выкинуть распечатки портов ввода/вывода, ведь новая функциональность Plug and Play операционной системы в определенной степени автоматизировала идентификацию и настройку оборудования. Впрочем, производителям оборудования все же приходилось писать драйверы реального режима для покупателей, продолжавших использовать Windows 3.1. Тем временем Windows NT развивалась до версий 3.5 и 4.0. Для поддержки этих систем требовался третий драйвер, причем опыт программирования старых драйверов никак нс помогал в новых проектах.
30
Глава 1. В начале работы над проектом драйвера
Дальше так продолжаться нс мог ю. Компания Microsoft разработала новую технологию драйверов устройств — WDM (Windows Driver Model) — и включила ее в Windows 98 и Windows Me наследников Windows 95, Кроме того, новая технология была включена в Windows 2000 и Windows ХР, наследников Windows NT 4.0. К моменту выхода Windows Me поддержка MS-DOS оставалась чистой формальностью; производители оборудования наконец-то были избавлены от хлопот с драйверами реального режима. Поскольку технология WDM (по крайней мере, в исходном проекте) работала практически одинаково на всех платформах, было достаточно написагь всего один драйвер.
Подводя итог, можно сказать, что мы и сегодня продолжаем пребывать в тени исходной архитектуры PC и первых версий MS-DOS. Конечным пользователям все еще приходится снимать кожух с компьюгера, чюбы устанавливать карты расширения, но в наши дни используются другие, более мощные технологии, чем прежде. Plug and Play и PCI (Peripheral Component Interconnect) в значительной степени избавили пользователей о необходимости следить за вводом/ выводом, памятью и использованием запросов на прерывания. BIOS еще существует, но в наши дни ее версия сводится в основном к загрузке и передаче операционной системе (Windows ХР или Windows Me) конфигурационных данных. А файлы драйверов WDM по-прежнему снабжаются расширением .SYS, как и первые драйверы реального режима
Обзор операционных систем
WDM обеспечивает инфраструктуру для драйверов устройств, работающих в двух операционных системах: Windows 98/Windows Me и Windows 2000/Windows ХР. Как упоминалось в предыдущей исторической справке, эти две пары операционных систем представляют две линейки параллельной эволюции. В дальнейшем я буду обозначать первую пару систем сокращением «98/Ме», чтобы подчеркнуть их общее происхождение, а вторую называть просто «ХР». С точки зрения конечного пользователя, эти две пары систем похожи, но их внутренние механизмы сильно различаются. В этом разделе приводится краткий обзор двух линеек.
Обзор архитектуры Windows ХР
На рис. 1.1 представлена сильно упрощенная функциональная схема операционной системы Windows ХР. На схеме особо выделены аспекты, важные при программировании драйверов устройств. Все платформы, на которых работает Windows ХР, поддерживают два режима выполнения. Программы работают либо в пользовательском режиме, либо в режиме ядра Если, допустим, программе пользовательского режима потребовалось прочитать данные с устройства, она вызывает функцию API (Application Programming Interface) ReadFile. Модуль подсистемы - такой как KERNEL32.DLL — реализует вызов, передавая его к низкоуровневой функции API NtReadFile. Дополнительная информация о низкоуровневом API приводится далее во врезке.
Обзор операционных систем
31
Рис. 1.1. Архитектура Windows ХР
Часто говорят, что NtReadFile является частью системного компонента, называемого администратором1 ввода/вывода (I/O Manager). Возможно, термин «администратор ввода/вывода» выбран не совсем удачно, потому что в системе не существует конкретного исполняемого модуля с таким именем. Тем не менее, нам потребуется какое-то название для обозначения «облака» функций операционной системы, окружающего наш собственный драйвер; на практике обычно применяется именно этот термин.
Многие функции выполняют ту же задачу, что и NtReadFile, — они работают в режиме ядра и обслуживают запросы приложений на обращение к устройству. Все они проверяют свои параметры, предотвращая возможные дефекты безопасности при выполнении операций или обращении к данным, недоступным для программ пользовательского режима. Затем они создают структуру данных, называемую пакетом запроса ввода-вывода, пли IRP (I/O Request Packet), и передают ее в точке входа некоторому драйверу устройства. В рассматриваемом примере с ReadFile функция NtReadFile создает IRP с основным кодом функции IRP_MJ_READ (константа, определяемая в заголовочном файле DDK (Driver Development Kit)). Дальнейшее может различаться в деталях, но чаще всего такие функции, как NtReadFile, возвращают управление вызывающей стороне пользовательского режима, указывая при этом, что операция, описанная IRP, еще не
1 Обычно мы называем manager диспетчером, но в тексте также встречается термин dispatch. - Примеч. перев.
32
Глава 1. В начале работы над проектом драйвера
закончена. Программа пользовательского режима может продолжить работу, а затем дождаться завершения операции либо же перейти в режим ожидания немедленно. Как бы то ни было, при обслуживании запросов драйвер устройства работает независимо от приложения.
НИЗКОУРОВНЕВЫЙ API--------------------------------------------------------------
Функция NtReadFile является частью так называемого низкоуровневого API системы Windows ХР. Существование низкоуровневого API объясняется в основном историческими причинами. Исходная операционная система Windows NT содержала ряд подсистем, реализующих семантику нескольких новых и существующих операционных систем: в ней была подсистема OS/2, подсистема POSIX и подсистема Win32. Реализация подсистем была основана на обращениях из пользовательского режима к низкоуровневому API, который сам по себе был реализован в режиме ядра.
Библиотека пользовательского режима NTDLL.DLL (на мой взгляд, имя содержит избыточную информацию) реализует низкоуровневый API для вызывающей стороны Win32. Каждая точка входа в этой библиотеке представляет собой тонкую «обертку» для функции режима ядра, фактически выполняющей эту функцию. Вызов использует платформенно-зависимый интерфейс системных функций для передачи управления через границу пользовательского режима/режима ядра. На новых процессорах Intel интерфейс системных функций использует команду SYSENTER. На старых процессорах Intel он использовал команду INT с кодом функции 0х2Е. На других процессорах действуют иные механизмы. Впрочем, для написания драйверов не нужно понимать все тонкости реализации. Достаточно понять, что этот механизм позволяет программе, работающей в пользовательском режиме, вызывать функции, находящиеся в режиме ядра, и эти функции в конечном счете возвращают управление на сторону пользовательского режима. При этом переключение контекста в программных потоках не производится: изменяется только уровень привилегий исполняемого кода (и еще некоторые детали, существенные только для программистов, работающих на ассемблере). Большинство прикладных программистов имеют дело с подсистемой Win32, потому что именно в ней реализованы функции, чаще всего ассоциируемые с графическим интерфейсом Windows. Другие подсистемы со временем потеряли актуальность. Тем не менее, низкоуровневый API остается, и подсистема Win32 все еще зависит от него, как будет показано в приведенном примере.
Возможно, для выполнения запроса IRP драйверу в конечном счете потребуется обратиться к своему устройству. В случае с запросом IRP_MJ__READ к устройству РЮ такое обращение может принять форму операции чтения к порту ввода/вывода или регистру памяти устройства. Драйверы, хотя они и работают в режиме ядра и могут напрямую общаться с оборудованием, используют средства прослойки HAL (Hardware Abstraction Layer) для обращения к оборудованию. Возможно, операция чтения потребует вызова READ_PORT_CHAR для получения одного байта данных из порта ввода/вывода. Функции HAL используют платформенно-зависимую реализацию для выполнения операций. На компьютерах х86 HAL применяет команду IN; возможно, в будущих платформах Windows вместо нее будет применяться выборка содержимого памяти.
Закончив операцию ввода/вывода, драйвер завершает обработку IRP, вызывая соответствующую функцию режима ядра. Завершение является последним этапом обработки IRP и позволяет ожидающему приложению продолжить работу.
Обзор архитектуры Windows 98/Windows Me
На рис. 1.2 показано одно из возможных представлений архитектуры Windows 98 Me. Ядро операционной системы называется администратором виртуальных машин (VMM, Virtual Machine Manager), потому что его главной функцией
Обзор операционных систем
33
является создание одной или нескольких виртуальных машин, совместно использующих оборудование одной физической машины. Исходной целью драйвера виртуального устройства в Windows 3.0 была виртуализация конкретных устройств, благодаря чему VMM создавал иллюзию, что каждая виртуальная машина обладает полным набором оборудования. Архитектура VMM, появившаяся с Windows 3.0, сохраняется в наши дни в Windows 98/Ме, но с некоторыми расширениями для нового оборудования и 32-разрядных приложений.
Системная виртуальная машина
Виртуальная машина DOS
Приложения Windows
Приложения ' MS-DOS
Пользовательский режим
Режим ядра
Администратор виртуальных машин (VMM)	Драйверы виртуальных устройств
Оборудование;
Рис. 1.2. Архитектура Windows 98/Ме
В Windows 98/Ме обработка операций ввода/вывода далеко не так упорядочена, как в Windows ХР. В частности, в Windows 98/Ме существуют серьезные различия в работе с дисками, коммуникационными портами, клавиатурами и т. д. Также имеются различия между обслуживанием 32- и 16-разрядных приложений (рис. 1.3).
Левый столбец на рис. 1.3 показывает, как выполняется ввод/вывод для 32-разрядных приложений. Приложение вызывает функцию Win32 API (например, ReadFile); вызов обслуживается системной DLL — такой, как KERNEL32.DLL Однако приложения могут использовать ReadFile только для чтения из дисковых файлов, портов передачи данных и устройств, для которых имеются драйверы WDM. Для всех остальных устройств приложение должно использовать специализированные механизмы на базе DeviceloControl. Кроме того, системная DLL содержит код, отличный от ее аналога в Windows ХР. Например, реализация ReadFile пользовательского режима проверяет параметры (в Windows ХР это делается в режиме ядра) и использует тот или иной способ обращения к драйверу режима ядра. Один способ предназначен для дисковых файлов, другой — для последовательных портов, третий — для устройств WDM и т. д. Во всех способах для перехода из пользовательского режима в режим ядра используется программное прерывание 30h, но в остальном они полностью различаются.
34
Глава 1. В начале работы над проектом драйвера
Системная виртуальная машина
Виртуальная машина DOS
Рис. 1.3. Запросы ввода/вывода в Windows 98/Ме
Средний столбец на рис. 1.3 показывает, как выполняется ввод/вывод в 16-раз-рядиых приложениях. Правый столбец дсмонстрируег управляющую логику в приложениях на базе MS-DOS. В обоих случаях программа пользовательского режима прямо или косвенно обращается к функциям драйвера пользовательского режима, который теоретически может существовать сам по себе на «голом» компьютере. Так, программы Winl6 осуществляют ввод вывод с последовательными портами косвенным вызовом 16-разрядной DLL с именем СОММ.DRV (до выхода Windows 95 файл COMM.DRV представлял собой автономный драйвер, который брал на себя обработку IRQ 3 и 4 и напрямую работал с чипом последовательного порта при помощи команд IN и OUT). Драйвер виртуального коммуникационного устройства (VCD, Virtual Communicarions Device Driver) перехватывает операции ввода вывода с портами, тюбы защититься от одновременных обращений к одному порту от двух виртуальных машин. Если взглянуть на происходящее с экстравагантной точки зрения, драйверы пользовательского режима используют интерфейс «АР1», основанный па перехвате операций ввода/ вывода. «Виртуализация» таких драйверов, как VCD, обслуживает эти вызовы «псевдо-API» посредством имитации работы оборудования.
Если все операции ввода/вывода в Windows ХР используют общую структуру данных (1RP), в Windows 98/Ме такого единообразия нс существует — даже после того, как запрос приложения достигнет режима ядра. Драйверы последовательных портов соответствуют парадигме вызова функций драйверов портов, определяемой VCOMM.VXD. В тоже время, драйверы дисковых устройств
 зкой драйвер вам нужен?
35
'частвуют в многоуровневой пакетной архитектуре, реализуемой IOS.VXD. Для ругих классов устройств применяются другие механизмы.
Впрочем, в контексте драйверов WDM внутренняя архитектура Windows 98/Ме •чснь похожа на архитектуру Windows ХР. Системный модуль (NTKERN.VXD) содержит специфические для Windows реализации многих вспомогательных функций ядра Windows NT. NTKERN создает пакеты IRP и посылает их драйверам WDM практически так же, как это делается в Windows ХР. В сущности, драйверы WDM не различают эти две среды.
Тем не менее, несмотря на сходство сред WDM в Windows ХР и Windows 98 Me, между ними существует целый ряд значительных различий!. В книге осо-эо выделены аспекты совместимости, важные для большинства программистов драйверов. Схема, позволяющая использовать одни и ге же двоичные драйверы в Windows 2000 ХР и Windows 98/Ме, несмотря на эти различия, описана в при-I ожени и А.
Какой драйвер вам нужен?
Полноценная система Windows ХР включает многие типы драй воров. Некоторые из них показаны на рис. 1.4.
Рис. 1.4. Типы драйверов устройств в Windows ХР
О Драйверы виртуальных устройств (VDD) представляют собой компоненты пользовательского режима, которые позволяют приложениям MS-DOS работать с оборудовании на платформе х86. VDD используют маски разрешений ввода/вывода для перехвата обращений к портам; фактически, они имитируют работ}7 оборудования для приложений, изначально запрограммированных на прямую работу с устройствами на «голой» машине». Не путайте драйверы VDD в Windows ХР с VxD в Windows 98/Ме! Обе разновидности называются
36
Глава 1. В начале работы над проектом драйвера
драйверами виртуальных устройств и выполняют общую функцию (виртуализация оборудования), но в них используются совершенно разные программные технологии.
О Категория драйверов режима ядра состоит из нескольких подкатегорий. Драйвером РпР называется драйвер режима ядра, поддерживающий протоколы Plug and Play в системе Windows ХР. Если уж быть совсем точным, в этой книге рассматриваются драйверы РпР, и ничего более.
О Драйвером WDM называется драйвер РпР, дополнительно поддерживающий протоколы управления питанием и совместимый с Windows 98/Ме и Windows 2000/ХР на уровне исходных кодов. В категории драйверов WDM также выделяются драйверы классов (предназначенные для управления устройствами, относящиеся к четко определенному классу), минидрайверы (предоставляющие драйверам классов специфическую поддержку для конкретного производителя), монолитные функциональные драйверы (реализующие всю функциональность, необходимую для поддержки устройства) и фильтрующие драйверы (перехватывающие операции ввода/вывода для конкретных устройств с целью их расширения или модификации).
О Драйверы файловой системы реализуют стандартную модель файловой системы PC (которая включает концепции иерархической структуры каталогов с именованными файлами) на локальных жестких дисках или по сетевым подключениям. Они также относятся к категории драйверов режима ядра.
О Наследные драйверы устройств представляют собой драйверы режима ядра, напрямую управляющие устройством без поддержки других драйверов. Фактически, к этой категории относятся драйверы более ранних версий Windows NT, без изменений работающие в Windows ХР.
Не все различия, обусловленные этой схемой классификации, всегда остаются принципиальными. Как я упоминал в своей предыдущей книге «Systems Programming for Windows 95» (Microsoft Press, 1996), покупка моей книги вовсе не означает, что вам нужно срочно становиться формалистом. В частности, я не всегда провожу жесткие различия между драйверами WDM и РпР, следующие из этой схемы. Различия в основном имеют феноменологическую природу в зависимости от того, способен ли данный драйвер работать как в Windows 2000/ХР, так и в Windows 98/Ме. Я не буду злоупотреблять технически точными терминами, но буду очень внимателен при обсуждении системно-зависимых аспектов.
Вполне естественно, что неопытный программист или руководитель, столкну вшийся с этой классификацией, не может понять, какой драйвер потребуется ему для конкретного устройства. Для некоторых устройств писать драйверы вообще не нужно, потому что Microsoft уже поставляет обобщенный драйвер, работающий с вашим устройством. Приведу лишь некоторые примеры:
О SCSI- и ATAPl-совместимые устройства большой емкости;
О любые устройства, подключенные к USB-порту и полностью соответствующие спецификации (при условии, что вас устраивают ограничения стандартного драйвера Microsoft);
Какой драйвер вам нужен?
37
О стандартная мышь для последовательного порта или PS 2;
О стандартная клавиатура;
О видеоадаптер без аппаратного ускорения или других специальных функций;
О стандартный параллельный или последовательный порт;
О стандартный дисковод для гибких дисков.
Драйверы WDM
Для большинства устройств, не поддерживаемых Microsoft напрямую, необходимо написать драйвер WDM. Сначала необходимо решить, собираетесь ли вы написать монолитный функциональный драйвер, фильтрующий драйвер или всего лишь минидрайвер. Скорее всего, вам никогда не придется писать драйверы классов, поскольку компания Microsoft предпочитает зарезервировать эту возможность для себя (это позволит ей обслуживать как можно более широкий диапазон производителей оборудования).
Минидрайверы WDM
Основное эмпирическое правило гласит: если компания Microsoft создала драйвер класса для типа устройств, поддержку которого вы хотите обеспечить, напишите минидрайвер, работающий с этим драйвером класса. Номинально устройством будет управлять ваш минидрайвер, но при этом он будет вызывать функции драйвера класса, который берет на себя работу с оборудованием и обратный вызов функций для выполнения различных аппаратно-зависимых операций. Объем работы по написанию минидрайвера очень сильно зависит от класса устройства.
Некоторые примеры классов устройств, для которых стоит выбрать минидрайвер:
О Устройства ввода с интерфейсом, отличным от USB: мыши, клавиаг1уры, джойстики, рули и т. д. Если имеется устройство USB, для которого обобщенного поведения HIDUSB.SYS (драйвер Microsoft для устройств USB HID) оказывается недостаточно, также можно подумать о написании минидрайвера для HIDCLASS. Главная особенность таких устройств заключается в том, что выдаваемая ими информация о действиях пользователя может быть описана структурой данных дескриптора. Для таких устройство HIDCLASS.SYS служит драйвером класса и реализует многие функции, используемые библиотекой Direct-Input и другими, более высокими уровнями программного обеспечения, поэтому особого выбора у вас нет. Тем не менее, задача не столь простая, поэтому в книге ей посвящено довольно много места. Кстати говоря, сам драйвер HIDUSB.SYS также является минидрайвером для HIDCLASS.
О Устройства WIA (Windows Image Acquisition), в том числе сканеры и цифровые камеры. В сущности, написанный вами минидрайвер должен реализовывать некоторые интерфейсы в стиле СОМ для поддержки специфических аспектов вашего оборудования.
38
Глава 1. В начале работы над проектом драйвера
О Потоковые устройства: аудио, видео, DVD и программные фильтры для мультимедийных потоков данных. Для них программист пишет потоковый мини драй в ср.
О Сетевые устройства, подключаемые к нетрадиционным шинам — таким, как USB или 1394. Для таких устройств пишется драйвер минипорта NDIS (Network Driver Interface Specification) «с нижней гранью WDM», как говорится в документации DDK по этой теме. Вряд ли такой драйвер будет портироваться между операционными системами, поэтому планируйте написание нескольких версий драйвера с учетом небольших различий между платформами.
О Видеокарты. Для таких устройств пишется минидрайвер, работающий с драйвером класса для видео порта.
О Принтеры, для которых необходимы DLL-библиотеки пользовательского режима вместо драйверов режима ядра.
О Аккумуляторы, для которых Microsoft поставляет обобщенный драйвер класса. Программист пишет минидрайвер (в DDK он называется драйвером миникласса, но это одно и то же) для работы с BATTC.SYS.
Фильтрующие драйверы WDM
Возможно, ваше устройство работает так близко к общепризнанному стандарту, что обобщенного драйвера Microsoft оказывается почти достаточно. Возможно, в некоторых ситуациях вам удастся написать фильтрующий драйвер, изменяющий поведение обобщенного драйвера ровно настолько, чтобы обеспечить работу вашего оборудования. Кстати говоря, такая возможность применяется довольно редко, потому что обычно бывает трудно изменить механизм работы обобщенного драйвера с оборудованием. Фильтрующие драйверы подробно рассматриваются в главе 16.
Монолитные функциональные драйверы WDM
Если не считать отдельных исключений, о которых речь пойдет в следующем разделе, для большинства других типов устройств требуется то, что я называю монолитным функциональным драйвером WDM. Фактически, такой драйвер работает самостоятельно и определяет все аспекты взаимодействия с оборудованием.
Если драйверы этого типа подходят для вашей ситуации, я рекомендую следующую процедуру, в результате которой создается единый двоичный файл, работающий на всех платформах Intel х86 во всех операционных системах. Прежде всего, используйте новейшую версию DDK — для всех примеров, прилагаемых к книге, использовалась бета-версия .NET DDK. При помощи loIsWdmVersionAvailable определяется версия операционной системы. Если это Windows 2000 или Windows ХР, вызовите MmGetSystemRoutineAddress для получения указателя на функцию, специфическую для Windows ХР. Я также рекомендую поставлять WDMSTUB.SYS (см. приложение А) для определения MmGetSystemRoutineAddress и других критических функций ядра в Windows 98/Ме; в противном случае ваш драйвер
Какой драйвер вам нужен?
39
попросту нс загрузится в Windows 98/Ме из-за наличия неопределенных импортированных функций.
Вот лишь некоторые примеры устройств, для которых может потребоваться монолитный функциональный драйвер WDM:
Э любые устройства для чтения SmartCard, кроме подключаемых к последовательному порту;
Э цифроаналоговые преобразователи;
Э карты ISA для обслуживания идентификационных датчиков.
: ЛВОИЧНОЙ СОВМЕСТИМОСТИ---------------------------------------------------
Изначально предполагалось, что драйверы WDM будут совместимы на двоичном уровне во всех версиях Windows. Из-за графика выпуска новых версий и соображений второго (и более высоких) порядков каждая версия, начиная с Windows 98, включала поддержку все большего количества функций ядра — полезных, а иногда даже необходимых для более надежного и удобного программирования. Примером служит семейство функций loXxxWorkItem (см. главу 14); эти функции были добавлены в Windows 2000 и должны использоваться вместо похожих, но менее надежных функций ExXxxWorkltem. Если не принять дополнительных мер, драйвер, вызывающий функции loXxxWorkItem, попросту не загрузится в Windows 98/Ме, потому что операционная система не экспортирует используемые им функции. К сожалению, функция MmGetSystemRoutineAddress также не поддерживается в Windows 98/Ме, поэтому вы даже не сможете выбирать вызываемые функции на стадии выполнения. А если этого недостаточно, проверки WHQL для всех драйверов инициируют вызов ExXxxWorkltem.
В Windows 98/Ме драйвер VxD с именем NTKERN реализует подмножество WDM-функций поддержки ядра. Как более подробно объясняется в приложении А, работа NTKERN зависит от определения новых символических имен экспорта для загрузчика времени выполнения. Вы также можете определять собственные символические имена экспорта; именно так WDMSTUB удается определять отсутствующие символические имена для драйверов, совместимых на двоичном уровне, которые я рекомендую создавать.
В состав прилагаемых материалов входит утилита WDMCHECK. Запуская ее в Windows 98/Ме, можно проверить драйвер на предмет отсутствующих импортируемых имен. Если разработанный вами драйвер идеально работает в Windows ХР, я рекомендую скопировать драйвер в систему Windows 98/Ме и для начала запустить WHMCHECK. Если WDMCHECK показывает, что драйвер вызывает какие-либо неподдерживаемые функции, далее проверьте, поддерживает ли эти функции WDMSTUB. Если поддерживает — просто включите WDMSTUB в пакет драйвера, как описано в приложении А. Если нет — либо измените драйвер, либо отправьте мне сообщение с просьбой изменить WDMSTUB. Так или иначе, в конечном итоге у вас появится драйвер, совместимый на двоичном уровне.
Другие типы драйверов
Иногда из-за архитектурных различий между Windows 98/Ме и Windows 2000/ ХР монолитного функционального драйвера WDM оказывается недостаточно. Далее перечислены случаи, в которых необходимо написать два драйвера: драйвер WDM для Windows 2000/ХР и драйвер VxD для Windows 98/Ме.
О Драйвер для последовательного порта. Драйвер дляWindows 98/Ме представляет собой VxD, предоставляющий интерфейс к драйверу порта VCOMM на верхней грани, тогда как драйвер для Windows 2000/ХР представляет собой драйвер WDM на верхней грани, предоставляющий более функциональный и жестко формализованный интерфейс IOCTL. Эти две спецификации верхней грани не имеют ничего общего
40
Глава 1. В начале работы над проектом драйвера
О Драйвер устройства, подключенного к последовательному порту. Драйвер для Windows 98/Ме представляет собой VxD, обращающийся к VCOMM для взаимодействия с портом. Драйвер для Windows 2000/ХР представляет собой драйвер WDM, взаимодействующий с SERIAL.SYS или другим драйвером последовательного порта, реализующим тот же интерфейс IOCTL.
О Драйвер нестандартного запоминающего устройства большой емкости с ин терфейсом USB. Для Windows 98/Ме пишется драйвер VxD, входящий в многоуровневую иерархию драйверов I/O Supervisor. Для Windows 2000/ХР пишется монолитный функциональный драйвер WDM, который на верхней грани получает блоки запросов SCSI, а на нижней взаимодействует с устройством USB.
Для двух классов устройств компания Microsoft определила портируемую архитектуру драйверов задолго для появления WDM:
О Адаптеры SCSI (Small Computer System Interface) обслуживаются драйверами «мини-порта SCSI», которые не используют стандартные функции поддержки ядра и вместо этого работают через специализированный API, экспортируемый SCSIPORT.SYS или SCSIPORT.VXD, в зависимости от обстоятельств. Минипорт портируется между системами.
О Сетевые карты обслуживаются драйверами «мини-порта NDIS», работающими исключительно через специализированный API, экспортируемый NDIS.SYS или NDIS.VXD, в зависимости от обстоятельств. Одно время драйверы минипорта NDIS были портируемыми, но к настоящему времени их портируемость в значительной степени утрачена. Драйверы сетевых протоколов и так называемые «промежуточные» (intermediate) драйверы, обеспечивающие фильтрацию, также функционируют «на орбите» ND1S.
Управление проектом и контрольный список
Если вы работаете руководителем проекта или в ином отношении отвечаете за выпуск устройства на рынок, вам кое-что нужно знать о драйверах устройств. Прежде всего необходимо решить, нужен ли специализированный драйвер, и если нужен, то какой. Предыдущий раздел поможет в принятии этого решения, но, возможно, вам также стоит нанять эксперта для проведения консультаций по этому вопросу.
Если анализ ситуации приводит к заключению, что вам потребуется нестандартный драйвер, после этого придется найти подходящего программиста. К сожалению, программирование драйверов WDM — весьма непростое дело, и только опытные (и дорогие!) программисты способны хорошо с ним справиться. В некоторых компаниях имеются штатные программисты драйверов, но большинство компаний не может себе позволить такой роскоши. Если ваша компания относится ко второй категории, возможно несколько вариантов: обучение человека, входящего в штат; наем программиста, обладающего необходимыми навыками; привлечение консультанта или программиста-«контрактника» или передача разработки на внешний подряд компании, специализирующейся
правление проектом и контрольный список
41
на программировании драйверов. У каждого варианта есть свои плюсы и минусы, и вам придется оценивать их на основании конкретных потребностей.
Программирование драйвера должно начаться сразу же после появления до-таточно устойчивой спецификации работы оборудования. Будьте готовы к тому, тю спецификацию придется менять в свете неприятных открытий, сделанных в процессе разработки драйвера. Также приготовьтесь к многократному пере-\ioipy оборудования «прошивки» и архитектуры драйвера. Гибкость и умение начать работу заново вам здесь основательно пригодятся.
Также приготовьтесь к тому, что программирование драйвера займет больше времени и обойдется дороже, чем предполагалось первоначально. Все проекты то разработке программного обеспечения подвержены перерасходам средств и времени. Дополнительные затраты в проектах такого рода обусловлены трудностями общения между «аппаратчиками» и программистами, неоднозначностями в спецификациях и документации DDK, ошибками во всех компонентах и задержками в разработке и производстве.
Как правило, оборудование и программы следует отправить в лабораторию WHQL (Windows Hardware Quality Lab) для получения цифрового сертификата, /прощающего процесс установки и дающего право на использование логотипов Microsoft. Большую часть тестирования вам придется выполнять самостоятельно, и для этот потребуется определенная конфигурация компьютеров, поэтому постарайтесь пораньше узнать требования для своего класса устройств, чтобы избежать сюрпризов на завершающей стадии проекта. Просто для примера: тестирование устройства USB требует включения различного аудиооборудования в конкретную топологию, даже если ваше устройство не имеет никакого отношения к звуку или другой разновидности потоковой передачи данных.
Также подготовьте свою деловую инфраструктуру к работе с WHQL. Как минимум, для этого потребуется получить номер DUNS (Data Universal Numbering System) от Dun and Bradstreet (или представить эквивалентное доказательство существования коммерческой организации) и сертификат цифровой подписи от Verisign. На момент написания книги номер DUNS предоставлялся бесплатно, но за сертификаты Verisign приходилось платить. Также учтите, что прохождение всех установленных процедур в нескольких компаниях потребует времени.
Пораньше продумайте процедуру установки драйверов конечным пользователем. Большинство производителей дополнительного оборудования предпочитают поставлять па компакт-диске специальную про!рамму установки, однако написание такой программы - длительный процесс, способный занять опытного программиста на несколько недель. Хранилища драйверов в Веб используются довольно часто, по требуют особого внимания к проблемам установки.
Драйверы предоставляют статистическую и другую управляющую информацию двумя способами. Подсистема WMI (Windows Management Instrumentation) обеспечивает канал передачи различных типов двоичных данных, по зависящий от языка и транспорта. Компания Microsoft определила стандартные классы WMI для некоторых типов устройств; возможно, в вашей отрасли установлены другие стандарты, которым должен соответствовать драйвер. В главе 10 рассказано, как обеспечить соответствие стандартам Microsoft, но для соблюдения стандартов отрасли может потребоваться участие представителей отраслевой группы вашей компании.
42
Глава 1. В начале работы над проектом драйвера
Второй механизм передачи управляющей информации — журнал системных событий, который с первых дней поддерживался в Windows NT. Журнал позволяет администратору легко узнать об исключительных ситуациях, возникавших в недавнем прошлом. Драйвер должен сообщать о событиях, которые представляют интерес для администратора и могут потребовать реакции с его стороны. Чтобы решить, какие события должны регистрироваться в журнале, разработчик драйвера должен проконсультироваться у опытного системного администратора - - это поможет избежать загромождения журнала рутинной повседневной информацией. Вероятно, исполняемый файл драйвера будет включать текст сообщений в виде специального многоязыкового ресурса; возможно, для составления этого текста стоит прибегнуть к услугам квалифицированного автора (я не говорю, что программист драйвера не справится с этой задачей, но он может быть не лучшей кандидатурой).
Кроме собственно драйвера, также может потребоваться панель управления или другая конфигурационная программа. Пост роение этих компонентов должно осуществляться совместными усилиями программиста драйвера и специалиста по взаимодействию с пользователем. Поскольку эти программы будут устанавливаться вместе с драйвером, они должны стать частью пакета, снабжаемого цифровой подписью WHQL, и поэтому они должны быть закопчены одновременно с драйвером.
Наконец, не относитесь к драйверу, как к второстепенной детали. Наличие хорошего драйвера с гладким процессом установки не менее важно, чем внешний вид продукта. Проще говоря, если драйвер угробит операционную систему, рецензенты оповестят об этом общественность, и народ побежит возвращать ваш продукт в магазин. Люди, у которых система «упала» из-за вашего драйвера, уже никогда не будут иметь дела с вашими продуктами. Таким образом, близорукое решение но недофинансированию разработки драйвера может привести к драматическим отрицательным последствиям на многие годы. Этот совет особенно важен для производителей оборудования в развивающихся странах, руководство которых склонно искать любые нуги для сокращения издержек. На мой взгляд, разработка драйвера — одна из тех областей, в которых экономия неуместна.
Подведем итог. При планировании проекта следует предусмотреть следующие этапы:
О выбор типа драйвера и программиста;
О составление спецификации оборудования, достаточной для начала работы над драйвером;
О построение прототипа устройства, достаточного для тестирования драйвера; О обеспечение совместной работы драйвера и устройства/«прошивки» в соответствии с исходными намерениями;
О тестирование установочного файла (INF) во всех операционных системах; О создание панелей управления и прочих вспомогательных программ;
О реализация и тестирование функциональности WMI и журнала событий;
О прохождение тестов WHQL и передача заявки;
О завершение пользовательской программы установки (не входит в заявку WHQL);
О готовность к записи дисков и поставке продукта!
Базовая структура А драйвера WDM
В первой главе была описана базовая архитектура операционных систем Microsoft Windows ХР и Microsoft Windows 98/Ме. Я объяснил, что драйверы устройств предназначены для управления оборудованием от имени операционной системы. Также было показано, как определить тип драйвера для вашего оборудования. В этой главе мы более подробно рассмотрим, какой программный код входит в драйвер WDM и как организуется взаимодействие различных типов драйверов при управлении оборудованием. Также мы в общих чертах рассмотрим процесс поиска и загрузки драйверов системой.
Как работают драйверы
Драйвер удобно рассматривать как контейнер для функций, вызываемых операционной системой для выполнения различных операций, относящихся к работе оборудования. Рисунок 2.1 поясняет эту концепцию. Некоторые функции (такие как DriverEntry и Add Device, а также диспетчерские функции для некоторых типов запросов IRP) присутствуют во всех контейнерах без исключения. Драйверы с очередями запросов могут содержать функцию Startlo. Драйверы, выполняющие передачу данных по каналам DMA, содержат функцию AdapterControl. Драйверы устройств, генерирующих аппарагные прерывания, содержат обработчики прерываний (ISR, Interrupt Service Routine) и функции отложенного вызова процедур (DPC, Deferred Procedure Call). Б большинстве драйверов присутствуют диспетчерские функции для нескольких типов IRP, помимо трех, показанных на рис. 2.1. Следовательно, одной из задач разработчика драйвера WDM должен стать выбор функций, которые должны быть включены в конкретный контейнер.
В этой главе я покажу, как пишутся функции DriverEntry и AddDevice для монолитных функциональных драйверов — одного из типов драйверов WDM, рассматриваемых в книге. Как будет показано в следующих главах, фильтрующие драйверы также содержат функции DriverEntry и AddDevice, аналогичные этим. Также вы узнаете, что минидрайверы содержат совершенно иные функции DriverEntry и могут содержать или не содержать функции AddDevice,
44
Глава 2. Базовая структура драйвера WDM
в зависимости от того, как автор ассоциированного драйвера класса спроектировал его интерфейс.
Основные функции драйвера
DriverEntry
AddDevice
Управляющие функции
Диспетчерские функции
DispatchPnp
Dispatch Power
|2] Обязательные функции драйвера
Г] Включение Startlo для реализации запросов очередей
Г] Включение AdapterControl для DMA
|2] Включение обработчиков прерываний и DPC для устройств с прерываниями gH Необязательные диспетчерские функции IRP
Рис- 2.1. Драйвер как совокупность функций
Как работают приложения
На модели драйвера как «пакета функций» (в отличие от модели «главной программы и вспомогательных функций», характерной для приложений) стоит остановиться подробнее. Для многих из нас изучение С начиналось со следующей программы:
int maindnt агдс, char* argv[])
{
printf("Hel1 о, world!");
return 0;
}
Программа состоит из главного модуля main и библиотеки вспомогательных функций, большинство из которых в программе явно не вызывается. Одна из вспомогательных функций, printf, выводит сообщение в стандартный выходной файл. В результате компиляции исходного модуля, содержагцего программу main, и ее компоновки с библиотекой времени выполнения, содержащей printf и другие вспомогательные функции, необходимые главной программе, формируется исполняемый модуль, которому присваивается имя (например, HELLO.EXE).
Как работают драйверы
45
Я даже рискну назвать этот модуль пышным именем приложение, потому что он принципиально ничем не отличается от любого другого приложения, уже существутощего или написанного в будущем. Приложение запускается из командной строки следующим образом:
С:\>hello
Hello, world!
С:\>
Далее перечислен ряд других базовых свойств приложений:
□ Некоторые вспомогательные функции, используемые приложением, берутся из статической библиотеки, откуда компоновщик извлекает их в процессе построения. Одной из таких функций является printf.
О Другие вспомогательные функции динамически связываются с библиотеками динамической компоновки (DLL). Для таких функций компоновщик включает в исполняемый файл специальные ссылки импорта, а загрузчик времени выполнения связывает эти ссылки с реальным системным кодом. Более того, весь интерфейс Win32 API, используемый прикладными программами, проходит динамическую компоновку; как видите, динамическая компоновка играет очень важную роль в программировании для Windows.
О Исполняемые файлы могут содержать символическую информацию, при помощи которой отладчики ассоциируют адреса времени выполнения с элементами исходного программного кода.
О Исполняемые файлы также могут содержать ресурсные данные — такие как шаблоны диалоговых окон, текстовые строки и идентификаторы версий. Хранить такие файлы внутри файла лучше, чем в отдельных файлах, потому что тем самым снимается проблема случайного использования файлов из других версий.
У программы HELLO.EXE есть одна интересная особенность: когда операционная система передает ей управление, возврат происходит лишь после завершения выполняемой задачи. Вообще говоря, это характерно для любого приложения, которое вы когда-либо будете использовать в Windows. В приложениях консольного режима (таких как HELLO) операционная система изначально передает управление функции инициализации, входящей в библиотеку времени выполнения компилятора. Функция инициализации в конечном итоге вызывает main для выполнения основной работы приложения.
Графические приложения в Windows работают практически так же, разве что главной процедуре присваивается имя WinMain вместо main. WinMain организует прием и доставку сообщений оконным процедурам. Она возвращает управление операционной системе при закрытии пользователем главного окна. Впрочем, если во всех написанных вами Windows-приложениях использовалась библиотека MFC (Microsoft Foundation Classes), возможно, вы никогда не видели процедуру WinMain, скрытую в глубинах библиотеки. И все же не сомневайтесь, она там есть.
46
Глава 2. Базовая структура драйвера WDM
В многозадачных системах создается впечатление одновременной работы нескольких приложений, даже если компьютер оснащен только одним процессором. В ядро операционной системы входит планировщик, предоставляющий короткие промежутки времени (называемые квантами) всем программным потокам, которым разрешено выполнение в настоящий момент. Приложение начинает работу с одного программного потока, но при желании может создавать дополнительные потоки. Каждый поток обладает приоритетом, который назначается ему системой, и может увеличиваться или уменьшаться по разным причинам. В момент принятия решения планировщик выбирает поток с наивысшим приоритетом и передает ему управление, загружая в регистры процессора сохраненный набор содержимого регистров, включая указатель команд. Истечение кванта, выделенного программному потоку, сопровождается выдачей прерывания от процессора. В процессе обработки этого прерывания система сохраняет текущее состояние регистров, которое восстанавливается при следующей активизации того же потока.
Допустим, программный поток запускает на выполнение некоторую длительную операцию. Вместо того чтобы просто дожидаться истечения выделенного кванта, он может добровольно уступить управление (блокироваться). Этот способ эффективнее, чем ожидание завершения интервала в цикле опроса, ведь он позволяет другим потокам получить управление раньше, чем если бы система передавала управление другому потоку только по истечении выделенного кванта.
Конечно, вы уже знаете все, о чем я сейчас говорил. Мне просто хотелось привлечь ваше внимание к тому факту, что приложение по своей сути представляет собой «эгоистичный» программный поток, который захватывает процессор и пытается удерживать его вплоть до своего завершения. Планировщик операционной системы играет роль арбитра, обеспечивающего нормальное сосуществование нескольких потоков-«эгоистов».
Драйверы устройств
Драйвер, как и файл HELLO.EXE, представляет собой исполняемый файл. Он обладает расширением .SYS, но с точки зрения структуры ничем не отличается от любого другого 32-разрядного графического или консольного приложения. Как и файл HELLO.EXE, драйвер использует ряд вспомогательных функций, многие из которых компонуются динамически из ядра операционной системы, из драйвера класса или другой дополнительной библиотеки. Файл драйвера также может содержать отладочную информацию символических имен и ресурсные данные.
Под управлением системы
Однако в отличие от HELLO.EXE в драйвере отсутствует главный модуль. Вместо него он содержит набор функций, вызываемых системой в тот момент, когда она сочтет нужным. Конечно, эти функции могут пользоваться другими вспомогательными функциями в драйвере, в статических библиотеках и операционной системе, но сам драйвер не отвечает ни за что, кроме своего оборудования;
Как работают драйверы
47
система отвечает за все остальное, в том числе и за принятие решений по поводу того, когда должен выполняться код драйвера.
Далее приводится один из возможных сценариев вызова функций драйвера операционной системой:
1.	Пользователь подключает устройство. Система загружает исполняемый файл драйвера в виртуальную память и вызывает функцию DriverEntry. Функция DriverEntry выполняет кое-какие операции и возвращает управление.
2.	Администратор Plug and Play (РпР Manager) вызывает функцию AddDevice. Функция также выполняет кое-какие операции и возвращает управление.
3.	РпР Manager отправляет драйверу несколько пакетов IRP. Диспетчерская функция поочередно обрабатывает все пакеты и возвращает управление.
4.	Приложение открывает манипулятор (handle) устройства, на что система посылает драйверу очередной пакет IRP. Диспетчерская функция выполняет кое-какие операции и возвращает управление.
5.	Приложение пытается прочитать данные, что также сопровождается отправкой IRP. Диспетчерская функция помещает IRP в очередь и возвращает управление.
6.	Предыдущая операция ввода/вывода завершается выдачей аппаратного прерывания, к которому подключен ваш драйвер. Обработчик прерывания выполняет кое-какие операции, планирует вызов DPC и возвращает управление.
7.	Выполняется функция DPC. Среди прочего, она удаляет пакет IRP, поставленный в очередь на шаге 5, и программирует оборудование на чтение данных. Затем функция DPC возвращает управление системе.
8.	Проходит время. Система многократно вызывает функции драйвера.
9.	В конечном итоге пользователь отключает устройство. РпР Manager отправляет драйверу несколько пакетов IRP; драйвер обрабатывает их и возвращает управление. Операционная система вызывает функцию DriverUnloajd, которая выполняет минимальный объем работы и возвращает управление. Система выгружает код драйвера из виртуальной памяти.
На каждом шаге процесса система решает, какие действия должен выполнить драйвер, будь то инициализация, обработка IRP, обработка прерывания или что-нибудь еще. Таким образом, система выбирает нужную функцию драйвера, а функция делает то, что ей положено, и возвращает управление системе.
Программные потоки и код драйвера
Другое отличие драйверов от приложений заключается в том, что система не создает специального программного потока для выполнения кода драйвера. Вместо этого функции драйверов выполняются в контексте потока, активного на тот момент, когда система принимает решение о вызове этой функции.
Невозможно предсказать, какой программный поток будет активен в момент возникновения аппаратного прерывания. Представьте, что вы наблюдаете за каруселью в парке аттракционов. Лошади на карусели — аналоги программных потоков в системе. Назовем ближайшую лошадь «текущей». Теперь допустим,
48
Глава 2. Базовая структура драйвера WDM
вы хотите сфотографировать карусель в следующий раз, когда кто-нибудь скажет «Вот здорово!» (знаю по собственному опыту, что долгое ожидание вам не грозит). Нельзя предсказать заранее, какая лошадь окажется «текущей» на вашем снимке. Точно так же нельзя предсказать, какой из возможных программных потоков будет выполняться в момент возникновения аппаратного прерывания. Мы будем называть его произвольным потоком и говорить о выполнении кода драйвера в контексте произвольного потока.
Принимая решение о вызове функции драйвера, система часто работает в контексте произвольного потока. Контекст потока будет произвольным, например, при передаче управления вашей функции обработки прерывания. Если запланировать вызов DPC, то поток, в котором будет выполняться DPC, также будет произвольным. Если создать очередь IRP, функция Startlo будет вызвана для произвольного потока. Более того, если какой-то драйвер, находящийся вне стека вашего драйвера, отправит IRP, придется предполагать, что контекст потока произволен. Такая ситуация характерна для драйверов запоминающих устройств, потому что драйвер файловой системы выполняет функции агента, в конечном счете отвечающего за чтение и запись.
Система не всегда выполняет код драйвера в контексте произвольного потока. Драйвер может создавать свои собственные системные потоки, вызывая PsCreateSystemThread. Кроме того, драйвер также может организовать обратный вызов своего кода в контексте системного потока посредством механизма планирования рабочего элемента. В таких ситуациях контекст потока не считается произвольным. Механизм системных потоков и планирования рабочих элементов рассматривается в главе 14.
Возможна и другая ситуация, в которой контекст потока не является произвольным: когда вызов приложением функции API приводит к тому, что I/O Manager отправляет пакет IRP напрямую драйверу. Во время написания драйвера вы будете знать, действует эта ситуация или нет, для каждого обрабатываемого типа IRP.
Произвольность контекста программного потока важна по двум причинам. Во-первых, драйвер не может блокировать (приостанавливать) произвольные потоки: было бы несправедливо приостанавливать один поток, выполняя операции для другого потока.
Вторая причина относится к созданию драйвером пакета IRP для отправки другому драйверу. Как более подробно обсуждается в главе 5, в произвольных потоках приходится создавать один тип IRP (асинхронный IRP), однако в непроизвольном потоке возможно создание другого типа IRP (синхронный IRP). I/O Manager связывает синхронный IRP с потоком, в котором он был создан. В случае завершения этого потока IRP автоматически отменяется. Однако I/O Manager не связывает асинхронный IRP с каким-либо конкретным потоком. Поток, в котором был создан асинхронный IRP, может не иметь никакого отношения к выполняемой операции ввода/вывода, и было бы неправильно отменять IRP только из-за того, что этот поток завершился. Соответственно, система этого и не делает.
Поиск и загрузка драйверов
49
Симметричная многопроцессорная модель
В Windows ХР используется так называемая симметричная модель управления компьютерами, оснащенными несколькими процессорами. В этой модели каждый процессор считается абсолютно равноправным с любым другим процессором в отношении планирования потоков. У каждого процессора имеется собственный текущий поток. Ничто не мешает I/O Manager, работающему в контексте потоков, выполняемых на двух и более процессорах, одновременно вызывать функции вашего драйвера. Речь идет не об имитации одновременности, при которой потоки выполняются на одном процессоре, — по временной шкале компьютера потоки на самом деле работают поочередно. На многопроцессорных компьютерах разные потоки действительно выполняются одновременно. Как нетрудно догадаться, одновременное выполнение ужесточает требования, предъявляемые к драйверам в области синхронизации доступа к общим данным. Различные методы синхронизации будут описаны в главе 4.
Поиск и загрузка драйверов
В предыдущем разделе я особо подчеркнул, что операционная система управляет работой компьютера и обращается к драйверам устройств для выполнения мелких операций с оборудованием. Драйверы играют аналогичную пассивную роль и в процессе своей начальной загрузки. Вам будет проще понять материал книги, если мы сразу выясним, каким образом система обнаруживает оборудование, определяет, какой драйвер необходимо загрузить, и настраивает драйвер для управления оборудованием. В системе используются два метода, слегка различающихся в зависимости от того, совместимо ли оборудование со стандартом Plug and Play:
О Устройство Plug and Play обладает электронной сигнатурой, которая распознается системой. Для устройств Plug and Play драйвер системной шины обнаруживает существование устройства и читает сигнатуру, чтобы определить тип устройства. Далее автоматический процесс, основанный на содержимом реестра и INF-файлов, обеспечивает загрузку нужного драйвера системой.
О Наследные устройства не обладают электронной сигнатурой, поэтому система не может распознавать их автоматически. Таким образом, пользователь должен инициировать процесс «распознавания» запуском мастера нового оборудования (Add New Hardware Wizard) — так система узнает о существовании некоторого устройства. В дальнейшем система использует для загрузки нужного драйвера тот же автоматизированный процесс с использованием реестра и INF-файлов, который применяется для устройств Plug and Play.
Какой бы метод ни выбрала система для распознавания оборудования и загрузки драйвера, сам драйвер будет драйвером WDM, пассивно реагирующим на обращения операционной системы. С этой точки зрения драйверы WDM резко отличаются от драйверов режима ядра в ранних версиях Windows NT и драйверов VxD до выхода Windows 95. В этих средах пользователь должен был каким-то образом обеспечивать загрузку драйвера системой. Далее драйвер сканировал
50
Глава 2. Базовая структура драйвера WDM
аппаратные шины, искал свое оборудование и решал, оставаться ли ему резидентным или нет. Кроме того, драйвер должен был определить используемые ресурсы ввода/вывода и принять меры по предотвращению использования тех же ресурсов другими драйверами.
Иерархия устройств и драйверов
Прежде чем объяснять процессы распознавания оборудования и загрузки драйверов, я должен объяснить концепцию иерархии драйверов, показанной на рис. 2.2. Левый столбец представляет вертикальный (направленный снизу вверх) стек структур ядра DEVICEJDBJECT; каждая структура описывает, как система управляет одним устройством. Средний столбец представляет набор драйверов устройств, участвующих в управлении. Правый столбец демонстрирует направление передачи IRP между драйверами.
Рис. 2.2. Иерархия объектов устройств и драйверов в модели WDM
В модели WDM каждое устройство обладает как минимум двумя драйверами. Один из этих драйверов, который мы будем называть функциональным драйвером, — то, что мы обычно подразумеваем под понятием «драйвер устройства». Он во всех подробностях знает, как происходит работа с оборудованием. Он отвечает за инициирование операций ввода/вывода, за обработку прерываний, происходящих по завершении этих операций, а также за предоставление пользователю всех необходимых возможностей управления этим устройством.
Второй из двух драйверов, имеющихся у каждого устройства, называется драйвером шины. Он отвечает за управление связью между оборудованием и компьютером. Например, драйвер шины PCI (Peripheral Component Interconnect) представляет собой программный компонент, который непосредственно обнаруживает карту, вставленную в слот PCI, и определяет требования карты к связям с компью-
Поиск и загрузка драйверов
51
тером, основанным на отображении на порты ввода/вывода или на память. Кроме того, этот же программный компонент включает и выключает подачу тока в слот карты.
ПРИМЕЧАНИЕ----------------------------------------------------------—--------
Монолитный функциональный драйвер WDM представляет собой отдельный исполняемый файл, содержащий динамические ссылки на файл NTOSKRNL.EXE с ядром операционной системы и на файл HAL.DLL с реализацией уровня HAL. Функциональный драйвер также может динамически компоноваться с другими DLL режима ядра. Если компания Microsoft предоставила драйвер класса для данного типа оборудования, ваш минидрайвер динамически связывается с драйвером класса. Комбинация из минидрайвера и драйвера класса формирует отдельный функциональный драйвер. Иногда в литературе встречаются описания иерархии, в которых драйверы классов располагаются выше или ниже минидрайверов. Я предпочитаю относить эти так называемые «драйверы классов» к автономным фильтрующим драйверам и использовать термин «драйвер класса» исключительно для обозначения драйверов, которые находятся на одном уровне с минидрайверами, доступны посредством явного импортирования и работают в добровольном сотрудничестве с минидрайверами.
Некоторые устройства имеют более двух драйверов. Мы будем обозначать их обобщенным термином фильтрующий драйвер. Некоторые фильтрующие драйверы просто следят за тем, как функциональный драйвер выполняет ввод/вывод. Впрочем, чаще производитель программного обеспечения или оборудования поставляет фильтрующие драйверы для модификации поведения существующих функциональных драйверов. Верхний фильтрующий драйвер получает доступ к IRP раньше функционального драйвера — это позволяет ему обеспечить поддержку дополнительных функций, неизвестных функциональному драйверу. Иногда верхний фильтр исправляет ошибки или другие недостатки функционального драйвера или оборудования. Нижний фильтрующий драйвер получает доступ к пакетам IRP, которые функциональный драйвер пытается отправить драйверу шины (нижний фильтр находится в стеке ниже функционального драйвера, но выше драйвера шины). В некоторых случаях — например, для устройств, подключенных к шине USB, — нижний фильтр может изменить последовательность операций с шиной, которые пытается выполнить функциональный драйвер.
Вернемся к рис. 2.2. Обратите внимание: каждый из четырех драйверов гипотетического устройства связан с одной из структур DEVICEJDBJECT в левом столбце. Структуры обозначены следующими сокращениями:
О PDO — физический объект устройства (Physical Device Object). Объект используется драйвером шины для представления связи между устройством и шиной.
О FDO — функциональный объект устройства (Function Device Object). Объект используется функциональным драйвером для управления функциональностью устройства.
О FiDO — фильтрующий объект устройства (Filter Device Object). Объект используется фильтрующим драйвером для хранения необходимой информации об оборудовании и о выполняемых операциях фильтрации. (Термин FiDO позаимствован мной из ранних бета-версий Windows 2000 DDK. Сейчас этот термин в DDK не используется — вероятно, его сочли несерьезным1.)
1 Фидо — популярная собачья кличка. — Примеч. перев.
52
Глава 2. Базовая структура драйвера WDM
ЧТО ТАКОЕ ШИНА?--------------—----------------------------------—---------------------
Я довольно свободно использовал термины «шина» и «драйвер шины», не объясняя их смысла. В контексте WDM шиной называется все, к чему подключается устройство — как на физическом, так и на метафорическом уровне.
Такое определение носит весьма общий характер. К нему причисляются не только шины в традиционном понимании (скажем, шина PCI), но и адаптеры SCSI, параллельные порты, последовательные порты, концентраторы USB и т. д. — словом, все, к чему можно подключить другое устройство. К этой же категории относится абстрактная корневая шина (root bus), существующая только в нашем воображении. Корневую шину можно рассматривать как шину, к которой подключаются все наследные устройства. Таким образом, корневая шина является родителем как для карт ISA (Industry Standard Architecture) без поддержки РпР, так и для устройства чтения SmartCard, подключенного к последовательному порту, но не отвечающего стандартной идентификационной строкой на сигналы перечисления последовательных портов. Мы также считаем корневую шину родителем шины PCI — ведь шина PCI не сообщает о своем присутствии на электронном уровне, поэтому операционной системе приходится рассматривать ее как наследное устройство.
Устройства Plug and Play
Напомню то, о чем уже говорилось ранее: устройство Plug and Play обладает электронной сигнатурой, но которой драйвер шины идентифицирует устройство. Некоторые примеры таких сигнатур:
О У карт PCI имеется конфигурационная область, содержимое которой может читаться драйвером шины PCI через выделенные адреса памяти или адреса портов ввода/вывода. В конфигурационной области хранятся данные, идентифицирующие производителя и продукт.
О Устройство USB возвращает дескриптор устройства в результате стандартизированной операции обмена данными по управляющему каналу. Дескриптор устройства содержит идентификационные данные производителя и продукта.
О Устройства PCMCIA (Personal Computer Memory Card International Association) оснащаются памятью, содержимое которой читается драйвером шины PCMCIA для идентификации карты.
Драйвер шины Plug and Play поддерживает функцию перечисления, то есть сканирования всех возможных слотов в момент запуска. Драйверы шин, поддерживающих оперативное подключение устройств во время сеанса (таких как USB и PCMCIA), также отслеживают некоторые аппаратные сигналы, свидетельствующие о появлении нового устройства; получив такой сигнал, драйвер производит повторное перечисление устройств на шине. Конечным результатом перечисления (исходного или повторного) является набор объектов PDO — см. п. 1 на рис. 2.3.
Когда драйвер шины обнаруживает факт подключения или отключения оборудования, он вызывает функцию loInvalidateDeviceRelations, оповещая РпР Manager об изменении состава дочерних устройств шины. Чтобы получить обновленный список PDO дочерних устройств, РпР Manager отправляет драйверу шины пакет IRP. Пакет содержит основной код функции IRP_MJ_PNP и дополнительный код функции IRP_MN_QUERY_DEVICE_RELATIONS. В сочетании эти коды указывают, что РпР Manager ищет так называемые «отношения шины». Этот этап соответствует п. 2 на рис. 2.3.
Поиск и загрузка драйверов
53
Драйвер шины
(PCI/PCMIA и т. д.)
РпР Manager
Рис. 2.3. Установка устройства Plug and Play
ПРИМЕЧАНИЕ---------------------------------------------------------------------
Каждый пакет IRP содержит два кода функции, основной и дополнительный. Основной код функции определяет тип запроса, содержащегося в IRP. IRP_MJ„PNP — основной код функции для за-просов, осуществляемых РпР Manager. Для некоторых основных кодов, включая IRP_MJ„PNP, требуется дальнейшее уточнение операции при помощи дополнительного кода функции.
В ответ на запрос драйвер шины возвращает свой список объектов PDO. РпР Manager может легко определить, какие из устройств, представленных PDO, еще не были инициализированы. Давайте пока сосредоточим внимание на PDO ваших устройств и посмотрим, что произойдет дальше.
РпР Manager посылает драйверу шины другой пакет IRP, на этот раз с дополнительным кодом функции IRP__MN_QUERY_ID (п. 3 на рис. 2.3). Точнее, РпР ’Manager отправляет несколько таких IRP, каждый из которых содержит операнд, приказывающий драйверу шины вернуть идентификатор определенного типа. Один из идентификаторов — идентификатор устройства — однозначно определяет
54
Глава 2. Базовая структура драйвера WDM
тип устройства. Идентификатор устройства представляет собой обычную строку и выглядит примерно так:
PCI\VEN_102C&DEV_00E0&SUBSYS_00000000
USB\VIDJ547&PID_2125&REVJ002
PCMCIAXMEGAHERTZ-CC10BT/2-BF05
ПРИМЕЧАНИЕ----------------------------------------------------------------------
Каждый драйвер шины использует свою схему форматирования электронной сигнатуры в строку идентификатора. Строки идентификаторов, используемые распространенными драйверами шин, описаны в главе 15. В этой главе также рассказано об INF-файлах, о местонахождении различных разделов реестра, упоминаемых в тексте, и о том, какая информация в них хранится.
PnP Manager использует идентификатор устройства для поиска сведений об устройстве в системном реестре. Допустим, устройство было подключено к компьютеру впервые. В этом случае реестр еще не содержит раздела с описанием этого устройства. На этой стадии в игру вступает подсистема установки, которая определяет, какое программное обеспечение необходимо для поддержки устройства (п. 4 на рис. 2.3).
Инструкции по установке для всех типов оборудования хранятся в файлах с расширением .INF. Каждый INF-файл содержит одну или несколько команд моделей, связывающих строки с идентификаторами устройств с установочными секциями в этом INF-файле. Таким образом, столкнувшись с новым устройством, подсистема установки пытается найти INF-файл с командой модели, соответствующей идентификатору устройства. Предоставление этого файла лежит на вашей ответственности, поэтому я обозначил блок, отвечающий за этот этап, подписью «Вы». Пока я намеренно обхожу тему поиска INF-файлов системой и упорядочения нескольких команд моделей, которые обычно находятся в таком файле. Мы еще вернемся к этим подробностям в главе 15, а пока сказанного будет вполне достаточно.
Обнаружив правильную команду модели, подсистема установки выполняет инструкции, приведенные в секции установки. Вероятно, эти инструкции будут копировать некоторые файлы на жесткий диск пользователя, включать в реестр сведения о новом драйвере и т. д. К завершению процесса программа установки создаст в реестре раздел для данного устройства и установит все предоставленное вами программное обеспечение.
Теперь вернемся на несколько абзацев назад и допустим, что на этом компьютере уже встречался экземпляр вашего устройства — скажем, речь идет об устройстве USB, которое пользователь когда-то установил в системе, а теперь подключает заново. В этом случае PnP Manager найдет описание устройства, и ему не придется вызывать программу установки. Соответственно, PnP Manager пропустит всю процедуру установки в и. 5 на рис. 2.3.
На этой стадии PnP Manager знает, что в системе имеется некое устройство и ваш драйвер отвечает за его обслуживание. Если драйвер еще не был загружен в виртуальную память, PnP Manager обращается к диспетчеру памяти с запросом на его отображение. Система не читает дисковый файл с драйвером в память.
Поиск и загрузка драйверов
55
Вместо этого создается файловое отображение (file mapping), что приводит к выборке кода драйвера и данных с использованием механизма подгрузки. Факт использования файлового отображения системой почти не влияет на вашу работу, если не считать одного побочного эффекта: вам придется проявить дополнительную осторожность при отключении драйвера от адресного пространства. Затем диспетчер памяти вызывает функцию DriverEntry.
Далее РпР Manager вызывает функцию AddDevice, чтобы сообщить драйверу об обнаружении нового экземпляра устройства (см. п. 5 на рис. 2.3). После этого РпР Manager посылает драйверу шины пакет IRP с дополнительным кодом функции IRP_MN_QUERY_RESOURCE_REQUIREMENTS. В сущности, этот IRP просит у драйвера шины описать требования устройства к линии запроса прерывания, адресам портов ввода/вывода и системным каналам DMA. Драйвер шины строит список требований к ресурсам и возвращает его (п. 6 на рис. 2.3).
Наконец РпР Manager готов к настройке оборудования. При выделении ресурсов устройству используется набор ресурсных арбитров. Если выделение ресурсов возможно (как это обычно бывает), РпР Manager посылает драйверу пакет IRP_MJ_PNP с дополнительным кодом функции IRP_MN_START_DEVICE. Драйвер обрабатывает IRP, производя настройку и подключение различных ресурсов ядра, после чего устройство готово к использованию.
ОСОБЕННОСТИ ДРАЙВЕРОВ В WINDOWS NT--------------------------------------------
Процесс поиска и загрузки драйверов в Windows ХР (а также Windows 2000, Windows 95 и всех системах линейки Windows 95), описанный в тексте, требует относительной пассивности драйвера. Windows NT 4.0 и более ранних версий работает иначе. В этих системах установка драйвера должна осуществляться специальной программой установки. Программа установки вносит изменения в реестр, чтобы драйвер автоматически загружался при следующей перезагрузке системы. На этой стадии система загружает драйвер и вызывает функцию DriverEntry.
Ваша версия DriverEntry должна каким-то образом определить, какие экземпляры оборудования уже присутствуют в системе. Например, можно просканировать все возможные слоты на шине PCI или предположить, что каждый экземпляр устройства представлен подразделом в реестре.
После идентификации оборудования функция DriverEntry переходит к назначению и резервированию ресурсов ввода/вывода, а затем к операциям настройки и подключения, выполняемым современными драйверами WDM. Как видите, драйверам WDM при инициализации приходится выполнять гораздо меньший объем работы, чем драйверам в более ранних версиях Windows NT.
Наследные устройства
Я использую термин наследные устройства для обозначения любых устройств, не поддерживающих стандарт Plug and Play, — это означает, что операционная система не может автоматически обнаружить их присутствие. Предположим, ваше устройство относится к этой категории. После приобретения устройства пользователь сначала запускает мастер установки нового оборудования, а затем в диалоговых окнах вводит данные, которые приводят программу установки к нужной секции INF-файла (см. рис. 2.4, п. 1).
Программа установки следует инструкциям и создает в реестре записи, используемые корневым перечислителем (п. 2 на рис. 2.4). Информация, сохраняемая
56
Глава 2. Базовая структура драйвера WDM
в реестре, может включать логическую конфигурацию с перечнем требований к ресурсам ввода/вывода для устройства (п. 3).
Пользователь
1 Запускает мастера
I установки нового
I оборудования
Подсистема установки
Выбирает «Установить с диска», чтобы указать INF-файл
Выбирает производителя и модель устройства
Настраивает и подключает карту
4
Создает разделы реестра для корневого перечислителя
3
_________1__________
Определяет конфигурацию на основании данных
LogConfig
Рис. 2.4. Процесс идентификации наследных устройств
Наконец, программа установки предлагает пользователю перезагрузить систему (п. 4). Проектировщики механизма установки предполагали, что в этот момент пользователь займется настройкой карты при помощи перемычек или переключателей, выполняя указания производителя, а затем вставит карту в слот расширения на отключенном компьютере.
После перезапуска (или решения пользователя об отказе от него) корневой перечислитель сканирует реестр и находит появившееся устройство. В дальнейшем процесс загрузки драйвера почти полностью идентичен процессу для устройств Plug and Play — см. рис. 2.5.
Рекурсивное перечисление
В предыдущих разделах был описан процесс загрузки системой драйвера для отдельного устройства. После знакомства с описанием возникает вопрос: как в системе организована загрузка драйверов для всего оборудования компьютера? Оказывается, для этой цели применяется рекурсивный процесс.
На первом этапе РпР Manager вызывает корневой перечислитель для поиска всего оборудования, которое не может заявить о своем присутствии на электронном уровне, включая основную аппаратную шину (например, PCI). Драйвер корневой шины получает информацию о компьютере из реестра, инициализиро
Поиск и загрузка драйверов
57
ванного программой установки Windows ХР. Чтобы получить эту информацию, программа установки запускает модуль подробного анализа оборудования, а также задает вопросы пользователю. Таким образом, драйвер корневой шины располагает достаточной информацией, чтобы создать PDO для основной шины.
Драйвер шины (корневой)
PnP Manager
Рис. 2.5. Загрузка наследного драйвера
Затем функциональный драйвер основной шины может провести электронное перечисление своего оборудования. Занимаясь перечислением оборудования, драйвер шины маскируется под обычный функциональный драйвер. Тем не менее, после обнаружения устройства драйвер меняет роль: он становится драйвером шины и создает новый объект PDO для найденного устройства. Затем РпР Manager загружает драйверы для PDO устройства, как было описано ранее. Может случиться так, что функциональный драйвер устройства найдет дополнительное оборудование, в этом случае весь процесс рекурсивно повторяется. Конечным результатом перечисления является дерево, показанное на рис. 2.6; ветвь устройства шины расходится на ветви оборудования, подключенного к этой шине. Затемненные блоки на рисунке показывают, как один драйвер может
58
Глава 2. Базовая структура драйвера WDM
играть роль функционального драйвера для своего оборудования и драйвера шины для подключенных устройств.
FiDO
FDO
FiDO
PDO
Рис. 2.6. Иерархия устройств при рекурсивном перечислении
Порядок загрузки драйверов
Ранее я уже упоминал о том, что наряду с функциональным драйвером устройство может обладать верхним и нижним фильтрующими драйверами. Информация о фильтрующих драйверах хранится в двух разделах реестра, связанных с устройством. Раздел устройства, содержащий информацию об экземпляре оборудования, может содержать параметры UpperFilters и LowerFilters, которые определяют фильтрующие драйверы для этого экземпляра. Также в реестре имеется отдельный раздел для класса, к которому принадлежит устройство. Скажем, мышь принадлежит к классу Mouse (наверное, вы бы и сами догадались). Раздел класса
Поиск и загрузка драйверов
59
тоже может содержать параметры UpperFilters и LowerFiIters. Они определяют фильтрующие драйверы, загружаемые системой для каждого устройства, принадлежащего классу.
В каком бы разделе ни находились параметры UpperFilters и LowerFilters, они относятся к типу REG_MULTI_SZ — это означает, что они могут содержать одну или несколько строк Юникода, завершенных нуль-символами.
ПРИМЕЧАНИЕ-------------------------------------------------------------------------
Windows 98/Ме не поддерживает тип параметров реестра REG_MULTI_SZ и не обладает полноценной поддержкой Юникода. В Windows 98/Ме параметры UpperFilters и LowerFilters относятся к типу REG_BINARY и содержат набор строк ANSI, завершенных нуль-символами, за которыми следует дополнительный завершающий нуль-символ.
В некоторых ситуациях может быть важно знать, в каком порядке система вызывает драйверы. Реальный процесс «загрузки» драйвера приводит к отображению образа кода на виртуальную память; порядок выполнения этой операции особого интереса не представляет. В тоже время, может быть интересно знать порядок вызовов функций AddDevice различных драйверов (рис. 2.7):
1.	Сначала система вызывает функции AddDevice всех нижних фильтрующих драйверов, указанных в разделе устройства, в порядке их следования в содержимом LowerFilters.
2.	Затем система вызывает функции AddDevice всех нижних фильтрующих драйверов, указанных в разделе класса, в порядке их следования в содержимом LowerFilters.
3.	Система вызывает AddDevice драйвера, определяемого параметром Service в разделе устройства (то есть функционального драйвера).
Верхние фильтры классов
Ж Верхние фильтры устройств
Ж Функциональные у драйверы
Нижние ” у фильтры классов
JZ Чижние у фильтры устройств
Рис. 2.7. Порядок вызовов AddDevice
60
Глава 2. Базовая структура драйвера WDM
4.	Сначала система вызывает функции AddDevice всех верхних фильтрующих драйверов, указанных в разделе устройства, в порядке их следования в содержимом UpperFilters.
5.	Система вызывает функции AddDevice всех верхних фильтрующих драйверов, указанных в разделе класса, в порядке их следования в содержимом UpperFilters.
Как объясняется позднее в этой главе, каждая функция AddDevice создает объект ядра DEVICEJDBJECT и включает его в стек, корнем которого является PDO. Следовательно, порядок вызовов AddDevice определяет порядок следования объектов устройств в стеке и в конечном счете — порядок получения IRP драйверами.
ПРИМЕЧАНИЕ------------------—----------—-----------------------------------—-----------
Возможно, вы заметили, что последовательность загрузки верхних и нижних фильтров, принадлежащих классу и устройству, не обеспечивает полноценной вложенности. Прежде чем я узнал факты, я полагал, что фильтры уровня устройств должны располагаться ближе к функциональному драйверу, чем фильтры уровня класса.
Последовательность передачи IRP
Формальная иерархия драйверов в WDM помогает организовать предсказуемую передачу IRP от одного драйвера к другому. Общий принцип показан на рис. 2.2: каждый раз, когда системе потребуется выполнить операцию с устройством, она отправляет пакет IRP верхнему фильтрующему драйверу в стеке. Этот драйвер решает, что делать дальше: обработать IRP, передать его на следующий уровень или сделать и то и другое. Каждый драйвер, получивший IRP, должен принять аналогичное решение. В конечном счете IRP может достигнуть драйвера шины в его роли PDO. Драйвер шины обычно не передает IRP дальше, хотя из рис. 2.6 вроде бы следует обратное. Вместо этого драйвер шины обычно завершает обработку IRP. В некоторых ситуациях драйвер шины передает тот же IRP в стек, в котором он играет роль FDO (стек родительского драйвера). В других ситуациях драйвер шины создает вторичный пакет IRP и передает его в стек родительского драйвера.
Приведу несколько примеров, которые помогут прояснить связь между объектами FiDO, FDO и PDO. В первом примере выполняется операция чтения с устройства, подключенного ко вторичной шине PCI, которая, в свою очередь, подключается к главной шине через мост PCI-PCI. Для простоты предположим, что устройство ассоциировано только с одним объектом FiDO, как показано на рис. 2.8. В дальнейших главах будет показано, что запрос на чтение преобразуется в пакет IRP с основным кодом функции IRP_MJ_READ. Такой запрос сначала передается верхнему объекту FiDO, а затем функциональному драйверу устройства (на рисунке соответствующий объект помечен FDOycTp). Функциональный драйвер напрямую обращается к HAL для выполнения своей работы, поэтому все остальные драйверы на схеме не увидят IRP).
Поиск и загрузка драйверов
61
КАК РЕАЛИЗОВАН СТЕК УСТРОЙСТВ--------------------------------------------------------------
Структура данных DEVICE_OBJECT будет описана немного позднее в этой главе. Закрытое1 поле Attached Device связывает объекты устройств в вертикальный стек. Начиная с PDO, каждый объект устройства указывает на объект, находящийся непосредственно над ним. Документированного обратного указателя не существует — драйверы должны самостоятельно отслеживать объекты нижнего уровня, (На самом деле loAttachDeviceToDeviceStack создает нижний указатель в структуре данных, полное объявление которой отсутствует в DDK. Было бы неразумно пытаться анализировать эту структуру и использовать ее, потому что она может измениться в любой момент.)
Поле AttachedDevice намеренно не документируется, потому что его корректное использование требует синхронизации с кодом, который может удалять объекты устройств из памяти. Нам с вами разрешено вызывать loGetAttachedDeviceReference для получения верхнего объекта устройства в конкретном стеке. Эта функция также увеличивает счетчик ссылок, благодаря чему предотвращается преждевременное удаление объекта из памяти. Если вы захотите добраться до PDO, отправьте своему устройству запрос IRP_MJ„PNP с дополнительным кодом функции IRP_MN_ QUERYJ3EVICE_RELATIONS и параметром Туре, равным TargetDeviceRelation. Драйвер PDO на этот запрос возвращает адрес PDO. Впрочем, существует гораздо более простое решение — запомнить адрес PDO при создании объекта устройства.
Аналогично, чтобы узнать, какой объект устройства находится уровнем ниже, следует сохранить указатель при первом включении объекта в стек. Поскольку каждый из драйверов в стеке обладает собственным неизвестным способом реализации указателей, направленных сверху вниз и используемых для диспетчеризации IRP, изменять стек устройств после создания нежелательно.
Устройство Вторичная шина Главная шина
Рис. 2.8. Порядок вызовов AddDevice
’ Opaque — речь идет о поле, которое объявлено в заголовках, но трогать которое программисту не положено.
62
Глава 2. Базовая структура драйвера WDM
На рис. 2.9 показана разновидность первого примера. Запрос на чтение обращен к устройству, подключенному к концентратору USB, который, в свою очередь, подключен к хостовому контроллеру. В этом случае полное дерево устройств содержит стеки устройства, концентратора и хостового контроллера. Пакет IRPJ4J_READ передается от FiDO функциональному драйверу, который затем отправляет один или несколько IRP другого типа на нижний уровень, своему объекту PDO. Драйвер PDO для устройства USB называется USBHUB.SYS, он пересылает IRP верхнему драйверу в стеке хостового контроллера, пропуская состоящий из двух драйверов сток концентратора USB (в середине рисунка).
Концентратор Хостовый Устройство	USB	контроллер
Рис. 2.9. Последовательность передачи запроса на чтение к устройству USB
Третий пример аналогичен первому, за одним исключением: пакет IRP содержит оповещение о том, будет ли дисковое устройство на шине PCI использоваться для хранения системного файла виртуальной памяти. Как будет показано в главе 6, это оповещение реализуется в виде запроса IRP_MJ_PNP с дополнительным кодом функции IRP_MN_DEVICEJJSAGE_NOTIFICATION. В этом случае драйвер FiDO передает запрос драйверу FDOyCTp, который запоминает это обстоятельство и передает пакет дальше по стеку, драйверу PDOyCTp. Это конкретное оповещение влияет на обработку других запросов ввода/выводд, отиоснщуж^ к РпР и управлению питанием, поэтому драйвер PDOyclp посылает аналогичное оповещение стеку, в котором находится РЭОШИН, как показано на рис. 2.10 (не все драйверы шин работают подобным образом, но драйвер шины PCI).
Две основные структуры данных
63
Устройство Вторичная шина Главная шина
Рис. 2.10. Последовательность передачи оповещения об использовании устройства
НАГЛЯДНОЕ ПРЕДСТАВЛЕНИЕ ДЕРЕВА УСТРОЙСТВ---------------------------------------------
Чтобы иерархия объектов устройств и драйверов стала более наглядной, стоит воспользоваться специальной программой. Для этой цели я написал утилиту DEWIEW, находящуюся в прилагаемых материалах. На рис. 2.11 и 2.12 показаны результаты, полученные при запуске DEWIEW для примера USB42 из главы 12 с подключением ко вторичному концентратору USB.
Для этого конкретного устройства используются только два объекта устройств. Объектом PDO управляет USBHUB.SYS, a FDO — USB42. На первом рисунке показана дополнительная информация о PDO.
Поэкспериментировав с DEWIEW в своей системе, вы начнете лучше представлять иерархию различных драйверов для вашей конфигурации оборудования.
Две основные структуры данных
В этом разделе описаны две основные структуры данных, связанные с драйверами WDM: объект драйвера и объект устройства. Объект драйвера представляет сам драйвер и содержит указатели на все функции драйвера, которые когда-лисю будут вызываться системой по ее усмотрению (для полноты картины следует упомянуть, что указатели на другие функции драйвера часто передаются в аргументах различных системных функций режима ядра). Объект устройства представляет конкретный экземпляр устройства и содержит данные, упрощающие управление этим устройством.
64
Глава 2. Базовая структура драйвера WDM
1*1
Rto | Fbo }
4DeviceWSBP0Q-3

DeWfe.. J
D&er.
Hags;
|usbHub
D0_8US.ENUMERATED_DEVICE
DO.POWER.PAGABLE
Device Details
Ch^asteristics;
Device Object Name-
Fl LE_D EVI UE.SE CUR E_C
instance Райх

EDO Service Name
|b$B42
 r Sscufity Infamwfcph: -Owner:
МетЙ^да-
HwdoedD
fecess Control Erfes:
Devic^Ofe^:
E^ryOne
SYSTEM Administrators RESTRICTED
jWaftwO^ISjo^jare
pSBWid^ZSPidJO^^He^OOOl
jvMMBci
FILE TRAVERSE fileZwrite.ea FILE READ EA FILE APPEND.DATA
□K
Cancel
Рис. 2.11. Информация DEWIEW по объекту PDO для USB42
The Answer Device
PDC FBQ
,\GLGB4L??®SB42
Ооуюё '
[FM^E>eviCEJUNmOWN
Driver:
pSB42
View... j
Rags:
DO,POWER_PAGABLE
ChaiactetKCK^
FILE_DEVICE_SECURE_OPEN
S&CLnity lrifovrn^^ •
Owner
praters
; Aceves Control Entries
Everyone-
SYSTEM Administrators RESTRICTED
SYNCHRONIZE READ.CONTROL
FILE WRITE ATTRIBUTES FILE.READ.ATTRIBUTES FILE.TRAVERSE FILE_WRITE_EA
FILE READ.EA
FILElAPPEND.DATA
f £anc&
AfcfeCv^ixte:
Рис. 2.12. Информация DEWIEW по объекту FDO для USB42
Две основные структуры данных
65
Объекты драйверов
Структура данных объекта драйвера используется I/O Manager для представления каждого драйвера устройства (рис. 2.13). Как и многие структуры данных, которые мы будем обсуждать, объект драйвера частично закрыт. Это означает, что нам с вами положено напрямую обращаться или изменять только отдельные поля этой структуры, хотя в заголовках DDK объявлена вся структура полностью. Закрытые поля объекта драйвера отмечены на рисунке серым фоном. Считайте их аналогами приватных и защищенных членов классов C++ (а доступные поля можно рассматривать как аналоги открытых членов классов).
В заголовочных файлах DDK объект драйвера, а также все остальные структуры данных режима ядра объявляются в определенном стиле, как показывает следующий фрагмент WDM.H:
yypedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
} DRIVERJBJECT, *PDRIVER_OBJECT;
В заголовке объявляется структура с именем типа DRIVERJDBJECT. Кроме того, для структуры объявляется тип указателя (PDRIVER_OBJECT) и назначается тег (_DRIVER_OBJECT). Такая схема объявления часто встречается в DDK; далее я не буду упоминать ее. В заголовочных файлах также объявляется небольшой набор имен типов (таких как CSHORT) для описания атомарных типов данных, используемых в режиме ядра. Например, CSHORT означает «короткое целое со знаком». Некоторые из этих имен перечислены в табл. 2.1.
Таблица 2.1. Стандартные имена типов для драйверов режима ядра
Имя типа	Описание
PVOID, PVOID64	Обобщенные указатели (стандартные и 64-разрядные)
NTAPI	Используется в объявлениях системных функций для принудительного использования конвенции вызова	stdcall в архитектурах 186
VOID	Эквивалент void
CHAR, PCHAR	8-разрядный символ и указатель на него (со знаком или без — в зависимости от конфигурации компилятора)
UCHAR, PUCHAR	8-разрядный символ без знака и указатель на него
SCHAR, PSCHAR	8-разрядный символ со знаком и указатель на него
SHORT, PSHORT	16-разрядное целое со знаком и указатель на него
•CSHORT	Короткое целое со знаком
□SHORT, PUSHORT	16-разрядное целое без знака и указатель на него
LONG, PLONG	32-разрядное целое со знаком и указатель на него
ULONG, PULONG	32-разрядное целое без знака и указатель на него
продолжение
66
Глава 2. Базовая структура драйвера WDM
Таблица 2.1 (продолжение)
Имя типа	Описание
WCHAR, PWSTR, PWCHAR PCWSTR	Символ или строка в расширенной кодировке (Юникод) Указатель на константную строку в Юникоде
NTSTATUS	Код состояния (соответствует длинному целому без знака)
LARGEJNTEGER ULARGEJNTEGER PSZ, PCSZ	64-разрядное целое со знаком 64-разрядное целое без знака Указатель на строку ASCIIZ (с однобайтовой кодировкой) или константную строку
BOOLEAN, PBOOLEAN	TRUE или FALSE (эквивалент UCHAR)
Type	х	Size
[
DeviceObject
Flags
Driverstart
DriverSize
DriverSection
DriverExtension
DriverName
HardwareDatabase
FastloDispatch
Driverlnit
DriverStartlo
DriverUnload
MajorFunction
Рис. 2.13. Структура данных DRIVER-OBJECT
Две основные структуры данных
67
ПРИМЕЧАНИЕ------------------------------------------------—---------------------
О 64-разрядных типах: имена типов в заголовочных файлах DDK позволяют авторам драйверов относительно легко компилировать один исходный код как для 32-, так и для 64~разрядных платформ Intel. Например, вместо того чтобы просто считать, что длинное целое и указатель имеют одинаковый размер, программист объявляет переменную LONG_PTR или ULONG_PTR. В такой переменной может храниться либо длинное целое (или длинное без знака), либо указатель. Кроме того, например, тип SIZE_T используется для объявления целого числа, разрядность которого соответствует разрядности указателя, — на 64-разрядной платформе вы получаете 64-разрядное целое число. Эти и другие 32/64-разрядные определения типов находятся в заголовочном файле DDK с именем BASETSD.H.
Давайте в общих чертах познакомимся с доступными полями структуры объекта драйвера.
Поле DeviceObject (PDEVICE_OBJECT) используется для создания связанного списка объектов устройств, по одному для каждого устройства, обслуживаемого драйвером. I/O Manager связывает объекты устройств и задает содержимое этого поля. Функция DriverUnload драйверов, не являющихся драйверами WDM, использует это поле для перебора списка объектов устройств с целью их удаления. Скорее всего, в драйверах WDM использовать это поле не потребуется.
Поле DriverExtension (PDRIVER_EXTENSION) указывает на небольшую подструктуру, в которой нам доступно только поле AddDevice (PDRIVER_ADD_DEVICE) (рис. 2.14). В AddDevice хранится указатель на функцию драйвера, создающую объекты устройств; эта функция играет очень важную роль, и мы подробно рассмотрим ее в разделе «Функция AddDevice» позднее в этой главе.
DriverObject
AddDevice
Count
ServiceKeyName
Рис. 2.14. Структура данных DRIVER-EXTENSION
В поле HardwareDatabase (PUNICODE_STRING) хранится строка с именем раздела данного устройства в базе данных реестра. Имя имеет вид \Registry\Machine\ Hardware\Description\System и определяет раздел реестра, содержащий информацию о выделенных ресурсах. Для драйверов WDM содержимое этого раздела интереса не представляет, потому что PnP Manager выделяет ресурсы автомати-’*ески. Имя раздела хранится в Юникоде (и вообще Юникод используется во
68
Глава 2. Базовая структура драйвера WDM
всех строковых данных режима ядра). Формат и использование структуры данных UNICODE_STRING рассматриваются в следующей главе.
Поле FastloDispatch (PFAST IG DISPATCH) ссылается на таблицу указателей на функции, экспортируемые драйверами файловой системы и сетевыми драйверами. Тема использования этих функций выходит за рамки книги. За дополнительной информацией о драйверах файловой системы обращайтесь к книге Раджива Нагара (Rajeev Nagar) «Windows NT File System Internals: A Developer’s Guide» (O’Reilly & Associates, 1997).
Поле DriverStartlo (PDRIVER_STARTIO) указывает на функцию драйвера, которая занимается обработкой запросов ввода/вывода, подготовленных I/O Manager. Очереди запросов вообще и использование этой функции в частности рассматриваются в главе 5.
Поле Driverllnload (PDRIVERJJNLOAD) указывает на функцию деинициализации драйвера. Позднее мы немного поговорим об этой функции в связи с DriverEntry, но в целом драйверу WDM, скорее всего, не придется выполнять сколько-нибудь значительную деинициализацию.
Поле MajorFunction (массив PDRIVERJDISPATCH) содержит таблицу указателей на функции драйвера, обрабатывающие каждые из 20+ типов запросов ввода/вывода. Как нетрудно предположить, эта таблица чрезвычайно важна, потому что она определяет путь запросов ввода/вывода к вашему программному коду.
Объекты устройств
На рис. 2.15 представлен формат объекта устройства. Закрытые поля обозначены серым фоном, как это делалось ранее при описании объектов драйверов. Вам как разработчику драйвера WDM предстоит создавать такие объекты вызовом loCreateDevice.
Поле Driverobject (PDRIVERJDBJЕСТ) указывает на объект с описанием драйвера связанного с этим объектом устройства -- как правило, это драйвер, который создал объект устройства вызовом loCreateDevice.
Поле NextDevice (PDEVICEJDBJECT) указывает на следующий объект устройства, принадлежащий тому же драйверу что и текущий объект. Оно объединяет в связанный список объекты устройств, начиная с объекта, указанного в поле DeviceObject. Вероятно, для драйверов WDM поле интереса не представляет. Кроме того, следует учитывать, что правильное использование этого указателя требует синхронизации с применением внутренней системной блокировки, недоступной для драйверов устройств.
Поле Currentlrp (PIRP) используется функциями Microsoft для работы с очередями IRP StartPacket и StartNextPacket для регистрации пакета IRP, последним отправленного функции Startlo. Драйверы WDM должны реализовывать очереди IRP самостоятельно (см. главу 5), поэтому для них это поле бесполезно.
Поле Flags (ULONG) содержит набор битовых флагов. Биты, доступные для разработчиков драйверов, перечислены в табл. 2.2.
Две основные структуры данных
69
Type	Size	(
I
ReferenceCount ।
DriverObject
NextDevice
AttatchedDevice
Currentlrp
Timer
Flags
Characteristics
DeviceExtension
DeviceType
StackSize	|
i
AHgnmentRequirement
i
Рис. 2.15. Структура данных DEVICE-OBJECT
Таблица 2.2. Флаги поля Flags в структуре данных DEVICE-OBJECT
Флаг	Описание
DO_BUFFERED_IO	Операции чтения и записи используют буферизацию
(системный буфер) при обращении к данным пользовательского режима
DO_EXCLUSIVE	В любой момент времени манипулятор устройства
может быть открыт только одним программным потоком
продолжение
70
Глава 2. Базовая структура драйвера WDM
Таблица 2.2 (продолжение)
Флаг	Описание
DO_DIRECT_IO	Операции чтения и записи используют прямой доступ (список дескрипторов памяти) при обращении к данным пользовательского режима
DO_DEVICE_INITIALIZING DO_POWER_PAGABLE	Объект устройства еще не инициализирован Запрос IRP_MJ_PNP должен обрабатываться на уровне PASSIVE_LEVEL
DCLPOWERJNRUSH	Включение питания устройства сопровождается большим броском тока
Поле Characteristics (ULONG) также содержит набор битовых флагов, описывающих различные дополнительные характеристики устройства (табл. 2.3). I/O Manager инициализирует эти флаги на основании аргумента loCreateDevice. Фильтрующие драйверы распространяют некоторые из них в верхнем направлении в стеке устройств (за информацией о распространении флагов обращайтесь к подробному описанию фильтрующих драйверов в главе 16).
Таблица 2.3. Флаги поля Characteristics в структуре данных DEVICEJDBJECT
Флаг	Описание
FILE_REMOVABLE_MEDIA FILE__READ_ONLY__DEVICE FILE_FLOPPY_DISKETTE FILEJ/VRITE„ONCE_MEDIA FILE_REMOTE_D EVICE	Устройство использует съемные носители Носитель поддерживает только чтение, но не запись Устройство является дисководом для гибких дисков Носитель допускает только однократную запись Доступ к устройству возможен через сетевое подключение
FILE_DEVICEJS_MOUNTED file_virtual_volume FILE_AUTOGENERATED_DEVICE_NAME FILE_DEVICE„SECURE_OPEN	Физический носитель присутствует в устройстве Устройство является виртуальным томом Имя устройства автоматически генерируется I/O Manager Принудительная проверка безопасности при открытии
Поле DeviceExtension (PVOID) указывает на структуру данных, определяемую авторОхМ драйвера и содержащую информацию о конкретном экземпляре устройства1. I/O Manager выделяет память для этой структуры, но ее имя и содержимое находятся полностью на вашем усмотрении. На практике структура часто объявляется с именем типа DEVICEJEXTENSION. Для обращения к ней через указатель на объект устройства (например, fdo) используется команда следующего вида:
PDEVICEJXTENSION pdx -
(PDEVICE_EXTENSION) fdo->DeviceExtension;
1 Далее — структура расширения устройства. — Примем. перев.
Функция DriverEntry
71
Структура расширения устройства располагается в памяти сразу же за объектом устройства — во всяком случае, сейчас. Однако было бы неправильно рассчитывать, что это условие будет выполняться всегда — тем более что вы всегда можете воспользоваться документированным методом перехода по указателю DeviceExtension.
Поле DeviceType (DEVICEJIYPE) содержит константу перечисляемого типа, описывающую тип устройства. I/O Manager инициализирует это поле на основании аргумента вызова loCreateDevice. Возможно, содержимое DeviceType будет представлять интерес для фильтрующих драйверов. На момент написания книги определено свыше 50 возможных значений этого поля. За полным списком обращайтесь к документации DDK (раздел «Specifying Device Types») в библиотеке MSDN.
В поле StackSize (CCHAR) хранится количество объектов устройств, начиная от текущего и вплоть до нижнего уровня PDO. По содержимому этого поля заинтересованные стороны определяют, сколько позиций в стеке следует создать для пакета IRP, который будет сначала отправлен драйверу устройства. Впрочем, драйверам WDM содержимое этого поля менять обычно не требуется, потому что вспомогательная функция, используемая для построения \стека устройств (loAttachDeviceToDeviceStack), делает это автоматически.
Поле AlignmentRequirement (ULONG) задает требования к выравниванию буферов данных, используемых запросами на чтение или запись в устройство. В файле WDM.H содержится набор констант, от FILE_BYTE_ALIGNMENT и FILE_WORD_ ALIGNMENT вплоть до FILE_512„BYTE_ALIGNMENT. Значения представляют собой степени 2 за вычетом 1. Например, константа FILE_64_BVTE„ALIGNMENT равна 0x3F.
Функция DriverEntry
В предыдущих разделах я упоминал, что PnP Manager загружает драйверы, необходимые для работы оборудования, и вызывает их функции AddDevice. Однако один драйвер может обслуживать нескольких похожих устройств, поэтому существует некоторая глобальная инициализация, которая должна выполняться драйвером только один раз при первой загрузке. За глобальную инициализацию отвечает функция DriverEntry:
extern "С" NTSTATUS DriveгEntry(IN PDRIVER_OBJECT DrlverObject.
IN PUNICODE_STRING ReglstryPath)
}
ПРИМЕЧАНИЕ---------------------------------------------------------------------
Главная точка входа драйвера режима ядра называется «DriverEntry», потому что сценарий сборки (при использовании стандартных процедур) сообщает компоновщику это имя в качестве точки входа. Лучше придерживаться этого предположения в своем коде (иначе придется вносить изменения в сценарий сборки, но зачем?).
72
Глава 2. Базовая структура драйвера WDM
ПРИМЕР КОДА------------------------------------------------------------------------
Пример драйвера STUPID поможет вам поэкспериментировать с концепциями, представленными в этой главе. STUPID реализует функции DriverEntry и AddDevice, и ничего более. Он очень похож на самый первый драйвер, написанный мной, когда я изучал программирование драйверов.
Прежде чем описывать код DriverEntry, следует упомянуть о некоторых особенностях самого прототипа функции. Скорее всего, вы и не подозревали (если вам не доводилось тщательно изучать параметры компилятора в сценарии сборки), что функции режима ядра и функции драйвера используют конвенцию вызова __stdcall при компиляции для компьютеров х86. Данный факт никак не
влияет на программирование, но вам следует учитывать его во время отладки. Я использовал директиву extern "С", поскольку я обычно оформляю свой код в компилируемые модули C++ — в основном для того, чтобы иметь возможность объявлять переменные где угодно, а не только за открывающей скобкой. Директива подавляет стандартное для C++ преобразование внешних имен, чтобы компоновщик мог найти эту функцию. Таким образом, в результате компиляции для х86 будет получена функция с внешним именем _DriverEntry@8.
Другая особенность прототипа DriverEntry — ключевые слова IN. IN и OUT — пышные названия, определяемые в DDK как пустые строки. По исходному замыслу, они должны служить целям документирования. При виде параметра IN программист должен сделать вывод, что в этом параметре функции передаются только входные данные. В параметре OUT функция возвращает выходные данные, а параметры IN OUT используются как для ввода, так и для вывода. К сожалению, использование этих ключевых слов в заголовочных файлах DDK не всегда соответствует здравому смыслу, и вообще от них не много проку. Приведу лишь один пример из многих: функция DriverEntry утверждает, что указатель DriverObject является входным (IN); действительно, указатель остается неизменным, зато объект, на который он ссылается, наверняка будет изменяться.
И последнее, на что я хочу обратить ваше внимание в этом прототипе, — в объявлении функции указано, что она возвращает значение типа NTSTATUS. В действительности NTSTATUS представляет собой обычное длинное целое, но вместо LONG лучше использовать синоним NTSTATUS, чтобы программа стала более понятной. Очень многие вспомогательные функции режима ядра возвращают коды состояния NTSTATUS; их список можно найти в заголовочном файле DDK NTSTATUS.Н. В следующей главе мы рассмотрим коды состояния более подробно, а пока просто запомните, что при завершении функция DriverEntry возвращает код состояния.
Обзор DriverEntry
В первом аргументе DriverEntry передается указатель на объект драйвера, представляющий ваш драйвер и прошедший минимальную инициализацию. Функция драйвера WDM DriverEntry должна завершить инициализацию объекта и вернуть управление. Драйверам, не являющимся драйверами WDM, необходимо
Функция DriverEntry
73
проделать большую дополнительную работу — они также должны обнаружить оборудование, за которое они отвечают, создать объекты устройства, представляющие это оборудование, и выполнить всю настройку и инициализацию, необходимую для полноценной работы оборудования. Относительно тяжелую работу по обнаружению и настройке для драйверов WDM выполняет автоматически РпР Manager (см. главу 6). Если вы захотите узнать, как проходит инициализация драйверов, не соответствующих модели WDM, обращайтесь к книгам Арта Бейкера (Art Baker) и Джерри Лозано (Jerry Lozano) «The Windows 2000 Device Driver Book» (Prentice Hall, 2d ed., 2001), а также Вискаролы (Vis-carola) и Мейсона (Mason) «Windows NT Device Driver Development» (Macmillan, 1998).
Во втором аргументе DriverEntry передается имя раздела службы1 в реестре. Эта строка существует временно — если вы собираетесь использовать ее в дальнейшем, скопируйте ее. В драйвере WDM я использовал эту строку только в процессе регистрации WMI (см. главу 10).
Главной задачей функции DriverEntry в драйвере WDM является заполнение всевозможных указателей на функции в объекте драйвера. Эти указатели сообщают операционной системе, где находятся функции, которые вы решили включить в свой драйвер-«контейнер». К их числу относятся следующие поля-указатели объекта драйвера:
Э Driverllnload — указатель на созданную вами процедуру деинициализации (зачистки). I/O Manager вызывает эту процедуру непосредственно перед выгрузкой драйвера. Даже если деинициализация не нужна, присутствие функции Driverllnload необходимо для динамической выгрузки драйвера.
Э DriverExtension->AddDevice — указатель на созданную вами функцию AddDevice. РпР Manager вызывает AddDevice один раз для каждого экземпляра оборудования, обслуживаемого драйвером. Так как функция AddDevice играет столь важную роль в работе драйверов WDM, ей посвящен следующий крупный раздел этой главы («Функция AddDevice»).
Э DriverStartlo — если драйвер использует стандартный метод формирования очередей запросов ввода/вывода, в этой переменной объекта драйвера сохраняется указатель на функцию Startlo. Если вы не понимаете, что такое «стандартный» метод формирования очередей, не беспокойтесь (во всяком случае, пока) — все станет ясно в главе 5. Там вы узнаете, что драйверы WDM не должны использовать этот метод.
Э MajorFu notion — I/O Manager инициализирует элементы вектора указателями на фиктивную диспетчерскую функцию, которая сообщает о неудачной обработке любого запроса. Вероятно, ваш драйвер должен обрабатывать некоторые типы IRP (в противном случае он не будет ни на что реагировать), поэтому по крайней мере некоторые из указателей должны ссылаться на ваши диспетчерские функции. IRP и диспетчерские функции подробно рассматриваются
См. раздел «Роль реестра» главы 15. — Примеч. перев.
74
Глава 2. Базовая структура драйвера WDM
в главе 5. А пока достаточно знать, что драйвер должен в обязательном порядке обрабатывать два типа IRP, но, скорее всего, он также будет обрабатывать несколько других типов.
Итак, практически полный код функции DriverEntry выглядит примерно так:
extern "С" NTSTATUS DriverEntryCIN PDRIVER-OBJECT Driverobject.
IN PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = DriverUnload;	// 1
Dr1ver0bject->Dr1verExtens1on->AddDevice = AddDevice;
Dr1verObject->MajorFunct1on[IRP_MJ_PNP] = DlspatchPnp;	// 2
DrlverObject->MajorFunct1on[IRP_MJ_POWER] = DlspatchPower;
DriverObject->MajorFunct1on[IRP_MJ_SYSTEM_CONTROL] =
DlspatchWml;
//3
servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool,	// 4
RegistryPath->Length + sizeof(WCHAR));
If ('servkey.Buffer)
return STATUSJNSUFFICIENT_RESOURCES;
servkey.MaxImumLength = Reg1stryPath->Length + slzeof(WCHAR);
RtlCopyUnicodeString(&servkey, RegistryPath);
servkey.Buffer[Reg1stryPath->Length/sizeof(WCHAR)j = 0;
return STATUS_SUCCESS;	// 5
1.	Эти две команды инициализируют указатели на функции, находящиеся в другом месте драйвера. Я решил присвоить им простые имена, свидетельствующие об их назначении: DriverUnload и AddDevice.
2.	Каждый драйвер WDM должен обрабатывать запросы ввода/вывода PNP, POWER и SYSTEM_CONTROL. Здесь назначаются диспетчерские функции для обработки этих запросов. Вместо константы IRP__MJ_SYSTEM_CONTROL в некоторых ранних версиях Windows ХР DDK использовалась константа IRP_ поэтому я присвоил своей диспетчерской функции имя DispatchWmi.
3.	На месте многоточия должен находиться код инициализации других указателей MajorFunction.
4.	Если вам когда-либо потребуется обратиться к разделу реестра из кода драйвера, в этой точке стоит создать копию RegistryPath. Предполагается, что глобальная переменная с именем servkey была где-то объявлена с типом UNICODE__STRING. Правила работы со строками Юникода описаны в следующей главе.
5.	Возвращение кода STATUS_SUCCESS является признаком успеха. При обнаружении каких-либо неполадок следует вернуть код ошибки из стандартного набора NTSTATUS.H или набора кодов, которые вы определяете самостоятельно. Числовое значение STATUS-SUCCESS равно 0.
Функция Add Device
75
ОБ ИМЕНАХ ФУНКЦИЙ-------------------------------------------------------------------------
Многие разработчики присваивают функциям своих драйверов имена, включающие имя драйвера. Например, вместо определения функций AddDevice и DriverUnload многие программисты определили бы имена Stupid_AddDevice и Stupid_DriverUnfoad. Мне говорили, что ранние версии отладчика Microsoft WinDbg заставляли программистов соблюдать это правило (возможно, против их воли), потому что в них было только одно глобальное пространство имен. В более поздних версиях отладчика ограничение отсутствовало, но в примерах драйверов из DDK используется именно такая схема формирования имен.
Я сторонник повторного использования кода, а вводить лишние символы не люблю. Мне кажется, что проще использовать функции с короткими именами, в точности совпадающими в разных проектах. Это позволяет скопировать блок кода из одного драйвера и вставить его в другой драйвер, обходясь без многочисленных переименований. Кроме того, при совпадении имен сравнение кода двух драйверов не загромождает результаты сравнения несущественными различиями в именах.
DriverUnload
Функция драйвера WDM DriverUnload «убирает мусор» после глобальной инициализации, которая могла быть выполнена функцией DriverEntry. На практике она почти ничего не делает. Впрочем, если в DriverEntry была скопирована строка RegistryPath, то DriverUnload может освобождать память, занимаемую копией:
VOID DriverUnloacl(PDRIVERJBJECT DriverObject) {
RtlFreeUnicodeStringC&servkey);
}
Если функция DriverEntry возвращает код ошибки, система не вызывает функцию DriverUnload. Следовательно, если выполнение DriverEntry до возврата кода ошибки приводит к каким-либо побочным эффектам, функция DriverEntry должна сама ликвидировать их.
Функция AddDevice
В предыдущем разделе я показал, как происходит инициализация драйвера WDM при его первой загрузке. На практике драйвер часто обслуживает более одного физического устройства. В архитектуре WDM у драйвера имеется специальная функция AddDevice, которую РпР Manager вызывает для каждого такого устройства. Основа функции выглядит так:
R’TSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
return STATUSJOMETHING; // Например,STATUS_SUCCESS
76
Глава 2. Базовая структура драйвера WDM
Аргумент DriverObject ссылается на объект драйвера, инициализированный функцией DriverEntry. Аргумент pdo содержит адрес объекта физического устройства в нижней части стека устройств (даже если под ним уже находятся фильтрующие драйверы).
Основной задачей AddDevice в функциональном драйвере является создание объекта устройства и его включение в стек, корнем которого является данный объект PDO. Последовательность действий выглядит так:
1.	Вызов loCreateDevice для создания объекта устройства и экземпляра вашего объекта расширения устройства.
2.	Регистрация одного или нескольких интерфейсов устройства, чтобы приложения знали о существовании устройства. Другой возможный вариант — присвоить имя объекту устройства и создать символическую ссылку.
3.	Инициализация объекта расширения устройства и поля Flags объекта устройства.
4.	Вызов loAttachDeviceToDeviceStack для включения нового объекта устройства в стек.
Далее приводятся более подробные описания каждого из этих этапов. В самом конце обсуждения приводится пример полного кода AddDevice.
ПРИМЕЧАНИЕ-------------------------------------------------------------------
Из приводимых далее фрагментов кода намеренно исключена вся обработка ошибок. Это сделано для того, чтобы лучше подчеркнуть нормальную последовательность действий в AddDevice. Не пытайтесь повторять этот стиль программирования в коммерческом продукте — а впрочем, это вы и так знаете. Обработка ошибок рассматривается в следующей главе. Во всех примерах в прилагаемых материалах реализована полноценная обработка ошибок.
Создание объекта устройства
Объект устройства создается вызовом функции loCreateDevice. Пример:
PDEVICE-OBJECT fdo;
NTSTATUS status = IoCreateDev1ce(Dr1verOoject,
sizeof(DEVICE_EXTENSION), NULL, FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN, FALSE, &fdo):
Первый аргумент (DriverObject) содержит то же значение, которое передавалось AddDevice в качестве первого параметра. Этот аргумент устанавливает связь между драйвером и новым объектом устройства, что позволяет I/O Manger посылать пакеты IRP, предназначенные для данного устройства. Второй аргумент определяет размер структуры расширения устройства. Как было сказано ранее в этой главе, I/O Manager выделяет указанный объем дополнительной памяти и инициализирует указатель DeviceExtension в объекте устройства ссылкой на нее.
Третий аргумент, который в данном примере равен NULL, может содержать адрес строки UNICODE_STRING с именем объекта устройства. Вопрос, стоит ли присваивать имя объекту устройства, и какое именно, оказывается на удивление непростым. Некоторые соображения по этому поводу изложены далее в разделе «Нужно ли присваивать имя объекту устройства?»
Функция AddDevice
77
Четвертый аргумент (FILE_DEVICE_UNKNOWN) определяет один из типов устройств, определенных в WDM.H. Какое бы значение ни было задано, оно может быть переопределено записью в разделе оборудования данного устройства или в разделе класса в системном реестре. Если переопределения присутствуют в обоих разделах, предпочтение отдается разделу оборудования. Для устройств, относящихся к одной из установленных категорий, в одном из этих мест должно быть задано правильное значение, потому что от него зависят некоторые аспекты взаимодействия драйвера с окружающей системой. В частности, тип устройства критически важен для правильного функционирования драйверов файловых систем, дисковых или ленточных устройств. Кроме того, от типа устройства зависят настройки безопасности объекта устройства, принятые по умолчанию.
Пятый аргумент (FILE_DEVICE_SECURE_OPEN) содержит флаги Characteristics для объекта устройства (см. табл. 2.3). В основном эти флаги относятся к запоминающим устройствам. Флаг FILE_AUTOGENERATED_DEVICE_NAME используется шиной и многофункциональными драйверами при создании PDO. Важная роль флага FILE__DEVICE_SECURE_OPEN будет описана позднее в этой главе, в разделе «Нужно ли присваивать имя объекту устройства?». Любое заданное значение может быть переопределено записью в разделе оборудования или разделе класса, соответствующем устройству. Если переопределения присутствуют в обоих разделах, предпочтение отдается разделу оборудования.
Шестой аргумент loCreateDevice (FALSE в моем примере) указывает, является ли устройством монопольным. Для монопольных устройств I/O Manager разрешает открыть обычными средствами только один манипулятор. Любое заданное значение может быть переопределено записью в разделе оборудования данного устройства или в разделе класса, соответствующем устройству. Если переопределения присутствуют в обоих разделах, предпочтение отдается разделу оборудования.
ПРИМЕЧАНИЕ----------------------------------------------------------------
Атрибут монопольное™ учитывается только для именованных объектов устройств, для которых выполняется запрос на открытие. Если вы следуете рекомендациям Microsoft для драйверов WDM, то не станете присваивать имена своим объектам устройств. В этом случае запросы на открытие будут выполняться для PDO, но PDO обычно не помечается как монопольный, потому что драйвер шины в общем случае не знает, должно ли ваше устройство обслуживаться в монопольном режиме. Объект PDO помечается как монопольный только в одном случае: при наличии переопределения Exclusive в разделе оборудования данного устройства или в подразделе Properties раздела класса. Таким образом, лучше постарайтесь избегать зависимости от атрибута монопольное™ — пусть обработчик IRP_MJ„CREATE отвергает запросы на открытие, которые нарушают установленные вами ограничения.
Последний аргумент (&fdo) указывает на область памяти, в которой функция loCreateDevice сохраняет адрес созданного объекта устройства.
Если вызов loCreateDevice по каким-то причинам завершается неудачей, функция возвращает код состояния и не изменяет указатель PDEVICEJDBJECT, заданный последним аргументом. Если же вызов завершается удачно, функция возвращает код успешного завершения и задает указатель PDEVICEJDBJECT. После
78
Глава 2, Базовая структура драйвера WDM
этого можно переходить к инициализации расширения объекта и выполнению другой работы, связанной с созданием нового объекта устройства. Если ошибка обнаруживается на более поздней стадии, освободите объект устройства и верните соответствующий код состояния. Соответствующий код может выглядеть примерно так:
NTSTATUS status = IoCreateDev1ce(...);
If (!NT_SUCCESS(status)) return status;
If ^обнаружена другая ошибка>)
{
loDeleteDevice(fdo);
return status;
}
Коды состояния NTSTATUS и макрос NT_SUCCESS рассматриваются в следующей главе.
Имена устройств
В Windows ХР управление многими внутренними структурами данных, включая объекты драйверов и устройств, о которых говорилось ранее, осуществляется централизованным администратором Object Manager. Дэвид Соломон (David Solomon) и Марк Руссинович (Mark Russinovich ) приводят довольно полное описание Object Manager и пространства имен в главе 3 «System Mechanisms» книги «Inside Windows 2000», 3rd ed. (Microsoft Press, 2000). Объекты обладают именами, которые организуются Object Manager в иерархическое пространство имен. На рис. 2.16 показан вид окна приложения DEWIEW с верхним уровнем иерархии имен. Объекты со значками в виде папок являются каталогами, содержащими подкаталоги и «обычные» объекты. Другие значки представляют обычные объекты. (Программа DEWIEW напоминает утилиту WINOBJ, находящуюся в каталоге BIN\WINNT Platform SDK. Однако WINOBJ не выдает информацию об объектах устройств и драйверах, поэтому я и написал DEWIEW.)
Имена объектов устройств обычно находятся в каталоге \Device. В Windows ХР имена устройств служат двум целям. Присваивание имени объекту устройства позволяет другим компонентам режима ядра находить его при помощи системных функций (таких как loGetDeviceObjectPointer). После успешного обнаружения объекта устройства они могут посылать ему IRP.
Вторая функция имен объектов устройства — возможность открытия манипуляторов устройства приложением для последующей отправки IRP. Приложение открывает манипулятор стандартной функцией API CreateFile, а для взаимодействия с драйвером использует функции Read File, WriteFile и DeviceloControl. Путь, указываемый приложением при открытии манипулятора устройства, начинается с префикса \\.\; он не является стандартным путем в формате UNC (Universal Naming Convention) вида C:\MYFILE.CPP или \\FRED\C-Drive\HISFILE.CPP. Во внутреннем формате I/O Manager преобразует этот префикс в \??\ перед тем, как
функция AddDevice
79
проводить поиск по имени. Чтобы имена в каталоге \?? могли связываться с объектами, находящимся в других местах (скажем, в каталоге \Device), в Object Manager предусмотрены объекты, называемые символическими ссылками.


I * Device Viewer
Не Нф
а ЕЗ \ ArcName Л СЗ BaseNamedObiects  О Callback h О Device Г~1 Driver 1+1 О FileSystem ЙО GLOBAL?? Cj Kernelobject? Ю KncwnDlls O NL5 • О] ObjectTypes СЗ RPC Control Г~1 Security «•: C3 Sessions r Q Windows Ready	..kjype	/_.... , AdditiopalAlciferrnation		 EJArcName	Directory Cj BaseNamedObjects	Directory О Callback	Directory О Device	Directory El Driver	Directory СЗ FileSystem	Directory E3 GLOBAL??	Directory OKemeJObjects	Directory E3 KnownDlls	Directory Cj NLS	Directory E3 ObjectTypes	Directory Cl R PC Control	D irectory E3 Security	Directory Cj S essicns	D irectory C3 Windows	Directory Dfs	D evice	D evice object for	\D fs /I DosD evrces	SymbolicLink	\?? fl EnorLogPort	Pod <3>Fat	Device	PDDforVFat FatCdrom	Device	Device ob|ect for	\FatCdrom _ FusApiPort	Port	ш ... LanmanServerAnnounceEvent Event ; J LsaAulhenticationPort	Port , NLAPrivatePort	WaitablePort _ NLAFublicPort	WaiCabtePort . NIsCacheMutant	Mutant <J>Ntfs	Device	PDD for □ registry	Key LjSAM_SERVICE_STARTED Event	H 11J	21
Рис. 2.16. Просмотр пространства имен в программе DEWIEW
Имя \?? имеет особый смысл в Windows. Встретив это имя, Object Manager сначала проводит поиск в части пространства имен ядра, локальной по отношению к сеансу текущего пользователя. Чтобы понять, как работает этот механизм, создайте два или более сеанса и запустите DEWIEW в одном из них. Откройте папку \Sessions — вы найдете в ней папки для каждого пользователя. Пример изображен на рис. 2.18. Если локальный поиск оказался безрезультатным, Object Manager проводит поиск в папке \GLOBAL??.
Символические ссылки
Символические ссылки немного напоминают ярлыки на рабочем столе: они тоже указывают на другой объект, который и является настоящим «центром внимания». В частности, в Windows ХР символические ссылки обеспечивают связывание начальной части имен в стиле MS-DOS с устройствами. На рис. 2.17 показана часть каталога \GLOBAL?? с несколькими символическими ссылками. Например, обратите внимание на то, что С и другие буквы дисков в схеме имен MS-DOS
80
Глава 2. Базовая структура драйвера WDM
в действительности представляют собой ссылки на объекты, имена которых находятся в каталоге \Device. Эти ссылки позволяют Object Manager обращаться к другим элементам пространства имен во время разбора имени. Таким образом, при вызове функции CreateFile с именем C:\MYFILE.CPP Object Manager выполняет следующие действия:
1.	Код режима ядра изначально видит имя \??\C:\MYFILE.CPP. Object Manager интерпретирует имя ?? как специальное обозначение каталога DosDevices для текущего сеанса (на рис. 2.18 этот каталог является одним из подкаталогов \Sessions\0\DosDevices),
2.	Object Manager не находит записи С: в сеансовом каталоге DosDevices и переходит по символической ссылке Global в каталог GLOBAL??.
3.	Object Manager ищет устройство С: в каталоге \GLOBAL??. По этому имени находится символическая ссылка, поэтому Object Manager формирует новый путь режима ядра \Device\HarddiskVolumel\MYFILE.CPP и приступает к его разбору.
4.	Работая с новым путем, Object Manager ищет запись Device в корневом каталоге и находит объект каталога.
1 * ре vice Ob ject Viewer			1	
Rle Help				
Cl ArcName Ф- О BaseNamedObjects  Сз Callback S' СП Device  О Driver Й CZ) FlleSystem			Name			 ‘	J Additional Information 			д! Global	SymbolicLink	\GLOBAL?? QD GLOBALROOT	SymbolicLink f $VDMLPT1	SymbolicLink	\Device\ParallelVdmO f A:	SymbolicLink	\Device\FloppyO !f ACPlttFixedButtontt28tdaba3... SymbolicLink	\Device\0000003e [£ ACPIttGenuinelntel_-_x86_F„. SymbolicLink	\Device\OOCOOD3a if' ACPIHGenumelnteL'_xS£_F... SymbolicLink	\Device\0000003b ig.| ACPI ttPN P0303t$4&2658dtL. SymbolicLink	\D evice\00000049 >| ACPIttPNP0401#4&2658dO.„ SymbolicLink	\Device\0000004d	: ACPIttPNP0501tt1#{4d36ea„ SymbolicLink	\Device\0000004b fc. ACPIttPNP050W1#{86e0d1... SymbolicLink	\Device\0000004b i A№l#PNP0501tt2ft{4d36ea.. SymbolicLink	\Device\0000004c > ACPIftPNP05D1tt2#{86eOd1... SymbolicLink	\Device\0000004c 'f ACPIttPNP0F13ti4&2658d0... SymbolicLink	\Device\0000004a if AUX	SymbolicLink	\DosDevices\C0M1 if C:	SymbolicLink	\Device\HarddiskVolume1	; If CdRomO	SymbolicLink	\Device\CdRomO If CMl8CHILD0000tt48c2ed4de.. SymbolicLink	\Device\GamePort If, COMI	SymbolicLink	\Device\SerialO !f COM2	SymbolicLink	\Device\Serial1 if, 0:	SymbolicLink	\Device\HarddiskVolume2 ^JDbgv	SymbolicLink	\Device\Dbgv DEWIEW	Device	Device object for \GLOBAL??\DEWIEW : If. DISPLAY1	SymbolicLink	\Device\VideoO If DISFLAY2	SymbolicLink	\Device\Videol > DISPLAYS	SymbolicLink	\DeviceWideo2 if DISPLAY4	SymbolicLink	\Device\Video3 If DISPLAYS	SymbolicLink	\Device\Video4 if DmConfig	SymbolicLink	\DeviceSDmControl\DmConfia	
Ф О	GLOBAL’?			
CD Kernelobjects CD KnownDlls  Ca nls Cj ObjectTypes  Cl RPC Control Cd Security EE G Sessions Э-Г1 Windows Ready				
Рис. 2.17. Каталог \GLOBAL?? с несколькими символическими ссылками
Функция AddDevice
81
5.	Object Manager ищет запись HarddiskVolumel в каталоге \Device и находит объект устройства с этим именем.
На этой стадии процесса Object Manager создает пакет IRP, который отправляется драйверу или драйверам устройства HarddiskVolumel. Обработка пакета в конечном счете приведет к тому, что один из драйверов файловой системы найдет и откроет дисковый файл. Описание работы драйверов файловых систем выходит за рамки книги, но врезка «Открытие файла на диске» дает некоторое представление о происходящем.
ОТКРЫТИЕ ФАЙЛА НА ДИСКЕ---------------------------------------------------------------
Процесс, происходящий при открытии дискового файла приложением, невероятно сложен. Продолжая пример, приведенный в тексте, драйвер HarddiskVolumel должен быть одним из драйверов файловой системы — например, NTFS.SYS, FASTFAT.SYS или CDFS.SYS. Как файловая система определяет, что конкретный дисковый том принадлежит ей, и инициализируется для работы с томом — само по себе сага воистину эпических масштабов. Впрочем, все это уже должно было произойти до того, как приложение смогло вызвать CreateFile с конкретной буквой тома в пути, поэтому мы проигнорируем этот процесс.
Драйвер файловой системы находит верхний объект устройства в стеке запоминающих устройств, включающем физическое дисковое устройство, на котором смонтирован том С. I/O Manager и драйвер файловой системы совместно управляют блоком параметров тома (VPB, Volume Parameter Block), связывающим стек запоминающих устройств со стеком томов файловой системы. Теоретически, драйвер файловой системы посылает IRP драйверу запоминающего устройства для чтения записей каталогов, среди которых проводится поиск при разборе пути, указанного при исходном вызове CreateFile. На практике файловая система обращается к администратору кэша ядра, который по возможности обслуживает запросы из кэша в памяти и производит рекурсивные вызовы драйвера файловой системы для заполнения буферов кэша. Предотвращение взаимных блокировок и обработка неожиданного демонтирования во время этой процедуры требуют героических усилий.
К счастью, VPB и другие осложнения, обусловленные работой драйверов файловой системы, актуальны лишь в драйверах запоминающих устройств. В книге эта тема более не рассматривается.
Если речь идет о таком имени устройства, как СОМ1, то пакет IRP будет в конечном счете получен драйвером устройства \Device\SerialO. Обработка запроса на открытие драйвером устройства определенно входит в рамки книги, и я буду подробно обсуждать эту тему в этой главе (раздел «Нужно ли присваивать имя объекту устройства?») и в главе 5, когда речь пойдет об обработке IRP в общем.
Программы пользовательского режима создают символические ссылки в локальном (сеансовом) пространстве имен при помощи функции DefineDosDevice, как в следующем примере (рис. 2.18):
BOOL okay = DefineDosDev1ce(DDD_RAW_TARGET_PATH, "barf", ”\\Device\\Beep");
Символические ссылки в драйверах WDM создаются функцией loCreate-SymbolicLink:
TOCreateSymbol1 сL1nk(11nkname. targname);
где linkname — имя создаваемой символической ссылки, a targname — имя, для которого создается ссылка. Кстати говоря, Object Manager не проверяет, является
82
Глава 2. Базовая структура драйвера WDM
ли targname именем существующего объекта; попытка обращения к объекту по ссылке на неопределенное имя просто приводит к ошибке. Если вы хотите, чтобы программа пользовательского режима могла заменить ссылку и перенаправить ее на другой объект, используйте функцию loCreateUnprotectedSyrnbolicLink.

File Help

ЧВ1
ГП ArcName
1$ О BaseNamedObjects
. Q) Callback
ж Qi Device
-ГЧ Driver
3? ГЧ FileSystem
Ий d global??
Ql Kernelobjects
ГЧ KnownDlls
Q NLS
- О OhiectTypes
ГЧ RPC Control
О Security
- РЧ Sessions
St&O
- DosDevices
T О ОООООООО-ОООООЗеЗ
S-Со ООООПООО-ООСООЗеб i+r-O OClOUOOOO-OOOOd97d +’ CO BNOLINKS
± CO Windows
Name
Qp Global zJbahf jf L:
j	, ^dftjonal In^grmaUon_	....	_______ъ
SymbolicLink \GlobaP?
SymbolicLmk ^Device\Beep
SymbolicLink \Device\LanmanRedffector\;L000000000000d97d\Concerto\c-drive
SymbolicLink \Device\LanrnanRedirector\jZ:000000000000d97d\dell\d-drive
Ready
Рис. 2.18. Символическая ссылка, созданная функцией DefineDosDevice
Аналог вызова DefineDosDevice в приведенном примере выглядит так:
UNICODE_STRING linkname;
UNICODE_STRING targname;
RtlInitllnicodeStrlng (&11nkname. L"\\DosDevices\\barf");
RtllnltUnicodeStrlngC&targname, L''\\Dev1ce\\Beep");
IoCreateSymbol1cLlnk(&11nkname, &targname);
Нужно ли присваивать имя объекту устройства?
Как уже говорилось ранее, вопрос о том, нужно ли присваивать имя объекту устройства, требует определенных размышлений. Если присвоить объекту имя, любая программа режима ядра сможет попытаться открыть манипулятор для вашего устройства. Более того, любая программа режима ядра или пользовательского режима сможет создать символическую ссылку на объект устройства
Функция AddDevice
83
и использовать ее для открытия манипулятора. Возможно, вы предпочтете запретить эти действия (а может, и нет).
Главным фактором в принятии решения о присваивании имени объекту устройства должна быть безопасность. Когда кто-то открывает манипулятор именованного объекта, Object Manager убеждается в том, что ему разрешено это делать. Когда функция loCreateDevice создает объект устройства, она присваивает дескриптор безопасности по умолчанию на основании типа устройства, указанного в четвертом аргументе. Для выбора дескриптора безопасности I/O Manager использует три базовые категории:
О Большинству объектов устройств файловой системы (диски, CD-ROM, файлы и ленты) назначаются права «публичного неограниченного доступа по умолчанию». Они ограничивают всех, за исключением администраторов и учетной записи System, правами SYNCHRONIZE, READ_CONTROL, FILE_READ_ATTRIBUTES и FILE_TRAVERSE. Кстати говоря, объекты устройств файловой системы существуют только как целевые объекты для вызовов CreateFile, открывающих манипуляторы файлов, находящихся под управлением файловой системы.
О Дисковым устройствам и сетевым объектам файловых систем назначаются те же права, что и объектам файловых систем, с некоторыми изменениями. Например, всем предоставляется полный доступ к именованному объекту устройства флоппи-дисковода, а администраторам предоставляются права, достаточные для запуска ScanDisk (DLL сетевых провайдеров пользовательского режима нуждаются в более высоком уровне доступа к объектам устройств соответствующих драйверов файловых систем, поэтому сетевые файловые системы рассматриваются отдельно от других файловых систем).
□ Всем остальным объектам устройств назначаются права «публичного открытого неограниченного доступа», которые позволяют каждому, у кого имеется манипулятор устройства, делать с ним практически все что угодно.
Получается, любой желающий может обратиться к не-дисковому устройству для чтения и записи, если при вызове loCreateDevice драйвер назначит имя объекту устройства. Дело в том. что схема безопасности по умолчанию разрешает почти полный доступ, а при создании символических ссылок проверки безопасности вообще не выполняются — безопасность проверяется при открытии на основании дескриптора безопасности именованного объекта. Это правило выполняется даже в том случае, если другие объекты устройств в том же стеке обладают более жесткими ограничениями.
ПРИМЕЧАНИЕ----------------------------------------------------------------------
Функция .NET DDK loCreateDeviceSecure позволяет задать дескриптор безопасности в ситуациях, когда в реестре отсутствуют переопределения. Однако эта функция появилась совсем недавно, -то не позволяет привести более полное описание.
Программа DEWIEW выводит атрибуты безопасности для отображаемых объектов устройств. Чтобы увидеть последствия применения описанной схемы безопасности по умолчанию, просмотрите информацию о файловой системе, диске или другом устройстве.
84
Глава 2. Базовая структура драйвера WDM
PDO тоже получает дескриптор безопасности по умолчанию, но его можно переопределить дескриптором безопасности, хранящимся в разделе оборудования или в подразделе Properties раздела класса. (Если переопределения присутствуют в обоих разделах, предпочтение отдается разделу оборудования.) Но даже при отсутствии явного переопределения уровня безопасности, если раздел оборудования либо подраздел Properties раздела класса переопределяет тип оборудования или спецификацию характеристик, I/O Manager конструирует новый дескриптор безопасности по умолчанию на основании нового типа. Тем не менее, I/O Manager не переопределяет настройки безопасности для каких-либо объектов устройств выше PDO. Соответственно, чтобы переопределения (и административные операции, которые их создали) возымели какой-либо эффект, не стоит присваивать имя объекту устройства. Впрочем, не отчаивайтесь — приложения все равно могут обращаться к вашему устройству через зарегистрированный интерфейс, о котором речь пойдет далее.
Остается упомянуть еще об одной проблеме безопасности. В процессе разбора имени объекта Object Manager нуждается только в доступе FILEJTRAVERSE к промежуточным компонентам имени. Полная проверка безопасности выполняется только с именем объекта, указанным в последнем компоненте. Допустим, имеется объект устройства, к которому можно обращаться по имени \Device\Beep или по символической ссылке \??\Barf. Приложение пользовательского режима, которое попытается открыть \\.\Barf для записи, будет заблокировано, если в атрибутах безопасности объекта запрещен доступ для записи. Но если приложение попытается открыть имя вида \\.\Barf\ExtraStuff, запрос на открытие пройдет весь путь до драйвера устройства (в форме запроса IRP„MJ_CREATE), если пользователь обладает минимальными разрешениями FILE_TRAVERSE, которые обычно предоставляются (более того, в большинстве систем даже сама возможность проверки разрешений FILE_TRAVERSE отключается). I/O Manager ожидает, что драйвер устройства сам обработает дополнительные компоненты имени и выполнит для них все необходимые проверки безопасности.
Чтобы избежать только что описанных проблем безопасности, можно передать флаг FILE_DEVICE_SECURE_OPEN в аргументе характеристик устройства при вызове loCreateDevice. При наличии этого флага Windows ХР проверяет, что вызывающая сторона имеет права на открытие манипулятора устройства даже при наличии в имени дополнительных компонентов.
Имя устройства
Если вы все же решите назначить имя объекту устройства, обычно такие имена размещаются в ветви \Device пространства имен. Чтобы присвоить объекту имя, следует создать структуру UNICODE_STRING для его хранения, а затем передать ее при вызове loCreateDevice:
UNICODE_STRING devname;
Rtl InitUm*codeStr 1 ng(&devname, L"WDeviceWSImpleO"): loCreateDevicetDriverObject. sizeof(DEVICE_EXTENSION). &devname, ...);
Использование функции RtilnitUnicodeString рассматривается в следующей главе.
Функция AddDevice
85
ПРИМЕЧАНИЕ-------------------------------------------------------------------------
Начиная с Windows ХР, регистр символов в именах устройств игнорируется. В Windows 98/Ме и Windows 2000 прописные и строчные буквы в именах различались. Если вы хотите, чтобы драйвер работал во всех системах, запишите \Device с точным соблюдением регистра символов. Также обратите внимание на правильное написание \DosDevices!
Обычно драйверы назначают имена своим объектам устройств, объединяя строку с описанием типа устройства ("Simple" в приведенном фрагменте) с целочисленным номером, обозначающим экземпляр типа (начиная с нуля). В общем случае жестко кодировать имена, как это сделал я, не рекомендуется — имя должно строиться динамически с применением строковых функций, как в следующем примере:
UNICODEJTRING devname;
static LONG lastindex = -1;
LONG devlndex = Interlockedlncrement(&last1ndex);
WCHAR name[32];
_snwprintf(name, arrayslze(name), L"\\Device\\SIMPLE£2.2d”, devlndex);
RtlInitUnlcodeStrlng(&devname, name);
IoCreateDev1ce(...);
Различные служебные функции, встречающиеся в этом фрагменте, рассматриваются в следующих двух главах. Номер экземпляра для приватных типов устройств также может храниться в статической переменной, как показано в примере.
ОБ ИМЕНАХ УСТРОЙСТВ--------------------------—--------------—--------------------------
Каталог \GLOBAL?? раньше назывался \DosDevices. Изменения были внесены для того, чтобы переместить часто просматриваемый каталог имен пользовательского режима в начало алфавитного списка каталогов. Windows 98/Ме не распознают имена \?? и \GLOBAL??.
Б Windows 2000 определяется символическая ссылка \DosDevices, указывающая на каталог \??. Windows ХР по-разному интерпретирует \DosDevices в зависимости от контекста процесса на момент создания объекта. Если объект (такой, как символическая ссылка) создается в системном программном потоке, то \DosDevices ссылается на \GLOBAL??, и вы получаете глобальное имя. Если объект создается в пользовательском потоке, то \DosDevices ссылается на \??, и вы получаете имя уровня сеанса. Как правило, драйверы устройств создают символические ссылки в функции AddDevice, выполняемой в системном потоке, поэтому простое размещение символической ссылки в \DosDevices приводит к созданию объектов в глобальном пространстве имен во всех средах WDM. Если символическая ссылка создается в другой момент, используйте \GLOBAL ?? в Windows ХР или \DosDevices — в более ранних системах. О том, как различать платформы WDM, рассказа--ов приложении А.
Для упрощения тестирования можно создать объект устройства в каталоге \DosDevices, как это делается во многих примерах в прилагаемых материалах. Тем не менее, в окончательной версии драйвера объект устройства должен создаваться в каталоге \Device — тем самым предотвращается создание объекта, который должен быть глобальным, в сеансовом пространстве имен.
Б предыдущих версиях Windows NT драйверы некоторых классов устройств (в первую очередь дисков, ленточных накопителей, последовательных и параллельных портов) вызывали функцию loGetConfigurationlnformation для получения указателя на глобальную таблицу со счетчиками устройств в каждом из специальных классов. Драйвер использовал текущее значение счетчика для “□строения имен вида HarddiskO, Tapel и т. д. и увеличивал счетчики. Драйверам WDM не нужна -и эта служебная функция, ни возвращаемая ею таблица. За конструирование имен устройств этих слассов теперь отвечает драйвер класса Microsoft для данного типа (например, DISK.SYS).
86
Глава 2. Базовая структура драйвера WDM
Интерфейсы устройств
Только что рассмотренный старый метод назначения имен назначение имени объекту устройства и создание символической ссылки для приложений — обладает двумя серьезными недостатками. Мы уже рассмотрели последствия присваивания имени объекту устройства с точки зрения безопасности. Кроме того, автор приложения, который желает обратиться к устройству, должен знать выбранную вами схему назначения имен. Если все приложения, работающие с вашим оборудованием, пишете только вы сами, особых проблем не будет. Но если несколько разных компаний пишут программное обеспечение для вашего оборудования, а особенно если аналогичные устройства производятся несколькими фирмами-производителями, сконструировать подходящую схему имен будет нелегко.
Для решения этих проблем в WDM появилась новая схема назначения имен — нейтральная по отношению к языку, легко расширяемая, подходящая для ситуаций с множеством производителей оборудования и программного обеспечения и легко документируемая. В основу этой схемы положена концепция интерфейса устройства, то есть, фактически, спецификации обращения к оборудованию со стороны программ. Интерфейс устройства однозначно идентифицируется 128-разрядным кодом GUID. Коды GUID генерируются утилитами UUIDGEN и GUIDGEN из Platform SDK — обе утилиты генерируют сходные числа, но выводят результаты в разных форматах. Допустим, некоторая отраслевая группа определяет стандартный механизм работы с неким типом оборудования. В процессе стандартизации кто-то запускает GUIDGEN, и полученный код GUID публикуется как идентификатор, который в дальнейшем навечно связывается с этим стандартом интерфейса.
ЧТО ТАКОЕ GUID---------------------------------------------------------------------—
Коды GUID, используемые для идентификации программных интерфейсов, относятся к той же категории, что и уникальные числовые коды, используемые в модели COM (Component Object Model) для идентификации COM-интерфейсов, и коды OSF (Open Software Foundation) DCE (Distributed Computing Environment) для идентификации приемника удаленного вызова процедур (RPC, Remote Procedure Call). В книге Крейга Брокшмидта (Kraig Brockschmidt) «Inside OLE», 2nd ed. (Microsoft Press, 1995), объясняется, как обеспечивается статистическая уникальность кодов GUID; там же приводится ссылка на исходную спецификацию алгоритма OSF. Я нашел соответствующую часть спецификации OSF в Интернете по адресу http://www.opengroup.org/onlinepubs/9629399/ apdxa.htm.
Механизм создания GUID для драйверов устройств подразумевает запуск UUIDGEN или GUIDGEN и сохранение полученного идентификатора в заголовочном файле. С GUIDGEN работать удобнее, потому что эта утилита позволяет выбрать формат GUID при помощи макроса DEFINE„GUID и скопировать результат в буфер обмена. На рис. 2.19 показано, как выглядит окно GUIDGEN. Результат вставки выходных данных в заголовочный файл выглядит примерно так:
// {CAF53C68-A94C-lld2-BB4A-00C04FA330A6}
ОЕРЩЕ-биТОСпаше1',
0xcaf53c68, Оха94с, 0xlld2. Oxbb, 0 x4а, 0x0. OxcO, Ox4f,
ОхаЗ, 0x30, Охаб):
Замените <<name>> более содержательным названием (скажем, GUID_DEVINTERFACE_SIMPLE) и включите определение в свой драйвер и в приложения.
Функция AddDevice
87
Create
Choose the desired format below,, then select “Copy” to copy the results to the clipboard (the results can then be pasted into your source code). Choose “Exit'1 when
GUID Format
J
New GUID
Exit
C 1. IMPLEMENT. OLECREATE(. J
<• 2. DEFINE_GUID(.„)
C 3. static const struct GUID -{...}
' 4. Registry Format (ie. {ххххихх-хххх... xxxx})
/7 {CAF53C68-A94C-11 d2BB4A-00C04FA330A6} DEF)NE_GUID(<<name>>,.
0xcaf53c68,0xa94c, 0x11 d2, Oxbb, 0x4a 0x0, OxcO 0x4f, 0xa3,0x30,
Рис 2-19. Генерирование кода GUID в программе GUIDGEN
Я представляю себе интерфейсы как аналоги белковых маркеров, заполняющих поверхность живых клеток. Приложение, желающее получить доступ к конкретному типу устройства, должно обладать собственными «белковыми маркерами», которые, словно ключ, подходят к маркерам соответствующих драйверов устройств (рис. 2.20).
Рис. 2.20. Использование интерфейсов драйверов для связывания приложений с устройствами
Регистрация интерфейса устройства
Функция AddDevice драйвера функции регистрирует один или несколько интерфейсов устройств, вызывая функцию loRegisterDevicelnterface:
88
Глава 2. Базовая структура драйвера WDM
include <1n1tguid.h>	// 1
^include "gulds.h"	// 2
NTSTATUS AddDevice(...)
{
loRegisterDevicelnterfacetpdo, &GUID_DEVINTERFACE_SIMPLE. // 3 NUlL, &pdx->ifname);
}
1.	В следующей строке будет включен заголовочный файл (GUID.H), содержащий один или несколько макросов DEFINE_GUID. Обычно DEFINE_GUID объявляет внешнюю переменную. Тем не менее, где-то в драйвере необходимо зарезервировать иницигишзированную область памяти для каждого GUID, на который мы собираемся ссылаться. Системный заголовочный файл INITGUID.H выполняет кое-какие препроцессорные фокусы, чтобы макрос DEFINE__GUID резервировал память даже в том случае, если его определение находится в одном из предварительно откомпилированных заголовочных файлов.
2.	Предполагается, что определения GUID, используемые в программе, размещаются в отдельном заголовочном файле. Это общепринятая практика — ведь эти определения также включаются в код пользовательского режима, и было бы неразумно создавать отдельный набор объявлений режима ядра, относящихся только к драйверу.
3.	Первый аргумент loRegisterDevicelnterface должен содержать адрес PDO устройства. Второй аргумент определяет код GUID, связанный с интерфейсом, а третий — дополнительные уточненные (qualified) имена, обеспечивающие дальнейшее структурное разбиение интерфейса. Схема разбиения используется только в коде Microsoft. Последний аргумент содержит адрес структуры UNICODE_STRING, в которую заносится имя символической ссылки, соответствующей объекту устройства.
Возвращаемое значение loRegisterDevicelnterface представляет собой строку в Юникоде, которую приложения могут использовать без какой-либо информации о внутреннем устройстве драйвера и при помощи которой они смогут открыть манипулятор устройства. Кстати говоря, выглядит эта строка довольно устрашающе — вот пример для одного из устройств в моих примерах: \\?\ROOT# UNKNOWN#0000#{b544b9a2-6995-lld3-81b5-00c04fa330a6}.
В действительности весь процесс регистрации сводится к созданию имени символической ссылки и его сохранению в реестре. Позднее, при обработке запроса Plug and Play IRP_MN_START„DEVICE (см. главу 7), интерфейс активизируется следующим вызовом loSetDevicelnterfaceState:
IoSetDev1ceInterfaceState(&pdx->1fname. TRUE);
Реагируя на этот вызов, I/O Manager создает фактический объект символической ссылки, указывающий на PDO вашего устройства. Позднее интерфейс
Функция AddDevice
89
деактивизируется парным вызовом функции (loSetDevicelnterfaceState с аргументом FALSE); при этом I/O Manager удаляет объект символической ссылки, сохраняя запись реестра с именем. Другими словами, имя остается постоянным и всегда ассоциируется с конкретным экземпляром вашего устройства, а объект символической ссылки создается и уничтожается.
Поскольку имя интерфейса в конечном итоге ссылается на PDO, возможность доступа к вашему устройству определяется дескриптором безопасности PDO. И это вполне логично, потому что в INF-файле, используемом при установке драйвера, вы управляете именно атрибутами безопасности PDO.
Перечисление интерфейсов устройств
Как код режима ядра, так и код пользовательского режима может идентифицировать все устройства, поддерживающие нужный интерфейс. Я объясню, как происходит перечисление всех устройств с конкретным интерфейсом в пользовательском режиме. Писать код перечисления так скучно, что я в конечном итоге написал класс C++, чтобы упростить себе работу. Вы найдете этот код в файлах DEVICELIST.CPP и DEVICELIST.H, входящих в состав примеров HIDFAKE и DEVPROP в главе 8. Эти файлы содержат объявление и реализацию класса CDeviceList, содержащего массив объектов CDeviceListEntry. Объявления этих двух классов выглядят так:
class CDeviceListEntry
{
public:
CDevIceLIstEntry(LPCTSTR linkname, LPCTSTR friendlyname);
CDevIceL1stEntry(){}
CStrlng m_lInkname;
CStrlng m_fr1endlyname;
}:
class CDeviceList
{
public:
CDev1ceL1st(const GUID& guld);
-CDeviceListО;
GUID m_gu1d;
CArray<CDev1ceListEntry, CDevIceListEntry&> m_11st; 1 nt InltlallzeO;
В этих классах задействованы класс CString и шаблон САггау, входящие в библиотеку MFC (Microsoft Foundation Classes). Конструкторы этих двух классов ограничиваются тривиальным копированием аргументов в переменные классов:
CDeviceList::CDevIceList(const GUID& guld)
m_gu1d = guld;
}
90
Глава 2. Базовая структура драйвера WDM
CDevi ceLIstEntry::CDevicellstEntry(LPCTSTR 1 inkname.
LPCTSTR friendlyname)
{
m_lInkname = 1 Inkname:
m_fr1endlyname = friendlyname;
}
Все самое интересное происходит в функции CDeviceList::Initialize. В общих чертах, эта функция перечисляет все устройства с поддержкой интерфейса, код GUID которого был передан конструктору. Для каждого такого устройства функция определяет «дружественное» имя, которое будет отображаться для рядового пользователя. Функция возвращает количество обнаруженных устройств. Ее код выглядит так:
int CDeviceList::In1tial1ze()
HDEVINFO Info = SetupD1GetClassDevs(&m_guid. NULL. NULL, // 1 DIGCFJRESENT DIGCFJNTERFACEDEVICE):
If (Info == INVALID_HANDLE_VALUE)
return 0;
SP_INTERFACE_DEVICE_DATA ifdata:
Ifdata.cbSIze = sizeof(ifdata);
DWORD devlndex:
for (devlndex =0:	//2
SetupDIEnumDevIcelnterfacesdnfo. NULL. &m_gu1d, devlndex, &1fdata); ++devindex)
{
DWORD needed;
SetupDiGetDevicelnterfaceDetail(info. &ifdata, NULL, 0,	// 3
Sneeded, NULL);
PSP_INTERFACE_DEVICE_DETAIL_DATA detail = (PSP_INTERFACE_DEVICE_DETAIL_DATA) ma Hoc (needed);
detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};
SetupDiGetDevicelnterfaceDetaiKinfo, &ifdata, detail, needed, NULL, &did));
TCHAR fname[256];	// 4
if (!SetupDiGetDeviceRegistryProperty(info, &did,
SPDRP_FRIENDLYNAME. NULL. (PBYTE) fname,
sizeof(fname), NULL)
&& !SetupDiGetDeviceRegistryProperty(info, &did.
SPDRP_DEVICEDESC.
NULL, (PBYTE) fname. sizeof(fname), NULL))
_tcsncpy(fname, detail->DevicePath, 256);
fname[255] = 0;
CDeviceListEntry e(detail->DevicePath, fname);	// 5
Функция AddDevice
91
free((PVOID) detail):
m_11st.Add(e):
SetupDi DestroyDevI celnfoil st(1nfo);
return m_list.GetSize();
}
1.	Команда открывает манипулятор перечисления, используемый для поиска всех устройств, зарегистрировавших интерфейсы с одинаковым кодом GUID.
2.	Циклический вызов SetupDiEnumDevicelnterfaces для поиска всех устройств.
3.	Из всех полученных сведений нас интересуют только подробное описание интерфейса и информация об экземпляре устройства. Подробное описание (detail) представляет собой обычное символическое имя устройства. Поскольку оно имеет переменную длину, мы вызываем SetupDiGetDevicelnterfaceDetail дважды. Первый вызов определяет длину буфера, а второй — читает имя.
4.	Получение пользовательского имени устройства из реестра (запрос FriendlyName или DeviceDesc).
5.	Создание временного экземпляра класса CDeviceListEntry с именем е на основании имени ссылки и дружественного имени.
ПРИМЕЧАНИЕ---------------------------------------------------------------------------
Возможно, вас интересует, откуда в реестре взялось дружественное имя устройства? INF-файл, исполь-?/емый при установке драйвера устройства (см. главу 15), может содержать секцию HW с описанием ’араметров реестра для устройства. Дружественное имя можно указать в одном из этих параметров, но учтите, что в этом случае все экземпляры устройства будут обладать одинаковыми именами. 3 примере MAKENAMES описан способ определения уникальных дружественных имен для каждого экземпляра, основанный на использовании DLL. Также можно написать вспомогательную устано-вс-чную DLL-библиотеку (coinstaller), которая будет определять уникальные дружественные имена. Кстати говоря, если дружественное имя не определено, большинство системных компонентов ис-хэльзует строку DeviceDesc в реестре. Содержимое строки берется из INF-файла и обычно описывает устройство по фирме-производителю и модели.
ITVMEP КОДА---------------------------------------------------------------------------
программа пользовательского режима DEVINTERFACE перечисляет все экземпляры всех известных GUID интерфейсов устройств в вашей системе. В частности, при помощи этого примера можно определить, по каким GUID необходимо выполнить перечисление, чтобы найти конкретное устройство.
Другая глобальная инициализация устройств
Возможно, в процессе выполнения AddDevice потребуется выполнить дополнительные действия по инициализации объекта устройства. Я опишу эти действия тол	шжжта, техк
их относительной важностью. Учтите, что фрагменты кода в этом разделе еще короче обычного — я привожу ровно столько кода общей функции AddDevice, чтобы создать контекст для поясняемых аспектов.
92
Глава 2. Базовая структура драйвера WDM
Инициализация расширения устройства
Содержимое структуры расширения устройства и работа с ней находятся полно-стью на вашей ответственности. Естественно, переменные, включаемые в структуру, зависят от особенностей оборудования и программирования устройства. Тем не менее, во многих драйверах в расширение устройства включается следующий набор атрибутов:
typedef struct _DEVICE_EXTENSION {	// 1
PDEVICE_OBJECT DeviceObject:	// 2
PDEVICE_OBJECT LowerDevIceObject;	// 3
PDEVICE_OBJECT Pdo;	// 4
UNICODE-STRING ifname;	// 5
IO_REMOVE_LOCK RemoveLock:	// 6
DEVSTATE devstate;	// 7
DEVSTATE prevstate; DEVICE_POWER_STATE devpower: SYSTEM_POWER_STATE syspower; DEVICE-CAPABILITIES devcaps;	// 8
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
1.	На мой взгляд, проще всего воспроизвести «официальную» схему объявления структур, используемую в DDK, поэтому я объявил расширение устройства как структуру с тегом, типом и именем указателя на тип.
2.	Вы уже знаете, что поиск структуры расширения устройства осуществляется по указателю DeviceExtension из объекта устройства. В некоторых ситуациях требуется выполнить обратную задачу — найти объект устройства по указателю на расширение. Дело в том, что наиболее логичным аргументом некоторых функций является объект расширения устройства (поскольку в нем хранится всевозможная информация об устройстве уровня экземпляра). По этой причине в расширение включается указатель Deviceobject.
3.	Как будет сказано через несколько абзацев, при вызове loAttachDeviceToDevice-Stack необходимо сохранить адрес объекта устройства, находящегося непосредственно под вашим объектом. Для хранения этого адреса создается поле LowerDeviceObject.
4.	Некоторым служебным функциям необходим адрес PDO вместо другого объекта, находящегося в более высокой позиции того же стека. Найти PDO всегда трудно, поэтому самый простой способ обеспечить потребности этих функций — сохранить адрес PDO в поле расширения устройства, инициализируемом во время вызова AddDevice.
5.	Какой бы метод (символическая ссылка или интерфейс устройства) не использовался для обращения к устройству, потребуется простой способ сохранения назначенного имени. В этом фрагменте объявляется поле ifname с типом строки Unicode, предназначенное для хранения имени интерфейса устройства. Если вы собираетесь использовать имя, основанное на символической ссылке,
функция AddDevice
93
вместо интерфейса устройства, будет логичнее присвоить этой переменной другое, более подходящее имя — например, iinkname.
6.	В главе 6 будет рассмотрена проблема синхронизации, возникающая при выборе момента для безопасного удаления объекта устройства вызовом loDeleteDevice. Решение проблемы основано на использовании объекта IO_REMOVE_LOCK, память для которого выделяется в расширении устройства. Функция AddDevice должна инициализировать этот объект.
7.	Вероятно, в объект расширения устройства следует включить переменные для отслеживания текущего состояния Plug and Play и текущего состояния энергопотребления устройства. Предполагается, что перечисление DEVSTATE было объявлено в другом месте заголовочного файла. О том, как использовать все эти переменные состояния, будет рассказано в следующих главах.
8.	Другие аспекты управления питанием связаны с сохранением параметров возможностей, инициализируемых системой посредством IRP. В своих примерах я сохраняют эти параметры в структуре devcaps в расширении устройства.
Команды инициализации в AddDevice (с выделением частей, относящихся к расширению устройства) выглядят так:
NTSTATUS AddDevice(...)
{
PDEVICE_OBJECT fdo;
IoCreateDev1ce(..., sizeof(DEVICE_EXTENSION), .... &fdo);
PDEVICEJXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; pdx->Dev1ce0bject = fdo;
pdx->Pdo = pdo;
IoInitial1zeRemoveLock(&pdx->ReiTOveLock. ...);
pdx->devstate = STOPPED;
pdx->devpower = PowerDevIceDO;
pdx->syspower = PowerSystemWorking;
loRegisterDevicelnterfacet..., &pdx->ifname);
pdx->LowerDeviceObject = loAttachDeviceToDeviceStackC...);
Встречающиеся в этом фрагменте STOPPED и DEVICEJEXTENSION определяются в одном из заголовочных файлов.
Инициализация объекта DPC по умолчанию
Многие устройства сообщают о завершении операций при помощи прерываний. Как будет показано в главе 7, существуют жесткие ограничения в отношении операций, которые могут выполняться в обработчиках прерываний (ISR, Interrupt Service Routine). В частности, обрабопикам прерываний не разрешается вызывать функцию (loCompleteRequest), сигнализирующую о завершении обработчика, но, скорее всего, именно это вам и потребуется сделать. Для обхода ограничений применяется отложенный вызов процедур (DPC). Объект устройства содержит
94
Глава 2. Базовая структура драйвера WDM
вспомогательный объект DPC, который может использоваться для планирования процедур DPC; этот объект должен инициализироваться вскоре после создания объекта устройства:
NTSTATUS AddDev1ce(...)
{
IoCreateDevice(...);
lolnitiallzeDpcRequest(fdo, DpcForlsr);
}
Задание маски выравнивания буфера
Устройства, пересылающие данные по каналам DMA, работают напрямую с буферами в памяти. HAL может потребовать, чтобы буферы, используемые в опера-циях DMA, выравнивались в памяти по определенным границам, а ваше устройство может установить еще более жесткие требования к выравниванию. Для выражения ограничений используется поле AlignmentRequirement объекта устройства — оно содержит битовую маску, равную разности между 1 и требуемой границей. Понижающее округление произвольного адреса до этой границы выполняется так:
PVOID address = ...;
SIZE_T ar = fdo ^AlignmentRequirement;
address = (PVOID) ((SIZEJ) address & -ar);
Повышающее округление адреса до следующей границы:
PVOID address = ...:
SIZE-T аг =* fdo->A11gnmentRequicement;
address *= (PVOID) (((SIZE_T) address + ar) & ~ar);
В этих двух фрагментах преобразование SIZE_T используется для преобразования указателя (который может быть 32- или 64-разрядным, в зависимости от платформы компиляции) в целое, разрядность которого достаточна для представления того же диапазона.
Функция loCreateDevice задает поле AlignmentRequirement нового объекта устройства в соответствии с требованиями HAL. Например, для чипов Intel х86 HAL не предъявляет требований к выравниванию, поэтому поле AlignmentRequirement изначально равно 0. Если устройство требует более жестких ограничений для буферов данных, с которыми она работает (например, при поддержке DMA с управлением шиной без участия процессора), переопределите значение по умолчанию. Пример:
if (MYDEVICE-ALIGNMENT - 1 > fdo->AlIgnmentRequIrement) fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;
Предполагается, что где-то в драйвере объявлена константа с именем MYDEVICE_ ALIGNMENT, которая равна степени 2 и представляет требуемое выравнивание буферов данных вашего устройства.
Функция AddDevice
95
Прочие объекты
Устройство также может использовать другие объекты, которые также должны инициализироваться во время работы AddDevice. К их числу могут относиться различные объекты синхронизации, якоря связных списков, списки буферов и т. д. Эти объекты, а также уместность их инициализации в процессе выполнения AddDevice будут рассматриваться в других частях книги.
Инициализация флагов устройства
Два битовых флага объекта устройства — DO_BUFFERED_IO и DO_DIRECT_IO — должны инициализироваться при выполнении AddDevice и никогда не изменяться в дальнейшем. Устанавливая один (и только один) из этих битов, вы раз и навсегда определяете, каким образом должна осуществляться работа с буферами, ггрелаваемыми из пользовательского режима в составе запросов на чтение и запись < в главе 7 я объясню, чем различаются два метода буферизации и в каких случаях выбирается тот или иной метод). Причина, по которой это важное решение д?лжно приниматься во время выполнения AddDevice, заключается в том, что все верхние фильтрующие драйверы, загружаемые в дальнейшем, копируют состояние флагов, а режим выполнения операций определяется значениями флагов верхнего объекта устройства. Если вы измените свое решение после загрузки фильтрующих драйверов, скорее всего, они не узнают о внесенных изменениях.
Два битовых флага в объекте устройства относятся к управлению питанием. В отличие от двух флагов буферизации, состояние этих флагов может изменяться в любое время. Более подробное описание будет приведено в главе 8, а пока ограничусь кратким обзором. Флаг DO_POWER_PAGABLE означает, что Power Manager должен отправлять запросы IRP_MJ„POWER на уровне запроса прерывания (IRQL, Interrupt Request Level) PASSIVE_LEVEL. (Если вы не понимаете некоторых концепций в приведенном предложении, не беспокойтесь — все они будут doдробно рассмотрены в последующих главах.) Флаг DO_POWER_INRUSH означает. что включение устройства сопровождается повышенным потреблением тока, гс» этому Power Manager должен проследить за тем, чтобы устройство не включалось одновременно с другими подобными устройствами.
Построение стека устройств
Каждый фильтрующий и функциональный драйвер несет ответственность за cv<троение стека объектов устройств, начиная с PDO и далее вверх. Ваша часть эд- и работы выполняется вызовом loAttachDeviceToDeviceStack:
'“STATUS AddDev1ce(..., PDEVICE_OBJECT pdo)
-DEVICE_OBJECT fdo;
loCreateDeviceC..., &fdo):
:dx->LowerDev1ceObject = loAttachDeviceToDeviceStack(fdo, pdo);
Первый аргумент loAttachDeviceToDeviceStack (fdo) содержит адрес созданного сюъекта устройства. Во втором аргументе передается адрес PDO (он же передается •о втором параметре AddDevice). Возвращаемое значение содержит адрес объекта
96
Г лава 2. Базовая структура драйвера WDM
устройства, находящегося непосредственно под вашим, — это может быть объект PDO или объект нижнего фильтра. В ситуации, изображенной на рис. 2.21, у устройства имеются три нижних фильтрующих драйвера. К моменту выполнения вашей функции AddDevice уже будут вызваны все три их функции AddDevice. Они создадут соответствующие объекты FiDO и свяжут их в стек, начинающийся с PDO. При вызове loAttachDeviceToDeviceStack вы получаете адрес верхнего объекта FiDO.
Рис. 2.21. Значение, возвращаемое функцией loAttachDeviceToDeviceStack
Нельзя исключать, что вызов loAttachDeviceToDeviceStack завершится неудачей, в этом случае функция вернет указатель NULL. Чтобы это произошло, физическое устройство должно быть удалено из системы как раз в тот момент, когда функция AddDevice выполняет свою работу, и РпР Manager придется обрабатывать последствия удаления на другом процессоре. Я не уверен, что даже этих условий достаточно для неудачи (предполагаю, что еще нижележащий драйвер должен забыть сбросить флаг DO_DEVICEJNITIALIZING). Обработка неудачного вызова сводится к деинициализации и возврату STATUS_DEVICE_REMOVED функцией AddDevice.
Сброс DO_DEVICE_INmALIZING
Работа AddDevice должна завершаться сбросом флага DO_DEVICE_INITIALIZING в объекте драйвера:
fdo->Flags &= ~DOJDEVICE_INITIALIZING;
Пока этот флаг установлен, I/O Manager откажется присоединять другие объекты устройств к вашему или открывать манипулятор вашего устройства. Флаг необходимо сбрасывать, потому что при создании объекта устройства этот флаг устанавливается. В предыдущих версиях Windows NT большинство драйверов
Функция AddDevice
97
создавало все свои объекты устройств во время выполнения DriverEntry. При возврате из DriverEntry I/O Manager автоматически перебирает список объектов устройств, связанных с объектом драйвера, и сбрасывает этот флаг. Но поскольку вы создаете свой объект устройства значительно позже выхода из DriverEntry, автоматический сброс флага не производится, и вам придется выполнить его самостоятельно.
Общая картина
Далее приводится полный код функции AddDevice, без проверки ошибок и комментариев. В него вошли все фрагменты, описанные в предыдущих подразделах:
MTSTATUS AddDevice(PDRIVER_OBJECT DriverObject.
PDEVICE_OBJECT pdo)
{
PDEVICE-OBJECT fdo;
NTSTATUS status = IoCreateDevice(Dr1verObject.
sizeof(DEVICE-EXTENSION), NULL, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE. &fdo);
PDEVICEJXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
IoRegisterDeviceInterface(pdo, &GUID_DEVINTERFACE_SIMPLE, NULL. &pdx->1fname);
pdx->DeviceObject = fdo;
pdx->Pdo = pdo;
IoIn1tial1zeRemoveLock(&pdx->RemoveLock, 0, 0, 0);
pdx->devstate = STOPPED;
pdx->devpower = PowerDeviceDO;
pdx->syspower = PowerSystemWorking;
lolnitializeDpcRequest(fdo. DpcForlsr);
If (MYDEVICE-ALIGNMENT - 1 > fdo->A11gnmentRequ1rement) fdo->AlIgnmentRequirement = MYDEVICE_ALIGNMENT - 1;
KeIn111a1izeSpinLock(&pdx->SomeSpi nLock);
KeInit1alizeEvent(&pdx->SomeEvc-nt, NotificationEvent. FALSE);
Initi al 1zeListHead(&pdx->SomeL1stAnchor);
fdo->Flags = DO_BUFFERED_IO	DO_POWER_PAGABLE;
pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo. pdo);
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
98
Глава 2. Базовая структура драйвера WDM
Проблемы совместимости с Windows 98/Ме
В Windows 98/Ме некоторые подробности, связанные с созданием объекта устройства и загрузкой драйвера, реализованы не так, как в Windows ХР. В этом разделе описаны некоторые различия, которые могут повлиять на работу драйвера. Некоторые из них уже упоминались ранее, но повторение не повредит.
Различия в вызове DriverEntry
Как я уже говорил, функции DriverEntry передается артумепт типа UNICODE_STRING с именем раздела службы для драйвера. В Windows ХР эта строка представляет собой полный путь в реестре вида \Registry\Machine\System\CurrentControlSet\ Services\xxr (где ххх — имя раздела службы для вашего драйвера). В Windows 98/Ме эта строка представляется в формате System\CurrentControlSet\ Ъ^у\с^\<класс>\<зкземпляр> (где <класс> — имя класса вашего устройства, а <экземпляр> — номер экземпляра вида 0000, определяющий конкретное устройство указанного класса). Впрочем, в обоих случаях для открытия раздела можно воспользоваться функцией ZwOpenKey.
DriverUnload
В Windows 98/Ме функция DriverUnload вызывается из функции loDeleteDevice, вызываемой в DriverEntry. Для вас это существенно, только если (1) ваша функция DriverEntry вызывает loCreateDevice, затем (2) решает вернуть код ошибки, после чего (3) производит «зачистку» вызовом loDeleteDevice.
Каталог \GLOBAL??
Windows 98/Ме не поддерживает имени каталога \GLOBAL??, поэтому символические ссылки следует помещать в каталог \DosDevices. Этот каталог также может использоваться в Windows ХР, потому что он представляет собой символическую ссылку на каталог \??, (виртуальное) содержимое которого включает \GLOBAL??.
Нереализованные типы устройств
В Windows 98 не поддерживается создание объектов запоминающих устройств. К этой категории относятся устройства с типами FILE_DEVICE_DISK, FILE_DEVICETAPE, FILE_DEVICE_CD_ROM и FILE_DEVICE_VIRTUAL_DISK. Вы можете вызвать функцию loCreateDevice, и она даже вернет код STATUS-SUCCESS, но при этом не создаст объект устройства и не изменит переменную PDEVICE_OBJECT, адрес которой передается в последнем аргументе.
Основные приемы программирования
В сущности, написание драйвера WDM является упражнением в области программотехники. Какие бы требования не предъявлял конкретный тип оборудования, программа складывается из различных стандартных элементов. В преды-2ущей главе была описана основная структура драйвера WDM с подробным рассмотрением двух элементов — DriverEntry и AddDevice. В этой главе рассматриваются еще более общие аспекты работы с многочисленными вспомогательными функциями режима ядра, предоставляемыми операционной системой в ваше распоряжение. Мы рассмотрим обработку ошибок, управление памятью и ра-•> «ту со структурами данных, реестром и файлами на диске, а также ряд других • -Зщих тем. Глава завершается кратким рассмотрением некоторых действий, уп--тающих отладку драйвера.
>еда программирования в режиме ядра
На рис. 3.1 изображены некоторые компоненты операционной системы Microsoft Windows ХР. Каждый компонент предоставляет ряд сервисных функций, имена ». -торых начинаются с двух- или трехбуквенного префикса:
Э .Администратор ввода/вывода I/O Manager (префикс 1о) содержит множество сервисных функций, используемых драйверами. Эти функции будут постоянно упоминаться в книге.
Э Модуль управления процессами (префикс Ps) создает программные потоки режима ядра и управляет ими. Рядовой драйвер WDM может использовать .независимые потоки для циклического опроса устройств, не способных генерировать прерывания, и для других целей.
□	Модуль управления памятью (префикс Мт) управляет таблицами страниц, «определяющими отображение виртуальных адресов на физическую память.
□	Модуль организационной поддержки (префикс Ех) обеспечивает сервис управления кучей (heap) и синхронизации. Функции управления кучей рассматриваются в этой главе, а функции синхронизации — в следующей.
100
Глава 3. Основные приемы программирования
Системный сервис					
I/O Manager	Управление процессами	Управление памятью	Object Manager	Монитор безопасности	Библиотека времени выполнения
		Организационная поддержка			Функция ZwXxx
	Ядро				
Уровень аппаратных абстракций (HAL)
Рис. 3-1. Обзор сервисных функций режима ядра
О Администратор объектов Object Manager (префикс Ob) обеспечивает централизованное управление многими объектами данных, используемыми во внутренней работе Windows ХР, Драйверы WDM пользуются услугами Object Manager для ведения счетчиков ссылок, которые предотвращают преждевременное уничтожение используемых объектов, а также для преобразования манипуляторов объектов в указатели на представляемые ими объекты.
О Монитор безопасности (префикс Se) позволяет драйверам файловой системы выполнять проверки безопасности. Как правило, к тому моменту, когда запрос ввода/вывода достигает драйвера WDM, проблемы безопасности уже решены, поэтому эти функции в книге рассматриваться не будут.
О Компонент, называемый библиотекой времени выполнения (префикс Rtl), содержит вспомогательные функции (например, функции для работы со списками и строками), которые могут использоваться драйверами вместо обычных библиотечных функций ANSI. В основном назначение этих функций понятно по их названиям, и даже если вы будете просто знать об их существовании, в целом у вас не возникнет трудностей с их использованием. Некоторые функции библиотеки времени выполнения будут описаны в этой главе.
О В Windows ХР «родной» API для вызовов из режима ядра реализуется в форме функций, имена которых начинаются с префикса Zw. В DDK документировано лишь небольшое подмножество функций ZwXxx, а именно функции для работы с реестром и файлами. Они также будут описаны в этой главе.
О В ядре Windows ХР (префикс Ке) выполняются все низкоуровневые операции по синхронизации работы программных потоков и процессоров. Функции КеХхх будут рассматриваться в следующей главе.
О На самом нижнем уровне операционной системы находится уровень аппаратных абстракций, или HAL (префикс Hal). Вся информация о работе физического оборудования компьютера с точки зрения операционной системы собрана
Среда программирования в режиме ядра
101
в HAL, HAL знает, как прерывания работают на конкретной платформе, умеет работать с устройствами через порты ввода/вывода и отображением на память и т. д. Вместо того чтобы работать с оборудованием напрямую, драйверы WDM вызывают функции HAL. Тем самым обеспечивается независимость драйвера от платформы и шины.
Стандартные функции библиотеки времени выполнения
Исторически архитекторы Windows NT решили, что драйверы не должны использовать библиотеки времени выполнения, поставляемые производителями компиляторов С. Одной из причин стало время: система Windows NT проектировалась тогда, когда еще не существовало стандарта ANSI для функций, входивших в стандартную библиотеку. Разработчиков компиляторов было много, у каждого были свои представления о том, что следует включить в библиотеку, и собственные стандарты качества. Другая причина состояла в том, что стандарт-:чые функции библиотеки времени выполнения иногда зависят от инициализации, выполняемой только в приложениях пользовательского режима и реализация которой иногда небезопасна по отношению к потокам или многопроцессорным системам.
В первом издании книги я предположил, что некоторые «стандартные» функции библиотеки времени выполнения, предназначенные для обработки строк, можно использовать в драйверах. Видимо, это был плохой совет, потому что . многих (включая меня самого!) возникают большие проблемы с их безопасным использованием. Действительно (по крайней мере, на момент написания этого абзаца), ядро экспортирует стандартные строковые функции, такие как strcpy, A'cscmp и strncpy. Но так как эти функции работают со строками, завершаемыми ?гсть-символами, при работе с ними слишком легко ошибиться. Вы абсолютно уверены в том, что для strcpy был выделен приемный буфер достаточно большого размера? Вы абсолютно уверены, что обе строки, сравниваемые функцией wesemp, свершаются нуль-символами до того, как они перейдут в отсутствующую страницу памяти? А вы знаете, что strncpy может не завершить нуль-символом присущую строку, если исходная строка длиннее приемной?
11з-за множества потенциальных проблем с функциями библиотеки времени выполнения Microsoft сейчас рекомендует использовать «безопасные» строковые функции, объявленные в NtStrsafe.h. Мы обсудим эти функции, а также некоторые стандартные функции для работы со строками и байтами, которые безопасно использовать в драйверах, позднее в этой главе.
Предупреждение о побочных эффектах
Многие «вспомогательные» функции, используемые в драйверах, определяются в виде макросов в заголовочных файлах DDK. Всех нас учили, что при передаче аргументов макросам следует избегать выражений, имеющих побочные эффекты (то есть приводящих к некоторому устойчивому изменению состояния
102
Глава 3. Основные приемы программирования
компьютера). Причина вполне очевидна: в результате макроподстановки аргумент может вызываться больше или меньше одного раза. Для примера возьмем следующий код:
int а = 2, Ь = 42, с: с - min(a++, b):
Какое значение будет иметь переменная с? Рассмотрим возможную реализацию min в виде макроса:
#define пгт(х.у) (С(х) < (у)) ? (х) : (у))
Если подставить а++ на место х, вы увидите, что результат равен 4, потому что выражение а++ выполняется дважды. Значение «функции» min будет равно 3 вместо предполагаемого 2, потому что оно будет получено при повторном вызове а++.
Нельзя предсказать заранее, в каких случаях DDK использует макрос, а в каких объявляет «настоящую» внешнюю функцию. Иногда какая-то сервисная функция реализуется в виде макроса на одних платформах и в виде вызова функции — на других. Более того, компания Microsoft может изменить свое решение в будущем. Соответственно, при программировании драйверов WDM необходимо соблюдать следующее правило:
Никогда не используйте выражения с побочными эффектами в качестве аргументов сервисных функций режима ядра.
Обработка ошибок
Человеку свойственно ошибаться, программисту свойственно разбираться с последствиями ошибок. В любой программе возникают исключительные состояния. Одни из них обусловлены ошибками программы — как в вашем собственном коде, так и в приложениях пользовательского режима, обращающихся к этому коду. Другие могут быть обусловлены повышенной загрузкой системы или состоянием оборудования в конкретный момент времени. Подобные исключительные ситуации, независимо от причины, требуют гибкой реакции со стороны нашего кода. В этом разделе рассматриваются три аспекта обработки ошибок: коды состояния, структурированная обработка ошибок и фатальные сбои. В общем случае вспомогательные функции режима ядра сообщают о непредвиденных ошибках, возвращая коды состояния, тогда как для сообщения об ожидаемых отклонениях от обычного хода программы возвращаются логические или числовые значения, отличные от формальных кодов состояния. Структурированная обработка ошибок предоставляет в распоряжение программиста стандартизированный механизм «зачистки» после действительно непредвиденных событий вроде разыменования недействительного указателя пользовательского режима, а также предотвращения системных сбоев, обычно следующих после таких событий. Фатальным сбоем называется катастрофический сбой, последствия которого могут быть исправлены только одним способом - перезагрузкой системы.
Обработка ошибок
103
оды состояния
Вспомогательные функции режима ядра (а также ваш код) обозначают успешное или неудачное завершение операции, возвращая код состояния вызывающей .тороне. Значение NTSTATUS представляет собой 32-разрядное целое, состоящее из нескольких полей, как показано на рис. 3.2. Старшие 2 бита обозначают степень «серьезности» состояния — успешное завершение, передача информации, предупреждение или ошибка. Смысл флага клиента объясняется далее. Код под-. истемы указывает, от какого системного компонента исходит сообщение; в сущ-чэсти, он позволяет разделить группы разработчиков в отношении присваивания числовых кодов ошибкам. Остаток кода состояния — 16 разрядов - описывает к знкретное состояние.
31 30 29 28 27	16 15	0
Sev	С	R	Код подсистемы	Код ошибки
------► Зарезервировано
—► Код клиента Степень серьезности
Рис. 3.2. Формат кода NTSTATUS
Всегда проверяйте коды состояния, возвращаемые функциями. В своих примерах я буду часто нарушать это правило, потому что включение всей необ-\ дпмой обработки ошибок часто скрывает пояснительные цели фрагмента. Н- ленитесь и не повторяйте этот стиль в своих примерах!
Если старший бит кода состояния равен 0, любые из остальных битов могут 'ьль установлены, а код является признаком успеха. А это означает, что для про-±<?ки успешного завершения операции никогда не следует сравнивать код с 0 — г Место этого воспользуйтесь макросом NT_SUCCESS:
'’STATUS status = SomeFunctlonf...);
(!NT_SUCCESS(status))
/ Обработка ошибки
Программист должен не только проверять коды состояния, полученные от вызываемых функций, но и возвращать коды тем функциям, которые обраща-гт.як его коду. В предыдущей главе рассматривались две функции драйверов --C^.erEntry и AddDevice. Обе функции определялись как возвращающие коды hTSTATUS. Как упоминалось ранее, для обозначения успешного завершения обеих рункций следует возвращать STATUS_SUCCESS. Если при выполнении функ-тч ' возникают проблемы, часто требуется вернуть соответствующий код со-ст яния; иногда его значение совпадает с тем кодом, который был получен вами st? вызове другой функции.
104
Глава 3. Основные приемы программирования
Для примера приведу начало функции AddDevice с реализованной обработкой ошибок:
NTSTATUS AddDevice! PDRIVER__OBJECT Driverobject, PDEVICE_OBJECT pdo) {
NTSTA7US status;
PDEVICE_OBJECT fdo:
status = IoCreateDevice(DriverObject, sizeof(DEVICE-EXTENSION), NULL, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &fdo):
If (!NT-SUCCESS(status))	// 1
{ KdPrint(("loCreateDevice failed - Шп", status));	// 2
return status: }
PDEVICE-EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; pdx->OeviceObject = fdo;
pdx->Pdo = pdo:
pdx->state = STOPPED;
IoInitializeRemoveLock(&pdx->RenioveLock, 0, 0, 0);	// 3
status = IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, // 4 &pdx->1fname):
If (!NT_SUCCESS(status)) {
KdPrint(C’loRegisterDevicelnterface failed - %X\n", status)): loDeleteDevice(fdo);
return status: }
}
1.	Если вызов loCreateDevice завершается неудачей, мы просто возвращаем полученный код состояния. Обратите внимание на использование макроса NT_ SUCCESS, упоминавшегося в тексте.
2.	Иногда (особенно в процессе отладки драйвера) бывает желательно выводить информацию обо всех выявленных ошибочных состояниях. Функция KdPrint будет подробно рассмотрена позднее в этой главе (раздел «Упрощение отладки»).
3.	Вызов функции loInitializeRemoveLock (см. главу 6) не может завершиться неудачей. Соответственно, проверять код состояния для нее не нужно. Большинство функций, объявляемых с типом VOID, относится к той же категории. Некоторые функции VOID могут сигнализировать о неудаче, инициируя исключения, но их поведение очень четко описано в DDK.
4.	Если вызов loRegisterDevicelnterface завершается неудачей, перед возвратом управления вызывающей стороне необходимо выполнить кое-какую зачистку, а именно: нужно уничтожить только что созданный объект устройства вызовом loDeleteDevice.
Конечно, ошибки при вызове функций не всегда должны приводить к возврату кода неудачи. Иногда ошибку можно просто проигнорировать. Например,
Обработка ошибок
105
в главе 8 я расскажу о запросах управления питанием с подтипом IRP_MN_POWER_ SEQUENCE, которые могут использоваться с целью оптимизации для предотвращения лишнего восстановления системы при включении питания. Использование этих запросов не только не является обязательным для вас, но и их реализация не является обязательной для драйвера шины. Следовательно, даже если запрос завершается неудачей, можно продолжить нормальное выполнение функции. Аналогичным образом игнорируются ошибки от loAllocateErrorLogEntry; даже если вам не удается включить описание ошибки в журнал ошибок, для работы кода это не критично.
Завершение обработки IRP кодом ошибки обычно приводит к возврату кода ошибки при вызове функций Win32 API в приложениях. Приложение может вызвать функцию GetLastError для определения причины неудачи. Если неудача при обработке IRP обозначается кодом состояния с установленным флагом клиента, GetLastError вернет именно этот код состояния. Если же флаг клиента в коде состояния равен 0 (это относится ко всем стандартным кодам состояния, определяемым Microsoft), GetLastError возвращает значение из файла WINERROR.H, входящего в Platform SDK. Соответствие между возвращаемыми значениями GetLastError и кодами состояния ядра документировано в статье Knowledge Base Ql 13996, «Mapping NT Status Error Codes to Win32 Error Codes». Соответствия между важнейшими кодами состояния представлены в табл. 3.1.
Таблица 3.1. Соответствие между стандартными кодами состояния в режиме ядра * в пользовательском режиме
Код состояния режима ядра	Код ошибки пользовательского режима
STATUS-SUCCESS	NO_ERROR (0)
STATUSJNVALID_PARAMETER	ERROR-INVALID-PARAMETER
STATU S_NO_SUCH_FILE	ERROR_FILE_NOT_FOUND
STATUS_ACCESS_DENIED	ERROR_ACCESS_DENIED
STATUS_INVALID_DEVICE_REQUEST	ERROR_INVALID_FUNCTION
ERROR_BUFFER_TOO-SMALL	ERROR_INSUFFICIENT_BUFFER
STATUS_DATA-ERROR	ERROR-CRC
Различия между ошибками и предупреждениями бывают весьма значительными. Например, если неудача при управляющей операции METHODJBUFFERED (см. главу 9) обозначается кодом STATUS__BUFFER_OVERFLOW (предупреждение), I О Manager копирует данные в буфер пользовательского режима. Если та же операция завершается неудачей с кодом STATUS_BUFFER_TOO„SMALL (ошибка), I О Manager никакие данные не копирует.
Структурированная обработка исключений
В операционных системах семейства Windows реализован метод обработки ис-зшюч игольных состояний, направленный на предотвращение возможных системных сбоев. Механизм структурированной обработки исключений тесно связан
106
Глава 3. Основные приемы программирования
с компиляторным генератором кода. Он позволяет легко организовать защиту секций программы — если в защищенной секции возникают какие-либо проблемы, управление передается обработчикам исключений.
Лишь немногие посетители моих семинаров были знакомы со структурированными исключениями, поэтому я намерен объяснить основные принципы этого механизма. Использование этих принципов позволяет создать более качественный, лучше защищенный код. Во многих ситуациях параметры, получаемые драйверами WDM, уже прошли тщательную проверку где-то в другом месте и не приведут к выдаче непредвиденных исключений. Таким образом, единственным стимулом для реализации возможностей, о которых речь пойдет в этом разделе, может оказаться хороший стиль программирования. Тем не менее, в общем случае рекомендуется всегда защищать прямые ссылки на виртуальную память пользовательского режима блоками структурированных исключений. Такие ссылки встречаются при прямом обращении к памяти, при вызове MmProbeAndLockPages, ProbeForRead п ProbeForWrite, а также, вероятно, в других ситуациях.
ПРИМЕЧАНИЕ-------------------------------------------------------------------
Применение механизма структурированной обработки исключений в драйверах WDM продемонстрировано в примере SEHTEST.
КОГДА ВОЗМОЖЕН ПЕРЕХВАТ ПРЕРЫВАНИЙ----------------------------------------------------------
Гэри Неббет (Gary Nebbett) выяснил, какие исключения могут перехватываться механизмом структурированной обработки исключений, и опубликовал свои результаты в Usenet несколько лет назад. Полученная им информация реализована в SEHTEST. Кратко говоря, возможен перехват следующих исключений, происходящих на уровне IRQL, меньшим либо равным DISPATCH_LEVEL (учтите, что некоторые исключения специфичны для процессоров Intel х86):
□	все исключения, обозначаемые ExRaiseStatus и другими аналогичными функциями;
□	попытки разыменования недействительных указателей в памяти пользовательского режима;
□	отладочные исключения и точки прерывания (breakpoints);
□	целочисленное переполнение (команда INTO);
□	недействительный код операции.
Учтите, что обращение по недействительному указателю режима ядра немедленно приводит к фатальному сбою и перехватываться не может. Исключение деления на 0 или исключение команды BOUND также приводит к фатальному сбою.
В программах режима ядра работа механизма структурированных исключений основана на создании кадров исключений в том же стеке, который используется для передачи аргументов, вызова функций и создания автоматических переменных. Ссылка на текущий кадр исключения хранится в специальном регистре процессора. В каждом кадре исключения содержится ссылка на предыдущий кадр. Когда происходит исключение, ядро ищет обработчик для него в списке кадров исключений. Поиск всегда успешен, потому что кадр исключения на вершине стека обрабатывает все необработанные ранее исключения. Обнаружив
Обработка ошибок
107
обработчик исключения, ядро производит параллельную раскрутку стеков исполнения и кадров исключений, последовательно вызывая обработчики для выполнения зачистки на промежуточных уровнях. После этого управление передается обработчику исключения.
В компиляторах Microsoft реализованы специфические расширения С/С ++, скрывающие некоторые сложности работы с низкоуровневыми примитивами операционной системы. Команда_____try обозначает составную команду как защищенный блок для кадра исключения; далее либо команда______finally назначает завершающий обработчик, либо команда _____________________except назначает обработчик ис-
ключения.
ПРИМЕЧАНИЕ------------------------------------------------------------------
Всегда лучше записывать слова_try,_finally и_except с начальными символами подчеркива-
ния. В модулях компиляции С заголовочный файл DDK WARNING.H определяет макросы try, finally и except как слова с символами подчеркивания. В примерах DDK имена макросов часто используются вместо имен с символами подчеркивания. Для вас такой подход может создать проблемы: □ единицах компиляции C++ команда try используется в сочетании с catch для активизации совершенно иного механизма обработки исключений, реализованного на уровне языка C++. Исключения C++ не работают в драйверах, если только вам каким-то образом не удастся продублировать часть инфраструктуры из библиотеки времени выполнения. С точки зрения Microsoft, делать этого стоит из-за увеличения объема драйвера и дополнительных затрат ресурсов, связанных с обработкой операции throw.
Блоки Try-Finally
Начать знакомство со структурированной обработкой исключений проще всего с описания блока try-finally, используемого для определения кода зачистки:
„try
{
защищенный фрагмент кода>
}
_flnally
{
завершающий обработчик
}
В этом фрагменте псевдокода защищенный фрагмент представляет собой табор команд и вызовов функций, выражающих основную идею программы. В общем случае выполнение этих команд приводит к некоторым побочным эффектам. При отсутствии побочных эффектов блоки try-finally не имеют особого < мыс ла. Завершающий обработчик содержит команды, частично или полностью । вменяющие побочные эффекты, которые могли остаться после выполнения защищенного фрагмента.
На семантическом уровне блок try-finally работает следующим образом: сначала компьютер выполняет защищенный фрагмент. Когда управление по каким-либо причинам передается за пределы защищенного фрагмента, компьютер выполняет завершающий обработчик (рис. 3.3).
108
Глава 3. Основные приемы программирования
Нормальное завершение, -leave, goto, return
. предыдущая команда> исключение
—try
защищенный фрагмент>
}
__finally
{
] завершающий обработчик |
}
* «следующая команда>
<раскрутка>
Рис. 3.3. Порядок передачи управления в блоке try-finally
Приведу простой пример:
LONG counter = 0:
_Jry
{
++counter;
___flrial ly
{
--counter;
}
KdPrint(("£d\n", counter)):
Сначала выполняется защищенный фрагмент, в котором значение счетчика увеличивается с 0 до 1. Когда управление «проходит» через закрывающую фигурную скобку в конце защищенного фрагмента, управление передается завершающему обработчику, который снова уменьшает счетчик до 0. Таким образом, выводимое значение будет равно 0.
Другой, более сложный вариант:
VOID RandomFunctionCPLONG pcounter)
{
_try
{
++*pcounter;
return;
_finally
{
--*pcounter;
}
В результате выполнения этой функции целое число, на которое ссылается указатель pcounter, не изменяется. Когда управление выходит за пределы защищенного фрагмента по любой причине (включая выполнение команды return или goto), выполняется обработчик исключения. В данном примере защищенный
Обработка ошибок
109
фрагмент увеличивает счетчик и возвращает управление. Код завершения счетчик уменьшает, и только после этого происходит выход из функции.
Последний пример должен окончательно закрепить идею блока try-finally:
static LONG counter = 0;
—try
++counter;
BadActorO;
}
__finally
{
—counter;
}
Предполагается, что мы вызываем функцию BadActor, в которой происходит некое исключение, приводящее к раскрутке стека. В процессе раскрутки стеков исполнения и исключений операционная система передает управление завершающему коду, который восстанавливает прежнее состояние счетчика. Затем система продолжает раскручивать стек, поэтому код, следующий за блоком_____finally,
выполняться не будет.
Блоки Try-Except
Другой способ использования структурированной обработки исключений основан на применении блока try-except:
—try
{
защищенный фра гмент>
}
__except(<фильтрующее выражение>)
{
<обработчик исключения> }
Защищенный фрагмент в блоке try-except содержит код, при выполнении которого может произойти исключение. Допустим, вы собираетесь вызвать сервисную функцию режима ядра вроде MmProbeAndLockPages, эта функция использует указатели, переданные из пользовательского режима, не проверяя их на действительность. А может быть, у вас есть другие причины. Так или иначе, если весь защищенный фрагмент будет выполнен без ошибок, управление продолжится с точки, следующей за кодом обработчика исключения. Этот сценарий представляет нормальное течение событий. Но если в вашем коде или в любой из вызванных функций произойдет исключение, операционная система начинает раскручивать стек исполнения, проверяя фильтрующие выражения в командах________except Ре-
зультат фильтрующего выражения представляет собой одну из трех величин: Э EXCEPTION_EXECUTE_HANDLER (числовое значение 1) — означает, что операционная система должна передать управление вашему обработчику исключения.
110
Глава 3. Основные приемы программирования
Если выполнение обработчика благополучно доходит до завершающей фигурной скобки, управление передается команде, следующей непосредственно за скобкой (в документации Platform SDK утверждалось, что управление возвращается в точку возникновения исключения, но это неверно);
О EXCEPTION_CONTINUE_SEARCH (числовое значение 0) — сообщает операционной системе, что ваш код не может обработать исключение. Система продолжает перебор стека в поисках другого обработчика. Если обработчик исключения не определен, происходит системный сбой;
О EXCEPTION-CONTINUEJzXECUTION (числовое значение -1) — приказывает операционной системе вернуть управление в точку, в которой возникло исключение. Этот тип будет подробно рассмотрен далее.
На рис. 3.4 изображены возможные пути передачи управления в блоке try-except.
предыдущая команда>
, л	<поиск обработчика>
Исключение
Нормальное завершение, Jeave, goto, return
—try
{
защищенный фрагмент>
} ' ___________________________________________
___ехерЦ<фильтрующее выражение>) Г"
{	I
<обработчик завершение «----------<р аскрутка до обработчика>
}
> ^следующая команда>
Рис- 3-4. Порядок передачи управления в блоке try-finally
Например, для защиты от получения недействительного указателя может применяться код следующего вида (см. пример SEHTEST в прилагаемых материалах):
PVOID р = (PVOID) 1;
__try
{
KdPrint(("About to generate exception^”));
ProbeForWr1te(p, 4, 4);
KdPr1nt(("You shouldn't see this messaged"));
}
_except(EXCEPTION_EXECUTE_HANDLER)
{
KdPrint(("Exception was caught\n”));
}
KdPr1nt(("Program kept control after exception^"));
Функция ProbeForWrite проверяет действительность области данных. В данном примере она выдаст исключение, потому что передаваемый аргумент-указатель не выровнен по границе 4 байт. Управление передается обработчику исключения. После его выполнения управление переходит к команде, следующей за обработчиком, и продолжается внутри программы.
Обработка ошибок
111
Если бы в предыдущем примере возвращалось значение EXCEPTION_CONTINUE_ SEARCH, операционная система продолжила бы раскрутку стека в поисках обработчика исключения. В этом случае не выполнялся бы ни сам обработчик, ни код, следующий за ним, либо управление было бы передано одному из обработчиков более высокого уровня, либо произошел бы сбой системы.
В режиме ядра не следует возвращать код EXCEPTION__CONTINUE_EXECUTION — у вас нет возможности изменить условия, вызвавшие исключение, чтобы повторная попытка стала возможной.
Помните, что механизм структурированных исключений не позволяет перехварывать математические исключения или страничные сбои, обусловленные разыменованием недействительных указателей режима ядра. Пишите свой код так, чтобы избежать возникновения таких исключений. С делением на 0 все достаточно очевидно — просто проверьте делитель, как это сделано в следующем примере:
JLONG numerator, denominator: // <== Полученные числа
JLONG quotient:
f (!denominator) обработка ошибки> else
quotient = numerator / denominator:
Но что делать с указателями, полученными из источника в другой части ядра? Hr существует функции, которая позволяла бы проверить действительность у картеля режима ядра. Просто соблюдайте следующее правило:
В общем случае можно доверять значениям, полученным от компонентов ре-жима ядра.
^УКАЗАТЕЛЯХ NULL------------------------------------------------------------------
уж мы коснулись темы недействительных указателей, обратите внимание на то, что указатель HULL (1) недействителен в пользовательском режиме Windows ХР и (2) абсолютно действителен Windows 98/Ме. Использование указателя NULL, прямое (*р) или косвенное (p->StructureMember), «вначает попытку обращения к содержимому первых байтов виртуальной памяти. В Windows ХР Э»о действие сопровождается перехватываемым (trappable) нарушением доступа.
|<ование указателя NULL в Windows 98/Ме само по себе не создает очевидных проблем, ы я потратил несколько дней на поиски ошибки, возникавшей из-за перезаписи по адресу 00С в системе Windows 95. В этом месте хранится вектор реального режима для точки пре-1 (INT 3). Его замена случайным значением не проявлялась до тех пор, пока какое-нибудь ^пользуемое приложение не выполняло команду INT 3, которая не перехватывалась отлад-Оистема передавала прерывание в реальный режим. Недействительный вектор преры-ередавал управление по адресу памяти, по которому следовали технически допустимые, ессмысленные команды, за которыми следовала недопустимая команда. Система «зависа-ia недействительного кода команды. Как видите, конечные симптомы сильно удалены (как ени, так и в пространстве) от случайной замены одного адреса памяти.
ешить другую проблему в Windows 98, я однажды установил отладочный драйвер для пе-изменений в первых 16 байтах виртуальной памяти. Драйвер пришлось удалить, потому '•ерехватывал обращения от огромного количества драйверов VxD (причем некоторые из надлежали Microsoft).
всегда проверяйте указатели перед использованием, если существует хотя бы малейшая ость того, что этот указатель равен NULL. Чтобы узнать о наличии такой вероятности, -•имательно читайте документацию и спецификации.
112
Глава 3. Основные приемы программирования
Это вовсе не означает, что вам не следует обильно и щедро оснащать свой код директивами ASSERT — пользуйтесь ими, пока многие тонкости других компонентов ядра останутся непонятными для вас. Я просто имею в виду, что код драйвера не следует отягощать лишней защитой от ошибок в других, хорошо протестированных, частях системы (если, конечно, вы не пытаетесь обойти обнаруженную ошибку).
Фильтрующие выражения
Возникает вопрос: как выполнить сколько-нибудь содержательный поиск или исправление ошибок, если все, что вам разрешается, — это проверка выражения, дающего одно из трех целочисленных значений? Можно объединить выражения при помощи оператора C/C++ «,» (запятая):
_except(ехрг-1, ... EXCEPTION_CONTINUE_SEARCH){}
Фактически, этот оператор отбрасывает все, что находится слева, а его результат равен результату вычисления правой части. Значение, остающееся после этой компьютерной версии игры в «стулья с музыкой»1 (всего с одним стулом), считается значением всего выражения.
Для реализации более сложной логики можно воспользоваться условным оператором C/C++:
__except(<выражение>
? EXCEPTION_EXECUTE_HANDLER
: EXCEPTION-CONTINUE-SEARCH)
Если выражение истинно, выполняется ваш обработчик. В противном случае вы приказываете операционной системе продолжить поиск другого обработчика, расположенного выше в стеке.
Наконец, существует еще одно очевидное решение: написать функцию, которая возвращает одно из значений EXCEPTION_Axx:
LONG EvaluateExceptlonO
If (<some-expr>)
return EXCEPTION_EXECUTE_HANDLER:
else
return EXCEPTION_CONTINUE_SEARCH;
}
__except(EvaluateExceptlon ())
Любой из этих форматов выражения принесет пользу лишь при наличии дополнительной информации об исключении. Существуют две функции, которые
1 Детская игра (дети бросаются занимать стулья, которых на один меньше, чем играющих); см. http://en.wikipedia.org/wiki/Musical_chairs. — Примеч. перев.
Обработка ошибок
113
можно вызвать при обработке выражения________except для получения необходимой
информации. Внутренние реализации обеих функций обеспечиваются компилятором Microsoft, а их вызов возможен только в указанные моменты времени: Э Функция GetExceptionCode() возвращает числовой код текущего исключения.
Полученное значение относится к типу NTSTATUS, и при желании его можно сравнить с константами из ntstatus.h. Функция может вызываться в выражениях ___except и в коде обработчиков исключений, следующих за______except.
Э Функция GetExceptionInformation() возвращает адрес структуры EXCEPTION-POINTERS, содержащей всю подробную информацию об исключении, месте его возникновения, содержимом регистров и т. д. Функция доступна только внутри выражений________except.
ЛМЕЧАНИЕ------------------------------------------------------------------------
Имена, встречающиеся в блоках try-except и try-finally, подчиняются стандартным правилам области видимости языка C/C++. В частности, переменные, объявленные в области видимости составной команды, следующей за_try, не будут видны в фильтрующем выражении, обработчике исключе-
ния или завершающем обработчике. Обратные утверждения, которые можно встретить в Platform SOK или MSDN, неверны. Насколько мне известно, кадр стека, содержащий все локальные переменные, объявленные в области видимости защищенного фрагмента, продолжает существовать во время обработки фильтрующего выражения. Таким образом, указатель (объявленный во внешней области видимости) на переменную, объявленную в защищенном фрагменте, может быть безопасно разыменован в фильтрующем выражении.
Вследствие ограничений, наложенных на использование этих двух выражений в программе, они часто используются при вызове внешней функции, как в следующем примере:
LONG EvaluateException(NTSTATUS status, PEXCEPTION_POINTERS xp)
_except (Eva 1 uateExcept 1 on (Get Except 1 onCode ().
GetExceptlonlnformatlon()))
Инициирование исключений
Исключения, приводящие в действие механизм структурированной обработки, могут происходить (непреднамеренно) в результате ошибок программы. Прикладным программистам знакома функция Win32 API RaiseException, которая позволяет генерировать пользовательские исключения. В драйверах WDM для этой цели используются функции, перечисленные в табл. 3.2. Я не собираюсь приводить конкретные примеры использования этих функций из-за следующего правила:
Исключения не должны инициироваться в контексте произвольного потока. Иначе говоря, вы должны знать, какой обработчик исключения находится на более высоком уровне, и вообще хорошо понимать, что вы делаете.
114
Глава 3. Основные приемы программирования
В частности, исключения не следует применять для передачи вызывающими сторонами информации, полученной в ходе нормального выполнения. Гораздо лучше вернуть код состояния, хотя такой код читается гораздо хуже. Избегайте исключений, потому что механизм раскрутки стека обходится очень дорого. Даже создание кадров исключений требует довольно заметных затрат, и по возможности их следует избегать.
Таблица 3.2. Сервисные функции для инициирования исключений
Функция	Описание
ExRaiseStatus	Инициирует исключение	с заданным кодом состояния
ExRaiseAccessViolation	Инициирует исключение	STATUS_ACCESS_VIOLATION
ExRaiseDatatypeMisalignment Инициирует исключение	STATUS„DATATYPE MISALIGNMENT
Реальные примеры
Невзирая на расходы, связанные с созданием и уничтожением кадров исключений, иногда в обычных драйверах приходится применять синтаксис структурированной обработки исключений.
Одна из ситуаций, при которых приходится назначать обработчик исключения, возникает при вызове функции MmProbeAndLockPages для блокировки страниц созданной вами таблицы дескрипторов памяти (MDL, Memory Descriptor List):
PMDL mdl = MmCreateMdl(...);
__try
{
MmProbeAndLockPages(mdl. ...);
}
_except(EXCEPTION_EXECUTE_HANDLER)
{
NTSTATUS status = GetExceptionCodeO;
loFreeMdl(mdl);
return CompleteRequestUrp, status, 0);
}
(Вспомогательная функция CompleteRequest используется для выполнения технических задач завершения запросов ввода/вывода. В главе 5 рассказано о запросах ввода/вывода и о том, что такое «завершение запроса»).
Другая ситуация для применения обработчиков исключений возникает при обращениях к памяти пользовательского режима по указателю, полученному из ненадежного источника. В следующем примере предполагается, что указатель р был получен из программы пользовательского режима и ссылается на целое число:
PLONG р; // Получено из кода пользовательского режима
_try
{
ProbeForRead(p, 4, 4);
LONG х = *р:
М*вботка ошибок
115
, _except(EXCEPTION_EXECUTE_HANDLER)
{
NtSTATUS status = GetExceptionCodeO •:
1
детальные сбои
Неисправимые ошибки в режиме ядра могут проявляться в виде так называемых «синих экранов смерти» (BSOD, Blue Screen of Death), отлично знакомых э ем разработчикам драйверов. Пример изображен на рис. 3.5 (рисунок подго-тч влен вручную, потому что при возникновении фатальной ошибки программы с хранения экрана уже не работают!). Для диагностики фатальных сбоев исполь-г ггся сервисная функция KeBugCheckEx. Главная особенность фатального сбоя заключается в том, что система по возможности организованно переводит себя з нерабочее состояние и отображает «синий экран». После появления «синего ?• рана» система «висит» и требует перезагрузки.
A problem Май Ье^п detcited and. w indons has heenshut down to Prevent damage to your cojnyulex.
11 this is the first time you've seen this Stuperror scxeen, ^restart your computei. ц this screen appeal^ again, follow these steps:
Chech to mafce stire any new hardware or software is properly installed. iT.this is a new installation, ash, your hardware ox software manufacturer for any Hamlows updates you might need.
If problems continue, disable Qi remove any newly installed, hardware ox software. Disable felOS memory options such as caching or shadowing. If you need to Use Softj Mode to remove ox disable	one nt a, restart
your computer, press t*J to select Advanced Startup Options, and then select Safe Mode.
technical information:
*** StOP: 0x0tj001214 (0x00000000,0x00000001,0x00000002,Qxa0U0G0«3>
Contact your system adminstiatoi ox technical support group for further assistance.
Рис. 3.5. «Синий экран смерти»
Функция KeBugCheckEx вызывается следующим образом:
KeBugCheckExtbugcode, Infol. 1nfo2, 1nfo3, 1nfo4);
me bugcode — числовой код, идентифицирующий причину ошибки, a infol, info2 л т. д. — целочисленные параметры, выводимые на «синем экране» для передачи программисту дополнительной информации об ошибке. Возврат из функции не происходит (!).
116
Глава 3. Основные приемы программирования
Разработчик вряд ли найдет полезную информацию на «синем экране». Если повезет, в выведенной информации будет присутствовать смещение команды внутри драйвера. Позднее вы сможете проанализировать соответствующий адрес в отладчике режима ядра и, возможно, вычислите вероятную причину фатального сбоя. Коды фатальных сбоев, определяемые Microsoft, находятся в bugcodes.h (один из заголовочных файлов DDK); более полное описание кодов и их различных параметров приведено в статье Knowledge Base QI03059, «Descriptions of Bug Codes for Windows NT». Статью можно найти в библиотеке MSDN и в других источниках.
ПРИМЕЧАНИЕ--------------------------------------------------------------------
Вызов KeBugCheckEx продемонстрирован в примере BUGCHECK. Я воспользовался этим примером для получения экрана, показанного на рис. 3.5.
Разумеется, при желании вы можете определять собственные коды фатальных сбоев. Значения Microsoft представляют собой простые целые числа от 1 (APC_INDEX_MISMATCH) до (в настоящий момент) 0xF6 (PCI_VERIFIER_DETECTED_ VIOLATION), а также ряд других значений. Чтобы создать собственный код фатального сбоя, определите целочисленную константу так, как если бы она была кодом состояния STATUS-SEVERITY-SUCCESS, но передайте либо флаг клиента, либо ненулевой код подсистемы:
#define MY_BUGCHECK_CODE Ох002А0001
KeBugCheckEx(MY_BUGCHECK_CODEt 0. О, 0, 0);
Ненулевой код подсистемы (42 в этом примере) или флаг клиента (здесь я его оставил равным 0) позволяют отличить ваши коды от кодов, используемых Microsoft.
Теперь вы умеете выдавать собственные «синие экраны смерти», и я скажу, когда это следует делать: никогда. Или в крайнем случае — только в отладочных сборках вашего драйвера, предназначенных исключительно для внутренней проверки. Вряд ли нам с вами доведется написать драйвер, обнаруживающий ошибку настолько серьезную, что единственным выходом из ситуации будет перезагрузка системы. Гораздо лучше зарегистрировать ошибку в журнале (с использованием средств, описанных в главе 14) и вернуть код состояния.
Учтите, что пользователь может настроить поведение KeBugCheckEx в дополнительных настройках значка Мой компьютер, выбирая между автоматическим перезапуском компьютера и выдачей «синего экрана». Также выбирается уровень детализации дампа и запись события в системном журнале.
Управление памятью
Этот раздел посвящен теме управления памятью. В Windows ХР существуют несколько схем деления виртуального адресного пространства. Первая схема — чрезвычайно жесткая, основанная на факторах безопасности и целостности систе
Управление памятью
117
мы. — различает адреса пользовательского режима и адреса режима ядра. В другой схеме, почти (но не полностью) параллельной первой, память делится на перемещаемую и неперемещаемую. Все адреса пользовательского режима и некоторые адреса режима ядра относятся к страницам, которые Memory Manager выгружает на диск и загружает с него, а некоторые адреса режима ядра всегда относятся к страницам, находящимся в физической памяти. Поскольку Windows ХР допускает выгрузку отдельных частей драйверов, я объясню, как управлять возможностью выгрузки драйвера в момент его построения и на стадии выполнения.
Windows ХР поддерживает несколько способов управления памятью. Я опишу две базовые функции — ExAllocatePoolWithTag и ExFreePool, используемые для выделения и освобождения блоков произвольного размера в куче. Также будут описаны примитивы, предназначенные для организации блоков памяти в связанные списки структур. Раздел завершается описанием концепции резервного списка1 (look-aside list), обеспечивающей эффективное выделение и освобождение блоков одинакового размера.
дресные пространства пользовательского ежима и режима ядра
Системы Windows ХР и Microsoft Windows 98/Ме работают на компьютерах с доддержкой виртуального адресного пространства. Виртуальные адреса отображаются либо на физическую память, либо (по крайней мере на концептуальном уровне) на страничные блоки в файле подкачки на диске. Сильно упрощая, можно представить себе адресное пространство разделенным на две части: часть режима ядра и часть пользовательского режима, как показано на рис. 3.6.
Рис. 3.6. Составляющие адресного пространства
' В русскоязычной литературе также встречается крайне неудачный перевод «ассоциативный список». — Примем. перев.
118
Глава 3. Основные приемы программирования
Каждый процесс пользовательского режима обладает собственным адресным контекстом, то есть отображением виртуальных адресов пользовательского режима на уникальную совокупность физических страничных блоков. Другими словами, смысл виртуального адреса изменяется по мере того, как планировщик Windows ХР переключается с потоков одного процесса на потоки другого процесса. Одной из стадий переключения потоков является замена таблиц страниц, используемых процессором, и приведение их в соответствие с контекстом процесса нового потока.
В общем случае маловероятно, чтобы драйвер WDM выполнялся в одном контексте потока с инициатором запроса ввода/вывода. Если мы не можем точно указать, какому процессу принадлежит текущий адресный контекст пользовательского режима, говорят, что выполнение ведется в контексте произвольного потока. В контексте произвольного потока мы попросту не можем использовать виртуальные адреса, принадлежащие пользовательскому режиму, потому что мы понятия не имеем, какому физическому адресу они могут соответствовать. Из-за этой неопределенности в драйверах обычно соблюдается следующее правило:
Никогда (или почти никогда) не выполняйте прямое обращение к памяти пользовательского режима.
Другими словами, не пытайтесь использовать адреса, полученные от приложений пользовательского режима, как указатели, для которых возможно прямое разыменование. Далее в книге будут рассмотрены некоторые приемы обращения к буферам данных, находящимся в памяти пользовательского режима. А пока дос -таточно запомнить, что при любых обращениях к памяти компьютера вам всегда (почти) придется иметь дело с виртуальными адресами пользовательского режима.
О размере страницы
В системах с поддержкой виртуальной памяти операционная система делит физическую память и содержимое файла подкачки на страничные блоки одинакового размера. В драйверах WDM для получения размера страницы можно воспользоваться константой PAGE_SIZE. На некоторых компьютерах с Windows ХР размер страницы составляет 4096 байт, в других случаях размер страницы равен 8192 байтам. Сопутствующая константа PAGE_SHIFT выражает размер страницы в виде степени 2. Иначе говоря,
PAGE_SIZE == 1 « PAGE_SHIFT
На уровне препроцессора определен ряд макросов, упрощающих работу с размером страницы:
О ROUND_TO_PAGES — округляет размер в байтах до ближайшей верхней границы страницы. Например, на компьютере с 4-килобайтными страницами результат ROUND_TO_PAGES(1) равен 4096.
О BYTES_TO_PAGES — определяет, сколько полных страниц потребуется для хранения заданного количества байтов. Например, результат BYTES„TO_PAGES(42) равен 1 на всех платформах, а результат BYTES_TO_PAGES(5000) на одних платформах равен 2, а па других — 1.
Управление памятью
119
О BYTE_OFFSET — возвращает смещение в байтах для виртуального адреса. Другими словами, макрос определяет, насколько заданный адрес удален от ближайшей нижней границы страницы. На компьютере с 4-килобайтными страницами результат BYTE_OFFSET(Ox12345678) равен 0x678.
□	PAGE_ALIGN — округляет виртуальный адрес до границы страницы. На компьютере с 4-килобайтными страницами результат PAGE_ALIGN(0xl2345678) равен 0x12345000.
□	ADDRESS_AND_SIZE_TO_SPAN_PAGES — возвращает количество страничных блоков, занимаемых указанным количеством байтов, начиная с заданного виртуального адреса. Например, результат ADDRESS_AND_SIZE_TO_SPAN_PAGES(Ox 12345FFF, 2) равен 2 на компьютере с 4-килобайтными страницами, потому что заданный 2-байтовый промежуток пересекает границу страницы.
Перемещаемая и неперемещаемая память
Основной целью систем виртуальной памяти является возможность создания виртуального адресного пространства, значительно превышающего объем физической памяти компьютера. Для достижения этой цели администратор памяти (Memory Manager) выгружает страничные блоки из физической памяти и загружает их обратно по мере надобности. Некоторые компоненты операционной системы выгружаться не могут, потому что они необходимы для обеспечения работы самого администратора памяти. Самый очевидный пример компонентов, которые должны постоянно находиться в памяти, — код обработки страничных сбоев (исключений, возникающих при обращениях к страницам, в данный момент отсутствующим в физической памяти) и структуры данных, используемые обработчиками страничных сбоев.
Категория кода, который должен постоянно оставаться резидентным, отнюдь не ограничивается обработчиками страничных сбоев. В Windows ХР аппаратные прерывания могут происходить практически в любой момент, в том числе и при обработке страничного сбоя. В противном случае обработчик страничных сбоев не мог бы читать и записывать страницы устройства, использующего прерывание. Следовательно, все обработчики аппаратных прерываний также должны находиться в неперемещаемой (то есть невыгружаемой) памяти. Разработчики архитектуры Windows NT решили дополнительно расширить категорию непере-мсщаемого кода, используя простое правило:
Код, выполняемый на уровне IRQL (Interrupt Request Level) DISPATCH LEVEL и выше, не может инициировать страничные сбои.
Смысл этого правила подробнее объясняется в следующей главе.
Чтобы упростить поиск нарушений этого правила в отладочной сборке вашего драйвера, можно воспользоваться препроцессорным макросом PAGED_CODE (объявленным в wdm.h). Например:
‘.^STATUS D1spatchPower(PDEVICEJDBJECT fdo, PIRP Irp)
i
DAGED_CODE()•
120
Глава 3. Основные приемы программирования
Макрос PAGEDJ20DE содержит команды условной компиляции. В среде отладочной сборки он выводит сообщение и генерирует сбой нарушения условия, если текущий уровень IRQL слишком высок. В среде свободной сборки он не делает ничего. Чтобы понять, зачем нужен PAGED_CODE, представьте, что функция DispatchPower, которая по каким-то причинам должна находиться в иепе-ремещаемой памяти, была ошибочно размещена в выгружаемой памяти. Если система вызовет DispatchPower в тот момент, когда страница отсутствует в памяти, произойдет страничный сбой, за которым последует фатальное нарушение работы системы. Код фатального сбоя будет малосодержательным (IRQL_NOT_ LESSJ3R_EQUAL или DRIVER_IRQL_NOT_LESS„OR_EQUAL), но по крайней мере вы будете знать о возникшей проблеме. Но если драйвер тестируется в ситуации, когда страница с DispatchPower благополучно находится в памяти, страничного сбоя не произойдет. Макрос PAGEDJ20DE поможет выявить проблему даже в этой ситуации.
Включение режима Force IRQL Checking в Driver Verifier существенно повышает 0 вероятность обнаружения нарушений правила IRQL. При включении этого режима перемещаемые страницы выгружаются из памяти каждый раз, когда про-. веряемые драйверы поднимают уровень IRQL до DISPATCHJ-EVEL и выше.
Управление перемещаемостью кода * на стадии компиляции
Итак, одни части драйвера должны постоянно оставаться резидентными, а другие могут выгружаться. Программисту необходимы средства, которые бы позволяли ему управлять распределением кода и данных между перемещаемой и неперемещаемой памятью. Проблема отчасти решается при помощи инструкций, на основании которых компилятор выбирает способ распределения кода и данных по разным секциям. На основании имен секций загрузчик размещает части драйвера в положенных участках памяти. Кроме того, для решения этой задачи вызываются различные функции Memory Manager, которые будут описаны в следующем разделе.
ПРИМЕЧАНИЕ----------------------------------------------—-----------------
Исполняемые файлы Win32, в том числе и драйверы режима ядра, состоят из одной или нескольких секций. Секция содержит код или данные и в общем случае может иметь дополнительные атрибуты: доступ для чтения, доступ для записи, общий доступ, исполнение и т. д. Секции также являются наименьшими единицами для определения перемещаемости. Загружая образ драйвера, система размещает секции с именами, начинающимися с PAGE или .EDA (начало .EDATA), в перемещаемом пуле, если только в реестре не установлен параметр HKLM\System\ CurrentControlSet\Control\Session Manager\Memory Management (в этом случае код драйверов не выгружается). Учтите, что в именах учитывается регистр символов! Один из капризов судьбы, которые время от времени настигают каждого из нас: для запуска Soft-Ice/W в Windows ХР необходимо запретить перемещение кода ядра указанным способом. Конечно, это сильно затрудняет поиск ошибок, обусловленных ошибочным размещением кода или данных драйвера в перемещаемой памяти! Если вы применяете этот отладчик, я рекомендую добросовестно использовать макрос PAGED_CODE и Driver Verifier.
Управление памятью
121
Традиционно для передачи компилятору инструкций о размещении кода в определенной секции использовалась директива alioc_text Поскольку эта директива поддерживается не всеми компиляторами, в заголовочных файлах DDK предусмотрена специальная константа ALLOC_PRAGMA; в зависимости от того, определена она или нет, программист узнает о возможности применения этой директивы. Далее директива alloc_text вызывается для определения размещения отдельных функций драйвера по секциям, как в следующем фрагменте:
#jfdef ALLOC_PRAGMA
#pragma alloc_text(PAGE, AddDevice)
#pragma alloc_text(PAGE, DispatchPnp)
#endif
Эти директивы обеспечивают размещение функций AddDevice и DispatchPnp в перемещаемой памяти.
Компилятор Microsoft C/C++ устанавливает два раздражающих ограничения на применение allocjext:
О Директива должна следовать за объявлением функции, но предшествовать ее определению. Один из способов соблюдения этого правила — объявление всех функций драйвера в стандартном заголовочном файле и вызов alloc_text в начале файла с кодом функции, после включения заголовка.
□ Директива может использоваться только для функций, использующих схему компоновки С. Другими словами, она не работает для функций классов и для функций в исходных файлах C++, которые не были объявлены с атрибутом extern "С".
Для управления размещением переменных применяется другая директива, работающая под управлением другого препроцессорного макроимени:
#1fdef ALLOC_DATA_PRAGMA
#pragma data^segC'PAGEDATA")
#endif
Все статические переменные, объявленные в исходном модуле после директивы data_seg, размещаются в перемещаемой памяти. Обратите внимание на принципиальное отличие этой директивы от alloc_text. Перемещаемая секция начинается с #pragma data_seg(,,PAGEDATA’') и завершается парной директивой #pragma data_seg. С другой стороны, директива alioc__text относится к конкретной функции.
Управление перемещаемостью кода на стадии выполнения
В табл. 3.3 перечислены сервисные функции, позволяющие управлять возможностью выгрузки кода драйвера в различных ситуациях. Все они имеют одну общую цель: освободить физическую память, занимаемую неиспользуемым кодом и данными. Так, в главе 8 будет рассказано, как перевести устройство в состояние пониженного энергопотребления в длительные периоды бездействия. Отключение питания может стать хорошим поводом для освобождения заблокированных страниц.
122
Глава 3. Основные приемы программирования
ДОПОЛНИТЕЛЬНО О РАЗМЕЩЕНИИ СЕКЦИЙ--------------------------------------------------------
Обычно я предпочитаю определять размещение секций для целых блоков программного кода при помощи директивы Microsoft code_seg. Эта директива работает так же, как data_seg, но только для программного кода. Иначе говоря, следующий фрагмент приказывает компилятору Microsoft приступить к размещению функций в перемещаемой памяти:
fpragma code_seg(,,PAGE")
NTSTATUS AddDevice
NTSTATUS DispatchPnp
Обе функции, AddDevice и DispatchPnp, попадают в перемещаемую память. Чтобы узнать, используется ли компилятор Microsoft, проверьте существование стандартного препроцессорного макроса __MSC_VER.
Чтобы вернуться к кодовой секции по умолчанию, используйте директиву #pragma code_seg без аргумента:
#pragma codecsegО
Аналогично, чтобы вернуться к обычной неперемещаемой секции данных, используйте директиву #pragma data_seg без аргументов:
^pragma data_seg()
В этой врезке также логично упомянуть о возможности размещения в секции INIT кода, который не понадобится драйверу после завершения инициализации. Например:
#pragma al 1oc_text(INIT, DriverEntry)
Эта директива размещает функцию DriverEntry в секции INIT. После выхода из функции система освобождает занимаемую ею память. Впрочем, эта мелкая экономия не играет заметной роли, потому что функция DriverEntry драйверов WDM занимает мало места. Предшествующие драйверы Windows NT содержали большие функции DriverEntry, которым приходилось создавать объекты устройств, выделять ресурсы, настраивать конфигурацию устройств и т. д. Для них эта возможность обеспечивала заметную экономию памяти.
Несмотря на малую пользу от размещения DriverEntry в секции INIT драйверов WDM, до недавнего времени я поступал именно так. Но из-за ошибки в Windows 98/Ме однажды возникла ситуация, в которой драйвер WDM был не совсем корректно удален из памяти после отключения устройства. Один из компонентов системы этого не понял и попытался вызвать DriverEntry при повторном подключении устройства. К этому моменту память, когда-то содержавшая DriverEntry, уже давно была заменена кодом INIT, принадлежащим другим драйверам, и в системе произошел сбой. Обнаружить такие ошибки очень трудно! Теперь я предпочитаю размещать DriverEntry в перемещаемой секции. Утилита DUMPBIN, входящая в поставку Microsoft Visual C++ .NET, позволяет легко определить, какая часть драйвера размещается в перемещаемой памяти. Возможно, ваш отдел маркетинга даже похвастается тем, насколько меньше неперемещаемой памяти использует ваш драйвер по сравнению с продуктами конкурентов.
Таблица 3.3. Функции динамического установления и снятия блокировки в памяти страниц драйверов
Функция	Описание
MmLockPagableCodeSection MmLockPagableDataSection MmLockPagableSectionByHandle	Блокирует секцию кода по адресу, находящемуся внутри нее Блокирует секцию данных по адресу, находящемуся внутри нее Блокирует секцию кода по манипулятору, полученному в результате предшествующего вызова MmLockPagableCodeSection (только в Windows 2000 и Windows ХР)
MmPageEntireDriver MmResetDriverPaging	Снимает блокировку со всех страниц, принадлежащих драйверу Восстанавливает атрибуты перемещаемости, назначенные на стадии компиляции, для всего драйвера
MmUnlockPagablelmageSection	Снимает блокировку с заблокированной секции кода или данных
Управление памятью
123
Я опишу один из способов применения этих функций для управления перемещаемостью кода вашего драйвера. В документации DDK можно найти другие способы. Прежде всего разделите функции своего драйвера по секциям кода:
#pragma alloc_text(PAGEIDLE, D1spatchRead)
#pragma alloc_text(PAGEIDLE, DlspatchWrlte)
Другими словами, определите имена секций, начинающиеся с префикса PAGE и заканчивающиеся любым четырехбуквенным суффиксом по вашему усмотрению. Затем воспользуйтесь директивой allocjext для размещения группы своих функций в этой специальной секции. Количество перемещаемых секций может быть произвольным, но с ростом количества секций возрастают и проблемы с сопровождением кода.
На стадии инициализации (скажем, в DriverEntry) заблокируйте перемещаемые секции в памяти:
PVOID hPageldleSect1 on;
NTSTATUS DrlverEntryC...)
{
hPageldleSection = MmLockPagableCodeSect1on((PVOID)
DIspatchRead);
}
При вызове MmLockPagabieCodeSection указывается любой адрес внутри блокируемой секции. Истинной целью этого вызова во время выполнения DriverEntry является получение манипулятора, возвращаемого функцией. В приведенном примере он сохраняется в глобальной переменной с именем hPageldleSection. Манипулятор будет использован гораздо позднее, когда вы решите, что в течение определенного времени присутствие некоторой секции в памяти не обязательно:
MmUnlockPagableImageSect1on(hPageIdleSect1on^:
Вызов снимает блокировку со страниц, содержащих секцию PAGEIDLE, и разрешает выгружать и подгружать их в память по мере надобности. Если позднее окажется, что страницы снова должны находиться в памяти, используйте вызов следующего вида:
MmLockPадаbleSecti onByHandlе(hPageldleSect1 on):
Секция PAGEIDLE снова окажется в неперемещаемой памяти (хотя и не обязательно в той же физической памяти, что и раньше). Обратите внимание на то, что функция доступна только в Windows 2000 и Windows ХР и только при включении заголовочного файла ntddk.h вместо wdm.h. В других ситуациях вам придется повторно вызывать MmLockPagabieCodeSection.
Размещение объектов данных в перемещаемых секциях производится примерно так:
PVOID hPageDataSectlon;
^pragma data_segC'PAGE")
ULONG ulSometh!ng:
124
Глава 3. Основные приемы программирования
#pragma data_seg()
hPageDataSection = MmLockPagableDataSection(CPVOID)
&ulSomething);
MmUnlockPagableImageSection(hPageDataSection);
MmLockPagableSecticnByHandle(hPageDataSection);
Здесь я обошелся с синтаксисом весьма вольно — на практике части драйвера, в которых располагаются эти команды, находятся на значительном расстоянии друг от друга.
Основная идея только что описанных сервисных функций управления памятью заключается в том, что секция с одной или несколькими страницами блокируется в памяти и вы получаете манипулятор, используемый при последующих вызовах. После этого для снятия блокировки со страниц секции вызывается функция MmUnlockPagablelmageSection, которой передается соответствующий манипулятор. Повторная блокировка секции потребует вызова MmLockPagableSectionByHandle.
Если вы уверены в том, что присутствие каких-либо частей драйвера в памяти на некоторое время не обязательно, существует упрощенный способ. Функция MmPageEntireDriver помечает все секции образа драйвера как перемещаемые. И наоборот, функция MmResetDriverPaging возвращает всему драйверу атрибуты перемещаемости, заданные на стадии компиляции. При вызове этих функций достаточно передать адрес произвольного фрагмента кода или объекта данных в драйвере. Пример:
MmPageEntireDriver((PVOID) DriverEntry);
MmResetDriverPagingf(PVOID) DriverEntry);
Если устройство использует прерывания, использование любых перечисленных функций управления памятью требует особой осторожности. При выгрузке всего драйвера система также выгружает обработчик прерывания (ISR). Если от вашего устройства (или любого другого устройства с совместно используемым вектором) поступит прерывание, система попытается вызвать обработчик прерывания. Даже если вы уверены в том, что прерывание не используется совместно, а устройству была запрещена выдача прерываний, учтите, что в системе могут происходить фиктивные прерывания. При отсутствии обработчика произойдет сбой. Чтобы предотвратить эту проблему, отключите свое прерывание перед тем, как разрешать выгрузку обработчика.
Выделение памяти в куче
Основная сервисная функция режима ядра для выделения памяти из кучи (heap) — ExAllocatePoolWithTag — вызывается следующим образом:
PVOID р = ExAllocatePoolWithTag(type, nbytes, tag);
Аргумент type содержит одну из констант перечисляемого типа POOL_TYPE, перечисленных в табл. 3.4. Аргумент nbytes определяет размер выделяемого блока
Управление памятью
125
в байтах, а аргумент tag содержит произвольное 32-разрядное значение (метку). Возвращаемое значение представляет собой виртуальный адрес режима ядра, указывающий на выделенный блок памяти.
В большинстве драйверов (в том числе и в примерах, приводимых в книге и DDK) встречаются вызовы старой функции ExAllocatePool:
PVOID р - ExAllocatePool(type, mbytes);
Функция ExAllocatePool применялась для выделения памяти в ранних версиях Windows NT, В Windows ХР DDK ExAllocatePool представляет собой макрос для вызова ExAllocatePoolWithTag с меткой « mdW» («Wdm» с завершающим пробелом после перестановки байтов).
Таблица 3.4. Тип пула (аргумент type) функции ExAllocatePool
Тип пула	Описание
NonPagedPool PagedPool NonPagedPoolCacheAligned	Память выделяется из неперемещаемого пула Память выделяется из перемещаемого пула Память выделяется из неперемещаемого пула и выравнивается с кэшем процессора
PagedPoolCacheAligned	Память выделяется из перемещаемого пула и выравнивается с кэшем процессора
Основное решение, которое необходимо принять при вызове ExAllocatePool-WithTag, — может ли выделенный блок выгружаться из памяти? Ответ зависит от того, какие компоненты драйвера будут обращаться к этому блоку. Если блок памяти будет использоваться на уровне DISPATCH-LEVEL и выше, он должен выделяться в неперемещаемом пуле. Если же блок всегда используется на уровне ниже DISPATCHJJEVEL, его можно выделять как в перемещаемом, так и в неперемещаемом пуле, по вашему усмотрению.
>АНИЧЕНИЯ РАЗМЕРА ВЫДЕЛЯЕМОГО БЛОКА —----------------------------------------------------
-ьасто задают вопрос: «Сколько памяти можно выделить одним вызовом ExAllocatePoolWithTag?» К сожалению, простого ответа на него не существует. Отправной точкой должно стать определение максимальных размеров перемещаемых и неперемещаемых пулов. В статье Knowledge Base QI26402 и главе 7 книги «Inside Windows 2000» (Microsoft Press, 2000) вы узнаете (вероятно) гораздо больше, чем вам когда-либо захочется знать по этой теме. Например, на компьютере с 512 Мбайт памяти максимальный размер неперемещаемого пула составил 128 Мбайт, а фактический размер •теремещаемого пула — 168 Мбайт.
Впрочем, знать размер пула недостаточно. Не стоит полагать, что на компьютере с 512 Мбайт па-ияти вам удастся выделить блок неперемещаемой памяти, размер которого сколько-нибудь близок к 128 Мбайт, одним вызовом ExAllocatePoolWithTag. Во-первых, другие компоненты системы уже занимают значительные объемы неперемещаемой памяти к тому моменту, когда вашему драйверу поедставится такая возможность; а если забрать всю оставшуюся память, система будет работать схень плохо. Во-вторых, после сколько-нибудь продолжительной работы системы виртуальное адресное пространство оказывается сильно фрагментированным, и подсистема управления памятью сможет найти очень большой смежный блок неиспользуемых виртуальных адресов.
В неформальных тестах с использованием программы MEMTEST из прилагаемых материалов мне удалось выделить около 129 Мбайт перемещаемой и 100 Мбайт неперемещаемой памяти за один вызов.
126
Глава 3. Основные приемы программирования
Выделение памяти в перемещаемом пуле должно происходить на уровне IRQL ниже DISPATCH_LEVEL. Выделение памяти в неперемещаемом пуле должно происходить на уровне IRQL, меньшем либо равном DISPATCH_LEVEL. Driver Verifier выявляет нарушения этих двух правил.
ПРИМЕР КОДА----------------_____—----------------------------------_____-------------------
Пример MEMTEST использует функцию ExAllocatePoolWithTagPriority для определения размера наибольшего блока, выделяемого в перемещаемом и неперемещаемом пулах.
При вызове ExAllocatePoolWithTag система выделяет на 4 байта больше памяти, чем было затребовано, и возвращает указатель, смещенный на 4 байта в этом блоке. Метка занимает первые 4 байта, таким образом, она предшествует полученному указателю. Метка отображается при просмотре содержимого памяти в процессе отладки и в аварийных дампах. Иногда она помогает найти блок памяти, с которым возникли проблемы. Например:
#define DRIVERTAG ' ’KNUJ"
PVOID p = ExAllocatePoolW1thTag(PagedPool, 42, DRIVERTAG);
Здесь в качестве метки используется 32-разрядная целочисленная константа. На компьютерах с прямым порядком байтов (little-endian) — таких, как х86, байты, входящие в это значение, будут переставлены в памяти и образуют общеизвестное английское слово1. Кстати говоря, работа некоторых функций Driver Verifier зависит от конкретных меток, так что вы можете упростить себе работу по отладке, используя уникальные метки при вызове функций выделения памяти.
Не используйте в качестве метки нули или строку « GIB» (после перестанов-!□ ки байтов — BIG с завершающим пробелом). Блоки с нулевыми метками не отслеживаются, а метка BIG используется во внутренней работе системы. Не запрашивайте блок нулевого размера. Обратите особое внимание на это ограничение, если вы пишете собственную поддержку времени выполнения для С или C++, поскольку malloc и оператор new допускают запросы нуля байтов.
ДОПОЛНИТЕЛЬНО О МЕТКАХ---------------------------------------------------=---——-
Некоторые диагностические механизмы ядра зависят от пометки блоков; кроме того, выбор уникальных меток упрощает анализ производительности драйвера. Также необходимо включить механизм пометки памяти в окончательной версии системы (в отладочных версиях он включается по умолчанию) при помощи утилиты GFLAGS.EXE. Эта утилита входит в состав Platform SDK и других компонентов.
При выполнении обоих условий (использование уникальных меток в драйвере и включение их поддержки в ядре) вы сможете использовать ряд полезных программ. Утилиты POOLMON и POOLTAG из DDK выводят информацию об использовании памяти по метке. Кроме того, можно потребовать, чтобы утилита GFLAGS считала один из пулов «специальным» и проверяла его перезапись.
Полученный указатель будет выровнен по крайней мере по 8-байтовой границе. Если разместить экземпляр некоторой структуры в выделенной памяти,
1 «Junk», то есть «мусор». — Примеч. перев.
Управление памятью
127
то ее поля со смещениями, кратными 4 и 8, также будут занимать адреса, кратные 4 и 8. Возможно, по соображениям быстродействия вы также захотите, чтобы блок памяти занимал как можно меньшее колотество строк процессорного кэша. Для достижения этого результата применяются коды типов XrvCacheAligned. Если размер запрашиваемого блока меньше размера страницы, то блок будет содержаться в одной странице. В противном случае блок начинается с границы страницы.
ПРИМЕЧАНИЕ------------------------------------------------------------------------
Наверное, запрос PAGE_SIZE + 1 байтов памяти — худшее, что можно придумать при выделении памяти в куче. Система резервирует две страницы, причем почти половина выделенного места пропадет даром.
Не стоит и говорить, что при работе с памятью, выделенной в пулах свободной памяти режима ядра, необходимо действовать крайне осторожно. Поскольку код драйвера выполняется в самом привилегированном из возможных режимов процессора, защита от нарушения границ практически отсутствует.
В режиме специального пула утилита GFLAGS или Driver Verifier упрощает поиск ошибок перезаписи памяти. В этом режиме память, выделяемая в «специальном» пуле, располагается в конце страницы, за которой в виртуальной памяти следует отсутствующая страница. Попытка обратиться к памяти за концом выделенного блока немедленно приводит к страничному сбою. Кроме того, функция выделения памяти записывает в остаток страницы определенный заполнитель. При освобождении памяти система проверяет, не был ли этот заполнитель стерт. Комбинация этих двух проверок значительно упрощает поиск ошибок, обусловленных выходом за границы выделенной памяти. Кстати говоря, вы также можете потребовать, чтобы память выделялась от начала страницы, которой предшествует отсутствующая страница. За дополнительной информацией о специальных пулах обращайтесь к статье Knowledge Base QI92486.
Обработка ситуаций с нехваткой памяти
Если в системе не хватает памяти для удовлетворения запроса, функция возвращает указатель NULL. Всегда проверяйте возвращаемое значение и предусмотрите разумную обработку нехватки памяти. Например:
DMYSTUFF р= (PMYSTUFF) ExAllocatePool (PagedPool. stzeof(MYSTUFF));
И (!p)
return STATUS_INSUFFICIENT_RESOURCES;
Существуют дополнительные типы пулов, для которых попытка выделения памяти обязана завершиться успешно. Если в системе недостаточно памяти для удовлетворения запроса из пула с гарантированным выделением, происходит фатальный сбой. Драйверы не должны выделять память со спецификаторами гарантированного выделения. Дело в том, что при работе драйвера практически лхюая операция теоретически может завершиться неудачей. Провоцировать системный сбой при нехватке памяти — крайне неудачная идея, особенно для драйвера. Более того, во всей системе объем памяти с гарантированным выделением весьма ограничен. Если драйверы будут захватывать ее, возможно, операционной
128
Глава 3. Основные приемы программирования
системе не удастся выделить память, необходимую для обеспечения нормальной работы компьютера. На самом деле компания Microsoft уже жалеет о том, что в DDK были документированы возможности гарантированного выделения. Каждый раз, когда драйвер при выделении памяти указывает тип пула с гарантированным выделением. Driver Verifier инициирует фатальный сбой. Кроме того, если включить в Driver Verifier режим имитации нехватки ресурсов, после семи-восьми минут работы системы при попытках выделения памяти начинают происходить случайные отказы. Каждые пять минут или около того система в течение 10 секунд будет отвергать все запросы на выделение памяти.
В некоторых ситуациях применяется прием, часто встречающийся в драйверах файловой системы. Если объединить значение POOL_RAISE_IF_ALLOCATION_FAILURE (0x00000010) с кодом типа пула операцией OR, то при нехватке памяти вместо возврата NULL будет инициироваться исключение STATUSJNSUFFICIENT_RESOURCES. Для его перехвата следует использовать кадры структурированных исключений. Пример:
#1 fndef POOL_RAISE_IF_ALLOCATION-FAILURE
#def1ne POOL_RAISE_IFJLLOCATION_FAILURE 16
#endif
#define PagedPoolRaiseExceptIon (POOL_TYPE) \
(PagedPool POOLJWSEJFJ\LLOCATION_ FAILURE)
#def1ne NonPagedPoolRaiseExceptlon (POOL_TYPE) \
(NonPagedPool	POOLJRAISE J F_ALLOCATI ON JAI LURE)
NTSTATUS SomeFunctlonO
NTSTATUS status:
__try
{
PMYSTUFE p = (PMYSTUFF)
ExAl1ocatePoolW1thTag(PagedPoolRa1seExcept1 on.
slzeof(MYSTUFF), DRIVERTAG);
<Код, использующий "p" без проверки на NULL>
status = STATUS-SUCCESS;
}
_except(EXCEPTION_EXECUTE_HANDLER)
status = GetExceptlonCodeO;
}
return status:
}
ПРИМЕЧАНИЕ---------------------------------------------------------------------------
Константа POOL„RAISE_IF_ALLOCAHON_FAILURE определяется в NTIFS.H — заголовочном файле, который доступен только в составе пакета Installable File System kit Тем не менее, выделение памяти с этим флагом так часто применяется в драйверах файловой системы, что я решил упомянуть о нем.
Управление памятью
129
Я бы не рекомендовал тратить чрезмерные усилия на диагностику или восстановление после сбоя при выделении небольших блоков памяти. На практике запросы, например, на выделение 32 байт памяти никогда не завершаются неудачей. Если системе настолько не хватает памяти, она будет работать так медленно, что кто-нибудь все равно перегрузит компьютер. Однако ваш код не должен стать причиной системного сбоя в этой ситуации, потому что это создает потенциальную угрозу атак класса DoS (Denial of Service, отказ в обслуживании). Но поскольку в реальной жизни эта ошибка не возникнет, нет смысла включать в драйвер сложный код регистрации ошибок, выдачи событий WMO, вывода отладочных сообщений, перехода на альтернативные алгоритмы и т. д. Возможно, именно из-за этого дополнительного кода системе не хватило памяти для выделения тех 32 байт! Итак, я рекомендую проверять возвращаемое значение при каждом вызове ExAllocatePoolWithTag. Если проверка обнаруживает ошибку, выполните всю необходимую зачистку и верните код состояния. Точка.
Освобождение блока памяти
Блоки памяти, ранее выделенные функцией ExAllocatePoolWithTag, освобождаются функцией ExFreePool:
ExFreePcol((PVOID) р);
Необходимо каким-то образом следить за памятью, выделенной из пула, чтобы освободить ее, когда надобность в ней отпадет. Никто другой за вас это не сделает. Иногда при чтении документации DDK по вызываемым функциям приходится особенно внимательно следить за принадлежностью выделяемой памяти. Например, в функции AddDevice, представленной в предыдущей главе, используется вызов loRegisterDevicelnterface. У этой функции имеется побочный эффект: она выделяет блок памяти для хранения строки с именем интерфейса. Вы отвечаете за его последующее освобождение.
Driver Verifier при вызове DriverUnload определяет, освободил ли проверяемый драйвер всю выделенную им память. Кроме того, Driver Verifier проверяет все вызовы ExFreePool и убеждается в том, что они относятся к полному блоку памяти, который выделен из пула, соответствующего текущему уровню IRQL.
В заголовочных файлах DDK объявлена недокументированная функция с именем ExFreePoolWithTag. Предполагается, что эта функция предназначена только для внутреннего использования — она следит за тем, чтобы системные компоненты не освобождали память, принадлежащую другим компонентам. Один из разработчиков Microsoft вежливо назвал эту функцию «напрасной тратой времени». Отсюда можно сделать вывод, что нам не нужно беспокоиться о том, что делает эта функция и как ее использовать. (Подсказка: для ее успешного использования необходимо задействовать другие недокументированные возможности.)
Еще две функции
Хотя для выделения памяти в куче следует использовать функцию ExAllocatePool-WthTag, существуют еще две функции выделения памяти, применяемые в особых обстоятельствах: ExAllocatePoolWithQuotaTag (и макрос ExAllocatePoolWithQuota,
130
Глава 3. Основные приемы программирования
передающий метку по умолчанию) и ExAllocatePoolWithTagPriority. Функция Ех-AllocatePoolWithQuotaTag выделяет блок памяти и учитывает его в квоте планирования текущего потока. Эта функция предназначена для драйверов файловой системы и других драйверов, не работающих в контексте произвольного потока, для выделения памяти, принадлежащей текущему потоку. Обычно драйверы не используют эту функцию, потому что при нарушении квоты система инициирует исключение.
Функция ExAllocatePoolWithTagPriority, появившаяся в Windows ХР, позволяет задать субъективный приоритет успешного выделения памяти:
PVOID р - ExAllocatePoolWithTagPriority(type, mbytes, tag, priority);
Она получает те же аргументы, которые были описаны ранее, а также дополнительный признак приоритета (табл. 3.5).
Таблица 3,5. Значения приоритета для функции ExAllocatePoolWithTagPriority
Аргумент	Описание
LowPool Priority	При нехватке ресурсов система может отказать в выделении памяти. Драйвер легко справится с неудачным завершением операции
NormalPoolPriority	При нехватке ресурсов система может отказать в выделении памяти
HighPoolPriority	Система не должна отказывать в выделении памяти (кроме полного отсутствия ресурсов)
В DDK указано, что при использовании этой функции драйверы обычно должны указывать приоритет NormalPoolPriority. Приоритет HighPoolPriority резервируется для ситуаций, в которых успешное выделение памяти критически важно для продолжения работы системы.
К именам, перечисленным в табл. 3.5, можно присоединять суффиксы Special-J PoolOverrun и SpecialPoolUnderrun (например, LowPoolPrioritySpecialPoolOverrun и т. д.).
Если при выделении используется специальный пул, эти флаги переопределяют способ размещения блоков, принятый по умолчанию.
На момент написания книги вызов ExAllocatePoolWithTagPriority преобразуется в простой вызов ExAllocatePoolWithTag при запросе перемещаемой памяти с высоким приоритетом или неперемещаемой памяти с любым приоритетом. Дополнительная проверка ресурсов выполняется только при запросах перемещаемой памяти с низким или норхМальным приоритетом. Это поведение может измениться в обновлениях Service Pack или в последующих версиях операционной системы.
Связанные списки
В Windows ХР связанные списки широко применяются для упорядочения наборов сходных структур данных. В этой главе рассматриваются основные сервисные функции, используемые для ведения двусвязных и односвязных списков. Отдельная группа функций позволяет организовать совместное использование связанных списков между программными потоками и несколькими процессорами.
Управление памятью
131
Я опишу эти функции в следующей главе, после знакомства с синхронизационными примитивами, от которых они зависят.
При объединении структур данных в двусвязный или односвязный список в структуру обычно включается связующая подструктура — LIST_ENTRY или SINGLE_LIST_ENTRY. Кроме того, где-то резервируется память для начального элемента, который использует ту же структуру в качестве связующего элемента. Пример:
typedef struct _TWOWAY
{
LIST_ENTRY linkfield;
} TWOWAY, *PTWOWAY;
lIST_ENTRY DoubleHead:
cypedef struct -ONEWAY
{
SINGLE_LIST_ENTRY linkfield;
} ONEWAY, *PONEWAY;
SINGLE_LIST_ENTRY SingleHead;
При вызове сервисных функций управления памятью никогда не используются контейнерные структуры — только связующие поля или заголовок списка. Допустим, имеется указатель (pdElement) на одну из структур TWOWAY. Чтобы включить структуру в список, следует разыменовать связующее поле:
InsertTai1 Li st(&DoubleHead, &pdE1ement->li nkfi eld);
Аналогично, при выборке элемента из списка вы в действительности получаете адрес внедренного связующего поля. Для получения адреса контейнерной структуры используется макрос CONTAININGJRECORD (рис. 3.7).
(PIRP) CONTAINING_RECORD(p, IRP, ListEntry)
Рис. 3.7. Макрос CONTAINING_RECORD
132
Глава 3. Основные приемы программирования
Таким образом, код обработки и удаления всех элементов односвязного списка выглядит примерно так:
PS INGLEJ_IST_ENTRY psLink = PopEntryListC&SingleHead):
while (psLink)
{
PONEWAY psElement = CONTAINING_RECORD(psLink,
ONEWAY, linkfield);
ExFreePool(psElement);
psLink = PopEntryList(&SingleHead);
}
Перед началом списка, а также после каждой итерации производится выборка текущего первого элемента списка вызовом PopEntryList. Эта функция возвращает адрес связующего поля в структуре ONEWAY или NULL (признак пустого списка). Не пытайтесь поспешно использовать CONTAINING_RECORD для получения адреса элемента, чтобы потом сравнить его с NULL, — проверять нужно адрес связующего поля, возвращаемый PopEntryList!
Двусвязные списки
В двусвязных списках каждый элемент содержит две ссылки: на предыдущий (обратная ссылка) и следующий элемент (прямая ссылка), — с циклическим связыванием концов списка (рис. 3.8). Иначе говоря, вы можете начать перебор с любого элемента, перемещаться вперед или назад и вернуться к исходному элементу по кругу. Важнейшей особенностью двусвязных списков является возможность добавления и удаления элементов в любой позиции списка.
Рис. 3.8. Топология двусвязного списка
В табл. 3.6 перечислены сервисные функции, используемые при работе с двусвязными списками.
Управление памятью
133
Таблица 3.6. Функции и макросы для работы с двусвязными списками
Функция или макрос	Описание
InitializeListHead	Инициализирует структуру LIST_ENTRY в начале списка
InsertHeadList	Вставляет элемент в начало списка
InsertTailList	Вставляет элемент в конец списка
IsListEmpty RemoveEntryList RemoveHeadList	Проверяет список на отсутствие элементов Удаляет элемент Удаляет первый элемент
RemoveTailList	Удаляет последний элемент
Использование некоторых функций продемонстрировано в следующем фрагменте вымышленной программы:
typedef struct _TWOWAY {
LIST_ENTRY linkfield;
} TWOWAY, *PTWOWAY;
LIST_ENTRY DoubleHead;
InitialIzeListHeadf&DoubleHead);	// 1
ASSERT(IsLi stEmpty(&DoubleHead));
PTWOWAY pdElement = (PTWOWAY) ExAllocatePool(PagedPool,
sizeof(TWOWAY));
InsertTailList(&DoubleHead, &pdElement->linkfield);	// 2
if (!IsListEmpty(&DoubleHead))	// 3
{
PLIST_ENTRY pdLink = RemoveHeadList(&DoubleHead);	// 4
pdElement = CONTAINING_RECORD(pdLink, TWOWAY, linkfield);
ExFreePool(pdElement);
, }
1.	Вызов InitializeListHead инициализирует структуру LIST_ENTRY ссылками на саму себя (как прямую, так и обратную ссылки). Такая конфигурация означает, что список пуст.
2.	Функция InsertTailList вставляет элемент в конец списка. Обратите внимание: вместо адреса вашей структуры TWOWAY передается адрес внедренного связующего поля. Также можно было вызвать функцию InsertHeadList и добавить элемент в начало списка. Передавая адрес связующего поля в существующей структуре TWOWAY, вы включаете новый элемент либо после существующего элемента, либо перед ним:
PTWOWAY prev;
InsertHeadList(&prev->linkfield, &pdElement->li nkfield);
134
Глава 3. Основные приемы программирования
PTWOWAY next;
InsertTal1 Li st(&next->11nkf1 eld, &pdE1ement->11nkf1 eld);
3.	Вспомните, что в пустом двусвязном списке заголовок указывает на самого себя — как по прямой, так и по обратной ссылке. Для упрощения этой проверки удобно воспользоваться функцией IsListEmpty. Значение, возвращаемое функцией RemoveXxrList, никогда не равно NULL!
4.	Функция RemoveHeadList удаляет элемент в заголовке списка и возвращает адрес связующего поля. Функция RemoveTailList делает то же самое с элементом в конце списка.
Точное знание реализации RemoveHeadList и RemoveTailList поможет вам избежать ошибок. Например, возьмем внешне безобидную команду:
if (<еь/ражение>)
pdLink = RemoveHeadList(&DoubleHead);
Предполагается, что эта конструкция извлекает первый элемент списка при выполнении некоторого условия. C’est raisonnable, n’est-ce pas? Но нет, при отладке обнаруживается, что элементы таинственным образом исчезают из списка. Оказывается, указатель pdLink обновляется только в том случае, если выражение истинно, но RemoveHeadList вызывается даже тогда, когда выражение ложно!
Mon dieu! Что происходит? Видите ли, RemoveHeadList — это всего лишь макрос, который развертывается на несколько команд. В действительности компилятор видит следующий фрагмент:
If (<some-expr>)
pdLink = (&DoubleHead)->F11nk;
{{
PLIST_ENTRY _EX_Blink;
PLIST_ENTRY _EX_F11nk:
_EX_F11nk = ((&DoubleHead)->F11nk)->F'link:
_EX_B11nk = ((&DoubleHead)->FTink)->B11nk;
_EX_Blink->Flink = _EX_Flink;
_EX_Flink->B11nk = _EX_B1Ink;
}}
Ага! Теперь причина таинственного исчезновения элементов списка ясна. Ветка TRUE команды if состоит из единственной команды pdLink = (&DoubleHead)-> Flink, сохраняющей указатель на первый элемент. Логика удаления элемента списка стоит отдельно в области видимости команды if и поэтому выполняется всегда. Макросы RemoveHeadList и RemoveTailList эквивалентны выражению с составной командой, и поэтому они не должны использоваться в тех местах, где по требованиям синтаксиса должны располагаться либо выражение, либо составная команда. Zut alors!
Кстати говоря, с другими списковыми макросами этой проблемы не возникает. Трудности с RemoveHeadList и RemoveTailList обусловлены тем, что эти макросы должны и возвращать значение, и выполнять операции со списком. Другие макросы делают что-то одно, поэтому их использование синтаксически безопасно.
Управление памятью
135
Односвязные списки
В односвязных списках элементы связываются только в одном направлении, как показано на рис. 3.9. Как можно предположить по именам сервисных функций, приведенных в табл. 3.7, в Windows ХР односвязные списки используются для реализации стеков. По аналогии с двусвязными списками, эти «функции» в действительности реализуются в виде макросов в wdm.h, поэтому на них распространяются аналогичные меры предосторожности. Подстановка PushEntryList и PopEntryList генерирует несколько команд, поэтому макросы могут использоваться только справа от знака равенства в контексте, в котором компилятор ожидает увидеть несколько команд.
Таблица 3,7. Функции для работы с односвязными списками
Функция или макрос	Описание
PushEntryList	Включает элемент в начало списка
PopEntryList	Удаляет начальный элемент
Рис. 3.9. Топология односвязного списка
Следующая псевдофункция демонстрирует основные операции с односвяз-ныхш списками:
cypedef struct „ONEWAY {
SINGLE_LIST_ENTRY linkfield:
} ONEWAY, *PONEWAY;
SINGLE_LIST_ENTRY SingleHead;
SingleHead.Next = NULL;	// 1
PONEWAY psElement = (PONEWAY) ExAllocatePool(PagedPool.
sizeof(ONEWAY)):
136
Глава 3. Основные приемы программирования
PushEntryList(&SingleHead, &psElement->l inkfield);	// 2
SINGLE_LIST_ENTRY psLink = PopEntryList(&SingleHead);	// 3
if (psLink)
{
psElement = CONTAINING_RECORD(psLink, ONEWAY, linkfield);
ExFreePool(psElement);
}
1.	Инициализация начального элемента односвязного списка не требует вызова специальной функции — достаточно присвоить NULL полю Next. Также обратите внимание на отсутствие отдельной функции для проверки пустых списков — задача решается простой проверкой Next.
2.	Функция PushEntryList помещает элемент в начало (заголовок) списка — единственную часть списка, доступную напрямую. Напомню, что вместо адреса пользовательской структуры ONEWAY указывается адрес внедренного связующего поля.
3.	Функция PopEntryList удаляет из списка первый элемент и возвращает указатель на связующее поле внутри него. В отличие от двусвязных списков, значение NULL означает, что список пуст. Для односвязных списков эквивалента IsListEmpty не существует.
Резервные списки
Даже если в подсистеме управления памятью используются самые лучшие алгоритмы, работа с блоками памяти произвольного размера потребует немалых затрат процессорного времени на периодическое слияние смежных свободных блоков. Как показано на рис. 3.10, если блоки А и С уже свободны, а блок В возвращается в кучу, подсистема управления памятью может объединить блоки А, В и С в один большой блок. В дальнейшем объединенный блок позволит удовлетворить запрос на выделение памяти, по объему превышающей любой из трех исходных компонентов.
БлокА	Блок В	Блок С
◄--------------------------------------------------------------->
Большой объединенный блок
Рис- 3-10. Слияние смежных блоков в куче
Но если заранее известно, что система работает только с блоками памяти фиксированного размера, это позволяет использовать гораздо более эффективные схемы управления кучей. Например, можно заранее выделить большой блок памяти и разбить его на фрагменты фиксированного размера. Далее определяется некоторая схема простой идентификации занятых и свободных блоков
Управление памятью
137
(рис. 3.11). Чтобы вернуть блок в кучу, достаточно пометить его как свободный — объединять его со смежными блоками не нужно, потому что система не имеет дела с запросами блоков произвольного размера.
Впрочем, выделение большого блока с последующим разбиением не является лучшим способом реализации кучи фиксированного размера. В общем случае трудно заранее предсказать объем заранее выделяемой памяти. Завышенная уценка приводит к неэффективному резервированию памяти. Если же оценка окажется заниженной, придется либо выдавать сбой при исчерпании всей свободной памяти (плохо!), либо часто обращаться к внешнему администратору памяти и запрашивать память под дополнительные блоки (чуть лучше). Для решения этой проблемы компания Microsoft разработала объект резервного спи-ска (lookaside list) и семейство адаптивных алгоритмов.
□ Используемые блоки
□ Свободные блоки
Рис. 3.11. Куча с блоками фиксированного размера
На рис. 3.12 продемонстрирована концепция резервных списков. Представьте, что у вас имеется стакан, который вам каким-то образом удалось уравновесить вертикально в бассейне. Стакан представляет объект резервного списка. При инициализации объекта вы указываете системе размер блоков памяти (капель воды в нашей аналогии), с которыми вы собираетесь работать. В более ранних версиях Windows NT также можно было указать емкость «стакана», но сейчас она адаптивно определяется операционной системой. Чтобы выделить блок памяти, система сначала пытается взять один блок из списка (отлить каплю воды из стакана). Если свободных блоков не осталось, система черпает дополнительную память из окружающего пула. И наоборот, чтобы вернуть освобожденный блок памяти, система сначала пытается вернуть в список (добавить каплю воды в стакан). Если список полон, блок снова возвращается в пул с использованием стандартной функции управления памятью (капля переливается через край в бассейн).
Система периодически регулирует глубину всех резервных списков в зависимости от их фактического использования. Подробности алгоритма несущественны. к тому же они могут в любой момент измениться. В двух словах, система (по крайней мере в текущей версии) уменьшает глубину резервных списков,
138
Глава 3. Основные приемы программирования
которые не использовались в последнее время или не обеспечивают обращения к пулу по крайней мере в 5 % времени. Тем не менее, глубина никогда не уменьшается ниже 4 — это пороговое значение также определяет начальную глубину нового списка.
Во время работы Driver Verifier все резервные списки приводятся к нулевой глубине, в результате все вызовы выделения и освобождения памяти выполняются напрямую с пулом. Это упрощает выявление проблем, связанных с порчей содержимого памяти. Просто учитывайте это обстоятельство при отладке драйвера с активным Driver Verifier.
В табл. 3.8 перечислены восемь сервисных функций, используемых при работе с резервными списками. В действительности это две группы из четырех функций: для резервных списков, управляющих перемещаемой памятью (группа ExXxrPagedLookasideList), и для списков, управляющих неперемещаемой памятью (группа ExXxrNPagedLookasideList). Прежде всего необходимо зарезервировать неперемещаемую память для объекта PAGED_LOOKASIDE_LIST или NPAGED_ LOOKASIDE_LIST. Даже перемещаемая версия объекта должна находиться в неперемещаемой памяти, потому что система будет обращаться к объекту списка на повышенном уровне IRQL.
Таблица 3.8. Функции резервного списка
Функция	Описание
ExInitializeNPagedLookasideList ExInitializePagedLookasideList ExAllocateFromNPagedLookasideList ExAllocateFromPagedLookasideList ExFreeToNPagedLookasideList ExFreeToPagedLookasideList ExDeleteNPagedLookasideList ExDeletePagedLookasideList	Инициализирует резервный список Выделяет блок фиксированного размера Возвращает блок в резервный список Уничтожает резервный список
Управление памятью
139
После того как для объекта резервного списка будет где-то зарезервирована память, вызывается соответствующая функция инициализации:
PPAGEDJ_OOKASIDE_LIST paged!1st;
PNPAGED J_OOKASIDE_LIST nonpagedl1 st;
ExIn1t1al1zePagedLookas1deL1st(pagedl1st, Allocate, Free,
0, blocksize, tag, 0);
ExIn1t1al1zeNPagedLookas1deL1st(nonpagedl1st. Allocate. Free, 0, blocksize, tag, 0);
(эти два примера различаются только написанием имени функции и первым аргументом).
Первый аргумент обеих функций указывает на объект /N/PAGEDJ_OOKASIDELIST, для которого уже была зарезервирована память. В аргументах Allocate и Free передаются указатели на пользовательские функции выделения и освобождения памяти в произвольной куче. Любой из указателей на функции может быть равен NULL, в этом случае используются функции ExAllocatePoolWithTag л ExFreePool соответственно. Параметр blocksize определяет размер блоков памяти, выделяемых из списка, а параметр tag — 32-разрядную метку, размещаемую теред каждым таким блоком. Два нулевых аргумента заменяют значения, которые передавались в предыдущих версиях Windows NT, но теперь вычисляются • истемой самостоятельно, — это флаги, управляющие типом выделения, и глубина резервного списка.
Выделение блока памяти в списке выполняется соответствующей функцией AllocateFrom:
PVOID р = ExAUocateFromPagedLookasldeList(pagedllst);
PVOID q = ExAl 1 ocateFroniNPagedLookasIdeList(nonpagedl 1 st);
Чтобы вернуть блок обратно в список, вызовите соответствующую функцию FreeTo:
ExFreeToPagedLookasIdell st(pagedl1 st, p);
ExFreeToNPagedLookasIdeLIst(nonpagedl1st, q);
Наконец, уничтожение списка осуществляется соответствующей функцией Delete:
ExDeietePagedLook a s1 del 1 st(pagedl1st);
ExDeieteNPagedLookasi deLIst(nonpagedl1st);
Очень важно явно уничтожить резервный список в программе (вместо того, лобы просто дать ему выйти из области видимости). Мне говорили, что многие рограммисты допускают одну распространенную ошибку — они размещают  >6ъект резервного списка в расширении устройства, а затем забывают удалить ' юъект перед вызовом loDeleteDevice. Когда система в следующий раз перебирает : -сзервные списки с целью настройки их глубин, она вносит изменения там, где раньше находился ваш список, — вероятно, с печальными последствиями.
140
Глава 3. Основные приемы программирования
Работа со строками
Драйверы WDM работают со строковыми данными в четырех форматах:
О Строки Юникода, обычно описываемые структурой UNICODE_STRING, содержат 16-разрядные символы. Количество кодовых пунктов в Юникоде позволяет представить символы всех алфавитов, используемых на нашей планете. Эксцентричная попытка стандартизации кодовых пунктов для клингонского1 алфавита, о которой упоминалось в первом издании книги, была отвергнута. Один из читателей первого издания прислал мне по этому поводу следующий комментарий:
Подозреваю, это что-то грубое... возможно, даже неприличное.
О Строки ANSI, обычно описываемые структурой ANSI_STRING, состоят из 8-раз-рядных символов. Их разновидностью являются строки OEM_STRING, также описывающие последовательности 8-разрядных символов. Различия между этими двумя кодировками заключаются в том, что графическое представление символов строк ANSI зависит от текущей кодовой страницы, тогда как строки ANSI состоят из символов, графическое представление которых не зависит от кодовой страницы. Драйверы WDM обычно не имеют дела со строками WDM — к тому моменту, когда драйвер получает строку, она уже оказывается переведенной в Юникод другим компонентом режима ядра.
О Последовательности символов, завершенные нуль-символами. Для выражения констант можно использовать стандартный синтаксис С вида "Hello, world!". В строках применяются 8-разрядные символы типа CHAR, которые, как предполагается, входят в набор символов ANSI. Кодировка символов в строковых константах определяется редактором, в котором вводится исходный код программы. Если ваш редактор задействует текущую кодовую страницу для отображения графики в окне редактирования, учтите, что в контексте набора символов Windows ANSI смысл некоторых символов может измениться.
О Последовательности символов в расширенной кодировке (широких символов), завершенные нуль-символами (тип WCHAR). Для представления широких строковых констант можно использовать стандартный синтаксис С вида L"Goodbye, cruel world!". Такие строки внешне похожи на константы Юникода. Но так как они в конечном счете создаются тем или иным текстовым редактором, реально в них используются только кодовые пункты ASCII и Latinl (0020-007F и OOAO-OOFF), что соответствует набору Windows ANSI.
Строение обеих структур данных, UNICODE_STRING и ANSI_STRING, показано на рис. 3.13. Поле Buffer обеих структур содержит указатель на область данных, содержащую строковые данные. Поле MaximumLength задает длину буфера, а поле Length содержит (текущую) длину строки без учета возможного
1 Клингонн — инопланетная раса из сериала «Star Track». — Примеч. перев.
Работа со строками
141
завершающего нуль-символа. Оба значения длины задаются в байтах, даже для строк UNICODE^STRING.
Рис. 3.13. Структуры UNICODE_STRING и ANSI-STRING
В ядре определяются три группы функций для работы со строками Юникода и ANSI. Имена первой группы начинаются с Rtl (Run-Time Library). Ко второй группе относится большинство функций стандартной библиотеки С для работы со строками, завершенными нуль-символами. Третья категория включает безопасные строковые функции из файла strsafe.h; вероятно, к тому моменту, когда вы будете читать эту книгу, они будут упакованы в заголовочный файл DDK с именем NtStrsafe.h. Я не стану повторять все, что говорится о функциях RtlAxx в документации DDK, — никакой пользы от такого пересказа не будет. Тем не менее, я выделил в табл. 3.9 список стандартных строковых функций С, ныне считающихся устаревшими, и рекомендуемые альтернативы для них из NtStrsafe.h.
Таблица 3.9. Безопасные функции для работы со строками
Стандартная функция (устаревшая)	Безопасная альтернатива для Юникода	Безопасная альтернатива для ANSI
strcpy, wcscpy, strncpy, wcsncpy	RtlStringCbCopyW, RtlStringCchCopyW	RtlStringCbCopyA, RtlStringCchCopyA
strcat, wcscat, strncat, -vcsncat	RtlStringCbCatW, RtlStringCchCatW	RtlStringCbCatA, RtlString CchCatA
sprintf, swprintf, -Snprintf, _snwprintf	RtlStringCbPrintfW, RtlStringCchPrintfW	RtlStringCbPrintfA, RtlStringCchPrintfA
vsprintf, vswprintf, vsnprintf, -vsnwprintf	RtlStringCbVPrintfW, RtlStringCchVPrintfW	RtlStringCbVPrintfA, RtlStringCchVPri ntfA
strlen, wcslen	RtlStringCbLengthW, RtfStringCchLengthW	RtlStringCbLengthA, RtiStringCchLengthA
ПРИМЕЧАНИЕ------------------------------------------------------------------------
Содержимое табл. 3.9 основано на информации одного из разработчиков ядра, собиравшегося создать фф NtStrsafe.h на базе существующего заголовка пользовательского режима с именем strsafe.h. Не доверяйте мне — доверяйте содержимому DDK!
142
Глава 3. Основные приемы программирования
В драйверах также допускается (хотя и не считается идиоматичным) использование функций memcpy, memmove, memcmp и memset. Тем не менее, большинство разработчиков драйверов предпочитают использовать функции RtIXxx
О Функции RtICopyMemory и RtICopyBytes вместо memcpy копируют «большой двоичный блок» байтов из одного места в другое. В текущей версии Windows ХР DDK эти функции идентичны. Более того, для 32-разрядных платформ Intel обе функции отображаются на memcpy при помощи макросов, а на memcpy распространяется директива #pragma intrinsic, поэтому для выполнения этой операции компилятор генерирует подставляемый (inline) код.
О Функция RtIZeroMemory используется вместо memset для обнуления блоков памяти. Для 32-разрядных платформ Intel RtIZeroMemory отображается на memset при помощи макроса.
Используйте безопасные строковые функции вместо стандартных функций времени выполнения (таких как strcpy и др.). Как упоминалось в начале главы, стандартные строковые функции остаются доступными, но их безопасное использование сопряжено с большими трудностями. Выбирая строковые функции для использования в драйвере, учтите следующее:
О Всевозможные формы strcpy, strcat, sprintf и vsprintf (и их эквиваленты в Юникоде) не защищают от переполнения приемного буфера. То же можно сказать о функции strncat (и ее Юникод-эквиваленте), у которой аргумент длины относится к исходной строке.
О Функции strncpy и wcsncpy не присоединяют завершающий нуль-символ к приемнику, если фактическая длина источника как минимум не меньше указанной. Кроме того, у этих функций имеется потенциально дорогостоящая возможность заполнения оставшейся части целевого буфера нуль-символами.
О Каждая из устаревших функций может выйти за границу страницы памяти в напрасных поисках завершающего нуль-символа. Из-за данной особенности эти функции особенно опасны при обработке строковых данных, поступивших из пользовательского режима.
О На момент написания книги в NtStrsafe.h не определяются функции сравнения (strcmp и т. д.). За информацией об этих функциях обращайтесь к DDK. Учтите, что сравнения ANSI-строк без учета регистра символов усложняются их зависимостью от настроек локального контекста, которые могут изменяться между сеансами на одном компьютере.
Выделение и освобождение строковых буферов
Структуры UNICODE_STRING (и ANSI_STRING) часто объявляются как автоматические переменные или как компоненты структуры расширения устройства. Строковые буферы, указатели на которые хранятся в структурах, обычно хранятся в динамически выделяемой памяти, но иногда в программах также используются строковые константы. Порой бывает трудно уследить за тем, кому
Itjyrne методы программирования
143
принадлежит та или иная структура UNICODE_STRING или ANSI_STRING. Возьмем следующий фрагмент функции:
UNICODE-STRING foo;
If (bArrlving)
RtlImtUnicodeStr1ng(&foo, "Hello, world!");
e ise
ANSIJTRING bar;
DtlImtAnsiStringt&bar, "Goodbye, cruel world!"); RtlAns1Str1ngToUn1codeStr1ng(&foo, &bar, TRUE);
RtlFreeUmcodeString(&foo); // <== Нельзя I
В одном случае foo.Length, foo. Maxi murnLength и foo.Buffer инициализируются винными, представляющими широкие строковые константы в нашем драйвере. В другом случае мы требуем (устанавливая третий аргумент RtlAnsiStringTollnicode-Sbrog равным TRUE), чтобы система выделила память для ANSI-строки, преобразованной в Юникод. В первом случае вызов RtlFreeUnicodeString является ошибкой, burro му чго функция попытается безусловно освободить блок памяти, который является частью нашего кода или данных. Во втором случае вызов RtlFreeUnicode-pbing обязателен для предотвращения утечки памяти.
’ Мораль: вы должны знать, откуда была получена память в любой структуре IJNICODE-STRING, и освобождать ее только в случае необходимости.
^>угие методы программирования
S оставшейся части этой главы обсуждаются прочие навыки, которые могут приго-Житься при написании драйвера. Мы начнем с работы с системным реестром — (базой данных, содержащей различные конфигурационные и управляющие данные, ^лияющие на работу кода и оборудования. Далее рассматриваются операции С дисковыми файлами и именованными устройствами и буквально в нескольких £ловах описываются вещественные вычисления в драйверах WDM. Напоследок я опишу некоторые конструкции, упрощающие отладку драйвера, — они пригодятся в «маловероятной» ситуации, если драйвер вдруг не заработает с первого раза. ^Ьбота с реестром
В Windows ХР и Windows 98/Ме конфигурационные данные и прочая важная Мнформация хранятся в базе данных, называемой реестром. В табл. 3.10 перечислены функции, которые могут вызываться драйверами WDM для обращения * реестру. Если вам уже доводилось программировать операции с реестром в про-&рйммах пользовательского режима, вероятно, вы разберетесь и с реестровыми функциями в драйверах. Однако, на мой взгляд, реестровые функции режима ядра довольно сильно отличаются от функций пользовательского режима, и об их возможном применении стоит рассказать подробнее.
144
Глава 3. Основные приемы программирования
Таблица 3.10. Функции работы с реестром
Функция	Описание
loOpenDeviceRegistryKey	Открывает специальный раздел, связанный с объектом физического устройства (PDO)
loOpenDevicelnterfaceRegistryKey	Открывает раздел реестра, связанный с зарегистрированным интерфейсом устройства
Rtl DeleteRegistryValue RtIQueryRegistryValues Rtl WriteRegistryVal ue ZwClose	Удаляет параметр из реестра Читает несколько параметров из реестра Записывает параметр в реестр Закрывает манипулятор раздела реестра
ZwCreateKey ZwDeleteKey ZwDeleteValueKey ZwEnumerateKey ZwEnumerateValueKey ZwFlushKey ZwOpenKey ZwQueryKey ZwQueryValueKey ZwSetValueKey	Создает раздел в реестре Удаляет раздел из реестра Удаляет параметр (Windows 2000 и выше) Перечисляет подразделы Перечисляет параметры в разделе реестра Записывает изменения в реестре на диск Открывает раздел реестра Получает информацию о разделе реестра Получает параметр из раздела реестра Задает параметр в разделе реестра
Среди прочего, в этом разделе обсуждаются семейство ZwXxx и функция RtIDeleteRegistryValue. В совокупности они обеспечивают базовую функциональность работы с реестром, достаточную для большинства драйверов WDM.
Открытие раздела реестра
Перед чтением параметров из реестра необходимо открыть раздел, в котором они находятся. Существующие разделы открываются функцией ZwOpenKey. Функция ZwCreateKey либо открывает существующий, либо создает новый раздел. Обе функции требуют предварительной инициализации структуры OBJECT-ATTRIBUTES с именем раздела и (возможно) дополнительной информацией. Объявление структуры OBJECT-ATTRIBUTES выглядит так:
typeclef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDIrectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurltyQualityOfService;
} OBJECTJVTTRIBUTES;
Вместо того чтобы инициализировать экземпляр структуры вручную, удобнее воспользоваться макросом InitializeObjectAttributes.
Другие методы программирования
145
Допустим, мы хотим открыть раздел службы для нашего драйвера. I/O Manager передает имя этого раздела в параметре DriverEntry, что позволяет использовать код следующего вида:
NTSTATUS DriverEntry (PDRIVERJBJECT DriverObject.
PUNICODE_STRING RegistryPath)
{
OBJECT_ATTRIBUTES oa;
Init1alizeObjectAttr1butes(&oa, RegistryPath, OBJ_KERNEL_HANDLE // 1
OBJ_CASE_INSENSITIVE, NULL, NULL):
HANDLE hkey:
status = ZwOpenKey(&hkey, KEY_READ, &oa);	// 2
If (NT_SUCCESS(status))
{
ZwClose(hkey);	//3
}
}
1.	Структура атрибутов объекта инициализируется реестровым путем, полученным от I/O Manager, и NULL в качестве дескриптора безопасности. Функция ZwOpenKey все равно игнорирует дескриптор безопасности — атрибуты безопасности указываются только при первом создании раздела.
2.	Функция ZwOpenKey открывает раздел для чтения и сохраняет полученный манипулятор в переменной hKey.
3.	ZwClose — обобщенная функция закрытия манипуляторов объектов режима ядра. В данном случае она используется для закрытия манипулятора раздела реестра.
Флаг OBJ_KERNEL_HANDLE, встречающийся в предыдущем примере, играет важную роль в обеспечении целостности системы. Если при вызове ZwOpenKey выполнение идет в контексте пользовательского потока и этот флаг не установлен, то полученный манипулятор будет доступен для процессов пользовательского режима. Может случиться даже так, что код пользовательского режима закроет манипулятор и откроет новый объект, получив манипулятор с тем же числовым значением. После этого вызовы реестровых функций в вашем коде будут выполняться с неверным манипулятором.
Хотя объект реестра обычно называется базой данных, он не обладает некоторыми атрибутами «настоящих» баз данных — например, он не позволяет закреплять или отменять внесенные изменения. Более того, права доступа, указанные при открытии раздела (KEY_READ в предыдущем примере), предназначены для проверок безопасности, а не для предотвращения несовместимого совместного доступа. Другими словами, два разных процесса могут одновременно открыть один раздел с указанием доступа для записи (например). Впрочем, система защищается от разрушающей записи, происходящей одновременно
146
Глава 3. Основные приемы программирования
с чтением, и гарантирует, что раздел не будет удален, пока для него существую! открытые манипуляторы.
Другие способы открытия разделов реестра
Помимо ZwOpenKey, в Windows предусмотрены еще две функции для открытия разделов реестра.
Функция loOpenDeviceRegistryKey позволяет открыть один из специальных разделов, связанных с объектом устройства:
HANDLE hkey;
Status = loOpenDeviceRegistryKeytpdo, flag, access, &hkey):
где pdo — адрес объекта физического устройства (PDO), находящегося внизу стека драйверов, flag — признак открываемого специального раздела (табл. 3.11), a access — маска доступа (скажем, KEY_READ).
Таблица 3.11. Коды разделов реестра для параметра flag функции loOpenDeviceRegistryKey
Флаг	Раздел реестра
PLUGPLAY_REGKEY_DEVICE	Подраздел оборудования (экземпляра)	раздела Enum
PLUGPLAY REGKEY DRIVER	Подраздел драйвера раздела класса
Я очень часто использую функцию loOpenDeviceRegistryKey с флагом PLUGPLAY_ REGKEY_DRIVER в своих драйверах. В Windows ХР эта функция открывает подраздел Device Parameters раздела оборудования данного устройства. В Windows 98/Ме она открывает сам раздел оборудования. Информацию о параметрах оборудования следует хранить именно здесь. Разделы реестра будут подробно описаны в главе 15 в связи с установкой и распространением драйверов.
Функция loOpenDevicelnterfaceRegistryKey открывает раздел, связанный с экземпляром зарегистрированного интерфейса устройства:
HANDLE hkey;
status = IoOperiDeviceIriterfaceRegistryKey(linkrianie, access, &hkey);
где linkname — имя символической ссылки на зарегистрированный интерфейс, a access — маска доступа (скажем, KEY_READ).
Раздел интерфейса в реестре является подразделом HKLM\System\CurrentControl Set\Control\DeviceClasses, а его содержимое сохраняется между сеансами. В нем удобно хранить параметры, доступные для программ пользовательского режима, так как код пользовательского режима может получить доступ к этому разделу при помощи функции SetupDiOpenDevicelnterfaceRegKey.
Чтение и запись параметров
Разделы реестра обычно открываются для чтения параметров из базы данных. Основная функция, которая используется для этой цели, — ZwQueryValueKey. Например, для получения параметра ImagePath в разделе службы драйвера
Другие методы программирования
147
(не знаю, зачем может понадобиться это значение, но это и неважно) можно воспользоваться следующим кодом:
UNICODE-STRING valname:
Rt11n 1tUnlcodeStг1 ng(&va1 name, L"ImagePath”);
size = 0;
status = ZwQueryValueKey(hkey, &valname, KeyValuePartlal Information,
NULL, 0, Sslze):
If (status == STATUS_OBJECT_NAME_NOTJOUND size == 0) обработка ошибки>;
size = m1n(s1ze, PAGE_SIZE):
PKEY_VALUE_PARTIAL_INFORMATION vplp =
PKEY_VALUE_PARTIAL_INFORMATION) ExAllocatePool(PagedPool, size);
If (’vplp)
обработка ошибки>;
status = ZwQueryValueKey(hkey, Svalname, KeyValuePartlal Information,
vplp, size, Sslze):
If (!NTJUCCESS(status))
обработка ошибки>;
Операции c vpip->Data>
ExFreePool(vplp):
В этом фрагменте функция ZwQueryValueKey вызывается дважды. Первый вызов определяет, сколько памяти необходимо выделить для структуры KEY_VALUE_ JWTTIALJNFORMATION, которую мы собираемся прочитать. Второй вызов читает згу информацию. Я оставил код проверки ошибок в этом фрагменте, потому что Вв практике ошибки работали не совсем так, как я ожидал. В частности, я пред-Воложил, что первый вызов ZwQueryValueKey вернет STATUS_BUFFER_TOO_SMALL (из за передачи буфера нулевой длины). Тем не менее, этого не произошло. Осо-Ко важную роль играет код ошибки STATUS_OBJECT_NAME_NOT_FOUND, означаю-ккй. что параметра не существует; я проверяю только этот код. Другие ошибки, Ьвешающие работе ZwQueryValueKey, обнаруживаются при втором вызове.
ЖЕЧАНИЕ--------------------------------------------------------------------------
Течение до PAGE_SIZE выполняется для того, чтобы установить разумные ограничения на объем Ьоеляемой памяти. Получив доступ к разделу, из которого читаются данные, злоумышленник монет подменить параметр ImagePath сколь угодно большим значением. В этом случае драйвер пре-Ьэтмтся в невольного сообщника при проведении DoS-атак, основанных на выделении огромных Вьемов памяти. Как правило, драйверы используют разделы реестра, модификация которых раз-Ьяена только администраторам, а в распоряжении администратора есть и более эффективные Крсобы нарушить работоспособность системы. И все же дополнительная защита от всех атак по-Кбного рода не помешает.
В полученной таким способом структуре с «частичной» (PARTIAL) информа-Вю содержится значение параметра и описание его типа данных:
tyoedef struct _KEY_VALUE_PARTIAL_INFORMATION {
ULONG Titleindex:
ULONG Type:
148
Глава 3. Основные приемы программирования
ULONG DataLength;
UCHAR Data[l]:
} KEY_VALUE_PARTIAL_INFORMATION.
*PKEY_VALUE_PARTIAL_INFORMATION:
Поле Type определяет тип данных параметра; его возможные значения перечислены в табл. 3.12 (также существуют другие типы, но они не актуальны для драйверов устройств). Поле DataLength определяет длину данных, а поле Data содержит сами данные. Поле Titleindex к драйверам не относится. Вот некоторые полезные факты о различных типах данных:
О Тип REG_DWORD означает 32-разрядное целое без знака в формате данной платформы (с прямым или обратным порядком байтов).
О Тип REG_SZ описывает строку Юникода, завершенную нуль-символом. Завершитель учитывается в счетчике DataLength.
О Чтобы данные типа REG_EXPAND_SZ расширялись с подстановкой переменных окружения, для обращения к реестру должна использоваться функция RtIQueryRegistryValues. Внутренние функции, используемые при обращениях к переменным окружения, не документируются и не предоставляются для использования в драйверах.
О Функцию RtIQueryRegistryValues также удобно использовать для получения данных REG__MULTI_SZ — она вызывает указанную функцию обратного вызова по одному разу для каждой из нескольких строк.
ПРИМЕЧАНИЕ----------------------------------------------------------------------------—
Несмотря на очевидную полезность функции RtIQueryRegistryValues, я стараюсь обходиться без нее с тех пор, как она стала причиной сбоя в одном из моих драйверов. Насколько я понял, читаемое значение потребовало вызова вспомогательной функции, находящейся в секции инициализации ядра, а следовательно, отсутствовавшей на тот момент.
Таблица 3.12- Типы параметров реестра, используемые в драйверах WDM
Константа типа данных	Описание
REG_BINARY	Двоичные данные переменной длины
REG_DWORD	Длинное целое без знака в формате, естественном для платформы
REG-EXPAND_SZ	Строка Юникода, завершенная нуль-символом и содержащая служебные %-последовательности с именами переменных окружения
REG_MULTI-SZ	Одна или несколько строк Юникода, завершенных нуль-символами; блок данных завершается дополнительным нуль-символом
REG_SZ	Строка Юникода, завершенная нуль-символом
Чтобы присвоить значение параметру реестра, необходимо обладать доступом KEY_SET_VALUE к родительскому разделу. В приведенном примере использовался доступ KEY_READ, который не дает такого права. Также можно использовать
не методы программирования
149
- • уп KEY_WRTTE или KEY_ALL_ACCESS, хотя при этом вы получаете больше прав, - с необходимо. Далее вызывается функция ZwSetValheKey. Пример:
7'11n1tUni codeStгi ng(&va1 name, L"TheAnswer"):
_ NG value = 42:
_ SetValueKey(hkey. Svalname, 0, REG_DWORD, &value, sizeof(value));
Удаление подразделов или параметров
В зление параметра в открытом разделе осуществляется функцией RtlDelete-11к /Value в специальном формате?
-'?eleteReg1stryValue(RTL_REGISTRY_HANDLE, (PCU/STR) hkey, CTheAnswer");
RtJDeleteRegistryValue — обобщенная сервисная функция, первый аргумент ко-k fi обозначает одно из нескольких специальных мест реестра. Значение RTL_ - E31STRY_HANDLE, как в данном примере, означает, что у вас уже имеется откры-манипулятор раздела, в котором находится удаляемый параметр. Раздел пе-:• ется во втором аргументе (с преобразованием типа, чтобы компилятор не ж - '-вался). Третий и последний ар!умент определяет имя удаляемого параметра • рмате строки Юникода, завершенной нуль-символом. Это один из тех слу-Ьг когда для описания строки не нужно создавать структуру UNICODEJ5TRING.
Windows 2000 и выше для удаления параметров можно воспользоваться WHKi ней ZwDeleteValueKey (в DDK эта функция не документирована):
_ ODEJSTRING valname;
• \nitUn1codeString(&valname, L iheAnswer");
| ~*“eleteValiieKey(hkey, Svalname):
У даление раздела возможно только в том случае, если он открывался с раз-> ниями не ниже DELETE (которые вы получаете на уровне KEY_ALL_ACCESS).
лы удаляются функцией ZwDeleteKey:
"aleteKeydikey);
F аздел продолжает существовать вплоть до закрытия всех манипуляторов, но ж • .-дующие попытки открыть новый манипулятор для раздела или обратиться I н по любому манипулятору, открытому в данный момент, завершаются м STATUS,KEYJDELETED. Открытые манипуляторы должны быть в какой-то г р нт закрыты вызовом ZwClose (в описании ZwDeleteKey в DDK сказано, что Ж- -пг лятор становится недействительным, но это не так — его все равно необ-д/мо закрыть вызовом ZwClose).
е «числение подразделов и параметров
'рытыми разделами реестра может выполняться и такая относительно слож-ерация, как перечисление всех элементов (подразделов и параметров), со-Кр ващихся в этом разделе. Для этого сначала вызывается функция ZwQueryKey, ж* питающая информацию о подразделах и параметрах — их количестве, наи-ж й длине имени и т. д. Аргумент функции ZwQueryKey указывает, к какому
150
Глава 3. Основные приемы программирования
из трех типов относится запрашиваемая информация о разделе. Существуют три типа: базовая, узловая и полная информация. При подготовке перечисления сначала нас будет интересовать полная информация:
typedef struct _KEY_FULL_INFORMATION {
LARGEJNTEGER LastWriteTime;
ULONG Titlelndex;
ULONG ClassOffset;
ULONG ClassLength;
ULONG SubKeys;
ULONG MaxNameLen;
ULONG MaxClassLen;
ULONG Values;
ULONG MaxValueNameLen:
ULONG MaxValueDataLen;
WCHAR Classfl];
} KEY_FULL_INFORMATION, *PKEY_FULLJNFORMATION;
Структура имеет переменную длину, потому что Class[O] является всего лишь первым символом в имени класса. Обычно первый вызов определяет размер выделяемого буфера, а второй используется для получения данных:
ULONG size;
ZwQueryKeythkey, KeyFul1 Information, NULL, 0. Ssize),
size = min(size, PAGE_SIZE);
PKEYJULLJNFORMATION fip = (PKEY_FULL_INFORMATION)
ExAllocatePool(PagedPool, size);
ZwQueryKey(hkey, KeyFullInformation. fip, size, &size);
Если теперь вас интересует информация о подразделах вашего раздела реестра, воспользуйтесь циклическим вызовом ZwEnumerateKey:
for (ULONG 1 = 0: i < fip->SubKeys; ++i)
ZwEnumerateKey(hkey, i, KeyBasIcInformation, NULL, 0, Ssize);
size = mintsize, PAGE_SIZE);
PKEY-BASICJNFORMATION bip = (PKEY_BASIC_INFORMATION)
ExAllocatePool(PagedPool, size);
ZwEnumerateKey(hkey, 1, KeyBasicInformation, bip, size, &size);
Операции c bip->Name> ExFreePool (bip);
}
Главная информация о каждом подразделе — это его имя, которое присутствует в виде счетной1 строки Юникода в читаемой в цикле структуре KEY_BASIC_ INFORMATION:
typedef struct JEYJASICJNFORMATION { LARGEJNTEGER LastWriteTime;
1 Речь идет о строках с отдельно хранимой длиной, то есть конец строки определяется ие нуль-терминатором, что позволяет включать нуль-символы в саму строку. — Прымеч. персе.
Другие методы программирования
151
ULONG Туре:
ULONG NameLength;
WCHAR Named];
} KEYJASICJNFORMATION, *PKEYJASIC_INFORMATION;
Имя не завершается нуль-символ ом; его длина определяется полем Name-Length структуры. Не забудьте, что длина задается в байтах! Имя не является полным реестровым путем, и указывает только имя подраздела в том разделе, в котором он находится. На самом деле это даже удобно, потому что подраздел легко открывается по имени и открытому манипулятору родительского раздела.
Перечисление параметров в открытом разделе осуществляется следующим способом:
jLONG maxlen = fip->MaxValueNameLen +
S1 zeof(KEY_VALUE_BASIC_I NFORMATION);
raxlen = min(maxlen, PAGE_SIZE);
-KEY_VALUE_BASIC_INFORMATION vip = (P KE YJALUE JAS I ^INFORMATION)
ExAl1ocatePool(PagedPool. maxlen);
‘dr (ULONG i =0; 1 < f1p->Values; ++1)
{
ZwEnumerateValueKey(hkey, 1, KeyValueBasIcInformatlon, vip, maxlen &s1ze);
'Операции c vip->Name>
t/FreePool(vip);
Выделите память для самой большой структуры KEYJ'ALUEJASICJNFORMATION, [®оторую вам предстоит прочитать (определяется на основании поля MaxValue-MtameLen структуры KEY_FULLJNFORMATION). Внутри цикла выполняется некая обработка имени параметра, передаваемого в виде счетной строки Юникода в сле-Лующей структуре:
*
►
lyoedef struct _KEY_VALUEJASIC_INFORMATION {
ULONG Titleindex;
ULONG Type:
ULONG NameLength;
OAR Name[l];
\EY_VALUE JASIC ^INFORMATION, *PKEY_VALUE_BAS I CONFORMATION:
b И снова для выборки параметра достаточно знать его имя и иметь открытый Ванипулятор родительского раздела, как показано в предыдущем разделе.
f У ZwQueryKey и двух функций перечисления существуют разновидности, о ко-Врых я ничего не сказал. Например, можно получить полную информацию о под-злеле при вызове ZwEnumerateKey. Я всего лишь показал, как получить базовую ^формацию, включающую имя. Функция ZwEnumerateValueKey позволяет полу-Вгь только значения параметров или имена вместе со значениями. Я всего лишь Сказал, как получить имя параметра.
152
Глава 3. Основные приемы программирования
Работа с файлами
Иногда в драйверах WDM возникает необходимость в выполнении чтения/ записи обычных дисковых файлов — например, чтобы загрузить в устройство большой объем микрокода или для ведения собственного подробного журнала с какой-либо информацией. Для упрощения подобных операций существует семейство функций ZwXrx
Для работы с файлами посредством функций ZwXrx выполнение должно происходить на уровне PASSIVE-LEVEL (см. следующую главу) в потоке, который может быть безопасно приостановлен. На практике последнее требование означает, что вы не должны запрещать асинхронные вызовы АРС (Asynchronous Procedure Calls) при помощи функции KeEnterCriticalRegion. Как будет показано в следующей главе, некоторые примитивы синхронизации требуют повышения уровня IRQL выше PASSIVE-LEVEL или запрета АРС. Просто помните, что эти примитивы синхронизации и операции с файлами несовместимы.
Первым шагом для обращения к файлу на диске должно стать открытие манипулятора функцией ZwCreateFile. Полное описание этой функции в DDK выглядит довольно сложно из-за разнообразных возможностей ее применения. Я приведу лишь два простых сценария, которые пригодятся для чтения или записи файлов с уже известными именами.
ПРИМЕР КОДА--------------------------------------------------------------------
Драйвер FILEIO демонстрирует вызовы некоторых функций ZwXxx, описанных в этом разделе. Данный пример полезен еще и тем, что в нем представлены обходные решения для платформенных несовместимостей, о которых будет рассказано в конце главы.
Открытие существующего файла для чтения
Следующий пример показывает, как открыть существующий файл для чтения данных:
NTSTATUS status,
OBJECT-ATTRIBUTES оа;
IO_STATUS-BLOCK lostatus;
HANDLE hfile;	// Результат открытия
PUNICODE-STRING pathname; // Исходные данные
Initial!zeObjectAttrlbutes(&oa, pathname,
OBJ-CASE-INSENSITIVE OBJ_KERNEL_HANDLE. NULL, NULL):
status = ZwCreateFile(&hfile, GENERIC_READ, &oa, Siostatus,
NULL, 0, FILE_SHARE_READ. FILE_OPEN,
FILE-SYNCHRONOUSJO_NONALERT, NULL, 0);
Создание или перезапись файла
Чтобы создать новый файл или усечь существующий файл до нулевой длины.
замените вызов ZwCreateFile в приведенном фрагменте следующим:
status = ZwCreateFile(&hfile, GENERIC_WRITE, &оа. Siostatus,
NULL, FILE-ATTRIBUTE NORMAL, 0, FILE_OVERWRITE_IF,
FILE-SYNCHRONOUS-IO-NONALERT, NULL. 0):
Другие методы программирования
153
В обоих фрагментах инициализируется структура OBJECT-ATTRIBUTES, предназначенная в основном для указания полного имени открываемого файла. Присутствие атрибута OBJ_CASE_INSENSITIVE объясняется тем, что модель файловой системы Win32 игнорирует различия регистра символов в именах файлов. Флаг OBJ_KERNEL_HANDLE используется по тем же причинам, что и в примере, приводившемся ранее в этой главе. Затем функция ZwCreateFile открывает манипулятор.
Первый аргумент ZwCreateFile (&hfile) определяет адрес переменной HANDLE, в которой функция ZwCreateFile возвращает созданный манипулятор. Второй аргумент (GENERIC_READ или GENERIC-WRITE) определяет уровень доступа, необходимый манипулятору для выполнения чтения или записи. Третий аргумент (&оа) содержит адрес структуры OBJECT-ATTRIBUTES с именем файла. Четвертый аргумент ссылается на структуру IO_STATUS_BLOCK, в которую заносится код диспозиции, описывающий, как функция ZwCreateFile выполнила затребованную операцию. Открывая для существующего файла манипулятор только для чтения, мы ожидаем, что поле Status этой структуры будет равным FILE_OPENED. При открытии манипулятора только для записи в этом поле должен оказаться код FILE-OVERWRITTEN или FILE_CREATED в зависимости от того, существовал файл ранее или нет. Пятый аргумент (NULL) может быть указателем на 64-разрядное целое, определяющее исходный объем дискового пространства, выделяемого для файла. Этот аргумент важен только при создании или перезаписи файлов; если опустить его, как это сделано в моем примере, файл будет увеличиваться от нулевой длины по мере записи данных. Шестой аргумент (0 или FILE-ATTRIBUTE-NORMAL) содержит флаги атрибутов для создаваемых файлов. Седьмой аргумент (FILE-SHARE-READ или 0) указывает, каким образом файл может использоваться совместно с другими потоками. Если файл открывается для ввода, вероятно, одновременное чтение данных несколькими потоками вполне допустимо. Но если файл открывается для последовательного вывода, вероятно, обращения к нему со стороны других потоков следует полностью запретить.
Восьмой аргумент (FILE_OPEN или FILE_OVERWRITE_IF) указывает, как следует действовать, если файл существует (или не существует). При открытии файла только для чтения была использована константа FILE_OPEN, потому что я предполагал, что файл уже существует, и при его отсутствии должен был произойти сбой. При открытии только для записи была использована константа FILE_ OVERWRITE-IF, чтобы существующие одноименные файлы заменялись, а несуществующие создавались. Девятый аргумент (FILE_SYNCHRONOUSJO_NONALERT) определяет дополнительные битовые флаги, управляющие операцией открытия и последующим использованием манипулятора. В приведенном примере я указал, что операции ввода/вывода буду выполняться синхронно (то есть функции чтения и записи не будут возвращать управление до завершения ввода/вывода). Десятый и одиннадцатый аргументы (NULL и 0) содержат, соответственно, необязательный указатель на буфер расширенных атрибутов и длину этого буфера.
При нормальном выполнении функция ZwCreateFile должна вернуть STATUS- SUCCESS и задать значение переменной манипулятора. Далее программист
154
Глава 3. Основные приемы программирования
выполняет любые операции чтения/записи при помощи функций ZwReadFile и ZwWriteFiie, после чего закрывает манипулятор вызовом ZwClose:
ZwClose(hflle):
Операции чтения/записи выполняются синхронно или асинхронно в зависимости от флагов, указанных при вызове ZwCreateFile. В простых сценариях, описанных ранее, операции выполняются синхронно, то есть возвращают управление лишь после их завершения. Пример:
PVOID buffer;
ULONG bufsize;
status = ZwReadF11e(hf11e, NULL, NULL, NULL, Slostatus, buffer, bufsize, NULL, NULL);
- или -
status = ZwWrlteFIleChflle. NULL, NULL, NULL, Slostatus, buffer, bufsize, NULL, NULL);
Эти вызовы аналогичны неперекрывающимся вызовам ReadFile или WriteFile в пользовательском режиме. Когда функция вернет управление, в поле iostatus. Information хранится количество байтов, переданных в результате операции, -возможно, вам пригодится эта информация.
О ВИДИМОСТИ МАНИПУЛЯТОРОВ-------------------------------------------------------
Каждый процесс обладает приватной таблицей манипуляторов, связывающей указатели на объекты ядра с числовыми манипуляторами. При открытии манипулятора функцией ZwCreateFile или NtCreateFile этот манипулятор принадлежит процессу, который являлся текущим на данный момент, если только при вызове не использовался флаг OBJ_KERNEL_HANDLE. При завершении процесса принадлежащие ему манипуляторы пропадают. Более того, при использовании манипулятора в контексте другого процесса вы косвенно ссылаетесь на объект (если он существует), которому этот манипулятор соответствует в другом процессе. С другой стороны, манипулятор ядра хранится в глобальной таблице, содержимое которой продолжает существовать вплоть до завершения работы операционной системы и может использоваться любым процессом без каких-либо неоднозначностей.
Если вы захотите прочитать весь файл в буфер, находящийся в памяти, предварительно определите его общую длину функцией ZwQuerylnformationFile:
FILE_STANDARDJNFORMATION si;
ZwQueryInformat1onF11e(hfile, &iostatus, &s1, slzeof(sl).
Fl1 eStandardinformation);
ULONG length = si.EndOfFIle.LowPart;
КОГДА ВЫПОЛНЯЮТСЯ ОПЕРАЦИИ С ФАЙЛАМИ----------------------------------------------------
Чтение дисковых файлов в драйверах WDM обычно выполняется при инициализации устройства в ответ на запрос IRP_MN_START_DEVICE (см. главу 6). Возможность обращения к файлам по стандартным именам вида \??\C:\dir\file.ext зависит от того, на какой стадии устройство включается в процесс инициализации. Для надежности поместите файлы данных в каталоге, находящемся в системном корневом каталоге, и используйте имена вида \SystemRoot\dir\file.ext. Ветвь SystemRoot пространства имен доступна всегда, так как для запуска операционной системы необходимо чтение файлов с диска.
Другие методы программирования
155
ПРИМЕР КОДА------------------------------------------------------------------------
Пример RESOURCE объединяет ряд концепций, описанных в этой главе. Он демонстрирует работу с данными, хранящимися в стандартных ресурсных сценариях, из драйвера. Как вы сами убедитесь при просмотре кода, это не такая уж простая задача.
Вещественные вычисления
В некоторых случаях целочисленной арифметики оказывается недостаточно, и тогда приходится выполнять вещественные вычисления (с плавающей точкой). На процессорах Intel математический процессор также используется для выполнения команд из набора MMX (Multimedia Extensions). Исторически вещественные вычисления в драйверах были сопряжены с двумя проблемами. Операционная система может эмулировать отсутствующий сопроцессор, однако эмуляция обходится дорого, и в ней обычно задействуются процессорные исключения. Обработка исключений, особенно на высоких уровнях IRQL, в режиме ядра создает немало трудностей. Кроме того, на компьютерах, оснащенных аппаратными сопроцессорами, архитектура центрального процессора может потребовать отдельной затратной операции для сохранения и восстановления состояния сопроцессора при переключении контекста. Из-за этого традиционно господствовало мнение, что в драйверах вещественные вычисления лучше не использовать.
В Windows 2000 и последующих системах появился обходной путь для ре-ения прошлых проблем. Прежде всего, системный поток (см. главу 14), ра-отающий на уровне DISPATCH LEVEL и ниже, свободно использует математиче-кий сопроцессор по своему усмотрению. Кроме того, драйверы, выполняемые контексте произвольного потока на уровне DISPATCH_LEVEL и ниже, могут за-тючать операции с математическим сопроцессором между двумя вызовами истемных функций:
SSERTCKeGetCurrentlrqlО <= DISPATCH LEVEL);
 -LOATINGJAVE FloatSave;
.TSTATUS status = KeSaveFloatjngPointState(&FloatSave);
f (NT_SUCCESS(status))
{ .
\eRestoreFloatingPointState(&FloatSave);
Эти вызовы всегда используются в паре, как показано здесь. Они сохраняют г | станавливают «устойчивое» состояние математического сопроцессора для Гущ его центрального процессора ~~ то есть всю информацию состояния, про-1 ющую существовать за рамками одной операции (содержимое регистров, Ьр ляюгпие слова и т. д.). В одних процессорных архитектурах это может Ж- те не приводить к дополнительным затратам, потому что архитектура из-ha но позволяет любому процессу выполнять вещественные операции. В дру-I хитетаурах сохранение и восстановление информации состояния сопряжено |о 'вателыюй работой. По этой причине Microsoft рекомендует по возможно-ГТ» бегать вещественных вычислений в драйверах режима ядра.
156
Глава 3. Основные приемы программирования
ПРИМЕР КОДА------------------------------------------------—---------------------
Пример FPCITEST демонстрирует один из способов использования команд вещественных вычислений и ММХ в драйверах WDM,
Как я уже сказал, конкретные события, происходящие при вызове KeSave-FloatingPointState, определяются архитектурой процессора. Скажем, на процессорах с архитектурой Intel эта функция сохраняет полное состояние вещественных вычислений командой FSAVE. Информация состояния может сохраняться либо в контекстном блоке, связанном с текущим программным потоком, либо в области динамически выделяемой памяти. В закрытой области FloatSave сохраняется метаинформация о сохраненном состоянии, по которой функция KeRestoreFloating-PointState в дальнейшем может корректно восстановить это состояние.
Если на компьютере отсутствует «физический» сопроцессор, вызов KeSave-FloatingPointState завершается неудачей с кодом STATUS_ILLEGAL_FLOAT_CONTEXT (кстати, на многопроцессорных компьютерах либо все процессоры должны обладать сопроцессорами, либо ни один из них сопроцессором не обладает). Следовательно, если компьютер не оснащен сопроцессором, необходимо либо включить в драйвер альтернативный код для выполнения запланированных вычислений, либо просто отказаться от загрузки (инициируя отказ в DriverEntry).
ПРИМЕЧАНИЕ----------------------------------------------------------—-----------------
Функция ExIsProcessorFeaturePresent проверяет различные возможности, связанные с вещественными вычислениями. Поскольку в Windows 98/Ме эта функция не поддерживается, вам придется поставлять вместе с драйвером заглушку WDMSTUB. За дополнительной информацией об этой и других несовместимостях систем обращайтесь к приложению А.
Упрощение отладки
Мои драйверы всегда содержат ошибки. Возможно, вы принадлежите к числу счастливчиков, которые сразу все делают правильно. Но если нет — вам придется провести немало времени в отладчике за попытками выяснить, почему программа что-нибудь делает (или не делает) именно так, а не иначе. Я не стану затрагивать потенциально спорную тему выбора лучшего отладчика или обсуждать методы отладки драйверов, которые ближе к искусству, чем к точной науке. И все же при программировании драйвера можно принять некоторые меры, упрощающие работу по отладке кода.
Отладочная и свободная сборка
При построении драйвера программист выбирает между отладочной и свободной сборкой. В среде отладочной сборки препроцессорное символическое имя DBG равно 1, тогда как в среде свободной сборки оно равно 0. Следовательно, вы можете ввести в драйвер дополнительный код, который будет выполняться только в отладочной версии:
#1f DBG
дополнительный отладочный код>
#endif
Другие методы программирования
157
Макрос Kd Print
Простой вывод сообщений принадлежит к числу самых полезных методов отладки из когда-либо изобретенных. Я пользовался им, когда только начинал изучать программирование (язык FORTRAN на самой настоящей ламповой вычислительной машине), и продолжаю пользоваться им сегодня. Сервисная функция режима ядра DbgPrint выводит отформатированное сообщение в окне, предоставленном вашим отладчиком. Другой способ просмотра вывода DbgPrint основан на использовании утилиты DebugView с сайта http://www.sysinternals.com. Вместо прямого вызова DbgPrint часто бывает удобнее использовать макрос с именем KdPrint, который вызывает DbgPrint при истинном значении DBG и не генерирует кода при ложном значении:
•d?rint((DRIVERNAME " - KeReadProgrammersMind failed - П\п", status)):
Присутствие двух пар круглых скобок в KdPrint объясняется особенностями ее определения. Первый аргумент представляет собой строку со служебными %-последовательностями. Второй, третий и последующие аргументы содержат значения, подставляемые на место %-последовательностей. Макрос расширяется в вызов DbgPrint, во внутренней реализации которого для форматирования строки применяется стандартная библиотечная функция _vsnprintf. Следовательно. вы можете использовать тот же набор %-последовательностей, доступный хтя прикладных программ, вызывающих эту функцию, за исключением последовательностей для вещественных чисел.
Во всех своих драйверах я определяю константу с именем DRIVERNAME:
refine DRIVERNAME "ххх"
Где ххх — имя драйвера. Вспомните, что компилятор воспринимает две смежные строковые константы как единую константу’. Этот трюк позволяет мне копиро-[ать и вставлять целые функции вместе с вызовами KdPrint между драйверами 0ез внесения изменений в исходный код.
|Накрос ASSERT
Другой полезный отладочный прием основан на применении макроса ASSERT:
• ASSERTCl 4 1 == 2):
В отладочной версии драйвера ASSERT генерирует код проверки логического выражения. Если выражение ложно, ASSERT пытается прервать выполнение программы в отладчике, чтобы программист мог разобраться в происходящем. Если Выражение истинно, программа продолжает нормально выполняться. Кстати говоря, отладчики режима ядра прерывают работу программы при невыполнении условий ASSERT даже в окончательных сборках операционных систем.
НИЕ----------------------------------------------------------------------------
невыполнении ASSERT в окончательной сборке операционной системы без отладчика режима > происходит фатальный сбой.
158
Глава 3. Основные приемы программирования
Driver Verifier
Утилита Driver Verifier входит как в отладочные, так и в свободные сборки операционных систем и быстро становится одним из основных инструментов Microsoft для проверки качества драйверов. Driver Verifier запускается из меню Пуск, а при запуске пользователь проходит через несколько экранов программы-мастера (wizard). Следующее краткое описание поможет вам разобраться в этих страницах на первых порах.
Рис. 3-14. Начальная страница мастера Driver Verifier
На рис. 3.14 изображена начальная страница мастера. Я рекомендую установить переключатель Create Custom Settings (For Code Developers) (Создание пользовательских настроек (для программистов)). В этом варианте вы сможете подробно указать, какие именно функции Driver Verifier нужно активизировать в вашем конкретном случае.
После выбора предложенного режима на первой странице на экране появляется вторая страница (рис. 3.15). Здесь я рекомендую установить режим Select Individual Settings From A Full List (Выбор настроек из полного списка).
На следующей странице (рис. 3.16) выбираются нужные режимы проверки. Они добавляются к тем проверкам, которые Driver Verifier выполняет автоматически.
На момент написания книги были доступны следующие режимы:
О Special Pool — вся память в проверяемых драйверах выделяется из специального пула. Как упоминалось ранее в этой главе, такие блоки располагаются в конце (или начале) страницы, поэтому сохранение данных перед выделенной памятью (или до нее) приводит к немедленному фатальному сбою.
О Pool Tracking — заставляет Driver Verifier отслеживать операции выделения памяти, выполненные проверяемыми драйверами. Пользователь может наблюдать
Л: -. е методы программирования
159
а статистикой использования памяти и за тем, как она изменяется со временем. Driver Verifier также гарантирует освобождение всей выделенной памяти при тгрузке проверяемых драйверов, что упрощает выявление утечки памяти.
Рис. 3.15. Вторая страница мастера Driver Verifier
Рис. 3.16. Страница выбора пользовательских настроек в мастере Driver Verifier
J :е IRQL Checking — фактически обеспечивает очистку перемещаемой па-м -1 и каждый раз, когда проверяемый драйвер поднимает IRQL до уровня С :S3ATCH_LEVEL и выше. Эта операция помогает выявлять некорректные общения к перемещаемой памяти в драйверах. Учтите, что при включении 9  >го режима система работает относительно медленно.
160
Глава 3. Основные приемы программирования
О I/O Verification — заставляет Driver Verifier выполнять базовые проверки пакетов IRP, создаваемых драйвером или пересылаемых другим драйверам.
О Enhanced I/O Verification — пытается выявить ошибки драйверов в пограничных случаях (скажем, при некорректной обработке РпР и Power IRP) на основании предположений о порядке загрузки драйверов PnP Manager и т. д. Кстати говоря, некоторые проверки выполняются при первоначальном запуске драйвера и могут помешать нормальному старту системы.
О Deadlock Detection — строит диаграмму с иерархией блокировок для спин-блокировок, мьютексов и быстрых мьютексов, используемых проверяемыми драйверами, с целью выявления потенциальных взаимных блокировок.
О DMA Checking — следит за тем, чтобы проверяемые драйверы использовали при работе с DMA только методы, предписанные в DDK,
О Low Resources Simulation — имитация случайных сбоев при выделении памяти проверяемыми драйверами, начиная через 7 минут после запуска системы. Таким образом проверяется обработка возвращаемых значений, получаемых драйверами при выделении памяти.
В DDK описана специальная процедура активизации проверок для драйверов мини-портов SCSI.
ПРИМЕЧАНИЕ-------------------------------------—------------------———---------
Выбранные режимы могут быть связаны между собой. Например, возможность в настоящее время включения проверки DMA или выявления взаимных блокировок приводит к отключению расширенной проверки ввода/вывода (Enhanced I/O).
Также следует помнить, что Driver Verifier быстро развивается. За обновленной информацией обращайтесь к используемой версии DDK.
После выбора режимов проверки открывается последняя страница мастера (рис. 3.17). На этой странице выбираются проверяемые драйверы, для чего пользователь устанавливает флажки в соответствующих строках списка. Затем компьютер необходимо перезагрузить, потому что многие проверки Driver Verifier требуют инициализации на стадии загрузки. На мой взгляд, при отладке драйверов удобнее, если драйвер не был загружен на момент перезапуска системы. В списке такие драйверы отсутствуют, и их приходится добавлять кнопкой Add Currently Not Loaded Driver(s) To The List (Включить в список драйверы, не загруженные в данный момент).
Кстати говоря, сбои Driver Verifier являются фатальными. Для выявления причины сбоя в системе должен работать отладчик режима ядра (или вам придется анализировать аварийный дамп).
Не распространяйте отладочные версии!
Любой пользователь отладчиков режима ядра скажет вам, что распространять отладочную версию драйвера крайне нежелательно. Как правило, отладочные версии содержат многочисленные директивы ASSERT, которые будут некстати срабатывать во время отладки программ. Скорее всего, это приведет к выводу множества посторонних сообщений, которые лишь затемняют сообщения от
Проблемы совместимости с Windows 98/Ме
161
вашего драйвера. Припоминаю одну фирму, которая распространяла отладочную  рсию драйвера для популярной сетевой карты. Этот драйвер регистрировал каждый пакет, получаемый им по сети. Сейчас я беру с собой на консультации ма-л нький драйвер-«заплатку», который исправляет недостаток исходного драйвера и «затыкает ему рот». А еще я больше не покупаю сетевые карты этой фирмы.
££ Driver Verifier Manager
ВЕЗ
( Select drivers to verify
Verify? ’	| Drivers
□	acpi.sys
□	afdsys
□	agp440.sys
□	aic78u2 sys
□	aic78xx.sys
□	atapi.sys
□	audstub.sys
Г"1	kaisn e-irr-
Microsoft Corporation Microsoft Corporation Microsoft Corporation Microsoft Corporation Microsoft Corporation M 1с*гги>г>ГГ Г'лтлгаГ|<-.г1
И  ДеЙ
1 Provider
Microsoft Corporation
Microsoft Corporation
51.2600 0 (xpclient.O .
5,1.2600.0 [xpclient.O.. v3 60a (Lab01_N 01 .. v3.60a (Lab01_N.01 .
5.1.2600.0 (XPCIient.. 5.1.2600 OfXPCIient.... £ 1 ОСЛО Л (VPrt.orJ
| Version ..............
5.1.2600.0 (xpclient 0...
Click Finish after selecting the drivers to verify. The current settings will be saved and this program will exit Click В ack.-to review or change the settings you want to create or to select another set of drivers verify.
< Elack Finish [ Cancel
Рис. 3.17. Страница выбора драйвера для Driver Verifier
эблемы совместимости с Windows 98/Ме йловые операции ввода/вывода
Функции ZwAxr для работы с дисковыми файлами плохо работают в Windows 96 Me из-за двух проблем. Одна проблема обусловлена архитектурой Windows, а ая — похоже, обычной ошибкой в исходной версии Windows 98.
1 рвая проблема с файловыми операциями возникает из-за порядка инициали-
I различных драйверов виртуальных устройств в Windows 98/Ме. Confi-gcrari n Manager (CONFIGMG.VXD) инициализируется раньше Installable File System Mar ger (IFSMGR.VXD). Драйверы WDM устройств, существующих на момент Залу ка, получают запросы IRP_MN_START_DEVICE во время фазы инициализации COXFIGMG. Но поскольку инициализация IFSMGR к этому моменту еще не 1ве*_ ена, драйвер не может выполнять файловые операции ввода/вывода с ис-хтъз ванием ZwCreateFile и других функций, упоминавшихся ранее в этой главе. cl.l того, драйвер WDM не может отложить обработку IRP_MN_START_DEVICE > 7 :>г момента, как функциональность файловой системы станет доступной. Если сис еме не работает отладчик вроде Soft-Ice/W, вы увидите «синий экран» жз.1 юой на ошибку защиты Windows при инициализации CONFIGMG.
162
Глава 3. Основные приемы программирована
Вторая, более серьезная проблема существовала в версии Windows 98 от июля 1998 года. Она была связана с проверкой аргументов функциями ZwReadFile ZwWriteFile и ZwQuerylnformationFile. При передаче IO_STATUS_BLOCK в памяти режима ядра (а это, фактически, единственный путь) эти функции обращались по несуществующему виртуальному адресу. В исходной версии системы возникающий страничный сбой перехватывался структурированным обработчиком исключения, и драйвер получал код STATUS_ACCESS_VIOLATION даже в том случае, если все было сделано правильно. Я не знаю обходных решений этой проблемы, кроме представленного в примере FILEIO. Кстати говоря, ошибка была исправлена в Windows 98, Second Edition.
Программа FILEIO в прилагаемых материалах показывает, как обойти эти проблемы Windows 98/Ме. Она на стадии выполнения решает, можно ли вызывать функции ZwXxx или же для выполнения файловых операций следует прибегнуть к сервису VxD.
Вещественные вычисления
Вещественные вычисления допустимы в драйверах WDM для Windows 98/Ме, но по сравнению с Windows ХР для них устанавливается ряд важных ограничений: О Вещественные вычисления (включая операции ММХ) могут выполняться драйверами WDM только в системном потоке. К этой категории относятся как потоки, созданные в вашем коде вызовами PsCreateSystemThread, так и системные рабочие потоки. Учтите, что обратные вызовы рабочих элементов производятся в системном рабочем потоке, поэтому в таких обратных вызовах можно выполнять вещественные вычисления.
О Вещественные вычисления выполняются только на уровне PASSIVE_LEVEL (уровень DISPATCH_LEVEL в Windows 98/Ме соответствует обработке аппаратных прерываний VxD).
DDK предупреждает, что программисты не должны пытаться обходить эти правила. Например, у вас может возникнуть искушение использовать KeSaveFloating-PointState и KeRestoreFloatingPointState в обработчике IOCTL, несмотря на явно выраженный запрет, или же вручную сохранить и восстановить состояние сопроцессора. Но если при исходном сохранении состояния в сопроцессоре оставалось необработанное исключение, оно будет перенесено и в момент восстановления состояния. Ядро не сможет правильно обработать такое исключение. У проблемы не существует обходных решений, потому что она обусловлена особенностями архитектуры процессоров Intel и механикой работы VMCPD.VXD.
Обратите внимание — программа FPUTEST соблюдает эти правила и отказывается работать в Windows 98/Ме.
Синхронизация
Microsoft Windows ХР — многозадачная операционная система, способная работать в симметричной многопроцессорной среде. Я не собираюсь формально описывать многозадачные возможности Microsoft Windows ХР; хорошим источником допол-жггтельной информации послужит книга Дэвида Соломона и Марка Руссиновича <Inside Windows 2000», 3rd ed. (Microsoft Press, 2000). Все, что необходимо разработчику драйверов, — понять, что его код выполняется в контексте некоторого жрограммного потока (и что контекст потока может изменяться между обращениями к его коду) и что из-за специфики многозадачности управление может выть перехвачено у него практически в любой момент. Более того, на многопроцес-сс<?ных компьютерах возможно полноценное одновременное выполнение несколь-ютх потоков. В общем случае необходимо учитывать два наихудших сценария: О Операционная система может в любой момент принудительно вытеснить лю-
Чю функцию на произвольно долгий промежуток времени, поэтому нельзя -ыть уверенным в том, что критическая операция завершится без вмешательства или задержек.
О Даже если предпринять действия для предотвращения вытеснения, одновременное выполнение кода на другом процессоре того же компьютера может повлиять на выполнение нашего — возможно даже параллельное выполнение одного набора команд, принадлежащих одной из наших программ, в контексте двух разных потоков.
В Windows ХР для решения общих проблем синхронизации существуют раз-жчные примитивы. Для упорядочения обработки аппаратных и программных прерываний в системе устанавливаются уровни запросов прерываний (IRQL). Систена предоставляет в распоряжение программиста разнообразные примитивы синхронизации. Некоторые из них уместны в ситуациях, в которых возмож-ж> Чзопасное установление и снятие блокировки1 потоков. Примитив, называе-ЖжТ спин-блокировкой (spin lock), позволяет синхронизировать доступ к общим ресурсам даже в тех случаях, когда блокировка потоков запрещена из-за уровня Приоритета, на котором работает программа.
К : жалению, термин «блокировка» в русскоязычной литературе используется для перевода <зк locking, то есть собственно блокировки в традиционном понимании, так и blocking, тс -сть приостановки с ожиданием. — Примеч. персе.
164
Глава 4. Синхронизации
Основные проблемы синхронизации
Банальный пример поможет изложению материала. Допустим, в драйвере определяется статическая целочисленная переменная, используемая для некоторой цели — скажем, для подсчета количества необработанных запросов ввода/вывода.
static LONG 1 ActiveRequests;
Переменная увеличивается при получении запроса и уменьшается при его завершении:
NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp)
{
++1ActiveRequests;
... // Обработка запроса PNP
--1Act1veRequests;
}
Конечно, вы отлично понимаете, что такой счетчик не должен быть статической переменной: он должен входить в расширение устройства, чтобы у каждого объекта устройства был собственный уникальный счетчик. Но пока давайте сделаем вид, что драйвер всегда управляет одним экземпляром устройства. Чтобы пример стал более содержательным, сделаем еще одно последнее допущение: когда наступает момент для удаления объекта устройства, вызывается специальная функция вашего драйвера. Эту операцию желательно отложить до того момента, когда не останется ни одного необработанного запроса, поэтому в программу включается проверка счетчика:
NTSTATUS Handl eRemoveDevice(PDEVICE_OBJECT fdo, PIRP Irp)
if (1ActiveRequests)
<ожидать завершения всех запросов>
loDeleteDevice(fdo);
}
Кстати говоря, в этом примере описана вполне реальная проблема, решением которой мы займемся в главе 6 при обсуждении запросов РпР (Plug and Play). I/O Manager может попытаться удалить одно из устройств при наличии активных запросов, и нам необходимо защититься от подобных попыток при помощи некоторой разновидности счетчика. В главе 6 я покажу, как эта проблема решается при помощи ToAcquireRemoveLock и других взаимосвязанных функций.
В только что приведенных фрагментах кода кроется ужасная проблема синхронизации, но чтобы она стала очевидной, необходимо заглянуть за кулисы операций инкремента/декремента в DispatchPnp. На процессоре х86 компилятор может реализовать их в виде следующих последовательностей команд:
; ++lActiveRequests;
mov eax, 1ActiveRequests
add eax, 1
Основные проблемы синхронизации
165
mov lActiveRequests, eax
; --lActiveRequests;
mov eax, lActiveRequests
sub eax, 1
mov lActiveRequests, eax
Чтобы понять суть проблемы синхронизации, давайте сначала посмотрим, что может произойти в однопроцессорной системе. Допустим, два потока пытаются почти одновременно пройти через DispatchPnp. Понятно, что полной одновременности быть не может, потому что оба потока поочередно выполняются на саном процессоре. Но представьте, что один из потоков приближается к концу функции и успел загрузить текущее содержимое lActiveRequests в регистр ЕАХ как раз перед тем, как он был вытеснен другим потоком. Допустим, переменная LActiveRequests в этот момент равна 2. В процессе переключения потоков операционная система сохраняет регистр ЕАХ (со значением 2) как часть образа контекста исходящего потока где-то в основной памяти.
1МЕЧАНИЕ-------------------------------------------------------------------------
Эта проблема не ограничивается вытеснением потоков, происходящим в результате истечения выделенных квантов. Потоки также могут уступать управление вследствие страничных сбоев, изменений -зиоритета, обусловленных действиями внешних агентов, и т. д. Таким образом, «вытеснение» сто-гт рассматривать как всеобъемлющий термин, который включает любые способы передачи управления другому потоку без специально предоставленного разрешения со стороны текущего потока.
Теперь представьте, что другой поток только преодолел операцию увеличения з начале DispatchPnp. Он увеличивает lActiveRequests с 2 до 3 (потому что первый поток еще не успел обновить переменную). Если первый поток вытеснит второй, операционная система восстановит контекст первого потока, который включает значение 2 в регистре ЕАХ. Теперь первый поток продолжает работу, уменьшает ЕАХ на 1 и сохраняет результат в lActiveRequests. На этой стадии lActiveRequests содержит значение 1, что неверно. В будущем это может привести к преждевременному удалению объекта устройства, потому что мы, фактически, потеряли один запрос ввода/вывода.
На компьютерах х86 эта конкретная проблема решается легко — достаточно заменить последовательности команд «загрузка/увеличение/сохранение» и «затру зка/уменыпение/сохранение» атомарными командами:
; ++1ActlveRequests;
Inc lActiveRequests
; --lActiveRequests;
dec lActiveRequests
На платформе Intel x86 команды INC и DEC прерываться не могут, поэтому ситуация с вытеснением потока в середине обновления счетчика никогда не
166
Глава 4. Синхронизация
возникнет. Впрочем, в многопроцессорной среде и этот код небезопасен, потому что команды INC и DEC реализованы в виде нескольких шагов микрокода. Теоретически два разных процессора могут выполнять свой микрокод с минимальным расхождением, в результате чего один из них обновит устаревшее значение. Многопроцессорная проблема в архитектуре х86 решается при помощи префикса LOCK:
: ++1 Actl veRequests;
lock inc lActiveRequests
; --lActiveRequests;
lock dec lActiveRequests
Префикс команд LOCK блокирует все остальные процессоры на время выполнения микрокода текущей команды, что гарантирует целостность данных.
К сожалению, не все проблемы синхронизации имеют столь простые решения. Я привел этот пример не для того, чтобы продемонстрировать решение одной простой проблемы на одной из платформ, на которых работает Windows ХР, а для демонстрации двух источников потенциальных проблем: вытеснения одного потока другим во время незавершенного изменения состояния и одновременного выполнения конфликтующих операций изменения состояния. Проблемы можно решить посредством использования примитивов синхронизации (скажем, мьютексов) для блокировки других потоков на то время, пока наш поток работает с общими данными. В тех случаях, когда блокировка потоков недопустима, можно предотвратить вытеснение с помощью схемы приоритетов IRQL, а одновременное выполнение — с помощью хорошо продуманных спин-блокировок.
Уровень запроса прерываний (IRQL)
В Windows ХР всем аппаратным прерываниям, а также некоторым программным прерываниям присваивается уровень запроса прерывания (IRQL). Каждый процессор обладает собственным уровнем IRQL. Обычно уровни IRQL обозначаются именами вида PASSIVE-LEVEL, APCJ-EVEL и т. д. На рис. 4.1 показан диапазон значений IRQL для платформы х86 (в общем случае числовые значения IRQL зависят от платформы). Большую часть времени компьютер работает в пользовательском режиме на уровне PASSIVE-LEVEL. На этом уровне применимо все, что вам известно о работе многозадачных операционных систем. Другими словами, планировщик может вытеснить поток по истечении выделенного кванта или из-за того, что управление было передано потоку с более высоким приоритетом. Потоки также могут намеренно блокироваться, ожидая наступления каких-либо событий.
При возникновении прерывания ядро поднимает уровень IRQL процессора, от которого оно поступило, до уровня, связанного с этихМ прерыванием. Операции по обработке прерывания могут быть... ммм... прерваны для обработки прерывания с более высоким уровнем IRQL, но никогда — для обработки прерывания с тем же или меньшим уровнем IRQL. К сожалению, мне приходится использо
вровень запроса прерываний (IRQL)
167
вать слово прерывание в двух разных смыслах. Я долго пытался найти слово для обозначения временной приостановки операций, которое бы не вызывало пута-ины с приостановкой вытесняемых потоков, но ничего лучше не нашел.
Сказанное достаточно важно, чтобы выделить его в отдельное правило:
Операции, выполняемые процессором, могут быть прерваны только операциями, выполняемыми на более высоком уровне IRQL.
Это правило следует воспринимать так, как это делает компьютер. По истечении выделенного кванта активизируется планировщик потоков на уровне OISPATCH_LEVEL Планировщик может сделать текущим другой поток. Когда IRQL возвращается к уровню PASSIVE-LEVEL, на процессоре выполняется другой поток, v. все же утверждение остается истинным: первая операция на уровне PASSIVELEVEL не была прервана второй операций уровня PASSIVE-LEVEL. Возможно, более полезная формулировка этого правила выглядит так:
Операции, выполняемые процессором, могут быть прерваны только операциями, выполняемыми на более высоком уровне IRQL. Операции уровня DISPATH LEVEL и выше не могут приостанавливаться для выполнения других операций на текущем уровне IRQL или ниже.
Поскольку каждый процессор обладает собственным уровнем IRQL, на многопроцессорном компьютере любой отдельный процессор может работать на хэовне IRQL, меньшем либо равном уровню IRQL любого другого процессора.
168
Глава 4. Синхронизация
В следующем разделе я расскажу о спин-блокировках, сочетающих внутрипро-цессорные синхронизационные возможности IRQL с многопроцессорным механизмом блокировки. Но пока речь идет лишь о том, что происходит на одном процессоре.
Повторю только что сказанное: программы пользовательского режима выполняются на уровне PASSIVE_LEVEL. Когда программа пользовательского режима вызывает функцию системного API, процессор переключается в режим ядра, но продолжает работать на уровне PASSIVE-LEVEL в том же контексте потока. Во многих случаях вызовы системных функций API передают управление на точку входа драйвера без повышения IRQL. Диспетчерские функции драйверов для большинства типов пакетов IRP также выполняются на уровне PASSIVE-LEVEL. Кроме того, некоторые функции драйверов — такие как DriverEntry и AddDevice — выполняются на уровне PASSIVE-LEVEL в контексте системного потока. Во всех перечисленных случаях код драйвера может вытесняться, как обычные приложения пользовательского режима.
Некоторые стандартные функции драйверов выполняются на уровне DISPATCHLEVEL, более высоком, чем PASSIVE-LEVEL. К их числу относятся функция Startlo, функции DPC (отложенные вызовы процедур, Deferred Procedure Call) и многие другие. У всех этих функций имеется общая особенность: необходимость обращения к полям объекта устройства и расширения устройства без вмешательства со стороны диспетчерских функций драйвера и друг друга. Во время выполнения одной из таких функций представленное выше правило гарантирует, что ни один поток не сможет вытеснить ее код на том же процессоре для выполнения диспетчерской функции драйвера, потому что диспетчерская функция работает на более низком уровне IRQL. Более того, ни один поток не может вытеснить ее для выполнения другой специальной функции, потому что другая функция работает на том же уровне IRQL.
ПРИМЕЧАНИЕ------------------------------------------------------------------—
К сожалению, термин «диспетчерские функции» (dispatch routines) напоминает DISPATCHJ-EVEL, но это лишь случайное сходство. Диспетчерские функции называются так потому, что I/O Manager перенаправляет им запросы ввода/вывода. Название DISPATCH_LEVEL объясняется тем, что на этом уровне IRQL изначально работал диспетчер потоков, решая, какой поток должен выполняться следующим. (Если вам интересно, сейчас диспетчер потоков работает на уровне SYNCHLEVEL. А если вам действительно интересно, на однопроцессорном компьютере это то же самое, что и DISPATCH_LEVEL.)
Между DISPATCH_LEVEL и PROFILE-LEVEL располагаются уровни различных аппаратных прерываний. В общем случае любое устройство, генерирующее прерывания, обладает некоторым уровнем IRQL, определяющим приоритет его прерываний по отношению к другим устройствам. Драйвер WDM определяет уровень IRQL своего прерывания при получении запроса IRP-MJ-PNP при помощи дополнительного кода функции IRP_MN_START_DEVICE. Уровень прерывания устройства входит в число многочисленных конфигурационных параметров, передаваемых в параметрах этого запроса. Этот уровень часто называют уровнем IRQL устройства, или, сокращенно, DIRQL (Device IRQL). DIRQL не является одноразовым
ib запроса прерываний (IRQL)
169
1нем запроса — это IRQL прерывания, назначенного рассматриваемому в дан-момент устройству.
Смысл других уровней IRQL иногда зависит от конкретной архитектуры просора. Поскольку эти уровни используются во внутренней работе ядра, они не ?ют особого отношения к работе разработчика драйвера. Например, уровень LLEVEL должен дать системе возможность планирования асинхронных вызовов седлр (АРС), которые будут подробно описаны в этой главе. К числу операций, юлняемых на уровне HIGH-LEVEL, относится снятие образа памяти перед пе-одом компьютера в спящий режим, действия при возникновении фатальных ев. обработка ложных прерываний и т. д. Я даже не пытаюсь приводить пол-л список, потому что, как я уже сказал, вам не нужно знать все подробности. Подведем итог. Драйверы обычно имеют дело с тремя уровнями запросов рываний:
PASSIVE_LEVEL — уровень выполнения многих диспетчерских функций и нескольких специальных функций;
DISPATCH-LEVEL — уровень выполнения Startlo и функций DPC;
DIRQL — уровень выполнения обработчиков прерываний.
L в действии
х5ы вы лучше поняли важность IRQL, обратимся к рис. 4.2, на котором изобра-43 возможная последовательность событий на отдельном процессоре. В начале
>м последовательности процессор работает на уровне PASSIVE_LEVEL В момент ступает прерывание, обработчик которого выполняется на уровне IRQL-1, од-
из уровней между DISPATCH_LEVEL и PROFILE-LEVEL.
Затем в момент t2 по-
знает другое прерывание, функция которого выполняется на уровне IRQL-2, ньшем IRQL-L
Рис. 4.2. Приоритеты прерываний в действии
170
Глава 4. Синхронизация
Согласно представленному ранее правилу, процессор продолжает обслуживать первое прерывание. Когда в момент времени t3 обработчик первого прерывания завершается, он может запросить DPC. Функции DPC выполняются на уровне DISPATCH_LEVEL. Незавершенной операцией с наивысшим приоритетом оказывается обработчик второго прерывания, который и выполняется следующим. За время его завершения к моменту никаких промежуточных событий не произошло, поэтому выполняется вызов DPC на уровне DISPATCH- LEVEL К моменту завершения функции DPC, то есть в точке t5, IRQL может вернуться к уровню PASSIVE-LEVEL.
IRQL и приоритеты потоков
Не путайте приоритеты потоков с IRQL — это совершенно иная концепция. Приоритет потоков управляет решениями планировщика относительно того, когда нужно вытеснить исполняемые потоки и какие потоки должны работать следующими. Единственным «приоритетом», который имеет хоть какое-то значение на уровнях IRQL выше APC_LEVEL, является сам уровень IRQL, однако он управляет тем, какой код может выполняться, а не тем, в каком контексте потока происходит его выполнение.
IRQL и перемещение памяти
Одно из последствий выполнения на повышенных уровнях IRQL заключается в том, что система теряет возможность обслуживания страничных сбоев. Правило, следующее из этого факта, выглядит предельно просто:
Выполнение кода на уровне DISPATCHLEVEL и выше не должно приводить к страничным сбоям.
В частности, из этого правила следует, что любая функция драйвера, выполняемая на уровне DISPATCH_LEVEL и выше, должна располагаться в неперемс-щаемой памяти. Более того, все данные, к которым вы обращаетесь из такой функции, также должны располагаться в неперсмещаемой памяти. Наконец, с повышением IRQL в вашем распоряжении остается все меньше вспомогательных функций режима ядра.
В документации DDK четко указаны ограничения, которые IRQL накладывает па использование вспомогательных функций. Например, в описании KeWait-ForSingleObject указаны два ограничения:
О вызывающая сторона должна работать на уровне DISPATCH_LEVEL и ниже;
О если при вызове указан ненулевой период тайм-аута, вызывающая сторона должна работать на уровне строго ниже DISPATCH-LEVEL.
Если читать между строк, здесь сказано следующее: если вызов KeWaitForSingle-Object теоретически может быть заблокирован на произвольный промежуток времени (то есть при указании ненулевого тайм-аута), выполнение должно вестись на уровне ниже DISPATCH-LEVEL, где разрешена блокировка потоков. Но если нужно всего лишь проверить, поступил ли сигнал о некотором событии, для этого можно находиться и на уровне DISPATCH_LEVEL. Вызов этой функции из
Уровень запроса прерываний (IRQL)
171
обработчиков прерываний или других функций, работающих на уровне выше DISPATCH- LEVEL, невозможен.
Для полноты картины стоит указать, что правило запрета страничных сбоев в действительности запрещает любые аппаратные исключения, в том числе страничные сбои, проверки деления, проверки границ и т. д. Программные исключения (вроде нарушений квот) разрешены. Таким образом, на уровне DISPATCH_LEVEL можно вызвать функцию ExAllocatePoolWithQuota для выделения неперемещаемой памяти.
Ьсвенное управление IRQL
Как правило, система вызывает функции вашего драйвера на правильном уров-IRQL для тех операций, которые вы собираетесь выполнять. Хотя многие из этих функций еще не обсуждались во всех подробностях, я приведу пример. Первая «встреча» драйвера с новым запросом ввода/вывода происходит в тот v мент, когда I/O Manager вызывает одну из диспетчерских функций для обра-6 тки IRP. Обычно вызов производится на уровне PASSIVE-LEVEL, потому что 9 ходе обработки может возникнуть необходимость в блокировке вызывающего z- тока и вызове любых вспомогательных функций. Конечно, блокировка потока более высоком уровне IRQL невозможна, а на уровне PASSIVE-LEVEL действу-1” минимальные ограничения на вызов вспомогательных функций.
АМЕЧАНИЕ --------------------------------------------------------------
диспетчерские функции драйвера обычно выполняются на уровне PASSIVE_LEVEL, но не всегда. Вь *акже можете указать, что запросы IRPJ4J_POWER должны приниматься на уровне DISPATCH-LEVEL, установкой флага DCLPOWERJNRUSH или сбросом флага DO_POWER_PAGABLE в объекте устюйства. Иногда архитектура драйвера требует, чтобы другие драйверы могли посылать отдельные ~акеты IRP на уровне DISPATCH_LEVEL. Скажем, драйвер шины USB принимает запросы на пере-съе’ку данных на уровне DISPATCHJ-EVEL и ниже. Стандартный драйвер последовательного порта ,тэс<е принимает любые операции чтения, записи и управления на уровне DISPATCHJ-EVEL и ниже.
Если диспетчерская функция ставит IRP в очередь вызовом loStartPacket, следующая «встреча» с запросом состоится в тот момент, когда I/O Manager вызовет Startlo. Вызов происходит на уровне DISPATCH_LEVEL, потому что система д» лжна работать с очередью запросов ввода/вывода без вмешательства со стороны других функций, добавляющих и удаляющих IRP из очереди. Как будет сказано позднее в этой главе, обращения к очереди защищаются спин-блоки-Р ьками, что приводит к выполнению на уровне DISPATCHJ-EVEL.
В дальнейшем устройство может сгенерировать прерывание, и тогда обработчик прерывания будет вызван на уровне DIRQL. Весьма вероятно, что для которых регистров устройства безопасный совместный доступ невозможен. Если обращаться к этим регистрам только на уровне DIRQL, вы можете быть сверены в том, что на однопроцессорном компьютере никто не помешает работе о.'работника прерывания. Если возникнет необходимость в обращении к этим кр.ггическим регистрам устройства из других частей драйвера, гарантируйте, что эти другие части выполняются только на уровне DIRQL. Сервисная функция
172
Глава 4. Синхронизация
KeSynchronizeExecution поможет обеспечить выполнение этого правила (мы рассмотрим эту функцию в главе 7, когда речь пойдет об обработке прерываний).
На еще более позднее время может быть запланирован вызов функции DPC Эти функции выполняются на уровне DISPATCH_LEVEL, потому что среди прочего они должны обратиться к очереди IRP, чтобы удалить следующий запрос и передать его функции Startlo. Извлечение следующего запроса из очереди осуществляется функцией loStartNextPacket, которая должна вызываться на уровне DISPATCH_LEVEL Перед тем как вернуть управление, она может вызвать вашу функцию Startlo. Обратите внимание на то, как четко эта последовательность укладывается в схему требований IRQL: обращение к очереди, вызов loStartNextPacket и возможный вызов Startlo — все это должно происходить на уровне DISPATCH- LEVEL, и на этом же уровне система вызывает функцию DPC.
Хотя уровнем IRQL можно управлять и напрямую (о том, как это делается, рассказано в следующем разделе), такая необходимость возникает крайне редко: как правило, уровни, на которых система вызывает код, хорошо соответствуют потребностям программиста. Соответственно, вам не нужно постоянно следить за тем, на каком уровне IRQL ведется выполнение в каждый конкретный момент — этот уровень почти наверняка будет правильно выбран для работы, выполняемой в данный момент.
Прямое управление IRQL
При необходимости можно повысить и понизить уровень IRQL для текущего процессора при помощи функций KeRaiselrql и KeLowerlrql. Для примера рассмотрим часть функции, выполняемой на уровне PASSIVE_LEVEL:
KIRQL oldirql;	//	1
ASSERT(KeGetCurrentlrqK) <= DISPATCH_LEVEL):	//	2
KeRaiselrql(DISPATCH_LEVEL, &old1rql);	//	3
KeLowerlrql(oldirql);	//	4
1.	KIRQL является определением типа для целого числа, содержащего значение IRQL. Нам потребуется переменная для хранения текущего IRQL, поэтому мы определяем ее подобным образом.
2.	Директива ASSERT представляет необходимое условие для вызова KeRaiselrql: новый уровень IRQL должен быть больше текущего уровня или равен ему. Если условие не выполняется, KeRaiselrql инициирует фатальный сбой (с выдачей синего «экрана смерти»).
3.	Функция KeRaiselrql повышает текущий уровень IRQL до уровня, заданного первым аргументом. Она также сохраняет текущее значение IRQL в области памяти, на которую ссылается второй аргумент. В данном примере мы поднимаем IRQL до уровня DISPATCH_LEVEL и сохраняем текущий уровень в oldirql.
4.	После выполнения того кода, который требовалось выполнить на повышенном уровне IRQL, мы понижаем его до предыдущего уровня. Для этого функции KeLowerlrql передается значение oldirql, полученное ранее при вызове KeRaiselrql.
 чировки
173
ле повышения уровня IRQL необходимо со временем восстановить ис-Ьи значение его уровня. В противном случае различные допущения в коде, Ьвв емом вами позднее, или в коде, который позднее вызывает ваш код, могут ься неверными. В документации DDK сказано, что функция KeLowerlrql Bfer 1 должна вызываться со значением, которое было получено при непосред-Ьг о предшествующем вызове KeRaiselrql, но эта информация не совсем вер-fee. действительности KeLowerlrql проверяет только одно правило: что новый Ьг>? нь IRQL должен быть меньше текущего либо равен ему. При желании • понизить уровень IRQL за несколько этапов.
L снижение IRQL ниже уровня, действовавшего на момент вызова драйвера > мной функций, является ошибкой (и притом серьезной!), даже если перед bi ратом уровню будет возвращено прежнее значение. В результате разрыва шизации какая-нибудь операция может вытеснить ваш код и изменить Iг ст данных, который, по предположению стороны, вызвавшей ваш код, дол-
Э таваться неизменным.
йин-блокировки
t з у прощения синхронизации доступа к общим объектам в симметричном • процессорном мире Windows ХР ядро позволяет определить любое число ектов, называемых спин-блокировками. При получении спин-блокировки код D I дном из процессоров выполняет атомарную операцию, которая проверяет, а I ем задает значение находящейся в памяти переменной так, что ни один й процессор не может обратиться к этой переменной до завершения Опера-O' Если проверка показывает, что изменяемая переменная свободна, програм-в родолжает работу. Если же проверка показывает, что для изменяемой пере-Шг .ой ранее была установлена блокировка, программа переходит в активное вглание, то есть повторяет операции проверки и изменения в цикле опроса । " IH»). В конечном счете владелец снимает блокировку, сбрасывая перемен-• а одна из операций проверки/установки на ожидающих процессорах сооб-От об освобождении блокировки.
г исунок 4.3 поясняет концепцию спин-блокировки. Допустим, имеется некий ; рс», который может использоваться одновременно на двух разных процес-.? х. Для конкретности предположим, что этим ресурсом является ячейка LST_ENTRY — якорь для связанного списка IRP. К списку могут обращаться Л етчерские функции, функции отмены, функции DPC и т. д. Теоретически,  у несколько функций, одновременно работающих на разных процессорах, ж* угг попытаться модифицировать якорь списка. Чтобы предотвратить хаос,  связываем с этим «ресурсом» спин-блокировку.
геперь предположим, что код, выполняемый на процессоре А, обращается < шему ресурсу в момент Он устанавливает спин-блокировку и начинает тать с ресурсом. Через некоторое время, в момент t2l код, выполняемый на г» ессоре В, также хочет получить спин-блокировку. Поскольку спин-блоки-। гка в настоящий момент принадлежит процессору А, процессор В входит в ак-т- ъш цикл, постоянно проверяя и перепроверяя состояние ресурса в ожидании
174
Глава 4, Синхронизация
его освобождения. Когда процессор А снимает блокировку в момент £3, процессор В обнаруживает, что ресурс освободился, и захватывает его. Отныне процессор В обладает неограниченным доступом к ресурсу. Наконец, в момент Г4 процессор В завершает доступ и освобождает блокировку.
Спин-блокировки
Процессор А
i Снятие спин-блокировки
Попытка
установления спин-блокировки
Попытка установления:: спин-блокировки ;
Общий ресурс
Спин-блокировка
Процессор В:
4
Снятие
f1 f2 f3
спин-блокировки
Рис. 4.3. Применение спин-блокировки для защиты общего ресурса
Необходимо очень четко представлять связь между спин-блокировкой и общим ресурсом. Мы устанавливаем эту связь при проектировании драйвера. Мы решаем, что доступ к ресурсу будет производиться только при успешно установленной спин-блокировке. Операционная система ничего не знает о нашем решении. Более того, мы можем определить любое количество спин-блокировок для защиты любого числа общих ресурсов.
Несколько фактов о спин-блокировках
Я должен подчеркнуть ряд важных свойств спин-блокировок. Прежде всего, если процессор уже установил спин-блокировку и пытается установить ее повторно, возникает состояние взаимной блокировки (deadlock). Со спин-блокировками не связываются никакие счетчики или идентификаторы владельцев: либо кто-то установил блокировку, либо нет. Если попытаться установить блокировку в то время, когда она уже кем-то установлена, придется ждать снятия блокировки ее текущим владельцем. Если блокировка уже установлена вашим процессором, код освобождения ресурса никогда не выполнится, потому что процессор будет в активном цикле проверять заблокированную переменную.
Установление спин-блокировки приводит к автоматическому повышению IRQL до уровня DISPATCH_LEVEL. Следовательно, код, установивший спин-блокировку, должен находиться в неперемещаемой памяти и не может приостанавливать поток во время своего выполнения (в Windows ХР и выше существует исключение из этого правила: функция KeAcquirelnterruptSpinLock повышает IRQL до
О ин-блокировки
175
уровня DIRQL для прерывания и захватывает спин-блокировку, ассоциированную с прерыванием).
4^дниЕ---------------------------------------------------------------------------
-~обы избежать взаимной блокировки, возникающей при попытке процессора повторно устано-спин-блокировку для уже захваченного им объекта, достаточно соблюдать простое правило:
.садитесь в том, что функция, устанавливающая блокировку, снимает ее и не пытается устанавли-эсть заново, и не вызывайте другие функции при установленной спин-блокировке. В операцион-ой системе нет никакого «сторожа», который бы предотвратил вызов других функций, — это все--о лишь правило проектирования, которое поможет вам избежать непреднамеренных ошибок. Оно "смогает вам (или другому программисту, занимающемуся сопровождением вашего кода) защититься от конкретной опасности: если вы забудете, что спин-блокировка уже была установлена 53 нее, и попытаетесь установить ее повторно. Я расскажу об одном уродливом исключении из это-"3 полезного правила в главе 5, при описании функций отмены IRP.
Как очевидное следствие указанного факта запросы на установление спин-бло-клровок могут осуществляться только при выполнении на уровне DI S PATCH J.. EVE L и ниже. В своей внутренней работе ядро может устанавливать спин-блокировки на уровнях IRQL выше DISPATCH_LEVEL, но для нас с вами такая возможность недоступна.
Другая особенность спин-блокировок состоит в том, что во время ожидания Ъокировки процессор почти не выполняет полезной работы. Активное ожидание происходит на уровне DISPATCH_LEVEL с разрешенными прерываниями, так что процессор, ожидающий снятия спин-блокировки, может обслуживать аппаратные прерывания. Но чтобы избежать снижения производительности системы, следует свести к минимуму объем работы с удержанием спин-блокировки для ресурсов, которые могут понадобиться какому-нибудь другому процессору.
Кстати говоря, два процессора могут одновременно удерживать две разные спин-блокировки. Это вполне логично, ведь спин-блокировка связывается с некоторым общим ресурсом или набором общих ресурсов. Нет смысла откладывать вычисления, которые относятся к разным ресурсам, защищенным разными г пин - блокировками.
Существуют разные версии ядра для однопроцессорных и многопроцессорных ллстем. Программа установки Windows ХР решает, какое ядро следует использовать, после анализа оборудования. В многопроцессорных ядрах спин-блоки-ровки реализованы именно так, как я описал. Однако в однопроцессорном ядре другого процессора заведомо быть не может, поэтому спин-блокировки в нем реализованы чуть проще. В однопроцессорной системе установление спин-блокировки повышает IRQL до уровня DISPATCH_LEVEL, и ничего более. Вы видите, как в этом случае обеспечивается синхронизация при установке «псевдоблоки-тювки»? Чтобы некоторая часть кода могла попытаться установить ту же спин-блокировку (а на самом деле и любую другую спин-блокировку, но это сейчас неважно), она должна выполняться на уровне DISPATCH_LEVEL и ниже — блоки-:ювку можно запрашивать только на этих уровнях. Но мы уже знаем, что это невозможно, потому что операции на уровнях IRQL выше PASSIVE_LEVEL не могут прерываться другими операциями, работающими на том же или более низком IRQL. Что и требовалось доказать, как говорил мой учитель геометрии.
176
Глава 4. Синхронизация
Работа со спин-блокировками
Чтобы использовать спин-блокировку напрямую, выделите память для объекта KSPIN_LOCK в неперемещаемой памяти. Затем вызовите функцию KelnitializeSpinLock для инициализации объекта. Позднее, во время выполнения на уровне DISPATCH-LEVEL и ниже, установите блокировку, выполните операцию, которая должна быть защищена от внешнего вмешательства, и снимите блокировку. Допустим, расширение устройства содержит спин-блокировку с именем QLock, предназначенную для защиты доступа к созданной вами специальной очереди IRP. Блокировка инициализируется в функции AddDevice:
typedef struct _DEVICE_EXTENSION {
KSPIN_LOCK QLock;
} DEVICE-EXTENSION, *PDEVICE_EXTENSION:
NTSTATUS AddDevice(...)
{
PDEVICE-EXTENSION pdx = ...;
KeIni11 a11zeSpInLock(&pdx->QLock);
}
Где-то в другом месте драйвера (скажем, в диспетчерской функции для какого-то типа IRP) вы можете установить (и быстро освободить) блокировку для защиты неких манипуляций с очередью. Помните, что эта функция должна находиться в неперемещаемой памяти, потому что она в течение некоторого времени выполняется на повышенном уровне IRQL:
NTSTATUS DIspatchSomethlng(...)
{
KIRQL oldirql;
PDEVICE_EXTENSION pdx = ...;
KeAcquireSpInLock(&pdx->QLock, Soldirql);	//1
KeReleaseSpinLock(&pdx->QLock, oldirql);	// 2
}
1. При установлении блокировки функция KeAcquireSpinLock также повышает IRQL до уровня DISPATCH_LEVEL и возвращает текущий (то есть предшествующий) уровень в переменной, на которую указывает второй аргумент.
2. Когда функция KeReleaseSpinLock освобождает спин-блокировку, она также понижает IRQL до уровня, заданного вторым аргументом.
Если заранее известно, что выполнение уже ведется на уровне DISPATCH_LEVEL, можно сэкономить немного времени за счет вызова двух специальных функций.
Слин-блокировки
177
Например, такая методика уместна для DPC, Startlo и других функций драйверов, выполняемых на уровне DISPATCH JLEVEL:
KeAcqu 1 reSpI nLockAtDpcLevel (&pdx->QLock);
KeReleaseSpInLockFromDpcLevel(&pdx->QLock);
Спин-блокировки с очередями
В Windows ХР появился новый тип спин-блокировок — так называемые вну три-стековые спин-блокировки с очередями, по эффективности реализации превосходящие обычные спин-блокировки. Механика использования этого типа блокировок несколько отличается от предыдущего описания. Как и в предыдущем случае. объект KSPIN_LOCK создается в неперемещаемой памяти, доступной для всех нужных частей драйвера, и инициализируется вызовом KeAcquireSpinLock. Одна-, ко для установления и снятия блокировки используется код следующего вида:
<	LOCK_QUEUE_HANDLE qh:	//1
<	eAcquirelnStackQueuedSpinLock(&pdx->QLock.	&qh);	// 2
<	eReleaseInStackQueuedSpinLock(&qh);	// 3
1.	Структура KLOCK_QUEUE_HANDLE является закрытой — вам не положено знать, I что в ней хранится, но вы должны выделить для нее память. Удобнее всего I для этого определить автоматическую переменную (отсюда и термин «внут-I ристековый» в названии).
| 2- Чтобы установить блокировку, вызовите KeAcquirelnStackQueuedSpinLock вместо I KeAcquireSpinLock и передайте во втором аргументе адрес KLOCK_QUEUE_HANDLE. | 3. Чтобы снять блокировку, вызовите KeReleaselnStackQueuedSpinLock вместо Ке-
ReleaseSpinLock.
Превосходство внутристековых спин-блокировок с очередями обусловлено неэффективностью стандартных спин-блокировок. При стандартных спин-блокиров-ках каждый процессор, претендующий на захват ресурса, постоянно модифицирует содержимое одной и той же ячейки памяти. Каждая модификация требует, чтобы все конкурирующие процессоры перезагружали одну и ту же строку кэша. Спин-блокировка с очередями, поддержка которой для внутреннего использования появилась в Windows 2000, избегает этого нежелательного эффекта за счет умного использования атомарных операций для отслеживания пользователей и сторон, ожидающих блокировки. Ожидающий процессор выполняет непрерывное чтение (но не запись) из уникальной ячейки памяти. Процессор, снимающий блокировку, изменяет переменную, которую активно ожидает другая сторона.
Внутренние спин-блокировки с очередями не могут напрямую использоваться в коде драйвера, потому что их работа зависит от таблицы указателей фиксированного размера, недоступной для драйверов. В Windows ХР появились внутри-стековые спин-блокировки с очередями, работа которых зависит от автоматических переменных, а не от таблицы фиксированного размера.
178
Глава 4. Синхронизация
Если вы знаете, что выполнение уже ведется на уровне DISPATCHJ-EVEL, то кроме представленных ранее двух функций установления/снятия блокировок нового типа также можно использовать две другие функции: KeAcquirelnStack-QueuedSpinLockAtDpcLeve! и KeReleaselnStackQueuedSpinLockFromDpcLevel (попробуй-те-ка трижды повторить вслух!)
ПРИМЕЧАНИЕ ---------------------------------—-------------------------------------
Поскольку предшественники Windows ХР не поддерживали внутрисгековые спин-блокировки с очередями и функции спин-блокировки прерываний, эти функции нельзя напрямую вызывать в драйверах, рассчитанных на двоичную совместимость между разными версиями Windows. Пример SPINLOCK показывает, как на стадии выполнения выбрать между новыми спин-блокировками в ХР и старыми спин-блокировками в остальных случаях.
Синхронизационные объекты ядра
Ядро поддерживает пять разновидностей объектов синхронизации, которые могут использоваться для управления работой программных потоков. Краткий перечень объектов синхронизации и их возможных применений представлен в табл. 4.1. В любой момент времени такие объекты находятся в одном из двух состояний: установленном или сброшенном. Если для потока, в контексте которого ведется выполнение, разрешена блокировка, программа может дождаться перехода одного или нескольких объектов в установленное состояние, вызывая функцию KeWaitForSingleObject или KeWaitForMultipleObjects. Ядро также предоставляет функции инициализации и управления состоянием для всех объектов синхронизации.
Таблица 4.1. Объекты синхронизации ядра
Объект	Тип данных	Описание
Событие	KEVENT	Блокирует поток до того момента, когда другой поток обнаружит наступление данного события
Семафор	KSEMAPHORE	Используется вместо событий при произвольном количестве вызовов ожидания
Мьютекс	KMUTEX	Запрещает другим потокам выполнение определенной секции кода
Таймер	KTIMER	Откладывает выполнение потока на заданный промежуток времени
Поток	KTHREAD	Блокирует один поток до завершения другого потока
В нескольких ближайших разделах я расскажу, как использовать синхронизационные объекты ядра. Сначала мы разберемся, когда можно блокировать поток вызовом одного из примитивов синхронизации, а затем обсудим вспомогательные функции, используемые для каждого типа объектов. Раздел завершается обсуждением логически связанных тем — потоковых сигналов и асинхронного вызова процедур.
Синхронизационные объекты ядра
179
[ак и когда блокировать
Чтобы понять, когда и как драйвер WDM может блокировать потоки по синхронизационным объектам ядра, необходимо припомнить некоторые базовые сведения о потоках, изложенные в главе 2. В общем случае поток, выполнявшийся на момент возникновения программного или аппаратного прерывания, остается текущим потоком на время обработки прерывания ядром. Мы говорим о выполнении кода режима ядра в контексте текущего потока. Конечно, в ответ на прерывания разных типов планировщик может принять решение о переключении потока; в этом случае «текущим» становится другой поток.
Термины контекст произвольного потока и контекст фиксированного (поп-arbitrary) потока определяют точность информации о потоке, в контексте которого происходит выполнение функции драйвера. Если нам точно известно, что выполнение ведется в контексте потока, инициировавшего запрос ввода/вывода, контекст является фиксированным («не произвольным»). Однако в большинстве случаев драйвер WDM не может быть уверенным в этом факте, потому что чистая случайность определяет, какой поток был активен на момент возникновения прерывания, приведшего к обращению к драйверу. Когда приложение выдает запрос ввода/вывода, это приводит к переключению из пользовательского режима в режим ядра. Функции I/O Manager, которые создают пакеты IRP и отправляют их диспетчерским функциям драйвера, продолжают работать в контексте фиксированного потока — как и первая диспетчерская функция, которая < увидит» IRP. Драйвером верхнего уровня мы будем называть драйвер, диспетчерская функция которого первой получает IRP.
В общем случае только драйвер верхнего уровня может быть уверен в том, что он работает в контексте фиксированного потока. Представьте, что вы — диспетчерская функция драйвера нижнего уровня и вас интересует, были ли вы вызваны в контексте произвольного потока. Если драйвер верхнего уровня просто отправил пакет IRP прямо из своей диспетчерской функции, то выполнение будет находиться в контексте исходного, то есть фиксированного, потока. Но предположим, что драйвер поместил IRP в очередь, а затем вернул управление приложению. В дальнейшем драйвер удалил пакет IRP из очереди в произвольном потоке и отправил его (или другой пакет IRP) вам. Если у вас нет твердой уверенности в том, что этого не произошло, следует предположить, что выполнение ведется в контексте произвольного потока (если только ваш драйвер не является драйвером верхнего уровня).
Несмотря на все сказанное, во многих ситуациях контекст потока точно известен. Функции DriverEntry и AddDevice вызываются в системном потоке, который при необходимости может блокироваться. Необходимость в явной блоки-оовке внутри этих функций возникает нечасто, но при желании это можно сделать. Запросы IRP_MJ_PNP также могут приниматься и в системном потоке. Во многих случаях для правильной обработки запроса поток необходимо заблокировать. Наконец, иногда запросы ввода/вывода поступают непосредственно от приложения, в этом случае вы точно знаете, что выполнение ведется в потоке. принадлежащем приложению.
180
Глава 4. Синхронизация
ПРИМЕЧАНИЕ-------------------------------------------------------------------
Компания Microsoft использует термин «драйвер верхнего уровня» в основном для разграничения драйверов файловой системы и драйверов запоминающих устройств, к которым они обращаются за выполнением фактического ввода/вывода. Драйвер файловой системы находится на «верхнем уровне», а драйвер запоминающего устройства — нет. Эту концепцию легко спутать с иерархией драйверов WDM, но это разные вещи. Я смотрю на происходящее так: все драйверы WDM для заданного устройства, включая все фильтрующие драйверы, функциональный драйвер и драйвер шины, либо одновременно находятся на «верхнем уровне», либо нет. Фильтрующему драйверу незачем ставить в очередь пакет IRP, который без вмешательства фильтра спокойно прошел бы вниз по стеку в контексте исходного потока. Таким образом, если контекст потока был фиксированным на тот момент, когда пакет IRP добрался до верхнего объекта FiDO (Filter Dispatch Object), он останется фиксированным и во всех диспетчерских функциях нижнего уровня.
Также вспомните, о чем говорилось в предыдущих обсуждениях этой главе: потоки нельзя блокировать при выполнении на уровне DISPATCH_LEVEL и выше.
Припомнив все эти факты о контекстах потоков и IRQL, можно сформулировать простое правило относительно возможности блокировки потоков:
Блокируется только поток, от которого поступил обрабатываемый запрос, и только при выполнении на уровне IRQL, строго меньшем DISPATCH LEVEL.
Некоторые объекты синхронизации, а также так называемые быстрые мьютексы, о которых речь пойдет далее в этой главе, обеспечивают функциональность «взаимоисключающего доступа». Иначе говоря, они предоставляют одному потоку монопольный доступ к общему ресурсу без вмешательства со стороны других потоков. В принципе, это очень напоминает то, что делают спин-блокировки, и у вас может возникнуть вопрос, как выбрать между разными методами синхронизации. Я считаю, что в общем случае следует отдать предпочтение синхронизации на уровнях ниже DISPATCH_LEVEL, потому что эта стратегия позволяет потоку-владельцу взаимоисключающей блокировки инициировать страничные сбои и вытесняться другими потоками, если поток продолжает удерживать блокировку в течение долгого времени. Кроме того, она дает возможность другим процессорам выполнять полезную работу, даже если на них имеются заблокированные потоки, ожидающие той же блокировки. Однако если какой-либо код, работающий с общим ресурсом, может выполняться на уровне DISPATCH_LEVEL, приходится использовать спин-блокировку, потому что код DISPATCHJJEVEL может прервать выполнение кода на более низком уровне IRQL.
Ожидание одного объекта синхронизации
Следующий пример демонстрирует вызов функции ожидания по одному объекту синхронизации:
ASSERT(KeGetCurrentlrqK) <= DISPATCH_LEVEL);
LARGEJNTEGER timeout:
NTSTATUS status = KeWaitForSingleObjectfobject, WaitReason, WaitMode, Aiertable, &timeout):
Как подсказывает директива ASSERT, для вызова этой сервисной функции выполнение должно вестись на уровне DISPATCH_LEVEL и ниже.
Синхронизационные объекты ядра
181
В этом вызове переменная object указывает на объект, по которому осуществляется ожидание. Хотя этот аргумент определяется с типом PVOID, он должен содержать указатель на один из объектов синхронизации, перечисленных в табл. 4.1. Объект должен находиться в неперемещаемой памяти — например, в структуре расширения устройства или в другой области данных, выделенной из непере-мещаемого пула. Для большинства целей стек выполнения может считаться не-перемещаемым.
WaitReason («причина ожидания») — сугубо рекомендательное значение, выбранное из перечисляемого типа KWAIT_REASON. Код ядра вообще не обращает внимания на этот аргумент, если только он не равен WrQueue (последнее значение влияет на некоторые решения, принимаемые во внутренней работе планировщика). Впрочем, причина блокировки потока сохраняется в закрытой структуре данных. Если бы мы располагали более полной информацией об этой структуре, возможно, код причины мог бы использоваться при отладке взаимных блокировок. Мораль: всегда передавайте в этом параметре Executive — нет никаких причин для использования других значений.
WaitMode — одно из двух значений перечисляемого типа MODE: KernelMode или UserMode. Аргумент Alertable представляет собой простую логическую величину. В отличие от WaitReason, эти параметры влияют на поведение системы, управляя возможностью раннего завершения ожидания для обеспечения асинхронных вызовов процедур (за подробностями обращайтесь к разделу «Сигналы потоков и АРС» позднее в этой главе). Ожидание в пользовательском режиме также позволяет подсистеме управления памятью выгружать стек режима ядра вашего потока. Примеры неоднократно встречаются в книге и везде, где драйверы, например, создают объекты событий в виде автоматических переменных. Если другой поток вызовет KeSetEvent на повышенном уровне IRQL в тот момент, когда объект события отсутствует в памяти, произойдет фатальный сбой. Мораль: всегда ожидайте в режиме KernelMode и передавайте FALSE в параметре Alertable.
Последний параметр KeWaitForSingleObject содержит адрес 64-разрядного интервала тайм-аута, выраженного в 100-наносекундных единицах. Положительное значение тайм-аута определяет абсолютный момент времени по отношению к 1 января 1601 года, «эпохе» системных часов. Вы можете определить текущее время вызовом KeQuerySystemTime и прибавить константу к полученному значению. Отрицательное значение определяет интервал по отношению к текущему времени. При задании абсолютного времени последующие изменения системных часов влияют на продолжительность тайм-аута. Другими словами, тайм-аут наступит лишь после того, как показания системных часов сравняются с заданным значением или превысят его. С другой стороны, относительный тайм-аут не зависит от изменений системных часов.
Нулевой тайм-аут заставляет функцию KeWaitForSingleObject немедленно вернуть управление с кодом состояния, указывающим, находится ли объект в установленном состоянии. При выполнении на уровне DISPATCH_LEVEL необходимо указывать нулевой тайм-аут, потому что блокировка потоков запрещена. Для каждого синхронизационного объекта ядра определена сервисная функция KeReadStateXxx, проверяющая состояние объекта. Впрочем, проверка состояния
182
Глава 4. Синхронизация
не совсем эквивалентна ожиданию с нулевым временем: если функция KeWait-ForSingleObject обнаруживает, что условие ожидания выполнено, это приводит к побочным эффектам, специфическим для конкретного объекта. С другой стороны, проверка состояния объекта не приводит к выполнению каких-либо действий, даже если объект уже установлен.
ПОЧЕМУ 1 ЯНВАРЯ 1601 ГОДА?---------------------------------------------------------
Много лет назад, когда я только начинал изучать Win32 API, меня удивило, почему дата 1 января 1601 года была выбрана в качестве точки отсчета для всех временных штампов в Windows NT. Я понял причину такого выбора, когда мне пришлось писать собственные функции преобразования. Всем известно, что годы, делящиеся на 4, являются високосными. Многие также знают, что годы начала века (скажем, 1900) не считаются високосными, хотя они и делятся на 4. Но мало кому известно, что каждый четвертый год начала века (такие как 1600 и 2000) составляют «исключение из исключения» — они также являются високосными. 1 января 1601 года начинает 400-летний цикл, завершающийся високосным годом. Отсчет времени от этой точки позволяет писать программы, преобразующие временные штампы Windows NT в стандартное представление даты (и наоборот) без лишних переходов.
В параметре тайм-аута допускается передача указателя NULL, on означает бесконечное ожидание.
Возвращаемое значение означает один из нескольких возможных результатов. Обычно предполагается, что вызов завершится с результатом STATUS_ SUCCESS — это признак успешного ожидания. Иначе говоря, объект находился в установленном состоянии на момент вызова KeWaitForSingleObject или же он находился в сброшенном состоянии и был установлен позже. При таком исходе ожидания иногда с объектом требуется выполнить некоторые операции, характер которых зависит от типа объекта. Мы рассмотрим их позднее в этой главе, при знакомстве с каждой разновидностью объектов синхронизации (например, для объектов событий успешное ожидание должно сопровождаться сбросом объекта).
Возвращаемое значение STATUS_TIMEOUT означает, что до перехода объекта в установленное состояние произошел тайм-аут. При нулевом тайм-ауте функция KeWaitForSingleObject немедленно возвращает управление либо с этим кодом (признак сброшенного объекта), либо с кодом STATUS_SUCCESS (признак установленного объекта). Если передать в качестве тайм-аута NULL, это возвращаемое значение невозможно, поскольку в этом случае запрашивается бесконечное ожидание.
Также возможны еще два возвращаемых значения. Коды STATUS_ALERTED и STATUSJJSER_APC указывают, что ожидание завершилось без установки объекта, потому что поток получил сигнал или вызов АРС пользовательского режима соответственно. Обе концепции будут рассмотрены немного позднее, в разделе «Сигналы потоков и АРС».
Учтите, что коды STATUS_TIMEOUT, STATUS_ALERTED и STATUS__USER_APC все проходят проверку NT_SUCCESS. Следовательно, не пытайтесь применять NT_ SUCCESS к возвращаемому коду KeWaitForSingleObject в ожидании, что он как-то различит случаи с установленным и сброшенным объектами.
> - ионизационные объекты ядра
183
ВМЕСТИМОСТЬ С WINDOWS 98/МЕ--------------------------------------------------------------
. ~dows 98 и Millennium функции KeWaitForSingleObject и KeWaitForMultipleObjects содержали Ь”чую ошибку: в двух ситуациях они возвращали недокументированное и бессмысленное значе-be OXFFFFFFFF. Одна из таких ситуаций возникала при завершении потока во время блокировки ьекту WDM. Ожидание прерывалось преждевременно с этим бессмысленным кодом. Такое -ение никогда не должно возвращаться (потому что оно не документировано), а ожидание не х" • но завершаться преждевременно, если только параметру Alertable не задано значение TRUE, тема решалась простым повтором ожидания.
рерэя ситуация с возвратом бессмысленного кода возникает в том случае, если поток, который вы =ъ-зетесь заблокировать, уже заблокирован. Вы спросите, как возможно выполнение в контексте > ' • а, если он заблокирован? Это могло происходить в Windows 98/Ме, если для блокировки ис-тмьзовался объект уровня VxD с флагом BLOCK_SVC_INTS, а система позднее вызывала функцию --ь.’Вера в так называемое время события (event time). Номинально выполнение происходило = •: - -ексте заблокированного потока, и вы попросту не могли устанавливать вторичную блокиров-• • по объекту WDM. Я даже видел, как в подобных обстоятельствах функция KeWaitForSingleObject ?:"=эащала управление с уровнем IRQL, повышенным до DISPATCH_LEVEL. Насколько мне извест-обходных решений у этой проблемы не существует. К счастью, она встречается только в драйве рах последовательных устройств, где код VxD соприкасается с кодом WDM.
Ожидание нескольких объектов синхронизации
ФуВиция KeWaitForMultipleObjects является логическим расширением KeWaitFor-
Е ngleObject. она используется в тех ситуациях, когда ожидание должно произво-I питься сразу по одному или нескольким объектам. Вызов функции выглядит так:
•’cSERT(KeGerCurrentIrql() <= DISPATCHJEVEL);
LARGEJNTEGER timeout:
‘iTSTATUS status *= KeWaitForMul tipi eObjects (count, objects,
WaitType, WaitReason. WaitMode, Alertable, Stimeout, waitblocks):
Здесь objects — адрес массива указателей на объекты синхронизации, a count — отчество указателей в массиве. Значение count должно быть меньше либо рав-о величине MAXIMUM_WAIT__OBJECTS, которая в настоящее время равна 64. Массив, вно как и все объекты, на которые ссылаются его элементы, должен находиться в неперемещаемой памяти. WaitType — одно из значений перечисляемого типа -itAII или WaitAny, оно указывает, нужно ли ожидать одновременного перехода ех объектов в установленное состояние либо же ожидание должно прерываться о установке хотя бы одного объекта.
Аргумент waitblocks указывает на массив структур KWAIT_BLOCK, используе-мых ядром для управления операцией ожидания. Никакой инициализации этих структур от вас не потребуется — ядру просто необходимо знать, где находится ал1ять для группы блоков ожидания, в которой будет храниться состояние каждого из объектов в процессе ожидания. Если ожидание происходит по небольшому ислу объектов (а конкретно — не большему величины THREAD„WAIT_OBJECTS, настоящее время равной 3), в этом параметре можно передать NULL. В этом лучае KeWaitForMultipleObjects использует заранее выделенный массив блоков - объекте потока. Если же ожидание ведется по большему количеству объек-св, вам придется предоставить неперемещаемую память объемом не менее count*S№of(KWAIT_BLOCK) байт.
184
Глава 4. Синхронизация
Остальные аргументы KeWaitForMultipleObjects не отличаются от соответствующих аргументов KeWaitForSingleObject, а возвращаемые коды имеют тот же смысл.
При передаче WaitAII возвращаемое значение STATUS_SUCCESS означает, что все объекты достигли установленного состояния одновременно. Если указать WaitAny, возвращаемое значение равно индексу объекта массива, перешедшего в установленное состояние. Если устанавливается сразу несколько объектов, вы получите информацию только об одном из них — возможно, с наименьшим индексом среди установленных, но, возможно, и о каком-нибудь другом. Полученное значение можно рассматривать как STATUS_WATT_O + индекс массива. Однако вы не должны выполнять обычную проверку NT_SUCCESS для возвращаемого кода перед извлечением индекса объекта в массиве, потому что проверку также пройдут другие возвращаемые значения (в том числе STATUS_TIMEOUT, STATUS_ALERTED и STATUS^ USER_APC). Используйте конструкции вида
NTSTATUS status = KeWa1tForMult1pleObjects(...);
if ((ULONG) status < count)
{
ULONG 1 Signaled = (ULONG) status - (ULONG) STATUS_WAIT_O;
'}
Когда функция KeWaitForMultipleObjects возвращает код состояния, равный индексу объекта в массиве для случая WaitAny, она также выполняет операции, необходимые для объекта этого типа. Если при вызове с WaitAny оказываются установленными сразу несколько объектов, операции выполняются только с тем объектом, индекс которого был возвращен функцией. Этот объект не всегда оказывается первым установленным объектом в массиве.
События ядра
В табл. 4.2 перечислены сервисные функции, используемые при работе с объектами событий ядра. Чтобы инициализировать объект события, сначала зарезервируйте неперемещаемую область памяти для объекта типа KEVENT, а затем вызовите KelnitializeEvent:
ASSERT(KeGetCurrentlrqK) == PASSIVE_LEVEL);
Kelnltlal1zeEvent(event. EventType, Initialstate);
Параметр event содержит адрес объекта события. EventType — одно из значений перечисляемого типа, NotificationEvent или Synchronization Event. Отличительной особенностью событий оповещения (NotificationEvent) является то, что после перехода в установленное состояние они остаются установленными вплоть до их явного перевода в сброшенное состояние. Более того, при установке события все потоки, ожидающие события оповещения, освобождаются. С другой стороны, событие синхронизации (SynchronizationEvent) переводится в сброшенное состояние с освобождением одного потока. Именно это происходит в пользовательском режиме при вызове SetEvent для объекта события с автоматическим сбросом.
Синхронизационные объекты ядра
185
Единственная операция, выполняемая функцией KeWaitXxx с объектами событий, — это перевод событий синхронизации в сброшенное состояние. Наконец, если параметр initialstate равен TRUE, то в исходном состоянии объект события установлен, а если параметр равен FALSE — сброшен.
Таблица 4.2. Функции для работы с объектами событий ядра
Функция	Описание
KeClearEvent	Переводит событие в сброшенное состояние (без возврата предыдущего состояния)
CelnitializeEvent	Инициализирует объект события
KeReadStateEvent	Определяет текущее состояние события (только Windows ХР и Windows 2000)
<eResetEvent	Переводит событие в сброшенное состояние (с возвратом предыдущего состояния)
KeSetEvent	Переводит событие в установленное состояние (с возвратом предыдущего состояния)
ФНЕЧАНИЕ-------------------------------------------------------------------------
В оазделах с описаниями примитивов синхронизации я повторяю ограничения IRQL, описанные в документации DDK. В текущей версии Microsoft Windows ХР в DDK иногда представлены ограничения более жесткие, нежели в реальной операционной системе. Например, функция KeClearEvent может вызываться на любом уровне IRQL, а не только на уровне DISPATCH_LEVEL и ниже. Функция KeinitializeEvent может вызываться на любом уровне IRQL, а не только на уровне PASSIVE-LEVEL. Тем не менее, информацию в SDK можно считать равносильной утверждению о том, что Microsoft огда-нибудь установит документированные ограничения. По этой причине я даже не пытаюсь сообщать об истинном положении дел.
Функция KeSetEvent переводит событие в установленное состояние:
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
_ONG wassignaled = KeSetEvent(event, boost, wait);
Как указывает директива ASSERT, для вызова этой функции выполнение должно вестись на уровне DISPATCH_LEVEL и ниже. Аргумент event содержит указатель на объект события, a boost определяет приращение приоритета ожидающего потока, если установка события приводит к прекращению чьего-либо ожидания. Логический аргумент wait описан во врезке («Хлопоты с третьим аргументом KeSetEvent»), в драйверах WDM он почти никогда не должен содержать TRUE. Возвращаемое значение отлично от нуля, если событие уже находится в установленном состоянии перед вызовом, и равно 0, если событие находилось в сброшенном состоянии.
Многозадачный планировщик должен искусственно повышать приоритет потока, ожидающего операций ввода/вывода или объектов синхронизации, чтобы потокам не приходилось слишком долго ждать. Дело в том, что поток, блокированный по какой-либо причине, обычно уступает свое процессорное время и не получает доступа к процессору до тех пор, пока не будет превосходить по
186
Глава 4. Синхронизация
приоритету другие потоки или пока не завершится время, выделенное другим потокам с тем же приоритетом. В то же время неб локированные потоки отрабатывают свои кванты полностью. Следовательно, если не «стимулировать» блокирующий поток, он потратит слишком много времени на ожидание завершения квантов потоков, интенсивно использующих процессор.
Не всегда очевидно, какое значение следует использовать в качестве приращения приоритета. Предлагаю хорошее эмпирическое правило: всегда используйте значение IOJ\IO_INCREMENTf если только у вас нет веских причин поступить иначе. Если установка события должна «разбудить» поток с передачей данных, критичной по времени (например, в звуковых драйверах), укажите приращение, подходящее для данного типа устройства (такое как IO_SOUND_INCREMENT). Здесь важно не «накачивать» приоритет ожидающей стороны без достаточных причин. Например, при попытке синхронной обработке запроса IRP_MJ_PNP (см, главу 6) вам придется ждать обработки IRP драйверами более низкого уровня, а функция завершения будет вызывать KeSetEvent. Поскольку запросы Plug and Play не особо интенсивно используют процессор и происходят относительно редко, даже для звуковой карты можно указать значение IO_NO_INCREMENT.
ХЛОПОТЫ С ТРЕТЬИМ АРГУМЕНТОМ KESETEVENT------------------------------------------
Третий аргумент KeSetEvent нужен для того, чтобы внутренний код мог очень быстро передавать управление от одного потока другому. Например, системные компоненты, не являющиеся драйверами устройств, могут создавать парные объекты событий, используемые клиентским и серверным потоками для организации обмена данными. Когда сервер хочет активизировать своего парного клиента, он вызывает KeSetEvent с аргументом wait, равным TRUE, а затем немедленно вызывает KeWaitXxx, переводя себя в режим ожидания. Использование wait позволяет выполнять эти две операции на атомарном уровне, чтобы ни один посторонний поток не активизировался между ними и не перехватил управление от клиента с сервером.
В DDK всегда приводятся какие-то описания внутренних механизмов системы, но это описание мне показалось малопонятным. Я попробую изложить его несколько иначе, чтобы вы поняли, почему в этом параметре всегда следует передавать FALSE. Во внутренней работе ядра используется блокировка диспетчерской базы данных для защиты операций, связанных с приостановкой, активизацией и планированием потоков. Если передать в аргументе wait значение TRUE, функция KeSetEvent устанавливает флаг, чтобы функции KeWaitXxx знали об этом факте, и возвращает управление без снятия блокировки. Когда наступит вторая фаза и вы вызовете KeWaitXxx (пожалуйста, сделайте это как можно быстрее — код работает на более высоком уровне IRQL, чем у всего оборудования, и вы владеете спин-блокировкой очень часто используемого ресурса), блокировку не придется устанавливать заново. В результате ваш поток активизирует ожидающий поток и переходит в состояние ожидания, не давая другим потокам возможности начать работу.
Прежде всего очевидно, что функция, вызывающая KeSetEvent с аргументом wait=TRUE, должна находиться в неперемещаемой памяти, потому что она в течение краткого времени будет выполняться на уровне выше DISPATCH_LEVEL. Однако трудно представить, зачем обычному драйверу устройства использовать этот механизм — вряд ли драйвер лучше ядра знает, какой поток будет запланирован следующим. Мораль: всегда передавайте FALSE в этом параметре. Я вообще не понимаю, зачем нужно было понапрасну искушать программистов этим параметром.
Текущее состояние события (на любом уровне IRQL) проверяется функцией KeReadStateEvent:
LONG signaled = KeReadStateEvent(event);
мзационные объекты ядра
187
ели событие установлено, возвращаемое значение отлично от нуля, а если шено — равно 0.
-ЛНИЕ —-------------------------------------------------------------------
1я KeReadStateEvent не поддерживается в Microsoft Windows 98/Ме, в отличие от других пе-енных функций KeReadStateXxx. Отсутствие поддержки этой функции обусловлено особен-
•ми реализации событий и других примитивов синхронизации в Windows 98/Ме.
Функция KeResetEvent проверяет текущее состояние события и* немедленно пелит его в сброшенное состояние (работает на уровне DISPATCH_LEVEL и ниже):
-^SERT(KeGetCurrentlrqK) <= DISPATCH_LEVEL);
. IG signaled = KeResetEvent(event);
Если предыдущее состояние события вас не интересует, можно сэкономить шого времени и воспользоваться функцией KeClearEvent:
~SERT(KeGetCurrentIrqlО <= DISPATCH_LEVEL);
eClearEvent(event);
Функция KeClearEvent работает быстрее, потому что ей не нужно читать теку-состояние события перед переводом его в сброшенное состояние. Однако льте внимательны и не вызывайте KeClearEvent в то время, когда то же собы-используется другим потоком, потому что не существует хорошего способа явления «состоянием гонки» между сбросом события и его установкой или лданием со стороны другого потока.
Использование событий синхронизации для взаимного сключения
зднее в этой главе я расскажу о двух типах объектах взаимного исключения — щгексах ядра и быстрых мьютексах. Такие объекты используются для ограниче-' доступа к общим данным в ситуациях, в которых спин-блокировка почему-X) не подходит. Иногда для решения этой задачи можно воспользоваться сонями синхронизации. Сначала определите событие в неперемещаемой памяти:
* pedef struct JEVICEJXTENSION {
KEVENT lock;
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
Инициализируйте его как событие синхронизации, находящееся в установ-ном состоянии:
•.elnitlal 1zeEvent(&pdx->lock, SynchronyzstlonEvent, TRUE);
Вход в «упрощенную критическую секцию» осуществляется ожиданием это-события, а выход — установкой события:
•e/altForSIngleObject(&pdx->lock, Executive, KernelMode, 7ALSE, NULL):
• eSetEvent(&pdx~>lock, EVENT INCREMENT, FALSE);
188
Глава 4. Синхронизация
Описанный прием следует использовать только в системных потоках для | предотвращения взаимных блокировок при вызовах NtSuspendThread из пользо-вательского режима (взаимная блокировка легко может возникнуть при запуске отладчика пользовательского режима для того же процесса). Но если выполнение ведется в пользовательском потоке, лучше воспользоваться быстрым мьютексом. И совсем не используйте этот прием в коде, выполняемом при перемещениях памяти (см. далее при описании «небезопасного» способа захвата быстрого мьютекса).
Семафоры ядра
Семафор ядра представляет собой целочисленный счетчик с синхронизационной семантикой. Семафор считается установленным при положительном значении счетчика и сброшенным, если счетчик равен нулю. Счетчик не может принимать отрицательные значения. Освобождение семафора приводит к увеличению счетчика, а успешное ожидание — к его уменьшению. Если счетчик уменьшается до 0, семафор считается сброшенным, как следствие, все стороны, вызывающие KeWaitXxx с ожиданием установки семафора, блокируются. Обратите внимание: если количество потоков, ожидающих семафора, превышает значение счетчика, не все ожидающие потоки будут разблокированы.
Ядро предоставляет три функции для управления состоянием объекта семафора (табл. 4.3). Семафор инициализируется вызовом следующей функции на уровне PASSIVE-LEVEL:
ASSERT(KeGetCurrentlrqK) == PASSIVEJ_EVEL);
KeIn1tializeSemaphore(semaphore, count, limit):
где semaphore — указатель на объект KSEMAPHORE в неперемещаемой памяти. Переменная count определяет начальное значение счетчика, a limit — максимальное допустимое значение счетчика.
Таблица 4.3. Функции для работы с объектами семафоров
Функция	Описание
KelnitializeSemaphore	Инициализирует объект семафора
KeReadStateSemaphore	Проверяет текущее состояние семафора
KeReleaseSemaphore	Переводит объект семафора в установленное состояние
Если создать семафор с предельным значение 1, получится объект, отчасти напоминающий мьютекс, — он также может быть захвачен только одним потоком. Тем не менее, мьютексы ядра обладают некоторыми возможностями для предотвращения взаимных блокировок, у семафоров такие возможности отсутствуют. Следовательно, создавать семафор с предельным значением 1 практически бессмысленно.
Другое дело — семафоры с предельным значением, большим 1. В вашем распоряжении появляется объект, который позволяет организовать работу нескольких
Синхронизационные объекты ядра
189
потоков с общим ресурсом. Хорошо известная теорема из теории очередей гласит, что создание единой очереди к нескольким серверам приводит к меньшему разбросу времени ожидания, чем ведение отдельной очереди для каждого сервера. Среднее время ожидания в обоих случаях одинаково, но разброс времени ожидания при единой очереди оказывается меньше; вот почему очереди в магазинах все чаще организуются так, что покупатели в общей очереди ждут освобождения следующей кассы. Данная разновидность семафоров помогает извлечь пользу из этой теоремы при организации групп программных или аппаратных серверов.
Владелец (или один из владельцев) семафора освобождает его вызовом KeReleaseSemaphore:
ASSERT(KeGetCurrentIrql О <= DISPATCHJ_EVEL);
_ONG wassignaled = KeReleaseSemaphoreCsemaphore, boost, delta, wait);
В результате этой операции значение delta (которое должно быть положительным) прибавляется к счетчику, связанному с семафором. Семафор переходит в установленное состояние, что приводит к освобождению других потоков. Как правило, в этом параметре передается число 1 -- это означает, что одна сторона, удерживающая семафор, отказывается от своих претензий. Параметры boost и wait обладают тем же смыслом, что и соответствующие параметры KeSetEvent, о которых говорилось ранее. Возвращаемое значение равно 0, если в предыдущем состоя-нш1 семафор был сброшен, или отлично от нуля, если семафор был установлен.
Функция KeReleaseSemaphore не позволяет увеличить счетчик сверх предела, указанного при инициализации семафора. Если попытаться это сделать, функция вообще не изменяет счетчик и инициирует исключение с кодом STATUS_ SEMAPHORE__LIMIT_EX CEEDED. Если исключение не будет перехвачено структурированным обработчиком, произойдет фатальный сбой.
Для получения информации о текущем состоянии семафора применяется следующий вызов:
ASSERT(KeGetCurrentlrqK) <= DISPATCHJ-EVEL);
-ONG signaled = KeReadStateSemaphore(semaphore):
Возвращаемое значение отлично от нуля, если семафор установлен, и равно О, если семафор сброшен. Не следует полагать, что возвращаемое значение совпадает с текущим значением счетчика, — если счетчик положителен, возвращаться может любая ненулевая величина.
После всего сказанного о семафорах ядра я должен сказать, что еще ни разу не видел драйвера, в котором бы они использовались.
Мьютексы ядра
Термин «мьютекс» (mutex) является сокращением от слов mutual exclusion, то есть «взаимное исключение». Объект мьютекса ядра предоставляет один из способов (причем не всегда лучший) упорядочения доступа к ресурсам, при котором
190
Глава 4. Синхронизация
несколько конкурирующих потоков оспаривают общий ресурс. Мьютекс считается установленным, если он не принадлежит ни одному потоку, и сброшенным, если у него в данный момент существует поток-владелец. Когда поток захватывает мьютекс вызовом одной из функций KeWaitXxr, ядро также запрещает доставку любых АРС, кроме специальных АРС режима ядра, для предотвращения возможных взаимных блокировок. Эта операция уже упоминалась ранее при обсуждении функции KeWaitForSingleObject (см. раздел «Ожидание одного объекта синхронизации»).
Обычно вместо мьютексов ядра лучше использовать быстрые мьютексы; эта тема более подробно рассматривается позже, в разделе «Объекты быстрых мьютексов»). Главное различие между этими объектами заключается в том, что захват быстрого мьютекса поднимает 1RQL до уровня APC_LEVEL, тогда как захват мьютекса ядра не изменяет IRQL. Почему это может быть существенно? В частности, потому, что завершение обработки так называемых синхронных IRP требует специальных вызовов АРС режима ядра, которые не могут происходить на уровнях IRQL выше PASSIVE__LEVEL. Таким образом, вы можете создавать и использовать синхронные 1RP при захваченном мьютексе ядра, но не при захваченном быстром мьютексе. Другая причина актуальна для драйверов, выполняемых при перемещениях памяти (см. далее при описании «небезопасного» способа захвата быстрого мьютекса).
Между двумя разновидностями объектов мьютексов существует еще одно, менее важное различие: мьютексы ядра могут захватываться рекурсивно, а для быстрых мьютексов это невозможно. Иначе говоря, владелец мьютекса ядра может снова вызвать KeWaitXxr с указанием того же мьютекса и немедленно перейти к ожиданию. Чтобы мьютекс считался свободным, поток, который поступает подобным образом, должен освободить мьютекс соответствующее количество раз.
В табл. 4,4 перечислены функции, используемые при работе с объектами мьютексов.
Таблица 4.4. Функции для работы с объектами мьютексов
Функция	Описание
KelnitializeMutex	Инициализирует объект мьютекса
KeReadStateMutex	Проверяет текущее состояние мьютекса
KeReleaseMutex	Переводит объект мьютекса в установленное состояние
Чтобы создать мьютекс, необходимо зарезервировать неперемещаемую память для объекта KMUTEX и инициализировать его следующим вызовом функции:
*ASSERT(KeGetCurrentIrqlО == PASSIVE_LEVEL);
KeImtializeMutex(miitex, level);
где mutex — адрес объекта KMUTEX, a level — параметр, изначально предназначенный для предотвращения взаимных блокировок в тех ситуациях, когда в вашем коде используется несколько мьютексов. В настоящее время ядро игнорирует параметр level, поэтому я не стану подробно описывать его.
Cr -. ? .низационные объекты ядра
191
Мьютекс начинает свое существование в установленном состоянии (то есть L> имеет владельца). Немедленный вызов KeWaitYxx берет мьютекс под контроль Ь ереводит его в сброшенное состояние.
Текущее состояние мьютекса проверяется вызовом следующей функции:
’ -~SERT(KeGetCurrentIrql() <= DISPATCHJ_EVEL);
_ NG signaled = KeReadStateMutex(mutex);
Возвращаемое значение равно 0, если мьютекс в настоящее время имеет вла-Ьд :.ца, или отлично от нуля, если мьютекс свободен.
. (сток, являющийся владельцем мьютекса, может отказаться от захвата и вер-12. т мьютекс в установленное состояние:
. -“SERHKeGetCurrentlrql О <= DISPATCH_LEVEL);
_?iG wassignaled = KeReleaseMutexCmutex, wait);
' 1араметр wait означает то же самое, что и соответствующий аргумент KeSet-z е " Возвращаемое значение всегда равно 0, то есть указывает на то, что мью-г* . с ранее был захвачен, если бы это было не так, вызов KeReleaseMutex привел И < фатальному сбою (освобождение мьютекса любой другой стороной, кроме |Ь> владельца, является ошибкой).
Для полноты картины я должен упомянуть о макросе KeWaitForMutexObject, г .ставленном в DDK (см. WDM.H). Макрос определяется очень просто:
define KeWaitForMutexObject KeWaitForSingleObject
1 1 Использование специального имени не дает ровно никаких преимуществ, lb ч-литятор даже не настаивает на том, чтобы первый аргумент содержал указа-I т* на KMUTEX (вместо указателя на произвольный тип).
Таймеры ядра
Я также поддерживает объекты таймера, они напоминают события, автома-т»- ски устанавливаемые после наступления заданного абсолютного времени истечения заданного интервала. Также существует возможность создания Ьй' ера, автоматически переходящего в установленное состояние, и организации ^ратного вызова DPC по истечении таймера. В табл. 4.5 перечислены функции : боты с объектами таймеров.
.•&-ица 4.5. Функции для работы с объектами таймеров
•>-ЧЦИЯ
•eCc-celTimer
er lalizeTimer
Ql" LjlizeTimerEx
< TrdStateTirner
• ‘. 1 imer
Ж--<:~ипегЕх
Описание
Отменяет активный таймер
Инициализирует одноразовый таймер оповещения
Инициализирует одноразовый или многоразовый таймер оповещения или синхронизации
Проверяет текущее состояние таймера
Задает (возможно, повторно) срок истечения для таймера оповещения
Задает (возможно, повторно) срок истечения и другие свойства таймера
192
Глава 4. Синхронизации
В нескольких ближайших разделах я опишу несколько основных сценарие? использования таймеров:
О таймеры как самоустанавливающиеся события;
О таймеры с функциями DPC, вызываемыми при истечении заданного времени О периодические таймеры, многократно вызывающие функции DPC.
Таймеры оповещения, используемые как события
В этом сценарии мы создаем объект таймера оповещения и дожидаемся его истечения. Прежде всего следует создать объект KTIMER в неперемещаемой памяти затем объект таймера инициализируется на уровне DISPATCH_LEVEL или ниже
PKTIMER timer;
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
Kelnltlal 1zeTlmer(tlmer);
На этой стадии таймер находится в сброшенном состоянии и не ведет отсчет — ожидание по такому таймеру будет бесконечным. Чтобы таймер начал отсчет, необходимо вызвать функцию KeSetTimer:
ASSERT(KeGetCurrentlrqK) <= DISPATCHJ_EVEL);
LARGE INTEGER duetime;
BOOLEAN wascountlng = KeSetTimer(timer, duetime, NULL);
Значение duetime представляет собой 64-разрядную величину, выраженную в 100-наносекундных единицах. Положительное значение определяет абсолютный момент времени по отношению к 1 января 1601 года, «эпохе» системного таймера. Отрицательное значение определяет интервал по отношению к текущему времени. При задании абсолютного времени последующие изменения системных часов влияют на продолжительность тайм-аута. Другими словами, таймер сработает лишь после того, как показания системных часов сравняются с заданным абсолютным значением или превысят его. С другой стороны, относительный таймаут не зависит от изменений системных часов. В отношении таймеров действуют те же правила, что и в отношении параметра тайм-аута в KeWaiLVxx.
Если возвращаемое значение KeSetTimer равно TRUE, это означает, что таймер уже ведет отсчет (в этом случае наш вызов KeSetTimer отменит его и начнет отсчет заново).
Состояние таймера в любой момент времени проверяется функцией KeRead-StateTimer:
ASSERT(KeGetCurrentlrqK) <= DISPATCH_LEVEL);
BOOLEAN counting = KeReadStateTlmer(tlmer):
Сейчас функции KelnitializeTimer и KeSetTimer считаются устаревшими, и у них появились новые усовершенствованные аналоги. Инициализация таймера также может выполняться следующим вызовом:
ASSERTCKeGetCurrentlqrl() <= DISPATCH_LEVEL);
KeimtlallzeTImerExCtlmer. NotlflcatlonTImer);
Синхронизационные объекты ядра
193
Другая расширенная функция, KeSetTimerEx, предназначена для установки таймера:
ASSERT(KeGetCurrentIrqlО <= DISPATCH_LEVEL);
LARGEJNTEGER duetime;
BOOLEAN wascounting = KeSetTimerEx(timer, duetime, 0, NULL);
Вскоре я объясню смысл дополнительных параметров в расширенных версиях этих функций.
Пока таймер ведет отсчет, он по-прежнему остается в сброшенном состоянии вплоть до заданного момента срабатывания. В этот момент объект переходит в установленное состояние, а все ожидающие его потоки освобождаются. Система гарантирует лишь то, что таймер сработает не раньше указанного вами времени. Если момент истечения задан с точностью, меньшей гранулярности системных часов (которой вы управлять не можете), то тайм-аут произойдет позднее заданного вами точного момента времени. Гранулярность системных часов проверяется функцией KeQueryTimelncrement.
Таймеры оповещения с DPC
В этом сценарии при истечении таймера должен происходить вызов DPC. Этот метод обычно выбирается в тех ситуациях, когда вы хотите быть уверены в своевременной обработке тайм-аута независимо от уровня приоритета потока (поскольку ожидание может происходить только на уровне ниже DISPATCH_LEVEL, возврат управления процессору после истечения таймера подвержен обычным превратностям планирования потоков, однако вызовы DPC выполняются на повышенном уровне IRQL и фактически вытесняют все остальные потоки).
Инициализация объекта таймера выполняется аналогичным образом. Кроме того, необходимо дополнительно инициализировать объект KDPC, для которого была выделена неперемещаемая память. Пример:
-<DPC dpc; // <== указатель на выделенный объект KDPC
-SSERT(KeGetCurrentIrqlО == PASSIVELEVEL);
selnitial 1zeTimer(tlmer);
*eInitializeDpc(dpc, DpcRoutine, context);
Объект таймера инициализируется вызовом функции KelnitializeTimer или telnitializeTimerEx, по вашему усмотрению. Параметр DpcRoutine содержит адрес функции отложенного вызова, которая должна находиться в неперемещаемой памяти. Параметр context представляет собой произвольное 32-разрядное значе-жие (типа PVOID), передаваемое в качестве аргумента функции DPC. Аргумент 4рс содержит указатель на объект KDPC, созданный в неперемещаемой памяти (например, находящийся в расширении устройства).
Запуская обратный отсчет на таймере, мы передаем объект DPC в одном из аргументов KeSetTimer или KeSetTimerEx:
-ZSERTCKeGetCurrentlrql () <= DISPATCH-LEVEL);
ARGEJNTEGER duetime;
EjjLEAN wascounting = KeSetTimer(timer. duetime, dpc);
194
Глава 4. Синхронизация
При желании также можно воспользоваться расширенной формой KeSetTimerEx. Единственное отличие этого вызова от описанного в предыдущем разделе состоит в том, что на этот раз в аргументе передается адрес объекта DPC. При истечении таймера система ставит DPC в очередь на выполнение, как только позволят условия. Это произойдет по крайней мере сразу же, как только ожидающий поток сможет активизироваться. Общая структура функции DPC выглядит примерно так:
VOID DpcRoutine(PKDPC dpc. PVOID context. PVOID junkl.
PVOID junk2)
{
}
Вообще говоря, даже если функции KeSetTimer или KeSetTimerEx передавался аргумент DPC, при желании вы все равно можете вызвать KeWaitAxx для ожидания на уровне PASSIVEJ-EVEL или APCJ_EVEL В однопроцессорной системе вызов DPC произойдет до завершения ожидания, потому что он выполняется на более высоком уровне IRQL.
Таймеры синхронизации
Объекты таймеров, как и объекты событий, существуют в двух разновидностях. Таймер оповещения позволяет любому числу ожидающих потоков продолжить работу после его истечения. С другой стороны, таймер синхронизации позволяет продолжить работу только одному потоку. После того как ожидание потока будет завершено, таймер переключается в сброшенное состояние. Для создания таймера синхронизации следует использовать расширенную функцию инициализации:
ASSERT(KeGetCurrentIrqlО <= DISPATCHJTVEL):
Kelnltial 1zeTimerEx(t1mer, SynchronlzatlonTImer):
Здесь SynchronizationTimer — одно из значений перечисляемого типа TIMER_TYPE (другое возможное значение — NotificationTimer).
Если вы используете DPC с таймером синхронизации, постановку DPC в очередь можно рассматривать как дополнительную операцию, происходящую при истечении таймера. Иначе говоря, истечение таймера переводит его в установленное состояние и ставит в очередь DPC. При установке такого таймера освобождается только один поток.
Единственное практическое применение, которое мне удалось найти для таймеров синхронизации, — это периодические таймеры (см. следующий раздел).
Периодические таймеры
Все таймеры, рассматривавшиеся до настоящего момента, срабатывали ровно один раз. Расширенная функция установки таймера также позволяет запросить периодический тайм-аут:
ASSERT(KeGetCurrentIrqlО <= DISPATCH_LEVEL):
LARGEJNTEGER duet 1 me;
Синхронизационные объекты ядра
195
BOOLEAN wascounting = KeSetTimerEx! timer. duetime, period, dpc):
Здесь аргумент period определяет периодический тайм-аут в миллисекундах, 2 dpc содержит необязательный указатель на объект KDPC. Таймеры такого рода г начала срабатывают в заданный момент, а затем периодически срабатывают в дальнейшем. Чтобы достичь точного периодического срабатывания, задайте . тносительное время срабатывания, совпадающее с интервалом. Если задать нулевое время, таймер сработает немедленно, после чего начинается его периоди--еское срабатывание. Кстати говоря, часто бывает удобно запустить периодиче-::<ий таймер в сочетании с объектом DPC, потому что это позволяет получать . повещения о срабатывании без многократного ожидания тайм-аута.
Обязательно вызовите KeCancelTimer для отмены периодического таймера пе-лд тем, как объект KTIMER или функция DPC исчезнут из памяти. Будет весьма О неприятно, если система выгрузит ваш драйвер, а спустя 10 нс вызовет несу-чествующую функцию DPC. Мало того, при этом произойдет фатальный сбой 0 гистемы. Подобные проблемы так трудно выявлять, что в Driver Verifier преду-гмотрена специальная проверка освобождения памяти, содержащей активный хъект KTIMER.
Пример
Одно из возможных применений таймеров ядра — организация цикла опроса = системном потоке, постоянно проверяющего устройство на выполнение каких-члбо действий. В наши дни циклы опроса редко используются в драйверах, но, возможно, ваше устройство принадлежит к числу немногочисленных исключений. Мы обсудим эту тему в главе 14, а в прилагаемые материалы включен пример драйвера (POLLING), который демонстрирует все задействованные концепции. Частью этого примера является цикл, опрашивающий устройство : фиксированными интервалами. Логика драйвера организована таким образом, что выход из цикла осуществляется по специальному событию, поэтому в драйвере используется функция KeWaitForMultipleObjects. Реальный код чуть сложнее те дующего фрагмента, специально отредактированного мной для выделения чети, связанной с таймером:
.DID Poll1ngThreadRout1ne(PDEVICE_EXTENSI0N pdx) {
NTSTATUS status:
KTIMER timer;
KeIn1t1al1zeT1merEx(&t1mer, SynchronlzatlonTImer);	// 1
PVOID polleventsE] = {	//2
(PVOID) &pdx->evK111, (PVOID) &t1mer. };
C__ASSERT(arrays1ze(pollevents) <= THREAD-WAIT-OBJECTS):
LARGEJNTEGER duetime = {0}:
#def1ne POLLING-INTERVAL 500
196
Глава 4. Синхронизация
KeSetT1merEx(&tImer, duetime, POLLING-INTERVAL. NULL); // 3 while (TRUE)
status = KeWa1tForMultiple0bjects(arraysize(pollevents),	// 4
poll events, WaitAny, Executive, KernelMode, FALSE, NULL, NULL);
if (status == STATUS_WAIT_O) break;
If (^устройство требует внимания^	// 5
<cделать что-то>;
}
KeCancelT1mer(&t1mer);
PsTerm1nateSystemThread(STATUS_SUCCESS);
}
1.	Инициализация таймера ядра. В данной ситуации необходимо использовать таймер синхронизации (SynchronizationTimer), потому что таймеры оповещения остаются в установленном состоянии после первого срабатывания.
2.	Здесь создается массив указателей на объекты синхронизации, передаваемый в одном из аргументов KeWaitForMultipleObjects. Первый аргумент массива представляет событие-завершитель, это событие может устанавливаться другими частями драйвера, когда наступает время завершать системный поток. Второй элемент представляет объект таймера. Директива C_ASSERT за определением массива убеждается в том, что количество объектов в массиве довольно мало для автоматического использования стандартного массива блокировок в объекте потока.
3.	Функция KeSetTimerEx запускает периодический таймер. Параметр duetime равен 0, поэтому таймер немедленно переходит в установленное состояние. В дальнейшем он будет срабатывать каждые 500 мс.
4.	В цикле опроса мы ожидаем срабатывания таймера или установки события-завершителя. Если ожидание завершается из-за события, мы выходим из цикла, проводим зачистку и выходим из системного потока. Если же ожидание завершилось из-за истечения таймера, можно переходить к следующему шагу.
5.	Здесь драйвер устройства занимается обслуживанием оборудования.
Альтернативы для таймеров ядра
Вместо применения объектов таймеров ядра иногда бывает удобнее воспользоваться двумя другими хронометражными функциями. Функция KeDelayExecutionThread ожидает на уровне PASSIVE_LEVEL в течение заданного интервала. Разумеется, такая запись гораздо компактнее, чем создание, инициализация, установка и ожидание таймера с применением отдельных вызовов:
ASSERTCKeGetCurrentlrqlО == PASSIVE_LEVEL);
LARGEJNTEGER duetime;
NSTATUS status = KeDelayExecut1onThread(WaitMode,
Alertable, Sduetime);
Синхронизационные объекты ядра
197
Здесь WaitMode, Alertable и возвращаемый код состояния имеют тот же смысл, что и соответствующие параметры KeWaitXxr, a duetime — такое же значение времени. какие обсуждались ранее при описании таймеров ядра. Учтите, что в параметре тайм-аута этой функции должен передаваться указатель на большое целое число, тогда как остальным функциям, связанным с таймерами, передается само число.
Для организации очень коротких задержек (менее 50 мс) также можно вызвать функцию KeStalJExecutionProcessor на любом уровне IRQL:
c	'eSta 11 Executi onProcessor (nMi croSeconds);
Такие задержки вносятся для того, чтобы оборудование успело подготовиться к следующей операции, прежде чем программа продолжит работу. Задержка может оказаться гораздо больше заданной, потому что выполнение KeStallExecution-frocessor может быть вытеснено операциями, которые используют более высокий уровень IRQL, нежели вызывающая сторона.
^пользование потоков для синхронизации
Подсистема управления процессами предоставляет ряд функций, которые могут использоваться драйверами WDM для создания системных потоков и управления ими. Эти функции рассматриваются в главе 14 в контексте их возможного применения для управления устройствами, требующими периодического опроса. А сейчас для полноты картины я хочу упомянуть, что при вызове KeWaitXxr можно использовать указатель на объект потока ядра, чтобы функция ожидала завершения потока. Сам поток завершает свою работу вызовом PsTerminateSystemThread.
Но прежде чем ожидать завершения потока, нужно сначала получить указатель на закрытый объект KTHREAD, представляющий этот поток во внутренних механизмах системы, а с этим возникают проблемы. При выполнении в контексте потока получить свой указатель на KTHREAD нетрудно:
-	SSERT(KeGetCurrentlrqK) <= DISPATCH_LEVEL);
ZKTHREAD thread = KeGetCurrentThreadO;
К сожалению, при создании нового потока вызовом PsCreateSystemThread вы получаете лишь приватный манипулятор HANDLE для этого потока. Для получения указателя на KTHREAD следует воспользоваться сервисной функцией Object Manager:
-	ANGLE hthread;
-	KTHREAD thread;
-	sCreateSystemThreadC&hthread, ...);
IbReferenceObjectByHandle(hthread, THREAD_ALL_ACCESS, NULL, KernelMode, (PVOID*) Sthread, NULL);
ZwClose(hthread);
Функция ObReferenceObjectByHandle преобразует манипулятор в указатель на базовый объект ядра. Получив указатель, манипулятор можно закрыть вызовом ZivClose. Далее в какой-то момент ссылку на объект потока необходимо освободить вызовом ObDereferenceObject:
loDereferenceObjectCthread);
198
Глава 4. Синхронизация
Сигналы потоков и АРС
Во внутренней работе Windows NT ядро активизирует ожидающие потоки при помощи сигналов (alerts). Асинхронный вызов процедур (АРС) используется для активизации потока с целью выполнения некоторой функции в контексте этого потока. Вспомогательные функции, генерирующие сигналы и АРС, не предоставляются в распоряжение разработчиков драйверов WDM. Но поскольку многочисленные упоминания этих концепций встречаются в документации DDK и заголовочных файлах, я хочу завершить знакомство с синхронизационными объектами ядра их кратким объяснением.
Начнем с «азов» работы этих двух механизмов. Когда кто-то блокирует поток вызовом одной из функций KeWaitXxx, он при помощи специального логического аргумента указывает, возможно ли прерывание ожидания по сигналу — то есть без удовлетворения условий ожидания или наступления тайм-аута, а из-за получения сигнала потока. Сигналы потоков возникают в пользовательском режиме при вызове функции API NtAlertThread. Если ожидание завершилось преждевременно из-за полученного сигнала, ядро возвращает специальный код состояния STATUS_ALERTED.
АРС (Asynchronous Procedure Call) — механизм, посредством которого операционная система может выполнить функцию в контексте конкретного потока. Упоминание асинхронности в названии обусловлено тем фактом, что система, фактически прерывает работу целевого потока для выполнения внеочередной функции.
Вызовы АРС делятся на три разновидности: вызовы пользовательского режима, вызовы режима ядра и специальные вызовы режима ядра. Код пользовательского режима запрашивает АРС пользовательского режима вызовом функции Win32 API QueuellserAPC. Код режима ядра запрашивает АРС вызовом недокументированной функции, прототип которой отсутствует в заголовках DDK. Наверное, квалифицированные аналитики уже знают имя этой функции и умеют ее вызывать, но эта функция предназначена для внутреннего использования, поэтому я о ней ничего больше не скажу. Система накапливает в очереди вызовы АРС для потока, пока не будут выполнены условия для их выполнения. Такие условия зависят от типа АРС:
О Специальные вызовы АРС режима ядра выполняются как можно раньше, то есть как только в потоке могут быть запланированы операции на уровне АРС-LEVEL Во многих ситуациях они даже могут временно активизировать заблокированные потоки.
О Вызовы АРС нормального режима ядра выполняются после всех специальных АРС, но только когда выполняется целевой поток и в нем не выполняются другие АРС режима ядра. Вызовы АРС нормального режима ядра и пользовательского режима можно заблокировать вызовом KeEnterCriticalRegion.
О Вызовы АРС пользовательского режима выполняются после того, как для целевого потока будут выполнены обе разновидности АРС режима ядра, но только если поток ранее находился в ожидании, для которого разрешено прерывание по сигналу, в пользовательском режиме. Выполнение, фактически, происходит, когда поток будет в следующий раз запланирован для выполнения в пользовательском режиме.
Синхронизационные объекты ядра
199
Если система активизирует поток для доставки АРС пользовательского режима, то функция ожидания, которой поток был ранее заблокирован, возвращает один из специальных кодов состояния, STATUS_KERNEL_APC или STATUS_USER_APC.
Странная роль APC_LEVEL
Как мне показалось, уровень IRQL с именем APC_LEVEL работает довольно странным образом. Блокировка разрешается для потоков, работающих на уровне ApC_LEVEL (а также на уровне PASSIVE_LEVEL, но сейчас нас интересует только APCJJEVEL). Потоки уровня APC_LEVEL также могут быть прерваны любым аппаратным устройством, после чего планировщик может счесть подходящим для выполнения другой поток с более высоким приоритетом. Так или иначе, планировщик потоков может передать процессор другому потоку, который может выполняться на уровне PASSIVE-LEVEL или APC_LEVEL. Фактически, уровни IRQL PASSIVE-LEVEL и APC_LEVEL относятся к потокам, тогда как более высокие уровни IRQL относятся к процессору.
Применение АРС в запросах ввода/вывода
Ядро использует концепцию АРС для нескольких целей. Но так как книга посвящена написанию драйверов устройств, я объясню лишь место АРС в процессе выполнения операций ввода/вывода. В одном из многих возможных сценариев, к>.»гда программа пользовательского режима выполняет синхронную операцию ReadFile с манипулятором, подсистема Win32 вызывает функцию режима ядра : именем NtReadFile. NtReadFile создает и направляет пакет IRP соответствующему драйверу устройства, который часто возвращает STATUS-PENDING, указывая тем самым, что операция не закончена. NtReadFile возвращает код состояния функции ReadFile, которая в результате вызывает NtWaitForSingleObject для ожидания объекта файла, на который указывает манипулятор пользовательского режима. В свою очередь, NtWaitForSingleObject вызывает KeWaitForSingleObject для выполнения ожидания пользовательского режима, не прерываемого по сигналу, для объекта события в объекте файла.
Когда драйвер устройства в конечном итоге завершает операцию чтения, он вызывает функцию loCompleteRequest, которая ставит в очередь специальный вызов АРС режима ядра. Функция АРС вызывает KeSetEvent для установки события •<ьекта файла; приложение освобождается для продолжения работы. Некоторая разновидность АРС необходима из-за того, что некоторые задачи, выполняемые при завершении запроса ввода/вывода (например, копирование буфера), должны осуществляться в адресном контексте запрашивающего потока. АРС режима *дра необходимы из-за того, что поток, о котором идет речь, не находится в состоя-нии ожидания, прерываемом по сигналу. Специальные АРС необходимы из-за того, что на момент доставки АРС система может отдать предпочтение другому потоку. В сущности, АРС представляет собой механизм активизации потоков.
Функции режима ядра могут вызывать функцию ZwReadFile, которая преобразуется в вызов NtReadFile. Если соблюдать все предписания в документации DDK при вызове ZwReadFile, ваш вызов NtReadFile будет очень похож на вызов пользовательского режима и будет обрабатываться почти так же, по с двумя отличиями.
200
Глава 4. Синхронизация
Первое — относительно второстепенное — состоит в том, что все ожидание будет производиться в режиме ядра. Второе отличие: если при вызове ZwCreateFile было указано, что вы намерены выполнять синхронные операции, I/O Manager автоматически ожидает завершения операции чтения. Ожидание может быть прерываемым по сигналу или нет, в зависимости от параметров, указанных при вызове ZwCreateFile.
Как задавать параметры Alertable и WaitMode
Теперь вы обладаете достаточной информацией, чтобы понять последствия от использования параметров Alertable и WaitMode в вызовах различных примитивов ожидания. Как правило, вам никогда не придется писать код синхронной обработки запросов из пользовательского режима. В принципе, такая необходимость может возникнуть, скажем, для некоторых запросов управления вво-дом/выводом. Однако в общем случае лучше объявить любые операции, на выполнение которых уходит много времени, незавершенными (возвратом кода STATUS_PENDING из диспетчерской функции) и завершить их в асинхронном режиме. Таким образом, необходимость в вызове примитивов ожидания возникает не так уж часто. Блокировка потоков в драйверах устройств оправданна лишь в нескольких ситуациях, которые будут описаны в следующих разделах.
Потоки ядра
Иногда вы создаете собственные потоки режима ядра — например, если ваше устройство нуждается в периодическом опросе. В этом сценарии все ожидание выполняется в режиме ядра, потому что поток работает исключительно в режиме ядра.
Обработка запросов Plug and Play
В главе 6 я покажу, как организуется обработка запросов ввода/вывода, отправляемых РпР Manager. Некоторые из этих запросов должны обрабатываться синхронно. Другими словами, вы должны передать эти запросы в стеке драйверов на нижний уровень и дождаться их завершения. Для ожидания в режиме ядра вызывается функция KeWaitForSingleObject, потому что вызов со стороны РпР Manager осуществляется в контексте потока режима ядра. Кроме того, если обработка запроса РпР потребует выполнения вспомогательных запросов (например, взаимодействия с устройством USB), ожидание будет производиться в режиме ядра.
Обработка других запросов ввода/вывода
Если вы обрабатываете другие виды запросов ввода/вывода и знаете, что выполнение ведется в контексте фиксированного потока, который должен получить результаты ваших действий перед продолжением, возможно, стоит заблокировать этот поток вызовом примитива ожидания. В таких случаях ожидание стоит вести в таком же режиме процессора, как у стороны, которая обратилась с вызовом. Как правило, в таких случаях можно просто положиться на значение RequestorMode в IRP, обрабатываемом в настоящий момент. Если управление было получено не в результате приема IRP, то для определения предыдущего режима процессора можно воспользоваться функцией ExGetPreviousMode. Если
Другие синхронизационные примитивы ядра
201
ожидание должно быть относительно долгим, передайте результат этих про* верок в аргументе WaitMode функции KeWaitXxx, а также укажите значение TRUE в аргументе Alertable.
ГИМЕЧАНИЕ---------------------------------------------------
Мораль: выполняйте ожидание без возможности прерывания по сигналу, если только вы твердо не уверены в необходимости обратного.
Другие синхронизационные примитивы ядра
Ядро Windows ХР также предоставляет некоторые дополнительные методы синхронизации выполнения между потоками и защиты доступа к общим объектам. В этом разделе рассматриваются быстрые мьютексы — объекты взаимного исключения, которые работают быстрее объектов ядра, потому что они оптимизированы для ситуаций с отсутствием конкуренции. Я также опишу категорию вспомогательных функций, в именах которых присутствует слово Interlocked. Эти функции предназначены для выполнения типовых операций (таких как ^сличение или уменьшение целого числа или вставка/удаление элементов в связанных списках) на атомарном уровне, предотвращающем вмешательство со сто-I роны механизма многозадачности или других процессоров.
Объекты быстрых мьютексов
Быстрые мьютексы могут использоваться вместо мьютексов ядра для защиты критических секций кода. В табл. 4.6 перечислены функции для работы с объектами этого типа.
Таблица 4.6. Функции для работы с быстрыми мьютексами
Функция	Описание
ExAcquireFastMutex ExAcq u i reFastM utexll nsafe	Захватывает мьютекс (с ожиданием в случае необходимости) Захватывает мьютекс (с ожиданием в случае необходимости) в ситуации, в которой вызывающая сторона уже запретила получение АРС
ExInitializeFastMutex	Инициализирует объект мьютекса
ExReleaseFastMutex ExReleaseFastMutexUnsafe	Освобождает мьютекс Освобождает мьютекс без повторного разрешения доставки АРС
ExT lyToAcquireFastMutex	Захватывает мьютекс, если это возможно сделать без ожидания
По сравнению с мьютексами ядра быстрые мьютексы обладают как преимуществами, так и недостатками, перечисленными в табл. 4.7. Основным преимуществом является гораздо большая скорость захвата и освобождения при отсутствии реальной конкуренции. Основной недостаток — невозможность получения потоком, захватившим мьютекс, некоторых типов АРС в зависимости от вызываемых функций — это ограничивает возможности отправки IRP другим драйверам.
202
Глава 4. Синхронизация
Таблица 4.7. Сравнение объектов мьютексов ядра с быстрыми мьютексами
Мьютекс ядра	Быстрый мьютекс
Захватывается рекурсивно одним потоком (система ведет счетчик захватов) Работает относительно медленно Владелец получает только «специальные» вызовы АРС ядра Может быть частью ожидания по нескольким объектам	Не может захватываться рекурсивно Работает относительно быстро Если не вызвать функцию XxxUnsafe, владелец не получает никаких АРС Не может использоваться в аргументах функции KeWaitForMultipleObjects
В документации DDK издавна утверждается, что ядро повышает приоритет потока, захватившего мьютекс. Я располагаю надежной информацией о том, что это утверждение не соответствует действительности с 1992 года. В документации также давно утверждается, что поток, удерживающий мьютекс, не может исключаться из балансового множества (то есть все его страницы не могут быть выгружены из физической памяти). Это утверждение было истинным на ранней стадии существования Windows NT, но сейчас оно уже давно не соответствует действительности.
Чтобы создать быстрый мьютекс, необходимо сначала выделить память для структуры данных FAST_MUTEX в неперемещаемой памяти, а затем инициализировать объект вызовом «функции» ExInitializeFastMutex, которая в действительности представляет собой макрос в WDM.H:
ASSERT(KeGetCurrentlrqK) <= DISPATCH_LEVEL);
ExInitial 1zeFastMutex(FastMutex):
где FastMutex — адрес объекта FAST_MUTEX. В своем исходном состоянии мьютекс не имеет владельца. Чтобы получить право владения мьютексом, вызовите одну из следующих функций:
ASSERT(KeGetCurrentlrqK) < DISPATCH_LEVEL);
ExAcqulreFastMutex(FastMutex);
или
ASSERT(KeGetCurrentlrqK) < DISPATCH_LEVEL);
ExAcquIreFastMutexUnsafe(FastMutex)
Первая функция ожидает освобождения мьютекса, назначает его владельцем поток, от которого поступил вызов, а затем повышает текущий уровень IRQL процессора до APC_LEVEL. Повышение IRQL, фактически, блокирует доставку всех АРС. Вторая функция не изменяет уровня IRQL.
При захвате быстрого мьютекса «небезопасной» функцией следует предусмотреть возможность потенциальных взаимных блокировок. Избегайте ситуаций, в которых код пользовательского режима приостанавливает поток, удерживающий мьютекс. Это приведет к взаимной блокировке с остальными потоками, нуждающимися в этом мьютексе. По указанной причине DDK рекомендует (a Driver Verifier требует) предотвратить вызовы АРС пользовательского режима и нор-
ме синхронизационные примитивы ядра
203
етьного режима ядра либо посредством повышения IRQL до уровня APC_LEVEL, вызовом KeEnterCriticalRegion перед ExAcquireFastMutexUnsafe (механизм АРС лействован в приостановке потоков, поэтому код пользовательского режима может приостановить поток, если вы запретите АРС пользовательского ре-
•гма. Да, я знаю, что это выглядит довольно странно!).
Другая возможная ситуация взаимной блокировки возникает в драйверах i пали перемещения памяти, то есть в драйверах, которые вызываются, чтобы ючъ подсистеме управления памятью обработать страничный сбой. Допустим, просто вызываете KeEnterCriticalRegion, а затем ExAcquireFastMutexUnsafe. Теперь •^положим, что система пытается выполнить специальный вызов АРС режима а в том же потоке — это возможно, потому что функция KeEnterCriticalRegion запрещает специальные АРС режима ядра. Вызов АРС может инициировать аничный сбой, а это может привести к повторному вхождению и взаимной •кировке при второй попытке захвата того же мьютекса. Ситуация предотвращен либо повышением IRQL до уровня APC_LEVEL перед захватом мьютекса, ю (более простой способ) использованием KeAcquireFastMutex вместо Ke Acquire-iMutexUnsafe. Конечно, аналогичная проблема возникает и при использовании шных объектов KMUTEX и событий синхронизации.
ti 4 u u i tй 3 H' J £ H
ЛИЕ---------------------------------------------------------------------------------
использовании KeAcquireFastMutex выполнение ведется на уровне APC_LEVEL. Это означает, зы не сможете создавать синхронные IRP (соответствующая функция должна вызываться на =не PASSIVE_LEVEL). Более того, попытка ожидания завершения синхронного IRP приведет « взаимной блокировке (потому что для завершения необходимо выполнение АРС, невозможное •&-за уровня IRQL). В главе 5 я покажу, как эта проблема решается с помощью асинхронных IRP.
Если вы не хотите ждать освобождения мьютекса, недоступного в данный момент, попробуйте воспользоваться функцией «попытки захвата»:
-SSERT(KeGetCurrentlrqK) < DISPATCH_LEVEL);
EZ3LEAN acquired = ExTryToAcqulreFastMutex(FastMutex);
Если возвращаемое значение равно TRUE, значит, мьютекс был успешно захвачен. Если же оно равно FALSE, то мьютекс уже кому-то принадлежит и захва-татъ его вам не удалось.
Чтобы освободить захваченный быстрый мьютекс и дать возможность захватывать его другим потокам, вызовите функцию освобождения, соответствующую способу захвата быстрого мьютекса:
-SSERTtKeGetCurrentIrqlО < DISPATCH_LEVEL):
ExReleaseFastMutex(FastMutex);
или
-SSERT(KeGetCurrentIrql() < DISPATCH_LEVEL);
ExReleaseFastMutexUnsafefFastMutex);
Быстрый мьютекс называется быстрым, потому что этапы захвата и освобождения оптимизированы для стандартной ситуации с отсутствием конкуренции
204
Глава 4. Синхронизация
за мьютекс. Критическим этапом захвата мьютекса являются атомарное уменьшение и проверка целочисленного счетчика, указывающего, сколько потоков владеет мьютексом или ожидает его. Если проверка показывает, что мьютекс не был захвачен другими потоками, никакой дополнительной работы не требуется. В противном случае текущий поток блокируется по событию синхронизации, которое является частью объекта FAST_MUTEX. Освобождение мьютекс влечет за собой атомарное увеличение и проверку счетчика. Если проверка показывает, что ожидающие потоки в данный момент отсутствуют, никакой дополнительной работы не требуется. Но если имеются другие ожидающие потоки, владелец вызывает KeSetEvent для освобождения одного из них.
О ПРЕДОТВРАЩЕНИИ ВЗАИМНЫХ БЛОКИРОВОК------------------------------------------------
Каждый раз, когда в драйвере используются объекты синхронизации (спин-блокировки, быстрые мьютексы и т. д.), будьте начеку и остерегайтесь потенциальных взаимных блокировок. Мы уже | рассмотрели две возможные ситуации с взаимными блокировками: попытку захвата уже удерживаемой спин-блокировки и попытку захвата быстрого мьютекса или события синхронизации с разрешенными АРС. В этой врезке представлена еще более коварная взаимная блокировка, возникающая при использовании в драйвере нескольких объектов синхронизации.
Допустим, имеются два объекта синхронизации, А и В. Неважно, к какому типу они относятся (и даже относятся ли они к одному типу). Далее предположим, что у нас имеются две функции — я назову их Fred и Barney просто для удобства. Функция Fred сначала захватывает объект А, а затем объект В. Функция Barney захватывает объекты в обратном порядке. Потенциальная опасность взаимной блокировки возникает в том случае, если Fred и Barney могут выполняться одновременно или если поток, в котором выполняется одна из этих функций, может быть вытеснен потоком другой функции.
Как вы, вероятно, помните из школьного курса информатики, взаимная блокировка возникает при более или менее одновременном выполнении потоками функций Fred и Barney. Поток Fred захватывает объект А, а поток Barney захватывает объект В. Теперь Fred пытается захватить объект В, но сделать этого не может (объект уже захвачен потоком Barney). В то же время, Barney пытается захватить объект А, но и это невозможно (объект уже принадлежит Fred). Потоки взаимно блокируются друг по другу, ожидая, пока каждый из них освободит необходимый для продолжения работы объект.
Простейший способ предотвращения взаимных блокировок такого рода заключается в том, чтобы объекты А и В захватывались в одном порядке, всегда и везде. Выбранный вами порядок захвата ресурсов называется иерархией блокировки. Существуют и другие схемы, основанные на условных попытках захвата ресурсов в сочетании с циклами отката, но они реализуются гораздо сложнее. При установке режима Deadlock Detection программа Driver Verifier выявляет взаимные блокировки, обусловленные нарушениями иерархии блокировки с участием спин-блокировок, мьютексов ядра и быстрых мьютексов.
В DDK описан еще один примитив синхронизации, не упоминавшийся в этой главе: ERESOURCE. Объекты ERESOURCE широко используются драйверами файловой системы, потому что они поддерживают как общий, так и монопольный захват. Поскольку в драйверах файловой системы часто приходится использовать сложную логику блокировки, Driver Verifier не проверяет иерархию блокировок для объектов ERESOURCE.
Атомарные вычисления
В драйверах WDM также можно вызывать некоторые служебные функции для выполнения вычислений, безопасных в отношении потоков и многопроцессорных сред (табл. 4.8). Эти функции делятся на две категории. Имена функций
Другие синхронизационные примитивы ядра
205
первой категории начинаются с префикса Interlocked; функции выполняют атомарные операции таким образом, что ни один другой поток и процессор не могут вмешаться в их выполнение. Имена функций второй категории начинаются с префикса Exinterlocked, и в них используются спин-блокировки.
Таблица 4.8. Функции для выполнения атомарных математических операций
Функция	Описание
InterlockedCompareExchange	Сравнивает два числа и заменяет значение переменной в случае выполнения условия
Interlocked Decrement	Уменьшает целое число на 1
InterlockedExchange interlockedExchangeAdd Interlockedlncrement	Заменяет значение переменной Суммирует два числа и возвращает их сумму Увеличивает целое число на 1
InterlockedOr	Объединяет биты операцией OR
inrerlockedAnd	Объединяет биты операцией AND
InterlockedXor	Объединяет биты операцией «исключающего OR» (XOR)
DdnterlockedAddLargelnteger Exinterlocked Add LargeStatistic ExInterlockedAddUlong	Прибавляет значение к 64-разрядному целому числу Прибавляет значение к числу типа ULONG Прибавляет значение к числу типа ULONG и возвращает исходное значение
ExInterlockedCompareExchange64	Меняет местами два 64-разрядных значения
Функции InterlockedXrr могут вызываться на любом уровне IRQL, кроме того, они могут обрабатывать перемещаемые данные на уровне PASSIVE-LEVEL, поскольку не требуют спин-блокировки. Хотя функции ExInterlockedXxr могут вызываться на любом уровне IRQL, они работают с целевыми данными на уров-ие DISPATCH_LEVEL и выше, а следовательно, требуют неперемещаемых аргументов. Единственная причина для использования ExInterlockedXxx возникает при работе с переменными данных, при обращениях к которым иногда выполняются операции инкремента/декремента, а иногда целые серии команд. Для серий команд устанавливается явная спин-блокировка, а для простых операций инкремента/декремента используются функции ExInterlockedXrr.
Функции InterlockedXxx
Функция Interlockedlncrement увеличивает длинное целое в памяти на 1 и возвращает значение, полученное в результате инкремента:
_3NG result = Interlockedlncrenient(pLong);
где pLong — адрес переменной с типом LONG (то есть длинной целой). На концептуальном уровне работа этой функции эквивалентна команде С return ++*pLong, во ее реализация обеспечивает потоковую и многопроцессорную безопасность. Функция Interlockedlncrement гарантирует, что целое число будет успешно увеличено даже в том случае, если код другого процессора или других потоков на
206
Глава 4. Синхронизация
том же процессоре одновременно попытается изменить ту же переменную. По самой природе операции функция Interlockedlncrement не может гарантировать, что возвращаемое значение по-прежнему представляет значение переменной, даже на один машинный такт после завершения операции, потому что другие потоки и процессоры могут изменить переменную сразу же после завершения операции атомарного инкремента.
Функция InterlockedDecrement аналогична Interlockedlncrement, но она уменьшает целевую переменную на 1 и возвращает итоговое значение — по аналогии с командой С return -*pLong, но с обеспечением дополнительных мер потоковой и многопроцессорной безопасности.
LONG result = InterlockedDecrement(pLong);
Функция InterlockedCompareExchange используется следующим образом:
LONG target;
LONG result = InterlockedCompareExchangef&target, newval. oldval);
Здесь аргумент target представляет длинное целое число, в котором передается как ввод, так и вывод функции, oldval — ваши предположения о текущем содержимом целевой переменной, a newval — новое значение, которое должно быть занесено в нее в том случае, если ваше предположение оказалось верным. Функция выполняет операцию, эквивалентную следующему фрагменту кода С, но делает это на атомарном уровне, с обеспечением потоковой и многопроцессорной безопасности:
LONG CompareExchangeCPLONG ptarget, LONG newval, LONG oldval)
LONG value = *ptarget;
If (value == oldval)
*ptarget = newval: return value;
Другими словами, функция всегда возвращает предыдущее значение целевой переменной. Кроме того, если предыдущее значение переменной равно oldval, ей присваивается значение newval. Сравнение и замена производятся на атомарном уровне, чтобы замена происходила только в том случае, если ваше предположение относительно предыдущего содержимого оказалось верным.
Аналогичная операция «сравнения с заменой» для указателей выполняется функцией InterlockedCompareExchangePointer. Функция определяется либо как внутренняя функция компилятора (то есть функция, для которой компилятор предоставляет встроенную (inline) реализацию), либо как реальная функция в зависимости от ширины указателей на платформе компиляции и от возможности компилятора генерировать встроенный код.
Последняя функция этой категории, InterlockedExchange, просто использует атомарную операцию для присваивания нового значения целочисленной переменной и возврата предыдущего значения:
LONG value:
LONG oldval = InterlockedExchangeC&value, newval);
Другие синхронизационные примитивы ядра
207
Как вы, вероятно, уже догадались, также существует функция Interlocked-ExchangePointer, которая осуществляет замену по указателям (64- или 32-разряд-ным, в зависимости от платформы). Обязательно преобразуйте тип приемника операции обмена, чтобы предотвратить ошибки компилятора при построении 64-разрядных драйверов:
PIRP Irp = (PIRP) Inter!ockedExchangePointer( (PVOID*) &foo, NULL);
Новые функции InterlockedOr, InterlockedAnd и InterlockedXor появились только в XP DDK. Они также могут использоваться в драйверах, работающих в предыдущих версиях Windows, потому что в действительности они реализованы как внутренние функции компилятора.
Атомарная выборка и сохранение
Многие программисты спрашивают, как выполнять операции выборки и сохранения с данными, при обращениях к которым используются функции Interlocked&x Для получения логически целостного значения переменной, изменяемой с применением атомарных функций, не нужно предпринимать никаких особых мер — при условии, что данные выровнены по естественной границе адресов. При подобном выравнивании данные не могут пересечь границу кэша памяти, а контроллер памяти всегда обновляет блоки памяти в пределах границ кэша на атомарном уровне. Иначе говоря, если кто-то попытается обновить переменную в то время, когда вы читаете ее, то вы получите либо значение до обновления, либо значение после обновления, и никогда — промежуточные состояния.
Впрочем, для операций сохранения данных ответ получается более сложным. Допустим, вы написали следующий код для защиты доступа к общим данным:
if (InterlockedExchangeC&lock, 42) == 0)
{
sharedthing++;
lock = 0;	// == так нельзя
}
Этот код нормально работает на компьютерах Intel х86, где все процессоры ♦ видят» операции записи в память в одном порядке. Тем не менее, на процессорах другого типа могут возникнуть проблемы. Например, какой-нибудь процессор может обнулить переменную lock перед тем, как обновлять память для команды инкремента. Такое поведение позволит двум процессорам одновременно обратиться к sharedthing. Подобные проблемы возникают из-за особенностей параллельного выполнения операций процессорами или специфики контроллера памяти. Соответственно, этот код следует переработать так, чтобы атомарные операции использовались при обоих изменениях lock:
if (InterlockedExchange(&lock. 42) == 0)
{
sharedthing++;
InterlockedExchange(&lock, 0):
208
Глава 4. Синхронизация
Функции ExInterlockedXxx
Перед вызовом каждой из функций ExInterlockedA/tx необходимо создать и инициализировать спин-блокировку. Учтите, что все операнды этих функций должны находиться в неперемещаемой памяти, потому что функции работают с данными на повышенном уровне IRQL.
Функция ExInterlockedAddLargelnteger складывает два 64-разрядных целых числа и возвращает предыдущее значение целевой переменной:
LARGE_INTEGER value, increment;
KSPIN_LOCK spinlock;
LARGE_INTEGER prev = ExInterlockedAddLargeInteger(Svalue,
Increment, &spinlock);
Здесь value — приемник операции сложения и один из операндов, increment — целочисленный операнд, прибавляемый к приемнику, spinlock — ранее инициализированный объект спин-блокировки. Функция возвращает значение приемника перед сложением. Иначе говоря, действие функции эквивалентно следующему фрагменту кода, но с выполнением под защитой спин-блокировки:
_int64 AddLargeInteger(__1nt64* pvalue. _int64 increment)
{
___int64 prev = *pvalue;
*pvalue += Increment;
return prev;
}
Обратите внимание: функция возвращает значение на момент перед сложением. В этом она отличается от функции InterlockedExchange, возвращающей значение после изменения (кроме того, не все компиляторы поддерживают тип данных_____int64, и не все компьютеры могут выполнять 64-разрядное сложение
атомарными командами).
Функция ExInterlockedAddUlong аналогична ExInterlockedAddLargelnteger, но работает с 32-разрядными целыми без знака:
ULONG value, increment;
KSPIN-LOCK spinlock;
ULONG prev = ExInterlockedAddUlong(&value, increment, Sspinlock);
Эта функция, как и предыдущая, возвращает значение приемника на момент перед сложением.
Функция ExInterlockedAddLargeStatistic, как и ExInterlockedAddUlong, суммирует 32-разрядное значение с 64-разрядным:
VOID ExInterlockedAddLargeStatistic(PLARGE_INTEGER Addend, ULONG Increment);
Однако новая функция работает быстрее ExInterlockedAddUlong, потому что ей не нужно возвращать значение Addend на момент перед выполнением операции. Соответственно, ей не нужно применять спин-блокировку для выполнения синхронизации. Впрочем, атомарность, обеспечиваемая функцией, работает только в отношении других сторон, вызывающих эту же функцию. Другими словами,
е синхронизационные примитивы ядра
209
I * и код на одном процессоре вызывает ExInterlockedAddLargeStatistic одновре-I  но с тем, как код на другом процессоре обращается к переменной Addend для Ь' ния или записи, может возникнуть расхождение. Чтобы объяснить, как это К' исходит, я приведу псевдореализацию этой функции для Intel х86 (не реаль-Нш I исходный код):
'V eax, Addend
~ v есх. Increment
ck add [еах]. есх
ck adc [еах+4], О
Этот код работает правильно в отношении увеличения Addend, потому что • анда lock гарантирует атомарность всех операций сложения, а переносы из Д^адших 32 разрядов никогда не теряются. Однако «мгновенное» значение I II-разрядной величины Addend не всегда последовательно, потому что «снимок» > лной 64-разрядной величины может быть сделан между выполнением ADD • ADC. Таким образом, даже при вызове ExInterlockedCompareExchange64 на дру-[ В процессоре может быть получено неверное значение.
Атомарная работа со списками
I Тполнительная система Windows NT поддерживает три группы вспомогатель-х функций для работы со связанными списками с обеспечением потоковой • многопроцессорной безопасности. Эти функции предназначены для работы двусвязными списками, односвязными списками и особой разновидностью од-Iвязных списков — так называемыми S-списками. Обычные (не атомарные) ерании с двусвязными и односвязными списками рассматривались в пре-I лыдущей главе. В завершение этой главы, посвященной синхронизации в драй-| играх WDM, мы рассмотрим использование этих атомарных примитивов син-I тронизации.
Для реализации функциональности очередей FIFO можно воспользоваться дв\ связным списком. Если вам потребуется функциональность стека с потоковой многопроцессорной безопасностью, используйте S-список. В обоих случаях для ..остижения потоковой и многопроцессорной безопасности необходимо создать инициализировать объект спин-блокировки. Однако S-списки могут и не исполь-вать спин-блокировку, так как присутствие порядкового номера позволят ядру ализовать их с применением операций типа атомарного сравнения с заменой.
Вспомогательные функции для выполнения атомарного доступа к разным объ-ктам списков схожи друг с другом, поэтому я упорядочил материал этого раздела о функциональности, а не по типу списка. Сначала я объясню, как инициализи-. уются все три типа списков. Затем мы займемся вставкой элементов во все три разновидности списков, а после этого рассмотрим операции удаления элементов.
Инициализация
Инициализация списков выполняется следующим образом:
LISTJNTRY DoubleHead;
SINGLE_LIST_ENTRY SingleHead:
210
Глава 4. Синхронизация
SLIST_HEADER SL1 stHesd;
Initial 1zeLIstHead(&DoubleHead)
SingleHead.Next = NULL;
ExIn1t1al1zeSL1stHead(&SL1stHead);
He забудьте, что для каждого списка также необходимо создать и инициализировать спин-блокировку. Кроме того, память для заголовков списков и всех элементов, помещаемых в список, должна выделяться из неперемещаемого пула потому что вспомогательные функции обращаются к элементам на повышенном уровне IRQL. Учтите, что спин-блокировка пе используется во время инициализации заголовка списка, потому что до завершения инициализации списка любая конкуренция за доступ к нему не имеет смысла.
Вставка элемента
В двусвязных списках элементы могут вставляться в начало и конец списка, а в односвязных списках и S-списках — только в начало:
PLIST_ENTRY pdElement, pdPrevHead, pdPrevTail;
PSINGLE_LIST_ENTRY psElement, psPrevHead;
PKSPINJ_OCK spinlock;
pdPrevHead = ExInterlockedlnsertHeadLlstC&DoubleHead,
pdElement, spinlock);
pdPrevTail = ExInterlockedlnsertTallLIstC&DoubleHead,
pdElement, spinlock);
psPrevHead = ExInterlockedPushEntryL1st(&S1ngleHead, psElement, spinlock);
psPrevHead - ExInterlockedPushEntrySL1st(&SL1stHead, psElement, spinlock);
Возвращаемые значения представляют собой адреса элементов, ранее находившихся в начале (или в конце) соответствующего списка. Помните, что в этих функциях используются адреса элементов списка, обычно внедряемых в более крупные структуры данных, и для получения адреса внешней структуры необходимо использовать CONTAINING_RECORD.
Удаление элементов
Для удаления элементов в начале списка используются следующие функции:
pdElement = ExInterlockedRemoveHeadList(&DoubleHead, spinlock);
psElement = ExInterlockedPopEntryList(&SingleHead. spinlock);
psElement = ExInterlockedPopEntrySList(&SListHead. spinlock);
'Г'/гие синхронизационные примитивы ядра
211
Если список пуст, функция возвращает NULL. Обязательно проверяйте его, жде чем вызывать макрос CONTAININGJRECORD для получения указателя на сшшою структуру.
Ограничения IRQL
функции. S-списков могут вызываться только при выполнении на уровне DISPATCH_ lEvEL и ниже. Функции ExInterlockedXxr для работы с двусвязными и односвяз-
ми списками могут вызываться на любом уровне IRQL при условии, что все Ьгылки в списке используют вызовы ExInterlockedXxr. Отсутствие ограничений £ ясняется тем, что реализации этих функций запрещают прерывания, что рав-
-льно подъему IRQL на максимально возможный уровень. После запрета прерываний эти функции захватывают указанную вами спин-блокировку. Посколь-I другой код не может перехватить управление на том же процессоре, а другой Ж /.на другом процессоре не может получить спин-блокировку, списки оказыва-г я надежно защищенными.
1ЕЧАНИЕ------------------------------------—-------------------------------
- ментации DDK это правило представлено в излишне ограниченном виде — по крайней мере, _ в ^котормх из функций ExInterlockedXxx. Там говорится, что все вызывающие стороны должны Эвсо ать на одном общем уровне IRQL, меньшем либо равном уровню DIRQL объекта прерывания. К самом деле требования о едином уровне IRQL для всех вызывающих сторон не существует, по-
что функции могут вызываться на любом уровне IRQL. Ограничения <=DIRQL тоже не суще-ет, но также нет и причин, по которым в наших с вами программах требовалось бы поднимать DL выше этого уровня.
ичто не мешает вам использовать вызовы ExInterlockedXxx в одно- и двусвяз-вн списках (но не S-списках) в одних частях кода, а затем использовать неатомарные версии функций (InsertHeadList и т, д.) — нужно лишь соблюдать одно тт- гое правило. Перед тем как использовать неатомарный примитив, захва-Ьте ту же спин-блокировку, которая используется атомарными вызовами. Кроме  . ограничьте доступ к списку кодом, работающим на уровне DISPATCHJ-EVEL ке. Пример:
Работа со списком с применением неатомарных вызовов:
* .ZID Function!О
(
ASSERT(KeGetCurrentIrql() <= DISPATCHLEVEL);
IRQL oldirql;
\eAcquireSpinLock(spin!ock. &oldirql):
InsertHeadListC..
RemoveTailListC...
•eReleaseSpinLockCspinlock, oldirql):
работа co списком с применением атомарных вызовов:
_Z? Function2()
212
Глава 4. Синхронизация
ASSERT(KeGetCurrentIrqlО <== DISPATCH_LEVEL): ExlnterlockedlnsertTai1L1 st(.... spinlock);
}
Первая функция должна выполняться на уровне DISPATCH_LEVEL или ниже, потому что это необходимо для вызова KeAcquireSpinLock. Причина для ограничения IRQL для атомарных вызовов во второй функции состоит в следующем: предположим, Function 1 получает спин-блокировку, готовясь к выполнению операций со списком. При получении спин-блокировки IRQL поднимается до уровня DISPATCH_LEVEL Теперь предположим, что на том же процессоре происходит прерывание с более высоким уровнем IRQL и Function? получает управление для использования одной из функций ExInterlockedXxr. Ядро попытается захватить ту же спин-блокировку, что приводит к взаимной блокировке. Проблема возникла из-за того, что коду, выполняемому на двух разных уровнях IRQL, было разрешено использовать одну спин-блокировку: функция Fu notion 1 работает на уровне DISPATCH-LEVEL, а функция Function?, когда она пытается осуществить рекурсивный захват блокировки, — на уровне HIGH„LEVEL (по крайней мере, в практическом смысле).
Проблемы совместимости с Windows 98/Ме
Кроме ужасной проблемы с функциями KeWaitXxr, описанной в одной из врезок, а также отсутствия функции KeReadStateEvent в ранних системах, следует учитывать следующие проблемы совместимости между Windows 98/Ме, с одной стороны, и Windows 2000/ХР — с другой.
В Windows 98/Ме невозможно ожидание по объекту KTHREAD. Подобные попытки приводят к системным сбоям, потому что объект потока не содержит полей, необходимых для ожидания со стороны VWIN32.
Уровень DISPATCHJJEVEL в драйверах WDM соответствует так называемому времени прерывания (interrupt time) в драйверах VxD. Все обработчики прерываний WDM работают на более высоком уровне IRQL, а это означает, чт-прерывания WDM имеют более высокий приоритет по сравнению с другими, прерываниями. Но если устройство WDM совместно использует прерывание с устройством VxD, оба обработчика работают на уровне DIRQL драйвера WDM
Код драйвера WDM, работающий на уровне PASSIVE-LEVEL, не вытесняется в Windows 98/Ме, если только они не блокируются явно, то есть посредством явного ожидания синхронизационного объекта, либо косвенно, инициируя страничный сбой.
Windows 98/Ме является однопроцессорной операционной системой, поэтому примитивы спин-блокировки всегда ограничиваются простым повышением IRQL Этот факт в сочетании с тем фактом, что неперемещаемый код драйвера не вытесняется системой, означает, что проблемы синхронизации в этой среде встречаются гораздо реже. Следовательно, основную отладку и диагностику следует выполнять в Windows ХР, иначе вы рискуете упустить многие потенциальные проблемы
5 Пакеты запросов ввода/вывода
Операционная система использует структуру данных, известную под названием пакета запроса ввода/вывода, или IRP (I/O Request Packet), для обмена данными с драйверами устройств режима ядра. В этой главе я опишу эту важную структуру данных, а также способы создания таких пакетов, их отправки, обработки и уничтожения. Также будет рассмотрена относительно сложная тема отмены IRP.
Боюсь, эта глава получилась излишне абстрактной, потому что мы еще не рассматривали многие концепции, связанные с конкретными типами пакетов IRP. Возможно, вам стоит бегло просмотреть эту главу и возвращаться к ней по мере «пения последующих глав. Последний крупный раздел этой главы содержит своего рода «поваренную книгу» с заготовками кода для обработки IRP в восьми стандартных сценариях. Чтобы использовать код из «поваренной книги», вам совершенно не обязательно понимать всю теорию, представленную в этой главе.
груктуры данных
В обработке запросов ввода/вывода важнейшую роль играют две структуры: сам пакет запроса ввода/вывода и структура IO_STACK_LOCATION. В этом разделе приводятся описания обеих структур.
Труктура IRP
На рис. 5.1 показана структура данных IRP, как обычно, закрытые поля выделены серым фоном. Далее следует краткое описание важнейших полей.
Поле MdlAddress (PMDL) содержит адрес таблицы дескрипторов памяти (MDL, Memory Descriptor List); таблица описывает буфер пользовательского режима, связанный с запросом. I/O Manager создает таблицу MDL для запросов IRP_MJ_ READ и IRP-MJ-WRITE, если флаги объекта верхнего устройства указывают режим DO_DIRECT_IO. Таблица MDL для выходного буфера, используемого с запросом IRP_MJ_DEVICE_CONTROL, создается при наличии в управляющем коде флагов METHOD_IN„DIRECT или METHOD„OUT_DIRECT. Сама таблица MDL описывает виртуальный буфер пользовательского режима, а также содержит физические адреса заблокированных страниц, содержащих этот буфер. Для обращения к буферу
214
Глава 5. Пакеты запросов ввода/вывода
пользовательского режима драйверу приходится проделывать дополнительную работу (впрочем, весьма небольшую).
UserEvent
Overlay
CancelEoutine
UserBuffer
Tail
Рис. 5.1. Структура пакета запроса ввода/вывода
Поле Flags (ULONG) содержит флаги, которые могут читаться драйвером устройства, но не могут изменяться им напрямую. Ни один из флагов не имеет отношения к драйверам WDM (Windows Driver Model).
Поле Associatedlrp является объединением (union) трех возможных указателей. Указатель, представляющий интерес для типичного драйвера WDM, называется Associatedlrp.SystemBuffer. Он содержит адрес буфера данных в неперемещаемой памяти режима ядра. Для операций IRP_MJ_READ и IRP_MJ_WRITE I/O
Структуры данных
215
Manager создает этот буфер данных, если флага объекта верхнего устройства указывают режим DCLDIRECT_IO. Для операций IRP_MJ_DEVICE_CONTROL I/O Manager создает этот буфер, если того требует код управляющей функции ввода/вывода (см. главу 9). I/O Manager копирует данные, отправленные драйверу кодом пользовательского режима, в этот буфер в процессе создания IRP. К их числу относятся данные, задействованные в вызове WriteFile, или «входные данные» для вызова OeviceloControL При запросах на чтение драйвер устройства заполняет этот буфер данными, I/O Manager позднее копирует его содержимое в буфер пользовательского режима. Для управляющих операций с флагом METHOD-BUFFERED драйвер помещает «выходные данные» в этот буфер, а I/O Manager копирует их в выходной буфер пользовательского режима.
Поле loStatus (IO_STATUS_BLOCK) содержит структуру с двумя полями, задаваемыми драйвером при завершении обработки запроса. В поле loStatus.Status заносится код NTSTATUS, а в поле loStatus.Information типа ULONG_PTR заносится информационное значение, точное содержимое которого зависит от типа IRP и статуса завершения. В поле Information часто сохраняется общее количество байтов, переданных в ходе операции (например, IRP_MJ_READ). Некоторые за-лоосы PnP (Plug and Play) хранят в этом поле указатель на структуру, которая ы жет рассматриваться как ответ на запрос.
Поле RequestorMode содержит одну из констант перечисляемого типа, UserMode или KernelMode, в зависимости от того, откуда поступил исходный запрос вво-вывода. Драйверы иногда анализируют содержимое этого поля, чтобы узнать, можно ли доверять некоторым параметрам.
Поле PendingReturned (BOOLEAN) содержит осмысленные данные при обработке завершения, оно указывает, вернула ли следующая диспетчерская функция нижнего уровня код STATUS_PENDING. В этой главе весьма подробно рассказано, как использовать этот флаг.
Поле Cancel (BOOLEAN) равно TRUE, если для отмены запроса была вызвана Функция loCancellrp, или FALSE, если функция (еще) не вызывалась. Отмена IRP — относительно сложная тема, которая будет подробно рассмотрена позднее в этой главе (см. раздел «Отмена запросов ввода/вывода»).
Поле Cancellrql (KIRQL) содержит уровень запроса прерывания (IRQL), на ко-т »ром была захвачена специальная спин-блокировка отмены. Это поле исполь-у.ется в функции отмены при освобождении спин-блокировки.
Поле CancelRoutine (PDRIVER_CANCEL) содержит адрес функции отмены IRP вашего драйвера. Вместо прямой модификации этого поля можно воспользоваться функцией loSetCancelRoutine.
Поле UserBuffer (PVOID) содержит виртуальный адрес пользовательского режима зля выходного буфера запроса IRP_MJ_DEVICE_CONTROL, для которого в управляющем коде задан режим METHOD_NEITHER. Кроме того, поле используется для хранения виртуального адреса пользовательского режима буфера запросов чтения/ записи, но драйверы обычно указывают один из флагов устройства DO_BUFFERED_ •О или DO_DIRECT_IO, поэтому у них нет необходимости обращаться к этому полю хтя чтения или записи. При обработке управляющих операций METHOD_NEITHER драйвер может использовать этот адрес для создания собственной версии MDL.
216
Глава 5. Пакеты запросов ввода/вывода
Поле Tail.Overlay представляет собой структуру внутри объединения, структура содержит несколько полей, которые могут оказаться полезными для драйверов WDM. Строение объединения Tail показано на рис. 5.2. На рисунке слева направо представлены альтернативные члены объединений, а по вертикали изображено строение каждой из альтернатив. Tail.Overlay.DeviceQueueEntry (KDEVICE_ QUEUE_ENTRY) и Tai [.Overlay. DriverContext (PVOID[4]) — альтернативы в безымянном объединении, входящем в Tail.Overlay. I/O Manager использует DeviceQueueEntry в качестве связующего поля в стандартных очередях запросов к устройству. Функции loCsqXxr используют последний элемент массива DriverContext. В то время, когда пакет IRP не находится в очереди, использующей это поле, и когда вы являетесь владельцем IRP, используйте четыре указателя в DriverContext по своему усмотрению. Поле Tail.Overlay.ListEntry (LIST_ENTRY) может использоваться в качестве связующего поля IRP в любых приватных очередях, которые вы сочтете нужным реализовать.
Tail.Overlay
Рис. 5.2. Строение объединения Tail в пакетах IRP
Структуры данных
217
Поля CurrentLocation (CHAR) и Tail.Overlay.CurrentStackLocation (PIO_STACK_LOCATION) не документированы для использования в драйверах, потому что вместо них могут использоваться вспомогательные функции — такие как loGetCurrentlrpStackLocation. Тем не менее, в процессе отладки бывает полезно знать, что поле CurrentLocation содержит индекс текущего элемента в стеке ввода/вывода, a CurrentStackLocation — указатель на него.
1тек ввода/вывода
Каждый раз, когда программа режима ядра создает пакет IRP, она также создает связанный с ним массив структур IO_STACK_LOCATION: стек содержит по одному элементу для каждого драйвера, который будет обрабатывать IRP, и иногда еще один дополнительный элемент, используемый создателем IRP (рис. 5.3). Элемент стека содержит коды типов и информацию о параметрах IRP, а также адрес функции завершения. Строение стека изображено на рис. 5.4.
Рис- 5-3. Соответствие между драйверами и стеками ввода/вывода
ММЕЧАНИЕ-------------------------------------------------------------------
Механика создания IRP будет описана чуть позже. А пока будет полезно знать, что поле StackSize объекта DEVICE-OBJECT указывает, сколько элементов должно быть зарезервировано для пакетов IRP, отправляемых этому драйверу устройства.
Поле MajorFunction (UCHAR) содержит основной код функции, связанный с IRP. Код представляет собой значение (например, IRP_MJ_READ), соответствующее одному из элементов таблицы диспетчерских функций MajorFunction объекта драйвера. Поскольку код находится в элементе стека ввода/вывода конкретного драйвера, может случиться так, что IRP начнет свое существование как запрос IRP_MJ_READ, например, а затем преобразуется в нечто иное в процессе перемещения по стеку
218
Глава 5. Пакеты запросов ввода/вывс^а
драйверов. В главе 12 я приведу примеры того, как драйвер USB превращает .- прос на чтение или запись во внутреннюю управляющую операцию для доставки запроса драйверу тины USB.
Рис. 5.4. Структура данных элемента стека ввода/вывода
Поле MinorFunction (UCHAR) содержит дополнительный код функции, который уточняет смысл пакетов IRP, принадлежащих к некоторым общим категориям. Например, запросы IRP_MJ_PNP подразделяются па десяток с лишним подтипов при помощи дополнительных кодов функций (IRP_MN_START_DEVICE, IRP_MN_ REMOVE-DEVICE и т. д.).
Поле Parameters представляет собой объединение субструктур, по одной для каждого типа запроса, обладающего определенными параметрами. К их числу, например, относятся субструктуры Create (для запросов IRP_MUCREATE), Reac (для запросов IRP_MJ_READ) и StartDevice (для подтипа IRP_MN_START_DEVICE запросов IRPJMJ_PNP).
Поле Deviceobject (PDEVICE_OBJECT) содержит адрес объекта устройства, соответствующего данной позиции стека. Поле заполняется вызовом функции loCallDriver.
Поле FileObject (PFILE_OBJECT) содержит адрес объекта файла режима ядра, которому направляется IRP. Драйверы часто используют указатель FileObject для связывания пакетов IRP в очереди с запросом на отмену (в форме IRP_MJJ2LEANUP) всех находящихся в очереди 1RP при подготовке к закрытию объекта файла.
Поле CompletionRoutine (PIO_COMPLETION_ROUTINE) содержит адрес функции завершения ввода/вывода — эта функция устанавливается драйвером, который находится в стеке выше драйвера, соответствующего данному элементу стека. Значение этого поля никогда не задается напрямую — вместо этого следует
стандартная модель» обработки IRP
219
<звать функцию loSetCompietionRoutine, которая умеет обращаться к элементу
под позицией, принадлежащей вашему
драйверу. Драйвер самого нижнего
стека
уровня в иерархии драйверов устройства не нуждается в функции завершения, <?тому что он обязан завершить запрос. Однако создателю запроса иногда бывает нужна функция завершения запроса, хотя обычно он не имеет собственного элемента в стеке. Именно по этой причине каждый уровень иерархии хранит свой указатель на функцию завершения в следующем элементе стека.
Поле Context (PVOID) содержит произвольный контекст, передаваемый в качестве аргумента функции завершения. Значение никогда не задается напря-мую — оно определяется автоматически по одному из аргументов функции loSet-
CompletionRoutine.
Стандартная модель» обработки IRP
В физике частиц существует своя «стандартная модель» Вселенной, есть она и в WDM. На рис. 5.5 показана типичная последовательность смены владельцев пакета IRP на различных стадиях его жизненного цикла. Не каждый тип пакета IRP проходит через все показанные фазы, причем некоторые фазы могут изменяться или исключаться в зависимости от типа устройства и типа IRP. Но, несмотря на все возможное разнообразие, эта схема станет хорошей отправной точкой для дальнейшего обсуждения.
Рис. 5.5. «Стандартная модель» обработки IRP
В режиме проверки ввода/вывода (I/O Verification) программа Driver Verifier выполняет ряд базовых тестов, относящихся к обработке IRP. Режим расширенной проверки (Extended I/O Verification) включает гораздо большее количество таких тестов, поэтому я не стал размещать на полях флаг Driver Verifier для каждого
220
Глава 5. Пакеты запросов ввода/вывода
возможного теста. Как правило, если в DDK или в тексте книги вам что-то не рекомендуется делать, в Driver Verifier на этот случай предусмотрена соответствующая проверка.
Создание IRP
Жизненный цикл пакета IRP начинается с его создания вызовом функции I/O Manager. На рис. 5.5 я использую обозначение I/O Manager, словно существует единый системный компонент, ответственный за создание IRP. В действительности было бы правильнее сказать, что кто-тпо создает IRP. Например, ваш драйвер будет время от времени создавать пакеты IRP, принимая на себя роль исходного владельца этих пакетов.
Следующие четыре функции создают новые пакеты IRP:
О loBuildAsynchronousFsdRequest — создает пакет IRP, завершения которого вы не собираетесь дожидаться. Эта и следующая функция годятся только для создания определенных типов IRP.
О loBuildSynchronousFsdRequest ~ создает пакет IRP, завершения которого вы намерены дождаться.
О loBuildDeviceloControlRequest — создает синхронный запрос IRP_MJ_DEVICE_ CONTROL или IRP_MJ_INTERNAL_DEVICE__CONTROL.
О loAllocatelrp — создает асинхронный пакет IRP любого типа.
Fsd в именах первых двух функций означает «драйвер файловой системы» (File System Driver). Тем не менее, любой драйвер может вызывать эти функции для создания пакетов IRP, предназначенных для любого другого драйвера. В DDK также документирована функция lolMakeAssociatedlrp для создания пакетов IRP, ассоциированных с другими IRP. Драйверы WDM не должны вызывать эту функцию. Кроме того, завершение ассоциированных IRP все равно неверно работает в Microsoft Windows 98/Ме.
ПРИМЕЧАНИЕ----------------------------------------------------------------------
В этой главе я использую термины «синхронные IRP» и «асинхронные IRP», потому что они используются в DDK. Квалифицированные разработчики в Microsoft считают, что вместо них следовало бы использовать термины «потоковые» и «не-потоковые», потому что они лучше отражают особенности использования этих двух типов IRP драйверами. Как вскоре станет ясно, синхронные (потоковые) IRP используются в фиксированных (то есть не произвольных) потоках, которые могут блокироваться в ожидании завершения IRP. Во всех остальных случаях используются асинхронные IRP.
Создание синхронных пакетов IRP
Выбор функции и определение дополнительной инициализации, которую необходимо выполнить с IRP, — дело весьма нетривиальное. Функции loBuildSynchronousFsdRequest и loBuildDeviceloControlRequest создают так называемый асинхронный IRP. I/O Manager считает, что синхронный IRP принадлежит потоку, в контексте которого он был создан. Концепция принадлежности имеет ряд следствий: О При завершении потока I/O Manager автоматически отменяет все незавершенные синхронные IRP, принадлежащие этому потоку.
«Стандартная модель» обработки IRP
221
Э Поскольку синхронные IRP принадлежат тому потоку, в котором он был создан, не создавайте их в произвольных потоках — в случае завершения потока I/O Manager отменит IRP, а это, конечно, недопустимо.
Э После вызова loCompleteRequest I/O Manager автоматически деинициализирует синхронный IRP и выдает событие, которое вы должны предоставить.
Э Вы должны проследить за тем, чтобы объект события продолжал существовать на момент его выдачи I/O Manager.
За примером кода с использованием синхронного IRP обращайтесь к сценарию 6 в конце главы.
Обе функции должны вызываться только на уровне PASSIVE-LEVEL. В частности, вызовы не должны происходить на уровне APC_LEVEL (скажем, в результате захвата быстрого мьютекса), потому что I/O Manager в этом случае не сможет доставить специальный вызов АСР режима ядра, осуществляющий всю обработку завершения. Другими словами, следующий фрагмент недопустим:
PIRP Irp = loBuildSynchronousFsdRequestt...):
ExAcqui reFastMutex(...);
NTSTATUS status = loCalIDrlverC...):
if (status == STATUS_PENDING)
KeWa1tForS1ngleObject(...): // <== так нельзя
ExReleaseFastMutex(,..):
Проблема заключается в том, что вызов KeWaitForSingleObject приведет к взаимной блокировке: когда IRP завершится, loCompleteRequest запланирует в этом потоке вызов АРС. Функция АРС, если бы она могла выполниться, инициировала бы событие. Но так как выполнение уже ведется на уровне APCJ-EVEL, механизм АРС отработать не может.
Если вам потребуется синхронизировать пакеты IRP, отправленные другому драйверу, рассмотрите следующие альтернативы:
Э Используйте обычный мьютекс ядра вместо быстрого мьютекса. Обычный мьютекс оставляет IRQL на уровне PASSIVE-LEVEL и не запрещает специальные АРС режима ядра.
Э Используйте KeEnterCriticalRegion для подавления всех АРС, кроме специальных АРС режима ядра, а затем воспользуйтесь функцией ExAcquireFastMutexUnsafe для захвата мьютекса. Это решение не работает в исходной версии Windows 98, потому что функция KeEnterCriticalRegion в ней не поддерживается. На всех последующих платформах WDM оно успешно работает.
Э Используйте асинхронный пакет IRP и инициируйте событие в функции завершения. Пример кода приводится в сценарии 8 в конце главы.
И последнее, о чем необходимо помнить при вызове двух синхронных функций IRP: они не позволяют просто создать IRP произвольного типа. За подробностями обращайтесь к табл. 5.1. Для создания других типов синхронных IRP существует стандартный прием: запросите тип IRP_MJ_SHUTDOWN, не имеющий параметров, а затем измените код MajorFunction в первом элементе стека.
222
Глава 5. Пакеты запросов ввода/вывода
Таблица 5.1. Типы синхронных IRP
Функция	Типы создаваемых IRP
loBuildSynchronousFsdRequest	IRP_MJ_READ IRP_MJ_WRITE IRP_MJ_FLUSH_BUFFERS IRPJ4J_SHUTDOWN IRPJ4J-PNP IRP„MJ_POWER (но только для IRP J4N_POWER_SEQUENCE)
loBuildDeviceloControlRequest	IRP_MJ_DEVICE„CONTROL IRP_MJ_INTERNAL„DEVICE_CONTROL
Создание асинхронных пакетов IRP
Две другие функции — loBuildAsynchronousFsdRequest и loAflocatelrp — предназначены для создания асинхронных IRP. Асинхронные IRP не принадлежат создавшему их потоку, I/O Manager не планирует АРС и не выполняет их деинициализацию при завершении IRP. Соответственно:
О При завершении потока I/O Manager не пытается отменить все асинхронные IRP, созданные в этом потоке.
О Асинхронные IRP могут создаваться как в произвольных, так и в фиксированных потоках.
О Поскольку I/O Manager не выполняет зачистку при завершении IRP, вы должны предоставить функцию завершения, которая освободит буферы и вызовет loFreelrp для освобождения памяти, используемой IRP.
О I/O Manager не отменяет асинхронные IRP автоматически, поэтому, возможно, вам придется предоставить код отмены, когда надобность в выполнении этих операций отпадет.
О Поскольку вы не собираетесь дожидаться завершения асинхронных IRP, такие IRP можно создавать и отправлять на уровне IRQL<=DISPATCH_LEVEL (конечно, при условии, что драйвер, которому отправляется IRP, способен обработать его на повышенном уровне IRQL — проверьте спецификацию драйвера!). Более того, создание и отправка асинхронных IRP возможны при захвате быстрого мьютекса.
В табл. 5.2 перечислены все типы IRP, создаваемых двумя асинхронными функциями. Учтите, что loBuildSynchronousFsdRequest и loBuildAsynchronousFsdRequest поддерживают одни и те же основные коды IRP.
Таблица 5.2. Типы асинхронных IRP
Функция	Типы создаваемых XRP
loBuildAsynchronousFsdRequest IRP_MJ_READ
IRPJMJ.WRITE
IRP MJ FLUSH BUFFERS
-Стандартная модель» обработки IRP
223
Функция	Типы создаваемых IRP
	IRP_MJ_SHUTDOWN IRP_MJ„PNP IRP_MJ_POWER (но только для IRP_MN_POWER„SEQUENCE)
loAllocatelrp	Любые (но вы должны инициализировать поле MajorFunction в первом элементе стека)
Примеры кода работы с асинхронными IRP приведены в сценариях 5 и 8 в конце главы.
Передача пакета диспетчерской функции
После создания IRP можно вызвать функцию loGetNextlrpStackLocation для получения указателя на первый элемент стека. Далее вы инициализируете только первый элемент. Если пакет IRP был создан функцией loAllocatelrp, необходимо заполнить как минимум поле MajorFunction. Если вы использовали другие три функции создания IRP, возможно, I/O Manager уже выполнил необходимую инициализацию, тогда этот этап иногда можно пропустить (в зависимости от правил для данного типа IRP). После инициализации стека пакет IRP посылается драйверу устройства функцией loCallDriver:
PDEVICE_OBJECT DevIceObject; // <== получено от внешнего источника PIO-STACK-LOCAHON stack = loGetNextlrpStackLocation(Irp); stack->MajorFunct1on = IRP_MJ_Xxx:
<прочая инициализация ”стека">
NTSTATUS status = loCal1Driver(DevIceObject. Irp);
Первый аргумент loCallDriver содержит адрес объекта устройства, каким-то бразом полученный вами из внешнего источника. Пакеты IRP часто посылают-чЯ драйверу, находящемуся под вашим драйвером в стеке РпР. В этом случае 3evice0bject в этом фрагменте содержит значение LowerDeviceObject, сохраненное в расширении устройства после вызова loAttachDeviceToDeviceStack. Далее я опиату ряд других стандартных способов идентификации объекта устройства.
I/O Manager инициализирует указатель на элемент стека в IRP для элемента. находящегося за 1 позицию до фактической. Поскольку стек ввода/вывода представляет собой массив структур IO_STACK__LOCATION, можно считать, что указатель инициализируется ссылкой на «минус первый» элемент, который на самом деле не существует (в действительности стек «растет» от старших адресов к младшим, но эта подробность не должна затемнять ту концепцию, которую я пытаюсь изложить). Таким образом, если мы желаем инициализировать .первый элемент стека, нужно запросить «следующий» элемент.
Что делает loCallDriver
Функцию loCallDriver можно представить себе в следующем виде (поспешу добавить, что это псевдореализация, а не фрагмент исходного кода):
NTSTATUS local 1Driver(PDEVICE_OBJECT DevIceObject. PIRP Irp)
224
Глава 5. Пакеты запросов ввода/вывода
loSetNextlrpStackLocatlon(Irp);
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
stack->Dev1ce0bject = Deviceobject;
ULONG fen = stack->MajorFunct1on;
PDRIVER_OBJECT driver = Device0bject->Dr1verObject:
return (*dr1ver->MajorFunct1on[fcn])(Dev1ce0bject, Irp);
}
Как видно из листинга, loCallDriver просто перемещает указатель стека и вызывает соответствующую диспетчерскую функцию драйвера для целевого объекта устройства. Функция возвращает код состояния, полученный от диспетчерской функции. Иногда я вижу на форумах сообщения, в которых люди приписывают loCallDriver ту или иную неудачу («loCallDriver возвращает код ошибки для моего IRP...»). Но как видите, настоящим виновником является диспетчерская функция другого драйвера.
Поиск объектов устройств
Кроме вызова loAttachDeviceToDeviceStack, драйверы мохут находить объекты устройств как минимум двумя способами. В этом разделе я расскажу о функциях loGetDeviceObjectPointer и loGetAttachedDeviceReference.
loGetDeviceObjectPointer
Если имя объекта известно, вызовите функцию loGetDeviceObjectPointer:
PUNICODE_STRING devname; // <== получено из внешнего источника
ACCESS_MASK access; // <== подробнее об этом позднее
PDEVICE-OBJECT Deviceobject;
PFILE-OBJECT FileObject:
NTSTATUS status;
ASSERT(KeGetCurrentIrql() == PASSIVE-LEVEL);
status = IoGetDev1ce0bjectPo1nter(devname, access,
&FileObject, &Dev1ce0bject);
Функция возвращает два указателя: на объекты FILE_OBJECT и DEVICE_OBJECT
Чтобы предотвратить атаки типа повышения уровня привилегий, выберите самый ограниченный вариант доступа^отвечающий вашим потребностям. Например, если вы собираетесь ограничиться чтением данных, укажите доступ FILE_READ__DATA.
Создавая IRP для приемника, обнаруженного этим способом, следует задать указатель FileObject в первом элементе стека. Более того, будет неплохо держать дополнительную ссылку на объект файла до возврата из loCallDriver. Следующий фрагмент демонстрирует обе идеи:
PIRP Irp = IoXxx(...);
PIO_STACK_LOCATION stack = loGetNextlrpStackLocation(Irp):
ObReferenceObj ect(Fl 1eObj ect);
stack->F11eObject = Fl 1 eObject:<etc.>
loCallDriver(Deviceobject, Irp);
ObDereferenceObject(Fl 1 eObject);
Стандартная модель» обработки IRP
225
Указатель на объект файла сохраняется в каждом элементе стека из-за того, что целевой драйвер может использовать поля объекта файла для хранения информации уровня манипулятора. Причина, по которой вы создаете дополнительную ссылку на объект файла, заключается в том, что код, находящийся в другой части драйвера, может уничтожить ссылку на объект файла для освобождения целевого устройства (см. далее). Если этот код будет выполнен до возврата из диспетчерской функции целевого драйвера, драйвер может быть удален из памяти до возврата управления из его диспетчерской функции. Лишняя ссылка предотвращает этот нежелательный результат.
4МЕЧАНИЕ-------------------------------------------------------------------------
Основным источником проблем «преждевременной выгрузки», упоминаемой в тексте, является возможность оперативного отключения устройств в среде Plug and Play. Эта проблема будет гораздо более подробно рассмотрена в следующей главе. А пока следует сделать вывод, что вы сами обязаны позаботиться о предотвращении отправки IRP драйверам, отсутствующим в памяти, и помешать PnP Manager выгрузить драйвер, который все еще обрабатывает отправленный ему IRP. Один из способов реализации показан в тексте: заключите вызов loCallDriver между операциями создания/ уничтожения дополнительной ссылки на объект файла, возвращаемый loGetDeviceObjectPointer. Вероятно, в большинстве драйверов лишняя ссылка потребуется только при отправке асинхронных IRP. В этом случае код, уничтожающий ссылку на объект файла, обычно находится в другой части драйвера, работающей асинхронно с вызовом loCallDriver, — скажем, в функции завершения, которую необходимо установить для асинхронного IRP. При отправке синхронных IRP драйвер с гораздо большей вероятностью реализуется таким образом, что ссылка на объект файла не будет уничтожена вплоть до завершения IRP.
Когда надобность в объекте устройства отпадает, освободите ссылку на него:
loDereferenceObject(FileObject);
После этого вызова указатели на объект файла или устройства использоваться не должны.
Чтобы найти два возвращаемых указателя, функция loGetDeviceObjectPointer выполняет ряд действий:
1.	Для именованного объекта устройства открывается манипулятор режима ядра при помощи функции ZwOpenFile. Во внутренней реализации Object Manager создает объект файла и отправляет целевому устройству запрос IRP„MJ_CREATE. ZwOpenFile возвращает манипулятор файла.
2.	Далее loGetDeviceObjectPointer вызывает функцию ObReferenceObjectByHandle для получения адреса объекта FILE_OBJECT, представленного этим манипулятором. Этот адрес становится возвращаемым значением FileObject.
3.	Функция loGetRelatedDeviceObject вызывается для получения адреса объекта DEVICE_OBJECT, на который ссылается FILE_OBJECT. Этот адрес становится возвращаемым значением Deviceobject.
4.	Манипулятор закрывается вызовом ZwClose.
Ссылка на объект файла, захватываемая loGetDeviceObjectPointer, также, фактически, фиксирует в памяти и объект устройства. Освобождение этой ссылки косвенно приводит к освобождению объекта устройства.
226
Глава 5. Пакеты запросов ввода/вывода
ИМЕНА ОБЪЕКТОВ УСТРОЙСТВ —--------------------------------------------------------------------
Чтобы вы могли использовать функцию loGetDeviceObjectPointer, драйвер в стеке устройства, с которым вы хотите связаться, должен был присвоить имя объекту устройства (см. главу 2). Напомни что драйвер может задать имя в папке \Device при вызове loCreateDevice, а также создать одг, или несколько символических ссылок в папке \DosDevices. Если вы знаете имя объекта устройства иъ* одной из его символических ссылок, их можно использовать при вызове loGetDeviceObjectPointe' Вместо назначения имени объекту устройства функциональный драйвер целевого устройства та«-же может зарегистрировать интерфейс устройства. Код пользовательского режима для перечисли ния экземпляров зарегистрированных интерфейсов приводился в главе 2. Эквивалент этого ко^а для режима ядра будет представлен в главе 6, при обсуждении Plug and Play. Из всего сказанное можно сделать вывод, что вы можете получить имена символических ссылок для всех устройств поддерживающих конкретный интерфейс. После этого ценой небольших дополнительных усилий находится объект устройства.
После знакомства с принципами работы loGetDeviceObjectPointer становитс,-. ясно, почему вызов этой функции иногда завершается неудачей с кодом STATUS^ ACCESS_DENIED, даже если все было сделано правильно. Если целевой драйве: реализует политику «только одного манипулятора» и этот манипулятор оказывается открытым, запрос IRP_MJ_CREATE завершается неудачей. В свою очередь это приводит к сбою при вызове ZwOpenFile. Например, этого результата можнг ожидать при попытке получения объекта устройства для уже открытого устройства SmartCard или последовательного порта.
Иногда разработчики драйверов решают, что два указателя на то, что, фактически, является одним объектом, -- это излишество, и освобождают объект файла сразу же после вызова ToGetDeviceObjectPointer:
status = IoGetDeviceObjectPointer(...);
ObReferenceObject(Devi ceOb j ect);
ObDereferenceObject(Fi1 eObject):
Захват ссылки на объект устройства фиксирует его в памяти до момента последующего освобождения ссылки. Освобождение объекта файла позволяет I/O Manager немедленно удалить его.
Немедленное освобождение объекта файла может быть приемлемым или нет. в зависимости от целевого драйвера. Прежде чем выбирать это решение, учтите следующие тонкости:
О Освобождение ссылки на объект файла приводит к тому, что I/O Manager немедленно отправляет целевому драйверу запрос IRP_MJ_CLEANUP.
О Пакеты IRP, поставленные целевым драйвером в очередь, перестают ассоциироваться с объектом файла. Когда ссылка на объект драйвера будет в конечном итоге освобождена, вероятно, целевой драйвер не сможет отменить никакие из отправленных ему IRP, остающиеся в очереди.
О Во многих ситуациях I/O Manager также отправляет целевому драйверу запрос IRP_MJ_CLOSE (если открыть дисковый файл, скорее всего, использование системного кэша драйвером файловой системы приведет к тому, что обработка IRP_MJ_CLOSE будет отложена). Многие драйверы, включая стандартный драйвер последовательных портов, после этого отказываются обрабатывать отправленные им IRP.
«Стандартная модель» обработки IRP
227
РИМЕЧАНИЕ---------------------------------------------------------------------
Я не рекомендую пользоваться старой функцией loAttachDevice, которая напоминает искусственный гибрид loGetDeviceObjectPointer и loAttachDeviceToDeviceStack. Эта функция содержит внутренний вызов ZwClose после присоединения к объекту устройства. Ваш драйвер получает сгенерированный запрос IRP_MJ__CLOSE. Для обеспечения правильной обработки IRP необходимо вызвать loAttachDevice таким образом, чтобы диспетчерская функция имела доступ к области памяти, ассоциированной с выходным указателем DEVICE_OBJECT. Но оказывается, функция loAttachDevice устанавливает выходной указатель перед вызовом ZwClose и рассчитывает на то, что он будет использован вами для пересылки IRP_MJ_CLOSE целевому устройству. За многие годы практического программирования я впервые встречаю подобную ситуацию: вы обязаны использовать возвращаемое значение функции до того, как эта функция вернет управление.
ToGetAttachedDeviceReference
Чтобы отправить IRP всем драйверам в вашем стеке РпР, используйте функцию loGetAttachedDeviceReference:
PDEVICEJJBJECT tdo = loGetAttachedDevlceReference(fdo):
ObDereferenceObject(tdo):
Функция возвращает адрес верхнего объекта устройства в стеке и захватывает ссылку на этот объект. Из-за удерживаемой ссылки вы можете быть уверены в том, что указатель останется действительным до освобождения ссылки. Как упоминалось ранее, вы также можете создать дополнительную ссылку на верхний объект устройства до того момента, как функция loCallDriver вернет управление.
Обязанности диспетчерской функции
Типичная диспетчерская функция IRP выглядит примерно так:
NTSTATUS D1spatchZxx(P0EVICE_0BJECT fdo. PIRP Irp)
{
PIO_STACK_LOCATION stack = ToGetCurrertlrpStackLocatjon(Irp);	// 1
PDEVICE-EXTENSION pdx =	//2
(PDEVICE_EXTENSION) dev1ce->Dev1ceExtension;	// 3
return SIATUSJfxx;
}
1.	Как правило, для определения параметров или дополнительного кода функции приходится обращаться к текущему элементу стека.
2.	Также обычно требуется доступ для обращения к расширению устройства, созданному и инициализированному во время выполнения AddDevice.
3.	Диспетчерская функция возвращает некоторый код NTSTATUS функции loCallDriver, а последняя передает его своей вызывающей стороне.
В том месте, где в прототипе находится многоточие, диспетчерская функция должна выбрать один из трех вариантов действий: завершить запрос немедленно, передать его драйверу более низкого уровня в том же стеке или поставить запрос в очередь для последующей обработки другими функциями драйвера.
228
Глава 5. Пакеты запросов ввода/вывода
Завершение IRP
Каждый пакет IRP рано или поздно должен быть завершен. Завершение IRP в диспетчерской функции производится в следующих случаях:
О если ошибочность запроса совершенно очевидна (например, запрос на перемотку принтера или извлечение клавиатуры), диспетчерская функция должна отклонить запрос, завершив его с подходящим кодом состояния;
О если запрос направлен на получение информации, которую диспетчерская функция может легко предоставить (скажем, управляющий запрос на получение номера версии драйвера), диспетчерская функция предоставляет ответ и завершает запрос с успешным кодом.
Формально завершение IRP сводится к заполнению полей Status и Information в блоке IRP loStatus и вызову loCompleteRequest. Значение Status представляет собой один из кодов, определяемых в виде констант в заголовочном файле DDK NTSTATUS.Н. Сокращенный список кодов состояния для стандартных ситуаций приведен в табл. 5.3. Значение Information зависит от типа завершаемого IRP и от того, как он завершается (успех или неудача). Как правило, при неудачном завершении IRP (то есть завершении с некоторым кодом ошибки) в поле Information заносится значение 0. При успешном завершении IRP, связанных с пересылкой данных, в поле Information обычно заносится количество переданных байтов.
Таблица 5.3. Часто используемые коды NTSTATUS
Код	Описание
STATUS-SUCCESS STATUS-UNSUCCESSFUL	Нормальное завершение Запрос завершился неудачно, но ни один код состояния не позволяет точно описать причину
STATUS—NOT_IMPLEMENTED STATUS_INVALID_HANDLE	Функция не была реализована Для выполнения операции был передан недействительный манипулятор
STATUS_INVALID_PARAMETER STATUS_INVALID_DEVICE_REQUEST STATUS_END_OF_FILE STATUS-DELETE_PENDING STATUS-INSUFFICIENT_RESOURCES	Ошибочное значение параметра Запрос недействителен для данного устройства Достигнут маркер конца файла Устройство находится в процессе отключения от системы Недостаточно системных ресурсов (обычно памяти) для выполнения операции
ПРИМЕЧАНИЕ------------------------------------------------------------------------------
Обязательно обратитесь к документации DDK за правильным значением loStatus.Information для того IRP, с которым вы работаете. Скажем, в некоторых разновидностях IRPJ4J_PNP это поле используется как указатель на структуру данных, за освобождение которой отвечает РпР Manager. Если при неудачном завершении запроса содержимое этого поля будет заменено нулем, это приведет к утечке ресурсов.
«Стандартная модель» обработки IRP
229
Завершение запроса принадлежит к числу самых частых операций, я счел полезным определить для нее небольшую вспомогательную функцию:
NTSTATUS CompleteRequest(PIRP Irp, NTSTATUS status,
ULONG_PTR Information)
{
Irp->IoStatus.Status = status;
Irp->IoStatus.Information = Information; loCompleteRequest(Irp, IO_NO_INCREMENT); return status;
}
Функция определена таким образом, что она возвращает код состояния, переданный во втором аргументе. Дело в том, что я не люблю вводить лишние символы: возвращаемое значение позволяет мне использовать эту функцию во всех случаях, когда я хочу завершить запрос, а затем немедленно вернуть код состояния. Пример:
NTSTATUS DlspatchControl(PDEVICE_OBJECT fdo, PIRP Irp)
{
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
ULONG code = stack->Parameters.DeviceloControlHoControl Code;
If (code == lOCTLJOASTERJOGUS)
return CompleteRequestdrp. STATUS_INVALID_DEVICE_REQUEST. 0):
}
Возможно, вы обратили внимание на то, что аргумент Information функции CompleteRequest объявлен с типом ULONG_PTR. Другими словами, это значение может быть либо величиной ULONG, либо указателем (потенциально 64-разрядным) на что-либо.
При вызове loCompleteRequest передается величина приращения приоритета, применяемая ко всем потокам, ожидающим завершения этого запроса. Как правило, приращение выбирается в зависимости от типа устройства, об этом нетрудно зогадаться по именам констант из табл. 5.4. Регулировка приоритета улучшает производительность потоков, часто ожидающих завершения операций ввода/вывода. События, находящиеся в прямой зависимости от конечного пользователя (такие как операции с клавиатурой и мышью), приводят к более заметному повышению приоритета, чтобы предпочтение отдавалось интерактивным задачам. Таким образом, при выборе приращения желательно действовать обдуманно. Например, не используйте IO__SOUND_INCREMENT для абсолютно всех операций, завершаемых звуковой картой, — для управляющих запросов на получение версии драйвера столь радикальное повышение приоритета попросту не нужно.
Кстати, не завершайте IRP со специальным кодом состояния STATUS-PENDING. Диспетчерские функции часто возвращают STATUS-PENDING, но вы никогда не должны заносить это значение в loStatus.Status. Для надежности отладочная версия loCompleteRequest, встречая STATUS-PENDING в итоговом состоянии, генерирует
230
Глава 5. Пакеты запросов ввода/вывозг
нарушение ASSERT. Многие программисты также ошибочно используют знач- -ние -1, которое вообще не имеет смысла как код NTSTATUS. Для выявления эт- ? ошибки в отладочной версии также присутствует директива ASSERT. В обогт случаях Driver Verifier пожалуется на попытку выполнения недопустимой операции.
Таблица 5.4. Приращения приоритета для функции loCompleteRequest
Константа	Величина приращения
IO_NO_INCREMENT IO_CD_ROM_INCREMENT IO_DISK_INCREMENT IO_KEYBOARD_INCREMENT lOJMAILSLOTJNCREMENT IO_MOUSE_INCREMENT IO_NAMED_PIPE_INCREMENT IO_NETWORK_INCREMENT IO_PARALLEL_INCREMENT IO_SERIAL_INCREMENT IO_SOUND_INCREMENT IO_VIDEO_INCREMENT	0 1 1 6 2 6 2 2 1 2 8 1
Прежде чем вызывать loCompleteRequest, обязательно удалите все функции отмены, которые могли быть связаны с IRP. Как будет показано позднее в этот: главе, функция отмены устанавливается при нахождении IRP в очереди. Вы должны удалить пакет IRP из очереди перед тем, как завершить его. Все схемы формирования очередей, описанные в книге, сбрасывают указатель на функции отмены при выводе IRP из очереди, поэтому вам, скорее всего, не придется включать в драйвер дополнительный код следующего вида:
IoSetCancelRoutine(Irp, NULL); // <== почти всегда излишне
IoCompleteRequest(Irp. ...);
Итак, мы только что разобрались, как вызывать loCompleteRequest. Эта функция решает несколько задач, которые вы должны хорошо понимать:
О Вызов функций завершений, которые могли быть установлены разными драйверами. Важная тема функций завершения ввода/вывода рассматривается позднее в этой главе.
О Снятие блокировки со страниц, принадлежащих структурам MDL (Метоп Descriptor List), присоединенным к IRP. MDL используется при описании буферов запросов IRP_MJ_READ и IRP_MJ_WRITE для устройств, в объектах которых установлен флаг DOJBUFFERED. Управляющие операции также используют MDL, если в методе буферизации указан один из методов METHOD_XX_DIRECT. Эти темы более подробно рассматриваются в главах 7 и 9 соответственно
«Стандартная модель» обработки IRP
231
О Планирование специального АРС режима ядра для выполнения итоговой зачистки. В процесс зачистки входят копирование входных данных обратно в пользовательский буфер, копирование итогового состояния IRP и установка события, которого ожидает создатель IRP. Тот факт, что обработка завершения включает АРС, а в зачистке задействовано событие, накладывает некоторые ограничения на реализацию функций завершения в драйверах. Этот аспект завершения ввода/вывода будет более подробно рассмотрен позднее.
Передача IRP вниз по стеку
Основной смысл создания иерархии объектов устройств (в чем вам помогает WDM) — это возможность простой передачи IRP с одного уровня на следующий, более низкий уровень. Как было сказано в главе 2, ваша функция AddDevice вносит свой вклад в работу по созданию стека объектов устройств командой следующего вида:
pdx->LowerDeviceObject = loAttachDevIceToDevIceStacktfdo, pdo):
где fdo — адрес вашего объекта устройства, a pdo — адрес физического объекта устройства (PDO) на нижнем уровне стека устройства. Функция loAttachDeviceToDeviceStack возвращает адрес объекта устройства, расположенного непосредственно под вашим объектом. Когда вы решаете переслать пакет IRP, полученный с более высокого уровня, именно этот объект устройства указывается в вызове loCallDriver.
Прежде чем передавать IRP другому драйверу, обязательно удалите все функции отмены, которые могли быть установлены для него. Как я упоминал всего несколько абзацев назад, скорее всего, это требование будет выполняться автоматически и вам не придется дополнительно беспокоиться о нем. Код управления очередью обнуляет указатель на функцию отмены при выводе IRP из очереди. Если же запрос IRP вообще не ставился в очередь, расположенный выше драйвер позаботится о том, чтобы указатель на функцию отмены был равен NULL. Driver Verifier следит за соблюдением этого правила.
При передаче IRP на нижний уровень на вас возлагаются дополнительные обязанности по инициализации структуры IO_STACK_LOCATION, которая будет использоваться следующим драйвером для получения параметров. Один из способов основан на выполнении физического копирования:
loCopyCurrentlrpStackLocationToNext(Irp);
status = loCalIDriver(pdx->LowerDev1ce0bject, Irp);
loCopyCurrentlrpStackLocationToNext — макрос из файла WDM.H, копирующий все поля IO_STACKJ_OCATION (кроме полей, относящихся к функциям завершения ввода/вывода) из текущей позиции стека в следующую. В предыдущих версиях Windows NT авторы драйверов режима ядра иногда копировали всю структуру, вследствие чего функция завершения вызывающей стороны вызывалась дважды. Макрос ToCopyCurrentlrpStackLocationToNext, появившийся в WDM, рюшает эту проблему.
232
Глава 5. Пакеты запросов ввода/вывода
Если вас не интересует, что произойдет с IRP после передачи вниз по стеку, используйте следующую альтернативу loCopyCurrentlrpStackLocationToNext:
NTSTATUS ForwardAndForget(PDEVICE_OBJECT fdo. PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension;
IoSki pCurrentIrpStackLocat1 on(Irp):
return IoCallDr1ver(pdx->LowerDevice0bject, Irp);
}
Функция loSkipCurrentlrpStackLocation задерживает указатель стека IRP на одну позицию. loCallDriver сдвигает указатель стека вперед. В конечном итоге указатель стека не изменяется. Когда диспетчерская функция следующего драйвера вызовет loGetCurrentlrpStackLocation, она получит именно тот указатель IO_STACK_LOCATION, с которым мы работали, и поэтому обработает тот же самый запрос (те же основной и дополнительный коды функций) с теми же параметрами.
ВНИМАНИЕ----------------------------------------------------------------------------------
Версия loSkipCurrentlrpStackLocation, которую вы получаете в среде сборки Windows Me или Windows 2000 в DDK, представляет собой макрос, генерирующий две команды без замыкающих скобок. Следовательно, она не должна использоваться в конструкциях следующего вида:
If (<выражение>)
loSkipCurrentIrpStackLocat 1оп(Irp); // <== так нельзя!
Объяснить, почему loSkipCurrentlrpStackLocation работает именно таким образом, оказалось довольно сложно, и я подумал, что сказанное стоит пояснить рисунком. В ситуации, показанной на рис. 5.6, стек содержит три драйвера: ваш (объект функционального устройства (FDO)) и два других (верхний фильтрующий объект устройства (FiDO) и PDO). Сверху изображены связи между позициями стека, параметрами и функциями завершения при копировании с использованием loCopyCurrentlrpStackLocationToNext Снизу показаны те же связи при использовании «упрощенного варианта» loSkipCurrentlrpStackLocation. На нижнем рисунке третий, и последний, элемент стека остается неинициализированным, но этот факт никого не смущает.
Постановка IRP в очередь для последующей обработки
Третье возможное действие диспетчерской функции — постановка IRP в очередь для последующей обработки. Следующий фрагмент предполагает, что для работы с очередями IRP используется объект DEVQUEUE, о котором будет рассказано позднее в этой главе:
NTSTATUS DispatchSometh1ng(PDEVICE_0BJECT fdo, PIRP Irp)
{
loMarklrpPendlng(Irp);	//	1
StartPacket(&pdx->dqSometh1ng, fdo,	Irp,	Cancel Routine);	//	2
return STATUS-PENDING:	//	3
}
«Стандартная модель» обработки IRP
233
1.	Во всех случаях, когда диспетчерская функция возвращает STATUS_PENDING (как здесь), следует вызвать эту функцию, чтобы помочь I/O Manager избежать внутреннего состояния гонки. Это должно быть сделано до того, как пакет IRP поменяет владельца.
а
Элемент стека FiDO
Функция завершения источника
Копирование
Параметры
CvmpletionRoutine
Элемент стека FDO
Функция завершения фильтрующего драйвера
Копирование
Параметры
CornpletionRoutifie
Элемент стека PDO
Параметры
Рис. 5.6. Сравнение копирования с пропуском позиций стека ввода/вывода
234
Глава 5. Пакеты запросов ввод?
2.	Если устройство в настоящий момент занято или остановлено из-за собып  или управления питанием, вызов StartPacket помещает запрос в очередь. ; тивном случае StartPacket помещает устройство как занятое и вызывает ф\ сзц Startle (см. следующий раздел). В последнем аргументе передается адрес ч ции отмены. Функции отмены будут рассматриваться позднее в этой г ч
3.	Мы возвращаем STATUS_PENDING, сообщая тем самым вызывающей стори что обработка данного IRP еще не закончена.
Не обращайтесь к IRP после вызова StartPacket. К тому моменту, когда | функция вернет управление, пакет IRP может оказаться завершенным, а .< маемая им память — освобожденной. Таким образом, имеющийся указатель i жет оказаться недействительным.
Функция Startlo
Схемы очередей IRP часто основаны на вызове функции Startlo для обработки I
VOID StartIo(PDEVICEJ3BJECT device, PIRP Irp)
{
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) dev1ce->DeviceExtens1on:
}
Функция Startlo обычно получает управление на уровне DISPATCH_LEVEL — э означает, что она не должна генерировать страничные сбои.
Ваша задача в Startlo — инициировать обработку IRP. Как именно это сделать полностью зависит от вашего устройства. Иногда вам приходится обращаться к аппаратным регистрам, также используемым обработчиком прерывания, а возможно, и другими функциями драйвера. В некоторых случаях работает простейший способ: вы сохраняете информацию состояния о расширении устройства и имитируете прерывание. Поскольку оба способа должны осуществляться пол защитой той же спии-блокировки, которая защищает обработчик прерывания (ISR), для продолжения следует вызвать KeSynchronizeExecution. Пример:
VOID StartloC...)
KeSynchronizeExecution(pdx->InterruptObject, TransferFirst, (PVOID) pdx):
}
BOOLEAN TransferF1rst(PVOID context)
{
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) context;
инициализация устройства для новой операции> return TRUE;
}
Стандартная модель» обработки IRP
235
Показанная функция TransferFirst является представителем семейства функ-ий SynchCritSection, получивших свое название из-за синхронизации с ISR. Кон-епция SynchCritSection более подробно рассматривается в главе 7.
В Windows ХР и последующих системах вместо вызова KeSynchronizeExecution можно действовать по следующей схеме:
;0ID StartloC...)
{
KIRQL oldirql =
KeAcqu1reInterruptSp1nLock(pdx->InterruptObject);
инициализация устройства для новой операции>
KeReleaselnterruptSpinLock(pdx->InterruptObject, oldirql);
}
Заняв устройство обработкой нового запроса, Startlo возвращает управление. Б следующий раз вы встретитесь со своим запросом, когда устройство выдаст -рерывание, свидетельствующее о завершении операции.
обработчик прерывания (ISR)
Завершив пересылку данных, устройство может выдать аппаратное прерывание. В главе 7 я покажу, как «перехватить» прерывание функцией loConnectlnterrupt. В одном из аргументов loConnectlnterrupt передается адрес обработчика прерывания  ISR), и при возникновении прерывания система передает управление обработчику. Обработчик прерывания работает на уровне IRQL конкретного устройства (DIRQL) и защищается спин-блокировкой. Основа кода ISR выглядит так:
BOOLEAN Onlnterrupt(PKINTERRUPT InterruptObject,
PDEVICE_EXENSION pdx)
{
If (<устойство не выдавало прерывания^ return FALSE;
return TRUE;
}
Первый аргумент ISR содержит адрес объекта прерывания, созданного loConnectlnterrupt, но, скорее всего, вам не придется использовать этот аргумент. Во втором аргументе передается значение контекста, указанное при исходном вызове loConnectlnterrupt, — скорее всего, это будет адрес расширения устройства, как показано в этом фрагменте.
Обязанности обработчика прерываний подробно описываются в главе 7 в связи с чтением и записью данных — темой, особенно актуальной для обработки прерываний. А чтобы продолжить обсуждение стандартной модели, я скажу, что одним из самых вероятных действий обработчика прерываний является планирование отложенного вызова процедуры (DPC, Deferred Procedure Call). Это делается для того, чтобы вы могли выполнить некоторые действия (скажем, вызвать loComplete-Request), невозможные на уровне DIRQL, на котором работает ваш обработчик. Таким образом, в программе может присутствовать строка следующего вида:
IoRequestDpc(pdx->Dev1ce0bject, NULL, pdx);
236
Глава 5. Пакеты запросов ввода/вывода
Следующая встреча с IRP состоится в функции DPC, зарегистрированной в AddDevice вызовом loInitializeDpcRequest. Традиционно этой функции присваивается имя DpcForlsr.
Функция DPC
Функция DpcForlsr, запрашиваемая вашим обработчиком прерывания, получает управление на уровне DISPATCHJ-EVEL В общем случае она должна обработать IRP, ставший причиной самого последнего прерывания. Часто для этого приходится вызывать функции loCompleteRequest для завершения IRP и StartNextPacket для исключения следующего IRP из очереди устройства с целью передачи его Startle.
VOID DpcForlsrJPKDPC Dpc PDEVICEJBJECT fdo. PIRP junk, PDEVICE-EXTENSION pdx) {
StartNextPacket(&pdx->dqSomething, fdo);	// 1
loCompleteRequest(Irp, boost):	// 2
1. StartNextPacket удаляет следующий пакет из очереди и посылает его Startlo
2. loCompleteRequest завершает IRP, указанный в первом аргументе. Второй аргумент указывает приращение приоритета для потока, ожидающего этого IRP Также перед вызовом loCompleteRequest в IRP заполняется блок loStatus, как объяснялось ранее в подразделе «Завершение IRP».
Я (пока) не стану объяснять, как определить, какой именно IRP только что завершился. Также можно заметить, что третий аргумент DPC объявлен с типом указателя на IRP. Когда-то было замечено, что адрес IRP часто передается в одном из контекстных параметров loRequestDpc, — так появилось это значение. 1 Тем не менее, в функции, ставящей DPC в очередь, указатель на IRP лучше ( не использовать, потому что один вызов DPC может соответствовать любом} количеству запросов на постановку DPC в очередь. Соответственно, функция DPC должна получать текущий указатель на IRP на основании той схемы, кото- ' рая используется при формировании очередей IRP.	1
Вызов loCompleteRequest завершает стандартный путь обработки запроса ввода/вывода. После этого вызова I/O Manager (или другая сторона, изначально создавшая IRP) снова становится владельцем IRP. Владелец уничтожает IRP и может снять блокировку потока, ожидавшего завершения запроса.
Функции завершения
Нередко требуется узнать результаты запросов ввода/вывода, передаваемых на нижние уровни иерархии драйверов. Чтобы узнать, что в конечном итоге случилось с запросом, следует установить для него функцию завершения вызовом loSetCompletionRoutine:
loSetCompletionRoutinetIrp, CompletlonRoutine, context, InvokeOnSuccess, InvokeOnError, InvokeOnCancel);
Функции завершения
237
где Irp — запрос, о завершении которого вы хотите узнать, CompletionRoutine — адрес функции завершения, которую необходимо вызвать, a context — произвольное значение, размер которого соответствует размеру указателя, передаваемого в качестве аргумента функции завершения. Аргументы InvokeOnXxx представляют собой логические значения, которые указывают, должна ли функция завершения вызываться в трех различных ситуациях:
Э InvokeOnSuccess означает, что функция должна вызываться при завершении IRP с кодом состояния, проходящим проверку NT__SUCCESS.
□ InvokeOnError означает, что функция должна вызываться при завершении IRP с кодом состояния, не проходящим проверку NT_SUCCESS.
Э InvokeOnCancel означает, что функция должна вызываться в том случае, если перед завершением была вызвана функция loCancellrp. Я специально использовал такую формулировку: loCancellrp устанавливает в IRP флаг Cancel — именно это условие проверяется при наличии этого аргумента. Отмененный пакет IRP может оказаться завершенным с кодом STATUS-CANCELLED (который не проходит проверку NT_SUCCESS) или любым другим кодом состояния. Если пакет IRP отменяется с ошибкой и InvokeOnError был установлен, то самого присутствия InvokeOnError будет достаточно для вызова вашей функции завершения. И наоборот, если пакет IRP завершается без ошибки, а флаг InvokeOnSuccess установлен, то он приведет к вызову функции завершения. В таких случаях присутствие InvokeOnCancel оказывается излишним. Но если флаг InvokeOnSuccess и/или InvokeOnError отсутствует, присутствие InvokeOnCancel позволит вам узнать о завершении IRP с установленным флагом Cancel, какой бы код состояния ни использовался при завершении.
По крайней мере один из этих трех флагов должен быть истинным. Учтите, что loSetCompletionRoutine — это макрос, поэтому старайтесь избегать аргументов с побочными эффектами. В частности, макрос дважды ссылается на три флаговых аргумента и указатель на функции.
Вызов loSetCompletionRoutine устанавливает адрес функции завершения и аргумент контекста в следующей позиции IO_STACK_LOCATION, то есть в позиции стека, из которой следующий драйвер нижнего уровня получает свои параметры. Соответственно, самый нижний драйвер в стеке драйверов не должен пытаться установить функцию завершения. Впрочем, это было бы бессмысленно. потому что по самому определению драйвера нижнего уровня ему дальше некуда передавать запрос.
ИМАНИЕ-----------------------------------------------------------------------------
Не забывайте, что вы несете ответственность за инициализацию следующего элемента стека евода/вывода перед вызовом loCallDriver. Выполните инициализацию перед установкой функции завершения. Этот шаг особенно важен, если вы используете loCopyCurrentlrpStackLocationToNext для инициализации следующей позиции стека, потому что функция сбрасывает некоторые флаги, устанавливаемые вызовом loSetCompletionRoutine.
Полная функция выглядит примерно так:
NTSTATUS CompletionRout1ne(PDEVICE_OBJECT fdo, PIRP Irp,
PVOID context)
238
Глава 5. Пакеты запросов ввода/вывода
{
return <код состояниям
}
В аргументах передаются указатели на объект устройства и IRP, а также контекстное значение, указанное при вызове loSetCompletionRoutine. Функции завершения могут вызываться на уровне DISPATCH_LEVEL в контексте произвольного потока, но также возможен их вызов на уровне PASSIVE_LEVEL и APC_LEVEL. Следовательно, с учетом худшего случая (DISPATCH_LEVEL) функции завершения должны находиться в неперемещаемой памяти и вызывать только те сервисные функции, которые могут вызываться на уровне DISPATCH_LEVEL и ниже. Для обеспечения возможности вызова на более низком IRQL функция завершения не должна вызывать функции, изначально рассчитанные на уровень DISPATCHLEVEL, такие как KeAcquireSpinLockAtDpcLevel.
Функция завершения на самом деле может возвращать всего два возможных значения:
О STATUS_MORE-PROCESSING_REQUIRED — процесс завершения немедленно отменяется. Название этого кода состояния («требуется дополнительная обработка») лишь затемняет его реальный смысл — ускорение завершения IRP. Иногда драйвер действительно нуждается в проведении дополнительной обработки для того же IRP. В других случаях этот флаг всего лишь означает: «Эй, loCompleteRequest! Не трогай больше этот IRP!» По этой причине в будущих версиях DDK будет определена константа StopCompletion, по числовому значению равная STATUS_MORE_PROCESSING_REQUIRED, но более четко выражающая намерения программиста.
О Все остальные значения просто разрешают продолжить процесс завершения. Поскольку все значения, кроме STATUS-MORE_PROCESSING-REQUIRED, имеют один и тот же смысл, обычно я просто использую код STATUS-SUCCESS. В будущих версиях DDK будут определены константы STATUS_CONTINUE-COMPLETION и ContinueCompletion, по числовому значению совпадающие со STATUS-SUCCESS. Мы еще вернемся к теме возвращаемых кодов немного позднее в этой главе.
ПРИМЕЧАНИЕ--------------------------------------------------------------------
Аргумент функции завершения, содержащий указатель на объект устройства, представляет собой значение из указателя Deviceobject элемента стека ввода/вывода. Обычно его значение задается функцией loCallDriver. Разработчики иногда создают IRP с дополнительным элементом стека, чтобы передавать параметры функции завершения без создания дополнительной структуры контекста. Такая функция завершения получает указатель на объект устройства NULL, если только создатель не задаст поле Deviceobject.
Как вызываются функции завершения
Функция loCompleteRequest отвечает за вызов всех функций завершения, установленных драйверами в соответствующих элементах стека. Общая схема этого процесса показана на рис. 5.7: некто вызывает loCompleteRequest, сигнализируя о конце обработки IRP. Функция loCompleteRequest проверяет текущий элемент стека
Функции завершения
239
и смотрит, установил ли драйвер предыдущего уровня функцию завершения. Если нет, указатель стека смещается на один уровень вверх, и проверка выполняется снова. Процесс повторяется до тех пор, пока не будет найден элемент стека, установивший функцию завершения или функция loCompleteRequest не достигнет верхнего элемента стека. Затем loCompleteRequest выполняет действия, которые в конечном счете приводят к освобождению памяти, занимаемой IRP (среди прочего).
Когда функция loCompleteRequest находит позицию стека с указателем на функцию завершения, она вызывает эту функцию и анализирует код возврата. Если код возврата отличен от STATU S__MORE_PROCESSING_REQU1RED, loCompleteRequest перемещает указатель стека па один уровень вверх и продолжает работу. Но если код возврата равен STATUS_MORE_PROCESSING_REQUIRED, loCompleteRequest прерывает работу и возвращает управление вызывающей стороне. Пакет IRP переходит в «подвешенное» состояние. Предполагается, что драйвер, функция завершения которого прервала процесс раскрутки стека, выполнит дополнительную работу с IRP и вызовет loCompleteRequest для возобновления процесса завершения.
Рис. 5.7. Логика loCompleteRequest
Done
240	Глава 5. Пакеты запросов ввода/вывода
Внутри функции завершения вызов loGetCurrentlrpStackLocation получает указатель на элемент стека, который был текущим на момент вызова loSetCompletionRoutine. В функции завершения нельзя полагаться на содержимое элементов стека нижнего уровня. Чтобы обеспечить соблюдение этого правила, loCompleteRequest обнуляет большую часть данных следующего элемента непосредственно перед вызовом функции завершения.
ПРИМЕЧАНИЕ------------------------------------------------------------------------------
Вопрос викторины «Кто хочет стать миллионером?»:
Допустим, вы устанавливаете функцию завершения, а затем немедленно вызываете loComplete-Request. Что при этом произойдет?
А.	Компьютер взорвется и создаст гравитационную аномалию, что приведет к немедленному коллапсу Вселенной.
В.	Вы получите «синий экран смерти», и поделом — нельзя устанавливать функцию завершения в подобных ситуациях.
С.	loCompleteRequest вызовет вашу функцию завершения. После этого, если только функция завершения не вернет STATUS_MORE_PROCESSING_REQLIIRED, loCompleteRequest завершает IRP нормальным образом.
D.	loCompleteRequest не станет вызывать вашу функцию и произведет нормальное завершение IRP.
ВИНЭШЙЭЯЕЕ СИИП
-ЯНЛф ЕН ЧХГЭХЕЕЕяЛ ХИ1/ИЯ ЭМ И ЕХИЭТМЭГГС ОХЭШЛяЭХ Э ЛЯХ09ЕС190 ХЭЕНИЬЕН JSenbQH -Э1Э)бшоэо] виПжЛф еяэхэ ахнэпэке кэШотЛ^экэ я вэхИГохем кинэшйэяее опт -ЯиЛф ЕН ЧГГЭХЕЕЕЯЛ :Q ХЭЯХО НЭЕИЯЕЙЦ нэьодипю НО ОН ‘ОНдОГЮШЯ/ЯЕЙП ХИйЕТАПЧЯ □ xohxq  jgj олоннвЦ кип* иипиеои эшчтгод хэн элэхэ я еШом ‘кеьХггэ oivocbi Щэйэяэн g хэяхо эхээк ен эШэ ээя кЕннэкээд ешен охь Лкохоп ‘нэбэяэн у хэях0 :хэях0
Проблема loMarklrpPending
У функций завершения имеется еще одна особенность, на которую следует об-ратить внимание. Как говорят в кино, есть два пути: простой и трудный. Если вы предпочитаете простой путь, просто соблюдайте следующее правило:
Выполняйте следующий код в любой функции завершения, которая не возвращает STATUS_MORE_PROCESSING_REQUIRED:
if (Irp->PendingReturned) loMarklrpPending(Irp);
Теперь посмотрим, как узнать о loMarklrpPending по «трудному» пути. В некоторых функциях I/O Manager при работе с IRP используется код следующего вида:
KEVENT event;
IO_STATUSJLOCK iosb;
KelnltiallzeEvent(&event, ...);
PIRP Irp = loBuiIdDeviceloControlRequest(.... &event, Siosb);
NTSTATUS status = IoCallDriver(SomeDeviceObject, Irp);
if (status == STATUS_PENDING)
{
KeWaitForSingleObject(&event, ...);
Функции завершения
241
status = iosb.Status:
else
<за чистка IRP>
Суть в следующем: если возвращаемое значение равно STATUS-PENDING, то сторона, создавшая IRP, будет ожидать события, указанного при вызове loBuild-DeviceloControlRequest. Сказанное в равной мере относится к IRP, созданным вызовом loBuildSynchronousFsdRequest, — здесь важен факт условного ожидания по событию.
Возникает вопрос: кто же устанавливает это событие? Функция loCompleteRequest делает это косвенно, планируя АРС для той же функции, котрая выполняет шаг <зачистка IRP> в показанном псевдокоде. Код зачистки выполняет много задач, включая вызовы loFreelrp для освобождения IRP и KeSetEvent для установки события, которого может ожидать создатель IRP. Для некоторых типов IRP loCompleteRequest всегда планирует АРС. Однако для других типов IRP функция loCompleteRequest планирует АРС только при установленном флаге SL-PENDING_RETURNED в верхнем элементе стека. Вам не нужно знать, какие IRP входят в ту или иную категорию, потому что Microsoft может изменить реализацию этой функции и все ваши предположения станут недействительными. Достаточно знать, что loMarkPending — макрос, единственной целью которого является установка флага SL_PENDING_RETURNED в текущем элементе стека. Таким образом, если диспетчерская функция верхнего драйвера в стеке сделает следующее:
NTSTATUS TopDrlverDIspatchSomethlng(PDEVICE_OBJECT fdo. PIRP Irp)
{
loMarklrpPending(Irp);
return STATUS PENDING;
}
все пойдет нормально (здесь я нарушаю собственную схему формирования имен, чтобы подчеркнуть местонахождение диспетчерской функции). Поскольку диспетчерская функция возвращает STATUS-PENDING, источник IRP вызовет KeWaitForSingleObject. А поскольку диспетчерская функция устанавливает флаг SL-PENDING-RETURNED, loCompleteRequest знает о необходимости установить событие, которого ожидает создатель IRP.
Но предположим, что верхний драйвер просто передает запрос вниз по стеку, а второй драйвер приостановил IRP:
NTSTATUS TopDriverDlspatchSomethingtPDEVICEJDBJECT fido,
PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fldo->DeviceExtensi on:
IoCopyCurrentIrpStackLocat1onToNext(Irp):
242
Глава 5. Пакеты запросов ввода/вывода
return IoCallDriver(pdx->LowerDeviceObject, Irp);
}
NTSTATUS SecondDriverD1spatchSornething(PDEVICEJ3BJECT fdo.
PIRP Irp)
{
loMarklrpPendlng(Irp);
return STATUS_PENDING;
}
Очевидно, элемент стека второго драйвера содержит флаг SL__PENDING_RETURNED. а элемент первого драйвера — нет. Однако loCompleteRequest предвидит эту ситуацию и «распространяет» флаг SL_PENDING_RETURNED при раскрутке элементов стека, с которыми не связана функция завершения. Поскольку драйвер верхнего уровня не установил функции завершения, loCompleteRequest устанавливает флаг в верхней позиции, и это приводит к установке события завершения.
В другом сценарии верхний драйвер использует loSkipCurrentlrpStackLocation вместо IoCopyCurrentIrpStackLocationToNext. На этот раз все работает, как положено, потому что вызов loMarklrpPending в SecondDriverDispatchSomething изначально устанавливает флаг в верхней позиции стека.
Ситуация несколько усложняется, если верхний драйвер устанавливает функцию завершения:
NTSTATUS TopDriverD1spatchSomething(PDEVICE_0BJECT fido,
PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) f1do->DeviceExtension;
loCopyCurrentIrpStackLocationToNext(Irp);
loSetCompletlonRoutinedrp, TopDriverCompletionRoutine, ...);
return IoCallDriver(pdx->LowerDev1ce0bject, Irp);
NTSTATUS SecondDrlverDispatchSorethIng(PDEVICE_OBJECT fdo,
PIRP Irp)
{
loMarklrpPending(Irp);
return STATUS-PENDING;
}
В этом случае loCompleteRequest не станет распространять SL__PENDING_RETURNED в верхнюю позицию стека. Не берусь точно сказать, почему разработчики Windows NT не использовали распространение, но факт остается фактом: они решили именно так. Вместо этого непосредственно перед вызовом функции завершения loCompleteRequest устанавливает флаг Pending Returned в IRP равным значению SL_PENDING_RETURNED в следующем снизу элементе стека. Далее
•ункции завершения
243
функция завершения должна позаботиться об установке SL_.PENDING_RETURNED в своем элементе:
NTSTATUS TopDriverComplet1onRoutine(PDEVICE_OBJECT fido.
PIRP Irp. ...)
{
if (Irp->PendingReturned)
loMarklrpPending(Irp):
return STATUS-SUCCESS;
}
Если этого не сделать, вы обнаружите, что потоки входят во взаимную блокировку и ожидают установки события, которое никогда не устанавливается. Никогда не пропускайте этот шаг.
Учитывая важность вызова loMarklrpPending, разработчики драйверов неустанно пытаются найти другие способы решения проблемы. Далее приводится подборка неудачных решений.
Плохое решение № 1 - условный вызов loMarklrpPending в диспетчерской функции
Первая неудачная идея — попытка решить все проблемы с флагом незавершенной обработки исключительно в диспетчерской функции, чтобы функция завершения оставалась более или менее четкой и понятной:
NTSTATUS TopDr1verDispatchSomething(PDEVICE_OBJECT fido,
PIRP Irp)
{
PDEVICE_EXTENS ION pdx =
(PDEVICE_EXTENSION) fldo->DeviceExtensi on;
IoCopyCurrentIrpStackLocat1onToNext(Irp);
loSetCompletlonRoutinetlrp, TopDrlverCompletlonRoutlne. . .):
NTSTATUS status = IoCallDriver(pdx->LowerDev1ce0bject. Irp):
if (status == STATUS_PENDING)
loMarklrpPending!Irp); 11 <-= Так нельзя!
return status:
}
Так поступать не следует, потому что пакет IRP может быть уже завершенным, и к моменту возврата из loCallDriver кто-то уже мог вызвать loFreelrp. После передачи указателя функции, которая могла завершить IRP, будьте крайне осторожны в обращении с ним.
Плохое решение № 2 - безусловный вызов loMarklrpPending в диспетчерской функции
В следующем примере диспетчерская функция безусловно вызывает loMarklrpPending, а затем возвращает значение, полученное от loCallDriver:
NTSTATUS TopDrlverDIspatchSonieth1ng(PDEVICE_OBJECT fido.
PIRP Irp)
244
Глава 5. Пакеты запросов ввода/вывода
{
PDEVICEJEXTENSION pdx =
(PDEVICE EXTENSION) f1 do->DeviceExtension;
loMarklrpPending(Irp); // <== И так тоже нельзя!
loCopyCurrentlrpStackLocatlonToNext!Irp);
loSetCompletionRoutine!Irp, TopDrlverCompletlonRoutlne, ...);
return IoCallDr1ver(pdx->LowerDevice0bject, Irp);
}
Такое решение становится плохим, если следующий драйвер завершит IRP в своей диспетчерской функции и вернет код состояния, отличный от STATUS^ PENDING. В этой ситуации loCompleteRequest приведет к выполнению всей завершающей зачистки. При возврате кода, отличного от STATUS_PENDING, функция I/O Manager, от которой поступил IRP, может вызвать ту же функцию зачистки во второй раз. Попытка повторного завершения заканчивается фатальным сбоем.
Помните, что вызов loMarklrpPending всегда должен сочетаться с возвратом STATUS_PENDING. Другими словами, должно происходить либо и то и другое, либо ничего — но никогда одно из двух.
Плохое решение № 3 — вызов loMarklrpPending независимо от кода возврата в функции завершения
В этом примере программист забыл о том, в каком случае следует вызывать loMarklrpPending в функции завершения:
NTSTATUS TopDrlverDIspatchSometh1ng(PDEVICE__OBJECT fido.
PIRP Irp)
{
PDEVICE-EXTENSION pdx =
(PDEVICEJEXTENSION) f1do->Dev1ceExtens1on;
KEVENT event;
KeIn1t1al1zeEvent(&event, NotlficationEvent, FALSE);
loCopyCurrentlrpStackLocatlonToNext!Irp);
loSetCompletionRoutine!Irp, TopDrlverCompletlonRoutlne, &event,
TRUE, TRUE, TRUE);
loCallDrl ver!pdx->LowerDev1ce0bject, Irp);
KeWa1tForS1ng1e0bject(&event, ...);
Irp->IoStatus.Status = status;
loCompleteRequest!Irp, I0_N0_INCREMENT);
return status;
}
NTSTATUS TopDrlverCompletionRoutine(PDEVICE_OBJECT fido,
PIRP Irp, PVOID pev)
{
if (Irp->PendingReturned)
loMarklrpPending(Irp); // <== Ошибка
KeSetEvent((PKEVENT) pev. IO_NO_INCREMENT. FALSE);
return STATUS_MORE_PROCESSING_REQUIRED:
}
функции завершения
245
Скорее всего, программист хочет передать IRP синхронно, а затем возобновить обработку IRP после того, как нижний драйвер закончит работу с пакетом (см. сценарий обработки IRP № 7 в конце главы). Действительно, некоторые РпР IRP должны обрабатываться именно таким образом, Однако этот пример может вызвать фатальный сбой двойного завершения, если нижний драйвер вернет STATUS-PENDING. В действительности это тот же сценарий, что и в предыдущем плохом решении: диспетчерская функция возвращает код, отличный от STATIIS_PENDING, но в элементе стека установлен флаг незавершенной обработки. Довольно часто это неудачное решение, встречавшееся в обработчиках IRP-MJ-PNP многих примеров в ранних версиях Windows 2000 DDK, сходит с рук, потому что никто не отправляет пакеты Plug and Play IRP (таким образом, флаг Pending Returned никогда не устанавливается и недопустимы!! вызов loMarklrpPending никогда не происходит).
Вариация на эту тему также встречается при создании асинхронных IRP. Вы должны предоставить функцию завершения для освобождения IRP, причем эта функция завершения обязательно должна возвращать STATUS-MORE-PROCESSING-REQUIRED, чтобы функция loCompleteRequest не пыталась ничего делать с исчезнувшим IRP:
SOMETYPE SorneFunctionO
{
PIRP Irp = loBuildAsynchronousFsdRequestC...);
ToSetComplet1onRoutine(Irp. MyConipletlonRoutine, ...): loCalIDrivert...):
}
NTSTATUS MyCompletionRoutine(PDEVICE_OBJECT junk. PIRP Irp. PVOID context) { if (Irp->PendingReturned)
loMarklrpPendlng(Irp); // <== Ошибка!
loFreelrp(Irp);
return STATUSJ10RE_PR0CESSING_REQUIRED;
}
Проблема в том, что внутри этой функции завершения нет текущего элемента стека! Соответственно, loMarklrpPending изменяет содержимое произвольного участка памяти. Кроме того, было бы в принципе глупо беспокоиться об установке флага, который функция loCompleteRequest никогда не просмотрит: вы возвращаете STATUS„MORE_PROCESSING_REQUIRED, а это приведет к тому, что loCompleteRequest немедленно вернет управление вызывающей стороне, не сделав с вашим IRP абсолютно ничего.
Обе проблемы решаются одинаково: помните, что loMarklrpPending не следует вызывать в функции завершения, возвращающей STATUS-MORE-PROCESSING-REQUIRED.
246
Глава 5. Пакеты запросов ввода/вывода
Плохое решение № 4 — без анализа возвращаемого значения
Здесь программист отказывается от попыток разобраться в происходящем и просто всегда возвращает STATUS_PENDING. В этом случае ему не придется выполнять какие-либо особенные действия в функции завершения.
NTSTATUS TopDrlverDIspatchSomething(PDEVICEJ3BJECT fido,
PIRP Irp)
{
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) f1do->Dev1ceExtension;
loMarkIrpPendlng(Irp);
loCopyCurrentlrpStackLocatlonToNext(Irp);
loSetCompletlonRoutineUrp. TopDrlverCompletlonRoutlne, ...);
loCal1 Dr1 ver(pdx->LowerDev1ceObject, Irp);
return STATUS_PENDING; }
NTSTATUS TopDrlverCompletionRoutlneCPDEVICE-OBJECT fido.
PIRP Irp, . ) {
return STATUS_SUCCESS;
}
Эта стратегия не столько плоха, сколько неэффективна. Если в верхнем элементе стека установлен флаг SL_PENDING_RETURNED, функция loCompleteRequest планирует специальный вызов АРС режима ядра для выполнения работы в контексте исходного потока. В общем случае, если диспетчерская функция посылает пакет IRP, он в конечном итоге завершается в другом потоке. Вызов АРС необходим для возврата к исходному контексту, чтобы выполнить копирование буферов. Однако планирование АРС обходится относительно дорого, и если вы все еще находитесь в исходном потоке, лучше бы избежать этих затрат.
Впрочем, при реализации этого неудачного решения ничего ужасного не произойдет — в том смысле, что система будет нормально работать. Также учтите, что когда-нибудь Microsoft может изменить способ выполнения завершающей зачистки, так что не пишите свой драйвер в предположении, что АРС будет происходить всегда.
Осложнения с Plug and Play
Теоретически, РпР Manager может принять решение о выгрузке вашего драйвера перед тем, как одна из функций завершения получит возможность вернуть управление I/O Manager. Предполагается, что каждый, кто посылает вам 1RP, должен предотвратить это несчастливое стечение обстоятельств и позаботиться о том, чтобы драйвер не мог быть выгружен до завершения обработки IRP. Но когда вы сами создаете IRP, о защите тоже придется побеспокоиться самостоятельно. К числу составляющих этой защиты относится так называемый объект блокировки удаления (см. главу 6), запрещающий операцию удаления РпР до того, как все нижележащие драйверы обработают все невыполненные IRP.
функции завершения
247
Другой составляющей защиты является следующая функция, доступная в ХР и последующих версиях Windows:
loSetCompletionRoutineEx(DeviceObject, Irp, CompletionRoutine, context, InvokeOnSuccess, InvokeOnError. InvokeOnCancel);
<4ЕЧАНИЕ--------------------------------------------------------------------------
В описании loSetCompletionRoutineEx в документации DDK утверждается, что эта функция бесполезна для драйверов РпР. Но, как будет показано, во многих случаях драйвер РпР может воспользоваться этой функцией с целью защиты от преждевременной выгрузки.
Параметр Deviceobject содержит указатель на ваш объект устройства. Функция loSetCompletionRoutineEx создает дополнительную ссылку на этот объект перед вызовом функции завершения и освобождает ее после того, как функция завершения вернет управление. Дополнительная ссылка фиксирует объект устройства — и, что еще важнее, ваш драйвер — в памяти. Но поскольку этой функции не существует в версиях Windows, предшествующих ХР, хорошенько подумайте, нужны ли вам хлопоты с вызовом MmGetSystemRoutineAddress (и загрузкой реализации той же функции для Windows 98/Ме) для динамической компоновки этой функции, если она окажется доступной. Мне кажется, что здесь стоит рассмотреть пять разных ситуаций.
Ситуация 1: синхронный вторичный пакет IRP
Первая ситуация возникает при создании синхронного IRP для упрощения обработки другого IRP, полученного от внешнего источника. Вы намереваетесь завершить главный IRP после завершения вторичного IRP.
Обычно функции завершения не используются с синхронными IRP, но такая возможность существует — правда, для этого потребуется реализовать логику защиты очередей, описанную позднее в этой главе. Если вы пойдете по этому пути, функция завершения безопасно вернет управление перед полным завершением обработки вторичного IRP, а следовательно, и перед завершением главного IRP. До того времени код будет удерживаться в памяти отправителем главного 1RP, поэтому использовать loSetCompletionRoutineEx вам не придется.
Ситуация 2: асинхронный вторичный пакет IRP
В этой ситуации асинхронный вторичный пакет 1RP используется для упрощения реализации главного пакета IRP, полученного от внешнего источника. Главный IRP завершается в функции завершения, которая обязательно должна быть установлена для вторичного IRP.
Здесь следует использовать функцию loSetCompletionRoutineEx, если она доступна, потому что защита отправителя главного IRP перестает действовать в момент завершения главного IRP. Функция завершения все еще должна вернуть управление I/O Manager, а следовательно, нуждается в защите, обеспечиваемой этой новой функцией.
Ситуация 3: создание IRP в вашем системном потоке
Третья ситуация в нашем анализе функций завершения возникает в том случае, когда созданный вами системный поток (см. главу 14) устанавливает функции
248
Глава 5. Пакеты запросов ввода/вывода
завершения для IRP, посылаемых другим драйверами. Если в этой ситуации создается полноценный асинхронный пакет IRP, воспользуйтесь функцией loSetCompletionRoutineEx для установки обязательной функции завершения и примите меры, чтобы драйвер не выгружался до фактического вызова функции завершения. Например, можно захватить объект IO_REMOVE_LOCK, освобождаемый в функции завершения. Но если вы воспользуетесь сценарием 8 из «поваренной книги» в конце главы, чтобы отправить номинально асинхронный IRP синхронным способом, или если изначально будет использоваться синхронный IRP, для вызова loSetCompletionRoutineEx нет особых причин — предполагается, что вы дождетесь завершения этих IRP, прежде чем вызывать PsTerminateSystemThread для завершения потока. Другие функции драйвера будут ожидать завершения потока перед тем, как разрешить операционной системе окончательную выгрузку драйвера. Эта комбинация защитных мер позволяет безопасно использовать обычную функцию завершения.
Ситуация 4: создание IRP рабочим элементом
Надеюсь, в этой ситуации вы будете использовать loAllocateWorkltem и loQueue-Workltem, защищающие драйвер от выгрузки вплоть до возврата управления функции обратного вызова рабочего элемента. Как и в предыдущей ситуации, рекомендуется использовать loSetCompletionRoutineEx при выдаче асинхронного IRP без ожидания его завершения (как в сценарии 8). Б остальных случаях новая функция не нужна, если только управление не будет почему-либо возвращено до завершения IRP — что противоречило бы не только правилам функций завершения, но п всем правилам обработки IRP.
Ситуация 5: синхронные и асинхронные IRP для других целей
Возможно, у вас есть какие-то причины для выдачи синхронного IRP, который не упрощает обработку другого IRP, полученного из внешнего источника, и не работает в контексте вашего системного потока или рабочего элемента. Честно говоря, я не могу представить ситуацию, когда бы это потребовалось, но мне кажется, что любые попытки такого рода обречены на неудачу. Наверное, защита вашей функции завершения немного поможет, и все же не существует стопроцентного способа гарантировать, что драйвер все еще будет находиться в памяти при возврате из loCallDriver. Даже если вы что-нибудь придумаете, в конечном счете проблема всего лишь будет отложена на момент после принятия всех мыслимых мер - - и тогда хотя бы одна команда возврата будет выполнена без защиты, обеспечиваемой за пределами вашего драйвера.
В общем, не делайте этого.
Очереди запросов ввода/вывода
Иногда ваш драйвер получает пакет IRP, который он не может обработать немедленно. Вместо того чтобы отвергать IRP с кодом ошибки, диспетчерская функция помещает IRP в очередь. В другой части драйвера реализуется логика извлечения пакета IRP из очереди и передачи его функции Startlo.
Очереди запросов ввода/вывода
249
ЙГНКЦИИ ДЛЯ РАБОТЫ С ОЧЕРЕДЯМИ ОТ MICROSOFT----------------------------------------------------
Если не считать этой врезки, я не стану рассматривать функции loStartPacket и loStartNextPacket, присутствующие в Windows NT с самых первых версий. Модель очереди, реализуемая этими функциями, не подходит для драйверов WDM. В этой модели устройство находится в одном из трех состояний: свободно, занято с пустой очередью или занято с непустой очередью. Если функция loStartPacket вызывается в то время, пока устройство свободно, она безусловно посылает IRP вашей функции Startlo. К сожалению, во многих случаях драйвер WDM должен ставить IRP в очередь даже в том случае, если устройство свободно. Кроме того, эти функции слишком сильно зависят от глобальной спин-блокировки, злоупотребление которой заметно влияет на производительность.
Но если вам доведется работать над старым драйвером, использующим эти устаревшие функции, я приведу описание их работы. Диспетчерская функция ставит IRP в очередь следующим образом:
NTSTATUS DispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp)
{
loMarklrpPending(Irp);
IoStartPacket(fdo, Irp, NULL, CancelRoutine);
return STATUS_PENDING;
}
Драйвер содержит только одну функцию Startlo. Ваша функция DriverEntry заносит в поле Driver-Startlo объекта драйвера указатель на эту функцию. Если функция Startlo завершает IRP, вам также следует вызвать loSetStartloAttributes (в Windows ХР или позднее), чтобы предотвратить лишнюю рекурсию в Startlo. Функции loStartPacket и loStartNextPacket вызывают Startlo для обработки пакетов, одного за одним. Другими словами, Startlo ~ это та точка, в которой I/O Manager организует последовательный доступ к устройству.
Функция DPC (см. далее описание работы DPC) завершает предыдущий и начинает следующий IRP с использованием следующего кода:
,0ID DpcForIsr(PKDPC junk, PDEVICE_OBJECT fdo, PIRP Irp,
PVOID morejunk)
{
loCompleteRequest (Irp, STATUSJO_ INCREME NT);
loStartNextPacket(fdo. TRUE);
}
Чтобы обеспечить возможность отмены IRP, находящихся в очереди, необходимо написать функцию отмены. Описание этой функции и логика отмены в Startlo выходят за рамки книги.
Кроме того, поле Currentlrp объекта DEVICE-OBJECT всегда содержит NULL или адрес последнего IRP, отправленного (функцией loStartPacket или loStartNextPacket) вашей функции Startlo.
На концептуальном уровне организовать очередь IRP очень просто. Якорь списка включается в объект расширения устройства и инициализируется в функции AddDevice:
typedef struct _DEVICE_EXTENSION {
LIST_ENTRY IrpQueue;
BOOLEAN DeviceBusy;
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
NTSTATUS AddDevice!...)
{
InitializeListHead(&pdx->IrpQueue);
250
Глава 5. Пакеты запросов ввода/вывода
После этого пишутся две наивные функции для постановки IRP в очередь и извлечения из нее:
VOID NaiveStartPacket(PDEVICE_EXTENSION pdx, PIRP Irp)
{
If (pdx->DeviceBusy)
InsertTa11L1st(&pdx->IrpQueue, &Irp->Tai1.Overlay.LIstEntry);
else
{
pdx->DeviceBusy = TRUE;
StartIo(pdx->DeviceObject, Irp);
}
}
VOID NaiveStartNextPacket(PDEVICE_EXTENSION pdx, PIRP Irp)
if (IsListEmpty(&pdx->IrpQueue)) pdx->DeviceBusy = FALSE;
else
{
PLIST_ENTRY foo = RernoveHeadList(&pdx->IrpQueue);
PIRP Irp = CONTAINING_RECORD(foo, IRP,
Tail.Overlay.ListEntry);
StartIo(pdx->DeviceObject, Irp);
}
}
После этого диспетчерская функция вызывает NaiveStartPacket, а функция DPC вызывает NaiveStartNextPacket способом, который был описан ранее в связи со стандартной моделью.
У такой схемы имеется много недостатков, поэтому я и назвал ее наивной. Самая главная проблема заключается в том, что функция DPC и множественные экземпляры диспетчерской функции могут быть одновременно активны на разных процессорах. Скорее всего, это приведет к конфликтам при обращении к очереди и к флагу занятости. Проблему можно решить созданием спин-блокировки и ее применением для защиты от наиболее очевидных «ситуаций гонок», как показано далее:
typedef struct _DEVICE_EXTENSION {
LIST_ENTRY IrpQueue;
KSPIN-LOCK IrpQueueLock;
BOOLEAN DeviceBusy;
} DEVICE-EXTENSION, *PDEVICEJXTENSION;
NTSTATUS AddDeviceC...)
Ini ti ali zeLIstHead(&pdx->IrpQueue):
Очереди запросов ввода/вывода
251
KeInitializeSpinLock(&pdx->IrpQueueLock);
}
VOID LessNaiveStartPacket(PDEVICE_EXTENSION pdx, PIRP Irp)
{
KIRQL oldirql;
KeAcquireSpinLock(&pdx->IrpQueueLock, &oldirql);
If (pdx->Dev1ceBusy)
{
InsertTa11List(&pdx->IrpQueue, &Irp->Tail.Overlay.ListEntry;
KeReleaseSpinLock(&pdx->IrpQueueLock, oldirql);
}
else
{
pdx->DeviceBusy = TRUE:
KeReleaseSpinLock(&pdx->IrpQueueLock, DISPATCHLEVEL);
StartIo(pdx->DeviceObject, Irp);
KeLowerlrql(oldirql);
}
}
VOID LessNaiveStartNextPacket(PDEVICE_EXTENSION pdx, PIRP Irp)
{
KIRQL oldirql;
KeAcquireSpinLock(&pdx->IrpQueueLock, Soldirql);
if (IsL1stEmpty(&pdx->IrpQueue)
{
pdx->DeviceBusy = FALSE:
KeReleaseSpinLock(&pdx->IrpQueueLock, oldirql);
else { PLIST_ENTRY Too = RemoveHeadLlst(&pdx->IrpQueue); KeReleaseSpi nLock(&pdx->1rpQueueLock,
DISPATCHLEVEL);
PIRP Irp - CONTAIN ING_RECORD(foo, IRP,
Tail.Overlay.ListEntry);
StartIo(pdx->DeviceObject, Irp):
KeLowerlrql(oldirql);
}
}
Между прочим, вызов Startlo всегда должен осуществляться на одном уровне IRQL. Поскольку функции DPC входят в число сторон, вызывающих LessNaive-StartNextPacket, и они работают на уровне DISPATCH_LEVEL, мы выбираем уровень DISPATCH_LEVEL. А это означает, что мы должны оставаться на уровне DISPATCHLEVEL при освобождении спин-блокировки. (Ведь вы не забыли, что эти две
252
Глава 5. Пакеты запросов ввода/вывода
функции управления очередями должны находиться в неперемещаемой памяти, потому что они работают на уровне DISPATCH- LEVEL, верно?)
Эти функции очередей почти хороши, но у них есть один дефект и одно неудобство. Неудобство состоит в том, что нам потребуется механизм приостановки очереди на время некоторых состояний РпР и управления питанием. IRP накапливаются в приостановленной очереди до тех пор, пока кто-то не снимет приостановку и диспетчер очереди не сможет возобновить отправку IRP функции: Startlo. Дефект «менее наивных» функций — то, что практически в любой момент кто-то может захотеть отменить IRP. Отмена IRP усложняет логику очередей IRP до такой степени, что я посвятил этой теме весь следующий основной раздел настоящей главы. Но прежде чем переходить к описанию, позвольте мне объяснить, как пользоваться функциями, созданными мной специально для этой цели.
Объект DEVQUEUE
Для решения разнообразных проблем, связанных с очередями IRP, я разработал пакет функций управления объектом очереди, который я назвал DEVQUEUE. Сначала я представлю общие принципы использования объекта DEVQUEUE, а позднее объясню, как работают его основные функции. В следующих главах рассматривается взаимодействие кода РпР и управления питанием с объектом DEVQUEUE или другими объектами, которые вы напишете самостоятельно.
typedef struct JJEVICEJXTENSION {
DEVQUEUE dqReadWrlte;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
НА КОМПАКТ-ДИСКЕ------------------------------------------------------------
Код DevQueue является частью GENERIC.SYS. Кроме того, если вы воспользуетесь моим мастером WDMWIZ для построения заготовки драйвера и откажетесь от поддержки GENERIC.SYS, в ваш проект будут включены файлы DEVQUEUE.CPP и DEVQUEUE.H, содержащие полную реализацию того же объекта. Я не рекомендую перепечатывать этот код из книги, потому что код на диске обладает дополнительными возможностями, которые мне не удастся описать в книге. Также посетите мой веб-сайт (www.oneysoft.com), на котором публикуются обновления и исправления.
На рис. 5.8 представлена логика обработки IRP типичным драйвером, использующим объекты DEVQUEUE. Каждый объект DEVQUEUE обладает собственной функцией Startlo, которая указывается при инициализации объекта в AddDevice:
NTSTATUS AddDevIceC...) {
PDEVICE_EXTENSION pdx - ...;
InitializeQueue(&pdx->dqReadWr1te, Startlo);
Очереди запросов ввода/вывода
253
Начало работы устройства
Аппаратные прерывания
Завершенные IRP
Рис 5.8. Логика обработки IRP с участием объекта DEVQUEUE и функции Startlo
Вы можете выбрать общую диспетчерскую функцию как для IRP_MJ_READ, так и для IRP_MJ_WRITE:
NTSTATUS Dr1verEntry(PDRIVER__OBJECT Driverobject,
PUNICODEJTRING Registrypath)
{
Driver0bject->MajorFunct1on[IRP_MJ_READ] = DispatchReadWrlte;
DriverObject->MajorFunct1on[IRP_MJ_WRITE] = DispatchReadWrite;
i j
#pragma PAGEDCODE
NTSTATUS D1spatchReadWrite(PDEVICEJ3BJECT fdo, PIRP Irp)
PAGED_CODE();
PDEVICEJXTENSION pdx =
(PDEVICEJXTENSION) fdo->DeviceExtension:
IoMa rkIrpPendi ng(Irp);
StartPacket(&pdx->dqReadWr1te. fdo, Irp, CancelRoutine); return STATUS-PENDING;
}
^pragma LOCKEDCODE
VOID CancelRoutine(PDEVICE_OBJECT fdo. PIRP Irp)
254
Глава 5. Пакеты запросов ввода/вывода
{
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension:
CancelRequest(&pdx->dqReadWrite. Irp);
}
Обратите внимание: в последнем аргументе StartPacket необходимо передать функцию отмены, но вы увидите, насколько простой будет эта функция.
При завершении IRP в функции DPC также вызывается функция StartNextPacket
VOID DpcForlsr!PKPDC junkl, PDEVICE JIBJECT fdo, PIRP junk2,
PDEVICEJXTENSION pdx)
{
StartNextPacket(&pdx->dqReadWrite, fdo);
}
Если вы завершаете IRP в функции Startlo, запланируйте DPC с вызовом StartNextPacket для предотвращения излишней рекурсии. Пример;
typedef struct _DEVICE_EXTENSION {
KDPC StartNextDpc;
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
NTSTATUS AddDevIceC..)
{
Keimti al 1zeDpc!&pdx->StartNextDpc, (PKDEFERRED_ROUTINE) StartNextDpcRoutine, pdx);
}
VOID Startlo!...)
{
loCompleteRequest!...);
KeInsertQueueDpc(&pdx->StartNextDpc. NULL, NULL);
VOID StartNextDpcRoutine!PKDPC junkl, PDEVICE_EXTENSION pdx.
PVOID junk2, PVOID junk3)
{
StartNextPacket(&pdx->dqReadWr1te. pdx->DeviceObject);
}
В этом примере Startlo вызывает loCompleteRequest для завершения только что обработанного IRP. Прямой вызов StartNextPacket может привести к рекурсивному вызову Startlo. После достаточно большого количества рекурсивных вызовов произойдет переполнение стека. Чтобы предотвратить переполнение, мы ставим
Очереди запросов ввода/вывода
255
в очередь объект DPC StartNextDpc и возвращаем управление. Поскольку функция Startlo работает на уровне DISPATCH_LEVEL, функция DPC не может быть вызвана перед возвратом из Startlo. Следовательно, StartNextDpcRoutine может вызвать StartNextPacket, не беспокоясь о рекурсии.
вМЕЧАНИЕ--------------------------—------------------------------------------------------
Если вы используете функции очередей Microsoft loStartPacket и loStartNextPacket, в вашем драйвере будет единая функция Startlo. Функция DriverEntry должна занести адрес этой функции в указатель DriverStartlo в объекте драйвера. Для предотвращения описанных проблем с рекурсией б Windows ХР и далее можно вызвать loSetStartloAttributes.
1спользование защищенных очередей
Некоторые драйверы лучше работают с отдельным потоком ввода/вывода. Такой поток активизируется каждый раз, когда появляются IRP для обработки, обрабатывает IRP, пока очередь не опустеет, а затем снова переходит в режим ожида-ния. В главе 14 я подробно опишу, как работают такие потоки, а сейчас уместно поговорить о том, как организовать очереди IRP в таком драйвере (рис. 5.9).
Рис, 5.9. Последовательность обработки IRP в потоке ввода/вывода
256
Глава 5. Пакеты запросов ввода/вывода
Объект DEVQUEUE для такой ситуации не подходит, потому что он вызывает Startlo для обработки IRP. При использовании отдельного потока ввода/вывода стоит сделать так, чтобы этот поток отвечал за выборку IRP. Microsoft предоставляет набор функций для работы с защищенными очередями (cancel-safe queues); эти функции предоставляют большую часть необходимой функциональности. Не стоит ожидать, что они будут автоматически поддерживать вашу логику РпР и управления питанием, но я уверяю, что добавить такую поддержку будет несложно. Пример Cancel в DDK показывает, как работать с защищенными очередями в подобных ситуациях, но я тоже опишу основную механику.
ПРИМЕЧАНИЕ---------------------------------------------------------------------
В своей исходной реализации функции защищенных очередей не подходили для ситуаций, когда для ввода/вывода использовалась функция Startlo, потому что они не давали возможности задать указатель Currentlrp и выполнить операцию с очередью внутри одного вызова блокировки очереди. Пока я работал над книгой, функции были изменены и в них появилась поддержка Startlo, но у меня уже не оставалось времени на добавление материала с объяснением новых возможностей, поэтому я рекомендую обращаться к документации DDK.
Также учтите, что функции защищенных очередей впервые были описаны в версии DDK для ХР. Тем не менее, они реализованы в виде статической библиотеки и поэтому доступны на всех предшествующих платформах.
Инициализация защищенных очередей
Чтобы воспользоваться функциями защищенных очередей, сначала объявите шесть вспомогательных функций (табл. 5.5), чтобы I/O Manager мог выполнять операции с вашей очередью. Объявите экземпляр структуры IO_CSQ в структуре расширения устройства. Также объявите якорный элемент очереди IRP и синхронизационный объект, который вы собираетесь использовать. Все перечисленные объекты инициализируются в функции AddDevice. Пример:
typedef struct _DEVICE_EXTENSION {
IO_CSQ IrpQueue:
LIST_ENTRY IrpQueueAnchor:
KSPINJ.OCK IrpQueueLock:
} DEVICE_EXTENSION. *PDEVICE_EXTENSION;
NTSTATUS AddDevice!PDRIVER_OBJECT Driverobject, POEVICE-OBJECT pdo)
Kelnlti alizeSpi nLock(&pdx->IrpQueueLock):
Initial!zeLi stHead(&pdx->IrpQueueAnchor):
IoCsqIn1tial1ze(&pdx->IrpQueue. Insertlrp, Removelrp,
PeekNextlrp, AcquireLock, ReleaseLock. CompleteCanceledlrp):
Очереди запросов ввода/вывода
257
Таблица 5.5. Функции обратного вызова защищенных очередей
Функция обратного вызова	Назначение
Acqui reLock CompleteCanceledlrp Insertlrp PeekNextlrp	Устанавливает блокировку очереди Завершает отмененный IRP Вставляет IRP в очередь Получает указатель на следующий IRP без его удаления из очереди
ReleaseLock	Снимает блокировку очереди
Removelrp	Удаляет IRP из очереди
Работа с очередью
Пакет IRP ставится в очередь в диспетчерской функции, как в следующем примере:
NTSTATUS DispatchSometh1ng(PDEVICE_OBJECT fdo, PIRP Irp)
{
PDEVICEJXTENSION pdx =
(PDEVICEJXTENSION) fdo->Devj ceExtensi on;
IoCsqInsertIrp(&pdx->IrpQueue. Irp, NULL):
return STATUSJENDING;
}
He вызывайте loMarklrpPending самостоятельно — это и не нужно, и неправильно, потому что loCsqlnsertlrp делает это автоматически. Как и в других схемах очередей, к моменту возвращения из loCsqlnsertlrp пакет IRP может оказаться завершенным, поэтому в дальнейшем указатель не используется.
Для исключения IRP из очереди (скажем, в потоке ввода/вывода) используется следующий код:
PIRP Irp = IoCsqRemoveNextIrp(&pdx->IrpQueue. PeekContext);
Аргумент PeekContext будет описан чуть позже. Обратите внимание: при отсутствии IRP в очереди возвращается NULL. Полученный IRP не был отменен, и все последующие вызовы loCancellrp гарантированно не делают ничего, кроме установки флага Cancel в IRP.
Также желательно предоставить диспетчерскую функцию для IRP„MJ__CLEANUP, которая будет взаимодействовать с очередью. Соответствующий код будет представлен позднее в этой главе.
Функции обратного вызова защищенных очередей
Когда I/O Manager вызывает функции обратного вызова защищенных очередей, в одном из аргументов он передает объект очереди. Для получения адреса струк-туры расширения устройства применяется макрос CONTAINING_RECORD:
#define GET_DEVICE_EXTENSION(csq) \
CONTAINING_RECORD(csq. DEVICE_EXTENSION. IrpQueue)
258
Глава 5. Пакеты запросов ввода/вывода
Вы должны предоставить функции обратного вызова для захвата и освобождения той блокировки, которую вы решили использовать для своей очереди. Например, если ваш выбор остановился на спин-блокировке, пишутся следующие две функции:
VOID AcquireLock(PlO_CSQ esq, PKIRQL Irql)
{
PDEVICEJXTENSION pdx = GET_DEVICE_EXTENSION(csq);
KeAcquireSpinLock(&pdx->IrpQueueLock, Irql);
}
VOID Re1easeLock(PIO_CSQ esq. KIRQL Irql1
{
PDEVICEJXTENSION pdx = GET_DEVICE_EXTENSION(csq):
KeReleaseSpinLock(&pdx->IrpQueueLock. Irql):
}
Тем не менее, для синхронизации не обязательно использовать именно спин-блокировку. Ее хможно заменить мьютексом, быстрым мьютексом или любым другим объектом по вашему усмотрению.
При вызове loCsqlnsertlrp I/O Manager блокирует очередь, вызывая вашу функцию AcquireLock, а затем вызывает вашу функцию Insertlrp:
VOID InsertIrp(PIO_CSQ esq, PIRP Irp)
{
PDEVICEJEXTENSION pdx = GETJ)EVICEJXTENSION(csq);
InsertTa11 LI st(&pdx->IrpQueueAnchor,
&Irp->Tail.Overlay.ListEntry):
}
Когда вы вызываете loCsqRemoveNextlrp. I/O Manager блокирует очередь и вызывает ваши функции PeekNextlrp и Removelrp:
PIRP PeekNextIrp(PIO_CSQ esq. PIRP Irp. PVOID PeekContext) {
PDEVICE-EXTENSION pdx = GETJEVICE JXTENSION(csq);
PLIST_ENTRY next = Irp
? Irp->Tai1.Overlay.ListEntry.Flink
: pdx->IrpQueueAnchor.F11nk;
while (next > &pdx->IrpQueueAnchor)
PIRP Nextlrp = CONTAINING JECORD(next, IRP, Tail.Overlay.ListEntry);
if (PeekContext && <NextIrp соответствует PeekContext>) return Nextlrp;
if (’PeekContext)
return Nextlrp;
next = next->F1ink;
}
return NULL;
}
Очереди запросов ввода/вывода
259
VOID RemoveIrp(PIO_CSQ esq. PIRP Irp)
{
RemoveEntryList(&Irp->Tail.Overlay.ListEntry);
}
Параметры PeekNextlrp заслуживают некоторых пояснений. Параметр Irp, если он отличен от NULL, является предшественником первого 1RP, к которому вы должны обратиться. Если параметр Irp равен NULL, обращайтесь к IRP в начале списка. Параметр PeekContext содержит произвольную информацию; вы можете использовать его по своему усмотрению для обмена данными между вызывающей стороной loCsqRemoveNextlrp и PeekNextlrp. На практике в этом аргументе часто передается указатель на объект FILE_OBJECT, задействованный текущим пакетом IRP_MJ_CLEANUP. Я написал эту функцию так, чтобы параметр PeekContext, равный NULL, означал: «Вернуть следующий IRP, точка». Значение, отличное от NULL, означает: «Вернуть следующее значение, соответствующее PeekContext». Вы сами определяете, что следует подразумевать под «соответствием».
А вот шестая и последняя функция обратного вызова, вызываемая I/O Manager при отмене IRP:
VOID Comp!eteCanceledlrp(PIO_CSQ esq, PIRP Irp)
{
PDEVICE_EXTENSION pdx = GET_DEVICE_EXTENSION(csq):
Irp->IoStatus.Status = STATUS_CANCELLED;
Irp->IoStatus.Information = 0;
loCompleteRequest(Irp, IO_NO_INCREMENT);
}
Фактически, вся ее работа сводится к отмене IRP с кодом STATU S_C AN CELLED.
Еще раз напомню преимущества, которые вы получаете при использовании функций защищенных очередей: вам не нужно писать функцию отмены и включать в драйвер какой-либо код (не считая функции CompleteCanceledlrp), связанный с отменой IRP в очереди. I/O Manager устанавливает собственную функцию отмены и гарантирует, что отмененные IRP никогда не будут возвращаться функцией loCsqRemoveNextlrp.
Парковка IRP в защищенных очередях
В предыдущих разделах было описано, как использовать защищенные очереди для организации последовательной обработки ввода/вывода в потоке ядра. Однако функции защищенных очередей также могут применяться для парковки IRP на время их обработки. Идея заключается в том, что IRP помещается в очередь при получении. Затем, когда наступает время завершения IRP, этот конкретный пакет IRP исключается из очереди. В этом сценарии очередь используется не в традиционном понимании, потому что порядок следования IRP в ней несуществен.
Чтобы выполнить парковку IRP, определите устойчивую (persistent) структуру контекста для использования подсистемой защищенных очередей. Вам потребуется одна такая структура для каждого отдельного IRP, который вы намерены парковать. Предположим, ваш драйвер обрабатывает «красные» и «синие»
260
Глава 5. Пакеты запросов ввода/выво£?
запросы (эти странные названия помогут избежать лишней нагрузки, котору-часто несут за собой реальные примеры):
typedef struct _DEVICE_EXTENSION {
IO__CSQ_IRP_CONTEXT RedContext;
IO_CSQ_IRP_CONTEXT BlueContext;
} DEVICEJXTENSION, *PDEVICE_EXTENSION:
При получении «красного» IRP контекстная структура указывается в вызова loCsqlnsertlrp:
IoCsqInsertIrp(&pdx->IrpQueue, Redlrp. &pdx->RedContext);
Полагаю, процесс парковки «синих» IRP вполне очевиден.
Когда позднее вы принимаете решение о завершении припаркованного IRP вы пишете код следующего вида:
PIRP Redlrp = IoCsqRemoveIrp(&pdx->IrpQueue, &pdx->RedContext);
if (Redlrp)
{
RedIrp->IoStatus.Status = STATUS_XXX:
RedIrp->IoStatus.Information = YYY:
loCompleteRequest(Redlrp, IO_NO_INCREMENT):
}
Функция loCsqRemovelrp вернет NULL, если пакет IRP, связанный с контекстной структурой, уже был отменен.
При использовании этого механизма необходимо учитывать следующее:
О Вы сами должны убедиться в том, что пакет IRP не был ранее припаркован, по конкретной структуре контекста. Функция loCsqlnsertlrp имеет тип VOID и не может сообщить вам о нарушении этого правила.
О Не прикасайтесь к буферу ввода/вывода, связанному с припаркованным пакетом IRP, потому что пакет может быть отменен (с освобождением буфера!) в любой момент во время парковки. Прежде чем пытаться использовать буфер, удалите IRP из очереди.
Отмена запросов ввода/вывода
Как это бывает с людьми в реальной жизни, программы иногда меняют свое решение относительно запросов ввода/вывода, которые они попросили выполнить для них. Речь идет вовсе не о капризах: приложение может завершиться после выдачи запроса, на выполнение которого потребуется много времени, и запрос останется невыполненным. Такие ситуации особенно часто встречаются в мире WDM, где появление нового оборудования может потребовать приостановки запросов, пока Configuration Manager перераспределяет ресурсы, и где в любой момент может поступить запрос на отключение питания устройства.
Чтобы отменить запрос в режиме ядра, некто вызывает loCancellrp. Операционная система автоматически вызывает loCancellrp для всех IRP, принадлежа-
Если бы не многозадачность...
261
щих потоку, завершаемому с необработанными запросами. Приложение пользовательского режима может вызвать Cancello, чтобы отменить все незаконченные асинхронные операции, выданные потоком для манипулятора файла. loCancellrp хотелось бы просто завершить указанный IRP с кодом STATUSJ2ANCELLED. но здесь возникает одно затруднение: функция loCancellrp не знает, где находятся указатели на IRP, и не может быть полностью уверена в том, обрабатывается IRP в данный момент или нет. Из-за этого для выполнения основной работы по отмене IRP она полагается на предоставленную вами функцию отмены.
В действительности вызов loCancellrp является скорее рекомендацией, нежели требованием. Конечно, было бы хорошо, если бы каждый IRP, который кто-то попытался отменить, действительно завершался с кодом STATUS_CANCELLED. Но, с другой стороны, драйвер также может пойти вперед и завершить IRP нормальным образом, если это можно сделать относительно быстро. Предоставьте возможность отмены для тех запросов ввода/вывода, которые могут провести продолжительное время в очереди между диспетчерской функцией и Startlo. Что следует считать «продолжительным», оценивайте сами; мой совет — лучше ошибиться в сторону предоставления отмены, потому что сделать это несложно, а ваш драйвер лучше впишется в работу операционной системы.
Если бы не многозадачность...
С отменой IRP связана нетривиальная проблема синхронизации. Прежде чем объяснять суть проблемы и ее решение, я хочу описать, как бы работал механизм отмены при отсутствии многозадачности и специфики многопроцессорных систем. В этой утопической картине взаимодействие I/O Manager с Startlo и предоставленной вами функцией отмены выглядит так:
Э При постановке IRP в очередь в поле CancelRoutine в IRP заносится адрес функции отмены. При выводе IRP из очереди поле CancelRoutine задается равным NULL.
□ Функция loCancellrp безусловно устанавливает флаг Cancel в IRP. Затем она проверяет, равен ли NULL указатель CancelRoutine в IRP. Пока IRP находится в очереди, CancelRoutine отличен от NULL. В этом случае вызов loCancellrp вызывает вашу функцию отмены. Функция отмены удаляет пакет IRP из очереди, где он в данный момент находится, и завершает IRP с кодом STATUS_CANCELLED.
3 После вывода IRP из очереди функция loCancellrp обнаруживает, что указатель CancelRoutine равен NULL, и не вызывает функцию отмены. IRP обрабатывается до завершения за разумное время (концепция, требующая технической оценки), и никого не интересует, что в действительности пакет 1RP не был отменен.
Синхронизация отмены
К сожалению, нам, программистам, приходится писать код для многопроцессорной, многозадачной среды, в которой следствия иногда (во всяком случае, внешне) опережают причины. В описанном мной тривиальном сценарии существует
262
Глава 5. Пакеты запросов ввода/вывода
множество потенциальных ситуаций «гонок» между постановкой в очередь, удалением из очереди и функциями отмены. Например, что произойдет, если loCancellrp вызовет функцию отмены для отмены пакета IRP, находящегося в начале очереди? Если пакет одновременно удаляется из очереди на другом процессоре, может возникнуть конфликт между функцией отмены и логикой удаления из очереди. Но это лишь простейшая из всех возможных «гонок».
Когда-то разработчики драйверов решали проблемы «гонок» при помощи глобальной спин-блокировки отмены. Поскольку вы не должны применять эту спин-блокировку для синхронизации в своем драйвере, я кратко опишу ее в следующей врезке. Ознакомьтесь с этой информацией для полноты картины, но пользоваться этой блокировкой вам не придется.
ГЛОБАЛЬНАЯ СПИН-БЛОКИРОВКА ОТМЕНЫ-------------------------------------------------
Исходная схема, разработанная Microsoft для синхронизации отмены IRP, строилась на основе глобальной спин-блокировки отмены. Для захвата и освобождения блокировки использовались функции loAcquireCancelSpinLock и loReleaseCancelSpinLock. Функции очередей Microsoft loStartPacket и loStartNextPacket захватывают и освобождают блокировку для защиты доступа к полям отмены IRP и полю Currentlrp объекта устройства. Функция loCancellrp захватывает блокировку перед вызовом функции отмены, но не освобождает ее. Ваша функция отмены какое-то время работает под защитой блокировки, после чего вызывает loReleaseCancelSpinLock перед возвратом.
В этой схеме ваша функция Startlo также должна захватывать и освобождать глобальную спин-блокировку для безопасной проверки флага Cancel в IRP и сброса указателя CancelRoutine в NULL. Вряд ли кому-нибудь удастся построить хоть сколько-нибудь безопасную логику ведения очередей и отмены пакетов на базе этой схемы. Даже самые лучшие алгоритмы обладают недостатком, обусловленным совпадением указателей в IRP. Кроме того, тот факт, что каждый драйвер в системе должен два или три раза использовать одну спин-блокировку в ходе нормального выполнения, существенно влияет на быстродействие. По этой причине Microsoft сейчас рекомендует либо использовать в драйверах функции защищенных очередей, либо скопировать чью-то проверенную логику работы с очередями. Microsoft (и я тоже) не рекомендует пытаться проектировать собственную логику работы очередей с отменой, потому что реализовать ее правильно очень сложно.
В наши дни проблемы «гонок» при отмене решаются одним из двух способов. Во-первых, это собственная логика очередей IRP (или, что более вероятно, копирование и вставка готового кода). Во-вторых, в некоторых драйверах можно воспользоваться функциями семейства loCsqAxr. Вам не обязательно понимать, как функции loCsqAxr обрабатывают отмену IRP, — компания Microsoft предполагала, что эти функции будут использоваться по принципу «черного ящика». Я подробно рассмотрю реализацию отмены в моем объекте DEVQUEUE, но сначала я должен немного рассказать о внутреннем устройстве loCancellrp.
Подробнее об отмене IRP
Далее приводится схема loCancellrp. Вы должны знать ее для написания правильного кода обработки IRP (это не копия исходного кода Windows ХР, а сокращенный фрагмент):
BOOLEAN loCancelIrp(PIRP Irp)
{
Если бы не многозадачность...
263
loAcquIreCancelSpinLock(&Irp->CancelIrql);	// 1
Irp->Cancel = TRUE;	// 2
PDRIVER_CANCEL Cancel Routine = loSetCancelRoutine(Irp, NULL); // 3 if (CancelRoutine)
{
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
(*CancelRoutine)(stack->DeviceObject, Irp);	// 4
return TRUE;
}
else {
IoReleaseCancelSpinLock(Irp->CancelIrql);	II 5
return FALSE; }
}
1.	Сначала loCancellrp захватывает глобальную спин-блокировку отмены. Как было сказано в приведенной ранее врезке, многие старые драйверы постоянно конкурируют за использование этой блокировки в процессе обычной обработки IRP. Новые драйверы используют эту блокировку лишь на короткое время, при обработке отмены IRP.
2.	Установка флага Cancel=TRUE сообщает всем заинтересованным сторонам, что для этого пакета была вызвана функция loCancellrp.
3.	loSetCancelRoutine получает текущий указатель CancelRoutine и задает поле равным NULL за одну атомарную операцию.
4.	loCancellrp вызывает функцию отмены, если она присутствует, без предварительного освобождения глобальной спин-блокировки отмены. Блокировка должна быть освобождена функцией отмены! Также обратите внимание на то, что аргумент объекта устройства, передаваемый функции отмены, берется из текущей позиции стека, где, как предполагается, он был оставлен функцией loCallDriver.
5.	Если функция отмены отсутствует, функция loCancellrp сама освобождает глобальную спин-блокировку отмены.
Как работает отмена в DEVQUEUE
Как я и обещал, теперь я перехожу к объяснению основных функций DEVQUEUE.
Это поможет вам понять, как безопасно организовать отмену IRP.
Внутреннее строение DEVQUEUE: инициализация
В заголовочных файлах DEVQUEUE.H и GENERIC.H объекта DEVQUEUE присутствует следующее объявление:
typedef struct J3EVQUEUE {
LIST_ENTRY head;	//	1
KSPIN.LOCK lock:	//	2
PDRIVERJ>TART Startlo;	//	3
LONG stall count;	//4
264
Глава 5. Пакеты запросов ввода/вывода
PIRP Currentlrp;	//	5
KEVENT evStop;	//	6
NTSTATUS abortstatus;	//	7
} DEVQUEUE, *PDEVQUEUE;
Функция InitializeQueue инициализирует один из этих объектов следующим образом:
VOID NTAPI Initial 1zeQueueCPDEVQUEUE pdq.
PDRIVER_STARTIO Startlo)
{
Initlal 1zeLIstHead(&pdq->head):
Kelnltlal 1zeSpi nLock(&pdq->lock):
pdq->StartIo = Startlo:
pdq->stallcount = 1;
pdq->Current!rp = NULL:
KeJnitializeEvent(&pdq->evStop, NotificatlonEvent, FALSE);
pdq->abortstatus = (NTSTATUS) 0;
}
1.	Для организации очереди IRP будет использоваться обычный (не атомарный) двусвязный список. Атомарные списки нам не нужны, потому что вся работа со списком будет осуществляться под защитой спин-блокировки.
2.	Спин-блокировка защищает доступ к очереди и другим полям структуры DEVQUEUE. Кроме того, она занимает место глобальной спин-блокировки отмены для защиты практически всего процесса отмены, что улучшает производительность системы.
3.	Каждая очередь имеет собственную функцию Startlo, которая вызывается автоматически в нужный момент.
4.	Счетчик приостановки указывает, сколько поступило запросов на приостановку доставки данного IRP в Startlo. Инициализация счетчика значением 1 означает, что обработчик IRP_MN_START_DEVICE должен вызвать RestartRequests для освобождения IRP. Эта тема более подробно рассматривается в главе 6.
5.	В поле Currentlrp регистрируется пакет IRP, последним отправленный функции Startlo. Инициализация этого поля значением NULL означает, что устройство в исходном состоянии свободно.
6.	Событие используется при необходимости блокировки WaitForCurrentlrp одной из функций DEVQUEUE, участвующих в обработке запросов РпР. Оно устанавливается в функции StartNextPacket, которая всегда вызывается при завершении текущего IRP.
7.	Входящие IRP отвергаются в двух ситуациях. Первая ситуация возникает после окончательного подтверждения отключения устройства, когда мы должны отвергать все новые IRP с кодом STATUS_DELETE_PENDING. Вторая ситуация встречается в режиме пониженного энергопотребления, когда в зависимости от типа запроса можно выбрать вариант отклонения новых IRP с кодом STATUS_DEVICE_POWERED_OFF. В поле abortstatus хранится код состояния, который должен использоваться при отклонении IRP в подобных ситуациях.
tern бы не многозадачность,..
265
В стабильном состоянии после завершения всей инициализации РпР каждый объект DEVQUEUE обладает нулевыми счетчиками stallcount и abortstatus.
Внутреннее строение DEVQUEUE: очереди и отмена
Далее приводится полная реализация трех функций DEVQUEUE, применение которых мы только что рассмотрели. Я вставил код прямо из файла GENERIC.SYS и выполнил минимальное форматирование, чтобы листинг нормально смотрелся на печатной странице. Кроме того, из StartNextPacket был исключен код управления питанием, потому что он лишь усложняет изложение материала.
VOID StartPacket(PDEVQUEUE pdq, PDEVICE_OBJECT fdo, PIRP Irp, PDRIVER_CANCEL cancel) {
KIRQL oldirql;
KeAcqu1reSpinLock(Spdq->lock, Soldirql);	// 1
NTSTATUS abortstatus = pdq->abortstatus;
If (abortstatus)	// 2
{
KeReleaseSpinLock(Spdq->lock, oldirql);
Irp->IoStatus,Status = abortstatus; loCompleteRequest!Irp, lOJQJNCREMENT);
else if (pdq->CurrentIrp pdq->sta11 count)	17 J
loSetCancelRoutine(Irp, cancel);
If (Irp->Cancel && loSetCancelRoutine!Irp, NULL))
{
KeReleaseSpi nLock(&pdq->1ock, oldi rql);
Irp->IoStatus.Status = STATUS__CANCELLED; loCompleteRequest!Irp, IO_NO_INCREMENT);
else	6
{
InsertTa11L1st(&pdq->head, SIrp->Ta11.Overlay.ListEntry)-
KeReleaseSpinLock(Spdq->lock, oldirql);
}
{
pdq->CurrentIrp = Irp;
KeRel easeSpinLockFromDpcLevel(&pdq->lock);
(*pdq->StartIo)(fdo, Irp);
KeLowerlrql(oldirql):
}
}
VOID StartNextPacket(PDEVQUEUE pdq, PDEVICE_OBJECT fdo)
266
Глава 5- Пакеты запросов ввода/вывода
{ KIRQL oldirql; KeAcquIreSpInLock(&pdq->lock. &oldi rql); pdq->CurrentIrp = NULL; while (!pdq->stallcount && !pdq->abortstatus && !IsL1stEmpty(&pdq->head)) r	// 8 // 9 // 10	
i PLIST ENTRY next = RemoveHeadL1st(&pdq->head); PIRP Irp = CONTAINING_RECORD(next, IRP, Tall.Overlay.ListEntry):	//	11
If (!IoSetCancelRout1ne(Irp, NULL)) { In1t1al1zeL1stHead(&Irp->Ta11.Overlay.LIstEntry)> continue;	//	12
I pdq->CurrentIrp = Irp: KeReleaseSpInLockFromDpcLevel(&pdq->lock): (*pdq->StartIo)(fdo, Irp); KeLowerlrql(oldirql); } KeReleaseSp1nLock(&pdq->lock, oldirql); } VOID Cancel Request(PDEVQUEUE pdq, PIRP Irp) { KIRQL oldirql = Irp->CancelIrql;	//	13
loReleaseCancelSpi nLock(DISPATCH_LEVEL):	//	14
KeAcquireSpinLockAtDpcLevel(&pdq->lock);	//	15
RemoveEntryL1st(&Irp->Ta11.Overlay.ListEntry): KeReleaseSpinLock(&pdq->1ock, oldirql);	//	16
Irp->IoStatus.Status = STATUSJANCELLED; loCompleteRequest(Irp, 10 NO INCREMENT); }	//	17
А теперь я подробно опишу, как организована совместная работа этих функций для реализации защищенных очередей. Для этого мы рассмотрим последовательность сценариев, в которых задействованы все возможные логические пути программы.
1.	Основной путь выполнения StartPacket
Основной путь выполнения StartPacket происходит в стабильном состоянии, когда пакет IRP (который, как мы предполагаем, не был отменен) поступает после выполнения всей обработки РпР и в режиме полного энергопотребления устройства. В этой ситуации оба счетчика stallcount и abortstatus должны быть равны 0. Дальнейший выбор логической ветви зависит от занятости устройства:
С) Сначала мы захватываем спин-блокировку, ассоциированную с устройством (1). Почти все функции DEVQUEUE захватывают эту блокировку (см. (8) и (15)), поэтому мы можем быть уверены в том, что никакой код на другом процессоре не внесет в состояние очереди изменения, из-за которых принимаемые нами решения могли бы стать недействительными.
Если бы не многозадачность...
267
О Если устройство занято, команда if в точке (3) обнаруживает, что поле Currentlrp отлично от NULL. Условие команды if в точке (5) тоже не выполняется (позднее я объясню, почему), поэтому управление передается в точку (6) для помещения IRP в очередь. Освобождение спин-блокировки — последнее, что происходит на этом пути выполнения.
О Если устройство свободно, команда if в точке (3) обнаруживает, что поле Currentlrp равно NULL. Мы уже предположили, что счетчик stallcount равен О, поэтому управление передается в точку (7) для обработки IRP. Обратите внимание на вызов Startlo на уровне DISPATCH_LEVEL после освобождения спин-блокировки.
2.	Основной путь выполнения StartNextPacket
Основной путь выполнения StartNextPacket аналогичен соответствующему пути StartPacket. Счетчики stallcount и abortstatus равны 0, а пакет IRP в начале очереди не был отменен. Функция StartNextPacket выполняет следующие действия:
О Прежде всего захватывается спин-блокировка очереди (8). Тем самым очередь защищается от одновременного доступа со стороны других процессоров, пытающихся выполнить StartPacket или CancelRequest. Другие процессоры не могут пытаться выполнить StartNextPacket, потому что вызов этой функции может осуществляться только стороной, только что завершившей обработку другого IRP. В нашем случае в любой момент времени активен только один IRP, поэтому такая сторона может быть только одна.
О Если список пуст, функция освобождает спин-блокировку и возвращает управление. Если функция StartPacket ожидала освобождения блокировки, она обнаруживает, что устройство освободилось, и вызывает Startlo.
Э Если список не пуст, условие if в точке (10) выполняется, и функция входит в цикл поиска следующего неотмененного IRP.
Э Первым шагом цикла (11) становится удаление следующего IRP из списка. Обратите внимание: функция RemoveHeadList возвращает адрес структуры LIST_ENTRY, встроенной в IRP. Для получения адреса IRP используется макрос CONTAINING_RECORD.
Э Функция loSetCancelRoutine (12) возвращает отличный от NULL адрес функции отмены, изначально переданный StartPacket. Никто (и тем более функция loCancellrp) не изменял этот указатель с того момента, когда он был задан функцией StartPacket Соответственно, мы попадаем в точку (13), где этот IRP передается функции Startlo на уровне DISPATCH_LEVEL.
3.	Отмена IRP до вызова StartPacket; устройство свободно
Предположим, функция StartPacket получает пакет IRP, отмененный некоторое время назад. Во время выполнения loCancellrp для IRP не существовало функции отмены (если бы она была, то она принадлежала бы драйверу, находящемуся в более высокой позиции стека, этот другой драйвер завершил бы IRP вместо того, чтобы передавать его вниз нашему драйверу). Следовательно, вся работа loCancellrp свелась к установке флага Cancel в IRP.
268
Глава 5. Пакеты запросов ввода/вывода
Если устройство свободно, условие if в точке (3) не выполняется, и мы снова переходим прямо к точке (7), где 1RP посылается Startlo. Фактически, мы собираемся проигнорировать флаг Cancel. Такое решение оправданно в той мере, в которой мы руководствуемся своим критерием «относительно быстрой» обработки IRP. Если IRP не будет обработан, функция Startlo и последующая логика обработки IRP должны содержать код проверки флага Cancel и раннего завершения IRP.
4.	Отмена IRP во время выполнения StartPacket; устройство свободно
В этом сценарии функция loCancellrp вызывается во время работы StartPacket. Как и в сценарии 3, loCancellrp устанавливает флаг Cancel и возвращает управление. Мы игнорируем этот флаг и передаем IRP функции Startlo.
5.	Отмена IRP до вызова StartPacket; устройство занято
Исходные условия те же, что и в сценарии 3, за исключением того, что устройство занято, а условие if в точке (3) выполняется. Мы задаем функцию отмены (4), а затем проверяем флаг Cancel (5). Поскольку флаг Cancel равен TRUE, функция loSetCancelRoutine вызывается во второй раз. Функция возвращает отличный от NULL адрес, который мы только что задали, после чего IRP завершается с кодом STATUS_CANCELLED.
6.	Отмена IRP во время выполнения StartPacket; устройство занято
Это первая проблемная ситуация, которая возникает в нашем анализе. Допустим, исходные условия те же, что и в сценарии 3, но на этот раз устройство занято и кто-то вызывает loCancellrp в процессе выполнения StartPacket. Рассмотрим несколько возможных ситуаций:
О Флаг Cancel (5) проверяется до того, как он будет установлен функцией loCancellrp. Поскольку на момент проверки флаг равен FALSE, мы переходим к точке (6) и ставим IRP в очередь. Дальнейшее зависит от взаимодействия функций loCancellrp, CancelRequest и StartNextPacket. Впрочем, к функции StartPacket все эти проблемы не имеют отношения, и она более не беспокоится об этом IRP.
О Флаг Cancel (5) проверяется после того, как он будет установлен функцией loCancellrp. Указатель на функцию отмены уже установлен (4). Дальнейшее зависит от того, кто первым выполнит вызов loSetCancelRoutine, возвращающий указателю значение NULL, — loCancellrp или мы. Вспомните, что вызов loSetCancelRoutine представляет собой атомарную операцию на базе Interlocked-ExchangePointer. Если наш вызов будет выполнен первым, мы получаем обратно значение, отличное от NULL, и завершаем IRP. Функция loCancellrp получает NULL, а следовательно, не вызывает функцию завершения.
О С другой стороны, если loCancellrp первой выполнит loSetCancelRoutine, мы при своем вызове получим NULL. Далее IRP ставится в очередь (6), и дальше функция действует по принципу «пусть другие разбираются» (см. ранее). loCancellrp вызывает функцию отмены, вызов блокируется (15) до момента освобождения спин-блокировки очереди. В конечном итоге наша функция отмены завершает IRP.
Если бы не многозадачность...
269
7.	Нормальная отмена IRP
IRP отменяются не так уж часто, и я не уверен, насколько оправдан термин нормальная в этом контексте. Но если и существует нормальный сценарий отмены IRP, он выглядит так: кто-то вызывает loCancellrp для отмены IRP, находящегося в очереди, но процесс отмены доходит до завершения до того, как StartNextPacket успеет вмешаться. Потенциальная «гонка» между StartNextPacket и CancelRequest не воплощается в жизнь. События развиваются так:
□	loCancellrp захватывает глобальную спин-блокировку, устанавливает флаг Cancel и выполняет loSetCan cel Routine для получения адреса нашей функции отмены и перевода указателя в IRP в NULL (см. описание loCancellrp на с. 262-263).
□	loCancellrp вызывает нашу функцию отмены без освобождения блокировки.
Функция отмены находит правильный объект DEVQUEUE и вызывает Cancel-Request. CancelRequest немедленно освобождает глобальную спин-блокировку отмены (14).
Э CancelRequest захватывает спин-блокировку очереди (15). После этой точки никакие «гонки» с другими функциями DEVQUEUE невозможны.
Э CancelRequest удаляет IRP из очереди (16) и освобождает спин-блокировку. Если бы функция StartNextPacket была запущена в этот момент, она бы не нашла IRP в очереди.
Э CancelRequest завершает IRP с кодом STATUS_CANCELLED (17).
8.	Аномальная отмена IRP
Самый трудный сценарий отмены IRP возникает в ситуациях, когда loCancellrp пытается отменить пакет IRP, находящийся в начале очереди, при активной функций StartNextPacket. В точке (12) StartNextPacket обнуляет указатель на функцию отмены. Если возвращаемое значение loSetCancelRoutine отлично от NULL, можно переходить к обработке IRP (13).
Но если возвращаемое значение loSetCancelRoutine равно NULL, значит, функция loCancellrp пришла к цели первой. Вероятно, CancelRequest прямо сейчас ожидает на другом процессоре освобождения спин-блокировки очереди, чтобы вывести IRP из очереди и завершить его. Проблема в том, что IRP из очереди уже удален. Я немного горжусь приемом, придуманным мной для решения проблемы: мы просто инициализируем связующее поле IRP так, словно оно содержит якорь списка! Вызов RemoveEntryList в точке (16) CancelRequest выполняет несколько безрезультатных операций для «удаления» IRP из вырожденного списка.
9.	Невозможные или несущественные ситуации
Предыдущий список исчерпывает все возможные конфликты между функциями DEVQUEUE и loCancellrp (правда, остается «гонка» между IRP_MJ_CLEANUP и отменой IRP, но мы вернемся к этой теме чуть позднее в этой главе). Далее перечислены некоторые вопросы, которые могут вызвать у вас ненужное беспокойство: О Может ли значение CancelRoutine быть отлично от NULL при получении управления StartPacket? Лучше этого не допускать, потому что драйвер должен
270
Глава 5. Пакеты запросов ввода/выводе
удалить свою функцию отмены из IRP, прежде чем передавать IRP другому драйверу. В StartPacket включена директива ASSERT для проверки этого условия. Если вы запустите программу Driver Verifier для своего драйвера, она будет проверять, что в передаваемых вниз по стеку IRP указатель на функцию отмены обнулен. Однако Driver Verifier не следит за тем, чтобы это условие выполнялось и в IRP, передаваемых вашем}^ драйверу с верхнего уровня. О Может ли аргумент функции отмены StartPacket быть равным NULL? Лучше не стоит: как вы, вероятно, заметили, значительная часть описанной логики отмены зависит от того, равен ли указатель CancelRoutine NULL или нет. Для проверки этого условия в StartPacket включена директива ASSERT.
О Возможен ли двукратный вызов loCancellrp? Здесь нужно подумать о том, что флаг Cancel может быть установлен в IRP в результате предшествующих вызовов loCancellrp и кто-то вызовет loCancellrp еще раз (люди так нетерпеливы) при активном StartPacket. Однако данная ситуация ничем не угрожает, потому что наша первая проверка флага Cancel происходит после установки нашего собственного указателя на функцию отмены. В этой гипотетической ситуации обнаруживается, что флаг равен TRUE, и функция loSetCancelRoutine вызывается во второй раз. Либо loCancellrp, либо наш код выигрывает «гонку» за сброс указателя в NULL, и победитель завершает IRP. Таким образом, пережитки предшествующих вызовов попросту несущественны.
Отмена пакетов IRP, созданных или обрабатываемых в вашем коде
Иногда бывает необходимо отменить IRP, который вы создали в своем коде или передали другому драйверу. Вы должны действовать чрезвычайно осторожно, чтобы избежать возникновения одной нетривиальной маловероятной проблемы. Просто для конкретности предположим, что вы хотите установить общий 5-секундный тайм-аут на синхронную операцию ввода/вывода. По истечении этого периода операцию необходимо отменить. Далее приводится наивный код, который, казалось бы, успешно решает эту задачу:
SomeFunctlonO
{
KEVENT event;
IO_STATUS_BLOCK losb;
KeIn1t1al1zeEvent(&event, ...);
PIRP Irp = IoBuildSynchronousFsdRequest(.... &event, &1osb);
NTSTATUS status = loCallDrlver(DevIceObject, Irp);
If (status == STATUSJENDING)
{
LARGEJNTEGER timeout;
timeout.QuadPart = -5 * 10000000;
If (KeWaitForS1ngleObject(&event. Executive, KernelMode.	// A
FALSE, Stimeout) == STATUSJ1MEOUT)
Если бы не многозадачность...
271
loCancellrp!Irp): // <== Так нельзя!
KeWaitForSingleObject(&event, Executive, Kernel Mode.	// В
FALSE, NULL):
}
}
Первый вызов KeWaitForSingleObject (А) переходит к ожиданию по одному из двух вариантов. Во-первых, IRP может быть завершен; в этом случае запускается код зачистки I/O Manager, который устанавливает event.
Во-вторых, интервал тайм-аута может истечь до того, как кто-то завершит IRP. В этом случае KeWaitForSingleObject возвращает STATUSjnMEOUT. Вскоре после этого пакет 1RP должен быть завершен по одному из двух путей. Первый путь завершения возникает в ситуации, когда сторона, обрабатывающая 1RP, почти завершила свою работу к моменту тайм-аута и уже вызвала (или собирается вызвать в самом ближайшем будущем) loCompleteRequest. Второй путь завершения проходит через функцию отмены, которая, как мы должны предположить, была установлена нижним драйвером. Эта функция отмены должна завершить ORP. Как говорилось ранее, мы должны доверять другим компонентам режима ядра и полагаться на то, что они выполнят свою работу; таким образом, мы должны положиться на то, что сторона, которой мы отправили IRP, его вскоре завершит. Независимо от выбора пути логика завершения I/O Manager устанавливает event и сохраняет завершающий статус IRP в iosb. Второй вызов KeWaitForSingleObject (В) предотвращает преждевременный выход объектов event и iosb из области видимости. Без второго вызова мы могли бы вернуть управление из функции, что, фактически, приведет к удалению event и iosb. В итоге I/O Manager изменит содержимое памяти, принадлежащей другой функции.
Проблема, связанная с этим кодом, крайне маловероятна. Представьте, что кому-то удалось вызвать loCompleteRequest для этого IRP в тот самый момент, когда мы решили отменить его выловом loCancellrp. Например, операция завершилась вскоре после того, как первый вызов KeWaitForSingleObject прервался по 5-секундному тайм-ауту. Если вызов loFreelrp произойдет до того, как loCancellrp завершит работу с IRP, функция loCancellrp непреднамеренно испортит память при обращении к полям IRP Cancellrql, Cancel и CancelRoutine. В зависимости от точной последовательности событий также может оказаться, что указатель CancelRoutine будет сброшен в процессе подготовки к завершению IRP и функция отмены вступит в «гонку» с процессом завершения.
Описанный мной сценарий крайне маловероятен. Но как однажды кто-то — Джеймс Тербер, кажется? — сказал насчет вероятности быть съеденным тигром на главной улице города (один шанс из миллиона, насколько я помню): «Одного вполне достаточно». Обнаружить такие ошибки практически невозможно, поэтому лучше изначально предотвратить их. Я покажу два способа безопасной отмены ваших IRP. Один способ предназначен для синхронных, а другой — для асинхронных IRP.
272
Глава 5. Пакеты запросов ввода/вывода
НЕ ДЕЛАЙТЕ ЭТОГО...---------------------------------------------------------------------------
Одна распространенная, но устаревшая методика предотвращения ошибки «тигра на главной улице», описанной в тексте, основана на том факте, что в предыдущих версиях Windows вызов loFreelrp происходил в контексте АРС потока, создавшего IRP. Таким образом, программист мог убедиться в том, что выполнение производится в том же потоке, поднять IRQL до уровня APC.LEVEL, проверить, не завершен ли еще запрос, и, если не завершен, вызвать loCancellrp. Тем самым обеспечивалась блокировка АРС и потенциально опасного вызова loFreelrp.
Однако нельзя быть уверенным в том, что будущие версии Windows всегда будут использовать АРС для выполнения зачистки синхронных IRP. Следовательно, подъем IRQL до уровня APCJ-EVEL не может рассматриваться как способ предотвращения «гонки» между loCancellrp и loFreelrp.
Отмена «своих» синхронных IRP
В примере из предыдущего раздела показана функция, которая создает синхронный IRP, отправляет его другому драйверу, а затем не более 5 секунд ожидает завершения IRP. Главное, чего необходимо добиться при решении проблемы «гонки» между loFreelrp и loCancellrp, — это предотвратить вызов loFreelrp до того, как будут отработаны все возможные вызовы loCancellrp. Для этого мы воспользуемся функцией завершения, возвращающей STATUSJ4ORE_PROCESSING__REQUIRED;
SomeFunctionO
{
KEVENT event;
IO_STATUS_BLOCK losb;
KelnitializeEventf&event. ..);
PIRP Irp = IoBuildSynchronousFsdRequest(.... &event, &1osb);
loSetCompletionRoutineCIrp, OnComplete, (PVOID) &event, TRUE, TRUE, TRUE);
NTSTATUS status = loCalIDriver(...);
if (status == STATUS-PENDING)
{
LARGE_INTEGER timeout;
timeout. QuadPart = -5 * 10000000;
if (KeWaitForSingleObject(&event. Executive, KernelMode. // A
FALSE, ^timeout) — STATUSJIMEOUT)
{
loCancellrp(Irp); // <== Допустимо в этом контексте
KeWaitForSingleObject(Sevent. Executive. KernelMode. // В FALSE. NULL);
}
}
loCompleteRequestdrp, IO_NO_INCREMENT);
}
NTSTATUS OnComp1ete(PDEVICE_OBJECT junk, PIRP Irp, PVOID pev)
{
if (Irp->PendingReturned)
KeSetEvent!(PKEVENT) pev, IO_NO_INCREMENT, FALSE);
return STATUS_MORE_PROCESSING_REQUIRED;
}
Если бы не многозадачность...
273
Новый код, выделенный жирным шрифтом, предотвращает возможную «гонку». Допустим, loCallDriver возвращает STATUS_PENDING. В обычном случае операция завершится нормально, и драйвер нижнего уровня вызовет loCompleteRequest Наша функция завершения получает управление и устанавливает событие, которого ожидает основная линия выполнения. Поскольку функция завершения возвращает STATUS_MORE_PROCESSING_REQUIRED, функция loCompleteRequest прекращает обработку этого IRP. В конечном счете мы получаем управление в функции SomeFunction и видим, что наша операция ожидания (с меткой А) завершилась нормально. Однако зачистка IRP еще не выполнена, поэтому мы должны вызвать loCompleteRequest повторно, чтобы привести в действие нормальный механизм зачистки.
Теперь допустим, что мы решили отменить IRP; с учетом возможного появления «тигра на главной улице» необходимо побеспокоиться о том, чтобы вызов loFreelrp не освободил IRP в самый неподходящий момент. Первая операция ожидания (А) завершается с кодом STATUS_TIMEOUT — это приводит к запуску второй операции ожидания (В). Функция завершения устанавливает событие, по которому ведется ожидание. Кроме того, она предотвращает запуск механизма зачистки, возвращая код STATUS_MORE_PROCESSING_REQUIRED. loCancellrp может сколько угодно изменять наш IRP — это не причинит никакого вреда. IRP не может быть освобожден до второго вызова loCompleteRequest из основной линии выполнения, а это не может произойти до благополучного возврата из loCancellrp.
Обратите внимание: функция завершения в этом примере вызывает KeSet-Event только при установленном флаге IRP PendingReturned, показывающем, что диспетчерская функция нижнего драйвера вернула STATUS_PENDING. Условная проверка на этом шаге — оптимизация, избегающая потенциально затратной операции установки события в тех случаях, когда SomeFunction не ожидает события.
И последнее замечание в связи с приведенным кодом. Вызов loCompleteRequest в самом конце функции инициирует процесс, включающий установку event и iosb при условии завершения IRP с кодом успеха. В первом издании книги в этом месте присутствовал дополнительный вызов KeWaitForSingleObject, который гарантировал, что event и iosb не выйдут из области видимости до того, как I/O Manager закончит работу с ними. Один из рецензентов отметил, что функция, ссылающаяся на event и iosb, уже будет запущена к моменту выхода из loCompleteRequest, соответственно, дополнительное ожидание оказывается лишним.
Отмена «своих» асинхронных IRP
Чтобы безопасно отменить IRP, созданный функцией loAllocatelrp или loBuild-AsynchronousFsdRequest, можно придерживаться следующего генерального плана. Сначала определите пару дополнительных полей в структуре расширения устройства:
typedef struct _DEVICE__EXTENSION {
PIRP Thelrp;
ULONG Cancel Flag;
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
274
Глава 5. Пакеты запросов ввода/вывода
Инициализируйте эти ноля непосредственно перед вызовом loCallDriver для запуска IRP:
pdx->TheIrp = IRP:
pdx->CancelFlag = 0;
IoSetComplet1onRout1ne(Irp,
(PIO_COMPLETION_ROUTINE) CompleteonRoutIne,
(PVOID) pdx, TRUE, TRUE. TRUE);
loCal1Driver(..., Irp);
Если позднее этот IRP потребуется отменить, это делается так:
VOID CancelTheIrp(PDEVICE_EXENSION pdx)
{
PIRP Irp =	X/1
(PIRP) InterlockedExchangePointer((PVOID*)&pdx->TheIrp, NULL);
if (Irp)
{
loCancellrp(Irp);
if (InterlockedExchange(&pdx->CancelFlag, 1)	// 2
loFreelrp(Irp);	// 3
}
}
Следующая функция работает в сочетании с функцией завершения, установленной для IRP:
NTSTATUS CompletionRoutine(PDEVICE_OBJECT junk. PIRP Irp.
PDEVICEJXTENSION pdx)
{
if (InterlockedExchangePointer(&pdx->TheIrp,	NULL)	// 4
InterlockedExchange(&pdx->CancelFlag, 1))	//5
loFreelrp(Irp);	// 6
return STATUS JOREJROCESS ING_REQU I RED ; }
Идея, лежащая в основе этого обманчиво простого кода, заключается в том, что функция, которая видит IRP последней (CompletionRoutine или CancelThelrp), осуществляет необходимый вызов loFreelrp в точке (3) или (6). Вот как это происходит:
О Нормальный случай ~ когда вы не пытаетесь отменить IRP. Сторона, которой был отправлен пакет IRP, в конечном итоге завершает его, и тогда управление передается вашей функции завершения. Первый вызов InterlockedExchange-Pointer (4) возвращает ненулевой адрес IRP. Поскольку этот адрес отличен от NULL, компилятор обходит ненужную проверку логического выражения и переходит к вызову loFreelrp. Все последующие вызовы CancelThelrp находят указатель IRP равным NULL (1) и ничего не делают.
О Другой легко анализируемый случай: функция CancelThelrp вызывается задолго до того, как кто-то приступит к завершению IRP, никаких «гонок» при этом не возникает. В точке (1) указатель Thelrp обнуляется. Поскольку указатель
Если бы не многозадачность...
275
на IRP ранее был отличен от NULL, выполнение переходит к loCancellrp. В этой ситуации наш вызов loCancellrp заставит кого-то в скором времени завершить IRP, и тогда будет выполнена наша функция завершения. Так как указатель Thelrp содержит NULL, проверка переходит ко второй части логического выражения. Сторона, первой выполнившая InterlockedExchange с CancelFlag, получает 0 и пропускает вызов loFreelrp. Та сторона, которая это сделает второй, получает 1 и вызывает loFreelrp.
О А теперь тот случай, о котором мы беспокоились: предположим, кто-то завершает IRP примерно в то время, когда CancelThelrp пытается отменить его. Самое худшее, что может произойти, — это если наша функция завершения запустится до того, как мы успеем вызвать loCancellrp. Функция завершения видит Thelrp равным NULL и поэтому заменяет CancelFlag на 1. Как и в предыдущем случае, функция получает возвращаемое значение 0 и пропускает вызов loFreelrp. loCancellrp может безопасно работать с IRP. (Скорее всего, она просто вернет управление без вызова функции отмены, потому что сторона, завершившая IRP, наверняка сначала сбросит указатель CancelRoutine в NULL.) Эта методика привлекает своей элегантностью: она полагается исключительно на атомарные операции и не нуждается в потенциально затратных примитивах синхронизации.
Отмена «чужих» IRP
В завершение нашего обсуждения отмены IRP предположим, что кто-то отправил вам пакет IRP, который вы пересылаете другому драйверу. Может возникнуть ситуация, в которой этот пакет потребуется отменить, — скажем, вы хотите избавиться от этого IRP, чтобы перейти к операции отключения питания. А может быть, вы ожидаете завершения IRP в синхронном режиме и хотите установить тайм-аут, как в первом примере этого раздела.
Чтобы предотвратить «гонку» loCancellrp/loFreelrp, необходимо установить собственную функцию завершения. Дальнейшие детали программирования зависят от того, собираетесь ли вы ждать завершения IRP.
Отмена «чужих» IRP с ожиданием
Допустим, ваша диспетчерская функция передает [RP и синхронно ожидает его завершения (пример см. в сценарии 7 в конце главы). Используйте код следующего вида для отмены пакета IRP, если он завершается недостаточно быстро для вас:
NTSTATUS DispatchSomething(PDEVICE OBJECT fdo, PIRP Irp)
{
PDEVICE-EXTENSION pdx =
(PDEVICEJXTENSION) fdo ->Devi ceExtens 1 on;
KEVENT event;
KelnitlalizeEventC&event, NotificationEvent, FALSE);
IoSetCompletionRoutine(Irp, OnComplete, (PVOID) &event,
TRUE, TRUE, TRUE);
NTSTATUS status = loCalIDriver(...);
if (status == STATUS PENDING)
276
Глава 5. Пакеты запросов ввода/вывода
{
LARGEJNTEGER timeout;
timeout.QuadPart = -5 * 10000000:
if (KeWaitForSingleObjectt&event, Executive. Kernel Mode.
FALSE, &timeout) == STATUSJIMEOUT)
{
loCancellrp(Irp);
KeWaltForSingleObject(&event, Executive, Kernel Mode,
FALSE. NULL):
}
}
status = Irp->IoStatus.Status;
loCompleteRequest(Irp. 10__NO_ INCREMENT);
return status;
}
NTSTATUS OnComplete(PDEVICE_OBJECT junk, PIRP Irp, PVOID pev)
{
if (Irp->PendingReturned)
KeSetEventl(PKEVENT) pev, IO_NO_INCREMENT. FALSE):
return STATUS_MORE_PROCESSING_REQUIRED;
}
Код почти не отличается от того, что приводился ранее для отмены «своих» синхронных IRP. Единственное различие — присутствие диспетчерской функции, которая должна возвращать код состояния. Как и в предыдущем примере, мы устанавливаем собственную функцию завершения, чтобы процесс завершения не дошел до конца раньше, чем будет пройдена точка, в которой может быть вызвана функция loCancellrp.
Возможно, вы заметили, что в названии раздела не указан тип IRP — синхронный или асинхронный. Дело в том, что различия между двумя типами IRP важны только для того драйвера, который их создает. Драйверы файловой системы должны различать синхронные и асинхронные IRP в отношении работы с системным кэшем, но в драйверах устройств это усложнение обычно отсутствует. Для драйвера нижнего уровня важна возможность блокировки потока с целью синхронной обработки IRP, а это зависит от текущего уровня IRQL и от контекста выполнения (произвольный или фиксированный поток).
Отмена «чужих» IRP без ожидания
Предположим, вы переслали чей-то IRP другому драйверу, но не собираетесь дожидаться его завершения. По какой-то причине вы позднее решаете, что IRP нужно отменить:
typedef struct _DEVICE_EXTENSION {
PIRP Thelrp:
ULONG Cancel Flag;
} DEVICE-EXTENSION. *PDEVICE_EXTENSION;
NTSTATUS D1spatchSometh1ng(PDEVICE_OBJECT fdo. PIRP Irp)
Если бы не многозадачность...
277
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
loCopyCur rentIrpStacktocat1onToNext(Irp);
loSetCompletlonRoutinedrp, (PIO_COMPLETION_ROUTINE) OnComplete.
(PVOID) pdx,
TRUE, TRUE, TRUE);
pdx->CancelFlag = 0;
pdx->The!rp = Irp;
loMarklrpPendlngdrp);
loCa11Driver(pdx->LowerDeviceObject, Irp): return STATUS_PENDING;
}
VOID CancelTheIrp(PDEVICE_EXTENSION pdx)
PIRP Irp = (PIRP) InterlockedExchangePointer(
(PVOID*) &odx->TiieIrp, NULL);
if (Irp)
{
loCancellrp(Irp);
if (InterlockedExchange(&pdx->CancelFlag, 1)) loCompleteRequest(Irp, I0_N0_INCREMENT):
}
NTSTATUS OnComplete(PDEVICE_OBJECT fdo. PIRP Irp.
POEVICE_EXTENSION pdx)
{
if (InterlockedExchangePointerUPVOID*) &pdx->TheIrp, NULL)
InterlockedExchange(8pdx->CancelFlag, 1))
return STATUS_SUCCESS;
return STATUS_MORE_PROCESSING_REQUIRED;
}
Этот код напоминает тот, который я приводил ранее для отмены «своих» асинхронных IRP. Однако на этот раз вместо вызова loFreelrp при работе со «своим» IRP мы даем возможность loCompleteRequest завершить обработку IRP. Если функция отмены окажется последней, она вернет STATUS_SUCCESS, чтобы дать возможность loCompleteRequest закончить процедуру завершения IRP. Если же последней окажется функция CancelThelrp, она вызывает loCompleteRequest для возобновления процедуры завершения, обойденной функцией завершения при возврате STATUSJ4ORE_PROCESSING__REQUIRED.
В этом примере следует обратить внимание на чрезвычайно тонкий нюанс — вызов loMarklrpPending. В обычных случаях этот шаг может безопасно выполняться условно в функции завершения, но не в данной ситуации. Если функция CancelThelrp будет вызвана в контексте потока, отличного от того, в котором выполняется диспетчерская функция, флаг незаконченного IRP понадобится для того, чтобы функция loCompleteRequest запланировала вызов АРС для зачистки IRP в соответствующем потоке.
278
Глава 5. Пакеты запросов ввода/вывода
Обработка IRP_MJ_CLEANUP
С отменой IRP тесно связана другая тема — запросы ввода/вывода с основным кодом IRP_MJ_CLEANUP. Но чтобы объяснить, как обрабатываются такие запросы, я должен привести немного общей информации.
Когда приложение или другой драйвер хочет обратиться к устройству, он сначала открывает манипулятор устройства. Приложения вызывают функцию CreateFile, драйверы вызывают ZwCreateFile. Во внутреннем представлении эти функции создают объект файла режима ядра и отправляют его драйверу в запросе IRP_MJ_CREATE. Завершив взаимодействие с драйвером, сторона, открывшая манипулятор, вызывает другую функцию — такую как CloseHandle или ZwClose. Внутренние реализации этих функций отправляют драйверу запрос IRP_MJ _ CLOSE. Но непосредственно перед отправкой IRP_MJ__CLOSE I/O Manager отправляет запрос IRP_MJ_CLEANUP, чтобы вы могли отменить все IRP, принадлежащие тому же объекту файла, но до сих пор находящиеся в одной из ваших очередей. С точки зрения драйвера, у этих запросов имеется общая черта: получаемая вами позиция стека во всех случаях указывает на один и тот же объект файла.
На рис. 5.10 показана примерная схема ваших действий при получении IRP_ MJ_CLEANUP. Вы должны перебрать содержимое очередей IRP и удалить пакеты, помеченные как принадлежащие тому же объекту файла. Такие IRP должны завершаться с кодом STATUS-CANCELLED.
IRP в очереди
I/O Manager закрывает объект файла
Рис. 5.10. Действия драйвера при обработке IRP_MJ_CLEANUP
Если бы не многозадачность...
279
БЪЕКТЫ ФАЙЛОВ--------------------------------------------------------------------
Обычно только один драйвер в стеке устройства (как правило, функциональный) реализует все три запроса: IRP„MJ_CREATE, IRP_MJ_CLOSE и IRP_MJ_CLEANUP. I/O Manager создает объект файла (стандартный объект ядра) и передает его в стек ввода/вывода диспетчерских функций для всех трех типов IRP. Любая сторона, отправляющая IRP устройству, должна располагать указателем на тот же объект файла и должна вставлять его в стек ввода/вывода. Драйвер, обрабатывающий эти три IRP, в определенном смысле является «владельцем» объекта файла — ему предоставляется право использовать поля FsContext и FsContext2 объекта. Таким образом, ваша функция DispatchCreate может разместить в этих полях какую-либо информацию, которая будет использоваться другими диспетчерскими функциями, а также в процессе итоговой зачистки функцией DispatchCJose.
С правильным использованием IRP_MJ_CLEANUP легко запутаться. Более того, некоторые программисты, плохо понимающие механизм отмены IRP, решают (неправильно) попросту игнорировать этот тип IRP. Однако в драйвере должна быть реализована как логика отмены, так и логика зачистки:
О IRP_MJ_CLEANUP означает, что манипулятор закрывается. Вы должны уничтожить все IRP, относящиеся к этому манипулятору;
О I/O Manager и другие драйверы отменяют отдельные IRP по разным причинам, не имеющим ничего общего с закрытием манипуляторов;
О одна из ситуаций, в которых I/O Manager отменяет IRP, возникает при завершении потока. Потоки часто завершаются из-за завершения своих родительских процессов, и I/O Manager также автоматически закрывает все манипуляторы, остающиеся открытыми при завершении процесса. Сходство между отменой такого рода и автоматическим закрытием манипулятора создает неправильное впечатление, что в драйвере достаточно обеспечить поддержку только одной концепции.
В этой книге я представлю два способа простой реализации поддержки IRP_ MJ_CLEANUP: для тех, кто использует мои объекты DEVQUEUE, и для пользователей защищенных очередей Microsoft.
Зачистка с использованием DEVQUEUE
Если для организации очередей IRP применяется объект DEVQUEUE, диспетчерская функция IRP__MJ_CLEANUP получается на удивление простой:
NTSTATUS DispatchCleanup(PDEVICE__OBJECT fdo. PIRP Irp)
{
PDEVICEJEXTENSION pdx =
(PDEVICE^EXTENSION) fdo->DeviceExtension:
PIO-STACKJ-OCATION stack = loGetCurrentlrpStackLocation(Irp);
PFILE_OBJECT fop = stack->F11e0bject;
CleanupRequests(&pdx->dqReadWrIte, fop,
STATUSCANCELLED);
return CompleteRequest(Irp, STATUS_SUCCESS, 0);
280
Глава 5. Пакеты запросов ввода/вывода
Функция CleanupRequests удаляет из очереди все IRP, принадлежащие тому же объекту файла, и завершает их с кодом STATUS-CANCELLED. Затем сам запрос IRP_MJ__CLEANUP завершается с кодом STATUS-SUCCESS.
Обо всех подробностях должна позаботиться функция CleanupRequests:
VOID CleanupRequests(PDEVQUEUE pdq, PFILE_OBJECT fop, NTSTATUS status)
LIST_ENTRY cancel 11 st;
InltlallzeLlstHeadC&cancellist);	// 1
KIRQL oldirql;
KeAcquIreSpInLock(&pdq->lock, &oldirql);
PLIST-ENTRY first = &pdq->head;
PLISTJNTRY next;
for (next = flrst->FlInk; next != first; )	// 2
{
PIRP Irp - CONTAINING-RECORD(next, IRP,
Tall.Overlay.LIstEntry);
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocatlon(Irp); // 3
PLIST_ENTRY current = next;	// 4
next = next->F1Ink;
If (fop && stack->F11eObject != fop) continue;
If (!loSetCancelRout1ne(Irp, NULL))	// 5
continue; RemoveEntryLIst(current);	// 6
InsertTal1 LI st(^cancel list, current); } KeReleaseSpinLock(&pdq->lock, oldirql);	// 7
while (!IsListEmpty(Scancellist)) { next = RemoveHeadL1st(&cancell1st): PIRP Irp = CONTAINING_RECORD(next, IRP. Tall.Overlay.LlstEntry); Irp->IoStatus.Status = status; loCompleteRequest(Irp, IO_NO_INCREMENT);	
}
}
1.	Используемая стратегия основана на перемещении отменяемых IRP в приватную очередь, защищенную спин-блокировкой. Соответственно, мы прежде всего инициализируем приватную очередь и захватываем спин-блокировку.
2.	Содержимое очереди перебирается в цикле до возврата к началу списка. Обратите внимание на отсутствие приращения цикла (третьего компонента команды for). Вскоре я объясню, почему лучше обойтись без явно заданного приращения.
3.	Если функция вызывается для содействия обработке IRP-MJ-CLEANUP, аргумент fop содержит адрес закрываемого объекта файла. Мы должны отделить IRP, относящиеся к тому же объекту файла, но для этого сначала нужно найти позицию стека.
Если бы не многозадачность...
281
4.	Если мы решим удалить этот IRP из очереди, в дальнейшем у нас не будет простого способа найти следующий IRP в главной очереди. По этой причине управляющая переменная изменяется на этом шаге в теле цикла.
5.	Эта особенно умная команда появилась благодаря любезности Джейми Ханрахана (Jamie Hanrahan). Мы должны принять меры на тот случай, если кто-нибудь попытается отменить IRP, рассматриваемый в ходе текущей итерации. Отмена дойдет лишь до точки, в которой CancelRequest попытается захватить спин-блокировку. Но перед этим в loCancellrp обязательно будет выполнена команда, обнуляющая указатель на функцию отмены. Следовательно, если мы обнаруживаем, что при вызове loSetCancelRoutine указатель равен NULL, можно быть уверенным в том, что кто-то пытается отменить этот IRP. Просто пропуская пакет в ходе итерации, мы разрешаем функции отмены завершить его.
6.	IRP выводится из главной очереди и перемещается в приватную очередь.
7.	После того как IRP будет перемещен в приватную очередь, спин-блокировку можно снимать. Далее все перемещенные IRP отменяются.
Зачистка в защищенных очередях
Чтобы легко зачистить все IRP, поставленные в очередь вызовом loCsqlnsertlrp, следуйте простой конвенции: если параметр контекста функции loCsqRemoveNextlrp отличен от NULL, он должен содержать адрес FILE_OBJECT. Ваша функция IRP_ MJ_CANCEL будет выглядеть примерно так (сравните с примером Cancel в DDK):
NTSTATUS D1spatchCleanup(PDEVlCE_OBJECT fdo. PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICE-EXTENSION) fdo->DeviceExtensIon;
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocatlon(Irp);
PFILEJBJECT fop = stack->F4eObject;
PIRP qlrp;
while ((qlrp = IoCsqRemoveNextIrp(&pdx->csq, fop))) CompleteRequest(q1rp, STATUS__CANCELLED. 0);
return CompleteRequestdrp, STATUS_SUCCESS. 0);
}
Функция обратного вызова PeekNextlrp реализуется так:
PIRP PeekNextlrpCPIO CSQ esq, PIRP Irp, PVOID PeekContext)
{
PDEVICE_EXTENSION pdx = GET_DEVICE_EXTENSION(csq):
PLIST_ENTRY next = Irp ? Irp->Tail.Overlay.LIstEntry.Flink
: pdx->IrpQueueAnchor.F11nk;
while (next != &pdx->IrpQueueAnchor)
{
PIRP Nextlrp = CONTAINING_RECORD(next. IRP,
Tail.Overlay.LIstEntry);
PIO_STACK_LOCATION stack =
IoGetCurrentIrpStackLocat1 on(NextIrp);
282
Глава 5. Пакеты запросов ввода/вывода
If ('PeekContext (PFILE_OBJECT) PeekContext
== stack->FiieObject)
return NextIrp;
next = next->F1Ink;
}
return NULL;
}
Восемь сценариев обработки IRP
Несмотря на обширные объяснения, обработка IRP в действительности весьма проста. Насколько я могу судить, на практике встречаются всего восемь существенно различающихся сценариев, и код реализации этих сценариев достаточно прост. В последнем разделе этой главы я собрал диаграммы и примеры кода, которые помогут вам окончательно разобраться в теоретическом материале.
Поскольку этот раздел задуман как «сборник рецептов», которые могут использоваться без полного понимания всех тонкостей, я включил в приводимый код вызовы снятия блокировки, которые будут подробно рассматриваться в главе 6. Условное обозначение IoSetCompletionRoutine[Ex] помечает те места, в которых для установки функции завершения следует использовать функцию loSetCompletionRoutineEx, если она поддерживается системой. Я также использовал перегруженную версию своей вспомогательной функции CompleteRequest, которая не изменяет loStatus.Information в этих примерах, потому что это будет правильно для IRP_MJ_PNP и не будет неправильно для других типов IRP.
Сценарий 1: передача вниз с функцией завершения
В этом сценарии кто-то отправляет вашему драйверу IRP. Вы перенаправляете IRP драйверу нижнего уровня в стеке РпР и выполняете некоторую заключительную обработку в функции завершения (рис. 5.11). Данная стратегия применяется при соблюдении двух следующих условий:
О IRP может поступить на уровне DISPATCH_LEVEL и в контексте произвольного потока (то есть блокировка на время обработки IRP драйверами нижнего уровня невозможна).
О Если потребуется, заключительная обработка может выполняться на уровне DISPATCH_LEVEL (так как функции завершения могут вызываться на уровне DISPATCH_LEVEL).
Основа кода диспетчерской функции и функции завершения выглядит так:
NTSTATUS DispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICEJXTENSION) fdo->DeviceExtension:
NTSTATUS status = IoAcqu1reRemoveLock(&pdx->RernoveLock, Irp);
восемь сценариев обработки IRP
283
if (!NT_SUCCESS(status))
return CompleteRequestdrp, status);
IoCopyCurrentIrpStackLocatlonToNext(Irp);
loSetCompletionRoutlne(Irp,
(PIOJZOMPLETION_ROUTINE) CompletionRout1ne,
pdx. TRUE, TRUE, TRUE):
return IoCal10r1ver(pdx->LowerDev1ce0bject. Irp);
}
NTSTATUS CompletwnRoutine(PDEVICEJBJECT fdo, PIRP Irp, PDEVICEJXTENSION pdx) {
if (Irp->PendingReturned)
IoMa rkIrpPendi ng(1rp);
whatever post processing you wanted to do>
IoReleaseRemoveLock(&pdx->RemoveLock, Irp);
return STATUSJUCCESS;
}
IRP, созданный другой стороной
Рис. 5.11. Передача вниз с функцией завершения
Сценарий 2: передача вниз без функции завершения
В этом сценарии кто-то отправляет вашему драйверу IRP. Вы перенаправляете 1RP драйверу нижнего уровня в стеке РпР, но далее с IRP делать ничего не нужно (рис. 5.12). Данная стратегия применяется при соблюдении двух следующих условий:
Э пакет IRP получен от внешнего источника (а не создается вами);
Э ваш драйвер не обрабатывает IRP, но, возможно, это захочет сделать драйвер более низкого уровня.
Сценарий часто применяется в фильтрующих драйверах, которые ограничиваются простой передачей всех IRP, не представляющих интереса для данного драйвера.
284
Глава 5. Пакеты запросов ввода/вывода
IRP, созданный другой стороной
Рис. 5.12. Передача вниз без функции завершения
Я рекомендую написать следующую вспомогательную функцию, которая упростит применение этой стратегии:
NTSTATUS ForwardAndForget(PDEVICE_EXTENSION pdx, PIRP Irp)
PDEVICE-EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension;
NTSTATUS status ~ IoAcquireRemoveLock(&pdx->RemoveLock, Irp);
If (!NT_SUCCESS(status))
return CompleteRequest(Irp, status);
loSkipCurrentlrpStackLocation (Irp);
status = IoCallDriver(pdx->LowerDev1ce0bject, Irp):
IoReleaseRemoveLock(&pdx->RernoveLock, Irp)-;
return status;
}
Сценарий 3: завершение в диспетчерской функции
В этом сценарии драйвер немедленно завершает IRP, отправленный внешним источником (рис. 5.13). Условия для применения этой стратегии:
О пакет IRP получен от внешнего источника (а не создается вами);
О возможна немедленная обработка IRP, как для многих разновидностей управляющих запросов ввода/вывода (IOCTL). Или...
Q с пакетом IRP что-то очевидно не так, и немедленный отказ — лучшее, что возможно в этой ситуации.
«Скелет» диспетчерской функции выглядит так:
NTSTATUS DispatchSometh1ng(PDEVICE_0BJECT fdo, PIRP Irp)
{
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) fdo->0ev1ceExtension;
<process the IRP>
;мь сценариев обработки IRP
285
Irp->IoStatus.Status - STATUS_XXX;
Irp->IoStatus.Information = YYY; loCompleteRequestdrp, IO_NO_INCREMENT) return STATUS_XXX;
}
IRP, созданный другой стороной
Рис. 5.13. Завершение в диспетчерской функции
денарий 4: постановка в очередь я последующей обработки
В этом сценарии кто-то отправляет вам пакет IRP, который вы не можете обработать немедленно. IRP помещается в очередь для последующей обработки в функции Startlo (рис. 5.14). Стратегия применяется при соблюдении двух следующих условий:
О пакет IRP получен от внешнего источника (а не создается вами);
О вы не знаете заранее, возможна ли немедленная обработка IRP. Это часто бывает с IRP, требующими последовательного доступа к оборудованию (скажем, при чтении и записи).
IRP, созданный другой стороной
Рис. 5.14. Очередь для последующей обработки
286
Глава 5. Пакеты запросов ввода/вывода
При всем обилии вариантов типичный способ реализации этого сценария основан на управлении очередью IRP при помощи объекта DEVQUEUE. Приводимые далее фрагменты кода демонстрируют взаимодействие различных частей драйвера устройства с программируемыми прерываниями ввода/вывода. Части, непосредственно относящиеся к обработке IRP, выделены жирным шрифтом:
typedef struct JOICEJXTENSION {
DEVQUEUE dqReadNrite,
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject.
PDEVICE_OBJECT pdo)
{
In1tial1zeQueue(&pdx->dqReadWrite, Startlo);
IoIn1t1aHzeDpcRequest(fdo, (PIOJ)PC_ROUT1NE) DpcForlsr);
}
NTSTATUS DispatchReadWrite(PDEVICEJ)BJECT fdo, PIRP Irp)
{
PDEVICE-EXTENSION pdx =
(РОЕVICEEXTENSION) fdo->Devi ceExtens ion:
loMarklrpPending(Irp);
StartPacket(&pdx->dqReadWrite( fdo, Irp, Cancel Routine);
return STATUS_PENDING:
}
VOID CancelRoutinelPDEVICEOBJECT fdo, PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICE EXTENSION) fdo->DeviceExtension:
CancelRequest(todx->dqReadWrite, Irp);
}
VOID StartIo(PDEVICE_OBJECT fdo. PIRP Irp) {
BOOLEAN OnInterruptIPKINTERRUPT junk, PDEVICEJXTENSION pdx) {
PIRP Irp = GetCurrentIrp(&pdx->dqReadWr1te);
Irp->IoStatus.Status = STATUS_XXX;
Irp->IoStatus.Information = YYY;
IoRequestDpc(pdx->DeviceObject. NULL, pdx);
}
VOID DpcForlsrIPKDPC junkl. PDEVICE_OBJECT fdo. PIRP junk2.
Восемь сценариев обработки IRP
287
PDEVICEJXTENSION pdx) {
PIRP Irp = GetCurrentIrp(&pdx->dqReadWnte);
StartNextPacket(&pdx->dqReadWrite, fdo); loCompleteRequest(Irp, IO_NO_INCREMENT);
Денарий 5: создание асинхронных IRP
В этом сценарии вы создаете асинхронный пакет IRP, который пересылается другому драйверу (рис. 5.15). Данная стратегия применяется при соблюдении двух условий:
Э имеется другой драйвер, выполняющий операцию по вашему поручению;
Э выполнение ведется либо в контексте произвольного потока (в котором блокировка нежелательна), либо на уровне DISPATCH_LEVEL (на котором блокировка невозможна).
Рис. 5.15. Создание асинхронного IRP
Далее приводится примерный код, включаемый в ваш драйвер. Он не обязан находиться в диспетчерской функции IRP, а целевой объект устройства не обязан быть следующим нижним объектом в стеке РпР. За подробными описаниями функ-15Ш loBuildAsynchronousFsdRequest и loAllocatelrp обращайтесь к документации DDK
SOMETYPE SomeFunc-tionCPDEVICE__EXTENSION pdx,
PDEVICE-OBJECT Deviceobject)
{
NTSTATUS status = IoAcqu1reRemoveLock(&pdx->RemoveLock,	// A
(PVOID) 42);
if (!NT_SUCCESS(status))	// A
return <status>;	//A
PIRP Irp;
Irp = IoBuildAsynchronousFsdRequest(IRP MJ XXX, DeviceObject,
288
Глава 5. Пакеты запросов ввода/вывода
-или-
Irp = loAl1ocatelгр(DevIceObject~>StackSize, FALSE);
PIO_STACK_LOCATION stack = loGetNextlrpStackLocation(Irp): stack->MajorFunction = IRP_MJ_XXX;
дополнительная инициализация>
loSetCompletionRoutine[Ex]([pdx->DeviceObject.] Irp,
(PIO COMPLETION ROUTINE) CompletionRoutine, TRUE. TRUE, TRUE);	pdx.		
ObReferenceObj ect(Devi ceObj ect);		//	В
IoCallDriver(DeviceObject, Irp); ObDereferenceObject(DeviceObject); }		//	В
NTSTATUS CompletionRoutine(PDEVICE_OBJECT junk.	PIRP Irp,		
PDEVICE EXTENSION pdx) { <зачистка IRP -- см. ниже> loFreelrp(Irp); loReleaseRemoveLock(&pdx->RemoveLock, (PVOID) return STATUS MORE PROCESSING REQUIRED; }	42);	//	A
Вызовы loAcquireRemoveLock и loReleaseRemoveLock (пункты А) необходимы только в том случае, если устройство, которому посылается IRP, является устройством нижнего уровня в стеке РпР. 42 — просто произвольный маркер; было бы слишком сложно пытаться устанавливать блокировку удаления только для того, чтобы иметь возможность использовать указатель на IRP в качестве маркера в отладочной версии.
Вызовы ObReferenceObject и ObDereferenceObject до и после вызова loCallDriver (в пунктах В) необходимы только тогда, когда функция loGetDeviceObjectPointer использовалась для получения DeviceObject, и функция завершения (или вызываемая ею функция) освобождает полученную ссылку на объект файла или устройства.
Коды (А) и (В) не используются одновременно — в программе присутствует либо одна ветвь, либо ни одной.
Если функция loBuildAsynchronousFsdRequest использовалась для построения пакетов IRP_MJ_READ или IRP_MJ_WRITE, функция завершения должна выполнять относительно сложную зачистку.
Зачистка для объектов DO_DIRECT_IO
Если в целевом объекте устройства обозначен метод буферизации DOJDIRECTJO. вам придется освободить списки MDL, выделенные I/O Manager для буфера данных:
NTSTATUS CompletionRoutine(...)
{
PMDL mdl;
while ((mdl = Irp->MdlAddress))
{
Irp->MdlAddress = mdl->Next;
Восемь сценариев обработки IRP
289
MniUnlockPagestmdl): // <== Только если ранее
// вызывалась функция MmProbeAndLockPages loFreeMdl(mdl):
}
loFreelrp(Irp);
«необязательное снятие блокировки удаления>
return STATUS_MORE_PROCESSING_REQUIRED;
}
Зачистка для объектов DO_BUFFERED_IO
Если в целевом объекте устройства обозначен метод буферизации DOJ3UFFEREDJO, I/O Manager создает системный буфер. Теоретически, ваша функция завершения должна скопировать данные из системного буфера в ваш буфер, а затем освободить системный буфер. К сожалению, необходимые для этого флаговые биты и поля в DDK не документированы. Мой совет — просто не отправляйте запросы чтения/записи напрямую драйверу, использующему буферизованный ввод/вы-вод. Вместо этого следует использовать функции ZwReadFile и ZwWriteFile.
Зачистка для других объектов
Если в целевом объекте устройства не обозначен ни один из методов DO_DIRECT_JO и DOJ3UFFERED_IO, дополнительной зачистки не потребуется. Вам повезло!
Сценарий 6: создание синхронных IRP
В этом сценарии вы создаете синхронный пакет IRP, который пересылается другому драйверу (рис. 5.16). Данная стратегия применяется при соблюдении следующих условий:
О имеется другой драйвер, выполняющий операцию по вашему поручению;
О вы должны дождаться завершения операции, чтобы продолжить работу.
Рис 5.16. Создание синхронного IRP
290
Глава 5. Пакеты запросов ввода/вывода
Далее приводится примерный код, включаемый в ваш драйвер. Он не обязан находиться в диспетчерской функции IRP, а целевой объект устройства не обязан быть следующим нижним объектом в стеке РпР. За подробными описаниями функций loBuildSynchronousFsdRequest и ToBuildDeviceloControlRequest обращайтесь к документации DDK.
SOMETYPE SomeFunct1on(PDEVICE_EXTENSI0N pdx,
PDEVICE-OBJECT DevIceObject)
{
NTSTATUS status = loAcquIreRemoveLock(&pdx->RemoveLock,	// A
(PVOID) 42);
If (!NT_SUCCESS(status))	// A
return <status>;	//A
PIRP Irp;
KEVENT event;
IO_STATUS_BLOCK losb;
KelnitializeEventC&event, NotlficationEvent, FALSE);
Irp = IoBuildAsynchronousFsdRequest(IRP_MJ_XXX,
DevIceObject, .... &event, &1osb);
-или-
Irp = loBulldDevIceloControlRequest(IOCTL_XXX, DevIceObject, .... &event, &1osb);
status = IoCallDr1ver(Dev1ceObject, Irp);
If (status == STATUS-PENDING)
{
KeWa1tForS1ng]eObject(&event, Executive, KernelMode,
FALSE, NULL);
status = losb.Status;
}
IoReleaseRemoveLock(8pdx->RemoveLock, (PVOID) 42);	// A •
}
Как и в сценарии 5, вызовы loAcquireRemoveLock и loReleaseRemoveLock (в пунктах А) необходимы только в том случае, если устройство, которому посылается IRP, является объектом устройства нижнего уровня в стеке РпР. 42 — просто произвольный маркер; было бы слишком сложно пытаться устанавливать блокировку удаления только для того, чтобы иметь возможность использовать указатель на IRP в качестве маркера в отладочной версии.
Этот сценарий будет часто использоваться в главе 12 для синхронной отправки блоков запросов USB (URB) вниз по стеку. В примерах, которые будут рассматриваться, это обычно будет происходить в контексте диспетчерской функции IRP, независимо устанавливающей блокировку удаления. По этой причине дополнительный код, связанный с блокировкой удаления, в этих примерах отсутствует.
Зачистка за такими IRP не выполняется! I/O Manager выполняет ее автоматически.
Восемь сценариев обработки IRP
291
Ценарий 7: синхронная передача вниз
В этом сценарии кто-то отправляет вам пакет IRP. Ваш драйвер синхронно передает его вниз по стеку РпР и продолжает работу (рис. 5.17). Используйте эту стратегию при соблюдении всех следующих условий:
Э пакет IRP получен от внешнего источника (а не создается вами);
О выполнение ведется на уровне PASSIVE-LEVEL в фиксированном потоке;
Э ваша заключительная обработка для этого IRP должна выполняться на уровне PASSIVE-LEVEL.
Рис. 5.17. Синхронная передача IRP вниз по стеку
Хороший пример ситуации, в которой необходимо использовать эту стратегию, встречается при обработке запросов РпР подвида IRP_MN_START_DEVICE.
Я рекомендую написать две вспомогательные функции, упрощающие выполнение синхронной передачи:
NTSTATUS ForwardAndWait(PDEVICE_EXTENSION pdx, PIRP Irp)
{
KEVENT event;
Kelnitial1ze(&event. NotificationRoutine, FALSE):
loCopyCurrentIrpStackLocati onToNext(Irp);
loSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE)
ForwardAndWaitCompletionRoutine, &event, TRUE. TRUE, TRUE);
NTSTATUS status = IoCallDr1ver(pdx->LowerDevice0bject, Irp);
if (status == STATUSJENDING)
{
KeWa1tForSingleObject(&event, Executive. KernelMode, FALSE, NULL);
status = Irp->IoStatus.Status;
}
return status:
}
NTSTATUS ForwardAndWaitCompletionRoutine(PDEVICE_OBJECT fdo, PIRP Irp. PREVENT pev)
292
Глава 5. Пакеты запросов ввода/вывода
{
1f (I гр->Pend1ngReturned)
KeSetEventtpev, IO__NO_INCREMENT, FALSE);
return STATUSJOREJROCESSINGJEQUIRED;
}
Вызывающая сторона этой функции должна вызвать loCompleteRequest для IRP, захватить и освободить блокировку удаления. Логику блокировки удаления не следует размещать в ForwardAndWait, потому что вызывающая сторона может и не захотеть так быстро освобождать блокировку.
Обратите внимание: все эти действия инкапсулированы в функции Windows ХР DDK loForwardlrpSynchronously.
Сценарий 8: синхронная обработка асинхронных IRP
В этом сценарии вы создаете асинхронный пакет IRP, пересылаете его другом} драйверу и ожидаете завершения IRP (рис. 5.18). Данная стратегия применяется при соблюдении следующих условий:
О имеется другой драйвер, выполняющий операцию по вашему поручению;
О вы должны дождаться завершения операции, прежде чем продолжать работу; О выполнение ведется на уровне APC_LEVEL в контексте фиксированного потока.
Рис. 5.18. Синхронная обработка асинхронных IRP
Я применяю эту методику в ситуациях, когда я захватил быстрый мьютекс и должен выполнить синхронную операцию. В программе сочетаются уже встречавшиеся ранее элементы (сравните со сценариями 5 и 7):
SOMETYPE SomeFunction(PDEVICE_EXTENSION pdx.
PDEVICE_OBJECT DeviceObject)
Восемь сценариев обработки IRP
293
{
NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock,	//	А
(PVOID) 42);
if (!NT_SLCCESS(status))	//	A
return <status>;	//	A
PIRP Irp;
Irp = IoBuildAsynchronousFsdRequest(IRP_MJ_XXX, DevIceObject.
...);
-ИПИ-
Irp = IoAllocateIrp(DeviceObject->StackSize, FALSE);
PIO_STACKJ_OCATION stack = loGetNextlrpStackLocation(Irp);
Stack->MajorFunction = IRP_MJ_XXX;
«дополнительная инициализация>
KEVENT event;
KeInitializeEvent(&event, NotlficationEvent, FALSE);
IoSetCompletionRoutine[Ex]([pdx->DeviceObject], Irp,
(PIO_COMPLETION_ROUTINE) Comp!etlonRoutlne,
&event, TRUE, TRUE, TRUE);
status = loCallDrivertDeviceObject. Irp);
if (status == STATUS-PENDING)
KeWaitForSingleObject(&event, Executive, KernelMode,
FALSE, NULL):
IoReleaseRemoveLock(&pdx->ReinoveLock, (PVOID) 42);	// A
}
NTSTATUS Complet1onRoutine(PDEVICE_OBJECT junk, PIRP Irp.
PKEVENT pev)
{
if (Irp->PendingReturned)
KeSetEvent(pev, EVENTJNCREMENT. FALSE);
«Зачистка IRP -- см. выше>
loFreelrp(Irp):
return STATUS_MORE_PROCESSING_REQUIRED;
}
Фрагменты, отличающиеся от сценария 5, выделены жирным шрифтом.
Как и в предыдущих сценариях, вызовы loAcquireRemoveLock и loReleaseRemove-Lock (в пунктах А) необходимы только в том случае, если устройство, которому посылается IRP, является объектом устройства нижнего уровня в стеке РпР. 42 — просто произвольный маркер; было бы слишком сложно пытаться устанавливать блокировку удаления только для того, чтобы иметь возможность использовать указатель на IRP в качестве маркера в отладочной версии.
Помните, что вы должны выполнить всю зачистку, о которой говорилось ранее, потому что I/O Manager не выполняет автоматическую зачистку для асинхронных IRP. Возможно, вам также придется обеспечить возможность отмены IRP, в этом случае используйте методику отмены асинхронных IRP, представленную в основном тексте главы.
6 Поддержка Plug and Play для функциональных драйверов
Plug and Play (PnP) Manager передает информацию и запросы драйверам устройств посредством пакетов запросов ввода/вывода (IRP) с основным кодом функции IRP_MJ_PNP. Запросы этого типа относятся к новшествам Microsoft Windows 2000 и WDM (Windows Driver Model) — в предыдущих версиях Microsoft Windows NT драйверы устройств были обязаны выполнять большую часть работы по обнаружению и настройке своих устройств. К счастью, драйверы WDM могут поручить эту работу PnP Manager. Для успешной работы с PnP Manager разработчики драйверов должны хорошо понимать несколько относительно сложных IRP.
Запросам Plug and Play в модели WDM отведены две роли. В своей первой роли эти запросы указывают драйверу, когда и как он должен изменять конфигурацию самого себя или устройства. В табл. 6.1 перечислено около двух десятков дополнительных функций запросов РпР. Девять функций, помеченных звездочкой, реализуются только драйвером шины, фильтрующий или функциональный драйвер просто передает такие IRP вниз по стеку. Из остальных дополнительных функций три особенно важны для типичного фильтрующего или функционального драйвера. PnP Manager при помощи запроса IRP_MN_ STARTED EVI СЕ сообщает функциональному драйверу, какие ресурсы ввода/вывода были выделены устройству, а также приказывает функциональному драйверу выполнить всю необходимую настройку аппаратной и программной части для нормального функционирования устройства. IRP_MN_STOP_DEVICE приказывает функциональному драйверу отключить устройство. IRP_MN_REMOVE_DEVICE приказывает функциональному драйверу отключить устройство и освободить связанный с ним объект устройства. Эти три дополнительные функции будут подробно рассматриваться в этой и следующей главах, попутно я опишу назначение других рядовых дополнительных функций, которые также могут обрабатываться фильтрующим или функциональным драйвером.
Однако запросам РпР отводится и другая, более сложная роль — они управляют драйвером в процессе смены состояний, показанных на рис. 6.1. Основные состояния устройства ~ WORKING и STOPPED. Состояние STOPPED является
Зоддержка Plug and Play для функциональных драйверов
295
исходным состоянием устройства непосредственно после создания объекта устройства. Состояние WORKING означает, что устройство полностью работоспособно. Два промежуточных состояния — PENDINGSTOP и PENDINGREMOVE — возникают из-за запросов, которые должны быть обработаны всеми драйверами устройства перед выходом из состояния WORKING. Состояние SURPRISEREMOVED возникает при непредвиденном удалении физического оборудования из системы.
Таблица 6Л. Дополнительные коды функций для пакетов IRP_MJ_PNP (* — обрабатывается "элько драйвером шины)	
Дополнительный код функции	Описание
IRP_MN_START_DEVICE	Настройка конфигурации и инициализация устройства
:rp_mn_query_remove_device	Возможно ли безопасное удаление устройства?
:rpjmn_remove_device	Отключение и удаление устройства
:RP_MN_CANCEL_REMOVE_DEVICE	Игнорировать предыдущий запрос QUERY-REMOVE
:rpj4n_stop__device	Отключение устройства
IRP_MN_QUERY_STOP_DEVICE	Возможно ли безопасное отключение устройства?
TRP_MN_CANCEL_STOP_DEVICE	Игнорировать предыдущий запрос QUERY_STOP
iRP_MN_QUERY_DEVICE_RELATIONS	Получить список устройств, связанных по некоторому критерию
IRP_MN_QUERY_INTERFACE	Получение адресов функций прямого вызова
IRP_MN_QUERY_CAPABILITIES	Определение возможностей устройства
IRP_MN_QUERY_RESOURCES*	Определение загрузочной конфигурации
IRP_MN_QUERY_RESOURCE_REQUIREMENTS*	Определение требований к ресурсам ввода/ вывода
LRP_MN_QUERY_DEVICE_TEXT*	Получение строки с описанием или местонахождением
1RP_MN_FILTER_RESOURCE_REQUIREMENTS	Изменение списка требований к ресурсам ввода/ вывода
IRP_MN_READ_CONFIG*	Чтение конфигурационного пространства
IRP_MN_WRITE_CONFIG*	Запись конфигурационного пространства
:rp_mn_eject*	Извлечение устройства
:rp_mn_set_lock*	Установление/снятие блокировки извлечения устройства
:rp_mn_query_id*	Определение аппаратного идентификатора устройства
IRP_MN_QUERY_PNP_DEVICE_STATE	Определение состояния устройства
IRP_MN_QUERY_BUS_INFORMATION*	Определение типа родительской шины
IRP_MN_DEVICE_USAGE_NOTIFICATION	Оповещение о создании и удалении файла подкачки, спящего режима и т. д.
IRP_MN_SURPRISE_REMOVAL	Оповещение о факте удаления устройства
296
Глава 6. Поддержка Plug and Play для функциональных драйверов
В предыдущей главе я описал функции управления очередями DEVQUEUE. Главной причиной для применения нестандартной схемы организации очереди было упрощение переходов между состояниями РпР, показанными на рис. 6.1, и состояниями управления питанием, о которых речь пойдет в главе 8. В этой главе будут описаны функции DEVQUEUE для поддержки упомянутых состояний.
Кроме того, в этой главе рассматривается механизм оповещений РпР, который дает возможность драйверам и программам пользовательского режима асинхронно узнавать о появлении и исчезновении устройств. Правильная обработка этих оповещений особенно важна для приложений, которые работают с устройствами, поддерживающими режим оперативного подключения/отклю-чения.
Драйверам шин и многофункциональным драйверам в книге посвящена отдельная глава (глава И).
СОВЕТ-----------------------------------------------------------------------
Готовый код из библиотеки GENERIC.SYS сэкономит вам немало времени. Вместо того чтобы писать собственную сложную диспетчерскую функцию для IRPJ4J_PNP, просто делегируйте IRP функции GenericDispatchPnp. Во введении приведена таблица с перечнем функций обратного вызова, реализуемых драйвером для выполнения операций, специфических для конкретного устройства. В этой главе я использовал те же имена функций обратного вызова. Кроме того, практически все примеры построены на основе кода обработки РпР из библиотеки GENERIC.
Зиспетчерская функция IRP MJ PNP
297
спетчерская функция IRP_MJ_PNP
Упрощенная версия диспетчерской функции для запроса IRP_MJ_PNP выглядит примерно так:
NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp)
{
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);	// 1
ULONG fen = stack->MinorFunction;	// 2
static NTSTATUS (*fcntab[])(PDEVICE_OBJECT, PIRP) = {	// 3
HandleStartDevice,	// IRP MN_START_DEVICE
Handl eQueryRemove,	// IRp2mN_QUERY_REMOVE_DEVICE
<etc.>, };
if (fen >= arraysize(fcntab))	// 4
return DefaultPnpHandlerffdo, Irp);
return (*fcntab[fcn])(fdo. Irp);	H 5
}
NTSTATUS DefaultPnpHandler(PDEVICE_OBJECT fdo, PIRP Irp)
{
loSkipCurrentlrpStackLocation(Irp);	// 6
PDEVICE-EXTENSION pdx =
(PDEVICE^EXTENSION) fdo->Devi ceExtensi on;
return IoCallDriver(pdx->LowerDeviceObject, Irp);
}
1.	Все параметры IRP, в том числе и важнейший (дополнительный код функции), находятся в элементе стека. Соответственно, мы получаем указатель на элемент стека вызовом loGetCurrentlrpStackLocation.
2.	Предполагается, что дополнительный код функции IRP входит в число перечисленных в табл. 6.1.
3.	Для организации обработки двух десятков возможных дополнительных кодов функций мы пишем диспетчерскую подфункцию для каждого из обрабатываемых случаев, а затем определяем таблицу указателей на диспетчерские подфункции. Многие записи таблицы содержат DefaultPnpHandier. Диспетчерские подфункции (такие как HandleStartDevice) получают указатели на объекты устройств и 1RP в параметрах и возвращают код NTSTATUS.
4.	Если мы получаем неизвестный дополнительный код функции, вероятно, компания Microsoft определила его в версии DDK, вышедшей после той, на базе которой строился наш драйвер. В такой ситуации будет правильно передать дополнительный код функции вниз по стеку, вызывая обработчик по умолчанию. Кстати, arraysize — определенный в одном из моих заголовочных
298
Глава 6. Поддержка Plug and Play для функциональных драйверов
файлов макрос, возвращающий количество элементов в массиве. Его определение выглядит так:
#define arraysize(p) (sizeof(p)/sizeof((p)CO])).
5.	Основная команда диспетчерской функции индексирует таблицу подфункций и вызывает нужную подфункцию.
6.	В сущности, DefaultPnpHandler — это та же функция ForwardAndForget, которая приводилась в сценарии 2 обработки IRP предыдущей главы. Мы передаем IRP вниз по стеку без функции завершения и поэтому используем loSkip-CurrentlrpStackLocation для задержки указателя стека IRP, поскольку loCallDriver немедленно сместит его вперед.
ТАБЛИЦЫ УКАЗАТЕЛЕЙ НА ФУНКЦИИ--------------------------------------------------------
Применение таблиц указателей на функции для выбора обработчика дополнительных кодов функций, как в функции DispatchPnp, сопряжено с небольшим риском. В будущих версиях операционной системы смысл некоторых кодов может измениться. Впрочем, беспокоиться об этом можно разве что на стадии бета-тестирования системы, потому что изменения на более поздней стадии приведут к нарушению работы множества существующих драйверов. Я предпочитаю использовать таблицы указателей для выбора подфункций, потому что, на мой взгляд, наличие разных функций для разных кодов является правильным техническим решением. Скажем, если бы я проектировал библиотеку классов C++, то я бы определил базовый класс с виртуальными функциями для всех дополнительных кодов.
Вероятно, многие программисты предпочтут включить команду switch в свою функцию DispatchPnp. При любом переназначении дополнительных кодов функций достаточно перекомпилировать свой драйвер. Перекомпиляция также привлечет ваше внимание — за счет ошибок компиляции! — к изменениям имен, которые могут свидетельствовать об изменении функциональности. На самом деле это пару раз происходило на стадии бета-тестирования Microsoft Windows 98 и Windows 2000. Более того, оптимизирующий компилятор сможет использовать таблицу переходов и сгенерировать для команды switch чуть более быстрый код, чем при вызове диспетчерских подфункций. Мне кажется, что выбор между командой switch и таблицей указателей на функции в основном определяется личными предпочтениями, а в моей шкале ценностей удобочитаемость и модульность программы стоят выше эффективности. Чтобы избежать неопределенности на стадии бета-тестирования, можно вставить в программу соответствующие контрольные проверки. Например, функция HandieStartDevice может проверять условие stack->MinorFunction == IRP_MN„START_DEVICE. Перекомпилируя свой драйвер для каждой новой бета-версии DDK, вы сможете выявить любые переназначения числовых кодов и изменения имен.
Запуск и остановка устройства
Работая с драйвером шины, РпР Manager автоматически обнаруживает оборудование и распределяет ресурсы ввода/вывода в Windows ХР и Windows 98/Ме. Большинство современных устройств обладают поддержкой РпР, которая позволяет системным программам автоматически обнаруживать их и автоматически определять нужные ресурсы ввода/вывода. Наследные устройства не способны на электронном уровне передавать операционной системе информацию о себе и требования к ресурсам, поэтому в базе данных системного реестра хранится вся информация, необходимая для обнаружения и назначения ресурсов таким устройствам.
Запуск и остановка устройства
299
ГИМЕЧАНИЕ-----------------------------------------------------------------------
Трудно подобрать абстрактное определение для термина «ресурс ввода/вывода», которое бы не было циклическим (например, «ресурс, используемый для ввода/вывода»), поэтому я лучше приведу конкретное определение. В WDM включены четыре стандартных типа ресурсов ввода/вывода: порты ввода/вывода, регистры памяти, каналы прямого доступа к памяти (DMA) и запросы прерываний.
Обнаружив оборудование, PnP Manager обращается к реестру и узнает, какие фильтрующие и функциональные драйверы должны им управлять. Как обсуждалось в главе 2, PnP Manager загружает эти драйверы (если потребуется — некоторые из драйверов уже могут находиться в памяти, обслуживая другое устройство) и вызывает их функции AddDevice. В свою очередь, функции AddDevice создают объекты устройств и подключают их к стеку. На этой стадии РпР Manager, обслуживающий все драйверы устройств, может переходить к назначению ресурсов ввода/вывода.
Первоначально PnP Manager создает список требований к ресурсам для каждого устройства и дает возможность драйверам отфильтровать этот список. Сейчас мы пока пропустим этап фильтрации, потому что не каждый драйвер должен принимать в нем участие. По готовому списку требований PnP Manager переходит к назначению ресурсов и пытается согласовать потенциально конфликтующие требования всех устройств в системе. Например, на рис. 6.2 показано, как PnP Manager решает конфликт между двумя устройствами с перекрывающимися требованиями к номеру запроса прерывания.
Запрос прерывания
Рис. 6.2. Разрешение конфликтов между перекрывающимися требованиями к ресурсам ввода/вывода
300
Глава 6. Поддержка Plug and Play для функциональных драйверов
IRP_MN_START—DEVICE
Когда все ресурсы будут распределены, РпР Manager оповещает об этом каждое устройство, посылая ему запрос РпР с дополнительным кодом функции IRP_MN_ START_DEVICE. Для фильтрующих драйверов этот тип IRP обычно не представляет интереса, поэтому они обычно передают его вниз по стеку при помощи метода DefaultPnpHander (см. ранее «Диспетчерская функция IRPMJPNP»). В то же время, функциональные драйверы при получении этого IRP должны проделать огромную работу по выделению и настройке дополнительных программных ресурсов, а также подготовке устройства к работе. Кроме того, эта работа должна выполняться на уровне PASSIVE-LEVEL после обработки IRP драйверами нижних уровней в иерархии устройства.
Обработку IRP_MN_START_DEVICE можно реализовать в виде диспетчерской подфункции, управление которой передается из функции DispatchPnp (см. ранее). Основной код выглядит примерно так:
NTSTATUS HandleStartDev1ce(PDEVICE_0BJECT fdo. PIRP Irp)
{
Irp->IoStatus.Status = STATUS-SUCCESS:	// 1
NTSTATUS status = ForwardAndWait(fdo. Irp):	//2
If (!NT_SUCCESS(status))
return CompleteRequestCIrp, status);
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocatlon(Irp);	// 3
status = St art De vice (fdo, дополнительные аргументы);	11 4
EnableAllInterfaces (pdx, True):	// 5
return CompleteRequestCIrp, status);	// 6
}
1.	Драйвер шипы по входному значению loStatus.Status определяет, был ли пакет IRP обработан драйверами верхних уровней. Драйвер шины также проводит аналогичную проверку для ряда других дополнительных функций IRP_ MJ_PNP. Следовательно, прежде чем передавать IRP вниз, мы должны инициализировать поле Status кодом STATUS-SUCCESS.
2.	Функция ForwardAndWait была представлена в главе 5 при описании сценария 7 обработки IRP (синхронная передача вниз). Функция возвращает код состояния. Если в коде состояния обозначен какой-либо сбой на нижних уровнях, мы передаем полученный код нашей вызывающей стороне. Поскольку наша функция завершения вернула STATUS_MORE-PROCESSING_REQUIRED, мы прервали процесс завершения в loCompleteRequest. Следовательно, запрос придется завершать заново, как показано здесь.
3.	Конфигурационные данные спрятаны в параметрах стека. Где именно — я покажу чуть позже.
4.	StartDevice — вспомогательная функция для обработки технических деталей извлечения и обработки конфигурационных данных. В своих примерах драйверов я разместил ее в отдельном кодовом модуле с именем READ WRITE.СРР. Вскоре я объясню, какие аргументы кроме адреса объекта устройства должны передаваться этой функции.
Запуск и остановка устройства
301
5.	Функция EnableAIIInterfaces активизирует все интерфейсы устройства, зарегистрированные в функции AddDevice. Этот шаг позволяет приложениям найти ваше устройство, когда они используют функции SetupDiXrr для перечисления экземпляров зарегистрированных интерфейсов.
6.	Поскольку функция ForwardAndWait обошла процесс завершения для запроса START_DEVICEf мы должны завершить IRP во второй раз. В этом примере я использую перегруженную версию CompleteRequest, которая в соответствии с правилами обработки запросов РпР в DDK не изменяет loStatus.Information.
Возможно, вы предположили (и совершенно справедливо!), что обработчик IRP_MN-START_DEVICE должен выполнить кое-какую работу, связанную с переходом от исходного состояния STOPPED к состоянию WORKED. Пока я не могу объяснять эту тему, потому что сначала я должен объяснить, как другие запросы IRP влияют на переходы состояний, очереди IRP и отмену IRP. По этой причине мы на некоторое время займемся конфигурационными аспектами запросов РпР.
Объединение Parameters в элементе стека ввода/вывода содержит субструктуру с именем StartDevice, эта субструктура содержит данные конфигурации, передаваемые вспомогательной функции StartDevice (табл. 6.2).
Таблица 6.2. Поля субструктуры Parameters.StartDevice в элементе стека ввода/вывода
Имя поля	Описание
Allocated Resources	Базовое назначение ресурсов
AllocatedResourcesTranslated	Преобразованное назначение ресурсов
Обе субструктуры, AllocatedResources и AllocatedResourcesTranslated, представляют собой экземпляры одной структуры данных CM_RESOURCE_LIST. Если судить только по объявлению в WDM.H, эта структура данных может показаться очень сложной. Но при использовании в IRP запуска устройства от всех сложностей остается лишь относительно большое количество символов. «Списки» состоят всего из одного элемента CM_PARTIAL_RESOURCE_LIST, который описывает все ресурсы ввода/вывода, назначенные устройству. При обращениях к спискам применяются команды следующего вида:
PCM_PARTIAL_RESOURCE_LIST raw, translated;
raw = &stack->Parameters.StartDevice
.Al 1ocatedResources->Li st[0].Parti alResourceList;
translated = &stack->Parameters.StartDevice
.Al 1ocatedResourcesTranslated->List[O].Parti alResourceLi st;
В сущности, две последние команды, AllocatedResources или AllocatedResourcesTranslated, различаются только ссылкой на поле структуры параметров.
Кстати говоря, базовые и преобразованные списки ресурсов передаются при вызове вспомогательной функции StartDevice:
status = StartDevice(fdo, raw, translated);
302
Глава 6. Поддержка Plug and Play для функциональных драйверов
Наличие двух разновидностей списков ресурсов объясняется тем, что шины ввода/вывода и процессор могут обращаться к одному физическому оборудованию разными способами. В базовых ресурсах содержатся числа уровня шины, тогда как в преобразованных ресурсах содержатся числа системного уровня. До появления WDM драйвер режима ядра мог получать базовые значения ресурсов из реестра, из конфигурационного пространства PCI (Peripheral Component Interconnect) или из другого источника, и тогда ему приходилось преобразовывать их вызовами таких функций, как HalTranslateBusAddress и Hal-Getlnterru ptVector. В наше время и выборку, и преобразование выполняет РпР Manager, а драйверу WDM остается лишь обратиться к параметрам IRP запуска устройства.
Что именно делается с описаниями ресурсов внутри функции StartDevice, будет рассказано в главе 7.
IRP_MN_STOP_DEVICE
Запрос на остановку означает, что устройство необходимо отключить, чтобы PnP Manager мог переназначить ресурсы ввода/вывода. На аппаратном уровне отключение подразумевает приостановку или прекращение текущей операции и запрет дальнейших прерываний. На программном уровне оно сопряжено с освобождением ресурсов ввода/вывода, заданных на момент запуска устройства. В архитектуре диспетчерских функций/подфункций, которую я описал, остановка устройства может осуществляться подфункцией следующего вида:
NTSTATUS HandleStopDev1ce(PDEVICE_0BJECT fdo, PIRP Irp)
{
<сложный код>	I/	1
StopDeviceffdo. oktouch):	H	2
Irp->IoStatus.Status = STATUS_SUCCESS;
return DefaultPnpHandler(fdo. Irp):	//	3
}
1.	В этом месте вставляется относительно сложный код, связанный с ведением очередей и отменой IRP. Я приведу его позднее, в разделе «Во время остановки устройства» этой главы.
2.	В отличие от запуска устройства, где мы передавали запрос вниз и выполняли операции, специфические для устройства, в данном случае нужно сначала выполнить специфические операции, а затем передать запрос вниз. Это делается для того, чтобы к моменту получения запроса нижними уровнями устройство уже находилось в покое. Я написал вспомогательную функцию StopDevice для выполнения работы по отключению. Второй аргумент указывает, может ли функция StopDevice обращаться к оборудованию в случае необходимости. О том, как задается этот аргумент, рассказано во врезке «Обращение к оборудованию при остановке устройства».
Запуск и остановка устройства
303
3.	Запросы РпР всегда передаются вниз по стеку. В данном случае нас не интересует, что будет сделано с запросом на нижних уровнях, поэтому мы просто используем код DefaultPnpHandler для выполнения механической работы.
Вспомогательная функция StopDevice, вызываемая в предыдущем примере, фактически, выполняет в обратном порядке те конфигурационные действия, которые выполняются в StartDevice. Я приведу код этой функции в следующей главе. У этой функции есть одна важная особенность*, она должна кодироваться такт! образом, чтобы ее можно было многократно вызывать при одном вызове StartDevice. Обработчику РпР IRP не всегда легко определить, вызывалась ли ранее функция StopDevice, — гораздо проще сделать StopDevice устойчивой к повтори ым вызовам.
ОБРАЩЕНИЕ К ОБОРУДОВАНИЮ ПРИ ОСТАНОВКЕ УСТРОЙСТВА------------------------------
В заготовке кода HandleStopDevice используется переменная oktouch — я не показал, как происходит ее инициализация. В схеме программирования драйверов, представленной в этой книге, функция StopDevice получает аргумент типа BOOLEAN, который указывает, насколько безопасно выполнение непосредственных операций ввода/вывода с оборудованием. Зачем нужен этот аргумент? Допустим, некоторые команды должны передаваться устройству в составе протокола отключения, но по какой-то причине сделать это нельзя. Скажем, вы хотите приказать своему модему PCMCIA (Personal Computer Memory Card International Association) разорвать связь, но в этом нет смысла, если пользователь уже отсоединил модемную карту от компьютера.
Существует только один абсолютно надежный способ узнать, остается ли устройство подключенным к устройству, — попытаться обратиться к нему. Однако Microsoft рекомендует при успешной обработке запроса START_DEVICE обращаться к оборудованию при обработке STOP_DEVICE и ряда других запросов РпР. Когда в конце этой главы речь пойдет об отслеживании смены состояний РпР, я буду соблюдать эту рекомендацию: если мы полагаем, что устройство в настоящий момент работает, аргументу oktouch будет присваиваться значение TRUE, а в противном случае — значение FALSE.
IRP_MN_REMOVE_DEVICE
Вспомните, что РпР Manager вызывает функцию AddDevice, чтобы оповестить ваш драйвер о появлении нового экземпляра обслуживаемого оборудования и дать ему возможность создать объект устройства. Однако вместо того чтобы вызывать функцию для выполнения противоположной операции, РпР Manager посылает РпР IRP с дополнительным кодом функции IRP_MN_REMOVE_DEVICE. В ответ на это вы делаете то же, что делали для IRPJ4N_ST0PJ)EVICE при отключении устройства, а затем удаляете объект устройства:
NTSTATUS HandleRemoveDev1ce(PDE\/ICE_0BJECT fdo, PIRP Irp)
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtens i on:
<СЛО№НЫЙ КОД>
Deregi sterAl1 Interfaces(pdx);
StopDevice(fdo, oktouch);
Irp->IoStatus.Status = STATUS^SUCCESS;
NTSTATUS status = DefaultPnpHandler(fdo, Irp);
304
Глава 6. Поддержка Plug and Play для функциональных драйверов
RemoveDevice(fdo); return status; }
Этот фрагмент напоминает HandleStopDevice с парой дополнений. Вызов De-registerAIIInterfaces отключает все зарегистрированные (вероятно, в AddDevice) и включенные (вероятно, в StartDevice) интерфейсы и освобождает память, занимаемую именами символических ссылок. Функция RemoveDevice отменяет всю работу, проделанную в AddDevice:
VOID RemoveDevice!PDEVICEJ3BJECT fdo)
{
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtens1on;
IoDetachDevice(pdx->LowerDeviceObject);	// 1
loDeleteDevice(fdo);	// 2
}
1. Вызов loDetachDevice является парным по отношению к вызову loAttachDeviceToDeviceStack в AddDevice.
2. Вызов loDeleteDevice является парным по отношению к вызову loCreateDevice в AddDevice. После возврата из этой функции следует действовать так, словно объект устройства более не существует. Если драйвер не обслуживает другие устройства, вскоре после этого он выгружается из памяти.
Обратите внимание на го, что вы не получаете запрос на остановку с последующим запросом на удаление устройства. Запрос на удаление устройства подразумевает отключение, поэтому при его обработке решаются обе задачи.
IRP_MN_SURPRISE_REMOVAL
Иногда пользователь обладает физической возможностью отсоединить устройство, не прибегая к пользовательскому интерфейсу. Если система обнаруживает такое непредвиденное отключение или если устройство кажется вышедшим из строя, она отправляет драйверу запрос РпР с дополнительным кодом функции IRP_ MN_SURPRISE_REMOVAL. Позднее будет отправлен запрос IRP_MN_REMOVE_DEVICE. Если ранее при обработке IRP_MN_QUERY_CAPABILITIES не был установлен флаг SurpriseRemovalOK (см. главу 8), некоторые платформы также выводят диалоговое окно с сообщением о потенциальной опасности поспешного отключения.
Получив запрос непредвиденного удаления, драйвер устройства должен отключить все зарегистрированные интерфейсы. Это даст возможность закрыть манипуляторы вашего устройства приложениям, отслеживающим оповещения (см. далее раздел «Оповещения РпР»). Затем драйвер освобождает ресурсы ввода/вывода и передает запрос вниз:
NTSTATUS HandleSurpriseRemoval(PDEVICE_OBJECT fdo, PIRP Irp)
{
Управление переходами состояний РпР
305
PDEVICEJXTENSION pdx -
(PDEVICEJXTENSION) fdo->Dev1ceExtens1on; complicated stuff>
EnableAllInterfaces(pdx, FALSE);
StopDev1ce(fdo, oktouch);
Irp->IoStatus.Status = STATUSJMCESS;
return DefaultPnpHandlerCfdo, Irp);
}
ГДА ВОЗНИКАЕТ IRP_MN_SURPRISE_REMOVAL?-----------------------------------------------
Оповещение о непредвиденном удалении РпР не является простым и прямолинейным результатом того, что конечный пользователь выдернул устройство из компьютера. Некоторые драйверы шин узнают об исчезновении устройства. Например, при отключении устройства USB генерируется электронный сигнал, распознаваемый драйвером шины. Тем не менее, для многих других шин подобных сигналов не существует. По этой причине PnP Manager полагается на другие методы распознавания отключаемых устройств.
Функциональный драйвер может подать сигнал об исчезновении устройства (если он об этом знает), вызывая loInvalidateDeviceState с последующим возвратом одного из значений PNP_DEVICE_ FAILED, PNP_DEVICE_REMOVED или PNP_DEVICE_DISABLED на последующий запрос IRP_MN_QUERY_ ?NP_DEVICE_STATE. Вы можете реализовать такую возможность в своем драйвере, если (лишь один пример из многих) ваш обработчик прерывания прочитал сплошные единичные биты из порта состояния, который обычно возвращает комбинацию 0 и 1. Или другая, более распространенная ситуация: драйвер шины инициирует повторное перечисление вызовом loInvalidateDeviceRelations и не сообщает о пропавшем устройстве. Учтите, что если пользователь отключает устройство, пока система находится в спящем режиме или режиме пониженного энергопотребления, то при восстановлении питания драйвер получит серию IRP управления питанием до получения запроса IRP_MN_SURPRISE_REMOVAL.
С практической точки зрения эти факты означают, что ваш драйвер должен уметь справляться с ошибками, возникающими при внезапном исчезновении устройства.
Управление переходами состояний РпР
Как я говорил в самом начале главы, драйверы WDM должны следить за тем, как их устройства переходят между состояниями, показанными на рис. 6.1. Отслеживание состояний также связано с очередями и отменой запросов ввода/вывода. В свою очередь, в отмене задействована глобальная спин-блокировка отмены, являющаяся причиной снижения производительности в многопроцессорных системах. Стандартная модель обработки IRP с применением функций очередей Microsoft не может решить все эти взаимосвязанные проблемы. Поэтому в этом разделе я расскажу, как мой объект DEVQUEUE помогает справиться со сложностями, создаваемыми Plug and Play.
На рис. 6.3 показаны состояния DEVQUEUE. В состоянии READY очередь принимает и передает запросы функции Startlo таким образом, что устройство остается занятым. Однако в состоянии STALLED очередь не передает IRP Startlo, даже если устройство свободно. В состоянии REJECTING очередь даже не принимает новые IRP. На рис. 6.4 изображена последовательность передачи IRP в очереди.
306
Глава 6. Поддержка Plug and Play для функциональных драйверов
Рис. 6.3. Состояния объекта DEVQUEUE
StallRequests/Restart Requests
Функция
Рис. 6.4. Маршрут IRP в DEVQUEUE
Startlo
В табл. 6.3 перечислены вспомогательные функции, используемые при работе с DEVQUEUE. Функции InitializeQueue, StartPacket, StartNextPacket и CancelRequest уже рассматривались в предыдущей главе. Сейчас пришло время поговорить обо всех остальных функциях.
Настоящее преимущество DEVQUEUE перед объектами очередей, определенными в DDK, состоит в том, что DEVQUEUE упрощает управление переходами между состояниями РпР. Во всех моих примерах драйверов расширение устройства содержит переменную состояния с образным именем state. Также я определяю перечне-
Управление переходами состояний РпР
307
ление с именем DEVSTATE, значения которого соответствуют состояниям РпР. При инициализации объекта устройства в AddDevice вы вызываете InitializeQueue для каждой из очередей устройств, указывая при этом, находится ли устройство в состоянии STOPPED:
NTSTATUS AddDevIсе(...) {
PDEVICEJXTENSION pdx = ...;
Initial1zeQueue(&pdx->dqReadWrite, Startlo);
pdx->state = STOPPED;
}
Таблица 6.3. Вспомогательные функции DEVQUEUE
Функция	Описание
AbortRequests	Запрещает текущие и будущие запросы
AllowRequests	Отменяет последствия предыдущего вызова AbortRequests
AreRequestsBeingAborted	Запрещаются ли новые запросы в настоящее время?
CancelRequest	Обобщенная функция отмены
CheckBusyAndStall	Проверяет свободное устройство и приостанавливает запросы за одну атомарную операцию
CleanupRequests	Отменяет все запросы к заданному объекту файла по требованию IRP_MJ_CLEANUP
GetCurrentlrp	Определяет, какой пакет IRP в настоящее время обрабатывается ассоциированной функцией Startlo
InitializeQueue	Инициализирует объект DEVQUEUE
RestartRequests	Перезапускает приостановленную очередь
StallRequests	Приостанавливает очередь
StartNextPacket	Выводит из очереди и запускает следующий запрос
StartPacket	Запускает или ставит в очередь новый запрос
WaitForCurrentlrp	Ожидает завершения текущего IRP
После возврата из AddDevice система посылает запросы IRP_MJ__PNP с информацией о различных состояниях РпР, принимаемых устройством.
ПРИМЕЧАНИЕ---------------------------------------------------------------------------------
Если в вашем драйвере используется GENERIC.SYS, то GENERIC инициализирует объект или объекты DEVQUEUE за вас. От вас потребуется лишь передать GENERIC адреса этих объектов при вызове InitializeGenericExtension,
Запуск устройства
Только что инициализированный объект DEVQUEUE находится в состоянии STALLED, поэтому вызов StartPacket поставит запрос в очередь, даже если устройство не
308
Глава 6. Поддержка Plug and Play для функциональных драйверов
занято. Очередь (или очереди) остается в состоянии STALLED до момента успешной обработки IRP_MN_START._DEVICE, когда выполняется код следующего вида:
NTSTATUS HandleStartDevice!.. )
{
status = StartDevice!, .);
if (NT_SUCCESS(status))
{
pdx->state = WORKING:
RestartRequests(&pdx->dqReadWr1te, fdo);
}
}
Состояние WORKING сохраняется как текущее состояние устройства, после чего для каждой очереди вызывается функция RestartRequests: она освобождает все IRP, которые могли поступить между запуском AddDevice и моментом получения запроса IRP_MN_START_DEVICE.
Возможна ли остановка устройства?
PnP Manager всегда спрашивает вашего разрешения перед тем, как отправлять пакет IRP„MN_STOP_DEVICE. Для этого он отправляет запрос IRP_MN_QUERY__ STOP_DEVICE, который может завершиться как успехом, так и неудачей — на ваш выбор. Фактически, этот запрос означает: «Сможете ли вы немедленно остановить свое устройство, если система через несколько наносекунд пришлет запрос IRP„MN_STOP_DEVICE?» Запрос можно обработать двумя слегка различающимися способами. Первый способ подходит в том случае, если устройство занято пакетом IRP, обработка которого либо быстро завершится, либо может быть легко прервана в середине:
NTSTATUS HandleQueryStop!PDEVICE-OBJECT fdo. PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS:
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension;
if (pdx->state != WORKING)	// 1
return DefaultPnpHandler(fdo, Irp);
if (’OkayToStop(pdx))	// 2
return CompleteRequest!Irp, STATUS-UNSUCCESSFUL, 0);
StallRequests(&pdx->dqReadWrite);	// 3
Wa i tForCurrentIrp(&pdx->dqReadWr1te):
pdx->state = PENDINGSTOP:	// 4
return DefaultPnpHandlertfdo, Irp); }
1.	Команда решает проблему, специфическую для загрузочных устройств: РпР Manager может отправить запрос QUERYJ5TOP еще до завершения инициализации. Такие запросы следует игнорировать, что равнозначно положительному ответу.
Управление переходами состояний РпР
309
2.	В этой точке проводится какой-то анализ, выясняющий, можно ли вернуться к состоянию STOPPED. Основные факторы этого анализа будут рассмотрены чуть позже.
3.	Функция StallRequests переводит DEVQUEUE в состояние STALLED, чтобы все новые IRP просто помещались в очередь. Функция WaitForCurrentIrp ожидает, пока устройство завершит обработку текущего запроса (если он имеется). Эти два шага переводят устройство в состояния покоя, пока мы не узнаем, действительно устройство собирается остановиться или нет. Если текущий IRP не может быстро завершиться сам по себе, нужно что-то сделать, чтобы «подтолкнуть» его к этому (например, вызвать loCancellrp, заставляя драйвер нижнего уровня завершить текущий IRP), в противном случае WaitForCurrentIrp не вернет управление.
4.	В этой точке причин для колебаний уже не осталось. Соответственно, мы сохраняем PENDINGSTOP как текущее состояние и передаем запрос вниз по стеку, чтобы другие драйверы тоже получили возможность принять или отклонить запрос.
Второй основной способ обработки QUERY_STOP уместен в ситуациях, когда ваше устройство занято обработкой продолжительного запроса, который не может быть прерван на середине, — например, при операции снятия неравномерного натяжения ленты прерывание может привести к разрыву ленты. В этом случае можно воспользоваться функцией CheckBusyAndStall объекта DEVQUEUE. Функция возвращает TRUE, если устройство занято, и тогда QUERY_STOP завершается неудачей с кодом STATUS-UNSUCCESSFUL. Если устройство свободно, функция возвращает FALSE, в этом случае очередь также приостанавливается. (Операции проверки состояния устройства и приостановки очереди должны защищаться спин-блоки-ровкой — это и стало основной причиной для написания функции.)
Запрос на остановку устройства может завершиться неудачей по многим причинам. Например, дисковые устройства, обслуживающие виртуальную намять, не могут останавливаться. Остановка невозможна и для устройств, используемых для хранения файлов спящего режима или аварийных дампов. (Информацию об этих характеристиках можно получить при помощи запроса IRP_MN_DEVICE_ USAGE-NOTIFICATION — см. далее в разделе «Другие конфигурационные функции».) Возможно, для вашего конкретного устройства найдутся и другие причины.
Даже если на вашем уровне запрос на установку получил подтверждение, он может быть по какой-либо причине отклонен одним из драйверов нижнего уровня. Более того, даже если все драйверы подтвердят отключение, РпР Manager может решить, что отключать устройство не следует. В любом из этих случаев вы получите другой запрос РпР с дополнительным кодом IRP_MN_CANCEL_STOP_DEVICE — это означает, что устройство отключаться не будет. В этом случае следует сбросить состояние, установленное при получении исходного запроса:
NTSTATUS Handl eCancelStop(PDEVICE_OBJECT fdo, PIRP Irp) {
Irp->IoStatus.Status = STATUS__SUCCESS;
PDEVICE-EXTENSION pdx =
310
Глава 6. Поддержка Plug and Play для функциональных драйверов
(PDEVICE_EXTENSION) fdo->DeviceExtensi on;
if (pdx->state != PENDINGSTOP)
return DefaultPnpHandlertfdo. Irp);
NTSTATUS status = ForwardAndWaltCfdo, Irp);
pdx->state « WORKING;
RestartRequests(&pdx->dqReadWrite, fdo);
return ConpleteRequestCIrp, status);
}
Сначала мы проверяем, имеется ли незавершенная операция остановки. Возможно, какой-то драйвер верхнего уровня заблокировал запрос, который до нас не дошел, и драйвер все еще находится в состоянии WORKING. Если драйвер не находится в состоянии PENDINGSTOP, то IRP просто пересылается. В противном случае драйверам нижнего уровня синхронно отправляется IRP CANCEL_STOP. Для этого вспомогательная функция Forward And Wait отправляет IRP вниз по стеку и ожидает его завершения. Мы ожидаем реакции драйверов нижнего уровня, потому что собираемся возобновить обработку IRP, но прежде чем пересылать IRP, драйверам нужно дать возможность закончить собственные дела. После этого изменение переменной state показывает, что устройство снова находится в состоянии WORKING, и мы вызываем RestartRequests для возобновления работы очередей, приостановленных при успешном выполнении запроса.
Во время остановки устройства
В то же время, если все драйверы устройства обработали запрос успешно и РпР Manager решает перейти к отключению, далее вы получите запрос IRP_MN_STOP_ DEVICE. Диспетчерская подфункция будет выглядеть примерно так:
NTSTATUS HandleStopDeviсе(PDEVICE_OBJЕСТ fdo, PIRP Irp)
{
Irp->IoStatus.Status - STATUS-SUCCESS;
PDEVICE_EXTENSION pdx =
(PDEVICE-EXTENSION) fdo->DeviceExtension;
if (pdx->state != PENDINGSTOP);	// 1
{
<сложный код>
}
StopDevice(fdo, pdx->state == WORKING);	//	2
pdx->state = STOPPED;	//	3
return DefaultPnpHandlerffdo, Irp);	//	4
}
1.	Предполагается, что перед отправкой запроса STOP система сначала отправит запрос QUERYJSTOP, поэтому устройство уже должно находиться в состоянии PENDINGSTOP с приостановленными очередями. Тем не менее, в Windows 98 присутствует ошибка, из-за которой драйвер иногда может получить запрос STOP (без QUERY_STOP) вместо REMOVE. На этой стадии нужно выполнить какие-то действия, в результате которых все новые IRP будут отвергаться, но
Управление переходами состояний РпР
311
вы не должны удалять объект устройства или выполнять другие действия, обычно выполняемые при получении запросов REMOVE.
2.	Уже упоминавшаяся ранее функция StopDevice выполняет деинициализацию устройства.
3.	Устройство переходит в состояние STOPPED. На этот момент ситуация выглядит практически так же, как после выполнения функции AddDevice: все очереди приостановлены, а устройство не имеет ресурсов ввода/вывода. Единственное различие заключается в том, что все зарегистрированные интерфейсы остаются разрешенными — это означает, что приложения еще не получили оповещений об удалении и содержат открытые манипуляторы устройства. В этой ситуации приложения еще могут открывать новые манипуляторы. И то и другое совершенно нормально, потому что состояние остановки продолжается недолго.
4.	Как упоминалось ранее, обработка IRP_MN_STOP_DEVICE должна завершиться передачей запроса на нижние уровни иерархии драйверов
Можно ли удалить устройство?
РпР Manager спрашивает вашего разрешения не только перед отключением устройства, посылая запрос на остановку, — он также может поинтересоваться вашим мнением перед удалением устройства. Его обращение принимает форму запроса IRP_MN_QUERY_REMOVE_DEVICE, который вы также можете отклонить или принять на свое усмотрение. И как и в случае с запросом на остановку, если РпР Manager изменит свое решение относительно удаления устройства, он оповестит вас об этом отправкой IRP IRP_MN_CANCEL_REMOVE„DEVICE.
NTSTATUS HandleQueryRernove(PDEVICE_OBJECT fdo, PIRP Irp)
{
Irp->lostatus.Status - STATUS_SUCCESS;
PDEVICE-EXTENSION pdx -
(PDEVICEJiXTENSION) fdo->DeviceExtension;
if (OkayToRemove(fdo))	// 1
{
Stal1 Requests(&pdx->dqReadWrite):	// 2
Wai tForCurrentIrp(&pdx->dqReadWrite);
pdx->prevstate = pdx->state:	// 3
pdx->state = PFNDINGREMOVE;
return DefaultPnpHandler(fdo, Irp):
}
return CompleteRequestCIrp. STATUS-UNSUCCESSFUL. 0):
}
NTSTATUS Handl eCancel Remove!PDEVICE_OBJECT fdo. PIRP Irp)
{
Irp->IoStatus.Status = STATUS-SUCCESS:
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension;
312
Глава 6. Поддержка Plug and Play для функциональных драйверов
if (pdx->state 1= PENDINGREMOVE)	// 4
return DefaultPnpHandler(fdo, Irp);
NTSTATUS status = ForwardAndWaitffdo, Irp);
pdx->state = pdx->prevstate;	// 5
RestartRequests(&pdx->dqReadWrite, fdo);
return CompleteRequestfIrp, status);
}
1.	Вспомогательная функция OkayToRemove отвечает на вопрос: «Можно ли удалить это устройство?» В общем случае ответ на него должен учитывать некоторые особенности устройства — например, содержит ли оно файл подкачки или спящего режима и т. д.
2.	Как было показано ранее для IRPJ4N_QUERY_STOP_DEVICE, следует приостановить очередь запросов и выждать некоторое время, пока не окончится обработка текущего запроса.
3.	Присмотревшись к рис. 6.1, вы заметите, что запрос QL1ERY_REMOVE также может быть получен в состояниях WORKING и STOPPED. Если текущий запрос позднее будет отменен, устройство следует вернуть в исходное состояние. Для этого в расширение устройства включается переменная prevstate, в которой хранится состояние устройства до получения запроса.
4.	Мы получаем запрос CANCEL_REMOVE, когда один из драйверов верхнего или нижнего уровня отклоняет запрос QUERY_REMOVE. Если этот запрос не дошел до нашего драйвера, устройство будет находиться в состоянии WORKING, и с IRP ничего делать нс нужно. В противном случае перед обработкой его необходимо переслать на нижние уровни, потому что мы хотим, чтобы нижние уровни были готовы обработать IRP, освобождаемые из наших очередей.
5.	Здесь выполняются действия, противоположные тем, которые выполнялись при подтверждении QUERY_REMOVE. Устройство возвращается в предыдущее состояние. При обработке запроса очереди были приостановлены, а теперь их необходимо снова запустить.
Синхронизация удаления
I/O Manager может отправлять запросы РпР одновременно с другими содержательными запросами ввода/вывода — например, запросами, сопряженными с чтением и записью. Таким образом, вполне возможно, что запрос IRP_MN_REMOVE_ DEVICE будет получен во время обработки другого IRP. Вы должны сами позаботиться о предотвращении нежелательных последствий. Стандартный способ основан на использовании объекта IO_REMOVE_LOCK и нескольких вспомогательных функций режима ядра.
Стандартная схема предотвращения преждевременного удаления основана па том, что при каждой передаче запроса вниз по стеку РпР устанавливается блокировка удаления. После завершения обработки блокировка снимается. Прежде чем удалять объект устройства, следует убедиться в том, что блокировка свободна, в противном случае подождите, пока не будут освобождены все ссылки на блокировку. Процесс продемонстрирован на рис. 6.5.
Управление переходами состояний РпР
313
Рис. 6.5. Работа с объектом IO_REMOVE_LOCK
В расширении устройства определяется переменная:
struct DEVICE-EXTENSION {
IO_REMOVE_LOCK RemoveLock;
Объект блокировки инициализируется при выполнении AddDevice:
NTSTATUS AddDevIсе(PDRIVER_OBJECT DriverObject,
PDEVICE_OBJECT pdo)
{
IoInitializeRemoveLock(&pdx->RemoveLock. 0, 0, 0);
}
В последних трех параметрах loInitializeRemoveLock передаются, соответственно, маркер (тег), предполагаемый максимальный жизненный цикл блокировки и максимальное значение счетчика блокировки. В окончательной версии операционной системы ни один из этих параметров не используется.
Предварительная подготовка создает условия для ваших дальнейших действий на протяжении жизненного цикла объекта. При каждом получении запроса ввода/вывода, который вы планируете направить вниз по стеку, вызывается функция loAcquireRemoveLock. При наличии незавершенной операции удаления loAcquireRemoveLock возвращает STATUS_DELETE_PENDING, в противном случае функция захватывает блокировку и возвращает STATUS_SUCCESS. При завершении такой операции ввода/вывода вы вызываете функцию loReleaseRemoveLock, которая освобождает блокировку, что может привести к активизации незавершенной операции удаления. В контексте чисто гипотетической диспетчерской функции, осуществляющей синхронную пересылку IRP, код может выглядеть примерно так:
NTSTATUS DispatchSomething(PDEVICE_OBJECT fdo, PIRP Irp)
(
PDEVICE_EXTENSION pdx =
314
Глава 6. Поддержка Plug and Play для функциональных драйверов
(PDEVICE_EXTENSION) fdo->DeviceExtension:
NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp);
if (!NT_SUCCESS(status))
return CompleteRequestCIrp, status. 0);
status = ForwardAndWaitCfdo. Irp);
if (!NT_SUCCESS(status))
{
IoReleaseRemoveLock(&pdx->RemoveLock, Irp);
return CompleteRequestCIrp, status, 0);
}
IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return CompleteRequestCIrp, <код>, <информация>);
}
Второй аргумент функции loAcquireRemoveLock и loReleaseRemoveLock представляет собой обычный маркер, используемый в отладочных версиях операционной системы для сопоставления вызовов захвата/снятия блокировки.
Вызовы функций захвата и снятия блокировки удаления работают в сочетании с дополнительной логикой диспетчерской функции РпР и диспетчерской подфункции удаления устройства. Прежде всего, функция DispatchPnp должна соблюдать правила блокировки устройства, поэтому в пее включается дополнительный код, не приводившийся ранее в разделе «Диспетчерская функция IRPMJPNP»;
NTSTATUS DispatchPnpCPDEVICEJDBJECT fdo, PIRP Irp)
{
PDEVICE-EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension;
NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp);
if (*NT_SUCCESS(status))
return CompleteRequestCIrp, status, 0);
status = (*fcntab[fcn](fdo. Irp):
if (fen != IRP_MN_REMOVE_DEVICE)
IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return status;
Другими словами, DispatchPnp блокирует устройство, вызывает диспетчерскую подфункцию, а затем (обычно) снимает блокировку. Диспетчерская подфункция для запросов IRP_MN_REMOVE_DEVICE содержит дополнительную специальную логику, которая ранее еще не показывалась:
NTSTATUS HandleRemoveDeviсе(PDEVICEJ3BJECT fdo. PIRP Irp)
{
Irp->IoStatus.Status = STATIIS_SUCCESS:
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Devi ceExtensi on;
Управление переходами состояний РпР
315
AbortRequests(&pdx->dqReadWrite, STATUS_DELETE_PENDING);	// 1
DeregisterAl1Interfaces(pdx);
StopDeviceCfdo. pdx->state == WORKING):
pdx->state = REMOVED;
NTSTATUS status =	//2
DefaultPnpHandler(pdx->LowerDeviceObject, Irp);
IoReleaseRemoveLockAndWa-it(&pdx->RemoveLock, Irp);	// 3
RemoveDevice(fdo); return status;
}
1.	Windows 98/Ме не отправляет запрос SURPRISE-REMOVAL, поэтому первым признаком исчезновения устройства может быть IRP REMOVE. Вызов StopDevice позволяет освободить все ресурсы ввода/вывода в случае, если вы не получили более ранний IRP, по которому они должны освобождаться. В результате вызова AbortRequests все IRP в очереди завершаются, а все новые IRP начинают отвергаться.
2.	Когда вся работа будет выполнена, запрос передается на нижние уровни.
3.	Диспетчерская функция РпР захватила блокировку удаления. Теперь мы вызываем специальную функцию loReleaseRemoveLockAndWait, чтобы освободить ссылку на эту блокировку и дождаться освобождения всех остальных ссылок па нее. После вызова loReleaseRemoveLockAndWait все последующие вызовы loAcquireRemoveLock возвращают STATUS_DELETE_PENDING, указывая на то, что устройство находится в процессе удаления.
ПРИМЕЧАНИЕ--------------------------------------------------------------------------
Обратите внимание: обработчик IRP_MN_REMOVE_DEVICE может блокироваться во время завершения IRP. Конечно, это совершенно нормально в системах Windows 98/Ме и Windows ХР, которые проектировались с учетом этой возможности, — IRP посылается в контексте системного потока, для которого разрешена блокировка. Часть функциональности WDM (один разработчик Microsoft даже назвал ее «эмбриональной») присутствует в OEM-версиях Microsoft Windows 95, но здесь блокировка запроса на удаление устройства невозможна. Следовательно, если ваш драйвер должен работать в Windows 95, вы должны выявить этот факт и избегать блокировки. Процесс выявления предоставляется читателю для самостоятельной работы.
Стоит повторить, что необходимость в использовании блокировки удаления возникает только для IRP, передаваемых вниз в стеке РпР. Если у вас хватит духу, прочитайте следующий раздел, и вы поймете, почему это утверждение истинно, заодно обратите внимание на то, что оно расходится с общепринятыми истинами, которые я и многие другие проповедовали уже много лет. Если кто-то отправит вам IRP, вся обработка которого производится внутри вашего драйвера, вы можете положиться на то, что отправитель IRP позаботится об удержании вашего драйвера в памяти на время завершения IRP и возврата из диспетчерской функции. Если же вы отправляете IRP кому-то за пределами своего стека РпР, используйте другие средства (например, ссылки на объекты файла или устройства) для удержания целевого драйвера в памяти, пока он не завершит IRP и не вернет управление из диспетчерской функции.
316
Глава 6. Поддержка Plug and Play для функциональных драйверов
Зачем нужна эта @#$! блокировка?!
Возникает естественный вопрос: почему в устойчивой полнофункциональной современной операционной системе вам вообще приходится беспокоиться о том, что кто-то выгрузит драйвер, — хотя он знает (или должен знать), что драйвер занят обработкой IRP? Ответить на этот вопрос нелегко, но я попробую.
Функция блокировки удаления не ограничивается защитой от удаления объекта устройства в то время, пока драйвер занят обработкой IRP. Она также защищает вас от отправки IRP вниз по стеку РпР объекту устройства более низкого уровня, который не существует или перестал существовать перед завершением IRP. Чтобы это стало понятно, я должен довольно подробно объяснить, как РпР Manager и Object Manager совместными усилиями удерживают в памяти драйверы и объекты устройств в то время, пока они необходимы. Мои объяснения сильно упрощены, чтобы лучше выделить основные принципы, которые вы должны понять.
Прежде всего, с каждым объектом, находящимся под управлением Object Manager, ассоциируется счетчик ссылок. При создании такого объекта Object Manager инициализирует счетчик ссылок равным 1. В дальнейшем любой желающий может увеличить счетчик ссылок вызовом ObReferenceObject или уменьшить его вызовом ObDereferenceObject. Для каждого типа объекта существует функция, вызываемая для его уничтожения. Например, функция loDeleteDevice вызывается для удаления объекта DEVICE_OBJECT. Эта функция никогда не освобождает память, занимаемую объектом, напрямую. Вместо этого она явно или косвенно вызывает ObDereferenceObject для освобождения ссылки. Только тогда, когда счетчик ссылок уменьшится до 0, Object Manager реально уничтожает объект.
ПРИМЕЧАНИЕ-----------------------------------------------------------------
В главе 5 я советовал вам заключать вызов loCallDriver для асинхронных IRP между получени-ем/освобождением дополнительной ссылки на объекты файлов и устройств, полученные вызовом loGetDeviceObjectPointer. Полагаю, теперь вы понимаете смысл этого совета: нужно быть уверенным в том, что целевой драйвер для этого IRP останется в памяти, пока его диспетчерская функция не вернет управление, независимо от того, освободила ли ваша функция завершения ссылку, полученную вызовом loGetDeviceObjectPointer. Черт, какая сложная фраза получилась!
Прежде чем освобождать последнюю ссылку на объект устройства, функция loDeleteDevice выполняет ряд проверок. В обеих операционных системах она проверяет, равен ли NULL указатель Attached Device. Это поле объекта устройства содержит указатель на объект устройства следующего верхнего уровня. Значение поля задается вызовом loAttachDeviceToDeviceStack и сбрасывается вызовом loDetachDevice, в драйверах WDM они располагаются в функциях AddDevice и RemoveDevice соответственно.
Весь стек РпР объектов устройств можно рассматривать как приемник для IRP, которые I/O Manager и драйверы за пределами стека отправляют «вашему» устройству. Это связано с тем, что драйвер верхнего объекта устройства в стеке всегда первым обрабатывает любой IRP. Но прежде чем кто-то отправит IRP в ваш стек, он должен получить ссылку на верхний объект устройства,
Управление переходами состояний РпР
317
причем эта ссылка будет освобождена только после завершения IRP. Значит, если стек драйвера состоит только из одного объекта устройства, объект устройства или код драйвера не может исчезнуть во время обработки IRP драйвером: ссылка отправителя IRP закрепляет объект устройства в памяти, даже если кто-то вызовет loDeleteDevice перед завершением 1RP, а объект устройства закрепляет в памяти код драйвера.
Стеки драйверов WDM обычно содержат два и более объекта устройства, поэтому вам придется побеспокоиться о втором и других нижних объектах стека. В конце концов, тот, кто отправляет IRO устройству, обладает ссылкой только на верхний объект устройства, а не на объекты, находящиеся ниже в стеке. Представьте следующий сценарий: кто-то отправляет IRP_MJ_SOMETHING (вымышленный основной код функции, чтобы не отвлекаться от блокировки удаления) верхнему фильтрующему объекту устройства (FiDO), а драйвер последнего посылает запрос вниз по стеку вашему функциональному драйверу. Вы собираетесь отправить этот 1RP фильтрующему драйверу, находящемуся в следующей нижней позиции стека. Но примерно в то же время на другом процессоре РпР Manager отправляет в ваш стек запрос IRP_MN_REMOVE_DEVICE.
Прежде чем отправлять запрос REMOVE-DEVICE, PnP Manager создает дополнительную ссылку на каждый объект устройства в стеке, а затем отправляет IRP. Каждый драйвер передает IRP вниз по стеку и вызывает функцию loDetachDevice, за которой следует вызов loDeleteDevice. На каждом уровне loDeleteDevice видит, что указатель AttachedDevice (пока) не равен NULL, и решает, что время доя освобождения ссылки на объект устройства еще не пришло. Но когда драйвер на следующем верхнем уровне вызывает loDetachDevice, момент считается подходящим, и I/O Manager освобождает ссылку на объект устройства. Без дополнительной ссылки PnP Manager объект исчезнет, что могло бы привести к выгрузке драйвера на этом уровне стека. После завершения запроса REMOVE-DEVICE PnP Manager освободит все дополнительные ссылки. Это позволит всем объектам устройств, кроме верхнего, исчезнуть, потому что только верхний объект устройства защищен дополнительной ссылкой, принадлежащей отправителю IRP_MJ_SOMETHING.
Во всех драйверах, который я видел или написал сам, запрос REMOVE-DEVICE обрабатывался синхронно. Другими словами, ни один драйвер никогда не оставляет незавершенный запрос REMOVEDEVICE. Соответственно, вызовы loDetachDevice и loDeleteDevice на любом уровне стека РпР всегда происходят уже после того, как эти вызовы были выполнены драйверами нижнего уровня. Данное обстоятельство не влияет на наш анализ блокировки удаления, потому что РпР Manager не освободит дополнительную ссылку на стек до фактического завершения REMOVE-DEVICE, для чего функция loCompleteRequest должна отработать до конца.
Итак, мы надеемся, что тот, кто находится выше нас в стеке РпР, будет удерживать объект устройства и код драйвера в памяти до завершения обработки гипотетического запроса IRP_MJ_SOMETHING. Но мы (пока) сами ничего не сделали, чтобы удержать в памяти следующий объект устройства нижнего уровня н драйвер! Пока мы готовились отправить IRP вниз по стеку, запрос IRP_ MN_REMOVE__DEVICE дошел до завершения, и нижний драйвер теперь исчез!
318
Глава 6. Поддержка Plug and Play для функциональных драйверов
Именно в этом заключается проблема, решаемая блокировкой удаления: мы просто не хотим передавать IRP вниз по стеку, если уже произошел возврат из обработки IRP_MN„REMOVE_DEVICE. И наоборот, мы не хотим возвращаться из IRP-MN_REMOVE_DEVICE (и следовательно, разрешать РпР Manager освобождать ссылку на нижний объект устройства, которая может оказаться последней), пока не будем точно знать, что нижний драйвер завершил обработку всех отправленных ему IRP.
Вооружившись этой информацией, давайте снова рассмотрим сценарий обработки IRP, в котором может пригодиться блокировка удаления. Далее следует пример из сценария 1 обработки IRP (передача вниз с функцией завершения) из главы 5:
NTSTATUS DispatchSomething(PDEVICE-OBJECT fdo. PIRP Irp)
PDEVICE_EXTENSION pdx =
(PDEVICEJEXTENSION) fdo->DeviceExtension;
NTSTATUS status = IoAcquireRemoveLock(&pdx->RemoveLock. Irp); // A
if (!NT_SUCCESS(status))
return CompleteRequesttIrp, status, 0);
IoCopyCurrentIrpStackLocat1on!oNext(Irp);
IoSetCompleti onRouti ne(Irp,
(PIO_COMPLETION_ROUTINE) CompletionRoutine, pdx, TRUE. TRUE, TRUE);
return IoCallDriver(pdx->LowerDeviceObject, Irp);
}
NTSTATUS CompletionRoutinetPDEVICE_OBJECT fdo, PIRP Irp,
PDEV1CE_EXTENSION pdx)
{
if (Irp->PendingReturned)
loMarklrpPending(Irp):
необходимая обработка завершения>
IoReleaseRemoveLock(&pdx->RemoveLock. Irp);	// В
return STATUS-SUCCESS;
}
В двух словах: мы захватываем блокировку удаления для этого IRP в диспетчерской функции и освобождаем ее в функции завершения. Предположим, по стеку вниз передается IRP_MN__REMOVE_DEVICE. Если наша функция HandleRemoveDevice добралась до точки вызова loReleaseRemoveLockAndWait перед тем, как мы дошли до точки А, возможно, все объекты устройств в стеке находятся на грани исчезновения, потому что запрос REMOVE-DEVICE мог быть давно завершен. Если наш объект устройства является верхним, он остается «живым» благодаря внешней ссылке. Если же объект находится ниже в стеке, то его существование обеспечивается верхним драйвером. Так или иначе, можно переходить к выполнению команд. Выясняется, что наш вызов loAcquireRemoveLock возвращает STATUS-DELETEPENDING, поэтому мы просто завершаем IRP и возвращаем управление.
Теперь предположим, что мы выиграли «гонку», вызвав loAcquireRemoveLock до того, как наша функция HandleRemoveDevice вызвала loReleaseRemoveLockAndWait.
Управление переходами состояний РпР
319
В этом случае мы передаем IRP вниз по стеку. loReleaseRemoveLockAndWait блокируется до того, как наша функция завершения (в точке В) снимет блокировку. В этот момент мы снова начинаем зависеть от ссылки отправителя IRP или драйвера более высокого уровня, которая удержит наш код в памяти столько времени, сколько понадобится для возврата управления функцией завершения.
На этой стадии анализа я должен поднять один неприятный вопрос. До настоящего времени его упускали из виду все, кто занимался разработкой WDM или преподаванием в этой области (включая меня). Передача IRP вниз без функции завершения в действительности небезопасна, потому что она позволяет нам отправить IRP вниз драйверу, не закрепленному в памяти. Каждый раз, когда вы видите вызов loSkipCurrentlrpStackLocation (а в Windows ХР DDK он встречается 204 раза), у вас должно что-то тревожно дрогнуть внутри. Пока все обходилось благодаря избыточной защите и крайне малой вероятности совпадения IRP__MN_REMOVE_DEVICE с каким-нибудь проблемным IRP. За дополнительной информацией обращайтесь к врезке.
УБЫТОЧНАЯ ЗАЩИТА ОТ ПРЕЖДЕВРЕМЕННОГО УДАЛЕНИЯ----------------------------------------
Как сказано в тексте, Windows ХР содержит ряд избыточных мер для защиты от преждевременного удаления объектов устройств. Как в Windows ХР, так и в Windows 2000 РпР Manager не посылает запрос IRP_MN_REMOVE_.DEVICE, если существует объект файла, ссылающийся на какой-либо из объектов устройств в стеке. Многие IRP создаются сторонами, хранящими указатель на объект файла, защищенный подсчетом ссылок. Следовательно, с такими IRP никогда не приходится беспокоиться об исчезновении объектов нижнего уровня. С такими IRP можно обойтись блокировкой удаления, если вы уверены в том, что все отправляющие их драйверы либо используют объект файла, защищенный ссылкой, либо устанавливают собственную блокировку удаления.
Многие IRP никогда не доходят до драйверов устройств, потому что эти IRP задействованы в операциях файловой системы с томами. Таким образом, было бы неразумно беспокоиться о том, что, например, может произойти при обработке драйвером устройства запроса IRP_MJ_QUERY_ VOLUME-INFORMATION.
Лишь немногие IRP не защищены подсчетом ссылок и не ориентированы на работу с драйверами файловой системы, но и они в большинстве содержат собственную встроенную защиту. Чтобы получить IRP_MJ_SHUTDOWN, необходимо специально зарегистрироваться в I/O Manager вызовом ZoRegisterShutdownNotification. Функция loDeieteDevice автоматически отменяет регистрацию, если зы случайно забудете об этом, и вы не будете получать запросы REMOVE-DEVICE в процессе опо-зещений об отключении. В DDK об этом ничего не сказано, но этот запрос не следует передавать вниз по стеку РпР: каждый драйвер в стеке, который хочет получать этот IRP, должен зарегистри-эоваться отдельно.
Еще один особый случай — запрос IRP_MJ_SYSTEM_CONTROL. Подсистема WMI (Windows Management Instrumentation) использует его для выполнения запросов и операций WMI. Одним из этапов обработки StopDevice должна стать отмена регистрации WMI, причем этот вызов не возвращает управление до тех пор, пока все эти IRP не пройдут через устройство. После отмены регистрации заш драйвер больше не будет получать запросы WMI.
Источником большинства запросов IRP_MJ_PNP является РпР Manager. Вы можете быть уверены в том, что REMOVE_DEVICE не будет перекрываться с другими РпР IRP. Тем не менее, такой ,зеренности не может быть для РпР IRP, отправленных другими драйверами, — скажем, QUERY_ DEVICE-RELATIONS для получения адреса PDO или QUERY-INTERFACE для получения интерфейса прямого вызова.
Наконец, запрос IRP_MJ_POWER также создает потенциальные проблемы, потому что Power Manager не блокирует весь стек устройства и не содержит указатель на объект файла.
320
Глава 6. Поддержка Plug and Play для функциональных драйверов
На самом деле вероятность уязвимости весьма мала. Возьмем следующий фрагмент диспетчерских функций двух драйверов:
NTSTATUS DriverA_DispatchSometh1ng(...)
{
NTSTATUS status = IoAcquireRemoveLock(...);
If (!NT_SUCCESS(status))
return CompleteRequestC...); loSkipCurrentlrpStackLocationf...); status = IoCalWriver(...): IoReleaseRemoveLock(...);
return status:
}
NTSTATUS DriverB_D1spatchSometh1ng(...)
{
return ??:
}
Использование блокировки удаления драйвером А защищает драйвер В до возврата управления диспетчерской функцией драйвера В. Таким образом, если драйвер В завершает IRP или сам передает IRP вниз, используя loSkipCurrentlrp-StackLocation, участие драйвера В в обработке IRP заведомо завершится к том\ моменту, когда драйвер А сможет освободить блокировку удаления. Если бы драйвер В приостановил IRP, то драйвер А не удерживал бы блокировку удаления к тому времени, когда драйвер В доберется до завершения IRP. Однако можно предположить, что в драйвере В будет реализован какой-либо механизм чистки очередей от незавершенных IRP перед возвратом управления из его функции HandleRemoveDevice. Пока этого не произойдет, драйвер А не будет вызывать loDetachDevice или возвращать управление из своей функции HandleRemoveDevice.
Проблемы возникнут только в одном случае: если драйвер В передаст вниз IRP с функцией завершения, установленной при помощи исходного макроса loSetCompletionRoutine. Но даже тогда, если обработка IRP драйвером нижнего уровня будет выполнена корректно, его функция HandleRemoveDevice не вернет управление до завершения IRP. Имеется лишь малая вероятность того, что драйвер В будет выгружен перед запуском функции завершения.
К сожалению, у драйверов не существует абсолютно падежной защиты от выгрузки во время обработки IRP. В любой схеме, придуманной мной или вами, неизбежно присутствует риск выполнения по крайней мере одной команды (возврата) уже после того, как образ драйвера будет выгружен из памяти. Тем не менее, используя описанные приемы, можно свести риск к минимуму.
Как DEVQUEUE работает с РпР
В отличие от других примеров данной книги, я собираюсь привести полную реализацию объекта DEVQUEUE, хотя весь исходный код имеется в прилагаемых материалах. В данном случае я делаю исключение, так как считаю, что откоммен-
Управление переходами состояний РпР
321
тированные листинги функций помогут вам лучше понять, как пользоваться этим объектом. Основные функции уже были описаны в предыдущей главе, и здесь основное внимание будет уделено функциям, связанным с IRP_MJ_PNP.
Приостановка очереди
В приостановке очередей IRP задействованы две функции DEVQUEUE:
VOID NTAPI Stall Requests(PDEVQUEUE pdq)
{
Interlockedlncrement(&pdq->stalIcount);	// 1
}
BOOLEAN NTAPI CheckBusyAndStal 1 (PDEVQUEUE pdq)
{
KIRQL oldirql:
KeAcquireSpinLock(&pdq->lock, &oldirql);	// 2
BOOLEAN busy = pdq->Current!rp != NULL:	// 3
if (!busy)
Interlockedlncrement(&pdq->stallcount):	// 4
KeReleaseSpinLock(&pdq->lock, oldirql);
return busy;
}
1.	Чтобы приостановить запросы, достаточно задать счетчику ненулевое значение. Защищать операцию спин-блокировкой не требуется, потому что любой поток, участвующий в «гонке» за изменение значения, также будет использовать атомарный инкремент или декремент.
2.	Функция CheckBusyAndStall должна выполняться как атомарная, поэтому мы прежде всего захватываем спин-блокировку очереди.
3.	Если значение Currentlrp отлично от NULL, это значит, что устройство занято обработкой запроса из данной очереди.
4.	Если устройство в настоящий момент свободно, команда начинает процесс приостановки очереди, тем самым предотвращая последующий переход устройства в занятое состояние.
Вспомните, что функции StartPacket и StartNextPacket не отправляют IRP функции Startlo очереди, пока счетчик приостановки отличен от нуля. Кроме того, функция InitializeQueue инициализирует счетчик приостановки 1, поэтому жизненный цикл очереди начинается в приостановленном состоянии.
Перезапуск очереди
Приостановка очереди снимается функцией RestartRequests. Эта функция напоминает функцию StartNextPacket, приведенную в главе 5:
VOID RestartRequests(PDEVQUEUE pdq. PDEVICE_OBJECT fdo)
{
KIRQL oldirql:
KeAcquireSpinLock(&pdq->lock. &oldirql):	// 1
322
Глава 6. Поддержка Plug and Play для функциональных драйверов
if (Inter!ockedDecrement(&pdq->stallcount) >0)	// 2
KeRel easeSpi nLock(&pdq->1ock, oldi rql): return:
}
while (!pdq->stallcount && !pdq->CurrentIrp && !pdq->abortstatus // 3
&& HsListEmpty(&pdq->head))
{
PLIST_ENTRY next = RemoveHeadList(&pdq->head);
PIRP Irp = CONTAININGJECORDCnext. IRP.
Tail.Overlay.ListEntry);
if (IloSetCancelRoutinedrp, NULL))
{
InitializeListHead(&Irp->Tai1.Overlay.ListEntry);
continue;
}
pdq->CurrentIrp = Irp;
KeReleaseSpinLockFromDpcLevel(&pdq->lock);
(*pdq->StartIo)(fdo, Irp);
KeLowerlrql(oldirql);
return;
}
KeReleaseSpinLock(&pdq->1ock, oldi rql);
}
1.	Захват спин-блокировки очереди предотвращает нежелательное вмешательство при одновременном вызове StartPacket.
2.	Уменьшение счетчика приостановки. Если счетчик все еще отличен от нуля, очередь остается приостановленной и мы возвращаем управление.
3.	Этот цикл дублирует аналогичный цикл в функции StartNextPacket. Дублирование кода позволяет выполнить все действия функции за одно обращение к спин-блокировке.
ПРИМЕЧАНИЕ--------------------------------------------------------------------------
В первом издании книги была описана гораздо более простая — и неправильная — реализация RestartRequests. Один из читателей указал на возможность возникновения «гонки» между этой реализацией и StartPacket. Здесь приводится исправленная версия, которая была опубликована на моем веб-сайте.
Ожидание текущего IRP
Возможно, обработчику IRP_MN_STOP_DEVICE потребуется дождаться завершения текущего IRP (если он есть); задача решается вызовом WaitForCurrentlrp:
VOID NTAPI WaitForCurrentlrpCPDEVQUEUE pdq)
{
KeClearEvent(&pdq->evStop);	// 1
ASSERT(pdq->sta11 count != 0);	//2
KIRQL oldirql;
Управление переходами состояний РпР
323
KeAcquireSpinLock(&pdq->lock, &oldirql);	// 3
BOOLEAN mustwait = pdq->CurrentIrp != NULL:
KeReleaseSpinLock(&pdq->lock, oldirql);
if (mustwait)
KeWaitForSingleObject(&pdq->evStop, Executive, KernelMode, FALSE, NULL):
}
1.	Функция StartNextPacket при каждом вызове устанавливает событие evStop. Мы хотим удостовериться в том, что планируемое ожидание не завершится из-за просроченного сигнала, и поэтому прежде всего сбрасываем событие.
2.	Нет смысла вызывать эту функцию без предварительной приостановки очереди. В противном случае StartNextPacket просто запустит следующий IRP, если он есть, и устройство снова окажется занятым.
3.	Если устройство занято в настоящий момент, мы ожидаем по событию evStop, пока кто-нибудь не вызовет StartNextPacket для установки этого события. Анализ Currentlrp необходимо защитить спин-блокировкой, потому что в общем случае проверка указателя на NULL не является атомарным событием. Если указатель равен NULL сейчас, он не может измениться позднее, потому что мы предполагаем, что очередь приостановлена.
Прерывание запросов
Непредвиденное удаление устройства требует, чтобы мы немедленно пресекали все попытки необработанных запросов IRP обратиться к оборудованию. Кроме того, нужно позаботиться о том, чтобы все дальнейшие IRP отвергались. Функция AbortRequests поможет решить эти задачи:
VOID NTAPI AbortRequests(PDEVQUEUE pdq, NTSTATUS status)
{
pdq->abortstatus = status:
CleanupRequests(pdq, NULL, status):
}
Задание abortstatus переводит очередь в состояние REJECTING, в котором все будущие IRP будут отвергаться с кодом состояния, предоставленным вызывающей стороной. На этой стадии вызов CleanupRequests — с указателем на объект файла, равным NULL, чтобы функция CleanupRequests обработала всю очередь, — полностью опустошает очередь.
Мы не рискуем что-либо сделать с запросом IRP, в настоящий момент занятым активной работой с оборудованием. Драйверы, не использующие уровень HAL для работы с оборудованием (например, драйверы USB, зависящие от драйверов концентраторов и хостовых контроллеров), могут рассчитывать на то, что другой драйвер приведет к аварийному завершению текущего IRP. Однако драйверы, использующие HAL, должны принять меры, чтобы не «подвесить» систему или, по крайней мере, не оставить IRP в неопределенном состоянии, потому что несуществующее оборудование не сможет сгенерировать
324
Глава 6. Поддержка Plug and Play для функциональных драйверов
прерывание, которое бы позволило IRP завершиться. Для решения подобных проблем вызывается функция AreRequests Being Aborted:
NTSTATUS AreRequestsBeingAborted(PDEVQUEUE pdq)
{
return pdq->abortstatus;
}
Кстати говоря, было бы глупо использовать спин-блокировку очереди в этой функции. Допустим, мы сохранили моментальное значение abortstatus способом, безопасным в отношении потоков и многопроцессорных систем. Возвращаемое значение может устареть сразу же, как только мы освободим спин-блокировку.
ПРИМЕЧАНИЕ----------------------------------------------------------------------
Если ваше устройство может быть удалено так, что незавершенные запросы попросту «зависают», предусмотрите сторожевой таймер, который бы уничтожал IRP по истечении заданного интервала. См. раздел «Сторожевые таймеры» главы 14.
Иногда бывает нужно отменить последствия предшествующего вызова Abort-Request. Функция AllowRequests позволяет нам это сделать:
VOID NTAPI AllowRequests(PDEVQUEUE pdq)
{
pdq->abortstatus = (NTSTATUS) 0:
}
Другие конфигурационные функции
До настоящего момента мы обсуждали важные концепции, которые необходимо знать при написании драйверов реального оборудования. Сейчас речь пойдет о двух менее важных дополнительных кодах функций — IRP_MN_FILTER-RESOURCE_ REQUIREMENTS и IRP_MN_DEVICE_ USAGE-NOTIFICATION, которые также могут пригодиться вам в реальных драйверах. В конце раздела я покажу, как зарегистрироваться для получения оповещений о событиях РпР, относящихся к другим устройствам (не только к обслуживаемым вашим драйвером).
Фильтрация требований к ресурсам
Иногда РпР Manager получает неверную информацию о требованиях вашего драйвера к ресурсам. Это может произойти из-за ошибок в оборудовании и микрокоде, ошибок в INF-файлах наследных устройств и по другим причинам. Система предоставляет «аварийный клапан» в виде запроса IRP_MN_FILTER_RESOURCE_ REQUIREMENTS, который дает возможность просмотреть, а возможно, изменить список ресурсов, прежде чем РпР Manager перейдет к процессу согласования и назначения, завершающемуся получением драйвером IRP запуска устройства.
При получении запроса на фильтрацию субструктура FilterResourceRequirements объединения Parameters в элементе стека содержит указатель на структуру данных
Другие конфигурационные функции
325
IO_RESOURCE_REQUIREMENTS_LIST с перечнем требований к ресурсам для вашего устройства. Кроме того, если какой-либо из вышележащих драйверов обработал IRP и изменил требования к ресурсам, поле IRP loStatus.Information ссылается на вторую структуру IO_RESOURCE_REQUIREMENTS_LIST, с которой вам следует работать. Общая стратегия выглядит примерно так: если вы хотите включить ресурс в текущий список требований, это делается в диспетчерской функции. Затем IRP синхронно передается вниз по стеку, то есть методом Forward And Wait, используемым с запросом на запуск устройства. После возврата управления вы можете изменить или удалить любые описания ресурсов в полученном списке.
Вот краткий и не слишком полезный пример, демонстрирующий механику процесса фильтрации:
NTSTATUS HandleFilterResources(PDEVICE_OBJECT fdo. PIRP Irp)
{
PDEVICEJXTENSION pdx =
(PDEVICE-EXTENSION) fdo->DeviceExtensi on;
P10JTfACK_LOCATION stack = loGetCurrentlrpStackLocatlon(Irp);
PIO_RESOURCE_REQUIREMENTS_LIST original = stack^Parameters // 1
.EllterResourceRequi rements.JoResourceRequIrementList;
PIO_RESOURCE_REQUIREMENTSJ_IST filtered =	//2
(PIO_RESOURCE__REQUIREMENTS-L1ST) Irp->IoStatus.Information;
PIO__RESOURCE_REQUIREMENTS_LIST source =	//3
filtered ? filtered : original;
if (source->AlternativeL1sts 1= 1)	// 4
return DefaultPnpHandlerCfdo, Irp);
ULONG sizelist = source->ListSize;	// 5
PIO-RESOURCE-REQUIREMENTS-LIST newlist =
(PIO-RESOURCE-REQUIREMENTS-LIST) ExAllocatePool(PagedPool. sizelist + sizeof(IO-RESOURCEJ)ESCRIPTOR));
if (!newlist)
return DefaultPnpHandlerCfdo, Irp);
RtlCopyMemory(newlist, source, sizelist);
newlist->ListSize += sizeof(IO_RESOURCE_DESCRIPTOR);	// 6
PIO_RESOURCE_DESCRIPTOR resource =
&newlist->List[0].Descriptors[newl1st->List[0].Count++];
RtlZeroMemoryCresource, sizeof(IO_RESOURCE_DESCRIPTOR)); resource->Type = CmResourceTypeDevicePrivate;
resource->ShareDisposition = CmResourceShareDeviceExclusive; resource->u.Deviceprivate.DataEO] = 42;
Irp->IoStatus.Information = (ULONG_PTR) newlist;	//7
if (filtered && filtered != original)
ExFreePool(filtered);
NTSTATUS status = ForwardAndWait(fdo, Irp);	// 8
if (NT_SUCCESS(status)) { // Разное }
Irp->IoStatus.Status = status;	H 9
326
Глава 6. Поддержка Plug and Play для функциональных драйверов
IoCompleteRequest(I гр, 10_N0_INCREMENT);
return status;
1.	Параметры запроса включают список требований к ресурсам ввода/вывода. Они определяются по данным из конфигурационного пространства устройства, реестра или иного места, в котором драйвер шины рассчитывает их найти.
2.	Возможно, драйверы более высоких уровней уже отфильтровали ресурсы, добавив новые требования в исходный список. В этом случае они включают в поле loStatus.Information указатель на структуру списка расширенных требований.
3.	Если отфильтрованного списка нет, дополняется исходный список, а если есть — отфильтрованный список.
4.	Теоретически, могут существовать несколько альтернативных списков требований, но обработка этой ситуации выходит за рамки простого примера.
5.	Все ресурсы должны добавляться до того, как запрос будет передан вниз по стеку. Сначала мы создаем новый список требований, а затем копируем в него старые требования.
6.	Приняв меры по сохранению исходного порядка дескрипторов, мы добавляем собственное описание ресурсов. В этом примере добавляется ресурс, приватный для драйвера.
7.	Адрес расширенного списка требований сохраняется в поле IRP loStatus. Information, в котором его будут искать драйверы нижнего уровня и система РпР. Если бы мы просто дополнили уже отфильтрованный список, то нам пришлось бы освободить память, занимаемую старым списком.
8.	Запрос передается вниз при помощи той же вспомогательной функции, которая использовалась для IRP_MN_START_DEVICE. Если бы мы не собирались изменять дескрипторы ресурсов на обратном пути IRP вверх по стеку, можно было бы просто вызвать DefaultPnpHandler и распространить возвращенное состояние.
9.	При завершении IRP, независимо от возврата признака успеха или неудачи, будьте внимательны и не изменяйте поле Information блока состояния ввода/вывода: оно может содержать указатель на списки требования к ресурсам, установленные каким-то драйвером (может быть, даже нашим!) на пути вниз по стеку. PnP Manager освободит память, занимаемую этой структурой, когда она станет ненужной.
Оповещения об использовании устройства
Драйверы дисковых устройств (а также драйверы дисковых контроллеров) особенно хорошо должны знать, как они будут использоваться операционной системой. Запрос IRP_MN_DEVICE_USAGE_NOTIFICATION дает им возможность получить эту информацию. Элемент стека ввода/вывода для IRP хранит два параметра в под
Другие конфигурационные функции
327
структуре Parameters.UsageNotification (табл. 6.4). Значение Туре определяет один из нескольких специальных вариантов использования, a InPath (логическое значение) указывает, входит ли устройство в необходимый для этого использования путь.
Таблица 6.4. Поля подструктуры Parameters.UsageNotjfication элемента стека ввода/вывода
Параметр Описание
InPath TRUE, если устройство входит в путь использования Type; FALSE, если не входит Туре Тип использования, к которому относится IRP
В диспетчерскую подфункцию для этого оповещения включается команда switch (или другая логическая конструкция), которая различает несколько возможных вариантов оповещения. В большинстве случаев запрос IRP просто передается по стеку. Соответственно, основа диспетчерской подфункции выглядит так:
NTSTATUS HandleUsageNotification(PDEVICE_OBJECT fdo, PIRP Irp)
{
PDEVICE-EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtens1 on:
PIO_STACKJ_OCATION stack = loGetCurrentlrpStackLocation(Irp):
DEVICE_USAGE_NOTIFICATION_TYPE type =
stack->Parameters.UsageNoti flcati on.Type;
BOOLEAN inpath = stack ^Parameters.UsageNotification.InPath:
switch (type)
{
case DeviceUsageTypeHibernation:
Irp->IoStatus.Status = STATUS-SUCCESS:
break:
case DeviceUsageTypeDumpFile:
Irp->IoStatus.Status = STATUS-SUCCESS:
break:
case DeviceUsageTypePaging:
Irp->IoStatus.Status = STATUS-SUCCESS;
break;
default:
break:
}
return DefaultPnpHandler(fdo. Irp);
}
Поле Status в IRP задается равным STATUS_SUCCESS только для явно распознаваемых оповещений, таких как сигнал драйверу шины о том, что оповещения были юработаны. Драйвер шины считает, что вы не знаегс (а следовательно, и не обрабатываете) об оповещениях, для которых не устанавливается STATU S_SUCCESS.
328
Глава 6. Поддержка Plug and Play для функциональных драйверов
Допустим, вы знаете, что ваше устройство не поддерживает некоторый тип использования. Например, из-за какой-то особенности дисковое устройство не может использоваться для хранения файла спящего режима. В этом случае при установленном флаге In Path IRP следует отклонить:
case DeviceUsageTypeHIbernatlon:
If (Inpath)
return CompleteRequestCIrp, STATUS-UNSUCCESSFUL, 0);
В оставшейся части этого раздела будут кратко описаны все типы использования, определенные в настоящее время.
DeviceUsageTypePaging
Оповещение InPath =TRUE означает, что на устройстве будет открыт файл подкачки. Оповещение InPath=FALSE означает, что файл подкачки был закрыт. Как правило, в драйверах ведется счетчик файлов подкачки, о которых вы получили оповещения. Пока хотя бы один файл подкачки остается активным, запросы STOP и REMOVE завершаются отказом. Кроме того, при получении первого оповещения подкачки убедитесь в том, что диспетчерские функции запросов READ, WRITE, DEVICE__CONTROL, PNP и POWER зафиксированы в памяти (за дополнительной информацией о выгрузке драйверов обращайтесь к разделу «Адресные пространства пользовательского режима и режима ядра» главы 3). Также сбросьте флаг DO_POWER_PAGABLE в объекте устройства, чтобы заставить Power Manager отправлять вам IRP питания на уровне DISPATCH__LEVEL. Для пущей надежности я также рекомендую обнулить все регистрации оповещений о бездействии устройства (см. главу 8).
ПРИМЕЧАНИЕ-----------------------------------------------------------------
В главе 8 я расскажу, как установить флаг DCLPOWER_PAGABLE в объекте устройства. Вы должны проследить за тем, чтобы этот флаг никогда не сбрасывался, если он установлен в объекте устройства следующего нижнего уровня. Флаг должен сбрасываться только в функции завершения, после того как драйверы нижнего уровня сбросят свои флаги. Функция завершения вам так или иначе понадобится, потому что в случае сбоя IRP на нижних уровнях необходимо отменить все, что было сделано в диспетчерской функции.
DeviceUsageTypeDumpFile
Оповещение InPath=TRUE означает, что устройство было выбрано для сохранения файла аварийного дампа (если такая необходимость возникнет). Оповещение InPath=FALSE отменяет выбор. Включите в драйвер счетчик разности между оповещениями TRUE и FALSE. Пока счетчик отличен от нуля:
О следите за тем, чтобы код управления питанием (см. главу 8) никогда не выводил устройство из состояния DO (полного включения). Для оптимизации энергопотребления устройством можно дополнительно анализировать значение ShutdownType в системных IRP управления питанием с учетом других типов использования, о которых вы получили оповещения. Впрочем, эта нетривиальная тема выходит за рамки книги;
Другие конфигурационные функции
329
Э избегайте регистрации устройства на выявление бездействия и обнулите все действующие регистрации;
Э убедитесь в том, что драйвер отклоняет запросы на остановку и удаление устройства.
DeviceUsageTypeHibernation
Оповещение InPath=TRUE означает, что устройство было выбрано для сохранения состояния спящего режима (если его потребуется сохранить). Оповещение InPath=FALSE отменяет такой выбор. Включите в драйвер счетчик разности между оповещениями TRUE и FALSE. Ваша реакция на системные IRP управления питанием, в которых задается состояние PowerSystemHibernate, будет отличаться от обычной, потому что устройство будет использовано для сохранения файла спящего режима. Реализация этой специфической особенности драйверов дисковых устройств выходит за рамки книги.
Оповещения РпР
В Windows ХР и Windows 98/Ме предусмотрена возможность оповещения компонентов как пользовательского режима, так и режима ядра о некоторых событиях РпР. В Windows 95 было сообщение WMJDEVICECHANGE, которое использовалось программами пользовательского режима для наблюдения (а иногда л управления) за состоянием оборудования и изменениями в энергопотреблении системы. В более новых операционных системах сообщение WM_DEVICECHANGE используется программами пользовательского режима для обнаружения включения или отключения драйверами зарегистрированных интерфейсов устройств. Драйверы режима ядра также регистрируются для получения похожих оповещений.
ИМЕЧАНИЕ---------------------------------------------------------------
Обращайтесь к документации по WM-DEVICECHANGE, RegisterDeviceNotification и UnregisterDevice-Notification в Platform SDK. Я приведу примеры использования этого сообщения и функций API, но не стану объяснять все возможные применения. Некоторые из приводимых пояснений также предполагают, что читатель достаточно хорошо владеет программированием в среде Microsoft -oundation Classes.
Сообщение WM_DEVICECHANGE
Приложение с окном может подписаться на сообщения WM_DEVICECHANGE, связанные с GUID (глобально-уникальным идентификатором) конкретного интерфейса. Например:
DEV_BROADCAST_DEVICEINTERFACE filter;
filter.dbcc_size = sizeof(filter);
filter,dbcc_devicetype = DBTJ3EVTYPJ3EVICEINTERFACE: filter.dbcc_classguid = GUID_DEVINTERFACE_PNPEVENT;
m_hInterfaceNotification =
RegisterDeviceNotificationtmJnWnd, ^filter, 0);
330
Глава 6. Поддержка Plug and Play для функциональных драйверов
ПРИМЕЧАНИЕ--------------------------------------------------------------
Пример PNPEVENT показывает, как организовать обработку сообщений WM_DEVICECHANGE для отслеживания событий вставки и удаления. Фрагменты кода в этом разделе взяты из программ» TEST, прилагаемой к примеру. Сам по себе драйвер PNPEVENT не очень интересен.
Центральное место в этом фрагменте занимает вызов RegisterDeviceNotification. он требует, чтобы РпР Manager отправлял нашему окну сообщение WM_DEVICE-CHANGE каждый раз, когда кто-нибудь включает или отключает интерфейс GUID_ DEVINTERFACE_PNPEVENT. Предположим, драйвер устройства вызывает loRegisterDevicelnterface с этим GUID интерфейса в своей функции AddDevice. Мы хотим получать оповещения о том, что этот драйвер вызывает loSetDevicelnterfaceState для включения или отключения этого зарегистрированного интерфейса.
Когда приложение располагает манипулятором устройства, оно может зарегистрироваться на оповещения, касающиеся этого конкретного манипулятора:
DEV_BROADCAST_HANDLE filter = {0}:
filter.dbch_size = sizeof(filter);
filter.dbch_devicet.ype = DBT_DEVTYP_HANDLE;
filter.dbch_handle = m_hDevice;
mJiHandleNotification =
RegisterDeviceNotification(m_hWnd. Sfilter, 0);
Для каждого зарегистрированного манипулятора в конечном итоге должна быть вызвана функция UnregisterDeviceNotification. При подготовке первого издания книги я обнаружил, что эта функция дестабилизирует Windows 98. Один из читателей вычислил следующие недокументированные правила безопасного использования этой функции:
О функция UnregisterDeviceNotification должна вызываться, пока окно, манипулятор которого был указан при регистрации, продолжает существовать;
О в Windows 98 (и, вероятно, Windows Me) функцию UnregisterDeviceNotification не следует вызывать из обработчика оповещающего сообщени, относящегося к тому же манипулятору оповещения. С другой стороны, в Windows 2000 и Windows ХР это допустимо.
После того как приложение зарегистрируется для получения оповещений, система начнет посылать ему оконные сообщения WM_DEVICECHANGE, уведомляющие о различных событиях, представляющих возможный интерес. Здесь мы рассмотрим оповещения DBT_DEVICEQUERYREMOVE и DBT.DEVICEREMOVECOMPLETE, которые представляют особый интерес для приложений. Если приложение не будет правильно обрабатывать оба этих сообщения, РпР Manager не сможет успешно реализовать два самых частых сценария удаления устройств.
Оповещения QUERYREMOVE и REMOVECOMPLETE связываются с конкретным манипулятором. В программе TEST примера PNPEVENT они обрабатываются так:
BEGIN_MESSAGE_MAP(CTestDl g. CDi а 1 og)
//{{AFX_MSG_MAP(CTestDlg)
//}}AFX_MSG_MAP
ON_WM__DEVICECHANGE()
Другие конфигурационные функции
331
ENDJESSAGEJAPO
BOOL CTestDlg: :0nDe vice Change ((JI NT nEventType. DWORD dwData)
{
If (!dwData)
return TRUE:
_DEVJ3ROADCAST_HEADER* p = (_DEV_BROADCAST-HEADER*) dwData;
if (p->dbcd_devicetype ~= DBTJJEVTYPJDEVICEINTERFACE)
return Handl eDevi ceChanget nEventType,
(PDEV^BROADCAST_DEVIСЕINTERFACE) p);
else if (p->dbcd_devicetype == DBT_DEVTYP_HANDLE)
return Handl eDevi ceChange(nEventType,
(PDEVBROADCAST-HANDLE) p);
else
return TRUE;
}
BOOL CTestDlg::HandleDev1ceChange(0W0RD evtype.
PDEV_BROADCAST_HANDLE dhp)
{
if (dhp->dbchjnandle != mJiDevice) return TRUE;
switch (evtype)
{
case DBTDE VICEQUERYREMOVE;	if (\<можно удалить устройство^ // 1
return BROADCAST_QUERYJENY;
case DBTDEV ICEREMOVECOMPLETE:
case D6T-DEVICEREM0VEPENDING:
1 m_h andleNotification && !win98)	2
Unregi sterDeviceNoti fi cation(ffl-hHandleNot'flcati on);
ffi-hHandleNotification = NULL:
}
CloseHandle(m_hDevice);	// 3
break; }
return TRUE;
1.	Программа TEST в этой точке выводит окно сообщения с вопросом, можно ли удалить устройство. Возможно, в реальном приложении у вас будут свои причины для сомнений. Если вы решите, что устройство можно удалить, управление переходит к следующей секции — case. Я обнаружил, что в Windows 98/Ме было необходимо закрыть манипулятор немедленно, вместо того чтобы ждать другого оповещения.
332
Глава 6. Поддержка Plug and Play для функциональных драйверов
2.	Как упоминалось ранее, закрыть манипулятор при обработке оповещения можно в Windows 2000 и ХР, но не в Windows 98/Ме.
3.	То, ради чего все делалось: мы хотим закрыть манипулятор устройства, когда оно собирается исчезнуть или уже исчезло.
Я рекомендую провести эксперимент, который позволит вам опробовать оба пути выполнения. Запустите тестовую программу для PNPEVENT и установите устройство PNPEVENT (о том, как это делается, рассказано в файле PNPEVENT. НТМ). Если в системе работает DvgView (см. http://www.sysinternals.com), в ней появляется отладочный вывод, показанный в строках 0-12 на рис. 6.6. В окне TEST выводится сообщение о появлении устройства (первая строка на рис. 6.7). Теперь выполните инструкции по установке и запуску апплета PNPDTEST из каталога DDK Tools и найдите запись устройства PNPEVENT (рис. 6.8). Она размещается с отступом под корневым узлом списка устройств. Щелкните на кнопке Test Surprise Remove в окне PNPDTEST.
ЖВжВщШРйЖ '-Vi'-:	’ >- .-'.-'.г 				Я|«Я1в гШВИЯ :Й111	-.'У
"О		|- DebugА:A/АЖ-? >"- ><:?.А - А-			
0	0.00000000	GENERIC -	- Dlllnitialise		
- 1	0.00227716	PNPEVENT	- Entering DriverEntry: DriverObject 8CFDDA60		
!2	0.00233781	PNPEVENT	- Running under NT		
 3	0.00365110	PNPEVENT	- Entering AddDevice: DrivarObject 80FDDA60, pdo	81С014В8	
?4	0.00374768	GENERIC -	- Initializing for PNPEVENT		
" 5	0.00795864	PNPEVENT	- PNP Request (IRP MN_FILTER RESOURCE—REQUIREMENTS)		
6	0.00803422	PNPEVENT	- PNP Request (IRPmMN_START_DEVICE)		
?	0.00807920	PNPEVENT	- To WORKING from STOPPED		
8	0.01100187	PNPEVENT	- PNP Request (IRP_MN_QUERY_CAPABILITIES)		
9	0.01160895	PNPEVENT	- PNP Request (IRF_MN_QUERY_PNP_DEVICE_STATE)		
10	0.01160471	PNPEVENT	- PNP Request (IRP_MN_QUERY_DE VICE-RELATIONS)		
11	0.01319003	PNPEVENT	- IRP„MJ_CREATE		
12	0.01410482	PNPEVENT	- PHP Request (IRP-MN_QUERY_DEVICE_RELATIONS)		
1 13	21.89183071	PNPEVENT	- PNP Request (IRP_MN_QUERY_DEVICE_RELATIONS)		
i 14	24.98957547	PNPEVENT	- IRP_MJ_CLOSE		
 15	24.99095984	PNPEVENT	- PNP Request (IRP„MN_QUERY—REMOVE-DEVICE)		
ч 16	24.99103198	PNPEVENT	- To PENDINGREMOVE from WORKING		
17	24.99365598	PNPEVENT	- PNP Request. (IRP_MN_REMOVE_DEVICE)		
; 18	24.99495700	PNPEVENT	- To REMOVED from PENDINGREMOVE		
; 19	25.03913735	PNPEVENT	- Entering DriverUnload: DriverObject 80FDDA60		
20	25.03943684	GENERIC -	- DllUnload		
121	34.28150737	GENERIC -	- Dlllnitialize		
122	34.28275922	PNPEVENT	- Entering DriverEntry; DriverObject 80FDDA60		
Й 23	34.28280688	PNPEVENT	- Running under NT		
: 24	34.28415931	PNPEVENT	- Entering AddDevice: DriverObject 80FDDA60, pdo	81С014В8	
; 25	34.28425163	GENERIC -	 Initializing for PNPEVENT		
26	34.28619274	PNPEVENT	- PNP Request (IRP_MN_FILTEI? RESOURCE REQUIREMENTS)		
27	34.28669794	PNPFILTR:	Received IRP_MN_START_DEVICE for PDO 0x81c014b8.		
1 28	34.28874795	PNPEVENT	- PNP Request (IRP_MN START_EEVICE)		
29	34.28878783	PNPEVENT	- To WORKING from STOPPED		
30	34.28971353	PNPFILTR:	Completing Start request with status « 0x0.		
31	34.29070312	PNPEVENT	- IRP_MJ_CREATE		
32	34.29087653	PNPEVENT	- PNP Request (IRP_MN_OUER?_CAPAEII1ITIES)		
<33	34.29137653	PNPEVENT	- PNP Request (IRP_MN_QUERY_DEVICE_RELATIONS)		
4 34	34.29146130	PNPEVENT	- PNP Request (IRP_MN_QUER¥_PNP_DEVICE_STATE)		
35	34.29153518	PNPFILTR:	Completing IRP_MN_QUERy_PNP_DEVICE_STATE for PDO	0z81c014b8 with	status
36	34.29162115	PNPEVENT	- PNP Request (IRP_MN_QUERY_DEVICE_RELATIONS)		
1 37	34.33739826	PNPEVENT	- PNP Request (IRP_MN_QUERY_PNP_DEVICE_STATE)		
;'38	34.33750326	PNPFILTR:	Completing IRP_MN_QUERY_PNP_DEVICE_STATE for PDO	Он81с014Ь8 with	statusj
" 39	34.33767018	PNPEVENT	- PNP Request (IRP_MN_QUER¥_DEVICE_RELATIONS)		
1 40	34.33778612	PNPFILTR;	Received IRP MN SURPRISE-REMOVAL for PDO 0x81c014b8.		
->	34.33788777	PNPEVENT	- PNP Request (IRP_MN_SURPRISE_REMOVAL)		
42	34.33793241	PNPEVENT	- To SURPRISEREMOVED from FORKING		
43	36.25171966	PNPEVENT	- IRP_MJ_CLOSE		
44	36.25334923	PNPFILTR:	Received IRP_MN_REMOVE_DEVICE for PDO Os81cO14b8		
45	36.25343825	PNPEVENT	- PNP Request (IRP_MN_EEMOVE_DEVICE)		
£ 46	36.25434009	PNPEVENT	- To REMOVED from SURPRISEREMOVED		
Ь 47	36.25444226	PNPFILTR:	FilterRemove returning 0x0 for PDO Ox81c014b8.		
5 48	36.25466027	PNPEVENT	- Entering DriverUnload: DriverObject 80FDDA60		
	36.25495445	GENERIC -	- DllUnload		
21.					;	:		  yK
Рис. 6.6. Трассировочный вывод в эксперименте PNPEVENT
Другие конфигурационные функции
333
PnP Event Driver Test Applet	'	ЕЗП|
‘Events:
• Device W?\rootttsamplett0000tt{6a061783-e697-11 d2-81 b5-00c04fa330a6} arrived
Device \\?\ROOTttSAMPLEttODOO#{6a061783-e697-11 d2-81 b5-00c04fa330a8} removed
i Device \\?\ROOTttSAMPLEtt0000tt{6a061783-e697-11d2-81b5-00c04fa330a8} arrived
Device \\?\ROOT USAMPLEttOOOOtt{6a061783-e697-11 d2-81 b5-00c04fa330a6) removed

-Send E‘'erJ | Г Ettit
Рис- 6-7- Трассировка событий TEST
PnP Driver Test

Device List:
Video Codecs
Standard 28800 bps Mod Standard 28800 bps Mod
Standard 28800 bps Mod
: WAN Miniport (L2TP)
: WAN Miniport (IP)
: WAN Miniport (PPPOE) WAN Miniport (PPTP)
I- Packet Scheduler Minipo Packet S cheduler M inipo Packet Scheduler Minipo Direct Parallel
Terminal Server Device F
Properties
REGISTRY INFORMATION
Description; PnP Event Sample
HardwarelD; *WC00601
Service:
Class:
Manufacturer: Walter Oney Software
PNPEVENT Sample
STATUS INFORMATION
Device driver running
Device may be reconfigured
Device was enumerated by root
Device instance is currently configurec
TerminalServer Keyboard Terminal Server Mouse D
PnP Event Sample!
.   Plug and Play Software D ;  Microcode Update Devic
11______J 2J i«l ................................. I
Test Removal Г Test Surprise Remove!
Te$t Rebalance ;r S-tM&W
Рис. 6.8. Окно PNPDTEST
Из-за особенностей внутреннего строения PNPDTEST все начинается с оповещения DBT.QUERYREMOVEDEVICE, чтобы дать возможность PNPDTEST отключить устройство для установки фильтрующего драйвера. TEST выводит диалоговое
334
Глава 6. Поддержка Plug and Play для функциональных драйверов
окно с вопросом, можно ли удалить устройство (рис. 6.9). Подтвердите удаление, в ответ на это TEST закрывает манипулятор (рис. 6.10). Далее PnP Manager отправляет драйверу PNPEVENT сообщение IRP_MN_QUERY__REMOVE_DEVICE, за которым следует IRP_MN_REMOVE_DEVICE. Этот период соответствует строкам 13-20 отладочной трассировки и второму сообщению события в окне TEST.
Okay to remove the device?
[t Ygs | No j
Рис. 6.9. TEST запрашивает разрешение на удаление устройства
Closing Handle
Рис. 6.10. TEST закрывает манипулятор
Теперь PNPDTEST перезапускает устройство (строки 21-32 трассировки и третье оповещение в окне TEST). Обратите внимание: в ответ на оповещение о появлении устройства TEST открывает новый манипулятор.
Наконец, PNPDTEST инициирует отправку запроса IRP_MN_SURPRISE__REMOVAL устройству PNPEVENT (на самом деле ассоциированный фильтрующий драйвер вызывает loInvalidateDeviceState, что в конечном итоге приводит к выдаче IRP непредвиденного удаления тем способом, который был описан ранее. Эти внутренние события описываются в строках 33-39 отладочной трассировки). PNPEVENT обрабатывает его способом, рассмотренным в этой главе (см. строки 40-42 отладочной трассировки).
На этой стадии PNPEVENT будет находиться в состоянии SURPRISEREMOVED. Драйвер не может быть выгружен, потому что манипулятор остается открытым. Тестовое приложение получает сообщение WM_DEVICECHANGE с кодом DBT_ DEVICEREMOVECOMPLETE. Приложение закрывает манипулятор (строка трассировки 43), на что PnP Manager отправляет PNPEVENT запрос IRP_MN_REMOVE_ DEVICE (строки трассировки 44-49).
Обратите внимание: в случае непредвиденного удаления запрос не доходит до приложения.
Я должен упомянуть еще об одном нетривиальном обстоятельстве в связи с оповещением DEJT_QUERYREMOVEDEVICE. Согласно документации Platform SDK, приложение должно вернуть специальный код BROADCAST_QUERY__DENY, чтобы отказать в удалении устройства. В Windows ХР этот механизм работает так, как предполагается. А именно, если вы зайдете в Диспетчер устройств и попробуете удалить устройство, а приложение откажет в удалении, устройство удалено не будет. Более того, в такой ситуации PnP Manager даже не отправит драйверу запрос IRP_MN„QUERY_REMOVE_DEVICE.
Однако в других версиях операционной системы BROADCAST_QUERY__DENY работает не так, как следовало бы ожидать. Диспетчер устройств игнорирует код возврата и помечает устройство для удаления. Впрочем, он понимает, что устройство удалить нельзя, и выводит диалоговое окно с предложением перезагрузить систему, чтобы удаление вступило в силу. Драйвер остается в памяти.
j нф игу рационные функции
335
овещения служб Windows ХР
Ьжбы (services) Windows ХР также могут подписываться на оповещения г. Служба должна зарегистрировать расширенный обработчик функцией ~ srerServiceCtrlHandlerEx, после чего она может регистрироваться на оповеще-Ьй с ; изменениях в интерфейсах устройства. Для примера рассмотрим слсдую-Ь код (см. пример AUTOLAUNCH главы 15):
► i -BROADCASTJ)EVICEINTERFACE filter = {0}:
I - 'ter.dbcc^slze = sizeof(filter);
I - <ter.dbcc_devicetype = DBT_DEVTYPE_DEVICEINTERFACE:
I f Iter.dbccjzlassguid = GUID_AUTOLAUNCH_NOTIFY;
I -^.Notification = Reg1sterDeviceNot1f1cation(gi_hServ1ce, .PVOID) ^filter. DEVICE_NOTIFY_SERVICE_HANDLE);
I Здесь mJiService — манипулятор службы, полученный oi диспетчера служб г । ее запуске, а код DEVICE_NOTIFY_SERVICE_HANDLE указывает, что вы регист-t/етесь на управляющие оповещения служб, а не на оконные сообщения. По-получения команды SERVICE_CONTROL__STOP следует отменить регистрацию ипулятора оповещения:
’ _nregisterDeviceNot1fication(mJnNotif1cation);
Когда происходит событие РпР, связанное с кодом GUID интерфейса, система вызывает функцию расширенного обработчика:
3RD __stdcall HandlerЕх(DWORD ctlcode, DWORD evtype.
PVOID evdata. PVOID context)
{
}
ie ctlcode = SERVICE_CONTROL_DEVICEEVENT, evtype = DBT.DEVICEARRIVAL или дру-)му коду DBTXxr, evdata — адрес Юникод версии структуры DEV_BROADCAST_ CEVICEINTERFACE, a context — значение контекста, указанное при вызове функции ^gisterServiceCtrlH ndlerEx.
Оповещения режима ядра
Драйверы WDM могут использовать функцию loRegisterPlugPlayNotification для подписки на оповещения, связанные с интерфейсами и манипуляторами. Далее приводится выдержка из драйвера PNPMON, регистрирующегося на оповещения о появлении и исчезновении GUID интерфейсов, задаваемых приложением (в данном случае программой TEST.EXE из PNPMON) посредством управляющей операции ввода вывода (IOCTL):
status = loRegisterPlugPlayNotification
(EventCategoryDevIcelnterfaceChange.
PNPNOTIFY_DEVICE_IN1ERFACE_INCLUDE_EXISTING_INTERFACES,
&p->gu1d. pdx->DriverObject,
(PDRIVER__NO1IFICATION_CALLBACK_ROUTINE) OnPnpNotlfy.
reg. &reg->InterfaceNotiflcationEntry);
336
Глава 6. Поддержка Plug and Play для функциональных драйверов
Первый аргумент указывает, что мы хотим получать оповещения каждый раз, когда кто-то включает или отключает интерфейс с конкретным GUID. Второй аргумент — флаг, указывающий, что мы хотим немедленно получить обратные вызовы для всех уже включенных экземпляров интерфейса с этим GUID. Этот флаг позволяет драйверу стартовать после некоторых (или всех) драйверов, предоставляющих указанный интерфейс, и все равно получить оповещающие обратные вызовы об этих интерфейсах. В третьем аргументе передается GUID интерфейса. В данном случае он поступает к нам через IOCTL от приложения. Четвертый аргумент содержит адрес объекта драйвера. PnP Manager создает дополнительную ссылку на этот объект, чтобы он не мог быть выгружен из памяти раньше времени. Пятый аргумент содержит адрес функции обратного вызова, через которую производится оповещение. Шестой аргумент определяет параметр контекста для функции обратного вызова. В данном случае я указал адрес структуры (reg) с информацией, относящейся к данному вызову регистрации. Седьмой и последний аргумент содержит адрес переменной, в которой РпР Manager сохраняет манипулятор оповещения. В дальнейшем манипулятор оповещения будет использован при вызове loUnregisterPlugPlayNotification.
Функция lollnregisterPlug Play Notification вызывается для закрытия манипулятора, полученного при регистрации. Поскольку loRegisterPlugPlayNotification создает дополнительную ссылку на объект драйвера, размещение этого вызова в функции Driverllnload никакой пользы не принесет. Функция Driverllnload не будет вызвана, пока счетчик ссылок не упадет до нуля, а этого никогда не случится, если отмена регистрации производится в самой функции Driverllnload. Проблема решается легко: просто необходимо выбрать подходящий момент для отмены регистрации например, при удалении последнего интерфейса конкретного типа или запроса IOCTL в приложении.
Если вы располагаете именем символической ссылки для включенного интерфейса, вы также можете запросить оповещения об изменениях в устройстве, соответствующем данной ссылке. Пример:
PUNICODE_STRING SymbolicLinkName; // <== Входные данные
PDEVICE_OBJECT DevIceObject; // <== Выходные данные
PFILE_OBJECT FileObject; // <== Тоже выходные данные loGetDeviceObjectPointerC&SymbolicLinkName, 0, &Fi1 eObject, &DeviceObject);
loRegi sterPlugPlayNoti fl cation(EventCategoryTargetDevi ceChange, 0, FileObject, pdx->DriverObject, (PDRIVER_NOTIFICATION_CALLBACK_ROUTINE) OnPnpNotify, reg, &reg->HandleNotificationEntry):
Кстати говоря, не включайте этот код в свои обработчики событий РпР. Функция loGetDeviceObjectPointer во внутренней реализации выполняет операцию открытия с именованным объектом устройства. При выполнении целевым устройством некоторых разновидностей операций РпР может возникнуть взаимная блокировка. Вместо этого следует запланировать рабочий элемент вызовом loQueueWorkltem. Более подробная информация о рабочих элементах приводится
Другие конфигурационные функции
337
в главе 14. Драйвер PNPMON демонстрирует применение рабочего элемента в этой конкретной ситуации.
Оповещения, получаемые в результате регистрации, принимают форму вызова заданной вами функции косвенного вызова:
NTSTATUS OnPnpNot1fy(PPLUGPLAY_NOTIFICATION_HEADER hdr,
PVOID Context)
{
return STATUS_SUCCESS;
}
Структура PLUGPLAY-NOTIFICATION_HEADER представляет собой общий заголовок для нескольких структур данных, используемых PnP Manager при работе с оповещениями:
typedef struct _PLUGPLAY_NOTIFICATION_HEADER {
USHORT Version:
USHORT Size;
GUID Event;
} PLUGPLAY_NOTIFICATION-HEADER.
*PPLUGPLAY_NOTIFICATION_HEADER:
GUID Event обозначает тип события, о котором вы получаете сообщение (табл. 6.5). Определения GUID находятся в заголовочном файле DDK WDMGUID.H.
Таблица 6.5. Коды GUID оповещений PnP	
Имя GUID	Цель оповещения
GUID_HWPROFILE_QUERY_CHANGE	Возможно ли переключение на новый профиль оборудования?
GUID_HWPROFILE-CHANGE_CANCELLED	Изменение, о котором раньше был отправлен запрос, отменено
GUID_HWPROFILE_CHANGE_COMPLETE	Изменение, о котором раньше был отправлен запрос, реализовано
GUID_DEVICE_INTERFACE_ARRIVAL	Интерфейс устройства только что был включен
GUID-DEVICEJNTERFACE-REMOVAL	Интерфейс устройства только что был отключен
GUID-TARGET-DEVICE-QUERY_REMOVE	Возможно ли удаление объекта устройства?
GUID_TARGET_DEVICE_REMOVE-CANCELLED	Удаление, о котором раньше был отправлен запрос, отменено
GUID_TARGET_DEVICE_REMOVE_COMPLETE	Удаление, о котором раньше был отправлен запрос, реализовано
При получении любого из оповещений DEVICE-INTERFACE аргумент hdr можно преобразовать в указатель на следующую структуру:
typedef struct _DEVICE_INTERFACE-CHAN6E_N0TIFICATI0N {
USHORT Version;
USHORT Size;
338
Глава 6. Поддержка Plug and Play для функциональных драйверов
GUID Event;
GUID InterfaceCIassGuid;
PUNICODE_STRING Symbol 1cLi nkName;
} DEVICE JNTER.FACE_CHANGE^NOTIFICATION, *PDEVICE_INTERFACE_CHANGE_NOTIFICATION;
Б структуре оповещения об изменении интерфейса поле InterfaceCIassGuid содержит GUID интерфейса, а поле SymbolicLinkName — имя экземпляра включаемого или отключаемого интерфейса.
При получении любого из оповещений TARGET-DEVICE аргумент hdr преобразуется в указатель на другую структуру:
typedef struct _TARGET_DEVICE_REMOVAL_NOTIFICATION {
USHORT Version;
USHORT Size;
GUID Event;
PFILE_OBJECT FileObject;
} TARGET-DEVICE_REMOVAL_NOTIFICATION,
*PTARGET_DEVICE_REMOVAL_NOTIFICATION;
i де FileObject — объект файла, для которого запрашиваются оповещения.
Наконец, при получении любого из оповещений HWPROFILE_CHANGE аргумент hdr представляет собой указатель на следующую структуру:
typedef struct _HWPROFILE_CHANGE_NOTIFICATION {
USHORT Version:
USHORT Size;
GUID Event;
} HWPROFILE-CHANGE-NOTIFICATION.
*PHWPROFILE_CHANGE_NOTIFICATION;
Структура не содержит дополнительной информации по сравнению с основной структурой заголовка — это всего лишь другое имя typedef.
Один из способов использования оповещений заключается в реализации фильтрующего драйвера для целого класса интерфейсов устройств (стандартный способ реализации фильтрующих драйверов — как для отдельных драйверов, так и для классов устройств, основанный на данных из реестра, будет рассмотрен в главе 16. Сейчас речь идет о фильтрации всех устройств, регистрирующих конкретный интерфейс, — для этой задачи другого механизма нс существует.) В функции DriverEntry своего драйвера вы регистрируетесь на оповещения РпР, относящиеся к одному или нескольким GUID интерфейсов. Получив оповещение о появлении, вы открываете объект файла loGetDeviceObjectPointer, а затем регистрируетесь на оповещения целевого устройства, относящиеся к ассоциированному устройству. Функция loGetDeviceObjectPointer также возвращает указатель на объект устройства, что позволяет отправлять IRP этому устройству, вызывая loCallDriver. Следите за возможным появлением оповещения GUID-TARGET-DEVICE-QUERY-REMOVE, потому что вы должны освободить ссылку на объект файла перед тем, как продолжится удаление файла.
Другие конфигурационные функции
339
1МЕР PNPMON--------------------------------------------------------------------------------
Пример PNPMON показывает, как происходят регистрация и обработка оповещений РпР в режиме ядра. Чтобы дать реальный пример, который можно запустить на компьютере и увидеть в работе, я заставил PNPMON передавать оповещения приложению пользовательского режима (которое называется TEST — а как еще?). Это выглядит немного глупо, потому что приложение пользовательского режима может получать оповещения самостоятельно, вызвав RegisterDeviceNotification. PNPMON отличается от других примеров драйверов в книге. Предполагается, что PNPMON будет загружаться динамически как вспомогательный модуль программы пользовательского режима. Другие рассматриваемые драйверы предназначены для управления оборудованием, реальным или вымышленным. Приложение пользовательского режима при помощи функций API Service Manager загружает модуль PNPMON, который создает ровно один объект устройства в своей функции DriverEntry, чтобы приложение могло использовать DeviceloControl для выполнения действий в режиме ядра. При выходе приложение закрывает манипулятор и вызывает функцию Service Manager для завершения драйвера.
PNPMON также включает драйвер виртуального устройства (VxD) для Windows 98/Ме, который может динамически загружаться тестовым приложением. В Windows 98/Ме существует возможность динамической загрузки драйверов WDM недокументированной функцией („NtKernLoadDriver, если вас это интересует), но не существует механизма выгрузки драйверов, загруженных подобным образом. Впрочем, вам не придется прибегать к недокументированным функциям, потому что драйверы VxD могут напрямую использовать большинство вспомогательных функций при помощи библиотеки импорта WDMVXD, входящей в Windows 98 DDK. (Эта библиотека отсутствует в части Windows ХР DDK, посвященной Windows Me.) В сущности, единственное, что вам необходимо сделать в проекте VxD, — это включить WDM.H до заголовочных файлов VxD и добавить WDMVXD.CLB в список входных файлов компоновщика. Таким образом, драйвер PNPMON.VXD просто регистрируется на оповещения РпР так, как если бы он был драйвером WDM, и поддерживает тот же интерфейс IOCTL, что и PNPMON.SYS.
Нестандартные оповещения
В завершение этого раздела я объясню, как драйвер WDM может генерировать нестандартные оповещения РпР. Чтобы подать сигнал о нестандартном событии РпР, создайте экземпляр пользовательской структуры оповещения и вызовите одну из функций: ToReportTargetDeviceChange или loReportTargetDeviceChange-Asynchronous. Асинхронная версия возвращает управление немедленно. Синхронная версия ждет (достаточно долго, по моему опыту), пока оповещение будет отправлено. Объявление структуры оповещения выглядит так:
typedef struct _TARGET_DEVICE_CUSTOM_NOTIFICATION {
USHORT Version:
USHORT Size;
GUID Event:
PFILE-OBJECT Fl 1 eObject;
LONG NameBufferOffset;
UCHAR CustomDataBuffer[l];
} TARGET J)EVICE_CUSTOM^NOTIFICATION,
*PTARGET_DEV I CE_CUSTOM__NOTI FI CATION;
Поле Event содержит GUID, определенный вами для оповещения. Поле File-Object равно NULL — PnP Manager будет отправлять оповещения драйверам, открывшим объекты файлов для объекта PDO, указанного при вызове loReportXrx Поле CustomDataBuffer содержит любые двоичные данные по вашему усмотрению,
340
Глава 6. Поддержка Plug and Play для функциональных драйверов
за которыми следуют строковые данные в Юникоде. Поле NameBufferOffset равно -1, если строковые данные отсутствуют, в противном случае оно содержит длину двоичных данных, предшествующих строковым. Чтобы определить общий размер данных, достаточно вычесть смещение CustomDataBuffer из Size.
А вот как PNPEVENT генерирует нестандартное оповещение при нажатии кнопки Send Event в диалоговом окне тестовой программы:
struct -RANDOM JOIFICATION
: public JARGET_DEVICE_CUSTOM_NOTIFICATION {
WCHAR text[14]:
}:
_RANDOM_NOTI FI CATION notify:
notlfy.Version = 1:
notify.Size = sizeof(notify);
notify.Event = GUID_PNPEVENT_EVENT:
notify.FileObject = NULL;
notify.NameBufferOffset = FIELD_OFFSET(RANDOM-NOTIFICATION. text)
- FIELD_OFFSET(RANDOM-NOTIFICATION. CustomDataBuffer);
*(PULONG)(notify.CustomDataBuffer) = 42;
wcscpy(notify.text, L"Hello. world!"):
IoReportTargetDev1ceChangeAsynchronous(pdx->Pdo, ^notify, NULL. NULL);
Как видите, PNPEVENT генерирует оповещение, данные которого состоят из числа 42, за которым следует строка Hello, world!.
Оповещение принимается любым драйвером, зарегистрировавшимся на оповещения целевого устройства, относящиеся к объекту файла для того же PDO. Если ваша функция обратного вызова, обслуживающая оповещение, получает структуру с нестандартным кодом GUID в поле Event, вероятно, это GUID чьего-то пользовательского оповещения. Выясните смысл GUID перед тем, как рыться в CustomDataBuffer!
Предполагается, что приложения пользовательского режима тоже могут получать нестандартные событийные оповещения, но добиться этого мне так и не удалось.
Проблемы совместимости с Windows 98/Ме
Между Windows 98/Ме, с одной стороны, и Windows 2000/ХР — с другой существует ряд важных различий, связанных с Plug and Play.
Непредвиденное удаление
Windows 98/Ме никогда не отправляет запросы IRP_MN_SURPRISE_REMOVAL. Соответственно, драйвер WDM должен рассматривать неожиданные IRP IRP_MN_ SURPRISE„REMOVAL как признак непредвиденного удаления. Примеры кода, при
Проблемы совместимости с Windows 98/Ме
341
веденные в этой главе, при получении подобного «сюрприза» вызывают AbortRequests и StopDevice.
Оповещения РпР
В Windows 98/Ме вызовы функции loReportTargetDeviceChange завершаются отказом с кодом STATUS_NOTJIMPLEMENTED. Символическое имя loReportTargetDevice-ChangeAsynchronous вообще не экспортируется, драйвер, вызывающий эту функцию, в Windows 98/Ме просто не загрузится. В приложении А рассказано, как создать заглушки для этой и других отсутствующих вспомогательных функций, чтобы распространять единый двоичный файл драйвера.
Блокировка удаления
Исходная версия Windows 98 не содержала функций, относящихся к блокировке удаления устройств. В Windows 98 Second Edition и Windows Me присутствовали все функции, кроме loReleaseRemoveLockAndWait, что, на мой взгляд, равносильно полному отсутствию этих функций. Я имею в виду, что механизм блокировки удаления предназначен для управления запросами IRP_MN_REMOVE_DEVICE, а эта возможность зависит от отсутствующей функции.
Ситуация усугубляется тем, что драйвер, ссылающийся на функцию, не экспортируемую ядром Windows 98/Ме, попросту не загрузится.
В примерах DDK эта проблема несовместимости решается одним из двух способов. В некоторых случаях вместо IO_REMOVE_LOCK используется нестандартный механизм блокировки, в других предоставляются функции с именами вида XrxAcquireRemoveLock, имитирующие имена стандартных функций блокировки удаления.
В моих драйверах используется разновидность второго способа. При помощи директив #define я заменяю «официальные» объявления объекта IO_REMOVE_ LOCK и вспомогательных функций своими собственными — таким образом, в моем коде используются вызовы loAcquireRemoveLock и т. д. В примерах, использующих библиотеку GENERIC.SYS, эти вызовы на уровне препроцессора перенаправляются функциям с именами GenericAcquireRemoveLock и др., входящим в GENERIC.SYS. В примерах, не использующих GENERIC.SYS, эти вызовы также на уровне препроцессора перенаправляются функциям AcquireRemoveLock и др. из файла REMOVELOCK.CPP.
Конечно, я мог бы написать свои примеры так, чтобы вместо моих функций в Windows ХР вызывались стандартные функции блокировки удаления. Но чтобы примеры работали в Windows 98/Ме, пользователю пришлось бы устанавливать WDMSTUB.SYS (см. приложение А). Не думаю, что этот путь хорошо подходит для изучения программирования WDM.
« Чтение и запись # данных
Вся инфраструктура, описывавшаяся до настоящего момента, лишь подводила нас к этой главе. В ней речь паконец-то пойдет о том, как происходит чтение и запись данных устройствами. Мы рассмотрим сервисные функции, вызываемые для выполнения этих важных операций на устройствах, подключенных к традиционным шинам, таким как PCI (Peripheral Component Interconnect). Поскольку многие драйверы оповещают системные программы о завершении ввода/вывода и исключительных ситуациях при помощи аппаратных прерываний, мы также рассмотрим тему обработки прерываний. Как правило, при обработке прерываний планируется вызов DPC (Deferred Procedure Call), поэтому мы рассмотрим и механизм DPC. Наконец, я расскажу, как организовать обмен памяти по каналам DMA (Direct Memory Access) между устройством и основной памятью.
Настройка конфигурации устройства
В предыдущей главе рассматривались различные запросы IRP_MJ„PNP, отправляемые драйверам со стороны PnP (Plug and Play) Manager. Запрос IRP_MN_START_ DEVICE является основным средством передачи информации о ресурсах ввода/ вывода, которые PnP Manager выделяет драйверам для использования. Я показал, как получить параллельные списки базовых и преобразованных ресурсов и как вызвать вспомогательную функцию StartDevice со следующим прототипом:
NTSTATUS StartDev1ce(PDEVICE_OBJECT fdo.
PCM_PARTIAL_RESOURCE_LIST raw, PCM_PARTIAL_RESOURCEJ_IST translated) {
Настало время объяснить, что же делать с этими списками ресурсов. В двух словах: вы извлекаете описания ресурсов из преобразованного списка и используете их для создания дополнительных объектов ядра, предоставляющих доступ к оборудованию.
Настройка конфигурации устройства
343
Структура CM_PARTIAL_RESOURCE_LIST содержит счетчик и массив структур дескрипторов ресурсов CM_PARTIAL__RESOURCE_DESCRIPTOR (рис. 7.1). Каждый дескриптор ресурса в массиве содержит поле Туре, обозначающее тип описываемого ресурса, а также ряд дополнительных полей с дополнительными сведениями о ресурсе. Кстати говоря, содержимое этого массива вас вряд ли удивит: если ваше устройство использует IRQ, и диапазон портов ввода/вывода, в массив будут включены два элемента. Первый дескриптор описывает IRQ, а второй — диапазон портов ввода/вывода. К сожалению, порядок следования дескрипторов в массиве заранее предсказать нельзя. По этой причине функция StartDevice должна начинаться с цикла, в котором содержимое массива «разбирается» по локальным переменным. Позднее локальные переменные используются для обработки назначенных ресурсов в любом удобном для вас порядке (который, разумеется, может отличаться от того порядка, в котором PnP Manager представляет их вам).
PartialDescriptorsfOJ
PartialDescriptors[ 1 ]
Рис. 7.1. Строение списка ресурсов (фрагмент)
Схема функции StartDevice выглядит примерно так:
NTSTATUS StartDevice(PDEVICE_OBJECT fdo,
PCMJWIALJESOURCEJIST raw,
PCMJARTIAL_RESOURCE_LIST translated)
344
Глава?. Чтение и запись данных
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
PCM_PARTIAL_RESOURCE_DESCRIPTOR resource =	// 1
translated->Part1alDescri ptors;
ULONG nres = translated->Count;	// 2
объявления локальных переменных>	// 3
for (ULONG 1 = 0; 1 < nres; ++1, ++resource)
{
switch (resource->Type)	// 4
{
case CmResourceTypePort:
сохранение информации портов в локальных переменных> break;
case CmResourceTypelnterrupt;
сохранение информации прерываний в локальных переменных> break;
case CmResourceTypcMemory:
сохранение информации адресов памяти в локальных переменных> break;
case CmResourceTypeDma;
сохранение информации DMA в локальных переменных> break;
}
}
<настройка драйвера и оборудования>	// 5
<на основании локальных переменных> return STATUS^SUCCESS;
}
1.	Указатель resource ссылается на дескриптор текущего ресурса в массиве переменной длины. После завершения цикла он будет указывать на позицию за последним действительным дескриптором.
2.	Поле Count списка ресурсов указывает, сколько дескрипторов ресурсов содержит массив PartialDescriptors.
3.	Объявите локальные переменные для всех ресурсов ввода/вывода, которые вы собираетесь получить. Разновидности ресурсов ввода/вывода будут описаны позднее, когда речь пойдет об обработке всех стандартных ресурсов ввода/вывода.
4.	В цикле перебора дескрипторов ресурсов команда switch используется для сохранения данных ресурсов в локальных переменных. Для упоминавшегося ранее устройства были необходимы только диапазон портов ввода и прерывание; такое устройство будет работать с двумя типами ресурсов — CmResourceTypePort и CmResourceTypelnterrupt. Для полноты картины я также включил два других типа стандартных ресурсов, CmResourceTypeMemory и CmResourceTypeDma.
5.	После выхода за пределы цикла локальные переменные, инициализированные в различных секциях case, будут содержать необходимую информацию о ресурсах.
Если вы используете несколько ресурсов конкретного типа, необходимо разработать схему, которая бы позволяла различать эти ресурсы. Возьмем конкрет
Адресация буфера данных
345
ный (но полностью вымышленный) пример: допустим, устройство использует один 4-килобайтный диапазон памяти для целей управления и другой, 16-кило-байтный, диапазон памяти для хранения данных. PnP Manager передает два ресурса типа CmResourceTypeMemory. Размер управляющего блока будет равен 4 Кбайт, тогда как блок данных займет 16 Кбайт. Если ресурсы вашего устройства обладают различительной характеристикой (такой, как размер устройства в данном примере), это позволит вам легко идентифицировать ресурсы.
При работе с несколькими однотипными ресурсами нельзя полагать, что дескрипторы ресурсов будут следовать в порядке их перечисления в пространстве конфигурации и что один драйвер шины всегда конструирует дескрипторы ресурсов в одинаковом порядке на всех платформах или версиях операционной системы. Первое предположение равносильно предположению о том, что программист драйвера шины использует конкретный алгоритм, а второе — что все разработчики драйверов шины мыслят одинаково и никогда не изменяют свои решения.
Все четыре типа стандартных ресурсов ввода/вывода будут рассмотрены в соответствующих разделах этой главы. В табл. 7.1 представлен краткий обзор основных действий по каждому типу ресурсов.
Таблица 7.1. Основные операции при обработке ресурсов ввода/вывода
Тип ресурса	Операции
Порт	Возможно, отображение диапазона портов; сохранение базового адреса в расширении устройства
Память	Отображение диапазона памяти; сохранение базового адреса в расширении устройства
DMA	Вызов loGetDmaAdapter для создания объекта адаптера
Прерывание	Вызов loConnectlnterrupt для создания объекта прерывания, указывающего на обработчик прерывания (ISR)
Адресация буфера данных
Приступая к операции чтения или записи, приложение предоставляет буфер данных и сообщает I/O Manager его виртуальный адрес пользовательского режима и длину. Как говорилось в главе 3, драйверы ядра почти никогда не используют виртуальные адреса пользовательского режима, потому что в общем случае нельзя быть уверенным в контексте потока. Microsoft Windows ХР предоставляет в ваше распоряжение три метода обращения к буферам пользовательского режима (методы буферизации):
О При буферизованном обращении I/O Manager создает системный буфер, размер которого совпадает с размером буфера данных пользовательского режима. Вы работаете с этим системным буфером. I/O Manager берет на себя копирование данных между буфером пользовательского режима и системным буфером.
О При непосредственном обращении I/O Manager фиксирует в памяти физические страницы, содержащие буфер пользовательского режима, и создает
346
Глава 7. Чтение и запись данных
вспомогательную структуру данных MDL (Memory Descriptor List) для описания зафиксированных страниц. Вы работаете с MDL.
О Наконец, I/O Manager может просто передать вам виртуальные адреса пользовательского режима. Вы работаете — очень осторожно! — с адресами пользовательского режима.
На рис. 7.2 продемонстрированы первые два метода. Разумеется, последний вариант полноценным методом назвать нельзя — система не делает ничего, чтобы помочь вам обратиться к данным.
DO_BUFFEREDJO
* Копия данных пользователя
Буфер данных пользователя
DO_DIRECTJO
Отображение системных адресов
Описок дескрипторов , памяти !
Рис. 7.2. Работа с буферами данных пользовательского режима
Выбор метода буферизации
Чтобы задать для устройства метод буферизации, используемый при чтении и записи, следует установить некоторые флаговые биты в объекте устройства вскоре после его создания в функции AddDevice:
NTSTATUS AddDevice(...)
{
PDEVICE_OBJECT fdo;
IoCreateDevice(.... &fdo);
fdo->Flags = DO_BUFFERED JO;
<or>
fdo->Flags = DOJJIRECTJO;
<or>
fdo->Flags =0; // Ни то ни другое
Адресация буфера данных
347
В дальнейшем вы уже не сможете переключиться на другой метод буферизации. Фильтрующие драйверы могут скопировать флаг и не узнают о том, что вы изменили свое решение и решили выбрать другой метод буферизации.
Буферизованный доступ
При создании запроса IRPJMJ_READ или IRP_MJ_WRITE I/O Manager просматривает флаги в объекте устройства и решает, как описать буфер данных в новом пакете запроса ввода/вывода (IRP). Если установлен флаг DO__BUFFERED__IO, I О Manager выделяет блок неперемещаемой памяти, размер которого совпадает с размером буфера. Адрес и длина буфера сохраняются в двух разных местах, в следующем фрагменте соответствующие строки выделены жирным шрифтом. Код I/O Manager работает примерно так (листинг не является исходным кодом Microsoft Windows NT):
PVOID uva;	// <== виртуальный адрес буфера пользовательского режима
ULONG length;	// <== длина буфера пользовательского режима
PVOID sva = ExAl1ocatePoolWithQuota(NonPagedPoolCacheAligned, length);
If (writing)
RtlCopyMemory(sva. uva, length);
Irp->AssociatedIrp.SystemBuffer «= sva;
PIO_STACK_LOCATION stack = loGetNextlrpStackLocation(Irp);
if (reading)
stack->Parameters.Read.Length = length;
else
stack^Parameters.Write.Length = length:
<Код отправки и ожидания IRP>
if (reading)
RtlCopyMemory(uva, sva, length);
ExFreePool(sva);
Другими словами, адрес системного буфера (копии) хранится в поле IRP Associatedlrp.SystemBuffer, а длина запроса — в объединении stack->Parameters. Процесс включает дополнительные тонкости, которые разработчику драйверов знать не обязательно. Например, копирование после успешной операции чтения в действительности выполняется во время вызова АРС в контексте исходного потока и не в той функции, которая конструировала IRP. I/O Manager сохраняет виртуальный адрес пользовательского режима (переменная uva в предыдущем фрагменте) в поле IRP UserBuffer, чтобы его можно было найти на этапе копирования. Тем не менее, ни на один из этих фактов не стоит полагаться — они в любой момент могут измениться.
I/O Manager также освобождает свободную память, выделенную для системной копии буфера, при завершении IRP.
348
Глава 7. Чтение и запись данных
Непосредственный доступ
Если в объекте устройства задается метод DO_DIRECT__IO, I/O Manager создает структуру MDL для описания зафиксированных страниц с буфером пользовательского режима. Объявление структуры MDL выглядит так:
typedef struct _MDL {
struct J10L *Next;
CSHORT Size;
CSHORT MdlFlags;
struct -EPROCESS ^Process;
PVOID MappedSystemVa;
PVOID StartVa;
ULONG ByteCount:
ULONG ByteOffset;
} MDL, *PMDL;
Рисунок 7.3 поясняет смысл MDL. Поле StartVa содержит виртуальный адрес буфера (действительный только в контексте процесса пользовательского режима, которому принадлежат данные). Поле ByteOffset содержит смещение начала буфера в странице, a ByteCount — размер буфера в байтах. Массив Pages, формально объявленный как часть структуры MDL, следует за MDL в памяти и содержит номера физических страниц, на которые отображаются виртуальные адреса пользовательского режима.
ByteOffset
... A ...
ByteCount
StartVa
Pages[2]
Pages[1]
Pages[O]
Виртуальные адреса в пользовательском пространстве
Пространство физических адресов
Рис. 7.3. Структура списка дескрипторов памяти
Адресация буфера данных
349
Кстати говоря, к полям структуры MDL никогда не обращаются напрямую — для этой цели используются макросы и вспомогательные функции, перечисленные в табл. 7.2.
Таблица 7.2. Макросы и вспомогательные функции для работы с MDL
Макрос или функция	Описание
loAllocateMdl loBuildPartialMdl	Создает MDL Строит MDL для подмножества существующего MDL
loFreeMdl	Уничтожает MDL
MmBuildMdIForNonPagedPool	Изменяет MDL так, чтобы он описывал блок неперемещаемой памяти режима ядра
MmGetMdIByteCount MmGetMdIByteOffset MmGetMdlPfnArray MmGetMdIVirtualAddress	Определяет размер буфера в байтах Получает смещение буфера в первой странице Находит массив указателей на физические страницы Получает виртуальный адрес
MmGetSystemAddressForMdl	Создает виртуальный адрес режима ядра, отображаемый на ту же память
MmGetSystemAddressForMdlSafe	То же, что MmGetSystemAddressForMdl, но рекомендуется для использования в Windows 2000 и последующих системах
MmlnitializeMdl	(Заново) инициализирует MDL для описания заданного виртуального буфера
MmMapLockedPages	Создает виртуальный адрес режима ядра, отображаемый на ту же память
MmMapLockedPagesSpecifyCache	То же, что MmMapLockedPages, но рекомендуется для использования в Windows 2000 и последующих системах
MmPrepareMdIForReuse M m ProbeAnd LockPages MmSizeOfMdl	Заново инициализирует MDL Фиксирует страницы после проверки действительности адресов Определяет, сколько памяти потребуется для создания MDL, описывающего заданный виртуальный буфер. Если для создания MDL использовалась функция loAllocateMdl, вызывать эту функцию не нужно
MmUnlockPages MmUnmapLockedPages	Отменяет фиксацию страниц для этого MDL Отменяет последствия предыдущего вызова MmMapLockedPages
Код, выполняемый I/O Manager при непосредственных чтении или записи, выглядит примерно так:
KPROCESSOR_MODE mode; // <=== Режим ядра или пользовательский режим
PMDL mdl = loAllocateMdl(uva, length, FALSE, TRUE, Irp);
MmProbeAndLockPages(mdl, mode,
reading ? loWriteAccess : loReadAccess);
<Код отправки и ожидания IRP>
350
Глава 7. Чтение и запись данных
MmUnlockPagesfmdl):
loFreeMdl(mdl):
I/O Manager сначала создает MDL для описания пользовательского буфера. Третий аргумент loAllocateMdl (FALSE) указывает, что структура относится к первичному буферу данных. Четвертый аргумент (TRUE) указывает, что подсистема управления памятью должна учесть вызов в квоте процесса. Последний аргумент (Irp) задает IRP, с которым связывается MDL. Во внутренней реализации loAllocateMdl заносит в поле Irp->MdlAddress адрес вновь созданного MDL; так вы находите MDL, и так I/O Manager находит MDL, когда приходит время деинициализации.
Ключевым событием в приведенном фрагменте является вызов MmProbeAnd-LockPages, выделенный жирным шрифтом. Функция убеждается в том, что буфер данных действителен и к нему можно обратиться в соответствующем режиме. Для записи в устройство необходимо иметь возможность чтения из буфера, а для чтения из устройства буфер должен быть доступен для записи. Кроме того, функция фиксирует физические страницы с буфером данных и заполняет массив номеров страниц, следующий за MDL в памяти. Фактически зафиксированная страница становится частью неперемещаемого пула до тех пор, пока количество снятий фиксации не сравняется с количеством ее установок.
Наиболее вероятным действием с MDL при непосредственных чтении или записи является его передача в аргументе другой стороне. Например, при пересылке по каналу DMA необходимо передать MDL функции MapTransfer, которая будет описана далее в разделе «Выполнение пересылки DMA». Или другой пример: во внутренней реализации чтения и записи по шине USB всегда используется MDL, поэтому вы с таким же успехом можете выбрать режим DO_DIRECT_IO и передать полученный MDL драйверу шины USB.
Между прочим, I/O Manager сохраняет длину запроса чтения или записи в объединении stack- > Parameters. Тем не менее, драйверы чаще узнают длину запроса непосредственно из MDL:
ULONG length = MmGetMdlByteCount(mdl);
Третий метод
Если в объекте устройства не установлен ни один из флагов DO_DIRECTJO и DO_ BUFFERED-IO, по умолчанию I/O Manager просто передает виртуальный адрес пользовательского режима и количество байтов (выделено жирным шрифтом) и оставляет все остальное на ваше усмотрение:
Irp->UserBuffer = uva:
PIO_STACK_LOCATION stack = loGetNextlrpStackLocation(Irp);
if (reading)
stack->Parameters.Read.Length = length:
else
stack^Parameters.Write.Length = length:
<Код отправки и ожидания IRP>
Порты и регистры
351
Никогда не пытайтесь просто обращаться к памяти по указателю, полученному из пользовательского режима. Либо постройте MDL для страниц пользовательского режима при помощи функции MmProbeAndLockPages, либо вызовите ProbeForRead/ProbeForWrite и убедитесь в том, что этот диапазон адресов действительно принадлежит пользовательскому режиму, и только потом обращайтесь к памяти. В обоих случаях используйте блоки структурированных исключений, чтобы предотвратить фатальный сбой при возникновении каких-либо проблем с указателем или длиной данных. Пример:
PVOID buffer = Irp->UserBuffer;
ULONG length = stack-Parameters.Read.Length;
If (Irp->RequestorMode !== KernelMode)
_try
PMDL mdl = IoAllocateMdl(...);
MmProbeAndLockPages(...);
-или-
ProbeForReadf...);
обращение к памяти в 6y(fiepe><$]kursiv>
}
_except(EXCEPTION_EXECUTE_HANDLER)
{
return CompleteRequest(I гр. GetExceptionCodeO, 0);
}
Порты и регистры
На рис. 7.4 показана абстрактная модель компьютера, используемая в Windows ХР для формирования унифицированного интерфейса драйверов во всех процессорных архитектурах. В этом режиме процессор может обладать раздельными адресными пространствами основной памяти и памяти ввода/вывода. Чтобы обратиться к устройству, отображаемому на память, процессор использует ссылки на виртуальные адреса. Процессор преобразует виртуальный адрес в физический по таблицам страниц. С другой стороны, чтобы обратиться к устройству, отображаемому на память ввода/вывода, процессор использует специальные механизмы, такие как команды IN и OUT на платформе х86.
Способ декодирования устройством адресов основной памяти и памяти ввода/ вывода зависит от шины. В случае шины PCI ведущий мост отображает физические адреса памяти и адреса памяти ввода/вывода на адресное пространство шины, напрямую доступное для устройств. Флаговые биты в конфигурационном пространстве устройства определяют, отображает ли мост регистры устройства на основную память или память ввода/вывода (для процессоров, поддерживающих оба адресных пространства).
Как я уже говорил, некоторые процессоры обладают раздельными адресными пространствами памяти и ввода/вывода. В частности, это относится к процессорам
352
Глава 7. Чтение и запись данных
архитектуры Intel. Другие процессоры, такие как Alpha, обладают только адресным пространством основной памяти. Если ваше устройство отображается на память ввода/вывода, PnP Manager предоставляет ему ресурсы портов. Устройствам, отображаемым на основную память, вместо этого предоставляются ресурсы памяти.
Обращения к основной памяти
Адресное: простр. ввода/ вывода .
Обращения к памяти ввода/вывода
Рис. 7.4. Обращение к портам и регистрам
Чтобы в драйвер не приходилось включать массу условно компилируемого кода для всех возможных платформ, проектировщики Windows NT разработали концепцию уровня аппаратных абстракций, или HAL (Hardware Abstraction Layer), уже несколько раз упоминавшуюся в книге. HAL предоставляет функции для обращения к ресурсам портов и памяти (табл. 7.3). Как видно из таблицы, вы можете читать/записывать как отдельные элементы UCHAR/USHORT/ ULONG, так и их массивы, в порты/регистры и из них. Все возможные комбинации дают 24 функции HAL, используемые при обращениях к устройству. Поскольку драйверы WDM не используют функции HAL напрямую для других целей, можно считать, что эти 24 функции составляют весь открытый интерфейс HAL.
Реализация функций доступа (разумеется!) сильно зависит от платформы. Например, версия READ_PORT_CHAR для Intel х86 выполняет команду IN для чтения 1 байта из указанного порта ввода/вывода. Реализация Microsoft Windows 98/Ме в отдельных ситуациях доходит до замера команды вызова драйвера фактической командой IN. Версия той же функции для процессоров Alpha выполняет выборку содержимого памяти. Версия READ_REGISTER__UCHAR для Intel х86 также выполняет выборку из памяти, а на платформе Alpha она с помощью макроса преобразуется в прямую ссылку на память. В то же время, буферизованная версия этой функции (READ_REGISTER_BUFFER_UCHAR) в среде Intel х86 выполняет кое-какую дополнительную работу, гарантирующую очистку всех процессорных кэшей при завершении операции.
Порты и регистры
353
Таблица 7.3. Функции HAL для работы с портами и регистрами памяти
Размер данных	Функции для обращения к портам	Функции для обращения к регистрам
8 бит	READ_PORT_UCHAR	READ_REGISTER_UCHAR
	WRITE-PORTJJCHAR	WRITE_REGISTER_UCHAR
16 бит	READ_PORT_USHORT	READ_REGISTER_USHORT
	WRITE_PORT_USHORT	WRITE_REGISTERJJSHORT
32 бита
Строка из 8-разрядных байтов
Строка из 16-разрядных слов
Строка из 32-разрядных двойных слов
READ_PORT_ULONG
WRITE_PORT_ULONG
READ_PORT_BUFFER_UCHAR WRITE_PORT_BUFFER_UCHAR
READ_PORT_BUFFER_USHORT WRITE J>ORT_BUFFER_USHORT
READ_PORT_BUFFER_ULONG
WRITE PORT BUFFER ULONG
READ_REGISTER_ULONG
WRITE_REGISTER_ULONG
READ_REGISTER__BUFFER_UCHAR
WRITE_REGISTER_BUFFER_UCHAR
READ_REGISTERJ3UFFERJJSHORT WRITE„REGISTER_BUFFER_USHORT
READ_REGISTER_BUFFER_ULONG WRITE REGISTER BUFFER ULONG
Прослойка HAL создавалась в первую очередь для того, чтобы вам не приходилось беспокоиться о различиях между платформами или порой запутанными требованиями при обращениях к устройствам в многозадачной, многопроцессорной среде Windows ХР. Ваша задача довольно проста: используйте вызовы PORT для обращения к тому, что вы считаете ресурсами портов, или вызовы REGISTER для обращения к тому, что вы считаете ресурсами памяти.
Ресурсы портов
Для обращения к устройствам, отображаемым на память ввода/вывода, используются аппаратные регистры, которые в некоторых процессорных архитектурах (включая Intel х86) адресуются через специальное адресное пространство ввода/ вывода. В других процессорных архитектурах отдельное адресное пространство ввода/вывода не существует, и обращения к регистрам осуществляются через обычные ссылки на памяти. К счастью, вам не придется разбираться в тонкостях адресации. Если ваше устройство запрашивает ресурс порта, одна итерация цикла по транслированным дескрипторам ресурсов найдет дескриптор CmResource-TypePort и в вашем распоряжении появятся три объекта данных:
typedef struct _DEVICE_EXTENSION {
PUCHAR portbase:
ULONG nports;
BOOLEAN mappedport:
} DEVICE-EXTENSION, *PDEVICE_EXTENSION:
PHYSICAL_ADDRESS portbase: // Базовый адрес диапазона
354
Глава 7. Чтение и запись данных
for (ULONG 1 = 0; 1 < nres; ++1, ++resource)
{
switch (resource->Type)
case CmResourceTypePort.
portbase = resource->u.Port.Start;	// 1
pdx->nports = resource->u.Port.Length;
pdx->mappedport =	//2
(resource->Flags & CM_RESOURCE_PORT__IO) == 0;
break;
if (pdx->mappedport)
{
pdx->portbase = (PUCHAR) MmMapIoSpace(portbase.	// 3
pdx->nports. MmNonCached);
if (!pdx->portbase)
return STATUSJO_MEMORY;
else
pdx->portbase = (PUCHAR) portbase.QuadPart;	// 4
1.	В дескриптор ресурса входит объединение и, содержащее субструктуры для каждого стандартного типа ресурсов, u.Port содержит информацию о ресурсе порта. Поле u.Port.Start определяет начальный адрес непрерывного диапазона портов ввода/вывода, а поле u.Port.Length — количество портов в диапазоне. Начальный адрес задается 64-разрядным значением PHYSICAL„ADDRESS.
2.	Установка флага CM_RESOURCE_PORT_IO в поле Flags дескриптора ресурса порта означает, что архитектура процессора обладает отдельным адресным пространством ввода/вывода, к которому относится заданный адрес порта.
3.	Если флаг CM_RESOURCE_PORT_IO сброшен, как на платформе Alpha и, вероятно, на других платформах RISC, необходимо вызвать MmMapIoSpace для получения виртуального адреса режима ядра, через который будет производиться обращение к порту. Обычно обращение будет производиться по ссылке на память, но вы все равно можете использовать PORT-всрсии функций HAL (READ_PORT_UCHAR и т. д.) в своих драйверах.
4.	Если флаг CM_RESOURCE__PORTJO был установлен, как на платформе х86, дополнительное отображение адресов портов не потребуется. При обращении к портам можно использовать PORT-всрсии функций HAL. Функциям HAL адрес порта передается в аргументе типа PUCHAR, поэтому базовый адрес преобразуется к этому типу. Благодаря использованию ссылки QuadPart вы получите 32- или 64-разрядный указатель в зависимости от платформы, для которой компилируется драйвер.
Независимо от того, нужно отображать адрес порта через MmMapIoSpace или нет, вы всегда можете вызвать функции HAL для работы с ресурсами портов ввода/вывода: READ_PORT_UCHAR, WRITE_PORT_UCHAR и т. д. На процессорах, тре
Порты и регистры
355
бующих отображения адресов портов, HAL сгенерирует обращения к основной памяти. На процессорах, не требующих отображения, HAL будет обращаться к памяти ввода/вывода, на х86 это означает использование команд семейства IN и OUT.
Вспомогательная функция StopDevice выполняет небольшую зачистку в случае отображения ресурса порта:
VOID StopDeviceC...)
{
if (pdx->portbase && pdx->mappedport)
MmUnmapIoSpace(pdx->portbase, pdx->nports);
pdx->portbase = NULL:
}
Ресурсы памяти
Доступ к устройствам, отображаемым на память, осуществляется командами за-грузки/сохранения данных через регистры. Преобразованное значение ресурса, полученное от PnP Manager, представляет собой физический адрес, поэтому вы должны зарезервировать виртуальные адреса для работы с физической памятью. Далее вызываются функции HAL для работы с регистрами памяти, такие как READ_REGISTER__UCHAR, WRITE_REGISTER_(JCHAR и т. д. Код извлечения ресурса и настройки конфигурации будет выглядеть примерно так:
typedef struct _DEV1CE_EXTENSION {
PUCHAR membase;
ULONG nbytes;
} DEVICEJXTENSION, *PDEVICE_EXTENSION;
PHYSICAL-ADDRESS membase:	// base address of range
for (ULONG i - 0; 1 < nres: ++1, ++resource)
{
switch (resource~->Type)
{
case CmResourceTypeMemory:
membase = resource-^. Memory. Start;	// 1
pdx->nbytes = resource->u.Memory.Length;
break;
}
pdx->membase = (PUCHAR) MmMapIoSpace(membase, pdx->nbytes, // 2 MmNonCached);
if (!pdx->membase)
return STATUS_NO_MEMORY;
356
Глава 7. Чтение и запись данных
1. В дескрипторе ресурса u.Memory содержит информацию о ресурсе памяти. Поле u.Memory.Start определяет начальный адрес непрерывного диапазона адресов памяти, а поле u.Memory.Length — размер диапазона в байтах. Начальный адрес задается в виде 64-разрядного значения PHYSICAL_ADDRESS. Идентичное строение субструктур u.Port и u.Memory не случайно — это было сделано намеренно, и если потребуется, вы можете положиться на этот факт.
2. Для получения виртуального адреса режима ядра, через который будет производиться обращение к диапазону памяти, необходимо вызвать MmMapIoSpace. В функции StopDevice происходит безусловная отмена отображения ресурсов памяти:
VOID StopDevIceC...) {
If (pdx->membase)
MmUnmapIoSpace(pdx->meiTibase, pdx->nbytes);
pdx->niembase = NULL:
}
Обработка прерываний
Многие устройства подают сигнал о завершении операций ввода/вывода посредством асинхронных прерываний процессора. В этом разделе я расскажу, как настроить драйвер для работы с прерываниями и как обрабатывать возникающие прерывания.
Настройка прерывания
Ресурсы прерываний настраиваются в функции StartDevice. Для этого вызывается функция loConnectlnterrupt с параметрами, извлеченными из дескриптора CmResourceTypelnterrupt. При вызове loConnectlnterrupt драйвер и устройство должны быть полностью готовы к нормальной работе — возможно, первое прерывание даже придется обработать прежде, чем функция вернет управление, поэтому этот вызов обычно располагается в конце процесса настройки конфигурации. У некоторых устройств имеются аппаратные функции запрета прерываний. Если ваше устройство обладает такой функцией, запретите прерывания перед вызовом loConnectlnterrupt, а потом разрешите их снова. Код извлечения данных и настройки конфигурации прерываний выглядит примерно так:
typedef struct _DEVICE_EXTENSION {
PKINTERRUPT InterruptObject:
} DEVICE_EXTENSION, *PDEVICE_EXTENSION:
Обработка прерываний
357
ULONG vector:	//	Вектор прерывания
KIRQL Irql:	//	Уровень прерывания
КINTERRUPT _MODE mode:	//	Режим срабатывания
KAFFINITY affinity:	//	Маска привязки
BOOLEAN Irqshare:	//	Общее прерывание?
for (ULONG 1 = 0; 1 < nres: ++1, ++resource)
{
switch (resource->Type)
{
case CmResourceTypelnterrupt:
Irql = (KIRQL) resource->u.Interrupt.Level:	// 1
vector = resource->u.Interrupt.Vector;	//	2
affinity = resource->u.Interrupt.Affinity;	//	3
mode =* (resource->Flags == CFI_RESOURCE_INTERRUPT_LATCHED)	//	4
? Latched : LevelSensItlve:
Irqshare =	//5
resource->ShareD1spos1t1on == CmResourceShareShared: break:
status = IoConnectInterrupt(&pdx->InterruptObject, (PKSERVICE-ROUTINE) Onlnterrupt, (PVOID) pdx, NULL, vector, Irql, Irql, mode, Irqshare, affinity, FALSE):
1.	Параметр Level задает уровень IRQL для данного прерывания.
2.	Параметр Vector задает аппаратный вектор прерывания. Его конкретное значение нас не интересует, потому что драйвер всего лишь обеспечивает обмен информацией между PnP Manager и loConnectlnterrupt. Достаточно, чтобы смысл этого числа был понятен для HAL.
3.	Affinity — битовая маска, указывающая, каким процессорам разрешено обрабатывать это прерывание.
4.	Мы должны сообщить loConnectlnterrupt, как генерируется прерывание — по уровню или по фронту сигнала. Если в поле Flags ресурса установлен флаг CM_RESOURCE_INTERRUPT_LATCHED, значит, прерывание генерируется по фронту, в противном случае прерывание генерируется по уровню.
5.	Команда проверяет, является ли прерывание общим.
При вызове loConnectlnterrupt в конце последовательности мы просто передаем значения, извлеченные из дескриптора ресурса прерывания. Первый аргумент (&pdx->InterruptObject) сообщает, где должен храниться результат операции подключения, — в нем передается указатель на объект прерывания ядра, описывающий прерывание. Второй аргумент (Onlnterrupt) зад;ает имя функции, обслуживающей прерывание (обработчики прерываний (ISR) более подробно рассматриваются далее в этой главе). Третий аргумент (pdx) содержит контекст, который будет
358
Глава 7. Чтение и запись данных
передаваться в аргументе ISR при каждом прерывании от устройства. Параметр контекста подробно рассматривается позднее, в подразделе «Выбор аргумента контекста».
Пятый и шестой аргументы (vector и irqi) определяют, соответственно, номер вектора и уровень запроса для подключаемого прерывания. Восьмой аргумент (mode) принимает значения Latched или LevelSensitive (соответственно, для прерываний, генерируемых по уровню и фронту). Девятый аргумент равен TRUE, если прерывание используется совместно с другими устройствами, или FALSE в противном случае. Десятый аргумент (affinity) содержит маску привязки к процессорам для данного прерывания. Одиннадцатый и последний аргументы указывают, должна ли операционная система сохранять контекст вещественных вычислений при прерывании от устройства. Поскольку на платформе х86 вещественные вычисления в ISR запрещены, в портируемых драйверах этот флаг всегда устанавливается равным FALSE.
Я не описал еще два аргумента loConnectlnterrupt. Они важны тогда, когда устройство обрабатывает более одного прерывания. В таких случаях вы создаете спин-блокировку для своих прерываний и инициализируете ее вызовом KelnitializeSpinLock. Также перед подключением вычисляется максимальный уровень IRQL, необходимый для прерываний. При каждом вызове loConnectlnterrupt адрес спин-блокировки передается в четвертом аргументе (в моем примере он равен NULL), а максимальный уровень IRQL — в седьмом аргументе (irql в моем примере). Седьмой аргумент обозначает уровень IRQL. используемый для синхронизации прерываний, он делается равным максимальному из IRQL всех прерываний, чтобы прерывания происходили по одному.
Но если устройство использует только одно прерывание, специальная снин-блокировка не потребуется (потому что I/O Manager автоматически создает ее за вас), а уровень синхронизации прерывания совпадает с 1RQL прерывания.
Обработка прерываний
Устройство может выдать прерывание на любом из процессоров в маске привязки, переданной при вызове loConnectlnterrupt. При возникновении прерывания система повышает IRQL процессора до соответствующего уровня синхронизации и захватывает спин-блокировку, связанную с объектом прерывания. Затем вызывается обработчик прерывания, основной код которого выглядит так:
BOOLEAN OnInterrupt(PKINTERRUPT InterruptObject, PVOID Context)
{
If (<прерывания нет>) return FALSE;
обработка ярерывания>
return TRIE;
}
Механизм обработки прерываний Windows NT допускает возможность совместного использования аппаратных прерываний несколькими устройствами.
Обработка прерываний
359
Таким образом, первое, что должен сделать обработчик прерывания, — проверить, имеется ли прерывание от устройства в данный момент. Если прерывания нет, немедленно верните FALSE, чтобы ядро могло отправить прерывание драйверу другого устройства. Если же прерывание есть, сбросьте прерывание на уровне устройства и верните TRUE. Будет ли ядро затем вызывать обработчики прерывания других драйверов, зависит от того, как генерируется прерывание (по фронту или уровню), а также от других особенностей платформы.
Главное, что необходимо сделать в обработчике прерывания, — обеспечить сброс прерывания на аппаратном уровне. Я скажу несколько общих слов по этому поводу, но подробности сильно зависят от работы оборудования. После решения этой главной задачи следует вернуть TRUE, тем самым вы сообщаете HAL, что прерывание устройства было успешно обслужено.
Ограничения на вычисления в обработчиках прерываний
Обработчик прерывания (ISR) выполняется на уровне IRQL выше DISPATCH_ LEVEL. Следовательно, весь код и данные, используемые в ISR, должны находиться в неперемещаемой памяти. Более того, набор функций режима, которые могут вызываться в ISR, сильно ограничен.
Так как ISR выполняется на повышенном уровне IRQL, он блокирует на своем процессоре все остальные операции, требующие того же или более низкого уровня IRQL. А это означает, что для сохранения оптимального быстродействия системы обработчик прерывания должен отрабатывать как можно быстрее. Фактически, вы должны выполнить минимальный объем работы, необходимой для обслуживания устройства, и вернуть управление. Для выполнения дополнительных операций (скажем, завершения IRP) следует запланировать вызовы DPC.
Впрочем, стремясь свести к минимуму объем работы, выполняемой в ISR, не стоит впадать в крайности. Например, если ваше устройство выдает прерывание как сигнал готовности следующего выходного байта, отправьте этот байт прямо из ISR. Было бы, в принципе, глупо планировать DPC только для того, чтобы переслать один байт. Помните: пользователь хочет, чтобы вы обслуживали его оборудование (иначе он не стал бы устанавливать это оборудование на своем компьютере), поэтому вы можете рассчитывать на справедливую долю системных ресурсов.
Впрочем, вычислять с точностью до тысяч знаков после запятой в ISR тоже не стоит (если только устройство не потребует чего-нибудь столь экстравагантного, но это маловероятно). Здравый смысл должен подсказать вам, как правильно распределить работу между ISR и DPC.
Выбор аргумента контекста
При вызове loConnectlnterrupt третий аргумент содержит произвольный контекст, который передается во втором аргументе ISR. Выберите этот аргумент так, чтобы обработчик прерывания мог отработать как можно быстрее, например, хорошим кандидатом может быть адрес объекта устройства или расширения устройства. В расширении устройства храня гея данные (такие как базовый адрес портов устройства), на основании которых проверяется наличие прерываний у устройства.
360
Глава 7. Чтение и запись данных
Предположим, у устройства, отображаемого на адресное пространство ввода/ вывода, имеется порт состояния, расположенный по базовому адресу, и младший бит состояния указывает, пытается ли устройство выдать прерывание в настоящий момент. В этой ситуации несколько первых строк ISR будут выглядеть примерно так:
BOOLEAN Onlnterrupt(PKINTERRUPT InterruptObject, PDEVICEJXTENSION pdx) {
UCHAR devstatus = READ JWT JJCHAR(pdx->portbase);
if ((devstatus & 1)) return FALSE;
<И т.д>
}
Полностью оптимизированный код этой функции состоит из нескольких команд для чтения содержимого порта состояния и проверки его младшего бита.
СОВЕТ-----------------------------------------------------—----------------------
Если вы имеете какое-то отношение к проектированию оборудования, сделайте так, чтобы порт состояния возвращал 0 для обозначения необработанного прерывания. При чтении из порта, к которому ничего не подключено, обычно возвращается бит 1, и это простое конструктивное решение поможет предотвратить бесконечный цикл вызовов ISR.
Если вы изберете расширение устройства в качестве аргумента контекста, не забудьте выполнить преобразование типа при вызове loConnectlnterrupt:
loConnectlnterrupt..., (PKSERVICEJWTINE) Onlnterrupt, ...);
Если опустить этот вызов, компилятор выдаст в высшей степени невразумительное сообщение об ошибке, потому что второй аргумент функции Onlnterrupt (PDEVICE„EXTENSION) не совпадает с прототипом аргумента указателя на функцию loConnectlnterrupt, для которого нужен тип PVOID.
Синхронизация операций в обработчиках прерываний
Как правило, ISR использует данные и аппаратные ресурсы совместно с другими частями драйвера. Но каждый раз, когда вы слышите слово «совместно», сразу начинайте думать о возможных проблемах синхронизации. Например, у стандартного устройства UART (Universal Asynchronous Receiver-Transmitter) имеется порт данных, который используется драйвером для чтения и записи данных. Можно предположить, что обработчик прерывания драйвера последовательного порта будет время от времени обращаться к этому порту. Изменение скорости передачи также требует установки управляющего флага, называемого делителем (divisor latch), с выполнением двух однобайтовых операций записи (в одной из которых задействован тот же порт данных), после чего делитель сбрасывается. Если работа UART будет прервана в процессе смены скорости, байт данных, предназначенный для передачи, легко может оказаться в регистре делителя (или байт, предназначенный для регистра делителя, может быть отправлен как данные).
Обработка прерываний
361
Система защищает 1ST спин-блокировкой и относительно высоким уровнем IRQL — DIRQL (Device IRQL). Для упрощения процедуры получения спин-блокировки и повышения IRQL до уровня прерывания в системе предусмотрена следующая вспомогательная функция:
BOOLEAN result = KeSynchronizeExecution(InterruptObject, SynchRoutine, Context):
где InterruptObject (PKINTERRUPT) — указатель на объект прерывания, описывающий прерывание, с которым мы пытаемся синхронизироваться, SynchRoutine (PKSYNCHRONIZE_ROUT[NE) — адрес функции обратного вызова в драйвере, a Context (PVOID) — произвольный контекст, передаваемый в аргументе SynchRoutine. Мы будем называть функции, вызываемые при помощи KeSynchronizeExecution, общим термином «синхронизированные функции критических секций». Синхронизированная функция критической секции имеет следующий прототип:
BOOLEAN SynchRoutlne(PVOID Context);
Функция получает один аргумент и возвращает результат BOOLEAN. При получении управления текущий процессор работает на уровне IRQL, указанном при исходном вызове loConnectlnterrupt, и владеет спин-блокировкой, связанной с прерыванием. Таким образом, прерывания от устройства временно блокируются, а функция SynchRoutine может свободно обращаться к данным и аппаратным ресурсам, используемым совместно с ISR.
Кстати, KeSynchronizeExecution возвращает значение, полученное от SynchRoutine. Это позволяет организовать минимальную обратную связь SynchRoutine со стороной, вызвавшей KeSynchronizeExecution.
Если вы проектируете драйвер, предназначенный только для ХР и последующих систем, используйте функции KeAcquirelnterruptSpinLock и KeReleaselnterrupt-SpinLock — они помогут избежать довольно громоздких конструкций, необходимых при использовании KeSynchronizeExecution.
Отложенные вызовы процедур (DPC)
Полная обработка прерывания от устройства часто требует выполнения операций, запрещенных в ISR или сопряженных со слишком высокими затратами для выполнения на повышенном уровне IRQL. Чтобы избавиться от подобных проблем, проектировщики Windows NT разработали механизм отложенного вызова процедур (DPC). DPC является механизмом общего назначения, но чаще всего используется в контексте обработки прерываний. В самом распространенном сценарии ISR решает, что текущий запрос завершен, и запрашивает DPC. Позднее ядро вызывает функцию DPC на уровне DISPATCH_LEVEL. Хотя в DPC все равно существуют определенные ограничения на вызываемые сервисные функции и на перемещение данных в памяти, этих ограничений меньше, потому что выполнение ведется на более низком уровне IRQL, чем в ISR. В частности, разрешается вызов таких функций, как loCompleteRequest и StartNextPacket, логически необходимых в конце операций ввода/вывода.
362
Глава 7. Чтение и запись данных
Каждому объекту устройства предоставляется «бесплатный» объект DPC. Иначе говоря, DEVICEJDBJECT содержит встроенный объект DPC со вполне предсказуемым именем Dpc. Встроенный объект DPC должен инициализироваться вскоре после создания объекта устройства:
NTSTATUS AddDeviсе(...)
{
PDEVICE-OBJECT fdo;
loCreateDevIсе(.... &f do);
IoIrrit1alizeDpcRequest(fdo, DpcForlsr);
Макрос loInitializeDpcRequest из файла WDM.H инициализирует объект DPC, встроенный в объект устройства. Второй аргумент содержит адрес процедуры DPC (см. далее).
При наличии инициализированного объекта DPC обработчик прерывания запрашивает вызов DPC при помощи следующего макроса:
BOOLEAN OnInterrupt!...)
{
IoRequestDpc(pdx->DeviceObject. NULL, (PVOID) pdx);
Этот вызов loRequestDpc помещает объект DPC объекта устройства в общесистемную очередь, как показано на рис. 7.5.
Обработка прерываний
363
NULL и pdx содержат контекстную информацию. Позднее, когда не будет других операций, выполняемых на уровне DISPATCH JLEVEL, ядро выводит объект DPC из очереди и вызывает функцию DPC со следующим прототипом:
VOID DpcForlsr(PKDPC Dpc. PDEVICE_OBJECT fdo, PIRP junk.
PDEVICE-EXTENSION pdx) {
Содержимое функции DPC сильно зависит от того, как работает ваше устройство. Один из вероятных вариантов — завершение текущего IRP и освобождение следующего IRP из очереди. Если для организации очереди IRP использовались мои объекты DEVQUEUE, код будет выглядеть примерно так:
VOID DpcForlsr(...)
{
PIRP Irp = GetCurrentIrp(&pdx->dqRead);
StartNextPacket(&pdx->dqRead, fdo);
loCompleteRequest(Irp, 1):
}
В этом фрагменте используется тот факт, что пакет DEVQUEUE запоминает IRP, отправленный функции Startlo. Мы собираемся завершить IRP, текущий на момент перехода к функции DPC. Перед loCompleteRequest обычно вызывается функция StartNextPacket, чтобы устройство было занято новым запросом перед началом теоретически длительного процесса завершения текущего IRP.
Планирование DPC
До настоящего момента я опустил две относительно важные и одну второстепенную подробность, относящиеся к DPC. Первая важная подробность состоит в том, что объект DPC помещается в очередь вызовом loRequestDpc. Если устройство сгенерирует дополнительное прерывание перед фактическим запуском функции DPC, а обработчик прерывания запросит другой вызов DPC, ядро попросту проигнорирует второй запрос. Другими словами, объект DPC помещается в очередь один раз независимо от того, сколько вызовов DPC было запрошено при последовательных вызовах ISR, а ядро вызовет функцию обратного вызова только один раз. Во время этого вызова функция DPC должна выполнить всю работу по обслуживанию всех прерываний, происшедших с момента последнего DPC.
Как только диспетчер DPC выведет объект DPC из очереди, он может быть повторно поставлен в очередь другой стороной, даже во время выполнения функции DPC. Это не создаст проблем, если объект будет оба раза поставлен в очередь на одном процессоре. Таким образом, вторая важная подробность, относящаяся к обработке DPC, связана с маской привязки. Обычно ядро ставит объект DPC в очередь обработки того же процессора, от которого поступил запрос DPC, — например, процессора, только что обработавшего прерывание и вызвавшего loRequestDPC. Как только диспетчер DPC выведет объект DPC из очереди и вызовет функцию обратного вызова на одном процессоре, теоретически, устройство может сгенерировать прерывание на другом процессоре —- это может привести к одновременному выполнению запроса DPC на другом процессоре.
364
Глава 7. Чтение и запись данных
Возникнут ли проблемы при параллельном выполнении функции DPC на разных процессорах? Разумеется, это зависит от особенностей кода.
Потенциальные проблемы, которые могут возникнуть при одновременном выполнении функции DPC на нескольких процессорах, решаются несколькими способами. Способ первый (не лучший) основан на выборе конкретного процессора для выполнения DPC вызовом KeSetTargetProcessorDpc. Теоретически, также можно ограничить привязку прерывания к процессору при исходном подключении; если DPC никогда не будет ставиться в очередь из других мест, кроме вашего ISR, то DPC никогда не будет выполняться на других процессорах. Впрочем, настоящей причиной для задания маски привязки для DPC и прерываний все же является повышение быстродействия (чтобы код и данные, с которыми работает DPC или ISR, оставались в кэше).
Для предотвращения взаимного влияния двух экземпляров функции DPC также можно воспользоваться спин-блокировкой или другим примитивом синхронизации. Будьте внимательны при использовании спин-блокировки: часто в ISR приходится координировать гипотетические последствия существования нескольких экземпляров функции DPC, a ISR работает на слишком высоком уровне IRQL для применения обычной спин-блокировки. Здесь могут пригодиться атомарные списки (то есть списки, для работы с которыми используются функции семейства ExInterlockedlnsertHeadList), так как они могут использоваться на любом уровне IRQL (при условии, что вы никогда явно не захватываете спин-блокировку, используемую для защиты списка). Для работы с битовой маской, управляющей работе!! DPC (скажем, маской, описывающей последние прерывания), также можно воспользоваться функциями InterlockedOr, InterlockedAnd и InterlockedXor.
И все же самый простой выход — просто позаботиться о том, чтобы устройство не выдавало прерывания между запросом DPC и моментом времени, когда функция DPC завершит свою работу.
Третья подробность DPC, которую я считаю менее важной по сравнению с двумя только что рассмотренными, относится к приоритетам DPC. Вызывая функцию KeSetlmportanceDpc, вы назначаете своему вызову DPC один из трех уровней приоритета:
О Mediumimportance (по умолчанию) — означает, что запрос DPC должен ставиться в очередь после всех DPC, находящихся в очереди в настоящий момент. Если DPC ставится в очередь другого процессора, то не стоит рассчитывать, что другой процессор немедленно прервет свою работу для обслуживания DPC. Если же запрос ставится в очередь текущего процессора, ядро запрашивает прерывание DPC сразу же, как только появится возможность приступить к обслуживанию DPC;
О Highlmportance — DPC ставится в очередь на первое место. Если одновременно поступают запросы на два и более высокоприоритетных вызова DPC, то запрос, поставленный в очередь последним, будет обслужен первым;
О Lowlmportance — DPC ставится в конец очереди. Кроме того, ядро не обязательно будет запрашивать прерывание DPC для процессора, которому положено обслужить запрос.
Обработка прерываний
365
Механизм приоритетов DPC призван влиять на скорость обслуживания DPC, но не обязательно контролировать ее. Даже запрос DPC с низким приоритетом может инициировать прерывание DPC на другом процессоре, если на нем будет достигнут пороговый размер очереди DPC или если обработка DPC идет слишком медленно. Если устройство способно выдать повторное прерывание перед выполнением функции DPC, то перевод DPC на низкий приоритет увеличивает вероятность появления нескольких рабочих элементов (work items). Если же DPC привязан к другому процессору (не к тому, который запрашивает DPC), установка для DPC высокого приоритета повышает вероятность того, что ISR будет оставаться активным на момент начала выполнения DPC. Однако ни одна из этих вероятностей не является гарантированной, и наоборот, изменение или сохранение прежнего приоритета не может заведомо предотвратить ни одну из них.
Пользовательские объекты DPC
Вы можете создавать и другие объекты DPC, помимо объекта Dpc, встроенного в объект устройства. Просто зарезервируйте память (в расширении устройства или в другом надежном месте, из которого объект не будет выгружен) для объекта KDPC и инициализируйте ее:
typedef struct _DEVICEJXTENSION {
KDPC CustomDpc;
};
KelnltiallzeDpc(&pdx->CustomDpc, (PKDEFERRED_ROUTINE) DpcRoutine. fdo);
При вызове KelnitializeDpc второй аргумент содержит адрес функции DPC в неперемещаемой памяти, а третий аргумент — произвольный контекст, который будет передан функции DPC во втором аргументе.
Функция KelnsertQueueDpc запрашивает отложенный вызов пользовательской функции DPC:
BOOLEAN inserted = KeInsertQueueDpc(&pdx->CustomDpc, argl. arg2);
Здесь argl и arg2 — произвольные контекстные указатели, передаваемые пользовательской функции DPC. Возвращаемое значение равно FALSE, если объект DPC уже находится в очереди процессора, и TRUE в противном случае.
Кроме того, объект DPC также можно удалить из очереди процессора вызовом KeRemoveQueueDpc.
Простое устройство, управляемое прерываниями
Чтобы показать, как пишутся различные функции драйвера типичного устройства, управляемого прерываниями и не использующего DMA, я написал простой пример драйвера PCI42 (см. прилагаемые материалы). Метод, применяемый при работе с такими устройствами, часто называется программируемым вводом/
366
Глава 7. Чтение и запись данных
выводом (РЮ, Programmed I/O), потому что для пересылки каждой единицы данных требуется вмешательство программы.
PCI42 представляет собой упрощенную версию драйвера контроллера PCI S5933 компании Applied Micro Circuits Corporation (AMCC). S5933 координирует взаимодействие шины PCI с дополнительным устройством, реализующим фактическую функцию устройства. Контроллер S5933 чрезвычайно гибок. В частности, он позволяет программировать энергонезависимую память, чтобы конфигурационное пространство PC вашего устройства инициализировалось нужным образом. Впрочем, в примере PC 142 контроллер S5933 используется в стандартном, «фабричном» состоянии.
Сильно упрощая, можно сказать, что драйвер WDM взаимодействует с внешним устройством, подключенным к S5933, либо посредством DMA (этот механизм будет рассматриваться в следующем основном разделе настоящей главы), либо посредством отправки/приема данных через регистры обмена данными. PCI42 использует 1 байт одного из регистров для передачи 1 байта данных.
Пакет разработчика компании АМСС для S5933 (код S5933DK1) включает две макетные карты и интерфейсную карту ISA (Industry Standard Architecture), подключаемую к макетной карте S5933 плоским кабелем. Карта ISA позволяет обращаться к S5933 со стороны внешнего устройства и обеспечивает его программную имитацию. Одним из компонентов примера PC 142 является драйвер (S5933DK1.SYS) карты ISA, он экспортирует интерфейс, используемый тестовыми программами.
Опытный специалист по «железу» лишь фыркнет над тем, как тривиально организовано управление устройством в PCI42. Однако у такого простого примера есть свое преимущество: он позволяет проследить за процессом обработки операции ввода/вывода в разумном темпе. Так что фыркните в ответ, если это позволяет ваш социальный статус.
Инициализация PCI42
Функция StartDevice в примере PCI42 работает с ресурсом порта и ресурсом прерывания. Ресурс порта описывает набор из шестнадцати 32-разрядных регистров в пространстве ввода/вывода, а ресурс прерывания описывает способность устройства работать с прерываниями INTA#. В конце StartDevice находится следующий код, специфический для конкретного устройства:
NTSTATUS StartDevIcet...)
{
ResetDevlce(pdx);
status = loConrsectInterrupt(...);
KeSynch ron1zeExecution(pdx->InterruptObject, (PKSYNCHRONIZE-ROUTINE) SetupDevIce. pdx);
return STATUS_SUCCESS;
}
Вспомогательная функция ResetDevice выполняет сброс оборудования. Одна из задач ResetDevice — предотвратить выдачу прерываний устройством (насколь-
Обработка прерываний
367
ко это возможно). Затем ISR подключается к прерыванию устройства вызовом loConnectlnterrupt. Даже перед возвратом из loConnectlnterrupt устройство может выдать прерывание, поэтому вся подготовка драйвера и оборудования должна быть завершена заранее. После подключения прерывания мы вызываем другую вспомогательную функцию SetupDevice, чтобы запрограммировать устройство на работу в нужном режиме. Этот шаг необходимо синхронизировать с ISR, потому что ISR использует те же регистры устройства, и мы должны предотвратить возможную порчу команд, передаваемых устройству. Вызов SetupDevice завершает функцию StartDevice примера PCI42. В отличие от того, о чем говорилось в главе 2, пример PCI42 не регистрирует никакие интерфейсы устройства, а следовательно, ему не нужно включать их в этот момент.
Функция ResetDevice сильно зависит от конкретного устройства. В нашем примере она выглядит так:
VOID ResetDeviceCPDEVICE-EXTENSION pdx)
{
PAGED_CODE();
WRITE_PORT_ULONG((PULONG) (pdx->portbase + MCSR). MCSR_RESET); // 1
LARGEJNTEGER timeout:
timeout.QuadPart = -10 * 10000; // To есть 10 не
KeDelayExecutlonThreadCKernel Mode, FALSE, Stlmeout):	// 2
WRITE_PORT_ULONG((PULONG) (pdx->portbase + MCSR), 0):
WRITE_PORT_ULONG((PULONG) (pdx->portbase + INTCSR),	// 3
INTCSR_INTERRUPT_MASK):
}
1.	Главный регистр управления/состояния (MCSR, Master Control/Status Register) контроллера S5933 управляет передачей данных DMA и другими действиями устройства. Изменение 4 битов этого регистра активизирует различные функции устройства. Я определил константу MCSR_RESET как маску, в которой все четыре флага сброшены. Эта и другие константы S5933 определены в файле S5933.H, входящем в проект РС142.
2.	Три флага сброса относятся к внутренним функциям S5933 и вступают в действие немедленно. Установка четвертого флага в 1 выдает сигнал сброса внешнему устройству. Чтобы отменить сигнал сброса, флаг необходимо обнулить. В общем случае необходимо дать оборудованию немного времени на распознавание сигнала сброса. Функция KeDelayExecutionThread, упоминавшаяся в главе 4, приостанавливает работу потока примерно на 10 мс. Значение этой константы можно повышать или понижать для устройств с разными требованиями, но не забывайте, что продолжительность ожидания не должна быть меньше гранулярности системных часов. Так как поток при этом блокируется, выполнение должно вестись на уровне PASSIVE^LEVEL в контексте фиксированного потока. Эти
368
Глава 7. Чтение и запись данных
условия выполняются, потому что вызов в конечном счете поступает от РпР Manager, который отправил нам запрос IRP_MN_START_DEVICE в полном ожидании, что мы заблокируем текущий системный поток,
3.	Последним шагом сброса устройства должна стать очистка всех необработанных прерываний. Регистр S5933 INTCSR (Interrupt Control/Status Register) содержит шесть флагов прерываний. Запись единичных битов во все шесть позиций стирает все необработанные прерывания, остальные биты INTCSR разрешают прерывания различных видов. Обнуляя все биты, мы блокируем работу устройства, насколько это возможно.
Наша функция SetupDevice весьма проста:
VOID SetupDevice! PDEV ICEJXTENS ION pdx)
{
WRITEJTRTJJLONGt (PULONG) (pdx->portbase + INTCSR),
INTCSR_IMBI_ENABLE
(INTCSR_MB1 « INTCSR_IMBI_REG_SELECT_SHIFT)
(INTCSRJYTEO « INTCSR_IMBIJYTE_SELECT_SHIFT)
):
}
Функция перепрограммирует INTCSR и указывает, что прерывание должно происходить при изменении байта 0 входного регистра обмена данными 1. Для этого контроллера также можно задать другие условия возникновения прерываний, включая сброс конкретного байта заданного выходного регистра обмена данными, а также завершение чтения или записи данных по каналу DMA.
Начало операции чтения
Функция Startlo в примере PCI42 написана по уже рассмотренному шаблону:
VOID Startlo!IN PDEVICE_OBJECT fdo, IN PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
PIO_STACK_LOCATION stack = JoGetCurrentlrpStackLocatlon(Irp);
i f (1 stack ^Parameters. Read. Length)
{
StartNextPacket(&pdx->dqReadWr1te, fdo);
CompleteRequest!Irp, STATUS_SUCCESS, 0);
return;
}
pdx->buffer = (PUCHAR) Irp->Assoc1atedIrp.SystemBuffer;	// 1
pdx->nbytes = stack->Parameters.Read.Length:
pdx->numxfer = 0;
KeSynchronizeExecution(pdx->InterruptObject,	//2
(PKSYNCHRONIZE-ROUTINE) TransferFIrst. pdx);
}
Обработка прерываний
369
1. В расширении устройства сохраняются параметры, описывающие предстоящую операцию ввода. В PCI42 используется метод DO_BUFFERED_IO — это нетипично, но зато драйвер получается достаточно простым для учебного примера.
2. Прерывание уже подключено, поэтому наше устройство может выдать его в любой момент. При возникновении прерываний ISR будет пересылать байты данных, но нужно позаботиться о том, чтобы ISR никогда не путался в буферах данных или количестве читаемых байтов. Чтобы ограничить энтузиазм ISR, мы включаем в расширение устройства флаг с именем busy, который обычно равен FALSE. Сейчас настало время установить его в состояние TRUE. Как обычно при работе с общими ресурсами, установку флага необходимо синхронизировать с кодом его проверки в ISR, поэтому нам придется вызвать синхронизированную функцию критической секции (см. главу 6). Также может оказаться, что байт данных уже доступен, в этом случае первое прерывание не произойдет. Вспомогательная функция TransferFirst проверяет эту возможность и читает первый байт. Код TransferFirst выглядит так:
VOID TransferFI rst(PDEVICEJEXTENSION pdx)
{
pdx->busy = TRUE;
ULONG mbef = READ_PORT_ULONG((PULONG) (pdx->portbase + MBEF)):
if (! (mbef & MBEF_IN1_O))
return;
*pdx->buffer = READ_PORT__UCHAR(pdx->portbase + IMB1);
++pdx->buffer;
++pdx->numxfer;
If (-pdx->nbytes == 0)
{
pdx->busy = FALSE;
PIRP Irp = GetCurrentIrp(&pdx~>dqReadWr1te):
Irp->IoStatus.Status = STATUS-SUCCESS;
Irp->IoStatus.Information = pdx->numxfer;
IoRequestDpc(pdx->DeviceObject, NULL, pdx):
}
}
S5933 содержит регистр MBEF (Mailbox Empty/Full), биты которого обозначают текущий статус каждого байта каждого регистра обмена данными. В данном примере мы проверяем, содержит ли байт регистра, используемого для ввода (регистр 1, байт 0), непрочитанные данные. Если данные имеются, мы их читаем. В результате счетчик может достигнуть порогового значения. У нас уже имеется функция (DpcForlsr), которая знает, что делать с полным запросом, поэтому если выясняется, что этот первый байт удовлетворяет запрос, мы запрашиваем DPC. (Вспомните, что выполнение ведется на уровне DIRQL под защитой спин-блокировки, потому что при вызове используется синхронизированная функция критической секции, поэтому мы не можем просто сразу завершить IRP.)
370
Глава?. Чтение и запись данных
Обработка прерывания
При нормальной работе в примере PC 142 контроллер S5933 выдает прерывание при поступлении нового байта данных в регистр обмена данными 1. Далее управление передается следующему обработчику прерывания:
BOOLEAN Onlnterrupt(PKINTERRUPT InterruptObject,
PDEVICE_EXTENSION pdx) {
ULONG intcsr =	// 1
READ_PORT_ULONG((PULONG) (pdx->portbase + INTCSR)) ;
If (!(intcsr & INTCSRJNTERRUPT-PENDING)) return FALSE;
BOOLEAN dpc = FALSE;
PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);	// 2
if (pdx->busy)	// 3
{
if (Irp->Cancel)
status = STATUS_CANCELLED;
else
status = AreRequestsBeingAborted(&pdx->dqReadWrite);
if (!NT_SUCCESS(status))
dpc = TRUE, pdx->nbytes = 0;
}
while (Intcsr & INTCSR_INTERRUPT_PENDING)	// 4
{
If (intcsr & INTCSRJMBI)	// 5
if (pdx->nbytes && pdx->busy) {
*pdx->buffer = READ_PORT_UCHAR(odx->portbase + IMB1);
++pdx->buffer;
++pdx->numxfer;
if (!--pdx->nbytes) {
Irp->IoStatus.Information = pdx->numxfer;
dpc = TRUE;
status = STATUS-SUCCESS;
}
}
}
WRITE_PORT_ULONG((PULONG) (pdx->portbase + INTCSR), intcsr); // 6
intcsr = READ_PORT_ULONG((PULONG) (pdx->portbase + INTCSR)); // 7 }
Обработка прерываний
371
if (dpc)	// 8
pdx->busy = FALSE;
Irp->IoStatus.Status = status;
IoRequestDpc(pdx->DeviceObject, NULL, NULL);
}
return TRUE;
1.	Наша первая задача — определить, пытается ли наше собственное устройство выдать прерывание в настоящий момент. Мы читаем регистр S5933 INTCSR и проверяем бит (INTCSR_INTERRUPT__PENDING) с информацией обо всех необработанных причинах прерываний. Если бит сброшен, управление немедленно возвращается. Теперь становится ясно, почему я выбрал указатель на расширение устройства в качестве контекстного аргумента (при вызове loConnectlnterrupt): нам нужен немедленный доступ к этой структуре для получения базового адреса порта.
2.	При использовании DEVQUEUE текущий запрос IRP отслеживается в объекте очереди. Прерывание может оказаться неожиданным, потому что в данный момент никакие IRP не обслуживаются. В этом случае следует сбросить прерывание, не делая ничего больше.
3.	Также может оказаться, что произошло событие Plug and Play или управления питанием, из-за которого все новые IRP отвергаются диспетчерской функцией. Функция DEVQUEUE AreRequestsBeingAborted сообщает нам об этом факте, чтобы мы могли немедленно отменить текущий запрос. Отмена активного запроса ~ вполне разумная мера для таких устройств, как наше, обрабатывающих байт за байтом. Также стоит проверить, не был ли отменен запрос IRP, если на его завершение уйдет много времени. Если ваше устройство выдает прерывания только после завершения долгой пересылки данных, исключите эту проверку из обработчика прерывания.
4.	Начинается цикл, который завершается после очистки всех прерываний нашего устройства. В конце цикла мы заново читаем INTCSR и определяем, не возникли ли новые условия прерываний. Если проверка дает положительный результат, цикл повторяется. В данном случае жадничать с процессорным временем не следует — мы хотим предотвратить каскадное накопление прерываний в системе, поскольку обработка прерывания сама по себе является довольно затратной операцией.
5.	Если работа S5933 была прервана из-за события обмена данными, мы читаем из регистра новый байт в буфер ввода/вывода текущего IRP. Если заглянуть в регистр MBEF сразу же после чтения, вы увидите, что операция чтения сбрасывает бит, соответствующий байту 0 входного регистра 1. Обратите внимание: нам не нужно проверять MBEF и убеждаться в том, что байт действительно изменился. Устройство было запрограммировано таким образом, чтобы прерывание возникало только при изменении этого одного байта.
372
Глава 7. Чтение и запись данных
6.	Запись в INTCSR предыдущего содержимого приводит к сбросу 6 битов прерываний R/WС, при этом несколько битов, доступных только для чтения, не изменяются, а все биты управления чтением/записью сохраняют исходные значения.
7.	Мы читаем содержимое INTCSR, чтобы узнать о возникновении дополнительных условий прерываний. Если такие условия возникли, цикл повторяется
8.	В процессе выполнения предыдущего кода логической переменной dpc было задано значение TRUE, если сейчас для завершения текущего IRP уместно использовать вызов DPC.
Функция DPC для PCI42 выглядит следующим образом:
VOID DpcForlsr(PKDPC Dpc, PDEVICE__OBJECT fdo, PIRP junk,
PDEVICEJXTENSION pdx) {
PIRP Irp = GetCurrentIrp(&pdx->dqReadWr1te);
StartNextPacket(&pdx->dqReadWrite, fdo); loCompleteRequestUrp, IO_NO_INCREMENT); }
Тестирование PCI42
Если вы хотите проверить PCI42 в действии, вам придется проделать ряд подготовительных действий. Прежде всего достаньте и установите макетную карту S5933DK1 вместе с интерфейсной картой ISA. Используйте мастера установки нового оборудования для установки драйверов S5933DK1.SYS и PCI42.SYS. В Windows 98 макетная карта почему-то была опознана как неработоспособная звуковая карта, и мне пришлось удалить ее в Диспетчере устройств, прежде чем я смог установить PCI42 в качестве ее драйвера. В Windows ХР идентификация прошла нормально.
Затем запустите программы ADDONSIM и TEST, находящиеся в каталоге PCI42 в прилагаемых материалах. ADDONSIM записывает данные в регистр обмена данными через интерфейс ISA. TEST читает байт данных из PC 142. Определение значения байта данных остается читателю для самостоятельной работы.
DMA
Поддержка пересылки данных по каналам прямого доступа к памяти, или DMA (Direct Memory Access), в Windows ХР базируется на абстрактной модели компьютера, показанной на рис. 7.6. В этой модели компьютер обладает набором регистров отображения, обеспечивающих преобразование между физическими адресами и адресами шины. Каждый регистр отображения содержит адрес одного физического блока памяти. Устройства обращаются к памяти для чтения и записи с использованием логического (специфического для шины) адреса. Регистры отображения играют ту же роль, что и записи таблиц страниц для программ: они позволяют оборудованию использовать при адресации числовые значения, отличные от тех, которые используются процессором.
DMA
373
пространство
Рис. 7.6. Абстрактная модель компьютера для пересылки данных по каналам DMA
Некоторые процессоры (например, Alpha) действительно содержат аппаратные регистры отображения. На одной из фаз инициализации пересылки DMA (а конкретно в фазе отображения, о которой вскоре пойдет речь) некоторые регистры резервируются для дальнейшего использования. На других процессорах, в том числе и на Intel х86, регистры отображения отсутствуют, но драйверы пишутся так, словно они существуют. В фазе отображения на таких компьютерах могут резервироваться буферы физической памяти, принадлежащие системе, в этом случае операция DMA использует зарезервированный буфер. Разумеется, кто-то должен скопировать данные в буфер DMA или из него до или после пересылки. В некоторых случаях (скажем, при обслуживании устройств управления пересылкой данных по шине1 с функцией scatter/gather) в фазе отображения в архитектурах без регистров отображения может вообще ничего не происходить.
Для описания характеристик DMA устройства и управления доступом к потенциально общим ресурсам (таким как системные каналы DMA и регистры отображения) ядро Windows ХР использует структуру данных, называемую объектом адаптера. Указатель на объект адаптера обычно получается при вызове loGetDma-Adapter во время обработки StartDevice. Объект адаптера содержит указатель на структуру DmaOperations, которая, в свою очередь, содержит указатели на все остальные функции, которые вам потребуется вызывать (табл. 7.4). Эти функции заменяют глобальные функции (loAllocateAdapter, loMapTransfer и т. д.), использовавшиеся в предыдущих версиях Windows NT. Более того, имена глобальных функций теперь преобразованы в макросы, вызывающие функции DmaOperations.
1 Далее для краткости — устройств управления шиной. — Примеч. перев.
374
Глава 7. Чтение и запись данных
Таблица 7.4, Указатели на функции DmaOperations
Указатель на функцию DmaOperation	Описание
PutDmaAdapter AllocateCommonBuffer FreeCommonBuffer	Уничтожает объект адаптера Выделяет общий буфер Освобождает общий буфер
AllocateAdapterChannel FlushAdapterBuffers FreeAdapterChannel FreeMapRegisters MapTransfer GetDmaAlignment	Резервирует объект адаптера и регистры отображения Очищает промежуточные буферы данных после пересылки Освобождает объект адаптера и регистры отображения Освобождает только регистры отображения Программирует одну фазу пересылки Получает информацию о выравнивании адресов, необходимую для адаптера
ReadDmaCounter	Определяет значение счетчика
GetScatterGatherList	Резервирует объект адаптера и конструирует список scatter/gather
PutScatterGatherList	Освобождает список scatter/gather
Стратегии пересылки
Способ пересылки DMA зависит от нескольких факторов:
О Если устройство обладает функцией управления шиной, оно обладает всей необходимой электроникой для обращения к основной памяти. Вы лишь должны сообщить ему несколько основных параметров — откуда начинать, сколько единиц данных передавать, какая выполняется операция (ввод или вывод) и т. д. За подробностями обращайтесь к проектировщикам оборудования, иначе вам придется самостоятельно извлекать информацию из спецификации, указывающей, что должно происходить на аппаратном уровне.
О Устройство с функцией scatter/gather способно пересылать большие блоки данных в несмежные участки физической памяти. Использование функции scatter/gather повышает эффективность программ, потому что она снимает необходимость в захвате больших блоков смежных страниц. Страницы фиксируются там, где они находятся в физической памяти, а устройству достаточно сообщить информацию об их местонахождении.
О Если устройство не управляет шиной, вам придется использовать системный контроллер DMA на материнской плате компьютера. Такая разновидность DMA иногда называется подчиненной. Системный контроллер DMA, связанный с шиной ISA, обладает рядом ограничений на доступность физической памяти и максимальным объемом пересылаемых данных без перепрограммирования. У контроллера шины EISA (Extended Industry Standard Architecture) такие ограничения отсутствуют. Вам не нужно знать (по крайней мере, в Windows ХР), к какому типу шины подключается устройство, потому что операционная система автоматически учтет все действующие ограничения.
DMA
375
О Обычно операции DMA сопряжены с программированием аппаратных регистров отображения или копированием данных до или после операции. Если устройство должно читать или записывать данные непрерывно, выполнять эти действия для каждого запроса ввода/вывода нежелательно — замедление обработки может оказаться неприемлемым для вашей конкретной ситуации. По этой причине обычно выделяется общий буфер, с которым драйвер и устройство могут работать одновременно.
Несмотря на многочисленные различия в подробностях, обусловленные взаимодействием этих четырех факторов, выполняемые действия обладают и определенным сходством. На рис. 7.7 изображена общая схема пересылки. Пересылка начинается в функции Startlo с запроса на владение объектом адаптера. Владение имеет смысл только при совместном использовании системного канала DMA с другими устройствами, но модель DMA в Windows ХР требует выполнять этот шаг в любом случае. Когда I/O Manager сможет предоставить вам право владения, он выделяет регистры отображения для временного использования и осуществляет обратный вызов предоставленной вами функции управления адаптером. В функции управления адаптером выполняется операция отображения, обеспечивающая условия для выполнения первого (а возможно, и единственного) этапа пересылки. Если выделить достаточное количество регистров не удалось, операцию придется выполнять в несколько этапов. Ваше устройство должно быть способно перенести любые задержки, возникающие между этапами.
Рис. 7,7. Последовательность действий при пересылке DMA
После того как функция управления адаптером инициализирует регистры отображения для первой фазы, устройству подается сигнал о начале операции.
376
Глава 7. Чтение и запись данных
При завершении этой исходной фазы устройство выдает прерывание, по которому планируется DPC. Функция инициирует другую пересылку или завершает запрос.
На какой-то стадии процесса происходит освобождение регистров отображения и объекта адаптера. Момент наступления этих двух событий — это одна из тех подробностей, которые различаются в зависимости от факторов, упоминавшихся ранее в этом разделе.
Выполнение пересылки DMA
В этом разделе я подробно опишу механику так называемого пакетной пересылки DMA, при которой изолированный блок данных передается в буфере, сопровождающем пакет запроса ввода/вывода. Начнем с простого. Допустим, вы столкнулись с весьма распространенной ситуацией: устройство управляет шиной PCI, но не обладает возможностями scatter/gather.
Начнем с того, что при создании объекта устройства вы обычно обозначаете свое намерение использовать прямой метод буферизации, устанавливая флаг DO_ DIRECT_IO. Выбор этого метода объясняется тем, что в конечном счете адрес списка дескрипторов памяти будет передаваться в одном из аргументов вызываемой функции MapTransfer. Правда, этот вызов создает некоторые проблемы с выравниванием буфера. Если только приложение не использует флаг FILE-FLAG-NO-BUFFERING при вызове CreateFile, I/O Manager не обеспечивает соблюдения требований к выравниванию для данного устройства (AlignmentRequirement) по отношению к буферам данных пользовательского режима (для вызовов со стороны режима ядра эти требования вообще не учитываются, за исключением отладочных сборок). Следовательно, если устройство или HAL требует, чтобы буфер DMA начинался с определенной границы, возможно, для соблюдения требований к выравниванию вам придется скопировать небольшой блок пользовательских данных в правильно выровненный внутренний буфер — либо отклонять любые запросы с неправильно выровненным буфером.
В функции StartDevice объект адаптера создается кодом следующего вида:
DEVICE-DESCRIPTION dd;
RtlZeroMemory(&dd. sizeof(dd));
dd.Version = DEVICE_DESCRIPTION_VERSION;
dd.Master = TRUE:
dd.InterfaceType = InterfaceTypeUndefined;
dd.MaximumLength = MAXTRANSFER:
dd.Dnia32BjtAddresses = TRUE;
pdx->AdapterObject = IoGetDmaAdapter(pdx->Pdo, &dd, &pdx->nMapRegisters);
Последняя команда в этом фрагменте особенно важна. Функция loGetDma-Adapter во взаимодействии с драйвером шины или HAL создает объект адаптера и возвращает вам его адрес. Первый параметр (pdx->Pdo) идентифицирует объект физического устройства (PDO). Второй параметр указывает на структуру
DMA
377
DEVICE-DESCRIPTION, инициализируемую характеристиками DMA устройства. Последний параметр указывает, где система должна хранить максимальное количество регистров отображения, которые вам будет разрешено пытаться резервировать в ходе одной пересылки. Обратите внимание: я зарезервировал два поля в расширении устройства (Adapterobject и nMapRegisters) для хранения выходных данных этой функции.
ЙОВЕТ-----------------------------------------------------------------—-------------------
Если задать в поле InterfaceType структуры DEVICE-DESCRIPTION значение InterfaceTypellndefined, I/O Manager направит внутренний запрос драйверу шины, чтобы узнать, к какому типу шины подключено устройство. Это избавит вас от необходимости жестко кодировать тип шины или вызывать loGetDeviceProperty для его самостоятельного определения.
В функции StopDevice объект адаптера уничтожается следующим вызовом:
VOID StopDevIceC...)
{
If (pdx->AdapterObject)
(*pdx->AdapterObject->DmaOperations->PutDmaAdapter) (pdx->AdapterObject);
pdx->AdapterObject = NULL;
}
В настройках Driver Verifier можно запросить режим проверки DMA. В этом случае программа будет следить за соблюдением протокола от создания объекта адаптера до его уничтожения вызовом PutDmaAdapter. Если драйвер портируется из Windows NT версии 4, возможно, при переходе на новый протокол Windows ХР вы столкнетесь с фатальными сбоями.
Если устройство управляет шиной, не рассчитывайте на получение «официальных» ресурсов DMA. Другими словами, в цикл извлечения ресурсов не нужно включать секцию CmResourceTypeDma. PnP Manager не выделяет ресурс DMA, потому что само устройство содержит всю необходимую электронику для выполнения пересылки DMA, поэтому никакие дополнительные ресурсы выделять не нужно.
В предыдущих версиях Windows NT для получения объекта адаптера DMA использовалась функция HalGetAdapter. Эта функция все еще продолжает существовать для сохранения совместимости, но новые драйверы WDM должны вызывать loGetDmaAdapter. Различия между этими двумя функциями состоят в том, что loGetDmaAdapter сначала выдает PnP IRP IRP_MN_QUERY_INTERFACE для определения того, поддерживает ли объект физического устройства интерфейс прямого вызова GUID_BUS_INTERFACE-STANDARD. Если интерфейс поддерживается, то loGetDmaAdapter использует его для создания объекта адаптера, а если нет — просто вызывает HalGetAdapter.
В табл. 7.5 перечислены поля структуры DEVICE-DESCRIPTION, передаваемой loGetDmaAdapter. Для устройств, управляющих шиной, важны только поля,
378
Глава 7. Чтение и запись данных
представленные в предыдущем фрагменте кода StartDevice. HAL может и не знать, распознает ли устройство 32- или 64-разрядные адреса (например, Intel х86 HAL использует этот флаг только при выделении общего буфера или прг поддержке РМЕ (Physical Memory Extensions)), но вы все равно должны указывать поддержку этой возможности для сохранения портируемости. Обнуляя всю структуру, мы задаем поле ScatterGather равным FALSE. Так как системный канал DMA использоваться не будет, функция создания объекта адаптера не будет проверять содержимое полей DmaChannel, DmaPort, DmaWidth, DemandMode. Autoinitialize, IgnoreCount и DmaSpeed.
Таблица 7.5, Структура описания устройства, используемая с loGetDmaAdapter
Имя поля	Описание	Важно для устройств
Version	Номер версии структуры — инициализируется значением DEVICE.DESCRIPTIO INVERSION	Все
Master	Устройство управления шиной — задается на основании информации об устройстве	Все
ScatterGather	Устройство поддерживает списки scatter/ gather — задается на основании информации об устройстве	Все
DemandMode	Использование режима требования системного контроллера DMA — задается на основании информации об устройстве	Подчиненные
Autoinitialize	Использование режима автоинициализации системного контроллера DMA — задается на основании информации об устройстве	Подчиненные
Dma32BitAddresses	Возможность использования 32-разрядных физических адресов	Все
IgnoreCount	Контроллер не обеспечивает точного значения счетчика пересылки — задается на основании информации об устройстве	Подчиненные
Reserved 1	Зарезервировано — должно быть равно FALSE	
Dma64BitAddresses	Возможность использования 64-разрядных физических адресов	Все
DoNotUse2	Зарезервировано — должно быть равно FALSE	
DmaChannel	Номер канала DMA — инициализируется на основании атрибута Channel дескриптора ресурса	Подчиненные
InterfaceType	Тип шины — инициализируется значением InterfaceTу peUndefined	Все
DmaWidth	Ширина передаваемых данных — Width8Bits, Widthl6Bits или Width32Bits в зависимости от информации об устройстве	Все
DmaSpeed	Скорость пересылки — Compatible, ТуреА, ТуреВ, ТуреС или TypeF в зависимости от информации об устройстве	Подчиненные
DMA
379
Имя поля	Описание	Важно для устройств
Maximum Length	Максимальная длина данных в одной	Все пересылке — задается на основании информации об устройстве (и округляется до кратного PAGE_SIZE)
DmaPort	Номер порта для шины Microchannel —	Подчиненные инициализируется на основании атрибута Port дескриптора ресурса
Чтобы инициировать операцию ввода/вывода, функция Startlo сначала должна зарезервировать объект адаптера вызовом функции AllocateAdapterChannel объекта. Один из аргументов AllocateAdapterChannel содержит адрес функции управления адаптером, которая вызывается I/O Manager после завершения резервирования. Пример кода подготовки и вызова AllocateAdapterChannel:
typedef struct _DEVICE_EXTENSION {
PADAPTER_OBJECT Adapterobject; // Объект адаптера устройства // 1
ULONG nMapRegisters; // Максимальное количество регистров отображения
ULONG nMapReglstersAllocated; // Количество для данной пересылки
ULONG numxfer:	//	Количество переданных байтов
ULONG xfer;	//	Количество байтов, пересылаемых в этой фазе
ULONG nbytes;	//	Количество байтов, оставшихся для пересылки
PVOID vaddr;	//	Виртуальный адрес для текущей фазы
PVOID regbase;	//	База регистров отображения для этой фазы
} DEVICEJXTENSION, *PDEVICE_EXTENSION;
VOID StartloCPDEVICEJDBJECT fdo, PIRP Irp)
{
PDEVICEJXTENSION pdx =
(PDEVICE-EXTENSION) fdo->Dev1ceExtensIon;
PMDL mdl = Irp->MdlAddress;	// 2
pdx->numxfer = 0: pdx->xfer = pdx->nbytes = MmGetMdlByteCount(mdl);
pdx->vaddr = MmGetMdlVirtual Address(mdl);
ULONG nregs = ADDRESS_AND_SIZE_TO_SPAN_PAGES(pdx->vaddr,	// 3
pdx->nbytes);
If (nregs > pdx->nMapReg1sters)
{
nregs = pdx->nMapReg1sters;
pdx->xfer = nregs * PAGERSIZE - MmGetMdlByteOffset(mdl);
}
pdx->nMapReg1stersAllocated = nregs;
NTSTATUS status = (*pdx->AdapterObject->DmaOperat1ons	// 4
^AllocateAdapterChannel)(pdx->AdapterObject, fdo, nregs,
380
Глава 7. Чтение и запись данных
(PDRIVER_CONTROL) AdapterControl, pdx);
if (!NT_SUCCESS(status))
{
CompleteRequestCIrp, status, 0);
StartNextPacket(&pdx->dqReadWrite, fdo)
}
1.	Расширение устройства содержит несколько полей, связанных с пересылкой DMA. Назначение полей описано в комментариях.
2.	Эти команды инициализируют поля расширения устройства для первой фазы пересылки.
3.	Здесь вычисляется количество регистров отображения, запрашиваемых у системы для использования на этой стадии пересылки. Сначала мы вычисляем количество регистров, необходимых для всей пересылки. Макрос ADDRESS-AND_SIZE -TO-SPAN_PAGES учитывает, что буфер может выходить за границу страницы. Тем не менее, полученное число может превышать максимум, разрешенный исходным вызовом loGetDmaAdapter. В этом случае пересылку приходится выполнять за несколько этапов. Таким образом, на первом этапе количество регистров сокращается до максимально разрешенного. Также мы запоминаем, сколько регистров отображения было выделено (в поле nMapRegistersAllocated расширения устройства), чтобы позднее освободить именно это число.
4.	При вызове AllocateAdapterChannel передаются адрес объекта адаптера, адрес нашего объекта устройства, вычисленное количество регистров отображения и адрес нашей функции управления адаптером. Последний аргумент (pdx) используется в качестве контекстного параметра функции управления адаптером. В общем случае один объект адаптера может совместно использоваться несколькими устройствами. Совместное использование объекта адаптера на практике применяется только при работе с системным контроллером DMA; устройства, управляющие шиной, обладают собственными, специализированными объектами адаптеров. Но так как вам не обязательно знать, как система принимает решение о создании объекта адаптера, не стоит делать на этот счет какие-либо предположения. Таким образом, в общем случае объект адаптера может оказаться занятым при вызове AllocateAdapterChannel, и ваш запрос будет помещен в очередь вплоть до его освобождения. Кроме того, все устройства DMA на компьютере совместно используют один набор регистров отображения. Ожидание освобождения запрашиваемого количества регистров также может стать причиной дополнительной задержки. Обе задержки возникают внутри функции AllocateAdapterChannel. вызывающей функцию управления адаптером, когда станут доступными объект адаптера и все запрошенные регистры отображения.
Хотя устройство PCI, управляющее шиной, обладает собственным объектом адаптера, при отсутствии функции scatter/gather ему также придется использовать регистры отображения. На процессорах с аппаратными регистрами (скажем, Alpha) функция AllocateAdapterChannel зарезервирует их для вашего использова
DMA
381
ния. На процессорах, не имеющих собственных регистров отображения (таких, как процессоры Intel), функция AllocateAdapterChannel резервирует программные суррогаты - например, непрерывные блоки физической памяти.
ГО СТАВИТ В ОЧЕРЕДЬ ФУНКЦИЯ ALLOCATEADAPTERCHANNEL?--------------——-----------------
Объектом, который AllocateAdapterChannel помещает в очередь при ожидании объекта адаптера или необходимого количества регистров отображения, является ваш объект устройства. Некоторые аппаратные архитектуры разрешают выполнять несколько пересылок DMA одновременно. Поскольку в очереди объектов адаптеров может находиться только один объект устройства (во всяком случае, без сбоя системы), для использования множественных пересылок DMA необходимо создать фиктивные объекты устройств.
Как я уже говорил, AllocateAdapterChannel в конечном итоге вызывает функцию управления адаптером (на уровне DISPATCHJ_EVEL, как функция Startlo). Вы должны выполнить две операции. Первая операция — вызов функции MapTransfer объекта адаптера для подготовки регистров отображения и других системных ресурсов к первой фазе операции ввода/вывода. Для устройств, управляющих шиной, MapTransfer возвращает логический адрес, представляющий начальную точку первой фазы. Этот логический адрес может совпадать с физическим адресом процессора, а может и не совпадать. Все, что вам необходимо знать о нем, — то, что этот адрес следует использовать при программировании оборудования. MapTransfer также может усечь длину запроса в соответствии с количеством используемых регистров отображения, именно по этой причине в аргументе передается адрес переменной, содержащей текущую длину фазы.
Вторая операция — выполнение всех действий по передаче устройству информации о физическом адресе и инициированию работы устройства:
1O__ALLOCATION_ACTION Adaptercontrol(PDEVICE_OBJECT fdo,
PIRP junk, PVOID regbase, PDEVICEJXTENSION pdx)
{
PIRP Irp = GetCurrentIrp(&pdx->dqReadWr1te);	// 1
PMDL mdl = Irp->MdlAddress;
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
BOOLEAN isread = stack->MajorFunction == IRP_MJ_READ;	11 2
pdx->regbase = regbase;	// 3
KeFlushloBufferslrndl, isread,	TRUE);	//4
PHYSICALJMDDRESS address -	//5
C*pdx->Adapter0bject->Dma0perat1ons->MapTrarisfer) (pdx->AdapterObject, mdl, regbase, pdx->vaddr, pdx->xfer, !1sread);
// 6
return DeallocateObjectKeepReglsters;	//7
}
1.	Во втором аргументе Adaptercontrol (я назвал его junk) находится содержимое поля Currentlrp объекта устройства на момент вызова AllocateAdapterChannel. Если для организации очереди IRP используется объект DEVQUEUE, информацию
382
Глава 7. Чтение и запись данных
о текущем IRP следует получить у него. Если для управления очередями используются функции Microsoft loStartPacket и loStartNextPacket, аргумент junk содержит правильный IRP. В данном случае я сохранил информацию в переменной с именем Irp.
2.	Коды операций ввода и вывода с использованием DMA различаются не так уж сильно, поэтому часто бывает удобно выполнять обе операции в одной функции. Эта строка кода проверяет основной код функции IRP и определяет, какая операция выполняется — чтение или запись.
3.	Аргумент regbase содержит приватный манипулятор, описывающий набор регистров отображения, зарезервированных на время этой операции. Значение reg base потребуется нам позднее, поэтому мы сохраняем его в расширении устройства.
4.	Функция KeFlushloBuffers обеспечивает очистку содержимого всех процессорных кэшей для используемого буфера. Третий аргумент (TRUE) означает, что кэш сбрасывается в процессе подготовки операции DMA. Необходимость этого шага может быть обусловлена архитектурой процессора, потому что в общем случае операции DMA выполняются непосредственно с памятью и кэши в них могут быть не задействованы.
5.	Функция MapTransfer программирует оборудование DMA для одного этапа пересылки и возвращает физический адрес, с которого она должна начаться. Во втором аргументе функции передается адрес MDL. Обычно при создании объекта устройства указывается метод буферизации DO_DIRECT_IO, и I/O Manager автоматически создает MDL за вас. В другом аргументе передается база регистров отображения (regbase). Чтобы указать, какая часть MDL задействована в этой фазе операции, мы передаем виртуальный адрес (pdx->vaddr) и количество байтов (pdx->xfer). MapTransfer использует аргумент виртуального адреса для вычисления смещения в области буфера, по которому определяются номера физических страниц с данными.
6.	В этой точке производится программирование, специфическое для данного устройства. Например, можно использовать одну из функций HAL WRITE_Xxr для пересылки физического адреса и количества байтов в регистры карты, а затем отправить в регистр команд сигнал о начале передачи данных.
7.	Возвращая константу DeallocateObjectKeepRegisters, мы показываем, что использование объекта адаптера завершено, но регистры отображения еще продолжают использоваться. В этом конкретном примере (устройство с функциями управления шиной PCI) изначально отсутствует какая-либо конкуренция за объект адаптера, поэтому освобождение объекта адаптера никакой роли не играет. Тем не менее, в других ситуациях с устройствами, управляющими шиной, контроллер DMA может использоваться совместно с другими устройствами. Освобождение объекта адаптера позволит этим устройствам начать пересылку с использованием набора регистров отображения, отличного от задействованных нами.
DMA
383
Как правило, прерывание происходит вскоре после начала пересылки, а обработчик прерывания запрашивает DPC для обработки завершения первой фазы пересылки. Функция DPC выглядит примерно так:
VOID DpcForIsr(PKDPC Dpc. PDEVICEJjBJECT fdo,
PIRP jjnk, PDEVICE-EXTENSION pdx)
{
PIRP Irp = GetCurrentIrp(&pdx->dqReadhlrite);	// 1
PMDL mdl = Irp->MdlAddress;
BOOLEAN isread = IoGetCurrentIrpStackLocation(Irp)
->MajorFunction == IRP_MJ_READ;
(*pdx->AdapterObject->DmaOperations->FlushAdapterBuffers) // 2 (pdx->AdapterObject, mdl. pdx->regbase, pdx->vaddr, pdx->xfer, !Isread):
pdx->nbytes -= pdx->xfer:	// 3
pdx->numxfer += pdx->xfer;
NTSTATUS status = STATUS-SUCCESS;
if (pdx->nbytes && NT_SUCCESS(status))	// 4
pdx->vaddr = (PVOID) ((PUCHAR) pdx->vaddr + pdx->xfer); // 5 pdx->xfer = pdx->nbytes:
ULONG nregs = ADDRESS_AND_SIZE_TO_SPAN_PAGES(pdx->vaddr.	// 6
pdx->nbytes);
if (nregs > pdx->nflapRegi stersAl located) {
nregs = pdx->nMapRegistersAllocated;
pdx->xfer = nregs * PAGE_SIZE;
}
PHYSICAL_ADDRESS address =
(*pdx->AdapterObject->DmaOperations->MapTransfer) (pdx->AdapterObject. mdl. pdx->regbase. pdx~>vaddr, pdx->xfer, !isread);
} else { ULONG numxfer = pdx->numxfer; (*pdx->AdapterObject->DmaOperations->PreeMapRegisters)	// 7
(pdx->AdapterObject. pdx->regbase.
pdx->nMapRegi stersAl1ocated):
StartNextPacket(&pdx->dqReadWrite. fdo):	// 8
CompleteRequestCIrp, status, numxfer);
}}
1.	При использовании DEVQUEUE текущий запрос IRP отслеживается в объекте очереди.
2.	Функция FlushAdapterBuffers предназначена для ситуации, в которой пересылка требует использования промежуточных буферов, принадлежащих системе.
384
Глава 7. Чтение и запись данны>
Если завершившаяся операция ввода вышла за пределы страницы, то входные данные хранятся в промежуточном буфере и их необходимо скопировать в буфер пользовательского режима.
3.	Мы обновляем счетчики данных после только что завершенной фазы пересылки.
4.	В этой точке следует проверить, как завершилась текущая фаза пересылки — успешно или с ошибкой. Например, можно прочитать порт состояния или проанализировать результаты аналогичной операции, выполненной вашим обработчиком прерывания. В нашем примере переменной status задается значение STATUS_SUCCESS; предполагается, что в случае обнаружения ошибки переменная будет изменена.
5.	Если пересылка еще не завершена, необходимо запрограммировать следующую фазу. Процесс начинается с вычисления виртуального адреса следующей части буфера пользовательского режима. Помните, что эти вычисления — не более чем работа с числами, мы не пытаемся обратиться к памяти по виртуальному адресу. Разумеется, обращение к памяти недопустимо, потому что выполнение ведется в контексте произвольного потока.
6.	Несколько следующих команд практически идентичны тем, которые выполнялись в первой фазе для Startlo и AdapterControi. Конечный результат представляет собой логический адрес, который может использоваться при программировании устройства. Он может, но не обязан соответствовать физическому адресу, используемому процессором. При этом возникает одно небольшое затруднение: возможности использования регистров отображения ограничены тем их числом, которое было выделено функцией управления адаптером. Функция Startlo сохранила это число в поле nMapRegistersAilocated расширения устройства.
7.	Если пересылка завершена, необходимо освободить используемые регистры отображения.
8.	Оставшиеся команды функции DPC обеспечивают завершение запроса IRP, из-за которого функция получила управление.
Пересылка с использованием списков scatter/gather
Если устройство обладает поддержкой scatter/gather, системе будет гораздо проще организовать прием и отправку данных по каналам DMA. Функция scatter/gather позволяет устройству выполнять пересылку с использованием страниц, не занимающих непрерывные участки физической памяти.
Функция StartDevice создает объект адаптера практически так же, как было показано ранее, с одним исключением: флагу ScatterGather задается значение TRUE.
Традиционный метод (то есть метод, использовавшийся в предыдущих версиях Windows NT) программирования пересылки DMA с использованием функциональности scatter/gather практически не отличается от пакетного примера, рассмотренного в предыдущем разделе. Единственное заметное отличие состоит в том, что вместо одного вызова MapTransfer для каждой фазы пересылки приходится выполнять несколько вызовов. Каждый вызов дает информацию,, необходимую для одного элемента списка scatter/gather, содержащего физический адрес
DMA
385
и длину данных. После завершения цикла список scatter/gather пересылается устройству методом, специфическим для данного устройства, после чего инициируется пересылка.
Мы сделаем ряд допущений о среде, в которой будет конструироваться список scatter/gather. Для начала будем считать, что константа MAXSG обозначает максимальное количество элементов списка scatter/gather, обрабатываемых устройством. Чтобы по возможности упростить задачу, будем считать, что для конструирования списка можно просто воспользоваться структурой SCATTER_GATHER_LIST, определенной в файле WDM.H:
typedef struct JCATTER_GATHER_ELEMENT {
PHYSICAL_ADDRESS Address;
ULONG Length;
ULONG_PTR Reserved;
} SCATTER_GATHER_ELEMFNT, *PSCATTER_GATHER_ELEMENT;
typedef struct _SCATTER_GATHER_LIST {
ULONG NumberOfElements:
ULONG_PTR Reserved:
SCATTER_GATHER_ELEMENT Elements[];
} SCATTER_GATHER_LIST, *PSCATTER-GATHER_LIST;
Наконец, будем считать, что мы можем просто выделить список scatter/gather максимального размера в функции AddDevice и оставить его до момента, когда в нем возникнет надобность:
pdx->sglist = (PSCATTER_GATHER_LIST)
ExAllocatePool(NonPagedPool. sizeof(SCATTER-GATHER-LIST) +
MAXSG * sizeof(SCATTER_GATHER_ELEMENT));
При наличии подобной инфраструктуры функция Adaptercontrol выглядит так:
IO_ALLOCATION-ACTION Adaptercontrol(PDEVICE_OBJECT fdo.
PIRP junk, PVOID regbase. PDEVICE-EXTENSION pdx)
{
PIRP Irp = GetCurrentIrp(&pdx->dqReadWr1te);	// 1
PMDL mdl = Irp->MdlAddress;
BOOLEAN Isread = loGetCurrentlrpStackLocation(Irp)
->MajorFunct1on == IRP_MJ_READ;
pdx->regbase = regbase:
KeFlushIoBuffers(mdl, Isread, TRUE);
PSCATTER_GATHER_LIST sgl1st = pdx->sgl1st;
ULONG xfer = pdx->xfer:	// 2
PVOID vaddr = pdx->vaddr;
pdx->xfer = 0;
ULONG isg = 0;
while (xfer && Isg < MAXSG)	// 3
386
Глава 7. Чтение и запись данных
ULONG el ein = xfer:
sglist->Elements[isg],Address =	// 4
(*pdx->AdapterObject->DmaOperations->MapTransfer) (pdx->AdapterObject. mdl, regbase, pdx->vaddr, &elen, llsread);
sglist->Elements[1sgLLength = elen;
xfer -= elen:	// 5
pdx->xfer += elen:
vaddr = (PVOID) ((PUCHAR) vaddr + elen);
++1sg:	// 6
sglist->NumberOfElements = Isg:
//7
return DeallocateObjectKeepRegisters:	// 8
1.	О получении указателя на правильный запрос IRP в функции управления адаптером уже говорилось ранее.
2.	В функции Startlo значение pdx->xfer вычислялось на основании допустимого количества регистров отображения. Сейчас мы пытаемся пересылать соответствующий объем данных, но он может быть дополнительно ограничен допустимым количеством элементов scatter/gather. В следующем цикле xfer обозначает количество неотображенных байт, и значение pdx->xfer пересчитывается при каждой итерации.
3.	А вот и обещанный цикл, в котором функция MapTransfer вызывается для конструирования элементов списка scatter/gather. Цикл продолжается вплоть до отображения всей фазы пересылки или исчерпания элементов scatter/gather (в зависимости от того, что произойдет раньше).
4.	При вызове для устройства scatter/gather функция MapTransfer изменяет аргумент длины (elen), показывая, какая часть MDL с заданного виртуального адреса (vaddr) является физически смежной, а следовательно, может быть отображена на один элемент списка scatter/gather. Функция возвращает физический адрес начала смежного блока.
5.	Здесь происходит обновление переменных, описывающих текущую фазу пересылки. При выходе из цикла переменная xfer уменьшается до 0 (в противном случае были исчерпаны все элементы списка scatter/gather), pdx->xfer содержит суммарное количество всех элементов, которые нам удалось отобразить, a vaddr — адрес байта за последним отображенным. Поле pdx->vaddr в расширении устройства не обновляется — это будет сделано в функции DPC. Просто еще одна важная мелочь...
6.	Увеличение индекса элемента scatter/gather означает, что обработка одного элемента была закончена.
7.	На этой стадии мы получили isg элементов scatter/gather, которые следует запрограммировать в устройство в соответствии с требованиями последнего. Затем устройство запускается на обработку запроса.
DMA
387
8.	Устройства, управляющие шиной, возвращают код DeallocateObjectKeepRegisters. Теоретически, устройство, не обладающее функциями управления шиной, также может поддерживать функцию scatter/gather; такие устройства возвращают код KeepObject.
Теперь устройство выполняет пересылку DMA и, вероятно, сигнализирует о завершении выдачей прерывания. Обработчик прерывания запрашивает DP, а функция DPC инициирует следующую фазу операции. В функции DPC используется цикл MapTransfer, аналогичный тому, который приводился при описании инициализации. Подробный анализ кода предоставляется читателю для самостоятельной работы.
Функция GetScatterGatherList
В Windows 2000 и Windows ХР предусмотрено ускоренное решение, позволяющее избежать относительно громоздких циклических вызовов MapTransfer для распространенных ситуаций, когда регистры отображения вообще не используются в пересылке либо их число не превышает максимума, возвращаемого функцией loGetDmaAdapter. В этом ускоренном решении, продемонстрированном в примере SCATGATH в прилагаемых материалах, вызов AllocateAdapterChannel заменяется вызовом GetScatterGatherList. Функция Startlo выглядит примерно так:
VOID StartIo(PDEVICE_OBJECT fdo, PIRP Irp)
{
PDEVICE-EXTENSION pdx =
(PDEVICEJXTENSION) fdo->DeviceExtension;
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp); NTSTATUS status;
PMDL mdl = Irp->MdlAddress;
ULONG nbytes = MmGetMdlByteCount(mdl);
PVOID vaddr = MmGetMdlVIrtualAddress(mdl);
BOOLEAN isread = stack->MajorFunction == IRP_MJ_READ; pdx->numxfer = 0;
pdx->nbytes = nbytes;
status =
C*pdx->AdapterObject->DmaOperations->GetScatterGatherList) Cpdx->AdapterObject, fdo, mdl, vaddr, nbytes, (PDRIVER_LIST_CONTROL) DmaExecutionRoutine, pdx, ’isread);
if (!NT_SUCCESS(status))
{
CompleteRequestCIrp, status, 0);
StartNextPacket(&pdx->dqReadWrite, fdo);
}
}
Вызов GetScatterGatherList, выделенный жирным шрифтом в этом фрагменте, и является главным различием между Startlo и той функцией, которую мы рассматривали в предыдущем разделе. GetScatterGatherList при необходимости ждет
388
Глава 7. Чтение и запись данных
получения доступа к объекту адаптера и всем необходимым регистрам отображения. Затем она создает структуру SCATTER_GATHER_LIST и передает ее Dma Execution -Routine. Устройство программируется с использованием физических адресов в элементах scatter/gather, после чего инициируется пересылка:
VOID DmaExecut1onRout1ne(PDEVICE_OBJECT fdo, PIRP junk, PSCATTER_GATHER_LIST sgl1st, PDEVICE_EXTENSION pdx) {
PIRP Irp = GetCurrentIrp(&pdx->dqReadWr1te);
pdx->sg!1st = sgl1st;	// 1
// 2
}
1. Адрес списка scatter/gather потребуется в функции DPC, которая освобождает список вызовом PutScatterGatherList.
2. Здесь устройство программируется на выполнение чтения или записи по парам «адрес—длина», содержащимся в списке scatter/gather. Если количество элементов в списке превышает возможности их обработки устройством, пересылку придется выполнять в несколько фаз. Если фазы программируются достаточно быстро, я рекомендую включить в обработчик прерывания логику инициирования дополнительных фаз. Если задуматься, DmaExecutionRoutine, вероятно, все равно будет синхронизироваться с обработчиком прерывания для запуска первой фазы, поэтому дополнительная логика много места не займет. Я учитывал эту идею при программировании примера SCATGATH. После завершения пересылки вызовите функцию PutScatterGatherList для объекта адаптера, чтобы освободить список и адаптер:
VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT fdo, PIRP junk,
PVOID Context)
{
(*pdx->AdapterObject->DmaOperdt1ons->PutScatterGatherL-ist) (pdx->AdapterObject, pdx->sg!1st, !Isread);
}
Чтобы принять решение об использовании GetScatterGatherList, необходимо заранее определить, выполняются ли все необходимые условия. Прежде всего, драйвер должен работать в Windows 2000 или более поздней системе, потому что эта функция недоступна в Windows 98/Ме. На 32-разрядных платформах Intel устройствам с поддержкой scatter/gather на шинах PCI и EISA регистры отображения не потребуются. Даже на шине ISA вам будет разрешено запросить до 16 суррогатов регистров отображения (8, если устройство управляет шиной) — если только в системе не возникла такая нехватка физической памяти, что система ввода/вывода не может выделить промежуточные буферы. Впрочем, в этом случае вам все равно не удастся использовать DMA традиционным способом, так что причин для беспокойства нет.
DMA
389
Если во время программирования драйвера вы не можете с полной уверенностью предсказать, можно ли будет использовать GetScatterGatherList, я рекомендую ограничиться традиционным циклическим вызовом MapTransfer. Этот код все равно придется включить для тех случаев, когда GetScatterGatherList не работает, а наличие двух логических ветвей является лишь напрасным усложнением.
Пересылка с использованием системного контроллера
Если устройство не управляет шиной, для выполнения пересылки DMA придется использовать системный контроллер DMA. Как я уже говорил, такие устройства часто называются подчиненными, потому что они зависят от внешнего контроллера. Системные контроллеры DMA обладают рядом характеристик, влияющих на внутренние особенности пересылки DMA:
О Существует ограниченное количество каналов DMA, совместно используемых всеми подчиненными устройствами. В таких ситуациях функция Ailocate-AdapterChannel обретает реальный смысл, потому что в любой момент времени конкретный канал может использоваться только одним устройством.
О В списке ресурсов ввода/вывода, полученных от PnP Manager, можно рассчитывать найти ресурс CmResourceTypeDma.
О Устройство связывается (на физическом или логическом уровне) с конкретным каналом, который им используется. Если вы можете настроить соединение с каналом DMA, вам придется отправлять соответствующие команды при выполнении StartDevice.
О Системные контроллеры DMA для компьютеров с шиной ISA могут обращаться к буферам данных, находящимся только в первых 16 мегабайтах физической памяти. В отдельный момент времени существуют четыре канала для пересылки 8-разрядных данных и три канала для пересылки 16-разрядных данных. Контроллеры 8-разрядных каналов некорректно работают с буферами, пересекающими границу 64 Кбайт; контроллеры 16-разрядных каналов некорректно работают с буферами, пересекающими границу 128 Кбайт.
Несмотря на эти обстоятельства, код драйвера будет в целом похож на код устройств, управляющих шиной, о которых говорилось ранее. Просто функции StartDevice придется выполнить дополнительную работу по настройке вызова loGetDmaAdapter, а операции освобождения объекта адаптера и регистров отображения по-другому распределяются между Adaptercontrol и функцией DPC.
В StartDevice включается дополнительный код, который определяет, какой канал DMA вам выделил PnP Manager, кроме того, в структуре DEVICE-DESCRIPTION также инициализируются дополнительные поля для loGetDmaAdapter:
NTSTATUS StartDevice(...)
{
ULONG dmachannel:	// Номер системного канала DMA
ULONG dmaport:	// Номер порта для шины MCA
for (ULONG 1 = 0: i < nres; ++1, ++resource)	*
390
Глава 7. Чтение и запись данных
switch (resource->Type)
case CmResourceTypeDma:
dmachannel = resource->u.Dma.Channel;
dmaport = resource->u.Dma.Port;
break:
}
// 1
DEVICE_DESCRIPTION dd:
RtlZeroMemory(&dd, sizeof(dd));
dd.Version = DEVICE-DESCRIPTION J/ERSION;
dd.InterfaceType *= InterfaceTypeUndeflned:
dd.MaxImumLength == MAXTRANSFER;
dd.DmaChannel = dmachannel:	// 2
dd.DmaPort = dmaport;
dd.DemandMode = ??;
dd.AutoInltlallze == ??;
dd.IgnoreCount = ??:
dd.DmaWldth = ??;
dd.DmaSpeed = ??;
pdx->AdapterObject = loGetDmaAdapter(...): }
1. В списке ресурсов ввода/вывода будет присутствовать запись ресурса DMA, из которой необходимо извлечь номера канала и порта. Номер канала идентифицирует один из каналов DMA, поддерживаемых системным контроллером DMA. Номер порта важен только на компьютерах с шиной MCA (Micro Channel Architecture).
2. Начиная с этой точки, необходимо инициализировать несколько полей структуры DEVICE-DESCRIPTION на основании имеющейся информации об устройстве (см. табл. 7.5),
Практически все, что относится к функции управления адаптером и процедурам DPC, идентично коду, приведенному ранее для устройств, управляющих шиной без функции scatter/gather, за исключением двух небольших подробностей. Во-первых, Adaptercontrol возвращает другое значение:
IOjALLOCATION_ACTION Adaptercontrol(...)
return KeepObject:
Возвращаемое значение KeepObject означает, что мы хотим сохранить контроль над регистрами отображения и используемым каналом DMA. Во-вторых,
DMA
391
поскольку объект адаптера не освобождался при выходе из AdapterControl, это необходимо сделать в функции DPC, вызывая FreeAdapterChannel вместо FreeMap-Registers:
VOID DpcForlsr(...)
{
(*pdx->AdapterObject->DmaOperations->FreeAdapterChairel)
(pdx->AdapterObject):
Использование общего буфера
Как я упоминал ранее в разделе «Стратегии пересылки», вы можете выделить общий буфер, который будет использоваться устройством при пересылках DMA. Общий буфер должен находиться в блоке неперемещаемой, физически смежной памяти. Драйвер обращается к буферу драйвера по фиксированному виртуальному адресу. Устройство использует фиксированный логический адрес для обращения к тому же буферу.
Возможны несколько вариантов работы с общим буфером. Ваш драйвер может поддерживать устройство, выполняющее непрерывную пересылку данных в память и из нее, используя режим автоинициализации системного контроллера DMA. В этом режиме завершение одной пересылки заставляет контроллер немедленно инициировать следующую пересылку.
Общий буфер также способен предотвратить лишнее копирование данных. Функция MapTransfer часто копирует передаваемые данные во вспомогательные буферы, принадлежащие I/O Manager и используемые в пересылках DMA. При выполнении подчиненных пересылок DMA по шине ISA особенно вероятно, что MapTransfer будет выполнять дополнительное копирование данных в соответствии с требованиями к адресации 16 Мбайт и выравниванию буферов, установленным для контроллера ISA DMA. Наличие общего буфера позволит избежать лишнего копирования.
Выделение общего буфера
Обычно общий буфер выделяется во время выполнения функции StartDevice, после создания объекта адаптера:
typedef struct _DEVICE_EXTENSION {
PVOID vaCommonBufter;
PHYSICALJ\DDRESS paCommonBuffer;
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
dd.Dma32BjtAddresses = ??;
dd.Dma64BitAddresses == ??;
392
Глава 7. Чтение и запись данньо
pdx->AdapterObject = IoGetDmaAdapter(...);
pdx->vaCommonBuffer =
(*pdx->AdapterObject->DmaOperations->AllocateCommonBuffer)
(pdx->AdapterObject, <length>, &pdx->paCommonBuffer, FALSE):
Перед вызовом loGetDmaAdapter следует установить в структуре DEVICE_ DESCRIPTION флаги Dma32BitAddresses и Dma64BitAddresses в соответствии с фактическими возможностями адресации вашего устройства. Иначе говоря, если устройство способно адресовать буфер по произвольному 32-разрядному физическому адресу, флаг Dma32BitAddresses устанавливается равным TRUE. Если устройство способно адресовать буфер по произвольному 64-разрядному физическому адресу, то флаг Dma64BitAddresses устанавливается равным TRUE.
При вызове AllocateCom mon Buffer второй аргумент содержит длину выделяемого буфера в байтах. Четвертый аргумент содержит значение типа BOOLEAN, которое указывает, может ли выделенная память попасть в кэш процессора (TRUE) или нет (FALSE).
Функция AllocateCom mon Buffer возвращает виртуальный адрес, который будет использоваться внутри драйвера для обращения к выделенному буферу. Функция AllocateCommonBuffer также заносит в структуру PHYSICAL_ADDRESS, на которую указывает третий аргумент, логический адрес, используемый устройством для обращения к своему буферу.
ПРИМЕЧАНИЕ---------------------------------------------------------------------------
В DDK адреса, возвращаемые функцией MapTransfer и передаваемые в третьем аргументе AllocateCommonBuffer, называются логическими. Во многих процессорных архитектурах логический адрес представляет собой физический адрес памяти, «понятный» для процессора. В других архитектурах это может быть адрес, «понятный» только для шины ввода/вывода. Возможно, его стоило бы назвать адресом шины.
Подчиненная пересылка DMA с общим буфером
Чтобы выполнить подчиненную пересылку DMA, необходимо создать MDL для описания получаемых виртуальных адресов. Впрочем, реально MDL создается лишь для занятия аргумента в предстоящем вызове MapTransfer. На самом деле функции MapTransfer копировать данные не придется, но чтобы узнать об этом, ей необходим список MDL! Обычно MDL создается в функции StartDevice сразу же после выделения общего буфера:
pdx->vaCommonBuffer = ...:
pdx->mdlCommonBuffer = loAllocateMdl(pdx->vaCommonBuffer,
<length>, FALSE, FALSE, NULL):
MmBuildMdlForNonPagedPool(pdx->mdlCommonBuffer);
Чтобы выполнить операцию вывода, сначала следует каким-то образом поместить в буфер данные, отправляемые устройству (скажем, прямым копированием содержимого памяти). Остальная логика DMA в драйвере, фактически, не отличается от той, что приводилась ранее (раздел «Выполнение пересылки DMA»). Вы вызываете функцию AllocateAdapterChannel, она вызывает функцию управления адаптером, которая, в свою очередь, вызывает KeFlushloBuffers (если был
DMA
393
выделен кэшируемый буфер), а затем MapTransfer. Ваша функция DPC вызывает FlushAdapterBuffers и FreeAdapterChannel. Во всех перечисленных вызовах список MDL общего буфера указывается вместо того, который сопровождал обрабатываемый запрос IRP на чтение или запись. Некоторые из вызываемых сервисных функций при наличии общего буфера не выполняют столько полезной работы, как без него, но их все равно необходимо вызвать. Вероятно, в конце операции ввода вам также придется скопировать данные из общего буфера в другое место.
Чтобы выполнить запрос на чтение или запись данных, объем которых превышает объем общего буфера, вам придется периодически заново заполнять или очищать буфер. Функция объекта адаптера ReadDmaCounter позволяет проверить ход текущей пересылки и решить, что делать дальше.
Пересылка DMA устройством, управляющим шиной, с общим буфером
Если устройство обладает функцией управления пересылкой данных по шине, выделение общего буфера позволит обойтись без AllocateAdapterChannel, MapTransfer и FreeMapRegisters. Вызывать эти функции не нужно, потому что AllocateCommon-Buffer также резервирует регистры отображения, необходимые устройству для обращения к буферу. Каждое устройство, управляющее шиной, обладает приватным объектом адаптера, который не используется совместно с другими устройствами, следовательно, вам никогда не придется ждать освобождения этого объекта. Так как у вас имеется виртуальный адрес, по которому можно в любой момент обратиться к буферу, а функция управления шиной позволяет обращаться к буферу по физическому адресу, полученному от AllocateCommonBuffer, никакой дополнительной работы не потребуется.
Предостережения относительно использования общих буферов
При выделении и использовании общих буферов необходимо учитывать некоторые обстоятельства. В работающей системе физически смежная память встречается редко - - настолько редко, что вам, возможно, не удастся выделить буфер нужного размера, если только попытка не делается на достаточно ранней стадии нового сеанса. Подсистема управления памятью пытается (правда, не слишком настойчиво) перемещать страницы в памяти для удовлетворения вашего запроса, и этот процесс может на некоторое время отложить возврат управления функцией AllocateCommonBuffer. Но попытка может завершиться неудачей — обязательно предусмотрите обработку для такого случая. Общий буфер связывает не только потенциально редкие физические страницы, но и регистры отображения, которые могли бы быть использованы другими устройствами. По этим двум причинам стратегию общего буфера следует применять осмотрительно.
Другое предостережение по поводу общих буферов обусловлено тем фактом, что подсистема управления памятью обязана выделить вам одну или несколько полных страниц памяти. Выделение общего буфера длиной всего несколько байт неэффективно, поэтому таких решений следует избегать. С другой стороны, также неэффективно выделять несколько страниц памяти, которые в действительности
394
Глава 7. Чтение и запись данных
не обязаны быть физически смежными. Как указано в DDK, если блоки не обязаны быть смежными, лучше запросить несколько блоков меньшего размера.
Освобождение общего буфера
Память, занимаемая общим буфером, обычно освобождается в функции StopDevice непосредственно перед уничтожением объекта адаптера:
(*pdx->Adapter0bject->Dma0perat1ons->FreeCommonBuffer) (pdx->AdapterObject, <length>, pdx->paCommonBuffer, pdx->vaComfflonBuffer, FALSE);
Второй параметр FreeCommonBuffer содержит значение длины, указанное при выделении буфера. Последний параметр указывает, является ли память кэшируемой, его значение также должно совпадать с последним аргументом, указанным при вызове AllocateCommonBuffer.
Простое устройство, управляющее шиной
Пример драйвера PKTDMA в прилагаемых материалах показывает, как выполнять операции DMA для устройств, управляющих шиной, без поддержки scatter/ gather, на примере контроллера PCI AMCC S5933. Инициализация устройства этим драйвером уже рассматривалась при описании функции StartDevice, а инициирование пересылки DMA — при описании Startlo. Кроме того, мы почти полностью рассмотрели все, что происходит в функциях AdapterControl и DpcForlsr. Ранее я указывал, что эти функции содержат код инициирования операции, специфический для конкретного устройства. Для этой цели я написал вспомогательную функцию с именем StartTransfer:
VOID StartTransfer(PDEVICE_EXTENSION odx, PHYSICAL_ADDRESS address. BOOLEAN isread)
ULONG mcsr = READ_PORTJJLONG((PULONG)(pdx->portbase + MCSR);
ULONG Intcsr =
READ_PORT__ULONG((PULONG)(pdx->portbase + INTCSR);
if (Isread)
{
mcsr = MCSR_WRITE_NEED4 MCSR_WRITE_ENABLE;
Intcsr = INTCSR-WTCI-ENABLE;
WRITE_PORT_ULONG((PULONG)(pdx->portbase + MWTC), pdx->xfer); // 1 WRITE_PORT_ULONG((PULONG)(pdx->portbase + MWAR),
address.LowPart);
} else { mcsr = MCSR_READ_NEED4 MCSR_READ_ENABLE; intcsr = INTCSR_RTCI_ENABLE;
WRITE_PORT_ULONG((PULONG)(pdx->portbase + MRTC), pdx->xfer); //1 WRITE_PORT_ULONG((PULONG)(pdx->portbase + MRAR).
DMA
395
address.LowPart);
}
WRITE_PORT_ULONG((PULONG)(pdx->portbase + INTCSR), intcsr); // 2
WRITE_PORT_ULONG((PULONG)(pdx->portbase + MCSR). mcsr);	// 3
}
Функция готовит регистры S5933 к пересылке DMA, после чего инициирует пересылку. Последовательность действий выглядит так:
1.	Регистры адреса (MxAR) и счетчика пересылки (МхТС) программируются в соответствии с направлением потока данных. Компания AM С С решила описывать термином «чтение» операцию, при которой данные перемещаются из памяти в устройство. Следовательно, при реализации запросов IRP__MJ_WRITE на уровне контроллера программируется операция чтения. В качестве адреса используется логический адрес, полученный от MapTransfer.
2.	Когда счетчик пересылки дойдет до 0, включите прерывание записью в регистр INTCSR.
3.	Начните пересылку, устанавливая один из битов включения пересылки в регистре MCSR.
Из приведенного фрагмента это не очевидно, но чип S5933 в действительности способен выполнять чтение DMA одновременно с записью DMA. Пример PKTDMA написан так, что в любой момент времени выполняется только одна операция (чтение или запись). Чтобы драйвер мог выполнять обе операции одновременно, необходимо: а) реализовать отдельные очереди для IRP чтения и записи и б) создать два объекта устройства и два объекта адаптера (одна пара для чтения, другая для записи) во избежание попыток дважды поставить в очередь один объект в функции AllocateAdapterChannel. Я решил, что дополнительное усложнение примера только запутает читателя. (Конечно, предполагая, что читатель еще не запутался, возможно, я слишком оптимистично отношусь к своему стилю подачи материала, но могло быть и хуже.)
Обработка прерываний в PKTDMA
Обработчик прерываний в примере PCI42 выполнял небольшую работу по перемещению данных. В PKTDMA обработчик прерывания выглядит чуть проще:
BOOLEAN OnI interrupt(PKINTERRUPT InterruptObject.
PDEVICE^EXTENSION pdx)
{
ULONG intcsr -
READ_PORT_ULONG((PULONG) (pdx->portbase + INTCSR));
If (!(intcsr & INTCSRJNTERRUPT-PENDING))
return FALSE;
ULONG mesr = READ_PORT_ULONG((PULONG) (pdx->portbase + MCSR));
WRITE_PORT_ULONG((PULONG) (pdx->portbase + MCSR),	// 1
mesr & ~(MCSR_WRITE_ENABLE MCSR_READJNABLE)):
intcsr &= ~(INTCSR_WTCI_ENABLE INTCSR_RTCI_ENABLE);	// 2
396
Глава 7. Чтение и запись данных
BOOLEAN dpc = GetCurrentIrp(&pdx->dqReadWrite) != NULL;
while (Intcsr & INTCSRJNTERRUPT_PENDING)
{
InterlockedOr(&pdx->intcsr, intcsr);
WRITE_PORT_ULONG((PULONG) (pdx->portbase + INTCSR). intcsr): intcsr = READ_PORT_ULONG((PULONG) (pdx->portbase + INTCSR)): }
if (dpc)
IoRequestDpc(pdx->DeviceObject, NULL. NULL);
return TRUE;
}
Я укажу лишь на различия между этими двумя обработчиками:
1. S5933 пытается передавать данные, учитываемые регистром счетчика, до тех пор, пока в MCSR остаются установленными разрешающие биты. Эта команда сбрасывает оба бита. Если бы драйвер выполнял операции чтения и записи одновременно, вы бы определили тип только что завершенной операции по флагам прерывания в INTCSR, после чего заблокировали бы передачу данных в этом направлении.
2. Мы собираемся вскоре выполнить обратную запись в INTCSR для сброса прерывания. Эта команда гарантирует, что мы также заблокируем прерывания по нулевому счетчику пересылки, чтобы они более не возникали.
Проверка PKTDMA
При наличии макетной карты S5933DK1 вы можете протестировать драйвер PKTDMA. Если вы уже запускали тест PCI42, значит, драйвер S5933DK1.SYS для интерфейсной карты ISA уже установлен в вашей системе, а если нет — вам придется установить его для тестирования. Затем установите PKTDMA.SYS, драйвер самой макетной карты S5933. После этого можно запускать тестовую программу TEST.EXE из каталога PKTDMA\TEST\DEBUG. TEST выполняет операцию записи 8192 байтов в PKTDMA. Кроме того, она выдает S5933 команду DeviceloControl на повторное чтение данных и проверяет, что данные были прочитаны верно.
Проблемы совместимости с Windows 98/Ме
MmGetSystemAddressForMdISafe — макрос, который вызывает функцию (МтМар LockedPagesSpecifyCache), не экспортируемую в Windows 98/Ме. Использовавшийся ранее макрос MmGetSystemAddressForMdl сейчас считается устаревшим. Driver Verifier отмечает вызовы старого макроса во время выполнения. Различия двух макросов заключаются в том, что MmGetSystemAddressForMdl инициирует фатальный сбой, если в таблице страниц не найдется достаточно записей для отображения на заданную память, a MmGetSystemAddressForMdISafe просто возвращает указатель NULL.
г лемы совместимости с Windows 98/Ме
397
У проблемы с макросом MmGetSystemAddressForMdISafe имеется портируемое ** пение:
"SHORT oldfail = mdl->MdlFlags & MDL_MAPPING_CAN_FAIL:
Tdl->MdlFlags = MDL_MAPPING_CAN_FAIL;
*VOID address = MmMapLockedPages(mdl, KernelMode);
if (loldfall)
mdl->MdlFlags &= ~MDL_MAPPING_CAN_FAIL;
Установка флага MDL_MAPPING_CAN_FAIL заставляет Windows 2000 и ХР выпол-и ить тот же внутренний код, что и при использовании MmMapLockedPagesSpecify-Cache, таким образом выполняется требование об использовании нового макроса Windows 98/Ме этот флаг игнорируют (и кстати говоря, в случае сбоя эти системы всегда возвращали NULL, поэтому реальной необходимости во флаге »*ти новом макросе никогда не существовало).
Если вы используете мою библиотеку GENERIC.SYS, просто вызовите функцию GenericGetSystemAddressForMdl, которая содержит приведенный код. Я не пытался включить MmMapLockedPagesSpecifyCache в WDMSTUB.SYS (см. приложение А), потому что Windows 98/Ме не обеспечивает всей инфраструктуры, необходимой тля поддержки этой функции.
8 Управление питанием
Технофобы находят утешение в том факте, что они, в конечном счете, сохраняют власть над своими электронными слугами, пока могут дотянуться до кнопки питания1. Конечно, питание абсолютно необходимо для любого электронного устройства, но до недавнего времени персональные компьютеры не особенно хорошо справлялись с задачами управления питанием.
Эффективное управление питанием важно по крайней мере по трем причинам. Во-первых, снижение энергопотребления снижает влияние компьютеров на окружающую среду. Не только компьютеры потребляют меньше энергии, но и системы кондиционирования воздуха в тех помещениях, где эти компьютеры находятся. Вторая причина хорошо знакома многим пользователям, которым приходится много путешествовать: технология изготовления аккумуляторов просто не поспевает за потребностями всевозможных мобильных устройств. И наконец, постепенное расширение функций PC по обслуживанию домашней техники также зависит от качественного управления питанием. Современные машины во включенном состоянии шумят вентиляторами и жесткими дисками, а их запуск из отключенного состояния занимает много времени. Ускорение перехода в работоспособное состояние и ликвидация лишнего шума — что также означает снижение энергопотребления для уменьшения потребностей в охлаждении — является необходимым условием, чтобы PC смогли занять достойную нишу в потребительском секторе.
В этой главе мы рассмотрим роль драйверов WDM в системе управления питанием операционных систем Microsoft Windows ХР и Microsoft Windows 98/Ме. В первом разделе, «Модель управления питанием в WDM», представлен обзор тех концепций, о которых вам необходимо знать. Второй раздел, «Управление переходами», занимает центральное место в этой главе: в нем описываются чрезвычайно сложные задачи, которые должен выполнять типичный функциональный драйвер. Глава заканчивается обсуждением некоторых вторичных аспектов функциональных драйверов WDM, относящихся к управлению питанием.
1 В английском тексте здесь игра слов: power — власть и power — электропитание. — Примеч. ред.
Модель управления питанием в WDM
399
1одель управления питанием в WDM
В Windows ХР и Windows 98/Ме операционная система берет на себя большую часть работы по управлению питанием. Конечно, это имеет смысл только в том случае, если операционная система действительно хорошо понимает, что же происходит в настоящий момент. Например, если поручить управление питанием системе BIOS, она не сможет отличить, когда экран используется приложением, а когда — экранной заставкой (screensaver). Но операционная система может отличить одно от другого, а следовательно, определить, когда можно отключить экран.
Операционная система как глобальный владелец политики энергопотребления содержит элементы пользовательского интерфейса, которые позволяют пользователю управлять всеми решениями из области энергопотребления. К их числу принадлежат панель управления, команды меню Пуск и API управления пробуждением устройств. Компонент ядра, называемый Power Manager, реализует политику управления питанием операционной системы, отправляя устройствам запросы (IRP) ввода/вывода. Драйверам WDM в основном отводится пассивная роль по обработке этих запросов. Но когда я покажу, какой объем кода в ней задействован, вы согласитесь, что эта пассивность включает массу активных действий.
Роли драйверов WDM
Один из драйверов устройства является владельцем политики энергопотребления для устройства. Поскольку эта роль чаще всего отводится функциональному драйверу, в дальнейшем обсуждении я буду считать, что это всегда так. Просто имейте в виду, что устройство может обладать уникальными требованиями, из-за которых ответственность владельца политики может быть поручена фильтрующему драйверу или драйверу шины.
Функциональный драйвер получает от Power Manager системные IRP, относящиеся к изменениям общего состояния энергопотребления в системе. Выполняя функции владельца политики для устройства, он преобразует эти инструкции в контекст устройства и выдает новые IRP (IRP устройства). Реагируя на IRP устройства, функциональный драйвер прежде всего обращает внимание на детали, относящиеся к устройству. Устройства могут хранить контекстную информацию, которая не должна теряться в периоды пониженного энергопотребления. Например, драйверы клавиатуры могут хранить состояние клавиш переключения режимов (Caps Lock, Num Lock и Scroll Lock), индикаторов и т. д. Функциональный драйвер отвечает за сохранение и восстановление этого контекста. Некоторые устройства обладают функцией пробуждения, которая позволяет им выйти из спящего состоянии при возникновении неких внешних событий; функциональный драйвер вместе с пользователем следят за тем, чтобы функция пробуждения была доступна в момент необходимости. Многие функциональные драйверы управляют очередями содержательных IRP (то есть IRP, которые читают и записывают данные на устройство), и им необходимо приостанавливать или освобождать эти очереди по мере снижения и восстановления питания.
400
Глава 8. Управление питанием
Драйвер шины, находящийся в нижней позиции стека устройства, отвечает за управление подачей тока устройству и за выполнение электронных действий, необходимых для включения/отключения функции пробуждения вашего устройства.
Фильтрующий драйвер обычно выполняет функции простого канала передачи запросов питания, передавая их драйверам нижнего уровня с использованием специального протокола, который будет описан чуть позже.
Питание устройств и состояния энергопотребления системы
В WDM для описания состояний энергопотребления используются те же термины, что и в спецификации ACPI (Advanced Configuration and Power Interface) — cm. http://www.acpi.info. Четыре возможных состояния устройств изображены на рис. 8.1. В состоянии DO устройство полностью функционально. В состоянии D3 устройство не потребляет энергии (или ограничивается минимальным потреблением, а следовательно, не функционирует (или функционирует на очень низком уровне). Промежуточные состояния D1 и D2 обозначают два разных «дремлющих» состояния устройства. По мере перехода от состояния DO к D3 устройство потребляет все меньше и меньше энергии. Кроме того, оно хранит все меньше и меньше контекстной информации о своем текущем состоянии. Соответственно, продолжительность задержки, необходимой для возврата устройства к состоянию DO, возрастает.
Снижение питания
Снижение объема сохраняемого контекста
Увеличение заделки перезапуска
Рис. 8.1. Состояния энергопотребления устройств ACPI
Компания Microsoft сформулировала требования для разных типов устройств и разделила их на классы. Я нашел эти требования по адресу http://www. microsoft.com/ hwdev/resources/specs/PMref/. Например, спецификация требует, чтобы каждое устройство поддерживало как минимум состояния DO и D3. Устройства ввода (клавиатуры, мыши и т. д.) поддерживают также состояние D1. Модемы также должны дополнительно поддерживать состояние D2. Эти различия
Модель управления питанием в WDM
401
в спецификации классов устройство обусловлены вероятными сценариями их использования и промышленными стандартами.
Операционная система не управляет состояниями энергопотребления устройств напрямую — этот вопрос находится в компетенции драйверов устройств. Вместо этого система управляет питанием при помощи системных состояний энергопотребления, аналогичных состояниям устройств ACPI (рис. 8.2). Рабочее состояние (Working) — полнофункциональное состояние компьютера с полным питанием. Программы могут выполняться только во время пребывания системы в состоянии Working.
Sleepingl
Sleeping^
SleepingS
Снижение питания
Увеличение задержки перезап^ ска
Hibernate
Shutdown
Рис» 8.2» Состояния энергопотребления системы
Остальные системные состояния соответствуют конфигурациям с пониженным энергопотреблением, в которых программы не выполняются. Состояние Shutdown соответствует отключенному питанию. (Обсуждение состояния Shutdown напоминает попытки ответить на вопросы типа «Что находится внутри черной дыры?» Тем не менее, вы должны знать, как происходит переход к состоянию Shutdown, — это необходимо для программирования драйверов.) Состояние Hibernate является разновидностью состояния Shutdown, в которой все состояние компьютера сохраняется на диске, — это позволяет восстановить состояние сеанса при возврате питания. Три состояния Sleeping между Hibernate и Working соответствуют различным градациям энергопотребления.
Переходы между состояниями питания
Система инициализируется в состоянии Working. Наверное, не стоит и говорить, что при выполнении любых команд компьютер по определению находится в состоянии Working. Большинство устройств начинают свою работу в состоянии DO, хотя владелец политики энергопотребления может перевести устройство в состояние пониженного энергопотребления, когда оно реально не используется. После полного запуска и инициализации система достигает стабильного состояния, в котором системное энергопотребление находится на уровне Working, а устройства
402
Глава 8. Управление питанием
находятся в различных состояниях в зависимости от возможностей и выполняемых действий.
В результате действий пользователя и внешних событий происходят последующие переходы между состояниями питания. Стандартный сценарий перехода встречается тогда, когда пользователь выбирает команду Ждущий режим (Stand By) в диалоговом окне Выключить компьютер (Turn Off Computer). В ответ Power Manager сначала спрашивает у каждого драйвера, можно ли отключить питание, посылая запрос IRP„MJ__POWER с дополнительным кодом функции IRP_MN„QUERY_POWER. Если все драйверы «дают добро», Power Manager посылает второй запрос IRP с дополнительным кодом функции IRP_MN_SET_POWER. В ответ на этот запрос драйверы переводят свои устройства в состояние низкого энергопотребления. Если какой-либо драйвер запретит переход, Power Manager все равно отправляет запрос IRP_MN_SET_POWER, но обычно задает текущий уровень энергопотребления вместо предложенного.
Кстати говоря, система не всегда отправляет запросы IRP_MN_QUERY_POWER. Некоторые события (такие как выключение компьютера пользователем или истечение заряда батареи) должны приниматься устройствами беспрекословно, и операционная система не запрашивает разрешения на изменение состояния. Но когда запрос выдается и драйвер соглашается с предложенным изменением состояния, он берет на себя обязательство не начинать никаких операций, способных помешать ожидаемому запросу на изменение состояния. Например, драйвер стримера перед тем, как положительно отвечать на запрос о снижении энергопотребления, должен убедиться в том, что в настоящее время не выполняется операция снятия неравномерного натяжения ленты, прерывание которой может привести к разрыву ленты. Кроме того, драйвер должен отвергать все последующие команды на перемотку, если не поступит другой запрос, свидетельствующий об отмене изменения состояния.
Обработка запросов IRP_MJ_POWER
Power Manager обменивается информацией с драйверами при помощи запросов IRP_MJ_POWER. В настоящее время определены четыре дополнительных кода функций (табл. 8.1).
Таблица 8.1. Дополнительные коды функций запроса IRP_MJ_POWER
Дополнительный код функции Описание
IRP_MN_QUERY_POWER	Проверяет, возможно ли безопасное выполнение предстоящего изменения состояния энергопотребления
IRP_MN_SET_POWER	Приказывает драйверу изменить состояние энергопотребления
IRP_MN_WAIT_WAKE	Приказывает драйверу шины активизировать функцию пробуждения; передает функциональному драйверу информацию о сигналах на пробуждение
IRP„MN_POWER_SEQUENCE	Обеспечивает оптимизацию для сохранения и восстановления контекста
Модель управления питанием в WDM
403
Субструктура Power объединения Parameters структуры IO_STACK_LOCATION содержит четыре параметра, описывающих запрос, только два из них представляют интерес для большинства драйверов WDM (табл. 8.2).
Таблица 8.2. Поля субструктуры Parameters.Power структуры IO_STACK_LOCATION
Имя поля	Описание
Systemcontext Type	Контекст, используемый во внутренней работе Power Manager DevicePowerState или SystemPowerState (значения перечисления POWER_STATE_TYPE)
State	Состояние энергопотребления — значение либо перечисляемого типа DEVICE. POWER_STATE, либо SYSTEM_POWER_STATE
ShutdownType	Код, обозначающий причину перехода в PowerSystemShutdown
Все драйверы — как фильтрующие, так и функциональные — обычно передают каждый запрос на управление питанием вниз по стеку следующему драйверу. Исключение составляют только запросы IRP_MN_.QUERY_POVVER, которые драйвер хочет отклонить, и запросы IRP, прибывающие во время удаления устройства.
Передача запросов драйверам нижних уровней подчиняется специальным правилам. Общая схема процесса и три ее возможные разновидности показаны на рис. 8.3. Во-первых, перед освобождением IRP управления питанием необходимо вызвать PoStartNextPowerlrp. Это делается даже в том случае, если IRP завершается с кодом ошибки. Необходимость такого вызова объясняется тем, что Power Manager ведет собственную очередь запросов управления энергопотреблением, поэтому ему необходимо сообщать, когда из очереди можно вывести следующий запрос и отправить его вашему устройству. Помимо вызова PoStartNextPowerlrp необходимо вызвать специальную функцию PoCailDriver (вместо loCallDriver) для отправки запроса следующему драйверу.
ПРИМЕЧАНИЕ---------------------—---------------------------------------------------
Power Manager поддерживает не одну, а целых две очереди IRP управления питанием для каждого устройства. Одна очередь предназначена для системных IRP (то есть запросов IRP_MN_SET_ POWER, в которых указывается состояние энергопотребления системы). Другая очередь предназначена для IRP устройств (то есть запросов IRP_MN_SET_POWER, в которых указывается состояние энергопотребления устройства). В любой момент времени активными могут быть по одному IRP каждого вида. Кстати говоря, драйвер также может одновременно обрабатывать запрос Plug and Play (РпР) и любое количество содержательных IRP.
Следующая функция поясняет механические аспекты передачи запроса управления энергопотреблением вниз по стеку:
NTSTATUS DefaultPowerHandlerUN PDEVICE^OBJECT fdo. IN PIRP Irp)
PoStartNextPowerlrp(Irp);	//1
loSkipCurrentlrpStackLocation(Irp):	// 2
PDEVICEJXTENSION pdx =	//3
(РОЕVICE-EXTENSION) fdo->DeviceExtens1 on;
return PoCa11 Driver(pdx->LowerDev1ceObject, Irp):
404
Глава 8. Управление питанием
а) Передача вниз на следующий уровень	б) Отказ в диспетчерской функции
I	PoStartNextPowerirp
Диспетчерская.	_	_ Диспетчерская:
функция	функция
loCopyCurrentlrpStackLocationToNext loSetCompletion Routine PoCallDriver
L________________________ ____________
в) Передача вниз с диспетчерской функцией
Рис- 8.3. Обработка запросов IRP_MJ_POWER
1.	Функция PoStartNextPowerirp сообщает Power Manager, что он может извлечь из очереди и отправить следующий IRP питания. Эта функция должна вызываться для каждого полученного IRP питания в то время, пока вы являетесь его владельцем. Другими словами, вызов должен располагаться либо в диспетчерской функции перед отправкой запроса PoCallDriver, либо в функции завершения.
2.	Функция loSkipCurrentlrpStackLocation используется для задержки указателя стека IRP на одну позицию, поскольку мы знаем, что PoCallDriver немедленно сместит его вперед. Данная методика уже рассматривалась при обсуждении передачи запросов вниз по стеку, когда нас не интересовало, что произойдет с запросом дальше.
3.	Функция PoCallDriver осуществляет пересылку запросов питания. Компания Microsoft реализовала эту функцию для предотвращения минимального, но все же ощутимого снижения производительности, которое может возникнуть из-за включения в loCallDriver дополнительной логики управления питанием. Функциональный драйвер выполняет два шага по передаче IRP вниз и выполнению действий, специфических для устройства, в строго определенном порядке, как показано на рис. 8.4. При переводе устройства в более низкое состояние энергопотребления сначала выполняются действия, зависящие от устройства, а затем запрос передается вниз. При переводе устройства на более высокое состояние энергопотребления запрос передается вниз, а действия, зависящие от устройства, выполняются в функции завершения. Подобное вложение
Модель управления питанием в WDM
405
операций гарантирует, что питание не пропадет во время манипуляций драйвера с оборудованием.
Рис. 8.4. Обработка системных запросов управления энергопотреблением
IRP питания поступают в контексте системного потока, который не должен блокироваться. Существует несколько причин, по которым блокировка потока невозможна. Если устройство обладает атрибутом INRUSH или если в объекте устройства был сброшен флаг DO_POWER_PAGABLE, Power Manager будет отправлять IRP на уровне DISPATCHJ-EVEL Конечно, вы помните, что потоки не могут блокироваться на уровне DISPATCH_LEVEL Но даже если установить флаг DO_POWERJPAGABLE, чтобы получать IRP питания на уровне PASSIVE_LEVEL, вы можете создать взаимную блокировку, запросив IRP устройства во время обработки системного IRP с последующей блокировкой: Power Manager не сможет послать IRP устройства, пока диспетчерская функция системного IRP не вернет управление, поэтому ожидание будет длиться вечно.
Обычно функциональному драйверу требуется время на выполнение некоторых шагов, необходимых для обработки некоторых запросов питания. В DDL указано, что завершение таких IRP можно откладывать на время, незаметное для конечного пользователя в конкретной ситуации, однако возможность задержки не означает возможности блокировки. Запрет блокировки во время выполнение этих операций означает широкое применение функций завершения для асинхронного выполнения этих действий.
Итак, IRP IRP__MN_QUERY_POWER задает вам вопрос, на который можно ответить как положительно, так и отрицательно. Это подразумевает, что IRP с этим дополнительным кодом функции может быть отклонен. Поступая подобным образом, вы даете отрицательный ответ. С запросами IRP_MN_SET_POWER подобной свободы выбора нет: вы должны беспрекословно выполнить полученную инструкцию.
406
Глава 8. Управление питание*
Управление переходами
Правильная обработка запросов управления энергопотреблением требует очень точного программирования, причем задача осложняется множеством факторов. Например, устройство может обладать функцией пробуждения системы из спящего состояния. Чтобы решить, подтвердить или отклонить запрос, а также какое состояние энергопотребления устройства должно соответствовать новому состоянию системы, необходимо учитывать, активна ли в настоящий момент функция пробуждения. Возможно, устройство было отключено из-за бездействия, и вам необходимо предусмотреть возможность восстановления питания при поступлении содержательного IRP. А может быть, включение устройства сопровождается большим броском тока, в этом случае Power Manager должен работать с ним особым образом... и т. д.
К сожалению, я не могу привести простое объяснение того, как организовать управление энергопотреблением в драйвере. Мне кажется, что функция, которая должна быть реализована в каждом драйвере для нормальной работы системы, должна иметь простое объяснение, понятное для любого программиста. Только здесь такого объяснения нет. По этой причине я рекомендую просто полностью скопировать чей-то проверенный код управления питанием. Если драйвер строится на базе библиотеки GENERIC.SYS, обработку IRP_MJ_POWER можно делегировать следующим образом:
NTSTATUS DispatchPower(PDEVICE__OBJECT fdo, PIRP Irp)
{
PDEVICEJXTENSION pdx -
(PDEVICEJXTENSION) fdo->Dev1ceExtension;
return GenericD1spatchPower(pdx->pgx, Irp);
Библиотека GENERIC выполняет обратный вызов функций драйвера для решения некоторых проблем, специфических для устройства. Функции обратного вызова управления питанием (ни одна из которых обязательной не является) перечислены в табл. 8.3.
Таблица 8.3. Функции обратного вызова, входящие в библиотеку GENERIC.SYS
Функция	Назначение
FlushPendinglo	Форсирует завершение всех незаконченных операций
GetDevicePowerState	Получает состояние устройства для заданного состояния энергопотребления системы
QueryPowerChange	Определяет, допустимо ли предлагаемое изменение состояния энергопотребления устройства
RestoreDeviceContext	Инициирует неблокируемый процесс для подготовки устройства к использованию после восстановления питания
SaveDeviceContext	Инициирует неблокируемый процесс подготовки устройства к отключению питания
Управление переходами
407
ОТЛАДКА КОДА УПРАВЛЕНИЯ ПИТАНИЕМ-------------------------------------------------------
Отладка кода управления питанием в драйвере — дело на редкость сложное. Проблемы начинаются с того факта, что многие настольные системы не обладают полноценной поддержкой состояний с пониженным энергопотреблением. Причину понять несложно: многие программисты драйверов не смогли успешно реализовать управление питанием (это оказалось слишком сложно!), их драйверы работают на тестовом компьютере и мешают отладке. Более того, не только компьютер не поддерживает нужные состояния, но и операционная система без лишних вопросов удаляет команды управления питанием из меню Пуск. Невозможно определить, почему ваш компьютер не поддерживает спящий или ждущий режим — не поддерживает, и все.
Только для Windows 98/Ме компания Microsoft выпустила программу PMTSHOOT, которая помогает определить, почему машина не поддерживает ждущий режим. Запустив ее на одном из своих компьютеров, я обнаружил, что мне нужно отключить сетевые карты (интересно, как они получили логотип Microsoft, если их драйверы не обеспечивают корректного управления питанием?) и запретить совместное использование подключения к Интернету. Вот так! Видимо, я должен объявить всем пользователям моей сети «домашнего офиса» (хотя обычно это я один), что Интернет будет временно недоступен. Это один из тех случаев, когда автоматизация вторгается в области, которые лучше было бы оставить людям.
Возможно, для серьезного тестирования вам придется приобрести современный портативный компьютер. Правда, для карт PCI эта альтернатива выглядит сомнительно, потому что на портативных компьютерах обычно нет слотов расширения, а при подключении к базе (docking station) управление питанием либо работает иначе, либо не работает вовсе. Следовательно, возможности расширения, предоставляемые базовой станцией, тоже могут не отвечать вашим потребностям. Контроллеры USB тоже сильно отличаются по уровню поддержки управления питанием, поэтому подобрать портативный компьютер для тестирования устройств USB окажется нелегко.
Но, допустим, вам все же удалось найти подходящее оборудование для тестирования, после этого начинаются проблемы, связанные с самим процессом отладки драйвера. На моем предыдущем портативном компьютере последовательный порт был недоступен для WinDbg и Softlce — похоже, из-за функций управления питанием (!) инфракрасного порта, который мне никогда не был нужен и никогда не использовался. Каждый раз, когда в моем драйвере возникали проблемы, выяснялось, что последовательный порт, сетевой адаптер или экран уже отключились и я не могу использовать стандартные приемы отладки для анализа сбоев. Запись сообщений в журнальные файлы также была бесполезной из-за того, что файловые системы решали отключиться в самый неподходящий момент. Все сбои с этой точки зрения выглядели одинаково: система зависает при переходе в ждущий режим или при последующем восстановлении — абсолютно черный экран. А теперь попробуйте-ка сделать выводы!
(Хотя дисковая система может быть отключена в те моменты, когда вам потребуется сохранить отладочные образы, связанные с управлением питанием, фильтрующий драйвер POWTRACE в прилагаемых материалах запишет информацию в файл на диске. О том, как им пользоваться, рассказано в документе POWTRACE.HTM.)
В конечном счете я обнаружил довольно неудобную процедуру, которая все же позволяет мне отлаживать код управления питанием в моих драйверах. Благодаря использованию Softlce я не слишком сильно завишу от работы большого количества периферийных устройств. Похоже, клавиатура и параллельный порт сохраняют питание дольше, чем мои устройства, — не знаю, было ли это сделано намеренно или случайно, но я благодарен за то, что это так. Поэтому я жду зависания системы и нажимаю клавишу Print Screen. Если повезет, Softlce получит управление и я смогу использовать принтер как заменитель экрана. Я ввожу команду и снова нажимаю Print Screen, чтобы просмотреть результат... и т. д. Утомительно, но все же лучше, чем метод проб и ошибок.
Гораздо лучшим решением всей проблемы тестирования и отладки кода управления питанием могла бы стать тестовая программа, пересылающая IRP драйверу для выполнения тестирования по принципу «черного ящика». Я уже давно пробиваю в Microsoft идею написания такой программы и надеюсь, что моя настойчивость в конечном счете принесет результат.
408
Глава 8. Управление питанием
Если вы с помощью мастера WDMWIZ сгенерировали скелет драйвера, не использующий GENERIC, то в полученном исходном файле (POWER.CPP) будет реализована та же модель управления питанием, что и в GENERIC. Однако вместо функций обратного вызова этот код будет содержать несколько блоков «if (FALSE)» с комментариями TODO в местах вставки кода, заменяющего функции обратного вызова GENERIC.
В следующих разделах я опишу требования к обработке запросов IRPJMN_ QUERY_POWER и IRPMN_SET_POWER, а также конечный автомат, построенный мной для реализации этих требований. Я не буду приводить весь код, но постараюсь описать все нюансы, необходимые для понимания чужого кода. Если вы хотите реализовать собственный код управления питанием, вам не обязательно использовать конечный автомат, как это сделал я, — например, вместо этого можно использовать традиционный набор функций завершения. Однако, на мой взгляд, вам придется организовать функции завершения таким образом, что полученная структура будет почти эквивалентна конечному автомату, который я опишу.
Необходимая инфраструктура
Любая сторона, непосредственно занимающаяся обработкой запросов питания в драйвере, должна поддерживать значения нескольких полей данных в структуре расширения устройства:
typedef struct _DEVICE_EXTENSION {
DEVICE_POWER_STATE devpower;	//	1
SYSTEMJOWERJTATE syspower:	//	2
BOOLEAN StalledForPower;	//	3
} DEVICE-EXTENSION, *PDEVICE_tXTENSION:
1.	Поле devpower содержит текущее состояние энергопотребления устройства. Обычно оно инициализируется значением PowerDeviceDO во время выполнения AddDevice.
2.	Поле syspower содержит текущее состояние энергопотребления системы. Оно всегда инициализируется значением PowerSystemWorking во время выполнения AddDevice, потому что компьютер заведомо находится в состоянии Working (так как он выполняет команды).
3.	Поле StaKedForPower равно TRUE, если код управления питанием приостановил содержательные запросы IRP на время низкого энергопотребления.
Учтите, что библиотека GENERIC.SYS, если вы захотите ее использовать, хранит эти переменные в приватной части структуры расширения устройства, объявлять их самостоятельно вам не придется.
Исходное разделение запросов
Диспетчерская функция IRP_MJ_POWER должна различать запросы IRP_MN_QUERY_ POWER и IRP_MN_SET_POWER, с одной стороны, и другие дополнительные коды функций — с другой. Практически во всех случаях вы будете приостанавливать
Управление переходами
409
запросы QUERY и SET, вызывая loMarklrpPending и возвращая STATUS-PENDING. Так соблюдается упоминавшееся ранее правило о невозможности блокирования системного потока, в котором принимаются IRP управления питанием. Кроме того, также желательно проанализировать параметры стека, чтобы различить три основных случая:
О Системные IRP управления питанием, повышающие энергопотребление системы, то есть IRP, для которых поле Parameters.Power.Type равно SystemPowerState, а числовое значение Parameters.Power.State.SystemState меньше сохраненного значения syspower. Обратите внимание: увеличение значения SYSTEM_ POWER_STATE соответствует снижению энергопотребления.
О Системные IRP управления питанием, снижающие энергопотребление системы или оставляющие его на прежнем уровне.
О IRP управления питанием устройств, то есть IRP, для которых поле Parameters. Power.Type равно DevicePowerState независимо от повышения или снижения питания по отношению к текущему уровню.
До определенного момента операции SET и QUERY обрабатываются практически одинаково, поэтому сразу различать их не обязательно.
Поскольку я собираюсь привести диаграммы состояний своего конечного автомата, я должен объяснить ряд терминов. Код конечного автомата помещен в функцию HandlePowerEvent, аргументами которой являются контекстная структура, содержащая всю информацию о состоянии автомата, и код события, указывающий, что стало причиной вызова конечного автомата. Определены только три других кода событий. Диспетчерская функция использует код Newlrp при первом вызове конечного автомата. Код MainlrpComplete означает, что функция завершения ввода/вывода вызывает конечный автомат для возобновления обработки после того, как драйвер нижнего уровня завершил IRP, a AsyncNotify — что завершился другой асинхронный процесс. Автомат выполняет одно из действий на основании состояния и кода события. Изначально он находится в исходном состоянии Initialstate, например, при вызове для обработки события Newlrp выполняется действие TriageNewIrp. Другие состояния и действия будут описаны по мере того, как они будут встречаться нам в описании.
Системные IRP, повышающие энергопотребление
Если системный IRP управления питанием подразумевает повышение энергопотребления, он немедленно пересылается следующему драйверу нижнего уровня после установки функции завершения. В функции завершения запрашивается соответствующий IRP устройства. Возвращаемое значение функции завершения выбирается следующим образом:
О В Windows 2000 и выше при обработке системного IRP IRP_MN_SET_POWER для состояния PowerSystemWorking возвращается код STATUS-SUCCESS, чтобы Power Manager мог немедленно отправлять аналогичные IRP другим драйверам. Перекрытие обработки IRP устройств ускоряет запуск системы после приостановки.
410
Глава 8. Управление питанием
О Во всех остальных случаях возвращается код STATUS_MORE_PROCESSING_RE-QUIRED, откладывающий завершение системного IRP. Оно состоится после того, как будет завершен IRP устройства.
На рис. 8.5 показана последовательность перемещения IRP по всем драйверам.
Системный IRP управления питанием
Рис. 8.5. Передача IRP при повышении энергопотребления системы
На рис. 8.6 изображена диаграмма работы моего конечного автомата.
О TriageNewIrp, как упоминалось ранее, является первым действием для каждого IRP управления питанием. Оно обнаруживает, что мы имеем дело с системным IRP, повышающим энергопотребление, и обеспечивает дальнейшее выполнение действия ForwardMainlrp.
О ForwardMainlrp устанавливает функцию завершения ввода/вывода и отправляет системный IRP вниз по стеку драйверов после перевода автомата в состояние SysPowerUpPending. В этой точке функция конечного автомата возвращает управление диспетчерской функции IRP_MJ_POWER, возвращающей код STATUS„PENDING.
О Когда драйвер шины завершает обработку системного IRP, функция завершения ввода/вывода заново вызывает конечный автомат с кодом события Main-IrpComplete.
О Действие SysPowerllpComplete сначала проверяет код завершения IRP. Если драйвер шипы отклонил IRP, мы организуем возврат STATUS_SUCCESS функцией завершения. Это позволяет IRP завершиться с кодом ошибки. Кроме того.
Управление переходами
411
в одной из точек программы мы проверяем, обрабатывается ли запрос IRP_MN_ SET_POWER для состояния PowerSystemWorking в Windows 2000 и выше. Если проверка дает положительный результат, мы также разрешаем IRP завершиться.
Рис. 8.6. Переходы между состояниями при повышении энергопотребления системы
Э Если системный IRP не был отклонен драйвером шины, мы переходим к выполнению действия SelectDState с целью выбора состояния энергопотребления устройства, соответствующего состоянию энергопотребления системы данного IRP, и выполнению действия SendDevicelrp для запроса IRP устройства с тем же дополнительным кодом функции. Вскоре мы подробно рассмотрим механику выполнения обоих действий. Этап выполнения SendDevicelrp может
412
Глава 8. Управление питанием
завершиться неудачей, в этом случае мы хотим заменить статус системного IRP кодом ошибки и разрешить IRP завершиться. Затем мы выходим из конечного автомата, а функция завершения возвращает тот код состояния (STATUS-SUCCESS или STATUS_MORE_PROCESSING_REQUIRED), который был выбран после перевода конечного автомата в состояние SubPowerUpPending.
О В течение некоторого времени наш драйвер обрабатывает только что запрошенный IRP устройства. Наконец, Power Manager вызывает функцию обратного вызова в драйвере, информируя нас о завершении IRP устройства. В свою очередь, функция обратного вызова снова вызывает конечный автомат с кодом события AsyncNotify.
О Действие SubPowerUpComplete в окончательной (не отладочной) версии моего конечного автомата не делает ничего, кроме цепочечной активизации события CompleteMainlrp.
О Событие CompleteMainlrp обеспечивает завершение системного IRP, если это еще не было сделано при выполнении SysPowerUpComplete. Поскольку конечный автомат на этот раз вызывается асинхронным событием (вместо функции завершения ввода/вывода), нам приходится вызывать loCompleteRequest. Тем не менее, выполнение может происходить на уровне DISPATCH-LEVEL. В Windows 98/Ме при завершении IRP управления питанием необходимо находиться на уровне PASSIVE-LEVEL, возможно, из-за этого нам придется запланировать рабочий элемент (см. главу 14) и вернуть управление без уничтожения конечного автомата. Функция обратного вызова рабочего элемента снова активизирует конечный автомат на уровне PASSIVE-LEVEL, чтобы довести дело до конца.
О DestroyContext — последнее действие, выполняемое конечным автоматом для любого IRP управления питанием. Оно освобождает структуру контекста, используемую для отслеживания информации состояния.
Получается, что общий результат всей этой процедуры сводится к запросу IRP управления питанием устройства. Не хочу отрицательно высказываться об архитектуре управления питанием в ядре, потому что мне определенно не хватает энциклопедических познаний относительно обеспечиваемых ею потребностей и решаемых проблем. И все же происходящее выглядит излишне сложно для столь тривиального результата.
Отображение состояния системы на состояние устройства Владелец политики энергопотребления устройства должен инициировать IRP управления питанием устройства (SET или QUERY) с указанием соответствующего состояния устройства. Я разбил происходящее на два этапа: SelectDState и Send-Devicelrp. Начнем с обсуждения первого этапа.
В общем случае устройство всегда должно находиться в состоянии минимального энергопотребления, отвечающего режиму текущей работы устройства, функциям пробуждения (если они есть), возможностям устройства и предстоящим изменениям в состоянии системы. Все эти факторы могут взаимодействовать между собой достаточно сложным образом. Но чтобы в полной мере объяснить
Управление переходами
413
их, я должен ненадолго отклониться от темы и начать с обсуждения PnP IRP, о котором ничего не говорилось в главе 6: IRP_MN_QUERY_ CAPABILITIES.
РпР Manager отправляет этот запрос вскоре после запуска устройства (а также в другие моменты). В качестве параметра запроса передается структура DEVICE_ CAPABILITIES, которая состоит из нескольких полей, относящихся к управлению питанием. Поскольку в других местах книги эта структура не упоминается, я приведу ее полное объявление:
typedef struct _DEVICE_CAPABILITIES {
USHORT Size;
USHORT Version;
ULONG DeviceDl;!;
ULONG DeviceD2:l;
ULONG LockSupported:l;
ULONG EjectSupported:l;
ULONG Removable:!;
ULONG DockDevice:l;
ULONG UniquelD:!;
ULONG Silentlnstall:1;
ULONG RawDeviceOK:!;
ULONG SurpriseRemovalOK:l;
ULONG WakeFromDO:l:
ULONG WakeFromDl:l:
ULONG WakeFromD2:l:
ULONG WakeFromD3:l;
ULONG HardwareDisabled:l;
ULONG NonDynamic:1;
ULONG Reserved:16;
ULONG Address;
ULONG UINumber;
DEVICE_POWER_STATE DeviceStateEPowerSystemMaxImum];
SYSTEM_POWER_STATE SystemWake;
DEVICE_POWER_STATE DeviceWake:
ULONG DiLatency;
ULONG D2Latency:
ULONG D3Latency:
} DEVICE-CAPABILITIES, *PDEVICE_CAPABILITIES;
В табл. 8.4 описаны поля структуры, связанные с управлением питанием.
Таблица 8.4. Поля управления питанием в структуре DEVICE-CAPABILITIES
Поле	Описание
DeviceState	Массив максимальных состояний энергопотребления устройства, возможных для каждого состояния системы
SystemWake	Минимальное состояние энергопотребления системы, в котором устройство может выдать сигнал пробуждения системы, — значение PowerSystemllnspecified означает, что устройство не может пробудить систему
продолжение
414
Глава 8. Управление питанием
Таблица 8.4 (продолжение)
Поле	Описание
DeviceWake	Минимальное состояние энергопотребления, в котором устройство может выдать сигнал пробуждения, — значение PowerSystemUnspecified означает, что устройство не может выдать пробуждающий сигнал
DI Latency	Приблизительная худшая оценка (в 100-микросекундных интервалах) времени, необходимого для переключения устройства из состояния D1 в DO
D2Latency	Приблизительная худшая оценка (в 100-микросекундных интервалах) времени, необходимого для переключения устройства из состояния D2 в DO
D3Latency	Приблизительная худшая оценка (в 100-микросекундных интервалах) времени, необходимого для переключения устройства из состояния D3 в DO
WakeFromDO	Флаг, означающий, работает ли функция пробуждения при нахождении устройства в состоянии DO
WakeFromDl	Флаг, означающий, работает ли функция пробуждения при нахождении устройства в состоянии D1
WakeFromD2	Флаг, означающий, работает ли функция пробуждения при нахождении устройства в состоянии D2
WakeFromD3	Флаг, означающий, работает ли функция пробуждения при нахождении устройства в состоянии D3
Обычно IRP, запрашивающие информацию о возможностях устройств, обрабатываются синхронно — драйвер передает пх вниз и ждет, когда они будут обработаны нижними уровнями. После передачи можно внести любые необходимые изменения в возможности, зарегистрированные драйвером шины. Таким образом, диспетчерская подфункция будет выглядеть так:
NTSTATUS HandleQueryCapabilitiesUN PDEVICE_OBJECT fdo,
IN PIRP Irp)
{
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp):
PDEVICEJXTENSION pdx =
(PDEVIСE_EXTENSION) fdo->Dev i ceExtens1 on;
PDEVICE_CAPABILITIES pdc = stack->
Pa rameters.Devi ceCapabi1i 11es.Capabi1i t i es:
If (pdc->Version < 1)	//1
return DefaultPnpHandler(fdo. Irp);
<и т.д.>	И 2
NTSTATUS status = ForwardAndWaiUfdo, Irp):
if (NT_SUCCESS(status))
{
stack = loGetCurrentlrpStackLocation(Irp):
pdc = stack->Parameters.Devicecapabilities.Capabilities:
<и т.д>	И	3
AdjustDeviceCapabil1ties(pdx. pdc):	//	4
pdx->devcaps = *pdc:	//	5
}
return Comp]eteRequest(Irp, status);
Управление переходами
415
1.	В структуре, описывающей возможности устройства, присутствует поле номера версии, сейчас оно всегда равно 1. Структура спроектирована в расчете на сохранение совместимости в будущем, поэтому вы сможете работать как с версией, определенной в DDK, которая использовалась при построении драйвера, так и со всеми ее будущими воплощениями. Но если вы все же столкнетесь со старой структурой, которая не может быть обработана вашим драйвером, просто проигнорируйте IRP и передайте его дальше.
2.	Как сказано в DDK, здесь следует добавлять описания возможности.
3.	Как сказано в DDK, здесь следует удалять возможности, возвращенные драйвером шины.
4.	В зависимости от платформы, на которой работает драйвер, и типа шины, к которой подключено устройство, может оказаться, что драйвер шины некорректно заполнил поля структуры, относящиеся к управлению питанием. Функция AdjustDeviceCapabilities исправляет этот недостаток.
5.	Структуру возможностей желательно скопировать. Отображение DeviceState будет использоваться при получении системного IRP управления питанием. Возможно, в отдельных случаях вам также потребуется обратиться и к другим полям этой структуры.
К сожалению, различия между «добавлением» и «удалением» возможностей пока не совсем ясны, а вследствие ошибок программирования некоторые драйверы шины отменяют изменения, внесенные в структуру возможностей по мере перемещения IRP по стеку. Соответственно, я рекомендую вносить изменения в обеих точках (2 и 3) предыдущего фрагмента, то есть как до, так и после передачи IRP вниз по стеку.
Вы можете изменить поля SystemWake и DeviceWake и указать в них более высокое состояние энергопотребления, чем рекомендует драйвер шины. Задать в полях пробуждения более низкое состояние энергопотребления нельзя, кроме того, нельзя переопределить решение драйвера шины относительно того, что устройство не может использоваться для пробуждения системы. Если устройство соответствует спецификации ACPI, фильтр ACPI автоматически устанавливает флаги LockSupported, EjectSupported и Removable на основании описания устройства на языке ASL (ACPI Source Language) — вам не придется беспокоиться об этих возможностях.
Возможно, вы также предпочтете установить флаг SurpriseRemovalOK. Установка флага подавляет диалоговое окно, которое появлялось в предыдущих версиях Windows при неожиданном удалении устройства. Как правило, устройства USB или 1394 могут отключаться без предварительного оповещения системы, и функциональные драйверы устанавливают этот флаг, чтобы не раздражать пользователя.
Как упоминалось ранее (см. пункт 4), некоторые драйверы шин неверно заполняют поля управления питанием в структуре возможностей устройства, поэтому вы можете внести в нее некоторые изменения. Моя функция AdjustDeviceCapabilities,
416
Глава 8. Управление питанием
написанная на основе доклада на конференции WinHEC 2002 и примера TOASTER из DDK, делает следующее:
О Прежде всего анализируется отображение DeviceState в структуре возможностей. Если в нем присутствуют состояния D1 или D2, можно сделать вывод, что флаги DeviceDl и DeviceD2 должны быть установлены.
О На основании полученного значения DeviceWake и значения DeviceState, соответствующего полученному значению SystemWake, определяются значения флагов WakeFromDx и флагов DeviceDl/DeviceD2.
О Значение SystemWake определяется на основании того факта, что некоторая запись в отображении DeviceState должна разрешить устройству находиться по крайней мере на одном уровне энергопотребления с минимальным D-состоянием, в котором возможно пробуждение.
Вернемся к обсуждению выбора состояния энергопотребления. GENERIC вычисляет минимальное и максимальное значения и возвращает меньшее из них. Минимальным значением должно быть D3, если только вы не включили функцию пробуждения, а система находится в состоянии, в котором она может быть пробуждена вашим устройством, в этом случае минимумом будет сохраненное значение DeviceWake. Максимумом же является сохраненное значение DeviceState для текущего состояния системы. Затем GENERIC вызывает функцию обратного вызова GetDevicePowerState, если она имеется, чтобы вы могли переопределить результат, выбрав более высокое состояние. Допустим, вы можете решить переводить устройство в состояние D0 при восстановлении питания системы, но только в том случае, если приложение имеет открытый манипулятор устройства:
DEVICE_POWER_STATE GetDev1cePowerState(PDEVICE_0BJECT fdo.
SYSTEM_POWER_STATE sstate, DEVICE_POWER__STATE dstate) {
PDEVICE_EXTENSION pdx -
(PDEVICEJXTENSION) fdo~>DeviceExtension;
if (sstate > SystemPowerWorking !pdx->handles) return dstate:
return PowerDeviceDO;
}
Отправка IRP устройства
Чтобы подать запрос IRP_MN_SET_POWER или IRP_MN_QUERY_POWER, вызовите следующую специальную функцию:
PSOMETHING_OR_ANOTHER ctx:
POWER_STATE devstate;
devstate.DeviceState = PowerDeviceDx;
NTSTATUS postatus =
PoRequestPowerIrp(pdx->Pdo, IRP_MN_XXX_POWER, devstate, (PREQUEST_POWER_COMPLETE) PoCalIbackRoutine, ctx. NULL):
Управление переходами
417
Первый аргумент PoRequestPowerlrp содержит адрес объекта физического устройства (PDO), находящегося в нижней позиции стека РпР устройства. Во втором аргументе передается дополнительный код функции для IRP, который мы хотим отправить. Сейчас достаточно знать, что он совпадает с дополнительным кодом функции системного IRP, который обрабатывается в данный момент (то есть IRP_MN_QUERY_POWER или IRP_MN_SET_POWER). Третий аргумент содержит состояние энергопотребления, вычисленное по правилам, упоминавшимся в предыдущем разделе. PoCallbackRoutine — функция обратного вызова (а не стандартная функция завершения ввода/вывода), a ctx — параметр контекста этой функции. Последний аргумент (NULL в данном примере) содержит адрес переменной, в которой PoRequestPowerlrp будет хранить адрес созданного IRP. Эта конкретная возможность не используется для запросов SET и QUERY.
Функция PoRequestPowerlrp создает IRP управления питанием устройства с заданным типом и заданным уровнем энергопотребления и отправляет его верхнему драйверу в стеке РпР. Если функция PoRequestPowerlrp возвращает управление с кодом STATUS_PENDING, можно сделать вывод, что она действительно создала и отправила IRP. Далее Power Manager в конечном итоге вызовет вашу функцию завершения. Но если PoRequestPowerlrp вернет что-либо, кроме STATUS__ PENDING, то Power Manager не будет вызывать функцию обратного вызова. Именно из-за этой вероятности действие SendDevicelrp в моем конечном автомате может передать управление действию CompleteMainlrp для завершения системного IRP.
Не следует запрашивать IRP устройства, если устройство уже находится в запрашиваемом состоянии. В Windows 98/Ме существовала ошибка, из-за которой казалось, что вызов PoRequestPowerlrp в такой ситуации завершился успехом, но в действительности драйвер CONFIGMG не отправляет NTKERN событие конфигурации. Код управления питанием входит во взаимную блокировку, ожидая вызова функции PoCallbackRoutine, который не наступит никогда. На моем опыте системы Windows 2000 и Windows ХР зависали при выходе из ждущего режима, если запросить текущее состояние.
Системные IRP, снижающие энергопотребление
Если системный IRP управления питанием не связан с изменением или снижением уровня энергопотребления системы, запрашивается IRP устройства с тем же дополнительным кодом функции (SET или QUERY) и состоянием устройства, соответствующим текущему состоянию системы. При завершении IRP устройства системный IRP передается следующему драйверу нижнего уровня. Для системного IRP потребуется функция завершения, чтобы вы могли сделать необходимый вызов PoStartNextPowerirp и выполнить дополнительную зачистку. На рис. 8.7 показано, как в этом случае происходит перемещение IRP в системе. Вообще говоря, выдача аппаратно-зависимого запроса управления питанием в процессе перемещения системного IRP вниз по стеку не является строго необходимой. Другими словами, запрос можно выдать из функции завершения ввода/вывода для системного IRP управления питанием, как это было сделано
418
Глава 8. Управление питанием
в случае системных IRP, повышающих уровень энергопотребления (см. ранее). Более того, в DDK рекомендуется поступать именно так. Однако у выполнения действий в предлагаемом мной порядке есть одно достоинство: с момента публикации первого издания книги этот путь был проверен и подтвердил свою работоспособность для многих драйверов на множестве платформ WDM. По этой причине я буду следовать житейскому правилу: «Не чините то, что не сломалось».
Системный IRP управления питанием
Рис. 8.7. Передача IRP при снижении энергопотребления системы
На рис. 8.8 показано, как мой конечный автомат обрабатывает подобные IRP. TriageNewIrp помещает конечный автомат в состояние SubPowerDownPending и переходит к действию SelectDState. Ранее уже было показано, что SelectDState выбирает состояние энергопотребления устройства и ведет к действию SendDevicelrp, выдающему IRP устройства. В сценарии со снижением энергопотребления системы в этом IRP устройства будет указываться более низкий уровень питания.
IRP устройства
Фактически, наша обработка системных IRP управления питанием сводится к их передаче и запросу IRP устройства, когда системный IRP перемещается вниз или вверх по стеку. С IRP устройств потребуется проделать более основательную работу.
Управление переходами
419
SelectDState
Событие
---к Нормальный переход
----> Состояние ошибки
- - - - ► Асинхронное событие
Рис. 8.8. Переходы между состояниями при снижении энергопотребления системы
Прежде всего, устройства не должны быть заняты продолжительными операциями ввода/вывода во время смены состояния питания. Следовательно, в последовательности, ведущей к снижению энергопотребления устройства, необходимо как можно раньше дождаться завершения незаконченных операций и прекратить обработку новых операций. Поскольку блокировка системного потока, в котором принимаются IRP управления питанием, запрещена, потребуется асинхронный механизм. После завершения текущего IRP мы продолжим обработку IRP устройства. Таким образом, все четыре диаграммы состояний
420
Глава 8. Управление питанием
(рис. 8.11-8.14) начинаются с одной последовательности. TriageNewIrp по флагу StalledForPower проверяет, были ли очереди содержательных IRP уже приостановлены для выполнения операции управления питанием, и если нет, выполняет две операции:
О Вызов функции DEVQUEUE с именем Sta ПАН Req uestsAnd Notify. Функция приостанавливает все очереди содержательных IRP и возвращает признак того, занято ли устройство в данный момент обслуживанием одного из них. В последнем случае GENERIC откладывает дальнейшую обработку IRP до того, когда функция StartNextPacket не будет вызвана для каждой очереди, занятой в настоящий момент.
О Вызов функции FlushPendinglo (если она была задана). Эта функция решает проблему, на которую указал один из читателей первого издания, а именно: функция Startlo могла начать продолжительную операцию, которая не завершится сама по себе. Допустим, вы перепаковали запрос IRP в виде IRP_MJ_ INTERNAL_DEVICE_CONTROL и отправили его драйверу шины USB, далее вы планируете вызвать StartNextPacket из функции завершения, когда устройство USB завершит перепакованный IRP. Это может произойти недостаточно быстро, если функция обратного вызова не окажет «содействия» (которым в данном случае может быть команда отмены).
Если IRP устройства подразумевает повышение уровня питания устройства, мы пересылаем его следующему драйверу нижнего уровня. Схема перемещения IRP в системе показана на рис. 8.9. Драйвер шины обрабатывает SET-IRP устройства, например, используя специфические механизмы шины, он направляет поток электронов вашему устройству, и это приводит к завершению IRP. Функция завершения инициирует операции, необходимые для восстановления контекстной информации устройства, и возвращает STATUS_MORE_PROCESSING_REQUIRED, чтобы прервать процесс завершения для IRP устройства. После завершения операции восстановления контекста возобновляется обработка содержательных IRP и завершается IRP устройства.
Если IRP устройства не подразумевает изменения или снижения уровня энергопотребления устройства, мы сначала выполняем обработку, специфическую для устройства (асинхронно, как объяснялось ранее), а затем пересылаем IRP устройства следующему драйверу нижнего уровня (рис. 8.10). «Специфическая обработка» для операции QUERY означает приостановку очередей и ожидание завершения всех текущих содержательных IRP. Для операции SET специфическая обработка включает сохранение контекстной информации устройства (если она есть) в памяти для последующего восстановления. Драйвер шины завершает запрос. В случае операции QUERY можно ожидать, что драйвер шины завершит запрос с кодом STATUS-SUCCESS, обозначая тем самым согласие на предложенное изменение питания. Для операций SET можно ожидать, что драйвер шины предпримет действия, зависящие от шины, и переведет устройство в заданное состояние энергопотребления. Ваша функция завершения производит зачистку, среди прочего вызывая PoStartNextPowerirp.
421
Управление переходами
IRP управления питанием устройства
Рис. 8.9. Передача IRP при повышении энергопотребления устройства
1RP управления питанием устройства
Рис. 8.10. Передача IRP при снижении энергопотребления устройства
422
Глава 8. Управление питанием
Переход к состоянию с более высоким энергопотреблением устройства
На рис. 8.11 изображена диаграмма переходов состояний для события IRP_MN_ SET_POWER, переводящего устройство в состояние более высокого энергопотреб-ления по сравнению с текущим.
Рис. 8.11. Переходы между состояниями при повышении энергопотребления устройства (запрос SET)
Управление переходами
423
В этом процессе задействованы следующие переходы и действия:
О TriageNewIrp проверяет, что все очереди содержательных IRP приостановлены. QueueStal(Complete возобновляет обработку IRP устройства после решения этой задачи.
О ForwardMainlrp отправляет IRP устройства вниз по стеку РпР. Драйвер шины включает подачу тока устройству и завершает IRP.
О Когда IRP устройства завершится, наша функция завершения заново обращается к конечному автомату для выполнения действия DevPowerUpComplete. Если IRP устройства будет отклонен (кстати, я еще ни разу не видел, чтобы это случилось), мы выходим через CompleteMainlrp.
О В этой точке GENERIC вызывает функцию обратного вызова RestoreDevice-Context (если она есть), чтобы дать возможность инициировать неблокируемый процесс с целью подготовки устройства к работе в новом состоянии с более высоким энергопотреблением. Вскоре этот аспект будет описан более подробно.
О После завершения операции восстановления контекста (или немедленно при отсутствии RestoreDeviceContext) функция ContextRestoreComplete отменяет приостановку очередей содержательных IRP (которые, как предполагается, были приостановлены при отключении питания) и передает управление CompleteMainlrp.
О CompleteMainlrp обеспечивает завершение IRP устройства. Иногда мы попадаем к этому действию из функции завершения, установленной для IRP устройства, в этом случае для продолжения процесса завершения достаточно вернуть STATUS_SUCCESS. В других случаях функция завершения давно вернула STATUS_MORE_PROCESSING_REQUIRED, и нам нужно вызвать loCompleteRequest для возобновления процесса. В любом случае, так как мы обычно обрабатываем IRP устройства, сгенерированный во время обработки системного IRP управления питанием, далее Power Manager вызовет функцию PoCompleteRoutine, чтобы сообщить о полном завершении IRP устройства. Далее экземпляр конечного автомата уничтожается, и другой (более ранний) экземпляр продолжает свою обработку системного IRP.
Функция обратного вызова RestoreDeviceContext занимает важное место в организации управления питанием устройства в библиотеке GENERIC. Как я уже говорил, эта функция дает возможность инициировать любой неблокируемый процесс, который необходимо выполнить перед тем, как устройство будет готово работать в новом состоянии с более высоким энергопотреблением. Когда GENERIC вызывает эту функцию, драйвер шины уже восстановил питание устройства. Основной код функции выглядит так:
VOID RestoreDeviceContext(PDEVICE_OBJECT fdo,
DEVICE__POWER__STATE oldstate, DEVICE_POWER_S7ATE newstate, PVOID context) {
424
Глава 8. Управление питанием
Параметры oldstate и newstate определяют предыдущее и новое состояния устройства, a context — закрытый параметр, передаваемый при вызове GenericSave-RestoreComplete. Внутри этой функции выполняются все неблокируемые действия, необходимые для подготовки устройства: чтение и запись аппаратных регистров, вызов других функций ядра, не блокирующих текущий поток, и т. д. Такие операции, как отправка синхронного IRP другому драйверу, невозможны, потому что они требуют блокировки текущего потока до завершения IRP. В то же время стороны, вы можете отправлять асинхронные IRP другим драйверам. Когда устройство будет полностью готово, выполните следующий вызов GENERIC:
Gener1cSaveRestoreComplete(context);
где context — контекст, полученный при вызове RestoreDeviceContext. GENERIC возобновляет обработку IRP устройства, как упоминалось ранее. Если все необходимые операции были закончены, то GenericSaveRestoreComplete можно вызвать из функции RestoreDeviceContext.
ПРИМЕЧАНИЕ---------------------------------------------------------------------------—
Так уж получилось, что мне довелось написать немало драйверов для устройств чтения смарт-карт. Моя функция RestoreDeviceContext для этих драйверов завершает все незавершенные IRP, связанные с отслеживанием отсутствия карты (необходимо для безопасной обработки возможного извлечения карты во время выключения питания). Кроме того, для устройств, требующих непрерывного опроса с целью выявления вставки/извлечения карты, я перезапускаю поток опроса.
Обратите внимание: если на стадии включения питания никакие дополнительные операции не нужны, определять функцию RestoreDeviceContext не обязательно.
Общий результат всех действий по запросу на повышение энергопотребления устройства заключается в том, что драйвер шины подает питание на наше устройство, а мы готовим его к повторному использованию. Работа все равно получается довольно большой, но, по крайней мере, из нее выходит хоть что-то полезное.
Запрос на переход в состояние с более высоким энергопотреблением
Теоретически, драйвер не должен получать запрос IRP_MN_QUERY_POWER для состояния с более высоким энергопотреблением по сравнению с текущим, но если он все же получит его, это не должно привести к сбою системы. На рис. 8.12 показана диаграмма состояний моего конечного автомата в подобных случаях. Автомат просто приостанавливает очереди IRP, если они не были приостановлены ранее при снижении питания.
Переход к более низкому состоянию энергопотребления устройства
При получении запроса IRPJMN_SET_POWER для уровня энергопотребления ниже текущего или равного ему конечный автомат проходит через состояния, показанные на рис. 8.13.
Управление переходами
425
Рис. 8.12. Переходы между состояниями при повышении энергопотребления устройства (запрос QUERY)
В этом процессе задействованы следующие переходы и действия:
О TriageNewIrp проверяет, что все очереди содержательных IRP приостановлены. QueueStallComplete возобновляет обработку IRP устройства после решения этой задачи.
О В этой точке GENERIC вызывает функцию обратного вызова SaveDeviceContext (если она есть), чтобы дать возможность инициировать неблокируемый процесс с целью подготовки устройства к работе в новом состоянии с более низким энергопотреблением. Вскоре этот аспект будет описан более подробно.
О После завершения операции сохранения контекста (или немедленно при отсутствии SaveDeviceContext) функция ContextSaveComplete передает управление ForwardMainlrp.
426
Глава 8. Управление питание*
Рис. 8.13. Переходы между состояниями при снижении энергопотребления устройства (запрос SET)
О ForwardMainlrp отправляет IRP устройства вниз по стеку РпР. Драйвер шины отключает подачу тока устройству и завершает IRP.
О Когда IRP устройства завершится, наша функция завершения заново обращается к конечному автомату для выполнения действия CompieteMainlrp.
О CompieteMainlrp обеспечивает завершение 1RP устройства. Мы всегда попадаем к этому действию из функции завершения, установленной для IRP
Управление переходами
427
устройства, поэтому для продолжения процесса завершения достаточно вернуть код STATUS_SUCCESS. Так как мы обычно обрабатываем IRP устройства, сгенерированный во время обработки системного IRP управления питанием, далее Power Manager вызовет функцию PoCompleteRoutine, чтобы сообщить о полном завершении IRP устройства. Далее экземпляр конечного автомата уничтожается, и другой (более ранний) экземпляр продолжает свою обработку системного IRP.
Протокол сохранения контекста библиотеки GENERIC в точности противоположен протоколу восстановления контекста, о котором говорилось ранее. Если вы определили функцию SaveDeviceContext, GENERIC вызовет ее:
VOID SaveDeviceContext(PDEVICE_OBJECT fdo,
DEVICE_POWER_STATE oldstate,
DEVICE_POWER_STATE newstate, PVOID context)
{
}
Вы инициируете любые неблокируемые операции, необходимые для подготовки устройства к состоянию с пониженным энергопотреблением, и по их завершении вызываете GenericSaveRestoreComplete. После этого GENERIC продолжает обработку IRP устройства так, как было описано ранее. Обратите внимание: на момент передачи управления функции обратного вызова устройство все еще получает питание.
Если на стадии отключения питания никакие дополнительные операции не нужны, определять функцию SaveDeviceContext не обязательно.
ПРИМЕЧАНИЕ-----------------------------------------------------------------------------
Снова о моих драйверах смарт-карт: я использую функцию SaveDeviceContext для остановки всех потоков опроса, которые могут использоваться для выявления вставки/извлечен и я карты. Поскольку эта операция требует блокировки текущего потока до выхода из потока опроса, обычно приходится планировать рабочий элемент, который блокирует другой системный поток, ожидает завершения потока опроса, а затем вызывает GenericSaveRestoreComplete.
Запрос на переход в состояние с более низким энергопотреблением
Запрос IRP_MN_QUERY_POWER с уровнем энергопотребления, более низким или равным текущему, является основным механизмом, при помощи которого функциональный драйвер высказывает свое мнение относительно изменений питания. На рис. 8.14 показано, как подобные запросы обрабатываются моим конечным автоматом:
О TriageNewIrp проверяет, что все очереди содержательных IRP приостановлены. QueueStallComplete возобновляет обработку IRP устройства после решения этой задачи.
О В этой точке (DevQueryDown) GENERIC вызывает функцию обратного вызова QueryPower (если она есть), чтобы вы могли согласиться или отказаться от
428
Глава 8. Управление питанием
предложенного изменения. Если функция возвращает FALSE, GENERIC обходит пару действий, напрямую переходит к DevQueryDownComplete, а затем к CompleteMainlrp.
О ForwardMainlrp отправляет IRP устройства вниз по стеку РпР. Драйвер шины обычно просто завершает IRP с кодом успешного завершения.
о
Состояние
Действие
Нормальный переход
Состояние ошибки
- - - - ► Асинхронное событие
Рис. 8.14. Переходы между состояниями при снижении энергопотребления устройства (запрос QUERY)
Другие аспекты управления питанием
429
О Когда IRP устройства завершится, наша функция завершения заново обращается к конечному автомату для выполнения действия DevQueryDownComplete. Если запрос отклонен, мы снимаем приостановку очередей (на случай, если позднее мы не получим запрос SET, который заставит нас это сделать).
О CompleteMainlrp обеспечивает завершение IRP устройства. Так как мы обычно обрабатываем IRP устройства, сгенерированный во время обработки системного IRP управления питанием, далее Power Manager вызовет функцию PoCompleteRoutine, чтобы сообщить о полном завершении IRP устройства. Далее экземпляр конечного автомата уничтожается, и другой (более ранний) экземпляр продолжает свою обработку системного IRP.
Конечным результатом всех этих действий является приостановка очередей содержательных IRP (в случае, если запрос будет принят).
Другие аспекты управления питанием
В этом разделе описаны дополнительные обстоятельства, относящиеся к управлению питанием, — флаги, устанавливаемые в объекте устройства, управление функцией пробуждения, выдача запросов на снижение энергопотребления при бездействии устройства в течение заданного времени и оптимизация операций восстановления контекста.
Флаги, устанавливаемые функцией AddDevice
Два флаговых бита в объекте устройства (табл. 8.5) контролируют различные аспекты управления питанием. После вызова loCreateDevice в функции AddDevice оба флага устанавливаются равными 0, и вы можете установить их в зависимости от обстоятельств.
Таблица 8.5. Флаги управления питанием в структуре DEVICE-OBJECT
Флаг	Описание
DO_POWER_PAGABLE	Диспетчерская функция IRP_MJ_POWER в драйвере должна
выполняться на уровне PASSIVE-LEVEL
DO POWER INRUSH	Включение питания устройства требует высокого уровня тока
Установите флаг DO_POWER_PAGABLE, если диспетчерская функция для запросов IRP_MJ_POWER должна выполняться на уровне PASSIVE-LEVEL. Название флага объясняется тем, что перемещение кода в памяти (paging) разрешено только на уровне PASSIVE-LEVEL. Если оставить этот флаг равным 0, Power Manager получает возможность отправлять запросы управления питанием на уровне DISPATCH_LEVEL. Более того, в текущей версии Windows ХР всегда следует поступать именно так.
Установите флаг DO_POWER_INRUSH, если устройство не должно включаться одновременно с другими устройствами из-за броска тока. Этот флаг решает житейскую проблему, хорошо известную многим из нас, — при одновременном включении сразу нескольких бытовых устройств может перегореть предохранитель.
430
Глава 8. Управление питанием
Power Manager гарантирует, что в любой момент времени будет включаться только одно устройство, создающее бросок тока. Кроме того, запросы управления питанием таким устройствам посылаются на уровне DISPATCH__LEVEL, это подразумевает, что вы не должны устанавливать флаг DO_POWER_PAGABLE.
Системный фильтрующий драйвер ACPI устанавливает флаг INRUSH в PDO автоматически, если это указано в описании устройства на языке ASL. Для правильной организации последовательного включения системе необходима лишь установка флага INRUSH в каком-либо из объектов устройства в стеке — вам не придется устанавливать флаг и в своем объекте устройства. Но если система не может автоматически определить, что устройство нуждается в особом обращении при включении питания, вам придется установить этот флаг самостоятельно.
Значения флагов PAGABLE и INRUSH должны быть согласованы во всех объектах, связанных с конкретным устройством. Если в PDO установлен флаг PAGABLE, он должен быть установлен также во всех объектах устройств, в противном случае произойдет фатальный сбой с кодом DRIVER_POWER_STATE_FAILURE (устройство PAGABLE может находиться над устройством «не-PAGABLE», но не наоборот). Если в объекте устройства установлен флаг INRUSH, ни он сам, ни объекты нижних уровней не могут устанавливать флаг PAGABLE, иначе произойдет фатальный сбой INTERNAL_POWERJERROR.
Функция пробуждения устройства
Некоторые устройства обладают аппаратной функцией пробуждения, что позволяет им активизировать «спящий» компьютер при возникновении внешнего события (рис. 8.15). Типичным примером такого устройства на современных PC служит кнопка питания. Также можно упомянуть многие модемы и сетевые карты, способные активизироваться по входящим звонкам и пакетам соответственно. Поддержка пробуждения обычно заявляется для устройств USB, и многие концентраторы и хост-контроллеры реализуют сигналы, необходимые для ее обеспечения.
Сетевая карта
Рис. 8.15. Примеры устройств с функцией пробуждения системы
Другие аспекты управления питанием
431
Если ваше устройство обладает функцией пробуждения, на функциональный драйвер возлагаются дополнительные обязанности по управления питанием, помимо уже описанных. Эти дополнительные обязанности связаны с разновидностью IRP_MJ_POWER с кодом IRP_MN)WAIT_WAKE:
О Проанализируйте возможности устройства и определите, способно ли устройство пробуждать систему по мнению драйвера шины и фильтрующего драйвера ACPI (и если может, то в каких обстоятельствах).
О Организуйте постоянное хранение признака, который бы указывал, хочет ли пользователь, чтобы устройство поддерживало функцию пробуждения.
О Когда стек устройства не находится в процессе перехода состояний питания, используйте функцию PoRequestPowerlrp для выдачи запроса IRPJMN J/VAITJ/VAKE (который обычно приостанавливается драйвером шины).
О Если вы можете выбрать состояние, в которое переводится устройство, постарайтесь выбрать состояние с минимальным энергопотреблением, соответствующим возможностям устройства в области пробуждения.
О Если устройство заставляет систему активизироваться из ждущего состояния, драйвер шины завершает запрос WAIT_WAKE. В дальнейшем Power Manager передает управление функции обратного вызова, которая должна выдать SET-запрос на восстановление устройства в состоянии DO.
ПРИМЕЧАНИЕ--------------------------------------------------------------------
Пример WAKEUP в прилагаемых материалах демонстрирует реализацию функциональности пробуждения на базе GENERIC.SYS. Весь код, рассмотренный в этом разделе, содержится в библиотеке GENERIC (также в прилагаемых материалах). Обратите внимание: если воспользоваться мастером WDMWIZ для построения заготовки проекта, не использующего GENERIC, он сгенерирует точно такой же код (но со слегка измененными именами функций).
Контроль со стороны пользователя
В конечном счете решение о том, нужно ли задействовать функцию пробуждения вашего устройства, должно приниматься конечным пользователем. Стандартный способ управления этим решением основан на поддержке класса WMI (Windows Management Instrumentation) с именем MSPower_DeviceWakeEnable (за дополнительной информацией о WMI обращайтесь к главе 10). Если устройство поддерживает MSPower_DeviceWakeEnable или MSPower_DeviceEnable, Диспетчер устройств автоматически включает в диалоговое окно свойств страницу Управление электропитанием (Power Management) (рис. 8.16).
Ваш драйвер должен запомнить текущее состояние поля Enable класса MSPower_ DeviceWakeEnable как в структуре расширения устройства, так и в реестре. Вероятно, переменная в расширении устройства будет инициализироваться на базе данных из реестра при выполнении AddDevice.
Механика WAIT_WAKE
Ваш драйвер как владелец политики энергопотребления для вашего устройства выдает запрос WAIT_WAKE вызовом функции PoRequestPowerlrp:
if (InterlockedExchange(&pdx->wwoutstand1ngf 1))
пропустить остальные команды>
432
Глава 8. Управление питанием
pdx->wwcaпсеlied = 0;
POWERSTATE junk;
junk.SystemState = pdx->devcaps.SystemWake
status = PoRequestPowerIrp(pdx->Pdo, IRP_MN_WAITJWAKE, junk. (PREQUEST POWER COMPLETE) WaitWakeCallback, pdx. &pdx->WaitWakeIrp);if (!NT_SUCCESS(status))
{
pdX'>WakeupEnabled = FALSE;
pdx->wwoutstanding = 0;
}
Рис. 8.16. Вкладка управления питанием в свойствах устройства
PoRequestPowerlrp создает запрос IRP_MJ_POWER с дополнительным кодом IRP_ MI\LWAIT_WAKE и отправляет его верхнему драйверу в стеке РпР (о других командах чуть позже). Этот факт означает, что ваша диспетчерская функция POWER «увидит» IRP в процессе перемещения вниз по стеку. Установите функцию завершения и передайте ее вниз:
IoCopyCurrentIrpStackLocationToNext(Irp);
loSetComplet1onRouti ne(Irp. (PIO_COMPLETION_ROUTINE)
WaitWakeCompletionRoutine. pdx, TRUE, TRUE, TRUE);
PoStartNextPowerlrp(Irp);
Другие аспекты управления питанием
433
status = PoCalIDriver(pdx->LowerDeviceObject, Irp):
return status:
Обычно драйвер шины приостанавливает IRP и возвращает STATUS-PENDING. Этот код состояния проходит наверх через диспетчерские функции всех драйверов в стеке и в конечном итоге вызывает PoRequestPowerlrp для возврата STATUSPENDING. Тем не менее, возможны и другие варианты действий:
О Если отправить более одного запроса WAIT_WAKE, то драйвер шины завершит второй и все последующие запросы с кодом STATUS_DEVICE_BUSY. Другими словами, необработанным может оставаться только один запрос WAIT_WAKE.
О Если устройство уже находится в слишком низком состоянии энергопотребления (другими словами, ниже уровня DeviceWake), драйвер шины завершает WAIT_WAKE с кодом STATUS_INVALID_DEVICE-STATE.
О Если из описания возможностей устройства видно, что устройство вообще не поддерживает функцию пробуждения, драйвер шины завершает WAIT_WAKE с кодом STATUS_NOT_SUPPORTED.
Учтите, что если драйвер шины немедленно отклонит IRP, будет вызвана ваша функция завершения ввода/вывода (WaitWakeCompletionRoutine в предыдущем примере), а функция обратного вызова (WaitWakeCallback) — нет.
Завершение IRP_MN„WAIT_WAKE
В нормальном случае, когда драйвер шины возвращает STATUS-PENDING, вы просто оставляете IRP в покое, ожидая выполнения одного из нескольких условий: О Система переходит в ждущий режим, а позднее «просыпается», потому что устройство выдало сигнал пробуждения. Оборудование компьютера автоматически получает питание. Драйвер шины обнаруживает, что пробуждение произошло по инициативе вашего устройства, и завершает WAIT_WAKE с кодом STATUS-SUCCESS. Позднее ваш драйвер (вместе со всеми) получает запрос SETPOWER для состояния PowerSystemWorking. В ответ вы создаете IRP управления питанием устройства, который будет переводить устройство в соответствующее состояние. Поскольку устройство пытается взаимодействовать с компьютером, вероятно, на этой стадии оно будет переводиться в состояние DO.
Э Система остается в состоянии PowerSystemWorking (или возвращается к нему), но ваше устройство оказывается в состоянии пониженного энергопотребления. Следовательно, устройство инициирует сигнал пробуждения, а драйвер шины завершает WAIT-WAKE. Б этом случае Power Manager не будет посылать системный IRP, потому что система уже находится в работоспособном состоянии, но устройство все еще остается в состоянии низкого энергопотребления. Для правильного разрешения этой ситуации функция обратного вызова (WaitWakeCallback в примере) должна выдать IRP устройства, чтобы перевести устройство (вероятно) в состояние DO.
Э Устройство или система входит в состояние энергопотребления, несовместимое с сигналами пробуждения. Другими словами, устройство переходит в состояние ниже DeviceWake или система переходит в состояние ниже SystemWake.
434
Глава 8. Управление питанием
Драйвер шины осознает, что сигнал пробуждения становится невозможным, и завершает запрос WAIT_WAKE с кодом STATUS_INVALID_DEVICE_STATE.
О Вы сами решаете отказаться от WAIT_WAKE и вызываете loCancellrp. Функция отмены драйвера шины завершает IRP с кодом STATUS_CANCELLED. Запрос WAIT_WAKE должен отменяться (по крайней мере) при обработке запросов IRP_MN_STOP_DEVICE, IRP_MN_SURPRISE-REMOVAL и IRP_MN_REMOVE-DEVICE.
Во всех перечисленных ситуациях I/O Manager вызывает функцию завершения ввода/вывода (WaitWakeCompletionRoutine) как нормальную составляющую завершения IRP. Кроме того, Power Manager передает управление функции обратного вызова (WaitWakeCallback) после полного завершения IRP.
Функция завершения ввода/вывода
При отмене IRP_MN_WAIT_WAKE возникает та же ситуация «гонки» между вашим вызовом loCancellrp и вызовом loCompleteRequest со стороны драйвера шины, которая рассматривалась в главе 5. Для безопасной отмены IRP можно использовать разновидность показанной методики отмены асинхронных IRP. Эта методика основана на атомарной синхронизации логики отмены с функцией завершения:
VOID CancelWaitWake(PDEVICE_EXTENSION pdx)
{
PIRP Irp = (PIRP) InterlockedExchangePointer(
(PVOID*) &pdx->WaitWakeIrp. NULL);
if (Irp)
{
loCancelIrp(Irp);
if (Inter]ockedExchange(&pdx->wwcance]led, 1))
IoCompleteRequest(Irp, IO_NO_INCREMENT);
}
}
NTSTATUS WaitWakeCompletionRoutine(PDEVICE_OBJECT junk, PIRP Irp, PDEVICEJXTENSION pdx) {
if (Irp->Pend1ngReturned) loMarklrpPending(Irp);
if (Inter!ockedExchangePointer(
(PVOID*) &pdx->WaitWakeIrp, NULL))
return STATUS_SUCCESS:
if CInter!ockedExchange(&pdx->wwcancelled, 1))
return STATUS-SUCCESS;
return STATUS-MORE_PROCESSING_REQUIRED;
}
В примере, приведенном в главе 5, мы имели дело с созданными нами же асинхронными IRP. Мы отвечали за вызов loFreelrp для удаления IRP после его завершения. На этот раз Power Manager создает IRP WAIT_WAKE и вызывает свою функцию loFreelrp после его завершения. Таким образом, чтобы предотвратить
аспекты управления питанием
435
мк>» между отменой/завершением, завершение необходимо отложить до пхождения той точки, в которой возможна отмена запроса.
Г Логика отмены частично базируется на недокументированном, но важном Ккяном эффекте внутреннего устройства PoRequestPowerlrp. Вспомните, что К -едний аргумент PoRequestPowerlrp содержит адрес переменной PIRP, в кото-Ь* заносится адрес созданного IRP. PoRequestPowerlrp задает значение этой пе->иной перед тем, как отправлять 1RP драйверу верхнего уровня. В примере Б использовал значение pdx->WaitWakeIrp. Это же поле использовалось в функ-Ьв завершения ввода/вывода и в функции CancelWaitWake. Я намеренно осно-ываюсь на том факте, что PoRequestPowerlrp задаст значение этой переменной тправкой 1RP, а не после нее — это обеспечит правильную работу логики Вершения и отмены даже в том случае, если вызов произойдет до того, как спетчерская функция верхнего уровня вернет управление PoRequestPowerlrp. Х>ратите внимание: было бы неправильно записывать код в следующем виде:
-IRP foo;
status = PoRequestPowerIrp(pdx->Pdo. IRP_MN_WAIT_WAKE. junk.
(PREQUEST_POWER_COMPLETE) WaitWakeCallback. pdx. &foo):
.:ax->WaitWakeIrp = foo, // <== Так нельзя
В этой последовательности возникает риск заполнения WaitWakelrp адресом Завершенного IRP. Если позднее вы попытаетесь отменить этот TRP, возникнет хаос.
Функция обратного вызова
Ваша функция обратного вызова для WAIT_WAKE должна выглядеть примерно так:
VOID lAiaitWakeCallback(PDEVICE_OBJECT junk, UCHAR Mi norFunction, POWER_STATE state, PDEVICEJXTENSION pdx, PIO_STAT(JS_BLOCK pstatus) {
InterlockedExchange(&pdx->woutstanding. 0):
If (!RT_SUCCESS(pstatus->Status)) return;
else { SendDeviceSetPower(pdx, PowerDeviceDO. FALSE); }
}
Допустим, вы написали (или скопировали!) функцию SendDeviceSetPower, выдающую запрос устройства SET_POWER. Я не хочу приводить эту функцию здесь, потому что она должна сочетаться с остальной логикой управления питанием. В GENERIC.SYS и в любом драйвере, построенном с использованием WDMWIZ, присутствует функция с таким именем, поэтому вы можете обратиться к моей реализации этой функции. Вызов SendDeviceSetPower переводит устройство в состояние DO — на случай, если система уже находится в рабочем состоянии при поступлении сигнала на пробуждение. Как упоминалось ранее, ваше устройство
436
Глава 8. Управление питанием
необходимо вывести из спящего режима, потому что в ближайшем будущем вы не получите системный сигнал SET_POWER.
Скромное предложение
Механизм пробуждения системы содержал множество ошибок в разных выпусках платформ WDM, разных шин и чипсетов. Приведу лишь несколько фактов, свидетельствующих о том, что до появления Windows ХР функция пробуждения работала довольно скверно:
О Не так давно я протестировал в компьютерном магазине несколько моделей портативных компьютеров от разных производителей, работавших под управлением Windows ХР Ноте или Windows ХР Professional. Мне не сразу удалось найти модель, которая реально поддерживала функцию пробуждения системы от устройства USB. Эта функция стандартизирована в спецификации USB, поэтому все компьютеры вроде бы должны вести себя одинаково. Тем не менее, пробуждение не только работало всего на одном компьютере, но и перестало работать при переходе с Windows ХР Ноше на Windows ХР Professional. Мой знакомый из разработчиков Microsoft на это сказал что-то вроде: «Гм, такого быть не должно».
О У меня есть старый портативный компьютер на базе АРМ (Advanced Power Management), на котором в результате обновления была установлена система Windows 2000. Если подключить к нему мышь USB и перевести компьютер в ждущий режим, то при попытке восстановления система виснет. Это происходит из-за того, что драйвер концентратора USB заставляет систему перейти в состояние, не поддерживаемое BIOS, чтобы предотвратить некорректную обработку ожидающего сигнала WAIT_WAKE, который все равно работать не будет, потому что контроллер USB на этом компьютере не поддерживает функцию пробуждения. Не существует элементов пользовательского интерфейса, которые позволили бы мне заблокировать выдачу WAIT_WAKE, используемую по умолчанию. В DDK нигде не говорится об отмене незавершенных запросов WAIT_WAKE перед отправкой системного запроса вниз по стеку, однако это единственное действие, которое бы предотвратило неверный выбор ждущего состояния. Конечно, пробуждение запрашивал не мой драйвер — это был драйвер MOUCLASS, созданный крупной фирмой-разработчиком, рассполо-женной близ Сиэтла. Я уверен, что вину следует разделить между разработчиком операционной системы, поставщиком компьютера и производителями различных его компонентов.
О На том же компьютере Windows 98 Second Edition (операционная система, установленная производителем) не зависает при выходе из спящего режима. Вместо этого она (сюрприз!) удаляет драйвер мыши, заново производит перечисление устройств на шине USB и перезагружает тот же драйвер. То же самое происходит с любыми устройствами USB с включенной функцией пробуждения при переводе машины в ждущий режим. Без сигнала WAITWAKE система ведет себя разумно и не перезагружает драйвер. Если при переходе компьютера в ждущий режим устройство использовалось приложением, то непредвиденное удаление сделает недействительным манипулятор,
Другие аспекты управления питанием
437
с которым работало приложение. Могу представить поток крайне странных звонков в службу технической поддержки.
О На другом компьютере из-за ошибки в описании ACPI поле SystemWake задается равным PowerSystemWorking, в результате чего USBHUB ассоциирует в отображении DeviceState поле PowerDeviceD2 с PowerSystemWorking для любого устройства USB. Другими словами, ни один драйвер, полностью доверяющий информации о возможностях, не сможет включить устройство, потому что оно по всем признакам не может подняться выше уровня D2 — даже в работающей системе!
С) Windows 98 Second Edition не завершает запросы WAIT_WAKE. В Windows 98 это происходит, но лишь спустя долгое время после пробуждения.
О До выхода Windows ХР HIDCLASS, стандартный драйвер устройств ввода (таких как мыши и клавиатуры — см. главу 13) выдавал сигнал WAIT_WAKE с нелепым значением Powerstate-PowerSystemWorking. Само по себе это означает, что система не может пробудиться ни из какого режима с пониженным энергопотреблением. Очевидно, ни один из драйверов на нижних уровнях стека не обращал на это внимания, потому что система все же пробуждалась из таких состояний. Это наблюдение подтверждается комментариями к одному из примеров в DDK, где сказано, что параметр PowerState сигнала WAIT_WAKE игнорируется. Что же делать разработчику драйвера? Задать параметр равным PowerSystemWorking, взять значение SystemWake из описания возможностей устройства или просто игнорировать его? И как узнать, как допущенная в этом отношении ошибка повлияет на поведение драйвера, без тщательного тестирования всех возможных комбинаций оборудования?
Столкнувшись с этой проблемой, которая кажется неразрешимой, я сформулировал следующий совет: никогда не включайте функцию пробуждения устройства, не предоставив интерфейсного элемента (такого как вкладка управления питанием в Диспетчере устройств) для управления использованием этой функции. На всех платформах, предшествующих Windows ХР, отключайте ее по умолчанию (то есть запрещайте использование пробуждения). В Windows 98/Ме вам придется создать собственный интерфейс для управления пробуждением, потому что Диспетчер устройств не генерирует страницу свойства даже при наличии элементов WMI. Как минимум создайте в разделе управляющий параметр, о котором будет известно специалистам из технической поддержки.
Отключение питания при бездействии
Как правило, пользователи предпочитают, чтобы неиспользуемые устройства не потребляли энергию. Ваш драйвер может использовать (по меньшей мере) две схемы реализации этой политики. Вы можете зарегистрироваться, чтобы Power Manager отправлял IRP устройства на снижение энергопотребления, если устройство бездействует в течение заданного периода времени. Кроме того, устройство можно оставить в состоянии пониженного энергопотребления, если в системе для него отсутствуют открытые манипуляторы.
438
Глава 8. Управление питанием
Отключение по периоду бездействия
Механизм выявления бездействия на основании временных измерений базирует-ся на двух сервисных функциях: PoRegisterDeviceForldleDetection и PeSetDeviceBusy.
Следующий вызов регистрируется на выявление бездействия устройства:
pdx->1diecount = PoRegisterDeviceForIdleDetectiorUpdx~>Pdo, ulConservationTImeout. ulPerformanceTimeout, PowerDeviceD3);
Первый аргумент функции PoRegisterDeviceForldleDetection содержит адрес PDO для вашего устройства. Второй и третий аргументы задают величину тайм-аута в секундах. Второй аргумент применяется при работе в экономном режиме — например, при работе от аккумулятора. Третий аргумент действует в режиме максимальной производительности — например, при работе от сети. Четвертый аргумент задает состояние энергопотребления, в которое должно переводиться устройство, если период его бездействия превышает действующий интервал тайм-аута.
Обозначение занятости устройства
Возвращаемое значение PoRegisterDeviceForldleDetection представляет собой адрес длинного целого числа, используемого системой в качестве счетчика. Каждую секунду Power Manager увеличивает счетчик. Если он достигает заданного тайм-аута, Power Manager отправляет SET-запрос устройства с указанием зарегистрированного состояния энергопотребления. В некоторых точках драйвера этот счетчик обнуляется, что приводит к перезапуску периода обнаружения бездействия:
if (pdx->idlecount)
PoSetDevi ceBusy(pdx->1dlecount):
PoSetDeviceBusy — макрос из заголовочного файла WDM.H, который без каких-либо предосторожностей разыменует свой аргумент-указатель и обнуляет соответствующую область памяти. Однако PoRegisterDeviceForldleDetection может вернуть указатель NULL, поэтому перед вызовом PoSetDeviceBusy значение следует проверить на NULL.
После описания PoSetDeviceBusy становится очевидно, что название макроса выбрано не очень удачно. Он не сообщает Power Manager, что устройство «занято», в этом случае было бы логично ожидать, что позднее последует другой вызов, указывающий, что устройство «освободилось». Скорее, макрос сообщает, что в конкретный момент его вызова устройство не находилось в бездействии. Не считайте это замечание простой семантической придиркой. Если устройство занято неким активным запросом, в драйвере потребуется логика, предотвращающая выявления бездействия. Таким образом, PoSetDeviceBusy может вызываться во многих точках драйвера: в различных диспетчерских функциях, в функции Startlo и т. д. Проследите за тем, чтобы период выявления был длиннее максимального времени, которое может пройти между вызовами PoSetDeviceBusy в ходе нормальной обработки запроса.
Другие аспекты управления питанием
439
ПРИМЕЧАНИЕ----------------------------------------------------------------------------
Функция PoRegisterSystemState позволяет запретить Power Manager изменять системное состояние энергопотребления, но она не может использоваться для предотвращения тайм-аутов бездействия. Кроме того, эта функция не реализована в Windows 98/Ме, поэтому ее вызов противопоказан в драйверах, которые должны работать как в Windows ХР, так и в Windows 98/Ме.
Выбор тайм-аута бездействия
Задача выбора продолжительности тайм-аута не всегда проста. Для некоторых устройств может указываться значение -1, обозначающее стандартный тайм-аут из политики управления питанием для данного класса устройств. На момент написания книги к этой категории относились только устройства FILE_DEVICE_DISK и FILE_DEVICE_MASS__STORAGE. Хотя, скорее всего, вы предпочтете оставить константам тайм-аута значения по умолчанию, их значения в конечном счете должны контролироваться пользователем. Тем не менее, сам метод передачи этих значений пользователем весьма нетривиален.
Если только проектировщики системы не запланировали для устройства обобщенную схему выявления бездействия, вы должны предоставить компонент пользовательского режима, при помощи которого пользователь задает величину тайм-аутов. Чтобы этот компонент оптимальным образом соответствовал другим частям операционной системы, он должен быть оформлен в виде расширения страницы свойств для приложения Электропитание (Power) панели управления. Таким образом, вы должны предоставить DLL-библиотеку пользовательского режима, реализующую интерфейсы COM ISheilPropSheetExt и IShellExtlnit. Эта библиотека должна соответствовать общему описанию DLL расширения оболочки — вам придется самостоятельно разобраться в этой теме, если вы хотите ознакомиться со всеми тонкостями написания этой разновидности компонентов пользовательского интерфейса.
ПРИМЕЧАНИЕ —-----------------------------------------------------------------------
На мой взгляд, изучение СОМ вообще и DLL расширений оболочки лишь уводит в сторону от программирования драйверов. Вы можете бесплатно загрузить программу-мастер для Visual Studio с моего веб-сайта (http://www.oneysoft.com) и воспользоваться ею для конструирования DLL страниц свойств приложения Электропитание (Power) панели управления. Вы можете определить приватный интерфейс IOCTL между DLL и драйвером для задания констант тайм-аута по бездействию и других значений, относящихся к политике. Кроме того, также можно определить пользовательскую схему WMI (WMI schema), включающую функциональность тайм-аута по бездействию. Как будет показано в главе 10, с WMI чрезвычайно удобно работать из сценарных языков.
Восстановление питания
Если вы реализуете выявление бездействия, вам также придется обеспечить механизм последующего включения питания — например, при получении IRP, для которого питание необходимо. Чтобы эта функция работала, необходимо написать довольно сложный код.
О Содержательные IRP часто принимаются в произвольном потоке или на повышенном уровне IRQL, поэтому распределяющий поток не удастся заблокировать на время включения питания устройства.
440
Глава 8. Управление питанием
О При получении IRP могут выполняться другие операции управления питанием или IRP, требующие восстановления питания устройства.
О Драйвер может получить несколько IRP, требующих включения питания, непосредственно друг за другом. Питание восстанавливается только один раз, a IRP не должны обрабатываться вне очереди.
О IRP, по которому восстанавливается питание, может быть отменен, пока вы ожидаете восстановления.
Я считаю, что лучший способ решения всех этих проблем основан на передаче всех IRP, связанных с управлением питанием, через объекты DEVQUEUE. «Скелет» диспетчерской функции может выглядеть так:
NTSTATUS DispatchReadWr1te(PDEVICE_0BJECT fdo. PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(РОЕ VICEJXTENSI ON) fdo - >De v 1 ceExtensi on;
if (pdx->powerstate > PowerDeviceDO)
SendDeviceSetPowerCpdx, PowerDeviceDO, FALSE):
loMarklrpPendlng(Irp);
StartPacket(&pdx->dqReadWr1te, fdo, Irp, OnCancel):
return STATUS_PENDING;
}
Идея заключается в безусловной постановке IRP в очередь после начала операции питания, завершаемой асинхронно. Код управления питанием в другой точке драйвера снимает приостановку очереди после возвращения питания, а это приводит к освобождению IRP.
Отключение при закрытии манипуляторов
В другой базовой стратегии управления питанием устройство содержится в состоянии низкого энергопотребления, если только у приложения нет открытых манипуляторов. Принимая решение о реализации этой стратегии, ваш драйвер должен учитывать требования элемента WMI MSPower_DeviceEnable. Кроме того, в реестре создается параметр с последним значением элемента, заданным пользователем. Допустим, вы определяете в структуре расширения устройства два поля: в одном хранится значение MSPower_DeviceEnable, а в другом — количество открытых манипуляторов:
typedef struct _DEVICE_EXTENSION {
LONG handles;
BOOLEAN autopower;
} DEVICE-EXTENSION, *PDEVICE_EXTENSION;
Диспетчерские функции IRP_MJ_CREATE и IRP_MJ_CLOSE будут выглядеть примерно так:
NTSTATUS D1spatchCreate(PDEVICE__0BJECT fdo. PIRP Irp)
{
PDEVICE-EXTENSION pdx =
Другие аспекты управления питанием
441
(PDEVICE_EXTENSION) fdo->Devi cExtens1 on;
if (Interlockedlncrement(&pdx->handles) == 1) SendDeviceSetPower(fdo, PowerDeviceDO, TRUE):
}
NTSTATUS DispatchClose(PDEVICE_OBJECT fdo, PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICEJXTENSION) fdo->DevicExtension;
if (InterlockedDecrement(&pdx->handles) = 0 && pdx->autopower) SendDeviceSetPower(fdo, PowerDeviceD3, TRUE);
Обратите внимание: вызовы SendDeviceSetPower являются сиюсронными, поэтому нам не нужно беспокоиться о «гонках» между операцией включения питания, инициированной DispatchCreate, и операцией отключения питания, инициированной парным вызовом DispatchClose.
При использовании этой стратегии DEVQUEUE или другой аналогичный пакет приостанавливает доставку содержательных IRP функции Startlo на время отключения питания.
ПРИМЕЧАНИЕ-----------------------------------------------------------------------
Драйверы USB в Windows ХР и последующих системах для перевода устройства в состояние бездействия должны использовать протокол Selective Suspend вместо прямой отправки IRP устройства. За дополнительной информацией о протоколе Selective Suspend обращайтесь к главе 12. Единственное изменение в приведенных примерах кода — то, что вместо простого вызова SendDeviceSetPower вы отправляете вниз по стеку РпР специальный регистрационный IRP с указателем на функцию обратного вызова. Родительский драйвер вызывает эту функцию, когда устройство может быть переведено в состояние пониженного энергопотребления, после чего следует вызов SendDeviceSetPower, Вся необходимая механика продемонстрирована в примере WAKEUP.
Оптимизация смены состояний
Процесс отключения и восстановления питания устройства можно оптимизировать. Два факта помогут вам понять смысл этой оптимизации. Во-первых, драйвер шины не всегда отключает устройство, даже получив SET-запрос устройства. Эта объясняется особенностями совместного подключения устройств. В системе может существовать один или несколько каналов питания, и к любому отдельному каналу может быть подключена произвольная группа устройств. Одно устройство может быть отключено только вместе со всеми остальными устройствами на том же канале питания. Жутковатый пример, который я иногда привожу на своих семинарах: допустим, модем, который вы хотите отключить, подключен к одному каналу питания с «искусственным сердцем» вашего компьютера — система не сможет отключить модем, пока не завершится «операция» над вашим компьютером.
442
Глава 8. Управление питанием
Второй факт: у некоторых устройств смена состояния энергопотребления занимает много времени. Возвращаясь к приведенному примеру, предположим, что модем является таким устройством. На какой-то стадии вы получаете и передаете SET-запрос на перевод модема в спящий режим. Однако вам неведомо, что драйвер шины не отключил модем. Когда приходит время восстанавливать питание, можно сэкономить немного времени, зная, что питание модема еще не отключено. Именно здесь приходит на помощь этот конкретный прием оптимизации.
В момент снижения энергопотребления вы создаете и отправляете запрос управления питанием с дополнительным кодом IRPJMN_POWER_SEQUENCE драйверам, находящимся в стеке ниже вашего драйвера. Хотя запрос формально относится к категории IRP_MJ_POWER, для его создания вместо функции PoRequestPowerlrp используется функция loBuildAsynchronousFsdRequest. Впрочем, при его обработке по-прежнему используются функции PoStartNextPowerirp и PoCallDriver. Запрос завершается после того, как драйвер шины сохранит в предоставленном вами массиве три числа, которые показывают, сколько раз устройство переводилось в состояния DI, D2 и D3. При последующем запросе на восстановление питания вы создаете и отправляете другой запрос IRP_MN_POWER_SEQUENCE, получая новый набор из трех чисел. Если новые числа совпадают с теми, которые были получены при отключении, значит, состояние не изменилось и потенциально затратный процесс восстановления питания можно пропустить.
IRP_MN_POWER_SEQUENCE всего лишь оптимизирует процесс, который будет работать и без оптимизации, поэтому применять данный прием не обязательно. Более того, драйвер шины не обязан его поддерживать, и отклонение такого запроса не является признаком ошибки. Пример GENERIC в прилагаемых материалах содержит код, в котором используется эта оптимизация, но я не хочу дополнительно усложнять описание конечного автомата, поэтому в тексте он не рассматривается.
Проблемы совместимости с Windows 98/Ме
В Windows 98/Ме многие функции управления питанием реализованы не в полном объеме. А это значит, что среда Windows 98/Ме отнесется к вашим ошибкам снисходительнее, чем Windows ХР, упрощая начальную разработку драйвера. Но поскольку Windows 98/Ме допускает ошибки, недопустимые в Windows ХР, обязательно протестируйте всю функциональность управления питанием своего драйвера в Windows ХР.
О важности DO-POWER-PAGABLE
Флаг DO_POWER_PAGABLE в Windows 98/Ме играет дополнительную и неожиданно важную роль. Если этот флаг не установлен во всех объектах устройства стека, включая PDO и все фильтрующие объекты, I/O Manager сообщает подсистеме конфигурации Windows 98/Ме, что устройство поддерживает только состояние DO и не может использоваться для пробуждения системы. Следовательно,
Проблемы совместимости с Windows 98/Ме
443
если флаг DO_POWER_PAGABLE не установлен, все запросы на выявление бездействия, инициируемые вызовом PoRegisterDeviceForldleDetection, фактически, игнорируются, то есть вы никогда не получите IRP управления питанием в результате слишком долгого бездействия устройства. Другое последствие заключается в том, что функция пробуждения вашего устройства (если она есть) также не будет использоваться.
Завершение IRP управления питанием
SET- и QUERY-запросы управления питанием в Windows 98/Ме должны завершаться только на уровне PASSIVE-LEVEL. Внимательно просмотрев код управления питанием библиотеки GENERIC в прилагаемых материалах, вы увидите, что GENERIC планирует рабочий элемент вместо завершения IRP на уровне DISPATCH_LEVEL.
Если вы скопируете мой код или используете GENERIC.SYS, возможно, вам придется кое-что объяснить при передаче драйвера в WHQL (Windows Hardware Quality Lab). Windows 98/Ме не поддерживает функции loXxrWorkltem, поддерживаемые в Windows 2000 и последующих системах, поэтому вам придется использовать старые функции ExXxrWorkltem. К сожалению, тесты WHQL проверяют импортируемые символические имена в драйверах, вместо того чтобы проверять фактически вызываемые функции на стадии выполнения. В моем коде используется проверка на стадии выполнения, поэтому он полностью соответствует правилам теста (хотя и не отвечает букве закона). Если достаточное количество читателей обратится с просьбой сделать исключение, возможно, WHQL изменит свои тесты. Рабочие элементы рассматриваются в главе 14, а о WHQL рассказано в главе 15.
Запрос IRP устройств
Как упоминалось ранее, в Windows 98/Ме имеется ошибка, из-за которой вызов PoRequestPowerlrp выглядит успешным (то есть возвращает STATUS-PENDING), тогда как в действительности вы не получаете SET-запрос устройства. Проблема возникает в том случае, если в SET-запросе указано состояние устройства, в котором оно находится в данный момент, — в Windows 98/Ме подсистема конфигурации «знает», что ей не нужно сообщать об изменениях, отправляя конфигурационное событие функции, находящейся под управлением NTKERN. Если вы ожидаете завершения IRP устройства, ваше устройство на этой стадии просто перестанет реагировать.
Я воспользовался очевидным обходным решением: если оказывается, что в IRP управления питанием устройства указано состояние энергопотребления, в котором устройство находится в данный момент, я просто считаю, что IRP устройства завершен успешно. Если же говорить о переходах, через которые проходит HandlePowerEvent, я перехожу от SendDevicelrp непосредственно к нужному действию (SubPowerUpCompiete или SubPowerDownComplete).
444
Глава 8. Управление питание*
PoCallDriver
В Windows 98/Ме PoCallDriver просто вызывает loCallDriver. Соответственно, вь< можете легко ошибиться и вызвать loCallDriver для пересылки IRP управления питанием. Однако в Windows 98/Ме существует еще более серьезная проблема
Версия PoCallDriver для Windows ХР следит за тем, чтобы IRP управления питанием пересылались драйверам DO_POWER„PAGABLE на уровне PASSIVE-LEVEL а прочим — на уровне DISPATCH-LEVEL. Этот факт используется в GENERIC для пересылки IRP питания в ситуациях, когда HandlePowerEvent вызывается на уровне DISPATCH-LEVEL из функции завершения ввода/вывода. Но версия для Windows 98/Ме, так как она представляет собой loCallDriver под другим именем, не переключает IRQL. Выясняется, что все IRP управления питанием в Windows 98/Ме должны отправляться на уровне PASSIVE-LEVEL. Я написал для GENERIC вспомогательную функцию с именем SafePoCallDriver, которая ставит в очередь рабочий элемент для отправки IRP на уровне PASSIVE-LEVEL. О последствиях применения рабочего элемента в данной ситуации уже говорилось ранее в контексте завершения IRP управления питанием.
Другие отличия
Между Windows 98/Ме и Windows ХР также существует ряд других различий, связанных с управлением питанием. Я кратко опишу их и укажу, как они могуч повлиять на разработку драйверов.
При вызове PoRegisterDeviceForldleDetection вместо адреса вашего объекта устройства необходимо передавать адрес PDO. Дело в том, что во внутренней реализации система должна найти адрес структуры DEVNODE, с которой работает подсистема конфигурации Windows 98/Ме, а он доступен только из PDO. Адрес PDO также может передаваться в Windows ХР, поэтому вы можете сразу писать свой код именно таким образом.
Вспомогательная функция PoSetPowerState в Windows 98/Ме выполняет пустую операцию. Более того, хотя в документации сказано, что она возвращает предыдущее состояние энергопотребления устройства или системы, версия для Windows 98/Ме возвращает переданный аргумент состояния. Это новое состояние, а не старое, — а может, просто случайное число из неинициализированной переменной, которая была передана в аргументе функции, — никто не проверяет.
В Windows 98/Ме PoStartNextPowerirp также является пустой операцией, поэтому при разработке в Windows 98/Ме о вызове этой функции легко забыть.
Функции PoRegisterDeviceNotify и PoCancelDeviceNotify не определены в Windows 98/Ме. Насколько я могу судить, Windows 98/Ме также не выдает запрос PowerRelations для сбора информации, необходимой для поддержки функций обратного вызова. Функции PoRegisterSystemState, PoSetSystemState и PoUnregisterSystem-State также не реализованы в Windows 98/Ме. Чтобы загрузить в Windows 98/Ме драйвер, вызывающий эти или другие неопределенные системные функции, необходимо воспользоваться специальными средствами определения отсутствующих функций — например, фильтрующим драйвером WDMSTUB.SYS, описанным в приложении А.
9 Управляющие операции ввода/вывода
Если присмотреться к различным типам запросов, поступающим устройству, окажется, что большинство из них связано с чтением или записью данных. Тем не менее, время от времени у приложений возникает необходимость во «внеочередном» взаимодействии с драйвером. Для большинства типов устройств приложения могут использовать стандартную функцию Microsoft Win32 API DeviceloControl. На стороне драйвера вызов DeviceloControl в приложении преобразуется в пакет запроса ввода/вывода (IRP) с основным кодом функции IRP_MJ_DEVICE_CONTROL.
В этой главе функция DeviceloControl рассматривается с обеих сторон — как со стороны пользовательского режима, так и со стороны ядра. Тем не менее, для некоторых типов устройств приложения не должны (или не могут) взаимодействовать с драйвером при помощи DeviceloControl. Эти типы устройств перечислены в табл. 9.1.
Таблица 9.1. Альтернативы DeviceloControl для некоторых типов драйверов
Тип драйвера	Альтернатива для DeviceloControl
Минидрайвер HID (см. главу 13)	HidID_GetFeature, HidD_SetFeature
Драйвер минипорта SCSI	IOCTL_SCSI_PASS_THROUGH
Драйвер минипорта NDIS (Network Driver Interface Specification)	Запрос WMI с использованием нестандартного кода GUID. В Win98 Gold WMI не работает; действуйте по своему усмотрению
Драйвер устройства чтения смарт-карт (Interface Device [IFD] Handler в контексте PC/SC)	ScardControl
Также существует особая проблема, связанная с использованием DeviceloControl для взаимодействия с фильтрующими драйверами. Проблема и ее решение рассматриваются в главе 16.
Функция API DeviceloControl
Прототип функции API пользовательского режима DeviceloControl выглядит так:
result = DeviceloControl(Handle. Code. InputData, InputLength, OutputData, OutputLength. ^Feedback, Overlapped);
446
Глава 9. Управляющие операции ввода/вывода
Параметр Handle (тип HANDLE) содержит открытый манипулятор устройства. Чтобы получить манипулятор, следует вызвать CreateFile следующим образом:
Handle = CreateFile(”\\\\.\\IOCTL", GE'NERIC_READ GENERIC_WRITE,
0, NULL, OPENEXISTING, flags, NULL);
if (Handle == INVALID_HANDLEJ/ALUE)
<error>
CloseHandle(Handle);
Аргумент flags функции CreateFile равен либо FILE_FLAG_OVERLAPPED, либо О, он показывает, будут ли выполняться асинхронные операции с этим манипулятором файла. Открытый манипулятор используется для вызовов функций ReadFile, WriteFile или DeviceloControl. Завершив работу с устройством, закройте манипулятор вызовом CloseHandle. Однако помните, что операционная система автоматически закрывает все манипуляторы, остающиеся открытыми, при завершении процесса.
Пользовательский режим
Режим ядра
Рис. 9.1. Входные и выходные буферы функции DeviceloControl
Аргумент Code (DWORD) функции DeviceloControl содержит управляющий код, обозначающий тип выполняемой операции. О том, как определяются эти коды, будет рассказано далее (см. раздел «Определение управляющих кодов ввода/вывода»). Аргументы InputData (PVOID) и InputLength (DWORD) описывают область данных, передаваемую драйверу устройства (эти данные являются входными с точки зрения драйвера). Аргументы OutputData (PVOID) и OutputLength (DWORD) описывают область данных, которую драйвер полностью или частично заполняет информацией, передаваемой вам (как и в предыдущем случае, данные считаются выходными с точки зрения драйвера). Драйвер обновляет переменную Feedback (DWORD), указывая, сколько байт выходных данных он вернул приложению. На рис, 9.1 изображены связи между буферами в приложении и драйвере. Структура Overlapped (OVERLAPPED) используется для управления асинхронными операциями
Функция API DeviceloControl
447
(см. следующий раздел). Если при вызове CreateFile был указан флаг FILE_FLAG_ OVERLAPPED, вы должны передать указатель на структуру OVERLAPPED. Если же флаг FILE FLAG OVERLAPPED не указан, в последнем аргументе можно передать NULL, потому что система его все равно проигнорирует.
Тип буфера, используемый конкретной управляющей операцией (входной или выходной), зависит от самой операции. Например, для получения номера версии драйвера потребуется только выходной буфер. С другой стороны, операция, которая только оповещает драйвер о некотором факте, относящемся к приложению, вероятно, ограничится входным буфером. Разумеется, возможны и другие операции, для которых необходимы оба буфера или буферы вообще не нужны, — все зависит от того, что делает управляющая операция.
Функция DeviceloControl возвращает логическую величину — код успешного выполнения (TRUE) или неудачи (FALSE). В случае сбоя приложение может определить его причину, вызывая функцию GetLastError.
Синхронные и асинхронные вызовы DeviceloControl
При синхронном вызове DeviceloControl вызывающий поток блокируется до завершения управляющей операции. Пример:
HANDLE Handle = CreateFIlе(’’\\\\.\\IOCTL"...0. NULL);
DWORD version, junk:
if (DeviceloControl(Handle, IOCTL_GETJ/ERSION_BUFFERED.
NULL, 0, &version, sizeofCversion). Sjunk, NULL))
printf("IOCTL.SYS version M£2.2d\n", HIWORD(version).
LOWORD(version));
else
printf("Error %d in IOCTL_GET_VERSION_BUFFERED call\n”, GetLastErrorO);
Здесь манипулятор устройства открывается без флага FILE_FLAG_OVERLAPPED, поэтому все последующие вызовы DeviceloControl не вернут управление до тех пор, пока драйвер не выдаст запрашиваемое значение.
При асинхронном вызове DeviceloControl вызывающий поток не блокируется сразу. Вместо этого он продолжает работать, пока не достигнет точки, в которой ему потребуется результат управляющей операции. В этой точке он вызывает функцию API, которая блокирует поток до завершения операции драйвером. Пример:
HANDLE Handle = CreateFile("\\\\.WIOCTL".....
FILEJLAGJDVERLAPPED, NULL);
DWORD version, junk;
OVERLAPPED Overlapped;
Overlapped.hEvent == CreateEventtNULL. TRUE, FALSE, NULL):
DWORD code:
if (DeviceloControl(Handle......^Overlapped))
448
Глава 9. Управляющие операции ввода/вывода
code = 0;
else
code = GetLastErrorO;
<продолжение обработки>
if (code == ERROR_IO_PENDING)
if (GetOverlappedResult(Handle. ^Overlapped. &junk, TRUE)) code = 0;
else
code = GetLastErrorO;
}
CloseHandle(Overlapped.hEvent);
if (code != 0)
<оимбка>
Между этим асинхронным примером и приведенным ранее синхронным приме* ром существуют два принципиальных различия. Во-первых, при вызове CreateFile задается флаг FILE_FLAG__OVERLAPPED. Во-вторых, при вызове DeviceloControl задается адрес структуры OVERLAPPED, в которой мы инициализировали манипулятор события hEvent для описания события сброса (за дополнительной информацией о событиях и синхронизации потоков вообще обращайтесь к книге Джеффри Рихтера (Jeffrey Richter) «Programming Applications for Microsoft Windows», 4th ed. (Microsoft Press, 1999)).
Асинхронный вызов DeviceloControl приводит к одному из трех результатов. Во-первых, функция может вернуть TRUE — это означает, что диспетчерская функция драйвера устройства смогла завершить запрос немедленно. Во-вторых, она может вернуть FALSE, а функция GetLastError вернет специальный код ошибки ERRORJO-PENDING. Этот результат означает, что диспетчерская функция драйвера вернула STATUS-PENDING и управляющая операция будет завершена позднее. Обратите внимание: код ERROR__IO_PENDING не является ошибкой — это один из двух кодов, при помощи которых система сообщает, что все идет нормально. Третий возможный результат асинхронного вызова DeviceloControl — возвращаемое значение FALSE в сочетании с результатом GtLastError, отличным от ERROR-IO-PENDING. Такой результат означает «настоящую» ошибку.
В той точке, где приложению потребуется получить результат управляющей операции, оно вызывает один из синхронизационных примитивов Win32 — GetOverlappedResult, WaitForSingleObject и т. д. Синхронизационный примитив GetOverlappedResult, использованный в приведенном примере, особенно удобен, потому что он также получает количество переданных байтов и инициализирует функцию GetLastError так, чтобы она возвращала результат операции ввода/вывода. Хотя вы также можете вызвать WaitForSingleObject или другую аналогичную функцию API (передавая манипулятор события Overlapped.hEvent в аргументе), вам не удастся узнать результат операции DeviceloControl — будет известно лишь то, что операция завершилась.
Функция API DeviceloControl
449
Определение управляющих кодов ввода/вывода
Аргумент Code функции DeviceloControl представляет собой 32-разрядную числовую константу, которая определяется при помощи препроцессорного макроса CTL_CODE, входящего как в DDK, так и в Platform SDK. На рис. 9.2 показана структура такого 32-разрядного кода.
31_______________________16151413_______________2 1 О
Тип устройства	А	Код функции	М
Рис. 9.2. Строение управляющего кода ввода/вывода
Поля интерпретируются следующим образом:
О Первое поле (16 бит, первый аргумент CTL_CODE) обозначает тип устройства, реализующего управляющую операцию. Вы должны использовать то же значение (например, FILE_DEVICE_UNKNOWN), которое было указано драйвером при вызове loCreateDevice. (Код типа устройства файловой системы заставляет I/O Manager использовать другой основной код функции в отправляемых IRP.)
О Код доступа (2 бита, четвертый аргумент CTL_CODE) обозначает права доступа, необходимые приложению для выдачи управляющей операции по данному манипулятору устройства.
О Код функции (12 бит, второй аргумент CTL_CODE) указывает, какую именно управляющую операцию описывает данный код. Microsoft резервирует первую половину диапазона значений поля (то есть значения от 0 до 2047) для стандартных управляющих операций. Разработчики обычно используют значения из диапазона 2048-4095. Эта схема создана в первую очередь для того, чтобы вы могли определять приватные управляющие операции для стандартных устройств.
О Метод буферизации (2 бита, третий аргумент CTL_CODE) указывает, как I/O Manager следует поступать с входным и выходным буферами, предоставленными приложением. Это поле будет подробно рассмотрено в следующем разделе, при описании реализации запросов IRP_MJ_DEVICE_CONTROL в драйверах.
Я хочу прояснить одно возможное затруднение. При создании собственного драйвера вы можете определить серию управляющих операций (IOCTL), используемых приложением при взаимодействии с драйвером. Хотя другой разработчик драйвера может определить свой набор операций IOCTL с точно такими же числовыми значениями, система никогда не спутает эти коды, потому что коды IOCTL интерпретируются только драйвером, которому они адресуются. Но если вы откроете манипулятор для устройства, принадлежащего гипотетическому другому драйверу, и попробуете отправить ему то, что вы считаете одной из своих операций IOCTL, путаницы не миновать.
450
Глава 9. Управляющие операции ввода/вывода
Код доступа в управляющем коде ввода/вывода позволяет разделить пользо-4 вателей на четыре категории на основании дескриптора безопасности, присоеди-J ненного к объекту устройства:
О пользователи, которым доступ полностью запрещен, не могут открыть манипулятор и поэтому вообще не смогут выдавать какие-либо операции IOCTL:
О пользователи, которым разрешен доступ для чтения, но не для записи, могут выдавать операции IOCTL с кодами функций FILE^READACCESS и FILE_ANY_ ACCESS, но не те, у которых в коде указан признак FILE_WRITE_ACCESS;
О пользователи, которым разрешен доступ для записи, но не для чтения, могут выдавать операции IOCTL с кодами функций FILE_WRITE_ACCESS и FILE_ANY_ ACCESS, но не те, у которых в коде указан признак FILE_READ_ACCESS;
О пользователи, которым разрешено открытие манипуляторов как для чтения, так и для записи, могут выдавать любые IOCTL.
Вы можете облегчить жизнь как себе, так и разработчикам приложений, которым придется обращаться к вашему драйверу, разместив все определения IOCTL в отдельном заголовочном файле. В прилагаемых материалах каждый проект содержит заголовочный файл IOCTLS.H, в котором собраны эти определения. Пример:
#	1fndef CTL_CODE
#pragma message ( \
"CTL_CODE undefined. Include wlnioctl.h or wdm.h1')
#end1f
#	def1ne IOCTL_GET_VERSION_BUFFERED \
CTL_CODE(FILE_DEVICE_UNKNOWN. 0x800, METHOD-BUFFERED. \
FILE_ANY_ACCESS)
#	def1ne IOCTL_GET_VERSION_DIRECT \
CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_OUT_DIRECT, \
FILE__ANY_ACCESS)
#	def1ne IOCTL_GETJERSION_NEITHER \
CTL_CODE(FILE_DEVICE_UNKNOWN. 0x802, METHOD_NEITHER, \
FILE_ANY_ACCESS)
Присутствие сообщения #pragma объясняется тем, что я вечно забываю включить заголовочный файл WINIOCTL.H с определениями CTLJCODE для программ пользовательского режима. Получить сообщение, которое скажет, что сделано не так, гораздо приятнее, чем несколько минут шарить по каталогу включаемых файлов.
Обработка IRP_MJ_DEVICE_CONTROL
При каждом вызове DeviceloControl из пользовательского режима I/O Manager создает IRP с основным кодом функции IRP_MJ_DEVICE_CONTROL и отправляет его диспетчерской функции драйвера, находящейся на вершине стека заданного устройства. Верхний элемент стека содержит параметры, перечисленные в табл. 9.2.
Обработка IRP MJ DEVICE CONTROL
451
Фильтрующие драйверы могут интерпретировать некоторые приватные коды самостоятельно, но обычно (во всяком случае, при правильном программировании) они передаются вниз по стеку. Диспетчерская функция, которая умеет обрабатывать IOCTL, находится где-то в стеке драйверов — чаще всего в функциональном драйвере.
Таблица 9.2. Параметры IRP_MJ_DEVICE_CONTROL в стеке
Поле Parameters.DeviceloControl	Описание
OutputBufferLength	Длина выходного буфера — шестой аргумент DeviceloControl
InputBufferLength	Длина входного буфера — четвертый аргумент DeviceloControl
loControlCode Type3InputBuffer	Управляющий код — второй аргумент DeviceloControl Виртуальный адрес пользовательского режима входного буфера для METHOD_NEITHER
Заготовка диспетчерской функции для управляющих операций выглядит примерно так:
#pragma PAGEDCODE
NTSTATUS DispatchControl(PDEVICE_OBJECT fdo, PIRP Irp)
{
PAGED_CODE();	// 1
PDEVICE-EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
NTSTATUS status = STATUS_SUCCESS;
ULONG info = 0;
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp); // 2
ULONG cbin =
stack^Parameters.DeviceloControl.InputBufferLength;
ULONG cbout =
stack->Parameters.DeviceIoControl.OutputBufferLength;
ULONG code =
stack->Parameters.Devi celoControl.loControlCode;
switch (code)
{
//3
default;	// 4
status = STATUS_INVALID_DEVICE_REQUEST; break;
}
return CompleteRequestCIrp, status, info);
452
Глава 9. Управляющие операции ввода/вывода
1.	Вызов заведомо осуществляется на уровне PASSIVE_LEVEL, поэтому у нас нет особых причин для размещения диспетчерской функции где-либо, кроме перемещаемой памяти.
2.	Несколько следующих команд извлекают код функции и размеры буферов из объединения параметров в стеке ввода/вывода. Эти значения часто необходимы независимо от конкретного типа IOCTL, поэтому мне показалось, что будет проще всегда включать их в функцию.
3.	Здесь вставляются секции case для разных типов поддерживаемых операций IOCTL.
4.	При получении операции IOCTL неизвестного типа желательно вернуть осмысленный код состояния.
ВНИМАНИЕ -------------------------------------------------------------------
Выбор всегда следует осуществлять по всем 32 битам управляющего кода, чтобы программа поль-Ц зовательского режима не смогла обойти проверки доступа или заставить I/O Manager подготовить /1 параметры с применением неверного метода буферизации.
Способ обработки каждой операции IOCTL зависит от двух факторов. Первый и самый важный фактор — это фактическое предназначение IOCTL в вашей схеме работы драйвера. Вторым фактором, также критически важным для механики кода, является выбранный метод буферизации данных пользовательского режима.
В главе 7 я рассказал, как работать с программой пользовательского режима, которая передает драйверу буфер с данными для вывода на устройство или заполняет буфер входными данными, полученными от устройства. Как было сказано, в том, что касается запросов чтения/записи, во время выполнения AddDevice необходимо принять решение о том, будете ли вы использовать буферизованный или непосредственный метод (или ни один из них) для обращения к буферам пользовательского режима во всех запросах чтения и записи. Управляющие запросы также используют один из этих методов адресации, но работают немного иначе. Вместо указания глобального метода адресации в флагах объекта устройства вы указываете метод адресации для каждой операции IOCTL в двух младших битах кода функции. Соответственно, одни IOCTL могут использовать буферизованный метод, другие — непосредственный, а третьи не будут использовать ни одного. Более того, выбор метода для операций IOCTL никак не влияет на адресацию буферов для IRP чтения и записи.
Метод буферизации выбирается на основании нескольких факторов. При большинстве операций IOCTL в обоих направлениях передается гораздо менее страницы данных, поэтому в них используется метод METHOD-BUFFERED. В операциях, пересылающих более страницы данных, следует использовать один из методов DIRECT. Может показаться, что имена методов DIRECT противоречат здравому смыслу: вы используете METHOD_IN__DIRECT, если приложение отправляет данные драйверу, и METHOD_OUT_DIRECT, если пересылка ведется в обратном направлении. Если приложение вообще не нуждается в пересылке данных, лучше всего выбрать метод METHOD_NEITHER.
Обработка IRP MJ DEVICE CONTROL
453
METHOD-BUFFERED
В METHOD_BUFFERED I/O Manager создает в режиме ядра системную копию буфера, достаточно большую для наибольшего из входного и выходного буферов пользовательского режима. Когда диспетчерская функция получает управление, входные данные пользовательского режима находятся в системном буфере. Перед завершением IRP системный буфер заполняется выходными данными, которые необходимо передать приложению. При завершении IRP в поле loStatus.Information заносится количество байт, помещенных в системный буфер. I/O Manager копирует указанное количество байтов данных в пользовательский режим и заносит количество в переменную обратной связи. Схема копирования данных изображена на рис. 9.3.
Пол ьзовател ьски й режим
Режим ядра
Входной буфер
Системный буфер
Рис. 9.3. Операции с буферами в методе METHOD_BUFFERED
Внутри драйвера вы работаете с обоими буферами по одному адресу, а именно по указателю Associatedlrp.SystemBuffer в IRP. И снова напомню, что речь идет о виртуальном адресе режима ядра, ссылающемся на копию входных данных. Разумеется, обработка входных данных должна быть закончена до того, как буфер будет перезаписан выходными данными (наверное, мне и говорить этого не стоило — такие ошибки больше одного раза не совершаются).
Приведу простой пример кода обработки операции METHOD_BUFFERED из примера IOCTL:
case IOCTL_GET__VERSION_BUFFERED:
{
if (cbout < sizeof(ULONG))
{
status = STATUS_INVALID_BUFFER_SIZE;
break;
}
454
Глава 9. Управляющие операции ввода/вывода
PULONG pversion = (PULONG) Irp->AssociatedIrp.SystemBuffer;
*pversion = 0х0004000А;
info = sizeof(ULONG);
break;
}
Сначала мы убеждаемся в том, что размер выходного буфера достаточен для двойного слова, которое мы собираемся в него поместить. Затем указатель SystemBuffer используется для адресации системного буфера, в котором сохраняет-ся результат простой операции. Когда внешняя диспетчерская функция завершит этот IRP, локальная переменная info превратится в поле ToStatus.Information. Наконец, I/O Manager копирует заданный объем данных из системного буфера-копии в буфер пользовательского режима.
Всегда проверяйте длину буферов, переданных с запросом IRP_MJ_DEVICE_ Ц| CONTROL, — по крайней мере тогда, когда значение RequestorMode в IRP отлично от KernelMode. При использовании METHOD_BUFFERED и двух методов METHOD-XXXJ3IRECT I/O Manager проверяет действительность адреса и длины входного и выходного буферов, но настоящую длину буфера знаете только вы.
Методы DIRECT
Методы METHOD_JN_DIRECT и METHOD_OUT_DIRECT обрабатываются в драйвере одинаково. Они различаются только правами доступа, необходимыми для доступа к буферу пользовательского режима. Методу METHOD_IN_DIRECT необходим доступ для чтения, методу METHOD^ OUTJDIRECT необходим доступ для чтения и записи. При обоих упомянутых методах I/O Manager предоставляет системный буфер режима ядра (Associatedlrp.SystemBuffer) для входных данных и список MDL для буфера выходных данных. За информацией о MDL обращайтесь к главе 7, а на рис. 9.4 изображена схема операций с буферами.
Рис. 9.4. Операции с буферами в методах METHODXXX_DIRECT
Обработка IRP MJ DEVICE CONTROL
455
Пример простого обработчика запроса METHOD_XXX_DIRECT:
case 10CTLJ3ETJERSION_DIRECT:
{
If (cbout < sizeof(ULONG))
{
status = STATUS_INVALID_BUFFER_SIZE:
break;
}
PULONG pversion = (PULONG)
MmGetSystemAddressForMdl Saf e (I rp - >Mdl Address);
*pversion = 0x0004000В:
info = sizeof(ULONG):
break:
}
Единственное существенное различие между этим и предыдущим примерами выделено жирным шрифтом (я также изменил сообщаемый номер версии, чтобы я мог легко определить, что тестовая программа вызывает правильную операцию IOCTL). При запросах, задействующих любой из методов DIRECT, список MDL, на который ссылается поле IRP MdlAddress, используется для обращения к выходному буферу пользовательского режима. По этому адресу можно выполнить доступ к памяти (DMA). В данном примере я просто вызвал функцию MmGetSystemAddressForMdISafe для получения адреса режима ядра, который ссылается на физическую память, описанную MDL.
СОВЕТ--------------------------------------------------------------------------------
Чтобы обеспечить портируемость на двоичном уровне, воспользуйтесь портируемой альтернативой для MmGetSystemAddressForMdISafe, описанной в главе 7.
METHOD_NEITHER
При использовании метода METHOD_NEITHER I/O Manager не пытается как-либо транслировать виртуальные адреса пользовательского режима. Соответственно, метод METHOD_NEITHER чаще всего применяется тогда, когда вы не собираетесь передавать или принимать данные от драйвера. Например, управляющая операция стандартного последовательного порта IOCTL_SERIAL_SET_DTR предназначена для установки сигнала DTR (Data Terminal Ready). Согласно определению, она использует метод METHOD_NEITHER, потому что данные в ней не задействованы.
Впрочем, метод METHODJNEITHER может использоваться и при передаче данных — вам придется лишь соблюдать некоторые правила преобразования указателей. Вы получаете (в параметре Type3InputBuffer в элементе стека) виртуальный адрес пользовательского режима для входного буфера, тогда как виртуальный адрес пользовательского режима для выходного буфера передается в ноле IRP UserBuffer. Любой из этих адресов может использоваться только в том случае, если выполнение ведется в контексте того же процесса, который используется
456
Глава 9. Управляющие операции ввода/вывода
вызывающей стороной пользовательского режима. Но если вы уверены в правильности контекста процесса, указатели можно разыменовать напрямую:
case IOCTL_GET_VERSION_NEITHER;
{
if (cbout < sizeof(ULONG))
{
status = STATUS_INVALID_BUFFER_SIZE;
break;
}
PULONG pverslon = (PULONG) Irp->UserBuffer;
if (Irp->RequestorMode 1= KernelMode)
{
_try
{
ProbeForWrite(pversion, sizeof(ULONG), 1);
*pversion « Ox000400DA:
}
_except(EXCEPTION_EXECUTE_HANDLER)
{
status = GetExceptionCodeO;
break;
}
}
else
*pversion = Dx0004000A;
Info = sizeof(ULONG);
break;
}
Как видно из фрагмента, выделенного жирным шрифтом, существует только одна реальная проблема: вы должны убедиться в допустимости записи в буфер, полученный от непроверенного источника. Структурированная обработка исключений рассматривалась в главе 3. ProbeForWrite — стандартная функция режима ядра для проверки возможности записи по заданному виртуальному адресу пользовательского режима. Второй аргумент обозначает длину проверяемой области данных, а третий — выравнивание, требуемое для области данных. В данном примере мы хотим убедиться в том, что 4 байта доступны для записи, и допускаем однобайтовое выравнивание для самой области данных. На самом деле функция ProbeForWrite (а также парная функция ProbeForRead) проверяет, что заданный адресный диапазон имеет правильное выравнивание и занимает часть адресного пространства, принадлежащую пользовательскому режиму, — она не пытается записывать (или читать) данные в соответствующем блоке памяти.
Обращение к памяти должно производиться в кадре структурированного исключения. Если окажется, что какая-либо часть буфера на момент обращения принадлежит несуществующей странице, подсистема управления памятью инициирует исключение (вместо немедленного фатального сбоя). Ваш обработчик исключения остановит исключение и предотвратит сбой системы.
Обработка IRP MJ DEVICE CONTROL
457
Проектирование надежного и безопасного интерфейса IOCTL
Спроектированный интерфейс IOCTL способен оказать заметное влияние на безопасность и устойчивость систем, в которых в конечном счете будет выполняться ваш код. Вероятно, вы заметили в этой главе множество значков, которыми я обозначаю потенциальные проблемы безопасности. Дело в том, что неаккуратное программирование способно легко создать непредвиденные дефекты в системе безопасности или привести к нарушению целостности системы.
Итак, помимо тех обстоятельств, на которые я указывал в этой главе, при проектировании интерфейса IOCTL к драйверу нужно учитывать следующие факторы:
Э Нельзя предполагать, что к интерфейсу IOCTL будет обращаться только ваше собственное приложение, поставляющее только действительные параметры (а как же иначе!). Кибертеррорист проникает в систему по тому же принципу, по которому муха проникает в дом: он кружит снаружи, пока не найдет подходящую дырку. Если такая дырка окажется в вашем драйвере, хакеры найдут ее и опубликуют, чтобы об этом мог узнать любой желающий.
Э Не передавайте строки, завершенные нуль-символами, в аргументах IOCTL. Передавайте длину строки в байтах. В этом случае драйвер не рискует выйти за границу страницы в поисках отсутствующего нуль-завершителя.
Э Не размещайте указатели в структурах, используемых в вызовах IOCTL. Вместо этого упакуйте все данные для конкретного вызова в единый буфер и используйте внутренние смещения в этом буфере. Не поленитесь проверить смещения и длины по отношению к общей длине структуры параметров.
Э Не пишите свой аналог IOCTL_POKE_KERNEL_MEMORY. Иначе говоря, не изобретайте вспомогательные управляющие операции, предназначенные для записи в память ядра. Не делайте этого — даже в отладочной версии драйвера, потому что отладочные версии нередко выбираются за пределы лаборатории.
Э Будьте осторожны со сквозными (pass-through) операциями IOCTL. Может быть, ваше приложение должно напрямую взаимодействовать с оборудованием, а может быть, его функциональность лучше реализуется с использованием сквозной операции. Просто будьте внимательны в отношении функциональности, открываемой для внешнего пользователя.
Э Старайтесь избегать зависимости IOCTL от информации состояния, оставшейся от предыдущей операции. Единственное незыблемое правило по поводу устойчивой информации состояния гласит, что ее не бывает... В смысле, не бывает ничего абсолютно устойчивого. Что-то всегда может сломаться и испортить данные, на которые вы так надеялись, поэтому старайтесь проектировать операции IOCTL так, чтобы они были как можно более автономными и независимыми.
Э Метод METHOD_NEITHER не рекомендуется использовать для управляющих операций, связанных с пересылкой данных, из-за двух опасностей. Во-первых, вы можете забыть о проверке буфера, необходимой для предотвращения
458
Глава 9. Управляющие операции ввода/вывода
дефектов безопасности. Во-вторых, кто-то может забыть о том, что IOCTL использует METHOD_NEITHER, и вызвать ваш код в неверном контексте потока. Если вы и те программисты, которые придут после вас, исключительно хорошо организованы, проблем не будет, но одной из составляющих успешного проектирования является учет человеческих слабостей.
О И самое главное — не предполагайте, что никто не попытается использовать ваш драйвер для взлома системы. Даже если бы в мире был всего один злоумышленник, безоглядно доверять всем окружающим было бы рискованно. — а в действительности таких злоумышленников гораздо больше. Не будет преувеличением сказать, что систематическое использование врагами цивилизации слабостей самой распространенной операционной системы иногда способно поставить под угрозу человеческие жизни.
Кстати говоря, в проверке безопасности интерфейса IOCTL вам поможет утилита DEVCTL из DDK. Она отправляет драйверу случайные запросы IOCTL и правильно сформированные запросы с неверными параметрами, пытаясь спровоцировать сбой. Атака такого рода имитирует действия начинающих хакеров, которым в руки попал ваш драйвер, поэтому вы в любом случае должны провести эту проверку.
Впрочем, какой бы изобретательной ни была утилита общего назначения DEVCTL, я также рекомендую написать собственную тестовую программу для тщательной проверки всех граничных состояний интерфейса IOCTL. Вот лишь некоторые объекты для проверки:
О недействительные коды функций. Впрочем, здесь особое рвение не потребуется — DEVCTL достаточно тщательно проводит этот тип тестирования;
О коды функций с ошибками в одном или нескольких из четырех полей (тип устройства, маска доступа, код функции и метод буферизации);
О нехватка или избыток данных во входных и выходных буферах;
О слишком короткие (но тем не менее присутствующие) буферы;
О одновременные операции от разных потоков, использующие одинаковые или разные манипуляторы.
Внутренние управляющие операции ввода/вывода
Система использует запросы IRPJ4JJDEVICE__CONTROL для реализации вызовов DeviceloControl из пользовательского режима. Драйверам иногда тоже приходится взаимодействовать друг с другом, но для этой цели они используют запросы IRP_ MJJNTERNALJDEVICE_CONTROL. Типичный фрагмент кода выглядит примерно так:
ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
KEVENT event;
KeIn1tializeEvent(&event, NotlficationEvent. FALSE):
IO_STATUS_BLOCK iostatus:
PIRP Irp = loBuildDeviceloControlRequesttloControlCode,
Внутренние управляющие операции ввода/вывода
459
'	Deviceobject, plnBuffer, cblnBuffer, pOutBuffer, cbOutBuffer,
*	TRUE, &event, &1ostatus);
NTSTATUS status = loCal1 Driver(DeviceObject, Irp):
If (NT_SUCCESS(status))
KeWa1tForS1ngleObject(&event, Executive, KernelMode, FALSE, NULL);
Выполнение на уровне PASSIVE_LEVEL является обязательным требованием как для вызова loBuildDeviceloControlRequest, так и для блокировки по объекту события, как показано в этом фрагменте.
Аргумент loControlCode функции loBuildDeviceloControlRequest содержит управляющий код, который представляет операцию, выполняемую приемным драйвером устройства. Он сходен с кодами стандартных управляющих операций. Аргумент DeviceObject является указателем на структуру DEVICE_OBJECT, драйвер которой выполняет указанную операцию. Параметры входного и выходного буферов служат той же цели, что и их аналоги в вызовах DeviceloControl пользовательского режима. Седьмой аргумент, равный TRUE в приведенном примере, означает, что строится внутренняя управляющая операция (если передать FALSE, будет создан запрос IRP__MJ_DEVICE_CONTROL). Назначение аргументов event и iostatus лтдет описано чуть позже.
Функция loBuildDeviceloControlRequest строит IRP и инициализирует первый элемент стека описаниями кода операции и буферов, заданных вами. Функция возвращает указатель на IRP, что позволяет вам провести любую дополнительную ;шициализацию. Например, в главе 12 я покажу, как использовать внутренний управляющий запрос для отправки блока запроса USB (URB) драйверу USB. Одна из фаз этого процесса включает сохранение указателя на URB в поле параметров в стеке. Затем функция loCallDriver отправляет IRP приемному устройству. Если возвращаемое значение проходит проверку NT_SUCCESS, происходит ожидание по объекту event, заданному в восьмом аргументе loBuildDeviceloControlRequest. I О Manager устанавливает событие при завершении IRP, а также заполняет структуру iostatus кодом статуса завершения и дополнительной информацией. Наконец, функция loFreelrp вызывается для освобождения IRP. Это означает, что после вызова loCallDriver к указателю на IRP обращаться вообще не следует.
Ь'МАНИЕ----------------------------------------------------------------------------------
Зри использовании автоматических переменных для аргументов события и блока статуса функций loBuildDeviceloControlRequest или loBuildSynchronousFsdRequest необходимо ожидать установки события, если loCallDriver вернет STATUS_PENDING. В противном случае возникает опасность -ого, что событие и блок статуса выйдут из области видимости прежде, чем I/O Manager завершит обработку IRP. Ожидание возможно и в том случае, если loCallDriver вернет код успеха, но в этом случае оно должно завершиться немедленно, потому что IRP уже полностью завершен. Не используйте ожидание, если loCallDriver вернет код ошибки, потому что при возникновении ошибки I/O Manager не устанавливает событие и не изменяет блок статуса при завершении IRP.
Поскольку во внутренних управляющих операциях используется взаимодействие между двумя драйверами, их отправка подчиняется меньшему количеству правил, чем можно было бы предположить. В частности, для их создания не
460
Глава 9. Управляющие операции ввода/вывода
обязательно использовать loBuildDeviceloControlRequest: можно просто вызвать loAllocatelrp и провести инициализацию самостоятельно. А если приемный драйвер не требует, чтобы обработка внутренних управляющих операций велась исключительно на уровне PASSIVE_LEVEL, такие IRP также можно отправлять на уровне DISPATCH_LEVEL — скажем, из функции завершения ввода/вывода или функции DPC. (Конечно, в таких случаях нельзя использовать loBuildDeviceloControlRequest или ожидать завершения IRP, но можно отправить IRP, потому что loAllocatelrp и loCallDriver могут выполняться на уровне DISPATCH-LEVEL и ниже.) Вы даже не обязаны использовать параметры стека ввода/вывода в точности так, как это делается для обычных IOCTL. Например, при вызовах драйвера USB в поле, в котором обычно хранится длина выходного буфера, сохраняется указатель на URB. Итак, при проектировании внутреннего управляющего протокола между двумя вашими драйверами запрос IRP_MJJNTERNAL_DEVICE_CONTROL можно рассматривать как контейнер для произвольных передаваемых сообщений.
Кстати говоря, использовать одну и ту же диспетчерскую функцию для внутренних и внешних управляющих операций не рекомендуется — по крайней мере, без проверки основного кода функции IRP. Приведу пример: допустим, драйвер имеет внешний управляющий интерфейс, через который приложение запрашивает номер версии вашего драйвера. Кроме того, у него имеется внутренний управляющий интерфейс, через который доверенная вызывающая сторона режима ядра может получить критическую информацию, которая не должна быть доступна программам пользовательского режима. Теперь предположим, что для обоих интерфейсов используется одна функция, как в следующем примере:
NTSTATUS DriverEntryC...)
DriverObject->MajorFunct1on[IRP_MJ_DEVICE_CONTROL] =
DIspatchControl:
Dri verObject->MajorFunction[IRP__MJ_INTERNAL__DEVICE_CONTROL] = DispatchControl;
'}
NTSTATUS Di spatchControl (...)
{
switch (code)
{
case IOCTL_GET_VERSION:
case IOCTL__INTERNAL__GET_SECRET:
// <=== доступно для вызовов пользовательского режима
}}
Если приложение каким-то образом определит числовое значение IOCTL_ INTERNAL_GET_SECRET, оно сможет выдать обычный вызов DeviceloControl и обойти меры безопасности, запланированные для этой функции.
Оповещение приложений о событиях
461
Оповещение приложений о событиях
Среди важнейших практических применений операций IOCTL следует отметить то, что с их помощью драйверы WDM могут оповещать приложения о происходящих интересных событиях (впрочем, вы сами определяете, какие события считаются «интересными»). Чтобы обсуждение было более конкретным, предположим, что некоторое приложение должно тесно взаимодействовать с драйвером. При каждом аппаратном событии ваш драйвер должен оповещать приложение, а последнее предпринимает действия, видимые для пользователя. Например, при нажатии кнопки на инструменте приложение должно приступить к сбору и отображению данных. В Windows 98/Ме существует пара механизмов для оповещения приложений драйверами в подобных ситуациях (а именно асинхронные вызовы процедур или посылка оконных сообщений), однако в Windows ХР эти методы не работают, так как операционная система не обладает необходимой инфраструктурой (или не предоставляет ее для внешнего использования).
Драйвер WDM может оповестить приложение о событии двумя способами:
О приложение создает событие, используемое совместно с драйвером. Затем приложение ожидает по этому событию, а драйвер устанавливает событие при возникновении необходимых обстоятельств:
О приложение выдает запрос DeviceloControl, который драйвер приостанавливает, возвращая STATUS.PENDING. Драйвер завершает IRP, когда произойдет что-то интересное.
В обоих случаях приложение обычно создает специальный поток для задачи ожидания по оповещению. Другими словами, при совместном использовании события в приложении создается поток, который большую часть своего жизненного цикла ожидает активизации по WaitForSingleObject. При использовании приостановки IOCTL создается поток, который проводит большую часть своего жизненного цикла в ожидании возврата из DeviceloControl. Ничего другого этот поток не делает — не считая, возможно, посылки оконного сообщения потоку пользовательского интерфейса, обеспечивающего выдачу визуальных признаков для пользователя.
ОРГАНИЗАЦИЯ ПОТОКА ОПОВЕЩЕНИЯ-----------------------------------------------------
В обоих методах оповещения можно избежать некоторых внутренних проблем, отказавшись от простой блокировки потока оповещения по событию или IOCTL Лучше действовать несколько иначе: сначала определите «событие уничтожения», которое будет устанавливаться основным потоком приложения, когда придет время выхода из потока оповещения.
Если для оповещения используется схема с общим событием, вызовите WaitForMultipleObject для ожидания либо события уничтожения, либо общего события с драйвером.
Если используется схема с приостановкой IOCTL, используйте асинхронные вызовы DeviceloControl. Вместо того чтобы вызывать GetOverlappedResult, вызовите WaitForMultipleObjects для ожидания, либо события уничтожения, либо события, связанного со структурой OVERLAPPED. Если возвращаемый код указывает, что операция DeviceloControl завершилась, неблокируемый вызов GetOverlappedResult используется для получения кода возврата и количества переданных байтов. Если код возврата указывает на установку события уничтожения, вызовите Cancello для отмены DeviceloControl и выходите через процедуру потока. Если приложение работает в Windows 98 Second Edition, вызов Cancello можно опустить.
462
Глава 9. Управляющие операции ввода/вывода
Каждый из этих двух методов решения проблемы оповещения обладает своими достоинствами и недостатками, перечисленными в табл. 9.3. Хотя на первый взгляд кажется, что метод приостановки IOCTL во всех отношениях превосходит метод общего события, я все же рекомендую использовать метод общего события из-за сложности проблем с «гонками», которые вам придется решать с методом приостановки IOCTL.
Таблица 9.3. Сравнение методов оповещения
Общее событие	Приостановка IRP„MJ_DEVICE_CONTROL
Приложение должно создать объект вызовом CreateEvent Драйвер должен преобразовать манипулятор в указатель на объект Логика отмены не нужна; тривиальная зачистка	Объект не нужен Преобразование не требуется Необходима логика отмены и зачистки, обычные жуткие состояния «гонок»
Приложение узнает только о самом факте возникновения события	При завершении IRP драйвер может передать дополнительные произвольные данные
Применение общего события для оповещения
Основная идея метода общего события заключается в том, что приложение создает событие вызовом CreateEvent, а затем отправляет манипулятор события драйверу при помощи функции DeviceloControl:
DWORD junk;
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
DeviceloControl(hdevlce, IOCTL_REGISTERJVENT, ShEvent, sizeof(hEvent), NULL, 0, &junk, NULL);
ПРИМЕЧАНИЕ--------------------------------------------------------------------
Метод оповещения приложения об «интересных» событиях с использованием общего события продемонстрирован в примере EVWAIT. Событие генерируется нажатием клавиши на клавиатуре, а тестовая программа направляет драйверу служебную операцию IOCTL. На практике событие будет генерироваться по определенным аппаратным условиям.
Вызов CreateEvent создает объект режима ядра KEVENT и создает в таблице манипуляторов процесса приложения ссылку на KEVENT. Значение HANDLE, возвращаемое приложением, фактически, представляет собой индекс в таблице манипуляторов. Тем не менее, манипулятор не может напрямую использоваться драйвером WDM по двум причинам. Во-первых, не существует документированного интерфейса режима ядра для установки события по манипулятору. Во-вторых (что более важно), манипулятор может использоваться только в потоке, принадлежащем тому же процессу. Если код драйвера выполняется в контексте произвольного потока (как это часто бывает), он не сможет сослаться на событие по манипулятору.
Чтобы обойти эти две проблемы, драйвер должен «преобразовать» манипулятор в указатель на базовый объект KEVENT. Чтобы выполнить управляющую
Оповещение приложений о событиях
463
операцию METHOD-BUFFERED, используемую приложением для регистрации события в драйвере, используйте код следующего вида:
HANDLE hEvent = *(PHANDLE) Irp->AssociatedIrp.SystemBufter;
PKEVENT pevent:
NTSTATUS status = ObReferenceObjectByHandleChEvent.
EVENTjraiFY_STATE, *ExEventObjectType. Irp->RequestorMode, (PVOID*) Spevent. NULL);
ObReferenceObjectBy Handle ищет hEvent в таблице манипуляторов текущего процесса и сохраняет адрес связанного с ним объекта ядра в переменной pevent. Если поле RequestorMode в IRP равно UserMode, эта функция также проверяет, что hEvent действительно является манипулятором, что манипулятор относится к объекту события и что для его открытия использовался способ, включающий привилегию EVENT„MODIFY_STATE.
Обращаясь Object Manager с запросом на преобразование манипулятора, полученного из пользовательского режима, запросите проверку уровня доступа и типа, для этого в аргументе режима доступа вызываемой функции Object Manager указывается значение UserMode. В конце концов, число, полученное из пользовательского режима, вообще может не быть манипулятором или же может относиться к объекту другого типа. Кроме того, избегайте пользоваться недокументированной функцией ZwSetEvent, чтобы не создавать дефект в безопасности системы: даже если вы убедитесь в том, что некий случайный манипулятор действительно относится к объекту события, вызывающая сторона пользовательского режима может закрыть манипулятор и вернуть то же числовое значение манипулятора для другого типа объекта. Это приведет к нежелательным последствиям, потому что ваш код является доверенной вызывающей стороной для ZwSetEvent.
Приложение может ожидать наступления события:
WaitForSIngleObject(hEvent, INFINITE);
Драйвер сигнализирует о наступлении события обычным способом:
KeSetEvent(pevent, EVENT__INCREMENT, FALSE);
В конечном итоге приложение производит зачистку, вызывая CloseHandle. Драйвер содержит отдельную ссылку на объект события, которую он должен освободить вызовом ObDereferenceObject. Object Manager не уничтожает объект события до тех пор, пока не будут выполнены оба условия.
Применение приостановки IOCTL для оповещения
Основная идея метода оповещения приостановкой IOCTL заключается в следующем: когда приложение хочет получать от драйвера оповещения о событиях, оно вызывает DeviceloControl:
HANDLE hDevice = CreateFI1e("\\\\ \\<driver-name>", ...):
BOOL okay = DeviceloControl(hDevice, IOCTL_WAIT_NOTIFY,
...);
464
Глава 9. Управляющие операции ввода/вывода
(Кстати, IOCTL_WAIT_NOTIFY — управляющий код, который я использовал в примере NOTIFY в прилагаемых материалах.)
Драйвер приостанавливает операцию IOCTL и завершает ее позднее. Без учета других возможных факторов код драйвера может быть совсем простым:
NTSTATUS DispatchControl(...)
{
switch (code)
{
case IOCTL_WAIT_NOTIFY:
IoMa rkIrpPendi ng(Irp);
pdx->Not1fyIrp = Irp;
return STATUS-PENDING;
}
}
VOID OnInterestingEvent(...)
{
CompleteRequest(pdx->Noti fy Irp, STATUS_SUCCESS. 0): // <== Так нельзя!
}
Разумеется, «другие факторы», которые я так легко обошел, играют важнейшую роль для построения работающего драйвера. Например, отправитель IRP может решить отменить его. Приложение может вызывать Cancello, или завершение потока приложения заставит компонент режима ядра вызвать loCancellrp. В любом случае, мы должны предоставить функцию отмены, чтобы обеспечить завершение IRP. При переводе устройства в состояние пониженного энергопотребления или внезапном отключении устройства от компьютера желательно отменить все незавершенные запросы IOCTL. В общем случае может возникнуть необходимость в аварийном завершении произвольного количества IOCTL. Соответственно, нам потребуется связанный список таких запросов. А поскольку сразу несколько потоков могут обращаться к этому связанному списку, нам также потребуется спин-блокировка для безопасной работы с ним.
Вспомогательные функции
Чтобы упростить свою работу, я написал несколько вспомогательных функций для управления асинхронными IOCTL. Самые важные из этих функций — Cache-Control Request и UncacheControlRequest. Они предполагают, что вы готовы принять только один асинхронный запрос IOCTL с конкретным управляющим кодом на один объект устройства, и, как следствие, вы можете зарезервировать в расширении устройства поле с указателем на текущий незавершенный IRP. В примере NOTIFY я буду называть этот указатель Notifylrp. Прием асинхронного IRP происходит так:
switch (code)
{
Оповещение приложений о событиях
465
case IOCTL_WAIT_NOTIFY;
If (<параметры не прошли проверку^)
status = STATUS_INVALID_PARAMETER;
else
status = CacheControlRequest(pdx, Irp, &pdx->NotifyIrp); break;
return status == STATUS-PENDING ? status :
CompleteRequestCIrp, status, info);
В этом фрагменте особенно важен вызов CacheControlRequest, который регистрирует IRP таким образом, чтобы при необходимости его можно было отменить. Кроме того, он сохраняет адрес IRP в поле Notifylrp расширения устройства. Мы ожидаем, что вызов вернет STATUS_PENDING, в этом случае мы обходимся без завершения IRP и просто возвращаем STATUS_PENDING вызывающей стороне.
ПРИМЕЧАНИЕ---------------------------------------------------------------------------
Описанная схема легко обобщается так, чтобы для каждого открытого манипулятора приложение могло иметь незавершенный IRP каждого типа. Вместо того чтобы помещать текущие указатели IRP в расширение устройства, поместите их в структуру, которая ассоциируется с объектом FILEOBJECT, соответствующим манипулятору. Указатель на FILE-OBJECT берется из элемента стека ввода/вывода для IRP_MJ-CREATE, IRP_MJ_CLOSE и, фактически, для всех остальных IRP, сгенерированных для манипулятора файла. Вы можете использовать поля FsContext и FsContext2 объекта файла для любых целей по своему усмотрению.
Позднее, когда произойдет событие, ожидаемое приложением, выполняется код следующего вида:
PIRP nfyirp = UncacheControlRequest(pdx, &pdx->NotifyIrp):
if (nfyirp)
{
<некоторые действ ия>
CompleteRequestCnfyirp, STATUS-SUCCESS, <1nfo value>);
}
Фрагмент получает адрес незавершенного запроса IOCTL_WAIT_NOTIFY, делает что-то для получения данных, возвращаемых приложению, а затем завершает пакет запроса ввода/вывода.
Как работают вспомогательные функции
В функциях CacheControlRequest и UncacheControlRequest скрыты многие потенциальные затруднения. Эти две функции обеспечивают механизм отслеживания асинхронных запросов IOCTL, безопасный по отношению к потокам и многопроцессорным средам. В них используется разновидность методов безопасной постановки и выведения IRP из очереди в то время, когда кто-то может пытаться отменить IRP. Я упаковал эти функции в библиотеку GENERIC.SYS, а пример NOTIFY в прилагаемых материалах показывает, как вызывать их. А вот как
466
Глава 9. Управляющие операции ввода/вывода
работают эти функции (учтите, что в версиях для GENERIC.SYS в их имена добавлено слово «Generic»):
typedef struct _DEVICE-EXTENSION {
KSPINJOCK loctlListLock;
LIST_ENTRY PendlngloctlLI st;
} DEVICE-EXTENSION. *PDEVICE_EXTENSION;
NTSTATUS CacheControlRequest(PDEVICE-EXTENSION pdx. PIRP Irp, PIRP* plrp)
KIRQL oldirql;
KeAcquireSpinLock(&pdx->IoctlListLock, Soldirql);	// 1
NTSTATUS status; if (*plrp)	// 2
status = STATUS-UNSUCCESSFUL;
else if (pdx->IoctlAbortStatus)	// 3
status = pdx->IoctlAbortstatus; else
{
loSetCancelRoutine!Irp, OnCancelPendingloctl);	// 4
if (Irp->Cancel && IoSetCancelRoutine(Irp, NULL)) status = STATUS-CANCELLED;
else { loMarklrpPending(Irp);	// 5
status = STATUS-PENDING;
Irp->Tail.Overlay.DriverContextEO] = plrp;	// 6
*plrp = Irp;
InsertTai1 Li st(&pdx->PendingloctlLi st, &Irp->Tai1.Overlay.ListEntry);
}
}
KeReleaseSpinLock(&pdx->IoctlListLock, oldirql); return status: }
VOID OnCancelPendingloctl(PDEVICE_OBJECT fdo, PIRP Irp)
KIRQL oldirql = Irp->CancelIrql:
loReleaseCancelSpinLock(DISPATCH_LEVEL);
PDEVICE-EXTENSION pdx =
(PDEVICE-EXTENSION) fdo->DeviceExtension;
KeAcqui reSpi nLockAtDpcLevel(&pdx->IoctlLi stLock);
RemoveEntryLi st C&Irp->Tai1.Overlay.Li stEntry);
PIRP plrp = (PIRP) Irp->Tail.Overlay.DriverContextEO];
InterlockedCompareExchange((PVOID*) plrp, Irp, NULL); KeReleaseSpinLock(&pdx->IoctlListLock, oldirql);
Оповещение приложений о событиях
467
Irp->IoStatus. Status = STATUS__C ANCELL ED; loCompleteRequest(Irp, IO_NO_INCREMENT); }
PIRP UncacheControlReauest(PDEVICE_EXTENSION pdx. PIRP* plrp)
{
KIRQL oldirql:
KeAcquireSpinLock(&pdx->IoctlListLock, Soldirql);
PIRP Irp = (PIRP) InterlockedExchangePointerfpIrp, NULL); // 7
if (Irp)
{
if (loSetCancelRoutinedrp, NULL))	// 8
RemoveEntryLi st(&Irp->Tai1. Overl ay. Li stEntry);
}
else
Irp = NULL;
}
KeReleaseSpinLock(&pdx->IoctlListLock, oldirql);
return Irp;
1.	Спин-блокировка используется для защиты списка незавершенных IOCTL и всех указателей, зарезервированных для ссылки на текущий экземпляр для всех типов асинхронных запросов IOCTL.
2.	Здесь обеспечивается соблюдение правила (впрочем, это скорее решение из области проектирования), согласно которому в любой момент времени может быть только один незавершенный IRP каждого типа.
3.	Команда if учитывает тот факт, что в какой-то момент входящие IRP могут отклоняться из-за событий РпР или управления питанием.
4.	Так как IRP может приостанавливаться на достаточно долгое время, для него следует определить функцию отмены. Логика отмены уже неоднократно обсуждалась в книге, и я уверен, что повторять ее снова не нужно.
5.	Здесь мы решаем отложить IRP для последующего завершения. Так как функция DispatchControl вернет STATUS-PENDING, необходимо вызвать loMarklrpPending.
6.	В случае отмены IRP нам потребуется способ сбросить указатель отложенного запроса в NULL. Так как не существует способа получить параметр контекста, переданный функции отмены, я решил сохранить указатель в одном из полей DriverContext в IRP.
7.	При нормальном развитии событий эта команда сбрасывает хранимый IRP.
8.	После того как IRP будет сброшен, в дальнейшем он отменяться не должен. Но если loSetCancelRoutine вернет NULL, значит, IRP в данный момент находится в процессе отмены. В этом случае вместо указателя на IRP возвращается NULL.
468
Глава 9. Управляющие операции ввода/вывода
Пример NOTIFY также содержит обработчик IRP_MJ_CLEANUP для незавершенных операций IOCTL, он почти не отличается от обработчиков зачистки, описанных для операций чтения/записи. Наконец, он включает вспомогательную функцию AbortPendingloctls, которая используется при снижении энергопотребления или непредвиденном удалении устройства:
VOID AbortPendingloctls(PDEVICEJEXTENSION pdx, NTSTATUS status)
{
InterlockedExchange(&pdx->IoctlAbortstatus, status);
CleanupControlRequests(pdx, status, NULL);
}
Функция CleanupControlRequests — обработчик запросов IRP„MJ_CLEANUP. Я написал ее таким образом, что она отменяет все незавершенные IRP, если третий аргумент (обычно указатель на объект файла) равен NULL.
Пример NOTIFY слишком прост, чтобы служить полноценной моделью для реального драйвера. Приведу лишь некоторые дополнительные факторы, которые следует обдумать в процессе проектирования:
О Драйвер может иметь различные типы событий, приводящих к срабатыванию оповещений. Вы сами выбираете, как они должны обрабатываться: с единым кодом IOCTL (в этом случае тип события передается в дополнительных выходных данных) или с несколькими разными кодами IOCTL.
О Регистрацию событий можно разрешить нескольким потокам. Конечно, в этом случае одним указателем на IRP в расширении устройства уже не обойтись — понадобится механизм отслеживания всех IRP, относящихся к конкретному типу события. Если вы используете один тип IOCTL для всех оповещений, один из вариантов отслеживания основан на их включении в очередь PendingloctIList (см. ранее). Затем, когда событие завершится, вы в цикле вызываете ExInterlockedRemoveHeadList и loCompleteRequest для очистки списка незавершенных запросов (я намеренно избавился от этой сложности в NOTIFY, решив, что в любой момент времени может выполняться только один экземпляр тестовой программы).
О Диспетчерская функция IOCTL может вступить в «гонку» с операцией, генерирующей события. Например, в примере USBINT (см. главу 12) возникает потенциальная опасность гонки между диспетчерской функцией IOCTL и функцией псевдопрерывания. Чтобы избежать потери событий или выполнения логически несогласованных действий, потребуется спин-блокировка. Правильное применение спин-блокировки продемонстрировано в примере USBINT.
Проблемы совместимости с Windows 98/Ме
Сервисная функция VxD, которая должна использоваться NTKERN для завершения перекрывающихся операций IOCTL (VWIN32_DIOCCompletionRoutine), не поддерживает код ошибки. Таким образом, если приложение выполнит перекрывающееся обращение к драйверу WDM, вызов GetOverlappedResult будет казаться успешным даже в том случае, если драйвер отклонит операцию.
Проблемы совместимости с Windows 98/Ме
469
Приложения Win32 могут использовать функцию DeviceloControl для взаимодействия как с виртуальными драйверами устройств (VxD), так и с драйверами WDM. Между IOCTL для драйверов WDM и IOCTL для VxD существуют три нетривиальных различия. Самое важное различие относится к интерпретации манипулятора устройства, полученного от CreateFile. При работе с драйвером WDM манипулятор относится к конкретному устройству, тогда как при взаимодействии с VxD вы получаете манипулятор драйвера. Возможно, на практике VxD придется реализовать механизм «псевдоманипулятора» (встроенный в поток данных IOCTL), чтобы приложения могли обращаться к конкретным экземплярам оборудования, находящегося под управлением VxD.
Другое различие между управляющими операциями VxD и WDM относится к присваиванию числовых управляющих кодов. Как упоминалось ранее, управляющие коды драйверов WDM определяются макросом CTL_CODE, и вы не можете определить более 2048 кодов. Для VxD доступны все 32-разрядные значения, кроме 0 и -1. Если вы хотите написать приложение, которое может работать как с драйвером VxD, так и с драйвером WDM, используйте CTL_CODE для определения управляющих кодов — драйвер VxD сможет работать с полученными числовыми значениями.
Третье различие является второстепенным: предпоследний аргумент DeviceloControl (указатель PDWORD, ссылающийся на переменную обратной связи) обязателен для драйверов WDM, но не для VxD. Другими словами, при вызове драйвера WDM вы должны передать отличное от NULL значение, указывающее на DWORD. Но при вызове драйвера VxD можно передать NULL, если вас не интересует, сколько байтов данных было помещено в выходной буфер. Впрочем, переменную обратной связи рекомендуется передавать и при вызове VxD, вреда от нее не будет. Более того, разработчику драйвера VxD легко забыть о том, что указатель способен быть равным NULL, — это может привести к ошибке, если возможность передачи NULL задействована в приложении.
1 WMI
Microsoft Windows ХР поддерживает механизм управления компьютерной системой, называемый WMI (Windows Management Instrumentation). WMI представляет собой реализацию от компании Microsoft более широкого промышленного стандарта WBEM (Web-Based Enterprise Management). Проектировщики WMI стремились создать модель управления системой и описания управляющих данных в корпоративных сетях, которая была бы по возможности независимой от конкретного набора функций API или модели объектов данных. Подобная независимость упрощает разработку общих механизмов создания, транспортировки и отображения данных, а также управления отдельными компонентами системы.
Драйверы WDM взаимодействуют с WMI по трем путям (рис. 10.1). Во-первых, WM1 отвечает на запросы информации, используемой для управления системой. Во-вторых, различные приложения-контроллеры могут использовать WMI для управления общей функциональностью устройств, поддерживающих этот стандарт. Наконец, WM1 предоставляет механизм событийных сигналов, при помощи которого драйверы могут оповещать приложения о важных событиях. Все три аспекта программирования драйверов будут рассмотрены в этой главе.
ОБ ИМЕНАХ WMI И WBEM--------------------------------------------------------------
Модель CIM (Common Information Model) представляет собой спецификацию системы управления предприятием на базе веб-технологий, поддерживаемой группой DMTF (Distributed Management Task Force). Microsoft обозначила свою реализацию CIM термином WBEM; фактически, это CIM для Windows. Часть режима ядра CIM для Windows была названа WMI. Чтобы способствовать более широкому распространению CIM, группа DMTF выступила с маркетинговой инициативой и использовала WBEM в качестве имени CIM. Тогда компания Microsoft переименовала свою реализацию WBEM в WMI, a WMI (часть режима ядра) —- в «расширения WMI для WDM». Таким образом, WMI соответствует спецификациям CIM и WBEM.
Боюсь, обилие разных терминов в этой главе лишь усилит то замешательство, которое вы, вероятно, испытываете в данный момент. Я рекомендую мысленно заменять все упоминания CIM и WBEM в этой книге (и во всей документации Microsoft) на WMI. Скорее всего, вы по крайней мере будете думать о той концепции, о которой пишу я или компания Microsoft, — пока не появятся новые термины вроде «базовых расширений Windows для простых смертных» (Windows Basic Extensions for Mortals) или «полностью интегрированной мыши» (Completely Integrated Mouse). В этом случае выкручивайтесь как знаете.
Основные концепции WMI
471
Статистика и данные о производительности
События
Драйвер WDM
Управление
Рис. 10.1. Роль драйвера WDM в WMI
Основные концепции WMI
На рис. 10.2 изображена общая архитектура WML В модели WM1 мир делится на поставщиков и потребителей данных и событий. Потребители потребляют, а поставщики, соответственно, поставляют блоки данных, которые являются экземплярами, абстрактных классов. Используемые при этом концепции не так уж сильно отличаются от концепции классов в языке C++. Классы WMI, как и классы C++, состоят из полей данных и методов, реализующих поведение объектов. Содержимое блока данных не задается WMI — оно полностью зависит от того, кто и для какой цели поставляет данные.
Рис. 10.2. Архитектура WMI
В WMI могут существовать несколько пространств имен, каждое из которых содержит классы, принадлежащие одному или нескольким поставщикам. Пространство имен содержит атрибуты и классы, каждый из которых должен обладать уникальным именем в данном пространстве имен. Пространство имен может обладать собственным дескриптором безопасности, который задается администратором в приложении Управление компьютером (Computer Management). Как поставщики, так и потребители указывают пространство имен, в границах которого они собираются работать. В табл. 10.1 перечислены некоторые пространства
472
Глава 10. WMI
имен, существующие на одном из моих компьютеров. В настоящей главе нас интересует пространство имен WMI, потому что именно в нем находятся классы, с которыми работают драйверы.
Таблица 10.1. Пространства имен WMI на одном из компьютеров автора
Пространство имен	Описание
root\CIMV2	Стандартные классы
root\DEFAULT	Работа с реестром
root\Directory\LDAP	Объекты Active Directory
root\MSAPPS	Прикладные классы Microsoft
root\WMI	Классы драйверов устройств WDM
Драйвер WDM может создать экземпляры одного или нескольких классов WMI из пространства имен root\wmi. Многие драйверы поддерживают стандартные классы, определяемые Microsoft в файле DDK с именем WMICORE.MOF. Также драйверы могут реализовать пользовательскую схему, включающую классы, специфические для данного поставщика или устройства. Схема определяется на языке MOF (Managed Object Format). Система ведет словарь данных, также называемый репозиторием, в нем содержатся определения всех известных схем. Если в драйвере все будет сделано правильно, система автоматически помещает вашу схему в репозиторий при инициализации драйвера.
ПРИМЕЧАНИЕ —------------------------------------------------------------------
WMI также можно рассматривать в контексте классических реляционных баз данных. Класс WMI является аналогом таблицы. Экземпляры класса соответствуют записям, а члены классов — полям записи. Репозиторий играет ту же роль, что и традиционный словарь данных. В WMI даже существует свой язык запросов, основные концепции которого позаимствованы из языка SQL (Structured Query Language), хорошо знакомого программистам баз данных.
Пример схемы
Позднее в этой главе будет показан пример WDM42.SYS (находится в прилагаемых материалах). В этом примере используется следующая схема MOF:
[Dynamic, Provider!"WMIProv"),	// 1
WMI,
Description("Wmi42 Sample Schema"),
guid("AOF95FD4-A587-lld2-BB3A-OOC04FA330A6").
locale("MS\\0x409“)]
class Wm142	// 2
{
[key, read] string InstanceName;
[read] boolean Active:
Основные концепции WMI
473
[WmiDatald(l),
Description "The Answer to the Ultimate Question") ]
uint32 TheAnswer;
}:
Я не собираюсь описывать синтаксис MOF во всех подробностях — эта информация имеется в документации Platform SDK и WMI SDK. Вы можете либо сконструировать MOF вручную, как это было сделано в моем простом примере, либо воспользоваться утилитой WBEM CIM Studio, входящей в WMI SDK. Здесь я лишь в общих чертах представлю содержимое MOF-файла:
1. Поставщик WMIProv представляет собой системный компонент, который умеет создавать экземпляры класса. Например, он знает, как обратиться с вызовом к режиму ядра и как отправить пакет запроса ввода/вывода (IRP) соответствующему драйверу. Он также может найти правильный драйвер по глобально-уникальному идентификатору (GUID), приведенному в начале файла.
2. В схеме объявляется класс с именем WMI42, имя которого случайно совпадает с именем драйвера. Экземпляры класса обладают свойствами с именами InstanceName, Active и TheAnswer.
Разработчик запускает MOF-компилятор для определения схемы. В результате обработки схемы создается двоичный файл, который в конечном итоге становится ресурсом в исполняемом файле нашего драйвера. Под ресурсом в данном случае понимается та же концепция, которую разработчики применяют при построении шаблонов диалоговых окон, таблиц строк и других данных, входящих в ресурсный сценарий проекта.
Соответствие между классами WMI и структурами С
Класс WMI42 получился особенно простым, так как он содержит всего одну переменную, которая к тому же оказалась 32-разрядным целым числом. В данном случае отображение структуры данных класса, используемой WMI, на структуру С, используемую драйвером, абсолютно очевидно. С отображением более сложных классов WMI ситуация не столь однозначна, особенно при объявлении элементов данных в порядке, отличном от порядка WmiDatald. Вместо того чтобы пытаться спрогнозировать отображение класса, я рекомендую при помощи утилиты WMIMOFCK создать заголовочный файл со всеми необходимыми объявлениями. Для этого в командной строке вводятся команды следующего вида:
mofcomp -wmi -b:wm142,bmf wmi42.mof
wnrimofck -hwnri42.h -m wmi42.bmf
Первая команда создает двоичный MOF-файл (wmi42.bmf). Вторая команда, среди прочего, создает заголовочный файл следующего вида:
#1fndef _wm142_h_
#define _wm142__h_
474
Глава 10. WMI
// Wm142 - Wmi42
// Wmi42 Примерная схема
#define Wm142Guid \
{ 0xa0f95fd4,0xa587.0xlld2, \
{ ОхЬЬ.ОхЗа,0x00,OxcO.Ox4f,0xa3.0x30.0xa6 } }
DEFINEJUID(Wmi42__GUID,
OxaOf95fd4,Oxa587, 0xlld2,Oxbb,0x3a,
0x00,OxcO,Ox4f,ОхаЗ.0x30,Охаб);
typedef struct _Wmi42
{
// Ответ на Главный Вопрос Жизни, Вселенной и всего такого1
ULONG TheAnswer;
#define Wmi42_TheAnswer_SIZE sizeof(ULONG)
#define Wwi42_TheAnswer_ID 1
} Wm142, *PWm142;
#define Wm142_SIZE (FIELD_0FFSET(Wm142, TheAnswer) \
+ Wml42_TheAnswer_SIZE)
#endif
Обратите внимание: размер структуры не равен просто sizeof(Wmi42). Несколько странное определение Wmi42_SIZE объясняется тем, что структуры класса WMI, в отличие от структур С, не выравниваются по границе, кратной самому жесткому требованию к внутреннему выравниванию.
СОВЕТ-----------------------------------------------------------------—— --------------
Во всех примерах книги используется версия WMIMOFCK, входящая в бета-версию Windows .NET Server DDK. Если для построения и тестирования примеров будет использоваться более ранняя версия DDK/ возможно, вам придется внести некоторые изменения в исходный код драйвера.
Драйверы WDM и WMI
Поддержка WMI в режиме ядра в основном базируется на IRP с основным кодом операции IRP_MJ_SYSTEM_CONTROL. Вы должны зарегистрировать свое намерение принимать эти IRP следующим вызовом:
loWMIRegistrationControl(fdo. WMI_ACTION_REGISTER);
Регистрацию лучше всего провести в функции AddDevice, в той точке, где система сможет безопасно отправить драйверу системный управляющий IRP. Далее система отправляет запрос IRP_MJ_SYSTEM„CONTROL для получения подробных
1 Отсылка к известной книге Дугласа .Адамса «The Hitchhiker’s Guide to the Galaxy». — Примеч. nepee.
Драйверы WDM и WMI
475
регистрационных данных о вашем устройстве. Регистрация отменяется следующим парным вызовом во время выполнения RemoveDevice:
loWMIRegistrationControl(fdo. WMI_ACTION_DEREGISTER);
Если на момент отмены регистрации имеются незавершенные вызовы WMI, loWMIRegistrationControl ожидает их завершения. Следовательно, перед отменой регистрации драйвер еще должен сохранять способность отвечать на IRP. Новые IRP могут отклоняться с кодом STATUS_DELETE_PENDING, но ответить все равно нужно.
Прежде чем объяснять, как обрабатываются регистрационные запросы, я опишу обработку системных управляющих IRP вообще. В табл. 10.2 перечислены дополнительные коды функций запросов IRP_MJ_SYSTEM__CONTROL.
Таблица 10.2. Дополнительные коды функций IRP_MJ_SYSTEM_CONTROL
Дополнительный код функции Описание
IRP_MN_QUERY_ALL„DATA	Получает все экземпляры всех элементов блока данных
IRP_MN_QUERY_SINGLE„INSTANCE	Получает все элементы в одном экземпляре блока данных
IRP_MN_CHANGE„SINGLE_INSTANCE	Заменяет все элементы в одном экземпляре блока данных
IRP_MN_CHANGE_SINGLE_ITEM	Заменяет один элемент в блоке данных
IRP„MN„ENABLE__EVENTS	Разрешает выдачу событий
IRP_MN_DISABLE_EVENTS	Запрещает выдачу событий
I RP_MN_ENABLE_CO ELECTION	Начинает сбор «дорогой» статистики
IRP__MN_DISABLE_COLLECTION	Прекращает сбор «дорогой» статистики
IRP_MN_REGINFO_EX	Получает подробную регистрационную информацию
IRPJ4N„EXECLITE_METHOD	Выполняет функцию-метод
Объединение Parameters в элементе стека включает субструктуру WMI с параметрами системного управляющего запроса:
struct {
ULONG_PTR Providerld;
PVOID Datapath;
ULONG BufferSize:
PVOID Buffer;
} WMI;
Providerld — указатель на объект устройства, которому направлен запрос. Buffer — адрес области ввода/вывода, в которой несколько первых байтов отображаются на структуру WNODEJHEADER. Поле BufferSize содержит размер области буфера. Ваша диспетчерская функция извлекает информацию из буфера и возвращает результаты запроса в той же области памяти. Для всех дополнительных кодов функций, за исключением IRP_MN_REGINFO, поле Datapath содержит адрес 128-разрядного GUID, идентифицирующего класс блока данных. Поле Datapath для запроса IRPJMNJREGINFO равно либо WMIREGISTER, либо WMIUPDATE (0 или 1 соответственно) в зависимости от того, что требуется сделать — предоставить
476
Глава 10. WMI
начальные регистрационные данные или просто обновить информацию, переданную ранее.
При проектировании драйвера необходимо выбрать один из двух способов обработки системных управляющих IRP. Первый способ основан на функциональности вспомогательного «драйвера» WMILIB. В действительности WMILIB представляет собой DLL-библиотеку режима ядра, эта библиотека экспортирует сервисные функции, вызываемые драйвером для выполнения «черной работы» по обработке IRP. Во втором способе вы просто обрабатываете IRP самостоятельно. Использование WMILIB снижает объем написанного кода, однако вы не сможете использовать все функции WMI в полном объеме, а будете ограничены их подмножеством, поддерживаемым WMILIB. Более того, ваш драйвер не будет работать в исходной версии Microsoft Windows 98, потому что библиотека WMILIB в ней не поддерживалась. Чтобы отсутствие WMILIB в исходной версии Windows 98 не испортило вам жизнь, просмотрите раздел с проблемами совместимости в конце главы.
Для большинства драйверов функциональности WMILIB оказывается достаточно, поэтому я ограничусь описанием WMILIB. В документации DDK описан процесс самостоятельной обработки системных управляющих IRP (если вам решительно необходимо использовать именно этот способ).
Обработка IRP с использованием WMILIB
В диспетчерской функции системных управляющих IRP основная часть работы поручается WMILIB; программная реализация выглядит примерно так:
WMIGUIDREGINFO guldllstC] = { {&Wm142_GUID. 1. 0}. };
WMILIB-CONTEXT llblnfo = {	// 1
arrayslze(guldllst),
guidlist,
QueryReglnfo,
QueryDataBlock,
SetDataBlock,
SetData Item,
ExecuteMethod,
FunctlonControl,
}
NTSTATUS DispatchWmiUN PDEVICEJDBJECT fdo, IN PIRP Irp)
{
POEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
NTSTATUS status;
SYSCTL_IRP_DISPOSITION disposition;
status = WmiSystemControK&libinfo, fdo, Irp, Sdisposition):	// 2
switch (disposition)
{
Драйверы WDM и WMI
477
case IrpProcessed:
break;	// 3
case IrpNotCompleted:
loCompleteRequest(Irp, 1O_NO_INCREMENT);	// 4
break;
default:	//5
case IrpNotWmi:
case IrpForward:
loSkipCurrentlrpStackLocation(lrp);	// 6
status = IoCallDriver(pdx->LowerDev1ce0bject, Irp);
break;
}
return status:
}
1.	Структура WMILIB_.CONTEXT, объявляемая на уровне файла, описывает коды GUID классов, поддерживаемые вашим драйвером, а также некоторые функции обратного вызова, используемые WMILIB для обработки запросов WMI с учетом специфики конкретного устройства и драйвера. Если информация остается неизменной для разных IRP, можно использовать статическую контекстную структуру.
2.	Эта команда обращается к WMILIB для обработки IRP. Мы передаем адрес структуры WMILIB-CONTEXT. Функция WmiSystemControl возвращает два объекта данных: код NTSTATUS и значение SYSCTLJRP„DISPOSITION.
3.	В зависимости от кода диспозиции может потребоваться дополнительная работа с данным IRP. Если код равен IrpProcessed, значит, запрос IRP уже завершен и с ним больше ничего делать не нужно. Это нормальный случай для всех дополнительных кодов функций, кроме IRP„MN_REGINFO.
4.	Если код диспозиции равен IrpNotCompleted, завершение TRP лежит на нашей совести. Этот случай является нормальным для IRP_MN_REGINFO. Блок loStatus в IRP уже заполнен в WMILIB, поэтому нам остается только вызвать loCompleteRequest.
5.	Случаи default и IrpNotWmi не должны встречаться в Windows ХР. Мы попадаем в секцию default, если код драйвера обрабатывает не все возможные коды диспозиции. Секция IrpNotWmi выполняется в случае, если WMILIB был послан запрос IPR с дополнительным кодом функции, не соответствующим функциональности WML
6.	Случай IrpForward используется для системных управляющих IRP, предназначенных для другого драйвера. Вспомните: параметр Providerid обозначает драйвер, которому положено обработать этот IRP. Функция WmiSystemControl сравнивает это значение с указателем на объект устройства, переданным во втором аргументе функции. Если они не совпадают, функция возвращает IrpForward, чгобы мы отправили IRP вниз по стеку следующего драйвера.
478
Глава 10. WMI
Путь подключения потребителя WMI к вашему драйверу, выполняющему функции поставщика WMI, базируется на коде (или кодах) GUID. заданных в структуре контекста. Когда потребитель желает получить данные, он (косвенно) обращается к словарю данных в репозитории WMI для преобразования символического имени объекта в GUID. Значение GUID включается в синтаксис MOF, как я показывал ранее. В структуре контекста указывается то же значение GUID, a WMILJB берет на себя их сопоставление.
Для выполнения действий, специфических для конкретного устройства или драйвера, WMILIB вызывает функции драйвера. Как правило, функции обратного вызова выполняют запрашиваемую операцию синхронно. Тем не менее, за исключением случая IRP_MN_REGINFO, обработку можно отложить, для этого следует вернуть код STATUS„PENDING и завершить запрос позднее.
Функция обратного вызова QueryReglnfo
Первый системный управляющий IRP, получаемый драйвером после регистрации, имеет дополнительный код функции IRP„MN_REGINFO. При передаче его WmiSystemControl библиотека WMILIB делает обратный ход и вызывает функцию QueryReglnfo — ее адрес берется из нашей структуры WMILIB__CONTEXT. Вот как этот обратный вызов обрабатывается в WMI42.SYS:
NTSTATUS QueryRegInfo(PDEVICE_OBJECT fdo, PULONG Hags, PUNICODE^STRING Instname, PUNICODE_STRING* regpath, PUNICODE_STRING resname, PDEVICE_OBJECT* pdo) {
PDEVICEJXTENSION pdx - (POEVICE_EXTENSION) fdo->DeviceExtension;
*	flags - WMIREG_FLAGJNSTANCE_PDO;
*	regpath = &servkey:
RtllnitUnicodeStringCresname, L" Hof Resource");
*	pdo = pdx->Pdo;
return STATUS SUCCESS;
}
Параметру regpath присваивается адрес структуры UNICODE_STRING, содержащей имя раздела реестра с описанием нашего драйвера. Он находится в разделе ...\System\CurrentControlSet\Services. Функция DriverEntry получает имя раздела в аргументе и сохраняет его в глобальной переменной servkey. Параметру resname присваивается имя, которое было присвоено схеме в ресурсном сценарии. Приведу содержимое файла ресурсов для WMI42.SYS, чтобы вы видели, откуда берется это имя:
#1nclude <windows.h>
LANGUAGE LANGJNGLISH, SUBLANG_NEUTRAL
MofResource MOFDATA wmi42.bmf
WMI42.BMF — файл, в который сценарий сборки помещает откомпилированный MOF-файл. Назвать ресурс можно как угодно, но традиционно используется имя Mofresource. Важно лишь то, чтобы это же имя было указано при обработке вызова QueryReglnfo.
Драйверы WDM и WMI
479
Затем мы заполняем оставшиеся значения в зависимости от того, как в нашем драйвере решается проблема с именами экземпляров (я вернусь к этой теме позднее, в подразделе «Имена экземпляров» настоящей главы). В WMI42.SYS использован простейший вариант, который также настоятельно рекомендует Microsoft: система автоматически генерирует имена по имени, присвоенному драйвером шины объекту физического устройства (PDO). При использовании такой схемы выбора имен в QueryReglnfo необходимо сделать следующее:
О установить флаг WMIREG_FLAG_INSTANCE_PDO в поле flags, возвращаемом WMILIB. Установка флага сообщает WMILIB, что по крайней мере один из объектов использует имена на базе PDO;
О задать значение pdo, возвращаемое WMILIB. В моих примерах для подобных случаев в расширение устройства включается поле с именем Pdo, значение которого задается во время выполнения AddDevice.
Помимо упрощения вашей работы, определение имен экземпляров на базе PDO имеет и другие преимущества: приложения могут автоматически определять дружественное имя устройства и другие свойства, и это не требует от вас никаких действий в драйвере.
Когда QueryReglnfo возвращает код успеха, WMILIB создает сложную структуру с именем WMIREGINFO, которая включает список GUID, раздел реестра, имя ресурса и информацию об именах экземпляров. Управление передается вашей диспетчерской функции, которая завершает IRP и возвращает управление. Схема процесса показана на рис. 10.3.
1. Система отправляет системный управляющий IRP (1RP_MN_REGINFO)
Рис. 10.3. Последовательность обработки IRP_MN_REGINFO
480
Глава 10. WMI
Функция обратного вызова QueryDataBlock
Информация, переданная вами в ответ на исходный регистрационный запрос, позволяет системе перенаправлять вашему драйверу те операции с данными, которые к нему относятся. Код пользовательского режима использует различные COM-интерфейсы для чтения и записи данных на нескольких уровнях. Четыре возможных варианта представлены в табл. 10.3.
Таблица 10.3. Формы запросов данных
Дополнительные функции IRP	Функция обратного вызова WMILIB	Описание
IRP_MN_QUERY_ALL_DATA	QueryDataBlock	Получает все элементы всех экземпляров
IRP_MN_QUERY_SINGLE_INSTANCE	QueryDataBlock	Получает все элементы одного экземпляра
IRP_MN_CHANGE_SINGLE_INSTANCE	SetData Block	Задает все элементы одного экземпляра
IRP„MN_CHANGE_SINGLE_ITEM	SetDataltem	Задает один элемент в одном экземпляре
Когда кто-то захочет узнать значение или значения хранимых данных, он отправляет системный управляющий IRP с одним из дополнительных кодов функций IRP_MN_QUERY_ALL_DATA или IRP_MN„QUERY_SINGLEJNSTANCE. При использовании WMILIB запрос делегируется функции WmiSystemControl, которая вызывает функцию обратного вызова QueryDataBlock. Вы предоставляете запрашиваемые данные, вызываете другую функцию WMILIB с именем WmiComplete-Request для завершения IRP, а затем возвращаете управление WMILIB для «раскрутки» процесса. В этой ситуации WmiSystemControl возвращает код диспозиции IrpProcessed, потому что IRP уже завершен. Общая схема передачи управления показана на рис. 10.4,
Функция обратного вызова QueryDataBlock может оказаться довольно сложной, если ваш драйвер поддерживает несколько экземпляров блока данных, размер которых изменяется в зависимости от экземпляра. Эти затруднения будут рассмотрены позднее, в подразделе «Работа с несколькими экземплярами». Пример WMI42 показывает, как обработать более простой случай, в котором ваш драйвер поддерживает только один экземпляр класса WMI:
NTSTATUS QueryDataBlock(PDEVICE_OBJECT fdo, PIRP Irp,
ULONG guidindex, ULONG instindex, ULONG wstcount, PULONG Instlength, ULONG bufsize, PUCHAR buffer) { if (quidindex > arraysize(guidlist))	// 1
return WmiCompleteRequest(fdo, Irp,
STATUS_WMI_GUID_NOT_FOUND, 0, IO_NO_INCREMENT):
if (instindex != 0 instcount != 1)
Драйверы WDM и WMI
481
return WmiCompleteRequest(fdo, Irp,
STATUS_WMI_INSTANCE_NOT_FOUND, 0, IO_NO_INCREMENT):
If (hnstlength bufsize < Wmi42_SIZE)	// 2
return WmiCompleteRequest(fdo, Irp, STATUS_BUFFER_TOO_SMALL,
Wm142_SIZE, IO_NO_INCREMENT):
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension:
PWm142 pvalue = (PWm142) buffer:	// 3
pva!ue->TheAnswer = pdx->TheAnswer;
InstlengthCO] = Wm142_SIZE;
return Wm1CompleteRequest(fdo, Irp, STATUS-SUCCESS,	//4
Wml42_SIZE, IO_NO_INCREMENT):
}
1. Система отправляет системный управляющий IRP (IRP_MN_QUERY_ALL_DATA или IRP_MN_QUERY_SINGLEJNSTANCE)
5. Драйвер возвращает управление
Рис. 10.4. Последовательность обработки запросов данных
482
Глава 10. WMI
1.	Подсистема WMI должна была уже убедиться в том, что запрос относится к экземпляру поддерживаемого класса. Таким образом, значение guidindex должно лежать в границах списка GUID, а значения instindex и instcount не должны превышать количество заявленных экземпляров. Но если регистрационная информация только что изменилась, может оказаться, что мы обрабатываем «уже запущенный» запрос, поэтому такие проверки необходимы для предотвращения ошибок.
2.	Мы обязаны выполнить эту проверку и убедиться в том, что длина буфера достаточно велика для хранения данных и их длин, которые мы собираемся в нем разместить. Первая часть условия («существует ли массив instlength?») является стандартной. Вторая часть условия («достаточен ли размер буфера для структуры WMI42?») проверяет, поместятся ли все данные в буфере.
3.	Параметр buffer указывает на область памяти для размещения наших данных. Параметр instlength указывает на массив, в котором мы должны поместить длину каждого возвращаемого экземпляра данных. В данном примере размещаются одно значение данных (значение свойства TheAnswer) и его длина. Определение числового значения TheAnswer предоставляется читателю для самостоятельной работы.
4.	Спецификация WMILIB требует, чтобы мы завершили IRP вызовом вспомогательной функции WmiCompleteRequest. Четвертый аргумент указывает, какая часть буферной области была использована для хранения данных. Полагаю, остальные аргументы не требуют пояснений.
Функция обратного вызова SetDataBlock
Система может потребовать, чтобы вы изменили целый экземпляр одного из классов, отправляя запрос IRP_MN_CHANGE_SINGLEJNSTANCE. При обработке этого запроса функция WmiSystemControl передает управление функции обратного вызова SetDataBlock. Простая версия этой функции выглядит так:
NTSTATUS SetDataBlock(PDEVICE_OBJECT fdo, PIRP Irp, ULONG guidindex. ULONG Instindex, ULONG bufslze, PUCHAR buffer)
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
If (quidindex > arrayslze(guldllst))	// 1
return WmiCompleteRequest(fdo, Irp,
STATUS-WMI_GUID-NOT_FOUND, 0, IO_NO_INCREMENT);
If (Instindex != 0 Instcount != 1)
return WmiCompleteRequest(fdo, Irp,
STATUS_WMI_INSTANCE_NOT_FOUND, 0, IO_NO_INCREMENT);
If (bufslze == Wm142_SIZE)	// 2
{
pdx->TheAnswer = ((PWm142) buffer)->TheAnswer:
status = STATUS-SUCCESS:
Info = Wm142_SIZE;
}
Драйверы WDM и WMI
483
else
Status = STATUS_INFO_LENGTH_MISMATCH, info = 0;
return WnnCompleteRequesttfdo, Irp. status, info,	//3
I0_N0_INCREMENT):
}
1.	Эти страховочные проверки уже упоминались ранее для функции обратного вызова QueryDataBlock.
2.	Система уже должна знать (на основании объявления MOF) размер экземпляра каждого класса и предоставить буфер именно этого размера. Если этого не произойдет, IRP необходимо отклонить. В противном случае новое содержимое блока копируется в то место, где мы храним свою копию этих данных.
3.	Мы отвечаем за завершение IRP вызовом WmiCompleteRequest.
Функция обратного вызова SetDataltem
Иногда потребитель хочет изменить только одно поле в поддерживаемом нашим драйвером объекте WMI. Каждое поле обладает идентификатором, который отображается в свойстве WmiDatald объявления MOF этого поля (свойства Active и InstanceName изменяться не могут и не обладают идентификаторами. Более того, они реализуются системой и даже не отображаются в блоках данных, с которыми мы работаем). Чтобы изменить значение одного поля, потребитель указывает идентификатор этого поля. Далее мы получаем запрос IRP_MN_CHANGE_ SINGLE_ITEM, для обработки которого функция WmiSystemControl вызывает функцию обратного вызова SetDataltem:
NTSTATUS SetDataItem(PDEVICE_OBJECT fdo. PIRP Irp. ULONG guidindex,
ULONG instindex, ULONG Id, ULONG bufsize, PUCHAR buffer) {
PDEVICE-EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
NTSTATUS status:
ULONG info:
if (quidindex > arraysize(guidlist)) return WmiCompleteRequest(fdo, Irp, STATUS_WMI_GUID_NOT_FOUND, 0, IO_NO_INCREMENT); if (instindex != 0 instcount ’= 1)
return WmiCompleteRequest(fdo, Irp,
STATUS_WMI_INSTANCE_NOT_FOUND, 0, IO_NO_INCREMENT);
if (id != Wmi42_TheAnswer_ID)
return WmiCompleteRequest(fdo, Irp,
STATUS_WMI_ITEMID_NOT_FOUND, 0, IO_NO_INCREMENT);
if (bufsize == Wmi42_SIZE)
{
pdx->TheAnswer = (PWmi42) buffer)->TheAnswer: status = STATUS_SUCCESS:
484
Глава 10. WMI
info = Wmi42_SIZE;
}
else
status = STATUS JNFOJENGTOISMATCH, info = 0;
return WmiCompleteRequestffdo, Irp. status, info, IO_NO_INCREMENT); }
В этом примере единственное различие между функциями SetDataltem и Set-Data Block сводится к дополнительной проверке идентификатора поля (выделено жирным шрифтом).
При запуске WMIMOFCK можно указать ключ -с. Он генерирует исходный файл на языке С с несколькими элементами TODO, завершив их, вы получите практически весь необходимый код. Я не использую эту возможность, потому что WDMWIZ генерирует «скелет» кода, который лучше укладывается в мою инфраструктуру и требует меньшего количества изменений. Впрочем, я использую ключ -h при вызове WMIMOFCK, потому что не существует нормальной альтернативы, обеспечивающей правильное отображение структур.
Расширенные возможности
В предыдущем разделе рассказано практически все, что необходимо знать для передачи осмысленной информации о производительности приложениям-мониторам. Включите воображение: вместо одного показателя (TheAnswer) вы можете накопить и вернуть любое число метрик производительности, подходящих для вашего конкретного устройства. Кроме того, вы можете задействовать дополнительные функции WMI для более специализированных целей. Я опишу их в этом разделе.
Работа с несколькими экземплярами
WMI позволяет создать несколько экземпляров конкретного блока данных класса для одного объекта устройства. Например, использование нескольких экземпляров может быть оправданно при написании драйвера контроллера или другого устройства, к которому подключаются другие устройства, в этом случае каждый экземпляр представляет данные одного из дочерних устройств. Количество экземпляров класса задается в структуре WMIGUIDREGINFO для GUID, ассоциированного с классом. Например, если бы стандартный блок данных WMI42 мог существовать в трех экземплярах, в структуре WMILIBJZONTEXT использовался бы следующий список GUID:
WMIGUIDREGINFO guidlistL] = {
{&Wm142_GUID, 3, 0},
}:
Единственное отличие этого списка GUID от предыдущего заключается в том, что счетчик экземпляров равен 3 вместо 1. Список сообщает, что в системе будут присутствовать три экземпляра блока данных WMI42, причем каждый
Драйверы WDM и WMI
485
экземпляр будет содержать собственные значения трех свойств (InstanceName, Active и TheAnswer), принадлежащих этому блоку.
Если количество экземпляров изменяется со временем, вы можете вызвать функцию loWmiRegistrationControl с кодом WMIREG_ACTION_UPDATE_GUID — это заставит систему отправить новый регистрационный запрос, который обрабатывается с использованием обновленной копии структуры WMILIB_CONTEXT. Кстати говоря, если вы собираетесь изменять регистрационную информацию, память для структуры WMILIB_CONTEXT и список GUID лучше выделить из свободного пула (вместо использования статических переменных).
Если бы код пользовательского режима провел перечисление всех экземпляров GUID_WMI42_SCHEMA, он обнаружил бы три экземпляра. Однако для кода пользовательского режима картина получилась бы довольно запутанной. В зависимости от платформы WMI было бы трудно заранее сказать, что три экземпляра, найденные в результате перечисления, принадлежат одному устройству, — в отличие от ситуации, при которой каждое из трех устройств WMI42 предоставляет один экземпляр одного и того же класса. Чтобы клиенты WMI смогли различить эти две ситуации, схема должна включать свойство, используемое в качестве ключа (имя устройства или что-нибудь в этом роде).
Если разрешить возможность создания нескольких экземпляров, вам придется внести изменения в некоторые функции обратного вызова, приводившиеся ранее. В частности:
О функция QueryDataBlock должна быть способна вернуть блок данных для одного экземпляра или для любого количества экземпляров, начиная с заданного индекса;
О функция SetDataBlock должна интерпретировать свой аргумент номера экземпляра и решить, какой из экземпляров следует изменить;
О функция SetDataltem также должна интерпретировать свой аргумент номера экземпляра для поиска экземпляра, к которому принадлежит изменяемый элемент данных.
Eta рис. 10.5 показано, как функция QueryDataBlock использует выходной буфер, когда она должна предоставить более одного экземпляра блока данных. Представьте, что вам потребовалось предоставить данные для двух экземпляров, начиная с номера 2. Данные (которые в данном случае имеют разный размер) копируются в буфер. Каждый экземпляр начинается с 8-байтовой границы. Общее количество байт указывается при завершении запроса, а длины всех отдельных экземпляров задаются в массиве instlength, как показано на рисунке.
Имена экземпляров
Каждый экземпляр класса WMI обладает уникальным именем. Потребители, знающие имя экземпляра, могут выполнять запросы и вызывать методы. Потребители, которые не знают имени (или имен) экземпляров, могут узнать их, выполнив перечисление класса. В любом случае разработчик драйвера отвечает за формирование имен, которые используются или определяются потребителями.
486
Глава 10. WMI
Экземпляры
Массив instleight
length-2
length-3
Рис. 10.5. Получение нескольких экземпляров блока данных
Я показал простейший (с точки зрения драйвера) способ формирования имен экземпляров блока данных: мы требуем, чтобы механизм WMI автоматически генерировал статические уникальные имена на основании имени PDO устройства. Например, если объект PDO устройства обладает именем Root\ SAMPLE\0000, то одному экземпляру блока данных будет присвоено имя Root\ SAMPLE\OOOO_O.
Бесспорно, определение имен экземпляров на базе имени PDO удобно, потому что для этого в драйвере достаточно установить флаг WMIREG_FLAG_ INSTANCE_PDO в переменной flags, передаваемой WMILIB вашей функции обратного вызова QueryReglnfo. Однако автор приложения-потребителя не знает, каким будет это имя, потому что оно изменяется в зависимости от способа установки устройства. Чтобы сделать имена экземпляров чуть более предсказуемыми, вы можете формировать имена объектов с использованием постоянного базового имени. Для этого следует ответить на регистрационный запрос следующим образом:
NTSTATUS QueryRegInfo(PDEVICE_OBJECT fdo, PULONG flags,
PUNICODE_STRING instname, PUNICODE_STRING* regpath.
PUNICODE_STRING resname, PDEVICE_OBJECT* pdo) {
*flags = WMIREG_FLAG_INSTANCE_BASENAME;
*regpath = &servkey:
RtlInitUnicodeStringCresname, L"MofResource");
static WCHAR basenamef] = L"WMIEXTRA";
instname->Buffer - (PWCHAR) ExAllocatePool(PagedPool, sizeof(basename)):
if (!instname->Buffer)
return STATUS-INSUFFICIENTRESOURCES;
Драйверы WDM и WMI
487
instname->MaximumLength - sizeof(basename);
instname->Length = sizeof(basename) - 2;
RtlCopyMemory(i nstname->Buffer, basename, si zeof(basename));
}
Фрагменты функции, отличающиеся от приведенного ранее примера QueryReglnfo, выделены жирным шрифтом. В примере WMIEXTRA каждый блок данных существует в единственном экземпляре, поэтому ему присваивается имя WMIEXTRA без каких-либо дополнений.
Выбрав вариант с базовым именем, постарайтесь избегать слишком общих имен (например, Toys), так как они могут привести к конфликтам. Эта функция спроектирована для того, чтобы вы могли использовать более конкретные имена вида (скажем, TailspinToys).
В некоторых обстоятельствах статические имена экземпляров не удовлетворяют ваших потребностей. Если вы поддерживаете группу часто изменяемых блоков данных, использование статических имен означает, что вам придется запрашивать обновление регистрации при каждом изменении группы. Обновление является относительно дорогостоящей операцией, поэтому запрашивать их слишком часто не стоит. В подобных ситуациях блокам данных вместо статических имен присваиваются динамические имена экземпляров. Имена экземпляров становятся частью запросов и ответов, с которыми вы имеете дело в драйвере. К сожалению, WMILIB не поддерживает динамические имена экземпляров. Это означает,, что для использования данной возможности вам придется полностью реализовать поддержку запросов IRP_MJ__SYSTEM_CONTROL, тогда как обычно WMILIB интерпретирует их за вас. Описание самостоятельной обработки IRP выходит за рамки книги, но в документации DDK подробно рассказано о том, как это делается.
Работа с несколькими классами
В WMI42 используется только один класс блока данных. Если вы хотите поддерживать более одного класса, вам придется увеличить размер массива информационных структур GUID, как это сделано в примере WMIEXTRA:
WMIGUIDREGINFO guidlist[] = {
{tai extra j?vent JUID, 1. WMIREG_FLAG_EVENT_ONLY_GUID}, {taiextra_expensive_GUID. 1. WMIREG_FEAG_EXPENSIVE }. {&wmiextra_method_GUID, 1, 0}.
};
Перед вызовом функций обратного вызова WMILIB ищет в списке код GUID, соответствующий данному IRP. Если GUID в списке отсутствует, WMILIB отклоняет IRP. Если же код GUID будет найден, WMILIB вызывает функцию обратного вызова с параметром guidindex, равным индексу GUID в списке. Проверяя этот параметр, вы узнаете, с каким блоком данных должна выполняться операция.
Вы также можете воспользоваться специальным флагом WMIREG_FLAG_REMOVE_ GUID в информационной структуре GUID. Этот флаг предназначен для исключения конкретных GUID из списка поддерживаемых GUID во время обновления
488
Глава 10. WMI
регистрации. Кроме того, при установке этого флага WMILIB также не сможет обратиться к вам с запросом на выполнение операции с кодом GUID, который вы пытаетесь удалить.
Затратная статистика
Иногда сбор всей статистики, которая может представлять потенциальный интерес для пользователя или администратора, окажется слишком обременительной задачей. Например, драйвер дискового устройства (или, более вероятно, фильтрующий драйвер, находящийся в одном стеке с ним) может собирать данные, показывающие, как часто запросы ввода/вывода обращаются к конкретному сектору диска. Собранная информация окажется полезной для программы дефрагментации диска, потому что часто используемые секторы можно переместить в середину диска для оптимизации времени поиска. Тем не менее, в обычной ситуации собирать эти данные нежелательно из-за больших затрат памяти. К тому же такая память должна быть неперемещаемой, поскольку запрос ввода/вывода может относиться к выгрузке страниц.
WMI позволяет объявить блок данных затратным, чтобы выборка из него осуществлялась только по специальному требованию, как показано в следующем фрагменте примера WMIEXTRA:
WMIGUIDREGINFO guldl1st[] = {
{&wm1extra_expens1ve_GUID, 1, WMIREG_FLAG_EXPENSIVE},
Флаг WMIREG_FLAG_EXPENSIVE указывает, что блок данных, на который ссылается wmiextra_expensive_GUID, обладает особой характеристикой — он является затратным.
Когда приложение проявляет интерес к чтению данных из затратного блока, WMI отправляет драйверу системный управляющий IRP с дополнительным кодом функции IRP_MN_ENABLE_COLLECTION. Если ни одно приложение более не интересуется затратным блоком данных, WMI отправляет другой IRP с дополнительным кодом функции IRP_MN_DISABLE_COELECTION. Если делегировать эти вызовы WMILIB, библиотека выполнит обратный вызов функции FunctionControi для разрешения или запрета выборки значений из блока данных:
NTSTATUS FunctionControi(PDEVICEJ3BJECT fdo. PIRP Irp,
ULONG guldlndex. WMIENABLEDISABLECONTROL fen, BOOLEAN enable) {
return WmlCompleteRequestCfdo, Irp. STATUS_SUCCESS. 0. lOJOJNCREMENT);
}
Аргумент guidindex содержит индекс GUID затратного блока данных в списке GUID. Аргумент fen равен значению перечисляемого типа WmiDataBlockContrc и указывает, разрешается или запрещается сбор затратной статистики. Наконец.
Драйверы WDM и WMI
489
значения TRUE и FALSE аргумента enable указывают, соответственно, следует или нет собирать статистику. Как видно из этого фрагмента, перед возвратом из функции следует вызвать WmiCompleteRequest.
Кстати говоря, приложение «выражает интерес» к блоку данных, получая указатель на интерфейс IWbemClassObject, связанный с конкретным экземпляром класса WMI вашего блока данных. Несмотря на тот факт, что приложение должно определить экземпляр класса, при обратном вызове Functioncontrol индекс экземпляра не указывается. Таким образом, приказ собирать или не собирать затратную статистику относится ко всем экземплярам класса.
События WMI
WMI предоставляет поставщикам возможность оповещать потребителей об интересных или экстренных событиях. Драйвер устройства может использовать эту функцию для предупреждения пользователя о некотором аспекте работы устройства, требующем вмешательства пользователя. Например, драйвер дискового устройства может заметить, что на диске скопилось необычно большое количество поврежденных секторов. Чтобы передать эту информацию в «мир людей», система может создать запись в журнале (см. главу 14), но администратор узнает о ней только в том случае, если он активно просматривает журналы событий. Но если кто-то напишет монитор событий, а ваш драйвер заметит тревожную ситуацию и выдаст событие WMI, это позволит немедленно привлечь внимание пользователя к событию.
ПРИМЕЧАНИЕ----------------------------------------------------------------
Значок сетевого подключения в системной панели реагирует на события WMI, о которых сигнализирует драйвер режима ядра (а именно NDIS.SYS).
События WMI представляют собой обычные классы WMI, используемые особым образом. В синтаксисе MOD блок данных объявляется производным от абстрактного класса WMIEvent, как показано в следующем фрагменте из MOF-файла WMIEXTRA:
[Dynamic, Provider("WMIProv"),
WMI,
Descrjption("Event Info from WMIExtra"),
guid("c4b678f6-b6e9-lld2-bb87-00c04fa330a6"),
local e("MS\\0x409")]
class wmiextra_event : WMIEvent
{
[key, read]
string InstanceName;
[read] boolean Active;
[WmiDatald(l), read] uint32 Eventinfo;
490
Глава 10. WMI
Хотя события могут представлять собой обычные блоки данных, вероятно, вы не захотите разрешать приложениям читать и записывать их по отдельности. В этом случае в объявление GUID включается флаг EVENT-ONLY:
WMIGUIDREGINFO guidl1st[J - {
{&wmiextra_event_GUID, 1,
WMIREG_FLAG_EVENT_ONLY_GUID},
};
Когда приложение выражает интерес к конкретному событию, WMO отправляет драйверу системный управляющий IRP с дополнительным кодом функции IRP_MN_ENABLE_EVENTS. Если ни одно приложение более не интересуется событием, WMI отправляет другой IRP с дополнительным кодом функции IRP-MN_ DISABLE_EVENTS. Если делегировать эти IRP библиотеке WMILIB, управление будет передано вашей функции обратного вызова Functioncontrol с указанием индекса в списке GUID, кода fen WmiEventControl и логического флага enable.
Чтобы инициировать событие, сконструируйте экземпляр класса события в неперемещаемой памяти и вызовите функцию WmiFireEvent. Пример:
Pwmiextra_event junk = (Pwmiextra_event)
ExAllocatePool(NonPagedPool. wmiextra_event^SIZE);
junk->Event!nfo = 42:
WmiFireEvent(fdo, CLPGUID) &wmiextra_event_GUID, 0, sizeof(wmiextra_event). junk):
Подсистема WMI в положенное время освободит память, занимаемую объектом события.
Методы WMI
Помимо механизмов пересылки данных и выдачи событий, в WMI предусмотрен механизм вызова методов, реализованных поставщиками. В примере WMIEXTRA определяется следующий класс, включающий метод:
[Dynamic, Provident"WMIProv"),
WMI,
Description("WMIExtra class with method"). guid("cd7ec27d-b6e9-lld2-bb87-00c04fa330a6"). locale("MS\\0x409")]
class wmiextrajnethod
{
[key, read]
string InstanceName;
[read] boolean Active:
[Implemented, WmiMethodld(l)] void
AnswenMethod([in,out] uint32 TheAnswer);
Драйверы WDM и WMI
491
Это объявление указывает, что AnswerMethod получает аргумент ввода/вывода с именем TheAnswer (32-разрядное целое без знака). Учтите, что все методы, предоставляемые драйверами WDM, должны объявляться с ключевым словом void, потому что для них не существует способа обозначить возвращаемое значение. Впрочем, метод может возвращать информацию в выходных аргументах (или аргументах ввода/вывода).
При делегировании обработки системных управляющих IRP библиотеке WMILIB вызов метода преобразуется в вызов функции обратного вызова Ехе-cuteMethod:
NTSTATUS ExecuteMethod(PDEVICE_OBJECT fdo. PIRP Irp.
ULONG guidindex, ULONG Instindex, ULONG id.
ULONG cblnbuf. ULONG cbOutbuf, PUCHAR buffer)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Devi ceExtensi on;
NTSTATUS status = STATUS-SUCCESS;
ULONG bufused = 0;
if (guidindex != INDEX_WMIEXTRA_METHOD) return WmiCompleteRequest(fdo. Irp.
STATUS_WMI_GUID_NOT_FOUND, 0, IO_NO_INCREMENT);
if (instindex != 0)
return WmiCompleteRequest(fdo, Irp, STATUS_WMI_INSTANCE_NOT_FOUND, 0. IO_NO_INCREMENT);
if (id != AnswerMethod)
return WmiCompleteRequest(fdo. Irp.
STATUS_WMI_ITEMID_NOT_FOUND, 0. IO_NO_INCREMENT);
if (cblnbuf < AnswerMethod_IN_SIZE)
status = STATUS_INVALID_PARAMETER;
else if (cbOutbuf < AnswerMethod_OUT_SIZE)
status = STATUS-BUFFER-TOO-SMALL;
else
{
PAnswerMethod_IN in = (PAnswerMethod_IN) buffer;
PAnswerMethod_OUT out = (PAnswerMethod_OUT) buffer;
out->TheAnswer = in->TheAnswer + 1;
bufused = AnswerMethod_OUT-SIZE;
}
return WmiCompleteRequest(fdo, Irp. status, bufused.
IO_NO_INCREMENT);
}
При вызовах методов WMI используется входной класс, содержащий входные аргументы, и (возможно, другой) выходной класс, содержащий возвращаемые значения. В блоке buffer содержится образ входного класса, длина которого составляет cblnbuf. Ваша задача — выполнить метод и перезаписать буфер образом выходного класса. При завершении запроса указывается размер в байтах (bufused) выходного класса.
492
Глава 10. WMI
В приведенном примере метод просто увеличивает входной аргумент на 1.
Простое перечисление экземпляра класса (такого как wmiextra_method) инициирует запрос блока данных. Запрос должен завершиться успешно, даже если класс, содержащий метод, не содержит полей данных. В таких случаях запрос просто завершается с нулевой длиной данных.
Будьте крайне осмотрительны в отношении функциональности, доступ к которой предоставляется через методы WM1. Этот совет особенно важен, потому что сценарии, поступившие из непроверенных источников, могут выполняться с правами любого пользователя, вошедшего в систему, а следовательно, могут вызывать ваши методы.
Стандартные блоки данных
Компания Microsoft определила некоторые стандартизированные блоки данных для различных типов устройств (табл. 10.4). Если устройство принадлежит классу, для которого определены стандартизированные блоки, обеспечьте поддержку этих блоков в своем драйвере. За определениями классов обращайтесь к файлу WMICORE.MOF в DDK.
Таблица 10.4. Стандартные блоки данных
Тип устройства	Стандартный класс	Описание
Клавиатура	MSKeyboard._PortInformation	Данные конфигурации и производительности
	MSKeyboard_ExtendedID	Расширенные идентификаторы типа и подтипа
	MSKeyboarcLCIassInformation	Идентификационный номер устройства
Мышь	MSMouse_PortInformation	Данные конфигурации и производительности
	MSMouse_ClassInformation	Идентификационный номер устройства
Диск	MSDiskDriverJSeometry	Форматные данные
	MSDiskDriver_Performance	Данные производительности и внутреннее имя устройства
	MSDiskDriver__PerformanceData	Только данные производительности
Запоминающее устройство	MSStorageDriver_FailurePredictStatus	Проверка прогнозирования сбоя
	MSStorageDriver_FailurePredictData	Данные прогнозирования сбоев
	MSStorageDriver_FailurePredictEvent	Событие, выдаваемое при прогнозировании сбоя
	MSStorageDriver_FailurePredictFunction	Методы, связанные с прогнозированием сбоев
	MSStorageDriver_ATAPISmartData	Данные прогнозирования сбоев ATAPI
ctlpar	MSStorageDriver_FailurePredictThresholds	Специфическая информация производителя
Драйверы WDM и WMI
493
Тип устройства	Стандартный класс	Описание
	MSStorageDriverJScsilnfoExceptions	Флаги и ключи, связанные с выдачей информационных исключений
Последовательный порт	MSSerial_PortName	Имя порта
	MSSeriaLCommlnfo	Коммуникационные параметры
	MSSerial_HardwareConfiguration	Информация о ресурсах ввода/вывода
	MSSerial_PerformanceInformation	Информация производительности
	MSSena!_CommProperties	Коммуникационные параметры
Параллельный порт	MSParalleLAIIocFreeCounts	Счетчики операций выделения и освобождения
	MSParallel_DeviceBytesTransferred	Счетчики пересылки
IDE	M SId export Devi cel nfo	Псевдоидентификация SCSI для порта IDE
Redbook	MSRedbook-Driverlnfornnation	Конфигурационные данные устройства, использующего аудиостандарт Redbook
	MSRedbook_Performance	Данные производительности для аудиодрайвера Redbook
Стример	MSTapeDri vePara m	Информация о функциях стримера
	MSTapeMediaCapacity	Информация о текущем носителе
	MSTapeSymbolicName	Символическое имя (например, ТареО)
	MSTapeDriveProblemEvent	Событие, используемое для оповещения о проблемах
	MSTapeProblemloError	Статистика ошибок ввода/вывода
	MSTapeProblemDeviceError	Сводная информация о проблемах
Чейнджер	MSChangerParameters	Информация о функциях CD-чейнджера
	MSChangerProblemEvent	Событие, используемое для оповещения о проблемах
	MSChangerProblemDeviceError	Сводная информация о проблемах
Все типы устройств	MSPower_DeviceEnable	Управление автоматическим отключением устройства
par	MSPower_DeviceWakeEnable	Разрешение или запрет функции пробуждения системы
	MSDeviceUIJ=irmwareRevision	Версия «прошивки» устройства
Чтобы реализовать поддержку стандартного блока данных, включите соответствующий код GUID в список, передаваемый по регистрационному запросу. Реализуйте код поддержки чтения и записи данных, разрешения и запрета
494
Глава 10. WMI
событий и т. д.» используя уже описанные методы. Не включайте определения стандартных блоков данных в свою схему — определения классов уже находятся в репозитории, и переопределять их не стоит.
Включив заголовочный файл DDK WMIDATA.H в драйвер, вы получите доступ к определениям GUID и структурам классов.
Кстати, во многих случаях драйвер класса от компании Microsoft предоставляет поддержку WM1 для стандартных классов - возможно, вам вообще ничего не придется делать.
Проблемы совместимости с Windows 98/Ме
Поскольку хорошо спроектированный драйвер должен поддерживать WMI, а библиотека WMILIB отсутствует в исходной версии Windows 98, возможно, вам придется предоставить заглушку VxD для функций WMILIB, чтобы драйвер нормально загружался. За дополнительной информацией о заглушках VxD обращайтесь к приложению А. (Фильтрующий драйвер VDMSTUB, описанный в приложении, не включает функции WMILIB, но в приложении рассказано, как это можно исправить.)
Исходная версия Windows 98 содержала ряд ошибок, влиявших на поддержку WML В последующих обновлениях Windows 98 (Second Edition и Service Pack 1) ошибки были исправлены. Впрочем, даже после этого поддержка WMI не устанавливается в ходе стандартной процедуры установки системы. Чтобы установить ее, запустите приложение Установка и удаление программ (Add/Remove Programs) из панели управления, перейдите на вкладку Установка компонентов Windows (Windows Setup) и запросите установку Web-Based Enterprise Mgmt в категории Средства Интернета (Internet Tools).
1 Контроллеры и м ногофу н кциона л ьн ые устройства
Две категории устройств недостаточно хорошо укладываются в иерархию РпР, описанную в главе 6. К первой категории относятся контроллеры, управляющие группой дочерних устройств, а ко второй — многофункциональные устройства, совмещающие ряд функций на одной карте. У таких устройств имеется одна общая особенность: правильное управление ими требует создания нескольких объектов устройств с независимыми ресурсам и ввода/вывода.
В Windows ХР очень легко обеспечить поддержку многофункциональных устройств PCI (Peripheral Component Interconnect), PCMCIA (Personal Computer Memory Card International Association) и USB, отвечающих соответствующим стандартам шин. Драйвер шины PCI автоматически распознает многофункциональные карты PCI. Для многофункциональных устройств PCMCIA в DDK приведены подробные инструкции по поводу назначения MF.SYS функциональным драйвером многофункциональной карты; MF.SYS перечисляет функции карты, вследствие чего PnP Manager загружает отдельные функциональные драйверы. Драйвер USB Generic Parent обычно загружает отдельные функциональные драйверы для каждого интерфейса устройства.
За исключением USB, в исходной версии Windows 98 отсутствовала поддержка многофункциональности, предоставляемая Windows ХР. В Windows 98/Ме для работы с контроллером или многофункциональным устройством или для обслуживания нестандартных устройств приходилось прибегать к более героическим мерам. Вы должны были предоставить функциональный драйвер для главного устройства, а также отдельные функциональные драйверы для всех дочерних устройств, подключавшихся к главному устройству. Функциональный драйвер главного устройства работал как миниатюрный драйвер шины: он перечислял дочерние устройства и обеспечивал обработку по умолчанию для запросов РпР и управления питанием. Написание полноценного драйвера шины — дело серьезное, и я не буду даже пытаться описывать здесь этот процесс. Тем не менее я опишу базовые механизмы, используемые для перечисления дочерних устройств. Эта информация позволит вам писать драйверы контроллеров и многофункциональных устройств, не укладывающиеся в стандартные рамки Microsoft.
496
Глава 11. Контроллеры и многофункциональные устройства
Общая архитектура
В главе 2 на рис. 2.6 изображена топология объектов устройств для случая, когда родительское устройство (такое как драйвер шины) обладает дочерними устройствами. Контроллеры и многофункциональные устройства используют аналогичную топологию. Родительское устройство подключается к стандартной шине. Драйвер стандартной шины распознает родительское устройство, а РпР Manager настраивает его, как любое обычное устройство, — до определенного момента. После запуска родительского устройства PnP Manager отправляет запрос Plug and Play с дополнительным кодом функции IRP_MN_QUERY_DEVICE_ RELATIONS, чтобы узнать о так называемых шинных связях родительского устройства. На самом деле запрос выполняется для всех устройств, потому что РпР Manager еще не знает, что у устройства есть дочерние устройства.
В ответ на запрос о шинных связях функциональный драйвер родительского устройства находит или создает дополнительные объекты устройств. Каждый из этих объектов становится объектом физического устройства (PDO) в нижней позиции стека одного из дочерних устройств. PnP Manager переходит к загрузке функциональных и фильтрующих драйверов дочерних устройств, так возникает иерархия, показанная на рис. 2.6.
Драйвер родительского устройства должен играть две роли. В одной роли он является драйвером функционального объекта устройства (FDO) для контроллера или многофункционального устройства. В другой роли он является драйвером PDO для дочерних устройств. В роли FDO он обрабатывает запросы РпР и управления питанием так, как они обычно обрабатываются функциональными драйверами. Однако в роли PDO он выполняет функции «драйвера последней инстанции» для запросов РпР и управления питанием.
Объекты дочерних устройств
Драйвер родительского многофункционального устройства отвечает за создание объектов PDO для своих дочерних устройств. Существуют два основных решения этой задачи:
О драйвер шины или устройства с возможностью оперативного подключения дочерних устройств ведет список PDO, который обновляется каждый раз. когда PnP Manager отправляет новый запрос шинных связей. Такой драйвер производит перечисление оборудования для обнаружения состава устройств, создает новые PDO для вновь появившихся устройств и уничтожает PDO устройств, которые перестали существовать;
О драйвер устройства с фиксированным количеством дочерних функций создает свой список PDO на как можно более ранней стадии и выдает его каждый раз, когда PnP Manager обращается с запросом на получение шинных связей. Объект PDO уничтожается одновременно с уничтожением своего FDO.
Общая архитектура
497
Структуры расширений устройств
Небольшое затруднение для многофункциональных драйверов заключается в том, что как FDO, так и все PDO принадлежат одному объекту драйвера. Это означает, что пакеты запросов ввода/вывода (IRP), направленные любому из этих объектов устройств, поступают одной группе диспетчерских функций. Драйвер должен обрабатывать запросы РпР и управления питанием по-разному для FDO и PDO. Соответственно, вы должны позаботиться о том, чтобы диспетчерская функция могла легко отличать FDO от одного из дочерних PDO. В одном из способов определяются две структуры расширения устройства с общим началом:
// Расширение FDO:
typedef struct _DEVICE_EXTENSION {
ULONG flags;
} DEVICE-EXTENSION, *PDEVICEJXTENSION:
// Расширение PDO:
typedef struct _PDO_EXTENSION {
ULONG flags;
} PDOJXTENSION, *PPDO_EXTENSION;
// Общая часть:
typedef struct _COMMON__EXTENSION {
ULONG flags:
} COMMON-EXTENSION, *PCOMMON_EXTENSION;
#define ISPDO 0x00000001
Диспетчерские функции в драйвере будут выглядеть так:
NTSTATUS DispatchSomething(PDEVICE_OBJECT DevIceObject, PIRP Irp)
{
PCOMMON-EXTENSION pcx =
(PCOMMON_EXTENSION) DeviceObject->DeviceExtension;
if (pcx->flags & ISPDO)
return DispatchSomethingPdo(Deviceobject, Irp): else
return DispatchSomethingFdo(DeviceObject, Irp):
}
To есть вы различаете роли FDO и PDO, анализируя заголовок, общий для обоих типов расширений устройств, а затем вызываете FDO- или PDO-функцию для обработки IRP.
498
Глава 11. Контроллеры и многофункциональные устройства
Пример создания дочерних объектов устройств
Пример MULFUNC из прилагаемых материалов представляет собой крайне при-митивное многофункциональное устройство: он имеет два дочерних устройства, и мы всегда знаем, какие это устройства. Я назвал их А и В. MULFUNC выполняет следующий код (с большим объемом проверки ошибок, чем показано в тексте) во время обработки IRP_MN__START_DEVICE для создания PDO объектов А и В:
NTSTATUS StartDev1ce(PDEVICE_OBJECT fdo, ...) {
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
CreateCh11d(pdx, CHILDTYPEA, Spdx->Ch11dA);	// 1
CreateCh11d(pdx, CHILDTYPEB, Spdx->Ch11dB); return STATUS_SUCCESS;
}
NTSTATUS CreateChild(PDEVICE_EXTENSION pdx, ULONG flags. PDEVICE-OBJECT* ppdo) { PDEVICE_OBJECT child;
IoCreateDev1ce(pdx->Dr1verObject, s1zeof(PDO_EXTENSION),	// 2
NULL, FILE_DEVICE__UNKNOWN, FILE_AUTOGENERATED_DEVICE_NAME, FALSE, Schild);
PPDO_EXTENSION px = (PPDO_EXTENSION) ch11d->Dev1ceExtens1on; px->flags = ISPDO flags;
px->Dev1ce0bject = child;
px->Fdo = pdx->Dev1ce0bject;
child->Flags S= -DO_DEVICEJNITIALIZING;
*ppdo = child;
return STATUS_SUCCESS; }
1. CHILDTYPEA и CHILDTYPEB — дополнительные флаговые биты для поля flags, с которого начинается общее расширение устройства. Если бы вы писали настоящий драйвер шины, то не стали бы создавать здесь дочерние PDO — вместо этого следовало бы произвести перечисление оборудования в ответ на запрос IRP_MN_QUERY_DEVICE_RELATIONS и создать PDO по его результатам.
2. Мы создаем именованный объект устройства, но просим систему автоматически сгенерировать его имя, устанавливая флаг FILE_AUTOGENERATED_DEVICE_NAME в аргументе Devicecharacteristics.
Конечным результатом процесса создания являются два указателя на объекты устройств (ChildA и ChildB) в расширении FDO родительского устройства.
Обработка запросов РпР
Драйвер контроллера или многофункционального родительского устройства содержит две диспетчерские подфункции для запросов IRP_MJ_PNP — одну для обработки запросов, полученных в роли FDO, а другую для обработки запросов,
Обработка запросов РпР
499
полученных в роли PDO. В табл. 11.1 указаны действия, предпринимаемые родительским драйвером для каждого типа запроса РпР в этих двух ролях. Смысл столбца «Голосование родителя» объясняется позже.
Таблица 11.1. Обработка запросов РпР родительским драйвером
Запрос РпР	Роль FDO	Роль PDO	Голосование родителя
IRP_MN_START_DEVICE	Обычная	Успешная	—
IRP_MN_QUERY_REMOVE_DEVICE	Обычная	Успешная	
IRP_MN_REMOVE_DEVICE	Обычная	Успешная	—
IRP_MN_CANCEL_REMOVE_DEVICE	Обычная	Успешная	__
IRP_MN_STOP_DEVICE	Обычная	Успешная	
IRP_MN_QUERY_STOP_DEVICE	Обычная	Успешная	
IRP_MN_CANCEL_STOP_DEVICE	Обычная	Успешная	
IRP_MN__QUERY_DEVICE_RELATIONS	Специальная обработка для запроса BusRelations; в других случаях обычная	Специальная обработка для запроса TargetRelations; в других случаях игнорируется	Нет
IRP_MN_QUERYJNTERFACE	Обычная	Специальная	Нет
IRP_MN_QUERY_CAPABILITIES	Обычная	Делегирование	Нет
IRP_MN_QUERY_RESOURCES	Обычная	Успешная	—
IRP_MN_QUERY„RESOURCE_ REQUIREMENTS	Обычная	Успешная	—
IRPJMN„QUERY_DEVICE_TEXT	Обычная	Успешная	—
IRP_MN_FILTER_RESOURCE_ REQUIREMENTS	Обычная	Успешная	—
IRP_MN_READ_CONFIG	Обычная	Делегирование	Да
irp_mn„write_config	Обычная	Делегирование	Да
IRP„MN_EJECT	Обычная	Делегирование	Да
IRP_MN_SETJ_OCK	Обычная	Делегирование	Да
IRP_MN_QUERYJD	Обычная	Специальная обработка	—
IRP_MN_QUERY_PNP_DEVICE „STATE	Обычная	Специальная обработка	Нет
IRP_MN__QUERY_BUS_INFORMATION	Обычная	Делегирование	Да
IRP_MN_DEVICE„USAGE„ NOTIFICATION	Обычная	Делегирование	Нет
IRP„MN_SURPRISE_REMOVAL	Обычная	Успешная	—
IRP_MN_QUERY_LEGACY_ BUSJNFORMATION	Обычная	Делегирование	Да
Все остальные	Обычная	Игнорирование	Да
500
Глава 11. Контроллеры и многофункциональные устройства
В таблице используются сокращенные обозначения действий:
О Обычная означает «обычную обработку для функционального драйвера». Другими словами, в роли FDO родительский драйвер обрабатывает практически все запросы РпР так, как это должен делать функциональный драйвер (область ответственности функционального драйвера подробно рассматривается в главе 6). Например, все запросы передаются вниз по стеку родительского устройства, кроме отклоняемых, в ответ на IRP_MN_START_DEVICE производится настройка конфигурации устройства и т. д.
□ Успешная означает, что IRP завершается с кодом STATUS_SUCCESS.
О Игнорирование означает, что IRP завершается с состоянием, уже указанным в поле IRP loStatus.
О Делегирование означает, что IRP повторяется в стеке FDO родительского устройства и возвращает те же результаты в стек PDO.
Механика этих действий будет описана в нескольких ближайших разделах настоящей главы.
Передача информации о дочерних устройствах PnP Manager
РпР Manager запрашивает информацию о дочерних устройствах каждого устройства, отправляя запрос IRP_MN_QUERY_DEVICE_RELATIONS с кодом типа BusRelations. В роли FDO родительский драйвер отвечает на этот запрос следующим образом:
NTSTATUS HandleQueryRelat1ons(PDEVICE_0BJECT fdo. PIRP Irp)
PDEVICE-EXTENSION pdx = ...:
PIO_STACKJ_OCATION stack = ...;
if (stack^Parameters.QueryDeviceRelatlons.Type != BusRelations) // 1
return DefaultPnpHandler(fdo, Irp):
PDEVICERELATIONS newrel = (PDEVICE_RELATIONS)	// 2
ExAllocatePooKPagedPool, sizeof(DEVICE-RELATIONS)
+ sizeof(PDEVICE_OBJECT));
newrel->Count = 2;
newrel->0bjects[0] = pdx->Ch11dA:
newrel->Objects[l] - pdx->Ch11dB;
ObReferenceObject(pdx->Ch11dA);	// 3
ObReferenceObject(pdx->Ch1IdB):
Irp->IoStatus.Information = (ULONG_PTR) newrel:	// 4
Irp->IoStatus.Status = STATUS-SUCCESS;
return DefaultPnpHandler(fdo, Irp):
}
1.	IRP может затрагивать несколько типов связей, помимо шинных связей, которые нас интересуют в данный момент. Мы просто делегируем другие запросы драйверу нижележащей аппаратной шины.
Обработка запросов РпР
501
2.	В этой точке выделяется структура, содержащая два указателя на объекты устройств. Структура DEVICE_RELATIONS находится в массиве с размерностью 1, поэтому при вычислении объема выделяемой памяти необходимо только прибавить размер дополнительного указателя.
3.	Вызов функции ObReferenceObject увеличивает счетчики ссылок, связанные с каждым из объектов устройства, помещаемых в массив DEVICE_RELATIONS. PnP Manager освободит ссылки на объекты в положенное время.
4.	Запрос необходимо передать вниз на тот случай, если «настоящий» драйвер шины или фильтр нижнего уровня располагает какой-то информацией, нам не известной. Этот IRP использует необычный протокол передачи вниз и завершения. Если вы обрабатываете IRP, поле loStatus задается так, как здесь показано, в противном случае loStatus остается без изменений. Обратите внимание на использование поля Information для хранения указателя на структуру DEVICE_RELATIONS. В других ситуациях, встречавшихся в книге, в поле Information всегда хранилось число.
В приведенном фрагменте я обошел еще одно возможное затруднение: фильтр верхнего уровня мог уже занести список объектов устройств в поле IRP loStatus. Information. Мы не должны потерять этот список, более того, мы должны расширить его и добавить два своих указателя на объекты устройств.
PnP Manager автоматически отправляет запрос на получение информации о шинных связях при запуске. Для форсированной отправки этого запроса вызовите следующую сервисную функцию:
lolnval1dateDeviceRelat1ons(pdx->Pdo, BusRela11ons);
Драйвер шины с возможностью оперативного подключения использует эту функцию при обнаружении появления или исчезновения дочернего устройства. Для драйвера контроллера или многофункционального устройства с фиксированным набором дочерних устройств этот вызов не обязателен.
Обработка запросов РпР в роли PDO
В этом разделе объясняется содержимое столбца «Роль PDO» в табл. 11.1. Из главы 6 и предыдущего раздела вы уже знаете, как обрабатывать IRP в роли FDO.
Успешное завершение
Многие PnP IRP просто завершаются родителем с кодом успеха без какой-либо дополнительной обработки:
NTSTATUS SucceedRequest(PDEVICE_OBJECT pdo. PIRP Irp)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
loCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUSJUCCESS:
}
Единственное, на что стоит обратить внимание в этой короткой функции, — это то, что она не изменяет поле IRP loStatus.Information. PnP Manager всегда
502
Глава 11. Контроллеры и многофункциональные устройства
инициализирует это поле некоторым образом перед запуском IRP. В некоторых случаях поле может изменяться фильтрующим или функциональным драйвером и в него может помещаться указатель на некоторую структуру данных. Драйверу PD О не следует изменять это поле.
Игнорирование
Некоторые IRP могут игнорироваться родительским драйвером. Игнорирование IRP напоминает его отклонение с кодом ошибки, если не считать того, что драйвер не изменяет поля состояния IRP:
NTSTATUS IgnoreRequest(PDEVICE_OBJECT pdo, PIRP Irp)
NTSTATUS status = Irp->IoStatus.Status: loCompleteRequest(Irp, IO_NO_INCREMENT): return status: }
Разумеется, IRP завершается с теми значениями, которые к настоящему моменту находятся в полях loStatus.Status и loStatus.Information. Такая стратегия базируется на том, что при создании запроса РпР эти поля инициализируются значениями STATUS_NOT_SUPPORTED и 0 соответственно. PnP Manager черпает информацию из того факта, что IRP завершается с теми же значениями, то есть ни один из драйверов в стеке ничего не сделал с IRP. В описаниях функционального и фильтрующего драйверов в DDK сказано, что драйвер, обрабатывающий некоторые типы IRP, должен изменить статус па STATUS„SUCCESS перед тем, как передавать IRP вниз по стеку. Эти инструкции соответствуют работе некоторых драйверов шин Microsoft, а также драйверов контроллеров и многофункциональных устройств, построенных по шаблону, описанному в этой главе.
Делегирование
Родительский драйвер может просто делегировать некоторые запросы РпР «настоящему» драйверу шины, расположенному ниже уровня FDO родительского устройства. Делегирование в данном случае не сводится к простому вызову loCallDriver, потому что к моменту получения IRP драйвером PDO стек ввода/ вывода обычно уже исчерпан. Следовательно, мы должны создать IRP, который я называю повторяющим, и отправить его в стек драйверов, занимаемый драйвером в роли FDO:
NTSTATUS RepeatRequestCPDEVICE_OBJECT pdo, PIRP Irp)
PPDO_EXTENSION pdx = (PPDOEXTENSION) pdo->DeviceExtension:
PDEVICE_OBJECT fdo = pdx->Fdo:
PDEVICE-EXTENSION pfx =
(PDEVICE-EXTENSION) fdo->DeviceExtension;
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp):
PDEVICE_OBJECT tdo = loGetAttachedDeviceReference(fdo):	// 1
PIRP subirp = IoAllocateIrp(tdo->StackSize + 1, FALSE);	// 2
Обработка запросов РпР
503
PIO_STACK_LOCATION substack = loGetNextIrpStackLocatlon(sublrp);
substack->DeviceObject - tdo;
substack->Parameters.Others.Argumentl = (PVOID) Irp;
loSetNextlrpStackLocation(subirp);	// 3
substack = loGetNextlrpStackLocatlon(subirp);
RtlCopyMemory(substack, stack,
FIELD_OFFSET(IO_STACK_LOCATION. CompletionRoutlne));
substack->Control = 0;
BOOLEAN needsvote = <потом объясню>;	// 4
loSetCompletlonRout1ne(sublrp, OnRepeaterComplete,
(PVOID) needsvote, TRUE, TRUE, TRUE);
sublrp->IoStatus.Status = STATUS_NOT_SUPPORTED:	// 5
loMarklrpPendlng(Irp);
IoCallDriver(tdo, sublrp);
return STATUS__PENDING
}
NTSTATUS OnRepeaterComplete(PDEVICE_OBJECT tdo, PIRP sublrp, PVOID needsvote) {
ObDereferenceObject(tdo);	// 6
PIO_STACK_LOCATION substack =
IoGetCurrent IrpStackLocatlon(sublrp);
PIRP Irp = (PIRP) substack->Parameters.Others.Argument!;	// 7
if (sublrp->IoStatus.Status === STATUS_NOT_SUPPORTED)	// 8
{
if (needsvote)
Irp->IoStatus.Status = STATUS^UNSUCCESSFUL;
}
else
Irp->IoStatus = subirp->IoStatus;
loFreelrp(subirp);	//	9
loCompleteRequest(Irp, IO_NO_INCREMENT);	//	10
return STATUS_MORE_PROCESSING_REQUIRED;	//	11
1.	Повторяющий IRP будет отправлен верхнему фильтрующему драйверу в стеке, к которому принадлежит наш объект FDO. Эта функция возвращает адрес верхнего объекта устройства, а также создает дополнительную ссылку на объект, чтобы Object Manager не удалил объект во время его использования.
2.	При создании IRP мы создаем в стеке дополнительный элемент для хранения контекстной информации для функции завершения, которую мы собираемся установить. Указатель DeviceObject, помещаемый в этот дополнительный элемент, становится первым аргументом функции завершения.
3.	Здесь мы инициализируем первый «настоящий» элемент стека, занимаемый верхним драйвером в стеке FDO, а затем устанавливаем свою функцию завершения. Это один из тех случаев, когда мы не можем использовать
504
Глава 11. Контроллеры и многофункциональные устройства
стандартный макрос loCopyCurrentlrpStackLocationToNext для копирования элемента стека: мы имеем дело с двумя разными стеками ввода/вывода.
4.	Необходимо заранее запланировать, что мы будем делать в том случае, если стек родительского устройства не производит фактической обработки повторяющего IRP. Последующие действия зависят от того, какая именно дополнительная функция IRP повторяется. Формально мы вычисляем логическую величину (я называю ее needsvote) и передаем ее в контекстном аргументе нашей функции завершения.
5.	Поле состояния нового PnP IRP всегда инициализируется специальным значением STATU S_NOT_S UP PORTED. Если этого не сделать, Driver Verifier выдает фатальный сбой.
6.	Команда освобождает ссылку на верхний объект устройства в стеке FDO.
7.	Мы сохраняем адрес исходного IRP.
8.	В этом коротком фрагменте задается состояние завершения исходного IRP.
О том, что здесь происходит, рассказано далее в основном тексте.
9.	Мы создали повторяющий IRP, теперь его нужно уничтожить.
10.	Теперь, когда стек драйвера FDO обработал копию, исходный IRP можно завершить.
И. Необходимо вернуть STATUS_MORE_PROCESSING_REQUIRED, потому что IRP, завершение которого мы обрабатывали, — повторяющий IRP — был удален.
В предыдущем коде решается довольно сложная проблема, относящаяся к различным PnP IRP, которые родительский драйвер повторяет в своем стеке FDO. PnP Manager инициализирует РпР IRP кодом STATUS_NOT_SUPPORTED. Проверяя итоговое состояние, он может сказать, обработал ли какой-либо драйвер один из этих IRP. Если IRP завершается с кодом STATUS_NOT_SUPPORTED, РпР Manager может сделать вывод, что ни один драйвер ничего не сделал с IRP. Если же IRP завершается с любым другим состоянием, РпР Manager знает, что какой-то драйвер осознанно завершил IRP с кодом успеха или неудачи, но не проигнорировал его.
Драйвер, создавший PnP IRP, должен соблюдать эту схему и инициализировать loStatus.Status значением STATUS_NOT_SUPPORTED. Как я уже говорил, если вы забудете это сделать, Driver Verifier выдаст фатальный сбой. Но при такой инициализации возникает проблема*, допустим, один из драйверов дочернего стека (то есть над PDO дочернего устройства) заменит loStatus.Status другим значением, прежде чем передать конкретный IRP вниз нашему драйверу в роли PDO. Мы создаем повторяющий IRP, инициализируем его значением STATUS_NOT„ SUPPORTED и передаем вниз по родительскому стеку (то есть стеку, к которому наш драйвер принадлежит в своей роли FDO). Если повторяющийся IRP завершится с кодом STATUS_NOT_SUPPORTED, какое состояние следует использовать для завершения исходного IRP? Это не должно быть состояние STATUS_NOT_ SUPPORTED, потому что оно подразумевает, что ни один из драйверов дочернего стека IRP не обрабатывал (но это произошло, и состояние основного IRP изменилось). Именно здесь нам понадобится флаг needsvote.
Обработка запросов РпР
505
Для некоторых из повторяемых IRP нас не интересует, выполняет ли родительский драйвер фактическую обработку IRP. Мы говорим (вернее, разработчики Microsoft говорят), что родительские драйверы не обязаны «голосовать» за IRP. Если внимательно присмотреться к OnRepeaterComplete, мы видим, что итоговое состояние основного IRP в этом случае не изменяется. Для других повторяемых IRP мы не сможем предоставить настоящий ответ, если драйверы родительского стека проигнорируют IRP. Для таких IRP, за которые должен «голосовать» родитель, основной IRP отклоняется с кодом STATUS-UNSUCCESSFUL. Чтобы узнать, какие IRP принадлежат к классу «обязательного голосования», а какие нет, обратитесь к последнему столбцу табл. 11.1. Кстати, дополнительные функции, для которых в таблице стоит прочерк («—»), вообще не должны повторяться родительским драйвером в родительском стеке.
Но если один из родительских драйверов все же обрабатывает повторяющий IRP, мы копируем все поле loStatus, включающее значения Status и Information, в основной IRP. Поле Information может содержать ответ на запрос, и этап копирования обеспечивает передачу ответа наверх.
В RepeatRequest присутствует еще одна тонкость — я помечаю IRP как незавершенный и возвращаю STATUS-PENDING. Большинство PnP IRP завершается синхронно, поэтому вызов loCallDriver с наибольшей вероятностью приводит к немедленному завершению IRP. Так зачем помечать IRP незавершенным и создавать I/O Manager лишние сложности в виде необходимости планирования асинхронного вызова (АРС) как части завершения основного IRP? Дело в том, что если диспетчерская функция не возвращает STATUS-PENDING (вспомните, что RepeatRequest выполняется как подфункция по отношению к диспетчерской функции IRP__MJ_PNP), мы должны вернуть в точности такое же значение, которое было использовано при завершении IRP. Однако только функция завершения после проверки STATUS_NOT_SUPPORTED и флага needsvote знает, каким будет это значение; не существует более удобного механизма, которым бы она могла сообщить о своем решении диспетчерской функции.
Обработка удаления устройств
PnP Manager знает об отношениях «родитель—потомок» между родительским FDO и дочерними PDO. Соответственно, когда пользователь удаляет родительское устройство, PnP Manager автоматически удаляет все дочерние устройства. Однако, как ни странно, родительский драйвер обычно не должен удалять дочерние PDO при получении запроса IRP_MN_REMOVE_DEVICE. PnP Manager ожидает, что PDO продолжат существование до того момента, когда будет отключено базовое оборудование. По этой причине многофункциональный драйвер не удаляет дочерние PDO, пока не получит приказ на удаление родительского FDO. С другой стороны, драйвер шины для устройства с оперативным подключением удаляет дочерние PDO при получении запроса IRP_MN_REMOVE_DEVICE, не обнаружив устройство в процессе перечисления.
506
Глава 11. Контроллеры и многофункциональные устройства
Обработка IRP_MN_QUERY_ID
Из всех запросов РпР, обрабатываемых родительским драйвером, самым важным является IRP_MN_QUERY_ID. PnP Manager выдает этот запрос в нескольких формах для определения идентификаторов, которые будут использоваться для обнаружения INF-файла дочернего устройства. В ответ на него вы возвращаете (в loStatus.Information) значение MULTI_SZ с необходимыми идентификаторами устройств. В примере MULFUNC устройство содержит два дочерних устройства с (фиктивными) идентификаторами *WCO1104 и *WCO1105. Запрос в нем обрабатывается следующим образом:
NTSTATUS HandleQueryId(PDEVICE_OBJECT pdo, PIRP Irp)
{
PPDO__EXTENSION pdx = (PPDO_EXTENSION) pdo->DeviceExtension;
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
PWCHAR Idstring;
switch (stack ^Parameters.QueryId.IdType)
{
case BusQuerylnstancelD:	// 1
idstring = L"0000";
break;
case BusQueryDevicelD:	// 2
If (pdx->flags & CHILDTYPEA)
idstring = LDRIVERNAME L'AWCOIW’;
else
Idstring = LDRIVERNAME L^\*WC01105M; break;
case BusQueryHardwarelDs:	// 3
if (pdx->flags & CHILDTYPEA)
idstring = L’^WCOIW;
else
idstring - L"*WC01105";
break;
default;
return CompleteRequesttlrp, STATUS_NOT_SUPPORTED, 0);
}
ULONG nchars = wcslen(idstring);
ULONG size = (nchars + 2) * sizeof(WCHAR);
PWCHAR id = (PWCHAR) ExAllocatePool(PagedPool, size);
wcscpydd, idstring);
id[nchars + 1] = 0;
return CompleteRequestCIrp, STATUS-SUCCESS, (ULONG_PTR) id);
}
1.	Идентификатор экземпляра представляет собой строковое значение, однозначно идентифицирующее устройство конкретного типа на шине. Константы вида «0000» не подходят, если на компьютере может устанавливаться несколько устройств родительского типа.
Обработка запросов РпР
507
2.	Идентификатор устройства представляет собой строку из двух компонентов в формате «перечислитель\тип>> и, фактически, определяет имя раздела оборудования в реестре. Например, для нашего устройства СЫНА он будет равен ...\Enum\Mulfunc\*WC01104\0000.
3.	Идентификаторы оборудования представляют собой строки, однозначно определяющие тип устройства. В нашем примере я просто сконструировал идентификаторы псевдо-EISA (Extended Industry Standard Architecture) *WCO1104 и *WCO1105.
ПРИМЕЧАНИЕ----------------------------------------------------------------------
Если вы собираетесь конструировать идентификатор устройства показанным способом, не забудьте заменить MULFUNC именем своего устройства. Чтобы вы не скопировали имя из моего примера в виде жестко закодированной константы, я использовал константу LDRIVERNAME, которая определяется в файле DRIVER.H в проекте MULFUNC.
В Windows 98/Ме PnP Manager спокойно отнесется к тому, что одна и та же строка используется как в качестве идентификатора устройства, так и в качестве идентификатора оборудования, но в Windows ХР PnP Manager этого не разрешит. Я узнал об этом на собственном опыте, пытаясь передать вымышленное имя перечислителя в идентификаторе устройства. Вызов loGetDeviceProperty для получения имени перечислителя PDO приводит к фатальному сбою, потому что PnP Manager вместо указателя на строку получает NULL. Использование родительского имени перечислителя (ROOT в примере MULFUNC) приводит к странному результату: PnP Manager возвращает дочерние устройства после удаления родителя!
Обработка запросов
IRP_MN_QUERY_DEVICE_RELATIONS
Нам остается рассмотреть последний запрос — IRP_MN_QUERY_DEVICE_RELATIONS. Вспомните, что драйвер FDO отвечает на него списком дочерних PDO. Однако в роли PDO родительскому драйверу достаточно вернуть информацию о связях целевого устройства, для чего он предоставляет адрес PDO:
NTSTATUS HandleQueryRelatlons(PDEVICEJDBJECT pdo, PIRP Irp)
{
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocatlon(Irp);
NTSTATUS status = Irp->IoStatus.Status;
If (stack->Parameters.QueryDeviceRelat1ons.Type ===
T a rgetDevi ceRelati on)
{
PDEVICE-RELATIONS newrel = (PDEVICE_RELATIONS)
ExAllocatePool(PagedPool, sizeof(DEVICE_RELATIONS));
newrel->Count = 1;
newrel->Objects[0] = pdo;
ObReferenceObject(pdo);
508
Глава 11. Контроллеры и многофункциональные устройства
status = STATUS_SUCCESS;
Irp->IoStatus.Information = (ULONG_PTR) newrel;
}
Irp->IoStatus.Status = status;
loCompleteRequestdrp, IO_NO_INCREMENT);
return
Обработка запроса IRP_MN_QUERY_INTERFACE
При помощи запроса IRP_MN_QUERY_INTERFACE любой драйвер в стеке устройства РпР может получить интерфейс прямого вызова для любого драйвера нижнего уровня в том же стеке. Интерфейс прямого вызова позволяет драйверу напрямую вызывать одну или несколько функций другого драйвера без предварительного конструирования IRP. Далее перечисляются основные концепции, задействованные в механизме интерфейсов прямого вызова:
О Каждый уникальный интерфейс прямого вызова идентифицируется кодом GUID. Сам по себе интерфейс представляется структурой, содержащей указатели на функции, реализующие методы интерфейса.
О Драйвер, который желает использовать конкретный интерфейс, выдает запрос QUERY-INTERFACE с указанием идентификатора GUID и экземпляра структуры интерфейса. В дальнейшем такой драйвер может напрямую вызывать функции, указатели на которые хранятся в полях структуры. Когда драйвер завершает работу с интерфейсом прямого вызова, он вызывает функцию InterfaceDereference для освобождения ссылки на экспортирующий драйвер.
О Драйвер, экспортирующий заданный интерфейс, отслеживает запросы QUERYINTERFACE, в которых указан код GUID этого интерфейса. Для обработки подобных запросов экспортирующий драйвер заполняет поля структуры интерфейса, предоставленной вызывающей стороной, указателями на функции драйвера. Драйвер остается в памяти до того, как вызывающая сторона освободит свою ссылку на интерфейс.
Все эти концепции будут подробно рассмотрены позднее. Пример MULFUNC в прилагаемых материалах дает работоспособный образец использования этих концепций в реальном драйвере.
Идентификация интерфейса
Интерфейс прямого вызова идентифицируется созданием/публикацией GUID и структурой. Традиционно символическое имя GUID задается в формате GUID_ XXX_STANDARD в соответствии с шаблоном, указанным в заголовочном файле DDK WDMGUID.H. Например, пример MULFUNC экспортирует интерфейс прямого вызова со следующим GUID:
DEFINE_GUID(GUID_RESOURCE_SUBALLOCATE_STANDARD. ОхааО454О,
Ox6fdl, 0xlld3, 0x81. 0xb5, 0x0. OxcO, 0x4f, ОхаЗ, 0x30, Охаб);
Обработка запросов РпР
509
Интерфейс RESOURCE_SUBALLOCATE позволяет дочерним устройствам делить ресурсы ввода/вывода, принадлежащие родительскому устройству; ближе к концу главы я объясню, как это делается.
Структура, ассоциированная с интерфейсом RESOURCE_SUBALLOCATE, выглядит так (помните, что INTERFACE объявляется в заголовке DDK, потому что этот класс является базовым для каждого интерфейса прямого вызова):
typedef struct _INTERFACE {
USHORT Size;
USHORT Version;
PVOID Context;
PINTERFACE-REFERENCE InterfaceReference;
PINTERFACE-DEREFERENCE InterfaceDereference;
// Записи, специфические для конкретного интерфейса
} INTERFACE. *PINTERFACE;
struct _RESOURCE_SUBALLOCATE_STANDARD : public INTERFACE { PGETRESOURCES GetResources;
};
typedef struct _RESOURCE_SUBALLOCATE_STANDARD
RESOURCE_SUBALLOCATE_STANDARD,
*PRESOURCE_SUBALLOCATE_STANDARD;
Другими словами, интерфейс RESOURCE_SUBALLOCATE включает функцию Get-Resources, а также функции InterfaceReference и InterfaceDereference из базового класса.
Поиск и использование интерфейса прямого вызова
Драйвер, пожелавший использовать интерфейс прямого вызова, экспортируемый драйвером более низкого уровня в стеке РпР, конструирует и отправляет запрос QUERY^INTERFACE. В табл. 11.2 перечислены параметры Parameters.Querylnterface для этого запроса.
Таблица 11.2. Параметры IRP_MN_QUERY_INTERFACE	
Параметр	Описание
InterfaceType	Указатель на GUID, идентифицирующий интерфейс
Size	Размер структуры интерфейса, на которую указывает параметр Interface
Version	Версия структуры интерфейса
Interface	Адрес структуры интерфейса, заполняемой экспортирующим драйвером
InterfaceSpecificData	Дополнительные данные для драйвера, экспортирующего интерфейс, — зависит от интерфейса
Пример одного из способов выдачи запроса QUERY-INTERFACE:
RESOURCE_SUBALLOCATE_STANDARD suballoc: // <== Конечный результат
KEVENT event;
510
Глава 11. Контроллеры и многофункциональные устройства
KelnitializeEventC&event, NotificationEvent. FALSE);
IO_STATUS_BLOCK Iosb;
PIRP Irp = loBuildSynchronousFsdRequest IRPJU_PNP,
pdx->LowerDeviceObject, NULL, 0. NULL, &event, &iosb);
PI0_STACK_L0CAT10N stack = loGetNextlrpStackLocation(Irp);
stack->MinorFunction = IRP_MNJUERY_INTERFACE;
stack->Parameters.QueryInterface.InterfaceType =
&GUID_RESOURCE_SUBALLOCATE_STANDARD;
stack->Parameters.Queryinterface.Size = sizeof(suballoc);
stack^Parameters.QueryInterface.Version =
RESOURCEJUBALLOCATE_STANDARD_VERSION;
stack->Parameters.QueryInterface.Interface ~ &suballoc;
stack->Parameters.QueryInterface.InterfaceSpecificData = NULL;
NTSTATUS status = loCallDriver(pdx->LowerDeviceObject, Irp);
if (status == STATUS_PENDING)
(
KeWaitForSingleObject(&event, Executive, KernelMode,
FALSE, NULL);
status = Irp->IoStatus.Status;
}
В этом примере для взаимодействия по стеку используется синхронный IRP. Мы ожидаем, что кто-то заполнит структуру suballoc и завершит IRP с кодом успеха.
Если запрос интерфейса завершится успехом, в дальнейшем мы можем напрямую вызывать функции, па которые указывают члены интерфейсной структуры. Обычно для каждой функции интерфейса требуется аргумент контекста, взятый из возвращенной структуры интерфейса, как в следующем примере (см. фильтр SUBALLOC из примера MULFUNC):
PCM_RESOURCE_LIST raw, translated;
status = suballoc.GetResourcesCsuballoc.Context, pdx->Pdo,
&raw, ^translated);
Другие аргументы функции интерфейса и интерпретация возвращаемого значения определяются проектировщиком интерфейса.
Когда работа с интерфейсом прямого вызова будет завершена, выполните следующий вызов:
s uba11 ос.1nterfaceDereference(suballoc.Context);
Экспортирование интерфейса прямого вызова
Чтобы экспортировать интерфейс прямого вызова, необходимо обработать за-прос IRP_MN_QUERYJNTERFACE. Прежде всего следует проанализировать GUID интерфейса в стековых параметрах и определить, пытается ли вызывающая сторона найти поддерживаемый вами интерфейс. Пример:
if (*stack->Parameters.Query!nterface.InterfaceType .’ =
GUID_RESOURCE_SUBALLOCATE_STANDARD)
обработка по умолчанию>
Обработка запросов РпР
511
ПРИМЕЧАНИЕ--------------------------------------------------------------------
В заголовочных файлах DDK для определения операторов сравнения GUID используются команды C++ operator. Если вы пишете свой драйвер исключительно на С, используйте функцию IsEqualGuid.
В DDK утверждается, что драйвер шины должен отклонять запросы к неизвестным интерфейсам, полученные им в роли PDO: «Драйвер, обрабатывающий этот IRP, должен избегать передачи IRO другим стекам устройств для получения запрашиваемого интерфейса. При подобной архитектуре между стеками устройств возникают зависимости, усложняющие управление. Например, устройство, представленное вторым стеком, не может быть удалено до тех пор, пока соответствующий драйвер первого стека не освободит ссылку на интерфейс». Я не могу согласиться и рекомендую поступать наоборот в драйверах контроллеров и многофункциональных устройств. У драйвера дочернего устройства не существует другого способа обратиться к функциональности, предоставляемой шиной. Кстати, стек родительского устройства все равно не может быть удален до удаления всех стеков дочерних устройств, а дочерние драйверы должны быть достаточно умны, чтобы освобождать ссылки на интерфейсы прямого вызова в процессе отключения.
Если эволюция интерфейса насчитывает более одной версии, следующим шагом обработки запроса должен стать выбор предоставляемой версии интерфейса. Предложу удобную схему: начните нумерацию версий интерфейса с 1 и увеличивайте номер версии на 1 каждый раз, когда в интерфейсе произойдут какие-либо важные изменения. Определите константу для текущей версии в том же заголовочном файле, в котором определяются GUID и структура интерфейса. Вызывающая сторона указывает требуемый номер версии в параметрах IRP, используя эту константу, что однозначно определяет версию структуры, для которой проводилась компиляция. Далее можно произвести согласование от самой старой (запрашиваемой) версии до более новой версии, предоставляемой драйвером. Пример:
USHORT version = RESOURCE_SUBALLOCATE_STANDARD_VERSION;
if (version > stack->Parameters.QueryInterface.Version) version = stack->Parameters.QueryInterface.Version;
if (version == 0)
return CompleteRequestCIrp, Irp->IoStatus.Status);
Если начать нумерацию версий с 1, номер версии 0 возможен только в том случае, если вызывающая сторона затребует номер версии 0. Правильной реакцией в подобном случае будет завершение IRP с состоянием, указанным в IRP, — обычно это код STATUS_NOT_SUPPORTED.
Третьим шагом должна стать инициализация структуры, предоставленной вызывающей стороной. Пример:
if (stack->Parameters.QueryInterface.S1ze <
si zeof(RESOURCE-SUBALLOCATE-STANDARD))
return CompleteRequestCIrp, STATUS_INVALID__PARAMETER);
512
Глава 11. Контроллеры и многофункциональные устройства
PRESOURCE_SUBALLOCATE_STANDARD ifp =
(PRESOURCE JUBALLOCATE_STANDARD)
stack->Parameters.QueryInterface.Interface;
1fp->S1ze = sizeof(RESOURCE_SUBALLOCATE_STANDARD);
1fp->Version = 1;
ifp->Context = (PVOID) fdx;
1fp->InterfaceReference = (PINTERFACE-REFERENCE)
SuballocInterfaceReference;
1fp->InterfaceDereference = (PINTERFACE_DEREFERENCE)
Subal1ocInterfaceDereference;
1fp->GetResources = (PGETRESOURCES) GetChIldResounces;
Наконец, остается создать ссылку на интерфейс таким способом, чтобы драйвер оставался загруженным до тех пор, пока вызывающая сторона не вызовет функцию InterfaceDereference.
Обработка запроса IRP_MN_QUERY_PNP_DEVICE_STATE
В некоторых ситуациях требуется подавить отображение всех или некоторых устройств в Диспетчере устройств. Для этого следует добавить флаг PNP_DEVICE_ DONT_DISPLAY_IN_UI к флагам устройства, выдаваемым по запросу IRP_MN_QUERY_ PNP_DEVICE_STATE. Если не считать этого необязательного шага, данный IRP делегируется родительскому стеку, как было описано ранее.
Обработка запросов управления питанием
В роли FDO контроллер или драйвер многофункционального устройства обрабатывает запросы IRP_MJ_POWER в точности так, как было описано в главе 8, с одним небольшим исключением, которое мы рассмотрим сейчас в связи с IRP_MN_ WAIT_WAKE. В роли PDO контроллер безусловно удовлетворяет запросы управления питанием, кроме запроса IRP_MN_WAIT_WAKE, для которого требуется специальная обработка. Краткая сводка действий по обработке разных запросов приведена в табл. 11.3.
Таблица 11.3. Обработка запросов питания родительским драйвером
Запрос РпР	Роль FDO	Роль PDO
IRP_MN_POWER_SEQUENCE	Обычная	Завершение
IRP_MN_QUERY_POWER	Обычная	Успех
IRP_MN_SET_POWER	Обычная	Специальная
IRP_MN_WAITWAKE	Завершение дочерних IRP, в остальном обычная	Специальная
Прочие	Обычная	Завершение
В оставшейся части этого раздела мы обсудим механику обработки запросов питания в тех областях, в которых она отличается от стандартной обработки функциональными драйверами.
Обработка запросов управления питанием
513
Завершение
В роли PDO родительский драйвер завершает все непонятные ему IRP управления питанием, со значениями Status и Information, уже сохраненными в IRP. В частности, выполнение этого условия проверяется Driver Verifier на стадии инициализации режима I/O Verification:
NTSTATUS DefdultPowerHandler(PDEVICE_OBJECT fdo, PIRP Irp)
{
PoStartNextPowerlrp(Irp);
NTSTATUS status = Irp->IoStatus.Information;
loCompleteRequest(Irp, IO_NO_INCREMENT); return status;
}
Успех
Когда родительский драйвер удовлетворяет запрос IRP_MJ_POWER для дочернего PDO, он вызывает PoStartNextPowerirp и завершает IRP, как показано в следующем фрагменте:
NTSTATUS HandleQueryPower(PDEVICE_OBJECT pdo, PIRP Irp)
{
PoStartNextPowerlrp(Irp);
return CompleteRequestCIrp, STATUS-SUCCESS, 0);
}
Отличие этого фрагмента от действий, обычно выполняемых в функциональном драйвере, состоит в том, что родительский драйвер находится в конце цепочки, не передает IRP вниз, а следовательно, должен завершить запрос.
Не повторяйте запрос IRP_MN_QUERY_POWER в родительском стеке. В Windows 98/Ме это приводит к тому, что Configuration Manager начинает рекурсивно опрашивать дочерние устройства и входит в бесконечный цикл, что в итоге приводит к переполнению стека. В этих системах Power Manager уже знает о связях «родитель-потомок» между устройствами и самостоятельно управляет обработкой необходимых запросов.
Обработка запроса IRP_MN_SET_POWER
Запрос IRP_MN_SET_POWER для дочернего устройства требует дополнительной работы, если в нем указано состояние энергопотребления устройства. Если родительское устройство может независимо управлять состоянием энергопотребления дочернего устройства, родительский драйвер должен сделать все необходимое для этого. Имеет ли дочернее устройство состояние энергопотребления, независимое от родителя, или нет, родительский драйвер должен вызвать PoSetPowerState, чтобы оповестить Power Manager о новом состоянии питания. Затем он вызывает PoStartNextPowerirp и завершает IRP:
NTSTATUS HandleSetPowerCIN PDEVICE_OBJECT pdo, IN PIRP Irp)
{
514
Глава 11. Контроллеры и многофункциональные устройства
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
if (stack->Parameters.Power.Type == DevicePowerState) {
<выбор уровня энергопотребления дочернего устройства> PoSetPowerState(pdo. DevIcePowerState, stack ^Parameters.Power.State);
}
PoStartNextPowerlrp(pdo):
CompleteRequestCIrp, STATUS_SUCCESS, 0);
<возможно, изменение уровня энергопотребления родительского устройства> return STATUS_SUCCESS;
}
Возможно, родительскому драйверу также потребуется довести уровень энергопотребления родительского устройства до минимального уровня, соответствующего уровням энергопотребления всех дочерних устройств. Например, если все дочерние устройства работают на уровне D3, весьма вероятно, что родительское устройство также должно работать на уровне D3. Если же все дочерние устройства работают на уровне DO, то и родительскому устройству было бы логично работать на уровне DO. Учтите, что в примере MULTIFUNC это поведение не представлено.
Обработка запроса IRP_MN_WAIT_WAKE
Скорее всего, работоспособность функции пробуждения системы дочерним устройством будет зависеть от того, имеется ли эта функция и у родительского устройства. По этой причине на драйвер контроллера или многофункционального устройства возлагается дополнительная ответственность за обработку IRP_ MN_WAIT_WAKE, отсутствующая у обычных функциональных драйверов.
Когда родительский драйвер получает запрос IRP_MN_WAIT_WAKE для дочернего PDO, он должен выполнить следующие действия:
О Если способности устройства показывают, что родительское устройство не может пробудить систему ни при каких условиях, родитель должен отклонить запрос с кодом STATUS_NOT_SUPPORTED (именно это делается в MULFUNC).
О Если для того же дочернего устройства уже имеется необработанный запрос IRP_ MN_WAIT_WAKE, родитель отклоняет новый запрос с кодом STATUS_DEVICE_BUSY.
О Если дочернее устройство уже находится в состоянии настолько низкого энергопотребления, что оно не может пробудить систему, или если устройство не может пробудить систему из состояния, указанного в IRP, родительский драйвер должен отклонить IRP с кодом STATUS_INVALID_DEVICE_STATE. Строго говоря, функциональный драйвер не должен выдавать запрос WAIT_WAKE при выполнении хотя бы одного из этих двух условий, но за соблюдение этих правил все равно отвечает родительский драйвер.
О В остальных случаях родительский драйвер помечает JRP как незавершенный, сохраняет его способом, безопасным по отношению к отмене, и возвращает STATUS_PENDING. Например, вы можете использовать схему сохранения управляющих операций ввода/вывода (IOCTL), описанную в главе 9, или выбрать
Обработка запросов управления питанием
515
в качестве места хранения защищенную очередь (см. главу 5). Этот случай является нормальным для устройств, действительно поддерживающих функцию пробуждения.
В последнем случае родительский драйвер также должен выдать собственный запрос WAIT_WAKE для родительского стека, если он еще не сделал этого ранее. В этом отношении родительский драйвер просто делает то, что должен делать любой функциональный драйвер с возможностью пробуждения, — не считая того, что незавершенность дочернего WAIT„WAKE становится дополнительным инициирующим условием для отправки собственного IRP.
Если WAIT_WAKE родительского драйвера в дальнейшем завершается с кодом успеха, родитель должен завершить один или несколько незавершенных дочерних запросов WAIT_WAKE. Если родитель может определить, что конкретный потомок пробудился, то родитель должен завершить IRP только этого потомка. В противном случае он завершает IRP, принадлежащие всем дочерним драйверам. Этот этап гарантирует, что соответствующие дочерние функциональные драйверы правильно обработают сигнал пробуждения.
О ФУНКЦИИ ПРОБУЖДЕНИЯ ДЛЯ МНОГОФУНКЦИОНАЛЬНЫХ УСТРОЙСТВ------------------------
Однажды я написал драйвер для стандартного класса устройств чтения смарт-карт с интерфейсом USB (CCID). Спецификация CCID допускает наличие нескольких слотов для карт — естественный путь реализации этой функциональности основан на создании многофункционального драйвера, создающего несколько идентичных дочерних устройств чтения смарт-карт. Многие производители таких устройств решили включить поддержку функции пробуждения системы, чтобы при вставке или извлечении карты компьютер выходил из ждущего режима.
Я посчитал, что пользователь вряд ли захочет независимо управлять каждым слотом многослотового устройства. По этой причине я решил скрыть дочерние устройства в Диспетчере устройств способом, описанным ранее в этой главе. Соответственно, пользователь мог управлять включением или отключением функции пробуждения только на странице свойств родительского устройства в Диспетчере устройств.
Итак, основная ответственность за пробуждение в этом драйвере возлагалась на родительский драйвер в роли FDO. Дочерний функциональный драйвер (который на самом деле находился в одном исполняемом файле с родительским драйвером) вроде бы создавал и обрабатывал запросы WAIT_WAKE, но родительский драйвер, фактически, игнорировал эти запросы, ограничиваясь простым сохранением и завершением.
Единственная странность этого драйвера была связана с выбором состояния энергопотребления дочерним драйвером. Поскольку стандартная точка входа для включения механизма пробуждения дочернего устройства отсутствовала, дочернему драйверу приходилось обходным путем узнавать, что родитель включил пробуждение, и выбирать совместимое состояние энергопотребления. Поскольку я решил включить дочерний функциональный драйвер в один исполняемый файл с родительским драйвером, сделать это было несложно.
Если родительский запрос WAIT_WAKE впоследствии завершается с ошибкой, родительский драйвер обычно завершает все дочерние запросы WAITJA/AKE с тем же кодом. Может показаться, что эта рекомендация имеет более глобальный характер, чем в действительности. На практике встречаются только два кода ошибок. Код STATUSJZANCELLED означает, что родительский драйвер сам решил отменить свои незавершенные запросы WAIT__WAKE, готовясь к отключению питания или потому, что конечный пользователь отключил функцию пробуждения у родительского
516
Глава 11. Контроллеры и многофункциональные устройства
устройства. Код STATUS_INVALID_DEVICE_STATE означает, что уровень энергопотребления системы или родительского устройства слишком низок для поддержки пробуждения. В обоих случаях родитель должен оповестить все дочерние устройства об отключении их функции пробуждения, отклоняя их запросы WAIT_WAKE.
Работа с ресурсами дочерних устройств
Если ваше устройство относится к категории контроллеров, вероятно, подключаемые к нему дочерние устройства захватывают собственные ресурсы ввода/ вывода. При наличии автоматизированного способа определения требований к ресурсам вы можете вернуть список требований в ответ на запрос IRP_MN_ QUERY_RESOURCE_REQUIREMENTS. Если автоматизированного способа нет, в INF-файле дочернего устройства должна присутствовать секция LogConfig, в которой эти требования перечисляются.
Для многофункциональных устройств родительское устройство обычно берет под свой контроль все ресурсы ввода/вывода, используемые дочерними функциями. Если дочерние функции обслуживаются разными драйверами WDM, вам придется разработать механизм распределения ресурсов по функциям и передачи информации каждому функциональному драйверу о том, какие именно ресурсы ему принадлежат. Эта задача не из простых. PnP Manager обычно передает функциональному драйверу информацию о выделенных ресурсах в запросе IRP_MN_START_DEVICE (см. подробное обсуждение в главе 7). Тем не менее, не существует стандартного способа заставить PnP Manager использовать нужные вам ресурсы вместо тех, которые он сам назначит.
Драйвер Microsoft MF.SYS решает проблему вторичного распределения ресурсов за счет использования внутренних интерфейсов с системными арбитрами ресурсов, недоступными для сторонних разработчиков. Существуют два разных способа вторичного распределения ресурсов: один работает в Windows ХР, а другой в Windows 98/Ме. Поскольку мы не можем пойти по пути MF.SYS, придется изобрести другой способ вторичного распределения ресурсов, принадлежащих родительскому устройству.
Если все функциональные драйверы дочерних устройств находятся под вашим контролем, родительский драйвер может экспортировать интерфейс прямого вызова. В этом случае дочерние драйверы получают указатель на дескриптор интерфейса, отправляя запрос IRP_MN_QUERY_INTERFACE родительскому драйверу. Вызывая функции родительского драйвера при запуске и остановке устройства, они получают и освобождают ресурсы, фактически принадлежащие родителю.
Если модификация функциональных драйверов дочерних устройств невозможна, проблему вторичного распределения ресурсов можно решить посредством установки крошечного верхнего фильтра (см. главу 16) над FDO каждого из дочерних устройств. Единственное назначение этого фильтра — подключение списка выделенных ресурсов к каждому IRP_MN_START_DEVICE. Фильтр взаимодействует через интерфейс прямого вызова с родительским драйвером. Пример MULFUNC работает именно по этому принципу, анализируя его, вы сможете лучше разобраться в механике.
USB
Основной концепцией интерфейса USB (Universal Serial Bus) было удобство конечного пользователя. Концепция PnP (Plug and Play) упростила процесс установки некоторых типов оборудования на существующих PC. Тем не менее, пользователей продолжали преследовать проблемы конфигурации таких наследных устройств, как параллельные и последовательные порты, клавиатуры и мыши. Доступность портов была одним из факторов, исторически ограничивших широкое развитие периферийных устройств, включая модемы, сканеры и т. д. Шина USB помогает решить эти проблемы, предоставляя единый метод подключения (теоретически) большого количества самоидентифицируемых устройств к одному порту PC.
Хотя эта книга посвящена программному обеспечению, мы рассмотрим некоторые электрические и механические аспекты USB, потому что они важны для разработчиков программного обеспечения. С точки зрения конечного пользователя, главной особенностью USB является то, что каждое устройство использует одинаковый 4-проводной кабель со стандартным разъемом, подключаемым к гнезду на задней панели PC или к концентратору, подключаемому к PC. Кроме того, устройства USB можно свободно подключать и отсоединять от компьютера, при этом вам не придется открывать или закрывать приложения, которые их используют, а также беспокоиться о возможном повреждении оборудования.
В этой главе рассматриваются две большие темы. В первой части главы я опишу программную архитектуру USB. В нее входит ряд концепций, включая иерархический способ подключения устройств к компьютеру, обобщенную схему управления питанием и стандарт самоидентификации, основанный на иерархии дескрипторов, закрепленных за оборудованием. В архитектуре USB также используется схема деления фреймов и микрофреймов фиксированной продолжительности на пакеты, передающие данные устройствам и от них. Наконец, USB допускает четыре разных метода передачи данных между хостом (управляющим компьютером) и конечными точками (endpoints) на устройствах. Первый метод, называемый изохронным, позволяет передавать фиксированные объемы данных без исправления ошибок с регулярными интервалами — вплоть до трех раз за каждый микрофрейм. Другие методы — управляющий, массовый и прерывающий — обеспечивают передачу данных с автоматическим исправлением ошибок.
518
Глава 12. USB
Во второй части этой главы я опишу дополнительные возможности драйверов WDM для устройств USB по сравнению с теми, которые вам уже известны. Вместо того чтобы взаимодействовать с оборудованием напрямую с использованием вызовов функций HAL, драйвер USB в значительной мере зависит от драйвера шины и библиотеки режима ядра с именем USBD.SYS. Чтобы отправить запрос устройству, драйвер создает блок запроса USB (URB), который он передает драйверу шины. Например, настройка конфигурации устройства USB требует передачи драйвером нескольких URB для чтения дескрипторов и отправки команд. В свою очередь, драйвер шины планирует запросы к шине в соответствии с потребностью и доступной пропускной способностью.
Авторитетным источником информации о USB является официальная спецификация, которая на момент публикации книги существовала в версии 2.0. Спецификация и другие документы, выпущенные комитетом USB и его рабочими группами, доступны в Интернете по адресу http://www.usb.org/developers/.
О ПРИМЕРАХ-----------------------------------------------------------------------------
Компания Anchor Chips, Incorporated, любезно предоставила мне один из своих пакетов разработчика EZ-USB, который я использовал при написании драйверов для первого издания книги. В дальнейшем компания Anchor Chips была приобретена Cypress Semiconductor (www.cypress.com). Чипсет USB jn Cypress Semiconductor построен на базе модифицированного микропроцессора 8051 и дополнительной логике для выполнения некоторых низкоуровневых протокольных функций, обусловленных спецификацией USB. Макетная плата также оснащена дополнительной внешней памятью, UART и последовательным разъемом, набором кнопок и светодиодным индикатором для упрощения разработки и отладки микрокода 8051 с использованием программной архитектуры Cypress Semiconductor. Одной из ключевых особенностей чипсета Cypress Semiconductor является возможность простой загрузки микрокода по подключению USB. Это настоящий подарок для программистов вроде меня, страдающих фобией по отношению к оборудованию вообще и перепрограммированию «прошивок» в особенности.
Примеры в прилагаемых материалах демонстрируют работу простейших устройств USB и сами по себе скорее дают примеры решения отдельных задач. Но если в вашем распоряжении окажется пакет разработчика Cypress Semiconductor, вы сможете опробовать эти примеры на реальном микрокоде. Каждый пример содержит подкаталог SYS с драйвером WDM, подкаталог TEST с тестовой программой Microsoft Win32 и каталог EZUSB с микрокодом. Постройте эти компоненты по инструкциям в прилагаемых НТМ-файлах или просто установите готовые версии.
Программная архитектура
Авторы спецификации USB предвидели, что программистов в первую очередь интересует написание программного обеспечения для хоста и устройств и они вряд ли захотят разбираться в электрических характеристиках шины. В главах 5 и 9 спецификации описаны возможности, представляющие наибольший интерес для разработчика драйверов. В этом разделе приводится краткая сводка содержимого этих глав.
Иерархия устройств
На рис. 12.1 изображена топология простой конфигурации USB. Хостовый контроллер подключается к системной шине так же, как любое другое устройство ввода/вывода. Операционная система взаимодействует с хостовым контроллером
Программная архитектура
519
через порты ввода/вывода или регистры памяти и получает оповещения о событиях от контроллера через стандартный сигнал прерывания. В свою очередь, хостовый контроллер подключается к дереву устройств USB. Особая разновидность устройства, называемая концентратором (hub), служит точкой подключения для других устройств. Хостовый контроллер содержит корневой концентратор. Концентраторы могут объединяться «гирляндой» до максимальной глубины, определенной в спецификации USB. В настоящее время допускается подключение до пяти концентраторов к корневому концентратору, то есть общая глубина дерева составляет до семи уровней. Другие разновидности устройств (цифровые камеры, мыши, клавиатуры и т. д.) подключаются к концентраторам. Точности ради в спецификации USB устройства, не являющиеся концентраторами, называются функциями. В настоящее время спецификация разрешает подключать к шине до 127 функций и концентраторов.
(Корневой) концентратор й> ф..... <
Рис. 12.1. Иерархия устройств USB
Высокоскоростные, полноскоростные и низкоскоростные устройства
В спецификации USB устройства классифицируются по скорости передачи данных. Контроллер USB 2.0 работает с шиной на скорости 480 Мбит/с. Устройства (как концентраторы, так и функции) могут работать на высокой скорости (480 Мбит/с), полной скорости (12 Мбит/с) или низкой скорости (1,5 Мбит/с).
В USB 2.0 концентраторы отвечают за взаимодействие с нолноскоростными и низкоскоростными устройствами по схеме, которая оказывает минимальное влияние
520
Глава 12. USB
на высокоскоростную передачу сигналов, задействованную в работе шины и высокоскоростных устройств.
В предыдущей версии спецификации USB (1.1) поддерживались только полноскоростные и низкоскоростные устройства. На шине 1.1 обмен данными обычно производится на полной скорости, а концентраторы обычно не отправляют данные низкоскоростным устройствам. Операционная система предваряет каждое сообщение, предназначенное для низкоскоростного устройства, специальным пакетом — преамбулой, заставляющим концентраторы временно включить поддержку низкоскоростных устройств.
Питание
По кабелю USB передаются как питание, так и сигналы данных. Каждый концентратор может подавать электропитание подключенным к нему устройствам, а в случае с подчиненными концентраторами — и нисходящим устройствам. Спецификация USB устанавливает ограничения на энергопотребление устройства, питаемого от шины. Ограничения изменяются в зависимости от того, подключено ли устройство к концентратору, обладающему собственным питанием, как далеко устройство расположено от ближайшего активного концентратора и т. д. Кроме того, USB позволяет устройствам работать в приостановленном состоянии с минимальным потреблением энергии — достаточным только для поддержки пробуждения и конфигурационных сигналов. Вместо использования питания шины можно строить концентраторы и устройства с независимым питанием. Более того, WHQL (Windows Hardware Quality Lab) принимает на тестирование концентраторы с питанием от шины только в тех случаях, когда они являются частью составных устройств.
Устройства USB способны пробуждать систему из состояния пониженного энергопотребления. При переходе в такое состояние операционная система также переводит USB в состояние пониженного энергопотребления. Устройство, обладающее включенной функцией удаленного пробуждения, может позднее подать восходящий сигнал на пробуждение промежуточных концентраторов, хостового контроллера USB, а в конечном итоге и всей системы.
Проектировщики устройств USB должны знать о некоторых ограничениях на функцию пробуждения. Во-первых, удаленное пробуждение системы работает только на компьютерах с включенной в BIOS поддержкой ACPI (Advanced Configuration and Power Interface). Старые системы либо поддерживают АРМ (Advanced Power Management), либо вообще не поддерживают никакие стандарты. Я также обнаружил, что в области поддержки пробуждения по шине USB на PC существует колоссальное разнообразие. Посетив крупный компьютерный магазин в середине 2002 года, я обнаружил всего один ноутбук, реагировавший на сигнал пробуждения от устройства USB. Как узнать, будет ли конкретная комбинация компьютера и операционной системы работать в этом отношении? Мне не известен ни один надежный способ, кроме экспериментального.
Концентраторы USB могут поддерживать функцию управления питанием, называемую избирательной приостановкой. Она позволяет концентратору
Программная архитектура
521
приостанавливать отдельные порты и в целом направлена на повышение удобства управления питанием устройств, работающих от батарей. В Windows ХР эта возможность поддерживается на уровне специальной управляющей операции ввода/вывода (IOCTL), которая будет рассмотрена позднее в этой главе.
Как организовано устройство?
В общем случае каждое устройство USB может иметь одну или несколько конфигураций управляющих его поведением (рис. 12.2). Конфигурации одного устройства могут различаться по энергопотреблению, способности удаленного пробуждения компьютера и набору интерфейсов. Драйверы Microsoft всегда работают только с первой конфигурацией устройства. Поддержка составных устройств от Microsoft не задействуется для устройств с несколькими конфигурациями. Соответственно, на практике многофункциональные устройства встречаются относительно редко, и Microsoft препятствует разработке новых. Мне известно лишь несколько сценариев, в которых наличие нескольких конфигураций имеет смысл: О коммуникационные устройства ISDN (Integrated Services Digital Network), предоставляющие либо два 56-килобитных канала, либо один 128-килобит-ный канал;
О устройство, предоставляющее простую конфигурацию для BIOS и более сложную — для драйверов Windows;
О трекбол, настраиваемый как мышь или как джойстик.
Конечные точки
Рис. 12,2. Конфигурации устройств, интерфейсы и конечные точки
522
Глава 12. USB
Каждая конфигурация устройства воплощает один или несколько интерфейсов, которые определяют, как программы должны работать с оборудованием. Концепция интерфейса сходна с концепцией, рассматривавшейся в главе 2 при рассмотрении имен устройств. Другими словами, устройства, поддерживающие одинаковые интерфейсы, фактически, взаимозаменяемы с точки зрения программ, потому что они реагируют на одни и те же команды заранее определенным образом. Кроме того, интерфейсы часто имеют альтернативные настройки, соответствующие разным требованиям к пропускной способности.
Интерфейс устройства экспортирует одну или несколько конечных точек, каждая из которых завершает коммуникационный канал. На рис. 12.3 изображена диаграмма многоуровневой коммуникационной модели, которая демонстрирует роль каналов и конечных точек. На самом нижнем уровне кабель USB соединяет хостовый контроллер шины с интерфейсом шины на устройстве. На втором уровне управляющий канал соединяет системное программное обеспечение с логическим устройством. На третьем, и самом высоком, уровне несколько каналов связывают клиентское программное обеспечение с группой интерфейсов, образующих функцию устройства. В действительности информация передается по вертикали, вверх и вниз по обеим сторонам диаграммы, но вам будет полезно представлять, что каналы передают информацию по горизонтали между соответствующими уровнями.
Компьютер	Устройство USB
Рис. 12.3. Многоуровневая коммуникационная модель USB
Набор драйверов, предоставляемых Microsoft, занимает нижнюю часть блока «Системное программное обеспечение» на этой диаграмме. К их числу относятся
Программная архитектура
523
драйверы хостовых контроллеров (USBOHCLSY5, USBUHCD.SYS или USBEHCLSYS), драйвер концентратора (USBHUB.SYS) и библиотека, используемая всеми системными и клиентскими драйверами (USBD.SYS). Для удобства я объединю все эти драйверы термином родительский драйвер. В совокупности эти драйверы управляют подключением оборудования и механикой передачи данных по различным каналам. Драйверы WDM - такие, которые пишем мы с вами, — занимают верхнюю часть блока системного программного обеспечения. Вообще говоря, работа драйвера WDM сводится к преобразованию запросов от клиентского программного обеспечения в транзакции, которые могут быть выполнены родительским драйвером. Клиентское программное обеспечение имеет дело с непосредственной функциональностью устройства. Например, графическое приложение может занять позицию клиентского программного обеспечения для функции получения статического изображения (например, цифровой камеры).
Передача информации
В USB определены четыре метода передачи данных (табл. 12.1). Эти методы различаются по объему данных, перемещаемых за одну транзакцию (термин транзакция объясняется в следующем разделе), по возможности предоставления гарантий периодичности или задержки, а также по возможности автоматического исправления ошибок. Каждый метод соответствует определенному типу конечной точки. Более того, конечная точка заданного типа (то есть управляющая, массовая, изохронная или прерывающая) всегда обменивается данными с хостом с использованием соответствующего типа передачи.
Таблица 12.1. Типы передачи данных
Тип передачи	Описание	Без потери данных?	Гарантии задержки?
Управляющая	Используется для отправки и приема структурированной информации управляющего характера	Да	Оптимальная
Массовая	Используется для отправки и приема блоков неструктурированных данных	Да	Нет
Прерывающая	Как канал массовой передачи, но с максимальной задержкой	Да	Опрос с гарантированной минимальной частотой
Изохронная	Используется для отправки и приема блоков неструктурированных данных с гарантированной периодичностью	Нет	Чтение и запись с регулярными интервалами
Помимо типа конечные точки обладают несколькими атрибутами. Одним из таких атрибутов является максимальный объем данных, который конечная точка может отправить или принять за одну транзакцию. В табл. 12.2 указаны максимальные значения для всех типов конечных точек и скоростей устройств.
524
Глава 12. USB
В общем случае объем данных, передаваемых за одну операцию пересылки, может быть меньше максимального объема для данной конечной точки. Другим атрибутом конечной точки является ее направление. Конечная точка описывается как входная (перемещение информации от устройства к хосту) или выходная (перемещение информации от хоста к устройству). Наконец, с каждой конечной точкой ассоциируется число, которое используется в сочетании с признаком направления как адрес конечной точки.
Таблица 12.2. Максимальные размеры пакетов для конечных точек
Тип передачи	Высокая скорость	Полная скорость	Низкая скорость
Управляющая	64	8, 16, 32 или 64	8
Массовая	<512	8, 16, 32 или 64	—(низкоскоростные устройства не могут иметь массовых конечных точек)
Прерывающая	<1024	<64	<8
Изохронная	<3072	<1023	—(низкоскоростные устройства не могут иметь изохронных конечных точек)
В USB используется протокол опроса, в котором хост более или менее регулярно обращается к устройству с запросом на выполнение некоторой функции. Когда устройству требуется отправить данные хосту, хост должен каким-то образом понять это и выдать устройству запрос на отправку данных. Однако устройства USB не генерируют прерывания на хосте в традиционном смысле. Вместо асинхронных прерываний USB предоставляет прерывающие конечные точки, которые хост периодически опрашивает. Опрос прерывающих конечных точек и изохронных точек производится с частотой, задаваемой параметром в дескрипторе конечной точки:
О для устройства USB 2.0, работающего на высокой скорости, интервал опроса blnterval задается в интервале от 1 до 16 включительно; опрос производится каждые 2bIntcrval 1 микрофреймов;
О для устройства USB 2.0, работающего на полной скорости, интервал опроса blnterval задается в интервале от 1 до 16 включительно; опрос производится каждые 2bIntcrva1-1 фреймов;
О для устройства USB 1.1, работающего на полной скорости, для изохронных конечных точек интервал опроса должен быть равен 1, а для прерывающих конечных точек он задается в интервале от 1 до 255 включительно. Обратите внимание: хостовому драйверу USB 2.0 не нужно различать устройства 2.0 и 1.1 при интерпретации интервала опроса для изохронного устройства, поскольку 21-1 = 1;
О для устройства USB 1.1, работающего на низкой скорости, для прерывающей конечной точки интервал опроса задается в интервале от 10 до 255. У низкоскоростных устройств изохронные конечные точки отсутствуют.
Программная архитектура
525
Упаковка информации
При отправке или приеме данных по каналу USB клиентская программа сначала вызывает функцию Win32 API, что в конечном случае приводит к получению функциональным (то есть нашим) драйвером пакета запроса ввода/вывода (IRP). Драйвер должен направить клиентский запрос в канал, ведущий к соответствующей конечной точке устройства. Он передает запрос драйверу шины, который разбивает запросы на транзакции. Драйвер шины планирует передачу транзакций оборудованию. По шине USB 2.0 информация передается в микрофреймах, передаваемых каждые 125 мкс, а по шине USB 1.1 — во фреймах, передаваемых каждую миллисекунду. Драйвер шины распределяет незавершенные транзакции по фреймам и микрофреймам с учетом их продолжительности. Результат этого процесса показан на рис. 12.4.
Рис. 12.4. Модель передачи информации с использованием транзакций и фреймов
Когда хост взаимодействует по шине USB 2.0 с полно- или низкоскоростным устройством, режим преобразования транзакций концентратора обеспечивает промежуточную буферизацию, позволяющую восходящей шине (то есть шине на хостовой стороне концентратора) продолжать работать на высокой скорости. Фактически, концентратор USB 2.0 обслуживает полно- и низкоскоростные нисходящие порты (то есть порты на стороне концентратора, обращенной от хоста) как шина USB 1.1, с планированием фреймов каждую миллисекунду.
При взаимодействии по шине USB 1.1 с низкоскоростным устройством хост вставляет преамбулу — специальный пакет для переключения шины на низкую скорость на время одной транзакции. В остальное время низкоскоростные устройства не обслуживаются схемами передачи сигналов.
В USB транзакция состоит из одной или нескольких фаз. Фаза содержит маркер (token), данные или пакет согласования. В зависимости от типа транзакция состоит из фазы передачи маркера, необязательной фазы данных и необязательной фазы согласования (рис. 12.5). Во время фазы передачи маркера хост передает пакет данных всем устройствам в действующей конфигурации. Пакет
526
Глава 12. USB
маркера включает адрес устройства и (часто) номер конечной точки. Транзакция будет обрабатываться только указанным устройством; устройства не читают и не записывают данные по шине во время транзакций, адресованных другим устройствам. Во время фазы данных данные передаются по шине. В выходных транзакциях хост помещает данные на шину, а указанное устройство их получает. Во входных транзакциях роли меняются — устройство подает данные на шину для передачи хосту. В фазе согласования либо устройство, либо хост подает на шину пакет с информацией состояния. Если пакет согласования передается устройством, это может быть пакет АСК (признак успешного получения информации), пакет NAK (означает, что устройство занято и не пытается принимать информацию) или пакет STALL (означает, что транзакция была принята успешно, но почему-либо оказалась логически недействительной). Если же пакет согласования передается хостом, возможна только отправка пакета АСК.
Рис. 12.5. Фазы транзакции
В USB 2.0 используется дополнительная разновидность пакетов согласования для операций вывода с массовыми конечными точками — NYET (то ли русское слово «нет» в английской транскрипции, то ли сокращение от «not yet», то есть «пока нет»). NYET входит в схему управления передачей информации, называемую PING, и означает, что конечная точка не может принять очередной полный пакет. Предполагается, что хост отложит отправку дополнительного вывода на период времени, определяемый атрибутом blnterval конечной точки (этот атрибут не использовался для массовых конечных точек в USB 1.1). Протокол PING и пакет NYET должны предотвратить загрузку шины пакетами данных, которые в любом случае будут отвергнуты занятым устройством.
ОБ АДРЕСАЦИИ УСТРОЙСТВ-------------------------------------------------------
В предшествующем тексте говорится, что все устройства в действующей конфигурации получают электрические сигналы, связанные со всеми транзакциями. Это почти правда, но по-настоящему компетентный программист должен знать одну деталь. При начальной активизации устройство USB реагирует на адрес по умолчанию (его числовое значение равно 0, но вам это знать не обязательно). Специальные электрические сигналы оповещают драйвер шины о появлении нового устройства, на что драйвер шины присваивает устройству адрес и передает управляющую транзакцию, которая сообщает «устройству номер 0» его настоящий адрес. В дальнейшем устройство реагирует только на свой настоящий адрес.
Обратите внимание: не существует пакета согласования, означающего «В этой транзакции обнаружена ошибка передачи». Предполагается, что сторона, ожидающая подтверждения, поймет, что отсутствие подтверждения подразумевает
Программная архитектура
527
ошибку, и попробует повторить передачу. Проектировщики USB посчитали, что ошибки будут относительно редкими, а задержки, обусловленными повторными попытками, не окажут заметного влияния на быстродействие.
Состояния конечной точки
В общем случае конечная точка может находиться в любом из состояний, изображенных на рис. 12.6. В состоянии бездействия (idle) конечная точка готова к обработке новой транзакции, инициированной хостом. В состоянии занятости конечная точка занята обработкой транзакции и не может инициировать новую транзакцию. Если хост пытается инициировать транзакцию к занятой конечной точке (кроме управляющих конечных точек — см. следующий раздел), устройство отвечает пакетом согласования NAK, чтобы хост повторил попытку позднее. Если устройство обнаруживает ошибки в собственной работе (без учета ошибок передачи), оно выдает пакет согласования STALL для текущей транзакции и переходит в состояние приостановки. Управляющие конечные точки автоматически выходят из приостановки при получении новой транзакции, но перед отправкой следующего запроса приостановленным конечным точкам других типов хост должен выполнить сброс отправкой соответствующего управляющего запроса.
Рис. 12.6. Состояния конечной точки
Управляющие передачи
Управляющая передача доставляет управляющую информацию управляющей конечной точке устройства или от нее. Например, одна из частей общего процесса, посредством которого операционная система настраивает устройство USB, использует входные управляющие передачи для чтения различных дескрипторов, хранящихся непосредственно в устройстве. Другая часть процесса настройки посредством выходных управляющих передач выбирает одну из конфигураций в качестве текущей, а также включает один или несколько интерфейсов. Управляющие передачи выполняются без потерь: драйвер шины трижды пытается повторить ошибочную передачу, прежде чем сдаться и выдать программе код ошибки. Как видно из табл. 12.2, управляющие конечные точки должны задавать максимальную длину пересылаемых данных 8, 16, 32 или 64 байта. В ходе отдельной транзакции объем пересылаемых данных может быть меньше обозначенного максимума, но не может превысить его.
528
Глава 12. USB
Управляющие транзакции в USB обладают высоким приоритетом. Устройство не может отказаться от обработки управляющей транзакции по причине занятости. Более того, драйвер шины резервирует до 10 % каждого фрейма (20 % каждого микрофрейма для высокоскоростных устройств) для управляющих транзакций.
Каждое устройство имеет как минимум одну управляющую конечную точку с номером 0, которая участвует во входных и выходных управляющих транзакциях. Формально конечные точки принадлежат конфигурациям, но конечная точка 0 является исключением — она завершает управляющий канал по умолчанию для устройства. Конечная точка 0 активна даже до того, как устройство получит свою конфигурацию, и независимо от наличия других конечных точек. Устройство не обязано иметь дополнительные управляющие конечные точки, помимо точки 0 (хотя спецификация USB допускает такую возможность), потому что конечная точка 0 нормально справляется с большинством управляющих запросов. Но если вы определите специфический запрос, который не может завершиться в пределах фрейма, создайте дополнительную управляющую конечную точку, чтобы работа встроенного обработчика не прерывалась новой транзакцией.
Каждая управляющая передача включает стадию настройки, за ней может следовать необязательная стадия данных, в ходе которой данные передаются устройству или от него. Далее следует стадия состояния, в которой устройство либо отвечает пакетом АСК или STALL, либо не отвечает вообще. На рис. 12.7 изображена стадия настройки, состоящая из маркера SETUP, фазы данных (не путайте со стадией данных в составе передачи) и фазы согласования. Стадии данных и состояния управляющей передачи подчиняются тем же правилам, как при массовой передаче (см. следующий подраздел). Устройства должны принимать управляющие передачи в любой момент времени, а следовательно, не могут отвечать кодом NAK (признаком занятой конечной точки). Отправка недействительного запроса управляющей конечной точке вызывает ответ STALL, но устройство автоматически сбрасывает состояние приостановки при получении следующего пакета SETUP. Существует специальный случай приостановки, названный в спецификации USB (см. раздел 8.5.3.4) протокольной приостановкой.
Маркер SETUP, предшествующий управляющей передаче, состоит из 8 байтов данных, как показано на рис. 12.8. На этой и других диаграммах, изображающих структуры данных, я показываю байты в порядке их передачи по кабелю USB, но биты в отдельных байтах перечисляются, начиная со старшего бита. Биты передаются, начиная с младшего, но программное обеспечение хоста и «прошивки» устройств обычно работают с данными после перестановки битов. Компьютеры Intel и протоколы шины USB используют представление данных с прямым порядком байтов (little-endian), при котором младший байт многобайтовых данных располагается по нижнему адресу. Микропроцессор 8051, используемый в некоторых чипсетах USB, в том числе и в чипсете Cypress Semiconductor, в действительности использует обратный порядок байтов (big-endian), поэтому микрокод должен позаботиться о соответствующей перестановке байтов данных.
Программная архитектура
529
| | Передает устройство
Рис. 12.7. Строение стадии настройки в управляющей передаче
bmRequest Туре	bFlequest	wValue	wlndex	wLength
х........Направление передачи:
О От хоста к устройству
1 От устройства к хосту
.XX......Тип запроса:
О Стандартный
1 Класс
*	2 Производитель
3 Зарезервировано
...х хххх Получатель:
О Устройство
1 Интерфейс
2 Конечная точка
3 Другие
4
- Зарезервировано
31
Рис. 12.8. Строение маркера SETUP
Обратите внимание: на рис. 12.8 первый байт маркера SETUP обозначает направление передачи информации, тип запроса и получателя управляющей передачи. Допустимые типы запросов — стандартный запрос (определяется в спецификации USB), запрос класса (определяется рабочей группой USB, ответственной за данный класс устройств) и запрос производителя (определяется производителем устройства). Управляющие запросы могут адресоваться устройству в целом, заданному интерфейсу, заданной конечной точке или иной сущности, определяемой производителем. Второй байт маркера SETUP указывает разновидность запроса,
530
Глава 12. USB
тип которого определяется первым байтом. В табл. 123 перечислены стандартные запросы, определенные в настоящее время. За информацией о запросах, относящихся к конкретным классам, обращайтесь к спецификации соответствующего класса устройств (см. первый URL-адрес, приведенный в начале главы). Производители устройств имеют право определять собственные коды запросов. Например, Cypress Semiconductor использует код запроса AOh для загрузки микрокода с хоста.
ПРИМЕЧАНИЕ----------------------------------------------------------------
Помните, что управляющие запросы, влияющие на состояние конкретной конечной точки, отправляются не ей, а управляющей конечной точке.
Таблица 12.3. Стандартные запросы устройств
Код запроса	Символическое имя	Описание	Возможные получатели
0	GET__STATUS	Получение информации состояния	Любой
1	CLEAR_FEATURE	Сброс функции с двумя состояниями	Любой
2	—	Зарезервировано	—
3	SET-FEATURE	Установка функции с двумя состояниями	Любой
4	—	Зарезервировано	—
5	SET-ADDRESS	Назначение адреса устройству	Устройство
6	GET-DESCRIPTOR	Получение дескриптора устройства, конфигурации или строки	Устройство
7	SET-DESCRIPTOR	Назначение дескриптора (не обязательно)	Устройство
8	GET-CONFIGURATION	Получение индекса текущей конфигурации	Устройство
9	SET-CONFIGURATION	Назначение индекса текущей конфигурации	Устройство
10	GET_INTERFACE	Получение индекса текущей альтернативной настройки	Интерфейс
11	SET-INTERFACE	Включение альтернативной настройки	Интерфейс
12	SYNCH_FRAI4E	Получение синхронизационного номера фрейма	(Изохронная) Конечная точка
Остаток пакета SETUP содержит код value, смысл которого зависит от запроса, значение index с такой же переменной интерпретацией и поле length, указывающее, сколько байтов данных должно быть передано во время стадии данных управляющей передачи. Если управляющий запрос адресуется конечной точке или интерфейсу, поле index содержит номер конечной точки или интерфейса. Значение 0 в поле length означает, что эта конкретная транзакция не имеет фазы данных.
Программная архитектура
531
Я не собираюсь во всех подробностях описывать все стандартные управляющие запросы — за полной информацией обращайтесь к разделу 9.4 спецификации USB. Тем не менее, я хочу кратко упомянуть концепцию возможностей устройства. USB предполагает, что любая адресуемая сущность, принадлежащая устройству, может обладать возможностями, состояние которых представляется одним битом. Две такие возможности стандартизированы для всех устройств, а еще одна возможность стандартизирована для контроллеров, концентраторов и функций, поддерживающих высокую скорость передачи.
Возможность DEVICE REMOTE WAKEUP (относящаяся к устройству в целом) указывает, может ли устройство использовать свою поддержку (если она имеется) удаленного пробуждения компьютера по внешнему событию. Программное обеспечение хоста (а конкретно драйвер шины) включает и отключает эту возможность, передавая устройству команду SET_FEATURE или CLEAR_FEATURE с кодом value 1. В DDK этот код обозначается символическим именем USB_FEATURE_ REMOTE_WAKEUP.
ВНИМАНИЕ-----------------------------------------------------------------
Прежде чем устанавливать бит DEVICE_REMOTE_WAKEUP в дескрипторе конфигурации, убедитесь в том, что ваше устройство действительно выдает сигнал пробуждения. Тесты WHQL для устройств USB проверяют, что устройство работает именно так, как было заявлено.
Возможность ENDPOINT„HALT (относящаяся к конечной точке) указывает, находится ли конечная точка в состоянии функциональной приостановки. Программное обеспечение хоста может приостановить конечную точку, отправив ей команду SET_FEATURE с кодом 0. Микрокод, управляющий конечной точкой, также может принять независимое решение о приостановке. Программное обеспечение хоста (также драйвер шины) сбрасывает состояние приостановки, отправляя команду CLEAR_FEATURE с кодом 0. В DDK данный код возможности обозначается символическим именем USB_FEATURE_ENDPOINT_STALL.
Установка возможности TEST_MODE (относящейся к устройству в целом) переводит устройство в специальных! тестовый режим, упрощающий проверку соответствия стандартам. Помимо концентраторов и контроллеров, только устройства, способные работать на высокой скорости, поддерживают эту возможность. Выход из тестового режима осуществляется не сбросом возможности TEST_MODE, а переключением питания.
В спецификации USB не задаются диапазоны кодов возможностей устройств или конечных точек, используемые производителями. Чтобы избежать в будущем возможных проблем со стандартизацией, старайтесь не определять новые возможности уровня устройства или конечной точки. Вместо этого следует определять собственные управляющие транзакции типа производителя. Впрочем, невзирая на этот совет, позднее в этой главе я приведу пример драйвера (FEATURE), управляющего 7-сегментным светодиодным индикатором на макетной плате Cypress Semiconductor. Для этого примера я определил возможность уровня интерфейса с номером 42. (Спецификация USB в настоящее время определяет несколько возможностей уровня интерфейса для управления питанием, поэтому не следует использовать мой пример для других целей, кроме изучения работы возможностей.)
532
Глава 12. USB
Массовые передачи
Массовая передача пересылает до 512 байт данных массовой конечной точке высокоскоростного устройства или от нее или до 64 байт данных массовой конечной точке полноскоростного устройства или от нее. Массовые передачи, как и управляющие, производятся без потерь данных. Но в отличие от управляющих передач массовые передачи не имеют гарантий в отношении задержки. Если у хоста остается место в фрейме или микрофрейме после резервирования других передач, он планирует незавершенные массовые передачи.
Рис. 12.9. Строение массовых и прерывающих передач
Программная архитектура
533
На рис. 12.9 показано строение массовой передачи. Передача начинается с маркера IN или OUT, обращенного к устройству и конечной точке. В случае выходной транзакции далее следуют фаза данных, в которой данные передаются от хоста к устройству, и фаза согласования, в которой устройство обеспечивает обратную связь состояния. Если конечная точка занята и не может принять новые данные, она генерирует пакет NAK в фазе согласования — хост попытается повторить выходную транзакцию позднее. Если конечная точка приостановлена, в фазе согласования выдается пакет пакет STALL — хост должен позднее сбросить состояние приостановки, прежде чем повторять передачу. Если конечная точка приняла и обработала данные правильно, она выдает пакет АСК в фазе согласования. Остается последний случай, когда конечная точка по какой-то причине не приняла данные и не вернула пакет согласования, — хост обнаруживает отсутствие подтверждения и автоматически повторяет попытку до трех раз.
После маркера IN, являющегося признаком входной массовой передачи, устройство выполняет одну из двух операций. Если возможно, оно отправляет данные хосту, на это хост либо генерирует пакет согласования АСК, являющийся признаком безошибочного получения данных, либо не реагирует, что является признаком ошибки. Если хост обнаруживает ошибку, при отсутствии подтверждения АСК данные остаются доступными -- позднее хост попытается повторить операцию ввода. Но если конечная точка занята или приостановлена, то вместо отправки данных устройство генерирует пакет NAK или STALL. NAK означает, что хост должен повторить операцию ввода позднее, a STALL требует выдачи команды сброса состояния приостановки.
Протокол управления передачей данных, используемый высокоскоростными выходными конечными точками, проектировался с таким расчетом, чтобы потери времени из-за безуспешных попыток передачи данных, не принимаемых конечной точкой, были сведены к минимуму. В USB 1.1 конечная точка сообщала о невозможности приема пакетом NAK. С другой стороны, в USB 2.0 значение blnterval в дескрипторах высокоскоростных массовых конечных точек задает частоту NAK. Если значение равно 0, конечная точка никогда не отправляет NAK. В остальных случаях параметр задает частоту (в микрофреймах), с которой конечная точка может посылать NAK для выходных транзакций. Хост при помощи специального пакета PING определяет, готова ли конечная точка к выводу, на что конечная точка отвечает пакетом АСК или NAK. После пакета NAK хост может принять решение об ожидании blnterval пакетов перед повторной отправкой PING. После АСК хост отправляет выходной пакет. Если конечная точка получает его нормальным образом, она отвечает пакетом АСК — если она может принять другой пакет, — или пакетом NYET в противном случае.
Прерывающие передачи
Прерывающая передача аналогична массовой передаче в том, что касается работы шины и устройства. Она перемещает до 1024 байт (высокая скорость), 64 байт (полная скорость) или 8 байт (низкая скорость) данных без потерь к конечной точке или от нее. Основное отличие прерывающей передачи от массовой передачи
534
Глава 12. USB
связано с задержкой. Прерывающая конечная точка задает интервал опроса (см. ранее в этой главе). Хост резервирует часть пропускной способности, достаточную для выполнения транзакций 10 или OUT, обращенных к конечной точке, с частотой по крайней мере не меньшей интервала опроса.
ПРИМЕЧАНИЕ----------------------------------------------------------------------
Устройства USB не генерируют асинхронные прерывания — они всегда отвечают на опрос. Полезно знать, что драйверы хостовых контроллеров Microsoft округляют интервал опроса, указанный в дескрипторе конечной точки, до степени 2, не превышающей 32. Например, конечная точка с интервалом опроса 31 мс в действительности будет опрашиваться каждые 16 мс. Если заданный интервал опроса лежит в диапазоне от 32 до 255 мс, используется интервал опроса, равный 32 мс.
Изохронные передачи
Изохронная передача перемещает до 3072 байт данных к высокоскоростной ко-нечной точке или от нее в течение каждого микрофрейма или до 1023 байт данных к массовой конечной точке или от нее в течение каждого фрейма шины. Из-за гарантированной периодичности изохронные передачи идеально подходят для пересылки данных, критичных по времени, — например, аудиосигналов. Тем не менее, гарантия периодичности не дается даром: изохронные передачи с поврежденными данными не повторяются автоматически — в сущности, при изохронной передаче опоздание не лучше ошибки, поэтому в повторной пересылке нет смысла.
Изохронная передача состоит из маркера IN или OUT, за которым следует фаза данных, при которой данные перемещаются к хосту или от него. Фаза согласования отсутствует, так как повторные попытки в случае ошибок но производятся (рис. 12.10).
Фаза	Фаза
| | Передает хост
П Передает устройство
Рис. 12.10. Строение изохронной передачи
Хост резервирует до 80 % пропускной способности шины (90 % в USB 1.1) для изохронных и прерывающих передач. Системные программы должны заранее
Программная архитектура
535
резервировать пропускную способность, чтобы обеспечить обслуживание всех активных устройств. В USB 1.1 доступная пропускная способность преобразуется в примерно 1500 байт на 1-миллисекундный фрейм, или приблизительно 1,5 конечных точек максимального размера. В USB 2.0 она преобразуется в 7400 байт на 125-микросекуидпый микрофрейм, или приблизительно 2,5 конечных точек максимального размера. С учетом повышенной скорости передачи данных USB 2.0 почти в 20 раз превосходит USB 1.1 в области изохронной передачи.
Дескрипторы
Устройства USB содержат встроенные (внутриплатные) структуры данных, которые называются дескрипторами и обеспечивают их самоидентификацию для программного обеспечения хоста. Различные типы дескрипторов перечислены в табл. 12.4. Каждый дескриптор начинается с 2-байтового заголовка, содержащего размер дескриптора в байтах и код типа. Если исключить особый случай строкового дескриптора (см. далее подраздел «Строковые дескрипторы»), длина дескриптора определяется его типом, потому что все дескрипторы одного типа имеют одинаковую длину. Тем не менее, явное значение длины включено в заголовок для обеспечения возможных расширений в будущем. За фиксированным заголовком следуют данные, специфические для конкретного типа.
В оставшейся части этого раздела я опишу строение каждого типа дескрипторов с использованием структур данных, определенных в DDK (и конкретно в файле USB100.H). Официальное представление этой информации содержится с разделе 9.6 спецификации USB.
Таблица 12.4. Типы дескрипторов
Тип дескриптора	Описание
Дескриптор устройства	Описывает все устройство
Дескриптор квалификатора устройства	Информация о конфигурации устройства для другой скорости работы
Дескриптор конфигурации	Описывает одну из возможных конфигураций устройства
Дескриптор конфигурации для другой скорости	Дескриптор конфигурации для другой скорости работы
Дескриптор интерфейса	Описывает один из интерфейсов, входящих в конфигурацию
Дескриптор конечной точки	Описывает одну из конечных точек, принадлежащих интерфейсу
Строковый дескриптор	Содержит строку в Юникоде, описывающую устройство, конфигурацию, интерфейс или конечную точку
Дескрипторы устройств
С каждым устройством связывается один дескриптор устройства, идентифицирующий устройство для хостового программного обеспечения. Для получения
536
Глава 12. USB
этого дескриптора хост направляет конечной точке 0 управляющую транзакцию GET_DESCRIPTOR. Дескриптор устройства определяется в DDK следующим образом:
typedef struct JJSB_DEVICE_DESCRIPTOR {
UCHAR bLength;
UCHAR bDescrlptorType;
USHORT bcdUSB:
UCHAR bDevIceClass;
UCHAR bOeviceSubClass;
UCHAR bDeviceProtocol;
UCHAR bMaxPacketSIzeO;
USHORT IdVendor;
USHORT IdProduct;
USHORT bcdDevIce;
UCHAR 1 Manufacturer;
UCHAR 1 Product;
UCHAR 1 Serial Number;
UCHAR bNumConflguratlons;
} USBJDEVICEJ3ESCRIPTOR, *PUSB_DEVICE_DESCRIPTOR;
Поле bLength в дескрипторе устройства равно 18, а поле bDescriptorType содержит значение 1 (признак дескриптора устройства). Поле bcdUSB содержит код версии (в двоично-десятичном представлении), обозначающий версию спецификации USB, которой соответствует этот дескриптор. У новых устройств это значение равно 0x0200 — признак соответствия спецификации 2.0.
Значения bDeviceClass, bDeviceSubClass и bDeviceProtocol идентифицируют тип устройства. Коды классов устройств определены в спецификации USB, их состав на момент написания книги перечислен в табл. 12.5. Рабочие группы по классам устройств в комитете USB определяют коды подклассов и протоколов для каждого класса устройств. Например, в классе аудиоустройств имеются коды подклассов для управляющих, потоковых и MIDI-потоковых интерфейсов, а в классе запоминающих устройств определены коды протоколов для различных методов использования конечных точек при пересылке данных.
Класс может задаваться как для всего устройства, так и на уровне интерфейсов, но на практике класс устройства, подкласс и код протокола чаще хранятся в дескрипторе интерфейса, нежели в дескрипторе устройства (в таких случаях эти коды в дескрипторе устройства равны 0). Спецификация USB также определяет «запасной выход» для необычных типов устройств в форме кода класса 255. Разработчик может использовать этот код типа для обозначения нестандартного устройства, у которого коды подкласса и протокола содержат описания, заданные производителем. Например, устройство, построенное на чипсете Cypress Semiconductor, поставляется с дескриптором устройства, у которого коды класса, подкласса и протокола равны 255 (устройство содержит обширный набор конечных точек и может принимать управляющие запросы на загрузку микрокода, который наделяет устройство новой «личностью» с (новым) набором дескрипторов).
Программная архитектура
537
Поле bMaxPacketSizeO дескриптора устройства содержит максимальный размер пакета данных для управляющей передачи через конечную точку 0. Для этой конечной точки (реализуемой каждым устройством) не существует отдельного дескриптора конечной точки, поэтому это единственное место, в котором может задаваться данное значение. Поскольку поле хранится в дескрипторе со смещением 7, хост всегда может прочитать достаточную часть дескриптора для выборки этого значения, даже если конечная точка 0 способна только на минимальный размер передачи (8 байт). Узнав размер передач для конечной точки 0, хост соответствующим образом структурирует последующие запросы.
Поля idVendor и idProduct задают код производителя и идентификатор продукта, назначаемый производителем. Поле bcdDevice задает номер версии устройства (например, 0x0100 для версии 1.0). Эти три поля определяют, какой драйвер будет загружаться хостовым программным обеспечением при обнаружении устройства. Организация USB назначает коды производителям, а каждый производитель назначает свои коды продуктам.
Таблица 12.5. Коды классов устройств USB		
Символическое имя в заголовке DDK	Код класса	Описание
USB_DEVICE_CLASS_RESERVED	0	Означает, что коды класса хранятся в дескрипторах интерфейсов
USB_DEVICE_CLASS_AUDIO	1	Устройства, предназначенные для обработки аналогового или цифрового аудио, голоса и других звуковых данных (не включая транспортные механизмы)
USB_DEVICE_CLASS_COMMUNICAnONS	2	Телекоммуникационные устройства (модемы, телефоны и т. д.)
USB_DEVICE_CLASS_HUMAN_INTERFACE	3	Устройства пользовательского интерфейса (HID): клавиатуры, мыши и т. д.
USB_DEVICE_CLASS_MONTTOR	4	Мониторы
USB_DEVICE_CLASS_PHYSICAL_INTERFACE	5	Устройства HID с физической обратной связью в реальном времени (например, активные джойстики)
USB_DEVICE_CLASS_POWER	6	Устройства с поддержкой управления питанием: аккумуляторы, зарядные устройства и т. д.
USB_DEVICE_CI_ASS_PRINTER	7	Принтеры
USB_DEVICE_CLASS_STORAGE	8	Запоминающие устройства большой емкости (диски, CD-ROM и т. д.)
USB_DEVICE_CLASS_HUB	9	Концентраторы USB
	10	Коммуникационные данные
продолжение &
538
Глава 12. USB
Таблица 12.5 (продолжение)
Символическое имя в заголовке DDK	Код класса	Описание
	11	Устройство чтения смарт-карт
	12	Безопасность содержания
	220	Диагностические устройства
	224	Контроллеры беспроводной связи (например, Bluetooth)
	254	Специализированные (обновление микрокода, мосты IRDA (Infrared Data Association) и т. д.)
USB_DEVICE_CLASS_VENDOR_SPECIFIC	255	Класс устройства, определяемый производителем
ПРИМЕЧАНИЕ —-------------------------------------------------------------------
Microsoft настоятельно рекомендует производителям увеличивать номер версии устройства для каждого обновления оборудования или микрокода, поскольку это упростит нисходящие программные обновления. Нередко производитель выпускает новую версию оборудования вместе с обновленным драйвером. Кроме того, после обновления оборудования могут стать недействительными некоторые программные «заплатки» или фильтрующие драйверы, введенные для исправления ошибок оборудования. Если механизму автоматического обновления системы не удастся определить, с какой версией оборудования он работает, у него могут возникнуть трудности с обновлением системы.
Поля {Manufacturer, iProduct и iSerialNumber идентифицируют строковые дескрипторы, содержащие пользовательские описания производителя, продукта и серийного номера устройства. Все эти строки не являются обязательными, и значение О в любом из этих полей указывает на отсутствие дескриптора. Если устройству присваивается серийный номер, Microsoft рекомендует делать его уникальным для каждого физического устройства. Если вы последуете этому совету, а ваш драйвер будет снабжен цифровой подписью, конечный пользователь сможет свободно переключать устройство между разными портами одного компьютера, и оно будет распознаваться как одно и то же устройство.
Наконец, поле bNumConfigurations указывает, сколько конфигураций способно поддерживать устройство. Драйверы Microsoft работают только с первой конфигурацией устройства. Позднее я объясню, что делать с устройством, имеющим несколько конфигураций (см. подраздел «Конфигурация»).
Дескриптор квалификатора устройства
Высокоскоростные устройства USB 2.0 могут работать либо на высокой, либо на полной скорости. Они передают дескриптор устройства, соответствующий фактической скорости, на которой работает устройство. Кроме того, они передают дескриптор квалификатора устройства с информацией уровня устройства, которая может измениться при переходе на другую скорость:
typedef struct _USBJEVICE_QUALIFIERJESCRIPTOR {
UCHAR bLength;
UCHAR bDescriptorType;
Программная архитектура
539
USHORT bcdUSB;
UCHAR bDeviceClass;
UCHAR bDeviceSubClass;
UCHAR bDeviceProtocol;
UCHAR bMaxPacketSlzeO;
UCHAR bNumConflguratlons;
UCHAR bReserved;
} USB_DEVICE_QUALZFIER_DESCRIPTOR, *PUSBJEVICEJUAL1FIER_DESCRIPTOR;
Кроме полей bLength (10) и bDescrlptorType (6), все эти поля имеют точно такой же смысл, как в дескрипторе устройства. Дескриптор не повторяет поля производителя, продукта, устройства, изготовителя и продукта из дескриптора устройства эти поля остаются постоянными для всех поддерживаемых скоростей.
Дескрипторы конфигураций
Каждое устройство обладает одним или несколькими дескрипторами конфигураций, которые описывают различные конфигурации, поддерживаемые устройством. Чтобы получить этот дескриптор, системное программное обеспечение направляет конечной точке 0 управляющую транзакцию GET_DESCRIPTOR. Дескриптор конфигурации определяется в DDK следующим образом:
typedef struct _USB_CONFIGURATION_DESCRIPTOR {
UCHAR bLength;
UCHAR bDescrlptorType;
USHORT wTotalLength:
UCHAR bNumlnterfaces;
UCHAR bConfigurationValue;
UCHAR iconfiguration;
UCHAR bmAttrlbutes;
UCHAR MaxPower;
} USB_CONFIGURATION_DESCRIPTOR, *PUSB_CONFIGURATION_DESCRIPTOR;
Поля bLength и bDescrlptorType содержат значения 9 и 4 соответственно, это означает, что длина дескриптора интерфейса равна 9 байтам. Поля blnterfaceNumber и bAlternateSetting содержат индексы, которые могут использоваться в управляющих транзакциях SET_INTERFACE для активизации интерфейсов. Нумерация интерфейсов внутри конфигурации, а также альтернативных настроек в интерфейсах должна начинаться с 0, потому что системное программное обеспечение и микрокод часто интерпретируют номер интерфейса как индекс массива.
Поле bNumEndpoint указывает, сколько конечных точек (кроме точки 0, которая считается присутствующей всегда) входит в интерфейс.
Поля blnterfaceClass, blnterfaceSubClass и blnterfaceProtocol описывают функциональность, предоставляемую устройством. Отличный от нуля код класса должен быть одним из кодов классов устройств, о которых я говорил ранее, в этом случае коды подкласса и протокола должны иметь тот же смысл. Нулевые значения в этих полях в настоящее время не разрешены — они зарезервированы для будущей стандартизации.
540
Глава 12. USB
Наконец, поле ilnterface содержит индекс строкового дескриптора с описанием интерфейса в Юникоде. Значение 0 означает, что строка с описанием отсутствует. Строки с описаниями интерфейсов желательно определять для составных устройств, потому что родительский драйвер использует их при перечислении интерфейсов как дочерних устройств.
Дескрипторы конечных точек
С каждым интерфейсом связан ноль или более дескрипторов конечных точек, которые описывают конечные точки, используемые при проведении транзакций с хостом. Системные программы получают дескрипторы конечных точек только как часть управляющего запроса GET_DESCRIPTOR, читающего весь дескриптор конфигурации, частью которого является дескриптор конечной точки. В DDK структура дескриптора конечной точки определяется следующим образом:
typedef struct JJSB_INTERFACE_DESCRIPTOR {
UCHAR bLength;
UCHAR bDescrlptorType;
UCHAR blnterfaceNumber;
UCHAR bAlternateSetting;
UCHAR bNumEndpolnts;
UCHAR blnterfaceClass;
UCHAR blnterfaceSubClass;
UCHAR blnterfaceProtocol;
UCHAR Ilnterface;
} USBJNTERFACEJDESCRIPTOR, *PUSB_INTERFACE_DESCRIPTOR;
Поля bLength и bDescrlptorType содержат значения 7 и 5 соответственно, это означает, что длина дескриптора конечной точки равна 7 байтам. Поле bEndPoint-Address содержит информацию о направленности и количестве конечных точек (рис. 12.11). Например, адрес 0x82 обозначает конечную точку IN с номером 2, а адрес 0x02 — конечную точку OUT с тем же номером 2. За исключением конечной точки 0, спецификация USB позволяет иметь две разные конечные точки с одинаковым номером, передающие данные в разных направлениях. Впрочем, многие чипсеты USB не поддерживают подобную перегрузку номеров конечных точек.
1 бит
3 бита
4 бита
Направление		Номер
------►1=Ввод
0=Вывод
Рис. 12.11. Назначение битов в адресном поле дескриптора конечной точки
На рис. 12.12 представлено строение поля bmAttributes дескриптора конечной точки. Биты 0-1 определяют тип конечной точки в соответствии с типами
Программная архитектура
541
пересылки данных, перечисленными в табл. 12.1. Биты 2-5 определяют дополнительные атрибуты для изохронных конечных точек.

2 бита 2 бита 2 бита 2 бита
*			
0-1 Тип конечной точки:
0 USBENDPOfNTTYPECONTROL
---И USB_ENDPOINT_TYPE_ISOCHRONOUS
2 USB_ENDPOINT_TYPE_BULK
3 USB_ENDPOINT_TYPEJNTERRUPT
Управляющая Изохронная Массовая Прерывающая
2-3 Тип синхронизации изохронной точки:
0 Нет синхронизации
*1 Асинхронная
2 Адаптивная
3 Синхронная
2-3 Тип использования изохронной точки:
0 Конечная точка данных
* 1 Конечная точка обратной связи
2 Конечная точка косвенной обратной связи
3(Зарезервировано)
Рис. 12.12. Назначение битов в поле атрибутов дескриптора конечной точки
Значение wMaxPacketSize обозначает наибольший объем данных, передаваемых конечной точкой за одну транзакцию. Структура этого поля показана на рис. 12.13, а в табл. 12.2 перечислены его допустимые значения для всех типов конечных точек. Например, для управляющей или массовой конечной точки на полноскоростном устройстве задаются значения 8, 16, 32 или 64. Высокоскоростные прерывающие и изохронные конечные точки могут выполнять 1, 2 или 3 транзакции за микрофрейм, что обозначается битами 11-12 этого поля. Другие типы транзакций позволяют пересылать дополнительные данные (табл. 12.6).
3 бита 2 бита
ч------и------►
11 бит
		
----► 0-10 Максимальный размер пакета
11-12 Дополнительные транзакции за микрофрейм:
____________________k 0 Нет
1	1 дополнительная
2	2 дополнительные
3(Зарезервировано)
Рис. 12.13. Назначение битов в поле размера пакета дескриптора конечной точки
542
Глава 12. USB
Таблица 12.6. Зависимость размера пакета и объема передачи от количества дополнительных транзакций
Количество допол н ител ьн ых транзакций	Допустимые значения wMaxPacketSize (биты 0-10)	Общий объем данных на микрофрейм
0	1-1024	<1024
1	513-1024	<2048
2	683-1024	<3072
В дескрипторах прерывающих и изохронных конечных точек интервал опроса задается в поле blnterval. Как было сказано ранее в этой главе, это число указывает, с какой периодичностью хост должен опрашивать конечную точку на предмет возможной пересылки данных. В дескрипторах высокоскоростных массовых конечных точек поле blnterval содержит частоту выдачи NAK (см. ранее).
Строковые дескрипторы
Дескриптор устройства, конфигурации или конечной точки может содержать необязательные индексы строк с описаниями, понятными для пользователя. Сами строки хранятся на устройстве в Юникоде в форме строковых дескрипторов USB. Системное программное обеспечение читает строковые дескрипторы, отправляя управляющий запрос GET_DESCRIPTOR управляющей конечной точке 0. Объявление структуры строкового дескриптора в DDK выглядит так:
typedef struct _USB_STRING_DESCRIPTOR {
UCHAR bLength;
UCHAR bDescrlptorType;
WCHAR bStringEU;
} USBJTRI ^DESCRIPTOR. *PUSB_STRING_DESCRIPTOR;
В поле bLength хранится длина строковых данных в байтах. Поле bDescrlptorType содержит значение 3, которое означает, что это строковый дескриптор. Поле bString содержит сами строковые данные без нуль-завершителя.
Устройства USB могут поддерживать строки на разных языках. Строка с номером 0 представляет собой не символьную строку, а массив поддерживаемых языковых идентификаторов. (Индекс строки 0, используемый в другом дескрипторе, означает отсутствие ссылки на строку. Таким образом, индекс 0 зарезервирован для специального использования.) Идентификаторы языков относятся к типу LANGID, используемому в программах Win32. Например, код 0x0409 обозначает американский диалект английского языка. Спецификация USB требует, чтобы при запросе строкового дескриптора, поддержка которого не заявлена устройством, последнее возвращало ошибку, поэтому перед выдачей запросов на строковые дескрипторы следует прочитать содержимое массива «нулевой строки». За дополнительной информацией об идентификаторах языков обращайтесь к разделу 9.6.7 спецификации USB.
Работа с драйвером шины
543
Другие дескрипторы
Спецификация USB продолжает развиваться, и я лишь описал ее состояние на момент написания книги. Во многих спецификациях классов USB определяются дескрипторы, специфические для класса, они появляются в блоках данных, возвращаемых запросом на чтение дескриптора конфигурации. Обсуждение этих специфических дескрипторов выходит за рамки книги, я лишь скажу, что специфические дескрипторы следуют за дескриптором интерфейса, к которому они относятся, и предшествуют дескрипторам конечных точек этого интерфейса.
Работа с драйвером шины
В отличие от драйверов устройств, подключаемых к традиционным шинам PC (например, PCI), драйвер устройства USB никогда не взаимодействует с оборудованием напрямую. Вместо этого он создает экземпляр структуры данных, называемой блоком запроса USB, и передает ее родительскому драйверу.
Для отправки блока запроса USB (URB) родительскому драйверу используется IRP с основным кодом функции IRP_MJ_INTERNAL_DEVICE_CONTROL В некоторых ситуациях функции можно вызывать напрямую через интерфейс прямого вызова родительского драйвера. В свою очередь, родительский драйвер резервирует в том или ином фрейме время на выполнение операции, закодированной в URB.
В этом разделе я опишу механику работы с родительским драйвером для выполнения типичных операций, реализуемых функциональным драйвером USB. Сначала я расскажу, как построить и отправить URB. Затем мы обсудим механику определения и изменения конфигурации устройства. Напоследок я в общих чертах опишу работу с каждым из четырех типов коммуникационных каналов.
Инициирование запросов
Чтобы создать блок URB, сначала выделите память для структуры URB, а затем вызовите функцию инициализации для заполнения полей, соответствующих типу отправляемого запроса. Предположим, вы начинаете задавать конфигурацию устройства в ответ на полученный запрос IRP_MN_START_DEVICE. Одной из первых выполняемых задач может стать чтение дескриптора устройства. Для этой цели можно воспользоваться следующим фрагментом:
USB_DEVICE_DESCRIPTOR dd:
URB urb;
UsbBuildGetDescriptorRequest(&urb,
slzeof(_URB-CONTROL_DESCRIPTOR_REQUEST),
USB_DEVICE_DESCRIPTOR_TYPE, 0. 0. &dd. NULL.
slzeof(dd), NULL);
Сначала мы объявляем локальную переменную (с именем urb) для хранения структуры данных URB. Тип URB объявляется в файле USBDI.H как объединение нескольких субструктур, по одной для каждого из запросов к устройству USB.
544
Глава 12. USB
Мы будем использовать субструктуру UrbControlDescriptorRequest объединения URB, которая объявляется как экземпляр struct _URB_CONTROL_DESCRIPTOR_ REQUEST. Подобное использование автоматической переменной допустимо, если вы знаете, что в стеке достаточно места для хранения наибольшего из возможных URB, и будете ожидать завершения URB до того, как переменная выйдет из области видимости.
Конечно, при желании память для URB можно динамически выделить из кучи:
PURB urb = (PURB) ExAllocatePool (NonPagedPool,
sizeof(_URB_CONTROL_DESCRIPTOR_REQUEST));
if (!urb) 
return STATUS_INSUFFICIENT_RESOURCES;
UsbBui1dGetDescri ptorRequest(urb, ...);
ExFreePool(urb);
UsbBuildGetDescriptorRequest документируется как обычная сервисная функция, но в действительности это макрос (объявленный в USBDLIB.H), который генерирует подставляемые команды для инициализации полей субструктуры запроса на получение дескриптора. В заголовочных файлах DDK такие макросы определяются для большинства типов URB, которые вы будете строить (табл. 12.7). Как и в случае с другими препроцессорными макросами, старайтесь избегать передачи в аргументах макроса выражений с побочными эффектами.
Таблица 12.7. Вспомогательные макросы для построения URB
Макрос	Тип транзакции
UsbBuildlnterruptOrBulkTransferRequest	Входная или выходная к прерывающей или массовой конечной точке
UsbBuildGetDescriptorRequest	Управляющий запрос GET_DESCRIPTOR для конечной точки 0
UsbBuildGetStatusRequest	Запрос GET_STATUS для устройства, интерфейса или конечной точки
UsbBuildFeatu reRequest	Запрос SET_FEATURE или CLEAR_FEATURE для устройства, интерфейса или конечной точки
UsbBuildSelectConfigurationRequest	SET_CON FIGURATION
UsbBuildSelectlnterfaceRequest	SETJNTERFACE
UsbBuildVendorRequest	Произвольный управляющий запрос, определяемый производителем
В предыдущем фрагменте кода мы указываем, что запрос должен загрузить дескриптор устройства в локальную переменную (dd), адрес и длину которой мы предоставляем. URB, в которых задействуется пересылка данных, позволяют задать неперемещаемый буфер данных одним из двух способов. Можно указать виртуальный адрес и длину буфера, как это было сделано в этом фрагменте. Кроме того, вы можете передать список дескрипторов памяти (MDL), для которого уже была вызвана функция MmProbeAndLockPages.
Работа с драйвером шины
545
ДОПОЛНИТЕЛЬНО О URB---------------------------------------------------------------
В своем внутреннем представлении драйвер шины всегда использует списки MDL для описания буферов данных. Если задать адрес буфера, родительский драйвер создаст MDL сам. Если у вас уже имеется MDL, было бы неэффективно вызывать MmGetSystemAddressForMdlSafe и передавать полученный виртуальный адрес родительскому драйверу: родительский драйвер сделает шаг в обратном направлении и создаст еще один список MDL для описания того же буфера!
URB также содержит связующее поле Urblink, которое используется родительским драйвером для одновременной отправки серий URB драйверу хостового контроллера. У различных макрофункций инициализации URB также имеется аргумент, в котором, теоретически, можно задать значение этого связующего поля. Рядовые программисты всегда должны передавать в нем NULL, потому что концепция связывания URB еще не была реализована в полной мере, — более того, попытки связать URB пересылки данных приводят к системным сбоям.
Отправка URB
После создания URB необходимо создать и отправить внутренний запрос IOCTL родительскому драйверу, расположенному на одном из нижних уровней иерархии драйверов вашего устройства. Обычно требуется дождаться ответа драйвера, в таких случаях используется вспомогательная функция следующего вида:
NTSTATUS SendAwaitUrb(PDEVICE_OBJECT fdo, PURB urb) { PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Devi ceExtensi on;
KEVENT event;
KelnitializeEventl&event, NotlficationEvent, FALSE);
IO_STATUS_BLOCK iostatus;
PIRP Irp = loBuildDeviceloControlRequest
(IOCTL_INTERNAL_USB_SUBMIT_URB, pdx->LowerDeviceObject, NULL, 0, NULL. 0. TRUE, Sevent, Siostatus);
PIO_STACK_LOCATION stack = loGetNextlrpStackLocation(Irp); stack->Parameters.Others.Argument! = (PVOID) urb;
NTSTATUS status = IoCaIIDriver(pdx->LowerDeviceObject, Irp);
if (status = STATUS_PENDING) {
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
status = iostatus.Status;
}
return status;
}
Это обычный пример создания и отправки синхронного IRP другому драйверу (сценарий 6 из главы 5). Единственная тонкость связана со способом упаковки «письма» URB в «конверт» INTERNAL_DEVICE_CONTROL: указатель на URB заносится в поле Parameters.Others.Argumentl в стеке.
ПРИМЕЧАНИЕ--------------------------------------------------------------------------
Стоит еще раз подчеркнуть, что драйвер упаковывает URB в обычные IRP с основным кодом функции IRP_MJ_INTERNAL_DEVICE_CONTROL. Чтобы верхний фильтрующий драйвер отправлял собственные URB, каждый драйвер устройства USB должен иметь диспетчерскую функцию, которая передает этот IRP вниз на следующий уровень.
546
Глава 12. USB
Возврат информации состояния URB
При отправке URB драйверу шины USB вы в конечном итоге получаете код NTSTATUS, описывающий результат операции. Во внутреннем представлении драйвер шины использует другой набор кодов состояния с typedef-именем USBD_ STATUS. Эти коды не являются кодами NTSTATUS.
Когда родительский драйвер завершает URB, он заносит в поле URB UrbHeader. Status одно из значений USBD_STATUS. Вы можете проанализировать это значение в своем драйвере, чтобы получить дополнительную информацию о том, как прошла обработка URB. Для упрощения доступа можно воспользоваться макросом URB_STATUS из DDK:
NTSTATUS status = SendAwaltUrb(fdo, &urb);
USBDJTATUS ustatus = URB_STATUS(&urb);
Тем нс менее, не существует специального протокола для сохранения этого состояния и его возврата приложению. Вы более или менее свободны делать с ним все, что сочтете нужным.
Конфигурация
Драйвер шины USB автоматически обнаруживает подключение новых устройств USB. Затем он читает структуру дескриптора устройства, чтобы определить, какое устройство неожиданно появилось в системе. Поля идентификаторов производителя и продукта в сочетании с другими дескрипторами определяют, какой драйвер следует для него загрузить.
Configuration Manager вызывает функцию драйвера AddDevice обычным способом. Функция AddDevice выполняет все задачи, которые вам уже известны: она создает объект устройства, вводит его в иерархию драйверов и т. д. В конечном итоге Configuration Manager отправляет драйверу запрос Plug and Play IRP_MN_ START-DEVICE. В главе 6 я показал, как обработать этот запрос вызовом вспомогательной функции StartDevice с аргументами, описывающими преобразованные и непреобразованные назначения ресурсов устройства. Могу вас обрадовать: в драйверах USB вам вообще не придется беспокоиться о ресурсах ввода/вывода, потому что их нет. Следовательно, заготовка вспомогательной функции Start-Device будет выглядеть так:
NTSTATUS StartDev1ce(PDEVICE_OBJECT fdo)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
<настройка конфигурации устройства>
return STATUS_SUCCESS;
Ранее я легко назвал «настройкой конфигурации устройства» довольно большой объем кода по настройке оборудования. Но, как было сказано ранее, вам не обязательно беспокоиться о портах ввода/вывода, прерываниях, объектах адап-
Работа с драйвером шины
547
теров прямого доступа к памяти (DMA) или других ресурсно-ориентированных элементах, упоминавшихся в главе 7.
ГДЕ НАХОДИТСЯ ДРАЙВЕР? —--------------------------------------------------------
Механика установки драйверов WDM будет рассмотрена в главе 15, однако некоторые детали полезно знать именно сейчас. Предположим, устройство обладает идентификатором производителя 0x0547, а его идентификатор продукта равен 0х102А. Для этого примера я позаимствовал идентификатор производителя, принадлежащий компании Cypress Semiconductor (с ее разрешения). Идентификатор продукта взят из примера USB42, который вы найдете в прилагаемых материалах. В спецификации USB описано много способов поиска драйвера устройства (или набора драйверов) на основании дескрипторов устройства, конфигурации и интерфейсов — см. «Universal Serial Bus Common Class Specification (Rev. 1.0, December 16, 1997)», раздел 3.10. Все мои примеры базируются на методе вторичного приоритета, при котором драйвер определяется исключительно по идентификатору производителя и продукта.
Столкнувшись с устройством, обладающим заданными идентификаторами производителя и продукта, PnP Manager ищет в реестре информацию об устройстве с именем USB\VID_0547&PID_102A. Если такой записи не существует, PnP Manager запускает мастер нового оборудования для поиска INF-файла с описанием такого устройства. Мастер может запросить у пользователя диск или попробует найти INF-файл, уже находящийся на компьютере. Затем мастер устанавливает драйвер и заполняет реестр. Получив информацию из реестра, PnP Manager может динамически загрузить драйвер — здесь и начинается наша работа.
Далее приводится краткое описание того, что необходимо сделать в функции StartDevice. Сначала вы выбираете конфигурацию устройства. Большинство устройств имеет всего одну конфигурацию. После выбора конфигурации выбираются интерфейсы, являющиеся частью этой конфигурации. Устройства, поддерживающие несколько интерфейсов, встречаются довольно часто. После выбора конфигурации и набора интерфейсов драйверу шины отправляется запрос URB на выбор конфигурации. В свою очередь, драйвер шины выдает устройству команды на активизацию конфигурации и интерфейсов. Драйвер шины создает коммуникационные каналы, позволяющие взаимодействовать с конечными точками через выбранные интерфейсы, и предоставляет манипуляторы для обращения к каналам. Кроме того, он создает манипуляторы для конфигурации и интерфейсов. Манипуляторы извлекаются из завершенного URB и сохраняются для будущего использования. После этого процесс конфигурации считается законченным.
СОСТАВНЫЕ УСТРОЙСТВА-------------------------------------------------------------
Если устройство обладает одной конфигурацией и несколькими интерфейсами, универсальный родительский драйвер Microsoft автоматически работает с ним как с составным (или многофункциональным) устройством. Функциональные драйверы для каждого из интерфейсов устройства предоставляются в виде INF-файлов, в которых указывается индекс подфункции вместе с идентификаторами производителя и продукта. Универсальный родительский драйвер создает объект физического устройства (PDO) для каждого интерфейса, в то время как PnP Manager загружает отдельные функциональные драйверы, предоставленные вами. Когда один из этих функциональных драйверов читает дескриптор конфигурации, универсальный родительский драйвер предоставляет отредактированную версию дескриптора, описывающую всего один интерфейс.
За дополнительной информацией о разных формах идентификаторов устройств в INF-файлах обращайтесь к главе 15.
548
Глава 12. USB
Чтение дескриптора конфигурации
Дескриптор конфигурации фиксированного размера лучше всего представить себе в виде заголовка структуры переменного размера, описывающей конфигурацию, все ее интерфейсы и все конечные точки интерфейсов (рис. 12.14).
Дескриптор интерфейса
Дескриптор конечной точки
Дескриптор конечной точки
Дескриптор конечной точки
Дескриптор интерфейса
Дескриптор _____конечной точки
Дескриптор конечной точки
> «Объединенный дескриптор», ' читаемый за одну управляющую передачу
Рис. 12.14. Структура дескриптора конфигурации
Вся структура переменной длины должна быть прочитана в смежную область памяти, потому что оборудование не позволит вам напрямую обращаться к дескрипторам интерфейсов и конечных точек. К сожалению, длина объединенной структуры заранее не известна. Следующий фрагмент кода показывает, как прочитать дескриптор конфигурации при помощи двух URB:
ULONG Iconfig = 0;
URB urb:
USB_CONFIGURATION_DESCRIPTOR ted:
UsbBu11dGetDescr1ptorRequest(&urb,
s1zeof(JJRB_CONTROL_DESCRIPTOR_REQUEST),
USB_CONFIGURATION_DESCRIPTOR_TYPE,
Iconfig. 0. Sited, NULL, slzeof(tcd), NULL):
SendAwaltUrbffdo, &urb);
ULONG size = ted.wTotalLength;
PUSB_CONFIGURATION_DESCRIPTOR ped =
(PUSB_CONFIGURATION_DESCRIPTOR) ExAllocatePool( NonPagedPool, size);
UsbBul1dGetDescrlptorRequest(&urb,
s1zeof(_URB_CONTROL__DESCRIPTOR_REQUEST),
USB_CONFIGURATION_DESCRIPTOR_TYPE,
Iconfig, 0, ped, NULL, size, NULL):
SendAwaltUrbtfdo, &urb);
ExFreePool(ped);
Работа с драйвером шины
549
В этом фрагменте мы выдаем один URB для чтения дескриптора конфигурации (индекс 0 определяет первый дескриптор) во временную область с именем ted. Этот дескриптор содержит длину (wTotalLength) объединенной структуры, которая содержит дескрипторы конфигурации, интерфейса и конечных точек. Мы выделяем блок памяти нужного размера и выдаем второй URB для чтения всего дескриптора. В конце этого процесса переменная ped указывает на полный набор данных (не опускайте проверку ошибок, как я сделал в этом фрагменте, — в прилагаемых материалах содержатся примеры обработки многочисленных ошибок, которые могут возникнуть в этом коротком фрагменте).
СОВЕТ-------------------------------------------------------------------------------------
Дескрипторы конфигураций читаются с использованием индексов, начинающихся с 0. При выдаче управляющей транзакции на включение этой конфигурации драйвер шины использует значение bConfigurationValue из дескриптора конфигурации. Обычно присутствует всего один дескриптор конфигурации с номером 1, для чтения которого используется индекс 0. У вас еще голова не идет кругом?
Если устройство обладает единственной конфигурацией, переходите к следующему шагу, на котором будет использоваться только что прочитанный набор дескрипторов. В противном случае вам придется выполнить перечисление конфигураций (то есть перебрать значения переменной iconfig от 0 до bNumConfigurations-1) и на основании некоторого алгоритма выбрать нужную конфигурацию.
Выбор конфигурации
В определенный момент вы должны выбрать конфигурацию, отправив устрой-ству серию управляющих команд для назначения конфигурации и активизации нужных интерфейсов. URB для этой последовательности команд будут создаваться функцией USBD_CreateConfigurationRequestEx. В одном из ее аргументов передается массив указателей на дескрипторы включаемых интерфейсов. Таким образом, следующим шагом после назначения конфигурации должна стать подготовка этого массива.
Вспомните, что при чтении дескриптора конфигурации мы также читаем все его дескрипторы интерфейсов в смежную область памяти. Таким образом, эта память содержит серию дескрипторов: дескриптор конфигурации, дескриптор интерфейса, за которым следуют все его конечные точки, затем следующий дескриптор интерфейса со всеми его конечными точками, и т. д. Один из способов выбора интерфейса основан на переборе коллекции дескрипторов и сохранении адресов интересующих вас дескрипторов интерфейсов. Для упрощения этой задачи драйвер шипы предоставляет функцию с именем USBD_Parse-ConfigurationDescriptorEx:
PUSBJNTERFACEJDESCRIPTOR pid;
pid = USBD_ParseConfigurat1onDescriptorEx(pcd, StartPositlon,
InterfaceNumber, AlternateSettlng, Interfaceclass, InterfaceSubclass, InterfaceProtocol);
550
Глава 12. USB
ЧТЕНИЕ СТРОКОВОГО ДЕСКРИПТОРА-----------—--------------------------------------
Возможно, для построения отчета или для другой цели вам потребуется прочитать некоторые строковые дескрипторы, предоставляемые вашим устройством. Так, в примере USB42 устройство содержит англоязычные дескрипторы производителя, продукта и серийного номера, а также конфигурации и интерфейса, поддерживаемых устройством. Я написал следующую вспомогательную функцию для чтения строковых дескрипторов:
NTSTATUS GetStr1ngDescr1ptor(PDEVICE_0BJECT fdo. UCHAR istring, PUNICODE_STRING s)
{
NTSTATUS status;
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtensi on;
URB urb;
UCHAR data[256];
If (!pdx->lang1d)
{
UsbBul1dGetDescrlptorRequest(&urb, slzeof(JJRB_CONTROL_DESCRIPTOR_REQUEST). USB_STRING_DESCRIPTOR_TYPE.
0, 0. data, NULL, slzeof(data). NULL);
status = SendAwa1tUrb(fdo, &urb);
If (!NT_SUCCESS(status))
return status;
pdx->langid = *(LANGID*)(data + 2);
UsbBul1dGetDescrlptorRequest(&urb, s1zeof(-URB_C0NTR0L_DESCRIPT0R_REQUEST), USB-STRING-DESCRIPTOR JYPE, Istring, pdx->Iang1d, data. MULL, slzeof(data), NULL);
status = SendAwa1tUrb(fdo, &urb);
If (!NT_SUCCESS(status)) return status;
ULONG nchars = (data[O] - slzeof(WCHAR)) / slzeof(WCHAR);
If (nchars > 257)
nchars = 257;
PWSTR p = (PWSTR) ExAIlocatePool(PagedPool, (nchars + 1) * slzeof(WCHAR));
If (!p)
return STATUSJ NSUFFIСIENT-RESOURCES;
RtlCopyMemory(p, data + 2, nchars * slzeof(WCHAR)):
p[nchars] = 0;
s->Length = (USHORT) (slzeof(WCHAR) * nchars);
s->Max1mumLength = (USHORT) ((slzeof(WCHAR) * nchars)
+ Slzeof(WCHAR));
s->Buffer = p;
return STATUS_SUCCESS;
}
Новой и интересной частью этой функции (при условии, что вы читали книгу последовательно и уже достаточно много знаете о программировании режима ядра) является инициализация URB для выборки строкового дескриптора. Кроме индекса читаемой строки мы также передаем стандартный
Работа с драйвером шины
551
идентификатор языка LANGID. Такие же идентификаторы языков используются в приложениях Win32. Как я упоминал ранее, устройство может предоставлять строки на нескольких языках, и список поддерживаемых идентификаторов языков хранится в строковом дескрипторе 0. Чтобы запрос всегда относился к поддерживаемому языку, я читаю строку 0 при первом выполнении функции и произвольно выбираю первый попавшийся язык. В моих примерах драйверов его идентификатор всегда будет равен 0x0409, что соответствует американской версии английского языка. Родительский драйвер передает идентификатор языка вместе с индексом строки в параметрах запроса на чтение дескриптора, передаваемого устройству. Само устройство решает, какую строку следует вернуть. Функция GetStringDescriptor возвращает строку UNICODE_STRING, которая используется обычным образом. После завершения работы строковый буфер освобождается вызовом функции RtlFree-UnicodeString.
Я воспользовался функцией GetStringDescriptor в примере USB42 для выдачи дополнительной отладочной информации об устройстве. Например, в функции StartDevice выполняется код следующего вида:
UNICODE_STRING sd;
if (pcd->1Configuration
&& NTJSUCCESS(GetSt г i ngDescri ptor(fdo, pcd->iConfiguration, &sd))) {
KdPrint(("USB42 - Selecting configuration named %ws\n",
sd.Buffer));
RtlFreeUnicodeStringt&sd);
}
На самом деле я воспользовался макросом, чтобы один и тот же код не приходилось вводить несколько раз, но, полагаю, общий смысл понятен.
Параметр ped содержит адрес «объединенного» дескриптора конфигурации. StartPosition содержит либо адрес дескриптора конфигурации (при первом вызове этой функции), либо адрес дескриптора, с которого следует начать поиск. Остальные параметры задают критерии поиска дескрипторов. Значение -1 означает, что соответствующий критерий не задействован в поиске. Возможен поиск следующего дескриптора интерфейса, обладающего нулем и более следующих атрибутов: О заданный номер интерфейса (InterfaceNumber);
О заданный индекс Alternatesetting;
О заданный индекс Interfaceclass;
О заданный индекс InterfaceSubclass;
О заданный индекс Interfaceprotocol.
Дескриптор интерфейса, возвращенный функцией USBD_ParseConfiguration-DescriptorEx, сохраняется в поле InterfaceDescriptor элемента массива структур USBD_ INTERFACEJJST_ENTRY, после чего мы переходим к следующему дескриптору интерфейса для разбора. Массив со списком интерфейсов будет передаваться в одном из параметров при будущем вызове USBD„CreateConfigurationRequestEx, поэтому он заслуживает более подробного описания. Каждая запись в массиве является экземпляром следующей структуры:
typedef struct _USBD_INTERFACE_LIST__ENTRY { PUSB_INTERFACE_DESCRIPTOR InterfaceDescriptor; PUSBDJNTERFACEJNFORMATION Interface;
} USBDJNTERFACEJISTJNTRY, *PUSBDJNTERFACE_LIST ENTRY;
552
Глава 12. USB
При инициализации элемента массива поле InterfaceDescriptor задается равным адресу дескриптора интерфейса, который вы хотите включить, а в поле Interface заносится NULL. Для каждого интерфейса определяется один элемент, а затем добавляется еще один элемент с InterfaceDescriptor=NULL, он является признаком конца списка. Например, в моем примере USB42 заранее известно, что существует только один интерфейс, поэтому для создания списка интерфейсов используется следующий код:
PUSB_INTERFACE_DESCRIPTOR pid =
USBD_ParseConfigurationDescriptorEx(pcd, ped, -1, -1,
-1, -1, -1);
USBD_INTERFACE_LIST_ENTRY -interfaces[2] = {
{pid. NULL},
{NULL, NULL},
};
Я разбираю дескриптор конфигурации и обнаруживаю первый (и единственный) дескриптор интерфейса, после чего определяю массив из двух элементов для описания этого интерфейса.
Если потребуется включить более одного интерфейса, потому что вы обеспечиваете поддержку составного устройства, вызов повторяется в цикле. Пример:
ULONG size = (ped->bNumInterfaces + 1) *	// 1
s 1 zeof (USBD_INTERFACE J_ISTJENTRY);
PUSBD_INTERFACE_LIST_ENTRY Interfaces = (PUSBD_INTERFACE_LISTJENTRY) ExAllocatePool (NonPagedPool, size);
RtlZeroMemoryCinterfaces, size);
ULONG i = 0:
PUSB_INTERFACE_DESCRIPTOR pid = (PUSBJNTERFACEJDESCRIPTOR) ped;
while ((pid = USBD_ParseConfigurationDescriptorEx(pcd,	// 2
pid. ...)))
interfaces[i++].InterfaceDescriptor = pid++;	// 3
1.	Сначала мы выделяем память для количества элементов, соответствующего числу интерфейсов в конфигурации, плюс еще один элемент. Весь массив заполняется нулями. При выходе из процедуры завершения массива в предстоящем цикле следующий элемент будет равен NULL, то есть будет содержать признак конца массива.
2.	При вызове функции разбора указываются критерии, относящиеся к вашему устройству. При первой итерации цикла pid указывает на дескриптор конфигурации, а при последующих итерациях — на позицию за дескриптором интерфейса, полученным при предыдущем вызове.
3.	Здесь инициализируется указатель на дескриптор интерфейса. Постфиксный инкремент i заставляет следующую итерацию инициализировать следующий элемент массива, а постфиксный инкремент pid перемещает указатель за текущий дескриптор интерфейса, чтобы при следующей итерации обрабатывался
Работа с драйвером шины
553
следующий интерфейс (если USBD_ParseConfigurationDescriptorEx вызывается со вторым аргументом, указывающим на дескриптор интерфейса, удовлетворяющий вашим критериям, вы получите указатель на тот же самый дескриптор. Если не сместить указатель за пределы дескриптора при следующем вызове, то цикл будет повторяться вечно.)
На следующем шаге создается URB, который мы отправим (совсем скоро, честное слово) для настройки конфигурации устройства:
PURB selurb = USBD_CreateConf1gurat1onRequestEx(pcd, Interfaces);
Помимо создания блока URB (на который в данный момент указывает selurb) USBD_CreateConfigurationRequestEx также инициализирует поля Interface в элементах USBD_INTERFACE_LIST структурами USBDJNTERFACEJNFORMATION. Эти структуры физически находятся в одном блоке памяти с URB, а следовательно, будут возвращены в кучу при последующем вызове ExFreePooL Объявление структуры с информацией об интерфейсе выглядит так:
typedef struct -USBDJNTERFACEJNFORMATION {
USHORT Length;
UCHAR InterfaceNumber:
UCHAR AlternateSettlng;
UCHAR Class;
UCHAR SubClass;
UCHAR Protocol: UCHAR Reserved;
USBD JNTERFACEJANDLE InterfaceHandle;
ULONG NumberOfPIpes;
USBD_PIPE_INFORMATION Plpesfl];
} USBDJNTERFACEJNFORMATION, USBDJNTERFACEJNFORMATION;
В действительности сейчас нас интересует массив структур с информацией о каналах, потому что остальные поля структуры будут заполнены родительским драйвером при отправке URB. Каждая структура выглядит так:
typedef struct _USBD_PIPEJNFORMATION {
USHORT MaxImumPacketSIze;
UCHAR EndpolntAddress;
UCHAR Interval;
USBD_PIPEJYPE PIpeType;
USBD_PIPE_HANDLE PIpeHandle;
ULONG MaxImumTransferSIze;
ULONG PIpeFlags;
} USBD-PIPEJNFORMATION, *PUSBD_PIPEJNFORMATION;
Итак, имеется массив элементов USBDJNTERFACE_LIST, каждый из которых указывает на структуру USBDJNTERFACEJNFORMATION, а эта структура содержит массив структур USBDJIPEJNFORMATION. Наша непосредственная задача — заполнить поля MaximumTransferSize всех структур данных, если мы не хотим соглашаться со значением по умолчанию, выбранным родительским драйвером.
554
Глава 12. USB
Значение по умолчанию обозначается именем USBD_DEFAULT_MAXIMUM_TRANSFER_ SIZE, и на момент написания книги в DDK оно определялось равным PAGE_SIZE. Задаваемое значение не связано напрямую с максимальным размером пакета для конечной точки (который определяет, сколько байтов может передаваться за одну транзакцию шины) или объемом данных, поглощаемых конечной точкой за серию транзакций (он зависит от объема доступной памяти устройства). Вместо этого оно представляет наибольший объем данных, которые мы собираемся перемещать в одном блоке URB. Этот объем может быть меньше максимального объема данных, который устройство передает приложению или принимает от него, в этом случае наш драйвер должен быть готов разбить запросы приложения на фрагменты, не большие максимального. О том, как это делается, будет рассказано позднее в разделе «Управление каналами массовой передачи».
Причина, по которой мы должны задавать максимальный размер передачи, кроется в алгоритме планирования, который используется драйверами хостового контроллера для разбиения запросов URB на транзакции во фреймах шины. Если объем отправленных данных окажется слишком большим, они могут занять весь фрейм и вытеснить данные других устройств. Следовательно, нужно ограничить загрузку шины со стороны устройства, устанавливая разумный максимальный объем URB, передаваемых за один раз.
Код инициализации структур с информацией о каналах выглядит примерно так:
for (ULONG 11 =0; 11 < <number of 1nterfaces>: ++11)
PUSBDJNTERFACEJNFORMATION pl 1 = Interfaces^ 1 ].Interface: for (ULONG Ip = 0; Ip < pl1->NumberOfPipes; ++1p) p11->Pipes[1p].Max1mumTransferS1ze = <some constants }
ПРИМЕЧАНИЕ--------------------------------------------------------------------------
Функция USBD_CreateConfigurationRequestEx инициализирует поле MaxirnumTransferSize каждой структуры с информацией о канале значением USBDJOEFAULT MAXIMUM_TRANSFER_SIZE и обнуляет поле PipeFlags. Помните об этом, когда вы просматриваете образцы старых драйверов, а потом пишете собственный драйвер.
После инициализации структур с информацией о канале можно переходить к отправке конфигурационного URB:
SendAwaitUrb(fdo, seiurb);
Поиск манипуляторов
В результате успешного завершения URB выбора конфшурации вы получаете различные манипуляторы, которые следует сохранить для будущего использования:
О поле URB UrbSelectConfiguration.ConfigurationHandle содержит манипулятор конфигурации;
О поле InterfaceHandle каждой структуры USBDJNTERFACEJNFORMATION содержит манипулятор интерфейса;
Работа с драйвером шины
555
О каждая из структура USBD_PIPE_INFORMATION содержит поле PipeHandle с манипулятором канала, ведущего к соответствующей конечной точке.
Так, в примере USB42 сохраняются два манипулятора (в расширении устройства):
typedef struct _DEVICE_EXTENSION {
USBD_CONFIGURATIOOANDLE hconflg;
USBD_PIPE_HANDLE hpnpe:
} DEVICE-EXTENSION, *PDEVICE-EXTENSION;
pdx->hconf1 g = seiurb->UrbSelectConflguratlon.Conf1guratl onHandle;
pdx->hp1pe = 1nterfacesE0].Interface->Pipes[0].PipeHandle;
ExFreePool(seiurb):
В этой точке программы блок URB выбора конфигурации становится ненужным и его можно удалить.
Отключение устройства
При получении драйвером запроса IRP_MN_STOP__DEVICE следует перевести устройство в состояние без конфигурации, для этого нужно создать и отправить запрос на выбор конфигурации с указателем на конфигурацию, равным NULL:
URB urb;
UsbBul1dSelectConflguratlonRequest(&urb, slzeof(_URB_SELECT_CONFIGURATION). NULL);
SendAwa1tUrb(fdo, &urb);
Управление каналами массовой передачи
В прилагаемых материалах представлены два примера, демонстрирующие массовые передачи. Первый, более простой, называется USB42. В нем используется входная массовая конечная точка, которая при каждом чтении возвращает константу 42. Код чтения выглядит так:
URB urb;
UsbBulIdlnterruptOrBulkTransferRequestf&urb,
sizeof(JJRB_BULK_OR_INTERRUPT-TRANSFER).
pdx->hpipe, Irp->Assoc1atedIrp.SystemBuffer, NULL, cbout, USBDJRANSFER-DIRECTIONJN USBD__SHORT_TRANSFER_OK, NULL);
status = SendAwa1tUrb(fdo. &urb):
Этот код выполняется в контексте обработчика вызова DeviceloControl, использующего буферизованный метод обращения к данным, поэтому поле SystemBuffer в IRP содержит указатель на область памяти, в которую должны быть доставлены данные. Переменная cbout определяет размер заполняемого буфера данных.
Сам запрос не требует подробных объяснений. Флаг указывает, какая операция выполняется с конечной точкой - чтение (USBD_TRANSFER_DIRECTION_IN) или запись (USBD_TRANSFER_DIRECTION„OUT). Также при помощи другого флагового
556
Глава 12. USB
бита (USBD_SHORT_TRANSFER_OK) можно указать, разрешено ли устройству предоставлять данные в объеме меньше максимума, установленного для конечной точки. Манипулятор канала сохраняется во время обработки IRPJ4N_START_DEVICE так, как было показано ранее.
Архитектура примера LOOPBACK
Пример LOOPBACK заметно сложнее USB42. Обслуживаемое им устройство имеет две конечные точки массовой передачи: одна для ввода, а другая для вывода. Вы можете подать до 4096 байт в выходной канал, а затем прочитать записанные данные из входного канала. Драйвер использует для перемещения данных стандартные запросы IRP_MJ_READ и IRP_MJ_WRITE.
LOOPBACK позволяет приложению читать или записывать за одну операцию данные в объеме, большем MaximumTransferSize. Этот факт накладывает ограничения на работу драйвера:
О обработка каждого IRP чтения или записи может состоять из нескольких сегментов. В соответствии со спецификацией USB (а именно разделом 5.8.3) объем данных в каждом сегменте, кроме последнего, должен быть кратен максимальному размеру пакета конечной точки;
О очевидно, сегменты разных запросов чтения/записи не должны смешиваться на уровне устройства. По этой причине в примере LOOPBACK доступ к конечной точке организуется с использованием очередей запросов чтения и записи;
О чтобы избежать хлопот с отменой IRP, LOOPBACK передает свои URB чтения и записи в том же пакете IRP_MJ_READ или IRP_MJ_WRITE, который был получен сверху.
Функция Startlo в примере LOOPBACK
В примере LOOPBACK интерес представляют функция Startlo, обрабатывающая отдельный запрос чтения или записи, и функция завершения ввода/вывода для запросов URB, отправляемых драйверу шины. Оба вида IRP (чтения и записи) обрабатываются одной функцией Startlo. Главная причина, по которой в этом драйвере можно обойтись одной функцией Startlo, заключается в том, что мы будем выполнять либо чтение, либо запись, но не обе операции одновременно. Использовать единую функцию удобно, потому что обработка чтения и записи почти не различается:
VOID StartIo(PDEVICE_OBJECT fdo, PIRP Irp)
{
PDEVICE_EXTENSION pdx =
(PDEVICE—EXTENSION) fdo->DeviceExtension;
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
BOOLEAN read = stack->MajorFunctionCode === IRP_MJ_READ;
USBD_PIPE_HANDLE hpipe = read ? pdx->hinpipe : pdx->houtpipe;
ULONG urbflags = USBD_SHORT_TRANSFER_OK
(read ? USBD_TRANSFER_DIRECTION_IN
: USBD_TRANSFER_DIRECTION_OUT):
Работа с драйвером шины
557
LOOPBACK устанавливает флаг DO_DIRECT_IO в объекте устройства. Соответственно, буфер данных описывается списком MDL, адрес которого хранится в Irp->MdlAddress. Длину запрашиваемой передачи можно определить двумя способами. Во-первых, можно прочитать stack->Parameters.Read.Length или stack-> Parameters.Write.Length (кстати говоря, Read и Write — идентичные субструктуры IO_STACK_LOCATION). Во-вторых, можно положиться на MDL:
ULONG length = Irp->MdlAddress ?
MmGetMdlByteCount(Irp->MdlAddress) : 0;
Лично я терпеть не могу, когда одна задача решается двумя способами, — меня беспокоит, что один из них перестанет работать в будущей версии операционной системы. Я взял за правило выбирать более распространенную альтернативу, полагая, что она с большей вероятностью сохранится со временем. Большинство попадавшихся мне драйверов, использовавших MDL, брали длину буфера из MDL, поэтому я сделаю то же.
Следующим логическим шагом в Startlo должно стать вычисление длины первого сегмента передачи, которая, теоретически, может состоять из нескольких сегментов:
ULONG seglen = length;
if (seglen > pdx->maxtransfer)
seglen = pdx->maxtransfer;
(Функция StartDevice в примере LOOPBACK задает значение maxtransfer равным MaximumTransferSize для входных и выходных каналов. В этом драйвере я сделал его равным 1024, чтобы реализовать логику многосегментной передачи. В микрокоде устройства для одной логической передачи установлен лимит 4096 байт.)
Наш вызов UsbBuildlnterruptOrBulkTransferRequest будет немного более сложным, чем в примере USB42, потому что мы используем DO_DIRECT_IO, а пересылка может осуществляться за несколько стадий. В процессе подготовки к этому вызову LOOPBACK создает частичный список MDL, описывающий часть всего буфера:
ULONG_PTR va =
(ULONG_PTR) MmGetMdlVIrtualAddress(Irp->MdlAddress);
PMDL mdl = loAllocateMdl((PVOID) (PAGE_SIZE - 1), seglen, FALSE, FALSE, NULL);
loBuiIdPartlalMdl(Irp->MdlAddress, mdl, (PVOID) va, seglen);
В этот момент кодирования Startlo необходимо принять принципиальное решение. Как создать и отправить один или несколько URB, необходимых для выполнения этой операции? Один способ (на мой взгляд, не лучший) заключается в создании серии запросов IRP_MJ_INTERNAL_DEVICE_CONTROL, каждый из которых обладает собственным URB, и отправить их вниз по стеку РпР драйверу шины USB. Мне этот вариант не нравится тем, что он требует множества лишних служебных операций при завершении и отмене главного IRP. Я реализовал его в примере USBISO (см. далее в этой главе), но в этом случае у него не было никаких разумных альтернатив из-за требований к согласованию по времени.
558
Глава 12. USB
Простейший способ организации массовой передачи заключается в использовании «основного» IRP (то есть переданного Startlo) в качестве «конверта» для упаковки URB последующих сегментов. Для этого необходимо лишь инициализировать следующий элемент стека вручную вместо вызова loCopyCurrentlrpStack-LocationToNext. Наша функция Startlo затем устанавливает функцию завершения и отправляет основной 1RP вниз драйверу шины. Затем функция завершения «перерабатывает» IRP и URB для выполнения следующего сегмента передачи. При завершении последнего сегмента функция завершения освобождает память, занимаемую IRP, и заносит в поле loStatus.Information количество фактически переданных байтов, как того требует спецификация IRP_MJ_READ и IRP_MJ_WRITE.
Тем не менее, информация, необходимая для функции завершения, не ограничивается адресом URB. В LOOPBACK определяется следующая контекстная структура:
struct „RWCONTEXT :	: public „URB
ULONG_PTR va:	// Виртуальный адрес следующего
	// сегмента передачи
ULONG length;	// Длина оставшихся данных
PMDL mdl:	// Частичный список MDL
ULONG numxfer;	// Накопительный счетчик переданных байтов
};
typedef struct _RWCONTEXT RWCONTEXT, *PRWCONTEXT;
(Я пишу свои драйверы на C++, что позволяет мне объявить одну структуру производной от другой.) Инициализация структуры контекста выполняется следующим образом:
PRWCONTEXT ctx = (PRWCONTEXT) ExAllocatePool(NonPagedPool, sizeof(RWCONTEXT)):
UsbBulIdlnterruptOrBulkTransferRequest(ctx,
si zeof(_URB_BULK_OR_INTERRUPT_TRANSFER),
hpipe, NULL, mdl, seglen, urbflags, NULL);
ctx->va = va + seglen:
ctx->length = length - seglen:
ctx->mdl = mdl:
ctx->numxfer = 0;
Обратите внимание: преобразование типа для ctx не обязательно, потому что эта структура является производной от структуры URB. Указатель MDL содержит указатель на частичный MDL, созданный ранее, а в поле длины хранится длина выбранного сегмента.
После проведения инициализации IRP отправляется вниз по стеку драйверу шины:
stack = loGetNextlrpStackLocation(Irp);
stack->MajorFunction - IRP_MJ_INTERNAL_DEVICE__CONTROL;
Работа с драйвером шины
559
stack^Parameters.Others.Argument1 - (PVOID) (PURB) ctx:
stack->Parameters.DeviceloControl.loControlCcde =
IOCTL_INTERNAL_USB_SUBMITJJRB;
loSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE)
OnReadWriteComplete, (PVOID) ctx, TRUE, TRUE, TRUE);
IoCallDriver(pdx->LowerDeviceObject, Irp);
Полезно знать, что драйвер шины USB принимает URB чтения/записи на уровне DISPATCH_LEVEL Нас это устраивает, поскольку Startlo также будет выполняться на уровне DISPATCH-LEVEL.
Функция завершения чтения/записи в примере LOOPBACK
Далее приводится содержательная часть функции завершения:
NTSTATUS 0nReadWr1teComplete(PDEVICE_OBJECT fdo,
PIRP Irp, PRWCONTEXT ctx)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->DeviceExtension;
BOOLEAN read = (ctx->UrbBulkOrlnterruptTransfer.TransferFlags &
USBD_TRANSFER_DIRECTION_IN) HO;
ctx->numxfer +=	// 1
ctx->UrbBulkOrInterruptTransfer.TransferBufferLength;
NTSTATUS status = Irp->IoStatus.Status:
if (NT-SUCCESS(status) && ctx->length && !Irp->Cancel)	// 2
{
ULONG seglen = ctx->length;	// 3
if (seglen > pdx->maxtransfer)
seglen = pdx->maxtransfer;
PMDL mdl = ctx->mdl;
MmPrepareMdlForReusetmdl);	// 4
loBuildPartialMdl(Irp->MdlAddress, mdl,
(PVOID) ctx->va, seglen);
ctx->UrbBulkOr!nterruptTransfer.TransferBufferLength =	//5
seglen;
PIO_STACK_LOCATION stack =* loGetNextlrpStackLocation(Irp);	// 6
stack->MajorFunction = IRP_MJ_INTERNAL_DEVICE_CONTROLj stack->Parameters.Others.Argumentl = (PVOID) (PURB) ctx;
stack->Parameters.DeviceloControl.I©Control Code =
IOCTL_INTERNAL_USB_SUBMIT_URB;
loSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE) OnReadWriteComplete, (PVOID) ctx, TRUE, TRUE, TRUE);
ctx->va += seglen;	//7
ctx->length -= seglen;
560
Глава 12. USB
loCal1Driver(pdx->LowerDeviceObject, Irp);	// g
return STATUS_MORE_PROCESSING_REQUIRED }
If (NT_SUCCESS(status))
Irp->IoStatus.Information = ctx->numxfer;	// 9
else <восстдновление после ошибки> loFreeMdl(ctx->mdl);	//10
ExFreePool(ctx); StartNextPacket(&pdx->dqReadWrite, fdo); IoReleaseRemoveLock(&pdx->RemoveLock. Irp);
return STATUS_SUCCESS; }
1.	В поле loStatus.Information должно быть сохранено общее количество переданных байтов. Эта команда суммирует промежуточные размеры.
2.	Здесь мы проверяем, нужно ли выполнять следующую стадию. Завершилась ли предыдущая стадия передачи нормально? Не равна ли нулю длина остатка? Не было ли попыток отмены предыдущей стадии?
3.	Каждая стадия, как и первая, ограничивается максимальным объемом пересылаемых данных для канала. Более того, размер каждого сегмента, кроме последнего, должен быть кратным размеру пакета конечной точки. Ранее я об этом не упоминал, но максимальный размер передачи выбирается с учетом кратности (как сделано здесь).
4.	Частичный список MDL будет использован заново для следующего сегмента. Вызов MmPrepareMdIForReuse сбрасывает флаговые биты и указатели, loBuild-PartialMdl инициализирует поля структуры MDL для описания данных, читаемых из главного буфера или записываемых в него на следующей стадии. Обратите внимание: виртуальный адрес (поле va структуры контекста) используется не как адрес, а как индекс в буфере, описываемом основным списком MDL.
5.	URB не изменился — отличается только длина.
6.	I/O Manager обнулил большую часть следующего элемента стека, поэтому мы не можем использовать его содержимое. Следовательно, следующий элемент стека необходимо инициализировать заново.
7.	Здесь обновляются виртуальный адрес и остаточная длина для следующего вызова этой функции завершения.
8.	Мы вызываем loCallDriver для переработки (повторного использования) IRP. Наши дальнейшие действия не зависят от состояния, возвращаемого драйвером шины.
9.	Одно из правил обработки IRP_MJ_READ и IRP_MJ_WRITE гласит, что при успешном завершении в поле loStatus.Information должно храниться количество фактически переданных байтов. Мы отслеживаем эту величину в numxfer, а здесь обеспечиваем выполнение этого правила.
Работа с драйвером шины
561
10.	Оставшаяся часть функции завершения сводится к прямолинейной зачистке после вызова Startlo.
Чтобы проверить, насколько внимательно вы следили за всем, что написано в этой книге, предложу три довольно глупых вопроса о функции завершения: 1. Почему функция завершения не вызывает loReuselrp перед переработкой IRP? 2. Почему функция завершения всегда возвращает STATUS_MORE_PROCESSING_ REQUIRED после отправки переработанного IRP вниз по стеку?
3. Какой вывод можно сделать из того факта, что автор перечитал абсолютно весь текст книги (притом несколько раз), вернул STATUS-SUCCESS из функции завершения, но при этом опустил шаблонный вызов loMarklrpPending?
ОТВЕТЫ НА ГЛУПЫЕ ВОПРОСЫ---------------------------------------------------------
1.	Вызов loReuselrp полностью переинициализирует IRP и подходит для тех случаев, когда создатель IRP хочет использовать его заново. Мы хотим переинициализировать только следующий элемент стека. Единственное, что действительно необходимо сбросить в IRP, — это флаг Cancel. Если флаг установлен, это означает, что кто-то вызвал loCancellrp для основного IRP. В этом случае мы не пытаемся переходить к следующей стадии.
2.	Если драйвер шины приостановил IRP текущей стадии, совершенно очевидно, что в такой ситуации следует вернуть STATUS_MORE_PROCESSING_REQUIRED. Позднее от драйвера шины последует другой вызов loCompleteRequest, и система снова вызовет функцию завершения. Если драйвер шины завершил стадию пересылки синхронно, эта функция завершения уже была вызвана рекурсивно. В любом случае, мы не хотим, чтобы вызов loCompleteRequest продолжил обработку этого IRP.
3.	Можно сделать вывод, что автор — лицемерный тип. Или что диспетчерская функция пометила IRP как незавершенный и вернула STATUS-PENDING как часть нормального протокола постановки IRP в очередь. Или и то и другое — эти варианты не являются взаимоисключающими.
Восстановление после ошибок в примере LOOPBACK
При отправке и приеме данных конечными точками массовой передачи шина и драйвер шины обеспечивают повторную передачу данных в случае возникновения ошибок. Следовательно, если ваш URB вроде бы завершился успешно, можно быть уверенным в том, что данные, которые вы намеревались переслать, действительно были переданы успешно. Тем не менее, при возникновении ошибок ваш драйвер должен попытаться произвести восстановление. Существует четко определенный протокол восстановления в случае ошибок, он продемонстрирован в дополнительном коде LOOPBACK (функция RecoverFromError), который ранее еще не приводился.
Начните с выдачи запроса IOCTL_INTERNAL_USB_GET_PORT_STATUS для проверки состояния порта концентратора, к которому подключено ваше устройство.
Если флаги состояния показывают, что порт неактивен, но продолжает оставаться подключенным (то есть мы не имеем дела с ситуацией непредвиденного отключения), выполните с конечной точкой операцию URB_FUNCTION_ABORT_PIPE, чтобы очистить все незавершенные операции ввода/вывода, а затем проведите сброс порта командой IOCTL_INTERNAL_USB_RESET_PORT.
В любом случае выдайте команду URB_FUNCTION„RESET_PIPE для сброса конечной точки. Среди прочего, эта команда сбрасывает состояние приостановки конечной точки.
562
Глава 12. USB
Повторите или отклоните проблемный запрос в зависимости от семантики устройства.
Неприятно то, что многие из этих действий должны выполняться на уровне PASSIVE_LEVEL, однако необходимость в них выявляется в функции завершения, которая выполняется (возможно) на уровне DISPATCH_LEVEL. Проблема решается планированием рабочего элемента так, как показано в следующем фрагменте (механика рабочих элементов рассматривается в главе 14):
struct -RWCONTEXT ; public JJRB {
PI0_WORKITEM rcltem; // Рабочий элемент, созданный для восстановления
PIRP Irp;	// Основной IRP
};
NTSTATUS OnReadWr1teComplete(...)
{
If (NT_SL)CCESS(status))
Irp->IoStatus.Information = ctx->numxfer;
else If (status != STATUSJANCELLED)
ctx->rc1tem = IoAllocateWork Item(fdo);
ctx~>Irp = Irp;
IoQueueWorkItem(ctx->rcitern.
(PIO_WORKITEM_ROUTINE) RecoverFromError, CrlticalWorkQueue. (PVOID) ctx);
return STATUS_MORE_PROCESSING_REQUIRED;
}
}
Сам код восстановления после ошибки выглядит так:
VOID RecoverFromError(PDEVICE_OBJECT fdo. PRWCONTEXT ctx)
{
PDEVICE-EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
BOOLEAN read = (ctx->UrbBulkOrInterruptTransfer.TransferFlags
& USBDJRANSFER-DIRECTIONJN) '= 0;
ULONG portstatus = GetStatus(fdo);
USBD_PIPE_HANDLE hplpe = read ? pdx->h1np1pe : pdx->houtpipe;
If (! (portstatus & USBOJWJNABLED) &&
(portstatus & USBD_PORT_CONNECTED))
AbortP1pe(fdo, hplpe);
ResetDevlce(fdo);
}
ResetP1pe(fdo. hplpe);
IoFreeWorkItem(ctx->rc1tem);
PIRP Irp = ctx->Irp;
IoFreeMdl(ctx->mdl);
Работа с драйвером шины
563
ExFreePool(ctx);
StartNextPacket(&pdx->dqResdWr1 te, fdo);
loReleaseRemoveLock(&pdx-MtemoveLock, Irp); loCompleteRequest!Irp. IO_NO_INCREMENT);
Вспомогательные функции AbortPipe, ResetDevice и ResetPipe выдают внутренние управляющие операции и URB, которые я описал ранее. Обратите внимание: когда RecoverFromError вызывает loCompleteRequest, наша собственная функция завершения не вызывается. Таким образом, всю зачистку, которая обычно выполняется функцией завершения, придется повторить заново.
Пожалуй, способ вызова RecoverFromError может показаться излишне хитроумным. Если функция завершения выполняется иа уровне PASSIVE_LEVEL, может показаться, что без постановки рабочего элемента в очередь можно обойтись. Тем не менее, такое решение будет ошибочным, если только вы не воспользуетесь функцией loSetCompletionRoutineEx для установки функции завершения. Потенциальная проблема заключается в том, что сторона, отправившая IRP, может снять свою защиту против выгрузки вашего кода сразу же после того, как RecoverFromError вызовет loCompleteRequest. В результате выполнение некоторых команд RecoverFromError и OnReadWriteComplete остается на время, когда драйвер уже выгружен из памяти. Использование loSetCompletionRoutineEx предотвращает выгрузку драйвера до возврата из функции завершения. Тем не менее, постоянный вызов loSetCompletionRoutineEx требует гораздо больших затрат, чем постановка в очередь рабочего элемента в маловероятном случае возникновения ошибки ввода/вывода, поэтому я выбрал решение с рабочим элементом.
В микрокоде LOOPBACK присутствует вполне реальная проблема, которую я даже не пытался решать в драйвере. Если при операции чтения или записи произойдет сбой, возможна дссинхроиизация операции записи и обратного чтения. Чтобы увидеть, как это происходит, включите режим имитации нехватки ресурсов в Driver Verifier для программы LOOPBACK — это приведет к возникновению случайных отказов при операциях выделения памяти в пуле. Последующие вызовы тестовой программы также будут обычно завершаться сбоем, потому что микрокод устройства будет возвращать неверные данные из своего кольцевого буфера.
Для решения подобных проблем одна из сторон (либо драйвер, либо приложение) должна знать, как работает устройство, и выдавать команды для восстановления синхронизации микрокода. Однако пример LOOPBACK и без того достаточно сложен, а решение слишком сильно привязано к конкретному устройству и его микрокоду, поэтому я решил обойтись без него.
Управление прерывающими каналами
Со стороны устройства прерывающий канал практически идентичен каналу массовой передачи. Единственное важное различие заключается в том, что хост опрашивает прерывающую конечную точку с гарантированной частотой. Устройство отвечает сигналом NAK, за исключением тех моментов, когда оно готово выдать прерывание хосту. Чтобы сообщить о событии прерывания, устройство
564
Глава 12. USB
посылает хосту пакет данных, сопровождающих прерывание, а затем передает сигнал АСК.
С точки зрения драйвера управлять прерывающим каналом чуть сложнее, чем массовым. Когда драйверу потребуется прочитать или записать данные в массовый канал, он просто создает соответствующий URB и отправляет его драйверу шины. Но чтобы прерывающий канал выполнял свою задачу по оповещению хоста об интересующих его аппаратных событиях, драйвер должен постоянно поддерживать активный запрос на чтение. Для этой цели можно воспользоваться идеей, продемонстрированной в примере LOOPBACK, где функция завершения продолжала многократно использовать URB.
Пример USBINT показывает, как управлять прерывающим каналом при помощи постоянно активного URB. Вместо того чтобы подробно описывать пример шаг за шагом, я выделю несколько ключевых положений:
О при остановке или отключении устройства не должно быть активных операций чтения. По этой причине USBINT дополнительно следит за тем, чтобы чтение прерываний подавлялось при отключении и перезапускалось при восстановлении питания. Так как эти действия должны выполняться асинхронно, чтобы не нарушать правила о блокировке во время переходов питания, драйвер использует функции обратного вызова SaveDeviceContext и RestoreDeviceContext из библиотеки GENERIC.SYS;
О функция завершения для чтения прерываний, фактически, представляет собой обработчик прерывания для драйвера. Можно ожидать, что она будет выполняться на уровне DISPATCH_LEVEL, поскольку она является функцией завершения ввода/вывода. Среди прочего, эта функция должна повторно инициализировать и выдать запрос на чтение прерывания, чтобы он всегда оставался активным;
О как обычно, может возникнуть ситуация «гонки» между драйвером, отменяющим чтение прерываний в StopDevice или при отключении питания, и драйвером шины, завершающим соответствующий IRP. Вероятно, к этому моменту читатель уже достаточно хорошо представляет, как предотвращаются подобные «гонки».
Управляющие запросы
Если еще раз обратиться к табл. 12.3, вы заметите, что в ней представлены И стандартных типов управляющих запросов. Нам с вами никогда не придется явно выдавать запросы SET_ADDRESS. Драйвер шины делает это при начальной активизации нового устройства, к тому моменту, когда драйвер WDM получает управление, драйвер шины уже назначил адрес устройству, прочитал дескриптор устройства и узнал из него, что устройство обслуживается нашим драйвером. Ранее в подразделах «Инициирование запросов» и «Конфигурация» мы уже говорили о том, как создаются URB, заставляющие драйвер шины отправлять управляющие запросы на получение дескриптора, назначение конфигурации или интерфейса. В этом разделе будут заполнены некоторые пробелы, относящиеся к другим видам управляющих транзакций.
Работа с драйвером шины
565
Управление возможностями
Чтобы включить или сбросить некоторую возможность (feature) устройства, интерфейса или конечной точки, мы отправляем соответствующий URB. Например, следующий код (позаимствованный из примера FEATURE в прилагаемых материалах) включает некую возможность интерфейса, определяемую производителем:
URB urb:
UsbBul1dFeatureRequest(&urb,
URB_FUNCTION_SET_FEATUREJO_INTERFACE,
FEATURE_LED_DISPLAY, 1, NULL);
status = SendAwa1tUrb(fdo, &urb);
Второй аргумент UsbBuildFeatureRequest указывает, какая операция (установка или сброс) выполняется с возможностью, относящейся к устройству, интерфейсу, конечной точке или другой сущности, определяемой производителем устройства. У параметра имеются восемь допустимых значений — полагаю, после следующей формулы мне не придется дополнительно пояснять, как они строятся:
URB_FUNCTION_ [SET CLEAR] _FEATURE_TO_ [DEVICE INTERFACE ENDPOINT OTHER]
Третий аргумент UsbBuildFeatureRequest определяет возможность, с которой выполняется операция. В примере FEATURE я придумал вымышленную возможность с именем FEATUREJ_ED_DISPLAY. Четвертый аргумент определяет конкретную сущность указанного типа. В данном примере запрос обращен к интерфейсу 1, поэтому я использовал значение 1.
О ПРИМЕРЕ FEATURE----------------------------------------------------------------
Включение и сброс возможностей продемонстрированы в примере FEATURE в прилагаемых материалах. Микрокод устройства (подкаталог EZUSB) определяет устройство без конечных точек. Устройство поддерживает возможность уровня интерфейса с номером 42, которая в драйвере обозначается символическим именем FEATURE J_ED_DISPLAY. При установке возможности на макетной плате Cypress Semiconductor включается семисегментный светодиодный индикатор, который показывает, сколько раз эта возможность устанавливалась с момента подключения устройства (по модулю 10). При сбросе возможности на индикаторе отображается только десятичная точка. Драйвер устройства FEATURE (подкаталог SYS) содержит код, управляющий установкой и сбросом возможности, а также реализующий ряд других управляющих команд в ответ на запросы IOCTL. За примерами обращайтесь к файлу CONTROL.CPP; по сложности этот код не превышает фрагменты, приводимые в тексте главы.
Тестовая программа (подкаталог TEST) представляет собой консольное приложение Win32, которое вызывает DeviceloControl для установки пользовательской возможности; при помощи дополнительных вызовов DeviceloControl получает маски состояния, номер конфигурации и альтернативный индекс единственного интерфейса, ожидает 5 секунд, а затем сбрасывает возможность другим вызовом DeviceloControl. При каждом запуске теста на макетной плате на 5 секунд включается индикатор, на котором отображаются последовательно возрастающие числа.
В спецификации USB определяются две стандартные возможности, которыми вроде бы было удобно управлять при помощи URB возможностей: возможности удаленного пробуждения и возможности приостановки конечных точек. Тем не менее, вам не придется устанавливать или сбрасывать эти возможности самостоятельно, потому что драйвер шины делает это автоматически. При выдаче запроса IRP_MN_WAIT_WAKE (см. главу 8) драйвер шины убеждается в том, что
566
Глава 12. USB
конфигурация устройства поддерживает удаленное пробуждение, и автоматически включает его для устройства. Драйвер шины выдает запрос на отмену приостановки устройства при выдаче URB RESET_PIPE.
Проверка состояния
Чтобы получить информацию о текущем состоянии устройства, интерфейса или конечной точки, создайте блок URB для получения информации состояния. Пример:
URB urb;
USHORT epstatus;
UsbBul1dGetStatusRequest(&urb,
URB_FUNCTIONJETJTATUS_FROM_ENDPOINT,
<индекс>, &epstatus, NULL, NULL);
SendAwa1tUrb(fdo, &urb);
В запросе на получение состояния используются четыре разные функции URB, которые позволяют получить текущую маску состояния для устройства в целом, для заданного интерфейса, для заданной конечной точки или для сущности, определяемой производителем (табл. 12.8).
Маска состояния устройства указывает, имеет ли устройство автономное питание и включена ли для него возможность удаленного пробуждения (рис. 12.15). Маска конечной точки указывает, приостановлена ли конечная точка в данный момент (рис. 12.16). В спецификации USB «Interface Power Management» ранее были определены биты состояния уровня интерфейса, относящиеся к управлению питанием, но на момент публикации книги эта спецификация была отменена. Биты состояния, определяемые производителем, в спецификации USB задаваться не могут... поскольку они по определению задаются фирмой-производителем.
Таблица 12.8. Коды функций URB, используемых для получения информации состояния
Код операции	Уровень информации
URB_FUNCTION_GET_STATUS„FROM_DEVICE	Устройство в целом
URB_FUNCnON_GET_STATUS_FROM_INTERFACE	Заданный интерфейс
URB_FUNCTION_GET_STATUS_FROM„ENDPOINT	Заданная конечная точка
URB__FUNCTION_GET_STATUS_FROM_OTHER	Объект, определяемый производителем
14 бит 4			1 бит	1 бит 	>4	ь
	Удаленное пробуждение	Автономное питание
_____1 -Автономное питание
0=Питание от шины
1=Включено
0=Отключено
Рис. 12.15. Биты состояния устройства
Работа с драйвером шины
567
15 бит
1 бит
	Приостановка
1 =Конечная точка приостановлена 0=Конечная точка работает нормально
Рис-12.16. Биты состояния конечной точки
Управление изохронными каналами
Изохронные каналы предназначены для организации обмена данными, критичными по времени, между хостом и устройством с гарантированной периодичностью. Драйвер шины резервирует до 80 % пропускной способности шины для изохронных и прерывающих передач. На практике это означает, что каждый 125-микросекундный микрофрейм включает зарезервированные слоты, размер которых достаточен для передач максимального размера для всех активных в данный момент изохронных и прерывающих конечных точек. На рис. 12.17 эта концепция показана для трех разных устройств. Каждое из устройств А и В имеет по изохронной конечной точке, для которых в каждом мшсрофрейме резервируется фиксированный (и достаточно большой) временной промежуток. Устройство С содержит прерывающую конечную точку с частотой опроса в два микрофрейма; для нее резервируется небольшая часть каждого второго микрофрейма. В микрофреймах, не включающих опрос прерывающей конечной точки устройства С, возможна пересылка дополнительных данных (скажем, для массовых передач и для других целей).
Время
Микрофрейм п	Микрофрейм л+1	Микрофрейм п+2
	А	В	.С	
	А	В	
	А	в	
Изохронная конечная точка
Прерывающая конечная точка
Рис. 12.17. Резервирование пропускной способности для изохронных и прерывающих конечных точек
Резервирование пропускной способности
Драйвер шины резервирует пропускную способность при включении интерфейса, для этого он анализирует дескрипторы конечных точек, являющихся частью
568
Глава 12. USB
интерфейса. Тем не менее, пропускная способность резервируется независимо от ее фактического использования. По этой причине важно включать интерфейс с изохронной конечной точкой только в том случае, если вы будете использовать зарезервированную полосу, кроме того, заявленный максимальный размер передачи для конечной точки должен приблизительно соответствовать тому объему, который вы намереваетесь использовать. Обычно устройства с изохронной поддержкой обладают интерфейсом по умолчанию, который не содержит ни изохронных, ни прерывающих конечных точек. Когда возникает необходимость в использовании этой возможности, вы включаете альтернативную настройку того же интерфейса, которая содержит изохронные или прерывающие конечные точки.
Следующий пример объясняет механику резервирования пропускной способности. В примере USB IS О в прилагаемых материалах определяется интерфейс с двумя настройками, стандартной и альтернативной. Стандартная настройка не содержит конечных точек. Альтернативная настройка содержит изохронную конечную точку с максимальным размером передачи 256 байт (рис. 12.18).
Изохронная конечная точка
Рис. 12.18. Структура дескриптора для устройства USBISO
Во время выполнения StartDevice выбирается конфигурация, основанная на интерфейсе по умолчанию. Так как интерфейс по умолчанию не содержит ни изохронных, ни прерывающих конечных точек, пропускная способность изначально не резервируется. Однако при открытии манипулятора устройства мы вызываем вспомогательную функцию SelectAlternatelnterface для переключения на альтернативную настройку того же интерфейса (я снова опустил необходимую проверку ошибок):
NTSTATUS SelectAlternateInterface(PDEVICE_OBJECT fdo)
{
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fdo->Dev1ceExtension;
PUSB_INTERFACE_DESCRIPTOR pid =	// 1
USBD_ParseConf1guratlonDescrlptorEx(pdx->pcd, pdx->pcd.
0, 1, -1, -1, -1);
Работа с драйвером шины
569
ULONG npipes = p1d->bNumEndpoints;
ULONG size = GET_SELECTJNTERFACE_REQUEST_SIZE(npipes);	// 2
PURB urb = (PURB) ExAllocatePool(NonPagedPool, size);
RtIZeroMemory(urb, size);
UsbBulIdSelectlnterfaceRequest(urb, size, pdx->hconf1g, 0, 1);	// 3
urb->UrbSelectInterface.Interface.Length =	//4
GET_USBD_INTERFACE_SIZE(np1pes);
urb->UrbSelectInterface.Interface.PIpes[0].MaximumT ransferSI ze =
PAGE-SIZE;
NTSTATUS status = SendAwa1tUrb(fdo,	&urb);	// 5
If (NT_SUCCESS(status))	// 6
{
pdx->h1np1pe =	Hl
urb.UrbSelectInterface.Interface.PIpes[0].PIpeHand]e;
status = STATUS_SUCCESS;
}
ExFreePool(urb);
return status;
}
1.	Прежде чем выделять память для блока URB, необходимо знать, сколько дескрипторов каналов он будет содержать. Самый распространенный способ получения этого числа — обращение к объединенному дескриптору конфигурации и поиск дескриптора интерфейса 0, альтернативная настройка 1. В этом дескрипторе хранится количество конечных точек, совпадающее с количеством открываемых каналов.
2.	Вызов GET_SELECTJNTERFACE_REQUEST_SIZE вычисляет размер в байтах запроса на выбор интерфейса с заданным количеством каналов. После этого можно выделить память для URB и инициализировать ее нулями. Кстати говоря, реальный код в прилагаемых материалах проверяет, что вызов ExAllocatePool завершился успешно.
3.	Здесь строится URB для выбора альтернативной настройки 1 (последний аргумент) интерфейса с номером 0 (предпоследний аргумент).
4.	Эти два дополнительных этапа инициализации необходимы для завершения настройки URB. Если не задана длина структуры с информацией об интерфейсе, вы немедленно получите ошибку STATUS_BUFFER_TOO_SMALL. Если не заполнены поля MaximumTransferSize дескрипторов каналов, при попытке чтения или записи в канал происходит ошибка STATUS_INVALID_PARAMETER.
5.	При отправке URB родительский драйвер автоматически закрывает текущую настройку интерфейса, включая все ее конечные точки. Затем родительский драйвер приказывает устройству включить альтернативную настройку и создает дескрипторы каналов для конечных точек, входящих в нее. Если открытие нового интерфейса по какой-то причине завершается неудачей, родительский драйвер заново открывает предыдущий интерфейс, а все предыдущие манипуляторы интерфейсов и каналов остаются действительными.
570
Глава 12. USB
6.	Вспомогательная функция SendAwaitUrb просто возвращает код ошибки, если ей не удается выбрать единственную существующую альтернативную конфигурацию интерфейса. Обработка ошибок более подробно рассматривается после завершения списка комментариев.
7.	Кроме выбора нового интерфейса на уровне устройства родительский драйвер также создает массив дескрипторов каналов, из которого можно читать манипуляторы для последующего использования.
Попытка выбора интерфейса может завершиться неудачей из-за нехватки пропускной способности, отвечающей требованиям нашей конечной точки. Причина неудачи определяется анализом состояния URB:
If (URB_STATUS(&urb) — USBD_STATUS_NO_BANDWIDTH)
В ситуации с нехваткой пропускной способности возникают некоторые проблемы. Операционная система в настоящее время не предоставляет удобного механизма, посредствОхМ которого конкурирующие драйверы могли бы согласовать справедливое распределение ресурсов. Также она не предоставляет никаких оповещений о том, что другому драйверу не удалось зарезервировать необходимую пропускную способность, чтобы наш драйвер хмог добровольно «поделиться». Следовательно, в подобных ситуациях есть два основных варианта. Вариант первый — предоставить несколько альтернативных настроек интерфейсов, различающихся максимальным размером передачи для изохронных конечных точек. Обнаружив неудачу при попытке резервирования, можно попытаться выбирать настройки с постепенно снижающимися требованиями, пока очередная попытка не завершится удачей.
Находчивый пользователь, запустивший Диспетчер устройств в Windows ХР, может вызвать страницу свойств хостового контроллера USB (рис. 12.19). На этой странице отображается информация о текущем распределении пропускной способности. Двойной щелчок на одном из устройств в списке вызывает страницу свойств для соответствующего устройства. Возможно, грамотно сконструированная страница могла бы взаимодействовать со связанным драйвером устройства для сокращения его требований к пропускной способности. Тем не менее, в этой области, похоже, у Microsoft остаются широкие возможности для разработки более автоматизированных решений.
Другое решение проблемы заключается в таком отклонении IRP, чтобы приложение узнало о нехватке пропускной способности. Возможно, пользователь сможет отключить другое устройство, чтобы обеспечить необходимые ресурсы. Именно этот путь был выбран в примере USBISO, хотя я и не стал включать в тестовое приложение код, реагирующий на неудачу при выделении пропускной способности, — TEST.EXE просто отвечает отказом. Если вы выберете этот путь, необходимо разработать способ получения информации об отказе в пользовательском режиме. Если URB отклоняется с кодом USBD_STATUS_NO_BANDWIDTH, то от внутреннего управляющего IRP будет получен код NTSTATUS STATUS_DEVICE_ DATA_ERROR, которому не хватает конкретности. Вызов GetLastError в приложении вернет код ошибки ERROR_CRC. К сожалению, у приложения не существует
Работа с драйвером шины
571
простого способа узнать, что причиной ошибки является нехватка пропускной способности. Но если вы все же захотите пойти по этому пути, прочитайте врезку.
Рис. 12.19. Страница свойств для хостового контроллера USB
КАК УЗНАТЬ О НЕХВАТКЕ ПРОПУСКНОЙ СПОСОБНОСТИ В ПРИЛОЖЕНИИ------------------------
Допустим, вы делаете то же, что делается в примере USBISO, а именно пытаетесь переключиться на альтернативный интерфейс с высокими требованиями к пропускной способности при получении IRP_MJ_CREATE. Также предположим, что IRP завершается с кодом состояния, полученным при нехватке пропускной способности, то есть STATUSJ)EVICE_DATA_ERROR. Приложение в конечном счете увидит код ошибки CRCJERROR, как указано в основном тексте. Что дальше? Приложение не может отправить вам IOCTL для определения настоящей причины ошибки, потому что у него нет манипулятора вашего устройства, ведь запрос IRP_MJ_CREATE завершился неудачей. Возможно, следует предусмотреть возможность открытия манипуляторов устройства, которые не пытаются резервировать ресурсы пропускной способности. Также понадобится другой способ запроса пропускной способности в приложениях — возможно, посредством управляющей операции IOCTL. А может быть, ваше приложение будет просто интерпретировать ERROR_CRC при вызове CreateFile как признак нехватки пропускной способности. В конце концов, ошибки в данных довольно маловероятны, поэтому такая интерпретация в большинстве случаев будет правильной.
И все же лучшим решением было бы определение специального кода NTSTATUS и соответствующего кода ошибки Win32, означающего нехватку пропускной способности. Следите за изменениями в файлах NTSTATUS.H и WINERROR.H.
При получении запроса IRP_MJ„CLOSE для последнего оставшегося открытого манипулятора USBISO выполняет обратную операцию и выбирает исходный
572
Глава 12. USB
интерфейс по умолчанию. В ходе этой операции выдается очередной URB выбора интерфейса с индексом альтернативной настройки, равным 0.
Инициирование серии изохронных передач
Изохронные каналы могут использоваться как для чтения/записи данных дискретными блоками, так и для выдачи/приема данных непрерывным потоком. Пожалуй, потоковая пересылка данных является самым частым применением для изохронных каналов. Но кроме понимания механики работы с драйвером шины USB для управления потоковым каналом необходимо понимать и уметь решать дополнительные проблемы, относящиеся к буферизации данных, подбору частоты и т. д. Все эти проблемы решаются при помощи специального компонента потоков данных ядра операционной системы. К сожалению, мне не удалось включить даже во второе издание книги главу, посвященную потокам данных ядра, поэтому я продемонстрирую лишь программирование дискретной передачи данных по изохронным каналам.
Конечно, для выполнения чтения или записи с изохронным каналом используется URB с соответствующим кодом функции. Тем не менее, при создании и отправке изохронных URB существуют некоторые тонкости, которые ранее еще не упоминались. Во-первых, необходимо знать, как устройство разбивает передаваемые данные на пакеты. В общем случае устройство имеет возможность принять или отправить любой объем данных меньше максимума, заявленного для конечной точки (лишняя пропускная способность шины попросту не используется). Размер пакета, используемого устройством, не всегда связан с максимальным размером передачи для конечной точки, максимальным объемом данных, передаваемых в URB, или объемом данных, передаваемых между устройством и приложением за серию транзакций. Например, микрокод устройства USBISO работает с 16-байтовыми пакетами, хотя соответствующая изохронная конечная точка в соответствии с ее дескриптором способна обрабатывать до 256 байт на фрейм. Размер пакета должен быть априорно известен перед конструированием URB, потому что блок URB должен включать массив дескрипторов для всех передаваемых пакетов с указанием их размеров.
В предельно упрощенной ситуации процесс построения изохронного URB может выглядеть так:
ULONG length = MmGetMdlByteCountCIrp->MdlAdcress);
ULONG packsize =16: //a constant In USBISO
ULONG npackets = (length + packsize - 1) I packsize:
ASSERTCnpackets <= 255):
ULONG size = GET_ISO_URB_SIZE(npackets);
PURB urb = (PURB) ExAllocatePool(NonPagedPool, size);
RtlZeroMemory(urb. size):
Обратите внимание на использование макроса GET_ISOJJRB_SIZE для вычисления суммарного размера, необходимого изохронному URB для передачи заданного количества пакетов данных. Кстати, один URB может вмещать до 255 изохронных пакетов (1024 для высокоскоростных устройств), поэтому я и включил директиву ASSERT в этот фрагмент. Ограничивать приложение 255 пакетами
Работа с драйвером шины
573
нереально, поэтому в примере USBISO используется более сложное решение. Но пока я всего лишь хочу описать механику построения одного URB для изохронной (ISO) передачи.
ПРИМЕЧАНИЕ--------------------------------------------------------------------
Как указано в тексте, один блок URB может передавать до 255 пакетов полноскоростному устройству за соответствующее число 1-микросекундных фреймов. Для высокоскоростных устройств максимальное количество пакетов равно 1024, а передача занимает до 128 1-микросекундных фреймов. Количество пакетов в каждом URB должно быть кратно 8 — и это вполне логично, потому что один фрейм состоит из 8 микрофреймов.
Поскольку макроса UsbBuildXxrRequest для построения изохронных URB не существует, новый URB инициализируется вручную:
urb->UrbIsochronousfransfer.Hdr.Length = (USHORT) size;
urb->UrbIsochronousTransfer.Hdr.Funct1 on =
URBJUNCTIONJSOCHJRANSFER;
urb->UrbIsochronousfransfer.P1peHandle = pdx->hinpipe;
urb->UrbIsochronousTransfer.TransferFlags =
USBD_TRANSFER_DIRECTION_IN USBD_SHORT_TRANSFER__OK; urb->Urb!sochronousTransfer.TransferBufferLength length; urb->Urb!sochronousTransfer.TransferBufferMDL =
Irp->MdlAddress:
urb->UrbIsochronousfransfer.NumberOfPackets = npackets;
urb->Urb!sochronousTransfer.StartFrame = frame:
for (ULONG j = 0; 1 < npackets; ++1, length ~= packsize)
{
urb->UrbIsochronousTransfer.IsoPacketLI]-Offset = 1 * packsize;
}
Массив дескрипторов пакетов описывает весь буфер данных, с которым выполняется чтение или запись. Буфер должен занимать смежную область виртуальной памяти; фактически, это означает, что для его описания необходим один список MDL. Для усиления требований к смежности каждый дескриптор пакета содержит только смещение и длину части всего буфера, но не реальный указатель. Драйвер хостового контроллера задает длину, а вы задаете смещение.
Второй нюанс, связанный с изохронными передачами, относится к согласованию по времени. В USB все фреймы (или микрофреймы, в зависимости от ситуации) однозначно идентифицируются числами из возрастающей последовательности. Иногда бывает важно, чтобы передача началась с конкретного фрейма. Родительский драйвер позволяет указать на это обстоятельство, задав значение поля URB StartFrame. В примере USBISO согласование по времени не используется. Казалось бы, в нем можно установить флаг USBD_START_ISO_TRANSFER_ASAP, указывающий на то, что передача должна начаться как можно раньше. Более того, в версиях Windows, предшествующих Windows ХР, этот флаг нормально работал. К сожалению, в Windows ХР была допущена ошибка, и ASAP-передачи, которые должны начаться через 256 и более фреймов, планируются немедленно. В контексте нашего примера ошибка приведет к тому, что пакеты будут пересылаться
574
Глава 12. USB
в порядке 0, 256, 2, 3... Чтобы избавиться от этой проблемы, я переработал пример USBISO так, чтобы в нем использовался конкретный номер фрейма, вычисляемый по формуле
ULONG frame = GetCurrentFrame(pdx) + 2:
Функция GetCurrentFrame определяется следующим образом:
ULONG GetCurrentFrame(PDEVICE_EXTENSION pdx)
{
URB urb;
urb.UrbGetCurrentFrameNumber.Hdr.Length -
sizeof(struct _URB_GET_CURRENT_FRAME_NUMBER.):
urb.UrbGetCurrentFrameNumber.Hdr.Functlon =
URB_FUNCTION_GET_CURRENT_FRAME_NUMBER;
NTSTATUS status = SendAwaitUrb(pdx->Dev1ce0bject, &urb);
if (!NT_SUCCESS(status)) return 0;
return urb.UrbGetCurrentFrameNumber.FrameNumber;
}
Впрочем, не думайте, что получение текущего номера фрейма обязательно для изохронных передач. В USBISO это делается потому, что этот пример читает 256 пакетов. В более типичной ситуации используется потоковый драйвер, который выдает несколько URB чтения или записи для нескольких пакетов, а потом постоянно заново использует эти URB. В этом случае вы не столкнетесь с проблемой «256 фреймов» и можете спокойно использовать флаг USBD_START_ ISO_TRANSFER_ASAP.
Последняя проблема с изохронной обработкой связана с завершением передачи. Блок URB завершается успешно даже в том случае, если один или несколько пакетов содержали ошибки данных. В URB имеется поле ErrorCount, определяющее количество пакетов с ошибками. Если значение поля отлично от нуля, переберите дескрипторы пакетов и проверьте хранящиеся в них поля состояния.
Достижение приемлемой производительности
Достижение приемлемой производительности при изохронных передачах может создать некоторые проблемы в средах потоковой пересылки данных или в ситуации, в которой приходится организовывать мпогосегментные пересылки. Одна из стратегий заключается в том, чтобы код выполнялся в программном потоке реального времени на уровне DISPATCHJ-EVEL, a URB отправлялись напрямую драйверу шины через функцию SubmitlsoOutUrb в интерфейсе прямого вызова драйвера шины. Однако при выполнении операций ввода или при необходимости поддержки платформ, предшествующих Windows ХР, необходимо отправлять несколько URB, чтобы сразу же после завершения одного URB драйвер шины переходил к следующему.
Работа с драйвером шины
575
Пример USBISO в прилагаемых материалах показывает, как управлять передачей больших блоков с использованием нескольких вторичных URB. Логика чтения/записи в USBISO основана на том, что функция завершения вторичных IRP завершает главный IRP чтения/записи при завершении последнего вторичного IRP. Чтобы эта идея заработала, я объявил следующую специализированную контекстную структуру:
typedef struct _RWCONTEXT {
PDEVICE_EXTENSION pdx:
PIRP mainirp;
NTSTATUS status:
ULONG numxfer;
ULONG numirps;
LONG numpending;
LONG refcnt;
struct {
PIRP Irp;
PURB urb;
PMDL mdl;
} sub[l];
} RWCONTEXT, *PRWCONTEXT;
Диспетчерская функция IRP_MJ_READ (USBISO не обрабатывает запросы IRP_ MJ__WRITE) вычисляет количество вторичных IRP, необходимых для завершения передачи, и создает контекстную структуру:
U^ONG packsize = 16;
UlONG segsize = USBD_DEFAULT_MAXIMUM_TRANSFER_SIZE;
if (segsize / packsize > 255)
segsize = 255 * packsize;
ULONG numirps = (length + segsize - 1);
ULONG ctxsize = sizeof(RWCONTEXT) +
(numirps - 1) * sizeof(((PRWCONTEXT) 0)->sub):
PRWCONTEXT ctx = (PRWCONTEXT) ExAllocatePool(NonPagedPool.
ctxsize);
RtlZeroMemory(ctx, ctxsize);
ctx->numirps = ctx->numpending = numirps;
ctx->pdx = pdx;
ctx->mainirp = Irp;
ctx->refcnt = 2;
Irp->Tail.Overlay.Dr1verContext[0] = (PVOID) ctx;
Назначение последних двух команд этого фрагмента будет описано позднее, при описании логики отмены USBISO в подразделе «Отмена главного IRP». Далее в цикле создаются numirps запросов IRP_MJ_INTERNAL_DEVICE_CONTROL. При каждой итерации цикла вызывается функция loAllocatelrp, которая создает IRP с одним дополнительным элементом стека, чем требуется для объекта устройства следующего нижнего уровня. Кроме того, мы создаем URB для управления одним сегментом передачи и частичный список MDL для описания части основного
576
Глава 12. USB
буфера ввода/вывода, относящегося к текущему сегменту. Адрес IRP, URB и частичный MDL сохраняются в элементе вложенного массива структуры RWCONTEXT. Инициализация URB выполняется способом, уже показанным ранее. Затем мы инициализируем первые два элемента стека ввода/вывода вторичного IRP:
loSetNextIrpStackLocati on(subiгр);
PIO_STACKJ_OCATION stack = loGetCurrentlrpStackLocation(subirp);
stack->0eviceObject = fdo;
stack->Parameters.Others.Argumentl = (PVOID) urb;
stack^Parameters.Others.Argument? = (PVOID) mdl;
stack = loGetNextlrpStackLocaticn(subirp);
stack->MajorFunction = IRP-MJ_INTERMAL_DEVICE-CONTROL;
stack^Parameters.Others.Argumentl = (PVOID) urb;
stack->Parameters.DeviceIoControl.loControlCode = IOCTL_INTERNAL_USB_SUBMITJJRB;
loSetCompletionRoutineCsubirp, (PIO_COMPLETION_ROUTINE) OnStageComplete, (PVOID) ctx, TRUE. TRUE. TRUE);
Первый элемент стека предназначен для использования устанавливаемой нами функцией завершения OnStageComplete. Второй элемент предназначен для драйвера нижнего уровня.
После того как все IRP и URB будут созданы, наступает время передавать их драйверу шины. Но перед этим было бы разумно проверить, не был ли отменен главный IRP, а также установить для него функцию завершения. Соответствующая логика в конце диспетчерской функции выглядит примерно так:
IoSetCancelRoutine(Irp, OnCancelReadWrite);
if (Irp->Cancel)
{
status = STATUS__CANCELLED;
if (loSetCancelRout)ne(Irp, NULL)) -ctx->refcnt;
} else
status = STATUS_SUCCESS;
loSetComplet1onRout1ne(Irp,
(PIO-COMPLETION-ROUTINE) OnReadWri teComplete.
(PVOID) ctx. TRUE. TRUE, TRUE);
IoMa rkIrpPend1 ng(Irp);
loSetNextlrpStackLocation(Irp);
if (’NT_SUCCESS(status))
{
for (j = 0; 1 < numirps; ++1)
{
if (ctx->sub[i].urb)
Работа с драйвером шины
577
ExFreePool(ctx->sub[1].urb);
If (ctx->sub[1].mdl)
loFreeMdl(ctx->sub[i].mdl);
CompleteRequestCIrp, status, 0);
return STATUS_PENDING;
}
for (1 = 0; i < numirps; ++1)
loCal1 Driver(pdx->LowerDeviceObject, ctx~>sub[i].1rp):
return STATUS-PENDING;
Отмена главного IRP
Чтобы читателю стали понятны две функции завершения, приведенные в этом примере (то есть функция OnReadWriteComplete для главного IRP и функция Оп-StageComplete для каждого вторичного IRP), я должен объяснить, как в USBISO реализована отмена главного IRP. Мы должны учитывать вероятность отмены, потому что мы отправили потенциально большое количество вторичных IRP, на завершение которых может уйти определенное время. Главный IRP нельзя завершать до того момента, пока не будут завершены все вторичные IRP. Следовательно, необходимо предусмотреть механизм отмены главного IRP вместе со всеми вторичными IRP.
Конечно, вы еще помните из главы 5, что отмена IRP сопряжена с множеством непростых проблем синхронизации. В драйвере USBISO ситуация еще хуже, чем обычно.
Логика отмены усложняется тем фактом, что мы не можем контролировать моменты вызова функции завершения вторичных IRP — после отправки владельцем этих IRP становится драйвер шины. Допустим, мы напишем следующую функцию отмены:
VOID OnCancelReadWr1te(PDEVICE_OBJECT fdo, PIRP Irp)
{
IoReleaseCancelSp1nLock(Irp->CancelIrql);
PRWCONTEXT ctx = (PRWCONTEXT)	// 1
Irp->Tai1.Overlay.DriverContextEO];
for (ULONG 1 = 0: 1 < ctx->num1rps; ++1)	//2
loCancelIrp(ctx->sub[i].irp);
дополнительные действие
}
1. Адрес структуры RWCONTEXT был сохранен в поле IRP DriverContext именно для того, чтобы мы могли его здесь прочитать. Поле DriverContext остается в нашем распоряжении до тех пор, пока мы остаемся владельцем IRP. Так как мы вернули из диспетчерской функции код STATUS_PENDING, владелец не изменился.
2. Здесь отменяются все вторичные IRP. Если вторичный IRP уже завершен или в настоящее время активизирован для устройства, соответствующий вызов
578
Глава 12. USB
loCancellrp ничего не делает. Если вторичный IRP все еще остается в очереди драйвера хостового контроллера, то функция отмены драйвера хостового контроллера запускается и завершает вторичный IRP. Таким образом, во всех трех случаях можно быть уверенным в том, что все вторичные IRP относительно скоро будут завершены.
Эта версия OnCancelReadWrite почти закончена, но от нас потребуется еще один дополнительный шаг; прежде чем описывать его, я должен объяснить проблемы синхронизации, которые нам необходимо решить. Чтобы проблема стала более наглядной, я приведу функции завершения с двумя наивными ошибками. Вот функция завершения для одного сегмента общей передачи:
NTSTATUS OnStageComplete(PDEVICE_OBJECT fdo. PIRP subirp, PRWCONTEXT ctx) {
PIO_STACK LOCATION stack = IoGetCurrentIrpStackLocation(Irp);	// 1
PIRP mainirp «= ctx->mainirp;
PURB urb = (PURB) stack-Parameters.Others.Argument!;
1f (NT_SUCCESS(Irp->IoStatus.Status))
InterlockedExchangeAdd(CPLONG) &ctx->numxfer,	// 2
(LONG) urb->UrbIsochronousTransfer.TransferBufferLength);
}
else
ctx->status = Irp->IoStatus.Status;	// 3
ExFreePool(urb);	// 4
loFreeMdl((PMDL) stack-Parameters. Others. Argument2);
loFreelrp(subirp); // <== так делать нельзя	// 5
if (InterlockedDecrement(&ctx->numpending) == 0)
{
IoSetCancelRoutine(malnirp, NULL); // <== тоже требует
// дополнительной работы mainirp->IoStatus.Status = ctx->status;
loCompleteRequest(mainirp, IO_NO_INCREMENT);	//6
}
return STATUS_MORE-PROCESSING-REQUIRED;
)
1.	Это дополнительный элемент стека, созданный в диспетчерской функции. Для этого сегмента нам потребуется адрес URB, и стек является самым удобным местом для его хранения.
2.	При нормальном завершении сегмента здесь обновляется накопительный счетчик переданных байтов. Окончательное значение numxfer будет сохранено в поле loStatus.Information главного IRP.
3.	Поле status было инициализировано значением STATUS-SUCCESS при обнулении всей структуры контекста. Если хотя бы одна стадия завершится с ошибкой, эта команда сохранит код ошибки. Окончательное значение будет сохранено в поле loStatus.Status главного IRP.
Работа с драйвером шины
579
4.	URB и частичный MDL для этого сегмента нам более не нужны, поэтому мы освобождаем занимаемую ими память.
5.	Этот вызов loFreelrp и является «наивной» частью этой функции завершения (см. далее).
6.	Завершение последнего сегмента приводит к завершению главного IRP. После отправки вторичных IRP это единственная точка программы, в которой завершается главный IRP, поэтому мы можем быть уверены в том, что указатель на главный IRP остается действительным.
А вот «наивная» версия функции завершения для главного IRP:
NTSTATUS OnReadWriteComplete(PDEVICE_OBJECT fdo, PIRP Irp, PRWCONTEXT ctx) {
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) ctx->pdx;
if (Irp->Cancel)	// 1
Irp->IoStatus.Status = STATUS_CANCELLED;
else if (NT_SUCCESS(Irp->IoStatus.Status))
Irp->IoStatus.Information = ctx->numxfer;
ExFreePool(ctx);	// <== так делать нельзя	// 2
return STATUS_SUCCESS;
}
1. Если кто-то пытался отменить главный IRP, эта команда установит соответствующий статус завершения.
2. Как я вскоре объясню, проблемы возникают именно с освобождением памяти структуры контекста.
Я долго готовил эффектное, драматическое представление проблемы синхронизации, связанной с отменой IRP. Настал подходящий момент: допустим, наша функция отмены вызывается после одного или нескольких вызовов loFreelrp внутри OnStageComplete. Как нетрудно убедиться, это может привести к вызову loCancellrp с недействительным указателем. Или предположим, что функция отмены вызывается более или менее одновременно с OnReadWriteComplete, — в этом случае функция отмены может обратиться к структуре контекста уже после того, как она будет удалена.
Можно попытаться решить эти проблемы при помощи различных уловок. А если функция OnStageComplete будет обнулять соответствующий вторичный указатель на IRP в структуре контекста, а функция OnCancelReadWrite будет проверять его перед вызовом loCancellrp? (Да, но все равно невозможно гарантировать, что вызов loFreelrp не вклинится между проверкой OnCancelReadWrite и тем моментом, когда loCancellrp завершит изменение полей IRP, связанных с запросом). Может, защитить различные стадии зачистки при помощи спин-блокировки? (Ужасная идея, потому что это потребует удержания спин-блокировки между вызовами функций, занимающих много времени.) Нельзя ли воспользоваться известным нам фактом, что текущая версия Windows ХР всегда зачищает завершенные IRP в функции АРС? (Нельзя — по причинам, рассмотренным в главе 5.)
580
Глава 12. USB
Я довольно долго мучился с этой проблемой, но потом меня осенило. Почему бы не защитить структуру контекста и указатели на вторичные IRP подсчетом ссылок, чтобы функция отмены и главные функции завершения несли общую ответственность за их освобождение? В конечном итоге так и было сделано: я включил в структуру контекста счетчик ссылок (refcnt) и инициализировал его значением 2. Одна ссылка относится к функции отмены, а другая — к главной функции завершения. Затем я написал следующую вспомогательную функцию для освобождения объектов в памяти, являющихся источником проблемы:
BOOLEAN DestroyContextStructure(PRWCONTEXT ctx)
{
If (InterlockedDecrement(&ctx->refcnt) > 0) return FALSE;
for (ULONG 1 = 0; i < ctx->numirps: ++1)
if (ctx->sub[i].1rp)
loFreelrp(ctx->sub[1].1rp);
ExFreePool(ctx);
return TRUE;
}
Эта функция вызывается в конце функции отмены:
VOID OnCancelReadWrite(PDEVICE_OBJECT fdo, PIRP Irp)
{
IoReleaseCancelSp1nLock(Irp->CancelIrql);
PRWCONTEXT ctx = (PRWCONTEXT)
Irp->Ta11.Overlay.DriverContextCO];
for (ULONG 1 = 0; 1 < Ctx->numirps; ++i)
loCancelIrp(ctx->sub[1]. 1 rp);
PDEVICE-EXTENSION pdx = ctx->pdx;
1f (DestroyContextStructure(ctx))
CompleteRequestdrp, STATUS-CANCELLED, 0); IoReleaseRemoveLock(&pdx->RemoveLock, Irp); }
}
Я убрал вызов loFreelrp в конце функции завершения сегмента и добавил одну дополнительную строку кода для уменьшения счетчика ссылок в тот момент, когда мы точно знаем, что функция отмены не вызывалась и вызываться не будет:
NTSTATUS OnStageComplete(PDEVICE_OBJECT fdo, PIRP subirp,
PRWCONTEXT ctx)
{
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
PIRP mainirp = ctx->mainirp;
PURB urb = (PURB) stack->Parameters.Others.Argument!;
i f (NT-SUCCESS(Irp->IoStatus.Status))
ctx->numxfer +=
urb->UrbIsochronousTransfer.TransferBufferLength;
Работа с драйвером шины
581
else
ctx->status = Irp->IoStatus.Status;
ExFreePool(urb);
loFreeMdl((PMDL) stack^Parameters.Others.Argument2);
If (InterlockedDecrement(&ctx->numpending) == 0)
{
If (IoSetCancelRoutine(main1rp, NULL))
InterlockedDecrement(&ctx->refcnt);
ma1n1rp->IoStatus.Status = ctx->status;
IoCompleteRequest(malnirp. I0_N0_INCREMENT);
}
return STATUS_MORE_PROCESSING_REQUIRED;
}
Напомню, что функция loSetCancelRoutine возвращает предыдущее значение указателя отмены. Если оно равно NULL, значит, функция отмены уже была вызвана и она вызовет DestroyContextStructure. Но если значение отлично от NULL, значит, функция отмены вызываться уже не будет и мы должны позаботиться об освобождении структуры контекста.
Кроме того, безусловный вызов ExFreePool в главной функции завершения заменяется вызовом DestroyContextStructure:
NTSTATUS OnReadWr1teComplete(PDEVICE_OBJECT fdo, PIRP Irp,
PRWCONTEXT ctx)
{
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) ctx->pdx;
If (Irp->Cancel)
Irp->IoStatus.Status = STATUS-CANCELLED;
else If (NT_SUCCESSCIrp->IoStatus.Status))
Irp->IoStatus.Information = ctx->numxfer;
If (DestroyContextStructure(ctx))
{
IoReleaseRemoveLock(&pdx->RemoveLock, Irp);
return STATUS_SUCCESS;
}
else
return STATUS-MORE_PROCESSING_REQUI RED;
}
А вот как работает дополнительная логика. Функция отмены при вызове проходит по структуре контекста и вызывает loCancellrp для каждого из вторичных IRP. Даже если все они уже завершены, эти вызовы остаются безопасными, потому что мы еще не вызвали loFreelrp. Ссылка на структуру контекста также безопасна, потому что функция ExFreePool еще не вызывалась. Функция отмены завершает свою работу вызовом функции DestroyContextStructure, уменьшающей счетчик ссылок. Если главная функция завершения еще не выполнялась, DestroyContextStructure вернет FALSE, а функция отмены вернет управление. В этой
582
Глава 12. USB
точке структура контекста все еще существует, и это хорошо, потому что главная функция завершения скоро к ней обратится. Последующий вызов DestroyContextStructure из функции завершения освободит вторичные IRP и саму структуру контекста. Затем функция завершения вернет STATUS_SUCCESS, чтобы позволить довести до конца процесс завершения главного IRP.
Предположим, вызовы функции отмены и главной функции завершения произойдут в другом порядке. В этом случае вызов DestroyContextStructure в OnRead-WriteComplete просто уменьшит счетчик ссылок и вернет FALSE, тогда как OnReadWriteComplete вернет STATUS_MORE_PROCESSING_REQUIRED. Структура контекста все еще существует. Мы также можем быть уверены в том, что остаемся владельцем IRP и поля DriverContext, из которого функция отмены берет указатель на контекст. Однако вызов DestroyContextStructure в функции отмены уменьшит счетчик ссылок до 0, освободит память и вернет TRUE. Затем функция отмены освободит блокировку удаления и вызовет loCompleteRequest для главного IRP. Вы знаете, что завершать один и тот же IRP дважды запрещено, но этот запрет не относится к двукратному вызову loCompleteRequest как таковому. Если первый вызов loCompleteRequest приведет к вызову функции завершения, возвращающей STATUS_MORE„PROCESSING_REQUIRED, то повторный вызов loCompleteRequest вполне допустим.
ГДЕ ЖЕ СИНХРОНИЗАЦИЯ?------------------------------------------------------------------
Вероятно, вы заметили, что в только что приведенном коде проверка отмены в диспетчерской функции не защищается спин-блокировкой. Синхронизация этого кода с гипотетической стороной, вызывающей loCancellrp, обусловлена двумя фактами: во-первых, loSetCancelRoutine является атомарной операцией замены, а во-вторых, loCancellrp устанавливает флаг Cancel перед вызовом loSetCancelRoutine. Краткое описание loCancellrp приведено в главе 5.
Первый вызов loSetCancelRoutine в нашей диспетчерской функции может произойти после того, как loCancellrp установит флаг Cancel, но прежде, чем loCancellrp сама вызовет loSetCancelRoutine. Наша диспетчерская функция проверит, что флаг Cancel установлен, и вызовет loSetCancelRoutine повторно. Если окажется, что второй вызов предшествует вызову loSetCancelRoutine из loCancellrp, то функция отмены вызвана не будет. Кроме того, мы уменьшаем счетчик ссылок для структуры контекста, чтобы она была освобождена при первом вызове DestroyContextStructure.
Если второй вызов loSetCancelRoutine диспетчерской функцией следует за вызовом из loCancellrp, счетчик ссылок не уменьшается. Структура контекста будет освобождена либо функцией отмены, либо функцией завершения.
Если диспетчерская функция проверяет флаг Cancel до того, как он будет установлен в loCancellrp, или если функция loCancellrp для этого IRP вообще не вызывалась, мы запускаем вторичные IRP. Если же функция loCancellrp была вызвана в далеком прошлом до того, как мы установили функцию отмены, она просто установит флаг Cancel и вернет управление. Далее происходит то же самое, как если бы наша диспетчерская функция обнулила указатель отмены до того, как loCancellrp вызовет loSetCancelRoutine.
Итак, вы видите, что для обеспечения безопасности в многопроцессорной среде не всегда нужна спин-блокировка — иногда атомарные операции сами решают проблему.
Остается проанализировать ситуацию, при которой функция отмены вообще не вызывается. Конечно, этот случай следует считать нормальным, потому что IRP обычно не отменяются. Этот факт обнаруживается, когда мы вызываем loSetCancelRoutine, готовясь к завершению главного IRP. Если loSetCancelRoutine
Работа с драйвером шины
583
вернет значение, отличное от NULL, мы знаем, что функция loCancellrp для главного IRP еще не вызывалась (иначе указатель отмены уже был бы равен NULL, а функция loSetCancelRoutine вернула бы NULL). Более того, мы знаем, что наша функция отмены теперь никогда вызываться не будет, а следовательно, счетчик ссылок можно уменьшить. Соответственно, мы уменьшаем счетчик ссылок вручную, чтобы вызов DestroyContextStructure из OnReadWriteComplete освободил память.
АССОЦИИРОВАННЫЕ IRP--------------------------------------------------------------------
На первый взгляд может показаться, что функция loMakeAssociatedlrp дает хорошую альтернативную возможность создания вторичных IRP, необходимых для примера USBISO. Общая идея loMakeAssociatedlrp заключается в том, что для одного главного IRP создается некоторое количество ассоциированных IRP. При завершении последнего ассоциированного IRP I/O Manager автоматически завершает главный IRP.
К сожалению, ассоциированные IRP плохо подходят для решения проблем, с которыми мы сталкиваемся в USBISO. Прежде всего, функция loMakeAssociatedlrp вообще не должна использоваться в драйверах WDM. Логика завершения ассоциированных IRP в Windows 98/Ме неверна — при завершении последнего ассоциированного IRP функции завершения главных IRP не вызываются. Впрочем, даже в Windows ХР I/O Manager не отменяет ассоциированные IRP при отмене главного IRP. Кроме того, вызов loFreelrp для ассоциированного IRP происходит внутри loCompleteRequest, в контексте того потока, который окажется текущим. Это обстоятельство усложняет безопасную отмену ассоциированных IRP.
Управление питанием при бездействии для устройств USB
Может, кто-то считает, что управление питанием в драйверах WDM недостаточно сложно? В главе 8 мы обсудили стратегии поддержания устройства в состоянии низкого энергопотребления в то время, пока оно не используется (что бы это ни значило для вашего конкретного устройства). Начиная с Windows ХР в системе появился специальный протокол для устройств USB, называемый избирательной приостановкой (selective suspend). В последней части этой главы я опишу механику реализации этого протокола в функциональных драйверах.
ПРИМЕЧАНИЕ----------------------------------------------------------
Код, рассматриваемый в этом разделе, взят из примера WAKEUP главы 8.
Появившийся в Windows ХР режим избирательной приостановки решает проблемы с пробуждением, возникающие в составных устройствах. Допустим, имеется устройство с двумя функциями, причем каждая функция обслуживается отдельным функциональным драйвером. Теперь предположим, что один из функциональных драйверов выдает запросы IRP_MN_WAIT_WAKE и IRP_MN„SET_ POWER для перевода своего интерфейса в состояние D2, а другой функциональный драйвер оставляет свой интерфейс в состоянии DO. Первый функциональный драйвер включает питание на своем интерфейсе по сигналу пробуждения. Если бы физическое устройство не было составным, родительский драйвер включил бы свою функцию пробуждения и перевел устройство в состояние D2. При
584
Глава 12. USB
последующей активизации устройства генерируется сигнал пробуждения, и родительский драйвер завершает запрос WAIT_WAKE.
Однако в составных устройствах сигнал пробуждения не сработает. Родительский драйвер не отключает реальное устройство (и не активизирует функцию пробуждения) до тех пор, пока все функциональные драйверы независимо друг от друга не запросят отключения своих интерфейсов. В приведенном примере только один из двух функциональных драйверов отключил питание своего интерфейса. Поскольку реальное устройство не приостанавливается, у него нет причин когда-либо выдавать сигнал пробуждения. Однако функциональный драйвер интерфейса, который должен был быть приостановлен, этого не понимает и не пытается взаимодействовать с устройством. В итоге мы получаем устройство с неработающей функцией.
Избирательная приостановка обеспечивает координацию между функциональными драйверами для решения этой проблемы. Вот как она работает: вместо того чтобы отключать питание своего интерфейса напрямую, функциональный драйвер выдает операцию IOCTL родительскому драйверу. Эта операция означает: «Я готов к приостановке; вызови эту функцию, чтобы я мог это сделать». Когда все функциональные драйверы некоторого составного устройства выдадут эту операцию IOCTL, родительский драйвер может вызвать все функции обратного вызова. Каждая функция обратного вызова отключает свой интерфейс. Затем родительский драйвер отключает питание всего устройства. Последующий сигнал пробуждения заново активизирует каждый функциональный драйвер, выдавший запрос. IRP_MN_WAIT_WAKE. Voila! Проблема с неработающими функциями решена.
Большинство функциональных драйверов USB обслуживает один интерфейс. Не следует полагать, что некоторый интерфейс никогда не будет частью составного устройства, кроме того, вы должны считать, что функциональные драйверы других интерфейсов того же устройства будут использовать сигналы пробуждения (даже если на самом деле это не так). Тем самым вы будете следовать протоколу избирательной приостановки, который я собираюсь описать. Если ваш драйвер будет работать на платформе, предшествующей Windows ХР, по умолчанию отключите возможности пробуждения и автоматической приостановки. Предоставьте инструкции, которые позволят конечному пользователю включить одну или обе эти возможности в тех ситуациях, в которых это не создаст проблем с «мертвыми интерфейсами».
Даже если ваш драйвер управляет всеми интерфейсами некоторого устройства, все равно поддерживайте протокол избирательной приостановки, потому что драйверы Microsoft также полагаются на него для компенсации аппаратных ошибок в различных чипсетах.
Прежде всего объявите ряд дополнительных полей в структуре расширения устройства:
typedef struct _DEVICE_EXTENSION {
PIRP Suspendlrp;
LONG SuspendlrpCancelled;
Работа с драйвером шины
585
USB_IDLE_CALLBACK_INFO cblnfo;
} DEVICE-EXTENSION, *PDEVICEJXTENSION;
Когда вы решите, что настало время перевести устройство в состояние пониженного энергопотребления из-за бездействия, выдайте родительскому драйверу внутренний управляющий запрос на регистрацию функции обратного вызова (SelectiveSuspendCallback):
NTSTATUS IssueSelect1veSuspendRequest(PDEVICE_EXTENSION pdx)
{
PIRP Irp = IoAllocateIrp(pdx->LowerDevice0bject->StackS1ze, FALSE);
pdx->cb1nfo.IdleCall back =
(USBJDLE_CALLBACK) Sei ecti veSuspendCal1 back;
pdx->cb1nfo.IdleContext = (PVOID) pdx;
PIO_STACK_LOCATION stack = loGetNextlrpStackLocation(Irp);
stack->MajorFunct1on = IRP_MJ_INTERNAL_DEVICE_CONTROL;
stack->Parameters.DeviceloControl.loControlCode =
IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION;
stack->Parameters.DeviceloControl.Type3InputBuffer =
&pdx->cb1nfo;
pdx->SuspendIrp = Irp;
pdx->SuspendIrpCancelled = 0;
loSetCompletionRoutine(Irp, (PIO_COMPLETION_ROUTINE)
SeiectiveSuspendCompletionRoutine, (PVOID) pdx,
TRUE, TRUE, TRUE);
loCa11 Dr1 ver(pdx->LowerDev1ceObject, Irp);
return STATUS__SUCCESS;
}
Для этой управляющей операции используется асинхронный IRP, так как он может остаться незавершенным в течение долгого времени, вследствие чего может возникнуть необходимость в его отмене. В том, что касается организации функций отмены и завершения, я последовал собственным рекомендациям из главы 5:
VOID Cancel Sei ecti veSuspend(PDEVICEJEXTENSION pdx)
{
PIRP Irp = (PIRP) InterlockedExchangePo1nter(
(PVOID*) &pdx~>SuspendIrp, NULL);
If (Irp)
{
loCancellrp(Irp);
If (InterlockedExchange(&pdx->SuspendIrpCancelled, D) loFreelrp(Irp);
}
}
NTSTATUS Sei ecti veSuspendCompl etionRouti ne(PDEVICE_OBJECT jLink,
PIRP Irp. PDEVICE_EXTENSION pdx)
586
Глава 12. USB
{
NTSTATUS status = Irp->IoStatus.Status;
if (InterlockedExchangePointer((PVOID*) &pdx->SuspendIrp, NULL)
Inter1ockedExchange(&pdx->SuspendIrpCancelled, 1)) loFreelrp(Irp);
if (!NT_SUCCESS(status) && status != STATUS JWR_STATE_INVALID) GenericWakeupFromIdle(pdx->pgx, FALSE);
return STATUS_MORE^PROCESSING_REQUIRED;
}
(Вскоре я объясню, зачем нужен вызов GenericWakeupFromldle.)
В нормальном случае родительский драйвер приостанавливает IRP оповещения о бездействии до тех пор, пока все устройства, подключенные к тому же концентратору, не обратятся с запросом на снижение энергопотребления. Когда это произойдет, родительский драйвер вызывает функцию обратного вызова, а вы должны выполнить два действия. Во-первых, убедитесь в том, что функция пробуждения (если она есть) включена и для устройства имеется необработанный запрос WAIT_WAKE. Во-вторых, потребуйте, чтобы IRP питания перевел ваше устройство в состояние пониженного энергопотребления. Например, в драйвере, использующем GENERIC.SYS для управления питанием, функция обратного вызова может быть совсем простой:
VOID SelectiveSuspendCallback(PDEVICE_EXTENSION pdx)
{
GenericWakeupControl(pdx->pgx, ManageWaitWake);
Gener1cIdleDevice(pdx->pgx, PowerDeviceDZ, TRUE);
}
С аргументом TRUE функции GenericIdleDevice операция управления питанием выполняется синхронно, что является обязательным требованием в данной ситуации. Более того, при возврате из функции обратного вызова до того, как устройство перейдет в состояние пониженного энергопотребления, родительский драйвер ошибочно решит, что отключить питание не удалось, а весь концентратор с присоединенными устройствами останется включенным.
Если родительский драйвер допускает отклонение запросов оповещения о бездействии, устройство может оказаться в состоянии низкого энергопотребления, и его необходимо включить в функции завершения — отсюда и вызов GenericWakeupFromldle в этом примере. Единственным исключением является завершение запроса с кодом STATUS_POWER_STATE„INVALID — это происходит только при переводе устройства в состояние D3 при необработанном IRP. Е1апример, это может произойти, пока система находится в спящем режиме.
Наконец, не забудьте отменить необработанные IRP оповещения о бездействии во время выполнения StopDevice.
Устройства взаимодействия с пользователем
Устройства взаимодействия с пользователем (HID, Human Device Interface) обмениваются информацией с компьютером посредством структурированных отчетов. Класс HID в основном состоит из устройств, используемых в интерфейсе конечного пользователя. К нему относятся клавиатуры, мыши и всевозможные игровые контроллеры, но он также может включать любые мыслимые кнопки, рукоятки, переключатели, регуляторы, экзоскелетные устройства и другие типы управляющих манипуляторов, используемых для управления компьютером. Неинтерактивные устройства вроде устройств чтения штрих-кодов и измерительных приборов тоже могут быть спроектированы так, что они будут соответствовать правилам класса HID. Кроме того, устройства HID могут включать такие компоненты, как светодиоды, мини-дисплеи, активную обратную связь и другие индикаторы.
Устройства HID, сконструированные для шины USB, соответствуют спецификации «Device Class Definition for Human Input Devices». Дополнительные спецификации, относящиеся к активной обратной связи, приведены в документе USB «Device Class Definition for Physical Interface Devices». HDD базируется на обширном наборе числовых констант, определения которых находятся в спецификации «HID Usage Tables». Все эти спецификации могут быть свободно загружены с сайта www.usb.org.
Хотя спецификации HID ориентированы на реализации USB, любое устройство может частично или полностью работать как устройство HID. Еще раз подчеркну: важная характеристика устройства HID заключается в том, что хост может выполнять операции ввода/вывода с использованием пакетов, соответствующих чрезвычайно гибкому определению структуры данных, называемой дескриптором отчета (report descriptor).
Приложение работает с HID-совместимыми клавиатурами и мышами только косвенно, на уровне обработки оконных сообщений, обозначения и содержание которых мало изменились за последние двадцать лет. С другими HID-устройства-ми приложения работают через COM-интерфейсы, являющиеся частью компонента Windows DirectX, и посредством вызова функций Win32 API.
588
Глава 13. Устройства взаимодействия с пользователем
Драйверы HID-устройств
Драйвер класса HID-устройств от Microsoft - HIDCLASS.SYS — обеспечивает общую инфраструктуру для драйверов WDM, управляющих устройствами HID на всех платформах Windows. Microsoft также предоставляет минидрайвер HIDCLASS с именем HIDUSB.SYS для устройств USB, у которых в дескрипторе устройства или интерфейса заявлена принадлежность к классу HID. Соответственно, если ваше устройство USB принадлежит к классу HID, возможно, вам вообще не придется писать для него специализированный драйвер, потому что драйвер класса Microsoft и минидрайвер полностью поддерживают спецификации USB.
Если вы проектируете устройство USB, которое включает HID-подобную функциональность, не забудьте о возможности создания составного устройства за счет определения нескольких интерфейсов. Универсальный родительский драйвер разделит функции устройства, чтобы система загрузила стандартные драйверы Microsoft для каждой функции HID.
Компания Microsoft также предоставляет драйверы для стандартных клавиатур и мышей PS2, а также для мышей, подключаемых к последовательному порту. Эти драйверы вместе с HIDCLASS располагаются ниже фильтрующих драйверов классов с именами KBDCLASS и MOUCLASS, предоставляющих согласованный интерфейс к компонентам более высокого уровня.
Возможно, вам придется написать собственный минидрайвер для замены HIDUSB.SYS, если ваш интерфейс или устройство USB предоставляют или принимают структурированные отчеты, но не принадлежат классу HID. В таком случае минидрайвер предоставляет HIDCLASS фиктивный дескриптор HID, а также создает структурированные отчеты для данного дескриптора в ответ на входные события.
Даже с полноценным устройством USB HI D-класса может потребоваться написать собственный минидрайвер для поддержки нестандартных возможностей. Я использовал этот подход при создании драйверов нескольких специализированных устройств, включая игровую мышь с множеством кнопок и индикаторов и устройств отслеживания перемещений головы, для которого показания датчиков приходилось транслировать в позиционные отчеты. В этих случаях устройство номинально являлось устройством USB HID-класса, но мои клиенты хотели, чтобы поставляемые устройством отчеты отличались от генерируемых микрокодом. Включать специализированную функциональность в «прошивку» было бы непрактично.
Наконец, если у вас имеется устройство с HID-подобной функциональностью, но не обладающее интерфейсом USB (кроме стандартных клавиатур и мышей), пользовательский минидрайвер класса HIDCLASS является единственным реальным способом предоставить доступ к этому устройству со стороны DirectX (а следовательно, и существующих приложений).
Отчеты и дескрипторы отчетов
Устройства HID передают информацию в блоках, называемых отчетами (reports). Отчет Содержит битовые и целочисленные поля, отформатированные в соответствии с дескриптором отчета. Многие спецификации HID и связанные с ними
Отчеты и дескрипторы отчетов
589
документы во всех подробностях описывают содержимое отчетов и их дескрипторов. Мы проанализируем примеры двух дескрипторов отчетов, чтобы вы лучше поняли спецификации.
Пример дескриптора клавиатуры
Для начала я рекомендую загрузить утилиту HID Descriptor Tool (DT.EXE) с сайта http://www.usb.org. Эта утилита позволяет создавать и редактировать дескрипторы отчетов с использованием символических имен. На рис. 13.1 изображены интерфейс и один из примеров дескрипторов, входящих в поставку утилиты.
’ J НЮ Descriptor Tool (DT) - C:\HidToohkeybrd.hid

File Edit Perse Descriptor About
HID Items
fUSAGE -------------
USAiGEZPAgE'.......
USAGE-MINIMUM USAGE-MAXIMUM DESIGNATOR-INDEX DESIGNATOR-MINIMUM DESIGNATOR-MAXIMUM STRING-INDEX STRING-MINIMUM STRING-MAXIMUM COLLECTION END-COLLECTION INPUT OUTPUT FEATURE LOGICAL-MINIMUM LOGICAL-MAXIMUM PHYSICAL-MINIMUM PHYSICAL-MAXIMUM UNIT-EXPONENT UNIT
REPORT-SIZE REPORT-ID REPORT-COUNT
Manual Entry
Clear Descriptor
Report Descriptor
USAGE_PAGE (Generic Desktop)0501
USAGE (Keyboard)	09	06
COLLECTION (Application)	Al	01
USAGE.PAGE (Keyboard)	05	07
USAGE-MINIMUM (Keyboard LeftControl) 19	EO
USAGE-MAXIMUM (Keyboard Right GUI) 29	E7
LOGICAL-MINIMUM (0)	15	00
LOGICAL-MAXIMUM (1)	25	01
REPORT-SIZE (1)	75	01
REPORT-COUNT (8)	95	08
INPUT (Data, Var, Abs)	81	02
REPORT-COUNT (1)	95	01
REPORT-SIZE (8)	75	08
INPUT (Cnst,Var,Abs)	81	03
REPORT-COUNT (5)	95	05
REPORT-SIZE (1)	75	01
USAGE.PAGE (LEDs)	05	08
USAGE-MINIMUM (Num Lock)	19	01
USAGE-MAXIMUM (Капа)	29	05
OUTPUT (Data,Var,Abs)	91	02
REPORT-COUNT (1)	95	01
REPORT-SIZE (3)	75	03
OUTPUT (Cnst,Var,Abs)	91	03
REPORT-COUNT (6)	95	06
REPORT-SIZE (8)	75	08
LOGICAL-MINIMUM (0)	15	00
LOGICAL-MAXIMUM (101)	25	65
USAGE—PAGE (Keyboard)	05	07
USAGE-MINIMUM (Reserved (no event indicated))
USAGE-MAXIMUM (Keyboard Application) 29	65
INPUT (Data,Ary, Abs)	81	00
END-COLLECTION	CO
Рис. 13.1. Определение дескриптора отчета клавиатуры в дескрипторе HID Tool
Первый элемент дескриптора отчета задает страницу использования (usage page), то есть, фактически, пространство имен для интерпретации некоторых числовых констант в последующих элементах дескриптора. Для интерпретации чисел вам потребуется документ «HID Usage Tables». Например, код 6 означает клавиатуру в странице обобщенных устройств настольных компьютеров, но устройство имитации кораблевождения в странице имитационных устройств.
Второй элемент задает тип использования для следующей коллекции верхнего уровня дескриптора. В спецификации HID коллекции предназначаются для группировки взаимосвязанных объектов данных. Например, в физических коллекциях группируются элементы, собранные в одной геометрической точке,
590
Глава 13. Устройства взаимодействия с пользователем
а в прикладных коллекциях группируются элементы, известные приложениям. Концепция логической коллекции позволяет группировать взаимосвязанные элементы в составных структурах данных — например, счетчик байтов, за которым следуют сами данные. Вследствие своей абстрактности эти концепции практически бесполезны, поэтому Microsoft наделяет их дополнительным смыслом: О Коллекция верхнего уровня (например, та, что начинается с третьего элемента в примере дескриптора клавиатуры) соответствует отдельно адресуемой сущности. Действуя в качестве драйвера шины, HIDCLASS создает объект физического устройства (PDO) для каждой коллекции верхнего уровня. Идентификатор устройства для коллекции включает обобщенный совместимый идентификатор, основанный на коде использования (табл. 13.1). Если коллекция имеет другое использование, HIDCLASS не создает совместимый идентификатор. За дополнительной информацией о роли совместимого идентификатора при поиске драйверов обращайтесь к главе 15. Затем объект PDO становится основанием стека РпР для некоторого типа устройства. Для нескольких коллекций верхнего уровня создаются несколько стеков. Чтобы эта схема работала на практике, устройство должно различать разные коллекции по идентификаторам отчетов.
О Ссылочные коллекции встраиваются в коллекции верхнего уровня. Ссылочные коллекции обеспечивают организационную структуру, которая может использоваться приложениями для группировки взаимосвязанных элементов составного устройства. Например, на геймпаде при помощи ссылочных коллекций могут различаться кнопки, активизируемые левой и правой рукой. На мой взгляд, это не имеет особого смысла, поскольку приложения чаще требуют, чтобы пользователь закреплял смысл за элементами на основании числовых данных, а не их позиции в иерархии. Но, возможно, я просто не видел достаточного количества приложений и HID-устройств для подобных обобщений.
Таблица 13.1. HIDCLASS-совместимые идентификаторы для каждого поддерживаемого типа использования
Страница использования	Использование	Совместимый идентификатор
Обобщенные настольные устройства	Указатель или мышь	HID_DEVICE_SYSTEM_MOUSE
	Клавиатура или малая клавишная панель	HID_DEVICE_SYSTEM_KEYBOARD
	Джойстик или геймпад	HID_DEVICE„SYSTEM_GAME
	Системное управляющее устройство	HID_DEVICE_SYSTEM_CONTROL
Потребительские	(любое)	HID__DEVICE_SYSTEM_CONSUMER
В единственной коллекции верхнего уровня рассматриваемого дескриптора самыми важными являются главные элементы с именами INPUT и OUTPUT. Элемент INPUT представляет поле входного отчета, а элемент OUTPUT — поле выходного отчета. Также могут присутствовать элементы FEATURE, определяющие
Отчеты и дескрипторы отчетов
591
поля в отчетах возможностей, но в нашем примере их пет. Главным элементам предшествует некоторое количество глобальных элементов, описывающих представление и смысл самих данных.
Важно понимать, что элементы отчетов INPUT, OUTPUT и FEATURE могут перемежаться в дескрипторе отчета. Группировка элементов данных в конкретном отчете не зависит от логической структуры коллекции верхнего уровня — она зависит от типа элементов. При виде перемежающихся элементов INPUT и OUTPUT в дескрипторе клавиатуры может возникнуть мысль, что мы имеем дело с пятью разными отчетами или с одним двунаправленным отчетом. В действительности имеется один входной отчет, определяемый элементами INPUT, и один выходной отчет, определяемый элементами OUTPUT.
Главные элементы, наряду со всеми уточняющими глобальными элементами, определяют битовую структуру отчета. Биты обозначаются справа налево и нс разделяются неиспользуемыми битами для обеспечения выравнивания. Многобитовые значения, в том числе и выходящие за границу байтов, хранятся в прямом порядке (наименее значащий бит располагается справа на диаграмме). Результат делится на байты, передаваемые устройством справа налево.
В дескрипторе клавиатуры коллекция состоит из пяти элементов данных, которые определяют входной и выходной отчеты (рис. 13.2):
О Входной элемент, состоящий из восьми (REPORT_COUNT) однобитовых значений (REPORT_SIZE 1), каждое из которых принимает значения О (LOGICAL-MINIMUM) или 1 (LOGICAL-MAXIMUM). Смысл этих битов соответствует типам использования клавиатуры (USAGE_PAGE) с ЕО по Е7 (от USAGE-MINIMUM до USAGEMAXIMUM). Другими словами, байт 0 входного отчета состоит из флаговых битов, которые указывают, какие клавиши модификации режимов в настоящее время нажаты на клавиатуре.
О Постоянный входной элемент, состоящий из одного (REPORT-COUNT) 8-битового значения (REPORT_SIZE). Байт 1 входного отчета представляет собой простой заполнитель, не содержащий данных.
О Выходной элемент, состоящий из пяти (REPORT_COUNT) однобитовых значений (REPORT_SIZE). На эти значения распространяются ранее заданные значения LOGICAL-MINIMUM и LOGICAL-MAXIMUM, потому что они не переопределялись. Тем не менее, эти биты несут другой смысл: они представляют светодиодные индикаторы (USAGE_PAGE) вроде NumLock (USAGE_MINUMUM и USAGEMAXIMUM). Другими словами, младшие 5 битов байта 0 выходного отчета содержат флаги, управляющие индикаторами клавиш-переключателей режимов.
О Постоянный выходной элемент, состоящий из одного (REPORT_COUNT) 3-битового значения (REPORT_SIZE). Эти три бита дополняют выходной отчет до полного байта.
О Входной элемент, состоящий из шести (REPORT_COUNT) 8-битовых значений (REPORT_SIZE), в интервале от 0 до 101 (LOGICAL-MINIMUM и LOGICAL-MAXIMUM), представляющих состояние клавиш стандартной 101-клавишной клавиатуры (USAGE-PAGE, USAGE-MINIMUM и USAGE-MAXIMUM). Другими словами, байты 2-7 входного отчета содержат коды до шести одновременно удерживаемых клавиш.
592
Глава 13. Устройства взаимодействия с пользователем
Входной отчет:
7	6	5	4	3	2	1
О
Коды до 6 клавиш
1.......Right	GUI -J
.1......Right	Alt----
..1.....Right	Shift —
...1	.... Right	Control -J
....1... Left GUI---------
....1.. Left Alt------—
....1. Left Shift---------
.....1 Left Control -
0001 ООО. ООО. ООО. ООО.
Выходной отчет:
Рис. 13.2. Структура входных и выходных отчетов клавиатуры
Дескриптор HIDFAKE
На рис. 13.3 изображен дескриптор отчета, используемый в примере HIDFAKE (см. прилагаемые материалы). Этот дескриптор обладает рядом отличий от дескриптора клавиатуры:
О Для приложения верхнего уровня указан тип использования Gun Device из страницы Gaming Controls. Это искусственный выбор, который я сделал для того, чтобы избежать трудностей с установкой драйвера. Для всех типов использования, перечисленных в табл. 13.1, HIDCLASS предоставляет совместимый идентификатор устройства вместе с идентификатором конкретного устройства. Windows ХР отдает предпочтение подписанному драйверу, соответствующему совместимому идентификатору, перед неподписанным драйвером (таким как HIDFAKE.SYS), соответствующим конкретному идентификатору устройства. (За дополнительной информацией о выборе драйверов в Windows ХР обращайтесь к главе 15.)
О В одной главной коллекции используются три логические коллекции. Они всего лишь наглядно выделяют структуру дескриптора, состоящего из трех отчетов. Пример полностью работоспособен и без них.
О Дескриптор включает один входной отчет и два отчета возможностей. Входной отчет (1) содержит информацию об одном типе использования. Первый ответ возможностей (2) предназначен для получения номера версии драйвера, а второй (3) — для управления состоянием фиктивной кнопки из тестового приложения.
HIDFAKE демонстрирует одну нетривиальную особенность дескрипторов отчетов. Для отчетов возможностей более или менее обязательно наличие числовых
Минидрайверы HIDCLASS
593
идентификаторов, потому что спецификация HID требует их указания в управляющих командах Get_Report_Request и Set_Report_Request. Если какой-либо отчет коллекции верхнего уровня обладает идентификатором, то идентификаторы должны быть у всех отчетов в этой коллекции. Однако в действительности HIDFAKE моделирует вымышленное устройство, которое обладает отчетом о состоянии кнопки и не имеет отчетов возможностей. Я определил отчеты возможностей для того, чтобы обеспечить «внеполосное» взаимодействие тестового приложения с драйвером. Если бы мы имели дело с настоящим устройством, драйвер был бы обязан вставлять идентификатор отчета в каждый входной отчет, прочитанный от устройства.
". ]HID Descriptor Tool (DTj D.\WDMBOOK\chap13\hidfake\sys\Repo<(Descriptor,hid

:i’ ---------------------
~ USAGEJHINIMUM USAGE-MAXIMUM
-DESIGNATOR-INDEX
; DESIGNATOR-MINIMUM > DESIGNATOR-MAXIMUM
; STRING-INDEX  1 STRING-MINIMUM STRING-MAXIMUM COLLECTION
< END-COLLECTION ' INPUT
,. OUTPUT FEATURE LOGICAL-MINIMUM LOGICAL-MAXIMUM PHYSICAL-MINIMUM PHYSICAL-MAXIMUM
< UNIT-EXPONENT UNIT REPORT-SIZE REPORT-ID REPORT-COUNT
 - J У*1?1	; 1	‘
’ 'I' Cl eeZ D'escrVp-t^r. * f'
U^AG E_P AG E До ami ng Co nt го1s) USAGE (Gun Device ) COLLECTION (Application) COLLECTION (Logical)
REPORT-ID (1) USAGE-PAGE (Button) USAGE (Button 1) LOGICAL-MINIMUM (0) LOGICAL-MAXIMUM (1) REPORT-SIZE (1) REPORT-COUNT (1) INPUT (Data,Var,Abs) REPORT-SIZE (7) INPUT (Cnst,Var,Abs)
END-COLLECTION
COLLECTION (Logical)
REPORT-ID (2)
USAGE_PAGE (Generic Desktop) USAGE (X) LOGICAL-MAXIMUM (-1) REPORT-SIZE (32) FEATURE (Data,Van,Abs)
END-COLLECTION
COLLECTION (Logical)
REPORT-ID (3) USAGE-PAGE (Button) USAGE (Button 1) LOGICAL-MAXIMUM (1) REPORT-SIZE (1) FEATURE (Data,Van,Abs) REPORT-SIZE (?) FEATURE (Cnst,Var,Abs) END-COLLECTION
END-COLLECTION
05“0l 09 Al Al 85 05 09 15 25 75 95 81 75 81 CO Al 85 05 09 25 75 81 CO Al 85 OS 09 25 75 81 75 81 CO CO
03 01 02 01 09
01 00 01 01 01
02 07 03
02 02 01 30 FF 20 02
02 03 09 01 01
01 02 07 03

3;





$
$
Рис. 13.3. Определение дескриптора отчета HIDFAKE
Минидрайверы HIDCLASS
Как упоминалось ранее, Microsoft поставляет драйвер (HIDUSB.SYS) для любого устройства USB, построенного в соответствии со спецификацией HID. В этом разделе рассказано, как построить минидрайвер HIDCLASS для другого типа устройства, которое вы хотите «замаскировать» под устройство HID.
DriverEntry
Функция DriverEntry минидрайвера HIDCLASS похожа на одноименную функцию драйвера WDM, но только до определенного момента. В этой функции мы
594
Глава 13. Устройства взаимодействия с пользователем
инициализируем структуру данных DRIVER-OBJECT указателями на функции AddDevice и Driverllnload, а также указателями на диспетчерские функции трех типов пакетов запросов ввода/вывода (IRP): IRP_MJ_PNP, IRP_MJ_POWER и IRP-MJ_INTERNAL_DEVICE_CONTROL. Затем мы создаем структуру HID_MINIDRIVER_ REGISTRATION и вызываем HidRegisterMinidriver  одну из функций, экспортируемых классом HIDCLASS.SYS. В табл. 13.2 описаны поля структуры HID_ MINIDRIVERREGISTRATION.
Таблица 13.2. Поля структуры HID_MINIDRIVER_REGISTRATION
Имя поля	Описание
Revision	(ULONG) Минидрайвер заносит в это поле значение HID_REVISION, в настоящее время равное 1
DriverObject	(PDRIVER-OBJECT) Минидрайвер заполняет это поле значением, передаваемым в аргументе DriverObject функции DriverEntry
RegistryPath	(PUNICODE_STRING) Минидрайвер заполняет поле значением, переданным в аргументе RegistryPath функции DriverEntry
DeviceExtensionSize	(ULONG) Размер в байтах структуры расширения устройства, используемой минидрайвером
DevicesArePolled	(BOOLEAN) TRUE, если устройства минидрайвера должны опрашиваться для получения отчетов, FALSE, если устройства спонтанно отправляют отчеты при наличии доступных данных
Единственным полем, смысл которого нельзя назвать абсолютно очевидным, является флаг DevicesArePolled. Большинство устройств спонтанно генерируют отчет при действиях конечного пользователя, с оповещением хоста посредством некоего рода прерывания. Для таких устройств флаг DevicesArePolled задается равным FALSE. Далее HIDCLASS пытается поддерживать два активных IRP двойной буферизации для чтения отчетов. Предполагается, что ваш минидрайвер будет ставить IRP в очередь и завершать их в нужном порядке при выдаче прерываний устройством.
Некоторые устройства не поддерживают спонтанную выдачу отчетов. Для таких устройств флаг DevicesArePolled задается равным TRUE. Далее HIDCLASS в цикле выдает TRP на чтение отчетов. Ваш минидрайвер читает данные от устройства только в ответ на очередной IRP. Компоненты высокого уровня (например, приложение, использующее интерфейсы DirectX) могут задавать интервал опроса. Прежде чем задавать флаг DevicesArePolled равным TRUE, стоит дважды подумать: для большинства устройств он должен содержать FALSE.
А вот почти законченный пример функции DriverEntry для минидрайвера HIDCLASS:
extern "С" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
DriverObject->DriverExtension->AddDevice = AddDevice: DriverObject->Driver(Jnload = DriverUnload;
DriverObject->MajorFunction[IRP_MJ_INTERNAL_DEVICE_CONTROL] =
Минидрайверы HIDCLASS
595
DispatchlnternalControl;
DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
Dri verObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;
HID_MINIDRIVER-REGISTRATION reg;
RtlZeroMemoryt&reg, sizeof(reg));
reg.Revision = HID-REVISION;
reg.DriverObject = DriverObject:
reg.RegistryPath = RegistryPath;
reg.DeviceExtensionSize = sizeof(DEVICE-EXTENSION);
reg.DevicesArePolled = FALSE; // <== зависит от оборудования
return HidRegisterMinidriver(&reg):
}
Функции обратного вызова в драйверах
Большинство интерфейсов драйверов классов/минидрайверов в Windows ХР содержит набор функций обратного вызова, указываемых минидрайвером при регистрационном вызове из DriverEntry. Многие драйверы классов полностью берут на себя управление DRIVERJDBJECT при обработке регистрационных вызовов. Это означает, что драйверы классов устанавливают собственные AddDevice и DriverUnload, а также собственные диспетчерские функции IRP. Затем драйверы классов вызывают функции обратного вызова минидрайверов для выполнения специфических действий.
HIDCLASS также работает по этой схеме, но с небольшими отклонениями. При вызове HidRegisterMinidriver HIDCLASS устанавливает собственные указатели на функции в вашем объекте DRIVER_OBJECT, как это делают большинство драйверов классов. Вместо использования функций обратного вызова, адреса которых предоставляются минидрайвером в структуре HID„MINIDRIVER„REGISTRATION, он запоминает указатели AddDevice и DriverUnload, а также адреса диспетчерских функций для запросов PNP, POWER и INTERNAL_DEVICE_CONTROL. Тем не менее, эти функции минидрайверов не полностью эквивалентны одноименным функциям в обычных драйверах WDM. В этом разделе я объясню, как пишутся такие функции обратного вызова.
Функция обратного вызова AddDevice
Прототип функции обратного вызова AddDevice в минидрайвере HIDCLASS сходен с прототипом обычной функции AddDevice:
NTSTATUS AddDeviсе(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT fdo);
Между функцией обратного вызова минидрайвера и обычной функцией существуют два важных различия:
О аргумент объекта устройства относится к объекту функционального устройства (FDO), уже созданному HIDCLASS;
О в системах, предшествующих Windows ХР, HIDCLASS игнорирует значение, возвращаемое функцией обратного вызова минидрайвера.
596
Глава 13. Устройства взаимодействия с пользователем
Поскольку HIDCLASS создает FDO перед вызовом функции обратного вызова AddDevice минидрайвера, вам не нужно вызывать loCreateDevice и вообще делать практически что-либо из того, что обычно делается в функции AddDevice модели WDM. Вместо этого достаточно инициализировать структуру расширения устройства и вернуть управление. Версии Windows, предшествующие Windows ХР, игнорируют возвращаемое значение этой функции. Соответственно, если в функции обратного вызова AddDevice произойдет ошибка, необходимо установить в расширении устройства флаг, проверяемый при выполнении StartDevice:
typedef struct _DEVICE_EXTENSION {
NTSTATUS AddDeviceStatus:
} DEVICE-EXTENSION, *PDEVICE_EXTENSION:
Также следует учитывать, что указатель FDO DeviceExtension содержит адрес структуры расширения, приватной для HIDCLASS. Первые несколько полей этой приватной структуры отображаются на структуру HID_DEVICE_EXTENSION в DDK:
typedef struct _HID-DEVICE_EXTENSION {
PDEVICE_OBJECT Physlcal Devi ceObject;
PDEVICE-OBJECT NextDeviceObject;
PVOID Mini DeviceExtension:
} HID_DEVICE_EXTENSION, *PHID J)EVICE_EXTENSION:
Поиск расширения устройства производится по следующей цепочке указателей:
PDEVICE-EXTENSION pdx = (PDEVICE_EXTENSION) ((PHID_DEVICE_EXTENSION)
(fdo->Devi ceExtensi on))->M1n iDeviceExtension:
Аналогичные конструкции используются для получения адреса PDO и того, что я называю в книге объектом устройства нижнего уровня (LowerDeviceObject); в HIDCLASS используется термин «следующий объект устройства» (NextDevice-Object). Я не люблю вводить лишние символы и поэтому обычно определяю макросы, упрощающие мою жизнь при написании минидрайвера:
#define PDX(fdo) ((PDEVICE_EXTENSION) ((PHIDJ3EVICEJXTENSION) \
С(fdo)->DeviceExtensi on))->M1ni Devi ceExtens1 on)
#define PDO(fdo) (((PHID_DEVICE_EXTENSION) ((fdo)->DeviceExtension)) \
->Physi cal Devi ceObject)
tfdefine LDO(fdo) (((PHID_DEVICE_EXTENSION) ((fdo)^DeviceExtension)) \ ->NextDeviceObject)
С использованием этих макросов и предыдущего фрагмента DEVICE-EXTENSION функция обратного вызова AddDevice вашего минидрайвера может выглядеть так:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject. PDEVICE_OBJECT fdo)
{
PDEVICE-EXTENSION pdx = PDX(fdo);
NTSTATUS status = STATUS-SUCCESS;
<код инициализации полей DEVICE_EXTENSION>
pdx->AddDeviceStatus = status: // <== то. что осталось
return status; // если система >= ХР
Минидрайверы HIDCLASS
597
Зачем мы возвращаем реальный код состояния из AddDevice? В Windows ХР и последующих системах в случае неудачи HIDCLASS завершит неудачей свой вызов AddDevice, и это ускорит инициализацию устройства. Но поскольку в более ранних версиях операционной системы HIDCLASS игнорирует возвращаемое значение, вам придется предусмотреть механизм возврата кода ошибки из функции StartDevice.
Функция обратного вызова DriverUnload
HIDCLASS вызывает функцию DriverUnload вашего минидрайвера из своей функции DriverUnload. Если в функции DriverEntry создавались какие-либо глобальные объекты, их необходимо уничтожить в функции обратного вызова DriverUnload.
Функция обратного вызова DispatchPnp
Функция обратного вызова DispatchPnp задается так, как если бы она была диспетчерской функцией для запросов IRPJMJ_PNP, то есть заданием элемента массива в таблице MajorFunction объекта драйвера. HIDCLASS вызывает функцию обратного вызова при обработке Plug and Play IRP различных типов. Ваша функция обратного вызова может выполнять большинство операций, выполняемых функциональным драйвером для этого типа IRP (за подробностями обращайтесь к главе 6). Существуют два исключения:
О обработчик IRP_MN_START„DEVICE должен проверить флаг ошибки, устанавливаемый вашей функцией обратного вызова AddDevice (ранее я назвал его Add Devicestatus), и отклонить IRP, если флаг свидетельствует об ошибке. Так решается проблема с игнорированием возвращаемого значения AddDevice в версиях Windows, предшествующих Windows ХР;
О ваш обработчик IRP_MN„REMOVE_DEVICE не вызывает loDetachDevice или loDeleteDevice. Вместо этого он просто освобождает любые ресурсы, выделенные функцией обратного вызова AddDevice. HIDCLASS сам позаботится об отсоединении и удалении FDO.
В примере HIDFAKE используется библиотека GENERIC.SYS, поэтому его функция DispatchPnp выглядит так:
NTSTATUS D1spatchPnp(PDEVICE_OBJECT fdo. PIRP Irp)
{
return GenericDispatchPnp(PDX(fdo)->pgx, Irp);
}
Если не считать использования макроса PDX для получения структуры расширения устройства, этот код почти совпадает с кодом обычного функционального драйвера, использующего GENERIC.SYS. Впрочем, функции RemoveDevice, StartDevice и StopDevice отличаются от обычных:
VOID RemoveDevice(PDEVICE_OBJECT fdo)
{
// 1
}
598
Глава 13. Устройства взаимодействия с пользователем
NTSTATUS StartDev1ce(PDEVICE_OBJECT fdo.
PCM_PARTIAL_RESOURCE_LIST raw.
PCMJARTI AL_RESOURCE_L I ST trans 1 ated)
{
PDEVICE_EXTENSION pdx = PDX(fdo);
if (!NT_SUCCESS(pdx->AddDeviceStatus))
return pdx->AddDev1ceStatus;
// 2 return STATUS_SUCCESS;
}
VOID StopDevice(PDEVICE_OBJECT fdo, BOOLEAN oktouch)
{
// 3
}
Пример HIDFAKE не содержит кода в помеченных точках. Если вы используете его в качестве шаблона для построения собственного минидрайвера, напишите код, который будет делать следующее:
1.	Освобождать любые ресурсы (память, резервные списки и т. д.), выделенные в AddDevice. В примере HIDFAKE таких ресурсов нет.
2.	Настраивать конфигурацию устройства. В примере HIDFAKE реального оборудования нет, поэтому и делать на этом шаге нечего.
3.	Деинициализировать устройство посредством отмены действий, выполненных во время StartDevice. Поскольку HIDFAKE ничего не делает в StartDevice, то и здесь ничего делать не нужно.
Функция обратного вызова DispatchPower
Функция обратного вызова DispatchPower задается так, как если бы она была диспетчерской функцией для запросов IRP_MJ„POWER, то есть заданием элемента массива в таблице MajorFunction объекта драйвера. HIDCLASS вызывает функцию обратного вызова при обработке IRP управления питанием различных типов. В большинстве случаев функция обратного вызова просто передает IRP вниз следующему драйверу, не выполняя никаких действий, потому что HIDCLASS содержит всю поддержку управления питанием, необходимую для типичных устройств (включая поддержку WAIT_WAKE).
Если при вызове HidRegisterMinidriver задать флаг DevicesArePolJed равным FALSE, то HIDCLASS отменит свои IRP двойной буферизации перед перенаправлением запроса управления питанием на снижение энергопотребления. Если вы просто использовали эти IRP для отправки запросов вниз по стеку РпР, вам не придется беспокоиться об их отмене. Если же указатели на эти IRP были где-то сохранены, предоставьте функцию отмены.
ПРИМЕЧАНИЕ------------------------------------------------------------------------
Если ваш минидрайвер использует GENERIC.SYS, рассмотрите возможность использования функций GenericCacheControlRequest и GenericUncacheControlRequest для отслеживания приостанавливаемых IRP. Эти функции включают логику отмены, безопасную в отношении потенциальных «гонок».
Минидрайверы HIDCLASS
599
Пример функции обратного вызова DispatchPower в минидрайвере HIDCLASS:
NTSTATUS DispatchPower(PDEVICE_OBJECT fdo. PIRP Irp)
{
PDEVICE_EXTENSION pdx = PDX(fdo);
PIO_STACK-LOCATION stack = loGetCurrentlrpStackLocatlon(Irp); loCopyCurrentlrpStackLocatlonToNext(Irp);
If (stack->M1norFunction == IRP_MN_SET_POWER	// 1
&& stack->Parameters.Power.Type == DevicePowerState)
{
DEVICE_POWER_STATE newstate -
stack->Parameters.Power.State.Devicestate;
If (newstate == PowerDevIceDO)
IoSetCornplet1onRout1ne(Irp, (PIO_COMPLETION_ROUTINE)	// 2
PowerUpCompletlonRoutlne, (PVOID) pdx, TRUE, TRUE, TRUE);
}
else If (pdx->devpower == PowerDevIceDO)
// TODO Сохранить информацию контекста, если она есть	// 3
pdx->devpower = newstate;
}
return PoCal1DrtverCLDO(fdo). Irp);	// 4
NTSTATUS PowerUpCompletionRoutine(PDEVICE_OBJECT fdo, PIRP Irp,
PDEVICEJXTENSION pdx)
{
// TODO Восстановить контекст устройства без блокировки потока // 5 pdx->devpower = PowerDevIceDO;
return STATUS_SUCCESS;
1.	IRP управления питанием не нуждаются в специальной обработке, за исключением вызова SET_POWER для состояния энергопотребления устройства.
2.	При восстановлении питания функция завершения устанавливается перед отправкой IRP вниз по стеку.
3.	При отключении питания перед отправкой IRP сохраняется вся контекстная информация. Поскольку HIDCLASS может снижать энергопотребление поэтапно (например, сначала на уровень D2, а затем на уровень D3), нам также необходимо отслеживать текущее состояние энергопотребления устройства. Независимо от того, есть ли у устройства контекстная информация для сохранения или нет, в этот момент также отменяются все вторичные IRP, выданные вашим драйвером, завершаются программные потоки опроса и т. д. HIDCLASS будет вызывать ваш код на уровне PASSIVE_LEVEL в контексте системного потока, который может блокироваться на время выполнения этих операций.
600
Глава 13. Устройства взаимодействия с пользователем
4.	Как обычно, мы вызываем PoCallDriver для перенаправления IRP. Вызывать PoStartNextPowerlrp не нужно, потому что это уже сделал HIDCLASS.
5.	Функция завершения вызывается только после того, как драйвер завершит операцию SetDO. Питание устройства восстановлено, и теперь можно выполнить в обратном порядке те действия, которые выполнялись при отключении питания. Но поскольку выполнение потенциально ведется на уровне DISPATCH^ LEVEL в контексте произвольного потока, эти операции необходимо выполнять без блокировки текущего потока.
Функция обратного вызова DispatchlnternalControl
Функция обратного вызова DispatchlnternalControl задается так, как если бы она была диспетчерской функцией для запросов IRP_MJ_INTERNAL_DEVICE_CONTROL, то есть заданием элемента массива в таблице MajorFunction объекта драйвера. HIDCLASS вызывает функцию обратного вызова для получения отчетов и другой информации или для передачи команд минидрайверу. Вы можете запрограммировать ее так, как если бы она была обычной диспетчерской функцией IRP, обрабатывающей управляющие коды, перечисленные в табл. 13.3.
Таблица 13.3. Внутренние коды управляющих операций в минидрайверах HIDCLASS
Внутренний код	Описание
IOCTL_GET_PHYSICAL_DESCRIPTOR	Получает физический дескриптор стандарта USB
IOCTL_HID_GET_DEVICE_ATTRIBUTES	Возвращает информацию об устройстве так, как если бы оно было устройством USB
IOCTL_HID_GET_DEVICE_DESCRIPTOR	Возвращает дескриптор HID стандарта USB
IOCTL„HID_GET_FEATURE	Читает отчет возможности
IOCTL_HID_GET_INDEXED_STRING	Возвращает строковый дескриптор стандарта USB
IOCTL_HID„GET_STRING	Возвращает строковый дескриптор стандарта USB
IOCTL_HID_GET_REPORT_DESCRIPTOR	Возвращает дескриптор отчета стандарта USB
IOCTL_HID_READ_REPORT	Читает отчет, соответствующий дескриптору отчета
IOCTL_HID_SEND_IDLE_NOTIFICATION	Переводит устройство в режим бездействия (поддерживается в Windows ХР)
IOCTL_HID_SET_FEATURE	Записывает отчет возможности
IOCTL_HID__WRITE_REPORT	Записывает отчет
ПРИМЕЧАНИЕ---------------------------------------------------------------------------
Список управляющих операций минидрайвера в DDK отличается от представленного. Я основывался на конкретной версии исходного кода HIDCLASS. Возможно, документация DDK была основана на более ранних версиях или на некоторой запланированной поддержке, которая так и не была реализована.
Управляющие операции подробно рассматриваются в следующем разделе настоящей главы. Впрочем, между ними существует определенное сходство:
О В общем случае HIDCLASS может вызвать вашу функцию обратного вызова DispatchlnternalControl на любом уровне IRQL, меньшем либо равном DISPATCH_
Минидрайверы HIDCLASS
601
LEVEL, и в контексте произвольного потока. Из этого следует, что функция обратного вызова, а также все используемые ею объекты данных должны находиться в неперемещаемой памяти. Более того, вызывающий поток нельзя блокировать. Если вы создаете вторичные IRP для взаимодействия с оборудованием, они должны быть асинхронными. Наконец, любой драйвер, которому вы отправляете IRP прямо из функции обратного вызова, должен нормально перенести получение IRP на уровне DISPATCH_LEVEL. Кстати говоря, стандартный драйвер SERIAL.SYS допускает получение IRP на уровне DISPATCH_LEVEL, а драйвер шины USB также разрешает отправлять блоки запросов USB (URB) на уровне DISPATCH_LEVEL.
О В большинстве управляющих операций используется метод METHOD_NEITHER, то есть входные и выходные буферы данных находятся в полях стека Рага-meters.DeviceIoContro!.Type3InputBuffer и IRP UserBuffer соответственно.
О Управляющие операции в значительной степени ориентированы на спецификацию USB для устройств HID. Если вы пишете минидрайвер HIDCLASS, скорее всего, вы используете либо нестандартное устройство USB, либо вообще какое-нибудь другое устройство и вам каким-то образом нужно ввести устройство в модель USB. В частности, придется изобрести фиктивные идентификаторы производителя и продукта, фиктивные строковые дескрипторы и т. д.
О IRP, получаемые в этой функции обратного вызова, содержат достаточное количество позиций IO_STACK_LOCATION для передачи IRP вниз по стеку РпР вашему драйверу шины. Этот факт позволяет задействовать управляющие IRP для выполнения функций, специфических для данного устройства и требующих IRP.
Приведу заготовку для этой функции обратного вызова в минидрайвере:
#pragma LOCKEDCODE
NTSTATUS Dispatchinternal Control(PDEVICE__OBJECT fdo, PIRP Irp)
{
PDEVICEJXTENSION pdx = PDX(fdo);
NTSTATUS status - STATUS-SUCCESS;
ULONG info = 0:
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocatlon(Irp);
ULONG cbln =
stack->Parameters.DeviceloControl.InputBufferLength;
ULONG cbout =
stack->Parameters.DeviceloControl.OutputBufferLength:
ULONG code =
stack->Parameters.DeviceloControl.loControlCode;
PVOID buffer = Irp->UserBuffer;
switch (code)
{
case IOCTL_HID_XXX:
break;
602
Глава 13. Устройства взаимодействия с пользователем
default:
status = STATUS_NOT_SUPPORTED: break;
if (status ! = STATUS-PENDING)
CompleteRequestCIrp, status, info); return status;
}
Внутренний интерфейс IOCTL
Основной интерфейс между HIDCLASS и минидрайвером базируется на функции обратного вызова DispatchlnternalControl, представленной в конце предыдущего раздела. В этом разделе я расскажу, как выполнять каждую из управляющих операций в том порядке, в котором HIDCLASS их обычно предоставляет. Учтите, что HIDCLASS вызывает эту функцию обратного вызова только после того, как минидрайвер успешно завершит запрос IRP_MN„START_DEVICE.
IOCTL_HID_GET_DEVICE_ATTRIBUTES
HIDCLASS отправляет запрос IOCTL_HID_GET_DEVICE_ATTRIBUTES в процессе обработки запроса IRP_MN_START_DEVICE, а также в других ситуациях для получения информации, хранящейся в дескрипторе устройства USB. Поле IRP UserBuffer ссылается на экземпляр следующей структуры, которую вы должны заполнить:
typedef struct _HID DEVICE-ATTRIBUTES {
ULONG Size;
USHORT VendorID:
USHORT ProductID;
USHORT VersionNumber;
USHORT Reserved[llJ;
} HID-DEVICE-ATTRIBUTES, * PHID_DEVICE_ATTRIBUTES;
Например, структуру можно заполнить в контексте приведенной ранее заготовки DispatchlnternalControl:
case IOCTL_HID_GET_DEVICE_ATTRIBUTES;
{
if (cbout < sizeof(HID_DEVICE_ATTRIBUTES))
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}
#define p ((PHID_DEVICE_ATTRIBUTES) buffer)
Rt1 Ze roMemo ry (p, s1zeo f(HID_DEVICE_ATTRIBUTES));
p->Size = sizeof(HID_DEVICE_ATTRIBUTES);
p->VendorID = 0;
p->ProductID = 0;
p->VersionNumber = 1;
#undef p
Минидрайверы HIDCLASS
603
info = sizeof(HID_DEVICE_ATTRlBUTES):
break;
}
Для нестандартных устройств USB вполне очевидно, какие значения следует передать для полей VendorlD, ProductID и VersionNumber — это содержимое полей idVendor, idProduct и bcdDevice реального дескриптора устройства. Если же устройство не является устройством USB, придется определять фиктивные значения. В этом фрагменте я задал полям значения 0, 0 и 1 соответственно, они подойдут для любого типа устройств HID, кроме джойстиков. Для джойстика необходимо выбрать уникальные значения, совпадающие с содержимым ОЕМ-подраздела раздела, создаваемого для устройства.
ОТКРЫТИЕ КОЛЛЕКЦИЙ HID В ПОЛЬЗОВАТЕЛЬСКОМ РЕЖИМЕ---------------------------:-------------------
Открыть манипулятор коллекции HID в пользовательском режиме совсем несложно, если вы присвоите уникальные значения полям VendorlD и ProductID структуры HID_DEVICE_ATTRIBUTES. Допустим, вашей компании выделен идентификатор производителя USB 0x1234 и вы назначили своему устройству идентификатор продукта 0x5678. Эти значения будут использоваться в ответе на запрос IOCTL_HID_GET_DEVICE_ATTRIBUTES.
В приложениях MFC, использующих класс CDeviceList (см. главу 2), для открытия манипулятора одной из коллекций драйвера может использоваться код следующего вида (см. программу TEST из примера HIDFAKE):
HANDLE CtestDlg;;FlndFakeDevice()
{
GUID hidguid;
HidD-GetHldGuidt&hidguid);	// 1
CDeviceList devlist(hidguld);
int ndevices = devl 1st.InitializeO;
for (Int 1 =0; i < ndevices; ++i)	// 2
{
HANDLE h = CreateF11e(devlist.m_11st[i].niJinknarne, 0,	// 3
FILE_SHARE_READ	FILE_SHARE_WRIТЕ, NULL,
OPENJXISTING, 0, NULL);
if (h == INVALID-HANDLEJ/ALUE)
continue;
HIDD_ATTRIBUTES attr = {sizeof(HIDD_ATTRIBUTES)};
BOOLEAN okay = HidD_GetAttributes(h. Sattr);	// 4
CloseHandle(h);
if (!okay) continue:
if (attr.VendorlD != HIDFAKE_VID	// 5
attr.ProductID != HIDFAKE_PID) continue:
return CreateF11e(devlist.ni_listEi].ni_linknanie.	//6
GENERIC_READ GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, 0, NULL);
}
return INVALID_HANDLE_VALUE:
}
604
Глава 13. Устройства взаимодействия с пользователем
1.	HidD_GetHidGuid определяет код GLIID (глобально-уникальный идентификатор) для устройств HID.
2.	Здесь мы перечисляем все устройства HID. На практике в перечисление не включаются стандартные устройства (такие как мыши и клавиатуры).
3.	После открытия манипулятора этим способом (без указания прав и с разрешением полного общего доступа) мы можем использовать его для выдачи запросов. В отличие от драйверов устройств HIDCLASS обращает особое внимание на права доступа и атрибуты общего доступа, указанные с запросом IRP_MJ„CREATE; мы пользуемся преимуществами этого поведения, открывая манипулятор устройства, которое может быть недоступно для обычной операции открытия.
4.	HidD_GetAttributes возвращает структуру атрибутов, производную от структуры HID_DEVICE_ ATTRIBUTES, заполняемой минидрайвером.
5.	Это самая важная команда во всем примере. Если идентификаторы устройства и продукта совпадают с искомыми, мы прекращаем перебор и открываем манипулятор.
6.	Вызов CreateFile открывает монопольный неперекрывающийся манипулятор для чтения и записи. Это действие должно выполняться тестовым приложением HIDFAKE. Возможно, в вашем случае будут действовать другие требования для общего доступа, прав и перекрывающегося ввода/ вывода. Обратите внимание: вызов CreateFile может завершиться неудачей, даже если предыдущий вызов был успешным, если другое приложение вмешалось в процесс открытия манипулятора.
Логика приложения усложняется, если устройство имеет более одной коллекции верхнего уровня или если требуется обслуживать более одного экземпляра устройства.
Совершенно иной подход к получению входных данных от устройств HID основан на использовании оконных сообщений WM_INPUT и сопутствующих функций API пользовательского режима. Эта возможность появилась только в Windows ХР, и я еще не опробовал ее на практике. Может, в третьем издании книги...
Открыть манипулятор для коллекции мыши или клавиатуры невозможно, потому что системный поток ввода открывает эти устройства в монопольном режиме. Более того, эти устройства не отображаются при перечислении HID-интерфейсов (HIDCLASS не публикует коды GUID для HID-интер-фейсов клавиатуры и мыши, чтобы предотвратить возможность открытия этих устройств программами пользовательского режима до того, как это сделает системный поток ввода). Регистрировать приватный GUID интерфейса для устройства бесполезно, потому что HIDCLASS отклонит запрос IRP_MJ_CREATE, отправленный главному объекту устройства. Соответственно, не существует механизма взаимодействия с нестандартным драйвером мыши или клавиатуры с использованием стандартных методов.
IOCTL_HID_GET_DEVICE_DESCRIPTOR
HIDCLASS отправляет запрос ICXZTL_HID_GET_DEVICE_DESCRIPTOR в процессе обработки запроса IRP_MN_START_DEVICE, а также в других ситуациях для получения описания характеристик HID устройства. Поле IRP UserBuffer ссылается на экземпляр структуры дескриптора HID стандарта USB, которую вы должны заполнить:
typedef struct _HID_DESCRIPTOR {
UCHAR bLength;
UCHAR bDescriptorType:
USHORT bcdHID;
UCHAR bCountry;
UCHAR bNumDescriptors;
struct _HID_DESCRIPTOR_DESC_LIST {
UCHAR bReportType;
USHORT wReportLength: } DescriptorList [1];
} HID_DESCRIPTOR, * PHID_DESCRIPTOR;
Минидрайверы HIDCLASS
605
Несмотря на очевидно общий характер этой структуры, HIDCLASS в настоящее время резервирует память только для одного элемента массива DescriptorList, который должен быть дескриптором отчета. Тем не менее, разработчики Microsoft рекомендуют анализировать размер выходного буфера и организовать копирование дополнительных дескрипторов (например, физического).
Ваш код в минидрайвере должен заполнить структуру дескриптора так, как если бы устройство было устройством HID стандарта USB. Пример:
case IOCTL_HID_GET_DEVICE_DESCRIPTOR:
{
#def1ne р ((PHIDJDESCRIPTOR) buffer)
If (cbout < sizeof(HID_DESCRIPTOR))
{
status = STATUS JUFFER_TOO_SMALL;
break;
}
RtlZeroMemoryfp, sizeof(HIDJ3ESCRIPT0R));
p->bl_ength = sizeof(HID_DESCRIPTOR);
p->bDescr1ptorType = HID_HID__DESCRIPTOR_TYPE;
p->bcdHID = HID^REVISION;
p->bCountry = 0;
p->bNumDescr1ptors = 1;
p->DescriptorL1st[0].bReportType = HID_REPORT_DESCRIPTOR_TYPE;
p->Descr1ptorL1st[0].wReportLength = sizeof(ReportDescrlptor);
fundef p
Info = s1zeof(HID_DESCRIPT0R);
break;
}
Единственный аспект этого кода, не повторяющийся между разными драйверами, — это длина, указанная в поле wReportLength единственного элемента DescriptorList. Значение должно совпадать с длиной реального или фиктивного дескриптора отчета, передаваемого в ответ на запрос IOCTL_HID_GET_REPORT_ DESCRIPTOR.
ПРИМЕЧАНИЕ-----------------------------------------------------------------------------
Код bCountry в дескрипторе HID определяет язык, для которого локализуется устройство. В соответствии с разделом 6.2.1 спецификации HID это значение не является обязательным. Например, если вы имитируете клавиатуру с локализованной раскладкой, вы можете задать ненулевое значение этого поля.
IOCTL_HID_GET_RE PORT-DESCRIPTOR
HIDCLASS отправляет запрос IOCTL_HID_GET_REPORT_DESCRIPTOR в процессе обработки запроса IRP_MN_START_DEVICE, а также в других ситуациях для получения дескриптора отчета HID стандарта USB. Поле IRP UserBuffer ссылается на буфер размера, указанного вами в ответе на запрос IOCTL_HID_GET_DEVICE_ DESCRIPTOR.
606
Глава 13. Устройства взаимодействия с пользователем
Предположим, имеется статическая область данных ReportDescriptor, содержащая дескриптор отчета в стандартном формате. Обработка запроса может выполняться следующим образом:
case IOCTL_HID_GET_REPORT-DESCRIPTOR:
{
If (cbout < sizeof(ReportDescriptor))
status = STATUS JUFFER_TOO_SMALL; break;
}
RtlCopyMemory(buffer, ReportDescri ptor, sizeof(ReportDescriptor));
Info = sizeof(ReportDescri ptor); break;
Построение дескриптора отчета начинается с проектирования структуры отчета. Из спецификации USB для устройств HID создается впечатление, что вы более или менее свободны проектировать любой отчет по своему усмотрению, a Windows как-нибудь разберется, что делать с полученными данными. По собственному опыту могу сказать, что у вас нет такой свободы. Компоненты Windows, обрабатывающие ввод с клавиатуры и мыши, используются для получения отчетов, удовлетворяющих некоторым условиям. Игровые приложения также сильно различаются по своим возможностям декодирования отчетов, полученных от джойстика. Оказалось, что драйвер HIDPARSE, используемый HIDCLASS для разбора дескрипторов HID, весьма придирчив и не всегда принимает даже те дескрипторы, которые вроде бы отвечают спецификации. По этой причине при проектировании отчетов для распространенных видов устройств я рекомендую взять за образец существующее устройство Microsoft.
При сохранении работы в утилите HID Tool существует возможность создать заголовочный файл на языке С вроде следующего (соответствующего дескриптору, представленному на рис. 13.3):
char ReportDescriptor[64] = {			
0x05,	0x05,	//	USAGE-PAGE (Gaming Controls)
0x09.	0x03,	//	USAGE (Gun Device )
Oxal.	0x01,	//	COLLECTION (Application)
Oxal,	0x02,	//	COLLECTION (Logical)
0x85.	0x01,	//	REPORT_ID (1)
0x05,	0x09,	//	USAGE_PAGE (Button)
0x09,	0x01,	//	USAGE (Button 1)
0x15.	0x00,	//	LOGICAL-MINIMUM (0)
0x25,	0x01,	//	LOGICAL-MAXIMUM (1)
0x75,	0x01,	//	REPORT_SIZE (1)
0x95,	0x01,	//	REPORT_COUNT (1)
0x81,	0x02,	//	INPUT (Data,Var.Abs)
0x75,	0x07,	//	REPORT_SIZE (7)
Минидрайверы HIDCLASS
607
0x81, 0x03,	//	INPUT (Cnst.Var,Abs)
ОхсО,	//	END_COLLECTION
Oxal, 0x02,	//	COLLECTION (Logical)
0x85. 0x02,	//	REPORTJD (2)
0x05, 0x01,	//	USAGE_PAGE (Generic Desktop)
0x09, 0x30,	//	USAGE (X)
0x25, Oxff,	//	LOGICAL-MAXIMUM (-1)
0x75, 0x20,	//	REPORT_SIZE (32)
Oxbl, 0x02.	//	FEATURE (Data,Var,Abs)
ОхсО,	//	END-COLLECTION
Oxal, 0x02,	//	COLLECTION (Logical)
0x85, 0x03,	//	REPORTJD (3)
0x05, 0x09,	//	USAGE_PAGE (Button)
0x09, 0x01,	//	USAGE (Button 1)
0x25, 0x01,	//	LOGICAL-MAXIMUM (1)
0x75, 0x01,	//	REPORT_SIZE (1)
Oxbl, 0x02,	//	FEATURE (Data,Var,Abs)
0x75, 0x07,	//	REPORT_SIZE (7)
Oxbl, 0x03,	//	FEATURE (Cnst.Var,Abs)
ОхсО,	//	END-COLLECTION
ОхсО	//	END COLLECTION
Вы можете просто включить этот заголовочный файл в свой драйвер для определения структуры ReportDescriptor, возвращаемой из IOCTL_HID_GET_„REPORT_ DESCRIPTOR.
IOCTL_HID_READ_REPORT
IOCTL_HID_READ_REPORT - основная рабочая операция в минидрайверах HIDCLASS. HIDCLASS выдает этот запрос для получения низкоуровневых отчетов HID. Низкоуровневые отчеты используются для удовлетворения запросов IRP_ MJ.READ и IOCTL_HID_GEr_INPUT_REPORT, выданных компонентами высокого уровня, включая приложения пользовательского режима, вызывающие ReadFile, HidD_ GetlnputReport, IDirectInputDevice8::GetDeviceData и IDirectInputDevice8::PolL
У минидрайвера имеется несколько стратегий выдачи отчетов:
О Если устройство относится к типу устройств РЮ (Programmed I/O) и подключается к традиционной шине (например, PCI), вероятно, для создания данных структурированных отчетов можно воспользоваться вызовами функций HAL. После этого следует немедленно завершить запрос IOCTL_HID_READ_REPORT.
О Если устройство подключается к традиционной шине и использует аппаратные прерывания для оповещения хоста о доступности данных отчетов, необходимо реализовать схему выдачи отчетов в ответ на запросы. Атомарные операции со списками позволяют читать и сохранять данные отчетов в обработчиках прерываний (ISR). Другие схемы требуют, чтобы ваш ISR ставил в очередь отложенный вызов процедуры (DPC), который затем читает и сохраняет данные отчета.
608
Глава 13. Устройства взаимодействия с пользователем
О Если устройство является нестандартным устройством USB, вероятно, вы можете отправить один URB для получения данных, по которым строится структурированный отчет. URB можно совместить с запросом IOCTL_HID_READ„ REPORT, если размер низкоуровневого отчета вашего устройства не превышает размера отчета, ожидаемого HIDCLASS. Вероятно, в этом случае диспетчерская функция создаст URB в неперемещаемой памяти, установит функцию завершения и направит IRP вниз по стеку РпР драйверу шины USB. Ваша функция завершения освобождает URB, переформатирует данные отчета, задает поле loStatus.Information равным размеру переформатированного отчета и возвращает STATUS.SUCCESS, разрешая завершение IRP.
О Еще в каких-то ситуациях вам потребуется приостановить запрос IOCTL_HID__ READ„REPORT на время выполнения одной или нескольких операций ввода/ вывода для получения низкоуровневых данных от устройства, которые затем переформатируются к нужному формату отчета. В этой схеме приходится решать стандартные проблемы, связанные с сохранением указателя на запрос IOCTL_HID_READ_REPORT безопасным в отношении отмены, а также отменой вторичных IRP.
Какую бы схему вы ни выбрали, ваш драйвер реализует IRP, заполняя буфер UserBuffer данными отчета. Пример:
case IOCTL_HIDJEAD_REPORT:
{
If (cbout < <s1ze of report>)
{
status = STATUS_BUFFERJOO_SMALL:
break;
}
получение данных отчета>
RtlCopyMemoryCbuffer, <отчет>, <размер отчета>);
Info = <размер отчета>:
break;
}
Не забывайте, что если дескриптор содержит более одного отчета, то данные отчета, возвращаемые HIDCLASS, начинаются с 1-байтового идентификатора отчета.
IOCTL_HID__WRITE_REPORT
HIDCLASS выдает запрос IOCTL__HID_WRITE_REPORT для удовлетворения запросов IRP_MJ_WRITE и IOCTL_HID_SET_OUTPUT__REPORT, выдаваемых компонентами высокого уровня, включая приложения пользовательского режима, вызывающие WriteFile, HidD_SetOutputReport или IDirectlnputDeviceB: :SendDeviceData.
Выходные отчеты обычно используются для установки различных индикаторов — например, светодиодов и мини-дисплеев. Ваш код в минидрайвере должен передать данные выходного отчета устройству или каким-то образом имитировать работу устройства HID при получении такого отчета. Для получения выходных
Минидрайверы HIDCLASS
609
отчетов устройства USB реализуют команду Set_Report_Request, специфическую для класса (или определяют конечную точку выдачи прерываний), но в вашей архитектуре может потребоваться иной подход.
В отличие от других внутренних управляющих операций HIDCLASS запросы IOCTL_HID_WRITE„REPORT используют метод METHOD_BUFFERED. Это означает, что поле IRP Associatedlrp.SystemBuffer содержит адрес выходных данных, а поле Parameters.DeviceloControl.OutputBufferLength структуры IO__STACK_LOCATION содержит их длину.
IOCTL_HID_GET_FEATURE и IOCTL_HID_SET_FEATURE
HIDCLASS выдает запросы IOCTL_HID_GET_FEATURE и IOCTL_HID_SET_FEATURE для чтения или записи так называемых отчетов возможностей. Приложение инициирует эти запросы, вызывая HidD_GetFeature или HidD_SetFeature соответственно.
Отчеты возможностей могут быть встроены в дескриптор отчета. Согласно спецификации HID, отчеты возможностей предназначены для получения и задания конфигурационной информации, а не для опроса устройства на регулярной основе. Для стандартных устройств USB драйвер реализует эту функциональность с использованием команд Get_Report_Request и Set_Report_Request, специфических для конкретных классов. В минидрайверах других устройств HID необходимо реализовать некий аналог этих команд, если дескриптор отчета включает отчеты возможностей.
Управляющие операции ввода/вывода (IOCTL) также рекомендуются компанией Microsoft для выполнения «внеполосного» обмена данными между приложением и минидрайвером HID. Помните, что HIDCLASS не позволяет любому желающему открыть манипулятор самого устройства (манипуляторы открываются только для коллекций верхнего уровня) и отклоняет любые нестандартные управляющие операции. Если не прибегать к сомнительным методам, о которых я ничего говорить не буду, у приложения нет других механизмов для взаимодействия с минидрайверами HIDCLASS.
«Выходной» буфер для этого запроса представляет собой экземпляр следующей структуры:
typedef struct JIDJFER_PACKET {
PUCHAR reportBuffer;
ULONG reportBufferLen;
UCHAR reportId;
} HID_XFER_PACKET, *PHID_XFER_PACKET;
HIDCLASS использует одну структуру как для запросов GET„FEATURE, так и для запросов SET_FEATURE и в обоих случаях сохраняет указатель на нее в поле Irp->UserBuffer. В сущности, единственное различие между двумя запросами заключается в том, что длина структуры (константа) для SET_FEATURE находится в параметре InputBufferLength, а для GET_FEATURE — в параметре OutputBufferLength. (Впрочем, даже это различие для вас несущественно. Поскольку HIDCLASS является доверенной вызывающей стороной режима ядра, нет причин для проверки длины этой структуры.)
610
Глава 13. Устройства взаимодействия с пользователем
Ваша задача при обработке таких запросов — декодировать значение reportld, обозначающее один из отчетов возможностей, поддерживаемых вашим драйвером. Для запроса GET_FEATURE вы помещаете до reportBufferLen байтов данных в буфер reportBuffer и завершаете IRP, сохраняя в поле loStatus.Information количество скопированных байтов данных. Для запроса SETJEATURE следует извлечь reportBufferLen байтов данных из буфера reportBuffer.
Вот как выглядит заготовка кода для обработки этих двух запросов:
case IOCTL_HID_GET_FEATURE:
{
^define р ((PHIDJFER_PACKET) buffer)
switch (p->reportld)
case FEATURE_CODE_XX:
If (p->reportBufferLen < si zeof (FEATURE JEPORTJX))
{
status = STATUS JUFFER JOOJMALL;
break;
}
RtlCopyMeiTiory(p->reportBuffer, FeatureReportXx,
si zeof (FEATUREJEPOR TJX));
info = si zeof (FEATURE_REPORT JX); break;
}
break;
#undef p
}
case IOCTL_HID_SET_FEATURE:
{
#def1ne p ((PHID_XFER_PACKET) buffer)
switch (p->reportld)
{
case FEATUREJODEJY;
if (p->reportBufferLen > s1zeof(FEATURE_REP0RT_YY))
{
status = STATUS-INVALID-PARAMETER;
break;
}
RtICopyMemory (FeatureReportYy, p->reportBuffer, p->reportBufferLen):
break;
}
break;
#undef p
}
Минидрайверы HIDCLASS
611
ВНИМАНИЕ------------—-------------------------------------------------------------
Если драйвер поддерживает отчеты возможностей, обычно байты идентификации отчетов используются для различия отчетов возможностей, входных и выходных отчетов. В этом случае буфер reportBuffer начинается с однобайтового идентификатора, равного значению reportlf в структуре HID_XFER„PACKET, — об этом заботится HIDCLASS. Счетчик в поле reportBufferLen включает байт идентификатора. Но если идентификаторы не используются, поле reportld равно 0, reportBuffer не содержит места для байта-идентификатора, а счетчик reportBufferLen не включает байт идентификатора. Эта схема действует даже тогда, когда сторона, вызывающая HidD_GetFeature или HidD_SetFeature, передает буфер, включающий нулевой идентификационный байт. Иначе говоря, если идентификаторы используются, то данные отчета возможностей копируются с позиции reportBuffer+l, а если нет — с позиции reportBuffer.
В этих фрагментах FEATUREJZODEJKX и FEATURE_CODE_YY — заполнители для констант, которые вы определяете в соответствии с идентификаторами отчетов возможностей в схеме вашего устройства. FEATURE_REPORT_XX и FEATURE^ REPORT_YY — структуры, включающие байт идентификатора и фактические данные отчета, a FeatureReportXx и FeatureReportYy — экземпляры этих структур.
IOCTL_GET_PHYSICAL_DESCRIPTOR
HIDCLASS отправляет отчет IOCTL_GET_PHYSICAL_DESCRIPTOR, когда компонент более высокого уровня запрашивает физический дескриптор устройства. Физические дескрипторы предоставляют информацию о том, какой частью (или частями) тела активизируется один или несколько управляющих элементов устройства. Если вы имеете дело с. нестандартным устройством HID, для которого этот запрос актуален, вы должны обеспечить поддержку этого запроса, возвращая фиктивный дескриптор, соответствующий спецификации HID, раздел 6.2.3. Пример:
case IOCTL_GET_PHYSICAL_DESCRIPTOR:
{
if (cbout < sizeof(PhysicalDescriptor))
{
status = STATUS_BUFFER_TOO_SMALL;
break;
}
PUCHAR p =
(PUCHAR) MmGetSystemAddressForMdlSafe(Irp->MdlAddress);
if (Ip)
{
status = STATUS_INSUFFICIENT_RESOURCES; break;
}
RtlCopyMemory(p.
Physical Descriptor, sizeof (PhysicalDescriptor));
info = sizeof(PhysicalDescriptor); break;
}
Обратите внимание: в этом IOCTL используется метод METHOD_OUT_DIRECT вместо METHODJMEITHER.
612
Глава 13. Устройства взаимодействия с пользователем
Также обратите внимание на следующее утверждение в спецификации HID: «Физические дескрипторы не являются обязательными. Для большинства устройств они лишь увеличивают сложность, не давая почти ничего взамен».
IOCTL_HID_GET_STRING
HIDCLASS отправляет запрос IOCTL__HID__GET_STRING для получения строковых дескрипторов, описывающих изготовителя, продукт или серийный номер устройства. Приложения пользовательского режима инициируют его вызовом HidD_ GetManufacturerString, HidD_GetProductString или HidD_GetSerialNumberString. Эти необязательные строковые данные задаются в дескрипторе стандартного устройства USB. Параметр операции указывает, какую строку и для какого языка нужно вернуть.
Заготовка кода этой управляющей операции выглядит так:
case IOCTL_HID_GET_STRING:
{
#define р ((PUSHORT) \
stack ^Parameters. DeviceloControl.Type3InputBuffer)
USHORT 1 string = p[0];
LANGID langid = p[l];
#undef p
PWCHAR string = NULL;
switch (1 string)
{
case HID_STRING_ID_IMANUFACTURER: string = manufacturer name>; break;
case HID_STRING_ID_IPRODUCT: string = <product name>; break;
case HID_STRING_ID_ISER1ALNUMBER:
string = <ser1al number>; break;
}
if (!string)
{
status = STATUS_INVALID-PARAMETER; break;
}
ULONG 1 string = wcslenCstring);
If (cbout < Istrlng * slzeof(WCHAR))
{
status - STATUS_INVALID_BUFFER_SIZE; break;
}
RtlCopyMemoryfbuffer, string, Istrlng * slzeof(WCHAR));
Info = Istrlng * slzeof(WCHAR);
Минидрайверы HIDCLASS
613
if (cbout >= info + sizeof(WCHAR))
{
((PWCHAR) buffer)[1 string] = UNICODE_NULL;
Info += slzeof(WCHAR);
}
break;
}
Обратите внимание на следующие ключевые аспекты этого фрагмента:
О Как и в большинство других запросов IOCTL минидрайверов, в нем используется метод METHOD_NEITHER. В контексте функции обратного вызова DispatchlnternalControl, представленной ранее, переменная buffer соответствует заполняемому выходному буферу.
О Серийный номер, если он есть, должен быть уникальным для каждого устройства.
О Минидрайвер должен отклонить запрос с кодом STATUS_INVALID_PARAMETER, если индекс строки недействителен, или с кодом STATUSJNVALIDJ3UFFER SIZE, если буфер слишком мал для хранения всей строки.
О Минидрайвер возвращает всю строку или ничего. Если размер выходного буфера достаточно велик, к строке присоединяется завершающий нуль-символ. В DDK не сказано, что делать, если запрашиваемый язык не поддерживается устройством или минидрайвером. Я бы порекомендовал отклонить запрос с кодом STATUS_DEVICE_DATA_ERROR, как это должны делать настоящие устройства USB. Впрочем, если не поддерживается язык 0x0409 (американский диалект английского языка), лучше вернуть какую-нибудь строку по умолчанию (например, для первого языка в списке поддерживаемых), потому что HIDCLASS всегда использует 0x0409 в качестве параметра идентификатора языка в Windows ХР и более ранних версиях системы.
IOCTL_HID_GET_INDEXED__STRING
HIDCLASS отправляет запрос IOCTL_HID_GET_INDEXED_STRING для получения строки с заданными индексом и идентификатором языка в соответствии со стандартом USB. Программа пользовательского режима инициирует эти операции, вызывая HidD_GetIndexedString. Обработка почти не отличается от обработки IOCTL_HIDJ3ET_STRING, кроме двух аспектов:
О в этой управляющей операции используется любопытная смесь двух методов буферизации: входные данные с индексом строки и идентификатором языка хранятся в поле stack->Parameters.DeviceIoControl.Type3InputBuffer (как для запросов METHOD_NEITHER), а выходной буфер описывается списком дескрипторов памяти (MDL) Irp->MdlAddress, как для запросов METHOD_OUT_DIRECT;
О индекс строки в младших 16 битах Type3InputBuffer представляет собой индекс строки стандарта USB, а не константу вроде HID_STRING_ID_IMANUFACTURER, Эти запросы предназначены для того, чтобы приложение могло получать строковые значения, соответствующие строковым типам использования в отчетах HID.
614
Глава 13. Устройства взаимодействия с пользователем
Таким образом, устройство USB адресует до 255 строковых значений. Для нестандартных устройств USB и устройств, не являющихся устройствами USB, минидрайвер должен предоставить аналогичный механизм, если дескриптор отчета содержит строковые типы использования.
IOCTL__HID_SEND_IDLE_NOTIFICATION_REQUEST
HIDCLASS отправляет запрос IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST для отключения бездействующего устройства. У «настоящих» устройств USB он работает в сочетании с механизмом избирательной приостановки USB, рассматривавшимся в предыдущей главе.
Входной буфер этого запроса METHODJMEITHER содержит экземпляр следующей структуры:
typedef struct _HID_SUBMIT_IDLEJOTIFICATION_CALLBACK_INFO { HID_SEND_IDLE_CALLBACK IdleCalIback;
PVOID IdleContext:
} HID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO, *PHID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO:
где HID_SEND_IDLE_CALLBACK объявляется следующим образом:
typedef void (*HID_IDLE_CALLBACK)(PVOID Context):
Обратите внимание: эта структура по строению и смыслу идентична той, которая используется механизмом избирательной приостановки USB. Более того, если ваше устройство является устройством USB, вы можете просто переслать IRP вниз по стеку после изменения кода функции:
case IOCTL_HID_SEND_IDLE_NOTIFICATION_REQUEST:
{
loCopyCurrentIrpStackLocatlonToNext(Irp):
stack = loGetNextlrpStackLocatlon(Irp):
stack->Parameters.Dev1ceIoControl.loControlCode = IOCTL_INTERNAL_USB_SUBMIT_IDLE_NOTIFICATION;
return IoCallDr1ver(pdx->LowerDev1ce0bject. Irp);
}
Но если устройство не является устройством USB, следует немедленно вызвать функцию обратного вызова HIDCLASS и завершить IRP:
case IOCTLJID_SEND_IDLE_NOTIFICATION_REQUEST:
{
PHID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO р = (PHID_SUBMIT_IDLE_NOTIFICATION_CALLBACK_INFO) stack^Parameters.DeviceloControl.Type3InputBuffer:
(*p->IdleCall back)(p->IdleContext): break:
}
Обратный вызов сообщает HIDCLASS, что устройство можно немедленно отключить.
Проблемы совместимости с Windows 98/Ме
615
Проблемы совместимости с Windows 98/Ме
На архитектуру HID в системах Windows 98/Ме значительно повлияли исторические причины, то есть необходимость поддержки наследных методов работы с клавиатурами, мышами и джойстиками. По собственному опыту могу сказать, что каждая попытка портирования рабочего минидрайвера HID из Windows ХР открывала новые возможности для обучения и инженерного анализа. Я просто не знаю всего, что можно узнать в этой области, но опишу две ситуации, встретившиеся в моей работе, и свои решения возникших проблем.
Обработка IRP_MN_QUERY_ID
Если вы пишете минидрайвер для фиктивного оборудования (как это пытается делать HIDFAKE), необходимо предусмотреть специальную обработку для IRP_MN_QUERY_ID. Если этого не сделать, корневой перечислитель завершает этот IRP успешно, но вместо списка идентификаторов возвращает NULL. Далее это приводит к сбою администратора виртуальной машины (VMM). Вот как эта проблема решается в HIDFAKE:
NTSTATUS D1spatchPnp(PDEVICE_OBJECT fdo, PIRP Irp)
PDEVICE_EXTENSION pdx = PDX(fdo):
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
If (w1n98
&& stack->M1norFunct1on == IRP_MN__QUERY_ID
&& !NT-SUCCESS(Irp->IoStatus.Status)) {
PWCHAR idstrlng:
switch (stack->Parameters.QueryId.IdType)
case BusQuerylnstancelD: Idstrlng = L"0000"; break:
case BusQueryDevIcelD:
Idstrlng = L"ROOT\\*WCOOD01": break;
case BusQueryHardwarelDs: Idstrlng = L"*WCOODOr; break:
default:
return CompleteRequesttIrp):
616
Глава 13. Устройства взаимодействия с пользователем
ULONG nchars = wcslen(idstring);
ULONG size = (nchars + 2) * sizeof(WCHAR);
PWCHAR id = (PWCHAR) ExAllocatePool(PagedPool, size);
if (lid)
return CompleteRequestUrp, STATUS_INSUFFICIENT_RESOURCES);
wcscpy(id, idstring);
idEnchars + 1] = 0;
return CompleteRequestdrp, STATUS_SUCCESS, (ULONG JTR) id);
}
return GenericDispatchPnp(PDX(fdo)->pgx, Irp);
}
(Обратите внимание на использование двух перегруженных версий моей вспомогательной функции CompleteRequest.)
На самом деле специальная обработка BusQueryHardwarelDs необходима даже в Windows ХР, потому что HIDCLASS пропускает создание совместимых идентификаторов, если драйвер шины отклоняет запрос, а именно это происходит при использовании корневого перечислителя. Если не знать этого трюка, вам не удастся создать фиктивное устройство одного из стандартных классов.
Джойстики
В системе Windows существуют два разных пути интерпретации информации об осях и кнопках джойстика. Первый путь базируется на дескрипторе HID, а второй — на данных из реестра. Если эти два пути не обеспечивают логически согласованные результаты, вы получаете неработоспособный джойстик, который при каждой попытке чтения текущей позиции выдает ошибку. Мне не известны никакие решения этой проблемы, кроме метода проб и ошибок. Однажды мне пришлось решать эту задачу для клиента, тогда все кончилось написанием сложного имитатора HID, который быстро программировался для создания новых джойстиков со специализированными атрибутами. После нескольких попыток мы получили рабочее устройство — но я бы не решился повторить этот результат.
1 /I Специализированные
L^T темы
В первых восьми главах книги я описал многие аспекты полноценного драйвера WDM, относящиеся к любой разновидности устройств. Однако наряду с общими аспектами также необходимо знать некоторые специализированные методы, представленные в этой главе. В первом разделе я объясню, как организовать ведение журнала ошибок для последующего просмотра системным администратором. Также будут приведены инструкции относительно создания новых системных потоков, постановки в очередь рабочих элементов для выполнения в контексте существующих системных потоков, а также установки сторожевых таймеров для «зависающих» устройств.
Журналы ошибок
До настоящего момента при обсуждении обработки ошибок меня интересовало только выявление ошибок (и передача их кодов), а также различные меры в отладочных версиях драйверов, направленные на выявление проблем, которые могут интерпретироваться как ошибки. Впрочем, даже в окончательной версии драйвера могут встречаться достаточно серьезные ошибки, о которых стоит сообщить системному администратору. Например, драйвер дискового устройства может обнаружить, что физическая поверхность диска содержит неожиданно большое количество поврежденных секторов. А может быть, драйвер обнаруживает неожиданно частые ошибки данных или испытывает трудности с настройкой конфигурации или запуском устройства.
В подобных ситуациях драйвер может создать запись в системном журнале ошибок. Позднее созданная запись просматривается в приложении Event Viewer (одном из административных инструментов в системе Microsoft Windows ХР), и администратор узнает о возникшей проблеме. Окно Event Viewer показано на рис. 14.1. Также для обозначения непредвиденных ошибок можно воспользоваться установкой события WMI (Windows Management Instrumentation). Журналы событий рассматриваются в этом разделе, а интерфейсу WMI посвящена глава 10.
618
Глава 14. Специализированные темы
П Л	.......................	« Г X
i He Action J0ew Help
EventVtewer(LDcal) System 1 event(s)
Application
Security
^Information 8/6/20D2	6:45:44 AM EventLogS&rvke None 1 N/Д SCHERZO
Рис. 14.1. Окно Event Viewer в Windows XP
На рис. 14.2 изображены основные этапы процесса построения административного отчета по журналу ошибок. Драйвер использует функцию режима ядра loWriteErrorLogEntry для отправки специальной структуры данных, называемой пакетом регистрации ошибки, системной службе регистрации событий. Вместо текста сообщения пакет содержит числовой код. Когда появится такая возможность, служба регистрации событий записывает пакеты в файл журнала на диске. Позднее Event Viewer объединяет пакеты в файле журнала с текстом сообщения, взятым из коллекции файлов сообщений, для построения отчета. Файлы сообщений представляют собой обычные 32-разрядные DLL-библиотеки с текстовыми описаниями всех зарегистрированных сообщений на локальном языке.
Ваша задача как разработчика драйвера — создать подходящие пакеты регистрации ошибок при возникновении события, заслуживающего внимания. Вероятно, на практике вам также придется создать файл сообщений хотя бы на одном естественном языке. Оба аспекта регистрации ошибок описываются в двух следующих разделах.
Создание пакета регистрации ошибок
619
.Файл сообщений
Рис. 14.2. Процесс регистрации событий и построения сводных отчетов
Создание пакета регистрации ошибок
Чтобы зарегистрировать ошибку, драйвер создает структуру данных IO_ERROR_ LOG_PACKET и отправляет ее службе регистрации режима ядра. Пакет представляет собой структуру переменной длины (рис. 14.3) с заголовком фиксированного размера, содержащим общую информацию о регистрируемом событии. Поле ErrorCode определяет регистрируемое событие, оно связывается с файлом текстовых сообщений, о котором речь пойдет далее. За фиксированным заголовком следует массив двойных слов с именем DumpData, который содержит DumpDataSize байт данных, отображаемых Event Viewer в шестнадцатеричной записи при запросе подробной информации о событии. Размер задается в байтах, несмотря на то что массив объявлен как состоящий из 32-разрядных целых чисел. После DumpData пакет может содержать ноль или более строк Юникода, завершенных нуль-символами, которые Event Viewer в конечном итоге преобразует в отформатированный текст сообщений. Строковая область начинается со смещением StringOffset байтов от начала пакета и содержит Number-ofStrings строк.
Вам не придется заполнять никакие поля фиксированного заголовка, кроме упомянутых мной. Тем не менее, они могут содержать дополнительную информацию, которая может пригодиться при диагностике.
Поскольку пакет имеет переменную длину, прежде всего необходимо определить, сколько памяти потребуется для создаваемого пакета. Просуммируйте размер фиксированного заголовка, размер DumpData в байтах и количество байтов, занимаемых строками (вместе с завершающими нуль-символами). Например, следующий фрагмент кода из примера EVENTLOG (см. прилагаемые
620
Глава 14. Специализированные темы
материалы) выделяет пакет регистрации ошибок, размер которого достаточен для хранения 4 байт информации DumpData и одной строки:
VOID LogEvent(NTSTATUS code. PDEVICE_OBJECT fdo) {
PWSTR myname = L"EventLog";
ULONG packetlen = (wcslen(mynanie) + 1) * sizeof(WCHAR)
+ s1zeof(I0_ERR0R_L0G_PACKET) + 4:
if (packetlen > ERROR_LOG_MAXIMUM_SIZE) return:
PIO_ERROR_LOG_PACKET p = (PIO_ERROR_LOG_PACKET)
IoAllocateErrorLogEntry(fdo. (UCHAR) packetlen); if (!p) return;
}
0
4
8
C
10
14
18
1C
20
28
FunctionCode RetryCount	DumpDataSize
NumberOfStrings	StringOffset
Eventcategory	
ErrorCode	
UniqueErrorValue	
Finalstatus	
SequenceNumber	
loControlCode	
DeviceOffset	
DumpData[DumpDa ta Size]	
<string data>	
Рис. 14.3. Структура IO_ERROR_LOG_PACKET
Создание пакета регистрации ошибок
621
Неопытного программиста здесь поджидает одна ловушка: максимальная длина пакета регистрации ошибок составляет 152 байта, значение ERROR_LOG_MAXIMUM_ SIZE. Кроме того, аргумент размера loAllocateErrorLogEntry относится к типу UCHAR, длина которого составляет всего 8 бит. Представьте, что вы запрашиваете пакет длиной, скажем, 400 байт, но получаете блок размером всего 144 байта (400 = = 0x190; 144 = 0x90 — то, что остается после усечения до 8 битов).
Обратите внимание: первый аргумент loAllocateErrorLogEntry содержит адрес объекта устройства. Имя этого объекта устройства (если оно есть) будет отображаться в записях журнала вместо подстановочного элемента %1 (подробнее см. в следующем разделе).
Из этого фрагмента также видно, какие действия следует предпринять при возникновении проблем с регистрацией ошибки: никаких. Невозможность регистрации ошибки не считается ошибкой, так что вы не должны отклонять пакеты запросов ввода/вывода, инициировать фатальные сбои или делать что-либо другое, что может привести к завершению обработки. Обратите внимание: вспомогательная функция Log Event объявлена с типов VOID, потому что программист вообще не должен беспокоиться об успехе или неудаче при вызове этой функции и включать в свой код соответствующие проверки.
После того как пакет будет успешно создан, на следующем этапе следует инициализировать структуру и передать ее подсистеме регистрации. Пример:
memset(p, 0. sizeof(IO_ERROR_LOG_PACKET));
p->ErrorCode = code:
p->DumpDataS1ze = 4:
p->DumpData[O] =« <ч то у годно*'.
p->Stг1ngOffset я sizeof(IO_ERROR_LOG_PACKET) ’+ p->DumpDataS1ze;
p->NumberOfStrings = 1;
wcscpy((PWSTR) ((PUCHAR) p + p->StringOffset). myname);
loWrlteErrorLogEntry(p):
}
При регистрации ошибки устройства состав заполняемых полей в заголовке не ограничивается кодом ошибки. За информацией об этих дополнительных полях обращайтесь к описанию функции loAllocateErrorLogEntry в документации DDK.
Записи, содержащие информацию об ошибках, остаются в системной памяти до тех пор, пока система не сможет записать их на диск. Если до этого произойдет системный сбой, позднее вы не увидите этих записей в Event Viewer. Если при этом работает отладчик режима ядра или если в системе создается аварийный дамп, для просмотра содержимого очередей можно воспользоваться командой lerrorlog.
Создание файла сообщений
По содержимому поля ErrorCode в пакете регистрации ошибки Event Viewer ищет текст соответствующего сообщения в одном или нескольких файлах сообщений, связанных с драйвером. Файл сообщений представляет собой обычную
622
Глава 14. Специализированные темы
DLL-библиотеку с ресурсом, содержащим тексты на одном или нескольких естественных языках. Поскольку драйвер WDM использует тот же формат исполняемого файла, что и DLL-библиотеки, приватные сообщения могут храниться в самом файле драйвера. Здесь я лишь в общих чертах расскажу, как строятся файлы сообщений. Дополнительную информацию можно найти в MSDN и книге Джеймса Мюррея (James D. Murray) «Windows NT Event Logging» (O’Reilly & Associates, 1998) на страницах 125-157.
На рис. 14.4 изображен процесс включения текста сообщения в драйвер. Все начинается с создания исходного файла сообщений с расширением МС. Сценарий сборки преобразует текстовые сообщения при помощи компилятора сообщений (МС.ЕХЕ). Среди прочего, компилятор сообщений создает заголовочный файл с символическими константами, представляющими сообщения; этот файл включается в драйвер, а константы определяют значения ErrorCode для регистрируемых сообщений. Также компилятор сообщений генерирует промежуточные файлы с текстами сообщений на одном или нескольких естественных языках и файл ресурсного сценария (.RC), в котором перечислены эти промежуточные файлы. Сценарий сборки компилирует ресурсный файл и указывает откомпилированные ресурсы как входные данные для редактора связей. В конце сборки драйвер содержит ресурсы сообщений, необходимые для нормальной работы Event Viewer.
Рис. 14.4. Создание файла сообщений
Далее приводится пример простого исходного файла сообщений (этот код является частью программы EVENTLOG):
MessageldTypedef = NTSTATUS	// 1
Severn tyNames = (	//2
Success = 0x0:STATUS_SEVERITY_SUCCESS
Informat 1 oral = 0x1: STATUS SEVERITY JNFORMATIONAL
Создание пакета регистрации ошибок
623
Warning	= 0x2:STATUS_SEVERITYJjARNING
Error	= 0x3:STATUS_SEVERITY__ERR0R
)
FacilityNames	=	(	//	3
System	=	0x0
Eventlog	=	0x2A: FACILITY JVENTLOGJRROR_CODE
)
LanguageNames	=	(	//4
English	=	0x0409:msg00001
German	=	0x0407:msg00002
French	=	Ox040C:msg00003
Spanish	=	0x040A:msg00004
)
Messageld = 0x0001	// 5
Facility = Eventlog
Severity = Informational
SymbolicName = EVENTLOG_MSGJEST
Language = English	// 6
%2 said, "Hello, world!"
Language = German
%>2 hat gesagt, "Wir sind nicht mehr in Kansas!"
Language = French
12 a dit. "Mon chien a mange mon devoir!"
Language = Spanish
12 hablo, ।La lluvia en Espana permanece principalmente en el llano!
1.	Директива MessageTypedef позволяет задать символическое имя, которое будет использоваться в операторе преобразования типа в определении каждого идентификатора, сгенерированного для файла сообщений. Например, позднее мы определим сообщение с символическим именем EVENTLOG_MSG_TEST. Благодаря присутствию директивы MessageldTypedef в заголовочном файле, сгенерированном компилятором сообщений, это символическое имя будет определяться в виде ((NTSTATUS)0x602A0001L).
2.	Директива SeverityNames позволяет вам определить собственные имена для четырех возможных уровней критичности сообщения. Имена слева от знака равенства (Success, Informational и т. д.) отображаются в определениях сообщений в этом файле. Символическое имя после двоеточия определяется (в заголовочном выходном файле) равным числу перед двоеточием — например, #define STATUS_SEVERITY_SUCCESS 0x0.
624
Глава 14. Специализированные темы
3.	Директива FacilityNames позволяет определять собственные имена для кодов подсистемы, включаемых в определения идентификаторов сообщений. В данном примере мы заявляем, что в дальнейшем в директивах Facility будет использоваться имя EventLog. Благодаря третьей строке команды FacilityNames компилятор сообщений генерирует команду #define FACILITY_EVENTLOG„ERROR_ CODE 0х2А.
4.	Директива LanguageNames позволяет определять собственные имена для языков, на которых записываются сообщения. В данном случае директива указывает, что в файле имя English везде будет обозначать LANGID 0x0409 (стандартный английский в схеме обозначения языков Microsoft Windows NT). После двоеточия указывается имя промежуточного двоичного файла, в который заносятся откомпилированные сообщения для этого конкретного языка.
5.	Каждое определение сообщения состоит из заголовка, за которым следует текст сообщения на всех языках, поддерживаемых исходным файлом сообщения. В директиве Messageld указывается абсолютное число, как в данном примере, или смещение по отношению к последнему сообщению (например, Messageld = +1). В кодах подсистемы и критичности используются имена, определенные в начале исходного файла сообщений. Директива SymbolicName определяет символическое имя для сообщения. Компилятор сообщений определяет это символическое имя в сгенерированном им заголовочном файле.
6.	Для каждого языка, заданного в директиве LanguageNames, создается подобное определение текста сообщения. Оно начинается с директивы Language, в которой используется одно из определенных вами имен языков. Далее следует текст сообщения. Каждое определение текста сообщения завершается строкой, содержащий единственный символ «.» (точка).
В тексте сообщения служебные последовательности, состоящие из символа % и цифры, обозначают место для подстановки строки. Последовательность %1 обозначает имя объекта устройства, сгенерировавшего сообщение. Имя неявно передается при создании записи в журнале ошибок, задавать его напрямую не нужно. Последовательности %2, %3 и т. д. соответствуют первой, второй и т. д. строкам Юникода, присоединяемым к записи. В рассматриваемом примере последовательность %2 будет заменена текстом EventLog, потому что именно эта строка была включена в наш пакет ошибки.
Такой способ обозначения замены особенно полезен тем, что вы можете свободно переставлять строки в порядке, подходящем для каждого конкретного языка. Например, если на английском сообщение читается как «The %1 %2 fox jumped over the %3 dog», то на немецком оно может читаться как «Der %3 Hund wurde vom %1 %2 Fuchs bbergesprungen» (конечно, пример глупый — подставляемые строки будут отображаться на английском языке во всех версиях сообщения... Но, надеюсь, вы поняли, что я имел в виду.).
Event Viewer не сможет найти файл сообщений без дополнительных подсказок в виде записей в реестре. Раздел EventLog находится в разделе HKLM\System\ CurrentControlSet\Services реестра Windows NT. Каждый драйвер или компонент, регистрирующий события, создает в этом разделе собственный подраздел. В каждом
Системные потоки
625
подразделе находятся параметры EventMessageFile и TypesSupported. Параметр EventMessageFile относится к типу REG_SZ или REG_EXPAND_SZ, в нем перечисляются все файлы сообщений, которые могут потребоваться Event Viewer для работы с сообщениями, генерируемыми вашим драйвером. Такое значение представляет собой строку вида %SystemRoot%\System32\iologmsg.dll;%SystemRoot°/o\Systerri32\ Drivers\EventLog.sys. Кстати говоря, файл IOLOGMSG.DLL содержит текст всех стандартных кодов NTSTATUS.Н. Некоторые советы по поводу того, как автоматически задавать значения этих параметров при установке драйвера, содержатся в следующей врезке. Параметр TypesSupported относится к типу REG_DWORD и равен 7 — это означает, что драйвер может генерировать все возможные события, то есть ошибки, предупреждения и информационные сообщения (сама необходимость задавать этот параметр выглядит как некий пережиток прошлого).
О ФАЙЛАХ СООБЩЕНИЙ--------------------------------------------------------------------
В том, что касается ресурсов сообщений драйверов, существуют два практических аспекта, которые довольно трудно обнаружить самому: как заставить сценарий сборки компилировать ваши сообщения и как убедить системные средства установки оборудования включить необходимые данные в реестр, чтобы утилита Event Viewer нашла ваши сообщения.
Пример EVENTLOG, как и другие примеры программ в книге, базируется на файлах проектов Microsoft Visual C++ 6.0. Я изменил определение проекта, определил дополнительный этап сборки для EVENTLOG.RC и включил полученный RC-файл в сборку. Откройте настройки проекта, и вы поймете, о чем идет речь. Еще проще воспользоваться утилитой DDK BUILD, которая позволяет добавить МС-файл в число исходных файлов.
Позднее в книге (см. главу 15) мы обсудим общую тему использования INF-файлов для установки драйверов. Чтобы увидеть, как задается файл сообщений в INF-файлах, просмотрите файл DEVICE.INF в каталоге проекта EVENTLOG, а точнее, директиву AddService. Вы увидите, что директива AddService содержит ссылку на секцию EventLogLogging, которая, в свою очередь, при помощи директивы AddReg ссылается на секцию EventLogAddReg. Последняя секция добавляет параметры EventMessageFile и TypesSupported в специализированный подраздел службы регистрации событий.
Системные потоки
Во всех драйверах устройств, рассматривавшихся до настоящего момента в книге, нас не слишком беспокоило, в контексте какого потока будут выполняться подфункции драйвера. В большинстве случаев функции выполняются в контексте произвольного потока — это означает, что мы не можем блокировать поток и напрямую обращаться к виртуальной памяти пользовательского режима. Для некоторых устройств первое условие существенно затрудняет программирование драйверов.
Некоторые устройства лучше всего обслуживаются посредством опроса. Например, устройство, которое не может асинхронно прервать работу процессора, должно время от времени опрашиваться с целью проверки его состояния. В других случаях естественный путь программирования устройства основан на поэтапном выполнении действий, разделенных периодами ожидания. Например, драйвер флоппи-дисковода выполняет операцию за несколько стадий. Как правило, драйвер должен отдать команду дисководу на достижение рабочей скорости вращения, дождаться, пока нужная частота будет достигнута, инициировать пересылку
626
Глава 14. Специализированные темы
данных, немного выждать, а затем снова снизить частоту вращения. Можно спроектировать драйвер, работающий по принципу конечного автомата, чтобы нужная последовательность операций реализовывалась при помощи функций обратного вызова. Тем не менее, будет гораздо проще, если вы сможете расставить события и ожидания по таймеру в соответствующих точках прямолинейной программы.
Если устройство требует периодического опроса, задача легко решается при помощи системного потока, принадлежащего драйверу. Системный поток работает под эгидой процесса, принадлежащего операционной системе в целом. В данном случае я говорю исключительно о системных потоках, выполняемых исключительно в режиме ядра. Механизм создания и уничтожения собственных системных потоков описан в следующем разделе. Затем я приведу пример использования системного потока для управления устройством посредством опроса.
Создание и завершение системного потока
Системный поток запускается функцией PsCreateSystemThread. В одном из аргументов этой функции передается адрес потоковой процедуры, которая играет роль главной программы нового потока. Когда потоковая процедура готовится к завершению потока, она вызывает функцию PsTerminateThread, которая не возвращает управление. Вообще говоря, вы должны обеспечить механизм, посредством которого событие РпР могло бы приказать потоку завершиться, и дождаться завершения. Объединяя все сказанное, мы получаем следующие три функции:
typedef struct _DEVICEJXTENSION {
KEVENT evKill;	// 1
PKTHREAD thread: };
NTSTATUS StartThread(PDEVICEJXTENSION pdx)
NTSTATUS status:
HANDLE hthread:
OBJECT-ATTRIBUTES oa;	//2
Imt1al1ze0bjectAttributes(&oa, NULL, OBJ_KERNEL_HANDLE, NULL, NULL);
status = PsCreateSystemThreadC&hthread, THREAD_ALL_ACCESS, // 3
&oa, NULL, NULL, (PKSTART_ROUTINE) ThreadProc, pdx);
if (!NT_SUCCESS(status)) return status;
ObReferenceObjectByHandle(hthread, THREAD_ALL_ACCESS, NULL, // 4 KernelMode, (PVOID*) &pdx->thread, NULL);
ZwClose(hthread);	// 5
return STATUS-SUCCESS; }
VOID StopThread(PDEVICE_EXTENSION pdx)
Системные потоки
627
{
KeSetEvent(&pdx->evKi11, О, FALSE);	//	6
KeWaitForSingleObject(pdx->thread, Executive,	//	7
KernelMode. FALSE. NULL);
ObDereferenceObject(pdx->thread);	//	8
}
VOID ThreadProc(PDEVICE_EXTENSION pdx)
{
KeWaitForXxx(<we менее pdx->evKill>):	//9
PsTerm1nateSysteniThread(STATUS_SUCCESS):	// 10
}
1.	Объявите событие KEVENT в расширении устройства, чтобы событие РпР могло послать потоку сигнал о завершении. Событие инициализируется в функции AddDevice.
2.	Очень важно, чтобы при создании потока был указан аргумент OBJ_KERNEL-HANDLE. Если выполнение ведется в контексте пользовательского потока, то при нарушении этого условия в приложении ненадолго возникает возможность закрытия манипулятора из пользовательского режима.
3.	Эта команда запускает новый поток. Возвращаемое значение при успешном вызове представляет собой манипулятор потока, который хранится по адресу, определяемому первым аргументом. Второй аргумент задает права доступа для потока, в данном случае наиболее подходящим будет значение THREAD_ALL_ACCESS. Следующие три аргумента предназначены для потоков, которые являются частью процессов пользовательского режима, при вызове из драйверов WDM они содержат NULL. Предпоследний аргумент (ThreadProc) определяет главную программу потока. Последний артумент PsCreateSystem-Thread (pdx) содержит контекст — это единственный аргумент потоковой процедуры.
4.	Чтобы дождаться завершения потока, вам потребуется адрес базового объекта KTHREAD вместо манипулятора, полученного от PsCreateSystemThread. Для получения адреса используется вызов ObReferenceObjectByHandle.
5.	После получения адреса KTHREAD манипулятор становится ненужным, поэтому мы закрываем манипулятор вызовом ZwClose.
6.	Такая функция, как StopDevice (которая в моей схеме модуляризации драйвера выполняет часть обработки IRP_MN_STOP_DEVICE, специфическую для конкретного устройства), может вызвать StopThread для остановки системного потока. Первым шагом должна стать установка события evKill.
7.	Этот вызов показывает, как ожидать завершения потока. Объект потока режима ядра принадлежит к числу объектов синхронизации, по которым может выполняться ожидание. Переход в установленное состояние происходит при окончательном завершении потока. В Windows ХР всегда следует выполнять
628
Глава 14. Специализированные темы
это ожидание, чтобы избежать неприятной ситуации — выгрузки образа драйвера в то время, пока один из системных потоков выполняет последние команды своей процедуры завершения. Иначе говоря, не ограничивайтесь ожиданием специального «подтверждающего события», устанавливаемого потоком непосредственно перед выходом, — поток должен выполнить PsTerminateSystem-Thread до того, как ваш драйвер может быть безопасно выгружен. Также ознакомьтесь с важной информацией в разделе «Проблемы совместимости с Windows 98/Ме» (подраздел «Ожидание завершения системных потоков») в конце главы.
8.	Этот вызов ObDereferenceObject является парным по отношению к вызову ObReferenceObjectByHandle, сделанному при исходном создании потока. Мы должны разрешить Object Manager освободить память, используемую объектом KTHREAD, ранее описывавшим наш поток.
9.	Логика потоковой процедуры сильно зависит от того, какой именно цели вы пытаетесь достичь. Если блокировка осуществляется в ожидании некоторого внешнего события, вызовите KeWaitForMuftipleObjects и задайте событие evKHI в одном из объектов.
10.	Обнаружив установку события evKill, вы вызываете функцию PsTerminate-SystemThread, которая завершает поток. Соответственно, эта функция не возвращает управление. Обратите внимание: завершить системный поток можно только вызовом этой функции в контексте самого потока.
Опрос устройств в системном потоке
Если вы пишете драйвер для устройства, которое не может сгенерировать прерывание, чтобы запросить обслуживание со стороны системы, возможно, стоит воспользоваться опросом устройства в системном потоке. Я покажу один из способов использования системных потоков для этой цели. Данный пример написан для гипотетического устройства с двумя входными портами. Один порт является управляющим, при отсутствии данных из него читается байт 0, а при наличии данных — байт 1. Другой порт передает один байт данных и сбрасывает управляющий порт.
В рассматриваемом примере системный поток создается при обработке запроса IRP_MN_START_DEVICE. Поток завершается при получении запроса Plug and Play (IRP_MN_STOP_DEVICE или IRP_MN_REMOVE_DE\/ICE), который требует освобождения ресурсов ввода/вывода. Поток проводит большую часть своего времени в заблокированном состоянии. Когда функция Startlo приступает к обработке запроса IRP_MJ_READ, она устанавливает событие, которого ожидает поток опроса. Далее поток опроса входит в цикл обслуживания запроса. В цикле поток опроса сначала блокируется на фиксированный интервал. По истечении этого интервала поток читает данные из управляющего порта. Если управляющий порт содержит 1, поток читает байт данных. Цикл повторяется вплоть до удовлетворения запроса, после чего поток снова «засыпает» до получения следующего запроса функцией Startlo.
Системные потоки
629
Потоковая функция в примере POLLING выглядит так:
VOID PollingThreadRout1ne(PDEVICE_EXTENSION pdx) {
NTSTATUS status;
KHMER timer;	// 1
KeIn1t1al1zeT1merEx(&t1mer, SynchronlzatlonTlmer);
PVOID mainevents[] = {	//2
(PVOID) &pdx->evK111, (PVOID) &pdx->evRequest, };
PVOID polleventsE] = { (PVOID) &pdx->evK111, (PVOID) &t1mer,
C_ASSERT(arraysize(iria1nevents) <= THREAD_WAIT_OBJECTS);
C_ASSERT(arrays1ze(pollevents) <= THREAD_WAIT_OBJECTS);
BOOLEAN kill = FALSE;
while (’kill)	// 3
{	// пока не будет приказано завершиться
status = KeWa1tForMult1ple0bjects(arraysize(mainevents).	// 4
malnevents, WaitAny, Executive, KernelMode, FALSE, NULL, NULL);
if (!NT_SUCCESS(status) status == STATUS_WAIT_O) break;
ULONG numxfer = 0;
LARGEJNTEGER duetime = {0}; fdeflne POLLING-INTERVAL 500 KeSetT1merEx(&t1mer, duetime, POLLING-INTERVAL, NULL); // 5
PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);
while (TRUE)	// 6
{	// Прочитать следующий байт
If (Irp->Cancel)	11 7
status = STATUS-CANCELLED; break;
}
status = AreRequestsBe1ngAborted(&pdx->dqReadWr1te);
If (!status) break;
status = KeWa1tForMult1ple0bjects(arrays1ze(pollevents),	// 8
poll events, WaitAny, Executive, KernelMode, FALSE, NULL, NULL);
630
Глава 14. Специализированные темы
if (!NT_SUCCESS(status))
{
kill = TRUE;
break;
{
if (status == STATUS_WAIT_O)
{
status = STATUS_DELETE_PENDING;
kill = TRUE;
break;
}
if (pdx->nbytes)	//9
if (READ_PORT_UCHAR(pdx->portbase) == D
{
*pdx->buffer++ = READ_PORT_UCHAR(pdx->portbase + 1);
--pdx->nbytes:
++numxfer;
}
}
if (!pdx->nbytes)
break;
}	// read next byte
KeCancelTimer(&timer):
StartNextPacket(&pdx->dqReadWrite, pdx->DeviceObject);
if (Irp)
{
loReleaseRemoveLock(&pdx->RemoveLock, Irp);
CompleteRequestdrp, STATUS_SUCCESS, numxfer);
}
}	// until told to quit
PsTerminateSystemThread(STATUS_SUCCESS);
}
1.	Этот таймер ядра будет использоваться позднее для управления частотой опроса устройства. Таймер должен относиться к типу SynchronizationTimer, чтобы он автоматически сбрасывался при истечении интервала.
2.	В этой функции мы дважды вызываем KeWaitForMultipleObjects, чтобы блокировать поток опроса, пока не произойдет чего-либо, заслуживающего внимания. Два массива содержат адреса объектов синхронизации, по которым осуществляется ожидание. Директивы CLASSERT проверяют, что количество ожидаемых событий достаточно мало для использования стандартного массива блокировок, встроенного в объект потока.
3.	Цикл завершается при возникновении ошибки или при установке события evKill. В этом случае завершается весь поток опроса.
4.	Ожидание прекращается при установке evKill или evRequest. Наша функция Startlo устанавливает evRequest, чтобы указать на появление IRP для обработки.
Рабочие элементы
631
5.	Вызов KeSetTimerEx начинает отсчет по таймеру. Таймер срабатывает многократно: сначала по истечении заданного интервала, а затем периодически. Мы задаем задержку 0, в результате чего устройство будет опрошено немедленно. Интервал POLLINGJNTERVAL задается в миллисекундах.
6.	Внутренний цикл завершается либо при установке события evKill, либо при завершении обработки текущего IRP.
7.	Пока в этом цикле мы занимаемся своими делами, текущий IRP может быть отменен или мы можем получить запрос РпР или управления питанием, требующий отмены IRP.
8.	В этом вызове KeWaitForMultipleObjects используется тот факт, что таймер режима ядра ведет себя как объект события. Вызов завершается либо при установке evKill (это означает, что поток опроса необходимо завершить), либо при истечении таймера (означает, что мы должны выполнить следующий опрос).
9.	Здесь и происходит опрос устройства в нашем драйвере. Мы читаем управляющий порт по базовому адресу, полученному от PnP Manager. Если прочитанное значение указывает на наличие доступных данных, мы читаем их из порта данных.
Функция Startlo, работающая в этой функции опроса, сначала задает поля buffer и nbytes в расширении устройства — вы уже видели, как они используются функцией опроса при последовательной обработке входных запросов. Затем она устанавливает событие evRequest, чтобы «разбудить» поток опроса.
Возможны и другие способы организации драйверов опроса, кроме продемонстрированного. Например, можно создавать новый поток опроса каждый раз, когда поступающий запрос обнаруживает бездействие запроса. Поток обрабатывает запросы до тех пор, пока устройство не освободится, после чего завершается. Такая стратегия лучше продемонстрированной в тех ситуациях, когда краткие выбросы активности устройства перемежаются долгими периодами бездействия, потому что поток опроса не будет занимать виртуальную память в относительно продолжительных интервалах пассивности. Но если устройство загружается более или менее постоянно, первая стратегия подойдет лучше, потому что она помогает избежать лишних затрат на запуск и остановку потока опроса.
Драйвер с опросом также можно построить на базе защищенных очередей вместо объектов DEVQUEUE и функции Startlo, как это сделано в POLLING. В общем и целом эти решения равноценны.
Рабочие элементы
Время от времени требуется временно понизить уровень запросов прерываний процессора (IRQL) для какой-либо операции, выполняемой на уровне PASSIVE. LEVEL. Конечно, понижение уровня IRQL полностью исключается, но если выполнение ведется на уровне DISPATCH_LEVEL или ниже, вы можете поставить в очередь рабочий элемент (work item), который в будущем передаст управление функции обратного вызова вашего драйвера. Вызов производится на уровне
632
Глава 14. Специализированные темы
PASSIVE-LEVEL в контексте рабочего потока, принадлежащего операционной системе. Рабочие элементы избавят вас от хлопот с созданием собственных относительно редко активизируемых потоков.
ПРИМЕЧАНИЕ —---------------------------------------------------------------
Не стоит парализовывать системный рабочий поток, планируя рабочий элемент, выполнение которого займет много времени. Количество рабочих потоков не так уж велико, и если вы помешаете выполнению рабочих элементов других драйверов, система может «зависнуть».
Обычно в драйвере объявляется контекстная структура, при помощи которой вы сообщаете рабочему элементу, какую функцию он должен вызвать. Независимо от остального содержимого в нем должен присутствовать указатель на структуру IO-WORKITEM:
typedef struct JANDOMJUNK {
PIOJORKITEM Item:
} RANDOM JUNK, *PRANDOMJUNK;
Когда потребуется поставить рабочий элемент в очередь, создайте экземпляр контекстной структуры и IO_WORKITEM:
PRANDOMJUNK stuff = (PRANDOMJUNK) ExAllocatePool (NonPagedPool,
si zeof (RANDOMJUNK));
stuff->1tem = loAllocateWorkltem(fdo);
где fdo — адрес объекта DEVICE-OBJECT, как-то связанного с рабочим элементом.
Затем вы инициализируете структуру контекста и помещаете рабочий элемент в очередь системного рабочего потока вызовом следующей функции:
IoQueueWorkItem(stuff->1tem, (PIO_WORKITEM_ROUTINE) Callback.
Queueidentifier, stuff);
Параметр Queueidentifier имеет два допустимых значения:
О DelayedWorkQueue — означает, что рабочий элемент должен выполняться в контексте системного рабочего потока, обладающего переменным приоритетом (то есть не на уровне приоритета реального времени);
О CriticalWorkQueue — означает, что рабочий элемент должен выполняться в контексте системного рабочего потока с приоритетом реального времени.
Тип очереди выбирается в зависимости от срочности выполняемой задачи. Помещение элемента в критическую очередь дает ему приоритет перед всеми некритическими рабочими элементами в системе за счет потенциального снижения процессорного времени, доступного для выполнения других критических задач. В любом случае, операции, выполняемые в функции обратного вызова, всегда могут вытесняться операциями, выполняемыми на повышенном уровне IRQL.
После того как рабочий элемент будет поставлен в очередь, операционная система будет передавать управление функциям обратного вызова в контексте системного рабочего потока с характеристиками, заданными третьим аргументом
Сторожевые таймеры
633
loQueueWorkltem. Код будет выполняться на уровне IRQL PASSIVE-LEVEL. Содержимое функции обратного вызова остается более или менее на ваше усмотрение, за одним исключением: вы должны освободить или иным образом вернуть в систему память, занимаемую рабочим элементом. Вот как выглядит заготовка функции обратного вызова:
VOID Call back (PRANDOMJUNK stuff)
PAGED_CODE();
IoFreeWorkItem(stuff->1tem);
ExFreePool(Item);
Эта функция получает один аргумент (stuff) с контекстом, переданным ранее при вызове loQueueWorkltem. В этот фрагмент также включены вызовы ExFreePool и loFreeWorkltem, парные по отношению к операциям выделения памяти, выполненным ранее.
Между вызовами loQueueWorkltem и моментом возврата из функции обратного вызова I/O Manager владеет дополнительной ссылкой на объект устройства, указанный при исходном вызове loAllocateWorkltem. Дополнительная ссылка фиксирует драйвер в памяти по крайней мере до момента возврата из функции обратного вызова. Без этой защиты ничто не помешает драйверу поставить в очередь рабочий элемент, а затем выгрузиться до того, как функция обратного вызова завершит свое выполнение. Система попытается выполнить код по адресу, внезапно ставшему недействительным, и произойдет фатальный сбой. В своем драйвере вы никак не сможете предотвратить проблемы такого рода, потому что для выхода из вашего кода и передачи управления системе потребуется как минимум выполнить команду возврата.
В версиях Windows, предшествующих Windows 2000, для создания и постановки в очередь рабочих элементов существовали функция ExQueueWorkltem и макрос ExInitializeWorkltem. Сейчас они считаются нежелательными из-за проблем с выгрузкой драйвера. Более того, комплекс тестов WHQL (Windows Hardware Quality Lab) даже особо выявляет вызовы ExQueueWorkltem. Это обстоятельство становится препятствием на пути к созданию двоично-совместимых драйверов для всех платформ WDM (см. раздел «Проблемы совместимости с Windows 98/Ме» в конце главы).
ПРИМЕР---------------------------------------------------------------------------------
Пример WORKITEM в прилагаемых материалах демонстрирует механику использования функций loXxxWorkltem, описанных в тексте.
Сторожевые таймеры
Некоторые устройства не оповещают вас о возникших проблемах — они просто перестают реагировать на любые запросы. С каждым объектом устройства связывается объект IO_TIMER, при помощи которого можно избежать бесконечного
634
Глава 14. Специализированные темы
ожидания завершения операции. Пока таймер работает, I/O Manager один раз в секунду активизирует функцию обратного вызова таймера. В этой функции следует принять меры по завершению всех операций, которым следовало бы завершиться (но этого почему-то не произошло).
Объект таймера инициализируется при выполнении AddDevice:
NTSTATUS AddDevTсе(...)
{
IoInitializeT1mer(fdo, (PIO_TIMER_ROUTINE) OrTimer, pdx):
где fdo — адрес объекта устройства, OnTimer — функция обратного вызова таймера, a pdx — аргумент контекста для вызовов OnTimer со стороны I/O Manager.
Таймер запускается функцией loStartTimer и останавливается функцией loStop-Timer. Между этими двумя вызовами ваша функция OnTimer вызывается один раз в секунду.
Пример PIOFAKE в прилагаемых материалах демонстрирует один из вариантов использования сторожевого таймера IO_TIMER. Я включил переменную таймера в расширение устройства, а также определил флаг BOOLEAN, который указывает, занят ли драйвер в настоящее время обработкой IRP:
typedef struct _DEVICE_EXTENSION {
LONG timer:
BOOLEAN busy:
} DEVICEJXTENSION. *PDEVICE_EXTENSION;
При обработке IRP_MJ_CREATE после периода, в котором для устройства не существовало открытых манипуляторов, я запускаю таймер. При обработке запроса IRP_MJ_CLOSE, закрывающего последний манипулятор, таймер останавливается:
NTSTATUS DIspatchCreatet...)
if (Inter1ockedlncrement(&pdx->handles == 1)
{
pdx->timer = -1;
loStartTimer(fdo);
}
}
NTSTATUS DispatchClose(...)
{
if (InterlockedDecrement(&pdx->handles) == 0) loStopTimer(fdo);
Сторожевые таймеры
635
Жизненный цикл timer начинается со значения L Я задаю ему значение 10 (что означает 10 секунд) в функции Startlo, а затем после каждого прерывания. Таким образом, устройство в течение 10 секунд пытается получить выходной байт и сгенерировать прерывание, обозначающее готовность следующего байта. Работа, выполняемая в функции OnTimer для каждого 1-секундного отсчета таймера, должна синхронизироваться с функцией обработки прерывания (ISR). Соответственно, я использую функцию KeSynchronizeExecution для вызова вспомогательной функции (CheckTimer) на уровне IRQL устройства (DIRQL) под защитой спин-блокировки. Функции отсчета таймера работают совместно с ISR и функциями отложенного вызова процедуры (DPC), как показывает следующий фрагмент:
VOID OnTimer(PDEVICE_OBJЕСТ fdo, PDEVICE_EXTENSION pdx) {
KeSynchronizeExecution(pdx->InterruptObject, (PKSYNCHRONIZE_ROUTINE) CheckTimer, pdx);
VOID CheckTimer(PDEVICE_EXTENSION pdx)
{
if (pdx->timer <= 0 --pdx->timer >0)	ц j
return:
If (!pdx->busy) return:
PIRP Irp = GetCurrentIrp(&pdx->dqReadWrite);	,, ?
if (11rp) return;
pdx->busy = FALSE;	, ,
Irp->IoStatus.Status = STATUS J OJIMEOUT;	3
Irp->IoStatus.Information = 0;
IoRequestDpc(pdx->DeviceObject, Irp, NULL);
}
BOOLEAN OnlnterruptC...)
{
if (!pdx->busy)
return TRUE;	4
if (!pdx->nbytes)
{
pdx->busy = FALSE;
Irp->IoStatus.Status = STATUS-SUCCESS;
Irp->IoStatus.Information = pdx->numxfer;
IoRequestDpc(pdx->DeviceObject, Irp, NULL);
pdx->timer =10;
1	// 5
636
Глава 14. Специализированные темы
VOID DpcForlsr(...)
{
PIRP Irp = StartNextPacket(&pdx->dqReadWrite, fdo);
loCompleteRequest(Irp, IO_NO_INCREMENT);	// 6
}
1.	Значение таймера -1 означает, что в настоящее время необработанные запросы отсутствуют. Значение 0 означает, что для текущего запроса произошел тайм-аут. В обоих случаях никакой дополнительной работы в этой функции не потребуется. Вторая часть команды if уменьшает таймер. Если значение еще не достигло 0, мы возвращаем управление без каких-либо дополнительных действий.
2.	В драйвере используются объекты DEVQUEUE, поэтому мы вызываем функцию DEVQUEUE с именем GetCurrentlrp для получения адреса запроса, обрабатываемого в настоящий момент. Если значение равно NULL, значит, устройство в настоящий момент бездействует.
3.	В этой точке мы решаем завершить текущий запрос, потому что в течение 10 секунд ничего не произошло. После заполнения полей состояния IRP мы запрашиваем вызов DPC. Код состояния STATUS_IO_TIMEOUT преобразуется в код ошибки Win32 (ERROR_SEM_TIMEOUT), стандартное текстовое описание которого («Истечение интервала тайм-аута для семафора») не дает представления о том, что же в действительности произошло. Если приложение, запросившее операцию, находится под вашим контролем, предоставьте более содержательное объяснение.
4.	Если флаг busy равен FALSE, значит, прерывание поступило неожиданно и будет проигнорировано. Например, это может произойти, когда устройство генерирует ложное прерывание (впрочем, сам пример PIOFAKE написан для фиктивного оборудования, а «устройство» представляет собой диалоговое окно с кнопкой Interrupt, которую можно нажимать в то время, пока тестовая программа не пытается записать строку в устройство). Также может оказаться, что для запроса произошел тайм-аут и функция CheckTimer сбросила флаг именно для того, чтобы обработчик прерывания ничего не делал.
5.	Между прерываниями проходит 10 секунд.
6.	Сторона, запросившая DPC, также заполнила поля состояния IRP. Следовательно, нам остается лишь вызвать loCompleteRequest.
Флаг busy играет важную роль в защите от «гонки» между обработчиком прерывания (Onlnterrupt) и функцией тайм-аута (CheckTimer). Он устанавливается функцией Startlo. Либо Onlnterrupt, либо CheckTimer сбрасывает флаг перед тем, как запросить DPC на завершение текущего IRP. Как только одна из этих функций установит флаг, другая начнет немедленно возвращать управление до тех пор, пока Startlo не начнет новый IRP. Все функции, работающие с флагом
Проблемы совместимости с Windows 98/Ме
637
busy, должны синхронизироваться с обработчиком прерывания. По этой причине KeSynchronizeExecution вызывает CheckTimer и функцию (в тексте не приведенную), которая изначально устанавливает флаг busy равным TRUE.
Проблемы совместимости с Windows 98/Ме
Между Windows 98/Ме и Windows ХР существует ряд различий, относящихся к темам, рассмотренным в этой главе.
Журналы ошибок
В Windows 98/Ме не реализованы файлы регистрации ошибок и нет Event Viewer. Вызов loWriteErrorLigEntry в Windows 98/Ме приводит лишь к тому, что на отладочном терминале выводятся несколько строк данных. Формат этой информации кажется мне неэстетичным, поэтому я предпочитаю попросту не использовать средства регистрации ошибок в Windows 98/Ме. В приложении А рассказано, как идентифицировать текущую систему (Windows 98/Ме или Windows ХР).
Ожидание завершения системных потоков
Windows 98/Ме не поддерживает использование указателей на объект потока (PKTHREAD) в аргументах KeWaitForSingleObject или KeWaitForMultipleObjects. Эти вспомогательные функции просто передают свои аргументы, содержащие указатели на объекты, VWIN32.VXD без какой-либо проверки действительности, а это приводит к сбою VWIN32, потому что объекты потоков не содержат полей, необходимых для поддержки синхронизации.
Следовательно, если вы собираетесь использовать ожидание завершения потоков режима ядра в Windows 98/Ме, придется сделать так, чтобы поток устанавливал событие непосредственно перед вызовом PsTerminateSystemThread. Возможно, установка этого события приведет к тому, что завершающийся поток передаст управление потоку, ожидающему того же события. Формально завершающийся поток все еще будет оставаться «живым», но я не думаю, что это приведет к каким-либо нежелательным последствиям в Windows 98/Ме. Пример POLLING показывает, как повысить приоритет завершающегося потока для снижения риска.
Рабочие элементы
В Windows 98 и Windows Me функции loAxrWorkltem не экспортируются. Впрочем, это обстоятельство не создает особых проблем с надежностью, потому что в этих системах код драйвера вряд ли станет недействительным или будет заменен до выполнения рабочего элемента в очереди. Драйвер, вызывающий ExQueue-Workltem для работы в Windows 98/Ме, в настоящее время не проходит тесты
638
Глава 14. Специализированные темы
WHQL, даже если в нем используется проверка реального времени для предотвращения вызова ExQueueWorkltem в Windows 2000 и последующих системах. Но если ваш драйвер вызывает функции loXrxWorkltem, необходимые для надежной работы с Windows 2000 и выше, он не загрузится в Windows 98 и Windows Me из-за неразрешенных записей импорта. Данная ситуация идеально подходит для применения решения с WDMSTUB.SYS. описанного в приложении А. WSMSTUB определяет заглушки для функций loXxrWorkltem, при наличии которых драйвер нормально загружается.
Моя библиотека GENERIC.SYS также содержит вызовы ExQueueWorkltem, помогающие обойти требование, согласно которому IRP управления питанием должны отправляться и завершаться на уровне PASSIVE-LEVEL в Windows 98 и Windows Me. Надеюсь, к тому моменту, когда вы будете читать книгу, мне удастся убедить WHQL включить проверки рабочих элементов в Driver Verifier, чтобы безопасные вызовы старых функций нормально проходили тестирование.
Распространение иЙайиВ' драйверов устройств
Еще на ранней стадии процесса разработки драйвера необходимо подумать о том, как вы будете распространять свой драйвер и как конечный пользователь будет устанавливать его вместе с обслуживаемым оборудованием. В Microsoft Windows ХР и Microsoft Windows 98/Ме для управления большинством операций, связанных с установкой драйверов, используется текстовый файл с расширением INF. INF-файл находится на дискете или компакт-диске, входящем в комплект поставки устройства, компания Microsoft предоставляет к нему доступ по Интернету или размещает на установочном диске системы. INF-файле сообщает операционной системе, какие файлы следует скопировать на жесткий диск пользователя, какие параметры реестра необходимо создать или изменить в реестре и т. д.
В этой главе рассматриваются некоторые аспекты установки драйверов. В частности, мы обсудим важную роль реестра в установке и инициализации драйвера. Я опишу важнейшие части простого INF-файла, чтобы вам было проще связать воедино все, что говорится в документации DDK о синтаксисе INF-фай-лов. Мы подробно рассмотрим формат идентификаторов для различных типов устройств. Для каждого примера «устройства», встречающегося в книге, мне пришлось определять собственный класс устройств, и я подумал, что вам будет полезно узнать, как это делается.
Компания Microsoft учредила сертифицирующую лабораторию WHQL (Windows Hardware Quality Lab). WHQL помогает поддерживать качество драйверов устройств, приобретаемых пользователями для использования в операционных системах семейства Windows. WHQL предоставляет тестовый пакет НСТ (Hardware Compatibility Test) для многих стандартных классов устройств. Обеспечив прохождение этих тестов, вы получите право на использование лицензионных логотипов Microsoft, а также файл цифровой подписи, который значительно упрощает установку драйвера на компьютерах конечных пользователей. Процесс оформления заявок WHQL также будет описан в этой главе.
Роль реестра
Работа PnP Manager и прдсистемы установки в значительной степени зависит от четырех разделов ветви HKEY_LOCAL_MACHINE реестра. Эти разделы называются разделом оборудования, разделом класса, разделом драйвера и разделом службы
640
Глава 15. Распространение драйверов устройств
(рис. 15.1). Сразу подчеркну, что речь идет не о разделах с конкретными именами — это обобщенные названия четырех разделов, а их имена в реестре зависят от устройства, к которому они относятся. В общих чертах, разделы оборудования и драйвера содержат информацию об одном устройстве, раздел класса относится ко всем устройствам одного типа, а раздел службы содержит информацию о драйвере. Иногда раздел оборудования называется разделом экземпляра, а раздел драйвера — разделом программы. Расхождения в терминологии объясняются тем фактом, что Windows 95/98/Ме и Windows ХР были написаны (в основном) разными людьми. Кроме того, в реестре может присутствовать пятый раздел — раздел параметров оборудования. В нем хранится информация о нестандартных параметрах устройства.
Раздел оборудования
Фильтрующие драйверы
Раздел класса
Фильтрующие драйверы
Раздел драйвера
Функциональный драйвер
Рис. 15.1. Основные разделы реестра, содержащие информацию об устройстве
В этом разделе описывается содержимое указанных разделов реестра. Никогда не изменяйте их напрямую в системе конечного пользователя — эти разделы и содержащиеся в них параметры создаются или поддерживаются автоматически программами установки, Диспетчером устройств и PnP Manager. Далее я покажу, как просматривать и изменять содержимое разделов при помощи функций пользовательского режима и режима ядра. Всегда используйте эти функции и не пытайтесь вносить изменения напрямую. Кстати, даже учетная запись администратора не обладает правом записи в некоторые из этих разделов.
Но при разработке и отладке драйвера (и особенно при отладке процедуры установки) часто приходится напрямую править реестр при помощи утилиты REGEDIT. В Windows ХР REGEDIT позволяет изменять разрешения безопасности для операций с реестром (в Windows 2000 для ©той цели использовалась программа REGEDT32, а в Windows 98/Ме реестр вообще не защищался).
Роль реестра
641
Раздел оборудования (экземпляра)
Разделы оборудования создаются в разделе \System\CurrentControlSet\Enum локального реестра. На рис. 15.2 показан раздел оборудования для такого устройства (а именно USB42 из главы 12). Подразделы первого уровня соответствуют различным перечислителям шин в системе. Описания всех старых и новых устройств USB собраны в подразделе ...\Enum\USB. На рисунке показано содержимое этого подраздела для примера USB42; как видите, идентификатор оборудования устройства (производитель 0547, продукт 102А) превратился в имя раздела (Vid_0547&Pidl02A), а конкретный экземпляр устройства с этим идентификатором отображается в виде подраздела следующего уровня с именем 6&16ГОа439&0&2. Раздел 6&16f0a439&0&2 является разделом оборудования (или разделом экземпляра) для данного устройства.
Рис-15-2. Раздел оборудования в реестре
Некоторые значения раздела оборудования содержат информацию, иссполь-зуемую компонентами пользовательского режима (такими как Диспетчер устройств). На рис. 15.3 показано, как выглядят свойства USB42 в окне Диспетчера устройств. Сравнив рис. 15.2 и 15.3, можно заметить между ними сходство. В частности, строка DeviceDesc в разделе оборудования определяет название устройства (если только в разделе не присутствует параметр FriendiyName, которого в данном случае нет), а свойство Mfg отображается как имя фирмы-производителя. Позднее я объясню, откуда берутся значения других свойств, а также покажу, как получить доступ к ним из приложения пользовательского режима или из драйвера WDM.
СОВЕТ---------------------—--------------------------------------------------------
Окно Диспетчера устройств можно открыть со страницы Оборудование (Hardware) свойств объекта Мой компьютер (Му Computer), из приложения панели управления Система (System) или из консоли управления компьютером из категории Администрирование (Administrative Tools). Так как я очень часто пользуюсь Диспетчером устройств, я создал на рабочем столе ярлык для файла devmgmt.msc, обычно находящегося в каталоге \windows\system32.
642
Глава 15. Распросгранениедрайверовустройств
Рио 15.3. Свойства устройства USB42 в Диспетчере устройств
Раздел оборудования также содержит несколько параметров, идентифицирующих класс устройства и его драйверы. Параметр ClassGUID содержит ASCII-представление кода GUID, однозначно идентифицирующего класс устройства, в сущности, это ссылка на раздел класса для этого устройства. Параметр Class содержит имя класса устройства. Параметр Driver определяет имя раздела драйвера, который является подразделом данного раздела класса. Параметр Service содержит указатель на раздел службы в ветви HKLM\System\CurrentControlSet\ Services. Необязательные параметры LowerFilters и UpperFilters (в примере USB42 их нет) определяют имена служб для нижних и верхних фильтрующих драйверов.
Раздел оборудования может содержать переопределяющие параметры Security, Exclusive, DeviceType и Devicecharacteristics, при наличии которых объект устройства, создаваемый драйвером, должен обладать определенными характеристиками. В USB42 этих параметров нет.
Наконец, раздел оборудования может содержать подраздел с именем Device Parameters с нестандартными конфигурационными данными об оборудовании (рис. 15.4). Sampleinfo, единственное свойство в примере USB42, задает справочный файл для драйвера (другие параметры на рисунке остались от проведения тестов НСТ, никакого вреда от них не будет).
Роль реестра
643
Рис. 15.4. Раздел параметров устройства в реестре
Раздел класса
Разделы классов для всех разновидностей устройств находятся в разделе НК1_М\ System\CurrentControlSet\Control\Class. На рис. 15.5 показано, как выглядит этот раздел для класса SAMPLE, к которому принадлежат пример USB42 и все остальные примеры драйверов в книге.
Рис. 15.5. Раздел класса в реестре
Раздел содержит следующие параметры:
О (По умолчанию) ((Default)) — имя класса в форме, понятной для пользователя. Используется Диспетчером устройств (см. рис. 15.3).
О Class — имя класса. Имя класса и GUID однозначно ассоциируются друг с другом здесь и в разделах оборудования устройств, принадлежащих этому классу.
О EnumPropPages32 — определяет DLL с нестандартными страницами свойств, отображаемыми в Диспетчере устройств для класса. В данном примере отображается страница с именем Sample Information. В общем случае значение параметр может включать имя DLL и имя точки входа. Если имя точки входа не указано, как в данном примере, система по умолчанию использует EnumPropPages.
О Install32 — задает имя DLL-библиотеки, которая используется подсистемой установки при любых действиях по установке устройств, принадлежащих данному
644
Глава 15. Распространение драйверов устройств
классу. В общем случае значение параметр может включать имя DLL и имя точки входа. Если имя точки входа не указано, как в данном примере, система по умолчанию использует то же имя, что и для DLL со страницами свойств. О Icon — идентификатор ресурса значка в DLL-библиотеке установки класса.
Диспетчер устройств и система установки используют этот значок при отображении информации о классе. Если DLL установки класса отсутствует, DDK рекомендует брать этот значок из DLL страницы свойств, но в Windows 2000 этого не происходило, поэтому я привык предоставлять хотя бы вырожденную точку входа установки класса, чтобы я мог предоставлять нестандартный значок.
В классе SAMPLE отсутствуют некоторые необязательные параметры, в том числе:
О NoInstallClass — если параметр существует и отличен от 0, он указывает, что устройства, принадлежащие к этому классу, автоматически обнаруживаются некоторым перечислителем. Если класс обладает этим атрибутом, мастер оборудования не включает его в список классов устройств, отображаемый для конечного пользователя;
О Silentlnstall — если параметр существует и отличен от 0, то PnP Manager устанавливает устройства этого класса, не отображая никаких диалоговых окон для конечного пользователя;
О UpperFilters и LowerFilters — задают имена служб для фильтрующих драйверов. PnP Manager загружает эти фильтры для каждого устройства, принадлежащего классу (фильтрующие драйверы, действующие только для одного устройства, указываются в разделе оборудования устройства);
О NoDisplayClass — если параметр существует и отличен от 0, устройства данного класса не отображаются в списке Диспетчера устройств.
Раздел класса также может содержать подраздел Properties, содержащий параметры с именами Security, Exclusive, DeviceType и Devicecharacteristics. Эти значения переопределяют настройки по умолчанию для объектов всех устройств класса. За дополнительной информацией об этих настройках обращайтесь к разделу «Свойства объекта устройства» (см. далее).
Раздел драйвера
Для каждого устройства также создается собственный подраздел в разделе класса. Имя этого раздела (относительно CurrentControlSet\Control\Class) определяется параметром Driver в разделе оборудования устройства. На рис. 15.6 изображено содержимое этого подраздела, предназначенного для установления связи между данными в реестре и INF-файлом, используемым для установки устройства и хранения конфигурационных данных, относящихся к этому устройству.
Как в разделе драйвера, так и в разделе параметров оборудования может храниться информация о параметрах устройства. Различия между ними довольно неочевидны. В DDK говорится, что раздел драйвера содержит «информацию, специфическую для драйвера», а в разделе параметров оборудования хранится «информация, специфическая для устройства». В обоих случаях «информация»
Роль реестра
645
относится к конкретному экземпляру устройства. По представлениям Microsoft, информация, специфическая для драйвера, относится к конкретному драйверу и не распространяется на другие драйверы того же оборудования. Признаюсь, я так и не понял глубокого смысла этого разделения, поскольку слишком сильно привык к тому, что любое устройство обслуживается только одним драйвером.
Рис. 15.6. Раздел драйвера в реестре
Раздел службы
Последним разделом реестра, играющим важную роль для драйверов устройств, является раздел службы. Он указывает, где на диске хранится исполняемый файл драйвера, и также содержит другие параметры, управляющие загрузкой драйвера. Разделы службы хранятся в разделе HKLM\System\CurrentControlSet\Services. На рис. 15.7 показано, как выглядит раздел службы для примера USB42.
Рис. 15.7. Раздел службы в реестре
Я не стану пересказывать все возможное содержимое раздела службы — оно хорошо документировано в нескольких местах, в том числе в разделе «Service Install» пакета Platform SDK. В приведенном примере использованы следующие параметры:
О ImagePath — означает, что исполняемый файл драйвера называется USB42.SYS и находится в каталоге %SystemRoot%\system32\drivers. Обратите внимание:
646
Глава 15. Распространение драйверов устройств
в параметре реестра в этом случае указывается относительный путь по отношению к корневому каталогу системы;
О Туре (1) — означает, что раздел описывает драйвер режима ядра;
О Start (3) —- означает, что система должна загрузить драйвер, когда это потребуется для обеспечения поддержки нового устройства. (Числовое значение соответствует константе SERVICE_DEMAND_START при вызове CreateService. Для драйверов режима ядра его смысл уже описывался! ранее — не обязательно вызывать StartService или выдавать команду NET START для запуска драйвера.);
О ErrorControl (1) — означает, что если попытка загрузить драйвер завершится неудачей, система сохраняет в журнале информацию об ошибке и выводит окно сообщения.
Работа с реестром из программы
Как я уже говорил, для работы с реестром следует использовать функции API пользовательского режима и режима ядра (и никогда не пытайтесь изменять его напрямую). В этом разделе мы рассмотрим соответствующие API.
Работа с реестром из драйвера
В табл. 15.1 перечислены функции режима ядра, которые должны использоваться для обращения к информации в только что рассмотренных разделах реестра. В большинстве случаев особые меры требуются! только для открытия раздела. Далее для чтения и записи данных используются стандартные функции ZwAxx, после чего раздел закрывается функцией ZwClose. За более подробными описаниями обращайтесь к примерам в главе 3.
Таблица 15-1, Интерфейс работы с реестром из драйверов устройств
Раздел реестра	Функция, используемая для обращения
Раздел оборудования	Отдельные стандартные свойства читаются функцией loGetDeviceProperty. Изменить значения этих свойств из драйвера невозможно; не пытайтесь определить полное имя этого раздела, чтобы открыть его напрямую
Раздел параметров оборудования	loOpenDeviceRegistryKey (режим PLUGPLAY_REGKEY_DEVICE)
Раздел драйвера	loOpenDeviceRegistryKey (режим PLUGPLAY_REGKEY„DRIVER)
Раздел класса	Метод доступа не указывается. Не пытайтесь определить полное имя раздела, чтобы открыть его напрямую
Раздел службы	ZwOpenKey с использованием параметра RegistryPath функции DriverEntry
Для обращения к «стандартным» параметрам, хранящимся в разделе оборудования, используется функция loGetDeviceProperty с одним из кодов свойств, перечисленных в табл. 15.2. Содержимое столбца «Источник» соответствует элементам INF-файла, описанным в следующем основном разделе этой главы.
Роль реестра
647
Учтите, что некоторые из поддерживаемых кодов свойств в таблице не приведены, но они относятся к свойствам, которые PnP Manager хранит в других местах реестра.
Таблица 15.2. Стандартные свойства устройств в разделе оборудования
Имя свойства	Имя значения	Источник	Описание
DevicePropertyDeviceDescription	DeviceDesc	Первый параметр описания модели	Описание устройства
DevicePropertyHardwareld	HardwarelD	Второй параметр описания модели	Идентифицирует устройство
DevicePropertyCompatiblelDs	CompatiblelDs	Создается драйвером шины во время опознания	Типы устройств, которые могут считаться подходящими
DevicePropertyClassName	Class	Параметр Class секции Version INF-файла	Имя класса устройства
DevicePropertyClassGuid	ClassGUID	Параметр ClassGuid секции Version INF-файла	Уникальный идентификатор класса устройства
DevicePropertyDriverKeyName	Driver	Первый параметр команды AddService	Имя раздела службы с информацией о драйвере
DevicePropertyManufacturer	Mfg	Фирма-производитель, в секции которой было обнаружено устройство	Имя производителя оборудования
DevicePropertyFriendlyName	FriendlyName	Команда Add Reg в INF-файле или DLL установки класса	«Дружественное» имя, подходящее для представления пользователю
Например, для получения описания устройства используется следующий код (см. функцию AddDevice в примере DEVPROP):
WCHAR name[256];
ULONG junk;
status = IoGetDev1ceProperty(pdo,
DevicePropertyDeviceDescription, sizeof(name), name, &junk);
KdPr1nt((DRIVERNAME
" - AddDevice has succeeded for ’Xws' deviceVn", name));
ПРИМЕР--------------------------------------------------------------------------------
Пример DEVPROP в прилагаемых материалах показывает, как получить информацию стандартных свойств из режима ядра и пользовательского режима.
648
Глава 15. Распространение драйверов устройств
Обращение к реестру из пользовательского режима
В табл. 15.3 перечислены функции API пользовательского режима, используемые для обращения к перечисленным разделам реестра. Большинство функций ориентировано на программы и библиотеки установки. Для работы с ними необходимо иметь манипулятор HDEVINFO и структуру SP_DEVINFO_DATA для интересующего вас устройства.
Таблица 15.3. Интерфейс работы с реестром из пользовательского режима
Раздел реестра	Функция, используемая для обращения
Раздел оборудования	Отдельные стандартные свойства читаются функциями Setu pDiGetDevice Reg istryProperty и SetupDiSetDeviceRegistryProperty
Раздел параметров оборудования Раздел драйвера Раздел класса	SetupDiOpenDevRegKey (режим DIREG DEV) SetupDiOpenDevRegKey (режим DIREGJDRV) SetupDiOpenClassRegKey. Начиная c Windows XP для чтения и записи свойств объектов устройств используются функции SetupDiGetClassRegistryProperty и SetupDiSetClassRegistryProperty
Раздел службы	QueryServiceConfig, ChangeServiceConfig
Например, в контексте перечисления зарегистрированных интерфейсов с использованием Setup-функций API можно получить дружественное имя устройства:
HDEVINFO info = SetupDIGetClassDevs(...);
SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)}:
SetupDIGetDevicelnterfaceDetail(Info. .. , &did);
TCHAR fname[256];
SetupDiGetDeviceRegi stryProperty(1nfо, &di d,
SPDRP_FRIENDLYNAME, NULL, (PBYTE) fname, sizeof(fname). NULL);
Полный список значений SPDRP_XXX, используемых при выборке различных свойств, приведен в описании функции SetupDiGetDeviceRegistryProperty в DDK.
Следующий трюк применяется в тех ситуациях, когда известно только символическое имя устройства:
LPCTSTR devname; // <== получено из внешнего источника
HDEVINFO info = SetupDiCreateDevicelnfoLIst(NULL, NULL);
SP_DEVICE_INTERFACE_DATA ifdata = {sizeof(SP_DEVICE_INTERFACE_DATA)}:
SetupDiOpenDevIceInterfaceCinfo, devname, 0. &1fdata);
SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)}:
SetupDiGetDevicelnterfaceDetaiHinfo, &ifdata, NULL, 0. NULL, &did);
После этого вы можете вызывать функции (такие как SetupDiGetDeviceRegistry-Property) обычным способом.
Роль реестра
649
ПРИМЕЧАНИЕ---------------------------------------------------------------------
В Windows 98 и Windows NT версии 4 прикладные программы использовали функции семейства CFGMGR32 для получения информации об устройствах и взаимодействия с PnP Manager. Эти функции поддерживаются в Windows 98/Ме и Windows ХР для сохранения совместимости, но Microsoft не рекомендует использовать их в новом коде, поэтому я даже не привожу примеры их вызова.
Свойства объекта устройства
Как вы уже знаете, объект устройства создается вызовом loCreateDevice. Б драйверах WDM функция AddDevice обычно создает один объект устройства и связывает его со стеком драйверов РпР, вызывая функцию loAttachDeviceToDeviceStack. После того как функциональный драйвер и все фильтрующие драйверы выполнят эти действия, PnP Manager обращается к реестру для переопределения некоторых настроек объекта устройства. Переопределение применяется к следующим атрибутам:
О дескриптору безопасности, связанному с объектом физического устройства (PDO), может переопределяться значением параметра Security из реестра;
О типу устройства (FILE_DEVICE_AXY) для PDO, может переопределяться значением параметра DeviceType из реестра;
О флагам характеристик устройства, могут определяться значением параметра Devicecharacteristics;
О режиму монопольного доступа PDO, может переопределяться значением параметра Exclusive.
PnP Manager снача а обращается к разделу оборудования, а затем к подразделу Properties раздела класса для поиска этих переопределений. Выполнив модификацию PDO, PnP Manager объединяет флаги характеристик из всех объектов устройств в стеке и задает некоторым из них (определяемым маской FILE_CHARACTERISTICS_PROPAGATED из файла ntddk.h) одинаковое значение для всех объектов устройств. В настоящее время этот механизм распространяется на следующие флаги:
О FILE_REMOVABLEJ4EDIA;
О FILE_READ_ONLY_DEVICE;
О FILE_FLOPPY_DISKETTE;
О FILE_WRITE_ONCE_MEDIA;
О FILE_DEVICE_SECURE_OPEN.
Чтобы переопределения безопасности и монопольного доступа нормально работали, ни один из фильтрующих и функциональных драйверов в стеке РпР не должен присваивать имя своему объекту устройства. Вместо этого драйверы должны использовать loRegisterDevicelnterface как единственный метод создания символических ссылок. Метод регистрации интерфейсов заставляет I/O Manager и Object Manager создавать ссылку на PDO при открытии манипулятора устройства, в результате чего эти два переопределения вступают в силу.
650
Глава 15. Распространение драйверов устройств
Переопределяющие значения попадают в реестр одним из двух способов. Во-первых, вы можете задать их при помощи специального синтаксиса в INF-файле. Во-вторых, ваша программа или какое-либо стандартное управляющее приложение могут задать их значения в разделах оборудования или класса при помощи функций SetupDiSetXxxRegistryProperty.
INF-файл
Вооружившись информацией о том, как РпР Manager и подсистема установки работают с реестром, давайте разберемся в структуре поставляемых INF-файлов. Основным предназначением INF-файла является передача подсистеме установки об установке информации файлов, относящихся к устройству, в системе конечного пользователя и о модификации реестра. В DDK приводится чрезвычайно подробное описание синтаксиса INF-файлов, которое я не стану повторять. Тем не менее, мне хотелось бы в общих чертах представить наиболее часто используемые секции INF-файлов.
INF-файл состоит из секций, имена которых указываются в квадратных скобках. Большинство секций содержит набор директив в формате «ключевое слово = значение». INF-файл начинается с секции Version, которая идентифицирует тип устройства на основании информации из файла и других глобальных характеристик в установочном пакете драйвера. В следующем примере секция Version содержит минимум обязательной информации:
[Version]
S1gnature=$CHICAG0$
Class=Sample
CIassGuid={894A7460-A033-lld2-821E-444553540000}
CatalogFI1e^whatever.cat
DriverVer=mm/dd/yyyy
: Copyright 2002 by Proseware, Inc.
Сигнатура (Signature) принимает одно из нескольких специальных значений. Я использую значение $Chicago$, работающее на всех платформах WDM. Параметр Class определяет класс устройства; заранее определенные классы, поддерживаемые Windows ХР, перечислены в табл. 15.4. Параметр ClassGuid однозначно идентифицирует класс устройства. Заголовочный файл DDK DEVGUID.H определяет GUID стандартных классов устройств, кроме того, они документированы в описании секции Version в DDK. Параметр CatalogFile задает имя файла с цифровой подписью, выдаваемой лабораторией WHQL после сертификации пакета драйверов; при проведении тестирования следует использовать пустой файл. Параметр DriverVer определяет дату создания пакета драйвера и, возможно, необязательный номер версии. Система использует номер версии для у поря-
INF-файл
651
дочения драйверов, снабженных цифровой подписью. В секцию Version также желательно включить комментарий (произвольная строка, начинающаяся с точки с запятой) со словом copyright. Вообще говоря, указывать информацию об авторских правах не обязательно, но, вероятно, это стоит сделать.
Подсистема установки успешно обработает INF-файл, у которого секция Version содержит только параметры Signature и Class. Тем не менее, остальные параметры, приведенные в предыдущем примере, необходимы для успешной сертификации драйвера в лаборатории WHQL.
Таблица 15,4. Классы устройств
Имя класса в INF-файле	Описание
1394	Хостовые контроллеры шины IEEE 1394 (но не периферийные устройства)
Battery CDROM	Батареи Дисководы CD-ROM (с интерфейсами SCSI и IDE)
DiskDrive	Жесткие диски
Display FDC	Видеоадаптеры Контроллеры флоппи-дисководов
FloppyDisk HDC	Флоп пи-дисководы Контроллеры жестких дисков
HIDCIass	Устройства взаимодействия с пользователем
Image	Устройства ввода статических изображений (в том числе цифровые камеры и сканеры)
Infrared	Драйверы минипортов NDIS (Network Driver Interface Specification) для инфракрасных портов (Serial-IR и Fast-IR)
Keyboard MediumChanger Media	Клавиатуры Чейнджеры с интерфейсом SCSI Мультимедийные устройства (аудио, DVD, джойстики, видеокамеры и т. д.)
Modem	Модемы
Monitor	Мониторы
Mouse	Мыши и другие указательные устройства
MTD Multifunction	Драйвер Memory Technology для устройств памяти Комбинированные устройства
MuitiportSerial Net	Многопортовые карты Сетевые адаптеры
NetClient	Провайдеры сетевых файловых систем и печати (клиентская сторона)
NetService	Поддержка сетевых файловых систем (клиентская сторона)
NetTrans	Драйверы сетевых протоколов
продолжение
652
Глава 15. Распространение драйверов устройств
Таблица 15.4 (продолжение)
Имя класса в INF-файле	Описание
PCMCIA	Хостовые контроллеры PCMCIA и CardBus
PNPPrinter	Драйвер класса печати для конкретной шины
Ports	Последовательные и параллельные порты
Printer	Принтеры
SCSIAdapter	Контроллеры SCSI и RAID, минипорты адаптеров главной шины и контроллеры дисковых массивов
SmartCardReader	Устройства чтения смарт-карт
System TapeDrive USB	Системные устройства Стримеры Хостовые контроллеры и концентраторы USB (но не периферийные устройства)
Volume	Драйверы логических томов
INF-файл удобно представлять себе в виде линейного описания древовидной структуры (рис. 15.8). Каждая секция представлена узлом дерева, а каждая директива соответствует ссылке на другую секцию.
Рис. 15.8. Иерархическое представление INF-файла
Вершиной дерева является секция Manufacturer, в ней перечисляются все компании, оборудование которых описывается в файле. Пример:
[manufacturer]
"Walter Oney Software"=DeviceList
"Finest Organization On Earth Yet"=FOOEY
INF-файл
653
[DeviceList]
[FOGEY]
Секция модели каждого отдельного производителя (DeviceList и FOOEY в этом примере) содержит описание одного или нескольких устройств:
[DeviceList]
Descrl pt 1	Inst a 1 ISect 1 onName ,Devi celd, Compatiblelds
Здесь Description — описание устройства, понятное для пользователя, a De-viceld — идентификатор устройства. Параметр Compatiblelds, если он указан, содержит список идентификаторов других устройств, с которыми может работать тот же драйвер. Параметр InstallSectionName идентифицирует другую секцию INF-файла (или ссылается на нее, если пользоваться моим иерархическим представлением), которая содержит инструкции по установке программного обеспечения для отдельного устройства. Далее приводится пример записи для одного типа устройства (позаимствован из примера PKTDMA из главы 7):
[DeviceList]
"AMCC S5933 Development Board (DMA),,=Dr1 verinstall ,PCI\VENJ0E8&DEV_4750
Информация в секции Manufacturer и в секции (или секциях) моделей отдельных производителей вступает в силу тогда, когда системе требуется установить драйвер некоторого устройства. Устройство PnP (Plug and Play) объявляет о своем присутствии и проводит самоидентификацию на электронном уровне. Драйвер шины автоматически распознает устройство и конструирует для него идентификатор на основании встроенных данных. Затем система пытается найти уже установленные INF-файлы, описывающие это устройство. INF-файлы хранятся в подкаталоге INF основного каталога Windows. Если системе не удается найти подходящий INF-файл, она запрашивает его у пользователя.
Наследные устройства не умеют объявлять о своем присутствии или проводить самоидентификацию. Чтобы установить наследное устройство, конечный пользователь должен запустить мастер оборудования и указать, где находится нужный INF-файл. Ключевыми этапами этого процесса являются выбор типа устанавливаемого устройства и имени производителя (рис. 15.9).
Мастер установки оборудования создает диалоговое окно наподобие показанного на рис. 15.9, перечисляя все INF-файлы для данного типа устройств, все директивы в секции Manufacturer и все команды моделей для каждого производителя. Как нетрудно предположить, имена производителей, отображаемые на левой панели, берутся из левой части директив секции Manufacturer, а все команды моделей — из левой части описаний моделей.
654
Глава 15. Распространение драйверов устройств
Рис. 15.9. Выбор устройства во время установки
О ДИАЛОГОВЫХ ОКНАХ МАСТЕРА УСТАНОВКИ НОВОГО ОБОРУДОВАНИЯ------------------------------------
Когда мастер пройдет фазу поиска устройств РпР, он строит список классов устройств и использует различные функции SetupDiXxx из SETUPAPI.DLL для получения значков и описаний. Информация, которую SETUPAPI использует для реализации этих функций, в конечном счете берется из реестра, куда она была занесена записями в секциях Classlnstall32. Не все классы устройств присутствуют в списке — мастер подавляет информацию о классах, обладающих атрибутом NoInstallClass.
После того как конечный пользователь выберет класс устройства, мастер вызывает функции SETUPAPI для построения списков производителей и устройств, как описано в тексте. Устройства, упомянутые в директивах ExcludeFromSelect, в список не включаются.
Секции установки
Секция установки содержит инструкции, необходимые для установки программного обеспечения устройства. Вернемся к примеру PKTDMA. Для этого устройства в секции модели DeviceList указано имя Driverlnstall. На мой взгляд, это имя удобно рассматривать как имя массива секций, каждая из которых относится к одной платформе Windows. «Нулевой» элемент массива содержит базовое имя секции (Driverlnstall). Имена платформенно-зависимых элементов массива состоят из базового имени и одного из суффиксов, перечисленных в табл. 15.5. Программа установки устройства ищет секцию установки, обладающую самым точным суффиксом. Для примера допустим, что имеются две секции установки: без суффикса и с суффиксом .NTx86. Если устройство устанавливается в Windows ХР на платформе Intel х86, система установки будет использовать секцию .NTx86, а при установке в Windows 98/Ме будет использоваться секция без суффикса.
INF-файл
655
Таблица 15.5. Суффиксы платформ
Платформа	Суффикс
Любая, включая Windows 98/Ме	(нет)
Любая платформа Windows ХР	.NT
Windows ХР на процессорах Intel х86	.NTx86
Windows ХР на 64-разрядных процессорах Intel	.NTIA64
Из-за правил поиска, которые я только что описал, все INF-файлы в моих примерах содержат секции без суффикса ц с суффиксом .NTx86. Благодаря этому INF-файлы нормально работают на всех платформах Intel х86.
ИДЕНТИФИКАЦИЯ ОПЕРАЦИОННЫХ СИСТЕМ-----------------------------------------------------------
Для Windows ХР и последующих операционных систем в синтаксисе INF-файлов предусмотрен довольно неудобный способ указания разных драйверов для разных операционных систем. Вы создаете несколько секций моделей с уникальными именами и различаете их, присоединяя строки TargetlsVersion к командам моделей в секции [Manufacturer]. Полное описание синтаксиса этих строк приведено в документации DDK. Например, следующий фрагмент INF-файла обеспечивает установку разных драйверов для Windows 98, Windows 2000 и Windows ХР:
[Manufacturer]
"Walter Oney Software"=DeviceL1st	; для 98/МЕ и 2K
"Walter Oney Software"=DeviceList,NTx86.5.1 ; ХР на процессорах x86
"Walter Oney Software"=DeviceList,Ntia64.5.1 ; ХР на процессорах IA64
За этой секцией следуют три секции моделей, в которых перечисляются одни и те же модели оборудования, и четыре секции установки, обладающие уникальными именами, для каждой модели. Также имеется дополнительная секция установки .NTx86 для Windows 2000; получается, что каждая модель устройства представлена четырьмя секциями установки. Так, секции моделей с именами [DeviceList], [DeviceList.NTx86.5.1] и т. д. ссылаются на секции установки с именами (например) [Widgetinstall], [Widgetinstall.NTx86], [Widgetinstall.NTx86.5.1] и т. д. Вам придется основательно прокомментировать INF-файл, иначе позднее вы сами не разберетесь, что в нем происходит! Почему я назвал этот механизм неудобным? Дело в том, что вы не можете просто присоединить суффикс операционной системы к имени секции установки — вам приходится строить отдельное дерево директив моделей, ссылающихся на секции установки с уникальными именами. Существование двух разных схем идентификации платформ и операционных систем объясняется историческими причинами. Системы Windows 98/Ме вообще не воспринимают имена секций с суффиксами. Windows 2000 присоединяет суффиксы к именам секции установки (и некоторым другим), но не распознает зависимости от операционной системы. Чтобы реализовать зависимость от операционной системы без нарушения работы существующих INF-файлов, компании Microsoft потребовалась новая схема.
Далее в этой главе я опишу другие секции INF-файлов, имена которых начинаются с имени секции установки. Если «массив» содержит несколько секций установки, то имена других секций также должны включать платформенные суффиксы. Например, вскоре мы рассмотрим секцию служб, предназначенную для включения описания драйвера в реестр. Имя секции строится из базового имени секции установки (например, Driverinstall), суффикса платформы (например, NTx86) и слова Services, в конечном итоге мы получаем что-то вроде [Dri verlnsta II. NT x86. Services].
656
Глава 15. Распространение драйверов устройств
Типичная секция установки Windows ХР содержит директиву CopyFiles и ничего более:
[Driverinstall.ntx86]
CopyF11es=Dr1 verCopyFi1es
Директива CopyFiles означает, что программа установки должна воспользоваться информацией из другой секции INF-файла для копирования файлов на жесткий диск конечного пользователя. В примере PKTDMA эта другая секция называется DriverCopyFiles;
rDriverCopyFIles]
pktdma.sys,,,2
Эта секция приказывает программе установки скопировать файл PKTDMA.SYS на жесткий диск конечного пользователя.
Команды секции CopyFiles имеют следующую общую форму:
Destination,Source, Temporary,Flags
Здесь Destination — имя файла (без имени каталога), создаваемого в системе конечного пользователя, Source — имя файла в том виде, в котором он существует на дистрибутивном носителе (если это имя отличается от Destination, поле просто остается пустым, как в данном примере). В Windows 98/Ме имена временных файлов, используемых только во время установки, задаются параметром Temporary. Windows 98/Ме заменяет имя временного файла именем Destination при следующей перезагрузке. Использовать этот параметр в Windows ХР не обязательно, потому что система автоматически генерирует временные имена.
Параметр Flags содержит битовую маску, которая указывает, должна ли система распаковать сжатый файл и что делать в ситуациях, когда файл с указанным именем уже существует. Интерпретация флагов отчасти зависит от того, являются ли INF-файл и драйвер частью пакета, снабженного цифровой подписью Microsoft после сертификации. За полными описаниями флагов обращайтесь к документации DDK. Обычно я передаю в этом параметре значение 2, оно означает, что если при копировании файла произошли ошибки, установка считается неудачной.
Информация, необходимая системе установки для копирования файла, не исчерпывается его именем. Система установки также должна знать каталог, в который копируется файл. Кроме того, если установочный комплект состоит из нескольких дискет, необходимо сообщить, какая дискета содержит исходный файл. Соответствующие данные берутся из других секций INF-файла (рис. 15.10). В примере PKTDMA эти секции выглядят так:
[DestinatlonDI rs]
DefaultDestDI r==10, System32\Dr1 vers
[SourceDisksFiles]
pktdma.sys=l,obj chk~1\1386,
[SourceDlsksNames]
1="WDM Book Companion Disc".dlskl
INF-файл
657
Из секции SourceDisksFiles мы видим, что программа установки берет файл PKTDMA.SYS на диске 1, в подкаталоге, имя которого в формате 8.3 имеет вид objchk~l\i386. Секция SourceDisksNames указывает, что диск 1 снабжен меткой «WDM Book Companion Disc» и содержит файл с именем diskl; по наличию этого файла программа установки может убедиться в том, что в дисковод вставлена правильная дискета. Обратите внимание на внутреннюю букву «s» в именах секций, которую так легко упустить.
[DriverCopyFiles] pktdma.sys„,2
[DestinationDirs]
DefaultDestDir-10,System32\Drivers
Cp0yjp^tma.sys Ш disk! Й c:\windows\systfenl32\drivers
Рис. 15.10. Информация об источнике и приемнике при копировании файлов
Секция DestinationDirs задает целевые каталоги для операций копирования. Параметр DefaultDestDir определяет целевой каталог для тех файлов, для которых он не задан. Для указания целевого каталога используется цифровой код, потому что конечный пользователь может установить Windows ХР в каталог с нестандартным именем. За полным списком кодов обращайтесь к описанию секции DestinationDirs в DDK; впрочем, на практике обычно используется лишь небольшое подмножество кодов:
О 10 — каталог Windows (например, \Windows или \Winnt);
О И — системный каталог (например, \Windows\System или \Winnt\System32);
О 12 — каталог драйверов в системе Windows ХР (например, \Winnt\System32\ Drivers). К сожалению, в Windows 98/Ме это число имеет другое значение (например, \Windows\System\Iosubsys).
Драйверы WDM находятся в каталоге драйверов. Если секция CopyFiles относится только к установке Windows ХР, укажите номер каталога 12, но если она должна совместно использоваться в Windows 98/Ме и Windows ХР, я рекомендую задать значение «10,System32\Drivers», потому что оно в обоих случаях задает каталог драйверов.
Секция Services
Синтаксис INF-файлов, рассмотренный до настоящего момента, всего лишь обеспечивает копирование файла (или файлов) драйвера на жесткий диск конечного пользователя. Однако вы также должны сообщить PnP Manager, какие
658
Глава 15. Распространение драйверов устройств
файлы следует загружать. Для этой цели используется секция Services, как в следующем примере:
[Driverinstall.NTx86.Services]
AddServIce=PKTDMA,2,Dr1verServi ce
[DrlverService]
ServiceType=l
StartType=3
ErrorControl =1
Servi ceBI na ry=£W\system32\dr1 vers\pktdma. sys
Значение 2 в директиве AddService означает, что PKTDMA является функциональным драйвером устройства. Имя секции образуется присоединением слова Services к имени соответствующей секции установки.
Конечным результатом этих директив является то, что в разделе реестра HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services создается подраздел с именем PKTDMA (первый параметр директивы AddService). Он указывает, что драйвер является драйвером режима ядра (ServiceType-1), который должен загружаться PnP Manager в случае необходимости (StartType=3). Ошибки, происходящие во время загрузки драйвера, регистрируются, но не могут помешать запуску системы (ErrorControl” 1). Исполняемый образ драйвера находится в каталоге \Winnt\System32\Drivers\pktdma.sys (значение ServiceBinary). Кстати говоря, заглянув в реестр, вы увидите, что имя исполняемого файла хранится в параметре ImagePath, а не ServiceBinary.
Имя службы (PKTDMA в данном примере) рекомендуется задавать совпадающим с именем двоичного файла драйвера (PKTDMA.SYS). Это позволяет не только сразу определить, какое имя службы соответствует тому или иному драйверу, но и избежать проблем в ситуациях, когда два разных раздела служб ссылаются на один драйвер.
Заполнение реестра
Многие INF-секции могут содержать директиву AddReg со ссылкой на секцию заполнения реестра, находящуюся в другом месте INF-файла:
[SomeSection]
AddReg=SomeAddReg
В свою очередь, секция заполнения реестра содержит команды, использующие позиционный синтаксис для определения параметров реестра:
[SomeAddReg]
раздел, подраздел, имя_параметра, флаги, значение
В этом синтаксисе раздел обозначает один из стандартных корневых разделов (HKCR, HKCU, HKLM или HKU) или содержит контекстно-зависимое
INF-файл
659
значение HKR. HKR соответствует «относительному корневому разделу», а его интерпретация зависит от того, где находится директива AddReg со ссылкой на данную секцию заполнения реестра. В табл. 15.6 перечислены значения HKR для различных источников AddReg.
Таблица 15.6. Интерпретация значения HKR в секции заполнения реестра
Секция, содержащая AddReg	Смысл HKR
Секция установки (например, [Driverlnstall])	Раздел драйвера
Секция оборудования (например, [Driverlnstall.ntx85.hw])	Раздел параметров оборудования
Секция службы (например, [Driverlnstall.ntx86.services])	Раздел службы
[Classlnstall32] или [Classinstall]	Раздел класса
Раздел регистрации событий (задается необязательным четвертым параметром директивы AddService)	Подраздел регистрации событий для этого драйвера
Раздел добавления интерфейса (задается необязательным третьим параметром в директиве Addinterface)	Раздел интерфейса (в книге не рассматривается)
Секция вспомогательной установочной DLL (например, [Driverlnstall.Coinstallers])	Раздел драйвера
Параметр подраздел в синтаксисе заполнения реестра определяет необязательный подраздел заданного раздела. Я редко использую подраздел с HKR, но это абсолютно необходимо при использовании других корневых разделов. Например, для создания новой записи в разделе RunOnce я бы добавил в секцию AddReg следующую кохманду:
HKLM,Software\Microsoft\ki1ndows\CurrentVerswn\Run0nce.<^ г. д.>
Кстати говоря, при задании абсолютного пути в реестре, как в этом примере, не так уж важно, какая секция INF-файла содержит команду AddReg.
Имя_параметра в директиве заполнения реестра задает название параметра, создаваемого в реестре. Опустите его (то есть не указывайте ничего между запятыми), чтобы задать значение по умолчанию для раздела реестра. В секции [Classlnstall32] параметры с именами Devicecharacteristics, DeviceType, Security и Exclusive интерпретируются особым образом — они ссылаются на параметры, находящиеся в подразделе Properties раздела класса.
Параметр флаги определяет формат данных, включаемых в реестр, а также задает дополнительные аспекты поведения. За полной информацией обо всех флагах обращайтесь к разделу DDK «INF AddReg Directive». Я использую в своих INF-файлах лишь значения:
О 0 — параметр типа REG 5Z (используется по умолчанию, если параметр не указан);
О 1 — параметр типа REG_BINARY;
О 0x00010001 — параметр типа REG_DWORD в Windows ХР или параметр REGBINARY в Windows 98/Ме (где параметр флаги усекается до 16 бит, поэтому бит 0x00010000 не воспринимается).
660
Глава 15. Распространение драйверов устройств
Поле значение определяет значение, которое вы пытаетесь задать. Для параметров REGSZ задается строка, заключенная в кавычки (в которой внутренние символы кавычек обозначаются удвоением символа). Полное описание синтаксиса приведено в разделе «INF String Section» документации DDK. Строки, не содержащие пропусков или специальных символов, могут записываться без кавычек. Таким образом, следующие два фрагмента считаются эквивалентными:
HKR,,NTMPD г1 ver,,"devprop.sys"
-и-
HKR,,NTMPD г1 ver,,devprop.sys
Двоичные значения задаются в виде серий из 8 битов. Это требование создает некоторые проблемы при определении значений REG„DWORD в INF-секциях, которые должны использоваться как в Windows ХР, так и в Windows 98/Ме. Портируемый способ задания параметров REG_DWORD выглядит так:
[DriverInstall.ntx86.hw]
AddReg=HwAddReg
[Driverinstall,hw]
AddReg=HwAddReg
[HwAddReg]
HKR,TrogrammersShoeSize,0x00010001, 0x2A, 0, 0, 0
По наличию 32 бит данных обе системы делают вывод, что в реестре следует создать параметр REG_DWORD со значением 42 (десятичное).
Познакомившись с общим синтаксисом, рассмотрим несколько примеров реестровых записей в INF-файлах.
Инициализация конфигурации оборудования
Сведения о конфигурации, относящиеся к оборудованию, находятся в разделе параметров оборудования. Ранее я уже приводил вымышленный пример параметра ProgrammersShoeSize. Более реалистичный пример — драйвер, который обслуживает два различных типа устройств, неразличимых во время выполнения AddDevice, но обслуживаемых по-разному (между устройствами не должно быть серьезных различий, иначе для них следовало бы написать два разных драйвера).
Функция AddDevice этого драйвера может открывать раздел параметров оборудования и проверять параметр, который я назову BoardType. Параметр относится к типу REG_DWORD и принимает значения 0 или 1. Код AddDevice выглядит примерно так (не считая того, что в него следовало бы включить проверку ошибок):
HANDLE hkey;
status = loOpenDeviceRegistryKeyfpdo,
PLUGPLAYJEGKEY-DEVICE, KEY_READ, &hkey);
UNICODE_STRING valname;
RtlInitUnicodeString(&valname, L"BoardType");
INF-файл
661
KEY__VALUE_PARTIAL_INFORMATION value;
ULONG junk:
status = ZwQueryValueKey(hkey, Svalname,
KeyValuePartialInformation, Svalue. sizeof(value), &junk);
ULONG BoardType = *(PULONG) value.Data;
ZwClose(hkey);
В этом фрагменте я использую то обстоятельство, что компилятор С дополняет переменную value до ближайшей границы, соответствующей самым жестким требованиям к одному из ее членов. В данном случае этот факт означает, что фактическая длина value.Data составит 4 байта, хотя переменная объявлена с длиной всего 1 байт.
Вероятно, INF-файл будет содержать две разные директивы модели и секции установки, как показано далее:
EDeviceList]
"Widget Model A"=WidgetInstallA,...
"Widget Model B"=WidgetInstallB,...
[ Wi dgetInsta11A.ntx86.hw]
AddReg=HwAddReg.A
[WidgetInstal1В.ntx86.hw]
AddReg=HwAddReg.В
[WidgetlnstallA.hw]	; для Win98/Me
AddReg=HwAddReg.A
[WidgetInstal1В.hw]	; то же
AddReg=HwAddReg.В
[HwAddReg.A]
HKR.,BoardType,0x00010001, 0.0,0,0
[HwAddReg.B]
HKR..BoardType,0x00010001, 1,0,0.0
Инициализация раздела драйвера
Я только один раз использовал раздел драйвера в секциях INF-файлов для Windows 98/Ме, и только потому, что Configuration Manager требует, чтобы для драйверов WDM в этом разделе присутствовали два значения. Практически во всех примерах, находящихся в прилагаемых материалах, INF-файл содержит следующие данные:
[Driverlnstall]
AddReg=Dr1verAddReg
662
Глава 15. Распространение драйверов устройств
[DriverAddReg]
HKR,.DevLoader,,*ntkern
HKR,.NTMPDrlver,.whatever.sys
Инициализация раздела службы
Раздел службы относительно редко используется в драйверах WDM. Впрочем, для отладочных версий драйверов, которые я создаю для своих клиентов, я разработал набор трассировочных функций общего назначения, которые работают в сочетании со страницами свойств Диспетчера устройств. Одна из особенностей этого набора функций заключается в том, что функция DriverEntry должна инициализировать слово флагов. Для функции DriverEntry доступен только один подходящий раздел реестра — раздел службы, на имя которого указывает аргумент RegistryPath.
Часть инициализации DriverEntry в таких драйверах выглядит следующим образом:
ULONG DriverTraceFlags:
KObjectAttrlbutes oa(ReglstryPath.
OBJ_CASE_INSENSITIVE OBJJCERNELJANDLE);
HANDLE hkey:
NTSTATUS status = ZwOpenKey(&hkey, KEY_READ, oa);
If (NT_SUCCESS(status))
{
GetReglstryValuethkey. L"Dr1verTraceFlags". DriverTraceFlags);
ZwClose(hkey):
}
(Этот фрагмент является частью библиотеки классов, которая позволяет мне избежать повторного набора или копирования/вставки стандартного кода. Несомненно, вы сможете понять, что здесь происходит.)
Создаваемая страница свойств содержит как стандартные, так и дополнительные элементы управления. Последние определяются в синтаксических конструкциях INF-файлов следующего вида:
[DrlverServIce]
Serv1ceType=l
StartType=3
ErrorControl=l
Serv1ceB1nary=^10^\system32\drivers\whatever.sys
AddReg=TraceFlags
[TraceFlags]
HKR,.CustomTraceNamel,,"Board Interrupts"
HKR,.CustomTraceFlagl,0x00010001, 01,00.00,00
HKR,,CustomTraceName2,,"Reads && Writes from/to board registers"
HKR,.CustomTraceFlag2,0x00010001, 02.00,00,00
Важнейшей частью данного примера является директива AddReg в секции [DriverService]. В секции, на которую она ссылается, HKR обозначает раздел службы.
INF-файл
663
Регистрация событий в журнале
Подготовьте свой драйвер к регистрации событий, создав соответствующий подраздел в разделе службы подсистемы регистрации. Для этой цели используется следующий синтаксис (фрагмент позаимствован из INF-файла примера EVENTLOG):
[Driverinstall.ntx86.Services]
AddSem ce=EventLogServi ce,2,Dri verServi ce. EventLoggl ng [Driverservice]
ServiceType^l
StartType=3
ErrorControl=l
ServlceBInary=%10^\system32\drivers\eventlog.sys
[EventLogglng]
AddReg^EventLogAddReg
[EventLogAddReg]
HKR,.EventMessageFile,0x00020000, \
"nO^\System32\iol ogmsg.dll Л 10Hsystern32\dri ver s\E vent Log. sys"
HKR,.TypesSupported,0x00010001,7
Параметр EventMessageFile относится к типу REGJEXPAND„SZ.
Настройки безопасности
В INF-файлах можно задать две разновидности настроек безопасности. Первая относится к параметрам реестра, создаваемым в разделе заполнения реестра:
[SoomeSechon]
AddReg^SomeAddReg
[SoineAddReg]
[SomeAddReg.security]
"<дескрмптор безопасности>”
Вторая разновидность настроек безопасности переопределяет дескрипторы безопасности объектов устройств либо для всего класса, либо только для одного экземпляра:
[Classinstall 32]
AddReg^Cl asslnstal1AddReg
[ClassInstallAddReg]
HKR,.Security,,<дескриптор безопасности
-или-
[Dri ver Install . hw]
664
Глава 15. Распространение драйверов устройств
AddReg=HwAddReg
EHwAddReg]
HKR,.Security,.^дескриптор безопасности
В обоих случаях <дескриптор безопасности> представляет собой строку со сжатым представлением стандартного дескриптора безопасности. Язык описания дескрипторов безопасности документируется в Platform SDK при описании функции ConvertStringSecurityDescriptorToSecurityDescriptor. Например, следующая строка дескриптора безопасности предоставляет неограниченный доступ системной учетной записи, доступ для чтения/записи/исполнения администраторам и доступ только для чтения всем остальным:
D:Р(А: ;GA;;;SY)(A: :GRGWGX::;ВАНА: :GR:; ;WD)
(Это строка SDDL_DEVOBJ_SYS_ALL_ADM_RWX_WORLD_R из WDMSEC.H, заголовочного файла .NET DDK.)
Формат этой строки удобен разве что для программ автоматизированного разбора. Вот краткое описание:
О D:P- дискреционный список контроля доступа (ACL), обладающий атрибутом защиты (то есть не изменяемый записями контроля доступа, унаследованными от родительских объектов);
О каждая из трех строк, заключенных в круглые скобки, представляет одну запись контроля доступа (АСЕ, Access Control Entry);
О (A;;GA;;;SY) — запись контроля доступа с разрешенным доступом (А) и флагами по умолчанию (второй параметр не задан). Системной учетной записи (SY) предоставлены права GENERIC_ALL (GA). Объекты, для которых задаются дескрипторы безопасности в INF-файле, определяются расположением директивы. По этой причине коды GUID объектов или унаследованных объектов в таких записях никогда не указываются (четвертый и пятый параметры опущены);
О (A;;GRGWGX;;;BA) - предоставляет права GENERIC-READ (GR), GENERICWRITE (GW) и GENERIC_EXECUTE (GX) членам встроенной административной группы (ВА);
О (A;;GR;;;WD) — предоставляет доступ GENERIC-READ всем остальным (WD).
Строки и локализация
Во всех приводившихся примерах я использовал строковые литералы там, где правильнее было бы выводить локализованный текст. В INF-файле можно создать таблицу именованных строк, а затем ссылаться на них при помощи условных обозначений. Пример:
EDeviceLi st]
WESCRIPTI 0N%=Dri verlnstal 1. *101503
[Strings]
DESCRIPTION=r,DEVPROP Sample"
INF-файл
665
Также можно определить группу секций с локализованными строками. Пример:
[Manufacturer]
%PROSEWARE^=Dev1 ceL 1 st
[Strings]
PROSEWARE=" Proseware, Inc. of North America"
[Strings.0407]
PROSEWARE=" Proseware, Inc. Deutschland GmbH"
[Strings.040C]
PROSEWARE=" Merchandise de Prose"
При установке драйвера система выбирает правильную локализованную версию строк. При необходимости она автоматически возвращается к использованию нелокализованной секции [Strings].
Идентификаторы устройств
Для полноценных устройств Plug and Play идентификатор устройства, указанный в секции модели INF-файла, играет очень важную роль. Как известно, устройства РпР способны на электронном уровне заявлять о своем присутствии и проводить самоидентификацию. Перечислитель шины может находить такие устройства автоматически и идентифицировать их на основании прочитанной встроенной информации. Например, у устройств USB коды производителя и продукта хранятся в дескрипторах устройства, а у устройств PCI — в конфигурационном пространстве.
При обнаружении устройства перечислитель строит список строк идентификации устройства. Одним из элементов этого списка является полный идентификатор устройства, в конечном итоге этот элемент определяет имя раздела оборудования в реестре. Другие элементы списка определяют «совместимые» идентификаторы. При проверке соответствия между INF-файлом и устройством PnP Manager использует все идентификаторы списка. Перечислители рассматривают более конкретные идентификаторы перед менее конкретными, чтобы производитель мог поставлять специализированные драйверы, заменяющие универсальные драйверы. Далее кратко представлены алгоритмы конструирования строк для различных типов перечислителей.
Устройства PCI
Полный идентификатор устройства записывается в форме
PCI\VEN_/ri/r&DEV_^&SUBSYS_ssssssss&REV_rr
где WW — идентификатор производителя, выделенный группой PCI Special Interest Group фирме, изготовившей карту, dddd — идентификатор устройства, присвоенный карте производителем, ssssssss — идентификатор подсистемы (часто 0), выдаваемый картой, гг — номер ревизии.
666
Глава 15. Распространение драйверов устройств
Например, видеоадаптер одного устаревшего портативного компьютера (на базе чипа Chips and Technologies 65550) обладает следующим идентификатором:
PCI\VEN_102CM)EVJ)OEO&SUBSYS_00000000&REV_04
Устройство также считается соответствующим модели INF с любым из следующих идентификаторов:
PC I \ V EN_i/у 1/1/&DE VjWd&SUBSYS_ssss5Sss
PCI\VEN_i/i/i/y&DEV_dddd&REV_rr
PCI\VEN_i/i/i/i/&DEV_dddd
PC I \ VENj/m&DE \ljdddd^E\l_rr^CC_ccss
PCI\VEN_i/i/i/i/&DEV_dd(7rf&CC_ccsspp
PC I \ VEN_y и w&DE\ljdddd&CC_ccss
PCI\VEN_i/vi/v&CC_ccsspp
PCI\VEN_ww&CC_ccss
PCIWENj/vw
PCI\CC_ccs5pp
PCI\CC_ccss
где cc — базовый код класса из конфигурационного пространства, ss — код подкласса, а рр — программный интерфейс. Например, следующие дополнительные идентификаторы упомянутого видеоадаптера считаются соответствующими информации в INF-файле:
PCI\VENJ02C&DEVJ0E0&SUBSYS_00000000
PCI\VEN_102C&DEVJ)OEO&REV_O4
PCI\VEN_102C&DEV_OOEO
PCI\VEN_102C&DEVJ0E0&REV_04&CCJ300
PCI\VEN_1Q2C&DEVJOEO&CC_030000
PCI\VEN_102C&DEV_0OE0&CC^03OO
PCI\VEN_102C&CC_030000
PCI\VEN_102C&CC_0300
PCI\VEN_102C
PCI\CC_030000
PCI\CC_0300
INF-файл, использованный системой для установки драйвера, был третьим в списке (то есть содержал только идентификаторы производителя и устройства).
Устройства PCMCIA
Идентификатор простого устройства записывается в форме
Р CMCIА\про из в од и тень - продукт-сгс
Например, идентификатор устройства для сетевой карты ЗСот на том же старом портативном компьютере выглядит так:
PCMCIA\MEGAHERTZ-CC10BT/2-BF05
Для отдельной функции многофункционального устройства идентификатор имеет форму
?C^l^\npon3BonnTenb-nposyKT-\)F\ldddd~crc
INF-файл
667
где производитель ~ имя фирмы-производителя, а продукт — имя продукта. Перечислитель PCMCIA читает эти строки непосредственно с карты. Сгс — контрольная сумма для карты, состоящая из четырех шестнадцатеричных цифр. Номер дочерней функции {dddd в этом шаблоне) представляет собой десятичное число без начальных пробелов.
Если карта не имеет имени производителя, то идентификатор записывается в одной из трех форм:
PCMCIA\UNKNOWN_MANUFACTURER-Crc
PCMCIA\UNKNOWNJANUFACTURER-DEV^d-Crc
PCMCIAWD-0000 или PCMCIA\MTD-0002
(Последняя запись относится к карте флэш-памяти, не имеющей идентификатора производителя. Идентификатор с 0000 соответствует карте SRAM, а идентификатор 0002 — карте ROM.)
Кроме только что описанных идентификаторов устройств, секция модели INF-файла может содержать идентификатор, который получается при замене контрольной суммы строкой, содержащей код производителя (4 шестнадцатеричные цифры), дефис и код информации производителя (также 4 шестнадцатеричные цифры). Оба кода берутся с карты. Пример:
PCMCIA\MEGAHERTZ-CC10BT/2-0128-0103
Устройства SCSI
Полный идентификатор устройства имеет вид
SCSI\ttttvvvvvvvvpppppppppppppppprrrr
где tttt — код типа устройства, vuuuvvuv - идентификатор производителя из 8 символов, рррррррррррррррр — идентификатор продукта из 16 символов, a rrrr — код ревизии из 8 символов. Из всех компонентов идентификатора только код типа устройства имеет переменную длину. Драйвер шины определяет эту часть идентификатора посредством индексирования по внутренней таблице строк с кодами типов (в табл. 15.7 приведены только коды типов стандартных устройств SCSI, для других типов перечислитель SCSI может возвращать дополнительные имена — за полной информацией обращайтесь к документации DDK). Остальные компоненты представляют собой обычные строки, хранящиеся в данных устройства с заменой специальных символов (пробелы, запятые, псевдографика) символами подчеркивания.
Таблица 15.7. Имена типов для устройств SCSI
Код типа SCSI	Тип устройства	Обобщенный тип
DIRECT_ACCESS_DEVICE (0)	Disk	GenDisk
SEQUENTIAL_ACCESS_DEVICE (1)	Sequential	
PRINTER-DEVICE (2)	Printer	GenPrinter
PROCESSOR-DEVICE (3)	Processor	
продолжение
668
Глава 15. Распространение драйверов устройств
Таблица 15.7 (продолжение)
Код типа SCSI	Тип устройства	Обобщенный тип
WRITE_ONCE_READ_MULTIPLE-DEVICE (4)	Worm	GenWorm
READ_ONLY_DIRECT_ACCESS_DEVICE (5)	Cd Rom	GenCdRom
SCANNER-DEVICE (6)	Scanner	GenScanner
OPTICAL-DEVICE (7)	Optical	GetOptical
MEDIUM_CHANGER (8)	Changer	ScsiChanger
COMMUNICATION-DEVICE (9)	Net	ScsiNet
Например, идентификатор жесткого диска на одной из моих рабочих станций выглядит так:
SCSI\D1skSEAGATE_ST39102LW 0004
Драйвер шины также создает следующие дополнительные идентификаторы:
SCSI\ttttvvvvvvvvpppppppppppppppp
SCSI\tttt vv vvvvi/v
SCSI\vvvvvvvvppppppppppppppppr vvvvvvvvppppppppppppppppr №9
В третьем и четвертом дополнительных идентификаторах г обозначает первый символ идентификатора ревизии. В последнем идентификаторе gggg — обобщенный код типа из табл. 15.7.
Так, в уже рассмотренном примере с жестким диском драйвер шины сгенерировал следующие дополнительные идентификаторы устройств:
SCSI\D1 skSEAGATEJST39102LW
SCSI\D1 skSEAGATE__
SCS I\Di skSEAGATE_ST39102LW 0
SEAGATE_ST39102LW 0
GenDisk
Последний идентификатор (GenDisk) указан в качестве идентификатора устройства в INF-файле, который используется PnP Manager для установки драйвера диска. Более того, в INF-файле обычно указывается именно обобщенный идентификатор, потому что драйверы SCSI обычно имеют общий характер.
Устройства IDE
Устройствам IDE назначаются идентификаторы устройств, аналогичные идентификаторам SCSI:
WE\ttttvp vprrrrrrrr
WE\vpvprrrrrrrr
IDE\ttttvpyp
vpvprrrrrrrr
дддд
INF-файл
669
Здесь tttt — имя типа устройства (то же, что SCSI); vpvp — строка, состоящая из имени производителя, подчеркивания, имени продукта и символов подчеркивания, дополняющих длину до 40 символов; гптптг ~~ 8-символьный номер ревизии, a gggg — обобщенное имя типа (аналог имен типов SCSI из табл. 15.7). Для чейнджеров с интерфейсом IDE вместо ScsiChanger используется обобщенное имя Gen-Changer, другие обобщенные имена IDE полностью совпадают с именами SCSI.
Например, идентификаторы устройства для жесткого диска IDE на одном из моих настольных компьютеров выглядят так:
IDE\D1skMaxtor_91000D8______________________SASX1B18
IDE\Maxtor_91000D8______________________SASX1B18
IDE\DiskMaxtor_91000D8______________________
Maxtor 910Q0D8 SASX1B18
GenDisk
Устройства ISAPNP
Перечислитель ISA Plug-And-Play (ISAPNP) создает два идентификатора:
ISAPNP\7Cf
*altid
где id и altid — идентификаторы устройства в стиле EISA (трехбуквенный идентификатор производителя + идентификатор конкретного устройства из 4 шестнадцатеричных цифр). Если устройство является одной из функций многофункциональной карты, первый идентификатор в списке принимает форму
ISAPNPVc/JJEVunnr?
где nnnn — десятичный индекс функции (с начальными нулями).
Например, функция кодека звуковой карты Crystal Semiconductor на одном из моих настольных компьютеров имеет два идентификатора оборудования:
ISAPNP\CSC6835_DEVOOOO
*CSCOOOO
Второй идентификатор соответствует содержимому INF-файла.
Устройства USB
Полный идентификатор устройства имеет вид
US В\ VI D_vvw&P ID j±W&RE\_rrrr
где wvv — идентификатор производителя, выделенный комитетом USB, dddd — идентификатор устройства, присвоенный карте производителем (4 шестнадцатеричные цифры), а гг — номер ревизии. Все три значения включаются в дескриптор устройства или дескриптор интерфейса для устройства.
Секция модели в INF-файле также может содержать следующие альтернативы:
USB\VID_wvWIDj^7
USB\CLASS_cc&SUBCLASS_ss&PROT_рр
USB\CLASS_cc&SUBCLASS_ss
670
Глава 15. Распространение драйверов устройств
USBVCLASSjX
USB\COMPOSITE
где сс — код класса из дескриптора устройства или интерфейса, 55 — код подкласса, ар/? — код протокола. Значения задаются в формате из 2 шестнадцатеричных цифр.
Составные устройства USB имеют более одного дескриптора интерфейса (не считая альтернативных настроек) в своей единственной конфигурации. Если ни один конкретный идентификатор устройства не соответствует INF-файлу, система использует совместимый идентификатор USB\COMPOSITE с универсальным родительским драйвером. В свою очередь, универсальный родительский драйвер создает PDO для каждого интерфейса. Идентификатор устройства PDO имеет следующую форму:
US В \ VI D_ и v 1/1/&Р I Djdddd^ 1_пп
где vvvv и dddd имеют тот же смысл, что и ранее, а пп содержит значение blnterfaceNumber из дескриптора интерфейса. Универсальный родительский драйвер также создает совместимые идентификаторы на основании кодов класса, подкласса и протокола в дескрипторе интерфейса:
USBXCLASS cc&SUBCLASS ss&PROTjp
USB\CLASS”cc&SUBCLASS\s
USB\CLASS_cc
Устройства 1394
Драйвер шины 1394 создает для устройства следующие идентификаторы:
1394\производитель&модель
139^\код_специфлкдции^версия
где производитель — имя производителя оборудования, модель — идентификатор устройства, код спецификации ~ авторитетный источник программной спецификации, а версия ~ идентификатор программной спецификации. Информация, используемая для конструирования идентификаторов, берется из ПЗУ устройства.
Если для устройства определены строки имен производителя и модели, драйвер шины использует первый идентификатор как идентификатор оборудования, а второй — как единственный совместимый идентификатор. Если у устройства отсутствует строка имени производителя или модели, драйвер шины использует второй идентификатор как идентификатор оборудования.
Поскольку ни на одном из моих компьютеров нет шины 1394, я воспользуюсь примерами, которые предоставил мой коллега-писатель Джефф Келлам (Jeff Kellam). В первом примере для видеокамеры Sony создается идентификатор устройства
1394\80NY&CCM-DS250_1.08
Второй пример относится к самой шине 1394, работающей в диагностическом режиме; идентификатор устройства имеет вид
1394\031887&040892
INF-файл
671
За информацией об идентификаторах составных устройств с шиной 1394 обращайтесь к DDK.
Идентификаторы обобщенных устройств
PnP Manager также работает с идентификаторами обобщенных устройств, под-ключаемых к разным шинам. Такие идентификаторы имеют форму
*PNPdcW
где dddd -- идентификатор типа, состоящий из 4 шестнадцатеричных цифр. Microsoft постоянно перемещает список идентификаторов, поэтому любая ссылка, которую я публикую, через несколько месяцев становится недействительной. Желаю удачи с поисками!
Ранжирование драйверов
Программа установки может обнаружить несколько INF-файлов, описывающих новое устройство с большей или меньшей точностью. В DDK описан полный алгоритм, который используется программой установки для ранжирования драйверов в подобных ситуациях (см. раздел «How Setup Selects Drivers»). Полный алгоритм чрезвычайно сложен и зависит от нескольких факторов:
О Снабжен ли пакет драйвера цифровой подписью?
О Соответствует ли идентификатор устройства или совместимый идентификатор, сгенерированный драйвером шины, идентификатору устройства или совместимому идентификатору в директиве модели, и если соответствует, то насколько точно? Как было показано на нескольких последних страницах, драйверы шин генерируют списки идентификаторов, которые начинаются с конкретного идентификатора и постепенно становятся все более общими. Кроме того, они генерируют как списки идентификаторов устройств, так и списки совместимых идентификаторов.
О Какая информация содержится в директиве DriverVer в подписанном INF-файле? О В какой операционной системе устанавливается устройство?
Я не стану повторять все правила ранжирования — вряд ли нам с вами доведется попасть в ситуацию, когда все они играют важную роль. Вот три приблизительных правила: во-первых, подписанные драйверы обладают абсолютным приоритетом по сравнению с неподписанными драйверами в Windows ХР. Во-вторых, в группе драйверов, которые все подписаны или не подписаны, совпадениям с более конкретными идентификаторами отдается предпочтение по сравнению с менее конкретными совпадениями. В-третьих, при прочих равных условиях программа установки выбирает один из подписанных драйверов, сравнивая значения DriverVer. Следовательно, следует снабжать драйверы цифровой подписью и указывать полный идентификатор устройства в директиве модели. При распространении обновлений обязательно изменяйте значение DriverVer, чтобы все системы программы понимали, что новый драйвер действительно является новым.
672
Глава 15. Распространение драйверов устройств
Некоторые драйверы шин Microsoft генерируют совместимые идентификаторы устройств, отображаемые на подписанные функциональные драйверы. Это создает кошмарную проблему для производителя, который хочет предоставить неподписанный драйвер для более конкретного идентификатора устройства. Приведу пример: допустим, вы создали новую замечательную мышь USB (кстати, пример вполне реальный — один из обратившихся ко мне клиентов разрешил мне рассказать его историю). Пользователь подключает мышь к компьютеру с Windows ХР. Драйвер концентратора USB создает конкретные идентификаторы устройств на основании идентификаторов производителя и продукта в дескрипторе устройства. Пока все хорошо. Если бы на этом все и закончилось, система запросила бы драйвер устройства и нашла бы ваш драйвер. В интервале между освобождением мыши и ее последующим переводом на драйвер, сертифицированный WHQL, появилось бы диалоговое окно с сообщением о неподписанном драйвере, игнорировать его или нет — дело пользователя.
К сожалению, дело на этом не заканчивается. Драйвер концентратора USB также создает совместимый идентификатор устройства, базирующийся на коде класса USB в дескрипторах мыши. Для совместимого идентификатора находится соответствие в INF-файле Microsoft, назначающем HIDUSB.SYS в качестве драйвера для устройства класса HID. Так как драйвер Microsoft снабжен цифровой подписью, система всегда отдает ему предпочтение перед неподписанным драйвером, даже если неподписанный драйвер написан специально для обнаруженного продукта, а конечный пользователь предпочел бы использовать неподписанный драйвер вместо подписанного.
Далее конечный пользователь должен абсолютно точно выполнить действия в диалоговых окнах обновления драйвера, чтобы заменить подписанный, но общий драйвер Microsoft вашим неподписанным, но специализированным драйвером. В этой процедуре легко запутаться, в конечном счете это обернется звонками в службу поддержки, на которые уйдет вся прибыль от продажи устройства. Более того, обновлять драйвер придется не только вашим пользователям, но и вам при проведении внутреннего тестирования пакета драйверов. Наконец, пользователи будут вынуждены проходить эту процедуру не только при установке мыши, но и каждый раз, когда мышь будет подключаться к другому порту на шине USB.
ПРИМЕЧАНИЕ-----------------------------------------------------------------—
Компания Microsoft предлагает программу, позволяющую получить некое подобие фиктивной цифровой подписи для драйвера. Для консультанта-одиночки этот вариант вряд ли подойдет, потому что вам потребуется получить сертификат VeriSign (что стоит отнюдь не дешево), а также обеспечить соблюдение различных юридических условий, не имеющих ничего общего с написанием драйверов. Компания Microsoft разрабатывала программу тестовых цифровых подписей для крупных компаний, которые в конечном счете создают абсолютное большинство драйверов. Я попытался получить тестовый сертификат, чтобы опробовать его и рассказать читателям, как работает эта система. Тем не менее, я отказался от этой затеи, когда стали очевидными вся жесткость и неудобство системы юридических требований.
Чтобы избежать подобных неприятностей, можно сделать пару вещей. Во-первых, избегайте включения своих устройств в классы, для которых Microsoft предоставляет универсальный драйвер на базе совместимого идентификатора.
INF-файл
673
Однако для устройств HID такой подход не позволяет использовать устройство во время загрузки системы и, как следствие, может оказаться неприемлемым. Другой путь — выделить достаточно времени и средств для получения сертификации WHQL перед тем, как начинать продажи устройства.
Инструменты для работы с INF-файлами
В подкаталоге TOOLS Windows ХР DDK находятся две полезные программы для работы с INF-файлами. Программа CHKINF предназначена для проверки синтаксиса INF-файлов, а программа GENINF упрощает создание новых INF-файлов. Средства для включения меток Version в INF-файлы, присутствовавшие в более ранних DDK, в .NET DDK были исключены.
CHKINF
CHKINF в действительности представляет собой ВАТ-файл для запуска сценария на языке PERL, анализирующего INF-файлы. Естественно, для использования CHKINF потребуется интерпретатор PERL. Вы можете загрузить его с сайта http:// www.peri.com, как когда-то сделал я, кроме того, копия входит в состав пакета НСТ.
CHKINF проще всего запускается в режиме командной строки. Пример:
С:\w1nddk\3615\tools\chk1nf>chk1nf C:\newbook\chapl5\devprop\sys\devprop. 1 nf
CHKINF генерирует выходные файлы в формате HTML и сохраняет их в подкаталоге НТМ (рис. 15.11). В выходные файлы включается сводка всех ошибок и предупреждений, найденных CHKINF, за которыми следует откомментированная версия INF-файла. Я решил привести реальный вывод для файла DEVPROP.INF, чтобы вы могли понять, поможет ли CHKINF в подготовке ваших пакетов драйверов.
CHKINF показывает, что в INF-файле отсутствуют секции Version и Catalogue, необходимые для получения сертификата WHQL. Кроме того, программа (на мой взгляд, некорректно) сообщает, что для класса SAMPLE не определен код GUID, хотя INF-файл содержит секцию Classlnstall32 с полным определением класса SAMPLE. Предупреждения нередко свидетельствуют о каких-то непредвиденных аспектах INF-файлов: например, предупреждение о неиспользуемых секциях может возникнуть из-за опечатки в имени секции. Кстати, INF-файл в своей текущей версии меня вполне устраивает, и я не собираюсь ничего менять на основании этих предупреждений.
Учтите, что CHKINF проверяет только базовый синтаксис и структуру INF-файлов. Программа не следит за тем, чтобы файлы копировались в правильные каталоги, и даже не проверяет, что все используемые файлы действительно копируются (впрочем, эти условия проверяются в тестах WHQL). CHKINF не воспринимает секцию [Classinstall], необходимую для новых классов устройств в Windows 98/Ме. Она не понимает, что в Windows 98/Ме должны использоваться «непреобразованные» имена секций (скажем, [Driverinstall]), и предупреждает о других неиспользуемых секциях Windows 98/Ме в INF-файлах. Иначе говоря, программа CHKINF не идеальна, но она выдает полезную информацию. Кроме того, для прохождения тестов WHQL INF-файл обязан пройти проверку CHKINF.
674
Глава 15. Распространение драйверов устройств
Рис. 15.11. Выходные данные утилиты CHKINF
GENINF
GENINF — программа-мастер с графическим интерфейсом, помогающая в создании INF-файлов. Она впервые появилась в ранних бета-версиях Windows 2000 DDK и с тех пор прошла значительный путь. Многим читателям стоит хотя бы опробовать ее в своей работе. Впрочем, в работе над примерами для книги я ею не пользовался, потому что GENINF не поддерживает нестандартные классы установки или кроссплатформенные INF-файлы. Кроме того, она несколько ограничена в отношении поддерживаемых классов устройств.
В GENINF также не воплощены некоторые аспекты, относящиеся к конкретным классам и необходимые для построения работоспособного INF-файла. Например, я воспользовался GENINF для построения INF-файла минидрайвера HID, но программа не сгенерировала синтаксические конструкции для проверки наличия HIDCLASS и HIDPARSE в системе конечного пользователя. Программа не спросила, не является ли мое устройство джойстиком, что потребовало бы ввода дополнительной информации об осях и конфигураций кнопок.
Я не придираюсь: на мой взгляд, GENINF является шагом в правильном направлении. И все же вы должны понимать, что программа не напишет за вас готовый INF-файл.
Определение класса устройств
675
Отладка INF-файла
Одна из основных проблем с отладкой INF-файлов — ее сходство со старой компьютерной игрой «Adventure». Попытайтесь сделать что-нибудь не так, и вы узнаете: «Так делать нельзя». Другими словами, система просто не принимает INF-файл, и вам придется разбираться, почему, — обычно для этого приходится постепенно удалять все больше строк, пока вы не найдете причину. Однажды я потратил несколько часов, выясняя, чем Windows Me не нравится мой файл, — оказалось, что в одной из секций Windows 2000 присутствовало имя, длина которого превышала 28 символов (тогда этот факт еще не был документирован в DDK). Затем мне пришлось тем же путем выяснять, что поле SourceDisksNames в Windows Me является строго обязательным.
В Windows ХР программа установки устройства записывает информацию о выполняемых операциях в файл SETUPAPI.LOG, находящийся в каталоге Windows. Вы можете задать имя файла журнала и управлять детализацией, вручную изменяя содержимое раздела реестра с именем HKEY_LOCAL_MACHINE\Software\ Microsoft\Windows\CurrentVersion\Setup. За подробной информацией о настройке журнала обращайтесь к документации DDK. Я обычно задаю параметру Log Level значение OxFFFFFFFF и получаю гораздо больше отладочной информации, чем требуется, -- и все же это проще, чем разбираться со смыслом 32 битов маски.
Иногда проблемы с INF-файлами или другими аспектами установки преобразуются в код проблемы в Диспетчере устройств (рис. 15.12). В DDK около 50 кодов проблем документировано в разделе «Device Manager Error Messages». Иллюстрация показывает, что происходит после установки примера STUPID из главы 2. Код проблемы 31 соответствует константе CM„PROB_FAILED__ADD и означает неудачный вызов AddDevice. Конечно, это довольно точно описывает происходящее в примере STUPID.
Определение класса устройств
Допустим, ваше устройство не соответствует ни одному из классов устройств, определенных компанией Microsoft. В процессе начального тестирования устройства и драйвера можно указать в INF-файле класс Unknown, однако окончательные версии устройств не должны относиться к этому классу. Вместо этого нестандартное устройство следует поместить в новый класс, определяемый в INF-файле. В этом разделе я объясню, как создаются нестандартные классы устройств.
В приведенном ранее примере INF-файла используется нестандартный класс устройства:
[Version]
Signaturе=$СНIСAGO$
Class=Samplе
ClassGuid={894A7460-A033-lld2-821E-444553540000}
Более того, все примеры книги используют класс Sample.
676
Глава 15. Распространение драйверов устройств
Рис. 15.12. Проблема с установкой в Диспетчере устройств
Чтобы определить новый класс устройств, необходимо сгенерировать для него уникальный код GUID утилитой GUIDGEN. Также можно доработать пользовательский интерфейс класса устройств — напишите поставщика страниц свойств для Диспетчера устройств и включите несколько специальных параметров в раздел реестра, соответствующий классу вашего устройства. Также можно указать фильтрующие драйверы и переопределения параметров, которые будут использоваться для каждого устройства класса. Управление всеми дополнительными возможностями осуществляется директивами INF-файла. Например, INF-файл каждого примера в прилагаемых материалах включает следующие шаблонные записи:
[Classlnstal132]	// 1
AddReg=Cl assinstall 32 Add Reg
CopyFI1es=ClassInstal132CopyF11es
[Classlnstal132AddReg]
HKR,,,,"WDM Book Samples"	//	2
HKR,,Installer32,."samclass.dll.SampleClassInstaller"	//	3
HKR,,EnumPropPages32,,samel ass.dll	//	4
HKR,,Icon,,101	//	5
[Cl assInsta1132CopyF11es]
samclass.dll,,,2	//6
Определение класса устройств
677
1.	Во всех INF-файлах, использующих нестандартные классы, должна присутствовать секция [Classlnstail32] с определением класса. Не поступайте так, как я поступил в первом издании книги, и не используйте другие средства для определения класса. Система использует эту секцию при самой первой установке устройства, принадлежащего нестандартному классу.
2.	В секции AddReg, соответствующей секции [Classlnstall32], HKR обозначает раздел класса. Значение по умолчанию представляет собой дружественное имя класса, отображаемое в Диспетчере устройств и диалоговых окнах мастера установки оборудования.
3.	Параметр InstalIer32 задает имя DLL установки класса. Для примера SAMPLE я объединил поставщика страниц свойств с системой установки в одну DLL с именем SAMCLASS.DLL
4.	Параметр EnumPropPages32 задает имя поставщика страниц свойств для Диспетчера устройств.
5.	Параметр Icon задает ресурс значка в DLL поставщика страниц свойств.
6.	Эта команда обеспечивает копирование DLL страниц свойств и системы установки при установке первого устройства SAMPLE. Команда секции в [Des-tinationDirs] INF-файла направляет этот файл в системный каталог.
Поставщик страниц свойств
Во введении к книге я привел изображение страницы свойств, созданной мной для класса устройств Sample. Исходный код поставщика страниц свойств, создавшего эту страницу, содержится в примере SAMCLASS в прилагаемых материалах, а сейчас я объясню, как он работает.
Поставщик страниц свойств для класса устройств представляет собой 32-раз-рядную DLL-библиотеку, которая содержит следующие компоненты:
О экспортируемую точку входа для каждого класса, для которого DLL поставляет страницы свойств;
О диалоговые ресурсы для каждой страницы свойств;
О диалоговую процедуру для каждой страницы свойств.
В общем случае одна DLL-библиотека может поставлять страницы свойств для нескольких классов устройств. В частности, Microsoft поставляет несколько таких DLL в составе операционной системы. Пример SAMCLASS поставляет всего одну страницу свойств для одного класса устройств и экспортирует единственную точку входа:
extern "С" BOCL CALLBACK EnumPropPages (PSP_PROPSHEETPAGEJEQUEST р.
LPFNADDPROPSHEETPAGE AddPage, LPARAM 1 Param)
{
PROPSHEETPAGE page;
HPROPSHEETPAGE hpage;
memset(&page, 0, sizeof(page)):
678
Глава 15. Распространение драйверов устройств
page.dwSize = sizeof(PROPSHEETPAGE);
page.hlnstance = hlnst;
page.pszTempiate = MAKEINTRESOURCE(IDD_SAMPAGE); page.pfnDlgProc = PageDlgProc;
<и г. д.>
hpage = CreatePropertySheetPage(&page);
if (Ihpage) return TRUE;
if (!(*AddPage)(hpage, IParam))
DestroyPropertySheetPage(hpage); return TRUE;
Приступая к созданию страницы свойств для устройства, Диспетчер устройств обращается к разделу класса в реестре и смотрит, где находится поставщик страницы класса. Он загружает указанную DLL (SAMCLASS.DLL в случае SAMPLE) и передает управление заданной точке входа (Enum Prop Pages). Если функция возвращает TRUE, Диспетчер устройств отображает страницу свойств, а если FALSE — страница не отображается. Функция может вернуть ноль или более страниц, вызывая функцию AddPage, как показано в предыдущем фрагменте.
Внутри структуры SP_PROPSHEETPAGE_REQUEST, передаваемой в аргументе функции перечисления, имеются два полезных поля: манипулятор информационного набора устройства и адрес структуры SP_DEVINFO_DATA для интересующего вас устройства. Эти данные (но, к сожалению, не содержащая их структура SP_PROPSHEETPAGE_REQUEST) остаются действительными все время, пока отображается страница свойств, и для вас было бы полезно иметь возможность обращаться к ним из диалоговой процедуры, написанной для страницы свойств. Начните с создания дополнительной структуры, адрес которой передается Create-PropertySheetPage в поле IParam структуры PROPSHEETPAGE:
struct SETUPSTUFF {
HDEVINFO info;
PSP_DEVINFOJ3ATA did;
char infopath[MAX_PATH];
}:
BOOL EnumPropPagesf...)
{
PROPSHEETPAGE page;
SETUPSTUFF* stuff = new SETUPSTUFF;
stuff->1nfo = p->Device!nfoSet;
stuff->did = p->DeviceInfoData;
page.IParam = (LPARAM) stuff;
page.pfnCallback = PageCallbackProc;
page.dwFlags = PSPJJSECALLBACK;
Определение класса устройств
679
LUNT CALLBACK PageCal1backProc(HWND junk, UINT msg, LPPROPSHEETPAGE p)
{
If (msg == PSPCB_RELEASE && p->lParam)
delete (SETUPSTUFF*) p->]Param;
return TRUE;
Сообщение WM JNITDIALOG, отправляемое системой Windows вашей диалоговой процедуре, получает в IParam указатель на ту же структуру PROPSHEETPAGE. Это позволяет получить указатель stuff и организовать его сохранение с использованием функций SetWindowLong/GetWindowLong.
Также необходимо предусмотреть механизм удаления структуры SETUPSTUFF, когда она станет ненужной. Простейший способ, который работает независимо от того, получите вы сообщение WMJNITDIALOG или нет (а вы его не получите, если при конструировании страницы свойств произошла ошибка), основан на использовании функции обратного вызова, как показано в предыдущем фрагменте.
В нестандартных страницах свойств можно делать все, что вы сочтете нужным. Так, для класса SAMPLE я решил создать кнопку, которая бы выдавала пояснения для каждого устройства. Чтобы решение оставалось как можно более общим, я решил включить в раздел оборудования устройства параметр Sampleinfo с именем файла. Чтобы запустить программу просмотра для файла с объяснениями, достаточно вызвать функцию ShellExecute, которая анализирует расширение файла и находит подходящее приложение для просмотра. В моих примерах файлы с объяснениями хранятся в формате HTML, поэтому программой просмотра будет веб-браузер.
Большая часть работы SAMCLASS выполняется в обработчике WMJNITDIALOG (проверка ошибок, как обычно, не приводится):
case WMJNITDIALOG:
SETUPSTUFF* stuff =
(SETUPSTUFF*) ((LPPROPSHEETPAGE) 1 Param)->1Param;
SetWindowLong(hdlg, DWLJJSER, (LONG) stuff);	// 1
TCHAR name[256];	// 2
SetupDiGetDeviceReglstryProperty(stuff->info, stuff->did,
SPDRPJRIENDLYNAME, NULL, (PBYTE) name, sizeof(name), NULL);
SetDlgltemTextChdlg, IDCJAMNAME, name);
HWND hClassIcon = GetDlgItem(hdlg, IDCJLASSICON);	// 3
HICON hlcon;
SetupD1LoadClassIcon(&stuff->did->ClassGu1d, &hlcon, NULL);
SendMessage(hClassIcon, STM_SETICON,
(WPARAM) (HANDLE) hlcon, 0);
HKEY hkey = SetupD1OpenDevRegKev(stuff->info, stuff->d1d, // 4 DICSJLAGJLOBAL, 0, DIREG JEV, KEY_READ);
DWORD length = slzeof(name);
RegQueryValueEx(hkey, “Sampleinfo’, NULL NULL.
680
Глава 15. Распространение драйверов устройств
(LPBYTE) name, ^length);
DoEnv1ronmentSubst(narne, sizeof(name));
strcpy(stuff->1nfopath, name):
RegCloseKey(hkey);
break;
}
1.	Директива сохраняет указатель SETUPSTUFF там, где диалоговая процедура может получить его для обработки последующих оконных сообщений.
2.	Здесь мы определяем дружественное имя устройства и помещаем его в статическое текстовое поле. В программе при отсутствии дружественного имени используется описание устройства. В шаблоне диалогового окна я тщательно разместил элемент в той позиции, где Диспетчер устройств отображает аналогичные элементы других страниц. Это сделано для того, чтобы текст (выводимый на каждой странице) не «прыгал» при переходе между страницами.
3.	Эта группа команд определяет значок класса. В этом конкретном примере я мог бы жестко закодировать значок класса в шаблоне диалогового окна, поскольку SAMCLASS используется только для устройств класса SAMPLE. Тем не менее, я предпочел показать более универсальный путь получения того же значка, который используется Диспетчером устройств на других страницах свойств. Как и в случае со статическим полем, содержащим дружественное имя, мне пришлось поработать над размещением значка в шаблоне диалогового окна.
4.	Следующие несколько команд определяют имя файла Sampleinfo из подраздела параметров раздела оборудования. Строки, которые я сохранил в реестре, имеют форму %wdmbook%\chapl5\devprop\devprop.htm, где строка %wdmbook% заменяется значением переменной окружения WDMBOOK. Вызов стандартной функции Win32 API DoEnvironmentSubst обеспечивает подстановку переменной окружения (поверьте, в реальном коде перед вызовом strcpy я проверяю длину). Когда конечный пользователь (скорее всего, в данном случае им будете вы) нажимает кнопку More Information на странице свойств, диалоговая процедура получает сообщение WM_COMMAND и обрабатывает его следующим образом:
SETUPSTUFF* stuff = (SETUPSTUFF*) GetW1ndowLong(hdlg, DWLJJSER);
case WIWMMAND:
switch (LOWORD(wParam))
{
case IDBJWREINFO:
{
ShellExecute(hdlg, NULL, stuff->1nfopath,
NULL, NULL, SW_SHOWNORMAL);
return TRUE;
}
}
break;
Настройка процесса установки
681
Функция ShellExecute запускает приложение, ассоциированное с файлом Sample-Info (а именно веб-браузер). Пользователь просматривает в файл и находит в нем интересующую его информацию.
Настройка процесса установки
В этом разделе я опишу некоторые способы адаптации процесса установки к требованиям устройства. Прежде всего, если ваше устройство принадлежит к новому классу установки, вы пишете установочную DLL, которой поручаются все тонкости установки и управления устройствами этого класса. Думаю, большинству читателей книги этим заниматься не придется. В крайнем случае может возникнуть необходимость в предоставлении вспомогательной установочной DLL класса, изменяющей стандартное поведение программы установки. Если ваше устройство принадлежит к существующему классу или к новому классу, не имеющему специальных требований к установке, предоставьте вспомогательную установочную DLL устройства, которая участвует лишь в некоторых действиях по установке устройства и управлению им.
Если ваш пакет драйвера обладает цифровой подписью (см. раздел «WHQL» в конце главы), процесс установки можно упростить за счет предварительной установки программного обеспечения на компьютер конечного пользователя. Если пакет будет спроектирован так, что он не будет требовать взаимодействия с пользователем, это даст возможность выполнять автоматизированные серверные установки, не требующие даже присутствия администратора за компьютером.
Нередко фирмы-производители поставляют свои устройства вместе со специальными приложениями. Компания Microsoft называет программы, не входящие в подписанный пакет драйвера, дополнительным программным обеспечением (value-added software). При помощи вспомогательной установочной DLL устройства можно организовать установку таких программ вместе с драйверами и другими компонентами пакета, снабженными цифровыми подписями. Иногда приложение должно автоматически запускаться после завершения процедуры установки, для этой цели используется раздел реестра с именем RunOnce. В особом случае приложение должно запускаться каждый раз, когда пользователь подключает устройство к компьютеру. Я кратко опишу, как эта задача решается в примере AUTOLAUNCH.
ПРИМЕЧАНИЕ------------------------------------------------------------
Так называемая серверная установка возможна для подписанных пакетов, не требующих взаимодействия с пользователем. Клиентская установка требует присутствия администратора и происходит тогда, когда пакет не подписан или требует участия пользователя (выбор INF-файла, поиск одного или нескольких файлов драйвера, запуск мастера установки оборудования или взаимодействие со страницами свойств компонента). Согласно DDK, «...термин „серверная установка" используется потому, что процесс установки может быть выполнен средствами PnP Manager системы, без участия клиента пользовательского режима, обращающегося с запросами к PnP Manager».
682
Глава 15. Распространение драйверов устройств
Основные и вспомогательные установочные DLL
Подсистема установки обеспечивает установку, удаление и управление всеми устройствами в системе. В ее работе задействованы основные и вспомогательные установочные DLL, которые выполняют необходимые действия. Вы можете писать такие DLL самостоятельно и устанавливать их вместе с пакетом драйвера, включая соответствующие директивы в INF-файл.
В общем случае программа установки должна сначала идентифицировать класс установки для устройства, только после этого она будет знать, какие основные и вспомогательные установочные DLL она будет вызывать. Идентификация класса установки может проходить по нескольким сценариям:
О При установке устройства РпР программа установки сопоставляет идентификатор устройства или совместимый идентификатор, возвращаемый драйвером шины, с директивой модели в INF-файле. Класс установки устройства определяется чтением раздела [Version] в INF-файле. При необходимости программа установки также обрабатывает директивы в секции [Ciasslnstall32] для создания нового класса.
О При установке устройства, не поддерживающего РпР, используется мастер нового оборудования. Мастер позволяет выбрать класс установки, к которому должно относиться устанавливаемое устройство.
О При установке устройства, не поддерживающего РпР и относящегося к новому классу, необходимо сообщить программе установки, какой INF-файл она должна использовать. Программа установки определяет класс по содержимому секции [Version] и устанавливает новый класс на основании содержимого секции [Classlnstall32].
О При модификации существующего устройства в Диспетчере устройств или в другой управляющей программе пользователь в диалоговом окне выбирает конкретное устройство, класс установки которого хранится в реестре.
В процессе установки нового или модификации существующего устройства программа установки определяет, с каким устройством вы работаете, а затем переходит к использованию вспомогательной установочной DLL устройства. Таким образом, во многих операциях, выполняемых программой установки, задействованы установочная DLL, одна или несколько вспомогательных установочных DLL класса и одна или несколько вспомогательных установочных DLL устройства.
Коды функций установки
Программа установки взаимодействует с DLL посредством вызова экспортируемых функций. В аргументах функции передается код DIF, который указывает, какую функцию должна выполнить DLL. В этом подразделе приведена сводка DIF-кодов для некоторых стандартных операций. За подробными описаниями выполняемых действий обращайтесь к DDK (раздел «Device Installation Function Codes»).
ПРИМЕР-----------------------------------------------------------------
Я построил отладочную версию SAMCLASS.DLL и сохранил трассировочные данные, полученные при выполнении различных операций с устройствами. Однако не следует полагать, что эти действия будут выполняться во всех системах или что они не могут выполняться в других ситуациях.
Настройка процесса установки
683
В следующих таблицах (табл. 15.8—15.13) второй столбец указывает, отправляет ли программа установки некоторый DIF-код вспомогательной установочной DLL. Основной установочной DLL отправляются все перечисленные коды.
Таблица 15.8. Установка устройств, не поддерживающих РпР (например, IOCTL)
КОД DIF	Вспомогательная установочная DLL?	Краткое описание операции
DIFJIEGISTERDEVICE	Нет	Определить, является ли устройство, не поддерживающее РпР, дубликатным
DIF_SELECTBESTCOMPATDRV	Нет	Изменить список возможных драйверов; возможно, произвести усечение списка или выбрать драйвер
DIF_ALLOWJNSTALL	Нет	Определить, должна ли программа установки переходить к установке устройства
DIFJNSTALLDEVICEFILES	Нет	Копировать файлы; изменить список файлов для последующего копирования
DIF_REGISTER_COINSTALLERS	Нет	Изменить список вспомогательных установочных DLL устройств
DIFJNSTALLINTERFACES	Да	Регистрировать интерфейсы устройств
DIFJNSTALLDEVICE	Да	Выполнить всю необходимую подготовку перед загрузкой драйверов устройства
DIF_NEWD EVICE WIZARD- FINISHINSTALL	Да	Создать дополнительные страницы мастера
DIFJ)ESTROYPRIVATEDATA	Да	Выполнить зачистку
Таблица 15.9. Установка устройств РпР (например, USB42)
Код DIF	Вспомогательная установочная DLL?	Краткое описание операции
DIF_SELECTBESTCOMPATDRV	Нет	Изменить список возможных драйверов; возможно, произвести усечение списка или выбрать драйвер
DIFJXLLOWJNSTALL	Нет	Определить, должна ли программа установки переходить к установке устройства
DIFJNSTALLDEVICEFILES	Нет	Копировать файлы; изменить список файлов для последующего копирования
DIF_REGISTER__COINSTALLERS	Нет	Изменить список вспомогательных установочных DLL устройств
DIFJNSTALLINTERFACES	Да	Регистрировать интерфейсы устройств
DIFJNSTALLDEVICE	Да	Выполнить всю необходимую подготовку перед загрузкой драйверов устройства
DIFJMEWDEVICEWIZARD_ FINISHINSTALL	Да	Создать дополнительные страницы мастера
DIFJ)ESTROYPRIVATEDATA	Да	Выполнить зачистку
684
Глава 15. Распространение драйверов устройств
Единственное различие между сценариями установки устройств с поддержкой РпР и без нее — отсутствие шага DIF_REGISTERDEVICE.
Обратите внимание: эти действия выполняются только при начальной установке драйверов устройства РпР. В дальнейшем устройство отключается и подключается повторно без участия программы установки.
Таблица 15.10. Анализ свойств в Диспетчере устройств
Код DIF	Вспомогательная установочная DLL?	Краткое описание операции
DIF_ADDPROPERTYPAGE_ ADVANCED	Да	Создать необходимые страницы свойств
DIF.POWERMESSAGEWAKE	Да	Предоставить текст для вкладки Управление электропитанием (Power Management)
D1F_DESTROYPRIVATEDATA	Да	Выполнить зачистку
Таблица 15.11. Отключение устройства в Диспетчере устройств
Код DIF	Вспомогательная установочная DLL?	Краткое описание операции
DIF_PROPERTYCHANGE	Да	Сообщить основной/вспомогательной установочной DLL об изменениях в состоянии устройства (запуск/ остановка, включение/отключение) или параметра конфигурации
DIF_DESTROYPRIVATEDATA	Да	Выполнить зачистку
Таблица 15.12. Включение устройства в Диспетчере устройств
Код DIF	Вспомогательная установочная DLL?	Краткое описание операции
DIFJ>ROPERTYCHANGE	Да	Сообщить основной/вспомогательной установочной DLL об изменениях в состоянии устройства (запуск/ остановка, включение/отключение) или параметра конфигурации
DIF_DESTROYPRIVATEDATA	Да	Выполнить зачистку
Таблица 15.13. Отключение устройства в Диспетчере устройств
Код DIF	Вспомогательная установочная DLL?	Краткое описание операции
DIF„REMOVE	Да	Решить, нужно ли продолжать процедуру удаления; уничтожить все хранимые приватные данные
DIF_DESTROYPRIVATEDATA	Да	Выполнить зачистку
Настройка процесса установки
685
Непредвиденное удаление устройств РпР
При непредвиденном удалении устройства РпР никакие действия по установке не выполняются.
Вспомогательные установочные DLL
Вспомогательная установочная DLL содержит экспортируемую точку входа со следующим прототипом:
DWORD __stdcal1 CoinstallerProctDIJUNCTION dif, HDEVINFO infoset, PSPJEVINFOJATA did, PCOINSTALLER_CONTEXT_DATA ctx);
В аргументе dif передается один из кодов DIF, описанных в предыдущем разделе. Аргумент infoset содержит манипулятор набора данных об устройствах, did — адрес структуры с информацией об одном конкретном устройстве, a ctx — указатель на структуру контекста (которая вместе со всем открытым интерфейсом объявляется в SETUPAPI.H):
typedef struct _COINSTALLER_CONTEXT_DATA {
BOOLEAN Postprocessing;
DWORD Install Result;
PVOID PrivateData;
} COINSTALLER_CONTEXT_DATA, *PCOINSTALLER_CONTEXT_DATA;
Программа установки вызывает процедуру вспомогательной установочной DLL один или два раза для каждого кода DIF (согласно указаниям самой DLL). Первый вызов обеспечивает предварительную обработку перед выполнением действия, поле Postprocessing контекстной структуры содержит FALSE. Вспомогательная установочная DLL возвращает одно из следующих значений:
О NO_ERROR — вспомогательная установочная DLL сделала все, что необходимо сделать для данного кода DIF;
О ERROR_DI_POSTPROCESSING_REQUIRED — вспомогательной установочной DLL необходимо выполнить дополнительную работу после завершения действий, связанных с кодом DIF;
О любой код ошибки Win32, кроме ERROR_DI_DO_DEFAULT, — произошла ошибка. Вспомогательные установочные DLL не должны возвращать ERROR_DLDO_ DEFAULT, так как это может нарушить работу программы установки.
Вообще говоря, если вспомогательная установочная DLL возвращает код ошибки, программа установки отменяет некоторую операцию. Но чтобы точно узнать, что именно произойдет, обращайтесь к подробной документации по каждому коду DIF.
Во втором случае (с возвратом ERROR_DI„POSTPROCESSING_REQUIRED) вспомогательная установочная DLL перед возвратом управления может задать полю PrivateData структуры контекста любое значение по своему усмотрению. Далее программа установки снова обращается с вызовом к вспомогательной установочной DLL с теми же значениями кода DIF, infoset и did. Поле Postprocessing структуры контекста будет равно TRUE, а в поле InstallResult будет храниться код возврата от предыдущего обращения к основной или вспомогательной установочной DLL.
686
Глава 15. Распространение драйверов устройств
Поле PrivateData указывает, какое значение вспомогательная установочная DLL вернула в фазе предварительной обработки. На вызов заключительной обработки вспомогательная установочная DLL возвращает NO.JERROR или код ошибки Win32, возвращаемое значение по умолчанию равно InstallResult.
В DDK включены два примера вспомогательных установочных DLL — COINST (TOASTER) и TOASTCO (TOASTPKG). Я бы предпочел писать код пользовательского режима (особенно код, включающий элементы пользовательского интерфейса) с использованием Microsoft Foundation Classes. Если вы разделяете мои предпочтения, обратите внимание на пример CON1STALLER в прилагаемых материалах. В этот пример входят два класса общего назначения, которые можно скопировать в DLL-проект на базе MFC для простого конструирования вспомогательной установочной DLL:
О класс CCoinstaller, производный от CExternaiDiaiogApp (мой класс), а в конечном итоге от CWinApp (стандартный класс MFC), инкапсулирует функциональность вспомогательной установочной DLL. Класс CCoinstaller включает процедуру вспомогательной установочной DLL, о которой говорилось ранее, виртуальные функции предварительной и заключительной обработки для каждого кода DIF и функцию Add Property Раде для удобного заполнения страницы свойств в Диспетчере устройств или создания мастера установки с нестандартными страницами;
О класс CCoinstallerDialog, производный от CExternalDialog (мой класс), а в конечном итоге от CPropertyPage (стандартный класс MFC), инкапсулирует страницу свойств, принадлежащую внешнему набору страниц. Функция CCoinstallerDialog:: OnlnitDialog также автоматически инициализирует элементы IDC_CLASSICON и IDC_DEVNAME, содержащие значок класса и имя устройства (если они существуют). Инициализация заметно снижает объем работы по построению страницы свойств в Диспетчере устройств.
Далее приводятся полное объявление и реализация класса CSampleCoinstaller. Я удалил некоторые комментарии MFC, которые лишь загромождают листинг:
class CSampleColnstal1erApp : public CCoinstaller
{
public:
CSampleCoinstal1erApp():
virtual -CSampleCoinstal1erApp();
public:
virtual DWORD AddPropertyPagesfHDEVINFO infoset, PSP_DEVINFO_DATA did, PVOID& PostContext);
virtual DWORD Finishlnstal1(HDEVINFO Infoset,
PSPJ3EVINF0_DATA did, PVOID& PostContext):
CShoeSize* m-Shoesize;
CShoeSizeProperty* m_shoesi zeprop;
DECLARE_MESSAGEJW()
}:
BEGIN_MESSAGE_MAP(CSampleCoinstallerApp, CExternaiDiaiogApp)
Настройка процесса установки
687
END_MESSAGE_MAP()
CSampleColnstal1erApp;:CSampleColnsta11erApp()
{
m_shoesize = NULL;
m_shoes1zeprop = NULL;
}
CSampl eColnsta11erApp::-CSampleCol nstal1erApp()
{
If (m_shoesize) delete m^shoesize;
If (m_shoesizeprop) delete m_shoesizeprop;
}
CSampleCoinstallerApp theApp;
DWORD CSampleColnstal1erApp::AddPropertyPages(HDEVINFO Infoset.
PSPJEVINFOJATA did. PVOID& PostContext)
m__shoesizeprop - new CShoeSizeProperty;
AddPropertyPagednfoset, did, m_shoes1zeprop);
return NOJERROR;
}
DWORD CSampleColnstal1erApp::Flnlshlnstal1(HDEVINFO Infoset.
PSPJJEVINFOJJATA did. PVOID& PostContext)
m_shoes1ze = new CShoeSlze;
AddPropertyPage(infoset, did. m_shoesize);
return NOJERROR;
CShoeSize и CShoeSizeProperty — стандартные диалоговые классы, сгенерированные средствами MFC и производные от CCoinstallerDialog. Они реализуют, соответственно, функциональность страницы мастера и страницы Диспетчера устройств.
Функция AddPropertyPages замещает виртуальную базовую функцию для обработки вызовов DIF_ADDPROPER7YPAGE_ADVANCED. Finishinstall замещает виртуальную базовую функцию для обработки вызова DIF_NEWDEVICEWIZARD_ FINISHINSTALL.
Поскольку основной темой книги является программирование драйверов (во всяком случае, так было задумано), я на стану углубляться в подробности реализации базовых классов. На рис. 15.13 изображена страница мастера для фиктивного устройства, связанного с примером СО INSTALLER.
Страница мастера в примере COINSTALLER имеет один важный недостаток, который следовало бы исправить в реальной ситуации: она подразумевает, что вам будет дана возможность инициализировать параметр конфигурации. Тем не менее, к моменту отображения этой страницы PnP Manager уже загрузил
688
Глава 15. Распространение драйверов устройств
и инициализировал драйвер. Вспомогательная установочная DLL и драйвер запрограммированы так, что изменения в параметре не будут отражены в драйвере до следующего запуска. Вспомогательная установочная DLL может инициировать автоматический перезапуск устройства при завершении мастера установки (более того, на странице свойств Диспетчера устройств для этого имеется специальный флажок). Также можно оповестить драйвер об изменении параметра динамическим способом — например, с использованием вызова IOCTL или WMI. Тем не менее, я решил не загромождать пример лишними усложнениями.
Shoe Size
Specify the size of у out shoes
Setup allows you to initialize the value of the ProgrammersS hoeSize parameter
Specify you shoe size:
'' ? ....I	J ।
Рис. 15.13. Страница мастера, отображаемая вспомогательной установочной DLL
ПРИМЕР--------------------------------------------------------------------------------------
Одна из важных задач, решаемых вспомогательными DLL, — присваивание уникального дружественного имени устройству в ситуациях, когда в системе установлено несколько сходных устройств. Но поскольку вспомогательные установочные DLL не поддерживаются в Windows 98/Ме, вам может потребоваться альтернативный метод. Пример MAKENAME в прилагаемых материалах показывает, как использовать RUNDLL32 с простыми DLL для создания уникальных дружественных имен.
Предварительная установка файлов драйверов
Самый удобный для конечного пользователя вариант основан на предварительном копировании всех файлов, необходимых для серверной установки. Процедура состоит из следующих этапов:
1.	Получите цифровую подпись для пакета установки драйверов. Предварительная установка сама по себе не позволит устанавливать неподписанный пакет без взаимодействия с пользователем. Иначе говоря, от предварительной установки неподписанного пакета вы ничего не выигрываете.
Настройка процесса установки
689
2.	Напишите программу установки, которая будет копировать все необходимые файлы драйвера в локальный каталог в системе конечного пользователя. Обычно для этой цели применяется сценарий InstallShield, но подойдет любое решение, удовлетворяющее требованиям Microsoft к программе установки приложений.
3.	В процессе выполнения программы установки, упоминаемой на предыдущем шаге, вызовите функцию SetupCopyOEMInf (документированную в PlatformSDK или MSDN) с параметром SPOST_PATH.
Пример TOASTPKG из DDK содержит полнофункциональный пример предварительной установки подписанного пакета драйвера в Windows ХР. В пример включен каталог с образом компакт-диска, содержащий файл AUTORUN.INF, который автоматически запускает программу предварительной установки.
Многофункциональным устройствам часто требуется более одного INF-фай-ла ~ как правило, для родительского и дочернего устройств, относящихся к разным классам установки. INF-файл основного устройства указывается при вызове SetupCopyOEMInf, его секция установки должна содержать директивы Copylnf для копирования INF-файлов, обеспечивающие установку дочерних устройств. Директива Copylnf появилась в Windows ХР. COCPYINF.DLL также реализует директиву Copylnf. Полная информация об этом свободно распространяемом компоненте DDK приведена в файле Tools\Coinstallers\x86\cocpyinf.htm.
Дополнительное программное обеспечение
Вы можете немного упростить себе жизнь, отказавшись от включения так называемого дополнительного программного обеспечения (то есть приложений и других компонентов пользовательского режима, помимо вспомогательных установочных DLL) в директивах CopyFiles в INF-файлах. Полученная вами цифровая подпись будет распространяться на все файлы, копируемые INF-файлом. При внесении любых изменений в такие файлы цифровая подпись становится недействительной, и вам придется получать ее заново от WHQL.
Microsoft рекомендует устанавливать дополнительное программное обеспечение при помощи отдельных программ — скажем, сценариев InstallShield. Отдельную программу можно запускать разными способами, но, вероятно, самый надежный из них продемонстрирован в примере DDK TOASTPKG. В этом примере параметр реестра сообщает, предоставлялась ли пользователю возможность установки дополнительного пакета в данной системе. Если флаг сброшен, вспомогательная установочная DLL отображает страницу мастера установки и предлагает пользователю установить дополнительное программное обеспечение (обратите внимание: диалоговые окна потребуют клиентской установки). Если на вопрос будет дан положительный ответ, вспомогательная установочная DLL запускает программу установки дополнительного программного обеспечения из подкаталога, находящегося в каталоге с INF-файлом.
В программе TOASTPKG вспомогательная установочная DLL устанавливает дополнительное программное обеспечение только в том случае, если пользователь
690
Глава 15. Распространение драйверов устройств
подключил оборудование до запуска программы предварительной установки, входящей в пакет. (Программа предварительной установки устанавливает или не устанавливает дополнительное программное обеспечение в зависимости от решения пользователя, а затем сохраняет информацию в реестре, чтобы вспомогательная установочная DLL не пыталась отображать окно мастера в будущем. Таким образом, после выполнения программы предварительной установки становится возможным серверный вариант установки.)
Планирование и подготовка к различным ситуациям, возникающим в ходе установки, - - сложная задача. Именно из-за этой сложности в разделе «Управление проектом и контрольный список» главы 1 я рекомендовал заранее продумать работу, необходимую для подготовки установочного пакета.
Программная установка драйвера
Если устройство не поддерживает РпР или для устройства РпР поставить обновленный драйвер, возможно, вы захотите написать программу для установки драйвера с минимальным участием пользователя. Пример DEVCON в DDK показывает, как на программном уровне выполнять установку или обновление драйверов и как решать в Windows 2000 и Windows ХР другие задачи управления устройствами. Пример FASTINST в прилагаемых материалах демонстрирует только программную установку драйверов, но работает на любой платформе WDM.
Версия FASTINST для Windows 2000 и Windows ХР работает следующим образом:
1.	Сначала программа разбирает INF-файл, имя которого передается в аргументе командной строки, и находит первую директиву модели, определяющую идентификатор устройства (вы можете запустить FASTINST из командной строки и указать любой идентификатор устройства по своему усмотрению, переопределяя вариант, используемый по умолчанию, — выбор идентификатора из первой директивы модели).
2.	Затем программа конструирует пустой узел с идентификатором устройства, определенным на первом шаге.
3.	Вызов функции UpdateDriverForPlugAndPlayDevices «замещает» драйвер пустого узла на основании INF-файла.
Параметр RunOnce
Параметры в разделе реестра HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\ CurrentVersion\RunOnce определяют команды, выполняемые после завершения установки устройства. Каждый параметр в разделе RunOnce содержит командную строку. После завершения процедуры установки система выполняет команды и удаляет параметры из реестра.
ПРИМЕЧАНИЕ--------------------------------------------------------------
Любые команды, находящиеся в разделе RunOnce, также выполняются при загрузке системы. В DDK указано, что команды RunOnce также могут выполняться в другие, не заданные заранее моменты времени. Кроме того, там сказано: «Невозможно точно предсказать, в какой момент будет выполнена команда, помещенная в раздел RunOnce».
Настройка процесса установки
691
Следующий фрагмент создания команд RunOnce в синтаксисе INF-файла позаимствован из примера AUTOLAUNCH:
[Driverinstall ,ntx86j
CopyFl 1 es4)r iverCopyFi1es,AutoLaunchCopyFI1es
AddReg4)r iverAddReg.ntx86
[Dr1verAddReg.ntx86]
HKLM, ^RUNONCEKEYIWO, AutoLaunchStart,, \
'Tundll32 StartService,Startservice AutoLaunch"
[Strings]
RUNONCEKEYNAME="Software\Mi crosoft\WindowsXCu rrentVersi orARundnce"
Стандартная системная утилита RUNDLL32 вызывает функцию, находящуюся в DLL. Если во всех созданных вами командах RunOnce используется RUNDLL32, становится возможной серверная установка с использованием INF-файла. Более того, при использовании любых других операций в командах RunOnce ваш INF-файл не пройдет тесты WHQL.
Синтаксис команды RUNDLL32:
rund!132 dll name,entryname [cmdline]
где dllname ~ имя DLL (с расширением .DLL или без него), entryname — имя экспортируемой точки входа, a cmdline — необязательная командная строка, обрабатываемая DLL. Функция entn/name должна определяться следующим образом:
[extern "С"] void CALLBACK entrynameCHWND hwnd. HINSTANCE hirst, LPSTR cmdllne, int nshow) {
}
Следует помнить, что объявление CALLBACK включает директиву________stdcall.
Аргумент hwnd содержит манипулятор окна, родительского для всех элементов пользовательского интерфейса, создаваемых функцией, hlnst — манипулятор экземпляра DLL, cmdline — точная копия одноименного аргумента команды RUNDLL32, a nshow — один из стандартных кодов состояния отображения (таких как SW_SHOWNORMAL).
Запуск приложения
Для первого издания книги я разработал схему, которая позволяет организовать запуск дополнительного приложения каждый раз, когда система запускает ваше устройство (в том числе и в первый). Эта схема продемонстрирована в примере AUTOLAUNCH в прилагаемых материалах. В общем виде она выглядит так: О INF-файл устанавливает службу AutoLaunch, которая работает в пользовательском режиме и отслеживает поступающие оповещения для GUID конкретного интерфейса;
692
Глава 15. Распространение драйверов устройств
О получив оповещение, AutoLaunch обращается к реестру, читает из него командную строку и выполняет ее на интерактивном рабочем столе;
О чтобы приложение запускалось и при первоначальной установке устройства, INF-файл использует команду Ru nOnce для запуска службы Auto Launch. Я практически не получал вопросов от читателей по поводу AutoLaunch, поэтому полагаю, что описывать ее во всех подробностях не стоит.
WHQL
Компания Microsoft стремится к тому, чтобы устройства и их драйверы отвечали определенным стандартам качества, отличались способностью к взаимодействию и простотой использования с точки зрения потребителя. С этой целью в 1996 была создана служба WHQL (Windows Hardware Quality Lab). Основной обязанностью WHQL является публикация и администрирование развивающегося набора тестов НСТ (Hardware Compatibility Tests) для систем и периферийных устройств. Успешное прохождение всех тестов дает три основных преимущества:
О доступ к различным маркетинговым программам, включая логотип «Designed for Windows», и различным спискам, поддерживаемым Microsoft;
О цифровая подпись для пакета драйвера, существенно упрощающая установку на машинах конечных пользователей;
О свободное распространение драйвера через службу Windows Update и другими средствами.
Отправной точкой в вашей работе с WHQL станет сайт http://www.rnicrosoft.com/ hwdq/hwtest. Как отмечалось в главе 1, важно начать эту работу на ранней стадии ведения проекта, потому что даже до первого обращения в WHQL с запросом на тестирование оборудования и программ вам придется преодолеть немало юридических и организационных препятствий. Затем вам придется раздобыть системы и оборудование, которые вам совершенно не нужны, кроме как для проведения обязательных тестов для вашего класса устройств.
Проведение тестов НСТ
Когда все будет готово к началу процесса сертификации WHQL, начните с за^ пуска соответствующих тестов НСТ. Компания Microsoft распространяет НСТ в составе некоторых видов подписки MSDN и программ бета-тестирования. Тесты также можно загрузить из Интернета. Мастер установки позволяет выбрать одну или несколько категорий проводимых тестов. При тестировании драйвера одного устройства я рекомендую выбрать одну категорию, включающую ваше устройство, — это позволит свести к минимуму количество бессмысленных вопросов, на которые вам придется позднее отвечать. Например, при установке тестов для устройств ввода, графических планшетов и устройств чтения
WHQL
693
смарт-карт вам придется выбрать одно устройство в каждой категории, прежде чем Test Manager разрешит начать тестирование.
Чтобы привести конкретный пример, я решил выполнить тесты для игровой мыши с интерфейсом USB, для которой я написал драйвер. На рис. 15.14 показано, как выбиралась категория тестов при установке НСТ.
Рис-15.14. Выбор категории тестов
Программа установки НСТ автоматически запускает мастер, который позволяет выбрать устройство для тестирования. Диалоговое окно напоминает, что оборудование и программное обеспечение уже должны быть установлены на этой стадии. Для каждой из установленных категорий выводится диалоговое окно наподобие показанного на рис. 15.15. Мое устройство отображается как «Н ID-совместимая мышь». В качестве производителя указана компания Microsoft, так как НСТ думает, что мое устройство использует стандартный драйвер MOUHID.SYS. В действительности моя мышь обслуживается нестандартным минидрайвером HIDCLASS со множеством дополнительных функций, выходящих за рамки основных функций, тестируемых НСТ.
Прежде чем переходить к тестированию, НСТ выводит несколько дополнительных диалоговых окон. На рис. 15.16 изображено базовое диалоговое окно администратора тестов. В идеальном случае вы просто нажимаете кнопку Add Not Run Tests (Добавить невыполненные тесты), заполняющую правую панель списком всех обратного тестов. Тем не менее, здесь потребуется некоторая осмотрительность.
694
Глава 15. Распространение драйверов устройств
Рис. 15.15. Выбор устройства для тестирования
Рис. 15.16. Диалоговое окно Test Manager
Один из тестов (ACPI Stress Test) выполняется в течение многих часов. На некоторых компьютерах он вообще не запускается (в том числе и на портативном
WHQL
695
компьютере, на котором я проводил тестирование). Для запуска этого теста понадобится система ХР Professional либо на настольном компьютере с поддержкой S1 и S3, либо на ноутбуке с поддержкой S1 или S3. (Я использовал Windows ХР Home Edition, потому что после обновления системы переставала работать функция пробуждения, ради которой я купил этот конкретный ноутбук). Видимо, у меня никогда не будет компьютера, на котором бы запускался этот тест, потому что я обычно покупаю компьютер с заранее установленной операционной системой, обновляю операционную систему как часть программы бета-тестирования — и после этого управление питанием перестает работать.
Для проведения теста USB Manual Interoperability необходимо мультимедийное оборудование общей стоимостью в несколько сотен долларов, которое после проведения теста стало бы совершенно липшим (на рис. 15.17 показано окно, полученное мной, когда я по ошибке разрешил выполнить этот тест). Этот тест довольно важен с точки зрения оборудования, он убеждается в том, что часто используемые устройства с интерфейсом USB продолжат работать после подключения вашего устройства (и наоборот).
Рис. 15.17а Оборудование, необходимое для проведения теста USB Manual Interoperability
Остальные тесты мало что сообщат мне о качестве моего драйвера. Тест Direct-Input Mouse убеждается в том, что драйверы Microsoft правильно взаимодействуют с Directlnput, — кто бы сомневался. Тест USB Selective Suspend в настоящее время для HID-устройств особой роли не играет, потому что HIDCLASS
696
Глава 15. Распространение драйверов устройств
никогда не приостанавливает устройства: большинство из них не могут «пробудиться» без потери входного события. В сущности, все автоматизированные тесты USB относятся к аппаратным проблемам. В этом конкретном примере я разрешил их проведение, потому что в то время совместно работал со старшим разработчиком «прошивки» над выводом продукта на рынок. После отбора всех тестов, которые бы мне хотелось выполнить (успешно или нет — другой вопрос, на который мне действительно хотелось получить ответ), диалоговое окно Test Manager выглядело так, как показано на рис. 15.18.
Рис, 15.18. Все готово к началу тестирования...
Прежде всего тестовый механизм запускает программу проверки драйверов (на неподходящих драйверах) и перезагружает компьютер. Не забывайте, что, по мнению НСТ, моя мышь обслуживается драйвером MOUHID.SYS. В действитель ности проверку следовало бы включить для моего минидрайвера. Однако при попытке сделать это вручную результаты запуска теста стали бы недействительными, поэтому я разрешил продолжить тестирование. Говорят, новые версии НСТ лучше справляются с идентификацией тестируемых драйверов. Позднее я запустил тесты для моего драйвера — и, как оказалось, не зря. Это позволило мне найти элементарную ошибку, когда мой минидрайвер HID отправлял устройству запрос IRP_MJ_POWER с дополнительным кодом функции IRP_MN_SET_ POWER и типом SystemPowerState после ожидания завершения IRP при опросе прерывающей конечной точки.
WHQL
697
Тест Mouse Functionality (рис. 15.19) оказался самым значимым для качества драйвера: он убеждается в том, что отчеты мыши действительно доставляются в формате, ожидаемом системой. Поскольку на моей мыши отсутствует колесо (пользователь может запрограммировать некоторые кнопки для выполнения функций колеса), мне пришлось имитировать часть теста функциональности с другой мышью, подключенной к той же системе.
Рис. 15.19. Тест Mouse Functionality
В тестах Public Import и Signability мне было предложено указать, устанавливает ли мой продукт «свой собственный драйвер». Я ответил, что устанавливает, и указал путь к каталогу с INF-файлом и всеми остальными файлами, устанавливаемыми на любых платформах. Тест Public Import убедился в том, что мой драйвер не вызывает запрещенные функции режима ядра. Тест Signability, в частности, проверил, что все файлы, копируемые INF-файлом, действительно присутствуют в системе (вспомните, что CHKINF этого не делает).
Тест ChkINF запустил программу CHKINF для неверного INF-файла — а именно для файла INPUT.INF, предоставленного компанией Microsoft. Будучи человеком добросовестным, я запустил CHKINF вручную. Сначала запуск тестового сценария PERL завершился неудачей из-за отсутствия файла STRICT. РМ; я нашел его в каталоге НСТ и скопировал вручную. В тестовом отчете говорилось, что использование записи RunOnce для запуска CONTROL.EXE (клиент пожелал, чтобы специализированная панель управления запускалась автоматически) не разрешено, потому что в нем не используется RUNDLL32. Поскольку эта идея мне все равно не нравилась, я использовал неудачу при тестировании в качестве лишнего довода, чтобы переубедить своего клиента. Конечно, можно было организовать запуск приложения панели управления при помощи RUNDLL32, но это бы противоречило подлинной, хотя и не объявленной, цели теста — обеспечить возможность выполнения серверной установки без участия элементов пользовательского интерфейса.
Остальные запланированные тесты проходили без хмоего участия — полагаю, поэтому они и называются автоматизированными тестами. В конечном итоге я получил протокол тестирования, показанный на рис. 15.20.
698
Глава 15. Распространение драйверов устройств
В протоколе отсутствуют записи для теста Enable/Disable, поскольку тест сгенерировал исключение в пользовательском режиме. Некая часть тестового механизма перехватила исключение и скрытно прервала тест.
Рис. 15.20. Результаты тестирования после выполнения избранных тестов
На пару с моим коллегой, специалистом по «прошивке», мы трудились над исправлением ошибок в различных тестах USB. Конечно, было бы очень хорошо, если бы документация НСТ полностью соответствовала описаниям сбоев в тестах. Например, в протоколе теста USB Address Description упоминался сбой по проверке условия 9.22.6. Открыв документацию НСТ 10.0 из меню Пуск (Start), я добрался до раздела Resources/WHQL Test Specification/Chapter 9 USB Test Specification/USB Test Assertions/Address Test Assertions и нашел информацию, показанную на рис. 15.21. Нарушение условия 9.22.6 — это... кто его знает. Наверное, что-нибудь важное.
Как видите, в процессе тестирования обнаружилось немало проблем. Подведем итоги:
О Некоторые тесты не удалось провести из-за нехватки оборудования или бюджета. Мне не удалось составить полную заявку WHQL для своего клиента. Как выяснилось, он тоже не располагал необходимыми ресурсами, и нам пришлось обратиться к внешнему подрядчику, специализировавшемуся по тестированию WHQL. Кстати, логотип вообще был не нужен — представители
WHQL
699
субкультуры, на которую была ориентирована мышь, предпочли бы, чтобы логотипов Microsoft на ней не было. Тем не менее, цифровая подпись была необходима из-за проблем с ранжированием драйверов, обсуждавшихся ранее в этой главе.
Рис. 15.21
О Один из тестов завершился неудачей по неизвестным причинам.
О Некоторые тесты проверяли не то, что требовалось.
О Некоторые сбои, встретившиеся в процессе тестирования, не были документированы.
В таких ситуациях остается только обратиться за помощью. Персонал WHQL ведет ряд групп новостей на сервере msnews.microsoft.com, в том числе microsoft. public.developmentdevice.drivers и microsoft.public.windowsxp.winlogo. Кроме того, WHQL отвечает на запросы по электронной почте по адресам, приведенным на домашней странице WHQL по адресу http://www.microsoft.com/hwdq/hwtest.
Передача пакета с драйвером
Последним шагом НСТ является создание пакета, поставляемого в WHQL. Это надлежит сделать отдельно для каждой операционной системы, поддерживаемой вашим драйвером, а затем собрать полученные САВ-файлы в одном удобном месте. Следующими шагами (которые, на мой взгляд, следовало бы перенести на несколько месяцев ранее) должны стать посещение сайта http://winqual.microsoft.com и регистрация вашей компании как клиента WHQL.
700
Глава 15. Распространение драйверов устройств
После получения имени и пароля вы сможете регистрироваться на странице winqual и сделать следующее:
О создать новый пакет;
О просмотреть данные о состоянии предыдущей заявки;
О просмотреть отчеты об ошибках, наиболее вероятной причиной которых стал ваш продукт.
Для этой главы я решил создать новую заявку на сертификацию устройства. На рис. 15.22 показана отправная точка процедуры создания.
Рис. 15.22. Начальный этап подачи новой заявки WHQL
Начиная с окна, показанного на рис. 15.22, несколько веб-форм помогают относительно легко составить описание заявки. Вам придется ответить на следующие вопросы:
О К какому классу относится продукт? Я указал, что мой продукт относится к классу устройств ввода, — эта же тестовая категория была выбрана при выполнении ПСТ.
О В какой операционной системе будет использоваться продукт? Проследите за тем, чтобы тесты НСТ были выполнены на всех выбранных платформах, потому что позднее вам придется ввести результаты тестов для всех платформ.
WHQL
701
О Какие два адреса электронной почты (включая ваш адрес) могут использоваться для взаимодействия, связанного с заявкой? Не знаю, как именно следует поступить, если вы работаете в одиночку (я проводил свои тесты в качестве участника фиктивной компании, которую WHQL использует в своем внутреннем тестировании, и у меня не было возможности узнать, как решается эта конкретная проблема).
О Как называется ваш продукт, когда он будет выпущен, какие платформы будет поддерживать? Также установлены определенные правила по поводу того, что следует считать допустимым именем продукта. Я не мог ограничиться единственным словом «мышь» — нужно было ввести описание, включающее номер модели производителя и обобщенное описание («специализированная игровая мышь»). Вопрос о поддержке платформ отличается от предыдущего вопроса об операционных системах, поскольку отдельные системы существуют в нескольких разновидностях. Например, можно указатель, что продукт поддерживает Windows ХР Home Edition, но не Windows ХР Professional и т. д.
Рис. 15.23. Ввод подробной информации о продукте
702
Глава 15. Распространение драйверов устройств
О Где находится пакет драйвера для каждой операционной системы? Отвечая на этот вопрос, следует указать для каждой операционной системы имя дерева каталога со всеми файлами, на которые будет распространяться действие файла цифровой подписи. Другими словами, дерево каталогов должно включать все INF-файлы и все файлы, устанавливаемые из этих INF-файлов. Простейший и самый лучший способ заключается в создании дерева каталогов, полностью воспроизводящего структуру дистрибутивного носителя и содержащего все файлы, которые должны оказаться на компьютере конечного пользователя (и ничего кроме них!). На этом этапе также задаются языки, поддерживаемые каждым пакетом драйверов.
О Где хранятся протоколы тестов для каждой операционной системы? Кстати говоря, каталог не может содержать более одного протокола из-за конфликтов имен.
О Для каких идентификаторов оборудования (РпР) компания Microsoft должна распространять драйверы через службу Windows Update? Существуют и другие требования для использования Windows Update, но на этот шаг следует обратить особое внимание (рис. 15.24).
Рис. 15.24. Указание идентификаторов РпР для Windows Update
WHQL
703
О Как будет организовано распространение драйвера? Существует много вариантов в зависимости от того, обладаете ли вы лицензией на распространение драйвера (рис. 15.25).
Рис. 15.25. Выбор каналов распространения драйвера
О Как вы собираетесь оплачивать тестирование? Да, представьте себе, — за тестирование приходится платить. На момент написания книги мне пришлось заплатить $250 за каждую из операционных систем (Windows ХР, Windows 2000 и Windows Me), которая должна была поддерживаться драйвером, итого $750. За повторное тестирование приходится платить столько же, так что не стоит подавать заявку при наличии очевидных недостатков в пакете.
О Где должно проводиться тестирование? У WHQL имеется несколько тестовых площадок по всему миру. На самом деле этот вопрос актуален только в том случае, если для вашей заявки потребуется специальное оборудование. В моем случае этого не требовалось (рис. 15.26). На момент написания книги многие тесты WHQL работали в режиме программного тестирования и не требовали отправки оборудования.
О Согласна ли вышеозначенная сторона с нижеизложенным и т. д.? Да, это юридическое соглашение, которое вы обязаны подписать.
704
Глава 15. Распространениедрайверовустройств
Рис. 15.26. Список комплектации
О Это ваше окончательное решение? А может, вы все-таки хотите изменить местонахождение драйвера и тестового протокола, указанные ранее? Говорите сейчас или никогда.
О Та-да! Все готово (рис. 15.27). Вы можете снабдить свою заявку цифровой подписью и отправить ее в WHQL. На этом я должен остановиться. Во-первых, пакет из моего примера не только содержит фатальные недостатки; во-вторых, у меня нет идентификатора VeriSign (и я не собираюсь предпринимать на основательные хлопоты, связанные с его получением).
При первом переборе веб-форм я узнал ряд полезных моментов. Как упоминалось ранее, желательно иметь под рукой все дистрибутивные пакеты и результаты тестирования. У вас будет достаточно времени для завершения процесса, но веб-приложение завершится по тайм-ауту примерно через час — так что не рассчитывайте, что вам удастся пообедать между этапами. Некоторые из принятых решений не удастся изменить другим способом, кроме возврата. Указание неверных каталогов на некоторых этапах может затянуть процесс на целые часы, так как приложению придется перебирать огромные деревья каталогов
Проблемы совместимости с Windows 98/Ме
705
в поисках файлов. Впрочем, формы предупреждают о двух последних обстоя’ тельствах, так что ошибка маловероятна.
Рис. 15.27. Заявка готова к подписи и отправке
Проблемы совместимости с Windows 98/Ме
В Windows 98/Ме используется совершенно иная технология установки и сопровождения устройств, нежели в Windows ХР. Я опишу некоторые из ее аспектов, которые могут отразиться на вашей работе.
Поставщики страниц свойств
Поставщик страниц свойств для нового класса устройств должен быть оформлен в виде 16-разрядной DLL. Если понадобится пример, обратитесь к проекту SAMCLS16 в прилагаемых материалах — и не торопитесь выбрасывать 16-раз-рядный компилятор!
Основные и вспомогательные установочные DLL
Основная установочная DKK класса в Windows 98/Ме представляет собой 16-раз-рядную DLL и использует функции из библиотеки SETUPX.DLL, документированной
706
Глава 15. Распространение драйверов устройств
в MSDN (а не в DDK). Я не знаю ни одного примера, демонстрирующего процесс написания таких DLL. Windows 98/Ме не поддерживает вспомогательные установочные DLL.
Предварительная установка пакетов драйверов
Функция SetupCopyOEMInf не реализована в Windows 98 и Windows Me. Чтобы обеспечить предварительную установку пакета драйвера на этих платформах, просто скопируйте файлы в нужное место и не включайте в INF-файл директивы копирования файлов.
Цифровые подписи
В Windows 98 цифровые подписи не используются. Windows Me не устанавливает неподписанные драйверы вместо подписанных драйверов для звуковых карт, некоторых мультимедийных устройств и видеоадаптеров. В DDK также описаны другие правила использования цифровых подписей в Windows Me. Честно говоря, я сам не разобрался во всех исключениях и частных случаях, и поэтому не смогу объяснить их вам.
Программная установка драйверов
Поскольку программа установки Windows 98/Ме является 16-разрядной, а функция UpdateDriverForPlugAndPlayDevices в этих системах не реализована, программная установка драйвера устройства, не являющегося устройством РпР, требует довольно героических усилий. Мне удалось собрать версию FASTINST для Windows 98/Ме методом проб и ошибок, потому что документация по 16-раз-рядным функциям установки слишком лаконична. В общих чертах, необходимо сконструировать структуру информации об устройстве для INF-файла, ограничить ее драйвером для выбранного идентификатора устройства, а затем вызвать DilnstallDevice. Не стоит и говорить, что процесс будет успешно работать лишь при соблюдении множества деталей.
CONFIGMG API
В Windows 98/Ме часто приходится передавать управление точкам входа API защищенного режима, экспортируемым CONFIGMG.VXD. Вызываемые функции документируются в Windows 95 DDK как сервисные функции кольца 0, имена которых начинаются с префикса CONFIGMG_. Функции API, вызываемые из кольца 3, обладают теми же именами и аргументами, но снабжаются префиксом СМ_. Пример SAMCLS16.DLL в прилагаемых материалах содержит несколько обращений к CONFIGMG, демонстрирующих механику подобных вызовов.
О несовместимости INF-файлов
Технология разбора и библиотеки установки, используемые в Windows 98/Ме, полностью отличаются от используемых в Windows 2000 и последующих системах.
Проблемы совместимости с Windows 98/Ме	707
Соответственно, существует ряд ограничений на написание INF-файлов, которые могут использоваться в обеих средах. Приведу неполный список:
О Имена секций ограничиваются максимальной длиной 28 символов.
О Windows 98/Ме игнорирует’ INF-файлы в Юникоде и суффиксы секций [Strings]. Таким образом, чтобы локализовать установку в Windows 98/Ме, вам придется предоставить совершенно новый INF-файл (для которого потребуется новая сигнатура WHQL). Это еще одна причина для отказа от поддержки Windows 98 и Windows Me на компьютерах закоренелых ретроградов.
О Windows 98/Ме не присоединяет суффиксы платформ и операционных систем к именам секций. В общем случае это означает, что в Windows 98/Ме используются исходные имена секций, а в более поздних системах — измененные имена.
О Windows 98/Ме не объединяет секции с одинаковыми именами в одну секцию, как это делается в Windows 2000 и последующих системах. Вместо этого выбирается первая из одноименных секций.
О Windows 98/Ме требует включения секции [Classinstall] для определения нового класса установки.
О Программа установки Windows 98/Ме не поддерживает длинные имена файлов (то есть имена с компонентами, длина которых превышает 8 символов). Например, если указать в секции CopyFiles файл с именем mydriverfile.sys, программа установки выдаст диалоговое окно с сообщением о том, что файл не найден (даже если файл существует). Но если нажать кнопку ОК, программа установки успешно скопирует файл. Аналогичная ситуация возникает с путями в директиве [SourceDisksFiles]. Возможно, вы заметили, что во всех INF-файлах вместо objchk_wxp__x86\i386 используется запись objchk~l\i386, чтобы эта странность вас не беспокоила.
Работа с реестром
Структура реестра Windows 98/Ме сильно отличается от структуры реестра Windows ХР:
О Параметры оборудования находятся в разделе HKLM\Enum.
О Не существует специального подраздела с параметрами оборудования. Вместо этого стандартные и нестандартные свойства хранятся вместе в разделе оборудования.
О Разделы классов находятся в ветви HKLM\System\CurrentControlSet\Services\Class, а их идентификация осуществляется исключительно по имени класса (раздел Class содержит фиктивный подраздел, имя которого совпадает с GUID класса, но это несущественно).
О Разделы драйверов представляют собой нумерованные подразделы раздела класса, как и в Windows ХР.
О В реестре Windows 98/Ме может присутствовать раздел службы, но важной роли он не играет. Вместо этого Configuration Manager загружает нужный драйвер на основании информации из раздела драйвера.
708
Глава 15. Распространение драйверов устройств
Чтобы инициализировать раздел драйвера, в секцию установки Windows 98/Ме следует включить директиву Add Reg следующего вида:
[Delverlnstal1]
AddRegOri verAddReg
<другие директивы установки>
[Dr1verAddReg]
HKR..DevLoader,,*ntkern
HKR,.NTMPDriver,.pktdma.sys
Иначе говоря, вы сообщаете, что NTKERN.VXD является загрузчиком устройства, и указываете драйвер WDM (NTMPDriver), который будет искать NTKERN.
Получение свойств устройств
В Windows 98/Ме неверно реализована функция loGetDeviceProperty для свойства FriendlyName. Чтобы загрузить дружественное имя для драйвера WDM, вызовите функцию loOpenDeviceRegistryKey и запросите свойство по имени. Пример DEVPROP демонстрирует, как это делается.
££ Фильтрующие О драйверы
Модель WDM предполагает, что устройство обслуживается несколькими драйверами, каждый из которых вносит свой вклад в успешное управление устройством. В WDM иерархия драйверов реализуется созданием стека объектов устройств (эта концепция обсуждалась в главе 2). До настоящего момента речь шла исключительно о функциональном драйвере, управляющем основной функциональностью устройства. В завершение книги я расскажу, как написать фильтрующий драйвер, который находится выше или ниже функционального драйвера и каким-то образом изменяет поведение устройства посредством фильтрации проходящих через него пакетов запросов ввода/вывода (IRP).
Роль фильтрующего драйвера
Фильтрующий драйвер, расположенный выше функционального драйвера, называется верхним фильтрующим драйвером. Фильтрующий драйвер, расположенный ниже функционального драйвера (но выше драйвера шины), называется нижним функциональным драйвером. Иерархия драйверов изображена на рис. 2.2. Механика построения фильтрующих драйверов обоих типов абсолютно одинакова, хотя сами драйверы служат разным целям. Более того, фильтрующий драйвер строится точно так же, как любой другой драйвер WDM, — с функциями DriverEntry, AddDevice, набором диспетчерских функций и т. д.
Верхние фильтрующие драйверы
Напомню, что I/O Manager отправляет пакеты IRP, адресованные устройству, верхнему фильтрующему объекту устройства (FiDO) в стеке РпР этого устройства. Следовательно, верхние фильтрующие драйверы видят все запросы раньше, чем функциональные драйверы, и могут модифицировать поток запросов любым способом по своему усмотрению. Вот лишь некоторые задачи, решаемые при помощи верхних фильтров:
О реализация унифицированных высокоуровневых интерфейсов для разнообразного базового оборудования. Я подробнее расскажу об этой концепции в нескольких ближайших абзацах;
710
Глава 16. Фильтрующие драйверы
О сбор и получение метрической информации — скажем, в ответ на запросы WMI (Windows Management Instrumentation);
О создание «обходных путей» для исправления ошибок в функциональных драйверах.
Примеры использования верхних фильтрующих драйверов часто встречаются в Windows ХР, я опишу некоторые из них. Рекомендую использовать утилиту DevView (см. главу 2) из прилагаемых материалов — она поможет наглядно представить структуру стеков драйверов.
Стеки драйверов для дисковых и ленточных запоминающих устройств
Существует несколько заметно различающихся технологий (SCSI, IDE, USB, 1394 и т. д.), используемых при построении дисковых и ленточных накопителей. Это означает, что функциональные драйверы также сильно различаются на нижнем уровне, то есть при непосредственном взаимодействии с оборудованием. Для упрощения общесистемной архитектуры каждый функциональный драйвер дискового или ленточного устройства экспортирует высокоуровневый интерфейс («верхнюю грань») SCSI. Другими словами, драйверы, находящиеся выше в стеке, отправляют функциональному драйверу блоки запросов SCSI (SRB, SCSI Request Blocks). Драйвер реального устройства SCSI просто извлекает из SRB стандартизированные блоки CDB (Command Description Block) и отправляет их оборудованию более или менее напрямую. Драйверы других типов устройств транслируют CD В в соответствии с требованиями своего аппаратного протокола.
ПРИМЕЧАНИЕ------------------------------------------------------------
Формально в стеках запоминающих устройств нет «функциональных» драйверов, потому что ни один из драйверов стека не обозначен в реестре как таковой. Драйвер шины, находящийся внизу стека, играет роль функционального драйвера. Все остальные драйверы в стеке, с технической точки зрения, являются верхними фильтрующими драйверами.
Компания Microsoft создала три верхних фильтрующих драйвера (DISK.SYS, CDROM.SYS и ТАРЕ.SYS), реализующих «дисковые» или «ленточные» аспекты работы оборудования. Microsoft называет их драйверами классов, однако смысл термина класс в данном контексте отличается от того, который использовался в книге. Ранее мы говорили о парах «класс/мини-драйвер», соответствующих функциональному драйверу. В данном случае термин относится к фильтрующему драйверу, который реализует интерфейс верхней грани для целого класса устройств.
Верхняя грань драйверов класса запоминающих устройство представляет обобщенный интерфейс диска, дисковода CD-ROM или ленточного устройства, состоящий из управляющих запросов ввода/вывода (IOCTL) и поведения, определяемого для запросов чтения и записи. На нижней грани эти драйверы общаются «на языке SCSI» с более низкими позициями стека.
Роль фильтрующего драйвера
711
На рис. 16.1 показано, как выглядит стек драйвера для жесткого диска SCSI в одной из моих систем. В самом низу стека находится AIC78U2 — драйвер мини-порта SCSI, который совместно с драйвером класса SCSIPORT.SYS обеспечивает управление конкретным адаптером SCSI, поставляемым вместе с компьютером. Благодаря драйверу DISK.SYS жесткий диск на более высоких уровнях воспринимается как дисковое устройство. PARTMGR.SYS управляет двумя разделами (partitions), созданными на диске.
Рис-16-1. Стек драйверов для жесткого диска
На рис. 16.2 изображен стек драйверов для дисковода DVD-ROM с интерфейсом IDE на другом компьютере. На этот раз в нижней позиции стека находится драйвер ATAPLSYS. На верхней грани он выглядит как драйвер порта SCSI, а на нижней — как контроллер IDE. Сверху от ATAPLSYS находятся IMAPI.SYS, CDROM.SYS (на верхней грани представляет образ CD-ROM стандарта ISO) и REDBOOK.SYS (реализует аудиостандарт Redbook).
REDBOOK.SYS
CDROM.SYS
IMAPI.SYS
ATAPLSYS
Рис-16.2. Стек драйверов для дисковода DVD-ROM
PnP Manager конструирует стеки драйверов на основании типа устройства, сообщаемого драйвером шипы. В примере с жестким диском (см. рис. 16.1) устройство относится к классу Disk, DISK.SYS и PARTMGR.SYS указаны в качестве верхних фильтров в разделе класса Disk. В примере с дисководом DVD-ROM (см. рис. 16.2)
712
Глава 16. Фильтрующие драйверы
устройство относится к классу Cdrom. REDBOOK.SYS и IMAPI.SYS указаны в разделе оборудования как верхний и нижний фильтры соответственно; CDROM.SYS указан как верхний фильтр в разделе класса Cdrom.
Драйверы мыши и клавиатуры
Для подключения клавиатур и мышей в системе Windows обычно применяются две технологии — USB (Universal Serial Bus) и 8042. На рис. 16.3 показан стек драйверов для мыши USB. Драйверу HIDUSB.SYS (минидрайвер HIDCLASS) отведена роль драйвера шины, a MOUHID.SYS — роль функционального драйвера. MOUCLASS.SYS является верхним фильтром для класса Mouse.
Рис. 16.3. Стек драйверов для мыши USB
На рис. 16.4 представлена аналогичная диаграмма для мыши PS/2 в другой системе. I8042.SYS — функциональный драйвер порта мыши PS/2. В этой конкретной системе роль драйвера шины отведена ACPLSYS (драйверу, отвечающему за общее управление питанием).
MOUCLASS.SYS
18042.SYS
ACPI.SYS
Рис. 16.4. Стек драйверов для мыши PS/2
Очевидно, оба стека имеют общий драйвер MOUCLASS.SYS, который предоставляет унифицированный интерфейс мыши для системы. Именно благодаря этому обстоятельству к компьютеру с системой Windows можно подключить мышь любого типа (или две мыши одновременно).
Единственное существенное отличие для клавиатур состоит в том, что на верхнем уровне стека драйверов находится драйвер KBDCLASS.
Роль фильтрующего драйвера
713
Последовательный перечислитель
Многие периферийные устройства рассчитаны на подключение к последовательному порту. Конечно, самыми распространенными из них являются модемы и мыши. До появления USB последовательный порт также использовался для подключения других устройств. Компания Microsoft давно определила протокол, посредством которого устройства с последовательным подключением могут предоставлять идентификатор Plug and Play. Протокол описан в документе «Plug and Play External COM Device Specification», но найти его практически невозможно (из-за сложностей индексирования, обусловленных отсутствием слова «serial» в названии). Я нашел копию по адресу http://www.microsoft.com/hwdev/resources/ specs/pnpcom.asp, но когда вы начнете его искать, скорее всего, он уже куда-нибудь переместится.
Microsoft реализует последовательный протокол РпР в верхнем фильтре устройства с именем SERENUM.SYS. SERENUM использует стандартный интерфейс последовательного порта для взаимодействия с драйвером последовательного порта. Обнаружив строку с идентификатором РпР, он действует как драйвер шины и создает объект физического устройства (PDO). Затем РпР Manager загружает функциональный и фильтрующий драйверы стандартным способом.
На рис. 16.5 изображен стек драйверов для последовательной мыши. SERENUM. SYS встречается на рисунке дважды, как верхний фильтр устройства для последовательного порта (функциональным драйвером которого является SERIAL.SYS) и как драйвер шины в нижней части стека SERMOUSE. Обратите внимание: MOUCLASS.SYS снова участвует в происходящем и предоставляет унифицированный интерфейс мыши остальным компонентам системы.
Рис. 16.5. Стек драйверов для последовательной мыши
Драйвер DISKPERF
Драйвер DISKPERF из DDK демонстрирует другое применение верхних фильтров. DISKPERF собирает статистику о запросах ввода/вывода к диску, проходящих через него. Собранная статистика выдается посредством запросов WMI, которые могут использоваться любым монитором производительности на базе WMI.
714
Глава 16. Фильтрующие драйверы
Нижние фильтрующие драйверы
Нижние фильтрующие драйверы встречаются гораздо реже, чем верхние фильтры. Поскольку (по определению) нижний фильтрующий драйвер располагается ниже функционального драйвера в стеке РпР, он получает только те IRP, которые функциональный драйвер решит отправить вниз. Для устройства, подключенного к традиционной шине (например, PCI), функциональный драйвер поглощает все IRP, которые представляют интерес, поручая непосредственное выполнение ввода/вывода уровню аппаратных абстракций (HAL). Только IRP управления питанием и РпР с наибольшей вероятностью будут проходить вниз от функционального драйвера. В общем случае фильтрующий драйвер вряд ли что-нибудь может (или должен) делать с этими запросами.
Систему драйверов можно спроектировать так, как показано на рис. 16.6. Идея заключается в создании единого функционального драйвера, который использует нижние фильтрующие драйверы для взаимодействия с оборудованием на различных архитектурах шины. Драйвер встроенного модема на одном из моих ноутбуков построен по этому принципу. Microsoft поставляет функциональный драйвер модема MODEM.SYS. Производитель модема поставляет нижний фильтрующий драйвер, который поддерживает интерфейс стандартного последовательного порта, и два дополнительных нижних фильтра (трудно сказать, зачем они нужны). В действительности концепция аналогична той, на базе которой работают DISK.SYS и MOUCLASS.SYS, — отличаются только имена, присвоенные драйверам. Конечно, по-настоящему важна многоуровневая структура драйверов, а не их имена.
Рис. 16.6. Использование нижних фильтрующих драйверов для обеспечения независимости от типа шины
Иногда я использую нижний фильтрующий драйвер для отслеживания трафика USB, генерируемого функциональным драйвером. Позднее в настоящей
Механика работы фильтрующего драйвера
715
главе будет описан нижний фильтр, который я построил для упрощения диагностики проблем с управлением питанием в моих собственных драйверах. Он выводит отладочные сообщения обо всех запросах IRP_MJ_POWER, сгенерированных функциональным драйвером или переданных им на нижние уровни.
В приложении А описан нижний фильтрующий драйвер с именем WDMSTUB.SYS. WDMSTUB определяет ряд функций ядра, не экспортируемых в Windows 98 и Windows Me. Его оформление в виде нижнего фильтра означает, что эти функции будут определены до того, как система попытается загрузить функциональный драйвер, вызывающий эти функции. В свою очередь, это делает возможным полноценную двоичную совместимость драйверов между Windows 2000, Windows ХР и Windows 98/Ме.
Механика работы фильтрующего драйвера
В этом разделе я опишу основные принципы построения фильтрующего драйвера. Как неоднократно говорилось ранее, фильтрующий драйвер представляет собой разновидность драйверов WDM и содержит функции DriverEntry, AddDevice, диспетчерские функции для IRP управления питанием и РпР и т. д. Как это часто бывает, «дьявол прячется в мелочах».
ПРИМЕР---------------------------------------------------------
Пример FILTER демонстрирует основные положения, описанные в этом разделе.
Функция DriverEntry
Функция DriverEntry фильтрующего драйвера сходна с одноименной функцией функционального драйвера. Главное отличие заключается в том, что фильтрующий драйвер должен установить диспетчерские функции для всех типов IRP, а не только для тех типов, которые он собирается обрабатывать:
extern "С" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
DriverObject->DriverUn]oad = DriverUnload;
DriverObject->DriverExtension->AddDevice = AddDevice;
for (int i = 0; 1 < arraysize(DriverObject->MajorFunction); ++1) DriverObject->MajorFunction[1] = DispatchAny;
DriverObject->MajorFunction[IRPJUJDOWER] = DispatchPower;
DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
return STATUS_SUCCESS;
}
Фильтрующий драйвер, как и любой другой, содержит функции DriverUnload и AddDevice. Я занес в основную таблицу функций адрес функции DispatchAny, которая передает любой произвольный запрос вниз по стеку. Для запросов управления питанием и РпР задаются диспетчерские функции.
716
Глава 16. Фильтрующие драйверы
Причина, по которой фильтрующий драйвер должен обрабатывать все возможные типы IRP, связана с порядком вызова функций AddDevice по отношению к DriverEntry. В общем случае фильтрующий драйвер должен поддерживать все типы IRP, которые поддерживаются драйвером, находящимся непосредственно под ним. Если бы фильтр оставил элемент таблицы MajorFunction в состоянии по умолчанию, то IRP этого типа отклонялись бы с кодом STATUS„INVALID_DEVICE„ REQUEST (I/O Manager содержит диспетчерскую функцию по умолчанию, которая просто завершает запрос с этим кодом. В исходном состоянии объекта драйвера все элементы MajorFunction содержат указатели на функцию по умолчанию.). Тем не менее, до выполнения AddDevice мы еще не знаем, какой объект (или объекты) устройства располагаются ниже. Конечно, можно было бы проанализировать таблицу диспетчерских функций для каждого нижнего драйвера в AddDevice и занести необходимые указатели в свою таблицу MajorFunction, но помните, что драйвер может принадлежать нескольким стекам устройств, поэтому вызовов AddDevice тоже может быть несколько. Проще заявить о поддержке всех IRP во время выполнения DriverEntry.
Функция AddDevice
Функции AddDevice фильтрующих драйверов вызываются для каждого подходящего устройства. Функция loCreateDevice используется для создания безымянного объекта устройства, а функция loAttachDevicToDeviceStack — для подключения его к стеку драйверов. Кроме того, вам придется скопировать несколько параметров из объекта устройства, находящегося на более низком уровне:
NTSTATUS AddDev1ce(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
{
PDEVICE-OBJECT fido;
NTSTATUS status = loCreateDevIcetDriverObject, slzeof(DEVICE-EXTENSION), NULL, GetDeviceTypeToUse(pdo), 0, FALSE, &f1do):
If (!NT_SUCCESS(status)) return status;
PDEVICE-EXTENSION pdx = (PDEVICE-EXTENSION) fido->Dev1ceExtension; do
{
pdx->Dev1ce0bject = fl do; pdx->Pdo = pdo;
PDEVICE_OBJECT fdo = IoAttachDev1ceToDev1ceStack(f1do, pdo); pdx->LowerDev1ce0bject = fdo:
fido->Flags = fdo->Flags &
(DO-DIRECT_IO DO-BUFFERED-IO DO_POWER_PAGABLE);
f1do->Flags &= ~DO_DEVICE_INITIALIZING;
}
while (FALSE);
If (!NT_SUCCESS(status)) loDeleteDevlce(fldo); return status;
}
Механика работы фильтрующего драйвера
717
Фрагменты, отличающиеся от аналогичных фрагментов функционального драйвера, выделены жирным шрифтом. В сущности, мы используем специфический метод определения типа устройства и переносим несколько битовых флагов из следующего объекта устройства, находящегося под нашим.
GetDeviceTypeToUse — локальная функция, определяющая тип устройства для объекта устройства, находящегося непосредственно под нашим. Функция loAttachDeviceToDeviceStack еще не вызывалась, поэтому мы не располагаем стандартным указателем LowerDeviceObject. Функция GetDeviceTypeToUse использует loGetAttachedDeviceReference для получения указателя на объект устройства, в настоящее время находящийся на вершине стека, корнем которого является наш объект PDO, и возвращает тип этого объекта устройства. Причина, по которой функция начинается с этого вызова, заключается в следующем: если фильтруется объект дискового запоминающего устройства, мы должны указать правильный код типа при вызове loCreateDevice, чтобы I/O Manager создал дополнительную структуру данных, называемую блоком параметров тома (VPB, Volume Parameters Block). Если некоторые объекты устройства в стеке не будут обладать VPB, это может привести к сбоям некоторых драйверов файловой системы Windows 2000.
Вместо характеристик объекта устройства передается значение 0. PnP Manager автоматически распространяет все критические флаги характеристик вверх и вниз по стеку. Для фильтрующего драйвера было бы неверно принудительно использовать флаг FILE_FLAG_„SECURE_OPEN, действующий на весь стек драйверов (разве что для исправления ошибки в функциональном драйвере, в котором забыли установить этот флаг).
Флаги буферизации копируются из нижнего объекта устройства, потому что I/O Manager принимает некоторые из своих решений на основании того, что он видит в верхнем объекте устройства. В частности, использование IRP чтения и записи списка дескрипторов памяти или системного буфера зависит от состояния флагов DO_DIRECTJO и DO_.BUFFERED._IO верхнего объекта. Теперь становится ясно, почему функциональный драйвер должен установить один из этих флагов в AddDevice и не может изменить решение позднее: фильтрующий драйвер копирует флаги во время AddDevice и не сможет узнать о том, что они были изменены нижним драйвером.
Мы копируем флаг DO_POWER__PAGABLE из нижнего объекта устройства для соблюдения туманных ограничений, накладываемых Power Manager (см. врезку). Другой аспект той же проблемы будет затронут в диспетчерской функции IRP_MK_PNP, Распространять флаг DO_POWERJNRUSH не нужно — для Power Manager достаточно, чтобы этот флаг был установлен в одном объекте устройства.
Копировать поля SectorSize или AlignmentRequirement нижележащего объекта устройства не нужно — loAttachDeviceToDeviceStack сделает это автоматически. Копировать флаги Characteristics тоже не обязательно, так как PnP Manager автоматически сделает это после того, как стек устройства будет полностью построен, и после применения всех замен из реестра.
718
Глава 16. Фильтрующие драйверы
ФЛАГ DO„POWER_PAGABLE —----------------------------------------—--------------
Драйверы должны активно использовать флаг DO_POWER_PAGABLE, чтобы обойти некоторые странности Power Manager в Windows 2000. Если этот флаг установлен для объекта устройства, Power Manager отправляет запросы IRP_MN_SET_POWER и IRP_MN_QUERY_POWER соответствующему драйверу на уровне PASSIVE-LEVEL. Если флаг сброшен, Power Manager отправляет эти IRP на уровне DISPATCH_LEVEL (запросы IRP_MN_WAIT_WAKE и IRP_MN_POWER-SEQUENCE всегда отправляются на уровне PASSIVE_LEVEL).
PoCallDriver действует как своего рода преобразователь уровня IRQL для запросов SET-POWER и QUERY_POWER. Драйвер может пересылать эти IRP на уровне PASSIVE-LEVEL или DISPATCHLEVEL в зависимости от того, на каком уровне сам драйвер принял IRP и является ли пересылка частью функции завершения ввода/вывода. При необходимости PoCallDriver поднимает IRQL или планирует рабочий элемент для вызова следующего драйвера на правильном уровне IRQL.
Тем не менее, в Windows 2000 Power Manager протестует (а конкретно — выдает фатальный сбой), обнаружив неперемещаемый объект устройства выше перемещаемого объекта в процессе построения внутренних списков при подготовке к операциям управления питанием. Driver Verifier во всех системах, начиная с Windows 2000, также проверяет это условие. Из-за него вы должны постоянно следить за тем, чтобы у объекта устройства был установлен флаг DO-POWER_PAGABLE, если этот флаг установлен у нижележащего драйвера (а также помочь с соблюдением этого правила вышележащим драйвером). Первым аспектом выполнения этого правила должно стать согласование флага с нижележащим объектом устройства во время выполнения AddDevice.
Обычно нет необходимости присваивать объекту FiDO собственное имя. Если функциональный драйвер присваивает имя объекту устройства и создает символическую ссылку или если функциональный драйвер регистрирует интерфейс устройства для своего объекта устройства, приложение получит возможность открыть манипулятор для устройства. Каждый IRP, отправляемый устройству, сначала отправляется драйверу верхнего объекта FiDO, независимо от того, обладает ли этот объект FiDO собственным именем. Далее в этой главе я объясню, почему создание дополнительного именованного объекта устройства позволяет вашим приложениям обратиться к фильтру, находящемуся в середине стека драйверов.
Driver Verifier следит за тем, чтобы флагам и типу объекта устройства были J заданы указанные значения.
Функция DispatchAny
Фильтрующие драйверы пишутся для того, чтобы каким-то образом изменять поведение устройства. Следовательно, входящие в них диспетчерские функции должны что-то делать с некоторыми из поступающих IRP, однако большинство IRP будет передаваться вниз по стеку. Вы уже знаете, как это делается:
NTSTATUS D1spatchAny(PDEVICEJ3BJECT fido, PIRP Irp)
{
PDEVICEJXTENSION pdx = (PDEVICEJXTENSION) fido->Dev1 ceExtension;
NTSTATUS status =* IoAcqu1reRemoveLock(&pdx->RenioveLock, Irp);
If (!NT_SUCCESS(status))
return CompleteRequestUrp, status, 0);
loSklpCurrentlrpStackLocatlon(Irp);
status = loCal]Dr1ver(pdx->LowerDev1ceObject, Irp);
Механика работы фильтрующего драйвера
719
IoReleaseRemoveLock(&pdx->RenioveLock, In);
return status;
}
ПРИМЕЧАНИЕ-----------------------------------------------------------------------------
При программировании фильтрующего драйвера следует руководствоваться принципом «Не навреди». Иначе говоря, вы не должны нарушить работу драйверов, находящихся выше или ниже, из-за изменения их рабочей среды или потока IRP.
В этот момент необходимо заново вернуться к обсуждению блокировки удаления в главе 6. Вспомните, что мы захватываем блокировку для каждого IRP, передаваемого вниз по стеку РпР, и освобождаем ее после завершения этого IRP. Наша диспетчерская функция IRP_MN_REMOVE_DEVICE вызывает loReleaseRemoveLockAndWait и убеждается в том, что все такие IRP прошли через нижний драйвер, прежде чем вызывать loDetachDevice и возвращать управление PnP Manager. Эти шаги предотвращают удаление нижнего драйвера во время обработки отправленного нами IRP.
Функция DispatchAny использует блокировку удаления, пытаясь отчасти реализовать нашу ответственность за удержание нижнего драйвера в памяти. Но, как обсуждалось в главе 6, в защите, которую мы пытаемся предоставить, существует небольшой дефект. Наша защита нижнего драйвера перестает действовать сразу же после освобождения блокировки в конце DispatchAny. Если нижний драйвер (или другой драйвер, расположенный еще ниже) вернет из диспетчерской функции STATUS_PENDING, блокировка освободится слишком рано. Чтобы защита была абсолютно надежной, необходимо установить функцию завершения (при помощи loSetCompletionRoutineEx, если эта функция доступна), которая освободит блокировку удаления.
Тем не менее, установка функции завершения для каждого IRP в каждом фильтрующем драйвере не может считаться приемлемым решением проблемы преждевременной выгрузки, так как оно слишком сильно повышает затраты на обработку каждого IRP только для того, чтобы предотвратить маловероятную ситуацию «гонки». Более того, многие тысячи уже существующих фильтрующих драйверов не идут на столь крайние меры. Соответственно, компании Microsoft придется поискать более общее решение проблемы. Более того, есть смысл вообще не беспокоиться о блокировке удаления в функции DispatchAny фильтрующего драйвера, потому что эта функция все же обеспечивает некоторую защиту относительно небольшой ценой, а большинство IRP, проходящих через фильтрующий драйвер, изначально безопасны, поскольку Windows ХР даже не отправляет запрос IRP_MN_REMOVE_DEVICE в стек устройства при наличии открытых манипуляторов.
Функция DispatchPower
Диспетчерская функция IRP_MJ_POWER в фильтрующем драйвере прямолинейна и (почти) тривиальна:
NTSTATUS DispatchPower!PDEVICE_OBJECT fido, PIRP Irp)
{
PDEVICEJXTENSION pdx = (PDEVICE_EXTENSION) f1do->Device0bject:
720
Глава 16. Фильтрующие драйверы
PoStartNextPowerIrp(Irp):
NTSTATUS status = IoAcqu1reRernoveLock(&pdx->RemoveLock, Irp);
if (!NT_SUCCESS(status))
return CompleteRequestCIrp. status, 0);
loSki pCurrentIrpStackLocatlon(Irp);
status = PoCalIDriver(pdx->LowerDeviceObject, Irp);
IoReleaseRemoveLock(&pdx->RemoveLock, Irp): return status;
}
Единственное, что заслуживает внимания в этой функции, — то, что, в отличие от всех остальных рассмотренных нами функций DispatchPower, она действительно просто кодируется.
Функция DispatchPnp
Диспетчерская функция для IRP_MJ_PNP обрабатывает несколько особых случаев:
NTSTATUS DispatchPnp(PDEVICE_OBJECT fido, PIRP Irp)
{
PIO_STACK_LOCATION stack = loGetCurrentlrpStackLocation(Irp);
ULONG fen = stack->MinorFunction:
NTSTATUS status;
PDEVICEJXTENSION pdx =
(PDEVICE_EXTENSION) fido->Dev1ceExtension;
status = IoAcquireRemoveLock(&pdx->RemoveLock, Irp);
if GNT_SUCCESS(status))
return CompleteRequestUrp, status. 0);
If (fen == IRP_MN_DEVICE_USAGE_NOTIFICATION) { if (!fido->AttachedDev1ce
(fido->AttachedDevice->Flags & DO__POWER_PAGABLE)) fido->Flags = DO_POWER_PAGABLE;
loCopyCurrentlrpStackLocationToNext(Irp);
loSetCompleti onRouti net Irp, (PIO_COMPLETION_ROUTINE)
UsageNoti f1cati onCompletionRouti ne,
(PVOID) pdx, TRUE, TRUE, TRUE);
return loCalIDriver(pdx->LowerDeviceObject, Irp);
}
if (fen == IRP_MN_START_DEVICE)
{
loCopyCurrentlrpStackLocationToNext(Irp);
loSetCompletlonRoutinedrp, (PIO_COMPLETION_ROUTINE)
StartDevi ceCompleti onRouti ne, (PVOID) pdx, TRUE, TRUE, TRUE);
Механика работы фильтрующего драйвера
721
return IoCallDriver(pdx->LowerDeviceObject, Irp);
}
if (fen == IRP_MN_REMOVE_DEVICE)
loSki pCurrentlrpStackLocati on(Irp);
status = IoCallDriver(pdx->LowerDeviceObject, Irp); IoReleaseRemoveLockAndWait(&pdx->RemoveLock, Irp);
RemoveDevice(fido):
return status;
}
loSkipCurrentlrpStackLocati on(Irp);
status == IoCallDriver(pdx->LowerDeviceObject, Irp);
IoReleaseRemoveLock(&pdx->RemoveLock, Irp);
return status:
IRP_MN_DEVICE_USAGE_NOTIFICATION
Как говорилось в главе 6, оповещения об использовании (usage notifications) информируют функциональный драйвер о том, что дисковое устройство содержит или не содержит файл подкачки, файл дампа или спящего режима. В ответ на поступившее оповещение функциональный драйвер может изменить состояние своего флага DO_POWER_PAGABLE. В этом случае нам также придется произвести соответствующее изменение в состоянии флага.
При перемещении IRP вниз по стеку мы устанавливаем DO_POWER_PAGABLE, если наш драйвер находится на вершине стека РпР или если находящийся выше драйвер установил этот флаг. В общем случае ссылка на поле AttachedDevice небезопасна, потому что мы не имеем доступа к внутренней спин-блокировке, защищающей стек объектов устройств. Тем не менее, в этом конкретном контексте ссылка безопасна, потому что PnP Manager не будет изменять стек при наличии необработанного IRP оповещения об использовании.
ПРИМЕЧАНИЕ---------------------------------------------------------------------
Если другой драйвер присоединяется к стеку устройства в то время, как оповещение об использовании перемещается вниз по стеку, возникает небольшая вероятность того, что неперемещаемый драйвер окажется над перемещаемым драйвером. Компания Microsoft в своих фильтрующих драйверах не беспокоится об этой возможности, вероятно, нам с вами стоит последовать ее примеру.
При обратном перемещении IRP вверх по стеку наша функция завершения распространяет состояние флага с нижнего драйвера. Так обеспечивается выполнение правила, согласно которому неперемещаемый обработчик не должен находиться над перемещаемым:
NTSTATUS UsageNoti fi cationCompletionRouti ne(
PDEVICEJ3BJECT fido, PIRP Irp, PDEVICE_EXTENSION pdx)
722
Глава 16. Фильтрующие драйверы
If (Irp->Pend1ngReturned) loMarklrpPending(Irp):
If (!(pdx->LowerDeviceObject->Flags & DO_POWER_PAGABLE)) f1do->F1ags &= ~DO_POWER_PAGABLE:
IoReleaseRemoveLock(&pdx->RemoveLock. Irp); return STATUS_SUCCESS;
}
Для правильной работы этого кода драйвер на вершине стека должен установить флаг DO_POWER_PAGABLE на пути вниз, чтобы мы установили его в своей диспетчерской функции. Далее мы оставляем флаг установленным, если нижний драйвер устанавливает его, и сбрасываем, если это делает нижний драйвер.
IRP_MN_START_DEVICE
Обработка IRP_MN_START_DEVICE позволяет нам распространять флаг FILE_RE-MOVABLE_MEDIA. Этот флаг нс устанавливается в AddDevice (потому что функциональный драйвер в это время еще не может взаимодействовать с устройством), но должен быть правильно установлен в верхнем объекте устройства стека. Функция завершения выглядит так:
NTSTATUS StartDev1ceComplet1onRout1ne(PDEVICE_0BJECT fdo.
PIRP Irp, PDEVICE-EXTENSION pdx)
{
If (Irp->PendingReturned) loMarklrpPending(Irp);
If (pdx->LowerDev1ceObject->Characterist1cs
& FILE_REMOVABLE_MEDIA)
fldo->Character1sties = FILE_REMOVABLE_MEDIA;
IoReleaseRemoveLock(&pdx->RemoveLock, Irp); return STATUS-SUCCESS;
}
IRP_J4N_REMOVE__DEVICE
Запрос IRP_MN_REMOVE_DEVICE требует специальной обработки, потому что здесь выполняется RemoveDevice с вызовами loDetachDevice и loDeleteDevice. Тем не менее, обработчик получается гораздо проще, чем в функциональном драйвере, поскольку нам не приходится беспокоиться об отмене очередей и освобождении ресурсов ввода/вывода.
Установка фильтрующего драйвера
Существуют всего два действительно простых сценария установки фильтрующего драйвера: когда фильтр предназначается для целого класса устройств и когда он является частью пакета, включающего функциональный драйвер. Чтобы установить фильтрующий драйвер для существующего устройства, необходимо найти и модифицировать нужные записи реестра, притом делается это более или менее вручную. Проблемы установки будут рассмотрены позднее в этой главе.
Установка фильтрующего драйвера
723
На рис. 2.7 изображена иерархия фильтров устройств и классов. Кратко напомню, что говорилось в главе 2 о порядке загрузки драйверов: сначала РпР Manager загружает нижние фильтры устройств и классов, затем функциональный драйвер и, наконец, верхние фильтры устройств и классов. Нижний фильтр в стеке (не драйвер шины!) первым упоминается в параметре LowerFilters раздела оборудования, а верхний драйвер упоминается последним в параметре UpperFilters раздела класса.
Установка фильтра класса
Чтобы установить фильтр класса вместе с классом, включите дополнительные синтаксические конструкции в раздел [Classlnstall32]:
[Classlnstal132]
AddReg=C1 a s sInsta1132AddReg
CopyFi1es=CopyClassFI1 tens
[Cl assInstal132AddReg]
HKR,.UpperFI1 tens.0x00010000,foo,bar
[Classlnstal132.Services]
AddService^FOO,.FooAddServlee
AddServ1ce=BAR,.BarAddServIce
[CopyClassFIIters]
foo.sys,.,2
bar.sys,, ,2
Секция AddReg определяет параметр типа REG„MULTI„SZ с двумя строками foo и bar, определяющими имена служб для двух фильтрующих драйверов. В секции [Classlnstall32.Services] используется синтаксис определения служб, показанный в главе 15 для функционального драйвера. В секции [CopyClassFilters] директива [DestinationDirs] (не показана) обеспечивает копирование файлов драйверов в соответствующий каталог.
Нижние фильтры классов определяются параметром LowerFilters с аналогичным синтаксисом.
После того как класс будет определен, в Windows 2000 и последующих системах можно добавлять фильтрующие драйверы в конец списка фильтров при помощи синтаксиса INF следующего вида (сравните с примером CLASFILT в DDK, который содержит больше синтаксических элементов, чем необходимо):
[Version]
S1gnature=$CHICAGO$
[Defaultinstall]
CopyF11es=CopyClassF11 tens
AddReg=F11terAddReg
724
Глава 16. Фильтрующие драйверы
[Defaultinstall.Services]
AddServ1ce=JUNK0LA., Fi 1 terAddSem ce
[SourceDlsksFIles] junkola.sys-1
[SourceDlsksNanies] l="F11ter Install disk"
[DestlnatlonDlrs]
DefaultDestDI r-12
[CopyClassFIIters] junkola.sys,,.2
[Fl1terAddService]
ServlceType-1
ErrorControl=1
StartType=3
Servl ceBI пагу=И2Г\ junkol a. sys
[FIlterAddReg]
HKLM,^CLASSKEY#,UpperFi1ters,0x00010008,junkola
[Strings]
CLASSKEY-System\CurrentControlSet\C1ass\{<GUID класса>}
В этом примере INF-файла интерес представляет только секция [FIlterAddReg], которая использует флаг FLG_ADDREG_APPEND для присоединения нового фильтра к существующему значению UpperFilters (если он не входит в список верхних драйверов). Обратите внимание на необходимость указания GUID класса установки, переопределяемого подобным образом.
ПРИМЕЧАНИЕ---------———----------------— ------------------------—--------------------
В документации DDK синтаксис параметров REG_MULTI_SZ описан недостаточно полно. Чтобы полностью заменить параметр типа REG_MULH_SZ одним или несколькими строковыми значениями, укажите флаг Ох 00010000 и перечислите строки, разделяя их запятыми, как в первом примере этого подраздела. Чтобы присоединить строку к существующему значению, укажите флаг 0x00010008, как во втором примере. Одна директива в секции AddReg позволяет присоединить только одно значение. Впрочем, вы можете использовать несколько директив для одного параметра, и они будут выполнены в порядке следования. Проверка того, что указанная строка уже присутствует в параметре, выполняется без учета регистра. Не существует синтаксиса для включения новой строки в другую позицию параметра, кроме последней.
Чтобы установить фильтр вручную, щелкните правой кнопкой мыши на INF-файле и выберите команду Установить (Install). Другой способ — запустите RUNDLL32 для выполнения функции InstallHinfSection из библиотеки setupapi.dll с аргументом, полученным объединением строки «Defaultinstall» (обратите внимание на завершающий пробел) с именем INF-файла.
Установка фильтрующего драйвера
725
Компания Microsoft не предоставила возможности расширения списка фильтров классов для Windows 98/Ме. Вам придется написать программу установки, которая будет вручную модифицировать раздел класса.
Установка фильтра устройства с функциональным драйвером
Чтобы установить верхний или нижний фильтр устройства с функциональным драйвером в Windows 2000 и последующих системах, достаточно определить параметр UpperFilters или LowerFilters в разделе оборудования, скопировать файлы фильтрующего драйвера в каталог драйверов и определить службы фильтра. Дополнительный синтаксис выглядит примерно так:
[DriverCopyFIles]
foo.sys,,,2
[Driverinstall.ntx86.Services]
AddServ1ce=whatever,2.Driverservice ; <== для функционального драйвера
AddServ1ce=foo,.FooServIce
[FooServIce]
ServiceType=l
ErrorControl=l
StartType=3
Servi ceBI пагу=И2Шоо. sys
[Driverlnstall.ntx86.hw]
AddReg=F1lterAddReg.ntx86
[F11terAddReg.ntx86]
HKR,,UpperFi1ters,0x00010000,foo
Чтобы назначить нижний фильтр, определите параметр LowerFilters в разделе оборудования. Если потребуется задать несколько драйверов, разделите имена служб запятыми в директиве (или директивах) в секции AddReg.
Установка фильтра для существующего устройства
Компания Microsoft не предоставила средств установки фильтрующих драйверов для существующих устройств. При решении этой задачи возникает проблема определения имени раздела оборудования в реестре, которая не имеет общего решения.
Возможная схема установки фильтра установки «постфактум» продемонстрирована в примере FILTJECT.DLL. Код FILTJECT вызывается при помощи системной утилиты RUNDLL32, что делает возможным его использование в разделе RunOnce с установкой из INF-файла (см. INF-файл из примера FILTER).
726
Глава 16. Фильтрующие драйверы
FILTJECT разбирает параметры командной строки, в число которых входит точное описание или строка дружественного имени устройства, для которого требуется организовать фильтрацию. Затем выполняется перечисление всех установленных устройств с целью нахождения всех экземпляров устройства, после чего параметры UpperFilters и LowerFilters изменяются в соответствии со значениями других параметров командной строки.
ПРИМЕЧАНИЕ--------------------------------------------------------------------
В файле FILTER.HTM, описывающем пример FILTER, также описывается синтаксис FILTJECT.DLL.
Пример POWTRACE представляет другую схему фильтрации для существующего устройства — в INF-файле определяется только служба POWTRACE. Далее вы можете вручную отредактировать реестр и добавить параметр LowerFilters для любого устройства, для которого требуется организовать фильтрацию.
Реальные примеры
Изложенная теория закладывает общую основу для написания любых разновидностей фильтрующих драйверов WDM. Несомненно, при написании фильтрующего драйвера для любой конкретной цели вы столкнетесь с множеством ловушек, зависящих от особенностей стеков устройств и позиции драйвера в стеке. В этом разделе рассматриваются дополнительные примеры фильтрующих драйверов.
Нижний фильтр для отслеживания трафика
Честно признаюсь, что у меня постоянно возникают трудности с правильной организацией управления питанием в моих драйверах. Они возникают так часто, что в конечном итоге я написал фильтрующий драйвер POWTRACE. Этот драйвер устанавливается в качестве нижнего фильтра для драйвера, отладкой которого вы занимаетесь. Далее он регистрирует все IRP, связанные с управлением питанием, которые генерируются вашим драйвером. Во внутреннем устройстве POWTRACE нет ничего особо сложного, поэтому я просто рекомендую обратиться к прилагаемым материалам.
Допустим, мы хотим выяснить, как драйверы Microsoft HIDUSB и HIDCLASS обслуживают функцию пробуждения от мыши с интерфейсом USB. Проследив за тем, чтобы записи служб POWTRACE были определены, внесите изменения в раздел оборудования мыши (рис. 16.7). Отключите мышь, подключите ее заново и начинайте просматривать отладочную трассировку в DbgView. На рис. 16.8 показана трассировка, полученная в результате включения пробуждения в Диспетчере устройств (строка 58), перевода компьютера в ждущий режим (строки 60-68) и последующего пробуждения системы щелчком кнопки мыши (строки 70-89).
Реальные примеры
727
Рис. 16.7. Подготовка к использованию POWTRACE
DebugView WSCFiTRZC (UkU)
:-;3®
Pte ЕЙ Captafe
rcs q з ю * « e> »	$ ? г Д
I Debris Print
-«_<i ru_4.<	। . ’	«

57	0.33734874 TOUTEACE - IHP_MJ_PNF (lRP_tlN_QUERY_DEVlCE_EEIATIC№)
53	19.73094960	POUTRACE	-	1ЕР_МЗ_ГСЖР	(IR?_MN_WAIT_VAKE}, FowcrStctc - Eto^^ysto^G leaping!
59	18 73099095	POTTRACK	-	IRP_MJ_PDUEP	(TRP„MN_UiIT„UAFEJ dispatch -ran tine returns 103
60	30.SO?26140	POUTRACE	-	IRP MJ DOVER	(IRP MN QUERY ROVERk SvsteaPcwerState = PovarSystmS leaping 3
61	ЗС.50728Э89	POTTRACE	-	IRPJIJJFOTER	(IRP_MN_QUER¥_POVER'J completes with 0
62	ЗС.Ь0730610	POWTRACE	~	IRP„MJ_POt!ER	(IR?_MN_QUER¥_POOERj aispatch routines returns 0
63	31.19297145 POWTRACE - IEP_MJ_FOVER CIRP„KN„SET_FOVER।, SystaAPowerSt^te - Fo^rSyetejuSleepingS
64	31.19300125 POWTRACE IKP_MJJ?jUER (IRP„MN_SET_POUER)	with 0
65	31 19663700 POUTRACE - IEP_MJ„POVFR (IR?_4N_SET„FOVER j. DevicePoverState = PorerDeviceDZ
66	31.1.9770893	POWTRACE	~	IEP„MJ_P3VER	(IRP_MN„SET_POUER)	completes with 0
67	31.19773714	POUTRACE	-	IEP_MJ_EOUER	(IR?JW_SET_P0VER)	dispatch routine	returns	103
63	31.19775307	POWTRACE	-	IEP_MJ_EOWER	(IR?_MN_SET_FOUER<	dispatch routine	returns	0
69	37 99695141	POUTRACE	~	IRF„MJ_P3UER	(IRP_MN_UAIT_UAKEj	eexpJete^ with 0
7j 37.98698354 POUTRACE - IRF_MJ_POUER (IR?_MN_SET>OVERJ, DevicePowerStM» - РкэтегОетхоаФО
71	37.98702377	POUTPACE	-	IRP MJ POWER	(IRP_MN_SET_P0UERl	dispatch routine	returns	103
72	37.98876198	POWTRACE	-	IEP_MJ,PDUER	(IRP_MN_SET„P0¥ER)	completes with 0
73 37.98877790 POWTRACE - IRP_MJ_1NTENNAL_DEV1CE_CONTROL
74	37.98975987 POWTRACE - IEF„MJ_INTERNAL_DEVICE_CONTROL
75 37.98980429 POUTRACE - IFP_MJ_INTERNAL_DEUICE_CQNTRO1.
76	37.98990207 POWTRACE - IFFJ4J .POWER (IR?^MN..SET„P0VERi. SvsteKPawerEtate * PowerSystemWorking
77	37.98991715	POUTRACE	-	IEP_MJ_PDUER	(IRP_MN_SET_POVER*	completes with 0
78	37.98993308	POWTRACE	-	IEP_MJ_FOTER	(IRP„MN_SET_POUERh DevicePowerState « FhverDeviceDO
79	37.58396521	POWTRACE	-	IEF_MJ^FOWER	(IRF„MN_5ET_FOWER}	dispatch routine	returns	103
00	37.90997034	POWTRACE	-	ITTJMJJPOUER	(IRP_KlI_GET_FOVERj	dispatch routine	returns	0
81	27 99277674	POITIRACE	-	TRP,MJ_TnTERHAL„DEWTCE_CQNTRnT
82	38.00082664	POUTRACE	-	IRP MJ INTERNAL DEVICE CONTROL
83	38.12766651	POWTRACE	-	IRP„MJ_P3VER (IRP_MN_UAIT_UAKE1, PowerState “ FOv©xSystsmSleeping3
84	38.12769751	POWTRACE	-	IRF_MJ„POWER (1R?_MM_WAIT„UAEE} dispatch routine	returns	103
85	38.20730959	FOUTRACE	-	IRF_MJ_FOWER (IRP_MN_SET_FOWER) ooaplet«» with 0
8ъ	38.207 33641	POUTRACE	-	IRE MJ_INTERNAL_DEVICE^CONTROL	Л
87	40.56622001	POWTRACE	-	IRP„MJ~PNP (IRP.„MN_QUERY^.CAPABILITIES)
88	40.56632226	POWTRACE	-	IRP_MJ PNP (IRP_MN_Q'JERY_CAPABIimES)
89	40.56638679	POUTPACE	-	IEP_MJ_PNP (IR?_MN„QUERY-_DEVICE„REIATIO№)	Y
" r^. 
Рис. 16.8. Протокол цикла пробуждения/возобновления работы, сгенерированный POWTRACE
Именованные фильтры
Иногда бывает нужно организовать прямое взаимодействие приложения с фильтрующим драйвером. В простейшем варианте приложение открывает манипулятор устройства, а затем использует приватные операции IOCTL, поглощаемые фильтром. К сожалению, такое решение не всегда приемлемо:
728
Глава 16. Фильтрующие драйверы
О некоторые устройства не позволяют приложению открыть манипулятор. Например, мышь или клавиатура уже открыта для низкоуровневого потока ввода, и открыть второй манипулятор не удастся. Последовательный порт обычно является монопольным устройством и тоже не позволяет открыть второй манипулятор, если порт в настоящее время используется кем-то другим;
О в прохождении трафика IOCTL до вашего драйвера вы полностью зависите от драйверов, находящихся выше вашего. Скажем, драйверы MOUCLASS и KBDCLASS блокируют приватные IOCTL. Таким образом, даже если приложение может открыть манипулятор, оно не сможет обмениваться данными с фильтром в стеке мыши или клавиатуры.
Стандартное решение подобных проблем основано на создании объекта EDO (Extra Device Object), который скрывает объект FiDO в стеке РпР. Эта концепция проиллюстрирована на рис. 16.9.
Рис. 16.9. Объект EDO для фильтрующего драйвера
Для реализации этого решения необходимо определить две разные структуры расширения устройства, имеющие как минимум одно общее поле. Пример:
typedef struct _COMMON_DEVICE_EXTENSION {
ULONG flag;
} COMMON_DEVICE_EXTENSION, *PCOMMON_DEVICEJXTENSION:
typedef struct _DEVICE_EXTENSION : public _COMMON_DEVICE-EXTENSION {
struct _EXTRA_DEVICE_EXTENSION* edx;
} DEVICE-EXTENSION. *PDEVICE_EXTENSION;
typedef struct _EXTRA_DEVICE_EXTENSION : public
_COMMON_DEVICE_EXTENSION {
PDEVICE_EXTENSION pdx;
} EXTRA_DEVICE_EXTENSION, *PEXTRA_DEVICE_EXTENSION;
#define FIDO-EXTENSION 0
tfdeflne EDO_EXTENSION 1
Реальные примеры
729
В функции AddDevice создаются два объекта устройства: объект FiDO, который подключается к стеку РпР, и объект EDO, с которым это не делается. Объекту EDO тоже присваивается уникальное имя. Пример:
NTSTATUS AddDev1ce(PDRIVER_OBJECT Driver Object.
PDEVICE_OBJECT pdo)
{
PDEVICE-OBJECT fido;
IoCreateDev1ce(...);
PDEVICE_EXTENSION pdx =
(PDEVICE_EXTENSION) fido->DeviceExtension;
<и т. д. --см. ранее в этой главе>
pdx->flag = FIDO-EXTENSION;
WCHAR namebuf[64];
static LONG numextra = -1;
-SnwprintfCnamebuf, arrays!ze(namebuf). CWDeviceWNyExtraM", InterlockedlncrementC&numextra)):
UNICODE_STRING edoname;
Rt 1 InitUn!codeStr!ng(&ednoname. namebuf);
IoCreateDev1ce(Dr1ver0bject. sizeof(EXTRA_DEVICE-EXTENSION),
&edoname, FILE_DEVICE_UNKNOWN, 0, FALSE, &edo);
PEXTRA-DEVICE-EXTENSION edx =
(PEXTRA-DEVICE_EXTENSION) edo->Dev1ceExtension;
edx->flag = EDO_EXTENSION;
edx->pdx = pdx;
pdx->edx = edx:
}
Также желательно создать символическую ссылку с уникальным именем, ссылающуюся на EDO. Это имя будет использоваться приложением, когда ему потребуется открыть манипулятор для вашего фильтра.
В каждой диспетчерской функции указатель DeviceExtension объекта устройства сначала преобразуется в PCOMMON_DEVICE_EXTENSION, а затем по содержимому поля флагов вы определяете, какому объекту предназначается IRP — FiDO или EDO. FiDO IRP обрабатываются так, как было показано ранее для обобщенного фильтрующего драйвера. EDO IRP вы обрабатываете так, как считаете нужным. Как минимум, следует успешно завершать запросы IRP_MJ_ CREATE и IRP__IMJ_CLOSE, обращенные к EDO, и отвечать на IOCTL из определяемого вами набора.
Руководствуясь описанными базовыми принципами, вы сможете предоставить доступ к своему фильтрующему драйверу независимо от его положения в стеке драйверов и от того, какой политикой руководствуются вышележащие драйверы по отношению к открытию файлов и приватным IOCTL. Чтобы
730
Глава 16. Фильтрующие драйверы
претворить идею в реальное воплощение, следует проследить еще за рядом деталей:
О обратите внимание на атрибуты безопасности в EDO. В частности, здесь будет уместно использовать функцию ToCreateDeviceSecure, когда она станет доступной в DDK (эта функция, появившаяся незадолго до написания книги, позволяет задать дескриптор безопасности для нового объекта устройства; собственно, это одна из ситуаций, для которых она создавалась);
О используйте инструкции, получаемые по каналу EDO, для управления обработкой IRP в канале FiDO;
О объект EDO должен удаляться одновременно с удалением FiDO;
О объект EDO не входит в стек РпР, поэтому он не получает запросы РпР, управления питанием или WML Более того, вам не удастся создать символическую ссылку на него функцией loRegisterDevicelnterface — придется использовать старый метод с определением имени и созданием символической ссылки.
Фильтры шин
Фильтр шины является особой разновидностью верхнего фильтра, который располагается непосредственно над драйвером шины. Вспомните, что драйвер шины играет две роли: FDO и PDO. Создание верхнего фильтра для FDO-роли драйвера шины является совершенно тривиальной задачей: включите свой драйвер в параметр UpperFilter устройства в разделе оборудования для драйвера шины. Основные трудности с драйвером шины связаны с вставкой в каждый из стеков дочерних устройств над PDO. создаваемых драйвером шины. Реализуемая топология показана на рис. 16.10.
Дочерний стек
Родительский стек
Рис. 16.10. Топология фильтра шины
Управлять объектами FiDO для всех дочерних стеков устройств не так уж сложно. В роли фильтра родительского стека (в роли FDO, другими словами) обращайте особое внимание на запросы IRP_MN_QUERY_DEVICE_RELATIONS для BusRelations. Этот запрос используется PnP Manager для получения у драйвера шины списка всех дочерних устройств. Если вы забыли, как работает этот протокол, обратитесь к главе И. Передавайте эти запросы вниз синхронно (сцена
Реальные примеры
731
рий ForwardAndWait в конце главы 5) и просматривайте списки PDO, возвращаемые драйвером шины. Каждый раз, когда в них впервые встречается дочерний PDO, следует создать объект FiDO дочернего стека и присоединить его к PDO. Если встречавшийся ранее PDO исчезает, отсоедините и удалите ставший ненужным объект FiDO дочернего стека.
Впрочем, создание работоспособного фильтра шины не сводится к созданию и уничтожению FiDO дочерних стеков. Например, стек драйверов USB использует «черный ход», который позволяет USBHUB эффективно взаимодействовать с драйвером хостового контроллера без отправки IRP через все промежуточные драйверы концентраторов, которые могут присутствовать в системе. Чтобы видеть трафик, идущий через «черный ход», фильтр шины USB использует USBD_ RegisterHcFilter в родительском стеке хостового контролллера. У других шин тоже могут существовать аналогичные требования к регистрации.
Фильтры мыши и клавиатуры
Фильтрация часто применяется к входным данным, полученным с мыши и клавиатуры. Вот некоторые применения фильтров такого рода, встречавшихся мне: О системы автоматизированного обучения (особенно при использовании низкоуровневых данных о перемещениях или нажатий клавиш, которые не могут перехватываться в пользовательском режиме);
О приложения для инвалидов;
О автоматизированное тестирование.
Ранее в этой главе я уже приводил примеры стеков драйверов для клавиатур и мышей. На мой взгляд, построение фильтра клавиатуры или мыши проще всего запланировать в виде верхнего фильтра класса, находящегося непосредственно под KBDCLASS или MOUCLASS. Функции DriverEntry и AddDevice будут выглядеть так, как было показано ранее для стандартного фильтрующего драйвера WDM. Вероятно, также стоит создать объект EDO для «внеполосного» взаимодействия с кодом приложения пользовательского режима.
ПРИМЕЧАНИЕ--------------------------------------------------------------
Основные принципы написания фильтра клавиатуры и мыши продемонстрированы в примерах DDK KBFILTR и MOUFILTR.
KBDCLASS и MOUCLASS используют интерфейс прямого вызова для получения отчетов клавиатуры или мыши от драйвера порта, непосредственно работающего с оборудованием (эти два драйвера включены в DDK в виде примеров, поэтому вы можете точно увидеть, как они работают). Чтобы организовать эффективную фильтрацию отчетов, необходимо подключиться к механизму прямого вызова посредством обработки внутренних управляющих запросов IOCTLJNTERNAL_KEYBOARD_CONNECT или IOCTLJNTERNAL_MOUSE_CONNECT. В этих IOCTL используется структура-параметр, объявляемая в KBDMOU.H:
typedef struct _CONNECT_DATA {
IN PDEVICE_OBJECT ClassDeviceObject;
732
Глава 16. Фильтрующие драйверы
IN PVOID ClassService;
} CONNECT_DATA, *PCONNECT_DATA;
Здесь ClassDeviceObject — адрес объекта устройства, принадлежащего KBDCLASS или MOUCLASS, a ClassService ~ адрес функции со следующим абстрактным прототипом:
typedef VOID (*PSERVICE_CALLBACK_ROUTINE) (PVOID Normal Context,
PVOID SystemArgumentl, PVOID SystemArgument2,
PVOID SystemArgument3);
Диспетчерская функция IRP_MJ_INTERNAL_DEVICE_CONTROL вашего фильтрующего драйвера обрабатывает запрос CONNECT, сохраняя значения ClassDeviceObject и ClassService в своем расширении устройства, а затем подставляя адреса вашего объекта устройства и функции обратного вызова перед отправкой IRP вниз по стеку драйверу порта.
После того как подключение прямого вызова будет создано, драйвер порта вызывает функцию обратного вызова ClassService каждый раз, когда происходит событие ввода. Функция обратного вызова должна прочитать некоторое количество отчетов из массива, предоставленного драйвером порта, а вернуть признак количества прочитанных отчетов.
Для фильтра клавиатуры функция обратного вызова обладает следующим прототипом:
VOID KeyboardCallback(PDEVICE_OBJECT fido, PKEYBOARD_INPUT_DATA start, PKEYBOARDJNPUTJATA end. PULONG consumed);
Параметры start и end определяют границы массива структур KEYBOARD_INPUT_ DATA, каждая из которых соответствует одному событию нажатия или отпускания клавиши (см. NTDDKKB.H):
typedef struct _KEYBOARD_INPUT_DATA {
USHORT Unltld;
USHORT MakeCode;
USHORT Flags;
USHORT Reserved;
ULONG ExtraInformation;
} KEYBOARO_INPUT_DATA, *PKEYBOARD_INPUT_DATA:
Поле MakeCode содержит низкоуровневый скан-код, сгенерированный клавиатурой. Скан-код описывает физическую позицию клавиши на клавиатуре. Поле Flags указывает, была ли клавиша нажата (КЕУ_МАКЕ) или отпущена (KEYJ3REAK). Флаговые биты КЕУ_Е0 и KEY_E1 обозначают расширенные состояния для специальных клавиш (таких как SysRq или Pause).
Для фильтра мыши прототип функции обратного вызова выглядит так:
VOID MouseCallback(PDEVICE_OBJECT fido. PMOUSE_INPUT_DATA start, PMOUSE_INPUT_DATA end, PULONG consumed);
Реальные примеры
733
В этой функции параметры start и end соответствуют экземплярам следующей структуры (см. NTDDMOU.H):
typedef struct _MOUSEJNPUTJ)ATA {
USHORT Unitld;
USHORT Flags;
union {
ULONG Buttons;
struct {
USHORT ButtonFlags;
USHORT ButtonData:
};
};
ULONG RawButtons;
LONG LastX;
LONG LastY;
ULONG Extrainformation;
} MOUSE_INPUT_JDATA, *PMOUSE_INPUT_OATA;
Информация отчетов мыши содержится в полях LastX и LastY (смещения или абсолютная позиция) и ButtonFlags (биты, обозначающие события мыши, — например, MOUSE„LEFT_BUTTON_DOWN).
В обоих случаях вы возвращаете (в ^consumed) количество событий, исключенных из массива с границами start и end. Функциональный драйвер сообщает о непоглощенных событиях при помощи функции обратного вызова. Во время тестирования фильтра может легко возникнуть ошибочное предположение, что за один раз вы никогда не получаете более одного отчета, но в действительности драйвер нужно программировать так, чтобы он мог принимать массив отчетов.
Внутри функции обратного вызова возможны следующие действия:
О перенаправление событий посредством их передачи функции обратного вызова драйвера более высокого уровня. Не забывайте, что драйвер более высокого уровня может не поглощать некоторые из перенаправляемых событий;
О удаление событий (отказ от их передачи наверх). Например, вы можете вызвать функции обратного вызова драйвера более высокого уровня для некоторых частей массива, кроме тех событий, которые нужно исключить.
Наконец, вы можете вставить новые события в поток, вызывая функции обратного вызова драйвера более высокого уровня.
Фильтрация для других устройств HID
Очень трудно представить себе полезный фильтрующий драйвер для устройства HID. Чтобы понять причину, необходимо внимательнее присмотреться к стеку устройства HID ~ например, джойстика (рис. 16.11). Функциональным драйвером физического устройства является HIDUSB, мииидрайвер HIDCLASS. Присоединение верхнего фильтра к этому устройству (что достаточно легко сделать средствами, уже рассматривавшимися в этой главе) особой пользы не принесет, потому что HIDCLASS будет отклонять все запросы IRP_MJ_CREATE, отправляемые FDO.
734
Глава 16. Фильтрующие драйверы
Фактически, не существует простого способа получения любых IRP фильтрующим драйвером родительского стека, кроме как с созданием объекта EDO.
Дочерний стек Родительский стек (коллекция Joystick)
Рис. 16.11. Стек драйверов для джойстика
Нередко фильтрация реального устройства вам и не требуется. Вместо этого необходимо фильтровать отчеты, проходящие снизу вверх в дочернем стеке, создаваемом HIDCLASS для каждой коллекции верхнего уровня, экспортируемой реальным устройством. Проблема в том, что вы заранее не знаете, какой идентификатор устройства HIDCLASS будет использовать для PDO коллекций, и не можете изменить INF-файлы, предоставленные Microsoft для классов устройств коллекций, без нарушения цифровой подписи драйверов. Если вы можете обойтись фильтром класса, как для клавиатур и мышей, очень хорошо — просто установите свой фильтр как фильтр класса. Но если вам нужен фильтр, специфический для конкретного устройства, может показаться, что вам не повезло.
В такой ситуации может пригодиться концепция фильтра шины. Установите свой фильтр как верхний фильтр родительского стека и обеспечьте отслеживание запросов BusRelations, как было описано ранее для фильтрующих драйверов шины. В нужный момент можно создать экземпляр FiDO в стеке коллекций. Если вас устраивает, что ваш фильтр будет находиться в нижней позиции стека коллекций, работа закончена. В противном случае придется пускаться на довольно героические усилия — скажем, изменять идентификаторы устройств, полученные по запросу IRP_MN_QUERY_ID, чтобы обеспечить принудительное использование ваших INF-файлов.
Проблемы совместимости с Windows 98/Ме
В контексте материала этой главы между Windows 98/Ме и Windows ХР существует ряд небольших различий.
Фильтры WDM для драйверов VxD
Многие программисты хотят взять фильтрующий драйвер WDM для последовательного или дискового устройства и портировать его в Windows 98/Ме. Из этой затеи ничего не выйдет. В Windows 98/Ме последовательные порты и блочные
Проблемы совместимости с Windows 98/Ме
735
запоминающие устройства обслуживаются драйверами VxD; не существует простого способа включить драйвер WDM в поток запросов ввода/вывода.
Сокращенная запись в INF-файле
Вместо того чтобы пытаться каким-то образом сохранить значение REG_MULTI_SZ в Windows 98/Ме (где этот тип реестровых данных вообще не поддерживается), я предпочитаю просто перечислить их в разделе драйвера:
[DrlverAddReg]
HKR, .DevLoader, ,*ntkern
HKR,.NTMPDriver,, \
"wdmstub.sys,powtrace.sys,whatever.sys,f11 ter.sys”
Такое решение работает, потому что загрузчики устройств в Windows 98/Ме, в том числе и NTKERN, неизменно вызывают функцию CONF1GMG, а эта функция загружает все драйверы, указанные в строке, разделенной запятыми.
Фильтрующие драйверы классов
Поскольку Windows 98/Ме не поддерживает тип параметров REG_MULTI_SZ, для указания фильтров классов используются двоичные параметры. Пример:
[Classinstall]
AddReg=ClassInstal 1 AddReg
CopyFI1es=CopyClassFi1 tens
[Cl ass InstallAddReg]
HKR,.UpperFiIters,1, 66, 6f, 6f, 2e, 73, 79, 73, 00, \
62, 61, 72, 2e, 73, 79, 63. 00, 00
Приведенный фрагмент определяет параметр UpperFilters типа REG_BINARY, содержащий строку foo.sys\0bar.sys\0\0. Обратите внимание: указывать нужно имя файла, а не имя службы.
Д Решение проблем несовмести мости между платформами
Многие главы в этой книге завершались разделом, в котором перечислялись проблемы совместимости с Microsoft Windows 98/Ме. Компания Microsoft изначально планировала, что один двоичный файл драйвера будет использоваться на всех платформах WDM, включая Windows 98, Windows 98 Second Edition, Windows Me, Windows 2000, Windows ХР и в последующих системах. Как ни печально, столь благородная цель на практике оказалась трудно достижимой. С того времени, как система Windows 98 заработала на миллионах PC, она продолжала развиваться, а компания Microsoft добавляла в нее многочисленные сервисные функции режима ядра, не поддерживаемые более ранними системами. Если в драйвере WDM вызывается одна из таких функций, система попросту не загрузит драйвер, потому что она не сможет разрешить ссылку на символическое имя функции. В этом приложении рассматриваются некоторые обходные решения, которые позволяют все равно использовать один двоичный файл.
Определение версии операционной системы
Windows 2000 и последующие системы поддерживают функцию PsGetVersion, идеально подходящую для определения текущей платформы. К сожалению, в Win' dows 98/Ме эта функция не поддерживается. Тем не менее, на всех платформах имеется одна общая функция:
BOOLEAN loIsWdmVersIonAvai1аЫeCmajor, minor);
Эта функция возвращает TRUE, если платформа поддерживает интерфейсы драйверов WDM на указанном уровне. Пример WHICOS в прилагаемых материалах демонстрирует один из способов использования loIsWdmVersionAvailable для идентификации платформы на основании информации из табл. А.1.
Например, следующий вызов проверяет, выполняется ли код на платформе Windows ХР:
BOOLEAN IsXP = IoIsWdmVers1onAva11able(l, 0x20):
Динамическая компоновка
737
Таблица А.1. Версии WDM и платформы
Платформа	Поддерживаемая версия WDM
Windows 98 «Gold» и Second Edition	1.0
Windows Me	1.05
Windows 2000	1.10
Windows XP	1.20
Windows .NET	1.30
Система Windows 98 выходила в двух версиях: исходной («золотой») и Second Edition. Функция loIsWdmVersionAvailable выдает версию 1.0 для обеих систем. Если вам потребуется различить две версии Windows 98, воспользуйтесь трюком, представленным в примере WHICHOS, с проверкой поля ServiceKeyName структуры DriverExtension:
BOOLEAN W1n98SE =
DriverObject->DriverExtension->ServiceKeyName.Length != 0;
Кстати говоря, я использовал этот трюк только в одной из моих функций DriverEntry. По имеющейся у меня информации, поле ServiceKeyName в будущем может измениться.
Конечно, основные различия между платформами связаны с существованием двух линеек: Windows 2000 и Windows 98/Ме. Во всех примерах драйверов в прилагаемых материалах определяется глобальная переменная win98, инициализируемая в DriverEntry на основании результата вызова loIsWdmVersionAvailable:
win98 = !loIsWdmVersionAvai1able(1, 0x10):
Динамическая компоновка
Как известно, в системах Windows для связывания приложений и драйверов с системными библиотеками широко применяется механизм динамической компоновки. Как в режиме ядра, так и в пользовательском режиме система отказывается загружать исполняемые модули, содержащие ссылки на несуществующую библиотеку или точку входа. Если вы обладаете большим опытом прикладного программирования для платформ Windows, возможно, вам знакома функция Win32 API GetProcAddress, которая позволяет получить указатель на экспортируемую функцию во время выполнения. Программисты часто используют GetProcAddress для динамического разрешения импорта символических имен, чтобы устранить проблемы с загрузкой приложений.
В Windows 2000 и последующих системах поддерживается функция MmGet-SystemRoutineAddress для поиска точек входа ядра или уровня HAL. В сущности, она представляет собой версию GetProcAddress для режима ядра, например (см. пример SPINLOCK в главе 4):
typedef VOID (FASTCALL *KEACQUIREINSTACKQUEUEDSPINLOCK) (PKSPIN_LOCK, PKLOCK_QUEUE_HANDLE);
738
Приложение А. Решение проблем несовместимости между платформами
KEACQUIREINSTACKQUEUEDSPINLOCK pKeAcqui relnStackQueuedSpInLock;
UNICODEJTRING us;
RtlInitUnlcodeStringC&us, L"KeAcqu1relnStackQueuedSpinLock");
pKeAcquirelnStackQueuedSpinLock = (KEACQUIREINSTACKQUEUEDSPINLOCK) MmGetSystemRoutineAddress(&us);
Чтобы эффективно использовать функцию MmGetSystemRoutineAddress, необходимо учитывать некоторые обстоятельства:
О функция ищет заданное символическое имя только в NTOSKRNL.EXE (ядро) и HAL.DLL (уровень аппаратных абстракций). Она не может использоваться для динамического разрешения точек входа в других драйверах или системных компонентах;
О в Windows 98/Ме эта функция не поддерживается (но далее описывается библиотека WDMSTUB, которая обеспечивает ее поддержку);
О вы должны использовать точно такое же символическое имя, которое используется ядром. Возможно, придется отследить цепочку макроопределений по файлам WDM.H или NTDDK.H.
Проверка совместимости платформ
Программа WDMCHECK в прилагаемых материалах проверяет драйвер WDM на совместимость импорта для платформ Windows 98 и Windows Me. Чтобы воспользоваться ею, выполните следующие действия:
1.	Разработайте свой драйвер в Windows ХР.
2.	Скопируйте двоичный файл драйвера в каталог %windir%\system32\drivers в системе Windows 98/Ме.
3.	Выберите каталог драйверов в качестве текущего каталога.
4.	Запустите WDMCHECK и передайте в единственном аргументе командной строки имя драйвера.
WDMCHECK выводит список всех импортируемых символических имен, не экспортируемых операционной системой или установленными библиотеками WDM. На рис. А.1 показан результат запуска WDMCHECK для драйвера, не имеющего неразрешенных импортированных имен.
Module has no missing import links ?
Рис. А.1. Успешное выполнение WDMCHECK
Сравните рис. А.1 с рис. А.2, на котором представлены результаты для драйвера с тремя функциями, не поддерживаемыми в Windows Me.
Проверка совместимости платформ
739
WdmCheck
Module uses the following missing functions;
PdSetSystemState
MpiGelSystemRoutineAddress loReuselrp
Рис. A.2. Результаты WDMCHECK для драйвера с отсутствующими импортируемыми именам,-
WDMCHECK анализирует секции импорта в драйвере и пытается раз, -шить каждое импортируемое символическое имя. Для символических имен, импортируемых из NTOSKRNL.EXE, HAL.DLL, NDIS.SYS и SCSI.SYS, программа вызывает вспомогательный драйвер VxD. В свою очередь, VxD использует сервисн;. функцию _PELDR_GetProcAddress для разрешения имени (кстати, эта же функци -используется системным загрузчиком при попытке загрузить ваш драйвер). Для символических имен, импортируемых из других модулей, WDMCHECK открывает целевой модуль и просматривает его таблицу экспортируемых символических имен. Фактически, этот алгоритм воспроизводит процедуру разрешения ссылок системой, а следовательно, обеспечивает достаточно надежную проверку.
Отсутствующие импортируемые имена
Dependency Walker							X	
ЕД	Wfndow ,Help ИГЗ Мйн %1в1тГ^Г		M'--- - -			-		„ |д	L2L
S О MISSINGIMPORT.SYS	Ordinal.*					L Function	/	J Entry Point	22
Э-ЙЗ NTOSKRNL.EXE	® N/A ® N/A IS n/a  n/a  n/a S3 N/A fs.nia <.r<			329__ftbc£LL49>		TnDRlRteDpvirp	...... Not Round	
5JHAL.DLL : Й-О BOOTVID.DLL лз ntoskrnl.exe -лЗ HAL.DLL ВС HAL.DLL i ' NTOSKRNL.EXE £ . 1 GENERIC.SYS ’ 5! NTOSKRNL.EXE ll HAL.DLL				332 (0x014C) a^-5 {nvnino}		loDetachDevice T^fr=tlry..JM	Not Bound	
				487 (0X01E7) 563 (0x0233) 928 (ОхОЗАО) 949. (ЛхПЗВ^		KeAcqurelnterruptSpinLock KeReleaselnterruptSpinLock RtlAssert RHC nnvt.lnirrirlftSlT'inn	Not Bound Not Bound Not Bound Nnt Rni inri^	! >L.
	Ordinal ,•		|Hint			] Function	... .	. . .	I Entry Point	A !
	@	1	(0x0001) @	2	(0x0002) @	3	(0x0003) @	4	(0x0004) 5 (0x0005) @	6	(0x0006) 7 (П^ППГГ7\			48 83 85 89 94 96 1 17	(0x0030) (0x0053) (0x0055) (0x0059) (ОхООБЕ) (0x0060)	ExAcquireFastMutexUnsafe ExInterfeckedAddLargeS^atistic ExInterlockedCompareExchange64 ExInterlockedFlushSList ExInterlockedPopEntrySList ExInterlockedPushEntrySList Pv0al«*ai.*-eF»irbMr iHevi kysRs	0x00061240 — 0x00061270 0x0006 KES 0X000613F8 0x00061360 0x00061304 ПуПЛПА1 -УС.- _2_ 		!±	
Module л	Tkne;5tamp	Sze	Attributes	Machine	Subsystem 	<E>ebug	Base	Fife Ver	Product * er
О BOOTVID.DLL	12/07/99 12:00p	10,784		Intel x86	Native	No	0x80010000	5.0.2172.1	5.0.2172 1
П GENERIC.SYS	09/04/02 6;21a	38,144		Intel x86	Native	Yes	0x00010000	2.0.0.0	2.0.0.0
О HAL.DLL	12/07/99 12:00p	82,048		Intel x86	Native	No	0x80010000	5.0.2171.1	5.0.21’1
"Л MISSINGIMPORT.SYS	09/14/02 6:10a	5,376	A	Intel x86	Native	Yes	0x00010000	1.0.0.0	1.0.0.0
О ntoskrnl.exe M....	-f..	12/07/99 12:00p	1,611,712		Intel x86	Native	No	0x00400000	5.0.2195.1	5,0.21_
For Help, press Fl									
Рис. А.З. DEPENDS показывает отсутствующие импортируемые имена
740
Приложение А. Решение проблем несовместимости между платформами
Чтобы проверить драйвер на совместимость с Windows 2000 и выше, скопируйте его вручную в каталог драйверов в целевой системе и запустите утилиту DEPENDS из Platform SDK. DEPENDS помечает все неопределенные импортированные имена. На рис. А.З показан пример запуска DEPENDS в Windows 2000 для модуля, использующего две функции, добавленные в Windows ХР.
Определение заглушек для функций режима ядра в Win98/Me
Пример WDMSTUB в прилагаемых материалах представляет собой нижний фильтрующий драйвер, в котором определяется ряд функций ядра, отсутствующих в Windows 98/Ме. Работа WDMSTUB основана на принципе, использованном Microsoft для портирования нескольких сотен вспомогательных функций режима ядра из Microsoft Windows NT в Windows 98/Ме, то есть расширении таблиц символических имен, используемых загрузчиком времени выполнения при разрешении ссылок импорта. Чтобы расширить таблицу символических имен, необходимо сначала определить три таблицы данных, которые будут находиться в памяти:
О таблица имен с именами определяемых функций;
О таблица адресов с адресами функций;
О таблица порядковых номеров, связывающая таблицы имен и адресов.
Примеры элементов таблиц из WDMSTUB:
static char* names □ = {
’'PoReglsterSystemState",
"ExSystemTimeToLocalTime".
}:
static WORD ordinals[] = {
0,
6,
};
static PFN addresses^ = {
(PFN) PoReglsterSystemState,
(PFN) ExSystemTimeToLocalTime,
Определение заглушек для функций режима ядра в Win98/Me
741
Таблица порядковых номеров предоставляет индексы в таблице addresses для записей таблицы names. Иначе говоря, функция с именем names[i] имеет адрес address[ordinals[i]].
Если бы не проблема с совместимостью версий, которую я вскоре опишу, функцию _PELDR_AddExportTable можно было бы вызвать следующим образом:
HPEEXPORTTABLE hExportTable = 0;
extern "С" BOOL OnDevIcelnit(DWORD dwRefData)
{
_PELDR_AddExportTable(&hExportTable,
"ntoskrnl.exe",
arrays!ze(addresses), // <== don't do It this way!
arrays!ze(names). 0, (PVOID*) names.
ordinals, addresses, NULL);
return TRUE;
Вызов _PELDR__AddExportTable расширяет таблицу символических имен, которая используется загрузчиком при попытке разрешения ссылок импорта из l\ITOSKRNL.EXE, то есть ядра Windows ХР. NTKERN.VXD, главный вспомогательный модуль драйверов WDM в Windows 98/Ме, инициализирует эту таблицу адресами нескольких сотен поддерживаемых функций. Таким образом, WDMSTUB, фактически, является расширением NTKERN.
Совместимость версий
Проблема совместимости, о которой я только что говорил, заключается в следующем: система Windows 98 поддерживала некоторое подмножество функций Windows 2000, используемых драйверами WDM. В Windows 98 Second Edition поддерживалось более широкое подмножество, а в последней версии, Windows Me, оно было еще шире. Драйвер-заглушка не должен дублировать функции, поддерживаемые операционной системой. По этой причине WDMSTUB во время инициализации динамически конструирует таблицы, передаваемые _PELDR__ AddExportTable:
HPEEXPORTTABLE hExportTable - 0;
extern "C" BOOL OnDevicelnit(DWORD dwRefData) {
char** stubnames = (char**) _HeapAllocateCsizeof(names), HEAPZEROINIT);
PFN* stubaddresses = (PEN*) JHeapAllocate(sizeof(addresses), HEAPZEROINIT);
WORD* ordinals = (WORD*) _HeapAllocate(arrays!ze(names) * sizeof(WORD). HEAPZEROINIT);
742
Приложение А. Решение проблем несовместимости между платформами
1 nt 1, 1 stub;
for (1 =0, istub = 0; i < arrays!ze(names); ++1)
{
if (PELDR_GetProcAddress((HPEMODULE) "ntoskrnl.exe", names[i], NULL) = 0)
{
stubnames[Istub] = names[i];
ord1nals[1stub] = istub:
stubaddresses[1stub] = addresses[l];
++1stub;
}
1
_PELDR_AddExportTable(&hExportTable, "ntoskrnl.exe", istub.
istub, 0, (PVOID*) stubnames, ordinals, stubaddresses, NULL); return TRUE;
}
Здесь особенно важна команда, выделенная жирным шрифтом, — она следит за тем, чтобы мы случайно не заменили функцию, уже существующую в NTKERN или другой системной VxD.
Описанное решение, обеспечивающее совместимость версий, обладает одним раздражающим недостатком. Windows 98 Second Edison и Windows Me экспортируют только три из четырех вспомогательных функций для управления объектом IO__REMOVE„LOCK (если вам интересно, отсутствует функция loRemoveLockAnd-WaitEx). Мой драйвер WDMSTUB.SYS компенсирует этот недостаток и определяет заглушки для всех функций блокировки удаления или пи для одной из них, в зависимости от того, отсутствует эта функция или нет.
Функции-заглушки
Основным назначением WDMSTUB.SYS является разрешение символических имен, на которые ваш драйвер может ссылаться, не вызывая их реально. Для некоторых функций (таких как PoRegisterSystemState) WDMSTUB.SYS просто содержит заглушку, которая в случае вызова выдает код ошибки:
PVOID PoRegisterSystemState(PVOID hstate, ULONG flags)
{
ASSERT(KeGetCurrentlrqK) < DISPATCH_LEVEL): return NULL;
}
Впрочем, не всегда приходится писать заглушку, которая выдает код ошибки при вызове функции, иногда можно реализовать функцию, как в следующем примере:
VOID ExLocalTimeToSystemTimetPLARGEJNTEGER localtime,
PLARGEJNTEGER systime)
{
Определение заглушек для функций режима ядра в Win98/Me
743
syst1rne->QuadPart = localt1me->QuadPart + GetZoneBiasO: }
где GetZoneBias — вспомогательная функция, определяющая смещение часового пояса (то есть отличие местного времени от времени по Гринвичу) посредством чтения параметра ActiveTimeBias из раздела реестра TimeZonelnformation.
В табл. А.2 перечислены вспомогательные функции режима ядра, экспортируемые WDMSTUB.SYS.
Таблица А.2. Функции, предоставляемые WDMSTUB.SYS
Вспомогательная функция	Комментарий
ExFreePoolWithT ад	Заглушка
ExLocalTimeToSystemTime	Реализация
ExSystemTimeToLocaJTime	Реализация
HalTranslateBusAddress	Частичная реализация
loAcquireRemoveLockEx	Реализация
loAl locateWorkltem	Реализация
loCreateNotificationEvent	Заглушка (всегда ошибка)
loCreateSynchronizationEvent	Заглушка (всегда ошибка)
loFreeWorkltem i	Реализация
loInitializeRerrioveLockEx	Реализация
loQueueWorkltem	Реализация
loRaiselnformationalHardError	Реализация
loReleaseRemoveLockEx	Реализация
IoReleaseRemoveLockAndWaitEx	Реализация
loReuselrp	Реализация
loReportTargetDeviceChangeAsynchronous	Заглушка (всегда ошибка)
ToSetCompletionRoutineEx	Реализация
KdDebuggerEnabled	Реализация
KeEnterCriticalRegion	Реализация
KeLeaveCriticalRegion	Реализация
KeNumberProcessors	Всегда возвращает 1
KeSetT a rgetProcessorDpc	Реализация
PoCancel DeviceNotify	Заглушка (всегда ошибка)
PoRegisterDeviceNotify	Заглушка (всегда ошибка)
PoRegisterSystemState	Заглушка (всегда ошибка)
PoSetSystemState	Заглушка (всегда ошибка)
PoUnregisterSystemState	Заглушка (всегда ошибка)
PsGetVersion	Реализация
продолжение
744
Приложение А. Решение проблем несовместимости между платформами
Таблица А.2 (продолжение)
Вспомогательная функция	Комментарий
RtlInt64TollnicodeString	Заглушка (всегда ошибка)
RtlUlong ByteSwap	Реализация
RtlUlonglongByteSwap	Реализация
RtlUshortByteSwap	Реализация
SeSinglePrivilegeCheck	Всегда возвращает TRUE
ExIsProcessorFeaturePresent	Реализация
M mGetSystem RoutineAddress	Реализация
ZwLoadDriver	Реализация
ZwQueryDefaultLocale	Реализация
ZwQuerylnformationFile	Реализация
ZwSetlnformationFile	Реализация
ZwUnloadDriver	Реализация
Использование WDMSTUB
Чтобы использовать WDMSTUB, включите файл WDMSTUB.SYS в свой пакет драйвера и укажите его в качестве нижнего фильтра для своего функционального драйвера. Приведу пример INF-файла для одного из своих драйверов из прилагаемых материалов:
[Driverinstall]
AddReg=DriverAddReg
CopyFi1es=Dri verCopyFl les.StubCopyFi1es
[StubCopyFiles]
wdmstub.sys,,,2
[DriverAddReg]
HKR,.DevLoader,,*ntkern
HKR,,NTMPDri ver,,"wdmstub.sys,workitem.sys"
Этот синтаксис параметра NTMPDriver из раздела драйвера заставляет систему загружать WDMSTUB.SYS перед тем, как пытаться загрузить функциональный драйвер WORKITEM.SYS. К тому моменту, когда система доходит до загрузки WORKITEM.SYS, функция DriverEntry в WDMSTUB уже будет выполнена и определит функции, перечисленные в табл. А.2.
Обратите внимание: использование WDMSTUB в качестве нижнего фильтрующего драйвера означает, что установка не потребует перезагрузки системы.
Взаимодействие между WDMSTUB и WDMCHECK
WDMCHECK сообщает о символических именах, определенных только благодаря присутствию в системе драйвера WDMSTUB. Пример показан на рис. А.4.
Определение заглушек для функций режима ядра в Win98/Me
745
WdmCheck
__И
Module uses the following missing functions:
loFreeWorkltem (exported by WDMSTUB) IcAllocateWorkltem [exported by WDMSTUB] loQueueWorkltem [exported by WDMSTUB]
Рис. A.4. Результаты WDMCHECK с упоминанием WDMSTUB
Специальное замечание о лицензировании
WDMSTUB.SYS составляет исключение из общих правил лицензирования, распространяющихся на примеры в прилагаемых материалах. Чтобы избежать проблем на компьютерах конечных пользователей, обусловленных несогласованностью версий WDMSTUB, я прошу не распространять WDMSTUB.SYS без получения лицензии от меня. Я предоставляю бесплатную лицензию на распространение WDMSTUB любому, кто обратится ко мне с запросом. Просто отправьте сообщение по адресу waltoney@oneysoft.com, укажите, что вы хотите распространять WDMSTUB, и укажите контактные данные, чтобы я мог послать вам факсимильную копию лицензионного соглашения.
Мастер WDMWIZ.AWX
В этом приложении описано, как использовать мастер WDMWIZ.AWX для построения проектов драйверов, предназначенных для Microsoft Visual C++ версии 6.0, Windows ХР или .NET DDK. Я создал этот мастер, потому что хотел иметь простой и легко воспроизводимый способ построения примеров драйверов для этой книги. Я включил его в прилагаемые материалы, потому что знал, что читателю тоже пригодится простой механизм генерирования драйверов по мере чтения книги.
В файле WDMBOOK.HTM в прилагаемых материалах рассказано, как установить мастер в вашей системе. После того как установка будет завершена, на странице Projects диалогового окна New, отображаемого в Visual C++ при создании нового проекта, появляется значок WDM Driver Wizard. Файл WDMBOOK.HTM также содержит подробные инструкции по настройке рабочей среды для использования мастера. Я не стану повторять эти инструкции в книге, так как они наверняка будут изменяться с выпуском новых версий DDK компанией Microsoft.
WDMWIZ.AWX не является коммерческим продуктом и никогда им не будет. Мне бы хотелось знать о ситуациях, в которых мастер генерирует неверный код, но я не планирую вносить какие-либо изменения в его неуклюжий интерфейс. Более того, проверка качества драйверов, созданных с его помощью, находится на вашей ответственности.
Основная информация о драйвере
На начальной странице мастера (рис. Б.1) вам предлагается ввести основную информацию о создаваемом драйвере.
В списке Type of Driver (Тип драйвера) выбирается один из следующих вариантов:
О Generic Function Driver (Обобщенный функциональный драйвер) — построение функционального драйвера для обобщенного устройства. Слово «Generic» (обобщенный) в данном случае не имеет никакого отношения к GENERIC.SYS;
Основная информация о драйвере
747
О Generic Filter Driver (Обобщенный фильтрующий драйвер) — построение фильтрующего драйвера с обработкой по умолчанию для всех типов IRP;
О USB Function Driver (Функциональный драйвер USB) — построение функционального драйвера для устройства USB (Universal Serial Bus);
О Empty Driver Project (Пустой проект драйвера) — построение проекта, не содержащего файлов, но с настроенными параметрами для построения драйвера WDM;
О Converting existing SOURCES file (Преобразование существующего файла SOURCES) — создание проекта в соответствии с определением в стандартном файле DDK SOURCES. Например, может использоваться для преобразования примеров DDK в проект Visual Studio.
ШМ PriraV^zard -	1 of В,
Type at driver: j G eneric Function D river
 Options
I*/ Verbose debugging trace
Г” Use buffered method for reads and writes
U U se old-style for device naming
P B?£fece .^SSER^forj8G platforms’
£/ Use GENERIC.SYS Library
iv* Windows®detection
Location of D DK:	|$(D D КРАТ H)
Location of WD MEO О К directory: |$(WD M В 0 0 K]
D ispatch F unctions.., |
Finish
Рис. Б.1. Страница для ввода основной информации о драйвере
Группа Options содержит следующие флажки:
О Verbose Debugging Trace (Подробная отладочная трассировка) — если установить этот флажок, в файлы проекта драйвера включаются многочисленные вызовы макроса KdPrint для трассировки важных операций, выполняемых драйвером;
О Use Buffered Method For Read And Writes (Использовать буферизацию при чтении и записи) — установите этот флажок, если вы хотите использовать метод DO_BUFFERED_IO в операциях чтения и записи. Если сбросить флажок, вместо него будет использоваться метод DO_DIRECT_IO;
748
Приложение Б. Мастер WDMWIZ.AWX
О Use Old-StyJe For Device Naming (Использовать старый стиль с именованными устройствами) — при установленном флажке в драйвере будут использоваться именованные объекты устройств. Если сбросить флажок, в сгенерированном драйвере используются интерфейсы устройств. Компания Microsoft рекомендует использовать для драйверов WDM второй вариант (интерфейсы устройств);
О Replace ASSERT for i86 Platforms (Заменять ASSERT для платформ i86) — макрос DDK ASSERT вызывает вспомогательную функцию режима ядра (RtlAssert), которая в окончательной версии Windows 2000 является пустой операцией. Следовательно, директива не будет вызывать остановку в окончательной версии операционной системы даже для отладочной сборки драйвера. При установке флажка ASSERT переопределяется таким образом, что отладочная сборка драйвера будет прерываться даже в окончательной сборке операционной системы;
О Use GENERIC.SYS Library (Использовать библиотеку GENERIC.SYS) — установите этот флажок, чтобы использовать стандартизированный код драйвера из GENERIC.SYS. Если флажок сбросить, весь стандартизированный код будет включен в ваш драйвер;
О Windows 98 Detection (Обнаружение Windows 98) — при установленном флажке драйвер будет во время выполнения проверять, работает ли драйвер в Windows 98/Ме или Windows 2000/ХР. При сброшенном флажке проверка не выполняется.
В окне также вводятся базовые пути, по которым был установлены пакет Windows .NET DDK и примеры к книге. Значения по умолчанию — $(DDKPATH) и $(WDMBOOK) — используют переменные окружения, созданные программой установки примеров.
I/O Request Types
а
IRP_MJ_CREATE
& IRP_MJ_DEVICE_CONTROL
Г 1RP_MJJNTERNAL_DEVICE_CONTROL

IRP_MJ_READ 1RP_MJ_WRITE
Р’ IRP_MJ_CLEANUP
П
hRpTiFSYSTEgjmNТВД
Cancel
Рис. Б.2. Диалоговое окно для выбора основных кодов функций IRP, для которых мастер должен сгенерировать диспетчерские функции
Коды DeviceloControl
749
Наконец, кнопка Dispatch Functions открывает диалоговое окно для указания типов IRP, обрабатываемых вашим драйвером (рис. Б.2). В диалоговом окне отражены некоторые решения из области проектирования, которые отменить нельзя. Ваш драйвер будет включать поддержку IRP_MJ_PNP и IRP_ MJ_POWER. Если выбрать обработку IRP_MJ_CREATE, вы также получите поддержку IRP_MJ_CLOSE. Если выбрать обработку IRP_MJ_READ, IRP_MJ_WRITE или IRP_MJ_DEVICE_CONTROL, вы получите поддержку IRP_MJ_CREATE (а следовательно, и IRP_MJ_CLOSE). WDMWIZ.AWX не генерирует заготовки диспетчерских функций для многих типов IRP, используемых только драйверами файловых систем.
Коды DeviceloControl
При выборе обработки IRP_MJ_DEVICE_CONTROL мастер отображает страницу (рис. Б.З) для ввода информации о поддерживаемых управляющих операциях.
Рис, Б,3, Страница для определения поддерживаемых управляющих операций ввода/вывода
На рис. Б.4 приведен пример ввода информации о конкретной операции DeviceloControl. Большинство полей напрямую соответствуют параметрам препро-цессорного макроса CTL_CODE и поэтому не требуют объяснений. При установке флажка Asynchronous генерируется поддержка операции, завершаемой асинхронно после того, как диспетчерская функция вернет STATUS-PENDING.
750
Приложение Б. Мастер WDMWIZAWX
f/0.ContndOperatfon
Symbolic Name:
|IOCTL_GEfVERSlON
Control Code:	J2048
File Access t? Any C Read
Write
Buffering Method <• Buffered
Direct (Input) r Direct (Output) ^either
Г Asynchronous
OK
Cancel ।
Рис. Б.4. Диалоговое окно для добавления и редактирования управляющих операций ввода/вывода
Ресурсы ввода/вывода
Если устройство использует какие-либо ресурсы ввода/вывода, введите информацию о них на третьей странице мастера (рис. Б.5).
WDM Driver Wizard - Step 3 of 8
Program iT.ing '
theWintfows i
Driver
You can specify the types of I/O resources (if any) your device uses:
p Interrupt request	p I/O Port
F~ Mapped memory window
Dynamic Memory Access Options
Г Device doesn't perform DMA Transfers
Г Device uses system DMA channel #
Width:
Speed:
г
Г :
< Back | Next >
Device is a bus master
F~ Scatter/Gather Capability
|4096
®-bii Addresses OH	64-Ьй Addresses OK
Mawinnum Length of transfers:
Finish j Cancel j
Help J
Рис. Б-5. Страница для определения ресурсов ввода/вывода
Конечные точки USB
751
Конечные точки USB
Если на первой странице был выбран функциональный драйвер USB, мастер отображает дополнительную страницу для описания конечных точек устройства (рис. Б.6). На странице выводятся имена переменных расширения устройства, в которых будут храниться манипуляторы каналов. Порядок имен соответствует порядку следования дескрипторов конечных точек в устройстве.
ПРИМЕЧАНИЕ---------------------------------------------------
Страница слишком проста для описания устройств с несколькими интерфейсами или альтернативными настройками интерфейсов.
Рис. Б,6. Страница для определения конечных точек USB
На рис. Б.7 изображено диалоговое окно с описанием одной конечной точки. Группа Description of Endpoint относится к описанию конечной точки в микрокоде устройства, а смысл входящих в нее элементов не требует дополнительных объяснений. В полях группы Resources In The Driver введите следующую информацию:
О Name Of Pipe Handle In Device Extension (Имя манипулятора канала в расширении устройства) — имя поля DEVICEJEXTENSION для хранения манипулятора канала, который будет использоваться для операций с конечной точкой;
О Maximum Transfer Per URB (Максимальный объем передаваемых данных в URB) — максимальное количество байтов, передаваемых в одном блоке URB.
752
Приложение Б. Мастер WDMWIZ.AWX
USB Endpoint
Index of descriptor within interface? 0
Description of Endpoint
Endpoint number	|2
Type:	[Bulk
Maximum transfer:	,{б4
Resources in the driver
Name of pipe handle in device extension:
Maximum transfer per URB:
OK
Direction -Input Output
14096
J
Cancel
Рис. Б.7. Диалоговое окно для добавления и модификации конечных точек USB
Поддержка WMI
Если вы указали, что драйвер должен обрабатывать запросы IRP_MJ_SYSTEM_ CONTROL, мастер выводит страницу, показанную на рис. Б.8. На этой странице задаются элементы нестандартной схемы WMI (Windows Management Instrumentation) или стандартные классы Microsoft, которые вы собираетесь поддерживать.
В списке Block Identifiers перечислены глобально-уникальные идентификаторы (GUID) классов в порядке их следования в списке GUID для WMILIB.
На рис. Б.9 приведен пример описания одного из стандартных классов Microsoft. Верхнее поле (без названия) содержит символическое имя GUID. Вводя имя вручную, вы можете указать класс, входящий в вашу нестандартную схему. Для классов WMI указываются следующие атрибуты:
О Number Of Instances (Количество экземпляров) — указывает, сколько экземпляров класса будет создавать ваш драйвер;
О Expensive (Затратный класс) — является признаком затратного класса, поддержку которого необходимо включать отдельно;
О Event Only (Только для событий) — означает, что класс используется только для инициирования событий;
О Traced (Трассировка) — соответствует параметру WMI, смысл которого мне пока не понятен. Но если я когда-нибудь узнаю, что он означает, то смогу использовать этот флажок для управления его состоянием.
Поддержка WMI
753
WDM Driver Wizard - Step 4 of 8
ч J, Specify your driver's use of Windows Management
V	Instrumentation on this page:
’ 1 ’ ’ Л ‘	'г '' t’
\	‘ Block Identifiers:
Help I
< Back | Next> |
Рис. Б.8. Страница для настройки параметров WMI
Также вы можете выбрать между присваиванием имен по объекту физического устройства (PDO) и по базовому имени. Microsoft рекомендует присваивать имена на базе PDO.
WMIOtyect	_______.|g
[{B27C8A6F FE BO-i 1 dTbDZB-OOAAOOBTBKA}
Г Expensive
Г Event Only
Number of instances: |T	F“ Traced
л	Г W-
Instance naming ~
& Static based on PDO Name C Static based on Base Name r •
OK
Cancel |
Рис. Б.9. Диалоговое окно для указания класса WMI
754
Приложение Б. Мастер WDMWIZ.AWX
Параметры INF-файла
На последней странице мастера (рис. Б. 10) вводится информация об INF-файле, являющемся частью проекта драйвера.
Рис. Б.10. Страница для указания параметров INF-файлов
Страница состоит из следующих полей:
О Manufacturer Name (Название производителя) — название производителя оборудования;
О Device Class (Класс устройства) — стандартный класс устройства, к которому относится оборудование. В примерах, приведенных в книге, используется отдельный класс Sample, не используйте его в коммерческих драйверах;
О Hardware ID (Идентификатор оборудования) — аппаратный идентификатор устройства. Я определил для своего примера идентификатор *WCO0B01. Задайте идентификатор, совпадающий с одним из идентификаторов, создаваемых соответствующим драйвером шины. За дополнительной информацией обращайтесь к разделу «Идентификаторы устройств» главы 15;
О Friendly Name For Device (Дружественное имя устройства) — если вы хотите, чтобы в разделе оборудования для устройства был создан параметр Friendly-Name, укажите его значение здесь;
О Auto-Launch Command (Команда автозапуска) — если вы хотите, чтобы служба автозапуска автоматически запускала приложение при инициализации
Что дальше?
755
устройства, укажите командную строку в этом поле. Например, при построении примера AutoLaunch для главы 15 я ввел в этом поле команду %windir%\aitestext %s %s;
О Device Description (Описание устройства) — введите описание устройства.
Что дальше?
После заполнения всех страниц мастера создается проект, на основе которого можно строить драйвер. Из-за ограничений на поддержку пользовательских мастеров в Visual C++ параметры проекта придется модифицировать вручную. За описанием этих параметров обращайтесь к файлу WDMBOOK.HTM в прилагаемых материалах.
Сгенерированный код содержит ряд комментариев TODO, выделяющих области, требующие написания дополнительного кода. Я рекомендую воспользоваться командой Find in Files для поиска таких областей.
WDMWIZ также генерирует стандартный файл DDK SOURCES, используемый с утилитой BUILD. Многие программисты предпочитают работать с BUILD при построении драйверов; если вы принадлежите к их числу, эта возможность немного упростит вашу задачу.
Ал
7 в
а битный указатель
А
AbortRequests, функция, 315, 323
ACPI, спецификация, 400
ACPLSYS, драйвер, 712
AdapterControl, функция, 43, 381
AddDevice, функция, 96, 429, 647
AddReg, директива, 658
ADDRESS_AND_SIZE_TO_SPAN—PAGES, макрос, 119
AdjustDeviceCapabilities, функция, 415
Alignment Requirement, поле, 94
AllocateAdapterChannel, функция, 379
AllowRequests, функция, 324
ANSI_STRING, структура, 140
АРС, общие сведения, 198
APCLEVELIRQL, 169
ASL (ACPI Source Language), 415
ASSERT, макрос, 157, 185, 572
ATAPLSYS, драйвер, 711
Attached Device, поле, 60
В
BASETSD.H, файл, 67
BYTE OFFSET, макрос, 119
BYTES_TO_PAGES, макрос, 118
С
CacheControlRequest, функция, 464
Cancello, функция, 261
Cancellrql, поле, 215
CancelRequest, функция, 306
CancelRoutine, поле, 215
CCoinstaller, класс, 686
CCoinstallerDialog, класс, 686
CDB (Command Description Block), 710
CDeviceList, класс, 89
CDROM.SYS, драйвер, 710
CheckBusyAndStall, функция, 309
CHKINF, программа, 673
CIM (Common Information Model), 470
CleanupRequests, функция, 323
CompleteCancelcdlrp, функция, 259
CompleteRequest, функция, 114, 229
CONTAINING_RECORD, макрос, 131
Context, поле, 219
CreateEvent, функция, 462
CTL-CODE, макрос, 449
D
DbgPrint, функция, 157
DbgView, программа, 332
DestroyContextStructure, функция, 581
DEVCTL, программа, 458
DEVGUID.H, файл, 650
DEVICE-CAPABILITIES, структура, 413
DEVICE DESCRIPTION, структура, 377
DEVICE OBJECT, структура, 51, 68
DEVICE-RELATIONS, структура, 501
DeviceloControl, функция, 445
DEVQUEUE
общие сведения, 252
отмена IRP, 268
переходы, 296
реализация, 265
DEWIEW, программа, 62
DIF, коды, 682
DIRQL (Device IRQL), 168, 169
DISK.SYS, драйвер, 711
DISKPERF, драйвер, 713
DispatchAny, функция, 718
DispatchClose, функция, 441
DispatchControl, функция, 467
DispatchCreate, функция, 441
DispatchPnP, функция, 597, 720
DMA
в Windows XP, 372
общие сведения, 43, 372
роль буферов данных, 94
DO_BUFFERED_IO, флаг, 95, 289
DO_DEVICE_1NITIALIZING, флаг, 96
DO_DIRECT_IO, флаг, 95, 288
DO_POWER_INRUSH, флаг, 95, 429
Алфавитный указатель
757
DO_POWER_PAGABLE, флаг, 95, 328
DPC
назначение приоритета, 364
общие сведения, 43
планирование, 363
DpcForlsr, функция, 236
Driver Verifier, 195
общие сведения, 158
DRIVEROBJЕСТ, структура, 65
DriverUnload, функция
Windows 98/Ме, 98
общие сведения, 47
Е
Event Viewer, 617
Ex AcquireFast Mutex, функция, 201
ExAllocateFromNPagedLookasideList, функция, 138
ExAllocateFromPagedLookasideList, функция, 138
ExAllocatePoolWithTag, функция, 117
ExDeleteNPagedLookasideList, функция, 138
ExDeletePagedLookasideList, функция, 138
ExFreePool, функция, 117, 129, 553
ExFreePoolWithTag, функция, 743
ExFreeToNPagedLookasideList, функция, 138
ExFreeToPagedLookasi deList, функция, 138
ExGetPrcviousMode, функция, 200
ExInitializeFastMutex, функция, 201
ExInitializeNPagedLookasideList, функция, 138
ExInitializePagedLookasideList, функция, 138
Ex InitializesListHead, функция, 210
ExInterlockedAddLargelnteger, функция, 205
ExInterlockedAddULong, функция, 205
ExInterlockedlnsertHeadList, функция, 210
ExInterlockedlnsertTailList, функция, 210
ExInterlockedPopEntryList, функция, 210
ExInterlockedPopEntrySList, функция, 210
ExIsProcessorFeaturePresent, функция, 156, 743
ExLocalTimeToSystemTime, функция, 743
ExRaiseAccessViolation, функция, 114
ExRaiseDatatypeMisalignment, функция, 114
ExRaiseStatus, функция, 114
ExReleaseFastMutex, функция, 201
ExReleaseFastMutexUnsafe, функция, 201
ExSystemTimeToLocalTime, функция, 743
ExTryToAcquireFastMutex, функция, 201
F
FastloDispatch, поле, 68
FDO
общие сведения, 51
фильтры шин, 730
FiDO
верхние фильтрующие драйверы, 709
общие сведения, 51
фильтры шин, 730
FILE_ANY_ACCESS, разрешение, 450
FILE DEVICE DISK, устройства, 439
FILE_DEVICEMASS^STORAGE,
устройства, 439
FILE_READ_ACCESS, разрешение, 450
FILE_READ_ATTRIBUTES, разрешение, 83
FILE_REMOVABLE_MEDIA, флаг, 722
FILE TRAVERSE, разрешение, 83
FILE_WRITE_ACCESS, разрешение, 450
FlushAdapterBuffers, функция, 383
FlushPendinglo, функция, 420
G
GENERIC.SYS, драйвер, 252, 416
GenericCacheControlRequest, функция, 598
GenericIdleDevice, функция, 586
GenericSaveRestoreComplete, функция, 424
GenericUncacheControlRequest, функция, 598
GENINF, программа, 673
Get Device Power State, функция, 416
GetDeviceTypeToUse, функция, 717
GetProcAddress, функция, 737
GetScatterGatherList, функция, 387
GetStringDescriptor, функция, 549
GetWindowLong, функция, 679
GUID
интерфейсы прямого вызова, 508
общие сведения, 86
создание, 86
GUIDGEN, программа, 86
н
HAL.DLL, файл, 50
Hal Get Adapter, функция, 377
HalTranslatcBusAddress, функция, 743
HID, устройства
драйверы, 37, 588
общие сведения, 587
HID DEVICE EXTENSION, структура, 596
HIDUSB.SYS, драйвер, 37, 588
HIGH-LEVEL, IRQL, 212
HKEY-LOCAL—MACHINE, раздел реестра, 639
I
I/O Manager
адресация буферов, 345
определение, 30
создание IRP, 220
IMAPLSYS, драйвер, 711
758
Алфавитный указатель
INF-файлы, 650
Initialize, функция, 90
InitializeListHead, функция, 132
InitializeObjectAttributes, макрос, 143 InitializeQueue, функция, 306 InsertTailList, функция, 132 INTERNALPOWERERROR, ошибка, 430 IO REMOVE LOCK, объект, 312
IO_STACK_LOCATION, структура, 213, 217 IO TIMER, объект, 633
IO WORKITEM, структура, 632 loAcquireRemoveLock, функция, 164, 288 loAcquireRemoveLockEx, функция, 743 loAllocate Adapter, функция, 373 loAllocatelrp, функция, 220, 273 loAllocateWorkltem, функция, 743 loAttachDevice, функция, 226 loBuildAsynchronousFsdRequest, функция, 222 Io Build DeviceloControlRequest, функция, 241 loBuildSynchronousFsdRequest, функция, 241 loCallDriver, функция, 223, 232, 459 loCancellrp, функция, 261 loCompleteRequest, функция, 199, 235 loConnectlnterrupt, функция, 235 loCreateDevice, функция, 68, 94, 649 loCreateDeviceSecure, функция, 83 loCreateNotificationEvent. функция, 743 loCreateSymbolicLink, функция, 81 loCsqlnsertlrp, функция, 257 loCsqRemoveNextlrp, функция, 258 IOCTL
асинхронные, 464
внутренние, 458
оповещение приложений, 461 loDeleteDevice, функция, 93, 104 loDereferenceObject, функция, 225 loDetachDevice, функция, 304, 316, 597 loForwardlrpSynchronously, функция, 292 loFreelrp, функция, 241, 274, 579 loFreeWorkltem, функция, 743 loGetAttachedDeviceReference, функция, 224 loGetCon figuration Information, функция, 85 loGetCurrentlrpStackLocation, функция, 240 loGetDeviceObjectPointer, функция, 78, 224 loGetDeviceProperty, функция, 646 loGetDmaAdapter, функция, 373 loGetNextlrpStackLocation, функция, 223 loInitializeDpcRequest, макрос, 362 loInitializeRemoveLock, функция, 313 loInitializeRemoveLockEx, функция, 743 loInvalidatcDeviceRelations, функция, 52 loMakeAssociatedlrp, функция, 583 loMapTransfer, функция, 373 loMarklrpPending, функция, 240, 409
loOpenDevicelnterfaceRegistryKey, функция, 143
loOpenDeviceRegistryKey, функция, 143
loQueu cWorkitem, функция, 336, 633
loRegisterDevicelnterface, функция, 88, 129, 330
loRegisterPlugPlayNotification, функция, 335
loReleaseCancelSpinLock, функция, 262
loReleaseRemoveLock, функция, 288
loReleaseRemoveLockAndWait, функция, 315
loReleaseRemoveLockEx, функция, 743
loReleaseRemoveLockExAndWaitEx,
функция, 743
loReportTargetDeviceChange, функция, 339
loReportTargetDeviceChangeAsynchronous,
функция, 339
loRequestDpc, функция, 236
loReuselrp, функция, 561, 743
loSetCancelRoutine, функция, 467, 582
loSkipCurrentlrpStackLocation, функция, 232
loStartNextPacket, функция, 248
loStartPacket, функция, 248
loStartTimer, функция, 634
loStatus, поле, 215
loUnregisterPIugPlayNotification, функция, 336
loWmiRegistrationControl, функция, 475
IRP
непредвиденное удаление, 295
общие сведения, 213
отмена, 261
очереди, 248
переходы, 296
прерывание запросов, 323
создание, 220
IRQL
APCJLEVEL, 166
DISPATCH_LEVEL, 167
HIGH-LEVEL, 169
общие сведения, 166
IShellExtlnit, интерфейс. 439
IShellPropSheetExt, интерфейс, 439
IsListEmpty, функция, 132
к
KBDCLASS.SYS, драйвер, 588
KDPC, объекты, 193
KdPrint, макрос, 157
KeAcquireFastMutex, функция, 203
KeAcquirelnStackQueuedSpinLock, функция, 177
KeAcquirelnStackQueuedSpinLockAtDpcLevel, функция, 178
KeAcquirelnterruptSpinLock, функция, 361
KeAcquireSpinLock, функция, 176
KeBugCheckEx, функция, 115
KeCancelTimer, функция, 195
KeClearEvent, объект, 185
Алфавитный указатель
759
KeDebuggerEnabled, функция, 743
KeDelayExecutionThread, функция, 196
KeEnterCriticalRegion, функция, 152, 221
KeFlushloBuffers, функция, 382
KelnitializeDpc, функция, 365
KelnitializeEvent, объект, 185
KelnitializeMutex, функция. 190
KelnitializeSemaphore, функция, 188
KelnitializeTimer, функция, 191
KelnitializeTimer Ex, функция, 191
KelnsertQueueDpc, функция, 365
KeLeaveCriticalRegion, функция, 743
KeLowerlrql, функция, 172
KeNumberProcessors, функция, 743
KeQuery Timeincrement, функция, 193
KeRaiselrql, функция, 172
KeReadStateEvent, объект, 185
KeReadStateMutex, функция, 190
KeReadStateSemaphore, функция, 188
KeReleaselnStackQueuedSpinLock, функция, 177
KeReleaselnStackQueuedSpinLockFromDpcLevel, функция, 178
KeReleaselnterruptSpinLock, функция, 361
KeRelcaseMutex, функция, 190
KeReleaseSemaphore, функция, 188
KeReleaseSpinLock, функция, 176
KeRemoveQucueDpc, функция, 365
KeResctEvent, объект, 185
KeRestoreFloatingPointState, функция, 162
KERNEL32.DLL, файл, 30,33^
KeSaveFloatingPointState, функция, 156
KeSetEvent, функция, 185
KeSetlmportanceDpc, функция, 364
KeSetTargetProcessorDpc, функция, 364
KeSetTimer, функция, 191
KeSetTimerEx, функция, 191
KeStallExecutionProcessor, функция, 197
KeSynchronizeExecution, функция, 234, 361
KeWaitForMultipleObjects, функция, 178
KeWaitForMutexObject, макрос, 191
KeWaitForSingleObject, функция, 178
KEY READ, разрешение, 145
KEYSETVALUE, разрешение, 148
KEY WRITE, разрешение, 149
KMUTEX, объект, 190
KTHREAD, объект, 197
м
MajorFunction, указатель, 73, 217
MDL, структура, 213
Mdl Ad dress, поле, 213
Microsoft Foundation Classes, 686
Minor Function, поле, 218
MODEM.SYS, драйвер, 714
MOUCLASS.SYS, драйвер, 588
MOUHID.SYS, драйвер, 712
M SPowet—Device Wake Enable, класс, 431
N
NDIS, 38
NextDevice, поле, 68
NTDDKKB.H, файл, 732
NTKERN.VXD, драйвер, 35
NTOSKRNL.EXE, файл, 50
NtReadFile, функция, 30
NTSTATUS.H, файл, 72, 228
NtWaitForSingleObject, функция, 199
О
Ob DereferenceObject, функция, 628
Object Manager, 78, 316
OBJECT-ATTRIBUTES, структура, 144
ObRefercnceObject, функция, 316
ObReferenceObj ect By Handle, функция, 197
P
PAGE ALIGN, макрос, 119
PAGE_SHIFT, константа, 118
PAGE—SIZE, константа, 118
PAGED-CODE, макрос, 119
PARTMGR.SYS, драйвер, 711
PASSIVE_LEVEL, IRQL, 166
PCI, устройства, 50, 408
PCMCIA, устройство, 666
PDEVICE—OBJECT, поле, 67
PDO
общие сведения, 51
PeekNextlrp, функция, 258
Plug and Play, технология, 294
PMTSHOOT, программа, 408
PnP Manager, 52, 294, 299	*
PoCallbackRoutine, функция, 417
PoCallDriver, функция, 442
PoCancelDeviceNotify, функция, 444
PopEntryList, функция, 132, 135
PoRegisterDeviceForldleDetection, функция, 444
PoRegisterSystcmState, функция, 438
PoRequestPowerlrp, функция, 417
PoSetDeviceBusy, функция, 438
PoSetPowerState, функция, 444
PoSetSystemState, функция, 743
PoStartNextPowerlrp, функция, 442, 600
PoUnregisterSystemState, функция, 743
ProbeForRead, функция, 351, 456
ProbeForWrite, функция, 351, 456
PsGetVersion, функция, 743
PsTerminateSystemThrcad, функция, 197, 628
PushEntryList, функция, 135
760
Алфавитный указатель
Q
Query Data Block, функция, 480
QueryPowerChange, функция, 406
QueryReglnfo, функция, 478
R
ReadFile, функция, 31, 33, 78, 154
REDBOOK.SYS, драйвер, 711
REGEDIT, программа, 640
RegisterDeviceNotification, функция, 330
RemoveEntryList, функция, 132
RemoveHeadList, функция, 132
Removelrp, функция, 258
RemoveTailList, функция, 132
ResetDevice, функция, 366
ROUNDJTO-PAGES, макрос, 118
Rtl, функции, 141
RUNDLL32, программа, 691
RunOnce, раздел, 681
RWCONTEXT, структура, 576
s
SaveDeviceContext, функция, 425
SCSI, устройства, 36, 445
SendDeviceSetPower, функция, 435
SERENUM.SYS, драйвер, 713
SERIAL.SYS, драйвер, 713
SetDataBlock, функция, 482
SetDataltem, функция, 483
SetWindowLong, функция, 679
StallRequests, функция, 306
StartDevice, функция, 342
Startlo, функция, 43, 234
StartNextPacket, функция, 254, 321
StartPacket, функция, 254
Succeed Request, функция, 501
S-списки, 209
T
TAPE.SYS, драйвер, 710
try-except, блоки, 109
try-finally, блоки, 107
u
UncacheControlRequest, функция, 464
UsbBuildFeatureRequest, макрос, 544
UsbBuildGetDescriptorRequest, макрос, 544
Usb Build Get StatusRequest, макрос, 544
UsbBuildlnterruptOrBulkTransfer Request, макрос, 544
UsbBuildSelectConfiguration, макрос, 544
UsbBuildVendorRequest, макрос, 544
USBHUB.SYS, драйвер, 62
UUIDGEN, программа, 86
V
VMM, 32
VxD, драйверы, 28, 39, 338
w
WaitForCurrentIrp, функция, 309
WaitForSingleObject, функция, 461
WDM, драйверы
глобальная инициализация, 71
монолитные функциональные, 38
общие сведения, 36
перемещение, 119
программная установка, 690
работа с файлами, 152
WDMGUID.H, файл, 337
WHQL
общие сведения, 638, 639
получение сертификатов, 41, 673
создание заявок, 698
Win32 API, 30
WinDbg, программа, 408
Windows Зх, 28
Windows 95, 29,329
Windows 98/Ме
архитектура, 32
история, 32
объекты устройств, 98
поддержка Юникода, 59
синтаксис INF-файлов, 655
совместимость, 738
Windows ХР
архитектура, 30
история, 30 *
общие сведения, 30
синтаксис INF-файлов, 655
управление питанием, 399
WINERROR.H, файл, 571
WINIOCTL.H, файл, 450
WINOBJ, программа, 78
WM COMMAND, сообщение, 680
WM DEVICECHANGE, сообщение, 329
WMI
концепция класса, 471
общие сведения, 41, 470
события, 489
WMIDATA.H, файл, 494
WMIEvent, класс, 489
WMIREGINFO, структура, 479
WriteFile, функция, 78, 154
Z
ZwClose, функция, 145, 225
ZwCreateFile, функция. 152
Алфавитный указатель
761
ZwCreateKey, функция, 143
ZwDeleteKey, функция, 143
ZwFlushKey, функция, 143
ZwEnumerateKey, функция, 143
ZwLoadDriver, функция, 743
ZwOpenFile, функция, 226
ZwQuery Default Locale, функция, 743
ZwQuery InformationFile, функция, 154, 743
ZwQuery ValueKey, функция, 143
ZwSetEvent, функция, 463
ZwUnloadDriver, функция, 743
ZwWriteFile, функция, 154, 289
A
асинхронные IOCTL, 464
асинхронные IRP
отмена, 273
создание, 220
Б
бездействие, 437
блоки данных, 490
буферизация, 345
буферы
адресация, 345
запросы IOCTL, 446
быстрые мьютексы, 201
В
вещественные вычисления, 155
взаимная блокировка, 174, 204
виртуальное адресное пространство, 116
д
двоичная совместимость, 39
дерево устройств, 62
дескрипторы
конечных точек, 540
конфигурации, 539
общие сведения, 517
устройства, 535
джойстики, 603
динамическая компоновка, 737
Диспетчер устройств, 431, 641
диспетчерские функции, 718
дочерние устройства, 52, 495
драйвер шины
PDO, 51
инициирование запросов, 543
конфигурация, 546
определение, 50
отправка IRP, 52
устройства Plug and Play, 52
драйверы
история, 26
процессоры, 26
роль в Windows ХР, 35
типы, 35
драйверы виртуальных устройств, 28, 33
драйверы классов
минидрайверы, 37, 50
определение, 36, 710
дружественные имена, 90, 680
ж
журнал событий, 42, 663
3
заголовочные файлы
BASETSD.H, 67
DEVICELIST.H, 89
GUIDS.H, 87
NTSTATUS.H, 72
загрузка драйверов, 49
И
идентификаторы устройств, 53
изохронная пересылка данных, 517
изохронные каналы, 567
изохронные конечные точки, 567
именованные фильтры, 727
интерфейсы прямого вызова, 508, 509
исключения, 102
исполняемые файлы, 44, 120
к
клавиатура, драйверы, 712
клавиатура, фильтры, 731
классы устройств
Unknown, 675
минидрайверы, 37
определение, 675
клиентская установка, 681
коллекции, HID, 590
конечные автоматы, 408
конечные точки, 517, 524
контроллеры, 495
концентраторы, 519
л
логические коллекции, 590
м
массовая передача, 517, 555
мастер установки оборудования, 653
микрофреймы, 517
762
Алфавитный указатель
минидрайверы, 37, 50
многофункциональные устройства, 495, 511 монолитные функциональные драйверы, 36 мыши
драйверы, 588
стандартные блоки, 492
мьютексы
быстрые, 201
ядра, 189, 201
н
наследные устройства, 36, 49, 298
нижние фильтрующие драйверы, 51, 59
о
обработка исключений, структурированная, 105
обработка ошибок, 102, 115, 617
объекты драйверов
DeviceObject, поле, 67
DriverExtension, поле, 67
DriverS tart Io, поле, 73
DriverUnload, поле, 73
HardwareDatabasc, поле, 67
общие сведения, 65
объекты устройств
инициализация, 90
опрос, 625
пробуждение, 430
создание, 76
удаление, 303
операционная система, 27, 47, 736
опрос устройств, 625
отладка
INF-файлы, 675
код управления питанием, 408
упрощение, 156
отмена запросов
общие сведения, 260
основные/вспомогательные, 577 синхронизация, 261
п
перечисление, рекурсивное, 57
порты
регистры, 351
ресурсы, 351
потоки
вытеснение, 165
произвольный контекст, 47,113
режима ядра, 200
прерывания, обработка, 345
прерывающая передача, 517
прерывающие конечные точки, 524
принтеры, написание минидрайверов, 38
пропускная способность, 535
пространства имен, 471
Р
раздел драйвера, 639
раздел класса, 639
раздел оборудования, 639, 646
раздел службы, 639, 646
раздел устройства, 58
расширение устройства, 92, 497
реестр
REGEDIT, 640
Windows 2000, 640
Windows ХР, 640
обращение из драйвера, 646
обращение из программы, 646
открытие разделов, 144
удаление подразделов и параметров, 149 режим ядра
наследные драйверы, 36
обзор драйверов, 36
обработка ошибок, 102
оповещения, 335
потоки, 200
символические ссылки, 79
резервные списки, 136
рекурсивное перечисление, 57
ресурсы ввода/вывода, 298, 342, 750
с
связанные списки, 130, 209
семафоры ядра, <188
сетевые карты, 38
символические ссылки, 79
синхронизация, 175, 185, 261
синхронные IRP, 220, 270
системные потоки, 626
системные часы, 181
составные устройства, 547, 590
спин-блокировки, 173, 262
спящий режим, 430
ссылочные коллекции, 590
строковые дескрипторы, 542
структурированная обработка исключений,
105, 107
Т
таймеры ядра
альтернативы, 196
общие сведения, 191
периодические, 194
синхронизация, 194
Алфавитный указатель
763
У
управление памятью, 116,136 управление питанием
общие сведения, 398 отладка, 408 пробуждение, 430
устройства USB, 516, 517
Ф
файлы сообщений, 618
фатальные сбои, 115 фильтр шины, 730 фильтрующие драйверы
Windows 98/Ме, 734 верхние, 51 нижние, 51
фильтрующие драйверы (продолжение) общие сведения, 36 создание, 731 установка, 722
фильтры классов
Windows 98/Ме, 735
установка, 723
фреймы, 517
функции завершения, 236, 556
функциональные драйверы, 50, 583, 710
ш
шины, 52
ю
Юникод, 59, 149
♦