距离深圳Shopee 9.19大裁员已经过了一周… 人还是有点懵懵的,只知道先走的是我,没想到mentor也走了,连带着走了好几个小伙伴,前阵子真的有些致郁,也不知道两个月左右的经验能面个锤子社招… 大概真的是部门问题吧,现在真的就走一步看一步了

课程相关的资源、视频和教材见Lab util: Unix utilities开头。

这次的Lab主要考察RISC-V的陷阱处理机制,在课程中以系统调用为例,介绍了从用户态切换内核态并最终退出的全过程。

准备

对应课程

本次的作业是Lab Traps,我们将探索陷阱处理机制等内容,并最终实现一个用户级别的陷阱处理。

我们需要看掉Lecture 5和Lecture 6,也就是B站课程的P4和P5。其中P4主要在讨论RISC-V汇编指令下,栈内存、栈帧相关知识以及调用Call的一些习惯,然后更多的时间花在了介绍如何使用GDB对xv6操作系统进行调试,这个就很优雅。而P5才是这次的主要内容,以一个系统调用为例,主要讲述了用户态和内核态之间是如何进行切换的,信息量还是有点大的,建议多过几遍…

另外请阅读教材第四章Traps and system calls,了解相关内容。

系统环境

使用Arch Linux虚拟机作为实验环境…

环境依赖配置,编译使用make qemu等,如遇到问题,见第一次的Lab util: Unix utilities

使用准备

参考本次的实验说明书:Lab traps: Traps

从仓库clone下来课程文件:

1
git clone git://g.csail.mit.edu/xv6-labs-2021

切换到本次作业的traps分支即可:

1
2
cd xv6-labs-2021
git checkout traps

在作业完成后可以使用make grade对所有结果进行评分。

题目

RISC-V汇编简答题

要求和提示

在本部分中将给出一段RISC-V汇编代码,通过阅读代码我们要回答几个问题,并把答案存储在主目录下的answers-traps.txt下。

我们在主目录下执行:

1
make fs.img

编译user/call.c,并生成汇编文件user/call.asm,我们需要观察call.asm下的g, fmain函数。RISC-V的指令手册可参考reference page

问题1

请观察在call.asm下的main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
000000000000001c <main>:

void main(void) {
1c: 1141 addi sp,sp,-16
1e: e406 sd ra,8(sp)
20: e022 sd s0,0(sp)
22: 0800 addi s0,sp,16
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0
2c: 79050513 addi a0,a0,1936 # 7b8 <malloc+0xea>
30: 00000097 auipc ra,0x0
34: 5e6080e7 jalr 1510(ra) # 616 <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 274080e7 jalr 628(ra) # 2ae <exit>

请问哪个寄存器会将参数传递给调用的函数?例如在printf中,参数13存储在哪个寄存器中?

Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?

通过查阅RISC-V的Calling conventions手册,我们看到:

a0~a7寄存器用于保存函数参数,因此由于13属于第三个参数,因此存储的寄存器是a2

顺带看到0x24位置,将13写入了a2寄存器,进一步印证了我们的想法。

非常的合理,梦回Bomb Lab了属于是。

问题2

前后对比call.ccall.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
return x+3;
}

int f(int x) {
return g(x);
}

void main(void) {
printf("%d %d\n", f(8)+1, 13);
exit(0);
}

请问主函数中对fg的调用在哪里(提示:编译器可能会使用内联函数)

Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)

还是看刚才的call.asm,很明显的可以看到在0x26位置,程序直接将12写入了a1寄存器的位置(也就是第二参数f(8)+1的位置),在处理完所有的寄存器参数后直接就调用了printf。那只有一种解释,就是编译器直接计算出了f(8)+1的结果是12,主函数中并没直接调用这两个函数。

问题3

printf函数位于哪个地址?

At what address is the function printf located?

额,看看main函数里面的系统调用怎么做的?在0x34位置,我们跳转到了printf地址,也就是ra+1510,这是偏移量记法,也叫0x616… 那这就是我们的地址了,你在call.asm往下翻翻顺便还能看到printf的汇编代码,非常的合理:

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
0000000000000616 <printf>:

void
printf(const char *fmt, ...)
{
616: 711d addi sp,sp,-96
618: ec06 sd ra,24(sp)
61a: e822 sd s0,16(sp)
61c: 1000 addi s0,sp,32
61e: e40c sd a1,8(s0)
620: e810 sd a2,16(s0)
622: ec14 sd a3,24(s0)
624: f018 sd a4,32(s0)
626: f41c sd a5,40(s0)
628: 03043823 sd a6,48(s0)
62c: 03143c23 sd a7,56(s0)
va_list ap;

va_start(ap, fmt);
630: 00840613 addi a2,s0,8
634: fec43423 sd a2,-24(s0)
vprintf(1, fmt, ap);
638: 85aa mv a1,a0
63a: 4505 li a0,1
63c: 00000097 auipc ra,0x0
640: de0080e7 jalr -544(ra) # 41c <vprintf>
}
644: 60e2 ld ra,24(sp)
646: 6442 ld s0,16(sp)
648: 6125 addi sp,sp,96
64a: 8082 ret

目测这个地址貌似因人而异… 可能大家编译出来的多多少少都会有点不同

问题4

jalr跳转至main函数的printf时,寄存器ra中有什么值?

What value is in the register ra just after the jalr to printf in main?

我们不妨查看一下spec文档RISC-V unprivileged instructions,可以看到:

当程序进行跳转时,我们需要将ra寄存器存储的返回地址指向printf执行结束后返回到主程序的位置,也就是当前位置PC加4,也就是0x38

问题5

运行如下代码:

1
2
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

判断输出是什么?可参考ASCII表来查询对应字符。

What is the output? Here’s an ASCII table that maps bytes to characters.

注意观察由于RISC-V是小端序,一个整型变量在内存中是这样存储的。

那很显然,显示结果就应该是HE110 World… 我们尝试修改一下call.cmain函数内容,然后在终端下运行一下,验证一下结果,是一致的:

如果RISC-V不是小端序而是大端序呢?我们需要对i57616做出什么改变吗?

The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change57616 to a different value?

那很显然,我们需要将i修改为0x726c6400,但是57616并不需要做出改变,因为它转化为二进制仍然是0xe110

问题6

当运行如下代码时,在y=后面会打印出什么?(注意:答案不是一个确定的值)为什么?

1
printf("x=%d y=%d", 3);

In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?

根据刚才的传参规则,打印出来的因该是寄存器a2的值,但是a2在这里并没有指定具体的数值…

那就真的是你说啥就是啥了,这个时候a2的值肯定是受之前的代码影响产生的随机值…

函数调用栈打印

要求和提示

我们在代码调试的过程中为了定位错误的位置,经常需要使用到函数的调用栈打印。当错误发生时,会打印出一系列之前的函数调用信息。

在这里要求我们需要在kernel/printf.c下实现函数backtrace(),并在sys_sleep中插入对此函数的一个调用。当我们在终端中调用bttest测试时,该命令会调起sleep系统调用,从而触发打印,运行bttest时打印结果示例为:

1
2
3
4
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

bttest退出了QEMU之后,在终端下,我们也可以使用addr2line命令来将指令地址转化为具体的代码位置,例如我们使用指令addr2line -e kernel/kernel

我们的目标是从顶部开始遍历各个栈帧,并将各个栈帧中保存的返回地址打印出来。(讲道理感觉做个-4打印调用函数的地址可能会更直观一点,但是他说是就是吧)

相关提示:

  • 需要在kernel/defs.h中添加backtrace的声明,使得我们可以在sys_sleep下调用backtrace

  • GCC编译器将当前执行的函数的栈帧指针frame pointer存储在s0寄存器中,为此我们需要在kernel/riscv.h下加入代码:

    1
    2
    3
    4
    5
    6
    7
    static inline uint64
    r_fp()
    {
    uint64 x;
    asm volatile("mv %0, s0" : "=r" (x) );
    return x;
    }

    以方便我们在backtrace中调用来读取栈帧的底部地址。

    P.S. spfp是典型的堆栈寄存器,用来标注当前栈帧的底部和顶部地址,由于我们在描述的时候对顶部和底部往往有自己的理解,因为栈地址是向下增长的,这里还是放一张图会比较好:

  • 由刚才贴出的图可见,注意到当前函数的返回地址和当前的fp存在一个-8的地址偏移量,上一个栈帧的fp保存位置和当前的fp存在一个-16的偏移量。

  • 在xv6中,操作系统内核会为每个栈分配一个页面的空间,并地址对齐。因此我们可以使用PGROUNDDOWN(fp)PGROUNDUP(fp)来寻找栈所在页面的顶部和底部地址,PGROUNDDOWNPGROUNDUP的定义可以看下kernel/riscv.h

    1
    2
    3
    4
    5
    #define PGSIZE 4096 // bytes per page
    #define PGSHIFT 12 // bits of offset within a page

    #define PGROUNDUP(sz) (((sz)+PGSIZE-1) & ~(PGSIZE-1))
    #define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))

    通过确定页面的顶部和底部地址可以方便我们及时的退出遍历循环。

一旦我们确认了backtrace正确运作了,我们就可以在kernel/printf.c下的panic打印函数中加入对backtrace的调用,好在程序发生panic的时候打印其调用栈。

完成backtrace函数

r_fp的代码需要添加在刚才的提示中写的很明白了,这里就不说了。

首先我们在kernel/defs.h中加入对backtrace的声明:

1
2
3
4
5
// printf.c
void printf(char*, ...);
void panic(char*) __attribute__((noreturn));
void printfinit(void);
void backtrace(void);

然后我们在kernel/printf.c中加入backtrace函数,逻辑比较简单,我们从当前栈帧开始,也就是地址最小,位置最靠顶部的栈帧,通过-8偏移量获取到返回地址ra并将其打印。随后我们通过-16偏移量来确定地址稍大一些、位置更往下的栈帧的底部位置,从而完成跳转。如此不断遍历,直到fp指针达到栈所在页面的最大值。

1
2
3
4
5
6
7
8
9
10
11
void backtrace(void) { 
printf("backtrace:\n");
uint64 fp = r_fp();
while (fp != PGROUNDUP(fp)) { // until get to stack bottom
// get return addr in current stack frame
uint64 ra = *(uint64 *)(fp - 8);
printf("%p\n", ra);
// go to prev stack frame
fp = *(uint64 *)(fp - 16);
}
}

调用backtrace

为了完成测试功能bttest,我们需要在kernel/sysproc.c下的sys_sleep方法中加入backtrace方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
uint64
sys_sleep(void)
{
int n;
uint ticks0;

if(argint(0, &n) < 0)
return -1;
backtrace();
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}

同理,在kernel/printf.c下的panic中,我们也要加入backtrace,使得内核函数执行发生panic的时候可以打印出调用栈情况:

1
2
3
4
5
6
7
8
9
10
11
12
void
panic(char *s)
{
pr.locking = 0;
printf("panic: ");
printf(s);
printf("\n");
backtrace();
panicked = 1; // freeze uart output from other CPUs
for(;;)
;
}

测试

在目录下执行make clean; make qemu,随后尝试执行bttest,可以看到对应的栈调用情况:

我们甚至可以进一步代入看看对应的代码位置在哪里:

实现定时器

要求和提示

在本部分中,我们将为xv6添加一个新的功能,使得当一个进程使用CPU时间的时候周期性的发出警告。这种功能对于想要限制它们占用多少CPU时间的计算型进程,或者对于想要计算但又想采取一些定期行动的进程,可能是很有用的。更广泛地说,我们将实现一个原始形式的用户级中断/故障处理程序;例如,我们可以使用类似的东西来处理应用程序中的页面故障。我们需要通过alarmtest来完成该部分。

我们需要在xv6中添加一个新的系统调用sigalarm(interval, handler),例如当程序调用sigalarm(n, fn)的时候,那么接下来程序每消耗n个ticks的CPU时间后,内核应该调用fn。当fn成功返回时,程序应当继续不受影响的运行,这部分由需要添加的系统调用sigreturn负责完成。如果应用程序调用sigalarm(0, 0),内核应该停止产生周期性的调用。

P.S. 在xv6中,tick是一个相当随意的时间单位,由硬件定时器产生中断的频率决定。

alarmtest的相关代码在user/alarmtest.c中可见,为了使得其能够被正确识别,我们需要将其加入到Makefile中,如果对这块已经有点不记得的小伙伴可以看看Lab Utils,可能可以记起来。

由于alarmtest文件中使用了两个我们待实现的系统调用sigalarmsigreturn,务必将系统调用正确添加,记不得的小伙伴可以再去看看Lab Syscall… 后面也会继续提

alarmtest中在test0调用了sigalarm(2, periodic),要求内核每隔2个ticks强制调用periodic(),然后尝试自旋一段时间。我们可以在user/alarmtest.asm中看到alarmtest的汇编代码,这可能对调试很有帮助。

正确运行alarmtestusertests的示例结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$

这块内容分为两部分完成,我们首先实现test0,再实现test1test2

对于test0而言,其功能是测试handler函数是否被正确执行了,首先我们需要修改内核代码使得内核可以跳转到用户空间中的periodic函数,从而打印出"alarm!"。我们在这里暂时不考虑打印之后怎么处理,如果你在打印出"alarm!"程序直接crash也是完全正常的。以下是关于test0的相关提示:

  • 请将alarmtest.c加入Makefile中,使得其能够被正常编译。

  • 两个系统调用需要在user/user.h中声明:

    1
    2
    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
  • 你需要更新user/usys.plkernel/syscall.hkernel/syscall.c来使得alarmtest可以正确的触发sigslarmsigreturn系统调用。这块不记得的可以看看Lab Syscall。

  • test0中对sys_sigreturn暂时没有要求,可以返回0完事。

  • 你的sys_sigalarm()需要在kernel/proc.h下的proc结构体中开辟新的空间额外存储间隔时间和handler函数的指针。

  • 你需要跟踪从最后一次调用直到下一次调用,过去了(或者说还剩下)多少时间,因此我们也需要在proc结构体中开辟额外的空间来实现。初始化proc结构体参数的方法详见kernel/proc.c下的allocproc()

  • 每一次tick周期,硬件始终都会强制中断,这块的逻辑代码由kernel/trap.c下的usertrap()实现。

  • usertrap()下,如果我们要控制硬件定时器中断发生时的行为,例如修改proc中存储的时间参数,我们只需要关注如下代码下的改动:

    1
    if(which_dev == 2)
  • 请当进程有一个未完成的定时器时才可调用handler函数。注意到handler函数的地址可能是0,例如在user/alarmtest.asm中,periodic位于地址0

  • 请修改usertrap()使得当进程的定时间隔过期时,用户进程可以执行handler函数。另外请思考,当在RISC-V上的陷阱返回到用户空间时,是什么决定了接下来用户空间代码开始执行的位置?

  • 为了方便使用GDB观察陷阱的运行,我们可以让QEMU只使用一个CPU,例如运行如下代码:

    1
    make CPUS=1 qemu-gdb
  • 在本阶段中,只要alarmtest成功打印出"alarm!."即为成功。

对于test1test2而言,其功能主要是测试被中断的代码接下来是否能够继续运行。为了确保程序接下来能够继续正确的运行,我们需要在handler函数执行完毕时,返回至被定时器中断的代码位置,同时保证寄存器内容被正确的存储并恢复了。最后我们需要在每次计时结束后重置计时器的计数器,使得handler函数能不断的被周期调用。

我们假定如下的设计:用户空间的handler函数在结束时需要调用sigreturn系统调用,参考user/alarmtest.cperiodic函数。这意味着我们需要在usertrapsys_sigreturn中进行修改,使得用户空间进程可以正确的继续执行下去。如下为关于test1test2的相关提示:

  • 为了保证被中断程序的正确运行,你需要保存和恢复寄存器。请思考一下,哪些寄存器需要你进行存储和恢复呢?
  • usertrap中需要向proc结构体中保存一些状态信息,以方便sigreturn正确返回到原先的代码位置。
  • 禁止可重入的调用handler函数,在handler函数没有返回之前,内核不应当再次调起。关于这块的测试将在test2中有所体现。

思路分析

好的,那么现在让我们梳理一下思路,想一下sys_sigalarmsys_sigreturnusertrap分别需要做些什么。简单的来说,sys_sigalarm用于设置进程的定时中断信息,并不负责具体的函数调用,例如将需要调用的handler地址和每次的间隔时间,内容自然需要额外保存在proc结构体中。而usertrap负责每次硬件定时器触发的中断发生的时候,检查proc结构体内部的状态信息,看看时间有没有到了,如果到了就从内核态跳转到用户态执行handler函数。sys_sigreturn则负责从内核态将程序转到用户态代码被中断的位置,同时把计时器ticks总时间重置,重新开始倒计时。

这里的一个需要想明白的点,是如何在内核态下跳转到位于用户态下的函数,其实也很简单,就是覆盖存储在trapframe中的epc返回地址,最终userret的时候,pc寄存器从trapframe中恢复数值,跳转到我们要的用户态函数上去。我们在usertrap定时器tick检查和sys_sigreturn到原先被中断代码的时候都需要使用这个小技巧。

值得注意的是,usertrap判定tick时间耗尽决定跳转到periodic函数并执行后,寄存器的数值可能就会被破坏,那最后sys_sigreturn从内核态恢复到原先用户代码被中断位置的时候就可能会有大问题,因此… 我们需要备份,在usertrap决定跳转到periodic的时候就要备份好一份trapframe,也存到proc结构体中。

我们可以大致画一下流程图:

P.S.

关于硬件时钟中断的处理逻辑实际上在提示和流程图中做出了简化。事实上,xv6处理硬件定时器中断的行为有别于trap处理机制,当发生硬件定时器中断时,会进入到machine mode,而非supervisor mode,相关处理代码可见kernel/kernelvec.S下的timervec

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
timervec:
# start.c has set up the memory that mscratch points to:
# scratch[0,8,16] : register save area.
# scratch[24] : address of CLINT's MTIMECMP register.
# scratch[32] : desired interval between interrupts.

csrrw a0, mscratch, a0
sd a1, 0(a0)
sd a2, 8(a0)
sd a3, 16(a0)

# schedule the next timer interrupt
# by adding interval to mtimecmp.
ld a1, 24(a0) # CLINT_MTIMECMP(hart)
ld a2, 32(a0) # interval
ld a3, 0(a1)
add a3, a3, a2
sd a3, 0(a1)

# raise a supervisor software interrupt.
li a1, 2
csrw sip, a1

ld a3, 16(a0)
ld a2, 8(a0)
ld a1, 0(a0)
csrrw a0, mscratch, a0

mret

在这里除了暂存寄存器、重置硬件定时器外,还发起了一个supervisor软件中断,从而在mret结束后进入到trap处理函数中。从外部来看,就是由于发生了硬件定时器中断而跳转到了trap处理函数中。

接下来,我们要考虑需要在proc结构体中额外插入哪些东西呢?思考下来,有这么几个:

  • sigalarm传入的参数,即周期间隔ticks时间和handler函数地址。周期ticks时间若为0,则表示我们实现的定时器处于关闭状态。
  • 当前计时器剩余ticks时间也需要计入,这样子每次硬件时钟中断发生时,我们就将数值减一,这个剩余时间也可作为是否要执行handler函数的标准。在sigreturn中,我们还需要将定时器剩余时间重置,重新开始新的周期。
  • usertrap中如果需要跳转到handler函数,那么就需要保存一下当前的trapframe,方便后续在sigreturn进行恢复。
  • handler函数不可重入,因此可以用一个int当作类似锁的组件,当handler正在执行中保持状态为1,执行完毕为0。如果发生了冲突,则不跳转到handler函数,即如果一个硬件时钟到期的时候已经有一个时钟处理函数正在运行,则会推迟到原处理函数运行完成后的下一tick才触发。

系统调用配置

首先我们需要将系统调用的相关配置设置好,这块内容可以参考Lab Syscall。

首先,不要忘记在主目录下的Makefile中将alarmtest.c纳入编译范围:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
UPROGS=\
$U/_cat\
$U/_echo\
$U/_forktest\
$U/_grep\
$U/_init\
$U/_kill\
$U/_ln\
$U/_ls\
$U/_mkdir\
$U/_rm\
$U/_sh\
$U/_stressfs\
$U/_usertests\
$U/_grind\
$U/_wc\
$U/_zombie\
$U/_alarmtest\

然后,我们在user/user.h中添加系统调用的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// system calls
int fork(void);
int exit(int) __attribute__((noreturn));
int wait(int*);
int pipe(int*);
int write(int, const void*, int);
int read(int, void*, int);
int close(int);
int kill(int);
int exec(char*, char**);
int open(const char*, int);
int mknod(const char*, short, short);
int unlink(const char*);
int fstat(int fd, struct stat*);
int link(const char*, const char*);
int mkdir(const char*);
int chdir(const char*);
int dup(int);
int getpid(void);
char* sbrk(int);
int sleep(int);
int uptime(void);
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

接下来,我们在kernel/syscall.h中添加新系统调用的编号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// System call numbers
#define SYS_fork 1
#define SYS_exit 2
#define SYS_wait 3
#define SYS_pipe 4
#define SYS_read 5
#define SYS_kill 6
#define SYS_exec 7
#define SYS_fstat 8
#define SYS_chdir 9
#define SYS_dup 10
#define SYS_getpid 11
#define SYS_sbrk 12
#define SYS_sleep 13
#define SYS_uptime 14
#define SYS_open 15
#define SYS_write 16
#define SYS_mknod 17
#define SYS_unlink 18
#define SYS_link 19
#define SYS_mkdir 20
#define SYS_close 21
#define SYS_sigalarm 22
#define SYS_sigreturn 23

并在user/usys.pl中添加系统调用项:

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
#!/usr/bin/perl -w

# Generate usys.S, the stubs for syscalls.

print "# generated by usys.pl - do not edit\n";

print "#include \"kernel/syscall.h\"\n";

sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}

entry("fork");
entry("exit");
entry("wait");
entry("pipe");
entry("read");
entry("write");
entry("close");
entry("kill");
entry("exec");
entry("open");
entry("mknod");
entry("unlink");
entry("fstat");
entry("link");
entry("mkdir");
entry("chdir");
entry("dup");
entry("getpid");
entry("sbrk");
entry("sleep");
entry("uptime");
entry("sigalarm");
entry("sigreturn");

最后我们在kernel/syscall.csyscalls表中添加新的映射关系,指向需要执行的函数:

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
extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};

至于两个关键的系统调用sys_sigalarmsys_sigreturn,我们将在kernel/sysproc.c下进行实现,不需要实现核心函数,先把参数从用户空间取出即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint64 sys_sigalarm(void) { 
int n;
uint64 fn;
if(argint(0, &n) < 0) {
return -1;
}
if(argaddr(1, &fn) < 0) {
return -1;
}
return sigalarm(n, (void(*)())(fn));
}

uint64 sys_sigreturn(void) {
return sigreturn();
}

具体的sigalarmsigreturn我打算在kernel/trap.c下实现。

完善proc结构体

根据刚才的思考和总结,我们在proc结构体中需要加入五个部分,在kernel/proc.h中如下所示:

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
// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
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

// wait_lock must be held when using this:
struct proc *parent; // Parent process

// these are private to the process, so p->lock need not be held.
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 context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
int alarm_interval; // Alarm interval (0 for disabled alarm)
void (*alarm_handler)(); // Alarm handler
struct trapframe *alarm_backup; // Copy of trapframe before handler triggered
int alarm_ticks_left; // How many ticks left before next alarm goes off
int alarm_handler_lock; // Avoid reentrance of handler function
};

由于添加了几个成员,因此我们的proc初始化和删除函数也需要处理,见kernel/proc.c

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
struct proc *p;

for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;

found:
p->pid = allocpid();
p->state = USED;

// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}

// Allocate a trapframe page for alarm backup trapframe
if((p->alarm_backup = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}

// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}

// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
p->alarm_interval = 0;
p->alarm_handler = 0;
p->alarm_ticks_left = 0;
p->alarm_handler_lock = 0;

return p;
}

// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
if (p->alarm_backup)
kfree((void *)p->alarm_backup);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
p->alarm_handler = 0;
p->alarm_interval = 0;
p->alarm_ticks_left = 0;
p->alarm_handler_lock = 0;
}

实现usertrap跳转

kernel/trap.c下的usertrap中主要用于处理中断、异常以及系统调用。

我们在专门处理硬件定时器中断的程序if(which_dev == 2)下加入相关代码:

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
66
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void)
{
int which_dev = 0;

if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");

// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);

struct proc *p = myproc();

// save user program counter.
p->trapframe->epc = r_sepc();

if(r_scause() == 8){
// system call

if(p->killed)
exit(-1);

// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;

// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();

syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}

if(p->killed)
exit(-1);

// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->alarm_interval) { // if alarm is enalbled
if(--p->alarm_ticks_left <= 0) { // if ticks expired, trigger the handler func
if(!p->alarm_handler_lock) { // if the handler func is not running
// save the current trapframe
*p->alarm_backup = *p->trapframe;
// modify return address
p->trapframe->epc = (uint64)p->alarm_handler;
// set reentrant lock
p->alarm_handler_lock = 1;
}
}
}
yield();
}

usertrapret();
}

其中主要判断当前alarm是否开启,时间是否过期需要触发handler,以及当前handler是否已经返回。如果这三者都满足,那么就可以将返回地址指向handler函数,但是在此之前还需要备份一下trapframe,并且还需要上可重入锁。

实现sigalarmsigreturn

接下来我们处理系统调用的核心函数sigalarm,根据刚才的总结,我们在需要在proc结构体中记录一些信息,包括周期ticks时间,handler函数地址,并初始化当前剩余ticks。我们将sigalarm放在kernel/trap.c下:

1
2
3
4
5
6
7
int sigalarm(int ticks, void (*handler)()) { 
struct proc *p = myproc();
p->alarm_interval = ticks;
p->alarm_handler = handler;
p->alarm_ticks_left = ticks;
return 0;
}

而在sigreturn中,我们需要取出刚才备份保存的trapframe,并将其全部覆盖在当前的trapframe中。另外记得将可重入锁取消。我们将sigreturn放在kernel/trap.c下:

1
2
3
4
5
6
7
8
int sigreturn() { 
struct proc *p = myproc();
// restore trapframe
*p->trapframe = *p->alarm_backup;
// release reentrant lock
p->alarm_handler_lock = 0;
return 0;
}

最后记得在kernel/defs.h中声明一下:

1
2
3
4
5
6
7
8
// trap.c
extern uint ticks;
void trapinit(void);
void trapinithart(void);
extern struct spinlock tickslock;
void usertrapret(void);
int sigalarm(int, void (*)());
int sigreturn(void);

测试

目测问题不大,我们跑一下alarmtest

总结

接下来我们来到主目录上使用make grade进行评分,有几点注意一下:

  • 你需要把刚才简答题答案放在主目录下的answer-pgtbl.txt下。

  • 你需要在主目录下创建time.txt表示完成任务的时间。

  • 测试过程中usertests耗时略长,电脑机能不行的小伙伴可以适当将测试的超时延长一点点,请把主目录下 grade-lab-traps 文件里面的 timeout 参数改大点,不然可能你的测试项目还没跑完他就跳出给你标个大大的 FAIL…

最后我们看一下测试的结果:

看起来没啥问题,那就这样了吧。