Linux内核读写屏障实现

45 阅读13分钟

一、前言

本节介绍Linux内核读写屏障的实现

二、动态指令替换之alternative

在运行时根据 CPU 的特性(如是否支持 SSE4.2、AVX 等)选择最优的指令实现,从而在不重新编译内核的情况下优化性能

#define alternative(oldinstr, newinstr, feature) \
    asm volatile ("661:\n\t" oldinstr "\n662:\n" \
                  ".section .altinstructions,"a"\n" \
                  "  .align 4\n" \
                  "  .long 661b\n"            /* label */ \
                  "  .long 663f\n"            /* new instruction */ \
                  "  .byte %c0\n"             /* feature bit */ \
                  "  .byte 662b-661b\n"       /* sourcelen */ \
                  "  .byte 664f-663f\n"       /* replacementlen */ \
                  ".previous\n" \
                  ".section .altinstr_replacement,"ax"\n" \
                  "663:\n\t" newinstr "\n664:\n" /* replacement */ \
                  ".previous" :: "i" (feature) : "memory")
​
  • oldinstr:默认的旧指令(兼容所有 CPU)
  • newinstr:优化的新指令(仅在某些 CPU 上可用)
  • feature:CPU 特性标志(如 X86_FEATURE_XMM 表示支持 SSE 指令集)
  • 动态替换:内核在启动时会检查 CPU 是否支持 feature,如果支持,则用 newinstr 替换 oldinstr;否则保留 oldinstr

1. alternative逐段解析

1.1.主代码段(默认执行 oldinstr

661:\n\t" oldinstr "\n662:\n
  • 定义标签 661662,中间是默认指令 oldinstr

  • 例如,如果oldinstr"lock; addl $0,0(%%esp)",则展开为:

    661:
        lock; addl $0,0(%esp)
    662:
    

1.2.altinstructions段(替换规则)

.section .altinstructions,"a"\n
    .align 4\n
    .long 661b\n"            /* 旧指令的标签(661) */
    .long 663f\n"            /* 新指令的标签(663) */
    .byte %c0\n"             /* CPU 特性标志(feature) */
    .byte 662b-661b\n"       /* 旧指令的长度 */
    .byte 664f-663f\n"       /* 新指令的长度 */
.previous\n
  • .section .altinstructions,"a"

    • 定义一个名为 .altinstructions 的特殊段("a" 表示可分配,allocatable
    • 内核启动时会解析此段,决定是否替换指令
  • .align 4:4 字节对齐

  • .long 661b:指向旧指令的标签(661

  • .long 663f:指向新指令的标签(663,在 .altinstr_replacement 段中定义)

  • .byte %c0:替换为第 1个操作数的 实际数值,CPU 特性标志(由 feature 参数传入,如 X86_FEATURE_XMM)。

  • .byte 662b-661b:旧指令的字节长度(662 - 661)。

  • .byte 664f-663f:新指令的字节长度(664 - 663)。

1.3..altinstr_replacement 段(新指令存储)

.section .altinstr_replacement,"ax"\n
663:\n\t" newinstr "\n664:\n" /* 新指令 */
.previous\n
  • .section .altinstr_replacement,"ax"

    • 定义一个可执行段("ax" 表示可分配 + 可执行),存储新指令
    • 内核启动时会从此段复制新指令到目标位置(如果 CPU 支持 feature
  • 663:664: :新指令的起始和结束标签。

  • .previous:切换为原来的代码段,如.text

1.4.输入和约束

:: "i" (feature) : "memory")

GCC 内联汇编 的约束部分,格式为:

asm volatile ("汇编代码" : 输出操作数 : 输入操作数 : 破坏描述);

上述汇编至.previous\n都是汇编代码,没有输出操作数,输入操作数即feature

  • "i" (feature) :将 feature 作为立即数传递给汇编代码
  • "memory" :告诉编译器此汇编会修改内存(防止优化)

2.动态替换机制

内核启动时:

  • 调用 apply_alternatives() 函数(位于 arch/i386/kernel/setup.c

  • 检查当前 CPU 是否支持 feature(通过 cpuid 指令)

  • 如果支持,则:

    • 执行指令替换并计算替换后剩余的字节数(需要用 NOP 填充)
    • 根据CPU架构填充对应NOP指令

3.实际示例

3.1.替换 mb() 的实现

#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
  • 旧指令lock; addl $0,0(%esp)(兼容所有 x86 CPU)

  • 新指令mfence(仅在支持 SSE 的 CPU 上使用)

  • 效果:

    • 如果 CPU 支持 SSE(X86_FEATURE_XMM2),则 mb() 会被替换为 mfence
    • 否则,继续使用 lock; addl $0,0(%esp)

4. 设计目的

  • 性能优化

    • 在支持新指令的 CPU 上使用更高效的实现(如 mfencelock; addl 更轻量)
  • 兼容性

    • 在旧 CPU 上回退到兼容但稍慢的指令
  • 动态适配

    • 无需编译多个内核版本,单个内核镜像即可适配不同 CPU

三、编译器内存屏障

__asm__ __volatile__("": : :"memory") 是 GCC 内联汇编中的一个 内存屏障(Memory Barrier) ,用于强制编译器在汇编指令前后重新加载内存中的变量,避免优化导致的数据不一致问题


1. 语法分解

// 格式:asm volatile ("汇编代码" : 输出操作数 : 输入操作数 : 破坏描述);
__asm__ __volatile__("" : : : "memory");
  • __asm__ :GCC 内联汇编的关键字,表示嵌入汇编代码
  • __volatile__ :告诉编译器不要优化这段汇编(原样保留)
  • "" :空的汇编指令模板(没有实际指令)
  • "memory"破坏性内存依赖,表示这段汇编会修改内存中的任意变量

2. 核心作用

防止编译器优化

  • 问题:编译器可能会重排、删除或缓存内存访问(如将变量值存入寄存器而不回写内存)

    "memory"破坏性依赖强制编译器:

    • 在汇编前:将所有缓存的变量值回写到内存
    • 在汇编后:重新从内存加载变量值(避免使用过期的寄存器值)

充当内存屏障(Memory Barrier)

  • 确保屏障前后的内存操作(读/写)不会跨屏障重排
  • 不保证 CPU 执行时的指令重排

3. 常见误区

误用为 CPU 内存屏障

// 错误:仅防止编译器优化,不保证 CPU 执行顺序!
__asm__ __volatile__("" : : : "memory");
x = 1;
y = 2;
  • CPU 仍可能重排 x = 1y = 2

过度使用

  • 在不需要内存屏障的地方使用 "memory" 会导致不必要的性能损失(编译器无法优化内存访问)

4. 正确用法示例

确保内存可见性

int *ptr = get_shared_pointer();
*ptr = 42;
__asm__ __volatile__("" : : : "memory"); // 强制写入内存

5. 总结

  • __asm__ __volatile__("" : : : "memory")的作用:

    • 禁止编译器优化:强制读写内存,避免缓存到寄存器。
    • 充当编译器屏障:防止指令重排(仅对编译器有效)
  • 不提供 CPU 级别的内存顺序保证,如需严格顺序需结合平台特定的屏障指令

四、CPU内存屏障

1.smp_read_barrier_depends()

#define read_barrier_depends()  do { } while(0)

#define smp_read_barrier_depends()      read_barrier_depends()

1.1. read_barrier_depends() 的核心作用

  • 功能:确保所有依赖先前读操作结果的后续读操作不会被 CPU 或编译器重排序

  • rmb() 的区别

    • rmb()(读内存屏障)会阻止所有读操作的重排序(无论是否数据依赖)
    • read_barrier_depends() 仅针对数据依赖的读操作,因此性能更高(在大多数 CPU 上是空操作或轻量级指令)

1.2. 关键点解析

数据依赖的读操作 后续读操作的值依赖于先前读操作的结果。例如:

int *p = ...;      // 读操作 1:获取指针 p
int val = *p;      // 读操作 2:依赖 p 的值(数据依赖)

CPU 不能将 val = *p 重排序到 p = ... 之前

非数据依赖的例子

int a = x;         // 读操作 1
int b = y;         // 读操作 2(与 x 无关)

这两个读操作可能被重排序(除非有显式屏障)

内存访问的顺序保证

  • 对本地 CPU 的保证:所有在read_barrier_depends()之前的依赖读操作必须在对该屏障之后的所有依赖读操作之前完成,但不保证对其他 CPU 缓存的可见性(不同于 rmb() 的强一致性)
  • 对编译器的保证:阻止编译器优化重排序数据依赖的读操作(例如,不能将 *p 的读取提到 p 的读取之前)

性能优势

  • 在大多数 CPU(如 x86)上,read_barrier_depends()是空操作do { } while(0)
  • 原因是x86 的强内存模型本身就保证数据依赖的顺序

1.3.注意事项

  • 不保证缓存一致性read_barrier_depends() 不强制其他 CPU 的缓存刷新(不同于 rmb()

  • 如果需要跨 CPU 的可见性,需结合其他同步机制(如 smp_rmb() 或锁)

  • 架构差异

    • 在 x86 上,read_barrier_depends() 通常是编译器的空指令(因为硬件已保证顺序)
    • 在 Alpha 等弱顺序 CPU 上,可能需要插入特殊指令(如 ld.acq

2.smp_mb()的实现

#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)

#define smp_mb()        mb()

定义一个全内存屏障(Full Memory Barrier),确保屏障之前的所有内存访问(读/写)在屏障之后的所有内存访问之前完成

2.1. 宏定义解析

2.1.1. mb()
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
  • 功能:定义一个全内存屏障(Full Memory Barrier),确保屏障之前的所有内存访问(读/写)在屏障之后的所有内存访问之前完成

  • alternative(): 这是 Linux 内核的一个机制,用于在编译时或运行时根据 CPU 特性选择不同的指令实现。其语法为:

    alternative(old_instr, new_instr, feature_bit)
    
    • 如果 CPU 支持 X86_FEATURE_XMM2(即支持 SSE2),则使用 mfence 指令
    • 否则,回退到 lock; addl $0,0(%%esp)
2.1.2.smp_mb()
#define smp_mb() mb()
  • 功能:在 SMP(对称多处理)环境下提供全内存屏障,直接调用 mb()。(在 UP(单核)环境下,smp_mb() 被定义barrier(),即__asm__ __volatile__("": : :"memory"),因为单核不需要生成相关CPU指令)

2.2. lock; addl $0,0(%%esp) 详解

指令分解

lock; addl $0,0(%%esp)
  • %%esp

    • %esp

      • esp 是 x86 的 32 位栈指针寄存器(Extended Stack Pointer),指向当前线程的栈顶
      • 寄存器名前必须加上 % 符号,用于将寄存器名与立即数或标签名区分开
    • %%esp

      • 为了与编译器的操作数占位符区分开,内联汇编中,所有寄存器名前必须使用两个百分号 %%
  • (%%esp)

    • 将寄存器用括号括起来表示间接寻址,不是操作 esp 寄存器本身的值,而是操作 esp 寄存器中所存储的地址所指向的内存位置的内容
  • 0(%%esp)

    • 0是偏移量,最终还是访问访问栈顶的内存内容
  • $0

    • $0指立即数0
  • addl $0,0(%%esp)

    • 给栈顶的内存地址指向的内容加0
  • lock 前缀

    • 使 addl 指令变成原子操作,并触发 CPU 缓存一致性协议(如 MESI)
    • 强制缓存同步lock 会使当前 CPU 缓存行失效或刷新,确保其他 CPU 核心能看到最新的内存值
    • 在 x86 中,lock 前缀会隐式触发一个全内存屏障(类似 mfence),阻止屏障前后的指令重排序
    • 所有在 lock 前的读写操作必须在 lock 指令前完成
    • 所有在 lock 后的读写操作必须在 lock 指令后执行

2.3.与 mfence 的对比

特性lock; addl $0,0(%%esp)mfence
指令类型原子算术指令 + lock 前缀专用内存屏障指令
性能较高开销(触发缓存同步)较低开销(仅屏障)
适用场景旧 CPU(无 SSE2)新 CPU(支持 SSE2)
副作用可能影响缓存无额外副作用

2.4. x86 内存模型背景

x86 的强内存顺序

x86 是强顺序(TSO, Total Store Order) 架构,默认保证:

  • 写操作严格顺序(不会乱序)
  • 读操作可能乱序(但 lock 前缀会阻止读乱序)
  • mfence/sfence/lfence 可显式控制顺序

为什么需要内存屏障?

即使 x86 是强顺序,以下情况仍需屏障:

  • 跨核缓存同步:确保其他核心能看到最新值
  • 指令重排序:防止编译器或 CPU 优化导致乱序

3.smp_rmb()的实现

#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)

#define smp_rmb()       rmb()

参考**smp_mb()的实现**一节的介绍

4.smp_wmb()的实现

#ifdef CONFIG_X86_OOSTORE
#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
#else
#define wmb()   __asm__ __volatile__ ("": : :"memory")
#endif

#define smp_wmb()       wmb()

参考**smp_mb()的实现**一节的介绍

5.set_mb()的实现

#define xchg(ptr,v) ((__typeof__(*(ptr)))__xchg((unsigned long)(v),(ptr),sizeof(*(ptr))))

struct __xchg_dummy { unsigned long a[100]; };
#define __xg(x) ((struct __xchg_dummy *)(x))

static inline unsigned long __xchg(unsigned long x, volatile void * ptr, int size)
{
        switch (size) {
                case 1: 
                        __asm__ __volatile__("xchgb %b0,%1" 
                                :"=q" (x)
                                :"m" (*__xg(ptr)), "0" (x) 
                                :"memory");
                        break;  
                case 2: 
                        __asm__ __volatile__("xchgw %w0,%1" 
                                :"=r" (x)
                                :"m" (*__xg(ptr)), "0" (x) 
                                :"memory");
                        break;  
                case 4: 
                        __asm__ __volatile__("xchgl %0,%1"
                                :"=r" (x)
                                :"m" (*__xg(ptr)), "0" (x) 
                                :"memory");
                        break;  
        }       
        return x;
}

#define set_mb(var, value) do { xchg(&var, value); } while (0)

5.1.宏xchg

#define xchg(ptr,v) ((__typeof__(*(ptr)))__xchg((unsigned long)(v),(ptr),sizeof(*(ptr))))

这行定义了一个宏 xchg,它是对下层函数 __xchg 的封装,目的是让接口更类型安全

  • xchg(ptr,v): 宏名,接受一个指针 ptr 和一个值 v

  • ((__typeof__(*(ptr))) ... ): 这是一个类型转换。__typeof__(*(ptr)) 会获取指针 ptr 所指向对象的原始类型(如 int, char 等)。整个宏最终会返回这个原始类型,而不是 __xchg 返回的 unsigned long。这确保了宏的类型安全

  • __xchg((unsigned long)(v), (ptr), sizeof(*(ptr))): 调用底层函数

    • (unsigned long)(v): 将值 v 强制转换为 unsigned long
    • (ptr): 直接传递指针
    • sizeof(*(ptr)): 计算指针所指向对象的大小(字节数)。这将决定在 __xchg 函数中使用哪个 case(1, 2, 或 4字节)

5.2.结构体定义:struct __xchg_dummy

struct __xchg_dummy { unsigned long a[100]; };
  • 作用:定义一个占位结构体,包含一个 unsigned long 数组(100 个元素)

  • 目的:

    • 用于类型转换(见 __xg(x) 宏),确保指针 ptr 被视为指向大型结构体的指针
    • 在 x86 中,xchg 指令对内存操作时,要求操作数的大小必须明确(如 32 位、64 位)。通过将指针转换为 struct __xchg_dummy*,可以强制编译器按特定对齐方式处理内存访问(尽管实际只使用第一个字节)
    • 历史原因:早期代码可能用此结构体避免编译器优化或对齐问题

5.3.宏定义:__xg(x)

#define __xg(x) ((struct __xchg_dummy *)(x))
  • 作用:将输入指针 x 强制转换为 struct __xchg_dummy* 类型

  • 目的:

    • 配合 xchgl 指令(32 位交换),确保编译器正确处理内存地址的对齐和大小
    • 实际不会访问整个结构体,仅利用其类型信息

5.2.底层函数:__xchg

static inline unsigned long __xchg(unsigned long x, volatile void * ptr, int size)
  • static inline: 建议编译器将函数内联展开,避免函数调用的开销

  • unsigned long __xchg: 函数返回 unsigned long 类型,即交换之前存储在 *ptr 中的旧值

  • unsigned long x: 要写入的新值

  • volatile void * ptr: 指向要操作内存地址的指针

    • volatile: 关键字。它告诉编译器,这个指针指向的内存是“易变的”,可能会被其他线程或硬件异步修改。禁止编译器对这个内存区域的访问进行任何优化(如缓存到寄存器、重排指令等),确保每次读写都直接与内存交互
  • int size: 要交换的数据的大小(1, 2, 4 字节)


5.2.1.内联汇编详解(以 case 4: 为例)
case 4:
        __asm__ __volatile__("xchgl %0,%1"
                :"=r" (x)          /* Output Operands */
                :"m" (*__xg(ptr)), "0" (x)  /* Input Operands */
                :"memory");         /* Clobbered List */
        break;

__asm__ __volatile__

  • __asm__: 引入内联汇编代码
  • __volatile__: 告诉编译器,不要对这段汇编指令进行优化(如移动位置、删除等),必须原样保留。对于内存操作,这通常是必须的

汇编指令字符串:"xchgl %0,%1"

  • xchgl: 这是x86的汇编指令,用于交换两个操作数的值。l 后缀表示操作 long (4字节)
  • %0, %1: 这是占位符,它们分别对应后面操作数约束列表中第0个和第1个操作数。编译器在生成最终汇编代码时,会用具体的寄存器或内存地址替换它们

输出操作数: :"=r" (x)

  • "=r",表示

    • =:写入(输出)
    • r:使用任意通用寄存器(如 eax, ebx 等)
  • (x):将 C 变量 x 绑定到此操作数

输入操作数: :"m" (*__xg(ptr)), "0" (x)

  • "m" (*__xg(ptr))

    • "m":约束,表示内存操作数(直接操作内存地址)
    • *__xg(ptr):将 ptr 转换为 struct __xchg_dummy* 后解引用
  • 作用:指定 xchg 的第一个操作数为 *ptr

  • "0" (x)

    • "0":约束,表示复用第 0 个操作数(即 %0)的寄存器
    • (x):绑定变量 x 的当前值
  • 作用:将 x 的值传入 xchg 的第二个操作数(寄存器),与 %0 共享同一寄存器。

破坏列表 (Clobber List): :"memory"

  • 确保内存一致性:强制编译器在执行这条汇编指令之前,将所有缓存在寄存器中的、属于“可写”内存地址的值刷新到内存中,以确保我们的 xchgl 操作能读到最新的值
  • 防止指令重排:防止编译器为了优化而将这条汇编指令之前或之后的任何内存访问指令重排跨越这个汇编块。它充当了一个编译器屏障
5.2.2.case 1case 2 的区别
  • case 1: (xchgb)

    • "=q" (x): q 约束表示限制编译器只能使用 a, b, c, d 这四个字节寄存器(eax, ebx, ecx, edx 的低8位),因为 xchgb 指令的操作数有这方面的硬件限制
    • %b0: b 修饰符指示使用寄存器 %0 的低8位部分(如 %al
  • case 2: (xchgw)

    • "=r" (x): r 约束,通用寄存器
    • %w0: w 修饰符指示使用寄存器 %0 的16位部分(如 %ax
  • case 4: (xchgl)

    • "=r" (x): r 约束,通用寄存器
    • %0: 无修饰符,使用完整的32位寄存器(如 %eax

5.3.总结

这个 __xchg 函数利用x86的 xchg 指令实现了原子性的交换操作。它是构建更高级同步设施(如自旋锁、信号量)的基石

6.mfence/sfence/lfence的实现

核心思想是控制存储缓冲区(Store Buffer)无效化队列(Invalidate Queue)

  • 存储缓冲区 (Store Buffer)

    • 是什么:每个CPU核心都有一个小的私有缓存,用于存放已执行但尚未写回L1缓存(因而尚未被其他核心看到)的存储(写)操作
    • 为什么存在:让CPU不必等待慢速的缓存一致性协议完成就能继续执行后续指令,极大提升性能
    • 导致的问题:一个核心可以先从自己的Store Buffer里读取刚写入的值,然后再将其写入缓存。这导致其他核心看到的写操作顺序可能与发出顺序不同。这是乱序的主要来源之一
  • 无效化队列 (Invalidate Queue)

    • 是什么:每个CPU缓存都有一个队列,用于存放其他核心发来的“无效化”(Invalidate)消息(例如,对方要写入一个共享缓存行,通知我方失效该行的副本)
    • 为什么存在:让CPU可以快速响应这些无效化消息(只需放入队列并立即回复ACK),而不必立即去处理复杂的缓存失效操作,从而减少延迟
    • 导致的问题:CPU可能会延迟处理无效化消息。这意味着它可能在一段时间内仍然读到旧的、已失效的缓存数据,从而观察到错误的内存顺序

mfence 的硬件实现(简化模型):

当CPU执行 mfence 时:

  • 核心停止发射新的内存操作。
  • 核心等待当前流水线中所有未完成的内存操作完成
  • 核心对它的存储缓冲区(Store Buffer)进行标记。所有在 mfence 之前的存储操作都被标记为“pre-mfence
  • 核心不允许任何在 mfence 之后的加载操作绕过这个屏障。它必须等待,直到它处理了所有来自其他核心的“无效化”消息(即清空或处理了它的无效化队列),以确保它之后发出的读操作能读到最新的值
  • 只有当Store Buffer中所有标记为“ pre-mfence ”的存储都已经被缓存系统接收(即已发出并收到其他核心的ACK确认),并且无效化队列已被处理,mfence 指令才完成
  • 之后的内存操作才可以继续

sfence 的硬件实现(简化模型) :

  • 给存储缓冲区 (Store Buffer) 做一个标记sfence之前的所有尚未提交的存储操作都归为“pre-sfence”组
  • CPU 的核心会继续执行后续的指令(包括 sfence 之后的加载操作,因为 sfence 不限制加载),但它会阻止任何 sfence 之后的存储操作被加入到存储缓冲区中
  • 核心会等待,直到存储缓冲区中所有“pre-sfence”组的存储操作都已经被处理完毕(即数据已被写入缓存,并且缓存一致性协议已经确保其他核心能够看到这些更新)
  • 屏障之后的存储操作现在被允许进入存储缓冲区并继续执行

lfence 的硬件实现(简化模型) :

  • CPU 执行到 lfence 时,会等待所有屏障之前发出的加载指令都真正获得最终数据。这包括那些已经发出但缓存未命中的加载(这些加载可能正在等待从其他缓存或内存返回数据)
  • 让流水线中 lfence 之后的所有新指令(不仅仅是加载指令)都暂停发射
  • 这意味着它确保了 lfence 之后的指令都能“看到” lfence 之前所有指令的执行结果。这是它与 sfence/mfence 的一个显著不同,后两者通常不序列化非内存操作
  • 由于 lfence 会序列化指令流,它有效地阻止了 CPU 对 lfence 之后的指令进行推测执行。这就是为什么 lfence 常被用于缓解基于推测执行的漏洞

五、例子参考

1.barrier()使用参考

参考博客 blog.csdn.net/weixin_5101… preempt_schedule() 的实现(kernel/sched.c

一节

2.smp_wmb()使用参考

参考博客 blog.csdn.net/weixin_5101… rcu_assign_pointer

一节