实验:陷阱

本实验探索如何使用陷阱来实现系统调用。你将首先完成关于栈的热身练习,然后实现一个用户级陷阱处理的示例。

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

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

  $ git fetch
  $ git checkout traps
  $ make clean
  

RISC-V 汇编

了解一些 RISC-V 汇编很重要,你在 6.004 中已经接触过。你的 xv6 仓库中有一个 user/call.c 文件。make fs.img 会编译它,并在 user/call.asm 中生成该程序的可读汇编版本。

阅读 call.asm 中 gfmain 函数的代码。RISC-V 的指令手册在参考页面上。以下是你应该回答的一些问题(将答案存储在 answers-traps.txt 文件中):

哪些寄存器包含函数的参数?例如,在 main 调用 printf 时,哪个寄存器保存了 13?
在 main 的汇编代码中,对函数 f 的调用在哪里?对 g 的调用在哪里?(提示:编译器可能会内联函数。)
函数 printf 位于什么地址?
mainjalr 调用 printf 之后,寄存器 ra 中的值是什么?

运行以下代码。

	unsigned int i = 0x00646c72;
	printf("H%x Wo%s", 57616, &i);
      
输出是什么? 这是一个 ASCII 码表,映射字节到字符。

输出取决于 RISC-V 是小端序这一事实。如果 RISC-V 是大端序,你应该将 i 设置为什么值才能得到相同的输出?你需要将 57616 改为不同的值吗?

这是小端序和大端序的描述以及更有趣的描述

在以下代码中,'y=' 之后会打印什么?(注意:答案不是一个具体的值。)为什么会这样?

	printf("x=%d y=%d", 3);
      

回溯

在调试时,拥有回溯信息通常很有用:它是栈上从错误发生点往上的函数调用列表。

kernel/printf.c 中实现一个 backtrace() 函数。在 sys_sleep 中插入对该函数的调用,然后运行 bttest,它会调用 sys_sleep。你的输出应该如下所示:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
  
bttest 之后退出 qemu。在你的终端中:地址可能略有不同,但如果你运行 addr2line -e kernel/kernel(或 riscv64-unknown-elf-addr2line -e kernel/kernel)并粘贴上述地址,如下所示:
    $ addr2line -e kernel/kernel
    0x0000000080002de2
    0x0000000080002f4a
    0x0000000080002bfc
    Ctrl-D
  
你应该看到类似这样的内容:
    kernel/sysproc.c:74
    kernel/syscall.c:224
    kernel/trap.c:85
  

编译器在每个栈帧中放置一个帧指针,保存调用者的帧指针地址。你的 backtrace 应该使用这些帧指针沿栈向上遍历,并打印每个栈帧中保存的返回地址。

一些提示:

当你的回溯功能正常工作后,从 kernel/printf.c 中的 panic 调用它,这样当内核 panic 时你就能看到内核的回溯信息。

闹钟

在本练习中,你将向 xv6 添加一个功能,在进程使用 CPU 时间时定期发出警报。这对于想要限制 CPU 时间消耗量的计算密集型进程可能很有用,或者对于想要计算但也想定期执行某些操作的进程也很有用。更一般地说,你将实现一种原始形式的用户级中断/异常处理程序;例如,你可以使用类似的方法来处理应用程序中的缺页异常。如果你的解决方案通过了 alarmtest 和 usertests,就是正确的。

你应该添加一个新的 sigalarm(interval, handler) 系统调用。如果应用程序调用 sigalarm(n, fn),那么在程序每消耗 n 个 CPU 时间"滴答"后,内核应该调用应用程序函数 fn。当 fn 返回时,应用程序应该从中断处继续执行。滴答是 xv6 中一个相当随意的时间单位,由硬件定时器产生中断的频率决定。如果应用程序调用 sigalarm(0, 0),内核应该停止产生周期性闹钟调用。

你会在你的 xv6 仓库中找到一个 user/alarmtest.c 文件。将其添加到 Makefile 中。在你添加 sigalarmsigreturn 系统调用之前(见下文),它无法正确编译。

alarmtesttest0 中调用 sigalarm(2, periodic) 来请求内核每 2 个滴答强制调用一次 periodic(),然后自旋一段时间。你可以在 user/alarmtest.asm 中查看 alarmtest 的汇编代码,这对调试可能很有用。当 alarmtest 产生如下输出且 usertests 也能正确运行时,你的解决方案就是正确的:

$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$

完成后,你的解决方案只需要几行代码,但可能很难写对。我们将使用原始仓库中的 alarmtest.c 版本来测试你的代码。你可以修改 alarmtest.c 来帮助调试,但确保原始 alarmtest 显示所有测试都通过。

test0:调用处理程序

首先修改内核以跳转到用户空间中的闹钟处理程序,这将使 test0 打印 "alarm!"。先不要担心 "alarm!" 输出之后会发生什么;如果你的程序在打印 "alarm!" 后崩溃,目前是可以的。以下是一些提示:

test1/test2():恢复被中断的代码

alarmtest 很可能在打印 "alarm!" 后在 test0 或 test1 中崩溃,或者 alarmtest(最终)打印 "test1 failed",或者 alarmtest 在没有打印 "test1 passed" 的情况下退出。要修复这个问题,你必须确保当闹钟处理程序完成时,控制返回到用户程序最初被定时器中断的指令处。你必须确保寄存器内容恢复到中断时的值,以便用户程序在闹钟之后能够不受干扰地继续执行。最后,你应该在每次闹钟触发后重新"设置"闹钟计数器,使处理程序被周期性地调用。

作为起点,我们已经为你做了一个设计决定:用户闹钟处理程序在完成后必须调用 sigreturn 系统调用。查看 alarmtest.c 中的 periodic 作为示例。这意味着你可以在 usertrapsys_sigreturn 中添加代码,使它们协作让用户进程在处理完闹钟后正确恢复。

一些提示:

通过了 test0test1test2 后,运行 usertests 确保你没有破坏内核的其他部分。

提交实验

实验到此完成。确保你通过了所有 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 文件,然后你可以通过我们的网页界面上传。

可选挑战练习