xv6 traps and system callls
trap
trap有三种类型
- system call
- exception
- interrupt
每次触发trap的时候我们都会从用户态陷入到内核态, 并且这个过程对于用户程序来说应该是没有感知的, 在执行完trap以后, 从内核态回到用户态.
所有的trap都只在内核中执行, 这样能保证对物理设备访问的隔离性, 也能在处理异常的时候可以做出像kill用户进程这样的内核才有权限执行的响应方式
完成一个trap需要四步
- 硬件CPU上的动作
- 准备好的汇编代码, 用于进入到内核中对印的trap处理c函数上
- 处理这个trap的c函数
- 内核运行代码的内核进程
RISC-V寄存器
指令
- 用于系统控制的特殊的寄存器, 不能随便地读写, 需要特殊的指令才能读写
csrr: CSR寄存器 readcsrw: CSR寄存器 write
寄存器
- 系统控制寄存器
- sstatus: Supervisor Status Register监督者状态
- SPP: (bit 8, 1L<< 8) 记录异常发生前的CPU的特权级别, 1 = 来自Supervisor, 0 = 来自User
- SIE: (bit 1, 1L<<1) Supervisor 中断使能 (0=关闭, 1=开启)
- SPIE: (bit 5, 1L<<5) 陷阱前的中断使能状态
- stvec: Supervisor Trap Vector 监督者陷阱向量
- 指向发生trap的时候, CPU接下来要跳转到函数的地址
- sepc: Supervisor Exception Program Counter 监督者异常pc
- 保存发生trap的时候的指令地址, 以便异常处理完成以后能够返回
- scause: Supervisor Cause Register 监督者原因
- 记录陷入trap的原因
- 8: 系统调用(ecall from U-mode)
- 13: Load page fault (读页面错误)
- 15: Store page fault (写页面错误)
- 记录陷入trap的原因
- stval: Supervisor Trap Value 监督者陷阱值
- 用于提供额外的信息, 对于页面错误会导致错误的虚拟地址, 对于非法指令, 会提供指令本身
- satp: Supervisor Address Translation and Protection 监督者地址转换和保护
- 控制页表的地址, 用于虚拟地址到物理地址的转换, 准备用户进程的页表地址, 用于返回用户态
- 访问函数:
#define MAKE_SATP(pagetable) (SATP_SV39 | (((uint64)pagetable) >> 12)) - 使用:
uint64 satp = MAKE_SATP(p->pagetable)
- sstatus: Supervisor Status Register监督者状态
xv6中进入到一个trap的过程讲解
用户态陷入trap的处理函数
kernel/trap.c: usertra
- 校验此刻CPU是不是U-Mode
- 将kernelvec函数的地址写入到stvec, 接下来执行kernelvec代码(在下一个小部分)
- 保存用户进程的pc(从sepc读取)
- 处理不同的异常情况 (读取scause的值)
- 8 -> 来自系统调用:
- 将sepc的值 += 4, 指向下一条指令, 从而在返回的时候是返回到出现trap的指令的下一条指令
- Supervisor Interrupt Enable, 将sstatus的bit 1设置为1, 开启中断, 允许系统在处理系统调用时响应其他的中断(比如timer系统终中断)
- 执行syscall()函数, 调用对应的syscall函数
- 13 / 15 ->
- 8 -> 来自系统调用:
- 如果这是个timer中断, 让出cpu
- prepare_return()
- 切换到用户页表
kernel/kernelvec.S
- 声明入口点
1 | .globl kerneltrap # 将kerneltrap声明为全局的符号 |
- 向下增长栈, 为保存通用寄存器留出空间
1 | # make room to save registers. |
- 保存用户态的寄存器
RISC-V调用约定中, 寄存器分成两类
- caller-saved registers: 调用函数前, 调用者必须保存这些寄存器
- callee-saved registers: 被调用函数必须保存和恢复这些寄存器
这里我们只保存了caller-save register的原因是C函数会遵循RISC-V的约定, 保存callee-save register, 所以我们只用保存另一部分
1 | # save caller-saved registers. |
- 调用kerneltrap C函数
1 |
|
- 恢复caller-saved寄存器
1 | # restore registers. |
1 | addi sp, sp, 256 # 将栈指针移回256字节前 |
sret指令: 行为
- 将PC设置成sepc的值 (保存的是发生trap的时候的pc)
- 将特权级别恢复成sstatus.SPP中保存的值
- 将中断使能恢复成sstatus.SPIE中保存的值
- 继续执行被中断的代码
trap.c/kerneltrap函数
用户态trap处理函数的返回准备函数
kernel/trap.c: prepare_return
- intr_off(): 关闭中断
小结: trap的调用链全过程
为什么同时要有kerneltrap和usertrap
关键点就在于我们同时有在kernel mode中和user mode中处理trap的需求, 而这两个mode执行环境存在不同
- 页表不同
- 最后要使用的都是内核页表
- kernel mode -> 不用切换页表
- user mode -> 需要将页表切换成内核页表
- 最后要使用的都是内核页表
- 栈不同
- 需要使用内核栈
- kernel mode -> 已经使用了内核栈
- user mode -> 需要切换到内核栈
- 需要使用内核栈
- 保存的上下文不同
- kernel mode -> 只需要保存几个关键寄存器 (如pc等), 其他的寄存器由调用约定保护
- user mode -> 需要保存完整的user mode的寄存器到 trapframe
- 返回方式不同
- user mode -> 需要切换页表和特区级别
- kernel mode -> 直接正常返回即可
trap的类型中的interrupt和exception是我们在执行内核代码的时候也会随时发生的事情(尤其是interrupt, 如时钟中断和设备中断)
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.