【Linux驱动开发】Linux 设备驱动中的异步通知与异步 I/O:原理、机制与实战
本文探讨Linux设备驱动中的异步通知与I/O机制,分析其原理、实现方法与适用场景。主要内容包括:1)通过wait_queue与.poll实现轮询式就绪检测;2)利用.fasync与kill_fasync()实现SIGIO信号驱动I/O;3)结合定时器/工作队列实现异步数据生产。文章详细解析了fasync内部数据结构(RCU链表)、poll/epoll内核路径及边沿触发语义,并以async_dem
·
Linux 设备驱动中的异步通知与异步 I/O:原理、机制与实战
概览
- 目标:在字符/块设备驱动中实现“事件就绪后再通知用户态”的能力,支持非阻塞读写、poll/select/epoll 以及信号驱动 I/O(SIGIO)。
- 方法:结合
wait_queue+.poll返回掩码实现可轮询的就绪检测;使用.fasync+kill_fasync()实现 SIGIO 异步通知;在需要内核侧异步生产数据时,配合定时器、软中断或工作队列。 - 适用范围:普适字符设备;块/文件系统可扩展到 AIO(
kiocb/read_iter/write_iter)。本树为 4.4.94,不包含 io_uring。
关键内核接口
file_operations成员:.poll与.fasync是异步通知与可等待的核心接口(include/linux/fs.h:1649、include/linux/fs.h:1658)。完整原型见include/linux/fs.h:1641-1673。- 等待队列与登记:驱动在
.poll中调用poll_wait(file, &wait_queue, wait)将队列登记到调用方(引用:Documentation/driver-model/blocking-nonblocking-io.md:18)。 - 信号驱动 I/O:驱动侧在
.fasync中调用fasync_helper()安装/移除异步队列(fs/fcntl.c:686),在事件到来时调用kill_fasync()发送SIGIO(fs/fcntl.c:723)。 - 常用同步原语:
wait_event_interruptible()在阻塞读中等待条件;非阻塞读检查O_NONBLOCK并返回-EAGAIN;多核可见性可用smp_store_release/smp_load_acquire确保就绪位有序。
异步通知(SIGIO/fasync)机制
- 用户态开启步骤:
fcntl(fd, F_SETOWN, getpid())设置信号接收者;fcntl(fd, F_SETFL, flags | O_ASYNC)开启SIGIO驱动;- 接收端注册
SIGIO处理函数。
- 驱动侧实现:
.fasync:return fasync_helper(fd, filp, on, &dev->async_queue);(示例:samples/async_demo/async_demo.c:118-122)。- 事件到来:
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);通知读取就绪(示例:samples/async_demo/async_demo.c:98、samples/async_demo/async_demo.c:159)。 .release:关闭时调用fasync_helper(-1, filp, 0, &dev->async_queue);清理(示例:samples/async_demo/async_demo.c:130-135)。
- 适用与取舍:
SIGIO适合事件频率不高、希望“被动接收”的场景;若事件密集,优先使用 epoll,避免信号风暴。
fasync 内部工作原理(链表、RCU、锁)
- 数据结构:
struct fasync_struct(include/linux/fs.h:1256-1263)保存每个打开文件在异步队列中的节点:fa_file指向对应struct file;fa_fd记录文件描述符编号;fa_next形成单向链表;fa_lock保护节点内部字段;fa_rcu支持 RCU 读侧遍历与延迟释放;magic必须为FASYNC_MAGIC,供运行时一致性校验(include/linux/fs.h:1265)。
- 安装/更新:
fasync_helper(fd, filp, on, &dev->async_queue)(fs/fcntl.c:686-691)on==0时调用fasync_remove_entry()(fs/fcntl.c:575-599)从链表移除并清除filp->f_flags & ~FASYNC;使用call_rcu()异步释放节点。on!=0时调用fasync_add_entry()(fs/fcntl.c:657-678),分配新节点并插入链表(fasync_insert_entry():fs/fcntl.c:623-651)。插入时持有filp->f_lock与全局fasync_lock,并设置FASYNC标志。
- 发送信号:
kill_fasync(&dev->async_queue, SIGIO, band)(fs/fcntl.c:723-734)- 快速路径检查非空后进入 RCU 临界区,遍历链表调用
kill_fasync_rcu()(fs/fcntl.c:698-721)。 - 对每个节点读取
fa_file->f_owner,调用send_sigio(fown, fa->fa_fd, band)将信号发送到拥有者进程或线程组。 - 支持从中断上下文调用(
include/linux/fs.h:1274说明),因此路径需短、持锁时间有限,使用 RCU 保证读侧无阻塞。
- 快速路径检查非空后进入 RCU 临界区,遍历链表调用
- 一致性保证:
filp->f_flags的FASYNC位必须与“是否在队列上”一致(见注释:fs/fcntl.c:571-574,620-622),否则用户态的O_ASYNC行为与内核队列状态可能不一致。- 节点移除使用 RCU 回收,避免并发遍历时释放内存导致 UAF。
非阻塞 I/O 与 poll/select/epoll
.read两条路径:- 非阻塞:若
filp->f_flags & O_NONBLOCK且未就绪,直接返回-EAGAIN(示例:samples/async_demo/async_demo.c:47-50)。 - 阻塞:调用
wait_event_interruptible(wq, data_ready)直到条件满足(示例:samples/async_demo/async_demo.c:51-53)。
- 非阻塞:若
.poll:- 登记:
poll_wait(filp, &dev->wq, wait);(示例:samples/async_demo/async_demo.c:108-109)。 - 返回掩码:就绪时返回
POLLIN|POLLRDNORM;一般也返回POLLOUT|POLLWRNORM(示例:samples/async_demo/async_demo.c:110-115)。
- 登记:
- 用户态范式:
- 建立 epoll 并添加
EPOLLIN|EPOLLET;在触发后循环read()直至耗尽(示例:samples/async_demo/user/async_demo_test.c:59-91)。
- 建立 epoll 并添加
poll/epoll 内核路径与事件语义
poll_wait(filp, &dev->wq, wait)的作用:将dev->wq注册到调用方的等待表(poll_table),使得后续wake_up_interruptible(&dev->wq)能唤醒上层poll/select/epoll(参考:samples/async_demo/async_demo.c:108-109)。- 掩码与就绪:
- 读就绪返回
POLLIN|POLLRDNORM;写就绪返回POLLOUT|POLLWRNORM(示例:samples/async_demo/async_demo.c:110-115)。 - 掩码应与
.read实际可读取的状态严格对应,避免出现“掩码可读但读不到”的情况。通常以一个原子“就绪位”作为唯一事实来源。
- 读就绪返回
- epoll 边沿触发(
EPOLLET)语义:- 驱动侧只需保证在状态由“不可读→可读”时唤醒一次;用户态必须在触发后循环
read()到返回-EAGAIN,否则会丢事件(示例:samples/async_demo/user/async_demo_test.c:80-85)。
- 驱动侧只需保证在状态由“不可读→可读”时唤醒一次;用户态必须在触发后循环
- 与阻塞读的关系:
- 阻塞读使用
wait_event_interruptible(wq, ready),与poll_wait()共享同一wait_queue_head_t;驱动只维护一次状态与一次唤醒,两个路径自然联动。
- 阻塞读使用
内核异步 I/O(AIO)在 4.4 的适用性
- 适用对象:主要面向文件系统与块设备,接口见
aio_read()等(例如:fs/nfs/direct.c、fs/ocfs2/file.c、fs/9p/vfs_addr.c)。 - 字符设备现实做法:通常不直接实现 AIO,而是通过工作队列/软中断在后台产生数据,配合
.poll/.fasync向用户态报告就绪。 - 迭代式读写:
read_iter/write_iter(include/linux/fs.h:1646-1647)用于高效分散/聚集 I/O,可与后台生产配合提升吞吐。
实战案例:samples/async_demo(驱动与用户态)
- 驱动数据结构:
- 等待队列
wait_queue_head_t wq(samples/async_demo/async_demo.c:26-27);就绪位data_ready与缓冲buf/len(samples/async_demo/async_demo.c:28-36)。 - 异步队列
struct fasync_struct *async_queue(samples/async_demo/async_demo.c:32)。 - 生产机制:hrtimer 周期触发,底半部
workqueue生成数据(samples/async_demo/async_demo.c:142-170)。
- 等待队列
- 写入事件:用户向设备写入时置位
data_ready,唤醒等待并发信号(samples/async_demo/async_demo.c:90-101)。 - 读路径与就绪:读在阻塞/非阻塞两路正确处理并拷贝数据(
samples/async_demo/async_demo.c:41-71)。 - 用户态信号:
F_SETOWN+O_ASYNC后由kill_fasync()推送SIGIO(用户侧处理:samples/async_demo/user/async_demo_test.c:21-57)。 - 用户态轮询:epoll 等待并在触发后读取(
samples/async_demo/user/async_demo_test.c:59-91)。
实现步骤清单(字符设备)
- 初始化:
- 定义设备状态:锁、等待队列、就绪位与缓冲;可选定时器/工作队列。
cdev/class/device注册并暴露节点。
.read:- 非阻塞:若未就绪返回
-EAGAIN;阻塞:wait_event_interruptible()等待。 - 成功后清除就绪位,返回已拷贝字节数。
- 非阻塞:若未就绪返回
.write:- 拷贝用户数据后置位就绪,
wake_up_interruptible(&wq)与kill_fasync()通知。
- 拷贝用户数据后置位就绪,
.poll:- 调用
poll_wait()登记队列;依据就绪返回POLLIN|POLLRDNORM,写侧返回POLLOUT|POLLWRNORM。
- 调用
.fasync:- 使用
fasync_helper()维护async_queue,在.release中关闭。
- 使用
- 事件生产:
- 中断、定时器或工作队列中生成数据;修改状态后唤醒队列并发信号。
- 清理:
- 退出路径取消定时器、同步取消工作、释放缓冲并注销字符设备。
常见坑与最佳实践
- 并发与可见性:就绪位涉及多核并发时使用
smp_store_release/smp_load_acquire(示例:samples/async_demo/async_demo.c:47-56,94-97)。 - 掩码一致性:
.poll返回的掩码要与实际.read就绪语义一致,否则会出现“可读但读不到”的假阳性。 - fasync 清理:在
.release中调用fasync_helper(-1, filp, 0, ...),防止遗留异步队列导致野指针(示例:samples/async_demo/async_demo.c:130-135)。 - 信号风暴:高频事件优先使用 epoll;如需信号,考虑节流或批量聚合通知。
- epoll 边沿触发:
EPOLLET下必须循环读取直至返回-EAGAIN,避免丢事件(示例:samples/async_demo/user/async_demo_test.c:80-85)。
阻塞/非阻塞读的内存可见性与顺序保证
- 就绪位写入侧:使用
smp_store_release(&ready, 1)保证就绪位在写入后对其他 CPU 可见,并且在此之前的缓冲数据写入对读侧有序可见(示例:samples/async_demo/async_demo.c:94-97,151-156)。 - 读侧检查:使用
smp_load_acquire(&ready)保证在看到就绪位为 1 后,随后的读取可以看到完整数据(示例:samples/async_demo/async_demo.c:47-56,110-111)。 - 这样替代粗粒度锁可减少临界区,并发更稳健;配合
mutex保护缓冲指针与长度的具体更新,实现锁与原子原语的合理分工。
用户态用法范式(摘要)
- epoll:创建 epoll → 添加
EPOLLIN→ 等待epoll_wait()→ 在触发后读取;适合高吞吐与高频事件。 - SIGIO:设置
F_SETOWN与O_ASYNC→ 注册SIGIO处理器 → 在信号处理函数中轻量读取;适合被动通知。
扩展与参考
- AIO:在 4.4 中主要面向文件系统/块设备;字符设备可通过后台生产 +
.poll/.fasync达到“异步使用效果”。 - 代码参考:
include/linux/fs.h:1641-1673(file_operations,含.poll/.fasync/read_iter/write_iter)。fs/fcntl.c:686(fasync_helper)与fs/fcntl.c:723(kill_fasync)。- fasync 内部:
include/linux/fs.h:1256-1266(结构体定义)、fs/fcntl.c:575-691(队列操作与一致性)、fs/fcntl.c:698-734(信号发送)。 - 示例驱动:
samples/async_demo/async_demo.c:41-170,198-207;用户态示例:samples/async_demo/user/async_demo_test.c:21-121。
结论
- 在字符设备层实现异步通知与异步 I/O的“实用组合”是:
.read支持阻塞/非阻塞,.poll提供精确就绪掩码,.fasync+kill_fasync()提供信号通知,后台用工作队列/定时器/中断产生数据。这套机制简单、稳健、与 epoll/SIGIO 用户态生态深度兼容。
更多推荐

所有评论(0)