本文简介
Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导致竞态。
Linux提供了多种解决竞态问题的方式,这些方式适合不同的应用场景。7.1节描述了并发和竞态的概念及发生场合。7.2~7.5节分别讲解了中断屏蔽、原子操作、自旋锁和信号量等并发控制机制。7.6节讲解了增加并发控制后的globalmem的设备驱动。
7.1 并发与竞态
并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问很容易导致竞态(race conditions)。例如,对于globalmem设备,假设一个执行单元A对其写入3000个字符“a”,而另一个执行单元B对其写入4000个字符“b”,第三个执行单元C读取globalmem的所有字符。如果执行单元A、B的写操作如图7.1所示的顺序执行,执行单元C的读操作不会有问题。但是,如果执行单元A、B如图7.2所示的顺序执行,而执行单元C又“不合时宜”地读,则会读出3000个“b”。
图7.1 并发执行单元的顺序执行
图7.2 并发执行单元的交错执行
比图7.2更复杂、更混乱的并发大量地存在于设备驱动中,只要并发的多个执行单元存在对共享资源的访问,竞态就可能发生。在Linux内核中,主要的竞态发生于如下几种情况。
1、对称多处理器(SMP)的多个CPU
对称多处理器是一种紧耦合、共享存储的系统模型,其体系结构如图7.3所示,它的特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
图7.3 对称多处理器体系结构
2、单CPU内进程与抢占它的进程
Linux2.6内核支持抢占调度,一个进程在内核执行的时候可能被另一个高优先级进程打断,进程与抢占它的进程访问共享资源的情况类似于对称多处理器的多个CPU。
3、中断(硬中断、软中断、Tasklet、底半部)与进程之间
中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态。
上述并发的发生情况除了对称多处理器是真正的并行以外,其他的都是“宏观并行、微观串行”的,但其引发的实质问题和对称多处理器相似。
解决竞态问题的途径是保证对共享资源的互斥访问,所谓的互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
访问共享资源的代码区域称为临界区,临界区需要以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是Linux设备驱动中可采用的互斥途径。7.2~7.5将进行一一讲解。
7.2 中断屏蔽
在单CPU范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。CPU一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序抢占,防止某些竞态条件的发生。具体而言,中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。
中断屏蔽的使用方法是:
local_irq_disable() //屏蔽中断
...
critical section //临界区
...
local_irq_enable() //打开中断
由于Linux系统的异步I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统崩溃。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。
local_irq_disable()和local_irq_enable()都只能禁止和使能本CPU内的中断,因此,并不能解决对称多处理器引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与自旋锁联合使用。
与local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前CPU的中断位信息,local_irq_restore(flags)进行的是与local_irq_save(flags)相反的操作。
如果只是想禁止中断的底半部,应使用local_bh_disable(),使能底半部应调用local_bh_enable()。
7.3 原子操作
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
Linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整型变量进行原子操作。它们的共同点是在任何情况下操作都是原子的,内核代码可以安全地调用它们而不被打断。位和整型变量原子操作都依赖底层CPU的原子操作来实现,因此所有这些函数都与CPU架构密切相关。
一、整形原子操作
1、设置原子变量的值
void atomic_set(atomic_t *v,int i); //设置原子变量的值为i
atomic_t v = ATOMIC_INIT(0); //定义原子变量v并初始化为0
2、获取原子变量的值
atomic_read(atomic *v); //返回原子变量的值
3、原子变量加/减
void atomic_add(int i, atomic_t *v); //原子变量增加1
void atomic_sub(int i, atomic)t *v); //原子变量减少1
4、原子变量自增/自减
void atomic_inc(atomic_t *v); //原子变量增加1
void atomic_dev(atomic)t *v); //原子变量减少1
5、操作并测试
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。
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、设置位
void set_bit(nr, void *addr);
上述操作设置addr地址的第nr位,所谓设置位即将位写入1。
2、清除位
void clear_bit(nr, void *addr);
上述操作清除addr地址的第nr位,所谓清除位即将位写入0。
3、改变位
void change_bit(nr, void *addr);
上述操作对addr地址的第nr位进行反置。
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)。
代码清单7.1给出了原子变量的使用实例,它用于使设备最多只被一个进程打开。
代码清单7.1 使用原子变量使设备只能被一个进程打开
static atomic_t xxx_available = ATOMIC_INIT(1); //定义原子变量并初始化为1
static int xxx_open(struct inode *inode, struct file *filp){
...
if(!atomlic_dec_and_test(&xxx_available)){
atomic_inc(&xxx_available);
return -EBUSY; //已经打开
}
...
return 0; //成功
}
static int xxx_release(struct inode *inode, struct file *filp){
atomic_inc(&xxx_available); //释放设备
return 0;
}
7.4 自旋锁
一、自旋锁的使用
自旋锁(spin lock)是一种对临界资源进行互斥访问的典型手段,其名称来源于它的工作方式。为了