I.MX6U Linux 驱动开发篇---异步通知(信号)实验--- Ubuntu20.04

🎬 渡水无言个人主页渡水无言

专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏

专栏传送门《产品测评专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、异步通知简介

[二、Linux 信号基础及核心API函数](#二、Linux 信号基础及核心API函数)

三、驱动程序编写

[四、测试 APP 代码(asyncnotiApp.c)](#四、测试 APP 代码(asyncnotiApp.c))

五、运行测试

[5.1、编译驱动程序和测试 APP](#5.1、编译驱动程序和测试 APP)

5.2、运行测试

总结


前言

在之前的按键驱动实验中,阻塞 IO实验或者非阻塞 IO实验都是应用程序主动来读取驱动,对于
非阻塞 IO还需要应用不断调用poll/select轮询查询。更高效的方式是驱动就绪后主动告诉应用 。
Linux 提供了异步通知这个机制来完成此功能。


一、异步通知简介

我们首先回顾一下 "中断" 的概念:中断是处理器提供的一种异步机制,配置好中断后,处理器可以去处理其他任务,当中断触发时,会自动执行我们预先注册好的中断服务函数。

比如在裸机 GPIO 按键中断实验中,通过中断方式实现按键控制蜂鸣器时,CPU 不需要轮询检测按键状态,按键按下会自动触发中断。

Linux 应用程序访问驱动设备,也有类似的 "阻塞" 和 "非阻塞" 两种方式:

阻塞 IO:应用调用read时会进入休眠状态,直到设备数据就绪才被唤醒。

非阻塞 IO:应用需要通过poll/select等接口不断轮询,主动查询设备状态。

这两种方式都需要应用主动去查询设备,而更高效的方式,是让设备在就绪时主动通知应用。

这就是 Linux 驱动中的异步通知机制。

"信号" 为此应运而生。信号类似于硬件层面的中断,只不过它工作在软件层面,是对硬件中断的一种模拟。驱动可以在设备就绪时,主动向应用程序发送信号,告知其 "设备已可访问";应用程序捕获到信号后,再读取或写入数据。整个过程就像应用程序收到了驱动发来的 "软件中断",全程无需主动轮询。

异步通知的核心是信号,在arch/xtensa/include/uapi/asm/signal.h文件中定义了 Linux 支持的所有信号,驱动异步通知中最常用的是SIGIO(也叫SIGPOLL)信号。

二、Linux 信号基础及核心API函数

Linux 异步通知的核心是信号 ,驱动通过kill_fasync()发送SIGIO信号,应用程序注册信号处理函数,捕获信号后处理设备数据。

核心API函数:

函数 作用
fasync_helper() 初始化 / 释放fasync_struct结构体,关联进程与驱动
kill_fasync() 驱动向应用程序发送 SIGIO 信号
fcntl() 应用程序设置进程归属、开启异步通知
signal() 应用程序注册 SIGIO 信号处理函数

异步通知依赖信号 (Signal),常用信号如下:

cpp 复制代码
#define SIGINT    2    /* Ctrl+C */
#define SIGKILL   9    /* 强制杀死进程 */
#define SIGUSR1   10   /* 用户自定义信号1 */
#define SIGUSR2   12   /* 用户自定义信号2 */
#define SIGIO     29   /* 异步IO通知(驱动常用) */
#define SIGPOLL   SIGIO

我们使用中断的时候需要设置中断处理函数,同样的,如果要在应用程序中使用信号,那么就必须设置信号所使用的信号处理函数,在应用程序中使用 signal 函数来设置指定信号的处理函数,signal 函数原型如下所示:

cpp 复制代码
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

函数参数和返回值含义如下:
signum :要设置处理函数的信号。
handler 信号的处理函数。
返回值: 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR 。

注意:之前使用的"kill -9 PID"杀死指定进程的方法就是向指定的进程(PID)发送SIGKILL 这个信号。当按下键盘上的 CTRL+C 组合键以后会向当前正在占用终端的应用程序发出 SIGINT 信号,SIGINT 信号默认的动作是关闭当前应用程序。

三、驱动程序编写

这里我们复用非阻塞 IO 实验的设备树与驱动框架,无需修改设备树。

驱动程序也在上一期linux驱动实验noblockio.c的基础上完成,在其中加入异步通知相关内容即可,当按键按下以后驱动程序向应用程序发送SIGIO信号,应用程序获取到 SIGIO 信号以后读取并且打印出按键值。

驱动实现步骤:

设备结构体中添加struct fasync_struct *async_queue。

实现fasync操作函数,调用fasync_helper。

设备就绪时(按键按下),调用kill_fasync发送信号(相当于产生中断)。

release函数中释放异步通知资源。

cpp 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/sched.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fcntl.h>    // 异步通知必须头文件

#define IMX6UIRQ_CNT       1        // 设备号个数
#define IMX6UIRQ_NAME      "asyncnoti"  // 设备名
#define KEY0VALUE          0X01     // 按键值
#define INVAKEY            0XFF     // 无效按键值
#define KEY_NUM            1        // 按键数量

/* 按键描述结构体 */
struct irq_keydesc {
    int gpio;               // GPIO编号
    int irqnum;             // 中断号
    unsigned char value;    // 按键值
    char name[10];          // 名字
    irqreturn_t (*handler)(int, void *); // 中断服务函数
};

/* 设备结构体 */
struct imx6uirq_dev {
    dev_t devid;            // 设备号
    struct cdev cdev;       // cdev
    struct class *class;    // 类
    struct device *device;  // 设备
    int major;              // 主设备号
    int minor;              // 次设备号
    struct device_node *nd; // 设备节点
    struct irq_keydesc keydesc[KEY_NUM]; // 按键描述
    struct timer_list timer; // 定时器(消抖)
    unsigned char curkeynum; // 当前按键号

    atomic_t keyvalue;      // 有效按键值
    atomic_t releasekey;    // 按键是否释放完成
    struct fasync_struct *async_queue; // 异步通知核心结构体
};

struct imx6uirq_dev imx6uirq; // 全局设备实例

/* 定时器服务函数:按键消抖 */
void timer_function(unsigned long arg)
{
    unsigned char value;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
    struct irq_keydesc *keydesc = &dev->keydesc[dev->curkeynum];

    value = gpio_get_value(keydesc->gpio);
    if(value == 0) { // 按键按下
        atomic_set(&dev->keyvalue, keydesc->value);
    } else { // 按键释放
        atomic_set(&dev->keyvalue, 0X80 | keydesc->value);
        atomic_set(&dev->releasekey, 1); // 标记按键释放完成
    }

    /* 异步通知:发送SIGIO信号给应用程序 */
    if(atomic_read(&dev->releasekey)) {
        if(dev->async_queue)
            kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
    }
}

/* 中断服务函数 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;
    dev->curkeynum = 0;
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(10)); // 10ms消抖
    return IRQ_RETVAL(IRQ_HANDLED);
}

/* 驱动open函数 */
static int imx6uirq_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &imx6uirq; // 私有数据指向设备结构体
    return 0;
}

/* 驱动read函数 */
static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int ret = 0;
    unsigned char keyvalue;
    unsigned char releasekey;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

    keyvalue = atomic_read(&dev->keyvalue);
    releasekey = atomic_read(&dev->releasekey);

    if(releasekey) { // 按键有效
        ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue));
        atomic_set(&dev->releasekey, 0); // 清除标志
    } else {
        return -EINVAL;
    }
    return 0;
}

/* poll函数:非阻塞IO */
static unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;

    if(atomic_read(&dev->releasekey))
        mask = POLLIN | POLLRDNORM;
    return mask;
}

/* 异步通知fasync函数:核心实现 */
static int imx6uirq_fasync(int fd, struct file *filp, int on)
{
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
    return fasync_helper(fd, filp, on, &dev->async_queue);
}

/* 驱动release函数:释放异步资源 */
static int imx6uirq_release(struct inode *inode, struct file *filp)
{
    return imx6uirq_fasync(-1, filp, 0); // 释放fasync_struct
}

/* 设备操作函数集 */
static struct file_operations imx6uirq_fops = {
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .read = imx6uirq_read,
    .poll = imx6uirq_poll,
    .fasync = imx6uirq_fasync,    // 注册异步通知函数
    .release = imx6uirq_release,  // 注册释放函数
};

/* 按键初始化 */
static int keyio_init(void)
{
    int ret = 0;
    struct irq_keydesc *keydesc;

    /* 设备树解析GPIO、中断等(省略,复用非阻塞IO代码) */
    imx6uirq.nd = of_find_node_by_path("/key");
    keydesc = &imx6uirq.keydesc[0];
    keydesc->gpio = of_get_named_gpio(imx6uirq.nd, "key-gpio", 0);
    keydesc->irqnum = gpio_to_irq(keydesc->gpio);
    keydesc->handler = key0_handler;
    keydesc->value = KEY0VALUE;
    sprintf(keydesc->name, "KEY0");

    /* 申请中断 */
    ret = request_irq(keydesc->irqnum, keydesc->handler,
                      IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
                      keydesc->name, &imx6uirq);

    /* 初始化定时器 */
    init_timer(&imx6uirq.timer);
    imx6uirq.timer.function = timer_function;
    imx6uirq.timer.data = (unsigned long)&imx6uirq;

    return 0;
}

/* 驱动入口函数 */
static int __init imx6uirq_init(void)
{
    /* 注册字符设备(省略,复用标准框架) */
    alloc_chrdev_region(&imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
    imx6uirq.major = MAJOR(imx6uirq.devid);
    imx6uirq.minor = MINOR(imx6uirq.devid);

    cdev_init(&imx6uirq.cdev, &imx6uirq_fops);
    cdev_add(&imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

    imx6uirq.class = class_create(THIS_MODULE, IMX6UIRQ_NAME);
    imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);

    /* 初始化按键、原子变量 */
    atomic_set(&imx6uirq.keyvalue, INVAKEY);
    atomic_set(&imx6uirq.releasekey, 0);
    keyio_init();
    return 0;
}

/* 驱动出口函数 */
static void __exit imx6uirq_exit(void)
{
    /* 释放中断、定时器、设备 */
    int i = 0;
    for(i = 0; i < KEY_NUM; i++) {
        free_irq(imx6uirq.keydesc[i].irqnum, &imx6uirq);
    }
    del_timer_sync(&imx6uirq.timer);
    cdev_del(&imx6uirq.cdev);
    unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
    device_destroy(imx6uirq.class, imx6uirq.devid);
    class_destroy(imx6uirq.class);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE("GPL");

头文件:新增#include <linux/fcntl.h>,异步通知必备。

设备结构体:添加struct fasync_struct *async_queue。

定时器函数:按键有效时调用kill_fasync发送 SIGIO 信号;

fasync 函数:调用fasync_helper初始化异步通知;

release 函数:释放异步通知资源;

操作函数集:注册fasync和release函数

四、测试 APP 代码(asyncnotiApp.c)

设置 SIGIO 信号的处理函数为 sigio_signal_func,当驱动程序向应用程序发送 SIGIO 信号以后 sigio_signal_func 函数就会执行。sigio_signal_func 函数内容很简单,就是通过 read 函数读取按键值。

即实现 SIGIO 信号捕获,读取并打印按键值:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>

static int fd = 0; // 设备文件描述符

/* SIGIO信号处理函数 */
static void sigio_signal_func(int signum)
{
    int err = 0;
    unsigned int keyvalue = 0;

    // 读取按键值
    err = read(fd, &keyvalue, sizeof(keyvalue));
    if(err >= 0) {
        printf("sigio信号触发!按键值 = %d\r\n", keyvalue);
    }
}

/* 主函数 */
int main(int argc, char *argv[])
{
    int flags = 0;
    char *filename;

    if(argc != 2) {
        printf("用法:./asyncnotiApp /dev/asyncnoti\r\n");
        return -1;
    }

    filename = argv[1];
    // 打开设备
    fd = open(filename, O_RDWR);
    if(fd < 0) {
        printf("打开设备失败!\r\n");
        return -1;
    }

    // 1. 注册SIGIO信号处理函数
    signal(SIGIO, sigio_signal_func);

    // 2. 设置进程归属:告诉内核当前进程接收SIGIO信号
    fcntl(fd, F_SETOWN, getpid());

    // 3. 开启异步通知功能
    flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | FASYNC);

    // 循环等待信号
    while(1) {
        sleep(2);
    }

    close(fd);
    return 0;
}

测试APP(应用程序)核心逻辑

  1. 注册SIGIO信号处理函数,信号触发时读取按键值;
  2. 通过fcntl(F_SETOWN)将进程 PID 告知内核;
  3. 通过fcntl(F_SETFL)开启FASYNC异步模式;
  4. 死循环等待信号,CPU 占用率极低。

五、运行测试

5.1、编译驱动程序和测试 APP

编写 Makefile 文件,本次实验的 Makefile 文件和之前的实验基本一样,只是将 obj-m 变量的值改为asyncnoti.o,Makefile 内容如下所示:

cpp 复制代码
KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2
CURRENT_PATH := $(shell pwd)

obj-m := asyncnoti.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第 4 行,设置 obj-m 变量的值为asyncnoti.o。

输入如下命令编译出驱动模块文件:

cpp 复制代码
make -j32

编译成功以后就会生成一个名为"asyncnoti.ko"的驱动模块文件。

编译测试 APP

输入如下命令编译测试asyncnotiApp.c这个测试程序:

cpp 复制代码
arm-linux-gnueabihf-gcc  asyncnotiApp.c -o   asyncnotiApp

编译成功以后就会生成asyncnotiApp这个应用程序。

5.2、运行测试

将上一小节编译出来的asyncnoti.ko 和asyncnotiApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。

cpp 复制代码
sudo cp asyncnoti.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f
cpp 复制代码
sudo cp  asyncnotiApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

进入到目录 lib/modules/4.1.15 中,输入如下命令加载asyncnoti.ko 驱动模块:

cpp 复制代码
depmod //第一次加载驱动的时候需要运行此命令
modprobe noblockio.ko  //加载驱动

驱动加载成功以后使用如下命令打开asyncnotiApp这个测试APP,并且以后台模式运行

cpp 复制代码
./asyncnotiApp /dev/asyncnoti &

按下开发板上的 KEY0 键,终端就会输出按键值,如下图所示:


从上图中 可以看出,捕获到 SIGIO 信号,并且按键值获取成功,大家可以自行以后台模式运行 asyncnotiApp ,查看一下这个应用程序的 CPU 使用率。如果要卸载驱动的话输入如下命令即可:

cpp 复制代码
rmmod asyncnoti.ko

总结

本实验完整实现了 Linux 驱动异步通知机制。

相关推荐
optimistic_chen6 小时前
【AI Agent 全栈开发】MCP
java·linux·运维·人工智能·ai编程·mcp
charlie1145141916 小时前
嵌入式Linux嵌入式Linux驱动开发:板级DTS实操与完整实战演练——从修改设备树到点亮LED的完整闭环
linux·运维·驱动开发
lularible13 小时前
HSM技术精讲(1.4):当信道不再可信——密码学的诞生
安全·开源·密码学·嵌入式
匆匆那年96714 小时前
VSCode 远程 Linux 使用Codex
linux·ide·vscode
SWAGGY..16 小时前
Linux系统编程:(七)Makefile入门:轻松掌握编译自动化
linux·运维·自动化
开开心心就好16 小时前
免费流畅的远程控制实用工具
linux·运维·服务器·网络·智能手机·excel
黑猫学长呀18 小时前
存储宝典第2篇:盲封TT wafer是什么意思?
linux·嵌入式硬件·项目·芯片·ufs·晶圆·产测
Strugglingler18 小时前
【Linux 用户态操作 UART】
linux·uart
代码熬夜敲Q18 小时前
ENSP 网络工程实验
linux·运维·服务器