20260111 - Linux驱动学习笔记:异步通知

📚 Linux 驱动开发笔记:异步通知 (Asynchronous Notification)

一、 核心概念:什么是异步通知?

在之前的学习中,以及接触了两种读取数据的方式:

  1. 阻塞 (Blocking) :APP 调用 read,没数据就睡觉(wait_event),有数据被唤醒。
  2. 非阻塞 (Non-blocking/Polling) :APP 调用 poll 或设置 O_NONBLOCK,没数据就立刻返回或周期性询问。

异步通知 则是第三种方式:"信号驱动 IO"

  • 类比
    • 阻塞:你在邮局门口一直坐着等,直到信来了才走。
    • 轮询:你每隔 5 分钟去邮局问一次"信来了吗?"
    • 异步通知:你在家看电视(干别的事),邮局有信了直接给你打电话(发信号),你接到电话再去取信。
  • 核心机制 :利用 Linux 的 信号 (Signal) 机制,特别是 SIGIO 信号。

二、 应用程序要做什么? (Receiver)

参考你的应用层代码 button_test.c,APP 需要完成"三部曲"才能接收驱动的信号。

1. 注册信号处理函数 (绑卡)

你需要告诉内核:当收到 SIGIO 信号时,请执行哪个函数。

c 复制代码
// 这里的 sig_fun 就是那个"回调函数"
static void sig_fun(int sig)
{
    int val;
    read(fd, &val, 4); // 收到信号后,立马去读数据
    printf("get button : 0x%08x\n", val);
}

// 在 main 函数中注册
signal(SIGIO, sig_fun); 

2. 设置文件的拥有者 (留名)

驱动程序发出信号时,必须知道发给哪个进程(PID)。

c 复制代码
// 告诉驱动:fd 这个文件的"主人"是当前进程 (getpid())
fcntl(fd, F_SETOWN, getpid());

3. 开启异步通知标志 (开通服务)

最后,通过 fcntl 设置 FASYNC 标志。这会触发驱动层调用 .fasync 函数。

c 复制代码
flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | FASYNC); // 这一步至关重要!

三、 驱动程序要做什么? (Sender)

参考你的内核代码,驱动层需要配合 APP 完成信号的发送。

1. 定义 fasync_struct 结构体

这是一个内核链表结构,用来存放"有哪些进程正在等待我的信号"。

c 复制代码
struct fasync_struct *button_fasync; // 定义一个指针

2. 实现 .fasync 操作函数

当 APP 调用 fcntl(fd, F_SETFL, flags | FASYNC) 时,内核会调用驱动的这个 .fasync 函数。

我们只需要调用内核帮我们写好的助手函数 fasync_helper 即可。它会把 APP 的进程信息初始化并加入到 button_fasync 链表中。

c 复制代码
static int gpio_key_drv_fasync(int fd, struct file *file, int on)
{
    // fasync_helper 负责维护 button_fasync 链表(添加或删除进程)
    if (fasync_helper(fd, file, on, &button_fasync) >= 0)
        return 0;
    else
        return -EIO;
}

// 别忘了在 file_operations 里注册它
static struct file_operations gpio_key_drv = {
    // ...
    .fasync  = gpio_key_drv_fasync,
};

3. 在中断里发送信号 (Trigger)

当硬件产生中断,数据准备好(放入环形缓冲区)后,驱动主动发射信号。

c 复制代码
static irqreturn_t gpio_key_isr(int irq, void *dev_id)
{
    // ... 读取 GPIO,放入环形缓冲区 ...
    put_key(key);
    
    // 1. 唤醒阻塞的进程(如果有)
    wake_up_interruptible(&gpio_key_wait);

    // 2. 发送异步信号!
    // 参数说明:
    // &button_fasync: 发给谁?(发给链表里的所有进程)
    // SIGIO: 发什么信号?
    // POLL_IN: 为什么发?(因为有数据进来了/Input)
    kill_fasync(&button_fasync, SIGIO, POLL_IN);
    
    return IRQ_HANDLED;
}

四、 完整的数据流向图解

让把代码串联起来,看看当按键按下时发生了什么:

  1. 初始化阶段
    • APP: open() -> signal() -> fcntl(F_SETOWN) -> fcntl(FASYNC).
    • Kernel: gpio_key_drv_fasync 被调用,APP 的 PID 被记录在 button_fasync 结构体中。
    • APP: 进入 while(1) 循环,打印 "zeku" 并睡觉,完全不理会按键
  2. 事件触发阶段
    • User: 按下按键。
    • Hardware: 触发 GPIO 中断。
    • Kernel (ISR) : 执行 gpio_key_isr
      • put_key(key): 数据存入环形缓冲区。
      • kill_fasync(...): 遍历 button_fasync 链表,找到 APP 的 PID,发送 SIGIO 信号。
  3. 响应阶段
    • Kernel : 暂停 APP 当前的 printf,强制跳转到注册好的 sig_fun 函数。
    • APP (sig_fun) :
      • 执行 read(fd, ...)
      • read 进入驱动 gpio_key_drv_read
      • 驱动发现缓冲区有数据 (!is_key_buf_empty()),直接拷贝数据返回,不休眠。
      • APP 打印用户按下的按键值。
    • APP : sig_fun 执行完毕,回到 main 函数刚才被打断的地方继续运行。

五、 关键代码细节解析

1. 环形缓冲区的配合

注意看 gpio_key_isr

c 复制代码
put_key(key);      // 先存数据
wake_up...         // 再唤醒阻塞者
kill_fasync...     // 最后通知异步者

顺序很重要。必须先让数据入队(put_key),然后再发信号。否则 APP 收到信号跑来调用 read,结果发现缓冲区是空的,又会进去休眠,导致逻辑混乱。

3. POLL_IN

kill_fasync 中使用的 POLL_IN (带下划线) 是告诉 APP:信号产生的原因是"有数据可读"。这与 POLLIN (无下划线) 在 poll 函数中作为返回值的意义在逻辑上是一致的,但定义来源不同。


六、 总结

通过这组代码,可以掌握 Linux 驱动中最高级的 IO 交互方式之一。

  • 优点:APP 不需要死等,也不需要不停问,CPU 利用率极高,响应速度极快(中断级)。
  • 缺点:APP 编写稍微复杂一点(需要处理信号),且信号处理函数中不能进行太耗时的操作(类似于中断处理)。
相关推荐
ling___xi29 分钟前
《计算机网络》计网3小时期末速成课各版本教程都可用谢稀仁湖科大版都可用_哔哩哔哩_bilibili(笔记)
网络·笔记·计算机网络
中屹指纹浏览器2 小时前
中屹指纹浏览器底层架构深度解析——基于虚拟化的全维度指纹仿真与环境隔离实现
经验分享·笔记
Hello_Embed2 小时前
libmodbus 移植 STM32(基础篇)
笔记·stm32·单片机·学习·modbus
无聊的小坏坏2 小时前
实习笔记:用 /etc/crontab 实现定期数据/日志清理
笔记·实习日记
香芋Yu2 小时前
【机器学习教程】第04章 指数族分布
人工智能·笔记·机器学习
深蓝海拓4 小时前
PySide6从0开始学习的笔记(二十六) 重写Qt窗口对象的事件(QEvent)处理方法
笔记·python·qt·学习·pyqt
中屹指纹浏览器4 小时前
中屹指纹浏览器多场景技术适配与接口封装实践
经验分享·笔记
qqssss121dfd4 小时前
STM32H750XBH6的ETH模块移植LWIP
网络·stm32·嵌入式硬件
想放学的刺客5 小时前
单片机嵌入式试题(第27期)设计可移植、可配置的外设驱动框架的关键要点
c语言·stm32·单片机·嵌入式硬件·物联网
BugShare6 小时前
Obsidian 使用指南:从零开始搭建你的个人知识库
笔记·obsidian