排队和悬而未决
你有权选择排队IRP, 在迟些的时间作处理或者在其他的线程处理。既然你拥有IRP, 当它在你的驱动栈层时,这是被允许的。你需要考虑IRP可以被取消的情况。问题是如果IRP被取消,既然结果将会被扔在一边,你真的不想执行任何处理?另一个问题是我们想要解决的是,如果一个有活动的IRP,他们和一个进程或者线程关联,直到活动的IRP完成,这个进程或者线程才能被完全终止。这是非常狡猾的,对于如何实现,这方面的文档非常少,在这里,我们将给你展示如何实现这些。
夺取你的锁
你需要做的第一件事情就是获取自旋锁保护你的IRP链,这将有助于在你的排队逻辑和取消例程之间同步执行。如果你使用某个系统提供的队列机制,有一种系统的取消自旋锁可以被获取,在一些情况下,是必须的。然而既然取消自旋锁是系统范围的,你所想的也是差不多接近的吗?另外的处理器会获取你的自旋锁,或者获取取消自旋锁吗?最有可能的是它会结束获取取消自旋锁,并且这将成为一种性能打击,在单处理器的机器上,很明显不需要关心你使用哪个自旋锁,但是你应该尝试去实现自己的自旋锁。
设置取消例程
你的取消例程需要获取你的自旋锁来同步执行或者从链中删除IRP. 设置取消例程,如果这个IRP被取消,你会知道并且可以从你的IRP列表中删除。记住,你仍然必须完成这个IRP。
没有其他办法。如果一个IRP被取消,他不会刚好从外面在你的脚下消失。如果是那样,当你在处理这个IRP时,如果它被取消,你会处于巨大的麻烦中。取消例程的作用是当IRP在队列中时,如果取消IRP时没有任何争抢,在任何时候你都可以把它从队列中删除。
检查取消标志
然后你必须检查IRP中的取消标志,如果它没有被取消,你需要调用IoMarkIrpPending以及将IRP排队到你的链表,无论哪种办法,你必须确信从你的驱动中返回了STATUS_PENDING。
如果它已经被取消,我们需要知道它是否调用了你的取消例程。通过把取消例程设置为NULL我们可以做到这点。如果返回值是NULL,那么取消例程被调用。如果返回值非NULL,那么取消例程没有被调用。这意味着在我们设置取消例程之前,它已经被取消。
你现在有两个选择,记住,仅有一个位置可以完成这个IRP. 如果取消例程被调用,那么在取消例程没有完成这个IRP时, 如果它不在你的IRP链中,那么你就可以释放它。如果取消例程已经完成了它,那么你一定不能完成它。如果取消例程没有被调用,那么你必须显式的完成它。不管发生什么事,你一定要记住两件事。第一件事是在你的驱动中某个地方,你必须要完成这个IRP. 第二件事情是记得一定不要完成两次。
同样的事情,当你从IRP链中删除一个IRP时,你需要检查这个IRP是否已经被取消。在删除IRP之前,你也要设置取消例程为NULL. 看下面的代码。
Irp->Tail.Overlay.DriverContext[0] =
(PVOID)pTdiExampleContext->pWriteIrpListHead;
NtStatus = HandleIrp_AddIrp(pTdiExampleContext->pWriteIrpListHead,
Irp, TdiExample_CancelRoutine, TdiExample_IrpCleanUp, NULL);
if(NT_SUCCESS(NtStatus))
{
KeSetEvent(&pTdiExampleContext->kWriteIrpReady,
IO_NO_INCREMENT, FALSE);
NtStatus = STATUS_PENDING;
}
...
/**********************************************************************
*
* HandleIrp_AddIrp
*
* This function adds an IRP to the IRP List.
*
**********************************************************************/
NTSTATUS HandleIrp_AddIrp(PIRPLISTHEAD pIrpListHead,
PIRP pIrp,
PDRIVER_CANCEL pDriverCancelRoutine,
PFNCLEANUPIRP pfnCleanUpIrp,
PVOID pContext)
{
NTSTATUS NtStatus = STATUS_UNSUCCESSFUL;
KIRQL kOldIrql;
PDRIVER_CANCEL pCancelRoutine;
PIRPLIST pIrpList;
pIrpList = (PIRPLIST)KMem_AllocateNonPagedMemory(sizeof(IRPLIST),
pIrpListHead->ulPoolTag);
if(pIrpList)
{
DbgPrint("HandleIrp_AddIrp Allocate Memory = 0x%0x \r\n", pIrpList);
pIrpList->pContext = pContext;
pIrpList->pfnCleanUpIrp = pfnCleanUpIrp;
pIrpList->pIrp = pIrp;
pIrpList->pfnCancelRoutine = pDriverCancelRoutine;
/*
* The first thing we need to to is acquire our spin lock.
*
* The reason for this is a few things.
*
* 1. All access to this list is synchronized, the obvious reason
* 2. This will synchronize adding this IRP to the
* list with the cancel routine.
*/
KeAcquireSpinLock(&pIrpListHead->kspIrpListLock, &kOldIrql);
/*
* We will now attempt to set the cancel routine which will be called
* when (if) the IRP is ever canceled. This allows us to remove an IRP
* from the queue that is no longer valid.
*
* A potential misconception is that if the IRP is canceled it is no
* longer valid. This is not true the IRP does not self-destruct.
* The IRP is valid as long as it has not been completed. Once it
* has been completed this is when it is no longer valid (while we
* own it). So, while we own the IRP we need to complete it at some
* point. The reason for setting a cancel routine is to realize
* that the IRP has been canceled and complete it immediately and
* get rid of it. We don't want to do processing for an IRP that
* has been canceled as the result will just be thrown away.
*
* So, if we remove an IRP from this list for processing and
* it's canceled the only problem is that we did processing on it.
* We complete it at the end and there's no problem.
*
* There is a problem however if your code is written in a way
* that allows your cancel routine to complete the IRP unconditionally.
* This is fine as long as you have some type of synchronization
* since you DO NOT WANT TO COMPLETE AN IRP TWICE!!!!!!
*/
IoSetCancelRoutine(pIrp, pIrpList->pfnCancelRoutine);
/*
* We have set our cancel routine. Now, check if the IRP has
* already been canceled.
* We must set the cancel routine before checking this to ensure
* that once we queue the IRP it will definately be called if the
* IRP is ever canceled.
*/
if(pIrp->Cancel)
{
/*
* If the IRP has been canceled we can then check if our
* cancel routine has been called.
*/
pCancelRoutine = IoSetCancelRoutine(pIrp, NULL);
/*
* if pCancelRoutine ==
* NULL then our cancel routine has been called.
* if pCancelRoutine !=
* NULL then our cancel routine has not been called.
*
* The I/O Manager will set the cancel routine to NULL
* before calling the cancel routine.
* We have a decision to make here, we need to write the code
* in a way that we only complete and clean up the IRP once.
* We either allow the cancel routine to do it or we do it here.
* Now, we will already have to clean up the IRP here if the
* pCancelRoutine != NULL.
*
* The solution we are going with here is that we will only clean
* up IRP's in the cancel routine if the are in the list.
* So, we will not add any IRP to the list if it has
* already been canceled once we get to this location.
*
*/
KeReleaseSpinLock(&pIrpListHead->kspIrpListLock, kOldIrql);
/*
* We are going to allow the clean up function to complete the IRP.
*/
pfnCleanUpIrp(pIrp, pContext);
DbgPrint("HandleIrp_AddIrp Complete Free Memory = 0x%0x \r\n",
pIrpList);
KMem_FreeNonPagedMemory(pIrpList);
}
else
{
/*
* The IRP has not been canceled, so we can simply queue it!
*/
pIrpList->pNextIrp = NULL;
IoMarkIrpPending(pIrp);
if(pIrpListHead->pListBack)
{
pIrpListHead->pListBack->pNextIrp = pIrpList;
pIrpListHead->pListBack = pIrpList;
}
else
{
pIrpListHead->pListFront = pIrpListHead->pListBack =
pIrpList;
}
KeReleaseSpinLock(&pIrpListHead->kspIrpListLock,
kOldIrql);
NtStatus = STATUS_SUCCESS;
}
}
else
{
/*
* We are going to allow the clean up function to complete the IRP.
*/
pfnCleanUpIrp(pIrp, pContext);
}
return NtStatus;
}