在应用程序中使用虚拟内存
Windows提供了以下三种机制对内存进行操控:
一:虚拟内存。最适合来管理大型对象数据或大型结构数组。
二:内存映射文件。最适合用来管理大型数据流,以及在同一机 器上运行的多个进程之间共享数据。
三:堆。最适合用来管理大量的小型对象。
很多人都对VirtualAlloc和malloc 或new的区别不是很清楚,我也一样。今天搜索下了,发现这句话说的很清楚了:
VirtualAlloc要进入内核模式,算法特复杂,比较慢,而且分配粒度是4k,用来分配小块内存很浪费
malloc先用VirtualAlloc弄一大块内存,后面在堆上分配时就不用进入内核模式,算法也简单些,而且分配粒度比较小
VirtualAlloc只能分配4KB为单位的页面,适合大型数据或者内存映射文件等用途。而堆的申请分配就没有这个限制,更为灵活。
有的人嫌malloc还不够精简,于是又在堆上面开辟自己的内存池,更加轻量级
本文将主要介绍虚拟内存。
Windows提供了一些用来操纵虚拟内存 的函数,我们可以通过这些函数直接预订地址空间区域,并给这些预订的区域调拨来自页交换文件的物理存储器。
预定地址空间区域。
可以通过调用VirtualAlloc函数来运行:
PVOID VirtualAlloc( PVOID pvAddress, SIZE_T dwSize, DWORD fdwAllocationType, DWORD fdwProtect);
pvAddress是内存地址。用来告诉我们想要运行地址空间中的哪一块。
当传入NULL时,系统会自动找到一块闲置区域。
如果在pvAddress标识的内存块中找不到闲置区域,或闲置区域不够大函数将返回NULL。
如果VirtualAlloc能满足我们的要求,它会预定一块区域并返回该区域的基地址。
dwSize用来指定我们想要预订的区域大小。以字节为单位。 系统始终以cpu页面大小整数倍来预定区域。且起始地址是按照分配粒度64kB的整数倍来预定的。
fdwAllocationType用来告诉系统我们到底是要预订还是要调拨物理存储器。如果要预订区域可以传入:MEM_RESERVE。如果我们想让系统从尽可能高的内存地址来预定区域,必须传入NULL给pvAddress,同时对MEM_TOP_DOWN和MEM_RESERVE标志进行按位或操作。
fdwProtect给区域指定保护属性。区域的保护属性对调拨给该区域的物理存储器不起任何作用。无论指定何种保护属性,只要还未给该区域调拨物理存储器都会导致访问违规。
预订时指定的属性应该跟调拨时指定的属性相同,这样系统内部处理效率会更高。
调拨物理存储器
预定区域后还需要给该区域调拨物理存储器。系统会从页交换文件中调拨物理存储器给该区域。在调拨物理存储器时,起始地址和区域大小始终都是页面大小的整数倍。调拨物理存储器同样需要调用VirtualAlloc。但这次我们需要传入MEM_COMMIT来作为fdwAllocationType的值。
pvAddress标识要调拨物理存储器的区域的起始地址。
dwSize表示物理存储器的大小。以字节为单位。我们并不需要给整个区域都调拨物理存储器。
有了起始地址和大小就可以标识一段区域。
同时预定和调拨物理存储器
有时我们项同时预定区域并给该区域调拨物理存储器。同样需要调用VirtualAlloc。但是MEM_RESERVE要和MEM_COMMIT按位或并将它们传给fdwAllocationType。此时系统会为整个区域调拨物理存储器。
撤销调拨物理存储器及释放区域。
要撤销调拨给区域的物理存储器或是释放地址空间中的一整块区域。可以调用VirtualFree函数:
BOOL VirtualFree( LPVOID pvAddress, SIZE_T dwSize, DWORD fdwFreeType);
pvAddress参数必须是区域的基地址。该地址就是预定区域时VirtualAlloc返回的地址。由于系统在内部会记录该地址处的区域大小,因此我们可以且必须传入0给dwSize。
当传入MEM_RELEASE给fdwFreeType时是想告诉系统撤销调拨给该区域的所有物理存储器,并释放该区域。
我们可以撤销调拨给该区域的一部分物理存储器。此时需要传入pvAddress来告诉系统我们想要撤销调拨的区域的起始地址。dwSize传入想要撤销调拨区域的物理存储器大小,并传入MEM_DECOMMIT给fdwFreeType。
和调拨物理存储器一样,撤销调拨也是基于页面粒度的。也就是说,如果给定的地址位于一个页面中,那么系统会撤销调拨整个页面。如果dwSize为0,而pvAddress又是该区域的基地址,那么VirtualFree会调拨给该区域的所有页面。
改变保护属性
实际中我们很少需要改变已调拨物理存储器的保护属性。比如,我们可以写一个管理链表的程序,把链表中的节点保存在一个已预订的区域中。我们可以设计这样的链表处理函数,让它们在每个函数的开头把物理存储器的保护属性改成PAGE_READWRITE,访问后,再把保护属性改回FPAGE_NOACCESS。这样就可以保护链表数据免受程序中其他缺陷的影响。
我们可以调用VirtualProtect函数来改变一个内存页面的保护属性:
BOOL VirtualProtect( PVOID pvAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD pflOldProtect);
pvAddress指向内存基地址。
dwSize表示要改变保护区域的大小,以字节为单位。
flNewProtect可以是除了PAGE_WRITECOPY和PAGE_EXECUTE_WRITECOPY之外的任何PAGE_*属性。
pflOldProtect是一个指针,返回原来的属性。一定不可以传入NULL,必须传入一个有效的地址给VirtualProtect,否则函数执行失败。
由于保护属性是与整个物理存储页相关联的,VirtualProtect会改变pvAddress和dwSize跨越的所有页面的属性。而不会仅仅对一个字节改变属性,这是没有意义的。
重置物理存储器的内容
当我们修改物理内存页时,系统会尽量把改动把持在内存中。当应用程序在运行时,可能需要将数据载入内存,系统会在内存中查找可用的页面。如果找到闲置页面,就将数据载入此页面。如果没有找到,系统就会采用一定的算法,置换一些页面。如果该页面已经被修改过,系统会将这些页面置换到页交换文件。
Windows提供一项特性,使得应用程序能够提高自身的性能。这一特性就是重置物理存储器。重置物理存储器的意思是告诉系统一个或几个物理存储器页中数据没有被修改过。在需要闲置页面时,可以直接将它们覆盖,而不需要将它们写入页交换文件。有些应用程序需要在一小段时间内使用存储器,之后就不需要保存存储器中的内容。为了提高性能,应用程序应该告诉系统此页面没有被修改过,不要在页交换文件中保存存储页。
为了重置存储器,应用程序应该调用VirtualAlloc函数并在第二个参数中传MEM_RESET标志。
PINT pnData=(PINT)VirtualAlloc(NULL,1024, MEM_RELEASE|MEM_COMMIT,PAGE_READWRITE); pnData[0]=100; pnData[1]=200; VirtualAlloc((PVOID)pnData,sizeof(int),MEM_RESET,PAGE_READWRITE);
这段代码首先调拨了一块存储器,然后将这块内存的前4个字节标记为可以被重置。但是重置存储器的操作会调用失败。第二个VirtualAlloc会返回NULL。GetLastError会返回ERROR_INVALID_ADDRESS。
这是因为在传入MEM_RESET给VirtualAlloc时,函数会把基地址向下取整到页面大小的整数倍。(其他标志时都是向上去整,此处要注意。)其目的是确保在同一页面中还有其他重要数据的情况下,不会把它们丢弃。前面的例子,向下去整到页面整数倍是0,这是没有意义的。也就是说我们重置的页面必须是一个或几个完整的页面。不足一页的为了保护数据就不将它们设为可重置。
还要注意MEM_RESET不能和其他标志一起使用,它不合群,只能单独使用。否则调用会失败。
VirtualQuery函数。
VirtualQuery可以用来查询与地址空间有关的特定信息。比如大小,存储器类型及保护属性。
DWORD VirtualQuery( LPCVOID pvAddress, PMEMORY_BASIC_INFORMATION pmbi, DWORD dwLength);
pvAddress指定需要查询的虚拟内存地址。
dwLength指定MEMORY_BASIC_INFORMATION结构大小。
pmbi返回MEM_BASIC_INFORMATION结构地址。该地址定义如下:
typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress;//等于pvAddress向下取整到页面大小。 PVOID AllocationBase;//区域基地址。 DWORD AllocationProtect;//预定区域时的保护属性。 SIZE_T RegionSize;//区域大小。以字节为单位。以BaseAddress为起始地址。 DWORD State;//区域页面的状态。 DWORD Protect;//保护属性 DWORD Type;// }MEMORY_BASIC_INFORMATION,*PMEMORY_BASIC_INFROMATION;
VirtualQueryEx与VirtualQuery的区别就是它可以传入一个进程句柄。也就意味着它可以查询其他进程地址空间的信息。
GlobalMemoryStatus可以用来取得当前内存状态的动态信息:
VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);
MEMORYSTATUS结构定义如下:
typedef struct _MEMORYSTATUS { DWORD dwLenght;//此结构大小。调用函数前必须初始化。 DWORD dwMemoryLoad;// SIZE_T dwTotalPhys; SIZE_T dwAvailablePhys; SIZE_T dwTotalPageFile; SIZE_T dwAvailablePageFile; SIZE_T dwTotalVirtual; SIZE_T dwAvailVirtual; }MEMORYSTATUS,*LPMEMORYSTATUS;
dwMemoryLoad成员告诉我们内存管理系统有多忙,它可以是0-100之间的任何值。实际中,这个值没有什么用处。
dwTotalPhys成员表示物理内存总量。
dwAvailPhys成员表示可分配的内存总理。都是以字节为单位。
dwTotalPageFile表示硬盘页交换文件最多能存放多少字节的数据。
dwTotalVirtual表示地址空间中各进程私有的那部分字节数。比2G少128k.从0x00000000-0x0000FFFF(空指针赋值分区)和从0x7FFF0000-0x7FFFFFFF(64KB进入分区)这两个分区应用程序不能访问。
dwAvailVirtual是唯一一个与进程有关的成员。它表示还有多少闲置的地址空间可以被使用。也就说dwTotalVirtual-进程预定的地址空间就等于dwAvailVirtual。
系统信息
操作系统的许多值是由系统所运行的主机决定的,如页面大小和分配粒度。使用GetSystemInfo可以获得与主机有关的值:
VOID GetSystemInfo(LPSYSTEM_INFO psi); SYSTEM_INFO结构定义如下: typedef struct _SYSTEM_INFO{ union{ struct { WORD wProcessorArchitecture; WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOLID lpMaximumApplicationAddress; DWORD_PTR dwActiveProcessorMask; DWORD dwNumberOfProcessor; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD wProcessorLevel; WORD wProcessorRevision; }SYSTEM_INFO,*LPSYSTEM_INFO;
上述这么多成员只有四个与内存有关。
dwPageSize表示cpu页面大小在x86和x64机器中,该值为4K。
lpMinimumApplicationAddress给出进程中可用地址空间最小的内存地址。由于每个进程地址空间最开始的64K是闲置的,因此该值为64K。
lpMaximumApplicationAddress给出每个进程私有地址空间中最大的
可用内存地址。
dwAllocationGranularity表示用于预定地址空间区域的分配粒度
其他的成员稍微做下介绍:
wReserved为今后拓展保留,不要使用。
dwNumOfProcessor机器cpu的数量。
dwActiveProcessorMask位掩码,用来表示哪些cpu处于活动状态。可以用来运行线程。
dwProcessorType已经作废。不再使用。
wProcessorArchitecture表示处理器的体系结构。如x86,x64;
wProcessorLevel进一步细分处理器的体系结构,比如Intel奔腾2或奔腾4.
wProcessorRevision进一步对wProcessLevel进行细分。
//以下代码演示了可重置内存及以上介绍的各个函数的使用。
#include"windows.h" #include<iostream> #include"tchar.h" TCHAR TextData[]=TEXT("c:\Users\yangyang\documents\visual studio 2010\Projects\MEM_RESET\MEM_RESET\main.cppc:\Users\yangyang\documents\visual studio 2010\Projects\MEM_RESET\MEM_RESET\main.cpp"); int main(int argc,char**argv) { PTSTR pszData=(PTSTR)VirtualAlloc(NULL,1024,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE); if(pszData==NULL) { MessageBox(NULL,TEXT("分配失败"),TEXT(""),MB_OK); } _tcscpy_s(pszData,1024,TextData); int ret=MessageBox(NULL,TEXT("以后是否还要访问该段内存?"),TEXT(""),MB_YESNO); if(ret==IDNO) { MEMORY_BASIC_INFORMATION mbi; VirtualQuery(pszData,&mbi,sizeof(mbi)); VirtualAlloc(pszData,mbi.RegionSize,MEM_RESET,PAGE_READWRITE); } MEMORYSTATUS mst; GlobalMemoryStatus(&mst); PVOID pvAddress=VirtualAlloc(NULL,mst.dwAvailVirtual,MEM_RESERVE|MEM_COMMIT,PAGE_READWRITE); if(pvAddress) { ZeroMemory(pvAddress,mst.dwAvailVirtual); } if(!_tcscmp(TextData,pszData)) { MessageBox(NULL,TEXT("已经保存!"),TEXT(""),MB_YESNO); } else { MessageBox(NULL,TEXT("未保存!"),TEXT(""),MB_YESNO); } getchar(); VirtualFree(pvAddress,0,MEM_RELEASE); VirtualFree(pszData,0,MEM_RELEASE); return 0; }
本文参考自《Windows核心编程》第五版第三部分。如有错误请不吝指正!
2012.12.27于山西大同