注:该文档是参考网上资料整理出来,以通俗的方式讲解,方便大家理解,如有不懂,可以私下讨论,共同进步。
1.屏蔽指令的介绍
1.1.屏蔽指令简介
DMB :全称 Data Memory Barrier,仅当所有在它前面的存储器访问都执行完毕后,才执行它后面的存储器访问动作(注意只对存储器访问敏感);
DSB:全称Data Synchronous Barrier,比DMB严格:仅当所有在它前面的存储器访问都执行完毕后,才执行它在后面的指令(亦即任何指令都要等待) ;
ISB:全称 Instruction Synchronous Barrier,最严格: 清洗流水线,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令。
DMB与DSB的区别:
DMB作用的范围比DSB小,DSB比DMB严格,DSB牺牲的性能比DMB大,DMB只对存储器访问的指令敏感,而DSB是对所有的指令都敏感,所以说它
宁可错杀也不漏网 ,对于新手来说更加保险,只有大虾才会使用DMB。
1.2.ARM官方文档介绍
数据内存屏障指令、数据同步屏障指令和指令同步屏障指令。
DMB{cond} {option}
DSB{cond} {option}
ISB{cond} {option}
cond
是一个可选的条件代码(请参阅条件执行)。
option
对提示操作的可选限制。
这些是提示指令。 是否实现这些指令是可选的。 只要其中任何一个指令未实现,则与 NOP 效果相同。
数据内存屏障可作为内存屏障使用。 它可确保会先检测到程序中位于 DMB 指令前的所有显式内存访问指令,然后再检测到程序中位于 DMB 指令后的显式内存访问指令。它不影响其他指令在处理器上的执行顺序。
option 的允许值为:
完整的系统DMB 操作。 这是缺省情况,可以省略。
数据同步屏障是一种特殊类型的内存屏障。 只有当此指令执行完毕后,才会执行程序中位于此指令后的指令。 当满足以下条件时,此指令才会完成:
位于此指令前的所有显式内存访问均完成。
位于此指令前的所有缓存、跳转预测和 TLB 维护操作全部完成。
允许的值为:
完整的系统 DSB 操作。 这是缺省情况,可以省略。
只可完成于统一点的DSB 操作。
存储完成后才可执行的DSB 操作。
UNST
只有当存储完成后才可执行的DSB 操作,并且只会完成于统一点。
指令同步屏障可刷新处理器中的管道,因此可确保在 ISB 指令完成后,才从高速缓存或内存中提取位于该指令后的其他所有指令。这可确保提取时间晚于 ISB 指令的指令能够检测到 ISB 指令执行前就已经执行的上下文更改操作的执行效果,例如更改ASID 或已完成的 TLB 维护操作,跳转预测维护操作以及对 CP15 寄存器所做的所有更改。
此外,ISB 指令可确保程序中位于其后的所有跳转指令总会被写入跳转预测逻辑,其写入上下文可确保 ISB 指令后的指令均可检测到这些跳转指令。这是指令流能够正确执行的前提条件。
option 的允许值为:
完整的系统DMB 操作。 这是缺省情况,可以省略。
这些 ARM 和 32 位 Thumb 指令可用于 ARMv7。
这些指令均无 16 位 Thumb 版本。
在《Cortex-M3权威指南》中对屏蔽指令的描述很具代表性,而且通俗易懂,故摘录如下。
Cortex-M3,这种内核支持许多屏蔽指令。在下一个指令或者事件开始执行以前,这些屏蔽指令常常用于确保必然事件执行完成。
在处理器内部,指令同步屏蔽(ISB)刷新流水线;以致于这个指令执行完以后,紧接着从缓存或者存储器取出指令表。这个表会改变这个系统,例如MPU立即生效。
数据同步屏蔽(DSB)指令作为一种特殊的存储器屏蔽指令。访问外部存储器操作完成之前,执行完成数据同步屏蔽指令(DSB)。执行数据同步屏蔽指令,在该指令没有完成以前不再执行任何指令——换句话说,执行完成所有的挂起操作。
数据存储屏蔽(DMB)指令作为一种存储器屏蔽指令。数据存储屏蔽(DMB)指令和数据同步屏蔽(DSB)指令有微小的差别。数据存储屏蔽(DMB)指令确保:数据同步屏蔽(DSB)指令执行完成之前,访问任意存储器;执行数据同步屏蔽(DSB)指令后,紧接着执行访问任意存储器操作。
如例8所示,典型MPU代码的一部分显示如何这些屏蔽指令。用嵌入汇编完成一个小的功能,每个功能都含有一条屏蔽指令。编译时链接器将连接这些功能。
例8用预处理指令使用屏蔽指令的范例
/*pseudo_intrinsics.c */
/*Small embedded assembly functions for barrier instructions*/
/*Link with armlink --inline ... */
__asmvoid __ISB(void)
__asmvoid __DSB(void)
/*scs.c - Initialize System Control Space registers */
voidSCS_init(void)
/*Code to configure the MPU regions inserted here
/*Enable the MPU */
SCS.MPU.Ctrl|= 1;
/*Force Memory Writes before continuing */
__DSB();
/*Flush and refill pipline with updated permissions */
__ISB();
CM3 中的另一股新鲜空气是一系列的隔离指令(亦可以译成“屏障”、“路障”,可互换使用——译者 注)。它们在一些结构比较复杂的存储器系统中是需要的(典型地用于流水线和写缓冲——译者注)。在 这类系统中,如果没有必要的隔离,会导致系统发生紊乱危象(race condition),(相当于数电中的“竞争 与冒险”——译者注).
举例来说,如果可以在运行时更改存储器的映射关系或者内存保护区的设置,(通过写 MPU 的寄存 器),就必须在更改之后立即补上一条 DSB 指令(数据同步指令)。因为对 MPU 的写操作很可能会被放 到一个写缓冲中。写缓冲是为了提高存储器的总体访问效率而设的,但它也有副作用,其中之一,就是 会导致写内存的指令被延迟几个周期执行,因此对存储器的设置不能即刻生效,这会导致紧临着的下一 条指令仍然使用旧的存储器设置——但程序员的本意显然是使用新的存储器设置。这种紊乱危象是后患 无穷的,常会破坏未知地址的数据,有时也会产生非法地址访问 fault。紊乱危象还有其它的表现形式, 后续章节会一一介绍。CM3 提供隔离指令族,就是要消灭这些紊乱危象(在有些讲解计算机体系体系结 构的书中,这类紊乱危象也被称为“存储器相关”——译注)。
CM3 中共有3 条隔离指令,如表4.27 所列
表4.27 隔离指令
数据存储器隔离。DMB 指令保证: 仅当所有在它前面的存储器访问操作
都执行完毕后,才提交(commit)在它后面的存储器访问操作。
数据同步隔离。比 DMB 严格: 仅当所有在它前面的存储器访问操作
都执行完毕后,才执行在它后面的指令(亦即任何指令都要等待存储器访 问操作——译者注)
指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执
行完毕之后,才执行它后面的指令。
DMB 在双口 RAM 以及多核架构的操作中很有用。如果 RAM 的访问是带缓冲的,并且写完之后
马上读,就必须让它“喘口气”——用 DMB 指令来隔离,以保证缓冲中的数据已经落实到 RAM 中。 DSB 比 DMB 更保险(当然也是有执行代价的),它是宁可错杀也不漏网——清空了写缓冲,使得任 何它后面的指令,不管要不要使用先前的存储器访问结果,通通等待访问完成。大虾们可以在有绝 对信心时使用 DMB,新手还是使用 DSB 比较保险。
同 DMB/DSB 相比,ISB 指令看起来似乎最强悍,但是却一身都是“愣劲”,不由分说就“动粗”。
不过它还有其它的用场——对于高级底层技巧:“自我更新”(self-mofifying)代码,非常有用。举例 来说,如果某个程序从下一条要执行的指令处更新了自己,但是先前的旧指令已经被预取到流水线 中去了,此时就必须清洗流水线,把旧版本的指令洗出去,再预取新版本的指令。因此,必须在被 更新代码段的前面使用 ISB,以保证旧的代码从流水线中被清洗出去,不再有机会执行(译者觉得 这种做法太工于技巧,有点“作秀”,现实编程中应该极少会用到,因此读者不必太钻它)。
2.屏蔽指令的运用场景
像ARM7TDMI这样经典的ARM处理器会按照程序的顺序来执行指令或访问数据。而最新的ARM处理器会对执行指令和访问数据的顺序进行优化。举个例子,ARM v6/v7的处理器会对以下指令顺序进行优化。
LDR r0, [r1] ; 从普通/可Cache的内存中读取,并导致cache未命中
STR r2, [r3] ; 写入普通/不可Cache的内存
假设第一条LDR指令导致Cache未命中,这样Cache就会填充行,这个动作一般会占用好几个时钟周期的时间。经典的ARM处理器(带Cache的),比如ARM926EJ-S会等待这个动作完成,再执行下一条STR指令。而ARM v6/v7处理器会识别出下一条指令(STR)并不需要等待第一条指令(LDR)完成(并不依赖于r0的值),于是就会先执行STR指令,而不是等待LDR指令完成。
在有些情况下,类似上面提到的这种推测读取或者乱序执行的处理器优化并不是我们所期望的,因为可能使程序不按我们的预期执行。在这种情况下,就有必要在需要严格的、“类经典ARM”行为的程序中插入内存隔离指令。ARM提供了3种内存隔离指令。简单起见,以下的描述都是在单处理器环境下。
l 数据同步隔离(DSB):等待所有在DSB指令之前的指令完成。
l 数据内存隔离(DMB):在DMB之后的显示的内存访问执行前,保证所有在DMB指令之前的内存访问完成。
l 指令同步隔离(ISB):清理(flush)流水线,使得所有ISB之后执行的指令都是从cache或内存中获得的(而不是流水线中的,也就是说等待流水线中所有指令执行完之后才执行ISB之后的指令,译注)。
需要注意,ARM v6中的CP15等价隔离指令在ARM v7中是弃用的。因此,可能的话,建议任何使用这些指令的代码应该改用以上3条新的隔离指令。
2.1.互斥量
以下情况软件必须使用DMB:
l 在请求资源期间,比如通过锁定一个互斥量或减少信号量以及任何形式的对资源的访问。
l 在使资源可用之前,比如通过解锁一个互斥量或增加信号量。
下面是一个阻塞互斥量的实现例子
l lock_mutex请求一个互斥量并阻塞直到请求到资源。如果阻塞了,在重试之前它会等待事件唤醒(通过WFE)。
l unlock_mutex释放一个互斥量并发送一个事件来通知等待的“进程”。
LOCKED EQU 1
UNLOCKED EQU 0
lock_mutex
; 互斥量是否锁定?
LDREX r1, [r0] ; 检查是否锁定
CMP r1, #LOCKED ; 和"locked"比较
WFEEQ ; 互斥量已经锁定,进入休眠
BEQ lock_mutex ; 被唤醒,重新检查互斥量是否锁定
; 尝试锁定互斥量
MOV r1, #LOCKED
STREX r2, r1, [r0] ; 尝试锁定
CMP r2, #0x0 ; 检查STR指令是否完成
BNE lock_mutex ; 如果失败,重试
DMB ; 进入被保护的资源前需要隔离,保证互斥量已经被更新
BX lr
unlock_mutex
DMB ; 保证资源的访问已经结束
MOV r1, #UNLOCKED ; 向锁定域写"unlocked"
STR r1, [r0]
DSB ; 保证在CPU唤醒前完成互斥量状态更新
SEV ; 像其他CPU发送事件,唤醒任何等待事件的CPU
BX lr
DSB指令保证了在发送事件之前,同步变量已经被更新了。
2.2.内存重映射
当复位服务程序或启动代码在flash(ROM)中,它们被映射在地址0x0处来保证程序能正确地从向量表中启动,通常它们驻留在内存的底部。
在系统初始化之后,你可能希望关闭flash的映射,这样就可以将底部的位置给RAM使用。下面的代码(永远在Flash中运行)在运行内存复制例程将一些数据复制到内存底部(RAM)前关闭了flash的映射。
MOV r0, #0
MOV r1, #REMAP_REG
STR r0, [r1] ; 关闭flash映射
DMB ; 保证STR完成
BL block_copy_routine() ; 复制代码到RAM
ISB ; 保证流水线清空
BL copied_routine() ; 运行复制后的代码 (RAM中)
如果没有STR和BL中间的DMB指令,就不能保证这条STR指令在复制代码到底部内存之前已经完成,因为复制例程可能会在通过STR写入的数据还在写缓冲(Write Buffer)中时就运行。DMB指令强迫所有DMB之前的数据访问完成。而ISB指令防止了在复制代码结束之前就从RAM中取指令。
2.3.中断
下面的框图表示了包含有基于通用中断控制器(GIC)的系统结构。当中断控制器检测到中断发生时,它会发送nIRQ信号给处理器。这会触发一系列事件包括处理器运行中断处理例程,并且屏蔽IRQ中断源(忽略接着到来的nIRQ的变化)。
下面的中断服务例程通过读取中断确认寄存器(IAR)来确认中断。例程不仅返回挂起中最高优先级的中断ID,还告诉中断控制器解除nIRQ的信号。在这之后,中断服务例程需要重新使能中断,以使更高等级的中断能抢占当前的中断。
interrupt_handler
; ...
LDR r0, =GIC_CPUIF_BASE ; 获得中断控制器基地址
LDR r1, [r0, #0x0c] ; 读取IAR,同时解除nIRQ信号
DSB ;确保内存访问结束,并且没有其他的指令运行
; 继续运行之前
CPSIE i ; 重新允许中断
; ...
RFE sp! ; 从服务程序返回
在这个过程中,正确的操作需要在CPSIE使能之前完成IAR的读取。因为这两条指令之间没有数据依赖,所以CPU会在LDR完成之前就执行CPSIE。这会导致处理器再次响应相同的中断(中断允许,且nIRQ没有解除,译注)。因此应该在这两条命令中插入一条DSB指令。
2.4.自修改代码
自修改代码必须在ISB之后运行,因为内核流水线中可能包含过期的指令。
下面的例子演示了一段将ROM中的代码搬运到RAM中,并跳转过去执行的代码。
Overlay_manager
; ...
BL block_copy ; 将新例程从ROM复制到RAM
B relocated_code ; 跳转到新例程
如果你使能了分支预测,并且像上面的例子一样重定位代码,处理器会预测到第二条分支指令即将执行,然后从其指示例程中取指。这就会导致处理器运行旧的例程。如果复制例程和跳转到新例程的分支指令离得很近,这样的问题就会发生。
为了确保这样的优化不要发生,你必须在新的重定位过的代码运行前插入一条ISB指令,以此来保证预取指缓冲在处理器重新取指前已经被清理:
Overlay_manager
; ...
BL block_copy ; 将新例程从ROM复制到RAM
ISB ; 保证流水线被清理
B relocated_code ; 跳转到新例程
如果你正在复制的内存被设置为了“写回型可cache的”,那么你应该清理(clean)cache以保证数据已经写入到了主存中。并且,指令cache应该被设置为无效,这样处理器就不会执行其他“被cache”的指令了。
Overlay_manager
; ...
BL block_copy ; 将新例程从ROM复制到RAM
DMB ; 保证内存访问结束
data_cache_clean ; 清理cache保证新例程已经被写入RAM
instruction_cache_invalidate ; 将指令cache设置为无效,这样旧指令就不会再被cache
DSB ; 在内存访问前,清理并使cache无效
ISB ; 保证流水线被清理
B relocated_code ; 跳转到新例程
2.5.类似的需要隔离的地方:
l JIT(Just-In-Time)编译器,比方说将Jazelle字节码转换为ARM代码。
l Post链接器或加载器,在运行时将代码重定位到内存中。
1.屏蔽指令的介绍1.1.屏蔽指令简介DMB :全称 Data Memory Barrier,仅当所有在它前面的存储器访问都执行完毕后,才执行它后面的存储器访问动作(注意只对存储器访问敏感);DSB:全称Data Synchronous Barrier,比DMB严格:仅当所有在它前面的存储器访问都执行完毕后,才执行它在后面的指令(亦即任何指令都要等待) ;ISB:全
内存屏障
指令
是系统编程中很重要的一部分,特别是在多核并行编程中。本章重点
介绍
内存屏障
指令
产生的原因、
ARM
64处理器内存屏障
指令
以及内存屏障的案例分析等内容。
18.2.1 使用内存屏障的场景
在大部分场景下,我们不用特意关注内存屏障的,特别是在单处理器系统里,虽然CPU内部支持乱序执行以及预测式的执行,但是总体来说,CPU会保证最终执行结果符合程序员的要求。在多核并发编程的场景下,程序员需要考虑是不是应该用内存屏障
指令
。下面是一些需要考虑使用内存屏障
指令
的典型场景。
在多个不同CPU内核之间共享数据