【MIT6.S081】Lab4 trap alarm

Alarm综合了该lab中前几个练习的知识点:系统调用、中断、寄存器等,我们需要对trap机制有比较好的认识才能理解。Alarm的任务是需要我们完成一个定时器的实现:sigalarm(interval, handler),当调用sigalarm(n, fn)时,内核会每n个时间间隔(tick)执行fn函数。

该实验的代码实现见:仓库commit

如何理解Alarm

“内核会每n个时间间隔执行fn函数”,如何理解这“每n个时间间隔”呢?在计算机中有一个“时钟周期”的概念,而我们这里所说的时间间隔就是xv6中设置的每次发生时钟中断所间隔的始终周期。在xv6内核初始化时会执行timerinit()函数,其中有:

1
2
3
// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

这里就是设置了xv6会每经过interval个时钟周期(在qemu中大概为0.1秒)进行一次时钟中断,这个中断是硬件自动执行的(在没看源码之前,一直没搞懂xv6是什么时候、怎么进行的时钟中断)。下文将以时钟中断的间隔tick作为基本单位。

所以简单来说,Alarm需要我们在内核中进行计数,每当经过n个tick的时候,需要去执行fn函数。

test0~3所做的事

  • test0:用于测试我们的sigalarm是否起作用了;
  • test1:用于测试内核是否多次调用处理函数,需要确保中断发生时跳转的地址为处理函数所在的地址,还有中断时需要保存好之前寄存器中的值;
  • test2:用于测试内核不允许重入sysalarm系统调用,即若某个进程正在执行处理函数,那么内核就不应该再次调用它;
  • test3:用于测试sys_sigreturn系统调用能否正确返回寄存器a0的值。

实现

注册系统调用

首先的注册系统调用的步骤这里就不展开了,具体可参考之前的lab。

struc proc结构体

为了完成定时执行某个函数,我们需要在struct proc结构体中加入一些成员:

1
2
3
4
5
6
7
8
struct proc {
...
int is_handling; // 用于判断当前进程是否正在执行处理函数
int tick_interval; // 定时器间隔,由系统sigalarm的第一个参数传入
int tick_counter; // 定时器计数器,每次tick进行+1
uint64 tick_handler; // 间隔到了后执行的处理函数
struct trapframe *saved_trapframe; // 保存寄存器
}

值的注意的是处理函数的类型我们设置为了uint64,为什么不是一个函数指针呢?其实都差不多,在之后设置跳转处理函数的时候,就是通过地址来跳转,而地址在xv6中就是用的uint64来表示。故这里的设置即是处理函数所在的起始地址。

实现sys_sigalarm

sysproc.c中实现sys_sigalarm()函数:

1
2
3
4
5
6
uint64 sys_sigalarm(void) {
struct proc *p = myproc();
argint(0, &(p->tick_interval));
argaddr(1, &(p->tick_handler));
return 0;
}

该函数要做的事情很简单,只需要接收从用户态传来的两个参数,并将其赋值给当前进程的tick_intervaltick_handler

进程初始化与结束销毁

接着需要在进程创建和销毁时对这些变量进行相应的初始化和清零:

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
static struct proc*
allocproc(void)
{
...
p->tick_interval = 0;
p->tick_counter = 0;
p->tick_handler = 0;
p->is_handling = 0;
...
// Allocate a saved_trapframe page.
if((p->saved_trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
...
}

static void
freeproc(struct proc *p)
{
...
if(p->saved_trapframe)
kfree((void*)p->saved_trapframe);
p->saved_trapframe = 0;
...
p->tick_counter = 0;
p->tick_interval = 0;
p->tick_handler = 0;
p->is_handling = 0;
}

补全usertrap

接下来就是需要实现在usertrap中处理时钟中断,在实验指导书中提示我们在if(which_dev == 2) ...中处理时钟中断。这里我的处理逻辑为,当时钟中断发生时:

  1. 当前进程的tick计数器++
  2. 判断进程设置的定时器间隔是否不为0、当前计数器是否已经经过了interval个间隔、且当前进程未执行处理函数,如果其中一项不满足,则不进行第三步
  3. 将当前进程的trapframe的内容(即寄存器的值)保存到saved_trapframe(用于恢复现场),将SEPC寄存器的值设置为处理函数的地址,这样中断结束返回时就会去执行处理函数了,最后设置当前进程“正在执行处理函数”

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if (which_dev == 2) {
p->tick_counter++;
if (p->tick_interval && p->tick_counter % p->tick_interval == 0 && p->is_handling == 0) {
memmove(p->saved_trapframe, p->trapframe, PGSIZE);
p->trapframe->epc = p->tick_handler;
p->is_handling = 1;
}
yield();
}
...

在第二步中不知道是xv6的bug还是我有地方没理解好,如果不先检查tick_interval != 0,则可能在执行p->tick_counter % p->tick_interval会有问题,因为取模运算符%在分母为0时是未定义的,但这么运行时xv6并没有任何错误。

返回,恢复现场

在处理函数执行的最后,将会执行sigreturn系统调用进行返回并恢复现场,这时我们就可以将之前存放在saved_trapframe中的值拷贝回trapframe中,并设置当前进程“未执行处理函数”。实验指导书中还提示我们最终返回的结果为a0寄存器中的值。

1
2
3
4
5
6
uint64 sys_sigreturn(void) {
struct proc *p = myproc();
memmove(p->trapframe, p->saved_trapframe, PGSIZE);
p->is_handling = 0;
return p->trapframe->a0; // return this for alarm test3
}