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) 继续循环。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐