前言
在内核代码中,许多条件判断在正常情况下的结果几乎是固定的,只有在极少数特殊场景下才会发生变化。单个判断的开销通常可以忽略不计,但如果此类判断数量庞大且被高频执行,就可能带来显著的性能损耗。
为此,内核提供了 likely() 和 unlikely() 这两个宏,用于帮助编译器和 CPU 进行分支预测。从字面上就可以看出,它们用于标注条件的结果更可能为真(likely)或更可能为假(unlikely)。编译器会根据这些提示优化生成的指令,使 CPU 的分支预测更倾向于正确的路径。
如果预测正确,分支跳转几乎没有额外开销;但若预测失败,处理器流水线需要被清空并重新取指,这会带来数个周期的损失。因此,只要预测在大多数情况下是正确的,就能获得显著的性能收益。
需要注意的是,likely/unlikely 只是程序员提供给编译器的提示,它们并不能完全避免分支预测失败。如果使用不当(例如将实际很少发生的情况标记为 likely),反而可能导致性能下降。
likely/unlikely 宏
作用
-
likely(expr)
:表示expr
的结果在大多数情况下为真。 -
unlikely(expr)
:表示expr
的结果在大多数情况下为假。
编译器会根据这些提示调整生成的指令布局,从而提高 CPU 分支预测的命中率。
性能影响
-
预测正确:跳转几乎没有额外开销。
-
预测失败:处理器流水线需要清空并重新取指,可能造成多个周期的性能损失。
-
总体效果:只要提示与实际运行情况大致相符,就能带来性能提升;如果标记错误,反而可能降低性能。
使用前后对比
假设有如下代码:
if (x > 0) {
fast_path();
} else {
slow_path();
}
编译器会尽量保持中立,不偏向任何一方,分支预测可能不够理想。
如果我们明确知道 x > 0
在绝大多数情况下成立,就可以加上 likely()
:
if (likely(x > 0)) {
fast_path();
} else {
slow_path();
}
这样,编译器会调整指令布局,把 fast_path()
的路径安排为“无条件顺序执行”,而把 slow_path()
分支编译成“跳转分支”。
换句话说:
-
未使用 likely/unlikely → 编译器无法预知哪个分支更常用,可能生成中立的跳转。
-
使用 likely/unlikely → 编译器将常见路径(
likely
部分)直接排在主流水线,提高命中率,减少跳转开销。
汇编层面的差别(简化示意)
未使用优化:
cmp $0, %eax
jle .Lslow_path # 小于等于 0 时跳转
call fast_path
jmp .Lend
.Lslow_path:
call slow_path
.Lend:
使用 likely()
后,编译器会倾向于把 常见路径放在直线执行路径:
cmp $0, %eax
jle .Lslow_path # 极少发生的情况
call fast_path # 直接顺序执行(主路径)
jmp .Lend
.Lslow_path:
call slow_path
.Lend:
这样,CPU 在流水线预测时,更可能正确命中常见的执行路径,减少“清空流水线”的情况。
小结
-
likely/unlikely
本质上就是 帮编译器安排分支布局,提高 CPU 分支预测的成功率。 -
在 高频执行、分支明显倾斜 的场景下(如内核路径判断、错误检查),能带来实际性能收益。
-
但 错误使用(标记方向和实际情况不符)会适得其反。
尽管我们已经加上unlikely修饰来进行优化,但是,读取 condition 仍然要访问内存,仍然需要用到cache;另外,也会CPU分支预测失败。虽然少数这样的代码影响不大,但当这样的条件判断代码(如内核中大量的tracepoint)增多的时候,将对cache会造成很大压力,所有这些代码导致的cache miss,以及CPU分支预测失败,所造成的性能损失,就变得可观起来。因此,内核需要一种方案来取消分支预测,来解决这样的问题。
如果某个判断分支在大多数情况下都是只走一个特定路径,除了加上likely和unlikely告诉编译器进行优化,是否还有其他方法?
gcc(v4.5)增加了一个新的”asm goto”语句,允许在内联汇编语句中跳转到C代码label(也就是内联汇编中可以得到C代码label的地址):
https://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html
通过使用”asm goto”,可以在不需要检查内存的情况下,创建出新的分支,无论默认状态下这个分支有没有被启用。之后在运行时,可以为分支位置打补丁修改分支方向。
内核开发者就开发了这样一种新的方法:通过动态替换内存中的代码段,去掉分支的判断条件,让代码根据动态设置要么直接执行a分支,要么直接执行b分支。
这种技术在底层是通过将汇编中的nop指令替换成jmp,或者将jmp指令替换成nop实现的。具体的实现和体系相关。
去掉分支的判断条件,取消分支预测的方案就是本章的主题:static keys + jump label
虽然修改分支方向成本很高,但是分支选择操作几乎没有损失。这就是static key这个优化的代价和优点。
这里使用的底层补丁机制被称为”jump label patching”,它为static keys功能提供了基础。
什么是Static Keys?
在Linux内核开发中,Static Keys(也称为静态键)是一种优化机制,用于在运行时高效地启用或禁用某些代码路径。它的目标是通过减少分支预测失败的开销,从而提升内核的整体性能。
Static Keys 的主要原理是基于条件跳转的动态替换。当某项功能被启用时,Static Keys 将对相关代码进行打补丁(patching),以实现高效的执行路径。
这种机制通常用于内核中的热路径(hot path),比如调试功能、性能计数器和特定功能的动态启用/禁用。
-
作用:彻底消除分支预测问题,通过 运行时动态修改指令 来控制分支执行
-
实现方式
-
使用 跳转标签(jump label) 技术,在内核中通过打补丁修改指令,把条件分支直接替换成 无条件跳转 或 NOP。
-
也就是说,代码路径在运行时可以被“打开”或“关闭”,分支判断几乎没有开销
-
-
特点
-
动态性强:开关可以在运行时改变,常用于某些内核特性启用/禁用。
-
零分支开销:当功能关闭时,对应分支完全消失,只剩一个
NOP
,不会影响性能关键路径。 -
用于 分布高度偏斜 且 有动态切换需求 的场景。
-
jump label和static keys
jump_lable屏蔽不同体系更改机器代码的不同,向上提供一个统一接口。不同体系会提供给jump_lable一个体系相关的实现。
jump_lable的实现原理很简单,就是通过替换内存中机器代码的nop空指令为jmp指令,或者替换机器代码的jmp指令为nop空指令,实现分支的切换。
obj-$(CONFIG_JUMP_LABEL) += jump_label.o
CONFIG_JUMP_LABEL=y
CONFIG_HAVE_ARCH_JUMP_LABEL=y
CONFIG_HAVE_ARCH_JUMP_LABEL_RELATIVE=y
数据结构
struct jump_entry
内核用数据结构 struct jump_entry
来描述 Jump label
。
struct jump_entry
实现是硬件架构相关的:
/* @include/linux/jump_label.h */
struct jump_entry {
s32 code; /* 被内核动态修改的 nop 或 跳转指令 地址 */
s32 target; /* l_yes 标号地址。后面 arch_static_branch() 函数中会描述 l_yes 标号 */
/*
* 关联的 static_key 的地址,static_key 的地址值总是起始于偶
* 数位置,所以地址值最低位总是 0 。
* 内核实现利用最低 1 位用来标识 branch 类型:
* 0 -> false (static_key_false() 构建的 jump_entry )
* 1 -> true (static_key_true() 构建的 jump_entry )
*/
long key;
};
struct static_key
struct static_key
是硬件架构实现无关的:
/* @include/linux/jump_label.h */
struct static_key {
atomic_t enabled; // enabled字段表示静态键的状态,0表示false,1表示true
#ifdef CONFIG_JUMP_LABEL
/*
* Note:
* To make anonymous unions work with old compilers, the static
* initialization of them requires brackets. This creates a dependency
* on the order of the struct with the initializers. If any fields
* are added, STATIC_KEY_INIT_TRUE and STATIC_KEY_INIT_FALSE may need
* to be modified.
*
* bit 0 => 1 if key is initially true
* 0 if initially false
* bit 1 => 1 if points to struct static_key_mod
* 0 if points to struct jump_entry
*/
/*
* 看到 union 定义方式,你可能会认为 3 个数据成员记录的信息
* 是互斥的。但事实并非如此,后面我们会看到,union 同时记录
* 了 @type 和 @entries 信息。
* 对于 @next ,我们在此篇不做展开,读者可自行分析。
*/
union {
unsigned long type; /* 关联的 jump_entry 类型:JUMP_LABEL_NOP, JUMP_LABEL_JMP */
struct jump_entry *entries; /* 关联的 jump_entry 地址 */
struct static_key_mod *next;
};
#endif /* CONFIG_JUMP_LABEL */
};
其关系如下图所示:
底层原理
jump label的初始化
所有的 struct jump_entry
都放在名为 __jump_table
的 section 内,在链接阶段,内核链接脚本片段
/* @ */
#define JUMP_TABLE_DATA \
. = ALIGN(8); \
__start___jump_table = .; \
KEEP(*(__jump_table)) \
__stop___jump_table = .;
将所有的 __jump_table
输入 section(也即 section 内的 struct jump_entry
变量),放置到区间 [__start___jump_table, __stop___jump_table]
内,且保证每个 struct jump_entry
的地址都位于4字节边界:ALIGN(8) 保证了第一个 struct jump_entry
的地址位于8字节边界,同时每个struct jump_entry
变量为 4 * 3 = 12
字节。地址4字节对齐,意味着地址的低2位总是为0,正是利用这一点,struct static_key::type
用这低位的2位来存储相关 struct jump_entry
(即 static_key::entries
指向的 struct jump_entry
)的类型。
/* 将系统中所有的 Jump label,按其定义的类型(JUMP_LABEL_NOP,JUMP_LABEL_JMP),
* 通过动态修改指令,将判定语句优化为 if (true) 或 if (false)
*/
start_kernel()
---> setup_arch()
--->jump_label_init()
void __init jump_label_init(void)
{
// 取出 整个 __jump_table 的起止地址,后面要在这段上整理
struct jump_entry *iter_start = __start___jump_table;
struct jump_entry *iter_stop = __stop___jump_table;
struct static_key *key = NULL;
struct jump_entry *iter;
/*
* Since we are initializing the static_key.enabled field with
* with the 'raw' int values (to avoid pulling in atomic.h) in
* jump_label.h, let's make sure that is safe. There are only two
* cases to check since we initialize to 0 or 1.
*/
/* 原子类型与 int 的初始化值一致性校验(编译期断言) */
BUILD_BUG_ON((int)ATOMIC_INIT(0) != 0);
BUILD_BUG_ON((int)ATOMIC_INIT(1) != 1);
if (static_key_initialized)
return;
// 上锁:防并发
cpus_read_lock();
jump_label_lock();
// 把 __jump_table 里的条目按一定顺序(通常按 key,再按地址)排序,这样同一 key 的位点在内存中 连续,便于后续 key->entries 指向一段连续区间
jump_label_sort_entries(iter_start, iter_stop);
for (iter = iter_start; iter < iter_stop; iter++) {
struct static_key *iterk;
bool in_init;
/* rewrite NOPs */
/* 把所有默认态为 NOP 的位点,先用架构特定方式“写成静态基线” */
if (jump_label_type(iter) == JUMP_LABEL_NOP)
arch_jump_label_transform_static(iter, JUMP_LABEL_NOP);
// 标记 这个位点是否在 .init.* 段
// 这决定了 init 段释放后(free_initmem() 之后),该位点是否仍可被热补丁。
// 对位于 .init 的位点,内核稍后会阻止再去修改(因为代码段可能被释放/覆盖)。
in_init = init_section_contains((void *)jump_entry_code(iter), 1);
jump_entry_set_init(iter, in_init);
// 按 key 归组:由于前面已经排序,这里当检测到 “换了一个新的 key” 时,就把这个 key 的 entries 指到当前 iter(本 key 的 首个 jump_entry)
iterk = jump_entry_key(iter);
if (iterk == key)
continue;
key = iterk;
static_key_set_entries(key, iter);
}
// 置位初始化完成,解锁
static_key_initialized = true;
jump_label_unlock();
cpus_read_unlock();
}
在上面的代码中,我们对函static_key_set_entries
、jump_label_type()
、 jump_entry_key()
未作展开,这几个是很关键的函数,我们有必要对它们进行展开。
jump_entry_key()
jump_entry_key()
用来返回关联 struct static_key
对象(即 jump_entry_key::key
指向的 struct static_key
对象)的首地址。来看它的具体实现:
static inline struct static_key *jump_entry_key(const struct jump_entry *entry)
{
long offset = entry->key & ~3L;
return (struct static_key *)((unsigned long)&entry->key + offset);
}
-
entry->key
并不是直接存绝对地址,而是一个 相对偏移 + 低 2 位标志位 的“打包字段”:-
低 2 位用来存放类型/标志(例如位点是 NOP 还是 JMP 等)。
-
其余位存放一个 相对偏移(signed long),相对于
&entry->key
这个位置。
-
-
entry->key & ~3L
:把低 2 位清零,只保留偏移值(按位与上...11111100
)。 -
&entry->key + offset
:用这个偏移量(相对于&entry->key
)计算出 真实的struct static_key
的地址。 -
最后把结果 cast 成
struct static_key *
返回。
为什么这么做呢?
关键在于:
-
key 字段 不是直接存放
struct static_key *
地址,而是存了:offset = (static_key地址 - &entry->key) flags = (低2位) key = offset | flags
-
这样节省空间,还能把低 2 位复用成标志位(因为指针天然 4 字节对齐,低 2 位本来就是 0)
逐步看:
-
entry->key & ~3L
-
~3L = ...11111100
-
作用:把低 2 位清零,只保留偏移量。
-
-
&entry->key + offset
-
&entry->key
= 当前jump_entry
表项里的key
成员地址。 -
加上
offset
,就得到了原本的struct static_key *
的真实地址。
-
假设内存布局如下:
-
entry->key
本身地址:0x1000
-
真实的
struct static_key
地址:0x2000
-
两者差值:
0x2000 - 0x1000 = 0x1000
于是:
-
存储时:
entry->key = 0x1000 | flags
-
取出时:
offset = entry->key & ~3L; // 得到 0x1000 key = &entry->key + offset; // 0x1000 + 0x1000 = 0x2000 return (struct static_key *)0x2000;
这样就还原回了 static_key
的真实地址。
一句话总结:
jump_entry_key()
的工作就是 把压缩存储在 jump_entry->key 里的“相对偏移 + 标志位”解码成真正的struct static_key *
地址。
jump_label_type()
jump_label_type()
用来判定 struct jump_entry
的类型(JUMP_LABEL_NOP
或 JUMP_LABEL_JMP
),来看它的具体实现:
static enum jump_label_type jump_label_type(struct jump_entry *entry)
{
struct static_key *key = jump_entry_key(entry); // 获取static key指针
bool enabled = static_key_enabled(key);
bool branch = jump_entry_is_branch(entry);
/* See the comment in linux/jump_label.h */
return enabled ^ branch;
}
static_key_set_entries()
/***
* A 'struct static_key' uses a union such that it either points directly
* to a table of 'struct jump_entry' or to a linked list of modules which in
* turn point to 'struct jump_entry' tables.
*
* The two lower bits of the pointer are used to keep track of which pointer
* type is in use and to store the initial branch direction, we use an access
* function which preserves these bits.
*/
static void static_key_set_entries(struct static_key *key,
struct jump_entry *entries)
{
unsigned long type;
WARN_ON_ONCE((unsigned long)entries & JUMP_TYPE_MASK);
type = key->type & JUMP_TYPE_MASK;
key->entries = entries;
key->type |= type;
}
功能:把某个 static_key
和它对应的 jump_entry 表挂起来,并保留低 2 位的类型/方向标志。
-
建立关联
-
参数
entries
指向该 key 在__jump_table
中的第一条记录(后面连续属于这个 key)。 -
调用后,
key->entries = entries
,static_key 就知道了自己的所有位点在哪。
-
-
保留低 2 位标志
-
低 2 位用来记录:
-
指针类型:是指向
jump_entry
表,还是指向模块链表; -
初始分支方向:默认 NOP 还是默认 JMP。
-
-
写入新指针时,先保存原有标志,再 OR 回去,避免覆盖。
-
-
对齐检查
-
entries
必须按 4/8 字节对齐(低 2 位为 0),否则低位不能安全复用。 -
WARN_ON_ONCE((unsigned long)entries & JUMP_TYPE_MASK)
确保这个条件。
-
可以把它理解为:
👉 static_key_set_entries()
就是给一个开关(static_key)挂上它控制的“所有开关点”的地址表(jump_entry 表),
同时保存开关的属性(低两位标志),保证后续能正确打开/关闭。
举个例子:
假设你写了两个地方用同一个 static key:
if (static_branch_unlikely(&key))
foo();
if (static_branch_unlikely(&key))
bar();
编译时 → 在 __jump_table
生成两条 jump_entry
。
初始化时 → jump_label_init()
遍历表,把它们按 key 归组。
最终 → 调用 static_key_set_entries(&key, first_entry)
,
这样 key
知道:“我名下有 2 个跳转点(foo 和 bar)”。
当以后 static_branch_enable(&key)
时,就能一次性 patch 这两个点。
static keys api
这里只讨论说明启用了CONFIG_JUMP_LABEL功能的情况
#elif defined(CONFIG_JUMP_LABEL)
#define JUMP_TYPE_FALSE 0UL
#define JUMP_TYPE_TRUE 1UL
#define JUMP_TYPE_LINKED 2UL
#define JUMP_TYPE_MASK 3UL
static __always_inline bool static_key_false(struct static_key *key)
{
return arch_static_branch(key, false);
}
static __always_inline bool static_key_true(struct static_key *key)
{
return !arch_static_branch(key, true);
}
extern struct jump_entry __start___jump_table[];
extern struct jump_entry __stop___jump_table[];
extern void jump_label_init(void);
extern void jump_label_lock(void);
extern void jump_label_unlock(void);
extern void arch_jump_label_transform(struct jump_entry *entry,
enum jump_label_type type);
extern bool arch_jump_label_transform_queue(struct jump_entry *entry,
enum jump_label_type type);
extern void arch_jump_label_transform_apply(void);
extern int jump_label_text_reserved(void *start, void *end);
extern void static_key_slow_inc(struct static_key *key);
extern void static_key_slow_dec(struct static_key *key);
extern void static_key_slow_inc_cpuslocked(struct static_key *key);
extern void static_key_slow_dec_cpuslocked(struct static_key *key);
extern int static_key_count(struct static_key *key);
extern void static_key_enable(struct static_key *key);
extern void static_key_disable(struct static_key *key);
extern void static_key_enable_cpuslocked(struct static_key *key);
extern void static_key_disable_cpuslocked(struct static_key *key);
extern enum jump_label_type jump_label_init_type(struct jump_entry *entry);
/*
* We should be using ATOMIC_INIT() for initializing .enabled, but
* the inclusion of atomic.h is problematic for inclusion of jump_label.h
* in 'low-level' headers. Thus, we are initializing .enabled with a
* raw value, but have added a BUILD_BUG_ON() to catch any issues in
* jump_label_init() see: kernel/jump_label.c.
*/
#define STATIC_KEY_INIT_TRUE \
{ .enabled = { 1 }, \
{ .type = JUMP_TYPE_TRUE } }
#define STATIC_KEY_INIT_FALSE \
{ .enabled = { 0 }, \
{ .type = JUMP_TYPE_FALSE } }
static_key_false/static_key_true
static __always_inline bool static_key_false(struct static_key *key)
{
return arch_static_branch(key, false);
}
static __always_inline bool static_key_true(struct static_key *key)
{
return !arch_static_branch(key, true);
}
启用jump label时,static_key_false
和static_key_true
实现上均通过arch_static_branch
做分支选择。
。对于同一个static_key
,这两个api是不应当混用的,初始化的值应该与大概率的分支匹配。
static_key
的默认值与enabled值,同时作用影响分支判断处使用nop还是jump。相同则使用nop,不同则使用jump。比如默认true当前true则使用nop,默认true当前false则使用jump。
-
static_key_false
生成的代码中,nop固定对应的是static_key
的false分支代码,jump对应true分支代码。 -
static_key_true
生成的代码中,nop固定对应的是static_key
的true分支代码,jump对应false分支代码。
因此如果分支判断api与初始化值错配使用,会导致运行路径与static_key
的值相反。
arch_static_branch
的实现如下
static __always_inline bool arch_static_branch(struct static_key *key,
bool branch)
{
asm goto(
"1: nop \n\t" // (A) 现场指令,默认是 NOP
" .pushsection __jump_table, \"aw\" \n\t" // (B) 写一条 jump_entry 到专用段
" .align 3 \n\t"
" .long 1b - ., %l[l_yes] - . \n\t" // (C) 记录 code位点 与 目标 的相对偏移
" .quad %c0 - . \n\t" // (D) 记录 key 指针(带低位标志)的相对偏移
" .popsection \n\t"
: : "i"(&((char *)key)[branch]) : : l_yes);
return false; // (E) 默认走不到 l_yes,返回 false
l_yes:
return true; // (F) 若位点被打成跳转,会到这里返回 true
}
(A) 生成“可补丁”的位点:默认是 nop
-
汇编里
1:
打个局部标签,紧接一条nop
。 -
默认状态:执行时就是 NOP,直接向下返回
false
。 -
启用 static key 后:内核把这一条 NOP 热补丁成跳转(到
l_yes
),这样就会返回true
。
(B)(C)(D) 把一条 jump_entry
写入 __jump_table
-
进入
__jump_table
段,写入三元信息(不同架构/版本布局略有不同,这里是 ARM64 风格):-
.long 1b - .
:code 位点 = 当前 NOP 的位置(相对偏移,PC-relative) -
.long %l[l_yes] - .
:目标地址 =l_yes
标签(也是相对偏移) -
.quad %c0 - .
:key 指针(带低位标志) 的相对偏移-
这里的
%c0
是内联汇编输入参数"i"(&((char*)key)[branch])
-
等价于:把
key
的地址加上branch
(0 或 1)。这样复用低 1 位保存“初始分支方向”(branch:默认走还是默认不走)。jump label 机制会把指针低 2 位当作标志位。
-
-
小结:
__jump_table
就多了一条“某个位点 ↔ 某个 key ↔ 某个目标”的索引。启动时jump_label_init()
会把这张表排序、分组,再把每个 key 下所有位点串起来。
(E)(F) asm goto
与返回值
-
这是
asm goto
:允许内联汇编跳转到 C 里的标签。 -
默认指令是 NOP → 不跳 → 执行
return false;
。 -
当
static_branch_enable(&key)
后,内核把该 NOP 补丁成跳转到l_yes
→ 执行return true;
运行期的两种形态
-
未启用(默认):
-
现场指令是
nop
-
不跳转 → 落到
return false;
-
几乎零开销(没有 if 取值分支)
-
-
启用后:
-
现场被补丁为
b l_yes
(或相应的跳转指令) -
直接跳到
l_yes:
→return true;
-
开销就是一次跳转
-
这段内联汇编结束后,返回false。编译器会根据该函数永远返回false将false分支的代码优化到直线路径中。
因此,static_key_false
返回false对应的代码分支会位于直线路径中,static_key_true
返回true对应的代码分支会位于直线路径中,与未启用jump label的版本逻辑一致。
新版本内核中,不推荐使用此旧版api,
推荐使用新版api
DEFINE_STATIC_KEY_TRUE(key);
DEFINE_STATIC_KEY_FALSE(key);
DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);
DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);
static_branch_likely()
static_branch_unlikely()
static_branch_likely/static_branch_unlikely
前面提到旧版的api中static_key
成员enabled大于0时,代码的运行路径应该是static_key_{false,true}
返回值为true的代码路径。
而且static_key_false
需要与STATIC_KEY_INIT_FALSE
一起使用,static_key_true
需要与STATIC_KEY_INIT_TRUE
一起使用,不能错配使用。
如果错配使用,在启用jump label时会发生代码运行路径与enabled值不符的情况。
新版api解决了这个问题。使用新版api时,无论初始化值是什么,static_branch_{likely,unlikely}
返回true的代码路径均可以在enabled大于0时运行。
static_branch_likely
-
使true分支代码块位于nop直线路径中,初始值为true时,初始执行true分支,分支位置填充nop
-
使true分支代码块位于nop直线路径中,初始值为false时,初始执行false分支,分支位置填充jump
-
同时为jump_entry的key成员最低比特位置1,用于标记该分支nop直线路径对应true分支
static_branch_unlikely
-
使true分支代码块位于jump路径中,初始值为true时,初始执行true分支,分支位置填充jump
-
使true分支代码块位于jump路径中,初始值为false时,初始执行false分支,分支位置填充nop
-
同时为jump_entry的key成员最低比特位置0,用于标记该分支nop直线路径对应false分支
#define static_branch_likely(x) \
({ \
bool branch; \
if (__builtin_types_compatible_p(typeof(*x), struct static_key_true)) \
branch = !arch_static_branch(&(x)->key, true); \
else if (__builtin_types_compatible_p(typeof(*x), struct static_key_false)) \
branch = !arch_static_branch_jump(&(x)->key, true); \
else \
branch = ____wrong_branch_error(); \
likely_notrace(branch); \
})
#define static_branch_unlikely(x) \
({ \
bool branch; \
if (__builtin_types_compatible_p(typeof(*x), struct static_key_true)) \
branch = arch_static_branch_jump(&(x)->key, false); \
else if (__builtin_types_compatible_p(typeof(*x), struct static_key_false)) \
branch = arch_static_branch(&(x)->key, false); \
else \
branch = ____wrong_branch_error(); \
unlikely_notrace(branch); \
})
--> 怎么用
-
如果功能默认关闭,用
DEFINE_STATIC_KEY_FALSE
并在代码用static_branch_unlikely()
包住慢路径(最佳实践)。 -
如果功能默认开启,用
DEFINE_STATIC_KEY_TRUE
并在代码用static_branch_likely()
包住快路径。 -
启停用
static_branch_enable()/disable()
(或static_branch_inc/dec()
计数式)。
这样你得到:关闭时零开销;开启后才有跳转代价,而宏保证返回的布尔值和 likely/unlikely
语义一致,不用关心底层 NOP/JMP 模板的细节与取反。
static_key_enable/static_key_disable
static inline void static_key_enable(struct static_key *key)
{
STATIC_KEY_CHECK_USE(key);
if (atomic_read(&key->enabled) != 0) {
WARN_ON_ONCE(atomic_read(&key->enabled) != 1);
return;
}
atomic_set(&key->enabled, 1);
}
static inline void static_key_disable(struct static_key *key)
{
STATIC_KEY_CHECK_USE(key);
if (atomic_read(&key->enabled) != 1) {
WARN_ON_ONCE(atomic_read(&key->enabled) != 0);
return;
}
atomic_set(&key->enabled, 0);
}
这两个函数是 static key 的最基础版本的“开关”,只是在 原子计数器 key->enabled
上做操作,并没有做 指令热补丁。
在 jump label 机制里,每个 static key 都有一个 enabled
字段,类型是 atomic_t
:
-
enabled = 0
→ 默认关闭 -
enabled = 1
→ 启用一次(开) -
enabled > 1
→ 如果用了引用计数式接口(static_branch_inc/dec
),可以支持多次启用/关闭配对
static_key_enable()/disable()
就是这个字段的最小封装。
-
这两个函数只是修改原子计数,属于“快速接口”。
-
它们本身 不会修改指令流(不会把
nop
补丁成jmp
)。 -
在实际使用中,通常会用
static_branch_enable()
或static_branch_inc()
这样的高级接口,这些接口内部会:-
修改
key->enabled
-
调用
jump_label_update()
→ 遍历该 key 的所有jump_entry
-
把对应的指令 NOP/JMP 做热补丁
-
所以说:
-
static_key_enable()/disable()
更像是 基础原子开关(只改标志位)。 -
真正“启用分支”的效果,要靠 jump label 框架去执行 patch。
使用方法
定义一个static key
DEFINE_STATIC_KEY_FALSE(my_feature); // 默认关闭
DEFINE_STATIC_KEY_TRUE(my_other_feature); // 默认开启
在代码里用static branch包住分支
if (static_branch_unlikely(&my_feature)) {
do_slow_path();
}
if (static_branch_likely(&my_other_feature)) {
do_fast_path();
}
在运行时打开或关闭
// 打开
static_branch_enable(&my_feature);
// 或者引用计数接口
static_branch_inc(&my_feature);
// 关闭
static_branch_disable(&my_feature);
static_branch_dec(&my_feature);
案例实战
demo 1
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/init.h>
#include <linux/jump_label.h>
#include <linux/printk.h>
#include <linux/debugfs.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("you");
MODULE_DESCRIPTION("Static Key / Jump Label demo with debugfs toggle (minimal)");
DEFINE_STATIC_KEY_FALSE(demo_key); /* 默认关闭 */
static struct dentry *demo_dir;
/* debugfs: /sys/kernel/debug/static_key_demo/enabled */
static ssize_t enabled_read(struct file *f, char __user *buf,
size_t len, loff_t *ppos)
{
char out[4];
/* 直接读当前 key 状态,避免自维护副本 */
int n = scnprintf(out, sizeof(out), "%d\n",
static_key_enabled(&demo_key) ? 1 : 0);
return simple_read_from_buffer(buf, len, ppos, out, n);
}
static ssize_t enabled_write(struct file *f, const char __user *ubuf,
size_t len, loff_t *ppos)
{
char kbuf[8];
bool on;
if (len == 0)
return -EINVAL;
if (len >= sizeof(kbuf))
len = sizeof(kbuf) - 1;
if (copy_from_user(kbuf, ubuf, len))
return -EFAULT;
kbuf[len] = '\0';
if (kbuf[0] == '1' || kbuf[0] == 'y' || kbuf[0] == 'Y' ||
!strncmp(kbuf, "on", 2))
on = true;
else if (kbuf[0] == '0' || kbuf[0] == 'n' || kbuf[0] == 'N' ||
!strncmp(kbuf, "off", 3))
on = false;
else
return -EINVAL;
/* 直接切换;内部已做幂等处理与必要的指令补丁 */
if (on) {
static_branch_enable(&demo_key);
pr_info("demo: ENABLED via debugfs\n");
} else {
static_branch_disable(&demo_key);
pr_info("demo: DISABLED via debugfs\n");
}
if (static_branch_unlikely(&demo_key))
pr_info("demo: hot path executed (static key ENABLED)\n");
return len;
}
static const struct file_operations enabled_fops = {
.owner = THIS_MODULE,
.read = enabled_read,
.write = enabled_write,
.llseek = no_llseek,
};
static int __init demo_init(void)
{
demo_dir = debugfs_create_dir("static_key_demo", NULL);
if (IS_ERR_OR_NULL(demo_dir)) {
pr_err("demo: failed to create debugfs dir\n");
return -ENODEV;
}
if (IS_ERR_OR_NULL(debugfs_create_file("enabled", 0600, demo_dir,
NULL, &enabled_fops))) {
debugfs_remove_recursive(demo_dir);
return -ENODEV;
}
pr_info("demo: loaded; toggle at /sys/kernel/debug/static_key_demo/enabled\n");
/* 初次演示一次热点调用(默认关闭 → 不打印) */
if (static_branch_unlikely(&demo_key))
pr_info("demo: hot path executed (static key ENABLED)\n");
return 0;
}
static void __exit demo_exit(void)
{
/* 可不必显式关闭;若你希望卸载时回到关闭态,可解注释: */
/* if (static_key_enabled(&demo_key)) static_branch_disable(&demo_key); */
debugfs_remove_recursive(demo_dir);
pr_info("demo: unloaded\n");
}
module_init(demo_init);
module_exit(demo_exit);
当我初始化时,默认关闭static key时
DEFINE_STATIC_KEY_FALSE(demo_key);
当初始化时,默认开启static key时
DEFINE_STATIC_KEY_TRUE(demo_key);
我们可以看到打印功能已经被demo_key所控制
demo 2
// jump_label_demo.c
// gcc -DJUMP_LABEL -O jump_label_demo.c -o demo -g
#include <stdio.h>
#include <sys/mman.h>
#ifdef JUMP_LABEL
struct entry {
unsigned long code;
unsigned long target;
unsigned long key;
};
#define MAX 2
struct entry base __attribute__ ((section ("__jump_table"))) = {0};
void update_branch(int key)
{
int i;
char *page;
struct entry *e = (struct entry *)((char *)&base - MAX*sizeof(struct entry));
for (i = 0; i < MAX; i++) {
e = e + i;
if (e->key == key) {
// 修改代码段
unsigned int *code = (int *)((char *)e->code + 1);
unsigned int offset = (unsigned int)(e->target - e->code - 5);
page = (char *)((unsigned long)code & 0xffffffffffff1000);
mprotect((void *)page, 4096, PROT_WRITE|PROT_READ|PROT_EXEC);
*code = offset;
mprotect((void *)page, 4096, PROT_READ|PROT_EXEC);
break;
}
}
}
#define STATIC_KEY_INITIAL_NOP ".byte 0xe9 \n\t .long 0\n\t"
static __attribute__((always_inline)) inline static_branch_true(int enty)
{
int ent = enty;
asm goto ("1:"
STATIC_KEY_INITIAL_NOP
".pushsection __jump_table, \"aw\" \n\t"
// 定义三元组{本函数内联后标号1的地址,本函数内联后标号l_yes的地址,参数enty}
".quad 1b, %l[l_yes], %c0\n\t"
".popsection \n\t"
:
: "i"(ent)
:
: l_yes);
return 0;
l_yes:
return 1;
}
#endif
int main(int argc, char **argv)
{
int E1, E2;
E1 = atoi(argv[1]);
E2 = atoi(argv[2]);
#ifdef JUMP_LABEL
int e1 = 0x11223344;
int e2 = 0xaabbccdd;
printf("Just Jump label\n");
if (E1) {
update_branch(e1);
}
if (E2) {
update_branch(e2);
}
#endif
#ifdef JUMP_LABEL
if (static_branch_true(e1)) {
#else
if (E1) {
#endif
printf("condition 1 is true\n");
} else {
printf("condition 1 is false\n");
}
#ifdef JUMP_LABEL
if (static_branch_true(e2)) {
#else
if (E2) {
#endif
printf("condition 2 is true\n");
} else {
printf("condition 2 is false\n");
}
return 0;
}
定义JUMP_LABEL宏编译之,看看效果:
[root@localhost checker]# gcc -DJUMP_LABEL -O jump_label_demo.c -o demo -g
[root@localhost checker]# ./demo 1 0
Just Jump label
condition 1 is true
condition 2 is false
[root@localhost checker]# ./demo 0 1
Just Jump label
condition 1 is false
condition 2 is true
如何做到的呢?static_branch_true内联函数是如何判断true or false的呢?
事实上,jump label逻辑修改了代码段,取消了条件判断!这一切都是在update_branch中发生的。我们看下update_branch调用之前,main函数的汇编码:
(gdb) disassemble main
Dump of assembler code for function main:
...
0x0000000000400662 <+74>: callq 0x4005ad <update_branch>
// 0x0000000000400667 <+79> 记住这里的指令吧!
0x0000000000400667 <+79>: jmpq 0x40066c <main+84>
0x000000000040066c <+84>: jmp 0x40067a <main+98>
0x000000000040066e <+86>: mov $0x400750,%edi
0x0000000000400673 <+91>: callq 0x400470 <puts@plt>
在执行了update_branch之后,main函数发生了变化:
(gdb) b main
Breakpoint 1 at 0x400618: file jump_label_demo.c, line 56.
(gdb) r 1 0
Starting program: /root/checker/./demo 1 0
Breakpoint 1, main (argc=3, argv=0x7fffffffe428) at jump_label_demo.c:56
56 {
(gdb) next
59 E1 = atoi(argv[1]);
(gdb) next
60 E2 = atoi(argv[2]);
(gdb)
65 printf("Just Jump label\n");
(gdb)
Just Jump label
66 if (E1) {
(gdb)
67 update_branch(e1);
(gdb)
69 if (E2) {
(gdb) disassemble main
Dump of assembler code for function main:
...
0x0000000000400662 <+74>: callq 0x4005ad <update_branch>
// 0x0000000000400667 <+79> 指令已经被修改为jmpq 0x40066e
0x0000000000400667 <+79>: jmpq 0x40066e <main+86>
0x000000000040066c <+84>: jmp 0x40067a <main+98>
0x000000000040066e <+86>: mov $0x400750,%edi
0x0000000000400673 <+91>: callq 0x400470 <puts@plt>
看样子就是这么回事!
之所以这件事可以发生得如此简单,多亏了一个新的section,即__jump_table,我们通过objdump看看__jump_table的内容:
Contents of section __jump_table:
601040 67064000 00000000 6e064000 00000000 g.@.....n.@.....
601050 44332211 00000000 84064000 00000000 D3".......@.....
601060 8b064000 00000000 ddccbbaa ffffffff ..@.............
601070 00000000 00000000 00000000 00000000 ................
601080 00000000 00000000
通过jump_label_demo.c的struct entry结构体,我们直到这个section中包含了多个3元组,包含3个字段:
需要修改的代码地址。
需要jmp到的代码地址。
匹配健。
我们看67064000 00000000按照小端就是0x400667,它就是需要修改的代码地址,而6e064000 00000000按照小端则是0x40066e:
400667: e9 00 00 00 00 jmpq 40066c <main+0x54>
40066c: eb 0c jmp 40067a <main+0x62>
40066e: bf 50 07 40 00 mov $0x400750,%edi
看来,这个__jump_table的item会将jmpq 40066c修改为jmpq 40066e,从而实现了 永久静态分支。
最后,__jump_table的内容就是在每一个内联的static_branch_true函数中被填充的,该参数的参数是一个key,它指示了branch entry三元组中的最后一个字段。
static_branch_true函数的内联非常重要,它实现了将branch entry三元组数据直接插入到__jump_table section,而不是共享同一个函数体。
总之,如果你看代码还是觉得别扭,手敲一遍我上面的示例程序,就理解了,内核里面的也就这么回事,总结一句话:
-
依靠运行时修改代码而不是依靠状态数据来控制执行流。
Static Keys 的应用场景
Linux Static Keys 非常适合用于以下场景:
-
调试功能:在开发阶段,某些调试功能只需在特定条件下启用,Static Keys 可以高效地控制这些功能的开关。
-
性能优化:对性能敏感的路径,可以使用 Static Keys 避免不必要的分支判断。
-
模块化功能:某些内核模块可能需要动态加载或卸载,Static Keys 能够确保这些操作对整体性能的影响最小。
Static Keys 的优缺点
尽管 Static Keys 提供了强大的性能优化能力,但也有其局限性:
优点
-
极大地减少了分支预测失败的开销。
-
动态切换功能开销低,实现了运行时的高效优化。
-
在代码热路径中使用,可显著提升内核性能。
缺点
-
实现复杂度较高,需要对内核代码有深入了解。
-
代码段的动态修改可能会影响某些调试工具的工作。
-
仅适用于特定场景,不适合频繁切换状态的功能。
如何在项目中高效使用Static Keys
要在Linux内核开发中高效使用 Static Keys,可以遵循以下建议:
-
识别性能关键点:分析代码的性能瓶颈,确定哪些路径适合使用 Static Keys。
-
合理设计代码结构:确保 Static Keys 的启用/禁用逻辑简单明了,不引入额外的复杂性。
-
充分测试:由于 Static Keys 涉及动态修改代码段,需要确保在各种场景下都能稳定运行。
通过这些实践,开发者能够最大化 Static Keys 的性能优势,同时最小化其潜在风险。
结论
Linux Static Keys 是一个强大的工具,它为内核开发者提供了优化代码性能的手段。通过动态切换代码路径,Static Keys 能够在不影响灵活性的前提下,显著提升内核的运行效率。
如果你正在从事内核开发或对性能优化感兴趣,不妨深入研究 Static Keys,并将其应用到你的项目中。key