Windows驅動之取消安全隊列
對於Windows驅動來說,IRP的取消一直都是比較複雜的問題,很多有經驗的驅動開發人員都不能完美的處理好IRP的取消問題。關於IRP的取消有兩種會出現:
- CancelIo函數的調用。
- 線程的結束。
取消IPR主要的難點在於,在取消IRP的時候,需要防止IRP被取出完成,因此需要同步兩者之間的操作;針對這種情況,Windows引入了取消安全隊列,專門用來處理IRP的取消。
1. IO_CSQ
首先windows提供一個IO_CSQ
結構,驅動程序可以通過這個結構向系統提供安全取消隊列信息,這個結構如下:
typedef struct _IO_CSQ {
ULONG Type;
PIO_CSQ_INSERT_IRP CsqInsertIrp;
PIO_CSQ_REMOVE_IRP CsqRemoveIrp;
PIO_CSQ_PEEK_NEXT_IRP CsqPeekNextIrp;
PIO_CSQ_ACQUIRE_LOCK CsqAcquireLock;
PIO_CSQ_RELEASE_LOCK CsqReleaseLock;
PIO_CSQ_COMPLETE_CANCELED_IRP CsqCompleteCanceledIrp;
PVOID ReservePointer; /* must be NULL */
} IO_CSQ, *PIO_CSQ;
這個結構體提供如下信息:
CsqInsertIrp
,提供插入回調函數;當設備實施異步IRP的時候,就會回調這個函數,一般來說,這個函數的主要用途是通過隊列將IRP保存起來。CsqRemoveIrp
: 這個是移除IRP的回調函數,意思是IRP取消獲取其他原因刪除的時候,就會調用這個函數來移除一個IRP。CsqPeekNextIrp
: 這個是從異步IRP隊列中彈出一個IRP調用的函數,一般來說,這個函數從異步隊列中出隊一個IRP。CsqAcquireLock
和CsqReleaseLock
: 我們在插入IRP,移除IRP的時候都需要保證同步,這兩個函數就是用來保證同步使用的。CsqCompleteCanceledIrp
: 取消回調例程。
從上面這些接口我們可以大概看出,我們的驅動不在需要管理IRP取消和完成之間的同步關係了,因爲系統已經幫我們做了同步操作了。
下面我們來看一下取消安全隊列的使用。
2. IoCsqInitialize
這個函數是用來初始化IO_CSQ
結構的,這個函數如下:
NTSTATUS
NTAPI
IoCsqInitialize(
_Out_ PIO_CSQ Csq,
_In_ PIO_CSQ_INSERT_IRP CsqInsertIrp,
_In_ PIO_CSQ_REMOVE_IRP CsqRemoveIrp,
_In_ PIO_CSQ_PEEK_NEXT_IRP CsqPeekNextIrp,
_In_ PIO_CSQ_ACQUIRE_LOCK CsqAcquireLock,
_In_ PIO_CSQ_RELEASE_LOCK CsqReleaseLock,
_In_ PIO_CSQ_COMPLETE_CANCELED_IRP CsqCompleteCanceledIrp)
{
Csq->Type = IO_TYPE_CSQ;
Csq->CsqInsertIrp = CsqInsertIrp;
Csq->CsqRemoveIrp = CsqRemoveIrp;
Csq->CsqPeekNextIrp = CsqPeekNextIrp;
Csq->CsqAcquireLock = CsqAcquireLock;
Csq->CsqReleaseLock = CsqReleaseLock;
Csq->CsqCompleteCanceledIrp = CsqCompleteCanceledIrp;
Csq->ReservePointer = NULL;
return STATUS_SUCCESS;
}
這個函數的主要作用就是用一些列的回調函數來初始化IO_CSQ
這個結構體,給驅動層返回IO_CSQ
,給後續相關操作。
3. IoCsqInsertIrp
如果驅動需要異步的處理IRP,那麼當IRP到來的時候,就需要將IRP保存起來,提供給後面使用;在驅動程序中,我們可以使用IoCsqInsertIrp
將IRP保存起來,這個函數實現如下:
NTSTATUS
NTAPI
IoCsqInsertIrpEx(
_Inout_ PIO_CSQ Csq,
_Inout_ PIRP Irp,
_Out_opt_ PIO_CSQ_IRP_CONTEXT Context,
_In_opt_ PVOID InsertContext)
{
NTSTATUS Retval = STATUS_SUCCESS;
KIRQL Irql;
Csq->CsqAcquireLock(Csq, &Irql);
do
{
/* mark all irps pending -- says so in the cancel sample */
IoMarkIrpPending(Irp);
/* set up the context if we have one */
if(Context)
{
Context->Type = IO_TYPE_CSQ_IRP_CONTEXT;
Context->Irp = Irp;
Context->Csq = Csq;
Irp->Tail.Overlay.DriverContext[3] = Context;
}
else
Irp->Tail.Overlay.DriverContext[3] = Csq;
/* Step 1: Queue the IRP */
if(Csq->Type == IO_TYPE_CSQ)
Csq->CsqInsertIrp(Csq, Irp);
else
{
PIO_CSQ_INSERT_IRP_EX pCsqInsertIrpEx = (PIO_CSQ_INSERT_IRP_EX)Csq->CsqInsertIrp;
Retval = pCsqInsertIrpEx(Csq, Irp, InsertContext);
if(Retval != STATUS_SUCCESS)
break;
}
/* Step 2: Set our cancel routine */
(void)IoSetCancelRoutine(Irp, IopCsqCancelRoutine);
/* Step 3: Deal with an IRP that is already canceled */
if(!Irp->Cancel)
break;
/*
* Since we're canceled, see if our cancel routine is already running
* If this is NULL, the IO Manager has already called our cancel routine
*/
if(!IoSetCancelRoutine(Irp, NULL))
break;
Irp->Tail.Overlay.DriverContext[3] = 0;
/* OK, looks like we have to de-queue and complete this ourselves */
Csq->CsqRemoveIrp(Csq, Irp);
Csq->CsqCompleteCanceledIrp(Csq, Irp);
if(Context)
Context->Irp = NULL;
}
while(0);
Csq->CsqReleaseLock(Csq, Irql);
return Retval;
}
這裏的操作可以分爲幾個步驟:
Csq->CsqAcquireLock(Csq, &Irql);
佔用我們CSQ的鎖,那麼其他線程將不能再操作CSQ了。Csq->CsqInsertIrp(Csq, Irp);
將IRP插入到CSQ隊列中。(void)IoSetCancelRoutine(Irp, IopCsqCancelRoutine);
設置IRP的取消例程。- 此時,這個IRP雖然放到了隊列中,但是這個期間可能被取消了,所以我們判斷一下是否IRP已經被取消(
if(!Irp->Cancel)
):- 如果IRP沒有被取消,那麼異步操作成功(插入到隊列中)。
- 如果此時IRP被取消了有兩種可能:
- IRP被另外線程取消了,並且調用了取消例程,那麼我們不應該再操作任何IRP了,因爲IRP已經調用取消例程取消了;滿足這種條件是
IoSetCancelRoutine(Irp, NULL)
返回一個NULL對象,說明取消例程被取出了。 - IRP被另外線程取消了,但是還沒開始調用取消例程,此時另外一個線程可能取消不了IRP了(因爲可能在設置IRP的取消例程之前IRP就被取消了)所以我們需要移除IRP,並且調用取消例程。
- IRP被另外線程取消了,並且調用了取消例程,那麼我們不應該再操作任何IRP了,因爲IRP已經調用取消例程取消了;滿足這種條件是
4. IoCsqRemoveNextIrp
IRP入隊之後,我們就要從隊列中取出IRP來執行了,這個函數爲IoCsqRemoveNextIrp
,這個函數返回取出的IRP,流程如下:
PIRP
NTAPI
IoCsqRemoveNextIrp(
_Inout_ PIO_CSQ Csq,
_In_opt_ PVOID PeekContext)
{
KIRQL Irql;
PIRP Irp = NULL;
PIO_CSQ_IRP_CONTEXT Context;
Csq->CsqAcquireLock(Csq, &Irql);
while((Irp = Csq->CsqPeekNextIrp(Csq, Irp, PeekContext)))
{
if(!IoSetCancelRoutine(Irp, NULL))
continue;
Csq->CsqRemoveIrp(Csq, Irp);
/* Unset the context stuff and return */
Context = (PIO_CSQ_IRP_CONTEXT)InterlockedExchangePointer(&Irp->Tail.Overlay.DriverContext[3], NULL);
if (Context && Context->Type == IO_TYPE_CSQ_IRP_CONTEXT)
{
Context->Irp = NULL;
ASSERT(Context->Csq == Csq);
}
Irp->Tail.Overlay.DriverContext[3] = 0;
break;
}
Csq->CsqReleaseLock(Csq, Irql);
return Irp;
}
這裏幾個操作:
Csq->CsqAcquireLock(Csq, &Irql)
鎖同步整個CSQ的操作。Irp = Csq->CsqPeekNextIrp(Csq, Irp, PeekContext)
從隊列中取出IRP。- 如果IRP沒有被取消,那麼就返回。
5. IRP的取消
以前我們人如果手動使用StartIo隊列或者自己實現隊列IRP的取消就會很麻煩,那麼CSQ怎麼取消IRP的呢?我們看下代碼,如下 :
static
VOID
NTAPI
IopCsqCancelRoutine(
_Inout_ PDEVICE_OBJECT DeviceObject,
_Inout_ _IRQL_uses_cancel_ PIRP Irp)
{
PIO_CSQ Csq;
KIRQL Irql;
/* First things first: */
IoReleaseCancelSpinLock(Irp->CancelIrql);
/* We could either get a context or just a csq */
Csq = (PIO_CSQ)Irp->Tail.Overlay.DriverContext[3];
if(Csq->Type == IO_TYPE_CSQ_IRP_CONTEXT)
{
PIO_CSQ_IRP_CONTEXT Context = (PIO_CSQ_IRP_CONTEXT)Csq;
Csq = Context->Csq;
/* clean up context while we're here */
Context->Irp = NULL;
}
/* Now that we have our CSQ, complete the IRP */
Csq->CsqAcquireLock(Csq, &Irql);
Csq->CsqRemoveIrp(Csq, Irp);
Csq->CsqReleaseLock(Csq, Irql);
Csq->CsqCompleteCanceledIrp(Csq, Irp);
}
這裏我們非常簡單了,只需要調用Csq->CsqRemoveIrp(Csq, Irp);
移除IRP;然後調用Csq->CsqCompleteCanceledIrp(Csq, Irp);
取消IRP就行了。
爲什麼不用考慮其他的呢?因爲從上面我們可以知道任何的insert,peek都有判斷了IRP是否在取消狀態了,如果是在取消狀態,那麼保證取消例程優先處理,所以IopCsqCancelRoutine
這個函數中,我們直接取消IRP即可。
6. 總結
上面分析都是Windows系統給我們封裝好了的,那我們如果使用這個CSQ應該怎麼用呢,其實非常簡單,自己實現隊列,進行插入刪除IRP操作至於取消IRP如下完成即可:
VOID CsqAcquireLock(PIO_CSQ IoCsq, PKIRQL PIrql)
{
KeAcquireSpinLock(SpinLock, PIrql);
}
VOID CsqReleaseLock(PIO_CSQ IoCsq, KIRQL Irql)
{
KeReleaseSpinLock(SpinLock, Irql);
}
VOID CsqCompleteCanceledIrp(PIO_CSQ Csq, PIRP Irp) {
Irp->IoStatus.Status = STATUS_CANCELLED;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
}
所有IRP的同步處理都由框架來完成處理了。
插入、刪除、取出IRP我們只要一行就可以完成:
//插入
IO_CSQ_IRP_CONTEXT ParticularIrpInQueue;
IoCsqInsertIrp(IoCsq, Irp, &ParticularIrpInQueue);
//刪除
IoCsqRemoveIrp(IoCsq, Irp, &ParticularIrpInQueue);
//取出
IoCsqRemoveNextIrp(IoCsq, NULL);
至於IRP的異步隊列,我們使用普通的LIST_ENTRY
來保存就可以了。