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

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

% b6 Q' |$ f- l[boot loader]- ~: O/ f2 X- u1 u; U
timeout=30! I& j* v/ @; T8 g0 x7 V
default=multi(0)disk(0)rdisk(0)partition(1)WINDOWS" b7 E+ A+ |4 m# ~$ S+ W* y
[operating systems]
/ n0 p5 v, G0 Jmulti(0)disk(0)rdisk(0)partition(1)WINDOWS="Windows Server 2003, Enterprise" /fastdetect  /3GB8 Q3 w& G2 Y+ z+ n6 ^
   虽然使用/3GB选项能够将用户模式的地址空间扩大50%(从2GB增加到3GB),但是对于数据库系统这样的应用程序来说,这1GB的地址空间的增加只能是杯水车薪,并不能解决多少问题,而且由于操作系统内核只能使用1GB地址空间,这样可能会给操作系统的运行带来一定的负面影响,因此除非没有更好的解决方案,是不建议使用/3GB方式的。4 C. G2 c6 j0 m7 R
; U4 m, f3 n3 ^. i

# u. M: @# O. B, J+ m   鉴于像数据库系统这样的应用程序对海量内存的需求,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?如果有知道的,请告诉我一声)。) [8 h& d7 g0 z* W/ {1 U) B7 Y9 Q" D
    虽然PAE使得在应用程序中使用超过4GB的物理内存成为可能,但是由于32位应用程序的虚拟地址空间并不随着物理内存的增大而有任何变化,这意味着你不可能使用类似VirtualAlloc( GetCurrentProcess,2GB,...,...)这样的函数=调直接分配接近用户模式地址空间大小的内存区域。为了突破32位地址空间的限制,需要使用一种被成为地址窗口扩展(AWE:Address Windowing Extensions)的机制(参见上图)。
. J6 Y; i/ \3 B    AWE是Windows的内存管理功能的一组扩展,它使应用程序能够使用的内存量超过通过标准32位寻址可使用的2~3GB内存。AWE允许应用程序获取物理内存,然后将非分页内存的视图动态映射到32位地址空间。虽然32位地址空间限制为4GB,但是非分页内存却可以远远大于4GB。这使需要大量内存的应用程序(如大型数据库系统)能使用的内存量远远大于32位地址空间所支持的内存量。
6 a) ?( @  s2 P3 h. [4 U    在使用AWE机制时,需要注意以下几点:
! C  Z3 n9 R- S1 `* Q0 ^) U    (1)AWE允许在32位体系结构上分配超过4GB的物理内存,只有当系统可用物理内存大于用户模式的虚拟地址空间时,才应该使用AWE。" k, S3 ?+ {: A! T& b* r
    (2)若要使32位操作系统支持4GB以上的物理内存,必须在Boot.ini文件启用/PAE选项。
0 Y" ~) L. N, f    (3)若在Boot.ini文件中启用了/3GB选项,则操作系统最多能够使用16GB的物理内存,因此如果实际的物理内存超过16GB,必须确保不使用/3GB选项。
" T& X# Z9 \& y5 j    (4)使用AWE分配的内存是非分页的物理内存,这意味着这部分内存只能由分配的应用程序独占使用,不能由操作系统或其他程序使用,直到这些内存被释放为止,这与通常的VirtualAlloc函数分配的虚拟内存存在显著的不同,它不会参与分页替换。: S9 ~+ d4 ^3 Y' W9 b
    在Windows中,跟AWE相关的API函数有以下几个:
1 O4 W% ~) P; w/ U7 J& X  h; S3 G6 m5 C
BOOL AllocateUserPhysicalPages(
% z& i4 E( B0 c, A; X1 W3 }  HANDLE hProcess,
" A3 c  a6 V* A. k( N' X  PULONG_PTR NumberOfPages,3 D) `9 E& w: ^8 N6 {- M
  PULONG_PTR UserPfnArray
" E" d  v3 `$ R% m- W' _);' ?/ c; _- S4 n6 u2 r2 o9 _
5 [; z$ S- d. }% h" w* m
BOOL WINAPI AllocateUserPhysicalPagesNuma(' e- E7 G  I* g* @# a# \7 {5 p, _
  HANDLE hProcess,, }$ }* B/ ~6 T4 {! K( I
  PULONG_PTR NumberOfPages,( |. A; _( U/ j/ h. F9 [
  PULONG_PTR PageArray,, D2 [$ w: z8 s' ~
  DWORD nndPreferred
0 k8 x9 t& ]: B5 U  {' r);' a$ b, }( {! \) r. G' j+ u0 b
! y$ n' Q9 [+ V# c) a  }! I# a& M
BOOL MapUserPhysicalPages(
% ^' I; @' _# J+ {5 s* d* c( v  PVOID lpAddress,4 b+ `4 H( R* U7 W) j4 U! C2 d; O
  ULONG_PTR NumberOfPages,9 ~6 @" X$ n1 w6 x! }9 |
  PULONG_PTR UserPfnArray
" ]4 j9 v' r5 M: x);
  W$ S% C; B: E7 |; r$ R( P) Y( W
+ c7 E/ R% j! A8 \" g  d" T! pBOOL MapUserPhysicalPagesScatter(
- n" C9 V& P, T" g+ e1 F9 H$ X$ h* k  PVOID* VirtualAddresses,
& m: b* Z* L8 S% @0 q: \  ULONG_PTR NumberOfPages,
8 c. a$ j1 k0 `) C( j4 o; M7 v* Z  PULONG_PTR PageArray
7 b9 U- I. X+ b( E* X);
& p6 z0 S  r4 U8 j; i3 i5 _, m3 Z  {
BOOL FreeUserPhysicalPages(
, j0 E; C- ~1 z7 E' _( K  HANDLE hProcess,/ J) R2 B2 e5 Q+ [# Q
  PULONG_PTR NumberOfPages,
! S+ h$ l) R3 O  PULONG_PTR UserPfnArray
2 L. y! Z+ L( c);/ ~; u: d% g" a7 P. W: d: s

# _" u& A2 L9 o. I  x    各个函数的具体参数含义可以参考MSDN,其中AllocateUserPhysicalPagesNuma是Windows Vista和Windows 2008 Server新增的函数,用于支持NUMA(非一致性内存访问)。以下就简单说一下如何使用这几个API函数来达到使用超过4GB的内存。
5 b5 B4 H* [% k  j    使用AllocateUserPhysicalPages函数分配需要的物理内存,使用方式如下:
5 ]4 M0 r. V5 B- I
- B: X8 {% h7 y# }ULONG_PTR NumberOfPages = xxx; // 需要分配的内存页数
2 V. z- Z: Z- W; CULONG_PTR *aPFNs               = new ULONG_PTR[NumberOfPages];# g) `3 h6 V( E* {) t' D
BOOL bResult                            = AllocateUserPhysicalPages( GetCurrentProcess(),&NumberOfPages,aPFNs);
5 a& `% L: q( @4 o
& y6 z$ a" d3 {9 ^$ Y/ t, t# u// 检查分配内存是否成功. [- i: t' e  w( `" B9 f  ~
if(!bResult)1 w% Z; R# z/ e4 D; O
{% A& R8 G( j* t( P/ f4 L
   // 分配识别,错误处理
( `2 g! A- |4 t* b   // .....( y/ {7 l; n, C' S. k
}
# k4 R; d- \/ y4 W$ l+ s( o1 G, q3 Q3 s3 F0 \# V2 i3 ?. W
// 检查实际分配的内存页数- P& l9 q' [# E) q! s
if( NumberOfPages != xxx )5 Y- ~  _0 G; x
{
! N5 X- T9 S: P0 o! \" \     // ..... c. j* n1 J" u
}1 V  E2 e4 R  ]( y( Y
    需要注意的是,调用上述代码的用户必须具有“Lock Pages in Memory”(内存中锁定页面)的权限。此权限使得用户可以使用进程将数据保持在物理内存中,这样可防止系统将数据分页到磁盘上的虚拟内存中。行使此权限会因降低可用随机存取内存(RAM)的数量而显著影响系统性能。需要在本地安全策略管理程序中给用户赋予该权限,如下图所示:
8 V0 i  H1 e7 w6 K5 ]$ s    7 T; R, |! S; r( I* v+ f2 |9 i
    给用户分配了上述权限之后,需要在程序中使用代码启用该权限,如下所示:- M  }; r( A! m& b8 D9 h

5 C- c$ d, F  N: n4 w$ x// 设置锁住物理内存的权限,此代码在调用AllocateUserPhysicalPages之前执行) ?* w+ C7 u' R3 R1 C, t. X
if( !AWESetLockPagesPrivilege( GetCurrentProcess(), TRUE) )7 E9 j) q& v  G0 z  Z8 i/ v
{/ y4 [, F' }* l
    // 输出错误信息6 c% ^: E- y( K2 ]. V4 j+ }
    ..........5 d- E$ p4 u3 \' m3 _7 A" O! n
}
  I' `. k9 S4 M9 e  E* P& {; V$ ^$ J# b/ B. o- m9 Q
///  
3 Z' {& w, ?7 s( g. {///        设置或清除启用AWE( Address Windowing Extensions )所需要的锁住内存的权限。% c1 k4 A4 C# o- v
///  : J: g9 |7 ^& l& l
/// 6 P: ^6 t9 h0 J2 [4 _# `
///        进程句柄。
/ e: T) _" m9 o# j0 J; K) U///
5 n# Y# T! m4 _///
0 O4 v+ }- N: i: e///        设置或者清除标志。
1 q$ x4 R" H) V+ E7 U8 l/// 5 h$ g, R$ A1 i8 i: v% R. {- \" i
///
. V0 U3 m6 g( c6 \% {: f///        如果成功,则返回TRUE,否则返回失败。
% s# D2 y1 [2 r" Y% }* d///
1 s! }6 G) f7 JBOOL AWESetLockPagesPrivilege( HANDLE hProcess, BOOL Enable )
- @$ `1 B7 ^" p9 Q{% L4 z2 L4 F/ r5 P
    HANDLE                Token    = NULL;& [2 L, l* W- V( ?: v
    BOOL                Result    = FALSE;) U% e( G# Q5 i- m& J
    TOKEN_PRIVILEGES    Info    = { 0 };
, Z2 Y0 `3 f0 s3 i5 u6 |& @' z
; V# V5 l/ S) G3 @1 S) s7 S# N    // 打开令牌
4 n# v/ l* {* O# q5 a& h0 G    Result = OpenProcessToken ( hProcess, TOKEN_ADJUST_PRIVILEGES, &Token );  w: p  j0 r4 N% b; Q+ o4 U% P
    if( !Result )4 _  N, f3 C& O8 j2 ^. T
        return FALSE;
. W, q) D* m: `3 u. ^; i9 f4 J: V
5 c: A  n& P3 V& X6 Y    // 设置权限信息* W9 P5 ^3 ?5 \; d! Y
    Info.PrivilegeCount = 1;5 |+ C. O% \/ Z& F
    Info.Privileges[0].Attributes = Enable? SE_PRIVILEGE_ENABLED : 0;
2 H9 }. d. E4 [7 p% G$ z/ _5 \" G6 q
    // 获得锁定内存权限的ID) T3 e$ Q! N' {6 d. ~5 ]) X  B3 ^* }
    Result = LookupPrivilegeValue ( NULL,SE_LOCK_MEMORY_NAME,&(Info.Privileges[0].Luid));6 L; m4 A# `; ?8 C" j7 c
    if( !Result ) 0 o# W$ @. g  F9 e$ W! D8 y
    {3 O. \' D% t7 `1 J) N7 \9 u- d5 I
        CloseHandle( Token );, G$ `" y* s( R+ N: a
        return FALSE;1 l0 \2 e3 l6 g- @
    }
3 Q! h* J9 f# D7 u. V
- t+ {) \9 G8 v4 X, H3 S- J# z    // 调整权限
5 |- s7 F4 Q! K& k9 b/ P1 c1 u# H! M    Result = AdjustTokenPrivileges ( Token, FALSE,(PTOKEN_PRIVILEGES) &Info,0, NULL, NULL);; a* [: {7 G2 b- M/ P( x, }5 _1 ]
    if( ( !Result )  || (  GetLastError() != ERROR_SUCCESS ) )
7 x' D/ T) W. `8 y4 S: w. W) T    {" v6 ]: [: ?$ F# g
        CloseHandle( Token );
1 H3 @1 J1 Q1 \" N        return FALSE;
# M+ @  ~: ^' `5 D    }
) i# l: i6 |9 z$ t  H. C; y! @7 v# u5 O# r8 \! ]. l! f
    // 成功返回
; w7 a; i+ x: k6 e" y) O    CloseHandle( Token );
7 M/ q7 F6 U* \- ^$ Q" ^    return TRUE;
  J& _3 {( ^4 V2 W# }3 D}
: }. ]- @) I+ L* p
% e: {/ r4 G% a# G! ?9 Y' |9 a. ^1 w    使用AllocateUserPhysicalPages分配了物理内存之后,下一步就是使用MapUserPhysicalPages或MapUserPhysicalPagesScatter函数将物理内存映射进用户模式地址空间内,这两个函数用法差不多,只是第一个参数有差别。由于分配的物理内存的大小超过了用户模式地址空间的大小,因此显然不可能一次将所有的物理内存都映射到地址空间中。通常的做法是在用户模式地址空间内分配一小块连续的区域(即地址窗口),然后根据使用的需要动态将部分的物理内存映射到地址空间,这也就是“地址窗口扩展”一词的真实含义。代码示例如下:
9 c% C9 X( d7 i3 K
+ @2 A1 u% y/ u  |4 Q& H; w  @// 定义16M的地址窗口+ a2 Q0 i& T8 o
#define MEMORY_REQUESTED (16*1024*1024)$ Y( ^) B" t1 y' N) ?
% }5 E2 K  E' V0 e/ n5 q/ u# z
// 分配地址窗口$ g# ~3 z3 Z/ F, q: }) E3 N% K: V
PVOID lpMemReserved = VirtualAlloc( NULL,MEMORY_REQUESTED, MEM_RESERVE | MEM_PHYSICAL,PAGE_READWRITE );. F5 \; o; ~7 H' B
5 t6 m" Z" S, g! `5 ?( z
// 将物理内存映射到地址空间(根据需要,每次映射的页面会不同,
) b+ [* ~2 B5 P// 即下面函数的第三个参数aPFNs会指向不同的物理页)
$ B8 _# R! E6 k- J. tbResult = MapUserPhysicalPages( lpMemReserved,NumberOfPages,aPFNs);
6 J0 K! X7 ~8 e6 n% A3 p( N) T2 d; R6 S0 c# D+ A2 a+ S
// 以下就像普通的内存一样使用lpMemReserved 指针来操作物理内存了+ ]6 F$ p% Y. s0 g% {9 v
...................
+ H1 x4 N( D+ v- m$ A9 s    使用完了之后,可以使用FreeUserPhysicalPages来释放分配的物理内存,示例如下:4 ~0 g# d/ M0 ]6 O

: N/ f$ |5 \  @5 B// 取消内存映射
( q0 b8 `$ f& d1 t! `bResult = MapUserPhysicalPages( lpMemReserved,NumberOfPages,NULL );
3 h% E) |0 q& y! ]( r- `- e  r. A# ]  i! j; d+ f
// 释放物理内存
, n2 c' y, T6 r( m% j( `, dbResult = FreeUserPhysicalPages( GetCurrentProcess(),&NumberOfPages,aPFNs );% F) Y2 F+ a4 X% R4 v

5 J- @9 R* x" }' ^// 释放地址窗口
3 \3 O8 y4 s) v. A( |% F" j+ @bResult = VirtualFree( lpMemReserved,0,MEM_RELEASE );
. A. i# u4 |6 Q2 w
1 l- o7 s# H) ?0 m' y- d( S* M// 释放物理页号数组% K' b" g8 y$ {1 ]# ?, ?  i9 `( B
delete[] aPFNs;9 t& x0 \) h# |4 w: q9 c
    AWE机制被使用最多的一个场合是数据库系统的缓存管理器(BufferManager),例如SQL Server的内存管理器。虽然以上代码都是基于Windows操作系统,但是PAE和AWE机制并不是Windows特有的,32位Linux也有类似的API。完整使用AWE机制的例子,大家可以参考MySQL的源码。* f9 l2 U' G( D9 q; u( I! j
    最后想说的是,对于开发人员来说,一个好消息是64位CPU和操作系统正越来越普及。在64位环境下,一个进程的用户模式的地址空间可达8TB(也就是说目前很多的64位系统只使用了40几位的内存地址,远没有充分使用64位的内存地址),在可以预见的未来很长一段时间,估计我们都不会再为地址空间不足而发愁了,让我们一起为64位时代的到来而欢呼吧!

 

posted @ 2009-12-02 18:38  wenanry  阅读(1305)  评论(0编辑  收藏  举报