《驱动学习 - 同步互斥阻塞》
1.同步互斥阻塞的概念
并发与竞态:
并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。
在linux中,主要的竞态发生在如下几种情况:
1、对称多处理器(SMP)多个CPU 特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
2、单CPU内进程与抢占它的进程
3、中断(硬中断、软中断、Tasklet、底半部)与进程之间
只要并发的多个执行单元存在对共享资源的访问,竞态就有可能发生。
如果中断处理程序访问进程正在访问的资源,则竞态也会会发生。
多个中断之间本身也可能引起并发而导致竞态(中断被更高优先级的中断打断)。
解决竞态问题的途径:
是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元在访问共享资源的时候,其他的执行单元都被禁止访问。
访问共享资源的代码区域被称为临界区,临界区需要以某种互斥机制加以保护,如:中断屏蔽,原子操作,自旋锁,和信号量都是linux设备驱动中可采用的互斥途径。
临界区和竞争条件:
所谓临界区(critical regions)就是访问和操作共享数据的代码段,为了避免在临界区中并发访问,编程者必须保证这些代码原子地执行——也就是说,代码在执行结束前不可被打断,就如同整个临界区是一个不可分割的指令一样,如果两个执行线程有可能处于同一个临界区中,那么就是程序包含一个bug,如果这种情况发生了,我们就称之为竞争条件(race conditions),避免并发和防止竞争条件被称为同步。
阻塞与非阻塞:
阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,而是直接返回。
2.为什么使用原子操作
原子操作:
顾名思义,就是说像原子一样不可再细分不可被中途打断。一个操作是原子操作,意思就是说这个操作是以原子的方式被执行,要一口气执行完,执行过程不能够被OS的其他行为打断,是一个整体的过程,在其执行过程中,OS的其它行为是插不进来的。
为什么要使用原子操作?
static int canopen = 1; static int drv_open(略) { if(--canopen != 0) { canopen++; return -EBUSY; } } static int drv_close(略) { canopen++; }
根据上面的代码,一旦应用程序A调用open,那么canopen就会调用驱动程序drv_open,canopen自减等于0,if判断不成立,就接下来去执行其他的操作。当应用程序B调用open的时候,canopen自减等于-1,if判断成立,然后返回。驱动程序看起来就具备了每次只能有一个进程使用该设备的现象。
但是实际不是这样。
canopen的自减在汇编层面分为三个步骤:
1.读出canopen的值
2.canopen的值减1
3.将canopen的值写回去
因为linux是多任务系统,那么如果在应用程序A调用open的时候,canopen=1被读出来,这时候应用程序B也调用open,并且中断了应用程序A,应用程序B自减完,将canopen=0写回去。这时候应用程序A后继续上一步执行,将canopen=1自减,然后写回去。这样A和B都成功执行了open操作。
因此需要原子操作来确保执行这些代码时不会被其他打断。
static atomic_t canopen = ATOMIC_INIT(1); static int drv_open(略) { if(!atomic_dec_and_test(&canopen)) { atomic_inc(&canopen); return -EBUSY; } } static int drv_close(略) { atomic_inc(&canopen); }
将canopen定义为原子变量,初始值为1
atomic_dec_and_test(&canopen)将canopen进行减1后判断它是否为0,如果为0,则返回true,否则flase。
atomic_inc就是自增。
原子操作的函数在arch/arm/include/asm/atomic.h中有定义,基本根据函数名就知道什么意思,这边就不列举了。
3.信号量
信号量(semaphore)是用于保护临界区的一种常用方法,只有得到信号量的进程才能执行临界区代码。当获取不到信号量时,进程进入休眠等待状态。
//定义信号量 struct semaphore sem; //初始化信号量 void sema_init(struct semaphore *sem, int val); void init_MUTEX(struct semaphore *sem); //初始化为0 static DECLARE_MUTEX(button_lock); //定义互斥锁,里面包含了定义和初始化。等价于上面两个函数 //获取信号量 void down(struct semaphore *sem); //获取信号量,获取不到就休眠 int down_interruptible(struct semaphore *sem);//这是获取不到信号量就去休眠,休眠可以被打断 int down_trylock(struct semaphore *sem);//尝试获取信号量,获取不到立即返回 //释放信号量 void up(struct semaphore *sem)
相关函数可以在include/linux/semaphore.h中可以查看到
4.阻塞与非阻塞模式
阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程,而是直接返回。
open函数:用来打开一个设备,他返回的是一个整型变量,是文件的标识符。如果这个值等于-1,说明打开文件出现错误。
open("/dev/xxx", O_RDWR );
这是以读写方式打开某种设备文件,默认是阻塞操作。
open("/dev/xxx", O_RDWR | O_NONBLOCK);
O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选择项为此文件的本次打开操作和后续的I/O操作设置非阻塞方式。
这时候在驱动程序中也应该添加对非阻塞的处理。
static DECLARE_MUTEX(button_lock); //定义互斥锁
static int sixth_drv_open(struct inode *inode, struct file *file) { if (file->f_flags & O_NONBLOCK) { if (down_trylock(&button_lock)) return -EBUSY; } else { /* 获取信号量 */ down(&button_lock); }
return 1; }
int sixth_drv_close(struct inode *inode, struct file *file)
{
up(&button_lock);
return 0;
}
获取file的标志位,判断是否为非阻塞。如果为非阻塞,就尝试获取信号量,没用获取到信号量则立即返回。否则为阻塞模式,没用获取到信号量进行休眠,直到获取信号量。