限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
本文基于 Linux 4.14
内核源码,以及 ARM32架构
进行分析。
在内核代码中,有很多分支判断条件,它们在绝大多数情形下,都是不成立的,类似如下代码:
if (unlikely(condition)) { /* condition 在极少数情况下才会成立 */
// do something if condition is true
}
尽管我们已经加上unlikely
修饰来进行优化,但是,读取 condition
仍然要访问内存,仍然需要用到cache
;另外,也会CPU分支预测失败。虽然少数这样的代码影响不大,但当这样的条件判断代码(如内核中大量的tracepoint
)增多的时候,将对cache会造成很大压力,所有这些代码导致的cache miss
,以及CPU分支预测
失败,所造成的性能损失,就变得可观起来。因此,内核需要一种方案,来解决这样的问题。这个解决方案,就是本文描述的 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
同时需要编译器
和硬件架构
的支持。
简单来讲,内核 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
,又是依据什么来修改条件判定指令的。
内核用数据结构 struct jump_entry
和 struct 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;
};
};
我们以内核 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.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_NOP
或 JUMP_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;
}
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
,本文未做展开,读者可自行分析。
对于 Jump label
的使用,内核最典型的例子是遍布各处的 tracepoint
,本文不做展开。