一、内存屏障
在 Linux C 语言编程 中,内存屏障(Memory Barrier) 是一种用于控制内存访问顺序的技术。它主要用于多处理器系统中,确保某些操作按预期顺序执行,避免 CPU 和编译器对内存访问进行优化,从而影响程序的正确性。内存屏障的功能在多线程和并发编程中尤为重要。
什么是内存屏障?
内存屏障的障中文意思是保护和隔离的,也有阻止的意思,阻止的是CPU对变量的继续访问,停下来更新下变量,从而保护变量的一致性。 内存屏障是针对线程所有共享变量的,而原子操作仅针对当前原子变量。
内存屏障是一种指令,它的作用是禁止 CPU 重新排序特定的内存操作。它确保在屏障之前的所有读/写操作在屏障之后的操作之前完成。内存屏障一般被用来控制多处理器环境中的内存可见性问题,尤其是在进行原子操作、锁和同步时。
在多核处理器上,每个处理器都有自己的缓存,CPU 会将内存操作缓存到自己的本地缓存中。在不同的 CPU 之间,内存的可见性并非立刻同步,这就可能导致不同线程看到不同的内存值。通过内存屏障,可以确保特定的操作顺序,以避免此类问题。
内存屏障的类型
Linux C 中,内存屏障通常有以下几种类型,主要通过内核提供的原子操作或者内存屏障函数来实现。
-
全屏障(Full Barrier 或者 LFENCE、SFENCE):
- 作用:以下两种相加。
- 用途:确保所有的内存操作都在内存屏障前完成,通常用于同步和锁定操作。
- 内核函数:
mb()
(Memory Barrier)
-
读屏障(Read Barrier 或者 LFENCE):
- 作用:保证屏障之前的所有读操作在屏障之后的读操作之前完成。---》翻译过来的有歧义,难以理解,那个“完成”是缓存同步主内存的意思。
- 本质:作用是强制将 CPU核心 中的 L1/L2 缓存 中的共享变量值写回到 主内存。
- 用途:在执行并行读操作时确保读顺序。
- 内核函数:
rmb()
(Read Memory Barrier)
-
写屏障(Write Barrier 或者 SFENCE):
- 作用:保证屏障之前的所有写操作在屏障之后的写操作之前完成。--》翻译过来有歧义,难以理解,那个“完成”是主内存同步到缓存的意思。
- 本质:作用是强制使数据从主内存加载,而不是直接使用可能已经过时的缓存数据。
- 用途:用于确保写操作顺序。
- 内核函数:
wmb()
(Write Memory Barrier)
-
无序屏障(No-op Barrier):
- 作用:没有实际影响,仅确保 CPU 不会重排序特定的指令。
- 用途:常用于确保指令的顺序性而不做其他强制性的内存同步。
读写屏障的作用域
- 读写屏障的作用域并不局限于当前函数或者某个函数调用的局部作用域,而是影响整个 当前线程的内存访问顺序。也就是说,只要在当前线程中,任何在屏障前后的内存操作都会受到屏障影响,而不管这些操作发生在同一个函数里还是不同的函数中。
线程之间的隔离
- 读写屏障是 线程级别的,因此它们只影响执行这些屏障操作的线程。也就是说,如果线程 1 执行了写屏障,它只会影响线程 1 后续的内存操作,而不会直接影响其他线程。---》翻译过来的,其实就是这个线程的读写屏障只会引发自己线程变量与主内存的同步,管不到其他线程的同步。但是写屏障触发后 会 通知其他线程,如果有现代 CPU 使用缓存一致性协议(如 MESI)的话,其他线程会把主内存中的最新值更新到自己缓存中。
- 读屏障不会触发其他线程去把自己的缓存同步到主内存中。
- 如果想让多个线程之间的共享变量同步并保持一致性,通常需要在多线程间使用某些同步机制(如锁、原子操作等),而不仅仅是依赖于单个线程的屏障。
具体来说:
-
写屏障(Write Barrier):会影响所有在屏障之前执行的写操作,无论这些写操作发生在当前函数内还是其他函数中。它确保屏障前的所有写操作都能同步到主内存,任何与此线程共享的缓存都能看到这些值。
-
读屏障(Read Barrier):会影响所有在屏障之后执行的读操作,确保这些读操作从主内存读取最新的值,而不是从 CPU 核心的缓存中读取过时的值。读屏障会影响当前线程的所有后续读取操作,无论这些读取发生在哪个函数中。
内存屏障的使用
在 Linux 中,内存屏障主要通过一组原子操作宏来提供。这些操作用于确保不同 CPU 或线程之间的内存同步。常见的内存屏障宏包括:
mb()
:全屏障,防止 CPU 重排序所有内存操作。rmb()
:读屏障,确保屏障之前的所有读操作完成。wmb()
:写屏障,确保屏障之前的所有写操作完成。
示例代码
#include <stdio.h>
#include <stdint.h>
#define wmb() __asm__ __volatile__("sfence" ::: "memory") // 写屏障
#define rmb() __asm__ __volatile__("lfence" ::: "memory") // 读屏障
#define mb() __asm__ __volatile__("mfence" ::: "memory") // 全屏障
void example_memory_barrier() {
int shared_variable = 0;
// 写入数据
shared_variable = 42;
// 在这里使用写屏障,确保共享变量的写操作
// 在执行屏障之后才会完成
wmb();
// 读取共享数据
printf("Shared Variable: %d\n", shared_variable);
// 使用读屏障,确保屏障前的所有读取操作完成
rmb();
// 这里是确保顺序执行的一部分
printf("Shared Variable read again: %d\n", shared_variable);
}
int main() {
example_memory_barrier();
return 0;
}
为什么需要内存屏障?
-
避免重排序:编译器和 CPU 会对内存访问进行优化,尤其是在多处理器系统中,这可能导致指令执行顺序与预期不一致,进而导致错误的程序行为。
-
保证内存一致性:当一个线程或 CPU 修改共享变量时,其他线程或 CPU 可能会看到不同的内存值,内存屏障可以保证修改操作在其他线程中是可见的。
-
同步操作:在多线程或多处理器环境中,内存屏障确保执行顺序和同步的正确性,尤其是在没有锁或原子操作的情况下。
缓存一致性协议(例如 MESI)
为了保证多核处理器之间缓存的数据一致性,现代 CPU 会使用 缓存一致性协议(如 MESI 协议,即 Modified、Exclusive、Shared、Invalid)。这个协议的作用是确保一个核心的缓存修改在其他核心的缓存中得到更新,避免出现“脏数据”。
但即便如此,MESI 协议的具体实现仍然依赖于硬件,缓存之间的同步可能不会在每一次内存访问时都发生。尤其是在没有任何同步机制的情况下,一个线程修改的值可能会暂时不被另一个线程看到,直到某些缓存刷新或同步操作发生。
Linux 内核中的内存屏障
在 Linux 内核中,内存屏障主要是通过原子操作来实现的。例如,atomic_set
、atomic_add
等原子操作通常会隐式地使用内存屏障来保证内存操作顺序。而直接的内存屏障通常通过 mb()
、wmb()
和 rmb()
函数来实现。
总结
内存屏障在多核处理器和并发程序中非常重要,用于控制内存操作顺序,避免由于硬件优化或编译器优化引起的内存同步问题。Linux 提供了多种类型的内存屏障函数,程序员可以根据需要使用它们来确保内存操作的顺序性。
二、变量存贮与内存屏障
你提到的问题涉及 程序执行过程中变量的存储位置、内存可见性和线程切换 的多个方面。为了更清晰地解释,我们需要从操作系统的内存管理和多线程模型入手。
1. 程序执行前变量存在哪里?
在程序执行之前,变量的存储位置主要依赖于变量的类型和生命周期。变量可以存储在以下几个区域:
- 栈区(Stack):局部变量通常会被分配到栈中。栈是线程私有的,每个线程都有一个独立的栈空间。