Condividi tramite


Uso di un blocco spin Driver-Supplied

I driver che gestiscono le proprie code di IRP possono usare un blocco spin fornito dal driver, anziché il blocco di spin spin del sistema, per sincronizzare l'accesso alle code. È possibile migliorare le prestazioni evitando l'uso del blocco di rotazione annulla, tranne quando è assolutamente necessario. Poiché il sistema ha solo un blocco di rotazione annulla, a volte un driver potrebbe dover attendere che tale blocco di rotazione diventi disponibile. L'uso di un blocco spin fornito dal driver elimina questo potenziale ritardo e rende disponibile il blocco di rotazione annulla per il gestore di I/O e altri driver. Anche se il sistema acquisisce ancora il blocco di rotazione annulla quando chiama la routine Annulla del driver, un driver può usare il proprio blocco spin per proteggere la coda di IRP.

Anche se un driver non esegue una coda di irp in sospeso, ma mantiene la proprietà in qualche altro modo, tale driver deve impostare una routine Annulla per l'IRP e deve usare un blocco spin per proteggere il puntatore IRP. Si supponga, ad esempio, che un driver contrassegni un'IRP in sospeso, quindi passi il puntatore IRP come contesto a una routine IoTimer . Il driver deve impostare una routine Cancel che annulla il timer e deve usare lo stesso blocco spin sia nella routine Annulla che nel callback timer quando si accede all'IRP.

Qualsiasi driver che accoda i propri IRP e usa il proprio blocco spin deve eseguire le operazioni seguenti:

  • Creare un blocco di rotazione per proteggere la coda.

  • Impostare e cancellare la routine Annulla solo tenendo premuto questo blocco di rotazione.

  • Se la routine Cancel inizia a eseguire mentre il driver sta dequeuando un'IRP, consentire alla routine Annulla di completare l'IRP.

  • Acquisire il blocco che protegge la coda nella routine Annulla .

Per creare il blocco spin, il driver chiama KeInitializeSpinLock. Nell'esempio seguente il driver salva il blocco spin in una struttura DEVICE_CONTEXT insieme alla coda creata:

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

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

Per accodare un'IRP, il driver acquisisce il blocco spin, chiama InsertTailList e quindi contrassegna l'IRP in sospeso, come nell'esempio seguente:

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;
}

Come illustrato nell'esempio, il driver mantiene il blocco di rotazione mentre imposta e cancella la routine Annulla . La routine di accodamento di esempio contiene due chiamate a IoSetCancelRoutine.

La prima chiamata imposta la routine Cancel per l'IRP. Tuttavia, poiché l'IRP potrebbe essere stato annullato durante l'esecuzione della routine di accodamento, il driver deve controllare il membro Annulla dell'IRP.

  • Se Cancel è impostato, l'annullamento è stato richiesto e il driver deve effettuare una seconda chiamata a IoSetCancelRoutine per verificare se è stata chiamata la routine Annulla impostata in precedenza.

  • Se l'IRP è stato annullato, ma la routine Cancel non è ancora stata chiamata, la routine corrente dequeue l'IRP e la completa con STATUS_CANCELLED.

  • Se l'IRP è stato annullato e la routine Annulla è già stata chiamata, la restituzione corrente contrassegna l'IRP in sospeso e restituisce STATUS_PENDING. La routine Annulla completerà l'IRP.

Nell'esempio seguente viene illustrato come rimuovere un'IRP dalla coda creata in precedenza:

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;
}

Nell'esempio il driver acquisisce il blocco spin associato prima di accedere alla coda. Tenendo premuto il blocco di rotazione, verifica che la coda non sia vuota e ottiene il successivo IRP fuori dalla coda. Chiama quindi IoSetCancelRoutine per reimpostare la routine Annulla per l'IRP. Poiché l'IRP potrebbe essere annullato mentre il driver dequeue l'IRP e reimposta la routine Annulla , il driver deve controllare il valore restituito da IoSetCancelRoutine. Se IoSetCancelRoutine restituisce NULL, che indica che la routine Cancel è stata o verrà presto chiamata, la routine di dequeuing consente di completare la routine Cancel . Rilascia quindi il blocco che protegge la coda e restituisce.

Si noti l'uso di InitializeListHead nella routine precedente. Il driver potrebbe riquezionare l'IRP, in modo che la routine Cancel possa dequeuerla, ma è più semplice chiamare InitializeListHead, che reinizializza il campo ListEntry dell'IRP in modo che punti all'IRP stesso. L'uso del puntatore self-referencing è importante perché la struttura dell'elenco potrebbe cambiare prima che la routine Annulla acquisisca il blocco di spin. E se la struttura dell'elenco cambia, eventualmente rendendo il valore originale di ListEntry non valido, la routine Cancel potrebbe danneggiare l'elenco quando dequeue l'IRP. Tuttavia, se ListEntry punta all'IRP stesso, la routine Annulla userà sempre l'IRP corretta.

La routine Annulla , a sua volta, esegue semplicemente quanto segue:

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;
}

Il gestore I/O acquisisce sempre il blocco di rotazione globale prima di chiamare una routine Annulla , quindi la prima attività della routine Cancel consiste nel rilasciare questo blocco di spin spin. Acquisisce quindi il blocco di rotazione che protegge la coda di IRP del driver, rimuove l'IRP corrente dalla coda, rilascia il blocco spin, completa l'IRP con STATUS_CANCELLED e nessun aumento della priorità e restituisce.

Per altre informazioni sull'annullamento dei blocchi di spin, vedere il white paper Annulla logica nei driver di Windows .