2. xv6内核架构概览

在深入 sleep/wakeup 同步机制之前,必须先理解 xv6 内核的宏观骨架。本章将聚焦三个核心支柱:进程状态机(进程的生命周期)、调度器(CPU 如何切换进程)以及锁(并发安全的基石)。这三者共同构成了 sleep/wakeup 机制运行的底层舞台。

2.1 进程状态机:从创建到消亡

xv6 的进程(struct proc)并非一直处于运行状态。它在生命周期中会经历多个状态,这些状态构成了一个有限状态机。理解这个状态机是理解进程调度和同步的前提。

核心概念: 进程状态是内核判断进程当前“能做什么”的唯一依据。例如,只有处于 RUNNABLE 状态的进程才会被调度器选中执行。

2.1.1 进程的六大状态

状态 枚举值 含义 典型触发事件
UNUSED 0 进程控制块(PCB)未被使用,是空闲槽位。 系统初始化、进程退出后被回收。
USED 1 PCB 已被分配,但尚未完成初始化或处于中间状态。 allocproc() 分配进程结构后。
SLEEPING 2 进程因等待某个事件(如 I/O 完成、锁释放)而主动让出 CPU。 调用 sleep() 函数。
RUNNABLE 3 进程已具备运行条件,正在就绪队列中等待调度器分配 CPU。 进程被创建、从 sleep() 中被唤醒、时间片用完。
RUNNING 4 进程正在 CPU 上执行指令。 调度器从就绪队列中选中该进程。
ZOMBIE 5 进程已退出,但其父进程尚未通过 wait() 系统调用回收其退出状态。 进程执行 exit() 系统调用。

2.1.2 状态转换图

下图展示了 xv6 进程状态之间的合法转换路径。箭头上的标签表示触发转换的内核函数或事件。

xv6 进程状态转换图 xv6 进程状态转换图 UNUSED USED RUNNABLE RUNNING SLEEPING ZOMBIE allocproc() fork()/userinit() scheduler() 选中 yield() (时钟中断) sleep() wakeup() exit() wait() 回收 图例说明 实线:主动/正常转换 虚线:被动/调度转换 核心:SLEEPING ↔ RUNNABLE
关键洞察: SLEEPING 和 RUNNABLE 之间的转换是 sleep/wakeup 机制的直接体现。进程调用 sleep() 进入等待,而 wakeup() 将其状态置回 RUNNABLE,等待调度器再次运行。

2.2 调度器:CPU 时间的分配者

调度器(scheduler)是内核中一个特殊的执行流,它负责在多个 RUNNABLE 进程之间切换 CPU 的使用权。xv6 采用一种简单而经典的轮转调度(Round-Robin)策略。

2.2.1 调度器核心逻辑

调度器的主体是一个无限循环,运行在 CPU 的内核态。其核心流程如下:

  1. 遍历进程表: 从进程数组 ptable 中查找下一个状态为 RUNNABLE 的进程。
  2. 进程切换: 如果找到,则调用 swtch() 函数,将 CPU 的执行上下文从当前调度器线程切换到目标进程的内核线程。
  3. 上下文保存与恢复: swtch() 会保存当前 CPU 寄存器(如 ra、sp、s0-s11)到当前上下文的 context 结构,然后加载目标进程的 context 结构到 CPU 寄存器。
  4. 返回用户空间: 进程的内核线程执行 trapret 代码,最终通过 sret 指令返回到用户空间继续执行。

2.2.2 调度器与进程的关系

调度器与进程切换关系图 调度器与进程切换关系图 调度器 (scheduler) 进程 A (RUNNING → RUNNABLE) 进程 B (RUNNABLE → RUNNING) swtch() 到 A yield() 返回调度器 swtch() 到 B 核心机制 调度器通过 swtch() 在自身与进程内核线程之间切换,实现多进程并发。
注意: 调度器本身不是一个独立的进程,它运行在 CPU 的内核态,拥有独立的栈(每个 CPU 一个)。当所有进程都处于 SLEEPING 或 ZOMBIE 状态时,调度器会进入一个空循环(idle),直到有进程变为 RUNNABLE。

2.3 锁:并发安全的守护者

xv6 是一个多处理器(SMP)内核,多个 CPU 可能同时访问共享数据结构(如进程表、文件系统缓存)。如果没有保护,就会发生竞态条件(race condition),导致数据损坏。锁(lock)是解决这个问题的基本工具。

2.3.1 xv6 中的锁类型

锁类型 数据结构 特点 典型用途
自旋锁 struct spinlock 获取不到锁时,CPU 会原地循环等待(忙等待)。 保护短临界区,如进程表、文件描述符表。
睡眠锁 struct sleeplock 获取不到锁时,进程会调用 sleep() 让出 CPU,不浪费 CPU 时间。 保护需要长时间持有的资源,如文件系统 inode。

2.3.2 自旋锁的使用模式

自旋锁是 xv6 中最基础的锁。其使用模式非常固定:

// 1. 定义并初始化锁
struct spinlock ptable_lock;
initlock(&ptable_lock, "ptable");

// 2. 在访问共享数据前获取锁
acquire(&ptable_lock);
// ... 临界区:安全地访问进程表 ...
// 3. 访问完成后释放锁
release(&ptable_lock);

2.3.3 锁与进程状态机的关系

锁的获取和释放与进程状态转换紧密相关:

  • acquire() 与自旋: 当尝试获取一个已被其他 CPU 持有的自旋锁时,当前 CPU 会进入一个循环,不断检查锁的状态。此时,当前 CPU 上的进程仍然处于 RUNNING 状态(因为 CPU 还在执行指令),但实际上它在“忙等”,没有做有用功。
  • release() 与唤醒: 当持有锁的 CPU 释放锁时,其他正在自旋等待的 CPU 会立即检测到锁可用,从而成功获取锁。
  • sleep() 与锁的释放: 这是 sleep/wakeup 机制的关键。进程在调用 sleep() 之前,必须持有与等待条件相关的锁。而 sleep() 内部会原子性地执行两个操作:将进程状态置为 SLEEPING,并释放该锁。这防止了在进程进入睡眠和释放锁之间,其他 CPU 错过唤醒信号。
工程实践要点: 在 xv6 中,sleep() 函数要求调用者必须持有锁,并且该锁会被 sleep() 内部释放。这个设计是避免“唤醒丢失”问题的关键。我们将在后续章节深入分析。

2.4 本章小结:三大支柱的协同

进程状态机、调度器和锁是 xv6 内核的三大基石,它们相互依赖,共同支撑起 sleep/wakeup 同步机制:

  • 进程状态机 定义了 SLEEPING 和 RUNNABLE 这两个与同步直接相关的状态。
  • 调度器 负责在进程状态变为 RUNNABLE 后,将其调度到 CPU 上执行。
  • 锁 保证了在检查和修改进程状态(以及共享条件变量)时的原子性,防止竞态条件。