《windows核心编程》 17章 内存映射文件
內(nèi)存映射文件主要用于以下三種情況:
- 系統(tǒng)使用內(nèi)存映射文件載入并運(yùn)行exe和dll,這大量節(jié)省了頁交換文件的空間以及應(yīng)用程序的啟動(dòng)時(shí)間
- 開發(fā)人員可以使用內(nèi)存映射文件來訪問磁盤上的數(shù)據(jù)文件。這使得我們可以避免直接對文件IO操作和對文件內(nèi)存進(jìn)行緩存
- 進(jìn)程間通訊
17.1 映射到內(nèi)存的可執(zhí)行文件和DLL
當(dāng)一個(gè)線程調(diào)用CreateProcess的時(shí)候,系統(tǒng)會(huì)執(zhí)行收入步驟:
1.判斷exe位置,如果無法找到exe那么不會(huì)創(chuàng)建進(jìn)程,這時(shí)會(huì)CreateProcess返回FALSE
2.創(chuàng)建一個(gè)新的進(jìn)程內(nèi)核對象
3.系統(tǒng)為新進(jìn)程創(chuàng)建一個(gè)私有地址空間
4.系統(tǒng)預(yù)訂一塊足夠大的地址空間容納exe
5.系統(tǒng)會(huì)對地址空間區(qū)域進(jìn)行標(biāo)注,表明該區(qū)域的后備物理存儲(chǔ)器來自磁盤exe文件,而并非來自系統(tǒng)的頁交換文件
當(dāng)系統(tǒng)把exe文件映射到進(jìn)程地址空間之后,會(huì)訪問exe文件中的一個(gè)段,這個(gè)段列來出一些DLL文件。系統(tǒng)每次載入DLL,執(zhí)行的操作與剛才列出的第4步和第5步相似。
1.系統(tǒng)會(huì)預(yù)訂足夠大的地址空間區(qū)域來容納DLL文件。待預(yù)訂的地址空間區(qū)域的具體位置已經(jīng)在DLL文件中指定
2.如果系統(tǒng)無法在DLL文件指定基地直處預(yù)訂區(qū)域,這可能是因?yàn)樵搮^(qū)域已經(jīng)被另一個(gè)DLL或EXE占用,也可能因?yàn)閰^(qū)域不夠大,這時(shí)系統(tǒng)會(huì)嘗試在另一個(gè)地址區(qū)域預(yù)訂。如果系統(tǒng)無法將DLL載入到指定基地址,那么這種情況就不太走運(yùn)了。這有兩個(gè)原因。首先,如果DLL不包含重定位信息,那么系統(tǒng)將無法載入DLL。其次,系統(tǒng)必須為DLL進(jìn)行重定位操作,重定位不僅需要占用頁交換文件中額外的地址空間,還會(huì)使載入DLL載入變慢
3.系統(tǒng)會(huì)對地址空間區(qū)域進(jìn)行標(biāo)注,表明該區(qū)域的后備存儲(chǔ)器來自磁盤上的DLL文件。如果Windows不能將DLL載入到指定基地址而必須重定位,那么系統(tǒng)還會(huì)另外進(jìn)行標(biāo)。表明DLL中有一部分物理存儲(chǔ)器被映射到了頁交換文件。
17.1.1 同一個(gè)可執(zhí)行文件 或DLL的多個(gè)實(shí)例不會(huì)共享靜態(tài)數(shù)據(jù)
當(dāng)我們?yōu)檫@個(gè)應(yīng)用程序合建一個(gè)新進(jìn)程時(shí),系統(tǒng)只不過是打開另量個(gè)內(nèi)存映射視圖,創(chuàng)建一個(gè)新的進(jìn)程對象,并創(chuàng)建一個(gè)線程對象。這個(gè)新打開內(nèi)存映射視圖隸屬于一個(gè)文件映射對象,后者用來標(biāo)識(shí)可執(zhí)行文件映象。系統(tǒng)同時(shí)給進(jìn)程對象和線程對象分配指定的ID。通過使用內(nèi)存映射文件,同一個(gè)應(yīng)用程序的多個(gè)實(shí)例可以共享內(nèi)存中的代碼和數(shù)據(jù)。
假設(shè)應(yīng)用程序的第二個(gè)實(shí)例開始運(yùn)行,這時(shí)系統(tǒng)只不過是把包含應(yīng)用程序代碼和數(shù)據(jù)的虛擬內(nèi)存頁映射到第二個(gè)實(shí)例地址空間中。如下圖
如果應(yīng)用程序的一個(gè)實(shí)例修改了數(shù)據(jù)頁面中的一個(gè)全局變量,那么應(yīng)用程序所有實(shí)例的內(nèi)存會(huì)被修改。由于這種類型的修改可能導(dǎo)致災(zāi)難性的后果,因?yàn)楸仨毐苊狻?/p>
操作系統(tǒng)通過內(nèi)存管理系統(tǒng)的寫時(shí)復(fù)制特性來防止這種情況的發(fā)生。當(dāng)寫入內(nèi)存映射文件時(shí),系統(tǒng)會(huì)截獲此嘗試,接著為應(yīng)用程序分配一塊新內(nèi)存,然后復(fù)制 頁面內(nèi)容,最終的結(jié)果是其它實(shí)例不會(huì)受到影響。
下圖描繪了當(dāng)應(yīng)用程序的第一個(gè)實(shí)例試圖修改數(shù)據(jù)頁面2雖的一個(gè)全局變量時(shí),會(huì)產(chǎn)生的結(jié)果。
17.1.2 在同一個(gè)可執(zhí)行文件或DLL的多個(gè)實(shí)例間共享靜態(tài)數(shù)據(jù)
每個(gè)exe文件和dll文件映象都有許多段組成。比如編譯器會(huì)將代碼放在一個(gè)叫.text段中,將已經(jīng)初始化的數(shù)據(jù)放在.data段中。
除了使用編譯器和鏈接器創(chuàng)建的標(biāo)準(zhǔn)段之外,我們可以自定義自己的段。
#pragma data_seg("sectionname")例如我們可以用下面代碼來合建一個(gè)名為Share的段,它只包含一個(gè)LONG的變量
#pragma data_seg("Shared") LONG g_lInstanceCount = 0; #pragma data_set();注意:編譯器只會(huì)將已經(jīng)初始化的變量放入自己定義的段當(dāng)中,如果上面代碼中g(shù)_lInstanceCount 沒有初始化,則不會(huì)放到我們指定的段之中。
但是Vc++ 編譯器提供了一個(gè)allocate聲明符,它允許我們將未經(jīng)初始化的數(shù)據(jù)放到任何我們想要放入的段中。
#pragma data_seg("Shared") int a = 0; //初始化的會(huì)放在shared段內(nèi) int b ;//沒有初始化的不會(huì)放在shared段內(nèi) #pragma data_set();__declspec(allocate("Shared")) int c = 0; //初始化的會(huì)放在shared段內(nèi) __declspec(allocate("Shared")) int d ; //沒有初始化的也會(huì)放在shared段內(nèi) int e = 0;//初始化確不在Shared段中 int f ; //未初始化不在Shared段中之所以將變量放在一個(gè)單獨(dú)的段中,最常見的原因就是為了共享exe或dll多個(gè)實(shí)例中共享數(shù)據(jù)。
為了共享變量,我們還需要告訴鏈接器要共享這個(gè)段中的變量。通過使用鏈接器命令行/SECTION 來實(shí)現(xiàn)
/SECTION:name,attributes
在本例中我們想要改變Share的屬性,因此應(yīng)用使用下面的開關(guān)
/SECTION:Shared,RWS我們也可以把這條命令嵌入到代碼中
#pragma comment(linker,"/SECTION:Shared,RWS")但是MricroSoft并不鼓勵(lì)使用共享段。1.有潛在安全漏洞? 2,意味著一個(gè)應(yīng)用程序中的錯(cuò)誤可能影響到另一應(yīng)用程序。
17.2 映射到內(nèi)存的數(shù)據(jù)文件
例子:顛倒文件內(nèi)容。
17.2.1 方法1:一個(gè)文件,一塊緩存
分配足夠大的內(nèi)存來存放整個(gè)文件。接著把文件所有內(nèi)容讀取到內(nèi)存中。這時(shí)我們可以對內(nèi)存中的文件內(nèi)容進(jìn)行操作,第第一個(gè)字節(jié)和最后一個(gè)字節(jié)交換,第二個(gè)字節(jié)和倒數(shù)第二個(gè)字節(jié)交換。以此類推。
缺點(diǎn):
1.必須根據(jù)文件大小來分配內(nèi)存,如果文件過大就不行了
2.把已經(jīng)顛倒的內(nèi)容寫入文件時(shí)中斷,那么文件內(nèi)容遭到破壞。避免這種錯(cuò)誤的方法是合建副本,但是這樣會(huì)浪費(fèi)磁盤空間
17.2.2方法2:兩個(gè)文件,一塊緩存
創(chuàng)建一個(gè)長度為0的新文件,接著分配一塊小的內(nèi)部緩存,比如8KB。然后將文件指針定位到原始文件末尾減去8KB的地方,最后8KB的內(nèi)容讀取到緩存中,然后顛倒內(nèi)容寫入到剛才創(chuàng)建的文件中。這個(gè)定位文件指針、讀取文件、寫入文件的過程一直繼續(xù)。直到原始文件達(dá)到起始位置。
缺點(diǎn):
1.每次操作必須對文件指針進(jìn)行定位移動(dòng)操作,因?yàn)樽值乃俣缺鹊谝环N方法要慢
2.處理過程中文件內(nèi)容一直在增大,如果原始文件為1GB,那么在原始文件刪除之前這兩個(gè)文件占用空間用2GB的磁盤空間。
17.2.3 方法3:一個(gè)文件,兩塊緩存
申請兩塊大小為8KB的緩存。程序接著把文件開始的8KB內(nèi)容讀取到第一塊緩存中,把文件末尾的8KB讀到第二塊緩存中。然后后兩塊緩存的內(nèi)容顛倒,并把第一塊緩存的內(nèi)容寫回到文件末尾,把第二塊緩存的內(nèi)容吃回到文件開頭。這個(gè)過程一直繼續(xù)。
與前一種方法相比,這種方法更好的節(jié)省磁盤空間,由于 所有數(shù)據(jù)都讀取自和寫入到了同一個(gè)文件,因此不需要額外的磁盤空間。在內(nèi)存方面這種方式也不差,只使用了16kb內(nèi)存。
17.2.4 方法3:一個(gè)文件,零個(gè)緩存
使用內(nèi)存映射文件來顛倒文件內(nèi)容時(shí),我們先打開文件并向系統(tǒng)預(yù)訂一塊虛擬地址空間區(qū)域。接著讓系統(tǒng)把文件的第一個(gè)字節(jié)映射到該區(qū)域的第一個(gè)字節(jié)。然后就可以訪問這塊虛擬內(nèi)存區(qū)域,就好像它實(shí)際上包含了文件一樣。事實(shí)上,如果要顛倒的是一個(gè)文本文件,而且文件末尾字節(jié)為0,則可以把這個(gè)文件當(dāng)作內(nèi)存中的一個(gè)字符串來處理,在這種情況下,直接調(diào)用c運(yùn)行庫函數(shù)_tcsrev就能顛倒文件中的數(shù)據(jù)。
這種方法最大的優(yōu)點(diǎn)就是讓系統(tǒng)為我們處理文件緩存有關(guān)操作。但遺憾的是,使用內(nèi)存映射文件的時(shí)候,如果操作過程被中途打斷(如斷電),仍然可能導(dǎo)致數(shù)據(jù)被破壞
17.3? 使用內(nèi)存映射文件
(1)創(chuàng)建或打開一個(gè)文件內(nèi)核對象,該對象標(biāo)識(shí)了我們想要用作內(nèi)存映射文件的哪 個(gè)磁盤文件。
(2)創(chuàng)建一個(gè)文件映射內(nèi)核對象,來告訴系統(tǒng)文件的大小以及我們打算如何訪問文件
(3)告訴系統(tǒng)把文件映射對象的部分或全部進(jìn)程地址空間中。
用完內(nèi)存映射文件之后,必須執(zhí)行下面三個(gè)步驟做清理工作
(1)告訴系統(tǒng)從進(jìn)程地址空間中取消對文件映射內(nèi)核對象的映射
(2)關(guān)閉文件映射內(nèi)存對象
(3)關(guān)閉文件內(nèi)核對象
17.3.1 第1步 創(chuàng)建或打開文件內(nèi)核對象
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步 創(chuàng)建文件映射內(nèi)存對象
HANDLE WINAPI CreateFileMapping(__in HANDLE hFile,__in_opt LPSECURITY_ATTRIBUTES lpAttributes,__in DWORD flProtect,__in DWORD dwMaximumSizeHigh,__in DWORD dwMaximumSizeLow,__in_opt LPCTSTR lpName );?
正如本章開頭所指出的,創(chuàng)建一個(gè)內(nèi)存映射文件相當(dāng)于先預(yù)訂一塊地址空間區(qū)域,然后再給區(qū)域調(diào)撥物理存儲(chǔ)器。唯一不同之處在于內(nèi)存映射文件的物理存儲(chǔ)器來自磁盤上的文件,而不是從系統(tǒng)頁交換文件文件中分配的。創(chuàng)建一個(gè)文件映射對象的時(shí)候,系統(tǒng)不會(huì)預(yù)訂一塊地址空間區(qū)域,并把文件映射到該區(qū)域中。但是,當(dāng)系統(tǒng)在映射進(jìn)程地址空間的時(shí)候,它必須知道應(yīng)該給物理存儲(chǔ)器的頁面指定何種保護(hù)屬性。fdwProtect參數(shù)就 是讓我們指定保護(hù)屬性的。
PAGE_READONLY、PAGE_READWRITE、PAGE_WRITECOPY、PAGE_EXECUTE_READ、PAGE_EXECUTE_READWRITE
除了上面的頁保護(hù)屬性,我們可以把5種段屬性fdwProcted參數(shù)按位或起來。
SEC_NOCACHE,SEC_IMAGE,SEC_RESERVE,SEC_COMMIT,SEC_LARGE_PAGES
要使用大頁面內(nèi)存必須滿足以下條件:
- 在調(diào)用CreateFileMapping的時(shí)候必須同時(shí)指定SEC_COMMIT屬性來調(diào)撥內(nèi)存
- 映像的大小必須大于GetLargePageMinimum函數(shù)返回值。
- 必須用PAGE_READWRITE保護(hù)屬性定義映射
- 用戶必須具有并啟用內(nèi)存中鎖定頁面用戶權(quán)限
CreateFileMapping有兩參數(shù)最重要dwMaximumSizeHigh,dwMaximumSizeLow這兩個(gè)參數(shù)告訴系統(tǒng)內(nèi)存映射文件的最大大小,以字節(jié)為單位。如果想要用當(dāng)前文件大小創(chuàng)建一個(gè)文件映射對象時(shí),只要傳0就可以 了。
// TestCreateMappingFile.cpp : 定義應(yīng)用程序的入口點(diǎn)。 // #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); //創(chuàng)建一個(gè)空文件 HANDLE hFileMap = ::CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,100,NULL); //此時(shí)文件大小為100 CloseHandle(hFile);CloseHandle(hFileMap);return 0; }17.3.3 第3步 將文件的數(shù)據(jù)映射到進(jìn)程地址空間
在創(chuàng)建了文件映射對象之后,還需要為文件的數(shù)據(jù)預(yù)訂一塊地址空間區(qū)域并將文件的數(shù)據(jù)做為物理存儲(chǔ)器調(diào)撥給區(qū)域。這可以通過調(diào)用MapViewOfFile來實(shí)現(xiàn)
?
LPVOID WINAPI MapViewOfFile(__in HANDLE hFileMappingObject,__in DWORD dwDesiredAccess,__in DWORD dwFileOffsetHigh,__in DWORD dwFileOffsetLow,__in SIZE_T dwNumberOfBytesToMap );我們主要看一下第三個(gè)參數(shù),第三個(gè)參數(shù)與預(yù)定地址空間和給區(qū)域調(diào)撥存儲(chǔ)器有關(guān),當(dāng)我們把一個(gè)文件映射到地址空間的時(shí)候不必一下子映射整個(gè)文件。可以每次只把一小部分映射到地址空間中。文件中被映射到地址空間的部分被稱為視圖。
把文件映射到地址空間需要告訴系統(tǒng)兩件事情:
1.我們必須告訴系統(tǒng)應(yīng)該把數(shù)據(jù)文件中的哪個(gè)字節(jié)映射到視圖中的第一個(gè)字節(jié)
2.我們必須告訴系統(tǒng)要把數(shù)據(jù)文件的多少映射到地址空間去。
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);//為文件創(chuàng)建一個(gè)文件映射對象HANDLE hFileMapping = ::CreateFileMapping(hFile,NULL,PAGE_WRITECOPPY,0,0,NULL);//以copy-on-write的方式映射到地址空間PBYTE pbFile = (PBYTE) MapViewOfFile(hFileMapping,FILE_MAP_COPY,0,0,0);//從映射讀一個(gè)字節(jié)//當(dāng)讀的時(shí)候系統(tǒng)不會(huì)改變頁屬性,依然保持PAGE_WRITECOPY屬性BYTE bSomeByte = pbaFile[0];//寫一個(gè)字節(jié)//當(dāng)?shù)谝淮螌懭胍粋€(gè)字節(jié)的時(shí)候,系統(tǒng)會(huì)捕獲,會(huì)復(fù)制原內(nèi)容到新的頁面次的頁面有PAGE_READWRITE屬性pbFile[0] = 0;//寫另一個(gè)字節(jié)//由于這個(gè)字節(jié)現(xiàn)在是PAGE_READWRITE屬性,系統(tǒng)會(huì)寫入字節(jié)到頁面pbFile[1] = 0;//當(dāng)完成內(nèi)存映射時(shí),要取消映射 UnmapViewOfFile(pFile);CloseHandle(hFileMapping);CloseHandle(hFile);return 0; }17.3.4 第4步 從進(jìn)程的地址空間撤銷對文件數(shù)據(jù)的映射
BOOL WINAPI UnmapViewOfFile(__in LPCVOID lpBaseAddress );如果需要確保所有修改已經(jīng)被寫入到磁盤中可以調(diào)用
BOOL WINAPI FlushViewOfFile(__in LPCVOID lpBaseAddress,__in SIZE_T dwNumberOfBytesToFlush );17.3.5? 第5步和第6步 關(guān)閉文件映射對象
CloseHandle(hFile);
CloseHandle(hMappfile);
17.3.6? 示例程序
FileReverse程序展示了如何使用內(nèi)存映射文件馬一個(gè)Ansi或Unicode文本文件內(nèi)容顛倒過來主要代碼如下\
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 用內(nèi)存映射文件來處理大文件
下現(xiàn)代碼統(tǒng)計(jì)一個(gè)二進(jìn)制文件中所有值為0的字節(jié)數(shù)
__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 內(nèi)存映射文件和一致性
只要我們映射的是同一個(gè)文件映象,那么系統(tǒng)會(huì)確保各個(gè)視圖中的數(shù)據(jù)是一致的。如果多個(gè)進(jìn)程把同一個(gè)數(shù)據(jù)文件映射到多個(gè)視圖中,那么數(shù)據(jù)也是一致的,這是因?yàn)閿?shù)據(jù)文件中每一個(gè)頁在內(nèi)存中只有一份,但這些內(nèi)面頁面會(huì)被映射到多個(gè)進(jìn)程地址空間而已。
如果打算將打開的文件用于內(nèi)存映射,那么調(diào)用 CraeteFile時(shí)最好傳0給dwShareMode參數(shù)。這等于告訴系統(tǒng)我想要獨(dú)占文件的訪問,使其它進(jìn)程無法訪問此文件。
由于只讀文件不存在 一致性的問題,因此它們非常適合內(nèi)存映射文件。我們絕對不應(yīng)該用內(nèi)存映射文件來跨網(wǎng)絡(luò)共享可寫文件,因?yàn)橄到y(tǒng)無法保證數(shù)據(jù)視圖的一致性。、
17.6 給內(nèi)存映射文件指定基地址
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 內(nèi)存映射文件的實(shí)現(xiàn)細(xì)節(jié)
第一個(gè)進(jìn)程調(diào)用MapViewOfFile時(shí)返回的內(nèi)存地址,與第二個(gè)進(jìn)程調(diào)用MapViewOfFile時(shí)返回的內(nèi)存地址很可能是不相同的。即使兩個(gè)映射同一個(gè)文件,情況也是如此。
?
17.8 使用內(nèi)存映射文件在進(jìn)程間共享數(shù)據(jù)。
讓 我們來看一個(gè)例子:啟動(dòng)應(yīng)用程序。當(dāng)一個(gè)應(yīng)用程序啟動(dòng)時(shí),系統(tǒng)會(huì)先調(diào)用CreateFile來打開磁盤上的.exe文件。接著系統(tǒng)會(huì)調(diào)用 CreateFileMapping來創(chuàng)建文件映射對象。最后系統(tǒng)會(huì)以新創(chuàng)建的進(jìn)程的名義調(diào)用MapViewOfFileEx(并傳入SEC_IMAGE 標(biāo)志),這樣就把.exe文件映射到了進(jìn)程的地址空間中。值所以調(diào)用MapViewOfFileEx而不是MapViewOfFile,是為了把文件映射 到指定的基地址,這個(gè)基地址保存在.exe的PE文件頭中。系統(tǒng)然后創(chuàng)建進(jìn)程的主線程,在映射得到的視圖中取得可執(zhí)行代碼的第一個(gè)字節(jié)的地址,把該地址放 到線程的指令指針中,最后讓CPU開始執(zhí)行其中的代碼。
???? 如 果用戶啟動(dòng)同一個(gè)應(yīng)用程序的第二個(gè)實(shí)例,那么系統(tǒng)會(huì)發(fā)現(xiàn)該.exe文件已經(jīng)有一個(gè)文件映射對象,因此就不會(huì)再創(chuàng)建一個(gè)新的文件對象或文件映射對象。取而代 之的是,系統(tǒng)會(huì)再次映射.exe文件的一個(gè)視圖,但這次是在新創(chuàng)建的進(jìn)程的地址空間中。至此,系統(tǒng)已經(jīng)把同一個(gè)文件同時(shí)映射到了兩個(gè)地址空間中。顯然,由 于物理內(nèi)存中包含.exe文件可執(zhí)行代碼的那些頁面為兩個(gè)進(jìn)程所共享,因此內(nèi)存的使用率更高。
17.9 以頁交換文件為后備存儲(chǔ)器的內(nèi)存映射文件
Microsoft加入了相應(yīng)的支持,讓系統(tǒng)能夠創(chuàng)建以頁交換文件為后備存儲(chǔ)器的內(nèi)存映射文件,這樣就不需要用磁盤上專門的文件來作為后備存儲(chǔ)器了。這種方法和為磁盤文件創(chuàng)建內(nèi)存映射文件的方法幾乎完全相同,甚至更簡單。 一方面,由于不必創(chuàng)建或打開一個(gè)專門的磁盤文件,因此不需要調(diào)用CreateFile。我們只需要像原來那樣調(diào)用CreateFileMapping,并將INVALID_HANDLE_VALUE作為hFile參數(shù)傳入。這告訴系統(tǒng)我們創(chuàng)建的文件映射對象的物理存儲(chǔ)器不是磁盤上的文件,而是希望系統(tǒng)從頁交換文件中調(diào)撥物理存儲(chǔ)器。 所需分配的存儲(chǔ)器大小由CreateFileMapping的dwMaximumSizeHigh和dwMaximumSizeLow參數(shù)決定
Memory-Mapped File Sharing 示例程序
主要關(guān)鍵代碼
void Dlg_OnCommand(HWND hWnd, int id, HWND hWndCtl, UINT codeNotify) {// Handle of the open memory-mapped filestatic 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 以頁交換文件為后備存儲(chǔ)器的內(nèi)存映射文件
我們只需要給CreateFileMapping第一個(gè)參數(shù)傳INVALID_HANDLE_VALUE的系統(tǒng)就以頁交換文件中調(diào)撥物理存儲(chǔ)器。
17.10稀疏調(diào)撥的內(nèi)存映射文件
CreateFileMapping為我們提供了一種方法,即在fdwProtect參數(shù)中指定SEC_RESERVE或SEC_COMMIT標(biāo)志。這樣我們把先前的那個(gè)電子的數(shù)據(jù)作為一個(gè)文件映射對象來共享,但又不希望在一開始就給它調(diào)撥所有物理存儲(chǔ)器。
SEC_RESERVE:系統(tǒng)不會(huì)從頁交換文件中調(diào)撥物理存儲(chǔ)器,它只返回文件映射對象的句柄。現(xiàn)在我們可以調(diào)用MapViewOfFile來給這個(gè)文件映射對象創(chuàng)建一個(gè)視圖。MapViewOfFile會(huì)預(yù)訂地址空間,但不會(huì)調(diào)任何物理存儲(chǔ)器。設(shè)計(jì)圖訪問會(huì)違規(guī)。通過使用SEC_RERVER和VirtualAlloc我們不僅能與其它進(jìn)程共享電子表格的數(shù)組,而且還能高效的使用物理存儲(chǔ)器。如果內(nèi)存映射文件是通過SEC_RESERVE標(biāo)志得到的,便不能用VirtualFree來撤銷調(diào)撥給它的存儲(chǔ)
NT文件系統(tǒng)(NTFS)提供了對稀疏文件的支持。這是一項(xiàng)非常棒的特征。我們可以用這項(xiàng)特性來創(chuàng)建和處理稀疏內(nèi)存映射文件,這樣一來,存儲(chǔ)器就不必總是在頁交換文件中,而可以在普通文件中。
假設(shè)我們要?jiǎng)?chuàng)建一個(gè)內(nèi)存映射文件來存儲(chǔ)錄音數(shù)據(jù)。當(dāng)用戶說話時(shí),我們希望把數(shù)字音頻數(shù)據(jù)寫入到內(nèi)存緩存中,并以磁盤文件為內(nèi)存緩存的后備存儲(chǔ)器。一個(gè)部分調(diào)撥的內(nèi)存映射文件當(dāng)然是最簡單高效的辦法。問題是我們不知道用戶在單擊停止按鈕前會(huì)說多久,可能是五分鐘,但也可以是5小時(shí)。差距不可謂不大!我們需要一個(gè)足夠大的文件來保存這些數(shù)據(jù)。但是,在使用稀疏調(diào)撥的內(nèi)存映射文件時(shí),大小并沒有多大關(guān)系。
轉(zhuǎn)載于:https://www.cnblogs.com/zhangdongsheng/p/3269515.html
總結(jié)
以上是生活随笔為你收集整理的《windows核心编程》 17章 内存映射文件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Fedora8上安装MySQL5.0.
- 下一篇: 【转】IOS动画的实现,其实很简单