文章目录
一、概念
(一)概念
1. 什么是竞态
竞态:当多个进程通过驱动同时访问临界资源时,竞态就会产生。
- 注:什么是临界资源
- 多个进程可以同时访问的相同的可以产生竞态的代码区,就称之为临界资源
2. 产生竞态的根本原因
对于单核处理器,如果内核支持抢占(一个进程(高优先级)抢走了另一个进程(低优先级)的时间片),就会产生竞态
对于多核处理器,核与核之间本身就是并行执行,会产生竞态
中断和进程间也会产生竞态,中断优先级高于进程
- 注:arm架构下,中断和中断之间不允许嵌套,因此
中断和中断之间会产生竞态是错误的说法 - M4核允许中断嵌套
(二)解决竞态的方法
中断屏蔽
中断屏蔽就是将中断临时关闭掉,只针对单核有效
抢占就是通过中断实现的,关闭中断就不会再存在抢占
中断屏蔽保护的临界区比较小,中断屏蔽的时间要 尽可能短,即不能执行任何延时,耗时,休眠操作。
如果时间长就会导致用户数据丢失或者内核崩溃。因此一般不使用这种方法
local_irq_disable() //关中断
//临界区
local_irq_enable() //开中断
自旋锁
当一个进程获取自旋锁后,如果此时有另一个进程也想获取这把锁,后一个进程处于自旋状态
自旋状态会持续消耗CPU资源
自旋锁保护的临界区域要尽可能的小,不能够有延时、耗时、休眠操作,此外不能有copy_to_user和copy_from_user、schedule函数等类似可以自动放弃cpu的函数
可以在中断上下文使用
信号量
当一个进程获取到信号量之后,如果此时有另一个进程想要获取这个信号量,后一个进程将进入休眠状态。
休眠状态不会消耗CPU资源
互斥体
当一个进程获取到互斥体之后,如果此时有另外一个进程也想获取这个互斥体,互斥体在获取不到资源时,会适当等一会再进入休眠状态,如果再等的期间有资源了就不会休眠了。
互斥体本身具备排它性,所以互斥体又叫做排它锁。
原子操作
原子操作主要强调的原子性,原子性就是将整个操作看成一个不可分割的整体,如果执行就必须一次性执行完成,过程中不能被打断。
二、自旋锁
(一)概念
当一个进程获取自旋锁后,如果此时有另一个进程也想获取这把锁,后一个进程处于自旋状态(相当于后一个进程会一直询问自旋锁是否解锁)
- 注:自旋状态会消耗CPU资源
因此更适用于临界区比较小的情况
(二)特点
- 自旋锁是针对多核设计的
- 自旋锁的自旋状态需要消耗CPU资源
- 自旋锁保护临界区比较小,里面不能有延时、耗时或者休眠的操作,在自旋锁的临界区中也不能有copy_to_user、copy_from_user等操作
- 注:copy_to_user、copy_from_user函数是允许被打断的,因此可能会在某个时间片放弃CPU,而使用自旋锁目的就是在上锁期间一直持有CPU,不允许被人打断,因此自旋锁的临界区内不允许加类似的会放弃CPU的函数
- 自旋锁可能会产生死锁,自旋锁死锁会导致进程一直占用CPU,可能会造成死机
- 注:
- 多线程死锁,会处于休眠态,其他进程可以正常运行
- 自旋锁死锁,不会处于休眠,CPU一直被占用,会产生死机的状态
- 自旋锁可以工作在中断上下文(进程->中断->进程),可以在中断处理函数中加锁
- 补:
- 进程上下文:进程状态切换的过程
- 中断上下文:有中断参与的状态的切换就称为中断上下文
- 自旋锁在上锁前会关闭抢占(并没有把所有中断关闭)
即使高优先级进程进入也不会抢占CPU
(三)自旋锁API
1. 定义自旋锁
spinlock_t lock;
2. 初始化锁
spin_lock_init(&lock)
3. 上锁
void spin_lock(spinlock_t *lock); //上锁,可以被中断打断
void spin_lock_irq(spinlock_t *lock); //上锁,不可以被中断打断
4. 解锁
void spin_unlock(spinlock_t *lock);
void spin_unlock_irq(spinlock_t *lock);
(四)使用示例
三、信号量
(一)概念
当一个进程获取到信号量之后,如果此时有另一个进程想要获取这个信号量,后一个进程将进入休眠状态。
注意,是进程进入休眠状态,而非CPU休眠。
(二)特点
- 信号量获取不到资源时会进入休眠状态,不消耗cpu资源
- 信号量对多核有效
- 信号量保护的临界区可以很大,里面可以有延时,耗时,甚至休眠的状态
- 信号量工作在进程上下文,不能工作在中断中
- 信号量不会产生死锁(在内核层面,进程会永久处于休眠态,但是CPU可以执行其他进程,不会死锁)
- 信号量在上锁前不会关闭抢占
(三)信号量的API
1. 定义信号量
struct semaphore mysem;
2. 初始化信号量
void sema_init(struct semaphore *sem, int val)
//第二个参数val在使用的时候值一般设置为1,代表只能被获取一次,再次获取就阻塞
3. 获取信号量
void down(struct semaphore *sem);
int down_trylock(struct semaphore *sem)
//尝试获取信号量,获取成功返回0,获取失败返回1,不会阻塞
4. 释放信号量
void up(struct semaphore *sem);
(四)信号量使用示例
四、互斥体
(一)概念
当一个进程获取到互斥体之后,如果此时有另外一个进程也想获取这个互斥体,后一个进程处于休眠状态。互斥体本身具备 排它性,所以互斥体又叫做排它锁。
信号量只有在 val 设置为1时,才具有排他性。
(二)特点
- 互斥体获取不到资源时不消耗cpu资源
- 互斥体保护的临界区可以很大,里面可以有延时,耗时,甚至休眠的状态
- 互斥体工作在进程上下文
- 互斥体不会产生死锁(在内核层面,信号量只会锁一个进程)
- 互斥体在上锁前不会关闭抢占
- 互斥体在获取不到资源时,会适当等一会再进入休眠状态,如果再等的期间有资源了就不会休眠了,所以进程上下文不确定临界区大小时,优先选择互斥体
因此临界区小的时候,没有获取到互斥体的进程就不会休眠,因此临界区小的时候效率高于信号量
(三)互斥体的API
1. 定义互斥体
struct mutex lock;
2. 初始化互斥体
mutex_init(&lock);
3. 上锁
void mutex_lock(struct mutex *lock);
void mutex_trylock(struct mutex *lock); //尝试上锁,成功返回1,失败返回0
4. 解锁
void mutex_unlock(struct mutex *lock)
(四)互斥体使用示例
五、原子操作
(一)概念
原子操作根据它的原子特性来命名的,强调原子性。
原子性就是将整个操作看成一个不可分割的整体,如果执行就必须一次性执行完成,过程中不能被打断。
(二)特点
原子操作内部通过两个步骤保证原子性:
1.关闭多核执行,保证只有 一个核 能够操作原子变量
2.对原子变量的修改通过 内联汇编(在C语言中运行汇编代码)完成
C语言定义变量,首先会放到内存中,然后被加载到cache中,之后才会被取到寄存器中; 汇编定义变量会直接放到寄存器中
本质就是对变量的操作,没有阻塞、自旋、休眠等特性
(三)原子操作的API
typedef struct {
volatile int counter;
} atomic_t;
注意必须使用接口进行赋值,而不能自己定义结构体赋值
自己赋值的操作就是使用C语言方式进行赋值,没有了内联汇编的特性了
1. 定义并初始化原子变量
atomic_t atm = ATOMIC_INIT(1);
2. 上锁
atomic_dec_and_test(&atm);
将原子变量的值减1然后和0比较,
如果结果为0,说明获取资源成功,成功返回真;否则返回假
3. 解锁
atomic_inc(&atm);
将原子变量的值加1
-----------------------------------------------------------
1.定义并初始化原子变量
atomic_t atm = ATOMIC_INIT(-1);
2.上锁
int atomic_inc_and_test(&atm)
让原子变量的值加1,然后和0比较如果结果为0,表示获取资源成功,成功返回真
3.解锁
atomic_dec(&atm)
将原子变量的值减1
- 注:在使用原子变量时,如果获取资源失败,在退出程序之前,需要重新加回或减回原子变量,否则后续无法正常释放资源。
(四)原子操作使用示例