Skip to content

6.1810 2024 第4讲:虚拟内存/页表

虚拟内存概述

今日议题: 假设shell有一个bug,它会向一个随机的内存地址写入数据。我们如何防止它破坏内核或其他进程?

解决方案:隔离的地址空间

  • 每个进程都拥有自己独立的内存视图,称为地址空间。
  • 进程可以读写自己的内存,但不能访问其他进程或内核的内存。
  • 挑战: 如何在单一的物理内存上,为多个进程提供各自独立的地址空间,同时保证隔离?

核心机制:分页硬件 (Paging Hardware)

  • xv6 使用 RISC-V CPU 的分页硬件来实现地址空间。
  • 间接层: CPU发出的地址是虚拟地址 (VA),内存管理单元 (MMU) 会将其翻译成物理地址 (PA),然后发送给物理内存。
  • 页表 (Page Table): 内核为每个进程维护一个页表,这个表定义了该进程的虚拟地址到物理地址的映射关系。MMU通过查询页表来进行地址翻译。
  • 切换地址空间: 当内核切换进程时,它会更新 satp 寄存器,使其指向新进程的页表,从而切换到新的地址空间。

RISC-V 分页详解

  • 页 (Page): RISC-V 将内存划分为固定大小的块,称为“页”,大小为 4KB (2^12 字节)。地址翻译以页为单位进行。
  • 三级页表 (Three-Level Page Table):
    • 为了节省存储页表的空间,RISC-V 使用了树状的三级页表结构。
    • 一个39位的虚拟地址被分为三部分,每部分9位,分别用作三级页表的索引,最后12位是页内偏移。
    • [9 bits L2 | 9 bits L1 | 9 bits L0 | 12 bits offset]
    • MMU 硬件会自动“遍历”这棵树来找到最终的物理地址。
  • 页表项 (Page Table Entry, PTE):
    • 每个页表项(64位)包含一个物理页号 (PPN) 和一些标志位。
    • 标志位:
      • PTE_V: Valid,此PTE是否有效。
      • PTE_R: Readable,是否允许读取。
      • PTE_W: Writable,是否允许写入。
      • PTE_X: Executable,是否允许执行。
      • PTE_U: User,用户模式是否可以访问此页。如果未设置,则只有监控模式可以访问。
  • 页错误 (Page Fault):
    • 如果一个地址的PTE无效,或者访问权限不足(例如,写入一个没有 PTE_W 标志的页面),MMU会停止执行并触发一个异常,将控制权交给内核。xv6的默认处理方式是杀死进程。

xv6 中的虚拟内存

  • 内核地址空间 (Kernel Address Space):

    • 内核拥有自己的页表,在启动时由 kvmmake() 创建。
    • 它将硬件设备(如UART)和物理内存“直接映射”到相同的虚拟地址,方便内核访问。
    • 内核代码(text)区域被映射为只读,其他数据区域则不可执行,以防止某些类型的攻击和bug。
    • Trampoline 页内核栈 被映射在高地址,并且 Trampoline 页同时被映射在所有用户地址空间中,以方便用户态和内核态的切换。
  • 用户地址空间 (User Address Space):

    • 每个进程都有自己独立的页表和地址空间。
    • 用户虚拟地址从 0 开始,向上增长。
    • 典型的布局包括:代码 (text), 数据 (data), 栈 (stack), 堆 (heap)。
    • 内核在切换进程时,会切换 satp 寄存器来激活对应进程的页表。

代码走读

  • kvmmake() (in vm.c): 在内核启动早期(此时分页未开启,地址都是物理地址)创建内核页表。
  • kvmmap() / mappages(): 负责在页表中创建一段虚拟地址到物理地址的映射。它会遍历页表树,如果中间的页目录页不存在,会调用 kalloc() 分配新的页目录页。
  • walk(): 模拟MMU硬件的行为,遍历三级页表,为给定的虚拟地址找到其对应的PTE的地址。这是虚拟内存管理的核心函数之一。
  • kvminithart():main() 函数的后期,将内核页表的物理地址加载到 satp 寄存器中,正式开启分页机制。

TLB (Translation Look-aside Buffer)

  • 为了加速地址翻译,CPU会缓存最近使用过的VA到PA的翻译结果,这个缓存就是TLB。
  • satp 改变时(即切换页表时),TLB中的旧缓存可能失效,需要刷新。xv6在每次用户/内核切换时都会刷新整个TLB。
  • RISC-V 提供了一些更高级的TLB管理机制(如 PTE_G 全局标志位、ASID 地址空间标识符),以减少不必要的TLB刷新,提高性能。