Linux内核的竞态和并发

目录

中断屏蔽

原子操作 

自旋锁

读写锁

顺序锁

 信号量

读写信号量

互斥体


竞态:多个任务对象同时访问系统共享资源会造成竞争的情况称为竞态。

并发:多个任务同时被执行,需要按照一定的顺序进行。

竞态产生的原因有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 <asm/atomic.h>

 位原子操作:

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 <linux/spinlock.h>

一般使用流程为:

//分配初始化
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 <linux/semaphore.h>

 一般使用流程为:

//分配初始化
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); //释放互斥体

以上这些都是解决内核竞态的方法,在不同的场景,选择合适的方法,例如访问临界区时间短适合使用自旋锁,时间长适合使用信号量,共享资源是互斥使用互斥体等。

最后,有什么疑问和建议都欢迎在评论区中提出来哟。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>