MIT 6.828 - 6. Lab 06: User-level Threads and Alarm

Tags: MIT 6.828

实验总结

  1. 本次实验用时约 20 个小时。
  2. 收获是对 context switch 的理解更深入了,了解了进程调度的一种良好设计(即用协程分割控制流,使代码更简洁)。

遇到的困难包括:

  1. 没有交叉的 gdb,调试困难。

实验结束后的全部代码在:https://github.com/monkey2000/xv6-riscv/tree/syscall/

测试结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
answers-syscall.txt: OK 
uthread:
$ make qemu-gdb
OK (2.3s)
running alarmtest:
$ make qemu-gdb
(3.7s)
alarmtest: test0: OK
alarmtest: test1: OK
usertests:
$ make qemu-gdb
OK (91.4s)
time: OK
Score: 100/100

0. 实验准备

实验指导链接

上来直接:

1
2
$ cd xv6-riscv-fall19
$ git checkout syscall

1. Warmup: RISC-V assembly

这块要回答几个问题,打开 user/call.cuser/call.asm ,有如下代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
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
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 ; printf("%d %d\n", f(8)+1, 13);
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0
2c: 75050513 addi a0,a0,1872 # 778 <malloc+0xea>
30: 00000097 auipc ra,0x0
34: 5a0080e7 jalr 1440(ra) # 5d0 <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 1fe080e7 jalr 510(ra) # 238 <exit>


... lines omitted

00000000000005d0 <printf>:

void
printf(const char *fmt, ...)
{

....

下面来回答问题:

  1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
    • 根据 riscv user-level isa (在 doc/riscv-calling.pdf) , a0-a7 和 fa0-fa7 共计 16 个寄存器会用于传递参数
    • 具体而言,根据以上代码,可以得知 a2 寄存器用于存传给 printf 的参数 13
  2. Where is the function call to f from main? Where is the call to g? (Hint: the compiler may inline functions.)
    • 注意括号里的话。根据笔算我们知道 f(8)+1 = 12,又发现 main 中并为调用 f 函数,说明 f 在编译期被直接优化成一个常量 12 塞到 a1 寄存器里了。
  3. At what address is the function printf located?
    • 观察 main 函数,在调用时使用了代码 auipc ra,0x0jalr 1440(ra),前者取当前指令的 pc 加上 0x0 存入 ra,后者跳转到 ra + 1440。计算知 0x0000000000000030 + 1440 = 0x00000000000005d0。经验证是 printf 的入口地址。
  4. What value is in the register ra just after the jalr to printf in main?
    • 此题需要看 rv spec 。jalr 指令完成后,ra 寄存器会存储返回点位置(也即 pc + 4 )

2. Uthread: switching between threads

这题并不难,我给想复杂了。

首先我们把 Hint 里面的问题解决:thread_switch needs to save/restore only the callee-save registers. Why? 这个是因为协程切换的过程本质是一个函数调用,因此 caller-save registers 是被调用者(如 thread_a() )保存好的。

然后我们研究以下代码该怎么写(抄):

  1. 首先打开 kernel/swtch.S ,查阅 riscv calling convention ,验证这段代码可以完成寄存器的切换。注意 ra 表示返回地址,sp 表示当前栈顶。直接复制到 user/uthread_switch.S 即可。
  2. 接着在 kernel/proc.h 中,找到上述代码配套的 context 结构体声明,复制到 user/uthread.c 中。
  3. 修改几行代码。当发生协程切换时调用 uthread_switch(old_ctx, new_ctx),完成寄存器状态的切换;当新建协程时,将 ra 设为协程入口点地址,sp 设为 thread.stack 的最高地址(栈底)。

第三部相对有思维量的代码如下,可以证明是正确的:

1
2
t->ctx.ra = (uint64) func;
t->ctx.sp = (uint64) (&t->stack) + STACK_SIZE;

3. Alarm

这块是以 alarm 为例实现一个 signal 系统,即 signal 触发的时候调用进程注册的 signal handler,运行结束后返回原来进程的位置,恢复状态。

首先,按照实验指导,添加 sigalarm 和 sigreturn 两个系统调用:

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
// kernel/signal.c
// Only for lab 06: syscall
// sys_sigalarm() and sys_sigreturn() are implemented here
//

#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "proc.h"
#include "signal.h"

uint64 sys_sigalarm(void) {
struct proc *p = myproc();
int interval; uint64 handler_addr;
if(argint(0, &interval) < 0 || argaddr(1, &handler_addr) < 0)
return -1;

p->alarm_interval = interval;
p->alarm_handler = handler_addr;
p->alarm_last_tick = ticks;

return 0;
}

uint64 sys_sigreturn(void) {
struct proc *p = myproc();
p->alarm_state = 0;
memmove(p->tf, &p->alarm_tf, sizeof(struct trapframe));
return 0;
}

这里我给 proc 结构体新增了一些字段,来存储 alarm 相关的信息:

1
2
3
4
5
6
7
8
9
// Per-process state
struct proc {
//... omitted
int alarm_interval; // Alarm's interval (0 for disabled)
uint64 alarm_handler; // Virtual address of the alarm handler (can be 0 due to xv6-rv's userspace memory layout)
uint64 alarm_last_tick; // Ticks of the last call
struct trapframe alarm_tf; // trapframe for storing original tf
uint alarm_state; // 1 if the handler hasn't return
};

前三个变量很好理解,这里解释一下 alarm_tfalarm_state 的意义:

  1. alarm_tf 用于在调用 alarm handler 的过程当中,存储原有的 trapframe(用户态的寄存器状态),这样在 sigreturn() 中可以恢复这个状态,实现恢复执行。
  2. alarm_state 用于防止因为上一个 alarm 信号还没处理完导致的重入


知识共享许可协议 2000 - 2099 MonKey's Blog | 自豪地采用 Hexo + Pandoc + KaTeX + Highlight.js