引言
Linux应用程序调用 read 系统调用时,由于内核未将数据准备好,造成应用程序阻塞,进入睡眠状态;当内核将数据准备好时,又会唤醒进程处理数据。
该流程的背后运行了什么操作呢?
在这之中,有一个关键角色,那就是内核的等待队列(Wait Queue)。
Wait Queue 简介
Linux内核提供了多种方式可以在不同场景或者目的下处理进程睡眠和唤醒。等待队列(Wait Queue)就是其中一种。
等待队列允许一个或多个进程等待某个条件成立,然后在条件满足时被唤醒,例如在设备驱动程序中等待设备状态的改变。
实际案例
gpio-input 中断驱动的实现。
- 应用程序打开 /dev/gpio-input 设备,调用 read 读取
- 驱动中 read 调用 wait_event_interruptible 阻塞,并让出 CPU
- 当 gpio 中断产生时,isr 调用 wake_up_interruptible 唤醒进程
- 驱动 read 继续执行,并返回到用户空间
- 应用程序 read 完成返回
内部原理
wait_event_interruptible
wait_event_interruptible() 调用 set_current_state(TASK_INTERRUPTIBLE) 将进程设置为可中断睡眠状态,并将当前进程添加到 wait->task_list,接着执行 schedule() 让出 CPU
c
#define __wait_event_interruptible(wq_head, condition) \
___wait_event(wq_head, condition, TASK_INTERRUPTIBLE, 0, 0, \
schedule())
condition
wait_event_interruptible() 内部是个死循环,如果 condition 不满足,即使被唤醒,也会重新执行睡眠
c
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
struct wait_queue_entry __wq_entry; \
long __ret = ret; /* explicit shadow */ \
\
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
for (;;) { \
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
\
if (condition) \
break; \
\
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
cmd; \
} \
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
})
wake_up_interruptible
wake_up_interruptible() 遍历 wait->task_list 链表,将进程状态设置为 TASK_RUNNING
c
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr_exclusive, void *key)
{
unsigned long flags;
spin_lock_irqsave(&q->lock, flags); // 加锁保护
__wake_up_common(q, mode, nr_exclusive, 0, key); // 核心唤醒
spin_unlock_irqrestore(&q->lock, flags); // 解锁
}
c
┌─────────────────────────────────────────────────────────────────┐
│ wake_up_interruptible │
├─────────────────────────────────────────────────────────────────┤
│ 1. spin_lock_irqsave(&q->lock, flags) │
│ └── 禁用本地中断,获取等待队列锁 │
│ │
│ 2. __wake_up_common() │
│ └── 遍历 task_list 链表 │
│ └── 对每个 wait_queue_entry: │
│ ├── 检查进程状态是否为 TASK_INTERRUPTIBLE │
│ ├── task->__state = TASK_RUNNING ← 关键! │
│ └── try_to_wake_up(task) │
│ └── 将进程加入 CPU 就绪队列(runqueue) │
│ │
│ 3. spin_unlock_irqrestore(&q->lock, flags) │
│ └── 释放锁,恢复中断 │
└─────────────────────────────────────────────────────────────────┘
↓
▼
┌─────────────────────────────────────────────────────────────────┐
│ 调度器 (scheduler) │
├─────────────────────────────────────────────────────────────────┤
│ - 下一次调度时,pick_next_task() 选择被唤醒的进程 │
│ - context_switch() 切换到该进程执行 │
└─────────────────────────────────────────────────────────────────┘
代码实现流程
1、初始化一个等待队列头
c
init_waitqueue_head(&wait);
2、将进程休眠
将进程添加到等待队列,并给定一个 confition 条件变量,内部是个死循环,confition 不满足时执行 schedule(),进程睡眠;被唤醒后,继续检查 confition,满足时跳出 wait_event_interruptible
c
condition = 0;
wait_event_interruptible(wait, condition);
3、唤醒等待队列中的进程
a) condition 置为1,目的是让 wait_event_interruptible() 跳出死循环,不然会再次触发进程睡眠;
b) 遍历等待队列,唤醒进程。
c
condition = 1;
wake_up_interruptible(wait);
特殊情况分析
只赋值 condition 为 1,不执行 wake_up_interruptible(wait);
==》进程得不到调度
只执行 wake_up_interruptible(wait); 不赋值 condition 为 1
==》进程被唤醒后立马又执行睡眠,实际上啥事也没干
所以,condition 赋值 为 1 和 调用 wake_up_interruptible(wait); 两者需同时满足,进程才会被唤醒