
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 : 《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)
前言
在之前的按键驱动实验中,阻塞 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(应用程序)核心逻辑
- 注册
SIGIO信号处理函数,信号触发时读取按键值;- 通过
fcntl(F_SETOWN)将进程 PID 告知内核;- 通过
fcntl(F_SETFL)开启FASYNC异步模式;- 死循环等待信号,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 驱动异步通知机制。