790 likes | 1.04k Views
IRP. sigang@mti.xidian.edu.cn. IRP. IRP ( I/O request packet ) 在 Windows 2000 中,幾乎所有的 I/O 操作都是包驅動的。. IRP 的生產者. 作業系統會把各種用戶模式的請求或者其它系統組件的請求轉化為 IRP 發送給驅動程式。 高層的驅動程式也有可能構造新的 IRP 發送給低層的驅動程式。 驅動程式中的主要工作就是處理各種各樣的 IRP ,然後完成這個 IRP ,表明處理完成。. IRP 的完成. 在標準模型中,至少有兩種完成 IRP 的環境。
E N D
IRP sigang@mti.xidian.edu.cn
IRP • IRP(I/O request packet) • 在Windows 2000中,幾乎所有的I/O操作都是包驅動的。
IRP的生產者 • 作業系統會把各種用戶模式的請求或者其它系統組件的請求轉化為IRP發送給驅動程式。 • 高層的驅動程式也有可能構造新的IRP發送給低層的驅動程式。 • 驅動程式中的主要工作就是處理各種各樣的IRP,然後完成這個IRP,表明處理完成。
IRP的完成 • 在標準模型中,至少有兩種完成IRP的環境。 • DpcForIsr通常用于完成導致最近中斷的IRP。 • 派遣函數也可以在下面這兩種情況下完成IRP︰ • 如果請求是錯誤的則派遣例程應以失敗模式完成該請求並返回適當的出錯代碼。 • 如果請求要求得到的僅是派遣函數可以容易確定的訊息(例如一個詢問驅動程式版本號的控制請求),則派遣例程應立即給出回答並完成請求,返回成功狀態碼。
MDL • MdlAddress(PMDL)域指向一個內存描述符表(MDL),它描述了一個與該請求關聯的用戶模式緩沖區。 • 如果設備對象的Flags域為DO_DIRECT_IO,則I/O管理器為IRP_MJ_READ或IRP_MJ_WRITE請求創建這個MDL。 • 如果一個IRP_MJ_DEVICE_CONTROL請求的控制代碼指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作模式,則I/O管理器為該請求使用的輸出緩沖區創建一個MDL。 • MDL本身用于描述用戶模式虛擬緩沖區,但它同時也含有該緩沖區鎖定內存頁的物理位址。為了訪問用戶模式緩沖區,驅動程式必須做一點額外工作。
AssociatedIrp • AssociatedIrp(union)域是一個三指針聯合。其中,與WDM驅動程式相關的指針是AssociatedIrp.SystemBuffer。 SystemBuffer指針指向一個數據緩沖區,該緩沖區位于內核模式的非分頁內存中。 • 對于IRP_MJ_READ和IRP_MJ_WRITE操作,如果頂級設備指定DO_BUFFERED_IO標誌,則I/O管理器就創建這個數據緩沖區。 • 對于IRP_MJ_DEVICE_CONTROL操作,如果I/O控制功能代碼指出需要DO_BUFFERED_IO則I/O管理器就創建這個數據緩沖區。 • 對于寫操作I/O管理器把用戶模式程式發送給驅動程式的數據複製到這個緩沖區,對于讀操作I/O管理器把這個緩沖區內的內容複製給用戶模式緩沖區。
IoStatus • IoStatus(IO_STATUS_BLOCK)是一個僅包含兩個域的架構,驅動程式在最終完成請求時設置這個架構。IoStatus.Status域將收到一個NTSTATUS代碼,而IoStatus.Information的類型為ULONG_PTR,它將收到一個訊息值,該訊息值的確切含義要取決于具體的IRP類型和請求完成的狀態。 • Information域的一個公認用法是用于保存數據傳輸操作,如IRP_MJ_READ,的流量總計。某些PnP請求把這個域作為指向另外一個架構的指針,這個架構通常包含查詢請求的結果。
其他成員 • Flags(ULONG)域包含一些對驅動程式只讀的標誌。但這些標誌與WDM驅動程式無關。 • RequestorMode是一個枚舉常量UserMode或KernelMode,指定原始I/O請求的來源。驅動程式有時需要查看這個值來決定是否要信任某些參數。 • PendingReturned(BOOLEAN)如果為TRUE,則表明處理該IRP的最低級派遣例程返回了STATUS_PENDING。完成例程透過參考該域來避免自己與派遣例程間的潛在競爭。
其他成員 • Cancel(BOOLEAN)如果為TRUE,則表明IoCancelIrp已被調用,該函數用于取消這個請求。如果為FALSE,則表明沒有調用IoCancelIrp函數。 • CancelIrql(KIRQL)是一個IRQL值,表明那個專用的取消自旋鎖是在這個IRQL上獲取的。當在取消例程中釋放自旋鎖時應參考這個域。 • CancelRoutine(PDRIVER_CANCEL)是驅動程式取消例程的位址。應該使用IoSetCancelRoutine函數設置這個域而不是直接修改該域。
其他成員 • UserBuffer(PVOID) 對于METHOD_NEITHER模式的IRP_MJ_DEVICE_CONTROL請求,該域用于保存讀寫請求緩沖區的用戶模式虛擬位址 • Tail.Overlay是Tail聯合中的一種架構,它含有幾個對WDM驅動程式有潛在用途的成員。下圖是Tail聯合的組成圖。
Tail.Overlay說明 • I/O管理器把DeviceQueueEntry作為設備標準請求隊列中的連接域。 • 當IRP還沒有進入某個隊列時,如果擁有這個IRP你可以使用這個域,可以任意使用DriverContext中的四個指針。 • Tail.Overlay.ListEntry(LIST_ENTRY) 能作為自己實現的私有隊列的連接域。
CurrentLocation • CurrentLocation (CHAR)和Tail.Overlay.CurrentStackLocation(PIO_STACK_LOCATION)沒有公開為驅動程式使用,因為可以使用象IoGetCurrentIrpStackLocation這樣的函數獲取這些訊息。 • 但意識到CurrentLocation就是當前I/O堆棧單元的索引以及CurrentStackLocation就是指向它的指針,會對驅動程式調試有一些幫助。
I/O堆棧 • 任何內核模式程式在創建一個IRP時,同時還創建了一個與之關聯的IO_STACK_LOCATION架構數組 • 數組中的每個IO_STACK_LOCATION都對應一個將處理該IRP的驅動程式。 IO_STACK_LOCATION中包含該IRP的類型代碼和參數訊息以及完成函數的位址。 • 下頁圖是驅動程式和I/O堆棧的對應關係
IO_STACK_LOCATION的數據架構 • MajorFunction(UCHAR)是該IRP的主功能碼。這個代碼應該為類似IRP_MJ_READ一樣的值,並與驅動程式對象中MajorFunction表的某個派遣函數指針相對應 • MinorFunction(UCHAR)是該IRP的副功能碼。它進一步指出該IRP屬于哪個主功能類。例如,IRP_MJ_PNP請求就有很多的副功能碼,如IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE,等等。 • Parameters(union)是幾個子架構的聯合,每個請求類型都有自己專用的參數,而每個子架構就是一種參數。這些子架構包括Create(IRP_MJ_CREATE請求)、Read(IRP_MJ_READ請求)、StartDevice(IRP_MJ_PNP的IRP_MN_START_DEVICE子類型),等等。
IO_STACK_LOCATION的數據架構 • DeviceObject(PDEVICE_OBJECT)是與該堆棧單元對應的設備對象的位址。 • FileObject(PFILE_OBJECT)是內核文件對象的位址,IRP的目標就是這個文件對象。驅動程式通常在處理清除請求(IRP_MJ_CLEANUP)時使用FileObject指針,以區分隊列中與該文件對象無關的IRP。 • CompletionRoutine(PIO_COMPLETION_ROUTINE)是一個I/O完成例程的位址,該位址是由與這個堆棧單元對應的驅動程式的更上一層驅動程式設置的。絕對不要直接設置這個域,應該調用IoSetCompletionRoutine函數 • Context(PVOID)是一個任意的與上下文相關的值,將作為參數傳遞給完成例程。絕對不要直接設置該域;它由IoSetCompletionRoutine函數自動設置,其值來自該函數的某個參數。
IRP的創造者(上面好像說過了) • IRP開始于某個實體調用I/O管理器函數創建它。 • 通常是用戶程式發送Win32 API請求,一個系統組件把這個請求轉化為一個IRP,然後發送到驅動程式。 • 驅動程式有時也會創建IRP,而此時出現下上圖中第一個方框中的實體就應該是驅動程式。
創建IRP的函數 • IoBuildAsynchronousFsdRequest 創建異步IRP(不需要等待其完成)。 • IoBuildSynchronousFsdRequest 創建同步IRP(需要等待其完成)。 • IoBuildDeviceIoControlRequest 創建一個同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL請求。 • IoAllocateIrp 創建上面三個函數不支持的其它種類的IRP。 • 前兩個函數中的Fsd表明這些函數專用于文件系統驅動程式(FSD)。雖然FSD是這兩個函數的主要使用者,但其它驅動程式也可以調用這些函數。
發往派遣例程 • 創建完IRP后,可以調用IoGetNextIrpStackLocation函數獲得該IRP下一個堆棧單元的指針。然後初始化這個堆棧單元。 • 對于用IoAllocateIrp創建的IRP,在初始化過程的最後,還需要填充MajorFunction代碼。 • 堆棧單元初始化完成后,就可以調用IoCallDriver函數把IRP發送到設備驅動程式︰ • 舉例︰
派遣例程的職責 • IRP派遣例程的原型看起來像下面這樣︰ • NTSTATUS DispatchXxx(PDEVICE_OBJECT device, PIRP Irp){ • PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); • PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension; • ... • return STATUS_Xxx; • }
派遣例程的職責 • 通常需要訪問當前堆棧單元以確定參數或副功能碼。 • 可能還需要訪問創建的設備擴展,因為設備擴展中保存著這個設備的相關訊息。 • 將向IoCallDriver函數返回某個NTSTATUS代碼,而IoCallDriver函數將把這個狀態碼返回給它的調用者。
派遣例程的職責 • 在上面派遣函數原型中省略號的地方,是派遣函數必須做出決定的地方,有三種選擇︰ • 1 派遣函數立即完成該IRP(例如取得驅動程式版本)。 • 2 把該IRP傳遞到處于同一堆棧的下層驅動程式。 • 3 排隊該IRP以便由這個驅動程式中的其它例程來處理。
硬體訪問串行化 • 當有大量讀寫請求進入設備時,通常需要把這些請求放入一個隊列中,以便使硬體訪問串行化。 • 使用隊列通常有兩種方法,自己管理的隊列和設備自帶的隊列,自己管理的隊列會在一個例子中說明。 • 每個設備對象都自帶一個請求隊列對象,下面是使用這個隊列的標準方法
使用設備隊列的標準方法 NTSTATUS DispatchXxx(...) { ... IoMarkIrpPending(Irp); IoStartPacket(device, Irp, NULL, NULL); return STATUS_PENDING; }
說明 • 無論何時,當派遣例程返回STATUS_PENDING狀態代碼時,應該先調用這個IoMarkIrpPending函數,以幫助I/O管理器避免內部競爭。而且必須在放棄IRP所有權之前做這一點。 • 如果設備正忙,IoStartPacket就把請求放到隊列中。如果設備空閒,IoStartPacket將把設備置成忙並調用StartIo例程。 • IoStartPacket的第三個參數是用于排序隊列的鍵(ULONG)的位址,如果在這裡指定一個NULL,則該請求被加到隊列的尾部。最後一個參數是取消例程的位址。 • 返回STATUS_PENDING以通知調用者我們沒有完成這個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例程 • StartIo例程在DISPATCH_LEVEL級上獲得控制,這意味著該函數不能生成任何頁故障。另外,設備對象的CurrentIrp域和Irp參數都指向I/O管理器送來的IRP。 • StartIo的工作是就著手處理IRP。如何做要完全取決于具體的設備。 • 通常需要訪問硬體暫存器,但可能有其它例程,如中斷服務例程,或者是驅動程式中的其它例程也需要訪問這些暫存器。這些例程的執行都需要在一個自旋鎖的保護之下,而這個自旋鎖與保護ISR所使用的是同一個自旋鎖,所以正確的方法是調用KeSynchronizeExecution函數(已經講過)。例如︰
KeSynchronizeExecution VOID StartIo(...) { ... KeSynchronizeExecution(pdx->InterruptObject, TransferFirst, (PVOID) pdx); }
BOOLEAN TransferFirst(PVOID context) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) context; ... return TRUE; } 這裡的TransferFirst例程是同步關鍵段(SynchCritSection)的一個例子,之所以這樣做是因為這段代碼需要與ISR同步。
中斷服務例程(Isr) • 當設備完成數據傳輸后,通常以硬體中斷形式發出通知。 • 當中斷發生時,硬體抽象層(HAL)就調用驅動程式的ISR。 • ISR營運在DIRQL上,並由ISR專用的自旋鎖保護。ISR的函數原型如下︰ • BOOLEAN OnInterrupt(PKINTERRUPT InterruptObject, PVOID context) • { • ... • }
ISR • ISR的第一個參數是中斷對象的位址,中斷對象由IoConnectInterrupt函數創建,但是太可能用到這個參數。第二個參數是在調用IoConnectInterrupt時指定的任意上下文值;它可能是設備對象或設備擴展的位址。 • 一個ISR最可能做的事就是調度DPC例程(延遲過程調用)。而DPC的目的就是做某些事情,如調用IoCompleteRequest,該調用不可能營運在ISR營運的DIRQL級上。 • 所以,ISR中將有下面一行語句(device是指向設備對象的指針)︰IoRequestDpc(device, device->CurrentIrp, NULL); 那么下一次將在DPC例程中看到這個IRP,這個DPC例程是在AddDevice函數中用IoInitializeDpcRequest註冊的。DPC例程的道統名稱為DpcForIsr,因為它是由ISR請求的。
DPC例程 VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT device, PIRP Irp, PDEVICE_EXTENSION pdx) { ... IoStartNextPacket(device, FALSE); IoCompleteRequest(Irp, boost); }
DPC例程 • IoStartNextPacket 取出設備隊列中的下一個IRP並發送到StartIo。FALSE參數指出該IRP不能以通常模式取消。 • IoCompleteRequest 完成第一個參數指定的IRP。第二參數是等待線程的優先級提升值。注意在調用IoCompleteRequest之前還要填充IRP中的IoStatus塊。 • 調用IoCompleteRequest例程是處理I/O請求的標準結束模式。在這個調用之后,I/O管理器(或者是任何在開始處創建該IRP的實體)將再次擁有該IRP。最後該IRP被這個實體銷毀並解除等待線程的阻塞狀態
完成IRP • 命中注定,每個IRP出現的目的就是最後被完成。它的一生或許很短,或許很長,取決于它的類型。 • 每個IRP都渴望被完成,每個程式員也喜歡完成一個IRP,因為終于完成了對它可能是很繁瑣的處理了
完成一個IRP必須先填充IoStatus塊的Status和Information成員,然後調用IoCompleteRequest例程。完成一個IRP必須先填充IoStatus塊的Status和Information成員,然後調用IoCompleteRequest例程。 • Status值就是NTSTATUS.H中定義的狀態代碼。而Information值要取決于完成的是何種類型的IRP以及是成功還是失敗。通常情況下,如果IRP完成失敗(即,完成的結果是某種錯誤狀態),應把Information域置0。如果成功地完成了一個數據傳輸IRP,通常應該把Information域設置成傳輸的位元組量。
特別注意 • 在設置IRP的IoStatus.Information域時,我們應該首先參考DDK文檔中的正確設置。例如,在某些IRP_MJ_PNP中,這個域用于指向一個數據架構,而該數據架構由PnP管理器負責釋放。如果在處理這些IRP_MJ_PNP請求失敗后把Information域置成0,將造成資源丟失。
輔助函數 • 在驅動程式中有很多地方都要完成一個IRP,所以通常編寫一個輔助函數來完成IRP︰ • NTSTATUS CompleteRequest(PIRP Irp, NTSTATUS status, ULONG_PTR Information) • { • Irp->IoStatus.Status = status; • Irp->IoStatus.Information = Information; • IoCompleteRequest(Irp, IO_NO_INCREMENT); • return status; • } • 該函數將返回其第二個參數給出的狀態值。該函數適用于需要完成一個請求並立即返回狀態碼的場合
優先級推進 • 當調用IoCompleteRequest時,應該為等待線程提供一個優先級推進值,該值將用于提升等待該請求完成的線程的優先級。 • 一般說來,需要根據設備類型來選擇這個推進值,下表列出了一些設備的建議值。優先級的調整提升了那些需要頻繁等待I/O操作完成的線程的吞吐量。對于那些直接附應用戶的事件,如鍵盤或鼠標操作,應該有一個比較大的優先級推進,以提升交互任務的表現。因此,要仔細地選擇推進值。
可能犯的錯誤 • 不要以專用狀態代碼STATUS_PENDING來完成一個IRP。派遣例程經常要使用STATUS_PENDING代碼作為返回值,但決不能在IoStatus.Status中設置這個值。 • 在checked版本的IoCompleteRequest函數中有一個ASSERT語句用于檢查該函數的最終返回值是否為STATUS_PENDING。 • 另一個常犯的錯誤是在返回值中使用“-1”,該值作為NTSTATUS代碼沒有任何意義,所以IoCompleteRequest函數中也有檢查這種錯誤的ASSERT語句。
使用完成例程 • 有時候,需要把請求發往低層驅動程式,而且要在低層驅動程式處理完成之后繼續處理。這就需要安裝一個完成例程,調用IoSetCompletionRoutine函數︰ • IoSetCompletionRoutine(Irp, CompletionRoutine, context, InvokeOnSuccess, InvokeOnError, InvokeOnCancel);
要在Irp被低層完成之后調用安裝的完成例程CompletionRoutine,context是任何一個指針,將作為完成例程的參數。InvokeOnXxx參數是布爾值,它們指出在三種不同的環境中是否需要調用完成例程︰ • InvokeOnSuccess 希望完成例程在IRP以成功狀態(返回的狀態代碼透過了NT_SUCCESS測試)完成時被調用。 • InvokeOnError 希望完成例程在IRP以失敗狀態(返回的狀態代碼未透過了NT_SUCCESS測試)完成時被調用。 • InvokeOnCancel 如果驅動程式在完成IRP前調用了IoCancelIrp例程,希望在此時調用完成例程。
說明 • 上面三個標誌中至少有一個要設置為TRUE。 • 注意,IoSetCompletionRoutine是一個宏,所以應避免使用有副作用的參數。 • IoSetCompletionRoutine將把完成例程位址和上下文參數安裝到下一個IO_STACK_LOCATION中,即下一層驅動程式將在那個堆棧單元中找到這些參數。因此,最底層的驅動程式不應該安裝一個完成例程。
CompletionRoutine NTSTATUS CompletionRoutine(PDEVICE_OBJECT device, PIRP Irp, PVOID context) { if (Irp->PendingReturned) IoMarkIrpPending(Irp); //如果這個完成例程不返回STATUS_MORE_PROCESSING_REQUIRED ,那么上面的代碼必須存在。 ... return <some status code>; } 函數將收到一個設備對象指針和一個IRP指針,一個在IoSetCompletionRoutine中指定的context。
說明 • 完成例程通常在DISPATCH_LEVEL級和任意線程上下文中被調用,但有時也在PASSIVE_LEVEL或APC_LEVEL級被調用。 • 為了適應大多數情況(DISPATCH_LEVEL),完成例程應存在于非分頁內存中,並且僅使用可在DISPATCH_LEVEL級上調用的服務例程。 • 然而,為了適應在低級IRQL上調用該例程的可能情況,完成例程不應調用像KeAcquireSpinLockAtDpcLevel這樣的函數,因為這些函數假定開始執行于DISPATCH_LEVEL級上。