static keys通过gcc特性和代码补丁技术的支持,可以在性能敏感的快速路径内核代码中包含很少使用的特性,使得在该特性不开启的情况下,尽可能降低对正常代码分支运行性能的影响。比如分支预测和缓存交换的性能损失。一句话就是性能损耗小。
static keys内核文档
动机 起初,tracepoints是使用条件分支实现的。条件检查要求为每个tracepoint检查一个全局变量。虽然这种检查的开销很小,但是当缓存承受压力时,开销会增加(这些全局变量可能与其他要访问的内存数据共享cache line)。随着我们在内核中增加跟踪点的数量,这个开销可能会变得更大。此外,tracepoints通常处于休眠状态(未启用),不提供直接的内核功能。因此,尽可能减少它们的影响是很有意义的。尽管tracepoints是这项工作的最初动机,但其他内核代码路径同样能够利用static keys。
方案 gcc(v4.5)增加了一个新的”asm goto”语句,允许在内联汇编语句中跳转到C代码label(也就是内联汇编中可以得到C代码label的地址):https://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html
通过使用”asm goto”,可以在不需要检查内存的情况下,创建出新的分支,无论默认状态下这个分支有没有被启用。之后在运行时,可以为分支位置打补丁修改分支方向。
比如,如果有一个默认状态为禁用的简单分支
1 2 if (static_branch_unlikely(&key)) printk("I am the true branch\n" );
那么,默认状态下这句”printk”是不会被发射的(应该是指不会进到CPU流水线)。生成的代码会是在直线代码路径中的一个原子的”no-op”指令(x86上为5字节)。当这个分支被翻转,这个直线代码路径中的”no-op”指令会被修改为始终跳转到原直线路径之外另一个分支的一个jump指令。虽然修改分支方向成本很高,但是分支选择操作几乎没有损失。这就是static key这个优化的代价和优点。
这里使用的底层补丁机制被称为”jump label patching”,它为static kyes功能提供了基础。
jump label jump label 提供了一个使用自修改代码生成动态分支的接口。假设工具链和体系结构支持,如果我们通过"DEFINE_STATIC_KEY_FALSE(key)"
定义了一个初始值为false的key,那么"if (static_branch_unlikely(&key))"
语句就是一个无条件分支(默认为false,true代码块被放置在直线路径之外)。类似地,我们可以通过"DEFINE_STATIC_KEY_TRUE(key)"
定义一个初始值为true的key,并在相同的"if (static_branch_unlikely(&key))"
中使用它,在这种情况下,我们将生成一个无条件的分支到直线路径之外的true分支。初始值为true或false的key都可以在static_branch_unlikely()
和static_branch_likely()
语句中使用。
运行时可以使用static_branch_enable()
将key设置为true,或使用static_branch_disable()
将key设置为false,以改变分支目标。如果这些调用改变了分支方向,则会在运行时通过no-op -> jump或jump -> no-op的转换修改分支目标。比如,对于在语句"if (static_branch_unlikely(&key))"
中使用的初始化为false的key,如果将key设置为true,则需要打上一个jump补丁指向直线路径之外的true分支。
除了static_branch_{enable,disable}
,我们还可以通过static_branch_{inc,dec}
引用key的计数或分支方向。 因此,static_branch_inc()
可以被认为是’make more true’,而static_branch_dec()
可以被认为是’make more false’。
由于依赖于修改代码, 一定要认识到修改分支的函数操作是非常慢的,比如做整个机器的同步。当然,由于受影响的分支是无条件判断的,运行时的开销会非常低,尤其是在默认的关闭情况下,所有的影响就是一个nop指令空间。开启的情况将会修改为一个跳转到直线路径之外的jump指令。
当控制操作直接向用户空间暴露时,一个明智的做法是延迟操作,以避免会导致明显性能下降的高频代码修改。结构体static_key_deferred
和函数static_key_slow_dec_deferred
提供了这种延迟操作。
当缺少工具链和体系结构支持时,jump label会退化为一个简单的条件分支。
API及实现 目前static keys存在新旧两套API,底层原理是一样的,分别记录一下。
旧版API 参考内核版本:3.10.0-862.el7.x86_64
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct static_key ;struct static_key false = STATIC_KEY_INIT_FALSE ;struct static_key true = STATIC_KEY_INIT_TRUE ;static __always_inline bool static_key_false (struct static_key *key) ;static __always_inline bool static_key_true (struct static_key *key) ;bool static_key_enabled (struct static_key *key) { return (atomic_read(&key->enabled) > 0 ); } void static_key_slow_inc (struct static_key *key) ;void static_key_slow_dec (struct static_key *key) ;
static_key_false
、static_key_true
,这两个api用于分支判断。 返回值的语义是相同的,都表示当前static_key
是启用还是关闭,返回true表示static_key
启用(也就是enabled大于0),返回false表示static_key
关闭(也就是enabled等于0)。 不同之处在于影响了编译生成的代码布局。static_key_false
会将false的代码分支置于直线路径,static_key_true
会true的代码分支置于直线路径。
当然这都是正确使用的情况下,正确使用就是:
static_key_false
应该与STATIC_KEY_INIT_FALSE
一起使用
static_key_true
应该与STATIC_KEY_INIT_TRUE
一起使用
错配使用会有不良后果,下面会详细说明。
这些api中static_key_false
和static_key_true
的意义不是很容易理解,因此先借助未启用jump label时的实现理解一下这两个api
未启用jump label时 从未启用jump label的api看,static_key_{false,true}
的返回值逻辑是一样的,均为enabled大于0时返回true。返回值表示了static_key
是否启用,也就是当前状态下应该执行的代码分支。通过likely
和unlikely
,也就是gcc内建函数__builtin_expect
,指导编译器对生成的指令布局做优化。
static_key_false
用于该static_key
大概率是关闭(对应初始化为关闭)的情况,该api返回false对应的代码分支在编译生成的二进制代码中是直线路径的。对应的初始化宏STATIC_KEY_INIT_FALSE
。
static_key_true
用于该static_key
大概率是开启(对应初始化为开启)的情况,该api返回true对应的代码分支在编译生成的二进制代码中是直线路径的。对应的初始化宏STATIC_KEY_INIT_TRUE
。
对于同一个static_key
,这两个api是不应当混用的,初始化的值应该与大概率的分支匹配。如果错配使用,由于二进制代码中条件判断还在,只是损失性能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 struct static_key { atomic_t enabled; }; static __always_inline bool static_key_false (struct static_key *key) { if (unlikely(atomic_read(&key->enabled)) > 0 ) return true ; return false ; } static __always_inline bool static_key_true (struct static_key *key) { if (likely(atomic_read(&key->enabled)) > 0 ) return true ; return false ; } static inline void static_key_slow_inc (struct static_key *key) { STATIC_KEY_CHECK_USE(); atomic_inc(&key->enabled); } static inline void static_key_slow_dec (struct static_key *key) { STATIC_KEY_CHECK_USE(); atomic_dec(&key->enabled); } #define STATIC_KEY_INIT_TRUE ((struct static_key) \ { .enabled = ATOMIC_INIT(1 ) }) #define STATIC_KEY_INIT_FALSE ((struct static_key) \ { .enabled = ATOMIC_INIT(0 ) })
关于likely
和unlikely
的影响,可以对比likely
和unlikely
时以下代码的反汇编内容,不需要执行,这里不多介绍了。
编译: gcc -O2 main.c
反汇编: objdump -d a.out
main.c 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <stdlib.h> # define likely(x) __builtin_expect(!!(x), 1) # define unlikely(x) __builtin_expect(!!(x), 0) int main (int argc, char **argv) { int i; i = atoi(argv[1 ]); if (likely(i != 2 )) { i++; } printf ("i %d\n" , i); return 0 ; }
已启用jump label时 分支代码 启用jump label时,static_key_false
和static_key_true
实现上均通过arch_static_branch
做分支选择。
这两个api的语义和使用场景,与未启用jump label时是一样的,可以参考上面。对于同一个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
的值相反。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct static_key { atomic_t enabled; struct jump_entry *entries ; #ifdef CONFIG_MODULES struct static_key_mod *next ; #endif }; static __always_inline bool static_key_false (struct static_key *key) { return arch_static_branch(key); } static __always_inline bool static_key_true (struct static_key *key) { return !static_key_false(key); } #define STATIC_KEY_INIT_TRUE ((struct static_key) \ { .enabled = { 1 }, .entries = (void *)1 }) #define STATIC_KEY_INIT_FALSE ((struct static_key) \ { .enabled = { 0 }, .entries = (void *)0 })
arch_static_branch
的实现如下,这里有一段使用了asm goto的内联汇编代码
当前地址生成5字节的nop指令
切换到段__jump_table,向其中推送3个64位数
5字节nop指令的地址
C代码中l_yes这个label的地址,这个地址对应了返回true的分支
key的值,也就是static_key
结构体的地址
从段__jump_table返回到之前的段
这段内联汇编结束后,返回false。编译器会根据该函数永远返回false将false分支的代码优化到直线路径中。 因此,static_key_false
返回false对应的代码分支会位于直线路径中,static_key_true
返回true对应的代码分支会位于直线路径中,与未启用jump label的版本逻辑一致。
arch_static_branch 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #define asm_volatile_goto(x...) do { asm goto(x); asm ("" ); } while (0) #define P6_NOP5 0x0f,0x1f,0x44,0x00,0 #define P6_NOP5_ATOMIC P6_NOP5 # define STATIC_KEY_INIT_NOP P6_NOP5_ATOMIC static __always_inline bool arch_static_branch (struct static_key *key) { asm_volatile_goto("1:" ".byte " __stringify(STATIC_KEY_INIT_NOP) "\n\t" ".pushsection __jump_table, \"aw\" \n\t" _ASM_ALIGN "\n\t" _ASM_PTR "1b, %l[l_yes], %c0 \n\t" ".popsection \n\t" : : "i" (key) : : l_yes); return false ; l_yes: return true ; }
还有一个对应的汇编版本,逻辑是一样的。
1 2 3 4 5 6 7 8 .macro STATIC_JUMP target, key .Lstatic_jump_\@: .byte STATIC_KEY_INIT_NOP .pushsection __jump_table, "aw" _ASM_ALIGN _ASM_PTR .Lstatic_jump_\@, \target, \key .popsection .endm
一个正确使用static_key_false的例子,第二个printk未在直线路径中。
一个正确使用static_key_true的例子,第二个printk在直线路径中。 static_key的entries成员最低比特位是1表示默认值为真,实际使用该指针时需要将最低比特位修改为0使用。
一个错误使用static_key_true与STATIC_KEY_INIT_FALSE的例子 可以看到log_key初始化是关闭的,且未经过翻转分支方向,保持着关闭状态。 但由于static_key_true的使用,第二个printk位于直线路径中,且由于5字节nop指令的原因,这第二个printk会被执行。这与log_key的状态是相反的。
jump table 初始化 前面提到每个用到arch_static_branch
函数的地方都向段__jump_table
中推送了3个64位数,对应的结构体jump_entry
如下
jump_entry 1 2 3 4 5 6 7 8 typedef u64 jump_label_t ;struct jump_entry { jump_label_t code; jump_label_t target; jump_label_t key; };
链接器脚本头文件include/asm-generic/vmlinux.lds.h中将所有目标文件中__jump_table
段的内容按链接顺序集合到段.data中,也就是存储了一个jump_entry
类型结构体数组,并且定义了两个符号,__start___jump_table
和__stop___jump_table
,用于记录这个结构体数组的起始地址和结束地址。
1 2 3 4 5 6 7 extern struct jump_entry __start___jump_table [];extern struct jump_entry __stop___jump_table [];static DEFINE_MUTEX (jump_label_mutex) ;
函数jump_label_init
在start_kernel
中被调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 void __init jump_label_init (void ) { 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 ; BUILD_BUG_ON((int )ATOMIC_INIT(0 ) != 0 ); BUILD_BUG_ON((int )ATOMIC_INIT(1 ) != 1 ); jump_label_lock(); jump_label_sort_entries(iter_start, iter_stop); for (iter = iter_start; iter < iter_stop; iter++) { struct static_key *iterk ; iterk = (struct static_key *)(unsigned long )iter->key; arch_jump_label_transform_static(iter, jump_label_type(iterk)); if (iterk == key) continue ; key = iterk; *((unsigned long *)&key->entries) += (unsigned long )iter; #ifdef CONFIG_MODULES key->next = NULL ; #endif } static_key_initialized = true ; jump_label_unlock(); } static enum jump_label_type jump_label_type (struct static_key *key) { bool true_branch = jump_label_get_branch_default(key); bool state = static_key_enabled(key); if ((!true_branch && state) || (true_branch && !state)) return JUMP_LABEL_ENABLE; return JUMP_LABEL_DISABLE; } static void __jump_label_transform(struct jump_entry *entry, enum jump_label_type type, void *(*poker)(void *, const void *, size_t )) { union jump_code_union code; if (type == JUMP_LABEL_ENABLE) { code.jump = 0xe9 ; code.offset = entry->target - (entry->code + JUMP_LABEL_NOP_SIZE); } else memcpy (&code, ideal_nops[NOP_ATOMIC5], JUMP_LABEL_NOP_SIZE); if (poker) (*poker)((void *)entry->code, &code, JUMP_LABEL_NOP_SIZE); else text_poke_bp((void *)entry->code, &code, JUMP_LABEL_NOP_SIZE, (void *)entry->code + JUMP_LABEL_NOP_SIZE); } void arch_jump_label_transform (struct jump_entry *entry, enum jump_label_type type) { get_online_cpus(); mutex_lock(&text_mutex); __jump_label_transform(entry, type, NULL ); mutex_unlock(&text_mutex); put_online_cpus(); } __init_or_module void arch_jump_label_transform_static (struct jump_entry *entry, enum jump_label_type type) { __jump_label_transform(entry, type, text_poke_early); }
分支控制 static_key_slow_inc,这个api用于增加static_key的计数,计数可以累计,大于0时代码执行true分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void static_key_slow_inc (struct static_key *key) { STATIC_KEY_CHECK_USE(); if (atomic_inc_not_zero(&key->enabled)) return ; jump_label_lock(); if (atomic_read(&key->enabled) == 0 ) { if (!jump_label_get_branch_default(key)) jump_label_update(key, JUMP_LABEL_ENABLE); else jump_label_update(key, JUMP_LABEL_DISABLE); } atomic_inc(&key->enabled); jump_label_unlock(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 static void __jump_label_update(struct static_key *key, struct jump_entry *entry, struct jump_entry *stop, int enable) { for (; (entry < stop) && (entry->key == (jump_label_t )(unsigned long )key); entry++) { if (entry->code) { if (kernel_text_address(entry->code)) arch_jump_label_transform(entry, enable); else WARN(1 , "can't patch jump_label at 0x%lx\n" , (unsigned long )entry->code); } } } static void jump_label_update (struct static_key *key, int enable) { struct jump_entry *stop = __stop___jump_table ; struct jump_entry *entry = jump_label_get_entries (key ); #ifdef CONFIG_MODULES struct module *mod = __module_address ((unsigned long )key ); __jump_label_mod_update(key, enable); if (mod) stop = mod->jump_entries + mod->num_jump_entries; #endif if (entry) __jump_label_update(key, entry, stop, enable); }
static_key_slow_dec,这个api用于减少static_key的计数,计数可以累计,但是不应该小于0,大于0时代码执行true分支。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 static void __static_key_slow_dec(struct static_key *key, unsigned long rate_limit, struct delayed_work *work) { if (!atomic_dec_and_mutex_lock(&key->enabled, &jump_label_mutex)) { WARN(atomic_read(&key->enabled) < 0 , "jump label: negative count!\n" ); return ; } if (rate_limit) { atomic_inc(&key->enabled); schedule_delayed_work(work, rate_limit); } else { if (!jump_label_get_branch_default(key)) jump_label_update(key, JUMP_LABEL_DISABLE); else jump_label_update(key, JUMP_LABEL_ENABLE); } jump_label_unlock(); } void static_key_slow_dec (struct static_key *key) { STATIC_KEY_CHECK_USE(); __static_key_slow_dec(key, 0 , NULL ); }
新版API 参考内核版本:4.18.0-193.el8.x86_64
新版本内核中,不推荐使用旧版api,包括
1 2 3 4 struct static_key false = STATIC_KEY_INIT_FALSE ;struct static_key true = STATIC_KEY_INIT_TRUE ;static_key_true() static_key_false()
推荐使用新版api,比如
1 2 3 4 5 6 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()
前面提到旧版的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时运行。
看一下新版api做了什么
static_key
结构体布局有了一点小变动,为了在没有外部模块引用该static_key
时减少一个next指针的内存占用,如果有外部模块引用就额外分配一个static_key_mod
结构体用来存储原始的entries,具体可以参考函数jump_label_add_module
的实现,这个变动不影响对新版api的理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 struct static_key { atomic_t enabled; union { unsigned long type; struct jump_entry *entries ; struct static_key_mod *next ; }; };
重点是增加了两个包装的结构体类型,推荐的宏定义通过使用这两个结构体类型,可以在编译时识别到static_key
初始化值的分支是true还是false,并记录到jump_entry的key成员最低比特位上用于运行时识别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct static_key_true { struct static_key key ; }; struct static_key_false { struct static_key key ; }; #define STATIC_KEY_TRUE_INIT (struct static_key_true) { .key = STATIC_KEY_INIT_TRUE, } #define STATIC_KEY_FALSE_INIT (struct static_key_false){ .key = STATIC_KEY_INIT_FALSE, } #define DEFINE_STATIC_KEY_TRUE(name) \ struct static_key_true name = STATIC_KEY_TRUE_INIT #define DEFINE_STATIC_KEY_FALSE (name ) \ struct static_key_false name = STATIC_KEY_FALSE_INIT
比如:
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分支
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #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(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(branch); \ })
jump_label_type
函数依然用于判断分支处应该使用nop还是jump,其实现方式也对应变更了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #define static_key_enabled(x) \ ({ \ if (!__builtin_types_compatible_p(typeof(*x), struct static_key) && \ !__builtin_types_compatible_p(typeof(*x), struct static_key_true) &&\ !__builtin_types_compatible_p(typeof(*x), struct static_key_false)) \ ____wrong_branch_error(); \ static_key_count((struct static_key *)x) > 0 ; \ }) static inline bool jump_entry_is_branch (const struct jump_entry *entry) { return (unsigned long )entry->key & 1U L; } static enum jump_label_type jump_label_type (struct jump_entry *entry) { struct static_key *key = jump_entry_key (entry ); bool enabled = static_key_enabled(key); bool branch = jump_entry_is_branch(entry); return enabled ^ branch; }
static_key_{false,true}
这两个旧api的实现也兼容了新的实现方式。逻辑与旧版实现一致。依然不支持与错配初始化值使用。
1 2 3 4 5 6 7 8 9 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 ); }
有两组分支控制操作,区别在于是否支持计数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #define static_branch_inc(x) static_key_slow_inc(&(x)->key) #define static_branch_dec(x) static_key_slow_dec(&(x)->key) #define static_branch_inc_cpuslocked(x) static_key_slow_inc_cpuslocked(&(x)->key) #define static_branch_dec_cpuslocked(x) static_key_slow_dec_cpuslocked(&(x)->key) #define static_branch_enable(x) static_key_enable(&(x)->key) #define static_branch_disable(x) static_key_disable(&(x)->key) #define static_branch_enable_cpuslocked(x) static_key_enable_cpuslocked(&(x)->key) #define static_branch_disable_cpuslocked(x) static_key_disable_cpuslocked(&(x)->key)