本文介紹了.NET記憶體映射檔案的原理、建立以及實作程序通訊的方法。NET 4.0新增加了一個System.IO. MemoryMappedFiles命名空間,其中添加了幾個類和相應的枚舉類型,進而使我們可以很友善地建立記憶體映射檔案。
作業系統很早就開始使用記憶體映射檔案(Memory Mapped File)來作為程序間的共享存儲區,這是一種非常高效的程序通訊手段。Win32 API中也包含有建立記憶體映射檔案的函數,然而,這些函數都運作于非托管環境下,在.NET中隻能通過平台調用機制來使用它們,用起來很不友善。幸運的是,.NET 4.0新增加了一個System.IO. MemoryMappedFiles命名空間,其中添加了幾個類和相應的枚舉類型,進而使我們可以很友善地建立記憶體映射檔案。
1 記憶體映射檔案原理
所謂記憶體映射檔案,其實就是在記憶體中開辟出一塊存放資料的專用區域,這區域往往與硬碟上特定的檔案相對應。程序将這塊記憶體區域映射到自己的位址空間中,通路它就象是通路普通的記憶體一樣。
圖 1 .NET記憶體映射檔案原理圖
在.NET中,使用MemoryMappedFile對象表示一個記憶體映射檔案,通過它的CreateFromFile()方法根據磁盤現有檔案建立記憶體映射檔案,調用這一方法需要提供一個與磁盤現有檔案相對應的FileStream對象。
以下示例代碼動态建立一個MyFile.dat檔案,然後将其映射到系統記憶體中,設定容量為1M:
FileStream fs = new FileStream("MyFile.dat", FileMode.Create, FileAccess.ReadWrite); MemoryMappedFile memoryFile = MemoryMappedFile.CreateFromFile(fs, "MyFile", 1024*1024);
注意用于建立記憶體映射檔案的檔案流必須是可讀寫的。
擴充閱讀:
關于記憶體映射檔案的容量
預設情況下,在調用MemoryMappedFile.CreateFromFile()方法時如果不指定檔案容量,那麼,建立的記憶體映射檔案的容量等同于檔案的大小。
在上面的示例代碼中,由于磁盤檔案是臨時生成的,其長度為0,是以,必須在建立記憶體映射檔案時同時指定其容量。
在設定記憶體映射檔案的容量時,其值不能小于磁盤檔案的現有長度,但可以比它大。但要注意這将導緻一個戲劇化的結果:磁盤檔案自動增長到聲明的容量大小!
可以多次調用MemoryMappedFile.CreateFromFile(),每次傳給它一個更大的容量數值以不斷擴充磁盤檔案的大小。
當不再使用一個MemoryMappedFile對象時,注意應該及時地調用其Dispose()方法釋放它所占有的系統資源。因為MemoryMappedFile實際上對應着運作作業系統核心的核心對象,如果不及時關閉,會造成作業系統核心資源(比如句柄)的浪費,要等到MemoryMappedFile對象被CLR垃圾回收,或者整個程序中止時,這些資源才會被作業系統回收再利用。
另外,記憶體映射檔案的容量其實是指最大允許配置設定給記憶體映射檔案的記憶體存儲區位元組數,并不意味着系統會馬上配置設定指定容量的記憶體。程序中通路這塊映射到磁盤檔案中的存儲區時,作業系統如果發現其内容還未裝入記憶體,就會從磁盤檔案中裝入相應内容到記憶體中。是以,不用擔心聲明一個大的記憶體映射檔案容量會導緻記憶體的浪費。
當MemoryMappedFile對象建立之後,我們并不能直接對其進行讀寫,必須通過一個MemoryMappedViewAccessor對象來通路這個記憶體映射檔案。
MemoryMappedFile. CreateViewAccessor()方法可以建立MemoryMappedViewAccessor對象,而此對象提供了一系列讀寫的方法,用于向記憶體映射檔案中讀取和寫入資料。
以下示例代碼建立了一個記憶體映射檔案通路對象并使用它寫入資料:
FileStream fs =…; //建立FileStream對象 MemoryMappedFile memoryFile=…; //建立記憶體映射檔案 //建立記憶體映射檔案通路對象 MemoryMappedViewAccessor accessor= memoryFile.CreateViewAccessor(0, 1024); for (int i = 0; i < 1024; i+=2) accessor.Write(i, ‘c’); |
上述代碼中要注意,在建立記憶體映射檔案通路對象需要指定它所能通路的記憶體映射檔案的内容範圍,這個“範圍”稱為“記憶體映射視圖(Memory Mapped View)”。可以将它與“放大鏡”類比,當使用一個放大鏡閱讀書籍時,一次隻能放大指定部分的文字。類似地,我們隻能在記憶體映射視圖所規定的範圍記憶體取記憶體映射檔案。
在上述代碼中,我們看到記憶體映射視圖對象accessor隻提取了記憶體映射檔案開頭1024個位元組的内容,然後,向其中寫入了512個“c”字元。
當調用記憶體映射視圖對象的Write()方法時,需要指明從哪個位置(即方法的第一個參數)開始寫入資料,并且需要計算清楚要寫入的資料占幾個位元組,這樣,當寫入下一個資料時,就知道應該從哪個位置開始。
注意,Write()方法中的位置是相對視圖對象而非記憶體映射檔案本身,是以,此位置數值再加上視圖距記憶體映射檔案開頭的位置資料才是寫入的資料在檔案中的真實位置。
Write()方法有多個重載形式,可以向記憶體映射檔案中寫入多種類型的資料,但要注意計算清楚其寫入的位置,避免造成資料覆寫問題。
類似地,記憶體映射視圖對象提供了多個重載的Read()方法,可以從記憶體映射檔案中讀取資料。
比較有趣的是,在同一個程序中可以針對同一個記憶體映射檔案建立多個視圖對象,進而允許我們同時修改同一個檔案的不同部分,在關閉視圖對象時由作業系統保證将所有修改都寫回到原始檔案中。
下面我們來看一個示例。
2 在同一程序内同時讀寫同一記憶體映射檔案
示例項目UseMMFInProcess運作時會在程式的目前目錄下建立一個“MyFile.dat”檔案,然後,建立了兩個記憶體映射視圖對象,分别向檔案的前半部分和後半部分寫入不同的資料,然後再從中讀出來(圖 2)。
圖 2 .NET記憶體映射檔案示例項目UseMMFInProcess
這個示例展示的技術很基礎,請讀者自行檢視源碼。
3 使用記憶體映射檔案在程序間傳送值類型資料
在前面的例子中,記憶體映射檔案直接與某個特定的磁盤檔案相對應,事實上,我們也可以不用建立磁盤檔案而直接使用Windows的分頁檔案。這種方式是實作程序間互傳資料的典型方式。
調用MemoryMappedFile.CreateNew()或MemoryMappedFile.CreateOrOpen()方法可以在系統記憶體(System Memory)中直接建立一個記憶體映射檔案,這個記憶體映射檔案所對應的“實體檔案”是Windows的系統分頁檔案。兩個方法都需要給映射檔案指定一個唯一的名稱。不同之處在于CreateOrOpen ()方法在指定名稱的映射檔案存在時就直接将其傳回給程序,而CreateNew()方法始終是新建立一個記憶體映射檔案。
Windows的系統分頁檔案和休眠檔案
預設情況下,在安裝Windows的分區根目錄下,會找到兩個具有“隐藏”屬性的pagefile.sys和hiberfil.sys檔案,前者(pagefile.sys)就是Windows的分頁檔案,用于儲存從實體記憶體中換出的記憶體頁,我們可以用它的一部分來建立記憶體映射檔案。後者(hiberfil.sys)則是“系統休眠”檔案,當Windows啟用了休眠功能時,就會在硬碟上找到這個檔案,它的内容是系統休眠時實體記憶體中的資料,當計算機從休眠中“醒”過來時,通過從此檔案中加載資訊以恢複上次工作的狀态。
記憶體映射檔案建立好以後,可以如同前面介紹的方法一樣建立視圖對象,然後使用Read和Write系列方法存取。
隻要指定同一個名字,那麼,多個程序就可以使用同一個記憶體映射檔案交換資料。示例UseMMFBetweenProcess展示了在兩個程序間互相交換一個結構體變量的情況:
圖 3 .NET記憶體映射檔案示例項目UseMMFBetweenProcess
兩個程序要交換的資料格式如下:
public struct MyStructure { public int IntValue { get; set; } public float FloatValue } |
啟動UseMMFBetweenProcess程式的兩個執行個體,在其中一個窗體上輸入兩個數字之後,點選“儲存”按鈕,然後在另一個程序的窗體上點選“提取”,可以看到另一個程序寫入的資訊出現在本程序的文本框中。
示例程式采用MemoryMappedFile.CreateOrOpen()方法建立或打開一個記憶體映射檔案,然後調用MemoryMappedViewAccessor類的泛型方法Write()和Read()向記憶體映射檔案中寫入和讀取資料。
注意,泛型方法Write()和Read()中的泛型參數T必須是值類型(比如整型int和結構體struct),特别地,對于使用者自定義的結構體,要求其成員也必須是值類型。
例如,以下結構體将無法寫入到記憶體映射檔案中,因為其成員Info是string類型的,這是一個引用類型。
public struct ErrorStruct { public string Info; } |
之是以要求泛型參數不能是引用類型,其道理非常簡單,如果結構體中的某個成員是引用類型,那麼在程式運作時,計算機無法知道應該向記憶體映射檔案中寫入多少個位元組,因為引用類型的變量所引用的對象位于托管堆中,其占用存儲空間的大小不經過計算是難以确定的,而完成這個計算工作将花費不少的系統資源(想想一個對象可能又會引用到另一個對象就明白了),這會嚴重影響記憶體映射檔案讀寫操作效率。
兩個程序不能交換引用類型的資料,這個限制似乎還不小,但事實上,我們完成可以通過對象序列化技術來突破這個限制,在兩個程序間交換任意大小的對象(隻要記憶體映射檔案有足夠的容量)。請看下一小節的示例UseMMFBetweenProcess2。
4 利用序列化技術通過記憶體映射檔案實作程序通訊
圖4 .NET記憶體映射檔案示例:UseMMFBetweenProcess2
如圖 4所示,運作示例程式的多個執行個體,加載圖檔并輸入圖檔說明,點選相應按鈕後,可以在多個程序間直接交換以下格式的資訊:
[Serializable] class MyPic { public Image pic;//圖檔 public string picInfo; //圖檔資訊說明 }
請注意這是一個引用類型的資料對象,并且它附加了可序列化“[Serializable]”的代碼屬性。
如果要向記憶體映射檔案中序列化對象,必須将記憶體映射檔案轉換為可順序讀取的流。幸運的是,MemoryMappedFile類的CreateViewStream()方法可以建立一個MemoryMappedViewStream對象,通過它即可序列化對象,其代碼架構如下:
//建立或打開記憶體映射檔案 MemoryMappedFile memoryFile = MemoryMappedFile.CreateOrOpen(...); //建立記憶體映射流 MemoryMappedViewStream stream = memoryFile.CreateViewStream(); //建立要在程序間交換的資訊對象 MyPic obj =...; //向記憶體映射流中序列化對象 IFormatter formatter = new BinaryFormatter(); stream.Seek(0, SeekOrigin.Begin); formatter.Serialize(stream, obj); |