在驱动和应用程序间共享内存
在不同的場合,很多驅(qū)動(dòng)編寫人員需要在驅(qū)動(dòng)和用戶程序間共享內(nèi)存。兩種最容易的技術(shù)是:?
?
l?應(yīng)用程序發(fā)送IOCTL給驅(qū)動(dòng)程序,提供一個(gè)指向內(nèi)存的指針,之后驅(qū)動(dòng)程序和應(yīng)用程序就可以共享內(nèi)存。(應(yīng)用程序分配共享內(nèi)存)?
?
l?由驅(qū)動(dòng)程序分配內(nèi)存頁,并映射這些內(nèi)存頁到指定用戶模式進(jìn)程的地址空間,并且將地址返回給應(yīng)用程序。(驅(qū)動(dòng)程序分配共享內(nèi)存)?
?
使用IOCTL共享Buffer:?
?
使用一個(gè)IOCT描述的Buffer,在驅(qū)動(dòng)和用戶程序間共享內(nèi)存是內(nèi)存共享最簡單的實(shí)現(xiàn)形式。畢竟,IOCTL也是驅(qū)動(dòng)支持其他I/O請求最經(jīng)典的方法。應(yīng)用程序調(diào)用Win32函數(shù)DeviceIoControl(),要被共享的Buffer的基地址和長度被放入OutBuffer參數(shù)中。對于使用這種Buffer共享方式的驅(qū)動(dòng)編寫者需要確定的事情就是對于特定的IOCTL采取哪種Buffer method。既可以使用METHOD_XXX_DIRECT,也可以使用METHOD_NEITHER。?
?
(PS:在METHOD_XXX_DIRECT模式下,IO管理器為應(yīng)用層指定的輸出緩沖區(qū)(OutputBuffer)創(chuàng)建一個(gè)MDL鎖住該應(yīng)用層的緩沖區(qū)內(nèi)存,然后,我們可以在內(nèi)核層中使用MmGetSystemAddressForMdlSafe獲得應(yīng)用層輸出緩沖區(qū)所對應(yīng)的內(nèi)核層地址。MDL地址被放在了Irp->MdlAddress中)。?
?
如果采用METHOD_XXX_DIRECT方式,那用戶Buffer將被檢查是否正確存取,檢查過后用戶Buffer將被鎖進(jìn)內(nèi)存。驅(qū)動(dòng)需要調(diào)用MmGetSystemAddressForMdlSafe將前述Buffer映射到內(nèi)核地址空間。這種方式的一個(gè)優(yōu)點(diǎn)就是驅(qū)動(dòng)可以在任意進(jìn)程上下文、任意IRQL優(yōu)先級別上存取共享內(nèi)存Buffer。如果只需要將數(shù)據(jù)傳給驅(qū)動(dòng)則使用METHOD_IN_DIRECT方式。如果從驅(qū)動(dòng)返回?cái)?shù)據(jù)給應(yīng)用程序或者做雙向數(shù)據(jù)交換則使用METHOD_OUT_BUFFER。?
?
(PS:這些檢查都將由IO管理器來負(fù)責(zé),并且,此時(shí)IO管理器將為用戶層的緩沖區(qū)創(chuàng)建MDL。因?yàn)榇藭r(shí)還未到設(shè)備驅(qū)動(dòng)層,當(dāng)前上下文還屬于當(dāng)前發(fā)起 DeviceIo調(diào)用的進(jìn)程,用戶模式緩沖區(qū)的內(nèi)存有效。但是,經(jīng)過IO管理器發(fā)送IRP到下層驅(qū)動(dòng)時(shí),就不能保證當(dāng)前上下文,幸而有IO管理器為我們創(chuàng)建MDL。這樣,我們就可以在內(nèi)核層獲得對應(yīng)的內(nèi)核地址,并且自由寫入數(shù)據(jù)。)?
?
使用METHOD_NEITHER方式描述一個(gè)共享內(nèi)存Buffer存在許多固有的限制和需要小心的地方。(基本上,在任何時(shí)候一個(gè)驅(qū)動(dòng)使用這種方式都是一樣的)。其中最主要的規(guī)則是驅(qū)動(dòng)只能在發(fā)起請求進(jìn)程的上下文中存取Buffer。這是因?yàn)橐ㄟ^Buffer的用戶虛擬地址存取共享內(nèi)存Buffer。這也就意味著驅(qū)動(dòng)必須要在設(shè)備棧的頂端,被用戶應(yīng)用程序經(jīng)由IO Manager直接調(diào)用。期間不能存在中間層驅(qū)動(dòng)或者文件系統(tǒng)驅(qū)動(dòng)在我們的驅(qū)動(dòng)之上。在實(shí)際情況下,WDM驅(qū)動(dòng)將嚴(yán)格限制在其Dispatch例程中存儲(chǔ)用戶Buffer。而KMDF驅(qū)動(dòng)則需要在EvtIoInCallerContext事件回調(diào)函數(shù)中使用。?
?
另外一個(gè)重要的固有限制就是使用METHOD_NEITHER方式的驅(qū)動(dòng)要存取用戶Buffer必須在PASSIVE_LEVEL的IRQL級別。這是因?yàn)?IO Manager沒有把Buffer鎖在內(nèi)存中,因此驅(qū)動(dòng)程序想要存取共享Buffer時(shí),內(nèi)存可能被換出去了。如果驅(qū)動(dòng)不能滿足這個(gè)要求,就需要驅(qū)動(dòng)創(chuàng)建一個(gè)mdl,然后將其共享Buffer鎖進(jìn)到內(nèi)存中。?
?
(PS: METHOD_NEITHER不建議使用,還是使用直接IO好。)?
?
另外,考慮到傳輸類型的選擇,對于這種方式可能的非直接明顯的限制是對于共享的內(nèi)存必須被用戶模式應(yīng)用程序分配。如果考慮到配額限制,能夠被分配的內(nèi)存數(shù)量是有限的。另外,用戶應(yīng)用程序不能分配物理連續(xù)的內(nèi)存和Non-cache內(nèi)存。當(dāng)然,如果驅(qū)動(dòng)和用戶模式應(yīng)用所有要做的就是使用合理大小的數(shù)據(jù) Buffer將數(shù)據(jù)傳入和傳出,這個(gè)技術(shù)可能是最簡單和實(shí)用的。?
?
和它的簡易一樣,使用IOCTL在驅(qū)動(dòng)和用戶模式應(yīng)用之間共享內(nèi)存的也是最常被誤解的方案。一個(gè)使用這種方案的新Windows驅(qū)動(dòng)開發(fā)者常犯的錯(cuò)誤就是當(dāng)驅(qū)動(dòng)已經(jīng)查詢到了Buffer的地址后就通知結(jié)束IOCTL。這是一個(gè)非常壞的事情。為什么?如果應(yīng)用程序突然退出了,比如有一個(gè)意味,會(huì)發(fā)生什么情況。另外一個(gè)問題就是當(dāng)使用METHOD_XXX_DIRECT,如果帶有MDL的IRP被完成,Buffer將不再被映射到系統(tǒng)內(nèi)核地址空間,一次試圖對以前有效的內(nèi)核虛擬地址空間的存取(MmGetSystemAddressForMdlSafe獲取)將使系統(tǒng)崩潰。這通常要避免。?
?
一個(gè)針對這個(gè)問題的方案是應(yīng)用程序使用FILE_FLAG_OVERLAPPED打開設(shè)備并且考慮IOCTL使用一個(gè)OVERLAPPED結(jié)構(gòu)。一個(gè)驅(qū)動(dòng)可以針對IRP設(shè)置cancel例程(使用IoSetCancelRoutine),將IRP標(biāo)記為掛起(使用IoMakeIrpPending),并且返回給調(diào)用者STATUS_PENGDING前將IRP放進(jìn)內(nèi)部隊(duì)列。當(dāng)然,KMDF驅(qū)動(dòng)對這類問題可以放心,只需要將請求設(shè)置為進(jìn)行中并且可取消,就像 WDFQUEUE。?
?
(PS: 要小心使用MDL,防止應(yīng)用層程序意外退出而造成MDL所描述的虛擬內(nèi)存無效。)?
?
使用這種方法有兩個(gè)優(yōu)點(diǎn):?
?
1、當(dāng)應(yīng)用程序從IOCTL調(diào)用中得到ERROR_IO_PENDING的返回結(jié)果時(shí),知道Buffer被映射了。并且知道什么時(shí)候IOCTL最終完成并將Buffer取消映射。?
?
2、通過取消例程(WDM)或者一個(gè)EvtIoCancelOnQueue事件處理回調(diào)例程,驅(qū)動(dòng)程序成功在應(yīng)用程序退出或者取消IO命令時(shí)得到通知,所以它可以執(zhí)行必要的操作來完成IOCTL。因而有MDL位置用于內(nèi)存取消映射操作。
分配并且映射頁:
?
?
現(xiàn)在剩下了前面提到的第二種方法:分配內(nèi)存頁并且映射這些頁到特定進(jìn)程的用戶虛擬地址空間上。使用大多數(shù)Windows驅(qū)動(dòng)編寫者常見的API,這個(gè)方法令人驚訝的容易,同時(shí)也允許驅(qū)動(dòng)對分配內(nèi)存的類型具有最大的控制能力。?
?
驅(qū)動(dòng)無論使用什么標(biāo)準(zhǔn)方法,都是希望分配內(nèi)存來共享。例如,如果驅(qū)動(dòng)需要一個(gè)適當(dāng)?shù)脑O(shè)備(邏輯)地址作DMA,就像內(nèi)存塊的內(nèi)核虛擬地址,它能夠使用 AllocateCommonBuffer來分配內(nèi)存。如果沒有要求特定的內(nèi)存特性,要被共享的內(nèi)存大小也是適度的,驅(qū)動(dòng)可以將0填充、非分頁物理內(nèi)存頁分配給Buffer。?
?
從主內(nèi)存分配0填充、非分頁的頁面,使用MmAllocatePagesForMDL或者M(jìn)mAllocatePagesForMdlEx。這些函數(shù)返回一個(gè)MDL描述內(nèi)存的分配。驅(qū)動(dòng)使用函數(shù)MmGetSystemAddressForMdlSafe映射MDL描述的頁到內(nèi)核虛擬地址空間。從主內(nèi)存分配頁比使用分頁內(nèi)存池或者非分頁內(nèi)存池得到的內(nèi)存更加安全,后者不是一個(gè)好主意。?
?
PS:這種方式是內(nèi)核來分配內(nèi)存空間,但是是使用MmAllocatePagesForMDL從主內(nèi)存池中分配,返回得到一個(gè)MDL,對于驅(qū)動(dòng)如何使用該共享內(nèi)存,采用MmGetSystemAddressForMdlSafe得到其內(nèi)核地址。對于應(yīng)用層使用該共享內(nèi)存,采用 MmMapLockedPagesSpecifyCache映射到應(yīng)用層進(jìn)程地址空間中,返回用戶層地址空間的起始地址,將其放在IOCTL中返回給用戶應(yīng)用程序。?
?
借助一個(gè)用來描述共享內(nèi)存的MDL,驅(qū)動(dòng)現(xiàn)在準(zhǔn)備映射這些頁到用戶進(jìn)程地址空間。這可以使用函數(shù)MmMapLockedPagesSpecifyCache來實(shí)現(xiàn)。你需要知道調(diào)用這個(gè)函數(shù)的竅門是:?
你必須在你希望映射Buffer的進(jìn)程上下文中調(diào)用這個(gè)函數(shù)。?
?
PS:如果是在別的進(jìn)程上下文中調(diào)用,就變成了映射到其他進(jìn)程上下文中了,但是我如何保證在我希望映射Buffer的進(jìn)程上下文調(diào)用呢??
?
設(shè)定AccessMode參數(shù)為UserMode。對MmMapLockedPagesSpecifyCache函數(shù)調(diào)用返回值是MDL描述內(nèi)存頁映射的用戶虛擬地址空間地址。驅(qū)動(dòng)可以將其放在對應(yīng)IOCTL的緩存中給用戶應(yīng)用程序 。
?
你需要有一個(gè)方法,在不需要時(shí)將分配的內(nèi)存清除掉。換句話說,你需要調(diào)用MmFreePageFromMdl來釋放內(nèi)存頁。并且調(diào)用IoFreeMdl來釋放由MmAllocatePageForMdl(Ex)創(chuàng)建的MDL。你幾乎都是在你驅(qū)動(dòng)的IRP_MJ_CLEANUP處理例程(WDM)或者 EvtFileCleanup事件處理回調(diào)(KMDF中作這個(gè)工作)。
?
這是所要做的,綜合起來,完成這個(gè)過程的代碼見下面。
?
PVOID CreateAndMapMemory(OUT PMDL* PMemMdl,OUT PVOID* UserVa){PMDL Mdl;PVOID UserVAToReturn;PHYSICAL_ADDRESS LowAddress;PHYSICAL_ADDRESS HighAddress;SIZE_T TotalBytes;// 初始化MmAllocatePagesForMdl需要的Physical Address LowAddress.QuadPart = 0;MAX_MEM(HighAddress.QuardPart);TotalBytes.QuadPart = PAGE_SIZE;// 分配4K的共享緩沖區(qū) Mdl = MmAllocatePagesForMdl(LowAddress,HighAddress,LowAddress,TotalBytes);if(!Mdl){Return STATUS_INSUFFICIENT_RESOURCES;}// 映射共享緩沖區(qū)到用戶地址空間 UserVAToReturn = MmMapLockedPagesSpecifyCache(Mdl,UserMode,MmCached,NULL,FALSE,NormalPagePriority);if(!UserVAToReturn){MmFreePagesFromMdl(Mdl);IoFreeMdl(Mdl);Return STATUS_INSUFFICIENT_RESOURCE;}// 返回,得到MDL和用戶層的虛擬地址*UserVa = UserVAToReturn;*PMemMdl = Mdl;return STATUS_SUCCESS;}?
?
當(dāng)然,這種方法也有缺點(diǎn),調(diào)用MmMapLockedPagesSpecifyCache必須在你希望內(nèi)存頁被映射的進(jìn)程上下文來做。較之使用 METHOD_NEITHER的IOCTL方法,該方法表現(xiàn)出不必其更多的靈活性。然而,不像前者,后者只需一個(gè)函數(shù)(MmMapLockerPagesSpecifyCache)在目標(biāo)上下文被調(diào)用。由于很多OEM設(shè)備驅(qū)動(dòng)在設(shè)備棧中只有一個(gè)且直接基于總線的(也就是在其上沒有別的設(shè)備,除了總線驅(qū)動(dòng)其下沒有別的驅(qū)動(dòng)),這個(gè)條件很容易滿足。對于那些少量的設(shè)備驅(qū)動(dòng),處于設(shè)備棧的深處并且需要和用戶模式應(yīng)用直接共享 Buffer的,一個(gè)企業(yè)級的驅(qū)動(dòng)編寫者可能能找到一個(gè)安全的地方在請求的進(jìn)程上下文中調(diào)用。
?
在頁面被映射以后,共享內(nèi)存就可以象使用METHOD_XXX_DIRECT的IOCTL方法一樣,能夠在任意的進(jìn)程上下文被存取,也可以在高IRQL上存取(因?yàn)楣蚕韮?nèi)存來之非分頁內(nèi)存)。
?
PS:需要我們確定的一點(diǎn)就是何時(shí)調(diào)用MmMapLockedPagesSpecifyCache安全的映射到指定進(jìn)程的上下文中。還有一點(diǎn),就是該共享內(nèi)存處于非分頁內(nèi)存中,所以可以在搞IRQL上存取。
?
?
如果你使用這種方法,有一個(gè)決定性的事情一直要記者:你必須確信你的驅(qū)動(dòng)要提供方法,在任何時(shí)候用戶進(jìn)程退出的時(shí)候,能夠?qū)⒛阌成涞接脩艨臻g的頁面作取消映射的操作。這件事情的失敗會(huì)導(dǎo)致系統(tǒng)在應(yīng)用層退出的時(shí)候崩潰。我們找到一個(gè)簡單方法就是無論何時(shí)應(yīng)用層關(guān)閉設(shè)備句柄,則對這些頁面作取消映射操作。由于應(yīng)用層關(guān)閉句柄,出現(xiàn)意外或者其他情況,驅(qū)動(dòng)將收到對應(yīng)于該應(yīng)用層打開的設(shè)備文件對象的一個(gè)IRP_MJ_CLEANUP,你可以確信這是工作的。你將在 CLEANUP使執(zhí)行這些操作,而不是CLOSE,因?yàn)槟憧梢员WC在請求線程的上下文中得到Cleanup IRP。下面代碼可以看見分配資源的釋放。
?
VOID UnMapAndFreeMemory(PMDL PMdl,PVOID UserVa){if(!PMdl){ return ;}// 解除映射 MmUnMapLockerPages(UserVa,PMdl);// 釋放MDL鎖定的物理頁 MmFreePagesFromMdl(PMdl);// 釋放MDL IoFreeMdl(PMdl);}?
其他挑戰(zhàn):
?
無論使用哪種機(jī)制,驅(qū)動(dòng)和應(yīng)用程序?qū)⑿枰С滞酱嫒」蚕韮?nèi)存的通用方式,這可以通過很多許多方法來做。可能最簡單的機(jī)制是共享一個(gè)或者多個(gè)命名事件。應(yīng)用和驅(qū)動(dòng)共享事件的最簡單方法就是應(yīng)用層生成事件,然后將事件句柄傳遞給驅(qū)動(dòng)層。驅(qū)動(dòng)然后從應(yīng)用層的上下文中Reference事件句柄。如果你使用這種方法,請不要忘記在驅(qū)動(dòng)的Cleanup處理代碼中Dereference這個(gè)句柄。
?
PS:一定要注意解引用來自應(yīng)用層的事件對象。
?
總結(jié):
?
我們觀察了兩種在驅(qū)動(dòng)和用戶模式應(yīng)用程序共享內(nèi)存的方法:
1、用戶層創(chuàng)建緩沖區(qū)并且通過IOCTL傳遞給驅(qū)動(dòng)
2、在驅(qū)動(dòng)中使用MmAllocatePagesForMdl分配內(nèi)存頁,得到MDL,然后將該MDL所描述的內(nèi)存映射到用戶層地址空間(MmMapLockedPagesSpecifyCache)。得到用戶地址空間的起始地址,并通過IOCTL返回給用戶層。
?
譯者注:
?
在使用命名事件來同步驅(qū)動(dòng)和應(yīng)用程序共享緩沖區(qū)時(shí),一般不要使用驅(qū)動(dòng)程序創(chuàng)建命名事件,然后根據(jù)應(yīng)用程序名稱打開的方法。這種方法雖然可以使得驅(qū)動(dòng)激活事件后,所有相關(guān)應(yīng)用程序都能夠被喚醒,方便程序的開發(fā),但是他有兩個(gè)問題:一是命名事件只有在WIN32子系統(tǒng)起來后才能正確創(chuàng)建,這會(huì)影響到驅(qū)動(dòng)程序開發(fā)。最嚴(yán)重的問題是在驅(qū)動(dòng)中創(chuàng)建的事件其存取權(quán)限要求比較高,在WinXP下要求具有Administrator組權(quán)限的用戶創(chuàng)建的應(yīng)用程序才能夠存取該事件。在Vista系統(tǒng)下由于安全功能的強(qiáng)化,這方面的問題更加嚴(yán)重。因此盡量使用應(yīng)用程序創(chuàng)建的事件,或者通過其他同步方式
posted on 2013-08-15 20:11 RodYang 閱讀(...) 評論(...) 編輯 收藏轉(zhuǎn)載于:https://www.cnblogs.com/RodYang/p/3260757.html
總結(jié)
以上是生活随笔為你收集整理的在驱动和应用程序间共享内存的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows环境下文件的彻底删除与恢复
- 下一篇: 多校7