溫故之.NET進程間通訊——內存映射文件

上一篇技術文章中,咱們講解了進程間通訊中的管道通訊方式,這只是多種進程間通訊方式中的一種,這篇文章咱們回顧一下另外一種進程間通訊的方式——內存映射文件數據庫

基礎概念

Windows 提供了 3 種進行內存管理的方法:數組

  • 虛擬內存:適合用來管理大型對象或結構數組
  • 內存映射文件:適合用來管理大型數據流(一般來自文件),也適合在單機上多個進程(運行着的進程)之間共享數據
  • 內存堆棧:適合用來管理大量的小對象

內存映射文件在 Windows 中使用場景不少,進程間通訊也只是其多個應用場景中的一個。它在操做大文件時很是高效,這種場景下也使用得很是普遍。好比數據庫文件安全

藉助文件和內存空間之間的這種映射,應用能夠直接對內存執行讀寫操做,從而間接的修改文件。自 .NET Framework 4 起(在 System.IO.MemoryMappedFiles 命名空間下),咱們即可以經過託管代碼去訪問內存映射文件bash

若是咱們須要使用內存映射文件,則必須建立該內存映射文件的視圖(該視圖映射到文件的所有內存或一部份內存上)。咱們也能夠爲內存映射文件的同一部分建立多個視圖,從而建立併發內存。若要讓兩個視圖一直處於併發狀態,必須經過同一個內存映射文件建立它們。當文件大於可用於內存映射的應用邏輯內存空間(在 32 位計算機中爲 2GB)時,也有必要使用多個視圖併發

視圖分爲如下兩種類型:流訪問視圖和隨機訪問視圖app

  • 使用流訪問視圖,能夠順序訪問文件。建議對非持久化文件和 IPC 使用這種類型(經過 MemoryMappedFile.CreateViewStream 建立此視圖)
  • 隨機訪問視圖是處理持久化文件的首選類型(經過 MemoryMappedFile.CreateViewAccessor 建立此視圖)

內存映射文件經過操做系統的內存管理程序進行訪問,所以文件會被自動分區到不少頁面,並根據須要進行訪問(即自動的內存管理,不須要咱們人爲干預)ide

內存映射文件分爲兩種類型:持久化內存映射文件和非持久化內存映射文件,不一樣的類型應用於不一樣的場景工具

持久化內存映射文件

持久化文件是與磁盤上的源文件相關聯的內存映射文件(即磁盤上須要有個文件才行)。當最後一個進程處理完文件時,數據保存到磁盤上的源文件中。此類內存映射文件適用於處理很是大的源文件,這種方式在不少數據庫中都有使用學習

可以使用 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:\test_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~

公衆號二維碼
相關文章
相關標籤/搜索