版权信息
warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
Linux 异步通知机制(Asynchronous Notification)是 Linux 设备驱动中一个非常经典且实用的概念。
简单来说,它的核心思想是 “被动接收,而非主动查询”。
1. 什么是异步通知机制
想象一下你在钓鱼,这时候你有两种方式知道鱼上钩了:
-
轮询(Polling):你每隔 5 秒钟拉起鱼竿看一看有没有鱼。这很累,而且如果你在看书,看书会被不断打断。
-
异步通知(Asynchronous):你在鱼竿上挂个铃铛,然后你就安心看书。当鱼上钩拉动鱼竿时,铃铛响了(信号),你再放下书去收杆。
在 Linux 中,异步通知机制是基于 信号(Signal) 实现的。
-
非阻塞 I/O:应用程序需要不断循环读取(轮询),浪费 CPU。
-
异步通知:应用程序告诉内核驱动:“如果有数据了,发个信号(通常是
SIGIO)告诉我。” 驱动程序在数据就绪时,主动向应用程序发送信号,应用程序暂停当前工作,进入信号处理函数处理数据。
2. 异步通知的优缺点
2.1. 优点
- 节省 CPU 资源:应用程序不需要死循环轮询(Polling),CPU 可以处理其他任务或进入休眠。
- 响应及时:数据一来,驱动立马发信号,比轮询的时间片机制反应更快。
- 逻辑解耦:应用程序可以专心做主逻辑,只有在 I/O 事件发生时才被打断。
2.2. 缺点
- 编程复杂度增加:需要处理信号并发、重入等问题。
- 不适合高频数据:如果数据来得太快(比如网络包狂轰滥炸),不断触发信号中断会让 CPU 把时间都花在上下文切换上(类似“中断风暴”),此时轮询反而更高效。
3. 理解异步通知机制
我们从用户态开始入手,看在使用异步通知时,内核里实际发生了什么。
首先我们需要了解三个核心API:
3.1. fcntl()
它定义在 <linux/fcntl.h>。是用户态用来“控制已打开文件描述符行为”的系统调用。
它不负责读写数据,而是用来:
- 改文件描述符的状态
- 设置 进程 / 信号 / 锁 等属性
函数原型:
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */);
fd:已经打开的文件描述符cmd:你要干什么arg:参数(根据 cmd 不同)
fcntl主要是操作 struct file 这个结构体,如下:
用户态 fcntl()
↓
sys_fcntl()
↓
VFS
↓
struct file
├─ f_flags ← O_NONBLOCK / O_ASYNC
├─ f_owner ← SIGIO 接收者
└─ f_op
四大类用法:
1. F_GETFL / F_SETFL
🟢作用:读取 / 修改 file->f_flags
🟢常见标志位:
| 标志 | 作用 |
|---|---|
O_NONBLOCK |
非阻塞 I/O |
O_ASYNC |
启用异步通知(SIGIO) |
O_APPEND |
追加写 |
O_DIRECT |
直接 I/O |
🟢用法:
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_NONBLOCK | O_ASYNC);
不能直接 F_SETFL 一个新值,否则你会把原有 flags 覆盖掉。
2. F_SETOWN / F_GETOWN
👉 异步通知必须用
🟢作用:指定信号发给谁 修改 file->f_owner.pid = pid
🟢用法:
fcntl(fd, F_SETOWN, getpid());
3. F_DUPFD / F_DUPFD_CLOEXEC
🟢作用:复制文件描述符
🟢用法:
int newfd = fcntl(fd, F_DUPFD, 0);
4. 文件锁
用于防止多个进程同时操作资源。现在不深入。
3.2. fasync_helper()
这是在驱动层使用的。fasync_helper() 用来维护“需要被异步通知的进程列表”。
它做的事只有一个核心目标——
哪个进程打开了设备并设置了 O_ASYNC,就把那个进程记下来。
函数原型:
int fasync_helper(int fd,
struct file *file,
int on,
struct fasync_struct **fapp);
-
fd:- 用户态传进来的文件描述符
- 只是标识用途
- 内核真正关心的是
file
可以把它当附带信息
-
file:- 对应用户态
open()返回的 fd - 即fcntl操作的
struct filef_owner(信号接收者)f_flags(是否 O_ASYNC)
异步通知最终是基于 file 的
- 对应用户态
-
on:on != 0启用异步(加入队列)on == 0关闭异步(移除队列)
-
fapp:
需要被异步通知的进程链表的链表头。
函数逻辑伪代码:
if (on) {
// 开启异步
if (file 不在 fapp 链表中) {
分配 fasync_struct
设置 file / f_owner
插入 fapp 链表
}
} else {
// 关闭异步
if (file 在 fapp 链表中) {
从链表删除
释放 fasync_struct
}
}
3.3. kill_fasync()
在驱动层使用。向应用程序发出信号的核心函数。
原型:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
fp:要操作的 fasync_struct。sig:要发送的信号。band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT。
3.4. 工作流程
- 在用户态打开设备:
int fd = open("/dev/key", O_RDWR);
此步在 VFS 中创建 struct file,没启用异步。
- 用户态登记进程PID:
fcntl(fd, F_SETOWN, getpid());
内核找到 struct file,把 file->f_owner.pid 设置为当前进程 PID,以后这个 file 触发信号,就发给这个进程。
- 用户态请求开启异步通知:
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_ASYNC);
此步会触发驱动的 fasync 回调函数。
- 驱动必须实现 fasync 这个fop:
static int key_fasync(int fd, struct file *file, int on)
{
struct key_dev *kdev = file->private_data;
return fasync_helper(fd, file, on, &kdev->async_queue);
}
fasync_helper:把当前进程封装成 struct fasync_struct 并加入 kdev->async_queue。
just like this:
key_dev -> 设备资源结构体
└── async_queue
├── 进程A (PID xxx)
├── 进程B
- 事件发生:
例如,按键中断触发
irq_handler()
{
key_value = 1;
kill_fasync(&kdev->async_queue, SIGIO, POLL_IN);
}
kill_fasync:通知 async_queue 里的所有进程:有 I/O 事件发生了。
just like this:
kill_fasync
└── 遍历 async_queue
└── send_sigio()
└── send_sig_info()
└── 把 SIGIO 放入目标进程的 signal queue
注意:此函数只是把只是把信号“挂”到目标进程上。
- 用户态必须注册信号处理函数:
signal(SIGIO, sigio_handler);
当内核发现:
current->pending signal == SIGIO
则
→ 保存当前上下文
→ 跳转到 sigio_handler()
4. 使用异步机制
4.1. 应用程序端 (User Space)
应用程序需要做三件事(简称“三板斧”):
- 绑定信号处理函数:使用
signal()让应用知道收到SIGIO信号后该干什么。 - 设置属主:使用
fcntl(fd, F_SETOWN, getpid())告诉内核:“这个设备文件的信号要发给当前进程”。 - 开启异步标志:使用
fcntl(fd, F_SETFL, flags | O_ASYNC)启用异步通知功能。
4.2. 驱动程序端 (Kernel Space)
驱动需要处理数据结构 fasync_struct 并实现三个部分:
- 定义结构体:在设备结构体中定义
struct fasync_struct *async_queue;。 - 实现
.fasync接口:当应用调用fcntl设置O_ASYNC时,内核会调用驱动的.fasync函数。驱动里只需调用内核辅助函数fasync_helper。 - 发送信号:当设备数据就绪(如中断来了),调用
kill_fasync发送信号。 - 清理:在
.release函数中把节点从异步队列中删除。
4.3. 番外:嵌入还是指针?
在设备资源结构体中,
struct fasync_struct async_queue 用嵌入还是指针?
用指针。因为它是“外部管理对象”,你只是引用。它由 fasync_helper 管理。
—END—