首先说说什么是阻塞和非阻塞的概念:阻塞操作就是指进程在操作设备时,由于不能获取资源或者暂时不能操作设备时,系统就会把进程挂起,被挂起的进程会进入休眠状态并且会从调度器的运行队列移走,放到等待队列中,然后一直休眠,直到该进程满足可操作的条件,再被唤醒,继续执行之前的操作。非阻塞操作的进程在不能进行设备操作时,并不会挂起,要么放弃,要么不停地执行,直到可以进行操作为止。
我们都知道,在应用中,打开一个设备文件时,指定了是以阻塞还是非阻塞打开(缺省是阻塞方式),然后后面的读写一切都是交由驱动来实现,那么驱动是如何实现read()和wri te ()的阻塞呢!下面以读写一个内存块为例子,当该内存写满了,不能写的时候,调用write()函数该怎么处理,当该内存已经读取完了,空了的时候,调用read()函数,又改如何处理(该代码简化了,只为说明问题,不能正常编译使用):
w ai t_queue_head_t read_queue; //定义读等待队列头部
wait_queue_head_t write_queue; //定义写等待队列头部
struct semaphore sem; //定义信号量,用于互斥访问公共资源
sta ti c ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
if(down_interrup TI ble(&sem))
return -ERESTARTSYS; //使用 down_interrup TI ble,给公共资源上 锁 ,以防出现并发引起的竞态问题
while (!have_data) //have_data用来判断缓冲区中是否有数据,如果有数据,直接跳过该while语句,执行下面的 // copy_to_user
{
up(&sem); //由于没有数据,不能进行读取数据操作,要释放锁,解锁,这里的解锁很重要,要是没有解锁,很容 //易进入死锁,具体怎样,下面再分析
if(filp->f_flags & O_NONBLOCK) //判断该文件时以阻塞方式还是非阻塞方式打开
return -EAGAIN; //由于是非阻塞打开,直接返回
wait_event_interrup TI ble(read_queue,have_date);//阻塞方式代开,该语句会让进程进入休眠状态,然后等待其他进程 //的唤醒并且have_data=true时,才会被完全唤醒,执行下面的语句
if(down_interrup TI ble(&sem)) //由于可以进行读取了,所以在此给公共资源上锁
return -ERESTARTSYS;
if (copy_to_user(buf, (void*)(dev->data + p), count)) { //实现数据从内核空间读取到用户空间,完成读取操作
..................
}
have_data = false; // 标记 该数据已经读取完毕
up(&sem); //释放锁
wake_up(&write_queue); //读取完毕,缓冲区有空间可以写入了,就唤醒写进程,让写进程把数据写入
return ;
}
下面分析write函数,其原理和实现也是和read函数一样,都是先给公共资源上锁,再判断是阻塞访问还是非阻塞访问,如果是非阻塞访问,且资源不能获取时,直接返回,若果时阻塞且不能获取资源时,就进入休眠,等待其他进程的唤醒。
static ssize_t mem_write(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
if(down_interruptible(&sem))
return -ERESTARTSYS; //使用 down_interruptible,给公共资源上锁,以防出现并发引起的竞态问题
while (have_data) //have_data用来判断缓冲区中是否有数据,如果有数据,表示缓冲区已经满了,不能写入,
//如果have_data是false,即没有数据,缓冲区是空的,可以写入数据,就执行下面的copy_f rom _user
{
up(&sem); //由于有数据,不能进行写入数据操作,要释放锁,解锁 if(filp->f_flags & O_NONBLOCK) //判断该文件时以阻塞方式还是非阻塞方式打开
return -EAGAIN; //由于是非阻塞打开,直接返回
wait_event_interruptible(write_queue,!have_date);//阻塞方式代开,该语句会让进程进入休眠状态,然后等待其他进程 //的唤醒并且have_data=false时,才会被完全唤醒,执行下面的语句
if(down_interruptible(&sem)) //由于可以进行写入操作了,所以在此给公共资源上锁
return -ERESTARTSYS;
if (copy_from_user((dev->data + p), buf,count)) { //实现数据从内核空间读取到用户空间,完成读取操作
..................
}
have_data = true; //标记该数据已经读取完毕
up(&sem); //释放锁
wake_up(&read_queue); //写入数据完毕,缓冲区有数据可以读取了,就唤醒读进程,让读进程开始读取数据
return ;
}
以上是驱动中的读取和写入操作,当写进程发现数据已满,不能写入时,且上层应用是以阻塞的方式打开设备文件时,所以必须要写入数据才能返回,否则不能返回,那么就有两种实现机制,要不就是不停地忙等待,等待设备可以写入时,便写入,然后返回,可是这样做的话,非常影响 CPU 的执行效率,大大降低了CPU的性能,所以 linux 内核中采取了等待队列的实现方式,就是当一个阻塞进程写入数据时,发现不能写入时,会把这个进程挂起,放到等待队列中休眠,然后一直在休眠,直到有个读进程,把缓冲区的数据读取完毕后,然后读进程会把写进程唤醒,告诉写进程缓冲区可以写入数据了,于是写进程继续写入操作,并且返回。举个例子,小明饿了,要吃饭,于是跑去妈妈那里,说要吃饭,妈妈说放没有做好,你说小明是继续在这里一直等着妈妈把饭做好,还是先去睡一觉好呢,如果我是小明,我就先去睡一觉,然后妈妈把饭做好了,就把小明叫醒,小明,可以吃饭了,于是小明起来,跑去吃饭。当读进程阻塞时,也是这样,就不分析了。
现在说说为什么每次进去阻塞前都要把锁释放掉,然后唤醒时再次上锁,我们试想一下,假如读进程发现缓冲区为空,不能读取时,准备进入休眠了,没有把锁释放,效果会怎样,就相当于读进程带着锁睡着了,一旦读进程带着锁睡着了,写进程来了,可是写进程因为不能获取锁,就不能访问临界区的资源,更不能往缓冲区里面写入数据,所以缓冲区会一直为空,且写进程也会不停地在那里休眠,等到读进程释放锁,可是读进程睡着了,不能释放锁,写进程也休眠了,不能唤醒读进程,于是就发生了死锁了。这就好比小明他爸爸藏了一个还魂丹在保险箱里,有一天,他爸爸晕倒了,可是没有告诉小明锁放在那里,于是小明只能在保险箱外面,看着他爸爸晕过去,却无能为力了.....