MIT 6.s081 2020 Lab2 System calls
Trap 的旅途
这一个 lab 实验反而是其次,我觉得更重要的是熟读并理解 book-riscv-rev1
第 4 章中的内容。以下内容尤其重要。
进程数据结构体
1 | enum procstate { UNUSED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE }; |
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 函数入口处
这里需要注意几点:
- 每个进程都是一座孤岛,都有自己独立的虚拟地址空间。
- uservec 这段代码存储在 TRAMPOLINE 这一物理页中。
- TRAMPOLINE 会被映射到每一个进程的同一处虚拟地址空间中(包括内核)(相当于共享这一物理页的同时,虚拟地址都一样)。
- 内核在创建进程时会为每一个进程分配 proc 结构体保存进程的状态并存储在内核空间中;除此之外还会为每个进程创建一页 TRAPFRAME 页(它将被映射到进程虚拟地址空间的 TRAMPOLINE 页的下面一页,即 TRAPFRAME + 4096 == TRAMPOLINE)用于进程陷入内核时保存 user mode 的寄存器状态。
1 | .globl uservec |
usertrap (以下皆在内核空间)
trap.c 中的 usertrap 函数
- 判断是否从 user mode 进到该函数的 (因为还有个 kerneltrap)
- 获取当前进程的 proc 结构体指针 p
- 将陷入内核的进程的 pc 保存到 p->trapframe->epc 中
- 判断系统调用还是中断还是异常,根据不同的情况做不同的操作 (我们这里只考虑系统调用)
- 调用 syscall 函数
1 | void |
可以看到在 usertrap 中关于此次 trap 的原因分为三类:系统调用、中断和异常。根据不同的情况做出不同的操作。我们这里只考虑系统调用,那么就是为了执行 syscall 函数。
syscall
这个函数是调用所有内核中的系统调用例程的总入口,也就是说所有的系统调用都要经过它。
1 | static uint64 (*syscalls[])(void) = { |
可以看到所有的系统调用例程都被注册成了函数指针的形式。
syscall 函数就是根据进程陷入内核时传入的系统调用号来判断具体要去执行哪个函数指针。系统调用号会存储在 proc::trapframe::a7
中。返回值会保存在proc::trapframe::a0
中。
为什么存在 a7 中呢?这是因为在调用 ecall 前我们自己存放到 a7 寄存器的,在进入 uservec 后被 save 到 proc::trapframe::a7
中的。具体可以看 user/usys.S
,这个函数编写了系统调用 wrapper:
1 | #include "kernel/syscall.h" |
usertrapret
这一函数在 kernel/trap.c
中。它要做的就是还原一些状态,为下次进程再次陷入内核做准备。
首先必要做的就是重新设置 stvec 控制状态寄存器的值为 uservec,为下次再次陷入内核做准备。其次将 内核页表、内核栈地址、usertrap 地址、core num 存入 TRAPFRAME 页中,供 userret 加载到寄存器中。重新设置 sstatus 控制状态寄存器。因为 sret 的时候,硬件会把 sepc 中的值拷贝到 pc 中,因此也要设置 sepc。并且获取到用户页表。
1 | void |
userret
这函数在 trampoline.S 中,它可以看成是 uservec 的镜像(对着干,uservec 是将寄存器的值 save 到内存,而它是将内存中的值 load 到寄存器)。这也是为了下次进程陷入内核做准备。
1 | .globl userret |
看完第三章的内容并彻底理解后,lab 就随便做做啦,洒洒水啦~~~