并发是指多个执行单元同时,并行的被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量,静态变量)的访问很容易导致竞态。在linux 内核中,主要的竞态发生在以下几种情况:
1 对称多处理器(SMP)的多个CPU
2 单CPU内进程与抢占它的进程
3 中断(硬中断,软终端,TASKlet,底半部)与进程之间
上述并发的情况除了SMP是真正的并行以外,其他都是宏观并行,微观串行。
解决竞态问题的本质的途径是保证对共享资源的互斥访问,访问共享资源的代码区称为临界区 ,临界区需要用某种互斥的机制加以保护。中断屏蔽,原子操作,自旋锁,信号量等是linux常采用的方法,下面一一介绍这几种方法。
第一种方法 中断屏蔽
在进入临界区之前屏蔽系统的中断,这样可以保证正在执行代码不被抢占,中断屏蔽将使得中断与进程之间的并发不在发生,而且由于linux内核的进程调度等操作都依赖中断实现,所以内核抢占进程的并发也避免了。
当然这两个函数,都只是使能本CPU内的中断,并不能解决SMP多CPU引发的竞态,他适合与自旋锁联合使用
第二种方法:原子操作
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。linux 内核提供了一系列的函数来实现,这些函数分为两类,分别针对位和整形变量,他们可以安全的调用不被打断。这些函数和CPU架构密切相关。
如下代码给出使用实例,它用于使设备最多只能被一个进程打开。
static atomic_t xxx = ATOMIC_INIT(1)//定义原子变量
static xxx_open( *****)
{
if(!atomic_dec_and_test(&xxx))
{
atomic_inc(&xxx);
return -EBUSY ;//已经打开
}
.....
.....
return 0;
}
staic int xxx_release(.....)
{
atomic_inc(&xxx);
}
如果没有进程在使用该驱动,原子变量值为1,将原子变量减1为0,函数返回true,在!true为假,
如果再有进程来打开驱动0-1 =-1 返回false,取反,运行里面的代码,原子变量恢复的1
第三种 自旋锁的使用
理解自旋锁的最简单方法是把它看作一个变量,改变量把一个临界区标记为“我当前正在运行,请稍等一会”或者标记为“我当前不再运行,可以被使用”
自旋锁主要针对SMP或单个cpu但内核可抢占的情况。尽管使用自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是还是会受到中断的影响。所以应该配合使用。
只有在占用锁很短的情况才适合使用自旋锁,自旋锁实际上是忙等锁,当临界区很大或者较长时间占用锁时,使用自旋锁会降低性能。
第四种 信号量的使用
信号量是用于保护临界区的一种常用方法,和自旋锁不同的是,当获取不到信号量时不会原地打转,而是进入休眠。
static DECLARE_MUTEX(xxx_lock)
static int xxx_open(...)
{
if(down_trylock(&xxx_lock))//获取到信号量返回0,说明没有进程尝试使用
return busy;
return 0;
}
使用信号量也可用于同步,同步意味着一个执行单元的继续执行需要等待另一执行单元完成某事,保证先后顺序,如图所示执行单元A在执行代码区域b之前必须等待执行单元b完成c区域代码。
或者使用“完成量这个机制”
自旋锁VS信号量
他们都是解决互斥问题的基本手段,但是如何选择使用哪一种呢?选择的依据是临界区的性质和系统的特点。
在多CPU情况下可以使用自旋锁来保证互斥。信号量是进程级的,用于多个进程之间资源的互斥,如果竞争失败,会发生上下文切换,当前进程进入睡眠状态,CPU将运行其他进程,只有当进程占用资源比较长时,用信号量才是比较好的选择。
当所访问的临界区时间比较短时,使用自旋锁,因为它节省上下文切换时间,CPU得不到锁,在那里空转,直到解锁为止。
信号量所保护的临界区可包含可能引起阻塞的代码(例如copy_from_user),阻塞意味进程的切换,进程被切换出去,另一个进程企图获取锁,那么就会发生死锁。对于信号量而言,如何获取不到信号量,就会休眠,CPU执行其他的进程,所以系统还是能运行。
并发和竞态广泛存在,中断屏蔽,原子操作,自旋锁和信号量都是解决并发问题的机制,中断屏蔽很少被单独使用,原子操作只能针对整数进行,因此自旋锁和信号量是用的做广泛的。