共用方式為


使用Driver-Supplied微調鎖定

管理自己的 IRP 佇列的驅動程式可以使用驅動程式提供的微調鎖定,而不是系統取消微調鎖定,以同步存取佇列。 您可以避免使用取消微調鎖定來改善效能,但絕對必要時除外。 因為系統只有一個取消微調鎖定,所以驅動程式有時可能需要等待該微調鎖定可供使用。 使用驅動程式提供的微調鎖定可消除此潛在延遲,並讓取消微調鎖定可供 I/O 管理員和其他驅動程式使用。 雖然系統在呼叫驅動程式的 Cancel 常式時仍會取得取消微調鎖定,但驅動程式可以使用自己的微調鎖定來保護其 IRP 佇列。

即使驅動程式不會排入擱置的 IRP 佇列,但以其他方式保留擁有權,該驅動程式也必須設定 IRP 的 Cancel 常式,而且必須使用微調鎖定來保護 IRP 指標。 例如,假設驅動程式將 IRP 標示為擱置中,然後將 IRP 指標當做內容傳遞至 IoTimer 常式。 驅動程式必須設定 Cancel 常式來取消計時器,而且必須在存取 IRP 時,在 Cancel 常式和計時器回呼中使用相同的微調鎖定。

任何將自己的 IRP 排入佇列並使用自己的微調鎖定的驅動程式都必須執行下列動作:

  • 建立微調鎖定來保護佇列。

  • 只有在保留此微調鎖定時,才設定並清除 Cancel 常式。

  • 如果 取消 常式在驅動程式取消佇列 IRP 時開始執行,請允許 Cancel 常式完成 IRP。

  • 取得在 Cancel 常式中保護佇列的鎖定。

若要建立微調鎖定,驅動程式會呼叫 KeInitializeSpinLock。 在下列範例中,驅動程式會將微調鎖定儲存在 DEVICE_CONTEXT 結構中,以及已建立的佇列:

typedef struct {
    LIST_ENTRYirpQueue;
    KSPIN_LOCK irpQueueSpinLock;
    ...
} DEVICE_CONTEXT;

VOID InitDeviceContext(DEVICE_CONTEXT *deviceContext)
{
    InitializeListHead(&deviceContext->irpQueue);
    KeInitializeSpinLock(&deviceContext->irpQueueSpinLock);
}

若要將 IRP 排入佇列,驅動程式會取得微調鎖定、呼叫 InsertTailList,然後將 IRP 標示為擱置中,如下列範例所示:

NTSTATUS QueueIrp(DEVICE_CONTEXT *deviceContext, PIRP Irp)
{
   PDRIVER_CANCEL  oldCancelRoutine;
   KIRQL  oldIrql;
   NTSTATUS  status;

   KeAcquireSpinLock(&deviceContext->irpQueueSpinLock, &oldIrql);

   // Queue the IRP and call IoMarkIrpPending to indicate
   // that the IRP may complete on a different thread.
   // N.B. It is okay to call these inside the spin lock
   // because they are macros, not functions.
   IoMarkIrpPending(Irp);
   InsertTailList(&deviceContext->irpQueue, &Irp->Tail.Overlay.ListEntry);

   // Must set a Cancel routine before checking the Cancel flag.
   oldCancelRoutine = IoSetCancelRoutine(Irp, IrpCancelRoutine);
   ASSERT(oldCancelRoutine == NULL);

   if (Irp->Cancel) {
      // The IRP was canceled. Check whether our cancel routine was called.
      oldCancelRoutine = IoSetCancelRoutine(Irp, NULL);
      if (oldCancelRoutine) {
         // The cancel routine was NOT called.  
         // So dequeue the IRP now and complete it after releasing the spin lock.
         RemoveEntryList(&Irp->Tail.Overlay.ListEntry);
         // Drop the lock before completing the request.
         KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);
         Irp->IoStatus.Status = STATUS_CANCELLED; 
         Irp->IoStatus.Information = 0;
         IoCompleteRequest(Irp, IO_NO_INCREMENT);
         return STATUS_PENDING;

      } else {
         // The Cancel routine WAS called.  
         // As soon as we drop our spin lock, it will dequeue and complete the IRP.
         // So leave the IRP in the queue and otherwise do not touch it.
         // Return pending since we are not completing the IRP here.
         
      }
   }

   KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);

   // Because the driver called IoMarkIrpPending while it held the IRP,
   // it must return STATUS_PENDING from its dispatch routine.
   return STATUS_PENDING;
}

如範例所示,驅動程式會在設定並清除 Cancel 常式時保留其微調鎖定。 範例佇列常式包含 兩次 IoSetCancelRoutine呼叫。

第一個呼叫會設定 IRP 的 Cancel 常式。 不過,因為佇列常式執行時可能已經取消 IRP,所以驅動程式必須檢查 IRP 的 Cancel 成員。

  • 如果已設定 Cancel ,則要求取消,而且驅動程式必須對 IoSetCancelRoutine 進行第二次呼叫,以查看是否已呼叫先前設定的 Cancel 常式。

  • 如果 IRP 已取消,但尚未呼叫 Cancel 常式,則目前的常式會清除 IRP 佇列,並使用STATUS_CANCELLED完成它。

  • 如果已取消 IRP,且已呼叫 Cancel 常式,則目前的傳回會標示 IRP 暫止並傳回STATUS_PENDING。 Cancel常式將會完成 IRP。

下列範例示範如何從先前建立的佇列中移除 IRP:

PIRP DequeueIrp(DEVICE_CONTEXT *deviceContext)
{
   KIRQL oldIrql;
   PIRP nextIrp = NULL;

   KeAcquireSpinLock(&deviceContext->irpQueueSpinLock, &oldIrql);

   while (!nextIrp && !IsListEmpty(&deviceContext->irpQueue)) {
      PDRIVER_CANCEL oldCancelRoutine;
      PLIST_ENTRY listEntry = RemoveHeadList(&deviceContext->irpQueue);

      // Get the next IRP off the queue.
      nextIrp = CONTAINING_RECORD(listEntry, IRP, Tail.Overlay.ListEntry);

      // Clear the IRP's cancel routine.
      oldCancelRoutine = IoSetCancelRoutine(nextIrp, NULL);

      // IoCancelIrp() could have just been called on this IRP. What interests us
      // is not whether IoCancelIrp() was called (nextIrp->Cancel flag set), but
      // whether IoCancelIrp() called (or is about to call) our Cancel routine.
      // For that, check the result of the test-and-set macro IoSetCancelRoutine.
      if (oldCancelRoutine) {
         // Cancel routine not called for this IRP. Return this IRP.
         ASSERT(oldCancelRoutine == IrpCancelRoutine);
      } else {
         // This IRP was just canceled and the cancel routine was (or will be)
         // called. The Cancel routine will complete this IRP as soon as we
         // drop the spin lock, so do not do anything with the IRP.
         // Also, the Cancel routine will try to dequeue the IRP, so make 
         // the IRP's ListEntry point to itself.
         ASSERT(nextIrp->Cancel);
         InitializeListHead(&nextIrp->Tail.Overlay.ListEntry);
         nextIrp = NULL;
      }
   }

   KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);

   return nextIrp;
}

在此範例中,驅動程式會在存取佇列之前取得相關聯的微調鎖定。 按住微調鎖定時,它會檢查佇列不是空的,並取得佇列的下一個 IRP。 然後它會呼叫 IoSetCancelRoutine 來重設 IRP 的 Cancel 常式。 因為當驅動程式清除 IRP 佇列並重設 Cancel 常式時,可能會取消 IRP,所以驅動程式必須檢查 IoSetCancelRoutine所傳回的值。 如果 IoSetCancelRoutine 傳回 Null,這表示 Cancel 常式已呼叫或即將呼叫,則清除佇列常式可讓 Cancel 常式完成 IRP。 然後,它會釋放保護佇列並傳回的鎖定。

請注意上述常式中的 InitializeListHead 用法。 驅動程式可以重新佇列 IRP,讓 Cancel 常式可以取消佇列,但呼叫 InitializeListHead會比較簡單,這會重新初始化 IRP 的 ListEntry 欄位,使其指向 IRP 本身。 使用自我參考指標很重要,因為在 Cancel 常式取得微調鎖定之前,清單的結構可能會變更。 如果清單結構變更,可能會使 ListEntry 的原始值無效, Cancel 常式可能會在取消佇列 IRP 時損毀清單。 但如果 ListEntry 指向 IRP 本身, 則 Cancel 常式一律會使用正確的 IRP。

接著,Cancel常式只會執行下列動作:

VOID IrpCancelRoutine(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
   DEVICE_CONTEXT  *deviceContext = DeviceObject->DeviceExtension;
   KIRQL  oldIrql;

   // Release the global cancel spin lock.  
   // Do this while not holding any other spin locks so that we exit at the right IRQL.
   IoReleaseCancelSpinLock(Irp->CancelIrql);

   // Dequeue and complete the IRP.  
   // The enqueue and dequeue functions synchronize properly so that if this cancel routine is called, 
   // the dequeue is safe and only the cancel routine will complete the IRP. Hold the spin lock for the IRP
   // queue while we do this.

   KeAcquireSpinLock(&deviceContext->irpQueueSpinLock, &oldIrql);

   RemoveEntryList(&Irp->Tail.Overlay.ListEntry);

   KeReleaseSpinLock(&deviceContext->irpQueueSpinLock, oldIrql);

   // Complete the IRP. This is a call outside the driver, so all spin locks must be released by this point.
   Irp->IoStatus.Status = STATUS_CANCELLED;
   IoCompleteRequest(Irp, IO_NO_INCREMENT);
   return;
}

I/O 管理員一律會在呼叫 Cancel 常式之前取得全域取消微調鎖定,因此 Cancel 常式的第一個工作是釋放此微調鎖定。 然後,它會取得可保護驅動程式 IRP 佇列的微調鎖定、從佇列中移除目前的 IRP、釋放其微調鎖定、完成具有STATUS_CANCELLED且沒有優先權提升的 IRP,然後傳回。

如需取消微調鎖定的詳細資訊,請參閱 Windows 驅動程式中的取消邏輯 白皮書。