[MIT6.S081] 还行还行不太难 · Lab4 · traps

452 阅读4分钟

La4 讲义

比起页表的Lab,这个Lab的难度要小不少。

RISC-V assembly (easy)

Backtrace (moderate)

对于这个题,要区分清楚内核栈的虚拟地址和存储内核代码的内存空间的虚拟地址,前者的pattern是0x0000003fffffxxxx,后者的pattern是0x000000008000xxxx。在Lab3 page table中,我们知道内核栈占据的虚拟地址空间是在trampoline之后。显然,这里内核栈的地址与这个做法中内核栈应该有的地址是相匹配的。而内核空间从0x0000000080000000开始(KERNELBASE = 0x0000000080000000),text segment就是由KERNELBASE开始的,这个也是匹配的。

完成本题主要需要两个信息,一是调用当前函数的函数对应栈帧的起始地址,用于递归得到下一个输出对应的函数的信息;二是调用当前函数的指令的地址(即本函数执行完成后,需要返回的指令的地址,这个指令是在text segment中的,指令的地址保存在Return Address中),这是需要输出的内容。这两者都可以从用户栈中得到,见下图用户栈的结构。 pic_kernel_stack.png 然后,只要想清楚指针指针指向地址保存的内容这两者的关系,就能做对本题。

各个函数的调用顺序是trap.cusertrap() 调用 syscall() , syscall() 调用 sysproc.csys_sleep(),在正确完成代码后,运行bttest可以看到backtrace()的输出顺序与调用顺序是恰好相反。

Alarm (hard)

本题主要需要解决的问题是两个:一是如何在计时中断发生时调用handler函数,二是handler函数结束后如何返回到原来正在执行的指令。

如何在计时中断发生时调用handler函数?

首先,我们回顾一下系统调用引起的陷入的过程: 调用系统调用后,首先会进入到trampoline中(trampoline的地址由vec寄存器中的值指定)。在trampoline中,epc寄存器保存了pc寄存器中的值,即发生陷入前用户进程正在执行的指令的地址。在usertrap()中,将epc寄存器的值保存在p->trapframe->epc中,并将p->trapframe->epc的值+4以保存下一条指令的地址。当要返回用户态时,usertrapret()将p->trapframe->epc的值加载回epc寄存器中。trampoline会再将epc寄存器的值复制到pc寄存器中。由此返回用户态后,就可以继续执行发生陷入前的指令的下一条指令了。

而现在我们是想在计时中断发生时,调用handler函数。十分简单,只需要将p->trapframe->epc保存的值换为handler函数的地址即可。

handler函数结束后如何返回到原来正在执行的指令?

只要保存好发生中断的时候的上下文,handler函数结束后恢复该上下文即可。 具体而言,可以在struct proc中维护一个origin_trapframe。在跳转到handler()之前将trapframe的内容保存到origin_trapframe中,当从handler()调用sigreturn()时,我们再将origin_frame的内容恢复到trapframe中就可以了。

需要注意的是,系统调用引起的陷入,在返回用户态后,应该执行下一条指令;而计时中断引起的陷入结束后,应该继续执行当前指令。

遇到的一些bugs

1. alarmtest test1 不通过

现象: 运行alarmtest,输出错误

test1 failed: foo() executed fewer times than it was called

分析: alarmtest的test1的注释中有提到:导致个错误的原因之一是计时中断返回后,寄存器的值没有被正确地还原。经过检查,发现是我直接将trapframe赋值给了origin_trapframe,即只进行了一个浅拷贝。

解决: 要将trapframe成员的值复制到origin_trapframe的成员上。

2. alarmtest test2 不通过

现象: 运行alarmtest,输出错误

test2 failed: alarm handler called more than once

分析: 通过阅读Lab的讲义和alarmtest.c,知道出现该错误的原因是在handler()函数还未返回的时候,又重入了handler()函数。

解决: 在struct proc中再加入一个标志位用于记录sigreturn()是否已经调用(sigreturn()调用过说明handler()函数已经结束并返回了),只有在已经调用过的情况下才再次调用handler()函数。

3. usertests 不通过

现象: 运行usertests,所有test都通过了,最后输出错误

FAILED -- lost some free pages 26018 (out of 32456)

分析: 从usertests.c可以看到:在测试前,统计空闲页的数量并存储在free0变量中;在测试后,再次统计空闲页数量并存储在free1变量中。若free1与free0的值不相同,则会输出该错误。

// /user/usertests.c
// ...
int free0 = countfree();
int free1 = 0;
int fail = 0;
// tests...

if(fail){
// ...
} else if((free1 = countfree()) < free0){
	printf("FAILED -- lost some free pages %d (out of %d)\n", free1, free0);
	exit(1);
} else {
	printf("ALL TESTS PASSED\n");
	exit(0);
}

在本lab中,并不涉及页表的修改,因此最有可能导致该错误的原因是申请了一些物理页,但在使用后没有释放。我是没有在进程结束后释放p->origin_trapframe(用作在记录发生计时中断前的上下文)。

解决: 在kernel/proc.c中的freeproc()函数中释放掉p->origin_trapframe占用内存空间。