9.1 异步通知的概念与作用
"异步通知"指设备在数据可用或状态改变时主动通知应用程序 ,而不是应用程序反复轮询或阻塞等待。Linux 用户空间的标准异步通知机制是 信号(signal)+ SIGIO。
| 机制 | 触发方 | 用户侧接口 | 驱动侧接口 |
|---|---|---|---|
| 阻塞 I/O | 应用主动等待 | read() 阻塞 |
等待队列 |
| 轮询 | 应用主动检查 | poll()/epoll |
fops->poll |
| 异步通知 | 设备主动通知 | SIGIO/fcntl |
fops->fasync |
| 异步 I/O | 应用提交请求 | io_submit/io_uring |
read_iter/write_iter/uring_cmd |
异步通知的完整链路:
bash
用户: fcntl(fd, F_SETOWN, getpid()); /* 设置属主 */
fcntl(fd, F_SETFL, O_ASYNC); /* 开启异步 */
signal(SIGIO, handler); /* 注册处理函数 */
驱动: 数据到达 -> kill_fasync(&dev->async_queue, SIGIO, POLL_IN)
内核: 发送 SIGIO 给属主进程 -> 调用 handler
9.2 Linux 异步通知编程
9.2.1 Linux 信号
驱动侧涉及 SIGIO(默认 29 号信号,include/uapi/asm-generic/signal.h),常配合 POLL_IN/POLL_OUT 传递原因。信号常量:
| 信号 | 用途 |
|---|---|
SIGIO |
异步 I/O 通知(最常用) |
SIGURG |
带外数据(TCP) |
SIGRTMIN+n |
实时信号,可携带数据 |
为什么用 SIGIO :它与 fcntl(F_SETOWN)/O_ASYNC 配合是 POSIX 异步 I/O 的标准路径,且内核 fasync 机制直接支持。
9.2.2 信号的接收(用户空间)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
static volatile sig_atomic_t got_sigio = 0;
static int fd;
static void sigio_handler(int signo)
{
char buf[64];
int n;
if (signo != SIGIO)
return;
/* 注意:信号处理函数中只能用 async-signal-safe 函数 */
n = read(fd, buf, sizeof(buf));
if (n > 0)
write(STDOUT_FILENO, buf, n);
got_sigio = 1;
}
int main(void)
{
struct sigaction sa = {0};
fd = open("/dev/globalfifo", O_RDWR | O_NONBLOCK);
if (fd < 0) { perror("open"); return 1; }
sa.sa_handler = sigio_handler;
sa.sa_flags = SA_RESTART;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGIO, &sa, NULL) < 0) { perror("sigaction"); return 1; }
if (fcntl(fd, F_SETOWN, getpid()) < 0) { perror("F_SETOWN"); return 1; }
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC); /* 开启异步通知 */
printf("waiting for SIGIO (pid=%d)...\n", getpid());
while (!got_sigio)
pause(); /* 或 sigsuspend() */
close(fd);
return 0;
}
要点:
F_SETOWN设置接收信号的进程/进程组(负数表示进程组)。- 7.2.5 中推荐用
F_SETOWN_EX指定F_OWNER_TID/F_OWNER_PID/F_OWNER_PGRP,可精确到线程。 - 处理函数中只能调用 async-signal-safe 函数(
read/write是安全的,printf/malloc不是)。 - 用
volatile sig_atomic_t或sig_atomic_t传递标志。
9.2.3 信号的释放(驱动侧)
驱动需要实现 fops->fasync 并在事件发生时调用 kill_fasync()。
cpp
/* include/linux/fs.h */
struct fasync_struct {
rwlock_t fa_lock;
int magic;
int fa_fd;
struct fasync_struct *fa_next;
struct file *fa_file;
struct rcu_head fa_rcu;
};
extern int fasync_helper(int, struct file *, int, struct fasync_struct **);
extern void kill_fasync(struct fasync_struct **, int, int);
驱动实现:
cpp
struct globalfifo_dev {
...
struct fasync_struct *async_queue; /* 异步通知队列 */
};
static int globalfifo_fasync(int fd, struct file *filp, int mode)
{
struct globalfifo_dev *dev = filp->private_data;
return fasync_helper(fd, filp, mode, &dev->async_queue);
}
/* 在写入数据、状态变化时调用 */
static void globalfifo_notify(struct globalfifo_dev *dev)
{
if (dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}
static const struct file_operations globalfifo_fops = {
.owner = THIS_MODULE,
.open = globalfifo_open,
.read = globalfifo_read,
.write = globalfifo_write,
.poll = globalfifo_poll,
.fasync = globalfifo_fasync, /* 必须注册 */
};
fasync_helper() 的 mode 语义:
| mode | 行为 |
|---|---|
非 0(如 FASYNC) |
把 filp 加入队列 |
| 0 | 从队列移除 |
VFS 会在 fcntl(F_SETFL) 修改 O_ASYNC 时自动调用驱动的 fasync,并在 release() 中调用 fasync(-1, filp, 0) 做清理。
在 release() 中必须清理:
cpp
static int globalfifo_release(struct inode *inode, struct file *filp)
{
struct globalfifo_dev *dev = filp->private_data;
globalfifo_fasync(-1, filp, 0); /* 移除异步通知登记 */
return 0;
}
7.2.5 变化:
fasync_struct增加了fa_rcu与fa_lock,kill_fasync()内部用 RCU 保护,可在原子上下文调用。fasync_helper()依然存在;同时新增了fasync_insert_entry()/fasync_remove_entry()/fasync_alloc()/fasync_free()供更细粒度控制。kill_fasync()的第三个参数是POLL_IN/POLL_OUT等,7.2.5 内部类型为__poll_t,但对外仍接受int。
9.3 支持异步通知的 globalfifo 驱动
9.3.1 在 globalfifo 驱动中增加异步通知
在 8.1.2 的 globalfifo 基础上:
- 在设备结构体中增加
struct fasync_struct *async_queue;。 - 实现
globalfifo_fasync()。 - 在
write()成功写入后调用kill_fasync(&dev->async_queue, SIGIO, POLL_IN)。 - 在
read()成功读出后调用kill_fasync(&dev->async_queue, SIGIO, POLL_OUT)(如果应用在等"可写")。 - 在
release()中调用globalfifo_fasync(-1, filp, 0)。 - 把
.fasync注册进file_operations。
注意唤醒顺序 :先 wake_up_interruptible()(唤醒阻塞读者)再 kill_fasync()(通知异步应用),或者反过来都行,但两者不能互相替代。
9.3.2 在用户空间中验证 globalfifo 的异步通知
终端 A:
bash
insmod globalfifo.ko
./sigio_test # 打印 waiting for SIGIO (pid=...)
终端 B:
bash
echo "async hello" > /dev/globalfifo
预期终端 A 立即打印收到的数据。
观察点:
bash
# 查看进程的信号掩码与属主
cat /proc/<pid>/status | grep -i sig
ls -l /proc/<pid>/fd
# 统计 SIGIO 次数(需要应用自己计数)
9.3.3 与 poll 的对比实验
| 维度 | SIGIO | poll/epoll |
|---|---|---|
| 编程模型 | 回调(信号处理函数) | 事件循环 |
| 并发能力 | 差(信号处理受限) | 好 |
| 数据竞争 | 需自行处理 | 由事件循环串行化 |
| 适用场景 | 低频率单设备 | 高并发多设备 |
| 7.2.5 推荐 | 仅用于简单场景 | 首选 |
结论 :SIGIO 是原书重点,但现代应用更倾向 epoll 或 io_uring;驱动仍应实现 .fasync,因为它是 O_ASYNC 的契约。
9.4 Linux 异步 I/O
9.4.1 AIO 概念与 GNU C 库 AIO
GNU C 库(glibc)提供 POSIX AIO 接口,定义在 <aio.h>:
cpp
#include <aio.h>
struct aiocb cb = {0};
char buf[512];
cb.aio_fildes = fd;
cb.aio_buf = buf;
cb.aio_nbytes = sizeof(buf);
cb.aio_offset = 0;
aio_read(&cb);
/* 做别的事 */
while (aio_error(&cb) == EINPROGRESS)
usleep(1000);
ssize_t n = aio_return(&cb);
glibc 的 POSIX AIO 在用户空间用线程池模拟(除非 aio_read 直接走内核 AIO),因此并非真正内核异步。
9.4.2 Linux 内核 AIO 与 libaio
内核原生 AIO(io_submit/io_getevents)通过 libaio 使用:
cpp
#include <libaio.h>
io_context_t ctx = 0;
struct iocb cb;
struct iocb *cbs[1];
struct io_event events[1];
io_setup(128, &ctx);
io_prep_pread(&cb, fd, buf, sizeof(buf), 0);
cbs[0] = &cb;
io_submit(ctx, 1, cbs);
io_getevents(ctx, 1, 1, events, NULL);
io_destroy(ctx);
内核 AIO 的局限(4.0 时代就有,7.2.5 依然):
- 只有
O_DIRECT才能真正异步,否则会被同步化到工作线程。 - 只支持
read/write/fsync/poll等少数操作。 - 与信号/事件通知结合复杂。
7.2.5 的现状 :内核 AIO(fs/aio.c)仍在维护但基本冻结;新代码应使用 io_uring。
9.4.3 io_uring(7.2.5 的现代异步 I/O)
io_uring 通过共享内存环形队列提交/完成请求,支持任意 fd 的 read/write/fsync/poll/splice/sendmsg 等,并支持驱动自定义命令。
驱动侧接口(7.2.5):
cpp
/* include/linux/fs.h */
struct io_uring_cmd;
struct file_operations {
...
int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
unsigned int poll_flags);
...
};
驱动通过 io_uring_cmd 实现自定义异步命令(例如 NVMe 的 passthrough、块设备的 zone 操作),完成后调用 io_uring_cmd_done()(见 include/linux/io_uring/cmd.h)。
异步读写的驱动侧契约:
| 操作 | 驱动实现 | 说明 |
|---|---|---|
| 异步 read | read_iter |
返回 -EIOCBQUEUED 表示已排队,稍后 iocb->ki_complete() |
| 异步 write | write_iter |
同上 |
| 异步 poll | poll + iopoll |
iopoll 用于忙轮询 |
| 自定义命令 | uring_cmd |
提交后调用 io_uring_cmd_done() |
read_iter/write_iter 的 ki_complete 契约:
cpp
static ssize_t my_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct my_req *req = ...;
req->iocb = iocb;
submit_to_hw(req);
return -EIOCBQUEUED; /* 告诉 VFS:稍后完成 */
}
/* 硬件完成(可在中断上下文) */
static void my_complete(struct my_req *req, ssize_t res)
{
req->iocb->ki_complete(req->iocb, res); /* 7.2.5 原型 */
}
7.2.5 中 ki_complete 的原型是:
void (*ki_complete)(struct kiocb *iocb, long ret);
(早期版本有 long ret2 参数,7.2.5 已简化为一个返回值,需以源码为准。)
9.4.4 AIO 与设备驱动的关系
- 对字符设备,最简单的异步支持就是实现
read_iter/write_iter并返回-EIOCBQUEUED。 - 对块设备,异步由 blk-mq 统一处理,驱动只需实现
queue_rq或submit_bio。 - 对网络设备,异步由 socket 层与 NAPI 处理,驱动不需要实现 AIO 接口。
- io_uring 的
uring_cmd是 7.2.5 新增的、面向驱动的自定义异步命令通道,值得关注。
9.5 常见错误
| 错误 | 现象 | 修正 |
|---|---|---|
忘记注册 .fasync |
O_ASYNC 无效,收不到 SIGIO |
实现并注册 fasync |
release() 中未移除 fasync |
悬挂指针,rmmod 时 oops | 调 fasync(-1, filp, 0) |
| 在信号处理函数中调用非 async-safe 函数 | 死锁/崩溃 | 只用 read/write/_exit |
kill_fasync 传 POLL_IN 但应用等 POLL_OUT |
应用判断错误 | 原因码要匹配 |
忘记 F_SETOWN |
SIGIO 发给进程组而非进程 | 用 F_SETOWN_EX 明确 |
用 aio_read 期望真异步 |
实际被线程池同步化 | 用 O_DIRECT 或 io_uring |
read_iter 返回 -EIOCBQUEUED 但从不 complete |
应用永久挂起 | 硬件完成后必须 ki_complete() |
9.6 本章实验清单
| 实验 | 目的 | 关键观察 |
|---|---|---|
| SIGIO 通知 | 理解 fasync 链路 | 终端 A 收到信号并读到数据 |
F_SETOWN_EX 指定线程 |
学习精确投递 | 只有目标线程收到信号 |
glibc aio_read |
理解用户态 AIO | aio_error 轮询完成 |
libaio 块设备 AIO |
理解内核 AIO | io_getevents 返回完成事件 |
io_uring 读字符设备 |
理解现代 AIO | IORING_OP_READ 完成 |
io_uring_cmd 自定义命令 |
理解驱动异步命令 | 自定义命令完成回调 |
9.7 总结
- 异步通知的标准实现是
fops->fasync+kill_fasync(),用户侧用F_SETOWN+O_ASYNC+SIGIO。 fasync与poll/等待队列是互补关系,不能互相替代。- 内核 AIO(libaio)在 7.2.5 中已基本冻结,新项目应使用 io_uring。
- 驱动异步 I/O 的核心契约是
read_iter/write_iter返回-EIOCBQUEUED并稍后调用ki_complete()。 - 7.2.5 的
uring_cmd为驱动提供了自定义异步命令入口。 - 所有异步路径都要在 QEMU 中用高并发压测,配合 KASAN 与 lockdep 检查竞态。