热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

系统崩溃时clflush或clflushopt是原子的吗?

通常,缓存行是 64B,但非易失性内存的原子性是 8B。例如:x[1]=100;x[2]=100;clflush(x);x缓存行对齐,并且最初设置为0。系统崩溃 clflush();是否有可能x[1]

通常,缓存行是 64B,但非易失性内存的原子性是 8B。

例如:

x[1]=100;
x[2]=100;
clflush(x);

x缓存行对齐,并且最初设置为0

系统崩溃 clflush();

是否有可能x[1]=0x[2]=100重新启动后?

回答


在以下假设下:


  • 我假设您显示的代码表示一系列 x86 汇编指令,而不是尚未编译的实际 C 代码。

  • 我还假设代码是在 Cascade Lake 处理器上执行的,而不是在下一代 Intel 处理器上执行的(我认为带有 Barlow Pass 的 CPL 或 ICX 支持 eADR,这意味着持久化不需要显式刷新,因为缓存位于持久域)。这个答案也适用于现有的 AMD+NVDIMM 平台。

存储的全局可观察性顺序可能与 Intel x86 处理器上的持久顺序不同。这称为松弛持久性。唯一保证顺序相同的情况是将 WB 类型的存储序列放入同一缓存行(但到达 GO 的存储并不一定意味着它变得持久)。这是因为CLFLUSH原子性和 WB 存储不能在全局可观察性中重新排序。请参阅:在 x86-64 上,系统崩溃时“movnti”或“movntdq”指令是原子的吗?.

如果两个存储跨越缓存线边界或者如果存储的有效内存类型是 WC:

在x86-TSO内存模型不允许重新排序店,所以它的另一个代理观察是不可能x[2] == 100x[1] != 100正常工作期间(即,在动荡的状态,而不会崩溃)。但是,如果系统崩溃并重新启动,则持久状态可能是x[2] == 100x[1] != 100。即使系统在退休后崩溃也是可能的,clflush因为退休clflush并不一定意味着刷新的缓存行已经到达持久域。

如果您想消除这种可能性,您可以clflush按以下方式移动:

x[1]=100;
clflush(x);
x[2]=100;

clflush英特尔处理器上的所有写入都是按顺序排列的,这意味着该行保证在任何后续存储变为全局可见之前到达持久域。请参阅:持久内存编程主要 (PDF)和英特尔 SDM V2。第二家商店可以在同一行或任何其他行。

如果您想x[1]=100x[2]=100全局可观察之前变得持久,请在 Intel CSX 或AMD 处理器上添加sfenceafter (仅在 AMD 处理器上排序)。本身足以控制持久顺序。clflushmfenceclflushmfenceclflush

或者,使用序列clflushopt+sfence(或clwb+sfence) 如下:

x[1]=100;
clflushopt(x);
sfence;
x[2]=100;

在这种情况下,如果发生崩溃并且x[2] == 100处于持久状态,则可以保证x[1] == 100. clflushopt本身不会强加任何持久排序。





回答


另请参阅@Hadi 的回答:x86 TSO 存储排序即使在一行内也不能保证持久性排序。 这个答案并没有试图解决这个问题。基于 Hadi 的回答,我的最佳猜测是单个原子存储到一个 32 字节的一半高速缓存线将自动持续,但这是基于当前硬件的工作方式,在内核、高速缓存和内存控制器之间以 2 个 32 字节的一半传输线。如果这真的很重要,请查找文档或询问英特尔。)


请记住,在显式刷新之前,存储数据可以自行传播出缓存(进入 DRAM 或 NVDIMM)。

以下事件序列是可能的:


  • x[2]=100; 首先存储缓存行的第 3 个字节。(编译时重新排序:这是一个C不ASM问题x显然是简单的uint8_t x[64],不_Atomic或挥发性所以x[1]=100;x[2]=100;不能保证在ASM的顺序发生。)

  • 中断到达;在某些时候,包含的缓存行从缓存中x[]被逐出,进入持久性域。(也许在上下文切换到另一个线程之后,因此在这两个 asm 存储之间运行了许多其他代码)。

  • 系统在恢复执行之前崩溃。(或在x[1]=100;饰面变得耐用之前。)

如果您想依赖 x86 内存排序规则来控制缓存行内的持久性顺序,您需要确保 C 遵守这一点。 volatile会工作,或_Atomicmemory_order_release至少2号店。(或者更好,如果它们在对齐的 8 字节块内,则将它们作为单个存储完成。)(x86 asm 内存模型 = 带有存储缓冲区的程序顺序;没有 StoreStore 重新排序。)

编译时重新排序通常不会无缘无故地发生(但它可以);更常见的是由于周围的代码使得这样做很有吸引力。但是周围的代码可能会导致这种情况。(当然x[1]=100;/x[2]=0;可以通过这种机制发生,而无需任何编译时重新排序,如果它是 2 个独立的存储。)


我认为持久性原子性的必要前提是作为单个原子存储完成。 例如,由 ISA 保证原子性,或者使用单个更广泛的 SIMD 存储1,因为实际上英特尔 CPU 不会将它们分开(但没有纸上保证)。原子性。中断(即单个指令)而不是单个存储 uop 使拆分更难,但仍然完全可能2,因此不能保证安全。例如,一个 10 字节的 x87fstp tbyte涉及 2 个单独的存储数据 uop,可以通过来自另一个核心的失效来拆分,即使没有错误共享也是可能的。(再次参见脚注 2。)

如果没有针对 16 字节或更宽的 SIMD 存储的任何纸上原子性保证,您将依赖于 SIMD 存储或未对齐存储的实现细节被拆分。

但是,即使是 ISA 保证的原子性也是不够的:lock cmpxchg跨越缓存行边界的 a 仍然保证原子性。其他内核和 DMA 读取器。(支持这个非常非常慢,不要这样做。)但是没有办法保证这两条线同时变得持久。但除了原子性的特殊情况 IDK 之外,我不能排除整线原子性。毫无疑问,在 asm 中原子化的单行存储将成为原子性的持久化,没有撕裂的机会。

单个缓存行中,我不知道。

我猜想 8 字节对齐块中的原子存储将使其以原子方式持久化或根本不持久化,但我还没有检查过英特尔的文档。(实际上甚至可能是整个 64 字节的行,您可以使用 AVX512 存储)。这个答案的重点是你甚至没有一个原子存储,所以有很多其他机制可以破坏你的测试用例。


脚注 1: 现代 Intel CPU 将 SIMD 存储作为单个事务提交到 L1d 缓存,只要它们不跨越缓存线即可。自 Sandy/Ivy Bridge 以来,英特尔还没有制造出将 SIMD 存储分成两半的 CPU,它具有全宽 256 位 AVX 执行单元,但只有 128 位宽的路径到/从加载单元中的缓存和存储中的 AFAIK -buffer-commit 的东西。(存储数据执行单元也用了 2 个周期将 32 字节存储数据写入存储缓冲区)。

脚注 2:对于像 in 一样属于同一指令一部分的单独存储 uops fstp tbyte [rdi],这可能是可能的:


  • 第一部分从存储缓冲区提交到 L1d 缓存


  • RFO 或共享请求到达并在来自同一指令的第二个存储提交之前处理:此内核的副本现在无效或共享,因此从存储缓冲区到 L1d 的提交被阻止,直到它重新获得独占所有权。该指令的第二部分存储位于存储缓冲区的头部,而不是在一致的缓存中。


  • 正在执行 RFO 的另一个核心使用 跟踪他们的存储,clflush在第一个核心可以取回它并完成提交来自该指令的其他数据之前,将此行驱逐到持久性内存。

    movnti另一个核心的 NT 存储将强制逐出该行,作为提交 NT 存储的一部分,就像一个普通的存储 + clflushopt。

    这种情况需要在试图在同一行中保留 2 个单独事物的两个线程之间进行错误共享,因此如果您避免错误共享(例如使用填充),则可以避免。(或者一些疯狂的真正共享,或者在clflush没有先存储的情况下在其他线程可能正在写入的内存上触发)。


  • (或者对软件来说更合理,对硬件来说更不合理):在第一个写入器取回之前,该线路会自行被逐出,即使核心有一个待处理的 RFO。(一旦失去所有权,第一个核心就会发出 RFO)。


  • 或者完全合理,没有错误共享):由于从包含缓存行跟踪结构的逐出而随时从 L2/L1d 强制逐出。这可能是由于对仅碰巧在 L3 中别名相同集合而不是错误共享的线路的需求而触发的。

    Skylake-server (SKX) 具有非包容性 L3,与后来的 Intel 服务器 CPU 一样。Cascade Lake (CSX) 是第一个支持持久内存的。即使它有一个非包含的 L3,监听过滤器也是包含的,并且导致驱逐的填充冲突确实会导致整个 NUMA 节点的返回失效。


因此,一个无效的请求可以到达任何时间,并且很可能在核心/存储缓冲器是不会保持到线以上的周期犯的一个未知的数量多店到同一线路。

(到那时,两个存储缓冲区条目都是一条指令的一部分这一事实可能会丢失。访问模式可能会创建一个存储缓冲区条目流,无限期地存储同一高速缓存行的不同部分,因此请等到“这条线的所有存储都完成了”可能让非特权代码为想要读取它的核心创建拒绝服务。所以我认为硬件不太可能有一种机制来避免释放存储之间缓存线的所有权来自同一条指令。)






推荐阅读
  • 兆芯X86 CPU架构的演进与现状(国产CPU系列)
    本文详细介绍了兆芯X86 CPU架构的发展历程,从公司成立背景到关键技术授权,再到具体芯片架构的演进,全面解析了兆芯在国产CPU领域的贡献与挑战。 ... [详细]
  • C语言是计算机科学和编程领域的基石,许多初学者在学习过程中会感到困惑。本文将详细介绍C语言的基本概念、关键语法和实用示例,帮助你快速上手C语言。 ... [详细]
  • PBO(PixelBufferObject),将像素数据存储在显存中。优点:1、快速的像素数据传递,它采用了一种叫DMA(DirectM ... [详细]
  • 面试题总结_2019年全网最热门的123个Java并发面试题总结
    面试题总结_2019年全网最热门的123个Java并发面试题总结 ... [详细]
  • PHP-Casbin v3.20.0 已经发布,这是一个使用 PHP 语言开发的轻量级开源访问控制框架,支持多种访问控制模型,包括 ACL、RBAC 和 ABAC。新版本在性能上有了显著的提升。 ... [详细]
  • 本文整理了一份基础的嵌入式Linux工程师笔试题,涵盖填空题、编程题和简答题,旨在帮助考生更好地准备考试。 ... [详细]
  • 本文探讨了Go语言中iota关键字的具体含义及其在常量声明中的应用。 ... [详细]
  • 本文总结了Java初学者需要掌握的六大核心知识点,帮助你更好地理解和应用Java编程。无论你是刚刚入门还是希望巩固基础,这些知识点都是必不可少的。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 单片微机原理P3:80C51外部拓展系统
      外部拓展其实是个相对来说很好玩的章节,可以真正开始用单片机写程序了,比较重要的是外部存储器拓展,81C55拓展,矩阵键盘,动态显示,DAC和ADC。0.IO接口电路概念与存 ... [详细]
  • malloc 是 C 语言中的一个标准库函数,全称为 memory allocation,即动态内存分配。它用于在程序运行时申请一块指定大小的连续内存区域,并返回该区域的起始地址。当无法预先确定内存的具体位置时,可以通过 malloc 动态分配内存。 ... [详细]
  • 2020年9月15日,Oracle正式发布了最新的JDK 15版本。本次更新带来了许多新特性,包括隐藏类、EdDSA签名算法、模式匹配、记录类、封闭类和文本块等。 ... [详细]
  • JUC(三):深入解析AQS
    本文详细介绍了Java并发工具包中的核心类AQS(AbstractQueuedSynchronizer),包括其基本概念、数据结构、源码分析及核心方法的实现。 ... [详细]
  • 多线程基础概览
    本文探讨了多线程的起源及其在现代编程中的重要性。线程的引入是为了增强进程的稳定性,确保一个进程的崩溃不会影响其他进程。而进程的存在则是为了保障操作系统的稳定运行,防止单一应用程序的错误导致整个系统的崩溃。线程作为进程的逻辑单元,多个线程共享同一CPU,需要合理调度以避免资源竞争。 ... [详细]
  • MySQL 5.7 学习指南:SQLyog 中的主键、列属性和数据类型
    本文介绍了 MySQL 5.7 中主键(Primary Key)和自增(Auto-Increment)的概念,以及如何在 SQLyog 中设置这些属性。同时,还探讨了数据类型的分类和选择,以及列属性的设置方法。 ... [详细]
author-avatar
accera_928
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有