Windows API筆記(六)內存映射文件

Windows API筆記(一)內核對象
Windows API筆記(二)進程和進程間通信、進程邊界
Windows API筆記(三)線程和線程同步、線程局部存儲
Windows API筆記(四)win32內存結構
Windows API筆記(五)虛擬內存
Windows API筆記(六)內存映射文件
Windows API筆記(七)堆
Windows API筆記(八)文件系統
Windows API筆記(九)窗口消息
Windows API筆記(十)動態鏈接庫
Windows API筆記(十一)設備I/O



使用內存映射文件主要有3個目的:

  • 系統使用內存映射文件來裝入和執行exe和dll文件。這大大節省了頁面文件空間和棄用程序開始執行的時間。
  • 可以使用內存映射文件來訪問磁盤上的數據文件,無需進行文件I/O操作或緩衝文件的內容。
  • 可以使用內存映射文件來允許運行在同一計算機上的多個進程之間共享數據。(Win32還提供了其他方法在進程間進行數據通信,但這些方法的實現都使用了內存映射文件)。

1. 內存映射exe和dll

當線程調用CreateProcess時,系統執行下列步驟:

  1. 系統定位在CreateProcess中指定的exe文件。如果找不到,就不會創建進程,CreateProcess返回FALSE。

  2. 系統創建一個新的進程內核對象。

  3. 系統爲新進程創建一個4GB的地址空間。

  4. 系統在地址空間中保留了足夠裝下exe文件的一塊區域。該區域的位置是在exe文件中指定的。默認,exe文件的基本地址是0x00400000。不過,在鏈接程序時,可以使用鏈接器的/BASE選項來重載這一地址。

  5. 系統注意到支持該保留區域的物理存儲是磁盤上的exe文件,而不是系統的頁面文件。在exe文件被映射到進程的地址空間之後,系統訪問exe文件裏的某一節,那裏列出了包含exe調用的函數的dll文件。然後系統爲每個dll調用LoadLibrary,如果某個dll還需要其他的dll,系統也會調用LoadLibrary來加載這些dll。每當調用LoadLibrary來加載一個dll時,系統執行蕾仕於上面的第4和5步的行動:

    1. 系統保留一塊足夠裝得下dll文件的區域。該區域的位置是dll文件自己指定的。缺省時,vc++使得dll的基本地址爲0x10000000。不過,在建立dll時,可以使用鏈接器的/BASE選項來重設該值。
    2. 如果系統不能再dll指定的基本地址處保留區域,或者是因爲該區域被其他dll或exe佔據了,或者是因爲該區域不夠大,系統就將在地址空間尋找另一塊區域來保留給該dll。如果dll不能加載到它指定的基本地址是很不幸的,這有兩個原因。首先,如果該dll不含有修正信息,系統就可能不能加載該dll。(在創建dll時,可以使用鏈接器的/FIXED開關來刪除修正信息。這能使dll文件變小,但也意味着該dll必須加載到它指定的地址。)其次,系統必須在dll內部進行一些重定位。在Windows NT上,這些重定位需要系統的頁面文件中的一些額外存儲;這還增加了加載dll所需的時間。
    3. 系統會記下來支持保留區域的文件存儲是在磁盤上的dll文件而不是在系統的頁面文件中。如果因爲dll不能加載到它指定的基本地址,Windows NT必須進行重定位的話,系統也會記下來該dll的一些物理存儲被映射到了頁面文件中。
  6. 如果系統因故不能映射exe和所需的dll,CreateProcess將向調用者返回FALSE,可以調用GetLastError來弄清進程爲什麼不能被創建。

在所有的exe和dll文件被映射進進程的地址空間之後,系統就能開始執行exe文件的啓動代碼了。在exe我呢見被映射之後,系統會負責所有的頁面、緩衝和緩存。例如,如果exe中的代碼跳到了還沒有裝入內存的一條指令的地址,會產生一個錯誤。系統檢測到錯誤後,會自動把該代碼所在頁從我呢見的映象裝入到RAM的頁中。而後系統把RAM頁映射到進程地址空間中的正確位置。允許線程繼續執行,就好像代碼頁早已被裝入一樣。當然,這些對程序是不可見的。每當進程中的線程試圖訪問的數據或代碼沒有被裝入RAM時,就會重複該過程。

1.1 不被exe或dll的多個實例共享的靜態數據

系統通過使用該內存管理系統和寫拷貝特性來防止某一實例改變了共享的靜態數據導致所有實例的內存內容都被改變。每當應用試圖寫它的內存映射文件時,系統捕捉到請求,對包含有被寫的內存的頁分配一塊新內存,拷貝頁的內容,然後允許應用程序寫這塊新分配的內存。這樣,其他的實例就不會受到影響

可以在exe或dll中創建能被所有實例共享的全局遍歷。簡單的說,該方法要求使用#pragma data_seg()編譯器指令將要共享的變量放在它們自己的節中。然後必須使用/SECTION: name,attributes開關來告訴鏈接器要讓該節中的數據被所有的實例或文件的映象共享。

2. 內存映射數據文件

在exe或dll文件被加載時,操作系統自動使用前一節中講述過的技術。不過,還可能在進程的地址空間中映射一個數據文件。這使得操縱大數據流非常方便。
爲了理解這樣使用內存映射文件的強大功能,讓我們看一下實現一個程序將文件中的所有字節倒放的4中可能的方法。

2.1 方法1:一個文件,一個緩衝區

流程:

  1. 申請一塊足夠大的內存
  2. 將文件讀入內存
  3. 倒置內存中的數據
  4. 寫入文件

缺點:

  • 必須分配一塊與文件大小相同的內存,如果文件較大(超過內存限制)就可能無法實現
  • 寫回文件時,如果過程被中斷了,文件的內容就被破壞了

2.2 方法2:兩個文件,一個緩衝區

流程:

  1. 創建一個新文件
  2. 創建一個較小的內部緩衝區,比如8KB
  3. 讀入源文件的最後8KB至內部緩衝區
  4. 倒置內部緩衝區的內容,然後寫入新文件
  5. 重複3-4,直至讀完源文件
  6. 刪除源文件,保留新文件

缺點:

  • 比方法1複雜
  • 處理速度比方法1要慢
  • 可能要佔用巨大的硬盤空間

2.3 方法3:一個文件,兩個緩衝區

流程:

  1. 申請兩個8KB的內存緩衝區
  2. 將文件的開始8KB字節讀入一個緩衝區
  3. 將文件的最後8KB字節讀入另一個緩衝區
  4. 分別倒置兩個緩衝區的數據,然後分別寫入文件的開始和結尾
  5. 重複2-4,只至文件全部讀完

優點:

  • 節省內存和硬盤空間

缺點:

  • 實現複雜
  • 處理過程被打斷則可能破壞源文件

2.4 方法4:一個文件,零個緩衝區

使用內存映射文件倒置文件內容。

流程:

  1. 將文件映射到虛擬地址空間
  2. 調用_strrev將文件中的數據倒置即可

優點:

  • 系統替你管理所有的文件緩存,不必分配任何內存,不必將文件裝入內存,頁不必將文件寫回文件和釋放任何內存塊

缺點:

  • 掉電等意外事故可能在處理過程中破壞源文件

3. 使用內存映射文件

要使用內存映射文件,必須執行下列3步:

  1. 創建或打開一個文件內核對象來標識硬盤上的想用作內存映射文件的文件
  2. 創建一個文件映射內核對象來告訴系統文件的大小和想要如何訪問文件
  3. 告訴系統把文件映射對象的全部或部分映射到進程的地址空間中

使用完內存映射文件後,必須執行下列3步進行清理工作:

  1. 告訴文件把文件映射對象從進行的地址空間中解除映射
  2. 關閉文件映射內核對象
  3. 關閉文件內核對象

3.1 第1步:創建或打開文件內核對象

調用CreateFIle創建或打開文件內核對象:

HANDLE
CreateFileA(
    _In_ LPCSTR lpFileName,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwShareMode,
    _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
    _In_ DWORD dwCreationDisposition,
    _In_ DWORD dwFlagsAndAttributes,
    _In_opt_ HANDLE hTemplateFile
    );

dwDesiredAccess 以何種方式訪問文件,取值範圍:

含義
0 不能讀寫文件的內容
GENERIC_READ 可讀
GENERIC_WRITE 可寫
GENERIC_READ|GENERIC_WRITE 可讀可寫

對於內存映射文件,必須以只讀或讀寫方式打開文件。
dwShareMode 如何共享該文件,取值範圍:

含義
0 不共享,獨佔文件
FILE_SHARE_READ 讀共享,其他以帶有寫的方式打開文件都會失敗
FILE_SHARE_WRITE 寫共享,其他以帶有讀的方式打開文件都會失敗
FILE_SHRE_READ|FILE_SHARE_WRITE 讀寫共享,其他打開文件的嘗試都會成功

3.2 第2步:創建文件映射內核對象

調用CreateFile是告訴操作系統文件映射的物理存儲的位置。傳送的路徑名指出了支持文件映射的物理存儲的確切位置,是在硬盤上、網絡上、CD_ROM盤上等等。現在,必須告訴系統文件映射對象需要多少物理存儲。這時調用CreateFileMapping:

HANDLE
CreateFileMappingA(
    _In_     HANDLE hFile,
    _In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
    _In_     DWORD flProtect,
    _In_     DWORD dwMaximumSizeHigh,
    _In_     DWORD dwMaximumSizeLow,
    _In_opt_ LPCSTR lpName
    );

第1個參數hFile標識了想要映射到進程的地址空間中的文件的句柄。該句柄是由CreateFile創建的。
當創建文件映射對象時,系統並不保留地址空間中的區域,並把文件的內存映射到該區域。不過,當系統要向進程的地址空間映射存儲時,系統必須知道賦給物理存儲頁的保護屬性。dwProtect允許指定保護屬性,大多數時候指定的是下表中給出的3個保護屬性之一:

保護屬性 含義
PAGE_READONLY 文件映射對象爲只讀,必須向CreateFile傳遞GENERIC_READ
PAGE_READWRITE 文件映射對象爲可讀可寫,必須向CreateFile傳遞GENERIC_READ|GENERIC_WRITE
PAGE_WRITECOPY 文件映射對象爲寫拷貝,可讀可寫;寫時會創建一份頁面的私有拷貝。必須向CreateFile傳遞GENERIC_READ或GENERIC_READ|GENERIC_WRITE

dwMaximumSizeHigh和dwMaximumSizeLow是最重要的參數。因爲該函數的主要目的是確保對於文件映射對象有足夠的物理存儲。這兩個參數告訴系統文件的最大字節大小。使用兩個32位值是因爲Win32支持64位的文件大小,dwMaximumSizeLow指定低32位,dwMaximumSizeHigh指定高32位。對於4GB以下的文件,dwMaximumSizeHigh總是爲0。
如果在調用CreateFileMapping時傳遞PAGE_READWRITE標誌,系統將會確保在磁盤上的相關數據文件的大小至少是由dwMaximumSizeHigh和dwMaximumSizeLow所給出的大小。如果文件比指定大小要小,將會增大磁盤上的文件。

#include <Windows.h>

int main()
{
    // 創建新文件
    HANDLE hFile = CreateFile("MMFTest.dat",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,NULL);

    // 將使文件大小爲100byte
    HANDLE hFilemap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,100,NULL);

    CloseHandle(hFilemap);
    CloseHandle(hFile);

    return 0;
}

最後一個參數lpName是一個字符串,用於給文件映射對象命名。該名字是用來與其他進程共享該對象的。不需要共享時,該參數一般爲NULL。

3.3 第3步:將文件數據映射入進程地址空間

在創建文件映射對象後,還要讓系統保留一塊地址空間區域,將文件數據作爲物理存儲提交到該空間。這通過調用MapViewOfFile來實現:

LPVOID
MapViewOfFile(
    _In_ HANDLE hFileMappingObject,
    _In_ DWORD dwDesiredAccess,
    _In_ DWORD dwFileOffsetHigh,
    _In_ DWORD dwFileOffsetLow,
    _In_ SIZE_T dwNumberOfBytesToMap
    )
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章