Condividi tramite


Uso di un blocco spin Driver-Supplied

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

Anche se un driver non accoda gli IRP in sospeso, ma mantiene la proprietà in un altro modo, quel driver deve impostare una routine Cancel 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 utilizzare lo stesso blocco di rotazione sia nella routine Cancel che nel callback timer durante l'accesso all'IRP.

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

  • Creare un blocco di selezione per proteggere la coda.

  • Impostare e deselezionare la routine Cancel solo tenendo premuto questo blocco di selezione.

  • Se la routine Cancel viene avviata mentre il driver sta dequeando un IRP, consentire alla routine Cancel di completare l'IRP.

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

Per creare il spinlock, il driver chiama KeInitializeSpinLock. Nell'esempio seguente, il driver salva lo spinlock in una struttura DEVICE_CONTEXT insieme alla coda che ha creato:

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 lo spin lock, chiama InsertTailList e quindi contrassegna l'IRP come 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 selezione mentre imposta e cancella la routine Cancel . 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 Cancel dell'IRP.

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

  • Se l'IRP è stato annullato ma la routine Cancel non è ancora stata chiamata, allora la routine corrente rimuove l'IRP dalla coda e lo completa con STATUS_CANCELLED.

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

L'esempio seguente mostra 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 lo spin lock associato prima di accedere alla coda. Tenendo lo spinlock, verifica che la coda non sia vuota e ottiene il successivo IRP dalla coda. Chiama quindi IoSetCancelRoutine per reimpostare la routine Cancel per l'IRP. Poiché l'IRP potrebbe essere annullato mentre il driver rimuove dalla coda l'IRP e reimposta la routine Cancel, 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 alla routine Cancel di completare l'IRP. Quindi rilascia il blocco che protegge la coda e termina.

Si noti l'uso di InitializeListHead nella routine precedente. Il driver potrebbe rimettere in coda l'IRP, in modo che la routine Cancel possa rimuoverlo dalla coda, ma è più semplice chiamare InitializeListHead, che reinizializza il campo ListEntry dell'IRP in modo che punti a l'IRP stesso. L'uso del puntatore auto-referente è importante perché la struttura dell'elenco potrebbe cambiare prima che la routine Cancel acquisisca il blocco a rotazione. Se la struttura dell'elenco viene modificata, potenzialmente rendendo non valido il valore originale di ListEntry, la routine Cancel potrebbe danneggiare l'elenco quando rimuove l'IRP dalla coda. Tuttavia, se ListEntry punta all'IRP stesso, la routine Cancel userà sempre l'IRP corretto.

La routine Cancel , a sua volta, esegue semplicemente le operazioni seguenti:

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 di I/O acquisisce sempre il blocco di selezione dell'annullamento globale prima di chiamare una routine Cancel , quindi la prima attività della routine Annulla consiste nel rilasciare questo blocco di selezione. Acquisisce quindi lo spin lock che protegge la coda di IRP del driver, rimuove l'IRP corrente dalla coda, rilascia lo spin lock, completa l'IRP con STATUS_CANCELLED e senza alcun boost di priorità, e termina.

Per ulteriori informazioni sull'annullamento degli spin lock, consultare il white paper Cancel Logic in Windows Drivers.