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 .