lab4 是 traps 相关

预备知识

系统调用的时候发生了什么?例如 write()?

  1. 保存 32 个用户寄存器以及 pc
  2. 切换到 supervisor 模式
  3. 切换到内核页表
  4. 切换到内核栈
  5. 跳到 C 代码处

不要在 supervisor 模式执行用户代码!我之前就是这个思路(如何在内核空间中执行用户代码),想了很久都没有答案,然后看了别人的解析才发现,原来思路错了!

在 supervisor 模式,多了哪些特权?

  1. supervisor 模式下能够使用 CPU 的控制状态寄存器:
    1. satp – 保存了页表物理地址
    2. stvec – ecall 会跳转到该寄存器保存的地址处,即 TRAMPOLINE
    3. sepc – ecall 会将用户 pc 保存到 sepc 中
    4. sscratch – 保存 trapframe 的地址
  2. supervisor 模式下能够使用没有 PTE_U 标志的 PTEs

除此之外,supervisor 模式没有什么特别的了!它也不能访问不在其页表中的内容!

Backtrace

添加一个 backtrace 函数,sys_sleep 调用这个函数后可以打印出函数调用栈

实现思路:

根据 RISCV ABI,ra 寄存器存放返回地址,sp 寄存器存放栈顶指针,s0 寄存器存放栈基指针。在 RISCV 上的汇编语言 ABI 规定,在开辟新的函数调用栈的时候,将上一个栈的 栈基指针 存到新的 栈基-16 处;将 ra 存放到新的 栈基-8 处。因此我们可以根据栈基指针遍历整个函数调用过程,直到 栈基指针 的值超过了内核栈。

实现:

这个实现还是很简单的。

首先我们要获取 栈基指针 s0:用 C 代码封装汇编指令。

1
2
3
4
5
6
7
8
9
10
kernel/riscv.h:

// my code:
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}

然后不断根据 栈基指针 来遍历内核栈即可,结束条件就是遍历超过一页时停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
kernel/printf.c:

// my code:
void backtrace() {
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 bottom = PGROUNDUP(fp), top = PGROUNDDOWN(fp);
while (fp < bottom && fp >= top) { // 只要 fp 超过了一页就结束
uint64 ra = *(uint64*)(fp - 8);
printf("%p\n", ra);
fp = *(uint64*)(fp - 16);
}
}

然后在 sys_sleep 中调用就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
kernel/sysproc.c:

uint64
sys_sleep(void)
{
// my code:
backtrace();
//////////////
int n;
uint ticks0;
····
}

Alarm

这个实验要我们实现一个定时器中断处理。

定时器中断是软中断,每过一个 ticks 都会触发定时器中断,即会从用户态陷入内核态处理定时器中断相关逻辑。

通过 sigalarm 系统调用来注册一个定时器中断(每隔多久执行以此中断处理函数)。添加系统调用的方式和 lab2 一样。这个注册是对应于某个具体进程的,因此需要在 struct proc 结构体中新添加几个字段来保存 间隔时间中断处理函数虚拟地址

1
2
3
4
5
6
7
8
9
// Per-process state
struct proc {
struct spinlock lock;
···
// my code:
uint64 alarm_interval; // 中断时间间隔
fn handler; // 中断处理函数
uint64 ticks_left; // 还剩几个 ticks 就要触发中断
};

这样,在 sys_sigalarm 中对当前申请注册中断处理的进程的结构体字段进行相应的填写,然后修改 trap.c/usertrap 中处理定时器中断的逻辑即可。

由于 系统调用、中断、异常都会将 TRAMPOLINE 作为内核的进入点,因此执行的是同一套逻辑,只是会根据 陷入内核的原因(scause 中的值)来区分到底是三类中的哪一类导致的 trap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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){
···
}
else {···}
}

如果 scause == 8 则是系统调用,其他的则为中断或异常,可以根据 devintr() 的返回值来判断:

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
// 2 if timer device,
// 1 if other device,
// 0 if not recognized.
int
devintr()
{
uint64 scause = r_scause();

if((scause & 0x8000000000000000L) &&
(scause & 0xff) == 9){
// 外部中断
// this is a supervisor external interrupt, via PLIC.
···
return 1;
} else if(scause == 0x8000000000000001L){
// 定时器中断
// software interrupt from a machine-mode timer interrupt,
// forwarded by timervec in kernelvec.S.

if(cpuid() == 0){
clockintr();
}

// acknowledge the software interrupt by clearing
// the SSIP bit in sip.
w_sip(r_sip() & ~2);

return 2;
} else {
// 异常
return 0;
}
}

我们只要添加一个处理定时器中断的逻辑即可。

这里这个逻辑该怎么实现呢?我刚开始一直琢磨着如何在内核态去执行用户空间的代码,想到了建立进程内核页表(即 lab3 的内容),但是一想这实验应该不会涉及到前面的内容呀。。。然后实在没办法才看了下解析,看到别人的思路后恍然大悟。

思路:用户陷入内核时把 pc 值存在了 sepc 中,然后在 TRAMPOLINE 中做了进程状态的保存(将 sepc 保存到 p->trapframe->sepc 中)进入 usertrap;随后必定要按原路返回,通过 usertrapret 进入 userret 恢复进程的寄存器状态,最后依赖 sepc 回到下一条地址处或原地址处。我们只要在定时器中断处理中,将 中断函数的地址赋值给 p->trapframe->sepc,然后一切就水到渠成了!

当然这样的话相当于打乱了原来用户空间栈中的执行顺序,因此在 中断处理函数 结尾处要调用相应的 sigreturn 系统调用来回到原来的状态。而这个 sigreturn 的功能也就明了了,无非就是在 定时器中断处理中 在修改 p->trapframe->sepc 之前对整个 trapframe 做个备份保存起来,在 sigreturn 中用备份恢复 trapframe 就可以了。

在 struct proc 中新增一个 trapframe 备份字段:

1
2
3
4
5
6
7
8
9
10
11
// Per-process state
struct proc {
struct spinlock lock;
···
// my code:
uint64 alarm_interval; // 中断时间间隔
fn handler; // 中断处理函数
uint64 ticks_left; // 还剩几个 ticks 就要触发中断
struct trapframe tmp;
int inHandler; // 表示是否有进程正在进行中断函数处理
}

在 usertrap 的定时器中断中保存 trapframe 备份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
···
else if((which_dev = devintr()) != 0){
if (which_dev == 2) // 定时器中断
{
if (p->alarm_interval != 0) {
if (p->ticks_left == 1 && p->inHandler == 0) {
p->inHandler = 1;
*(p->tmp) = *(p->trapframe); // copy saved user registers.
p->ticks_left = p->alarm_interval;
p->trapframe->epc = (uint64)p->handler;
}
else
if (p->ticks_left == 1) // it means p->inHandler == 1
p->ticks_left = 1; // 等着另一个进程从中断处理函数中出来
else p->ticks_left--;
}
}
}
else {···}
}

在 sigreturn 中恢复 trapframe:

1
2
3
4
5
6
7
8
9
10
kernel/sysproc.c:

uint64
sys_sigreturn(void) {
struct proc* p = myproc();
*(p->trapframe) = *(p->tmp);
p->inHandler = 0;
return 0;
}