ARM基础(5):内存屏障的必要性、内存类型和内存排序

文章探讨了内存屏障的必要性,特别是在处理器优化和外设访问中的作用。内存屏障用于确保内存访问的顺序,防止编译器和处理器的优化造成的顺序混乱。文章介绍了内存类型,如NormalMemory、DeviceMemory和Strongly-orderedMemory,并强调了内存排序的限制和在并发系统中的重要性。最后,文章指出在Cortex-M处理器中如何使用内存屏障和内存类型来保证数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在我们写代码的过程中,经常会发现在SDK中会出现__ISB()__DSB()等语句,这也做的目的是建立一个内存屏障,内存屏障可以由处理器内的硬件操作或内存屏障指令触发,它能够让CPU或编译器对屏障指令之前和之后的内存操作施加排序约束。

1 内存屏障的必要性

在大多数的情况下,编译器可能会将一些数据缓存在寄存器中,或者对指令进行重排序以使程序运行地更快。这都可能造成我们数据传输的顺序与我们代码中预期的顺序不同。

比如,外设寄存器需要使用volatile声明以防止C编译器缓存数据,这样在每次访问寄存器时,处理器都将在总线上生成对应的传输时序。举个例子:我们需要定期从ADC寄存器中读出转换值,由于编译器不知道这个内存里的数据会改变,就可能就会对这个读取的过程进行优化,将读取的结果保存在寄存器中,而不是每次都从内存中读取。当然,如果将外设地址空间定义为cacheable,仍可能出现问题。

对于大多数系统来说,如果将所有外设寄存器声明为volatile,并将它们的内存空间定义为non-cacheable,那么一般不需要维护内存访问的顺序。然而,但流水线和cache本就是用来加速我们程序的执行速度的,特别是在一些GPU处理,数学运算的代码中,如果简单地关闭这些功能,处理器执行的效率会变低很多。举个例子,在Cortex-M处理器中,可以使用DMB(Data Memory Barrier)指令,以确保影响之前的内存访问已经完成后再执行下一个操作:
在这里插入图片描述
由于加了DMB指令,就保证了在指令N+3执行之前,所有的内存操作都已完成,即暂时停止指令流水线。

2 内存类型

在之前MPU的相关文章中,我有介绍我们一般将内存分为三种:Normal Memory(普通内存)、Device Memory(设备内存)和Strongly-ordered Memory(强有序内存)。

通常,用于程序代码和数据存储的内存是Normal Memory。但是有一些系统外设(I/O)通常不能按照普通内存来访问,以下是几个例子:

  • 中断控制器寄存器:用于处理中断的硬件组件。通过访问中断控制器寄存器,可以确认中断事件并改变控制器的状态。
  • 内存控制器配置寄存器:用于设置普通内存区域的时序和正确性。通过访问这些寄存器,可以配置内存控制器的行为,以确保内存的正确操作。
  • 内存映射外设:是指将外设的功能映射到内存地址空间的硬件组件。通过访问特定的内存位置,可以触发系统的一些副作用或执行外设的特定操作。

在这些情况下,由于外设的访问规则与Normal Memory不同,需要使用内存屏障来确保正确的内存访问顺序和同步。

在ARMv7(包括ARMv7-m)中,对系统外设的访问被定义为Device MemoryStrongly-ordered Memory访问,相比Normal Memory的访问来说,它们有更多限制:

  • 读和写都可能导致系统的异常行为
  • 访问不能重复,例如,从异常返回时
  • 必须维护访问的数量、顺序和大小。

Strongly-ordered Memory
内存映射的外设和I/O位置通常是强有序内存。在Cortex-M处理器中,System Control Space是强有序的,而SCS中包括NVICMPUSysTick定时器和调试组件。

对强有序内存的显式访问,以下规则适用:

  • 访问的大小与程序中指定的大小相符。比如,程序要求以字节为单位进行访问,处理器就必须按字节进行读取或写入,而不能超过或低于指定的大小。
  • 访问的次数与程序中指定的次数相符,即必须按照程序中指定的次数进行访问
    • 这个规则有例外情况,具体参考《ARMv7-M Architecture Reference Manual (ARM DDI 0403).》中的Exceptions in Load Multiple and Store Multiple operations

强有序内存中的地址范围不会被缓存,即MPU配置为shareable。这意味着内存系统必须维护数据的一致性,以允许多个处理器共享这些数据。对强有序内存的显式访问都必须符合内存排序要求中描述的排序要求,下面就来看看内存排序(Memory Ordering)。

3 内存排序

1、支持多种实现:ARMv7-M和ARMv6-M体系结构支持从低端微控制器到高端超标量SoC设计的广泛实现范围。为此,这些体系结构采用弱序内存模型。这个模型定义了三种内存类型,每种类型具有不同的属性和排序要求。

2、指令顺序和内存事务(对内存的读取和写入操作,包括指令读取和数据访问)顺序的不一致性:程序中指令的顺序不总是保证对应内存事务的顺序。这是因为:

  • 处理器可以重新排序一些内存访问以提高效率,前提是不影响指令序列的行为
  • 处理器可以拥有多个总线接口
  • 内存或内存映射的设备可以位于互联结构(用于连接各个处理器、内存和其他设备的通信通道或总线系统)的不同分支上
  • 某些内存访问是缓冲或推测性的(在等待某些操作完成的同时,预先执行可能的下一条指令)。

3、内存屏障的需要:在应用级别考虑内存排序模型时,关键是对于对普通内存的访问,在某些情况下需要屏障(barriers),以控制访问的顺序。

4、内存访问排序的限制:ARMv7-M和ARMv6-M定义了对内存访问排序的限制。这些限制取决于访问的内存属性。

内存访问排序要求中有两个概念:地址依赖性和控制依赖性
(1)地址依赖性(Address dependency)
地址依赖性发生在一个读操作返回的值被用于计算后续读或写操作的地址时。即使第一个读操作返回的值不改变第二个读或写操作的地址,仍然可能存在地址依赖性。这与处理器接口的实现方式有关。
(2)控制依赖性(Control dependency)
控制依赖性发生在一个读操作返回的数值用于决定一个条件码标志(condition code flags),而这个标志的值决定了后续读操作的地址时。

简而言之,地址依赖性关注的是读操作返回值对后续访问地址的影响,而控制依赖性关注的是读操作返回值对后续读操作地址的条件判断的影响。这些依赖关系在内存排序中需要考虑,以确保正确的数据访问顺序和正确的计算结果。

4 内存排序的限制

在并发系统中,多个访问操作可能会同时发生或交织在一起,而正确的访问顺序对于保证数据的一致性和正确性至关重要。通过要求访问在程序顺序中被全局观察到(访问在程序顺序中的先后顺序必须得到全局范围内所有观察者的一致确认。也就是说,无论是处理器、内存还是其他系统组件,它们必须能够准确地察觉到访问的顺序),系统能够确保每个观察者都能够按照正确的顺序感知到访问的发生,从而避免了由于并发导致的数据访问问题。

下表显示了两个显式访问(读或写操作)A1和A2之间的内存顺序,其中A1在程序中出现在A2之前。表中有两个符号:

  • <:在程序执行中,必须准确地按照顺序观察到访问行为,也就是说,A1必须在A2之前被全局观察到。
  • -:只要满足单处理器(这里不讨论多核)指令执行的顺序和行为与程序编写者的意图一致,遵守指令之间的依赖关系,那么访问可以以任意顺序在全局范围内被观察到。这强调了保持程序正确性的前提下,对访问顺序的灵活性。

注意:下表来自于ARMv6-M Architecture Reference ManualRevision D of the ARMv7-M Architecture Reference Manual。这与之前的ARMv7-M Architecture不同,Revision D版本删除了强顺序(Strongly-ordered)和普通(Normal)内存事务之间的排序要求。这是由于在最新的架构中,普通事务具有推测执行的能力,并且提供了更高性能实现的能力,可以在强顺序访问周围重新排序普通内存访问,除非通过屏障明确阻止这样做。


在这里插入图片描述
上表中标记为-的内存访问操作还有以下额外的限制:

  • 如果存在地址依赖性,那么两个内存访问会按照程序顺序进行观察
  • 如果只存在控制依赖性,则不需要按照顺序要求来观察两个读访问
  • 如果同时存在地址依赖性和控制依赖性,那么需要满足地址依赖性的排序要求
  • 如果一个读访问的返回值用于后续的写访问,那么这两个内存访问会按照程序顺序进行观察
  • 如果在程序的顺序执行中,某个内存位置不会被写入,那么观察者(如外设或第二个处理器)就不能观察到对该内存位置的写入操作或写入的值。即观察者只能观察到实际发生的写操作,而不能观察到未发生的写操作。

举个例子,A1和A2都是对一个Non-ShareableDevice Memory进行访问,这种情况下一定是要等A1的所有序列执行完毕后再执行A2,这个好理解,因为内存是不可共享的,也是不可缓存的。

再来来分析一下表中为-情况,举个例子:

A1: 处理器对外设A的Non-ShareableDevice Memory进行读取操作。
A2: 处理器对外设B的ShareableDevice Memory进行写入操作。

根据之前提到的原则,只要满足单处理器指令执行的顺序和行为与程序编写者的意图一致,保证处理器中指令之间的依赖关系,这两个访问操作可以以任意顺序在全局范围内被观察。这意味着观察者(例如外设或其他处理器)在观察到这两个访问操作时,它们的顺序是任意的。

5 总结

Cortex-M处理器相对于指令流程从不执行乱序内存访问,当然,未来不一定会有改变。因此,针对ARMv7-M的代码如果要在ARMv7-AR处理器(如Cortex-A9)上可移植,必须已经考虑了上面的排序模型。在Cortex-M处理器中,内存类型与内存映射相关联。定义了一些体系结构区域,并为每个区域定义了预定义的内存类型。当实现了MPU时,还可以通过编程来更改一些内存类型的定义。

在Cortex-M处理器上,可以使用MPU将SRAM定义为DeviceStrongly-ordered区域,但这会降低性能,并且无法阻止C编译器在生成的代码中重新排序数据传输。为了获得最佳效率,ARM建议将SRAM定义为Normal内存,并在内存排序重要的情况下使用内存屏障memory barriers

关于内存屏障和内存排序的知识十分复杂和抽象,但又十分重要,下一节我们将继续深入研究。

<think>好的,我需要详细解释Java内存屏障的作用工作原理。首先,我得回顾一下内存屏障的基本概念。根据引用[2],内存屏障是同步屏障指令,确保在此点之前的所有读写操作完成后,才能执行之后的操作,防止代码重排序。这点很重要,尤其是在多线程环境下。 接下来,内存屏障的作用。引用[1]提到内存屏障阻止处理器指令重排序强制刷新缓存,这会影响性能,所以JVM会优化屏障的使用。而引用[4]指出,volatile变量在赋值后会插入类似内存屏障的指令,比如lock addl,这保证了可见性有序性。我需要把这些点结合起来,说明内存屏障如何保证可见性、有序性,以及为什么无法保证原子性。 关于工作原理,引用[3]提到不同CPU架构实现不同,比如x86的强内存模型可能需要的屏障较少。而JVM通过插入特定屏障指令来符合Java内存模型的要求,如引用[2]中提到的JIT编译器优化屏障位置。此外,volatile的实现细节,如lock前缀指令,也涉及到内存屏障,这点引用[4]有详细说明。 需要分点整理作用:可见性、有序性;原理部分要包括编译器插入屏障、CPU指令支持、volatile的实现。同时要引用对应的资料来支持每个点,比如可见性部分引用[2][4],有序性引用[2][1]。 可能还需要比较内存屏障在不同架构下的差异,如引用[3]提到的x86DEC Alpha,但用户主要关心Java层面的实现,所以可以简要带过,强调JVM的处理。 最后,检查是否有遗漏,比如内存屏障类型(LoadLoad、StoreStore等),但用户可能不需要太底层的分类,保持解释在Java应用层面即可。确保每个引用都正确标注,语言口语化但信息准确。</think>### Java内存屏障的作用与原理详解 #### 一、内存屏障的作用 1. **保证可见性** 内存屏障强制将当前线程的写操作结果**立即刷新到主内存**,并**使其他线程的缓存失效**,确保后续读取操作能获取最新数据。例如,`volatile`变量修改后,JVM会插入内存屏障,确保其他线程能立即看到修改后的值[^2][^4]。 2. **禁止指令重排序** 编译器处理器可能对指令进行重排序以优化性能,但这会导致多线程环境下的执行结果不符合预期。内存屏障通过**限制重排序范围**,确保屏障前后的指令顺序不被破坏。例如,`volatile`变量的读写操作不会被重排序到屏障之外[^2][^4]。 3. **同步多核缓存状态** 内存屏障通过触发缓存一致性协议(如MESI协议),强制多核CPU同步缓存行状态,避免因缓存不一致导致的数据错误[^3][^4]。 --- #### 二、内存屏障的实现原理 1. **JVM层面的实现** - Java内存模型(JMM)定义了`LoadLoad`、`StoreStore`、`LoadStore`、`StoreLoad`四种屏障类型,对应不同的重排序限制场景。 - JIT编译器在生成字节码时,会根据代码的同步需求(如`synchronized`、`volatile`)插入对应的屏障指令。例如,`volatile`写操作后会自动插入`StoreStore``StoreLoad`屏障[^2][^4]。 2. **CPU指令支持** - 不同CPU架构对内存屏障的实现不同。例如: - x86架构通过`LOCK`前缀指令(如`LOCK ADD`)隐式实现内存屏障功能[^4]。 - ARM架构需要显式使用`DMB`(数据内存屏障)指令[^3]。 - JVM会根据目标平台的特性,选择最优的屏障实现方式,以平衡性能与正确性[^1][^3]。 3. **volatile变量的内存语义** - **写操作**:在写入`volatile`变量后,JVM插入`StoreStore`屏障(防止与普通写重排序`StoreLoad`屏障(强制刷新缓存到主内存)。 - **读操作**:在读取`volatile`变量前,JVM插入`LoadLoad`屏障(防止与普通读重排序`LoadStore`屏障(确保后续写操作可见)。 例如以下代码的汇编层会插入`LOCK ADD`指令实现屏障效果[^4]: ```java volatile int value = 1; value = 2; // 对应汇编指令:mov + lock addl ``` --- #### 三、性能与优化 - **性能开销**:内存屏障会阻止指令重排序强制刷新缓存,可能降低并行度并增加延迟[^1]。 - **JVM优化策略**:JIT编译器通过分析程序执行路径,仅在必要位置插入屏障。例如,单线程环境或无竞争场景下会省略部分屏障[^1][^3]。 --- #### 四、典型应用场景 1. **volatile变量**:通过内存屏障实现多线程间的可见性与有序性。 2. **锁机制(synchronized)**:锁释放时插入屏障,确保临界区内的修改对其他线程可见。 3. **并发工具类(如AtomicInteger)**:基于CAS操作与内存屏障保证原子性可见性。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tilblackout

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值