概述
原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”,可以保证指令以原子的方式运行,即执行过程不被打断,如果被中断则可能会引起执行结果和预期不符。
竟态问题
原子操作的提出主要是为了解决程序运行过程中的竞态问题,以程序中一个共享全局变量的自加操作count++为例,处理器完成这样一个操作需要三个步骤:
从内存中读取count变量的值到寄存器中;
将寄存器中的值加1;
将寄存器中的值写回到内存中。
这个操作也称为RMW(Read-Modify-Write)。现在假设存在两个执行线程同时在对count(count初始值假设为0)执行自加操作,预期的结果应当是两个线程串行地完成了count的自增操作,count最终的值为2;但在并发场景下,处理器的实际执行序列却可能会是下面这个情况:
之所以会产生上面的情况,原因就是RMW操作本身不具备原子性,它可能会被中断打断或者被并行程序产生的数据竞争影响。为了解决这种问题,就需要让RMW操作成为一个原子操作,这需要硬件提供机制来保证。
单处理器系统下的原子操作
在单处理器(Uni-Processor)系统中,处理器的执行流程只会受到中断机制的影响,由于中断只能发生于指令之间,因此能够在单条指令中完成的操作都可以认为是“原子操作”。单处理器系统实现原子操作的方式有两种:
提供能完成多步操作的单条指令:这在采用复杂指令集架构的处理器上比较常见,例如x86架构提供的inc指令就可以通过一条指令完成变量的自加操作;
关中断:处理器中断关闭后,就可以不间断地执行一系列指令,等所有操作完成后再打开中断。
多处理器系统下的原子操作
在多处理器(Multi-Processor)系统中,面临的并发问题要严峻很多,由于系统中有多个处理器在独立地运行,即使是一条指令执行期间也会受到其它处理器的干扰,导致指令执行结果错误。在不同的处理器体系结构下,硬件提供的原子操作实现机制会存在区别,但通常情况下,都会保证多处理器下进行零次或一次对齐内存访问的指令是具备原子性的。这里,我们以x86和ARM架构为例说明它们提供的原子机制。
x86架构下的原子操作
在x86架构下,处理器提供了在指令执行期间对总线加锁的手段。通过在需要原子操作的指令前附加lock指令前缀将总线锁定,保证了指令执行时不会受到其它处理器的影响。
当某条指令被加上lock指令前缀时,该指令在执行前,会把处理器的#HLOCK引脚拉低,该引脚被拉低导致总线被锁,其他处理器不能访问总线,直到指令执行完毕,处理器的#HLOCK引脚恢复以后,总线的访问权才被释放。x86架构只有指定的几个指令才可以附加lock指令前缀。操作系统把这些附加了lock指令前缀的指令包装后,做成多种原子操作API供应用使用。
ARM架构下的原子操作
ARM架构下的原子操作不使用总线锁定的方式,而是采用独占访问机制。ARM提供了一对独占内存加载和存储的指令,用于支持原子操作实现:
ldxr指令:内存独占加载指令。从内存中以独占的方式加载内存地址的值到通用寄存器里;
stxr指令:内存独占存储指令。以独占的方式把新的数据存储到内存中。
ldxr是内存加载指令的一种,不过它会通过独占监控器(exclusive monitor)来监控器这个内存的访问,独占监控器会把这个内存地址标记为独占访问模式,保证以独占的方式来访问这个内存地址,不受其他因素的影响。而stxr是有条件的存储指令,它会把新数据写入ldxr指令标记独占访问的内存地址里。
对于ARM处理器而言,比如atomic_inc()底层的实现会调用到atomic_add(),其代码如下:
static inline void atomic_add(int i, atomic_t *v)
{
unsigned long tmp;
int result;
prefetchw(&v->counter);
__asm__ __volatile__("@ atomic_add\n"
"1:
ldrex
%0, [%3]\n"
"
add
%0, %0, %4\n"
"
strex
%1, %0, [%3]\n"
"
teq
%1, #0\n"
"
bne
1b"
: "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
: "r" (&v->counter), "Ir" (i)
: "cc");
}
ARM结构提供了两种类型的独占监视器:
本地监视器(Local Monitor): 每一个处理器内部包含一个本地监视器,标记了本地处理器对某块内存的独占访问;
全局监视器(Global Monitor):全局唯一,标记了系统中每个处理器对某块内存的独占访问。
独占监控器一共有两种状态:开放访问状态和独占访问状态。
独占访问机制
当CPU通过ldxr指令从内存加载数据时,CPU会把这个内存地址标记为独占访问,然后CPU内部的独占监控器的状态变成了独占访问状态。当CPU执行stxr指令的时候,需要根据独占监控器的状态来做决定:
如果独占监控器的状态为独占访问状态,并且stxr指令要存储的地址正好是刚才使用ldxr指令标记过的,那么stxr指令存储成功,stxr指令返回0,独占监控器的状态变成了开放访问状态;
如果独占监控器的状态为开发访问状态,那么stxr指令存储失败,stxr指令返回1,独占监控器的状态不变,依然保持开发访问状态。
整型原子操作
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。*/
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)。
使用原子变量使设备只能被一个进程打开
static atomic_t xxx_available = ATOMIC_INIT(1); /* 定义原子变量 */
static int xxx_open(struct inode *inode, struct file *filp)
{
...
if (!atomic_dec_and_test(&xxx_available)) {
atomic_inc(&xxx_available);
return - EBUSY;
/* 已经打开 */9 }
...
return 0;
/* 成功 */
}
static int xxx_release(struct inode *inode, struct file *filp)
{
atomic_inc(&xxx_available);
/* 释放设备 */
return 0;
}
领取专属 10元无门槛券
私享最新 技术干货