[MIT 6.1810]Xv6 Chapter 3
Page tables
3.1 Paging hardware
RISC-V的指令操控的是虚拟地址,而物理内存是按照物理地址索引的。页表硬件负责进行虚拟地址和物理地址的转换。
xv6运行在Sv39 RISC-V上,只使用64位虚拟地址的低39位。在这种情况下,页表逻辑上是一个2^27次方的页表项(Page table entrie,PTE)数组,每个PTE包含44位物理页码(Physical page number, PPN)和一些标识位。页表硬件将虚拟地址低39位的高27位作为页表索引,然后将取得的44位PPN作为高44位,虚拟地址的低12位作为页内偏移,获得一个56位的物理地址。
xv6中采取的是三级页表,第一级页表页的物理起始地址由satp寄存器保存,27位的高9位用来索引第一级页表,第一级页表页表项的PPN字段是第二级页表页的起始物理地址,27位的中间9位用来索引第二级页表,其页表项的PPN字段是第三级页表页的起始物理地址,27位的低9位用来索引第三级页表,其页表项的PPN字段是实际访问内存页的物理地址。每级页表页大小为一页,包含512个页表项。
在上述地址转换过程中,如果某个页表项不存在,都会引发一个页面错误异常(page fault exception),由内核负责处理这个异常。
采取这种多级页表的好处是节省了内存空间,用不到的页表页将不会占据空间;缺点是在地址转化的时候需要进行多次访问。以三级页表为例,首先需要访问第一级页表获取第二级页表的物理地址,然后需要访问第二级页表获取第三级页表的物理地址,再访问第三级页表获取实际物理页的物理地址,之后才能访问我们实际需要读取的数据。为了解决这个问题,CPU可以将一些页表项缓存到TLB(Translation Look-aside Buffer)中,这样,如果在TLB中命中,则直接可以拿到实际物理页的物理地址,不需要进行额外的访存操作。
xv6中和页表硬件相关的结构定义在(kernel/riscv.h)中。
每个CPU都有自己的satp寄存器,该寄存器指定了第一级页表的物理地址。
3.2 Kernel address space
xv6为每个进程维护一个页表,并且维护一个单独的页表指明内核地址空间。内核配置内存空间的布局使其可以访问物理内存和各种设备资源,如图3.3所示。内核内存布局用到的常量定义在kernel/memlayout.h里。
QEMU 模拟了一个计算机系统,物理内存(RAM)从物理地址 0x80000000 开始,至少延续到 0x88000000,称为 PHYSTOP。QEMU 还包含了 I/O 设备,比如磁盘接口。这些设备接口通过内存映射控制寄存器暴露给软件,这些寄存器位于物理地址空间的 0x80000000 之下。内核可以通过读取或写入这些特殊的物理地址与设备进行交互;这样的读写操作是与设备硬件直接通信,而不是与 RAM 进行交互。第 4 章将解释 xv6 如何与这些设备进行交互。
内核通过“直接映射”访问 RAM 和内存映射的设备寄存器,即将资源映射到与物理地址相同的虚拟地址。例如,内核本身位于虚拟地址空间和物理内存中的 KERNBASE=0x80000000
。直接映射简化了内核读取或写入物理内存的代码。
直接映射:在直接映射中,虚拟地址与物理地址相等,这意味着内核可以使用相同的地址来访问内存。这种方式使得内核在处理物理内存时更加简便,不需要额外的地址转换。
内存分配示例:例如,在
fork
系统调用中,当为子进程分配用户内存时,分配器返回的是物理内存地址。由于内核采用直接映射,fork
可以直接将这个物理地址作为虚拟地址使用,将父进程的用户内存内容复制到子进程中。这减少了需要进行地址转换的复杂性。
在内核的虚拟地址空间中,有几个地址并不是直接映射的,包括:
Trampoline Page
映射位置:Trampoline 页位于虚拟地址空间的顶部,并且用户页面表也具有相同的映射。
作用:Trampoline 页用于实现上下文切换等功能。它的物理页面(存放 Trampoline 代码)在内核的虚拟地址空间中被映射了两次:一次在虚拟空间的顶部,另一次是直接映射的地址。
特点:这种设计展示了页面表的灵活使用,能够在需要时同时使用不同的映射。
Kernel Stack Pages
每个进程的内核栈:每个进程都有自己的内核栈,这些栈映射在高地址,以便在它们下面保留一个未映射的保护页(guard page)。
保护页:保护页的页表项(PTE)是无效的(即 PTE_V 没有设置),因此如果内核栈溢出,它将可能导致异常,从而引发内核恐慌(panic)。没有保护页的情况下,溢出的栈可能会覆盖其他内核内存,导致错误操作。
映射设计:内核通过高内存映射使用这些栈,但也可以通过直接映射的地址访问它们。如果只使用直接映射而不设置保护页,处理保护页将涉及取消映射原本指向物理内存的虚拟地址,这将变得难以使用。
权限设置
- Trampoline 页和内核文本的映射:这些页面的权限设置为 PTE_R(可读)和 PTE_X(可执行),内核可以从这些页面读取和执行指令。
- 其他页面的映射:其他页面的权限设置为 PTE_R(可读)和 PTE_W(可写),允许内核读取和写入这些页面的内存。
- 保护页的映射:保护页的映射是无效的,以防止访问。
3.3 Code: creating an address space
xv6中大多数操控地址空间和页表的代码在vm.c中。
核心数据结构是 pagetable_t
,这是一个指向第一级页表的指针,它要么指向内核页表,要么指向进程自己的页表。
核心函数有:
walk
- 找到一个虚拟地址对应的PTEmappages
- 为新的映射设立PTE
以kvm为前缀的函数操作内核页表;以uvm为前缀的函数操作进程页表;其它函数两者都可以操作。
在 xv6 操作系统中,copyout
和 copyin
函数用于将数据复制到用户虚拟地址和从用户虚拟地址复制数据,这些地址作为系统调用参数传入。这些函数在 vm.c
文件中实现,因为它们需要显式地转换这些地址以找到对应的物理内存。
内核的页表创建:
- 在启动序列的早期,
main
函数调用kvminit
来创建内核的页表。此调用发生在 xv6 启用分页之前,因此地址直接指向物理内存。 kvmmake
首先分配一页物理内存,用于存放根页面表。然后,它调用kvmmap
安装内核所需的地址转换,包括内核的指令和数据、到 PHYSTOP 的物理内存,以及实际上是设备的内存范围。
分配内核栈:
proc_mapstacks
为每个进程分配一个内核栈。它调用kvmmap
将每个栈映射到由KSTACK
生成的虚拟地址,这样可以为无效的栈保护页留出空间。
页面映射的安装:
kvmmap
调用mappages
,将虚拟地址范围映射到相应的物理地址范围。对于范围内的每个虚拟地址,mappages
分别处理,并按页间隔进行映射。- 对于每个需要映射的虚拟地址,
mappages
调用walk
找到该地址的页面表项(PTE)。然后,它初始化 PTE,以保存相关的物理页面编号、所需的权限(PTE_W、PTE_X 和/或 PTE_R),并设置 PTE_V 将 PTE 标记为有效。
PTE 查找
- walk 函数:
walk
函数模拟 RISC-V 的分页硬件,逐级查找虚拟地址的 PTE。它使用每级地址的 9 位虚拟地址索引相关的页面目录页。每一层中,它找到的是下一层页面目录页的 PTE,或者是最终页面的 PTE。 - 如果一级或二级页面目录页中的 PTE 无效,则所需的目录页尚未分配;如果
alloc
参数被设置,walk
将分配一个新的页面表页,并将其物理地址放入 PTE 中。它返回树中最低层的 PTE 地址。
物理内存与虚拟地址的直接映射
上述代码依赖于物理内存直接映射到内核虚拟地址空间。例如,当 walk
函数向下查找页面表时,它从 PTE 中提取下一层页面表的物理地址,然后将该地址用作虚拟地址以获取下一层的 PTE。所以必须保证是采用直接映射。
安装内核页表
main
调用kvminithart
安装内核页面表。它将根页面表的物理地址写入satp
寄存器。此后,CPU 将使用内核页面表进行地址转换。由于内核使用直接映射,下一条指令的虚拟地址将正确映射到对应的物理内存地址。
TLB
在 RISC-V CPU 中,页表项会被缓存到转换后备缓冲区(TLB)中。当 xv6 修改页表时,必须通知 CPU 使对应的 TLB 缓存条目失效。如果不这样做,TLB 可能会在后续使用旧的缓存映射,这可能会导致指向的物理页面被分配给其他进程,从而使一个进程能够访问或篡改另一个进程的内存。
TLB 失效机制
sfence.vma 指令:
RISC-V 提供了
sfence.vma
指令,用于刷新当前 CPU 的 TLB。xv6 在kvminithart
中重新加载satp
寄存器后执行此指令,同时在切换到用户页面表的 Trampoline 代码中也会调用它(在kernel/trampoline.S:89
处)。确保旧表格的完整性:
在更改
satp
之前,也需要发出sfence.vma
指令,以确保所有未完成的加载和存储操作已完成。这一等待过程确保先前对页表的更新完成,同时保证之前的加载和存储使用旧的页表,而不是新的。
地址空间标识符(ASIDs)
- 为了避免刷新整个 TLB,RISC-V CPU 可能支持地址空间标识符(ASIDs)。这样,内核可以仅刷新特定地址空间的 TLB 条目,而无需影响其他地址空间。
- 然而,xv6 并未使用这一特性,因此每次修改页表时仍需完全刷新 TLB。
3.4 Physical memory allocation
xv6使用介于内核末尾和PHYSTOP直接的物理内存来进行运行时分配。通过页面链表来维护空闲页面,当分配的时候需要从链表中移除分配的页面,当释放的时候需要将页面添加到链表中。
3.5 Code:Physical memory allocator
分配器代码在kalloc.c中。
分配器的数据结构是空闲页面的链表。链表中的元素是 struct run
,这些结构体保存在相应的空闲页面内。链表被一个自旋锁保护。
main
调用 kinit
来初始化这个分配器。 kinit
负责初始化链表,维护每个空闲页面。用到了 freerange
和 kfree
。
3.6 Process address space
每个进程都有自己的页表,进程切换的时候会更换页表。
内核在用户地址空间的顶部映射了一页包含 trampoline 代码的页面,该页面没有设置用户访问权限(PTE_U)。这样,虽然这页物理内存在所有地址空间中可见,但只有内核能够使用它。
共享页面:
这页物理内存被映射到每个用户进程的地址空间中,但由于没有用户访问权限,用户进程无法访问或修改该页。
内核的用途:
该页面的主要用途是在上下文切换时,内核可以通过 trampoline 代码快速切换到用户模式。这种设计简化了用户与内核之间的切换过程。
安全性:
通过限制用户访问这页内存,内核能够防止潜在的安全问题,确保用户进程无法干扰或篡改关键的内核代码。