MIT6.S081 Lab-4: Traps

经历了残酷的Page Tables\text{Page Tables},这一次的lab就显得简单些了。

1. 一些汇编语言\text{1. 一些汇编语言}

略去。

2. Backtrace\text{2. Backtrace}

要求我们编写一个backtrace函数,能够打印函数调用栈信息,从后往前依次输出每个函数调用的返回地址。

只要了解函数调用栈和栈帧的结构即可,特别要注意的是栈指针从前往后是递减的。

1
2
3
4
5
6
7
8
9
10
11
12
13
void backtrace()
{
uint64 fp = r_fp();
uint64 up = PGROUNDUP(fp);
uint64 low = PGROUNDDOWN(fp);
printf("backtrace:\n");
while(fp < up && fp >= low)
{
uint64 retaddr = *((uint64*)(fp - 8));
printf("%p\n", retaddr);
fp = *((uint64*)(fp - 16));
}
}

3. alarm\text{3. alarm}

你说它难吧。。。它像也不难。但说它不难吧,它也没那么简单。。。

我们需要实现sigalarmsigreturn两个系统调用。其中sigalarm接受一个整数t和一个无参数的void函数指针handler,调用该系统调用以后,每隔tCPU ticks\text{CPU ticks}就调用一次handler指向的函数,并在执行handler结束后继续程序运行,直到调用sigalarm(0, 0)

这里约定,handler使用sigreturn结束函数执行。

3.1. 初步实现sigalarm的周期性调用\text{3.1. 初步实现sigalarm的周期性调用}

按照Lab-2\text{Lab-2}中走过的写系统调用的流程,我们可以很容易地把两个系统调用的框架都写好,接下来的问题是怎么。我们分布走,先初步实现sigalarm使其周期性调用handler,暂时不考虑怎么让程序继续运行。这一步里,暂时可以直接令sigreturn返回00

trap.c\text{trap.c}usertrap函数中,有一个对于trap\text{trap}原因的集中判断,其中就有对于来自设备的中断判断。找到语句if(which_dev == 2),这就是判断来自时钟的中断,每发生一次就意味着经过了一个CPU tick\text{CPU tick}

看这里的原始代码,可以发现,我们的内核每经过一次CPU tick\text{CPU tick}就要调用一次yield函数,重新调度进程,以实现多个进程的并发,不过这并不是本次重点。我们只需要给进程结构体加一个字段ticks作为计数器,在每次发生时钟中断的时候给计数器加一,再加两个字段表示handler和调用handler的时间间隔,当ticks达到时间间隔则进行操作并归零即可实现周期性调用。

所以这一块的代码可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(which_dev == 2)
{
if(p->intv)
{
p->ticks++;
if(p->ticks == p->intv)
{
... // do something to call handler
}
else yield();
}
else yield();
}

sys_sigalarm非常简单,只是修改几个字段,此处就不列出了。

接下来考虑怎么调用handlerhandler,其实非常简单。在trap\text{trap}结束时我们将从内核返回用户空间,并恢复所有保存在trapframe\text{trapframe}中的状态,当然也要恢复程序计数器PC\text{PC}作为下一条指令地址。我们只需要将trapframe\text{trapframe}中的相关字段epc修改为handler就能在返回用户空间时直接执行handler指向的函数的第一条指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(which_dev == 2)
{
if(p->intv)
{
p->ticks++;
if(p->ticks == p->intv)
{
p->trapframe->epc = (uint64)(p->handler);
p->ticks = 0;
}
else yield();
}
else yield();
}

这样我们就能顺利通过test0\text{test0},但是剩下两个测试就挂了,这很正常。

3.2. 使程序能够继续运行,并且防止handler重复调用\text{3.2. 使程序能够继续运行,并且防止handler重复调用}

我们在上面做的这个事情虽然实现了周期性调用,但是保证不了程序能在handler结束后继续运行。一个很简单的理由是,时钟中断的发生是程序无法预料的,因此函数调用之前寄存器的保存和完成之后寄存器的恢复在发生时钟中断并且调用handler的时候做不了。这时候我们就需要sigreturnusertrap的配合来完成这些寄存器的保存和恢复。此外,我们不希望在一个handler返回之前重复调用handler,这一点也没有保证。

test1\text{test1}主要测试程序能否正常继续运行,test2\text{test2}主要测试handler是否会被重复调用。后者是很容易的,我们开一个字段表示handler是否返回,在usertrap中标记为未返回,在sigreturn中标记为返回,若未返回则在时钟中断时不执行handler即可。

真正困难的在于test1\text{test1}。我们很容易想到添加一个字段sigret_pc用来保存执行handlertrapframeepc字段,在上面我们修改了这个字段,丢弃了原始值,这个原始值指向发生中断前的下一条指令,也就是我们执行完handler之后应当返回的地址,需要将其保存。接下来可能会想到将sigret_pc赋值给trapframera字段,在时钟中断结束并恢复寄存器后使之成为执行handler结束后的返回地址。

但是这么做是错误的。如果在发生时钟中断的时刻,PC\text{PC}寄存器指向另一个函数foo的第一条指令,那么当我们执行完毕handlerra的值是foo的首地址,返回到foo的首地址并依次执行foo的指令。然而ra寄存器是由调用者保存的,因此接下来foo一定会有这样的代码————在执行其他语句之前,先创建栈帧,将ra保存在栈帧中,然后执行结束后将ra恢复为栈帧中保存的值,弹栈并且跳转,但是ra还是会指向foo的首地址,于是我们回到了foo函数的开头,创建栈帧并执行foo函数体,调用结束后再重复。。。整个过程不会产生栈空间的额外增长,因为我们总是在不断地创建栈帧并弹出栈帧,因此不会爆栈,只会成为一个没有边界的尾递归,一直执行下去你永远也无法达到跳出函数的真实

为了避免黄金体验镇魂曲这种无穷尾递归,正确的做法是在sigreturn中通过设置trapframeepc字段来强制跳转。只要我们能够完美地保存进程中断时的寄存器状态,并在sigreturn中恢复进程上下文就能正确地继续运行。具体而言,整个过程是这样的:

程序执行发生时钟中断保存程序的trapframe保存并修改trapframe的epc,\text{程序执行}\rightarrow\text{发生时钟中断}\rightarrow\text{保存程序的trapframe}\rightarrow\text{保存并修改trapframe的epc,}

保存必要寄存器返回用户空间,跳转至handler执行handlersigreturn系统调用,用户trap\text{保存必要寄存器}\rightarrow\text{返回用户空间,跳转至handler}\rightarrow\text{执行handler}\rightarrow\text{sigreturn系统调用,用户trap}

保存handler的trapframe执行sigreturn,将handler的trapframe恢复为之前的保存值,\rightarrow\text{保存handler的trapframe} \rightarrow\text{执行sigreturn,将handler的trapframe恢复为之前的保存值,}

修改handler的trapframe的epc恢复为保存值sigreturn系统调用结束,返回用户空间\text{修改handler的trapframe的epc恢复为保存值}\rightarrow\text{sigreturn系统调用结束,返回用户空间}\rightarrow

将必要寄存器恢复为发生时钟中断时的值,pc也被恢复为时钟中断时的值\text{将必要寄存器恢复为发生时钟中断时的值,pc也被恢复为时钟中断时的值}

于是就可以继续运行了。

接下来的问题是哪些必要的寄存器是我们需要保存并恢复的,我也懒得仔细想,前面说过,程序无法预判何时会中断,于是我把除了内核寄存器以外的寄存器全部保存了(

有可能不精细,但是你就说对不对吧(

于是代码就很好写了,注意这里我们要保存的值很多,所以建议和trapframe一样开指针免得proc结构体太大,在跑usertests的时候吃了这个亏。

首先是proc.h\text{proc.h}中的修改部分,添加了一个结构体定义,并给proc添加了前面提到的必要字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
struct sigframe
{
uint64 ra;
uint64 sp;
uint64 gp;
uint64 tp;
uint64 t0;
uint64 t1;
uint64 t2;
uint64 s0;
uint64 s1;
uint64 a0;
uint64 a1;
uint64 a2;
uint64 a3;
uint64 a4;
uint64 a5;
uint64 a6;
uint64 a7;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
uint64 t3;
uint64 t4;
uint64 t5;
uint64 t6;
};

enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID

// these are private to the process, so p->lock need not be held.
int intv; // alarm interval
int ticks; // ticks after last time handler is called
int handler_ret; // whether a handler has returned
uint64 sigret_pc; // pc after returning from sigreturn
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct sigframe *sigframe; // data saved for sigalarm and sigreturn
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
void (*handler)(); // handler function
char name[16]; // Process name (debugging)
};

然后是trap.c\text{trap.c}中完整版的时钟中断处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if(which_dev == 2)
{
if(p->intv)
{
p->ticks++;
if(p->ticks == p->intv && p->handler_ret)
{
// p->trapframe->epc stores the pc when trap occurred
// we will recover it when sigreturn is called
savesigreg();// copy regs, it is easy so the definition isn't shown here~
p->sigret_pc = p->trapframe->epc;
p->trapframe->epc = (uint64)(p->handler);
p->handler_ret = 0;
p->ticks = 0;
}
else yield();
}
else yield();
}

最后是sys_sigreturn函数

1
2
3
4
5
6
7
8
9
uint64
sys_sigreturn(void)
{
struct proc *p = myproc();
... // what should we be do here? :)
p->trapframe->epc = p->sigret_pc;
p->handler_ret = 1;
return 0;
}

这样差不多就做完了,相比于前一次难度上天的Page Table Lab\text{Page Table Lab},这次实验确实好了一些。