閱讀286 返回首頁    go 阿裏雲 go 技術社區[雲棲]


在驅動和應用程序間共享內存

在不同的場合,很多驅動編寫人員需要在驅動和用戶程序間共享內存。兩種最容易的技術是: 

 

l 應用程序發送IOCTL給驅動程序,提供一個指向內存的指針,之後驅動程序和應用程序就可以共享內存。(應用程序分配共享內存 

 

l 由驅動程序分配內存頁,並映射這些內存頁到指定用戶模式進程的地址空間,並且將地址返回給應用程序。(驅動程序分配共享內存 

 

使用IOCTL共享Buffer: 

 

使用一個IOCT描述的Buffer,在驅動和用戶程序間共享內存是內存共享最簡單的實現形式。畢竟,IOCTL也是驅動支持其他I/O請求最經典的方法。應用程序調用Win32函數DeviceIoControl(),要被共享的Buffer的基地址和長度被放入OutBuffer參數中。對於使用這種Buffer共享方式的驅動編寫者需要確定的事情就是對於特定的IOCTL采取哪種Buffer method。既可以使用METHOD_XXX_DIRECT,也可以使用METHOD_NEITHER。 

 

(PS:在METHOD_XXX_DIRECT模式下,IO管理器為應用層指定的輸出緩衝區(OutputBuffer)創建一個MDL鎖住該應用層的緩衝區內存,然後,我們可以在內核層中使用MmGetSystemAddressForMdlSafe獲得應用層輸出緩衝區所對應的內核層地址。MDL地址被放在了Irp->MdlAddress中)。 

 

如果采用METHOD_XXX_DIRECT方式,那用戶Buffer將被檢查是否正確存取,檢查過後用戶Buffer將被鎖進內存。驅動需要調用MmGetSystemAddressForMdlSafe將前述Buffer映射到內核地址空間。這種方式的一個優點就是驅動可以在任意進程上下文、任意IRQL優先級別上存取共享內存Buffer。如果隻需要將數據傳給驅動則使用METHOD_IN_DIRECT方式。如果從驅動返回數據給應用程序或者做雙向數據交換則使用METHOD_OUT_BUFFER。 

 

(PS:這些檢查都將由IO管理器來負責,並且,此時IO管理器將為用戶層的緩衝區創建MDL。因為此時還未到設備驅動層,當前上下文還屬於當前發起 DeviceIo調用的進程,用戶模式緩衝區的內存有效。但是,經過IO管理器發送IRP到下層驅動時,就不能保證當前上下文,幸而有IO管理器為我們創建MDL。這樣,我們就可以在內核層獲得對應的內核地址,並且自由寫入數據。) 

 

使用METHOD_NEITHER方式描述一個共享內存Buffer存在許多固有的限製和需要小心的地方。(基本上,在任何時候一個驅動使用這種方式都是一樣的)。其中最主要的規則是驅動隻能在發起請求進程的上下文中存取Buffer。這是因為要通過Buffer的用戶虛擬地址存取共享內存Buffer。這也就意味著驅動必須要在設備棧的頂端,被用戶應用程序經由IO Manager直接調用。期間不能存在中間層驅動或者文件係統驅動在我們的驅動之上。在實際情況下,WDM驅動將嚴格限製在其Dispatch例程中存儲用戶Buffer。而KMDF驅動則需要在EvtIoInCallerContext事件回調函數中使用。 

 

另外一個重要的固有限製就是使用METHOD_NEITHER方式的驅動要存取用戶Buffer必須在PASSIVE_LEVEL的IRQL級別。這是因為 IO Manager沒有把Buffer鎖在內存中,因此驅動程序想要存取共享Buffer時,內存可能被換出去了。如果驅動不能滿足這個要求,就需要驅動創建一個mdl,然後將其共享Buffer鎖進到內存中。 

 

(PS: METHOD_NEITHER不建議使用,還是使用直接IO好。) 

 

另外,考慮到傳輸類型的選擇,對於這種方式可能的非直接明顯的限製是對於共享的內存必須被用戶模式應用程序分配。如果考慮到配額限製,能夠被分配的內存數量是有限的。另外,用戶應用程序不能分配物理連續的內存和Non-cache內存。當然,如果驅動和用戶模式應用所有要做的就是使用合理大小的數據 Buffer將數據傳入和傳出,這個技術可能是最簡單和實用的。 

 

和它的簡易一樣,使用IOCTL在驅動和用戶模式應用之間共享內存的也是最常被誤解的方案。一個使用這種方案的新Windows驅動開發者常犯的錯誤就是當驅動已經查詢到了Buffer的地址後就通知結束IOCTL。這是一個非常壞的事情。為什麼?如果應用程序突然退出了,比如有一個意味,會發生什麼情況。另外一個問題就是當使用METHOD_XXX_DIRECT,如果帶有MDL的IRP被完成,Buffer將不再被映射到係統內核地址空間,一次試圖對以前有效的內核虛擬地址空間的存取(MmGetSystemAddressForMdlSafe獲取)將使係統崩潰。這通常要避免。 

 

一個針對這個問題的方案是應用程序使用FILE_FLAG_OVERLAPPED打開設備並且考慮IOCTL使用一個OVERLAPPED結構。一個驅動可以針對IRP設置cancel例程(使用IoSetCancelRoutine),將IRP標記為掛起(使用IoMakeIrpPending),並且返回給調用者STATUS_PENGDING前將IRP放進內部隊列。當然,KMDF驅動對這類問題可以放心,隻需要將請求設置為進行中並且可取消,就像 WDFQUEUE。 

 

(PS: 要小心使用MDL,防止應用層程序意外退出而造成MDL所描述的虛擬內存無效。) 

 

使用這種方法有兩個優點: 

 

1、當應用程序從IOCTL調用中得到ERROR_IO_PENDING的返回結果時,知道Buffer被映射了。並且知道什麼時候IOCTL最終完成並將Buffer取消映射。 

 

2、通過取消例程(WDM)或者一個EvtIoCancelOnQueue事件處理回調例程,驅動程序成功在應用程序退出或者取消IO命令時得到通知,所以它可以執行必要的操作來完成IOCTL。因而有MDL位置用於內存取消映射操作。

分配並且映射頁:

 

 

現在剩下了前麵提到的第二種方法:分配內存頁並且映射這些頁到特定進程的用戶虛擬地址空間上。使用大多數Windows驅動編寫者常見的API,這個方法令人驚訝的容易,同時也允許驅動對分配內存的類型具有最大的控製能力。 

 

驅動無論使用什麼標準方法,都是希望分配內存來共享。例如,如果驅動需要一個適當的設備(邏輯)地址作DMA,就像內存塊的內核虛擬地址,它能夠使用 AllocateCommonBuffer來分配內存。如果沒有要求特定的內存特性,要被共享的內存大小也是適度的,驅動可以將0填充、非分頁物理內存頁分配給Buffer。 

 

從主內存分配0填充、非分頁的頁麵,使用MmAllocatePagesForMDL或者MmAllocatePagesForMdlEx。這些函數返回一個MDL描述內存的分配。驅動使用函數MmGetSystemAddressForMdlSafe映射MDL描述的頁到內核虛擬地址空間。從主內存分配頁比使用分頁內存池或者非分頁內存池得到的內存更加安全,後者不是一個好主意。 

 

PS:這種方式是內核來分配內存空間,但是是使用MmAllocatePagesForMDL從主內存池中分配,返回得到一個MDL,對於驅動如何使用該共享內存,采用MmGetSystemAddressForMdlSafe得到其內核地址。對於應用層使用該共享內存,采用 MmMapLockedPagesSpecifyCache映射到應用層進程地址空間中,返回用戶層地址空間的起始地址,將其放在IOCTL中返回給用戶應用程序。 

 

借助一個用來描述共享內存的MDL,驅動現在準備映射這些頁到用戶進程地址空間。這可以使用函數MmMapLockedPagesSpecifyCache來實現。你需要知道調用這個函數的竅門是: 

你必須在你希望映射Buffer的進程上下文中調用這個函數。 

 

PS:如果是在別的進程上下文中調用,就變成了映射到其他進程上下文中了,但是我如何保證在我希望映射Buffer的進程上下文調用呢? 

 

設定AccessMode參數為UserMode。對MmMapLockedPagesSpecifyCache函數調用返回值是MDL描述內存頁映射的用戶虛擬地址空間地址。驅動可以將其放在對應IOCTL的緩存中給用戶應用程序 。

 

你需要有一個方法,在不需要時將分配的內存清除掉。換句話說,你需要調用MmFreePageFromMdl來釋放內存頁。並且調用IoFreeMdl來釋放由MmAllocatePageForMdl(Ex)創建的MDL。你幾乎都是在你驅動的IRP_MJ_CLEANUP處理例程(WDM)或者 EvtFileCleanup事件處理回調(KMDF中作這個工作)。

 

這是所要做的,綜合起來,完成這個過程的代碼見下麵。

 

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;

}

 

當然,這種方法也有缺點,調用MmMapLockedPagesSpecifyCache必須在你希望內存頁被映射的進程上下文來做。較之使用 METHOD_NEITHER的IOCTL方法,該方法表現出不必其更多的靈活性。然而,不像前者,後者隻需一個函數(MmMapLockerPagesSpecifyCache)在目標上下文被調用。由於很多OEM設備驅動在設備棧中隻有一個且直接基於總線的(也就是在其上沒有別的設備,除了總線驅動其下沒有別的驅動),這個條件很容易滿足。對於那些少量的設備驅動,處於設備棧的深處並且需要和用戶模式應用直接共享 Buffer的,一個企業級的驅動編寫者可能能找到一個安全的地方在請求的進程上下文中調用。

 

在頁麵被映射以後,共享內存就可以象使用METHOD_XXX_DIRECT的IOCTL方法一樣,能夠在任意的進程上下文被存取,也可以在高IRQL上存取(因為共享內存來之非分頁內存)。

 

PS:需要我們確定的一點就是何時調用MmMapLockedPagesSpecifyCache安全的映射到指定進程的上下文中。還有一點,就是該共享內存處於非分頁內存中,所以可以在搞IRQL上存取。

 

 

如果你使用這種方法,有一個決定性的事情一直要記者:你必須確信你的驅動要提供方法,在任何時候用戶進程退出的時候,能夠將你映射到用戶空間的頁麵作取消映射的操作。這件事情的失敗會導致係統在應用層退出的時候崩潰。我們找到一個簡單方法就是無論何時應用層關閉設備句柄,則對這些頁麵作取消映射操作。由於應用層關閉句柄,出現意外或者其他情況,驅動將收到對應於該應用層打開的設備文件對象的一個IRP_MJ_CLEANUP,你可以確信這是工作的。你將在 CLEANUP使執行這些操作,而不是CLOSE,因為你可以保證在請求線程的上下文中得到Cleanup IRP。下麵代碼可以看見分配資源的釋放。

 

VOID UnMapAndFreeMemory(PMDL PMdl,PVOID UserVa)

{

if(!PMdl)

{ return ;}

 

// 解除映射

MmUnMapLockerPages(UserVa,PMdl);

// 釋放MDL鎖定的物理頁

MmFreePagesFromMdl(PMdl);

// 釋放MDL

IoFreeMdl(PMdl);

}

 

其他挑戰:

 

無論使用哪種機製,驅動和應用程序將需要支持同步存取共享內存的通用方式,這可以通過很多許多方法來做。可能最簡單的機製是共享一個或者多個命名事件。應用和驅動共享事件的最簡單方法就是應用層生成事件,然後將事件句柄傳遞給驅動層驅動然後從應用層的上下文中Reference事件句柄如果你使用這種方法,請不要忘記在驅動的Cleanup處理代碼中Dereference這個句柄

 

PS:一定要注意解引用來自應用層的事件對象。

 

總結:

 

我們觀察了兩種在驅動和用戶模式應用程序共享內存的方法:

1、用戶層創建緩衝區並且通過IOCTL傳遞給驅動

2、在驅動中使用MmAllocatePagesForMdl分配內存頁,得到MDL,然後將該MDL所描述的內存映射到用戶層地址空間(MmMapLockedPagesSpecifyCache)。得到用戶地址空間的起始地址,並通過IOCTL返回給用戶層。

 

譯者注:

 

在使用命名事件來同步驅動和應用程序共享緩衝區時,一般不要使用驅動程序創建命名事件,然後根據應用程序名稱打開的方法。這種方法雖然可以使得驅動激活事件後,所有相關應用程序都能夠被喚醒,方便程序的開發,但是他有兩個問題:一是命名事件隻有在WIN32子係統起來後才能正確創建,這會影響到驅動程序開發。最嚴重的問題是在驅動中創建的事件其存取權限要求比較高,在WinXP下要求具有Administrator組權限的用戶創建的應用程序才能夠存取該事件。在Vista係統下由於安全功能的強化,這方麵的問題更加嚴重。因此盡量使用應用程序創建的事件,或者通過其他同步方式

最後更新:2017-04-03 15:21:55

  上一篇:go Ajax基礎 同步請求與異步請求
  下一篇:go 實戰DeviceIoControl 之二:獲取軟盤/硬盤/光盤的參數