📚 Linux 驱动开发笔记:异步通知 (Asynchronous Notification)
一、 核心概念:什么是异步通知?
在之前的学习中,以及接触了两种读取数据的方式:
- 阻塞 (Blocking) :APP 调用
read,没数据就睡觉(wait_event),有数据被唤醒。 - 非阻塞 (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;
}
四、 完整的数据流向图解
让把代码串联起来,看看当按键按下时发生了什么:
- 初始化阶段 :
- APP:
open()->signal()->fcntl(F_SETOWN)->fcntl(FASYNC). - Kernel:
gpio_key_drv_fasync被调用,APP 的 PID 被记录在button_fasync结构体中。 - APP: 进入
while(1)循环,打印 "zeku" 并睡觉,完全不理会按键。
- APP:
- 事件触发阶段 :
- User: 按下按键。
- Hardware: 触发 GPIO 中断。
- Kernel (ISR) : 执行
gpio_key_isr。put_key(key): 数据存入环形缓冲区。kill_fasync(...): 遍历button_fasync链表,找到 APP 的 PID,发送SIGIO信号。
- 响应阶段 :
- Kernel : 暂停 APP 当前的
printf,强制跳转到注册好的sig_fun函数。 - APP (sig_fun) :
- 执行
read(fd, ...)。 read进入驱动gpio_key_drv_read。- 驱动发现缓冲区有数据 (
!is_key_buf_empty()),直接拷贝数据返回,不休眠。 - APP 打印用户按下的按键值。
- 执行
- APP :
sig_fun执行完毕,回到main函数刚才被打断的地方继续运行。
- Kernel : 暂停 APP 当前的
五、 关键代码细节解析
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 编写稍微复杂一点(需要处理信号),且信号处理函数中不能进行太耗时的操作(类似于中断处理)。