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)。前者是队列的管理者,后者是每个睡眠进程在队列中的“代言人”。
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)的队列结构。
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 等待队列的典型生命周期
一个等待队列条目从创建到销毁,经历以下阶段:
- 声明与初始化:使用
DECLARE_WAITQUEUE(name, tsk)宏创建条目,并绑定当前进程。 - 加入队列:调用
add_wait_queue(&wq_head, &wq_entry)将条目挂入等待队列头。 - 设置进程状态:调用
set_current_state(TASK_INTERRUPTIBLE)将进程置为睡眠状态。 - 条件检查与调度:检查资源是否可用,若不可用则调用
schedule()让出 CPU。 - 被唤醒:条件满足时,内核调用
wake_up()系列函数,通过条目中的func回调将进程状态改回TASK_RUNNING。 - 从队列移除:进程被唤醒后,调用
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 函数,看内核如何利用这些结构体实现阻塞与唤醒的完整流程。