Linux驱动开发十一.阻塞与非阻塞IO操作——2.非阻塞IO操作
在前面一章我们实现了通过阻塞操作来访问硬件资源,下面我们要通过非阻塞的模式来尝试一下如何实现这一效果。
用户态APP
我们在上一章引出非阻塞模式的时候已经说明了,非阻塞IO主要是属于异步IO的模式。那么对于用户态APP来说,有几个接口我们需要了解
select
epoll
还有poll
其中 select和poll都属于轮询机制,而epoll属于事件驱动机制。这里要讲的内容太多,以后有机会再深入。我们第一步的目的是能够使用这个轮询机制实现非阻塞IO的效果。所以这里只讲select函数的使用。
文件打开模式
我们前面所有的用户态APP都是用来访问驱动节点的(/dev路径下的设备文件),所以在程序中需要将文件打开
f = open(filename, O_RDWR);
这里open函数传入的参数flags是O_RDWR,这种打开方式是默认的阻塞模式。而我们想要使用非阻塞模式访问文件时,需要加上新的标志
也就是这样打开文件
f = open(filename, O_RDWR|O_NONBLOCK);
在原先的基础上加上非阻塞的标志。
fd_set
要使用select函数,首先要明白一个数据类型:fd_set,先看看内核里是怎么描述这个数据的。
select函数
select函数用来根据IO状态修改fd_set
/* fd_set for select and pselect. */ typedef struct { /* XPG4.2 requires this member name. Otherwise avoid the name from the global namespace. */ #ifdef __USE_XOPEN __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->fds_bits) #else __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; # define __FDS_BITS(set) ((set)->__fds_bits) #endif } fd_set;
追溯到底,fd_set是一个long形式的数组,每一个元素都能和一个文件句柄建立联系,建立联系的过程有我们来完成。具体这个fd_set的具体作用真心没搞清,但是根据教程上说的大概意思就是值fd_set类型变量的每一位都代表了一个文件描述符。select函数在监视文件的时候需要通过fd_set将是否监视该文件句柄,一般来说这个数组是0和1来描述的,1表示监控,0表示不监控,比如我们需要 从一个设备文件中读取数据,就洗先定义个fd_set变量,然后通过下面几个宏来操作
void FD_CLR(int fd, fd_set *set); //将fd_set某一位清零 int FD_ISSET(int fd, fd_set *set); //测试某一个文件是否属于某个文件集合 void FD_SET(int fd, fd_set *set); //将fd_set变量某个bit置一,也就是向fd_set添加一个文件描述符 void FD_ZERO(fd_set *set); //将fd_set所有位都清零
我们要用的时候就这么做
1 f = open("file",O_RDWR|O_NONBLOCK); //文件句柄 2 fd_set readfds; //读操作文件描述符集 3 4 FD_SERO(&readfds); //readfds清除 5 FD_SET(f,&readfds); //将打开的文件句柄f添加到文件描述符集里
在用非阻塞模式打开文件后,创建一个新的文件描述符集,清除后将文件句柄添加到这个描述符里。就可以通过select调用了。
select函数
当调用select函数时,内核根据IO状态修改fd_set的内容,由此来通知执行了select的进程哪一个文件可以读或写。先看下select的函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数nfds 是select见识的文件句柄数,根据进程中打开的文件数量而定,一般是我们要监视的文件的最大文件号加一(注:nfds并非一定表示监视的文件句柄数。官方文档仅指出nfds is the highest-numbered file descriptor in any of the three sets, plus 1. (可在linux环境中通过man select命令查得))
参数readfds,writefds和exceptfds是select监视的文件操作类型,分别为读、写和异常,可以根据需求按照上面的要求创建对应的文件描述符集合传进来就可以了
参数timeout是本次select操作的时长。这个时间是由一个精确到微秒的结构体来描述的
struct timeval { __time_t tv_sec; /* Seconds. */ __suseconds_t tv_usec; /* Microseconds. */ };
我们需要按照需求来构造select的超时时间
struct timeval timeout; /*设置select超时为500ms*/ timeout.tv_sec = 0; timeout.tv_usec = 500000;
select的返回值时int形式,如果有错误,就会返回-1,如果超时就会返回0,否则返回值就是可以进行操作都文件描述符的个数。
写一个程序来大概演示一下select函数是怎么使用的
1 void main(void) 2 { 3 /*变量声明*/ 4 int ret,f; 5 fd_set readfds; //文件描述符集 6 struct timeval timeout; //超时结构体 7 8 f = open("/dev/xxx",O_RDWR|O_NONBLOCK); //打开文件获取文件句柄 9 10 FD_ZERO(&readfds); //将文件描述符集全部清零 11 FD_SET(f,&readfds); //将文件句柄添加至描述符集 12 13 timeout.tv_sec = 0; 14 timeout.tv_usec = 100000; //超时时长100ms 15 16 ret = select(f+1,&readfds,NULL,NULL,&timeout); //使用select函数监控文件read状态 17 switch(ret){ //select返回值为0 超时 18 case 0: 19 printf("timeout!\r\n"); 20 break; 21 case -1: //select返回值为-1,错误 22 printf("err!\r\n"); 23 break; 24 default: //可以read操作 25 if(FD_ISSET(f,&readfds)){ //判断是否是f文件句柄 26 /*读取数据等操作*/ 27 printf("read data from dev\r\n"); 28 } 29 break; 30 } 31 }
上面的代码只是使用了select一次检查文件描述符是否发生变化,代码的注释还是很清楚的。因为我们只需要考虑读取的情况,所以在select的时候我们只传给readfds参数,其余几个都是NULL。如果需要进行写操作则应该对writefds参数也要传入相应的描述符集。
最后看看APP是怎么实现的
/** * @file irqAPP.c * @author your name (you@domain.com) * @brief 按键中断APP测试程序——非阻塞版本 * @version 0.1 * @date 2022-07-26 * * @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 <sys/select.h> #include <sys/time.h> #include <unistd.h> /** * @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 f = 0; //初始化文件句柄 fd_set readfds; struct timeval timeout; unsigned char data; //内核传来的值 f = open(filename, O_RDWR|O_NONBLOCK); //打开文件(非阻塞方式) if(f < 0){ printf("file open error\r\n"); return -1; } while(1){ FD_ZERO(&readfds); FD_SET(f,&readfds); timeout.tv_sec = 0; timeout.tv_usec = 500000; //500ms ret = select(f+1,&readfds,NULL,NULL,&timeout); switch(ret){ case 0: //超时 break; case -1: //错误 break; default: //可以读数 if(FD_ISSET(f,&readfds)){ ret = read(f,&data,sizeof(data)); if(ret<0){ //读取错误 printf("data read err\r\n"); } else{ if(data) //获取数据有效 printf("key value = %#X\r\n",data); } } break; } } close(f); //关闭文件 return 0; }
整个应用程序主要依赖一个while循环,在循环里先将文件描述符清零,再导入我们关注的文件,然后就是不停的使用select函数按照指定的超时时间去看read的状态,如果可以read就从驱动读取数据。
APP完成后下面要针对非阻塞IO来做一下驱动程序
驱动程序构成
在这个驱动构建的过程有太多的问题没解决,搞了快1周,只能按照案例把驱动先写出来。很多函数的使用说实话真没搞清,网上也没查到很详细的说明。只能先把轮子抄出来,至于为什么是这样只能等以后水平能上来再考虑了。
我们在用户态里的while不停的调用select查询文件可悲访问状态,每次select的时候对应驱动里的文件操作集合会执行poll对应的函数。所以特别要关注的就是poll操作对应的函数。
先看下poll函数的原型
unsigned int (*poll)(struct file *filp, struct poll_table *wait);
第一个参数filp就是文件句柄,比较好理解,后面这个指向poll_table的wait指针着实没搞懂应用程序是怎么传过来的。
1 static unsigned int new_dev_poll(struct file *filp,struct poll_table_struct * wait) 2 { 3 int mask = 0; 4 struct new_dev *dev = filp->private_data; 5 poll_wait(filp,&dev->r_wait,wait); 6 if(atomic_read(&dev->keyreleased)){ 7 //按键被按下,可读取数据 8 mask = POLLIN | POLLRDNORM; 9 } 10 return mask; 11 }
每一次select的时候,都会在驱动里执行这个函数。首先在第5行调用poll_wait函数将等待队列头添加到poll_table中。
可以看一下poll_wait函数在内核里的定义include/linux/poll.h
1 /* 2 * Do not touch the structure directly, use the access functions 3 * poll_does_not_wait() and poll_requested_events() instead. 4 */ 5 typedef struct poll_table_struct { 6 poll_queue_proc _qproc; 7 unsigned long _key; 8 } poll_table; 9 10 static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p) 11 { 12 if (p && p->_qproc && wait_address) 13 p->_qproc(filp, wait_address, p); 14 }
这个poll_wait函数的作用和用法我实在是搞不懂,但是把这个函数屏蔽掉程序竟然还能正常运行,不知道是什么情况。还有第三个参数wait,应该是从new_dev_poll函数中传进来的,poll_table结构体的指针。
在Poll_wait函数执行完成后,根据按键状态给定返回值,如果按键被按下过,返回带有可被读的mask状态。否则返回mask状态就是原先状态。(我这里打印了一下mask的值,一直都是0,不知道是为什么)。应用程序看到文件句柄的可读状态发生了变化,就调用read函数从内核读取数据。
最后把驱动代码放出来
/** * @file unblockIO.c * @author your name (you@domain.com) * @brief 非阻塞IO测试驱动 * @version 0.1 * @date 2022-08-10 * * @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/wait.h> #include <linux/poll.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表示未被释放 wait_queue_head_t r_wait; }; 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("read\r\n"); if(filp->f_flags & O_NONBLOCK){ //非阻塞方式处理 if(atomic_read(&dev->keyreleased) == 0){ return -EAGAIN; } } else{ //阻塞方式处理 wait_event_interruptible(dev->r_wait,atomic_read(&dev->keyreleased)); } 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; singal_err: data_err: return -EINVAL; }; static unsigned int new_dev_poll(struct file *filp,struct poll_table_struct * wait) { int mask = 0; struct new_dev *dev = filp->private_data; if(atomic_read(&dev->keyreleased)){ //按键可读 mask = POLLIN | POLLWRNORM; } printk("mask=%d\r\n",mask); return mask; } /** * @brief 文件操作集合 * */ static const struct file_operations key_fops = { .owner = THIS_MODULE, .open = new_dev_open, .read = new_dev_read, .poll = new_dev_poll, }; static irqreturn_t key0_handle_irq(int irq, void *dev_id) { 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); } } 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");