6 - Isolation & System Call Entry_Exit
Trap
硬件:
在 Risc-V 的 CPU 中会有 32 寄存器(registers),其中一些是特殊的寄存器:
栈指针 x2 sp (stack pointer)
程序计数器 pc (program counter)
satp (satp) ,指向页表的指针,用于页表的查找
stvec (Supervisor Trap Vector Base Address) ,指向中断向量表的指针,用于处理 kernel 中的 trap
sepc (sepc) ,保存触发 trap 的指令地址,指向当前正在执行的指令的地址,用于保存系统调用的入口和出口,pc
sscratch (sscratch) ,用于保存 user 态和 kernel 态之间的切换状态
当 mode 是 user 模式的时候,拥有的权限是可以读写少数 control 寄存器,只能使用 PTE_U 的 PTE ,如果在 super 模式时,所有寄存器都可以读写,但是默认不使用 PTE_U 的 PTE。
在 trap 发生的之前, CPU 的所有状态都设置为运行 user 代码,而不是 kernel 代码。操作系统需要实现几点:
需要透明的恢复 user 代码:特别是在 user 预期设备不会中断的情况下,希望让 kernel 实现中断,然后恢复 user 代码,而 user 感觉不到这个过程,因此不能让 kernel 干扰这个过程,也需要将原本的 kernel 的寄存器信息保存起来;
需要实现安全和隔离: user 的代码不能干扰 user kernel 的转换,也不能依赖任何 user 态传递过来的东西,它们可能有恶意。 xv6 中,甚至不会去查看寄存器的值,只是保存起来。
在系统当中,有 user 态和 kernel 态, user 态有 user 程序,,例如 shell ,当 shell 希望进行系统调用时,例如 write 。
首先会将模式切换到 super 模式,因为需要使用到 kernel 的权限, satp 目前之前 user 页表,它只包含 user 程序需要的映射,但是 user 的页表没有 kernel 页表的映射,因此需要切换页表到 kernel 页表,栈指针需要指向 kernel 中某个位置的栈等等,切换完所有的寄存器状态。
当 shell 调用 write 时,write 会调用 ecall 指令,直接跳到 super mode
之后的第一条指令是用 assembled 编写的
uservec(),这是 trampoline 的一部分,它在 trampoline.S 中实现。
接着执行 trap.c 中的usertrap()usertrap()会调用syscall(),syscall()会查看系统调用号 sys_write ,然后执行对应的系统调用
当系统调用执行结束,返回时sys_write()会返回到syscall()中syscall()会返回到usertrap()
然后usertrap()会调用usertrapret()
最后会返回到trampoline.S中的userret()中
对于寄存器:
- ecall 前(用户态)
- 寄存器全是用户值:a7=系统调用号,a0..=参数,sp/ra… 也是用户的。
- 执行 ecall(硬件行为)
- 切到 S 态;sepc ← ecall 的地址;scause ← U-ecall;pc ← stvec;GPR 原封不动(仍是用户值)。
- 入口汇编 trampoline.S:uservec
- 从 sscratch 取到当前进程的 trapframe 指针。
- 把所有用户 GPR(ra、sp、a0..a7、…)保存到 trapframe。
注:这一步“顺带”保存了系统调用参数和号,因为它们就在 a0..a7 里。 - 切到内核页表(写 satp + sfence.vma)、切到该进程的内核栈。
- 跳到 C 函数 trap.c:usertrap。
- trap.c:usertrap(内核 C 入口)
- p->trapframe->epc = r_sepc()(把硬件 CSR 里的 sepc 拷到 trapframe,用作返回的 pc)。
- 若是系统调用:p->trapframe->epc += 4(返回时跳过 ecall);然后调用 syscall()。
- syscall() 分发与执行
- 从 p->trapframe->a7 取系统调用号;从 p->trapframe->a0.. 取参数。
- 对指针参数用 copyin/copyout 校验并拷贝,避免直接信任用户地址。
- 调用具体 sys_* 实现;把返回值写回 p->trapframe->a0。
- 返回用户态准备 usertrapret → trampoline.S:userret
- 把 stvec 设回 uservec(为下一次 U→S 做好入口)。
- userret:设置 sstatus.SPP=U;w_sepc(trapframe->epc);切回用户页表;从 trapframe 逐一恢复所有 GPR(此时 a0 已是返回值);sret 回到用户态,执行 ecall 之后的一条指令。
sequenceDiagram
participant User as 用户态 (User)
participant HW as 硬件 (CSR)
participant ASM as 汇编 (trampoline.S)
participant Kernel as 内核 C (trap.c)
User->>HW: 执行 ecall 指令
HW->>HW: U 态→S 态,保存 PC 到 sepc
HW->>ASM: 跳转到 stvec (uservec)
ASM->>ASM: 交换 sp & sscratch (保存用户栈)
ASM->>ASM: 保存所有寄存器到 trapframe
ASM->>ASM: 切换 satp (内核页表) & sp (内核栈)
ASM->>Kernel: 调用 usertrap()
Kernel->>Kernel: 识别系统调用,epc += 4
Kernel->>Kernel: 执行 syscall() (可能 yield)
Kernel->>ASM: 返回 usertrapret() -> userret()
ASM->>ASM: 恢复寄存器,切换回用户页表
ASM->>HW: 执行 sret
HW->>User: S 态→U 态,恢复 PC (sepc)ecall 会将模式设置为 super ,然后将程序计数器保存到 sepc 中,然后将程序计数器设置为 stvec 的寄存器
ecall 所做的事情十分少,是因为 RISC-V 的设计者希望软件有更大的灵活性。