5. Linux内核异步通知¶
在Linux内核驱动开发中,异步通知是一种高效的IO事件通知机制,与阻塞IO、非阻塞IO共同构成了Linux内核IO模型的核心体系。 阻塞IO通过进程休眠等待IO就绪,非阻塞IO通过轮询或事件监听检测IO状态, 而异步通知则打破了“主动等待”的模式,由内核在IO事件就绪时主动向应用层发送信号,通知应用程序处理IO操作。
5.1. 异步通知概念¶
异步通知(Asynchronous Notification),本质是一种“被动式”IO事件通知机制,核心逻辑是应用程序注册IO事件通知后, 可正常执行其他任务,无需主动检测IO状态;当内核检测到IO事件就绪时,会主动向应用程序发送指定信号,应用程序捕获到信号后,再去处理对应的IO操作。
异步通知的核心是“信号驱动”,依托Linux内核的信号机制实现内核与应用层的通信,其核心目标是解决阻塞IO的“等待浪费”和非阻塞IO轮询的“CPU消耗”问题, 实现IO事件的高效响应与资源的合理利用。
5.1.1. 与阻塞/非阻塞IO的区别¶
异步通知与阻塞IO、非阻塞IO的核心区别在于“IO事件的检测主体”和“进程的执行状态”,具体对比如下:
阻塞IO:进程主动发起IO请求,IO未就绪时进程休眠,检测主体是内核,进程处于被动等待状态,无需消耗CPU。
非阻塞IO:进程主动轮询检测IO状态,IO未就绪时立即返回错误,检测主体是应用程序,若不配合事件监听会消耗大量CPU。
异步通知:应用程序无需检测IO状态,由内核检测IO就绪后主动发送信号,检测主体是内核,进程可自由执行其他任务,CPU消耗低且响应实时。
5.2. 信号基础¶
异步通知的核心是信号通信,Linux内核提供了多种信号,其中最常用于异步通知的是SIGIO信号(异步IO信号),该信号专门用于通知应用程序“IO事件已就绪”。 信号的核心特性信号是内核向进程发送的异步通知,进程收到信号后,会暂停当前执行的任务,转而执行预先注册的信号处理函数,处理完成后再恢复原有任务的执行; 若未注册信号处理函数,进程会执行信号的默认行为(如终止进程、忽略信号等)。 在异步通知中,需确保应用程序正确注册SIGIO信号的处理函数,且告知内核“当前进程需要接收该设备的SIGIO信号”,否则无法正常接收内核发送的IO事件通知。
具体可以参考 应用手册的信号章节 。
5.3. 异步通知原理¶
异步通知的实现依赖三个核心环节,三者协同完成IO事件的通知与处理:
信号注册:应用程序向内核注册信号处理函数,指定需要接收的信号,并告知内核自身进程ID,以便内核精准发送信号。
事件监听:驱动层检测IO事件状态,当IO就绪时,通过内核提供的函数向应用层发送注册的信号,完成通知触发。
信号处理:应用程序捕获到内核发送的信号后,执行预先注册的信号处理函数,在函数中完成IO数据传输、设备状态读取等操作。
异步通知的整体执行流程图如下:
5.4. 异步通知内核常用函数¶
5.4.1. 异步通知辅助函数¶
5.4.1.1. fasync_helper函数¶
fasync_helper函数用于完成异步通知结构体的初始化、注册或注销,是连接应用层异步通知配置与驱动层信号发送的关键函数。 当应用层开启或关闭异步通知时,该函数会自动更新异步通知结构体的状态。
函数原型:
1 | int fasync_helper(int fd, struct file *filp, int on, struct fasync_struct **fasync);
|
参数说明:
fd:应用层打开设备的文件描述符。
filp:文件结构体指针,指向当前打开的设备文件。
on:控制标志,1表示注册异步通知,0表示注销异步通知。
fasync:异步通知结构体指针的指针,用于存储异步通知相关信息(如应用进程ID、信号类型)。
返回值:0表示操作成功;非0表示操作失败,返回对应的错误码。
5.4.2. 信号发送¶
5.4.2.1. kill_fasync函数¶
kill_fasync函数用于向应用层发送信号的核心函数,当IO事件就绪时, 调用该函数向注册了异步通知的应用进程发送指定信号(通常为SIGIO),告知应用程序处理IO操作。
函数原型:
1 | void kill_fasync(struct fasync_struct **fasync, int sig, int band);
|
参数说明:
fasync:异步通知结构体指针的指针,与fasync_helper函数中的参数一致,存储应用进程的通知信息。
sig:需要发送的信号,异步通知中常用SIGIO。
band:IO事件类型掩码,用于告知应用程序具体的IO事件,常用POLL_IN(表示数据可读)、POLL_OUT(表示数据可写)。
5.4.3. 异步通知回调函数¶
struct file_operations中的.fasync,当应用层通过fcntl函数开启或关闭异步通知时,内核会自动调用该函数, 该函数内部通常会调用fasync_helper函数,完成异步通知的注册/注销。
函数原型:
1 | int (*fasync)(int fd, struct file *filp, int on);
|
参数说明:与fasync_helper函数的前三个参数一致(fd、filp、on),用于传递应用层的异步通知配置信息。
返回值:通常返回fasync_helper函数的返回值,0表示成功,非0表示失败。
5.5. 异步通知应用层常用函数¶
5.5.1. 文件状态控制¶
5.5.1.1. fcntl函数¶
fcntl函数用于设置设备文件的异步通知相关属性,包括设置文件拥有者、开启异步通知标志等,是应用层开启异步通知的核心函数。
函数原型:
1 | int fcntl(int fd, int cmd, ... /* arg */);
|
参数说明:
fd:设备文件描述符,由open函数返回。
cmd:操作命令,与异步通知相关的常用命令有两个:
F_SETOWN:设置设备文件的拥有者为当前进程,让内核知道向哪个进程发送SIGIO信号。
F_GETFL:获取当前文件的状态标志,用于后续添加异步通知标志。
F_SETFL:设置文件的状态标志,添加FASYNC标志,开启文件的异步通知功能。
arg:可变参数,根据cmd的不同传递不同的值(如F_SETOWN时传递当前进程ID,F_SETFL时传递新的状态标志)。
返回值:根据cmd的不同返回不同结果,F_SETOWN、F_SETFL返回0表示成功,-1表示失败;F_GETFL返回当前文件的状态标志。
5.5.2. 信号注册¶
signal函数用于注册信号处理函数的函数,指定当收到某个信号时,执行对应的处理函数,是应用层接收异步通知信号的前提。
函数原型:
1 | void (*signal(int signum, void (*handler)(int)))(int);
|
参数说明:
signum:需要注册的信号,异步通知中常用SIGIO。
handler:信号处理函数指针,指向当收到该信号时需要执行的函数,函数参数为信号值。
5.6. 异步通知实验¶
本实验在Linux中断子系统实验基础上进行修改,依托Linux异步通知机制,实现“按键触发中断->驱动发送异步通知->应用层响应通知”的完整流程, 实现应用层无操作时休眠、有事件时实时响应的高效设计,理解异步通知在Linux驱动与应用层通信中的核心作用。
本章的示例代码目录为: linux_driver/22_asyncnoti
5.6.1. 设备树插件详解¶
设备树插件完整代码如下,本实验改为双边沿触发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
/ {
fragment@0 {
target-path = "/";
__overlay__ {
led_test: led_test{
compatible = "fire,led_test";
led-gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_HIGH>;
button-gpios= <&gpio1 RK_PB2 GPIO_ACTIVE_HIGH>;
interrupt-parent = <&gpio1>;
interrupts = <RK_PB2 IRQ_TYPE_EDGE_BOTH>;
pinctrl-names = "default";
pinctrl-0 = <&led_button_pin>;
};
};
};
fragment@1 {
target = <&pinctrl>;
__overlay__ {
led_button_test {
led_button_pin: led_button_pin {
rockchip,pins =
<0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>,
<1 RK_PB2 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
};
};
fragment@2 {
target = <&leds>;
__overlay__ {
status = "disabled";
};
};
};
|
第18行,中断触发方式改为IRQ_TYPE_EDGE_BOTH(上升沿+下降沿触发),对应按键按下(下降沿)和松开(上升沿)两种状态,均触发中断并发送异步通知。
5.6.2. 驱动代码详解¶
核心数据结构定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /* 定义 LED 字符设备结构体 */
struct led_chrdev {
/* 字符设备结构体 */
struct cdev dev;
/* 自旋锁 */
spinlock_t spinlock;
/* LED 状态 */
int led_state;
/* LED 的 GPIO 描述符 */
struct gpio_desc *led_gpio;
/* 按钮的 GPIO 描述符 */
struct gpio_desc *button_gpio;
/* 消抖定时器 */
struct timer_list debounce_timer;
/* 中断号 */
int irq;
/* 按键状态 */
int button_state;
/* 异步通知结构体*/
struct fasync_struct *fasync;
};
/* 定义 led_chrdev 结构体指针,用于动态分配内存管理 LED 硬件信息 */
static struct led_chrdev *led_cdev;
|
新增button_state成员,用于记录按键稳定状态,供应用层读取,配合异步通知实现状态同步;
新增异步通知结构体用于内核管理异步通知的应用层进程信息。
驱动初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | static int pdrv_led_probe(struct platform_device *pdev)
{
// 内存分配,从设备树获取资源,字符设备注册(省略重复代码)
/* 初始化自旋锁 */
spin_lock_init(&led_cdev->spinlock);
/* 初始化消抖定时器 */
timer_setup(&led_cdev->debounce_timer, button_debounce_callback, 0);
/* 默认关闭 LED */
led_cdev->led_state = 0;
/* 初始化按键状态,默认处于高电平 */
led_cdev->button_state = 1;
/* 初始化异步通知结构体为NULL */
led_cdev->fasync = NULL;
/* 获取按键GPIO对应的中断号 */
led_cdev->irq = gpiod_to_irq(led_cdev->button_gpio);
if (led_cdev->irq < 0) {
ret = led_cdev->irq;
/* 打印获取中断号失败 */
printk(KERN_ERR "Failed to get button IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
/* 申请中断,下降沿触发 */
ret = devm_request_irq(&pdev->dev, led_cdev->irq, button_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"button_irq", led_cdev);
if (ret < 0) {
/* 打印申请中断失败 */
printk(KERN_ERR "Failed to request IRQ: %d\n", ret);
/* 跳转到错误处理标签 */
goto device_err;
}
return 0;
// 错误处理(省略重复代码)
}
|
第15行:初始化按键状态,默认内部上拉,电平处于高电平,值设置为1;
第18行:初始化异步通知结构体为NULL。
异步通知函数实现
异步通知需实现fasync操作函数(初始化/清理fasync结构体),并在合适时机调用kill_fasync发送信号,通知应用层。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | /* 定义字符设备的文件操作结构体 */
static struct file_operations pdrv_led_fops = {
.owner = THIS_MODULE,
.open = pdrv_led_open,
.release = pdrv_led_release,
.write = pdrv_led_write,
.read = pdrv_led_read,
.fasync = pdrv_led_fasync,
};
/* 异步通知处理函数:初始化/清理异步通知结构体 */
static int pdrv_led_fasync(int fd, struct file *filp, int on)
{
/* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
struct led_chrdev *led_cdev = filp->private_data;
/* 调用内核异步通知辅助函数,初始化/清理fasync结构体 */
return fasync_helper(fd, filp, on, &led_cdev->fasync);
}
/* 消抖定时器回调函数:确认按键稳定按下后翻转LED */
static void button_debounce_callback(struct timer_list *t)
{
/* 反向找到包含这个定时器的设备结构体 led_chrdev 指针 */
struct led_chrdev *led_cdev = from_timer(led_cdev, t, debounce_timer);
/* 用于保存中断状态信息 */
unsigned long flags;
/* 获取自旋锁并保存中断状态 */
spin_lock_irqsave(&led_cdev->spinlock, flags);
/* 读取稳定后的按键电平 */
led_cdev->button_state = gpiod_get_value(led_cdev->button_gpio);
/* 低电平表示按键稳定按下 */
if(led_cdev->button_state == 0){
/* 翻转LED电平状态 */
gpiod_set_value(led_cdev->led_gpio, !gpiod_get_value(led_cdev->led_gpio));
}
/* 无论按下还是松开,都发送信号通知应用层 */
kill_fasync(&led_cdev->fasync, SIGIO, POLL_IN);
/* 释放自旋锁并恢复中断状态 */
spin_unlock_irqrestore(&led_cdev->spinlock, flags);
/* 按键按下打印日志 */
if(led_cdev->button_state == 0){
printk(KERN_INFO "按键已按下,LED状态翻转\n");
} else {
printk(KERN_INFO "按键已松开\n");
}
}
|
fasync函数:是字符设备文件操作的成员,应用层开启异步通知时(设置FASYNC标志),内核会调用该函数,通过fasync_helper初始化fasync结构体,关联应用层进程;
kill_fasync函数:在定时器回调中调用,向应用层发送SIGIO信号,告知应用层“按键状态已变化,可读取”,是异步通知的核心触发动作;
read函数
read函数供应用层读取按键状态,通过自旋锁保护共享数据,保证数据一致性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | static ssize_t pdrv_led_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
/* 从文件结构体的私有数据中获取 led_chrdev 结构体指针 */
struct led_chrdev *led_cdev = filp->private_data;
int ret = 0;
/* 临时变量,用于存储读取的按键状态 */
int btn_state;
/* 按键状态是实时状态,每次读取都重置 ppos,从头开始读最新的数据 */
*ppos = 0;
/* 自旋锁加锁保护读取共享数据 */
spin_lock(&led_cdev->spinlock);
/* 非阻塞IO判断:若为非阻塞模式,且按键未按下,返回-EAGAIN */
if ((filp->f_flags & O_NONBLOCK) && !led_cdev->button_pressed) {
/* 解锁后再返回,避免死锁 */
spin_unlock(&led_cdev->spinlock);
return -EAGAIN;
}
/* 读取当前按键电平 */
btn_state = led_cdev->button_state;
/* 自旋锁解锁 */
spin_unlock(&led_cdev->spinlock);
/* 如果不使用临时变量加锁保护,直接返回led_cdev->button_state,
可能会因为copy_to_user睡眠,如果此时定时器中断触发修改button_state,
数据一致性无法保证 */
/* 将按键状态拷贝到用户空间 */
if (copy_to_user(buf, &btn_state, min(count, sizeof(btn_state)))) {
ret = -EFAULT;
return ret;
}
/* 更新 ppos,反映本次读取的字节数 */
*ppos += min(count, sizeof(btn_state));
/* 返回实际读取的字节数 */
ret = min(count, sizeof(btn_state));
return ret;
}
|
release函数
1 2 3 4 5 6 7 8 9 | static int pdrv_led_release(struct inode *inode, struct file *filp)
{
/* 清理异步通知资源 */
pdrv_led_fasync(-1, filp, 0);
printk("pdrv_led release\r\n");
return 0;
}
|
调用pdrv_led_fasync(-1, filp, 0),清理异步通知资源,避免内存泄漏;
5.6.3. 应用层代码详解¶
应用层需配合驱动实现异步通知响应:注册SIGIO信号处理函数、设置文件拥有者、开启异步通知标志, 实现“休眠等待->信号响应->读取状态”的流程,无需轮询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | #include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
// 定义全局变量,用于存储文件描述符
int fd;
// 信号处理函数,当接收到 SIGIO 信号时调用
void sigio_handler(int signum) {
int ret;
int button_state;
// 读取按键状态
ret = read(fd, &button_state, sizeof(int));
if (ret < 0) {
perror("read error");
return;
}
printf("button_state = %d\n", button_state);
}
int main(int argc, char *argv[]) {
int ret;
if (argc != 2) {
printf("Usage: ./asyncnoti_app /dev/asyncnoti\n");
return -1;
}
// 打开设备文件
fd = open(argv[1], O_RDONLY | O_NONBLOCK);
if (fd < 0) {
printf("ERROR: %s file open failed!\n", argv[1]);
return -1;
}
// 注册信号处理函数
signal(SIGIO, sigio_handler);
// 设置文件的拥有者为当前进程
ret = fcntl(fd, F_SETOWN, getpid());
if (ret < 0) {
perror("fcntl F_SETOWN");
close(fd);
return -1;
}
// 获取当前文件状态标志
int flags = fcntl(fd, F_GETFL);
if (flags < 0) {
perror("fcntl F_GETFL");
close(fd);
return -1;
}
// 开启异步通知功能
ret = fcntl(fd, F_SETFL, flags | FASYNC);
if (ret < 0) {
perror("fcntl F_SETFL");
close(fd);
return -1;
}
// 进入无限循环,等待信号
while (1) {
pause();
}
// 关闭文件描述符
close(fd);
return 0;
}
|
应用层步骤:
第43行:注册SIGIO信号处理函数:通过signal函数,将SIGIO信号与sigio_handler绑定,驱动发送SIGIO时,应用层执行该函数;
第46行:设置文件拥有者:通过fcntl(fd, F_SETOWN, getpid()),告知内核“向当前进程发送该设备的SIGIO信号”;
第62行:开启异步通知:通过fcntl设置FASYNC标志,触发驱动的fasync函数,完成异步通知的初始化;
第71行:休眠等待:通过pause()函数让进程休眠,避免CPU空转,直到收到SIGIO信号后唤醒,执行信号处理函数。
5.6.4. 编译设备树和驱动¶
此部分和Linux中断子系统实验完全一致不作过多说明。
编译得到设备树插件lubancat-led-overlay.dtb、驱动模块asyncnoti.ko和应用程序asyncnoti_app。
5.6.5. 程序运行结果¶
如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。
设备树插件加载方法和设备树插件实验完全一致,加载设备树插件并重启板卡后会发现系统心跳灯默认没有闪烁, 是因为我们使用设备树插件关闭了leds节点,释放了引脚。
使用以下命令加载驱动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #加载驱动
sudo insmod asyncnoti.ko
#信息输出如下
[ 48.115938] led platform driver init
[ 48.116491] led platform driver probe
[ 48.116631] major=236, minor=0
#运行应用程序
sudo ./asyncnoti_app /dev/asyncnoti
#打开新终端或应用程序后台运行,查看程序CPU占用率
ps aux | grep asyncnoti_app
#信息输出如下,
root 14397 0.0 0.0 1788 420 ttyFIQ0 S 17:50 0:00 ./asyncnoti_app /dev/asyncnoti
#14397:进程ID
#第一个0.0:CPU使用率0
#第二个0.0:内存使用率0
|
应用程序启动后,无按键操作时处于休眠状态,CPU占用率为0。
使用杜邦线一端连接按键引脚,另一端多次连接和断开连接GND引脚模拟按键按下和松开,信息打印如下:
1 2 3 4 5 6 7 8 | [ 402.756463] 按键已按下,LED状态翻转
button_state = 0
[ 403.043123] 按键已松开
button_state = 1
[ 403.843110] 按键已按下,LED状态翻转
button_state = 0
[ 404.109927] 按键已松开
button_state = 1
|
每一次按键按下/松开,都会触发中断,驱动发送SIGIO信号,应用层被唤醒,读取并打印按键状态。
5.6.6. 实验注意事项¶
异步通知开启三要素缺一不可:应用层必须完成“注册SIGIO信号处理函数->设置文件拥有者->开启FASYNC标志”,否则无法收到驱动发送的通知;
驱动中fasync函数必须调用fasync_helper:该函数是内核提供的异步通知辅助函数,负责初始化/清理fasync结构体,未调用会导致异步通知失效;
kill_fasync调用时机:需在中断底半部(定时器回调)中调用,确保按键状态稳定后再发送通知,避免应用层读取到不稳定的电平;
中断触发方式匹配:驱动中中断申请的触发方式(IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING)必须与设备树一致,否则中断无法触发,异步通知也无法生效;
应用层文件打开方式:建议用O_NONBLOCK非阻塞方式打开设备,避免read函数阻塞应用层进程(虽有异步通知,但增加非阻塞更稳妥)。