Post

6.S081|第三章-页表

6.S081|第三章-页表

页式硬件

RISCV里执行的指令中的地址都是使用的虚拟地址(Q:为什么要用虚拟地址?A:为了隔离性和保护性),但是机器的物理内存是由物理地址索引的,所以我们需要一种映射机制来完成由虚拟地址到物理地址的转换。

在开始下面内容之前,我们需要知道xv6的内存管理机制是Sv39 RISC-V,这意味着我们只使用底部的39位,尽管虚拟地址是64位的(因为这是64位机器,寄存器也是64位的)。

一般来说一个分页是4KB大小,也就是4096个字节,想要索引这4096个字节,我们需要12位,因为$2^{12} = 4096$,又因为Sv39不使用高25位,所以39 - 12 = 27,所以我们除了用于寻址的12位之外,还有多出了一个27位的空间可以使用。那么这27位是用来干嘛的呢?

答案是:这27位是用来寻址页表项的(Page Table Entries/PTE),每个PTE包含一个44位的物理页码(Physical Page Number/PPN)和一些标志位(标志位占10位)(Q: PTE格式中有空间让物理地址长度再增长10个比特位。RISC-V 的设计者根据技术预测选择了这些数字。)。分页硬件通过使用虚拟地址39位中的前27位索引页表,以找到该虚拟地址对应的一个PTE,然后生成一个56位的物理地址(Q:为什么物理地址是56位?A:RISC-V 的设计者根据技术预测选择了这些数字。),其前44位来自PTE中的PPN,其后12位来自原始虚拟地址

6.S081|第三章-页表-20250122.png

如下图所示,实际的转换分三个步骤进行。页表以三级的树型结构存储在物理内存中。该树的根是一个4096字节的页表页,其中包含512个PTE(因为一个PTE是8B),每个PTE中包含该树下一级页表页的物理地址。这些页中的每一个PTE都包含该树最后一级的512个PTE(也就是说每个PTE占8个字节,正如图3.2最下面所描绘的)。分页硬件使用27位中的前9位在根页表页面中选择PTE,中间9位在树的下一级页表页面中选择PTE,最后9位选择最终的PTE。

通俗来说,PPN是用来索引下一级页表的地址,而虚拟地址中的9位是用来索引找到的页表中的页表项。

6.S081|Lab5-Copy-on-Write Fork for xv6-20250122.png

如果转换地址所需的三个PTE中的任何一个不存在,页式硬件就会引发页面故障异常(page-fault exception),并让内核来处理该异常(参见第4章)。

为什么要用三级页表设计?

主要是为了解决单级页表资源浪费的问题

如果使用单级页表来映射整个64位虚拟地址空间,会导致非常高的内存开销。假设我们使用4KB的页大小,每个页表项占用8字节,那么对于64位地址空间($2^{64}$ 个地址),页表的大小将非常庞大:

  • 单级页表需要映射整个虚拟地址空间,即需要 $\frac{2^{64}}{4KB} = 2^{52}$ 个页表项。

  • 这意味着单级页表的大小为 $2^{52} \times 8$ 字节,约为32 PB(32 Petabytes)的内存。

举个例子,如果一个应用程序只使用了一个页面,那么顶级页面目录将只使用条目0,条目 1 到 511 都将被忽略,因此内核不必为这511个条目所对应的中间页面目录分配页面,也就更不必为这 511 个中间页目录分配底层页目录的页。 所以,在这个例子中,三级设计仅使用了三个页面,共占用 $3\times4096$个字节。

Q:计算机是动态分配页表的吗,要是没有东西 就不会有页表项吗?我有点不能理解,假如又开了一个程序,没有预留空间的话,岂不是要移动所有地址

A:计算机的页表是动态分配的,但并不意味着地址会因为新程序启动而移动。让我们一步步来解释:

1. 动态分配页表

页表的动态分配意味着,当程序使用某个虚拟地址时,操作系统才会为该地址分配物理页,并创建或更新相应的页表条目。未使用的虚拟地址不会有页表项,也不需要分配任何内存。因此,页表项是在需要时动态生成的,而不是提前为所有可能的虚拟地址预留空间。

例如:

  • 如果一个程序刚启动并且只使用了一个页面,那么只会为该页面创建相关的页表项。

  • 如果这个程序后来需要更多的虚拟地址空间(比如分配了新的内存页),操作系统会动态分配新的页表项。

2. 虚拟地址和物理地址的关系

虚拟内存系统的一个重要特性是,虚拟地址与物理地址是独立的。每个程序都有自己的虚拟地址空间,而操作系统负责将这些虚拟地址映射到物理内存。这种映射关系保存在页表中。

即使有新的程序启动或旧的程序释放了内存,虚拟地址空间的布局并不会因此变化。每个程序都有自己的独立虚拟地址空间,不会影响其他程序的虚拟地址布局。

3. 为什么不需要移动地址?

你提到“没有预留空间”的担忧是针对物理内存的情况。在虚拟内存系统中,并不需要提前为程序预留物理内存空间,具体原因如下:

  • 虚拟内存的独立性:每个程序有自己独立的虚拟地址空间,它们之间互不干扰。即使物理内存紧张,虚拟地址也不会改变或移动。

  • 按需分配物理内存:当程序需要更多内存时,操作系统会按需分配物理页并更新对应的页表项。这种分配是动态的,但不会影响已经分配的虚拟地址,也不会影响其他程序。

  • 交换(Swap)机制:如果物理内存不足,操作系统可以将不常用的页面移到硬盘(即“交换”到swap空间),释放出物理内存供其他程序使用。这不会影响程序的虚拟地址。

4. 新程序启动时不会影响地址布局

假设启动了一个新程序:

  • 操作系统会为该程序分配一个新的虚拟地址空间,通常从一个标准的起始地址(比如用户空间的某个区域)开始。

  • 新程序的虚拟地址不会影响其他已经在运行的程序的虚拟地址,也不需要移动现有的地址布局。

  • 如果需要分配物理内存,会为该程序按需分配物理页,并更新相应的页表。其他程序的地址映射不会因此而移动或改变。

TLB的引入

因为 CPU 在执行转换时会在硬件中遍历三级结构,所以缺点是 CPU 必须从内存中加载三个 PTE 以将虚拟地址转换为物理地址。为了减少从物理内存加载 PTE 的开销,RISC-V CPU 将页表条目缓存在 Translation Look-aside Buffer (TLB) 中。

标志位

每个PTE包含标志位,这些标志位告诉分页硬件允许如何使用关联的虚拟地址。PTE_V指示PTE是否存在:如果它没有被设置,对页面的引用会导致异常(即不允许)。PTE_R控制是否允许指令读取到页面。PTE_W控制是否允许指令写入到页面。PTE_X控制CPU是否可以将页面内容解释为指令并执行它们。PTE_U控制用户模式下的指令是否被允许访问页面;如果没有设置PTE_U,PTE只能在管理模式下使用。图3.2显示了它是如何工作的。标志和所有其他与页面硬件相关的结构在(kernel/riscv.h)中定义。

satp寄存器

satp寄存器是用来存放根页表在物理内存中的地址的。每个CPU都有自己的satp,因此不同的CPU可以运行不同的进程。

tips

关于术语的一些注意事项。物理内存是指DRAM中的存储单元。物理内存以一个字节为单位划为地址,称为物理地址。指令只使用虚拟地址,分页硬件将其转换为物理地址,然后将其发送到DRAM硬件来进行读写。与物理内存和虚拟地址不同,虚拟内存不是物理对象,而是指内核提供的管理物理内存和虚拟地址的抽象和机制的集合。

内核地址空间

6.S081|第三章-页表-20250122-2.png

QEMU模拟了一台计算机,它包括从物理地址0x80000000开始并至少到0x86400000结束的RAM(物理内存),xv6称结束地址为PHYSTOP。QEMU模拟还包括I/O设备,如磁盘接口。QEMU将设备接口作为内存映射控制寄存器暴露给软件,这些寄存器位于物理地址空间0x80000000以下。内核可以通过读取/写入这些特殊的物理地址与设备交互;这种读取和写入与设备硬件而不是RAM通信。第4章解释了xv6如何与设备进行交互。

内核使用“直接映射”获取内存和内存映射设备寄存器;也就是说,将资源映射到等于物理地址的虚拟地址。例如,内核本身在虚拟地址空间和物理内存中都位于KERNBASE=0x80000000。直接映射简化了读取或写入物理内存的内核代码。例如,当fork为子进程分配用户内存时,分配器返回该内存的物理地址;fork在将父进程的用户内存复制到子进程时直接将该地址用作虚拟地址。

有几个内核虚拟地址不是直接映射:

  • 蹦床页面(trampoline page)。它映射在虚拟地址空间的顶部;用户页表具有相同的映射。第4章讨论了蹦床页面的作用,但我们在这里看到了一个有趣的页表用例;一个物理页面(持有蹦床代码)在内核的虚拟地址空间中映射了两次:一次在虚拟地址空间的顶部,一次直接映射。

  • 内核栈页面。每个进程都有自己的内核栈,它将映射到偏高一些的地址,这样xv6在它之下就可以留下一个未映射的保护页(guard page)。保护页的PTE是无效的(也就是说PTE_V没有设置),所以如果内核溢出内核栈就会引发一个异常,内核触发panic。如果没有保护页,栈溢出将会覆盖其他内核内存,引发错误操作。恐慌崩溃(panic crash)是更可取的方案。(注:Guard page不会浪费物理内存,它只是占据了虚拟地址空间的一段靠后的地址,但并不映射到物理地址空间。)

进程地址空间分配

每个进程都有一个单独的页表,当xv6在进程之间切换时,也会更改页表。如图2.3所示,一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA (kernel/riscv.h:348),原则上允许一个进程内存寻址空间为256G(因为xv6保留一位只用到38位),这一点在MAXVA的定义中可以得知。

6.S081|第三章-页表-20250122-3.png

This post is licensed under CC BY 4.0 by the author.