目录
一、ACCESS_ONCE、READ_ONCE、WRITE_ONCE
一、ACCESS_ONCE、READ_ONCE、WRITE_ONCE
原文:内存模型与同步原语 - 1 内存屏障-阿里云开发者社区
1、volatile和barrier()
volatile 关键词用于告知编译器,其修饰的变量的值很可能被程序之外的因素(如该变量存储于硬件寄存器 IO 映射的内存)改变,因而防止编译器对该变量进行缓存优化;对于 volatile 修饰的变量,编译器不能对该变量进行缓存,当每次使用该变量的值时,编译器必须从内存重新读取该变量的值。虽然 barrier() 和 volatile 都有抑制编译器优化的效果,但是两者还是存在着细微的差别由于 volatile 是修饰一个变量的,那么 volatile 就会一直伴随着这个变量,也就是说这个变量再也不能使用寄存器对其进行缓存,今后访问这个变量时每次都需要从内存重新读取该变量的值。
编译器的优化:
int a = 1;
void foo(void) {
while (a) ;
}
void bar(void) {a = 0;}
假设程序中有两个线程,一个线程执行 foo()函数,另一个线程执行 bar() 函数,两个线程会并行访问全局变量 a在默认不开启任何优化选项时,编译器输出的 foo() 函数为
foo:
.L2:
movl a(%rip), %eax // %eax = a
testl %eax, %eax // test if a == 0
jne .L2 // reenter the loop if a != 0
popq %rbp
.cfi_def_cfa 7, 8
ret // exit and return if a == 0
可以看到在未开启优化的时候,编译器的输出是符合程序的设计意图的再来看看开启编译优化 (gcc -O2) 时,编译器输出的 foo() 函数
foo:
.LFB0:
movl a(%rip), %eax // %eax = a
testl %eax, %eax // test if %eax == 0
jne .L4
rep ret // exit and return if %eax == 0
.L4:
jmp .L4 // infinite loop
可以看到此时 foo()函数会陷入无限循环。
barrier()、volatile编译器进行的优化对比:
#define barrier()__asm__ __volatile__("": : :"memory")
int a = 1;
void foo(void) {while (a) barrier();}
此时开启编译优化 (gcc -O2) 时,编译器输出的 foo() 函数为
foo:
.LFB0:
.cfi_startproc
jmp .L6
.p2align 4,,10
.p2align 3
.L5:
.L6:
movl a(%rip), %eax
testl %eax, %eax
jne .L5
rep ret
.cfi_endproc
可以看到加上 barrier() 之后,恢复为从内存读取变量的值。同样的例子,再来看一下使用 barrier() 的效果:
void foo(void) {while (a) {
b = a;
barrier()};}
此时编译器 gcc -O2 的编译输出为
foo:
jmp .L9
.L7:
movl %eax, b(%rip) // b=a if a != 0
.L9:
movl a(%rip), %eax // %eax = a
testl %eax, %eax // test if %eax == 0
jne .L7
rep ret // exit and return if a == 0
可以看到每次循环执行一次读内存的操作,同时在执行 b=a 时复用寄存器中缓存的 a 值。
2、barrier()和mb()
#define barrier()__asm__ __volatile__("": : :"memory")
#define mb() asm volatile("mfence":::"memory")
#define rmb() asm volatile("lfence":::"memory")
#define wmb() asm volatile("sfence" ::: "memory")
- __volatile__: 告诉编译barrier()周围的指令不要被优化;
- memory:是告诉编译器汇编代码会使内存里面的值更改,编译器应使用内存里的新值而非寄存器里保存的老值;
- sfence, 在写指令之后插入写屏障,将处于store buffers中的条目写回到主内存;
- lfence,清空invalidate queue后,再从主内存加载数据;
- mfence, 是一种全能型的屏障,具备ifence和sfence的能力。
3、ACCESS_ONCE()
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
这里虽然还是使用 volatile 关键字来抑制编译器的优化,但是之前是直接将整个变量声明为 volatile,而这里 ACCESS_ONCE() 则是通过指针的方式,将变量临时地转换为 volatile 变量,相当于只有调用 ACCESS_ONCE() 的时候才会抑制编译器优化,而在其他地方访问变量的时候,编译器优化是正常开启的
4、READ_ONCE()/WRITE_ONCE()
#define __READ_ONCE(x, check) \
({ \
union { typeof(x) __val; char __c[1]; } __u; \
if (check) \
__read_once_size(&(x), __u.__c, sizeof(x)); \
else \
__read_once_size_nocheck(&(x), __u.__c, sizeof(x)); \
smp_read_barrier_depends(); /* Enforce dependency ordering from x */ \
__u.__val; \
})
#define READ_ONCE(x) __READ_ONCE(x, 1)
#define __READ_ONCE_SIZE \
({ \
switch (size) { \
case 1: *(__u8 *)res = *(volatile __u8 *)p; break; \
case 2: *(__u16 *)res = *(volatile __u16 *)p; break; \
case 4: *(__u32 *)res = *(volatile __u32 *)p; break; \
case 8: *(__u64 *)res = *(volatile __u64 *)p; break; \
default: \
barrier(); \
__builtin_memcpy((void *)res, (const void *)p, size); \
barrier(); \
} \
})
static __always_inline
void __read_once_size(const volatile void *p, void *res, int size)
{
__READ_ONCE_SIZE;
}
规避的原理非常简单,就是将 non-scalar 类型强制转换为 scalar 类型,之后再按照ACCESS_ONCE() 中那样将转换后的 scalar 类型的变量临时转换为 volatile 的进行访问 ,WRITE_ONCE()同样如此。
4、__read_mostly
vmlinux.lds.S:
vmlinux.lds.h:
在vmlinux的链接脚本中定义了.data..read_mostly,将锁有这种属性的数据放入到.data中,并且这种数据的起始地址和结束地址都按照L1 cache对齐(__cache_aligned结束地址没有按L1 cache对齐),这样在同一缓存行的数据都是mosly read的数据,这样会减少常读数据和常写数据混合在同一cache,由于常写数据导致的缓存invalid,但这避免不了缓存行full后的缓存弹出策略和正常读写内存的内存恰好命中该缓存行的情况,read_mostly对常读数据有一定的性能优化但不多。网上有很多文章说read_mostly的数据会常驻cache中,这样不是很合理,L1缓存是稀缺资源内核中有很多变量的定义为这种属性,如果这些变量常驻缓存会造成缓存资源变少从而导致其他数据缓存miss的情况增加。