[Previous] [Next]

Создание 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, поступивший из приложения пользовательского режима.

Создание 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 все равно должны быть инициализированы вызовом IoInitializeIrp, однако, применять к ним вызов IoFreeIrp нельзя, поскольку тот, скорее всего, нарушит учет памяти в драйвере.

При всей сложности самостоятельного создания 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

Таблица 9.16. Описание прототипа функции IoInitializeIrp

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

Как было сказано ранее, пакеты, созданные IoBuildSynchronousFsdRequest или IoBuildDeviceIoControlRequest, освобождаются самим Диспетчером ввода/вывода, когда драйвер завершает обработку такого пакета вызовом IoCompleteRequest. Освобождения пакетов, сделанных нестандартными способами (например, с помощью ExAllocatePool) выполняет сам драйвер.

Таблица 9.18. Описание прототипа функции IoSizeOfIrp

USHORT IoSizeOfIrp IRQL — любой
Параметры Определяет размер IRP пакета, как если бы он имел StackSize ячеек стека
IN CCHAR StackSize Предполагаемое число ячеек стека IRP пакета
Возвращаемое значение

Размер в байтах