牛骨文教育服务平台(让学习变的简单)
博文笔记

内存管理机制(详细介绍)

创建时间:2016-09-13 投稿人: 浏览次数:160

(一):进程空间

在编程中,很多Windows或C++的内存函数不知道有什么区别,更别谈有效使用;根本的原因是,没有清楚的理解操作系统的内存管理机制,本文企图通过简单的总结描述,结合实例来阐明这个机制。

本文目的:

对Windows内存管理机制了解清楚,有效的利用C++内存函数管理和使用内存。

本文内容:

本文一共有六节,由于篇幅较多,故按节发表。其他章节请看本人博客的Windows内存管理及C++内存分配实例(二)(三)(四)(五)和(六)。

1.      进程地址空间

1.1地址空间

·        32|64位的系统|CPU

        操作系统运行在硬件CPU上,32位操作系统运行于32位CPU上,64位操作系统运行于64位CPU上;目前没有真正的64位CPU。

32位CPU一次只能操作32位二进制数;位数多CPU设计越复杂,软件设计越简单。

       软件的进程运行于32位系统上,其寻址位也是32位,能表示的空间是232=4G,范围从0x0000 0000~0xFFFF FFFF。

·        NULL指针分区

范围:0x0000 0000~0x0000 FFFF

作用:保护内存非法访问

例子:分配内存时,如果由于某种原因分配不成功,则返回空指针0x0000 0000;当用户继续使用比如改写数据时,系统将因为发生访问违规而退出。

        那么,为什么需要那么大的区域呢,一个地址值不就行了吗?我在想,是不是因为不让8或16位的程序运行于32位的系统上呢?!因为NULL分区刚好范围是16的进程空间。

·        独享用户分区

范围:0x0001 0000~0x7FFE FFFF

作用:进程只能读取或访问这个范围的虚拟地址;超越这个范围的行为都会产生违规退出。

例子:

        程序的二进制代码中所用的地址大部分将在这个范围,所有exe和dll文件都加载到这个。每个进程将近2G的空间是独享的。

注意:如果在boot.ini上设置了/3G,这个区域的范围从2G扩大为3G:0x0001 0000~0xBFFE FFFF。

·        共享内核分区

范围:0x8000 0000~0xFFFF FFFF

作用:这个空间是供操作系统内核代码、设备驱动程序、设备I/O高速缓存、非页面内存池的分配、进程目表和页表等。

例子:

       这段地址各进程是可以共享的。                                                                                                                                        

注意:如果在boot.ini上设置了/3G,这个区域的范围从2G缩小为1G:0xC000 0000~0xFFFF FFFF。

       通过以上分析,可以知道,如果系统有n个进程,它所需的虚拟空间是:2G*n+2G (内核只需2G的共享空间)。

 

1.2地址映射

·        区域

区域指的是上述地址空间中的一片连续地址。区域的大小必须是粒度(64k) 的整数倍,不是的话系统自动处理成整数倍。不同CPU粒度大小是不一样的,大部分都是64K。

区域的状态有:空闲、私有、映射、映像。

在你的应用程序中,申请空间的过程称作保留(预订),可以用VirtualAlloc;删除空间的过程为释放,可以用VirtualFree。

        在程序里预订了地址空间以后,你还不可以存取数据,因为你还没有付钱,没有真实的RAM和它关联。这时候的区域状态是私有;默认情况下,区域状态是空闲;当exe或DLL文件被映射进了进程空间后,区域状态变成映像;

当一般数据文件被映射进了进程空间后,区域状态变成映射。

·        物理存储器

Windows各系列支持的内存上限是不一样的,从2G到64G不等。理论上32位CPU,硬件上只能支持4G内存的寻址;能支持超过4G的内存只能靠其他技术来弥补。顺便提一下,Windows个人版只能支持最大2G内存,Intel使用Address Windows Extension (AWE) 技术使得寻址范围为236=64G。当然,也得操作系统配合。

        内存分配的最小单位是4K或8K,一般来说,根据CPU不同而不同,后面你可以看到可以通过系统函数得到区域粒度和页面粒度。

·        页文件

页文件是存在硬盘上的系统文件,它的大小可以在系统属性里面设置,它相当于物理内存,所以称为虚拟内存。事实上,它的大小是影响系统快慢的关键所在,如果物理内存不多的情况下。

       每页的大小和上述所说内存分配的最小单位是一样的,通常是4K或8K。

·        访问属性

物理页面的访问属性指的是对页面进行的具体操作:可读、可写、可执行。CPU一般不支持可执行,它认为可读就是可执行。但是,操作系统提供这个可执行的权限。

PAGE_NOACCESS

PAGE_READONLY

PAGE_READWRITE

PAGE_EXECUTE

PAGE_EXECUTE_READ

PAGE_EXECUTE_READWRITE

这6个属性很好理解,第一个是拒绝所有操作,最后一个是接受收有操作;

PAGE_WRITECOPY

PAGE_EXECUTE_WRITECOPY

这两个属性在运行同一个程序的多个实例时非常有用;它使得程序可以共享代码段和数据段。一般情况下,多个进程只读或执行页面,如果要写的话,将会Copy页面到新的页面。通过映射exe文件时设置这两个属性可以达到这个目的。

PAGE_NOCACHE

PAGE_WRITECOMBINE

这两个是开发设备驱动的时候需要的。

PAGE_GUARD

当往页面写入一个字节时,应用程序会收到堆栈溢出通知,在线程堆栈时有用。

·        映射过程

进程地址空间的地址是虚拟地址,也就是说,当取到指令时,需要把虚拟地址转化为物理地址才能够存取数据。这个工作通过页目和页表进行。

wps_clip_image-23282

从图中可以看出,页目大小为4K,其中每一项(32位)保存一个页表的物理地址;每个页表大小为4K,其中每一项(32位)保存一个物理页的物理地址,一共有1024个页表。利用这4K+4K*1K=4.4M的空间可以表示进程的1024*1024* (一页4K) =4G的地址空间。

进程空间中的32位地址如下:

wps_clip_image-3621

高10位用来找到1024个页目项中的一项,取出页表的物理地址后,利用中10位来得到页表项的值,根据这个值得到物理页的地址,由于一页有4K大小,利用低12位得到单元地址,这样就可以访问这个内存单元了。

        每个进程都有自己的一个页目和页表,那么,刚开始进程是怎么找到页目所在的物理页呢?答案是CPU的CR3寄存器会保存当前进程的页目物理地址。

        当进程被创建时,同时需要创建页目和页表,一共需要4.4M。在进程的空间中,0xC030 0000~0xC030 0FFF是用来保存页目的4k空间。0xC000 0000~0xC03F FFFF是用来保存页表的4M空间。也就是说程序里面访问这些地址你是可以读取页目和页表的具体值的(要工作在内核方式下)。有一点我不明白的是,页表的空间包含了页目的空间!

        至于说,页目和页表是保存在物理内存还是页文件中,我觉得,页目比较常用,应该在物理内存的概率大点,页表需要时再从页文件导入物理内存中。

        页目项和页表项是一个32位的值,当页目项第0位为1时,表明页表已经在物理内存中;当页表项第0位为1时,表明访问的数据已经在内存中。还有很多数据是否已经被改变,是否可读写等标志。另外,当页目项第7位为1时,表明这是一个4M的页面,这值已经是物理页地址,用虚拟地址的低22位作为偏移量。还有很多:数据是否已经被改变、是否可读写等标志。

 

1.3 一个例子

·        编写生成软件程序exe

软件描述如下:

Main ()

{

1:定义全局变量

2:处理函数逻辑(Load 所需DLL库,调用方法处理逻辑)

3:定义并实现各种方法(方法含有局部变量)

                       4:程序结束

}

将程序编译,生成exe文件,附带所需的DLL库。

·        exe文件格式

exe文件有自己的格式,有若干节(section):.text用来放二进制代码(exe或dll);.data用来放各种全局数据。

.text

指令1:move a, b

指令2:add a, b

.data

数据1:a=2

数据2:b=1

这些地址都是虚拟地址,也就是进程的地址空间。

·        运行exe程序

建立进程:运行这个exe程序时,系统会创建一个进程,建立进程控制块PCB,生成进程页目和页表,放到PCB中。

 

数据对齐:数据的内存地址除以数据的大小,余数为0时说明数据是对齐的。现在的编译器编译时就考虑数据对齐的问题,生成exe文件后,数据基本上是对齐的,CPU运行时,寄存器有标志标识CPU是否能够自动对齐数据,如果遇到不能对齐的情况,或者通过两次访问内存,或者通知操作系统处理。

要注意的是,如果数据没有对齐,CPU处理的效率是很低的。

 

文件映射:系统不会将整个exe文件和所有的DLL文件装载进物理内存中,同时它也不会装载进页面文件中。相反,它会建立文件映射,也就是利用exe本身当作页面文件。系统将部分二进制代码装载进内存,分配页面给它。

        假设分配了一个页面,物理地址为0x0232 FFF1。其中装载的一个指令虚拟地址为0x4000 1001=0100 0000 00 0000 0000 01 0000 0000 0001。一个页面有4K,系统会将指令保存在低12位0x0001的地址处。同时,系统根据高10位0x0100找到页目项,如果没有关联的页表,系统会生成一个页表,分配一个物理页;然后,根据中10位0x0001找到表项,将物理地址0x0232 FFF1存进去。

 

执行过程:

执行时,当系统拿到一个虚拟地址,就根据页目和页表找到数据的地址,根据页目上的值可以判断页表是在页文件中还是在内存中;

如果在页文件中,会将页面导入内存,更新页目项。读取页表项的值后,可以判断数据页文件中还是在物理内存中;如果在页文件中,会导入到内存中,更新页表项。最终,拿到了数据。

        在分配物理页的过程中,系统会根据内存分配的状况适当淘汰暂时不用的页面,如果页面内容改变了(通过页表项的标志位),保存到页文件中,系统会维护内存与页文件的对应关系。

由于将exe文件当作内存映射文件,当需要改变数据,如更改全局变量的值时,利用Copy-On-Write的机制,重新生成页文件,将结果保存在这个页文件中,原来的页文件还是需要被其他进程实例使用的。

        在清楚了指令和数据是如何导入内存,如何找到它们的情况下,剩下的就是CPU不断的取指令、运行、保存数据的过程了,当进程结束后,系统会清空之前的各种结构、释放相关的物理内存和删除页文件。  

(二):内存状态查询

2.      内存状态查询函数

2.1系统信息

Windows 提供API可以查询系统内存的一些属性,有时候我们需要获取一些页面大小、分配粒度等属性,在分配内存时用的上。

请看以下C++程序:

SYSTEM_INFO sysInfo;

            GetSystemInfo(&sysInfo);

            cout<<"机器属性:"<<endl;

            cout<<"页大小="<<sysInfo.dwPageSize<<endl;

            cout<<"分配粒度="<<sysInfo.dwAllocationGranularity<<endl;

            cout<<"用户区最小值="<<sysInfo.lpMinimumApplicationAddress<<endl;

   cout<<"用户区最大值="

<<sysInfo.lpMaximumApplicationAddress<<endl<<endl;

结果如下:

wps_clip_image-9865

 

可以看出,页面大小是4K,区域分配粒度是64K,进程用户区是0x0001 0000~0x7FFE FFFF。

 

2.2内存状态

·        内存状态可以获取总内存和可用内存,包括页文件和物理内存。

请看以下C++程序:

MEMORYSTATUS memStatus;

            GlobalMemoryStatus(&memStatus);

            cout<<"内存初始状态:"<<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<<endl<<endl;

结果如下:

wps_clip_image-30233

可以看出,总物理内存是1G,可用物理内存是510兆,总页文件是2.5G,这个是包含物理内存的页文件;可用页文件是1.9G。这里还标识了总进程空间,还有可用的进程空间,程序只用了22兆的内存空间。这里说的都是大约数。

内存繁忙程序是标识当前系统内存管理的繁忙程序,从0到100,其实用处不大。

 

·        在函数里面静态分配一些内存后,看看究竟发生什么

char stat[65536];

            MEMORYSTATUS memStatus1;

            GlobalMemoryStatus(&memStatus1);

            cout<<"静态分配空间:"<<endl;

            printf("指针地址=%x ",stat);

cout<<"减少物理内存="<<memStatus.dwAvailPhys-memStatus1.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus.dwAvailPageFile-memStatus1.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="<<memStatus.dwAvailVirtual-             

memSta tus1.dwAvailVirtual<<endl<<endl;

结果如下:

wps_clip_image-28965

 

可以看出,物理内存、可用页文件和进程空间都没有损耗。因为局部变量是分配在线程堆栈里面的,每个线程系统都会建立一个默认1M大小的堆栈给线程函数调用使用。如果分配超过1M,就会出现堆栈溢出。

 

·        在函数里面动态分配300M的内存后,看看究竟发生什么

char *dynamic=new char[300*1024*1024];

            MEMORYSTATUS memStatus2;

            GlobalMemoryStatus(&memStatus2);

            cout<<"动态分配空间:"<<endl;

            printf("指针地址=%x ",dynamic);

cout<<"减少物理内存="<<memStatus.dwAvailPhys-memStatus2.dwAvailPhys<<endl;

cout<<"减少可用页文件="<<memStatus.dwAvailPageFile-memStatus2.dwAvailPageFile<<endl;

cout<<"减少可用进程空间="<<memStatus.dwAvailVirtual-memStatus2.dwAvailVirtual<<endl<<endl;

结果如下:

wps_clip_image-26367

 

动态分配情况下,系统分配直到内存页文件使用完为止,当然,系统要留一下系统使用的页面。

 

2.3 进程区域地址查询

在给定一个进程空间的地址后,可以查询它所在区域和相邻页面的状态,包括页面保护属性、存储器类型等。

·        C++静态分配了两次内存,一次是4K大一点,一个是900K左右。

char arrayA[4097];

            char arrayB[900000];

第一次查询:

            long len=sizeof(MEMORY_BASIC_INFORMATION);

            MEMORY_BASIC_INFORMATION mbiA;

            VirtualQuery(arrayA,&mbiA,len);

            cout<<"静态内存地址属性:"<<endl;

            cout<<"区域基地址="<<mbiA.AllocationBase<<endl;

            cout<<"区域邻近页面状态="<<mbiA.State<<endl;

            cout<<"区域保护属性="<<mbiA.AllocationProtect<<endl;

            cout<<"页面基地址="<<mbiA.BaseAddress<<endl;

            printf("arrayA指针地址=%x ",arrayA);

            cout<<"从页面基地址开始的大小="<<mbiA.RegionSize<<endl;

            cout<<"邻近页面物理存储器类型="<<mbiA.Type<<endl;

            cout<<"页面保护属性="<<mbiA.Protect<<endl<<endl;

第二次查询:

            MEMORY_BASIC_INFORMATION mbiB;

            VirtualQuery(arrayB,&mbiB,len);

            cout<<"静态内存地址属性:"<<endl;

            cout<<"区域基地址="<<mbiB.AllocationBase<<endl;

            cout<<"区域邻近页面状态="<<mbiB.State<<endl;

            cout<<"区域保护属性="<<mbiB.AllocationProtect<<endl;

            cout<<"页面基地址="<<mbiB.BaseAddress<<endl;

            printf("arrayB指针地址=%x ",arrayB);

            cout<<"从页面基地址开始的大小="<<mbiB.RegionSize<<endl;

            cout<<"邻近页面物理存储器类型="<<mbiB.Type<<endl;

   cout<<"页面保护属性="<<mbiB.Protect<<endl<<endl;

 

说明:区域基地址指的是给定地址所在的进程空间区域;

邻近页面状态指的是与给定地址所在页面状态相同页面的属性:MEM_FREE(空闲=65536)、MEM_RESERVE(保留=8192)和MEM_COMMIT(提交=4096)。

区域保护属性指的是区域初次被保留时被赋予的保护属性:PAGE_READONLY(2)、PAGE_READWRITE(4)、PAGE_WRITECOPY(8)和PAGE_EXECUTE_WRITECOPY(128)等等。

页面基地址指的是给定地址所在页面的基地址。

从页面基地址开始的区域页面的大小,指的是与给定地址所在页面状态、保护属性相同的页面。

邻近页面物理存储器类型指的是与给定地址所在页面相同的存储器类型,包括:MEM_PRIVATE(页文件=131072)、MEM_MAPPED(文件映射=262144)和MEM_IMAGE(exe映像=16777216)。

页面保护属性指的是页面被指定的保护属性,在区域保护属性指定后更新。

 

结果如下:

wps_clip_image-9190

 

如前所说,这是在堆栈区域0x0004 0000里分配的,后分配的地址arrayB反而更小,符合堆栈的特性。arrayA和arrayB它们处于不同的页面。页面都受页文件支持,并且区域都是提交的,是系统在线程创建时提交的。

 

·        C++动态分配了两次内存,一次是1K大一点,一个是64K左右。所以应该不会在一个区域。

char *dynamicA=new char[1024];

            char *dynamicB=new char[65467];

            VirtualQuery(dynamicA,&mbiA,len);

            cout<<"动态内存地址属性:"<<endl;

            cout<<"区域基地址="<<mbiA.AllocationBase<<endl;

            cout<<"区域邻近页面状态="<<mbiA.State<<endl;

            cout<<"区域保护属性="<<mbiA.AllocationProtect<<endl;

            cout<<"页面基地址="<<mbiA.BaseAddress<<endl;

            printf("dynamicA指针地址=%x ",dynamicA);

            cout<<"从页面基地址开始的大小="<<mbiA.RegionSize<<endl;

            cout<<"邻近页面物理存储器类型="<<mbiA.Type<<endl;

            cout<<"页面保护属性="<<mbiA.Protect<<endl<<endl;

 

            VirtualQuery(dynamicB,&mbiB,len);

            cout<<"动态内存地址属性:"<<endl;

            cout<<"区域基地址="<<mbiB.AllocationBase<<endl;

            cout<<"区域邻近页面状态="<<mbiB.State<<endl;

            cout<<"区域保护属性="<<mbiB.AllocationProtect<<endl;

            cout<<"页面基地址="<<mbiB.BaseAddress<<endl;

            printf("dynamicB指针地址=%x ",dynamicB);

            cout<<"从页面基地址开始的大小="<<mbiB.RegionSize<<endl;

            cout<<"邻近页面物理存储器类型="<<mbiB.Type<<endl;

            cout<<"页面保护属性="<<mbiB.Protect<<endl;

 

结果如下:

wps_clip_image-22398

 

这里是动态分配,dynamicA和dynamicB处于两个不同的区域;同样,页面都受页文件支持,并且区域

声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。