Trap
在 RISC-V 中,Trap(陷阱)并不是 CSAPP 所述的 exception 的一种,其是一个总称:CPU 因为某个事件暂时打断当前指令流,转入更高特权级的处理程序。
也就是说,用户态和内核态的切换主要是通过 trap 来实现,通常包括三类:
- 系统调用:程序主动请求内核服务(如
read,fork)。用户程序执行ecall指令陷入内核。 - 异常:程序运行中产生的错误(如除零、缺页、非法指令)。CPU 自动触发异常处理流程。
- 硬件中断:外部设备或时钟等异步事件。
其次,切换过程中最关键的是上下文的保存与恢复。 但在 RISC-V/xv6 中,硬件只完成最小的一组动作:
- 记录 trap 发生位置和原因、切换到 supervisor mode,并跳转到
stvec指定的入口 - CPU 不会自动切换到内核栈,也不会自动把通用寄存器压入栈;这些工作由 xv6 的汇编入口(如
uservec、kernelvec)和后续 C 代码完成
xv6 对 trap 的处理存在通用的框架,通常的顺序是:
- trap 强制将控制权转移到内核中;
- 内核保存寄存器和其它状态;
- 内核执行处理程序代码;
- 内核恢复保存的状态并从 trap 中返回,原始代码从中断处继续。
但针对用户空间 trap、内核空间 trap 和计时器中断这三种特殊情况,设计了独立的处理路径。
- 处理 trap 的内核代码称为 trap handler
- CPU 发生 trap 后跳去执行的第一段入口代码,称为 vector
RISC-V 对 Trap 的硬件支持
每个 RISC-V CPU 都有一组特权寄存器,内核写入这些控制寄存器来告诉 CPU 如何处理 trap,并且内核可以读取这些寄存器来找出已发生的 trap。
重要特权寄存器概述:
stvec:trap 入口地址。trap 发生后,CPU 跳到这里执行,也就是 vector。sepc:保存 trap 发生前的用户/内核pc。sret会把它恢复到pc。scause:放置一个数字来描述 trap 的原因sstatus:保存/控制特权状态。SIE:当前是否允许 supervisor 中断SPP:记录 trap 前来自 user 还是 supervisorSPIE:sret返回后用它恢复中断开启状态
sscratch:给 trap 入口代码临时使用。xv6 用它保存/交换TRAPFRAME地址和用户a0。satp:当前页表的根地址
上述寄存器与内核模式下处理的 trap 相关,并且不能在用户模式下读取或写入。
RISC-V 硬件在 trap 发生时会执行以下最小动作:
- 中断屏蔽检查
- 如果 trap 是由设备中断引发的,且
sstatus.SIE=0,则处理器会暂存该中断,暂缓执行 - 如果是异常或系统调用,会跳过该步骤
- 如果 trap 是由设备中断引发的,且
- 禁用中断
- 设置
sstatus.SIE=0:防止 trap 处理期间被其他中断嵌套
- 设置
- 保存上下文
sepc:保存当前pc,以便 trap 返回时恢复执行sstatus.SPP:保存当前特权模式(0=user, 1=kernel)
- 设置 trap 原因
scause:记录 trap 类型(中断或异常)和具体原因(如中断号或异常码)。
- 切换管理模式
- 将当前模式设置为内核态,以便执行内核空间中的 vector
- 跳转到 trap handler
- 将
stvec的地址加载到pc
- 将
以上步骤为硬件操作,在发生 trap 后自动执行,并没有显式代码。
未完成的步骤(需软件处理):
- 切换页表:CPU 不会自动切换页表
- 切换栈指针:CPU 不会自动切换栈
- 保存寄存器:通用寄存器需由软件保存
CPU 保留上述步骤交给软件处理是为软件提供灵活性,比如某些操作系统在某些情况下会省略页表切换,硬件仅提供最小必要的支持。
在 xv6 中,来自用户态的 trap 由 trampoline 页中的 uservec 完成这些准备工作;来自内核态的 trap 则走 kernelvec,因为此时已经在内核页表和内核栈上。
User Trap
以下情况属于 user trap:
- system call (
ecall指令) - 非法指令
- 设备中断
发生 trap 时,依次执行以下处理程序(执行完硬件操作后):
uservec:保存程序状态、切换页表和栈指针、跳转到usertrapusertrap:分类处理 trap,之后跳转到usertrapret进行返回- 返回时,
usertrapret:重置特权寄存器,准备返回用户模式 - 然后,
userret:切换用户页表、恢复程序上下文,并通过sret真正回到用户态
用户虚拟地址空间:

Trapframe
trapframe 用来保存“用户态现场”,这样 trap 处理完以后还能回到原来的用户程序继续跑。
其主要保存两类东西:
// 1. 用户寄存器
...
// 2. trap 返回/再次进入内核需要的信息
kernel_satp 内核页表
kernel_sp 当前进程的内核栈顶
kernel_trap usertrap() 的地址
epc 用户程序返回地址
kernel_hartid 当前 CPU/hart id
每个进程都有自己的 trapframe,同时,这块物理内存会映射到该进程用户页表顶部附近的固定虚拟地址。
- 这个虚拟地址是给
trampoline用的,用户态代码没有访问权限
问题:为什么要映射到用户页表里?
用户态发生 trap 后,CPU 刚跳到 uservec 时,虽然已经处于 supervisor mode,但还在使用用户页表。这时还没切换到内核页表。
所以 uservec 如果要保存用户寄存器,必须能在当前页表下找到一块内存。这个地址就是 trapframe。之后再从 trapframe 中取内核页表进行切换。
xv6 把 trapframe 和 trampoline 同时映射到用户页表和内核页表,并且地址都是相同的。这样切换页表前后,CPU 仍然可以连续执行以及读写数据。
Trampoline
trampoline 是 xv6 放在固定虚拟地址上的一小页汇编代码,用来完成用户态和内核态之间最危险的过渡。
它里面主要有两段代码:
uservec:用户态 trap 进入内核时执行
userret:内核返回用户态时执行
trapframe 是数据页,而 trampoline 则是代码页。
Uservec
uservec 核心任务是:在还没有真正进入普通内核 C 代码之前,把用户态现场保存好,并把执行环境切换成内核能正常运行的状态。
其会执行:
1. 用 sscratch 找到 TRAPFRAME
2. 保存用户寄存器到 trapframe
3. 从 trapframe 取 kernel_sp、kernel_satp、kernel_trap
4. 切换到内核栈
5. 切换到内核页表
6. 跳到 usertrap()
这里存在一个状态的转变:
uservec开始时:supervisor mode + 用户页表uservec结束时:supervisor mode + 内核页表 + 内核栈
所以它是“进入内核”的桥。
Userret
userret 负责返回用户态。
内核处理完 trap 后,usertrapret() 会准备好:
sepc = 用户程序返回地址
sstatus.SPP = user
sstatus.SPIE = 1
stvec = 下一次用户 trap 的 uservec 地址
然后跳到 trampoline 里的 userret。
userret 做这些事:
1. 切换到用户页表
2. 从 trapframe 恢复用户寄存器
3. 用 sscratch 恢复用户 a0
4. 执行 sret
最后 sret 会:
pc = sepc
特权级 = user
于是回到用户程序。
也就是说:
userret开始时:supervisor mode + 内核页表userret中途:supervisor mode + 用户页表userret结束时:user mode + 用户页表
所以它是“返回用户”的桥。
Usertrap
usertrap 是用户态 trap 进入内核后的分发器。
执行步骤:
- 将
stvec更新为kernelvec(现在已经在内核里了,再次发生 trap 时应该由kernelvec处理) - 保存
sepc到p->trapframe->epc- 防止再次引发 trap 时覆盖用户返回地址
- 根据
scause处理不同类型的trap:- 系统调用
p->trapframe->epc += 4,指向ecall的下一条指令,后续由usertrapret()写回sepc- 启用中断
- 调用
syscall()
- 设备中断:调用
devintr() - 异常:终止出错的进程
- 系统调用
- 调用
usertrapret()准备返回
Usertrapret
当 trap 处理完成后,调用 usertrapret 准备返回用户态:
- 关闭中断:防止过渡状态被打断
- 把
stvec改回uservec,确保下次用户态 trap 能够调到uservec - 设置
trapframe中的内核字段,使其在下次触发 trap 时被使用kernel_satp:内核页表地址kernel_sp:内核栈指针kernel_trap:usertrap的地址
- 设置
sstatus,决定sret返回到哪里SPP = 0:回到用户态SPIE = 1:sret返回用户态后重新启用中断
- 设置
sepc:将p->trapframe->epc写到sepc(无论其是否被覆盖),方便后续userret返回到用户程序 - 准备用户页表
satp,后续传递给userret - 调用
userret函数,并传递两个参数:trapframe和satp- 写
satp,切到用户页表 - 从
trapframe恢复用户寄存器 - 恢复用户
a0 - 执行
sret
- 写