Verwenden einer Driver-Supplied Spin-Sperre
Treiber, die ihre eigenen Warteschlangen von IRPs verwalten, können eine vom Treiber bereitgestellte Spinsperre anstelle der Systemabbruch-Spinsperre verwenden, um den Zugriff auf die Warteschlangen zu synchronisieren. Sie können die Leistung verbessern, indem Sie die Verwendung der Abbruch-Drehsperre vermeiden, außer wenn dies unbedingt erforderlich ist. Da das System nur über eine Abbruchsperre verfügt, muss ein Treiber manchmal warten, bis diese Spinsperre verfügbar wird. Die Verwendung einer vom Treiber bereitgestellten Drehsperre beseitigt diese potenzielle Verzögerung und stellt die Drehsperre für den E/A-Manager und andere Treiber zur Verfügung. Obwohl das System immer noch die Abbruch-Spin-Sperre erhält, wenn es die Cancel-Routine des Treibers aufruft, kann ein Treiber seine eigene Drehsperre verwenden, um seine Warteschlange mit IRPs zu schützen.
Auch wenn ein Treiber keine ausstehenden IRPs in der Warteschlange ausführt, sondern auf andere Weise den Besitz behält, muss dieser Treiber eine Cancel-Routine für den IRP festlegen und eine Drehsperre verwenden, um den IRP-Zeiger zu schützen. Angenommen, ein Treiber markiert eine ausstehende IRP und übergibt dann den IRP-Zeiger als Kontext an eine IoTimer-Routine . Der Treiber muss eine Cancel-Routine festlegen, die den Timer abbricht und beim Zugriff auf die IRP die gleiche Drehsperre sowohl in der Cancel-Routine als auch im Timer-Rückruf verwenden muss.
Jeder Treiber, der seine eigenen IRPs in die Warteschlange stellt und eine eigene Drehsperre verwendet, muss folgendes tun:
Erstellen Sie eine Drehsperre, um die Warteschlange zu schützen.
Legen Und löschen Sie die Routine Abbrechen nur, wenn Sie diese Drehsperre gedrückt halten.
Wenn die Cancel-Routine ausgeführt wird, während der Treiber eine IRP aus dem Löschvorgang entfernt, lassen Sie die Abbruchroutine zu, um den IRP abzuschließen.
Rufen Sie die Sperre ab, die die Warteschlange in der Cancel-Routine schützt.
Um die Drehsperre zu erstellen, ruft der Treiber KeInitializeSpinLock auf. Im folgenden Beispiel speichert der Treiber die Drehsperre in einer DEVICE_CONTEXT Struktur zusammen mit der erstellten Warteschlange:
typedef struct {
LIST_ENTRYirpQueue;
KSPIN_LOCK irpQueueSpinLock;
...
} DEVICE_CONTEXT;
VOID InitDeviceContext(DEVICE_CONTEXT *deviceContext)
{
InitializeListHead(&deviceContext->irpQueue);
KeInitializeSpinLock(&deviceContext->irpQueueSpinLock);
}
Um eine IRP in eine Warteschlange zu stellen, ruft der Treiber die Spinsperre ab, ruft InsertTailList auf und markiert dann die ausstehende IRP, wie im folgenden Beispiel gezeigt:
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;
}
Wie das Beispiel zeigt, behält der Treiber seine Drehsperre bei, während er die Cancel-Routine festlegt und löscht. Die Beispielwarteschlangenroutine enthält zwei Aufrufe von IoSetCancelRoutine.
Der erste Aufruf legt die Cancel-Routine für die IRP fest. Da die IRP jedoch möglicherweise abgebrochen wurde, während die Warteschlangenroutine ausgeführt wird, muss der Treiber das Element Abbrechen des IRP überprüfen.
Wenn Abbrechen festgelegt ist, wurde die Abbruchanforderung angefordert, und der Treiber muss einen zweiten Aufruf an IoSetCancelRoutine senden, um zu sehen, ob die zuvor festgelegte Cancel-Routine aufgerufen wurde.
Wenn die IRP abgebrochen wurde, aber die Cancel-Routine noch nicht aufgerufen wurde, wird die IRP von der aktuellen Routine entfernt und mit STATUS_CANCELLED abgeschlossen.
Wenn die IRP abgebrochen wurde und die Cancel-Routine bereits aufgerufen wurde, markiert die aktuelle Rückgabe die ausstehende IRP und gibt STATUS_PENDING zurück. Die Cancel-Routine schließt die IRP ab.
Das folgende Beispiel zeigt, wie Sie ein IRP aus der zuvor erstellten Warteschlange entfernen:
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;
}
Im Beispiel ruft der Treiber die zugeordnete Spinsperre ab, bevor er auf die Warteschlange zugreift. Beim Halten der Spin-Sperre wird überprüft, ob die Warteschlange nicht leer ist, und ruft den nächsten IRP aus der Warteschlange ab. Anschließend wird IoSetCancelRoutine aufgerufen, um die Cancel-Routine für die IRP zurückzusetzen. Da die IRP abgebrochen werden konnte, während der Treiber die IRP aus der Warteschlange ausgibt und die Cancel-Routine zurücksetzt, muss der Treiber den von IoSetCancelRoutine zurückgegebenen Wert überprüfen. Wenn IoSetCancelRoutineNULL zurückgibt, was angibt, dass die Cancel-Routine entweder aufgerufen wurde oder bald aufgerufen wird, lässt die Dequeuierungsroutine die Cancel-Routine den IRP abschließen. Anschließend wird die Sperre, die die Warteschlange schützt, freigegeben und zurückgegeben.
Beachten Sie die Verwendung von InitializeListHead in der vorherigen Routine. Der Treiber könnte den IRP erneut ausstellen, sodass die Cancel-Routine sie dequenieren kann, aber es ist einfacher , InitializeListHead aufzurufen, wodurch das ListEntry-Feld des IRP neu initialisiert wird, sodass es auf den IRP selbst verweist. Die Verwendung des selbstreferenzierenden Zeigers ist wichtig, da sich die Struktur der Liste ändern kann, bevor die Cancel-Routine die Drehsperre erhält. Und wenn sich die Listenstruktur ändert und möglicherweise der ursprüngliche Wert von ListEntry ungültig wird, kann die Cancel-Routine die Liste beschädigen, wenn sie die IRP ausgibt. Wenn ListEntry jedoch auf den IRP selbst verweist, verwendet die Cancel-Routine immer den richtigen IRP.
Die Abbruchroutine wiederum führt einfach Folgendes aus:
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;
}
Der E/A-Manager ruft immer die globale Abbruch-Spinsperre ab, bevor er eine Cancel-Routine aufruft. Daher besteht die erste Aufgabe der Cancel-Routine darin, diese Drehsperre zu lösen. Anschließend wird die Drehsperre abgerufen, die die IRPs-Warteschlange des Treibers schützt, den aktuellen IRP aus der Warteschlange entfernt, die Spin-Sperre freigegeben, die IRP mit STATUS_CANCELLED und ohne Prioritätsschub abgeschlossen und zurückgegeben.
Weitere Informationen zum Abbrechen von Drehsperren finden Sie im Whitepaper Abbrechen der Logik in Windows-Treibern .