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架构和缓存刷新)。频繁切换会显著降低系统吞吐量,驱动设计应尽量减少不必要的唤醒和切换。
进程状态转换与上下文切换流程 TASK_RUNNING TASK_INTERRUPTIBLE TASK_UNINTERRUPTIBLE 调度器 / 运行队列 等待资源(可被信号唤醒) 等待内核资源(不可中断) 资源就绪 / 信号唤醒 资源就绪唤醒 上下文切换:保存/恢复寄存器、栈、MMU

2.3 调度器与等待队列的关系

等待队列(Wait Queue)是内核实现阻塞I/O的核心数据结构。它本质上是一个双链表,挂载了所有等待某事件发生的进程。调度器与等待队列的协作遵循“出队睡眠 → 事件触发 → 入队唤醒”的闭环:

  1. 进程主动睡眠: 驱动中的 wait_event_interruptible() 将当前进程状态设为 TASK_INTERRUPTIBLE,将其加入等待队列链表,然后调用 schedule() 触发上下文切换。
  2. 调度器移除进程: schedule() 从运行队列中移除该进程,选择下一个就绪进程执行。此时进程处于“睡眠”状态,不消耗CPU。
  3. 事件发生(如中断): 硬件中断处理函数或内核其他路径调用 wake_up() 系列函数,遍历等待队列,将队列中所有(或指定)进程状态改回 TASK_RUNNING,并将其重新加入运行队列。
  4. 调度器重新选择: 下次调度时,被唤醒的进程获得CPU,从 schedule() 调用处继续执行,检查等待条件是否满足。
注意竞态条件: 进程在检查条件(如设备寄存器状态)和调用 schedule() 之间,如果条件已经满足但尚未睡眠,就会导致“丢失唤醒”。等待队列内部使用自旋锁保护链表操作,驱动开发者必须使用标准宏(如 wait_event_interruptible),它们已经内置了条件检查和锁保护。
调度器与等待队列协作架构 运行队列 进程A (RUNNING) 进程B (RUNNING) 等待队列 进程C (INTERRUPTIBLE) 进程D (UNINTERRUPTIBLE) 调度器 schedule() 上下文切换 中断处理函数 进程睡眠 选择下一个进程 wake_up() 重新加入运行队列

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() 配对。这保证了用户态程序可以通过信号终止阻塞的系统调用,同时避免意外唤醒不可中断的进程导致内核态资源泄漏。