上篇文章,介绍了linux中的五种I/O模型,本篇,就来使用阻塞式I/O和非用阻塞式I/O两种方式进行按键的读取实验,并对比之前使用输入捕获和中断法检测的按键程序,查看CPU的使用率是否降低。
[TOC]
1 阻塞I/O方式的按键检测
1.1 阻塞I/O之等待队列
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作。
等待队列头使用结构体wait_queue_head_t 表示:
1 2 3 4 5 6
| struct __wait_queue_head { spinlock_t lock; struct list_head task_list; };
typedef struct __wait_queue_head wait_queue_head_t;
|
使用 init_waitqueue_head 函数初始化等待队列头:
1 2 3 4 5
|
void init_waitqueue_head(wait_queue_head_t *q)
|
当设备不可用的时, 将这些进程对应的等待队列项(wait_queue_t )添加到等待队列里面:
1 2 3 4 5 6 7 8
| struct __wait_queue { unsigned int flags; void *private; wait_queue_func_t func; struct list_head task_list; };
typedef struct __wait_queue wait_queue_t;
|
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项:
1
| DECLARE_WAITQUEUE(name, tsk)
|
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中:
1 2 3 4 5 6
|
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
|
当设备可以访问以后再将进程对应的等待队列项从等待队列头中删除即可:
1 2 3 4 5 6
|
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
|
当设备可以使用的时候就要唤醒进入休眠态的进程:
1 2
| void wake_up(wait_queue_head_t *q) void wake_up_interruptible(wait_queue_head_t *q)
|
1.2 阻塞I/O程序编写
这里仅介绍与之前按键程序的主要区别。
1.2.1驱动程序
阻塞读取逻辑如下,首先要定义一个等待队列,当按键没有按下时,就要阻塞等待了(将等待队列添加到等待队列头),然后进行行一次任务切换,交出CPU的使用权。等待有按键按下时,会有信号唤醒该等待,并将按键值返回给应用层的程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int ret = 0; unsigned char keyvalue = 0; unsigned char releasekey = 0; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data; DECLARE_WAITQUEUE(wait, current); if(atomic_read(&dev->releasekey) == 0) { add_wait_queue(&dev->r_wait, &wait); __set_current_state(TASK_INTERRUPTIBLE); schedule(); if(signal_pending(current)) { ret = -ERESTARTSYS; goto wait_error; } __set_current_state(TASK_RUNNING); remove_wait_queue(&dev->r_wait, &wait); }
keyvalue = atomic_read(&dev->keyvalue); releasekey = atomic_read(&dev->releasekey);
if (releasekey) { if (keyvalue & 0x80) { keyvalue &= ~0x80; ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue)); } else { goto data_error; } atomic_set(&dev->releasekey, 0); } else { goto data_error; } return 0;
wait_error: set_current_state(TASK_RUNNING); remove_wait_queue(&dev->r_wait, &wait); return ret; data_error: return -EINVAL; }
|
按键的定时器去抖逻辑中的,读取到按键后,触发唤醒,这里以其中的一个按键为例,其逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void timer1_function(unsigned long arg) { unsigned char value; struct irq_keydesc *keydesc; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)arg;
keydesc = &dev->irqkeydesc[0];
value = gpio_get_value(keydesc->gpio); if(value == 1) { printk("get key1: high\r\n"); atomic_set(&dev->keyvalue, keydesc->value); } else { printk("key1 release\r\n"); atomic_set(&dev->keyvalue, 0x80 | keydesc->value); atomic_set(&dev->releasekey, 1); } if(atomic_read(&dev->releasekey)) { wake_up_interruptible(&dev->r_wait); } }
|
1.2.2 应用程序
应用程序不需要修改,还使用之前的轮询读取的方式,为了在测试时看出阻塞与非阻塞方式的区别,在read函数前后添加打印,如果程序运行正常,会先打印read前一句的打印,直到有按键按下后,read函数才被接触阻塞,read后一句的打印才会打印出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| while(1) { printf("[APP] read begin...\r\n"); read(fd, &keyvalue, sizeof(keyvalue)); printf("[APP] read end\r\n"); if (keyvalue == KEY1VALUE) { printf("[APP] KEY1 Press, value = %#X\r\n", keyvalue); } else if (keyvalue == KEY2VALUE) { printf("[APP] KEY2 Press, value = %#X\r\n", keyvalue); } }
|
1.2 实验
和之前一样,使用Makefile编译驱动程序和应用程序,并复制到nfs根文件系统中。
开始测试,按如下图,当没有按键按下时,应用程序被阻塞:
按键程序在后台运行,此时使用top指令开查看CPU的使用率,可以发现阻塞式按键驱动这种方式,CPU的暂用率几乎为0,虽然按键应用程序中仍实现循环读取的方式,但因平时读取不到按键值,按键应用程序被阻塞住了,CPU的使用权被让出,自然CPU的使用率就降下来了。
2 非阻塞I/O方式的按键检测
按键应用程序以非阻塞的方式读取,按键驱动程序也要以非阻塞的方式立即返回。应用程序可以通过select、poll或epoll函数来
查询设备是否可以操作,驱动程序使用poll函数。
2.1 非阻塞I/O之select/poll
1 2 3 4 5 6 7 8 9 10 11 12 13
|
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
|
其中超时时间使用结构体timeval表示:
1 2 3 4
| struct timeval { long tv_sec; long tv_usec; };
|
当timeout为NULL的时候就表示无限等待。
1 2 3 4 5 6 7 8 9
|
int poll(struct pollfd *fds, nfds_t nfds, nt timeout)
|
2.2 非阻塞I/O程序编写
2.2.1 驱动程序
poll函数处理部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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;
poll_wait(filp, &dev->r_wait, wait); if(atomic_read(&dev->releasekey)) { mask = POLLIN | POLLRDNORM; } return mask; }
static struct file_operations imx6uirq_fops = { .owner = THIS_MODULE, .open = imx6uirq_open, .read = imx6uirq_read, .poll = imx6uirq_poll, };
|
read函数处理部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) { int ret = 0; unsigned char keyvalue = 0; unsigned char releasekey = 0; struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp->private_data;
if (filp->f_flags & O_NONBLOCK) { if(atomic_read(&dev->releasekey) == 0) { return -EAGAIN; } } else { ret = wait_event_interruptible(dev->r_wait, atomic_read(&dev->releasekey)); if (ret) { goto wait_error; } }
keyvalue = atomic_read(&dev->keyvalue); releasekey = atomic_read(&dev->releasekey);
if (releasekey) { if (keyvalue & 0x80) { keyvalue &= ~0x80; ret = copy_to_user(buf, &keyvalue, sizeof(keyvalue)); } else { goto data_error; } atomic_set(&dev->releasekey, 0); } else { goto data_error; } return 0;
wait_error: return ret; data_error: return -EINVAL; }
|
2.2.2 应用程序
2.2.2.1 poll方式读取
注意open函数的参数是O_NONBLOCK,即非阻塞访问,并且为了在测试时看出阻塞读取与非阻塞读取的区别,在poll函数前后添加打印,如果程序正常运行,poll函数则不会被阻塞,500ms超时未读取到按键值后会再次循环读取,实际效果就是可以看打一直有打印输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| filename = argv[1]; fd = open(filename, O_RDWR | O_NONBLOCK); if (fd < 0) { printf("[APP] Can't open file %s\r\n", filename); return -1; }
fds.fd = fd; fds.events = POLLIN; while(1) { printf("[APP] poll begin... \r\n", data); ret = poll(&fds, 1, 500); printf("[APP] poll end \r\n", data); if (ret > 0) { ret = read(fd, &data, sizeof(data)); if(ret < 0) { } else { if(data) { printf("[APP] key value = %d \r\n", data); } } } else if (ret == 0) { } else { } }
|
2.2.2.2 select方式读取
select方式读取与poll方式类似,都是非阻塞读取,程序类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| while(1) { FD_ZERO(&readfds); FD_SET(fd, &readfds); timeout.tv_sec = 0; timeout.tv_usec = 500000; ret = select(fd + 1, &readfds, NULL, NULL, &timeout); switch (ret) { case 0: break; case -1: break; default: if(FD_ISSET(fd, &readfds)) { ret = read(fd, &data, sizeof(data)); if (ret < 0) { } else { if (data) { printf("key value=%d\r\n", data); } } } break; } }
|
2.3 实验
2.3.1 poll方式读取
和之前一样,使用Makefile编译驱动程序和应用程序,并复制到nfs根文件系统中。
开始测试,按如下图,当没有按键按下时,应用程序也没有被阻塞,从不断的打印就可以看出应用程序在循环运行。当有按键按下时,能够读取到对应的按键值。
按键程序在后台运行,此时使用top指令开查看CPU的使用率,可以发现非阻塞式按键驱动这种方式,CPU的暂用率也几乎为0,虽然按键应用程序中仍实现循环读取的方式,但poll函数有500ms的超时设置,在超时等待的时间里,CPU的使用权也是被让出,所以CPU的使用率也降下来了。
2.3.2 select方式读取
select方式读取与poll方式读取的效果一样。
使用ps指令查看poll方式的按键进行号,使用kill杀带该进程,再运行select方式的按键应用程序:
select非阻塞读取的方式,CPU的暂用率也几乎为0:
3 总结
本篇使用两种I/O模型进行按键读取:阻塞式I/O和非用阻塞式I/O,通过实际的实验,对比两者方式的实际运行效果与主要区别,并查看CPU的占用率,两种方式的CPU使用率都几乎为0。