✦ 问得非常好!这是理解 Trap 机制的最后一块关键拼图。kernelvec 的存在解决了这样一个问题:如果 Trap 发生时,CPU 已经处于内核态(Supervisor Mode)了,该怎么办?
我们来梳理一下 kernelvec、uservec 和 stvec 这三者的关系。
核心矛盾:单一的 stvec vs 两种 Trap 场景
RISC-V 硬件规定,无论 Trap 来自哪里(用户态还是内核态),CPU 都只会做一件事:跳转到 stvec 寄存器指向的唯一地址。
但是,处理来自用户态的 Trap 和处理来自内核态的 Trap,需求是完全不同的:
- 用户态 Trap (User Trap):
- 需要:保存完整的用户寄存器上下文。
- 需要:从用户页表切换到内核页表。
- 需要:切换到进程的内核栈。
- 处理器:
uservec->usertrap()
- 内核态 Trap (Kernel Trap):
- 场景: 最常见的例子是,当内核正在执行代码时,一个硬件中断(比如定时器中断)到来了。
- 不需要: 切换页表(因为已经是内核页表了)。
- 不需要: 切换栈(因为已经在内核栈上运行了)。
- 需要: 只需要保存一部分寄存器(因为 C 语言的调用约定会保证另一部分寄存器的保存),然后跳转到一个更简单的 C 处理函数。
- 处理器:
kernelvec->kerneltrap()
为了解决这个矛盾,xv6 采取了一个非常聪明的策略:在进入和离开内核时,动态地修改 stvec 寄存器的值。
kernelvec 的作用和实现
kernelvec 是专门为处理内核态 Trap 设计的汇编代码。它位于 kernel/kernelvec.S。
可以看到,kernelvec 的工作流程比 uservec 简单得多:
- 在当前的内核栈上分配 256 字节的空间。
- 保存一些关键的寄存器(
ra,sp,gp,a0-a7等)。注意它没有保存所有寄存器,因为它假设自己遵守了 C 语言的调用约定。 call kerneltrap:直接调用 C 函数kerneltrap(位于kernel/trap.c)。kerneltrap返回后,恢复寄存器。sret:从内核 Trap 中返回,继续执行之前被中断的内核代码。
三者关系的动态切换
现在我们把整个流程串起来,看看 stvec 是如何在这两者之间切换的。
- 程序启动时: 内核初始化时,
trapinithart()函数会将stvec设置为kernelvec的地址。因为此时只有内核在运行。 -
当一个用户程序将要运行时: 在
usertrapret函数(即从任何系统调用或中断返回到用户空间之前)的最后,会执行以下代码:// in usertrapret() from kernel/trap.c // send syscalls, interrupts, and exceptions to uservec in trampoline.S uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline); w_stvec(trampoline_uservec); // ... then jump back to userret in trampoline.S这行
w_stvec把stvec的值改成了uservec的地址。这是在告诉 CPU:“接下来我要去运行用户代码了,如果再发生 Trap,请跳转到uservec去处理”。 -
当一个用户态 Trap 发生后: CPU 跳转到
uservec,经过一系列操作后,最终调用了 C 函数usertrap。在usertrap的开头,有这样一行代码:// in usertrap() from kernel/trap.c // send interrupts and exceptions to kerneltrap(), // since we're now in the kernel. w_stvec((uint64)kernelvec);这行代码又把
stvec的值改回了kernelvec的地址。这是在告诉 CPU:“我现在已经安全进入内核了,在处理这个 Trap 的期间如果又发生了新的 Trap(比如定时器中断),请跳转到kernelvec去处理”。
总结
stvec: 是一个硬件寄存器,是 CPU 唯一认识的 Trap 入口地址。它的值是动态可变的。uservec: 是一个软件定义的汇编函数,是处理用户态 Trap 的入口。当内核准备好要运行用户代码时,它会把uservec的地址写入stvec。kernelvec: 是另一个软件定义的汇编函数,是处理内核态 Trap 的入口。当代码从用户态进入内核态后,内核会立刻把kernelvec的地址写入stvec,以防在内核执行期间发生新的中断。
我们可以用一个简单的状态图来表示这个关系:
+---------------------+ Trap from User +--------------------+
| Running User Code | ---------------------> | Running Kernel Code|
| (stvec = uservec) | <--------------------- | (stvec = kernelvec)|
+---------------------+ Return to User (sret) +--------------------+
^ |
| | Nested Trap (e.g., timer interrupt)
| V
| +--------------------+
+------------------| (still in kernel) |
| (stvec = kernelvec)|
+--------------------+
所以,uservec 和 kernelvec 就像是两个“值班表”,内核根据当前 CPU 在哪个“地盘”(用户态还是内核态)干活,就把对应的“值班人员”(uservec 或 kernelvec)的地址填入 stvec 这个“岗位”上。