如何在32位程序中突破地址空间限制使用超过4G的内存

众所周知,所有的32位应用程序都有4GB的进程地址空间,因为32位地址最多可以映射4GB的内存(对于虚拟地址空间概念不太熟悉的朋友建议去看一下《Windows核心编程》这本书)。对于Microsoft Windows操作系统,应用程序可以访问2GB的进程地址空间(32位Linux可以访问3GB地址空间),这就是称为用户模式的虚拟地址空间。这2GB的用户模式虚拟地址空间位于4GB地址空间的低一半,而与之相对应的高一半2GB地址空间由操作系统内核使用,因此被成为内核模式的虚拟地址空间。在一个进程中,所有的线程读共享相同的2GB用户模式虚拟地址空间。3 u/ C, p7 ]% w! [& t" D, b* O
    对于一般的应用程序来说,2GB的地址空间是足够使用的了,但是对于一些特殊的需要使用海量内存的应用程序(典型的例子是数据库系统)来说,2GB的地址空间就远远不够了。为了缓解地址空间的不足,微软提供了一个权宜的解决方案,所有从Windows 2000 Server开始的操作系统版本都提供了一个boot.ini启动开关(/3GB),可以为应用程序提供访问3GB的进程地址空间的能力,从而将内核模式的地址空间限定为1GB。以下就是一个开启了3GB选项的boot.ini文件示例:) e* M- e( W; v, t8 K$ Z4 L1 s6 e

[boot loader]

timeout=30; N/ b  ]0 s6 j/ U$ e" J9 m+ a7 b4
default=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方式的。% l/ O2 {& s6 j1 C
" Z; l$ Z/ ]  C

鉴于像数据库系统这样的应用程序对海量内存的需求,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?如果有知道的,请告诉我一声)。( W% X3 E1 }: ?$ G+ C- f
    虽然PAE使得在应用程序中使用超过4GB的物理内存成为可能,但是由于32位应用程序的虚拟地址空间并不随着物理内存的增大而有任何变化,这意味着你不可能使用类似VirtualAlloc( GetCurrentProcess,2GB,...,...)这样的函数=调直接分配接近用户模式地址空间大小的内存区域。为了突破32位地址空间的限制,需要使用一种被成为地址窗口扩展(AWE:Address Windowing Extensions)的机制(参见上图)。& p2 {" y$ c. N. R' G4 S9 P* a
    AWE是Windows的内存管理功能的一组扩展,它使应用程序能够使用的内存量超过通过标准32位寻址可使用的2~3GB内存。AWE允许应用程序获取物理内存,然后将非分页内存的视图动态映射到32位地址空间。虽然32位地址空间限制为4GB,但是非分页内存却可以远远大于4GB。这使需要大量内存的应用程序(如大型数据库系统)能使用的内存量远远大于32位地址空间所支持的内存量。% Z% o. B1 K3 j
    在使用AWE机制时,需要注意以下几点:: f9 N5 r! P8 m* }5 l
    (1)AWE允许在32位体系结构上分配超过4GB的物理内存,只有当系统可用物理内存大于用户模式的虚拟地址空间时,才应该使用AWE。9 }/ f7 u9 `, m- P
    (2)若要使32位操作系统支持4GB以上的物理内存,必须在Boot.ini文件启用/PAE选项。
    (3)若在Boot.ini文件中启用了/3GB选项,则操作系统最多能够使用16GB的物理内存,因此如果实际的物理内存超过16GB,必须确保不使用/3GB选项。
    (4)使用AWE分配的内存是非分页的物理内存,这意味着这部分内存只能由分配的应用程序独占使用,不能由操作系统或其他程序使用,直到这些内存被释放为止,这与通常的VirtualAlloc函数分配的虚拟内存存在显著的不同,它不会参与分页替换。3 l9 P* y5 S: I

 

在Windows中,跟AWE相关的API函数有以下几个:# m) M# N. d. N( {5 W5 C9 ?5 M% ?2 C
/ a( `$ |$ u5 C  v7 r& h5 b

 

[cpp]  view plain  copy
  1. BOOL AllocateUserPhysicalPages(  
  2.   HANDLE hProcess,  
  3.   PULONG_PTR NumberOfPages,  
  4. PULONG_PTR UserPfnArray);  
  5.   
  6.   
  7. BOOL WINAPI AllocateUserPhysicalPagesNuma(  
  8.   HANDLE hProcess,  
  9.   PULONG_PTR NumberOfPages,  
  10.   PULONG_PTR PageArray,  
  11.   DWORD nndPreferred);  
  12.   
  13.   
  14. BOOL MapUserPhysicalPages(  
  15.   PVOID lpAddress,  
  16.   ULONG_PTR NumberOfPages,  
  17.   PULONG_PTR UserPfnArray);  
  18.   
  19. BOOL MapUserPhysicalPagesScatter(  
  20.   PVOID* VirtualAddresses,  
  21.   ULONG_PTR NumberOfPages,  
  22.   PULONG_PTR PageArray  );  
  23.   
  24.   
  25. BOOL FreeUserPhysicalPages(  
  26.   HANDLE hProcess,  
  27.   PULONG_PTR NumberOfPages,  
  28.   PULONG_PTR UserPfnArray);  


各个函数的具体参数含义可以参考MSDN,其中AllocateUserPhysicalPagesNuma是Windows Vista和Windows 2008 Server新增的函数,用于支持NUMA(非一致性内存访问)。以下就简单说一下如何使用这几个API函数来达到使用超过4GB的内存。8 n/ E  r8 H! K+ }0 @5 R
    使用AllocateUserPhysicalPages函数分配需要的物理内存,使用方式如下:
ULONG_PTR NumberOfPages = xxx; // 需要分配的内存页数
ULONG_PTR *aPFNs               = new ULONG_PTR[NumberOfPages];
BOOL bResult                            = AllocateUserPhysicalPages( GetCurrentProcess(),&NumberOfPages,aPFNs);& \0 h$ H6 f3 R. Q* t

 检查分配内存是否成功
if(!bResult)- |: j& k. G% x& R( }
{$ D6 k. a5 I; s. s4 g) `
   // 分配识别,错误处理
   // .....8 ~5 B" {0 d' ~/ \1 F
}6 T$ X1 u% E5 P

检查实际分配的内存页数$ \  U: S% w$ u' L- Z9 J
if( NumberOfPages != xxx ): H4 z# e( R" N1 Z/ a4 N. |
{
   // ....8 P' a- H( n  o6 L* c
}
需要注意的是,调用上述代码的用户必须具有“Lock Pages in Memory”(内存中锁定页面)的权限。此权限使得用户可以使用进程将数据保持在物理内存中,这样可防止系统将数据分页到磁盘上的虚拟内存中。行使此权限会因降低可用随机存取内存(RAM)的数量而显著影响系统性能。需要在本地安全策略管理程序中给用户赋予该权限,如下图所示:
.   0 E! @1 Y2 W; A/ X( u6 m, P+ v
    给用户分配了上述权限之后,需要在程序中使用代码启用该权限,如下所示:4 |( \7 k( P/ V" p1 ^; `8 `4 z
7 @4 [1 g( X+ h8 x8 O) e+ M
// 设置锁住物理内存的权限,此代码在调用AllocateUserPhysicalPages之前执行2 H% e9 U- p: ]; k9 P5 i
if( !AWESetLockPagesPrivilege( GetCurrentProcess(), TRUE) ). }7 ~: \4 s( ?' ^
{4 J: B0 W  @7 H& P3 {! S; X! m: B
    // 输出错误信息

}
E" l/ A1 J
    ///        设置或清除启用AWE( Address Windowing Extensions )所需要的锁住内存的权限。
: E! |9 U. B
    ///        进程句柄。  y: S& R& q( q" w
    /// 7 D% m6 c$ K  H6 x! g8 x
    /// 
    ///        设置或者清除标志。
    /// ( s* [- [! P5 ^8 G2 `0 D% u
    /// 
    ///        如果成功,则返回TRUE,否则返回失败。/ P) G) ^: `0 E0 i) R# s1 _+ O
    /// 5 r5 W: D8 E- g


BOOL AWESetLockPagesPrivilege( HANDLE hProcess, BOOL Enable )
{9 K, R' J( M+ \0 a
    HANDLE                Token    = NULL;# M* l, K0 g: X1 w2 E+ N+ j' _' e" m
    BOOL                Result    = FALSE;8 {& H1 n* O( H# V" U. d
    TOKEN_PRIVILEGES    Info    = { 0 };' v7 W7 ?8 g) `( Q" ?! o1 }
6 r4 S5 D3 ^1 A( E
    // 打开令牌( h% ~% I7 W. G  h0 I
    Result = OpenProcessToken ( hProcess, TOKEN_ADJUST_PRIVILEGES, &Token );
    if( !Result )  H& G2 B* y, N8 b9 P5 n$ D. A
        return FALSE;

    // 设置权限信息
    Info.PrivilegeCount = 1;( B. M  N( Q6 _, c9 X; Z
    Info.Privileges[0].Attributes = Enable? SE_PRIVILEGE_ENABLED : 0;1 ?' B- d: ~# M

    // 获得锁定内存权限的ID$ w5 F; n- B) n/ c1 W' n  _9 r
    Result = LookupPrivilegeValue ( NULL,SE_LOCK_MEMORY_NAME,&(Info.Privileges[0].Luid));
    if( !Result ) 
    {
       CloseHandle( Token );/ C5 ?; {4 F' p
        return FALSE;5 j8 ^% \% s& S( _, o8 C5 C
    }2 C& E. I5 _2 T/ L/ [


1 m# g' f- P$ h" C- d- X# c
    // 调整权限* A# o2 N* Q) K6 N1 ^) M9 e
    Result = AdjustTokenPrivileges ( Token, FALSE,(PTOKEN_PRIVILEGES) &Info,0, NULL, NULL);" ^! C: Y) l) T9 `& m2 o, W. U5 J
    if( ( !Result )  || (  GetLastError() != ERROR_SUCCESS ) ); }# G* G. J. g5 V5 [+ D' Q8 }
    {3 d8 F" p6 l+ Q: L
        CloseHandle( Token );
        return FALSE;. x3 m( ]/ h# A% t  T
    }


    // 成功返回
   CloseHandle( Token );. R0 O( Y0 @, w3 y
    return TRUE;& M+ K. J) Y4 X6 T% ]
}

使用AllocateUserPhysicalPages分配了物理内存之后,下一步就是使用MapUserPhysicalPages或MapUserPhysicalPagesScatter函数将物理内存映射进用户模式地址空间内,这两个函数用法差不多,只是第一个参数有差别。由于分配的物理内存的大小超过了用户模式地址空间的大小,因此显然不可能一次将所有的物理内存都映射到地址空间中。通常的做法是在用户模式地址空间内分配一小块连续的区域(即地址窗口),然后根据使用的需要动态将部分的物理内存映射到地址空间,这也就是“地址窗口扩展”一词的真实含义。代码示例如下:1 [) \. {  r7 B8 f3 o7 t
( [1 L& e+ e( e$ S1 I
// 定义16M的地址窗口: ]: A. a9 i, Y2 v" ]( e: j
#define MEMORY_REQUESTED (16*1024*1024)/ P, [- D! ^: Z* G$ H
, e$ U5 L/ e$ |
// 分配地址窗口# o$ n) o# S5 @4 S* z" p9 [
PVOID lpMemReserved = VirtualAlloc( NULL,MEMORY_REQUESTED, MEM_RESERVE | MEM_PHYSICAL,PAGE_READWRITE );9 l/ a) [- B9 H( ]  v7 h3 R. j

// 将物理内存映射到地址空间(根据需要,每次映射的页面会不同,
// 即下面函数的第三个参数aPFNs会指向不同的物理页)
= MapUserPhysicalPages( lpMemReserved,NumberOfPages,aPFNs);
// 以下就像普通的内存一样使用lpMemReserved 指针来操作物理内存了
使用完了之后,可以使用FreeUserPhysicalPages来释放分配的物理内存,示例如下:( V, Q4 C9 s8 L, o8 E2 U
4 y" p3 @: C; Z4 o* ?

// 取消内存映射 6 O0 u" U& ]- G, p
bResult = MapUserPhysicalPages( lpMemReserved,NumberOfPages,NULL );; B* a  q" H1 ^/ j" ]; ^0 J
  j) M! w4 m7 F, r

// 释放物理内存: t$ A; V0 p9 b3 A: D! ]3 t
bResult = FreeUserPhysicalPages( GetCurrentProcess(),&NumberOfPages,aPFNs );

// 释放地址窗口
bResult = VirtualFree( lpMemReserved,0,MEM_RELEASE );

// 释放物理页号数组/ w+ M& y5 M2 t, ]
delete[] aPFNs;
AWE机制被使用最多的一个场合是数据库系统的缓存管理器(BufferManager),例如SQL Server的内存管理器。虽然以上代码都是基于Windows操作系统,但是PAE和AWE机制并不是Windows特有的,32位linux也有类似的API。完整使用AWE机制的例子,大家可以参考MySQL的源码。1 w  K* v- K% E6 w
    最后想说的是,对于开发人员来说,一个好消息是64位CPU和操作系统正越来越普及。在64位环境下,一个进程的用户模式的地址空间可达8TB(也就是说目前很多的64位系统只使用了40几位的内存地址,远没有充分使用64位的内存地址),在可以预见的未来很长一段时间,估计我们都不会再为地址空间不足而发愁了,让我们一起为64位时代的到来而欢呼吧!

参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值