2. Linux进程调度基础:进程状态、上下文切换与等待队列
在深入驱动等待队列之前,必须理解Linux内核如何管理进程。进程调度是内核的核心职责,它决定了哪个进程获得CPU使用权、何时被暂停、以及如何等待资源。等待队列正是调度机制在驱动层的直接体现——当进程无法立即获得资源时,调度器将其置入特定状态并移出运行队列,直到条件满足再重新唤醒。
2.1 进程状态机:三种核心状态
Linux进程在生命周期中会在多个状态间切换。对于驱动开发者,最关键的三个状态是 TASK_RUNNING、TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE。它们直接决定了进程如何响应信号和资源就绪事件。
| 状态 | 内核宏定义 | 含义 | 是否在运行队列 | 是否响应信号 | 典型场景 |
|---|---|---|---|---|---|
| 运行 | TASK_RUNNING |
进程正在CPU上执行或已就绪等待调度 | 是 | — | 普通用户态进程、内核线程执行中 |
| 可中断睡眠 | TASK_INTERRUPTIBLE |
进程等待某事件(如I/O完成),可被信号唤醒 | 否 | 是 | read() 等待数据、wait_event_interruptible() |
| 不可中断睡眠 | TASK_UNINTERRUPTIBLE |
进程等待内核态资源(如磁盘I/O),不响应信号 | 否 | 否 | 同步块设备读写、wait_event() |
驱动开发建议: 驱动中默认使用
TASK_INTERRUPTIBLE,允许用户通过 Ctrl+C 终止阻塞的进程。仅在必须保证操作原子性(如等待硬件复位完成)时使用 TASK_UNINTERRUPTIBLE,否则可能导致“不可杀死的”僵尸进程。
2.2 进程上下文切换:内核的“换人”机制
上下文切换(Context Switch)是指内核暂停当前进程,保存其执行上下文(寄存器、程序计数器、栈指针、内存映射等),并恢复另一个进程的上下文,使其继续执行。这是多任务操作系统实现“同时运行”假象的基础。
上下文切换的触发时机包括:
- 时间片耗尽: 调度器选择下一个就绪进程。
- 进程主动睡眠: 进程调用
schedule()进入等待队列。 - 中断/异常返回: 内核在中断处理完成后检查是否需要重新调度。
- 系统调用阻塞: 如
read()等待数据时触发切换。
关键指标: 每次上下文切换约消耗数微秒(取决于CPU架构和缓存刷新)。频繁切换会显著降低系统吞吐量,驱动设计应尽量减少不必要的唤醒和切换。
2.3 调度器与等待队列的关系
等待队列(Wait Queue)是内核实现阻塞I/O的核心数据结构。它本质上是一个双链表,挂载了所有等待某事件发生的进程。调度器与等待队列的协作遵循“出队睡眠 → 事件触发 → 入队唤醒”的闭环:
- 进程主动睡眠: 驱动中的
wait_event_interruptible()将当前进程状态设为TASK_INTERRUPTIBLE,将其加入等待队列链表,然后调用schedule()触发上下文切换。 - 调度器移除进程:
schedule()从运行队列中移除该进程,选择下一个就绪进程执行。此时进程处于“睡眠”状态,不消耗CPU。 - 事件发生(如中断): 硬件中断处理函数或内核其他路径调用
wake_up()系列函数,遍历等待队列,将队列中所有(或指定)进程状态改回TASK_RUNNING,并将其重新加入运行队列。 - 调度器重新选择: 下次调度时,被唤醒的进程获得CPU,从
schedule()调用处继续执行,检查等待条件是否满足。
注意竞态条件: 进程在检查条件(如设备寄存器状态)和调用
schedule() 之间,如果条件已经满足但尚未睡眠,就会导致“丢失唤醒”。等待队列内部使用自旋锁保护链表操作,驱动开发者必须使用标准宏(如 wait_event_interruptible),它们已经内置了条件检查和锁保护。
2.4 等待队列在内核中的数据结构
等待队列的核心结构体定义在 <linux/wait.h> 中。理解其内部布局有助于调试和性能优化:
// 等待队列头(每个等待队列一个)
struct wait_queue_head {
spinlock_t lock; // 保护链表操作的自旋锁
struct list_head head; // 双向链表头
};
// 等待队列项(每个睡眠进程一个)
struct wait_queue_entry {
unsigned int flags; // 标志位(如 WQ_FLAG_EXCLUSIVE)
void *private; // 指向 task_struct 的指针
wait_queue_func_t func; // 唤醒回调函数(默认 default_wake_function)
struct list_head entry; // 链表节点
};
独占等待(WQ_FLAG_EXCLUSIVE): 当多个进程等待同一事件时,默认
wake_up() 会唤醒所有进程。如果使用 wake_up_nr() 或设置了独占标志,则只唤醒指定数量的进程,避免“惊群效应”(Thundering Herd)。驱动中常用 wake_up_interruptible() 唤醒所有可中断睡眠的进程。
2.5 核心API速查表
驱动开发中与调度和等待队列交互的常用函数:
| 函数/宏 | 作用 | 对应进程状态 |
|---|---|---|
wait_event_interruptible(wq, condition) |
检查条件,若为假则睡眠(可被信号打断) | TASK_INTERRUPTIBLE |
wait_event(wq, condition) |
检查条件,若为假则睡眠(不可中断) | TASK_UNINTERRUPTIBLE |
wake_up_interruptible(wq) |
唤醒等待队列中所有 TASK_INTERRUPTIBLE 进程 |
→ TASK_RUNNING |
wake_up(wq) |
唤醒等待队列中所有进程(包括不可中断) | → TASK_RUNNING |
schedule() |
主动触发调度,让出CPU | 取决于当前进程状态 |
set_current_state(state) |
手动设置当前进程状态(底层操作,慎用) | 指定状态 |
最佳实践: 驱动中优先使用
wait_event_interruptible() 和 wake_up_interruptible() 配对。这保证了用户态程序可以通过信号终止阻塞的系统调用,同时避免意外唤醒不可中断的进程导致内核态资源泄漏。