当不同的 CPU 内核在没有同步的情况下写入同一个 RAM 地址时会发生啥?

Posted

技术标签:

【中文标题】当不同的 CPU 内核在没有同步的情况下写入同一个 RAM 地址时会发生啥?【英文标题】:What happens when different CPU cores write to the same RAM address without synchronization?当不同的 CPU 内核在没有同步的情况下写入同一个 RAM 地址时会发生什么? 【发布时间】:2018-07-26 18:53:40 【问题描述】:

假设 2 个内核在同一时间(正负 eta)尝试将不同的值写入同一个 RAM 地址(1 个字节),并且不使用任何互锁指令或内存屏障。在这种情况下会发生什么以及将什么值写入主 RAM?第一个赢了?最后一个赢了?不确定的行为?

【问题讨论】:

如果只有一个字节,那么有人会赢。在没有线程写入的垃圾的意义上,它不会是不确定的。 术语“first”和“last”在非同步程序中没有任何意义。无法观察到,您只能在事后发现,不能保证代码第二次会表现相同。它必须强制执行,这需要同步。除非您将“eta”定义为至少与操作系统的硬故障响应时间和调度延迟一样大。在 RTOS 上只有一个上限。没有人会等那么久,所以它是 UB。 最后一个完成交易的人将获胜,但比赛不可见,不是最后一个处理器开始交易的情况将获胜,而是来自任何主人的最后一个交易将被处理有问题的 ram 控制器将从该点可见,直到发生另一个写入事务。 在过去的美好时光,这并不意味着现在有设计存在这个问题,如果两个事务碰巧“同时”命中(一个在多时钟期间进入)交易完成的周期),后者将被丢弃。早期/原始电脑上的视频闪烁/闪烁。如果在软件尝试与该内存通信时视频扫描正在从内存中读取,则会丢失并且该字符/像素对于该水平扫描会出现错误。 就你而言,它是不确定的......谁会赢,但你不确定谁会在每个实例中获胜。 【参考方案1】:

x86(与其他所有主流 SMP CPU 架构一样)具有 coherent data caches。两个不同的缓存(例如 2 个不同内核的 L1D)不可能为同一缓存行保存冲突的数据。

硬件强加一个命令(通过一些特定于实现的机制来打破联系,以防两个所有权请求在同一时钟周期内从不同的内核到达)。在大多数现代 x86 CPU 中,第一个存储不会被写入 RAM,因为有一个共享的回写式 L3 缓存来吸收一致性流量,而无需往返内存。

在全局顺序中出现在两个存储之后的加载将看到第二个存储存储的值。


(我假设我们谈论的是普通(不是 NT)存储到可缓存内存区域(WB,不是 USWC,UC,甚至是 WT)。不过,无论哪种情况,基本思想都是相同的;一个 store 会先走,下一个会踩到它。如果在全局 order 中它们之间碰巧有负载,则可以临时观察第一个 store 中的数据,否则来自 store 中的数据硬件选择做第二个将是长期的影响。

我们讨论的是单个字节,因此存储不能跨两个缓存行拆分,因此每个地址自然对齐,所以 Why is integer assignment on a naturally aligned variable atomic on x86? 中的所有内容都适用。


一致性是通过要求一个核心获得对该缓存行的独占访问权限它可以修改它(即通过从存储中提交来使存储全局可见排队到 L1D 缓存)。

这个“获得独占访问”的东西是使用the MESI protocol(的变体)完成的。缓存中的任何给定行都可以是 Modified(脏)、Exclusive(由尚未写入的所有)、Shared(干净的副本;其他缓存也可能有副本,因此在写入之前需要 RFO(读取/请求所有权)),或无效的。 MESIF (Intel) / MOESI (AMD) 添加额外的状态来优化协议,但不要改变任何时候只有一个内核可以改变一行的基本逻辑。

如果我们关心对两条不同行的多个更改进行排序,那么内存排序和内存屏障就会发挥作用。但是,当商店在同一时钟周期内执行或退出时,关于“哪个商店获胜”的问题,这些都不重要。

当存储执行时,它进入存储队列。它可以提交到 L1D 并在其退休后的任何时间变得全局可见,但不能在退休之前;未退休的指令被视为推测性的,因此它们的架构效果必须在 CPU 内核之外不可见。推测性负载没有架构影响,只有微架构1

因此,如果两个存储都准备好“同时”提交(时钟不一定在内核之间同步),则其中一个或另一个将首先使其 RFO 成功并获得独占访问权,并使其存储数据全局可见。然后,不久之后,另一个核的 RFO 将成功并使用其数据更新缓存行,因此其存储在所有其他核观察到的全局存储顺序中排名第二。

x86 有一个总存储顺序内存模型,其中所有内核都遵循 相同 顺序,即使存储到不同的缓存行(除了总是按照程序顺序查看它们自己的存储)。一些弱排序架构(如 PowerPC)将允许某些内核看到与其他内核不同的总顺序,但这种重新排序只能发生在不同行的存储之间。单个高速缓存行始终有一个修改顺序。 (相对于彼此和其他存储的负载重新排序意味着您必须小心如何在弱排序的 ISA 上观察事物,但缓存行有一个单一的修改顺序,由 MESI 强加)。

哪一个赢得比赛可能取决于一些平淡无奇的事情,比如环形总线上的内核布局相对于该线路映射到的共享 L3 缓存片。 (注意“比赛”这个词的使用:这是“比赛条件”错误描述的那种比赛。编写两个不同步的存储更新相同位置的代码并不总是错误的,你不在乎哪个获胜,但很少见。)

顺便说一句,现代 x86 CPU 在多个内核竞争原子读取-修改-写入同一高速缓存行(因此是holding onto it for multiple clock cycles to make lock add byte [rdi], 1 atomic)的情况下具有硬件仲裁,但常规加载/存储只需要拥有一个高速缓存用于执行加载或提交存储的单个周期的行。我认为locked 指令的仲裁与当多个内核尝试将存储提交到同一缓存行时内核获胜不同。除非您使用pause 指令,否则内核假定其他内核没有修改相同的缓存行,并且推测性地提前加载,因此如果确实发生了内存排序错误推测。 (What are the latency and throughput costs of producer-consumer sharing of a memory location between hyper-siblings versus non-hyper siblings?)

IDK 如果两个线程都只是存储而不加载时发生类似的事情,但可能不是因为存储没有被推测性地重新排序并且与存储队列的无序执行分离。一旦存储指令退出,存储肯定会发生,因此 OoO exec 不必等待它实际提交。 (事实上​​,它必须在提交之前从 OoO 核心退出,因为 CPU 就是这样知道它是非推测性的;即没有更早的指令出错或者是错误预测的分支)


脚注:

    Spectre 通过使用缓存定时攻击将微架构状态读入架构状态,从而模糊了这条界限。

【讨论】:

【参考方案2】:

它们最终会被排序,可能在 L1 缓存之间。一个写入将首先出现,另一个将出现在第二个。无论哪一个第二个都会是后续读取会看到的结果。

【讨论】:

是的,L1D 缓存使用 MESI 相互通信(通过 Intel CPU 中的 L3)。有关详细信息,请参阅我的答案。

以上是关于当不同的 CPU 内核在没有同步的情况下写入同一个 RAM 地址时会发生啥?的主要内容,如果未能解决你的问题,请参考以下文章

一文搞懂 , Linux内核—— 同步管理(下)

内核同步机制

在没有易失性机制的情况下,CPU 何时写入主存?

使用 CPU 时的 OpenCL 段错误

每个CPU变量上的锁定

Java并发-对象共享