內(nèi)存映射文件允許開發(fā)人員預(yù)訂一塊地址空間并為該區(qū)域調(diào)撥物理存儲(chǔ)器,與虛擬內(nèi)存不同的是,內(nèi)存映射文件的物理存儲(chǔ)器來自磁盤中的文件,而非系統(tǒng)的頁交換文件。將文件映射到內(nèi)存中后,我們就可以在內(nèi)存中操作他們了,就像他們被載入內(nèi)存中一樣。
內(nèi)存映射文件主要有三方面的用途:
1:系統(tǒng)使用內(nèi)存映射文件來將exe或是dll文件本身作為后備存儲(chǔ)器,而非系統(tǒng)頁交換文件,這大大節(jié)省了系統(tǒng)頁交換空間,由于不需要將exe或是dll文件加載到頁系統(tǒng)交換文件,也提高了啟動(dòng)速度。
2:使用內(nèi)存映射文件來將磁盤上的文件映射到進(jìn)程的空間區(qū)域,使得開發(fā)人員操作文件就像操作內(nèi)存數(shù)據(jù)一樣,將對(duì)文件的操作交由操作系統(tǒng)來管理,簡(jiǎn)化了開發(fā)人員的工作。
3:windows提供了多種進(jìn)程間通信的方法,但他們都是基于內(nèi)存映射文件來實(shí)現(xiàn)的。
這里首先討論第一種情況:
在一個(gè)exe文件運(yùn)行之前,系統(tǒng)首先為新進(jìn)程創(chuàng)建一個(gè)進(jìn)程內(nèi)核對(duì)象,同時(shí)預(yù)訂一塊足夠大的地址空間來容納該文件。然后,對(duì)該地址空間進(jìn)行標(biāo)注,注明他的后備存儲(chǔ)器來自exe文件,而非系統(tǒng)的頁交換文件。此措施對(duì)提高系統(tǒng)性能有重大意義。
一個(gè)可執(zhí)行文件,當(dāng)他有多個(gè)實(shí)例同時(shí)運(yùn)行,系統(tǒng)在創(chuàng)建另一個(gè)新的實(shí)例時(shí),僅僅是打開了另一個(gè)內(nèi)存映射視圖。所有這些視圖都來自于同一個(gè)文件映射對(duì)象(即可執(zhí)行文件本身)。
當(dāng)新實(shí)例開始運(yùn)行時(shí),系統(tǒng)只是把包含應(yīng)用程序代碼和數(shù)據(jù)的虛擬內(nèi)存頁面映射到了他的地址空間中,當(dāng)其中的一個(gè)實(shí)例試圖去修改數(shù)據(jù)段中的數(shù)據(jù),如果不采取有效措施,那么應(yīng)用程序的所有其他實(shí)例的內(nèi)存都會(huì)被修改,這是不合常理的。因此windows采取了一種叫做寫時(shí)復(fù)制的特性,來防止這種情況的發(fā)生。
系統(tǒng)將可執(zhí)行文件映射到地址空間中時(shí),會(huì)計(jì)算有多少頁面是可寫的。(通常包含數(shù)據(jù)的頁面被標(biāo)記為PAGE_READWRITE屬性,它們是可寫的)然后會(huì)從系統(tǒng)的頁交換文件中調(diào)撥物理存儲(chǔ)器,來容納這些可寫的頁面。但是系統(tǒng)只是調(diào)撥這些頁面,并不會(huì)實(shí)際載入頁面的內(nèi)容,只有當(dāng)寫入可寫頁面的時(shí)候才會(huì)真正實(shí)際載入。(后面會(huì)詳細(xì)介紹)
任何時(shí)候當(dāng)應(yīng)用程序試圖寫入內(nèi)存映射文件的時(shí)候,系統(tǒng)會(huì)截獲此類嘗試,接著從先前在系統(tǒng)頁交換文件中分配的空間中取出一頁,復(fù)制要寫入頁面的內(nèi)容,讓應(yīng)用程序?qū)懭雱倓倧南到y(tǒng)頁交換文件中分配的頁,而不是內(nèi)存映射文件中的頁。由于寫入到的區(qū)域僅僅是內(nèi)存映射文件的副本,不會(huì)對(duì)內(nèi)存映射文件寫入,這樣就保證了其他實(shí)例不會(huì)受到任何影響。另外需要注意的是,內(nèi)存映射文件的副本(在系統(tǒng)頁交換文件中)被映射到了新實(shí)例的地址空間區(qū)域的同一位置。
以上介紹的是在同一個(gè)可執(zhí)行文件的多個(gè)實(shí)例之間不會(huì)共享數(shù)據(jù)的情況。有時(shí)候在多個(gè)實(shí)例之間共享數(shù)據(jù)非常有用,可以大大提高編程效率。接著我們就討論如何在一個(gè)可執(zhí)行文件的多個(gè)實(shí)例中共享數(shù)據(jù)。
我們知道默認(rèn)情況下,我們定義的初始化數(shù)據(jù)被放到了數(shù)據(jù)段,未初始化的數(shù)據(jù)放到了.bss段。除了使用這些標(biāo)準(zhǔn)段之外,我們也可以將數(shù)據(jù)放在我們自己的段中。
首先,就要知道如何創(chuàng)建一個(gè)段。
#pragm data_seg("sectionname")//創(chuàng)建一個(gè)名為sectionname的段。
看例子:
#pragm data_seg("newsection")//此處創(chuàng)建一個(gè)名為newsection的段
int a=23;//向此段中添加變量。
#pragm data_seg()//結(jié)束添加
此例創(chuàng)建了一個(gè)名為newsection的段,并向此段添加int類型變量a。#pragm data_seg()用于結(jié)束向段中添加數(shù)據(jù)。
要注意一點(diǎn)編譯器只會(huì)將以初始化的變量放入我們的段中,如上例中的a。
如果這樣:
#pragm data_seg("newsection")
int a=23;
int b;
#pragm data_seg()
b是不會(huì)被添加到段newsection中的。而是放到默認(rèn)的標(biāo)準(zhǔn)段中。
雖然編譯器只會(huì)將初始化的變量放入自定義段中,但是我們可以強(qiáng)制的將一個(gè)未初始化的數(shù)據(jù)放我任何我們想放入的段中。
_declspec(allocate("newsection") ) int b;將b放入newsection中。
僅僅新建一個(gè)段,并將要共享的數(shù)據(jù)放入新建段中是不夠的,還需要將該段聲明為共享段。
我們可以使用:
1: #pragm comment(linker,"/SECTION:newsection,RWS")
2:鏈接器開關(guān):/SECTON:newsecton,RWS
其中R表示READ,W表示W(wǎng)RITE,S表示SHARE。他們?yōu)閚ewsection指定的屬性。SHARE即為共享的意思,意思是把此段讓所有實(shí)例共享。
放入共享段的變量在多個(gè)實(shí)例中只有一份,不會(huì)再向數(shù)據(jù)段中的變量一樣:每個(gè)實(shí)例都有一個(gè)副本。所以任何實(shí)例都可以修改它們。非常重要的一點(diǎn)就是:由于多個(gè)實(shí)例可以同時(shí)修改共享段中的變量,因此要注意同步問題??梢圆扇【€程同步中所介紹的一些方法。
現(xiàn)在來討論內(nèi)存映射文件介紹的第二個(gè)用途:內(nèi)存映射磁盤數(shù)據(jù)文件。
要使用內(nèi)存映射磁盤文件需要三個(gè)步驟:
1:創(chuàng)建或打開一個(gè)文件內(nèi)核對(duì)象。
2:創(chuàng)建一個(gè)文件映射內(nèi)核對(duì)象。
3:將文件映射對(duì)象映射到進(jìn)程地址空間。
對(duì)于第一點(diǎn),可以調(diào)用CreateFile或是OpenFile,很簡(jiǎn)單,此處不作介紹。
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 );
第二點(diǎn):可以調(diào)用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 );
第一個(gè)hFile為要映射到進(jìn)程地址空間中的文件句柄,CreateFile或是OpenFile返回。
第二個(gè)psa為安全屬性,一般都傳NULL,表示使用默認(rèn)安全屬性。
第三個(gè)為fdwProtect保護(hù)屬性,指定當(dāng)將文件映射到進(jìn)程地址空間的時(shí)候,應(yīng)該給物理存儲(chǔ)器的頁面指定何種保護(hù)屬性。
第四個(gè),第五個(gè)參數(shù)告訴系統(tǒng)內(nèi)存映射文件的最大大小。
第四個(gè)參數(shù)dwMaximumSizeHigh為表示文件大小的64位整數(shù)的高字節(jié),dwMaximumSizeLow為低字節(jié)。對(duì)于小于4G的文件來說,高字節(jié)當(dāng)然為0.
如果要以文件的當(dāng)前大小創(chuàng)建一個(gè)映射對(duì)象時(shí),只要將他們?cè)O(shè)為0就可以。如果要文件中添加數(shù)據(jù),一定要使指定的大小大于文件的真實(shí)大小。
第六個(gè)參數(shù)為文件映射內(nèi)核對(duì)象的名稱。用于跨進(jìn)程共享命名內(nèi)核對(duì)象。(請(qǐng)參考windows核心編程 第五版 第三章)需要特別強(qiáng)調(diào)下,如果為flProtect指定PAGE_READWRITE屬性,當(dāng)文件的真實(shí)大小小于參數(shù)中指定的大小的時(shí)候,CreateFileMapping會(huì)自動(dòng)增大文件大小。為的是在將文件作為內(nèi)存映射文件后,物理存儲(chǔ)器已經(jīng)就緒。向其寫入數(shù)據(jù)不會(huì)發(fā)生錯(cuò)誤。如果指定PAGE_READONLY或是PAGE_WRITECOPY,那么傳入的大小不能大于文件的真實(shí)大小,因?yàn)槲覀冎徊⒉荒芟蛭募性黾訑?shù)據(jù)。
第三步:將文件映射到進(jìn)程地址空間。
MapViewOfFile
LPVOID WINAPI MapViewOfFile( __in HANDLE hFileMappingObject, __in DWORD dwDesiredAccess, __in DWORD dwFileOffsetHigh, __in DWORD dwFileOffsetLow, __in SIZE_T dwNumberOfBytesToMap );
第一個(gè)參數(shù)hFileMappingObject即為CreateFileMapping或是OpenFileMapping返回的文件映射內(nèi)核對(duì)象句柄。
第二個(gè)參數(shù)是訪問數(shù)據(jù)的方式。他們依賴于CreateFileMapping 和CreateFile傳遞的訪問方式。
第三個(gè)和第四個(gè)參數(shù)告訴系統(tǒng)把數(shù)據(jù)文件中的的那些內(nèi)容映射到進(jìn)程地址空間中。他們分別為要映射文件的偏移 量,是64位的,分別表示高32位和低32位。
第五個(gè)參數(shù)指明要把磁盤文件的多少數(shù)據(jù)映射到進(jìn)程地址空間中。如果指定為0,系統(tǒng)會(huì)把文件中從偏移量開始直到文件末尾的數(shù)據(jù)全部映射到進(jìn)程地址空間中。
當(dāng)調(diào)用MapViewOfFile時(shí)指定FILE_MAP_COPY標(biāo)志,系統(tǒng)會(huì)從系統(tǒng)頁交換文件調(diào)撥物理存儲(chǔ)器,大小有dwNumberOfBytesToMap指定。只要我們不執(zhí)行讀取數(shù)據(jù)之外的任何操作,系統(tǒng)就不會(huì)使用從頁交換文件中調(diào)撥頁面 。但是一旦有任何線程寫入文件映射視圖的任何地址,系統(tǒng)就會(huì)從已經(jīng)調(diào)撥的頁交換文件中選擇一個(gè)頁面把原始數(shù)據(jù)復(fù)制到頁交換文件中的頁面,然后讓線程進(jìn)行修改這個(gè)副本,再將此頁面映射到進(jìn)程地址空間中。因此任何線程都只會(huì)修改數(shù)據(jù)的副本而不會(huì)修改原始數(shù)據(jù)。
當(dāng)不再需要把文件中的數(shù)據(jù)映射到進(jìn)程的地址空間的時(shí)候,可以調(diào)用UnmapViewOfFile 來釋放映射的數(shù)據(jù)。
BOOL WINAPI UnmapViewOfFile( __in LPCVOID lpBaseAddress );
lpBaseAddress用于指定區(qū)域的基地址,必須和MapViewOfFile相同。
為了提高運(yùn)行速度,系統(tǒng)會(huì)對(duì)文件數(shù)據(jù)的頁面進(jìn)行緩存處理,也就是說對(duì)文件映射對(duì)象映射后的視圖進(jìn)行修改,不會(huì)立即反映到數(shù)據(jù)文件中。如果想要立即反映到數(shù)據(jù)文件中,可以調(diào)用FlushViewOfFile。來強(qiáng)制系統(tǒng)把修改過的數(shù)據(jù)協(xié)會(huì)磁盤文件。
如果視圖最初使用FILE_MAP_COPY標(biāo)志來映射的,那么對(duì)數(shù)據(jù)文件的修改實(shí)際上對(duì)系統(tǒng)頁交換文件中的副本進(jìn)行的修改。請(qǐng)參考紅色字段。如果這種情況下調(diào)用FlushViewOfFile,系統(tǒng)不會(huì)將做出的修改保存到磁盤文件中,而會(huì)直接釋放系統(tǒng)頁交換文件中的相關(guān)數(shù)據(jù),導(dǎo)致數(shù)據(jù)丟失。這只是FILE_MAP_COPY的特性,為了防止數(shù)據(jù)丟失可以用其他標(biāo)志進(jìn)行映射。
最后不要忘記調(diào)用CloseHandle關(guān)閉文件內(nèi)核對(duì)象和文件映射內(nèi)核對(duì)象。
如果文件非常大,一次無法全部映射到進(jìn)程的地址空間中,這是該怎么辦呢?
此時(shí)可以每次只映射一部分文件到進(jìn)程空間,使用完畢后,撤銷映射。再映射下一部分,使用完畢后再次撤銷映射。如此循環(huán)往復(fù)。直至將整個(gè)文件映射完畢 。
系統(tǒng)允許我們把一個(gè)數(shù)據(jù)文件映射到多個(gè)視圖中。如果我們使用的是同一個(gè)文件映射對(duì)象映射到不同視圖,一旦有一個(gè)視圖中的數(shù)據(jù)被修改,其他視圖中會(huì)立刻更新進(jìn)而顯示更新后的視圖。也就是說各個(gè)視圖中的數(shù)據(jù)是一致的。為什么各個(gè)視圖的數(shù)據(jù)都是一致的呢?
因?yàn)樗麄兌际菑耐粋€(gè)文件映射對(duì)象映射的,數(shù)據(jù)文件在內(nèi)存中只有一份,卻映射到了不同視圖中。但要注意,此處有一前提,就是各個(gè)視圖都是有同一文件映射對(duì)象映射的,如果是同一數(shù)據(jù)文件為后備存儲(chǔ)器創(chuàng)建不同文件映射對(duì)象,那就不能保證他們的數(shù)據(jù)是一致的了。為了防止這種情況,可以在CreateFile時(shí)將dwShareMode 設(shè)為獨(dú)占對(duì)文件的訪問。從而防止不一致性。
注意:在使用完內(nèi)存映射文件后一定要先撤銷對(duì)視圖的映射和關(guān)閉文件映射對(duì)象句柄,在對(duì)執(zhí)行打開文件等對(duì)文件進(jìn)行的操作。否則將會(huì)造成執(zhí)行文件操作的失敗。另外,執(zhí)行的操作又失敗的可能性,一定要檢查。(2011年11月24日,在寫文件逆置程序時(shí),撤銷對(duì)視圖的映射后,沒有關(guān)閉文件映射對(duì)象的句柄就執(zhí)行文件操作,結(jié)果這些文件操作都失敗了。但是沒有執(zhí)行判斷,花了好久才查到錯(cuò)誤。)
下面來討論第三個(gè)問題:內(nèi)存映射文件實(shí)現(xiàn)進(jìn)程間共享數(shù)據(jù)。
如果我們?cè)趧?chuàng)建文件映射對(duì)象時(shí)為它命名,那么就可以實(shí)現(xiàn)在不同進(jìn)程間訪問同一文件映射內(nèi)核對(duì)象了。但要注意,要在不同進(jìn)程分別調(diào)用MapViewOfFile,來將同一命名文件映射內(nèi)核對(duì)象,映射到各自的進(jìn)程地址空間中。
到此,以我們目前掌握的知識(shí),我們知道要實(shí)現(xiàn)在多個(gè)進(jìn)程間共享數(shù)據(jù),要?jiǎng)?chuàng)建一個(gè)文件對(duì)象和一個(gè)命名的文件映射內(nèi)核對(duì)象。然后在另一個(gè)進(jìn)程內(nèi)將此命名的內(nèi)核對(duì)象映射到本進(jìn)程。這一系列的步驟說明:如果我們要在多個(gè)進(jìn)程間共享數(shù)據(jù),我們就必須創(chuàng)建文件,將數(shù)據(jù)保存在文件中,然后創(chuàng)建文件對(duì)象,文件映射對(duì)象。。。。這是很繁瑣的。
Microsoft意識(shí)到了這一點(diǎn),為我們提供了支持:讓系統(tǒng)創(chuàng)建以頁交換文件為后備存儲(chǔ)器的內(nèi)存映射文件。這就是說當(dāng)實(shí)現(xiàn)進(jìn)程共享數(shù)據(jù)時(shí),不再需要?jiǎng)?chuàng)建以磁盤文件為后備存儲(chǔ)器的文件映射對(duì)象。此時(shí),文件映射對(duì)象的后備存儲(chǔ)器來自系統(tǒng)頁交換文件。這種方法和為磁盤文件創(chuàng)建內(nèi)存映射文件幾乎完全相同。區(qū)別就是:此時(shí)無需創(chuàng)建文件對(duì)象,在創(chuàng)建文件映射對(duì)象時(shí),只需將INVALID_HANDLE_VALUE傳給hFile就可以了。他告訴系統(tǒng)要以系統(tǒng)頁交換文件中調(diào)撥物理存儲(chǔ)器。以后的步驟跟為磁盤文件創(chuàng)建內(nèi)存映射文件相同。
很簡(jiǎn)單,不是嗎?來看個(gè)例子:
HANDLE hFile=CreateFile(...)
HANDLE hMap=CreateFileMapping(hFile........);
if(hMap==NULL)
{
.....................
}
看出來什么問題嗎?
我們知道調(diào)用CreateFile失敗的時(shí)候,返回的是INVALID_HANDLE_VALUE,而此處沒有判斷文件對(duì)象是否成功,就直接創(chuàng)建文件映射對(duì)象,一旦創(chuàng)建文件對(duì)象失敗,hFile就是INVALID_HANDLE_VALUE,系統(tǒng)會(huì)以為程序員要?jiǎng)?chuàng)建以系統(tǒng)頁交換文件為后備存儲(chǔ)器的內(nèi)存映射文件,而不是為磁盤文件創(chuàng)建內(nèi)存映射文件。這就導(dǎo)致了錯(cuò)誤。所以在可能導(dǎo)致失敗的函數(shù)執(zhí)行之后一定要進(jìn)程判斷。
參考自《windows核心編程—第五版》第三部分,以上僅僅是個(gè)人總結(jié),如有紕漏,請(qǐng)不吝賜教,謝謝。同時(shí),想結(jié)交志同道合之士,交流windows核心編程的學(xué)習(xí)。
聯(lián)系客服