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