Windows驅動之取消安全隊列

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來保存就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章