前面20多节的文章在分析 Linux 内核设计与C语言代码实现时,常会遇到全局变量。全局变量显然属于多个函数的共享资源,因此若想安全的使用它,必须做好同步。事实上,Linux 内核也确实提供了一些用于同步共享资源的接口,不过之前的文章都对此避而不谈,接下来几节将尝试学习一下 Linux 内核同步方法。
原子操作
在讨论 Linux 内核同步方法之前,先来了解一下原子操作,因为原子操作是内核其他同步方法的基石。那么什么是原子操作呢?相信大家都知道,原子是组成物体的不可再分割的微粒,那与之对应,原子操作就是不能再被分割的指令。原子操作的意义是什么呢?请考虑这个问题:
假设在某个C语言程序开发中,定义了一个全局变量 i,这时有两个线程对 i 执行“加一”的操作(即执行 i++)。
假设 i 的初值为 0,我们自然期望两个线程是下面这种执行流程:
但是若两个线程没有对全局变量 i 做任何同步操作,实际上可能是下面这样的执行流程:
线程1和线程2可能都在全局变量 i 的值增加之前读取到了它的初值,这就可能导致出现不期望的结果:当两个线程执行完毕后,全局变量 i 的值本来应该是 2 的,结果却为 1 了。
不过,如果使用了原子操作,上面这种竞争情况就不会出现了,整个过程只有可能是下面这两种情况:
最后必定会得到预期结果(i==2),因为两个原子操作绝对不可能并发的访问同一个变量,也即不会出现上面那种“竞争”现象。
Linux 内核提供了两组原子操作接口,一组是针对整数的,一组是针对单个位操作的。
原子整数操作
Linux 内核为整数原子操作专门定义了 atiomic_t 类型,它是因平台而异的,在x86平台下,它的C语言代码定义如下,请看:
typedef struct {
int counter;
} atomic_t;
显然,atomic_t 其实就是个只有一个 int 成员的结构体,Linux 内核这么定义整数原子操作的数据类型,主要就是为了区分非原子操作类型。
定义一个 atomic_t 类型的数据就很简单了,直接将 atomic_t 当作C语言中一个普通的结构体就可以了,例如:
atomic_t a;
atomic_t b = ATOMIC_INIT(0);
上面定义了原子类型 b,并对其赋了初值 0,ATMOIC_INIT 是一个宏,在 x86 平台,它的 C语言代码如下:
#define ATOMIC_INIT(i) { (i) }
定义好原子变量后,若是想使用它,可以使用 Linux 内核提供的这几个接口,请看下面的C语言示范代码:
// a = 4
atomic_set(&a, 4);
// a = a+2
atomic_add(2, &a);
// a ++
atomic_inc(&a);
// 读取
int a = atomic_read(v);
在 x86 平台,atomic_set() 和 atomic_read() 的C语言定义很简单,请看:
#define atomic_set(v, i) (((v)->counter) = (i))
#define atomic_read(v) ((v)->counter)
其实就是直接赋值和直接读取而已,x86 架构在物理上保证了这两个操作的原子性。atomic_inc() 和 atomic_add() 函数的C语言定义稍微复杂一点,请看:
static inline void atomic_inc(atomic_t *v)
{
asm volatile(LOCK_PREFIX "incl %0"
: "+m" (v->counter));
}
static inline void atomic_add(int i, atomic_t *v)
{
asm volatile(LOCK_PREFIX "addl %1,%0"
: "+m" (v->counter)
: "ir" (i));
}
显然,这段C语言代码是使用内嵌汇编完成的,与 atomic_add() 函数对应的atomic_sub() 函数的C语言定义也是类似的。原子整数操作最常见的用途就是实现计数器,因为如果使用其他复杂的方法去保护一个单纯的计数器,明显就是高射炮打蚊子,大材小用了。
原子位操作
再来看看 Linux 内核关于原子位操作的设计与实现,内核没有为原子位操作定义新的专用的数据类型,最常用的几个操作是 set_bit(),clear_bit(),以及 test_bit() 函数,它们的C语言定义如下:
static inline void set_bit(int nr, volatile void *addr)
{
asm volatile(LOCK_PREFIX "bts %1,%0" : ADDR : "Ir" (nr) : "memory");
}
static inline void clear_bit(int nr, volatile void *addr)
{
asm volatile(LOCK_PREFIX "btr %1,%0" : ADDR : "Ir" (nr));
}
#define test_bit(nr, addr) \
(__builtin_constant_p((nr)) \
? constant_test_bit((nr), (addr)) \
: variable_test_bit((nr), (addr)))
看到这里,读者可能有些疑惑,位操作不存在发生矛盾的可能性吧?那原子位操作存在的意义是什么呢?原子操作意味着指令会完整的执行,或者完全不执行。
假设有两个原子位操作,第一个操作是将 a 的 bit 3 置零,第二个操作是将 a 的 bit 3 置一。那么显然,在第一个操作完成之后,第二个操作进行之前,a 的 bit 3 必定为零,当第二个操作完成后,a 的 bit 3 必定为一。也就是说,所有的中间结果都是可预知的,都是正确无误的。
如果对变量 a 的 bit 3 先置零,再置一的两个操作不是原子操作,那么 a 的 bit 3 最后可能的确等于一了,但是中间可能根本没有被置零过,因为两个操作可能同时发生,导致 a 的 bit 3 置零失败了。这在操作硬件寄存器的时候,是绝对不能容忍的。
小结
本节先介绍了Linux 内核 C语言开发中,共享资源的竞争问题,接着讨论了内核中关于原子整数操作和原子位操作的设计与实现。