android smp内存屏障,搞懂内存屏障-指令与JMM

前面讲了CPU的演进,提出了StoreBuffer和InvalidateQueue的设计,并且讲解了这两个设计会带来的问题。

解决这两个问题就是引入内存屏障:强制刷新StoreBuffer和InvalidateQueue。

这里详细讲讲x86机器上的内存屏障指令与其他隐式的含有内存屏障的指令。

然后再聊一聊JMM与内存屏障的对应关系。

x86与内存屏障

前面提到的StoreBuffer和InvalidateQueue并不是所有的CPU都会去实现。

其中x86的机器上,遵循的内存一致性协议叫TSO协议。

在这个协议中,有个叫WriteBuffer的东西,就是对应StoreBuffer。

但是并没有InvalidateQueue的存在。

内存屏障指令集

上文中,提到了三个内存屏障的指令:

lfence():读屏障

sfence():写屏障

mfence():读写屏障

那么在代码中是怎么定义的呢:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18#define barrier() __asm__ __volatile__("": : :"memory")

#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)

#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)

#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)

#ifdef CONFIG_SMP

#define smp_mb() mb()

#define smp_rmb() rmb()

#define smp_wmb() wmb()

#define smp_read_barrier_depends() read_barrier_depends()

#define set_mb(var, value) do { (void) xchg(&var, value); } while (0)

#else

#define smp_mb() barrier()

#define smp_rmb() barrier()

#define smp_wmb() barrier()

#define smp_read_barrier_depends() do { } while(0)

#define set_mb(var, value) do { var = value; barrier(); } while (0)

#endif

首先来看barrirer()的定义,这个是禁止编译器进行重排序的。

具体的解释可以参考笔者的另外一个文章:volatile和内存屏障

然后我们看CONFIG_SMP,如果定义了这个,说明该机器上不止一个Core,否则就是单核心的机器。

在单核心的机器上,所有的CPU的内存屏障指令都是空指令,只有禁止编译器重排序的作用。

这个也好理解,就不多做解释了。

而在多核心的机器上,分别定义了:

smp_mb():读写屏障

smp_rmb():读屏障

smp_wmb():写屏障

同时我们看具体的实现,也就是用到了我们上面提到了lfence,sfence,mfence。

但是我们再仔细看看这句话:

#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)

如果CPU没有lfence指令,那么就用lock; addl $0,0(%%esp)代替。

为什么?难道lock; addl $0,0(%%esp)也能有内存屏障的语义吗?

是的!

除了fence指令,还有很多的其他的指令也隐藏了内存屏障的语义。

下面笔者来总结一下:

常见的三种

x86/64系统架构提供了三种多核的内存屏障指令:(1) sfence; (2) lfence; (3) mfence

sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。

lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。

mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

其实总结起来就是读屏障,写屏障,读写屏障。

上述的是显式的会起到内存屏障作用的指令,但是还有许多指令带有异常的内存屏障的作用。

MMIO写屏障

Linux 内核有一个专门用于 MMIO 写的屏障:

mmiowb()

笔者也不熟悉这个的作用,后续再补上

隐藏的内存屏障

Linux 内核中一些锁或者调度函数暗含了内存屏障。

锁函数:

spin locks

R/W spin locks

mutexes

semaphores

R/W semaphores

中断禁止函数:

启动或禁止终端的函数的作用仅仅是作为编译器屏障,所以要使用内存或者 I/O 屏障 的场合,必须用别的函数。

SLEEP和WAKE-UP以及其它调度函数:

使用 SLEEP 和 WAKE-UP 函数时要改变 task 的状态标志,这需要使用合适的内存屏 障保证修改的顺序。

JMM

在JMM中,定义了4中内存可见性语义:

LoadLoad

LoadStore

StoreStore

StoreLoad

但是这些指令对应到x86的机器上,并不是都需要实现的。

因为x86的核心问题是有StoreBuffer,一个值被Core0写入了StoreBuffer,另外一个Core可能读不到最新的值,除非Flush StoreBuffer。所以StoreLoad语义需要内存屏障来维持。

例如以下的例子:

1

2

3

4

5

6

7

8void foo(void){

x=1; //S1

r1=y; //S2

}

void bar(void){

y=1; //L1

r2=x;//L2

}

在这个例子中,如果没有内存屏障,Core0执行foo,Core1执行bar,则r1和r2可能出现同时为0的情况。

更具体的例子

下面我们看看代码,经过JIT编译后的指令

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15static int a = 0;

static int b = 0;

public static void main(String[] args){

for (int i = 0; i < 10000; i++) {

add();

}

}

public static void add(){

for (int i = 0; i < 100; i++) {

a++;

b += 2;

}

}

如果a没有被volatile修饰:

9153d1d2921cca02188c2346c2179e43.png

可以看到a和b的操作分别对应:

inc %r9d

add $0x2, %r9d

中间没有任何内存屏障的指令

如果我们加上volatile修饰呢?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15static volatile int a = 0;

static volatile int b = 0;

public static void main(String[] args){

for (int i = 0; i < 10000; i++) {

add();

}

}

public static void add(){

for (int i = 0; i < 100; i++) {

a++;

b += 2;

}

}

2688baaac9d1914d760d39d9b16591e1.png

可以很明显的看到两个lock指令。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值