1. 程式人生 > >讓32位應用程式不再為2G記憶體限制苦惱

讓32位應用程式不再為2G記憶體限制苦惱

最近在做個程式,雖然是小型程式,但是使用的記憶體量卻很大,動輒達到10G。在64位系統上可以輕鬆實現,無奈我是基於32位的系統進行開發,程式還沒跑起來就已經被終止了。 
    試過很多辦法,包括檔案記憶體對映等,效率不高,而且由於32位應用程式的限制,可用的記憶體地址最高只能到0x7FFFFFFF,能呼叫的記憶體到2G就是極限了。最後好不容易找到了AWE(Address Windowing Extensions)。
    AWE是Windows的記憶體管理功能的一組擴充套件,它允許應用程式獲取實體記憶體,然後將非分頁記憶體的檢視動態對映到32位地址空間。雖然32位地址空間限 製為4GB,但是非分頁記憶體卻可以遠遠大於4GB。這使需要大量記憶體的應用程式(如大型資料庫系統)能使用的記憶體量遠遠大於32位地址空間所支援的記憶體 量。
    與AWE有關的函式在後面介紹。
    為了使用大容量記憶體,除了要用到AWE外,還有一樣東西不能少,那就是PAE(Physical Address Extension)。PAE是基於x86的伺服器的一種功能,它使執行Windows Server 2003,Enterprise Edition 和Windows Server 2003,Datacenter Edition 的計算機可以支援 4 GB 以上實體記憶體。實體地址擴充套件(PAE)允許將最多64 GB的實體記憶體用作常規的4 KB頁面,並擴充套件核心能使用的位數以將實體記憶體地址從 32擴充套件到36。
    一般情況下,windows系統的PAE沒有生效,只有開啟了PAE後windows系統才可以識別出4G以上的記憶體。在使用boot.int的系統中, 要啟動PAE必須在boot.ini中加入/PAE選項。在Windows Vista和Windows7中則必須修改核心檔案,同時設定BCD啟動項。針對Vista系統和Win7系統可以使用Ready For 4GB這個軟體直接完成這一操作,具體方法見Ready For 4GB的軟體說明。以下就是一個開啟了/PAE選項的boot.ini檔案示例:

  1. [boot loader]  
  2. timeout=30
  3. default=multi(0)disk(0)rdisk(0)partition(1)WINDOWS  
  4. [operating systems]  
  5. multi(0)disk(0)rdisk(0)partition(1)WINDOWS="Windows Server 2003, Enterprise" /fastdetect /PAE  


    本文將以Windows 7旗艦版為例介紹如何在開啟PAE的情況下使用AWE在程式中達到使用2G以上記憶體的目的。下圖分別為開啟PAE和未開啟PAE時系統識別出的記憶體容量區別。 

圖一.開啟PAE
 開啟PAE

圖二.關閉PAE

關閉PAE


    如果沒有開啟PAE,系統只能認出3G的記憶體,最多可以再多0.5G不到,這樣即使使用AWE,由於系統和其他應用程式已經佔去了一部分記憶體,剩下的記憶體或許也只有2G多一點了,沒什麼太大提高。只有當系統認出了4G以上的記憶體,AWE才能發揮它真正的作用。

    下面我們看看windows中給出的有關AWE的API函式,它們都定義在winbase.h中。

  1. #if (_WIN32_WINNT >= 0x0500)
  2. //
  3. // Very Large Memory API Subset
  4. //
  5. WINBASEAPI  
  6. BOOL
  7. WINAPI  
  8. AllocateUserPhysicalPages(  
  9.     __in    HANDLE hProcess,  
  10.     __inout PULONG_PTR NumberOfPages,  
  11.     __out_ecount_part(*NumberOfPages, *NumberOfPages) PULONG_PTR PageArray  
  12.     );  
  13. WINBASEAPI  
  14. BOOL
  15. WINAPI  
  16. FreeUserPhysicalPages(  
  17.     __in    HANDLE hProcess,  
  18.     __inout PULONG_PTR NumberOfPages,  
  19.     __in_ecount(*NumberOfPages) PULONG_PTR PageArray  
  20.     );  
  21. WINBASEAPI  
  22. BOOL
  23. WINAPI  
  24. MapUserPhysicalPages(  
  25.     __in PVOID VirtualAddress,  
  26.     __in ULONG_PTR NumberOfPages,  
  27.     __in_ecount_opt(NumberOfPages) PULONG_PTR PageArray  
  28.     );  
  29. //...
  30. #endif

    從winbase.h中的定義可以看出,只有當你的系統版本大於或等於0x0500時,才能夠使用AWE。各個版本的_WIN32_WINNT值見下表,Windows 2000以下的版本不能使用AWE。

Minimum system required

Minimum value for _WIN32_WINNT and WINVER

Windows 7

0x0601

Windows Server 2008

0x0600

Windows Vista

0x0600

Windows Server 2003 with SP1, Windows XP with SP2

0x0502

Windows Server 2003, Windows XP

0x0501

Windows 2000

0x0500

    如果你的系統版本符合要求,但是編譯器在編譯加入了AWE API的程式碼出錯,可以在程式標頭檔案中加入下面的程式碼。

  1. #ifndef _WIN32_WINNT
  2. #define _WIN32_WINNT 0x0501
  3. #endif  

    下面簡要介紹一下每個API的功能。

  1. BOOL WINAPI AllocateUserPhysicalPages(  //分配實體記憶體頁,用於後面AWE的記憶體對映
  2.   __in     HANDLE hProcess,     //指定可以使用此函式分配的記憶體頁的程序
  3.   __inout  PULONG_PTR NumberOfPages,    //分配的記憶體頁數,頁的大小由系統決定
  4.   __out    PULONG_PTR UserPfnArray  //指向儲存分配記憶體頁幀成員的陣列的指標
  5. );  
  6. BOOL WINAPI FreeUserPhysicalPages(  //釋放AllocateUserPhysicalPages函式分配的記憶體
  7.   __in     HANDLE hProcess,     //釋放此程序虛擬地址空間中的分配的記憶體頁
  8.   __inout  PULONG_PTR NumberOfPages,    //要釋放的記憶體頁數
  9.   __in     PULONG_PTR UserPfnArray  //指向儲存記憶體頁幀成員的陣列的指標
  10. );  
  11. BOOL WINAPI MapUserPhysicalPages(   //將分配好的記憶體頁對映到指定的地址
  12.   __in  PVOID lpAddress,        //指向要重對映的記憶體區域的指標
  13.   __in  ULONG_PTR NumberOfPages,    //要對映的記憶體頁數
  14.   __in  PULONG_PTR UserPfnArray     //指向要對映的記憶體頁的指標
  15. );  

    在看例項程式前還有一些設定需要做,需要對系統的本地安全策略進行設定。在win7中,開啟“控制面板->系統和安全->管理工具->本地安全策略”,給“鎖定記憶體頁”添加當前使用者,然後退出,重啟(不重啟一般無法生效!)。

本地安全策略

    經過前面的準備(再囉嗦一次:確認自己的電腦裝有4G或4G以上的記憶體;開啟PAE,使系統認出4G或以上的記憶體;設定好本地安全策略),我們就可以通過下面的程式碼來做個實驗了。

    程式碼是從MSDN中AWE的一個Example修改而來的,具體流程見程式碼中的註釋,如果對該Example的原始碼有興趣可以參考MSDN。

  1. #include "AWE_TEST.h"
  2. #include <windows.h>
  3. #include <stdio.h>
  4. #define MEMORY_REQUESTED ((2*1024+512)*1024*1024) //申請2.5G記憶體,測試機上只有4G記憶體,而且系統是window7,比較佔記憶體.申請3G容易失敗.
  5. #define MEMORY_VIRTUAL 1024*1024*512        //申請長度0.5G的虛擬記憶體,即AWE視窗.
  6. //檢測"鎖定記憶體頁"許可權的函式
  7. BOOL LoggedSetLockPagesPrivilege ( HANDLE hProcess, BOOL bEnable);  
  8. void _cdecl main()  
  9. {  
  10.     BOOL bResult;                   // 通用bool變數
  11.     ULONG_PTR NumberOfPages;        // 申請的記憶體頁數
  12.     ULONG_PTR NumberOfPagesInitial; // 初始的要申請的記憶體頁數
  13.     ULONG_PTR *aPFNs;               // 頁資訊,儲存獲取的記憶體頁成員
  14.     PVOID lpMemReserved;            // AWE視窗
  15.     SYSTEM_INFO sSysInfo;           // 系統資訊
  16.     INT PFNArraySize;               // PFN佇列所佔的記憶體長度
  17.     GetSystemInfo(&sSysInfo);  // 獲取系統資訊
  18.     printf("This computer has page size %d./n", sSysInfo.dwPageSize);  
  19.     //計算要申請的記憶體頁數.
  20.     NumberOfPages = MEMORY_REQUESTED/sSysInfo.dwPageSize;  
  21.     printf ("Requesting %d pages of memory./n", NumberOfPages);  
  22.     // 計算PFN佇列所佔的記憶體長度
  23.     PFNArraySize = NumberOfPages * sizeof (ULONG_PTR);  
  24.     printf ("Requesting a PFN array of %d bytes./n", PFNArraySize);  
  25.     aPFNs = (ULONG_PTR *) HeapAlloc(GetProcessHeap(), 0, PFNArraySize);  
  26.     if (aPFNs == NULL)   
  27.     {  
  28.         printf ("Failed to allocate on heap./n");  
  29.         return;  
  30.     }  
  31.     // 開啟"鎖定記憶體頁"許可權
  32.     if( ! LoggedSetLockPagesPrivilege( GetCurrentProcess(), TRUE ) )   
  33.     {  
  34.         return;  
  35.     }  
  36.     // 分配實體記憶體,長度2.5GB
  37.     NumberOfPagesInitial = NumberOfPages;  
  38.     bResult = AllocateUserPhysicalPages( GetCurrentProcess(),  
  39.         &NumberOfPages,  
  40.         aPFNs );  
  41.     if( bResult != TRUE )   
  42.     {  
  43.         printf("Cannot allocate physical pages (%u)/n", GetLastError() );  
  44.         return;  
  45.     }  
  46.     if( NumberOfPagesInitial != NumberOfPages )   
  47.     {  
  48.         printf("Allocated only %p pages./n", NumberOfPages );  
  49.         return;  
  50.     }  
  51.     // 保留長度0.5GB的虛擬記憶體塊(這個記憶體塊即AWE視窗)的地址
  52.     lpMemReserved = VirtualAlloc( NULL,  
  53.         MEMORY_VIRTUAL,  
  54.         MEM_RESERVE | MEM_PHYSICAL,  
  55.         PAGE_READWRITE );  
  56.     if( lpMemReserved == NULL )   
  57.     {  
  58.         printf("Cannot reserve memory./n");  
  59.         return;  
  60.     }  
  61.     char *strTemp;  
  62.     for (int i=0;i<5;i++)  
  63.     {  
  64.         // 把實體記憶體對映到視窗中來
  65.         // 分5次對映,每次對映0.5G實體記憶體到視窗中來.
  66.         // 注意,在整個過程中,lpMenReserved的值都是不變的
  67.         // 但是對映的實際實體記憶體卻是不同的
  68.         // 這段程式碼將申請的2.5G實體記憶體分5段依次對映到視窗中來
  69.         // 並在每段的開頭寫入一串字串.
  70.         bResult = MapUserPhysicalPages( lpMemReserved,  
  71.             NumberOfPages/5,  
  72.             aPFNs+NumberOfPages/5*i);  
  73.         if( bResult != TRUE )   
  74.         {  
  75.             printf("MapUserPhysicalPages failed (%u)/n", GetLastError() );  
  76.             return;  
  77.         }  
  78.         // 寫入字串,雖然是寫入同一個虛存地址,
  79.         // 但是視窗對映的實際記憶體不同,所以是寫入了不同的記憶體塊中
  80.         strTemp=(char*)lpMemReserved;  
  81.         sprintf(strTemp,"This is the %dth section!",i+1);  
  82.         // 解除對映
  83.         bResult = MapUserPhysicalPages( lpMemReserved,  
  84.             NumberOfPages/5,  
  85.             NULL );  
  86.         if( bResult != TRUE )   
  87.         {  
  88.             printf("MapUserPhysicalPages failed (%u)/n", GetLastError() );  
  89.             return;  
  90.         }  
  91.     }  
  92.     // 現在再從5段記憶體中讀出剛才寫入的字串
  93.     for (int i=0;i<5;i++)  
  94.     {  
  95.         // 把實體記憶體對映到視窗中來
  96.         bResult = MapUserPhysicalPages( lpMemReserved,  
  97.             NumberOfPages/5,  
  98.             aPFNs+NumberOfPages/5*i);  
  99.         if( bResult != TRUE )   
  100.         {  
  101.             printf("MapUserPhysicalPages failed (%u)/n", GetLastError() );  
  102.             return;  
  103.         }  
  104.         // 將對映到視窗中的不同記憶體塊的字串在螢幕中打印出來
  105.         strTemp=(char*)lpMemReserved;  
  106.         printf("%s/n",strTemp);  
  107.         // 解除對映
  108.         bResult = MapUserPhysicalPages( lpMemReserved,  
  109.             NumberOfPages/5,  
  110.             NULL );  
  111.         if( bResult != TRUE )   
  112.         {  
  113.             printf("MapUserPhysicalPages failed (%u)/n", GetLastError() );  
  114.             return;  
  115.         }  
  116.     }  
  117.     // 釋放實體記憶體空間
  118.     bResult = FreeUserPhysicalPages( GetCurrentProcess(),  
  119.         &NumberOfPages,  
  120.         aPFNs );  
  121.     if( bResult != TRUE )   
  122.     {  
  123.         printf("Cannot free physical pages, error %u./n", GetLastError());  
  124.         return;  
  125.     }  
  126.     // 釋放虛擬記憶體地址
  127.     bResult = VirtualFree( lpMemReserved,  
  128.         0,  
  129.         MEM_RELEASE );  
  130.     // 釋放PFN佇列空間
  131.     bResult = HeapFree(GetProcessHeap(), 0, aPFNs);  
  132.     if( bResult != TRUE )  
  133.     {  
  134.         printf("Call to HeapFree has failed (%u)/n", GetLastError() );  
  135.     }  
  136. }  
  137. /***************************************************************** 
  138. 輸入: 
  139. HANDLE hProcess: 需要獲得許可權的程序的控制代碼 
  140. BOOL bEnable: 啟用許可權 (TRUE) 或 取消許可權 (FALSE)? 
  141. 返回值: TRUE 表示許可權操作成功, FALSE 失敗. 
  142. *****************************************************************/
  143. BOOL
  144. LoggedSetLockPagesPrivilege ( HANDLE hProcess,  
  145.                              BOOL bEnable)  
  146. {  
  147.     struct {  
  148.         DWORD Count;  
  149.         LUID_AND_ATTRIBUTES Privilege [1];  
  150.     } Info;  
  151.     HANDLE Token;  
  152.     BOOL Result;  
  153.     // 開啟程序的安全資訊
  154.     Result = OpenProcessToken ( hProcess,  
  155.         TOKEN_ADJUST_PRIVILEGES,  
  156.         & Token);  
  157.     if( Result != TRUE )   
  158.     {  
  159.         printf( "Cannot open process token./n" );  
  160.         return FALSE;  
  161.     }  
  162.     // 開啟 或 取消?
  163.     Info.Count = 1;  
  164.     if( bEnable )   
  165.     {  
  166.         Info.Privilege[0].Attributes = SE_PRIVILEGE_ENABLED;  
  167.     }   
  168.     else
  169.     {  
  170.         Info.Privilege[0].Attributes = 0;  
  171.     }  
  172.     // 獲得LUID
  173.     Result = LookupPrivilegeValue ( NULL,  
  174.         SE_LOCK_MEMORY_NAME,  
  175.         &(Info.Privilege[0].Luid));  
  176.     if( Result != TRUE )   
  177.     {  
  178.         printf( "Cannot get privilege for %s./n", SE_LOCK_MEMORY_NAME );  
  179.         return FALSE;  
  180.     }  
  181.     // 修改許可權
  182.     Result = AdjustTokenPrivileges ( Token, FALSE,  
  183.         (PTOKEN_PRIVILEGES) &Info,  
  184.         0, NULL, NULL);  
  185.     // 檢查修改結果
  186.     if( Result != TRUE )   
  187.     {  
  188.         printf ("Cannot adjust token privileges (%u)/n", GetLastError() );  
  189.         return FALSE;  
  190.     }   
  191.     else
  192.     {  
  193.         if( GetLastError() != ERROR_SUCCESS )   
  194.         {  
  195.             printf ("Cannot enable the SE_LOCK_MEMORY_NAME privilege; ");  
  196.             printf ("please check the local policy./n");  
  197.             return FALSE;  
  198.         }  
  199.     }  
  200.     CloseHandle( Token );  
  201.     return TRUE;  
  202. }  

程式執行結果如下:

程式結果

    可以看出系統分頁的大小為4K,總共申請了655360個分頁,也就是2.5G。每個分頁成員佔4位元組,總共2621440位元組。2.5G記憶體分成5段512M的塊,成功寫入了字串併成功讀取。

    在除錯過程中,在執行了AllocateUserPhysicalPages函式後設置斷點,檢視工作管理員,可以看出成功分配了實體記憶體後,實際實體記憶體被佔用了2.5G,從而驗證了AWE的效果。

記憶體消耗

    通過上述示例,我們成功的在32位系統中識別出了4G的記憶體,並且在32位程式中成功使用了超過2G的記憶體。藉助PAE和AWE,即使在32位系統上,我們也能夠順利開發對記憶體消耗較大的應用程式,而不需要依賴於64位平臺。