MIT6.S081 ---- Lab traps
Lab traps
RISC-V assembly
阅读 call.asm
的 f(),g()
函数,回答一些问题:
Backtrace
backtrace 对于 debug 很有用:在 error 发生时输出一列栈上的函数调用链。
编译器在每个 stack frame 中存放有一个 frame 指针(To Prev.Frame(fp)),这个 fp 指针指向调用者的 frame pointer 。本实验需要使用这些 fp 遍历 stack 区域然后打印每个 stack frame 保存的返回地址(return address)。
stack frames 的布局:
--->Return Address
| To Prev.Frame(fp)
| Saved Registers
| Local Variables
| ...
| Return Address
----To Prev.Frame(fp)
Saved Registers
Local Variables
...
$sp
指向的是当前 stack frame 的底部(低地址)
$fp
指向的是当前 stack frame 的顶部(高地址)
当前 $fp - 8
的位置是 Return Address , $fp - 16
的位置是 To Prev.Frame(fp)。
void
backtrace(void)
{
printf("backtrace:\n");
uint64 fp = r_fp(); // top address of current stack frame
uint64 stackpagetop = PGROUNDUP(fp); // top address of stack page
while (fp < stackpagetop)
{
printf("%p\n", *(uint64 *)(fp - 8)); // Saved registers: $ra
fp = *(uint64 *)(fp - 16); // Saved registers: To Prev.Frame
}
}
Alarm
为xv6增加一个特性:定期向一个使用CPU时间的进程发出警报。对于想限制占用CPU时间的进程很有用,对于想进行计算又想执行周期任务的进程很有用。
更一般的说:实现一个原始的用户级 interrupt/fault handlers 。也可以使用类似的方法处理应用中的缺页。
需要增加一个新的 sigalarm(interval, handler)
系统调用。如果一个应用调用 sigalarm(n, fn)
,那么应用程序每消耗 \(n\) 个ticks,内核需要让应用程序执行函数 fn(test0)
。当 fn
返回时,应用程序需要恢复 fn
函数执行之前的代码执行(test1/test2的实验目标)。tick 是 xv6 的时间单元,由硬件时钟产生中断决定。如果应用程序调用 sigalarm(0,0)
,内核需要停止生成周期时钟调用。
kernel/proc.h
中定义alarm相关属性。
histrapframe
用于在 \(n\) 个 ticks 后,执行fn
前,在内核中保存用户态的用户寄存器,因为此次返回用户态并不是为了恢复正常代码的执行,而是为了执行fn
。在fn
中必须有sigreturn
系统调用再次进入内核,同时用histrapframe
恢复原用户态的寄存器状态,从而在返回用户态时恢复正常执行。流程为:原流程代码 --> 调用sigalarm系统调用 --> 进入内核设置alarm相关数据--> 返回用户态执行原流程代码 --> ticks 到,因为时钟中断,所以此时在内核态,设置 $sepc 指向 fn,保存原 trapframe --> 返回用户态,但是执行的是fn,而不是原流程代码 --> 在 fn 中通过 sigreturn 再次进入内核态,恢复原流程代码的trapframe --> 返回用户态执行原流程代码
alarmisreturn
用来标记 handler 是否返回,如果 handler 没有返回,则内核不能再次调用它。如 handler 的处理时间大于给定时间 \(n\) ticks 的时候:在用户态处理 handler 时,因为 ticks 到期再次 trap 进入内核,尽管时钟满足条件,但是 handler 没有返回,所以内核不应该再次调用 handler。
(分析下,实验这样限制的原因:当一个handler没有结束,再响应一个handler时,新的handler会污染原handler的PC和trapframe,造成程序执行异常。)alarminterval
表示周期数 \(n\),alarmpassed
表示当前经过的时间,(*alarmhandler)()
表示fn的函数指针。
int alarminterval; // sigalarm alarm interval
int alarmpassed; // sigalarm passed time
void (*alarmhandler)(); // sigalarm handler
struct trapframe histrapframe; // save trapframe to retrieve to original code from alarm handler.
int alarmisreturn; // 0 is not return so that can not re-enter handler. 1 can do.
kernel/sysproc.c
:取出系统调用的参数,配置 proc 的 alarm 相关属性。
// After every n "ticks" of CPU time that the program consumes,
// the kernel should call fn.
uint64
sys_sigalarm(void)
{
int n;
uint64 addr;
if (argint(0, &n) < 0 || argaddr(1, &addr) < 0)
return -1;
myproc()->alarminterval = n;
myproc()->alarmpassed = 0;
myproc()->alarmisreturn = 1;
myproc()->alarmhandler = (void(*)())addr;
return 0;
}
uint64
sys_sigreturn(void)
{
*(myproc()->trapframe) = myproc()->histrapframe;
myproc()->alarmpassed = 0;
myproc()->alarmisreturn = 1;
return 0;
}
kernel/trap.c
:判断是否应该执行 alarm handler(时钟周期到且上一个alarm handler已经返回)。保存 trapframe 以便 sigreturn
中返回,记录当前 alarm handler 没有返回,防止后续 handler 的“覆盖“执行。
// give up the CPU if this is a timer interrupt.
if (which_dev == 2) {
if (p->alarminterval > 0) {
p->alarmpassed++;
if (p->alarmisreturn && p->alarmpassed >= p->alarminterval) {
p->histrapframe = *(p->trapframe);
p->trapframe->epc = (uint64)p->alarmhandler;
p->alarmisreturn = 0;
}
}