标签:ola print 架构 解决 cache read 输出变量 如何 条件
考虑如下的简单程序,全局变量x初始值为0:
int x = 0;
void thread1_func() {
x++;
print(x);
}
void thread2_func() {
x++;
print(x);
}
程序输出 1 2 或 2 2很容易理解,但也有可能输出为1 1。 Why?
原因便是x++不是原子操作,如果把它转为CPU指令形式,则很容易理解:
(1) Load x
(2) Inc x
(3) Store x
当第一个线程运行完第一步时,第二个线程也运行到此,这时它们得到的值都是0,然后将值加1再存回去,这时两个线程运行完时,x的值是1。
最简单的解决方式便是使用原子操作,Linux中提供了以atomic_开头的原子操作函数,例如:
#define atomic_inc(v)
#define atomic_dec(v)
#define atomic_add(i, v)
...
v是atomic_t类型的变量,定义如下:
typedef struct {
inc counter;
} atomic_t;
原子操作需要靠硬件实现,我们以arm64平台为例,看看atomic_add函数是如何实现的。
<arch/arm64/include/asm/atomic_lse.h>
#define ATOMIC_OP(op, asm_op) static inline void atomic_##op(int i, atomic_t *v) { register int w0 asm ("w0") = i; register atomic_t *x1 asm ("x1") = v; \
asm volatile(ARM64_LSE_ATOMIC_INSN(__LL_SC_ATOMIC(op), " " #asm_op " %w[i], %[v]\n") : [i] "+r" (w0), [v] "+Q" (v->counter) : "r" (x1) : __LL_SC_CLOBBERS); }
ATOMIC_OP(add, stadd)
在介始具体实现前,我们先了解一下GCC内联汇编,GCC内联汇编的格式如下:
asm volatile(指令部:输出部:输入部:损坏部)
上节中我们发现原子add操作是通过原子指令stadd实现的,在不同的架构上实现的方式可能不一样。
CPU执行原子指令时,给总线上锁,这样在释放前,可以防止其它CPU的内存操作。
除了和IO紧密相关的(如MMIO),大部分的内存都是可以被cache的,由前面介绍的cache一致性原理,我们知道由cacheline处于Exclusive或Modified时,该变量只有当前CPU缓存了数据,因此当进行原子操作时,发出Read Invalidate消息,使其它CPU上的缓存无效,cacheline变成Exclusive状态然后将该cacheline上锁,接着就可以取数据,修改并写入cacheline,如果这时有其它CPU也进行原子操作,发出read invalidate消息,但由于当前CPU的cacheline是locked状态,因此暂时不会回复消息,这样其它CPU就一直在等待,直到当前CPU完成,使cacheline变为unlocked状态。
在ARMv8.1之前,为实现RMW的原子操作的方法主要是LL/SC(Load-link/Store-condition).ARMv7中实现的指令是LDREX/STREX,原理如下:
假设CPU0进行load操作,标记变量V所在的内存地址为exclusive, 在CPU0进行store前,这时CPU1也对变量V进行了load操作,这时exclusive标记属于CPU1而不再属于CPU0,在CPU0进行store时会测试该地址的exclusive标记是不是自己的,如果不是,store失败。CPU1进行store, 因为exclusive标记是自己的,所以store成功,同时exclusive失效,这时CPU0会再次尝试一试LL/SC操作,直天成功为止。
如果CPU之间竞争比较激烈,可能导致重试的次数比较多,因此从2014年ARMv8.1开始,ARM推出了原子操作的LSE(Large System Extention)指令集扩展,新增的指令包括CAS, SWP和LD
标签:ola print 架构 解决 cache read 输出变量 如何 条件
原文地址:https://www.cnblogs.com/miaolong/p/12587812.html