static-keys.txt 翻译

静态键
-----------

作者:Jason Baron <jbaron@redhat.com>

0) 摘要

静态键允许通过 GCC 功能和代码修补技术在对性能敏感的快速路径内核代码中包含很少使用的功能。 一个简单的例子:

struct static_key key = STATIC_KEY_INIT_FALSE;
...
if (static_key_false(&key))
    do unlikely code
else
    do likely code
...
static_key_slow_inc();
...
static_key_slow_inc();
...

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


1) 动机

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


2) 解决方案

gcc (v4.5) 添加了一个新的“asm goto”语句,允许分支到标签:http://gcc.gnu.org/ml/gcc-patches/2009-07/msg01556.html

使用“asm goto”,我们可以创建默认情况下采用或不采用的分支,而无需检查内存。 然后,在运行时,我们可以修补分支站点以改变分支方向。

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

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

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

这种低级修补机制称为“跳转标签修补”,它为静态键设施提供了基础。


3) 静态键标签API、用法及示例:

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

struct static_key key;

初始化为:

struct static_key key = STATIC_KEY_INIT_TRUE;

或者:

struct static_key key = STATIC_KEY_INIT_FALSE;

如果键未初始化,则默认为 false。 'struct static_key'必须是全局变量。 也就是说,它不能在堆栈上分配或在运行时动态分配。

然后在代码中使用键为:

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

或者:

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

通过“STATIC_KEY_INIT_FALSE”初始化的键必须在“static_key_false()”构造中使用。 同样,通过“STATIC_KEY_INIT_TRUE”初始化的键必须用在“static_key_true()”构造中使用。 一个键可以在许多分支中使用,但所有
分支必须与键的初始化方式相匹配。

然后可以通过以下方式切换分支:

static_key_slow_inc(&key);
...
static_key_slow_dec(&key);

因此,“static_key_slow_inc()”意味着“使分支为真”,并且'static_key_slow_dec()' 表示使用适当的引用计数'使分支为假'。 例如,如果键被初始化为真,static_key_slow_dec() 会将分支切换为假。 随后的 
static_key_slow_inc() 会将分支更改回 true。 同样,如果键被初始化为 false,则“static_key_slow_inc()”会将分支更改为 true。 然后'static_key_slow_dec()',将再次使分支为假。

内核中的示例用法是跟踪点的实现:

static inline void trace_##name(proto)                   \
{                                                        \
    if (static_key_false(&__tracepoint_##name.key))        \
            __DO_TRACE(&__tracepoint_##name,               \
                    TP_PROTO(data_proto),                  \
                    TP_ARGS(data_args),                 \
                    TP_CONDITION(cond));                   \
}

跟踪点默认是禁用的,可以放置在内核的性能关键部分。 因此,通过使用静态键,跟踪点在不使用时可以产生绝对最小的影响。


4) 架构级代码打补丁接口,‘跳转标签(jump labels)’


为了利用这种优化,架构必须实现一些功能和宏。 如果没有架构支持,我们就简单地退回到传统的加载、测试和跳转序列。

* 选择 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),见: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

* __init_or_module void arch_jump_label_transform_static(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


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

例如,让我们将以下分支添加到“getppid()”,这样系统调用现在看起来像:

SYSCALL_DEFINE0(getppid)
{
        int pid;

+       if (static_key_false(&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 字节的原子无操作指令。)因此,禁用跳转标签的情况添加:

6 (mov) + 2 (test) + 2 (jne) = 10 - 5 (5 byte jump 0) = 5 个加法字节。

如果我们随后包含填充字节,则跳转标签代码将为这个小功能节省总共 16 个字节的指令存储器。在这种情况下,非跳转标签函数的长度为 80 个字节。因此,我们节省了 20% 的指令占用空间。事实上,我们可以
进一步改进这一点,因为 5 字节无操作实际上可以是 2 字节无操作,因为我们可以使用 2 字节 jmp 到达分支。但是,我们还没有实现最佳的无操作大小(它们目前是硬编码的)。

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

跳转标签禁用:

'bash -c /tmp/pipe-test' 的性能计数器统计信息(50 次运行):

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


启用跳转标签:

'bash -c /tmp/pipe-test' 的性能计数器统计信息(50 次运行):

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

已保存分支的百分比为 0.7%,我们在“分支未命中”上节省了 12%。 这是我们期望获得最多节省的地方,因为这种优化是关于减少分支数量。 此外,我们还节省了 0.2% 的指令、2.8% 的周期和 1.4% 的运行时间。

 

7)补充测试demo

#define pr_fmt(fmt) "kfalse_test: " fmt

#include <linux/module.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/jump_label.h>

#if 0
struct static_key false_key = STATIC_KEY_INIT_TRUE;
struct static_key true_key = STATIC_KEY_INIT_FALSE;
#else
struct static_key false_key = STATIC_KEY_INIT_FALSE;
struct static_key true_key = STATIC_KEY_INIT_TRUE;
#endif

static int __init kernel_file_op_init(void)
{
    if (static_key_false(&false_key)) {
        pr_info("=========1==========");
    } else {
        pr_info("=========2==========");
    }
    static_key_slow_inc(&false_key);
    if (static_key_false(&false_key)) {
        pr_info("=========3==========");
    } else {
        pr_info("=========4==========");
    }


    if (static_key_true(&true_key)) {
        pr_info("=========5==========");
    } else {
        pr_info("=========6==========");
    }
    static_key_slow_dec(&true_key);
    if (static_key_true(&true_key)) {
        pr_info("=========7==========");
    } else {
        pr_info("=========8==========");
    }

    return 0;
}

static void __exit kernel_file_op_exit(void)
{
    pr_info("kernel file operation module exit.\n");
}
 
module_init(kernel_file_op_init);
module_exit(kernel_file_op_exit);
 
MODULE_LICENSE("GPL");

/*
这样初始化:
struct static_key false_key = STATIC_KEY_INIT_TRUE;
struct static_key true_key = STATIC_KEY_INIT_FALSE;
执行打印:
kfalse_test: =========2==========
kfalse_test: =========4==========
kfalse_test: =========5==========
kfalse_test: =========7==========
总结:static_key_false()的功能比较正常,static_key_true()的功能不正常!

这样初始化:
struct static_key false_key = STATIC_KEY_INIT_FALSE;
struct static_key true_key = STATIC_KEY_INIT_TRUE;
执行打印:
kfalse_test: =========2==========
kfalse_test: =========3==========
kfalse_test: =========5==========
kfalse_test: =========8==========

相信下面的这个,符合上面static_key_false只能初始化为STATIC_KEY_INIT_FALSE的规范。
*/

由实验得出结论,if语句中判别真假时,可理解为 static_key 为真即为真,static_key 为假即为假

 

posted on 2021-07-18 14:46  Hello-World3  阅读(235)  评论(0编辑  收藏  举报

导航