在现代计算机体系结构中,内存一致性模型决定了处理器对内存访问的顺序保证。ARM架构采用弱内存一致性模型(Weakly Ordered Memory Model),这种设计为处理器提供了更高的性能潜力,但也带来了复杂的内存同步挑战。
弱内存顺序模型的核心特征是允许处理器对内存操作进行重排序,只要这种重排序不会影响单线程程序的正确性。这种灵活性主要来自三个方面:
在单核环境下,这些优化对程序员完全透明。但在多核系统中,当核间通过共享内存通信时,这种重排序可能导致违反直觉的结果。例如:
assembly复制; 处理器P1
STR R5, [R1] ; 存储数据到地址R1
STR R0, [R2] ; 设置标志位到地址R2
; 处理器P2
WAIT([R2]==1) ; 等待标志位
LDR R5, [R1] ; 读取数据
在弱内存模型下,P2可能读到R1的旧值(0),尽管从程序顺序看P1是先写数据后写标志位。这是因为两个存储操作可能被乱序提交到内存系统。
ARMv7架构提供了两条关键的内存屏障指令:
DMB(Data Memory Barrier):
DSB(Data Synchronization Barrier):
典型使用模式:
assembly复制STR R0, [R1] ; 存储数据
DMB ; 确保存储完成
STR R1, [R2] ; 设置标志位
关键区别:DMB只保证顺序不保证完成时间,DSB则保证所有操作实际完成。在需要严格时序的场景(如中断触发前)必须使用DSB。
在多核系统中,正确同步通常遵循"发布-订阅"模式:
数据发布方:
数据订阅方:
这种模式确保数据的可见性顺序,是构建更高级同步原语的基础。
消息传递是多核通信的基础模式,正确实现需要深入理解屏障的放置位置。考虑以下典型错误实现:
assembly复制; P1: 发送消息
STR R5, [R1] ; 存储消息数据
STR R0, [R2] ; 设置消息就绪标志
; P2: 接收消息
WAIT([R2]==1) ; 等待标志
LDR R5, [R1] ; 读取数据
这种实现的问题是:由于弱内存顺序,P2可能先看到标志位更新后看到数据更新,导致读取到旧数据。正确实现需要双屏障:
assembly复制; P1: 发送消息
STR R5, [R1] ; 存储消息数据
DMB ST ; 仅需保证存储顺序
STR R0, [R2] ; 设置标志
; P2: 接收消息
WAIT([R2]==1)
DMB ; 确保标志读取先于数据读取
LDR R5, [R1]
ARM架构提供了一种优化屏障使用的特性:地址依赖。当后续内存操作的地址依赖于前一个加载操作的结果时,处理器会保证这两个操作的顺序:
assembly复制WAIT([R2]==1) ; 等待标志
AND R12, R12, #0 ; 清零临时寄存器
LDR R5, [R1, R12] ; 地址依赖加载
这种模式避免了接收方的DMB,减少了同步开销。但发送方仍需DMB保证存储顺序。
锁是多核同步的核心原语,ARM使用LDREX/STREX指令实现原子操作。一个完整的锁获取/释放序列如下:
assembly复制lock_acquire:
LDREX R5, [R1] ; 加载锁状态
CMP R5, #0 ; 检查是否可用
STREXEQ R5, R0, [R1]; 尝试获取锁
CMPEQ R5, #0 ; 检查STREX是否成功
BNE lock_acquire ; 失败则重试
DMB ; 获取屏障
; 临界区开始
关键点:
assembly复制; 临界区结束
MOV R0, #0 ; 准备解锁值
DMB ; 释放屏障
STR R0, [R1] ; 释放锁
释放锁时的DMB确保临界区内的所有操作在锁释放前完成。
ARMv7引入WFE(Wait For Event)和SEV(Send Event)指令支持低功耗同步:
assembly复制; 低功耗锁获取
lock_acquire_lp:
LDREX R5, [R1]
CMP R5, #0
WFENE ; 锁被持有时进入低功耗状态
STREXEQ R5, R0, [R1]
CMPEQ R5, #0
BNE lock_acquire_lp
DMB
; 锁释放
DMB
STR R0, [R1] ; 释放锁
DSB ; 确保存储完成
SEV ; 唤醒等待的核
关键改进:
ARM提供多种缓存维护指令,必须与内存屏障配合使用:
assembly复制; 使缓存行无效
DCIMVAC R1 ; 无效化R1地址对应的缓存行
DMB ; 确保无效化完成
; 清理缓存行到内存
DCCMVAC R1 ; 清理R1地址对应的缓存行
DMB ; 确保清理操作完成
当外部设备(DMA)访问缓存内存时,必须正确处理缓存一致性:
DMA写入前:
assembly复制DCIMVAC R1 ; 无效化缓存
DMB ; 确保无效化完成
STR R0, [R2]; 通知DMA开始
DMA写入后读取:
assembly复制WAIT([R3]==1) ; 等待DMA完成
DMB
DCIMVAC R1 ; 再次无效化,防止推测读取污染缓存
LDR R5, [R1] ; 读取DMA数据
修改可执行代码需要特殊处理:
assembly复制STR R11, [R1] ; 写入新指令
DCCMVAU R1 ; 清理数据缓存
DSB
ICIMVAU R1 ; 无效化指令缓存
BPIMVA R1 ; 无效化分支预测
DSB
ISB ; 同步指令流
BX R1 ; 跳转到新代码
关键步骤:
ARM屏障指令可以指定作用范围:
assembly复制DMB ISH ; 仅同步Inner Shareable域内的观察者
DMB NSH ; 仅同步非共享观察者
DMB SY ; 同步全系统(默认)
合理选择作用域可以减少同步开销。例如,仅需同步CPU集群时使用ISH而非SY。
根据场景选择最小必要屏障:
仅需存储顺序:DMB ST
assembly复制STR R0, [R1]
DMB ST ; 仅保证存储顺序
STR R0, [R2]
需要加载-存储顺序:DMB
assembly复制LDR R0, [R1]
DMB
STR R0, [R2]
需要完全同步:DSB
assembly复制STR R0, [R1]
DSB ; 确保存储完成
SEV ; 发送事件
修改页表时的标准流程:
assembly复制STR R11, [R1] ; 更新页表项
DSB
TLBIMVAIS R10 ; 无效化TLB项(广播)
BPIALLIS ; 无效化分支预测
DSB
ISB ; 同步本核指令流
注意事项:
缺失发布屏障:
assembly复制STR R0, [R1] ; 数据
// 缺少DMB
STR R0, [R2] ; 标志
可能导致其他核看到标志更新但数据未更新
缺失订阅屏障:
assembly复制WAIT([R2]==1)
// 缺少DMB
LDR R0, [R1]
可能导致读取到过期数据
错误屏障类型:
assembly复制STR R0, [R1]
DMB ; 应该用DSB
SEV ; 事件可能在存储前发出
在实际项目中,我曾遇到一个难以复现的多核数据竞争问题。通过系统性地添加诊断日志和逐步移除优化,最终发现是一个DMA操作缺少必要的缓存无效化屏障。这个经验教会我:在弱内存模型中,必须对每一处共享访问保持高度警惕,即使代码"看起来"顺序正确。