4. 等待队列初始化:静态初始化(DECLARE_WAIT_QUEUE_HEAD)、动态初始化(init_waitqueue_head)、使用场景对比

等待队列在使用前必须完成初始化。Linux 内核提供了两种初始化方式:静态初始化(编译时完成)和动态初始化(运行时完成)。两种方式在功能上等价,但在使用场景、代码风格和灵活性上存在差异。理解它们的区别,是正确使用等待队列的第一步。

4.1 静态初始化:DECLARE_WAIT_QUEUE_HEAD

静态初始化通过宏 DECLARE_WAIT_QUEUE_HEAD 在编译时完成等待队列头的定义和初始化。该宏在声明变量的同时,将等待队列头中的自旋锁和链表头初始化为默认值。

#include <linux/wait.h>

/* 静态初始化:在全局或局部作用域直接声明 */
DECLARE_WAIT_QUEUE_HEAD(my_wq);

/* 宏展开后的等价形式 */
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
    .lock       = __SPIN_LOCK_UNLOCKED(name.lock), \
    .head       = { &name.head, &name.head } \
}

#define DECLARE_WAIT_QUEUE_HEAD(name) \
    struct wait_queue_head name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
适用场景:静态初始化最适合全局等待队列或模块加载时即确定存在的等待队列。例如,字符设备驱动中用于管理 read/write 阻塞的全局等待队列,通常在文件操作结构体外部定义。

4.2 动态初始化:init_waitqueue_head

动态初始化在运行时调用 init_waitqueue_head 函数完成。它适用于等待队列头作为结构体成员、动态分配内存或需要在特定初始化函数中创建的场景。

#include <linux/wait.h>

/* 方式一:作为结构体成员 */
struct my_device {
    int             data;
    struct wait_queue_head   read_wq;
    struct wait_queue_head   write_wq;
};

struct my_device *dev;

dev = kzalloc(sizeof(*dev), GFP_KERNEL);
init_waitqueue_head(&dev->read_wq);
init_waitqueue_head(&dev->write_wq);

/* 方式二:在 probe 或 init 函数中初始化 */
static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    init_waitqueue_head(&dev->read_wq);
    init_waitqueue_head(&dev->write_wq);
    return 0;
}
注意事项:动态初始化必须确保在任何进程尝试休眠(调用 wait_event 系列函数)之前完成。在 probe 函数中初始化是推荐做法,因为 probe 执行时设备尚未对外暴露,不会有并发访问。

4.3 两种初始化方式对比

对比维度 静态初始化 (DECLARE_WAIT_QUEUE_HEAD) 动态初始化 (init_waitqueue_head)
初始化时机 编译时 运行时
代码位置 全局作用域或函数外部 函数内部(probe/init)
适用对象 全局等待队列、模块级单例 结构体成员、动态分配、多实例设备
灵活性 低:编译时固定,无法动态创建 高:可配合 kzalloc、devm_kzalloc 使用
内存位置 数据段(.data / .bss) 堆(动态分配)或栈(局部变量)
典型场景 简单字符驱动、单设备驱动 platform 驱动、USB 驱动、多实例设备

4.4 初始化流程对比图

下图展示了两种初始化方式在驱动生命周期中的位置差异:

等待队列初始化流程对比 静态初始化路径 编译阶段 DECLARE_WAIT_QUEUE_HEAD(my_wq) 数据段中分配内存 模块加载时即可使用 动态初始化路径 模块加载 / probe 阶段 kzalloc 分配设备结构体 init_waitqueue_head(&dev->wq) 设备注册后对外可用

4.5 多等待队列场景:设备结构体中的动态初始化

在实际驱动中,一个设备往往需要多个等待队列来管理不同的阻塞条件。例如,一个全双工字符设备需要独立的读等待队列和写等待队列。动态初始化可以优雅地处理这种多实例、多队列的场景。

struct my_device {
    spinlock_t          lock;
    struct cdev         cdev;
    struct wait_queue_head   read_wq;    /* 等待数据可读 */
    struct wait_queue_head   write_wq;   /* 等待空间可写 */
    unsigned int        read_avail;     /* 可读数据量 */
    unsigned int        write_avail;    /* 可写空间量 */
};

static int my_open(struct inode *inode, struct file *filp)
{
    struct my_device *dev = container_of(inode->i_cdev, struct my_device, cdev);
    filp->private_data = dev;
    return 0;
}

static ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    struct my_device *dev = filp->private_data;
    /* 等待数据可读 */
    wait_event_interruptible(dev->read_wq, dev->read_avail > 0);
    /* ... 读取数据逻辑 ... */
    return 0;
}

static ssize_t my_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct my_device *dev = filp->private_data;
    /* 等待空间可写 */
    wait_event_interruptible(dev->write_wq, dev->write_avail > 0);
    /* ... 写入数据逻辑 ... */
    return 0;
}
最佳实践:
  • 单设备、简单驱动:优先使用静态初始化,代码简洁,编译时即完成初始化。
  • 多实例、结构体成员:必须使用动态初始化,在 probe 函数中为每个实例独立初始化等待队列。
  • 混合使用:全局控制路径(如设备打开计数)可用静态初始化;每个设备实例的阻塞队列用动态初始化。
  • 检查初始化状态:动态初始化后,可通过 waitqueue_active() 检查队列是否被初始化,但通常不需要——只要 probe 成功,初始化必然完成。

4.6 初始化选择决策图

下面的决策图帮助你在实际开发中快速选择正确的初始化方式:

等待队列初始化方式选择决策 等待队列需要初始化 是结构体成员? 是 动态初始化 init_waitqueue_head() 否 静态初始化 DECLARE_WAIT_QUEUE_HEAD()

4.7 常见错误与避坑指南

  • 重复初始化:对已经静态初始化的等待队列再次调用 init_waitqueue_head 会导致自旋锁状态混乱。静态初始化的队列不应再动态初始化。
  • 未初始化即使用:动态分配的等待队列在使用前必须调用 init_waitqueue_head。未初始化的等待队列头中的链表指针为垃圾值,调用 wait_event 会导致内核崩溃。
  • 在错误上下文中初始化:init_waitqueue_head 内部调用 spin_lock_init,该函数在进程上下文或 tasklet 中均可安全调用,但不要在中断上下文(hardirq)中初始化等待队列。
  • 忘记包含头文件:使用等待队列必须包含 <linux/wait.h>,否则编译时会报隐式声明错误。
内核调试提示:如果驱动加载后出现 spin_lock 相关异常,且等待队列涉及自旋锁,优先检查等待队列是否被正确初始化。使用 lockdep 内核选项可以检测未初始化的自旋锁。