如果你有Windows API开发背景,你会意识到一种老技术叫做内存映射文件(memory-mapped files,有时所写成MMF)。内存映射文件或是文件映射的想法就是将文件加载到内存中,这样它会作为一个连续块在你的应用程序的地址空间中出现。然后,读取和写入文件是访问正确内存位置的最简单方法。事实上,当操作系统加载器获取你应用程序的EXE或DLL文件来执行它们的代码的时候,文件映射会在幕后被使用。(ASP.NET2.0的URL映射的实现方法)
使用来自.NET应用程序的内存映射文件本身已不再新鲜,因为通过使用在.NET1.0中的Platform Invoke (P/Invoke),它可能使用底层操作系统APIs。但是,在.NET4.0中,使用内存映射文件可适用于所有没有直接使用Windows APIs管理代码的开发者们。
内存映射文件和大文件总是在开发者的思想中出现,但是没有实际的限制来考虑到底内存可以映射多大或是多小的文件。虽然对大文件使用内存映射会使编程变得简单,但是你会观察到当使用更小一点的文件的时候执行得会更好,因为它们可以适用于整个文件系统缓存。
本文中的信息和代码列表是基于2009年5月发布的.NET 4.0 Beta 1版本。由于是预发布的软件,一旦最后的.NET的RTM版本确定,所有技术细节,类名称和方法会改变。在研究或是开发任何测试库的时候一定要牢记这一点。(VB.NET中的动态代码生成技巧)
新命名空间和它的类
对于.NET4.0 开发者来说,与内存映射文件一起工作的最有趣的类是存在于新的System.IO.MemoryMappedFiles命名空间中。目前,这个命名空间包含四个类和一些列举来帮助你访问并保护你的文件映射。实际的执行是在集合System.Core.dll中。
对于开发者最重要的类是MemoryMappedFile类。这个类允许你创建一个内存映射对象,反过来你可以创建一个视图访问对象。然后你可以使用这个accessor直接操作来自文件映射的内存块。通过使用Read 和Write方法完成这个操作。
注意的是直接指针在管理的世界中不被视为一种良好的编程习惯,这样的方法对象是需要保持整洁的。在传统的Windows API 开发的本地代码中,你只会获得一个指针来启动你的内存块。
尽管如此,获取一个内存映射文件的过程和必要的accessor对象,你需要遵循三个简单的步骤。首先,你需要一个文件流对象指向在磁盘上的(一个现有的)文件。第二,从这个文件中你可以创建映射对象,最后一个步骤,你创建accessor对象。这里有一段C#代码示例:
FileStream file= new FileStream(
@"C:\Temp\MyFile.dat", FileMode.Open); MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file); MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(); |
这个代码首先打开带有System.IO.FileStream类的一个文件,然后将流对象实例传递给MemoryMappedFile类的静态CreateFromFile方法。第三步是调用MemoryMappedFile类的CreateViewAccessor方法。
在上面的代码中,CreateViewAccessor方法在没有任何参数的情况下被调用。在这种情况下,映射从文件开头(offset零)开始,以文件的最后字节为结束。你可以轻松的映射任何文件的部分。例如,如果你的文件有十亿字节的大小,然后你可以映射,用以下调用可以完成:
MemoryMappedViewAccessor accessor =
mmf.CreateViewAccessor(1024 * 1024, 10000);
然后,你将看到对于这些映射视图的更先进的使用,但是首先,你必须学习从这个视图中读取。
从映射文件中读取
要使用先前的映射内存地址,你需要使用MemoryMappedViewAccessor类的方法。例如,要从文件映射的开端读取10个字节,你应该使用ReadByte方法,如下:
...
MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor(); byte[] buffer = new byte[10]; for (int index = 0; index < buffer.Length; index++) { buffer[index] = accessor.ReadByte(index); } |
这个Read方法或者可以填入给出的一般对象的内容,或者可以通过使用泛型 Read or ReadArray而采取更具体的对象。例如,假设你有一个Guid类型的对象(定义为一个结构),然后两个ReadNNN方法调用,以下有相似的结果:
// method1:
byte[] buffer = new byte[16]; accessor.ReadArray(0, buffer, 0, buffer.Length); Guid guid = new Guid(buffer); MessageBox.Show(guid.ToString()); // method 2: Guid guid2 = new Guid(); accessor.Read(0, out guid2); MessageBox.Show(guid2.ToString()); |
注意的是在两个Read方法调用中,你需要指定读取开始的位置。这个基于零的offset与映射视图总是相对的,但不一定是原始文件。当你创建内存映射对象的时候,你需要指定你想要操作文件(图1)的内存窗口。如果你没有指定任何的offset,像是以上代码列表中的,然后该视图被假定是从文件的开端开始的。
图1. View offsets总是与mapped view相对
为了帮助提供灵活性,你可以从零offset开始并运行直到文件的长度或你可以从中间开始,并只映射文件的一部分。通过offsets相对视图,accessor对象的读取就可以完成。也就是说,原始文件的offset成为view的 起始offset加上view offset。
要记住内存映射对象和文件下有操作系统处理。因此,重要的是要记住在完成它们之后要处理这些对象。否则它们将保留无限大的开放时间直到垃圾回收的进入。最好的办法就是使用try-finally blocks或是使用C#语句。
如果你用.NET流对象工作非常开心,但是仍然希望从内存映射文件中受益,那么你就是幸运的。MemoryMappedFile类包含一个很方便的方法叫做CreateViewStream,,可以返回一个MemoryMappedViewStream对象。这个对象允许序列访问映射视图;这个可能是使用映射视图流(mapped view streams)与使用允许随即访问的accessor对象相比的最大缺点。但是如果你不介意这个局限,CreateViewStream方法就是你的朋友。