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 编写稍微复杂一点(需要处理信号),且信号处理函数中不能进行太耗时的操作(类似于中断处理)。
相关推荐
wdfk_prog2 小时前
[Linux]学习笔记系列 -- 内存管理与访问
linux·笔记·学习
go_bai2 小时前
Linux-网络基础
linux·开发语言·网络·笔记·学习方法·笔记总结
崎岖Qiu2 小时前
【OS笔记38】:设备管理 - I/O 设备原理
笔记·操作系统·os·设备管理·io设备
TEC_INO2 小时前
STM32_9:I2C_DHT11_OLED项目
stm32·单片机·嵌入式硬件
__万波__2 小时前
STM32基于HAL 库开发包创建新的工程-编译-烧录
stm32·单片机·嵌入式硬件
曾浩轩3 小时前
跟着江协科技学STM32之4-5OLED模块教程OLED显示原理
科技·stm32·单片机·嵌入式硬件·学习
BreezeJuvenile3 小时前
ADC_案例练习:独立模式多通道采集
stm32·单片机·adc·多通道采集·dma辅助
代码游侠3 小时前
学习笔记——HC-SR04 超声波测距传感器
开发语言·笔记·嵌入式硬件·学习
Abbylolo4 小时前
《Obsidian Excalidraw插件配置与使用指南》
笔记