15.1 预订(Reserve)地址空区域
(1)VirutalAlloc(pvAddress,dwSize,fdwAllocationType,fdwProtect)
①PVOID pvAddress参数:内存地址,要预订的地址空间中的哪一块。
A.这个参数也说明也该函数是较底层函数的原因,因为可以由我们指定在哪个地址分配内存!但这也是有限制的,即只能在用户模式分区中分配内存,否则将会失败。
B.预订区域时,系统始终是按CPU的分配粒度来分配区域的。如果指定的pvAddress不是64K的整数倍时,则系统会将pvAddress向下取整到64K的整数倍作为预订区域的起始地址(也叫区域的基地址)。如pvAddress=300×65536 + 8192时,会被取整为300×65536。(但应注意:在提交时,起始地址始终(与区域大小一样),始终都是页面大小的整数倍)
C.当该参数为NULL时,系统会自动找一块空闲的区域来预订。但系统不保证一定会从地址空间的底部往上分配,也不保证一定从地址空间的顶部往下分配。
D.如果在fdwAllocatioType指定MEM_TOP_DOWN时会尽可能地在高地址中分配空间
②SIZE_T dwSize参数——用来指定预订区域的大小,以字节为单位
A.区域大小是以CPU页面大小的整数倍来预订的(一般为4KB)。
B.如果指定的dwSize不是4KB的整数倍,则会被向上取整。如预订大小为62KB,则最终得到的区域大小为64KB。
③DWORD fdwAllocationType参数——要预订还是提交等
类型 |
备注 |
MEM_RESERVE |
A.预留区域时并没有分配物理存储器(物理存储器指物理内存、页交换文件或文件映像),只是增加了一个描述进程虚拟地址空间使用状态的数据结构,用来记录这个区域己被预订。 B.因没有真正分配物理存储器,所以这个区域不能直接访问,否则会引起“内存访问违规”。但“预订”操作的速度相对较快。 |
MEM_COMMIT |
A.提交区域,为预订区域调拨后备的物理储存器类型。注意,提交时也并没有立刻从物理内存分配空间,而是提交给页交换文件! B.直到当第一次访问这段区域时,系统会抛出“缺页错误”并处理该错误,这时才真正映射到物理内存。这种策略叫“demand-paging”策略,这既节省了时间,也节省了内存的浪费。 |
MEM_PHYSICAL |
A.只用在“地址空间扩展(AWE)”中,该标志必须且只能与MEM_RESERVE一起使用。 B.页面必须具有可读写属性(具体该标志的使用见后面的《AWE》一节) |
MEM_TOP_DOWN |
传NULL给pvAddress,同时对MEM_TOP_DOWN与MEM_RESERVE按位或传给fdwAllocationType可以让系统从尽可能高的内存地址来预订区域。这样可以防止从进程地址空间的中间预订,从而避免引起内存碎片 |
④DWORD fdwProtect参数——给区域指定保护属性
A.预订区域时,可以使用PAGE_NOACCESS、PAGE_READWRITE、PAGE_READONLY、PAGE_EXECUTE_*等几类。
B.但预订时不能使用PAGE_WRITECOPY、PAGE_EXECUTE_WRITECOPY、PAGE_GUARD、PAGE_NOCACHE和PAGE_WRITECOMBINE,因为这些标志跟物理存储器有关。
15.2 给区域调拨(Commit)物理存储器
(1)调用VirtualAlloc时传入MEM_COMMIT标志,在给物理存储器指定页面保护属性时,可以与预订时一致,也可以完全不同!但最终的保护性页以物理页面的保护属性为准(而不是区域的保护属性)
(2)在己预订的区域中,可以无须一下子给整个区域都调拨物理存储器,可以用pvAddress和dwSize来指定要想提交的部分。(即哪个内存地址,大小为多少,同时x86中都是以4KB的页面大小为基本单位)
(3)由于系统是基于一个完全页面(4KB大小)来指定保护属性的,所以同一个物理存储页不可能有不同的保护属性。但同一区域中的另一个页面可以是另一种保护属性。
15.3 同时预订和调拨物理存储器
(1)调用VirtualAlloc,并传入MEM_RESERVE|MEM_COMMIT标志
(2)大页面内存的分配
①返回大页面分配粒度:SIZE_T GetLargePageMinimum(),如果系统不支持大页面,则返回0
②调用VirtualAlloc时传入MEM_SERVE|MEM_COMMIT|MEM_LARGE_PAGE,即必须是同时预订和提交内存,而不能分开来实现。
③同时要分配的内存块大小(dwSize)必须是大页面分配粒度的整倍数,页面的保护属性fdwProtect也必须指定为PAGE_READWRITE。
【注意】
①大页面分配到的内存是不可换页的,也就是必须驻留内存,而不会被换出到页交换文件。所以就要求物理内存页面要被锁定。
②默认下,要使用“锁定内存页面”要先得到授权。(方法是“控制面板”→“管理工具”→“本地安全策略”→“本地策略”→“用户权限分配”→“锁定内存页”中添加用户或组)
15.4 何时调拨物理存储器
(1)先预订区域而不提交物理存储器(再次提醒,实际上是提交到页交换文件的)可以节省大量的物理存储器。
(2)当应用程序要访问未提交的内存地址时,会引发异常,系统会通知我们的应用程序。我们要用的就是给应用程序设置一个异常处理程序,在这里面再去真正地执行提交物理存储器的操作,从而实现内存的“按需分配”。
15.5 撤消调拨物理存储器及释放区域
(1)VirtualFree(pvAdress,dwSize,fdwFreeType)
①撤消并释放整个区域:pvAddress指定为区域的基地址,即预订区域时VirtualAlloc的返回值。同时dwSize必须传入0,因为系统知道该区域的大小。第3个参数传MEM_RELEASE。(注意此时不能只撤消一部分区域。此外,这时提交的内存被释放且预订的虚拟地址空间也归返,这就意味着MEM_RELEASE不能得MEM_DECOMMAND一起使用!)
②撤消部分物理存储器(但保留虚拟地址空间):指定pvAddress和dwSize,并传入MEM_DECOMMIT。与预订和提交物理存储器一校址,撤消也是基于页面粒度的,即系统会撤消地址空间被pvAddress到pvAddress+dwSize覆盖的所有页面(注意,页面是个4KB的空间,即如果pvAddress是位于某个页面中间,那该页面也作为整体被撤消)。
(2)何时撤消调拨物理存储器
①由于撤消操作是以页面粒度为基本单位的,当我们进行撤消时,可能会把当前正在使用的某个变量的内存也给撤消掉。
②要安全的撤消操作,就要为每个变量或结构体增加一个标志位,用来记录它们正在被使用的情况。只有等到同一个页面所有相邻的结构不在被使用时,才能进行撤消操作。
【DemandPaging程序】利用结构化异常机制进行内存的“按需分配”
#include <windows.h> #include <tchar.h> #include <locale.h> #define PAGELIMIT 80 //请求页面的总数 LPBYTE lpNextPage=NULL; //下一次请求的页面地址 DWORD dwPageSize = 0; //页面的大小(分配粒度) DWORD dwPages = 0; //当前己经请求的页面数量 INT PageFaultExceptionFilter(DWORD dwCode){ LPVOID lpvResult; //如果不是页面错误,则退出 if (dwCode !=EXCEPTION_ACCESS_VIOLATION){ _tprintf(_T("错误发生[%d]\n"), dwCode); return EXCEPTION_EXECUTE_HANDLER; //当错误发生时,执行except代码块中的内容。 } _tprintf(_T("页面访问错误,")); if (dwPages >=PAGELIMIT){ _tprintf(_T("超出页面数量%d\n"), PAGELIMIT); return EXCEPTION_EXECUTE_HANDLER; //当错误发生时,执行except代码块中的内容。 } lpvResult = VirtualAlloc((LPVOID)lpNextPage, dwPageSize, MEM_COMMIT, PAGE_READWRITE); if (lpvResult == NULL){ _tprintf(_T("提交新页面失败!\n")); return EXCEPTION_EXECUTE_HANDLER; } else{ _tprintf(_T("将重新提交一个新页面!\n")); } dwPages++; lpNextPage += dwPageSize; return EXCEPTION_CONTINUE_EXECUTION; //当错误发生时,执行except代码块后面的内容。 } VOID ErrorExit(LPTSTR oops){ _tprintf(_T("错误!%s,出错代码为%ld\n"), oops, GetLastError()); exit(0); } int _tmain(){ _tsetlocale(LC_ALL, _T("chs")); LPVOID lpvBase; //要测试的内存基地址 LPTSTR lpPtr; //通用的字符指针 BOOL bSuccess; // SYSTEM_INFO si; //系统信息结构体 GetSystemInfo(&si); dwPageSize = si.dwPageSize; _tprintf(_T("CPU页面大小为%dKB.\n"), si.dwPageSize/1024); //在进程的虚拟空间中预订页面 lpvBase = VirtualAlloc(NULL, //系统自动选择基地址 PAGELIMIT*dwPageSize,//区域的大小 MEM_RESERVE, //预订(注意不是提交) PAGE_NOACCESS);//保护属性,不可读写 if (lpvBase == NULL){ ErrorExit(_T("预订页面失败")); } lpPtr =(LPTSTR)lpvBase; lpNextPage = (LPBYTE)lpvBase; for (DWORD i = 0; i < PAGELIMIT*dwPageSize/sizeof(TCHAR);i++){ __try{ //写入内存 lpPtr[i] = _T(‘a‘); //因未提交物理存储器,这里会引发异常 } //如果发生页面错误,则提交另一个页面并尝试继续 __except (PageFaultExceptionFilter(GetExceptionCode())){ //以下的代码只有在“过滤函数”中提交下个页面失败时 //才会被调用 ExitProcess(GetLastError()); } } //释放区域 bSuccess = VirtualFree(lpvBase, 0, //当使用MEM_RELEASE时,必须为0 MEM_RELEASE);//撤消提交的区域(这里不用加MEM_DECOMMIT) _tprintf(_T("释放操作%s.\n"), bSuccess ? _T("成功") : _T("失败")); return 0; }
【VMAlloc程序】内存的垃极收集(VirtualFree函数的应用)