[Previous] [Next]

AddDevice — новая процедура в драйверах модели WDM

В драйверах модели WDM функция DriverEntry все еще служит в качестве начальной точки соприкосновения операционной системы с драйвером. Однако теперь обязанности ее сократились. В частности, роль DriverEntry теперь состоит только в том, чтобы "опубликовать" (передать Диспетчеру ввода/вывода соответствующие адреса для вызова функций по адресу) остальные процедуры драйвера. Теперь DriverEntry не создает объект устройства (Device Object) для подконтрольного аппаратного обеспечения.

Обязанности по созданию объекта устройства возложены на новую функцию драйвера AddDevice, которая теперь публикуется (обратите внимание!) в структуре расширения драйвера (Driver Extension) во время работы DriverEntry. Структура расширения драйвера является строго определенной структурой — не следует путать ее с определяемой разработчиком драйвера структурой расширения устройства (Device Extension). Пример публикации AddDevice был закомментирован в теле функции DriverEntry в главе 3. Повторим еще раз:

DriverObject->DriverExtension->AddDevice = MyAddDeviceRoutine; 

Таблица 9.1. Прототип функции AddDevice

NTSTATUS AddDevice IRQL == PASSIVE_LEVEL
Параметры Описание
IN PDRIVER_OBJECT pDriverObject Указатель на объект данного драйвера
IN PDEVICE_OBJECT pPDO Указатель на объект физического устройства, созданного родительским (шинным) драйвером
Возвращаемое значение STATUS_SUCCESS или код ошибки

Основной обязанностью MyAddDeviceRoutine является создание объекта устройства (теперь уже — функционального) с использованием вызова системного IoCreateDevice и, скорее всего, подключение его к объекту физического устройства (вызовом IoAttachDevice), указатель на который поступает в параметре pPDO.

Откуда взялся объект физического устройства, указатель на который получает процедура AddDevice? Его создает шинный драйвер, когда обнаруживает подключенное PnP устройство. Зачем нужно подключаться к шинному драйверу?

Роль драйверных слоев в модели 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
Стек устройств

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

  1. Во время инсталляции операционной системы, операционная система обнаруживает и составляет список (enumerate) всех шин в Системном Реестре (System Registry). Кроме того, детектируется и регистрируется топология и межсоединения этих шин.
  2. Во время процесса загрузки производится загрузка шинного драйвера для каждой известной системе шины. Как правило, Microsoft поставляет все шинные драйверы, однако могут быть установлены и специализированные драйвера для патентованных шин данных.
  3. Одна из первоочередных задач шинного драйвера состоит в том, чтобы составить перечень (enumerate) всех устройств, подключенных к шине. Объект PDO создается для каждого обнаруженного устройства.
  4. Для каждого обнаруженного устройства в Системном Реестре определен класс устройств (class of device), который определяет верхний и нижний фильтры, если таковые имеются, так же, как и драйвер для FDO.
  5. В случае если фильтрующий драйвер или FDO драйвер еще не загружены, система выполняет загрузку и вызывает DriverEntry.
  6. Функция AddDevice вызывается для каждого FDO, которая, в свою очередь, вызывает IoCreateDevice и IoAttachDeviceToDeviceStack, обеспечивая построение стека устройств (device stack).
Рис. 9.3
Шинные и не-шинные FDO

Функция IoAttachDeviceToStack вызывается из AddDevice для того, чтобы разместить FDO в вершине (на текущий момент) стека устройств. Прототип функции IoAttachDeviceToStack описывается в таблице 9.2.

Таблица 9.2. Прототип функции IoAttachDeviceToDeviceStack

PDEVICE_OBJECT IoAttachDeviceToDeviceStack IRQL == PASSIVE_LEVEL
Параметры Выполняет подключение вновь созданного объекта устройства, pNewDevice, к стеку устройств
IN PDEVICE_OBJECT pNewDevice Указатель на подключаемый к стеку объект (созданный в данном драйвере)
IN PDEVICE_OBJECT pOldDevice Указатель на объект устройства, к которому подключается новое устройство
Возвращаемое значение

• Указатель на устройство, бывшее на вершине стека до данного вызова
• NULL (в случае ошибки, например, если драйвер целевого устройства еще не загружен)

Возвращаемый вызовом IoAttachDeviceToDeviceStack указатель может отличаться от переданного значения pOldDevice, например, по той причине, что над объектом устройства pOldDevice уже размещены объекты устройств, предоставленные фильтр-драйверами.

Как видно из таблицы 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 — код ошибки

Вызов IoGetDeviceObjectPointer может быть интересен и тем, что драйвер мог бы адресовать IRP запросы непосредственно искомому объекту устройства (при помощи IoCallDriver), создавая IRP пакеты с размером стека StackSize+1, где значение StackSize получено из найденного объекта устройства.

Вообще говоря, Диспетчер ввода/вывода автоматически устанавливает необходимое значение 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 — код ошибки

Второй способ подключения к стеку устройств через объект устройства с известным именем осуществляется при помощи вызова IoAttachDevice, прототип которого представлен в таблице 9.4.

В результате вызовов IoAttachDeviceToDeviceStack или IoAttachDevice будет найден объект устройства, находящийся на вершине стека над указанным целевым объектом (по имени или по указателю). К нему и будет подключен новый объект устройства. Соответственно, разработчик, подключающий свой объект устройства к устройству в "середине" стека и надеющийся, что таким образом через его драйвер будут "протекать" IRP запросы от вышестоящих драйверов к нижестоящим, глубоко заблуждается. На самом деле, для достижения этой цели необходимо не просто выполнить подключение к нужному объекту устройства, но и сделать это в строго определенный момент загрузки — ранее, чем будет выполнена загрузка вышестоящих драйверов, чьи запросы предполагается перехватывать. Однако рассмотрение данной проблемы выходит за рамки данной книги.

Полученный указатель на объект устройства, к которому произведено подключение, следует сохранить, поскольку он может понадобиться, например, в обработчике запросов IRP_MJ_PNP, см. ниже. Это можно сделать в структуре расширения объекта устройства.

Заключительной задачей функции AddDevice драйверов модели WDM является создание символьного имени-ссылки (symbolic link name), если это необходимо, для вновь созданных и доступных устройств. Для этого используется вызов IoCreateSymbolicLink, применение которого было продемонстрировано ранее в DriverEntry, глава 3.

Новые рабочие процедуры в WDM драйверах

Процедура AddDevice, вызываемая PnP Менеджером, только лишь производит инициализацию объекта устройства и, если необходимо, структуры данных расширения объекта устройства. В процедуре AddDevice, по правилам хорошего тона WDM модели, действия над собственно аппаратурой не должны совершаться. Но тогда остаются нерешенными две важные задачи:

Все это должен сделать драйвер по получении IRP пакета с кодом IRP_MJ_PNP. Такие IRP пакеты посылается PnP Менеджером, когда происходят события включения или выключения устройства, либо возникают вопросы по конфигурированию устройства.

Категория IRP_MJ_PNP пакетов включает запросы широкого спектра, которые детализируются суб-кодами IRP_MN_Xxx. Поскольку они пропускается через единственную рабочую процедуру, то ее обязанностью является вторичная диспетчеризация по этим суб-кодам, содержащимся в IRP пакете и описывающим специфические действия, осуществления которых ожидает PnP Менеджер.

Регистрация новой для WDM модели рабочей процедуры, которой будет поручено обрабатывать запросы IRP_MJ_PNP со всеми подтипами IRP_MN_Xxx, производится традиционным образом в процедуре DriverEntry:

pDriverObj->MajorFunction[IRP_MJ_PNP] = MyPnP_Handler; 

Пример программного кода для осуществления вторичной диспетчеризации на основе суб-кодов IRP_MN_Xxx приводится ниже.

NTSTATUS MyPnP_Handler ( IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp )
{
	// Получить указатель на текущую ячейку стека IRP пакета
	PIO_STACK_LOCATION pIrpStackLocation = IoGetCurrentIrpStackLocation( pIrp );
	
	switch (pIrpStackLocation ->MinorFunction) {
	case IRP_MN_START_DEVICE: . . .
	// Внимание. Все ветви оператора switch должны возвратить
	// результаты обработки
	. . .
	default:
	// если не поддерживается здесь, то передать запрос вниз:
	IoSkipCurrentIrpStackLocation(pIrp);
	return IoCallDriver(. . ., pIrp);
	}
}

Первым параметром вызова IoCallDriver (см. таблицу 9.5), разумеется, является указатель на объект устройства, к которому было произведено подключение в процедуре AddDevice.

Вызов IoSkipCurrentIrpStackLocation (см. таблицу 9.6) сообщает Диспетчеру ввода/вывода, что драйвер отказывается от дальнейшего участия в судьбе данного IRP пакета. В том случае, если драйвер желает получить управление над IRP пакетом в момент, когда его обработка нижними слоями драйверов будет завершена, то он должен воспользоваться системным вызовом IoCopyCurrentIrpStackLocationToNext (см. таблицу 9.7) и зарегистрировать процедуру CompletionRoutine. Она будет вызвана в соответствующий момент.

Таблица 9.5. Прототип функции IoCallDriver

NTSTATUS IoCallDriver IRQL <= DISPATCH_LEVEL
Параметры Обращается к другому драйверу с запросом, сформулированным в пакете IRP (запросы типа IRP_MJ_POWER следует выполнять при помощи вызова PoCallDriver)
IN PDEVICE_OBJECT pDevObj

Указатель на объект устройства, которому адресован IRP запрос

IN PIRP pIrp Указатель на отправляемый IRP пакет
Возвращаемое значение

• STATUS_SUCCESS
• STATUS_PENDING — в случае, если пакет требует дополнительной обработки
• STATUS_Xxx — в случае ошибки

Таблица 9.6. Прототип функции IoSkipCurrentIrpStackLocation

VOID IoSkipCurrentIrpStackLocation IRQL <= DISPATCH_LEVEL
Параметры Изменяет указатель стека IRP так, что нижестоящий драйвер будет считать текущую ячейку стека IRP своей
IN PIRP pIrp Указатель на модифицируемый IRP пакет
Возвращаемое значение void

Таблица 9.7. Прототип функции IoCopyCurrentIrpStackLocationToNext

VOID IoCopyCurrentIrpStackLocationToNext IRQL <= DISPATCH_LEVEL
Параметры Копирует содержимое ячейки стека IRP для текущего драйвера в ячейку стека для нижестоящего драйвера
IN PIRP pIrp Указатель на модифицируемый IRP пакет
Возвращаемое значение void

Процедура завершения ввода/вывода CompletionRoutine есть обратный вызов от Диспетчера ввода/вывода, который позволяет перехватить IRP пакет после того, как низкоуровневый драйвер завершит его обработку. Процедура завершения ввода/вывода регистрируется вызовом IoSetCompletionRoutine (см. таблицу 9.8).

Таблица 9.8. Прототип макроопределения IoSetCompletionRoutine

VOID IoSetCompletionRoutine IRQL <= DISPATCH_LEVEL
Параметры Выполняет регистрацию callback-функции завершения обработки IRP пакета
IN PIRP pIrp Указатель на отслеживаемый IRP пакет
IN PIO_COMPLETE_ROUTINE CompletionRoutine Функция, которая должна получить управление, когда обработка IRP будет завершена
IN PVOID pContext Параметр, который получит регистрируемая callback функция CompletionRoutine
IN BOOLEAN doCallOnSuccess

Вызывать CompletionRoutine в случае успешного завершения обработки данного IRP пакета

IN BOOLEAN doCallOnError Вызывать CompletionRoutine в случае завершения обработки данного IRP с ошибкой
IN BOOLEAN doCallOnCancel Вызывать CompletionRoutine в случае прерванной обработки данного IRP пакета
Возвращаемое значение void

Подключать собственные процедуры CompletionRoutine для детектирования окончания обработки IRP пакетов можно не только к пакетам с кодом IRP_MJ_PNP, но также ко всем остальным, отправляемым драйверам нижних слоев.

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

Таблица 9.9. Прототип функции завершения ввода/вывода CompletionRoutine

NTSTATUS CompletionRoutine IRQL == см. текст ниже
Параметры Перехватывает пакет IRP после завершения работы нижнего драйверного слоя
IN PDEVICE_OBJECT pDevObj Объект устройства (в составе данного драйвера), которому был ранее адресован данный IRP пакет
IN PIRP pIrp Указатель на IRP пакет, обработка которого только что завершена
IN PVOID pContext Аргумент, указанный в IoSetCompleteRoutine
Возвращаемое значение

STATUS_MORE_PROCESSING_REQUIRED
STATUS_SUCCESS

Однозначно предсказать, на каком уровне IRQL выполняется процедура завершения, невозможно. В том случае, если нижележащий драйвер, вызывает IoCompleteRequest (подробности — ниже) с уровня IRQL равного PASSIVE_LEVEL, то процедура завершения находящегося выше драйвера выполняется на уровне PASSIVE_LEVEL. В случае, если лежащий ниже драйвер завершает обработку IRP пакета на уровне DIPATCH_LEVEL (например, из DPC процедуры), то и процедура завершения лежащего выше драйвера выполняется на уровне DISPATCH_LEVEL.

Выполнение программного кода на уровне DISPATCH_LEVEL ограничивается системными вызовами, которые работают на этом уровне IRQL. Разумеется, следует особо позаботиться, чтобы здесь не производилась работа со страничной памятью.

Таблица 9.10. Прототип функции IoCompleteRequest

VOID IoCompleteRequest IRQL <= DISPATCH_LEVEL
Параметры Вызывается, когда драйвер желает полностью завершить обработку данного IRP пакета. Обеспечивает вызов процедур завершения всех драйверов, имеющихся над данным (см. ниже)
IN PIRP pIrp Указатель на текущий IRP пакет, обработка которого только что завершена
IN CCHAR PriorBoost Величина, на которую следует изменить приоритет потока, выполняющего обработку данного IRP пакета. Величина IO_NO_INCREMENT используется, если никаких изменений делать не нужно.
Возвращаемое значение void

Чтобы устранить упомянутую выше неоднозначность уровня IRQL работы процедуры завершения, можно прибегнуть к следующей уловке. Предположим, что мы имеем программный код рабочей процедуры. Известно также, что PnP Менеджер (как, впрочем, и Диспетчер ввода/вывода) всегда выполняет вызов рабочей процедуры драйвера на уровне PASSIVE_LEVEL. Тогда, отправляя пакет IRP нижним слоям драйвера, организуем ожидание (средствами объекта события режима ядра) не выходя из кода данной рабочей процедуры, пока отосланный нижним слоям IRP пакет не возвратится в зарегистрированную функцию CompletionRoutine. Как только это произойдет, объект события кодом функции CompletionRoutine будет переведен в сигнальное состояние, и стадия ожидания в основном потоке завершится. Таким образом, мы получим сигнал о завершении обработки пакета IRP на вполне определенном уровне IRQL, равном именно PASSIVE_LEVEL. Полностью данный метод описывается в примере ниже:

. . . . . .
// код рабочей процедуры, выполняющийся на уровне PASSIVE_LEVEL
. . . . . .
IoCopyCurrentIrpStackLocationToNext(pIrp);


// Резервируем место под объект события:
KEVENT myEvent;
// Инициализируем его, состояние не сигнальное:
KeInitializeEvent( &myEvent, NotificationEvent, FALSE );
// Регистрируем свою процедуру завершения обработки IRP пакета.
// Указатель на объект myEvent передаем как дополнительный параметр.
IoSetCompletionRoutine( pIrp,
                        MyCompleteRoutine,
                        (PVOID)&myEvent,
                        TRUE, TRUE, TRUE);

// Предположим, что указатель на объект устройства,
// к которому был подключен текущий объект устройства, был ранее
// сохранен в структуре расширения текущего объекта устройства.
PDEVICE_EXTENSION pDeviceExtension = (PDEVICE EXTENSION) pDeviceObject->DeviceExtension;
PDEVICE_OBJECT pUnderlyingDevObj = pDeviceExtension->pLowerDevice;

// Отправляем IRP пакет на обработку нижними драйверными слоями
IoCallDriver( pUnderlyingDevObj, pIrp );

// Организуем ожидание, пока не закончится работа на нижних уровнях
KeWaitForSingleObject( &myEvent,
                       Execute,
                       KernelMode,
                       FALSE,
                       NULL);

// Теперь завершаем обработку IRP пакета.
// Его адрес не изменился - pIrp.
// По "возвращении" из "ожидания" уровень IRQL остался прежним для
// данного потока.
// Поскольку Диспетчер ввода/вывода и PnP Менеджер вызывают
// рабочие процедуры драйвера на уровне PASSIVE_LEVEL, то таким
// он в данном потоке и остался.
. . .
}
NTSTATUS MyCompleteRoutine( IN PDEVICE_OBJECT pDevObj,
                            IN PIRP pIrp,
                            IN PVOID pContextArgument )
{
// Вычисляем указатель на Объект События:
PEVENT pEvent = (PEVENT) pContextArgument;
// Устанавливаем его в сигнальное состояние
KeSetEvent( pEvent, 0, FALSE );    // IRQL <=DISPATCH_LEVEL

// Пакет IRP получен. Завершение работы здесь. Но не окончательно.
return STATUS_MORE_PROCESSING_REQUIRED;
}

Рассмотрим подробнее работу вызова IoCompleteRequest. Когда некий код некоего драйвера делает этот вызов, программный код IoCompleteRequest обращается к ячейкам стека IRP пакета и анализирует, зарегистрировал ли верхний (над текущим) драйвер процедуру завершения CompleteRoutine — это как раз отмечено в стеке IRP пакета. В том случае, если таковой процедуры не обнаруживается, указатель стека поднимается и снова выполняется проверка. Если обнаружена зарегистрированная функция, то она выполняется. В том случае, если вызванная таким образом функция возвращает код завершения, отличный от STATUS_MORE_PROCESSING_REQUIRED, то указатель стека снова поднимается, и действия повторяются. Если в результате вызова получен код завершения STATUS_MORE_PROCESSING_REQUIRED, то управление возвращается инициатору вызова IoCompleteRequest.

Когда код IoCompleteRequest благополучно достигает в своем рассмотрении вершины стека, то Диспетчер ввода/вывода выполняет действия по освобождению данного IRP пакета (наряду с некоторыми другими операциями).

Отсюда несколько важных следствий.

Во-первых, если драйвер сам создал IRP пакет (подробно рассматривается ниже), то вызов IoCompleteRequest означает приказ Диспетчеру ввода/вывода заняться его освобождением — поскольку иных драйверов, процедуры завершения которых можно было бы рассматривать, просто нет.

Во-вторых, если текущий драйвер зарегистрировал свою процедуру завершения и, не вызывая нижних драйверов, сразу выполнил IoCompleteRequest, то такая процедура завершения вызвана не будет — код IoCompleteRequest ee в рассмотрение просто не примет, переходя сразу к анализу ячеек стека IRP для вышестоящих драйверов.

В-третьих, возможна ситуация, когда после выполнения вызова IoCompleteRequest драйвером в середине драйверного стека IRP пакет все еще существует. Не исключено, что он не завершен, поскольку один из верхних драйверов (который, возможно, существует) отказался это сделать в своей процедуре завершения, которую он, возможно, зарегистрировал. Однако текущий драйвер таких допущений делать не должен. Впрочем, как и всех остальных предположений относительно верхних драйверных слоев.

В любом случае, после вызова IoCompleteRequest драйвер не имеет права прикасаться к IRP пакету, который передан этому вызову как завершаемый. Кроме того, возврат кода STATUS_MORE_PROCESSING_REQUIRED — это практика зарегистрированных процедур завершения, что является "просьбой" Диспетчеру ввода/вывода возвратиться к данной процедуре завершения позже.