歡迎您光臨本站 註冊首頁

談談內存映射文件

←手機掃碼閱讀     火星人 @ 2014-03-03 , reply:0

內存映射文件允許開發人員預訂一塊地址空間並為該區域調撥物理存儲器,與虛擬內存不同的是,內存映射文件的物理存儲器來自磁碟中的文件,而非系統的頁交換文件。將文件映射到內存中后,我們就可以在內存中操作他們了,就像他們被載入內存中一樣。

內存映射文件主要有三方面的用途:

1:系統使用內存映射文件來將exe或是dll文件本身作為後備存儲器,而非系統頁交換文件,這大大節省了系統頁交換空間,由於不需要將exe或是dll文件載入到頁系統交換文件,也提高了啟動速度。

2:使用內存映射文件來將磁碟上的文件映射到進程的空間區域,使得開發人員操作文件就像操作內存數據一樣,將對文件的操作交由操作系統來管理,簡化了開發人員的工作。

3:windows提供了多種進程間通信的方法,但他們都是基於內存映射文件來實現的。


這裡首先討論第一種情況:


      在一個exe文件運行之前,系統首先為新進程創建一個進程內核對象,同時預訂一塊足夠大的地址空間來容納該文件。然後,對該地址空間進行標註,註明他的後備存儲器來自exe文件,而非系統的頁交換文件。此措施對提高系統性能有重大意義。

         一個可執行文件,當他有多個實例同時運行,系統在創建另一個新的實例時,僅僅是打開了另一個內存映射視圖。所有這些視圖都來自於同一個文件映射對象(即可執行文件本身)。

當新實例開始運行時,系統只是把包含應用程序代碼和數據的虛擬內存頁面映射到了他的地址空間中,當其中的一個實例試圖去修改數據段中的數據,如果不採取有效措施,那麼應用程序的所有其他實例的內存都會被修改,這是不合常理的。因此windows採取了一種叫做寫時複製的特性,來防止這種情況的發生。

        系統將可執行文件映射到地址空間中時,會計算有多少頁面是可寫的。(通常包含數據的頁面被標記為PAGE_READWRITE屬性,它們是可寫的)然後會從系統的頁交換文件中調撥物理存儲器,來容納這些可寫的頁面。但是系統只是調撥這些頁面,並不會實際載入頁面的內容,只有當寫入可寫頁面的時候才會真正實際載入。(後面會詳細介紹)

       任何時候當應用程序試圖寫入內存映射文件的時候,系統會截獲此類嘗試,接著從先前在系統頁交換文件中分配的空間中取出一頁,複製要寫入頁面的內容,讓應用程序寫入剛剛從系統頁交換文件中分配的頁,而不是內存映射文件中的頁。由於寫入到的區域僅僅是內存映射文件的副本,不會對內存映射文件寫入,這樣就保證了其他實例不會受到任何影響。另外需要注意的是,內存映射文件的副本(在系統頁交換文件中)被映射到了新實例的地址空間區域的同一位置。

        以上介紹的是在同一個可執行文件的多個實例之間不會共享數據的情況。有時候在多個實例之間共享數據非常有用,可以大大提高編程效率。接著我們就討論如何在一個可執行文件的多個實例中共享數據。



      我們知道默認情況下,我們定義的初始化數據被放到了數據段,未初始化的數據放到了.bss段。除了使用這些標準段之外,我們也可以將數據放在我們自己的段中。

       首先,就要知道如何創建一個段。

       #pragm data_seg("sectionname")//創建一個名為sectionname的段。


      看例子:   #pragm data_seg("newsection")//此處創建一個名為newsection的段

      int a=23;//向此段中添加變數。

     #pragm data_seg()//結束添加此例創建了一個名為newsection的段,並向此段添加int類型變數a。#pragm data_seg()用於結束向段中添加數據。

要注意一點編譯器只會將以初始化的變數放入我們的段中,如上例中的a。

      如果這樣:  #pragm data_seg("newsection")

    int a=23;

    int b;

    #pragm data_seg()b是不會被添加到段newsection中的。而是放到默認的標準段中。

    雖然編譯器只會將初始化的變數放入自定義段中,但是我們可以強制的將一個未初始化的數據放我任何我們想放入的段中。

    _declspec(allocate("newsection") ) int b;將b放入newsection中。

    僅僅新建一個段,並將要共享的數據放入新建段中是不夠的,還需要將該段聲明為共享段。

    我們可以使用:

    1: #pragm comment(linker,"/SECTION:newsection,RWS")

    2:鏈接器開關:/SECTON:newsecton,RWS

   其中R表示READ,W表示WRITE,S表示SHARE。他們為newsection指定的屬性。SHARE即為共享的意思,意思是把此段讓所有實例共享。

       放入共享段的變數在多個實例中只有一份,不會再向數據段中的變數一樣:每個實例都有一個副本。所以任何實例都可以修改它們。非常重要的一點就是:由於多個實例可以同時修改共享段中的變數,因此要注意同步問題。可以採取線程同步中所介紹的一些方法。



     現在來討論內存映射文件介紹的第二個用途:內存映射磁碟數據文件。

要使用內存映射磁碟文件需要三個步驟:

     1:創建或打開一個文件內核對象。

     2:創建一個文件映射內核對象。

    3:將文件映射對象映射到進程地址空間。



    對於第一點,可以調用CreateFile或是OpenFile,很簡單,此處不作介紹。 HANDLE WINAPI CreateFile( __in      LPCTSTR lpFileName, __in      DWORD dwDesiredAccess, __in      DWORD dwShareMode, __in_opt  LPSECURITY_ATTRIBUTES lpSecurityAttributes, __in      DWORD dwCreationDisposition, __in      DWORD dwFlagsAndAttributes, __in_opt  HANDLE hTemplateFile );
第二點:可以調用CreateFileMapping HANDLE WINAPI CreateFileMapping( __in      HANDLE hFile, __in_opt  LPSECURITY_ATTRIBUTES lpAttributes, __in      DWORD flProtect, __in      DWORD dwMaximumSizeHigh, __in      DWORD dwMaximumSizeLow, __in_opt  LPCTSTR lpName );第一個hFile為要映射到進程地址空間中的文件句柄,CreateFile或是OpenFile返回。

   第二個psa為安全屬性,一般都傳NULL,表示使用默認安全屬性。

   第三個為fdwProtect保護屬性,指定當將文件映射到進程地址空間的時候,應該給物理存儲器的頁面指定何種保護屬性。

   第四個,第五個參數告訴系統內存映射文件的最大大小。

第四個參數dwMaximumSizeHigh為表示文件大小的64位整數的高位元組,dwMaximumSizeLow為低位元組。對於小於4G的文件來說,高位元組當然為0.

     如果要以文件的當前大小創建一個映射對象時,只要將他們設為0就可以。如果要文件中添加數據,一定要使指定的大小大於文件的真實大小。

    第六個參數為文件映射內核對象的名稱。用於跨進程共享命名內核對象。(請參考windows核心編程 第五版 第三章)需要特彆強調下,如果為flProtect指定PAGE_READWRITE屬性,當文件的真實大小小於參數中指定的大小的時候,CreateFileMapping會自動增大文件大小。為的是在將文件作為內存映射文件后,物理存儲器已經就緒。向其寫入數據不會發生錯誤。如果指定PAGE_READONLY或是PAGE_WRITECOPY,那麼傳入的大小不能大於文件的真實大小,因為我們只並不能向文件中增加數據。

    第三步:將文件映射到進程地址空間。

   MapViewOfFile LPVOID WINAPI MapViewOfFile( __in  HANDLE hFileMappingObject, __in  DWORD dwDesiredAccess, __in  DWORD dwFileOffsetHigh, __in  DWORD dwFileOffsetLow, __in  SIZE_T dwNumberOfBytesToMap );第一個參數hFileMappingObject即為CreateFileMapping或是OpenFileMapping返回的文件映射內核對象句柄。
     第二個參數是訪問數據的方式。他們依賴於CreateFileMapping 和CreateFile傳遞的訪問方式。

     第三個和第四個參數告訴系統把數據文件中的的那些內容映射到進程地址空間中。他們分別為要映射文件的偏移 量,是64位的,分別表示高32位和低32位。

    第五個參數指明要把磁碟文件的多少數據映射到進程地址空間中。如果指定為0,系統會把文件中從偏移量開始直到文件末尾的數據全部映射到進程地址空間中。

        當調用MapViewOfFile時指定FILE_MAP_COPY標誌,系統會從系統頁交換文件調撥物理存儲器,大小有dwNumberOfBytesToMap指定。只要我們不執行讀取數據之外的任何操作,系統就不會使用從頁交換文件中調撥頁面 。但是一旦有任何線程寫入文件映射視圖的任何地址,系統就會從已經調撥的頁交換文件中選擇一個頁面把原始數據複製到頁交換文件中的頁面,然後讓線程進行修改這個副本,再將此頁面映射到進程地址空間中。因此任何線程都只會修改數據的副本而不會修改原始數據。

      當不再需要把文件中的數據映射到進程的地址空間的時候,可以調用UnmapViewOfFile  來釋放映射的數據。

BOOL WINAPI UnmapViewOfFile( __in  LPCVOID lpBaseAddress );
    lpBaseAddress用於指定區域的基地址,必須和MapViewOfFile相同。

為了提高運行速度,系統會對文件數據的頁面進行緩存處理,也就是說對文件映射對象映射后的視圖進行修改,不會立即反映到數據文件中。如果想要立即反映到數據文件中,可以調用FlushViewOfFile。來強制系統把修改過的數據協會磁碟文件。

        如果視圖最初使用FILE_MAP_COPY標誌來映射的,那麼對數據文件的修改實際上對系統頁交換文件中的副本進行的修改。請參考紅色欄位。如果這種情況下調用FlushViewOfFile,系統不會將做出的修改保存到磁碟文件中,而會直接釋放系統頁交換文件中的相關數據,導致數據丟失。這只是FILE_MAP_COPY的特性,為了防止數據丟失可以用其他標誌進行映射。

       最後不要忘記調用CloseHandle關閉文件內核對象和文件映射內核對象。

       如果文件非常大,一次無法全部映射到進程的地址空間中,這是該怎麼辦呢?

        此時可以每次只映射一部分文件到進程空間,使用完畢后,撤銷映射。再映射下一部分,使用完畢后再次撤銷映射。如此循環往複。直至將整個文件映射完畢 。

       系統允許我們把一個數據文件映射到多個視圖中。如果我們使用的是同一個文件映射對象映射到不同視圖,一旦有一個視圖中的數據被修改,其他視圖中會立刻更新進而顯示更新后的視圖。也就是說各個視圖中的數據是一致的。為什麼各個視圖的數據都是一致的呢?

         因為他們都是從同一個文件映射對象映射的,數據文件在內存中只有一份,卻映射到了不同視圖中。但要注意,此處有一前提,就是各個視圖都是有同一文件映射對象映射的,如果是同一數據文件為後備存儲器創建不同文件映射對象,那就不能保證他們的數據是一致的了。為了防止這種情況,可以在CreateFile時將dwShareMode 設為獨佔對文件的訪問。從而防止不一致性。


       下面來討論第三個問題:內存映射文件實現進程間共享數據。

       如果我們在創建文件映射對象時為它命名,那麼就可以實現在不同進程間訪問同一文件映射內核對象了。但要注意,要在不同進程分別調用MapViewOfFile,來將同一命名文件映射內核對象,映射到各自的進程地址空間中。

       到此,以我們目前掌握的知識,我們知道要實現在多個進程間共享數據,要創建一個文件對象和一個命名的文件映射內核對象。然後在另一個進程內將此命名的內核對象映射到本進程。這一系列的步驟說明:如果我們要在多個進程間共享數據,我們就必須創建文件,將數據保存在文件中,然後創建文件對象,文件映射對象。。。。這是很繁瑣的。

        Microsoft意識到了這一點,為我們提供了支持:讓系統創建以頁交換文件為後備存儲器的內存映射文件。這就是說當實現進程共享數據時,不再需要創建以磁碟文件為後備存儲器的文件映射對象。此時,文件映射對象的後備存儲器來自系統頁交換文件。這種方法和為磁碟文件創建內存映射文件幾乎完全相同。區別就是:此時無需創建文件對象,在創建文件映射對象時,只需將INVALID_HANDLE_VALUE傳給hFile就可以了。他告訴系統要以系統頁交換文件中調撥物理存儲器。以後的步驟跟為磁碟文件創建內存映射文件相同。

很簡單,不是嗎?來看個例子:HANDLE hFile=CreateFile(...)

HANDLE hMap=CreateFileMapping(hFile........);

if(hMap==NULL)

{

.....................

}看出來什麼問題嗎?

我們知道調用CreateFile失敗的時候,返回的是INVALID_HANDLE_VALUE,而此處沒有判斷文件對象是否成功,就直接創建文件映射對象,一旦創建文件對象失敗,hFile就是INVALID_HANDLE_VALUE,系統會以為程序員要創建以系統頁交換文件為後備存儲器的內存映射文件,而不是為磁碟文件創建內存映射文件。這就導致了錯誤。所以在可能導致失敗的函數執行之後一定要進程判斷。



參考自《windows核心編程—第五版》第三部分,以上僅僅是個人總結,如有紕漏,請不吝賜教,謝謝。同時,想結交志同道合之士,交流windows核心編程的學習。

[火星人 ] 談談內存映射文件已經有417次圍觀

http://coctec.com/docs/service/show-post-1661.html