mb与使用场景
它是 Linux 内核等领域的一个底层原语。
mb()
是 内存屏障 或更具体地说是 内存隔离屏障 的一种。它的使用场景深刻关系到现代计算机体系的乱序执行和多核并发。换句话说,如果不是编译器的优化,把c语句进行可能的调整,导致执行次序和代码前后关系不一致,也不需要这个patch式功能;比如c语句 a-b-c三行,三者没有逻辑依赖,只是先后次序,编译器可能会优化为a-c-b;
核心问题:为什么需要内存屏障?
现代 CPU 和编译器为了提升性能,会对指令执行顺序和内存访问顺序进行优化,这可能导致指令不按代码的先后顺序执行。主要在两个层面发生:
- 编译器优化:编译器在编译时,可能会将指令重新排序,以提高指令流水线的效率。
- CPU 乱序执行:CPU 在执行时,如果后续指令不依赖于前面指令的结果,它可能会让后续指令先执行。此外,由于多级缓存的存在,对不同内存地址的写入操作,在其他 CPU 核心看来,其全局可见的顺序可能与写入顺序不一致。
在单核、单线程程序中,这种乱序通常是透明且无害的,因为最终结果是一致的。但在多核并发环境(特别是内核中)下,这种乱序会导致严重问题。
mb()
函数的使用场景
mb()
(全称 smp_mb()
, 即 Symmetric Multi-Processor Memory Barrier)的作用是创建一个完整的 memory barrier。它确保:
- 屏障之前的所有内存访问操作(读和写),一定在屏障之后的所有内存访问操作之前完成。
- 这个“完成”意味着结果已经对系统中的所有 CPU 核心可见。
它就像一个栅栏,阻止屏障两侧的指令穿越它进行重排序
。
以下是几个经典的使用场景:
场景一:实现锁机制
锁是内存屏障最根本的应用场景之一。
static int locked = 0; // 0: unlocked, 1: lockedvoid acquire_lock(void) {while (test_and_set(&locked) == 1) { // 尝试原子性地获取锁/* 等待 */}mb(); // 内存屏障!确保在拿到锁之后,临界区的读写操作不会跑到拿锁之前去。
}void release_lock(void) {mb(); // 内存屏障!确保在释放锁之前,临界区内的所有读写操作都已经完成。locked = 0;
}
- 在
acquire_lock
中:mb()
确保一旦成功获取锁,后续进入临界区的代码不会因为乱序执行而被提前到获取锁之前,从而避免了数据竞争。 - 在
release_lock
中:mb()
确保在释放锁(让其他线程可见)之前,临界区内的所有修改都已经完成并全局可见。否则,另一个线程可能看到旧的、不一致的数据。
场景二:生产者-消费者模式(使用共享标志位)
这是最经典、最能说明问题的场景。
// 共享数据
struct data {int value;int ready; // 标志位:0-数据未就绪,1-数据已就绪
};// 生产者
void producer(struct data *data) {data->value = 42; // 1. 准备数据mb(); // 2. 内存屏障!确保 value=42 的写入一定在 ready=1 之前对消费者可见。data->ready = 1; // 3. 发布数据,标记为就绪
}// 消费者
void consumer(struct data *data) {while (data->ready == 0) { // A. 等待数据就绪/* 循环 */}mb(); // B. 内存屏障!确保在读取 value 之前,一定已经看到了 ready=1 这个结果。int used_value = data->value; // C. 使用数据
}
如果没有内存屏障会发生什么?
由于乱序执行,CPU 或编译器可能让生产者的 data->ready = 1
先于 data->value = 42
执行并变得全局可见。这时,消费者线程看到 ready
变为 1,便跳出循环去读取 value
,但此时 value
可能还是旧的(未初始化的)值,导致程序错误。
内存屏障的作用:
- 生产者的
mb()
保证了1 -> 2 -> 3
的顺序,消费者绝不会看到一个ready=1
但value
却不正确的状态。 - 消费者的
mb()
保证了A -> B -> C
的顺序,确保在读取value
之前,已经正确地感知到了ready
的变化。
场景三:与设备交互(DMA)
当内核与硬件设备通过 DMA 直接访问内存时,顺序至关重要。
// 1. 告诉设备要读取的数据在哪里(写入设备寄存器)
writel(DMA_SOURCE_ADDR_REG, buffer_phys_addr);
writel(DMA_SIZE_REG, size);mb(); // 内存屏障!确保上面两条配置信息一定先于下面的启动命令被设备看到。// 2. 启动 DMA 读取操作
writel(DMA_START_REG, 1);
如果没有屏障,设备可能会先收到 DMA_START
命令,然后才收到地址和大小,这会导致设备行为异常。
其他类型的内存屏障
Linux 内核提供了不同强度和作用范围的屏障,mb()
是其中最严格的。
- 写屏障
wmb()
:只确保写操作的顺序。屏障前的写操作一定在屏障后的写操作之前完成。 - 读屏障
rmb()
:只确保读操作的顺序。屏障前的读操作一定在屏障后的读操作之前完成。 - 编译器屏障
barrier()
:仅阻止编译器重排序,不生成任何 CPU 指令。适用于防止编译器优化但不需要干预 CPU 乱序的场景(例如,在循环忙等待中)。
总结
在任何一个需要明确指定内存操作顺序,否则并发逻辑就会出错的地方,就是你该考虑使用内存屏障(如 mb()
)的时候。它是编写正确、可靠的多核并发底层代码的基石。
mb()
内存屏障的使用场景可以归结为:
场景特征 | 具体例子 | 屏障的作用 |
---|---|---|
存在共享数据的多核/多线程环境 | 锁、生产者-消费者 | 确保修改顺序和可见性顺序的一致性,防止数据竞争和状态不一致。 |
对访问顺序有严格要求的硬件交互 | 设备驱动、DMA | 确保CPU对设备寄存器的写入顺序符合硬件的工作逻辑。 |
需要保证指令执行顺序 | 各种同步原语的实现 | 作为一个“栅栏”,强制在屏障点完成所有之前的操作,再开始之后的操作。 |