让32位应用程序不再为2G内存限制苦恼

时间:2023-03-10 04:21:59
让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,系统只能认出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位平台。