字符设备准备好了数据,应用又不想一直阻塞在 read() 上,可以让驱动主动发一条通知吗?fasync 与 SIGIO 就是 Linux 提供的一条老而直接的路径。它传递的是“文件描述符状态可能变化了”,不是数据本身。应用收到信号后,仍要回到正常的 read() 或 write() 路径确认状态。
这种方式适合低频、简单的设备事件,例如按键、少量传感器数据或实验性字符驱动。连接多、事件密集或需要精确排队时,poll/epoll、eventfd 或专用消息队列通常更容易控制。
订阅是怎样建立的
用户空间先用 F_SETOWN 指定由谁接收 I/O 信号,再通过 F_SETFL 打开 O_ASYNC。VFS 随后调用驱动 file_operations.fasync 回调。驱动一般把工作交给 fasync_helper(),由它维护与该文件关联的 struct fasync_struct 订阅链。
驱动侧的骨架并不长,难点在对象生命周期和并发:
1 | static struct fasync_struct *async_queue; |
关闭文件时必须把订阅者移出队列。设备解绑、错误恢复和模块卸载也要先阻止新事件,再同步仍在运行的中断或工作项,最后释放队列依赖的对象。否则 kill_fasync() 可能碰到已经失效的状态。
用户空间不要在信号里读完所有数据
信号处理函数只能调用异步信号安全函数。更稳妥的做法是让处理器只设置 sig_atomic_t 标志,或向 self-pipe 写一个字节,再在主循环里读取设备直到 EAGAIN。
1 | static volatile sig_atomic_t io_ready; |
标准信号通常不会为每次设备事件各保存一个排队实例。两个事件在应用处理前连续到来,可能只观察到一次 SIGIO。所以正确语义应是“有工作可做”,收到通知后把当前可读数据排空,而不是把信号次数当成事件数。
容易踩的坑
- 先发信号、后写入设备缓冲区,应用醒来却读不到数据。
- 在处理器里调用
printf、分配内存或取得普通互斥锁。 - 把
SIGIO次数当作无损计数,忽略信号合并。 - 只实现
fasync,却没有让read()、poll()和非阻塞语义保持一致。 - 多个打开者共享一条设备队列,却没有明确每个文件实例是否都应收到通知。
证据边界
本文描述字符设备常见实现,不保证每种文件类型都支持 O_ASYNC。信号投递时机、所有者语义和可用 band 还受具体驱动与内核版本影响。要验证“没有丢数据”,应给设备事件编号并比较生产与消费序列,不能只统计收到多少次 SIGIO。
参考:fcntl(2) · signal-safety(7) · Linux Driver Basics · 吃透内核 fasync 机制,弄懂信号驱动异步通知