✦ 问得非常好!这是理解 Trap 机制的最后一块关键拼图。kernelvec 的存在解决了这样一个问题:如果 Trap 发生时,CPU 已经处于内核态(Supervisor Mode)了,该怎么办?

我们来梳理一下 kernelvecuservecstvec 这三者的关系。


核心矛盾:单一的 stvec vs 两种 Trap 场景

RISC-V 硬件规定,无论 Trap 来自哪里(用户态还是内核态),CPU 都只会做一件事:跳转到 stvec 寄存器指向的唯一地址。

但是,处理来自用户态的 Trap 和处理来自内核态的 Trap,需求是完全不同的:

  1. 用户态 Trap (User Trap):
    • 需要:保存完整的用户寄存器上下文。
    • 需要:从用户页表切换到内核页表。
    • 需要:切换到进程的内核栈。
    • 处理器:uservec -> usertrap()
  2. 内核态 Trap (Kernel Trap):
    • 场景: 最常见的例子是,当内核正在执行代码时,一个硬件中断(比如定时器中断)到来了。
    • 不需要: 切换页表(因为已经是内核页表了)。
    • 不需要: 切换栈(因为已经在内核栈上运行了)。
    • 需要: 只需要保存一部分寄存器(因为 C 语言的调用约定会保证另一部分寄存器的保存),然后跳转到一个更简单的 C 处理函数。
    • 处理器:kernelvec -> kerneltrap()

为了解决这个矛盾,xv6 采取了一个非常聪明的策略:在进入和离开内核时,动态地修改 stvec 寄存器的值。


kernelvec 的作用和实现

kernelvec 是专门为处理内核态 Trap 设计的汇编代码。它位于 kernel/kernelvec.S

可以看到,kernelvec 的工作流程比 uservec 简单得多:

  1. 在当前的内核栈上分配 256 字节的空间。
  2. 保存一些关键的寄存器(ra, sp, gp, a0-a7 等)。注意它没有保存所有寄存器,因为它假设自己遵守了 C 语言的调用约定。
  3. call kerneltrap:直接调用 C 函数 kerneltrap (位于 kernel/trap.c)。
  4. kerneltrap 返回后,恢复寄存器。
  5. sret:从内核 Trap 中返回,继续执行之前被中断的内核代码。

三者关系的动态切换

现在我们把整个流程串起来,看看 stvec 是如何在这两者之间切换的。

  1. 程序启动时: 内核初始化时,trapinithart() 函数会将 stvec 设置为 kernelvec 的地址。因为此时只有内核在运行。
  2. 当一个用户程序将要运行时: 在 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 去处理”。

  3. 当一个用户态 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)|
                            +--------------------+

所以,userveckernelvec 就像是两个“值班表”,内核根据当前 CPU 在哪个“地盘”(用户态还是内核态)干活,就把对应的“值班人员”(userveckernelvec)的地址填入 stvec 这个“岗位”上。