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);
等待队列核心数据结构关系
3.2 阻塞流程:从 read() 到 schedule()
当设备无数据时,驱动中的 read() 回调函数执行以下标准步骤,将当前进程置入睡眠:
- 声明等待队列条目:使用
DECLARE_WAITQUEUE(wait, current);创建节点,关联当前进程。 - 设置进程状态:
set_current_state(TASK_INTERRUPTIBLE);标记进程为可中断睡眠。 - 加入等待队列:
add_wait_queue(&dev->wq, &wait);将节点挂入队列。 - 检查条件并调度:如果数据仍未就绪,调用
schedule()让出 CPU。 - 被唤醒后处理:从
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) |
只唤醒指定数量的进程 | 资源有限,避免惊群 |
阻塞与唤醒完整流程
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() |