Appearance
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的默认处理方式是杀死进程。
- 如果一个地址的PTE无效,或者访问权限不足(例如,写入一个没有
xv6 中的虚拟内存
内核地址空间 (Kernel Address Space):
- 内核拥有自己的页表,在启动时由
kvmmake()创建。 - 它将硬件设备(如UART)和物理内存“直接映射”到相同的虚拟地址,方便内核访问。
- 内核代码(text)区域被映射为只读,其他数据区域则不可执行,以防止某些类型的攻击和bug。
- Trampoline 页 和 内核栈 被映射在高地址,并且 Trampoline 页同时被映射在所有用户地址空间中,以方便用户态和内核态的切换。
- 内核拥有自己的页表,在启动时由
用户地址空间 (User Address Space):
- 每个进程都有自己独立的页表和地址空间。
- 用户虚拟地址从
0开始,向上增长。 - 典型的布局包括:代码 (text), 数据 (data), 栈 (stack), 堆 (heap)。
- 内核在切换进程时,会切换
satp寄存器来激活对应进程的页表。
代码走读
kvmmake()(invm.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刷新,提高性能。