2. Linux 内核调度基础:进程状态、上下文切换、调度器与睡眠机制简介

在编写驱动时,阻塞 I/O 操作(如等待数据到达)是常见需求。要实现正确的阻塞与唤醒,必须理解内核如何管理进程、如何切换执行上下文以及调度器如何决定哪个进程获得 CPU。本章将梳理这些基础概念,为后续实现等待队列和阻塞 I/O 打下坚实的理论基础。

2.1 进程状态与状态转换

Linux 内核使用 task_struct 描述每个进程(或线程),其中 state 字段记录当前状态。驱动开发者最关心的状态是 TASK_RUNNING、TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE。

状态宏 数值 含义 驱动场景
TASK_RUNNING 0 进程正在运行或处于就绪队列等待调度 驱动正常执行路径
TASK_INTERRUPTIBLE 1 进程因等待某事件而睡眠,可被信号唤醒 驱动阻塞读,允许用户按 Ctrl+C 终止
TASK_UNINTERRUPTIBLE 2 进程睡眠等待,不响应任何信号 驱动等待硬件 DMA 完成,不可中断
__TASK_STOPPED 4 进程被暂停(如收到 SIGSTOP) 调试场景
__TASK_TRACED 8 进程被 ptrace 跟踪 调试器使用
驱动要点: 驱动中调用 wait_event_interruptible() 会将当前进程置为 TASK_INTERRUPTIBLE 并加入等待队列;调用 wait_event() 则置为 TASK_UNINTERRUPTIBLE。选择哪种取决于是否允许用户通过信号打断等待。
进程核心状态转换图 TASK_RUNNING TASK_INTERRUPTIBLE TASK_UNINTERRUPTIBLE TASK_STOPPED / TRACED 等待事件(可中断) 等待事件(不可中断) 条件满足 / 收到信号 条件满足 SIGSTOP / ptrace SIGCONT

2.2 上下文切换:内核的“换人”机制

上下文切换(Context Switch)是指内核暂停当前进程,保存其 CPU 寄存器、程序计数器、栈指针等状态,然后恢复另一个进程的状态并让其继续执行。这是多任务操作系统的核心操作。

2.2.1 切换开销

  • 直接开销: 保存/恢复寄存器、切换页表(TLB 刷新)、更新调度数据结构。
  • 间接开销: 缓存和 TLB 失效导致后续访问变慢。

2.2.2 驱动开发者的关注点

  • 在中断上下文或持有自旋锁时不能睡眠,因为无法进行上下文切换。
  • 等待队列机制本质上是让当前进程主动触发上下文切换,让出 CPU。
  • 频繁的上下文切换会降低系统吞吐量,驱动应尽量合并唤醒操作。
上下文切换流程 进程 A(运行中) 内核调度器 进程 B(就绪) ① 触发调度(时间片耗尽/主动睡眠) ② 保存 A 的上下文 ③ 恢复 B 的上下文 关键:上下文切换由内核调度器发起,驱动通过睡眠/唤醒间接触发 切换期间 CPU 处于特权模式,用户态无法干预

2.3 调度器:谁下一个运行?

Linux 内核使用 完全公平调度器(CFS,Completely Fair Scheduler) 作为默认调度策略。CFS 的目标是让每个进程获得公平的 CPU 时间比例。

2.3.1 CFS 核心概念

概念 说明 驱动影响
虚拟运行时间(vruntime) 进程实际运行时间经优先级加权后的值 优先级高的进程 vruntime 增长慢,获得更多 CPU
红黑树 按 vruntime 排序的就绪队列数据结构 调度器总是选择最左侧(最小 vruntime)的进程
调度实体(sched_entity) 嵌入在 task_struct 中的调度单元 驱动无需直接操作,但理解其存在有助于调试优先级问题
时间片 CFS 没有固定时间片,由目标延迟和进程数量动态计算 睡眠进程唤醒后可能立即获得 CPU,因为其 vruntime 较小
驱动提示: 当驱动在中断处理中调用 wake_up() 时,被唤醒的进程会被放入 CFS 就绪队列。如果该进程优先级足够高,调度器可能在下一次 schedule() 调用时立即选择它运行。这就是“中断下半部唤醒进程”的调度基础。
CFS 调度器核心结构 红黑树根 vruntime 最小 vruntime 较大 调度决策:每次选择红黑树最左节点(vruntime 最小)运行 进程睡眠时 vruntime 保持不变,唤醒后因 vruntime 较小而获得优先运行权

2.4 睡眠机制:进程如何主动让出 CPU

当驱动需要等待某个条件(如硬件数据就绪)时,当前进程应进入睡眠状态,而不是忙等待。睡眠的本质是将进程状态从 TASK_RUNNING 切换为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,然后调用调度器选择其他进程运行。

2.4.1 睡眠的典型步骤

  1. 设置进程状态: 将当前进程标记为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE。
  2. 加入等待队列: 将进程添加到驱动维护的等待队列头(wait_queue_head_t)中。
  3. 检查条件: 再次检查等待条件是否满足(防止条件在设置状态和加入队列之间发生变化)。
  4. 调用 schedule(): 主动触发上下文切换,让出 CPU。
  5. 被唤醒后恢复: 当条件满足时,其他上下文(如中断处理函数)调用 wake_up(),将进程状态改回 TASK_RUNNING 并加入就绪队列。
常见错误: 在持有自旋锁或处于中断上下文时调用 schedule() 会导致内核崩溃。睡眠只能在进程上下文(且不持有自旋锁)时进行。驱动中应使用 wait_event 系列宏,它们自动处理了上述步骤的原子性。
驱动中进程睡眠与唤醒流程 进程(运行中) 驱动代码 等待队列 ① 调用 wait_event() ② 设置 TASK_INTERRUPTIBLE ③ 加入等待队列 调用 schedule() ④ 让出 CPU wake_up() 被调用 ⑤ 条件满足,中断/其他进程唤醒 ⑥ 进程恢复运行,检查条件

2.5 本章小结

  • 进程状态是驱动阻塞 I/O 的基础:TASK_INTERRUPTIBLE 允许信号打断,TASK_UNINTERRUPTIBLE 则不允许。
  • 上下文切换是进程让出 CPU 的底层机制,驱动通过睡眠触发切换。
  • CFS 调度器根据 vruntime 选择下一个运行的进程,睡眠进程唤醒后因 vruntime 较小而具有调度优势。
  • 睡眠机制的核心步骤:设置状态 → 加入等待队列 → 检查条件 → 调用 schedule() → 被唤醒后恢复。