3. 阻塞IO在内核中的实现原理:等待队列与调度机制

阻塞 I/O 是 Linux 驱动中最基础的读写模式。当应用程序调用 read() 而设备暂无数据时,内核不会立即返回 -EAGAIN,而是将进程置入睡眠状态,直到数据到达后再唤醒。这一过程的核心基础设施是 等待队列(wait_queue) 与内核调度器提供的 schedule() / wake_up() 原语。

3.1 等待队列(wait_queue_head_t)

等待队列是一个双链表结构,链表的每个节点代表一个等待在该队列上的进程。驱动开发者通过等待队列管理哪些进程在等待某个条件(如数据可读、缓冲区可写)。

内核结构 头文件 核心字段 用途
wait_queue_head_t <linux/wait.h> spinlock_t lock + struct list_head head 等待队列头,管理所有等待进程
wait_queue_entry_t <linux/wait.h> unsigned int flags + void *private + wait_queue_func_t func + struct list_head entry 等待队列节点,代表一个等待进程
关键初始化宏:
  • 静态初始化:DECLARE_WAIT_QUEUE_HEAD(my_wq);
  • 动态初始化:init_waitqueue_head(&my_wq);

等待队列核心数据结构关系

等待队列数据结构关系图 wait_queue_head_t spinlock_t lock struct list_head head wait_queue_entry_t unsigned int flags void *private (task_struct) wait_queue_func_t func struct list_head entry wait_queue_entry_t unsigned int flags void *private (task_struct) wait_queue_func_t func struct list_head entry 双向链表组织 head 管理链表头 entry 通过 list_head 串联所有等待进程

3.2 阻塞流程:从 read() 到 schedule()

当设备无数据时,驱动中的 read() 回调函数执行以下标准步骤,将当前进程置入睡眠:

  1. 声明等待队列条目:使用 DECLARE_WAITQUEUE(wait, current); 创建节点,关联当前进程。
  2. 设置进程状态:set_current_state(TASK_INTERRUPTIBLE); 标记进程为可中断睡眠。
  3. 加入等待队列:add_wait_queue(&dev->wq, &wait); 将节点挂入队列。
  4. 检查条件并调度:如果数据仍未就绪,调用 schedule() 让出 CPU。
  5. 被唤醒后处理:从 schedule() 返回后,检查是否被信号中断,移除等待队列节点。
典型代码模板:
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;
    DECLARE_WAITQUEUE(wait, current);
    ssize_t ret = 0;

    // 1. 加入等待队列
    add_wait_queue(&dev->rq, &wait);

    while (dev->data_avail == 0) {
        // 2. 设置进程状态
        set_current_state(TASK_INTERRUPTIBLE);
        if (dev->data_avail)
            break;
        // 3. 让出 CPU
        schedule();
        // 4. 检查信号
        if (signal_pending(current)) {
            ret = -ERESTARTSYS;
            goto out;
        }
    }

    // 5. 拷贝数据到用户空间
    // ...

out:
    // 6. 恢复状态并移除等待队列
    set_current_state(TASK_RUNNING);
    remove_wait_queue(&dev->rq, &wait);
    return ret;
}

3.3 唤醒流程:wake_up() 内部机制

当设备数据到达(如中断处理函数或内核线程完成写入),驱动调用 wake_up() 系列函数唤醒等待队列上的进程。唤醒的核心流程如下:

  • wake_up(x):遍历等待队列链表,对每个节点调用其 func 回调(默认是 autoremove_wake_function)。
  • try_to_wake_up():将目标进程的状态设为 TASK_RUNNING,并将其加入运行队列。
  • 调度器重调度:被唤醒的进程在下次调度时获得 CPU 时间,从 schedule() 返回继续执行。
唤醒函数 行为 适用场景
wake_up(wq) 唤醒所有 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 的进程 数据就绪、缓冲区可写
wake_up_interruptible(wq) 仅唤醒 TASK_INTERRUPTIBLE 的进程 允许信号打断的场景
wake_up_nr(wq, nr) 只唤醒指定数量的进程 资源有限,避免惊群

阻塞与唤醒完整流程

阻塞 I/O 完整生命周期:schedule() 与 wake_up() 流程 进程 A:调用 read() add_wait_queue() + set_current_state() schedule() → 进程睡眠 被唤醒 → 从 schedule() 返回 检查数据 → 拷贝到用户空间 中断处理 / 生产者线程 wake_up(&dev->rq) try_to_wake_up(进程A) 唤醒 关键点 schedule() 不返回直到被 wake_up() wake_up() 只改状态,不抢占当前 CPU

3.4 等待队列的变体与高级用法

内核提供了多种等待队列的封装,简化常见模式:

  • wait_event_interruptible(wq, condition):自动完成声明节点、设置状态、加入队列、检查条件、调度的全部步骤。
  • wait_event_timeout():增加超时机制,超时后自动返回。
  • exclusive 等待:设置 WQ_FLAG_EXCLUSIVE 标志,避免惊群效应(多个进程同时被唤醒竞争资源)。
常见陷阱:
  • 丢失唤醒:在检查条件与 schedule() 之间,如果条件变为真且 wake_up() 被调用,进程将永远睡眠。必须使用 set_current_state() 保证原子性。
  • 虚假唤醒:即使没有显式调用 wake_up(),进程也可能因信号被唤醒。因此必须使用 while 循环重新检查条件。
  • 中断上下文调用:schedule() 不能在中断上下文调用;wake_up() 可以在中断上下文调用,但需使用 wake_up_locked() 或自旋锁保护。

3.5 等待队列与调度器协作总结

组件 角色 关键函数/宏
等待队列头 管理等待进程链表 init_waitqueue_head()
等待队列节点 代表一个睡眠进程 DECLARE_WAITQUEUE()
进程状态切换 标记进程为可睡眠 set_current_state()
主动让出 CPU 触发调度,进程睡眠 schedule()
唤醒等待进程 将进程置回运行队列 wake_up() / wake_up_interruptible()
高级封装 简化阻塞读/写实现 wait_event_interruptible()