驅(qū)動(dòng)編程學(xué)習(xí)筆記之IO處理(轉(zhuǎn))
關(guān)鍵詞: i/o IRP IOCTL
典型的i/o處理過(guò)程 操作系統(tǒng)將所有的i/o請(qǐng)求都抽象成針對(duì)一個(gè)虛擬文件的操作,從而掩蓋了"一個(gè)i/o操作的目標(biāo)可能不是一個(gè)文件結(jié)構(gòu)的設(shè)備"這樣的事實(shí)。這一抽象也使得應(yīng)用程序?qū)ΥO(shè)備的接口變得泛化。 用戶(hù)模式api
驅(qū)動(dòng)程序?qū)ο螅╠river object)代表了系統(tǒng)中一個(gè)單獨(dú)的驅(qū)動(dòng)程序。i/o管理器從該驅(qū)動(dòng)程序?qū)ο笾蝎@得其每一個(gè)分發(fā)例程(入口點(diǎn))的地址; 設(shè)備對(duì)象(device object)代表了系統(tǒng)中一個(gè)物理的或邏輯的設(shè)備,并且描述了它的特征,比如它要求的緩沖區(qū)的對(duì)齊特性,以及它的設(shè)備隊(duì)列(用于存放進(jìn)來(lái)的irp)的位置。 當(dāng)一個(gè)驅(qū)動(dòng)程序被加載到一個(gè)系統(tǒng)中時(shí),i/o管理器創(chuàng)建一個(gè)驅(qū)動(dòng)程序?qū)ο螅缓笏{(diào)用該驅(qū)動(dòng)程序的初始化例程(DriverEntry),該例程會(huì)利用該驅(qū)動(dòng)程序的入口點(diǎn)來(lái)填充此對(duì)象的屬性。 設(shè)備對(duì)象指回到它的驅(qū)動(dòng)程序?qū)ο笊?,這正是當(dāng)i/o管理器接收到一個(gè)i/o請(qǐng)求時(shí),它如何知道該調(diào)用哪一個(gè)驅(qū)動(dòng)例程的原因。它利用該設(shè)備對(duì)象來(lái)找到一個(gè)驅(qū)動(dòng)程序?qū)ο?,此?duì)象代表了負(fù)責(zé)為該設(shè)備提供服務(wù)的驅(qū)動(dòng)程序。然后,它利用原始請(qǐng)求中所提供的功能代碼,索引到該驅(qū)動(dòng)程序?qū)ο笾?;每個(gè)功能代碼對(duì)應(yīng)于驅(qū)動(dòng)程序的一個(gè)入口點(diǎn)。 驅(qū)動(dòng)程序?qū)ο笸ǔS卸鄠€(gè)與之關(guān)聯(lián)的設(shè)備對(duì)象。這些設(shè)備對(duì)象的列表代表了該驅(qū)動(dòng)程序所控制的物理和邏輯設(shè)備。例如,一個(gè)硬盤(pán)的每個(gè)分區(qū)都有一個(gè)單獨(dú)的設(shè)備對(duì)象,該設(shè)備對(duì)象包含了與此分區(qū)相關(guān)的信息。然而,同樣的硬盤(pán)驅(qū)動(dòng)程序被用于訪問(wèn)所有這些分區(qū)。當(dāng)一個(gè)驅(qū)動(dòng)程序被從系統(tǒng)卸載時(shí),i/o管理器使用設(shè)備對(duì)象的隊(duì)列來(lái)決定哪些設(shè)備將會(huì)受到"該驅(qū)動(dòng)程序被移除"的影響。
IRP結(jié)構(gòu) MdlAddress (PMDL)域指向一個(gè)內(nèi)存描述符表(MDL),該表描述了一個(gè)與該請(qǐng)求關(guān)聯(lián)的用戶(hù)模式緩沖區(qū)。如果頂級(jí)設(shè)備對(duì)象的Flags域?yàn)?DO_DIRECT_IO,則I/O管理器為IRP_MJ_READ或IRP_MJ_WRITE請(qǐng)求創(chuàng)建這個(gè)MDL。如果一個(gè) IRP_MJ_DEVICE_CONTROL請(qǐng)求的控制代碼指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,則 I/O管理器為該請(qǐng)求使用的輸出緩沖區(qū)創(chuàng)建一個(gè)MDL。MDL本身用于描述用戶(hù)模式虛擬緩沖區(qū),但它同時(shí)也含有該緩沖區(qū)鎖定內(nèi)存頁(yè)的物理地址。為了訪問(wèn)用戶(hù)模式緩沖區(qū),驅(qū)動(dòng)程序必須做一點(diǎn)額外工作。 Flags(ULONG)域包含一些對(duì)驅(qū)動(dòng)程序只讀的標(biāo)志。但這些標(biāo)志與WDM驅(qū)動(dòng)程序無(wú)關(guān)。 AssociatedIrp (union)域是一個(gè)三指針聯(lián)合。其中,與WDM驅(qū)動(dòng)程序相關(guān)的指針是AssociatedIrp.SystemBuffer。 SystemBuffer指針指向一個(gè)數(shù)據(jù)緩沖區(qū),該緩沖區(qū)位于內(nèi)核模式的非分頁(yè)內(nèi)存中。對(duì)于IRP_MJ_READ和IRP_MJ_WRITE操作,如果頂級(jí)設(shè)備指定DO_BUFFERED_IO標(biāo)志,則I/O管理器就創(chuàng)建這個(gè)數(shù)據(jù)緩沖區(qū)。對(duì)于IRP_MJ_DEVICE_CONTROL操作,如果 I/O控制功能代碼指出需要緩沖區(qū)(見(jiàn)第九章),則I/O管理器就創(chuàng)建這個(gè)數(shù)據(jù)緩沖區(qū)。I/O管理器把用戶(hù)模式程序發(fā)送給驅(qū)動(dòng)程序的數(shù)據(jù)復(fù)制到這個(gè)緩沖區(qū),這也是創(chuàng)建IRP過(guò)程的一部分。這些數(shù)據(jù)可以是與WriteFile調(diào)用有關(guān)的數(shù)據(jù),或者是DeviceIoControl調(diào)用中所謂的輸入數(shù)據(jù)。對(duì)于讀請(qǐng)求,設(shè)備驅(qū)動(dòng)程序把讀出的數(shù)據(jù)填到這個(gè)緩沖區(qū),然后I/O管理器再把緩沖區(qū)的內(nèi)容復(fù)制到用戶(hù)模式緩沖區(qū)。對(duì)于指定了METHOD_BUFFERED 的I/O控制操作,驅(qū)動(dòng)程序把所謂的輸出數(shù)據(jù)放到這個(gè)緩沖區(qū),然后I/O管理器再把數(shù)據(jù)復(fù)制到用戶(hù)模式的輸出緩沖區(qū)。 IoStatus(IO_STATUS_BLOCK) 是一個(gè)僅包含兩個(gè)域的結(jié)構(gòu),驅(qū)動(dòng)程序在最終完成請(qǐng)求時(shí)設(shè)置這個(gè)結(jié)構(gòu)。IoStatus.Status域?qū)⑹盏揭粋€(gè)NTSTATUS代碼,而 IoStatus.Information的類(lèi)型為ULONG_PTR,它將收到一個(gè)信息值,該信息值的確切含義要取決于具體的IRP類(lèi)型和請(qǐng)求完成的狀態(tài)。Information域的一個(gè)公認(rèn)用法是用于保存數(shù)據(jù)傳輸操作,如IRP_MJ_READ,的流量總計(jì)。某些PnP請(qǐng)求把這個(gè)域作為指向另外一個(gè)結(jié)構(gòu)的指針,這個(gè)結(jié)構(gòu)通常包含查詢(xún)請(qǐng)求的結(jié)果。 RequestorMode將等于一個(gè)枚舉常量UserMode或KernelMode,指定原始I/O請(qǐng)求的來(lái)源。驅(qū)動(dòng)程序有時(shí)需要查看這個(gè)值來(lái)決定是否要信任某些參數(shù)。 PendingReturned(BOOLEAN)如果為T(mén)RUE,則表明處理該IRP的最低級(jí)派遣例程返回了STATUS_PENDING。完成例程通過(guò)參考該域來(lái)避免自己與派遣例程間的潛在競(jìng)爭(zhēng)。 Cancel(BOOLEAN)如果為T(mén)RUE,則表明IoCancelIrp已被調(diào)用,該函數(shù)用于取消這個(gè)請(qǐng)求。如果為FALSE,則表明沒(méi)有調(diào)用IoCancelIrp函數(shù)。取消IRP是一個(gè)相對(duì)復(fù)雜的主題,我將在本章的最后詳細(xì)描述它。 CancelIrql(KIRQL)是一個(gè)IRQL值,表明那個(gè)專(zhuān)用的取消自旋鎖是在這個(gè)IRQL上獲取的。當(dāng)你在取消例程中釋放自旋鎖時(shí)應(yīng)參考這個(gè)域。 CancelRoutine(PDRIVER_CANCEL)是驅(qū)動(dòng)程序取消例程的地址。你應(yīng)該使用IoSetCancelRoutine函數(shù)設(shè)置這個(gè)域而不是直接修改該域。 UserBuffer(PVOID) 對(duì)于METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL請(qǐng)求,該域包含輸出緩沖區(qū)的用戶(hù)模式虛擬地址。該域還用于保存讀寫(xiě)請(qǐng)求緩沖區(qū)的用戶(hù)模式虛擬地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO標(biāo)志的驅(qū)動(dòng)程序,其讀寫(xiě)例程通常不需要訪問(wèn)這個(gè)域。當(dāng)處理一個(gè)METHOD_NEITHER控制操作時(shí),驅(qū)動(dòng)程序能用這個(gè)地址創(chuàng)建自己的MDL。 Tail.Overlay是Tail聯(lián)合中的一種結(jié)構(gòu),它含有幾個(gè)對(duì)WDM驅(qū)動(dòng)程序有潛在用途的成員。圖5-2是Tail聯(lián)合的組成圖。在這個(gè)圖中,以水平方向從左到右是這個(gè)聯(lián)合的三個(gè)可選成員,在垂直方向是每個(gè)結(jié)構(gòu)的成員描述。Tail.Overlay.DeviceQueueEntry(KDEVICE_QUEUE_ENTRY)和 Tail.Overlay.DriverContext(PVOID[4])是Tail.Overlayare內(nèi)一個(gè)未命名聯(lián)合的兩個(gè)可選成員(只能出現(xiàn)一個(gè))。I/O管理器把DeviceQueueEntry作為設(shè)備標(biāo)準(zhǔn)請(qǐng)求隊(duì)列中的連接域。當(dāng)IRP還沒(méi)有進(jìn)入某個(gè)隊(duì)列時(shí),如果你擁有這個(gè)IRP你可以使用這個(gè)域,你可以任意使用DriverContext中的四個(gè)指針。Tail.Overlay.ListEntry(LIST_ENTRY)僅能作為你自己實(shí)現(xiàn)的私有隊(duì)列的連接域。 CurrentLocation (CHAR)和Tail.Overlay.CurrentStackLocation(PIO_STACK_LOCATION)沒(méi)有公開(kāi)為驅(qū)動(dòng)程序使用,因?yàn)槟阃耆梢允褂孟驣oGetCurrentIrpStackLocation這樣的函數(shù)獲取這些信息。但意識(shí)到CurrentLocation就是當(dāng)前I/O堆棧單元的索引以及CurrentStackLocation就是指向它的指針,會(huì)對(duì)驅(qū)動(dòng)程序調(diào)試有一些幫助。 &nbp;
irp的棧單元包含一個(gè)功能代碼(由一個(gè)主碼和一個(gè)次碼組成)與功能相關(guān)的參數(shù),以及一個(gè)指向調(diào)用者文件對(duì)象的指針。 MajorFunction (UCHAR)是該IRP的主功能碼。這個(gè)代碼應(yīng)該為類(lèi)似IRP_MJ_READ一樣的值,并與驅(qū)動(dòng)程序?qū)ο笾蠱ajorFunction表的某個(gè)派遣函數(shù)指針相對(duì)應(yīng)。如果該代碼存在于某個(gè)特殊驅(qū)動(dòng)程序的I/O堆棧單元中,它有可能一開(kāi)始是,例如IRP_MJ_READ,而后被驅(qū)動(dòng)程序轉(zhuǎn)換成其它代碼,并沿著驅(qū)動(dòng)程序堆棧發(fā)送到低層驅(qū)動(dòng)程序。我將在第十一章(USB總線)中舉一個(gè)這樣的例子,USB驅(qū)動(dòng)程序把標(biāo)準(zhǔn)的讀或?qū)懻?qǐng)求轉(zhuǎn)換成內(nèi)部控制操作,以便向 USB總線驅(qū)動(dòng)程序提交請(qǐng)求。 MinorFunction(UCHAR)是該IRP的副功能碼。它進(jìn)一步指出該IRP屬于哪個(gè)主功能類(lèi)。例如,IRP_MJ_PNP請(qǐng)求就有約一打的副功能碼,如IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE,等等。 Parameters (union)是幾個(gè)子結(jié)構(gòu)的聯(lián)合,每個(gè)請(qǐng)求類(lèi)型都有自己專(zhuān)用的參數(shù),而每個(gè)子結(jié)構(gòu)就是一種參數(shù)。這些子結(jié)構(gòu)包括Create (IRP_MJ_CREATE請(qǐng)求)、Read(IRP_MJ_READ請(qǐng)求)、StartDevice(IRP_MJ_PNP的 IRP_MN_START_DEVICE子類(lèi)型),等等。 DeviceObject(PDEVICE_OBJECT)是與該堆棧單元對(duì)應(yīng)的設(shè)備對(duì)象的地址。該域由IoCallDriver函數(shù)負(fù)責(zé)填寫(xiě)。 FileObject(PFILE_OBJECT)是內(nèi)核文件對(duì)象的地址,IRP的目標(biāo)就是這個(gè)文件對(duì)象。驅(qū)動(dòng)程序通常在處理清除請(qǐng)求(IRP_MJ_CLEANUP)時(shí)使用FileObject指針,以區(qū)分隊(duì)列中與該文件對(duì)象無(wú)關(guān)的IRP。 CompletionRoutine (PIO_COMPLETION_ROUTINE)是一個(gè)I/O完成例程的地址,該地址是由與這個(gè)堆棧單元對(duì)應(yīng)的驅(qū)動(dòng)程序的更上一層驅(qū)動(dòng)程序設(shè)置的。你絕對(duì)不要直接設(shè)置這個(gè)域,應(yīng)該調(diào)用IoSetCompletionRoutine函數(shù),該函數(shù)知道如何參考下一層驅(qū)動(dòng)程序的堆棧單元。設(shè)備堆棧的最低一級(jí)驅(qū)動(dòng)程序并不需要完成例程,因?yàn)樗鼈儽仨氈苯油瓿烧?qǐng)求。然而,請(qǐng)求的發(fā)起者有時(shí)確實(shí)需要一個(gè)完成例程,但通常沒(méi)有自己的堆棧單元。這就是為什么每一級(jí)驅(qū)動(dòng)程序都使用下一級(jí)驅(qū)動(dòng)程序的堆棧單元保存自己完成例程指針的原因。 Context(PVOID)是一個(gè)任意的與上下文相關(guān)的值,將作為參數(shù)傳遞給完成例程。你絕對(duì)不要直接設(shè)置該域;它由IoSetCompletionRoutine函數(shù)自動(dòng)設(shè)置,其值來(lái)自該函數(shù)的某個(gè)參數(shù)。
I/O請(qǐng)求派遣機(jī)制 Win2000的I/O請(qǐng)求是包驅(qū)動(dòng)的,當(dāng)一個(gè)I/O請(qǐng)求開(kāi)始,I/O管理器先創(chuàng)建一個(gè)IRP去跟蹤這個(gè)請(qǐng)求,另外,它存儲(chǔ)一個(gè)功能代碼在IRP的I/O堆棧區(qū)的MajorField域中來(lái)唯一的標(biāo)識(shí)請(qǐng)求的類(lèi)型。 MajorField 域是被I/O管理器用來(lái)索引驅(qū)動(dòng)程序?qū)ο蟮腗ajorFunction表,這個(gè)表包含一個(gè)指向一個(gè)特殊I/O請(qǐng)求的派遣例程的功能指針,如果驅(qū)動(dòng)程序不支持這個(gè)請(qǐng)求,MajorFunction表就會(huì)指向I/O管理器函數(shù)_IopInvalidDeviceRequest,該函數(shù)返回一個(gè)錯(cuò)誤給原始的調(diào)用者。驅(qū)動(dòng)程序的作者有責(zé)任提供所有的驅(qū)動(dòng)程序支持的派遣例程。
如果想要啟用特殊的功能代碼,驅(qū)動(dòng)程序必須聲明一個(gè)響應(yīng)這個(gè)請(qǐng)求的派遣例程,聲明機(jī)制是很簡(jiǎn)單的,只需要在DriverEntry例程中存儲(chǔ)派遣例程函數(shù)的地址到驅(qū)動(dòng)程序?qū)ο蟮腗ajorFunction表的適當(dāng)?shù)奈恢?,I/O功能代碼是這個(gè)表的索引。如下列代碼片段所示: NTSTATUS DriverEntry (IN PDRIVER_OBJECT pDO, IN PUNICODE_STRING pRegPath) pDO->MajorFunction[ IRP_MJ_CREATE ] = DispCreate; : 注意,每一個(gè)I/O功能代碼(表的索引)是被一個(gè)IRP_MJ_XXX形式的符號(hào)所標(biāo)識(shí),它們被NTDDK.h和WDM.h文件定義,這些符號(hào)常量總是被用在固定的地方。 聲明方法也允許一個(gè)單一的例程被用來(lái)處理多個(gè)請(qǐng)求類(lèi)型。DriverEntry可以放置公共的派遣例程,因?yàn)镮RP命令包含請(qǐng)求代碼,公共的函數(shù)可以被作為合適的例程而被調(diào)用。 最后,驅(qū)動(dòng)程序不支持的例程應(yīng)該被DriverEntry例程忽略,I/O管理器在調(diào)用DriverEntry例程之前就已經(jīng)將整個(gè)的MajorFunction表填充為_(kāi)IopInvalidDeviceRequest。
所有的驅(qū)動(dòng)程序必須支持IRP_MJ_CREATE功能代碼,因?yàn)檫@個(gè)功能代碼是用來(lái)響應(yīng)Win32用戶(hù)模式的CreateFile調(diào)用,如果不支持這功能代碼,Win32程序就沒(méi)有辦法獲得設(shè)備的句柄,類(lèi)似的,驅(qū)動(dòng)程序必須支持IRP_MJ_CLOSE功能代碼,因?yàn)樗脕?lái)響應(yīng)Win32用戶(hù)模式的 CloseHandle調(diào)用。順便提一下,系統(tǒng)自動(dòng)調(diào)用CloseHandle函數(shù),因?yàn)樵诔绦蛲顺龅臅r(shí)候,所有的句柄都沒(méi)有被關(guān)閉。 其它的功能代碼是否支持依賴(lài)它控制的設(shè)備的性質(zhì),當(dāng)編寫(xiě)分層的驅(qū)動(dòng)程序的時(shí)候,高層的驅(qū)動(dòng)程序必須支持連接所有的低層驅(qū)動(dòng)程序的例程,因?yàn)橛脩?hù)請(qǐng)求先通過(guò)高層的驅(qū)動(dòng)程序。
大部分的I/O管理器的操作支持一個(gè)標(biāo)準(zhǔn)的讀寫(xiě)提取,請(qǐng)求者提供一個(gè)緩沖區(qū)和緩沖區(qū)的長(zhǎng)度和傳送到或者從設(shè)備的數(shù)據(jù)。不是所有的操作都適合這個(gè)抽象。例如,磁盤(pán)的格式化或者重新分區(qū)操作不適合一般的讀或者寫(xiě)操作。這種請(qǐng)求使用擴(kuò)展的I/O請(qǐng)求代碼處理,這些代碼允許任何數(shù)量的驅(qū)動(dòng)程序指定的操作,不被讀寫(xiě)抽象所約束。 1. IRP_MJ_DEVICE_CONTROL允許擴(kuò)展的I/O請(qǐng)求,使用用戶(hù)模式的DeviceIoControl函數(shù)來(lái)調(diào)用,I/O管理器創(chuàng)建一個(gè) IRP,這個(gè)IRP的MajorFunction和IoControlCode是被DeviceIoControl函數(shù)指定其內(nèi)容。 2. IRP_MJ_INTERNAL_DEVICE_CONTROL允許內(nèi)核模式的請(qǐng)求,用戶(hù)模式?jīng)]有權(quán)力使用這個(gè)操作,它主要是用于在一個(gè)分層的堆棧中其它驅(qū)動(dòng)程序傳遞特殊的請(qǐng)求。這兩個(gè)版本的設(shè)備操作的其它的地方是相同的。請(qǐng)求者放置了一個(gè)IoControlCode值到IRP。 結(jié)果,這兩個(gè)中的任何一個(gè)的執(zhí)行都需要一個(gè)于IRP的IoControlCode值有關(guān)的二級(jí)派遣例程。這個(gè)值叫做設(shè)備I/O控制代碼(IOCTL)。因?yàn)槎?jí)派遣例程機(jī)制完全包含驅(qū)動(dòng)程序的私有例程。IOCTL值的意義是由驅(qū)動(dòng)程序指定的。下面詳細(xì)介紹設(shè)備控制接口。
傳遞給驅(qū)動(dòng)程序的IOCTL遵循一個(gè)特殊的結(jié)構(gòu),它有32-bit大小,DDK包含一個(gè)方便的產(chǎn)生IOCTL值的機(jī)制的宏,CTL_CODE。 31-16 | 15-14 | 13-2 | 1-0
IOCTL請(qǐng)求可以在同一個(gè)調(diào)用中指定同時(shí)指定一個(gè)輸入緩沖區(qū)和一個(gè)輸出緩沖區(qū)。這樣,提供了一個(gè)寫(xiě)之后再讀的抽象給調(diào)用者。有兩個(gè)訪問(wèn)用戶(hù)緩沖區(qū)的方法。 緩沖區(qū)傳輸機(jī)制是用IOCTL的ControlCode定義,獨(dú)立與整個(gè)的設(shè)備對(duì)象策略之外。
I/O 管理器從非分頁(yè)的緩沖池分配一個(gè)單一的臨時(shí)緩沖區(qū),它足夠容納調(diào)用者的輸入或者輸出緩沖區(qū)中的數(shù)據(jù),這個(gè)緩沖池的地址放在IRP的 AssociatedIrp.SystemBuffer域。然后復(fù)制請(qǐng)求者的出入緩沖區(qū)到緩沖池,再設(shè)置IRP的UserBuffer域?yàn)橛脩?hù)空間輸出緩沖區(qū)的地址。 完成IOCTL請(qǐng)求之后,I/O管理器復(fù)制系統(tǒng)緩沖池中的數(shù)據(jù)到請(qǐng)求者的用戶(hù)空間緩沖區(qū),注意只有一個(gè)內(nèi)部的緩沖池提供給驅(qū)動(dòng)程序代碼,甚至用戶(hù)指定了獨(dú)立的輸入和輸出緩沖區(qū)。驅(qū)動(dòng)程序代碼必須在向緩沖池中寫(xiě)數(shù)據(jù)之前將收到的數(shù)據(jù)全部從緩沖池中讀出。 METHOD_IN_DIRECT I/O管理器檢查請(qǐng)求者的輸入緩沖區(qū),然后將它鎖定到物理存儲(chǔ)空間中,為輸入緩沖區(qū)創(chuàng)建一個(gè)MDL,存儲(chǔ)指向MDL的指針到IRP的MdlAddress域。 也從非分頁(yè)的緩沖池中分配一個(gè)臨時(shí)的輸出緩沖區(qū),存儲(chǔ)這個(gè)緩沖區(qū)的地址到IRP的AssociatedIrp.SystemBuffer域。IRP的 UserBuffer域設(shè)置為調(diào)用者的輸出緩沖區(qū)地址。當(dāng)IOCTL IRP完成,系統(tǒng)緩沖區(qū)中的內(nèi)容被復(fù)制到調(diào)用者原始的輸出緩沖區(qū)。 METHOD_OUT_DIRECT I/O管理器檢查請(qǐng)求者的輸出緩沖區(qū),然后將它鎖定到物理存儲(chǔ)空間中,為輸出緩沖區(qū)創(chuàng)建一個(gè)MDL,存儲(chǔ)指向MDL的指針到IRP的MdlAddress域。 也從非分頁(yè)的緩沖池中分配一個(gè)臨時(shí)的輸入緩沖區(qū),存儲(chǔ)這個(gè)緩沖區(qū)的地址到IRP的AssociatedIrp.SystemBuffer域。調(diào)用者原始的輸入緩沖區(qū)的內(nèi)容被復(fù)制到系統(tǒng)緩沖區(qū),設(shè)置IRP的UserBuffer域?yàn)镹ULL。 METHOD_NEITHER I/O管理器放置調(diào)用者輸入緩沖區(qū)的地址到IRP的當(dāng)前I/O堆棧單元的Parameters.DeviceIoControl.Type3InputBuffer域,存儲(chǔ)輸出緩沖區(qū)的地址到IRP的UserBuffer域中,兩個(gè)都是用戶(hù)空間地址。
擴(kuò)展的功能用IOCTL值來(lái)定義,它存儲(chǔ)在驅(qū)動(dòng)程序常常需要的輸入或者輸出的緩沖區(qū)中,驅(qū)動(dòng)程序常使用IOCTL值來(lái)匯報(bào)執(zhí)行的數(shù)據(jù),這個(gè)數(shù)據(jù)通過(guò)用戶(hù)提供的緩沖區(qū)傳輸。確實(shí),DeviceIoControl函數(shù)為兩個(gè)緩沖區(qū)定義參數(shù),一個(gè)用來(lái)輸入,另一個(gè)用來(lái)輸出。I/O管理器提供的緩沖區(qū)傳輸機(jī)制在 IOCTOL值中定義著,可以是緩沖區(qū)I/O或者是直接I/O。像以前介紹的,對(duì)于緩沖區(qū)I/O,I/O管理器復(fù)制用戶(hù)緩沖區(qū)到(假如是寫(xiě)操作)系統(tǒng)中非分頁(yè)的緩沖池,這時(shí)驅(qū)動(dòng)程序代碼才可以方便的操作。對(duì)于直接I/O驅(qū)動(dòng)程序直接訪問(wèn)用戶(hù)緩沖區(qū)。 有趣的是,驅(qū)動(dòng)程序整個(gè)的緩沖區(qū)處理策略(在DriverEntry例程中定義)對(duì)于IOCTL傳輸不是強(qiáng)制的,也就是說(shuō),可以使用也可以不使用。確實(shí),I/O管理器使用IOCTL結(jié)構(gòu)的一個(gè)域來(lái)確定傳輸方法,這樣每個(gè)IOCTL有不同的傳輸方法。這給執(zhí)行DeviceIoControl操作最大的靈活性。
IOCTL的TransferType域有two-bits寬,它定義了下面的其中一個(gè): METHOD_BUFFERED. I/O管理器使用一個(gè)中介的非分頁(yè)的緩沖池在用戶(hù)緩沖區(qū)和驅(qū)動(dòng)程序間交換數(shù)據(jù)。 METHOD_NEITHER. I/O管理器不援助緩沖區(qū)傳輸,給驅(qū)動(dòng)程序的是用戶(hù)的原始的緩沖區(qū)地址(大概來(lái)
處理IOCTL請(qǐng)求 一旦驅(qū)動(dòng)程序聲明了IRP_MJ_DEVICE_CONTROL或者IRP_MJ_INTERNAL_DEVICE_CONTROL功能代碼的派遣例程,嚴(yán)格的解釋IOCTL設(shè)備控制代碼是驅(qū)動(dòng)程序的職責(zé)。I/O管理器不檢驗(yàn)IOCTL代碼的各個(gè)域,請(qǐng)求者傳遞的任何隨機(jī)數(shù)都可以傳遞到驅(qū)動(dòng)程序,所以驅(qū)動(dòng)程序必須作檢驗(yàn)的工作。 因此,設(shè)備控制派遣例程的典型的結(jié)構(gòu)是一個(gè)大的switch語(yǔ)句。如以下的例子: NTSTATUS DispatchIoControl (IN PDEVICE_OBJECT pDO, IN PIRP pIrp) PDEVICE_EXTENSION pDE; pIrpStack = IoGetCurrentIrpStackLocation (pIrp); // 取出IOCTL請(qǐng)求代碼 // 得到請(qǐng)求緩沖區(qū)大小 //現(xiàn)在執(zhí)行二次派遣 IoMarkIrpPending (pIrp); // 有效的IRP值- 準(zhǔn)備設(shè)備 case IOCTL_DEVICE_LAUNCH: // Same kind of processing start the device default: // 驅(qū)動(dòng)程序收到了未被承認(rèn)的控制代碼 status = STATUS_INVALID_DEVICE_REQUEST; // Valid control code cases returned above. Execution here means an error pIrp->IoStatus.Information = 0; // 數(shù)據(jù)沒(méi)有傳輸
編寫(xiě)IOCTL頭文件 因?yàn)轵?qū)動(dòng)程序工程和所有的驅(qū)動(dòng)程序的客戶(hù)需要IOCTL代碼的符號(hào)定義,通常,驅(qū)動(dòng)程序作者提供一個(gè)單獨(dú)的驅(qū)動(dòng)過(guò)程控制代碼定義的頭文件。這個(gè)頭文件應(yīng)該包含描述特殊控制操作的緩沖區(qū)內(nèi)容和所有的結(jié)構(gòu)定義。一個(gè)WIN32程序需要在包含驅(qū)動(dòng)程序的IOCTL頭文件之前包含WINIOCTL.h文件,驅(qū)動(dòng)程序工程需要在包含驅(qū)動(dòng)程序指定的IOCTL頭文件之前包含DEVIOCTL.h文件。這些文件中定義了CTL_CODE宏,下前是一個(gè)IOCTL頭文件的例子: #define IOCTL_MISSLEDEVICE_AIM CTL_CODE( \ typedef struct _AIM_OUT_BUFFER
IRP 緩沖區(qū)管理 當(dāng)一個(gè)應(yīng)用程序或者設(shè)備驅(qū)動(dòng)程序間接地利用NtReadFile, NtWriteFile或NtDeviceIoControlFile系統(tǒng)服務(wù)(或者對(duì)應(yīng)于這些服務(wù)的Windows API函數(shù),即ReadFile,WriteFile和DeviceIoControl)來(lái)創(chuàng)建一個(gè)IRP時(shí),
最基本的I/O請(qǐng)求是在用戶(hù)緩沖區(qū)和設(shè)備之間交換數(shù)據(jù),I/O管理器提供了以傳統(tǒng)的讀寫(xiě)抽象來(lái)請(qǐng)求這樣的數(shù)據(jù)傳輸,給驅(qū)動(dòng)程序majorfunction域是IRP_MJ_READ或者IRP_MJ_WRITE的IRP的形式來(lái)傳遞請(qǐng)求,另一個(gè)IRP的域指定請(qǐng)求者的緩沖區(qū)地址。I/O管理器分配和維護(hù)的緩沖區(qū)地址是否是一個(gè)直接的虛擬地址或者一個(gè)中介的非分頁(yè)緩沖池是由設(shè)備對(duì)象的Flags域決定的。無(wú)論如何,在這個(gè)緩沖區(qū)和設(shè)備件的數(shù)據(jù)傳送是讀寫(xiě)派遣例程的工作。 緩沖的I/O 在讀或者寫(xiě)操作的開(kāi)始,I/O管理器確認(rèn)用戶(hù)緩沖區(qū)的所有的虛擬存儲(chǔ)頁(yè),對(duì)于緩沖區(qū)I/O,它然后分配一個(gè)足夠用戶(hù)請(qǐng)求所使用的大小的非分頁(yè)緩沖池,這個(gè)暫時(shí)的非分頁(yè)緩沖區(qū)地址被存放在IRP的 AssociatedIrp.SystemBuffer域中,這個(gè)地址在數(shù)據(jù)傳輸過(guò)程中保持有效(也就是直到IRP被標(biāo)記上完成)。 對(duì)于讀操作,I/O管理器在IRP的UserBuffer域中紀(jì)錄原始的用戶(hù)緩沖區(qū)地址。然后使用這個(gè)保持的地址完成從非分頁(yè)的緩沖池到用戶(hù)存儲(chǔ)空間的數(shù)據(jù)拷貝工作。 對(duì)于寫(xiě)操作,I/O管理器在調(diào)用寫(xiě)派遣例程之前拷貝用戶(hù)緩沖區(qū)中的數(shù)據(jù)到非分頁(yè)的緩沖池中。然后設(shè)置IRP的UserBuffer域?yàn)镹ULL,因?yàn)闆](méi)有必要保持這個(gè)狀態(tài)。 直接I/O 在開(kāi)始操作之前,I/O管理器確認(rèn)用戶(hù)緩沖區(qū)使用的整個(gè)頁(yè)表,然后創(chuàng)建一個(gè)存儲(chǔ)器描述符表(MDL)的數(shù)據(jù)結(jié)構(gòu),然后存儲(chǔ)MDL的地址到IRP的 MdlAddress域,AssociatedIrp.SystemBuffer域和UserBuffer域被設(shè)置為NULL。 對(duì)于 DMA操作,MDL結(jié)構(gòu)被直接用來(lái)和adapter對(duì)象一起執(zhí)行數(shù)據(jù)傳輸。對(duì)于過(guò)程控制I/O,MDL結(jié)構(gòu)被用來(lái)和 MmGetSystemAddressForMdl函數(shù)一起去得到用戶(hù)緩沖區(qū)的系統(tǒng)映射地址,通過(guò)這個(gè)地址可以直接訪問(wèn)用戶(hù)緩沖區(qū)。使用這個(gè)技術(shù),用戶(hù)緩沖區(qū)被鎖定到了物理存儲(chǔ)器(也就是強(qiáng)制被變成非分頁(yè)地址),當(dāng)I/O請(qǐng)求完成,用戶(hù)緩沖區(qū)被解除鎖定。
在設(shè)備對(duì)象的Flags域中有兩個(gè)bits,它指定DO_BUFFERED_IO或者DO_DIRECT_IO。如果都沒(méi)有設(shè)定,I/O管理器不會(huì)執(zhí)行上面的兩種方式。代替的是,它簡(jiǎn)單的將用戶(hù)空間的請(qǐng)求的緩沖區(qū)地址放到IRP的UserBuffer域, AssociatedIrp.SystemBuffer和MdlAddress域被設(shè)置為NULL。 一個(gè)簡(jiǎn)單的用戶(hù)模式的地址不是十分有用,驅(qū)動(dòng)程序中的大多數(shù)例程這時(shí)不能確定映射的這個(gè)原始的頁(yè)表。因此,用戶(hù)空間地址通常是無(wú)用的。只有一個(gè)例外:在高層驅(qū)動(dòng)程序的派遣例程被調(diào)用的時(shí)候,執(zhí)行使用原始的請(qǐng)求線程,這時(shí)用戶(hù)空間地址是有效的。中間驅(qū)動(dòng)程序或者任何DPC或者中斷服務(wù)例程從來(lái)都不依賴(lài)用戶(hù)空間緩沖區(qū)。 讀寫(xiě)請(qǐng)求實(shí)例 這是一個(gè)有趣的例子,雖然它很簡(jiǎn)單。通過(guò)保留一個(gè)分頁(yè)的緩沖池和拷貝用戶(hù)的數(shù)據(jù)到這個(gè)暫時(shí)的緩沖區(qū),來(lái)完成寫(xiě)操作,這個(gè)緩沖區(qū)一直保持到讀請(qǐng)求產(chǎn)生,這時(shí)這個(gè)暫時(shí)的緩沖區(qū)中的數(shù)據(jù)將被復(fù)制到用戶(hù)緩沖區(qū),然后釋放暫時(shí)的緩沖區(qū)。下面例子示范了讀寫(xiě)派遣例程和用戶(hù)緩沖區(qū)的訪問(wèn): NTSTATUS DispatchWrite (IN PDEVICE_OBJECT pDO, IN PIRP pIrp) // 堆棧中包含用戶(hù)緩沖區(qū)的信息 // 假定使用緩沖區(qū)I/O // 暫時(shí)的緩沖區(qū)指針在DEVICE_EXTENSION 中 // 如果已經(jīng)有一個(gè)緩沖區(qū)的話,釋放它 if (pDE->deviceBuffer == NULL) // 完成IRP IoCompleteRequest (pIrp, IO_NO_INCREMENT);
NTSTATUS DispatchRead (IN PDEVICE_OBJECT pDO, IN PIRP pIrp) //堆棧中包含用戶(hù)緩沖區(qū)的信息 // 暫時(shí)的緩沖區(qū)指針在DEVICE_EXTENSION 中 // 僅傳輸用戶(hù)請(qǐng)求數(shù)量的數(shù)據(jù) // 復(fù)制臨時(shí)緩沖區(qū)到用戶(hù)空間 // 釋放臨時(shí)分頁(yè)緩沖池 // 完成I/O請(qǐng)求
IRP 開(kāi)始于某個(gè)實(shí)體調(diào)用I/O管理器函數(shù)創(chuàng)建它。在上圖中,我使用術(shù)語(yǔ)"I/O管理器"來(lái)描述這個(gè)實(shí)體,盡管系統(tǒng)中確實(shí)有一個(gè)單獨(dú)的系統(tǒng)部件用于創(chuàng)建IRP。事實(shí)上,更精確地說(shuō),應(yīng)該是某個(gè)實(shí)體創(chuàng)建了IRP,并不是操作系統(tǒng)的某個(gè)例程創(chuàng)建了IRP。但是你的驅(qū)動(dòng)程序有時(shí)也會(huì)創(chuàng)建IRP。 可以使用下面任何一種函數(shù)創(chuàng)建IRP: IoBuildAsynchronousFsdRequest 創(chuàng)建異步IRP(不需要等待其完成)。該函數(shù)和下一個(gè)函數(shù)僅適用于創(chuàng)建某些類(lèi)型的IRP。
一些IRP訪問(wèn)函數(shù)在IRP頭,其它的是處理特殊的IRP堆棧函數(shù)。了解訪問(wèn)函數(shù)需要指向整個(gè)IRP,或者僅僅指向IRP堆棧是非常重要的。 I/O管理器提供多個(gè)處理IRP的函數(shù),表4.3列出了常用的一些。 函數(shù) 描述 調(diào)用者 I/O管理器同樣提供多個(gè)函數(shù),驅(qū)動(dòng)程序可以用它們?cè)L問(wèn)IRP的堆棧部分,這些函數(shù)列在表4.4中: 函數(shù) 描述 調(diào)用者
創(chuàng)建完IRP后,你可以調(diào)用IoGetNextIrpStackLocation函數(shù)獲得該IRP第一個(gè)堆棧單元的指針。然后初始化這個(gè)堆棧單元。在初始化過(guò)程的最后,你需要填充MajorFunction代碼。堆棧單元初始化完成后,就可以調(diào)用IoCallDriver函數(shù)把IRP發(fā)送到設(shè)備驅(qū)動(dòng)程序: PDEVICE_OBJECT DeviceObject; //something gives you this PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation (Irp); <other initialization of "stack"> NTSTATUS status = IoCallDriver(DeviceObject, Irp); IoCallDriver函數(shù)的第一個(gè)參數(shù)是你在某處獲得的設(shè)備對(duì)象的地址。我將在本章的結(jié)尾處描述獲得設(shè)備對(duì)象指針的兩個(gè)常用方法。在這里,我們先假設(shè)你已經(jīng)有了這個(gè)指針。 IRP 中的第一個(gè)堆棧單元指針被初始化成指向該堆棧單元之前的堆棧單元,因?yàn)镮/O堆棧實(shí)際上是IO_STACK_LOCATION結(jié)構(gòu)數(shù)組,你可以認(rèn)為這個(gè)指針被初始化為指向一個(gè)不存在的"-1"元素,因此當(dāng)我們要初始化第一個(gè)堆棧單元時(shí)我們實(shí)際需要的是"下一個(gè)"堆棧單元。IoCallDriver將沿著這個(gè)堆棧指針找到第0個(gè)表項(xiàng),并提取我們放在那里的主功能代碼,在上例中為IRP_MJ_Xxx。然后IoCallDriver函數(shù)將利用 DriverObject指針找到設(shè)備對(duì)象中的MajorFunction表。IoCallDriver將使用主功能代碼索引這個(gè)表,最后調(diào)用找到的地址 (派遣函數(shù))。 你可以把IoCallDriver函數(shù)想象為下面代碼: NTSTATUS IoCallDriver(PDEVICE_OBJECT device, PIRP Irp)
管理自己的IRP 現(xiàn)在,我已經(jīng)解釋完IRP處理的所有底層結(jié)構(gòu),我們可以返回到創(chuàng)建IRP的主題上。我曾提到過(guò)有四種不同的服務(wù)函數(shù)可以用來(lái)創(chuàng)建IRP。但我不得不推遲到現(xiàn)在才討論如何選擇它們。下面事實(shí)供你在選擇時(shí)參考: IoBuildAsynchronousFsdRequest和IoBuildSynchronousFsdRequest函數(shù)僅能用于創(chuàng)建主功能碼在表5-3中列出的IRP。 主功能碼
使用IoBuildSynchronousFsdRequest IoBuildSynchronousFsdRequest函數(shù)的調(diào)用格式如下: PIRP Irp = IoBuildSynchronousFsdRequest(MajorFunction, MajorFunction (ULONG)是新IRP的主功能碼(見(jiàn)表5-3)。DeviceObject(PDEVICE_OBJECT)是該IRP最初要發(fā)送到的設(shè)備對(duì)象的地址。對(duì)于讀寫(xiě)請(qǐng)求,你必須提供Buffer(PVOID)、Length(ULONG)、StartingOffset(PLARGE_INTEGER) 參數(shù)。Buffer是一個(gè)內(nèi)核模式數(shù)據(jù)緩沖區(qū)的地址,Length是讀寫(xiě)操作的字節(jié)長(zhǎng)度,StartingOffset是讀寫(xiě)操作在目標(biāo)文件中的定位。對(duì)于該函數(shù)創(chuàng)建的其它請(qǐng)求,這些參數(shù)將被忽略。(這就是為什么該函數(shù)在WDM.H中的原型將這些參數(shù)定為"可選的",但對(duì)于讀寫(xiě)請(qǐng)求它們則是必需的) I/O管理器假定你給出的緩沖區(qū)地址在當(dāng)前進(jìn)程上下文中是有效的。 Event(PKEVENT)是一個(gè)事件對(duì)象的地址, IoCompleteRequest應(yīng)該在操作完成時(shí)設(shè)置這個(gè)事件,IoStatusBlock(PIO_STATUS_BLOCK)是一個(gè)狀態(tài)塊的地址,該狀態(tài)塊用于保存IRP結(jié)束狀態(tài)和信息。在操作完成前,事件對(duì)象和狀態(tài)塊必須一直存在于內(nèi)存中。 如果創(chuàng)建的是讀寫(xiě)IRP,那么在提交該 IRP前你不需要做任何事。如果創(chuàng)建的是其它類(lèi)型的IRP,你需要用附加的參數(shù)信息完成第一個(gè)堆棧單元;MajorFunction已經(jīng)被設(shè)置,不能設(shè)置未公開(kāi)的Tail.Overlay.OriginalFileObject域,這樣做將使文件對(duì)象在IRP完成時(shí)被錯(cuò)誤地廢除。同樣也不可能設(shè)置 RequestorMode,它已經(jīng)被初始化成KernelMode。(我提到這兩點(diǎn)是因?yàn)槲一叵肫鹪x過(guò)該函數(shù)的一個(gè)公開(kāi)討論,該討論認(rèn)為應(yīng)該做這兩件事,但我認(rèn)為沒(méi)有必要) 現(xiàn)在,你可以提交該IRP并等待其完成: PIRP Irp = IoBuildSynchronousFsdRequest(...); IRP完成后,你可以通過(guò)察看你的I/O狀態(tài)塊來(lái)了解該IRP的結(jié)束狀態(tài)和相關(guān)信息。
IoBuildAsynchronousFsdRequest是用于創(chuàng)建表5-3中列出的IRP的另一個(gè)例程。該函數(shù)的原型如下: PIRP IoBuildAsynchronousFsdRequest(ULONG MajorFunction, 這個(gè)原型與IoBuildSynchronousFsdRequest不同,它沒(méi)有Event參數(shù),并且IoStatusBlock指針可以為NULL。你仍需要為該函數(shù)創(chuàng)建的IRP安裝一個(gè)完成例程,這個(gè)完成例程的工作就是調(diào)用IoFreeIrp并返回 STATUS_MORE_PROCESSING_REQUIRED。 我想知道這兩個(gè)函數(shù)在創(chuàng)建IRP上有什么不同之處,因此我稍微深入了一些。這兩個(gè)函數(shù)的代碼基本上是相同的。實(shí)際上,IoBuildAsynchronousFsdRequest是 IoBuildSynchronousFsdRequest的子函數(shù)。IoBuildSynchronousFsdRequest僅有的額外操作就是保存事件指針到IRP中(I/O管理器需要找到這個(gè)事件對(duì)象并把它置成信號(hào)態(tài)),并把該IRP放到當(dāng)前線程的IRP隊(duì)列中。這可以使IRP線程死亡時(shí)能被取消。 你可能在這樣兩種情況下需要調(diào)用IoBuildAsynchronousFsdRequest:第一種情況,當(dāng)你發(fā)現(xiàn)自己執(zhí)行在任意線程上下文中并需要?jiǎng)?chuàng)建一個(gè)IRP時(shí),IoBuildAsynchronousFsdRequest函數(shù)就是理想選擇,因?yàn)楫?dāng)前線程(任意線程)的結(jié)束過(guò)程不能取消這個(gè)新創(chuàng)建的IRP。第二種情況,當(dāng)你運(yùn)行在APC_LEVEL級(jí)的非任意線程上下文時(shí),你需要同步執(zhí)行一個(gè)IRP。 IoBuildSynchronousFsdRequest不能滿(mǎn)足這個(gè)要求,因?yàn)檫@種IRQL將阻塞設(shè)置事件的APC。所以你應(yīng)該調(diào)用 IoBuildAsynchronousFsdRequest并在某個(gè)事件上等待,而這個(gè)事件將由你的完成例程去置成信號(hào)態(tài)。第二種情況不經(jīng)常出現(xiàn)在設(shè)備驅(qū)動(dòng)程序中。 通常情況下,與IoBuildAsynchronousFsdRequest配合使用的完成例程不僅僅是調(diào)用 IoFreeIrp。實(shí)際上,你需要實(shí)現(xiàn)I/O管理器用于清除已完成IRP的內(nèi)部例程(IopCompleteRequest)。你不能依賴(lài)I/O管理器來(lái)清除IoBuildAsynchronousFsdRequest創(chuàng)建的IRP。因?yàn)樵诋?dāng)前版本的Windows 98和Windows 2000中清除操作需要一個(gè)APC,并且在任意線程下執(zhí)行APC是錯(cuò)誤的,所以I/O管理器不能為你做清除工作,你必須自己做全部的清除工作。 如果IRP的目標(biāo)設(shè)備對(duì)象設(shè)置了DO_DIRECT_IO標(biāo)志,IoBuildAsynchronousFsdRequest將創(chuàng)建一個(gè)MDL,這個(gè)MDL必須由你自己釋放,如下面代碼: NTSTATUS CompletionRoutine(...) 如果IRP的目標(biāo)設(shè)備對(duì)象設(shè)置了DO_BUFFERED_IO標(biāo)志,IoBuildAsynchronousFsdRequest將分配一個(gè)系統(tǒng)緩沖區(qū),但這個(gè)緩沖區(qū)需要你來(lái)釋放。如果你正在做一個(gè)輸入操作,那么在釋放這個(gè)緩沖區(qū)之前你還需要把輸入數(shù)據(jù)從系統(tǒng)緩沖區(qū)復(fù)制到你自己真正的輸入緩沖區(qū)。并且要保證這個(gè)真正的緩沖區(qū)在非分頁(yè)內(nèi)存中,因?yàn)橥瓿衫绦枰贒ISPATCH_LEVEL級(jí)上運(yùn)行。你還需要確保你得到的是緩沖區(qū)的內(nèi)核地址,因?yàn)橥瓿衫踢\(yùn)行在任意線程上下文中。如果這些限制還不足以使你在使用IoBuildAsynchronousFsdRequest(用于DO_BUFFERED_IO設(shè)備)時(shí)感到泄氣,那么想一想你還必須在完成例程中檢測(cè)未公開(kāi)標(biāo)志位IRP_BUFFERED_IO、IRP_INPUT_OPERATION、 IRP_DEALLOCATE_BUFFER。我不將給出關(guān)于這個(gè)函數(shù)的代碼,因?yàn)槲以WC過(guò)不在本書(shū)中使用未公開(kāi)的內(nèi)部技術(shù)。 我的建議是,僅在你知道IRP的目標(biāo)設(shè)備不使用DO_BUFFERED_IO標(biāo)志時(shí),才使用IoBuildAsynchronousFsdRequest。
使用IoAllocateIrp 如果你想更深入一些,你可以使用IoAllocateIrp函數(shù)創(chuàng)建任何類(lèi)型的IRP: PIRP Irp = IoAllocateIrp(StackSize, ChargeQuota); PDEVICE_OBJECT DeviceObject; NTSTATUS OnComplete(PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)
IoCallDriver函數(shù)需要一個(gè)PDEVICE_OBJECT作為它的第一個(gè)參數(shù)。你也許想知道我是從哪得到設(shè)備對(duì)象的指針的。 一個(gè)最明顯的獲得設(shè)備對(duì)象指針的方式是調(diào)用IoAttachDeviceToDeviceStack函數(shù),這也是每個(gè)WDM驅(qū)動(dòng)程序的AddDevice函數(shù)應(yīng)做的一步。在本書(shū)的所有例子驅(qū)動(dòng)程序中,你都將看到下面一行代碼: pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo); PUNICODE_STRING DeviceName; // something gives you this 通過(guò)指定設(shè)備對(duì)象名稱(chēng)和與其相關(guān)的文件對(duì)象指針你可以得到這個(gè)設(shè)備對(duì)象的指針。文件對(duì)象就是文件句柄指向的對(duì)象。最后,你還需要解除文件對(duì)象參考,如下: ObDereferenceObject(FileObject); // DeviceObject now poison! 解除文件對(duì)象參考后,你還要釋放設(shè)備對(duì)象的隱含引用。如果你要繼續(xù)使用該設(shè)備對(duì)象,應(yīng)該先做設(shè)備對(duì)象引用: ObReferenceObject(DeviceObject); PIRP Irp = IoBuildXxxRequest(...); 你不必總在新IRP中設(shè)置文件對(duì)象指針,如果你在 IRP_MJ_CREATE的對(duì)應(yīng)例程中獲得了文件對(duì)象,那么你下面的驅(qū)動(dòng)程序就沒(méi)有必要查看該文件對(duì)象。而在前面描述的情況中,文件對(duì)象的所有者是你用 IoGetDeviceObjectPointer函數(shù)獲得設(shè)備對(duì)象的驅(qū)動(dòng)程序,在這種情況下,你必須設(shè)置文件對(duì)象指針。
尋址數(shù)據(jù)緩沖區(qū) 當(dāng)應(yīng)用程序發(fā)起一個(gè)讀或?qū)懖僮鲿r(shí),通過(guò)給出一個(gè)用戶(hù)模式虛擬地址和長(zhǎng)度,應(yīng)用程序向I/O管理器提供了一個(gè)數(shù)據(jù)緩沖區(qū)。正如我在第三章中提到的,內(nèi)核模式驅(qū)動(dòng)程序幾乎從不使用用戶(hù)模式虛擬地址訪問(wèn)內(nèi)存,因?yàn)槟悴荒馨丫€程上下文確定下來(lái)。Windows 2000為驅(qū)動(dòng)程序訪問(wèn)用戶(hù)模式數(shù)據(jù)緩沖區(qū)提供了三種方法: 在buffered方式中,I/O管理器先創(chuàng)建一個(gè)與用戶(hù)模式數(shù)據(jù)緩沖區(qū)大小相等的系統(tǒng)緩沖區(qū)。而你的驅(qū)動(dòng)程序?qū)⑹褂眠@個(gè)系統(tǒng)緩沖區(qū)工作。I/O管理器負(fù)責(zé)在系統(tǒng)緩沖區(qū)和用戶(hù)模式緩沖區(qū)之間復(fù)制數(shù)據(jù)。 指定緩沖方式 為了指定設(shè)備讀寫(xiě)的緩沖方式,你應(yīng)該在AddDevice函數(shù)中,在創(chuàng)建設(shè)備對(duì)象后,立即設(shè)置其中的標(biāo)志位: NTSTATUS AddDevice(...) Buffered方式 當(dāng)I/O 管理器創(chuàng)建IRP_MJ_READ或IRP_MJ_WRITE請(qǐng)求時(shí),它探測(cè)設(shè)備的緩沖標(biāo)志以決定如何描述新IRP中的數(shù)據(jù)緩沖區(qū)。如果 DO_BUFFERED_IO標(biāo)志設(shè)置,I/O管理器將分配與用戶(hù)緩沖區(qū)大小相同的非分頁(yè)內(nèi)存。它把緩沖區(qū)的地址和長(zhǎng)度保存到兩個(gè)十分不同的地方,見(jiàn)下面代碼片段中用粗體字表示的語(yǔ)句。你可以假定I/O管理器執(zhí)行下面代碼(注意這并不是Windows NT的源代碼): PVOID uva; // user-mode virtual buffer address PVOID sva = ExAllocatePoolWithQuota(NonPagedPoolCacheAligned, length); Irp->AssociatedIrp.SystemBuffer = sva; PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp); <code to send and await IRP> if (reading) ExFreePool(sva); 可以看出,系統(tǒng)緩沖區(qū)地址被放在IRP的AssocatedIrp.SystemBuffer域中,而數(shù)據(jù)的長(zhǎng)度被放到stack-> Parameters聯(lián)合中。在這個(gè)過(guò)程中還包含作為驅(qū)動(dòng)程序開(kāi)發(fā)者不必了解的額外細(xì)節(jié)。例如,讀操作之后的數(shù)據(jù)復(fù)制工作實(shí)際發(fā)生一個(gè)APC期間,在原始線程的上下文中,由一個(gè)與構(gòu)造該IRP完全不同的子例程執(zhí)行。I/O管理器把用戶(hù)模式虛擬地址(uva變量)保存到IRP的UserBuffer域中,這樣一來(lái)復(fù)制操作就可以找到這個(gè)地址。但你不要使代碼依賴(lài)這些事實(shí),因?yàn)樗鼈冇锌赡軙?huì)改變。IRP最終完成后,I/O管理器將釋放系統(tǒng)緩沖區(qū)所占用的內(nèi)存。
如果你在設(shè)備對(duì)象中指定DO_DIRECT_IO方式,I/O管理器將創(chuàng)建一個(gè)MDL用來(lái)描述包含該用戶(hù)模式數(shù)據(jù)緩沖區(qū)的鎖定內(nèi)存頁(yè)。MDL結(jié)構(gòu)的聲明如下: typedef struct _MDL { 圖7 -3顯示了MDL扮演的角色。StartVa成員給出了用戶(hù)緩沖區(qū)的虛擬地址,這個(gè)地址僅在擁有數(shù)據(jù)緩沖區(qū)的用戶(hù)模式進(jìn)程上下文中才有效。 ByteOffset是緩沖區(qū)起始位置在一個(gè)頁(yè)幀中的偏移值,ByteCount是緩沖區(qū)的字節(jié)長(zhǎng)度。Pages數(shù)組沒(méi)有被正式地聲明為MDL結(jié)構(gòu)的一部分,在內(nèi)存中它跟在MDL的后面,包含用戶(hù)模式虛擬地址映射為物理頁(yè)幀的個(gè)數(shù)。 用于訪問(wèn)MDL的宏和訪問(wèn)函數(shù) 宏或函數(shù) 描述 對(duì)于I/O管理器執(zhí)行的Direct方式的讀寫(xiě)操作,其過(guò)程可以想象為下面代碼: KPROCESSOR_MODE mode; // either KernelMode or UserMode <code to send and await IRP> MmUnlockPages(mdl); I/O 管理器首先創(chuàng)建一個(gè)描述用戶(hù)緩沖區(qū)的MDL。IoAllocateMdl的第三個(gè)參數(shù)(FALSE)指出這是一個(gè)主數(shù)據(jù)緩沖區(qū)。第四個(gè)參數(shù)(TRUE)指出內(nèi)存管理器應(yīng)把該內(nèi)存充入進(jìn)程配額。最后一個(gè)參數(shù)(Irp)指定該MDL應(yīng)附著的IRP。在內(nèi)部,IoAllocateMdl把Irp-> MdlAddress設(shè)置為新創(chuàng)建MDL的地址,以后你將用到這個(gè)成員,并且I/O管理器最后也使用該成員來(lái)清除MDL。 這段代碼的關(guān)鍵地方是調(diào)用MmProbeAndLockPages。該函數(shù)校驗(yàn)?zāi)莻€(gè)數(shù)據(jù)緩沖區(qū)是否有效,是否可以按適當(dāng)模式訪問(wèn)。如果我們向設(shè)備寫(xiě)數(shù)據(jù),我們必須能讀緩沖區(qū)。如果我們從設(shè)備讀數(shù)據(jù),我們必須能寫(xiě)緩沖區(qū)。另外,該函數(shù)鎖定了包含數(shù)據(jù)緩沖區(qū)的物理內(nèi)存頁(yè),并在MDL的后面填寫(xiě)了頁(yè)號(hào)數(shù)組。在效果上,一個(gè)鎖定的內(nèi)存頁(yè)將成為非分頁(yè)內(nèi)存池的一部分,直到所有對(duì)該頁(yè)內(nèi)存加鎖的調(diào)用者都對(duì)其解了鎖。 在Direct方式的讀寫(xiě)操作中,對(duì)MDL你最可能做的事是把它作為參數(shù)傳遞給其它函數(shù)。例如,DMA傳輸?shù)腗apTransfer步驟需要一個(gè)MDL。另外,在內(nèi)部,USB讀寫(xiě)操作總使用MDL。所以你應(yīng)該把讀寫(xiě)操作設(shè)置為DO_DIRECT_IO方式,并把結(jié)果MDL傳遞給USB總線驅(qū)動(dòng)程序。 順便提一下,I/O管理器確實(shí)在stack->Parameters聯(lián)合中保存了讀寫(xiě)請(qǐng)求的長(zhǎng)度,但驅(qū)動(dòng)程序應(yīng)該直接從MDL中獲得請(qǐng)求數(shù)據(jù)的長(zhǎng)度。 ULONG length = MmGetMdlByteCount(mdl);
Windows NT提供了五種內(nèi)核同步對(duì)象(Kernel Dispatcher Object),你可以用它們控制非任意線程(普通線程)的流程。表4-1列出了這些內(nèi)核同步對(duì)象的類(lèi)型及它們的用途。在任何時(shí)刻,任何對(duì)象都處于兩種狀態(tài)中的一種:信號(hào)態(tài)或非信號(hào)態(tài)。有時(shí),當(dāng)代碼運(yùn)行在某個(gè)線程的上下文中時(shí),它可以阻塞這個(gè)線程的執(zhí)行,調(diào)用KeWaitForSingleObject或 KeWaitForMultipleObjects函數(shù)可以使代碼(以及背景線程)在一個(gè)或多個(gè)同步對(duì)象上等待,等待它們進(jìn)入信號(hào)態(tài)。內(nèi)核為初始化和控制這些對(duì)象的狀態(tài)提供了例程。 表4-1. 內(nèi)核同步對(duì)象 對(duì)象 數(shù)據(jù)類(lèi)型 描述
何時(shí)阻塞和怎樣阻塞一個(gè)線程 為了理解WDM驅(qū)動(dòng)程序何時(shí)以及如何利用內(nèi)核同步對(duì)象阻塞一個(gè)線程,你必須先對(duì)線程有一些基本了解。通常,如果在線程執(zhí)行時(shí)發(fā)生了軟件或硬件中斷,那么在內(nèi)核處理中斷期間,該線程仍然是"當(dāng)前"線程。而內(nèi)核模式代碼執(zhí)行時(shí)所在的上下文環(huán)境就是指這個(gè)"當(dāng)前"線程的上下文。為了響應(yīng)各種中斷,Windows NT調(diào)度器可能會(huì)切換線程,這樣,另一個(gè)線程將成為新的"當(dāng)前"線程。 術(shù)語(yǔ)"任意線程上下文(arbitrary thread context)"和"非任意線程上下文(nonarbitrary thread context)"用于精確描述驅(qū)動(dòng)程序例程執(zhí)行時(shí)所處于的上下文種類(lèi)。如果我們知道程序正處于初始化I/O請(qǐng)求線程的上下文中,則該上下文不是任意上下文。然而,在大部分時(shí)間里,WDM驅(qū)動(dòng)程序無(wú)法知道這個(gè)事實(shí),因?yàn)榭刂颇膫€(gè)線程應(yīng)該激活的機(jī)會(huì)通常都是在中斷發(fā)生時(shí)。當(dāng)應(yīng)用程序發(fā)出I/O請(qǐng)求時(shí),將產(chǎn)生一個(gè)從用戶(hù)模式到內(nèi)核模式的轉(zhuǎn)換,而創(chuàng)建并發(fā)送該IRP的I/O管理器例程將繼續(xù)運(yùn)行在非任意線程的上下文中。我們用術(shù)語(yǔ)"最高級(jí)驅(qū)動(dòng)程序"來(lái)描述第一個(gè)收到該IRP的驅(qū)動(dòng)程序。 通常,只有給定設(shè)備的最高級(jí)驅(qū)動(dòng)程序才能確切地知道它執(zhí)行在一個(gè)非任意線程的上下文中。這是因?yàn)轵?qū)動(dòng)程序派遣例程通常把請(qǐng)求放入隊(duì)列后立即返回調(diào)用者。之后通過(guò)回調(diào)函數(shù),請(qǐng)求被提出隊(duì)列并下傳到低級(jí)驅(qū)動(dòng)程序。一旦派遣例程掛起某個(gè)請(qǐng)求,所有對(duì)該請(qǐng)求的后期處理必須發(fā)生在任意線程上下文中。 解釋完線程上下文后,我們可以陳訴出關(guān)于線程阻塞的簡(jiǎn)單規(guī)則: 當(dāng)我們處理某個(gè)請(qǐng)求時(shí),僅能阻塞產(chǎn)生該請(qǐng)求的線程。
Windows NT為每個(gè)硬件中斷和少數(shù)軟件事件賦予了一個(gè)優(yōu)先級(jí),即中斷請(qǐng)求級(jí)(interrupt request level - IRQL)。IRQL為CPU上的活動(dòng)提供了同步方法,它基于下面規(guī)則: 一旦某CPU執(zhí)行在高于PASSIVE_LEVEL的IRQL上時(shí),該CPU上的活動(dòng)僅能被擁有更高IRQL的活動(dòng)搶先。 圖4 -1顯示了x86平臺(tái)上的IRQL值范圍。(通常,這個(gè)IRQL數(shù)值要取決于你所面對(duì)的平臺(tái)) 用戶(hù)模式程序執(zhí)行在PASSIVE_LEVEL上,可以被任何執(zhí)行在高于該IRQL上的活動(dòng)搶先。許多設(shè)備驅(qū)動(dòng)程序例程也執(zhí)行在 PASSIVE_LEVEL上。第二章中討論的DriverEntry和AddDevice例程就屬于這類(lèi),大部分IRP派遣例程也屬于這類(lèi)。 某些公共驅(qū)動(dòng)程序例程執(zhí)行在DISPATCH_LEVEL上,而DISPATCH_LEVEL級(jí)要比PASSIVE_LEVEL級(jí)高。這些公共例程包括 StartIo例程,DPC(推遲過(guò)程調(diào)用)例程,和其它一些例程。這些例程的共同特點(diǎn)是,它們都需要訪問(wèn)設(shè)備對(duì)象和設(shè)備擴(kuò)展中的某些域,它們都不受派遣例程的干擾或互相干擾。當(dāng)任何一個(gè)這樣的例程運(yùn)行時(shí),上面陳述的規(guī)則可以保證它們不被任何驅(qū)動(dòng)程序的派遣例程搶先,因?yàn)榕汕怖瘫旧韴?zhí)行在更低級(jí)的 IRQL上。另外,它們也不會(huì)被同類(lèi)例程搶先,因?yàn)槟切├踢\(yùn)行的IRQL與它們自己的相同。只有擁有更高IRQL的活動(dòng)才能搶先它們。 注意 派遣例程(Dispatch routine)和DISPATCH_LEVEL級(jí)名稱(chēng)類(lèi)似。之所以稱(chēng)做派遣例程是因?yàn)镮/O管理器向這些函數(shù)派遣I/O請(qǐng)求。而存在派遣級(jí) (DISPATCH_LEVEL)這個(gè)名稱(chēng)是因?yàn)閮?nèi)核線程派遣器運(yùn)行在這個(gè)IRQL上,它決定下一次該執(zhí)行哪個(gè)線程。(現(xiàn)在,線程調(diào)度程序通常運(yùn)行在 SYNCH_LEVEL級(jí)上) ---------------- 在DISPATCH_LEVEL 級(jí)和PROFILE_LEVEL級(jí)之間是各種硬件中斷級(jí)。通常,每個(gè)有中斷能力的設(shè)備都有一個(gè)IRQL,它定義了該設(shè)備的中斷優(yōu)先級(jí)別。WDM驅(qū)動(dòng)程序只有在收到一個(gè)副功能碼為IRP_MN_START_DEVICE的IRP_MJ_PNP請(qǐng)求后,才能確定其設(shè)備的IRQL。設(shè)備的配置信息作為參數(shù)傳遞給該請(qǐng)求,而設(shè)備的IRQL就包含在這個(gè)配置信息中。我們通常把設(shè)備的中斷級(jí)稱(chēng)為設(shè)備IRQL,或DIRQL。 其它IRQL級(jí)的含義有時(shí)需要依靠具體的CPU結(jié)構(gòu)。這些IRQL通常僅被Windows NT內(nèi)核內(nèi)部使用,因此它們的含義與設(shè)備驅(qū)動(dòng)程序的編寫(xiě)不是特別密切相關(guān)。例如,我將要在本章后面詳細(xì)討論的APC_LEVEL,當(dāng)系統(tǒng)在該級(jí)上為某線程調(diào)度APC(異步過(guò)程調(diào)用)例程時(shí)不會(huì)被同一CPU上的其它線程所干擾。在HIGH_LEVEL級(jí)上系統(tǒng)可以執(zhí)行一些特殊操作,如系統(tǒng)休眠前的內(nèi)存快照、處理bug check、處理假中斷,等等。 IRQL的變化 為了演示IRQL的重要性,參見(jiàn)圖 4-2,該圖顯示了發(fā)生在單CPU上的一系列事件。在時(shí)間序列的開(kāi)始處,CPU執(zhí)行在PASSIVE_LEVEL級(jí)上。在t1時(shí)刻,一個(gè)中斷到達(dá),它的服務(wù)例程執(zhí)行在DIRQL1上,該級(jí)是在DISPATCH_LEVEL和PROFILE_LEVEL之間的某個(gè)DIRQL。在t2時(shí)刻,另一個(gè)中斷到達(dá),它的服務(wù)例程執(zhí)行在DIRQL2上,比DIRQL1低一級(jí)。我們討論過(guò)搶先規(guī)則,所以CPU將繼續(xù)服務(wù)于第一個(gè)中斷。當(dāng)?shù)谝粋€(gè)中斷服務(wù)例程在t3時(shí)刻完成時(shí),該中斷服務(wù)程序可能會(huì)請(qǐng)求一個(gè)DPC。而DPC例程是執(zhí)行在DISPATCH_LEVEL上。所以當(dāng)前存在的未執(zhí)行的最高優(yōu)先級(jí)的活動(dòng)就是第二個(gè)中斷的服務(wù)例程,所以系統(tǒng)接著執(zhí)行第二個(gè)中斷的服務(wù)例程。這個(gè)例程在t4時(shí)刻結(jié)束,假設(shè)這之后再?zèng)]有其它中斷發(fā)生,CPU將降到DISPATCH_LEVEL 級(jí)上執(zhí)行第一個(gè)中斷的DPC例程。當(dāng)DPC例程在t5時(shí)刻完成后,IRQL又落回到原來(lái)的PASSIVE_LEVEL級(jí)。 基本同步規(guī)則 遵循下面規(guī)則,你可以利用IRQL的同步效果: 所有對(duì)共享數(shù)據(jù)的訪問(wèn)都應(yīng)該在同一(提升的)IRQL上進(jìn)行。 換句話說(shuō),不論何時(shí)何地,如果你的代碼訪問(wèn)的數(shù)據(jù)對(duì)象被其它代碼共享,那么你應(yīng)該使你的代碼執(zhí)行在高于PASSIVE_LEVEL的級(jí)上。一旦越過(guò) PASSIVE_LEVEL級(jí),操作系統(tǒng)將不允許同IRQL的活動(dòng)相互搶先,從而防止了潛在的沖突。然而這個(gè)規(guī)則不足以保護(hù)多處理器機(jī)器上的數(shù)據(jù),在多處理器機(jī)器中你還需要另外的防護(hù)措施--自旋鎖(spin lock)。如果你僅關(guān)心單CPU上的操作,那么使用IRQL就可以解決所有同步問(wèn)題。但事實(shí)上,所有WDM驅(qū)動(dòng)程序都必須設(shè)計(jì)成能夠運(yùn)行在多處理器的系統(tǒng)上。 IRQL與線程優(yōu)先級(jí) 線程優(yōu)先級(jí)是與IRQL非常不同的概念。線程優(yōu)先級(jí)控制著線程調(diào)度器的調(diào)度動(dòng)作,決定何時(shí)搶先運(yùn)行線程以及下一次運(yùn)行什么線程。然而,當(dāng)IRQL級(jí)高于或等于DISPATCH_LEVEL級(jí)時(shí)線程切換停止,無(wú)論當(dāng)前活動(dòng)的是什么線程都將保持活動(dòng)狀態(tài)直到IRQL降到DISPATCH_LEVEL級(jí)之下。而此時(shí)的"優(yōu)先級(jí)"僅指IRQL本身,由它控制到底哪個(gè)活動(dòng)該執(zhí)行,而不是該切換到哪個(gè)線程的上下文。
執(zhí)行在提升的IRQL級(jí)上的一個(gè)后果是,系統(tǒng)將不能處理頁(yè)故障(系統(tǒng)在APC級(jí)處理頁(yè)故障)。這意味著: 執(zhí)行在高于或等于DISPATCH_LEVEL級(jí)上的代碼絕對(duì)不能造成頁(yè)故障。 這也意味著執(zhí)行在高于或等于DISPATCH_LEVEL級(jí)上的代碼必須存在于非分頁(yè)內(nèi)存中。此外,所有這些代碼要訪問(wèn)的數(shù)據(jù)也必須存在于非分頁(yè)內(nèi)存中。最后,隨著IRQL的提升,你能使用的內(nèi)核模式支持例程將會(huì)越來(lái)越少。 DDK文檔中明確指出支持例程的IRQL限定。例如,KeWaitForSingleObject例程有兩個(gè)限定: 調(diào)用者必須運(yùn)行在低于或等于DISPATCH_LEVEL級(jí)上。
內(nèi)核同步對(duì)象 Windows NT提供了五種內(nèi)核同步對(duì)象(Kernel Dispatcher Object),你可以用它們控制非任意線程(普通線程)的流程。表4-1列出了這些內(nèi)核同步對(duì)象的類(lèi)型及它們的用途。在任何時(shí)刻,任何對(duì)象都處于兩種狀態(tài)中的一種:信號(hào)態(tài)或非信號(hào)態(tài)。有時(shí),當(dāng)代碼運(yùn)行在某個(gè)線程的上下文中時(shí),它可以阻塞這個(gè)線程的執(zhí)行,調(diào)用KeWaitForSingleObject或 KeWaitForMultipleObjects函數(shù)可以使代碼(以及背景線程)在一個(gè)或多個(gè)同步對(duì)象上等待,等待它們進(jìn)入信號(hào)態(tài)。內(nèi)核為初始化和控制這些對(duì)象的狀態(tài)提供了例程。 表4-1. 內(nèi)核同步對(duì)象
為了理解WDM驅(qū)動(dòng)程序何時(shí)以及如何利用內(nèi)核同步對(duì)象阻塞一個(gè)線程,你必須先對(duì)線程有一些基本了解。通常,如果在線程執(zhí)行時(shí)發(fā)生了軟件或硬件中斷,那么在內(nèi)核處理中斷期間,該線程仍然是"當(dāng)前"線程。而內(nèi)核模式代碼執(zhí)行時(shí)所在的上下文環(huán)境就是指這個(gè)"當(dāng)前"線程的上下文。為了響應(yīng)各種中斷,Windows NT調(diào)度器可能會(huì)切換線程,這樣,另一個(gè)線程將成為新的"當(dāng)前"線程。 術(shù)語(yǔ)"任意線程上下文(arbitrary thread context)"和"非任意線程上下文(nonarbitrary thread context)"用于精確描述驅(qū)動(dòng)程序例程執(zhí)行時(shí)所處于的上下文種類(lèi)。如果我們知道程序正處于初始化I/O請(qǐng)求線程的上下文中,則該上下文不是任意上下文。然而,在大部分時(shí)間里,WDM驅(qū)動(dòng)程序無(wú)法知道這個(gè)事實(shí),因?yàn)榭刂颇膫€(gè)線程應(yīng)該激活的機(jī)會(huì)通常都是在中斷發(fā)生時(shí)。當(dāng)應(yīng)用程序發(fā)出I/O請(qǐng)求時(shí),將產(chǎn)生一個(gè)從用戶(hù)模式到內(nèi)核模式的轉(zhuǎn)換,而創(chuàng)建并發(fā)送該IRP的I/O管理器例程將繼續(xù)運(yùn)行在非任意線程的上下文中。我們用術(shù)語(yǔ)"最高級(jí)驅(qū)動(dòng)程序"來(lái)描述第一個(gè)收到該IRP的驅(qū)動(dòng)程序。 通常,只有給定設(shè)備的最高級(jí)驅(qū)動(dòng)程序才能確切地知道它執(zhí)行在一個(gè)非任意線程的上下文中。這是因?yàn)轵?qū)動(dòng)程序派遣例程通常把請(qǐng)求放入隊(duì)列后立即返回調(diào)用者。之后通過(guò)回調(diào)函數(shù),請(qǐng)求被提出隊(duì)列并下傳到低級(jí)驅(qū)動(dòng)程序。一旦派遣例程掛起某個(gè)請(qǐng)求,所有對(duì)該請(qǐng)求的后期處理必須發(fā)生在任意線程上下文中。 解釋完線程上下文后,我們可以陳訴出關(guān)于線程阻塞的簡(jiǎn)單規(guī)則: 當(dāng)我處理某個(gè)請(qǐng)求時(shí),僅能阻塞產(chǎn)生該請(qǐng)求的線程。 通常,僅有設(shè)備的最高級(jí)驅(qū)動(dòng)程序才能應(yīng)用這個(gè)規(guī)則。但有一個(gè)重要的例外,IRP_MN_START_DEVICE請(qǐng)求,所有驅(qū)動(dòng)程序都以同步方式處理這個(gè)請(qǐng)求。即驅(qū)動(dòng)程序不排隊(duì)或掛起該類(lèi)請(qǐng)求。當(dāng)你收到這種請(qǐng)求時(shí),你可以直接從堆棧中找到請(qǐng)求的發(fā)起者。正如我在第六章中講到的,處理這種請(qǐng)求時(shí)你必須阻塞那個(gè)線程。 下面規(guī)則表明在提升的IRQL級(jí)上不可能發(fā)生線程切換: 執(zhí)行在高于或等于DISPATCH_LEVEL級(jí)上的代碼不能阻塞線程。 這個(gè)規(guī)則表明你只能在DriverEntry函數(shù)、AddDevice函數(shù),或驅(qū)動(dòng)程序的派遣函數(shù)中阻塞當(dāng)前線程。因?yàn)檫@些函數(shù)都執(zhí)行在 PASSIVE_LEVEL級(jí)上。沒(méi)有必要在DriverEntry或AddDevice函數(shù)中阻塞當(dāng)前線程,因?yàn)檫@些函數(shù)的工作僅僅是初始化一些數(shù)據(jù)結(jié)構(gòu)。 在單同步對(duì)象上等待 你可以按下面方法調(diào)用KeWaitForSingleObject函數(shù): ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); 在這個(gè)調(diào)用中,object指向你要等待的對(duì)象。注意該參數(shù)的類(lèi)型是PVOID,它應(yīng)該指向一個(gè)表4-1中列出的同步對(duì)象。該對(duì)象必須在非分頁(yè)內(nèi)存中,例如,在設(shè)備擴(kuò)展中或其它從非分頁(yè)內(nèi)存池中分配的數(shù)據(jù)區(qū)。在大部分情況下,執(zhí)行堆棧可以被認(rèn)為是非分頁(yè)的。 WaitReason 是一個(gè)純粹建議性的值,它是KWAIT_REASON枚舉類(lèi)型。實(shí)際上,除非你指定了WrQueue參數(shù),否則任何內(nèi)核代碼都不關(guān)心此值。線程阻塞的原因被保存到一個(gè)不透明的數(shù)據(jù)結(jié)構(gòu)中,如果你了解這個(gè)數(shù)據(jù)結(jié)構(gòu),那么在調(diào)試某種死鎖時(shí),你也許會(huì)從這個(gè)原因代碼中獲得一些線索。通常,驅(qū)動(dòng)程序應(yīng)把該參數(shù)指定為Executive,代表無(wú)原因。 WaitMode是MODE枚舉類(lèi)型,該枚舉類(lèi)型僅有兩個(gè)值:KernelMode和UserMode。 Alertable 是一個(gè)布爾類(lèi)型的值。它不同于WaitReason,這個(gè)參數(shù)以另一種方式影響系統(tǒng)行為,它決定等待是否可以提前終止以提交一個(gè)APC。如果等待發(fā)生在用戶(hù)模式中,那么內(nèi)存管理器就可以把線程的內(nèi)核模式堆棧換出。如果驅(qū)動(dòng)程序以自動(dòng)變量(在堆棧中)形式創(chuàng)建事件對(duì)象,并且某個(gè)線程又在提升的IRQL級(jí)上調(diào)用了KeSetEvent,而此時(shí)該事件對(duì)象剛好又被換出內(nèi)存,結(jié)果將產(chǎn)生一個(gè)bug check。所以我們應(yīng)該總把a(bǔ)lertable參數(shù)指定為FALSE,即在內(nèi)核模式中等待。 最后一個(gè)參數(shù)&timeout是一個(gè) 64位超時(shí)值的地址,單位為100納秒。正數(shù)的超時(shí)表示一個(gè)從1601年1月1日起的絕對(duì)時(shí)間。調(diào)用KeQuerySystemTime函數(shù)可以獲得當(dāng)前系統(tǒng)時(shí)間。負(fù)數(shù)代表相對(duì)于當(dāng)前時(shí)間的時(shí)間間隔。如果你指定了絕對(duì)超時(shí),那么系統(tǒng)時(shí)鐘的改變也將影響到你的超時(shí)時(shí)間。如果系統(tǒng)時(shí)間越過(guò)你指定的絕對(duì)時(shí)間,那么永遠(yuǎn)都不會(huì)超時(shí)。相反,如果你指定相對(duì)超時(shí),那么你經(jīng)過(guò)的超時(shí)時(shí)間將不受系統(tǒng)時(shí)鐘改變的影響。 指定0超時(shí)將使 KeWaitForSingleObject函數(shù)立即返回,返回的狀態(tài)代碼指出對(duì)象是否處于信號(hào)態(tài)。如果你的代碼執(zhí)行在DISPATCH_LEVEL級(jí)上,則必須指定0超時(shí),因?yàn)樵谶@個(gè)IRQL上不允許阻塞。每個(gè)內(nèi)核同步對(duì)象都提供一組KeReadStateXxx服務(wù)函數(shù),使用這些函數(shù)可以直接獲得對(duì)象的狀態(tài)。然而,取對(duì)象狀態(tài)與0超時(shí)等待不完全等價(jià):當(dāng)KeWaitForSingleObject發(fā)現(xiàn)等待被滿(mǎn)足后,它執(zhí)行特殊對(duì)象要求的附加動(dòng)作。相比之下,取對(duì)象狀態(tài)不執(zhí)行任何附加動(dòng)作,即使對(duì)象已經(jīng)處于信號(hào)態(tài)。 超時(shí)參數(shù)也可以指定為NULL指針,這代表無(wú)限期等待。 該函數(shù)的返回值指出幾種可能的結(jié)果。STATUS_SUCCESS結(jié)果是你所希望的,表示等待被滿(mǎn)足。即在你調(diào)用 KeWaitForSingleObject時(shí),對(duì)象或者已經(jīng)進(jìn)入信號(hào)態(tài),或者后來(lái)進(jìn)入信號(hào)態(tài)。如果等待以第二種情況滿(mǎn)足,則有必要在同步對(duì)象上執(zhí)行附加動(dòng)作。當(dāng)然,這個(gè)附加動(dòng)作還要參考對(duì)象的類(lèi)型,我將在后面討論具體對(duì)象類(lèi)型時(shí)再解釋這一點(diǎn)。(例如,一個(gè)同步類(lèi)型的事件在你的等待滿(mǎn)足后需要重置該事件) 返回值STATUS_TIMEOUT指出在指定的超時(shí)期限內(nèi)對(duì)象未進(jìn)入信號(hào)態(tài)。如果指定0超時(shí),則函數(shù)將立即返回。返回代碼為 STATUS_TIMEOUT,代表對(duì)象處于非信號(hào)態(tài),返回代碼為STATUS_SUCCESS,代表對(duì)象處于信號(hào)態(tài)。如果指定NULL超時(shí),則不可能有返回值。 其它兩個(gè)返回值STATUS_ALERTED和STATUS_USER_APC表示等待提前終止,對(duì)象未進(jìn)入信號(hào)態(tài)。原因是線程接收到一個(gè)警惕(alert)或一個(gè)用戶(hù)模式的APC 在多同步對(duì)象上等待 KeWaitForMultipleObjects函數(shù)用于同時(shí)等待一個(gè)或多個(gè)同步對(duì)象。該函數(shù)調(diào)用方式如下: ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); 在這里,objects指向一個(gè)指針數(shù)組,每個(gè)數(shù)組元素指向一個(gè)同步對(duì)象,count是數(shù)組中指針的個(gè)數(shù)。count必須小于或等于 MAXIMUM_WAIT_OBJECTS值(當(dāng)前為64)。這個(gè)數(shù)組和它所指向的所有對(duì)象都必須在非分頁(yè)內(nèi)存中。WaitType是枚舉類(lèi)型,其值可以為WaitAll或WaitAny,它指出你是等到所有對(duì)象都進(jìn)入信號(hào)態(tài),還是只要有一個(gè)對(duì)象進(jìn)入信號(hào)態(tài)就可以。 waitblocks參數(shù)指向一個(gè)KWAIT_BLOCK結(jié)構(gòu)數(shù)組,內(nèi)核用這個(gè)結(jié)構(gòu)數(shù)組管理等待操作。你不需要初始化這些結(jié)構(gòu),內(nèi)核僅需要知道這個(gè)結(jié)構(gòu)數(shù)組在哪里,內(nèi)核用它來(lái)記錄每個(gè)對(duì)象在等待中的狀態(tài)。如果你僅需要等待小數(shù)量的對(duì)象(不超過(guò)THREAD_WAIT_OBJECTS,該值當(dāng)前為3),你可以把該參數(shù)指定為 NULL。如果該參數(shù)為NULL,KeWaitForMultipleObjects將使用線程對(duì)象中預(yù)分配的等待塊數(shù)組。如果你等待的對(duì)象數(shù)超過(guò) THREAD_WAIT_OBJECTS,你必須提供一塊長(zhǎng)度至少為count * sizeof(KWAIT_BLOCK)的非分頁(yè)內(nèi)存。 其余參數(shù)與KeWaitForSingleObject中的對(duì)應(yīng)參數(shù)作用相同,而且大部分返回碼也有相同的含義。 如果你指定了WaitAll,則返回值STATUS_SUCCESS表示等待的所有對(duì)象都進(jìn)入了信號(hào)態(tài)。如果你指定了WaitAny,則返回值在數(shù)值上等于進(jìn)入信號(hào)態(tài)的對(duì)象在objects數(shù)組中的索引。如果碰巧有多個(gè)對(duì)象進(jìn)入了信號(hào)態(tài),則該值僅代表其中的一個(gè),可能是第一個(gè)也可能是其它。你可以認(rèn)為該值等于STATUS_WAIT_0加上數(shù)組索引。你可以先用NT_SUCCESS測(cè)試返回碼,然后再?gòu)钠渲刑崛?shù)組索引: NTSTATUS status = KeWaitForMultipleObjects(...); 如果KeWaitForMultipleObjects返回成功代碼,它也將執(zhí)行等待被滿(mǎn)足的那個(gè)對(duì)象的附加動(dòng)作。如果多個(gè)對(duì)象同時(shí)進(jìn)入信號(hào)態(tài)而你指定的WaitType參數(shù)為WaitAny,那么該函數(shù)僅執(zhí)行返回值指定對(duì)象的附加動(dòng)作。 內(nèi)核事件 表4-2列出了用于處理內(nèi)核事件的服務(wù)函數(shù)。為了初始化一個(gè)事件對(duì)象,我們首先應(yīng)該為其分配非分頁(yè)存儲(chǔ),然后調(diào)用KeInitializeEvent: ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); 表4-2. 用于內(nèi)核事件對(duì)象的服務(wù)函數(shù) ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); 在上面代碼中,ASSERT語(yǔ)句強(qiáng)制你必須在低于或等于DISPATCH_LEVEL級(jí)上調(diào)用該函數(shù)。event參數(shù)指向一個(gè)事件對(duì)象,boost值用于提升等待線程的優(yōu)先級(jí)。wait參數(shù)的解釋見(jiàn)文字框"KeSetEvent的第三個(gè)參數(shù)",WDM驅(qū)動(dòng)程序幾乎從不把wait參數(shù)指定為T(mén)RUE。如果該事件已經(jīng)處于信號(hào)態(tài),則該函數(shù)返回非0值。如果該事件處于非信號(hào)態(tài),則該函數(shù)返回0。 多任務(wù)調(diào)度器需要人為地提升等I/O操作或同步對(duì)象的線程的優(yōu)先級(jí),以避免餓死長(zhǎng)時(shí)間等待的線程。這是因?yàn)楸蛔枞木€程往往是放棄自己的時(shí)間片并且不再要求獲得CPU,但只要這些線程獲得了比其它線程更高的優(yōu)先級(jí),或者其它同一優(yōu)先級(jí)的線程用完了自己的時(shí)間片,它們就可以恢復(fù)執(zhí)行。注意,正處于自己時(shí)間片中的線程不能被阻塞。 用于提升阻塞線程優(yōu)先級(jí)的boost值不太好選擇。一個(gè)較好的笨方法是指定IO_NO_INCREMENT值,當(dāng)然,如果你有更好的值,可以不用這個(gè)值。如果事件喚醒的是一個(gè)處理時(shí)間敏感數(shù)據(jù)流的線程(如聲卡驅(qū)動(dòng)程序),那么應(yīng)該使用適合那種設(shè)備的boost值(如IO_SOUND_INCREMENT)。重要的是,不要為一個(gè)愚蠢的理由去提高等待者的優(yōu)先級(jí)。例如,如果你要同步處理一個(gè)IRP_MJ_PNP請(qǐng)求,那么在你要停下來(lái)等待低級(jí)驅(qū)動(dòng)程序處理完該IRP時(shí),你的完成例程應(yīng)調(diào)用KeSetEvent。由于PnP請(qǐng)求對(duì)于處理器沒(méi)有特殊要求并且也不經(jīng)常發(fā)生,所以即使是聲卡驅(qū)動(dòng)程序也也應(yīng)該把boost參數(shù)指定為 IO_NO_INCREMENT。
IoSetCompletionRoutine(Irp, Irp就是你要了解其完成的請(qǐng)求。CompletionRoutine是被調(diào)用的完成例程的地址,context是任何一個(gè)指針長(zhǎng)度的值,將作為完成例程的參數(shù)。InvokeOnXxx參數(shù)是布爾值,它們指出在三種不同的環(huán)境中是否需要調(diào)用完成例程: InvokeOnSuccess 你希望完成例程在IRP以成功狀態(tài)(返回的狀態(tài)代碼通過(guò)了NT_SUCCESS測(cè)試)完成時(shí)被調(diào)用。 |
|
來(lái)自: tuohuang0303 > 《編程》