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 图展示了初始化后的等待队列头结构:
4.6 初始化后的操作流程
等待队列头初始化完成后,驱动中典型的阻塞 I/O 操作流程如下:
- 进程尝试读取数据:调用
read()系统调用进入驱动。 - 检查资源可用性:驱动判断数据是否就绪。
- 资源不可用时:调用
wait_event_interruptible(wq_head, condition)将当前进程放入等待队列并休眠。 - 资源变为可用时:中断处理函数或其他上下文调用
wake_up_interruptible(&wq_head)唤醒队列中的进程。 - 进程被唤醒:重新检查条件,满足则继续执行,否则再次休眠。
📌 最佳实践:
- 对于单设备驱动,优先使用
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 机制也能检测自旋锁相关的初始化问题。