09-设备驱动中的异步通知与异步 I/O

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 基础上:

  1. 在设备结构体中增加 struct fasync_struct *async_queue;。
  2. 实现 globalfifo_fasync()。
  3. 在 write() 成功写入后调用 kill_fasync(&dev->async_queue, SIGIO, POLL_IN)。
  4. 在 read() 成功读出后调用 kill_fasync(&dev->async_queue, SIGIO, POLL_OUT)(如果应用在等"可写")。
  5. 在 release() 中调用 globalfifo_fasync(-1, filp, 0)。
  6. 把 .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 总结

  1. 异步通知的标准实现是 fops->fasync + kill_fasync(),用户侧用 F_SETOWN + O_ASYNC + SIGIO。
  2. fasync 与 poll/等待队列是互补关系,不能互相替代。
  3. 内核 AIO(libaio)在 7.2.5 中已基本冻结,新项目应使用 io_uring。
  4. 驱动异步 I/O 的核心契约是 read_iter/write_iter 返回 -EIOCBQUEUED 并稍后调用 ki_complete()。
  5. 7.2.5 的 uring_cmd 为驱动提供了自定义异步命令入口。
  6. 所有异步路径都要在 QEMU 中用高并发压测,配合 KASAN 与 lockdep 检查竞态。
相关推荐
Snasph4 小时前
17-SPI 驱动与 I2C/SPI/USB 架构类比
linux 驱动学习
Snasph7 小时前
21-Linux 设备驱动的调试
linux 驱动学习
Snasph8 小时前
22-QEMU 实验总览
linux 驱动学习
Snasph8 小时前
13-Linux 块设备驱动
linux 驱动学习
Snasph10 小时前
15- Linux I2C 核心、总线与设备驱动
linux 驱动学习
Snasph10 小时前
附录-API 速查与术语表
linux 驱动学习
Snasph11 小时前
11-内存与 I/O 访问
linux 驱动学习
Snasph12 小时前
14-Linux 网络设备驱动
linux 驱动学习
Snasph13 小时前
18-ARM Linux 设备树
linux 驱动学习