一、异步通知
1. 异步通知简介
阻塞和非阻塞两种方式都是需要应用程序去主动查询设备的使用情况。
异步通知类似于驱动可以主动报告自己可以访问,应用程序获取信号后会从驱动设备中读取或写入数据。
异步通知最核心的就是信号
:
#define SIGHUP 1 /* 终端挂起或控制进程终止 */
#define SIGINT 2 /* 终端中断(Ctrl+C 组合键) */
#define SIGQUIT 3 /* 终端退出(Ctrl+\组合键) */
#define SIGILL 4 /* 非法指令 */
#define SIGTRAP 5 /* debug 使用,有断点指令产生 */
#define SIGABRT 6 /* 由 abort(3)发出的退出指令 */
#define SIGIOT 6 /* IOT 指令 */
#define SIGBUS 7 /* 总线错误 */
#define SIGFPE 8 /* 浮点运算错误 */
#define SIGKILL 9 /* 杀死、终止进程 */
#define SIGUSR1 10 /* 用户自定义信号 1 */
#define SIGSEGV 11 /* 段违例(无效的内存段) */
#define SIGUSR2 12 /* 用户自定义信号 2 */
#define SIGPIPE 13 /* 向非读管道写入数据 */
#define SIGALRM 14 /* 闹钟 */
#define SIGTERM 15 /* 软件终止 */
#define SIGSTKFLT 16 /* 栈异常 */
#define SIGCHLD 17 /* 子进程结束 */
#define SIGCONT 18 /* 进程继续 */
#define SIGSTOP 19 /* 停止进程的执行,只是暂停 */
#define SIGTSTP 20 /* 停止进程的运行(Ctrl+Z 组合键) */
#define SIGTTIN 21 /* 后台进程需要从终端读取数据 */
#define SIGTTOU 22 /* 后台进程需要向终端写数据 */
#define SIGURG 23 /* 有"紧急"数据 */
#define SIGXCPU 24 /* 超过 CPU 资源限制 */
#define SIGXFSZ 25 /* 文件大小超额 */
#define SIGVTALRM 26 /* 虚拟时钟信号 */
#define SIGPROF 27 /* 时钟信号描述 */
#define SIGWINCH 28 /* 窗口大小改变 */
#define SIGIO 29 /* 可以进行输入/输出操作 */
#define SIGPOLL SIGIO
#define SIGPWR 30 /* 断点重启 */
#define SIGSYS 31 /* 非法的系统调用 */
#define SIGUNUSED 31 /* 未使用信号 */
其中 9 和 19 是不能被忽略的,这些信号相当于中断号,不同的中断号代表不同的中断,不同的中断又可以实现不同的功能。
使用中断的时候需要设置中断处理函数,那么应用程序中使用了信号,也要有信号处理函数,在应用程序中使用 signal
函数来设置信号的处理函数:
/** @description : 设置指定信号的处理函数* @param - signum : 要设置处理函数的信号* @param - handler : 信号的处理函数* @return : 设置成功的话返回信号的前一个处理函数,设置失败的话返回 SIG_ERR*/
sighandler_t signal(int signum, sighandler_t handler);
这里的信号处理原型为:
typedef void (*sighandler_t)(int)
之前使用的 kill -9 PID
就是指定进程发送 SIGKILL 这个信号。当我们按下 CTRL + C
的时候,会向当前正在占用终端的应用程序发送 SIGINT(2)信号,最终中断终端。
在 linux/atk-mpl 文件夹下,创建 signaltest.c,并输入以下代码:
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>/* 信号处理函数 */
void signalint_handler(int num)
{printf("\r\nSIGINT signal!\r\n");exit(0);
}int main(void)
{signal(SIGINT, signalint_handler);while(1);return 0;
}/* 总体流程:首先自己设置信号处理函数,之后应用程序开启,按下CTRL+C向signaltest发送SIGINT信号以后signalint_handler函数就会执行,此函数会先输出SIGINT signal!后,exit函数会关闭signaltest的应用程序。*/
输入命令编译 signaltest.c:
gcc signaltest.c -o signaltest
输入命令执行应用程序 signaltest:
./signaltest
按下 CTRL+ C
后,可以看到:
2. 驱动中的信号处理
① fasync_struct 结构体
如果在驱动中使用信号,首先先定义一个fasync_struct
结构体变量,一般将这个结构体放在 xxx_dev 结构体中,例如:
/* key设备结构体 */
struct key_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */struct device_node *nd; /* 设备节点 */int key_gpio; /* key所使用的GPIO编号 */struct timer_list timer; /* 按键值 */int irq_num; /* 中断号 */atomic_t status; /* 按键状态 */wait_queue_head_t r_wait; /* 读等待队列头 */struct fasync_struct *async_queue; /* fasync_struct结构体 */
};
② fasync 函数
函数负责将用户空间应用程序的进程 ID 添加到驱动程序相应文件的异步通知队列中。
如果要使用异步通知,需要在驱动程序里实现 file_operation
操作集中的 fasync
函数:
int (*fasync) (int fd, struct file *filp, int on)
驱动中的 fasync 函数示例如下:
struct xxx_dev {......struct fasync_struct *async_queue; /* 异步相关结构体 */
};static int xxx_fasync(int fd, struct file *filp, int on) // fasync 函数
{struct xxx_dev *dev = (xxx_dev)filp->private_data; // 私有数据if (fasync_helper(fd, filp, on, &dev->async_queue) < 0) // 这里的fasync_helper其实是被fasync调用的return -EIO;return 0;
}static struct file_operations xxx_ops = {.......fasync = xxx_fasync, // 只要是 file_operations里的函数,都要这样的形势走一遍......
};
如果需要关闭驱动文件,那么需要在 release
函数里面释放 fasync_struct,示例如下:
static int xxx_release(struct inode *inode, struct file *filp)
{return xxx_fasync(-1, filp, 0); /* 删除异步通知 */
}
static struct file_operations xxx_ops = {.......release = xxx_release, // 只要用到了file_operations就要这样
};
③ kill_fasync 函数
/** @description : 当设备可以访问的时候,驱动程序需要向应用程序发出信号,此函数负责发送指定的信号* @param - fp : 要操作的 fasync_struct* @param - sig : 要发送的信号* @param - band : 可读时设置为 POLL_IN,可写时设置为 POLL_OUT* @return : 无*/
void kill_fasync(struct fasync_struct **fp, int sig, int band);
3. 应用程序对异步通知的处理
总共包含三步:
① 注册信号处理函数
这里的处理函数需要自己设置,并且最后传给 signal
函数。
② 将本应用程序的进程号告诉内核
使用 fcntl(fd, F_SETOWN, getpid())
将本应用程序的进程号告诉给内核。
③ 开启异步通知
flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
重点就是通过 fcntl 函数设置进程状态为 FASYNC,经过这一步,驱动程序中的 fasync 函数就会执行。
当用户空间应用程序调用 fcntl
系统调用并设置 F_SETFL
命令以及 FASYNC
标志时,内核会调用驱动程序中的 fasync
函数。
二、程序编写
在 /linux/atk-mpl/Drivers 文件夹下创建 16_asynchronization 文件夹,并创建 asynchronization.c 文件,并输入以下代码:
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.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/semaphore.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/fcntl.h>#define KEY_CNT 1 /* 设备号个数 */
#define KEY_NAME "key" /* 名字 *//* 定义按键状态 */
enum key_status {KEY_PRESS = 0, // 按键按下KEY_RELEASE, // 按键松开KEY_KEEP, // 按键状态保持
};/* key设备结构体 */
struct key_dev{dev_t devid; /* 设备号 */struct cdev cdev; /* cdev */struct class *class; /* 类 */struct device *device; /* 设备 */struct device_node *nd; /* 设备节点 */int key_gpio; /* key所使用的GPIO编号 */struct timer_list timer; /* 按键值 */int irq_num; /* 中断号 */ atomic_t status; /* 按键状态 */wait_queue_head_t r_wait; /* 读等待队列头 */struct fasync_struct *async_queue; /* fasync_struct结构体 */
};static struct key_dev key; /* 按键设备 */static irqreturn_t key_interrupt(int irq, void *dev_id) // 中断处理函数
{/* 按键防抖处理,开启定时器延时15ms */mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));return IRQ_HANDLED;
}/** @description : 初始化按键IO,open函数打开驱动的时候* 初始化按键所使用的GPIO引脚。* @param : 无* @return : 无*/
static int key_parse_dt(void)
{int ret;const char *str;/* 设置LED所使用的GPIO *//* 1、获取设备节点:key */key.nd = of_find_node_by_path("/key");if(key.nd == NULL) {printk("key node not find!\r\n");return -EINVAL;}/* 2.读取status属性 */ret = of_property_read_string(key.nd, "status", &str);if(ret < 0) return -EINVAL;if (strcmp(str, "okay"))return -EINVAL;/* 3、获取compatible属性值并进行匹配 */ret = of_property_read_string(key.nd, "compatible", &str);if(ret < 0) {printk("key: Failed to get compatible property\n");return -EINVAL;}if (strcmp(str, "alientek,key")) {printk("key: Compatible match failed\n");return -EINVAL;}/* 4、 获取设备树中的gpio属性,得到KEY0所使用的KYE编号 */key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);if(key.key_gpio < 0) {printk("can't get key-gpio");return -EINVAL;}/* 5 、获取GPIO对应的中断号 */key.irq_num = irq_of_parse_and_map(key.nd, 0);if(!key.irq_num){return -EINVAL;}printk("key-gpio num = %d\r\n", key.key_gpio);return 0;
}static int key_gpio_init(void) // gpio初始化
{int ret;unsigned long irq_flags;ret = gpio_request(key.key_gpio, "KEY0"); // 申请gpioif (ret) {printk(KERN_ERR "key: Failed to request key-gpio\n");return ret;} /* 将GPIO设置为输入模式 */gpio_direction_input(key.key_gpio);/* 获取设备树中指定的中断触发类型 */irq_flags = irq_get_trigger_type(key.irq_num);if (IRQF_TRIGGER_NONE == irq_flags)irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;/* 申请中断 */ret = request_irq(key.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);if (ret) {gpio_free(key.key_gpio);return ret;}return 0;
}static void key_timer_function(struct timer_list *arg) // 定时器处理函数
{static int last_val = 1;int current_val;/* 读取按键值并判断按键当前状态 */current_val = gpio_get_value(key.key_gpio);if (0 == current_val && last_val){atomic_set(&key.status, KEY_PRESS); // 按下wake_up_interruptible(&key.r_wait); // 唤醒r_wait队列头中的所有队列if(key.async_queue)kill_fasync(&key.async_queue, SIGIO, POLL_IN); // }else if (1 == current_val && !last_val) {atomic_set(&key.status, KEY_RELEASE); // 松开wake_up_interruptible(&key.r_wait); // 唤醒r_wait队列头中的所有队列if(key.async_queue)kill_fasync(&key.async_queue, SIGIO, POLL_IN);}elseatomic_set(&key.status, KEY_KEEP); // 状态保持last_val = current_val;
}/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做private_data的成员变量* 一般在open的时候将private_data指向设备结构体。* @return : 0 成功;其他 失败*/
static int key_open(struct inode *inode, struct file *filp)
{return 0;
}/** @description : 从设备读取数据 * @param – filp : 要打开的设备文件(文件描述符)* @param – buf : 返回给用户空间的数据缓冲区* @param – cnt : 要读取的数据长度* @param – offt : 相对于文件首地址的偏移* @return : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t key_read(struct file *filp, char __user *buf,size_t cnt, loff_t *offt)
{int ret;if (filp->f_flags & O_NONBLOCK) { // 非阻塞方式访问if(KEY_KEEP == atomic_read(&key.status))return -EAGAIN;} else { // 阻塞方式访问/* 加入等待队列,当有按键按下或松开动作发生时,才会被唤醒 */ret = wait_event_interruptible(key.r_wait, KEY_KEEP != atomic_read(&key.status));if(ret)return ret;}/* 将按键状态信息发送给应用程序 */ret = copy_to_user(buf, &key.status, sizeof(int));/* 状态重置 */atomic_set(&key.status, KEY_KEEP);return ret;
}/** @description : fasync函数,用于处理异步通知* @param – fd : 文件描述符* @param – filp : 要打开的设备文件(文件描述符)* @param – on : 模式* @return : 负数表示函数执行失败*/
static int key_fasync(int fd, struct file *filp, int on)
{return fasync_helper(fd, filp, on, &key.async_queue);
}/** @description : 向设备写数据 * @param - filp : 设备文件,表示打开的文件描述符* @param - buf : 要写给设备写入的数据* @param - cnt : 要写入的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 写入的字节数,如果为负值,表示写入失败*/
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}/** @description : 关闭/释放设备* @param - filp : 要关闭的设备文件(文件描述符)* @return : 0 成功;其他 失败*/
static int key_release(struct inode *inode, struct file *filp)
{return key_fasync(-1, filp, 0);
}/** @description : poll函数,用于处理非阻塞访问* @param - filp : 要打开的设备文件(文件描述符)* @param - wait : 等待列表(poll_table)* @return : 设备或者资源状态,*/
static unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{unsigned int mask = 0;poll_wait(filp, &key.r_wait, wait);if(KEY_KEEP != atomic_read(&key.status)) // 按键按下或松开动作发生mask = POLLIN | POLLRDNORM; // 返回PLLINreturn mask;
}/* 设备操作函数 */
static struct file_operations key_fops = {.owner = THIS_MODULE,.open = key_open,.read = key_read,.write = key_write,.release = key_release,.poll = key_poll,.fasync = key_fasync,
};/** @description : 驱动入口函数* @param : 无* @return : 无*/
static int __init mykey_init(void)
{int ret;/* 初始化等待队列头 */init_waitqueue_head(&key.r_wait);/* 初始化按键状态 */atomic_set(&key.status, KEY_KEEP);/* 设备树解析 */ret = key_parse_dt();if(ret)return ret;/* GPIO 中断初始化 */ret = key_gpio_init();if(ret)return ret;/* 注册字符设备驱动 *//* 1、创建设备号 */ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME); /* 申请设备号 */if(ret < 0) {pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);goto free_gpio;}/* 2、初始化cdev */key.cdev.owner = THIS_MODULE;cdev_init(&key.cdev, &key_fops);/* 3、添加一个cdev */ret = cdev_add(&key.cdev, key.devid, KEY_CNT);if(ret < 0)goto del_unregister;/* 4、创建类 */key.class = class_create(THIS_MODULE, KEY_NAME);if (IS_ERR(key.class)) {goto del_cdev;}/* 5、创建设备 */key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);if (IS_ERR(key.device)) {goto destroy_class;}/* 6、初始化timer,设置定时器处理函数,还未设置周期,所有不会激活定时器 */timer_setup(&key.timer, key_timer_function, 0);return 0;destroy_class:device_destroy(key.class, key.devid);
del_cdev:cdev_del(&key.cdev);
del_unregister:unregister_chrdev_region(key.devid, KEY_CNT);
free_gpio:free_irq(key.irq_num, NULL);gpio_free(key.key_gpio);return -EIO;
}/** @description : 驱动出口函数* @param : 无* @return : 无*/
static void __exit mykey_exit(void)
{/* 注销字符设备驱动 */cdev_del(&key.cdev);/* 删除cdev */unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */del_timer_sync(&key.timer); /* 删除timer */device_destroy(key.class, key.devid);/*注销设备 */class_destroy(key.class); /* 注销类 */free_irq(key.irq_num, NULL); /* 释放中断 */gpio_free(key.key_gpio); /* 释放IO */
}module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");
编写 asynchronizationApp.c 文件:
#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>static int fd;/** SIGIO信号处理函数* @param – signum : 信号值* @return : 无*/
static void sigio_signal_func(int signum)
{unsigned int key_val = 0;read(fd, &key_val, sizeof(unsigned int));if (0 == key_val)printf("Key Press\n");else if (1 == key_val)printf("Key Release\n");printf("SIGIO signal!\r\n");
}/** @description : main主程序* @param – argc : argv数组元素个数* @param – argv : 具体参数* @return : 0 成功;其他 失败*/
int main(int argc, char *argv[])
{int flags = 0;/* 判断传参个数是否正确 */if(2 != argc) {printf("Usage:\n""\t./asyncKeyApp /dev/key\n");return -1;}/* 打开设备 */fd = open(argv[1], O_RDONLY | O_NONBLOCK); // 非阻塞O_NONBLOCKif(0 > fd) {printf("ERROR: %s file open failed!\n", argv[1]);return -1;}/* 设置信号SIGIO的处理函数 */signal(SIGIO, sigio_signal_func); // signal函数,用于设置信号的处理函数fcntl(fd, F_SETOWN, getpid()); // 将当前进程的进程号告诉给内核flags = fcntl(fd, F_GETFD); // 获取当前的进程状态fcntl(fd, F_SETFL, flags | FASYNC); // 设置进程启用异步通知功能/* 循环轮询读取按键数据 */while(1) {sleep(2);}/* 关闭设备 */close(fd);return 0;
}
三、运行测试
首先先编写 Makefile 文件:
KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)obj-m := asynchronization.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modulesclean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
之后编译 asynchronization.c 和 asynchronizationApp 文件:
make
arm-none-linux-gnueabihf-gcc asynchronizationApp.c -o asynchronization
将编译好的 asynchronizationApp 和 asynchronization.ko 复制到:
sudo cp asynchronization asynchronization.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/ -f
开启开发板,输入以下命令:
cd lib/modules/5.4.31/
depmod
modprobe asynchronization.ko
测试 App 应用程序:
./asynchronization /dev/key
按下 Key0,则会出现:
每当按下 KEY0 的时候,就会捕获到 SIGIO信号,并且按键获取成功。
卸载驱动:rmmod asynchronization.ko
总结
概念:
其实异步通知就是让驱动设备空出来的时候发送一个信号给用户进程空间,让进程空间知道,哦原来可以使用这个设备了,一般搭配 非阻塞+异步通知使用,效果更佳。
异步通知的使用方法需要掌握,他们分为驱动和用户进程空间:
在内核空间中:① 定义 fasync_struct
结构体;② 编写fasync
函数;③ 最后当设备空出来的时候指定信号 kill_fasync
。
在用户空间中:① 编写信号处理函数,并且最后把信号处理函数传给 signal
函数;② 本程序进程号告诉内核 fcntl(fd, F_SETOWN, getpid())
;③ 开启异步通知:flags = fcntl(fd, F_GETFL); /* 获取当前的进程状态 */
、fcntl(fd, F_SETFL, flags | FASYNC); /* 开启当前进程异步通知功能 */
;
代码:其实跟上一节差不太多,在按键按下/松开的时候设置kill_fasync
发送信号给用户空间。有个小tip,只要使用了file_operations函数里的函数,都要在 static struct file_operations key_fops = {...}
这个函数下写下设备操作函数。测试代码中按照上述用户空间编写即可。