如果说
poll机制是“应用层主动去内核盯着数据”(属于轮询/阻塞的变种),那么fasync机制就是“应用层做别的事,内核有数据了主动发微信(信号)通知应用层”(属于完全的被动接收)。以下是根据你提供的 ①~⑬ 步骤,将整个异步通知机制的建立、触发、处理流程进行的详细拆解:
准备阶段:应用层向内核“挂号” (②~⑦)
这一阶段的核心目的是:让应用层和驱动程序“对上暗号”,并在内核中登记应用层的联系方式(PID)。
② 绑定信号处理函数:APP 通过
signal(SIGIO, func)告诉操作系统:“只要我收到了SIGIO(I/O异步通知信号),你就立刻停下我手头的工作,去执行func这个函数。”③ 登记进程 PID:APP 调用
fcntl(fd, F_SETOWN, getpid())。注意:这一步是在内核的文件系统层完成的。内核会将当前 APP 的进程 ID(PID)绑定到打开的文件描述符filp结构体上。这样内核就知道:“以后这个设备有动静,我该通知哪一个具体的进程。”④ & ⑤ 修改标志触发驱动:APP 先读取文件状态 Flag,然后通过
fcntl(fd, F_SETSETFL, flags | FASYNC)将FASYNC(异步标志位)设置为 1。关键化学反应:一旦这个标志位被应用层修改,Linux 内核框架就会自动调用驱动程序里的字符设备接口
.fasync(即驱动中的drv_fasync函数)。
⑥ & ⑦ 驱动内部结构绑定 (
fasync_helper):驱动的
drv_fasync被调用后,必须在内部调用内核现成的工具函数fasync_helper(...)。这个函数会动态创建一个结构体
button_async(类型为struct fasync_struct),并把当前的驱动文件指针filp塞进去。因为
filp在第 ③ 步时已经捆绑了 APP 的 PID,所以至此,驱动程序(通过button_async)彻底掌握了应用层的联系方式(PID 和文件指针)。
闲置阶段:应用层各忙各的 (⑧)
⑧ APP 释放 CPU:完成上述登记后,APP 的
main函数里可以去执行完全无关的代码(比如打印进度、做复杂的数学计算,甚至单纯进入一个不带超时的while(1) { sleep(1); })。它不需要像poll一样在内核里挂队死等,不消耗这部分系统资源。
触发与回调阶段:中断生信,信号传导 (⑨~⑬)
这一阶段是数据的产生和通知过程。
⑨ & ⑩ 硬件中断触发并“拍电报”:
当用户按下硬件按键,触发 GPIO 硬件中断,内核转入执行驱动的中断服务程序(ISR)。
中断服务程序读取并记录完按键硬件数据后,调用内核的核心发送函数:
kill_fasync(&button_async, SIGIO, POLL_IN);底层动作:内核会顺着
button_async里存着的联系方式,找到在第 ③ 步登记的 APP 的 PID,然后向该进程定向发送一个SIGIO信号。
⑪, ⑫, ⑬ 应用层收信处理:
应用层进程收到操作系统投递来的
SIGIO信号,CPU 立即强制暂停 APP 当前正在执行的普通代码(第 ⑧ 步的代码)。CPU 跳转到第 ② 步注册的信号处理函数
func中执行。在
func函数内部,APP 调用read(fd, &val, 1)。由于此时中断刚发生,驱动里的数据必定是现成的,因此read会极其顺畅、绝不阻塞地将按键数据读取到应用层。执行完
func后,进程回到第 ⑧ 步被中断的地方继续做别的事。
机制核心总结(精简要点)
谁负责发信号?:内核通过驱动里的
kill_fasync负责发信号。驱动不维护进程,只维护关系:驱动程序员不需要写怎么给进程发信号的代码,只需要用
fasync_helper把关系建立好,在中断里调用kill_fasync,内核大管家自然会根据 PID 去送达信号。核心优势:这是完全的事件驱动(Event-Driven)。对应用层而言,没有任何阻塞或主动轮询的开销,效率极高,非常适合高并发或需要实时响应、但平时数据频率很低的硬件设备(如报警按键、异常跌倒传感器等)。
实现方法:
驱动核心代码
#include <linux/module.h> #include <linux/fs.h> #include <linux/poll.h> #include <linux/wait.h> #include <linux/sched.h> #include <linux/interrupt.h> // 1. 定义一个异步通知结构体指针 static struct fasync_struct *button_async = NULL; static int has_data = 0; static char key_val = 0; // 2. 实现 file_operations 中的 fasync 接口 static int gpio_key_drv_fasync(int fd, struct file *filp, int on) { // 调用内核提供的帮助函数,它会自动根据 on (1或0) 来初始化或释放 button_async 结构体 // 这对应了原理图中的 ⑥ 和 ⑦ return fasync_helper(fd, filp, on, &button_async); } // 3. 模拟硬件中断服务程序(对应原理图中的 ⑨ 和 ⑩) static irqreturn_t gpio_key_isr(int irq, void *dev_id) { // 假设硬件产生数据 key_val = 0x55; has_data = 1; // 关键:释放信号。内核会根据 button_async 里的登记信息,向对应的进程发送 SIGIO 信号 // POLL_IN 表示有数据可读 kill_fasync(&button_async, SIGIO, POLL_IN); return IRQ_HANDLED; } // 4. 实现常规的 read 接口 static ssize_t gpio_key_drv_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off) { if (!has_data) return -EAGAIN; if (copy_to_user(buf, &key_val, 1)) return -EFAULT; has_data = 0; // 读取后清除标志 return 1; } // 5. 当 APP 关闭文件时,必须把登记的异步结构体清理掉 static int gpio_key_drv_close(struct inode *inode, struct file *filp) { // 最后一个参数传入 0,表示注销 gpio_key_drv_fasync(-1, filp, 0); return 0; } // 6. 绑定到 file_operations static struct file_operations gpio_key_fops = { .owner = THIS_MODULE, .read = gpio_key_drv_read, .fasync = gpio_key_drv_fasync, // 绑定 fasync 接口 .release = gpio_key_drv_close, };应用层完整代码
应用层需要按照顺序完成:注册信号 -> 绑定 PID -> 修改 FASYNC 标志。#include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <signal.h> int fd; // 对应原理图中的 ⑪ ⑫ ⑬:信号处理函数 void my_signal_func(int signum) { char key_val; if (signum == SIGIO) { // 信号来了,说明驱动有数据了,直接 read 绝对不会阻塞 if (read(fd, &key_val, 1) > 0) { printf("APP 成功接收到驱动发来的信号!读取到按键值: 0x%x\n", key_val); } } } int main(int argc, char **argv) { int flags; fd = open("/dev/my_key", O_RDWR); if (fd < 0) { printf("打开驱动失败!\n"); return -1; } // 步骤 ②:给 SIGIO 信号注册处理函数 func signal(SIGIO, my_signal_func); // 步骤 ③:把 APP 的 PID 告诉内核文件系统层次 fcntl(fd, F_SETOWN, getpid()); // 步骤 ④:读取驱动程序文件当前的 Flag flags = fcntl(fd, F_GETFL); // 步骤 ⑤:设置 Flag 里面的 FASYNC 位为 1 // 这一步一执行,内核就会立刻去调用驱动中的 gpio_key_drv_fasync 函数 fcntl(fd, F_SETFL, flags | FASYNC); // 步骤 ⑧:APP 可以去做任何其他事情,完全不占用监控硬件的 CPU 资源 while (1) { printf("APP 正在做其他复杂的计算或工作...\n"); sleep(2); } close(fd); return 0; }实现逻辑串联核对
我们可以用这段真实代码对照你之前给出的流程图:
APP 侧:
signal(SIGIO, my_signal_func)准备好收信框。APP 侧:
fcntl(fd, F_SETOWN, getpid())在filp上写下进程号。内核与驱动侧:APP 执行
fcntl(fd, F_SETFL, flags | FASYNC),内核检测到标志变化,调用驱动的.fasync虚函数。驱动在gpio_key_drv_fasync里通过fasync_helper把filp(带PID)打包挂载到全局变量button_async上。到这一步,管道彻底打通。中断侧:硬件被触发,驱动进入
gpio_key_isr中断服务程序,执行kill_fasync(&button_async, SIGIO, POLL_IN)。内核顺着button_async找到对应的进程 PID,把SIGIO送过去。APP 收尾:APP 正在执行
while(1)的sleep被强行打断,操作系统强行让 CPU 跳去执行my_signal_func。在里面调用read完成数据获取,随后返回while(1)继续循环。
嵌入式linux学习记录九,异步通知