当前位置: 首页 > 工具软件 > jump > 使用案例 >

Linux: Jump label实现简析

戚阳
2023-12-01

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文基于 Linux 4.14 内核源码,以及 ARM32架构 进行分析。

3. Jump label 实现

3.1 为什么引入 Jump label?

在内核代码中,有很多分支判断条件,它们在绝大多数情形下,都是不成立的,类似如下代码:

if (unlikely(condition)) { /* condition 在极少数情况下才会成立 */
	// do something if condition is true
}

尽管我们已经加上unlikely修饰来进行优化,但是,读取 condition 仍然要访问内存,仍然需要用到cache;另外,也会CPU分支预测失败。虽然少数这样的代码影响不大,但当这样的条件判断代码(如内核中大量的tracepoint)增多的时候,将对cache会造成很大压力,所有这些代码导致的cache miss,以及CPU分支预测失败,所造成的性能损失,就变得可观起来。因此,内核需要一种方案,来解决这样的问题。这个解决方案,就是本文描述的 Jump label

3.2 实现简析

3.2.1 Jump label 实现的前提条件

Jump label 的实现,是需要一定前提条件的。我们先看一下 Jump label 相关的编译配置:

/* @include/linux/jump_label.h */

/* CC_HAVE_ASM_GOTO 表示,编译器需支持 asm goto */
#if defined(CC_HAVE_ASM_GOTO) && defined(CONFIG_JUMP_LABEL)
# define HAVE_JUMP_LABEL
#endif

再看一下架构通用配置文件 arch/Kconfig

config JUMP_LABEL
       bool "Optimize very unlikely/likely branches"
       depends on HAVE_ARCH_JUMP_LABEL
	   ...

config HAVE_ARCH_JUMP_LABEL
        bool

ARM32 架构配置文件 arch/arm/Kconfig

config ARM
	...
	select HAVE_ARCH_JUMP_LABEL if !XIP_KERNEL && !CPU_ENDIAN_BE32 && MMU

的相关内容。由上可见,Jump label 同时需要编译器硬件架构的支持。

3.2.2 Jump label 实现原理

简单来讲,内核 Jump label 的实现,就是通过运行时动态修改指令,将条件判定代码

if (unlikely(condition)) { /* condition 在极少数情况下才会成立 */
	// do something if condition is true
}

优化为等同于下列两种情形之一:

if (true) {
	// do someting if condition == true
}

if (false) {
	// 此处代码不会被执行,自然也不会带来任何开销
}

注意,这和将代码固定编写为 if (true)if (false) 然后编译是两回事,因为内核是通过动态修改指令来完成的。接下来,我们来看具体的实现,看内核是如何实现 Jump label ,又是依据什么来修改条件判定指令的。

3.2.3 Jump label 实现数据结构

内核用数据结构 struct jump_entrystruct static_key 来描述 Jump label
struct jump_entry 实现是硬件架构相关的:

/* @arch/arm/include/asm/jump_label.h */

typedef u32 jump_label_t;

struct jump_entry {
	jump_label_t code; /* 被内核动态修改的 nop 或 跳转指令 地址 */
	jump_label_t 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 )
	 */
	jump_label_t key;
};

struct static_key 是硬件架构实现无关的:

/* @include/linux/jump_label.h */

struct static_key {
	atomic_t enabled;
	/* 
	 * 看到 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;
	};
};

3.2.4 Jump label 实现代码

3.2.4.1 Jump label 构建

我们以内核 cfs 调度器带宽控制开关为例,来看如何构建 Jump label (包含一个 struct jump_entry):

/* @kernel/sched/fair.c */

static struct static_key __cfs_bandwidth_used; /* 调度带宽控制 */

static inline bool cfs_bandwidth_used(void)
{
	return static_key_false(&__cfs_bandwidth_used);
}

static_key_false() 实现:

/* @include/linux/jump_label.h */

static __always_inline bool static_key_false(struct static_key *key)
{
	return arch_static_branch(key, false);
}

static_key_false() 调用架构相关接口 arch_static_branch(),看一下 ARM32 架构的实现:

/* @arch/arm/include/asm/jump_label.h */

static __always_inline bool arch_static_branch(struct static_key *key, bool branch)
{
	asm_volatile_goto("1:\n\t"
		// 运行时被动态修改的指令,或修改为 nop,或修改为 跳转到 l_yes 标号的指令
		 WASM(nop) "\n\t" 
		 // 在一个名为 __jump_table 的 section 内,构建 1 个
		 // jump_entry 变量:
		 // (struct jump_entry){ 
		 //		.code = 上面标号1的地址,即运行是被动态修改指令的地址
		 // 	.target = 标号 l_yes 地址
		 //		.key = key 地址,用最低 1 位标记 branch 类型: false, true
		 // }
		 // 
		 ".pushsection __jump_table,  \"aw\"\n\t"
		 ".word 1b, %l[l_yes], %c0\n\t"
		 ".popsection\n\t"
		 : :  "i" (&((char *)key)[branch]) :  : l_yes);

	return false;
l_yes:
	return true;
}

上面我们分析了用来构建 Jump label 的第1个接口 static_key_false() ,内核还有第2个用来构建 Jump label 的接口 static_key_true()

static __always_inline bool static_key_true(struct static_key *key)
{
	return !arch_static_branch(key, true);
}

static_key_true()static_key_false() 有着相似的逻辑,在此不做展开。
对于函数对 static_key_*() 前的 __always_inline 修饰符,还得额外说明一下, __always_inline 是告诉编译器,函数代码必须就地展开。在我们的场景下,不加__always_inline 修饰行不行?答案是不行。因为函数 arch_static_branch() 里面使用了标号 1标号 l_yes 的地址,为了保证每个 struct jump_entry 记录的标号地址唯一,我们必须要就地展开函数代码。

3.2.4.2 Jump label 初始化

在 3.2.4.1 中,所有的 struct jump_entry 都放在名为 __jump_table 的 section 内,在链接阶段,内核链接脚本片段

. = ALIGN(8);                                                   
VMLINUX_SYMBOL(__start___jump_table) = .;                       
KEEP(*(__jump_table))                                           
VMLINUX_SYMBOL(__stop___jump_table) = .;                        
. = ALIGN(8);

将所有的 __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()
	jump_label_init()
		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;
		
		...
		/* 
	 	 * 将所有的 jump_entry (即 "__jump_table" 段),  
	 	 * 按关联 static_key 的地址【升序排列】。
	 	 */
	jump_label_sort_entries(iter_start, iter_stop);
	
	for (iter = iter_start; iter < iter_stop; iter++) {
		struct static_key *iterk;
	
		if (jump_label_type(iter) == JUMP_LABEL_NOP) /* 如果 jump_entry 为 JUMP_LABEL_NOP 类型 */
			/* 替换地址 @iter->entries->code 处的指令为 nop */
			arch_jump_label_transform_static(iter, JUMP_LABEL_NOP)
				__arch_jump_label_transform(entry, type, true)
					insn = arm_gen_nop(); /* 构建 nop 指令 */
					__patch_text_early(addr, insn)
						__patch_text_real(addr, insn, false)
							*(u32 *)addr = insn; /* 覆写地址 @addr 处原有指令 */
							flush_icache_range((uintptr_t)(addr), (uintptr_t)(addr) + size) /* 地址 @addr 处指令可能之前有被cache,刷一下 icache 保证同步 */

		iterk = jump_entry_key(iter);
		/*
		 * 由于前面已经按 jump_entry::key 值给 jump_entry 列表排序,
		 * 所以这是【连续】几个 jump_entry (的 key) 指向同一 static_key 对象。
		 * 此时,将第 1 个 jump_entry 关联到该 static_key 对象。
		 */
		if (iterk == key)
			continue;

		key = iterk;
		/* 
		 * 设定 static_key @key 关联 jump_entry @iter 的 【地址】 和 【类型】
		 */
		static_key_set_entries(key, iter)
			type = key->type & JUMP_TYPE_MASK;
			key->entries = entries; /* 记录关联 jump_entry 的地址 */
			key->type |= type; /* 低 2 位记录关联 jump_entry 的类型:JUMP_LABEL_NOP 或 JUMP_LABEL_JMP */
	}
	static_key_initialized = true;

在上面的代码中,我们对函数 jump_label_type()jump_entry_key() 未作展开,这是两个很关键的函数,我们有必要对它们进行展开。
jump_entry_key() 用来返回关联 struct static_key 对象(即 jump_entry_key::key 指向的 struct static_key 对象)的首地址。来看它的具体实现:

static inline struct static_key *jump_entry_key(struct jump_entry *entry)
{
	/*
	 * 注意到: 
	 * static_key_true()
	 *		arch_static_branch(key, true)
	 *			asm_volatile_goto("1:\n\t"
	 *	 			WASM(nop) "\n\t"
	 *	 			".pushsection __jump_table,  \"aw\"\n\t"
	 *	 			".word 1b, %l[l_yes], %c0\n\t"
	 *	 			".popsection\n\t"
	 *	 			: :  "i" (&((char *)key)[branch]) :  : l_yes);
	 * 其中 branch == true, 此时 entry->key 的值是 static_key 对象的首地址 +1(也即一个奇数地址),
	 * 在这种情形下,如果返回 entry->key 的值,则不是 static_key 对象的首地址,
	 * 此处的 ((unsigned long)entry->key & ~1UL) 保证了返回 static_key 对象的首地址。
	 * 当然,这暗示 static_key 对象的首地址是一个偶数值。
	 * 
	 * static_key_false() 的情形,entry->key 的值正好是 static_key 对象的首地址(是一个偶数地址),无论是否经过修正,
	 * 返回的都是正确的地址。
	 */
	return (struct static_key *)((unsigned long)entry->key & ~1UL);
}

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);
	bool enabled = static_key_enabled(key); /* key->enabled > 0 ? true : false */
	bool branch = jump_entry_branch(entry); /* (unsigned long)entry->key & 1UL, 参看 struct jump_entry 的注释 */

	/* See the comment in linux/jump_label.h */
	return enabled ^ branch; 
}

3.2.4.3 Jump label 更新

Jump label 更新,是指动态修改 Jump label 处指令,让判定条件固定成立(if (true))或失败(if (false))。最典型的例子 tracepoint 的启用和禁用。更新动作通过头文件 include/linux/jump_label.h 头文件导出的接口发起:

extern void arch_jump_label_transform(struct jump_entry *entry,
				      enum jump_label_type type);
extern void arch_jump_label_transform_static(struct jump_entry *entry,
					     enum jump_label_type type);

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 void jump_label_apply_nops(struct module *mod);
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);

上述接口,除头2个是直接按指定类型修改之另外,其它的都要调用函数 jump_label_update() ,我们来看一下它的实现:

static void jump_label_update(struct static_key *key)
{
	struct jump_entry *stop = __stop___jump_table;
	struct jump_entry *entry;

	entry = static_key_entries(key);
	__jump_label_update(key, entry, stop)
		for (; (entry < stop) && (jump_entry_key(entry) == key); entry++) {
			if (entry->code && kernel_text_address(entry->code))
				arch_jump_label_transform(entry, jump_label_type(entry)) /* 从这里开始,和头文件导出的第1个接口殊途同归了 */
		}
}

有了前面的分析,相信这段代码也不难理解了。
我们内核模块的 Jump label ,本文未做展开,读者可自行分析。

4. Jump label 的应用场景

对于 Jump label 的使用,内核最典型的例子是遍布各处的 tracepoint ,本文不做展开。

5. 参考资料

Jump label
Jump label reworked

 类似资料: