This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.
lab4 相对 lab3 要简单很多,需要写的代码行数不多,并且给的 hints 也很直接。只要听了 lecture 5 和 lecture 6 都能够明白其中的原理。这两次课分别讲了 xv6 的栈结构跟 trap 的一些细节。
RISC-V assembly (easy)
就是去看 user/call.c 和 user/call.asm 中的函数 g, f, main,然后回答一些问题。我没有看 RISC-V 的手册,太长了有点不好搜索。有不懂的指令去 Google 一下反而还方便一点。这边就直接贴出答案了,第三题的答案可能会不一样。
1 | Q: Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf? |
Backtrace (moderate)
这部分是实现一个类似于 gdb 的 backtrace,打印函数调用栈帧中的返回地址。在 kernel/printf.c 中添加一个 backtrace() 函数,然后在 sys_sleep 中调用 进行测试。
要实现 backtrace 首先要理解 xv6 的函数栈帧结构,根据老师上课的 ppt:
可以看到,函数栈帧之间其实就像一个链表,被 fp 指针给连接到一起,所以我们只需要拿到当前函数的 fp,就可以一直遍历整个函数调用链。当前的 fp 被存放在 s0 寄存器中。在 kernel/risv.h 中添加以下函数来取出 s0 中的值:
1 | static inline uint64 |
在栈中,函数的返回地址固定在 fp - 8 的位置,调用该函数的函数的 fp 在 fp - 16 的位置。我们一直遍历到这一页结束就可以了。
1 | void |
Alarm (hard)
这部分就是有难度的地方了。要实现一个用户级的中断,用户程序通过调用 sigalarm(interval, handler) 系统调用来设置一个计时器,每隔 interval 个 tick,就调用 handler 函数进行处理。并且在 handler 函数返回之后,会调用 sigreturn() 函数,被中断的用户程序要正常恢复执行,也就是说对于被中断的用户程序来说 alarm handler是透明的。
每次在收到 timer 硬件发出的中断时,就是一个 tick,它不是一个确定的时间单位。而 timer interrupt 会触发 trap,所以可以知道 alarm handler 是要在 trap 中触发的,类似于在 trap 中又套了一层 trap。
我们需要在 struct proc 结构体中添加一些新的属性来保存触发 handler 的间隔,handler 函数指针,距离上次调用 handler 过去了几个 tick,当前是否有 handler 在执行,以及一个 struct trapframe 用来保存被打断的程序的寄存器值:
1 | // ... struct proc |
ticks、handler、passedticks 在 sigalarm 系统调用中设置:
1 | uint64 |
接着在 kernel/trap.c#usertrap 中处理 timer interrupt。当 which_dev = 2 的时候,表明是 timer interrupt。
由于执行完 handler 之后,需要回到被中断的程序继续执行,所以在这里要将这个程序的状态保存下来,在 sigreturn 系统调用中恢复。我这里图方便就直接将所有的寄存器和状态都保存下来了。保存完状态后,将 p->trapframe->epc 的值设置为 handler 的地址。usertrap 会调用 usertrapret,usertrapret 会将 p->trapframe->epc 设置到 spec 寄存器中,userret 在返回时调用 sret 指令会将 sepc 寄存器中的值设置到 pc 中,所以用户程序就会跳到 handler 中去执行了。
1 | // ok |
在 handler 程序执行完后,在测试中就是 periodic 函数,其中会调用 sigreturn,在这里面回复原来的函数的执行。
首先将 passedticks 重置,以便下一轮触发,然后将 isrunninghandler 置 0,表示没有在执行的 handler,然后将保存在 savedfp 中的寄存器值都重新写入 trapframe 中,接着调用 usertrapret 将寄存器的值都恢复,就像从 trap 中恢复出来一样。
1 | uint64 |
总结
这次的 lab 其实就像老师喂饼一样,看过 lecture5/6 之后,再把 xv6 book Chapter4 看完,其中的原理肯定会明白,然后就是一些小细节可能会为难一点(C 语言不太好的甚至会被语法为难😭),其它难点不多,但是确实对 trap 有深入的理解。Robert 我的神!