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 完整销毁流程

正确的销毁步骤:

  1. 唤醒所有等待进程:调用 wake_up_all(&wq_head) 确保链表清空。
  2. 调用 destroy_waitqueue_head:触发链表空检查。
  3. 释放内存:如果 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 等待队列初始化与销毁流程图

下图展示了等待队列从创建到销毁的完整生命周期,以及两种初始化路径:

等待队列头生命周期流程图 等待队列头创建 静态/动态? DECLARE_WAIT_QUEUE_HEAD 编译时初始化 init_waitqueue_head 运行时初始化 静态声明 动态分配 等待队列就绪 进程等待/唤醒 销毁等待队列头 ① wake_up_all 清空链表 ② destroy_waitqueue_head ③ kfree 释放内存 生命周期结束

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 完成完整清理。
  • 核心原则:初始化与销毁必须配对,销毁前确保链表为空,避免悬空指针。