让.Net 应用程序突破2G的内存访问限制
Author: Eaglet
32位Windows操作系统下单个进程的用户模式内存访问的限制是2G,如果在boot.ini中设置了/3G开关,则最大为3G,超过3G将无法访问。由于Hubble.net 项目是一个数据库系统,必须要考虑使用大内存缓存数据的问题,于是最近对这个问题进行了一些研究。其实这块的技术是现成的,32位操作系统下只有通过 AWE的方式来扩展内存。这块的文章也很多,但很少有.net 下如何使用的实例,我做了一个类似MemoryStream的封装,可以让.Net程序员轻松操作AWE内存,从而使其程序轻松突破2G内存的限制。
在开始这篇文章之前,我们还是先来了解一下AWE.
AWE (Address Windowing Extensions)是 Windows 的内存管理功能的一组扩展,它使应用程序能够使用的内存量超过通过标准 32 位寻址可使用的 2-3 GB 内存。AWE 允许应用程序获取物理内存,然后将非分页内存的视图动态映射到 32 位地址空间。虽然 32 位地址空间限制为 4 GB,但是非分页内存却可以远远大于 4 GB。这使需要大量内存的应用程序(如大型数据库系统)能使用的内存量远远大于 32 位地址空间所支持的内存量。
如上图所示AWE 实际上就是将用户模式下的32位内存地址映射到用户需要访问的物理内存上去。不同操作系统运行被映射的物理内存大小是不一样的。
Vista, XP 和 Windows 2003 标准版 最多可以映射 4G 内存。
Windows 2003 企业版的限制是32G (要使用超过4G的内存必须打开 /PAE 开关)
Windows 2003 数据中心版本限制是64G (要使用超过4G的内存必须打开 /PAE 开关)
由于被映射的物理内存为不分页内存,无法进行页保护,为了保证内存使用的安全,防止其他进程越界访问,AWE 在映射这些内存之前必须将这些内存锁定,即只有锁定这块内存的进程可以访问这块内存,其它进程无法访问。这里就产生了一个有趣的现象,我们可以在 windows 下像实时操作系统那样操作物理内存,而不用担心操作系统进行页交换时对系统实时性的影响。虽然不采用AWE,也可以通过VirtualLock API函数来锁定物理内存,但这个函数在一个进程中最多可以锁定30个页面,以一个页大小4096来计算,最多可以锁定30*4094字节的内存。当然这是默认设置,你也可以通过调整工作 WorkingSet 来调整。看来AWE对于那些实时性比较高的应用,比如游戏,动画,通讯等还确实是一个福音。
由于需要锁定物理内存,所以运行AWE功能的程序,必须要具备锁定内存的权限,系统管理员帐号是没有这个权限的,只有 System帐号有这个权限。当然你也可以在本地安全设置中指定某个帐号拥有这个权限。方法如下:
gpedit.msc ->Windows Settings->Security Settings->Local Policies->User Rights Assignment->Lock pages in memory
谈完锁定内存的问题,我们再看看上面那个图,我们会发现虽然AWE允许访问最多64G的内存,但这64G内存是被AWE映射到一个32位的用户模式下的内存地址中去的,也就是说通常情况下,我们最多可以同时访问64G内存中的2G内存(如果配置了/3G开关,可以同时访问最多16G内存中的3G内存),如果要访问整个64G的内存,我们需要将一些不访问的内存取消映射,这样可以空出足够的用户模式下的虚拟内存地址来访问我们需要访问的内存。因此我封装的类中添加了Map和UnMap两个方法,让调用者可以根据实际情况来决定映射和去映射。2G的32位虚拟内存地址对于我们来是是如此的宝贵,调用者在贪婪的消耗大量内存时一定要注意节约这个资源。
谈完这些东西,下面让我们结合代码来看看在.Net 下如何来操作AWE 内存吧。
为了方便.Net 程序员访问AWE内存,我封装了一个AweStream类,这个类继承自Stream类。.Net程序员可以像操作普通的MemoryStream流那样操作AWE内存。同时我还为那些对效率要求非常苛刻的调用者提供了一个通过指针访问AWE内存的方法。
调用示例如下:注意必须在构造函数中指明申请的AWE内存的大小。
Stopwatch stopWatch = new Stopwatch();
using (AweStream.AweStream aweStream = new AweStream.AweStream(1024 * 1024 * 100))
{
//Map
aweStream.Map();
stopWatch.Start();
//Copy one bytes
//Use unsafe pointer
for (int i = 0; i < 1024 * 1024 * 100; i++)
{
unsafe
{
aweStream.LpMemory[i] = 1;
}
}
stopWatch.Stop();
Console.WriteLine(stopWatch.ElapsedMilliseconds);
aweStream.Position = 0;
//Block copy
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < 1024 * 100; i++)
{
unsafe
{
aweStream.Write(inputBuffer, 0, 1024);
}
}
stopWatch.Stop();
Console.WriteLine(stopWatch.ElapsedMilliseconds);
//UnMap
aweStream.UnMap();
}
我在 6G内存 windows 2003 企业版的环境中做了测试,申请内存到5G以上没有任何问题。
下面再看看如何来申请AWE内存
下面的AweStream构造函数完成了对AWE内存的申请过程。
整个申请过程分为下面几步
1、为当前进程申请锁定内存的权限(注意 调用进程的帐号必须具备锁定内存的权限,否则这一步会失败)
2、就是需要申请的页面数量
3、通过 AllocateUserPhysicalPages API 申请AWE内存
{
unsafe
{
// Enable the privilege of lock memory.
lock (_SetLockPagesPrivilegeLockObj)
{
if (!_SetLockPagesPrivilegeOk)
{
LoggedSetLockPagesPrivilege.SetLockPagesPrivilege(System.Diagnostics.Process.GetCurrentProcess(), true);
_SetLockPagesPrivilegeOk = true;
}
}
General.SYSTEM_INFO sysInfo;
General.GetSystemInfo(out sysInfo); // fill the system information structure
_PageSize = sysInfo.dwPageSize;
if ((capacity % _PageSize) != 0)
{
_NumberOfPages = capacity / _PageSize + 1;
}
else
{
_NumberOfPages = capacity / _PageSize;
}
_PFNArraySize = (UInt32)(_NumberOfPages * sizeof(UInt64*)); // memory to request for PFN array
_PFNArray = Marshal.AllocHGlobal((int)_PFNArraySize);
UInt32 numberOfPagesInitial = _NumberOfPages;
if (!AweApi.AllocateUserPhysicalPages(System.Diagnostics.Process.GetCurrentProcess().Handle,
ref _NumberOfPages, _PFNArray))
{
Dispose();
throw new AweStreamException("Cannot allocate physical pages", AweStreamException.Reason.CannotAllocatePhysicalPages);
}
_AweAllocated = true;
if (numberOfPagesInitial != _NumberOfPages)
{
Dispose();
throw new AweStreamException(string.Format("Allocated only {0} pages.", _NumberOfPages),
AweStreamException.Reason.AweMemoryNotEnough);
}
_Capacity = _PageSize * _NumberOfPages;
}
}
AWE内存申请完毕后并不能被立即访问到,我们必须将其映射到32位内存地址中才可以访问。
下面是内存映射的代码:
也很简单:
首先先通过VirtualAlloc函数申请一块32位虚拟内存区域
然后通过 MapUserPhysicalPages API 函数将AWE内存映射到这个虚拟内存地址区域。
{
unsafe
{
if (IsMapped)
{
return;
} if (readOnly)
{
_VirtualAddress = AweApi.VirtualAlloc(null, Capacity, AweApi.MEM_RESERVE | AweApi.MEM_PHYSICAL,
AweApi.PAGE_READONLY);
}
else
{
_VirtualAddress = AweApi.VirtualAlloc(null, Capacity, AweApi.MEM_RESERVE | AweApi.MEM_PHYSICAL,
AweApi.PAGE_READWRITE);
} if (_VirtualAddress == null)
{
throw new AweStreamException("Cannot reserve memory.", AweStreamException.Reason.CannotReserveMemory);
} if (!AweApi.MapUserPhysicalPages(_VirtualAddress, _NumberOfPages, _PFNArray))
{
AweApi.VirtualFree(_VirtualAddress, Capacity, AweApi.MEM_RELEASE);
_VirtualAddress = null;
throw new AweStreamException(string.Format("MapUserPhysicalPages failed ({0})", General.GetLastError()),
AweStreamException.Reason.MapUserPhysicalPagesFail);
}
_CanWrite
= !readOnly;}
}
去映射和归还AWE内存的过程是上面两个过程的逆过程,这里就不再多讲,有兴趣可以看我的代码。
下面是实例代码下载位置