IMX6ULL_并发与竞争笔记

linux驱动中的并发处理

1> 什么是共享资源?什么是并发?
多个任务或者中断都能访问的资源叫做共享资源
并发就是多个用户同时访问一个共享资源

2> linux系统发生并发事件的几个主要原因
a. 多线程并发访问,linux是多任务(线程)的系统,这个是并发的最基本原因
b. 抢占式并发访问,linux内核支持抢占,即调度程序可以在任意时刻抢占正在运行的线程,从而运行其他线程
c. 中断式并发访问
d. 多核间并发访问,多核cpu的存在

3> 并发处理操作一般在编写驱动的时候就要考虑到并发与竞争,而不是在驱动编写完之后在处理并发与竞争

4> 并发与竞争操作的关键点?
在程序中的共享资源的数据而不是代码,数据一般为全局变量,设备结构体这种

5> 原子操作就是指不能在进一步分割的操作,一般用于变量或者位操作
a. 例如变量操作demo1
a = 3;
==>
这行赋值代码会先被编译为汇编指令
ldr r0,= 0X30000000 //变量a的地址
ldr r1, =3
str r1, [r0] //将变量a所在的寄存器的值赋值为3

当在同一时刻,在线程1中执行代码 a=10,在线程2中执行代码a=20就可能发生这种情况
线程1 ldr r0,= 0x30000000
线程1 ldr r1,= 10
线程2 ldr r0,= 0x30000000
线程2 ldr r1,= 20
线程1 str r1, [r0]
线程2 str r1, [r0]
==>
最后的结果是在线程1中 a=20,在线程2中a=20,这不是我们想要的结果
	==> 
	解决办法: 保证线程1中a=10对应的3个汇编指令是一个整体运行,即作为一个原子存在
	
b. 原子操作的API接口
变量操作
linux内核定义了atomic_t结构体来完成整型数据的原子操作(整型变量操作)
对于不同的位的soc来说,其表示原子操作的结构体不同
32bits soc  atomic_t == int 
64bits soc  atomic_64t == long long

32bits soc的原子操作常用demo2
atomic_t v  = ATOMIC_INIT(0);		//初始化原子变量 v = 0
atomic_set(10);						//v = 10;
atomic_read(&v);					//读取变量v的值
atomic_inc(&v)						//v++
... ... 还有其他操作(+,-,++,--,自加/自减后在判断原子变量的值是否为0)

位操作
位操作不像前面的整型操作,有专门定义的atomic_t结构体而是直接操作内存
void set_bit(int nr,void *p); 		//将地址p的第nr位置1
void clear_bit(int nr,void *p); 	//将地址p的第nr位清0
void change_bit(int nr,void *p);	//将地址p的第nr位的状态翻转
... ...

原子操作使用注意事项:
原子操作只能对整型变量或者位进行保护

6> 自旋锁
自旋锁中"自旋"的意思也就是"原地打转","原地打转"的目的是为了等待自旋锁可以用,可以访问共享资源

对于自旋锁来说,当自旋锁正被线程A所持有,线程B如果想要获取自旋锁就会处于忙循环-旋转-等待状态,即线程B不会进入睡眠状态或者说去做其他事情
它就会一直查询自旋锁的状态,直到线程A释放了自旋锁
==> 
从这里可以看出,自旋锁的一个缺点就是: 等待子旋锁的线程(线程B)因为一直处于自旋状态,会浪费处理器时间,降低系统性能
	==> 故线程A(获取了自旋锁之后的线程)对自旋锁的持有时间不能太长,即自旋锁适用于短时期的轻量级加锁

linux内核中通过使用结构体spinlock_t来表示自旋锁
typedef struct spinlock ... ...spinlock_t;

a. 最基本的自旋锁API操作函数
DEFINE_SPINLOCK(spinlock_t lock)  			//定义并初始化一个自旋变量  
int spin_lock_init(spinlock_t *lock)		//初始化自旋锁
void spin_lock(spinlock_t *lock)			//获取指定的自旋锁,也称加锁
void spin_unlock(spinlock_t *lock)			//释放指定的自旋锁
int spin_trylock(spinlock_t *lock)			//尝试获取指定的自旋锁,没有获取到就返回0
int spin_is_lock(spinlock_t *lock)			//检查指定的自旋锁是否被获取,没有被获取就返回非0,否则返回0
 
b. 单cpu下 一般死锁是怎么形成的?
linux上的 自旋锁 有三种实现: 
1. 在单cpu,不可抢占内核中, 自旋锁 为空操作。 
2. 在单cpu,可抢占内核中, 自旋锁 实现为“禁止内核抢占”,并不实现“自旋”。(注意)   我们这里分析的就是这种情况下的自旋锁
3. 在多cpu,可抢占内核中, 自旋锁 实现为“禁止内核抢占” + “自旋”。
自旋锁有一个特性就是禁止内核抢占,即当线程A获取自旋锁时,就禁止内核抢占,直到线程A释放自旋锁才会允许内核抢占

如果线程A在持有锁期间进入休眠状态,那么线程A会自动放弃CPU使用权,与此同时线程B想要获取锁,因为线程A在休眠前没有释放锁,故线程B不可能获取锁,这样线程B会一直处于自旋状态,
因为不能内核抢占,故线程B会一直占用cpu,而cpu休眠结束还是没有cpu可以使用,故形成死锁,这里还有一个条件是线程A和线程B处于同一个cpu,而且

学习网站: https://blog.csdn.net/gl_zyc/article/details/38389901?utm_source=blogxgwz6

c. 中断和自旋锁的关系
首先中断中是可以使用自旋锁的,但是中断处理函数在获取锁之前,必须禁止本地中断,否则持有锁的内核代码会被中断处理程序打断,中断抢占了cpu,然后中断接着试图去争用这个锁,
而持有锁的代码因为cpu被中断抢了,所以他没有cpu可用,就不能释放锁,中断就会一直自旋,这就是中断形成的死锁现象
注意点: 这里只需要关闭当前处理器上的中断即可,因为中断发生在不同的处理器,及时这个中断一直自旋,也不会影响锁的持有者释放锁,因为你那个中断抢占的不是我这个cpu
学习网站: https://blog.csdn.net/qinrenzhi/article/details/79799384

知识点: 中断获取自旋锁需要禁止本地(中断所在cpu下的)中断的原因是: 中断是可以抢占内核的,但是自旋锁的使用时是需要禁止内核抢占的所有你想用自旋锁
		必须按规定来

故有如下几个和中断有关的API
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags);		//保存中断状态,禁止本地中断,并获取自旋锁
void spin_unlock_irqstore(spinlock_t *lock,unsigned long flags);	//将中断状态恢复到以前的状态,重新激活本地中断,释放自旋锁

常用demo: 在线程中使用spin_lock_irqsave/spin_unlock_irqstore函数,在中断处理函数中使用spin_lock/spin_unlock函数

//定义并初始化一个自旋锁变量
DEFINE_SPINLOCK(lock);

//线程A
void functionA()
{
	unsigned long flags;
	
	//保存中断状态,禁止本地中断,获取锁
	spin_lock_irqsave(&lock,flags);
	
	//临界区域的代码
	
	//释放锁,花费之前的中断状态,激活本地中断
	spin_unlock_irqstore(&lock,flags)
}

中断处理函数
void irq()
{
	//获取锁
	spin_lock(&lock);
	
	//临界区域
	
	//释放锁
	spin_unlock(&lock);
}

d. 下半部和自旋锁的联系  下半部:HB
   如果想要在下半部中使用自旋锁需要和中断差不多,需要在获取锁之前先关闭下半部,因为下半部也会竞争共享资源
   常用函数
   void spin_lock_bh(spinlock_t *lock);		//关闭下半部,获取自旋锁
   void spin_unlock_bh(spinlock_t *lock);	//释放自旋锁,打开下半部

e. 自旋锁使用注意事项
	1. 持有锁的时间不能太长,一定有短,因为在其他代码在等待自旋锁时处于自旋状态,占用cpu
	2. 在自旋锁保护的临界区域内不能调用任何可能导致线程休眠的API函数(例如kmalloc(),copy_to_user(),msleep()... ),不然会造成死锁
	3. 不能递归申请自旋锁,这个不用想了吧,百分百死锁,递归 == 你申请你自己持有的自旋锁,申请导致自己处于自旋状态,自旋状态就不能释放锁,==我自己锁自己
	4. 考虑到驱动的可移植性,不管是单cpu还是多cpu的soc,都必须当作多核来编写驱动

7> 信号量
现实中运用信号量的具体例子 – 停车场
某个停车场有100个停车位,这100个停车位就是共享资源,你想将车停在停车场里面,那么你肯定要先看一下这个停车场现在停了多少辆车了
这里,当前停车数量就是一个信号量,具体的停车数量就是这个信号量具体的值,有车开出来,当前停车数量减一,有车停进去当前停车数量+1
这就是计数型信号量

信号量的特点:
1. 信号量可以使等待资源的线程进入休眠状态,不会在一直自旋,浪费cpu
2. 信号量不能使用于中断中,因为信号量会引起休眠,而中断不能休眠。 ==>中断不能通过信号量的形式去获取共享资源
3. 信号量不适用于共享资源持有时间短的情况,因为频繁的休眠,切换线程的开销远大于信号量带来的优势 ==> 因为信号量在使线程进入睡眠之后会切换线程,切换线程会有开销

信号量和自旋锁的区别:
信号量对等待获取共享资源的线程进行休眠处理,而自旋锁对等待获取资源的线程做自旋处理
信号量适用于操作共享资源,即临界区操作耗时的情形,而自旋锁适用于临界区域是短时期的情形
信号量中的临界区域可以调用引起睡眠的函数,但自旋锁不可以
中断不能使用信号量,而中断可以使用自旋锁

信号量和互斥体的关系
如果信号量的值为1就类似于是互斥体,此时信号量就是一个二值信号量

信号量的API函数
linux内核使用semaphore结构体来表示信号量

DEFINE_SEAMPHORE(name); 						//定义一个信号量,并将信号量的值设置为1  ==>定义一个二值信号量
void sema_init(struct semaphore *sem,int val);	//初始化信号量sem,并将该信号量的值设置为val
void down(struct semaphore *sem);				//获取信号量
void up(struct semaphore *sem);					//释放信号量

信号量的常用使用demo
//定义一个信号量变量
struct semaphore sem;

//设置该信号量的值为1
sema_init(&sem,1);	

//获取信号量
down(&sem);

//临界区域

//释放信号量
up(&sem);

8> 互斥体
将信号量设置为1,就可以使用信号量进行互斥访问了,但是linux内核提供了一个互斥体机制来实现互斥访问
什么是互斥访问?
==>
表示一次只有一个线程可以访问共享资源,不能递归申请互斥体

a. 互斥体的变量表示
   在linux内核中使用mutex结构体来表示互斥体
   struct mutex
   {
		atomic_t count;
		spinlock_t wait_lock;
		... ...
   }

b. 互斥体的特点(互斥体进制是比二值信号量更专业实习互斥访问,故互斥体和信号量很类似)
   mutex可以导致休眠,因此不能在中断中使用mutex,中断只能使用自旋锁
   和信号量一样,mutex保护的临界区域可以调用引起阻塞的API函数
   因为一次只有一个线程可以持有mutex,故不能递归上锁和解锁
   
c. 互斥体的常用API
	DEFINE_MUTEX(name);						//定义并初始化一个mutex变量
	void mutex_init(mutex *lock);			//初始化一个mutex变量
	mutex_lock(mutex *lock);				//获取mutex变量
	mutex_unlock(mutex *lock);				//释放mutex变量
	
d. 互斥体的常用demo
   //定义一个互斥体变量
   struct mutex lock;
   
   //初始化这个互斥体变量
   mutex_init(&lock);
   
   //获取互斥体变量
   mutex_lock(&lock);
   
   //临界区域
   
   //释放互斥体变量
   mutex_unlock(&lock);
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值