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。选择哪种取决于是否允许用户通过信号打断等待。
2.2 上下文切换:内核的“换人”机制
上下文切换(Context Switch)是指内核暂停当前进程,保存其 CPU 寄存器、程序计数器、栈指针等状态,然后恢复另一个进程的状态并让其继续执行。这是多任务操作系统的核心操作。
2.2.1 切换开销
- 直接开销: 保存/恢复寄存器、切换页表(TLB 刷新)、更新调度数据结构。
- 间接开销: 缓存和 TLB 失效导致后续访问变慢。
2.2.2 驱动开发者的关注点
- 在中断上下文或持有自旋锁时不能睡眠,因为无法进行上下文切换。
- 等待队列机制本质上是让当前进程主动触发上下文切换,让出 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() 调用时立即选择它运行。这就是“中断下半部唤醒进程”的调度基础。
2.4 睡眠机制:进程如何主动让出 CPU
当驱动需要等待某个条件(如硬件数据就绪)时,当前进程应进入睡眠状态,而不是忙等待。睡眠的本质是将进程状态从 TASK_RUNNING 切换为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE,然后调用调度器选择其他进程运行。
2.4.1 睡眠的典型步骤
- 设置进程状态: 将当前进程标记为
TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE。 - 加入等待队列: 将进程添加到驱动维护的等待队列头(
wait_queue_head_t)中。 - 检查条件: 再次检查等待条件是否满足(防止条件在设置状态和加入队列之间发生变化)。
- 调用 schedule(): 主动触发上下文切换,让出 CPU。
- 被唤醒后恢复: 当条件满足时,其他上下文(如中断处理函数)调用
wake_up(),将进程状态改回TASK_RUNNING并加入就绪队列。
常见错误: 在持有自旋锁或处于中断上下文时调用
schedule() 会导致内核崩溃。睡眠只能在进程上下文(且不持有自旋锁)时进行。驱动中应使用 wait_event 系列宏,它们自动处理了上述步骤的原子性。
2.5 本章小结
- 进程状态是驱动阻塞 I/O 的基础:
TASK_INTERRUPTIBLE允许信号打断,TASK_UNINTERRUPTIBLE则不允许。 - 上下文切换是进程让出 CPU 的底层机制,驱动通过睡眠触发切换。
- CFS 调度器根据 vruntime 选择下一个运行的进程,睡眠进程唤醒后因 vruntime 较小而具有调度优势。
- 睡眠机制的核心步骤:设置状态 → 加入等待队列 → 检查条件 → 调用
schedule()→ 被唤醒后恢复。