原文链接 : http://blogs.msdn.com/salvapatuel/archive/2009/06/08/working-with-memory-mapped-files-in-net-4.aspx
预备知识 : 本文需要你对 OS 内存管理有一定了解。
我想探索下即将到来的 .NET 4 中一些与众不同的新特性,而不是已被大众所熟知的动态类型、协变与逆变等特性。出于对性能增强的喜爱,接下来俺将发表几篇新特性的博文。
内存映射文件对于托管世界的开发人员来说,似乎就像是火星人一样陌生(画外音 : 还是回火星吧,地球上很危险嘀)。但是它确实已经行之经年,毫不客气的说,它在 OS 中的地位相当重要。实际上,任何想要共享数据的通信模型都会在幕后使用它。
那么内存映射文件究竟是个什么玩意呢?内存映射文件允许你保留一块地址空间,然后提交物理存储到这块空间(嗯,听起来是不是很像虚拟内存?)。这哥俩最主要的区别就在于物理存储来自于磁盘上已有的文件,而不是内存管理器。这样做有两大意图 :
译注 : Windows 操作系统自身就使用内存映射来执行 DLL,并装载和执行 EXE 文件。
内存映射文件是单一机器多进程间数据通信的最高效的方式。此外,如果你参考其他 IPC 方法,就会看到如下图所示的架构 :
Woo…,现在你对该技术的强大威力有个大概印象了吧(在 System.IO 命名空间下,它甚至可以完全替代 P/Invoke 技术)。
现在,让我来快速解释下它如何工作。我们有两种内存映射文件模型。一个呢,就是使用自定义文件。它可以是应用程序想要访问的任意文件。另一个呢,则是页面文件。我们将和内存管理器一块共享它(这是最常用的技术模型)。
先来介绍下自定义文件模型。首先我们要做的事就是为我们要使用的文件创建一个 FileStream,它可以是已有的文件或者新文件(记住,你应该以共享方式打开这个文件,否则其他进程就无法访问它)。创建合适的流之后,现在就可以创建内存映射文件了。如下所示 :
1: using System.IO.MemoryMappedFiles;
1: MemoryMappedFile memoryMappedFile = MemoryMappedFile.CreateFromFile(
2: new FileStream(@"E:\Temp\Map.mp", FileMode.Create), //Any stream will do it
3: "MyMemoryMappedFile", //Name
4: 1024 * 1024, //Size in bytes
5: MemoryMappedFileAccess.ReadWrite); //Access type
这里,我使用了 MemoryMappedFile 类的一个特简单的构造函数。我们定义了要用到的流,然后给内存映射文件起了个名字。此外,我们还需要知道内存映射文件的大小(按字节计)和访问的类型。这样,我们就创建了一个内存映射文件。但是在开始使用它之前,我们还需要一个映射视图。接下来,我们就来创建它 :
MemoryMappedViewAccessor FileMapView = memoryMappedFile.CreateViewAccessor();
这个映射覆盖了整个文件。如果我们现在就要从内存映射文件读或写信息,只需要调用带有正确偏移量的映射视图方法就行了。
int number = 1234;
FileMapView.Write(0, number);
FileMapView.Write<Container>(4, ref container);
我们可以往任何内置类型或带有泛型约束的自定义类型写入数据。内存映射文件有个很好的特性就是持久性。在你关闭它之前,里面的内容都会堆积在磁盘上。这对于应用程序间共享缓存信息非常有用。
Well,其它进程如何读取该内存映射文件的内容呢。一样,我们先创建一个内存映射文件。不过我们用的是另外的构造函数,以打开已有的内存映射文件。如果不存在,再新建也不迟。译注 : 原文中的代码示例是错误的。
MemoryMappedFile MemoryMapped = MemoryMappedFile.CreateOrOpen(
"MyMemoryMappedFile",
1024 * 1024,
MemoryMappedFileAccess.ReadWrite);
接下来创建映射视图来读取信息,如下所示 :
using (MemoryMappedViewAccessor FileMap = memoryMappedFile.CreateViewAccessor())
{
Container newContainer = new Container();
FileMap.Read<Container>(4, out newContainer);
}
看,是不是很简单?但是这种方法有个小小的缺点,这和内存映射文件的大小有关。如果事先不知道大小的话,为了以防万一,你可能会构造一个超级大的文件。可是这样就浪费了大量的地址空间。毕竟你保留的地址远比你提交的物理空间要大,不是吗?
为了解决这个问题,我们可以使用页面文件。这对于百忙中提交数据非常有利。但同时也引入了一个新问题 : 你不会拥有自己的文件,而且映射会一直持续到最后的句柄销毁之后。不过仔细想想,这种方案挺合理的。
现在我们重新构造下前面的示例。这次,我将使用最完整参数的构造函数,这样还可以介绍一些其他的特性。当然,这些特性同样适用于自定义文件。
MemoryMappedFileSecurity customSecurity = new MemoryMappedFileSecurity();
MemoryMappedFile pagedMemoryMapped = MemoryMappedFile.CreateNew(
@"Salvador", //Name
1024*1024, //Size
MemoryMappedFileAccess.ReadWrite, //Access type
MemoryMappedFileOptions.DelayAllocatePages, //Pseudo reserve/commit
customSecurity, //You can customize the security
HandleInheritability.Inheritable //Inherit to child process
);
MemoryMappedFileSecurity 类允许你自定义哪个进程可以访问资源。当你想要保护敏感信息时,或者你不想其他进程改变此内存映射文件时,这个特性非常有用。你可以查看这个对象所有你想要改变的不同设置。在这里你可以获得更详细的信息。如果我们想要探究内存映射文件结构的话,不需要构造一个流,只要找到该资源的名字即可。这会把映射名称和基于文件大小的区域链接起来。如此多个进程间就知道如何访问文件了。注意,使用 DelayAllocatePages 属性后,只有在必要的时候才会预留/提交地址空间。最后一个有意思的参数就是句柄继承模型了。它允许在必要的时候,可以喝子进程共享资源。
如何访问文件就跟前述的示例一样了。千万记住,如果你关闭了内存映射文件,那么它就不能访问了。这个问题困扰了很多开发人员。
最后,让我们来看一下另外一个有意思的领域 : 创建多个映射视图。它们同时工作访问同一个内存映射文件的不同区域。这个时候你就得适当保护内容和同步访问。
我们可以定义不同的偏移量和长度来创建自己的映射视图。如下所示 :
MemoryMappedViewAccessor writeMapView = memoryMappedFile.CreateViewAccessor(0, 1024,
MemoryMappedFileAccess.ReadWrite);
MemoryMappedViewAccessor readMapView = memoryMappedFile.CreateViewAccessor(1025, 1024,
MemoryMappedFileAccess.Read);
现在你可以享受 .NET 4.0 所带来的高性能数据共享模型的强大威力了。注意,本文基于 .NET 4.0 BETA1 创作。
译注 : UNIX 没有使用映射文件来实现内存共享,它有显式的内存共享 API 函数。这些函数式 shmget、shmctl、shmat、shmdt,以及 mmap、munmap。最后两个函数虽然跟本文所讲的有点类似,但是它们没有映射对象。