原文链接 : http://blogs.msdn.com/salvapatuel/archive/2009/06/08/working-with-memory-mapped-files-in-net-4.aspx
预备知识 : 本文需要你对 OS 内存管理有一定了解。
我想探索下即将到来的 .NET 4 中一些与众不同的新特性,而不是已被大众所熟知的动态类型、协变与逆变等特性。出于对性能增强的喜爱,接下来俺将发表几篇新特性的博文。
内存映射文件对于托管世界的开发人员来说,似乎就像是火星人一样陌生(画外音 : 还是回火星吧,地球上很危险嘀)。但是它确实已经行之经年,毫不客气的说,它在 OS 中的地位相当重要。实际上,任何想要共享数据的通信模型都会在幕后使用它。
那么内存映射文件究竟是个什么玩意呢?内存映射文件允许你保留一块地址空间,然后提交物理存储到这块空间(嗯,听起来是不是很像虚拟内存?)。这哥俩最主要的区别就在于物理存储来自于磁盘上已有的文件,而不是内存管理器。这样做有两大意图 :
- 1.访问磁盘文件上的数据不需要执行文件 I/O 操作和缓存文件内容。当你访问超大型文件数据时,作用尤其显著。
- 2.你能使用内存映射文件让运行在同一台机器上的多个进程共享彼此数据。
译注 : 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。最后两个函数虽然跟本文所讲的有点类似,但是它们没有映射对象。