Windows驱动之StartIO模型

文章目录

  • Windows驱动之StartIO模型
    • 1. KDEVICE_QUEUE
    • 2. IoStartPacket
    • 3. IoStartNextPacket
    • 4. StartIO
    • 5. Cleanup

Windows驱动之StartIO模型

有时候,我们需要做这样一个东西,例如一个硬件有时可能会产生很多的数据,而有时候可能没有数据,在数据量很多的时候,需要我们应用程序及时的读取设备中的数据,防止设备数据丢失或者积压数据太多导致占用内存过大。对于这种问题,我们很容易就想到,应用层使用多线程去读取数据,这样的话,如果数据量太多的话,在多核的情况下,消耗数据就会变大。在这种应用层产生多个IRP的情况下,就需要驱动对IRP串行处理了。

本文就来探讨一下Windows 标准串行处理队列.

1. KDEVICE_QUEUE

我们知道,在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系列函数来处理,下面我们来看下这些函数的使用场景和原理。

2. IoStartPacket

当驱动程序的分发例程收到IRP之后,就开始处理IRP,如果使用标准的StartIO模型的话,我们的分发例程实现应该如下:

NTSTATUS DispatchXxx(...)
{
    //...
    IoMarkIrpPending(Irp);
    IoStartPacket(device, Irp, NULL, NULL);
    return STATUS_PENDING;
}
  1. IoMarkIrpPending(Irp);标记我们的IRP被PENDING住,后续在其他某个时候可能完成。
  2. IoStartPacket(device, Irp, NULL, NULL); : 开始IRP入队,这个函数有两种情况:
    1. 设备没有IRP在处理,此时IoStartPacket立即调用StartIO例程完成IRP,此时后面返回STATUS_PENDING并没有影响,因为上层会等待IRP完成,此时IRP刚好完成而已。
    2. 设备正在处理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;
}

这里有几个值得注意的地方:

  1. 如果IRP允许取消的话,那么需要IoAcquireCancelSpinLock( &cancelIrql );,并且设置取消例程。为什么需要获取取消锁呢?主要一个原因就是,如果设置完成取消例程之后,不获取锁的话,此时其他线程IoCancelIrp,就会导致IRP被取消完成,而此时又在操作IRP,肯定是不行的。
  2. KeInsertDeviceQueue : IRP放入到设备队列中存在两种情况,第一设备处于Busy状态,那么插入队列中等待处理;第二,设备处于非Busy状态,此时并不插入到队列中,只是将设备状态设置Busy,代表设备正要处理当前的IRP,后续新的IRP请入队。
  3. 对于当前没有插入的IPR,设置当前的IRP(DeviceObject->CurrentIrp = Irp;),这个状态很重要,因为有时候,我们可以通过这个标记来判断当前处理的IRP是哪一个。接着取消取消例程的设置,并调用StartIO例程完成IRP。
  4. 对于已经插入队列的IRP,我们需要判断IRP是否被取消,如果被取消,那么就应该立即取消掉。为什么需要立即取消呢?如果等待IRP完成的话,可能会等待很久,这样会导致Cancel操作一直不成功。 如果没有取消,那么释放取消锁,这个时候插入成功。

3. IoStartNextPacket

假如我们的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 );
        }
    }
}

这个函数的流程比较简单:

  1. 为了防止我们取到的IRP在使用的时候被取消掉,我们在使用的时候,需要先获取取消锁,保证此时获取的IRP没有被取消掉。
  2. KeRemoveDeviceQueue 从队列中移除一个IRP,如果IRP存在,那么DeviceObject->DriverObject->DriverStartIo( DeviceObject, irp );调用StartIO完成IRP。

4. StartIO

那么我们的StartIO函数应该怎么完成一个IRP呢?这里存在一个比较麻烦的问题,就是和IRP取消的同步问题。如果我们从设备队列中取出了IRP并且调用StartIO的时候,此时IRP正好取消了呢?有两种处理方案:

  1. 优先取消,也就是说只要判断当前IRP正在取消的话,那么StartIO就不处理了,交给取消例程处理。
  2. 优先StartIO,这种就是反过来处理,当取消例程发现IRP正在被StartIO完成的话,就不做处理了。

一般来说,我们是优先取消例程,因为这样比较合理,对于上层来说,这个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);
}
  1. 这里是取消优先,所以走到取消回调函数中的IRP一定会直接被Complete.
  2. 但是这里应该有一个判断,就是取消的IRP是否正在StartIO完成(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就会明白。
  3. 如果当前IRP并没有被StartIO处理的话,那么KeRemoveEntryDeviceQueue就可以了。
  4. 最后调用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);	
}
  1. IoAcquireCancelSpinLock(&oldirql);先获取到取消锁,然后判断IRP是否被取消(Irp->Cancel);在使用IRP的时候,需要判断这个IRP是否被取消完成了,怎么判断呢?这里需要取消例程设置一个技巧点(参考上面取消例程的分析),需要判断取消例程有没有调用IoStartNextPacket,这个就是Irp != fdo->CurrentIrp,如果走到这里,那么可以肯定的是这个IRP已经或者应该被取消了,这个时候就不应该对IRP做任何处理了,因为可能已经被取消了。
  2. 如果IRP没有取消,那么应该IoSetCancelRoutine(Irp, NULL);禁止这个IRP被取消了。
  3. 完成IRP.
  4. 接着处理下面的IRP(IoStartNextPacket(device, FALSE);)。

由此可以看到,使用StartIO模型的IRP取消是比较麻烦的,流程理解起来比较烧脑,总结为一点:谁完成了当前的IRP,谁应该负责调用IoStartNextPacket,并且在完成之前调用IoStartNextPacket,使得fdo->CurrentIrp能够指向一个存在的IRP;如果存在IRP取消,那么优先取消IRP。

5. Cleanup

当使用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);				
}

你可能感兴趣的:(Windows驱动开发)