(MIT6.S081)页表

(MIT6.S081)页表

页表是在硬件中通过处理器和内存管理单元(Memory Management Unit)实现。

对于任何一条带有地址的指令,其中的地址应该认为是虚拟内存地址而不是物理地址。假设寄存器a0中是地址0x1000,那么这是一个虚拟内存地址。虚拟内存地址会被转到内存管理单元(MMU,Memory Management Unit),内存管理单元会将虚拟地址翻译成物理地址。之后这个物理地址会被用来索引物理内存,并从物理内存加载,或者向物理内存存储数据。

(MIT6.S081)页表_第1张图片

从CPU的角度来说,一旦MMU打开了,它执行的每条指令中的地址都是虚拟内存地址。

为了能够完成虚拟内存地址到物理内存地址的翻译,MMU会有一个表单,表单中,一边是虚拟内存地址,另一边是物理内存地址。举个例子,虚拟内存地址0x1000对应了一个我随口说的物理内存地址0xFFF0。这样的表单可以非常灵活。

通常来说,内存地址对应关系的表单也保存在内存中。所以CPU中需要有一些寄存器用来存放表单在物理内存中的地址。现在,在内存的某个位置保存了地址关系表单,我们假设这个位置的物理内存地址是0x10。那么在RISC-V上一个叫做SATP的寄存器会保存地址0x10。

这样,CPU就可以告诉MMU,可以从哪找到将虚拟内存地址翻译成物理内存地址的表单。

MMU并不会保存page table,它只会从内存中读取page table。

这里的基本想法是每个应用程序都有自己独立的表单,并且这个表单定义了应用程序的地址空间。所以当操作系统将CPU从一个应用程序切换到另一个应用程序时,同时也需要切换SATP寄存器中的内容,从而指向新的进程保存在物理内存中的地址对应表单。这样的话,cat程序和Shell程序中相同的虚拟内存地址,就可以翻译到不同的物理内存地址,因为每个应用程序都有属于自己的不同的地址对应表单。

内核会写SATP寄存器,写SATP寄存器是一条特殊权限指令。所以,用户应用程序不能通过更新这个寄存器来更换一个地址对应表单,否则的话就会破坏隔离性。所以,只有运行在kernel mode的代码可以更新这个寄存器。

对于一个64bit的寄存器,可以表示264个地址,如果单纯以地址为单位来管理,表单会变得十分巨大,所有的内存都会在表单这里被耗尽,所以实际情况不可能是一个虚拟内存地址对于page table中的一个条目。

(MIT6.S081)页表_第2张图片

可以把虚拟地址拆分为三个部分,EXT没被使用,Index用来找到Page中的位置,Offset直接对应物理地址当中的后12位,Index经过MMU从27bit转换为44bit的物理地址,对于物理地址的前44位。

假设offset是12,那么page中的第12个字节被使用了。将offset加上page的起始地址,就可以得到物理内存地址。

Ofset必须是12bit,为什么呢?因为212对应了一个page----4kb

物理内存地址是56bit,其中44bit是物理page号(PPN,Physical Page Number),剩下12bit是offset完全继承自虚拟内存地址(也就是地址转换时,只需要将虚拟内存中的27bit翻译成物理内存中的44bit的page号,剩下的12bitoffset直接拷贝过来即可)。

如果每个进程都有自己的page table,那么每个page table表会有多大呢?

这个page table最多会有2^27个条目(虚拟内存地址中的index长度为27),这是个非常大的数字。如果每个进程都使用这么大的page table,进程需要为page table消耗大量的内存,并且很快物理内存就会耗尽。

所以实际上,硬件并不是按照这里的方式来存储page table。从概念上来说,你可以认为page table是从0到2^27,但是实际上并不是这样。实际中,page table是一个多级的结构。下图是一个真正的RISC-V page table结构和硬件实现。

(MIT6.S081)页表_第3张图片

我们之前提到的虚拟内存地址中的27bit的index,实际上是由3个9bit的数字组成(L2,L1,L0)。前9个bit被用来索引最高级的page directory(注:通常page directory是用来索引page table或者其他page directory物理地址的表单,但是在课程中,page table,page directory, page directory table区分并不明显,可以都认为是有相同结构的地址对应表单)。

一个directory是4096Bytes,就跟page的大小是一样的。Directory中的一个条目被称为PTE(Page Table Entry)是64bits,就像寄存器的大小一样,也就是8Bytes。所以一个Directory page有512个条目。

4096/8=512

所以实际上,SATP寄存器会指向最高一级的page directory的物理内存地址,之后我们用虚拟内存中index的高9bit用来索引最高一级的page directory,这样我们就能得到一个PPN,也就是物理page号。这个PPN指向了中间级的page directory。

当我们在使用中间级的page directory时,我们通过虚拟内存地址中的L1部分完成索引。接下来会走到最低级的page directory,我们通过虚拟内存地址中的L0部分完成索引。在最低级的page directory中,我们可以得到对应于虚拟内存地址的物理内存地址。

从某种程度上来说,与之前一种方案还是很相似的,除了实际的索引是由3步,而不是1步完成。这种方式的主要优点是,如果地址空间中大部分地址都没有使用,你不必为每一个index准备一个条目。举个例子,如果你的地址空间只使用了一个page,4096Bytes。

(MIT6.S081)页表_第4张图片

除此之外,你没有使用任何其他的地址。现在,你需要多少个page table entry,或者page table directory来映射这一个page?

在最高级,你需要一个page directory。在这个page directory中,你需要一个数字是0的PTE,指向中间级page directory。所以在中间级,你也需要一个page directory,里面也是一个数字0的PTE,指向最低级page directory。所以这里总共需要3个page directory(也就是3 * 512个条目)。

而在前一个方案中,虽然我们只使用了一个page,还是需要2^27个PTE。这个方案中,我们只需要3 * 512个PTE。所需的空间大大减少了。这是实际上硬件采用这种层次化的3级page directory结构的主要原因。这里有什么问题吗?这部分还是很重要的。

如果你把44bit的PPN和10bit的Flags相加是54bit,也就是说还有10bit未被使用,这10bit被用来作为未来扩展。比如说某一天你有了一个新的RISC-V处理器,它的page table可能略有不同,或许有超过44bit的PPN。如果你看下面这张图,你可以看到,这里有10bit是作为保留字段存在的。

(MIT6.S081)页表_第5张图片

  • 第一个标志位是Valid。如果Valid bit位为1,那么表明这是一条合法的PTE,你可以用它来做地址翻译。对于刚刚举得那个小例子(应用程序只用了1个page的例子),我们只使用了3个page directory,每个page directory中只有第0个PTE被使用了,所以只有第0个PTE的Valid bit位会被设置成1,其他的511个PTE的Valid bit为0。这个标志位告诉MMU,你不能使用这条PTE,因为这条PTE并不包含有用的信息。
  • 下两个标志位分别是Readable和Writable。表明你是否可以读/写这个page。
  • Executable表明你可以从这个page执行指令。
  • User表明这个page可以被运行在用户空间的进程访问。
  • 其他标志位并不是那么重要,他们偶尔会出现,前面5个是重要的标志位。

页表缓存

如果我们回想一下page table的结构,你可以发现,当处理器从内存加载或者存储数据时,基本上都要做3次内存查找,第一次在最高级的page directory,第二次在中间级的page directory,最后一次在最低级的page directory。所以对于一个虚拟内存地址的寻址,需要读三次内存,这里代价有点高。所以实际中,几乎所有的处理器都会对于最近使用过的虚拟地址的翻译结果有缓存。这个缓存被称为:Translation Lookside Buffer(通常翻译成页表缓存)。你会经常看到它的缩写TLB。基本上来说,这就是Page Table Entry的缓存,也就是PTE的缓存。

当处理器第一次查找一个虚拟地址时,硬件通过3级page table得到最终的PPN,TLB会保存虚拟地址到物理地址的映射关系。这样下一次当你访问同一个虚拟地址时,处理器可以查看TLB,TLB会直接返回物理地址,而不需要通过page table得到结果。

参考:https://www.zhihu.com/column/c_1294282919087964160

你可能感兴趣的:(Linux,linux,服务器,c++)