静态键

警告

已弃用的 API

直接使用 ‘struct static_key’ 现在已弃用。此外,static_key_{true,false}() 也已弃用。即不要使用以下内容

struct static_key false = STATIC_KEY_INIT_FALSE;
struct static_key true = STATIC_KEY_INIT_TRUE;
static_key_true()
static_key_false()

更新后的 API 替代方案是

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

摘要

静态键允许通过 GCC 功能和代码修补技术,将性能敏感的快速路径内核代码中不常用的功能包含进来。一个简单的例子

DEFINE_STATIC_KEY_FALSE(key);

...

if (static_branch_unlikely(&key))
        do unlikely code
else
        do likely code

...
static_branch_enable(&key);
...
static_branch_disable(&key);
...

static_branch_unlikely() 分支将生成到代码中,尽可能减少对可能代码路径的影响。

动机

目前,跟踪点是使用条件分支实现的。条件检查需要检查每个跟踪点的全局变量。虽然这种检查的开销很小,但当内存缓存承受压力时(这些全局变量的内存缓存行可能与其他内存访问共享),这种开销会增加。随着我们增加内核中跟踪点的数量,这种开销可能会变得更加严重。此外,跟踪点通常处于休眠状态(禁用),不提供直接的内核功能。因此,非常希望尽可能减少它们的影响。虽然跟踪点是这项工作的最初动机,但其他内核代码路径也应该能够利用静态键功能。

解决方案

gcc (v4.5) 添加了一个新的 ‘asm goto’ 语句,允许跳转到标签

https://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html

使用 ‘asm goto’,我们可以创建默认情况下要么被采用要么不被采用的分支,而无需检查内存。然后,在运行时,我们可以修补分支点以更改分支方向。

例如,如果我们有一个默认情况下禁用的简单分支

if (static_branch_unlikely(&key))
        printk("I am the true branch\n");

因此,默认情况下不会发出 ‘printk’。生成的代码将在直线代码路径中包含单个原子 ‘no-op’ 指令(x86 上为 5 个字节)。当分支被 ‘翻转’ 时,我们将使用一个 ‘跳转’ 指令来修补直线代码路径中的 ‘no-op’,以跳转到行外的 true 分支。因此,更改分支方向是昂贵的,但分支选择基本上是 ‘免费’ 的。这就是这种优化的基本权衡。

这种低级修补机制称为 ‘跳转标签修补’,它为静态键功能提供了基础。

静态键标签 API、用法和示例

为了利用这种优化,您必须首先定义一个键

DEFINE_STATIC_KEY_TRUE(key);

或者

DEFINE_STATIC_KEY_FALSE(key);

该键必须是全局的,也就是说,它不能在堆栈上分配,也不能在运行时动态分配。

然后,该键在代码中按以下方式使用

if (static_branch_unlikely(&key))
        do unlikely code
else
        do likely code

if (static_branch_likely(&key))
        do likely code
else
        do unlikely code

通过 DEFINE_STATIC_KEY_TRUE() 或 DEFINE_STATIC_KEY_FALSE 定义的键,可以在 static_branch_likely() 或 static_branch_unlikely() 语句中使用。

可以通过以下方式将分支设置为 true

static_branch_enable(&key);

或通过以下方式设置为 false

static_branch_disable(&key);

然后可以通过引用计数来切换分支

static_branch_inc(&key);
...
static_branch_dec(&key);

因此,‘static_branch_inc()’ 表示 ‘使分支为 true’,而 ‘static_branch_dec()’ 表示 ‘使分支为 false’,并带有适当的引用计数。例如,如果该键初始化为 true,则 static_branch_dec() 会将分支切换为 false。随后,static_branch_inc() 会将分支改回 true。同样,如果该键初始化为 false,则 ‘static_branch_inc()’ 会将分支更改为 true。然后,‘static_branch_dec()’ 会再次使分支为 false。

可以使用 ‘static_key_enabled()’ 和 ‘static_key_count()’ 来检索状态和引用计数。通常,如果您使用这些函数,则应使用围绕启用/禁用或递增/递减函数使用的相同互斥锁来保护它们。

请注意,切换分支会导致获取一些锁,特别是 CPU 热插拔锁(以避免在内核被修补时与内核中引入的 CPU 竞争)。因此,从热插拔通知器内部调用静态键 API 肯定会导致死锁。为了仍然允许使用该功能,提供了以下函数

static_key_enable_cpuslocked() static_key_disable_cpuslocked() static_branch_enable_cpuslocked() static_branch_disable_cpuslocked()

这些函数不是通用的,只有当您确实知道自己处于上述上下文中时才必须使用,而没有其他情况。

如果需要键数组,可以定义为

DEFINE_STATIC_KEY_ARRAY_TRUE(keys, count);

或者

DEFINE_STATIC_KEY_ARRAY_FALSE(keys, count);
  1. 架构级别代码修补接口,“跳转标签”

架构必须实现一些函数和宏,才能利用这种优化。如果没有架构支持,我们只需回退到传统的加载、测试和跳转序列。此外,struct jump_entry 表必须至少 4 字节对齐,因为 static_key->entry 字段使用了两个最低有效位。

  • select HAVE_ARCH_JUMP_LABEL,

    参见:arch/x86/Kconfig

  • #define JUMP_LABEL_NOP_SIZE,

    参见:arch/x86/include/asm/jump_label.h

  • __always_inline bool arch_static_branch(struct static_key *key, bool branch),

    参见:arch/x86/include/asm/jump_label.h

  • __always_inline bool arch_static_branch_jump(struct static_key *key, bool branch),

    参见:arch/x86/include/asm/jump_label.h

  • void arch_jump_label_transform(struct jump_entry *entry, enum jump_label_type type),

    参见:arch/x86/kernel/jump_label.c

  • struct jump_entry,

    参见:arch/x86/include/asm/jump_label.h

  1. 静态键 / 跳转标签分析,结果 (x86_64)

例如,让我们将以下分支添加到 ‘getppid()’,以便现在系统调用看起来像

SYSCALL_DEFINE0(getppid)
{
      int pid;

+     if (static_branch_unlikely(&key))
+             printk("I am the true branch\n");

      rcu_read_lock();
      pid = task_tgid_vnr(rcu_dereference(current->real_parent));
      rcu_read_unlock();

      return pid;
}

GCC 生成的带跳转标签的指令如下

ffffffff81044290 <sys_getppid>:
ffffffff81044290:       55                      push   %rbp
ffffffff81044291:       48 89 e5                mov    %rsp,%rbp
ffffffff81044294:       e9 00 00 00 00          jmpq   ffffffff81044299 <sys_getppid+0x9>
ffffffff81044299:       65 48 8b 04 25 c0 b6    mov    %gs:0xb6c0,%rax
ffffffff810442a0:       00 00
ffffffff810442a2:       48 8b 80 80 02 00 00    mov    0x280(%rax),%rax
ffffffff810442a9:       48 8b 80 b0 02 00 00    mov    0x2b0(%rax),%rax
ffffffff810442b0:       48 8b b8 e8 02 00 00    mov    0x2e8(%rax),%rdi
ffffffff810442b7:       e8 f4 d9 00 00          callq  ffffffff81051cb0 <pid_vnr>
ffffffff810442bc:       5d                      pop    %rbp
ffffffff810442bd:       48 98                   cltq
ffffffff810442bf:       c3                      retq
ffffffff810442c0:       48 c7 c7 e3 54 98 81    mov    $0xffffffff819854e3,%rdi
ffffffff810442c7:       31 c0                   xor    %eax,%eax
ffffffff810442c9:       e8 71 13 6d 00          callq  ffffffff8171563f <printk>
ffffffff810442ce:       eb c9                   jmp    ffffffff81044299 <sys_getppid+0x9>

如果没有跳转标签优化,它看起来像

ffffffff810441f0 <sys_getppid>:
ffffffff810441f0:       8b 05 8a 52 d8 00       mov    0xd8528a(%rip),%eax        # ffffffff81dc9480 <key>
ffffffff810441f6:       55                      push   %rbp
ffffffff810441f7:       48 89 e5                mov    %rsp,%rbp
ffffffff810441fa:       85 c0                   test   %eax,%eax
ffffffff810441fc:       75 27                   jne    ffffffff81044225 <sys_getppid+0x35>
ffffffff810441fe:       65 48 8b 04 25 c0 b6    mov    %gs:0xb6c0,%rax
ffffffff81044205:       00 00
ffffffff81044207:       48 8b 80 80 02 00 00    mov    0x280(%rax),%rax
ffffffff8104420e:       48 8b 80 b0 02 00 00    mov    0x2b0(%rax),%rax
ffffffff81044215:       48 8b b8 e8 02 00 00    mov    0x2e8(%rax),%rdi
ffffffff8104421c:       e8 2f da 00 00          callq  ffffffff81051c50 <pid_vnr>
ffffffff81044221:       5d                      pop    %rbp
ffffffff81044222:       48 98                   cltq
ffffffff81044224:       c3                      retq
ffffffff81044225:       48 c7 c7 13 53 98 81    mov    $0xffffffff81985313,%rdi
ffffffff8104422c:       31 c0                   xor    %eax,%eax
ffffffff8104422e:       e8 60 0f 6d 00          callq  ffffffff81715193 <printk>
ffffffff81044233:       eb c9                   jmp    ffffffff810441fe <sys_getppid+0xe>
ffffffff81044235:       66 66 2e 0f 1f 84 00    data32 nopw %cs:0x0(%rax,%rax,1)
ffffffff8104423c:       00 00 00 00

因此,禁用跳转标签的情况会添加 ‘mov’、‘test’ 和 ‘jne’ 指令,而跳转标签情况只有一个 ‘no-op’ 或 ‘jmp 0’。(jmp 0 在启动时被修补为 5 字节的原子 no-op 指令。)因此,禁用跳转标签的情况会添加

6 (mov) + 2 (test) + 2 (jne) = 10 - 5 (5 byte jump 0) = 5 addition bytes.

如果我们然后包括填充字节,则跳转标签代码为此小函数节省了总共 16 个字节的指令内存。在这种情况下,非跳转标签函数长 80 个字节。因此,我们节省了 20% 的指令占用空间。事实上,我们可以进一步改进这一点,因为 5 字节的 no-op 实际上可以是 2 字节的 no-op,因为我们可以使用 2 字节的 jmp 到达分支。但是,我们尚未实现最佳的 no-op 大小(它们目前是硬编码的)。

由于调度程序路径中存在许多静态键 API 用法,因此可以使用 ‘pipe-test’(也称为 ‘perf bench sched pipe’)来显示性能改进。在 3.3.0-rc2 上完成的测试

跳转标签禁用

Performance counter stats for 'bash -c /tmp/pipe-test' (50 runs):

       855.700314 task-clock                #    0.534 CPUs utilized            ( +-  0.11% )
          200,003 context-switches          #    0.234 M/sec                    ( +-  0.00% )
                0 CPU-migrations            #    0.000 M/sec                    ( +- 39.58% )
              487 page-faults               #    0.001 M/sec                    ( +-  0.02% )
    1,474,374,262 cycles                    #    1.723 GHz                      ( +-  0.17% )
  <not supported> stalled-cycles-frontend
  <not supported> stalled-cycles-backend
    1,178,049,567 instructions              #    0.80  insns per cycle          ( +-  0.06% )
      208,368,926 branches                  #  243.507 M/sec                    ( +-  0.06% )
        5,569,188 branch-misses             #    2.67% of all branches          ( +-  0.54% )

      1.601607384 seconds time elapsed                                          ( +-  0.07% )

跳转标签启用

Performance counter stats for 'bash -c /tmp/pipe-test' (50 runs):

       841.043185 task-clock                #    0.533 CPUs utilized            ( +-  0.12% )
          200,004 context-switches          #    0.238 M/sec                    ( +-  0.00% )
                0 CPU-migrations            #    0.000 M/sec                    ( +- 40.87% )
              487 page-faults               #    0.001 M/sec                    ( +-  0.05% )
    1,432,559,428 cycles                    #    1.703 GHz                      ( +-  0.18% )
  <not supported> stalled-cycles-frontend
  <not supported> stalled-cycles-backend
    1,175,363,994 instructions              #    0.82  insns per cycle          ( +-  0.04% )
      206,859,359 branches                  #  245.956 M/sec                    ( +-  0.04% )
        4,884,119 branch-misses             #    2.36% of all branches          ( +-  0.85% )

      1.579384366 seconds time elapsed

节省的分支百分比为 .7%,我们在 ‘branch-misses’ 上节省了 12%。这正是我们期望获得最大节省的地方,因为此优化旨在减少分支的数量。此外,我们在指令上节省了 .2%,在周期上节省了 2.8%,在经过的时间上节省了 1.4%。