一、前言
本节介绍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
-
定义标签
661和662,中间是默认指令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)
- 如果 CPU 支持 SSE(
4. 设计目的
-
性能优化
- 在支持新指令的 CPU 上使用更高效的实现(如
mfence比lock; addl更轻量)
- 在支持新指令的 CPU 上使用更高效的实现(如
-
兼容性
- 在旧 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 = 1和y = 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)
- 在 x86 上,
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)
- 如果 CPU 支持
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-
%espesp是 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 1 和 case 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
一节