次の方法で共有


ドライバーによって提供されるスピン ロックの使用

IRP の独自のキューを管理するドライバーは、システムのキャンセル スピン ロックではなく、ドライバーが提供するスピン ロックを使用して、キューへのアクセスを同期できます。 特別に必要な場合を除き、キャンセル スピン ロックの使用を回避することで、パフォーマンスを向上させることができます。 システムにはスピン ロックのキャンセルが 1 つしかないため、ドライバーはそのスピン ロックが使用可能になるまで待機しなければならない場合があります。 ドライバーが提供するスピン ロックを使用すると、この潜在的な遅延がなくなり、I/O マネージャーやその他のドライバーでキャンセル スピン ロックを使用できるようになります。 システムは、ドライバー のキャンセル ルーチンを呼び出すときに、まだキャンセル スピン ロックを取得しますが、ドライバーは、IRP のキューを保護する独自のスピン ロックを使用できます。

ドライバーが保留中の IRP をキューに入れず、他の方法で所有権を保持する場合でも、そのドライバーは IRP のキャンセル ルーチンを設定する必要があり、IRP ポインターを保護するためにスピン ロックを使用する必要があります。 たとえば、ドライバーが保留中の IRP をマークし、IoTimer ルーチンにコンテキストとして IRP ポインターを渡したとします。 ドライバーはタイマーをキャンセルするキャンセル ルーチンを設定し、IRP にアクセスする際には キャンセル ルーチンとタイマー コールバックの両方で同じスピン ロックを使用する必要があります。

独自の IRP をキューに入れ、独自のスピン ロックを使用するドライバーは、次の操作を行う必要があります。

  • スピン ロックを作成してキューを保護します。

  • このスピン ロックを保持している間にのみ、キャンセル ルーチンを設定してクリアします。

  • ドライバーが IRP のデキュー中にキャンセル ルーチンの実行を開始する場合は、キャンセル ルーチンが IRP を完了できるようにします。

  • キャンセル ルーチンのキューを保護するロックを取得します。

スピン ロックを作成するために、ドライバーは KeInitializeSpinLock を呼び出します。 次の例では、ドライバーは、スピン ロックを作成したキューと共に DEVICE_CONTEXT 構造体に保存します。

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

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

IRP をキューに入れるには、次の例のように、ドライバーはスピン ロックを取得し、InsertTcailList を呼び出し、保留中の 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;
}

この例に示すように、ドライバーはスピン ロックを保持しながら、キャンセル ルーチンの設定とクリアを行います。 サンプル キュー ルーチンには、IoSetCancelRoutine への 2 つの呼び出しが含まれています。

最初の呼び出しは、IRP のキャンセル ルーチンを設定します。 ただし、キュー ルーチンの実行中に IRP が取り消された可能性があるため、ドライバーは IRP のキャンセル メンバーをチェックする必要があります。

  • キャンセル が設定されている場合、取り消しが要求され、ドライバーは IoSetCancelRoutine に対して 2 回目の呼び出しを行って、以前に設定されたキャンセル ルーチンが呼び出されたかどうかを確認する必要があります。

  • IRP が取り消されたものの、キャンセル ルーチンがまだ呼び出されていない場合、現在のルーチンは IRP をデキューし、STATUS_CANCELLED で完了します。

  • IRP が取り消され、キャンセル ルーチンが既に呼び出されている場合、現在の戻り値は保留中の IRP をマークし、STATUS_PENDING を返します。 キャンセル ルーチンは、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 のキャンセル ルーチンをリセットします。 ドライバーが IRP をデキューし、キャンセル ルーチンをリセットするときに IRP を取り消すことができるため、ドライバーは IoSetCancelRoutine によって返される値をチェックする必要があります。 IoSetCancelRoutineNULL を返す場合は、キャンセル ルーチンが呼び出されたか、間もなく呼び出されることを示します。その後、デキュー ルーチンはキャンセル ルーチンに IRP を完了します。 次に、キューを保護して返すロックを解放します。

前のルーチンでの InitializeListHead の使用に注意してください。 ドライバーは、IRP を再度キューに入れ、キャンセル ルーチンは、それをデキューすることができますが、IRP 自体を指す IRP の ListEntry フィールドを再初期化する InitializeListHead を呼び出す方が簡単です。 キャンセル ルーチンがスピン ロックを取得する前にリストの構造が変更される可能性があるため、自己参照ポインターの使用は重要です。 また、リスト構造が変更された場合、ListEntry の元の値が無効になる可能性があります。キャンセル ルーチンは、IRP をデキューするときにリストを破損する可能性があります。 ただし、ListEntry が IRP 自体を指している場合、キャンセル ルーチンは常に正しい IRP を使用します。

次に、キャンセル ルーチンは次の操作を行います。

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 マネージャーは、キャンセル ルーチンを呼び出す前に常にグローバルキャンセル スピン ロックを取得するため、キャンセル ルーチンの最初のタスクは、このスピン ロックを解放することです。 次に、IRP のドライバーのキューを保護するスピン ロックを取得し、キューから現在の IRP を削除し、そのスピン ロックを解放し、STATUS_CANCELLED と優先度ブーストなしで IRP を完了し、戻ります。

スピン ロックのキャンセルの詳細については、Windows ドライバーのキャンセル ロジックに関するホワイト ペーパーを参照してください。