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:1649include/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() 发送 SIGIOfs/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 处理函数。
  • 驱动侧实现:
    • .fasyncreturn 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:98samples/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_structinclude/linux/fs.h:1256-1263)保存每个打开文件在异步队列中的节点:
    • fa_file 指向对应 struct filefa_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 保证读侧无阻塞。
  • 一致性保证:
    • filp->f_flagsFASYNC 位必须与“是否在队列上”一致(见注释: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)。

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.cfs/ocfs2/file.cfs/9p/vfs_addr.c)。
  • 字符设备现实做法:通常不直接实现 AIO,而是通过工作队列/软中断在后台产生数据,配合 .poll/.fasync 向用户态报告就绪。
  • 迭代式读写:read_iter/write_iterinclude/linux/fs.h:1646-1647)用于高效分散/聚集 I/O,可与后台生产配合提升吞吐。

实战案例:samples/async_demo(驱动与用户态)

  • 驱动数据结构:
    • 等待队列 wait_queue_head_t wqsamples/async_demo/async_demo.c:26-27);就绪位 data_ready 与缓冲 buf/lensamples/async_demo/async_demo.c:28-36)。
    • 异步队列 struct fasync_struct *async_queuesamples/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_SETOWNO_ASYNC → 注册 SIGIO 处理器 → 在信号处理函数中轻量读取;适合被动通知。

扩展与参考

  • AIO:在 4.4 中主要面向文件系统/块设备;字符设备可通过后台生产 + .poll/.fasync 达到“异步使用效果”。
  • 代码参考:
    • include/linux/fs.h:1641-1673file_operations,含 .poll/.fasync/read_iter/write_iter)。
    • fs/fcntl.c:686fasync_helper)与 fs/fcntl.c:723kill_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 用户态生态深度兼容。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐