.NET 4.0內(nèi)存映射文件詳解
如果你有Windows API開發(fā)背景,你會(huì)意識(shí)到一種老技術(shù)叫做內(nèi)存映射文件(memory-mapped files,有時(shí)所寫成MMF)。內(nèi)存映射文件或是文件映射的想法就是將文件加載到內(nèi)存中,這樣它會(huì)作為一個(gè)連續(xù)塊在你的應(yīng)用程序的地址空間中出現(xiàn)。然后,讀取和寫入文件是訪問(wèn)正確內(nèi)存位置的最簡(jiǎn)單方法。事實(shí)上,當(dāng)操作系統(tǒng)加載器獲取你應(yīng)用程序的EXE或DLL文件來(lái)執(zhí)行它們的代碼的時(shí)候,文件映射會(huì)在幕后被使用。
使用來(lái)自.NET應(yīng)用程序的內(nèi)存映射文件本身已不再新鮮,因?yàn)橥ㄟ^(guò)使用在.NET1.0中的Platform Invoke (P/Invoke),它可能使用底層操作系統(tǒng)APIs。但是,在.NET4.0中,使用內(nèi)存映射文件可適用于所有沒(méi)有直接使用Windows APIs管理代碼的開發(fā)者們。
內(nèi)存映射文件和大文件總是在開發(fā)者的思想中出現(xiàn),但是沒(méi)有實(shí)際的限制來(lái)考慮到底內(nèi)存可以映射多大或是多小的文件。雖然對(duì)大文件使用內(nèi)存映射會(huì)使編程變得簡(jiǎn)單,但是你會(huì)觀察到當(dāng)使用更小一點(diǎn)的文件的時(shí)候執(zhí)行得會(huì)更好,因?yàn)樗鼈兛梢赃m用于整個(gè)文件系統(tǒng)緩存。
本文中的信息和代碼列表是基于2009年5月發(fā)布的.NET 4.0 Beta 1版本。由于是預(yù)發(fā)布的軟件,一旦***的.NET的RTM版本確定,所有技術(shù)細(xì)節(jié),類名稱和方法會(huì)改變。在研究或是開發(fā)任何測(cè)試庫(kù)的時(shí)候一定要牢記這一點(diǎn)。
新命名空間和它的類
對(duì)于.NET4.0 開發(fā)者來(lái)說(shuō),與內(nèi)存映射文件一起工作的最有趣的類是存在于新的System.IO.MemoryMappedFiles命名空間中。目前,這個(gè)命名空間包含四個(gè)類和一些列舉來(lái)幫助你訪問(wèn)并保護(hù)你的文件映射。實(shí)際的執(zhí)行是在集合System.Core.dll中。
對(duì)于開發(fā)者最重要的類是MemoryMappedFile類。這個(gè)類允許你創(chuàng)建一個(gè)內(nèi)存映射對(duì)象,反過(guò)來(lái)你可以創(chuàng)建一個(gè)視圖訪問(wèn)對(duì)象。然后你可以使用這個(gè)accessor直接操作來(lái)自文件映射的內(nèi)存塊。通過(guò)使用Read 和Write方法完成這個(gè)操作。
注意的是直接指針在管理的世界中不被視為一種良好的編程習(xí)慣,這樣的方法對(duì)象是需要保持整潔的。在傳統(tǒng)的Windows API 開發(fā)的本地代碼中,你只會(huì)獲得一個(gè)指針來(lái)啟動(dòng)你的內(nèi)存塊。
盡管如此,獲取一個(gè)內(nèi)存映射文件的過(guò)程和必要的accessor對(duì)象,你需要遵循三個(gè)簡(jiǎn)單的步驟。首先,你需要一個(gè)文件流對(duì)象指向在磁盤上的(一個(gè)現(xiàn)有的)文件。第二,從這個(gè)文件中你可以創(chuàng)建映射對(duì)象,***一個(gè)步驟,你創(chuàng)建accessor對(duì)象。這里有一段C#代碼示例:
- FileStream file = new FileStream(
- @"C:\Temp\MyFile.dat", FileMode.Open);
- MemoryMappedFile mmf =
- MemoryMappedFile.CreateFromFile(file);
- MemoryMappedViewAccessor accessor =
- mmf.CreateViewAccessor();
這個(gè)代碼首先打開帶有System.IO.FileStream類的一個(gè)文件,然后將流對(duì)象實(shí)例傳遞給MemoryMappedFile類的靜態(tài)CreateFromFile方法。第三步是調(diào)用MemoryMappedFile類的CreateViewAccessor方法。
在上面的代碼中,CreateViewAccessor方法在沒(méi)有任何參數(shù)的情況下被調(diào)用。在這種情況下,映射從文件開頭(offset零)開始,以文件的***字節(jié)為結(jié)束。你可以輕松的映射任何文件的部分。例如,如果你的文件有十億字節(jié)的大小,然后你可以映射,用以下調(diào)用可以完成:
MemoryMappedViewAccessor accessor =
mmf.CreateViewAccessor(1024 * 1024, 10000);
然后,你將看到對(duì)于這些映射視圖的更先進(jìn)的使用,但是首先,你必須學(xué)習(xí)從這個(gè)視圖中讀取。
從映射文件中讀取
要使用先前的映射內(nèi)存地址,你需要使用MemoryMappedViewAccessor類的方法。例如,要從文件映射的開端讀取10個(gè)字節(jié),你應(yīng)該使用ReadByte方法,如下:
- ...
- MemoryMappedViewAccessor accessor =
- mmf.CreateViewAccessor();
- byte[] buffer = new byte[10];
- for (int index = 0; index < buffer.Length; index++)
- {
- buffer[index] = accessor.ReadByte(index);
這個(gè)Read方法或者可以填入給出的一般對(duì)象的內(nèi)容,或者可以通過(guò)使用泛型 Read or ReadArray而采取更具體的對(duì)象。例如,假設(shè)你有一個(gè)Guid類型的對(duì)象(定義為一個(gè)結(jié)構(gòu)),然后兩個(gè)ReadNNN方法調(diào)用,以下有相似的結(jié)果:
- // method 1:
- byte[] buffer = new byte[16];
- accessor.ReadArray(0, buffer, 0, buffer.Length);
- Guid guid = new Guid(buffer);
- MessageBox.Show(guid.ToString());
- // method 2:
- Guid guid2 = new Guid();
- accessor.Read(0, out guid2);
- MessageBox.Show(guid2.ToString());
注意的是在兩個(gè)Read方法調(diào)用中,你需要指定讀取開始的位置。這個(gè)基于零的offset與映射視圖總是相對(duì)的,但不一定是原始文件。當(dāng)你創(chuàng)建內(nèi)存映射對(duì)象的時(shí)候,你需要指定你想要操作文件(圖1)的內(nèi)存窗口。如果你沒(méi)有指定任何的offset,像是以上代碼列表中的,然后該視圖被假定是從文件的開端開始的。
為了幫助提供靈活性,你可以從零offset開始并運(yùn)行直到文件的長(zhǎng)度或你可以從中間開始,并只映射文件的一部分。通過(guò)offsets相對(duì)視圖,accessor對(duì)象的讀取就可以完成。也就是說(shuō),原始文件的offset成為view的 起始o(jì)ffset加上view offset。
要記住內(nèi)存映射對(duì)象和文件下有操作系統(tǒng)處理。因此,重要的是要記住在完成它們之后要處理這些對(duì)象。否則它們將保留無(wú)限大的開放時(shí)間直到垃圾回收的進(jìn)入。***的辦法就是使用try-finally blocks或是使用C#語(yǔ)句。
如果你用.NET流對(duì)象工作非常開心,但是仍然希望從內(nèi)存映射文件中受益,那么你就是幸運(yùn)的。MemoryMappedFile類包含一個(gè)很方便的方法叫做CreateViewStream,,可以返回一個(gè)MemoryMappedViewStream對(duì)象。這個(gè)對(duì)象允許序列訪問(wèn)映射視圖;這個(gè)可能是使用映射視圖流(mapped view streams)與使用允許隨即訪問(wèn)的accessor對(duì)象相比的***缺點(diǎn)。但是如果你不介意這個(gè)局限,CreateViewStream方法就是你的朋友。
【編輯推薦】