Windows驱动之取消安全队列

文章目录

  • Windows驱动之取消安全队列
    • 1. IO_CSQ
    • 2. IoCsqInitialize
    • 3. IoCsqInsertIrp
    • 4. IoCsqRemoveNextIrp
    • 5. IRP的取消
    • 6. 总结

Windows驱动之取消安全队列

对于Windows驱动来说,IRP的取消一直都是比较复杂的问题,很多有经验的驱动开发人员都不能完美的处理好IRP的取消问题。关于IRP的取消有两种会出现:

  1. CancelIo函数的调用。
  2. 线程的结束。

取消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;

这个结构体提供如下信息:

  1. CsqInsertIrp,提供插入回调函数;当设备实施异步IRP的时候,就会回调这个函数,一般来说,这个函数的主要用途是通过队列将IRP保存起来。
  2. CsqRemoveIrp: 这个是移除IRP的回调函数,意思是IRP取消获取其他原因删除的时候,就会调用这个函数来移除一个IRP。
  3. CsqPeekNextIrp : 这个是从异步IRP队列中弹出一个IRP调用的函数,一般来说,这个函数从异步队列中出队一个IRP。
  4. CsqAcquireLockCsqReleaseLock : 我们在插入IRP,移除IRP的时候都需要保证同步,这两个函数就是用来保证同步使用的。
  5. 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;
}

这里的操作可以分为几个步骤:

  1. Csq->CsqAcquireLock(Csq, &Irql);占用我们CSQ的锁,那么其他线程将不能再操作CSQ了。
  2. Csq->CsqInsertIrp(Csq, Irp); 将IRP插入到CSQ队列中。
  3. (void)IoSetCancelRoutine(Irp, IopCsqCancelRoutine);设置IRP的取消例程。
  4. 此时,这个IRP虽然放到了队列中,但是这个期间可能被取消了,所以我们判断一下是否IRP已经被取消(if(!Irp->Cancel)):
    1. 如果IRP没有被取消,那么异步操作成功(插入到队列中)。
    2. 如果此时IRP被取消了有两种可能:
      1. IRP被另外线程取消了,并且调用了取消例程,那么我们不应该再操作任何IRP了,因为IRP已经调用取消例程取消了;满足这种条件是IoSetCancelRoutine(Irp, NULL)返回一个NULL对象,说明取消例程被取出了。
      2. 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;
}

这里几个操作:

  1. Csq->CsqAcquireLock(Csq, &Irql)锁同步整个CSQ的操作。
  2. Irp = Csq->CsqPeekNextIrp(Csq, Irp, PeekContext)从队列中取出IRP。
  3. 如果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来保存就可以了。

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