粒子物理學裡有關於宇宙的“標準模型”,WDM也是這樣。圖5-5顯示了一個典型的IRP在各個處理階段的所有權流程。並不是每種IRP都經過這些步驟,由於裝置類型和IRP種類的不同某些步驟會改變或根本不存在。儘管這個過程可能有各種變化形式,但這個圖為我們將要展開的討論提供了一個很好的起點。
比你想象的更複雜...
當你第一次遇到IRP處理標準模型這個概念時,你也許認為這是個比較複雜的概念。但不幸的是,這個概念還不能滿足所有問題,比如熱拔插裝置、動態資源再分配,和電源管理等等。在後面的章節中,我將描述處理這些額外問題的其它IRP排隊和取消方法。標準模型僅作為你閱讀時清晰的參考模型!
拋開這些特殊問題,許多裝置仍能利用這個標準模型。如果你的裝置在系統運行時不能被刪除或重配置,並且在低電源狀態下拒絕I/O請求,那麼你可以使用這個標準模型。
建立IRP
IRP開始於某個實體調用I/O管理器函數建立它。在中,我使用術語“I/O管理器”來描述這個實體,儘管系統中確實有一個單獨的系統組件用於建立IRP。事實上,更精確地說,應該是某個實體建立了IRP,並不是作業系統的某個常式建立了IRP。例如,你的驅動程式有時會建立IRP,而此時出現在圖中第一個方框中的實體就應該是你的驅動程式。
可以使用下面任何一種函數建立IRP:
- IoBuildAsynchronousFsdRequest 建立非同步IRP(不需要等待其完成)。該函數和下一個函數僅適用於建立某些類型的IRP。
- IoBuildSynchronousFsdRequest 建立同步IRP(需要等待其完成)。
- IoBuildDeviceIoControlRequest 建立一個同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL請求。
- IoAllocateIrp 建立上面三個函數不支援的其它種類的IRP。
前兩個函數中的Fsd表明這些函數專用於檔案系統驅動程式(FSD)。雖然FSD是這兩個函數的主要使用者,但其它驅動程式也可以調用這些函數。DDK還公開了一個IoMakeAssociatedIrp函數,該函數用於建立某些IRP的從屬IRP。WDM驅動程式不應該使用這個函數。
決定該調用哪一個函數,和決定對IRP執行什麼額外的初始化是更複雜的問題,我將在本章的結尾再回到這個問題上。
發往派遣常式
建立完IRP後,你可以調用IoGetNextIrpStackLocation函數獲得該IRP第一個堆棧單元的指標。然後初始化這個堆棧單元。在初始化過程的最後,你需要填充MajorFunction代碼。堆棧單元初始化完成後,就可以調用IoCallDriver函數把IRP發送到裝置驅動程式:
PDEVICE_OBJECT DeviceObject; //something gives you thisPIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp);stack->MajorFunction = IRP_MJ_Xxx;NTSTATUS status = IoCallDriver(DeviceObject, Irp);
IoCallDriver函數的第一個參數是你在某處獲得的裝置對象的地址。我將在本章的結尾處描述獲得裝置對象指標的兩個常用方法。在這裡,我們先假設你已經有了這個指標。
IRP中的第一個堆棧單元指標被初始化成指向該堆棧單元之前的堆棧單元,因為I/O堆棧實際上是IO_STACK_LOCATION結構數組,你可以認為這個指標被初始化為指向一個不存在的“-1”元素,因此當我們要初始化第一個堆棧單元時我們實際需要的是“下一個”堆棧單元。IoCallDriver將沿著這個堆棧指標找到第0個表項,並提取我們放在那裡的主功能代碼,在上例中為IRP_MJ_Xxx。然後IoCallDriver函數將利用DriverObject指標找到裝置對象中的MajorFunction表。IoCallDriver將使用主功能代碼索引這個表,最後調用找到的地址(派遣函數)。
你可以把IoCallDriver函數想象為下面代碼:
NTSTATUS IoCallDriver(PDEVICE_OBJECT device, PIRP Irp){ IoSetNextIrpStackLocation(Irp); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); stack->DeviceObject = device; ULONG fcn = stack->MajorFunction; PDRIVER_OBJECT driver = device->DriverObject; return (*driver->MajorFunction[fcn])(device, Irp);}
派遣常式的職責
IRP派遣常式的原型看起來像下面這樣:
NTSTATUS DispatchXxx(PDEVICE_OBJECT device, PIRP Irp){ PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp);DeviceExtension;
- 你通常需要訪問當前堆棧單元以確定參數或副功能碼。
- 你可能還需要訪問你建立的裝置擴充。
- 你將向IoCallDriver函數返回某個NTSTATUS代碼,而IoCallDriver函數將把這個狀態代碼返回給它的調用者。
在本書中,我使用DispatchXxx(如DispatchRead、DispatchPnp,等等)來代表例子驅動程式中的派遣常式。其它人可能會使用另外的約定,但Microsoft推薦用這樣的方法,例如,如果你的驅動程式名為RANDOM.SYS,那麼你應該命名IRP_MJ_READ派遣函數為RandomDispatchRead。這個方法使驅動程式調試跟蹤起來更容易,但它同時也需要你輸入更多的文字。由於這些名稱在驅動程式的名空間之外是不可見的,所以由你自己決定是使用Microsoft推薦的命名方案,還是使用你認為更有意義的命名方法。
在上面派遣函數原型中省略符號的地方,是派遣函數必須做出決定的地方,有三種選擇:
- 派遣函數立即完成該IRP。
- 把該IRP傳遞到處於同一堆棧的下層驅動程式。
- 排隊該IRP以便由這個驅動程式中的其它常式來處理。
我將在本章詳細討論這三種選擇,但在這裡我僅討論排隊的可能性,因為這個過程就是IRP處理標準模型所描述的。你知道,當有大量讀寫請求進入裝置時,通常需要把這些請求放入一個隊列中,以便使硬體訪問序列化。
每個裝置對象都內建一個請求隊列對象,下面是使用這個隊列的標準方法:
NTSTATUS DispatchXxx(...){ ... IoMarkIrpPending(Irp);
- 無論何時,當你的派遣常式返回STATUS_PENDING狀態碼時,你應該先調用這個IoMarkIrpPending函數,以協助I/O管理器避免內部競爭。我們必須在放棄IRP所有權之前做這一點。
- 如果裝置正忙,IoStartPacket就把請求放到隊列中。如果裝置空閑,IoStartPacket將把裝置置成忙併調用StartIo常式。IoStartPacket的第三個參數是用於排序隊列的鍵(ULONG)的地址,例如磁碟驅動程式將在這裡指定一個柱面地址以提供順序搜尋的排隊。如過你在這裡指定一個NULL,則該請求被加到隊列的尾部。最後一個參數是取消常式的地址。我將在本章的後面討論取消常式,這種常式比較複雜。
- 返回STATUS_PENDING以通知調用者我們沒有完成這個IRP。
注意,一旦我們調用了IoStartPacket函數,就不要再碰IRP。因為在該函數返回之前,IRP可能已經被完成並且其佔用的記憶體可能被釋放,而我們擁有的該IRP的指標也許是無效的。
StartIo常式每處理一個IRP,I/O管理器就調用一次StartIo常式:
VOID StartIo(PDEVICE_OBJECT device, PIRP Irp){ PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension; ...}
StartIo常式在DISPATCH_LEVEL級上獲得控制,這意味著該函數不能產生任何頁故障。另外,裝置對象的CurrentIrp域和Irp參數都指向I/O管理器送來的IRP。
StartIo的工作是就著手處理IRP。如何做要完全取決於你的裝置。通常你需要訪問硬體寄存器,但可能有其它常式,如你的插斷服務常式,或者是驅動程式中的其它常式也需要訪問這些寄存器。實際上,有時著手一個新操作的最容易的方式是在裝置擴充中儲存某些狀態資訊,然後偽造一個中斷。由於這些方法的執行都需要在一個自旋鎖的保護之下,而這個自旋鎖與保護你的ISR所使用的是同一個自旋鎖,所以正確的方法是調用KeSynchronizeExecution函數。例如:
VOID StartIo(...){ ... KeSynchronizeExecution(pdx->InterruptObject, TransferFirst, (PVOID) pdx);}BOOLEAN TransferFirst(PVOID context){ PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) context; ... return TRUE;}
這裡的TransferFirst常式是同步關鍵段(SynchCritSection)的一個例子,之所以這樣做是因為StartIo需要與ISR同步。我將在第七章中詳細討論同步關鍵段(SynchCritSection)的概念。
一旦StartIo使裝置忙於處理新請求,它就立即返回。當裝置完成傳輸並發出中斷時你將看到下一個請求。
插斷服務常式當裝置完成資料轉送後,它將以硬體中斷形式發出通知。在第七章中,我將講述如何用IoConnectInterrupt函數“鉤住”一個中斷,該函數的一個參數就是ISR的地址。因此當中斷髮生時,硬體抽象層(HAL)就調用你的ISR。ISR運行在DIRQL上,並由ISR專用的自旋鎖保護。ISR的函數原型如下:
BOOLEAN OnInterrupt(PKINTERRUPT InterruptObject, PVOID context){ ...}
ISR的第一個參數是中斷對象的地址,中斷對象由IoConnectInterrupt函數建立,但是你不太可能用到這個參數。第二個參數是在調用IoConnectInterrupt時你指定的任意上下文值;它可能是裝置對象或裝置擴充的地址,完全由你決定。
我將在第七章中詳細討論ISR的職責。為了繼續標準模型的討論,我要告訴你一點,一個ISR最可能做的事就是調度DPC常式(延遲程序呼叫)。而DPC的目的就是讓你做某些事情,如調用IoCompleteRequest,而該調用不可能運行在ISR啟動並執行DIRQL級上。所以,你的ISR中將有下面一行語句(device是指向裝置對象的指標):
IoRequestDpc(device, device->CurrentIrp, NULL);
那麼下一次你將在DPC常式中看到這個IRP,這個DPC常式是你在AddDevice函數中用IoInitializeDpcRequest寄存的。DPC常式的傳統名稱為DpcForIsr,因為它是由ISR請求的。
DPC常式DpcForIsr常式在DISPATCH_LEVEL級上獲得控制。通常,它的工作就是完成IRP(導致最近的中斷髮生)。但一般情況下,它通過調用IoCompleteRequest函數把剩餘的工作交給完成常式來做。
VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT device, PIRP Irp, PDEVICE_EXTENSION pdx){ ... IoStartNextPacket(device, FALSE);
- IoStartNextPacket 取出裝置隊列中的下一個IRP並發送到StartIo。FALSE參數指出該IRP不能以通常方式取消。
- IoCompleteRequest 完成第一個參數指定的IRP。第二參數是等待線程的優先順序提高值。注意在調用IoCompleteRequest之前你還要填充IRP中的IoStatus塊。
調用IoCompleteRequest常式是處理I/O請求的標準結束方式。在這個調用之後,I/O管理器(或者是任何在開始處建立該IRP的實體)將再次擁有該IRP。最後該IRP被這個實體銷毀並解除等侍線程的阻塞狀態。
定製隊列有些裝置的操作需要多個請求隊列。一個常見的例子就是串列口,它可以同時地並且分開地處理輸入輸出請求流。IoStartPacket和IoStartNextPacket函數(以及其它含有鍵排序功能的等價函數)都使用裝置對象內建的隊列。建立與標準隊列有相同工作方式的附加隊列要相對容易一些。
為了使我們更容易討論問題,讓我們假設你需要一個單獨的隊列來管理IRP_MJ_SPECIAL(並不存在這個主功能碼,使用它是為了使問題更具體一些)請求。你將寫兩個與StartIo和DpcForIsr常式功能類似的,但專用於處理這些假想IRP的輔助常式:
- 與StartIo類似的函數 --- 我們稱它為StartIoSpecial --- 它啟動下一個IRP_MJ_SPECIAL請求。
- 與DPC類似的函數 --- 我們稱它為DpcSpecial --- 它處理IRP_MJ_SPECIAL請求的完成。
你還需要在你的裝置擴充中建立一個KDEVICE_QUEUE對象,並在AddDevice常式中初始化這個隊列對象:
NTSTATUS AddDevice(...){ ... KeInitializeDeviceQueue(&pdx->dqSpecial); ...}
dqSpecial就是KDEVICE_OBJECT對象的名字,用於排隊IRP_MJ_SPECIAL請求。裝置隊列對象是一種三態對象(見圖5-6)。這三種狀態反映了裝置隊列常式是如何操作裝置隊列的:
- idle狀態是指裝置不忙於處理任何請求並且隊列為空白。KeInsertDeviceQueue或KeInsertByKeyDeviceQueue函數把隊列標記為busy-empty狀態並返回FALSE。你不應該在隊列為idle狀態時調用KeRemoveDeviceQueue或KeRemoveByKeyDeviceQueue函數。
- busy-empty狀態是指裝置忙但隊列中沒有IRP。KeInsertDeviceQueue和KeInsertByKeyDeviceQueue函數向隊列尾加入新IRP,使隊列進入busy-not empty狀態,並返回TRUE。KeRemoveDeviceQueue或KeRemoveByKeyDeviceQueue函數返回NULL並使隊列進入idle狀態。
- busy-not empty狀態是指裝置忙且隊列中至少存有一個IRP。KeInsertDeviceQueue和KeInsertByKeyDeviceQueue函數向隊列尾加入新IRP並返回TRUE,但隊列狀態不變。KeRemoveDeviceQueue或KeRemoveByKeyDeviceQueue函數提取隊列的第一個IRP並返回其地址,此時,如果隊列為空白,這些函數將把隊列置成busy-empty狀態。
在下面代碼中,我們在派遣常式和DPC常式中使用了這些支援常式和我們專用的裝置隊列:
NTSTATUS DispatchSpecial(PDEVICE_OBJECT fdo, PIRP Irp){ IoMarkIrpPending(Irp);DeviceExtension; if (!KeInsertDeviceQueue(&pdx->dqSpecial, &Irp->Tail.Overlay.DeviceQueueEntry))dqSpecial);
- 作為一個“規矩”的派遣常式,我們把該IRP標記為pending,因為我們要讓它進入隊列,然後派遣常式返回STATUS_PENDING狀態。
- KeInsertDeviceQueue函數和我們自己的StartIoSpecial常式希望在DISPATCH_LEVEL級上被調用。所以我們明確地提升了IRQL,之後很快我們又調用KeLowerIrql函數把IRQL降低到原來的層級(可能是PASSIVE_LEVEL)。
- KeInsertDeviceQueue函數把IRP加入到專用隊列中,如果傳回值為TRUE,表明IRP已被排入佇列中,所以我們不用再做任何關於IRP的事。如果裝置正空閑,那麼傳回值應該為FALSE並且IRP也用不著排入隊列,我們直接調用StartIoSpecial常式。
- DPC中的這個KeRemoveDeviceQueue調用將產生兩種結果。如果隊列當前為空白,則傳回值為NULL並且我們也不用啟動新請求。否則,傳回值將為某IRP內嵌串連域的地址。我們可以使用CONTAINING_RECORD宏來獲得外圍的真正的IRP地址。然後我們把這個地址傳遞給StartIoSpecial常式。注意,這個DPC常式已經運行在DISPATCH_LEVEL級上,所以我們不需要在刪除隊清單項目或調用StartIo之前調整IRQL。
我以前描述的StartPacket和StartNextPacket函數使用一個名為DeviceQueue的KDEVICE_QUEUE對象。該對象是裝置對象中的一個不透明域,其工作原理與管理私人裝置隊列相同。