溫故之.NET程式間通訊——記憶體對映檔案

JameLee發表於2019-02-08

上一篇技術文章中,我們講解了程式間通訊中的管道通訊方式,這只是多種程式間通訊方式中的一種,這篇文章我們回顧一下另一種程式間通訊的方式——記憶體對映檔案

基礎概念

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.CreateNewMemoryMappedFile.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 寫入的資料

這種方式在一個主程式,多個從程式之間通訊會非常的方便,不但穩定而且快速。並且,這種方式相比於其他的程式間通訊方式,效率是最高的。因此這種方式在單機中多個從程式間的通訊採用得最多

對於一些比較複雜的程式間通訊,如果需要傳遞大量的不同型別的資料,我們可以使用序列化的方式將需要傳遞的物件序列化。比如我們可以採用以下工具對傳遞的資料序列化:ProtobufJilMsgPack等。這三種序列化庫是目前市面上比較快的,當然我們也可以根據專案的實際情況來選擇合適的庫

記憶體對映檔案二三事

關於記憶體對映檔案,我們還需要了解以下幾點

  • 預設情況下,在呼叫 MemoryMappedFile.CreateFromFile 方法時如果不指定檔案容量,那麼,建立的記憶體對映檔案的容量等同於檔案的大小
  • 如果磁碟上的檔案是新建立的,那麼必須為它指定容量(MemoryMappedFile.CreateFromFilecapacity 引數)
  • 在指定記憶體對映檔案的容量時,其值不能小於磁碟檔案的現有長度。如指定了一個大於磁碟檔案大小的容量,則磁碟檔案的大小會被擴充至指定容量
  • 當不再使用一個 MemoryMappedFile 物件時,我們應該及時地呼叫 Dispose 方法釋放它佔有的資源(程式結束後,其資源也會被釋放,但我們應該養成良好的習慣,主動釋放)

至此,這篇文章的內容講解完畢。
歡迎關注公眾號【嘿嘿的學習日記】,所有的文章,都會在公眾號首發,Thank you~

公眾號二維碼

相關文章