温故之.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~

公众号二维码
相关文章
相关标签/搜索