购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

6.2 存储器访问顺序

以前的Arm实现的架构按程序的顺序执行所有指令,在开始执行下一条指令前,完成当前执行的指令。

新的处理器进行了大量优化,这些优化与指令的执行顺序,以及访问存储器路有关。正如前面所看到的,CPU执行指令的速度比访问外部存储器的速度快得多。因此,高速缓存和写缓冲区被用来隐藏两者在速度方面的差异。高速缓存和写缓冲区潜在的影响在于对存储器的访问进行重新排序。CPU执行加载和保存指令的顺序不必和外设看到的顺序一致。

例如,对于代码清单6.4,执行顺序如下。

(1)访问1进入写缓冲区。

(2)访问2引起缓存查找(在缺失时)。

(3)访问3引起缓存查找(在命中时)。

(4)访问2返回的值引起缓存行的填充操作。

(5)执行访问1触发的存储器保存操作。

代码清单6.4 存储器访问顺序

第一条指令执行对外部存储器的写操作,在该例子中,写入写缓冲区中(访问1),其后面是两个读操作,一个在缓存中缺失(访问2),另一个在缓存中命中(访问3)。在访问1完成写缓冲区访问之前,可以完成所有的读访问。缓存中的缺失命中(Hit-Under-Miss)行为意味着在缓存中的加载命中(访问3)可以在前面的从缓存加载缺失之前完成。

因此,此时仍然可能造成假象,即硬件按照程序员编写代码的顺序执行指令。通常,只有在很少的情况下需要关注这个影响。例如,如果用户正在修改CP15寄存器、复制或修改存储器中的代码,那么必须让CPU等待这些操作完成。

对于超高性能的CPU,其支持预测数据访问、多发指令、缓存一致性协议和无序执行,用于实现额外的性能提高,甚至更有可能用于重新排序。通常,在单核系统中,重新排序对程序员的影响是不可见的,硬件会关心这些可能的问题。硬件保证遵守数据依赖性并确保读操作返回正确的值,允许前面的写操作引起潜在的修改。

然而,在多核系统中,当通过共享存储器进行通信(或以其他方式共享数据)时,考虑存储器访问顺序是非常重要的。程序员很可能关心正确的存储器访问顺序点,在这些点上,必须同步多个执行的线程。

使用Armv7-A架构的处理器采用了存储器弱顺序模型,这就意味着用于加载和保存操作的存储器访问顺序不需要与程序代码的顺序一致。模型可以对存储器读交易进行重新排序(如LDR、LDM和LDD指令),以及保存操作和某些指令。当读/写普通存储器时,硬件可以重排序,但受限于数据依赖性,以及准确的存储器屏障指令。在对顺序有更高要求的情况下,通过描述存储器的转换表入口的存储器类型属性与CPU进行通信。CPU的强制顺序规则对可能的硬件优化进行限制,因此降低了系统性能,并且提升了功耗。

Cortex-A9结构定义了一套存储器属性,用于支持系统存储器映射中所有的存储器和设备。下面的互斥主存储器属性描述了存储器区域:普通、设备和强顺序。对普通存储器来说,可以指定存储器是否可以共享,以及内部和外部的可缓存属性。对于A1和A2这两个访问,如果没有地址重叠,则在程序代码中,A1发生在A2之前,但是写可以无序发布,如表6.1所示。

表6.1 存储器类型访问顺序

6.2.1 普通、设备和强顺序存储器模型

1.普通存储器

所有的ROM和RAM设备都被看作普通存储器,处理器所要执行的代码必须在普通存储器中,不能在被标记为设备和强顺序的存储区域内。它的属性主要如下。

(1)处理器可以反复执行读和某些写访问操作。

(2)处理器可以预取或预测性地访问其他存储位置,如果MMU允许它访问预先允许的设置,则不会产生不利的结果。处理器执行预测的写操作。

(3)可以执行非对齐访问。

(4)处理器硬件可以将多个访问合并到一个更大的较少次数的访问中。例如,多个字节的写操作可以合并成单个双字的写操作。

普通存储器区域可以使用可缓存属性来描述。Arm架构支持将普通存储器的可缓存属性用于两级高速缓存(内部和外部缓存)。内部缓存是指最里面的缓存,总是包含CPU的第一级高速缓存。一个实现可能没有任何外部缓存,或者实现将外部可缓存属性应用到第二级或第三级高速缓存上。在包含Cortex-A9处理器的系统中,L2C-310第二级高速缓存控制器被认为是外部缓存。

2.设备和强顺序存储器

访问设备和强顺序存储器使用相同的存储器模型。例如,系统外设属于设备和强顺序存储器。它们的访问规则如下。

(1)保护访问的次数和大小。访问基于原子,不能被中途打断。

(2)所有的读/写访问对系统可能都有不利的影响。这些访问不会被缓存,并且不执行预测访问。

(3)不支持非对齐访问。

(4)根据程序中指令访问设备或强顺序存储器的顺序访问设备或存储器。这种访问顺序只应用于相同的外设或存储器块内。

对于设备和强顺序存储器的访问,它们的不同之处如下。

(1)对强顺序存储器的写操作只能在访问到达写操作需要访问的外设或存储器芯片时才能完成。

(2)对设备存储器的写操作允许在访问到达写操作需要访问的外设或存储器芯片之前就完成。

注:(1)系统外设基本上都映射为设备存储器。

(2)可以使用可共享属性描述设备存储器类型。

6.2.2 存储器属性

除存储器类型外,存储器属性也定义了存储器区域的访问顺序。

1.共享

共享域定义了总线拓扑内存储器访问的区域,这些访问保持一致性和潜在的一致性。在这个区域之外,主设备不能看到与该区域内相同的存储器访问顺序。存储器访问顺序发生在以下定义域内。

(1)非共享(NSH)。该域只由本地主设备构成。对其的访问不必与其他核、处理器或设备同步。在SMP系统内,不常使用NSH。

(2)内部共享(ISH)。多个主设备潜在共享该域,但不是系统内的所有主设备。一个系统可以有多个内部共享域。在一个系统中,对一个内部共享域的操作不影响其他内部共享域。

(3)外部共享(OSH)。该域基本上确定被多个主设备共享,很像由一些内部共享域构成。对一个外部共享域的操作会对该域内其他可内部共享的域产生影响。

(4)全系统(SY)。在全系统内进行的操作会影响系统内的所有主设备、所有非共享域、所有内部共享域和所有外部共享域。

该属性只能用于普通存储器和一个实现内的设备存储器(它不包含大的物理地址扩展LPAE)。在一个包含LPAE的实现中,设备存储器总是外部共享的。

2.可缓存

可缓存属性只用于普通存储器类型。可缓存属性提供了控制一致性机制,即主设备位于存储器共享区域外。普通存储器内的每个区域可分配的缓存属性包括写回可缓存、写通过可缓存和不可缓存。

此外,Cortex-A9处理器为两级缓存提供了单独的缓存属性,即内部可缓存和外部可缓存。内部可缓存是指最内侧的高速缓存,总是包含底层的缓存,即L1高速缓存。外部可缓存是指L2高速缓存。

6.2.3 存储器屏障

存储器屏障是一个指令,它要求在对存储器进行操作时对CPU使用顺序约束。这些对存储器进行的操作发生在程序中的存储器屏障前后。在其他架构中,这些操作也称为存储器围墙(Memory Fences)。

术语屏障又指编译器机制,当执行优化时,它阻止编译器调度数据访问指令穿越屏障。例如,在GCC内,程序员可以使用内联汇编器存储器“敲打”(Clobber),用于说明指令修改存储器,因此优化器不能跨越屏障对存储器的访问进行重新排序,语法如下:

前面提到,由于采用了很多CPU优化技术,使得程序执行的顺序与书写的顺序并不一致。通常,这对于程序员是不可见的。应用程序开发人员不用担心存储器屏障。然而,在有些情况下,必须关注这些顺序问题,如在设备驱动中,或者在多个主设备进行数据同步时。

Arm架构指定了存储器屏障指令,这样可以强制CPU等待完成存储器访问。在Arm/Thumb指令集中,用户模式和特权模式均支持这些指令。在以前的架构中,只在Arm指令集中可以操作CP15。

1.屏障指令

下面详细说明在单核系统中,屏障指令的实际影响。术语显式访问用于描述程序中加载和保存指令所产生的数据访问,并不包含取指。

1)数据同步屏障(Data Synchronization Barrier,DSB)

DSB指令强迫在执行任何其他指令集之前,CPU等待完成所有未完成的显式访问。它对预取指令没有影响。

2)数据存储器屏障(Data Memory Barrier,DMB)

DMB指令保证在系统中观察到屏障之前,按照程序顺序访问所有的存储器,即只有在它前面的存储器访问操作都执行完成后才执行它后面的指令。它不影响CPU中任何其他指令的执行顺序,或者指令加载的顺序。

3)指令同步屏障(Instruction Synchronization Barrier,ISB)

ISB指令将刷新CPU的流水线和预取缓冲区。这样,当执行完所有指令后,CPU将从高速缓存或存储器中加载ISB后的所有指令。这保证在ISB指令后,在ISB指令对任何已经加载的指令可见之前,执行修改上下文操作的结果(如CP15或ASID的变化),或者TLB或分支预测器操作。它本身不会引起数据和指令缓存之间的同步,但是要求它作为其中的一部分。

此外,DMB和DSB指令提供了一些选项,用于提供访问类型和可共享域。

(1)SY:默认选项,表示屏障用于整个系统,包括所有CPU与外设。

(2)ST:屏障只等待保存完成。

(3)ISH:屏障只应用于内部共享域。

(4)ISHST:对于ST和ISH的组合,它只对内部共享域执行。

(5)NSH:屏障只用于PoU。

(6)NSHST:屏障只等待保存完成,并且只会完成于PoU。

(7)OSH:屏障只用于外部共享域。

(8)OSHST:屏障只等待保存完成,并且只会完成于外部共享域。

在多核系统中,程序员必须使用DMB和DSB指令更通用的定义。在下面的描述中,使用字处理器(或代理),并不一定是CPU,也可指DSP、DMA控制器、硬件加速器或其他访问共享存储器的块。

在一个共享域内,DMB指令的效果是强制访问存储器的顺序。在执行DMB指令之前,保证共享域内的所有处理器都可以看到所有显式的存储器访问,即在执行DMB指令之后,以及在所有处理器观察到任何显式的存储器访问之前。

DSB和DMB指令有相同的效果,但是它额外地同步包含有全指令流的存储器访问,而不仅仅是其他存储器访问。这意味着,当发布DSB指令时,将停止执行程序,一直等到完成所有提交的显式的存储器访问,之后继续正常执行程序。

考虑一个四核Cortex-A9簇的例子。簇构成了一个内部共享域,当簇内的单核执行DMB指令时,在执行完屏障指令之前,该CPU确保按照程序顺序访问所有数据存储器,在执行屏障指令后,以及在任何显式的存储器访问按照程序顺序出现之前,簇内的所有CPU都能在屏障的一侧以与CPU执行它们相同的顺序看到访问。当使用DMB ISH时,对于外部观察者,并不保证相同的访问顺序,如DMA控制器或DSP。

2.屏障指令的例子

考虑以下的例子,有两个CPU(A和B),在核寄存器中保存了普通存储器内的两个地址Addr1和Addr2。每个CPU执行如代码清单6.5所示的两个指令。

代码清单6.5 存储器访问顺序例子

这里没有顺序要求,不能说明任何交易发生的顺序。地址Addr1和Addr2是独立的,不要求其中的一个CPU按程序书写的顺序执行加载和保存指令,或者考虑另一个CPU的活动情况。因此,这段代码有4种可能的结果。

(1)A得到旧的数据,B得到旧的数据。

(2)A得到旧的数据,B得到新的数据。

(3)A得到新的数据,B得到旧的数据。

(4)A得到新的数据,B得到新的数据。

如果想包含第3个CPU(C),就必须注意到没有要求每两个CPU看到的顺序是一样的。对A和B来说,在地址Addr1和Addr2中看到旧的数据是没问题的;但是对C来说,它会看到新的数据。

考虑下面的情况:B先查找由A设置的标志控制,然后读取存储器。例如,如果将消息从A传递到B,则可以看到类似的代码,如代码清单6.6所示。

代码清单6.6 使用邮箱的可能风险

这可能没有按照我们预期的目标实现。这里不存在任何理由不允许B在该[Flag]之前执行从[Msg]预测的读操作。这是很正常的,弱顺序和CPU并不知道两者之间可能的相关性。通过一个存储器屏障,程序员必须显式地强调依赖性。在这个例子中,程序员要求两个存储器屏障。A要求在两个保存操作之间有一个DMB指令,保证它们按照最初指定的顺序访问存储器。B要求在指令LDR R0,[Msg]之前有一个DMB指令,保证在设置标志之前不会读取消息。

3.屏障避免死锁

没有使用屏障指令而可以引起死锁的情况是处理器核写一个地址,并轮询一个外设给出的响应值,如代码清单6.7所示。

代码清单6.7 死锁

如果没有多处理器扩展,那么Armv7架构并不严格要求完成保存到[Addr]的操作(它可以在写缓冲区内,同时存储器系统忙于读取标志)。这样,所有核都将潜在地死锁,它们都在互相等待。在STR之后插入DSB,在读标志前,强迫核能观察到它的保存操作。

实现多处理器扩展的CPU要求在一个有限的时间内完成访问(必须清空写缓冲区),因此不要求屏障指令。

4.WFE和WFI与屏障交互

等待事件指令WFE和等待中断指令WFI可以停止CPU的运行并使之进入低功耗状态。为了保证在执行WFE和WFI之前完成所有的存储器访问操作,必须插入DSB指令。

在一个多核系统中,其他考虑与等待事件指令WFE和发送等待事件指令SEV的使用有关。这些指令能够降低自旋锁功耗,取代CPU重复轮询锁的方法。

当识别出一个中断或其他异步异常,或者其他核发送的一个事件时,唤醒CPU。在释放锁之后,有锁的核将使用SEV指令唤醒其他处于WFE状态的核。对于存储器屏障指令的目的,事件信号并没有被看作一个显式的存储器访问。因此,在执行SEV指令之前,必须考虑更新存储器,因为释放锁对其他处理器是可见的。这要求使用DSB指令,DMB指令是不充分的,因为它只影响存储器访问的顺序,并没有将它们同步为一条指令。然而,DSB指令将阻止SEV指令的执行,直到其他处理器可以看到前面的存储器访问。

5.Linux中的屏障用法

屏障被用于强制存储器的操作顺序,通常不需要理解或显式地使用存储器屏障。这是因为在操作系统的内核中已经包含了解锁和调度原语。然而,编写设备驱动程序的人员或理解操作系统内核的人员会觉得这些细节比较有用。

编译器和核架构的优化允许改变指令与相关的存储器操作,有时仍然需要强制指定存储器的操作顺序。例如,用户想写存储器映射的外设,这个写操作可能对系统有一些不利的影响。在程序中,先于这个写操作或之后的存储器操作,由于操作在存储器的不同位置,因此看上去好像能重新排序。在一些情况下,在写外设完成之前,必须保证所有操作都已完成;或者用户可能需要在任何其他存储器操作开始前确认已经完成外设操作。Linux提供相关函数用于实现这些功能。

(1)对于特殊的存储器操作,不允许指导编译器进行重新排序,而通过调用函数barrier()来实现,它只控制编译器代码的生成和优化,不影响硬件的重新排序。

(2)调用映射到Arm处理器指令的存储器屏障指令执行存储器屏障操作。它强迫一个特殊的硬件排序。下面给出可用的屏障(在Linux中支持Cortex-A SMP)。

①读存储器屏障函数rmb(),确保在执行出现在屏障后的任何读操作之前,已经完成出现在屏障前的任何读操作。

②写存储器屏障函数wmb(),确保在执行出现在屏障后的任何写操作之前,已经完成出现在屏障前的任何写操作。

③存储器屏障函数mb(),确保在执行出现在屏障后的任何访问之前,已经完成出现在屏障前的任何访问存储器的操作。

(3)这里有共享的SMP版本的屏障,即smp_mb()、smp_rmb()和smp_wmb()函数。它们用于在相同簇内多个处理器之间强迫普通可缓存存储器的访问顺序。当没有使用CONFIG_SMP编译它们时,这些调用扩展到barrier()描述。 7dcooR/rPfUgJw4W6VucVjJ0ioHgHXWaC+bvD13NSfYgCwAPKzDBbOe74AN00XVi

点击中间区域
呼出菜单
上一章
目录
下一章
×