为什么在两个内核之间的邮箱数据通信中需要两个内存屏障?

toz*_*zak 5 c memory gcc arm

这里我们有一段用于两个ARM内核之间数据通信的邮箱代码(直接引用自《ARM Cortex A系列编程指南》)。

核心A:

STR R0, [Msg] @ write some new data into postbox
STR R1, [Flag] @ new data is ready to read
Run Code Online (Sandbox Code Playgroud)

核心B:

Poll_loop:
LDR R1, [Flag]
CMP R1,#0 @ is the flag set yet?
BEQ Poll_loop
LDR R0, [Msg] @ read new data.
Run Code Online (Sandbox Code Playgroud)

为了强制执行依赖关系,文档指出我们需要在代码中插入两个而不是一个内存屏障 DMB。

核心A:

STR R0, [Msg] @ write some new data into postbox
DMB
STR R1, [Flag] @ new data is ready to read
Run Code Online (Sandbox Code Playgroud)

核心B:

Poll_loop:
LDR R1, [Flag]
CMP R1,#0 @ is the flag set yet?
BEQ Poll_loop
DMB
LDR R0, [Msg] @ read new data.
Run Code Online (Sandbox Code Playgroud)

我理解核心 A 中的第一个 DMB:它可以防止编译重新排序以及系统观察到的对 [Msg] 变量的内存访问。以下是同一文档中 DMB 的定义。

数据内存屏障 (DMB)
该指令确保在屏障之后按程序顺序出现的任何显式内存访问之前,在系统中观察到屏障之前按程序顺序进行的所有内存访问。它不会影响在核心上执行的任何其他指令或指令提取的顺序。

不过,我不知道为什么要使用Core B中的DMB。文件中写道:

核心 B 需要在 LDR R0 [Msg] 之前有一个 DMB,以确保在设置标志之前不会读取消息。

如果核心 A 中的 DMB 使系统能够观察到 [Msg] 的存储,那么我们就不需要第二个核心中的 DMB。我的猜测是,编译器可能会对 Core B 中的 [Flag] 和 [Msg] 的读取进行重新排序(尽管我不明白为什么它应该这样做,因为对 [Msg] 的读取取决于 [Flag])。

如果是这种情况,编译屏障(asm 易失性(“” :::“内存)而不是 DMB 应该足够了。我在这里错过了什么吗?

Not*_*hat 5

这两个障碍都是必要的,并且确实需要dmb- 这仍然与硬件内存模型有关,与编译器重新排序无关。

我们先来看看A核心的作者:

STR R0, [Msg] @ write some new data into postbox
STR R1, [Flag] @ new data is ready to read
Run Code Online (Sandbox Code Playgroud)

由于这是对不同地址的两个独立存储,它们之间没有依赖性,因此没有什么可以强制核心 A 按程序顺序实际发出存储。比如说,存储 toMsg可以在部分填充的写入缓冲区中徘徊,而存储 to 则Flag超越它并直接进入内存系统。因此,除核心 A 之外的任何观察者都可以看到 的新值Flag,但尚未看到 的新值Msg

STR R0, [Msg] @ write some new data into postbox
DMB
STR R1, [Flag] @ new data is ready to read
Run Code Online (Sandbox Code Playgroud)

现在,有了屏障,商店 toFlag不允许在商店 to 之前可见Msg,因为这将需要一个或另一个商店看起来穿过屏障。因此,任何外部观察者都可能看到两个旧值、新值Msg但旧值Flag,或者两个新值。Flag以前见新见旧的情况Msg不能再出现了。

好的,所以第一个障碍处理以正确顺序写入的内容,但还有如何读取它们的问题。在核心 B 上...

Poll_loop:
LDR R1, [Flag]
CMP R1,#0 @ is the flag set yet?
BEQ Poll_loop
LDR R0, [Msg] @ read new data.
Run Code Online (Sandbox Code Playgroud)

请注意,分支 toPoll_loop不会在两个负载之间形成控制依赖关系;如果考虑程序顺序,则 的加载Msg是无条件的,并且 的值Flag不会影响它是否执行,只会影响执行是否进展到程序的该部分。因此,代码可以等效地写成:

Poll_loop:
LDR R1, [Flag]
LDR R0, [Msg] @ read data, just in case.
CMP R1,#0 @ is the flag set yet?
BEQ Poll_loop @ no? OK, throw away that data and read everything again.
... @ do stuff with R0, because Flag was set so it must be good data, right?
Run Code Online (Sandbox Code Playgroud)

开始看到问题了吗?即使使用原始代码,核心 BMsg一旦到达,就可以自由地推测加载Poll_loop,因此即使核心 A 的存储按程序顺序变得可见,事情仍然可能会这样发生:

  core A   |  core B
-----------+-----------
           | load Msg
store Msg  |
store Flag |
           | load Flag
           | conclude that old Msg is valid
Run Code Online (Sandbox Code Playgroud)

因此你要么需要一个屏障:

...
BEQ Poll_loop
DMB
LDR R0, [Msg] @ read new data.
Run Code Online (Sandbox Code Playgroud)

或者可能是假地址依赖:

...
BEQ Poll_loop
EOR R1, R1, R1
LDR R0, [Msg, R1] @ read new data.
Run Code Online (Sandbox Code Playgroud)

使两个负载相互对抗。