参考:《Linux驱动开发入门与实战》
现代操作系统有三大特性;中断处理、多任务处理、多处理器(SMP)。这些特性导致当多个进程、线程或CPU同时访问同一个资源时,可能会发生错误。即会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。严重的话可能会导致系统崩溃。
1. 并发与竞争
现在的 Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因:
1.1 多线程并发访问,Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
1.2 抢占式并发访问,从 2.6 版本内核开始,Linux 内核支持抢占,也就是说调度程序可以
在任意时刻抢占正在运行的线程,从而运行其他的线程。
1.3 中断程序并发访问,硬件中断的权利很大。
1.4 SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。
并发访问带来的问题就是竞争,如果多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发和防止竞争访问。(所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临界区是原子访问的。)
并发控制机制有原子变量操作、自旋锁、信号量、完成量。
2. 原子变量操作
所谓原子变量操作,就是该操作在执行完之前绝不会被任何其他任务或事件打断。原子变量操作需要硬件的支持,因此是与架构相关的,其API和原子类型的定义在内核源码的路径为:include/asm/atomic.h。他们都是使用汇编语言实现,因为C语言并不能实现这样的操作。
原子变量操作不会只执行一半。它要么执行完毕,要么一点也不执行。原子变量操作的优点是编写简单;缺点是功能太简单,只能做计数操作。但是原子变量操作是其他同步手段的基石。
在Linux中,原子变量的定义如下:
typedef struct{
volatile int counter;
} atomic_t;
关键字volatile用来指示GCC不要对该类型做数据优化,所以对这个变量counter的访问都是基于内存的,不要将其缓冲到寄存器中。存储到寄存器中,可能导致内存中的数据已经改变,而寄存器中的数据没有改变。
在Linux中,定义了两种原子变量操作的方法,一种是原子整形操作,另一种是原子位操作。
2.1 原子整形操作
当需要共享的资源只是一个简单的整形数值。例如在驱动程序中,需要包含一个计数器count。这个计数器表示有多少个应用程序打开了设备所对应的设备文件。在设备驱动程序的open()函数种将count加1,在close()函数中将count减1。如果有多个应用程序同时打开或关闭设备文件,那么就可能导致count多加或少加。为了避免这个问题,内核提供了原子整形变量atomic_t。该变量定义如下:
typedef struct{
volatile int counter;
} atomic_t;
定义count计数器范例如下:
atomic_t count;
这句代码定义了一个atomic_t类型的count变量,atomic_t变量只能通过linux内核中定义的专用函数来操作,不能在变量上直接加1或者减1。
2.1.1 定义atomic_t变量
#define ATOMIC_INIT(i) { {i} }
atomic_t count = ATOMIC_INIT(0);
第一行宏 ATOMIC_INIT(i) 用于给atomic_t类型的变量初始化,宏的参数i就是初始化的值。
第二行对atomic_t类型变量count初始化。
因为atomic_t类型变量是一个结构体类型,所以对其进行定义和初始化应该用结构体定义和初始化的方法。
2.1.2 设置atomic_t变量的值
atomic_set(v,t)宏用来设置v变量的值为i。其定义如下所示:
#define atomic_set(v, i) ( ( (v)->counter ) == i )
2.1.3 读atomic_t变量的值
atomic_read(v)宏用来读v变量的值。其定义如下所示:
#define atomic_read(v) ( (v)->counter ) )
该宏对原子类型变量进行原子读操作,它返回原子类型变量v的值。
2.1.4 原子变量的加减法
atomic_add()函数用来将第一个参数i的值加到第二个参数v中,并返回一个void值。
static inline void atomic_add(int i, volatile atomic_t *v)
与atomic_sub()函数功能相反的函数是atomic_sub()函数,该函数从原子变量v中减去i的值。
static inline void atomic_sub(int i, volatile atomic_t *v)
2.1.5 原子变量的自加自减
atomic_inc()函数用来将v指向的变量加1,并返回一个void值。
static inline void atomic_inc( volatile atomic_t *v)
atomic_dec()函数用来将v指向的变量减1,并返回一个void值。
static inline void atomic_dec( volatile atomic_t *v)
2.1.6 加减测试
atomic_inc_and_test()函数用来将v指向的变量加1,如果结果是0,则字节返回真;如果是非0,则返回假。
static inline int atomic_inc_and_test( volatile atomic_t *v)
atomic_dec_and_test()函数用来将v指向的变量减1,如果结果是0,则字节返回真;如果是非0,则返回假。
static inline int atomic_dec_and_test( volatile atomic_t *v)
综上所述,atomic_t类型的变量必须使用上面介绍的函数来访问,如果试图将原子变量看作整形变量来使用,则会出现编译错误。
2.2 原子位操作
原子位操作是根据数据的每一位单独进行操作。这些函数的原型如下所示:
static inline void set_bit(intnr, volatile unsigned long *addr)
static inline void clear_bit(intnr, volatile unsigned long *addr)
static inline void change_bit(intnr, volatile unsigned long *addr)
static inline void test_and_set_bit(intnr, volatile unsigned long *addr)
static inline void test_and_clear_bit(intnr, volatile unsigned long *addr)
static inline void test_and_change_bit(intnr, volatile unsigned long *addr)
需要注意的是,原子位操作和原子整数操作是不同的。原子位操作不需要专门定义一个类似atomic_t类型的变量,只需要一个普通的变量指针就可以。
set_bit函数将addr指向的变量的第nr位设置为1;
clear_bit函数将addr指向的变量的第nr位设置为0;
change_bit函数将addr指向的变量的第nr位设置为相反的数;
test_and_set_bit函数将addr指向的变量的第nr位设置为1,并返回没有修改之前的值;
test_and_clear_bit函数将addr指向的变量的第nr位设置为0,并返回没有修改之前的值;
test_and_change_bit函数将addr指向的变量的第nr位设置为相反的数,并返回没有修改之前的值;