当前位置: 首页 > 面试题库 >

Java 8不安全:xxxFence()指令

长孙朝明
2023-03-14
问题内容

在Java 8中,向Unsafe类(source)添加了三个内存屏障指令:

/**
 * Ensures lack of reordering of loads before the fence
 * with loads or stores after the fence.
 */
void loadFence();

/**
 * Ensures lack of reordering of stores before the fence
 * with loads or stores after the fence.
 */
void storeFence();

/**
 * Ensures lack of reordering of loads or stores before the fence
 * with loads or stores after the fence.
 */
void fullFence();

如果我们通过以下方式定义内存屏障(我认为或多或少容易理解):

将X和Y视为需要重新排序的操作类型/类,

X_YFence() 是一个内存屏障指令,可确保在屏障启动之前,在屏障之前的所有类型X的操作在屏障开始之前的Y类型的任何操作之前完成。

现在,我们可以将“障碍”名称从此Unsafe术语“映射” 到:

  • loadFence()成为load_loadstoreFence();
  • storeFence()成为store_loadStoreFence();
  • fullFence()成为loadstore_loadstoreFence();

最后, 我的问题是
-我们为什么不有load_storeFence()store_loadFence()store_storeFence()load_loadFence()

我的猜测是-他们并不是真正的必要,但我目前不明白为什么。因此,我想知道不添加原因的原因。对此的猜测也很受欢迎(但是希望这不会导致该问题成为基于观点的话题)。

提前致谢。


问题答案:

摘要

CPU内核具有特殊的内存排序缓冲区,以帮助它们无序执行。这些可以(并且通常是)分开进行加载和存储:用于加载顺序缓冲区的LOB和用于存储顺序缓冲区的SOB。

为Unsafe API选择的防护操作是基于以下 假设
选择的:基础处理器将具有单独的加载顺序缓冲区(用于重新排序负载),存储顺序缓冲区(用于重新排序存储)。

因此,基于此假设,从软件角度来看,您可以向CPU请求以下三项之一:

  1. 清空LOB(loadFence):这意味着在处理完LOB的所有条目之前,没有其他指令将在此内核上开始执行。在x86中,这是LFENCE。
  2. 清空SOB(storeFence):意味着在处理完SOB中的所有条目之前,不会再有其他指令在此内核上开始执行。在x86中,这是SFENCE。
  3. 清空LOB和SOB(fullFence):表示以上两者。在x86中,这是MFENCE。

实际上,每个特定的处理器体系结构都提供了不同的内存排序保证,这些保证可能比上述要求更为严格或更灵活。例如,SPARC体系结构可以重新排序加载-存储和存储-
加载序列,而x86则不能。此外,存在无法单独控制LOB和SOB的体系结构(即仅全栅栏是可能的)。但是,在两种情况下:

  • 当体系结构更加灵活时,出于选择的考虑,API根本就不提供对“宽松”排序组合的访问

  • 当体系结构更严格时,API会在所有情况下简单地实现更严格的顺序保证(例如,实际上所有3个调用都将被实现为完全隔离)

根据Asylias提供的答案(即100%当场),在JEP中说明了选择特定API的原因。如果您了解内存排序和缓存一致性,则必须使用assylias的答案。我认为它们与C

API中的标准化指令相匹配的事实是一个主要因素(大大简化了JVM的实现):http
:
//en.cppreference.com/w/cpp/atomic/memory_order在任何可能性下,实际实现都会调用相应的C
API,而不是使用一些特殊的指令。

下面,我对基于x86的示例进行了详细的解释,这些示例将提供理解这些内容所必需的所有上下文。实际上,标定的(下面的部分回答了另一个问题:“您能否提供有关内存围栏如何控制x86架构中的缓存一致性的基本示例?”

原因是我自己(来自软件开发人员而不是硬件设计人员)在理解什么是内存重新排序方面遇到了麻烦,直到我了解了有关缓存一致性在x86中实际如何工作的特定示例。这为一般性讨论内存隔离提供了宝贵的上下文(也适用于其他体系结构)。最后,我将使用从x86示例中获得的知识来讨论SPARC。

参考文献[1]是更详细的说明,并且具有单独的部分来讨论x86,SPARC,ARM和PowerPC中的每一个,因此如果您对更多详细信息感兴趣的话,则是很好的阅读。

x86体系结构示例

x86提供了3种类型的防护指令:LFENCE(装入防护),SFENCE(存储防护)和MFENCE(装入防护),因此它将100%映射到Java API。

这是因为x86具有单独的加载顺序缓冲区(LOB)和存储顺序缓冲区(SOB),因此LFENCE /
SFENCE指令确实适用于相应的缓冲区,而MFENCE则适用于两者。

SOB用于存储输出值(从处理器到高速缓存系统),而高速缓存一致性协议用于获取写入高速缓存行的权限。LOB用于存储失效请求,以便失效可以异步执行(减少了接收方的停顿,希望在那里执行的代码实际上不需要该值)。

乱序商店和SFENCE

假设您有一个双处理器系统,其两个CPU 0和1执行下面的例程。考虑以下情况:高速缓存行保留failure最初由CPU
1拥有,而高速缓存行保留shutdown最初由CPU 0拥有。

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  if (failure) { ...}
}

在没有存储栅栏的情况下,CPU 0可能由于故障而发出关闭信号,但是CPU 1将退出循环,并且如果阻塞则不会进入故障处理。

这是因为CPU0会将值1写入failure存储顺序缓冲区,同时还会发出缓存一致性消息以获取对缓存行的独占访问权。然后它将继续执行下一条指令(在等待独占访问时)并shutdown立即更新标志(该高速缓存行已由CPU0独占,因此无需与其他内核进行协商)。最后,当它稍后从CPU1接收到一个无效确认消息(关于failure)时,它将继续处理SOB
failure并将其值写入高速缓存(但是现在顺序相反)。

插入storeFence()将解决问题:

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  SFENCE // next instruction will execute after all SOBs are processed
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  if (failure) { ...}
}

最后值得一提的是x86具有存储转发功能:当CPU写入卡在SOB中的值(由于高速缓存一致性)时,它随后可能会尝试在SOB之前对同一地址执行加载指令。处理并交付给缓存。因此,CPU将在访问缓存之前先咨询SOB,因此在这种情况下检索到的值是从SOB中最后写入的值。
这意味着THIS核心的存储无论如何都无法与THIS核心的后续加载进行重新排序

乱序负载和LFENCE

现在,假设您已经安装了存储栅栏,并且很高兴shutdown不会超过failure其进入CPU
1的路径,而将注意力集中在另一侧。即使存在商店围栏,在某些情况下也会发生错误的事情。考虑failure在两个高速缓存(共享)中shutdown都存在,而仅在CPU0的高速缓存中存在且仅由CPU0高速缓存拥有的情况。坏事情可能会发生如下:

  1. CPU0将1写入failure; 它还作为高速缓存一致性协议的一部分,向CPU1发送一条消息,以使其共享高速缓存行的副本无效
  2. CPU0执行SFENCE并停顿,等待用于failure提交的SOB 。
  3. shutdown由于while循环,CPU1进行了检查,并(意识到它缺少该值)发送了一个缓存一致性消息来读取该值。
  4. 在步骤1中,CPU1从CPU0接收到使该消息无效的消息,并为其failure发送立即确认。 注意:这是使用失效队列实现的,因此实际上它只是输入一个注释(在其LOB中分配一个条目)以稍后进行失效,但实际上在发送确认之前并不执行该失效。
  5. CPU0收到确认,failure并经过SFENCE转到下一条指令
  6. CPU0在不使用SOB的情况下将1写入关闭状态,因为它已专门拥有高速缓存行。 由于缓存行是CPU0专用的,因此不会发送额外的无效消息
  7. CPU1接收该shutdown值并将其提交到其本地缓存,然后继续进行下一行。
  8. CPU1检查failureif语句的值,但是由于尚未处理无效队列(LOB注释),因此它使用其本地缓存中的值0(如果阻塞则不输入)。
  9. CPU1处理无效队列并更新failure为1,但是为时已晚…

我们所谓的加载顺序缓冲区实际上是无效请求的排队,并且可以通过以下方法解决上述问题:

// CPU 0:
void shutDownWithFailure(void)
{
  failure = 1; // must use SOB as this is owned by CPU 1
  SFENCE // next instruction will execute after all SOBs are processed
  shutdown = 1; // can execute immediately as it is owned be CPU 0
}
// CPU1:
void workLoop(void)
{
  while (shutdown == 0) { ... }
  LFENCE // next instruction will execute after all LOBs are processed
  if (failure) { ...}
}

您在x86上的问题

现在您知道SOB / LOB的功能,请考虑您提到的组合:

loadFence() becomes load_loadstoreFence();

不,负载防护栏等待处理LOB,实际上是清空了失效队列。这意味着所有后续加载都将看到最新数据(无需重新排序),因为它们将从缓存子系统中获取(它们是连贯的)。存储CANNNOT会因后续加载而重新排序,因为它们不会通过LOB。(此外,存储转发还负责本地修改的缓存行)从此特定内核(执行负载隔离的内核)的角度来看,在所有寄存器加载完数据后,将在负载隔离之后执行存储。没有其他办法了。

load_storeFence() becomes ???

不需要load_storeFence,因为它没有意义。要存储某些内容,您必须使用输入进行计算。要获取输入,您必须执行加载。将使用从加载中获取的数据进行存储。如果要确保在加载时看到所有其他处理器的最新值,请使用loadFence。对于围栏之后的货物,存储转发要注意保持一致的顺序。

所有其他情况都是相似的。

SPARC

SPARC更加灵活,可以通过后续加载(以及后续存储的加载)对存储进行重新排序。我对SPARC不太熟悉,所以我的 GUESS
是没有存储转发(重新加载地址时未咨询SOB),因此可能出现“脏读”的情况。实际上,我错了:我在[3]中发现了SPARC体系结构,实际上是存储转发是线程化的。从5.3.4节开始:

所有加载都会检查存储缓冲区(仅同一线程)是否存在写入后读取(RAW)的危害。当加载的双字地址与STB中存储的双字地址匹配并且加载的所有字节在存储缓冲区中有效时,就会发生完整RAW。当双字地址匹配但所有字节在存储缓冲区中无效时,将发生部分RAW。(例如,ST(字存储)后跟LDX(双字加载)到相同的地址会导致部分RAW,因为完整的双字不在存储缓冲区条目中。)

因此,不同的线程会查询不同的存储顺序缓冲区,因此有可能在存储后进行脏读。

参考文献

[1]内存壁垒:针对软件黑客的硬件视图,Linux技术中心,IBM Beaverton
http://www.rdrop.com/users/paulmck/scalability/paper/whymb.2010.07.23a.pdf

[2]英特尔®64和IA-32体系结构软件开发人员手册,第3A卷
http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-
software-developer-
vol-3a-part-1-manual.pdf

[3] OpenSPARC
T2核心微体系结构规范http://www.oracle.com/technetwork/systems/opensparc/t2-06-opensparct2-core-
microarch-1537749.html



 类似资料:
  • 以下的指南包含了一些在你创建Cordova应用的时候需要考虑的一些安全最佳实践。你要意识到安全问题是一个非常复杂的问题,因此这篇指南也不能详尽的阐明。如果你觉得你可以为这篇指南做出贡献,请在"Documentation"下面的Cordova问题追踪里提出你的问题。这篇指南对常规的Cordova开发(所有平台)都适用,一些特定平台的注意事项会标注出来。 这篇指南主要讨论以下的主题: 白名单 ifra

  • Rails 安全指南 本文介绍网页程序中常见的安全隐患,以及如何在 Rails 中防范。 读完本文,你将学到: 所有推荐使用的安全对策; Rails 中会话的概念,应该在会话中保存什么内容,以及常见的攻击方式; 单单访问网站为什么也有安全隐患(跨站请求伪造); 处理文件以及提供管理界面时应该注意哪些问题; 如何管理用户:登录、退出,以及各种攻击方式; 最常见的注入攻击方式; 1 简介 网页程序框架

  • 使用活动的风险和对策取决于活动的使用方式。 在本节中,我们根据活动的使用情况,对 4 种活动进行了分类。 你可以通过下面的图表来找出,你应该创建哪种类型的活动。

  • 安全约束是一种定义 web 内容保护的声明式方式。安全约束关联授权和或在 web 资源上对 HTTP 操作的用户数据约束。安全约束,在部署描述符中由 security-constraint 表示,其包含以下元素: web资源集合 (部署描述符中的 web-resource-collection) 授权约束 (部署描述符中的 auth-constraint) 用户数据约束 (部署描述符中的 user

  • 在Java8中使用缺省方法作为穷人版本的traits是一种安全的做法吗? 有人说,如果你只是为了熊猫而使用它们,可能会让熊猫伤心,因为它很酷,但这不是我的本意。人们还经常提醒说,引入缺省方法是为了支持API演进和向后兼容性,这是真的,但这并不使使用它们作为特征本身是错误的或扭曲的。 我脑海中有以下实际用例: 或者,定义: 关于SO的几个问题与Java vs Scala特性有关;这不是重点。我也不仅

  • 为了介绍本章内容,我们借用官方文档的一句话, “在基本代码中尽可能减少不安全的代码”(”one should try to minimize the amount of unsafe code in a code base.”)。记住这句话,接着我们进入学习!在 Rust 中,不安全代码块是用于避开编译器的保护策略;具体地说,不安全代码块主要有 4 方面内容: 解引用裸指针 通过 FFI 调用函数