多级页表
首先我们讲讲2级页表,然后通过2级页表延伸到多级页表,现假设有16KB(214)的地址空间,并且每页大小为64(26)字节,每个PTE为4字节,那么说明页表为1KB(4 * 214 / 26),若我们有64字节的页,那么将1KB可划分为16个64字节的页面,每个页面可容纳16个PTE
前面我们讲解到虚拟地址划分为虚拟页号(VPN)和虚拟页偏移量(VPO),但虚拟页偏移量已经固定,那么我们只能从VPN下手将其作为索引用于索引页表目录,那么我们该如何利用VPN来构建各个部分的索引呢?
我们首先需要构建页表目录,根据上述假设,总共有256个PTE分布在16页上,页目录在页表的每页上需要一个条目,因此它具有16个条目, 最终我们需要VPN中的4位索引到目录中,这也就意味着我们需要使用VPN的前4位,如下所示:
我们从VPN提取出了页表目录索引(PDI),那么我们就可以计算出每个PDE(Page Directory Entry)的地址值:
PDEAddress = PageDirectoryBase + (PDIndex * sizeof(PDE))。
从页面目录获得的页面帧号(PFN)必须先左移到适当位置,然后再与页表索引组合以形成PTE的地址。假设如下为二级页表扁平化的片段
如上第1片段为页表目录,在其中存在索引到第2级页表的索引,还包括有效位,第2和第3片段分别为第1级页表目录索引对应的页表(其中包含保护位,可读?可写?等等)
假设CPU产生虚拟地址(0xFE16 = 25410 = 111111102),由于我们假设虚拟地址空间为14位,所以将转换后的2进制不足用0填充即11111110000000,同时我们将地址空间进行虚拟页号(VPN)和虚拟页偏移量(VPO)划分
然后对VPN划分为页表目录和页表索引,我们通过红色、绿色、蓝色由左至右分别代表页表目录索引、页表索引、虚拟页偏移量即1111 1110 000000,经过如此划分后,此时前4位(11112 = 1510)为页表目录索引,对应上述页表目录最后一行,此时页表目录帧号为101对应第2个页表片段,然后根据接下来的4位(11102 = 1410),最终得到索引为倒数第2行,即最终物理页帧号为55。最后我们通过如下物理地址计算公式
PhysAddress = (PTE.PFN << SHIFT) + offset
即最终物理地址为:55 * 26 + 000000 = 352010 = 0XDC016。假设为32位地址空间,那么页目录索引、页表索引、虚拟页偏移量分别对应为10、10、12位,那么对应的2级页表将是如下形式
简而言之,对于32位地址空间,会将VPN中的前10位(位22..31)用于索引页表目录,紧接下来的10位(12 ..21)用于索引所选的页表。换言之,对于2级页表结构其本质是:VPN的前m位为页表目录索引,而接下来的n位为页表索引,同时需要注意的是2级页表其地址是从上往下增加。
根据上述将32位地址空间中的页表以2级结构划分,此时第1级页表大小为(1024 * 4) = 4KB,而第2级页表为(1024 * 1024 * 4) = 4MB,所以页表大小将为4KB + 4MB,这么算来比直接使用单级页表结构为4MB情况更糟糕了不是吗,其实情况并不是这样,如上算出的4KB + 4MB为最极限的情况,上述已经讲解过只有经常需要用到的2级页表才缓存在主存中,所以实际情况下页表大小会小于4MB。
早期操作系统采用的是2级页表结构,但是现如今大多数操作系统采用多级页表结构,就像树一样,不过是深度或层次更深了而已。
假设我们有一个30位虚拟地址空间和一个较小的页面(512字节),因此,我们的虚拟地址具有21位的虚拟页号和9位的偏移量,使页表的每个部分都适合单个页面是构建多级页表的目标
但到目前为止,我们仅考虑了页表本身,如果页表目录很大,那该怎么办?
为了确定一个多级页表中需要多少级才能使页表的所有部分用一个页面容纳,我们首先确定一个页面中可以容纳多少个PTE。我们假设给定的页面大小为512字节,并假设PTE大小为4字节,我们知道在单个页面上可容纳128个PTE。
当我们索引到页表的页面时,可以得出结论,我们需要使用VPN的最低有效7位(log2128)作为索引
通过确定单页面需要容纳128个PTE,那么将占据地址空间7位,那么还剩下14位地址空间,如果将剩下的214作为页表目录, 那么将横跨128页而不再是1页,那么对于构建多级页表的目标将无法实现
为了解决这个问题,我们需要将14位进行再次划分,将页表目录进行设置为多页,页表目录位于上方从而指向另一页表目录,因此我们可以进行如下划分
现在,在索引上层页表目录时,我们使用虚拟地址的最高位(图中PD Index:0),该索引可用于从顶级页表目录中获取页表目录的条目,如果有效,则对来自自顶层PDE和VPN的下一部分(PD Index:1)的物理帧号组合来查询页表目录的第二层,最后,如果有效,则为PTE地址通过将页表索引与第二级PDE中的地址结合使用,可以形成一个地址。
当然这个过程需要做很多工作,所有这些都是为了在多级表中查找物理页帧号。最终多级页表结构如下这般
上述我们讲过若为64位地址空间,4KB的页面(212)且每个PTE为4字节,在单级页表情况下,那么页表大小为16TB(4 * 264 / 212)= 16TB,若我们划分为3级,如下:
那么对于上述外部页即页目录索引将需要占内存4 * 232 = 16GB,所以我们仍需继续划分层级,但是每个层级都有一个额外的间接方式,因此会产生额外的开销。
比如64位地址空间在4KB页面上将使用大地址空间,所以多级页表成为具有小页的大地址空间的内存消耗。
哈希页表
处理大于32位地址空间常用的方法是使用哈希页表(使用稀疏的地址空间),采用虚拟页码作为哈希值,对于每一个PTE使用链表结构存储从而解决冲突或碰撞
每个元素由三个字段组成:虚拟页码、映射的页帧、指向链表内下一个元素的指针。
通过哈希算法将虚拟页码映射到哈希页表,然后将虚拟页码与链表第一个元素的第一个字段进行比较,若匹配则将第二个字段用来形成物理地址,否则遍历链表查找对应匹配项。哈希页表如下图所示
虽然通过哈希页表查找很快,同时采用如上标记的链表数据结构解决冲突问题,虽说消除了条目在内存中连续的需求,但是仍然以更高的内存开销进行存储即消耗更多内存,特别是如果页表是完整的,并且具有有效/无效位以使未使用的条目无效,那么哈希页表不再那么适用,此时我们采用其他方案,如下倒置页表。
倒置页表
通过前面内容学习我们知道对于每个进程都有一个关联的页表,该进程中的每一个虚拟页都在页表中对应一项,不管是否有效,进程通过虚拟地址引用页,操作系统通过计算虚拟地址在页表中的位置即PTE
但这种方式有明显的缺点,如上我们也叙述过,每个页表可能包含数以百万计的条目,如此一来,页表将占用大量的物理内存以跟踪其他物理内存是如何使用的,为解决这个问题,我们可以使用倒置页表(inverted page table),对于每个真正的内存页,倒置页表才有一个条目,每个条目包含保存在真正内存位置上的页的虚拟地址,以及拥有该页进程的信息
因此,整个系统中所有进程将只有一个页表,并且每个物理内存的页只有一个相应的条目,换言之,与知道每个进程的虚拟页在哪里相反,现在我们知道拥有哪个物理页的进程与它对应的虚拟页。
IBM是最早采用倒置页表的公司,从IBM System 38、RS/6000、到现代的IBM Power CPU。
对于IBM RT,系统的虚拟地址包含三部分:进程Id、页码、页偏移量,每个倒置页表条目包含两部分:进程Id、页码,这里的进程Id作为地址空间的标识符,当发生内存引用时,由进程Id和页码组成的虚拟地址被提交到内存子系统,然后搜索倒置页表来寻址匹配,如果找到匹配条目,则生成物理地址,如果未找到匹配条目则为非法地址访问。倒置页表结构如下:
虽然倒置页表减少了存储每个页表所需的内存空间,但是它增加了由于引用页而查找页表所需要的时间,由于倒置页表是按照物理地址排序,而查找则是根据虚拟地址,因此查找匹配可能需要搜索整个表,这种搜索需要耗费很长时间
为解决这个问题,可以使用一个哈希表结构,从而将搜索限制在一个或最多数个页表条目,当然,每访问哈希表就增加了一次内存引用,因此每次虚拟地址的引用至少需要两个内存读,一个用于哈希表条目,另一个用于页表条目即PTE,同时结合前面所学,在搜索哈希表之前,肯定先搜索TLB,这样可大大提高性能。
对于倒置页表还会带来一个问题,那就是实现共享内存,共享内存需要将多个虚拟地址映射到同一物理地址,很显然,这种标准的方式无法应用于倒置页表,因为每一个物理页只有一个虚拟页条目,一个物理页不可能有两个或多个共享的虚拟地址
为解决这个问题,只能允许页表包含一个虚拟地址到共享物理地址的映射,这也就意味着,对于未映射的虚拟地址的引用势必会导致页错误。
本节我们非常详细的讨论了多级页表结构、对于哈希页表和倒置页表数据结构通过看图理解起来非常简单
从本节内容我们可总结出:对应页表结构可以拥有良好的时间复杂度或空间复杂度,但不能同时兼得。
到此关于虚拟内存重要内容基本上都已囊括,若有遗漏,后续我会继续进行补充。接下来我们将进入内存管理分页和分段的学习,讲完之后,会陆续进入到程序的执行、进程、死锁、并发等,相信大家会比较感兴趣。