Работа с Системным Реестром через вызовы ZwXxx
Все-таки наиболее богатым и основательным является набор функций для работы с Системным Реестром ZwXxx.
ZwCreateKey открывает доступ к существующему подразделу Системного Реестра или создает новый. Возвращает дескриптор открытого объекта.
ZwOpenKey открывает доступ к существующему разделу Системного Реестра и возвращает дескриптор открытого объекта (см.пример ниже).
ZwQueryKey возвращает информацию о подразделе — его класс, число и размер вложенных подразделов. Инициатор вызова должен предоставить достаточного размера буфер для принимаемой информации, иначе вызов будет завершен с кодом ошибки STATUS_BUFFER_TOO_SMALL или STATUS_BUFFER_OVERFLOW.
ZwEnumerateKey возвращает информацию о вложенных подразделах предварительно открытого подраздела Системного Реестра.
ZwEnumerateValueKey возвращает информацию о параметрах и их значениях для предварительно открытого подраздела Системного Реестра.
ZwQueryValueKey возвращает информацию о значении параметра, присутствующего в данном предварительно открытом разделе Системного Реестра. Характер возвращаемой информации определяет третий аргумент вызова, который принимает одно из значений KeyValueBasicInformation, KeyValueFullInformation или KeyValuePartialInformation. Пример применения данной функции приводится ниже.
ZwSetValueKey создает или изменяет значение параметра в открытом подразделе Системного Реестра. Для возможности применения этой функции, дескриптор подраздела при открытии должен быть получен с применением маски DesiredAccess, содержащей флаг KEY_SET_VALUE
ZwFlushKey форсирует фиксацию изменений, сделанных в открытом подразделе вызовами ZwCreateKey или ZwSetValueKey, на диске.
ZwDeleteKey удаляет открытый подраздел из Системного Реестра.
ZwClose закрывает дескриптор открытого ранее подраздела Системного Реестра, фиксирует произведенные изменения на жестком диске.
Ниже приводится пример программного кода, выполняющий операции по получении значения параметра ErrorControl из раздела Системного Реестра, который был создан для описания драйвера и поступил в процедуру DriverEntry в аргументе RegistryPath.
Для случая драйвера Example.sys этот подраздел называется HKLM\SYSTEM\ControlSet001\Services\Example, а собственно строка RegistryPath хранит значение:
L"\\REGISTRY\\MACHINE\\SYSTEM\\ControlSet001\\Services\\Example"
Вся работа с Системным Реестром вынесена в отдельную функцию GetRegValueDword.
// Сначала объявляем прототип GetRegValueDword: int GetRegValueDword(PCWSTR RegPath,PCWSTR ValueName,PULONG pValue); extern "C" NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath ) { . . .
if(!GetRegValueDword( RegistryPath->Buffer, L"ErrorControl", &ulValue)) { #if DBG DbgPrint("Error in GetRegValueDword."); #endif } else { #if DBG DbgPrint("RegistryPath\\ErrorControl = %x.", ulValue); #endif }
int GetRegValueDword(PCWSTR RegPath, PCWSTR ValueName, PULONG pValue) { int ReturnValue = 0; NTSTATUS status; OBJECT_ATTRIBUTES ObjectAttributes; HANDLE KeyHandle; KEY_VALUE_PARTIAL_INFORMATION *pInformation; ULONG uInformationSize; UNICODE_STRING UnicodeRegPath; UNICODE_STRING UnicodeValueName; // Инициализация UNICODE_STRING полученными // не-счетными строками двухбайтных символов RtlInitUnicodeString(&UnicodeRegPath, RegPath); RtlInitUnicodeString(&UnicodeValueName, ValueName);
// Описание атрибутов, в частности, полного имени подраздела InitializeObjectAttributes(&ObjectAttributes, &UnicodeRegPath, 0, // Flags NULL, // Root directory NULL); // Security descriptor
status = ZwOpenKey( &KeyHandle, KEY_QUERY_VALUE, &ObjectAttributes );
if( !NT_SUCCESS(status) ) // Если не получен доступ к подразделу: { #if DBG DbgPrint("=Example= Can not open reg path %ws .", UnicodeRegPath.Buffer); DbgPrint("=Example= Status = %x.",status); if (Status==STATUS_INVALID_HANDLE) DbgPrint("=Example= STATUS_INVALID_HANDLE."); if(Status==STATUS_ACCESS_DENIED) DbgPrint ("=Example= STATUS_ACCESS_DENIED."); #endif return 0; }
// Вычисляем размер буфера для получения информации о параметре: uInformationSize = sizeof(KEY_VALUE_PARTIAL_INFORMATION) + sizeof(ULONG);
// Выделение области в страничной памяти. // Область будет помечена тегом 'EXaM' pInformation = (KEY_VALUE_PARTIAL_INFORMATION*) ExAllocatePoolWithTag(PagedPool, uInformationSize,'EXaM');
if( pInformation == NULL ) // Не выделена память { ZwClose(KeyHandle); return 0; }
// Получить описание типа KeyValuePartialInformation: // status = ZwQueryValueKey(KeyHandle, &UnicodeValueName, KeyValuePartialInformation, pInformation, uInformationSize, &uInformationSize );
if( !NT_SUCCESS(status) ) { #if DBG DbgPrint("=Example= ZwQueryValueKey not successful."); #endif } else { if( pInformation->Type == REG_DWORD && pInformation->DataLength == sizeof(ULONG) ) { RtlCopyMemory(pValue, pInformation->Data, sizeof(ULONG)); ReturnValue = 1; } }
// Завершение работы ExFreePool(Information); ZwClose(KeyHandle);
return ReturnValue; }
Практически все функции для работы с Системным Реестром должны вызываться из кода, работающего на уровне IRQL равном PASSIVE_LEVEL |
Распознавание оборудования
NTDETECT.COM запускается после надписи "Выберите операционную систему для загрузки", составляет список установленного на данный момент оборудования, и возвращает этот список в NTLDR для последующего включения его в раздел системного реестра HKLM\HARDWARE. NTDETECT.COM выполняет определение характеристик устройств:
Тип шины.
Последовательные порты.
Математический сопроцессор.
Гибкие диски.
Клавиатура и указывающие устройства.
Параллельные порты.
Адаптеры SCSI.
Видео адаптеры.
Следует отметить, что устройства, которые подключаются к шинам и должны быть обнаружены шинными драйверами (например, внешние устройства на шинах USB), здесь не обнаруживаются — по той простой причине, что шинные драйверы, которые должны выполнить эту работу, на данный момент еще не загружены.
Расширения базовой операционной системы
Исполнительные компоненты Windows 2000/XP/2003 определяют и представляют основные сервисы операционной системы. Однако эти сервисы никогда не предоставляются программам пользовательского режима непосредственно. Вместо этого разработчики из Microsoft определили несколько интерфейсов прикладного программирования (Application Programming Interfaces), при помощи которых код пользовательского режима может обращаться к абстракциям системных служб.
Эти интерфейсы формируют различные среды (environmental subsystems), в которых и обитают прикладные программы. В настоящее время в Windows NT 5 представлены:
Win32 подсистема, являющаяся собственным (native-mode) API для 32-разрядных версий Windows. Bce остальные среды (environmental subsystems) используют эту подсистему для выполнения своей работы. Все новые приложения 32-разрядных Windows 2000/XP/2003 (а также и все перенесенные) полагаются на Win32 как на среду своего функционирования. Из-за важности (и достаточно интересной реализации) эта подсистема будет рассмотрена далее более детально. Следует, однако, отметить, что в 64-разрядной версии Windows (версии XP/Server 2003) она сама становится клиентом WOW64 (см. ниже).
Virtual DOS Machine (VDM, виртуальная DOS машина) подсистема обеспечивает 16-разрядную MS DOS операционную среду для старых DOS приложений. Несмотря на уверения в совместимости, множество существующих DOS программ в этой среде не работают надлежащим образом. Происходит это по той причине, что Microsoft, проповедуя консервативный подход, предоставляет эмуляцию аппаратуры вместо возможности непосредственного обращения к ней. В результате, прямой доступ к аппаратуре приводит к ограничению со стороны операционной системы и, зачастую, отказу данного DOS приложения работать.
Подсистема 'Windows on Windows' (WOW) поддерживает операционную среду для возможности работы старых 16-битных приложений Windows (например, Windows 3.x). В 64-разрядных клонах Windows XP/Server 2003 подсистема WOW 64 служит для запуска созданных ранее 32-разрядных приложений, перенесенных на новые аппаратные платформы.
Подсистема POSIX обеспечивает выполнение Unix-приложений, которые удовлетворяют стандарту POSIX 1003.1. К сожалению, большинство перенесенных Unix- подобных систем приложений не работает должным образом в этой подсистеме. В данном случае большинство Unix-приложений переносятся под Windows путем переписывания под Win32 подсистему или они изначально создаются с использованием специальных программных пакетов, типа MainWin, Motif и OpenMotif.
Подсистема OS/2 создает среду выполнения для 16-разрядных программ операционной системы OS/2 — по крайней мере, тех из них, которые не используют в своей работе сервисов такого компонента OS/2, как Presentation Manager (PM). На эту подсистему можно рассчитывать только в версии Windows для платформы Intel (x86).
Каждое приложение однозначно связано с одной средой выполнения. Приложения не могут осуществлять API вызовы к другим исполнительным средам. Кроме того, подсистема Win32 является основной в 32-разрядных версиях Windows NT 5.x. Другие подсистемы эмулируют соответствующие свойства реализуемых сред через средства и методы Win32. Соответственно, параметры выполнения программ в этих средах деградируют и существенно уступают аналогичным программам для Win32.
Расширяемость
На рисунке 4.1 обозначена и еще одна важная особенность представленной архитектуры — ядро отделено от слоя, который носит название "исполнительные компоненты" (Executive).
В данном случае, ядро несет ответственность за планировку активности программных потоков (threads). Поток является всего лишь "независимой тропинкой" в выполнении программного кода. Чтобы сохранить независимость от деятельности других потоков, для каждого из них необходимо сохранять уникальный потоковый контекст (thread context). Потоковый контекст состоит из состояния регистров процессора (включая также изолированный стек и счетчик инструкций, Program Counter), сохраненного ID (идентификатора потока, так называемого Thread ID или TID), значения приоритета, распределения памяти, связанной с потоком (Thread Local Storage), и другой информации, имеющей отношение к данному потоку.
Обязанностью планировщика потоков является определение, какой поток должен выполняться в данный момент. В среде с единственным процессором, в каждый момент времени только один поток получает в свое распоряжение процессор. В многопроцессорной конфигурации разные потоки могут выполняться на разных процессорах, реализуя настоящую параллельность выполнения кода. Планировщик в большинстве случаев выделяет потоку процессор на фиксированный временной интервал, известный под названием thread time quantum (потоковый временной квант). Предоставление процессора происходит, главным образом, на основе величины приоритета потока.
Так как основной задачей ядра является управление потоками, работа по управлению памятью, вопросами доступа (security) и действиями по вводу/выводу возлагается на другие компоненты операционной системы. Эти компоненты известны под собирательным названием 'Executive', Исполнительные Компоненты. Они сконструированы как модульное программное обеспечение (хотя, Диспетчер ввода/вывода сам является существенным исключением из этого правила).
Идея поддержания ядра как "маленького и чистого", при сохранении модульности исполнительных компонентов, обеспечивает основу заявления Microsoft o сохранении курса NT на расширяемость. По крайней мере, следует признать, что эта операционная система выдержала более десяти лет переработок и регулировок, значительно улучшив свои показатели.
Разделение времени и данных с ISR процедурой
Вместо вызовов InterlockedXxx, примененных выше для синхронизации доступа к счетчику вызовов MyIoTimerRoutine, можно применить следующий метод, который пригоден всегда, когда нужно гарантировать, что в некую работу над некими данными не вмешается неожиданно процедура обработки прерываний, работающая, как правило, с максимальным для конкретного драйвера приоритетом.
Для выполнения такой работы (например, модификации совместно используемых данных из низкоприоритетной процедуры) создается обособленная функция IsrRoutineConcurrent по прототипу, описанному в таблице 10.13.
Таблица 10.13. Прототип функции IsrRoutineConcurrent
BOOLEAN IsrRoutineConcurrent | IRQL == см. ниже | |
Параметры | Манипуляции на уровне IRQL прерывания | |
IN PVOID pContext | Контекстный указатель | |
Возвращаемое значение | TRUE — в случае успешного завершения (с точки зрения разработчика драйвера) или FALSE |
При необходимости выполнить некоторую работу, которая не может быть прервана функцией обработки прерывания, следует выполнить вызов KeSynchronizeExecution, см. таблицу 10.14.
Вызов KeSynchronizeExecution повышает уровень IRQL до значения SynchronizeIrql, указанного при создании объекта прерывания pInterruptObj системным вызовом IoConnectInterrupt, см. таблицу 8.10, в результате чего с данным объектом прерывания оказалась связана ISR процедура драйвера. Кроме того, данный вызов получает доступ к объекту спин-блокировки, связанному с данным объектом прерывания. В результате доступ к данным по контекстному указателю pContext становится безопасным в том смысле, что другие низкоприоритетные процедуры драйвера просто не могут работать в это время, так же, как не может стартовать и процедура обработки прерывания (если, разумеется, значения Irql и SynchronizeIrql равны, таблица 8.10). В том случае, если Irql превышает SynchronizeIrql, то доступ по указателю pContext из функции IsrRoutineConcurrent остается безопасным по причине владения упомянутым объектом спин-блокировки.
Таблица 10.14. Прототип вызова KeSynchronizeExecution
Развитие спецификации Plug & Play
Два десятилетия бурного развития вычислительной техники, в течение которого доступ к компьютерам стал действительно массовым, завершились вполне закономерно. Функционально насыщенная аппаратура выполняет не только работу, для которой она приобретается (управление механизмами, ведение финансовой отчетности, проектирование, игры и т.п.), но и в высокой степени самостоятельно решает задачи второго плана — собственное конфигурирование и настройку.
В начале девяностых годов пользователь персонального компьютера должен был уметь настраивать свой ПК, экономно при этом расходуя запас "незанятых" прерываний, переставляя перемычки и меняя положение DIP-переключателей на дополнительный картах в поисках оптимального быстродействия. Предварительная подготовка касалась не только необходимого знания портов ввода/вывода, настроек режима прямого доступа к памяти. Необходимо было знать, насколько хорошо сочетаются программы (которые предполагается установить) как с имеющимся "железом", так и между собой. И так далее, и так далее, включая "разгон" процессоров и преодоление проблем перевода ОС и принтеров на родной язык...
Разумеется, о таких "пустяках", как возможность подсоединения новых устройств без выключения компьютера, не было речи вовсе! Максимум сервиса предлагали игровые программы, которые могли предложить "поиграть" настройками, спрашивая в конце каждой итерации "Слышите ли Вы звук в колонках?"
Пик беспорядка в вопросах конфигурирования устройств, составляющих компьютер, пришелся на то время, когда массовый пользователь вместе со своими любимыми программами вырос из рамок возможностей шины ISA. Разумеется, проблемы конфигурирования не давали покоя специалистам и раньше (скажем, со времен Unibus периферии для PDP-11), но только в данной временной точке распространенность вычислительной техники сделали преодоление этой проблемы делом почти что первостепенным.
Решение пришло в виде разработки спецификации Plug and Play, согласно которой устройства должны выдерживать определенные механические и электрические нормы.
Основное же требование Plug and Play состоит в том, что устройства должны уметь предоставлять идентификационную информацию о себе в формате, определенном для данного типа (PCI, USB, FireWire, CardBus) подключения.
С выходом Windows 95 (и появлением некоторых сдвигов в подходах к разработке аппаратной части) усилия были сконцентрированы на автоматизации конфигурирования системы при добавлении и удалении устройств. Эти попытки усилили тенденции перехода пользователей на Windows 95, что в свою очередь ускорило миграцию на 32-разрядные операционные системы Microsoft, в частности на Windows NT. Наконец, с выпуском Windows 2000 Microsoft реализовала законченную архитектуру Plug and Play для подсистем ввода/вывода.
В настоящее время подключение устройств по шинам USB, CardBus (модифицированная PCMCIA) и FireWire (IEEE-1394) при работающем основном компьютере является безопасным (и даже штатным) режимом работы. Драйверная архитектура Windows 2000/XP/2003 полностью поддерживает эти события "появления" в системе новых устройств, позволяя конфигурировать и делать их доступными для использования без выключения питания компьютера и перезагрузки операционной системы.
Главным плюсом использования методологии Plug and Play является обеспечение автоматической поддержки инсталляции и удаления системных устройств. Чтобы добиться этого, необходимо выполнить несколько условий.
Устройства должны быть ориентированы на выполнение программного конфигурирования. Должна существовать возможность установки портов ввода/вывода, задействованных прерываний и ресурсов DMA (параметров прямого доступа к памяти) из управляющего программного обеспечения, исключая механическое конфигурирование при помощи переставляемых перемычек и DIP-переключателей на платах.
Система должна обеспечивать надежное автоматическое обнаружение нового устройства или факт удаления существующего. Устройство и шина, к которой устройство подключено (было подключено), должны информировать управляющее программное обеспечение о том, что аппаратная конфигурация претерпела частичные изменения.
Необходимые драйверы для новых устройств должны загружаться автоматически, по мере обнаружения этих устройств операционной системой. (Вмешательство пользователя все-таки требуется, но только лишь для установки драйверов для никогда ранее не присутствовавших в системе нестандартных устройств.)
В тех случаях, когда устройство и его интерфейсная шина позволяют, операционная система должна поддерживать и так называемое "горячее" (при включенном питании) присоединение аппаратуры. То есть должна быть возможность подключения/отключения устройства непосредственно в "живую" систему без создания для нее стрессовых ситуаций.
Registry
Системный Реестр. База данных разнообразных настроечных и информационных параметров (вплоть до ключа, использованного при инсталляции данного экземпляра Windows), как для приложений пользовательского режима, так и конфигурационных параметров для всей системы. Файлы, составляющие эту базу, в 32-разрядных версиях операционной системы хранятся в директории %systemroot%\System32\Config\ и защищены системой от изменений. Несмотря на то, что программисты предпочитают обходить стороной это "страшное" место, тем не менее, запись конфигурационных параметров в Системный Реестр является нормальной и рядовой практикой. Такая программа, как Internet Explorer при простом движении курсора мышки по верхней части окна (где размещается меню и кнопки) делает до трех десятков операций над Реестром.
Информация об обнаруженном PnP оборудовании и установленных драйверах также размещается в Системном Реестре.
Составляющие разделов будем называть подразделами. В каждый подраздел могут быть вложены другие подразделы, но он может иметь и собственные данные, которые имеют имя и значение. Имена данных в подразделе будем далее называть параметрами (именами параметров), а собственно данные - значениями (значениями параметров.).
Для редактирования и просмотра содержимого Системного Реестра предназначена программа редактирования Реестра, которая в Windows 2000 запускается командой regedit32, а в Windows XP и Windows Server 2003 командой regedit.
Весь Реестр разделен на разделы. Самые крупные разделы носят названия hive (улей). Различают разделы HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, HKEY_CURRENT_USER, HKEY_USERS. (Здесь НКЕY, очевидно, образовано от HiveKEY.)
Наиболее употребительным для разработчика драйверов является раздел HKEY_LOCAL_MACHINE, который далее будет часто упоминаться в сокращенной форме как HKLM. Он содержит общую информацию об аппаратном обеспечении и операционной системе (в том числе — установленных службах и драйверах). Инсталляция не-WDM драйвера может быть сведена к созданию подраздела в HKLM\System\CurrentControlSet\Services
с занесением туда трех-четырех параметров (и их значений) с последующей перезагрузкой, после чего драйвер появляется в системе.
Работа с Системным Реестром через системные функции в Windows NT требует использования кодировки Unicode.
Регистры управления
Должна быть известна схема адресации, а также размер регистров устройства. Следует полностью проанализировать назначение каждого регистра управления, каждого регистра состояния и каждого регистра данных, так же как и возможные варианты их содержимого. Необходимо рассмотреть варианты возможного необычного поведения, например:
Отдельные регистры могут быть регистрами только для чтения или только для записи.
Один и тот же регистр может представлять одни функции при операциях чтения и совершенно иные при операциях записи.
Регистры данных или состояния могут содержать неверные данные до истечения некоторого временного интервала после поступления команды-запроса.
Доступ к регистру (регистрам) может проходить в специфической последовательности.
Регистры устройств
Драйверы взаимодействуют с подключаемыми устройствами путем чтения из регистров или записи в их внутренние регистры. Каждый внутренний регистр устройства обычно реализует одну из функций, перечисленных ниже:
Регистр состояния. Обычно считывается драйвером, когда тому необходимо получить информацию о текущем состоянии устройства.
Регистр команд. Биты этого регистра управляют устройством некоторым образом, например, начиная или прекращая передачу данных. Драйвер обычно производит запись в такие регистры.
Регистры данных. Обычно такие регистры используются для передачи данных между устройством и драйвером. В выходные (output) peгucmpы, регистры вывода, драйвер производит запись, в то время как информация входных (input) регистров, регистров ввода, считывается драйвером.
Доступ к регистрам устройства достигается в результате выполнения инструкций доступа к портам ввода/вывода (port address) или обращения к определенным адресам в адресном пространстве оперативной памяти (memory-mapped address), что и интерпретируются системой как доступ к аппаратным регистрам.
Простые устройства (такие, как стандартный интерфейс параллельного порта, см. таблицу 5.1 — не путать с регистрами устройств, которые могут подключаться извне к параллельному порту!) имеют небольшое число ассоциированных регистров. В то же время, сложное аппаратное обеспечение (например, графические адаптеры) может иметь значительно больше регистров. Число и назначение регистров определяется разработчиками аппаратного обеспечения и должно быть полно и однозначно описано в документации. Однако зачастую такая однозначность так и остается недостижимой мечтой, а разработчику драйвера приходится определять реальное
назначение нужных битов в устройствах используя случайно добытый тестовый программный пример неизвестного автора или метод собственных проб и собственных ошибок. Более того, часто выясняется, что биты, объявленные в документации как "зарезервированные" (reserved), вовсе не являются тем безобидным предметом, о котором не следует и беспокоиться.
Таблица 5.1. Регистры интерфейса стандартного параллельного порта (SPP)
Смещение | Доступ | Регистр | Описание |
0 | Read/Write | Data (DR) | Байт данных, передаваемый через параллельный порт |
1 | Read only Биты 0-1 Бит 2 Бит 3 Бит 4 Бит 5 Бит 6 Бит 7 |
Status (SR) PIRQ ERROR# SELECT OUT_OF_PAPER ACK# BUSY# |
Текущее состояние порта Зарезервированы 0 — прерывание было запрошено портом (т.е. если сигнал ACK# вызвал прерывание) 0 — произошла ошибка 1 — принтер выбран (включен) 1 — в принтере отсутствует бумага отображает состояния линии Ack# 0 — принтер занят (1 - разрешение на вывод очередного байта) |
2 | Read/Write Бит 0 Бит 1 Бит 2 Бит 3 Бит 4 Биты 5-7 |
Control (CR) STROBE# AUTO_LF INIT# SELECT_IN# ENABLE_INT |
Команды, посылаемые в порт 1 — строб передачи данных в/из порта 1 — автоматическая подача строки 0 — инициализировать принтер 1 — выбрать принтер 1 — разрешает прерывания по спаду сигнала на линии ACK# зарезервированы |
Ресурсы, используемые устройством
Правильно спроектированное устройство должно идентифицировать (проявить) себя и предоставить системе перечень ресурсов, которые оно потребляет. Это перечень, формулируемый в некоторых позициях собственно устройством, а в некоторых — его драйвером, должен включать:
Идентификатор производителя (Manufacturer ID).
Идентификатор типа устройства (Device type ID).
Требования к пространству ввода вывода (I/O space requirements).
Требования по использованию прерываний.
Требования по использованию каналов DMA.
Требования относительно памяти, отведенной устройству.
В случае PnP устройств, идентификаторы производителя и типа устройства являются критерием выбора драйвера при загрузке системы или же при подключении устройства (если оно было подключено после загрузки).
Для обеспечения автоматической конфигурируемости, устройство должно разрешать авторизованному программному обеспечению динамически устанавливать и изменять установки порта ввода/вывода, прерывания и канала DMA, которые будут использованы при работе с данным устройством. Это позволит операционной системе разрешить конфликты ресурсов среди конкурирующих устройств.
Роль драйверных слоев в модели WDM
Драйверная модель WDM построена на организации и манипуляции слоями Объектов Физических устройств (Physical Device Object, PDO) и Объектов Функциональных устройств (Functional Device Object, FDO). Объект PDO создается для каждого физически идентифицируемого элемента аппаратуры, подключенного к шине данных, и подразумевает ответственность за низкоуровневый контроль, достаточно общий для набора функций, реализуемых этим аппаратным элементом. Объект FDO предлагает "олицетворение" каждой логической функции, которую "видит" в устройстве программное обеспечение верхних уровней.
В качестве примера рассмотрим привод жесткого диска и его драйвер. Привод диска может быть представлен объектом PDO, который реализует функции шинного адаптера (присоединяет IDE диск к шине PCI). Как только возникает PDO объект, можно реализовывать объект FDO, который примет на себя выполнение функциональных операций над собственно диском. Обращаясь к FDO, можно будет сделать конкретный функциональный запрос к диску, например, чтение или запись сектора. Однако FDO может выбрать и передачу без модификации конкретного запроса своим партнерам по обслуживанию данного устройства (например, сообщение о снижении напряжения питания).
В действительности, роль PDO объектов быстро усложняется и становится рекурсивной. Например, USB хост-контроллер начинает жизнь как физическое устройство, подключенное к шине PCI. Ho вскоре этот хост-контроллер сам начинает выступать в роли шинного драйвера и, по мере обнаружения устройств, подключенных к USB шине, создает свою коллекцию PDO объектов, каждый из которых контролирует собственный FDO объект.
Эта методология в дальнейшем усложняется еще более, поскольку Функциональным Объектам устройств (FDO) разрешается окружать себя Объектами-Фильтрами (filter device objects, FiDO). Соответственно, каждому FiDO объекту сопоставлен драйвер, выполняющий определенную работу (иначе — зачем их создавать?). Эти фильтрующие объекты верхнего и нижнего уровня могут существовать в любом количестве.
Назначение их в том, чтобы модифицировать или обогатить процесс обработки запросов ввода/вывода возможностью использования всего результирующего стека объектов устройств. Следует отметить, что FDO и FiDO объекты отличаются только в смысловом отношении — FDO объект и его драйвер являются главной персоной, FiDO объекты и их драйверы являются вспомогательными (вплоть до того, что предпочитают не иметь собственных имен).
Для того чтобы сделать различие между FDO объектами, которые представляют аппаратные шины, и FDO объектами, которые аппаратные шины не представляют, в документации DDK используются термины шинные FDO (bus FDO) и не-шинные FDO (nonbus FDO). Первые реализуют обязанности драйвера по перечислению (enumerating) всех устройств, подключенных к шине. Такой шинный FDO объект затем создает новые PDO объекты для каждого из подключенных к шине устройств.
Добавляет проблем тот факт, что существует лишь небольшая смысловая разница между не-шинным FDO и фильтрующим объектом устройства (filter device object). C точки зрения Менеджера PnP, все объекты устройств позиционируют себя в стеке устройств (device stack), a тот факт, что некоторые устройства считают себя более чем просто объектами-фильтрами, кажется ему малозначительным.
Последовательность в стеке устройств показана на рисунке 9.2. Различия между шинными и не-шинными FDO отражены на рисунке 9.3.
Рис. 9.2 Стек устройств |
Во время инсталляции операционной системы, операционная система обнаруживает и составляет список (enumerate) всех шин в Системном Реестре (System Registry). Кроме того, детектируется и регистрируется топология и межсоединения этих шин.
Во время процесса загрузки производится загрузка шинного драйвера для каждой известной системе шины.
Как правило, Microsoft поставляет все шинные драйверы, однако могут быть установлены и специализированные драйвера для патентованных шин данных.
Одна из первоочередных задач шинного драйвера состоит в том, чтобы составить перечень (enumerate) всех устройств, подключенных к шине. Объект PDO создается для каждого обнаруженного устройства.
Для каждого обнаруженного устройства в Системном Реестре определен класс устройств (class of device), который определяет верхний и нижний фильтры, если таковые имеются, так же, как и драйвер для FDO.
В случае если фильтрующий драйвер или FDO драйвер еще не загружены, система выполняет загрузку и вызывает DriverEntry.
Функция AddDevice вызывается для каждого FDO, которая, в свою очередь, вызывает IoCreateDevice и IoAttachDeviceToDeviceStack, обеспечивая построение стека устройств (device stack).
Рис. 9.3 Шинные и не-шинные FDO |
Таблица 9.2. Прототип функции IoAttachDeviceToDeviceStack
PDEVICE_OBJECT IoAttachDeviceToDeviceStack | IRQL == PASSIVE_LEVEL |
Параметры | Выполняет подключение вновь созданного объекта устройства, pNewDevice, к стеку устройств |
IN PDEVICE_OBJECT pNewDevice | Указатель на подключаемый к стеку объект (созданный в данном драйвере) |
IN PDEVICE_OBJECT pOldDevice | Указатель на объект устройства, к которому подключается новое устройство |
Возвращаемое значение |
• Указатель на устройство, бывшее на вершине стека до данного вызова • NULL (в случае ошибки, например, если драйвер целевого устройства еще не загружен) |
Как видно из таблицы 9.2, для подключения данного объекта устройства (по указателю pNewDevice) необходимо владеть указателем на целевой объект устройства (pOldDevice).
Прекрасна ситуация, когда драйвер подключает свой объект устройства к родительскому объекту устройства (шинного драйвера), указатель на который поступает в процедуру AddDevice при вызове через заголовок (pPDO, см. таблицу 9.1). Но что делать, если имеется желание подключить новый объект устройства к объекту устройства другого драйвера, отличающегося от pPDO? (Заметим, что подключение к стеку устройств не есть исключительное право процедуры AddDevice драйверов WDM модели — это могут делать и драйверы "в-стиле-NT", правда, к результатам такой операции следует относиться критически — по причинам, о которых ниже.)
При подключении драйвера к произвольному объекту устройства можно поступить двумя способами. Во-первых, если известно имя нужного устройства, можно получить указатель на искомый объект устройства, воспользовавшись предварительно вызовом IoGetDeviceObjectPointer (см. таблицу 9.3). Полученный указатель на искомый объект устройства (возвращаемый по адресу ppDevObj), можно применить в вызове IoAttachDeviceToDeviceStack, описанном выше.
Таблица 9.3. Прототип функции IoGetDeviceObjectPointer
NTSTATUS IoGetDeviceObjectPointer | IRQL == PASSIVE_LEVEL |
Параметры | Получает указатель на объект устройства по имени устройства |
IN PUNICODE_STRING DeviceName | Имя устройства |
IN ACCESS_MASK Access | Маска доступа: FILE_READ_DATA, FILE_WRITE_DATA или FILE_ALL_ACCESS |
OUT PFILE_OBJECT *ppFileObj | Указатель на файловый объект, которым представлен искомый объект устройства для кода пользовательского режима |
OUT PDEVICE_OBJECT *ppDevObj | Указатель на искомый объект устройства |
Возвращаемое значение |
• STATUS_SUCCESS • STATUS_Xxx — код ошибки |
Вообще говоря, Диспетчер ввода/ вывода автоматически устанавливает необходимое значение StackSize (то есть StackSize нижнего объекта плюс 1) в подключаемых к стеку объектах устройств, если это делается при помощи вызовов IoAttachDeviceToDeviceStack
или IoAttachDevice. Но в том случае, если драйвер пытается обойтись без этих вызовов, то должен установить StackSize своего объекта явным образом.
Таблица 9.4. Прототип функции IoAttachDevice
NTSTATUS IoAttachDevice | IRQL == PASSIVE_LEVEL |
Параметры | Выполняет подключение вновь созданного объекта устройства, pNewDevice |
IN PDEVICE_OBJECT pNewDevice | Указатель на подключаемый объект устройства |
IN PUNICODE_STRING TagDevName | Имя целевого устройства |
OUT PDEVICE_OBJECT *ppTagDevice | Указатель на объект устройства, к которому подключается новое устройство (точнее, указатель на место для указателя) |
Возвращаемое значение | • STATUS_SUCCESS • STATUS_Xxx — код ошибки |
В результате вызовов IoAttachDeviceToDeviceStack или IoAttachDevice
будет найден объект устройства, находящийся на вершине стека над указанным целевым объектом (по имени или по указателю). К нему и будет подключен новый объект устройства. Соответственно, разработчик, подключающий свой объект устройства к устройству в "середине" стека и надеющийся, что таким образом через его драйвер будут "протекать" IRP запросы от вышестоящих драйверов к нижестоящим, глубоко заблуждается. На самом деле, для достижения этой цели необходимо не просто выполнить подключение к нужному объекту устройства, но и сделать это в строго определенный момент загрузки — ранее, чем будет выполнена загрузка вышестоящих драйверов, чьи запросы предполагается перехватывать. Однако рассмотрение данной проблемы выходит за рамки данной книги.
Полученный указатель на объект устройства, к которому произведено подключение, следует сохранить, поскольку он может понадобиться, например, в обработчике запросов IRP_MJ_PNP, см.ниже. Это можно сделать в структуре расширения объекта устройства.
Заключительной задачей функции AddDevice драйверов модели WDM является создание символьного имени-ссылки (symbolic link name), если это необходимо, для вновь созданных и доступных устройств. Для этого используется вызов IoCreateSymbolicLink, применение которого было продемонстрировано ранее в DriverEntry, глава 3.
Роль Системного Реестра
Совсем еще недавно, общепринятым образом поведения для аппаратного обеспечения было оставаться в состоянии молчания до тех пор, пока программное обеспечение неким магическим образом не узнает о его существовании и не примется стимулировать эту аппаратуру. Методы, которыми действовали драйверы или операционная система NT, можно было разделить на три группы:
Драйвер получал список потенциально возможных ресурсов аппаратуры (адреса портов ввода/выводов, используемых каналов прямого доступа к памяти, DMA, и номера прерываний) для каждого устройства, с которым возможно взаимодействие. Путем "подергивания" за каждый потенциально возможный ресурс во время исполнения DriverEntry, драйвер создавал приемлемый объект устройства (вызовом IoCreateDevice).
Драйвер полагался на инсталляционную программу, которая методом проб и ошибок или при активном участии пользователя производила приемлемое описание ресурсов и устройств, которыми мог бы управлять драйвер впоследствии. Список таких устройств и их ресурсов сохранялся в Системном Реестре.
Операционная система NT выполняла как часть загрузочного процесса (см. Приложение Б) испытание стандартных устройств и ресурсов. Например, параллельные порты представлены обычно по адресам 0x378 или 0x278, что и проверяется в процессе загрузки. Все обнаруженное заносилось в Системный Реестр.
Разработчики компьютерных систем признали правомерной потребность в более упорядоченном процессе конфигурирования аппаратного обеспечения. Новые шины и протоколы проектируются так, чтобы автоматически сообщать о появлении или удалении устройств. Все типы шин поддерживают теперь такую форму автоматического определения.
Промежуточным решением для получения автоматически распознаваемых шин и аппаратуры было в ранних версиях NT расширение загрузочного процесса, во время которого в Системный Реестр включается информация об обнаруженном оборудовании. Таким образом, проходящий инициализацию драйвер в ходе работы DriverEntry получал возможность увидеть список автоматически обнаруженных на данный момент устройств и создать подходящие объекты устройств.
Записи, появившиеся в результате в Системном Реестре, позволяли драйверу загрузиться (в момент загрузки системы или позже), чтобы тот мог затем заняться конфигурированием устройства. Первична в таком подходе загрузка драйвера, выполненная хотя бы один раз. Вполне естественно, что такой подход называется иногда "драйверо-центричным".
С выходом Windows 95, а затем и Windows 98, и Windows 2000, данная модель была преобразована в обратную. Устройства объявляли о себе сами, либо во время загрузки, либо во время "горячего" подключения (hot plug), таким образом, настаивая на установке соответствующего регистрируемого драйвера. Такой метод получил название "аппаратно-центричного".
Следует отметить, что в настоящий момент пользователь может самостоятельно инициировать установку драйвера для устройств, не поддерживающих PnP (или даже — "как бы" устройств "как бы" не поддерживающих, что было в примере драйвера Example.sys несуществующего устройства, глава 3). Соответствующая информация будет сохранена в Системном Реестре для последующих загрузок — иными словами, старый "драйверо-центричный" механизм сохранен.
Более того, информация о PnP устройствах, однажды обнаруженных системой полностью из Системного Реестра не удаляется, даже если устройство не будет подключено при следующей загрузке системы — система "помнит" обо всех ранее произведенных подключениях и установленных драйверах, что позволяет ей экономить время, если вдруг, после длительного перерыва, пользователь решит использовать это устройство снова (см. раздел HKLM\System\CurrentControlSet\Enum).
Изначально предназначенная для Windows 95, WDM модель поддерживала методологию PnP, что существенно отличало ее от драйверной модели Windows NT. Компания Microsoft настойчиво продвигалась к достижению совместимости драйверных сред, и в результате NT модель была дополнена поддержкой PnP. Родился обобщенный подход к драйверной среде для Windows 98 и Windows NT 5, а новая общая модель получила наименование Windows Driver Model (WDM).
Routine
Процедура. Строго говоря, под процедурой в программировании (начиная с "древнего" языка Fortran) понимается модуль, получающий через заголовок параметры и ничего не возвращающий, в отличие от функции. В языке С таких традиционно понимаемых процедур нет - он привык обходиться одними функциями (правда, "старого типа" процедуру легко можно представить функцией типа void). В результате "высвободилось" слово 'процедура'. Разработчики драйверов (как и многие программисты С и С++) стали позволять себе следующую вольность: вместо слова "функция" произвольно применяются и "функция", и "процедура" (впрочем, как и слово "вызов"). Поэтому, встречая в тексте книги слово "процедура", следует его понимать исключительно так: функция языка С. То же относится и ко всей документации на английском языке (относительно слова "routine").
Scatter/Gather Problem
Проблема сборки/разборки адресов. Возникла в момент определения методов работы аппаратуры DMA в системах с виртуальной адресацией. В рамках общего подхода Windows, предлагается выполнять разборку непрерывной виртуальной области на локально непрерывные физические фрагменты. Результат помещается в MDL список, специально приспособленный для этого пакет данных. Драйвер получает MDL список и настраивает
SEH, Structured exception handling
Поддерживаемая операционной системой передача управления обработчику исключений, которые возникли во время работы (runtime exceptions).
Секции [DDInstall]
Для каждой модели, указанной в секции описания моделей аппаратуры данного поставщика, следует сделать ссылку на секцию описания собственно установки программного обеспечения драйвера — секции [DDInstall]. Конкретное название этой секции устанавливает разработчиком драйвера и, в общем случае, должно быть уникальным для каждой модели каждого производителя из тела каждой секции описания моделей. Однако бывают случаи, когда одному драйверу удается обслуживать сразу несколько моделей PnP устройств, предоставляющих при подключении разные идентификаторы. В таких случаях возможна ситуация, когда одна секция типа [DDInstall] соответствует сразу нескольким ссылкам из секции описания моделей.
Основные директивы секции [DDInstall] перечисляются в таблице 12.3. По поводу использования остальных следует обратиться к документации пакета DDK.
Таблица 12.3. Элементы секции [DDInstall]
Записи | Значения | |
DriverVer | mm/dd/yyyy[,x.y.v.z] Здесь версия драйвера может быть введена через запятую после указания даты |
|
CopyFiles | Любое имя секции, указывающей имена файлов для инсталляции, либо конкретное имя файла, предваряемое префиксом @ | |
CopyInf | Директива, определяющая копирование inf-файлов на целевой диск. Введена только в Windows XP. | |
AddReg | Обязательна для ввода. Перечисляет имена секций, где содержится информация, предназначенная для занесения в Системный Реестр во время инсталляции. | |
Include | Указатель на другие INF файлы, необходимые для данной инсталляции | |
Needs | Подмножество/а записи Include (выше), перечисляющее имена всех необходимых секций (считая все включаемые INF файлы). | |
DelFiles | Указывает имена других секций, которые перечисляют файлы, подлежащих удалению в целевой директории (обычно, в процессе обновления, upgrade). | |
RenFiles | Указывает имена других секций, которые перечисляют файлы, подлежащих переименованию перед инсталляцией (обычно, чтобы сохранить состояние предыдущей инсталляции). Об организации секций, описывающих переименование см. подробнее в документации DDK. | |
DelReg | Указывает имена других секций, которые содержат информацию, что именно следует удалить из Системного Реестра при инсталляции |
В то время, как AddReg требуется только с точки зрения синтаксиса, директива CopyFiles является весьма значимой директивой секции [DDInstall]. Директива CopyFiles имеет форму
CopyFiles = file_list_section[,file_list_section...]
либо
CopyFiles=@filename
Первый из двух вышеприведенных вариантов является более емким, поскольку позволяет косвенно указать другую секцию, где содержится список файлов, подлежащих инсталляции. Однако для простых инсталляций, непосредственное указание имени файла успешно справляется с этой задачей. Назначение AddReg и CopyFiles
более проясняется в нижеследующих частях данной главы.
Когда имя секции [DDInstall] упоминается в ссылке из секции описания модели, то суффиксы, задающие версию системы, применять не следует. В момент ссылки в директивах секций описания моделей имя секции [DDInstall] задается универсально, одинаково для всех типов операционных систем (без стандартных суффиксов, типа .NT или .NTx86). Зато, в начале собственно тела секции, имя [DDInstall] секции может быть декорировано одним из суффиксов типа .nt, .ntx86 или ntia64, что означает принятие к исполнению данной секции только в соответствующей операционной системе. Пример ниже демонстрирует, что конкретизация происходит в момент описания собственно секции [DDInstall].
[Manufacturer] %MSFT%=MSFT
[MSFT] %_MCADesc%=_MCA_Inst, _MCA0000
[_MCA_Inst.ntx86] CopyFiles = _MCA.Files.x86_12
Здесь в секции моделей [MSFT] введена ссылка на [DDInstall] секцию с конкретным именем _MCA_Inst, и эта секция была введена только для использования в Windows NT. Поэтому имя было декорировано суффиксом .NTx86, что в результате выглядит как [_MCA_Inst.ntx86].
Секции [DefaultInstall32.Xxx] и [DefaultInstall32.Xxx.Services]
В Windows 98 была возможность установки драйвера по нажатию правой кнопки мышки в программе Проводник на inf-файле с последующим выборе в открывшемся меню пункта "Установить". В Windows 2000/XP/2003 для такой установки необходимо наличие в inf-файле секций [DefaultInstall32.Xxx] и [DefaultInstall32.Xxx.Services], где "Ххх" обозначает суффиксы декорирования имен nt, ntx86, ntia64.
Использование таких секций и усеченная установка из программы Проводник (то есть без вовлечения Мастера Установки) зачастую дают неприемлемые результаты, поэтому рекомендуется при установке драйверов использовать обычный способ установки через Мастера Установки новых устройств.
Секции [ServiceInstall]
Секции типа [ServiceInstall] предназначены для заполнения или модификации подраздела Системного Реестра, описывающего загрузку драйвера в сервисном подразделе для данного драйвера, а именно — в подразделе HKLM\System\CurrentControlSet\Services\<service-name>. Здесь <service-name> — это значение поля service-name, указанное в директиве AddService в секции [DDInstall.Xxx.Services]. Конкретное имя секции типа [ServiceInstall] выбирается разработчиком inf-файла. Декорирование имен секций данного типа (с целью отразить ее предназначение для конкретной версии системы) уже не имеет смысла и не воспринимается, поскольку эта принадлежность должна была быть введена раньше — на уровне секций [DDInstall.Xxx.Services]. Описание директив для секций типа [ServiceInstall] приводится в таблице 12.10, причем директивы ServiceType, StartType, ErrorControl и ServiceBinary являются обязательными. Эти директивы однозначно определяют информацию (значения одноименных параметров), которая появится в Системном Реестре в сервисном подразделе для данного драйвера — пример, касающийся драйвера Example.sys, рассмотрен в Приложении В.
Таблица 12.10. Записи секции [ServiceInstall]
Запись | Значение поля | |
DisplayName | Развернутое наименование драйвера, выводится на экран Мастером Установки Оборудования | |
Description | Краткое описание назначения драйвера или сервиса, выводится Мастером Установки Оборудования | |
ServiceType | Для драйвера режима ядра 0x01 (см. также Приложение В) | |
StartType | Определяет момент загрузки драйвера 0 — SERVICE_BOOT_START — во время загрузки системы (WDM драйверы, опирающиеся на системные драйверы не должны использовать такой тип запуска) 1 — SERVICE_SYSTEM_START — во время инициализации системы (WDM драйверы, опирающиеся на системные драйверы должны использовать такой тип запуска с осторожностью) 2 — SERVICE_AUTO_START — автостарт после запуска системы средствами SCM Менеджера (WDM драйверы и драйверы РпР устройств не должны указывать этот код запуска) 3 — SERVICE_DEMAND_START — старт по требованию: либо по запросу РnР Менеджера при обнаружении РnР устройства, либо по явному запросу приложения при помощи вызовов SCM Менеджера 4 — SERVICE_DISABLED — не может стартовать |
|
ErrorControl | Распоряжение относительно возникающих ошибок: 0 — игнорировать все ошибки при загрузке драйвера 1 — показывать сообщения об ошибках пользователю 2 — выполнить рестарт с набором параметров, обеспечившим последнюю удачную загрузку (LastKnownGood), игнорировать дальнейшие ошибки 3 — выполнить рестарт с набором параметров, обеспечившим последнюю удачную загрузку (LastKnownGood), контроль ошибок если таковые возникнут со стороны пользователя |
|
ServiceBinary | Путь к файлу драйвера (может включать коды dirid, таблица 12.6) | |
AddReg | Вводит (через запятую) ссылки на секции типа [AddReg], в которых описываются действия над Реестром, которые следует выполнить дополнительно к описанным в данной секции | |
LoadOrderGroup | Идентифицирует группу, в которой должен загружаться драйвер (возможные группы можно увидеть в разделе Системного Реестра HKLM\System\CurrentControlSet\Control\GroupOrderList) | |
Dependencies | Указывает сервисы (драйверы) или группы загрузки, которые должны быть загружены к моменту загрузки драйвера. Имена групп выделяются при вводе предшествующим им знаком '+'. |
Директивы LoadOrderGroup и Dependencies широко используются при установке драйверов SCSI устройств и фильтр-драйверов.
Директива DelReg, которая также может быть в составе [ServiceInstall], вводит ссылки на секции, описывающие удаление из Системного Реестра информации для уже установленных программных продуктов. Используется эта директива редко.
Остальные директивы, возможные для ввода в секциях типа [ServiceInstall], а именно, StartName и BitReg практически не используются.
Секции inf-файла и основные общие правила ввода записей
Инсталляционный inf-файл поделен на секции, каждая из которых начинается с идентификатора (имени секции), заключенного в квадратные скобки. Часть секций является обязательной, присутствие других секций зависит от назначения драйвера.
Записи внутри каждой секции описывают действия по инсталляции, либо ссылаются на другие секции. Записи, которые регламентированы для секций определенного типа (обязательные или нет), в литературе и в документации DDK часто называются директивами.
Весь текст, введенный в inf-файле, не различается в смысле регистра символов — все имена секций и записи могут быть введены и в верхнем, и в нижнем регистре. Поэтому слова version, VERSION и Version являются идентичными для процесса установки. Текст не должен содержать символов табуляции и других невидимых управляющих символов.
Символ "точка с запятой" означает начало комментариев в следующей (за точкой с запятой) позиции, которые продолжаются до конца строки. Комментарии не принимаются в рассмотрении при анализе inf-файла. Данное правило не действует только в том случае, если такой текстовый фрагмент (содержащий точку с запятой) заключен в кавычки.
Строка, содержащая только символы возврата каретки и перевода строки, считается пустой и игнорируется. Если существует необходимость продолжить запись на следующей строке, то в последней позиции текущей строки следует ввести обратный слэш \.
Порядок следования секций в файле не играет роли, важно лишь, чтобы секции носили корректные имена и были правильно соотнесены в перекрестных ссылках. (Правда, сами разработчики придерживаются некоторых правил, например, секция [Strings] вводится обычно последней.) Секция продолжается до объявления начала следующей секции, либо до обнаружения конца файла. Имя секции должно быть уникальным для данного inf-файла. Хотя в некоторых источниках указывается, что содержимое секций, имеющие одинаковые имена, системное программное обеспечение, занимающееся интерпретацией inf-файла, объединяет, тем не менее, практика показывает, что это не так.
Например, из двух секций [Strings] принимается во внимание только содержимое первой (по тексту inf-файла).
Имя секции не должно содержать более 28 символов для Windows 9x и более 255 символов для Windows NT. Ссылки на секции могут содержать в своем составе пробелы, но только если имя в целом заключено в кавычки (то же относится и к символу точки с запятой). Допустимы точка и символ подчеркивания. Например, как указывает документация DDK, строка в кавычках ";;Std Mfg " является приемлемой ссылкой на имя секции, если указываемая секция имеет имя, в точности совпадающее с содержимым внутри кавычек, а именно [;; Std Mfg ].
В теле секции информация представляется в форме записей. Общий формат элемента секции для inf-файлов, применяемых в Windows:
entry = value[,value[,value...]]
где entry является ключевым словом (начало директивы) либо маркером (ссылкой на значение — такая ссылка обособляется символами %...%). Параметры value являются значениями, соотносимыми с полем entry (именно, соотносимыми, а не присваиваемыми). В редких типах секций в роли поля entry могут выступать имена файлов (например, в секции [SourceDiskFiles]) или имена других секциях (например, в секции [DestinationDirs]).
В inf-файлах для нескольких систем в секциях для Windows 9x все запятые должны присутствовать в указанном в документации количестве, а в секциях для NT замыкающие перечисления запятые (если значения опущены) можно опускать (то есть в тех секциях, имена которых которые оформлены суффиксами .nt, .ntx86, и т.п.). Например, запись в секции SourceDisksFiles в общей нотации описывается следующим образом:
filename = diskid[, [subdir] [, size] ]
Пропуская значение subdir, указываем значение size и оставляем две запятых в середине:
filename = diskid,,size
Если пропускаем два значения (subdir и size) в inf-файле для NT, то запись выглядит так:
filename = diskid
Для Windows9x это требуется ввести иначе:
filename = diskid,,
Секция [AddReg]
Секции типа [AddReg] содержат описание действий по внесению новых подразделов и/или параметров и их значений в Системный Реестр, а также действия по модификации значений уже существующих параметров.
Ссылки на секции данного типа могут присутствовать в секциях [DDInstall], [ClassInstall32] и секциях [ServiceInstall] (обозначенных ссылками из директив AddService в секциях [DDInstall.Services]). Кроме того, ссылка на [AddReg] может быть введена и из секций, описывающих установку интерфейса (для организации доступа к объекту устройства по идентификатору интерфейса), что в данной книге не рассматривается. В перечисленных типах секций ссылки на [AddReg] вводятся директивами AddReg. Конкретное имя секций типа [AddReg] зависит от разработчика inf-файла. Каждая запись внутри секции [AddReg] имеет вид
reg-root, [subkey], [value-name], [flag] , [value]
Здесь в поле reg-root следует ввести аббревиатуру одного из корневых разделов Системного Реестра, возможные значения которых перечислены в таблице 12.7. Эти значения указывают на корневые разделы, в чьих подразделах будут сделаны изменения. Поле subkey представляет наименование подраздела внутри указанного корневого раздела. Значении HKR не имеет конкретного, раз и навсегда определенного, значения. Его конечное значение в записях секции [AddReg] зависит от того, из какой секции была сделана ссылка на [AddReg].
Таблица 12.7. Аббревиатуры корневых разделов Системного Реестра
Значения | Описание | |
HKCR | HKEY_CLASSES_ROOT | |
HKCU | HKEY_CURRENT_USER | |
HKLM | HKEY_LOCAL_MACHINE | |
HKU | HKEY_USERS | |
HKR | Контекстный раздел Системного Реестра (то есть какой конкретно раздел Реестра будет модифицирован, зависит от того, в какой секции была сделана ссылка на секцию типа [AddReg]) |
Значение value-name обозначает имя параметра в модифицируемом подразделе subkey, который (параметр подраздела, то есть) будет добавлен или модифицирован.
Значение flag описывает тип данных, который должен быть сохранен в поле значения параметра данного модифицируемого подраздела Системного Реестра.
Возможные значения (из числа применимых в Windows 2000, XP и 2003), которые может принимать поле flag, перечисляются в таблице 12.8.
Таблица 12.8. Основные значение поля flag
в записях секции [AddReg]
Значение | Символьное имя | Описание |
0x00000 | FLG_ADDREG_TYPE_SZ | Строка символов, завершающаяся нулем (значение flag по умолчанию, если это поле в записи опущено) |
0x00001 | FLG_ADDREG_BINVALUETYPE | Бинарные данные |
0x00002 | FLG_ADDREG_NOCLOBBER | Не замещать существующее значение |
0x00004 | FLG_ADDREG_DELVALUE | Стереть подраздел или параметр |
0x00010 | FLG_ADDREG_KEYONLY | Создать подраздел, игнорировать параметр и его значение |
0x00020 | FLG_ADDREG_OVERWRITEONLY | Если параметр существует, заменить, иначе ничего не предпринимать |
0x10000 | FLG_ADDREG_TYPE_MULTI_SZ | Данные REG_MULTI_SZ (массив строк) |
0x00008 | FLG_ADDREG_APPEND | Присоединить к существующему массиву строк REG_MULTI_SZ. Применим только совместно с FLG_ADDREG_TYPE_MULTI_SZ |
0x20000 | FLG_ADDREG_TYPE_EXPAND_SZ | Данные типа REG_EXPAND_SZ |
0x10001 | FLG_ADDREG_TYPE_DWORD | Данные типа REG_DWORD |
0x20001 | FLG_ADDREG_TYPE_NONE | Данные REG_NONE |
[DeviceInstallSection] AddReg = DeviceAddRegSection
[DeviceAddRegSection] HKR,, ThisDriver, %REG_DWORD%, 1 HKR,, InstalledDrivers, %REG_MULTI_SZ%, Device0001
[Strings] ; расшифровка значений маркеров REG_SZ = 0x00000000 REG_MULTI_SZ = 0x00010000 REG_EXPAND_SZ = 0x00020000 REG_BINARY = 0x00000001 REG_DWORD = 0x00010001
Следует помнить, что в том случае, если в полях записей вводится значение, содержащее пробелы или другие специальные символы, то эту группу символов следует заключить в кавычки. Если же значение простое (пусть даже строка), то его заключать в кавычки не обязательно, например:
[MyDriver.Install] CopyFiles=. . .AddReg=MyDriver.AddReg
[MyDriver.AddReg] HKR,,DevLoader,,*ntkern HKR,,NTMPDriver,, MyDriver.sys ; (flag = FLG_ADDREG_TYPE_SZ)
что равносильно
HKR,,"NTMPDriver",,"MyDriver.sys"
Секция [ClassInstall32]
Разработчик драйвера может создать собственный класс устройств (с собственным GUID, созданным при помощи программы GuidGen) и использовать его при установке своего драйвера. Данная операция не является сложной и выполняется при помощи секции [ClassInstall32], например:
[Version] Signature="$Chicago$" Class=ExampleDrvClass ClassGuid={DC16BE99-C06B-4801-A144-43A98BB99052} . . . [ClassInstall32] Addreg=ExampleClassReg
[ExampleClassReg] ; секция изменений в Реестре HKR,,,0,%ClassName% ; имя класса вводится через маркер %ClassName%
[Strings] ; Дополняем секцию значением маркера ClassName="Example's Driver Class" . . .
Рис. 12.2 Новый класс в окне Диспетчера устройств |
Внесем приведенные выше дополнения в inf-файл, предназначенный для установки драйвера Example.sys, см. главу 3. В результате установки Мастером установки появится новый класс с указанным GUID в разделе HKLM\System\CurrentControlSet\Control\Class
Системного Реестра, см. рисунок 12.3. В его подразделе будет указан параметр Class, содержащий значение имени "ExampleDrvClass", и один вложенный подраздел \0000, описывающий установленный драйвер Example.Sys, см. рисунок 12.4.
Рис. 12.3. Новый класс в окне Редактора Системного Реестра |
Рис. 12.4. Вновь установленный драйвер класса ExampleClassReg |
Открывая описания классов в Системном Реестре при помощи Редактора Реестра, можно увидеть, что другие классы имеют существенно больше параметров, чем создано для нового класса ExampleDrvClass при помощи указанных выше записей. Все недостающие параметры можно ввести в секции описания изменений в Реестре, в данном случае — [ExampleClassReg].
Имя секции [ClassInstall32] может быть декорировано при помощи суффиксов .nt, .ntx86 и .ntia64 для того, чтобы ограничить применимость данной секции.
Помимо директивы AddReg, обязательной для секции [ClassInstall32], в данной секции могут присутствовать некоторые другие директивы (следует обратиться к документации DDK), из которых самой важной является директива CopyFiles.
Синтаксис директивы CopyFiles совершенно аналогичен тому, который используется в секции [DDInstall], и вводит информацию о копировании файлов, если таковые необходимы для завершения установки нового класса.
В том случае, если при установке нового класса устройств еще требуется установить и некоторые драйверы, предусмотрена возможность их установки с использованием секции [ClassInstall32.Services] ([ClassInstall32.Xxx.Services]), использование которой аналогично [DDInstall.Services].
Созданный класс легко и безболезненно удаляется из Системного Реестра (в интерактивном Редакторе), если только в системе не осталось устройств данного класса.
Секция [CopyFiles]
Секции [CopyFiles] имеют уникальные для INF файла названия, ссылки на них исходят из директив CopyFiles секций [DDInstall]. Соответственно, конкретные имена этих секций определяет сам разработчик inf-файла.
Каждая запись внутри секции [CopyFiles] имеет вид
destination-filename[, source-filename][, temp-filename][, flag]
где destination-filename является целевым (то есть новым, конечным) именем файла после копирования. Предполагается, что и исходный файл имеет такое же имя. В том случае, если исходный файл все-таки называется иначе, необходимо указать source-filename. Требование указывать temp-filename
все еще требуется для Windows 98/Me, и это поле вводит промежуточное имя для нового файла до момента первой перезагрузки системы. В Windows 2000/XP/2003 это значение игнорируется.
Таблица 12.5. Определение значения flag
в записях секции [CopyFiles]
Значение | Символьное имя | Описание | |
0x0400 | COPYFLG_REPLACEONLY | Копировать исходный файл только в том случае, если в целевой директории есть файл с таким именем | |
0x0800 | COPYFLG_NODECOMP | Копировать без разархивации (если файл обработан архиватором) | |
0x0008 | COPYFLG_FORCE_FILE_IN_USE | Если файл с целевым именем в целевой директории сейчас открыт, то следует копировать исходный файл в файл с временным именем, форсировать перезагрузку, после чего переименовать временный файл | |
0x0010 | COPYFLG_NO_OVERWRITE | Не переписывать существующие одноименные файлы в целевой директории | |
0x1000 | COPYFLG_REPLACE_BOOT_FILE | Файл является частью системной загрузки, форсировать перезагрузку системы | |
0x2000 | COPYFLG_NOPRUNE | Осуществить копирование, даже если инсталлятор не считает эту операцию целесообразной | |
0x0020 | COPYFLG_NO_VERSION_DIALOG | Не переписывать одноименные существующие файлы, которые датированы как более новые, нежели предназначенные к записи (игнорируется, если инсталлируемый пакет имеет цифровую подпись) | |
0x0004 | COPYFLG_NOVERSIONCHECK | Всегда переписывать целевые файлы (флаг игнорируется, если инсталлируемый пакет имеет цифровую подпись) | |
0x0040 | COPYFLG_OVERWRITE_OLDER_ONLY | Переписывать только те существующие файлы, которые являются более старыми, чем имеющиеся в пакете (данный флаг игнорируется, если инсталлируемый пакет имеет цифровую подпись) | |
0x0001 | COPYFLG_WARN_IF_SKIP | Предупреждать пользователя о возникшей необходимости пропустить переписывание файл (игнорируется, если инсталлируемый пакет имеет цифровую подпись) | |
0x0002 | COPYFLG_NOSKIP | Запретить пользователю выбор возможности пропуска каких-либо файлов при копировании (всегда применяется, если инсталлируемый пакет имеет цифровую подпись) |
Значение flag определяет управление новым целевым файлом, что подробнее отражено в таблице 12.5. Для описания сложного управления необходимо выполнять ИЛИ над операндами — для получения одновременного воздействия указываемых вариантов. Некоторые варианты взаимно исключают друг друга (например, COPYFLG_WARN_IF_SKIP и COPYFLG_NOSKIP), поэтому следует в сомнительных ситуациях обратиться к документации.
Так как секции [CopyFiles] не имеют синтаксических средств указывать диск или полный путь к исходному файлу, то следует использовать другие секции, такие как [SourceDisksNames] и [SourceDisksFiles]. Место (конкретные файловые каталоги), куда файлы будут помещены в результате установки, определяется другой секцией, называемой [DestinatonDirs].
Следует отметить, что здесь секция [CopyFiles] описывается, как присутствующая в inf-файле по той причине, что на нее ссылалась директива CopyFiles
из секции [DDInstall]. На самом деле, директива CopyFiles
может присутствовать и в секции [ClassInstall32], которая посвящена инсталляции нового класса устройств в системе (будет рассмотрена ниже). Вводимая таким образом секция [CopyFiles] должна быть построена по таким же правилам, как указано здесь.
Секция [DDInstall.Services]
Чтобы скопированные в положенное место файлы действительно заработали как настоящий драйвер, необходимо надлежащим образом уведомить SCM Менеджер. Соответственно, для этого необходимо иметь записи в системном Реестре в разделе HKLM\System\CurrentControlSet\Services
по поводу каждого драйвера.
В Windows 2000/XP/2003 работа по регистрации драйвера вынесена в отдельный тип секций [DDInstall.Services].
Можно считать, что данная секция является придатком секции [DDInstall] и всего лишь конкретизирует, какие секции inf-файла (service-install-section) определяют, что именно будет внесено/удалено в Системный Реестр в рамках установки драйвера. В Windows 98 следовало пользоваться директивой AddReg
в секции [DDInstall]. В Windows 2000 inf-файл стал сложнее, и работа с Системным Реестром была выделена в еще один "промежуточный" тип секций [DDInstall.Services].
В том случае, если имя [DDInstall] секции декорировано одним из суффиксов .nt, .ntx86 или ntia64 (что означает принятие к исполнению данной секции только в соответствующей операционной системе), то суффикс .Services
следует присоединять именно к получившемуся декорированному имени [DDInstall.Xxx].
Среди записей секции [DDInstall.Xxx.Services] имеется обязательная директива AddService (помимо еще трех необязательных), которая имеет вид:
AddService=service-name,[flags],service-install-section[ ,eventlog-install-section[,event-log[,event-name]]]
где service-name представляет название сервиса, что обычно совпадает с названием Драйвера (если отбросить расширение .sys). Подраздел с таким именем будет создан в результате инсталляции в разделе Системного Реестра HKLM\System\CurrentControlSet\Services.
Директив AddService в секциях [DDInstall.Xxx.Services] может быть несколько.
Возможные значения поля flags приводятся в таблице 12.4. Можно вводить значения, которые представляют "побитовое ИЛИ" указанных в таблице значений.
Значение поля service-install-section и необязательное для ввода значение поля eventlog-install-section (ссылка на секцию, где описывается установка сервисов протоколирования) объявляют имена секций INF файла типа [ServiceInstall], например:
[Manufacturer] ; секция описания поставщиков %ThisMfg%=DeviceList
[DeviceList] ; секция описания моделей %Model1%= Model1_Inst, _MXX0001 ; <- идентификатор
[Model1_Inst.ntx86] ; [DDInstall] секция инсталляции для NT . . . . [Model1_Inst.ntx86.Services] ; установка драйвера как сервиса Mxx0001 ; Поле flags имеет значение 0x0002 (см. таблицу 12.4) ; ссылка на секцию [ServiceInstall] AddService= Mxx0001, 0x0002, MODEL1_ADDSERVICE_SECTION
[MODEL1_ADDSERVICE_SECTION] ; секция [service-install-section] DisplayName = %Model1.ServiceDesc% ServiceType = 1 ; = SERVICE_KERNEL_DRIVER, см. ntddk.h, wdm.h StartType = 3 ; = SERVICE_DEMAND_START, см. ntddk.h, wdm.h ErrorControl = 1 ; = SERVICE_ERROR_NORMAL, см. ntddk.h, wdm.h ServiceBinary= %12%\SpecialDrv.sys
[Strings] ; Расшифровка значений ThisMfg= "This Manufacturer" Model1= "Model1 (made by This Manufacturer)" Model1.ServiceDesc= "Model1 Special Driver v.1.000"
Таблица 12.4. Значения flags
директивы AddService
Значение | Символьное имя SPSVCINST_Xxx | Описание |
0x0002 | SPSVCINST_ASSOCSERVICE | Драйвер является функциональным драйвером или драйвером в "стиле-NT" |
0x0008 | ...NOCLOBBER_DISPLAYNAME | Не переписывать существовавшее в Системном Реестре значение DisplayName, если такой сервис уже был установлен ранее |
0x0100 | ...NOCLOBBER_DESCRIPTION | Не переписывать описание |
0x0010 | ...NOCLOBBER_STARTTYPE | Не переписывать StartType |
0x0020 | ...NOCLOBBER_ERRORCONTROL | Не переписывать ErrorControl |
0x0040 | ...NOCLOBBER_LOADORDERGROUP | Не переписывать LoadOrderGroup |
0x0080 | ...NOCLOBBER_DEPENDENCIES | Не переписывать Dependencies |
и т.п.) будет рассмотрена далее, после описания секций типа [AddReg].
В секциях типа [DDInstall.Services] могут быть введены директивы DelService (удаления ранее установленных сервисов), Include (включения текста внешних inf-файлов) и директива Needs.Для получения информации по данным необязательным директивам следует обратиться к соответствующей документации, например, документации DDK.
Секция [DestinationDirs]
Данная обязательная секция INF файла описывает файловый каталог (директорию), в который будет производиться копирование исходных файлов. Без этой секции программа, выполняющая установку драйвера, не будет знать, куда копировать файлы исходных носителей. Записи в секции [DestinationDirs] имеют вид:
file_list_section=dirid[,subdir]
либо
DefaultDestDir=dirid[,subdir]
где file_list_section как раз указывает на секцию типа [CopyFiles], на которую была объявлена ссылка также и в директиве CopyFiles
из секции типа [DDInstall] (или [DDInstall.Xxx]).
Это означает, что все файлы, перечисленные в этой "перекрестно ссылаемой" секции типа [CopyFiles], будут скопированы в файловый каталог (директорию), которому присвоен идентификатор dirid. Значение dirid указывает численное значение, за которым стоит достаточно конкретный файловый каталог (директория) назначения. Таблица 12.6 описывает возможные числовые коды dirid
и ассоциированные с ними файловые каталоги.
Запись DefaultDestDir=... указывает место, куда будут скопированы файлы, перечисленные в остальных секциях типа [CopyFiles], для которых не было указано в явной форме (записями вида file_list_section=dirid[,subdir]) то место, куда переносить эти файлы.
В том случае, если указано значение subdir, то оно используется для того, чтобы конкретнее указать позицию целевого файлового каталога относительно каталога, заданного кодом dirid.
Таблица 12.6. Значения кодов dirid
в контексте секции DestinationDirs
Значения | Описание | |
12 | %windir%\system32\drivers для Windows 2000/XP/2003 %windir%\system\IoSubsys для Windows 98 |
|
10 | %windir%\ | |
11 | %windir%\system32 для Windows 2000/XP/2003 %windir%\system для Windows 98 |
|
30 | Корневая директория загрузочного диска | |
54 | Загрузочная (boot) директория Windows 2000/XP/2003 | |
01 | Директория данного INF файла | |
17 | Директория INF файлов | |
20 | Директория шрифтов | |
51 | Spool-директория | |
52 | Директория spool-драйверов | |
55 | Директория программ поддержки печати (print processors) | |
23 | Color (ICM) | |
-1 | Полное имя файлового каталога (абсолютный путь) | |
21 | Директория вьюверов (программ просмотра файлов различных форматов — графики, текстов, файлов БД и т.п.) | |
53 | Директория пользовательского лицевого счета (User Profile) | |
24 | Директория размещения приложений | |
25 | Совместно используемая директория | |
18 | Директория файлов-справок (help directory) | |
16406 | All Users\Start Menu | |
16407 | All Users\Start Menu\Programs | |
16408 | All Users\Start Menu\Programs\Startup | |
16409 | All Users\Desktop | |
16415 | All Users\Favorites | |
16419 | All Users\Application Data | |
16422 | Program Files | |
16427 | Program Files\Common | |
16429 | All Users\Templates | |
16430 | All Users\Documents |
В секции [DestinationDirs] может быть только одна запись DefaultDestDir=...
и много ссылок на секции вида file_list_section=dirid[,subdir].
Секция [SourceDiskFiles]
Инсталляционный inf-файл должен содержать секцию [SourceDiskFiles], в которой перечисляются имена файлов, составляющих предмет инсталляции. Каждый файл представлен одной записью в этой секции в форме:
filename = diskid [, [subdir] [,size] ]
Соответственно, значение diskid указывает диск, введенный в секции [SourceDisksNames], где находится файл filename.
Необязательное для ввода значение subdir указывает путь к этому файлу относительно директории, указанной полем path в соответствующей (по diskid) записи секции [SourceDisksNames]. Если значение path не было там указано, то подразумевается инсталляционная директория (там, откуда взят inf-файл).
Необязательное значение size описывает размер файла в байтах в несжатой форме. В процессе инсталляции эти данные о размерах могут быть использованы для прогнозирования, достаточно ли дискового пространства в системе, до начала копирования файлов.
Практически, значение diskid следует рассматривать всего лишь как идентификатор для построения связки "запись в секции [SourceDiskFiles]" — "запись в секции [SourceDisksNames]", что необходимо системному программному обеспечению, выполняющему установку, для уяснения, откуда следует брать исходные файлы. |
Имя секции [SourceDiskFiles] также может декорироваться суффиксами версий операционной системы x86 и ia64.
Секция [SourceDiskNames]
В случае, если файлы, относящиеся к установке драйвера и управляемые данным INF файлом, размещены более чем на одном диске (CD или дискете), то INF файл должен содержать секцию [SourceDisksNames]. Эта секция содержит по одной записи на каждый диск из набора, предлагаемого для установки. Запись имеет вид:
diskid=disk_description[,[tagfile],[неиспользуемое_поле,path][,flags]]
где diskid это уникальное в пределах набора дисков неотрицательное целое число. Как правило, нумерация дисков начинается с 1, хотя возможна и шестнадцатеричная нумерация, например 0x0, 0x1 и т.п.
Поле disk_description является понятной человеку строкой (выделенной кавычками), которая может быть использована для того, чтобы проинформировать пользователя о том, какой диск требуется установить в привод. Здесь можно применять маркер (вместо строки в кавычках), который, соответственно, раскрывается строкой в секции [Strings].
Значение tagfile играет двойную роль. Для уверенности в том, что пользователь предоставил правильный диск во время процесса установки, значение tagfile (имя файла и расширение, путь не указывается) используется для проверки: файл tagfile должен присутствовать на вставленном носителе в корневой инсталляционной директории или в путях path. B случае, если файл на носителе отсутствует, будет выведена строка подсказки, предлагающая пользователю вставить правильный диск (дискету). В случае, если значение tagfile
содержит расширение .CAB, в дальнейшем будет полагаться, что этот файл представляет собой набор сжатых (архивированных) файлов в качестве файлов, предназначенных для инсталляции с этого диска.
Поле path является значением пути (относительно инсталляционной директории, то есть где находится интерпретируемый inf-файл) к исходным драйверным файлам на предлагаемом диске. Так же, как и tagfile, значение path
является необязательным. В случае, если оно не указано, то полагается, что инсталляционная директория и содержит исходные файлы, относящиеся к установке драйвера.
Поле flags обычно не используется, как и еще одно поле (tag_file), введенное только в Windows XP.
Имя секции [SourceDisksNames] может декорироваться суффиксами версий операционной системы x86 и ia64, так что в inf-файле может быть несколько секций данного типа, отличающихся суффиксами, например:
[SourceDisksNames.x86] [SourceDisksNames.ia64]
Секция описания моделей аппаратуры [Models]
Для каждого поставщика, указанного в секции [Manufacturer], должна быть представлена соответствующая секция описания моделей его аппаратуры [Models]. Имя данного типа секций не может быть жестко регламентировано, потому что разработчик сам задает его в секции [Manufacturer].
В каждой такой секции [Models] записи представляются по следующей форме:
device_description = install_section_name,hw_id[,compatible_id...]
где device_description представляет собой уникальный набор видимых символов либо маркер, обязательный для определения в секции [Strings]. Данная строка будет предъявляться пользователю во время инсталляционного диалога, так что имеет смысл позаботиться о поддержке нескольких языков.
Значение install_section_name представляет собой ссылку на секцию, описывающую собственно действия по инсталляции для данной модели (в документации DDK такого типа секции обозначены как [DDInstall]).
Значение hw_id является PnP идентификатором, возвращаемым аппаратным устройством во время опроса PnP-совместимой шины. Например, USB\VID_04B4&PID_1002 определяет плату тестового набора фирмы Cypress (так называемый EZUSB Kit). Любое количество значений compatible_id может быть приведено для обозначения того, что та же самая инсталляционная запись должна быть использована для указанного в этом списке устройства.
Применение одной и той же группы символов может сбить с толку начинающего разработчика inf-файлов. Рассмотрим показательный пример из DDK для Windows XP.
[Version] Signature = "$Windows NT$" ; inf-файл для установки только под NT Class=System ClassGUID={4d36e97d-e325-11ce-bfc1-08002be10318} Provider=%MSFT% DriverVer= 5/1/2001
[Manufacturer] %MSFT%=MSFT ; со знаками процента - маркер
[MSFT] %_MCADesc%=_MCA_Inst,_MCA0000
[_MCA_Inst.ntx86] CopyFiles = _MCA.Files.x86_12
[Strings] MSFT= "Microsoft" ; раскрываем маркер _MCADesc= "Microsoft MCA Driver"
В секции [Manufacturer] видим, что маркер %MSFT% "приравнивается" ссылке MSFT.
Секция описания поставщика [Manufacturer]
Второй по важности секцией любого inf-файла является секция поставщиков оборудования. В ней указываются ссылки на секции описания моделей [Models] устанавливаемого оборудования.
Первая из возможных форм записей в данной секции:
%token% = model_section_name
В секции может быть много поставщиков и, соответственно, ссылок на секции описания моделей. Вот один из примеров DDK:
[Manufacturer] %ATAPI_CHGR% = atapi_chgr %CHINON% = chinon_cdrom %DENON% = denon_cdrom %FUJITSU% = fujitsu_cdrom %HITACHI% = hitachi_cdrom %HP% = hp_cdrom %MITSUMI% = mitsumi_cdrom %NEC% = nec_cdrom %OTI% = oti_cdrom %PIONEER% = pioneer_cdrom %WEARNES% = wearnes_cdrom %GenManufacturer% = cdrom_device
Данная секция описания поставщиков содержит 12 записей о поставщиках. Слева указаны маркеры (token — идентификаторы, обособленные двумя знаками процента, см. ниже), которые позже, в том же inf-файле в секции [Strings], соотнесены со строками-названиями производителей в полной текстовой форме. Справа от знака равенства указаны имена секций (например, секции [atapi_chgr]), описывающих установку программного обеспечения для моделей аппаратуры данного производителя (ссылки на секции моделей), которые были в этом файле рассмотрены позже.
Другая форма возможна для операционных систем, начиная с Windows XP, где можно указывать разные секции моделей в связи с версией операционной системы. В результате строки в секции [Manufacturer] приобретают вид:
%token%= model_section_name[,TargetOS] [,TargetOS]
Например, для Windows Server 2003:
[Manufacturer]
%MSFT%=Microsoft_Model_Section, NT.5.2
Соответственно, секции моделей будет начинаться так:
[Microsoft_Model_Section] ; допустимо для ОС систем до и включая <тело секции моделей> ; Windows XP
[Microsoft_Model_Section.NT.5.2] ; для Windows Server 2003 <тело секции моделей>
Значение маркера следует раскрыть в секции [Strings].
Для секции описания производителя [Manufacturer] возможна и другая форма (содержащая только идентификатор производителя, являющийся одновременно и ссылкой на секцию моделей), например:
Секция описания версии [Version]
Корректно составленный inf-файл начинается с секции [Version], которая является заголовком и меткой для всего драйверного inf-файла. Допустимые и необходимые записи внутри секции [Version] перечисляются в таблице 12.1.
Таблица 12.1. Элементы секции [Version]
Записи | Значения | |
Signature | Обязательная запись. Одно из указанных ниже значений "$Windows NT" — для ОС ряда Windows NT "$Windows 95" — для ОС Windows 9x/Me "$Chicago$" — для всех версий ОС, поддерживающих WDM драйвера |
|
Class | Имя класса для целого семейства драйверов. Некоторые имена, например, Net, Display или Unknown зарезервированы (предопределены). В секции [Version] должна быть либо директива Class, соответствующая типу устройства, обслуживаемого устанавливаемым драйвером, либо ClassGuid, либо обе сразу. | |
ClassGuid | Уникальный GUID идентификатор для класса устройства, которое обслуживает данный набор драйверного программного обеспечения (см. таблицу 12.2). | |
Provider | Поставщик INF файла, наименование организации и т.п. | |
LayoutFile | Используется только в INF файлах, поставляемых с операционной системой. Файлы, поставляемые OEM (Original Equipment Manufacturer), то есть "при аппаратуре", должны вместо этого элемента использовать SourceDiskNames и SourceDiskFiles | |
CatalogFile | Указывает на cat-файл (с расширением .CAT), содержащий набор драйверных файлов. Этот набор формируется лабораторией Microsoft HW Quality Lab и содержит зашифрованную цифровую подпись проверенного драйверного программного обеспечения. Данный файл не должен подвергаться каким-либо формам архивации. | |
DriverVer | Обязательная запись. Независимо от локализации версии ОС имеет формат mm/dd/yyyy[,x.y.v.z]; Здесь версия драйвера может быть введена через запятую после указания даты. |
В таблице 12.2 приводятся некоторые из инсталляционных классов, которые можно указывать в директивах Class и ClassGuid. Наиболее полный и верный на текущий момент набор классов можно найти в разделе Системного Реестра HKLM\CurrentControlSet\Control\Class\{...}, где операционная система хранит все поддерживаемые на текущий момент классы устройств.
Указанный раздел разбит на подразделы в соответствии с GUID идентификаторами классов, причем в каждом подразделе имеется параметр Class, в котором хранится наименование соответствующего класса в текстовой форме.
Таблица 12.2. Инсталляционные классы: названия и глобально-уникальные идентификаторы
Наименование | Описание | GUID идентификаторы |
1394 | Хост-контроллер шины 1394 | {6BDD1FC1-810F-11D0-BEC7-08002BE2092F} |
Battery | Аккумуляторные устройства питания | {72631E54-78A4-11D0-BCF7-00AA00B7B32A} |
CDROM | Устройства CD ROM | {4D36E965-E325-11CE-BFC1-08002BE10318} |
Display | Дисплейные адаптеры | {4D36E968-E325-11CE-BFC1-08002BE10318} |
HIDClass | HID устройства | {745A17A0-74D3-11D0-B6FE-00A0C90F57DA} |
Infrared | Устройства ИК-связи (IrDA) | {6BDD1FC5-810F-11D0-BEC7-08002BE2092F} |
Keyboard | Клавиатура | {4D36E96B-E325-11CE-BFC1-08002BE10318} |
Media | Устройства мультимедиа | {4D36E96C-E325-11CE-BFC1-08002BE10318} |
Modem | Модем | {4D36E96D-E325-11CE-BFC1-08002BE10318} |
Monitor | Монитор | {4D36E96E-E325-11CE-BFC1-08002BE10318} |
Mouse | Манипулятор "мышь" | {4D36E96F-E325-11CE-BFC1-08002BE10318} |
MultiPortSerial | Многопортовые последовательные адаптеры | {50906CB8-BA12-11D1-BF5D-0000F805F530} |
Network | Сетевой адаптер | {4D36E972-E325-11CE-BFC1-08002bE10318} |
NetClient | Сетевой клиент | {4D36E973-E325-11CE-BFC1-08002BE10318} |
NetService | Сетевой сервис | {4D36E974-E325-11CE-BFC1-08002BE10318} |
PCMCIA | Адаптеры PCMCIA | {4D36E977-E325-11CE-BFC1-08002BE10318} |
Ports | Порты (COM&LPT) | {4D36E978-E325-11CE-BFC1-08002BE10318} |
Printer | Принтер | {4D36E979-E325-11CE-BFC1-08002BE10318} |
System | Системные устройства | {4D36E97D-E325-11CE-BFC1-08002BE10318} |
TapeDrive | Устройства работы с магнитной лентой | {6D807884-7D21-11CF-801C-08002BE10318} |
Unknown | Другие устройства | {4D36E97E-E325-11CE-BFC1-08002BE10318} |
USB | USB устройства | {36FC9E60-C465-11CF-8056-444553540000} |
Семафоры
Семафоры — это объекты синхронизации, имеющие внутренний счетчик обращений и пребывающие в сигнальном состоянии тогда, когда значение этого счетчика больше нуля. Состояние становится несигнальным сразу же, как только счетчик принимает нулевое значение. Другими словами, семафор — это не что иное, как счетный мьютекс (см. ниже).
Для увеличения на единицу значения внутреннего счетчика семафора следует выполнить вызов KeReleaseSemaphore. Предположим, два потока t1 и t2 ожидают (используя вызов KeWaitForSingleObject) сигнального состояния семафора S, счетчик которого в настоящий момент равен 0. Когда третий поток t0 выполнит вызов KeReleaseSemaphore, значение счетчика возрастет до 1. Следовательно, одному из ожидающих потоков будет разрешено продолжить работу. Вызов KeWaitForSingleObject вернется из состояния ожидания, уменьшив счетчик семафора на единицу. Соответственно, второй поток останется заблокированным. Какому конкретно потоку повезет, почти что неизвестно — в том смысле, что не следует строить на этом расчет. Если это имеет большое значение, то следует усложнить схему синхронизации.
Удобно использовать семафоры для "охраны" созданных драйвером очередей или списков объектов. При добавлении объекта в очередь (например, собственную очередь IRP пакетов) производится увеличение на единицу счетчика семафора. Как только некий рабочий поток удаляет объект из очереди или списка, он уменьшает значение семафора. Когда счетчик семафора станет равным нулю, а очередь опустеет, поток (или потоки) перейдет в состояние ожидания.
Таблица 10.31. Функции для работы с объектами семафоров
Что необходимо сделать | Используемый вызов |
Создать семафор | KeInitializeSemaphore |
Увеличить счетчик семафора | KeReleaseSemaphore |
Запросить состояние | KeReadStateSemaphore |
Уменьшить счетчик семафора | KeWaitForSingleObject KeWaitForMultipleObject |
Для инициализации семафора используется вызов, KeInitializeSemaphore, которому необходимо передать не только два параметра будущего семафора (см.
таблицу 10.32), но и область памяти под будущий объект, выделенную, например, вызовом ExAllocatePool.
Таблица 10.32. Прототип вызова KeInitializeSemaphore
VOID KeInitializeSemaphore | IRQL == PASSIVE_LEVEL |
Параметры | Инициализирует объект семафора и устанавливает текущее значение его счетчика и предельное значение, которого этот счетчик может достигать |
IN PKSEMAPHORE pSemaphore | Указатель на область, подготовленную для объекта семафора |
IN LONG CountValue | Текущее (начальное) значение счетчика |
IN LONG CountLimit | Предел для значений счетчика (должно быть положительным) |
Возвращаемое значение | void |
Параметры вызова KeReleaseSemaphore описаны в таблице 10.33.
Таблица 10.33. Прототип вызова KeReleaseSemaphore
VOID KeReleaseSemaphore | IRQL == PASSIVE_LEVEL |
Параметры | Инициализирует объект семафора и устанавливает текущее значение его счетчика и предельное значение, которого этот счетчик может достигать |
IN PKSEMAPHORE pSemaphore | Указатель на область, подготовленную для объекта семафора |
IN LONG CountValue | Текущее (начальное) значение счетчика |
IN LONG CountLimit | Предел для значений счетчика (должно быть положительным) |
Возвращаемое значение | void |
Шина PC Card (PCMCIA)
Около десяти лет назад, несколько компаний совместно разработали стандарт шинной архитектуры для мобильных устройств. Первоначально, внимание было сфокусировано на платах оперативной памяти, и эта группа получила название Personal Computer Card International Association (PCMCIA). Мобильные устройства ограничены в размерах и энергоресурсах, и на этом сделан акцент стандартов данной группы. На сегодня более 300 компаний являются членами PCMCIA.
Первоначально, стандарт PC Card определял 68-выводной интерфейс при одной из трех величин толщины печатной платы, типа I, типа II или типа III. Данный стандарт определяет скорость передачи данных практически такую же, что и для ISA шины. Но в данном случае более высокий приоритет был установлен для минимизации энергопотребления и размеров, а не для улучшения скоростных параметров шины.
Термин PCMCIA зачастую используется вместо термина PC card, и наоборот. Такое использование создает некое противоречие: PCMCIA является организацией, в то время, как PC Card определяет шинный интерфейс. В настоящее время PCMCIA определяет, по крайней мере, три стандарта: PC Card, DMA и CardBus. Таким образом, когда используется термин 'PCMCIA card', следует насторожиться, поскольку, строго говоря, он не описывает точно, какой же конкретно тип устройств обсуждается.
Первоначально, тактовая частота шины PC Card была установлена равной тактовой частоте шины ISA, то есть 8 МГц, что может быть использовано при работе с 8 и 16 разрядными устройствами. Соответственно, максимальная пропускная способность шины при работе 16 разрядными картами достигает 16 Мбайт в секунду.
Архитектура CardBus позволяет использовать 32 разрядные устройства. Кроме того, тактовая частота увеличена до величины, определенной для шины PCI, то есть 33 МГц, что увеличивает пропускную способность CardBus до более чем 128 Мбайт/сек.
Шины в компьютерных системах
Шина (bus) представляет из себя совокупность данных, адресов и линий (проводников на печатной плате или в кабеле или шлейфе) сигналов контроля, которые позволяют устройствам организовать сообщение между собой. Некоторые шины являются "широкими", обеспечивая одновременную (параллельную) передачу многих битов данных и битов контроля. Другие представляют собой всего лишь пару проводов, позволяющую устройствам передавать данные и управляющие сигналы последовательно. Некоторые шины позволяют связываться с любым другим устройством на шине. Другие требуют наличия "хозяина шины", master controller (центральный процессор или контроллер ввода/вывода), выступающего в роли получателя или отправителя данных.
От того, какими свойствами обладает главная шина компьютера зависит производительность всей системы (недостаточно корректно сказано, но все-таки можно ее определить, как шину, к которой подключается большинство системных устройств и которая ближе всего "подходит" к процессору). В середине 80-x, например, рабочая станция Apollo на базе процессора 68020 была ориентирована на ISA шину как на стандарт. В настоящее время персональные компьютеры с Intel-архитектурой эксплуатируют шину PCI, хотя и претерпевшую модификации, но очевидно главную — стоит лишь нарисовать блок-схему системы в целом. (Вообще говоря, PCI шина применима на многих платформах, что и было изначально заложено в стандарт.)
Архитектура драйверов в операционной системе Windows NT 5 эффективно поддерживает введение новых шин, а поддержка многих популярных шин встроена в стандартную поставку Windows.
Сигналы прерываний
Как правило, устройства функционируют параллельно и асинхронно относительно действий центрального процессора. Поэтому, для ситуаций, когда им требуется внимание со стороны драйвера, код которого выполняется на центральном процессоре, была выработана тактика использования прерываний, когда устройства сигнализируют о необходимости обслуживания, то есть генерируют прерывания. Разные процессоры реализуют разные способы, как "привлечь" их, процессорное, внимание. Но имеется и общее место в эти способах: всегда задействован один (или несколько) из выводов процессора (или вспомогательной микросхемы). За этот вывод устройство может "подергать", когда потребуется участие процессора.
В обязанности процессора, если он намерен перейти к обслуживанию по поступившему сигналу, входит сохранение состояния процессора, контекста выполняемого программного потока. Лишь после этого процессор(будет правильнее сказать — операционная система) может выполнить переход к программному коду Процедуры Обслуживания Прерывания (Interrupt Service Routine, ISR), которую регистрирует драйвер, взявший на себя работу по представлению данного устройства в операционной системе.
Обычно устройства генерируют сигналы прерываний в ситуациях:
Устройство завершило обработку предыдущего запроса (поступившего от драйвера) и теперь готово к обработке нового.
Буфер или очередь FIFO устройства почти полны (во время ввода) или почти пусты (во время вывода). Такое прерывание разрешает драйверу получение из буфера устройства или вывод в буфер устройства данных для обеспечения его непрерывной работы.
Устройство столкнулось с нештатной или ошибочной ситуацией во время выполнения операции, и прерывание может быть специально для того предназначенной формой завершения операции.
Устройства, которые не способны генерировать прерывания, могут создать ситуацию серьезной деградации операционной системы. В связи с тем, что огромное количество выполняющихся потоков использует центральный процессор совместно, недопустимо позволять какому-либо драйверу такую роскошь, как ожидание полного завершения "его" текущей операции. При работе с подобными устройствами, не умеющими "разговаривать прерываниями", можно применять тактику опроса через определенные промежутки времени. В англоязычной литературе такой метод называется 'polling'.
С усложнением аппаратного обеспечения возникли конфигурации, где шины стали подключаться к другим шинам через интерфейсные элементы, называемые мостами. В результате, источниками прерываний (а практически — устройствами) оказываются несколько аппаратных слоев, образовавшихся вокруг процессора, и методы определения приоритетов и передачи сигналов процессору претерпели изменения. Тем не менее, осталось и нечто общее.
Синхронизация в режиме ядра
Синхронизационные функции и примитивы позволяют организовать временные задержки в исполнении программных потоков, которые бывают нужны либо одному отдельно взятому потоку, либо для согласования действий нескольких потоков. Разумеется, следует помнить, что при использовании приемов синхронизации, ориентированных на несколько программных потоков, действие этих приемов распространяется только на тех участников, которые признают эти правила. Никакой код операционной системы не останавливает программный поток по указке кого-то третьего. Иными словами, регулировщик движения (объект синхронизации) действует только на тех участников движения (программные потоки), которые признают его право это движение регулировать. Состояние объекта синхронизации является своего рода жестом регулировщика, который плохие участники могут игнорировать, конечно же, внося угрозу неправильной работы всей системы.
Системные программные потоки
Процесс в операционной системе Windows является единицей владения. Процессу принадлежат, в частности, программные потоки, которые уже и являются единицами исполнения. Для каждого потока установлен независимый программный счетчик и контекст исполнения, который включает состояние регистров центрального процессора, значение уровня IRQL, от которого зависит, когда этот поток получит возможность распорядиться системным процессором.
Потоки пользовательского режима хорошо известны программистам пользовательских приложений. Несколько отличаются от них потоки режима ядра. Системный поток есть такой поток, который выполняется исключительно в режиме ядра. Он не имеет контекста пользовательского режима и не может получить доступ в пользовательское адресное пространство. Соответственно, программный код рабочих процедур (в частности, код процедуры обработки IOCTL запросов, который может пользоваться виртуальными адресами пользовательского приложения) не может считаться системным потоком, хотя и относится к коду драйвера режима ядра. Системные программные потоки созданы специальными вызовами и не имеют возможности интерпретировать виртуальные пользовательские адреса (ниже 0x80000000) ни в одном из пользовательских контекстов. Системный поток не имеет корней в пользовательском режиме. Данная особенность накладывает основное ограничение, свойственное системным программным потокам, — они могут пользоваться только адресами системного адресного пространства. Все остальные адреса (виртуальные адреса пользовательских приложений), переданные им каким-нибудь способом, будут не просто бесполезны — их использование может привести к краху операционной системы.
Вторая особенность системных программных потоков состоит в особом статусе программного кода режима ядра. Если пользовательское приложение снимается, например, из панели Диспетчера задач, то происходит приостановка работы всех потоков снимаемого процесса и соответствующая очистка ресурсов, им принадлежащих. В режиме ядра такую работу за программный код системного потока никто выполнять не будет.
Системный программный поток должен самостоятельно завершить свою работу и самостоятельно освободить занимаемые ресурсы.
Системные потоки выполняются обычно на уровне приоритета IRQL APC_LEVEL или PASSIVE_LEVEL (если системный программный поток искусственно не повысил свой приоритет определенными вызовами). Соответственно, эти потоки соревнуется за использование центрального процессора наряду с программными потоками пользовательского режима.
Основная причина использования программных потоков в режиме ядра та же, что и в пользовательских приложениях — обслуживание медленных операций либо событий, наступление которых плохо прогнозируется. Сюда относятся и длительные операции инициализации некоторых специфических устройств. Диспетчер ввод/вывода отводит процедуре DriverEntry около полуминуты, после чего загрузка драйвера прекращается. Вполне приемлемым решением мог бы быть запуск программного потока, который продолжал бы инициализацию устройства, а процедура DriverEntry быстро возвратила бы код успешного завершения.
Таблица 10.1. Прототип вызова PsCreateSystemThread
NTSTATUS PsCreateSystemThread | IRQL == PASSIVE_LEVEL |
Параметры | Создает системный программный поток |
OUT PHANDLE pThreadHandle | Указатель на переменную для сохранения дескриптора нового программного потока |
IN ULONG DesiredAccess | THREAD_ALL_ACCESS (или 0L) для создаваемого драйвером потока |
IN POBJECT_ATTRIBUTES Attrib | NULL для создаваемого драйвером потока |
IN HANDLE ProcessHandle | NULL для создаваемого драйвером потока |
OUT PCLIENT_ID ClientId | NULL для создаваемого драйвером потока |
IN PKSTART_ROUTINE StartAddr | Стартовая функция потока — точка входа в поток |
IN PVOID Context | Аргумент, передаваемый в стартовую функцию |
Возвращаемое значение |
• STATUS_SUCCESS — поток создан • STATUS_Xxx — код ошибки |
Как поступить, если разработчик драйвера желает протоколировать события в драйвере, записывая их в файл на диске, включая события, происходящие при повышенных приоритетах IRQL? Ведь функция ZwCreateFile и ZwWriteFile
работают только на уровне PASSIVE_LEVEL, следовательно, о протоколировании из ISR и DPC процедур следует забыть? Подобная задача достаточно легко решается, если высокоприоритетный код будет помещать свои записи в промежуточный буфер, который будет сброшен на диск позже системным программным потоком, выполняющимся на подходящем для этого уровне PASSIVE_LEVEL.
Системные программные потоки создаются вызовом PsCreateSystemThread
(таблица 10.1), а завершиться они должны самостоятельно — выполнением вызова PsTerminateSystemThread.
Таблица 10.2. Прототип вызова PsTerminateSystemThread
NTSTATUS PsTerminateSystemThread | IRQL == PASSIVE_LEVEL |
Параметры | Вызывается системным программным потоком при окончании работы |
IN NTSTATUS ExitStatus | Код завершения потока |
Возвращаемое значение | STATUS_SUCCESS — поток прекращен |
Создаваемые драйвером системные программные потоки имеют при создании уровень IQRL равный PASSIVE_LEVEL в диапазоне приоритетов Normal (см. таблицу 6.2), хотя могут иметь любой приоритет в диапазоне Normal и RealTime.
В общем случае системный поток, запущенный из драйвера, должен выполняться при приоритете, находящемся вблизи нижней границы диапазона real-time. Изменить текущий приоритет потока можно в нем самом, используя вызов KeSetPriorityThread, как демонстрируется в следующем фрагменте:
VOID ThreadStartRoutine (PVOID pContext) { : KeSetPriorityThread ( KeGetCurrentThread(), LOW_REALTIME_PRIORITY); }
Заметим, что численное значение LOW_REALTIME_PRIORITY в заголовочных файлах DDK установлено равным 16 (это нижняя граница диапазона RealTime).
Следует помнить, что потоки RealTime не имеют квантования по времени. Это означает, что процессор перестанет заниматься данным потоком только тогда, когда поток добровольно перейдет в состояние ожидания или его не "перебьет" поток более высокого приоритета. Таким образом, здесь драйвер не может рассчитывать на обычный для пользовательского режима циклический подход в планировании заданий.
Системные рабочие потоки
Для нерегулярных коротких операций на уровне IRQL, равном PASSIVE_LEVEL, использование полноценных потоков, которые создаются и тут же завершаются, вряд ли будет эффективным. Альтернативой этому может быть создание системных рабочих потоков, system worker threads.
Для того чтобы проделать какую-нибудь несложную и не очень продолжительную работу, драйвер должен выделить память под структуру типа WORK_QUEUE_ITEM, затем инициализировать ее вызовом ExInitializeWorkItem, связав с ней собственную функцию (callback-функцию), и поместить ее в очередь объектов WORK_QUEUE_ITEM вызовом ExQueueWorkItem. Приоритет, на котором будет работать код вызываемой callback-функции, зависит от второго параметра вызова ExQueueWorkItem, QueueType, то есть от того, в какую очередь помещен данный объект WORK_QUEUE_ITEM, например, DelayedWorkQueue. В конце своей работы вызванная callback-функция должна освободить память, занятую под объектом типа WORK_QUEUE_ITEM (указатель на него поступает в callback-функцию при вызове).
Перечисленные функции ExInitializeWorkItem и ExQueueWorkItem
считаются устаревшими (предлагаемые теперь функции будут рассмотрены ниже), однако они были удобны тем, что позволяли использовать в качестве объекта-посредника структуры данных большего размера, например, при следующем техническом приеме. Описываем структуру:
typedef struct _MY_WORK_ITEM { WORK_QUEUE_ITEM Item; char AdditionalData[64]; } MY_WORK_ITEM, *PMY_WORK_ITEM;
Данная структура создается и удаляется драйвером, что позволяет ей иметь нестандартную длину — главное, что начальный блок используется обычным для системы способом (как для WORK_QUEUE_ITEM).
Таблица 10.3. Прототип вызова IoAllocateWorkItem
PIO_WORKITEM IoAllocateWorkItem | IRQL<=DISPATCH_LEVEL | |
Параметры | Создает объект рабочего потока | |
IN PDEVICE_OBJECT pDevObject | Объект устройства инициатора вызова | |
Возвращаемое значение | Указатель на созданный объект или NULL в случае неудачи |
Новые (поддерживаются в Windows Me/2000/XP/2003) предлагаемые вызовы IoAllocateWorkItem, IoQueueWorkItem и IoFreeWorkItem перераспределили обязанности.
Теперь вызов IoAllocateWorkItem (таблица 10.3) создает структуры типа IO_WORKITEM (разумеется, только размером sizeof(IO_WORKITEM)), которые "записываются" за соответствующим объектом устройства. Объект IO_WORKITEM инициализируется вызовом IoQueueWorkItem, который связывает с ним callback-процедуру драйвера и передаваемый при ее вызове контекстный аргумент. Вызов IoQueueWorkItem также помещает объект IO_WORKITEM в очередь объектов, тип которой определяется значением третьего параметра, то есть QueueType. В конце работы callback-процедура драйвера должна выполнить освобождение созданного объекта IO_WORKITEM вызовом IoFreeWorkItem.
Таблица 10.4. Прототип вызова IoQueueWorkItem
VOID IoQueueWorkItem | IRQL<=DISPATCH_LEVEL |
Параметры | Инициализирует объект рабочего потока и помещает его в очередь (обычно используется сразу после вызова IoAllocateWorkItem) |
IN PIO_WORKITEM pWorkItem | Объект рабочего потока, созданный вызовом IoAllocateWorkItem |
IN PIO_WORKITEM_ROUTINE pWorkRoutine |
Callback-процедура, предоставляемая драйвером (ее прототип описан в таблице 10.5) |
IN WORK_QUEUE_TYPE QueueType | Тип очереди. Драйвер должен предоставить одно из значений: • CriticalWorkQueue • DelayedWorkQueue |
IN PVOID pContext | Контекстный аргумент. Его получит callback-функция при вызове |
Возвращаемое значение | void |
— в очередь объектов с приоритетом Normal.
Следует помнить, что число объектов IO_WORKITEM, которые операционная система позволяет получить каждому объекту устройства (соответственно, разместить в своих очередях) не бесконечно, поэтому следует проверять результат вызова IoAllocateWorkItem
на равенство NULL. Кроме того, не рекомендуется надолго задерживаться в callback-функции, поскольку это может затормозить извлечение из соответствующей очереди других IO_WORKITEM объектов, принадлежащих другим драйверам.
В частности, не рекомендуется из таких потоков обращаться к другим драйверам вызовом IoCallDriver. Для выполнения продолжительных операций рекомендуется использовать полноценные системные программные потоки, создаваемые вызовом PsCreateSystemThread.
Термин 'системные рабочие потоки' (system worker threads) нельзя считать удачным, потому что он в точности копирует термин API пользовательского режима, обозначающий программные потоки пользовательского режима, которые уже никакими временными ограничениями не стеснены. Кроме того, лексическое отличие от "нормальных" системных программных потоков, с описания которых началась данная глава, просто неуловимо. |
VOID workCallback | IRQL == PASSIVE_LEVEL |
Параметры | Функция, предоставляемая драйвером, которая будет вызвана при извлечении из очереди объекта IO_WORKITEM (вызывается в контексте, как для системного программного потока, см. выше) |
IN PDEVICE_OBJECT pDevObject | Объект устройства, которому принадлежит извлеченный из очереди объект IO_WORKITEM |
IN PVOID pContext | Контекстный аргумент — для получения дополнительной информации, "запланированной" при вызове IoQueueWorkItem (например, указатель на IRP пакет и т.п.) |
Возвращаемое значение | void |
Таблица 10.6. Прототип вызова IoFreeWorkItem
VOID IoFreeWorkItem | IRQL<=DISPATCH_LEVEL |
Параметры | Удаляет объект рабочего потока |
PIO_WORKITEM pWorkItem | Объект рабочего потока, созданный вызовом ранее IoAllocateWorkItem |
Возвращаемое значение | void |
Драйвер не должен делать какие-либо предположения о внутренней организации объектов IO_WORKITEM и изменять данные внутри. Для работы с этими объектами следует использовать только описанные выше вызовы. |
Системный апплет "Производительность"
Более глубоко лежащим средством мониторинга системы и отдельных процессов и служб является апплет "Производительность" системной панели "Администрирование", вызываемой запускающей последовательностью Пуск — Настройка — Панель управления — Администрирование — Производительность.
Рабочее окно данного системного апплета показано на рисунке 2.9. По нажатию правой кнопки мышки появляется меню, допускающее добавление новых графиков в окно работающего апплета ("Добавить счетчики").Сочетание параметров, которые можно просматривать таким образом, настолько велико, что для их описания потребовалась бы отдельная глава. Для просмотра доступны параметры функционирования памяти, процессора, КЭШа, протоколов IP, TCP, UDP, отдельных процессов и сервисов, включая их отдельные потоки, — всего чуть менее полусотни информативных единиц. Для каждой такой единицы возможен просмотр достаточно большого набора счетчиков (как правило, не менее полутора десятков). Например, для процессора среди счетчиков, доступных для просмотра, можно назвать счетчики числа прерываний в секунду, процента времени бездействия, процента времени работы в пользовательском и привилегированном режимах, счетчика DPC процедур, поставленных в очередь, в секунду.
Краткие пояснения, касающиеся смысла указываемых счетчиков, можно получить при выборе счетчика по нажатию кнопки "Объяснение".
Рис. 2.9 Рабочее окно системного Диспетчера Задач |
Словарь разработчика драйвера
Впервые открывая документацию, поставляемую вместе с пакетом Microsoft DDK, новичок неожиданно обнаруживает, что понять ее практически невозможно - периодически на форумах в Интернете возникают удивленные сообщения об этом феномене. Между тем, никакого феномена нет. Документация DDK - это весьма лаконичное "издание" справочного характера (с коварным множеством "любезных" переходов по ссылкам), и изучение предмета по нему во многом похоже на изучение медицины по энциклопедии.
Для русскоязычного читателя-новичка, даже хорошо владеющего языком оригинала, положение усугубляется тем, что для многих терминов отсутствуют устоявшиеся отечественные аналоги. Особенно печально положение некоторых терминов, представленных в оригинале словосочетаниями. Порой, при самом добросовестном переводе так и не получается хорошего определения, поскольку добросовестный 'пословник' дает смысловые оттенки, как раз затрудняющие понимание внутренней логики предмета обсуждения (как, например, это происходит с термином 'dispatch routine').
Словарь терминов, который приводится ниже, призван уравнять шансы читателя в борьбе со сложностью материала и дать стартовые сведения новичкам. Статьи этого мини-словаря расположены в порядке, при котором более заметны внутренние взаимосвязи терминов. Некоторые термины, как, например 'IRP', переведены, но далее в книге будут использоваться в оригинальном виде, который компактнее выглядит и обеспечивает привыкание к синтаксису будущего программного кода.
Случай 1: Ошибочная ситуация
В случае, если процедура диспетчеризации не может разрешить проблем, возникших при обработке запроса, ей необходимо отклонить запрос и сообщить об этом вызывающей стороне. Следующие шаги необходимо предпринять при отражении "запроса".
Соответствующий код ошибки сохраняется в поле Status в блоке IoStatus пакета IRP и производится обнуление поля Information.
Производится вызов IoCompleteRequest для того, чтобы завершить обработку пакета IRP (без повышения приоритета).
Рабочая процедура, возвращая управление, должна возвратить тот же код ошибки, что был помещен в поле IoStatus.Status пакета IRP.
NTSTATUS WriteRequestHandler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { : // Запрос не поддерживается данным устройством (например): pIrp->IoStatus.Status = STATUS_NOT_SUPPORTED; pIrp->IoStatus.Information = 0; // Ни одного байта не передано IoCompleteRequest(pIrp, IO_NO_INCREMENT); // без изменения приоритета return STATUS_NOT_SUPPORTED; }
Вызов IoCompleteRequest будет подробно рассмотрен в следующей главе, но сейчас следует отметить, что после него область памяти, занятая под собственно пакет IRP может оказаться свободной. Поэтому категорически нельзя экономить и писать операторы типа "return pIrp->IoStatus.Status;", впрочем, как и обращаться по адресу pIrp в каких бы то ни было целях после вызова IoCompleteRequest.
Случай 2: Завершение работы над IRP запросом
Некоторые запросы могут быть полностью обработаны без обращения к физическому устройству, за которое отвечает драйвер, например, получение дескриптора устройства или настройки режима работы самого драйвера. В этом случае рабочая процедура должна выполнить следующие действия:
Поместить код успешного завершения в поле Status в блоке IoStatus пакета IRP и указать приемлемое значение в поле Information.
Выполнить вызов IoCompleteRequest, чтобы освободить пакет IRP без повышения приоритета.
Возвратить управление с кодом STATUS_SUCCESS.
NTSTATUS CloseRequestHandler( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp) { : pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = 0; IoCompleteRequest ( pIrp, IO_NO_INCREMENT ); return STATUS SUCCESS; }
Случай 3: Работа через очереди IRP пакетов
Разумеется, простейший драйвер может инициализировать свое устройство в процедуре DriverEntry, в обработчике запросов IRP_MJ_READ сразу же считывать данные из устройства (например, LPT порта). Тем не менее, полномасштабный ритуал работы с подсистемой ввода/вывода Windows (то есть Диспетчером ввода/вывода) диктует иную последовательность действия. Рабочая процедура должна поместить пакет IRP в очередь для последующей обработки процедурой StartIO и сразу же возвратить Диспетчеру ввода/вывода сообщение о том, что обработка IRP не завершена, а именно:
Выполнить вызов IoMarkIrpPending — чтобы информировать Диспетчера ввода/вывода о том, что пакет IRP поставлен в очередь на обработку.
Выполнить вызов IoStartPacket, чтобы поместить пакет IRP в системную очередь для последующей его обработки процедурой StartIO. Драйвер может реализовывать и свои очереди IRP пакетов.
Возвратить управление из рабочей процедуры с кодом завершения STATUS_PENDING.
Фрагмент кода, приведенный ниже, демонстрирует, как рабочая процедура размещает IRP запрос в очереди на обработку.
NTSTATUS ReadRequestHandler( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp ) { : // IRP "в работе", но работа с ним будет происходить // через очередь пакетов (в данном случае - системную): IoMarkIrpPending( pIrp );
// Четвертый параметр позволяет указывать процедуру удаления // CancelRoutine, что подробно обсуждается в конце главы 9. IoStartPacket( pDeviceObject, pIrp, 0, NULL );
return STATUS_PENDING; }
Некоторые источники указывают, что Диспетчер ввода/вывода автоматически завершает запросы, не помеченные кодом STATUS_PENDING, сразу же после получения управления из рабочей процедуры. Возможно, данный автоматически запускающийся механизм когда-то и работал именно так. Однако в доступных на сегодня версиях Windows это не подтверждается, то есть для нормального завершения обработки IRP пакета необходимо, чтобы текущий драйвер производил вызов IoCompleteRequest
в конце обработки IRP пакета в своей рабочей процедуре (если только он не помечен как отложенный в системную очередь, STATUS_PENDING). К тому же, при этом условии будут запущены все процедуры завершения в вышестоящих драйверах, если таковые, конечно, имеются. Подробнее эти вопросы рассмотрены в главе 9.
Соглашения об именах функций драйвера и системных вызовов
Не последнюю роль в разработке программного продукта играют правила составления идентификаторов и размещения исходного текста в файлах. Если хорошие правила вырабатываются годами участия в крупных проектах, то плохие каждый может сформулировать без труда: называйте функции и переменные случайным образом, а еще лучше — x1, x2, function133 и т.п.
Поскольку Microsoft следует определенным правилам составления имен своих вызовов, то системные функции отличить в тексте драйвера несложно: они имеют префикс из числа представленных в таблице 4.1, например, HalGetInterruptVector, вызов, относящийся к множеству аппаратных абстракций, что обозначено префиксом Hal. Правда, Microsoft все-таки не до конца последовательно проводит эту линию в жизнь. Например, в именах функций, обслуживающих объект адаптера (структура DMA_OPERATIONS), нет уже никаких префиксов такого типа, как, скажем, в именах FreeAdapterChannel, CalculateScatterGatherList
и AllocateCommonBuffer.
В пакете DDK все имена типов данных и все макроопределения вводятся прописными (большими) буквами, например, PVOID, PHYSICAL_ADDRESS, READ_PORT_UCHAR. Упоминание о так называемой венгерской нотации в DDK практически не встречается, однако, Microsoft рекомендует разработчикам драйверов использовать собственные короткие префиксы для обозначения собственных функций и, возможно, идентификаторов переменных.
Сохранение отладочного кода в исходном тексте драйвера
Хорошим приемом в деле доводки драйвера является сохранение отладочных фрагментов кода на их месте, даже если для потребителя была собрана релизная версия (free версия без отладочных инструкций). Все это несложно сделать при помощи директив условной компиляции, и при возврате к выявлению более глубоких ошибок и повторному тестированию этот прием окажет отличную поддержку.
Утилита BUILD использует символ времени компиляции DBG, который может быть использован при составлении условно компилируемых фрагментов. В отладочной версии (checked build) этому символу присвоено значение 1, в версии free значение DBG равно 0. При внесении отладочного кода в драйвер следует ограничивать его рамками директив условной компиляции типа #if DGB и #endif.
Советы по работе с аппаратурой
Перед началом разработки нового драйвера, постарайтесь узнать о новом устройстве как можно больше. Постарайтесь выделить информацию, касающуюся, по крайней мере, следующих вопросов:
Используемая шинная архитектура.
Регистры управления и способ доступа к ним из программного обеспечения.
Способ сообщения о текущем состоянии устройства и ошибках.
Поведение устройства в связи с использованием прерываний.
Механизмы передачи данных.
Память, реализованная в устройстве.
Создание IRP пакетов "с нуля"
В отдельных редких случаях, когда нужно формировать запрос, отличающийся от операций чтения, записи, очистки буферов, операции shutdown или IOCTL операций, единственным вариантом остается выделение памяти под пакет IRP и заполнение его нужными данными "вручную".
Для формирования пакетов IRP можно использовать функцию IoAllocateIrp, которая выполняет выделение памяти под пакет IRP в зонном буфере Диспетчера ввода/вывода, после чего выполняет некоторые действия по инициализации полей в выделенной области.
Попробуем отказаться и от этой услуги Диспетчера ввода/вывода и создать пакет IRP "совершенно с нуля" на примере IRP пакета для буферизованного ввода/вывода.
В данном случае память под IRP пакет выделяется в нестраничном пуле при помощи вызова ExAllocatePool, а затем производится инициализация необходимых полей внутри созданной области. Общая инициализация выделенной области по типу "IRP пакет" должна быть выполнена при помощи вызова IoInitializeIrp. Установка полей в той ячейке стека IRP пакета, которую будет разбирать драйвер-получатель (владеющий устройством pTargetDevice), и буферных областей для передачи данных возлагается на текущий драйвер.
Предполагается, что текущий драйвер получил IRP пакет pOriginalIrp и должен сформировать IRP пакет для запроса на чтение (хотя именно его проще было бы сформировать описанными ранее вызовами).
. . . . . #define BUFFER_SIZE (1024) CCHAR nOfRequiredStackLocs = pTargetDevice->StackSize; USHORT irpSize = IoSizeOfIrp(nOfRequiredStackLocs); PIO_STACK_LOCATION pTagDevIrpStackLocation;
PIRP pCreatedIrp = (PIRP) ExAllocatePool( NonPagedPool, irpSize ); IoInitializeIrp( pCreatedIrp, irpSize, nOfRequiredStackLocs);
// Получаем указатель на ячейку стека IRP, которая после вызова // IoCallDriver будет ассоциирована с нижним драйвером: pTagDevIrpStackLocation = IoGetNextIrpStackLocation( pCreatedIrp );
// Подразумевая операцию чтения, устанавливаем поля ячейки: pTagDevIrpStackLocation->MajorFunction = IRP_MJ_READ; pTagDevIrpStackLocation->Parameters.Read.Length = BUFFER_SIZE; pTagDevIrpStackLocation->Parameters.Read.ByteOffset.QuadPart = 0i64;
// B запросе IRP_MJ_READ список MDL не может использоваться. // Передаем собственный буфер в качестве системного, // требующегося при данном типе запросов: PVOID newBuffer = ExAllocatePool ( NonPagedPool, BUFFER_SIZE ); pCreatedIrp -> AssociatedIrp.SystemBuffer = newBuffer;
// Если вызываемое устройство имеет свойство (флаг) DO_DIRECT_IO: if( pTargetDevice->Flags & DO_DIRECT_IO ) { // Описание IoAllocateMdl см. в таблице 7.19. Поскольку третий // параметр равен FALSE, указатель на созданный MDL список будет // сразу занесен в поле pCreatedIrp-> MdlAddress PMDL pNewMdl = IoAllocateMdl ( newBuffer, BUFFER_SIZE, FALSE, FALSE, pCreatedIrp);
// для буфера в нестраничной памяти: MmBuildMdlForNonPagedPool( pNewMdl); }
// Копируем информацию о потоке инициатора вызова: pCreatedIrp -> Tail.Overlay.Thread = pOriginalIrp -> Tail.Overlay.Thread;
// Устанавливаем процедуру завершения обработки сформированного IRP IoSetCompletionRoutine ( pCreatedIrp, MyIoCompletionRoutine, NULL, TRUE, TRUE, TRUE );
// Передаем созданный пакет драйверу нижнего уровня IoCallDriver ( pTargetDevice, pCreatedIrp ); . . . .
Неочевидность приведенных выше манипуляций с ячейкой стека IRP пакета говорит о том, что необходимо в совершенстве владеть тонкостями формирования пакетов для тех типов запросов, которые вам захочется создавать самостоятельно.
В процедуре завершения следует переместить полученные "снизу" данные соответствующему получателю наверху. Разумеется, в процедуре завершения необходимо выполнить и действия по освобождению ресурсов, присвоенных IRP пакету, то есть выполнить освобождение памяти, занятой для системного буфера и собственно IRP пакета.
NTSTATUS MyIoCompletionRoutine(IN PDEVICE_OBJECT pThisDeviceObject, IN PIRP pIrp, IN PVOID pContext ) { . . . // Очистка структуры MDL списка: IoFreeMdl( pIrp->MdlAddress );
// Освобождение специального буфера: IoFreePool ( pIrp->AssociatedIrp.SystemBuffer );
// Освобождение собственно IRP: IoFreeIrp ( pIrp );
return STATUS_MORE_PROCESSING_REQUIRED; }
Следует обратить внимание на то, что освобождение памяти выполняется при помощи вызова IoFreeIrp, a нe ожидаемого ExFreePool. В данном случае так можно поступать потому, что Диспетчер ввода/вывода получает из определенного поля IRP информацию о том, получен ли данный пакет из нестраничного пула или из специального зонного буфера, которым распоряжается только Диспетчер ввода/вывода. |
При всей сложности самостоятельного создания IRP пакетов с нуля, в этом есть одно важное преимущество — драйвер контролирует количество создаваемых ячеек стека IRP пакета. В том числе — дополнительных, которые могут оказаться в некоторых случаях незаменимыми для хранения специфичной (для данного IRP пакета и для данного драйвера) информации в течении времени жизни этого IRP пакета.
Ниже приводятся описания прототипов использованных функций.
Таблица 9.15. Описание прототипа функции IoAllocateIrp
PIRP IoAllocateIrp | IRQL <= DISPATCH_LEVEL |
Параметры | Формирует IRP пакет с выделением памяти (не требует последующего вызова IoInitializeIRP) |
IN CCHAR StackSize | Количество ячеек стека во вновь создаваемом IRP пакете |
IN BOOLEAN ChargeQuota | FALSE |
Возвращаемое значение |
Адрес нового пакета IRP либо NULL — невозможно создать новый IRP |
VOID IoInitializeIrp | IRQL <= DISPATCH_LEVEL |
Параметры | Формирует IRP пакет в ранее выделенной области памяти (не должна использоваться для пакетов, созданных вызовом IoAllocateIrp) |
IN PIRP pIrp | Указатель на область, используемую под IRP |
IN USHORT PacketSize | Заранее вычисленный общий размер IRP пакета (можно использовать вызов IoSizeOfIrp) |
IN CCHAR StackSize | Количество ячеек стека во вновь создаваемом IRP пакете |
Возвращаемое значение | void |
Таблица 9.17. Описание прототипа функции IoFreeIrp
VOID IoFreeIrp | IRQL <= DISPATCH_LEVEL |
Параметры | Очищает и освобождает IRP пакеты, созданные вызовами IoAllocateIrp или IoBuildAsynchronousFsdRequest |
IN PIRP pIrp | Указатель на освобождаемый IRP пакет |
Возвращаемое значение | void |
или IoBuildDeviceIoControlRequest, освобождаются самим Диспетчером ввода/вывода, когда драйвер завершает обработку такого пакета вызовом IoCompleteRequest. Освобождения пакетов, сделанных нестандартными способами (например, с помощью ExAllocatePool) выполняет сам драйвер.
Таблица 9.18. Описание прототипа функции IoSizeOfIrp
USHORT IoSizeOfIrp | IRQL — любой |
Параметры | Определяет размер IRP пакета, как если бы он имел StackSize ячеек стека |
IN CCHAR StackSize | Предполагаемое число ячеек стека IRP пакета |
Возвращаемое значение | Размер в байтах |
Создание IRP пакетов вызовами IoBuild(A)SynchronousFsdRequest
Как уже, наверное, понял читатель, пакеты IRP можно создавать с нуля (обладая только областью памяти достаточного размера), но можно и прибегнуть к помощи рекомендованных системных вызовов IoBuildSynchronousFsdRequest, IoBuildAsynchronousFsdRequest и IoBuildDeviceControlRequest. Первые два вызова предназначены для конструирования IRP пакетов с кодами IRP_MJ_READ, IRP_MJ_WRITE, IRP_MJ_FLUSH_BUFFERS и IRP_MJ_SHUTDOWN, вполне пригодные для использования во всех драйверах, несмотря на устрашающий суффикс Fsd. Последний из этих вызовов, IoBuildDeviceControlRequest, предназначен для конструирования таких IRP пакетов, как если бы они были инициированы пользовательским API вызовом DeviceIoControl, то есть с кодом IRP_MJ_DEVICE_CONTROL или IRP_MJ_INTERNAL_DEVICE_CONTROL
Таблица 9.13. Описание прототипа функций IoBuild(A)SynchronousFsdRequest
PIRP IoBuildSynchronousFsdRequest PIRP IoBuildAsynchronousFsdRequest | IRQL == PASSIVE_LEVEL IRQL <= DISPATCH_LEVEL |
|
Параметры | Построение IRP пакета (выделение памяти и настройка полей) | |
IN ULONG MajorFunction | • IRP_MJ_PNP или • IRP_MI_READ или • IRP_MJ_WRITE или • IRP_MJ_FLUSH_BUFFERS или • IRP_MJ_SHUTDOWN |
|
IN PDEVICE_OBJECT pTargetDevice | Объект устройства, которому отдается IRP | |
IN OUT PVOID pBuffer | Адрес буфера данных ввода/вывода | |
IN ULONG uLenght | Размер порции данных в байтах | |
IN PLARGE_INTEGER StartingOffset | Смещение в устройстве, где начинается/продолжается операция ввода/вывода | |
Только для IoBuildSynchronousFsdRequest:
IN PREVENT pEvent |
Объект события, используемый для сигнализации об окончании ввода/вывода (должен быть инициализирован к моменту вызова). Объект переходит в сигнальное состояние, когда нижний драйвер завершил обработку данного IRP пакета. | |
OUT PIO_STATUS_BLOCK Iosb | Для получения завершающего статуса операций ввода/вывода | |
Возвращаемое значение | • Не NULL — адрес нового пакета IRP • NULL — невозможно создать новый IRP |
Число ячеек, создаваемых в стеке ввода/вывода, размещающемся в пакете IRP, равно значению, указанному в поле pTargetDevice->StackSize.
В данном случае нет простого способа создать дополнительную ячейку в стеке пакета IRP собственно для самого вызывающего драйвера.
Значения аргументов Buffer, Length и StartingOffset требуются для операций чтения и записи. Для операций flush и shutdown они должны быть установлены равными 0.
Нужные значения в области Parameters ячейки стека, соответствующей нижнему драйверу, устанавливаются автоматически, то есть нет необходимости передвигать указатель стека. Для запросов чтения или записи эти функции еще выделяют системное буферное пространство или выполняют построение MDL — в зависимости от того, выполняет ли вызываемое устройство (по указателю pTargetDevice) буферизованный или прямой ввод/вывод. При буферизованных операциях вывода производится также копирование содержимого буфера инициатора вызова в системный буфер, а в конце операции буферизованного ввода данные автоматически копируются из системного буфера в буферное пространство инициатора вызова.
Здесь общие черты этих двух функций заканчиваются. Начинаются различия.
Как следует из названия функции IoBuildSynchronousFsdRequest, она работает синхронно. Другими словами, поток, который выполняет вызов IoCallDriver, прекращает свою работу до тех пор, пока не завершится операция ввода/вывода в нижних драйверных слоях. Для более удобной реализации такой блокировки, в создаваемый пакет IRP в виде аргумента передается адрес инициализированного объекта события (event object). Затем, после передачи созданного пакета драйверу нижнего уровня (вызовом IoCallDriver) следует использовать функцию KeWaitForSingleObject — для организации ожидания перехода этого объекта синхронизации в сигнальное состояние. Когда драйвер нижнего уровня завершит обработку данного пакета IRP, Диспетчер ввода/вывода переведет данный объект события в сигнальное состояние, что и "разбудит" данный драйвер в нужный момент. Аргумент Iosb позволяет получить информацию о том, как завершилась обработка. Заметим, что, поскольку текущий драйвер узнает о завершении обработки нового IRP пакета от функции KeWaitForSingleObject, то он
не должен устанавливать свою процедуру завершения перед тем, как обратиться к нижнему драйверу вызовом IoCallDriver. Если же процедура завершения все-таки установлена, она всегда должна возвращать STATUS_SUCCESS.
Пакеты, созданные функцией IoBuildSynchronousFsdRequest, должны освобождаться только косвенно — в результате вызова IoCompleteRequest
после получения сигнала от объекта события, а Диспетчер ввода/вывода уже сам очистит и освободит память, занятую IRP пакетом. Это включает освобождение системных буферных областей или MDL, выделенных для использования в обработке этого IRP. Использовать IoFreeIrp нельзя, так как такой IRP пакет участвует в очереди, организованной для пакетов, ассоциированных с данным программным потоком. Применение к нему вызова IoFreeIrp ранее, чем он будет удален из данной очереди, приведет к краху системы. Кроме того, во избежание неприятностей, следует следить за тем, чтобы объект события существовал к моменту, когда Диспетчер ввода/вывода соберется перевести его в сигнальное состояние.
Соответственно, фрагмент кода, который создает синхронный IRP и адресует его объекту устройства pTargetDeviceObject в нижнем драйвере, мог бы выглядеть следующим образом:
PIRP pIrp; KEVENT Event; IO_STATUS_BLOCK iosb; KeInitializeEvent(&Event, NotificationEvent, FALSE); pIrp = IoBuildSynchronousFsdRequest(IRP_NJ_Xxx, pTargetDeviceObject, . . . &Event, &iosb); status = IoCallDriver(pTargetDeviceObject, pIrp); if( status == STATUS_PENDING ) { // Ожидаем окончания обработки в нижних слоях KeWaitForSingleObject(&Event, Executive, KErnelMode, FALSE,NULL); status = iosb.Status; } . . .
В отличие от пакетов IRP, производимых по запросу синхронной версии, функция IoBuildAsynchronousFsdRequest конструирует пакеты, которые не освобождаются автоматически по окончании работы над ним в нижнем драйвере. Вместо этого, драйвер, создающий "асинхронный" пакет IRP должен обязательно подключить свою процедуру завершения, которая и должна выполнять вызов IoFreeIrp.
Процедура завершения и должна выполнить очистку IRP с освобождением выделенных ему системных буферных областей или MDL, а затем и освобождения памяти, занятой под структуру самого IRP пакета. В данном случае, процедура завершения должна возвратить значение STATUS_MORE_PROCESSING_REQUIRED. Соответствующий пример кода может выглядеть следующим образом:
. . . . . PIRP pIrp; IO_STATUS_BLOCK iosb; pIrp = IoBuildAsynchronousFsdRequest(IRP_NJ_Xxx, pTargetDeviceObject, . . . &iosb); IoSetCompletionRoutine( pIrp, (PIO_COMPLETION_ROUTINE) MyCompletionRoutine, pThisDevExtension, TRUE,TRUE,TRUE); //Чтобы целевое устройство не "растворилось" за время обработки IRP: ObReferenceObject(pTargetDeviceObject); status = IoCallDriver(pTargetDeviceObject, pIrp); ObDereferenceObject(pTargetDeviceObject); . . . . . // Процедура завершения, зарегистрированная ранее NTSTATUS MyCompletionRoutine( PDEVICE_OBJECT pThisDevice, PIRP pIrp, VOID pContext ) { // Действия по очистке IRP . . . . IoFreeIrp( pIrp ); return STATUS_MORE_PROCESSING_REQUIRED; }
Драйверы, которые реализуют блокирующийся механизм работы (как это получается при синхронизации по объекту события), могут привести к деградации системы. Такое может случиться, если они будут выполнять вызовы IoCallDriver
с повышенных уровней IRQL. В этом случае они могут остановиться на неопределенно долгое время, ожидая отклика с нижних уровней. Это противоречит общей философии построения Windows NT 5. Видимо, поэтому разработчики Windows искусственно затруднили построение синхронных IRP пакетов на повышенных уровнях IRQL тем, что вызов IoBuildSynchronousFsdRequest можно сделать только с уровня IRQL, равного PASSIVE_LEVEL.
Кроме того, объект события, используемый для организации ожидания, когда же закончится обработка пакета IRP на нижних уровнях, должен использоваться с максимальной осторожностью, поскольку при использовании такого драйвера в многопоточной манере могут возникнуть сложные ситуации. Допустим, два программных потока одного и того же пользовательского процесса делают запрос на запись с использованием одного и того же дескриптора (иными словами, делают запрос к одному и тому же драйверу).Тогда рабочая процедура WriteRequestHandler выполняется в контексте первого потока и останавливается в том месте, где она желает дождаться сигнала от объекта события. Затем, та же самая процедура WriteRequestHandler, выполняемая в контексте другого потока, использует повторно тот же самый объект события для обработки другого запроса. Когда запускаются оба потока, то ни один из них не может быть уверен, чей же конкретно пакет IRP обработан, поскольку при окончании обработки любого из IRP с одинаковым успехом возникает сигнал от объекта события. Решение может состоять в том, чтобы подстраховать объект события при помощи быстрого мьютекса или даже создавать новые объекты события для каждого вновь конструируемого IRP пакета.
Создание IRP пакетов вызовом IoBuildDeviceIoControlRequest
Последняя из трех упомянутых, предназначенных функций для создания IRP пакетов, IoBuildDeviceIoControlRequest (таблица 9.14) также призвана облегчить этот процесс. Этот весьма полезный вызов предназначен для построения пакетов IRP, обслуживающих ввод/вывод устройств с большими вариациями в поведении и с использованием пользовательских IOCTL кодов запросов.
Таблица 9.14. Описание прототипа функции IoBuildDeviceIoControlRequest
PIRP IoBuildDeviceIoControlRequest | IRQL == PASSIVE_LEVEL | |
Параметры | Формирует IRP пакет (с выделением памяти), описывающий обращение с IOCTL запросом | |
IN ULONG IoControlCode | Код IOCTL, принимаемый (допускаемый) к обработке целевым устройством | |
IN PDEVICE_OBJECT pTargetDevice | Объект устройства, которому предназначен формируемый пакет IRP | |
IN PVOID pInputBuffer | Адрес буфера ввода/вывода, передаваемого драйверу нижнего уровня | |
IN ULONG inputLenght | Длина буфера pInputBuffer в байтах | |
OUT PVOID pOutputBuffer | Адрес буфера ввода/вывода для данных, возвращаемых драйвером нижнего уровня | |
IN ULONG outputLenght | Длина буфера pOutputBuffer в байтах | |
IN BOOLEAN InternalDeviceIoControl | TRUE — буден сформирован IRP пакет с кодом IRP_MJ_INTERNAL_DEVICE_CONTROL FALSE — с кодом IRP_MJ_DEVICE_CONTROL |
|
IN PREVENT pEvent | Объект события (event object), используемый для сообщения об окончании ввода/вывода | |
OUT PIO_STATUS_BLOCK pIosb | Для получения завершающего статуса операций ввода/вывода | |
Возвращаемое значение | • Адрес нового пакета IRP либо • NULL — невозможно создать новый IRP |
Следует также отметить, что этот вызов может конструировать IRP как с синхронным способом обработки, так и асинхронным. Для получения "синхронного" IRP в функцию необходимо просто передать адрес инициализированного объекта события. После того как IRP пакет будет передан нижнему драйверу вызовом IoCallDriver, следует использовать KeWaitForSingleObject для организации ожидания сигнала от этого объекта события. Когда драйвер нижнего уровня завершит обработку IRP, Диспетчер ввода/вывода переведет объект события в "сигнальное" состояние, и в результате будет разбужен драйвер, который "организовал" весь этот процесс.
Блок данных по указателю pIosb сообщает об окончательном состоянии пакета IRP. Так же, как и в случае с IoBuildSynchronousFsdRequest, следует аккуратнее работать в многопоточном режиме.
Диспетчер ввода/вывода автоматически выполняет очистку и освобождение IRP пакетов, созданных по вызову IoBuildDeviceIoControlRequest по завершении их обработки, включая подключенные к этому пакету системные буферные области или MDL. Для запуска такой очистки драйвер должен просто сделать вызов IoCompleteRequest.
Обычно, нет необходимости подключать процедуру завершения к пакетам IRP такого типа, если только у драйвера нет необходимости выполнить какие-нибудь специфические действия пост-обработки. Но уж если такая процедура подключена, то она должна возвращать значение STATUS_SUCCESS, чтобы позволить Диспетчеру ввода/вывода выполнить очистку этого пакета по окончании процедуры завершения.
Метод буферизации, который указан в IOCTL коде, влияет на формирование IRP пакета. В том случае, если IOCTL код описан как METHOD_BUFFERED, внутри вызова IoBuildDeviceIoControlRequest выполняется выделение области нестраничной памяти, куда производится копирование содержимого буфера по адресу pInputBuffer. Когда обработка IRP завершается, содержимое буфера в нестраничном пуле автоматически копируется в область памяти по адресу pOutputBuffer.
В случае, если IOCTL код содержит флаги METHOD_OUT_DIRECY или METHOD_IN_DIRECT, то IoBuildDeviceIoControlRequest всегда выполняет построение MDL списка для буфера pOutputBuffer и всегда использует буфер в нестраничной памяти для буфера pInputBuffer, независимо
от того, указан ли METHOD_IN_DIRECT или METHOD_OUT_DIRECT. В общем-то, формирование IRP пакета в обоих случаях происходит совершенно аналогично тому, как если бы в Win32 обрабатывался вызов DeviceIoControl, поступивший из приложения пользовательского режима.
Специальные драйверные архитектуры
Microsoft предлагает специфические драйверные архитектуры для нескольких типов или классов устройств, а именно:
Видеодрайверы.
Драйверы принтеров.
Драйверы устройств мультимедиа.
Сетевые драйверы.
Рассмотрение этих архитектур выходит за рамки задач данной книги, поэтому ограничимся лишь данным перечислением.
Спин-блокировки
Чуть позже будет рассмотрено использование изменения уровня IRQL для синхронизации доступа к данным. Однако в многопроцессорных системах изменение IRQL одного процессора никак не сказывается на значении IRQL программного кода, исполняемого на другом процессоре. То есть IRQL предоставляет способ защиты совместно используемых данных только при работе с одним процессором. Для безопасного доступа к данным в мультипроцессорной среде, Window NT использует синхронизационные объекты, называемые спин-блокировками (spin locks).
Спин-блокировка является, по сути, объектом типа мьютекс, однако, с более широкими полномочиями. Когда фрагмент кода, работающего на уровне режима ядра, собирается обратиться к одной из "охраняемых" структур данных, он должен сначала выполнить запрос на владение спин-блокировкой. Так как только один из процессоров в каждый момент времени имеет право собственности на объект спин-блокировки, то таким образом и обеспечивается разделение доступа к охраняемым данным между потоками, работающими на разных процессорах.
Если рассматривать функционально полную группу вызовов KeInitializeSpinLock
— KeAcquireSpinLock — KeReleaseSpinLock, то можно сказать, что объект спин-блокировки должен запрашиваться из программного кода, работающего на уровнях IRQL ниже DISPATCH_LEVEL, а освобождается на уровне IRQL, равном DISPATCH_LEVEL.
Таблица 10.44. Прототип вызова KeInitializeSpinLock
VOID KeInitializeSpinLock | IRQL == любой |
Параметры | Инициализирует объект спин-блокировки |
IN PKSPIN_LOSK pSpinLock | Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта спин-блокировки |
Возвращаемое значение | void |
Ограничение на выделение памяти под объект спин-блокировки только из пула нестраничной памяти проистекает из того, что программный код, получивший владение объекта спин-блокировки, начинает работать на уровне DISPATCH_LEVEL.
После получения владения объектом спин-блокировки в результате вызова KeAcquireSpinLock
(таблица 10.45), программный код данного потока получает уровень IRQL равный DISPATCH_LEVEL, что автоматически означает торможение всех программных потоков, выполняемых на данном процессоре с IRQL ниже DISPATCH_LEVEL. Таким образом, на этом процессоре реализуется синхронизация доступа к данным методом повышения IRQL. (Разумеется, это не спасет, если за данными обратятся из процедуры обработки прерывания, работающей на более высоких уровнях DIRQL.)
Таблица 10.45. Прототип вызова KeAcquireSpinLock
VOID KeAcquireSpinLock | IRQL <= DISPATCH_LEVEL |
Параметры | Инициализирует объект спин-блокировки |
IN PKSPIN_LOCK pSpinLock | Указатель на место в нестраничной памяти, подготовленное инициатором данного вызова для объекта спин-блокировки |
OUT PKIRQL pOldIrql | Место для сохранения старого значения уровня IRQL для использования позже в вызове KeReleaseSpinLock |
Возвращаемое значение | void |
00000015 0.00203462 -Example- IRQLs are old=2 ...
хотя изначально обработчик IOCTL запросов драйвера вызывается драйвером на уровне PASSIVE_LEVEL (0). Эта неявная работа вызова KeAcquireSpinLock
приводит к тому, что при обработке запроса IOCTL_MAKE_SYSTEM_CRASH в драйвере Example.sys не происходит перехвата исключительной ситуации конструкцией try-exception, нормально работающей при уровне PASSIVE_LEVEL.
Таблица 10.46. Прототип вызова KeReleaseSpinLock
VOID KeReleaseSpinLock | IRQL == DISPATCH_LEVEL |
Параметры | Освобождает объект спин-блокировки |
IN PKSPIN_LOCK pSpinLock | Указатель на освобождаемый объект спин-блокировки |
IN PKIRQL pNewIrql | Устанавливаемый данным вызовом уровень IRQL (предполагается, что это — сохраненное ранее вызовом KeAcquireSpinLock значение) |
Возвращаемое значение | void |
Попытка получить объект спин- блокировки на процессоре, который уже владеет этим объектом, приводит к надежному "замерзанию" процессора. В драйвере Example.sys такая ситуация легко моделируется следующим образом. Если при выходе из обработчика IOCTL запросов не освободить объект спин-блокировки MySpinLock, то при следующем входе в этот код система "подвисает": процессор ждет, когда он сам освободит объект спин-блокировки.
Чревато опасностями и использование конструкций, в которые заложена зависимость одновременно от нескольких спин-блокировок. По крайней мере, следует избегать получения новых спин-блокировок, когда не освобождены ранее полученные: другой поток, владея запрашиваемыми объектами, в это же время может ожидать доступа к спин-блокировкам, которые отданы первому. Такие ситуации называются еще взаимоблокировками, deadlocks.
Рассмотренный тип спин-блокировок носит название спин-блокировок выполнения (executive spin locks), и их основная область применения — охрана различных структур данных при совместном использовании несколькими программными потоками. Уровень IRQL, на котором они применимы, ограничивается значением DISPATCH_LEVEL.
Помимо рассмотренных "явно выраженных" объектов спин-блокировок выполнения (которые создаются драйвером), существуют и спин-блокировки, косвенно "ощущаемые" драйвером. Например, с объектом прерывания ассоциирован объект спин-блокировки, который практически используется при участии вызова KeSynchronizeExecution
(см. таблицу 10.14 и пояснительный текст к ней). Спин-блокировки этого типа носят название спин-блокировок прерываний (interrupt spin locks), их область применения — охрана различных структур данных на уровнях приоритетов DIRQL.
Общая схема использования спин-блокировок выполнения такова.
Следует определить, какие элементы данных должны оберегаться и как много спин-блокировок следует использовать.
Дополнительные спин-блокировки позволяют более точно настроить доступ к данным.
Однако когда для получения доступа необходимо получить более одного объекта спин-блокировок, возрастает опасность возникновения взаимоблокировок.
Резервирование памяти под структуру (структуры) типа KSPIN_LOCK в памяти нестраничного типа. Имеет смысл запомнить указатель на полученную область памяти в структуре расширения объекта устройства.
Инициализация спин-блокировки вызовом KeInitializeSpinLock. Этот вызов может быть сделан из кода любого уровня IRQL, хотя лучше всего это сделать там, где создается структура расширения объекта устройства (AddDevice или DriverEntry, для драйверов "в-стиле-NT").
Перед обращением к охраняемым данным следует выполнить получение прав на владение объектом спин-блокировки при помощи вызова KeAcquireSpinLock. Эта функция повышает значение IRQL до уровня DISPATCH_LEVEL, получает спин-блокировку и возвращает значение IRQL на момент перед вызовом (не восстанавливает, а возвращает значение в одном из параметров, см. таблицу 10.45), которое следует сохранить либо в локальной переменной (если это возможно по логике работы) либо в переменной в нестраничной памяти. Эта функция должна вызываться из кода на уровне ниже уровня DISPATCH_LEVEL IRQL.
Когда доступ к ресурсам завершен, следует освободить объект спин-блокировки вызовом KeReleaseSpinLock (см. таблицу 10.46), восстанавливающим ранее сохраненное значение IRQL. Это делается из кода уровня DISPATCH_LEVEL.
Дополнение к п. 5 и п. 6. Если программный код уже выполняется на уровне DISPATCH_LEVEL, то для получения спин-блокировки следует применять вызов KeAcquireSpinLockAtDpcLevel, а для освобождения, соответственно, вызов KeReleaseSpinLockFromDpcLevel, который освобождает объект спин-блокировки без изменения IRQL. Эти вызовы получают единственный параметр, указатель на объект спин-блокировки, поскольку значение IRQL теперь предполагается вполне определенным, то есть равным DISPATCH_LEVEL.
Стремление уменьшить вероятность неточного или недобросовестного программирования (в частности, исключить возможность передачи в KeReleaseSpinLock
значения уровня IRQL, отличного от значения, полученного ранее из вызова KeAcquireSpinLock) привело к тому, что в Windows XP появился новый тип спин-блокировок. Этот усовершенствованный тип объектов спин-блокировки получил название квид-спин-блокировок (вольный перевод термина queued spin locks). Практически, помимо некоторого ускорения в работе, новый тип отличается для разработчика драйвера только тем, что уровень IRQL, предшествующий запросу спин-блокировки, сохраняется без его участия — он ассоциирован теперь с дескриптором, возвращаемым при запросе на владение спин-блокировкой. Можно сказать, что логически квид-спин-блокировка состоит из простой спин-блокировки и дескриптора, полученного при доступе к спин-блокировке при помощи соответствующего вызова, см. ниже. Тем не менее, нельзя смешивать работу над спин-блокировками при помощи разнотипных вызовов (то есть KeXxxQueuedSpinLockXxx, см. ниже, и KeXxxSpinLockXxx).
Механизм использования квид-спин-блокировок предполагает следующую последовательность действий.
Создание (получение области нестраничной памяти и инициализацию вызовом KeInitializeSpinLock) объекта спин-блокировки обычным способом на уровне IRQL равном PASSIVE_LEVEL.
При необходимости синхронизировать доступ к охраняемым данным следует получить право на владение объектом спин-блокировки вызовом KeAcquireInStackQueuedSpinLock
либо вызовом KeAcquireInStackQueuedSpinLockAtDpcLevel (в зависимости от уровня IRQL кода, из которого производится вызов — если код уже выполняется на уровне DISPATCH_LEVEL, то следует применять второй вызов). Примененный вызов возвращает (по адресу, переданному во втором параметре) дескриптор полученной спин-блокировки, которая к этому моменту уже может считаться квид-спин-блокировкой. При этом текущий программный код безусловно приобретает уровень DISPATCH_LEVEL.
Выполнив необходимую работу над совместно используемыми данными, драйвер должен (максимально быстро) освободить спин-блокировку либо вызовом KeReleaseInStackQueuedSpinLock, либо вызовом KeReleaseInStackQueuedSpinLockFromDpcLevel, в зависимости от того, как был получен доступ к объекту спин-блокировки ранее.Единственным параметром, который передается этим вызовам, является дескриптор квид-спин-блокировки, полученный ранее.
Дескриптор полученной квид-спин-блокировки должен сохраняться в переменной типа KLOCK_QUEUE_HANDLE, локальной (если позволяет логика работы текущей процедуры драйвера) или размещенной в области нестраничной памяти, полученной, например, при помощи вызова ExAllocatePool.
Способы доступа к буферным областям
Диспетчер ввода/вывода предоставляет драйверам два разных метода для осуществления доступа к пользовательским буферным областям. Драйвер, при инициализации, должен сообщить Диспетчеру ввода/вывода, какой из методов он планирует использовать. Выбор определяется логикой и скоростью работы устройства, которое обслуживает драйвер.
Первая стратегия состоит в том, чтобы поручить Диспетчеру ввода/вывода копирование пользовательской буферной области в специальную область оперативной памяти, которая не является страничной и зафиксирована в физической памяти. Драйвер использует копию буфера для работы с устройством и осуществления операций ввода/вывода. По завершении работы, Диспетчер ввода/вывода обычно производит копирование данных из системного буфера в пользовательский. При запросе на операцию записи (то есть при переносе данных в обслуживаемое устройство), пользовательская область копируется в область в системном адресном пространстве до того, как последняя будет представлена драйверу. При запросе на чтение (получение данных из обслуживаемого устройства) копирование системного буфера в пользовательский производится после того, как драйвер помечает запрос как завершенный. Стандартные запросы на чтение или запись не требуют выполнения двунаправленного копирования, хотя при обработке IOCTL запросов (выполненных в пользовательских приложениях при помощи функции DeviceIoControl) это может потребоваться.
Описанный выше метод носит название buffered I/O — буферизованного ввода/вывода. Он используется медленными устройствами, которые редко работают с большими объемами данных. Этот метод не сложен для реализации в логике драйвера, но требует дополнительных временных затрат на операции по копированию буферных областей.
Вторая стратегия позволяет избежать операций копирования путем предоставления драйверу прямого доступа к пользовательской буферной области в оперативной физической памяти. В начале выполнения операции Диспетчер ввода/вывода фиксирует всю область пользовательского буфера в памяти, что предотвращает перемещение этого блока в swap-файл и саму возможность возникновения ошибки отсутствующей страницы (page fault). Затем он создает список элементов страничной таблицы, которые отображаются на область памяти выше 2 Гбайт (на системную область), таким образом, устраняя повод для переключений контекста процесса. В сложившейся ситуации, когда память и элементы страничной таблицы зафиксированы на время обработки всего запроса ввода/вывода, драйверный код может без опаски работать с пользовательским буфером. Правда, свойства этого буфера существенно изменились: теперь эта область зафиксирована в памяти (фактически стала нестраничной), а оригинальный пользовательский адрес транслирован в другой адрес, пригодный для использования только в коде, работающем в режиме ядра.
Второй метод хорошо подходит для использования драйверами быстрых устройств, выполняющих перенос больших объемов данных. Этот метод известен как direct I/O — прямой ввод/вывод. Устройства, имеющие способность к операциям DMA, практически всегда используют этот механизм.