內存映射文件主要用於以下三種情況:
- 系統使用內存映射文件載入並運行exe和dll,這大量節省了頁交換文件的空間以及應用程序的啟動時間
- 開發人員可以使用內存映射文件來訪問磁盤上的數據文件。這使得我們可以避免直接對文件IO操作和對文件內存進行緩存
- 進程間通訊
17.1 映射到內存的可執行文件和DLL
當一個線程調用CreateProcess的時候,系統會執行收入步驟:
1.判斷exe位置,如果無法找到exe那么不會創建進程,這時會CreateProcess返回FALSE
2.創建一個新的進程內核對象
3.系統為新進程創建一個私有地址空間
4.系統預訂一塊足夠大的地址空間容納exe
5.系統會對地址空間區域進行標注,表明該區域的后備物理存儲器來自磁盤exe文件,而並非來自系統的頁交換文件
當系統把exe文件映射到進程地址空間之后,會訪問exe文件中的一個段,這個段列來出一些DLL文件。系統每次載入DLL,執行的操作與剛才列出的第4步和第5步相似。
1.系統會預訂足夠大的地址空間區域來容納DLL文件。待預訂的地址空間區域的具體位置已經在DLL文件中指定
2.如果系統無法在DLL文件指定基地直處預訂區域,這可能是因為該區域已經被另一個DLL或EXE占用,也可能因為區域不夠大,這時系統會嘗試在另一個地址區域預訂。如果系統無法將DLL載入到指定基地址,那么這種情況就不太走運了。這有兩個原因。首先,如果DLL不包含重定位信息,那么系統將無法載入DLL。其次,系統必須為DLL進行重定位操作,重定位不僅需要占用頁交換文件中額外的地址空間,還會使載入DLL載入變慢
3.系統會對地址空間區域進行標注,表明該區域的后備存儲器來自磁盤上的DLL文件。如果Windows不能將DLL載入到指定基地址而必須重定位,那么系統還會另外進行標。表明DLL中有一部分物理存儲器被映射到了頁交換文件。
17.1.1 同一個可執行文件 或DLL的多個實例不會共享靜態數據
當我們為這個應用程序合建一個新進程時,系統只不過是打開另量個內存映射視圖,創建一個新的進程對象,並創建一個線程對象。這個新打開內存映射視圖隸屬於一個文件映射對象,后者用來標識可執行文件映象。系統同時給進程對象和線程對象分配指定的ID。通過使用內存映射文件,同一個應用程序的多個實例可以共享內存中的代碼和數據。
假設應用程序的第二個實例開始運行,這時系統只不過是把包含應用程序代碼和數據的虛擬內存頁映射到第二個實例地址空間中。如下圖
如果應用程序的一個實例修改了數據頁面中的一個全局變量,那么應用程序所有實例的內存會被修改。由於這種類型的修改可能導致災難性的后果,因為必須避免。
操作系統通過內存管理系統的寫時復制特性來防止這種情況的發生。當寫入內存映射文件時,系統會截獲此嘗試,接着為應用程序分配一塊新內存,然后復制 頁面內容,最終的結果是其它實例不會受到影響。
下圖描繪了當應用程序的第一個實例試圖修改數據頁面2雖的一個全局變量時,會產生的結果。
17.1.2 在同一個可執行文件或DLL的多個實例間共享靜態數據
每個exe文件和dll文件映象都有許多段組成。比如編譯器會將代碼放在一個叫.text段中,將已經初始化的數據放在.data段中。
除了使用編譯器和鏈接器創建的標准段之外,我們可以自定義自己的段。
#pragma data_seg("sectionname")
例如我們可以用下面代碼來合建一個名為Share的段,它只包含一個LONG的變量
#pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_set();注意:編譯器只會將已經初始化的變量放入自己定義的段當中,如果上面代碼中g_lInstanceCount 沒有初始化,則不會放到我們指定的段之中。
但是Vc++ 編譯器提供了一個allocate聲明符,它允許我們將未經初始化的數據放到任何我們想要放入的段中。
#pragma data_seg("Shared") int a = 0; //初始化的會放在shared段內 int b ;//沒有初始化的不會放在shared段內 #pragma data_set(); __declspec(allocate("Shared")) int c = 0; //初始化的會放在shared段內 __declspec(allocate("Shared")) int d ; //沒有初始化的也會放在shared段內 int e = 0;//初始化確不在Shared段中 int f ; //未初始化不在Shared段中之所以將變量放在一個單獨的段中,最常見的原因就是為了共享exe或dll多個實例中共享數據。
為了共享變量,我們還需要告訴鏈接器要共享這個段中的變量。通過使用鏈接器命令行/SECTION 來實現
/SECTION:name,attributes
在本例中我們想要改變Share的屬性,因此應用使用下面的開關
/SECTION:Shared,RWS
我們也可以把這條命令嵌入到代碼中
#pragma comment(linker,"/SECTION:Shared,RWS")
但是MricroSoft並不鼓勵使用共享段。1.有潛在安全漏洞 2,意味着一個應用程序中的錯誤可能影響到另一應用程序。
17.2 映射到內存的數據文件
例子:顛倒文件內容。
17.2.1 方法1:一個文件,一塊緩存
分配足夠大的內存來存放整個文件。接着把文件所有內容讀取到內存中。這時我們可以對內存中的文件內容進行操作,第第一個字節和最后一個字節交換,第二個字節和倒數第二個字節交換。以此類推。
缺點:
1.必須根據文件大小來分配內存,如果文件過大就不行了
2.把已經顛倒的內容寫入文件時中斷,那么文件內容遭到破壞。避免這種錯誤的方法是合建副本,但是這樣會浪費磁盤空間
17.2.2方法2:兩個文件,一塊緩存
創建一個長度為0的新文件,接着分配一塊小的內部緩存,比如8KB。然后將文件指針定位到原始文件末尾減去8KB的地方,最后8KB的內容讀取到緩存中,然后顛倒內容寫入到剛才創建的文件中。這個定位文件指針、讀取文件、寫入文件的過程一直繼續。直到原始文件達到起始位置。
缺點:
1.每次操作必須對文件指針進行定位移動操作,因為字的速度比第一種方法要慢
2.處理過程中文件內容一直在增大,如果原始文件為1GB,那么在原始文件刪除之前這兩個文件占用空間用2GB的磁盤空間。
17.2.3 方法3:一個文件,兩塊緩存
申請兩塊大小為8KB的緩存。程序接着把文件開始的8KB內容讀取到第一塊緩存中,把文件末尾的8KB讀到第二塊緩存中。然后后兩塊緩存的內容顛倒,並把第一塊緩存的內容寫回到文件末尾,把第二塊緩存的內容吃回到文件開頭。這個過程一直繼續。
與前一種方法相比,這種方法更好的節省磁盤空間,由於 所有數據都讀取自和寫入到了同一個文件,因此不需要額外的磁盤空間。在內存方面這種方式也不差,只使用了16kb內存。
17.2.4 方法3:一個文件,零個緩存
使用內存映射文件來顛倒文件內容時,我們先打開文件並向系統預訂一塊虛擬地址空間區域。接着讓系統把文件的第一個字節映射到該區域的第一個字節。然后就可以訪問這塊虛擬內存區域,就好像它實際上包含了文件一樣。事實上,如果要顛倒的是一個文本文件,而且文件末尾字節為0,則可以把這個文件當作內存中的一個字符串來處理,在這種情況下,直接調用c運行庫函數_tcsrev就能顛倒文件中的數據。
這種方法最大的優點就是讓系統為我們處理文件緩存有關操作。但遺憾的是,使用內存映射文件的時候,如果操作過程被中途打斷(如斷電),仍然可能導致數據被破壞
17.3 使用內存映射文件
(1)創建或打開一個文件內核對象,該對象標識了我們想要用作內存映射文件的哪 個磁盤文件。
(2)創建一個文件映射內核對象,來告訴系統文件的大小以及我們打算如何訪問文件
(3)告訴系統把文件映射對象的部分或全部進程地址空間中。
用完內存映射文件之后,必須執行下面三個步驟做清理工作
(1)告訴系統從進程地址空間中取消對文件映射內核對象的映射
(2)關閉文件映射內存對象
(3)關閉文件內核對象
17.3.1 第1步 創建或打開文件內核對象
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 );
17.3.2 第2步 創建文件映射內存對象
HANDLE WINAPI CreateFileMapping( __in HANDLE hFile, __in_opt LPSECURITY_ATTRIBUTES lpAttributes, __in DWORD flProtect, __in DWORD dwMaximumSizeHigh, __in DWORD dwMaximumSizeLow, __in_opt LPCTSTR lpName );
正如本章開頭所指出的,創建一個內存映射文件相當於先預訂一塊地址空間區域,然后再給區域調撥物理存儲器。唯一不同之處在於內存映射文件的物理存儲器來自磁盤上的文件,而不是從系統頁交換文件文件中分配的。創建一個文件映射對象的時候,系統不會預訂一塊地址空間區域,並把文件映射到該區域中。但是,當系統在映射進程地址空間的時候,它必須知道應該給物理存儲器的頁面指定何種保護屬性。fdwProtect參數就 是讓我們指定保護屬性的。
PAGE_READONLY、PAGE_READWRITE、PAGE_WRITECOPY、PAGE_EXECUTE_READ、PAGE_EXECUTE_READWRITE
除了上面的頁保護屬性,我們可以把5種段屬性fdwProcted參數按位或起來。
SEC_NOCACHE,SEC_IMAGE,SEC_RESERVE,SEC_COMMIT,SEC_LARGE_PAGES
要使用大頁面內存必須滿足以下條件:
- 在調用CreateFileMapping的時候必須同時指定SEC_COMMIT屬性來調撥內存
- 映像的大小必須大於GetLargePageMinimum函數返回值。
- 必須用PAGE_READWRITE保護屬性定義映射
- 用戶必須具有並啟用內存中鎖定頁面用戶權限
CreateFileMapping有兩參數最重要dwMaximumSizeHigh,dwMaximumSizeLow這兩個參數告訴系統內存映射文件的最大大小,以字節為單位。如果想要用當前文件大小創建一個文件映射對象時,只要傳0就可以 了。
// TestCreateMappingFile.cpp : 定義應用程序的入口點。 // #include "stdafx.h" #include "TestCreateMappingFile.h" int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); HANDLE hFile = ::CreateFile(TEXT("C:\\mmText.dat"), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); //創建一個空文件 HANDLE hFileMap = ::CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 100, NULL); //此時文件大小為100 CloseHandle(hFile); CloseHandle(hFileMap); return 0; }
17.3.3 第3步 將文件的數據映射到進程地址空間
在創建了文件映射對象之后,還需要為文件的數據預訂一塊地址空間區域並將文件的數據做為物理存儲器調撥給區域。這可以通過調用MapViewOfFile來實現
LPVOID WINAPI MapViewOfFile( __in HANDLE hFileMappingObject, __in DWORD dwDesiredAccess, __in DWORD dwFileOffsetHigh, __in DWORD dwFileOffsetLow, __in SIZE_T dwNumberOfBytesToMap );
我們主要看一下第三個參數,第三個參數與預定地址空間和給區域調撥存儲器有關,當我們把一個文件映射到地址空間的時候不必一下子映射整個文件。可以每次只把一小部分映射到地址空間中。文件中被映射到地址空間的部分被稱為視圖。
把文件映射到地址空間需要告訴系統兩件事情:
1.我們必須告訴系統應該把數據文件中的哪個字節映射到視圖中的第一個字節
2.我們必須告訴系統要把數據文件的多少映射到地址空間去。
int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); //打開要映射的文件 HANDLE hFile = ::CreateFile(pszFileName, GERERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); //為文件創建一個文件映射對象 HANDLE hFileMapping = ::CreateFileMapping(hFile,NULL,PAGE_WRITECOPPY,0,0,NULL); //以copy-on-write的方式映射到地址空間 PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping,FILE_MAP_COPY,0,0,0); //從映射讀一個字節 //當讀的時候系統不會改變頁屬性,依然保持PAGE_WRITECOPY屬性 BYTE bSomeByte = pbaFile[0]; //寫一個字節 //當第一次寫入一個字節的時候,系統會捕獲,會復制原內容到新的頁面次的頁面有PAGE_READWRITE屬性 pbFile[0] = 0; //寫另一個字節 //由於這個字節現在是PAGE_READWRITE屬性,系統會寫入字節到頁面 pbFile[1] = 0; //當完成內存映射時,要取消映射 UnmapViewOfFile(pFile); CloseHandle(hFileMapping); CloseHandle(hFile); return 0; }
17.3.4 第4步 從進程的地址空間撤銷對文件數據的映射
BOOL WINAPI UnmapViewOfFile( __in LPCVOID lpBaseAddress );
如果需要確保所有修改已經被寫入到磁盤中可以調用
BOOL WINAPI FlushViewOfFile( __in LPCVOID lpBaseAddress, __in SIZE_T dwNumberOfBytesToFlush );
17.3.5 第5步和第6步 關閉文件映射對象
CloseHandle(hFile);
CloseHandle(hMappfile);
17.3.6 示例程序
FileReverse程序展示了如何使用內存映射文件馬一個Ansi或Unicode文本文件內容顛倒過來主要代碼如下\
BOOL FileReverse(PCTSTR pszPathname, PBOOL pbIsTextUnicode) { *pbIsTextUnicode = FALSE; // Assume text is Unicode // Open the file for reading and writing. HANDLE hFile = CreateFile(pszPathname, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { chMB("File could not be opened."); return(FALSE); } // Get the size of the file (I assume the whole file can be mapped). DWORD dwFileSize = GetFileSize(hFile, NULL); // Create the file-mapping object. The file-mapping object is 1 character // bigger than the file size so that a zero character can be placed at the // end of the file to terminate the string (file). Because I don't yet know // if the file contains ANSI or Unicode characters, I assume worst case // and add the size of a WCHAR instead of CHAR. HANDLE hFileMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, dwFileSize + sizeof(WCHAR), NULL); if (hFileMap == NULL) { chMB("File map could not be opened."); CloseHandle(hFile); return(FALSE); } // Get the address where the first byte of the file is mapped into memory. PVOID pvFile = MapViewOfFile(hFileMap, FILE_MAP_WRITE, 0, 0, 0); if (pvFile == NULL) { chMB("Could not map view of file."); CloseHandle(hFileMap); CloseHandle(hFile); return(FALSE); } // Does the buffer contain ANSI or Unicode? int iUnicodeTestFlags = -1; // Try all tests *pbIsTextUnicode = IsTextUnicode(pvFile, dwFileSize, &iUnicodeTestFlags); if (!*pbIsTextUnicode) { // For all the file manipulations below, we explicitly use ANSI // functions because we are processing an ANSI file. // Put a zero character at the very end of the file. PSTR pchANSI = (PSTR) pvFile; pchANSI[dwFileSize / sizeof(CHAR)] = 0; // Reverse the contents of the file. _strrev(pchANSI); // Convert all "\n\r" combinations back to "\r\n" to // preserve the normal end-of-line sequence. pchANSI = strstr(pchANSI, "\n\r"); // Find first "\r\n". while (pchANSI != NULL) { // We have found an occurrence.... *pchANSI++ = '\r'; // Change '\n' to '\r'. *pchANSI++ = '\n'; // Change '\r' to '\n'. pchANSI = strstr(pchANSI, "\n\r"); // Find the next occurrence. } } else { // For all the file manipulations below, we explicitly use Unicode // functions because we are processing a Unicode file. // Put a zero character at the very end of the file. PWSTR pchUnicode = (PWSTR) pvFile; pchUnicode[dwFileSize / sizeof(WCHAR)] = 0; if ((iUnicodeTestFlags & IS_TEXT_UNICODE_SIGNATURE) != 0) { // If the first character is the Unicode BOM (byte-order-mark), // 0xFEFF, keep this character at the beginning of the file. pchUnicode++; } // Reverse the contents of the file. _wcsrev(pchUnicode); // Convert all "\n\r" combinations back to "\r\n" to // preserve the normal end-of-line sequence. pchUnicode = wcsstr(pchUnicode, L"\n\r"); // Find first '\n\r'. while (pchUnicode != NULL) { // We have found an occurrence.... *pchUnicode++ = L'\r'; // Change '\n' to '\r'. *pchUnicode++ = L'\n'; // Change '\r' to '\n'. pchUnicode = wcsstr(pchUnicode, L"\n\r"); // Find the next occurrence. } } // Clean up everything before exiting. UnmapViewOfFile(pvFile); CloseHandle(hFileMap); // Remove trailing zero character added earlier. SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN); SetEndOfFile(hFile); CloseHandle(hFile); return(TRUE); }
17.4 用內存映射文件來處理大文件
下現代碼統計一個二進制文件中所有值為0的字節數
__int64 Count0s(void) { SYSTEM_INFO sinf; GetSystemInfo(&sinf); HANDLE hFile = CreateFile(TEXT("C:\\HugeFile.big"),GENERIC_READ, FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_FLAG_SEQUENTIAL_SCAN,NULL); HANDLE hFileMapping = CreateFileMapping(hFile,NULL,PAGE_READONLY,0,0,NULL); DWORD dwFileSizeHight; __int64 qwFileSize = GetFileSize(hFile,&dwFileSizeHight); qwFileSize += (((__int64)dwFileSizeHight) << 32); CloseHandle(hFile); __int64 qwFileOffset = 0,qwNumOf0s = 0; while(qwFileOffset > 0) { DWORD dwBytesInBlock = sinf.dwAllocationGranularity; if(qwFileSize < sinf.dwAllocationGranularity) { dwBytesInBlock = (DWORD) qwFileSize; } PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping,FILE_MAP_READ, (DWORD) (qwFileOffset >> 32), (DWORD) (qwFileOffset & 0xFFFFFFFF), dwBytesInBlock); for(DWORD dwByte = 0;dwByte < dwBytesInBlock;dwByte ++ ) { if(pbFile[dwByte] == 0) qwNumOf0s ++; } UnmapViewOfFile(pbFile); qwFileOffset += dwBytesInBlock; qwFileOffset -= dwBytesInBlock; } CloseHandle(hFileMapping); return qwNumOf0s; }
17.5 內存映射文件和一致性
只要我們映射的是同一個文件映象,那么系統會確保各個視圖中的數據是一致的。如果多個進程把同一個數據文件映射到多個視圖中,那么數據也是一致的,這是因為數據文件中每一個頁在內存中只有一份,但這些內面頁面會被映射到多個進程地址空間而已。
如果打算將打開的文件用於內存映射,那么調用 CraeteFile時最好傳0給dwShareMode參數。這等於告訴系統我想要獨占文件的訪問,使其它進程無法訪問此文件。
由於只讀文件不存在 一致性的問題,因此它們非常適合內存映射文件。我們絕對不應該用內存映射文件來跨網絡共享可寫文件,因為系統無法保證數據視圖的一致性。、
17.6 給內存映射文件指定基地址
LPVOID WINAPI MapViewOfFileEx( __in HANDLE hFileMappingObject, __in DWORD dwDesiredAccess, __in DWORD dwFileOffsetHigh, __in DWORD dwFileOffsetLow, __in SIZE_T dwNumberOfBytesToMap, __in_opt LPVOID lpBaseAddress );
17.6 內存映射文件的實現細節
第一個進程調用MapViewOfFile時返回的內存地址,與第二個進程調用MapViewOfFile時返回的內存地址很可能是不相同的。即使兩個映射同一個文件,情況也是如此。
17.8 使用內存映射文件在進程間共享數據。
讓 我們來看一個例子:啟動應用程序。當一個應用程序啟動時,系統會先調用CreateFile來打開磁盤上的.exe文件。接着系統會調用 CreateFileMapping來創建文件映射對象。最后系統會以新創建的進程的名義調用MapViewOfFileEx(並傳入SEC_IMAGE 標志),這樣就把.exe文件映射到了進程的地址空間中。值所以調用MapViewOfFileEx而不是MapViewOfFile,是為了把文件映射 到指定的基地址,這個基地址保存在.exe的PE文件頭中。系統然后創建進程的主線程,在映射得到的視圖中取得可執行代碼的第一個字節的地址,把該地址放 到線程的指令指針中,最后讓CPU開始執行其中的代碼。
如 果用戶啟動同一個應用程序的第二個實例,那么系統會發現該.exe文件已經有一個文件映射對象,因此就不會再創建一個新的文件對象或文件映射對象。取而代 之的是,系統會再次映射.exe文件的一個視圖,但這次是在新創建的進程的地址空間中。至此,系統已經把同一個文件同時映射到了兩個地址空間中。顯然,由 於物理內存中包含.exe文件可執行代碼的那些頁面為兩個進程所共享,因此內存的使用率更高。
17.9 以頁交換文件為后備存儲器的內存映射文件
Microsoft加入了相應的支持,讓系統能夠創建以頁交換文件為后備存儲器的內存映射文件,這樣就不需要用磁盤上專門的文件來作為后備存儲器了。這種方法和為磁盤文件創建內存映射文件的方法幾乎完全相同,甚至更簡單。 一方面,由於不必創建或打開一個專門的磁盤文件,因此不需要調用CreateFile。我們只需要像原來那樣調用CreateFileMapping,並將INVALID_HANDLE_VALUE作為hFile參數傳入。這告訴系統我們創建的文件映射對象的物理存儲器不是磁盤上的文件,而是希望系統從頁交換文件中調撥物理存儲器。 所需分配的存儲器大小由CreateFileMapping的dwMaximumSizeHigh和dwMaximumSizeLow參數決定
Memory-Mapped File Sharing 示例程序
主要關鍵代碼
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) { // Handle of the open memory-mapped file static HANDLE s_hFileMap = NULL; switch (id) { case IDCANCEL: EndDialog(hWnd, id); break; case IDC_CREATEFILE: if (codeNotify != BN_CLICKED) break; // Create a paging file-backed MMF to contain the edit control text. // The MMF is 4 KB at most and is named MMFSharedData. s_hFileMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 4 * 1024, TEXT("MMFSharedData")); if (s_hFileMap != NULL) { if (GetLastError() == ERROR_ALREADY_EXISTS) { chMB("Mapping already exists - not created."); CloseHandle(s_hFileMap); } else { // File mapping created successfully. // Map a view of the file into the address space. PVOID pView = MapViewOfFile(s_hFileMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); if (pView != NULL) { // Put edit text into the MMF. Edit_GetText(GetDlgItem(hWnd, IDC_DATA), (PTSTR) pView, 4 * 1024); // Protect the MMF storage by unmapping it. UnmapViewOfFile(pView); // The user can't create another file right now. Button_Enable(hWndCtl, FALSE); // The user closed the file. Button_Enable(GetDlgItem(hWnd, IDC_CLOSEFILE), TRUE); } else { chMB("Can't map view of file."); } } } else { chMB("Can't create file mapping."); } break; case IDC_CLOSEFILE: if (codeNotify != BN_CLICKED) break; if (CloseHandle(s_hFileMap)) { // User closed the file, fix up the buttons. Button_Enable(GetDlgItem(hWnd, IDC_CREATEFILE), TRUE); Button_Enable(hWndCtl, FALSE); } break; case IDC_OPENFILE: if (codeNotify != BN_CLICKED) break; // See if a memory-mapped file named MMFSharedData already exists. HANDLE hFileMapT = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE, FALSE, TEXT("MMFSharedData")); if (hFileMapT != NULL) { // The MMF does exist, map it into the process's address space. PVOID pView = MapViewOfFile(hFileMapT, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0); if (pView != NULL) { // Put the contents of the MMF into the edit control. Edit_SetText(GetDlgItem(hWnd, IDC_DATA), (PTSTR) pView); UnmapViewOfFile(pView); } else { chMB("Can't map view."); } CloseHandle(hFileMapT); } else { chMB("Can't open mapping."); } break; } }
17.9 以頁交換文件為后備存儲器的內存映射文件
我們只需要給CreateFileMapping第一個參數傳INVALID_HANDLE_VALUE的系統就以頁交換文件中調撥物理存儲器。
17.10稀疏調撥的內存映射文件
CreateFileMapping為我們提供了一種方法,即在fdwProtect參數中指定SEC_RESERVE或SEC_COMMIT標志。這樣我們把先前的那個電子的數據作為一個文件映射對象來共享,但又不希望在一開始就給它調撥所有物理存儲器。
SEC_RESERVE:系統不會從頁交換文件中調撥物理存儲器,它只返回文件映射對象的句柄。現在我們可以調用MapViewOfFile來給這個文件映射對象創建一個視圖。MapViewOfFile會預訂地址空間,但不會調任何物理存儲器。設計圖訪問會違規。通過使用SEC_RERVER和VirtualAlloc我們不僅能與其它進程共享電子表格的數組,而且還能高效的使用物理存儲器。如果內存映射文件是通過SEC_RESERVE標志得到的,便不能用VirtualFree來撤銷調撥給它的存儲
NT文件系統(NTFS)提供了對稀疏文件的支持。這是一項非常棒的特征。我們可以用這項特性來創建和處理稀疏內存映射文件,這樣一來,存儲器就不必總是在頁交換文件中,而可以在普通文件中。
假設我們要創建一個內存映射文件來存儲錄音數據。當用戶說話時,我們希望把數字音頻數據寫入到內存緩存中,並以磁盤文件為內存緩存的后備存儲器。一個部分調撥的內存映射文件當然是最簡單高效的辦法。問題是我們不知道用戶在單擊停止按鈕前會說多久,可能是五分鍾,但也可以是5小時。差距不可謂不大!我們需要一個足夠大的文件來保存這些數據。但是,在使用稀疏調撥的內存映射文件時,大小並沒有多大關系。