3. 等待队列数据结构:wait_queue_head_t 与 wait_queue_entry_t 详解

等待队列(Wait Queue)是 Linux 内核实现阻塞 I/O 的核心机制。它允许进程在条件不满足时主动让出 CPU,进入睡眠状态,并在条件满足时被内核唤醒。理解等待队列的数据结构是掌握驱动阻塞 I/O 编程的基础。

等待队列由两个核心数据结构组成:等待队列头(wait_queue_head_t)和等待队列条目(wait_queue_entry_t)。前者是队列的管理者,后者是每个睡眠进程在队列中的“代言人”。

等待队列数据结构整体关系图 wait_queue_head_t spinlock_t lock struct list_head head wait_queue_entry_t (进程A) unsigned int flags void *private (task_struct) wait_queue_func_t func 进程B ... 说明: 1. wait_queue_head_t 是队列的管理者,包含一个自旋锁和一个双向链表头。 2. wait_queue_entry_t 是队列中的节点,每个睡眠进程对应一个条目。 3. 多个条目通过 list_head 链接成双向链表,等待队列头指向第一个条目。

3.1 wait_queue_head_t:等待队列头

wait_queue_head_t 是等待队列的“大脑”,定义在 <linux/wait.h> 中。它负责管理整个队列的同步与遍历。

// 内核源码:include/linux/wait.h
typedef struct wait_queue_head {
    spinlock_t      lock;       // 自旋锁,保护队列操作
    struct list_head head;      // 双向链表头,链接所有 wait_queue_entry_t
} wait_queue_head_t;
成员 类型 作用
lock spinlock_t 自旋锁,用于保护对等待队列的并发访问(添加/删除条目、唤醒操作)。
head struct list_head 双向链表的头节点,通过 prev 和 next 指针链接所有等待队列条目。
初始化方式: 驱动中通常使用 DECLARE_WAIT_QUEUE_HEAD(wq_name) 宏静态声明,或在运行时调用 init_waitqueue_head(&wq) 动态初始化。

3.2 wait_queue_entry_t:等待队列条目

每个因等待某个条件而睡眠的进程,都会在等待队列中对应一个 wait_queue_entry_t 结构体。它是进程与队列之间的“绑定凭证”。

// 内核源码:include/linux/wait.h
typedef struct wait_queue_entry {
    unsigned int        flags;      // 标志位,如 WQ_FLAG_EXCLUSIVE
    void                *private;   // 通常指向当前进程的 task_struct
    wait_queue_func_t   func;       // 唤醒回调函数
    struct list_head    entry;      // 链表节点,用于挂入 wait_queue_head_t.head
} wait_queue_entry_t;
成员 类型 作用
flags unsigned int 控制条目行为,常用 WQ_FLAG_EXCLUSIVE(独占唤醒,避免惊群效应)。
private void * 通常指向当前进程的 struct task_struct,内核通过它找到要唤醒的进程。
func wait_queue_func_t 唤醒时调用的回调函数,默认是 default_wake_function。
entry struct list_head 链表节点,通过它将条目挂入等待队列头的 head 链表。
关键理解: private 成员是连接内核调度器与等待队列的桥梁。当条件满足时,内核通过 func 回调,并利用 private 找到目标进程,将其状态置为 TASK_RUNNING 并移入就绪队列。

3.3 等待队列的链表组织方式

等待队列采用内核标准的 双向循环链表 组织。一个 wait_queue_head_t 可以挂载多个 wait_queue_entry_t,形成一个先进先出(FIFO)的队列结构。

等待队列双向链表组织细节 wait_queue_head_t head (链表头) next 条目1 (进程A) entry.next/prev next 条目2 (进程B) entry.next/prev next head.prev 指向最后一个条目(循环链表)

3.4 独占等待标志:WQ_FLAG_EXCLUSIVE

当多个进程同时等待同一个资源时,如果使用默认的非独占唤醒,一次 wake_up 会唤醒队列中所有进程。这在高并发场景下会导致“惊群效应”(Thundering Herd),大量进程被唤醒后竞争资源,只有少数能成功,其余重新睡眠,造成性能浪费。

WQ_FLAG_EXCLUSIVE 标志用于解决此问题。设置了该标志的条目在唤醒时,内核只会唤醒队列中第一个设置了独占标志的进程。

唤醒方式 行为 适用场景
wake_up / wake_up_all 唤醒所有非独占条目 + 第一个独占条目 事件广播(如数据到达多个读取者)
wake_up_interruptible 同上,但只唤醒可中断睡眠的进程 可被信号打断的等待
wake_up_nr 唤醒指定数量的独占条目 有限资源分配(如内存池)
避坑指南: 如果所有等待条目都设置了 WQ_FLAG_EXCLUSIVE,则 wake_up 只会唤醒第一个条目。驱动设计时,通常将“生产者-消费者”模式中的消费者设为独占等待,避免惊群。

3.5 等待队列的典型生命周期

一个等待队列条目从创建到销毁,经历以下阶段:

  1. 声明与初始化:使用 DECLARE_WAITQUEUE(name, tsk) 宏创建条目,并绑定当前进程。
  2. 加入队列:调用 add_wait_queue(&wq_head, &wq_entry) 将条目挂入等待队列头。
  3. 设置进程状态:调用 set_current_state(TASK_INTERRUPTIBLE) 将进程置为睡眠状态。
  4. 条件检查与调度:检查资源是否可用,若不可用则调用 schedule() 让出 CPU。
  5. 被唤醒:条件满足时,内核调用 wake_up() 系列函数,通过条目中的 func 回调将进程状态改回 TASK_RUNNING。
  6. 从队列移除:进程被唤醒后,调用 remove_wait_queue(&wq_head, &wq_entry) 将条目从队列中摘除。
// 等待队列典型使用模式(伪代码)
static wait_queue_head_t my_wq;
DECLARE_WAITQUEUE(wait, current);   // 步骤1:创建条目,绑定当前进程

add_wait_queue(&my_wq, &wait);      // 步骤2:加入队列
set_current_state(TASK_INTERRUPTIBLE); // 步骤3:设置睡眠状态

while (!condition_met) {            // 步骤4:条件检查
    schedule();                     // 让出CPU,进入睡眠
    if (signal_pending(current))    // 处理信号
        break;
}

set_current_state(TASK_RUNNING);    // 恢复运行状态
remove_wait_queue(&my_wq, &wait);  // 步骤6:从队列移除
内核封装: 实际驱动开发中,上述手动流程通常被 wait_event_interruptible(wq_head, condition) 等宏封装。理解底层数据结构有助于在需要自定义唤醒行为时进行精细控制。

3.6 数据结构对比总结

特性 wait_queue_head_t wait_queue_entry_t
角色 队列管理者 队列节点(每个睡眠进程一个)
核心成员 lock + head flags + private + func + entry
初始化宏 DECLARE_WAIT_QUEUE_HEAD(name) DECLARE_WAITQUEUE(name, tsk)
链表方向 双向循环链表头 链表节点,可双向遍历
并发保护 自带自旋锁 由队列头的锁保护
生命周期 驱动加载到卸载 进程睡眠到唤醒

掌握这两个数据结构,就掌握了等待队列的“骨架”。下一层我们将深入等待队列的 API 函数,看内核如何利用这些结构体实现阻塞与唤醒的完整流程。