
【嵌入式liunx学习】内存屏障CPU 为了提升性能不会严格按照代码书写顺序执行内存读写会发生指令重排同时多核 CPU 有各自缓存缓存同步也会带来内存可见性问题。内存屏障 强制规定屏障前的内存访问操作必须全部完成之后才能执行屏障后面的内存访问操作。它阻止特定类型的内存指令越过屏障发生重排内存屏障本身不做数据读写只约束读写指令的顺序。文章目录【嵌入式liunx学习】内存屏障为什么会有指令重排四种内存屏障场景场景1——指令重排导致 bug单生产者单消费者场景2——StoreLoad 屏障场景 3读屏障 LoadLoadbarrier总结编程语言里面的对应实现C / Clinux内核注意点总结为什么会有指令重排A1;// 写AB2;// 写B在单核 CPU 上逻辑结果肯定 A 先赋值再 B。 但 CPU 硬件或者编译器觉得这两句没有依赖关系可以调换顺序执行变成B2;A1;重排不影响单线程最终结果但多线程下就会出诡异 bug重排分两类编译器重排编译阶段编译器优化打乱指令顺序CPU 硬件重排CPU 执行时乱序执行、缓存带来的内存顺序乱掉 内存屏障可以抑制 CPU 硬件重排部分屏障也会阻止编译器优化重排四种内存屏障类型作用通俗理解LoadLoad 屏障屏障前所有读必须完成才能执行屏障后的读读→读栅栏StoreStore 屏障屏障前所有写必须完成才能执行屏障后的写写→写栅栏LoadStore 屏障屏障前所有读必须完成才能执行屏障后的写读→写栅栏StoreLoad 屏障屏障前所有写必须完成才能执行屏障后的读写→读栅栏开销最大最常用写屏障 (Store Barrier) StoreStore保证屏障之前所有写操作刷新到缓存屏障后的写不会跑到前面。读屏障 (Load Barrier) LoadLoad保证屏障之后的读不会跑到屏障前面。全屏障 (Full Barrier) 同时包含 LoadLoad StoreStore LoadStore StoreLoad所有读写都不能穿越屏障例如 x86 的mfence、ARM 的dmb ishARM、RISC-V 弱内存模型四类重排都可能出现大量需要手动加内存屏障。场景场景1——指令重排导致 bug单生产者单消费者两个共享变量初始flag false; data 0;线程 A生产者data 100; // 写数据 flag true; // 标记数据准备好了线程 B消费者while(!flag); // 等待flag变成true printf(%d, data); // 读取data可能发生的重排CPU 可以调换线程 A 两条写指令顺序flag true; data 100;线程 B 看到flagtrue立刻进入打印此时data还没写完成读到data0出现错误 解决方案在线程 A 两个写之间加 StoreStore 写屏障data 100; store_barrier(); // StoreStore屏障前面所有写必须完成后面写不能跑到前面 flag true;屏障强制data100写完成才允许执行flagtrue不会颠倒。注意这里只约束写之间顺序是 StoreStore 屏障。场景2——StoreLoad 屏障初始x0,y0线程 Ax 1; // store x r1 y; // load y线程 By 1; // store y r2 x; // load x问有没有可能r10 r20原因StoreLoad 重排同一个线程内后面的读跑到前面的写之前完成。 线程 A先读 y再写 x线程 B先读 x再写 y。于是两个读到都是 0。✅ 修复在每个线程的写和读之间加StoreLoad 全内存屏障// 线程A x 1; full_barrier(); // StoreLoad屏障前面写必须完成后面读不能提前 r1 y; // 线程B y 1; full_barrier(); r2 x;加完屏障后就不会出现r10 r20。为什么 StoreLoad 开销最大因为它要求把前面所有写入操作对其他核可见才能执行后续读要等待缓存一致性同步。场景 3读屏障 LoadLoad共享变量ready false; msg线程 Amsg hello; store_barrier(); ready true;线程 Bwhile(!ready); load_barrier(); // LoadLoad屏障保证后面读取msg不能提前于ready读取 print(msg);如果没有 LoadLoad 屏障CPU 可能提前把msg读到寄存器while 循环才读到readytrue此时 msg 还是旧值。读屏障保证读到 readytrue 之后才去读取 msg。barrierbarrier()是什么Linux 里#define barrier() __asm__ __volatile__(: : :memory)这是一条空汇编作用只有一个告诉编译器不要把这个 asm 前后的内存访问指令互相调换顺序不要把内存变量一直缓存到寄存器里、跳过内存读写。单核唯一的风险点编译器优化打乱代码读写顺序。barrier()刚好解决编译器乱序所以单核下它够用。典型单核使用场景访问 IO 寄存器驱动里 in/out防止编译器把寄存器读写优化合并或者调换顺序。多核的问题不是本核单线程逻辑而是一个核写的数据另一个核看到的顺序。 这里的乱序来自CPU 硬件不是编译器。举个经典场景// CPU0data100;barrier();// 仅仅阻止编译器乱序flag1;// CPU1if(flag1){print(data);}即使加了barrier() 编译器会保证代码顺序一定是先写 data再写 flag。 但是CPU0 硬件有 Store Buffer CPU0 写data的时候Cache 行不在本地CPU 把写操作丢进 Store Buffer不等写入 Cache直接继续执行写flag。flag的 Cache 行命中很快写入缓存并对 CPU1 可见。于是出现CPU1 先看到 flag1但 data 还没提交到缓存读到旧 data。barrier()拦不住这个硬件行为。 必须换成smp_wmb()硬件写屏障会强制排空 Store Buffer保证屏障前所有写操作对其他核可见之后才执行屏障后的写。总结barrier()只是编译器屏障只能挡住编译器乱序管不了 CPU 硬件层面的访存重排。单核不存在多个 CPU没有 CPU 硬件跨核可见性乱序只需要阻止编译器乱序所以barrier()够用。多核除了编译器多个 CPU 之间有 Store Buffer/Invalidate Queue 带来的硬件访存乱序barrier()无能为力必须加上 smp_xxx () 硬件内存屏障。编程语言里面的对应实现C / CC11 原子操作std::memory_order本质就是封装内存屏障memory_order_relaxed无屏障只保证原子性不保证顺序memory_order_release写屏障StoreStore对应生产者memory_order_acquire读屏障LoadLoad对应消费者memory_order_acq_rel读写屏障memory_order_seq_cst全屏障最强默认linux内核smp_rmb(); // 读屏障 LoadLoad多核生效 smp_wmb(); // 写屏障 StoreStore多核生效 smp_mb(); // 全屏障 Full Barrier内核里大量用于无锁环形缓冲区、rcu 等无锁数据结构。注意点❌ 内存屏障 锁锁mutex包含内存屏障但内存屏障不是锁。内存屏障只保证顺序不保证互斥访问不能保护原子性。多个线程同时写同一个变量就算加屏障依然会数据竞争。❌volatile等于内存屏障C/C volatile阻止编译器优化把变量放寄存器阻止编译器重排但完全管不了 CPU 硬件指令重排。❌ 屏障可以保证所有缓存立刻刷新内存屏障不是强制立刻刷 Cache它是约束内存操作的可见顺序依靠 CPU 缓存一致性协议 (MESI) 工作。总结CPU 乱序执行 多核缓存会让多线程内存读写顺序和代码顺序不一致。内存屏障就是限制哪些读写指令不能互相越过用来实现无锁并发下的内存顺序保证。