<p>在不一樣的場合,不少驅動編寫人員須要在驅動和用戶程序間共享內存。兩種最容易的技術是:</p> <p>l 應用程序發送IOCTL給驅動程序,提供一個指向內存的指針,以後驅動程序和應用程序就能夠共享內存。(應用程序分配共享內存)</p> <p>l 由驅動程序分配內存頁,並映射這些內存頁到指定用戶模式進程的地址空間,而且將地址返回給應用程序。(驅動程序分配共享內存)</p> <p>使用IOCTL共享Buffer:</p> <p>使用一個IOCT描述的Buffer,在驅動和用戶程序間共享內存是內存共享最簡單的實現形式。畢竟,IOCTL也是驅動支持其餘I/O請求最經典的方法。應用程序調用Win32函數DeviceIoControl(),要被共享的Buffer的基地址和長度被放入OutBuffer參數中。對於使用這種Buffer共享方式的驅動編寫者須要肯定的事情就是對於特定的IOCTL採起哪一種Buffer method。既可使用METHOD_XXX_DIRECT,也可使用METHOD_NEITHER。</p> <p>(PS:在METHOD_XXX_DIRECT模式下,IO管理器爲應用層指定的輸出緩衝區(OutputBuffer)建立一個MDL鎖住該應用層的緩衝區內存,而後,咱們能夠在內核層中使用MmGetSystemAddressForMdlSafe得到應用層輸出緩衝區所對應的內核層地址。MDL地址被放在了Irp->MdlAddress中)。</p> <p>若是採用METHOD_XXX_DIRECT方式,那用戶Buffer將被檢查是否正確存取,檢查事後用戶Buffer將被鎖進內存。驅動須要調用MmGetSystemAddressForMdlSafe將前述Buffer映射到內核地址空間。這種方式的一個優勢就是驅動能夠在任意進程上下文、任意IRQL優先級別上存取共享內存Buffer。若是隻須要將數據傳給驅動則使用METHOD_IN_DIRECT方式。若是從驅動返回數據給應用程序或者作雙向數據交換則使用METHOD_OUT_BUFFER。</p> <p>(PS:這些檢查都將由IO管理器來負責,而且,此時IO管理器將爲用戶層的緩衝區建立MDL。由於此時還未到設備驅動層,當前上下文還屬於當前發起 DeviceIo調用的進程,用戶模式緩衝區的內存有效。可是,通過IO管理器發送IRP到下層驅動時,就不能保證當前上下文,幸而有IO管理器爲咱們建立MDL。這樣,咱們就能夠在內核層得到對應的內核地址,而且自由寫入數據。)</p> <p>使用METHOD_NEITHER方式描述一個共享內存Buffer存在許多固有的限制和須要當心的地方。(基本上,在任什麼時候候一個驅動使用這種方式都是同樣的)。其中最主要的規則是驅動只能在發起請求進程的上下文中存取Buffer。這是由於要經過Buffer的用戶虛擬地址存取共享內存Buffer。這也就意味着驅動必需要在設備棧的頂端,被用戶應用程序經由IO Manager直接調用。期間不能存在中間層驅動或者文件系統驅動在咱們的驅動之上。在實際狀況下,WDM驅動將嚴格限制在其Dispatch例程中存儲用戶Buffer。而KMDF驅動則須要在EvtIoInCallerContext事件回調函數中使用。</p> <p>另一個重要的固有限制就是使用METHOD_NEITHER方式的驅動要存取用戶Buffer必須在PASSIVE_LEVEL的IRQL級別。這是由於 IO Manager沒有把Buffer鎖在內存中,所以驅動程序想要存取共享Buffer時,內存可能被換出去了。若是驅動不能知足這個要求,就須要驅動建立一個mdl,而後將其共享Buffer鎖進到內存中。</p> <p>(PS: METHOD_NEITHER不建議使用,仍是使用直接IO好。)</p> <p>另外,考慮到傳輸類型的選擇,對於這種方式可能的非直接明顯的限制是對於共享的內存必須被用戶模式應用程序分配。若是考慮到配額限制,可以被分配的內存數量是有限的。另外,用戶應用程序不能分配物理連續的內存和Non-cache內存。固然,若是驅動和用戶模式應用全部要作的就是使用合理大小的數據 Buffer將數據傳入和傳出,這個技術多是最簡單和實用的。</p> <p>和它的簡易同樣,使用IOCTL在驅動和用戶模式應用之間共享內存的也是最常被誤解的方案。一個使用這種方案的新Windows驅動開發者常犯的錯誤就是當驅動已經查詢到了Buffer的地址後就通知結束IOCTL。這是一個很是壞的事情。爲何?若是應用程序忽然退出了,好比有一個意味,會發生什麼狀況。另一個問題就是當使用METHOD_XXX_DIRECT,若是帶有MDL的IRP被完成,Buffer將再也不被映射到系統內核地址空間,一次試圖對之前有效的內核虛擬地址空間的存取(MmGetSystemAddressForMdlSafe獲取)將使系統崩潰。這一般要避免。</p> <p>一個針對這個問題的方案是應用程序使用FILE_FLAG_OVERLAPPED打開設備而且考慮IOCTL使用一個OVERLAPPED結構。一個驅動能夠針對IRP設置cancel例程(使用IoSetCancelRoutine),將IRP標記爲掛起(使用IoMakeIrpPending),而且返回給調用者STATUS_PENGDING前將IRP放進內部隊列。固然,KMDF驅動對這類問題能夠放心,只須要將請求設置爲進行中而且可取消,就像 WDFQUEUE。</p> <p>(PS: 要當心使用MDL,防止應用層程序意外退出而形成MDL所描述的虛擬內存無效。)</p> <p>使用這種方法有兩個優勢:</p> <p>一、當應用程序從IOCTL調用中獲得ERROR_IO_PENDING的返回結果時,知道Buffer被映射了。而且知道何時IOCTL最終完成並將Buffer取消映射。</p> <p>二、經過取消例程(WDM)或者一個EvtIoCancelOnQueue事件處理回調例程,驅動程序成功在應用程序退出或者取消IO命令時獲得通知,因此它能夠執行必要的操做來完成IOCTL。於是有MDL位置用於內存取消映射操做。</p> <p>分配而且映射頁:</p> <p>如今剩下了前面提到的第二種方法:分配內存頁而且映射這些頁到特定進程的用戶虛擬地址空間上。使用大多數Windows驅動編寫者常見的API,這個方法使人驚訝的容易,同時也容許驅動對分配內存的類型具備最大的控制能力。</p> <p>驅動不管使用什麼標準方法,都是但願分配內存來共享。例如,若是驅動須要一個適當的設備(邏輯)地址做DMA,就像內存塊的內核虛擬地址,它可以使用 AllocateCommonBuffer來分配內存。若是沒有要求特定的內存特性,要被共享的內存大小也是適度的,驅動能夠將0填充、非分頁物理內存頁分配給Buffer。</p> <p>從主內存分配0填充、非分頁的頁面,使用MmAllocatePagesForMDL或者MmAllocatePagesForMdlEx。這些函數返回一個MDL描述內存的分配。驅動使用函數MmGetSystemAddressForMdlSafe映射MDL描述的頁到內核虛擬地址空間。從主內存分配頁比使用分頁內存池或者非分頁內存池獲得的內存更加安全,後者不是一個好主意。</p> <p>PS:這種方式是內核來分配內存空間,可是是使用MmAllocatePagesForMDL從主內存池中分配,返回獲得一個MDL,對於驅動如何使用該共享內存,採用MmGetSystemAddressForMdlSafe獲得其內核地址。對於應用層使用該共享內存,採用 MmMapLockedPagesSpecifyCache映射到應用層進程地址空間中,返回用戶層地址空間的起始地址,將其放在IOCTL中返回給用戶應用程序。</p> <p>藉助一個用來描述共享內存的MDL,驅動如今準備映射這些頁到用戶進程地址空間。這可使用函數MmMapLockedPagesSpecifyCache來實現。你須要知道調用這個函數的竅門是:</p> <p>你必須在你但願映射Buffer的進程上下文中調用這個函數。</p> <p>PS:若是是在別的進程上下文中調用,就變成了映射到其餘進程上下文中了,可是我如何保證在我但願映射Buffer的進程上下文調用呢?</p> <p>設定AccessMode參數爲UserMode。對MmMapLockedPagesSpecifyCache函數調用返回值是MDL描述內存頁映射的用戶虛擬地址空間地址。驅動能夠將其放在對應IOCTL的緩存中給用戶應用程序 。</p> <p>你須要有一個方法,在不須要時將分配的內存清除掉。換句話說,你須要調用MmFreePageFromMdl來釋放內存頁。而且調用IoFreeMdl來釋放由MmAllocatePageForMdl(Ex)建立的MDL。你幾乎都是在你驅動的IRP_MJ_CLEANUP處理例程(WDM)或者 EvtFileCleanup事件處理回調(KMDF中做這個工做)。</p> <p>這是所要作的,綜合起來,完成這個過程的代碼見下面。</p> <div id="scid:9D7513F9-C04C-4721-824A-2B34F0212519:3c8500d5-d6c2-48ac-82a8-27f784a03119" class="wlWriterEditableSmartContent" style="float: none; padding-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; display: inline; padding-right: 0px"><pre class="brush: cpp; gutter: true; first-line: 1; tab-size: 4; toolbar: false; width: 497px; height: 410px;" style=" width: 497px; height: 410px;overflow: auto;">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的共享緩衝區 Mdl = MmAllocatePagesForMdl(LowAddress, HighAddress, LowAddress, TotalBytes); if(!Mdl) { Return STATUS_INSUFFICIENT_RESOURCES; } // 映射共享緩衝區到用戶地址空間 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;
}spa
</pre><!-- Code inserted with Steve Dunn's Windows Live Writer Code Formatter Plugin. http://dunnhq.com --></div>線程
<p>固然,這種方法也有缺點,調用MmMapLockedPagesSpecifyCache必須在你但願內存頁被映射的進程上下文來作。較之使用 METHOD_NEITHER的IOCTL方法,該方法表現出沒必要其更多的靈活性。然而,不像前者,後者只需一個函數(MmMapLockerPagesSpecifyCache)在目標上下文被調用。因爲不少OEM設備驅動在設備棧中只有一個且直接基於總線的(也就是在其上沒有別的設備,除了總線驅動其下沒有別的驅動),這個條件很容易知足。對於那些少許的設備驅動,處於設備棧的深處而且須要和用戶模式應用直接共享 Buffer的,一個企業級的驅動編寫者可能能找到一個安全的地方在請求的進程上下文中調用。</p>指針
<p>在頁面被映射之後,共享內存就能夠象使用METHOD_XXX_DIRECT的IOCTL方法同樣,可以在任意的進程上下文被存取,也能夠在高IRQL上存取(由於共享內存來之非分頁內存)。</p>code
<p>PS:須要咱們肯定的一點就是什麼時候調用MmMapLockedPagesSpecifyCache安全的映射到指定進程的上下文中。還有一點,就是該共享內存處於非分頁內存中,因此能夠在搞IRQL上存取。</p>orm
<p>若是你使用這種方法,有一個決定性的事情一直要記者:你必須確信你的驅動要提供方法,在任什麼時候候用戶進程退出的時候,可以將你映射到用戶空間的頁面做取消映射的操做。這件事情的失敗會致使系統在應用層退出的時候崩潰。咱們找到一個簡單方法就是不管什麼時候應用層關閉設備句柄,則對這些頁面做取消映射操做。因爲應用層關閉句柄,出現意外或者其餘狀況,驅動將收到對應於該應用層打開的設備文件對象的一個IRP_MJ_CLEANUP,你能夠確信這是工做的。你將在 CLEANUP使執行這些操做,而不是CLOSE,由於你能夠保證在請求線程的上下文中獲得Cleanup IRP。下面代碼能夠看見分配資源的釋放。</p>對象
<div id="scid:9D7513F9-C04C-4721-824A-2B34F0212519:898ad414-41d5-4703-8b70-801b06b0f32a" class="wlWriterEditableSmartContent" style="float: none; padding-bottom: 0px; padding-top: 0px; padding-left: 0px; margin: 0px; display: inline; padding-right: 0px"><pre class="brush: cpp; gutter: true; first-line: 1; tab-size: 4; toolbar: false; width: 497px; height: 410px;" style=" width: 497px; height: 410px;overflow: auto;">VOID UnMapAndFreeMemory(PMDL PMdl,PVOID UserVa)隊列
{
if(!PMdl) { return ;} // 解除映射 MmUnMapLockerPages(UserVa,PMdl); // 釋放MDL鎖定的物理頁 MmFreePagesFromMdl(PMdl); // 釋放MDL IoFreeMdl(PMdl);
}
</pre><!-- Code inserted with Steve Dunn's Windows Live Writer Code Formatter Plugin. http://dunnhq.com --></div>
<p>其餘挑戰:</p>
<p>不管使用哪一種機制,驅動和應用程序將須要支持同步存取共享內存的通用方式,這能夠經過不少許多方法來作。可能最簡單的機制是共享一個或者多個命名事件。應用和驅動共享事件的最簡單方法就是應用層生成事件,而後將事件句柄傳遞給驅動層。驅動而後從應用層的上下文中Reference事件句柄。若是你使用這種方法,請不要忘記在驅動的Cleanup處理代碼中Dereference這個句柄。</p>
<p>PS:必定要注意解引用來自應用層的事件對象。</p>
<p>總結:</p>
<p>咱們觀察了兩種在驅動和用戶模式應用程序共享內存的方法:</p>
<p>一、用戶層建立緩衝區而且經過IOCTL傳遞給驅動</p>
<p>二、在驅動中使用MmAllocatePagesForMdl分配內存頁,獲得MDL,而後將該MDL所描述的內存映射到用戶層地址空間(MmMapLockedPagesSpecifyCache)。獲得用戶地址空間的起始地址,並經過IOCTL返回給用戶層。</p>
<p>譯者注:</p>
<p>在使用命名事件來同步驅動和應用程序共享緩衝區時,通常不要使用驅動程序建立命名事件,而後根據應用程序名稱打開的方法。這種方法雖然可使得驅動激活事件後,全部相關應用程序都可以被喚醒,方便程序的開發,可是他有兩個問題:一是命名事件只有在WIN32子系統起來後才能正確建立,這會影響到驅動程序開發。最嚴重的問題是在驅動中建立的事件其存取權限要求比較高,在WinXP下要求具備Administrator組權限的用戶建立的應用程序纔可以存取該事件。在Vista系統下因爲安全功能的強化,這方面的問題更加嚴重。所以儘可能使用應用程序建立的事件,或者經過其餘同步方式</p>