Wi n d o w s提供了3种进行内存管理的方法,它们是:
• 虚拟内存,最适合用来管理大型对象或结构数组。
• 内存映射文件,最适合用来管理大型数据流(通常来自文件)以及在单个计算机上运行的多个进程之间共享数据。
• 内存堆栈,最适合用来管理大量的小对象。
虚拟内存的状态
Wi n d o w s函数G l o b a l M e m o r y S t a t u s可用于检索关于当前内存状态的动态信息:
VOID GlobalMemoryStatus(LPMEMORYSTATUS pmst);
如果希望应用程序在内存大于4 G B的计算机上运行,或者合计交换文件的大小大于4 G B,那么可以使用新的G l o b a l M e m o r y S t a t u s E x函数:
BOOL GlobalMemoryStatusEx(LPMEMORYSTATUSEX pmst);
确定地址空间的状态
Wi n d o w s提供了一个函数,可以用来查询地址空间中内存地址的某些信息(如大小,存储器类型和保护属性等)。
这个函数称为Vi r t u a l Q u e r y:
DWORD VirtualQuery(
LPCVOID pvAddress,
PMEMORY_BASIC_INFORMATION pmbi,
DWORD dwLength);
Wi n d o w s还提供了另一个函数,它使一个进程能够查询另一个进程的内存信息:
DWORD VirtualQueryEx(
HANDLE hProcess,
LPCVOID pvAddress,
PMEMORY_BASIC_INFORMATION pmbi,
DWORD dwLength);
这两个函数基本相同,差别在于使用Vi r t u a l Q u e r y E x时,可以传递你想要查询的地址空间信息的进程的句柄。调试程序和其他实用程序使用这个函数最多,几乎所有的应用程序都只需要调用Vi r t u a l Q u e r y函数。
为了获得完整的内存信息,我创建了一个函数,即V M Q u e r y:
BOOL VMQuery(
HANDLE hProcess,
PVOID pvAddress,
PVMQUERY pVMQ);
系统信息
许多操作系统的值是根据主机而定的,比如页面的大小,分配粒度的大小等。这些值决不应该用硬编码的形式放入你的源代码。相反,你始终都应该在进程初始化的时候检索这些值,并在你的源代码中使用检索到的值。G e t S y s t e m I n f o函数将用于检索与主机相关的值:
VOID GetSystemInfo(LPSYSTEM_INFO psinf);
必须传递S Y S T E M _ I N F O结构的地址给这个函数。这个函数将初始化所有的结构成员然后返回。下面是S Y S T E M _ I N F O数据结构的样子。
typedef struct _SYSTEM_INFO
{
union
{
DWORD dwOemId; // Obsolete, do not use
struct
{
WORD wProcessorArchitecture;
WORD wReserved;
} ;
} ;
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO, * LPSYSTEM_INFO;
当系统引导时,它要确定这些成员的值是什么。对于任何既定的系统来说,这些值总是相同的,因此决不需要为任何既定的进程多次调用该函数。
表14-1 与内存有关的成员函数
成员名 |
描述 |
d w P a g e S i z e |
用于显示C P U的页面大小。在x86 CPU上,这个值是4 0 9 6字节。在Alpha CPU 上,这个值是8 1 9 2字节。在I A - 6 4上,这个值是8 1 9 2字节 |
l p M i n i m u m A p p l i c a t i o n A d d r e s s |
用于给出每个进程的可用地址空间的最小内存地址。在Windows 98上,这个值是4 194 304,或0 x 0 0 4 0 0 0 0 0,因为每个进程的地址空间中下面的4 M B是不能使用的。在Windows 2000上,这个值是65 536或0 x 0 0 0 1 0 0 0 0,因为每个进程的地址空间中开头的6 4 K B总是空闲的 |
l p M a x i m u m A p p l i c a t i o n A d d r e s s |
用于给出每个进程的可用地址空间的最大内存地址。在Windows 98 上,这个地址是2 147 483 647或0 x 7 F F F F F F F,因为共享内存映射文件区域和共享操作系统代码包含在上面的2 GB分区中。在Windows 2000上,这个地址是内核方式内存开始的地址,它不足6 4 K B |
d w A l l o c a t i o n G r a n u l a r i t y |
显示保留的地址空间区域的分配粒度。截止到撰写本书时,在所有Wi n d o w s平台上,这个值都是65536 |
该结构的其他成员与内存管理毫无关系,为了完整起见,下面也对它们进行了介绍(见表1 4 - 2)。
表14-2 与内存无关的成员函数
成员名 |
描述 |
d w O e m I d |
已作废,不引用 |
W R e d e r v e d |
保留供将来使用,不引用 |
d w N u m b e r O f P r o c e s s o r s |
用于指明计算机中的C P U数目 |
d w A c t i v e P r o c e s s o r M a s k |
一个位屏蔽,用于指明哪个C P U是活动的(允许运行线程) |
d w P r o c e s s o r Ty p e |
只用于Windows 98,不用于Windows 2000,用于指明处理器的类型,如Intel 386、4 8 6或P e n t i u m |
w P r o c e s s o r A r c h i t e c t u r e |
只用于Windows 2000,不用于Windows 98,用于指明处理的结构,如I n t e l、A l p h a、Intel 64位或Alpha 64位 |
w P r o c e s s o r L e v e l |
只用于Windows 2000,不用于Windows 98,用于进一步细分处理器的结构,如用于设定Intel Pentium Pro或Pentium II |
w P r o c e s s o r R e v i s i o n |
只用于Windows 2000 ,不用于Windows 98,用于进一步细分处理器的级别 |
在地址空间中保留一个区域
通过调用Vi r t u a l A l l o c函数,可以在进程的地址空间中保留一个区域:
PVOID VirtualAlloc(
PVOID pvAddress,
SIZE_T dwSize,
DWORD fdwAllocationType,
DWORD fdwProtect);
第一个参数p v A d d r e s s包含一个内存地址,用于设定想让系统将地址空间保留在什么地方。在大多数情况下,你为该参数传递M U L L。它告诉Vi r t u a l A l l o c,保存着一个空闲地址区域的记录的系统应该将区域保留在它认为合适的任何地方。系统可以从进程的地址空间的任何位置来保留一个区域,因为不能保证系统可以从地址空间的底部向上或者从上面向底部来分配各个区域。可以使用M E M _ TO P _ D O W N标志来说明该分配方式。如果Vi r t u a l A l l o c函数能够满足你的要求,那么它就返回一个值,指明保留区域的基地址。如果传递一个特定的地址作为Vi r t u a l A l l o c的p v A d d r e s s 参数,那么该返回值与传递给Vi r t u a l A l l o c的值相同,并被圆整为(如果需要的话) 6 4 K B边界值。
Vi r t u a l A l l o c函数的第二个参数是d w S i z e,用于设定想保留的区域的大小(以字节为计量单位)。由于系统保留的区域始终必须是C P U页面大小的倍数,因此,如果试图保留一个跨越6 2 K B的区域,结果就会在使用4 KB、8 KB或16 KB页面的计算机上产生一个跨越6 4 K B的区域。
Vi r t u a l A l l o c函数的第三个参数是f d w A l l o c a t i o n Ty p e,它能够告诉系统你想保留一个区域还是提交物理存储器(这样的区分是必要的,因为Vi r t u a l A l l o c函数也可以用来提交物理存储器)。若要保留一个地址空间区域,必须传递M E M _ R E S E RV E标识符作为F d w A l l o c a t i o n Ty p e参数的值。
最后一个参数是f d w P r o t e c t,用于指明应该赋予该地址空间区域的保护属性。与该区域相关联的保护属性对映射到该区域的已提交内存没有影响。无论赋予区域的保护属性是什么,如果没有提交任何物理存储器,那么访问该范围中的内存地址的任何企图都将导致该线程引发一个访问违规。
在保留区域中的提交存储器
当保留一个区域后,必须将物理存储器提交给该区域,然后才能访问该区域中包含的内存地址。系统从它的页文件中将已提交的物理存储器分配给一个区域。物理存储器总是按页面边界和页面大小的块来提交的。
若要提交物理存储器,必须再次调用Vi r t u a l A l l o c函数。不过这次为f d w A l l o c a t i o n Ty p e参数传递的是M E M _ C O M M I T标志,而不是M E M _ R E S E RV E标志。传递的页面保护属性通常与调用Vi r t u a l A l l o c来保留区域时使用的保护属性相同(大多数情况下是PA G E _ R E A D W R I T E),不过也可以设定一个不同的保护属性。
在已保留的区域中,你必须告诉Vi r t u a l A l l o c函数,你想将物理存储器提交到何处,以及要提交多少物理存储器。为了做到这一点,可以在p v A d d r e s s参数中设定你需要的内存地址,并在d w S i z e参数中设定物理存储器的数量(以字节为计量单位)。注意,不必立即将物理存储器提交给整个区域。
同时进行区域的保留和内存的提交
有时你可能想要在保留区域的同时,将物理存储器提交给它。只需要一次调用Vi r t u a l A l l o c函数就能进行这样的操作,如下所示:
PVOID pvMem = VirtualAlloc(NULL, 99 * 1024 , MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
这个函数调用请求保留一个99 KB的区域,并且将99 KB的物理存储器提交给它。当系统处理这个函数调用时,它首先要搜索你的进程的地址空间,找出未保留的地址空间中一个地址连续的区域,它必须足够大,能够存放100 KB(在4 KB页面的计算机上)或104 KB(在8 KB页面的计算机上)。
系统之所以要搜索地址空间,原因是已将p v A d d r e s s参数设定为N U L L。如果为p v A d d r e s s设定了内存地址,系统就要查看在该内存地址上是否存在足够大的未保留地址空间。如果系统找不到足够大的未保留地址空间,Vi r t u a l A l l o c将返回N U L L,
如果能够保留一个合适的区域,系统就将物理存储器提交给整个区域。无论是该区域还是提交的内存,都将被赋予PA G E _ R E A D W R I T E保护属性。
最后需要说明的是,Vi r t u a l A l l o c将返回保留区域和提交区域的虚拟地址,然后该虚拟地址被保存在p v M e m变量中。如果系统无法找到足够大的地址空间,或者不能提交该物理存储器,Vi r t u a l A l l o c将返回N U L L。
回收虚拟内存和释放地址空间区域
若要回收映射到一个区域的物理存储器,或者释放这个地址空间区域,可调用Vi r t u a l F r e e函数:
BOOL VirtualFree(
LPVOID pvAddress,
SIZE_T dwSize,
DWORD fdwFreeType);
改变保护属性
虽然实践中很少这样做,但是可以改变已经提交的物理存储器的一个或多个页面的保护属性。
若要改变内存页面的保护属性,可以调用Vi r t u a l P r o t e c t函数:
BOOL VirtualProtect(
PVOID pvAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD pflOldProtect);
当然,保护属性是与内存的整个页面相关联的,而不是赋予内存的各个字节的。因此,如果要使用下面的代码来调用4 KB 页面的计算机上的Vi r t u a l P r o t e c t函数,其结果是把PA G E _ N O A C C E S S保护属性赋予内存的两个页面:
VirtualProtect(pvRgnBase + (3 * 1024), 2 * 1024,
PAGE_NOACCESS, &flOldProtect);
清除物理存储器的内容
为了说明内存的内容已经被清除,我们必须对系统的R A M提出大量的使用需求。若要进行这项操作,可以分3步来进行:
1) 调用G l o b a l M e m o r y S t a t u s函数,获取计算机中R A M的总容量。
2) 调用Vi r t u a l A l l o c函数,提交该数量的内存。这项操作的运行速度非常快,因为在进程试图访问页面之前,系统实际上并不为该内存分配R A M。
3) 调用Z e r o M e m o r y函数,使新提交的页面可以被访问。这将给系统的R A M带来沉重的负担,导致当前正在R A M中的某些页面被写入页文件。
如果用户指明该数据将在以后被访问,那么该数据将不被清除,并且在以后访问该数据时将数据转入R A M。但是,如果用户指明以后将不再访问该数据,那么数据将被清除,并且系统不把数据写入页文件,这样就可以提高应用程序的运行性能。