实验:页表

在本实验中,你将探索页表并对其进行修改,以简化从用户空间复制数据到内核空间的函数。

在开始编码之前,请阅读 xv6 book 的第3章,以及相关文件:

开始实验前,切换到 pgtbl 分支:

  $ git fetch
  $ git checkout pgtbl
  $ make clean
  

打印页表

为了帮助你学习 RISC-V 页表,并可能有助于将来的调试,你的第一个任务是编写一个函数来打印页表的内容。

定义一个名为 vmprint() 的函数。它接受一个 pagetable_t 参数,并按以下格式打印该页表。在 exec.c 中 return argc 之前插入 if(p->pid==1) vmprint(p->pagetable),以打印第一个进程的页表。如果你通过了 make gradepte printout 测试,即可获得该作业的满分。

现在,当你启动 xv6 时,它应该打印如下输出,描述第一个进程在刚完成 exec() init 时的页表:

page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
..255: pte 0x0000000021fdb401 pa 0x0000000087f6d000
.. ..511: pte 0x0000000021fdb001 pa 0x0000000087f6c000
.. .. ..510: pte 0x0000000021fdd807 pa 0x0000000087f76000
.. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
  
第一行显示 vmprint 的参数。之后每一行对应一个 PTE,包括指向更深层页表页的 PTE。每个 PTE 行前面用若干 " .." 表示其在树中的深度。每个 PTE 行显示该 PTE 在其页表页中的索引、PTE 的位以及从 PTE 中提取的物理地址。不要打印无效的 PTE。在上面的例子中,顶层页表页有入口 0 和 255 的映射。入口 0 的下一层只有索引 0 被映射,而该索引 0 的底层有入口 0、1 和 2 被映射。

你的代码可能会产生与上面显示的不同物理地址。入口数量和虚拟地址应该相同。

一些提示:

结合教材中的图 3-4 解释 vmprint 的输出。页 0 包含什么?页 2 包含什么?在用户模式下运行时,该进程能否读写页 1 映射的内存?

每个进程一个内核页表

Xv6 有一个单一的内核页表,在内核中执行时使用。内核页表是直接映射到物理地址的,因此内核虚拟地址 x 映射到物理地址 x。Xv6 还为每个进程的用户地址空间维护一个独立的页表,其中只包含该进程用户内存的映射,从虚拟地址零开始。由于内核页表不包含这些映射,用户地址在内核中是无效的。因此,当内核需要使用系统调用中传入的用户指针时(例如传递给 write() 的缓冲区指针),内核必须先将该指针转换为物理地址。本节和下一节的目标是允许内核直接解引用用户指针。

你的第一个任务是修改内核,使每个进程在内核中执行时使用自己的内核页表副本。修改 struct proc 以维护每个进程的内核页表,并修改调度器以在切换进程时切换内核页表。在这一步中,每个进程的内核页表应该与现有的全局内核页表完全相同。如果 usertests 能正确运行,你就通过了这部分实验。

请阅读本作业开头提到的书籍章节和代码;理解了虚拟内存的工作原理后,修改虚拟内存代码会更容易。页表设置中的 bug 可能导致由于缺少映射而触发陷阱,可能导致加载和存储操作影响意外的物理内存页,还可能导致从错误的内存页执行指令。

一些提示:

简化 copyin/copyinstr

内核的 copyin 函数读取用户指针指向的内存。它通过将用户指针转换为物理地址来实现,内核可以直接解引用物理地址。它通过在软件中遍历进程页表来完成这一转换。你在本实验这部分的作业是将用户地址映射添加到每个进程的内核页表(在前一节中创建的),使 copyin(以及相关字符串函数 copyinstr)能够直接解引用用户指针。
kernel/vm.ccopyin 的函数体替换为调用 copyin_new(定义在 kernel/vmcopyin.c 中);对 copyinstrcopyinstr_new 做同样的处理。将用户地址的映射添加到每个进程的内核页表,使 copyin_newcopyinstr_new 能够正常工作。如果 usertests 能正确运行且所有 make grade 测试都通过,你就通过了该作业。

该方案依赖于用户虚拟地址范围不与内核用于自身指令和数据的虚拟地址范围重叠。Xv6 使用从零开始的虚拟地址作为用户地址空间,幸运的是内核的内存从更高的地址开始。然而,该方案确实将用户进程的最大大小限制为小于内核的最低虚拟地址。内核启动后,该地址在 xv6 中是 0xC000000,即 PLIC 寄存器的地址;参见 kernel/vm.c 中的 kvminit()kernel/memlayout.h 以及教材中的图 3-4。你需要修改 xv6 以防止用户进程增长超过 PLIC 地址。

一些提示:

Linux 使用了一种与你实现的类似的技术。直到几年前,许多内核在用户空间和内核空间中使用相同的每进程页表,其中包含用户地址和内核地址的映射,以避免在用户空间和内核空间之间切换时必须切换页表。然而,这种设置允许侧信道攻击,如 Meltdown 和 Spectre。

解释为什么 copyin_new() 中第三个测试 srcva + len < srcva 是必要的:给出 srcvalen 的值,使得前两个测试失败(即不会导致返回 -1),但第三个测试为真(导致返回 -1)。

提交实验

实验到此完成。确保你通过了所有 make grade 测试。如果本实验有问题,不要忘记在 answers-lab-name.txt 中写下你的答案。提交你的更改(包括添加 answers-lab-name.txt),然后在实验目录中运行 make handin 来提交你的实验。

花费时间

创建一个新文件 time.txt,在其中写入一个整数,即你在本实验上花费的小时数。不要忘记 git addgit commit 该文件。

提交

你将使用
提交网站来提交你的作业。在提交任何作业或实验之前,你需要从提交网站申请一次 API 密钥。

在提交你对实验的最终更改后,运行 make handin 来提交你的实验。

$ git commit -am "ready to submit my lab"
[util c2e3c8b] ready to submit my lab
 2 files changed, 18 insertions(+), 2 deletions(-)

$ make handin
tar: Removing leading `/' from member names
Get an API key for yourself by visiting https://6828.scripts.mit.edu/2020/handin.py/
Please enter your API key: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 79258  100   239  100 79019    853   275k --:--:-- --:--:-- --:--:--  276k
$
make handin 会将你的 API 密钥存储在 myapi.key 中。如果你需要更改 API 密钥,只需删除此文件并让 make handin 重新生成(myapi.key 不能包含换行符)。

如果你运行 make handin 时有未提交的更改或未跟踪的文件,你将看到类似以下的输出:

 M hello.c
?? bar.c
?? foo.pyc
Untracked files will not be handed in.  Continue? [y/N]
检查上述行,确保你的实验解决方案所需的所有文件都已跟踪,即不在以 ?? 开头的行中列出。你可以使用 git add filenamegit 跟踪你创建的新文件。

如果 make handin 无法正常工作,请尝试修复 curl 或 Git 命令的问题。或者你可以运行 make tarball。这将为你生成一个 tar 文件,然后你可以通过我们的网页界面上传。

可选挑战练习