对于Windows驱动来说,IRP的取消一直都是比较复杂的问题,很多有经验的驱动开发人员都不能完美的处理好IRP的取消问题。关于IRP的取消有两种会出现:
取消IPR主要的难点在于,在取消IRP的时候,需要防止IRP被取出完成,因此需要同步两者之间的操作;针对这种情况,Windows引入了取消安全队列,专门用来处理IRP的取消。
首先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取消和完成之间的同步关系了,因为系统已经帮我们做了同步操作了。
下面我们来看一下取消安全队列的使用。
这个函数是用来初始化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
,给后续相关操作。
如果驱动需要异步的处理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的取消例程。if(!Irp->Cancel)
):
IoSetCancelRoutine(Irp, NULL)
返回一个NULL对象,说明取消例程被取出了。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。以前我们人如果手动使用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即可。
上面分析都是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
来保存就可以了。