温故之.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;// 8Kstring 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.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 方法释放它占有的资源(进程结束后,其资源也会被释放,但我们应该养成良好的习惯,主动释放)至此,这篇文章的内容讲解完毕

  • 网络安全成长路线图

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成hei客大神,这个方向越往后,需要学习和掌握的东西就会越来越多,以下是学习网络安全需要走的方向:

网络安全学习方法

上面介绍了技术分类和学习路线,这里来谈一下学习方法:

## 视频学习

无论你是去B站或者是油管上面都有很多网络安全的相关视频可以学习,当然如果你还不知道选择那套学习,我这里也整理了一套和上述成长路线图挂钩的视频教程,完整版的视频已经上传至CSDN官方,朋友们如果需要可以点击这个链接免费领取。网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值