目录
中断屏蔽
原子操作
自旋锁
读写锁
顺序锁
信号量
读写信号量
互斥体
竞态:多个任务对象同时访问系统共享资源会造成竞争的情况称为竞态。
并发:多个任务同时被执行,需要按照一定的顺序进行。
竞态产生的原因有4种情况:
1、SMP(对称多处理器),就是多核cpu之间可能会同时访问共享资源,而发生竞态。
2、单cpu内进程与进程,当两个进程并发的访问共享资源。
3、进程与中断,当进程正在访问共享资源,而中断打断了正在执行的进程,而发出中断的进程与被打断的进程之间也可能发生竞态。
4、中断与中断,中断之间会发生嵌套,所以也可能会发生竞态。
临界区:访问共享资源的代码区称为临界区,这段代码区就是需要各种方法和机制去保护的,以防止由于竞态时发生的资源读写错误。
内核竞态问题中主要的解决方法:中断屏蔽,原子操作,自旋锁,信号量,互斥体。
在执行临界区代码之前屏蔽中断,临界区代码执行完毕之后解除中断屏蔽,这样程序在进行的时候就不会被中断打断了。但是频繁使用中断屏蔽或者长时间屏蔽中断造成系统响应变慢,导致系统运行故障。尽量少使用中断屏蔽,如果使用也要尽快恢复。
中断屏蔽的代码实现比较简单。
local_irq_disable() //关闭全部中断
local_irq_enable() //开启全部中断local_irq_save(flags) //屏蔽已开启的的中断,保留标志
local_irq_restore(flags) //根据标志恢复被屏蔽的中断
通常不会使用开启,关闭全部中断。
unsigned long flags;
local_irq_save(flags);//屏蔽已开启的中断并且保留当前标志
临界区代码;
local_irq_restore(flags);//恢复屏蔽前的状态
原子操作指在执行过程中不可中断,分为位原子操作和整形原子操作。
需要的头文件为:
#include
位原子操作:
void set_bit(int nr, void *addr) //设置addr地址所指的第nr位为1
void clear_bit(int nr, void *addr) //清空addr地址所指的第nr位为0
void change_bit(nr, void *addr) //翻转addr地址所指的第nr位值
int test_bit(nr, void *addr) //返回addr地址所指的第nr位值
int test_and_set_bit(nr, void *addr) //返回addr地址所指的第nr位值,并设置为1
int test_and_clear_bit(nr, void *addr) //返回addr地址所指的第nr位值,并清空为0
int test_and_change_bit(nr, void *addr) //返回addr地址所指的第nr位值,并翻转
示例:
unsigned long num = 0;
set_bit(5,&num); //将num的第5位设置为1
change_bit(8,&num); //将num的第8位的值翻转
test_and_clear_bit(3,&num); //将num第3位的值返回,并清空为0
整型原子操作:
atomic_t x = ATOMIC_INIT(0) //定义并初始化原子变量x为0
void atomic_set(atomic_t *v, int i) //设置原子变量的值为i
void atomic_add(int i, atomic_t *v) //原子变量加i
void atomic_sub(int i, atomic_t *v) //原子变量减i
void atomic_inc(atomic_t *v) //原子变量自增1
void atomic_dec(atomic_t *v) //原子变量自减1
int atomic_add_return(int i,atomic_t *v) //原子变量加i,并返回其值
int atomic_sub_return(int i,atomic_t *v) //原子变量减i,并返回其值
int atomic_inc_return(atomic_t *v) //原子变量自减1,并返回其值
int atomic_dec_return(atomic_t *v) //原子变量自减1,并返回其值
int atomic_inc_and_test(atomic_t *v) //原子变量加1,结果为 0 就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v) //原子变量减1,结果为 0 就返回真,否则返回假
int atomic_sub_and_test(int i,atomic_t *v) //原子变量减 i,结果为 0 就返回真,否则返回假int atomic_add_negative(int i, atomic_t *v) //原子变量加 i,结果为负就返回真,否则返回假
示例:
//初始化一个原子变量
atomic_t x = ATOMIC_INIT(0);
//或者
atomic_t x;
atomic_set(&x,0);
atomic_read(&x); //读取原子变量的值
atomic_add(5, &x); //原子变量加5
atomic_dec(&x); //原子变量减1
使用整型原子变量实现驱动只允许一个进程访问。
以上是32位整型原子操作函数,如果使用64位的SOC就需要使用64位的整型原子操作函数。将函数名中atomic前缀换为atomic64,将 int 换为 long long。
一种对临界区进行互斥访问的方法,在访问临界区之前加锁,获取不到就会原地自旋(循环忙等待),临界区执行完毕以后解锁。
需要的头文件为:
#include
一般使用流程为:
//分配初始化
spinlock_t lock;
spin_lock_init(&lock);
//获取锁(选取一种)
spin_lock(&lock); //获取不到原地打转
spin_trylock(&lock);//获取不到直接返回,成功返回真,失败返回假
//临界区代码
临界区代码执行必须很快,不能睡眠/阻塞
//释放锁
spin_unlock(&lock);
如果竞态中有中断的参与,需要使用衍生自旋锁(自旋锁+中断屏蔽)
spin_lock_irq(&lock) = spin_lock(&lock) + local_irq_disable()
spin_unlock_irq(&lock) = spin_unlock(&lock) + local_irq_enable()
spin_lock_irqsave(&lock,flags) = spin_lock(&lock) + local_irq_save(flags)
spin_unlock_irqrestore(&lock,flags) = spin_unlock(&lock) + local_irq_restore(flags)
自旋锁实质是忙等锁,因此在占用锁时间极短的情况下,使用锁才是合理的,反之则会影响系统性能。
自旋锁还有两种扩展:读写锁和顺序锁 。
有读锁和写锁两种,允许多个并发的读操作,不允许多个并发的写操作。简单来说就是读进程访问共享资源时,允许其他读进程也访问,但不允许写进程访问。写进程访问共享资源时,不允许读进程访问,也不允许写进程访问。
rwlock_t my_rwlock=RW_LOCK_UNLOCKED //静态初始化
rwlokc_t my_rwlock
rwlock_init(&my_rwlock) //动态初始化
void read_lock(rwlock_t *lock) //读锁定
void read_unlock(rwlock_t *lock) //读解锁
void read_lock_irq(rwlock_t *lock) //读锁定+关中断
void read_unlock_irq(rwlock_t *lock) //读解锁+开中断
void read_lock_irqsave(rwlock_t *lock,unsigned long flags) //读锁定+关中断
void read_unlock_irqrestore(rwlock_t *lock,unsigned long flags) //读解锁+开中断
void write_lock(rwlock_t *lock) //写锁定
void write_unlock(rwlock_t *lock) //写解锁
void write_lock_irq(rwlock_t *lock) //写锁定+关中断
void write_unlock_irq(rwlock_t *lock) //写锁定+开中断
void write_lock_irqsave(rwlock_t *lock,unsigned long flags) //写锁定+关中断
void write_unlock_irqrestore(rwlock_t *lock,unsigned long flags) //写解锁+开中断
int write_trylock(rwlock_t *lock) //获取不到直接返回,成功返回真,失败返回假
对读写锁的优化,当读进程访问共享资源时,允许其他读进程也访问,也允许一个写进程访问。当写进程访问共享资源时,允许读进程访问,也不允许写进程访问。在有读写进程同时访问共享资源时,写进程发生了写操作,那么读进程要重新开始,以保证数据的完整性。顺序锁的性能是非常好的,同时他允许读写同时进行,大大的提高了并发性。
//读执行单元在访问共享资源时调用该函数,返回锁s1的顺序号
unsigned read_seqbegin(const seqlock_t *s1)
//读执行单元在访问共享资源时调用该函数,返回锁s1的顺序号+关中断
read_seqbegin_irqsave(lock,flags)
//在读结束后调用此函数来检查,是否有写操作,若有则重新读。iv 为锁的顺序号
int read_seqretry(const seqlock_t *s1,unsigned iv)
//在读结束后调用此函数来检查,是否有写操作,若有则重新读。iv 为锁的顺序号+开中断
read_seqretry_irqrestore (lock,flags)
void write_seqlock(seqlock_t *s1)
void write_sequnlock(seqlock_t *s1)
write_seqlock_irq(lock)
write_sequnlock_irq(lock)
write_seqlock_irqsave(lock,flags)
write_sequnlock_irqrestore(lock,flags)
int write_tryseqlock(seqlock_t *s1)
注意 :顺序锁保护的共享资源不含有指针,因为在写执行单元可能使得指针失效,但读执行单元如果此时访问该指针,将导致错误。
一种用于同步于互斥的方式,信号量本质上是睡眠锁,获取不到信号量进入睡眠状态。使用信号量,临界区时间可以很长,也允许睡眠。
需要的头文件为
#include
一般使用流程为:
//分配初始化
struct semaphore sem;
sema_init(&sem,初始值);
//获取信号量(P操作)
void down(struct semaphore *sem);//进程获取不到信号量,进入不可中断的睡眠
//进程获取不到信号量,进入可中断的睡眠 ,可以被信号唤醒
//返回值0表示获取到信号量唤醒,返回非0表示被信号打断
int down_interruptible(struct semaphore *sem);
//获取不到信号量也直接返回不睡眠,返回非0,获取到信号量返回0
int down_trylock(struct semaphore *sem);//可以在中断中使用
//临界区代码
时间可以很长,允许睡眠
//释放信号量(V操作)
up(&sem);//释放信号量,还会唤醒等待的进程
信号量也有读写信号量,与自旋锁和读写自旋锁的关系类似。
分为读信号量和写信号量两种,获取读信号量之后可以继续获取读信号量,但是不允许获取写信号量。获取写信号量之后,既不允许获取读信号量,也不允许获取写信号量。
提供的函数有:
init_rwsem(&sem) //初始化信号量
down_read () //读信号量
down_read_trylock()up_read () //释放读信号量
down_write () //写信号量
down_write_trylock ()up_write(); //释放写信号量
mutex是睡眠等待类型的锁,当线程抢互斥锁失败的时候,线程会陷入休眠,这和信号量差不多。但是互斥体同一时间只能有一个线程去访问共享资源,表示改共享资源是互斥。
提供的函数有:
mutex_init(&mutex) //初始化互斥体
void mutex_lock(struct mutex *lock) //获取互斥体
int mutex_lock_interruptible(struct mutex *lock) //进入休眠的进程能别信号打断,返回非零
int mutex_trylock(struct metex *lock) //获取不到互斥体不会进入睡眠
int mutex_is_locked(struct mutex *lock) //如果 mutex 被占用返回1,否则返回 0
void mutex_unlock(struct mutex *lock) //释放互斥体
struct mutex mutex; //定义初始化互斥体
mutex_init(&mutex);
metex_lock(&mutex); //获取互斥体
//临界区
mutex_unlock(mutex); //释放互斥体
以上这些都是解决内核竞态的方法,在不同的场景,选择合适的方法,例如访问临界区时间短适合使用自旋锁,时间长适合使用信号量,共享资源是互斥使用互斥体等。
最后,有什么疑问和建议都欢迎在评论区中提出来哟。