你写的应该是:

init_waitqueue_head()

不是 init_waitqeue_head,中间是 queue

它的作用是:

初始化一个等待队列头 wait_queue_head_t,让后续进程可以在这个队列上睡眠,等事件发生后再被 wake_up 唤醒。


1. 它解决什么问题?

在驱动里经常有这种场景:

用户进程 read() 设备
    ↓
当前没有数据
    ↓
不能一直死循环占 CPU
    ↓
让进程睡眠
    ↓
DMA 完成 / 中断来了 / 数据准备好
    ↓
中断函数唤醒睡眠的进程

这个“让进程睡眠、之后再唤醒”的机制,就常用 wait queue 等待队列

Linux 内核文档中,wait_event() / wait_event_interruptible() 都是让进程在某个 wait queue 上睡眠,直到 condition 条件成立;并且要求在改变条件变量后调用 wake_up() 唤醒等待者。(Linux Kernel 文档)


2. 基本写法

一般在设备结构体里定义:

struct my_dma_dev {
    wait_queue_head_t read_wq;
    int data_ready;
};

然后在 probe() 或模块初始化里初始化:

init_waitqueue_head(&dev->read_wq);
dev->data_ready = 0;

含义是:

初始化 read_wq 这个等待队列头
以后进程可以睡在 read_wq 上
中断来了可以通过 wake_up(&dev->read_wq) 唤醒它

3. read() 里面等待数据

例如用户态调用:

read(fd, buf, len);

驱动的 read() 回调里可以这样写:

static ssize_t my_read(struct file *file,
                       char __user *buf,
                       size_t count,
                       loff_t *ppos)
{
    struct my_dma_dev *dev = file->private_data;
    int ret;

    ret = wait_event_interruptible(dev->read_wq, dev->data_ready == 1);
    if (ret)
        return ret;   // 被信号打断,一般返回 -ERESTARTSYS

    /*
     * 能执行到这里,说明 data_ready == 1
     * 也就是 DMA 数据已经准备好
     */

    dev->data_ready = 0;

    /*
     * copy_to_user(buf, ...)
     */

    return count;
}

wait_event_interruptible() 的意思是:当前进程进入 TASK_INTERRUPTIBLE 睡眠,直到条件为真,或者收到信号;如果被信号打断,会返回 -ERESTARTSYS,如果条件变为真则返回 0。(Linux Kernel 文档)


4. 中断函数里唤醒

当 FPGA DMA 完成后,触发 MSI 中断,驱动中断函数里:

static irqreturn_t dma_irq_handler(int irq, void *dev_id)
{
    struct my_dma_dev *dev = dev_id;
    u32 status;

    status = ioread32(dev->bar0 + INTERRUPT_STATUS);

    if (status & INT_C2H_DONE) {
        dev->data_ready = 1;
        wake_up_interruptible(&dev->read_wq);
    }

    return IRQ_HANDLED;
}

这几句的含义是:

1. 中断来了
2. 驱动确认 C2H DMA 完成
3. 设置 data_ready = 1
4. 唤醒睡在 read_wq 上的进程
5. read() 继续往下执行

5. 完整流程

1. 驱动 probe()
2. init_waitqueue_head(&dev->read_wq)
3. 用户调用 read()
4. read() 发现 data_ready == 0
5. wait_event_interruptible() 让进程睡眠
6. FPGA DMA 完成
7. FPGA 触发 MSI 中断
8. Linux 调用 irq_handler()
9. irq_handler 设置 data_ready = 1
10. irq_handler 调用 wake_up_interruptible()
11. 睡眠的 read() 被唤醒
12. read() 检查 data_ready == 1
13. copy_to_user() 把数据给用户态
14. read() 返回

6. init_waitqueue_head() 初始化了什么?

可以简单理解为它初始化两个东西:

1. 等待队列内部链表
2. 保护等待队列的锁

也就是把这个队列变成一个合法可用的等待队列。

如果不初始化,后面直接 wait_event_interruptible()wake_up_interruptible(),可能导致内核异常、死机或者不可预期行为。


7. 和 DECLARE_WAIT_QUEUE_HEAD() 的区别

有两种常见初始化方式。

方式一:动态初始化

适合结构体成员:

struct my_dma_dev {
    wait_queue_head_t read_wq;
};

init_waitqueue_head(&dev->read_wq);

这是驱动里最常见的方式。


方式二:静态定义并初始化

适合全局变量:

static DECLARE_WAIT_QUEUE_HEAD(read_wq);

它等价于:

定义 read_wq
同时完成初始化

老版 Linux Device Drivers 也说明,等待队列可以先定义 wait_queue_head_t,再用 init_waitqueue_head() 初始化;如果是静态声明,也可以用 DECLARE_WAIT_QUEUE_HEAD() 在编译期初始化。(LWN.net)


8. 在 PCIe DMA 驱动里的典型用法

你的 Block DMA 场景可以这样对应:

C2H DMA:FPGA → Host

用户态可能调用:

read()

然后驱动等待 C2H DMA 完成:

wait_event_interruptible(dev->c2h_wq, dev->c2h_done);

DMA 完成中断里:

dev->c2h_done = 1;
wake_up_interruptible(&dev->c2h_wq);

所以 init_waitqueue_head() 一般在 probe() 里做:

static int my_pcie_probe(struct pci_dev *pdev,
                         const struct pci_device_id *id)
{
    struct my_dma_dev *dev;

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

    init_waitqueue_head(&dev->c2h_wq);
    init_waitqueue_head(&dev->h2c_wq);

    dev->c2h_done = 0;
    dev->h2c_done = 0;

    return 0;
}

中断里:

if (status & INT_BLC2H) {
    dev->c2h_done = 1;
    wake_up_interruptible(&dev->c2h_wq);
}

if (status & INT_BLH2C) {
    dev->h2c_done = 1;
    wake_up_interruptible(&dev->h2c_wq);
}

用户线程里:

ret = wait_event_interruptible(dev->c2h_wq, dev->c2h_done);

9. 最容易出错的点

第一,必须先改变条件,再 wake_up

dev->data_ready = 1;
wake_up_interruptible(&dev->read_wq);

不要反过来:

wake_up_interruptible(&dev->read_wq);
dev->data_ready = 1;

因为被唤醒的线程会检查 condition,如果条件还没变成真,可能继续睡回去。内核文档也强调,改变会影响等待条件的变量后,需要调用 wake_up()。(Linux Kernel 文档)

第二,wait_event_interruptible() 的条件必须是真实状态,不要写成单纯等中断:

wait_event_interruptible(dev->read_wq, dev->data_ready == 1);

因为唤醒可能是虚假唤醒,醒来后还要再次判断条件。

第三,多个线程同时等同一个队列时,要考虑锁、状态变量和数据队列,否则可能一个 DMA 完成事件被多个线程同时消费。


10. 一句话总结

init_waitqueue_head(&dev->read_wq);

意思是:

初始化一个等待队列。后面 read/ioctl 等进程可以通过 wait_event_interruptible() 睡在这个队列上;DMA 中断来了以后,驱动通过 wake_up_interruptible() 把它们唤醒。

在 PCIe DMA 驱动中,它通常用于:

用户进程等待 DMA 完成
中断函数通知 DMA 已完成
驱动唤醒等待的用户进程
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐