4. 等待队列头初始化:静态初始化(DECLARE_WAIT_QUEUE_HEAD)与动态初始化(init_waitqueue_head)

等待队列(Wait Queue)是 Linux 内核实现阻塞 I/O 的核心机制。当进程因资源不可用而需要休眠时,内核将其放入一个等待队列中;当资源可用时,内核唤醒队列中的进程。等待队列的头部(wait_queue_head_t)是队列的管理结构,在使用前必须正确初始化。本章详细讲解两种初始化方式:静态初始化与动态初始化。

4.1 等待队列头数据结构

等待队列头在内核中定义为 wait_queue_head_t 类型,其核心结构如下:

// 定义于 <linux/wait.h>
typedef struct __wait_queue_head {
    spinlock_t      lock;       // 自旋锁,保护队列操作
    struct list_head    task_list;  // 双向链表头,链接等待队列项
} wait_queue_head_t;

该结构包含两个成员:

  • lock:自旋锁,用于保护队列的并发访问(多核环境下的互斥)。
  • task_list:双向链表头,所有等待该资源的进程通过 wait_queue_entry_t 挂入此链表。
💡 关键理解: 等待队列头本身不存储进程信息,它只是一个链表的管理节点。每个等待进程对应一个 wait_queue_entry_t 结构,通过 task_list 串联起来。

4.2 静态初始化:DECLARE_WAIT_QUEUE_HEAD

静态初始化在编译时完成,适用于全局变量或模块级别的等待队列头。使用宏 DECLARE_WAIT_QUEUE_HEAD(name) 声明并初始化一个等待队列头。

4.2.1 宏定义与展开

// 内核源码中的宏定义
#define DECLARE_WAIT_QUEUE_HEAD(name)                   \
    wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)

// 展开后的实际初始化
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) {           \
    .lock       = __SPIN_LOCK_UNLOCKED(name.lock),      \
    .task_list  = { &(name).task_list, &(name).task_list } }

4.2.2 使用示例

#include <linux/wait.h>

// 静态声明并初始化一个名为 my_wq 的等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wq);

// 驱动模块入口函数中可直接使用,无需额外初始化
static int __init my_driver_init(void)
{
    // my_wq 已经初始化完毕,可以直接用于 wait_event / wake_up
    printk(KERN_INFO "my_wq 已静态初始化\n");
    return 0;
}

4.2.3 适用场景

场景 说明
全局等待队列 驱动中只有一个设备实例,等待队列头定义为全局变量
模块级资源 等待队列与模块生命周期一致,不需要动态创建/销毁
简单驱动 字符设备驱动中,使用单一等待队列管理所有读/写阻塞
✅ 优点: 代码简洁,编译时完成初始化,无需运行时调用函数;适合大多数单实例驱动场景。

4.3 动态初始化:init_waitqueue_head

动态初始化在运行时完成,适用于等待队列头作为结构体成员、动态分配内存或需要按需创建的场景。使用函数 init_waitqueue_head() 进行初始化。

4.3.1 函数原型

// 定义于 <linux/wait.h>
void init_waitqueue_head(struct wait_queue_head *wq_head);

4.3.2 使用示例

#include <linux/wait.h>
#include <linux/slab.h>

// 场景1:等待队列头作为设备结构体成员
struct my_device {
    int                 data_ready;
    wait_queue_head_t   read_wq;    // 等待队列头成员
    // ... 其他成员
};

static int __init my_driver_init(void)
{
    struct my_device *dev;

    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    // 动态初始化等待队列头
    init_waitqueue_head(&dev->read_wq);

    // 场景2:单独动态分配的等待队列头
    wait_queue_head_t *dyn_wq;
    dyn_wq = kmalloc(sizeof(*dyn_wq), GFP_KERNEL);
    if (!dyn_wq) {
        kfree(dev);
        return -ENOMEM;
    }
    init_waitqueue_head(dyn_wq);

    return 0;
}

4.3.3 适用场景

场景 说明
多设备实例 每个设备结构体包含独立的等待队列头,在 probe 时动态初始化
动态分配 等待队列头通过 kmalloc 分配,必须在分配后调用 init_waitqueue_head
热插拔设备 设备插入时创建等待队列,拔出时销毁
复杂驱动架构 驱动需要管理多个不同类型的等待队列(读、写、异常等)
⚠️ 常见错误: 动态分配的等待队列头必须调用 init_waitqueue_head() 进行初始化。直接使用未初始化的等待队列头会导致链表操作崩溃(NULL 指针解引用或野指针遍历)。

4.4 两种初始化方式对比

对比维度 静态初始化 DECLARE_WAIT_QUEUE_HEAD 动态初始化 init_waitqueue_head
初始化时机 编译时 运行时
声明位置 全局作用域(文件顶部或模块外) 函数内部或结构体成员
内存分配 静态分配(数据段或 BSS 段) 静态分配或动态分配(堆)
适用变量类型 全局变量、模块级变量 结构体成员、动态分配指针
代码简洁性 一行宏完成声明+初始化 需要先声明再调用函数
灵活性 低(固定变量名) 高(可嵌入任意数据结构)
多实例支持 不直接支持(需为每个实例单独声明) 天然支持(每个实例独立初始化)

4.5 初始化后的等待队列结构

无论使用哪种初始化方式,初始化完成后等待队列头的内部状态是一致的。下面的 SVG 图展示了初始化后的等待队列头结构:

初始化后的等待队列头结构 wait_queue_head_t lock (spinlock_t) task_list (list_head) task_list.next task_list.prev 初始化完成后的状态 • task_list.next 指向自身(空队列) • task_list.prev 指向自身(空队列) • lock 自旋锁处于未锁定状态

4.6 初始化后的操作流程

等待队列头初始化完成后,驱动中典型的阻塞 I/O 操作流程如下:

  1. 进程尝试读取数据:调用 read() 系统调用进入驱动。
  2. 检查资源可用性:驱动判断数据是否就绪。
  3. 资源不可用时:调用 wait_event_interruptible(wq_head, condition) 将当前进程放入等待队列并休眠。
  4. 资源变为可用时:中断处理函数或其他上下文调用 wake_up_interruptible(&wq_head) 唤醒队列中的进程。
  5. 进程被唤醒:重新检查条件,满足则继续执行,否则再次休眠。
📌 最佳实践:
  • 对于单设备驱动,优先使用 DECLARE_WAIT_QUEUE_HEAD 静态初始化,代码更简洁。
  • 对于多设备实例(如 USB 设备、PCI 设备),将等待队列头放入设备结构体,使用 init_waitqueue_head 动态初始化。
  • 始终在模块卸载或设备移除时确保没有进程在等待队列中休眠,避免悬挂指针。

4.7 常见错误与调试

错误类型 现象 原因 解决方案
未初始化使用 内核崩溃(Oops),回溯指向 list_add/del 等待队列头未调用 init_waitqueue_head 检查所有分配路径,确保调用初始化函数
重复初始化 自旋锁递归锁定,系统死锁 对已初始化的等待队列头再次调用 init_waitqueue_head 使用静态初始化或确保只初始化一次
静态宏误用 编译错误:expected expression 在函数内部使用 DECLARE_WAIT_QUEUE_HEAD 该宏只能在文件作用域使用,函数内改用动态初始化
动态分配后未初始化 随机崩溃,链表操作异常 kmalloc 分配后直接使用,未调用 init_waitqueue_head 分配后立即调用初始化函数
🔧 调试技巧: 使用 WARN_ON(!wq_head->task_list.next) 或 BUG_ON(!wq_head->task_list.next) 在关键路径上检查等待队列头是否已初始化。内核的 lockdep 机制也能检测自旋锁相关的初始化问题。