1. 背景与意义

1.1 并发与同步的挑战

在 Linux 内核开发中,进程/线程经常需要等待某些条件成立(如 I/O 完成、资源可用、事件发生等),而不是一直占用 CPU 忙等。如何高效地让进程“睡眠”并在条件满足时被唤醒,是内核并发同步的核心问题之一。

1.2 等待队列的作用

等待队列(wait queue)是 Linux 内核提供的一种高效、灵活的睡眠与唤醒机制。它允许一个或多个进程/线程在某个条件未满足时主动挂起自己(进入睡眠),并在条件满足时被其他线程或中断处理程序唤醒。等待队列广泛应用于设备驱动、内存管理、文件系统、进程调度等内核子系统。

2. 核心数据结构

2.1 struct wait_queue_head

struct wait_queue_head {
    spinlock_t      lock;
    struct list_head head;
};
typedef struct wait_queue_head wait_queue_head_t;
  • lock:自旋锁,保护等待队列的并发访问。

  • head:链表头,挂载所有等待该队列的 wait_queue_entry。

2.2 struct wait_queue_entry

struct wait_queue_entry {
    unsigned int        flags;
    void                *private;
    wait_queue_func_t   func;
    struct list_head    entry;
};
  • flags:标志位(如 WQ_FLAG_EXCLUSIVE 等),控制唤醒策略。

  • private:通常指向等待的 task_struct(进程/线程描述符)。

  • func:唤醒回调函数,决定如何唤醒等待者。

  • entry:链表节点,用于挂载到 wait_queue_head 的链表上。

3. 等待队列的实现原理

3.1 等待队列的基本思想

  • 当进程需要等待某个条件时,将自身以 wait_queue_entry 的形式加入 wait_queue_head 链表,并进入睡眠状态(如 TASK_UNINTERRUPTIBLE)。

  • 其他线程或中断处理程序在条件满足时,调用唤醒函数(如 wake_up),遍历等待队列并唤醒相应的进程。

  • 支持多种唤醒策略(全部唤醒、只唤醒一个、优先级唤醒等)。

3.2 等待队列的类型

  • 普通等待队列:所有等待者平等,唤醒时可全部唤醒或唤醒一个。

  • 独占等待队列(exclusive):只有一个等待者被唤醒(如互斥锁、信号量等场景)。

  • 优先级等待队列:根据等待者优先级决定唤醒顺序。

4. 等待队列的用法

4.1 声明与初始化

4.1.1 静态声明
DECLARE_WAIT_QUEUE_HEAD(my_queue);
4.1.2 动态初始化
struct wait_queue_head my_queue;
init_waitqueue_head(&my_queue);

4.2 等待与唤醒的基本流程

4.2.1 等待(睡眠)

1. 定义等待队列项

//定义名为wait的entry
DEFINE_WAIT(wait);
//指定entry的func为function
DEFINE_WAIT_FUNC(wait, function);

struct wait_queue_entry wait;
init_waitqueue_entry(&wait, current);

 2. 加入等待队列并睡眠

add_wait_queue(&my_queue, &wait);
set_current_state(TASK_UNINTERRUPTIBLE);
schedule(); // 进入睡眠

3. 条件满足后移除等待队列

set_current_state(TASK_RUNNING);
remove_wait_queue(&my_queue, &wait);
4.2.2 唤醒
//唤醒一个等待者
wake_up(&my_queue);

//唤醒所有等待者
wake_up_all(&my_queue);

//唤醒可中断等待者
wake_up_interruptible(&my_queue);

4.3 等待队列的高级用法

4.3.1 条件等待宏

内核提供了大量基于等待队列的条件等待宏,简化常见用法。大家经常看到的wait_event_*系列就是基于wait_queue技术的一系列的宏定义。具体如下:

wait_event:进程在 condition 为假时睡眠,直到 condition 为真被唤醒。

wait_event(my_queue, condition);

wait_event_interruptible:可被信号中断的等待。

wait_event_interruptible(my_queue, condition);

wait_event_timeout:带超时的等待。

wait_event_timeout(my_queue, condition, timeout);

wait_event_interruptible_timeout:可被信号中断且带超时的等待。

wait_event_interruptible_timeout(my_queue, condition, timeout);
4.3.2 独占等待
  • add_wait_queue_exclusive
    只唤醒一个等待者,常用于互斥锁、信号量等场景。

4.3.3 优先级等待
  • add_wait_queue_priority
    按优先级插入等待队列,唤醒时优先级高的先被唤醒。

5. 内核实现细节

5.1 等待队列的链表结构

  • 每个 wait_queue_head 维护一个链表,链表节点为 wait_queue_entry。

  • 每个等待的进程/线程都以 wait_queue_entry 的形式挂在链表上。

5.2 睡眠与唤醒的流程

  1. 睡眠:进程设置自身状态为 TASK_UNINTERRUPTIBLE 或 TASK_INTERRUPTIBLE,加入等待队列,调用 schedule() 让出 CPU。

  2. 唤醒:其他线程或中断处理程序调用 wake_up/wake_up_all,遍历等待队列,调用每个 wait_queue_entry 的 func 回调,唤醒相应进程。

5.3 唤醒函数

  • default_wake_function:标准唤醒函数,将等待进程状态设置为 TASK_RUNNING。

  • autoremove_wake_function:唤醒后自动从等待队列移除。

  • 自定义唤醒函数:可通过 func 字段指定自定义唤醒逻辑。

5.4 并发与锁保护

  • 所有对 wait_queue_head 的操作都需持有其自旋锁 lock,保证多核/多线程安全。

6. 典型应用场景

6.1 设备驱动中的阻塞 I/O

驱动在无数据可读时让进程睡眠,有数据到来时唤醒:

static DECLARE_WAIT_QUEUE_HEAD(read_queue);

ssize_t mydev_read(...) {
    wait_event_interruptible(read_queue, data_ready());
    // 读取数据
}

void mydev_irq_handler(...) {
    // 数据到来
    wake_up_interruptible(&read_queue);
}

6.2 poll/epoll/select 支持

驱动的 f_op->poll 实现中,调用 poll_wait() 将当前进程加入等待队列,实现异步 I/O 支持。

6.3 进程同步与互斥

如信号量、互斥锁、条件变量等,底层都可用等待队列实现。

6.4 内存管理

如页回收、swap、OOM 等场景,进程等待内存资源可用时进入等待队列。

7.总结

  • wait_queue_head 是等待队列的头部,管理所有等待者。

  • wait_queue_entry 描述每个等待的进程/线程,支持自定义唤醒逻辑。

  • 等待队列机制高效、灵活,适合各种同步、阻塞、事件通知场景。

  • 推荐使用内核提供的 wait_event_xxx 宏,简化代码、减少错误。

  • 注意并发安全、死锁防范和唤醒时机,保证系统健壮性。

7.1 超时与中断

  • 等待队列支持超时等待和可中断等待,便于实现复杂同步逻辑。

  • 推荐使用 wait_event_xxx 宏,避免手动管理队列和状态。

7.2 唤醒优化

  • 对于只需唤醒一个等待者的场景,使用独占等待队列和 wake_up/wake_up_locked。

  • 对于所有等待者都需唤醒的场景,使用 wake_up_all。

7.3 死锁与竞态防范

  • 等待条件的判断和唤醒必须配合,避免丢失唤醒或死锁。

  • 修改影响等待条件的变量后,必须立即调用 wake_up。

7.4 与信号、定时器协作

  • 可结合信号处理和定时器,实现可中断和超时等待。

    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐