[Previous] [Next]

Рабочие процедуры драйвера

Рабочие процедуры драйвера (в англоязычной литературе по драйверам они всегда называются dispatch routines) регистрируются драйвером во время работы DriverEntry. Фактически, таким образом драйвер сообщает Диспетчеру ввода/вывода о своих намерениях, какого типа запросы (то есть IRP_MJ_Xxx коды) он собирается поддерживать.

В тот момент, когда инициализируется запрос ввода/вывода, Диспетчер ввода/вывода в первую очередь создает пакет IRP, чтобы зафиксировать след запроса, факт его прохождения. Наряду с другой информацией, в этом IRP пакете (в ячейках стека IRP пакета) в поле MajorFunction сохраняется код IRP_MJ_Xxx для того, чтобы однозначно идентифицировать тип этого запроса.

Код IRP_MJ_Xxx используется Диспетчером ввода/вывода для того, чтобы извлечь из массива MajorFunction объекта драйвера указатель на нужную для обработки запроса процедуру драйвера. В том случае, если драйвер не поддерживает запрошенную операцию, то соответствующий элемент MajorFunction указывает на код внутри Диспетчера ввода/вывода (поскольку драйвер предоставил в таком случае Диспетчеру ввода/вывода право распорядиться по собственному усмотрению), а именно — на функцию _IoInvalidDeviceRequest, который возвращает клиенту драйвера сообщение об ошибке. Таким образом, инициатива обеспечения каждого нужного кода IRP_MJ_Xxx собственной обрабатывающей процедурой принадлежит автору драйвера.

Метод объявления процедур ввода/вывода позволяет также объявить одну процедуру (функцию драйвера) для обслуживания нескольких запросов ввода/вывода. Для этого нужно лишь поместить адрес такой универсальной (если она действительно для этого предназначена) функции в нескольких ячейках массива MajorFunction, как это было показано выше для DriverEntry фильтр-драйвера. Так как пакет IRP содержит код IRP_MJ_Xxx, то с его помощью всегда можно восстановить точный код требующегося действия.

Не следует выполнять внутри DriverEntry операции над позициями в массиве MajorFunction, соответствующих функциям ввода/вывода, не поддерживаемым драйвером. Диспетчер ввода/вывода заполняет пропущенные места в массиве MajorFunction значением указателя на функцию _IoInvalidDeviceRequest прежде, чем обращается к процедуре DriverEntry.

Все функции драйвера, участвующие в обработке запросов и подлежащие занесению в таблицу (массив) MajorFunction используют один и тот же протокол вызовов, что включает число и тип передаваемых им параметров и тип вызова. Рабочие процедуры выполняются на PASSIVE_LEVEL уровне IRQL, что означает, что они могут обращаться к ресурсам страничной памяти. (Разумеется, возможны ситуации искусственного изменения этого правила, как это было показано в примере Example.sys главы 3.)

Таблица 8.7а. Прототип, описывающий рабочую (dispatch) процедуру драйвера

NTSTATUS DispatchRoutine IRQL == PASSIVE_LEVEL
Параметры Описание
IN PDEVICE_OBJECT pDevObject Указатель на объект устройства, для которого предназначается IRP запрос
IN PRIP pIrp Указатель на пакет IRP, описывающий этот запрос
Возвращаемое значение

o STATUS_SUCCESS — запрос обработан o STATUS_PENDING — ожидается обработка запроса o STATUS_XXX — код ошибки

Набор рабочих процедур

Все драйверы обязаны поддерживать функцию, которая отвечает за обработку запроса с кодом IRP_MJ_CREATE, так как этот код генерируется в ответ на вызов пользовательского режима CreateFile. Без поддержки этого запроса пользовательское приложение не будет иметь никакой возможности получить дескриптор (handle) для доступа к драйверу устройства, а значит, и к самому устройству. Разумеется, должна существовать функция, обрабатывающая запрос с кодом IRP_MJ_CLOSE, который генерируется по обращению приложения к драйверу при помощи функции API пользовательского режима CloseHandle. Кстати заметить, вызов CloseHandle выполняется системой автоматически в момент завершения приложения пользовательского режима для всех дескрипторов ресурсов, оставленных эти приложением открытыми.

Набор остальных поддерживаемых функций зависит от особенностей устройства, которое находится "под покровительством" драйвера. Таблица 8.7б показывает взаимосвязь между кодами запросов ввода/вывода и вызовами API пользовательского режима, которые приводят к их генерации. При написании многослойных драйверов следует помнить, что вышестоящий драйвер обязан поддерживать подмножество запросов нижестоящего драйвера (драйверов), так как запрос пользовательского приложения проникает в нижние слои только через вышестоящий драйвер.

Таблица 8.7б. Коды запросов IRP и функции API пользовательского режима

IRP коды Вызовы API пользовательского режима
IRP_MJ_CREATE CreateFile
IRP_MJ_CLEANUP Очистка ожидающих обработки пакетов IRP при закрытии дескриптора при отработке вызова CloseHandle
IRP_MJ_CLOSE CloseHandle
IRP_MJ_READ ReadFile
IRP_MJ_WRITE WriteFile
IRP_MJ_DEVICE_CONTROL DeviceIoControl
IRP_MJ_INTERNAL_DEVICE_CONTROL Действия по управлению устройством, доступные только для клиентов, работающих в режиме ядра (недоступно для вызовов пользовательского режима)
IRP_MJ_QUERY_INFORMATION Передача длины файла в ответ на вызов GetFileSize
IRP_MJ_SET_INFORMATION Установка длины файла по вызову SetFileSize
IRP_MJ_FLUSH_BUFFERS Запись или очистка служебных буферов при отработке вызовов (например):
FlushFileBuffers
FlushConsolelnputBuffer
PurgeComm
IRP_MJ_SHUTDOWN

Действия, которые нужно выполнить драйверу в процессе подготовки системы к завершению работы

Диспетчер ввода/вывода вызывает процедуры диспетчеризации в ответ на поступающие запросы от пользовательских приложений или запросов от клиентов, работающих в режиме ядра. Перед вызовом Диспетчер ввода/вывода строит пакет IRP и заполняет его необходимыми данными, включая указатель на пользовательский буфер. Этот пользовательский буфер проверяется Диспетчером ввода/вывода с той целью, чтобы обеспечить доступность (для чтения или записи) страничных адресов буфера (если таковые имеются в области страничной памяти) в контексте поступающего запроса. В том случае, если поступил запрос на буферированную операцию ввода/вывода, Диспетчер сначала выделяет буфер в нестраничной памяти и, в случае запроса на запись, копирует данные из пользовательского буфера в созданный. В случае запроса на прямой ввод/вывод, Диспетчер "вызывает" целиком пользовательский буфер в физическую память и фиксирует его (не допуская его перемещения на жесткий диск, как это может быть с областями страничной памяти).

Рабочая процедура обычно может отслеживать состояние обработки запроса только с использованием пакета IRP. В том случае, если рабочая процедура использует какие-либо данные за пределами пакета IRP, драйвер должен обеспечить достаточно надежные меры по синхронизации доступа к этим данным. Это означает, что можно было бы использовать спин-блокировку для координации действий с другими процедурами, выполняющимися на уровне IRQL не выше DISPATCH_LEVEL. Для синхронизации доступа из процедур, выполняющихся на уровне кода обслуживания прерываний, следует использовать KeSyncronizeExecution.

Пакеты IRP являются данными коллективного пользования, хотя и с последовательным доступом. В частности, Диспетчер ввода/вывода использует поля объединения Parameters для того, чтобы завершить обработку запроса. Например, по окончании обработки буферизованного ввода/вывода, ему необходимо выполнить копирование данных из области памяти нестраничного пула в буфер, заданный пользовательским приложением. По завершении копирования Диспетчер ввода/вывода должен освободить буфер в нестраничном пуле. Одно из полей объединения (union) Parameters как раз указывает на этот буфер, и изменение кодом драйвера данного указателя приведет к нарушению работы системы.

Последовательность действий рабочих процедур

Конкретное поведение каждой из рабочих процедур драйвера будет зависеть от функций, которые ей будет поручено поддерживать. Тем не менее, общие обязанности этих процедур включают следующие моменты:

  1. Вызов IoGetCurrentIrpStackLocation для того, чтобы получить указатель на ячейку стека IRP пакета, относящуюся к ведению данного драйвера.
  2. Дополнительную проверку параметров, специфичную для данного типа запроса и устройства.
  3. Продолжение обработки IRP до момента успешного завершения или возникновения ошибочной ситуации, препятствующей дальнейшей обработке.

Когда рабочая процедура драйвера обрабатывает пакет IRP, существует только три возможных варианта окончания ее работы.

Случай 1: Ошибочная ситуация

В случае, если процедура диспетчеризации не может разрешить проблем, возникших при обработке запроса, ей необходимо отклонить запрос и сообщить об этом вызывающей стороне. Следующие шаги необходимо предпринять при отражении "запроса".

  1. Соответствующий код ошибки сохраняется в поле Status в блоке IoStatus пакета IRP и производится обнуление поля Information.
  2. Производится вызов IoCompleteRequest для того, чтобы завершить обработку пакета IRP (без повышения приоритета).
  3. Рабочая процедура, возвращая управление, должна возвратить тот же код ошибки, что был помещен в поле 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 запросом

Некоторые запросы могут быть полностью обработаны без обращения к физическому устройству, за которое отвечает драйвер, например, получение дескриптора устройства или настройки режима работы самого драйвера. В этом случае рабочая процедура должна выполнить следующие действия:

  1. Поместить код успешного завершения в поле Status в блоке IoStatus пакета IRP и указать приемлемое значение в поле Information.
  2. Выполнить вызов IoCompleteRequest, чтобы освободить пакет IRP без повышения приоритета.
  3. Возвратить управление с кодом 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 не завершена, а именно:

  1. Выполнить вызов IoMarkIrpPending — чтобы информировать Диспетчера ввода/вывода о том, что пакет IRP поставлен в очередь на обработку.
  2. Выполнить вызов IoStartPacket, чтобы поместить пакет IRP в системную очередь для последующей его обработки процедурой StartIO. Драйвер может реализовывать и свои очереди IRP пакетов.
  3. Возвратить управление из рабочей процедуры с кодом завершения 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.