欢迎光临
我们一直在努力

Linux 内核等待队列 wait_queue 机制在非阻塞驱动读写中的底层剖析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}

Linux 内核等待队列 wait_queue 机制在非阻塞驱动读写中的底层剖析

封面信息图

在 Linux 字符设备驱动开发中,当用户态应用程序调用 read() 试图从一个设备(如按键驱动、串口 FIFO、传感器数据队列)读取数据,而此时硬件底层**尚无任何数据就绪(Buffer Empty)**时,驱动程序面临两种标准的 POSIX 行为规范:

  • 阻塞模式(Blocking I/O,默认):当前调用进程必须陷入休眠(Sleep/Blocked),让出 CPU 核心给其他进程运行,直到硬件产生中断并准备好数据后,再将该进程精准唤醒并返回数据;
  • 非阻塞模式(Non-blocking I/O,打开文件时带有 O_NONBLOCK 标志):驱动不得陷入任何睡眠,必须立即向用户空间返回 -EAGAIN 或 -EWOULDBLOCK 错误码。
  • 实现这一精妙机制的内核核心基础设施,就是 等待队列(Wait Queue / wait_queue_head_t)。

    深入掌握等待队列的双向循环链表数据结构、wait_event_interruptible 宏展开状态机、进程睡眠状态(TASK_INTERRUPTIBLE) 以及 wake_up_interruptible 唤醒机制,是编写符合 Linux 标准 POSIX 规范的高性能驱动的立身之本。

    等待队列的微观内核数据结构拓扑

    Linux 内核中的等待队列本质上是一个由自旋锁保护的、挂载了多个进程描述符(task_struct)的双向循环链表:

    Linux 等待队列底层双向链表拓扑:

    +=========================================================================+
    | 【等待队列头 (wait_queue_head_t)】 |
    | ├── spinlock_t lock; (自旋锁,保护多核并发增删节点) |
    | └── struct list_head head; (双向循环链表头) |
    +=========================================================================+
    │
    ├───► [ 等待项 1 (wait_queue_entry_t) ]
    │ ├── flags: 0 (非互斥唤醒)
    │ ├── private: 指向 Process A 的 task_struct 结构体!
    │ └── func: default_wake_function (内核默认唤醒回调函数)
    │
    └───► [ 等待项 2 (wait_queue_entry_t) ]
    ├── flags: 0
    ├── private: 指向 Process B 的 task_struct 结构体!
    └── func: default_wake_function

    进程睡眠与唤醒的内核宏展开状态机

    当驱动内部调用 wait_event_interruptible(wq_head, condition) 时,内核通过宏展开执行了一套极其严密的四步状态机:

    wait_event_interruptible 核心状态机时序:

    1. [条件初检]
    – 检查 condition 是否已经为真 (如 buffer_count > 0);
    – 若真,直接退出宏,无需任何睡眠!

    2. [构建节点并入队]
    – 在当前进程的内核栈上分配一个 wait_queue_entry_t 节点;
    – 获取 wq_head.lock 自旋锁,将该节点插入等待队列的双向链表中!

    3. [修改进程状态并调度让出 CPU]
    – set_current_state(TASK_INTERRUPTIBLE): 将当前进程状态从运行态 (TASK_RUNNING)
    修改为【可中断睡眠态 (TASK_INTERRUPTIBLE)】;
    – 再次检查 condition (防止在修改状态瞬间发生并发唤醒 Race Condition);
    – 调用 schedule(): 触发内核调度器!当前进程彻底交出 CPU,陷入休眠!

    4. [被唤醒与恢复执行]
    – 当硬件中断调用 wake_up_interruptible(&wq_head) 时:
    内核遍历链表,将进程状态修改回 TASK_RUNNING 并挂成就绪态;
    – 进程重新获得 CPU 执行时间,从 schedule() 下一行醒来;
    – 检查是否是被系统信号 (如 Ctrl+C / SIGINT) 意外中断:
    ├─► 若是信号唤醒: 返回 -ERESTARTSYS;
    └─► 若是条件满足: 将自身节点移出等待队列,返回 0 成功继续!

    工业级支持阻塞与非阻塞读取的字符驱动源码实战

    编写一个带有环形缓冲区(Ring Buffer)和等待队列的标准 Linux 字符设备驱动:

    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/wait.h>
    #include <linux/sched/signal.h>
    #include <linux/uaccess.h>
    #include <linux/interrupt.h>

    #define BUF_SIZE 256

    struct custom_fifo_dev {
    struct cdev cdev;
    wait_queue_head_t r_wait; // 专用于读等待的等待队列头
    wait_queue_head_t w_wait; // 专用于写等待的等待队列头
    uint8_t buffer[BUF_SIZE];
    uint32_t head;
    uint32_t tail;
    uint32_t count;
    struct mutex lock;
    };

    static struct custom_fifo_dev g_dev;

    // 1. 字符设备 read 接口实现 (完美支持阻塞与 O_NONBLOCK)
    static ssize_t custom_fifo_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos) {
    struct custom_fifo_dev *dev = filp->private_data;
    uint8_t data;
    int ret;

    // 步骤 1: 处理非阻塞模式 (O_NONBLOCK)
    if (filp->f_flags & O_NONBLOCK) {
    if (dev->count == 0) {
    // 无数据立即返回,绝不睡眠!
    return -EAGAIN;
    }
    } else {
    // 步骤 2: 处理阻塞模式 —— 等待数据到来 (dev->count > 0)
    // 使用带信号中断的 wait_event_interruptible
    ret = wait_event_interruptible(dev->r_wait, (dev->count > 0));
    if (ret != 0) {
    // 被用户态信号 (如 SIGINT) 打断,通知系统重启系统调用
    return -ERESTARTSYS;
    }
    }

    // 步骤 3: 获取互斥锁,从环形缓冲区提取数据
    if (mutex_lock_interruptible(&dev->lock)) {
    return -ERESTARTSYS;
    }

    data = dev->buffer[dev->tail];
    dev->tail = (dev->tail + 1) % BUF_SIZE;
    dev->count–;

    // 步骤 4: 唤醒可能正在因缓冲区满而阻塞的写进程!
    wake_up_interruptible(&dev->w_wait);

    mutex_unlock(&dev->lock);

    // 步骤 5: 拷贝到用户空间
    if (copy_to_user(buf, &data, 1)) {
    return -EFAULT;
    }

    return 1; // 成功读取 1 字节
    }

    // 2. 硬件数据接收中断服务例程 (ISR)
    static irqreturn_t hardware_rx_data_isr(int irq, void *dev_id) {
    struct custom_fifo_dev *dev = (struct custom_fifo_dev *)dev_id;

    // 从硬件寄存器读取新字节并推入环形缓冲区…
    dev->buffer[dev->head] = read_hardware_rx_reg();
    dev->head = (dev->head + 1) % BUF_SIZE;
    dev->count++;

    // 核心唤醒: 瞬间唤醒所有挂在 r_wait 等待队列中的休眠读进程!
    wake_up_interruptible(&dev->r_wait);

    return IRQ_HANDLED;
    }

    // 3. 设备初始化
    static int __init custom_fifo_init(void) {
    // 初始化等待队列头
    init_waitqueue_head(&g_dev.r_wait);
    init_waitqueue_head(&g_dev.w_wait);
    mutex_init(&g_dev.lock);
    // … 注册 cdev 与中断 …
    return 0;
    }

    编写等待队列驱动的三大铁律

    第一,严禁使用无信号响应的 wait_event()。如果使用 wait_event() 将进程置于不可中断休眠态(TASK_UNINTERRUPTIBLE),一旦硬件故障未发出中断,该进程将变成不可杀死的“僵尸 D 状态进程(D state)”,连 kill -9 都无法终止,必须使用 wait_event_interruptible()!

    第二,条件判断表达式必须是“幂等”的纯布尔表达式。条件表达式会被内核反复多次求值,严禁在条件宏内部包含带有自增或副作用的代码(如 dev->count– > 0);

    第三,中断唤醒使用 wake_up_interruptible。在顶半部或底半部中安全唤醒,由内核完成锁与上下文切换。

    掌握等待队列的微观流转,Linux 驱动程序才能在阻塞休眠与极速唤醒之间展现出标准的工业级操作系统规范。

    赞(0)
    未经允许不得转载:171主机测评 » Linux 内核等待队列 wait_queue 机制在非阻塞驱动读写中的底层剖析
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址