Linux驱动开发十二.异步通知
我们在前面通过阻塞和非阻塞的访问方式完成了用户态APP和驱动文件之间进行交互。但是这两种方式都是通过应用程序主动去读取驱动程序,对于非阻塞模式来说是通过poll函数不断的轮询,阻塞模式就是把进程挂起,直到中断或其他事件发生重新启动进程。今天我们讲一种新的方法:让驱动主动向应用程序发出通知,报告自己可以被访问,然后应用程序和驱动程序进行数据交互。这个过程相当于驱动从软件方面产生了一个面相APP程序的中断,用户APP响应中断去执行数据交互的流程。这个中断的产生和响应的过程就是异步通知。
异步通知
我们先回顾一下外部中断的原理以及处理流程:中断是处理器提供的一种异步机制,我们在配置好中断就可以去做别的事情了,一旦中断事件发生就会触发我们绑定的中断服务函数,函数去做相应的处理内容。比如我们前面一直使用的按键驱动,就是不管程序运行在什么状态,一旦按键被按下就触发了中断,Linux内核响应中断并打印相关信息。同时Linux用户态的APP通过阻塞或非阻塞的模式来访问驱动设备文件,阻塞模式在read函数被调用时使应用程序进入休眠状态,在中断产生时通过wake_up()函数唤醒进程后读取按键数据;而非阻塞模式是通过poll函数不停查询设备状态,在可以被调用的时候读取数据。所以这两种方式都是从APP程序主动发出请求去查询底层文件状态。我们现在就需要另一种模式:用户态APP只管自己运行,在按键中断产生时,程序产生软中断告诉APP,这时候APP响应中断并执行中断处理函数。
上面说的过程就是“信号”,信号其实可以算一种软中断,就是通过软件模拟中断的过程,这个过程也叫做异步通知。阻塞、非阻塞和异步通知是针对三个不同应用场合的使用方法。这三种模式没有优劣之分,我们只需要根据实际场合选择合适的方法。异步通知的核心部分就是信号,在arch/arm/include/uapi/asm/signal.h路径下内核定义了我们使用的I.MX6U支持的所有异步通知的信号
1 #define SIGHUP 1 2 #define SIGINT 2 3 #define SIGQUIT 3 4 #define SIGILL 4 5 #define SIGTRAP 5 6 #define SIGABRT 6 7 #define SIGIOT 6 8 #define SIGBUS 7 9 #define SIGFPE 8 10 #define SIGKILL 9 11 #define SIGUSR1 10 12 #define SIGSEGV 11 13 #define SIGUSR2 12 14 #define SIGPIPE 13 15 #define SIGALRM 14 16 #define SIGTERM 15 17 #define SIGSTKFLT 16 18 #define SIGCHLD 17 19 #define SIGCONT 18 20 #define SIGSTOP 19 21 #define SIGTSTP 20 22 #define SIGTTIN 21 23 #define SIGTTOU 22 24 #define SIGURG 23 25 #define SIGXCPU 24 26 #define SIGXFSZ 25 27 #define SIGVTALRM 26 28 #define SIGPROF 27 29 #define SIGWINCH 28 30 #define SIGIO 29 31 #define SIGPOLL SIGIO 32 /* 33 #define SIGLOST 29 34 */ 35 #define SIGPWR 30 36 #define SIGSYS 31 37 #define SIGUNUSED 31
每一个信号都是一个宏,对应了一个int类型的数据。这些信号有点类似于中断号,不同模式下产生的信号对应了不同的信号值。在上面30多个信号中,有两个信号是不能被忽略的:SIGKILL和SIGSTOP。SIGKILL(9)是由终端输入kill -9命令时产生的(不知道这两个9是不是巧合),另一个SIGSTOP是程序在运行时通过CTRL+C时产生的。
我们在使用中断等时候需要注册中断处理函数,同理,在使用信号的时候也要设置一个信号处理函数
sighandler_t signal(int signum, sighandler_t handler);
这个signal函数在linux里的man手册里能查到
其中参数signum就是需要响应的信号,handler就是信号响应函数。
我们可以写一个程序来模拟一下这个信号处理的过程。
1 #include "stdlib.h" 2 #include "stdio.h" 3 #include "signal.h" 4 5 void sigint_handler(int num) 6 { 7 printf("\r\nSIGINT signal!\r\n"); 8 exit(0); 9 } 10 11 int main(void) 12 { 13 signal(SIGINT,sigint_handler); 14 while(1); 15 return 0; 16 }
使用gcc编译文件后运行程序
使用CTRL+C将程序中断后程序就会对信号SIGINT进行响应,调用信号处理函数打印信息。
驱动中的信号处理
在内核中,有个结构体是用来辅助完成异步通知这个功能的:
1 struct fasync_struct { 2 spinlock_t fa_lock; 3 int magic; 4 int fa_fd; 5 struct fasync_struct *fa_next; /* singly linked list */ 6 struct file *fa_file; 7 struct rcu_head fa_rcu; 8 };
结构体里的成员比较复杂这里我们不再细讲,我们这一步要知道这个异步通知是如何使用的fasync_struct类型的指针变量。
文件操作函数
在定义完结构体以后,我们需要定义一个文件操作函数,对应file_operations里的fasync操作。这个文件操作函数的格式要求如下:
int (*fasync) (int, struct file *, int);
在这个文件操作函数中,主要通过一个函数fasync_helper来将fasync_struct结构体初始化
extern int fasync_helper(int, struct file *, int, struct fasync_struct **);
fasync_helper的前3个参数就是fasync的三个参数,第四个参数就是要初始化的fasync_struct结构体指针变量。当应用程序改变文件fasync标记的时候,file_operations操作集合里的fasync函数就会执行。
释放fasync资源
在关闭APP程序然后执行file_operations操作集合中release函数时,需要释放fasync_struct资源,释放的时候调用的函数也是fasync_helper,所以我们可以直接调用前面fasync绑定的函数
static int new_dev_fasync(int f,struct file *filp,int on) { struct new_dev *dev = (new_dev)filp->private_data; return fasync_helper(f,filp,on,&dev->async_queue); } static int new_dev_release(struct inode *inode,struct file *filp) { return new_dev_fasync(-1,filp,0) } static const struct file_operations key_fops = { .owner = THIS_MODULE, .fasync = new_dev_fsync, .release = new_dev_release, };
在release的过程直接调用new_dev_fasync函数,传入的参数为f值为-1,on的值为0表示关闭文件释放资源,删除了异步通知。
信号发生
在设备可以被访问的时候我们需要驱动程序向应用程序发出相当于“中断”效果的信号,信号的产生是通过下面的函数实现的:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
参数sig就是我们在开始那一堆宏对应的信号
参数band可以设置为POLL_IN表示可读,POLL_OUT表示可写。
最后放出来最终的驱动程序代码
/** * @file irq.c * @author your name (you@domain.com) * @brief 异步通知驱动测试程序 * @version 0.1 * @date 2022-08-08 * * @copyright Copyright (c) 2022 * */ #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/types.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of.h> #include <linux/of_address.h> #include <linux/of_irq.h> #include <linux/gpio.h> #include <linux/of_gpio.h> #include <linux/irq.h> #include <linux/interrupt.h> #include <linux/fcntl.h> #include <linux/ide.h> #define DEVICE_CNT 1 #define DEVICE_NAME "imx6uirq" #define KEY_NUM 1 #define KEY0VALUE 0x01 #define INVALKEYS 0xFF /** * @brief 按键中断结构体 * */ struct irq_keydesc { int gpio; //io编号 int irqnum; //中断号 unsigned char value; //键值 char name[10]; //按键名字 irqreturn_t (*handler)(int,void*); //中断处理函数 }; /** * @brief 设备结构体 * */ struct new_dev { dev_t dev_id; int major; int minor; struct class *class; struct device *device; struct cdev cdev; struct device_node *dev_nd; int dev_gpio; struct irq_keydesc irqkey[KEY_NUM]; //按键描述数组 struct timer_list timer; //定时器 int timer_per; atomic_t keyvalue; atomic_t keyreleased; //1表示1次有效按键并被释放,0表示未被释放 struct fasync_struct *fasync_queue; // }; struct new_dev new_dev; static int new_dev_open(struct inode *inode, struct file *filp) { filp->private_data = &new_dev; /* 设置私有数据 */ return 0; } static ssize_t new_dev_read(struct file *filp,char __user *buf,size_t cnt,loff_t *offt) { int ret = 0; unsigned char keyvalue; unsigned char keyreleased; struct new_dev *dev = filp->private_data; //私有数据 printk("dev read\r\n"); keyvalue = atomic_read(&dev->keyvalue); //获取按键状态标志及按键值 keyreleased = atomic_read(&dev->keyreleased); if(keyreleased){ //出现按键被按下并释放的过程 if(keyvalue&0x80){ //获取最高位,如果为1表示出现过按键被按下后释放掉状态,是有效状态 keyvalue &= ~0x80; ret = copy_to_user(buf,&keyvalue,sizeof(keyvalue)); } else{ goto data_err; } atomic_set(&dev->keyreleased,0); //恢复keyreleased的状态 } else{ goto data_err; //返回负数,在用户态跳出循环 } return ret; data_err: return -EINVAL; } static int new_dev_fsync(int fd,struct file *filp,int on) { struct new_dev *dev = filp->private_data; return fasync_helper(fd,filp,on,&dev->fasync_queue);//异步通知初始化 } static int new_dev_release(struct inode *inode,struct file *filp) { return new_dev_fsync(-1,filp,0); //释放异步通知资源 } /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, .open = new_dev_open, .read = new_dev_read, .fasync = new_dev_fsync, .release = new_dev_release, }; static irqreturn_t key0_handle_irq(int irq, void *dev_id) { int value = 0; struct new_dev *dev = dev_id; dev->timer.data = dev_id; mod_timer(&dev->timer,jiffies + msecs_to_jiffies(10)); return IRQ_HANDLED; } static void timer_func(unsigned long arg) { int value = 0; struct new_dev *dev =(struct new_dev*)arg; value = gpio_get_value(dev->irqkey[0].gpio); if(value == 0){ //按下 atomic_set(&dev->keyvalue,dev->irqkey[0].value); } else{ //释放 atomic_set(&dev->keyvalue,0x80|(dev->irqkey[0].value)); //将最高位置一,表示按钮释放 atomic_set(&dev->keyreleased,1); } if(atomic_read(&dev->keyreleased)){ //按键有效 printk("kill fasync\r\n"); kill_fasync(&dev->fasync_queue,SIGIO,POLL_IN); //生成信号 } } static int dev_gpio_init(struct new_dev *dev) { int ret = 0; int i = 0; //搜索设备树节点 dev->dev_nd = of_find_node_by_path("/key"); if(dev->dev_nd == NULL){ printk("can't find device key\r\n"); ret = -EINVAL; goto fail_nd; } for(i=0;i<KEY_NUM;i++) { dev->irqkey[i].gpio = of_get_named_gpio(dev->dev_nd,"key-gpios",i); //多个按键获取 if(dev->irqkey[i].gpio<0){ ret = -EINVAL; goto fail_gpio_num; } ret = gpio_request(dev->irqkey[i].gpio,dev->irqkey[i].name); if(ret){ ret = -EBUSY; goto fail_gpio_request; } gpio_direction_input(dev->irqkey[i].gpio); dev->irqkey[i].irqnum = gpio_to_irq(dev->irqkey[i].gpio); //获取中断号 // dev->irqkey[i].irqnum = irq_of_parse_and_map(dev->dev_nd,i) //方法2获取中断号 } dev->irqkey[0].handler = key0_handle_irq; dev->irqkey[0].value = KEY0VALUE; for(i=0;i<KEY_NUM;i++){ memset(dev->irqkey[i].name,0,sizeof(dev->irqkey[i].name)); sprintf(dev->irqkey[i].name,"KEY%d",i); //将格式化数据写入字符串中 ret = request_irq(dev->irqkey[i].irqnum, //中断号 key0_handle_irq, //中断处理函数 IRQ_TYPE_EDGE_RISING|IRQ_TYPE_EDGE_FALLING, //中断处理函数 dev->irqkey[i].name, //中断名称 dev //设备结构体 ); if(ret){ printk("irq %d request err\r\n",dev->irqkey[i].irqnum); goto fail_irq; } } //此处不设置定时值,防止定时器add后直接运行 init_timer(&dev->timer); dev->timer.function = timer_func; return 0; fail_gpio_request: fail_irq: for(i=0; i<KEY_NUM;i++){ gpio_free(dev->irqkey[i].gpio); } fail_gpio_num: fail_nd: return ret; } static int __init new_dev_init(void){ int ret = 0; // unsigned int val = 0; //申请设备号 new_dev.major = 0; if(new_dev.major){ //手动指定设备号,使用指定的设备号 new_dev.dev_id = MKDEV(new_dev.major,0); ret = register_chrdev_region(new_dev.dev_id,DEVICE_CNT,DEVICE_NAME); } else{ //设备号未指定,申请设备号 ret = alloc_chrdev_region(&new_dev.dev_id,0,DEVICE_CNT,DEVICE_NAME); new_dev.major = MAJOR(new_dev.dev_id); new_dev.minor = MINOR(new_dev.dev_id); } printk("dev id geted!\r\n"); if(ret<0){ //设备号申请异常,跳转至异常处理 goto faile_devid; } //字符设备cdev初始化 new_dev.cdev.owner = THIS_MODULE; cdev_init(&new_dev.cdev,&key_fops); //文件操作集合映射 ret = cdev_add(&new_dev.cdev,new_dev.dev_id,DEVICE_CNT); if(ret<0){ //cdev初始化异常,跳转至异常处理 goto fail_cdev; } printk("chr dev inited!\r\n"); //自动创建设备节点 new_dev.class = class_create(THIS_MODULE,DEVICE_NAME); if(IS_ERR(new_dev.class)){ //class创建异常处理 printk("class err!\r\n"); ret = PTR_ERR(new_dev.class); goto fail_class; } printk("dev class created\r\n"); new_dev.device = device_create(new_dev.class,NULL,new_dev.dev_id,NULL,DEVICE_NAME); if(IS_ERR(new_dev.device)){ //设备创建异常处理 printk("device err!\r\n"); ret = PTR_ERR(new_dev.device); goto fail_device; } printk("device created!\r\n"); ret = dev_gpio_init(&new_dev); if(ret<0){ goto fail_gpio_init; } //初始化原子变量 atomic_set(&new_dev.keyvalue,INVALKEYS); atomic_set(&new_dev.keyreleased,0); return ret; fail_gpio_init: fail_device: //device创建失败,意味着class创建成功,应该将class销毁 printk("device create err,class destroyed\r\n"); class_destroy(new_dev.class); fail_class: //类创建失败,意味着设备应该已经创建成功,此刻应将其释放掉 printk("class create err,cdev del\r\n"); cdev_del(&new_dev.cdev); fail_cdev: //cdev初始化异常,意味着设备号已经申请完成,应将其释放 printk("cdev init err,chrdev register\r\n"); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); faile_devid: //设备号申请异常,由于是第一步操作,不需要进行其他处理 printk("dev id err\r\n"); return ret; } static void __exit new_dev_exit(void){ int i = 0; //释放中断 for(i=0;i<KEY_NUM;i++){ free_irq(new_dev.irqkey[i].irqnum,&new_dev); } for(i=0;i<KEY_NUM;i++){ gpio_free(new_dev.irqkey[i].gpio); } del_timer_sync(&new_dev.timer); cdev_del(&new_dev.cdev); unregister_chrdev_region(new_dev.dev_id,DEVICE_CNT); device_destroy(new_dev.class,new_dev.dev_id); class_destroy(new_dev.class); } module_init(new_dev_init); module_exit(new_dev_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
驱动代码是在原始的按键中断基础上修改的,区别如下:
在设备结构体里新添加了fasync结构体指
重新修改了文件操作集合函数
在定时器回调函数最后使用了kill_fasync函数向用户APP发出SIGIO相应的信号
kill_fasync(&dev->fasync_queue,SIGIO,POLL_IN);
驱动的思路就是在按键触发中断以后,中断服务函数启动定时器实现按键消抖。定时器到时间后执行定时回调函数,在定时回调函数内设置按键值以及按键标志(标志置1),最后向应用程序发送信号。应用程序在收到信号以后执行文件read操作对应函数。在read函数里通过copy_to_user函数向应用程序发送数据,最后将标志flag置0,表示完成一次数据同步操作。
应用程序的处理
先把整个程序放出来
/** * @file asyncnotyAPP.c * @author your name (you@domain.com) * @brief 异步通知应用程序 * @version 0.1 * @date 2022-08-08 * * @copyright Copyright (c) 2022 * */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <sys/ioctl.h> #include <signal.h> #include <unistd.h> #include <fcntl.h> int f; /** * @brief 信号处理函数 * * @param num */ static void sigio_singal_func(int num) { int err; unsigned int keyvalue = 0; err = read(f,&keyvalue,sizeof(keyvalue));if(err<0){ //读取失败 } else{ printf("sigio singal! key value = %d\r\n",keyvalue); } } /** * @brief * * @param argc //参数个数 * @param argv //参数 * @return int */ int main(int argc,char *argv[]) { char *filename; //文件名 filename = argv[1]; // int value = 0; int ret = 0; //初始化操作返回值 int flags = 0; unsigned char data; //内核传来的值 f = open(filename, O_RDWR); //打开文件 if(f < 0){ printf("file open error\r\n"); return -1; } signal(SIGIO,sigio_singal_func); fcntl(f,F_SETOWN,getpid()); //设置当前进程接收信号 flags =fcntl(f,F_GETFL); fcntl(f,F_SETFL,flags|FASYNC); //开启异步通知 while(1){ sleep(2); } close(f); //关闭文件 return 0; }
因为信号处理函数和主函数都要用到文件句柄,并且处理函数里的参数没有文件句柄,所以变量f定义成了全局变量。
其他要注意的地方就是函数fcntl,先看下man手册里的说明
函数是针对文件描述服提供控制,参数fd是文件句柄,cmd是操作描述,在/usr/include/asm-generic/fcntl.h下定义了各种宏供其使用,第三个参数在不同的cmd下有不同的含义
fcntl函数主要作用有5种
- 复制一个现有的描述符(cmd=F_DUPFD)
- 获取/设置文件描述符(cmd=F_GETFD/SETFD)
- 获取/设置文件状态标记(cmd=F_GETFL/SETFL)
- 获取/设置IO所有权(cmd=F_GETOWN/F_SETOWN)
- 获取/设置记录所(cmd=F_GETLK/F_SETLK)
我们先用了参数
fcntl(f,F_SETOWN,getpid());
表示将指定的ID进程获取SIGIO信号,指定的进程就是getpid(),就是当前的id。
然后使用下面的命令
flags =fcntl(f,F_GETFL); fcntl(f,F_SETFL,flags|FASYNC); //开启异步通知
首先获取了当前的文件状态标志,再把标志或上一个FASYNC开启了异步通知然后重新赋值给文件。这样就搞定了
每次按下按键应用程序都会响应,运行程序时候在后面加上&在后台运行,使用top命令查看下资源占用。
也是没问题的。
这就是阻塞、非阻塞以及异步通知的方法来实现用户程序和驱动程序之间的数据交互。至于后面想用那种,就看自己的需求了(说实话非阻塞的IO的流程还是不太懂,觉得阻塞的方法是最简单的!)。