AI智能摘要
在Linux内核高频路径中,likely/unlikely宏帮助提升分支预测准确率,但随着判断增多,分支预测失败和cache压力导致性能瓶颈。为彻底消除分支带来的损耗,内核引入static keys和jump label机制,通过运行时动态替换代码段,实现零开销切换分支。
此摘要由AI分析文章内容生成,仅供参考。

前言

在内核代码中,许多条件判断在正常情况下的结果几乎是固定的,只有在极少数特殊场景下才会发生变化。单个判断的开销通常可以忽略不计,但如果此类判断数量庞大且被高频执行,就可能带来显著的性能损耗。

为此,内核提供了 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 */
};

其关系如下图所示:

2025/09/halo_owtb5gb.png

底层原理

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_entriesjump_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)

逐步看:

  1. entry->key & ~3L

    • ~3L = ...11111100

    • 作用:把低 2 位清零,只保留偏移量。

  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_NOPJUMP_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 位的类型/方向标志。

  1. 建立关联

    • 参数 entries 指向该 key 在 __jump_table 中的第一条记录(后面连续属于这个 key)。

    • 调用后,key->entries = entries,static_key 就知道了自己的所有位点在哪。

  2. 保留低 2 位标志

    • 低 2 位用来记录:

      • 指针类型:是指向 jump_entry 表,还是指向模块链表;

      • 初始分支方向:默认 NOP 还是默认 JMP。

    • 写入新指针时,先保存原有标志,再 OR 回去,避免覆盖。

  3. 对齐检查

    • 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_falsestatic_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() 这样的高级接口,这些接口内部会:

    1. 修改 key->enabled

    2. 调用 jump_label_update() → 遍历该 key 的所有 jump_entry

    3. 把对应的指令 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