前言
内核中有很多特性开关(tracepoint、debug、feature flag),每次调用都要检查 if (enabled)。即使开关关闭,这个 if 也会产生 load + 分支预测的开销。static_branch_likely 通过 运行时代码热补丁 ,在开关关闭时直接把分支 nop 掉,实现 绝对零开销。
一、用法
1.1 定义 static key
c
#include <linux/jump_label.h>
// 定义一个 key,默认关闭(不跳转)
static DEFINE_STATIC_KEY_FALSE(my_feature_key);
// 定义一个 key,默认开启(跳转)
static DEFINE_STATIC_KEY_TRUE(my_feature_on_key);
命名约定:FALSE 表示默认走 fall-through,TRUE 表示默认走分支。
1.2 在代码中使用
c
// likely:期望条件为真(开关通常开着)
if (static_branch_likely(&my_feature_on_key)) {
// 开关开启时:这段代码存在
// 开关关闭时:整个分支被 nop 掉,零开销
do_feature_work();
}
// unlikely:期望条件为假(开关通常关着)
if (static_branch_unlikely(&my_feature_key)) {
// 开关关闭时:这段代码被 nop 掉
// 开关开启时:执行这段代码
trace_something();
}
1.3 运行时控制
c
// 启用(nop → jmp)
static_branch_enable(&my_feature_key);
// 禁用(jmp → nop)
static_branch_disable(&my_feature_key);
通常在 module_init、sysctl、debugfs 等地方触发切换。
1.4 完整示例
c
#include <linux/module.h>
#include <linux/jump_label.h>
// 定义一个 trace 开关,默认关闭
static DEFINE_STATIC_KEY_FALSE(verbose_trace_key);
static void my_trace_func(int val)
{
// 开关关闭时:整个 if 被 nop 掉,零开销
// 开关开启时:执行 pr_info
if (static_branch_unlikely(&verbose_trace_key))
pr_info("trace: val=%d\n", val);
}
// 通过 sysctl 控制开关
static int verbose_handler(const char *val, const struct kernel_param *kp)
{
int ret, enable;
ret = kstrtoint(val, 10, &enable);
if (ret)
return ret;
if (enable)
static_branch_enable(&verbose_trace_key);
else
static_branch_disable(&verbose_trace_key);
return 0;
}
static struct kernel_param_ops verbose_ops = {
.set = verbose_handler,
.get = param_get_int,
};
module_param_cb(verbose, &verbose_ops, &verbose_enabled, 0644);
1.5 典型使用场景
Tracepoint --- 内核中最经典的用例:
c
// 内核 tracepoint 定义
DEFINE_EVENT(sched_switch_event, sched_switch,
TP_PROTO(struct task_struct *prev, struct task_struct *next),
TP_ARGS(prev, next)
);
// 宏展开后大致等价于:
static DEFINE_STATIC_KEY_FALSE(__tracepoint_sched_switch_key);
// 每次调度切换都检查,但因为 static_branch:
// 没人 probe 这个 tracepoint 时 → 代码被 nop 掉 → 零开销
if (static_branch_unlikely(&__tracepoint_sched_switch_key)) {
// 执行 trace
__traceiter_sched_switch(data, prev, next);
}
内核有几千个 tracepoint,绝大部分时间没人 trace,static_branch 让它们在关闭时零开销。
Debug 开关:
c
static DEFINE_STATIC_KEY_FALSE(debug_locks_key);
void do_lock_operation(void)
{
if (static_branch_unlikely(&debug_locks_key)) {
// debug 模式:检查锁顺序、记录调用栈
lockdep_acquire(&lock);
}
// 正常路径:debug 检查被 nop 掉
raw_spin_lock(&lock);
}
二、与普通 bool 变量的对比
c
// 方案 A:普通 bool
static bool my_feature_enabled;
if (my_feature_enabled) {
do_expensive_work();
}
// 方案 B:static_branch
static DEFINE_STATIC_KEY_FALSE(my_feature_key);
if (static_branch_unlikely(&my_feature_key)) {
do_expensive_work();
}
| 维度 | 普通 bool | static_branch |
|---|---|---|
| 每次检查的开销 | load bool + 分支预测 | 零(指令已被 nop/patch) |
| 切换开销 | 只写一个 bool | text_poke 修改代码段(较重) |
| I-cache 影响 | 分支目标代码始终占用 | nop 路径可被优化掉 |
| 适用场景 | 频繁切换的标志 | 很少切换、但每次调用都检查的标志 |
| 指令空间 | 无额外开销 | nop 指令占少量空间 |
核心优势: 检查频率极高、切换频率极低的场景。
三、原理:运行时代码热补丁
3.1 普通 if 的汇编
c
if (static_branch_unlikely(&my_key)) {
do_work();
}
编译后(key 默认关闭时):
asm
; 普通 if --- 每次都要比较和分支
mov rax, [rip + my_key] ; load key 当前值
test rax, rax ; 比较
jnz .Ldo_work ; 分支跳转
; ... fall through ...
.Ldo_work:
call do_work
每次执行都要 load + test + branch,即使开关永远关着。
3.2 static_branch 编译后的汇编
asm
; static_branch --- nop 替代了整个分支
nop ; 原本是 jmp 指令,被 nop 掉
nop
nop
; ... fall through 正常路径 ...
call do_work
注意:不是用 if (bool) 实现的,而是直接把分支指令 替换为 nop。CPU 执行到 nop 时什么都不做,直接滑过,相当于这个 if 根本不存在。
3.3 启用时的热补丁
asm
; static_branch --- 启用后 nop 被改写为 jmp
jmp .Ldo_work ; nop 被 text_poke() 改写为 jmp
nop
nop
; ... fall through 正常路径 ...
.Ldo_work:
call do_work
3.4 热补丁的实现机制
┌──────────────────────────────────────────────┐
│ 静态 key 数据结构 │
│ │
│ struct static_key { │
│ atomic_t enabled; │
│ struct static_key_mod *next; // 链表 │
│ struct jump_entry *entries; // 代码位置│
│ }; │
└──────────────┬───────────────────────────────┘
│
│ static_branch_enable() 被调用
▼
┌──────────────────────────────────────────────┐
│ text_poke_bp_batch() │
│ │
│ 1. 将 nop 指令替换为 jmp 指令 │
│ 2. 用 int3(断点指令)作为中间过渡 │
│ 3. 等所有 CPU 离开被修改的代码区域 │
│ 4. 再替换为最终的 jmp │
└──────────────────────────────────────────────┘
启用/禁用过程:
c
// 简化后的核心逻辑
void static_branch_enable(struct static_key *key)
{
// 1. 原子地增加 key->enabled
atomic_inc(&key->enabled);
// 2. 遍历所有注册了这个 key 的代码位置
// 3. 用 text_poke() 把 nop 改写为 jmp
text_poke_bp(&entry, new_insn, ...);
}
3.5 为什么用 nop 而不是用 cmp + jcc
方案 A:普通 if(每次都有开销)
mov rax, [key]
test rax, rax
jnz target ; 每次执行 3 条指令
方案 B:static_branch(零开销)
nop ; 只有 1 条 nop,CPU 直接滑过
nop ; 甚至编译器可能优化掉 nop 占的空间
nop
方案 B 的 nop 会占用 I-cache 空间,但 不执行任何实际操作,不产生分支预测失败的代价。而方案 A 虽然分支预测命中率可能很高,但每次都要消耗 decode 带宽和分支预测器的容量。
四、底层细节
4.1 指令模板
在 ARM64 上:
asm
; static_branch_unlikely 编译后的模板
b 1f ; 4 字节无条件跳转(默认被 nop 替代)
nop ; 4 字节 NOP
; ... 代码 ...
1:
当 key 启用时,内核把 b 1f 的编码保留,把 nop 替换为跳转到实际代码的指令。实际上,初始状态是把跳转指令改写为 nop:
asm
; key 关闭时(初始状态)
nop ; 4 字节,替代了 b 1f
; key 开启时(热补丁后)
b target ; 4 字节,跳转到 target
在 x86 上类似,使用 jmp rel32 和 nop 的互换。
4.2 text_poke 的安全性
直接修改正在执行的代码是很危险的。内核使用了 bp(breakpoint) 过渡机制:
第一步:把 nop 改写为 int3(断点指令)
第二步:所有 CPU 通过 context tracking 离开该代码段
第三步:int3 改写为最终的 jmp 指令
这保证了不会有 CPU 正在执行被修改的那条指令。
4.3 多 CPU 同步
当一个 CPU 执行 static_branch_enable() 时,其他 CPU 可能正在执行该 nop 路径。text_poke_bp 会等待所有 CPU 经过修改点之后才完成替换。在替换完成前,代码路径是:
int3(触发异常)→ 异常处理程序 → 单步执行旧代码 → 完成替换
这保证了即使在热补丁过程中,也不会有 CPU 执行到半修改的指令。
五、static_key 的内部数据结构
c
struct static_key {
atomic_t enabled;
struct static_key_mod *next; // 修改链表(module 间共享)
};
struct static_key_mod {
struct static_key_mod *next;
struct jump_entry *entries; // 需要 patch 的代码位置
struct module *mod; // 所属模块
};
struct jump_entry {
s32 code; // 被 patch 的代码地址
s32 target; // 跳转目标地址
u32 key; // 对应的 static_key 地址
u32 opcode; // 指令编码
};
jump_entry 是核心:它记录了"哪个地址的哪条指令需要被 patch"。
六、总结
普通 if:
CPU 每次: load → compare → branch → 预测失败惩罚
static_branch:
CPU 每次: nop(或 jmp)→ 无任何计算开销
切换时: text_poke 热补丁(一次性开销)
一句话总结: static_branch_likely 把运行时的 if 判断,变成了运行时的代码段改写,用一次性的热补丁开销,换取每次调用的绝对零开销。适合 检查频率极高、切换频率极低 的场景。
内核代码参考:
include/linux/jump_label.h、kernel/jump_label.c