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数据传输、设备状态读取等操作。

异步通知的整体执行流程图如下:

../_images/subsystem_asyncnoti_0.jpg

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. 设备树插件详解

设备树插件完整代码如下,本实验改为双边沿触发:

设备树插件(位于linux_driver/18_irq_subsystem/lubancat-led-overlay.dts)
 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. 驱动代码详解

核心数据结构定义

核心数据结构定义(位于linux_driver/22_asyncnoti/asyncnoti.c)
 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成员,用于记录按键稳定状态,供应用层读取,配合异步通知实现状态同步;

  • 新增异步通知结构体用于内核管理异步通知的应用层进程信息。

驱动初始化

驱动初始化(位于linux_driver/22_asyncnoti/asyncnoti.c)
 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发送信号,通知应用层。

异步通知函数(位于linux_driver/22_asyncnoti/asyncnoti.c)
 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函数供应用层读取按键状态,通过自旋锁保护共享数据,保证数据一致性。

pdrv_led_read函数(位于linux_driver/22_asyncnoti/asyncnoti.c)
 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函数

pdrv_led_release函数(位于linux_driver/22_asyncnoti/asyncnoti.c)
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信号处理函数、设置文件拥有者、开启异步通知标志, 实现“休眠等待->信号响应->读取状态”的流程,无需轮询。

asyncnoti_app.c(位于linux_driver/22_asyncnoti/asyncnoti_app.c)
 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. 实验注意事项

  1. 异步通知开启三要素缺一不可:应用层必须完成“注册SIGIO信号处理函数->设置文件拥有者->开启FASYNC标志”,否则无法收到驱动发送的通知;

  2. 驱动中fasync函数必须调用fasync_helper:该函数是内核提供的异步通知辅助函数,负责初始化/清理fasync结构体,未调用会导致异步通知失效;

  3. kill_fasync调用时机:需在中断底半部(定时器回调)中调用,确保按键状态稳定后再发送通知,避免应用层读取到不稳定的电平;

  4. 中断触发方式匹配:驱动中中断申请的触发方式(IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING)必须与设备树一致,否则中断无法触发,异步通知也无法生效;

  5. 应用层文件打开方式:建议用O_NONBLOCK非阻塞方式打开设备,避免read函数阻塞应用层进程(虽有异步通知,但增加非阻塞更稳妥)。