Abstraction
Абстракция, представление сложного предмета искусственно созданным формальным описанием. Абстракция позволяет отойти от рассмотрения некоторых излишне конкретных вопросов реализации своего прототипа. Абстракции, вводимые Microsoft DDK, возникли, главным образом из стремления облегчить переносимость кода на другие аппаратные платформы (обеспечение HAL) и стремления уберечь разработчика драйвера от необходимости вникать в тонкости постоянно меняющихся версий аппаратного обеспечения на каждой конкретной платформы (например, объект адаптера позволяет абстрагироваться от реализаций контроллеров DMA).
Access Violation
Нарушение доступа. Попытка доступа к области памяти, которая вызвала исключение вследствие защиты доступа к страницам памяти (вследствие того, что данная страница относится к набору для другого процесса, а не из соображений безопасности). Типы нарушений доступа:
Попытка доступа к памяти за пределами доступного текущему процессу адресного пространства (length violation).
Ошибочная операция, например, попытка записи на странице "только для чтения".
Попытка доступа по запрещенному системой адресу, например, приложениям запрещено обращаться к нижним 64K адресного пространства, что упрощает обнаружение обращений по нулевому указателю.
Доступ к странице, которая в данный момент предназначена для использования одним из системных компонентов (хотя и присутствует в физической оперативной памяти).
ACPI
Advanced Configuration and Power Interface. Абстрактный интерфейс, определяющий механизмы управления энергопотреблением и конфигурирования аппаратного обеспечения и компонентов операционной системы. Является частью так называемой "Инициативы OnNow".
ACPI Driver
ACPI драйвер. В системах с ACPI BIOS программное обеспечение HAL обеспечивает загрузку ACPI драйвера во время старта операционной системы в корне дерева устройств. Драйвер ACPI функционально прозрачен для других драйверов, которые не должны делать каких-либо допущений о наличии или отсутствии в своих стеках устройств фильтр-объектов от ACPI драйвера. В обязанности ACPI драйвера входит поддержка PnP возможностей системы и управления энергопотреблением, так что результаты работы ACPI драйвера в виде многочисленных PDO или фильтр-объектов можно найти в ветвях дерева устройств (если воспользоваться программой DeviceTree), связанных с реальной аппаратурой.
AddDevice
Функция, которую должны поддерживать WDM драйверы, для того, чтобы в своем составе иметь обработчик, который будет вызван в момент обнаружения устройства. Регистрация AddDevice выполняется в момент работы DriverEntry. Сама же процедура AddDevice в WDM драйверах, как правило, выполняет такую важную работу, как создание объекта устройства и подключение его к стеку устройств в момент вызова. (Справедливости ради, следует отметить, что если не-WDM драйвер зарегистрирует процедуру AddDevice, то ничего страшного не произойдет — и ее в соответствующий момент вызовет ДВВ, правда это событие уже не будет исполнено того смысла, как это имеет место в случае WDM драйверов для PnP устройств.)
AddDevice — новая процедура в драйверах модели WDM
В драйверах модели WDM функция DriverEntry все еще служит в качестве начальной точки соприкосновения операционной системы с драйвером. Однако теперь обязанности ее сократились. В частности, роль DriverEntry теперь состоит только в том, чтобы "опубликовать" (передать Диспетчеру ввода/вывода соответствующие адреса для вызова функций по адресу) остальные процедуры драйвера. Теперь DriverEntry не создает объект устройства (Device Object) для подконтрольного аппаратного обеспечения.
Обязанности по созданию объекта устройства возложены на новую функцию драйвера AddDevice, которая теперь публикуется (обратите внимание!) в структуре расширения драйвера (Driver Extension) во время работы DriverEntry. Структура расширения драйвера является строго определенной структурой — не следует путать ее с определяемой разработчиком драйвера структурой расширения устройства (Device Extension). Пример публикации AddDevice был закомментирован в теле функции DriverEntry в главе 3. Повторим еще раз:
DriverObject->DriverExtension->AddDevice = MyAddDeviceRoutine;
Таблица 9.1. Прототип функции AddDevice
NTSTATUS AddDevice | IRQL == PASSIVE_LEVEL |
Параметры | Описание |
IN PDRIVER_OBJECT pDriverObject | Указатель на объект данного драйвера |
IN PDEVICE_OBJECT pPDO | Указатель на объект физического устройства, созданного родительским (шинным) драйвером |
Возвращаемое значение | STATUS_SUCCESS или код ошибки |
Основной обязанностью MyAddDeviceRoutine является создание объекта устройства (теперь уже — функционального) с использованием вызова системного IoCreateDevice
и, скорее всего, подключение его к объекту физического устройства (вызовом IoAttachDevice), указатель на который поступает в параметре pPDO.
Откуда взялся объект физического устройства, указатель на который получает процедура AddDevice? Его создает шинный драйвер, когда обнаруживает подключенное PnP устройство. Зачем нужно подключаться к шинному драйверу?
Адресация и доступ к данным в IRP пакетах чтения/записи
Сразу же после создания объекта устройства (будет ли это сделано в процедуре DriverEntry, как это было указано выше для драйвера "в-стиле-NT", или в процедуре AddDevice для WDM драйверов) рекомендуется явно описать способ, каким новый объект устройства готов воспринимать поля пакета IRP, описывающие адреса областей памяти, через которые будет происходить обмен между драйвером и его клиентами. Например:
PDEVICE_OBJECT pNewDeviceObject; IoCreateDevice(. . . , &pNewDeviceObject); pNewDeviceObject->Flags |= DO_BUFFERED_IO;
либо:
pNewDeviceObject->Flags |= DO_DIRECT_IO;
По умолчанию подразумевается 'pNewDeviceObject->Flags |= 0' — метод NEITHER (ни один из первых двух).
В том случае, если новый объект устройства ориентирован на работу с нижним драйвером в стеке драйверов, следует скопировать эти флаги из объекта устройства, к которому подключен данный (одним из вызовов IoAttachXxx, см. главу 9), например:
pNewDeviceObject->Flags |= (pUnderlyingDevObject->Flags) & (DO_BUFFERED_IO | DO_DIRECT_IO);
По традиции, главными считаются запросы в форме пакетов IRP с основным кодом IRP_MJ_READ (в результате вызова ReadFile) либо IRP_MJ_WRITE (в результате вызова WriteFile).
Диспетчер ввода/вывода, если он замечает в описании устройства установленный флаг DO_DIRECT_IO, непременно проверяет возможность доступа к буферу, который клиент драйвера указывает в своем запросе как буфер с данными (WRITE) или для данных (READ), подготавливает MDL список для него и фиксирует страницы в оперативной памяти. Адрес подготовленного таким образом MDL списка вносится в поле IRP пакета под названием MdlAddress. Если попробовать получить виртуальный адрес от данного MDL списка вызовом MmGetMdlVirtualAddress, то получится именно виртуальный адрес (пользовательского адресного пространства), который предоставило пользовательское приложение в качестве адреса буфера с выводимыми данными. Адрес в терминах системного адресного пространства для той же области данных можно получить, если вызвать MmGetSystemAddressForMdl.
Когда обработка запроса ввода/ вывода полностью завершена, клиентский буфер де-блокируется и удаляется из схемы распределения системной области памяти.
В том случае, если объект устройства, которому адресован IRР пакет, описан флагом DO_BUFFERED_IO, то драйвер должен взять адрес буферной области из поля IRP пакета AssociatedIrp.SystemBuffer. Данный адрес будет адресом в системном адресном пространстве. Диспетчер ввода/вывода выделяет этот буфер в нестраничной памяти после проверки на доступность предоставленного клиентом буфера. При запросе ввода данных (READ-запрос) Диспетчер ввода/вывода по окончании операции переносит данные из системного буфера в клиентский, а адрес клиентского буфера запоминается в поле IRP пакета UserBuffer. При запросе вывода данных (WRITE-запрос) в системный буфер переносятся данные из клиентского буфера (поле UserBuffer равно NULL). B обоих описанных выше случаях, предоставленные в IRP пакете адреса AssociatedIrp.SystemBuffer можно использовать в произвольном контексте в потоках режима ядра.
В том случае, если устройство не объявило признаков DO_DIRECT_IO или DO_BUFFERED_IO, то Диспетчер ввода/вывода не делает никаких особых действий, а просто помещает адрес буферной области, переданной ему инициатором запроса в поле IRP пакета UserBuffer. Оба поля Associate.SystemBuffer и MdlAddress устанавливаются равными NULL.
В последнем случае помещенный в пoлe UserBuffer виртуальный адрес является "нехорошим" адресом. В большинстве случаев инициатором обращения к драйверу является программный поток пользовательского режима. Соответственно, предоставленный им виртуальный адрес требует для своей корректной интерпретации контекст соответствующего пользовательского потока. Что, разумеется, исключает возможность использования такого адреса в произвольных контекстах режима ядра без предварительной подготовки (подготовки MDL списка и т.п.). Поэтому метод NEITHER можно рекомендовать только для драйверов самых верхних драйверных слоев.
Длина буфера во всех случаях передается в полях Parameters.Write.Length (при WRITE-запросах) или Parameters.Read.Length (при READ-запросах).
Affinity
Сродство к процессору. В многопроцессорной среде этот параметр определяет, на каком процессоре следует выполнять данный код. На симметричных многопроцессорных системах (SMP) — по умолчанию — потоки могут выполняться на любом. Соответственно, потоки, принадлежащие одному процессу, могут выполняться на разных процессорах одновременно. Это обстоятельство обязывает разработчика драйвера рассматривать вероятность того, что его продукт будет работать и на многопроцессорном компьютере, что возлагает дополнительные требования к синхронизации потоков (если драйвер многопоточный) и синхронизацию доступа к обслуживаемому устройству (правда, о последней проблеме следует помнить всегда).
Особую заботу в многопроцессорных средах представляет обработка прерываний. Это связано с тем, что не исключена ситуация, когда, без должных предупредительных мер, при обработке прерывания на одном процессоре начинается обработка (вновь прибывшего) прерывания на другом.
Анализ информации Crash Dump файлов
Когда происходит фатальный сбой системы, операционная система может (при определенных настройках системы) сохранить состояние системы в момент сбоя в файле на жестком диске. Чтобы это выполнялось, в апплете Пуск - Настройка - Панель Управления - Система - Дополнительно - Загрузка и восстановление - Параметры (рисунок 13.3) необходимо ввести приемлемые данные. Фиксирование состояние системы в момент сбоя позволит затем вернуться к анализу причин сбоя.
Рис. 13.3 Окно системного апплета для установки параметров crash dump файла |
При помощи отладчика WinDbg можно проанализировать состояние системы в момент сбоя по содержимому crash dump файла. Там можно обнаружить всю информацию, которая была бы доступна отладчику, если бы он оказался включенным в момент сбоя. Это тип экспертизы позволяет добывать убедительные доказательства причин, приведших к фатальному сбою. Во время анализа необходимо установить:
Какой драйвер выполнялся в момент сбоя?
Какую операцию пытался выполнить драйвер в момент сбоя?
Каково содержимое структуры расширения объекта устройства в момент сбоя?
Какой объект устройства работал?
После запуска WinDbg необходимо открыть crash dump файл (полученный с целевого компьютера) для анализа, выбрав пункт меню File и затем Open Crash Dump. После выбора и загрузки необходимого файла, на экране появится информация следующего (например) типа:
Windows XP Kernel Version 2600 UP Free x86 compatible Kernel base = 0x804d4000 PsLoadedModuleList = 0x8054be30 Bufcheck 000000Dl : 00000000 00000002 00000000 F8B57624 . . .
Как видим, начальная информация показывает те же данные, что и сообщение экрана BSOD. Не должно вводить в заблуждение сообщение о не перехваченном исключении с кодом 0x80000003. Это точка прерывания, используемая собственно KeBugCheck
для останова системы, поэтому не следует придавать ей значения.
Следует отметить, что в отсутствие отладочных символов операционной системы картина получается безрадостной. Распечатка сообщений WinDbg для этого случая приводится ниже.
Loading Dump File [E:\SystemCrashDump\MEMORY.DMP] Kernel Dump File: Full address space is available
Microsoft (R) Windows Kernel Debugger Version 3.0.0020.0 Copyright (c) Microsoft Corporation. All rights reserved.
Loaded dbghelp extension DLL Loaded ext extension DLL Loaded exts extension DLL Loaded kext extension DLL Loaded kdexts extension DLL Symbol search path is: *** Invalid *** : Verify _NT_SYMBOL_PATH setting Executable search path is: ********************************************************************* * Symbols can not be loaded because symbol path is not initialized. * * * * The Symbol Path can be set by: * * using the _NT_SYMBOL_PATH environment variable. * * using the -y <symbol_path> argument when starting the debugger. * * using .sympath and .sympath+ * ********************************************************************* *** ERROR: Symbol file could not be found. Defaulted to export symbols for ntoskrnl.exe - Windows XP Kernel Version 2600 (Service Pack 1) UP Free x86 compatible Built by: 2600.xpspl.020828-1920 Kernel base = 0x804d4000 PsLoadedModuleList = 0x8054be30 Debug session time: Fri Jun 13 02:20:17 2003 System Uptime: 0 days 3:15:59 ********************************************************************* * Symbols can not be loaded because symbol path is not initialized. * * * * The Symbol Path can be set by: * * using the _NT_SYMBOL_PATH environment variable. * * using the -y <symbol_path> argument when starting the debugger. * * using .sympath and .sympath+ * *********************************************************************
WaitForEvent failed WARNING: Stack unwind information not available. Following frames may be wrong. WARNING: Stack unwind information not available. Following frames may be wrong. ******************************************************************************* * * * Bugcheck Analysis * * * *******************************************************************************
***** Kernel symbols are WRONG.
Please fix symbols to do bugcheck analysis. ******************************************************************************* * * * Bugcheck Analysis * * * ******************************************************************************* Bugcheck code 000000D1 Arguments 00000000 00000002 00000000 f8b57624
ChildEBP RetAddr Args to Child WARNING: Stack unwind information not available. Following frames may be wrong. f2270b54 804dce53 0000000a 00000000 00000002 ntoskrnl!KeBugCheckEx+0xl9 f2270b70 00000000 00000000 00000000 00000000 ntoskrnl!Kei386EoiHelper+0x251c
eax=ffdff13c ebx=0000000a ecx=00000000 edx=40000000 esi=f8b57624 edi=00000000 eip=805266db esp=f2270b3c ebp=f2270b54 iopl=0 nv up ei ng nz na po nc cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00000286 ntoskrnl!KeBugCheckEx+19: 805266db 5d pop ebp
Самой важной информацией, которую можно извлечь из приведенного выше набора строк — это код ошибочной ситуации (bugcheck code), равный 0x00D1. Остальные сведения, связанные с раскруткой стека вызовов в данной ситуации недоступны. Хотя именно данная информация особенно ценна в crash dump файлах.
Аппаратные проблемы
Априори, аппаратура есть источник проблем. Сильнее всех в этом убежден разработчик программного обеспечения. Вероятность того, что это действительно так, тем выше, чем новее и неиспытаннее аппаратура. Симптомами аппаратных проблем являются:
ошибки при передаче данных;
коды состояния устройства сигнализируют об ошибке;
устройство не реагирует должным образом на команды;
сигналы прерывания не поступают, либо они ложные.
Причина упомянутых отклонений может быть и просто в недостаточной документированности поведения устройства. Разработчик аппаратуры после некоторых размышлений изменил конструкцию, но не исправил документацию и не сообщил об изменениях разработчику драйвера, возможно, посчитав их незначительными. Могут существовать малоизвестные или неисследованные ограничения на порядок следования команд — как в смысле последовательности, так и в смысле их временных диаграмм. Аппаратные прошивки (программы, загружаемые в обслуживаемые драйвером устройства) также могут содержать ошибки. Могут возникать сбои в шинных протоколах, причем из-за непериодических сбоев других устройств, подключенных к данной шине.
Архитектура шины
Шинная архитектура, в которую должно вписаться новое устройство, будет иметь определяющее значение для конструкции нового драйвера — сможет ли он опираться на лежащие ниже в стеке драйверы или вынужден будет реализовывать весь физический интерфейс заново. Должны быть понятны все особенности, касающиеся автоматического распознавания и конфигурирования нового устройства. Следует рассчитывать на то, что новое устройство будет участвовать в общих механизмах PnP и обмена сообщениями об изменениях энергоснабжения, предлагаемых Windows 2000/XP/Server 2003.
Архитектура Windows NT 5. Введение
Различия базисов операционных систем Windows 2000, Windows XP и Windows Server 2003 настолько невелики, что позволяет в большинстве случаев говорить о них собирательно, используя термин Windows NT 5. Однако Windows 2000 является первой из них, потому о ней особенная речь.
Операционная система Windows NT 5.0 (то есть Windows 2000) представляет наиболее энергичную попытку создания гибкой и самоуправляемой операционной системы в компьютерной истории.
Является очевидным фактом то, что драйверы устройств во всех операционных системах тесно связаны с основным кодом операционной системы, и это позволяет по праву называть их привилегированными программными единицами. Предваряя глубокое погружение в мир драйверов устройств, данная глава дает представление о психологии, насколько это понятие применимо к программному обеспечению, и общей организации архитектуры Windows NT 5.
Автоматическое распознавание и конфигурирование
За каждым аппаратным устройством, подключенным к компьютеру, закрепляются системные ресурсы, которые могут включать:
диапазон адресов ввода/вывода (портов вода/вывода);
диапазон отведенных адресов памяти;
номер прерывания IRQ;
DMA канал.
Поскольку разные устройства были изготовлены в разное время разными поставщиками, то конфликты ресурсов совершенно неизбежны. Первые персональные компьютеры требовали немалой сообразительности от пользователя при конфигурировании подключаемых устройств, правильного, а подчас — единственно верного, выставления перемычек или DIP переключателей, определяющих уникальную настройку ресурсов для данной системы. Инсталляция нового устройства требовала знания, какие ресурсы уже выделены существующим устройствам. Ошибки в таком ручном конфигурировании были частыми, а результатом были либо невозможность загрузить систему, либо непредсказуемые зависания системы и неработающие устройства.
Для преодоления этих проблем были введены новые принципы построения шинных архитектур, которые бы поддерживали автоматическое распознавание и конфигурирования устройств. Автоматическое распознавание должно происходить в момент загрузки/перезагрузки системы или непосредственно в момент подключения устройства к компьютеру ("горячее" подключение). Возможности операционной системы, соответственно, должны быть таковы, чтобы соответствующее сопроводительное программное обеспечение вступало в работу без общей перезагрузки системы.
Автоматическое конфигурирование позволяет программному обеспечению устанавливать приемлемые значения ресурсов для устройств, допускающих программную настройку. Эти усовершенствования позволяют отойти от использования перемычек на подключаемых устройствах, а конечному пользователю более нет нужды знать обо всех особенностях конфигурирования системы и нового устройства в ней.
Спецификация PCI обязывает каждую отдельную функцию (функциональную единицу устройства, а значит, и шины) иметь свою собственную область для хранения конфигурационных данных, размер которой равен 256 байт. Эта область известна как конфигурационное пространство функциональных единиц PCI (PCI function's configuration space).
Первые 64 байта такого конфигурационного пространства (называемого заголовком) имеют предопределенную структуру (таблица 2.4), в то время как остальные 192 байта могут быть использованы по усмотрению разработчика данной PCI платы. Системное программное обеспечение может использовать этот заголовок для идентификации функциональной единицы PCI и выделения ей ресурсов. Заголовок включает:
Информацию о поставщике (Vendor ID), тип устройства (Device ID) и версию устройства (Revision ID).
Два стандартных регистра состояния команд для того, чтобы имелась возможность работы с ошибками в устройстве (Status Register и Command Register).
Список ресурсов, в котором описываются требования к памяти и пространству ввода/вывода.
Регистр вывода прерывания (Int Pin) и регистр линии прерывания (Int Line), описанные выше.
Указатель на расширение ПЗУ (Expansion ROM Base Address), специфичное для данного устройства.
Регистр Class Code внутренне разбит на три поля (Class Code, старший байт, Sub-Class Code и ProgIF, младший байт) и содержит информацию для использования классовыми системными драйверами (предназначенными для обслуживания целого класса родственных устройств, например, сетевых контроллеров, устройств мультимедиа, docking-станций, шинных мостов, кодирующих/декодирующих контроллеров, интеллектуальных контроллеров ввода/вывода). Например, значение Class Code, равное 0C (hex), в спецификации PCI 2.2 определяет контроллеры последовательных шин. Тогда значение Sub-Class Code, равное 00, определяет IEEE 1394, 01 — ACCESS.bus, 02 — SSA (Serial Storage Architecture), 03 — USB, 04 — Fibre Channel, 05 — SMBus (System Management Bus).
Спецификация шины 1394 была разработана с непосредственной поддержкой спецификации PnP. Каждое устройство при подключении к шине сигнализирует о своем существовании при выполнении шинной операции 'reset', что в сильной степени напоминает способ обращения с шиной USB и ее устройствами. Хост-компьютер (или другие узлы) выполняют операцию "перечисления" конфигурационных ПЗУ для того, чтобы обнаружить данное устройство и обеспечить запуск (а при необходимости — и инсталляцию) соответствующего драйвера.
Спецификация USB была разработана с непосредственной поддержкой спецификации PnP (иногда кажется, что — с откровенной ориентацией на WDM модель драйверов Windows). Каждое USB устройство при подключении к шине USB сигнализирует о своем существовании и сообщает идентификатор производителя (vendor ID) и идентификатор устройства (device ID). Эти идентификаторы являются определяющей информацией в выборе загружаемого драйвера — соответствующая информация должна присутствовать в Системном Реестре (если ничего подходящего там не обнаруживается, то инициируется процесс установки нового драйвера).
Стандарт шины PC Card удовлетворяет спецификации PnP полностью, хотя еще продолжается внесение изменений, касающихся "горячего подключения" устройств и автоматического конфигурирования.
Программное обеспечение для стандартов PC Card или CardBus разделяется на два крупных фрагмента: Сервисы Сокетов, Socket Services (в данном случае, socket — гнездо, разъем), и Службы Карт (Card Services). Первый представляет программное обеспечение уровня BIOS, и оно управляет в системе одним или более разъемов. Это программное обеспечение отвечает за обнаружение и объявление факта подключения нового устройства. Вторая часть (Card Service) управляет аппаратными ресурсами данной карты.
Для подкласса USB значение ProgIF, равное 00, описывает USB контроллер, соответствующий спецификации Universal host Controller. Значению 10 (hex) соответствует контроллер спецификации Open Host Controller, а значение FE (hex) описывает USB устройство (не являющееся хост-контроллером).
|
Таблица 5.4. Заголовок конфигурационного пространства одной функции устройства PCI |
Регистры Base Address Register 5..0 содержат адреса портов ввода/вывода (или диапазонов отведенной памяти) для доступа к внутренней памяти данной PCI функции.
Отображать все 256 адресов конфигурационного пространства в памяти или пространстве ввода/вывода достаточно расточительно, поэтому операционная система дает возможность доступа к конфигурационному пространству путем обращения к следующим двум регистрам (адреса которых устанавливаются операционной системой):
Конфигурационный адресный регистр. Этот регистр определяет номер шины (bus number), устройство, функцию и адрес доступа к данным в конфигурационном пространстве.
Конфигурационный регистр данных. Этот регистр действует как буфер между процессором и конфигурационным пространством. После установки значения в адресном регистре, чтение или запись в этот регистр данных приводит к собственно переносу информации из/в конфигурационное пространство.
Операционная система предоставляет HAL функции для доступа к конфигурационным данным. Для этого предлагается использовать функции HalGetBusData, HalSetBusData и HalAssignSlotResources. Правда, в WDM модели они считаются устаревшими (даже их описание в DDK отсутствует) — рекомендовано работать через запросы к нижнему драйверу стека.
Для идентификации драйвера, загружаемого операционной системой для обслуживания данного устройства, спецификация PCI рекомендует разработчикам ОС использовать информацию из конфигурационных регистров Vendor ID, Device ID, Revision ID, Class Code, Subsystem Vendor ID, Subsystem ID (последние два стали обязательными только в спецификации версии 2.2).
Callback, callback function
Функция обратного вызова, callback функция. Прием программирования, когда один программный поток сообщает адрес известной ему функции другому программному потоку с той целью, чтобы второй выполнил вызов этой указанной функции в определенный момент (например, если поступил некий сигнал от аппаратного обеспечения или истек определенный интервал ожидания). Процесс передачи адреса вызываемой функции часто называется регистрацией.
Цели разработки
Впервые концепции Windows NT ('New Technology', безусловно, вызывающее название из числа тех еще!) начали приобретать черты реальности в начале 1989 года. Однако хотя дата рождения столь отдаленна, пять фундаментальных NT задач остались неизменными.
Совместимость. Операционная система должна поддерживать максимально возможное множество программного и аппаратного обеспечения.
Переносимость. Операционная система должна функционировать на максимально возможном количестве имеющихся сейчас и ожидаемых в перспективе аппаратных платформ.
Расширяемость. Поскольку требования рынков постоянно растут, операционная система должна уметь с легкостью расширять набор своих возможностей и перечень поддерживаемой аппаратуры при минимальном вмешательстве в свой внутренний код.
Надежность и устойчивость. Операционная система должна быть стойкой к неумышленному или преднамеренному неправильному использованию компонентов. Пользовательские приложения не должны иметь возможности приведения системы к фатальным сбоям. (Несмотря на огромное количество злых шуток в адрес зависаний Windows, следует, наконец, признать, что XP Prof работает хорошо.)
Производительность. Операционная система должна обеспечивать хорошую производительность на всех поддерживаемых аппаратных платформах.
Разумеется, провозглашение и достижение цели не всегда есть одно и то же, и серьезные компромиссы общих и текущих задач оказываются неизбежными. NT столь же подвержена компромиссам, как и все остальные операционные системы.
Цели разработки подсистемы ввода/вывода
Подсистема ввода/вывода вносит корректировки в список задач новых систем Windows, но особенно следует отметить:
Конфигурируемость и в терминах аппаратуры, и в терминах программного обеспечения. Для драйверов Windows это означает полную поддержку PnP спецификации для шин и устройств.
Приоритетность и прерываемость. Код, написанный для обслуживания ввода/вывода, никогда не должен блокироваться и должен содержать безопасные программные потоки.
Безопасность при использовании на многопроцессорных платформах. Один и тот же драйверный код должен безошибочно работать и на однопроцессорных и на многопроцессорных компьютерах.
Объектная ориентированность. Услуги, предоставляемые кодом, должны "формулироваться" в терминах вполне определенных структур данных, которые служат выполнению разрешенных операций.
Пакетное управление. Запросы, сделанные к подсистеме ввода/вывода формулируются, передаются и отслеживаются с помощью четкого формата "рабочего рецепта", известного как IRP пакет (I/o Request Packet, пакет запроса на ввод/вывод).
Поддержка асинхронного ввода/вывода. Подсистема ввода/ввода должна позволять коду программы, по обращению которой создан запрос IRP, выполняться параллельно тому программному коду, который осуществляет обработку запроса. Также должен существовать механизм оповещения инициатора вызова о полном завершении обработки запроса.
Помимо уже указанных задач, существует и насущная необходимость обеспечения повторного использования кода (так называемой реентерабельности, повторной входимости в код). Это означает необходимость не только сложного структурирования собственно кода, отвечающего за операции ввода/вывода в одном устройстве, но и другого взгляда на взаимодействие слоев всей подсистемы ввода/вывода. Например, код, отвечающий за управление шиной, должен быть реализован автономно от кода, отвечающего за конкретное устройство, подключенное к шине. Выполнение этого требования позволяет повторно использовать код шинного драйвера программными блоками (например, драйверами), отвечающими за другие устройства на этой шине, которые, кстати сказать, могут подключаться, и удаляться, и, весьма возможно, пока еще и вовсе не изготовлены.
Частные приемы восстановления системы
При поэтапном тестировании драйвера (сначала — процедур загрузки и выгрузки, затем скелета рабочих процедур, затем алгоритма функционирования и т.п.) неполадки в коде вряд ли вызовут проблемы общего функционирования системы. Однако полностью исключать вероятность разрушения системы нельзя. Поэтому рассмотрим некоторые методы приведения ее к работоспособному состоянию после сбоев.
Прежде всего, после фатального сбоя системы следует разрешить ей выполнить проверку целостности файловой системы. Несмотря на длительность этой процедуры, следует довести ее до завершения (при соответствующем запросе системы), поскольку накопления дефектов недопустимо — это опаснее, нежели каждый из сбоев в отдельности.
В некоторых случаях установка нового драйвера может приводить к невозможности загрузить систему обычным способом. В этом случае возможны несколько вариантов развития событий.
Во-первых, по нажатию клавиши F8 можно произвести выбор в начале загрузки компьютера разных вариантов, из которых наиболее часто применяемыми для восстановления работоспособности являются два:
загрузка последней удачной конфигурации;
загрузка в безопасном режиме.
В последнем случае по завершении загрузки можно удалить драйвер, ставший источником неприятностей, из системы средствами Диспетчера устройств, либо просто стереть бинарный файл нового "плохого" драйвера в том месте, где его должна обнаруживать операционная система — с целью не допустить его загрузку в следующий раз.
В тех случаях, когда загрузка системы более или менее успешно доходит до конца, администратор может выполнить восстановление состояния системы в одной из ранее сохраненных резервных точек (об этом говорилось в 3 главе).
При более серьезных проблемах можно воспользоваться загрузкой с внешнего носителя. Способ создания загрузочных дискет для Windows XP описан на Интернет-сайте Microsoft (статья Базы Знаний номер Q30559) по адресу support.microsoft.com/default.aspx?scid=kb;ru;30559. Загрузочная дискета позволит получить доступ к логическим дискам с установленной системой Windows.
Логично пользоваться этим способом на компьютерах, не поддерживающих загрузки с CD ROM. (Имеется в виду то, что дискета обеспечивает доступ к загрузочному CD диску, который также необходим в этом процессе.)
В том случае, если в распоряжении имеется компакт-диск для установки Windows и компьютер может быть загружен с CD ROM (практически все современные компьютеры на платформе Intel позволяют это сделать), то следует выполнить именно такой способ загрузки и войти в Консоль Восстановления системы (Recovery Console). B этом режиме будут доступны системные консольные команды, список которых можно получить по команде help. Наиболее актуальной является команда принудительной проверки диска (например, логического диска D)
CHKDSK D: /p
В данном случае ключ /p обеспечивает принудительную проверку.
В том случае, если оказалась испорчена загрузочная запись (MBR) или следует избавиться от загрузочной записи нестандартных загрузчиков LILO, MILO и т.п., следует воспользоваться программой fixmbr. В этом же режиме можно выполнить поверку диска. Перезаписать загрузочный (boot) сектор на логическом диске можно при помощи команды fixboot.
При ненарушенной файловой системе затруднения в загрузке могут быть обусловлены плохим состоянием информации в Системном Реестре. Некоторые источники предлагают (помимо специальных программ) такой метод восстановления как простое копирование ранее сохраненных файлов Системного Реестра на их место в директории %systemroot%\config. В нормальном рабочем состоянии операционная система не разрешает доступа к ним (для просмотра, копирования или перезаписи), однако, при загрузке с установочного CD диска, это становится допустимой операцией. Файлы Реестра, которые всегда имеют солидные размеры, для этого случая можно сохранять на других логических дисках компьютера.
Заметим также, что все затруднения, связанные со спецификой доступа к разделам файловой системы в формате NTFS, можно обойти, если установить операционную систему на логический диск с форматом FAT32.
Утратив некоторые преимущества NTFS (такие, как запись файлов более 4Гбайт и более высокая надежность), можно будет получать доступ к таким разделам с дискеты MS-DOS или при загрузке Windows 98 с другого логического диска. Совершенно аналогичный и официальный совет дает Microsoft, предлагая установить на компьютере еще один экземпляр Windows NT на другом логическом диске, что, безусловно, предоставит возможность доступа ко всем NTFS разделам, в том числе — к тому, на котором испытывается драйвер и, возможно, произошел сбой.
Наконец, еще одним средством доступа к файлам операционной системы Windows NT 5, размещенным на диске с файловой системой NTFS, является программа NTFSDOS, разработанная фирмой SysInternals. При загрузке MS DOS либо при загрузке с загрузочной дискеты, сделанной под Windows 98, эта программа (если она записана в не-NTFS разделах) может быть запущена и позволяет получить доступ к NTFS разделам жесткого диска, включая запуск программы CHKDSK. (Интересно, что в свой загрузочный набор программа NTFSDOS при установке включает еще файлы Ntoskrnl.exe, Ntfs.sys и Ntdll.dll из установленной на компьютере версии Windows NT.) Соответственно, это позволяет модифицировать данные в NTFS разделах — при наличии полной версии NTFSDOS Professional — или копировать их в другие не-NTFS разделы (демо-версия), если они присутствуют в системе. Возможен вариант, при котором подготавливается комплект из 2-х дискет, с которых и выполняется загрузка.
Приступая к тестированию драйвера, разработчик должен в достаточной мере подготовиться к потенциально возможным неприятностям, которые могут возникнуть из-за нарушения целостности операционной системы, сводя свое вмешательство к минимуму. Тем более, что неполадки, действительно требующие переустановки всей операционной системы, возникают не так уж часто.
Чтение crash-экранов
Несмотря на ужасающее название "system crash", системный сбой является вполне заурядным событием. Операционная система всего лишь сообщает, что она перешла в такое внутреннее состояние, которое несовместимо с возможностью продолжения работы. Следовательно, полагает операционная система, лучше прекратить работу сейчас, нежели функционировать с этим ошибочным состоянием. И это решение во многих случаях предпочтительнее.
Кто инициирует объявление системного сбоя?
Во-первых, некоторые компоненты уровня ядра, которые "по долгу службы" обнаружили некорректное состояния, могут решить, что пришла пора "сказать стоп". Например, если Диспетчер ввода/вывода обнаруживает, что драйвер передал в вызов IoCompleteRequest
завершенный ранее пакет IRP, то Диспетчер ввода/вывода непременно вызовет этот самый сигнал фатального системного сбоя.
Второй сценарий сложнее. Программный код, работающий в режиме ядра, является непосредственным источником ошибки и генерирует исключение (exception) во время какой-либо операции. Если процедура уровня ядра, которая выполнила эту запрещенную операцию, непосредственно не перехватит это исключение, то результатом будет необрабатываемое исключение режима ядра. Когда это происходит, в работу включается стандартный обработчик исключений в ядре операционной системы, который инициирует системный останов.
Независимо от того, какие подсистемы ядра выступают инициатором системного останова, реально выполняется вызов одной из двух функций:
VOID KeBugCheck( BugChCode ); VOID KeBugCheckEx( BugChCode, Arg1, Arg2, Arg3, Arg4 );
Любая из этих функций вызывает останов системы и (необязательно) сохраняет файл со снимком памяти (crash dump file) на жестком диске. В зависимости от выбора системного администратора, операционная система после этого прекращает работу, перезапускается или запускает отладку ядра (kernel's debug client).
Аргумент BugChCode в вызове KeBugCheck указывает причину сбоя в виде значения типа DWORD. Этот аргумент известен также под именем "bugcheck code". Дополнительные 4 аргумента в функции KeBugCheckEx дают возможность видеть в диагностическом сообщении дополнительную информацию (что, возможно, позволит удачнее локализовать источник сбоя).
Коды "bugcheck codes" могут быть использованы из числа определенных Microsoft (см. файл BUGCODES.h), либо можно определить в драйвере дополнительно.
Драйвер может выполнить вызов любой из этих двух функций. Вообще говоря, общепринятой практикой для отладочных версий драйвера является организация системных STOP-сообщений в критических точках кода.
Что следует проверять?
В общем случае, тесты для драйвера (предполагая, что аппаратное обеспечение тестируется дополнительно) можно разделить на следующие категории.
Тесты на нормальную реакцию должны подтверждать полноту и точность функций драйвера. Так ли откликается драйвер на команды, как это от него ожидается?
Тесты на ошибочные воздействия должны удостовериться, правильно ли реагирует драйвер на воздействия, которые, вообще говоря, не должны к нему применяться. "Ошибочное" воздействие может также состоять в плохом наборе данных, поступившем в пользовательском запросе.
Тесты граничных условий испытывают анонсированные пределы функционирования драйвера и устройства. Не исключено, что в силу работы драйвера в системе конечные предельные параметры окажутся хуже прогнозируемых.
Тесты на предельную нагрузку проверяют драйвер и устройство при высоких уровнях активности.
Тесты на функционирование в условиях ограниченности ресурсов, то есть работа при ограниченной доступности центрального процессора, ограниченность объемов доступной оперативной памяти.
Фирма Microsoft предлагает тесты на аппаратурную совместимость (HCT, Hardware Compatibility Tests), которые являются официальными тестами для аппаратуры по поводу возможности ее работы под Windows 2000/XP/2003. Набор включает следующие тесты:
Общие системные тесты, которые экзаменуют центральный процессор, последовательные и параллельные порты материнской платы, клавиатуру и средства поддержки слоя аппаратный абстракций HAL.
Тесты по проверке специфических типов аппаратного обеспечения, а именно — видеоадаптеров, мультимедийных устройств, сетевых интерфейсов, накопителей на магнитной ленте, SCSI устройств и т.п.
Общие тесты по тестированию работы системы под действием высоких нагрузок на системные ресурсы и устройства ввода/вывода.
Тестирование проходит под управлением тест-менеджера с графическим интерфейсом, который автоматизирует прохождение тестов и сбор результатов. Даже если класс аппаратуры, для которой разрабатывается драйвер, не прошел серию испытаний HCT, то все равно этот набор тестов может послужить средством для выяснения того, как будет работать новый драйвер в условиях повышенной нагрузки на систему.
Цифровое подписание драйвера
Огромное количество драйверов сторонних поставщиков (третьей стороны, third-party companies) поставляется в составе CD дистрибутива Microsoft Windows. Для того чтобы участвовать в этой программе, поставщик драйвера должен выполнить некоторые требования, сформулированные Microsoft. B интересах Microsoft обеспечить достижение двух взаимоисключающих целей:
обеспечить работу под управлением Windows максимально разнообразной и многочисленной аппаратуры при обычном способе распространения дистрибутива;
обеспечить стабильность работы новых драйверов без нарушения каких-либо свойств или целостности системы.
Поскольку драйверы работают в режиме ядра, у них имеется возможность привести систему к краху. Так как нестабильность системы, скорее всего, будет отнесена пользователем на нестабильность самого ядра, очевидно, что в интересах Microsoft определить список сертифицированных поставщиков и сертифицированных драйверов для своей операционной системы.
Разумеется, совместимость Windows 2000/XP/2003 с большим количеством аппаратуры, нежели другие операционные системы, является сильным фактором стимулирования продаж. Следовательно, Microsoft непрерывно контактирует с производителями аппаратного обеспечения на предмет получения своевременных и протестированных конечных версий драйверов. Вплоть до того, что конфигурация Windows Server 2003 Datacenter Edition поставляется только OEM поставщиками совместно с компьютером.
Для достижения вышеописанных целей, в Microsoft была создана специальная группа, the Windows Hardware Quality Labs (WHQL), которая проводит сертификацию аппаратуры и драйверов устройств. Выгоды от участия в этой программе сертификации для производителей/поставщиков аппаратного обеспечения состоит в
использовании логотипа Windows как знака соответствующей ступени сертифицированности поставляемой аппаратуры и программного обеспечения к нему;
включении в официальный перечень поддерживаемого и сертифицированного оборудования для различных операционных систем разработки Microsoft (см.
интернет-страницу microsoft.com/hcl);
автоматического распространение драйверного обеспечения вместе с будущими поставками новых под-версий Windows;
получении цифровой подписи.
Подробнее ознакомиться с условиями участия в этой программе (процедурой и ценами на работы) можно на интернет-странице microsoft.com/hwtest.
Как часть результата участия в программе WHQL, драйвер получает цифровую подпись (digital signature), защищенную криптографическими методами, которая позволяет Windows 2000/XP/2003 осуществлять инсталляцию драйвера без выдачи обескураживающего предупреждения о "надвигающейся" опасности. Цифровая подпись является, по мнению Microsoft, средством отсеивания ненадежного кода и устройств и гарантией того, что устанавливаемый код является оригинальным драйверным кодом от поставщика. Применение собственно цифровой подписи касается двух моментов:
Файла-каталога (с расширением CAT), который включается в распространяемый пакет драйверного программного обеспечения. Он и содержит цифровую подпись, предоставленную Microsoft.
Запись в INF файле в составе секции [Version], где указывается ссылка на CAT файл.
Сама цифровая подпись ни в коей степени не производит модификацию кода драйверного программного обеспечения.
Class Driver
Классовый драйвер. Высокоуровневый драйвер, который представляет поддержку целого класса устройств (например, клавиатуры и мыши). При этом классовый драйвер абстрагируется от их аппаратных особенностей, поддержка которых возлагается на драйверы более низкого уровня, общение с которыми происходит при помощи IOCTL запросов, callback функций или функций, экспортируемых этими драйверами нижнего уровня.
Context
Контекст. Этот термин употребляется программистами для обозначения двух существенно различающихся явлений.
Когда производится регистрация callback функции, производится определения параметра, который она получит в качестве аргумента при вызове. Как правило, это указатель на буфер с данными, которые callback функции необходимо знать, чтобы ориентироваться, зачем же ее вызвали. В драйверах чаще всего в роли контекста (контекстного указателя) выступает указатель на структуру описания устройства или структуру описания расширения устройства, например при регистрации DpcForISR (см. ниже). Именно так чаще всего и следует трактовать словосочетание "in device context" - в контексте устройства, иными словами: применительно к данному устройству.
Структура, отражающая состояние программного потока на данный момент. Создаваемая операционной системой Windows для каждого потока, служит, например, для запоминания состояния регистров и некоторой другой информации на момент окончания последнего по времени кванта времени, выделенного потоку. Структура контекста потока режима ядра несколько проще, чем контекста потока пользовательского режима. Для драйвера к контексту (по сути этого понятия) следует отнести также состояние объектов, которыми он владеет, IRP пакеты (см. ниже) в очереди, к которым он может получить доступ, и даже состояние обслуживаемого устройства.
Вольное, но все еще допустимое, значение слова "контекст" подразумевает некие признаки потока, вызвавшего одну из процедур драйвера (Driver Routine), которые перешли на вызванный код. С этой точки зрения, код драйвера режима ядра может выполняться в одном из трех контекстов:
в контексте свойств пользовательского потока, который инициировал обращение к драйверу
в контексте системного рабочего потока режима ядра
как результат поступления сигнала прерывания от аппаратуры, то есть ни в каком из контекстов существующих потоков или процессов, которые были текущими в момент прерывания
Приняв это определение, несложно понять, почему становится возможным благополучное разрешение следующей ситуации.
Предположим, в некотором драйвере рабочая (dispatch) процедура, предназначенная для обработки IOCTL запросов от пользовательских приложений, получает при методе буферизации METHOD_NEITHER виртуальный адрес буфера с пользовательскими данными (или для пользовательских данных). Этот адрес поступит в драйвер в том самом виде, как он был виден в пользовательском приложении (с адресом меньше 0x80000000, например, 0x00012A0). Однако этот виртуальный пользовательский адрес имеет смысл только в адресном пространстве вызывающего пользовательского приложения (такое же число в качестве адреса в другом приложении пользовательского режима указывало бы на совершенно другую область памяти) - и это одно из неотъемлемых свойств виртуальной адресации. Должен произойти крах системы?
Тем не менее, драйвер, к которому могут обратиться с подобными запросами разные пользовательские приложения (причем, почти одновременно), выполнит свою работу абсолютно корректно - данные попадут по назначению. Почему? Потому что рабочая процедура, обрабатывающая IOCTL запросы от клиентов драйвера, работает в контексте вызвавших ее потоков, вследствие чего трактовка виртуальных адресов в подобных случаях не вызывает у операционной системы никаких затруднений. Драйвер получает доступ к нужной области памяти, и при этом никакой ошибки доступа к, на первый взгляд, "чужой" памяти не возникает.
Cover
[Next]
©Солдатов В.П., 2004 E-Book Copyright© by [Korvin] |
CurrentControlSet
Текущая работающая конфигурация. Подраздел Системного Реестра HKLM\System\CurrentControlSet, описывающий текущую конфигурацию системы, на самом деле представляет собой информацию из одного из подразделов Системного Реестра, называющихся HKLM\System\CurrentControlSet00X, где X - число от 1 до 3. Информация какого конкретно фрагмента реестра используется в качестве текущей (то есть значение X), можно определить по значению параметра Current в разделе HKLM\System\Select, который является указателем на один из подразделов HKLM\System\CurrentControlSet00X. (В самом деле, если что-то добавить или удалить в подразделе CurrentControlSet, то точно такое же изменение произойдет и в том подразделе CurrentControlSet00X, на который "указывает" параметр Current.)
Deferred Procedure Call
Процедура отложенного вызова. Функция, которая будет вызвана позже (можно считать, "в более спокойной обстановке"). Из таких функций составляется очередь, чем ведает Менеджер (Диспетчер) ввода/вывода. Одно из применений этого типа функций уже упомянуто выше (DpcForISR), o некоторых других будет рассказано чуть позже. Для учета DPC процедур операционная система поддерживает DPC объекты.
Device Enumeration — всеобщая перепись устройств
Как было сказано ранее, Менеджер конфигурирования PnP ответственен за выполнение переписи устройств, обнаруженных в системе. Стартовало ли устройство при загрузке системы, или оно добавляется/удаляется позже, шинный драйвер отвечает за идентификацию и ведение списка подключенной аппаратуры. Аппаратные ресурсы, необходимые устройству, предоставляются драйверу этого устройства, когда ему отправляется сообщение (IRP пакет) с кодом IRP_MJ_PNP и с суб-кодом IRP_MN_START_DEVICE. Весьма показателен в этом отношении пример из пакета DDK, посвященный драйверам шины и устройств TOASTER.
Device Extension
Расширение объекта устройства. Структура, конечный вид которой определяется автором драйвера (иными словами, он может делать в ней все что угодно). Данная структура создается в самый момент создания объекта устройства при помощи системного вызова IoCreateDevice, одним из аргументов которого является ее размер — в момент создания объекта устройства система выделяет место (в нестраничном пуле памяти) и под эту "авторскую" структуру. В программировании драйверов плохим тоном считается создание переменных, глобальных для всего кода драйвера. Взамен, рекомендуется размещать эти претендующие на глобальность данные в структуре расширении устройства. Указатель на структуру расширения можно найти в объекте FDO сразу же после его создания по вызову IoCreateDevice.
Существуют устоявшиеся традиции того, какие данные должны присутствовать обязательно в расширении устройства (это несложно увидеть из двух-трех примеров), внесение же дополнительных данных — по воле разработчика драйвера.
Device Instance
Физический аппаратный компонент. Экземпляр устройства, возможно, один из нескольких других однотипных устройств, обслуживаемых данным драйвером. Это может быть как геометрически большой и автономный фрагмент (монитор или привод CD-ROM), так и мелкий, физически неотделимый от других, несомненно солидных устройств внутри современного компьютера, компонентов, например, набор микросхем на материнской плате для обслуживания PCI шины.
Device Object, PDO, FDO
Объект устройства, объект физического устройства (Physical Device Object), объект функционального устройства (Functional Device Object). Абстракции, созданные для представления дееспособных единиц в драйверной архитектуре. Объекты физических устройств (далее будем обозначать их аббревиатурой 'PDO') создаются шинными драйверами, когда те находят подключенные к шине реальные (физические) устройства - для отображения самого факта их существования. Именно к PDO объектам, принадлежащих шинному драйверу, будут подключаться объекты функциональных устройств (далее будем обозначать их как 'FDO' либо 'объект-устройство') полноценных WDM драйверов. Объект FDO создается собственно драйвером обнаруженного устройства (при помощи вызова IoCreateDevice). Объект FDO может иметь свою "символьную ссылку" (symbolic link), по которой будет получать запросы от клиентов своего драйвера. Следует обратить внимание на то, что IRP запросы получает не драйвер (хотя, и он тоже), а именно FDO объект, созданный в драйвере: в каждую рабочую процедуру (Dispatch Routine) драйвера в качестве аргументов передается не только указатель на пакет IRP запроса, но и указатель на FDO, которому адресован этот запрос. В результате подключения FDO-объекта (что внутри драйвера устройства) к PDO-объекту (что внутри шинного драйвера) создается впечатление, что один драйвер подключается к другому. Отсюда и берет начало жаргонизм "стек драйверов"!
Те, кто не боится трудностей, может, например, создать в своем драйвере несколько FDO и получить для каждого символьные ссылки. Чего этим можно добиться? Например, можно предоставлять разным клиентам драйвера функционально разные услуги по доступу к обслуживаемому данным драйвером устройству — в зависимости от того, какую символьную ссылку использовал новый клиент при открытии драйвера.
Драйвер в-стиле-NT, который не обслуживает реального устройства или обслуживает не-PnP устройство, не получает никаких PDO ни от каких шинных драйверов. Однако он все равно создает FDO объект, хотя бы только для того, чтобы иметь символьную ссылку (symbolic link) для связи с внешним миром. Возможны возражения, что существуют способы, как "добраться" до драйвера и без символьной ссылки — по зарегистрированному им при помощи вызова IoRegisterDeviceInterface интерфейсу (в большинстве случаев — путь для тех, кто коллекционирует трудности), однако, в данном случае для регистрации интерфейса опять-таки необходимо иметь указатель на PDO объект! Иными словами, трудно представить себе драйвер, которому не нужен был бы свой FDO объект, — этакий живущий сам по себе обрывок кода режима ядра.
Device Stack, Driver Stack
Стек устройств, стек драйверов. Страшилка для легковерного новичка, которому может показаться, что все объекты устройств (PDO и FDO) всех устройств, создаваемые в драйверах, включаются в один стек, в соответствии с многослойным подходом WDM модели. Как там только не пропадают IRP пакеты и еще остается какое-то быстродействие?!
Между тем все устройства образуют довольно объемное дерево устройств (а вовсе не единый сквозной стек!) — и его можно рассмотреть, если обратиться в программе DeviceTree, поставляемой в составе пакета Microsoft DDK. B этом дереве, например, из точки, которую можно назвать "PCI шина" вырастают ветви шины USB, шины FireWire, ISA шины, устройств SCSI протокола. На самом деле, стеком следует считать относительно короткий отрезок соответствующей ветви, начиная от драйвера и его объекта устройства, куда вошел IRP запрос (например, от пользовательского приложения), до места, где запрос окончательно был обработан. Можно считать, что в случае монолитного драйвера стека устройств нет вовсе, поскольку драйвер сам занимается обработкой всех своих IRP. В случае же WDM драйвера для устройства на шине USB — стек от объекта устройства внутри этого драйвера "дотягивается" до объекта устройства внутри драйвера хост-контроллера USB.
Стек устройств формируется динамически по мере загрузки драйверов и подключения к нему устройств. Следовательно, подключение FDO нового драйвера к какому-нибудь драйверу в середине стека по окончании его формирования означает лишь одно — это будет боковой "сучок" и никакие запросы от верхних драйверов через него не пойдут (это замечание к популярной теме фильтр-драйверов).
DeviceID
Идентификатор устройства. Информация, идентифицирующая устройство. В inf-файле, используемом при инсталляции, а позже — в Системном Реестре информация об устройстве хранится в виде строки, формат которой может быть определен, например, как
VEN_XXXX&DEV_YYYY&SUBSYS_ZZZZZZZZZ&REV_VV
Где XXXX — идентификатор производителя (VENDOR — поставщик), YYYY — идентификатор устройства, ZZZZZZZZ и W уточняющие параметры. (Заметим, что эти числа хранятся во внутренних регистрах PnP устройства и становятся доступными при его подключении к шине.)
Драйвер шины, к которой подключается устройство, передает идентификатор устройства в PnP Менеджер по запросу IRP_MN_QUERY_ID. PnP Менеджер использует эту информацию для того чтобы определить — какой драйвер следует использовать (если он уже был до этого установлен) или начинает процесс инсталляции драйвера, в результате чего будет создан и соответствующий подраздел в Реестре.
Подробно форматы идентификационных записей будут рассмотрены в главе 12.
Динамическое перемещение кода драйвера в страничную память
Наконец, третий способ перемещения кода драйвера в страничную память является динамическим и происходит под управлением самого драйвера. Весь программный код драйвера, обычно размещенный в области кода операционной системы (начинающейся с адреса 0x80000000), можно пометить как "страничный" и переместить в странично организованную память системным вызовом MmPageEntireDriver, которому в качестве параметра передается любой адрес или указатель на любую процедуру в составе кода драйвера, например, AddDevice (если эта процедура присутствует). Обратное действие выполняет вызов MmResetDriverPaging, который восстанавливает статус всех секций кода драйвера, данный им при компиляции, и все секции, которые были созданы как нестраничные, фиксируются в оперативной памяти.
Вызовы MmPageEntireDriver и MmResetDriverPaging
следует выполнять только в коде, работающем на уровне IRQL равном PASSIVE_LEVEL.
Директории идентификаторов
Файлы идентификаторов (symbol files) создаются по дополнительно установленным флагам компиляции при компиляции и сборке проекта. В них находятся имена локальных и глобальных переменных, адреса функций и информация об определениях типов данных. Приводится также информация о номерах строк, поэтому машинные инструкции могут быть легко соотнесены с исходным текстом. Программные средства Microsoft могут предоставлять эти файлы в нескольких форматах: более старом (но более типичном) COFF (Common Object File Format) и формате PDB (Program Database), что определяется настроечными флагами компиляции и сборки.
Сама операционная система Windows тоже поставляется с файлами идентификаторов, которые могут быть и на дополнительном диске дистрибутива, и в составе DDK (разумеется, речь не идет об исходных текстах). Для операционной системы Windows 2000 такие файлы, возможно, до сих пор можно загрузить с Интернет сервера Microsoft. Для остальных версий они распространяются Microsoft в рамках подписки MSDN либо в составе набора CD дисков Driver Suite. Поскольку файлы идентификаторов меняются при каждой новой сборке (build), то обновление системы (service pack) требует также и обновления файлов идентификаторов, относящихся к системным модулям.
Доступ к файлам идентификаторов драйвера и системных модулей исключительно важен, поскольку они обеспечивают информацию о стеке вызовов и привязку к исходному тексту (драйвера).
Когда установлены необходимые файлы идентификаторов, отладчику WinDbg необходимо сообщить об их местонахождении. Это выполняется с использованием в пункте Symbol File Path пункта меню File. Идентификаторы операционной системы обычно устанавливаются в директории %SystemDir%\Symbols. Файлы идентификаторов драйвера (расширения .DBD или .PDB) обычно хранятся вместе с бинарным файлом драйвера (.SYS, .WDM).
Директории исходных текстов
Помимо файлов идентификаторов отладчику понадобятся файлы исходных текстов драйвера (.c, .cpp, .h). Путь к ним необходимо указать в пункте Source File Path пункта меню File.
DIRQL
Уровни аппаратных прерываний. Обработка сигналов прерываний (IRQ), поступающих от реальных устройств, считается важным делом и должна проходить при повышенных уровнях приоритета режима ядра (IRQL). Поэтому данным уровням дано особое название Device IRQL, то есть DIRQL. Уровни приоритетов DIRQL в системах на базе Intel занимают верхние 16 IRQL уровней в операционной системе.
Dispatch Routines
Рабочие процедуры. Функции, которые регистрируется в вызываемой самой первой процедуре драйвера (DriverEntry). Регистрация производится путем заполнения элементов массива MajorFunction (указатель на начало этого массива DriverEntry получает косвенно через аргументы своего вызова). Индексом в этом массиве являются коды IRP_MJ_Xxx, то есть описанные числами типы пакетов IRP. Если драйвер считает необходимым обрабатывать IRP запросы какого-либо типа, то в соответствующем элементе массива MajorFunction он регистрирует соответствующую функцию (записывает ее адрес). Диспетчер ввода/вывода, ориентируясь на заполнение этого массива, вызывает нужные функции драйвера - dispatch routines. Смысл данного словосочетания - "диспетчеризуемые" процедуры драйвера (а не диспетчерские!), что правильнее будет заменить на "рабочие процедуры" (функции) драйвера. Поскольку вне драйвера важны только адреса рабочих процедур (которые и регистрирует функция DriverEntry), то все рабочие процедуры драйвера могут иметь совершенно произвольные имена. Редкие исключения составляют обязательные имена функций в некоторых специальных драйверах, например, видео.
Диспетчер (менеджер) ввода/вывода
Диспетчер ввода/вывода (I/O Manager), ДВВ, является исполнительным компонентом, который реализован в виде множества процедур уровня ядра. ДВВ представляет для процессов пользовательского режима единообразный подход к операциям ввода/вывода. Предлагаемая модель не делает различий, получает ли пользовательский процесс доступ к клавиатуре, коммуникационному порту или файлу на диске — способ обращения оформлен одинаково.
ДВВ представляет запросы от процессов пользовательского режима драйверным процедурам в форме пакета запроса на ввод/вывод, то есть пакета IRP. Пакет IRP является своего рода рабочим рецептом, созданным ДВВ, который передается в драйверные процедуры. Работа же драйвера состоит в том, чтобы должным образом этот запрос обработать. Большая часть данной книги как раз посвящена правильной организации драйверного кода, обрабатывающего IRP пакеты.
Фактически, Диспетчер ввода/вывода является интерфейсом между кодом пользовательского режима и драйверами устройств. Таким образом, он является первым и важнейшим системным компонентом, с которым взаимодействует драйвер.
При посредничестве Диспетчера ввода/вывода с драйвером могут общаются и компоненты уровня ядра, например, могут обращаться другие драйверы (вспомним, хотя бы, случай с программой Debug Print Monitor, глава 2).
Дизассемблер IDA
Интерактивный дизассемблер IDA фирмы DataRescue позволяет получать крупинки знания о взаимодействии драйверов с операционной системой путем изучения хорошо зарекомендовавших себя драйверов, исходный текст которых зачастую недоступен. Для исследования драйверов NT, которые собираются в бинарный модуль (файл) с расширением sys, наиболее подходят версии IDA после 4.15. Последнюю испытательную версию можно загрузить (после регистрационного запроса) с Интернет-сайта фирмы datarescue.com. На момент подготовки книги к печати это была версия 4.6.
Помимо того, что полная версия дизассемблера IDA позволяет работать с исполняемым кодом не только для процессоров Intel, весьма полезной и интересной особенностью этой программы является ее умение визуализировать структуру вызовов в графических диаграммах, как это показано на рисунке 2.22.
Рис. 2.22 Диаграмма вызовов в одном из окон дизассемблера IDA |
Безусловно, рассмотреть возможности дизассемблера IDA Pro весьма сложно даже в рамках отдельной главы. Поэтому, учитывая мощь и полезность этой программы для самообразования разработчика драйверов, следует порекомендовать книгу Криса Касперских "Образ мышления — дизассемблер IDA", изд. Солон-Р, 2001, ISBN 5-93455-093-4, 480 страниц (правда, читателю следует быть готовым к тому, что в упомянутой книге рассмотрена далеко не последняя версия программы).
DMA, Direct Memory Access
Метод обмена данными между устройством и оперативной памятью без участия центрального процессора. Процесс DMA переноса данных может протекать либо под управлением самого устройства (bus-mastering DMA или first-party DMA) либо под управлением системного DМА контроллера (slave DMA или third-party DMA).
DMA операции с использованием системных контроллеров
Первоначальная спецификация персонального компьютера по IBM (и последовавшие стандарты) включали основную плату (называемую ныне "материнской") с набором общих DMA котроллеров. Каждый DMA контроллер предоставляет DMA каналы (DMA channel), и данное устройство может быть сконфигурировано так, чтобы использовать один (или более) доступных каналов. Изначально существовало четыре канала, которые были расширены до семи при введении спецификации AT. Системный DMA (system DMA) известен еще как slave DMA.
Преимущество использования системного DMA состоит в том, что количество аппаратной логики для реализации DMA в устройстве уменьшается. К минусам следует отнести то, что в случае совместного использования канала данным устройством, оно получает возможность участия в DMA передаче данных только один раз за определённый временной интервал. В каждый конкретный момент времени DMA канал находится "в собственности" только одного устройства, остальные попытки использования этого канала со стороны других устройств откладываются до момента, когда первое устройство "откажется" от собственности на канал. Такое совместное использование канала дает плохие результаты при использовании его для двух высокоскоростных устройств. Контроллер флоппи дисководов в большинстве персональных компьютеров как раз является устройством, реализующим операции slave DMA.
Документация Microsoft DDK
После инсталляции пакета DDK XP (build 2600) или DDK Server 2003 (build 3790) среди вновь созданных директорий можно обнаружить каталог \help\, в котором размещены файлы-справки, имеющие расширение .chm. При входе через "Пуск-Программы-..." чтение документации становится доступно в режиме, когда возможны переходы по ссылкам между разными файлами. (Таким образом, вся справочная информация выступает сплошным массивом, что вряд ли можно признать методически правильным в период начального ознакомления.) В противном случае, пользователь имеет дело с автономными файлами.
Перечислим наиболее значительные из них.
Gstart.chm | Введение для начинающих (Getting Started) | |
Kmarch.chm | Основное руководство по драйверной архитектуре Windows, модели WDM, деталям внутреннего устройства Windows NT, которые следует знать разработчику драйверов, справочник по функциям ЕхХхх, КеХхх, HalXxxx, IoXxx, MmXxx, ОЬХхх, PsXxx, RtlXxx, WMI и ZwXxx | |
Buses.chm | Особенности разработки драйверов устройств, подключаемых к шинам FireWire (IEEE 1394), USB (включая описание URB), SCSI, IDE и PCMCIA | |
Gloss.chm | Терминологический глоссарий | |
Install.chm | Освещены вопросы инсталляции драйвера и весьма подробно разобрана организация inf-файлов | |
Graphics.h |
Вопросы проектирования видео драйверов и драйверов для принтеров |
Вообще говоря, каждому, кто приступает к использованию пакета DDK, рекомендуется ознакомиться с содержанием всех файлов документации. Причина проста — некоторая информация встречается в не вполне ожидаемых местах, например, правила присвоения PnP идентификаторов устройствам шины PCI и другим устройствам, можно обнаружить в конце файла Appendix.chm.
Следует обратить внимание так же и на то, что пакет DDK поставляется с примерами драйверов и их исходными текстами. Некоторые из представленных таким образом примеров являются реально работающими в системе драйверами, как, скажем, драйвер параллельного порта parport.sys. Как своего рода документацию можно рассматривать и представленные в примерах весьма подробные комментарии.
Дополнительные описатели типов
В документации и примерах DDK можно видеть относительно новые способы задания типов данных, которые внедряются там примерно с тем же энтузиазмом и размахом, как ранее это происходило с так называемой венгерской нотацией.
Помимо типов данных, представляющих сложные данные (структуры и объединения, типа DRIVER_OBJECT), в драйверных исходных текстах широко применяются новые обозначения известных стандартных типов данных. Заголовочный файл ntdef.h содержит множество определений, которые придают новый вид старым и хорошо известным описателям типов языка С. Например, описатели беззнаковых целых типов и указателей на переменные такого типа принимают вид:
typedef unsigned char UCHAR; typedef unsigned short USHORT; typedef unsigned long ULONG; ....... typedef UCHAR *PUCHAR; typedef USHORT *PUSHORT; typedef ULONG *PULONG; ....... typedef unsigned __int64 ULONGLONG; ....... typedef ULONGLONG *PULONGLONG;
Для чего это затеяна эта игра? В основном — для унификации стиля, когда в обращение вводятся совершенно новые типы данных, которые не являются базисными для языка С, скажем типа WCHAR (двухбайтный Unicode символ), или типа данных (а на самом деле — объединения) LARGE_INTEGER, наиболее часто используемого при программировании таймерных объектов, например:
typedef union _LARGE_INTEGER { struct { ULONG LowPart; LONG HighPart; }; struct { ULONG LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER;
Другая цель этих дополнений — достичь универсальности исходных текстов при переходе с 32-разрядных платформ на 64-разрядные.
Дополнительный тест на скорость переноса
В приведенном выше консольном приложении, предназначенном для элементарного тестирования первого варианта драйвера, обслуживающего заглушку CheckIt, имеется закомментированный фрагмент, который (если его включить в программу) позволяет многократно повторять элементарный тест по записи и считыванию данных из параллельного порта.
for(i=0; i
Воспользовавшись системным апплетом "Производительность", и включив просмотр графика "Число прерываний в секунду", увидим, что при запуске тестирующего приложения резко возрастает нагрузка на систему, рис. 11.3.
Рис. 11.3 График числа прерываний в секунду при интенсивном использовании драйвера, обслуживающего заглушку CheckIt |
Если обратится к Диспетчеру задач Windows и посмотреть на график разделения времени процессора, расходуемого на обслуживание кода режима ядра и пользовательского режима, то увидим, что доля кода ядра в процентном отношении непривычно велика, см. рис. 11.4. Можно сразу же предположить, что все пользовательские процессы будут притормаживаться. И в самом деле, пока работает тестовое приложение в указанном интенсивном режиме "жизнь" в системе замирает — очень долго открываются и перемещаются окошки (время отклика на процессоре с тактовой частотой ЗГГц до 10 секунд), слегка "зависает" курсор мышки и т.п.
Рис. 11.4 Окно Диспетчера задач в момент запуска тестирующего приложения в интенсивном режиме (работа в режиме ядра занимает более 50% времени процессора) |
Проанализировав приведенную информацию, несложно сделать заключение, что описанный драйвер, вполне пригодный для изучения прерываний — поскольку показывает прерывания от момента их зарождения до момента вызова программного кода, отвечающего за их обработку, оказывается не столь хорошим для реальной жизни. "Расход" прерываний в отношении 1 прерывание на перенос 1/2 байта данных является непозволительной роскошью, которая приводит к существенной деградации системы. Разумеется, лучшим вариантом использования прерываний является их генерация устройством при готовности к переносу как можно большей порции данных. Это является общей закономерностью использования прерываний.
Доступ через адресацию в памяти
Далеко не все создатели процессоров находили целесообразным организацию "портового" доступа к регистрам устройств через адресное пространство ввода/вывода. В альтернативном подходе доступ к регистрам устройств осуществлялся путем обращения к определенным адресам в пространстве памяти (memory address space). В пример можно привести архитектуру PDP-11 Unibus, где вовсе не было портов ввода/вывода и инструкций процессора для работы с ними: все регистры всех устройств имели свое место в общем пространстве адресации памяти и реагировали на обычную инструкцию доступа к памяти. В некоторых случаях (например, для видеоадаптеров в архитектуре Intel x86) допускаются оба способа доступа сразу: и через пространство ввода/вывода, и через адресное пространство
Таблица 5.3. Макроопределения HAL для доступа к регистрам устройств через адресацию в памяти.
Поля | Описание | |
READ_REGISTER_XXX | Чтение одного значения из регистра ввода/вывода | |
WRITE_REGISTER_XXX | Запись одного значения в регистр ввода/вывода | |
READ_REGISTER_BUFFER_XXX | Чтение массива значений из последовательного набора регистров ввода/вывода | |
WRITE_REGISTER_BUFFER_XXX | Запись массива значений в набор из следующих друг за другом регистров ввода/вывода |
Как и в предыдущем случае, определены макросы HAL для доступа к таким memory mapped (то есть с доступом посредством адресации в памяти) регистрам, что описывается в таблице 5.3 (XXX принимает значения UCHAR, USHORT или ULONG). Так как эти макроопределения по содержанию отличаются от HAL макроопределений для операций с портами ввода/вывода, драйверный код должен быть разработан так, чтобы компиляция для разных платформ (с разными методами доступа к регистрам) проходила корректно. Хорошим приемом является составление таких макроопределений условной компиляции, которые указывали бы на один из нужных HAL макросов в зависимости от ключей компиляции.
Доступ к областям памяти пользовательских приложений
В момент, когда программный код приложений, выполняющийся в пользовательском режиме, делает запрос на ввод/вывод, то он передает адрес буферной области, которая содержит данные (или предназначена для их получения) и размещена в пользовательском адресном пространстве. Поскольку адреса пользовательского режима указывают в нижнюю часть (ниже 2 Гбайт для обычных версий ОС) страничных таблиц, драйвер должен рассматривать возможность того, что страничные таблицы могут (и будут) меняться до окончания обработки запроса. Это может произойти в случае, если рабочая ветвь кода драйвера выполняется в контексте прерывания или в контексте потока, выполняющегося в режиме ядра. Нижняя часть страничных таблиц меняется при каждом переключении процесса (process switch). Таким образом, далеко не весь программный код драйвера может рассчитывать на то, что все адреса пользовательского режима будут верны и применимы в течение всего времени обработки запроса.
Хуже того, пользовательский буфер может быть и вовсе перемещен из оперативной памяти на жесткий диск в системный swap-файл. Области памяти, выделенные пользовательским приложениям, всегда рассматриваются операционной системой в качестве кандидатов для сброса на диск, если системе не хватает ресурсов оперативной памяти для других процессов.
Общим правилом является то, что виртуальная память пользовательских приложений является страничной, а к такой памяти нельзя обращаться из программного кода, выполнение которого происходит на IRQL уровне DISPATCH_LEVEL или выше. Этому постулату следовать необходимо по той простой причине, что работа по возвращению страницы памяти из swap-файла (в случае, если она туда попала) в оперативную память выполняется службами, которым для завершения этой операции могут потребоваться устройства, работающие при DIRQL более низком, чем IRQL кода-инициатора обращения к этой "неудачной" странице виртуальной памяти.
Доступ к регистрам
Шина PCI позволяет использовать 32 разрядные адреса, но системное пространство ввода/вывода все еще ограничено адресами до 64K (16 разрядов). Соответственно, там должны пребывать и все регистры PCI. Более того, в системах, содержащих шины EISA или ISA, разработчикам следует воздерживаться и от использования адресов, которые могли бы использовать устаревшие устройства, подключаемые к этим шинам, по причинам, указанным выше.
Наряду с адресацией пространства ввода вывода и адресацией памяти, архитектура PCI определяет диапазон адресов во внутренней памяти устройства, называемых конфигурационным пространством (configuration space). При обсуждении автоматического конфигурирования PCI будет объяснено, какую структуру это конфигурационное пространство имеет и как используется в процессе идентификации устройств (плат) на шине PCI.
Спецификация IEEE 1394 удовлетворяет стандарту IEEE 1212 архитектуры Control and Status Register (CSR). Стандарт CSR определяет фиксированную 64-разрядную схему адресации, включающую 10-разрядный шинный номер и 6-разрядный идентификатор узла (Node ID) c остающимся 48-разрядным полем для использования по усмотрению устройства.
Как в случае со спецификацией USB, регистры предназначены для управления и хранения информации о состоянии устройства, а также для передачи небольших (до 64 байт) пакетов данных. Синхронная передача используется в тех случаях, когда необходимо выдержать указанную пропускную способность шины (например, при работе с видеокамерой), асинхронная передача данных используется при гарантированном поступлении данных (например, при считывании данных с жесткого диска.)
Доступ к регистрам устройств (со стороны хост-контроллера и системных драйверов) осуществляется с применением специальных USB команд, которые применяются для конфигурирования, управления энергопотреблением и переноса небольших объемов данных. Прохождение команд (предназначенных для конкретного устройства) в сильной степени определяется устройством, так что число и назначение команд определяется каждым устройством индивидуально путем заявления (передачи в хост-контроллер) конфигурационных данных по соответствующим запросам в начале работы.
Механизм передачи данных является асинхронным и блочным. В каждом блоке передаваемых данных, называемом USB-фреймом, может передаваться до 1280 байт данных. Каждый фрейм занимает фиксированный временной интервал 1 мсек.
Оперирование командами и блоками данных реализуется при помощи такой логической абстракции, как USB pipe-канал. Pipe-канал является каналом связи между хост-контроллером и конечной точкой (endpoint — еще одна логическая абстракция спецификации USB) внешнего устройства, которой обычно является буфер некоторой протяженности внутри внешнего устройства. Открытый pipe-канал можно сравнить с открытым файлом. Кстати сказать, с ним ассоциирован дескриптор открытого канала.
Для передачи команд (и данных, входящих в состав команд) используется pipe-канал по умолчанию (default pipe), в то время как для передачи данных может быть использовано любое количество потоковых pipe-каналов (stream pipes) и pipe-каналов сообщений (message pipes).
Концепция "конечных точек" (endpoints) USB сильно напоминает концепцию функций устройств PCI, поскольку после конфигурирования устройства за конечными точками внутри него закрепляется и определенная функция (0 — только функции управления и контроля, остальные — по замыслу разработчика, но — на все время работы в системе).
Стандарт PC Card определяет 26 разрядный адресный диапазон (64 Мбайт). С другой стороны, адресация происходит по схеме, сходной со схемой, используемой в ISA архитектуре. В архитектуре CardBus схема использования пространства, адресуемого 32 разрядным адресом, аналогична схеме, примененной в архитектуре PCI.
Доступ к регистрам устройств
Когда функционирование устройства стало почти понятным, останется небольшая проблема: как программно получить доступ к регистрам устройства.
Как правило, регистры следуют друг за другом в своем адресном пространстве. Следовательно, для начала, необходим адрес первого из них. К сожалению, значение термина 'адрес' сильно варьируется при использовании его относительно виртуальных адресных пространств на различных платформах.
Вариант первый. Используем специфическую для данного процессора инструкцию ввода/вывода в порт, а значит, нужен адрес порта ввода/вывода.
Вариант второй. Особые области в памяти совмещены с адресами доступа к устройству. Запись по адресу в памяти означает перенос данных в устройство. Самый распространенный прием для доступа к видеопамяти — доступ по специальным адресам с использованием стандартных обращений к памяти.
Пример старой DOS программы (назовем ee Abc), выполняющей вывод алфавита на экран в текстовом режиме, построен как раз на том, что видимая область первой текстовой страницы DOS экрана "совмещалась" с оперативной памятью и начиналась с адреса B800:0000 (что в переводе на современный... где-то... 0xB8000).
#include <dos.h> int main(void) { int i; unsigned int far *screen; // <- адрес начала видимой 1-й страницы // Собираем адрес из сегмента (0xB800) и смещения: screen = (unsigned int *) MK_FP(0xB800, 0); for (i = 0; i < 26; i++) screen[i] = 0x0F00 + ('a' + i); // <- Вывод одного символа return 0; }
В результате работы этого кода (его следует собрать DOS компилятором) в верхней строке экрана появится строка букв о 'а' до 'z'. (Здесь 0x0F — байт означающий, что символы будут белыми на черном фоне.)
Если говорить в терминах ассемблера, то инструкции для доступа к пространству памяти — это MOV (load/store
для других типов ассемблера), а для доступа к пространству ввода/вывода используются инструкции типа IN или OUT.
Доводы "за"
Драйвер, реализованный по многослойной методике, имеет два преимущества. Использование слоев позволяет отделить вопросы использования высокоуровневых протоколов от вопросов, связанных с управлением собственно оборудованием. Это позволяет осуществлять поддержку аппаратуры от разных производителей без переписывания больших объемов кода. Многослойная архитектура позволяет повысить гибкость системы за счет использования при одном драйвере, реализующем протокол, сразу нескольких драйверов аппаратуры, подключаемых непосредственно во время работы. Этот прием реализован в сетевых драйверах Windows NT 5.
В случаях, когда к одному и тому же контроллеру (как это происходит в случае со SCSI адаптерами) подключаются различные типы периферийных устройств, многослойная методология позволяет отделить управление периферией от управления контроллером. Для того чтобы решить подобную проблему, необходимо написать лишь один драйвер для контроллера (порт-драйвер, port driver) и после этого реализовать классовые драйверы (class driver) для каждого типа подключаемых периферийных устройств. Две основные выгоды, получаемые здесь, состоят в том, что классовые драйвера меньше и проще, и при этом вполне вероятна ситуация, когда порт-драйвер и классовые драйвера поступают от разных поставщиков. В таком случае сборщик компьютера не обременен подбором аппаратных компонентов, имеющих общий драйвер.
Создание аппаратных шин USB и IEEE 1394 базируется на многослойном подходе к драйверам именно по перечисленным выше причинам.
Внесение драйверных слоев предоставляет простой способ добавления и удаления дополнительных возможностей устройств без установки нескольких вариантов программного обеспечения для одного и того же устройства.
Использование многослойных драйверов позволяется также скрыть от конечного пользователя некоторые аппаратные ограничения используемого устройства или ввести некоторые свойства, не поддерживаемые собственно устройством. Haпример, если элемент аппаратуры поддерживает работу с данными только определенного размера, то можно поставить над его драйвером другой, который будет дробить поток данных на более мелкие порции, скрывая от пользователя ограниченность возможностей этого устройства.
DPC процедуры как средство синхронизации
Процедуры DPC, точнее, объекты, с ними ассоциированные, могут размещаться в очереди DPC объектов только в единичном экземпляре. Если DPC объект находится в очереди, то следующему запросу на размещении там DPC объекта (соответственно, и отложенного вызова DPC процедуры) будет отказано.
Таким образом, в многопроцессорных архитектурах DPC процедура может безопасно обращаться к данным, если доступ к ним производится только из этой DPC процедуры драйвера (ассоциированной с данным DPC объектом), не опасаясь вмешательства кода драйвера, возможно, работающего на другом процессоре.
Правда, такого типа синхронизация достаточно вычурна, поскольку произвольно запланировать вызов DPC процедуры можно при помощи вызова KeInsertQueueDpc, а сделать его можно только из кода, работающего на уровне не ниже DISPATCH_LEVEL. Процедура DPC будет вызвана, когда приоритет снизится до уровня ниже DISPATCH_LEVEL.
Следует также помнить, что к моменту вызова KeInsertQueueDpc
должен существовать инициализированный DPC объект (см. описание вызова KeInitializeDpc, таблица 10.23), соотнесенный с интересующей DPC процедурой.
DpcForISR, Deferred Procedure Call for Interrupt Service Routine
Процедура отложенного вызова для обслуживания прерываний. Функция, которую драйвер регистрирует в момент работы ISR-процедуры для выполнения основной работы при получения сигнала прерывания от устройства. Сама процедура ISR работает при очень высоком приоритете, так что задержка внутри нее может привести к серьезной деградации системы, поэтому длительные операции следует "скидывать" процедуре DpcForISR (далее в тексте практически всегда будет использоваться именно это имя для данной функции, однако автор драйвера может присвоить ей любое название).
Драйвер отказывается работать?
Если тестирование сигнализирует о наличии ошибок, то более сложной проблемой является локализация их источника. Разумеется, драйверы могут отказываться работать многими интересными способами. Вряд ли возможно или следовало бы приводить обширный список причин сбоев, но перечислить некоторые общие типы драйверных патологий все-таки имеет смысл.