这里仅是对windows内存的简单介绍,适合编写windows应用程序的人阅读,主要参考《windows核心编程》及《深入解析windows操作系统》第四版。对windows内存管理的内部机制,将在以后加以介绍。
首先,用户用到的内存都是虚拟内存,windows内存管理器负责将虚拟地址转译成物理内存。对于32位机器,虚拟地址空间就是4G大小,用4个byte就可以覆盖,因此,32位机的指针大小就是4个字节。
在这4G的地址空间中,windows把它一分为二,高2G的地址空间属于操作系统(内核)使用,低2G归用户模式(即用户进程可以访问)。当然,可以用/3GB开关,将操作系统压缩到高1G,低3G归用户模式。通过这个划分,操作系统将自己与用户进程隔离,用户进程不能直接访问操作系统地址空间(高2G),从而将操作系统保护起来。另外,每个用户进程拥有自己的地址空间,即每个用户进程所使用的低2G空间,仅为自己可见。举个例子,进程a的地址0x00E39BA4所存放的数据,跟进程b的0x00E39BA4数据,不是同一个数据。这样,进程与进程隔离开,保证了进程的独立性。因此,我们可以这样理解,每个进程都拥有自己低2G的虚拟地址,高2G的地址空间归操作系统使用。假如一个进程运行失败,它既不会使操作系统瘫痪,也不会导致其他进程无法运行,操作系统简单的把这个用户进程杀死即可。关于操作系统地址空间如何划分和使用,以及用户进程间如何通信,将在以后的文章中给出。
后面,我将着重介绍用户进程的虚拟地址空间的分布以及使用,然后,再向大家介绍下编程常用的“堆栈”。
以win2000为例,如下图
从低位向高位看,首先是NULL指针分配的分区。编程中,为了防止出现野指针,我们把该指针赋为NULL,就是让指针指向这个区域。如果线程试图根据指针来读取或写入该区域,就会引发一个访问违规。这个分区非常有用,它有助于我们发现程序中的错误。
然后是DOS/16位Windows应用程序兼容分区,这个我们暂切不必考虑。
然后是用户方式分区,这是我们用的最多的一块区域。用户进程的代码、数据均放在该区域中。
再后面是禁止进入区,此区用于将用户区与内核区隔离。
最后就属于内核区了,本文暂不介绍。
在最初,我们认为整个2G空间都是空的。当进程加载并创建成功的时候,有一部分虚拟内存空间已经被使用了,还有一部分是空的。那么,我们可以通过WindowsApi来申请和使用这些空闲区域。在使用这块内存前,需要有两个操作,一个称作保留,一个称作提交。先介绍下保留,保留的意思,就是说,这块内存区域已经有人要了,但是,这块内存区域到底有没有映射的存储器呢?如果你初次保留,是没有被映射的。提交,就是将物理存储器映射到内存地址空间。举个例子,某市刚新建了一个1000米的路段,路段两旁已经建了一些建筑,但还有很大一部分是空的,现在需要规划、建设一些新项目,这些项目的生杀大权归建设局局长管。包工头a跟局长说,“我要在30米处建一座大楼,长10米”,局长说,“好,我划给你”,然后在笔记本上记录了下来。这块土地已经划给包工头a,但是包工头a还没有拿到合同证书,没有进入实质性阶段,所以还不能直接使用这块土地。这个过程就是保留。后来包工头拿到合同证书,正式拥有了这块土地,这就是提交。应该注意的是,物理存储器指的并不是物理内存,而是物理内存跟页文件(用于虚拟内存的硬盘空间)。保留内存区域,用VirtualAlloc;显式提交,也用VirtualAlloc,只是有所参数不同。在使用完后可以用VirtualFree来释放该内存区域。
到这里,我们已经知道如何直接申请和释放虚拟内存区域了。其实,在写程序时,我们用到最多的是堆栈了,下面将就这部分内容展开讨论。
堆栈包括两部分,堆和栈。每个进程都至少会有一个堆,在创建进程的时候已经建立好了,堆是进程所有的,也就是说,进程的所有线程会共用一个堆,当然,你也可以自己创建辅助堆。栈呢,是线程所有,每创建一个线程,系统就会为这个线程保留一个栈。现在我们分别来讨论。
首先,介绍下栈。创建“线程”时,系统已经为栈“保留”一段内存区域,win2000中栈的默认大小是1M,通常,栈会放在较低的虚拟地址上,比如0x080XXXX。栈的使用,是从高位向低位分配的,比如栈的区域为0x08000000-0x080FF000,那么最先申请的局部变量放在0x080FF000的位置,然后依次往低处放,直到0x08000000处。0x08000000处是一个守护页面,如果访问该页面,将引发一个异常,即栈溢出所至。另外,栈中还有一个带保护属性的页面,该页面是栈中已分配(提交)内存的最后一个页面。栈中存放函数的局部变量,当函数退出时,栈会退,也就是那个保护属性页面会往回退,那么,存放原先函数局部变量的内存页面已经无效,不能被访问了。因此,局部变量不用写程序来显式释放。
下面再说说进程的堆。当创建“进程”时,系统会“保留”一段地址空间归堆使用,win2000中堆默认大小是1M。堆是由堆管理器来维护的,当我们用new或malloc向堆管理器发出请求时,堆管理器会从堆中分出一块内存区域并返回。刚才说了,堆默认初始大小是1M,那么当我们从堆中申请的内存超过了1M,堆管理器会怎样处理呢。它会通过调用VirtualAlloc,来向内存管理器申请虚拟内存。另外,堆是所有线程共用的,当写一个单线程程序时,不会有什么问题,如果是多线程,那么就存在一个线程同步的问题。在用vc进行编译的时候,如果选用多线程运行期库,那么,我们所调用的new或malloc就是一个加锁的方法,这样就能安全正确的使用堆。new完之后我们就可以使用这块内存了,当不再使用时,我们必须通过delete或free来显式释放它,否则,直到进程结束前,这块内存会一直存在。
至此,本文的主要内容,虚拟地址空间,堆栈,就已经介绍完了。
可能有人会有这样的疑问,用VirtualAlloc和VirtualFree可以申请和释放虚拟内存,我们凭什么使用new和delete或malloc和free呢?为了解答这个问题,先介绍下地址空间的内存页面分配粒度。迄今为止,windows环境下,其分配粒度大小均为64k。那么,我们可以把整个虚拟内存空间看作是由一个个以64k为边界的64k大小的内存页面组成。如果用VirtualAlloc来申请内存,不管申请多大,内存管理器都会把整张整张的页面给你,即使你只申请一个字节的内存,内存管理器也会把一个64k大小的未用的页面返回给你。这样势必会造成内存资源的浪费。而调用new来申请堆中的空间,就不会出现这种情况。堆就是一个内存池,微软已经对堆管理器分配堆内存的策略做了高度的优化。当我们调用new或malloc时,堆管理器会从堆中找出一块恰当的内存返回给我们。因此,还是建议大家使用new来申请内存。