控制寄存器
trap 机制的核心是一组控制状态寄存器,它们不占用通用寄存器编号,用专门的指令读写
本节涉及的六个寄存器都工作在S模式:stvec、sepc、scause、stval、sscratch、sstatus。用户模式不能读写它们
stvec:保存 trap 处理程序的地址。硬件受理 trap 时把PC设为stvecsepc:硬件受理 trap 时把当时的PC保存到sepc。sret(从 trap 中返回)指令将sepc复制到pc中scause:放了一个数字,保存 trap 的原因sscratch:进入 trap 时的临时寄存器,用法详见trampoline部分sstatus:状态位。xv6 只使用其中三位- SIE(中断总开关):控制设备/定时器中断是否被受理
- SPIE:
sret后SIE的恢复值 - SPP:记录 trap 发生前的特权级(1=内核,0=用户),
sret依据它决定返回哪个模式
stval:trap 发生时的附加信息。对于 page fault,它保存了访问的虚拟地址
M 模式有一组功能对应的寄存器:mstatus、mepc、mtvec、mcause、mscratch。xv6 在启动阶段使用它们
硬件执行trap
当受理一个trap(包括设备中断和异常)时,硬件自动执行如下步骤
- 判断
sstatus.SIE是否启用了中断。若为0,则不受理,后续步骤全部不执行,直到SIE被重新置 1 - 清除
sstatus.SIE,禁止中断,防止嵌套 - 将
pc保存到sepc - 将当前模式(S模式或U模式)保存到
sstatus.SPP - 将
scause设置为 trap 的原因 - 模式转为S模式
- 将
pc设置为stvec - 执行新的
pc,跳转到stvec指向的 trap 处理程序
异常(ecall、页故障等)不经过第 1 步的 SIE 判断,一定受理。SIE 只影响中断
由于设置了S模式,因此必须把pc设置为trap处理程序的地址,防止用户程序在S模式下运行
特殊页
用户态 trap 的处理有两个硬性约束:
- trap 受理后,
PC已经跳到stvec,但此时satp仍指向用户页表。除非内核代码在用户页表中也有映射,否则无法执行内核代码;而切换页表之后,PC指向的下一条取指又必须在新页表中有效。这使得必须有一段内核代码在用户态和内核页表中都有映射,且映射到同一个位置。 - 用户提供的用户栈
sp不可信,需要独立的页面保存寄存器等数据
xv6 用两个特殊页面解决这两个问题:TRAMPOLINE(代码页)和 TRAPFRAME(数据页)
TRAMPOLINE
TRAMPOLINE 是一个只读的内核代码页,映射在所有页表中,且固定位于虚拟地址空间的最高端(MAXVA - PGSIZE)
该页没有 PTE_U,因此用户态不能读取或执行这一页
它的内容就是trampoline.S编译出的机器码,包含 uservec 和 userret 两个入口
这里便是trap受理后,硬件跳转到 stvec 的地址的地方。stvec 被设置为 TRAMPOLINE 的入口地址
TRAPFRAME
TRAPFRAME映射于用户页表TRAMPOLINE的下方,大小为一页
不同于TRAMPOLINE在所有页表中指向同一个物理页,TRAPFRAME是每个进程独有的,内核单独分配
内核页表中没有映射TRAPFRAME,内核直接通过p->trapframe访问它的地址
TRAPFRAME页内存放struct trapframe,保存了用户态的寄存器状态与系统信息
struct trapframe {
uint64 kernel_satp; // 内核页表
uint64 kernel_sp; // 进程内核栈顶
uint64 kernel_trap; // usertrap()
uint64 epc; // 保存的用户程序计数器
uint64 kernel_hartid; // 保存的内核 tp
uint64 ra;
// ... 中间寄存器省略 ...
uint64 t6;
};当进程被创建时,内核为其分配一页TRAPFRAME
每次返回用户态前,都会设置kernel_satp,kernel_sp,kernel_trap,kernel_hartid
用户态trap
总路径
- 用户程序执行时,发生 trap (ecall、异常、中断等),硬件执行跳转到
stvec,即TRAMPOLINE的入口地址 - 执行
trampoline.S中的uservec,保存用户态寄存器到TRAPFRAME,切换页表到内核页表,调用usertrap()处理 usertrap()判断类型,调用相应函数处理- 处理完毕,
prepare_return()布置返回所需的控制寄存器与 trapframe 字段,随后返回 usertrap()返回到uservec末尾也就是userret的入口,之后执行userretuserret切换页表到用户页表,恢复用户态寄存器- 执行
sret返回用户态
uservec
uservec是trampoline.S中定义的汇编函数
uservec:
csrw sscratch, a0 # 暂存用户 a0,腾出一个可用寄存器
li a0, TRAPFRAME # a0 = 本进程 trapframe 虚拟地址
sd ra, 40(a0) # 保存 31 个通用寄存器
...
sd t6, 280(a0)
csrr t0, sscratch # 取回用户 a0
sd t0, 112(a0) # 存入 trapframe->a0
ld sp, 8(a0) # kernel_sp → sp,切换到本进程内核栈
ld tp, 32(a0) # kernel_hartid → tp
ld t0, 16(a0) # kernel_trap(usertrap 地址)
ld t1, 0(a0) # kernel_satp(内核页表)
sfence.vma zero, zero # 确保上面的 store 已完成
csrw satp, t1 # 切换到内核页表
sfence.vma zero, zero # 清掉 TLB 中残留的用户页表项
jalr t0 # 调用 usertrap;ra = 下一条指令(userret)- 使用临时CSR寄存器
sscratch保存用户态的a0,腾出一个寄存器用于保存TRAPFRAME的地址。之后将寄存器全部存入TRAPFRAME - 读取
TRAPFRAME中已经填好了的内核数据,切换到内核栈与内核页表,之后跳转到设置好的kernel_trap(即usertrap())处理trap
要点:
- 所有对 trapframe 的读写都必须在
csrw satp之前完成。因为内核页表不映射TRAPFRAME虚拟地址,一旦切换,a0指向的地址就无法访问 - 用户
sp从头到尾只被当作数据保存,从未被用作栈。真正使用的栈是从 trapframe 中读出的kernel_sp
usertrap
- 判定 SPP 是否为 0,也就是必须是用户态进入,否则说明入口配置有误
int which_dev = 0;
if ((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
- 立即更换 trap 入口:从这一行
w_stvec((uint64)kernelvec)起中断和异常由kernelvec处理。如果仍保留uservec,内核态 trap 会再次进入uservec,处理流程会彻底错乱
// 将中断和异常发送给 kerneltrap(),
// 因为我们现在已在内核中。
w_stvec((uint64)kernelvec); //DOC: kernelvec- 保存用户 PC:
p->trapframe->epc = r_sepc()(trap.c:52)。之后会开中断并可能发生进程切换,sepc、scause、sstatus都可能被新的 trap 覆盖,必须现在保存。
struct proc *p = myproc();
// 保存用户程序计数器。
p->trapframe->epc = r_sepc();- 按 scause 分发:
scause == 8:先检查killed,然后pc + 4跳过 ecall 指令指向下一条,intr_on()开启中断,之后syscall()执行系统调用devintr() != 0:设备中断或定时器中断,设置局部变量which_devscause == 15 || scause == 13且vmfault(...) != 0(trap.c:71-74):页故障,尝试按懒分配处理- 其他情况(trap.c:75-79):打印信息,终止进程
if (r_scause() == 8) {
// 系统调用
if (killed(p))
kexit(-1);
// sepc 指向 ecall 指令,
// 但我们希望返回到下一条指令。
p->trapframe->epc += 4;
// 中断会改变 sepc、scause 和 sstatus,
// 所以只有在我们处理完这些寄存器后才启用中断。
intr_on();
syscall();
} else if ((which_dev = devintr()) != 0) {
// 正常
} else if ((r_scause() == 15 || r_scause() == 13) &&
vmfault(p->pagetable, p->sz, r_stval(),
(r_scause() == 13) ? 1 : 0) != 0) {
// 惰性分配页面的缺页异常
} else {
printk("usertrap(): unexpected scause 0x%lx pid=%d\n", r_scause(), p->pid);
printk(" sepc=0x%lx stval=0x%lx\n", r_sepc(), r_stval());
setkilled(p);
}
- 进程已被杀则
kexit(-1);若是定时器中断则yield()让出 CPU;调用prepare_return()
if (killed(p))
kexit(-1);
// 如果这是定时器中断,就让出 CPU。
if (which_dev == 2)
yield();
prepare_return();
// 要切换到的用户页表,供 trampoline.S 使用
uint64 satp = MAKE_SATP(p->pagetable);
// 返回到 trampoline.S;satp 值放在 a0 中。
return satp;prepare_return
这个函数为即将返回用户态做准备
void
prepare_return(void)
{
struct proc *p = myproc();
// 我们即将把陷阱的目标从 kerneltrap() 切换到
// usertrap()。因为从内核代码陷入到 usertrap 会是一场灾难,
// 所以关闭中断。
intr_off();
// 将系统调用、中断和异常发送到 trampoline.S 中的 uservec
uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(trampoline_uservec);
// 设置 uservec 在进程下次陷入内核时所需的 trapframe 值。
p->trapframe->kernel_satp = r_satp(); // 内核页表
p->trapframe->kernel_sp = p->kstack + PGSIZE; // 进程的内核栈
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // 用于 cpuid() 的 hartid
// 设置 trampoline.S 的 sret 将用来进入用户空间的寄存器。
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // 清除 SPP 为 0,表示用户模式
x |= SSTATUS_SPIE; // 在用户模式下启用中断
w_sstatus(x);
// 将 S Exception Program Counter 设置为保存的用户 pc。
w_sepc(p->trapframe->epc);
}函数操作两组数据,分别是硬件和内存数据
硬件数据,供本次返回使用
| 操作 | 对象 | 作用 |
|---|---|---|
intr_off() | sstatus.SIE | 准备过程中不允许嵌套 trap |
w_stvec(uservec地址) | stvec | 下次用户 trap 的入口 |
x &= ~SSTATUS_SPP | sstatus.SPP | 置 0,sret 将返回用户模式 |
x |= SSTATUS_SPIE | sstatus.SPIE | 置 1,sret 后用户态中断开启 |
w_sepc(p->trapframe->epc) | sepc | sret 的落点 |
trapframe 数据,供下次进入内核时使用
| 字段 | 值 |
|---|---|
kernel_satp | r_satp() |
kernel_sp | p->kstack + PGSIZE |
kernel_trap | usertrap 地址 |
kernel_hartid | r_tp() |
之后prepare_return()返回到usertrap(),usertrap()返回到userret()的入口
userret
执行userret()时,usertrap()返回了用户页表地址,存储在a0中。userret()将其写入satp,切换到用户页表
.globl userret
userret:
# usertrap() 返回这里,a0 = 用户进程的 satp。
# 刷新指令缓存,防止本核此前执行过同一虚拟地址上的旧代码
fence.i
# 切回用户页表。与 uservec 同理,
# 代码仍位于两张页表同址映射的 trampoline 页上
sfence.vma zero, zero
csrw satp, a0
sfence.vma zero, zero
# 用用户页表访问本进程的 trapframe
li a0, TRAPFRAME
# 恢复除 a0 之外的 30 个寄存器
ld ra, 40(a0)
# ... 中间寄存器省略 ...
ld t6, 280(a0)
# 最后恢复用户 a0:系统调用的返回值就在这里
ld a0, 112(a0)
# 返回用户态:PC←sepc,特权级←SPP,SIE←SPIE,SPIE←1
# sepc 与 sstatus 已由 prepare_return() 设置
sret