1. 课程导论
1.1 为什么需要理解阻塞与非阻塞I/O?
在嵌入式Linux驱动开发中,应用程序与硬件设备之间的数据交互方式直接决定了系统的实时性与资源利用率。阻塞(Blocking)与非阻塞(Non-Blocking)I/O是两种最基本的访问模式,也是驱动开发者必须掌握的核心概念。
核心问题:当设备没有数据可读或缓冲区已满无法写入时,驱动应该如何响应?是让应用程序“等一会儿”,还是立即返回一个错误?
1.2 阻塞I/O与非阻塞I/O对比
| 特性 | 阻塞I/O | 非阻塞I/O |
|---|---|---|
| 调用行为 | 条件不满足时,进程进入睡眠等待 | 条件不满足时,立即返回错误(如 -EAGAIN) |
| CPU利用率 | 等待时不占用CPU,资源利用率高 | 需要轮询检查,可能浪费CPU周期 |
| 编程复杂度 | 驱动实现简单,应用逻辑直观 | 驱动需处理非阻塞标志,应用需循环轮询 |
| 典型场景 | 串口读取、按键输入、数据采集 | 网络socket、高速数据管道、多路复用 |
| 应用感知 | 调用返回前一直“卡住” | 调用立即返回,应用可做其他事 |
驱动开发者视角:阻塞I/O的底层实现依赖“等待队列”机制。当设备不可用时,驱动将当前进程放入等待队列并调度出去;当设备可用时,驱动唤醒等待队列中的进程。这是Linux内核提供的高效进程调度手段。
1.3 等待队列在驱动中的核心作用
等待队列(Wait Queue)是Linux内核中用于实现阻塞I/O的基础设施。它本质上是一个“睡眠进程链表”,配合内核调度器实现进程的挂起与唤醒。
关键理解:等待队列不是“轮询”,而是“事件驱动”。进程在条件不满足时主动让出CPU,当硬件数据就绪(通常通过中断)时,驱动调用
wake_up() 唤醒进程。这种机制比轮询节省大量CPU资源。
1.4 阻塞I/O的完整生命周期
- 应用发起调用:应用程序调用
read(fd, buf, size),陷入内核。 - 驱动检查条件:驱动在
read()回调中检查设备是否有数据可读。 - 条件不满足:若数据未就绪,驱动调用
wait_event_interruptible()将当前进程加入等待队列。 - 进程睡眠:内核调度器将当前进程移出运行队列,CPU执行其他任务。
- 硬件中断:设备产生数据,触发中断。
- 中断处理:中断服务程序(ISR)接收数据,调用
wake_up()唤醒等待队列中的进程。 - 进程恢复:被唤醒的进程重新进入运行队列,获得CPU后继续执行。
- 数据拷贝:驱动将数据从内核缓冲区拷贝到用户空间,
read()返回。
1.5 非阻塞I/O的驱动处理路径
当应用以非阻塞方式打开设备(open("/dev/xxx", O_RDWR | O_NONBLOCK)),驱动必须检查 file->f_flags 中的 O_NONBLOCK 标志:
// 驱动 read() 回调中的典型处理
ssize_t my_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
// 检查是否有数据可读
if (无数据可读) {
if (filp->f_flags & O_NONBLOCK) {
// 非阻塞模式:立即返回 -EAGAIN
return -EAGAIN;
} else {
// 阻塞模式:等待数据
wait_event_interruptible(wq, 有数据可读);
}
}
// 拷贝数据到用户空间
...
}
避坑指南:非阻塞模式下返回
-EAGAIN 而不是 -EWOULDBLOCK(两者值相同,但语义上 -EAGAIN 表示“请重试”)。应用层通常配合 select() / poll() / epoll() 使用,驱动需要实现 poll() 回调。
1.6 本课程学习目标
| 目标编号 | 学习目标 | 对应能力 |
|---|---|---|
| 1 | 理解等待队列的内核数据结构与API | 掌握 wait_queue_head_t、DECLARE_WAIT_QUEUE_HEAD、wait_event 系列宏 |
| 2 | 实现阻塞I/O驱动 | 在字符设备驱动中集成等待队列,实现 read/write 的阻塞语义 |
| 3 | 实现非阻塞I/O与多路复用 | 实现 poll() 回调,配合应用层 select/epoll 使用 |
| 4 | 处理中断驱动的唤醒机制 | 在中断服务程序中安全调用 wake_up,避免竞态条件 |
| 5 | 调试与性能分析 | 使用 ftrace、procfs 观察等待队列状态,分析阻塞I/O对系统延迟的影响 |
1.7 前置知识要求
- Linux C编程基础:熟悉指针、结构体、链表操作。
- Linux内核模块开发基础:了解模块加载/卸载、
module_init/module_exit、printk调试。 - 字符设备驱动框架:理解
cdev、file_operations、register_chrdev_region等基础概念。 - 并发与同步基础:了解自旋锁、互斥锁、原子操作的基本使用场景。
- 中断处理基础:了解中断注册
request_irq、中断服务程序(ISR)的编写约束。
学习建议:如果你对字符设备驱动还不熟悉,建议先完成一个简单的“LED驱动”或“按键驱动”作为热身。本课程将在此基础上深入等待队列与阻塞I/O的实现细节。