Armv8-A memory systems 笔记

Armv8-A memory systems 笔记

1 Armv8-A memory systems

Armv8-A 架构中的内存系统,涵盖以下关键内容:

  • 内存模型(Memory Model)
  • 内存类型(Memory Types)
  • 内存属性(Memory Attributes)
  • 屏障机制(Barriers)

在以下场景中,开发者必须理解内存系统的运行机制及访问顺序(Access Ordering),以确保系统行为的正确性和一致性。:

  • 代码直接与硬件交互;
  • 代码与其他核心上运行的代码交互;
  • 代码直接加载或写入将被执行的指令;
  • 代码修改地址转换表(Translation Tables)。

当你的代码需要确保内存访问顺序在系统中被其他核心或设备可见时,可能需要显式地插入内存屏障指令来防止重排序。

2 The memory model

编译器提供了多种选项,旨在提升生成的可执行文件的运行速度或减小其体积。对于源代码中的每一行,编译器都可以选择多种不同的汇编指令进行实现。

Armv8-A 架构采用弱排序(Weakly Ordered)的内存模型。这意味着对于加载(load)和存储(store)操作,内存访问的实际顺序不一定与程序中指令的顺序一致。

在优化过程中,处理器和系统组件可以对内存读取操作进行重排序,以提高数据吞吐量。写操作也可能被重排序。这种机制有助于降低处理器与外部内存之间所需的带宽,并隐藏外部内存访问所带来的高延迟。

为了使重排序成为可能,必须使用允许此类优化的内存类型。硬件可以对Normal 类型内存的读写操作进行重排序。读写操作也可以通过地址依赖性或半屏障(half barriers)来进行排序。然而,如果存在数据依赖关系或显式的内存屏障指令,则会阻止这种重排序。在某些情况下,系统需要更强的访问顺序规则,可以通过页表项中的内存类型属性来向处理器传达这些顺序要求。

PS:“half barrier”指的是一种不具备完整屏障效果的机制,它只能在某些条件下部分阻止内存访问重排序,但不能像完整的内存屏障指令(如DSB、ISB)那样强制所有访问顺序。它通常指以下两类行为:

  • 地址依赖性(Address Dependency):当一个内存访问的地址依赖于前一个加载指令的结果时,ARM 处理器会保证地址依赖顺序,即:
    加载 A → 加载 B(B 的地址依赖于 A 的结果)
    ARM 保证 B 不会在 A 完成之前执行
    这种依赖关系会形成一个“半屏障”,因为它只约束了加载指令之间的顺序,但不约束写入或其他类型的访问。
  • 数据依赖性(Data Dependency):类似地,如果一个指令的执行依赖于前一个指令的结果(例如,写入地址或数据依赖于前一个加载),ARM 也会保持这种顺序。但这种依赖性不等同于完整的屏障,因为它不阻止其他非依赖指令被乱序执行;不保证其他核心或设备看到的顺序一致。

高性能系统通常支持以下技术,从而进一步增强硬件对内存访问的重排序能力:

指令多发(Multiple Issue of Instructions)
处理器可以在每个时钟周期中发出并执行多条指令。一些指令可能会并行进入流水线的执行阶段,因此它们的执行顺序可能与程序中的顺序不同。

指令乱序执行(Out-of-Order Execution)
许多处理器支持对无依赖关系的指令进行乱序执行。当某些指令在执行阶段因等待资源而阻塞时,其他无依赖的指令仍可继续执行。这种机制也可能改变程序的执行顺序。

推测执行(Speculation)
当处理器遇到条件指令(如分支)时,它可以在尚未确定该指令是否会被执行的情况下,提前执行后续指令。如果推测正确,结果将更早可用。指令获取推测(Instruction Fetch Speculation),使得处理器获取的指令可能并不是当前程序定义的顺序。

推测性加载(Speculative Loads)
如果被推测执行的指令是一个从可缓存区域执行加载的指令,则可能会触发缓存行填充,并可能驱逐已有的缓存行。

加载与存储优化(Load and Store Optimizations)
由于访问外部内存的延迟较高,处理器可以通过合并多个写操作为一个更大的事务来减少传输次数,从而提升效率。

外部内存系统行为(External Memory Systems)
在许多 SoC(系统级芯片)中,存在多个主控(master)模块可以发起内存传输,并且存在多条通往从属设备(slave)的路径。

例如,DRAM 控制器可以同时接受来自多个主控的请求。这些事务可能被缓冲或重排序,因此来自不同主控的访问可能需要不同的周期数完成,甚至可能出现访问顺序颠倒的情况。

缓存一致性集群处理(Cache Coherent Cluster Processing)
在多核集群中,硬件缓存一致性机制可以在核心之间迁移缓存行。不同核心可能以不同顺序观察到对同一缓存地址的更新,这些更新也可能与外部内存中的数据不一致。

编译器优化行为(Optimizing Compilers)
优化编译器可以重排指令,以隐藏延迟或更好地利用硬件特性:
编译器可能会提前移动内存访问指令,使其更早执行,从而在需要使用该值之前完成加载;编译器还可以根据处理器的多发流水线结构进行指令调度,以提升执行效率。
在单核系统中,这种重排序对程序员是透明的,因为处理器会自动检查数据依赖关系并确保正确性。但在多核系统中,多个核心通过共享内存进行通信或数据共享时,内存访问顺序问题变得尤为重要,必须显式管理

3 Memory Types

ARMv8-A 架构定义了两种互斥的内存类型:Normal(普通)内存Device(设备)内存。系统中的所有内存区域都必须被配置为这两种类型之一。

3.1 Normal 内存(普通内存)

Normal 类型用于存储代码和大多数数据区域,例如 RAM、Flash 或 ROM。这类内存具有最高的处理器性能,因为它采用弱排序模型(Weakly Ordered),允许编译器进行更激进的优化。处理器可以对 Normal 内存进行重排序、重复访问、合并访问。
PS:这里的“重复访问”(repeat),指的是处理器在访问 Normal 类型内存时,可能会重复执行某些内存访问操作,即使在程序语义上只执行一次。这种行为是 ARM 弱内存模型的一部分,允许硬件或编译器在不违反程序可观察行为的前提下,进行性能优化。
例如,处理器在分支预测或乱序执行中,可能多次尝试读取某个地址。某些架构中,如果处理器在执行写操作时遇到中断或异常,恢复执行后可能会重新执行该写指令,导致重复写入。
NOTE:这类重复行为只适用于 Normal memory,因为它不会引发副作用。对于 Device memory,重复访问是不允许的,因为每次访问都可能触发外设行为(如中断、状态变化等)

处理器可以进行推测性访问(Speculative Access),即在程序未显式引用之前就提前读取数据或指令,来源包括:分支预测、推测性缓存行填充、乱序加载等硬件优化。

为了获得最佳性能,对于应用代码和数据,建议始终标记为 Normal 类型,若需要强制顺序访问,应使用显式内存屏障指令(如 DMB、DSB)。Normal 类型之间的访问不要求顺序完成,也不要求与 Device 类型之间的访问顺序一致。不过,处理器自身必须处理地址依赖性引发的风险(Hazards)。例如:

1
2
STR X0, [X2]     ; 写入地址 X2
LDR X1, [X2] ; 从地址 X2 读取

运行单个线程的单个处理器,需要始终确保放置在X1中的值就是从寄存器X0写到X2中存储的地址的值。

更复杂的例子:

1
2
3
4
5
6
ADD X4, X3, #3
ADD X5, X3, #2
...
STR X0, [X3] ; 将 X0 写入地址 X3
STRB W1, [X4] ; 将 W1 的低字节写入地址 X3 + 3
LDRH W2, [X5] ; 从地址 X3 + 2 读取半字(16 位)到 W2

在这个例子中,内存访问发生在彼此重叠的地址区域。处理器必须确保内存的更新行为看起来像 STR 和 STRB 指令按程序顺序执行,以便 LDRH 指令能够读取到最新的、正确的数据值。同时,处理器仍然可以将 STR 和 STRB 指令合并为一次写入操作,只要该合并写入包含了所有最新的数据,并且不会破坏程序语义。

3.2 Device 内存(设备内存)

设备内存类型用于内存映射外设以及所有可能在访问时产生副作用的内存区域。例如:对定时器的读取是不可重复的,因为每次读取可能返回不同的值;向控制寄存器写入可能会触发中断。

因此,设备内存类型对处理器核心施加了更多的访问限制。
对这类内存的访问必须严格按照程序执行的次数进行:

  • 两次写入必须实际执行两次;
  • 两次读取必须都发生;

这对于访问外设控制寄存器尤为重要。然而,设备之间的访问顺序,或不同内存类型之间的访问顺序,通常不提供保证。

此外,处理器不能对标记为 Device 的内存区域执行推测性数据访问。尝试从标记为 Device 的区域执行代码是不可预测的(UNPREDICTABLE)行为。当某条指令可能导致不可预测行为时,ARM 架构可以定义一组受限不可预测(Constrained UNPREDICTABLE)的行为范围。具体实现可以选择:

  • 将该指令获取视为访问一个具有 Normal 类型但不可缓存属性的内存;
  • 或者触发权限错误(Permission Fault)。

ARM 定义了四种不同的设备内存类型,用于规定访问规则的严格程度。随着类型的“放松”,规则也逐渐宽松:

  • Device-nGnRnE:限制最严格;
  • Device-nGnRE
  • Device-nGRE
  • Device-GRE:限制最宽松。

后缀字母含义说明:
G(Gathering):允许将多个访问合并为一个事务。例如,将两个字节写合并为一个半字写;
nG(non-Gathering):禁止合并,必须严格按照代码中指定的访问次数和大小执行。

R(Reordering):允许对同一设备的访问进行重排序;
nR(non-Reordering):禁止重排序,访问必须按照程序顺序在总线上出现;

E(Early Ack):允许中间写缓冲器在数据尚未到达外设之前就发送写入完成信号;
nE(non-Early Ack):写入响应必须由外设本身发出,不能由中间缓冲器提前确认。

注意: 虽然 Device-GRE 是设备内存类型中限制最少的一种,但不等同于 Normal 类型内存。它们的本质差异有:

特性 Device-GRE Normal memory
内存类型分类 属于设备内存(Device) 属于普通内存(Normal)
是否可能有副作用 ✅ 有副作用(如触发中断、状态变化) ❌ 无副作用,纯数据读写
是否允许推测访问 ❌ 禁止推测性读取和执行 ✅ 允许推测性加载和执行
是否可执行代码 ❌ 执行代码为 UNPREDICTABLE ✅ 可执行代码(如程序指令)
是否可缓存 ❌ 不可缓存 ✅ 可配置为缓存(Write-back、Write-through)
是否可合并访问 ✅ 可合并(Gathering) ✅ 可合并
是否可重排序 ✅ 可重排(Reordering) ✅ 可重排
是否可提前确认写入 ✅ 可提前确认(Early Ack) ✅ 可通过写缓冲器提前确认
编译器优化空间 ❌ 极度受限,必须精确执行 ✅ 可重排、合并、提前执行等优化
适用场景 外设寄存器、控制器 应用代码、数据、堆栈、等

4 Memory attributes

系统的内存映射可以被划分为多个区域。每个区域都可以具有不同的内存属性,例如:

  • 访问权限(包括不同特权级别的读写权限);
  • 内存类型(Normal 或 Device);
  • 缓存策略(Cache Policy)。

下图展示了一个示例系统的内存映射结构:

在内存映射中,功能性代码和数据被分组管理,每个区域的属性由内存管理单元(MMU)单独控制。

除了内存类型之外,内存属性还包括对以下方面的控制:

  • 可缓存性(Cacheability)
  • 可共享性(Shareability)
  • 访问权限(Access Permissions)
  • 执行权限(Execution Permissions)

共享性(Shareability)和缓存属性(Cacheability)仅适用于 Normal 类型内存。 Device 类型内存始终是 不可缓存(Non-cacheable) 且 Outer-shareable。对于可缓存的内存区域,可以使用属性向处理器指示缓存分配策略。

4.1 缓存与共享属性详解

Normal 类型内存可以被标记为 可缓存(Cacheable) 或 不可缓存(Non-cacheable);缓存行为可以通过 Inner 和 Outer 属性分别控制,以支持多级缓存结构;Inner 属性通常用于处理器内部缓存(如 L1);Outer 属性用于处理器外部缓存(如 L2/L3 或系统缓存);Inner 与 Outer 的划分是 实现定义(IMPLEMENTATION DEFINED),即由具体硬件平台决定。

共享属性(shareable attribute)用于定义某个内存位置是否可被多个处理器核心共享访问。将某个区域标记为“非共享”(Non-shareable)表示该区域仅供单一核心使用。而将其标记为“内部共享”(Inner Shareable)、“外部共享”(Outer Shareable)或同时标记为两者,则表示该区域可被其他观察者共享访问,例如 GPU 或 DMA 控制器也被视为观察者。

“内部”与“外部”的划分是由具体实现定义的(IMPLEMENTATION DEFINED),即不同的芯片或平台可能有不同的定义标准。这些共享属性定义了一组观察者集合,在该集合中,缓存系统对数据访问是透明的,也就是说,系统会自动维护缓存一致性,使得多个观察者看到的数据是一致的

这也意味着系统必须提供硬件一致性管理机制,以确保在“内部共享域”中的多个核心能够看到该区域的一致副本。如果某个处理器或系统中的其他主控设备不支持一致性机制,那么它必须将这些共享区域视为不可缓存(Non-cacheable),以避免数据不一致

PS:Shareable(可共享) 和 Cacheable(可缓存) 是 ARM 架构中用于描述内存访问行为的两个关键属性,它们虽然常常一起出现,但本质上关注的是不同的系统层面。

  • Cacheable 属性决定处理器在访问该内存区域时,是否可以使用缓存(如 L1/L2)来加速读写
    • 如果访问的是程序代码、局部变量、堆栈,通常是 Cacheable,因为这些数据频繁访问,缓存可以显著提升性能。
    • 如果访问的是设备寄存器,必须是 Non-cacheable,否则可能读到旧值或写入被延迟。
  • Shareable 属性决定该内存区域是否需要在多个处理器核心或主控设备之间保持一致性(coherency)。
    • 如果多个核心共享一个缓冲区(如多线程通信),则必须是 Shareable,否则一个核心写入的数据,其他核心可能看不到。
    • 如果某个区域只被一个核心使用则可以标记为 Non-shareable,避免一致性开销。

注意:Shareable 属性本身不决定是否缓存,而是告诉系统是否需要在多个观察者之间维护一致性;如果某个设备不支持一致性机制,它必须将 Shareable 区域视为 Non-cacheable

4.2 共享域(Domains)

由于缓存一致性硬件的存在,数据内存访问可能比没有一致性机制时更耗时且更耗能。为了降低这种开销,系统可以只在少量主控设备之间维持一致性,并确保它们在物理上彼此接近。因此,ARM 架构将系统划分为多个共享域(Domains),从而可以将一致性维护的开销限制在真正需要的区域。

系统中每一次内存事务的共享性属性是根据以下因素分配的:

  • 所访问内存区域的属性(由 MMU 页表定义);
  • 核心的配置(同一集群中的核心可能配置不同);
  • 互连结构的实现方式;
  • 互连与主控设备之间的集成方式。

此外,系统还可以通过一些特定操作,结合共享域的定义,来限定这些操作的作用范围。

共享域示例:

非共享(Non-shareable)
该域仅包含本地代理(local agent),即当前处理器或主控设备。访问该域的内存不需要与其他核心、处理器或设备进行同步。

内部共享(Inner Shareable)
该域可被其他代理共享,但不一定包括系统中的所有代理。 一个系统可以拥有多个内部共享域。 对某个内部共享域的操作不会影响系统中其他内部共享域。

外部共享(Outer Shareable)
该域由多个代理共享,可能包括一个或多个内部共享域。 对外部共享域的操作会影响其包含的所有内部共享域。 但它的行为并不等同于内部共享操作,即它不会在内部共享域之间自动传播一致性操作。

多层共享域的行为逻辑
情况一:Inner A 内部的操作

  • 如果核心 0 和核心 1 都属于 Inner Shareable A,那么它们之间的缓存一致性是自动维护的;核心 0 写入某个共享内存区域,核心 1 可以自动看到更新;
  • 但这个操作不会自动传播到 Inner B,即使 Inner B 也属于同一个 Outer Shareable 域。

情况二:Outer Shareable 层面的操作

  • Outer Shareable 域包含多个 Inner Shareable 域(如 A 和 B);如果你执行一个作用于 Outer Shareable 的同步操作(如 DMB 或 DSB 指令,指定 Outer Shareable 范围),那么:
  • 该操作会影响所有包含的 Inner Shareable 域;即核心 0 的写入可以通过同步机制显式传播到核心 2(属于 Inner B);
  • 但这依赖于你明确指定同步范围为 Outer Shareable,而不是依赖 Inner A 的自动一致性。

全系统(Full system)
对整个系统的操作会影响系统中所有观察者(observers),包括所有核心、外设、主控设备等。

5 屏障(Barriers)

ARM 架构提供了屏障指令,用于在特定位置强制执行访问顺序和访问完成。 屏障的作用是防止不安全的优化行为发生,并确保特定的内存访问顺序。 但如果在不必要的场景中使用屏障指令,可能会降低软件性能。 因此,在使用屏障时应仔细评估其必要性,并选择正确的屏障类型。

ARM 架构定义了三种屏障指令:

5.1 指令同步屏障(ISB)

ISB(Instruction Synchronization Barrier)用于确保在 ISB 执行完成之前,所有之前的上下文变更操作(如写入系统控制寄存器)已经完成。通常用于 MMU 配置变更、启用 FPU、修改页表等影响执行环境的操作。

在硬件层面,这通常意味着需要刷新指令流水线。 典型应用包括内存管理、缓存控制、上下文切换代码,或代码在内存中被移动的场景。

例如,在 AArch64 中启用浮点单元(FPU)和 SIMD,可以通过写入 CPACR_EL1 寄存器的第 20 位实现。ISB 保证启用操作完成后,FPU 或 NEON 指令才会执行:

1
2
3
4
MRS X1, CPACR_EL1        // 读取 CPACR 到 X1  
ORR X1, X1, #(0x3 << 20) // 设置第 20 位,启用 FPU 和 SIMD
MSR CPACR_EL1, X1 // 写回 CPACR
ISB // 同步上下文,刷新流水线

ISB 会刷新流水线,确保在它之前完成的上下文变更对之后的指令是可见的。 它还确保 ISB 之后的上下文变更不会被之前的指令提前看到。

但这并不意味着每次修改处理器寄存器后都需要使用 ISB。 例如,对 PSTATE 字段、ELR、SP、SPSR 的读写总是按程序顺序执行,不需要额外同步(写入后,后续指令会自然地看到新值;处理器保证这些操作的顺序性和可见性,后续指令不能被提前执行到这些寄存器写入之前。)。

5.2 数据内存屏障(DMB)

DMB(Data Memory Barrier)用于防止数据访问指令在 DMB 指令前后发生重排序。 根据屏障类型,处理器在 DMB 之前执行的某些数据访问(如加载或存储)必须在 DMB之后的某些其他数据访问之前,在指定的可共享性域中的所有其他主机都是可见的。但 DMB 不影响指令获取行为。

例如:

1
2
3
4
LDR X0, [X1]     // 读取必须在下面的 STR 之前被内存系统看到  
DMB ISHLD // 内部共享域的加载屏障
ADD X2, #1 // 可在 LDR 之前或之后执行
STR X3, [X4] // 写入必须在 LDR 之后被内存系统看到

DMB 还确保任何之前的缓存维护操作在后续数据访问执行前已完成:

1
2
3
4
DC CSW, X5       // 按 Set/Way 清理数据缓存  
LDR X0, [X1] // 可能看不到缓存清理的效果
DMB ISH // 内部共享域的屏障
LDR X2, [X3] // 能看到缓存清理的效果

5.3 数据同步屏障(DSB)

DSB(Data Synchronization Barrier)与 DMB 保证相同的访问顺序,但它还会阻止任何后续指令的执行,直到同步完成。 例如,在执行 SEV 指令(向其他核心发送事件)之前,可以使用 DSB 来确保所有维护操作完成。

DSB 会等待当前处理器发出的所有缓存、TLB 和分支预测维护操作在指定共享域中完成。

例如:

1
2
3
4
DC ISW, X5       // 缓存操作必须完成  
STR X0, [X1] // 写入必须完成
DSB ISH // 内部共享域的同步屏障
ADD X2, X2, #3 // 直到 DSB 完成后才可执行

DMB 和 DSB 指令可以带参数,用于指定屏障作用的访问类型(加载/存储)和共享域。 以下是可选参数及其含义:

参数选项 描述 排序要求(前 - 后) 共享域
OSHLD 仅等待加载完成,作用范围为外部共享域 加载-加载,加载-存储 Outer Shareable(外部共享域)
OSHST 仅等待存储完成,作用范围为外部共享域 存储-存储(屏障前的写入必须完成,屏障后的写入必须等待) Outer Shareable(外部共享域)
OSH 任意访问,作用范围为外部共享域 任意-任意(所有在屏障之前的加载和存储都必须完成,屏障之后的加载和存储都必须等待屏障完成) Outer Shareable(外部共享域)
NSHLD 仅等待加载完成,作用范围为非共享域(统一点之前) 加载-加载,加载-存储 (屏障前的加载必须完成,屏障后的加载或存储必须等待) Non-shareable(非共享域)
NSHST 仅等待存储完成,作用范围为非共享域 存储-存储 Non-shareable(非共享域)
NSH 任意访问,作用范围为非共享域 任意-任意 Non-shareable(非共享域)
ISHLD 仅等待加载完成,作用范围为内部共享域 加载-加载,加载-存储 () Inner Shareable(内部共享域)
ISHST 仅等待存储完成,作用范围为内部共享域 存储-存储 Inner Shareable(内部共享域)
ISH 任意访问,作用范围为内部共享域 任意-任意 Inner Shareable(内部共享域)
LD 仅等待加载完成,作用范围为整个系统 加载-加载,加载-存储 Full System(全系统)
ST 仅等待存储完成,作用范围为整个系统 存储-存储 Full System(全系统)
SY 任意访问,作用范围为整个系统(默认值,可省略) 任意-任意 Full System(全系统)

表格中的排序要求说明:

  • Load-Load / Load-Store 屏障要求所有加载在屏障前完成,但不要求存储完成。屏障后的加载和存储必须等待屏障完成。
  • Store-Store 屏障仅影响存储访问,加载可自由重排。
  • Any-Any 屏障要求加载和存储都在屏障前完成,屏障后的加载和存储必须等待屏障完成。

处理器的指令接口、数据接口和 MMU 表遍历器被视为不同的观察者。 因此,为了确保某个接口的访问能被另一个接口观察到,可能需要使用 DSB 指令。

例如,执行数据缓存清理和失效指令 DC CVAU, X0 后,必须插入 DSB,确保后续的页表遍历、页表项修改、指令获取或指令更新能看到新的值。

更新页表的示例:

1
2
3
4
5
STR X0, [X1]         // 更新页表项  
DSB ISHST // 确保写入完成
TLBI VAE1IS, X2 // 失效对应的 TLB 项
DSB ISH // 确保 TLB 失效完成
ISB // 同步上下文,刷新指令流水线

DSB 确保维护操作完成,ISB 确保这些操作的效果对后续指令可见。

推测访问注意事项:处理器可能在任意时间对标记为 Normal 的地址进行推测访问。 因此,在考虑是否需要屏障时,不能只关注显式的加载或存储指令,还要考虑可能的推测行为。

5.4 DSB 对比 DMB

DMB:数据内存屏障

  • 目的:防止数据访问指令(如加载、存储)在屏障前后发生重排;
  • 特点:不阻塞指令执行;仅控制内存访问顺序;
  • 常用于多核共享内存通信、缓存清理后确保顺序;

示例:

1
2
3
LDR X0, [X1]     // 加载数据
DMB ISHLD // 保证加载完成后再执行后续存储
STR X2, [X3] // 写入数据

DSB:数据同步屏障

  • 目的:确保所有数据访问和维护操作完成后,才允许执行后续指令;
  • 特点:阻塞指令执行,直到同步完成;等待缓存、TLB、分支预测等维护操作完成;
  • 常用于页表更新、TLB失效、系统初始化等关键同步点;

示例:

1
2
3
4
5
STR X0, [X1]         // 更新页表项
DSB ISHST // 确保写入完成
TLBI VAE1IS, X2 // TLB失效
DSB ISH // 确保失效完成
ISB // 刷新指令流水线

5.5 单向屏障(One-way Barriers)

AArch64 引入了具有隐式屏障语义的新型加载和存储指令,这些指令比 DMB 或 DSB 更轻量,要求加载和存储在程序顺序中被观察到。

LDAR(Load-Acquire):所有在程序顺序中位于 LDAR 之后的加载和存储,且与目标地址属于同一共享域,必须在 LDAR 之后被观察到
STLR(Store-Release):所有在程序顺序中位于 STLR 之前的加载和存储,且与目标地址属于同一共享域,必须在 STLR 之前被观察到
还有对应的排他版本:LDAXR 和 STLXR。

与 DMB/DSB 不同,LDAR 和 STLR 使用的是目标地址的属性来决定屏障的作用范围,而不是通过参数指定共享域。

LDAR 是“后向屏障”:它只约束自己之后的访问顺序。
STLR 是“前向屏障”:它只约束自己之前的访问顺序;

示例,定义两个共享区域:

  • lock:用于同步的锁变量,标记为 Inner Shareable;
  • data:共享数据,标记为 Inner Shareable;
  • log_buffer:日志缓冲区,标记为 Non-shareable(仅本核使用)。

示例 1:LDAR 的顺序约束

1
2
3
LDAR X0, [lock]       // 读取锁状态(Inner Shareable)
LDR X1, [data] // 读取共享数据(Inner Shareable)
LDR X2, [log_buffer] // 读取日志缓冲区(Non-shareable)

这里LDR X1, [data]:必须在 LDAR 之后执行,因为它与 lock 同属 Inner Shareable 域;
LDR X2, [log_buffer]:不受 LDAR 顺序约束,因为它是 Non-shareable,不在同一共享域中,可能被提前执行。

示例 2:STLR 的顺序约束

1
2
3
STR X1, [data]        // 写入共享数据(Inner Shareable)
STR X2, [log_buffer] // 写入日志缓冲区(Non-shareable)
STLR X0, [lock] // 释放锁(Inner Shareable)

这里STR X1, [data]:必须在 STLR 之前完成,因为它与 lock 同属 Inner Shareable 域;
STR X2, [log_buffer]:不受 STLR 顺序约束,因为它是 Non-shareable,可能被延后执行。

使用 LDAR、STLR实现临界区

图分为三个垂直区块:

  • 进入临界区之前的准备阶段(顶部)

    • LDAR 是关键:它用于读取一个同步变量(如锁),并建立后向顺序约束;
    • 它保证后续的加载和存储指令不能被提前执行,必须在 LDAR 完成后执行;
    • 这可以确保进入临界区前,已经确认锁状态,并且不会提前访问共享数据。
  • 临界区本身(中间)

    • 这些是在临界区中对共享资源的实际操作;
    • 因为之前有 LDAR,这些指令的执行顺序是受约束的,是在锁获取后才访问。

退出临界区后的释放阶段(底部)

  • STLR 是关键:它用于写入同步变量(如释放锁),并建立前向顺序约束;
  • 它保证之前的加载和存储指令必须完成后,STLR 才能被其他核心观察到;这确保其他核心在看到锁被释放时,临界区的操作已经完成

6 在 C 代码中使用屏障

C11 和 C++11 提供了良好的、平台无关的内存模型,比使用平台相关的内建函数(intrinsics)更可取。

所有版本的 C 和 C++ 都定义了序列点(sequence points),但 C11 和 C++11 还引入了内存模型(memory model)的概念。

序列点只能阻止编译器在源代码层面重排指令。它无法阻止处理器在生成的目标代码中重排指令,也无法阻止读写缓冲区在将数据传输到缓存时重排顺序。换句话说,序列点只对单线程代码有效。

对于多线程代码,应使用 C11/C++11 的内存模型特性,或者使用操作系统提供的其他同步机制(如互斥锁 mutex)。

代码中的序列点示例包括:

  • 函数调用;所有之前的表达式求值和副作用(如赋值、内存写入)都完成;函数调用之后的操作不会被提前执行。
  • 对 volatile 变量的访问,与 volatile 变量相关的副作用必须在访问点完成,编译器不能重排这些访问。

C 语言规范对序列点的定义如下:在执行序列中的某些特定点,称为序列点(sequence points),所有先前求值的副作用必须已经完成,而后续求值的副作用尚未发生。

参考连接:

【1】Learn the architecture - ARMv8-A memory systems