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;

// 结构体变量初始化方式,使用两个宏,未初始化的就默认是STATIC_KEY_INIT_FALSE
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);

// 下面这个判断无论是否启用jump label,都只有一种实现。这个条件检查,需要做内存检查。
// 一般使用在控制路径中判断当前状态,选择是否要对static_key做inc或dec操作。
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_falsestatic_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_falsestatic_key_true的意义不是很容易理解,因此先借助未启用jump label时的实现理解一下这两个api

未启用jump label时

从未启用jump label的api看,static_key_{false,true}的返回值逻辑是一样的,均为enabled大于0时返回true。返回值表示了static_key是否启用,也就是当前状态下应该执行的代码分支。通过likelyunlikely,也就是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
// 未启用jump label时,static_key只是atomic_t的一个包装
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_key的inc和dec操作。也就是单纯的对于atomic_t变量的操作了
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) })

关于likelyunlikely的影响,可以对比likelyunlikely时以下代码的反汇编内容,不需要执行,这里不多介绍了。

编译: 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_falsestatic_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
// 启用jump label的static_key
// atomic_t类型成员依然存在
// 多了jump_entry类型指针,指向jump_entry数组,用于遍历该static_key对应的所有分支代码的位置,也就是需要将指令修改为nop或jump的位置。
// 多了static_key_mod类型指针,这是一个链表,用于记录已加载的内核模块中使用该static_key位置和情况。
struct static_key {
atomic_t enabled;
/* Set lsb bit to 1 if branch is default true, 0 ot */
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);
}

/*
* We should be using ATOMIC_INIT() for initializing .enabled, but
* the inclusion of atomic.h is problematic for inclusion of jump_label.h
* in 'low-level' headers. Thus, we are initializing .enabled with a
* raw value, but have added a BUILD_BUG_ON() to catch any issues in
* jump_label_init() see: kernel/jump_label.c.
*/
#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)

// 这里只关注X86_64下
#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;

// 每个jump_entry对应一处分支判断代码
struct jump_entry {
jump_label_t code; // nop指令的地址,也就是翻转分支方向时需要修改为jump的地址
jump_label_t target; // 另一个分支的地址,就是翻转分支方向时jump指令要跳转到的地址
jump_label_t key; // static_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
/* 这个数组可以称为jump_label table或者jump table,虽然没有这个符号 */
extern struct jump_entry __start___jump_table[];
extern struct jump_entry __stop___jump_table[];

/* 对jump table的任何操作都要在这个锁的保护下 */
/* mutex to protect coming/going of the the jump_label table */
static DEFINE_MUTEX(jump_label_mutex);

函数jump_label_initstart_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)                                        
{
// 这两个变量记录jump table的首尾地址
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;

// 编译期检查
/*
* Since we are initializing the static_key.enabled field with
* with the 'raw' int values (to avoid pulling in atomic.h) in
* jump_label.h, let's make sure that is safe. There are only two
* cases to check since we initialize to 0 or 1.
*/
BUILD_BUG_ON((int)ATOMIC_INIT(0) != 0);
BUILD_BUG_ON((int)ATOMIC_INIT(1) != 1);

// 对jump table任何操作都需要上锁jump_label_mutex
jump_label_lock();
// 对jump table中的所有jump_entry结构体以static_key地址做排序。也就是将同一个static_key的所有jump_entry聚集起来
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;
// jump_label_type函数获取分支处应该写入的指令类型,nop或jump
// arch_jump_label_transform_static函数将分支处写入具体指令
arch_jump_label_transform_static(iter, jump_label_type(iterk));
// static_key的entries只需要写一次,iterk与key相同说明该static_key已经写过entries,对应的jump_entry数组在遍历中
if (iterk == key)
continue;

// 遇到了新的static_key,说明遍历到了新的static_key对应的jump_entry数组了
key = iterk;
/*
* Set key->entries to iter, but preserve JUMP_LABEL_TRUE_BRANCH.
*/
// 使用entries记录该static_key对应的jump_entry数组的首地址。entries的最低比特位用于标示默认值,这个最低比特位原值依然保留。
*((unsigned long *)&key->entries) += (unsigned long)iter;
#ifdef CONFIG_MODULES
// 初始化阶段,还没有其他内核模块加载进来,也就没有其他内核模块使用内核内置的static_key
key->next = NULL;
#endif
}
// 该变量用于在运行时检查并警告jump_label_init完成初始化前对static_key发起的操作。
static_key_initialized = true;
jump_label_unlock();
}

static enum jump_label_type jump_label_type(struct static_key *key)
{
// 获取static_key的默认分支,也就是初始化时的分支
bool true_branch = jump_label_get_branch_default(key);
// 获取static_key成员enabled计数,大于0为true,等于0为false,这个是当前应该执行的代码分支
bool state = static_key_enabled(key);

// 默认分支对应的是nop指令的直线路径,如果默认分支与当前应该执行的分支不同,说明需要将分支代码写为jump,也就是启用jump,执行非默认代码分支
if ((!true_branch && state) || (true_branch && !state))
return JUMP_LABEL_ENABLE;

// 默认分支与当前应该执行的分支相同,说明需要将分支代码写为nop,就是关闭jump,执行默认代码分支
return JUMP_LABEL_DISABLE;
}

// 根据传入的参数将对应分支位置写入nop或jump指令
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);

/*
* Make text_poke_bp() a default fallback poker.
*
* At the time the change is being done, just ignore whether we
* are doing nop -> jump or jump -> nop transition, and assume
* always nop being the 'currently valid' instruction
*
*/
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();
// 返回非0,说明旧值非0,因此不需要翻转分支方向,且计数已经增加了。函数返回即可。
if (atomic_inc_not_zero(&key->enabled))
return;
// 上一步返回0,说明旧值为0,因此未增加计数。函数继续运行。

// 对jump table的所有操作都需要上锁
jump_label_lock();
// 如果上锁后读取到的非0,说明执行间隙有其他路径已经改写了计数,操作了分支方向,因此本路径不需要再操作分支方向了。
if (atomic_read(&key->enabled) == 0) {
// 如果默认分支是false,说明nop路径为false分支。当前enabled是0,说明当前是false分支,要从0变成1,就是要启用true分支,需要翻转分支方向,将分支处代码修改为jump,也就是启用jump
// 如果默认分支是true,说明nop路径为true分支。当前enabled是0,说明当前是false分支,要从0变成1,就是要启用true分支,需要翻转分支方向,将分支代码修改为nop,也就是关闭jump
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)
{
// 遍历有两个结束条件,1是遍历到了jump table的末尾,2是遍历到了其他static_key对应的jump_entry
for (; (entry < stop) &&
(entry->key == (jump_label_t)(unsigned long)key);
entry++) {
/*
* entry->code set to 0 invalidates module init text sections
* kernel_text_address() verifies we are not in core kernel
* init code, see jump_label_invalidate_module_init().
*/
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)
{
// jump table的末尾地址,确定数组边界
struct jump_entry *stop = __stop___jump_table;
// 拿到static_key的entries字段,也就是这个static_key对应的jump_entry数组的起始地址,返回地址的最低比特位已经清零了
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 there are no users, entry can be NULL */
// 检查这个static_key是有被使用的
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)
{
// 返回false说明计数减1后仍不为0,也未持有锁。因此不需要翻转分支方向。
// 返回true说明计数减1后已经为0,且持有锁。需要操作分支方向了。
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) {
// rate_limit这个先不管了
atomic_inc(&key->enabled);
schedule_delayed_work(work, rate_limit);
} else {
// 如果默认分支是false,说明nop路径为false分支。当前enabled刚刚从1减到了0,说明需要从true分支变更为false分支,需要将分支处代码修改为nop,也就是关闭jump
// 如果默认分支是true,说明nop路径为true分支。当前enabled刚刚从1减到了0,说明需要从true分支变更为false分支,需要将分支处代码修改为jump,也就是启用jump
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;
/*
* Note:
* To make anonymous unions work with old compilers, the static
* initialization of them requires brackets. This creates a dependency
* on the order of the struct with the initializers. If any fields
* are added, STATIC_KEY_INIT_TRUE and STATIC_KEY_INIT_FALSE may need
* to be modified.
*
* bit 0 => 1 if key is initially true
* 0 if initially false
* bit 1 => 1 if points to struct static_key_mod
* 0 if points to struct jump_entry
*/
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
/*
* Two type wrappers around static_key, such that we can use compile time
* type differentiation to emit the right code.
*
* All the below code is macros in order to play type games.
*/
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 & 1UL;
}

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);

/* See the comment in linux/jump_label.h */
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
/*
* Advanced usage; refcount, branch is enabled when: count != 0
*/

#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)

/*
* Normal usage; boolean enable/disable.
*/

#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)