Windows内存管理
本博文很大程度上参考了,潘爱民先生的《Windows内核原理与实现》一书,在此对他表示感谢。
记得是在学C语言指针的时候,首次比较实际的使用内存寻址。也是在那个时候知道不能使用未初始化的指针,记得当时老师还说过,如果使用了未初始化的指针,轻则运行错误,重则操作系统崩溃。现在看起来那个重则系统崩溃还是比较可笑的,如果真的这么容易就让系统崩溃,那么Windows早就被用户抛弃了。而且我在调程序的时候,如果出现指针解引用错误,基本都是让系统直接终止掉我的程序,Windows一向安然无恙。当然,也许老师指的是DOS环境,不过我在dos下只写过汇编代码。
我在学C语言之后的较长一段时间,都天真的认为我的程序在运行时候是直接使用物理内存的。那个时候我做过这样的尝试:访问地址为0x0的内存值。结果可想而知。那个时候我只知道那块内存系统禁止访问,觉得也许是因为操作系统在时刻监视着所有进程代码的原因吧。再后来写C程序输出地址的时候,发现每次程序变量的内存地址都差不多,开始有点怀疑Windows是怎么让我的程序跑起来的。因为我觉得每次程序运行的变量地址应该有较大的偏差才是。而实际结果却是,不论我计算机运行程序的数量多少,当前物理内存的使用量是多少,地址范围始终都差不多,而且地址的位置比较低,好像二进制的高8位一直都是0。再后来看Windows的时候,才知道,我在程序中并非直接使用物理地址,而是一个虚拟地址。
所以,我找了本Windows内核的书好好看了看,并写了这篇博文。
因为内存管理的优劣会直接影响到系统本身的性能,所以操作系统管理内存的方法一般要根据处理器的硬件情况来决定。Windows的主要硬件体系结构就是Intel的架构,所以在内存管理上就受很多的x86架构的影响。另外为了能让多个进程之间不互相干扰破坏,并与操作系统隔离,Windows在处理器寻址的基础上,又应用了大量的内存管理技术来满足各种要求。在所有进程对于内存的要求已经超过了实际物理内存的时候,Windows还必须要平衡各种需求,借助如硬盘的外部存储,使计算机能够正常运行。由此可见,内存管理是所有操作系统非常重要的一部分。
首先,在x86体系中,内存地址有三类:
1 物理地址(直接对应物理内存的地址,一般为32位或36位的无符号整数)
2 虚拟地址(也称作线性地址,处理器使用前需要转换为物理地址,为32位无符号整数,因此地址空间范围为4GB)
3 逻辑地址(学过8086汇编的人都知道,逻辑地址分为两个部分:段地址和偏移地址,这里也是一样的)
然后再介绍一下两种比较主流的内存管理方式:页式内存管理,段式内存管理。
1 页式内存管理
如果直接让进程使用物理地址来访问内存将会使进程的动态内存分配难以有效实施,因为内存单元和进程紧密的联系在了一起,从而使内存的回收和再分配受限于特定的进程和物理地址。一种简单的思路就是让进城使用虚拟地址,在虚拟地址和物理地址之间建立一个映射表来完成转译。
页式内存管理中,虚拟地址空间是按照页来管理的,对应的物理内存同样是按照页来管理,二者的页大小是相同的。而因为这样的一个转译关系存在,在虚拟地址中连续的页面在物理地址中可以不连续。通过维护这样的一个转译关系,物理页面可以被动态的分配给虚拟页面,这样可以做到当虚拟页面真正被使用的时候才为其分配物理页面,这样的好处就是能够节省物理内存,使用效率更高。
另外需要注意的是,在一个系统中,物理地址空间只有一个,而虚拟内存空间可以有多个。而且每个虚拟内存空间都必须要维护一个映射关系,这样每个进程的地址空间都是独立的。进程A地址为0x00FF0000对应的物理地址和进程B的0x00FF0000地址对应的物理地址就不相同。另外,每个虚拟地址空间实际映射的物理页面很少。而物理页面一般只被映射到一个虚拟地址空间中。当然存在例外,比如DLL在被多个进程使用的时候,其函数所在的物理页面必然被映射到了多个进程的虚拟地址空间中。
在页面划分机制下,32位的虚拟地址被分为了两个部分:页索引+页内偏移。Intel x86下的页面大小标准为4KB,也就是2^12。因此,虚拟地址中的低12位可以用于页内偏移。前20位可以用于页索引部分,用于找到一个实际的物理页面。这样的页面映射表大小就是1M。若用线性关系表示的话,那么就需要用4M的内存,因为一个地址的大小是4Byte。不过这种方式浪费比较严重,毕竟Windows默认线程栈的大小也不过才1M。
另外在这1M的地址空间中,还有很大一部分的表项并未使用,浪费很严重。
所以Intel x86的虚拟地址解析过程如下:
32位的虚拟地址被分为了3个部分:31 - 22 页目录索引 21 - 12 页表索引 11- 0 页内偏移
1 对于一个虚拟地址,首先解析页目录索引,在也目录中查找。页目录的大小就是4KB。页目录索引的首地址位于CR3寄存器中。
2 在目录中找到后,再根据页表索引来找到页对应的物理地址。
3 最后加上页内偏移,就定位到了最终的物理地址。
可以知道,这样下来需要使用的空间达到了4KB+4MB的大小,但是这4MB的页表索引在对应页表不使用的时候完全可以不构造出来,从而节省大量的内存。
同时,因为二级页表的查找也需要付出一定性能上的代价,毕竟多了一次查找。对于这个问题,intel x86处理器缓存了地址转译信息,也就是虚拟地址到物理地址的映射关系。这样当处理器重复访问一个虚拟地址时,就不用再次进行转译了。此缓存被称为TLB,中文意思就是:地址转译快查缓冲区。
TLB是硬件级的地址查找支持电路,专门用来做这个,效率非常高。也因此,这一块是软件所无法触及的,知道优惠这么个东西就OK了。因为TLB的存在,二级查找引发的性能下降就不是很明显了。
另外当CR3寄存器的值改变的时候,TLB中的数据就全部无效了,因为这意味着进程的切换,之前的映射关系自然就不再成立了。
另外Intel x86 Pentium Pro还提供了成为PAE物理地址扩展的内存映射模式,支持36位物理地址,但虚拟地址仍旧是32位。而且PAE采用的是3级页表机制。但是基本原理一样。
段式内存管理还看得不是很清楚,看明白了再把整理篇博文发出来。