有时候,我们需要做这样一个东西,例如一个硬件有时可能会产生很多的数据,而有时候可能没有数据,在数据量很多的时候,需要我们应用程序及时的读取设备中的数据,防止设备数据丢失或者积压数据太多导致占用内存过大。对于这种问题,我们很容易就想到,应用层使用多线程去读取数据,这样的话,如果数据量太多的话,在多核的情况下,消耗数据就会变大。在这种应用层产生多个IRP的情况下,就需要驱动对IRP串行处理了。
本文就来探讨一下Windows 标准串行处理队列.
我们知道,在Windows的设备中存在一个队列数据结构:
typedef struct _DEVICE_OBJECT {
//...
KDEVICE_QUEUE DeviceQueue;
//...
};
DeviceQueue
这个成员就是用来入队还没有处理的IRP结构的,这个结构如下:
typedef struct _KDEVICE_QUEUE {
CSHORT Type;
CSHORT Size;
LIST_ENTRY DeviceListHead; //IRP的链表
KSPIN_LOCK Lock; //DeviceListHead的锁
BOOLEAN Busy; //设备是否正在忙着处理IRP
} KDEVICE_QUEUE, *PKDEVICE_QUEUE;
针对设备对象的DeviceQueue
,Windows提供了一个对应的StartIO系列函数来处理,下面我们来看下这些函数的使用场景和原理。
当驱动程序的分发例程收到IRP之后,就开始处理IRP,如果使用标准的StartIO模型的话,我们的分发例程实现应该如下:
NTSTATUS DispatchXxx(...)
{
//...
IoMarkIrpPending(Irp);
IoStartPacket(device, Irp, NULL, NULL);
return STATUS_PENDING;
}
IoMarkIrpPending(Irp);
标记我们的IRP被PENDING住,后续在其他某个时候可能完成。IoStartPacket(device, Irp, NULL, NULL);
: 开始IRP入队,这个函数有两种情况:
IoStartPacket
立即调用StartIO例程完成IRP,此时后面返回STATUS_PENDING
并没有影响,因为上层会等待IRP完成,此时IRP刚好完成而已。IoStartPacket
只是入队IRP,此时IRP被挂起,返回STATUS_PENDING
。接下来,我们看下IoStartPacket
这个函数的具体实现流程:
VOID
IoStartPacket(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp,
IN PULONG Key OPTIONAL,
IN PDRIVER_CANCEL CancelFunction OPTIONAL
)
{
KIRQL oldIrql;
KIRQL cancelIrql = PASSIVE_LEVEL;
BOOLEAN i;
KeRaiseIrql( DISPATCH_LEVEL, &oldIrql );
//是否支持取消
if (CancelFunction) {
IoAcquireCancelSpinLock( &cancelIrql );
Irp->CancelRoutine = CancelFunction;
}
//插入到设备队列中
if (Key) {
i = KeInsertByKeyDeviceQueue( &DeviceObject->DeviceQueue,
&Irp->Tail.Overlay.DeviceQueueEntry,
*Key );
} else {
i = KeInsertDeviceQueue( &DeviceObject->DeviceQueue,
&Irp->Tail.Overlay.DeviceQueueEntry );
}
if (!i) {
//没有插入到设备队列,那么需要完成IRP
DeviceObject->CurrentIrp = Irp;
if (CancelFunction) {
if (DeviceObject->DeviceObjectExtension->StartIoFlags & DOE_STARTIO_NO_CANCEL) {
Irp->CancelRoutine = NULL;
}
IoReleaseCancelSpinLock( cancelIrql );
}
DeviceObject->DriverObject->DriverStartIo( DeviceObject, Irp );
} else {
//插入到设备队列中了
if (CancelFunction) {
if (Irp->Cancel) {
Irp->CancelIrql = cancelIrql;
Irp->CancelRoutine = (PDRIVER_CANCEL) NULL;
CancelFunction( DeviceObject, Irp );
} else {
IoReleaseCancelSpinLock( cancelIrql );
}
}
}
KeLowerIrql( oldIrql );
}
BOOLEAN
KeInsertDeviceQueue (
__inout PKDEVICE_QUEUE DeviceQueue,
__inout PKDEVICE_QUEUE_ENTRY DeviceQueueEntry
)
{
BOOLEAN Busy;
BOOLEAN Inserted;
KLOCK_QUEUE_HANDLE LockHandle;
ASSERT_DEVICE_QUEUE(DeviceQueue);
Inserted = FALSE;
KiAcquireInStackQueuedSpinLockForDpc(&DeviceQueue->Lock, &LockHandle);
Busy = DeviceQueue->Busy;
DeviceQueue->Busy = TRUE;
if (Busy == TRUE) { //是否设备正在忙着处理IRP
InsertTailList(&DeviceQueue->DeviceListHead,
&DeviceQueueEntry->DeviceListEntry);
Inserted = TRUE;
}
DeviceQueueEntry->Inserted = Inserted;
KiReleaseInStackQueuedSpinLockForDpc(&LockHandle);
return Inserted;
}
这里有几个值得注意的地方:
IoAcquireCancelSpinLock( &cancelIrql );
,并且设置取消例程。为什么需要获取取消锁呢?主要一个原因就是,如果设置完成取消例程之后,不获取锁的话,此时其他线程IoCancelIrp
,就会导致IRP被取消完成,而此时又在操作IRP,肯定是不行的。KeInsertDeviceQueue
: IRP放入到设备队列中存在两种情况,第一设备处于Busy状态,那么插入队列中等待处理;第二,设备处于非Busy状态,此时并不插入到队列中,只是将设备状态设置Busy,代表设备正要处理当前的IRP,后续新的IRP请入队。DeviceObject->CurrentIrp = Irp;
),这个状态很重要,因为有时候,我们可以通过这个标记来判断当前处理的IRP是哪一个。接着取消取消例程的设置,并调用StartIO例程完成IRP。假如我们的StartIo已经完成了IRP之后,那么是否可以直接返回呢?答案是不行的。因为如果完成IRP直接返回的话,设备对象中的设备队列中的其他IRP就得不到执行机会了,为了其他IRP能够继续执行,需要调用IoStartNextPacket
,这个函数的流程如下:
VOID
IopStartNextPacket(
IN PDEVICE_OBJECT DeviceObject,
IN LOGICAL Cancelable
)
{
KIRQL cancelIrql = PASSIVE_LEVEL;
PIRP irp;
PKDEVICE_QUEUE_ENTRY packet;
//获取取消锁
if (Cancelable) {
IoAcquireCancelSpinLock( &cancelIrql );
}
DeviceObject->CurrentIrp = (PIRP) NULL;
//移除一个IRP
packet = KeRemoveDeviceQueue( &DeviceObject->DeviceQueue );
if (packet) {
irp = CONTAINING_RECORD( packet, IRP, Tail.Overlay.DeviceQueueEntry );
//设置当前处理的IRP
DeviceObject->CurrentIrp = irp;
if (Cancelable) {
if (DeviceObject->DeviceObjectExtension->StartIoFlags & DOE_STARTIO_NO_CANCEL) {
irp->CancelRoutine = NULL;
}
IoReleaseCancelSpinLock( cancelIrql );
}
DeviceObject->DriverObject->DriverStartIo( DeviceObject, irp );
} else {
if (Cancelable) {
IoReleaseCancelSpinLock( cancelIrql );
}
}
}
这个函数的流程比较简单:
KeRemoveDeviceQueue
从队列中移除一个IRP,如果IRP存在,那么DeviceObject->DriverObject->DriverStartIo( DeviceObject, irp );
调用StartIO完成IRP。那么我们的StartIO函数应该怎么完成一个IRP呢?这里存在一个比较麻烦的问题,就是和IRP取消的同步问题。如果我们从设备队列中取出了IRP并且调用StartIO的时候,此时IRP正好取消了呢?有两种处理方案:
一般来说,我们是优先取消例程,因为这样比较合理,对于上层来说,这个IRP确实被取消了,这与第一情况对应,StartIO先判断IRP是否被取消,才能完成,如下:
VOID OnCancel(PDEVICE_OBJECT fdo, PIRP Irp)
{
if (fdo->CurrentIrp == Irp)
{
KIRQL oldirql = Irp->CancelIrql;
IoReleaseCancelSpinLock(DISPATCH_LEVEL);
IoStartNextPacket(fdo, TRUE);
KeLowerIrql(oldirql);
}
else
{
KeRemoveEntryDeviceQueue(&fdo->DeviceQueue, &Irp->Tail.Overlay.DeviceQueueEntry);
IoReleaseCancelSpinLock(Irp->CancelIrql);
}
CompleteRequest(Irp, STATUS_CANCELLED, 0);
}
if (fdo->CurrentIrp == Irp)
)。如果取消的IRP正在StartIO的话,那么怎么样告诉StartIO当前被取消了呢?有一个条件是StartIO判断当前IRP是否有取消状态Irp->Cancel
,但是如果只判断这个状态,那么存在一个严重的问题,如果IRP到了CompleteRequest
之后,IRP才到StartIO,那就比较麻烦了,因为此时IRP已经被释放掉了。因此在判断IRP的取消状态(Irp->Cancel
)之前,还需要提供IRP是否被取消完成的状态,这个就是如果当前IRP正在被StartIO的话,就要调用IoStartNextPacket(fdo, TRUE);
的原因。至于具体的细节,我们看StartIO就会明白。KeRemoveEntryDeviceQueue
就可以了。CompleteRequest
完成取消的IRP。VOID StartIo(PDEVICE_OBJECT fdo, PIRP Irp)
{
KIRQL oldirql;
IoAcquireCancelSpinLock(&oldirql);
if (Irp != fdo->CurrentIrp || Irp->Cancel)
{
IoReleaseCancelSpinLock(oldirql);
return;
}
else
{
IoSetCancelRoutine(Irp, NULL);
IoReleaseCancelSpinLock(oldirql);
}
//...
IoStartNextPacket(device, FALSE);
IoCompleteRequest(Irp, boost);
}
IoAcquireCancelSpinLock(&oldirql);
先获取到取消锁,然后判断IRP是否被取消(Irp->Cancel
);在使用IRP的时候,需要判断这个IRP是否被取消完成了,怎么判断呢?这里需要取消例程设置一个技巧点(参考上面取消例程的分析),需要判断取消例程有没有调用IoStartNextPacket
,这个就是Irp != fdo->CurrentIrp
,如果走到这里,那么可以肯定的是这个IRP已经或者应该被取消了,这个时候就不应该对IRP做任何处理了,因为可能已经被取消了。IoSetCancelRoutine(Irp, NULL);
禁止这个IRP被取消了。IoStartNextPacket(device, FALSE);
)。由此可以看到,使用StartIO模型的IRP取消是比较麻烦的,流程理解起来比较烧脑,总结为一点:谁完成了当前的IRP,谁应该负责调用IoStartNextPacket
,并且在完成之前调用IoStartNextPacket
,使得fdo->CurrentIrp
能够指向一个存在的IRP;如果存在IRP取消,那么优先取消IRP。。
当使用StartIO模型的时候,Cleanup的时候也要防止IRP被取消或者StartIO完成,不过这个判断比较简单,只要占用取消锁就可以了(因为对于设备队列的操作,都是先占用取消锁然后来操作的)。
NTSTATUS DispatchCleanup(PDEVICE_OBJECT fdo, PIRP Irp)
{
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);
PFILE_OBJECT fop = stack->FileObject;
LIST_ENTRY cancellist;
InitializeListHead(&cancellist);
KIRQL oldirql;
//暂用取消锁
IoAcquireCancelSpinLock(&oldirql);
//从队列中取消所有IRP,这些IRP肯定没有被StartIO和Cancel
KeAcquireSpinLockAtDpcLevel(&fdo->DeviceQueue.Lock);
PLIST_ENTRY first = &fdo->DeviceQueue.DeviceListHead;
PLIST_ENTRY next;
for (next = first->Flink; next != first; )
{
PIRP QueuedIrp = CONTAINING_RECORD(next,
IRP, Tail.Overlay.ListEntry);
PIO_STACK_LOCATION QueuedIrpStack =
IoGetCurrentIrpStackLocation(QueuedIrp);
PLIST_ENTRY current = next;
next = next->Flink;
if (QueuedIrpStack->FileObject != fop)
continue;
IoSetCancelRoutine(QueuedIrp, NULL);
RemoveEntryList(current);
InsertTailList(&cancellist, current);
}
KeReleaseSpinLockFromDpcLevel(&fdo->DeviceQueue.Lock);
IoReleaseCancelSpinLock(oldirql);
//直接取消就行,不用再判断了
while (!IsListEmpty(&cancellist))
{
next = RemoveHeadList(&cancellist);
PIRP CancelIrp = CONTAINING_RECORD(next, IRP, Tail.Overlay.ListEntry);
CompleteRequest(CancelIrp, STATUS_CANCELLED, 0);
}
return CompleteRequest(Irp, STATUS_SUCCESS, 0);
}