Linux内核static-keys
一、static-keys.txt 翻译
静态键
-----------
作者:Jason Baron <jbaron@redhat.com>
0) 摘要
静态键允许通过 GCC 功能和代码修补技术在对性能敏感的快速路径内核代码中包含很少使用的功能。 一个简单的例子:
struct static_key key = STATIC_KEY_INIT_FALSE; ... if (static_key_false(&key)) do unlikely code else do likely code ... static_key_slow_inc(); ... static_key_slow_inc(); ...
static_key_false() 分支将生成到代码中,对可能的代码路径的影响尽可能小。
1) 动机
目前,跟踪点是使用条件分支实现的。 条件检查需要检查每个跟踪点的全局变量。 虽然这个检查的开销很小,但是当内存缓存受到压力时它会增加(这些全局变量的内存缓存行可能与其他内存访问共享)。
随着我们增加内核中跟踪点的数量,这种开销可能会成为一个更大的问题。 此外,跟踪点通常处于休眠状态(禁用)并且不提供直接的内核功能。 因此,非常希望尽可能减少它们的影响。 尽管跟踪点是这
项工作的最初动机,但其他内核代码路径应该能够利用静态键设施。
2) 解决方案
gcc (v4.5) 添加了一个新的“asm goto”语句,允许分支到标签:http://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html
使用“asm goto”,我们可以创建默认情况下采用或不采用的分支,而无需检查内存。 然后,在运行时,我们可以修补分支站点以改变分支方向。
例如,如果我们有一个默认禁用的简单分支:
if (static_key_false(&key)) printk("I am the true branch\n");
因此,默认情况下不会发出“printk”。 生成的代码将在直线代码路径中包含单个原子“无操作(no-op)”指令(x86 上为 5 个字节)。 当分支被“翻转(flipped)”时,我们将使用“跳转(jump)”指令将直线代码路径
中的“无操作(no-op)”修补到线外真实分支。 因此,改变分支方向是昂贵的,但分支选择基本上是“免费的”。 这是这种优化的基本权衡。
这种低级修补机制称为“跳转标签修补”,它为静态键设施提供了基础。
3) 静态键标签API、用法及示例:
为了利用这种优化,您必须首先定义一个键:
struct static_key key;
初始化为:
struct static_key key = STATIC_KEY_INIT_TRUE;
或者:
struct static_key key = STATIC_KEY_INIT_FALSE;
如果键未初始化,则默认为 false。 'struct static_key'必须是全局变量。 也就是说,它不能在堆栈上分配或在运行时动态分配。
然后在代码中使用键为:
if (static_key_false(&key)) do unlikely code else do likely code
或者:
if (static_key_true(&key)) do likely code else do unlikely code
通过“STATIC_KEY_INIT_FALSE”初始化的键必须在“static_key_false()”构造中使用。 同样,通过“STATIC_KEY_INIT_TRUE”初始化的键必须用在“static_key_true()”构造中使用。 一个键可以在许多分支中使用,但所有
分支必须与键的初始化方式相匹配。
然后可以通过以下方式切换分支:
static_key_slow_inc(&key);
...
static_key_slow_dec(&key);
因此,“static_key_slow_inc()”意味着“使分支为真”,并且'static_key_slow_dec()' 表示使用适当的引用计数'使分支为假'。 例如,如果键被初始化为真,static_key_slow_dec() 会将分支切换为假。 随后的
static_key_slow_inc() 会将分支更改回 true。 同样,如果键被初始化为 false,则“static_key_slow_inc()”会将分支更改为 true。 然后'static_key_slow_dec()',将再次使分支为假。
内核中的示例用法是跟踪点的实现:
static inline void trace_##name(proto) \ { \ if (static_key_false(&__tracepoint_##name.key)) \ __DO_TRACE(&__tracepoint_##name, \ TP_PROTO(data_proto), \ TP_ARGS(data_args), \ TP_CONDITION(cond)); \ }
跟踪点默认是禁用的,可以放置在内核的性能关键部分。 因此,通过使用静态键,跟踪点在不使用时可以产生绝对最小的影响。
4) 架构级代码打补丁接口,‘跳转标签(jump labels)’
为了利用这种优化,架构必须实现一些功能和宏。 如果没有架构支持,我们就简单地退回到传统的加载、测试和跳转序列。
* 选择 HAVE_ARCH_JUMP_LABEL,参见:arch/x86/Kconfig
* #define JUMP_LABEL_NOP_SIZE,见:arch/x86/include/asm/jump_label.h
* __always_inline bool arch_static_branch(struct static_key *key),见:arch/x86/include/asm/jump_label.h
* void arch_jump_label_transform(struct jump_entry *entry, enum jump_label_type type),参见:arch/x86/kernel/jump_label.c
* __init_or_module void arch_jump_label_transform_static(struct jump_entry *entry, enum jump_label_type type), 参见:arch/x86/kernel/jump_label.c
* struct jump_entry,参见:arch/x86/include/asm/jump_label.h
5)静态键/跳转标签分析,结果(x86_64):
例如,让我们将以下分支添加到“getppid()”,这样系统调用现在看起来像:
SYSCALL_DEFINE0(getppid) { int pid; + if (static_key_false(&key)) + printk("I am the true branch\n"); rcu_read_lock(); pid = task_tgid_vnr(rcu_dereference(current->real_parent)); rcu_read_unlock(); return pid; }
GCC生成的带有跳转标签的指令是:
ffffffff81044290 <sys_getppid>: ffffffff81044290: 55 push %rbp ffffffff81044291: 48 89 e5 mov %rsp,%rbp ffffffff81044294: e9 00 00 00 00 jmpq ffffffff81044299 <sys_getppid+0x9> ffffffff81044299: 65 48 8b 04 25 c0 b6 mov %gs:0xb6c0,%rax ffffffff810442a0: 00 00 ffffffff810442a2: 48 8b 80 80 02 00 00 mov 0x280(%rax),%rax ffffffff810442a9: 48 8b 80 b0 02 00 00 mov 0x2b0(%rax),%rax ffffffff810442b0: 48 8b b8 e8 02 00 00 mov 0x2e8(%rax),%rdi ffffffff810442b7: e8 f4 d9 00 00 callq ffffffff81051cb0 <pid_vnr> ffffffff810442bc: 5d pop %rbp ffffffff810442bd: 48 98 cltq ffffffff810442bf: c3 retq ffffffff810442c0: 48 c7 c7 e3 54 98 81 mov $0xffffffff819854e3,%rdi ffffffff810442c7: 31 c0 xor %eax,%eax ffffffff810442c9: e8 71 13 6d 00 callq ffffffff8171563f <printk> ffffffff810442ce: eb c9 jmp ffffffff81044299 <sys_getppid+0x9>
如果没有跳转标签优化,它看起来像:
ffffffff810441f0 <sys_getppid>: ffffffff810441f0: 8b 05 8a 52 d8 00 mov 0xd8528a(%rip),%eax # ffffffff81dc9480 <key> ffffffff810441f6: 55 push %rbp ffffffff810441f7: 48 89 e5 mov %rsp,%rbp ffffffff810441fa: 85 c0 test %eax,%eax ffffffff810441fc: 75 27 jne ffffffff81044225 <sys_getppid+0x35> ffffffff810441fe: 65 48 8b 04 25 c0 b6 mov %gs:0xb6c0,%rax ffffffff81044205: 00 00 ffffffff81044207: 48 8b 80 80 02 00 00 mov 0x280(%rax),%rax ffffffff8104420e: 48 8b 80 b0 02 00 00 mov 0x2b0(%rax),%rax ffffffff81044215: 48 8b b8 e8 02 00 00 mov 0x2e8(%rax),%rdi ffffffff8104421c: e8 2f da 00 00 callq ffffffff81051c50 <pid_vnr> ffffffff81044221: 5d pop %rbp ffffffff81044222: 48 98 cltq ffffffff81044224: c3 retq ffffffff81044225: 48 c7 c7 13 53 98 81 mov $0xffffffff81985313,%rdi ffffffff8104422c: 31 c0 xor %eax,%eax ffffffff8104422e: e8 60 0f 6d 00 callq ffffffff81715193 <printk> ffffffff81044233: eb c9 jmp ffffffff810441fe <sys_getppid+0xe> ffffffff81044235: 66 66 2e 0f 1f 84 00 data32 nopw %cs:0x0(%rax,%rax,1) ffffffff8104423c: 00 00 00 00
因此,禁用跳转标签案例添加了“mov”、“test”和“jne”指令,而跳转标签案例只有“无操作(no-op)”或“jmp 0”。 (jmp 0 在启动时被修补为 5 字节的原子无操作指令。)因此,禁用跳转标签的情况添加:
6 (mov) + 2 (test) + 2 (jne) = 10 - 5 (5 byte jump 0) = 5 个加法字节。
如果我们随后包含填充字节,则跳转标签代码将为这个小功能节省总共 16 个字节的指令存储器。在这种情况下,非跳转标签函数的长度为 80 个字节。因此,我们节省了 20% 的指令占用空间。事实上,我们可以
进一步改进这一点,因为 5 字节无操作实际上可以是 2 字节无操作,因为我们可以使用 2 字节 jmp 到达分支。但是,我们还没有实现最佳的无操作大小(它们目前是硬编码的)。
由于调度程序路径中使用了许多静态键 API,因此可以使用“pipe-test”(也称为“perf bench sched pipe”)来显示性能改进。在 3.3.0-rc2 上完成的测试:
跳转标签禁用:
'bash -c /tmp/pipe-test' 的性能计数器统计信息(50 次运行):
855.700314 task-clock # 0.534 CPUs utilized ( +- 0.11% )
200,003 context-switches # 0.234 M/sec ( +- 0.00% )
0 CPU-migrations # 0.000 M/sec ( +- 39.58% )
487 page-faults # 0.001 M/sec ( +- 0.02% )
1,474,374,262 cycles # 1.723 GHz ( +- 0.17% )
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
1,178,049,567 instructions # 0.80 insns per cycle ( +- 0.06% )
208,368,926 branches # 243.507 M/sec ( +- 0.06% )
5,569,188 branch-misses # 2.67% of all branches ( +- 0.54% )
1.601607384 seconds time elapsed ( +- 0.07% )
启用跳转标签:
'bash -c /tmp/pipe-test' 的性能计数器统计信息(50 次运行):
841.043185 task-clock # 0.533 CPUs utilized ( +- 0.12% )
200,004 context-switches # 0.238 M/sec ( +- 0.00% )
0 CPU-migrations # 0.000 M/sec ( +- 40.87% )
487 page-faults # 0.001 M/sec ( +- 0.05% )
1,432,559,428 cycles # 1.703 GHz ( +- 0.18% )
<not supported> stalled-cycles-frontend
<not supported> stalled-cycles-backend
1,175,363,994 instructions # 0.82 insns per cycle ( +- 0.04% )
206,859,359 branches # 245.956 M/sec ( +- 0.04% )
4,884,119 branch-misses # 2.36% of all branches ( +- 0.85% )
1.579384366 seconds time elapsed
已保存分支的百分比为 0.7%,我们在“分支未命中”上节省了 12%。 这是我们期望获得最多节省的地方,因为这种优化是关于减少分支数量。 此外,我们还节省了 0.2% 的指令、2.8% 的周期和 1.4% 的运行时间。
二、实验
1. 测试demo
#define pr_fmt(fmt) "kfalse_test: " fmt #include <linux/module.h> #include <linux/init.h> #include <linux/module.h> #include <linux/jump_label.h> #if 0 struct static_key false_key = STATIC_KEY_INIT_TRUE; struct static_key true_key = STATIC_KEY_INIT_FALSE; #else struct static_key false_key = STATIC_KEY_INIT_FALSE; struct static_key true_key = STATIC_KEY_INIT_TRUE; #endif static int __init kernel_file_op_init(void) { if (static_key_false(&false_key)) { pr_info("=========1=========="); } else { pr_info("=========2=========="); } static_key_slow_inc(&false_key); if (static_key_false(&false_key)) { pr_info("=========3=========="); } else { pr_info("=========4=========="); } if (static_key_true(&true_key)) { pr_info("=========5=========="); } else { pr_info("=========6=========="); } static_key_slow_dec(&true_key); if (static_key_true(&true_key)) { pr_info("=========7=========="); } else { pr_info("=========8=========="); } return 0; } static void __exit kernel_file_op_exit(void) { pr_info("kernel file operation module exit.\n"); } module_init(kernel_file_op_init); module_exit(kernel_file_op_exit); MODULE_LICENSE("GPL"); /* 这样初始化: struct static_key false_key = STATIC_KEY_INIT_TRUE; struct static_key true_key = STATIC_KEY_INIT_FALSE; 执行打印: kfalse_test: =========2========== kfalse_test: =========4========== kfalse_test: =========5========== kfalse_test: =========7========== 总结:static_key_false()的功能比较正常,static_key_true()的功能不正常! 这样初始化: struct static_key false_key = STATIC_KEY_INIT_FALSE; struct static_key true_key = STATIC_KEY_INIT_TRUE; 执行打印: kfalse_test: =========2========== kfalse_test: =========3========== kfalse_test: =========5========== kfalse_test: =========8========== 相信下面的这个,符合上面static_key_false只能初始化为STATIC_KEY_INIT_FALSE的规范。 */
由实验得出结论,if语句中判别真假时,可理解为 static_key 为真即为真,static_key 为假即为假。
三、CONFIG_JUMP_LABEL 配置宏的作用
CONFIG_JUMP_LABEL 是 Linux 内核的一个性能优化配置项,核心思想是把"条件判断"从运行时 branch 变成可动态修改的 NOP/JMP 指令。
1. 基本原理
传统写法(无 jump label):
if (static_key_enabled(&my_key)) { do_something(); }
每次执行都要读一个全局变量、做条件跳转,即使"几乎永远不走这条路"也一样有开销。启用 CONFIG_JUMP_LABEL 后,编译器把这个 if 变成:
nop //默认不开启时,这里是一条 NOP(无操作)
当需要开启时,内核在运行时直接修改指令流,把那条 NOP 替换成 JMP(或反过来),之后该路径执行时完全没有条件判断开销。
2. 依赖条件
需要编译器和架构支持 asm goto(GCC 4.5+ / Clang); 支持的架构:x86、ARM64、ARM、MIPS、PowerPC、s390 等主流架构;
若不满足条件,自动退化为普通 if 判断,功能不变,只是没有性能优化.
3. 主要用途
内核里大量使用 static_key / static_branch 系列 API,这些都依赖 jump label:
---------------------------------------------------------- 使用场景 举例 ---------------------------------------------------------- tracepoint 默认关闭时零开销 perf/ftrace 未激活时不影响热路径 网络协议特性开关 TCP 扩展选项按需激活 调试/统计功能 生产环境关闭后完全无代价 cgroup/namespace 特性 未使用时不走额外判断 ----------------------------------------------------------
4. 核心数据结构
//include/linux/jump_label.h struct static_key { atomic_t enabled; }; //典型用法 DEFINE_STATIC_KEY_FALSE(my_feature_key); //默认关闭 if (static_branch_unlikely(&my_feature_key)) { //默认路径生成 NOP,开启后变为 JMP } //两个常用变体: static_branch_likely() //默认开启,关闭才少见 static_branch_unlikely() //默认关闭,开启才少见
编译器据此做更好的分支预测提示。
5. 开关代价
切换状态(static_branch_enable/static_branch_disable)本身有开销:
(1) 需要暂停相关 CPU(stop_machine 或架构级同步).
(2) 直接写指令流,涉及 icache 同步.
(3) 适合低频切换,不适合高频切换的场景.
6. 与不开启相比
------------------------------------------------------------------------ CONFIG_JUMP_LABEL=y CONFIG_JUMP_LABEL=n ------------------------------------------------------------------------ 热路径开销 一条 NOP(≈0) 一次内存读 + 条件分支 icache 占用 稍低(NOP vs 分支) 稍高 状态切换代价 高(改指令流) 低(改内存变量) 适用场景 状态稳定、热路径敏感 状态频繁变化或不支持的架构 ------------------------------------------------------------------------
7. 一句话总结
CONFIG_JUMP_LABEL 让内核里大量 "默认不走" 或 "默认必走" 的条件分支在未激活时变成一条 NOP,彻底消除热路径上的判断开销,是内核 tracepoint、perf、网络协议扩展等大量子系统保持 "零成本抽象" 的基础机制。
注: 这些 static key 特性要生效,需要配置使能 CONFIG_JUMP_LABEL 才行!此feature适合判断频繁开关不频繁的场景使用。
posted on 2021-07-18 14:46 Hello-World3 阅读(301) 评论(0) 收藏 举报
浙公网安备 33010602011771号