并发竞态
Linux设备驱动必须解决的问题:
多个进程对共享资源并发访问,并发访问易导致竞态
并发:
多个执行单元同时、并行执行
竞态:
并发的执行单元同时访问共享资源(硬件资源和软件上的全局变量等)易导致竞态
竞态产生的情况
对称多处理器(SMP)的多个CPU核
竞态可能发生于CPUx进程和CP3Uy进程、CPUx进程和CPUy的中断之间以及CPUx 中断和CPUy的中断之间
单CPU内进程被高优先级进程打断
高优先级的进程也要访问共享资源,这就易导致竞态
中断(硬中断、软中断、Tasklet、底半部)与进程之间
可以打断进程的中断刚好也访问了共享资源,可能导致竞态
解决竞态问题
解决办法:
保证对共享资源的互斥访问(一个执行单元访问共享资源时,其他执行单元不能访问)
临界区:
访问共享资源代码区域称为临界区
Linux中常用的互斥手段:
可能造成程序出错原因:
ARM的屏障指令:
Linux的互斥锁、自旋体等用到上面的指令
Linux内核中将上面指令写成函数:
mb() //读写屏障rmb() //读屏障wmb() //写屏障
中断屏蔽
正常的CPU都有开关中断的能力,通过开关中断保护临界区,可以防止某些竞态发生。但是不建议这么做,Linux的进程调度是依赖中断进行的,当临界区过长,带来可能是导致其他进程、中断无法响应等,以及中断屏蔽只能屏蔽本CPU的中断,当是SMP时,中断屏蔽不能解决多核CPU带来的竞态问题
使用方法:
local_irq_disable() //屏蔽中断……… //临界区local_irq_enable() //使能中断
local_bh_disable() //禁止中断底半部local_bh_disable() //使能中断底半部
原子操作
Linux的原子操作分两类:
两种原子操作都依赖底层CPU的原子操作原子操作可以防止多核之间的并发,以及单核内部之间的并发。ARM处理器实现原子操作指令:ldrex和strex
整形变量原子操作:
1、设置原子变量的值
void atomic_set(atomic *v, int i) //设置原子变量的值为iatomic_t v = ATOMIC_INIT(0) ; //定义原子变量V并初始化为0
2、获取原子变量的值
atomic_read(atomic_t *v) //返回值原子变量的值
3、原子变量值加/减
void atomic_add(int i,atomic_t *v)//原子变量加ivoid atomic_sub(int i, atomic_t *v)//原子变量减i
4、原子变量自增或自减
int atomic_inc(atomic_t *v)//原子变量自增1int atomic_dec(atomic_t *v)//原子变量自减1
5、操作原子变量并测试原子变量是否为零
int atomic_inc_and_test(atomic_t *v)//自加1并测试原子变量是否为0,为0返回true,否则返回falseint atomic_dec_and_test(atomic_t *v)//自减1并测试原子变量是否等于0
6、对原子变量进行加/减或自加/自减操作,并返回新的值
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、设置位,将addr中的nr位设置为1
voidset_bit(int nr, void *addr)
2、清除位,将addr中第nr位设置为0
voidclear_bit(int nr, void *addr)
3、将addr地址的第nr位进行反置
voidchange_bit(int nr, void *addr)
4、返回addr地址的nr位
inttest_bit(int nr, void *addr)
5、返回并操作位
inttest_and_set_bit(int nr, void *addr);//设置addr地址第nr位为1,并返回第nr位的值inttest_and_clear_bit(int nr, void *addr);//设置addr地址的第nr位为0,并返回第nr位的值inttest_and_change_bit(int nr, void *addr)
使用原子变量使设备只能被一个进程打开:
staticatomic_t led_avaliable = ATOMIC_INIT(1);staticintled_open(struct inode *inode, struct file *filp){if(!atomic_dec_test(&led_avaliable)) //原子变量已经为0,打开失败 {atomic_inc(&led_avalible);//恢复原子变量的值为0 }return0; //打开成功}staticintled_release(struct inode *inode, struct file *filp){atomic_inc(&led_avalible);//设备关闭,原子变量自加1return0;}
自旋锁
通俗理解:
自旋锁看做一个变量,通过不断查询还变量的值来标记某个执行单元是否总有自旋锁(原子操作)。A执行单元先执行拥有了自旋锁,B执行单元想要获取自旋锁执行就会被阻塞,知道A执行单元释放自旋锁,B才将获得自旋锁执行。
自旋锁的使用:
spinlock_t lock; //定义自旋锁spin_lock_init(&lock);//初始化自旋锁spin_lock(&lock);//获取自旋锁,保护临界区... //临界区...spin_unlock(&lock); //释放自旋锁
主要解决SMP或单CPU内核可抢占的情况。中断和底半部仍会影响临界区。
再多核SMP情况下,获得自旋锁的核抢占调度也会暂时禁止,但其他核的抢占调用不会被禁止。
结合中断和底半部的自旋锁机制:
//自旋锁与开关中断的结合spin_lock_irq() = spin_lock() + local_irq_disable()spin_unlock_irq() = spin_unlock() + local_irq_enable()//自旋锁与开关中断保存恢复状态字的结合spin_lock_irqsave() = spin_lock() + local_irq_save()spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()//自旋锁和底半部的结合spin_lock_bh() = spin_lock() + local_bh_disable()spin_unlock_bh() = spin_lock() + local_bh_enable()
多核编程中进程和中断可能访问同一个临界区时进程和中断中的自旋锁使用:
进程上下文:
spin_lock_irqsave();... //临界区资源spin_unlock_irqrestore();
中断上下文:
spin_lock();... //临界区spin_unlock();
避免了一切CPU核之间并发的可能性。
谨慎使用自旋锁:
1、自旋锁是忙等待锁,没有获得就会一直等。一旦一个临界区过长的执行单元获得自旋锁,就会长时间占用自旋锁,其他程序难得到运行,会降低系统性能。所以使用自旋锁的临界区执行时间必须要极短
2、 自旋锁可能完成死锁: 一个已经拥有自旋锁的CPU再次想要获得自旋锁,CPU将死锁
3、在自旋锁锁定期间不能调用可能引起进程调度的函数,如msleep()等,可能导致内核崩溃
4、在中断服务程序中必须调用spin_lock(),不然CPU变多核依然存在并发问题:spin_lock_irqsave()不能屏蔽另一个CPU的中断
使用自旋锁使设备只能被一个进程打开:
int xxx_cout = 0;spinlock_t xxx_lock;staticintxxx_open(struct inode *inode, struct file *filep){ spin_lock(&xxx_lock);if(xxx_cout) //已经打开 { spin_unlock(&xxx_lock);return -EBUSY; } xxx_cout++; //没有打开,标志打开次数 spin_unlock(&xxx_lock);//解锁 ...return0;}staticintxxx_release(struct inode *inode, struct file *filp){ ... spin_lock(&xxx_lock);//加锁 xxx_cout--;//使用次数减1 spin_lock(&xxx_lock);//解锁return0;}
读写自旋锁
1、从自旋锁衍生而来2、允许读并发即可以多个进程读3、不允许写并发即只能一个进程写4、读写不能同时进行
1、定义初始化读写自旋锁
rwlock_t xxx_rwlock;rwlock_init(&xxx_rwlock);
2、读锁定
voidread_lock(rwlock_t *lock);voidread_lock_irqsave(rwlock_t *lock, unsignedlong flags);voidread_lock_irq(rwlock_t *lock);voidread_lock_bh(rwlock_t *lock);
3、读解锁
voidread_unlock(rwlock_t *lock);voidread_unlock_irqrestore(rwlock_t *lock, unsignedlong flags);voidread_unlock_irq(rwlock_t *lock);voidread_unlock_bh(rwlock_t *lock);
4、写锁定
voidwrite_lock(rwlock_t *lock);voidwrite_lock_irqsave(rwlock_t *lock, unsignedlong flags);voidwrite_lock_irq(rwlock_t *lock);voidwrite_lock_bh(rwlock_t *lock);intwrite_trylock(rwlock_t *lock);//尝试写加锁,不能加锁立即返回
5、写锁定
voidwrite_unlock(rwlock_t *lock);voidwrite_unlock_irqrestore(rwlock_t *lock, unsignedlong flags);voidwrite_unlock_irq(rwlock_t *lock);voidwrite_unlock_bh(rwlock_t *lock);
读写自旋锁的使用:
读写自旋锁读操作:
rwlock_t lock;rwlock_init(&lock);read_lock(&lock); //读数据时获得读锁... //临界区read_unlock(&lock);//读解锁
读写自旋锁写操作:
write_lock_irqsave(&lock, flags); //写数据时获得锁... // 临界区write_unlock_irqrestore(&lock, flags); //写解锁
顺序锁(seqlock)
1、读写锁的一种优化2、顺序锁保护的资源进行写操作时仍可以读无需等待写操作完成3、写操作也不需要等待所有读操作完成才进行写操作4、写执行单元与写执行单元之间仍然是互斥的5、读操作期间又进行写操作,必须重新进行读操作才能读取最新数据
定义顺序锁:
seqlock_t seqlock;
写操作:
1、获得顺序锁
void write_seqlock(seqlock_t *sl);int write_tryseqlock(seqlock_t *sl);void write_seqlock_irqsave(seqlock *lock, unsigned long flags);void write_seqlock_irq(seqlock_t *lock);void write_seqlock_bh(seqlock_t *lock);
2、释放顺序锁
void write_sequnlock(seqlock_t *sl);void write_sequnlock_irqsave(seqlock *lock, unsigned long flags);void write_sequnlock_irq(seqlock_t *lock);void write_sequnlock_bh(seqlock_t *lock);
写操作用法:
write_seqlock(&seqlock);...... //写操作代码wtite_sequnlock(&seqlock);
读操作:
1、读开始在对共享资源访问前调用下面函数,返回的是顺序锁的当前顺序号
intread_seqbegin(constseqlock_t *lock); intread_seqbegin_irqsave(constseqlock_t *lock, unsignedlong flags);
2、重读读操作访问顺序锁保护的共享资源,检查在读期间是否有写操作,有写操作就需要重新读
int read_seqretry(const seqlock_t *s1, unsigned iv);int read_seqretry_irqrestore(const seqlock_t *lock, unsigned iv, unsigned long flags);
模板操作:
do{ seqnum = read_seqbegin(&seqlock); ... //读操作代码}while(read_seqretry(&seqlock));
RCU(read-copy-update,读-复制-更新)
- 写执行单元访问共享资源前先复制一个副本,对副本进行修改,使用回调机制在适当时机(所有引用该数据的CPU退出共享数据读操作)把指向原来的数据的指针指向新的被修改的数据。等待适当时机这一时期称为宽限期
- 相当于读写锁高性能版本,允许多个读执行单元和多个写执行单元同时访问被保护数据
1、读锁定
rcu_read_lock();rcu_read_lock();
2、读解锁
rcu_read_unlock();rcu_read_unlock_bh();
RCU读临界区:
rcu_read_lock();... //临界区rcu_read_unlock();
3、同步RCU
写执行单元调用,阻塞写执行单元,知道CPU所有读执行单元完成读临界区。并不等待后续的读临界区操作
sysnchronize_rcu();
4、挂接回调函数
voidcall_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu));
信号量
- 与自旋锁类似,自旋锁获取不到锁会进程会原地打转,进程不会休眠,信号获取不到时进程会进入休眠
- 信号量的P操作: 将信号量的值减1,信号量值大于等于0,进程继续执行,否则为等待状态,进去等待队列
- 信号量的V操作:将信号量的值加1,信号量值大于0会唤醒队列所有的等待信号量的进程
structsemaphoresem;//定义信号量voidsema_init(struct semaphore *sem, int val); //初始化信号量的值为valvoiddown(struct semaphore *sem); //获取信号量(P操作),会使进程进入睡眠(不能被信号打断),中断上下文中不能调用voiddown_interruptible();//获取信号量(P操作),进程会进入睡眠(能被信号打断)intdown_trylock(struct semaphore *sem);//尝试获取信号量,获取成功返回0,不能立即返回非0值,不会导致进程睡眠,可在中断上下文使用voidup(struct semaphore *sem);//释放信号量(V操作)
互斥体
- 新的linux内核倾向使用mutex互斥体作为互斥手段
- 互斥体是进程级的,竞争失败,会发生进程上下文切换,当前进程进入睡眠状态
structmutexmy_mutex;//定义互斥体mutex_init(&my_mutex);//互斥体初始化voidmutex_lock(struct mutex *lock);//获取互斥体,导致进入睡眠的进程不能被信号打断voidmutex_lock_interrupt(struct mutex *lock);//获取互斥体,导致进入睡眠的进程可以被信号打断intmutex_trylock(struct mutex *lock);//尝试获取互斥体,不能获取立即返回,不会阻塞导致进程睡眠
互斥体和自旋锁是应用最广泛的互斥手段
自旋锁和互斥体选用的三个原则:
1、锁不能获取时,互斥体开销是进程上下文切换时间,自旋锁是等待获取锁的时间(临界区的执行时间决定)。所以,临界区较小,使用自旋锁;临界区较大,使用互斥体2、互斥体保护的临界区可能包含引起阻塞代码,自旋锁要避免临界区调用这样可以引起阻塞的代码,一旦进程切换,另一个进程尝试获取自旋锁便会造成死锁3、互斥体存在进程上下文中,被保护资源要在中断或者软中断中使用时必须选择自旋锁。一定要使用互斥体也要使用不阻塞的获取方法——mutex_trylock()