如果由于某個原因系統無法映射. e x e和所有必要的D L L文件,那么系統就會向用戶顯示一個消息框,并且釋放進程的地址空間和進程對象。
當所有的. e x e和D L L文件都被映射到進程的地址空間之后,系統就可以開始執行. e x e文件的啟動代碼。當. e x e文件被映射后,系統將負責所有的分頁、緩沖和高速緩存的處理。
在可執行文件或DLL的多個實例之間共享靜態數據 (通過定義共享的節)
全局數據和靜態數據不能被同一個. e x e或D L L文件的多個映像共享,這是個安全的默認設置。但是,在某些情況下,讓一個. e x e文件的多個映像共享一個變量的實例是非常有用和方便的。例如,Wi n d o w s沒有提供任何簡便的方法來確定用戶是否在運行應用程序的多個實例。但是,如果能夠讓所有實例共享單個全局變量,那么這個全局變量就能夠反映正在運行的實例的數量。
?內存映射數據文件
操作系統使得內存能夠將一個數據文件映射到進程的地址空間中。因此,對大量的數據進行操作是非常方便的。
為了理解用這種方法來使用內存映射文件的功能,讓我們看一看如何用4種方法來實現一個程序,以便將文件中的所有字節的順序進行倒序。
方法1:一個文件,一個緩存
第一種方法也是理論上最簡單的方法,它需要分配足夠大的內存塊來存放整個文件。該文件被打開,它的內容被讀入內存塊,然后該文件被關閉。文件內容進入內存后,我們就可以對所有字節的順序進行倒序,方法是將第一個字節倒騰為最后一個字節,第二個字節倒騰為倒數第二個字節,依次類推。這個倒騰操作將一直進行下去直到文件的中間位置。當所有的字節都已經倒騰之后,就可以重新打開該文件,并用內存塊的內容來改寫它的內容。
這種方法實現起來非常容易,但是它有兩個缺點。首先,必須分配一個與文件大小相同的內存塊。如果文件比較小,那么這沒有什么問題。但是如果文件非常大,比如說有2 G B大,那該怎么辦呢?一個3 2位的系統不允許應用程序提交那么大的物理內存塊。因此大文件需要使用不同的方法。
第二,如果進程在運行過程的中間被中斷,也就是說當倒序后的字節被重新寫入該文件時進程被中斷,那么文件的內容就會遭到破壞。防止出現這種情況的最簡單的方法是在對它的內容進行倒序之前先制作一個原始文件的拷貝。如果整個進程運行成功,那么可以刪除該文件的拷貝。這種方法需要更多的磁盤空間。
?方法2:兩個文件,一個緩存
在第二種方法中,你打開現有的文件,并且在磁盤上創建一個長度為0的新文件。然后分配一個比較小的內部緩存,比如說8 KB。你找到離原始文件結尾還有8 KB的位置,將這最后的8 KB讀入緩存,將字節倒序,再將緩存中的內容寫入新創建的文件。這個尋找、讀入、倒序和寫入的操作過程要反復進行,直到到達原始文件的開頭。如果文件的長度不是8 KB的倍數,那么必須進行某些特殊的處理。當原始文件完全處理完畢之后,將原始文件和新文件關閉,并刪除原始文件。
這種方法實現起來比第一種方法要復雜一些。它對內存的使用效率要高得多,因為它只需要分配一個8 KB的緩存塊,但是它存在兩個大問題。首先,它的處理速度比第一種方法要慢,原因是在每個循環操作過程中,在執行讀入操作之前,必須對原始文件進行尋找操作。第二,這種方法可能要使用大量的硬盤空間。如果原始文件是400 MB,那么隨著進程的不斷運行,新文件就會增大為400 MB。在原始文件被刪除之前,兩個文件總共需要占用800 MB的磁盤空間。這比應該需要的空間大400 MB。由于存在這個缺點,因此引來了下一個方法。
?方法3:一個文件,兩個緩存
如果使用這個方法,那么我們假設程序初始化時分配了兩個獨立的8 KB緩存。程序將文件的第一個8 KB讀入一個緩存,再將文件的第二個8 KB 讀入另一個緩存。然后進程將兩個緩存的內容進行倒序,并將第一個緩存的內容寫回文件的結尾處,將第二個緩存的內容寫回同一個文件的開始處。每個迭代操作不斷進行(以8 KB為單位,從文件的開始和結尾處移動文件塊)。如果文件的長度不是16 KB的倍數,并且有兩個8 KB的文件塊相重疊,那么就需要進行一些特殊的處理。這種特殊處理比上一種方法中的特殊處理更加復雜,不過這難不倒經驗豐富的編程員。
與前面的兩種方法相比,這種方法在節省硬盤空間方面有它的優點。由于所有內容都是從同一個文件讀取并寫入同一個文件,因此不需要增加額外的磁盤空間,至于內存的使用,這種方法也不錯,它只需要使用16 KB的內存。當然,這種方法也許是最難實現的方法。與第一種方法一樣,如果進程被中斷,本方法會導致數據文件被破壞。
下面讓我們來看一看如何使用內存映射文件來完成這個過程。
方法4:一個文件,零緩存
當使用內存映射文件對文件內容進行倒序時,你打開該文件,然后告訴系統將虛擬地址空間的一個區域進行倒序。你告訴系統將文件的第一個字節映射到該保留區域的第一個字節。然后可以訪問該虛擬內存的區域,就像它包含了這個文件一樣。實際上,如果在文件的結尾處有一個單個0字節,那么只需要調用C運行期函數_ s t r r e v,就可以對文件中的數據進行倒序操作。
這種方法的最大優點是,系統能夠為你管理所有的文件緩存操作。不必分配任何內存,或者將文件數據加載到內存,也不必將數據重新寫入該文件,或者釋放任何內存塊。但是,內存映射文件仍然可能出現因為電源故障之類的進程中斷而造成數據被破壞的問題。
?使用內存映射文件
若要使用內存映射文件,必須執行下列操作步驟:
1) 創建或打開一個文件內核對象,該對象用于標識磁盤上你想用作內存映射文件的文件。
2) 創建一個文件映射內核對象,告訴系統該文件的大小和你打算如何訪問該文件。
3) 讓系統將文件映射對象的全部或一部分映射到你的進程地址空間中。
當完成對內存映射文件的使用時,必須執行下面這些步驟將它清除:
1) 告訴系統從你的進程的地址空間中撤消文件映射內核對象的映像。
2) 關閉文件映射內核對象。
3) 關閉文件內核對象。
步驟1:創建或打開文件內核對象
若要創建或打開一個文件內核對象,總是要調用C r e a t e F i l e函數:
HANDLE CreateFile(
PCSTR pszFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
PSECURITY_ATTRIBUTES psa,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
步驟2:創建一個文件映射內核對象
調用C r e a t e F i l e函數,就可以將文件映像的物理存儲器的位置告訴操作系統。你傳遞的路徑名用于指明支持文件映像的物理存儲器在磁盤(或網絡或光盤)上的確切位置。這時,必須告訴系統,文件映射對象需要多少物理存儲器。若要進行這項操作,可以調用C r e a t e F i l e M a p p i n g函數:
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD fdwProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
步驟3:將文件數據映射到進程的地址空間
當創建了一個文件映射對象后,仍然必須讓系統為文件的數據保留一個地址空間區域,并將文件的數據作為映射到該區域的物理存儲器進行提交。可以通過調用M a p Vi e w O f F i l e函數來進行這項操作:
PVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
SIZE_T dwNumberOfBytesToMap);
步驟4:從進程的地址空間中撤消文件數據的映像
當不再需要保留映射到你的進程地址空間區域中的文件數據時,可以通過調用下面的函數將它釋放:
BOOL UnmapViewOfFile(PVOID pvBaseAddress);
為了提高速度,系統將文件的數據頁面進行高速緩存,并且在對文件的映射視圖進行操作時不立即更新文件的磁盤映像。如果需要確保你的更新被寫入磁盤,可以強制系統將修改過的數據的一部分或全部重新寫入磁盤映像中,方法是調用F l u s h Vi e w O f F i l e函數:
BOOL FlushViewOfFile(
PVOID pvAddress,
SIZE_T dwNumberOfBytesToFlush);
步驟5和步驟6:關閉文件映射對象和文件對象
不用說,你總是要關閉你打開了的內核對象。如果忘記關閉,在你的進程繼續運行時會出現資源泄漏的問題。當然,當你的進程終止運行時,系統會自動關閉你的進程已經打開但是忘記關閉的任何對象。但是如果你的進程暫時沒有終止運行,你將會積累許多資源句柄。因此你始終都應該編寫清楚而又“正確的”代碼,以便關閉你已經打開的任何對象。若要關閉文件映射對象和文件對象,只需要兩次調用C l o s e H a n d l e函數,每個句柄調用一次:
讓我們更加仔細地觀察一下這個進程。下面的偽代碼顯示了一個內存映射文件的例子:
HANDLE hFile = CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
PVOID pvFile = MapViewOfFile(hFileMapping, ...);
// Use the memory-mapped file.
UnmapViewOfFile(pvFile);
CloseHandle(hFileMapping);
CloseHandle(hFile);
上面的代碼顯示了對內存映射文件進行操作所用的“預期”方法。但是,它沒有顯示,當你調用M a p Vi e w O f F i l e時系統對文件對象和文件映射對象的使用計數的遞增情況。這個副作用是很大的,因為它意味著我們可以將上面的代碼段重新編寫成下面的樣子:
HANDLE hFile = CreateFile(...);
HANDLE hFileMapping = CreateFileMapping(hFile, ...);
CloseHandle(hFile);
PVOID pvFile = MapViewOfFile(hFileMapping, ...);
CloseHandle(hFileMapping);
// Use the memory-mapped file.
UnmapViewOfFile(pvFile);
當對內存映射文件進行操作時,通常要打開文件,創建文件映射對象,然后使用文件映射對象將文件的數據視圖映射到進程的地址空間。由于系統遞增了文件對象和文件映射對象的內部使用計數,因此可以在你的代碼開始運行時關閉這些對象,以消除資源泄漏的可能性。
如果用同一個文件來創建更多的文件映射對象,或者映射同一個文件映射對象的多個視圖,那么就不能較早地調用C l o s e H a n d l e函數——以后你可能還需要使用它們的句柄,以便分別對C r e a t e F i l e M a p p i n g和M a p Vi e w O f F i l e函數進行更多的調用。
使用內存映射文件來處理大文件
使用內存映射文件在進程之間共享數據
Wi n d o w s總是出色地提供各種機制,使應用程序能夠迅速而方便地共享數據和信息。這些機制包括R P C、C O M、O L E、D D E、窗口消息(尤其是W M _ C O P Y D ATA)、剪貼板、郵箱、管道和套接字等。在Wi n d o w s中,在單個計算機上共享數據的最低層機制是內存映射文件。不錯,如果互相進行通信的所有進程都在同一臺計算機上的話,上面提到的所有機制均使用內存映射文件從事它們的煩瑣工作。如果要求達到較高的性能和較小的開銷,內存映射文件是舉手可得的最佳機制。
數據共享方法是通過讓兩個或多個進程映射同一個文件映射對象的視圖來實現的,這意味著它們將共享物理存儲器的同一個頁面。因此,當一個進程將數據寫入一個共享文件映射對象的視圖時,其他進程可以立即看到它們視圖中的數據變更情況。注意,如果多個進程共享單個文件映射對象,那么所有進程必須使用相同的名字來表示該文件映射對象。
讓我們觀察一個例子,啟動一個應用程序。當一個應用程序啟動時,系統調用C r e a t e F i l e函數,打開磁盤上的. e x e文件。然后系統調用C r e a t e F i l e M a p p i n g函數,創建一個文件映射對象。最后,系統代表新創建的進程調用M a p Vi e w O f F i l e E x函數(它帶有S E C _ I M A G E標志),這樣, . e x e文件就可以映射到進程的地址空間。這里調用的是M a p Vi e w O f F i l e E x,而不是M a p Vi e w O f F i l e,這樣,文件的映像將被映射到存放在. e x e文件映像中的基地址中。系統創建該進程的主線程,將該映射視圖的可執行代碼的第一個字節的地址放入線程的指令指針,然后C P U啟動該代碼的運行。
如果用戶運行同一個應用程序的第二個實例,系統就認為規定的. e x e文件已經存在一個文件映射對象,因此不會創建新的文件對象或者文件映射對象。相反,系統將第二次映射該文件的一個視圖,這次是在新創建的進程的地址空間環境中映射的。系統所做的工作是將相同的文件同時映射到兩個地址空間。顯然,這是對內存的更有效的使用,因為兩個進程將共享包含正在執行的這部分代碼的物理存儲器的同一個頁面。
與所有內核對象一樣,可以使用3種方法與多個進程共享對象,這3種方法是句柄繼承性、句柄命名和句柄復制。
?內存映射文件與數據視圖的相關性
附錄:
系統允許你映射一個文件的相同數據的多個視圖。例如,你可以將文件開頭的10 KB映射到一個視圖,然后將同一個文件的頭4 KB映射到另一個視圖。只要你是映射相同的文件映射對象,系統就會確保映射的視圖數據的相關性。例如,如果你的應用程序改變了一個視圖中的文件內容,那么所有其他視圖均被更新以反映這個變化。這是因為盡管頁面多次被映射到進程的虛擬地址空間,但是系統只將數據放在單個R A M頁面上。如果多個進程映射單個數據文件的視圖,那么數據仍然是相關的,因為在數據文件中,每個R A M頁面只有一個實例——正是這個R A M頁面被映射到多個進程的地址空間。
注意Wi n d o w s允許創建若干個由單個數據文件支持的文件映射對象。Wi n d o w s不能保證這些不同的文件映射對象的視圖具有相關性。它只能保證單個文件映射對象的多個視圖具有相關性。
然而,當對文件進行操作時,沒有理由使另一個應用程序無法調用C r e a t e F i l e函數以打開由另一個進程映射的同一個文件。這個新進程可以使用R e a d F i l e和Wr i t e F i l e函數來讀取該文件的數據和將數據寫入該文件。當然,每當一個進程調用這些函數時,它必須從內存緩沖區讀取文件數據或者將文件數據寫入內存緩沖區。該內存緩沖區必須是進程自己創建的一個緩沖區,而不是映射文件使用的內存緩沖區。當兩個應用程序打開同一個文件時,問題就可能產生:一個進程可以調用R e a d F i l e函數來讀取文件的一個部分,并修改它的數據,然后使用Wr i t e F i l e函數將數據重新寫入文件,而第二個進程的文件映射對象卻不知道第一個進程執行的這些操作。由于這個原因,當你為將被內存映射的文件調用C r e a t e F i l e函數時,最好將d w S h a r e M o d e參數的值設置為0。這樣就可以告訴系統,你想要單獨訪問這個文件,而其他進程都不能打開它。
只讀文件不存在相關性問題,因此它們可以作為很好的內存映射文件。內存映射文件決不應該用于共享網絡上的可寫入文件,因為系統無法保證數據視圖的相關性。如果某個人的計算機更新了文件的內容,其他內存中含有原始數據的計算機將不知道它的信息已經被修改。