管理自己 IRP 佇列的驅動程式開發者可以使用驅動程式提供的自旋鎖,而不是系統取消自旋鎖,以同步處理佇列的存取。 您可以避免使用取消微調鎖定來改善效能,但絕對必要時除外。 因為系統只有一個取消自旋鎖,因此驅動程式有時可能需要等待該自旋鎖可用。 使用驅動程式提供的旋轉鎖定可以消除此潛在的延遲,並使取消旋轉鎖定可供 I/O 管理器和其他驅動程式使用。 雖然系統在呼叫驅動程式的 Cancel 例程時仍會取得取消旋轉鎖,但驅動程式可以使用自己的旋轉鎖來保護其 IRP 佇列。
即使驅動程式未將擱置的 IRP 排入佇列,但以其他方式保留其擁有權,該驅動程式仍必須為 IRP 設置 Cancel 例程,且必須使用旋轉鎖來保護 IRP 指標。 例如,假設驅動程式將 IRP 標示為擱置中,然後將 IRP 指標當做內容傳遞至 IoTimer 例程。 驅動程式必須設定 Cancel 例程來取消定時器,並且在存取 IRP 時,必須在 Cancel 例程和定時器回呼中使用相同的自旋鎖。
將其自己的 IRP 排入佇列並使用自旋鎖的任何驅動程式都必須執行下列動作:
建立自旋鎖來保護佇列。
只有在保持此旋轉鎖的狀態下,才設定並清除 取消 例行程序。
如果 取消 例程在驅動程式從佇列中取出 IRP 時開始執行,則允許 取消 例程完成該 IRP。
取得在 Cancel 例程中保護佇列的鎖。
若要建立自旋鎖,驅動程式會呼叫 KeInitializeSpinLock。 在下列範例中,驅動程式會將旋轉鎖連同其建立的佇列一起儲存在 DEVICE_CONTEXT 結構中:
typedef struct {
LIST_ENTRYirpQueue;
KSPIN_LOCK irpQueueSpinLock;
...
} DEVICE_CONTEXT;
VOID InitDeviceContext(DEVICE_CONTEXT *deviceContext)
{
InitializeListHead(&deviceContext->irpQueue);
KeInitializeSpinLock(&deviceContext->irpQueueSpinLock);
}
若要將 IRP(I/O 請求封包)排入佇列,驅動程式會取得旋轉鎖定、呼叫 InsertTailList,然後將 IRP 標示為擱置中,如下列範例所示:
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;
}
如範例所示,驅動程式在設定並清除 Cancel 例程時會保持其自旋鎖。 範例佇列例程包含兩個 IoSetCancelRoutine 呼叫。
第一個呼叫會設定 IRP 的 Cancel 例程。 不過,由於 IRP 可能會在佇列例程執行時取消,因此驅動程式必須檢查 IRP 的 Cancel 成員。
如果已設定 Cancel ,則會要求取消,而且驅動程式必須對 IoSetCancelRoutine 進行第二次呼叫,以查看先前設定 的 Cancel 例程是否已呼叫。
如果 IRP 已取消,但尚未呼叫 Cancel 例程,則目前的例程會清除 IRP,並使用STATUS_CANCELLED完成它。
如果 IRP 已取消且已呼叫 Cancel 例程,則目前的傳回會標示 IRP 暫止並傳回STATUS_PENDING。 Cancel 例程將會完成 IRP。
下列範例示範如何從先前建立的佇列中移除 IRP:
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;
}
在此範例中,驅動程式會在存取佇列之前取得相關聯的自旋鎖。 在持有自旋鎖時,它會檢查佇列是否為空,並從佇列中取出下一個 IRP。 然後它會呼叫 IoSetCancelRoutine 來重設 IRP 的 Cancel 例程。 因為 IRP 可能會在驅動程式從佇列中取出 IRP 並重設 Cancel 例程的過程中被取消,因此驅動程式必須檢查 IoSetCancelRoutine 傳回的值。 如果 IoSetCancelRoutine 傳回 NULL,表示已呼叫或即將呼叫 Cancel 例程,則清除佇列例程可讓 Cancel 例程完成 IRP。 然後,它會釋放保護佇列的鎖定並返回。
請注意,在上述例程中使用 InitializeListHead 。 驅動程式可以重新佇列 IRP,讓 Cancel 例程可以清除佇列,但呼叫 InitializeListHead 會更簡單,它會重新初始化 IRP 的 ListEntry 欄位,使其指向 IRP 本身。 使用自我指涉指標很重要,因為列表的結構可能會在 Cancel 例程獲得自旋鎖之前變更。 如果清單結構變更,可能會使 ListEntry 的原始值無效, Cancel 例程可能會在清除 IRP 佇列時損毀清單。 但是,如果 ListEntry 指向 IRP 本身, 則 Cancel 例程一律會使用正確的 IRP。
接著, Cancel 例程只會執行下列動作:
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;
}
I/O 管理員一律會在呼叫 Cancel 例程之前取得全域取消微調鎖定,因此 Cancel 例程的第一個工作是釋放此微調鎖定。 然後,它會取得自旋鎖,以保護驅動程式的 IRP 佇列,並從佇列中移除目前的 IRP,釋放自旋鎖,以 STATUS_CANCELLED 並且不提高優先順序的方式完成 IRP,然後返回。
如需有關取消自旋鎖定的詳細資訊,請參閱 Windows 驅動程式中的取消邏輯 白皮書。