本节介绍了static_key这个内核基础设施。

理解问题

linux内核中,control cgroup是实现容器的核心技术之一,在control cgroup中有一个叫做cpu cgroup的子系统,它用来限制一组进程的cpu使用量。为了达到限制的目的,就需要对进程单位时间内使用的cpu时间进行统计,而统计会耗费一定的代价,所以内核设计者为了减少系统损耗,只有当系统启用了cpu cgroup且被设置了cpu限制时,采用执行统计cpu时间信息的代码。

内核中有一个全局的变量__cfs_bandwidth_used,初始化时,其值为0

1
2
3
4
5
6
7
8
9
crash> p __cfs_bandwidth_used
__cfs_bandwidth_used = $1 = {
  enabled = {
    counter = 0
  }, 
  entries = 0xffffffffa9521640, 
  next = 0x0
}
crash> 

当创建了一个新的cpu cgroup,并且设置cpu quota,其值加1:

1
2
3
# cd /sys/fs/cgroup/cpu
# mkdir test
# echo 100000 > test/cpu.cfs_quota_us 

查看__cfs_bandwidth_used的值

1
2
3
4
5
6
7
8
crash> p __cfs_bandwidth_used
__cfs_bandwidth_used = $2 = {
  enabled = {
    counter = 1
  }, 
  entries = 0xffffffffa9521640, 
  next = 0x0
}

当取消对该cpu cgoupcpu限制时,该值减少1

1
# echo -1 > test/cpu.cfs_quota_us 

查看__cfs_bandwidth_used的值

1
2
3
4
5
6
7
8
crash> p __cfs_bandwidth_used
__cfs_bandwidth_used = $3 = {
  enabled = {
    counter = 0
  }, 
  entries = 0xffffffffa9521640, 
  next = 0x0
}

在内核代码中,就是通过判断__cfs_bandwidth_used的值是否为0,来决定是否需要执行相关cpu时间的统计代码。

  • __cfs_bandwidth_used的值为0, 不执行时间统计相关代码
  • __cfs_bandwidth_used的值为非0,执行时间统计相关代码

由于调度代码比较核心,在里面判断__cfs_bandwidth_used是否为0也有一定的开销,所以内核开发者为了优化,利用编译器的特性,就有了static_key这个机制了。

static_key机制

简单来说,如果你对代码性能很敏感,而且大多数情况下分支路径是确定的,可以考虑使用static keysstatic keys可以代替普通的变量进行分支判断,目的是用来优化频繁使用if-else判断的问题,这里涉及到指令分支预取的一下问题。简单地说,现代cpu都有预测功能,变量的判断有可能会造成硬件预测失败,影响流水线性能。虽然有likelyunlikely,但还是会有小概率的预测失败。

定义一个static_key

1
struct static_key key = STATIC_KEY_INIT_FALSE; 

注意:这个key及其初始值必须是静态存在的,不能定义为局部变量或者使用动态分配的内存。通常为全局变量或者静态变量。 其中的STATIC_KEY_INIT_FALSE表示这个key的默认值为false,对应的分支默认不进入,如果是需要默认进入的,用STATIC_KEY_INIT_TRUE,这里如果不赋值,系统默认为STATIC_KEY_INIT_FALSE,在代码运行中不能再用STATIC_KEY_INIT_FALSE/STATIC_KEY_INIT_TRUE进行赋值。

判断语句

对于默认为false(STATIC_KEY_INIT_FALSE)的,使用

1
2
3
4
if (static_key_false(&key))
	do unlikely code
else
	do likely code

对于默认为true(STATIC_KEY_INIT_TRUE)的,使用

1
2
3
4
if (static_key_true((&static_key))) 
	do the likely work; 
else 
	do unlikely work

修改判断条件

使用static_key_slow_inc让分支条件变成true,使用static_key_slow_dec让分支条件变成false,与其初始的默认值无关。该接口是带计数的, 也就是:

  • 初始值为STATIC_KEY_INIT_FALSE的,那么: static_key_slow_inc; static_key_slow_inc; static_key_slow_dec 那么 if (static_key_false((&static_key)))对应的分支会进入,而再次static_key_slow_dec后,该分支就不再进入了。
  • 初始值为STATIC_KEY_INIT_TRUE的,那么: static_key_slow_dec; static_key_slow_dec; static_key_slow_inc 那么 if (static_key_true((&static_key)))对应的分支不会进入,而再次static_key_slow_inc后,该分支就进入了。

static-key的内核实现

static_key_false的实现:

X86场景其实现如下,其它架构下的实现类似。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
static __always_inline bool static_key_false(struct static_key *key)
{
        return arch_static_branch(key);
}

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;
}
  • 其中的asm_volatile_goto宏 使用了asm goto,是gcc的特性,其允许在嵌入式汇编中jump到一个C语言的label,详见gccmanual(https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html), 但是本处其作用只是将C语言的label “l_yes”传递到嵌入式汇编中。
  • STATIC_KEY_INITIAL_NOP其实就是NOP指令
  • .pushsection __jump_table是通知编译器,以下的内容写入到段“__jump_table”
  • _ASM_PTR “1b, %l[l_yes], %c0,是往段“__jump_table”中写入label “1b”、C label “l_yes”和输入参数struct static_key *key的地址,这些信息对应于struct jump_entry 中的codetargetkey成员,在后续的处理中非常重要。
  • .popsection表示以下的内容回到之前的段,其实多半就是.text段。

可见,以上代码的作用就是:执行NOP指令后返回false,同时把NOP指令的地址、代码”return true”对应地址、struct static_key *key的地址写入到段“__jump_table”。由于固定返回为false且为always inline,编译器会把

1
2
3
4
if (static_key_false((&static_key))) 
do the unlikely work; 
else 
do likely work 

优化为:

1
2
3
4
5
nop 
do likely work 
retq 
l_yes: 
do the unlikely work; 

正常场景,就没有判断了。

static_key_true的实现:

1
2
3
4
static __always_inline bool static_key_true(struct static_key *key)                                               
{                                    
        return !static_key_false(key);                                                                               
}

执行static_key_slow_inc(&key)后,底层通过gcc提供的goto功能,再结合c代码编写的动态修改内存功能,就可以让使用key的代码从执行false分支变成执行true分支。当然这个更改代价时比较昂贵的,不是所有的情况都适用。

参考文章