讓.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 來調整??磥鞟WE對于那些實時性比較高的應用,比如游戲,動畫,通訊等還確實是一個福音。
由于需要鎖定物理內存,所以運行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內存的大小。
byte[] inputBuffer = new byte[1024];
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內存
public AweStream(UInt32 capacity)
{
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內存的過程是上面兩個過程的逆過程,這里就不再多講,有興趣可以看我的代碼。
下面是實例代碼下載位置

浙公網安備 33010602011771號