Linux 设备驱动中的并发控制(1)

Linux 设备驱动中的并发控制(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.2 更复杂、更混乱的并发大量地存在于设备驱动中,只要并发的多个执行单元存在对共享资源的访问,,竞态就可能发生。在 Linux 内核中,主要的竞态发生于如下几种情况。

1.对称多处理器(SMP)的多个 CPU

SMP 是一种紧耦合、共享存储的系统模型,其体系结构如图 7.3 所示,它的特点是多个 CPU 使用共同的系统总线,因此可访问共同的外设和储存器。

2.单 CPU 内进程与抢占它的进程

Linux 2.6 内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断,进程与抢占它的进程访问共享资源的情况类似于 SMP 的多 个 CPU。

3.中断(硬中断、软中断、Tasklet、底半部)与进程之间

中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。 此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态。

上述并发的发生情况除了 SMP 是真正的并行以外,其他的都是“宏观并行、微观串行”的,但其引发的实质问题和 SMP 相似。 解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。 访问共享资源的代码区域称为临界区(critical sections),临界区需要以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是 Linux 设备驱动中可采用的互斥途径,下面将进行一一讲解。

中断屏蔽

在单 CPU 范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。CPU 一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。具体而言,中断屏蔽将使得中断与进程之间的并发不再发生,而且,由于 Linux 内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。

中断屏蔽的使用方法为:

local_irq_disable() //屏蔽中断 …

critical section //临界区 …

local_irq_enable() //开中断

由于 Linux 系统的异步 I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统崩溃。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。

local_irq_disable()和 local_irq_enable()都只能禁止和使能本 CPU 内的中断,因此,并不能解决 SMP 多 CPU 引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与自旋锁联合使用。

与 local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前 CPU 的中断位信息,local_irq_restore(flags)进行的是与 local_irq_save(flags)相反的操作。

如果只是想禁止中断的底半部,应使用 local_bh_disable(),使能被 local_bh_disable()禁止的底半部应该调用 local_bh_enable()。

原子操作

原子操作指的是在执行过程中不会被别的代码路径所中断的操作。 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_t *v); //返回原子变量的值

3.原子变量加/减

void atomic_add(int i, atomic_t *v); //原子变量增加 i

void atomic_sub(int i, atomic_t *v); //原子变量减少 i

4.原子变量自增/自减

void atomic_inc(atomic_t *v); //原子变量增加 1

void atomic_dec(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)。

唯有斯人面上簌簌流下的,是点点无声无行的热泪。

Linux 设备驱动中的并发控制(1)

相关文章:

你感兴趣的文章:

标签云: