【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 | // ask the CLINT for a timer interrupt. |
这里就是设置了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 | struct proc { |
值的注意的是处理函数的类型我们设置为了uint64
,为什么不是一个函数指针呢?其实都差不多,在之后设置跳转处理函数的时候,就是通过地址来跳转,而地址在xv6中就是用的uint64来表示。故这里的设置即是处理函数所在的起始地址。
实现sys_sigalarm
在sysproc.c
中实现sys_sigalarm()
函数:
1 | uint64 sys_sigalarm(void) { |
该函数要做的事情很简单,只需要接收从用户态传来的两个参数,并将其赋值给当前进程的tick_interval
和tick_handler
。
进程初始化与结束销毁
接着需要在进程创建和销毁时对这些变量进行相应的初始化和清零:
1 | static struct proc* |
补全usertrap
接下来就是需要实现在usertrap中处理时钟中断,在实验指导书中提示我们在if(which_dev == 2) ...
中处理时钟中断。这里我的处理逻辑为,当时钟中断发生时:
- 当前进程的tick计数器++
- 判断进程设置的定时器间隔是否不为0、当前计数器是否已经经过了interval个间隔、且当前进程未执行处理函数,如果其中一项不满足,则不进行第三步
- 将当前进程的trapframe的内容(即寄存器的值)保存到saved_trapframe(用于恢复现场),将
SEPC
寄存器的值设置为处理函数的地址,这样中断结束返回时就会去执行处理函数了,最后设置当前进程“正在执行处理函数”
代码如下:
1 | void |
在第二步中不知道是xv6的bug还是我有地方没理解好,如果不先检查
tick_interval != 0
,则可能在执行p->tick_counter % p->tick_interval
会有问题,因为取模运算符%
在分母为0时是未定义的,但这么运行时xv6并没有任何错误。
返回,恢复现场
在处理函数执行的最后,将会执行sigreturn
系统调用进行返回并恢复现场,这时我们就可以将之前存放在saved_trapframe中的值拷贝回trapframe中,并设置当前进程“未执行处理函数”。实验指导书中还提示我们最终返回的结果为a0寄存器中的值。
1 | uint64 sys_sigreturn(void) { |