首页 > Web开发 > 详细

VB.Net 进程间通信——内存映射文件

时间:2021-09-07 15:27:52      阅读:9      评论:0      收藏:0      [点我收藏+]

原文地址:https://zhuanlan.zhihu.com/p/39925519

温故之.NET进程间通信——内存映射文件

 

上一篇技术文章中,我们讲解了进程间通信中的管道通信方式,这只是多种进程间通信方式中的一种,这篇文章我们回顾一下另一种进程间通信的方式——内存映射文件

基础概念

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:\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 方法释放它占有的资源(进程结束后,其资源也会被释放,但我们应该养成良好的习惯,主动释放)



至此,这篇文章的内容讲解完毕。欢迎关注公众号【嘿嘿的学习日记】,所有的文章,都会在公众号首发;或加个人微信【JameLee6292】共同探讨。Thank you~

根据这篇文章修改的VB.net 代码

 

Imports System.IO
Imports System.IO.MemoryMappedFiles
Imports System.Threading

‘‘‘ <summary>
‘‘‘ 进程间通讯——内存映射文件
‘‘‘ </summary>
Public Class Class_IPC_MAP
    Private mmf As MemoryMappedFile
    Private mutexCreated As Boolean

    Public Sub New()
        MasterStart()
    End Sub

    ‘‘‘ <summary>
    ‘‘‘ 创建内存映射文件
    ‘‘‘ </summary>
    Public Sub MasterStart()
         此处的 MemoryMappedFile 实例不能使用 using 语法
         因为它会自动释放我们的内存映射文件,会导致进程B找不到这个映射文件而抛出异常
        mmf = MemoryMappedFile.CreateNew("IPC_MAP", 10000)

         创建互斥量以协调数据的读写
        Dim mutex As New Mutex(True, "IPC_MAP_MUTEX", mutexCreated)

        Using stream As MemoryMappedViewStream = mmf.CreateViewStream()
             向内存映射文件种写入数据
            Dim sw As New StreamWriter(stream)
            sw.WriteLine("IPC MAP Is Created.")
             这一句是必须的,在某些情况下,如果不调用Flush 方法会造成进程B读取不到数据
             它的作用是立即写入数据
             这样在此进程释放 Mutex 的时候,进程B就能正确读取数据了
            sw.Flush()
        End Using

        mutex.ReleaseMutex()
    End Sub

    Public Sub SlaveRead()
        Using mmf As MemoryMappedFile = MemoryMappedFile.OpenExisting("IPC_MAP")
            Dim mutex As Mutex = Mutex.OpenExisting("IPC_MAP_MUTEX")
              等待写入完成
            mutex.WaitOne()
            Using stream As MemoryMappedViewStream = mmf.CreateViewStream()
                Dim SR As New StreamReader(stream)
                 读取进程 A 写入的内容
                Console.WriteLine(SR.ReadLine())
            End Using
            mutex.ReleaseMutex()
        End Using
    End Sub

End Class

 

VB.Net 进程间通信——内存映射文件

原文:https://www.cnblogs.com/ovbm/p/15237437.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!