linux知识点介绍:内核并发和竟态
扫描二维码
随时随地手机看文章
1.概念:并发:多个执行单元同时发生;注意:执行单元包括硬件中断、软件中断、多进程(进程的抢占通过中断实现),
竟态:并发的多个执行单元同时访问共享资源,引起的竞争状态
形成竟态条件:1一定要有并发情况2一定要有共享资源 硬件资源(小到寄存器的而某个bit位)软件上的全局变量,例如open_cnt3并发的多个执行单元要同时访问共享资源
互斥访问:当多个执行单元对共享资源进行访问时,只能允许一个执行单元对共享资源进行访问,其他执行单元被禁止访问!
互斥:指多个进程不能同时使用同一个资源;
死锁:指多个进程互不相让,都得不到足够的资源;
饥饿:指一个进程一直得不到资源(其他进程可能轮流占用资源)
临界资源:系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量
临界区:访问共享资源的代码区域,所以互斥访问就是对临界区的互斥访问!
进程的同步(直接制约):synchronism
指系统中一些进程需要相互合作,共同完成一项任务。具体说,一个进程运行到某一点时要求另一伙伴进程为它提供消息,在未获得消息之前,该进程处于等待状态,获得消息后被唤醒进入就绪态。同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
进程的互斥(间接制约)mutual exclusion
由于各进程要求共享资源,而有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥。某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
例如:
cpu读取数据到寄存器,修改数据,写回数据
static int open_cnt=1;
int uart_open(struct inode*inode,struct file *file){
unsigned long flags;
local_irq_save(flags); //屏蔽中断
if(--open_cnt!=0)
{
printk("已经被打开!n");
open_cnt++;
//local_irq_retore(flags);//使能中断
rerturn -EBUSY;
}
local_irq_retore(flags);//使能中断
printk("打开成功n");
return 0;
}
由于linux内核支持进程之间的抢占,当A在执行以上临界区时,一定要进行互斥访问,不让B进程发生抢占情况,保证A的执行路径不被打断!
总结:互斥访问本质上其实就是让临界区的而执行路径具有原子性(不可再发-不能被打断)。
问题:如何做到一个执行单元在访问临界区。其他执行单元不被打断正在访问临界区的执行单元的路径呢?强调:执行单元:中断和进程。
2.linux内核产生竟态的情形
第一种是多核(多cpu),多个cpu他们共享系统总线,共享内存,外存,系统io单只竟态;
第二种是单cpu的进程之间的抢占(必须具备抢占,抢占的原因是进程能够指定优先级),
由于linux内核支持进程的抢占,多个进程访问共享资源,并且有抢占,也会产生竟态;
第三种情形是中断(硬件中断,软中断)和进程也会形成竟态(中断的优先级高于进程)
第四种是中断和中断.硬件中断优先级高于软中断,软中断又分优先级。
3.linux内核解决竟态的方法:
这些方法的本质目的就是让临界区的访问具有原子性。
1.中断屏蔽 2.原子操作 3.自旋锁 4.信号量
1.中断操作:屏蔽的硬件中断和软中断
能够解决竟态情形:1进程与进程之间的抢占(由于linux进程的调度,抢占都是基于软中断实现)2中断和进程 3中断和中断 中断发生时会向多个cpu发送中断信号.
linux内核提供相关的中断屏蔽的方法:#include
1.屏蔽中断
unsigned long flsgs;
local_irq_disable();//屏蔽中断
local_irq_save(floags);//屏蔽中断并且保存中断状态到flags中
2.使能中断
unsigned long flags;
local_irq_enable();//使能中断
local_irq_restore(flags); //使能中断,并且从flags中恢复屏蔽中断前保存的中断状态
linux内核中断屏蔽的使用方法:
1.临界区之前屏蔽中断
2.执行临界区,中断不能被打断,进程的抢占也不能发生
3.临界区之后恢复中断
2.原子操作:
笔试题:请实现将一个数的某个bit置1或者清0;
第一个:
int data =0x1234;data |=(1<<5);data &=~(1<<5);
第二个
void set_bit(int nbit,int *data){
.....
}
第三个:
void set_bit(int nbit,void *data){
....
}
第四个:
#define SET_BIT(nr,data)......
基于linux系统的参考答案(GNU C)--析取c++的优秀代码:
inline void set_bit(int nr,void *data){
.....
}
linux内核原子操作:
原子操作能够解决所有竟态问题:
原子操作分为:位原子操作 整形原子操作
1位原子操作:如果以后驱动中对共享资源进行位操作,并且为了避免竟态问题,一定要使用内核提供的位原子操作的方法,保证位操作的方法,保证位操作的过程是原子的,不能自己去实现位操作,例如:
static int data; //全局变量,共享资源
//临界区
data |=(1<<5); //这个代码不是原子的,有可能被别的任务打断!
如果不考虑多核引起的竟态,还有一种通过中断屏蔽(屏蔽只是单核)来解决以上代码的竟态问题:
insigned long flags;
local_irq_save(flags);
data |=(1<<5);
local_irq_restore(flags);
以上代码无法表面多核引起的竟态!
内核提供的位原子操作的方法:
#include
set_bit/clear_bit/change_bit/test_bit组合函数
Tset_and_set_bit/test_and_clear_bit/test_and_change_bit
对于data进行位操作的正确方法
static int data; //将data数据的第5位设置为1
set_bit(5,&data); //这个代码时原子的,不能被别的任务打断
注意:以上函数在多核cpu情况下,会使用两条arm的原子指令:ldrex,strex,这个两条保证在cpu那一级别能够避免竟态,以上函数都是采用c的内嵌汇编来实现,如果用c语言来实现,编译器肯定用ldr,str,但这个两条指令不能避免竟态!
案例:利用位原子操作将0x5555->0xaaaa,不允许使用change_bit函数!取反操作
2整形原子操作:
如果以后驱动程序中,涉及的共享资源是整型数,就是原型要定义为char short int
long型的数据,并且他们是共享资源,为了避免竟态,可以考虑使用内核提供的整型原子
操作机制来避免竟态问题。说白了就是将原先的char short int long型换成atomic_t数据类型即可,然后配合内核提供的整型原子操作的函数对整型变量进行数学运算。
整形原子变量数据类型定义#include
typedef struct{
volatile int counter;
}atomic_t;
如何使用:分配整型原子变量atomic_t v;
进行对整型变量的操作:
atomic_set/atomic_read/atomic_add/atomi_sub/atomic_inc/atomic_dec、/atomic_inc_and_test。。
对整型变量的操作一定要使用以上的函数进行,保证具有原子性。不能使用如下代码:
static int data; //全局变量,共享资源
//临界区
data++;//不是原子的!有可能被打断
解决的方法:如果不考虑多核:
unsigned long flags;
local_irq_save(flags);
data++;
loacl_irq_restore(flags);
如果考虑多核:atomic data;atomic_inc(&data);
注意:以上整型原子操作的函数,如果在多核情况下,他们的实现都是c的内嵌汇编
来实现的,都调用了ldrex,strex来避免竟态.
案例;实现led灯驱动,要求这个设备只能被一个应用程序打开
分析:明确:app会调用open开发设备,close关闭设备
驱动:一定要提供对应的底层open,close的实现,注意不能省略
底层的这两个函数,因为需要在底层的open,close函数中做一些用户需求的代码(设备只能被一个应用程序打开)
方案:static int open_cnt; //可以采用中断
方案;static atomic_t open_cnt; //利用整型原子操作 top查看进程状态
实验步骤:
1.insmod led_drv.ko
2.cat /proc/devices //查看申请的主设备号
3.cat /sys/class/myleds/myled/uevent //查看创建设备文件的原材料
4.ls /dev/myled //查看设备文件
5../led_test &、、启动a进程,让其后台运行,a进程进入休眠
6.ps //查看a进程的pid
7.top //查看a进程的状态和cpu的利用率,内存使用率
8../led_test //启动b进程
9.kill a进程的pid //杀死a进程
*******************************************************************************
自旋锁:等于“自旋” + “锁”
自旋锁特点:
1.自旋锁一般要附加在共享资源上;类似光有自行车锁没有自行车是没有意义的!
2.自旋锁的“自旋”不是锁在自旋,意思是想获取的自旋锁的执行的单元,在没有获取自旋锁的情况下,原地打转,忙着等待获取自旋锁;
3.一旦一个执行单元获取了自旋锁,在执行临界区时,不要进行休眠操作。 “不够意思”
4.自旋锁也是让临界区的访问具有原子性!
linux内核如何描述一个自旋锁#include
Typedef struct {
Raw_spinlock_t lock;
}spinlock_t;
Typedef struct {
Volatile unsigned int lock;
}raw_spinlock_t;
如何使用自旋锁来对临界区进行互斥访问:
static int open_cnt; //全局变量,共享资源
1.分配自旋锁spinlock_t lock
2.初始化自旋锁spin_lock_init(&lock);
3.访问临界区之前获取自旋锁,进行锁定spin_lock(&lock); //如何执行单元获取自旋锁,函数立即返回,如果执行单元没有获取锁,执行单元不会返回,而是原地打转!处于忙等待,直到持有自旋锁的执行单元释放自旋锁或者:
spin_trylock(&lock); //如果执行单元获取自旋锁,函数返回true,如果没有获取咨询所,返回false,不会原地打转!
4.执行临界区的代码
if(--opencnt!=0){
.......
}
这个过程其他cpu或者本cpu的抢占进程就无法执行临界区,但是还会被中断锁打断!如果考虑中断的因素,要使用衍生自旋锁。
5.释放自旋锁
spin_unlock(&lock); //获取锁的执行单元释放锁,然后等待获取锁的执行单元停止原地打转而是获取自旋锁,然后开始对临界资源的访问。
注意;以上自旋锁的操作只能解决多cpu和本cpu的进程抢占引起的竟态,但是无法处理中断(中断和中断底半部)引起的竞态,如果考虑到中断,必须采用衍生自旋锁!
衍生自旋锁本质上其实就是在普通的自旋锁的基础上进行屏蔽中断和使能中断的动作。
衍生自旋锁的使用:static int open_cnt; //全局变量,共享资源
1.分配自旋锁spinlock_t lock
2.初始化自旋锁spin_lock_init(&lock);
3.访问临界区之前获取自旋锁,进行锁定
spin_lock_irq(&lock); //屏蔽中断,获取自旋锁
=local_irq_disable() + spin_lock()
或者spin_lock_irqsave(&lock,flags); //屏蔽中断,保存中断状态,获取自旋锁
= lock_irq_save() + spin_lock();
4.执行临界区的代码
if(--opencnt!=0){
.......
}
5.释放自旋锁spin_unlock_irq(*lock); //释放自旋锁,使能中断
Local_irq_disable() + spin_lock();
或者spin_unlock_irqrestore(&lock,flags); //释放自旋锁,使能中断,保存中断状态= spin_unlock() + local_irq_restore();
注意:衍生自旋锁能够解决所有的竟态问题。
自旋锁使用的注意事项:
1. 一旦获取自旋锁,临界区的执行速度要快,不能做休眠动作。由于获取锁是一直等待,所以临界区较大或有共享设备的时候,使用自旋锁会降低系统性能.
2. 自旋锁可能导致死锁:--递归调用,也就是已经拥有自旋锁的CPU想要第二次获取锁
--获取自旋锁之后再被阻塞,所以,再自旋锁占有期间,不能调用可能引起阻塞的函数:如kammoc(),copy_from_uesr()等.
案例:利用自旋锁,来实现一个设备只能被一个应用程序打开
******************************************************************
linux系统进程的状态:三个状态
1.进程的运行状态,linux系统描述运行中的进程通过TASK_RUNNING宏来表示!
2.进程的准备就绪状态,linux系统描述准备就绪用TASK_READY来表示.
3.进程的休眠状态,进程的休眠状态又分可中断的休眠状态和不可休眠的。
3.1不可中断休眠状态,linux系统用TASK_UNINTERRUPTIBLE来表示,睡眠期间如果
接受到了信号,不会立即处理信号,但是唤醒以后会判断是否接受到信号,有,处理信号。
3.2可中断的休眠状态,linux系统用TASK_INTERRUPTIBLE,在休眠期间,如果接受到了信号,会被信号唤醒,并且立即处理信号。
信号量--不可以在中断上下文使用(因为会导致休眠)
由于自旋锁在使用的时候,要求临界区不能做休眠操作,但是在某些场合需要在临界区做休眠操作,又要考虑竟态问题,此时可以使用信号量来保护临界区。
信号量的特定:1.又叫”睡眠锁“,内核的信号量在概念和原理上与用户态的信号量是一样的,但是它不能再内核之外使用;
2.如果一个执行但愿你想要获取信号量,如果信号量已经被别的执行单元所持有,那么这个执行单元将会进入休眠状态;直到持有信号量的执行单元释放信号量为止。
3.已经获取信号量的执行单元在执行临界区的代码时,也可以进行休眠操作!
4.明确信号量能让进程放入一个等待队列中,然后让其进行休眠操作,
linux内核描述信号量的数据类型:#include
struct semaphore{
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
一般使用方法:DECLARE_MUTEX(semm);//定义一个互斥信号量
..
Down(&semm);//获取信号量,保护临界区
Critical section code; //执行临界区代码
Up(&semm); //释放信号量
如何使用信号量:1.分配信号量struct semaphore sema;
2.初始化信号量为互斥信号量init_MUTEX(&sema,1);
3.在访问临界区之前获取信号量,对临界区进行锁定
dowm(&sema); //获取信号量 ,如果信号量已经被别的任务给持有,d为不可中断的信号状态那么,进程将进入不可中断的休眠状态;不能使用在中断上下文。
或者:dowm_interruptible(&sema); //获取信号量,如果信号量已经被别的任务给持有,那么进程将进入可中断的休眠状态;一般在使用的时候一定要对这个函数的返回值进行判断,如果函数返回0,表明进程正常获取信号量,然后访问临界区;如果函数返回非0,表明进程是由于接受到了信号引起的唤醒:
if(dowm_interruptible(&sema)){
printk("进程被唤醒的原因是接受到了信号n");
return -EINTR;
}
else {
printk("正常获取信号量引起的唤醒");
printk("进程可以访问临界区");
} 或者:
dowm_trylock(&sema); //获取信号量,如果没有获取信号量,返回false,
如果获取信号量,返回true。所以对返回值也要做判断
if(dowm_trylock(&sema)){
printk("无法获取信号量");
return -EBUSY;
}else {
printk("获取信号量");
printk("访问临界区");
}
4.访问临界区
5.释放信号量up(&sema); //一方面释放信号量,另一方面还要唤醒之前休眠的进程
案例;采用信号量,实现一个设备只能被一个应用程序打开; 一般设置为可中断的状态
对于这个案例
gpc0_3
gpio_config{
//gpio共享资源 互斥访问
unsigned long flags;
local_irq_save(flags);
gpio_set_vluae(0);
udelay(5);
gpio_set_vluae(1);
udelay(10);
gpio_set_vluae(0);
udelay(15);
........
local_irq_restore(flags);
}