上一篇技术文章中,我们讲解了进程间通信中的管道通信方式,这只是多种进程间通信方式中的一种,这篇文章我们回顾一下另一种进程间通信的方式——内存映射文件
基础概念
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
方法释放它占有的资源(进程结束后,其资源也会被释放,但我们应该养成良好的习惯,主动释放)至此,这篇文章的内容讲解完毕。 欢迎关注公众号【嘿嘿的学习日记】,所有的文章,都会在公众号首发,Thank you~
# 学习计划安排
我一共划分了六个阶段,但并不是说你得学完全部才能上手工作,对于一些初级岗位,学到第三四个阶段就足矣~
这里我整合并且整理成了一份【282G】的网络安全从零基础入门到进阶资料包,需要的小伙伴可以扫描下方CSDN官方合作二维码免费领取哦,无偿分享!!!
如果你对网络安全入门感兴趣,那么你需要的话可以
点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!
①网络安全学习路线
②上百份渗透测试电子书
③安全攻防357页笔记
④50份安全攻防面试指南
⑤安全红队渗透工具包
⑥HW护网行动经验总结
⑦100个漏洞实战案例
⑧安全大厂内部视频资源
⑨历年CTF夺旗赛题解析
![](https://img-blog.csdnimg.cn/img_convert/e35d659bba037ae795ca0cd4e657229c.jpeg)