Driver-Supplied 스핀 잠금 사용

자체 IRP 큐를 관리하는 드라이버는 시스템 취소 스핀 잠금 대신 드라이버 제공 스핀 잠금을 사용하여 큐에 대한 액세스를 동기화할 수 있습니다. 절대적으로 필요한 경우를 제외하고 취소 스핀 잠금을 사용하지 않도록 하여 성능을 향상시킬 수 있습니다. 시스템에 취소 스핀 잠금이 하나만 있기 때문에 드라이버가 해당 스핀 잠금을 사용할 수 있을 때까지 기다려야 할 수 있습니다. 드라이버 제공 스핀 잠금을 사용하면 이 잠재적인 지연이 제거되고 I/O 관리자 및 기타 드라이버에서 취소 스핀 잠금을 사용할 수 있습니다. 시스템은 드라이버의 취소 루틴을 호출할 때 여전히 취소 스핀 잠금을 획득하지만 드라이버는 자체 스핀 잠금을 사용하여 IRP의 큐를 보호할 수 있습니다.

드라이버가 보류 중인 IRP를 큐에 대기하지 않고 다른 방식으로 소유권을 유지하더라도 해당 드라이버는 IRP에 대한 취소 루틴을 설정해야 하며 IRP 포인터를 보호하기 위해 스핀 잠금을 사용해야 합니다. 예를 들어 드라이버가 보류 중인 IRP를 표시한 다음 IRP 포인터를 IoTimer 루틴에 컨텍스트로 전달한다고 가정합니다. 드라이버는 타이머를 취소하는 Cancel 루틴을 설정해야 하며 IRP에 액세스할 때 Cancel 루틴과 타이머 콜백 모두에서 동일한 스핀 잠금을 사용해야 합니다.

자체 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를 큐에 넣기 위해 드라이버는 다음 예제와 같이 스핀 잠금을 획득하고 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;
}

예제와 같이 드라이버는 취소 루틴을 설정하고 지우는 동안 스핀 잠금을 유지합니다. 샘플 큐 루틴에는 IoSetCancelRoutine에 대한 두 가지 호출이 포함됩니다.

첫 번째 호출은 IRP에 대한 취소 루틴을 설정합니다. 그러나 큐 루틴이 실행되는 동안 IRP가 취소되었을 수 있으므로 드라이버는 IRP의 Cancel 멤버를 검사 합니다.

  • Cancel이 설정되면 취소가 요청되고 드라이버가 IoSetCancelRoutine을 두 번째로 호출하여 이전에 설정한 Cancel 루틴이 호출되었는지 확인해야 합니다.

  • IRP가 취소되었지만 Cancel 루틴이 아직 호출되지 않은 경우 현재 루틴은 IRP를 큐에서 제거하고 STATUS_CANCELLED 완료합니다.

  • IRP가 취소되고 Cancel 루틴이 이미 호출된 경우 현재 반환은 보류 중인 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 드라이버에서 논리 취소 백서를 참조하세요.