Linux 设备驱动中的阻塞与非阻塞 I/O:机制、源码与示例
1. 基本概念与区别
- 阻塞 I/O:当资源不可用时,调用方进入睡眠(
TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE),直到条件满足或被信号打断后返回。 - 非阻塞 I/O:当资源不可用时,系统调用立即返回(通常
-EAGAIN),不发生睡眠,由用户态自行决定重试或走多路复用。 - 关键差异:
- 进程状态:阻塞路径发生
TASK_RUNNING → TASK_* 的转换;非阻塞路径保持运行态。 - 系统调用行为:阻塞路径等待条件成立;非阻塞路径迅速返回错误码。
2. 底层实现机制
等待队列 (wait_queue)
- 类型与初始化:
wait_queue_head_t 与 init_waitqueue_head()(include/linux/wait.h:39-79)。 - 睡眠与唤醒:
wait_event*() 系列宏执行入队、设置任务睡眠态与调度;wake_up*() 负责唤醒等待者(include/linux/wait.h:165-175, 399-425)。 - 内核实现:入队/出队与通用唤醒逻辑在
kernel/sched/wait.c:14-22, 65-77, 89-98, 171-183, 199-220。
file_operations 中的相关方法
read/write 的 O_NONBLOCK:驱动读取 file->f_flags & O_NONBLOCK 决定是否走非阻塞路径(drivers/char/random.c:1456-1460,drivers/char/rtc.c:370-377)。poll/select/epoll:驱动 .poll 必须调用 poll_wait(file, &wait_queue, wait) 将等待队列登记到调用方,依据资源状态返回掩码(include/linux/poll.h:42-46)。
调度器与进程状态转换
- 阻塞路径设置
TASK_INTERRUPTIBLE/UNINTERRUPTIBLE 并调度;唤醒后恢复运行。相关实现与内存序保证见 kernel/sched/wait.c:159-170, 171-183。
3. 驱动开发实践
阻塞模式实现步骤
- 初始化等待队列头:
init_waitqueue_head(&wq)(示例:drivers/char/example_blocking.c:103-105)。 - 检查资源并判断条件(示例:
drivers/char/example_blocking.c:29-40)。 - 睡眠等待:
wait_event_interruptible(wq, condition),被信号打断返回 -ERESTARTSYS(示例:drivers/char/example_blocking.c:38-40,参照 drivers/char/random.c:1448-1453)。 - 唤醒机制:资源状态变化后调用
wake_up_interruptible(&wq)(示例写路径:drivers/char/example_blocking.c:73-74)。
非阻塞模式实现要点
- 检查
O_NONBLOCK:file->f_flags & O_NONBLOCK(drivers/char/example_blocking.c:35-37)。 - 返回
-EAGAIN:资源不可用立即返回(drivers/char/random.c:1445-1447,drivers/char/rtc.c:370-373)。 - 即时检测逻辑:快速判断可读/可写并返回,避免睡眠和忙等。
.poll/select/epoll 实现
.poll 中调用 poll_wait(file, &wq, wait) 登记等待队列(drivers/char/example_blocking.c:80-81)。- 返回掩码:可读返回
POLLIN|POLLRDNORM,可写返回 POLLOUT|POLLWRNORM(示例:drivers/char/example_blocking.c:82-87;参考 drivers/char/random.c:1492-1496 与 drivers/char/rtc.c:800-809)。
4. 性能与适用场景分析
- 阻塞 I/O:
- 优点:避免忙等,CPU 利用率高;适合低吞吐但需要正确同步的场景。
- 缺点:响应受资源就绪时间影响,存在上下文切换。
- 非阻塞 I/O:
- 优点:适合实时与事件驱动;配合
poll/select/epoll 多路复用。 - 缺点:用户态需编排重试与回退,不当实现可能忙等。
- 混合策略:同时提供阻塞与非阻塞路径,并实现
.poll;用户空间以 select/poll/epoll 统一管理,参照 /dev/random(drivers/char/random.c:1425-1460, 1484-1497, 1591-1599)。
5. 用户空间接口
- 设置非阻塞:
open(path, O_NONBLOCK) 或 fcntl(fd, F_SETFL, O_NONBLOCK)。 - 行为表现:
- 阻塞读:无数据时睡眠;收到信号返回
-ERESTARTSYS(drivers/char/random.c:1451-1453,drivers/char/rtc.c:374-377)。 - 非阻塞读:无数据立刻返回
-EAGAIN。 poll/select/epoll:驱动 .poll 返回掩码,内核登记等待队列并在事件到来时唤醒调用方(include/linux/poll.h:42-46)。
6. 调试与问题排查
- 常见死锁:持有不可睡眠锁(自旋锁)期间调用睡眠原语;应在睡眠前释放锁或使用合适锁策略。示例代码采用
mutex,在睡眠前释放(drivers/char/example_blocking.c:33-40)。 - 竞态防护:在持锁下修改资源状态并确保等待/唤醒条件一致,之后再唤醒;等待宏内部已有必要的内存序(
kernel/sched/wait.c:86-96)。 - 核查要点:
.poll 必须调用 poll_wait;错误码(-EAGAIN/-ERESTARTSYS)符合约定;唤醒路径触发预期事件掩码。
7. 源码对照与示例
/dev/random
- 阻塞读与
O_NONBLOCK:drivers/char/random.c:1425-1460(非阻塞 -EAGAIN:1445-1447;信号中断:1451-1453)。 .poll:drivers/char/random.c:1484-1497。file_operations:drivers/char/random.c:1591-1599。
/dev/rtc
.read 阻塞与非阻塞:drivers/char/rtc.c:329-394。.poll:drivers/char/rtc.c:793-809。
/dev/mem
- 直接读写物理内存,无等待队列与睡眠:
drivers/char/mem.c:102-173, 174-243。
等待队列与 poll API
wait_event_interruptible 宏:include/linux/wait.h:399-425。wake_up_interruptible 宏:include/linux/wait.h:171-175。poll_wait:include/linux/poll.h:42-46。- 核心实现:
kernel/sched/wait.c:14-22, 65-77, 89-98, 171-183, 199-220。
8. 典型驱动示例(已集成)
- 文件:
drivers/char/example_blocking.c - 关键路径:
- 阻塞/非阻塞读:
drivers/char/example_blocking.c:22-51 - 写入与唤醒:
drivers/char/example_blocking.c:53-75 .poll:drivers/char/example_blocking.c:77-88- 等待队列初始化与设备注册:
drivers/char/example_blocking.c:103-123
9. 构建与启用
- 配置项:
CONFIG_EXAMPLE_BLOCKING(drivers/char/Kconfig)。 - Makefile 目标:
drivers/char/Makefile:63。 - 设备节点:启用
devtmpfs/udev 自动生成 /dev/exblk;或手动创建:
mknod /dev/exblk c $(grep exblk /proc/devices | awk '{print $1}') 0
10. 用户态测试程序
- 文件:
samples/exblk/exblk_test.c - 编译:
cc -O2 -Wall -o exblk_test samples/exblk/exblk_test.c
- 运行与验证:
- 非阻塞读:程序启动即测试
O_NONBLOCK 路径(预期 EAGAIN)。 poll/epoll:在另一终端执行 echo "hello" > /dev/exblk 触发事件;程序读取并打印内容。- 阻塞读:提示后执行写入,可观察阻塞到事件到来。