Trap 的旅途

这一个 lab 实验反而是其次,我觉得更重要的是熟读并理解 book-riscv-rev1 第 4 章中的内容。以下内容尤其重要。

进程数据结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };
// Per-process state
struct proc {
struct spinlock lock;

// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
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

// 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)
};

uservec

trampoline.S 中的 uservec

  • 系统已经设置好 uservec 是内核进入点
    • 也就是说此时的 stvec 的值是 uservec 的地址
  • trap (ecall)使得进程陷入内核 (对所有的 trap 类型(除了定时器中段)RISC-V 硬件会做出以下步骤)
    • 如果 trap 是一个设备中段,并且 sstatus 的 SIE 位为 0,则跳过以下所有步骤
    • 通过对 sstatus 的 SIE 位置零来关闭中断
    • 将 pc 的值拷贝到 sepc
    • 将当前的处理模式(user mode 或 supervisor mode)保存到 sstatus 的 SPP 位中
    • 设置 scause 来反应导致 trap 的原因
    • 将 mode 切换到 supervisor mode
    • 将 stvec 中的值拷贝到 pc
    • 开始执行新的 pc 处的指令
  • 将陷入内核的进程的所有寄存器状态保存到内存 proc 结构体的 trapframe 字段中 (用户进程空间)
  • 将 trapframe 字段中的内核栈指针,hartid, usertrap 地址, 内核页表加载到对呀的寄存器中 (用户进程空间)
  • 切换到内核页表 (从此进入内核空间)
  • 跳转到 usertrap 函数入口处

这里需要注意几点:

  1. 每个进程都是一座孤岛,都有自己独立的虚拟地址空间。
  2. uservec 这段代码存储在 TRAMPOLINE 这一物理页中。
  3. TRAMPOLINE 会被映射到每一个进程的同一处虚拟地址空间中(包括内核)(相当于共享这一物理页的同时,虚拟地址都一样)。
  4. 内核在创建进程时会为每一个进程分配 proc 结构体保存进程的状态并存储在内核空间中;除此之外还会为每个进程创建一页 TRAPFRAME 页(它将被映射到进程虚拟地址空间的 TRAMPOLINE 页的下面一页,即 TRAPFRAME + 4096 == TRAMPOLINE)用于进程陷入内核时保存 user mode 的寄存器状态。
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
.globl uservec
uservec:
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# sscratch points to where the process's p->trapframe is
# mapped into user space, at TRAPFRAME.
#

# swap a0 and sscratch
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0

# save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0)

# save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0)

# restore kernel stack pointer from p->trapframe->kernel_sp
ld sp, 8(a0)

# make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0)

# load the address of usertrap(), p->trapframe->kernel_trap
ld t0, 16(a0)

# restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero

# a0 is no longer valid, since the kernel page
# table does not specially map p->tf.

# jump to usertrap(), which does not return
jr t0

usertrap (以下皆在内核空间)

trap.c 中的 usertrap 函数

  • 判断是否从 user mode 进到该函数的 (因为还有个 kerneltrap)
  • 获取当前进程的 proc 结构体指针 p
  • 将陷入内核的进程的 pc 保存到 p->trapframe->epc 中
  • 判断系统调用还是中断还是异常,根据不同的情况做不同的操作 (我们这里只考虑系统调用)
  • 调用 syscall 函数
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
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)
yield();

usertrapret();
}

可以看到在 usertrap 中关于此次 trap 的原因分为三类:系统调用、中断和异常。根据不同的情况做出不同的操作。我们这里只考虑系统调用,那么就是为了执行 syscall 函数。

syscall

这个函数是调用所有内核中的系统调用例程的总入口,也就是说所有的系统调用都要经过它。

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
 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_trace] sys_trace,
[SYS_sysinfo] sys_sysinfo,
};


void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

可以看到所有的系统调用例程都被注册成了函数指针的形式。

syscall 函数就是根据进程陷入内核时传入的系统调用号来判断具体要去执行哪个函数指针。系统调用号会存储在 proc::trapframe::a7 中。返回值会保存在proc::trapframe::a0 中。

为什么存在 a7 中呢?这是因为在调用 ecall 前我们自己存放到 a7 寄存器的,在进入 uservec 后被 save 到 proc::trapframe::a7 中的。具体可以看 user/usys.S,这个函数编写了系统调用 wrapper:

1
2
3
4
5
6
7
#include "kernel/syscall.h"
···
.global sleep
li a7, SYS_sleep
ecall
ret
···

usertrapret

这一函数在 kernel/trap.c 中。它要做的就是还原一些状态,为下次进程再次陷入内核做准备。

首先必要做的就是重新设置 stvec 控制状态寄存器的值为 uservec,为下次再次陷入内核做准备。其次将 内核页表、内核栈地址、usertrap 地址、core num 存入 TRAPFRAME 页中,供 userret 加载到寄存器中。重新设置 sstatus 控制状态寄存器。因为 sret 的时候,硬件会把 sepc 中的值拷贝到 pc 中,因此也要设置 sepc。并且获取到用户页表。

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
void
usertrapret(void)
{
struct proc *p = myproc();

// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off();

// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));

// set up trapframe values that uservec will need when
// the process next re-enters the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()

// set up the registers that trampoline.S's sret will use
// to get to user space.

// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);

// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);

// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable);

// jump to trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}

userret

这函数在 trampoline.S 中,它可以看成是 uservec 的镜像(对着干,uservec 是将寄存器的值 save 到内存,而它是将内存中的值 load 到寄存器)。这也是为了下次进程陷入内核做准备。

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
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.

# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero

# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0

# restore all but a0 from TRAPFRAME
ld ra, 40(a0)
ld sp, 48(a0)
ld gp, 56(a0)
ld tp, 64(a0)
ld t0, 72(a0)
ld t1, 80(a0)
ld t2, 88(a0)
ld s0, 96(a0)
ld s1, 104(a0)
ld a1, 120(a0)
ld a2, 128(a0)
ld a3, 136(a0)
ld a4, 144(a0)
ld a5, 152(a0)
ld a6, 160(a0)
ld a7, 168(a0)
ld s2, 176(a0)
ld s3, 184(a0)
ld s4, 192(a0)
ld s5, 200(a0)
ld s6, 208(a0)
ld s7, 216(a0)
ld s8, 224(a0)
ld s9, 232(a0)
ld s10, 240(a0)
ld s11, 248(a0)
ld t3, 256(a0)
ld t4, 264(a0)
ld t5, 272(a0)
ld t6, 280(a0)

# restore user a0, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0

# return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret

看完第三章的内容并彻底理解后,lab 就随便做做啦,洒洒水啦~~~