并发与竞态
并发(Concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(Race Conditions)。 竞态的几种情况:- 对称多处理器(SMP)的多个CPU
- 单CPU内进程与抢占它的进程
- 中断(硬中断、软中断、Tasklet、底半部)与进程之间
中断屏蔽
在单CPU范围内避免竞态的一种简单而有效的方法是在进入临界区之前屏蔽系统的中断,但是在驱动编程中不值得推荐,驱动通常需要考虑跨平台特点而不假定自己在单核上运行。CPU一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。具体而言,中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免了。local_irq_disable() //屏蔽中断 ........ critical section //临界区 ........ local_irq_enable() //打开中断
底层原理是:使CPU本身不响应中断。比如,对于ARM处理器而言,其底层的实现是屏蔽ARM CPSR的I位。
local_irq_save(flags) 禁止中断操作,保存目前CPU的中断位信息。
local_irq_restore(flags) 使能中断操作,回复CPSR
local_bh_disable() 禁止底半部
local_bh_enable() 恢复底半部
Linux内核中断顶半部和底半部的理解:https://blog.csdn.net/qq_16933601/article/details/107239908
原子操作
原子操作可以保证对一个整型数据的修改是排他性的。整型原子操作:
//设置原子变量 void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为i */ atomic_t v = ATOMIC_INIT(0); /* 定义原子变量v并初始化为0 */
//获取原子变量 atomic_read(atomic_t *v); //放回原子变量的值
//原子变量加/减 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_inc_and_test(atomic_t *v) int atomic_dec_and_test(atomic_t *v) int atomic_sub_and_test(int i, atomic_t *v) //上述操作对原子变量执行自增、自减和减操作后(注意没有加),测试其是否为0,为0返回true,否 则返回false。
//操作并返回
int atomic_add_return(int i, atomic_t *v)
int atomic_sub_return(int i, atomic_t *v)
int atomic_inc_return(atomic_t *v)
int atomic_dec_return(atomic_t *v)
位原子操作:
//1.设置位 void set_bit(nr,void *addr) //2.清除位 void clear_bit(nr, void *addr) //3.改变位 void change_bit(nr, void *addr) //4.测试位 test_bit(nr,void *addr) //返回addr地址的第nr位 //5.测试并操作位 int test_and_set_bit(nr, void *addr) int test_and_clear_bit(nr, void *addr) int test_and_change_bit(nr, void *addr) //上述test_and_xxx_bit(nr,void*addr)操作等同于执行test_bit(nr,void*addr)后再执行xxx_bit(nr,void*addr)。
自旋锁
//定义自旋锁
spinlock_t lock;
//初始化自旋锁
spin_lock_init(lock)
//获得自旋锁
spin_lock(lock) //如果能获得锁马上返回,是否则,他将在那里自旋,直到自旋锁的持有者释放
spin_trylock(lock) //如果能获取锁返回ture,否则放回false,不会在原地自选等待
//释放自旋锁
spin_unlock(lock) //该宏释放自旋锁lock,它与spin_trylock或spin_lock配对使用
//自旋锁的衍生
spin_lock_irq(lock) //获取自旋锁,并关闭中断
spin_unlock_irq(lock) //释放自旋锁,并使能中断
spin_lock_irqsave(lock) //获取自旋锁,禁止中断,保存目前CPU的中断位信息
spin_unlock_irqrestore(lock) //释放自旋锁,使能中断,恢复中断位信息
spin_lock_bh(lock) //获取自旋锁,禁止底半部
spin_unlock_bh(lock) //释放自旋锁,使能底半部
出现死锁的几种情况:
- 递归使用一个自旋锁;
- 在自旋锁锁定期间调研可能引起进程调度的函数;
- 无论如何,我们在中断服务程序里也应该调用spin_lock()
读写自旋锁
实际上,对共享资源并发访问时,多个执行单元同时读取它是不会有问题的,自旋锁的衍生锁读写自旋锁(rwlock)可允许读的并发读写自旋锁是一种比自旋锁粒度更小的锁机制,它保留了“自旋”的概念,但是在写操作方面,只能最多有1个写进程,在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。//1.定义和初始化读写自旋锁 rwlock_t my_rwlock; rwlock_init(&my_rwlock); //初始化读写自旋锁 //2.读锁定 void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock); //3.读解锁 void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);
//4.写锁定
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);
//5.写解锁
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);
顺序锁
若使用顺序锁,读执行单元不会被写执行单元阻塞,也就是说,读执行单元在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。 对于顺序锁而言,尽管读写之间不互相排斥,但是如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。所以,在这种情况下,读端可能反复读多次同样的区域才能读到有效的数据。//1.获取顺序锁
void write_seqlock(seqlock_t *sl);
int write_tryseqlock(seqlock_t *sl);
write_seqlock_irqsave(lock, flags)
write_seqlock_irq(lock)
write_seqlock_bh(lock)
//2.释放顺序锁写执行单元使用顺序锁的模式如下: 读执行单元使用顺序锁的模式如下:
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)
//3.读开始
unsigned read_seqbegin(const seqlock_t *sl);
read_seqbegin_irqsave(lock, flags);
//4.重读
int read_seqretry(const seqlock_t *sl, unsigned iv);
read_seqretry_irqrestore(lock, iv, flags)
信号量
信号量(Semaphore)是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n。信号量与操作系统中的经典概念PV操作对应。
//1.定义信号量 struct semaphore sem; //2.初始化信号量 void sema_init(struct semaphore *sem, int val); //3.获取信号量 void down(struct semaphore *sem); //它会导致休眠,因此不能在中断上下文中使用 void down_interruptible(struct semaphore *sem) //与down()类似,不同之处在于down()进入睡眠状态的进程不能被信号打断,down_interruptible()进入睡眠状态的进程能被信号打断。并放回非0。 void down_trylock(struct semaphore *sem) //尝试获取型号量sem,能立刻获取并放回0,否则放回非0,不会导致调用者睡眠,可以在中断上下文中使用 //4.释放信号量 void up(struct semaphore *sem); //释放型号量,唤醒等待者
信号量的作用:
- 作为一种可能的互斥手段,信号量可以保护临界区,与自旋锁类似;
- 用于同步:一个进程A执行down()等待信号量,另外一个进程B执行up()释放信号量,这样进程A就同步地等待了进程B。
互斥体
互斥体使用方法与信号量用于互斥场景完全一样。
//定义互斥体
struct mutex my_mutex;
//初始化互斥体
mutex_init(struct mutex *lock);
//获取互斥体
void mutex_lock(struct mutex *lock);
int mutex_lock_interruptible(struct mutex *lock);
int mutex_trylock(struct mutex *lock);
//释放互斥体
void mutex_unlock(struct mutex *lock);
完成量(completion)
它用于一个执行单元等待另一个执行单元执行完某事。//定义完成量
struct completion my_completion;
//初始化完成量
//下列代码初始化或者重新初始化my_completion这个完成量的值为0(即没有完成的状态)
init_completion(&my_completion)
reinit_completion(&my_completion)
//等待完成量
void wait_for_completion(struct completion *c)
//唤醒完成量
void completion(struct completion *c) //只唤醒一个等待的执行单元
void completion_all(struct completion *c) //唤醒所有等待同一完成量的执行单元
总结
并发和竞态广泛存在,中断屏蔽、原子操作、自旋锁和互斥体都是解决并发问题的机制。中断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和互斥体应用最为广泛。自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区小。互斥体允许临界区阻塞,可以适用于临界区大的情况。
标签:控制,rwlock,int,lock,void,并发,atomic,自旋 From: https://www.cnblogs.com/tongxiguo24/p/17073952.html