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