嵌入式linux学习记录九,异步通知
2026/6/6 13:46:57 网站建设 项目流程
  1. 如果说poll机制是“应用层主动去内核盯着数据”(属于轮询/阻塞的变种),那么fasync机制就是“应用层做别的事,内核有数据了主动发微信(信号)通知应用层”(属于完全的被动接收)。

    以下是根据你提供的 ①~⑬ 步骤,将整个异步通知机制的建立、触发、处理流程进行的详细拆解:

    1. 准备阶段:应用层向内核“挂号” (②~⑦)

      这一阶段的核心目的是:让应用层和驱动程序“对上暗号”,并在内核中登记应用层的联系方式(PID)。

      1. ② 绑定信号处理函数:APP 通过signal(SIGIO, func)告诉操作系统:“只要我收到了SIGIO(I/O异步通知信号),你就立刻停下我手头的工作,去执行func这个函数。”

      2. ③ 登记进程 PID:APP 调用fcntl(fd, F_SETOWN, getpid())注意:这一步是在内核的文件系统层完成的。内核会将当前 APP 的进程 ID(PID)绑定到打开的文件描述符filp结构体上。这样内核就知道:“以后这个设备有动静,我该通知哪一个具体的进程。”

      3. ④ & ⑤ 修改标志触发驱动:APP 先读取文件状态 Flag,然后通过fcntl(fd, F_SETSETFL, flags | FASYNC)FASYNC(异步标志位)设置为 1。

        • 关键化学反应:一旦这个标志位被应用层修改,Linux 内核框架就会自动调用驱动程序里的字符设备接口.fasync(即驱动中的drv_fasync函数)。

      4. ⑥ & ⑦ 驱动内部结构绑定 (fasync_helper)

        • 驱动的drv_fasync被调用后,必须在内部调用内核现成的工具函数fasync_helper(...)

        • 这个函数会动态创建一个结构体button_async(类型为struct fasync_struct),并把当前的驱动文件指针filp塞进去。

        • 因为filp在第 ③ 步时已经捆绑了 APP 的 PID,所以至此,驱动程序(通过button_async)彻底掌握了应用层的联系方式(PID 和文件指针)

    2. 闲置阶段:应用层各忙各的 (⑧)

      1. ⑧ APP 释放 CPU:完成上述登记后,APP 的main函数里可以去执行完全无关的代码(比如打印进度、做复杂的数学计算,甚至单纯进入一个不带超时的while(1) { sleep(1); })。它不需要像poll一样在内核里挂队死等,不消耗这部分系统资源。

    3. 触发与回调阶段:中断生信,信号传导 (⑨~⑬)

      这一阶段是数据的产生和通知过程。

      1. ⑨ & ⑩ 硬件中断触发并“拍电报”

        • 当用户按下硬件按键,触发 GPIO 硬件中断,内核转入执行驱动的中断服务程序(ISR)

        • 中断服务程序读取并记录完按键硬件数据后,调用内核的核心发送函数:

          kill_fasync(&button_async, SIGIO, POLL_IN);
        • 底层动作:内核会顺着button_async里存着的联系方式,找到在第 ③ 步登记的 APP 的 PID,然后向该进程定向发送一个SIGIO信号

      2. ⑪, ⑫, ⑬ 应用层收信处理

        • 应用层进程收到操作系统投递来的SIGIO信号,CPU 立即强制暂停 APP 当前正在执行的普通代码(第 ⑧ 步的代码)。

        • CPU 跳转到第 ② 步注册的信号处理函数func中执行。

        • func函数内部,APP 调用read(fd, &val, 1)。由于此时中断刚发生,驱动里的数据必定是现成的,因此read极其顺畅、绝不阻塞地将按键数据读取到应用层。

        • 执行完func后,进程回到第 ⑧ 步被中断的地方继续做别的事。

    4. 机制核心总结(精简要点)

      1. 谁负责发信号?:内核通过驱动里的kill_fasync负责发信号。

      2. 驱动不维护进程,只维护关系:驱动程序员不需要写怎么给进程发信号的代码,只需要用fasync_helper把关系建立好,在中断里调用kill_fasync,内核大管家自然会根据 PID 去送达信号。

      3. 核心优势:这是完全的事件驱动(Event-Driven)。对应用层而言,没有任何阻塞或主动轮询的开销,效率极高,非常适合高并发或需要实时响应、但平时数据频率很低的硬件设备(如报警按键、异常跌倒传感器等)。

  2. 实现方法:

    1. 驱动核心代码

      #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, };
    2. 应用层完整代码
      应用层需要按照顺序完成:注册信号 -> 绑定 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; }
    3. 实现逻辑串联核对

      我们可以用这段真实代码对照你之前给出的流程图:

      1. APP 侧signal(SIGIO, my_signal_func)准备好收信框。

      2. APP 侧fcntl(fd, F_SETOWN, getpid())filp上写下进程号。

      3. 内核与驱动侧:APP 执行fcntl(fd, F_SETFL, flags | FASYNC),内核检测到标志变化,调用驱动的.fasync虚函数。驱动在gpio_key_drv_fasync里通过fasync_helperfilp(带PID)打包挂载到全局变量button_async上。到这一步,管道彻底打通。

      4. 中断侧:硬件被触发,驱动进入gpio_key_isr中断服务程序,执行kill_fasync(&button_async, SIGIO, POLL_IN)。内核顺着button_async找到对应的进程 PID,把SIGIO送过去。

      5. APP 收尾:APP 正在执行while(1)sleep被强行打断,操作系统强行让 CPU 跳去执行my_signal_func。在里面调用read完成数据获取,随后返回while(1)继续循环。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询