并发指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源的访问则很容易导致竞态
linux内核中主要竞态
1.多对称处理器的多个CPU 2.单CPU内进程与抢占它的进程 3.中断(硬中断、软中断、Tasklet、下半部)与进程之间
访问共享内存资源的代码区称为“临界区”,临界区需要被以某种互斥机制加以保护,中断屏蔽、原子操作、自旋锁和信号量等
是linux设备驱动中可采用的互斥途径。
这几个互斥的介绍:
1.中断屏蔽,这个主要用于单CPU,中断屏蔽将使得中断和进程之间的并发不再发生。使用方法:
local_irq_disable();//屏蔽中断
...
...
临界区
...
local_irq_enable();//开中断
由于linux的异步IO、进程调度等很多重要的操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法处理,
因此长时间的屏蔽中断很危险,有可能导致数据丢失甚至系统崩溃。所以这个不作为重点讨论。
**********************************************************************************************************************************************************
2.原子操作,原子操作是一系列的不能被打断的操作。linux内核提供了一系列的函数来实现内核中的原子操作,这些函数分为2类,分别针对位和整型变量
进行原子操作。
实现原子操作的步骤:
1.定义原子变量并设置变量值
void atomic_set(atomic_t *v , int i); //设置原子变量值为i
atomic_t v = ATOMIC_INIT(0); //定义原子变量v,初始化为0
2.获取原子变量的值
atomic_read(atomic_t *v);
3.原子变量加减操作
void atomic_add(int i,atomic_t *v);//原子变量加i
void atomic_sub(int i ,atomic_t *v);//原子变量减i
4.原子变量自增/自减
void atomic_inc(atomic_t *v);//自增1
void atomic_dec(atomic_t *v);//自减1
5.操作并测试:对原子变量执行自增、自减后(没有加)测试其是否为0,如果为0返回true,否则返回false。
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i ,atomic_t *v);
6.操作并返回
int atomic_add_return(int i , atomic_t *v);
int atomic_sub_return(int i , atomic_t *v);
int atomic_inc_return(atomic_t * v);
int atomic_dec_return(atomic_t * v);
**********************************************************************************************************************************************************
3.自旋锁
自旋锁是一个忙锁,它在一个小的循环内不断的重复测试并设置的操作。
自旋锁保护临界区的特点:临界区要小,并且临界区内不能有导致睡眠的操作,否则可能引起系统崩溃。自旋锁可能导致系统死锁,
引发这个问题最常见的情况是递归使用一个自旋锁。
自旋锁的操作步骤:
1.定义自旋锁
spinlock_t lock;
2.初始化自旋锁
spin_lock_init(lock);这是个宏,它用于动态初始化自旋锁lock;
3.获得自旋锁
spin_lock(lock);该宏用于加锁,如果能够立即获得锁,它就能马上返回,否则,他将自旋在那里,直到该自旋锁的保持者释放。
spin_trylock(lock);能够获得,则返回真,否则返回假,实际上是不在原地打转而已。
4.释放自旋锁
spin_unlock(lock);
与上面的两个配对使用。
例子:
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock); //获取自旋锁,保护临界区
。。。。临界区
spin_unlock(&lock);//释放自旋锁
自旋锁不关心锁定的临界区究竟是如何执行的。不管是读操作还是写操作,实际上,对共享资源进行读取的时候是应该可以允许多个执行单元同时
访问的,那么这样的话,自旋锁就有了弊端。于是便衍生出来一个读写锁。
它保留了自旋的特性,但在对操作上面可以允许有多个单元进程同时操作。当然,读和写的时候不能同时进行。
现在又有问题了,如果我第一个进程写共享资源,第二个进程读的话,一旦写了,那么就读不到了,可能写的东西比较多,但是第二个进程读很小,那么
能不能第一个进程写的同时,我第二个进程读呢?
当然可以,那么引出了顺序锁的概念。都是一样的操作。
**********************************************************************************************************************************************************
4.信号量
是用于保护临界区的一种常用的方法,它的使用与自旋锁差不多,但是它不在原地打转,当获取不到信号量时候,进程会进入休眠等待状态。
主要操作:
1.定义sem信号量
struct semaphore sem;
2.初始化信号量
void sema_init(struct semaphore *sem, int val);
初始化信号量,并设置sem的值为val
初始化的时候还可以这样用,init_MUTEX(sem),这是个宏 #define init_MUTEX(sem) sema_init(sem , 1)
init_MUTEX_LOCKED(sem),这是个宏 #define init_MUTEX_LOCKED(sem) sema_init(sem , 0)
3.获得信号量
void down(struct semaphore * sem);
该函数用于获得信号量sem,他会导致睡眠,所以不能在中断中使用。
int down_interruptible(struct semaphore* sem);
与上面功能类似,因为down进入睡眠状态的进程不能被信号打断,而它能被信号打断,信号也会导致该函数返回。
int down_trylock(struct semaphore * sem);
4.释放信号量
void up(struct semaphore * sem);
该函数用于释放信号量,同时唤醒等待者。
信号量一般这样使用:
DECLARE_MUTEX(sem);
down(&sem);
.....临界区
up(&sem);
linux自旋锁和信号量采用的“获取锁-访问临界区-释放锁”的方式。
*************************************************************************************************************************
5.互斥体
互斥体和信号量基本上差不多。不介绍了。
总结:并发和竞态广泛存在,这几个机制是解决问题的好方法,中断屏蔽很少单独使用,原子操作只能针对整数进行,因此,自旋锁和信号量应用最为广泛。
自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区要小。信号量允许临界区阻塞,可以适用于临界区较大的情况。读写自旋锁和读写信号量是
放宽了条件的自旋锁和信号量,他们允许多个进程并发的读取共享空间。