3. 等待队列核心数据结构:wait_queue_head_t 与 wait_queue_entry_t 详解
等待队列是 Linux 内核实现阻塞 I/O 与异步事件通知的基石。其核心由两个数据结构构成:队列头(wait_queue_head_t)和队列项(wait_queue_entry_t)。理解这两个结构体的字段布局与语义,是掌握等待队列运作机制的第一步。
3.1 wait_queue_head_t 结构体:队列的“锚点”
每个等待队列都有一个唯一的队列头,它作为整个链表的入口点,管理所有等待在该事件上的进程。定义位于 <linux/wait.h>。
struct wait_queue_head {
spinlock_t lock; // 自旋锁,保护链表操作
struct list_head head; // 双向链表头,链接所有 wait_queue_entry_t
};
typedef struct wait_queue_head wait_queue_head_t;
| 字段 | 类型 | 作用 |
|---|---|---|
lock |
spinlock_t |
自旋锁,用于保护对链表 head 的并发访问。在添加/删除等待项、唤醒队列时,必须持有此锁。 |
head |
struct list_head |
标准内核双向链表的头节点。所有注册到该队列的 wait_queue_entry_t 通过其 entry 字段链接在此链表上。 |
初始化方式: 静态初始化使用
DECLARE_WAIT_QUEUE_HEAD(name);动态初始化使用 init_waitqueue_head(&wq_head)。两者都会将 lock 初始化为未锁定状态,并将 head 初始化为空链表。
3.2 wait_queue_entry_t 结构体:每个等待进程的“代言人”
每一个因等待某条件而进入睡眠的进程,在内核中都对应一个 wait_queue_entry_t 实例。它记录了进程的身份、唤醒行为以及自定义数据。
struct wait_queue_entry {
unsigned int flags; // 控制唤醒行为与队列属性
void *private; // 通常指向 task_struct,也可用于自定义数据
wait_queue_func_t func; // 唤醒回调函数,决定如何唤醒进程
struct list_head entry; // 链表节点,用于挂入 wait_queue_head_t.head
};
typedef struct wait_queue_entry wait_queue_entry_t;
| 字段 | 类型 | 作用 |
|---|---|---|
flags |
unsigned int |
控制唤醒行为与队列属性。常用值:WQ_FLAG_EXCLUSIVE(排他等待)、WQ_FLAG_WOKEN(已被唤醒标记)。 |
private |
void * |
通用指针。默认指向当前进程的 task_struct;在自定义等待逻辑中可指向任意私有数据。 |
func |
wait_queue_func_t |
唤醒回调函数指针。内核默认使用 default_wake_function,负责将进程状态设为 TASK_RUNNING 并加入就绪队列。 |
entry |
struct list_head |
链表节点,通过此字段将当前项挂入队列头的 head 链表。 |
3.3 flags 字段:排他等待与唤醒策略
flags 字段是等待队列实现高效唤醒策略的关键。最核心的标志位是 WQ_FLAG_EXCLUSIVE。
- 非排他等待(默认,flags = 0):当调用
wake_up时,内核会遍历整个队列,唤醒所有非排他等待项。适用于“广播”事件,如共享资源状态变化。 - 排他等待(flags |= WQ_FLAG_EXCLUSIVE):当调用
wake_up时,内核只唤醒第一个排他等待项,然后停止遍历。适用于“互斥”场景,如一次只允许一个消费者获取资源。
性能优化: 在
__wake_up_common 内核函数中,唤醒逻辑会检查当前项是否设置了 WQ_FLAG_EXCLUSIVE。如果遇到排他项并成功唤醒,则立即返回,不再继续遍历链表。这避免了“惊群效应”(thundering herd),即多个等待进程被无效唤醒后只有其中一个能获得资源,其余重新睡眠。
3.4 private 字段:从进程指针到自定义上下文
private 字段是一个 void * 指针,其语义由 func 回调函数解释。
| 使用场景 | private 指向 | 说明 |
|---|---|---|
| 标准阻塞 I/O | current(struct task_struct *) |
默认行为。唤醒回调通过 private 找到进程描述符,调用 try_to_wake_up。 |
| 自定义唤醒逻辑 | 自定义结构体指针 | 驱动可定义自己的 func,从 private 中提取自定义上下文,如 DMA 缓冲区状态、完成量等。 |
| 等待队列复用 | NULL 或特殊标记 | 某些高级用法(如 poll_wait)中,private 可能被用于传递文件指针或轮询表。 |
3.5 数据结构关系总览
下图展示了 wait_queue_head_t 与多个 wait_queue_entry_t 实例之间的链接关系,以及 flags 和 private 在唤醒流程中的角色。
3.6 内核提供的辅助宏与内联函数
为了简化等待队列的创建与操作,内核提供了一系列宏和内联函数,它们封装了对上述数据结构的初始化与访问。
| 宏/函数 | 作用 | 内部操作 |
|---|---|---|
DECLARE_WAIT_QUEUE_HEAD(name) |
静态声明并初始化队列头 | 定义 wait_queue_head_t name,调用 __init_waitqueue_head |
init_waitqueue_head(&wq) |
动态初始化队列头 | 初始化自旋锁,初始化链表头 |
DECLARE_WAITQUEUE(name, tsk) |
静态声明并初始化等待项 | 设置 private = tsk,func = default_wake_function,flags = 0 |
add_wait_queue(&wq, &wait) |
将等待项加入队列(非排他) | 持有 wq.lock,将 wait.entry 添加到 wq.head 尾部 |
add_wait_queue_exclusive(&wq, &wait) |
将等待项加入队列(排他) | 设置 wait.flags |= WQ_FLAG_EXCLUSIVE,然后加入链表 |
remove_wait_queue(&wq, &wait) |
从队列中移除等待项 | 持有 wq.lock,从链表中删除 wait.entry |
注意: 在中断上下文中调用
add_wait_queue 或 remove_wait_queue 时,必须确保队列头的 lock 使用 spin_lock_irqsave 变体,以避免死锁。标准做法是在进程上下文中操作等待队列,中断中只调用 wake_up。
3.7 典型使用模式:从初始化到唤醒
以下代码片段展示了驱动中如何使用上述数据结构实现一个完整的等待-唤醒循环。
// 1. 定义队列头(通常在设备结构体中)
static wait_queue_head_t my_wq;
// 2. 初始化(在 probe 或 init 中)
init_waitqueue_head(&my_wq);
// 3. 定义等待项(通常在 read/write 系统调用中)
DECLARE_WAITQUEUE(wait, current);
// 4. 加入队列并睡眠
add_wait_queue(&my_wq, &wait);
set_current_state(TASK_INTERRUPTIBLE); // 或 TASK_UNINTERRUPTIBLE
if (!condition_met) {
schedule(); // 让出 CPU
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&my_wq, &wait);
// 5. 在中断或另一线程中唤醒
condition_met = true;
wake_up(&my_wq); // 唤醒所有非排他等待项
// 或 wake_up_interruptible(&my_wq);