- 虚拟内存
:最适合用来管理大型对象数组或大型结构数组。 - 内存映射文件
:最适合用来管理大型数据流(通常是文件),以及在同一机器上运行的多个进程之间共享数据。 - 堆
:最适合用来管理大量的小型对象。
以下将讨论第一种方式,即虚拟内存。
1.预定地址空间区域
我们可以调用VirtualAlloc函数来预订进程中的地址空间区域:
PVOID VirtualAlloc(
PVOID pvAddress,
SIZE_T dwSize,
DWORD fdwAllocationType,
DWORD fdwProtect);
第一个参数
pvAddress
,
是内存地址,用来告诉系统我们想要预定地址空间中的哪一块。由于系统会记录所有闲置的地址区间,因此大多数时候我们只要给该参数传NULL就可以了。这等于是告诉系统自动找一块闲置区域。记住:这里的内存地址指的是进程的地址空间中的用户模式地址空间。
第二个参数
dwSize
用来指定我们想要预定的区域大小,以字节为单位。系统始终都根据CPU页面大小的整数倍来预订区域,如我们预定5KB的空间,那么最终得到的会是8KB(两个页面2*4KB)。
第三个参数
fdwAllocationType
,用来告诉系统是要预定区域还是要调拨物理存储器(因为调拨物理存储器也是用的VirtualAlloc函数,MEM_RESERVE:预定地址空间区域,MEM_COMMIT:调拨物理存储器,MEM_TOP_DOWN:指定从尽可能高的内存地址来预订区域)。
第四个参数
fdwProtect
是给区域指定的保护属性。区域的保护属性对调拨给该区域的物理存储器不起任何作用。无论为区域指定什么保护属性,只要还没有给它调拨物理存储器,试图访问区域内的任何内存地址都会引发访问违规。
当区域的保护属性和将要调拨的物理存储器的保护属性相一致时,系统内部的处理效率会更高。
保护属性
|
描述
|
---|---|
PAGE_NOACCESS |
试图读取页面、写入页面或执行页面中的代码将引发访问违规。 |
PAGE_READONLY |
试图写入页面或执行页面中的代码将引发访问违规。 |
PAGE_READWRITE |
试图执行页面中的代码将引发访问违规。 |
PAGE_EXECUTE |
试图读取页面或写入页面将引发访问违规。 |
PAGE_EXECUTE_READ |
试图写入页面将引发访问违规。 |
PAGE_EXECUTE_READWRITE |
对页面执行任何操作都不会引发访问违规 |
PAGE_WRITECOPY |
试图执行页面中的代码将引发访问违规。试图写入页面将使系统为进程 单独创建一份该页面的私有副本(以页交换文件为后备存储器) |
PAGE_EXECUTE_WRITECOPY |
对页面执行任何操作都不会引发访问违规。试图写入页面将使系统为 进程单独创建一份该页面的私有副本( |
2.给区域调拨物理存储器
在预定了区域之后,我们还需要给区域调拨物理存储器,这样才能访问其中的内存地址。系统会从页交换文件中来调拨物理存储器给区域。在调拨物理存储器时,起始地址始终都是页面大小的整数倍,整个大小也是页面大小的整数倍。
调拨物理存储器,我们同样使用VirtualAlloc。但这次我们给参数
fdwAllocationType
传MEM_COMMIT。在给物理存储器指定保护属性的时候,通常我们使用的保护属性会和预定区域时相同(大多数情况下是PAGE_READWRITE)。
在已预订的区域中,我们必须告诉VirtualAlloc要调拨多少物理存储器给哪里。这是通过
pvAddress
和
dwSize
来指定的。前者表示想要调拨物理存储器给哪个内存地址,后者表示物理存储器的数量,以字节为单位。值得注意的是,我们无须一下子给整个区域调拨物理存储器。
由于系统是基于整个页面来指定保护属性的,因此不可能出现同物理存储页面有不同保护属性的情况。但是,同一区域的不同页面可以有不同的保护属性。
3.同时预定和调拨物理存储器
有时我们想同时预定区域并给区域调拨物理存储器。只需调用一次VirtualAlloc一次就能达到这一目的,如下所示:
PVOID pvMem = VirtualAlloc(NULL, 99 * 1024,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
函数将分配一个100KB(25*4KB)的区域并为其调拨物理存储器。
Windows还提供大页面支持,可以在处理大块内存的时候提升性能。这种情况下,系统分配内存时,不再使用GetSystemInfo函数返回的SYSTEM_INFO结构中的dwPageSize字段来作为分配粒度,而是使用下面的函数返回的大页面分配粒度:
SIZE_T GetLargePageMinimum();
注意:如果CPU不支持大页面分配,那么GetLargePageMinimum会返回0.
- 要分配的内存块的大小(即dwSize参数的值),必须是
GetLargePageMinimum函数返回值的整数倍。 - 在调用VirtualAlloc时,必须把MEM_RESERVE | MEM_COMMIT与MEM_LARGE_PAGE参数按位或起来。换句话说,我们必须同时预定和
调拨内存,我们不能先预定一块区域然后再给其中的一部分调拨物理存储器。 - 在用VirtualAlloc分配内存时必须传PAGE_READWRITE保护属性给fdwProtect参数。
Windows认为用MEM_LARGE_PAGE标志分配到的内存是不可换页的(unpagable):也就是说必须驻留在内存中。这也是为什么这种方式得到的内存能提供更好的性能。
4.何时调拨物理存储器
有以下四种方法可以用来确定是否需要给区域中的某一部分调拨物理存储器:
- 总
是尝试调拨物理存储器。这种方法每次调用VirtualAlloc函数,尝试去调拨物理存储器。系统会先检查是否已经调拨了物理存储器,如果是,它将不会
再次调拨额外的物理存储器。它的缺点是,每次都要调用多一次函数调用,而不管是否已经调拨了物理存储器,这就降低了性能。 - 使用VirtualQuery函数来判断是否调拨了物理存储器。这方法更糟,额外的调用了VirtualQuery。
- 记录哪些页面已经调拨而哪些页面尚未调拨。这可能非常简单,也可能机器复杂。
- 使用结构化异常处理(structured exception handling,SEH)——最佳方案。SEH是操作系统的一项特性,它可以让系统在发生某种情况时通知我们的应用程序。
5.撤销调拨物理存储器及释放区域
要撤销调拨给区域的物理存储器,或是释放地址空间中的一整块区域,可以调用VirtualFree函数:
BOOL VirtualFree(
LPVOID pvAddress,
SIZE_T dwSize,
DWORD fdwFreeType);
用VirtualFree来释放已预订区域,那么只需调用VirtualFree一次,就能够释放整个区域以及调拨给该区域的物理存储器。在调用时,pvAddress
参数必须是区域的基地址。该地址就是预订区域时VirtualAlloc返回的地址。给定一个内存地址,系统可以知道位于该地址的区域的大小,由于这个原因我们可以传0给dwSize
参数。实际上,我们必须传0给dwSize
,否则VirtualFree会失败。我们必须传MEM_RELEASE给第三个参数fdwFreeType
,来告诉系统撤销调拨给该区域的所有物理存储器,并释放区域。当释放区域时,我们必须释放为该区域预订的所有地址空间。
如果向撤销调拨给区域的一部分物理存储器,但又不想释放整个区域,那么我们也还是调用VirtualFree函数。我们必须传一个内存地址给pvAddress
参数,用来告诉系统要撤销调拨的第一个页面的地址。我们在dwSize
参数中指定想要释放的物理存储器的大小,并传MEM_DECOMMIT给fdwFreeType
参数。
如果dwSize
为0,而
pvAddress
又是区域的基地址,那么VirturalFree会撤销调拨给该区域的所有页面。
何时撤销调拨物理存储器
在实践中,想知道何时能够安全地撤销调拨物理存储器是需要一些技巧的。
- 最简单的方法是将结构设计成正好等于一个页面的大小。
- 另一种更为实用的解决方案是记录哪些结构正在使用。
- 最后一种解决方案是实现一个垃圾收集函数。
6.改变保护属性
虽然在实际中很少需要改变已调拨的物理存储页的保护属性,但这样做仍然是可行的。
我们可以调用VirtualProtect函数来改变一个内存页面的保护属性:
BOOL VirtualProtect(
PVOID pvAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD pflOldProtect);
保护属性是与整个物理存储页相关联的,我们不能给一个字节指定保护属性。
7.重置物理存储器的内容
为了满足最近的载入请求,系统会在内存中查找可用的页面,如果找到的页面已经被修改过,那么系统还必须将它们患处到页交换文件中。
Windows提供了一项特性,使得应用程序能够提高自身的性能——这项特性就是重置物理存储器
。重置物理存储器的意思就是,我们告诉系统一个或几个物理存储页中的数据没有被修改过。如果系统正在查找一页闲置内存并且找到了一个修改过的页面,那么系统必须把该内存页写入到页交换文件中。这个操作比较慢,会影响性能。
对大多数应用程序来说,我们都希望系统把修改后的页面保存到页交换文件中。
但
是,有些应用程序只需要在一小段时间内使用存储器,之后也不需要保留存储器中的内容。为了提高性能,应用程序可以告诉系统不要在页交换文件中保存指定的存
储页。因此,如果系统决定将一个内存页挪作它用,那么它不会将页面的内容保存到页交换文件中,这样提高了性能。为了重置存储器,应用程序应该调用
VirtualAlloc函数,并在第三个参数中传MEM_RESET标志。
8.地址窗口扩展
随着时间的推移,应用程序需要越来越多的内存。为了提高性能,服务器应用程序需要在内存中保持更多的数据以减少磁盘和内存间的页交换。
为了帮助这些应用程序,Windows提供了一项特性,即地址窗口扩展(Address Windowsing Extention,AWE)。创建AWE时,Microsoft有以下两个目标:
- 允许应用程序以一种特殊的方式分配内存,操作系统保证不会将以这种方式分配的内存换出3到磁盘上。
- 允许应用程序访问比进程地址空间还要多的内存。
程
序通过调用VirtualAlloc函数来预订地址窗口。MEM_PHYSICAL标志表示该区域最终会以物理内存作为后备。AWE的一个限制是所有映射
到地址窗口的存储器必须是可读/写的。因此,PAGE_READWRITE是我们能传给VirtualAlloc的唯一有效地保护属性。
分配物理存储器,调用AllocateUserPhysicalPages函数:
BOOL AllocateUserPhysicalPages(
HANDLE hProcess,
PULONG_PTR pulRAMPages,
PULONG_PTR aRAMPages);
这个函数会根据pulRAMPages
参数所指向的值来分配相应数量的内存页,然后将这些页面分配给hProcess
参数所标识的进程。操作系统会给每个页面指定一个页框号。
系统在分配内存页面时,会将每个内存页面的页框号保存到aRAMPages
参数所指向的数组中。
当该函数返回时,
pulRAMPages
指向
的值表示函数成功分配的页面的数量,通常这个值和传给该函数的值相同,但也有可能更小。
创建一个地址窗口并分配一块内存之后,接下来调用MapUserPhysicalPages把内存块指定给地址窗口。
BOOL MapUserPhysicalPages(
PVOID pvAddressWindow,//地址窗口的虚拟地址
ULONG_PTR ulRAMPages,//要通过该地址窗口看到多少个页面的内存
PULONG_PTR aRAMPages);表示要通过该地址窗口看到哪些页面的内存
不再需要使用该内存块时,可以调用FreeUserPhysicalPages来释放它:
BOOL FreeUserPhysicalPages(
HANDLE hProcess,
PULONG_PTR pulRAMPages,
PULONG_PTR aRAMPages);