記憶體映射檔案

記憶體映射檔案與虛擬記憶體有些類似,通過記憶體映射檔案可以保留一個地址空間的區域,同時將物理存儲器提交給此區域,只是記憶體檔案映射的物理存儲器來自一個已經存在於磁碟上的檔案,而非系統的頁檔案,而且在對該檔案進行操作之前必須首先對檔案進行映射,就如同將整個檔案從磁碟載入到記憶體。

概念

記憶體映射檔案是由一個檔案到一塊記憶體的映射。Win32提供了允許應用程式把檔案映射到一個進程的函式(CreateFileMapping)。這樣,檔案內的數據就可以用記憶體讀/寫指令來訪問,而不是用ReadFileWriteFile這樣的I/O系統函式,從而提高了檔案存取速度。
Win32提供了允許應用程式把檔案映射到一個進程的函式(CreateFileMapping)。記憶體映射檔案與虛擬記憶體有些類似,通過記憶體映射檔案可以保留一個地址空間的區域,同時將物理存儲器提交給此區域,記憶體檔案映射的物理存儲器來自一個已經存在於磁碟上的檔案,而且在對該檔案進行操作之前必須首先對檔案進行映射。使用記憶體映射檔案處理存儲於磁碟上的檔案時,將不必再對檔案執行I/O操作,使得記憶體映射檔案在處理大數據量的檔案時能起到相當重要的作用。

引言

檔案操作是應用程式最為基本的功能之一,Win32API和MFC均提供有支持檔案處理的函式和類,常用的有Win32API的CreateFile()、WriteFile()、ReadFile()和MFC提供的CFile類等。一般來說,以上這些函式可以滿足大多數場合的要求,但是對於某些特殊套用領域所需要的動輒幾十GB、幾百GB、乃至幾TB的海量存儲,再以通常的檔案處理方法進行處理顯然是行不通的。目前,對於上述這種大檔案的操作一般是以記憶體映射檔案的方式來加以處理的,本文下面將針對這種Windows核心編程技術展開討論。

作用

這種函式最適用於需要讀取檔案並且對檔案內包含的信息做語法分析的應用程式,如對輸入檔案進行語法分析的彩色語法編輯器編譯器等。把檔案映射後進行讀和分析,能讓應用程式使用記憶體操作來操縱檔案,而不必在檔案里來回地讀、寫、移動檔案指針。
有些操作,如放棄“讀”一個字元,在以前是相當複雜的,用戶需要處理緩衝區的刷新問題。在引入了映射檔案之後,就簡單的多了。應用程式要做的只是使指針減少一個值。
映射檔案的另一個重要套用就是用來支持永久命名的共享記憶體。要在兩個應用程式之間共享記憶體,可以在一個應用程式中創建一個檔案並映射之,然後另一個應用程式可以通過打開和映射此檔案把它作為共享的記憶體來使用。

適用範圍及工作

適用範圍
這種函式最適用於需要讀取檔案並且對檔案內包含的信息做語法分析的應用程式,如:
對輸入檔案進行語法分析的彩色語法編輯器,編譯器等。
工作
把檔案映射後進行讀和分析,能讓應用程式使用記憶體操作來操縱檔案,而不必在檔案里來回地讀、寫、移動檔案指針。

相關函式

在使用記憶體映射檔案時,所使用的API函式主要就是前面提到過的那幾個函式,下面分別對其進行介紹:
HANDLECreateFile(LPCTSTRlpFileName,
DWORDdwDesiredAccess,
DWORDdwShareMode,
LPSECURITY_ATTRIBUTESlpSecurityAttributes,
DWORDdwCreationDisposition,
DWORDdwFlagsAndAttributes,
HANDLEhTemplateFile);
函式CreateFile()即使是在普通的檔案操作時也經常用來創建、打開檔案,在處理記憶體映射檔案時,該函式來創建/打開一個檔案核心對象,並將其句柄返回,在調用該函式時需要根據是否需要數據讀寫和檔案的共享方式來設定參數dwDesiredAccess和dwShareMode,錯誤的參數設定將會導致相應操作時的失敗。
HANDLECreateFileMapping(HANDLEhFile,
LPSECURITY_ATTRIBUTESlpFileMappingAttributes,
DWORDflProtect,
DWORDdwMaximumSizeHigh,
DWORDdwMaximumSizeLow,
LPCTSTRlpName);
CreateFileMapping()函式創建一個檔案映射核心對象,通過參數hFile指定待映射到進程地址空間的檔案句柄(該句柄由CreateFile()函式的返回值獲取)。由於記憶體映射檔案的物理存儲器實際是存儲於磁碟上的一個檔案,而不是從系統的頁檔案中分配的記憶體,所以系統不會主動為其保留地址空間區域,也不會自動將檔案的存儲空間映射到該區域,為了讓系統能夠確定對頁面採取何種保護屬性,需要通過參數flProtect來設定,保護屬性PAGE_READONLY、PAGE_READWRITE和PAGE_WRITECOPY分別表示檔案映射對象被映射後,可以讀取、讀寫檔案數據。在使用PAGE_READONLY時,必須確保CreateFile()採用的是GENERIC_READ參數;PAGE_READWRITE則要求CreateFile()採用的是GENERIC_READ|GENERIC_WRITE參數;至於屬性PAGE_WRITECOPY則只需要確保CreateFile()採用了GENERIC_READ和GENERIC_WRITE其中之一即可。DWORD型的參數dwMaximumSizeHigh和dwMaximumSizeLow也是相當重要的,指定了檔案的最大位元組數,由於這兩個參數共64位,因此所支持的最大檔案長度為16EB,幾乎可以滿足任何大數據量檔案處理場合的要求。
LPVOIDMapViewOfFile(HANDLEhFileMappingObject,
DWORDdwDesiredAccess,
DWORDdwFileOffsetHigh,
DWORDdwFileOffsetLow,
DWORDdwNumberOfBytesToMap);
MapViewOfFile()函式負責把檔案數據映射到進程的地址空間,參數hFileMappingObject為CreateFileMapping()返回的檔案映像對象句柄。參數dwDesiredAccess則再次指定了對檔案數據的訪問方式,而且同樣要與CreateFileMapping()函式所設定的保護屬性相匹配。雖然這裡一再對保護屬性進行重複設定看似多餘,但卻可以使應用程式能更多的對數據的保護屬性實行有效控制。MapViewOfFile()函式允許全部或部分映射檔案,在映射時,需要指定數據檔案的偏移地址以及待映射的長度。其中,檔案的偏移地址由DWORD型的參數dwFileOffsetHigh和dwFileOffsetLow組成的64位值來指定,而且必須是作業系統的分配粒度的整數倍,對於Windows作業系統,分配粒度固定為64KB。當然,也可以通過如下代碼來動態獲取當前作業系統的分配粒度:
SYSTEM_INFOsinf;
GetSystemInfo(&sinf);
DWORDdwAllocationGranularity=sinf.dwAllocationGranularity;
參數dwNumberOfBytesToMap指定了數據檔案的映射長度,這裡需要特別指出的是,對於Windows9x作業系統,如果MapViewOfFile()無法找到足夠大的區域來存放整個檔案映射對象,將返回空值(NULL);但是在Windows2000下,MapViewOfFile()只需要為必要的視圖找到足夠大的一個區域即可,而無須考慮整個檔案映射對象的大小。
在完成對映射到進程地址空間區域的檔案處理後,需要通過函式UnmapViewOfFile()完成對檔案數據映像的釋放,該函式原型聲明如下:
BOOLUnmapViewOfFile(LPCVOIDlpBaseAddress);
唯一的參數lpBaseAddress指定了返回區域的基地址,必須將其設定為MapViewOfFile()的返回值。在使用了函式MapViewOfFile()之後,必須要有對應的UnmapViewOfFile()調用,否則在進程終止之前,保留的區域將無法釋放。除此之外,前面還曾由CreateFile()和CreateFileMapping()函式創建過檔案核心對象和檔案映射核心對象,在進程終止之前有必要通過CloseHandle()將其釋放,否則將會出現資源泄漏的問題。
除了前面這些必須的API函式之外,在使用記憶體映射檔案時還要根據情況來選用其他一些輔助函式。例如,在使用記憶體映射檔案時,為了提高速度,系統將檔案的數據頁面進行高速快取,而且在處理檔案映射視圖時不立即更新檔案的磁碟映像。為解決這個問題可以考慮使用FlushViewOfFile()函式,該函式強制系統將修改過的數據部分或全部重新寫入磁碟映像,從而可以確保所有的數據更新能及時保存到磁碟。

套用示例

下面結合一個具體的實例來進一步講述記憶體映射檔案的使用方法。該實例從連線埠接收數據,並實時將其存放於磁碟,由於數據量大(幾十GB),在此選用記憶體映射檔案進行處理。下面給出的是位於工作執行緒MainProc中的部分主要代碼,該執行緒自程式運行時啟動,當連線埠有數據到達時將會發出事件hEvent[0],WaitForMultipleObjects()函式等待到該事件發生後將接收到的數據保存到磁碟,如果終止接收將發出事件hEvent[1],事件處理過程將負責完成資源的釋放和檔案的關閉等工作。下面給出此執行緒處理函式的具體實現過程:
……
//創建檔案核心對象,其句柄保存於hFile
HANDLEhFile=CreateFile("Recv1.zip",
GENERIC_WRITE|GENERIC_READ,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
//創建檔案映射核心對象,句柄保存於hFileMapping
HANDLEhFileMapping=CreateFileMapping(hFile,NULL,PAGE_READWRITE,
0,0x4000000,NULL);
//釋放檔案核心對象
CloseHandle(hFile);
//設定大小、偏移量等參數
__int64qwFileSize=0x4000000;
__int64qwFileOffset=0;
__int64T=600*sinf.dwAllocationGranularity;
DWORDdwBytesInBlock=1000*sinf.dwAllocationGranularity;
//將檔案數據映射到進程的地址空間
PBYTEpbFile=(PBYTE)MapViewOfFile(hFileMapping,
FILE_MAP_ALL_ACCESS,
(DWORD)(qwFileOffset>>32),(DWORD)(qwFileOffset&0xFFFFFFFF),dwBytesInBlock);
while(bLoop)
{
//捕獲事件hEvent[0]和事件hEvent[1]
DWORDret=WaitForMultipleObjects(2,hEvent,FALSE,INFINITE);
ret-=WAIT_OBJECT_0;
switch(ret)
{
//接收數據事件觸發
case0:
//從連線埠接收數據並保存到記憶體映射檔案
nReadLen=syio_Read(port[1],pbFile+qwFileOffset,QueueLen);
qwFileOffset+=nReadLen;
//當數據寫滿60%時,為防數據溢出,需要在其後開闢一新的映射視圖
if(qwFileOffset>T)
{
T=qwFileOffset+600*sinf.dwAllocationGranularity;
UnmapViewOfFile(pbFile);
pbFile=(PBYTE)MapViewOfFile(hFileMapping,
FILE_MAP_ALL_ACCESS,
(DWORD)(qwFileOffset>>32),(DWORD)(qwFileOffset&0xFFFFFFFF),dwBytesInBlock);
}
break;
//終止事件觸發
case1:
bLoop=FALSE;
//從進程的地址空間撤消檔案數據映像
UnmapViewOfFile(pbFile);
//關閉檔案映射對象
CloseHandle(hFileMapping);
break;
}
}

在終止事件觸發處理過程中如果只簡單的執行UnmapViewOfFile()和CloseHandle()函式將無法正確標識檔案的實際大小,即如果開闢的記憶體映射檔案為30GB,而接收的數據只有14GB,那么上述程式執行完後,保存的檔案長度仍是30GB。也就是說,在處理完成後還要再次通過記憶體映射檔案的形式將檔案恢復到實際大小,下面是實現此要求的主要代碼:
//創建另外一個檔案核心對象
hFile2=CreateFile("Recv.zip",
GENERIC_WRITE|GENERIC_READ,
FILE_SHARE_READ,
NULL,
CREATE_ALWAYS,
FILE_FLAG_SEQUENTIAL_SCAN,
NULL);
//以實際數據長度創建另外一個檔案映射核心對象
hFileMapping2=CreateFileMapping(hFile2,
NULL,
PAGE_READWRITE,
0,
(DWORD)(qwFileOffset&0xFFFFFFFF),
NULL);
//關閉檔案核心對象
CloseHandle(hFile2);
//將檔案數據映射到進程的地址空間
pbFile2=(PBYTE)MapViewOfFile(hFileMapping2,
FILE_MAP_ALL_ACCESS,
0,0,qwFileOffset);
//將數據從原來的記憶體映射檔案複製到此記憶體映射檔案
memcpy(pbFile2,pbFile,qwFileOffset);
file://從進程的地址空間撤消檔案數據映像
UnmapViewOfFile(pbFile);
UnmapViewOfFile(pbFile2);
//關閉檔案映射對象
CloseHandle(hFileMapping);
CloseHandle(hFileMapping2);
//刪除臨時檔案
DeleteFile("Recv1.zip");

評價

經實際測試,記憶體映射檔案在處理大數據量檔案時表現出了良好的性能,比通常使用CFile類和ReadFile()和WriteFile()等函式的檔案處理方式具有明顯的優勢。本文所述代碼在Windows98下由MicrosoftVisualC++6.0編譯通過。

相關詞條

相關搜尋

熱門詞條

聯絡我們