上一篇技術文章中,我們講解了程式間通訊中的管道通訊方式,這只是多種程式間通訊方式中的一種,這篇文章我們回顧一下另一種程式間通訊的方式——記憶體對映檔案
基礎概念
Windows
提供了 3 種進行記憶體管理的方法:
- 虛擬記憶體:適合用來管理大型物件或結構陣列
- 記憶體對映檔案:適合用來管理大型資料流(通常來自檔案),也適合在單機上多個程式(執行著的程式)之間共享資料
- 記憶體堆疊:適合用來管理大量的小物件
記憶體對映檔案在 Windows
中使用場景很多,程式間通訊也只是其多個應用場景中的一個。它在操作大檔案時非常高效,這種場景下也使用得非常廣泛。比如資料庫檔案
藉助檔案和記憶體空間之間的這種對映,應用可以直接對記憶體執行讀寫操作,從而間接的修改檔案。自 .NET Framework 4
起(在 System.IO.MemoryMappedFiles
名稱空間下),我們便可以通過託管程式碼去訪問記憶體對映檔案
如果我們需要使用記憶體對映檔案,則必須建立該記憶體對映檔案的檢視(該檢視對映到檔案的全部記憶體或一部分記憶體上)。我們也可以為記憶體對映檔案的同一部分建立多個檢視,從而建立併發記憶體。若要讓兩個檢視一直處於併發狀態,必須通過同一個記憶體對映檔案建立它們。當檔案大於可用於記憶體對映的應用邏輯記憶體空間(在 32
位計算機中為 2GB
)時,也有必要使用多個檢視
檢視分為以下兩種型別:流訪問檢視和隨機訪問檢視
- 使用流訪問檢視,可以順序訪問檔案。建議對非持久化檔案和
IPC
使用這種型別(通過MemoryMappedFile.CreateViewStream
建立此檢視) - 隨機訪問檢視是處理持久化檔案的首選型別(通過
MemoryMappedFile.CreateViewAccessor
建立此檢視)
記憶體對映檔案通過作業系統的記憶體管理程式進行訪問,因此檔案會被自動分割槽到很多頁面,並根據需要進行訪問(即自動的記憶體管理,不需要我們人為干預)
記憶體對映檔案分為兩種型別:持久化記憶體對映檔案和非持久化記憶體對映檔案,不同的型別應用於不同的場景
持久化記憶體對映檔案
持久化檔案是與磁碟上的原始檔相關聯的記憶體對映檔案(即磁碟上需要有個檔案才行)。當最後一個程式處理完檔案時,資料儲存到磁碟上的原始檔中。此類記憶體對映檔案適用於處理非常大的原始檔,這種方式在很多資料庫中都有使用
可使用 MemoryMappedFile.CreateFromFile
建立此型別的對映檔案。要想訪問此型別的對映檔案,可通過 MemoryMappedFile.CreateViewAccessor
建立一個隨機訪問檢視。這也是訪問持久化記憶體對映檔案推薦的方式
示例程式碼如下
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
namespace App {
class Program {
static void Main(string[] args) {
long offset = 0x0000;
long length = 0x2000; // 8K
string mapName = "Demos.MapFiles.TestInstance";
int colorSize = Marshal.SizeOf(typeof(Color));
long number = length / colorSize;
Color color;
// 從磁碟上現有檔案,建立記憶體對映檔案,第三個引數為這個記憶體對映檔案的名稱
var firstMapFile = MemoryMappedFile.CreateFromFile(@"d: est_data.data", FileMode.OpenOrCreate, mapName);
// 建立一個隨機訪問檢視
using (var accessor = firstMapFile.CreateViewAccessor(offset, length)) {
// 更改對映檔案內容
for (long i = 0; i < number; i += colorSize) {
accessor.Read(i, out color);
color.Add(new Color() { R = 10, G = 10, B = 10, A = 10 });
accessor.Write(i, ref color);
}
}
// 開啟已經存在的記憶體對映檔案
// 第一個引數為這個記憶體對映檔案的名稱
// 【此處的程式碼可以放在另一個程式中】
var secondMapFile = MemoryMappedFile.OpenExisting(mapName);
using (var secondAccessor = secondMapFile.CreateViewAccessor(offset, length)) {
// 讀取對映檔案內容
for (long i = 0; i < number; i += colorSize) {
secondAccessor.Read(i, out color);
Console.WriteLine(color);
}
}
Console.ReadLine();
// 釋放記憶體對映檔案資源
firstMapFile.Dispose();
secondMapFile.Dispose();
}
}
// 為了便於測試,建立一個簡單的結構
public struct Color {
public byte R, G, B, A;
public void Add(Color color) {
this.R = (byte)(this.R + color.R);
this.G = (byte)(this.G + color.G);
this.B = (byte)(this.B + color.B);
this.A = (byte)(this.A + color.A);
}
public override string ToString() {
return $"Color({R},{G},{B},{A})";
}
}
}
複製程式碼
以上示例可多執行幾次,就能發現輸出的顏色值的變化
非持久化記憶體對映檔案
非持久化檔案是不與磁碟上的檔案相關聯的記憶體對映檔案(即磁碟上沒有對應的檔案,這裡的檔案我們是看不見的)。當最後一個程式處理完檔案時,資料會丟失,且檔案被垃圾回收器回收。此類檔案適合建立共享記憶體,以進行程式間通訊
可使用 MemoryMappedFile.CreateNew
或 MemoryMappedFile.CreateOrOpen
建立此型別的對映檔案。訪問此種型別的對映檔案,推薦使用方法 MemoryMappedFile.CreateViewStream
來建立一個流訪問檢視,它可以實現順序訪問檔案
這種方式的示例程式碼會在下面的 使用記憶體對映檔案實現程式間通訊 小節給出
使用記憶體對映檔案實現程式間通訊
要實現程式間通訊,單個程式需要對映到相同的記憶體對映檔案,並使用相同的記憶體對映檔名稱。為了保證共享資料的安全,往往我們需要藉助 Mutex
或者其他的互斥訊號來對共享記憶體區域進行讀寫的控制
程式 A 示例程式碼如下
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading;
namespace App {
class Program {
static void Main(string[] args) {
// 此處的 MemoryMappedFile 例項不能使用 using 語法
// 因為它會自動釋放我們的記憶體對映檔案,會導致程式B找不到這個對映檔案而丟擲異常
MemoryMappedFile mmf = MemoryMappedFile.CreateNew("IPC_MAP", 10000);
// 建立互斥量以協調資料的讀寫
Mutex mutex = new Mutex(true, "IPC_MAP_MUTEX", out bool mutexCreated);
using (MemoryMappedViewStream stream = mmf.CreateViewStream()) {
StreamWriter sw = new StreamWriter(stream);
// 向記憶體對映檔案種寫入資料
sw.WriteLine("This is IPC MAP TEXT");
// 這一句是必須的,在某些情況下,如果不呼叫Flush 方法會造成程式B讀取不到資料
// 它的作用是立即寫入資料
// 這樣在此程式釋放 Mutex 的時候,程式B就能正確讀取資料了
sw.Flush();
}
mutex.ReleaseMutex();
Console.ReadLine();
mmf.Dispose();
}
}
}
複製程式碼
程式 B 示例程式碼如下
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Threading;
namespace App {
class Program {
static void Main(string[] args) {
using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("IPC_MAP")) {
Mutex mutex = Mutex.OpenExisting("IPC_MAP_MUTEX");
// 等待寫入完成
mutex.WaitOne();
using (MemoryMappedViewStream stream = mmf.CreateViewStream()) {
StreamReader sr = new StreamReader(stream);
// 讀取程式 A 寫入的內容
Console.WriteLine(sr.ReadLine());
}
mutex.ReleaseMutex();
}
Console.ReadLine();
}
}
}
複製程式碼
這兒我們需要先執行示例 A 以啟動程式 A,再執行示例 B 啟動程式 B。程式 B 輸出為
This is IPC MAP TEXT
複製程式碼
表示成功讀取到了程式 A 寫入的資料
這種方式在一個主程式,多個從程式之間通訊會非常的方便,不但穩定而且快速。並且,這種方式相比於其他的程式間通訊方式,效率是最高的。因此這種方式在單機中多個從程式間的通訊採用得最多
對於一些比較複雜的程式間通訊,如果需要傳遞大量的不同型別的資料,我們可以使用序列化的方式將需要傳遞的物件序列化。比如我們可以採用以下工具對傳遞的資料序列化:Protobuf
、Jil
、MsgPack
等。這三種序列化庫是目前市面上比較快的,當然我們也可以根據專案的實際情況來選擇合適的庫
記憶體對映檔案二三事
關於記憶體對映檔案,我們還需要了解以下幾點
- 預設情況下,在呼叫
MemoryMappedFile.CreateFromFile
方法時如果不指定檔案容量,那麼,建立的記憶體對映檔案的容量等同於檔案的大小 - 如果磁碟上的檔案是新建立的,那麼必須為它指定容量(
MemoryMappedFile.CreateFromFile
的capacity
引數) - 在指定記憶體對映檔案的容量時,其值不能小於磁碟檔案的現有長度。如指定了一個大於磁碟檔案大小的容量,則磁碟檔案的大小會被擴充至指定容量
- 當不再使用一個
MemoryMappedFile
物件時,我們應該及時地呼叫Dispose
方法釋放它佔有的資源(程式結束後,其資源也會被釋放,但我們應該養成良好的習慣,主動釋放)
至此,這篇文章的內容講解完畢。
歡迎關注公眾號【嘿嘿的學習日記】,所有的文章,都會在公眾號首發,Thank you~