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;
}
這裏有兩個重要的標記DeferredIoCompletion
和SynchronousIo
,下面詳細解釋一下這個標記的意義:
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;
}
//...
}
- 所以,類似
NtReadFile
和NtWriteFile
發起的IRP就會標記IRP_DEFER_IO_COMPLETION
這個標記,並且DeferredIoCompletion
這個參數設置成爲TRUE,因此對於IRP_MJ_READ
和IRP_MJ_WRITE
如果返回一個非STATUS_PENDING
在這裏就會判斷失敗(if (status != STATUS_PENDING)
),導致調用IopCompleteRequest
直接完成IRP。因此有很多的開發朋友在IRP_MJ_READ
和IRP_MJ_WRITE
分發函數中錯誤的返回了一個非STATUS_PENDING
,導致返回一個IRP就被釋放而出現莫名其妙的問題的現象。 - 對於
if (SynchronousIo)
是判斷是否是同步完成這個請求,如果是同步,那麼如果返回STATUS_PENDING
的時候,就會引起Wait操作,等待底層IRP的完成。從這裏也可以發現,其實上層都是默認底層是異步完成的IRP的,只是上層針對是否應用層是同步請求還是異步請求來判斷是否等待IRP完成。
2. IoMarkIrpPending
對於驅動層,異步完成IRP的請求的流程是:
IoMarkIrpPending(Irp);
標記IRP異步。- 排隊IRP,等待完成。
- 返回
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 狀態設置的代碼。
Irp->PendingReturned = stackPointer->Control & SL_PENDING_RETURNED;
: 我們需要根據設備棧中釋放PENDING返回,來標記IRP的PendingReturned
狀態。- 然後針對IRP的返回值,判斷
SL_INVOKE_ON_SUCCESS
,SL_INVOKE_ON_ERROR
和SL_INVOKE_ON_CANCEL
狀態,來調用完成例程。 - 如果沒有調用完成例程,那麼判斷
Irp->PendingReturned
並設置IoMarkIrpPending( Irp );
狀態,這樣下次循環的時候Irp->PendingReturned = stackPointer->Control & SL_PENDING_RETURNED;
這個語句繼續可以設置PendingReturned
了。
爲什麼需要這樣操作呢?這個流程是比較複雜的,這裏只是大概解釋一下原因:
- 如果有中間的設備有一個設備是異步完成的,這個時候標記了
stackPointer->Control & SL_PENDING_RETURNED
,然後IRP的請求線程就返回了,這個時候如果是同步函數,那麼就存在一個問題,發起IRP的IopSynchronousServiceTail
處於KeWaitForSingleObject(&FileObject->Event
等待狀態,那麼這個是在什麼時候設置的呢?其實大概會有如下的判斷:
if (irp->PendingReturned && fileObject) {
(VOID) KeSetEvent( &fileObject->Event, 0, FALSE );
}
- 那麼這裏的
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完成了。