使用 .NET Framework 编程内存映射文件
.NET 应用程序中 MMF 和共享内存的介绍。
引言
首先,什么是内存映射文件 (MMF)?MMF 是一个内核对象,它将磁盘文件映射到一块内存地址空间,作为已提交的物理存储。通俗地说,MMF 允许您保留一个地址范围,并使用磁盘文件作为保留地址的物理存储。创建 MMF 时,您可以像访问内存一样访问映射的磁盘文件。MMF 使文件访问变得容易。例如,Windows 使用 MMF 加载 EXEs 或 DLLs。MMF 是几乎所有现代进程间通信 (IPC) 的基石。我发现早期 .NET Framework 版本不支持 MMF 时感到很不方便。好消息是,从 4.0 版本开始,.NET Framework 内置了对 MMF 的支持!
本文有两个目标
- 本教程将重点介绍 MMF 示例应用程序。有关理论讨论,请参阅 Jeffery Richter 的经典著作《Programming Applications for Microsoft Windows - 4th Ed.》第 17 章。有关 .NET Framework 4 MMF 规范,请查看此 MSDN 页面。
- 本教程是为熟悉 C# 语言的程序员编写的。C++ 的一般知识将有助于您理解共享内存透明性的讨论。要构建和运行这些示例,您应该在机器上安装 Visual Studio 2010。
假设您有一些用原生 C/C++ 编写的模块或应用程序,并且您想使用它们。旧模块使用 MMF 交换数据。在早期 .NET 版本不支持 MMF 的情况下,您可以采取以下几种方法:
- 调用 Win32 MMF 支持。您需要处理托管和非托管世界之间的许多封送操作。
- 您始终可以使用 Windows 调试符号支持,它允许您将变量作为符号进行访问。但是,这种机制相当复杂,并且超出了初学者的掌握范围。您还必须处理封送操作。
使用 .NET Framework 4,您只需编写一个支持 MMF 的应用程序即可直接与旧的 C/C++ 应用程序交换数据。本文将演示如何编写支持 MMF 的 .NET 应用程序。我们将从基本示例开始。然后,我们将深入探讨 MMF 在共享内存设计中的更高级用法。
简单的 MMF 应用程序
我们将演示三个 MMF 示例应用程序:
- 文件复制
- 简单的 IPC
- 单例实例
文件复制
第一个示例程序将现有文件的内容复制到新文件中。源代码本身具有解释性,并列在下面:
using System;
using System.IO;
using System.IO.MemoryMappedFiles;
namespace NetMMFCopyFile
{
class Program
{
static void Main(string[] args)
{
int offset = 0;
int length = 0;
byte[] buffer;
if (args.GetLength(0) != 2)
{
Console.WriteLine("Usage: NetMMFCopyFile.exe file1 file2");
return;
}
FileInfo fi = new FileInfo(args[0]);
length = (int)fi.Length;
// Create unnamed MMF
using (var mmf1 = MemoryMappedFile.CreateFromFile(args[0],
FileMode.OpenOrCreate, null, offset + length))
{
// Create reader to MMF
using (var reader = mmf1.CreateViewAccessor(offset,
length, MemoryMappedFileAccess.Read))
{
// Read from MMF
buffer = new byte[length];
reader.ReadArray<byte>(0, buffer, 0, length);
}
}
// Create disk file
using (FileStream fs = File.Create(args[1]))
{
fs.Close();
}
// Create unnamed MMF
using (var mmf2 = MemoryMappedFile.CreateFromFile(args[1],
FileMode.Create, null, offset + length))
{
// Create writer to MMF
using (var writer = mmf2.CreateViewAccessor(offset,
length, MemoryMappedFileAccess.Write))
{
// Write to MMF
writer.WriteArray<byte>(0, buffer, 0, length);
}
}
}
}
}
有几点值得注意。我们首先调用静态方法 CreateFromFile()
来创建一个 MMF 对象。MemoryMappedFile
类提供了几个重载的静态方法:
CreateFromFile()
从现有磁盘文件创建 MMF。CreateNew()
基于系统分页文件创建 MMF。CreateOrOpen()
创建一个新的 MMF 或打开一个现有的 MMF。OpenExisting()
打开一个现有的 MMF。
第二种和第三种方法创建非持久化 MMF。在 Windows 实现中,非持久化 MMF 由系统分页文件支持。它仅在 MMF 的作用域内存在。
我们调用 CreatedViewAccessor()
来创建对我们创建的 MMF 的视图。MMF 视图可以是只读的,也可以是读写的,具体取决于传递给调用的可访问性参数。视图可以映射 MMF 的全部或部分。当您通过视图访问数据时,您只能访问映射在视图中的数据。还有两个重载方法可以为 MMF 创建视图:
CreateViewAccessor()
从 MMF 创建一个访问对象。CreateViewStream()
从 MMF 创建一个流。
显然,您可以使用 CreateViewStream()
来修改上述示例应用程序,这留给您作为练习。为简洁起见,省略了对 copy-to 文件是否存在进行的检查。您应该添加逻辑来处理 copy-to 文件已存在的情况。
简单的 IPC
在第二个示例中,我们将讨论两个应用程序如何通过 MMF 交换数据。显然,我们可以扩展复制文件示例,使两个应用程序能够通过磁盘文件交换数据。基本上,一个应用程序将数据写入文件,另一个应用程序从同一个文件读取数据。
更好的机制是在不依赖磁盘文件存在的情况下交换数据。MMF 可以支持这一点。以下是创建 MMF 并向其中写入一些字节的完整代码:
using System;
using System.IO.MemoryMappedFiles;
using System.Threading;
namespace NetMMF_Provider
{
class Program
{
static void Main(string[] args)
{
int offset = 0;
int length = 32;
byte[] buffer = new byte[length];
if (args.GetLength(0) != 1)
{
Console.WriteLine("Usage: NetMMF_Provider.exe name");
return;
}
// Fill buffer with some data
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = (byte)('0' + i);
}
// Create named MMF
using (var mmf = MemoryMappedFile.CreateNew(args[0], offset + buffer.Length))
{
// Lock
bool mutexCreated;
Mutex mutex = new Mutex(true, "MMF_IPC", out mutexCreated);
// Create accessor to MMF
using (var accessor = mmf.CreateViewAccessor(offset, buffer.Length))
{
// Write to MMF
accessor.WriteArray<byte>(0, buffer, offset, buffer.Length);
}
mutex.ReleaseMutex();
// Press any key to exit...
Console.ReadKey();
}
}
}
}
显然,这次我们以不同的方式创建了 MMF。首先,我们调用了前面讨论过的 CreateNew()
。其次,CreateNew()
的第一个参数是我们创建的 MMF 的名称。我们将此 MMF 称为命名 MMF。命名的目的是在外部进程中识别 MMF。
这是访问 MMF 并从中读取数据的代码片段:
// Create named MMF
using (var mmf = MemoryMappedFile.OpenExisting(args[0]))
{
// Create accessor to MMF
using (var accessor = mmf.CreateViewAccessor(offset, buffer.Length))
{
// Wait for the lock
Mutex mutex = Mutex.OpenExisting("MMF_IPC");
mutex.WaitOne();
// Read from MMF
accessor.ReadArray<byte>(0, buffer, 0, buffer.Length);
}
}
在读取器代码中,我们调用 CreateOrOpen()
方法,因为 MMF 可能已经创建。基本上,如果它已被创建,我们就打开它;如果没有,我们就创建它。务必将正确的名称传递给调用。否则,您将创建一个新的 MMF,而不是像您认为的那样共享现有的 MMF。
您可能想知道为什么我们在编写器和读取器代码中都创建了一个互斥体。这是为了同步两个进程之间的数据访问。编写器锁定互斥体,写入 MMF,然后释放锁。读取器等待锁,直到编写器完成写入并释放它。然后读取器从 MMF 读取数据。这就是我们如何确保数据完整性,这与其他形式的 IPC 没有区别。
单例实例
所谓的“单实例”是指您只想在同一时间运行一个程序的实例。由于 MMF 可以唯一命名,因此您可以使用 MMF 作为令牌来指示您的应用程序是否已在运行。思路很简单:不能创建两个同名的 MMF。这是代码片段:
static void Main(string[] args)
{
string name = "NetMMFSingle";
// Create named MMF
try
{
var mmf = MemoryMappedFile.CreateNew(name, 1);
} catch
{
Console.WriteLine("Instance already running...");
return;
}
ConsoleKeyInfo key = Console.ReadKey();
// Your real code follows...
}
希望您发现 MMF 有用。我们已准备好探索它更多的强大功能。
共享内存
您可能时不时地听说过“共享内存”。这个术语通常指的是由一个或多个计算机工作站上运行的一个或多个应用程序共享的数据块。共享内存解决方案有几种不同的实现方式:硬件、软件或组合。本文重点介绍基于 MMF 的共享内存设计,这是您在简单 IPC 示例中所见内容的扩展。
当您设计共享内存解决方案时,一些基本功能是普遍需要的:
- 共享内存应以数据为导向,即所有要交换的数据都应封装在一个结构中,以便于访问。
- 对共享内存的访问应该是透明的,即使用共享内存就像直接使用数据结构一样。
- 对共享内存的访问应受到保护,以确保数据完整性。
以下代码表示 .NET 4 中共享内存类的有限实现:
using System;
using System.IO;
using System.Collections.Generic;
using System.IO.MemoryMappedFiles;
using System.Threading;
namespace NetSharedMemory
{
public class SharedMemory<T> where T: struct
{
// Constructor
public SharedMemory(string name, int size)
{
smName = name;
smSize = size;
}
// Methods
public bool Open()
{
try
{
// Create named MMF
mmf = MemoryMappedFile.CreateOrOpen(smName, smSize);
// Create accessors to MMF
accessor = mmf.CreateViewAccessor(0, smSize,
MemoryMappedFileAccess.ReadWrite);
// Create lock
smLock = new Mutex(true, "SM_LOCK", out locked);
}
catch
{
return false;
}
return true;
}
public void Close()
{
accessor.Dispose();
mmf.Dispose();
smLock.Close();
}
public T Data
{
get
{
T dataStruct;
accessor.Read<T>(0, out dataStruct);
return dataStruct;
}
set
{
smLock.WaitOne();
accessor.Write<T>(0, ref value);
smLock.ReleaseMutex();
}
}
// Data
private string smName;
private Mutex smLock;
private int smSize;
private bool locked;
private MemoryMappedFile mmf;
private MemoryMappedViewAccessor accessor;
}
}
现在我们编写一个示例应用程序来使用这个 SharedMemory
类。首先,我们创建一些数据。
public struct Point
{
public int x;
public int y;
}
public struct MyData
{
public int myInt;
public Point myPt;
}
为了保持代码简单,我将结构中的所有属性都声明为 public
。在实际应用中,您应该保护数据并通过属性公开它们。无论如何,现在我们编写一个类来使用 SharedMemory
。该类应从共享内存块读取数据,修改数据,最后将更改写回共享内存。
class Program
{
static void Main(string[] args)
{
SharedMemory<MyData> shmem =
new SharedMemory<MyData>("ShmemTest", 32);
if(!shmem.Open()) return;
MyData data = new MyData();
// Read from shared memory
data = shmem.Data;
Console.WriteLine("{0}, {1}, {2}",
data.myInt, data.myPt.x, data.myPt.y);
// Change some data
data.myInt = 1;
data.myPt.x = 2;
data.myPt.y = 3;
// Write back to shared memory
shmem.Data = data;
// Press any key to exit
Console.ReadKey();
// Close shared memory
shmem.Close();
}
}
代码非常简单。构建“SharedMemoryClient”项目。运行它的两个实例。您会注意到第二个实例读取的数据包含第一个实例所做的一些更改。
有几个地方您应该注意:
ShareMemory
是一个泛型(参数化)类,它接受一个结构作为类的参数。该结构参数是要共享的数据的占位符。- 在保护共享内存时,我们只锁定对数据块的写入,而不是读取。这纯粹是出于性能考虑的实际考虑。您可以根据应用程序的需求选择保护读取和写入。
- 我们执行的结构复制是浅拷贝,而不是深拷贝。换句话说,我们交换的数据不应包含引用类型。这很有意义,因为我们不知道数据块实际包含什么。浅拷贝通常足够了,因为共享内存是以数据为导向的设计,而不是面向对象的。
关注点
上面介绍的共享内存实现并未完全满足透明性的要求,这主要是由于 C# 中缺乏对指针操作的支持。在原生 C++ 中,重载“->
”运算符可以提供更好的解决方案。遗憾的是,我们无法像 C++ 的“->
”运算符那样重载 C# 的“.
”运算符。因此,属性和结构复制的组合使用是一种折衷的解决方案。
正如我们所见,MMF 可以有不同的实现。C++ 和 C# 实现 MMF 的方式不同。托管和非托管平台实现 MMF 的方式也不同。一个我们尚未解决的问题是:.NET 4 应用程序是否可以与用 VC++ 6.0 编写和构建的应用程序交换数据?答案是肯定的。在 Windows 内部,MMF 是一个命名的内核对象,就像互斥体一样。MMF 真正重要的是它的名称、它保留的内存大小以及内存地址的偏移量。请注意,大小和偏移量很重要,因为您希望确保您访问的数据不会被截断或以其他方式损坏!
历史
这是本文和源代码的第一个修订版。