在我们写代码的过程中,经常会发现在SDK中会出现__ISB()
或__DSB()
等语句,这也做的目的是建立一个内存屏障,内存屏障可以由处理器内的硬件操作或内存屏障指令触发,它能够让CPU或编译器对屏障指令之前和之后的内存操作施加排序约束。
文章目录
- 1 内存屏障的必要性
- 2 内存类型
- 3 内存排序
- 4 内存排序的限制
- 5 总结
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 Memory
或Strongly-ordered Memory
访问,相比Normal Memory
的访问来说,它们有更多限制:
- 读和写都可能导致系统的异常行为
- 访问不能重复,例如,从异常返回时
- 必须维护访问的数量、顺序和大小。
Strongly-ordered Memory
内存映射的外设和I/O位置通常是强有序内存。在Cortex-M处理器中,System Control Space
是强有序的,而SCS
中包括NVIC
、MPU
、SysTick
定时器和调试组件。
对强有序内存的显式访问,以下规则适用:
- 访问的大小与程序中指定的大小相符。比如,程序要求以字节为单位进行访问,处理器就必须按字节进行读取或写入,而不能超过或低于指定的大小。
- 访问的次数与程序中指定的次数相符,即必须按照程序中指定的次数进行访问
- 这个规则有例外情况,具体参考
《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 Manual
和Revision D of the ARMv7-M Architecture Reference Manual
。这与之前的ARMv7-M Architecture
不同,Revision D
版本删除了强顺序(Strongly-ordered
)和普通(Normal
)内存事务之间的排序要求。这是由于在最新的架构中,普通事务具有推测执行的能力,并且提供了更高性能实现的能力,可以在强顺序访问周围重新排序普通内存访问,除非通过屏障明确阻止这样做。
上表中标记为-
的内存访问操作还有以下额外的限制:
- 如果存在地址依赖性,那么两个内存访问会按照程序顺序进行观察
- 如果只存在控制依赖性,则不需要按照顺序要求来观察两个读访问
- 如果同时存在地址依赖性和控制依赖性,那么需要满足地址依赖性的排序要求
- 如果一个读访问的返回值用于后续的写访问,那么这两个内存访问会按照程序顺序进行观察
- 如果在程序的顺序执行中,某个内存位置不会被写入,那么观察者(如外设或第二个处理器)就不能观察到对该内存位置的写入操作或写入的值。即观察者只能观察到实际发生的写操作,而不能观察到未发生的写操作。
举个例子,A1和A2都是对一个Non-Shareable
的Device Memory
进行访问,这种情况下一定是要等A1的所有序列执行完毕后再执行A2,这个好理解,因为内存是不可共享的,也是不可缓存的。
再来来分析一下表中为-
情况,举个例子:
A1: 处理器对外设A的Non-Shareable
的Device Memory
进行读取操作。
A2: 处理器对外设B的Shareable
的Device Memory
进行写入操作。
根据之前提到的原则,只要满足单处理器指令执行的顺序和行为与程序编写者的意图一致,保证处理器中指令之间的依赖关系,这两个访问操作可以以任意顺序在全局范围内被观察。这意味着观察者(例如外设或其他处理器)在观察到这两个访问操作时,它们的顺序是任意的。
5 总结
Cortex-M处理器相对于指令流程从不执行乱序内存访问,当然,未来不一定会有改变。因此,针对ARMv7-M的代码如果要在ARMv7-AR处理器(如Cortex-A9)上可移植,必须已经考虑了上面的排序模型。在Cortex-M处理器中,内存类型与内存映射相关联。定义了一些体系结构区域,并为每个区域定义了预定义的内存类型。当实现了MPU时,还可以通过编程来更改一些内存类型的定义。
在Cortex-M处理器上,可以使用MPU将SRAM定义为Device
或Strongly-ordered
区域,但这会降低性能,并且无法阻止C编译器在生成的代码中重新排序数据传输。为了获得最佳效率,ARM建议将SRAM定义为Normal
内存,并在内存排序重要的情况下使用内存屏障memory barriers
。
关于内存屏障和内存排序的知识十分复杂和抽象,但又十分重要,下一节我们将继续深入研究。