10、内存屏障(核心代码)
一、内存乱序场景
ARMv8指令集提供了三种内存屏障指令,按作用范围从弱到强排序为:DMB < DSB < ISB。
数据存储屏障(DMB)
- 确保前面的内存操作必须在后面的内存操作之前完成
- 允许处理器内部优化指令执行顺序
- 保证其他处理器能观察到正确的内存操作顺序
数据同步屏障(DSB)
- 强制所有之前的内存操作完全完成后再执行后续操作
- 禁止任何指令重排序
- 让所有处理器都能立即看到内存操作的结果
指令同步屏障(ISB)
- 确保之前的所有指令完全执行完毕
- 重置指令流水线
- 特别用于处理自修改代码或异常返回等场景
简单来说:DMB维护内存操作顺序,DSB强制全局内存同步,ISB确保指令执行顺序。三者按强度递增,ISB效果最强。
二、经典内存屏障接口函数
编译器屏障:
- barrier() 阻止编译器打乱代码顺序以优化性能,确保代码执行顺序与书写顺序一致。
内存屏障(通用):
- mb() 同时处理读和写的内存屏障,适用于多CPU系统(SMP)和单CPU系统(UP)。确保所有CPU可见一致的内存操作顺序。
读/写专用屏障:
- rmb() 仅处理读操作的内存屏障,保证读操作的顺序性,适用于SMP和UP系统。
- wmb() 仅处理写操作的内存屏障,保证写操作的顺序性,适用于SMP和UP系统。
多CPU专用屏障(SMP):
- smp_mb() 仅在多CPU系统(SMP)中起作用的内存屏障。在单CPU系统(UP)中,它退化为编译器屏障,仅确保汇编代码与C代码的内存操作顺序一致。
- smp_rmb() 多CPU系统的读屏障,类似rmb()但仅在SMP生效。
- smp_wmb() 多CPU系统的写屏障,类似wmb()但仅在SMP生效。
特殊场景屏障:
- smp_read_barrier_depends() 处理读操作依赖的屏障(例如,先读取指针再读取数据),确保依赖关系的正确性。
- smp_mb_before_atomic/smp_mb_after_atomic 在原子操作(如
atomic_t
)前后插入通用内存屏障,确保原子操作与前后内存操作的顺序一致。
关键区别总结
函数 | 适用场景 | 作用范围 | 在UP系统的行为 |
---|---|---|---|
barrier() | 编译器优化 | 阻止指令重排 | 同样有效 |
mb(), rmb(), wmb() | SMP和UP系统 | 内存顺序保证 | 完整生效 |
smp_*()系列 | 仅SMP系统 | CPU间内存同步 | 退化为编译器屏障(如barrier()) |
在ARM64 Linux内核中实现内存屏障函数的代码如下。
C
<arch/arm64/include/asm/barrier.h>
#define mb() dsb(sy)
#define rmb() dsb(ld)
#define wmb() dsb(st)
#define dma_rmb( dmb(oshld)
#define dma_wmb() dmb(oshst)
1
2
3
4
5
6
2
3
4
5
6
三、案例
在Linux内核中有很多使用内存屏障指令的例子,下面举两个例子。
例1: 在网卡驱动中发送数据包时,先把数据包内容写入内存缓冲区。之后,硬件DMA控制器会自动发送这些数据。此时需要使用wmb()
函数,它能确保DMA开始传输前,所有数据已经完整地写入缓冲区——避免DMA提前读取到未完成的数据。
C
<drivers\net\ethernet\realtek\8139too.c>
static netdev_tx_t rtl8139_start_xmit (struct sk_buff *skb,
struct net_device *dev)
{
skb_copy_and_csum_dev(skb, tp->tx_buf[entry]);
wmb();
RTL_W32_F (TxStatus0 + (entry * sizeof (u32)),
tp->tx_flag | max(len, (unsigned int)ETH_ZLEN));
...
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
例2:
在Linux系统中,当一个进程需要等待某个条件发生(比如等待数据准备好)时,它会通过类似wait_event()这样的函数进入"睡眠"状态暂停执行。这个过程需要用到内存屏障技术来保证数据同步。
具体来说:
- 当进程调用wait_event()时,会先检查等待条件是否满足
- 如果条件不满足,进程会暂停自己(进入睡眠)
- 内存屏障指令在这里的作用是:确保在进入睡眠前的所有操作都已完成,并且这些操作的结果对其他CPU是可见的
当发生唤醒事件时:
- 唤醒函数会先更新共享数据(比如标记数据已就绪)
- 内存屏障指令会强制保证:在发出唤醒信号前,所有相关数据更新必须完成并写入内存
- 这样其他CPU在被唤醒后检查条件时,就能看到最新的数据状态
简单来说,内存屏障就像交通信号灯,控制着数据操作的顺序,确保不同CPU之间的通信不会因为缓存或指令重排而出现混乱。这就像在说:"在继续下一步操作之前,请先完成并同步所有未完成的数据修改"。这样当进程被唤醒后,就能可靠地读取到最新的数据状态。