Compartilhar via


Usando um Driver-Supplied Spin Lock

Os drivers que gerenciam suas próprias filas de IRPs podem usar um bloqueio de rotação fornecido pelo driver, em vez do sistema cancelar o bloqueio de rotação, para sincronizar o acesso às filas. Você pode melhorar o desempenho evitando o uso do bloqueio de rotação de cancelamento, exceto quando absolutamente necessário. Como o sistema tem apenas um bloqueio de rotação de cancelamento, às vezes, um driver pode ter que esperar que esse bloqueio de rotação fique disponível. O uso de um bloqueio de rotação fornecido pelo driver elimina esse possível atraso e disponibiliza o bloqueio de rotação de cancelamento para o gerente de E/S e outros drivers. Embora o sistema ainda adquira o bloqueio de rotação de cancelamento ao chamar a rotina cancelar do driver, um driver pode usar seu próprio bloqueio de rotação para proteger sua fila de IRPs.

Mesmo que um driver não enfileira IRPs pendentes, mas mantenha a propriedade de alguma outra maneira, esse driver deve definir uma rotina Cancelar para o IRP e deve usar um bloqueio de rotação para proteger o ponteiro IRP. Por exemplo, suponha que um driver marque um IRP pendente e passe o ponteiro IRP como contexto para uma rotina IoTimer . O driver deve definir uma rotina Cancelar que cancele o temporizador e deve usar o mesmo bloqueio de rotação na rotina Cancelar e no retorno de chamada do temporizador ao acessar o IRP.

Qualquer driver que enfileira seus próprios IRPs e usa seu próprio bloqueio de rotação deve fazer o seguinte:

  • Crie um bloqueio de rotação para proteger a fila.

  • Defina e desmarque a rotina Cancelar somente enquanto mantém esse bloqueio de rotação.

  • Se a rotina Cancelar começar a ser executada enquanto o driver estiver desativando um IRP, permita que a rotina Cancelar conclua o IRP.

  • Adquira o bloqueio que protege a fila na rotina Cancelar .

Para criar o bloqueio de rotação, o driver chama KeInitializeSpinLock. No exemplo a seguir, o driver salva o bloqueio de rotação em uma estrutura DEVICE_CONTEXT junto com a fila que ele criou:

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

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

Para enfileirar um IRP, o driver adquire o bloqueio de rotação, chama InsertTailList e marca o IRP pendente, como no exemplo a seguir:

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

Como mostra o exemplo, o driver mantém o bloqueio de rotação enquanto define e limpa a rotina Cancelar . A rotina de enfileiramento de exemplo contém duas chamadas para IoSetCancelRoutine.

A primeira chamada define a rotina Cancelar para o IRP. No entanto, como o IRP pode ter sido cancelado enquanto a rotina de enfileiramento está em execução, o driver deve marcar o membro Cancel do IRP.

  • Se Cancelar estiver definido, o cancelamento será solicitado e o driver deverá fazer uma segunda chamada para IoSetCancelRoutine para ver se a rotina de Cancelamento definida anteriormente foi chamada.

  • Se o IRP tiver sido cancelado, mas a rotina Cancelar ainda não tiver sido chamada, a rotina atual removerá o IRP e o concluirá com STATUS_CANCELLED.

  • Se o IRP tiver sido cancelado e a rotina Cancelar já tiver sido chamada, o retorno atual marcará o IRP pendente e retornará STATUS_PENDING. A rotina Cancelar concluirá o IRP.

O exemplo a seguir mostra como remover um IRP da fila criada anteriormente:

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

No exemplo, o driver adquire o bloqueio de rotação associado antes de acessar a fila. Ao manter o bloqueio de rotação, ele verifica se a fila não está vazia e obtém o próximo IRP da fila. Em seguida, ele chama IoSetCancelRoutine para redefinir a rotina Cancelar para o IRP. Como o IRP pode ser cancelado enquanto o driver desativa o IRP e redefine a rotina Cancelar, o driver deve marcar o valor retornado por IoSetCancelRoutine. Se IoSetCancelRoutine retornar NULL, o que indica que a rotina Cancelar foi ou será chamada em breve, a rotina de desqueução permitirá que a rotina Cancelar conclua o IRP. Em seguida, ele libera o bloqueio que protege a fila e retorna.

Observe o uso de InitializeListHead na rotina anterior. O driver pode redirecionar o IRP, para que a rotina Cancelar possa desempacolá-lo, mas é mais simples chamar InitializeListHead, que reinicializa o campo ListEntry do IRP para que ele aponte para o próprio IRP. O uso do ponteiro de autorreferência é importante porque a estrutura da lista pode ser alterada antes que a rotina Cancelar adquira o bloqueio de rotação. E se a estrutura de lista for alterada, possivelmente tornando o valor original de ListEntry inválido, a rotina Cancelar poderá corromper a lista quando desativar o IRP. Mas se ListEntry apontar para o IRP em si, a rotina Cancelar sempre usará o IRP correto.

A rotina Cancelar , por sua vez, simplesmente faz o seguinte:

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

O gerente de E/S sempre adquire o bloqueio de rotação de cancelamento global antes de chamar uma rotina cancelar , portanto, a primeira tarefa da rotina Cancelar é liberar esse bloqueio de rotação. Em seguida, ele adquire o bloqueio de rotação que protege a fila de IRPs do driver, remove o IRP atual da fila, libera seu bloqueio de rotação, conclui o IRP com STATUS_CANCELLED e sem aumento de prioridade e retorna.

Para obter mais informações sobre como cancelar bloqueios de rotação, consulte o white paper Cancelar Lógica em Drivers do Windows .