Windows驅動之IRP PENDING

Windows驅動之IRP PENDING

我們知道Windows是一個異步操作系統,那麼異步具體是怎麼實現的呢?這就依賴設備驅動程序的實現。例如當我們應用程序發起IRP請求,IRP請求到達驅動的時候,這個時候設備並沒有產生數據;作爲異步操作,這個時候驅動立即返回給應用程序,應用程序可以繼續其他操作,當設備準備好數據之後,再完成IRP,此時通知應用程序,IO請求完成。

當設備並沒有真實完成數據,只是掛起IRP的時候,給上層返回的狀態就叫PENDING狀態,例如:

NTSTATUS DispatchXxx(...)
{
    //...
    IoMarkIrpPending(Irp);
    IoStartPacket(device, Irp, NULL, NULL);
    return STATUS_PENDING;
}

那麼對於PENDING的IRP應用程序做了什麼事情呢?本文來分析一下整個過程原理。

1. IRP的發起

對於操作系統響應應用層發起的請求,都是通過IopSynchronousServiceTail調用IoCallDriver向設備發送IRP響應的,這個函數的代碼如下:

NTSTATUS
IopSynchronousServiceTail(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP Irp,
    IN PFILE_OBJECT FileObject,
    IN BOOLEAN DeferredIoCompletion,
    IN KPROCESSOR_MODE RequestorMode,
    IN BOOLEAN SynchronousIo,
    IN TRANSFER_TYPE TransferType
    )
{
	//...
	status = IoCallDriver(DeviceObject, Irp);
	if (DeferredIoCompletion) {

		if (status != STATUS_PENDING) {
			PKNORMAL_ROUTINE normalRoutine;
			PVOID normalContext;
			KIRQL irql = PASSIVE_LEVEL; 
			ASSERT(!Irp->PendingReturned);

			if (!SynchronousIo) {
				KeRaiseIrql(APC_LEVEL, &irql);
			}
			IopCompleteRequest(&Irp->Tail.Apc,
				&normalRoutine,
				&normalContext,
				(PVOID *)&FileObject,
				&normalContext);

			if (!SynchronousIo) {
				KeLowerIrql(irql);
			}
		}
	}
	if (SynchronousIo) {
		if (status == STATUS_PENDING) {

			status = KeWaitForSingleObject(&FileObject->Event,
				Executive,
				RequestorMode,
				(BOOLEAN)((FileObject->Flags & FO_ALERTABLE_IO) != 0),
				(PLARGE_INTEGER)NULL);

			if (status == STATUS_ALERTED || status == STATUS_USER_APC) {

				IopCancelAlertedRequest(&FileObject->Event, Irp);

			}
			status = FileObject->FinalStatus;
		}
		IopReleaseFileObjectLock(FileObject);

	}
	return status;
}

這裏有兩個重要的標記DeferredIoCompletionSynchronousIo,下面詳細解釋一下這個標記的意義:

  1. DeferredIoCompletion這個是一個標記,有些IRP會設置標記IRP_DEFER_IO_COMPLETION, 這個標記的一個用途是在調用IoCompleteRequest的時候不會完成IRP,直接返回,提交給IopSynchronousServiceTail來完成,例如:
VOID
FASTCALL
IopfCompleteRequest(
    IN PIRP Irp,
    IN CCHAR PriorityBoost
    )
{
    //...
     if (Irp->Flags & IRP_DEFER_IO_COMPLETION && !Irp->PendingReturned) {

        if ((Irp->IoStatus.Status == STATUS_REPARSE )  &&
            (Irp->IoStatus.Information == IO_REPARSE_TAG_MOUNT_POINT)) {

            Irp->Tail.Overlay.AuxiliaryBuffer = saveAuxiliaryPointer;
        }

        return;
    }
    //...
}
  1. 所以,類似NtReadFileNtWriteFile發起的IRP就會標記IRP_DEFER_IO_COMPLETION這個標記,並且DeferredIoCompletion這個參數設置成爲TRUE,因此對於IRP_MJ_READIRP_MJ_WRITE如果返回一個非STATUS_PENDING在這裏就會判斷失敗(if (status != STATUS_PENDING)),導致調用IopCompleteRequest直接完成IRP。因此有很多的開發朋友在IRP_MJ_READIRP_MJ_WRITE分發函數中錯誤的返回了一個非STATUS_PENDING,導致返回一個IRP就被釋放而出現莫名其妙的問題的現象。
  2. 對於if (SynchronousIo)是判斷是否是同步完成這個請求,如果是同步,那麼如果返回STATUS_PENDING的時候,就會引起Wait操作,等待底層IRP的完成。從這裏也可以發現,其實上層都是默認底層是異步完成的IRP的,只是上層針對是否應用層是同步請求還是異步請求來判斷是否等待IRP完成。

2. IoMarkIrpPending

對於驅動層,異步完成IRP的請求的流程是:

  1. IoMarkIrpPending(Irp);標記IRP異步。
  2. 排隊IRP,等待完成。
  3. 返回STATUS_PENDING, 這個比較重要(只能返回這個值)。

例如StartIO的異步函數如下:

NTSTATUS DispatchXxx(...)
{
    //...
    IoMarkIrpPending(Irp);
    IoStartPacket(device, Irp, NULL, NULL);
    return STATUS_PENDING;
}

從上面的分析,我們知道了,return STATUS_PENDING;這條語句的重要性,但是IoMarkIrpPending這個是幹什麼用途的呢?

#define IoMarkIrpPending( Irp ) ( \
    IoGetCurrentIrpStackLocation( (Irp) )->Control |= SL_PENDING_RETURNED )

這裏只是在設備棧上面設置了一個控制標記SL_PENDING_RETURNED這個標記的具體用途我們需要從IoCompleteRequest中才能知道。

3. IoCompleteRequest

這個函數是用來完成一個IRP使用的,我們分析這個函數之前,先提出一個問題,就是如果我們在設備棧上面設置了完成例程的話,那麼完成例程就應該是這樣的:

NTSTATUS CompletionRoutine(PDEVICE_OBJECT device, PIRP Irp, PVOID context)
{
    if (Irp->PendingReturned)
        IoMarkIrpPending(Irp);
    //...
    //不返回 STATUS_MORE_PROCESSING_REQUIRED
}

也就是說所有不返回STATUS_MORE_PROCESSING_REQUIRED狀態的完成例程都需要判斷Irp->PendingReturned並調用IoMarkIrpPending(Irp);設置IRP PENDING狀態。

爲什麼需要這樣呢?我們看下IoCompleteRequest的流程就清楚了:

VOID
FASTCALL
IopfCompleteRequest(
    IN PIRP Irp,
    IN CCHAR PriorityBoost
)
{
    //...
    //從當前設備開開始遍歷所有設備棧到頂層
    for (stackPointer = IoGetCurrentIrpStackLocation( Irp ),
         Irp->CurrentLocation++,
         Irp->Tail.Overlay.CurrentStackLocation++;
         Irp->CurrentLocation <= (CCHAR) (Irp->StackCount + 1);
         stackPointer++,
         Irp->CurrentLocation++,
         Irp->Tail.Overlay.CurrentStackLocation++) {

        //設置IRP是否是PENDING返回的
        Irp->PendingReturned = stackPointer->Control & SL_PENDING_RETURNED;

        if (!NT_SUCCESS(Irp->IoStatus.Status)) {

            if (Irp->IoStatus.Status != errorStatus) {
                errorStatus = Irp->IoStatus.Status;
                stackPointer->Control |= SL_ERROR_RETURNED;
                bottomSp->Parameters.Others.Argument4 = (PVOID)(ULONG_PTR)errorStatus;
                bottomSp->Control |= SL_ERROR_RETURNED; // Mark that there is status in this location
            }
        }

        if ( (NT_SUCCESS( Irp->IoStatus.Status ) &&
             stackPointer->Control & SL_INVOKE_ON_SUCCESS) ||
             (!NT_SUCCESS( Irp->IoStatus.Status ) &&
             stackPointer->Control & SL_INVOKE_ON_ERROR) ||
             (Irp->Cancel &&
             stackPointer->Control & SL_INVOKE_ON_CANCEL)
           ) {
             //調用完成例程

            ZeroIrpStackLocation( stackPointer );

            if (Irp->CurrentLocation == (CCHAR) (Irp->StackCount + 1)) {
                deviceObject = NULL;
            }
            else {
                deviceObject = IoGetCurrentIrpStackLocation( Irp )->DeviceObject;
            }

            status = stackPointer->CompletionRoutine( deviceObject,
                                                      Irp,
                                                      stackPointer->Context );

            if (status == STATUS_MORE_PROCESSING_REQUIRED) {

                return;
            }

        } else {
            //如果不調用完成例程,那麼IoMarkIrpPending
            if (Irp->PendingReturned && Irp->CurrentLocation <= Irp->StackCount) {
                IoMarkIrpPending( Irp );
            }
            ZeroIrpStackLocation( stackPointer );
        }
    }
    //...
}

對於IopfCompleteRequest這個函數,實在過於複雜,這裏我們只看對於PENDING 狀態設置的代碼。

  1. Irp->PendingReturned = stackPointer->Control & SL_PENDING_RETURNED; : 我們需要根據設備棧中釋放PENDING返回,來標記IRP的PendingReturned狀態。
  2. 然後針對IRP的返回值,判斷SL_INVOKE_ON_SUCCESS, SL_INVOKE_ON_ERRORSL_INVOKE_ON_CANCEL狀態,來調用完成例程。
  3. 如果沒有調用完成例程,那麼判斷Irp->PendingReturned並設置IoMarkIrpPending( Irp ); 狀態,這樣下次循環的時候Irp->PendingReturned = stackPointer->Control & SL_PENDING_RETURNED; 這個語句繼續可以設置PendingReturned 了。

爲什麼需要這樣操作呢?這個流程是比較複雜的,這裏只是大概解釋一下原因:

  1. 如果有中間的設備有一個設備是異步完成的,這個時候標記了stackPointer->Control & SL_PENDING_RETURNED,然後IRP的請求線程就返回了,這個時候如果是同步函數,那麼就存在一個問題,發起IRP的IopSynchronousServiceTail處於KeWaitForSingleObject(&FileObject->Event等待狀態,那麼這個是在什麼時候設置的呢?其實大概會有如下的判斷:
if (irp->PendingReturned && fileObject) {
    (VOID) KeSetEvent( &fileObject->Event, 0, FALSE );
}
  1. 那麼這裏的irp->PendingReturned這個標記就非常重要了,因爲我們頂層設備棧可能並沒有異步完成IRP,只是簡單的調用了IoCallDriver傳遞到了底層設備,一旦底層返回了STATUS_PENDING之後,那麼就需要設置irp->PendingReturned這個標記了(需要設置完成的EVENT)。因爲在完成IRP的時候PendingReturned是和IoMarkIrpPending設置的狀態息息相關的(Irp->PendingReturned = stackPointer->Control & SL_PENDING_RETURNED;),所以一旦底層有設備棧設置了stackPointer->Control & SL_PENDING_RETURNED,那麼在上次設備棧一定也要調用IoMarkIrpPending設置標記。在設備棧沒有完成回調函數的時候,IoCompleteRequest自己調用了了IoMarkIrpPending(如上面IopfCompleteRequest的分析),但是如果有完成回調函數,那麼這個操作是沒有的,所以這個操作交給了回調函數來完成。

因此我們的完成例程應該具有如下代碼:

NTSTATUS CompletionRoutine(PDEVICE_OBJECT device, PIRP Irp, PVOID context)
{
    if (Irp->PendingReturned)
        IoMarkIrpPending(Irp);
    //...
    //不返回 STATUS_MORE_PROCESSING_REQUIRED
}

4. 總結

因此從上面的分析,我們大概可以知道IoMarkIrpPending就是告訴系統,我異步返回了,上面可能在等待這個IRP的完成,請你在IRP完成的時候告訴我IRP完成了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章