swtch.S 文件是 xv6-riscv操作系统中实现多任务切换(context switch)的魔法核心。
我们来一步步解析它。
1. 宏观视角:swtch 函数的作用
首先,看它的函数签名注释:
void swtch(struct context *old, struct context *new);
这个函数从 C 语言的角度看,它接受两个参数:
- a0 (第一个参数寄存器): 指向 old 上下文结构的指针。
- a1 (第二个参数寄存器): 指向 new 上下文结构的指针。
它的核心任务是:
- 保存当前执行环境:将当前的寄存器状态保存到 old 指针指向的内存中。
- 加载新的执行环境:从 new 指针指向的内存中,加载一套全新的寄存器状态。
- 返回到新的执行环境中:函数返回时,它不会返回到调用 swtch 的地方,而是返回到 new 上下文所记录的返回点。
这就是上下文切换的精髓:在一个函数调用中,我们“潜入”了另一个完全不同的执行流。
2. struct context 和涉及的寄存器
swtch.S 中保存和加载的寄存器,就是 struct context 结构体定义的内容。在 RISC-V 架构中,寄存器有不同的用途,并遵循一套“调用约定”(Calling Convention)。swtch 只保存和恢复那些被调用者保存(Callee-Saved)的寄存器。
为什么?因为调用者(Caller)负责保存它自己需要的临时寄存器(Caller-Saved Registers)。而被调用者(Callee,这里就是 swtch 函数)必须保证在它返回时,这些 Callee-Saved 寄存器的值和它被调用时一样。swtch 通过保存它们来遵守这个约定,但它巧妙地恢复了另一个上下文的值。
swtch.S 中涉及的寄存器如下:
- ra (Return Address Register): 返回地址寄存器。它存放着函数调用结束后应该返回到的指令地址。这是最重要的寄存器之一,因为它决定了代码的执行流。保存 ra 就是保存“我们从哪里来,要回到哪里去”。
- sp (Stack Pointer Register): 栈指针寄存器。它指向当前栈的栈顶。每个进程(或内核线程)都有自己的栈,保存 sp 就等于保存了整个函数调用链的状态。
- s0 - s11 (Saved Registers): 被调用者(calleed)保存寄存器。这些是通用寄存器,按照约定,如果一个函数要使用它们,必须在函数返回前将它们恢复到原始值。因此,它们的值在函数调用之间是“稳定”的,必须作为进程上下文的一部分被保存。
struct context 在 kernel/proc.h 中大致是这样定义的:
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
这个结构体的字段顺序和 swtch.S 中的保存/加载顺序是严格对应的。
3. swtch.S 逐行代码解析
我们把代码分成三个部分来看。
第一部分:保存旧上下文 (Save current registers in old)
.globl swtch
swtch:
sd ra, 0(a0) # sd = store doubleword. 将 ra 寄存器的值存到 a0 指向的地址 (old->ra)
sd sp, 8(a0) # 将 sp 存到 a0+8 的地址 (old->sp)
sd s0, 16(a0) # 将 s0 存到 a0+16 的地址 (old->s0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0) # 将 s11 存到 a0+104 的地址 (old->s11)
- a0 寄存器里放着 old 上下文的地址。
- sd 指令将一个 64 位(doubleword)的寄存器值存入内存。
- offset(reg) 是寻址方式,表示 reg 寄存器中的地址值 + offset。
- RISC-V 是 64 位架构,所以每个寄存器占 8 字节。因此偏移量以 8 递增。
- 执行完这部分代码后,调用 swtch 的那个执行环境(比如一个放弃 CPU 的进程,或者调度器本身)的 ra, sp, s0-s11 寄存器已经被完整地保存在了 old 指向的 struct context 内存中。
第二部分:加载新上下文 (Load from new)
ld ra, 0(a1) # ld = load doubleword. 从 a1 指向的地址加载值到 ra (ra = new->ra)
ld sp, 8(a1) # sp = new->sp
ld s0, 16(a1) # s0 = new->s0
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1) # s11 = new->s11
- a1 寄存器里放着 new 上下文的地址。
- ld 指令从内存中加载一个 64 位的值到寄存器。
- 这是最关键的一步。执行完这部分代码后,CPU 的核心寄存器已经被 new 上下文完全覆盖了。
- sp 寄存器现在指向了新任务的内核栈。
- ra 寄存器现在包含了新任务上次被切换出去时记录的返回地址。
第三部分:返回 (Return)
ret
- ret (return) 是一个伪指令,它等价于
jalr zero, 0(ra)。 - 它的作用是:跳转到
ra寄存器中存储的地址。 - 此时的 ra 是什么?是刚刚从
new上下文中加载进来的那个ra!
所以,ret 指令执行后,CPU 的程序计数器(PC)就指向了新任务被中断的地方,从而让新任务得以继续执行。swtch 函数的调用彻底完成,但它返回到了一个全新的世界。
比喻解释
工作台比喻
-
CPU 核心 (CPU Core): 想象成一个工匠的工作台。在任何时刻,一个工作台只能用于一个项目。如果你的电脑是 4 核 CPU,那就有 4 个独立的工作台。
- 寄存器 (
ra,sp,s0-s11等):这些是工作台上面的工具和正在加工的零件。- sp (栈指针) 就像是你的“待办事项清单/流程图”,告诉你当前步骤在整个项目流程中的位置。
- ra (返回地址) 就像是你刚刚完成一个子任务后,写在便签上的“下一步该做什么”。
- s0-s11 等通用寄存器就像是你手边最常用的螺丝刀、扳手,上面沾着当前项目的零件。
- 关键点:这些工具和零件就在工作台上,是当前正在进行的工作的唯一体现。它们是“现场”,不是“档案”。
- 内存 (RAM):这是工作台旁边的巨大货架。上面放着很多项目盒子。
-
进程 (Process): 货架上的每一个项目盒子都代表一个进程。比如“项目A:造一辆玩具车”、“项目B:修一个收音机”。
struct context: 这就是项目盒子里的“状态托盘”。当你需要暂停一个项目时,你把工作台上的所有工具(寄存器)原样不动地摆放到这个托盘里,然后把托盘放回项目盒子,再把盒子放回货架。
总结
swtch.S 的整个流程就像一个精密的“偷天换日”:
- 一个进程(或调度器)调用 swtch。
- swtch 把这个调用者的核心寄存器打包存好(存入 old)。
- 然后,swtch 打开另一个之前打包好的包裹(new),把里面的寄存器状态装载到 CPU 上。
- 最后,swtch 执行 ret,但这个返回操作会把 CPU 带到新包裹所记录的目的地。