[Previous] [Next]

Работа с IRP пакетами-репликантами

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

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

  1. Выполняет вызовы IoBuildSynchronousFsdRequest или вызовы IoBuildDeviceIoControlRequest для того, чтобы создать необходимое количество IRP пакетов "синхронного" типа.
  2. Выполняет вызовы IoCallDriver для передачи всех созданных драйвером пакетов IRP другим драйверам.
  3. Выполняет вызовы KeWaitForMultipleObjects и ожидает завершения обработки всех переданных IRP пакетов.
  4. Выполняет действия по переносу информации из полученных пакетов и их последующую очистку и освобождение.
  5. Наконец, выполняет вызов IoCompleteRequest относительно исходного IRP пакета для того, чтобы возвратить его инициатору вызова.

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

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

  1. Пометить пакет IRP, поступивший в рабочую процедуру от Диспетчера ввода/вывода как ожидающий обработки при помощи IoMarkPending.
  2. Создать дополнительные пакеты IRP с использованием одного из описанных выше методов.
  3. Подключить процедуру завершения (возможно — одну и ту же) к каждому из вновь созданных IRP пакетов вызовом IoSetCompletionRoutine. При выполнении этого вызова следует передать указатель на исходный IRP пакет в аргументе pContext.
  4. Запомнить число созданных пакетов IRP в неиспользуемом поле исходного IRP пакета. Поле Parameters.Key текущей ячейки стека IRP пакета вполне годится.
  5. Передать пакеты всем нужным драйверам вызовом IoCallDriver.
  6. Возвратить значение STATUS_PENDING, поскольку обработка исходного запроса (пакета IRP) не завершена.

По окончании обработки каждого IRP пакета драйвером нижнего уровня во втором, "асинхронном", варианте вызывается процедура завершения рассматриваемого ("нашего") драйвера, которая выполняет следующие операции:

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

Удаление IRP пакетов

Как бывает и в реальной жизни, кто-то, инициировавший IRP запрос, может передумать и инициализировать снятие запроса "с повестки". Пользовательское приложение может запросить уничтожение пакета после длительного ожидания. Приложение может вовсе прекратить работу, бросив все на попечение операционной системы. Наконец, приложение может попытаться завершить свою асинхронную операцию Win32 API вызовом CancelIo.

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

BOOLEAN IoCancelIrp IRQL <= DISPATCH_LEVEL
Параметры Помечает пакет IRP как требующий удаления и вызывает процедуры удаления, если таковые определены
IN PIRP pIrp Указатель на удаляемый IRP пакет
Возвращаемое значение TRUE — если пакет удален
FALSE — в случае неудачи

В режиме ядра для удаления запроса выполняется вызов IoCancelIrp (таблица 9.19). Операционная система также вызывает IoCancelIrp для всех IRP пакетов, относящихся к потоку, выполнение которого прекращается.

Предположим, некий код режима ядра направил пакет (в данном случае — синхронный) другому драйверу. Как он может выполнить удаление отправленного пакета, например, в результате превышения времени ожидания? Пример ниже иллюстрирует этот случай.

// формирует синхронный пакет:
PIRP pIrp= IoBuildSynchronousFsdRequest(. . ., &event, &iosb);
// Подключаем процедуру завершения:
IoSetCompletionRoutine( pIrp, MyCompletionRoutine, (VOID*)&event,
                        TRUE, TRUE, TRUE );
NTSTATUS status = IoCallDriver(. . .);
if( status == STATUS_PENDING )
{	// Некоторое время ожидаем естественного завершения
	LARGE_INTEGER waitDelay;
	waitDelay.QuadPart = - 10000; // относительное время
	if( KeWaitForSingleObject( &event,
	    KernelMode, FALSE, &waitDelay) == STATUS_TIMEOUT )
	{
		IoCancelIrp(pIrp);
		KeWaitForSingleObject( &event, KernelMode, FALSE, NULL);
	}
}
// Синхронные IRP пакеты - их удаляет Диспетчер ввода/вывода:
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
. . .

// Процедура завершения
NTSTATUS MyCompletionRoutine( PDEVICE_OBJECT pThisDevice,
                              PIRP pIrp,
                              PVOID pContext )
{
	if (pIrp->PendingReturned)
		KeSetEvent((PKEVENT) pContext, IO_NO_INCREMENT, FALSE);
	return STATUS_MORE_PROCESSING_REQUIRED;
} 

Процедура IoCancelIrp устанавливает флаг (cancel bit) в IRP пакете и выполняет вызов процедуры CancelRoutine, если таковая имеется в IRP пакете.

Таблица 9.20. Прототип предоставляемой драйвером функции CancelRoutine

VOID CancelRoutine IRQL == DISPATCH_LEVEL
Параметры Выполняет действия, сопутствующие удалению пакета IRP
IN PDEVICE_OBJECT pDevObj Указатель на объект устройства, которое (точнее — драйвер) и зарегистрировало ранее эту функцию в IRP пакете вызовом IoSetCancelRoutine (см. ниже)
IN PIRP pIrp Указатель на удаляемый IRP пакет
Возвращаемое значение void

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

PDRIVER_CANCEL IoSetCancelRoutine IRQL <= DISPATCH_LEVEL
Параметры Устанавливает (переустанавливает) определяемую драйвером функцию CancelRoutine для данного IRP пакета
IN PIRP pIrp Указатель на IRP пакет, которому будет соответствовать устанавливаемая функция CancelRoutine
IN PDRIVER_CANCEL CancelRoutine Указатель на функцию, которая соответствует прототипу, описанному в таблице 9.20, или NULL (если следует отменить функцию CancelRoutine для данного пакета IRP)
Возвращаемое значение

Указатель на ранее установленную для данного IRP пакета функцию CancelRoutine. Соответственно, если таковой не было, то возвращается NULL. Значение NULL возвращается также, если пакет находится в обработке и не может быть удален

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

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

Код вызова IoCancelIrp устроен примерно следующим образом — по крайней мере, так уверяет Уолтер Оней:

BOOLEAN IoCancelIrp(PIRP pIrp)
{
	IoAcquireCancelSpinLock(&pIrp->CancelIrql);
	pIrp->Cancel=TRUE;
	PDRIVER_CANCEL CancelRoutine = IoSetCancelRoutine(pIrp, NULL);
	if( CancelRoutine != NULL)
	{
		PIO_STACK_LOCATION currentCell =
			IoGetCurrentIrpStackLocation(pIrp);
		(*CancelRoutine)(currentCell->DeviceObject, pIrp);
		return TRUE;
	}
	else
	{
		IoReleaseCancelSpinLock(pIrp->CancelIrql);
		return FALSE;
	}
}

Для ограничения доступа к удаляемому пакету, код IoCancelIrp, прежде всего, запрашивает объект спин-блокировки вызовом IoAcquireCancelSpinLock (в переменной pIrp->CancelIrql сохраняется значение текущего уровня IRQL для использования при последующем вызове IoReleaseCancelSpinLock). В случае, если за IRP пакетом закреплена процедура CancelRoutine, то она вызывается (теперь на нее возложена задача освобождения спин-блокировки). Если же такой процедуры нет, то вызов IoCancelIrp завершает работу, освобождая спин-блокировку.

Процедура CancelRoutine, получив управление, может оказаться в одной из описанных ниже ситуаций.

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

Irp->IoStatus.Status = STATUS_IO_TIMEOUT;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT); 

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

Выяснить, обрабатывается ли рассматриваемый пакет именно сейчас, можно при помощи следующего кода, поскольку, если задействован механизм System Queuing и какой-либо пропущенный через него IRP пакет в настоящий момент обрабатывается, то именно адрес этого IRP пакета "лежит" в поле pDeviceObject->CurrentIrp (иначе там будет NULL):

VOID MyCancelRoutine ( IN PDEVICE_OBJECT pDeviceObject,
                       IN PIRP pIrp)
{
	if( pIrp == pDeviceObject->CurrentIrp )
	{
	. . .

Конкретная реализация действий по удалению текущего пакета (если она возможна) остается задачей разработчика драйвера.

Существенно проще становится ситуация, когда пакет, предназначенный для уничтожения, только что поступил в процедуру StartIo либо еще находится в очереди отложенных (pending) пакетов.

VOID StartIo ( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIRp)
{
	KIRQL CancelIrql;
	IoAcquireCancelSpinLock(&CancelIrql);
	If(pIrp->Cancel)
	{
		IoReleaseCancelSpinLock(CancelIrql);
		return;
	}
	// Удаляем процедуру обработки удаления, делая пакет
	// "not cancelable" - неуничтожаемым
	IoSetCancelRoutine(pIrp, NULL);
	IoReleaseCancelSpinLock(CancelIrql);
	. . .
}

В случае, если удаляемый IRP пакет пока находится в системной очереди (которая называется еще "управляемая StartIo"), a перед его размещением там (то есть вместе с вызовом IoMarkIrpPending) была зарегистрирована процедура MyCancelRoutine для этого IRP пакета, то действия по удалению такого пакета (не текущего — не находящегося в обработке) могут выглядеть следующим образом:

VOID MyCancelRoutine( IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
	if( pIrp == pDeviceObject->CurrentIrp )
	{	// Текущий IRP
		IoReleaseCancelSpinLock(pIrp->CancelIrql);
		// Вряд ли можно сделать что-то еще...
	}
	else
	{	// Удаляем из системной очереди:
		KeRemoveEntryDeviceQueue( &pDeviceObject->DeviceQueue,
			&pIrp->Tail.Overlay.DeviceQueueEntry);
		// Только теперь можно освободить спин-блокировку:
		IoReleaseCancelSpinLock(pIrp->CancelIrql);

		pIrp->IoStatus.Status=STATUS_CANCELLED;
		pIrp->IoStatus.Information = 0;
		IoCompleteRequest( pIrp, IO_NO_INCREMENT );
	}
	return;
} 

Приведенный ниже пример выполняет удаление пакета из очереди, поддерживаемой собственно драйвером (так называемой "Device-Managed Queue").

VOID MyOtherCancelRoutine ( IN PDEVICE_OBJECT pDeviceObject,
                            IN PIRP pIrp )
{
	KIRQL oldIRQL;
	PMYDEVICE_EXTENSION pDevExt =
		(PMYDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
		IoSetCancelRoutine(pIrp, NULL);
	// Освобождаем спин-блокировку, установленную еще IoCancelIrp
	IoReleaseCancelSpinLock(pIrp->CancelIrp);

	// Удаляем IRP из очереди под защитой спин-блокировки
	KeAcquireSpinLock(&pDevExt->QueueLock, &oldIRQL);
	RemoveEntryList (&pIrp->Tail.Overlay.ListEntry);
	KeReleaseSpinLock(&pDevExt->QueueLock, oldIRQL);
	//
	pIrp->IoStatus.Status = STATUS_CANCELLED;
	pIrp->IoStatus.Information = 0;
	IoCompleteRequest( pIrp, IO_NO_INCREMENT );
	return;
}

Предполагается, что объект спин-блокировки, используемый для синхронизации доступа к очереди пакетов, pDevExt->QueueLock был заранее создан и сохранен в структуре расширения данного устройства.

Вопросы создания и поддержки очередей IRP пакетов, ведомых собственно драйвером, в данной книге не рассматриваются. Хотя этот прием достаточно широко распространен и присутствует в примерах пакета DDK.

В заключение, следует отметить, что после вызова IoCompleteRequest в конце процедуры CancelRoutine (обработки удаления IRP пакета в данном драйвере) запускаются вызовы процедур завершения вышестоящих драйверов — если такие драйвера имеются и если соответствующие процедуры были зарегистрированы для данного IRP пакета на случай его удаления (см. описание вызова IoSetCompletionRoutine в таблице 9.8).