cpp-内存屏障-MESI
参考博客
- ✨ memory-barriers in Linux
- memory ordering at compile time
- Memory Barriers Are Like Source Control Operations
- # C++多线程序列——理解 memory barrier
- 跟我学C++中级篇——内存屏障内存栅栏和编译器屏障以及相关
CPU内存屏障
在CPU运行时防止指令乱序执行;另外一个功能是保证数据的可见性,即:每一次值的改动,都可以保证被所有相关者看到。这种指令一般都涉及到了==机器指令==,对上层开发者来说,就是汇编指令
- 在==多核程序上==需要关注
使用
调用具有memory barrier效果的指令可以强制要求CPU产生正确的内存存取顺序,相关的指令有(不限于下面列出的这些指令):
- GCC 中的某些内联汇编指令
- Any Win32 Interlocked operation, except on Xbox 360
- CPP的原子类型相关操作,例如:
load(std::memory_order_acquire) - POSIX锁操作,例如
pthread_mutex_lock - 需要注意的是,以上这些操作产生的
memory barrier可能是不同类型的
类型
- LoadLoad
- StoreStore
- LoadStore
- StoreLoad
编译器屏障
对编译器的一种约束,让编译器按要求编译。比如在gcc中有一个定义:#define barrier() __asm__ __volatile__("": : :"memory")
- 编写==单核程序时也要==注意
Linux内核提供的内存屏障
- 对于任何一个类型的内存屏障,用户可以信赖的其最小功能
- 实现任一种类型的内存屏障,CPU需要提供的最小功能集
- 如何使用内核提供的内存屏障
CPU应提供的最低保障
- 有依赖的内存访问会被按序执行
Q = READ_ONCE(P); D = READ_ONCE(*Q);对应的CPU实际指令执行顺序应该为:Q = LOAD P, D = LOAD *Q;
- 在某些架构的实现中
READ_ONCE也会加入内存屏障,因此实际上CPU执行的指令顺序为:Q = LOAD P, MEMORY_BARRIER, D = LOAD *Q, MEMORY_BARRIER
- 有重叠操作数的load/store,也会按序执行
a = READ_ONCE(*X); WRITE_ONCE(*X, b);对应的CPU执行顺序为:a = LOAD *X, STORE *X = b
内存屏障类型
显式的
- Write (or Store): 屏障前的指令在屏障后指令之前执行;从内存管理器的角度来看:屏障之前的指令先于屏障之后的指令发往内存控制器
- ✨写屏障通常与地址依赖屏障/读屏障配对使用
- 地址依赖屏障(Address-dependency barriers)(weak版的读屏障)
- 5.9之后的内核移除了显式的该屏障API;但是
APIs for marking loads from shared variables such as
READ_ONCE()andrcu_dereference()provide implicit address-dependency barriers.
- 5.9之后的内核移除了显式的该屏障API;但是
- Read (or Load): 加强版的地址依赖屏障,同样不会影响Write指令
- ✨读屏障通常与写屏障配对使用
- 通用屏障:保证在其他部件(内存/设备)看来,屏障前的LOAD/Write指令先于屏障后的Load?Writer指令到达
隐式的
ACQUIRE: 保证在其它部件看来,屏障后面的指令在屏障之后到达;但是屏障之前的指令有可能出现在屏障后。- 通常与release操作成对出现
- 相关接口包括:
- LOCK
smp_load_acquire()andsmp_cond_load_acquire()
RELEASE:保证屏障之前的操作在屏障之前发出;但是后面的操作也可能在屏障之前进行
Address-dependency barriers
There is no guarantee that a CPU will see the correct order of effectsfrom a second CPU’s accesses, even if the second CPU uses a memory
barrier, unless the first CPU also uses a matching memory barrier (see
the subsection on “SMP Barrier Pairing”).
✨这部分内容需要好好看一下!!!
Linux开发时必须/禁止的假设
- 对于不受
READ_ONCE()和WRITE_ONCE()保护的内存引用,不能假定编译器会按你的意愿行事 - 不能认为==独立的==加载和存储将按给出的顺序发出(emit)
什么时候需要使用内存屏障
不相关的内存存取操作可能以任意顺序被CPU执行,这对CPU-CPU,CPU-IO来说可能会产生问题;因此在CPU-CPU,IO的场景下,某些关键内存访问顺序需要保证按序执行【
举例PHY先写地址,后读写数据】。因此需要给程序员提供一些手段来干预引导==编译器==和==CPU==按序执行指令
它们对屏障两侧的内存操作施加了可感知的部分排序。内存屏障用于覆盖或抑制这些技巧【为了性能进行的指令重排、合并等优化手段】,使代码能够合理地控制多个 CPU 和/或设备的交互。
只有指令操作的内存涉及多个CPU之间,或者与设备之间进行,才需要考虑使用内存屏障
应用开发使用到的粗粒度多核同步技术:自旋锁等,内部实现肯定用到了内存屏障