.NET 4.0系列之
使用内存映射文件实现进程通讯
************************************************************************
版权声明:
此文章由原作者金旭亮出于技术共享与交流目的而发布,作者金旭亮拥有全部版权。任何人及机构不得将其用于商业用途,如收费培训及出版技术书籍,有这方面需求的个人及培训机构请与本人直接联系,本人可提供针对.NET 3.5和4.0完备的教学与培训资料。
本人联系方式:
JinXuLiang@bit.edu.cn 北京理工大学计算机学院软件研究所 100081
更多技术与学习资源请关注作者博客:
金旭亮
2009.8.12
*************************************************************************
操作系统很早就开始使用内存映射文件(Memory Mapped File)来作为进程间的共享存储区,这是一种非常高效的进程通讯手段。Win32 API中也包含有创建内存映射文件的函数,然而,这些函数都运行于非托管环境下,在.NET中只能通过平台调用机制来使用它们,用起来很不方便。幸运的是,.NET 4.0新增加了一个System.IO. MemoryMappedFiles命名空间,其中添加了几个类和相应的枚举类型,从而使我们可以很方便地创建内存映射文件。
1 内存映射文件原理
所谓内存映射文件,其实就是在内存中开辟出一块存放数据的专用区域,这区域往往与硬盘上特定的文件相对应。进程将这块内存区域映射到自己的地址空间中,访问它就象是访问普通的内存一样。
图 1 内存映射文件原理图
在.NET中,使用MemoryMappedFile对象表示一个内存映射文件,通过它的CreateFromFile()方法根据磁盘现有文件创建内存映射文件,调用这一方法需要提供一个与磁盘现有文件相对应的FileStream对象。
以下示例代码动态创建一个MyFile.dat文件,然后将其映射到系统内存中,设定容量为1M:
FileStream fs = new FileStream("MyFile.dat", FileMode.Create,
FileAccess.ReadWrite);
MemoryMappedFile memoryFile = MemoryMappedFile.CreateFromFile(fs, "MyFile", 1024*1024);
注意用于创建内存映射文件的文件流必须是可读写的。
扩充阅读:
关于内存映射文件的容量
默认情况下,在调用MemoryMappedFile.CreateFromFile()方法时如果不指定文件容量,那么,创建的内存映射文件的容量等同于文件的大小。
在上面的示例代码中,由于磁盘文件是临时生成的,其长度为0,所以,必须在创建内存映射文件时同时指定其容量。
在设定内存映射文件的容量时,其值不能小于磁盘文件的现有长度,但可以比它大。但要注意这将导致一个戏剧化的结果:磁盘文件自动增长到声明的容量大小!
可以多次调用MemoryMappedFile.CreateFromFile(),每次传给它一个更大的容量数值以不断扩充磁盘文件的大小。
当不再使用一个MemoryMappedFile对象时,注意应该及时地调用其Dispose()方法释放它所占有的系统资源。因为MemoryMappedFile实际上对应着运行操作系统核心的核心对象,如果不及时关闭,会造成操作系统核心资源(比如句柄)的浪费,要等到MemoryMappedFile对象被CLR垃圾回收,或者整个进程中止时,这些资源才会被操作系统回收再利用。
另外,内存映射文件的容量其实是指最大允许分配给内存映射文件的内存存储区字节数,并不意味着系统会马上分配指定容量的内存。进程中访问这块映射到磁盘文件中的存储区时,操作系统如果发现其内容还未装入内存,就会从磁盘文件中装入相应内容到内存中。因此,不用担心声明一个大的内存映射文件容量会导致内存的浪费。
当MemoryMappedFile对象创建之后,我们并不能直接对其进行读写,必须通过一个MemoryMappedViewAccessor对象来访问这个内存映射文件。
MemoryMappedFile. CreateViewAccessor()方法可以创建MemoryMappedViewAccessor对象,而此对象提供了一系列读写的方法,用于向内存映射文件中读取和写入数据。
以下示例代码创建了一个内存映射文件访问对象并使用它写入数据:
FileStream fs =…; //创建FileStream对象
MemoryMappedFile memoryFile=…; //创建内存映射文件
//创建内存映射文件访问对象
MemoryMappedViewAccessor accessor=
memoryFile.CreateViewAccessor(0, 1024);
for (int i = 0; i < 1024; i+=2)
accessor.Write(i, ‘c’);
上述代码中要注意,在创建内存映射文件访问对象需要指定它所能访问的内存映射文件的内容范围,这个“范围”称为“内存映射视图(Memory Mapped View)”。可以将它与“放大镜”类比,当使用一个放大镜阅读书籍时,一次只能放大指定部分的文字。类似地,我们只能在内存映射视图所规定的范围内存取内存映射文件。
在上述代码中,我们看到内存映射视图对象accessor只提取了内存映射文件开头1024个字节的内容,然后,向其中写入了512个“c”字符。
当调用内存映射视图对象的Write()方法时,需要指明从哪个位置(即方法的第一个参数)开始写入数据,并且需要计算清楚要写入的数据占几个字节,这样,当写入下一个数据时,就知道应该从哪个位置开始。
注意,Write()方法中的位置是相对视图对象而非内存映射文件本身,因此,此位置数值再加上视图距内存映射文件开头的位置数据才是写入的数据在文件中的真实位置。
Write()方法有多个重载形式,可以向内存映射文件中写入多种类型的数据,但要注意计算清楚其写入的位置,避免造成数据覆盖问题。
类似地,内存映射视图对象提供了多个重载的Read()方法,可以从内存映射文件中读取数据。
比较有趣的是,在同一个进程中可以针对同一个内存映射文件创建多个视图对象,从而允许我们同时修改同一个文件的不同部分,在关闭视图对象时由操作系统保证将所有修改都写回到原始文件中。
下面我们来看一个示例。
2 在同一进程内同时读写同一内存映射文件
示例项目UseMMFInProcess运行时会在程序的当前目录下创建一个“MyFile.dat”文件,然后,创建了两个内存映射视图对象,分别向文件的前半部分和后半部分写入不同的数据,然后再从中读出来(图 2)。
图 2 示例项目UseMMFInProcess
这个示例展示的技术很基础,请读者自行查看源码。
3 使用内存映射文件在进程间传送值类型数据
在前面的例子中,内存映射文件直接与某个特定的磁盘文件相对应,事实上,我们也可以不用创建磁盘文件而直接使用Windows的分页文件。这种方式是实现进程间互传数据的典型方式。
调用MemoryMappedFile.CreateNew()或MemoryMappedFile.CreateOrOpen()方法可以在系统内存(System Memory)中直接创建一个内存映射文件,这个内存映射文件所对应的“物理文件”是Windows的系统分页文件。两个方法都需要给映射文件指定一个唯一的名称。不同之处在于CreateOrOpen ()方法在指定名称的映射文件存在时就直接将其返回给进程,而CreateNew()方法始终是新创建一个内存映射文件。
扩充阅读:
Windows的系统分页文件和休眠文件
默认情况下,在安装Windows的分区根目录下,会找到两个具有“隐藏”属性的pagefile.sys和hiberfil.sys文件,前者(pagefile.sys)就是Windows的分页文件,用于保存从物理内存中换出的内存页,我们可以用它的一部分来创建内存映射文件。后者(hiberfil.sys)则是“系统休眠”文件,当Windows启用了休眠功能时,就会在硬盘上找到这个文件,它的内容是系统休眠时物理内存中的数据,当计算机从休眠中“醒”过来时,通过从此文件中加载信息以恢复上次工作的状态。
内存映射文件创建好以后,可以如同前面介绍的方法一样创建视图对象,然后使用Read和Write系列方法存取。
只要指定同一个名字,那么,多个进程就可以使用同一个内存映射文件交换数据。示例UseMMFBetweenProcess展示了在两个进程间相互交换一个结构体变量的情况:
图 3 示例项目UseMMFBetweenProcess
两个进程要交换的数据格式如下:
public struct MyStructure
{
public int IntValue
{ get; set; }
public float FloatValue
{ get; set; }
}
启动UseMMFBetweenProcess程序的两个实例,在其中一个窗体上输入两个数字之后,点击“保存”按钮,然后在另一个进程的窗体上点击“提取”,可以看到另一个进程写入的信息出现在本进程的文本框中。
示例程序采用MemoryMappedFile.CreateOrOpen()方法创建或打开一个内存映射文件,然后调用MemoryMappedViewAccessor类的泛型方法Write<T>()和Read<T>()向内存映射文件中写入和读取数据。
注意,泛型方法Write<T>()和Read<T>()中的泛型参数T必须是值类型(比如整型int和结构体struct),特别地,对于用户自定义的结构体,要求其成员也必须是值类型。
例如,以下结构体将无法写入到内存映射文件中,因为其成员Info是string类型的,这是一个引用类型。
public struct ErrorStruct
{
public string Info;
}
之所以要求泛型参数不能是引用类型,其道理非常简单,如果结构体中的某个成员是引用类型,那么在程序运行时,计算机无法知道应该向内存映射文件中写入多少个字节,因为引用类型的变量所引用的对象位于托管堆中,其占用存储空间的大小不经过计算是难以确定的,而完成这个计算工作将花费不少的系统资源(想想一个对象可能又会引用到另一个对象就明白了),这会严重影响内存映射文件读写操作效率。
两个进程不能交换引用类型的数据,这个限制似乎还不小,但事实上,我们完成可以通过对象序列化技术来突破这个限制,在两个进程间交换任意大小的对象(只要内存映射文件有足够的容量)。请看下一小节的示例UseMMFBetweenProcess2。
4 利用序列化技术通过内存映射文件实现进程通讯
图4 示例:UseMMFBetweenProcess2
如图 4所示,运行示例程序的多个实例,加载图片并输入图片说明,点击相应按钮后,可以在多个进程间直接交换以下格式的信息:
[Serializable]
class MyPic
{
public Image pic; //图片
public string picInfo; //图片信息说明
}
请注意这是一个引用类型的数据对象,并且它附加了可序列化“[Serializable]”的代码属性。
如果要向内存映射文件中序列化对象,必须将内存映射文件转换为可顺序读取的流。幸运的是,MemoryMappedFile类的CreateViewStream()方法可以创建一个MemoryMappedViewStream对象,通过它即可序列化对象,其代码框架如下:
//创建或打开内存映射文件
MemoryMappedFile memoryFile = MemoryMappedFile.CreateOrOpen(...);
//创建内存映射流
MemoryMappedViewStream stream = memoryFile.CreateViewStream();
//创建要在进程间交换的信息对象
MyPic obj =...;
//向内存映射流中序列化对象
IFormatter formatter = new BinaryFormatter();
stream.Seek(0, SeekOrigin.Begin);
formatter.Serialize(stream, obj);
请读者自行阅读源码了解更多技术细节。
=================================================
在CSDN下载频道下载本文示例源码及PDF文档