linux驱动系列学习之并发(七)

一、并发

1. 并发        

        并发指多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源、软件资源上的全局变量、静态变量)的访问很容易导致竞态。典型的并发导致竞态的是多个电脑共享打印机。

在linux中,发生竞态的主要有如下情况:
1)  对称多处理器(SMP)的多个CPU
2)  单个CPI内进程与抢占它的进程
3)    中断(硬中断、软中断、Tasklet、底半部)与进程之间。
在多核CPU中,由于CPU乱序执行代码,可能如下情况。

cpu0:x=1;
cpu1:x=2;printf("x:%d\n",x);

不能认定,输出的x一定是2。因此,需要引入了内存屏障的指令。

2. 内存屏障

        内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制。例如arm的屏障指令包括:
1) DMB(数据内存屏障):在DMB之后的显式内存访问执行前,保证所有在DMB指令之前的内存访问完成。
2) DSB(数据同步屏障):等待所有在DSB指令之前的指令完成(包括缓存、跳转预测、TLB维护等操作)。
3) LSB(指令同步屏障):FLush流水线,使得所有LSB之后执行的指令都是从缓存或内存中获得的。
linux内核的自旋锁、互斥体顶互斥逻辑,需要用到上述指令,请求锁时,调用屏障指令,释放锁时,也用屏障指令。

3. 死锁

如果在一个系统中以下四个条件同时成立,那么就能引起死锁:

  1. 互斥:至少有一个资源必须处于非共享模式,即一次只有一个进程可使用。如果另一进程申请该资源,那么申请进程应等到该资源释放为止。
  2. 占有并等待:—个进程应占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有。
  3. 非抢占:资源不能被抢占,即资源只能被进程在完成任务后自愿释放。
  4. 循环等待:有一组等待进程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。

只有四个条件必须同时成立才会出现死锁。循环等待条件意味着占有并等待条件,这样四个条件并不完全独立。

二、Linux避免竞态的方式

0. 中断屏蔽

        避免CPU竞态的一种简单的方式是使用中断屏蔽,但在驱动中不推荐这种方式。使用方式如下:

local_irq_disable();   //关闭中断
local_irq_enable();   //打开中断

直接关闭\打开中断,过于粗暴,可能会引起意想不到的问题。关闭中断,也会导致Linux系统的调度器无法运行,该方法并不推荐使用。Linux系统里面更推荐的是使用原子操作、互斥锁

1. 原子操作
        

        原子操作可以保证对一个整形数据的修改是排他性的,linux内核提供了一系列API实现内核的原子操作。


1.1. 整形原子操作

1.1.1 设置 

void atomic_set(atomic_t* v,int i);
atomic_t    a = ATOMIC_INIT(i);  //初始化一个原子变量

1.1.2 读取

atomic_read(atomic_t* v);
int i = atomic_read(v);

1.1.3加/减

void atomic_add(int i, atomic_t* v);
void atomic_sub(int i, atomic_t* v);

1.1.4自增/减

void atomic_inc(atomic_t* v);
void atomic_dec(atomic_t* v);

1.1.5操作并测试

int atomic_sub_and_test(int i, atomic_t* v);
int atomic_inc_and_test(atomic_t* v); 
int atomic_dec_and_test(atomic_t* v);

对原子变量操作自增、自减、减操作后,判断是否为0,为0返回true,否则返回false
1.1.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);

对原子变量进行加、减、自增、自减操作后,返回新值。

1.2. 位原子操作

1.2.1. 设置位

void set_bit(nr,void *addr);  //将add地址的第nr位,置1

1.2.2. 清除位    

void clear_bit(nr,void *addr); //将add地址的第nr位,置0

1.2.3. 改变位

void change_bit(nr,void *addr); //将add地址的第nr位,取反

1.2.4. 测试并操作位

int test_and_set_bit(nr,void *addr);
int test_and_clear_bit(nr,void *addr);
int test_and_change_bit(nr,void *addr);    

对addr测试并对nr位进行操作。


2. 自旋锁

    自旋锁是一种典型的对临界资源进行互斥访问的手段。其名字来源于它的工作方式。为了获得自旋锁,进行会一直检测并等待,像原地打转一样。,即所谓"自旋"。自旋锁只要被获取,其他的进程要获取就需要原地等待。自旋锁有如下操作。
2.1 定义自旋锁

spinlock_t lock;

2.2 初始化

spin_lock_init(lock);

2.3 获取自旋锁

spin_lock(lock);      //获取自旋锁,若能获得,则返回true,否则原地等待,知道自旋锁被释放。
spin_trylock(lock);   //获取自旋锁,若能获得,则返回true,否则返回false,"不在原地打转版本"。

2.4 释放自旋锁

    spin_unlock(lock);

自旋锁针对多核CPU和单核CPU可抢占式的情况,对于单核且不可抢占的CPU,自旋锁退化为空造作。尽管获得自旋锁不会被别的cpu或进程抢占,但会受到中断和底半部的影响(BH),故还有各种自旋锁的延伸操作。如spin_lock_irq()、spin_unlock_irq()。这些函数能给自旋锁更好的"安全带",避免突如其来的中断的影响。
一般,在进程上下文中调用spin_lock_irqsave()/spin_unlock_irqrestore()。在中断上下文中,调用
spin_lock()/spin_unlock()。
对于驱动来说,还有如下问题
1). 自旋锁实际上是忙等待,cpu什么也不做。临界区代码应该执行时间短,否则会降低系统的性能。
2) 自旋锁的忙等待可能会导致死锁。操作系统课程里面死锁的四个条件之一。
3) 自旋锁在锁定期间不能调用可能引起进程调度的函数。如果获取自旋锁锁期间调用如copy_from_user()、copy_to_user()、kmalloc()、msleep()等,可能导致内核崩溃。
4) 在单核cpu下的编程,也应该认为自己是在多核环境下运行,保证驱动的通用性。除了基础版本的自旋锁,还有自旋锁的衍生版本,如读写自旋锁、顺序自旋锁等等。


3. 信号量


    信号量是操作系统中经典的用于同步和互斥的手段,信号量的值可以为0,1.....n。针对消费者/生产者问题较为适合。
配合PV操作。
P(s):
    1. 信号量s减1。
    2. 若s>=0,进程继续执行,否则进入等待队列。
V(s):
    1. 信号量s加1
    2. 若s>0,则唤醒等待队列的进程。
3.1 定义信号量 

struct semaphore s;

3.2 初始化信号量

 void sema_init(struct semaphore *sem, int val);

3.3 获取信号量

void down(struct semaphore *sem); 
//该函数用于获取信号量sem,会导致睡眠,故不能用于中断上下文中。
void down_interruptible(struct semaphore *sem); 

        与down类似,但因down_interruptible进入睡眠的进程能被信号打断,信号也会导致该函数返回,这时返回值非0。

 void down_trylock(struct semaphore *sem); 

  获取信号量,能获取则返回0,否则非0。不会导致休眠,可以用于中断上下文中。
3.4 释放信号量    

void up(struct semaphore *sem);

4. 互斥体


用于多个进程并发的访问共享资源。
4.1 定义互斥体

struct mutex m;

4.2 初始化

mutex_init(&m);

4.3 获取互斥体

void mutex_lock(struct mutex *m);
int  mutex_lock_interruptible(struct mutex *m);
int  mutex_trylock(struct mutex *m);

4.4 释放互斥体

void mutex_unlock(struct mutex *m);

互斥和自旋锁是不同层级的互斥方式,互斥体实现依赖于自旋锁,自旋锁是更底层的方式。互斥是进程级的,进程上下文切换比较消耗资源,只有当进程占用资源时间较长时,互斥体才是好的选择。
互斥体和自旋锁选用的原则:
1) 当锁不能被获取时,使用互斥体的开销时进程上下文切换时间,使用
自旋锁的开销时等待获取自旋锁(由临界区执行时间决定)。若临界区较小,宜用自旋锁,
若临界区较大,应使用互斥体。
2) 互斥体所保护的临界区可包含可能引阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
3.)互斥体存在于进程上下文,因此,如果被保护的共享资源需要在中断或者软中断情况下使用,则只能使用自旋锁。
若一定使用互斥体,则需要使用mutex_trylock(struct mutex *m),不能获取就立即返回。

5. 完成量


用于一个执行单元等待另一个执行单元执行完某事。
5.1 定义完成量

struct completion c;

5.2 初始化

init_completion(&m);
reinit_completion(&m);

5.3 等待完成量

void wait_for_completion(&m);

5.4 唤醒完成量

void completion(&m);     //只唤醒一个等待的执行单元
void completion_all(&m);  //释放所有等待同一完成量的执行单元。

参考书:Linux设备驱动开发详解(基于最新的Linux4.0内核) 宋宝华著

              Linux设备驱动程序   J & G著  

猜你喜欢

转载自blog.csdn.net/zichuanning520/article/details/124635244
今日推荐