10.Memory Ordering
旧版本的 ARM 架构按程序顺序执行所有指令,每条指令在开始下一条指令之前都会完全执行完毕。
较新的处理器采用了多种优化措施,这些措施与指令的执行顺序以及内存访问的方式有关。如我们所见,核心执行指令的速度明显高于外部内存的速度。缓存和写缓冲区用于部分隐藏这种速度差异所带来的延迟。其一个潜在的影响是重新排序内存访问。核心执行加载和存储指令的顺序不一定与外部设备看到的访问顺序相同。
在上图中,列出了按程序顺序排列的三条指令。第一条指令向外部内存执行写操作,在此示例中,该操作进入写缓冲区(访问 1)。接下来是两条读取操作,一条在缓存中未命中(访问 2),另一条在缓存中命中(访问 3)。这两条读取操作可能在写缓冲区完成与访问 1 相关的写入之前完成。缓存中的命中-未命中行为意味着,在缓存中命中的加载(如访问 3)可以在程序中较早未命中的加载(如访问 2)之前完成。
仍然可以保持硬件按您编写的顺序执行指令的假象。一般而言,只有少数情况下需要担心此类影响。例如,如果您正在修改 CP15 寄存器,复制或以其他方式更改内存中的代码,可能需要明确使核心等待这些操作完成。
对于支持投机数据访问、多发指令、缓存一致性协议和乱序执行以实现额外性能提升的高性能核心,重新排序的可能性更大。通常,在单核系统中,这种重新排序的影响对您是不可见的。硬件处理许多可能的风险,确保数据依赖关系得到遵守,并确保读取返回正确的值,以允许由先前写入引起的潜在修改。
然而,在多个核心通过共享内存(或以其他方式共享数据)进行通信的情况下,内存排序问题变得更加重要。一般来说,您最关心的确切内存排序是在多个执行线程必须同步的点。
符合 ARMv7-A 架构的处理器采用弱排序的内存模型,这意味着内存访问的顺序不必与加载和存储操作的程序顺序相同。该模型可以在内存读取操作(如 LDR、LDM 和 LDD 指令)与彼此、存储操作以及某些其他指令之间进行重新排序。对正常内存的读写可以由硬件重新排序,这种重新排序仅受数据依赖关系和显式内存屏障指令的限制。在需要更强排序规则的情况下,这会通过描述该内存的转换表条目的内存类型属性传达给核心。对核心强制执行排序规则限制了可能的硬件优化,因此会降低性能并增加功耗。
10.1 ARM memory ordering model
Cortex-A 系列处理器采用弱排序的内存模型。然而,在此模型中,特定的内存区域可以标记为强排序。在这种情况下,内存事务保证按照发出顺序发生。
定义了三种互斥的内存类型。所有内存区域都配置为这三种类型之一:
此外,对于普通内存,可以指定该内存是否可共享(由其他代理访问)。对于普通内存,还可以指定内部和外部的可缓存属性。
在下表中,A1 和 A2 是两个访问不同地址的操作。A1 在程序代码中发生在 A2 之前,但写入操作可以以乱序方式发出。
10.1.1 Strongly-ordered and Device memory
对强排序和设备内存的访问具有相同的内存排序模型。此内存的访问规则如下:
访问的数量和大小将被保留。访问是原子的,不会在中途被中断。
读写访问都可能对系统产生副作用。访问永远不会被缓存。不会执行投机性访问。
访问不能是非对齐的。
到设备内存的访问顺序保证与访问设备内存的指令的程序顺序相对应。此保证仅适用于同一外设或内存块中的访问。此块的大小由实现定义,但最小大小为 1KB。
在 ARMv7 架构中,内核可以在强排序或设备内存访问周围重新排序普通内存访问。
设备内存与强排序内存之间的唯一区别是:
系统外设几乎总是被映射为设备内存。
设备内存类型的区域可以使用共享属性进行描述。在某些 ARMv6 处理器上,设备访问的共享属性用于确定将用于访问的内存接口,标记为设备、非共享的区域的内存访问使用专用接口,即私有外设端口。此机制在 ARMv7 处理器上不使用。
注意:这些内存排序规则仅提供有关显式内存访问(由加载和存储指令引起的访问)的保证。该架构不提供关于指令获取或翻译表遍历与此类显式内存访问的排序的类似保证。
10.1.2 Normal memory
普通内存用于描述内存系统的大部分区域。所有的 ROM 和 RAM 设备都被视为普通内存。
普通内存的属性如下:
普通内存区域还必须具有缓存属性。有关支持的缓存策略的详细信息
。ARM 架构为普通内存支持两级缓存的缓存属性,即内缓存和外缓存。这些缓存级别与实现的物理缓存级别之间的映射由实现定义。内缓存指的是最内部的缓存,始终包括核心的一级缓存。实现可能没有任何外缓存,或者可以将外缓存属性应用于二级或三级缓存。例如,在包含 Cortex-A9 处理器和 L2C-310 级二缓存控制器的系统中,L2C-310 被视为外缓存。Cortex-A8 的二级缓存可以配置为使用内缓存或外缓存策略。
共享性 普通内存还必须被指定为可共享(Shareable)或不可共享(Non-Shareable)。具有不可共享属性的普通内存区域仅供该核心使用。核心不需要使对该位置的访问与其他核心一致。如果其他核心共享此内存,任何一致性问题必须通过软件处理。例如,可以通过让各个核心执行缓存维护和屏障操作来实现。
外部可共享属性允许定义包含多个一致性控制级别的系统。例如,一个内部可共享域可以由 Cortex-A15 集群和 Cortex-A7 集群组成。在一个集群内,核心的数据缓存对于所有具有内部可共享属性的数据访问是一致的。与此同时,外部可共享域可能包括这个集群和一个具有多个核心的图形处理器。一个外部可共享域可以包含多个内部可共享域,但一个内部可共享域只能属于一个外部可共享域。
设置了可共享属性的区域是可以被系统中其他代理访问的区域。在同一共享域内,其他处理器对该区域内存的访问是一致的。这意味着您不必担心数据或缓存的影响。如果没有可共享属性,在未保持缓存一致性的情况下,您必须自己显式管理一致性。
ARMv7 架构允许您将可共享内存指定为内部可共享或外部可共享(后者意味着该位置既是内部可共享的,又是外部可共享的)。
10.2 Memory barriers
内存屏障是一种指令,它要求核心在程序中内存屏障指令前后应用顺序约束,确保发生在屏障前后的内存操作按指定顺序执行。在其他架构中,这类指令也被称为“内存栅栏”(memory fences)。
术语“内存屏障”也可以用来指代一种编译器机制,防止编译器在执行优化时跨越屏障调度数据访问指令。例如,在 GCC 中,您可以使用内联汇编的内存破坏(memory clobber)机制,表示该指令更改了内存,因此优化器不能重新排序屏障后的内存访问。其语法如下:
asm volatile("" ::: "memory");
ARM 的 RVCT 包含一个类似的内部函数,称为 __schedule_barrier()
。
然而,这里我们讨论的是通过专用 ARM 汇编语言指令提供的硬件内存屏障。正如我们所看到的,诸如缓存、写缓冲和乱序执行等内核优化可能导致内存操作的执行顺序与代码中的顺序不一致。通常,这种重新排序对您是不可见的。应用程序开发者通常不需要担心内存屏障问题。然而,在某些情况下,例如设备驱动程序中,或者当您有多个数据观察者时,可能需要处理这些排序问题以确保数据同步。
ARM 架构指定了一些内存屏障指令,可以让您强制核心等待内存访问的完成。这些指令在 ARM 和 Thumb 代码中均可使用,并且适用于用户模式和特权模式。在架构的早期版本中,这些指令仅通过 CP15 操作在 ARM 代码中执行。虽然这些操作现已弃用,但为了兼容性仍然保留。
让我们从单核系统中这些指令的实际效果开始描述。这是《ARM 架构参考手册》中描述的简化版本,旨在介绍这些指令的使用。术语“显式访问”指的是程序中的加载或存储指令导致的数据访问,不包括指令获取。
数据同步屏障 (DSB)
此指令强制核心等待所有挂起的显式数据访问完成后,才能执行任何其他指令阶段。它对指令的预取没有影响。
数据内存屏障 (DMB)
此指令确保在屏障之前的所有内存访问按程序顺序在系统中被观察到,然后显式内存访问才能按程序顺序在屏障之后出现。它不会影响核心上正在执行的其他指令的顺序,也不影响指令获取。
指令同步屏障 (ISB)
此指令刷新核心中的流水线和预取缓冲区,以便 ISB 之后的所有指令从缓存或内存中获取。它确保诸如 CP15 或 ASID 更改、TLB 或分支预测器操作等上下文改变操作,在 ISB 指令之前执行的操作,对 ISB 之后获取的指令是可见的。该操作本身不会导致数据和指令缓存之间的同步,但作为这类操作的一部分是必须的。
可以使用 DMB 或 DSB 指令指定几种选项,以提供访问类型和适用的共享域,具体如下:
SY:这是默认选项,表示屏障适用于整个系统,包括所有核心和外设。
ST:仅等待存储操作完成的屏障。
ISH:仅适用于内部可共享域的屏障。
ISHST:结合 ST 和 ISH 的屏障,即仅存储到内部可共享域。
NSH:仅适用于统一点 (PoU) 的屏障。
NSHST:仅等待存储操作完成且仅到统一点的屏障。
OSH:仅适用于外部可共享域的屏障操作。
OSHST:仅等待存储操作完成且仅适用于外部可共享域的屏障操作。
为了更好地理解这些操作,您需要在多核系统中使用更广义的 DMB 和 DSB 操作定义。以下文本中的“处理器(或代理)”一词不一定仅指核心,也可以指 DSP、DMA 控制器、硬件加速器或任何访问共享内存的模块。
DMB 指令的作用是在共享域内强制内存访问顺序。共享域内的所有处理器都会确保在 DMB 指令之前的所有显式内存访问,在观察到 DMB 之后的显式内存访问之前完成。
DSB 指令与 DMB 的效果相同,但除此之外,它还将内存访问与完整的指令流同步,不仅是其他内存访问。这意味着当发出 DSB 时,执行将暂停,直到所有未完成的显式内存访问完成。当所有未完成的读取完成且写缓冲区清空后,执行恢复正常。
通过一个示例更容易理解屏障的效果。考虑一个四核 Cortex-A9 集群的情况。该集群形成了一个单一的内部可共享域。当集群中的某个核心执行 DMB 指令时,该核心将确保屏障前按程序顺序的所有数据内存访问在屏障之后的任何显式内存访问之前完成。这样,可以保证集群中的所有核心都按相同顺序看到屏障两侧的访问。如果使用 DMB ISH 变体,则无法保证外部观察者(如 DMA 控制器或 DSP)也能看到相同的顺序。
10.2.1 Memory barrier use example
考虑有两个核心 A 和 B,以及普通内存中的两个地址(Addr1 和 Addr2),它们存储在核心寄存器中。每个核心执行两条指令,如以下示例所示:
在这个场景中,没有顺序要求,无法确定任何事务发生的顺序。地址 Addr1 和 Addr2 是独立的,两个核心(A 和 B)都没有必须按程序中的顺序执行加载和存储操作的要求,也无需关心对方核心的活动。
因此,这段代码可能有四种合法的结果,即核心 A 的寄存器 R1 和核心 B 的寄存器 R3 最终从内存中获得四种不同的值组合:
A 获取旧值,B 获取旧值。
A 获取旧值,B 获取新值。
A 获取新值,B 获取旧值。
A 获取新值,B 获取新值。
如果引入第三个核心 C,您还必须注意,C 观察到的存储顺序不需要与其他核心相同。A 和 B 都可能在 Addr1 和 Addr2 中看到旧值,而 C 看到新值也是完全允许的。
再考虑一个场景,B 核心的代码在读取内存前等待 A 核心设置一个标志位,例如,在 A 核心向 B 核心传递消息的情况下。我们可能会有类似于如下示例的代码:
再次强调,这可能不会按照预期的方式执行。没有理由禁止核心B在读取[Flag]之前,推测性地从[Msg]中读取。这是正常的弱序内存,核心并不知道两者之间可能存在的依赖关系。你必须通过插入内存屏障来明确地强制这种依赖。在这个例子中,实际上你需要两个内存屏障。核心A在两个存储操作之间需要一个DMB,以确保它们按照你最初指定的顺序发生。核心B在执行LDR R0, [Msg]之前需要一个DMB,以确保在标志被设置之前不会读取消息。
10.2.2 Avoiding deadlocks with a barrier
另一种如果不使用屏障指令可能导致死锁的情况是,当一个核心写入一个地址后,轮询外设应用的确认值。下面的示例展示了可能导致问题的代码类型。
在没有多处理扩展的情况下,ARMv7架构并不严格要求对[Addr]的存储操作必须完成(它可能在写缓冲区中,而内存系统忙于读取标志),因此两个核心可能会发生死锁,各自等待对方。在存储操作后插入一个DSB可以强制核心在读取Flag之前先观察到其存储。实现多处理扩展的核心要求在有限的时间内完成访问(即,它们的写缓冲区必须排空),因此不需要屏障指令。
10.2.3 WFE and WFI Interaction with barriers
WFE(等待事件)和WFI(等待中断)指令使你能够停止执行并进入低功耗状态。为了确保在执行WFI或WFE之前的所有内存访问都已完成(并对其他核心可见),你必须插入一个DSB指令。
另一个考虑因素与在多处理系统中使用WFE和SEV(发送事件)有关。这些指令使你能够减少与锁获取循环(自旋锁)相关的功耗。试图获取互斥锁的核心可能会发现其他核心已经持有该锁。与其让核心重复轮询锁,不如使用WFE指令暂停执行并进入低功耗状态。
当识别到中断或其他异步异常时,核心会唤醒,或者另一个核心通过SEV指令发送事件。持有锁的核心将在释放锁后使用SEV指令唤醒处于WFE状态的其他核心。就内存屏障指令而言,事件信号不被视为显式内存访问。因此,我们必须确保释放锁的内存更新在执行SEV指令之前实际上对其他处理器可见。这需要使用DSB。DMB不足以满足要求,因为它只影响内存访问的顺序,而没有将它们与特定指令同步,而DSB将防止SEV执行,直到所有前面的内存访问被其他核心看到。
10.2.4 Linux use of barriers
屏障是为了强制内存操作的顺序而要求使用的。通常你不需要理解或明确使用内存屏障,因为它们已经包含在内核的锁定和调度原语中。然而,设备驱动程序的编写者或那些希望理解内核操作的人可能会发现详细描述很有用。
编译器和核心微架构优化允许指令及其相关内存操作的顺序被改变。然而,有时你希望强制执行特定的内存操作执行顺序。例如,你可以写入内存映射的外设寄存器。这一写操作可能在系统的其他地方产生副作用。在程序中的该操作之前或之后的内存操作可能看起来可以被重新排序,因为它们操作于不同的位置。然而,在某些情况下,你希望确保在该外设写操作完成之前,所有操作都已完成。或者,你可能希望确保外设写操作完成后再开始任何额外的内存操作。
Linux提供了一些函数来实现这一点,如下所示:
指示编译器不允许特定内存操作的重新排序。通过调用barrier()
函数来实现。这仅控制编译器的代码生成和优化,并不影响硬件的重新排序。
调用映射到ARM处理器指令的内存屏障函数,这些指令执行内存屏障操作。这些操作强制执行特定的硬件顺序。可用的屏障如下(在编译了Cortex-A SMP支持的Linux内核中):— 读内存屏障rmb()
函数确保在屏障之前出现的任何读取在执行屏障之后的任何读取之前完成。— 写内存屏障wmb()
函数确保在屏障之前出现的任何写入在执行屏障之后的任何写入之前完成。— 内存屏障mb()
函数确保在屏障之前出现的任何内存访问在执行屏障之后的任何内存访问之前完成。
这些屏障有对应的SMP版本,称为smp_mb()
、smp_rmb()
和smp_wmb()
。这些用于在同一集群内部的核心之间强制正常可缓存内存的顺序,例如,Cortex-A15集群中的每个核心。它们可以与设备一起使用,甚至对正常的非可缓存内存也有效。当内核在没有CONFIG_SMP
的情况下编译时,这些函数的每次调用都扩展为barrier()
语句。
Linux提供的所有锁定原语都包含任何所需的屏障。对于这些内存屏障,几乎总是需要一对屏障。有关更多信息,请参见相关内容。
10.3 Cache coherency implications
缓存对应用程序员来说大多数是不可见的。然而,当系统中其他地方的内存位置发生变化,或应用代码中的内存更新需要对系统的其他部分可见时,缓存可能会变得可见。
一个包含外部DMA设备和核心的系统提供了可能问题的简单示例。可以出现两种导致一致性失效的情况。如果DMA从主内存读取数据,而核心缓存中存在更新的数据,DMA将读取到旧数据。类似地,如果DMA将数据写入主内存,而核心缓存中存在陈旧数据,核心可能继续使用旧数据。
因此,在DMA开始之前,核心数据缓存中的脏数据必须被显式清除。同样,如果DMA正在复制数据供核心读取,则必须确保核心数据缓存中不包含陈旧数据。DMA写入内存时不会更新缓存,这可能要求核心在启动DMA之前清除或使受影响的内存区域无效。由于所有ARMv7-A处理器都可以进行推测性内存访问,因此在使用DMA后还需要进行无效化处理。
10.3.1 Issues with copying code
引导代码、内核代码或JIT编译器可以将程序从一个位置复制到另一个位置,或修改内存中的代码。没有硬件机制来维护指令缓存和数据缓存之间的一致性。你必须通过使受影响区域无效来无效化陈旧的指令缓存中的代码,并确保写入的代码实际上已到达主内存。如果核心打算跳转到修改后的代码,则需要特定的代码序列,包括指令屏障。
10.3.2 Compiler re-ordering optimizations
重要的是要理解,内存屏障指令仅适用于硬件对内存访问的重新排序。插入硬件内存屏障指令可能不会对编译器的操作重新排序产生任何直接影响。在C语言中,volatile
类型修饰符告诉编译器,该变量可能会被当前正在访问它的代码以外的其他事物更改。这通常用于C语言访问内存映射的I/O,使得可以通过指向volatile
变量的指针安全地访问这些设备。C标准没有提供与在多核心系统中使用volatile
相关的规则。因此,虽然你可以确信volatile
的加载和存储会按程序指定的顺序发生,但对于相对于非volatile
加载或存储的访问重新排序,并没有这样的保证。这意味着volatile
并不提供实现互斥锁的捷径。
(广告时间)
Arm架构类课程:
安全热销大课程:
安全类经典课程:
其它课程:
铂金VIP课程介绍
之最介绍
招牌课程:Truszone标准版、Trustzone高配版
销量前三课程:ARM三期、Secureboot、Android15安全架构
持续更新的课程:ARM三期、铂金VIP
非常好非常好但又被忽视的课程:CA/TA开发
近期更新/力推的课程/重点课程:optee系统架构从入门到精通
说点心里话:
1、不要再说课贵了,你看看咱这是啥课?别家的能比不?请不要拿通用的linux、android、python、C语言和咱这专业课比。
2、咱们的VIP是数十门课程的集合。不要拿别人一门课程的价格对标咱这20门课程价格。
3、这些知识很多人都会,但能拿出来讲的有多少人? 愿意拿出来讲的有多少人?会讲的又有多少人?
4、价格都是认真计算的,并非随意定价。都是根据内容质量、核心知识点、时长和节数计算而来。从来不无缘无故涨价(涨价是需要理由的,如课程内容增加了....)。咱靠的是内容质量和长期服务,而不是运营和营销(无脑涨价)。
5、如果你刷到此处,可能是老粉/铁粉,记得点赞、评论哦。感谢您的支持。