1 引言
汇编指令读写内存变量的过程我们称为read-modify-write
,简称为RMW操作。也就是说,它们读写一个内存区域两次,第一次读取旧值,第二次写入新值。
假设有两个不同的内核控制路径运行在两个CPU上,同时尝试RMW操作相同的内存区域且执行的是非原子操作。起初,两个CPU尝试读取相同位置,但是内存仲裁器(促使串行访问RAM的电路)确定一个可以访问,让另一个等待。但是,当第一个读操作完成,延时的CPU也会读取相同的旧值。但是等到两个CPU都往这个内存区域写入新值的时候,还是由内存仲裁器决定谁先访问,然后写操作都会成功。但是,最终的结果却是最后写入的值,先写入的值会被覆盖掉。
防止RMW操作造成的竞态条件最简单的方式就是保证这样的指令操作是原子的,也就是这个指令的执行过程不能被打断。这就是原子操作的由来。
2 X86体系架构
2.1 X86原子指令
让我们看一下X86的汇编指令有哪些是原子的:
- 进行零或一对齐内存访问的汇编指令是原子的。
- RMW操作汇编指令(比如
inc
或dec
),如果在read之后,write之前内存总线没有被其它CPU抢占,那么这些指令就是原子的。 - 所以,基于上一点,RMW操作汇编指令前缀
lock(0xf0)
就称为原子操作指令。当控制单元检测到这个前缀,它会锁住内存总线,直到指令完成。 - 带有前缀
rep
(0xf2、0xf3,强迫控制单元重复指令多次)的汇编指令就不是原子的。
通过上面的描述可知,X86体系架构本身有一些指令就是原子指令。对于RMW操作指令(比如inc
或dec
),本身不是原子指令,但是可以通过在指令前面,使用前缀lock
指令锁住内存总线,阻止在写内存时,其它CPU抢占,从而实现原子操作。
2.2 ARM原子指令
但是,ARM体系架构中不存在lock指令,所以它在原子指令的实现上是不一样的。ARMv6之前的版本,因为不支持多核,所以只要关闭中断即可;而ARMv6及以后的版本,支持多核系统,只关闭中断是不可以实现原子指令的。于是,该版本引入了新的独占指令ldrex
和strex
,通过这两个指令实现原子操作。比如,下面以原子加法为例,代码如下:
static inline void atomic_add(int i, atomic_t *v)
{
unsigned long tmp;
int result;
__asm__ __volatile__("@ atomic_addn"
"1: ldrex %0, [%3]n" // ---------- (0)
" add %0, %0, %4n" // ---------- (1)
" strex %1, %0, [%3]n" // ---------- (2)
" teq %1, #0n" // ---------- (3)
" bne 1b" // ---------- (4)
: "=&r" (result), "=&r" (tmp), " Qo" (v->counter)
: "r" (&v->counter), "Ir" (i)
: "cc");
}
代码解析:
- (0)从v->counter地址处取出其值,将其存入result;
- (1)计算result=result i;
- (2)将result的结果存入v->counter地址处,这一步操作是否成功的结果写入到tmp临时变量中;
- (3)判断tmp是否等于0;
- (4)第(3)结果如果等于0,则成功;如果不等于0,则跳转到标签1处继续执行,直到成功。
所以说,X86这种锁内存总线的方式简单好用,但是毕竟牺牲了性能;而ARM这种独占指令则更为高效,只不过实现上更为复杂一点。
3 Linux原子操作
但是,我们在编写完C代码后,编译器不能保证给你使用原子指令进行替代。因此,Linux内核提供了atomic_t
类型变量并提供了相关的操作函数和宏(如表5-4所示)。
表5-4 Linux中的原子操作
代码语言:javascript复制返回
*v