static_branch_likely 原理及用法
看irq-gic-v3.c时,发现中断回调中有static_branch_likely 这么一个函数。看起来是为了减少cpu 分支预测失败带来的巨大代价而做的优化。具体原理还没细看。
内核源码中这里有个文档介绍 kernel/Documentation/static-keys.txt。
用法
为了分析实现原理,先从它的用法说起
irq-gic-v3.c 中,
/* supports_deactivate_key 被定义为一个static全局变量 */
static DEFINE_STATIC_KEY_TRUE(supports_deactivate_key);
/* 在__init 代码段中,有如下代码 */
if (!is_hyp_mode_available())
static_branch_disable(&supports_deactivate_key);
/* 在高频调用的函数gic_handle_irq 中,有如下使用 */
if (static_branch_likely(&supports_deactivate_key))
gic_write_eoir(irqnr);
如上,可以看出,基本的用法就是在初始化中对 _key 进行disable 或者enable。在高频if 判断中,使用static_branch_likely 来提高分支预测命中率。
此时的疑问:
- 在static_branch_disable(&supports_deactivate_key) 后,static_branch_likely(&supports_deactivate_key)的值还有可能为真吗?
- 实现原理是什么?
原理分析
数据结构定义
typedef u64 jump_label_t;
/*
* jump_entry 实体是通过函数arch_static_branch 中汇编代码
* .pushsection __jump_table, \"aw\" 定义的,因此也无需额外的初始化
*/
struct jump_entry {
jump_label_t code; /* 需要修改的代码地址 */
jump_label_t target; /* 需要jmp到的代码地址 */
jump_label_t key; /* 匹配健 */
};
struct static_key_mod {
struct static_key_mod *next;
struct jump_entry *entries;
struct module *mod;
};
/* static_key 一般都是全局变量。在内核启动时初始化 */
struct static_key {
atomic_t enabled;
union {
unsigned long type;/*bit[0] 表示被初始化成false(0) 或者true(1);bit[1] 表示指向entries(0), 还是指向next(1) */
struct jump_entry *entries;
struct static_key_mod *next;
};
};
static key 初始化过程
/**/
smp_prepare_boot_cpu()
/* 名字虽然叫jump_label_init,其实更主要的是对每个static_key进行初始化,即填充每个static_key */
jump_label_init()
/* 这里的__start___jump_table 定义在include/asm-generic/vmlinux.lds.h 中 */
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;
if (static_key_initialized)
return;
jump_label_sort_entries(iter_start, iter_stop);
for (iter = iter_start; iter < iter_stop; iter++) {
struct static_key *iterk;
/* rewrite NOPs */
if (jump_label_type(iter) == JUMP_LABEL_NOP)
arch_jump_label_transform_static(iter, JUMP_LABEL_NOP);
iterk = jump_entry_key(iter);
if (iterk == key)
continue;
key = iterk;
static_key_set_entries(key, iter);
type = key->type & JUMP_TYPE_MASK;
key->entries = entries;
key->type |= type;
}
static_key_initialized = true;
去使能
void static_key_disable(struct static_key *key)
/* 这里的cpus_read_lock 是读写sem 锁,每个cpu 独立的(percpu)。这个锁的作用有待分析 */
cpus_read_lock();
static_key_disable_cpuslocked(key);
/* 判断static_key 有没有被初始化*/
STATIC_KEY_CHECK_USE(key);
jump_label_lock();
if (atomic_cmpxchg(&key->enabled, 1, 0)) /*这里*/
jump_label_update(key);
entry = (struct jump_entry *)(key->type & ~JUMP_TYPE_MASK);
/*
* 这里根据key->enabled 的状态,修改key->entry->code 处的代码。
* 改成nop 或者是需要jump 到的位置,也就是key->entry->target。
* 该函数后边将单独展开分析。
*/
__jump_label_update(key, entry, stop);
jump_label_unlock();
cpus_read_unlock();
__jump_label_update 展开分析
__jump_label_update(struct static_key *key, struct jump_entry *entry, struct jump_entry *stop)
enum jump_label_type label_type; /*等效代码,非内核中的原代码*/
label_type = jump_label_type(entry)
key = jump_entry_key(entry);
enabled = static_key_enabled(key);
branch = (unsigned long)entry->key & 1UL;
/*
* 这里是异或操作。即:假如branch == 1 && enabled == 1,将得到label_type = 0,
* 也就是 JUMP_LABEL_NOP,不跳转。
*/
return enabled ^ branch;
arch_jump_label_transform(entry, label_type );
if (type == JUMP_LABEL_JMP)
insn = aarch64_insn_gen_branch_imm(entry->code, entry->target, AARCH64_INSN_BRANCH_NOLINK);
else
insn = aarch64_insn_gen_nop();
/* 将指令写入addr */
aarch64_insn_patch_text_nosync(addr, insn);
判断
#define static_branch_likely(x) \
({ \
bool branch; \
if (__builtin_types_compatible_p(typeof(*x), struct static_key_true)) \ /*这一行在编译时就决定了true/false,因此不会带来分支预测失败的开销*/
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); \
})
static __always_inline bool arch_static_branch(struct static_key *key,
bool branch)
{
asm_volatile_goto(
"1: nop \n\t" /* 就是这个位置,可以通过enable,disable来改指令。从而实现直接返回false 或者true,而无需读取变量值,cpu 也无需执行cmp 指令 */
" .pushsection __jump_table, \"aw\" \n\t" /* 此处往下三行代码被放到了__jump_table 段 */
" .align 3 \n\t"
" .long 1b - ., %l[l_yes] - . \n\t" /* %l 的语法可以参考附录 */
" .quad %c0 - . \n\t"
" .popsection \n\t"
: : "i"(&((char *)key)[branch]) : : l_yes);
return false;
l_yes:
return true;
}
总结
上边展开了很多源码,这里对整个static_key 的用法和原理进行总结。
- 每一处使用static_key 的地方,先定义一个全局变量,如irq-gic-v3.c 中 static DEFINE_STATIC_KEY_TRUE(supports_deactivate_key);
- 需要高频判断的地方使用if (static_branch_likely(&supports_deactivate_key)),static_branch_likely这个函数在编译时,会在__jump_table 段中创建一个jump_entry,并填充了jump_entry 的code, target, key 三个成员变量。
- 内核启动时,会遍历__jump_table,从每个jump_entry 中拿到对应的static_key 全局变量,并对初始化static_key 的每一个字段。此时建立了static_key 对jump_entry 的关联,即key->entry = jump_entry
- 源码中,需要对这个static_key 进行dis/enable 的位置,会将static_key->enable 置0/1。同时,也就最重要的,会根据对应的jump_entry 修改jump_entry->code 位置的代码,使得static_branch_likely 这个函数仅通过jmp 指令,无需读取变量以及cmp指令,就可以直接返回相应的true 或者false。从而无需判断,无需cpu分支预测,免除了分支预测失败带来的代价
- 此后每次使用if(static_branch_likely(&supports_deactivate_key)),就等同于if(true) 或者 if(false),不再会出现分支预测失败。
相关的汇编语法
.pushsection 和.popsection
.pushsection 和.popsection
(1).pushsection伪操作将之前的段设置保存起来,并且将当前的段设置改为名为name的段。即,指明将接下来的代码汇编链接到名为name的段中。
(2).popsection伪操作将最近保存的段设置恢复出来。
(3)通过“.pushsection”和“.popsection”的组合,便可以在汇编程序的编写过程中,在某一个段的汇编代码中特别的插入另外一个段的代码。这种编写方式在某些情况下会给代码编写带来极大的方便
来源://www.jianshu.com/p/3f7387faceef
asm goto 与 %l
asm_volatile_goto 展开后就是asm goto,与asm 不同的是,asm goto提供了第四个“:”,其后是一个c代码中的goto label,汇编中可以跳转到这个lable 去执行。
Alternately, you can reference labels using the actual C label name enclosed in brackets. For example, to reference a label named carry, you can use ‘%l[carry]’. The label must still be listed in the GotoLabels section when using this approach.
Here is an example of asm goto for i386:
asm goto (
"btl %1, %0\n\t"
"jc %l2"
: /* No outputs. */
: "r" (p1), "r" (p2)
: "cc"
: carry);
return 0;
carry:
return 1;
%c0, %c1 … 作用
在上面嵌入式汇编中,出现了%c0 这样的代码。奔跑吧linux读者群里的笨叔的解释如下
我们在 GCC 源代码(gcc9.3.0/ gcc/config/aarch64/aarch64.c, aarch64_print_operand()函数)
中发现“c”操作数修饰符的含义。代码注释为:An integer or symbol address without a preceding
#sign,所以“c”操作数修饰符函数是表示整型立即数或者符号地址。
下面是一个我写的小测试例子,它的目的是打印出 test()函数的符号地址。例子代码:https://github.com/runninglinuxkernel/arm64_programming_practice/blob/main/chapter_10/e
xample_c_operand_modifier/asm_inline_c.c
这里我也简单写了个测试
void test(void)
{
__asm__ (".quad %c1, %c0, %2\n\r"
: : "S" (&test + 4), "i" (&test + 8), "S" (&test + 12));
}
aarch64-linux-gnu-gcc -S test.c 编译(未汇编)后如下:
.arch armv8-a
.file "test.c"
.text
.align 2
.global test
.type test, %function
test:
#APP
// 3 "test.c" 1
.quad test+8, test+4, test+12
// 0 "" 2
#NO_APP
nop
ret
.size test, .-test
.ident "GCC: (Linaro GCC 7.5-2019.12) 7.5.0"
.section .note.GNU-stack,"",@progbits
可以看出,%c[num] 的作用貌似和%[num] 的作用是类似的。具体有什么区别,这里暂时不深究了。
参考
https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
https://mp.weixin.qq.com/s/kivMB0u7ZQFikO6QGA0pcw
https://blog.csdn.net/dog250/article/details/106715700
https://blog.csdn.net/dog250/article/details/6123517