4. 等待队列的初始化与销毁:init_waitqueue_head、DECLARE_WAIT_QUEUE_HEAD、destroy 相关宏
等待队列在使用前必须正确初始化,在模块卸载或设备移除时必须妥善销毁。Linux 内核提供了静态声明与动态初始化两套接口,以及对应的清理机制。本节将系统梳理这些 API 的用法、适用场景与底层实现。
4.1 等待队列头的数据结构
等待队列的核心是 wait_queue_head_t 结构体,定义在 <linux/wait.h> 中:
struct wait_queue_head {
spinlock_t lock; // 自旋锁,保护队列操作
struct list_head head; // 双向链表头,链接等待队列项
};
typedef struct wait_queue_head wait_queue_head_t;
每个等待队列头包含一个自旋锁和一个链表头。自旋锁用于保护链表操作(添加/删除等待项),链表头链接所有正在等待的进程描述符(wait_queue_entry_t)。
关键点: 等待队列头本身不存储进程信息,它只是一个“挂载点”。所有等待进程通过
wait_queue_entry_t 结构体挂入链表。
4.2 静态声明:DECLARE_WAIT_QUEUE_HEAD
如果等待队列头是全局变量或静态变量,可以在编译时直接声明并初始化:
#include <linux/wait.h>
// 静态声明并初始化一个等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wq);
宏展开后等价于:
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
其中 __WAIT_QUEUE_HEAD_INITIALIZER 完成结构体字段的编译时赋值:
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.head = { &(name).head, &(name).head } \
}
| 特性 | 说明 |
|---|---|
| 使用场景 | 全局变量、模块级静态变量、文件静态变量 |
| 初始化时机 | 编译时(链接前完成) |
| 锁状态 | 自旋锁初始化为未锁定状态 |
| 链表状态 | 空链表(头尾指针指向自身) |
| 优点 | 无需额外调用初始化函数,避免运行时遗漏 |
4.3 动态初始化:init_waitqueue_head
对于动态分配(如 kmalloc、kzalloc)或嵌入到其他结构体中的等待队列头,必须在运行时调用 init_waitqueue_head:
#include <linux/wait.h>
// 方式一:单独动态分配
wait_queue_head_t *wq;
wq = kmalloc(sizeof(*wq), GFP_KERNEL);
if (wq) {
init_waitqueue_head(wq);
}
// 方式二:嵌入到设备结构体中
struct my_device {
int data;
wait_queue_head_t read_wq; // 嵌入等待队列头
// ... 其他字段
};
struct my_device *dev;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (dev) {
init_waitqueue_head(&dev->read_wq);
}
init_waitqueue_head 的实现非常简洁:
static inline void init_waitqueue_head(wait_queue_head_t *wq_head)
{
spin_lock_init(&wq_head->lock);
INIT_LIST_HEAD(&wq_head->head);
}
最佳实践: 在设备驱动中,推荐将
wait_queue_head_t 嵌入到设备结构体中,并在设备探测(probe)函数中调用 init_waitqueue_head。这样生命周期与设备绑定,避免全局变量带来的命名冲突。
4.4 两种初始化方式的对比
| 对比维度 | DECLARE_WAIT_QUEUE_HEAD |
init_waitqueue_head |
|---|---|---|
| 声明位置 | 文件作用域(全局/静态) | 函数内部(堆/栈/嵌入) |
| 分配方式 | 编译时静态分配 | 运行时动态分配 |
| 初始化时机 | 编译阶段 | 运行时调用 |
| 适用场景 | 模块级全局等待队列 | 设备结构体成员、动态创建 |
| 错误风险 | 低(编译器保证初始化) | 中(需确保调用,否则内核崩溃) |
| 可移植性 | 所有内核版本一致 | 所有内核版本一致 |
4.5 等待队列的销毁
等待队列头本身不包含动态分配的内存(链表节点由等待进程动态管理),因此“销毁”操作主要是清理链表中的残留项,并释放关联的内存。
4.5.1 核心销毁宏:destroy_waitqueue_head
内核提供了 destroy_waitqueue_head 宏(定义在 <linux/wait.h>):
#define destroy_waitqueue_head(wq_head) \
do { \
WARN_ON(!list_empty(&(wq_head)->head)); \
} while (0)
该宏的核心行为:
- 检查链表是否为空:如果链表非空,说明仍有进程在等待,触发内核警告(
WARN_ON)。 - 不释放内存:宏只做完整性检查,不释放
wq_head本身占用的内存。
警告: 如果在链表非空时销毁等待队列头,
WARN_ON 会打印内核回溯(backtrace),但不会阻止后续操作。此时链表中的等待项成为悬空指针,后续唤醒操作将访问已释放的内存,导致内核崩溃(use-after-free)。必须在销毁前确保所有等待进程已被唤醒并移除。
4.5.2 完整销毁流程
正确的销毁步骤:
- 唤醒所有等待进程:调用
wake_up_all(&wq_head)确保链表清空。 - 调用
destroy_waitqueue_head:触发链表空检查。 - 释放内存:如果
wq_head是动态分配的,调用kfree释放。
// 设备移除(remove)函数中的典型销毁代码
static void my_device_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
// 1. 唤醒所有等待进程
wake_up_all(&dev->read_wq);
// 2. 销毁等待队列头(检查链表空)
destroy_waitqueue_head(&dev->read_wq);
// 3. 释放设备结构体(包含等待队列头)
kfree(dev);
}
4.6 等待队列初始化与销毁流程图
下图展示了等待队列从创建到销毁的完整生命周期,以及两种初始化路径:
4.7 常见错误与避坑指南
| 错误模式 | 后果 | 正确做法 |
|---|---|---|
忘记调用 init_waitqueue_head |
自旋锁未初始化,链表指针为垃圾值,内核立即崩溃 | 动态分配后立即调用初始化函数 |
| 在链表非空时销毁 | 悬空指针,后续唤醒导致 use-after-free | 先 wake_up_all,再 destroy_waitqueue_head |
对静态声明再次调用 init_waitqueue_head |
重复初始化自旋锁,可能破坏锁状态 | 静态声明后直接使用,不再调用初始化函数 |
| 销毁后未置空指针 | 野指针风险,后续误用导致崩溃 | 销毁后立即将指针置为 NULL |
在中断上下文中调用 init_waitqueue_head |
可能睡眠(kmalloc),违反中断上下文规则 | 在进程上下文中完成初始化 |
内核调试技巧: 如果怀疑等待队列初始化或销毁有问题,可以开启内核的等待队列调试选项
CONFIG_DEBUG_WAITQ(如果内核支持)。该选项会在 destroy_waitqueue_head 时打印更详细的警告信息,包括链表中的等待项数量。
4.8 小结
- 静态声明:使用
DECLARE_WAIT_QUEUE_HEAD(name),编译时完成初始化,适合全局/静态变量。 - 动态初始化:使用
init_waitqueue_head(&wq),运行时调用,适合嵌入结构体或动态分配。 - 销毁:使用
destroy_waitqueue_head宏做完整性检查,但必须配合wake_up_all和kfree完成完整清理。 - 核心原则:初始化与销毁必须配对,销毁前确保链表为空,避免悬空指针。