MIT6.1810/doc/Lab3-Page_Tables --Inspect a user-process page table

368 阅读20分钟

Lab: Page Tables

准备工作(含 RISC-V 的安装 - MacOS)

Installing RISC-V Toolchain on MacOS

这份指南将指导您在 macOS 上安装 RISC-V 编译工具链和 QEMU。

Step 1: 安装开发者工具

首先,您需要安装提供基本命令行工具(如 gitmakegcc)的开发者工具。打开终端并运行以下命令:

xcode-select --install

安装 Homebrew:

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

安装 RISC-V 工具链:

brew tap riscv/riscv
brew install riscv-tools

PATH=$PATH:/usr/local/opt/riscv-gnu-toolchain/bin

source ~/.bashrc    # 如果您使用 bash
source ~/.zshrc     # 如果您使用 zsh

Step 2: 安装 QEMU

brew install qemu

安装完成后,输入 qemu-system-riscv64 --version 查看是否安装成功。

Step 3: 安装 xv6

git clone https://github.com/mit-pdos/xv6-riscv.git

编译 xv6:

cd xv6-riscv
make
make qemu

虚拟内存概述

假设 shell 有一个 bug:

  • 有时它会写入随机的内存地址
  • 物理内存,0..2^64:应用程序和内核在同一内存中

我们如何防止它破坏内核?

  • 以及防止它破坏其他进程?

我们想要隔离的地址空间

  • 每个进程有自己的内存
  • 它可以读写自己的内存
  • 它不能读写其他的内容

挑战:

  • 如何将多个地址空间复用到一个物理内存上?
  • 同时保持隔离
  • 利用虚拟内存和**内存管理单元(MMU)**的机制
  • 初始化:操作系统为每个进程创建独立的页表,并分配所需的物理内存页框。
  • 地址映射:在页表中建立虚拟页到物理页框的映射关系。
  • 内存访问:当进程访问内存时,MMU根据当前的页表将虚拟地址转换为物理地址,并检查访问权限。
  • 隔离保障:如果进程尝试访问未映射或无权限的地址,MMU会触发异常,操作系统进行相应的处理(如终止进程或抛出错误)。

xv6 使用 RISC-V 的分页硬件来实现地址空间

  • 请随时提问!这些内容重要但复杂
  • 将是周四实验的主题(在其他实验中也会涉及)

页表为寻址提供了间接层

  • CPU -> MMU -> RAM
    • VA(虚拟地址) -> PA(物理地址)
  • 软件只能对虚拟地址进行加载/存储操作,不能对物理地址
  • 内核告诉 MMU 如何将每个虚拟地址映射到物理地址
    • MMU 本质上有一个表,索引是虚拟地址,结果是物理地址
    • 这个表叫“页表”
va | pa
-------
 x  |  y
  • 代码只能使用在表中有映射的地址

我们希望每个进程有不同的地址空间

  • 因此我们需要不止一个页表——并且需要切换页表
  • MMU 有一个寄存器(satp),内核通过写入它来更改页表

页表存放在哪里?

  • 在内存中
  • satp 持有当前页表的(物理)地址
  • MMU 从内存中加载页表项
  • 内核可以通过在内存中写入来修改页表

页表有多大?

  • 理论上有 264 个虚拟地址
  • 不现实的是创建一个有 264 个条目的表!
  • 许多细节都是关于如何减少表的大小

RISC-V 映射 4KB 的“页”

  • 因此页表只需要为每页有一个条目
  • 4KB = 12 位
  • RISC-V 有 64 位地址
  • 因此页表的索引是虚拟地址的高 64 - 12 = 52 位
    • 但高 52 位中的 25 位未使用
      • 目前没有 RISC-V 有那么多内存
      • 将来可以扩展
    • 因此,索引是 27 位

图 3.1 —— 简化视图

  • MMU 使用虚拟地址的索引位找到页表项 (PTE)
  • MMU 使用 PTE 中的 PPN 和虚拟地址的偏移量构建物理地址

/Users/Apple/Desktop/xv6/3.1.png

页表项 (PTE) 中包含什么?

  • [10 保留位 | 44 PPN | 10 标志]
  • 每个 PTE 是 64 位,但只用了 54 位
  • 44 位的 PPN(物理页号)是物理地址的高位
  • PTE 的低 10 位是标志
    • 有效、可写等
  • 再次提醒,物理地址的低 12 位是从虚拟地址中复制的

将页表只是作为 PTE 数组是否合理?

  • 如图 3.1 所示
  • 由虚拟地址的 27 位索引直接索引?
  • 页表有多大?
    • 227 大约是 1.34 亿
    • 每个条目 64 位
    • 完整页表大约 1GB
      • 每个地址空间——每个进程一个页表
  • 对于小程序会浪费很多内存!
    • 你可能只需要映射一小部分页
    • 其余的条目会消耗 RAM 但不需要

RISC-V 64 使用“三级页表”来节省空间

  • 虚拟地址的高 9 位索引到一级页目录
  • 一级 PTE 包含二级页目录的物理地址
    • 第二个 9 位索引二级目录
  • 同样适用于第三级
    • 现在我们有了包含所需内存页的 PTE
  • 实际上是一个树形结构:
  • 每次下降 9 位

虚拟地址转换过程

  1. 虚拟地址部分 (L2, L1, L0, 偏移量)

    • L2, L1, L0:这些是虚拟地址的不同部分,分别用于索引不同层级的页表,每一部分指向对应的页目录项。
    • 偏移量(Offset):虚拟地址的最后一部分,它表示在实际物理内存页中的具体位置。
  2. 页目录层级

    • 图中展示了虚拟地址如何依次通过多个页目录层级(L2、L1、L0)。
    • 每一个层级都指向下一个层级,最终解析出一个物理页号(PPN)
  3. 箭头

    • 箭头表示地址查找的方向。
    • 过程从左上角的虚拟地址开始,依次通过不同的页目录层级,最终得到物理地址
  4. 物理地址

    • 一旦通过页目录层级确定了物理页号(PPN),物理地址就由物理页号和虚拟地址中的偏移量组合而成。
  5. 示例

    • 假设我们有一个虚拟地址 0x123456789ABC
    • 虚拟地址的高 9 位是 0x123,它指向根页表中的一个页表项。

为什么树形页表节省空间?

  • 只有当需要时才分配页表页,未使用的部分无需分配内存。

为什么是 9 位?

  • 9 位决定了页目录的大小
  • 9 位 -> 512 个 PTE -> 64 位/PTE -> 4096 字节,即一页
  • 也就是说,9 位意味着一个目录适合一个单页

遍历树的操作是否昂贵,即使在硬件中?

  • 是的,CPU 的 MMU 通常会缓存最近使用的翻译
  • 这个缓存称为转换后备缓冲区(TLB)
  • 为了使 TLB 高效:页表支持超级页

页表项中的标志

  • V, R, W, X, U

如果 V 位没有设置?或者写操作时 W 位没有设置?

  • 页故障
  • 强制转移到内核
    • xv6 源代码中的 trap.c
  • xv6 内核只会打印错误,终止进程
"usertrap(): unexpected scause ... pid=... sepc=... stval=..."
  • 内核可以安装一个 PTE,然后恢复进程
    • 例如,在从磁盘加载内存页后
    • 有很多技巧可以在这里实现,比如Memory Compression,Lazy Page Table Installation,Page Reclaiming。

xv6 中的虚拟内存

内核页表

  • 图3.1

  • 左侧是虚拟地址

  • 右侧是物理地址

物理地址布局是什么?

  • 通常由硬件定义——主板
  • RAM 和内存映射设备寄存器

对于我们来说,QEMU 模拟了主板及其物理地址布局

MROM, UART, VIRTIO, DRAM
  • 与图 3.3 的右侧相同

图 3.3 的左侧由内核的页表定义

  • 内核在启动时设置它
  • 大多数是“直接映射”
    • 允许内核使用物理地址作为虚拟地址
    • 非常方便!
  • 内核文本没有 W 位
  • 内核数据等没有 X 位
  • xv6 假定有 128 MB 的 RAM —— PHYSTOP = 0x88000000
    • 应该动态查找 RAM 大小!
  • 在顶部:trampoline,内核栈
    • 注意高位页有两个虚拟映射!
  • 内核在切换页表时执行 trampoline
    • 在相同虚拟地址上创建与 trampoline 相同的用户页表

我们可以在没有分页的情况下运行内核吗?关闭 MMU?

  • 通常可以(取决于 CPU 设计)
  • 为什么要分页内核?
    • 将 RAM 放在预期位置
    • 双重映射
    • 禁止某些访问以捕获错误

分页硬件

页表是操作系统为每个进程提供专用地址空间和内存的常用机制。页表决定了内存地址的含义,以及物理内存的哪些部分可以被访问。在 xv6 中,在多个空间地址中映射相同的内存页,并使用未映射的页保护内核和用户的栈。

xv6 操作系统中两个关键的内存管理技巧:

  1. 跳板页的共享

    • 图中有两个不同的地址空间(比如两个进程),它们各自有独立的虚拟地址空间。但这两个地址空间中有一块虚拟内存(跳板页)是映射到相同的物理内存地址上的。
    • 通过这种方式,多个进程可以共享同一段物理内存,这在操作系统执行某些任务时(例如进程切换)非常有用。
  2. 未映射页保护内核和用户栈

    • 图中内核栈和用户栈的上下各有一页未映射的虚拟内存。这些未映射的页(称为保护页)起到保护作用,防止栈的溢出。
    • 当程序试图访问这些未映射的页时,会触发页错误,从而保护系统的安全性和稳定性。

页表硬件

页表硬件是 RISC-V 处理器中的一个重要组成部分,它负责将虚拟地址映射到物理地址。在 xv6 中,页表硬件主要用于管理进程的地址空间,包括用户栈和内核栈。

RISC-V 指令(无论是用户态还是内核态)都操作虚拟地址。机器的 RAM(物理内存)是通过物理地址进行索引的。RISC-V 的页表硬件连接了这两类地址,通过将每个虚拟地址映射到一个物理地址上。

xv6 运行在 Sv39 模式下,这是一种基于页表的地址翻译模式。Sv39 模式下,每个进程都有自己的页表,用于将虚拟地址映射到物理地址。页表的大小通常为 220 字节,即 1MB。在这种模式下,RISC-V 页表在逻辑上是一个包含 227(134,217,728)个页表条目(PTE)的数组。

每个 PTE 包含一个 44 位的物理页号(PPN)和 10 位的标志位。分页硬件通过使用虚拟地址的 39 位中的前 27 位在页表中查找一个 PTE,并生成一个 56 位的物理地址。这个物理地址的前 44 位来自 PTE 中的 PPN,而后 12 位则直接复制自原始虚拟地址。

图 3.1:

通过一个简单的表格形式展示 Sv39 RISC-V 的虚拟地址到物理地址的映射过程:

  1. 虚拟地址 (64 位):虚拟地址由 64 位组成,但在 Sv39 模式下,只使用底部的 39 位,顶部的 25 位未使用。

  2. 页表结构:RISC-V 的页表是一个包含 227 个页表条目 (PTE) 的数组。每个 PTE 包含一个 44 位的物理页号 (PPN) 和一些控制标志。

  3. 虚拟地址拆分:在虚拟地址中,前 27 位用于索引页表,最后 12 位则直接复制到物理地址中,用于页内偏移。

  4. 物理地址生成:当找到页表条目后,取出其中的 PPN,并将它的 44 位作为物理地址的高 44 位,然后将虚拟地址的最后 12 位复制到物理地址的低 12 位,组成最终的 56 位物理地址。

表示图结构:

字段位数描述
虚拟地址64 位仅使用底部的 39 位
虚拟地址高位27 位用于索引页表,找到对应的页表项 (PTE)
页内偏移12 位虚拟地址的最后 12 位,直接复制到物理地址中
页表条目 (PTE)44 位包含物理页号 (PPN),用于生成物理地址
物理地址56 位前 44 位来自 PPN,后 12 位来自虚拟地址的页内偏移

在 Sv39 RISC-V 中,虚拟地址的前 25 位在转换中不会被使用。物理地址也有增长空间:PTE 格式中为物理页号预留了额外的 10 位。RISC-V 的设计者基于技术发展的预测选择了这些数字。239 字节相当于 512 GB,这应该为运行的应用程序提供了足够的地址空间。

RISC-V 计算机上的虚拟地址转换使用了一种三级页表结构。这种结构有助于在物理内存中高效存储页表条目,并提供了灵活的虚拟地址到物理地址的映射方式。以下是关于这种三级页表结构的详细信息:

在 RISC-V 中,虚拟地址被转换为物理地址分为三个步骤:

  1. 根页表页

    • 根页表是一个大小为 4096 字节的页,包含 512 个页表项(PTE)。
    • 每个根页表项指向下一级页表页的物理地址。
  2. 中级页表页

    • 每个根页表项指向的中级页表页也是一个大小为 4096 字节的页,同样包含 512 个页表项。
    • 每个中级页表项指向最终级别的页表页的物理地址。
  3. 最终级页表页

    • 最终级页表页包含实际的页表项,用于将虚拟地址映射到物理地址。
    • 每个最终级页表页也是一个大小为 4096 字节的页,其中的页表项直接映射到物理页框。

在硬件执行加载或存储指令时,CPU 使用这种三级结构来进行虚拟地址到物理地址的转换。如果所需的任何一个页表项不存在,硬件将引发页面故障异常,由操作系统内核处理。

三级页表结构相较于单级设计,具有更高的内存效率。在许多虚拟地址范围没有映射的常见情况下,三级结构可以避免分配整个页目录,从而节省内存。例如,如果一个应用程序只使用从地址零开始的几个页面,则顶级页目录的 1 到 511 项将无效,操作系统不必为这些中间页目录分配页面。同样,对于这些中间页目录,底级页目录也无需分配页面。因此,在这种情况下,三级设计可以节省大量内存页的分配。

尽管三级结构允许硬件在执行加载或存储指令时高效地遍历页表,但潜在的缺点是需要从物理内存中加载三个页表项,这可能会增加地址转换的成本。为了避免从物理内存加载页表项的成本,RISC-V CPU 使用页表项缓存在 TLB 中。

转换后备缓冲区(TLB)

转换后备缓冲区(Translation Lookaside Buffer,TLB)是一种用于提高虚拟地址到物理地址转换效率的缓存机制。TLB 存储最近使用的页表项(PTE),使得 CPU 可以快速查找这些项,而无需每次都访问主内存中的页表。以下是 TLB 的一些关键点:

  1. 工作原理

    • 当 CPU 需要转换一个虚拟地址时,它首先检查 TLB 中是否存在相应的页表项。
    • 如果找到(称为 TLB 命中),则可以直接使用缓存中的物理地址进行访问,这样可以显著减少访问延迟。
    • 如果没有找到(称为 TLB 未命中),CPU 将需要访问页表,从而获取相应的页表项。这时,TLB 也会将新的页表项加载到缓存中,以便将来使用。
  2. TLB 的结构

    • TLB 通常使用关联映射来存储页表项。这使得 TLB 可以更快速地查找项。
    • TLB 中的每个条目通常包含以下信息:
      • 虚拟页号(VPN):用来标识虚拟地址中的页。
      • 物理页框号(PFN):对应于虚拟页的物理地址。
      • 其他信息,如有效位、访问权限位等。
  3. 性能优势

    • TLB 的使用可以显著减少内存访问的延迟,尤其是在访问频繁使用的内存地址时。
    • 通过减少对主内存的访问,TLB 减少了内存带宽的消耗,提升了整体系统性能。
  4. TLB 的局限性

    • TLB 的大小有限,因此可能无法缓存所有页表项。当 TLB 满时,需要实施替换策略(如 LRU、随机等)来决定哪个条目被替换。
    • 由于 TLB 是硬件实现的,其大小和复杂性增加可能会带来额外的功耗和成本。

RISC-V 特权级

LevelEncoding特权级缩写权限
000User/ApplicationU运行用户程序
101SupervisorS运行操作系统内核和驱动
210---
311MachineM运行 BootLoader 和其他固件

开始实验

Inspect a user-process page table (easy)

实验的目的是查看用户进程的页表项,并解释页表的逻辑内容和权限位。


实验步骤

1. 创建或修改系统调用 pgpte

实验提到需要用到一个 pgpte 系统调用来打印用户进程的页表条目。这个系统调用在 xv6 的默认版本中是不存在的,所以我们需要自己实现这个系统调用。以下是实现的步骤:

a. 在 syscall.h 中添加系统调用号

我们需要在 syscall.h 文件中定义 pgpte 系统调用号。

打开 kernel/syscall.h 文件,添加一行定义:

#define SYS_pgpte 23

(确保它不会和已有的系统调用号冲突)

b. 在 syscall.c 中注册系统调用

kernel/syscall.c 文件中,我们需要注册这个新的系统调用。

syscall.csyscalls[] 数组中,添加一行:

extern uint64 sys_pgpte(void);

然后在 syscalls[] 数组中,添加对 sys_pgpte 的引用:

[SYS_pgpte] sys_pgpte,
c. 实现 pgpte 系统调用

我们需要在 kernel 目录中创建一个实现 pgpte 系统调用的函数。

kernel/sysproc.c 中添加以下函数来打印页表条目:

uint64
sys_pgpte(void)
{
    // 实现遍历当前进程页表并打印页表项的逻辑
    struct proc *p = myproc();  // 获取当前进程
    pagetable_t pagetable = p->pagetable;  // 获取页表
    uint64 va;
    pte_t *pte;
    uint64 pa;
    int perm;

    for (va = 0; va < MAXVA; va += PGSIZE) {  // 遍历虚拟地址空间
        pte = walk(pagetable, va, 0);  // 获取虚拟地址对应的 PTE
        if (pte == 0 || (*pte & PTE_V) == 0)
            continue;  // 如果 PTE 不存在或无效,跳过

        pa = PTE2PA(*pte);  // 从 PTE 中获取物理地址
        perm = *pte & 0xFF;  // 获取权限位
        printf("va 0x%lx pte 0x%lx pa 0x%lx perm 0x%x\n", va, *pte, pa, perm);
    }
    return 0;
}

这个函数的逻辑是遍历当前进程的整个虚拟地址空间,找到有效的页表项并打印出虚拟地址(va)、页表项(pte)、物理地址(pa)和权限位(perm)。

d. 在 user.h 中声明系统调用

user/user.h 文件中添加一行声明:

int pgpte(void);

这个声明告诉编译器 pgpte 是一个用户程序可以调用的函数。

e. 在 usys.S 中添加系统调用

user/usys.S 文件中添加对 pgpte 的系统调用封装:

.global pgpte
pgpte:
 li a7, SYS_pgpte
 ecall
 ret

2. 编写 pgtbltest 用户程序

现在你可以编写一个简单的用户程序来调用 pgpte 系统调用并打印页表。

user/pgtbltest.c 文件中写一个测试程序:

#include "kernel/types.h"
#include "user/user.h"

int
main(void)
{
    printf("Running pgtbltest...\n");
    pgpte();  // 调用系统调用,打印页表信息
    exit(0);
}

编写完成后,在 Makefile 中添加这个程序以便编译。在 UPROGS 列表中,添加:

$U/_pgtbltest\

然后在 user/usys.pl 中添加:

entry("pgpte");

分析输出

运行 pgtbltest,你会看到以下输出:

$ patbltest
Running pgtbltest...
va 0x0 pte 0x21fc885b pa 0x87f22000 perm 0x5b
va 0x1000 pte 0x21fc7c17 pa 0x87f1f000 perm 0x17
va 0x2000 pte 0x21fc7807 pa 0x87f1e000 perm 0x7
va 0x3000 pte 0x21fc74d7 pa 0x87f1d000 perm 0xd7
va 0x3fffffe000 pte 0x21fd08c7 pa 0x87f42000 perm 0xc7
va 0x3ffffff000 pte 0x2000184b pa 0x80006000 perm 0x4b

页表测试输出分析 (pgtbltest)

每一行输出包括虚拟地址(va)、页表项(pte)、物理地址(pa)以及权限位(perm),以下是详细的解释:

  1. 虚拟地址(va

    • va表示虚拟地址,程序通过页表将虚拟地址映射到物理地址。
  2. 页表项(pte

    • pte是页表项,包含指向物理内存页的物理地址,以及相关的控制和权限位。
  3. 物理地址(pa

    • pa是物理地址,即虚拟地址通过页表转换后的实际物理地址。
  4. 权限位(perm

    • perm表示与页面关联的权限控制位,如读、写、执行权限。

输出分析

  1. 条目 1:

    • 虚拟地址: 0x0
    • 页表项: 0x21fc885b
    • 物理地址: 0x87f22000
    • 权限位: 0x5b
      • 权限 0x5b: 二进制 1011011,表示页面的读写权限和执行权限。
  2. 条目 2:

    • 虚拟地址: 0x1000
    • 页表项: 0x21fc7c17
    • 物理地址: 0x87f1f000
    • 权限位: 0x17
      • 权限 0x17: 二进制 00010111,表示页面的读权限和部分写权限。
  3. 条目 3:

    • 虚拟地址: 0x2000
    • 页表项: 0x21fc7807
    • 物理地址: 0x87f1e000
    • 权限位: 0x7
      • 权限 0x7: 二进制 00000111,表示页面具有读、写和执行权限。
  4. 条目 4:

    • 虚拟地址: 0x3000
    • 页表项: 0x21fc74d7
    • 物理地址: 0x87f1d000
    • 权限位: 0xd7
      • 权限 0xd7: 二进制 11010111,表示页面具有读、写、执行权限,并且可能有其他特殊控制位。
  5. 条目 5:

    • 虚拟地址: 0x3fffffe000
    • 页表项: 0x21fd08c7
    • 物理地址: 0x87f42000
    • 权限位: 0xc7
      • 权限 0xc7: 二进制 11000111,表示页面具有读、写、执行权限。
  6. 条目 6:

    • 虚拟地址: 0x3ffffff000
    • 页表项: 0x2000184b
    • 物理地址: 0x80006000
    • 权限位: 0x4b
      • 权限 0x4b: 二进制 01001011,表示页面具有读写权限和其他部分控制。

结论

通过解析上述虚拟地址与物理地址的映射关系,以及权限位,可以观察到各个虚拟地址所映射的物理地址及其对应的权限控制。不同权限位允许不同的访问权限,帮助操作系统控制内存访问的安全性和正确性。


参考资料