上篇文章,介绍了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
/**
* q: 要初始化的等待队列头
* return: 无
*/
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
/**
* q: 要加入的等待队列头
* wait:要加入的等待队列项
* return: 无
*/
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)

当设备可以访问以后再将进程对应的等待队列项从等待队列头中删除即可:

1
2
3
4
5
6
/**
* q: 要删除的等待队列头
* wait:要删除的等待队列项
* return: 无
*/
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)
{
//printk("releasekey!\r\n");
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); /* 读取IO值 */
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

  • select函数原型:
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* nfs: 所要监视的这三类文件描述集合中,最大文件描述符加1
* readfds: 用于监视指定描述符集的读变化
* writefds: 用于监视文件是否可以进行写操作
* exceptfds: 用于监视文件的异常
* timeout: 超时时间
* return: 0 超时发生, -1 发生错误, 其他值 可以进行操作的文件描述符个数
*/
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的时候就表示无限等待。

  • poll函数原型:
1
2
3
4
5
6
7
8
9
/**
* fds: 要监视的文件描述符集合以及要监视的事件,为一个数组
* nfds: 监视的文件描述符数量
* timeout: 超时时间,单位为 ms
* return: 0 超时发生, -1 发生错误, 其他值 可以进行操作的文件描述符个数
*/
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_table中 */
poll_wait(filp, &dev->r_wait, wait);

/* 按键按下 */
if(atomic_read(&dev->releasekey))
{
mask = POLLIN | POLLRDNORM; /* 返回PLLIN */
}
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)
{
/* 没有按键按下,返回-EAGAIN */
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)
{
//printk("releasekey!\r\n");
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; /* 500ms */
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。