Linux内核的竞态与并发(中断屏蔽、原子操作、自旋锁、信号量、互斥体的互斥机制)
文章目录一、基本概念:二、解决竞争状态的策略:系列文章目录前言一、pandas是什么?二、使用步骤1.引入库2.读入数据总结一、基本概念:● linux内核中产生竞态的原因SMP对称多处理器 (多核CPU)比如都要操作LCD进程和进程之间的抢占共享资源,进程和中断之间发生共享资源的抢占,中断和中断之间的资源抢占(中断是有优先级的)比如:LCD 网卡可见的内存 (文件 共享内存 全局变量)。● 并发
文章目录
所有的热爱都要不遗余力,真正喜欢它便给它更高的优先级,和更多的时间吧!
关于 LINUX驱动 的其它文章请点击这里: LINUX驱动
一. 基本概念
● linux内核中产生竞态的原因
- SMP对称多处理器 (多核CPU)
比如都要操作LCD - 进程和进程之间的抢占共享资源,进程和中断之间发生共享资源的抢占,中断和中断之间的资源抢占(中断是有优先级的)
比如:LCD 网卡 可见的内存 (文件 共享内存 全局变量)。
● 共享资源
- 文件、硬件设备、共享内存、内核中的全局变量等
● 并发
- 多任务同时执行,对于单核的CPU来说,宏观上并行,微观上串行。而并发的执行单元对共享资源的访问则很容易导致竞态(Race Conditions)
● 临界区
- 访问共享资源的代码段
对某段代码而言,可能会在程序中多次被执行,每次执行的过程我们称作代码的执行路径。当两个或多个代码路径要竞争共同的资源的时候,该代码段就是临界区。
二. 解决竞争状态的策略
常用一下四种策略:(速记:中原武林很自信)
1)中断屏蔽(内核空间)
不推荐使用
2)原子操作(内核空间)
事务的原子性:要么做完 ,要么不做
3)自旋锁(内核空间)
自旋锁相应快,逻辑不允许重入,要等待锁释放的
4)信号量 (用户空间)
相对慢,要从睡眠态唤醒
1. 中断屏蔽
中断屏蔽可以保证正在执行的内核执行路径不被中断处理程序抢占,防止竞态的产生,但内核的正常运行依赖于中断机制。在屏蔽中断期间,任何中断都无法得到处理,而必须等待屏蔽解除。所以关中断的时间要非常短, 如果关中断时间过长,可能直接造成内核崩溃,建议在写驱动过程中尽量不使用。
使用流程为:关中断----访问共享资源----开中断
使用方法如下:
local_irq_disable()
local_irq_enable()
//更安全的:
local_irq_save() //保存中断的状态(开/关) 关闭中断
local_irq_restore() //恢复保存的中断状态
2. 原子操作
原子操作底层表现为一条汇编指令(ldrex、strex)。所以他们在执行过程中不会被别的代码路径所中断。
事务的原子性就是要么做完 要么不做。而如何实现的原子性不被打断,不需要去关注,内核中实现的原子操作都是与CPU架构息息相关的,只需要掌握原子的使用方法即可。
很好理解,用上厕所的例子来说明。厕所就是共享资源,去上厕所的行为被称作代码路径。
原子操作就是大家每次上厕所都用时非常短,短到什么程度呢,只要一条汇编指令的时间。当然拉的量也非常少(只改变一个整型或者是位)。所以就不存在抢厕所的问题了。
2.1 位原子操作
// arch/arm/include/asm/bitops.h
set_bit(nr, void *addr) // addr内存中的nr位置1
clear_bit
change_bit
test_bit
...
2.2 整型原子操作
使用步骤:
//1)定义原子变量 atomic_t tv; //就是用原子变量来代替整形变量
//核心数据结构:
typedef struct {
int counter;
} atomic_t;
//2) 设置初始值的两种方法
tv = ATOMIC_INIT(0); //① 定义原子变量 v 并初始化为0
atomic_set(&tv, i) //② 设置原子变量的值为 i
//3) 操作原子变量
int atomic_read(atomic_t *v) //返回原子变量的值
atomic_add(int i, atomic_t *v); //v += i
atomic_sub(int i, atomic_t *v); //v -= i
atomic_inc(atomic_t *v); //v++;
atomic_dec(atomic_t *v) //v--
...
代码过长,具体代码:Linux内核的竞态与并发——原子操作实例
3. 自旋锁
多处理器之间设置一个全局变量V,表示锁。并定义当V=1时为锁定状态,V=0时为解锁状态自旋锁同步机制是针对多处理器设计的,属于忙等机制。
自旋锁,逻辑不允许重入,要等待锁释放的,注意以下:
1) 自旋锁的获取与释放逻辑上要保证成对出现
2) 只允许一个持有单元,获取锁不成功原地自旋等待
3) 临界区中不能调用引起阻塞或者睡眠的函数
4) 临界区执行速度要快, 持有自旋锁期间,整个系统几乎不做任务切换,持有自旋锁时间过长,会导致整个系统性能严重下降
5) 避免死锁, A,B互相锁死,可以建议使用spin_trylock(&btn_lock)
还是用上厕所的例子:这次给厕所上把锁,只有拥有这个锁钥匙的人A才能进厕所。进去后把锁锁上,外面的人B急得团团转(自旋),A出来后把锁释放,在门口等着的B拿了钥匙赶紧开了锁进去了。但是缺点就是,B在外面团团转,没有功夫去做别的事情,所以一旦A 上厕所的时间很长,B就浪费了很长时间在自旋上。对系统的性能有所影响。
使用步骤:
// 1)定义一个自旋锁变量:
spinlock_t btn_lock;
// 2) 初始化自旋锁 :
spin_lock_init(&btn_lock)
// 3) 获取自旋锁 (获取权利)
spin_lock(&btn_lock); //获取自旋锁不成功,原地自旋等待,直到锁被释放,获取成功才返回
//或:
int spin_trylock(&btn_lock);//不成功,直接返回一个错误信息,调试的时候可用,可以避免死锁
// 4) 访问共享资源
// 5) 释放自旋锁
spin_unlock(&btn_lock);
自旋锁还有很多衍生自旋锁:读锁 写锁 顺序锁 内核的大锁:
// 1)定义一个自旋锁变量:
spinlock_t btn_lock;
// 2) 初始化自旋锁 :
spin_lock_init(&btn_lock)
// 3) 获取自旋锁 (获取权利)
unsigned long flags;
spin_lock_irq(&lock); // = spin_lock() + local_irq_disable()
//或
spin_lock_irqsave(&lock, flags); // = spin_lock() local_irq_save()
// 4) 访问共享资源
// 5) 释放自旋锁
spin_unlock_irq(&lock); // = spin_unlock()+ local_irq_enable()
//或
spin_unlock_irqrestore(&lock, flags); // = spin_unlock() + local_irq_restore()
Q:编程时有可能需要在临界区代码中执行阻塞睡眠函数 怎么办?
A:这时可以考虑使用信号量来保护临界区。
4 信号量
在用户空间只有进程的概念。当一个临界区有多个用户态进程竞争时,最好的方法是用信号量保护这个临界区。只有得到信号量进程才能执行临界区代码,当获取不到信号量时,进程进入休眠状态。
因此,我们可以说,信号量是进程级的互斥机制,它代表进程来争夺共享资源,如果竞争失败,就会发生进程上下文切换,当前进程进入睡眠状态,CPU运行其他进程。
此外,信号量在SMP(对称多处理器)系统同样起作用;内核中的信号量也只能用于内核态编程
比方说:一间公共厕所N 个坑位,N 不为 1, 且 N为有限个,算是N个资源。在同一时间可以容纳N个人,当满员的时候,外面的人必须等待里面的人出来,释放一个资源,然后才能在进一个,当他进去之后,厕所又满员了,外面的人还得继续等待……
● 特点:
a.基于自旋锁机制实现的
b.可以有多个持有者,获取信号量不成功睡眠等待
c. 可以调用引起阻塞或者睡眠的函数
d. 用信号量保护的临界区执行速度相对慢(见图二 )
● 内核中关于信号量的核心数据结构
struct semaphore {
raw_spinlock_t lock;
unsigned int count;//计数器
...
};
● 使用步骤:
// 1)定义一个信号量
struct semaphore btn_sem;
// 2) 初始化信号量
void sema_init(&btn_sem, 5); //该信号量可以被5个执行单元持有
//还可以通过以下宏完成信号量的定义和赋值为1
DEFINE_SEMAPHORE(btn_sem);
// 3) 获取信号量,本质就是给其中的计数-1(获取权利)
//成功立即返回,失败使用调用者进程进入睡眠状态(深度睡眠kiii -9都杀不死) ,
//直到可以获取信号量成功才被唤醒、返回
void down(struct semaphore *sem);
//成功立即返回,失败进入可中断的睡眠状态(潜睡眠,可被ctrl+c打断)
//可以获取信号量 + 收到信号(ctrl+c)
int down_interruptible(struct semaphore *sem); //关注返回值
//失败立即返回一个错误信息,不会导致睡眠
//可以在中断上下文中使用
int down_trylock(struct semaphore *sem);
//失败进入可以kill的睡眠状态
int down_killable(struct semaphore *sem);
//获取信号量,指定超时时间为x
//如果获取信号量不成功,对应的进程进入睡眠状态
//可能因为信号量可用而被唤醒/也可能因为定时时间到而被唤醒
int down_timeout(struct semaphore *sem, long jiffies);
// 4) 执行临界区代码,访问共享资源
// 5)释放信号量,本质就是给计数器+1
void up(struct semaphore *sem);
5 互斥体
在 FreeRTOS 和 UCOS 中也有互斥体,将信号量的值设置为 1 就可以使用信号量进行互斥访问了,虽然可以通过信号量实现互斥,但是 Linux 提供了一个比信号量更专业的机制来进行互斥,它就是互斥体—mutex。互斥访问表示一次只有一个线程可以访问共享资源,不能递归申
请互斥体。在我们编写 Linux 驱动的时候遇到需要互斥访问的地方建议使用 mutex。 Linux 内核
使用 mutex 结构体表示互斥体,定义如下(省略条件编译部分):
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};
在使用 mutex 之前要先定义一个 mutex 变量。在使用 mutex 的时候要注意如下几点:
①、 mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。
③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并
且 mutex 不能递归上锁和解锁。
参考:
● [Linux]互斥机制(中断屏蔽、原子操作、自旋锁、信号量)
● Linux内核并发和竟态 (解决竟态的5种方式屏蔽中断,原子操作,自旋锁,信号量,互斥体
关于 LINUX驱动 的其它文章请点击这里: LINUX驱动
更多推荐
所有评论(0)