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 来提高分支预测命中率。
此时的疑问:

  1. 在static_branch_disable(&supports_deactivate_key) 后,static_branch_likely(&supports_deactivate_key)的值还有可能为真吗?
  2. 实现原理是什么?

原理分析

数据结构定义


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 的用法和原理进行总结。

  1. 每一处使用static_key 的地方,先定义一个全局变量,如irq-gic-v3.c 中 static DEFINE_STATIC_KEY_TRUE(supports_deactivate_key);
  2. 需要高频判断的地方使用if (static_branch_likely(&supports_deactivate_key)),static_branch_likely这个函数在编译时,会在__jump_table 段中创建一个jump_entry,并填充了jump_entry 的code, target, key 三个成员变量。
  3. 内核启动时,会遍历__jump_table,从每个jump_entry 中拿到对应的static_key 全局变量,并对初始化static_key 的每一个字段。此时建立了static_key 对jump_entry 的关联,即key->entry = jump_entry
  4. 源码中,需要对这个static_key 进行dis/enable 的位置,会将static_key->enable 置0/1。同时,也就最重要的,会根据对应的jump_entry 修改jump_entry->code 位置的代码,使得static_branch_likely 这个函数仅通过jmp 指令,无需读取变量以及cmp指令,就可以直接返回相应的true 或者false。从而无需判断,无需cpu分支预测,免除了分支预测失败带来的代价
  5. 此后每次使用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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值