阻塞IO
在文章正题之前,先上一段代码(也是为什么会又阻塞或者非阻塞乃至异步):
//app.c
//省略无关代码
while(1)
{
printf("read file \r\n");
ret = read(fd, &str[0], sizeof(char));
if(str[0] == '0')
{
}
else if(str[0] == '1')
{
printf("value is %x\r\n",str[0]);
}
}
一般如果我们在驱动层不对调用该驱动的进程进行处理,就像以上代码在while循环里面一直死循环,将会导致如下现象.
在运行该APP,并且驱动层并没有对其进行阻塞,非阻塞以及异步的处理,就会导致这一个APP占用CPU很高,如果应用程序多了,是不是就不太好了呢?那么您一定会有疑问了,那该怎么办?我们就带着疑问来言归正传,在这篇文章最后我们再看看使用了阻塞这个使用率会变成多少?
- 需要阻塞IO原因
当应用程序需要读取设备数据,但是设备并没有准备好数据提供给应用程序;或者应用程序需要往设备中写入数据,但是设备现在繁忙,数据缓冲区是满的不能接受应用层的数据。等等这些情况下,有必要让此应用程序(一个进程)进入阻塞状态,直到能操作唤醒当前进程.
举个栗子:比如我们下班回家累了一天,打算洗澡放松,但是现在没有热水,所以不能直接用冷水洗,那么就需要热水,但是我们不洗澡啥也不愿干,所以就一直盯着热水器,啥也不干,哪也不去,直到热水器上的温度从5°变为40°左右感觉水温不凉的时候就洗澡.
注意 :这里面黄色标注的就是阻塞的特点.需要和异步进行区别,关于异步的会在另一篇单独进行阐述. - 预备知识
- 睡眠状态:linux会把运行程序形成一个队列,通过调度器进行切换调度。所谓睡眠就是将一个程序(进程)从运行队列中移除,直到等待的时间将其唤醒再次加入到运行队列中,再有调度器进行调度.
- 让程序(进程)进入睡眠几点需要注意:
- 驱动除了信号量以外,持有自旋锁,原子,互斥锁等这些并发竞争操作时候,或者中断关闭情况下,是不能进入睡眠的.可以想象,如果应用在操作一个设备时候,当这个设备驱动正在持有一个自旋锁,那么调用这个驱动的应用如果进入睡眠,另一边如果等待这个自旋锁的进程是否就不会得到这个锁呢,别的同样道理.
- 如果驱动持有的是信号量,那么另一边等待这个信号量的线程也会进入睡眠状态,所以任何一个可能持有这个信号量的睡眠都不应该太久,应该短暂,并且不能阻塞当前运行线程也就是调用的驱动文件.
Tips:以上几点需要仔细考虑
本文涉及到的内核头文件主要有:
----->linux/wait.h
eg1.—第一种睡眠方式:等待事件
DECLEARE_WAIT_QUEUE_HEAD(name)//定义等待队列头
/*
wait_queue_head_t name;//对上面的宏进行展开是一下样子
init_waitqueue_head(&name);
*/
int flag = 0;//提供唤醒睡眠的条件
/*以下只是一个使用例子,所以不代表某个函数*/
void read_date(void)
{
...
wait_event_interruptible(name,flag != 0);
flag = 0;
...
}
void write_date(void)
{
...
flag = 1;
wake_up_interruptible(&name);//这里并没有写错,再read函数里面调用的是一个宏,宏内部处理了,所以不用取地址.
...
}
以上是一个很简单的一个模板,当应用读数据调用驱动层的read后,会进入睡眠状态(当然前提是在调用read前任何操作flag的值仍然是0).
当应用层写数据,调用驱动层的write后,因为write将flag变成1,并且唤醒队列。导致调用此进程退出睡眠。
首先最简单的一种阻塞睡眠方式:
方式1:
定义等待队列,并且在read函数内部设置等待条件,这里等待原子变量为1就退出睡眠.如图:
下面需要一个动作让该原子变量变为1,我们就用另一个app写一个数据调用write,在驱动的write函数内部将原子变量变为1.唤醒前文中的进程,如图:(这里需要注意,必须调用wakeup函数,才会让等待队列去查看条件是否可以让其醒来.)
那么如果我们app1去读该驱动设备,第一次会让该进程app1进入睡眠(原子变量初始时候设置为0,等待队列是判断该原子变量是否为1才不会进入睡眠状态).
直到让原子变量为1才会向下执行.请看下面现象:
综上所述,确实如此,会让进程一直阻塞在该语句,并不会向下执行。
那么问题来了。。。。
当同时两个进程都读这个驱动操作,那么这两个进程都会进入睡眠状态,当一个进程写这个驱动后.这个原子变量就会变为1,并且会唤醒一个进程.具体唤醒哪一个,另一个进程会怎么样呢?
下面来看看现象:
//编者只是在驱动层read函数中加上打印进程号语句,其他都没有变动.
static ssize_t _key_read(struct file * pfile, char __user * _date, size_t size, loff_t * pvoid)
{
char date[1] = {
0x88};
printk("sleep task %d\r\n",current->pid);
wait_event_interruptible(key_wait_head,(atomic_read(&akey_dev_s.key_flag) == 1));
printk("wake up task %d\r\n",current->pid);
atomic_set(&akey_dev_s.key_flag,0);
if(copy_to_user(_date,date,1))
{
return -1;
}
return 0