相信很多人都知道Windows页表自映射一说,也晓得Linux内核的一一线性映射。然而很多人也仅仅就是知道而已,记住一个结论比理解一个原因要简单得多。
上周末,有人极具挑衅态度的问我能否分别用一句话描述它们,我承认我不是布道者,也难以说出”道可道,非常道“的玄语,但我十分赞同老子的观点,能说出来的道就不是大道,虽难以说出,但却可以解释,说到解释,那就是越详细越好了,冗长并不总是贬义词。在这样的心理慰藉下,才会有以下的文字,虽已深更,却不无聊...
本文基于32位Intel体系结构讨论!怕打字跟不上思维,有些地方只好缺失严谨性,比如在应该写下”1G内存或者2G内存(后者处在2G/2G模式下)“的时候,我会直接写”1G内存“,但是并不总是这样。
1.虚拟地址空间概述
现代操作系统上,物理内存不再对程序可见。也就是说,程序指令本身以及其访问的任何数据都处在虚拟地址空间,机器通过一个叫做MMU的机构将其映射为真实的物理内存页面。
程序直接访问的地址为虚拟地址,访问地址(也包含取指等)会触发MMU工作,MMU自动将访问的地址映射到真实的物理地址,如果没有分配物理页面,将会触发缺页异常,系统捕获该异常,之后默默地分配一个页面,重新发起由于没有分配页面而失败的访问,所有这一切都是自动且默默地发生的,对应用程序是完全透明的。页面调度这个机制完美地迎合了程序访问的局部性原则。
虚拟地址填充整个32位地址空间,为了管理的高效性,很多的系统将这32位的地址空间拆分成了两个部分,即用户空间和内核空间。但是记住,这个拆分并不是必须的!所谓的内核空间和用户空间在Intel体系上表现为特权环0和特权环3。根本上的意义是,一个任务有一个满32位的地址空间,如果某个系统将进程32位的地址空间拆成了两个部分,那么则说明该任务进程本身包含内核特权环0的部分,如果没有拆分,那么就说明该任务进程没有内核部分。Remenber,一个满32位的地址空间使用一套MMU页表!
如果一个进程没有内核部分,当系统中断,系统异常,或者该进程本身调用系统调用的时候,怎么办呢?不要被现有的Linux,Windows的实现迷惑了,再次声明,拆分地址空间并不是必须的!如果在没有拆分地址空间的情况下出现上述情况,很简单,切换MMU页表即可,也就是说,系统单独维护一个满32位的内核地址空间为所有的满32位地址空间的进程服务!
说了这么多,该来点实例了,我们熟知的Linux,Windows系统,可以支持3G/1G模式,意即满32位的进程地址空间中,用户态占3G,内核态占1G;可以是2G/2G模式,解释同上,这些都是拆分地址空间的情况,这些情况在进入内核态的时候叫做陷入内核,因为即使进入了内核态,还处在同一个地址空间中,并不切换CR3寄存器。还有一种模式是4G/4G模式,即不拆分地址空间的情况,内核单独占有一个4G的地址空间,所有的用户进程独享自己的4G地址空间,这种模式下,在进入内核态的时候,叫做切换到内核,因为需要切换CR3寄存器(切换MMU页表),所以进入了不同的地址空间!
说到这里,应该知道为何文档上说4G/4G模式虽然解放了内核地址空间,使其可以容纳更多的管理机构,然而会付出一点小的代价了吧,所谓的代价就是切换CR3以及所有因此而引发的副作用!
2.Windows地址空间
一直我都以为,一个好的开始会带来令人愉快的结果,一个不好的开始会让人很累!确实是这样。很多人想理解Windows页表自映射,然后去google,去百度,得到的结果几乎都是在解释以下这个宏定义:
#define MiGetVirtualAddressMappedByPte(PTE) ((PVOID)((ULONG)(PTE) << 10))
于是很多人都在纠结于那个魔术字10,画了N个图,但是基本都是在抄袭Dave Probert很久前写的一篇文章《Windows Kernel Internals II Processes, Threads, VirtualMemory》。关键是最终还是没有讲明白。本来是一个很简单的事情,被却无端复杂化了。我觉得就是没有找到一个好的开始。什么是好的开始呢?
我认为我找到了,那就是WIndows进程虚拟地址空间的布局!如果这个布局设计的原则你搞明白了,那些宏你自己也能写出来了!不管怎么说,在看图之前,还是要先说一下Windows地址空间设计的原则,那就是:每个进程拥有自己单独的满32位地址空间!不管是3G/1G模式,还是2G/2G模式,还是4G/4G模式,每个进程都是独立的虚拟地址空间,这也是现代操作系统的设计原则,并非Windows独创。在这些单独的地址空间中,所有进程拥有相同的映射规则,比如虚拟地址XX不管在进程A还是在进程B,映射的都是自己的进程控制块PCB...如下图所示:
其实知道了这个,悟性好的同学可能已经知道自映射的设计细节了,但是我还是继续下去吧,以免让人家说我虎头蛇尾。
页表自映射,一个神奇的映射方式,为什么呢?它可不仅仅是为了节省4K的内存空间,虽然它确实可以节省4K的内存空间。最要紧的是,页表自映射机制提供了一套内核空间直接访问任意页面的高效方式。在讲页表自映射前,我先说一下WIndows的线性映射机制,即“页表项虚拟地址和进程地址空间虚地址页面的线性映射关系”,简称页表的线性映射。(注意和下一节中要讲的Linux的内核虚拟地址和物理地址的线性映射相区分)
Windows页表的线性映射理解起来很简单。Windows的所有页表处在地址空间的固定部分且按照虚拟地址连续分布,那么所有的页表项的虚拟地址也是连续分布,从最开始处,连续的页表项负责连续的虚拟地址的映射,如下图所示:
注意,直到现在,我都没有提到页目录,因为页目录纯粹是为了多级页表而引入的,Windows只是借助了页目录的概念,无形中用将页表映射到虚拟地址空间而取消了页目录带来的4K开销。Windows只是在地址空间的固定位置开始连续映射所有的页表,这些页表当中存在一个页表的页表,即页目录,页目录就这样湮没在页表中了。且往下看!
有了这个基础做依托,后面的自映射以及神奇的宏就是一个自然而然的结果了。为何这么说呢?分别来说。