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 进程状态之间的合法转换路径。箭头上的标签表示触发转换的内核函数或事件。
关键洞察:
SLEEPING 和 RUNNABLE 之间的转换是 sleep/wakeup 机制的直接体现。进程调用 sleep() 进入等待,而 wakeup() 将其状态置回 RUNNABLE,等待调度器再次运行。
2.2 调度器:CPU 时间的分配者
调度器(scheduler)是内核中一个特殊的执行流,它负责在多个 RUNNABLE 进程之间切换 CPU 的使用权。xv6 采用一种简单而经典的轮转调度(Round-Robin)策略。
2.2.1 调度器核心逻辑
调度器的主体是一个无限循环,运行在 CPU 的内核态。其核心流程如下:
- 遍历进程表: 从进程数组
ptable中查找下一个状态为RUNNABLE的进程。 - 进程切换: 如果找到,则调用
swtch()函数,将 CPU 的执行上下文从当前调度器线程切换到目标进程的内核线程。 - 上下文保存与恢复:
swtch()会保存当前 CPU 寄存器(如 ra、sp、s0-s11)到当前上下文的context结构,然后加载目标进程的context结构到 CPU 寄存器。 - 返回用户空间: 进程的内核线程执行
trapret代码,最终通过sret指令返回到用户空间继续执行。
2.2.2 调度器与进程的关系
注意: 调度器本身不是一个独立的进程,它运行在 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 上执行。 - 锁 保证了在检查和修改进程状态(以及共享条件变量)时的原子性,防止竞态条件。