1 前言
如图1所示, Cortex-M7最大支持4GB的内存寻址,并对内存映射(memory map)做了初步的规定,将整个内存空间划分为了多个内存区域(region)。每个内存区域有着既定的内存类型(memory type)和内存属性(memory attribute),这两者决定了访存的具体行为。
图1 Cortex-M7 memory map
2 内存区域的类型和属性
Memory ordering(内存排序) 描述了CPU访问内存的前后顺序,既可以指编译器编译时生成的内存访问序列,也可以指在运行时(runtime)由CPU生成的内存访问序列。CPU 并不是完全按照我们写的程序的逻辑顺序来访问内存的,在保证原始语义的情况下,编译器会对代码指令进行重新排序, CPU也会对指令进行重新排序、延缓执行、各种缓存等等,即通过乱序执行(out-of-order execution)来提升程序执行效率。
所以,内存排序描述了CPU在内存顺序重排方面的能力,这也直接决定了CPU是否支持乱序执行。乱序执行使得CPU可以在最大程度上利用各类型的内存的总线带宽(bus-bradwidth)以提高程序运行效率。例如,cache比普通的memory banks的访问带宽要大很多。在单线程模式下,编译器和CPU偷偷地做了优化,乱序执行的细节无须由应用层代码操心,彷佛程序确实在严格按照代码的逻辑顺序执行;但在多线程模式下,或者通过内存总线和其他硬件进行数据交互时,乱序执行很容易出问题,这时候就需要内存屏障(memory barriers)指令出场了。
2.1 内存类型memory types
Cortex-M7的内存区域的类型主要有以下几种:
① 正常(Normal)
Cortex-M7采用了流水线机制,通过执行前预取指令和分支预测来优化程序执行效率。对于normal类型内存区域,CPU可以重新排序处理(指令)以提高效率,或者可以做一些乐观的猜测性读操作(ldr);
② 设备内存和强排序内存区域(Device and Strongly-Ordered)
对于设备内存(device memory)或强排序内存区域(Strongly-ordered memory)来说,CPU会老老实实的严格按照程序定义的顺序进行指令执行。
此外,设备内存和强排序内存区域也有所不同,外部内存系统(external memory system)可以用带缓冲地写设备内存(device memory)进行写操作,但不可带缓冲地写强排序内存区域。
2.2 内存属性memory attributes
内存属性主要包括以下两种:
① 可共享(Shareable)
对于可共享内存区域,如果系统中存在多个总线master(bus master,例如CPU配有DMA控制器),内存系统需要在总线master之间提供数据同步机制。所以,可共享即意味着拿来即可用,其内存区域内的数据由内存系统来保证同步性。
此外,强排序内存区域总是可共享的,强排序的目的其实是为了防范语义风险;如果多个总线master可以访问一个不可共享的内存区域,则必须由软件来保证个总线master之间的数据一致性(data coherency)。
② 不可执行(Execute Never,XN)
意为不可执行,即不可从该内存区域取值。谁胆敢从XN内存区域进行取值操作,立马错误异常(fault exception)伺候。
3 内存系统的内存访问顺序
Memory system ordering of memory accesses,描述了内存系统对于内存访问顺序相关的规范。
在不影响指令序列的行为的前提下,对于由显式内存访问指令引起的内存访问,内存系统不保证访问的实际顺序与指令的程序级顺序完全相同,即支持乱序执行。如果程序的正确执行依赖于两个内存访问指令的执行顺序,则软件必须在这两条指令之间加入内存屏障指令来保证其时序。
但是,对于设备内存(device memory)或强排序内存区域(Strongly-ordered memory)来说,内存系统确实在一定程度上保证了访存的时序。例如,假设有由同一主接口(master interface)发起的A1和A2两条访存指令,如果A1在程序中执行的顺序在A2之前,则这两条访存指令的最终内存访问顺序如表1所示:
表1 内存访问顺序示例
从中可见,若两者都为可共享的设备内存或强排序内存,亦或者两者都为不可共享的设备内存,则内存访问都是严格序列化的。
4 各内存区域的访存行为(Behavior of memory accesses)
Cortex-M7对于不同内存区域的访存行为具有不同的规范。其中,Code,SRAM和 external RAM区域可用于加载程序代码,具体如表2所示。
表2 各内存区域的内存访问行为明细
以上只是默认的内存访问行为,如果使能MPU,则可以对其进行重新定义。
当系统拥有cache和可共享内存(shared memory)时,某些内存区域的约束有所增加,某些内存区域被划分成更小颗粒度的区域,具体如表3所示:
表3 各内存区域的共享性和cache策略
其中,WT表示的是write through,no allocate;WBWA表示write back,write allocate,具体含义如图2所示。
图2 Cache读写策略示意图
5 软件程序定义的内存访问顺序
软件程序直观地定义了一个程序执行流(program flow),其规则时基于一定的逻辑顺序。而当软件程序经编译器翻译成可执行的机器码并在CPU上执行时,内存系统通常不会严格按照软件程序定义的时序来进行内存访问,这也就是前文描述的乱序执行。
CPU采用乱序执行的原因主要有以下几点:
① 在不改变软件程序定义的指令序列的预期行为的前提下,CPU可能会对一些访存操作重新排序;
② CPU拥有多个总线接口(bus interface);
③ 不同的内存区域或设备具有不同的等待状态;
④ 一些内存访问是带缓冲的,或者是带分支预测的;
举例来说,如果可以在运行时更改存储器的映射关系或者内存保护区的设置, (通过写MPU 的寄存器),就必须在更改之后立即补上一条 DSB 指令(数据同步指令)。因为对 MPU 的写操作很可能会被放到一个写缓冲中。
写缓冲是为了提高存储器的总体访问效率而设的,但它也有副作用,其中之一,就是会导致写内存的指令被延迟几个周期执行,因此对存储器的设置不能即刻生效,这会导致紧临着的下一条指令仍然使用旧的存储器设置——但程序员的本意显然是使用新的存储器设置。这种紊乱危象是后患无穷的,常会破坏未知地址的数据,有时也会产生非法地址访问 fault。
章节3中描述的一些由内存系统保驾护航的,严格按程序定义的顺序来访问内存的情况。除此之外,需要软件程序通过内存屏障指令(memory barrier instructions)来强制保证CPU的内存访问顺序,即严格按照软件程序定义的指令序列执行。
这些相关的内存屏障指令指令有如下几种:
① DMB(Data Memory Barrier)
DMB指令可以保证其身前的内存指令全部处理完成后,其身后的指令才可以开始处理;DMB对于非访存指令的执行顺序没有影响;
② DSB(Data Synchronization Barrier)
DSB架起了一道屏障,保证其身前所有的内存指令都执行完毕后,才会打开屏障,使得其身后的内存指令得以开始执行。在屏障打开之前,后续的所有内存指令都只能乖乖等待。此外,DSB指令不会更新xPSR的标志位。
③ ISB(Instruction Synchronization Barrier)
ISB指令是指令级别的同步指令,在其身前的内存指令全部处理完毕后,对于其后的已预取指令通通抛掉,直接清洗流水线,重新从cache或内存中取指。这也就,处理结果对于后续的指令来说,ISB之前的所有指令都已执行完毕,且结果也已经落到实处;
DMB 在双口 RAM 以及多核架构的操作中很有用。如果 RAM 的访问是带缓冲的,并且写完之后马上读,就必须让它“喘口气” ——用 DMB 指令来隔离,以保证缓冲中的数据已经落实到 RAM 中。
DSB 比 DMB 更保险(当然也是有执行代价的),它是宁可错杀也不漏网——清空了写缓冲,使得任何它后面的指令,不管要不要使用先前的存储器访问结果,通通等待访问完成。
同 DMB/DSB 相比, ISB 指令看起来似乎最强悍,对于高级底层技巧:“自我更新” (self-mofifying)代码,非常有用。举例来说,如果某个程序从下一条要执行的指令处更新了自己,但是先前的旧指令已经被预取到流水线中去了,此时就必须清洗流水线,把旧版本的指令洗出去,再预取新版本的指令。因此,必须在被更新代码段的前面使用 ISB,以保证旧的代码从流水线中被清洗出去,不再有机会执行。
5 总结
以上讨论的都是Cortex-M7默认的内存框架和内存访问行为,在功能安全的加注下,MPU的地位愈发重要,很多内容都还要和MPU结合起来一起考虑。不过,这都是后话了。