让进程在条件不满足时 “休眠等待”,条件满足后被 “唤醒继续运行”。
典型场景:
- 读数据时没有数据 → 休眠
- 设备没准备好 → 休眠
- 中断来了 → 唤醒
- 资源可用了 → 唤醒
它是 Linux 内核进程同步 / 阻塞最基础、最常用的机制。
等待队列只有两个核心结构体:
1. 等待队列头wait_queue_head_t
- 相当于 “队列管理器”
- 管理一整个等待的进程列表
- 你必须先初始化它
2. 等待队列项wait_queue_entry_t
- 相当于 “正在等待的那个进程”
- 每个要休眠的进程,都要创建一个队列项
- 挂到队列头里排队等待
等待队列本质
等待队列 =链表 + 进程休眠机制 + 唤醒回调
作用:让进程主动放弃 CPU 进入休眠,直到某个事件发生,再被重新唤醒并调度。
它依赖三个内核基础:
task_struct进程描述符- 进程状态:
TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE - 调度器:
schedule()
核心数据结构
1. 等待队列头
struct wait_queue_head { spinlock_t lock; struct list_head head; };- 一个双向链表,挂所有等待的进程
- 自旋锁保护链表并发操作
2. 等待队列项
struct wait_queue_entry { unsigned int flags; void *private; // 一般 = current wait_queue_func_t func; // 唤醒函数 struct list_head entry; // 链入队列 };private:指向要休眠的进程task_structfunc:唤醒时调用的函数,默认是default_wake_function- 常用
autoremove_wake_function(唤醒后自动摘链)
最常用 API
定义 + 初始化队列头
// 静态定义+初始化(全局/静态变量最常用) DECLARE_WAIT_QUEUE_HEAD(my_wq); // 或者动态初始化(先定义,再运行时初始化) wait_queue_head_t my_wq; init_waitqueue_head(&my_wq);进程休眠等待
1. 可中断休眠(推荐 99% 场景)
wait_event_interruptible(wq, condition);- 休眠直到condition 为真
- 可以被信号唤醒
- 返回 0:正常唤醒;返回 -ERESTARTSYS:被信号打断
2. 不可中断休眠
wait_event(wq, condition);- 不响应信号
- 常用于 IO、磁盘操作
3. 带超时(常用)
wait_event_timeout(wq, condition, timeout);- 时间到了自动醒
- 返回值:0 超时,>0 剩余时间
4. 可中断 + 超时
wait_event_interruptible_timeout(wq, condition, timeout);唤醒等待的进程
1. 唤醒队列上所有等待进程(最常用)
wake_up(&wq);2. 唤醒可中断的等待
wake_up_interruptible(&wq);3. 只唤醒一个等待进程
wake_up_one(&wq);wake_up_one是 Linux 等待队列里只唤醒队列中一个等待进程的 API,专门用来解决惊群效应(thundering herd)。
核心作用
wake_up:唤醒所有等待在该队列上的进程wake_up_one:只唤醒一个等待进程
适用场景:
- 资源一次只允许一个进程使用(如单个缓冲区、单个硬件资源)
- 不想唤醒一堆进程抢资源,减少调度开销
内部原理:
- 加自旋锁保护等待队列
- 遍历等待队列链表
- 找到第一个非独占 / 合适的等待项
- 调用其唤醒函数,把进程设为
TASK_RUNNING - 立即停止遍历,只唤醒这一个
- 解锁返回
关键要点:
- 唤醒的是队列里最早等待的那一个(FIFO 顺序)
- 不保证唤醒哪个,只保证只唤醒一个
- 仍然要配合条件判断,防止虚假唤醒
- 进程如果是
EXCLUSIVE等待,也会被正确识别
4. 中断上下文里唤醒(必须用这个)
wake_up_interruptible(&wq);等待队列项 (底层手动控制)
1. 定义一个等待项(绑定当前进程)
DEFINE_WAIT(wait);2. 初始化等待项
init_wait(&wait);3. 加入等待队列
add_wait_queue(&wq, &wait);4. 从队列移除
remove_wait_queue(&wq, &wait);核心原理流程
进程休眠的底层逻辑(wait_event 内部做了什么)
简化伪代码:
while (!condition) { // 1. 设置进程状态为可中断休眠 set_current_state(TASK_INTERRUPTIBLE); // 2. 把当前进程包装成 wait_queue_entry,加入等待队列 add_wait_queue(&wq_head, &wait); // 3. 主动让出 CPU,调度器切换其他进程 schedule(); // 4. 被唤醒后回到这里,先摘链/清理,再重新检查条件 } // 5. 条件满足,恢复为运行态 set_current_state(TASK_RUNNING); remove_wait_queue(&wq_head, &wait);关键点:
- schedule () 一调用,当前进程立刻休眠
- 进程不再被调度,直到被唤醒
- 醒来后必须重新检查条件,防止惊群效应
唤醒的底层逻辑(wake_up 做了什么)
spin_lock(&wq_head.lock); // 遍历整个等待队列链表 list_for_each_entry_safe(wait, next, &wq_head.head, entry) { // 调用唤醒函数:把进程状态设回 TASK_RUNNING wait->func(wait, TASK_NORMAL, 0, NULL); } spin_unlock(&wq_head.lock);唤醒函数default_wake_function做两件事:
private取出task_structtry_to_wake_up(p)把进程状态改为TASK_RUNNING- 把进程扔回运行队列,等待下一次调度
为什么要 “循环检查条件”?(核心原理)
两个原因:
惊群效应(thundering herd),wake_up 会唤醒所有等待进程,但只有一个能抢到资源,其他必须继续睡。
虚假唤醒,进程可能被信号、超时等意外唤醒,条件未必真满足。
所以等待队列内部永远是:
while (!cond) { 休眠; }进程状态原理
TASK_RUNNING:可运行 / 正在运行TASK_INTERRUPTIBLE:可被信号唤醒的休眠TASK_UNINTERRUPTIBLE:不可被信号唤醒(常见于 IO、disk 等待)
等待队列就是把进程从 RUNNING 变成休眠态,再变回来。
工作流程
- 定义并初始化等待队列头
- 进程检查条件
- 条件不满足 → 调用 wait_event 休眠
- 其他地方(中断 / 驱动 / 内核线程)条件满足 → 调用 wake_up
- 进程被唤醒,继续执行
等待队列完整原理流程图
┌─────────────────────────────────────────────────────────────┐ │ 进程 A 开始执行 │ └───────────────────────┬─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 检查等待条件 condition 是否成立? │ └───┬─────────────────────────┬───────────────────────────────┘ │ 是 │ 否 ↓ ↓ ┌─────────────────┐ ┌───────────────────────────────────────┐ │ 条件满足,直接 │ │ 1. 初始化等待队列项 wait_entry │ │ 继续往下执行 │ │ 2. 设置进程状态: │ └─────────────────┘ │ TASK_INTERRUPTIBLE / UNINTERRUPTIBLE│ └─────────────┬───────────────────────────┘ ↓ ┌───────────────────────────────────────────┐ │ 将当前进程加入等待队列链表 │ │ add_wait_queue(&wq_head, &wait) │ └───────────────┬───────────────────────────┘ ↓ ┌───────────────────────────────────────────┐ │ 主动调用 schedule() 让出 CPU │ │ ↓ 进程进入休眠,不再被调度 │ └───────────────┬───────────────────────────┘ │ ┌────────────────────┴────────────────────┐ │ │ ↓ ↓ ┌────────────────────┐ ┌─────────────────────────────┐ │ 进程 A 处于休眠 │ │ 其他进程/中断/内核线程 B │ │ 不占用 CPU │ │ 执行并触发事件: │ │ │ │ 1. 条件 condition = 1 │ └────────────────────┘ │ 2. 调用 wake_up(&wq_head) │ └──────────┬────────────────┘ │ ┌────────────────────────┴──────────────────┐ │ wake_up 内部流程: │ │ 1. 加自旋锁保护队列 │ │ 2. 遍历等待队列链表 │ │ 3. 对每个等待项调用唤醒函数 func() │ │ → try_to_wake_up(p) │ │ → 进程状态设为 TASK_RUNNING │ │ → 加入运行队列,等待调度器调度 │ │ 4. 解锁 │ └───────────────┬───────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────┐ │ 调度器下次选中进程 A 恢复运行 │ └───────────────┬─────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 进程 A 从 schedule() 返回,继续执行 wait_event 内部循环 │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 1. 将进程状态恢复为 TASK_RUNNING │ │ 2. 将自己从等待队列中移除 remove_wait_queue │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 再次检查 condition 是否成立 │ └───────────┬───────────────────────────────────┬─────────────┘ │ 是 │ 否 ↓ ↓ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │ 退出循环,条件满足 │ │ 重新入队 → 再次休眠 → 等待唤醒 │ │ 继续执行业务逻辑 │ └─────────────────────────────────┘ └─────────────────────────┘完整可运行内核示例
#include <linux/module.h> #include <linux/wait.h> #include <linux/delay.h> #include <linux/kthread.h> // 1. 定义等待队列头 DECLARE_WAIT_QUEUE_HEAD(my_wq); // 条件标志 int cond = 0; // 等待线程:会休眠直到 cond=1 static int wait_thread(void *arg) { printk("等待线程:开始等待...\n"); // 2. 休眠,直到 cond == 1 wait_event_interruptible(my_wq, cond == 1); printk("等待线程:被唤醒啦!条件满足\n"); return 0; } // 唤醒线程:2秒后设置条件并唤醒 static int wake_thread(void *arg) { msleep(2000); // 延时2秒 cond = 1; // 3. 条件满足 wake_up(&my_wq);// 4. 唤醒等待队列 printk("唤醒线程:已发送唤醒\n"); return 0; } static int __init test_init(void) { kthread_run(wait_thread, NULL, "wait_thread"); kthread_run(wake_thread, NULL, "wake_thread"); return 0; } static void __exit test_exit(void) { printk("模块退出\n"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL");关键要点
- 队列头 = 管理者,队列项 = 等待者
- 不初始化队列头 → 内核崩溃
wait_event会自动阻塞 + 自动检查条件,不用自己写循环wake_up只是唤醒,真正醒来后会再检查一次条件- 中断里唤醒必须用:
wake_up_interruptible() - 等待队列:进程主动休眠 + 显式唤醒
等待队列未初始化使用= 内核必崩
直接内核崩溃(panic / OOPS)
进程直接死掉,系统卡死 / 重启。
等待队列头wait_queue_head_t内部有两个关键成员:
struct wait_queue_head { spinlock_t lock; // 自旋锁 struct list_head head; // 链表 };未初始化 = 这两个成员都是随机值 / 0
- lock 未初始化
- 锁状态非法
- 加锁 / 解锁直接触发内核 BUG
- head 链表未初始化
- 链表指针是野指针
- 执行
add_wait_queue时直接非法访问内存
只要你调用:
wait_eventwake_upadd_wait_queue
立刻内核崩溃!
总结
等待队列 =睡觉 + 叫醒
wait_event:睡觉wake_up:叫醒wait_queue_head_t:睡觉的房间- 等待 = 入队列 + 设休眠 + 让出 CPU
- 唤醒 = 遍历队列 + 改状态为 RUNNING + 扔回运行队列
- 醒来第一件事:重新检查条件,防止虚假唤醒 / 惊群
- 整个机制完全基于进程状态 + 链表 + 调度器实现,没有黑魔法