如何在32位程式中突破地址空間限制使用超過4G的記憶體
眾所周知,所有的32位應用程式都有4GB的程序地址空間,因為32位地址最多可以對映4GB的記憶體(對於虛擬地址空間概念不太熟悉的朋友建議去看一下《Windows核心程式設計》這本書)。對於Microsoft Windows作業系統,應用程式可以訪問2GB的程序地址空間(32位Linux可以訪問3GB地址空間),這就是稱為使用者模式的虛擬地址空間。這2GB的使用者模式虛擬地址空間位於4GB地址空間的低一半,而與之相對應的高一半2GB地址空間由作業系統核心使用,因此被成為核心模式的虛擬地址空間。在一個程序中,所有的執行緒讀共享相同的2GB使用者模式虛擬地址空間。
對於一般的應用程式來說,2GB的地址空間是足夠使用的了,但是對於一些特殊的需要使用海量記憶體的應用程式(典型的例子是資料庫系統)來說,2GB的地址空間就遠遠不夠了。為了緩解地址空間的不足,微軟提供了一個權宜的解決方案,所有從Windows 2000 Server開始的作業系統版本都提供了一個boot.ini啟動開關(/3GB),可以為應用程式提供訪問3GB的程序地址空間的能力,從而將核心模式的地址空間限定為1GB。以下就是一個開啟了3GB選項的boot.ini檔案示例:
timeout=30default=multi(0)disk(0)rdisk(0)partition(1)WINDOWS
[operating systems]
multi(0)disk(0)rdisk(0)partition(1)WINDOWS="Windows Server 2003, Enterprise"/fastdetect /3GB
雖然使用/3GB選項能夠將使用者模式的地址空間擴大50%(從2GB增加到3GB),但是對於資料庫系統這樣的應用程式來說,這1GB的地址空間的增加只能是杯水車薪,並不能解決多少問題,而且由於作業系統核心只能使用1GB地址空間,這樣可能會給作業系統的執行帶來一定的負面影響,因此除非沒有更好的解決方案,是不建議使用/3GB方式的。
鑑於像資料庫系統這樣的應用程式對海量記憶體的需求,Intel公司也覺得4GB的記憶體不夠用,因此就將CPU晶片中記憶體地址線由32根擴充套件到了36根(即最多64GB),這就是所謂的實體地址擴充套件(PAE:Physical Address Extension)。PAE使得作業系統或應用程式能夠最多使用64GB的實體記憶體,對於Windows系統(2000以上)來說,只需在boot.ini檔案中使用/PAE選項即可(類似於上面的/3GB選項)。需要提醒大家的是,如果沒有在boot.ini檔案中使用/PAE選項,那麼即使計算機已經配置了超過4GB的實體記憶體,在Windows作業系統中也不能使用超過4GB的那些記憶體(事實上,根據我的經驗,如果沒有使用/PAE選項,Windows系統最多隻能識別3.25GB的實體記憶體,我也不清楚為什麼不是4GB?如果有知道的,請告訴我一聲)。
雖然PAE使得在應用程式中使用超過4GB的實體記憶體成為可能,但是由於32位應用程式的虛擬地址空間並不隨著實體記憶體的增大而有任何變化,這意味著你不可能使用類似VirtualAlloc( GetCurrentProcess,2GB,...,...)這樣的函式=調直接分配接近使用者模式地址空間大小的記憶體區域。為了突破32位地址空間的限制,需要使用一種被成為地址視窗擴充套件(AWE:Address Windowing Extensions)的機制(參見上圖)。
AWE是Windows的記憶體管理功能的一組擴充套件,它使應用程式能夠使用的記憶體量超過通過標準32位定址可使用的2~3GB記憶體。AWE允許應用程式獲取實體記憶體,然後將非分頁記憶體的檢視動態對映到32位地址空間。雖然32位地址空間限制為4GB,但是非分頁記憶體卻可以遠遠大於4GB。這使需要大量記憶體的應用程式(如大型資料庫系統)能使用的記憶體量遠遠大於32位地址空間所支援的記憶體量。
在使用AWE機制時,需要注意以下幾點:
(1)AWE允許在32位體系結構上分配超過4GB的實體記憶體,只有當系統可用實體記憶體大於使用者模式的虛擬地址空間時,才應該使用AWE。
(2)若要使32位作業系統支援4GB以上的實體記憶體,必須在Boot.ini檔案啟用/PAE選項。
(3)若在Boot.ini檔案中啟用了/3GB選項,則作業系統最多能夠使用16GB的實體記憶體,因此如果實際的實體記憶體超過16GB,必須確保不使用/3GB選項。
(4)使用AWE分配的記憶體是非分頁的實體記憶體,這意味著這部分記憶體只能由分配的應用程式獨佔使用,不能由作業系統或其他程式使用,直到這些記憶體被釋放為止,這與通常的VirtualAlloc函式分配的虛擬記憶體存在顯著的不同,它不會參與分頁替換。
在Windows中,跟AWE相關的API函式有以下幾個:
HANDLE hProcess,
PULONG_PTR NumberOfPages,
PULONG_PTR UserPfnArray
);
BOOL WINAPI AllocateUserPhysicalPagesNuma(
HANDLE hProcess,
PULONG_PTR NumberOfPages,
PULONG_PTR PageArray,
DWORD nndPreferred
);
BOOL MapUserPhysicalPages(
PVOID lpAddress,
ULONG_PTR NumberOfPages,
PULONG_PTR UserPfnArray
);
BOOL MapUserPhysicalPagesScatter(
PVOID* VirtualAddresses,
ULONG_PTR NumberOfPages,
PULONG_PTR PageArray
);
BOOL FreeUserPhysicalPages(
HANDLE hProcess,
PULONG_PTR NumberOfPages,
PULONG_PTR UserPfnArray
);
各個函式的具體引數含義可以參考MSDN,其中AllocateUserPhysicalPagesNuma是Windows Vista和Windows 2008 Server新增的函式,用於支援NUMA(非一致性記憶體訪問)。以下就簡單說一下如何使用這幾個API函式來達到使用超過4GB的記憶體。
使用AllocateUserPhysicalPages函式分配需要的實體記憶體,使用方式如下:
BOOL bResult = AllocateUserPhysicalPages( GetCurrentProcess(),&NumberOfPages,aPFNs);
// 檢查分配記憶體是否成功if(!bResult)
{
// 分配識別,錯誤處理
// .....}
// 檢查實際分配的記憶體頁數if( NumberOfPages != xxx )
{
// ....}
需要注意的是,呼叫上述程式碼的使用者必須具有“Lock Pages in Memory”(記憶體中鎖定頁面)的許可權。此許可權使得使用者可以使用程序將資料保持在實體記憶體中,這樣可防止系統將資料分頁到磁碟上的虛擬記憶體中。行使此許可權會因降低可用隨機存取記憶體(RAM)的數量而顯著影響系統性能。需要在本地安全策略管理程式中給使用者賦予該許可權,如下圖所示:
給使用者分配了上述許可權之後,需要在程式中使用程式碼啟用該許可權,如下所示:
{
// 輸出錯誤資訊 ..........
}
///<summary>/// 設定或清除啟用AWE( Address Windowing Extensions )所需要的鎖住記憶體的許可權。
///</summary>///<param name="hProcess">/// 程序控制代碼。
///</param>///<param name="Enable">/// 設定或者清除標誌。
///</param>///<returns>/// 如果成功,則返回TRUE,否則返回失敗。
///</returns>BOOL AWESetLockPagesPrivilege( HANDLE hProcess, BOOL Enable )
{
HANDLE Token = NULL;
BOOL Result = FALSE;
TOKEN_PRIVILEGES Info = { 0 };
// 開啟令牌 Result = OpenProcessToken ( hProcess, TOKEN_ADJUST_PRIVILEGES, &Token );
if( !Result )
return FALSE;
// 設定許可權資訊 Info.PrivilegeCount =1;
Info.Privileges[0].Attributes = Enable? SE_PRIVILEGE_ENABLED : 0;
// 獲得鎖定記憶體許可權的ID Result = LookupPrivilegeValue ( NULL,SE_LOCK_MEMORY_NAME,&(Info.Privileges[0].Luid));
if( !Result )
{
CloseHandle( Token );
return FALSE;
}
// 調整許可權 Result = AdjustTokenPrivileges ( Token, FALSE,(PTOKEN_PRIVILEGES) &Info,0, NULL, NULL);
if( ( !Result ) || ( GetLastError() != ERROR_SUCCESS ) )
{
CloseHandle( Token );
return FALSE;
}
// 成功返回 CloseHandle( Token );
return TRUE;
}
使用AllocateUserPhysicalPages分配了實體記憶體之後,下一步就是使用MapUserPhysicalPages或MapUserPhysicalPagesScatter函式將實體記憶體對映進使用者模式地址空間內,這兩個函式用法差不多,只是第一個引數有差別。由於分配的實體記憶體的大小超過了使用者模式地址空間的大小,因此顯然不可能一次將所有的實體記憶體都對映到地址空間中。通常的做法是在使用者模式地址空間內分配一小塊連續的區域(即地址視窗),然後根據使用的需要動態將部分的實體記憶體對映到地址空間,這也就是“地址視窗擴充套件”一詞的真實含義。程式碼示例如下:
// 將實體記憶體對映到地址空間(根據需要,每次對映的頁面會不同,
// 即下面函式的第三個引數aPFNs會指向不同的物理頁)bResult = MapUserPhysicalPages( lpMemReserved,NumberOfPages,aPFNs);
// 以下就像普通的記憶體一樣使用lpMemReserved 指標來操作實體記憶體了...................
使用完了之後,可以使用FreeUserPhysicalPages來釋放分配的實體記憶體,示例如下:
// 釋放實體記憶體bResult = FreeUserPhysicalPages( GetCurrentProcess(),&NumberOfPages,aPFNs );
// 釋放地址視窗bResult = VirtualFree( lpMemReserved,0,MEM_RELEASE );
// 釋放物理頁號陣列delete[] aPFNs;
AWE機制被使用最多的一個場合是資料庫系統的快取管理器(BufferManager),例如SQL Server的記憶體管理器。雖然以上程式碼都是基於Windows作業系統,但是PAE和AWE機制並不是Windows特有的,32位Linux也有類似的API。完整使用AWE機制的例子,大家可以參考MySQL的原始碼。
最後想說的是,對於開發人員來說,一個好訊息是64位CPU和作業系統正越來越普及。在64位環境下,一個程序的使用者模式的地址空間可達8TB(也就是說目前很多的64位系統只使用了40幾位的記憶體地址,遠沒有充分使用64位的記憶體地址),在可以預見的未來很長一段時間,估計我們都不會再為地址空間不足而發愁了,讓我們一起為64位時代的到來而歡呼吧!