2. xv6 内核架构概览:进程调度、中断处理与并发基础
在深入调试 spinlock 竞争条件之前,必须建立对 xv6 内核整体架构的清晰认知。本章从进程调度、中断处理与并发模型三个维度,拆解 xv6 的核心运行机制,为后续分析锁竞争提供必要的上下文。
2.1 xv6 内核整体结构
xv6 是一个基于 宏内核 设计的类 Unix 教学操作系统,所有内核服务(进程管理、内存管理、文件系统、设备驱动)运行在同一个内核地址空间,共享同一个页表。这种设计简化了模块间通信,但也意味着任何模块的并发错误都可能影响整个系统。
2.2 进程调度:上下文切换与调度器
xv6 的进程调度采用 协作式 + 时钟中断抢占 的混合模型。每个 CPU 核心运行独立的调度器线程,通过 scheduler() 函数循环选择 RUNNABLE 状态的进程执行。
2.2.1 进程状态机
| 状态 | 枚举值 | 含义 | 典型触发操作 |
|---|---|---|---|
| UNUSED | 0 | 进程槽位空闲 | 进程退出后回收 |
| USED | 1 | 进程已分配但未就绪 | allocproc() 初始化 |
| SLEEPING | 2 | 进程因等待事件阻塞 | sleep() 调用 |
| RUNNABLE | 3 | 进程可被调度执行 | wakeup() 或 yield() |
| RUNNING | 4 | 进程正在 CPU 上执行 | scheduler() 选中 |
| ZOMBIE | 5 | 进程已退出,等待父进程回收 | exit() 后 |
2.2.2 上下文切换流程
xv6 的上下文切换通过 swtch.S 汇编函数实现,保存当前 CPU 的寄存器状态到 context 结构体,然后恢复目标进程的上下文。关键路径如下:
- 时钟中断触发:每个 CPU 的定时器中断调用
trap()→usertrap()或kerneltrap()。 - yield() 主动让出:中断处理中调用
yield(),将当前进程状态设为RUNNABLE,然后调用sched()。 - sched() 执行切换:
sched()调用swtch(),保存当前进程的context,恢复当前 CPU 调度器线程的context。 - scheduler() 选择新进程:调度器遍历进程表,找到下一个
RUNNABLE进程,调用swtch()切换到该进程。
sched() 和 scheduler() 之间,进程状态 p->state 的修改必须受 p->lock 保护。如果两个 CPU 同时修改同一个进程的状态,会导致进程被重复调度或丢失调度。
2.3 中断处理:PLIC 与 trap 路径
xv6 运行在 RISC-V 架构上,中断处理分为 U 模式(用户态) 和 S 模式(内核态) 两条路径。所有中断和异常首先进入 stvec 指向的 trampoline.S,然后分发到 usertrap() 或 kerneltrap()。
2.3.1 中断类型与来源
| 中断类型 | scause 编码 | 来源 | 处理函数 |
|---|---|---|---|
| 软件中断 | 1 (S-mode) | 核间中断 (IPI) | kerneltrap() |
| 定时器中断 | 5 (S-mode) | CLINT 定时器 | kerneltrap() → yield() |
| 外部中断 | 9 (S-mode) | PLIC (UART / VirtIO 磁盘) | kerneltrap() → devintr() |
| 环境调用异常 | 8 (U-mode) | ecall 指令(系统调用) | usertrap() → syscall() |
| 缺页异常 | 12 / 13 / 15 | 地址转换失败 | usertrap() → 终止进程 |
2.3.2 PLIC 中断路由
平台级中断控制器 (PLIC) 负责将外部设备中断路由到指定的 CPU 核心。xv6 在启动时配置 PLIC,使每个 CPU 都能接收所有设备中断,但通过 plic_claim() 和 plic_complete() 实现一次只有一个 CPU 处理一个中断源。
2.4 并发基础:多核环境下的竞争条件
xv6 支持多核 (SMP) 后,并发问题成为内核稳定性的核心挑战。以下三个并发场景是 spinlock 调试的焦点:
2.4.1 三种并发场景
| 场景 | 描述 | 典型例子 | 保护机制 |
|---|---|---|---|
| CPU vs CPU | 两个核心同时访问共享数据结构 | 两个 CPU 同时调用 kalloc() | kmem.lock (spinlock) |
| CPU vs 中断 | CPU 在临界区中被中断处理打断 | CPU 修改链表时 UART 中断到达 | push_off() / pop_off() |
| 中断 vs 中断 | 嵌套中断处理共享数据 | 时钟中断中访问进程表 | 中断优先级 + 锁 |
2.4.2 竞争条件示例:kalloc 内存分配器
xv6 的物理内存分配器维护一个空闲页链表 kmem.freelist。在没有锁保护的情况下,两个 CPU 同时调用 kalloc() 会导致:
// 未加锁的 kalloc() 伪代码
void *kalloc(void) {
struct run *r = kmem.freelist; // CPU 0 读取 freelist
// ---- CPU 1 此时也读取 freelist,得到相同地址 ----
if (r)
kmem.freelist = r->next; // CPU 0 更新 freelist
// ---- CPU 1 也更新 freelist,但 r->next 已被 CPU 0 修改 ----
return (void*)r; // 两个 CPU 返回同一块内存!
}
2.5 本章小结:架构与并发的关系
- 宏内核架构 使得所有模块共享地址空间,锁保护成为必需品。
- 进程调度 中的状态切换和上下文保存/恢复必须原子化,否则会导致进程丢失或重复调度。
- 中断处理 的非确定性路由增加了竞争条件复现的难度,调试时需要固定中断亲和性或使用确定性触发。
- 三种并发场景 对应不同的锁使用模式,spinlock 是解决 CPU vs CPU 和 CPU vs 中断场景的基础工具。