现在的位置: 首页 > 综合 > 正文

Windows内存管理机制及C++内存分配实例(三):虚拟内存

2013年10月05日 ⁄ 综合 ⁄ 共 9722字 ⁄ 字号 评论关闭

3.内存管理机制--虚拟内存 (VM)

·虚拟内存使用场合
虚拟内存最适合用来管理大型对象或数据结构。比如说,电子表格程序,有很多单元格,但是也许大多数的单元格是没有数据的,用不着分配空间。也许,你会想到用动态链表,但是访问又没有数组快。定义二维数组,就会浪费很多空间。
它的优点是同时具有数组的快速和链表的小空间的优点。

·分配虚拟内存
如果你程序需要大块内存,你可以先保留内存,需要的时候再提交物理存储器。在需要的时候再提交才能有效的利用内存。一般来说,如果需要内存大于1M,用虚拟内存比较好。

·保留
用以下Windows 函数保留内存块VirtualAlloc (PVOID 开始地址,SIZE_T 大小,DWORD 类型,DWORD 保护属性)
一般情况下,你不需要指定“开始地址”,因为你不知道进程的那段空间是不是已经被占用了;所以你可以用NULL。“大小”是你需要的内存字节;“类型”有MEM_RESERVE(保留)、MEM_RELEASE(释放)和MEM_COMMIT(提交)。“保护属性”在前面章节有详细介绍,只能用前六种属性。
如果你要保留的是长久不会释放的内存区,就保留在较高的空间区域,这样不会产生碎片。用这个类型标志可以达到:
MEM_RESERVE|MEM_TOP_DOWN。
C++程序:保留1G的空间
   LPVOID pV=VirtualAlloc(NULL,1000*1024*1024,MEM_RESERVE|MEM_TOP_DOWN,PAGE_READWRITE);
   if(pV==NULL)
       cout<<"没有那么多虚拟空间!"<<endl;
   MEMORYSTATUS memStatusVirtual1;
   GlobalMemoryStatus(&memStatusVirtual1);
   cout<<"虚拟内存分配:"<<endl;
   printf("指针地址=%x/n",pV);
   cout<<"减少物理内存="<<memStatusVirtual.dwAvailPhys-memStatusVirtual1.dwAvailPhys<<endl;
   cout<<"减少可用页文件="<<memStatusVirtual.dwAvailPageFile-memStatusVirtual1.dwAvailPageFile<<endl;
   cout<<"减少可用进程空间="<<memStatusVirtual.dwAvailVirtual-memStatusVirtual1.dwAvailVirtual<<endl<<endl;

结果如下:

可见,进程空间减少了1G;减少的物理内存和可用页文件用来管理页目和页表。但是,现在访问空间的话,会出错的:
   int * iV=(int*)pV;
   //iV[0]=1;现在访问会出错,出现访问违规

·提交
你必须提供一个初始地址和提交的大小。提交的大小系统会变成页面的倍数,因为只能按页面提交。指定类型是MEM_COMMIT。保护属性最好跟区域的保护属性一致,这样可以提高系统管理的效率。
C++程序:提交100M的空间
   LPVOID pP=VirtualAlloc(pV,100*1024*1024,MEM_COMMIT,PAGE_READWRITE);    
   if(pP==NULL)
      cout<<"没有那么多物理空间!"<<endl;
   int * iP=(int*)pP;
   iP[0]=3;
   iP[100/sizeof(int)*1024*1024-1]=5;//这是能访问的最后一个地址
   //iP[100/sizeof(int)*1024*1024]=5;访问出错

·保留&提交
你可以用类型MEM_RESERVE|MEM_COMMIT一次全部提交。但是这样的话,没有有效地利用内存,和使用一般的C++动态分配内存函数一样了。

·更改保护属性
更改已经提交的页面的保护属性,有时候会很有用处,假设你在访问数据后,不想别的函数再访问,或者出于防止指针乱指改变结构的目的,你可以更改数据所处的页面的属性,让别人无法访问。
VirtualProtect (PVOID 基地址,SIZE_T 大小,DWORD 新属性,DWORD 旧属性)
“基地址”是你想改变的页面的地址,注意,不能跨区改变。
C++程序:更改一页的页面属性,改为只读,看看还能不能访问
   DWORD protect;
   iP[0]=8;
   VirtualProtect(pV,4096,PAGE_READONLY,&protect);
   int * iP=(int*)pV;
   iP[1024]=9;//可以访问,因为在那一页之外
              //iP[0]=9;不可以访问,只读
              //还原保护属性
   VirtualProtect(pV,4096,PAGE_READWRITE,&protect);
   cout<<"初始值="<<iP[0]<<endl;//可以访问

·清除物理存储器内容

清除页面指的是,将页面清零,也就是说当作页面没有改变。假设数据存在物理内存中,系统没有RAM页面后,会将这个页面暂时写进虚拟内存页文件中,这样来回的倒腾系统会很慢;如果那一页数据已经不需要的话,系统可以直接使用。当程序需要它那一页时,系统会分配另一页给它。
VirtualAlloc (PVOID 开始地址,SIZE_T 大小,DWORD 类型,DWORD 保护属性)
“大小”如果小于一个页面的话,函数会执行失败,因为系统使用四舍五入的方法;“类型”是MEM_RESET。
有人说,为什么需要清除呢,释放不就行了吗?你要知道,释放了后,程序就无法访问了。现在只是因为不需要结构的内容了,顺便提高一下系统的性能;之后程序仍然需要访问这个结构的。

C++程序:
清除1M的页面:
   PVOID re=VirtualAlloc(pV,1024*1024,MEM_RESET,PAGE_READWRITE);
   if(re==NULL)
      cout<<"清除失败!"<<endl;
这时候,页面可能还没有被清零,因为如果系统没有RAM请求的话,页面内存保存不变的,为了看看被清零的效果,程序人为的请求大量页面:

C++程序:
   VirtualAlloc((char*)pV+100*1024*1024+4096,memStatus.dwAvailPhys+10000000,MEM_COMMIT,PAGE_READWRITE);//没访问之前是不给物理内存的。   
   char* pp=(char*)pV+100*1024*1024+4096;
   for(int i=0;i<memStatus.dwAvailPhys+10000000;i++)
      pp[i]='V';//逼他使用物理内存,而不使用页文件
   GlobalMemoryStatus(&memStatus);
   cout<<"内存初始状态:"<<endl;
   cout<<"长度="<<memStatus.dwLength<<endl;
   cout<<"内存繁忙程度="<<memStatus.dwMemoryLoad<<endl;
   cout<<"总物理内存="<<memStatus.dwTotalPhys<<endl;
   cout<<"可用物理内存="<<memStatus.dwAvailPhys<<endl;
   cout<<"总页文件="<<memStatus.dwTotalPageFile<<endl;
   cout<<"可用页文件="<<memStatus.dwAvailPageFile<<endl;
   cout<<"总进程空间="<<memStatus.dwTotalVirtual<<endl;
   cout<<"可用进程空间="<<memStatus.dwAvailVirtual<<end;
   cout<<"清除后="<<iP[0]<<endl;
结果如下:

当内存所剩无几时,系统将刚清除的内存页面分配出去,同时不会把页面的内存写到虚拟页面文件中。可以看见,原先是8的值现在是0了。

·虚拟内存的关键之处
虚拟内存存在的优点是,需要的时候才真正分配内存。那么程序必须决定何时才提交内存。
如果访问没有提交内存的数据结构,系统会产生访问违规的错误。提交的最好方法是,当你程序需要访问虚拟内存的数据结构时,假设它已经是分配内存的,然后异常处理可能出现的错误。对于访问违规的错误,就提交这个地址的内存。

·释放
可以释放整个保留的空间,或者只释放分配的一些物理内存。
释放特定分配的物理内存:
如果不想释放所有空间,可以只释放某些物理内存。
“开始地址”是页面的基地址,这个地址不一定是第一页的地址,一个窍门是提供一页中的某个地址就行了,因为系统会做页边界处理,取该页的首地址;“大小”是页面的要释放的字节数;“类型”是MEM_DECOMMIT。
C++程序:
   //只释放物理内存
   VirtualFree((int*)pV+2000,50*1024*1024,MEM_DECOMMIT);
   int* a=(int*)pV;
   a[10]=2;//可以使用,没有释放这一页
   MEMORYSTATUS memStatusVirtual3;
   GlobalMemoryStatus(&memStatusVirtual3);
   cout<<"物理内存释放:"<<endl;
   cout<<"增加物理内存="<<memStatusVirtual3.dwAvailPhys-memStatusVirtual2.dwAvailPhys<<endl;
   cout<<"增加可用页文件="<<memStatusVirtual3.dwAvailPageFile-memStatusVirtual2.dwAvailPageFile<<endl;
   cout<<"增加可用进程空间="<<memStatusVirtual3.dwAvailVirtual-memStatusVirtual2.dwAvailVirtual<<endl<<endl;
结果如下:

可以看见,只释放物理内存,没有释放进程的空间。

释放整个保留的空间:
VirtualFree (LPVOID 开始地址,SIZE_T 大小,DWORD 类型)
“开始地址”一定是该区域的基地址;“大小”必须是0,因为只能释放整个保留的空间;“类型”是MEM_RELEASE。
C++程序:
   VirtualFree(pV,0,MEM_RELEASE);
   //a[10]=2;不能使用了,进程空间也释放了
   MEMORYSTATUS memStatusVirtual4;
   GlobalMemoryStatus(&memStatusVirtual4);
   cout<<"虚拟内存释放:"<<endl;
   cout<<"增加物理内存="<<memStatusVirtual4.dwAvailPhys-memStatusVirtual3.dwAvailPhys <<endl;
   cout<<"增加可用页文件="<<memStatusVirtual4.dwAvailPageFile-memStatusVirtual3.dwAvailPageFile<<endl;
   cout<<"增加可用进程空间="<<memStatusVirtual4.dwAvailVirtual-memStatusVirtual3.dwAvailVirtual<<endl<<endl;
结果如下:

整个分配的进程区域被释放了,包括所占的物理内存和页文件。

·何时释放
如果数组的元素大小是小于一个页面4K的话,你需要记录哪些空间不需要,哪些在一个页面上,可以用一个元素一个Bit来记录;另外,你可以创建一个线程定时检测无用单元。

·扩展地址AWE
AWE是内存管理器功能的一套应用程序编程接口 (API) ,它使程序能够将物理内存保留为非分页内存,然后将非分页内存部分动态映射到程序的内存工作集。此过程使内存密集型程序(如大型数据库系统)能够为数据保留大量的物理内存,而不必交换分页文件以供使用。相反,数据在工作集中进行交换,并且保留的内存超过 4 GB 范围。

对于物理内存小于2G进程空间时,它的作用是:不必要在物理内存和虚拟页文件中交换。

对于物理内存大于2G进程空间时,它的作用是:应用程序能够访问的物理内存大于2G,也就相当于进程空间超越了2G的范围;同时具有上述优点。
3GB,当在boot.ini 上加上 /3GB 选项时,应用程序的进程空间增加了1G,也就是说,你写程序时,可以分配的空间又增大了1G,而不管物理内存是多少,反正有虚拟内存的页文件,大不了慢点。
PAE,当在boot.ini上加上 /PAE 选项时,操作系统可以支持大于4G的物理内存,否则,你加再多内存操作系统也是不认的,因为管理这么大的内存需要特殊处理。所以,你内存小于4G是没有必要加这个选项的。注意,当要支持大于16G的物理内存时,不能使用/3G选项,因为,只有1G的系统空间是不能管理超过16G的内存的。
AWE,当在boot.ini上加上 /AWE选项时,应用程序可以为自己保留物理内存,直接的使用物理内存而不通过页文件,也不会被页文件交换出去。当内存大于3G时,就显得特别有用。因为可以充分利用物理内存。
当物理内存大于4G时,需要/PAE的支持。
以下是一个boot.ini的实例图,是我机器上的:

要使用AWE,需要用户具有Lock Pages in Memory权限,这个在控制面板中的本地计算机政策中设置。
第一,分配进程虚拟空间:
VirtualAlloc (PVOID 开始地址,SIZE_T 大小,DWORD 类型,DWORD 保护属性)
“开始地址”可以是NULL,由系统分配进程空间;“类型”是MEM_RESERVE|MEM_PHYSICAL;“保护属性”只能是
PAGE_READWRITE。
MEM_PHYSICAL指的是区域将受物理存储器的支持。
第二,你要计算出分配的页面数目PageCount:利用本文第二节的GetSystemInfo可以计算出来。
第三,分配物理内存页面:
AllocateUserPhysicalPages (HANDLE 进程句柄,SIZE_T 页数,ULONG_PTR 页面指针数组)
进程句柄可以用GetCurrentProcess()获得;页数是刚计算出来的页数PageCount;页面数组指针unsigned long* Array[PageCount]。
系统会将分配结果存进这个数组。
第四,将物理内存与虚拟空间进行映射:
MapUserPhysicalPages (PVOID 开始地址,SIZE_T 页数,ULONG_PTR 页面指针数组)
“开始地址”是第一步分配的空间;
这样的话,虚拟地址就可以使用了。
如果“页面指针数组”是NULL,则取消映射。
第五,释放物理页面
FreeUserPhysicalPages (HANDLE 进程句柄,SIZE_T 页数,ULONG_PTR 页面指针数组)
这个除了释放物理页面外,还会取消物理页面的映射。
第六,释放进程空间
VirtualFree (PVOID 开始地址,0,MEM_RELEASE)
C++程序:
首先,在登录用户有了Lock Pages in Memory权限以后,还需要调用Windows API激活这个权限。
BOOL VirtualMem::LoggedSetLockPagesPrivilege ( HANDLE hProcess,BOOL bEnable)                     
{
   struct {
      DWORD Count;//数组的个数
      LUID_AND_ATTRIBUTES Privilege [1];} Info;
      HANDLE Token;
      //打开本进程的权限句柄
      BOOL Result = OpenProcessToken ( hProcess,TOKEN_ADJUST_PRIVILEGES,& Token);
      If (Result!= TRUE )
      {
         printf( "Cannot open process token./n" );
         return FALSE;
      }
      //我们只改变一个属性
      Info.Count = 1;
      //准备激活
      if( bEnable )
         Info.Privilege[0].Attributes = SE_PRIVILEGE_ENABLED;
      else
         Info.Privilege[0].Attributes = 0;
      //根据权限名字找到LGUID
      Result = LookupPrivilegeValue ( NULL,SE_LOCK_MEMORY_NAME,&(Info.Privilege[0].Luid));
      if( Result != TRUE )
      {
         printf( "Cannot get privilege for %s./n", SE_LOCK_MEMORY_NAME );
         return FALSE;
      }
      // 激活Lock Pages in Memory权限
      Result = AdjustTokenPrivileges ( Token, FALSE,(PTOKEN_PRIVILEGES) &Info,0, NULL, NULL);
      if( Result != TRUE )
      {
         printf ("Cannot adjust token privileges (%u)/n", GetLastError() );
         return FALSE;
      }
      else
      {
         if( GetLastError() != ERROR_SUCCESS )
         {
            printf ("Cannot enable the SE_LOCK_MEMORY_NAME privilege; ");
            printf ("please check the local policy./n");
            return FALSE;
         }
      }
      CloseHandle( Token );
      return TRUE;
}

分配100M虚拟空间:
   PVOID pVirtual=VirtualAlloc(NULL,100*1024*1024,MEM_RESERVE|MEM_PHYSICAL,PAGE_READWRITE);
   if(pVirtual==NULL)
      cout<<"没有那么大连续进程空间!"<<endl;
   MEMORYSTATUS memStatusVirtual5;
   GlobalMemoryStatus(&memStatusVirtual5);
   cout<<"虚拟内存分配:"<<endl;
   cout<<"减少物理内存="<<memStatusVirtual4.dwAvailPhys-memStatusVirtual5.dwAvailPhys<<endl
   cout<<"减少可用页文件="<<memStatusVirtual4.dwAvailPageFile-memStatusVirtual5.dwAvailPageFile<<endl;
   cout<<"减少可用进程空间="<<memStatusVirtual4.dwAvailVirtual-memStatusVirtual5.dwAvailVirtual<<endl<<endl;
结果如下:

可以看见,只分配了进程空间,没有分配物理内存。
分配物理内存:
   ULONG_PTR pages=(ULONG_PTR)100*1024*1024/sysInfo.dwPageSize;
   ULONG_PTR *frameArray=new ULONG_PTR[pages];
   //如果没激活权限,是不能调用这个方法的,可以调用,但是返回FALSE
   BOOL flag=AllocateUserPhysicalPages(GetCurrentProcess(),&pages,frameArray);
   if(flag==FALSE)
      cout<<"分配物理内存失败!"<<endl;
   MEMORYSTATUS memStatusVirtual6;
   GlobalMemoryStatus(&memStatusVirtual6);
   cout<<"物理内存分配:"<<endl;
   cout<<"减少物理内存="<<memStatusVirtual5.dwAvailPhys-memStatusVirtual6.dwAvailPhys<<endl
   cout<<"减少可用页文件="<<memStatusVirtual5.dwAvailPageFile-memStatusVirtual6.dwAvailPageFile<<endl;
   cout<<"减少可用进程空间="<<memStatusVirtual5.dwAvailVirtual-memStatusVirtual6.dwAvailVirtual<<endl<<endl;
结果如下:

分配了物理内存,可能分配时需要进程空间管理。
物理内存映射进程空间:
   int* pVInt=(int*)pVirtual;
   //pVInt[0]=10;这时候访问会出错
   flag=MapUserPhysicalPages(pVirtual,1,frameArray);
   if(flag==FALSE)
      cout<<"映射物理内存失败!"<<endl;
   MEMORYSTATUS memStatusVirtual7;
   GlobalMemoryStatus(&memStatusVirtual7);
   cout<<"物理内存分配:"<<endl;
   cout<<"减少物理内存="<<memStatusVirtual6.dwAvailPhys-memStatusVirtual7.dwAvailPhys<<endl
   cout<<"减少可用页文件="<<memStatusVirtual6.dwAvailPageFile-memStatusVirtual7.dwAvailPageFile<<endl;
   cout<<"减少可用进程空间="<<memStatusVirtual6.dwAvailVirtual-memStatusVirtual7.dwAvailVirtual<<endl<<endl;
结果如下:

这个过程没有损失任何东西。
看看第一次映射和第二次映射的值:
   pVInt[0]=10;
   cout<<"第一次映射值="<<pVInt[0]<<endl;
   flag=MapUserPhysicalPages(pVirtual,1,frameArray+1);
   if(flag==FALSE)
      cout<<"映射物理内存失败!"<<endl;
   pVInt[0]=21;
   cout<<"第二次映射值="<<pVInt[0]<<endl;
   flag=MapUserPhysicalPages(pVirtual,1,frameArray);
   if(flag==FALSE)
      cout<<"映射物理内存失败!"<<endl;
   cout<<"再现第一次映射值="<<pVInt[0]<<endl;
结果如下:

可以看出,第二次映射的值没有覆盖第一次映射的值,也就是说,用同一个进程空间地址可以取出两份数据,这样的话,相当于进程的地址空间增大了。

抱歉!评论已关闭.