一、概述
Linux设备驱动中必须解决的一个问题就是多个进程对共享资源(如全局变量、静态变量、硬件资源等)的并发访问,会导致竟态,如可能会出现以下情况:导致执行单元C独处的数据不符合预期
导致竟态发生有如下几种情况:
对称多处理器(SMP)的多个CPU:由于多个CPU使用共同的系统总线,因此可以访问共同的外设和存储器,CPU0和CPU的进程、中断都会可能造成相互之间的竟态
单CPU进程之间的抢占:由于Linux内核支持抢占调度,所有当前进程执行时可能会被其他进程抢占资源,产生竞态
中断与进程之间的抢占:中断打断正在执行的进程,如果中断服务程序访问临界资源,也会产生竞态
为了避免竞态的发生,必须保证共享资源的互斥访问,Linux提供了中断屏蔽、原子操作、自旋锁、信号量、互斥锁等途径来解决。
二、解决方法
1. 中断屏蔽
中断屏蔽是一种简单粗暴有效的方法,可以保证正在执行的内核程序不被中断处理程序所抢占,而且由于进程调度也依赖于中断,所以内核进程之间的抢占问题夜避免了。
以下函数只能屏蔽当前CPU的中断,并不能解决SMP多CPU之间的竞态。
API
local_irq_disable(); //屏蔽本CPU中断
local_irq_enable();
local_irq_save(); //除了禁止中断外,保存当前CPU的中断信息
local_irq_restore();
local_bh_disable(); //禁止底半部中断
local_bh_disable();
使用
local_irq_disable(); //屏蔽中断
... //临界区资源
local_irq_enable(); //开中断
其本质就是让CPU不再响应中断,即将ARM处理器的CPSR寄存器的I位清零。但由于Linux的异步IO和进程调度等操作都依赖于中断,所以长时间屏蔽中断可能会造成数据丢失乃至系统崩溃后果,需要尽快执行完临界区的代码
2. 原子操作
原子操作是为了保证对一个整型数据修改的排他性,分为针对位操作和整形变量的原子操作。当发生两个任务同时调用原子操作时,先调用的会失败,后调用的会成功
整型原子操作
atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
void atomic_set(atomic_t *v, int i); //设置原子变量v的值为 i
int atomic_read(atomic_t *v); //返回原子变量v的值
void atomic_add(int i, atomic_t *v); //原子变量v增加i
void atomic_sub(int i, atomic_t *v); //原子变量v减少i
void atomic_inc(atomic_t *v); //原子变量自增1
void atomic_dec(atomic_t *v); //原子变量自减1
//尝试自增/自减/减i,成功返回0
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);
//进行加i/减i/自增/自减,成功返回新值
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);
位原子操作
void set_bit(nr, void *addr); //将addr的nr位 置1
void clear_bit(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);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
3. 自旋锁
自旋锁是一种对临界资源进行互斥访问的方式,本质上就是在访问临界资源前一直执行Test-And-Set
某个内存变量的原子操作,成功则表示锁空闲可以访问,失败则表示锁被其他进程占用,一直循环获取这个锁知道成功。
spinlock_t lock; //定义自旋锁
spin_lock_init(lock); //初始化自旋锁
spin_lock(lock); //获取自旋锁,失败则一直获取
spin_trylock(lock); //尝试获取自旋锁,失败则直接返回
spin_unlock(lock); //释放自旋锁
自旋锁只能保证进程间抢占时的互斥访问,但中断依然可以打扰,所以自旋锁提供了与中断屏蔽相结合的接口
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_unlock() + local_bh_enable()
注意:
- 上述的接口也不能屏蔽另一个核的中断,因此需要在中断中也调用
spin_lock()
; - 只有在临界资源比较小,占用锁处理时间极短的情况下使用自旋锁才合理,否则会导致其它进程一直处于等待过程中影响系统性能
- 自旋锁使用时应注意避免陷入死锁,如递归使用自旋锁等情况
- 使用自旋锁访问临界资源期间也不能调用可能引起进程调度的函数如copy_from_user()等,否则可能导致内核崩溃
读写锁
读写锁是自旋锁的扩展,其特点是读时共享,写时互斥,即读时允许并发,写时最多只能有1个进程访问临界资源
读锁, 而且读和写操作互斥
rwlock_t my_rwlock; //定义一个读写锁
rwlock_init(&my_rwlock); //初始化读写锁
//读锁定
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);
//读解锁
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);
写锁
//写锁定
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);
//写解锁
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);
尝试解锁:立即返回
spin_trylock();
write_trylock();
顺序锁
顺序锁是对读写锁的扩展,特点是在读写锁基础上解除了读和写之间的互斥,即读时也可以写,写时也可以读,但写时不能写。在读操作之前检测到了写操作,读写锁的读操作会阻塞,顺序锁会一直检测
获取顺序锁
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)write_seqlock_irqsave() = loal_irq_save() + write_seqlock()
write_seqlock_irq() = local_irq_disable() + write_seqlock()
write_seqlock_bh() = local_bh_disable() + write_seqlock()
释放顺序锁
void write_sequnlock(seqlock_t *sl);
write_sequnlock_irqrestore(lock, flags)
write_sequnlock_irq(lock)
write_sequnlock_bh(lock)
write_sequnlock_irqrestore() = write_sequnlock() + local_irq_restore()
write_sequnlock_irq() = write_sequnlock() + local_irq_enable()
write_sequnlock_bh() = write_sequnlock() + local_bh_enable()
4. 信号量
信号量是操作系统中最典型的用于同步的手段,通过PV操作来实现互斥,即P:S–,V:S++,当S>0时唤醒队列中的等待信号量的进程,最典型的就是生产消费者模式,当获取不到信号量时,进程会进入休眠。适用方式如下:
struct semaphore sem; //定义信号量
void sema_init(&sem, 0); //初始化信号量初始值为0
//获取信号量
down(&sem); //获得信号量,若获取不到会睡眠,不能在中断中使用
down_interruptible(&sem); //与down()类似,该函数在睡眠中可以被信号打断
down_trylock(&sem); //尝试获取信号量,会立刻返回,成功返回0
//释放信号量,唤醒等待者
void up(&sem);
5. 互斥锁
常用API
struct mutex my_mutex;
mutex_init(&my_mutex);
void mutex_unlock(struct mutex *lock);
int mutex_lock_interruptiable(struct mutex *lock);
int mutex_trylock(struct mutex *lock);
void mutex_unlock(struct mutex *lock);
使用方式
struct mutex my_mutex;
mutex_init(&my_mutex);
mutex_lock(&my_mutex);
xxx
mutex_unlock(&my_mutex);
自旋锁和互斥锁的区别:当得不到锁时,自旋锁会占用CPU一直等待,互斥锁会睡眠。所以自旋锁适用于临界资源比较小时,其他进程可以快速执行完当前进程可以快速获取;