学过操作系统的都知道,在操作系统中存在一个虚拟内存的概念,它用于内存的管理,使得应用程序认为它有一段连续的内存,大大地简化了程序员码代码的难度。程序员只用关注在这个连续的虚拟内存段中怎么使用内存,不用关心在物理内存中到底用那一段内存,进程运行的时候操作系统会自动进行映射。操作系统是怎么做到的呢?实际上操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。
那么什么样的数据结构能够表示这个映射关系呢?最容易想到的方法就是用数组保存,虚拟空间中的每一个页都分配一个数组项。该数组指向与之关联的页帧, 但这会引发一个问题, 例如, IA-32体系结构使用4KB大小的页,在虚拟地址空间为4GB的前提下,则需要包含1048576
(4G/4K=1024*1024=1048576)项的页表,要寻址4G的空间,需要32位,也就是4Bytes, 1048576项需要4M(4 * 1024 * 1024 Bytes=4M)的内存.这个问题在64位体系结构下, 情况会更加糟糕. 而每个进程都需要自身的页表,这会导致系统中大量的内存都用来保存页表.
为了减少页表占用空间,我们可以考虑一个实际情况,对于一个进程的虚拟空间,其实大部分的内存都是没有使用的,因此在页表中其实是不需要这部分的映射的,因此前辈们设计出了多级页表的结构。
那么到底多少级才是最合适的呢?
页表级数的选择和发展
多级页表的设计思想其实就是现实生活中分组的概念,举个例子,我们研究中心有1000个人,现在秘书准备组织一次春游活动,需要统计一下有多少人去春游,如果秘书每个人都问一下记录一下每个人的信息,那么就需要1000行去记录这个信息,但是这样效率就很低而且浪费空间,所以秘书就让每个实验室单独统计,秘书只用要记录每个实验室去春游的人数以及负责人的联系方式即可,假设有五个实验室,则只需要五行即可,然后每个实验室的负责人统计一下自己实验室需要去的人员信息,那么这样效率就比较高了,而且因为只是记录了那些去春游的的人员信息,因此这样既提高了效率,也大大的也省了存储空间,当然你可能回发现一个小问题,如果10000个人都去那么实际上需要10005行统计信息,但是考虑到一些可能也是经常发生的情况,比如我们软件系统实验室春游那天需要加班赶项目,那么实际上我们实验室负责人就不用进行记录了,秘书那里直接记录软件实验室0个人去,甚至不记录也可以,所以一般情况下这样是可以提高效率,并且能够节省空间的。分级的本质其实也是将页表项进行分组管理,考虑到进程内存大部分空闲以及局部性,因此实际记录的时候大部分的记录是没有的,所以相比上面提到的直接使用一个数组(也就是一级页表)大大地节省了存储需要的空间。
我们再来算一笔帐,从Intel 80386开始,默认的页大小是4k, 提供32位的寻址空间。一页4k需要12位进行寻址,因此对于32的寻址空间,还有32-12=20位可以用来页表的索引。因为页表项存储的是物理内存一页的物理地址,因此一个页表项只能指向一页,而硬件的实现是使用CR3寄存器保存了页表的起始地址,因此对于页表我们应该使其只占用一页的内存。4k能够存储的页表项个数为4k/4B=1024, 寻址1024个索引只需要10位即可,因此在这个前提下,用于页表索引的20位自然而然的就分为了两段,也就是页表被分成了两级,十位用来表示一级页表内部偏移,十位用来表示二级页表的内部偏移,一级页表的页表项存储二级页表的物理地址。其实这也是Linux最初采用的两级页表分页机制。
Linux的两级分页机制
两级分页机制将32位的虚拟空间分成三段,对就是三段,低十二位表示页内偏移,高20分成两段分别表示两级页表的偏移。
- PGD(Page Global Directory): 最高10位,全局页目录表索引
- PTE(Page Table Entry):中间10位,页表入口索引
在进行地址转换时,结合CR3寄存器中存放的全局页表物理地址,然后加上虚拟地址的高10位也就是PGD段,作为页表偏移找到相应的页表项,从改页表项得到二级页表的物理地址,然后结合中间十位即PTE段,作为偏移找到PTE项,即获得了要访问的内存地址所在的物理页,然后结合Page Offset即定位到了需要访问的物理内存,这样就完成了从虚拟地址到物理地址的转换。如果没有缓存的情况下,整个过程需要三次内存访问。
Linux的三级页表
Intel通过在处理器上把管脚数从32增加到36,以提高处理器的寻址能力,使其达到236=64GB236=64GB,但是linux依然使用32位的虚拟寻址空间,为此,需引入一种新的分页机制。
如果页的大小依然是4k, 64G(234234)的内存可以分为224224个页框,然后页表项的物理地址字段从20位扩展到24位,每个页表项必须包含12个标志位(固定)和24个物理地址位(36-12),共36位,因此,每个页表项须从32位扩展到64位(36位>32位,考虑到对齐,因此应将页表项扩大一倍到64位)。所以原来32位的页表项现在变成了64位后,一页可以存储的页表项从1024变成了512,需要9位进行寻址,高一级的页表也需要9位,加上页内偏移12位,现在32位的地址还剩余2位,因此又引入一级页表,该页表只含有4个页表项。这样就解决了PAE情况下的分页。
下图是PAE情况下的三级页表情况,图片来自维基百科
注意(个人观点):虽然页表项扩展到了64位,选址达到了36位,即64G的空间,但是实际上一个进程可以使用的虚拟空间大小仍然是4G,因为虚拟地址依然只有32位,即使每个页表项都填满也只能有4G的空间。唯一不同的是物理内存进行分配时候就可以使用超过4G的空间了。
Linux的四级页表
随着硬件的发展,64位的CPU出现了,这个时候基于32位处理器的三级页表又不能使用了,不过现在的硬件已经可以支持四级页表了,可以使用48位的虚拟空间了(虚拟空间的位数和硬件的对应关系理的还不是很清楚),经过前辈们的争论,最后确定了下面的四级页表形式:
从图中可以看出,64位的虚拟地址只使用了低48位,每一级的页表项都有64位,其中低12位作为标志位,然后紧接着的40位作为物理寻址使用,加上页内偏移一共52位用于物理地址,但是实际上CPU的引脚并没有52位,一般是40位,46位等。比如我们实验室使用的Intel Xeon E5645 就是40位的物理地址,所以52位只使用了40位(240=1T240=1T)。
下面是IA32-e模式下的线性地址映射
参考资料
[1] Linux分页机制之概述
[2] 物理地址扩展(PAE)的分页机制
[3] 物理地址扩展
[4] 《深入理解计算机系统》第九章:虚拟存储器
[5] 《Linux内核设计的艺术》第六章:用户进程与内存管理