前言
LINUX系统引入了Static_Keys机制,该机制中的部分函数的实现利用了代码段修改来实现的,而不是每次都是通过变量判断,从而直接改变代码段。而修改代码段的工作就交给了一个函数就是DO_ONCE中的__do_once_done中,那么我们就先来分析一下代码。
DO_ONCE函数解析
DO_ONCE机制想法很简单,有些函数只应该调用一次,那么这些函数调用一次后,如果下次再调用就应该直接返回。以前我们要实现这种功能总是需要判断一个变量,而使用DO_ONCE机制,自动帮你完成判断,保证函数只会在第一次调用的时候被执行,以后都直接返回。很自然,DO_ONCE机制就采用了Static_Keys来实现。使用static_key外 ,另一个关键在__do_once_done中。
#define DO_ONCE(func, ...) \
({ \
bool ___ret = false; \
static bool ___done = false; \
static DEFINE_STATIC_KEY_TRUE(___once_key); \
if (static_branch_unlikely(&___once_key)) { \
unsigned long ___flags; \
___ret = __do_once_start(&___done, &___flags); \
if (unlikely(___ret)) { \
func(__VA_ARGS__); \
__do_once_done(&___done, &___once_key, \
&___flags); \
} \
} \
___ret;// 由于是宏函数所以不需要使用return \
})
如果有一个func只能被调用一次,例如初始化函数的话,则可以用这个宏,如下例所示foo这个函数即使被调用两次,也只会运行一次:
/* Call a function exactly once. The idea of DO_ONCE() is to perform
* a function call such as initialization of random seeds, etc, only
* once, where DO_ONCE() can live in the fast-path. After @func has
* been called with the passed arguments, the static key will patch
* out the condition into a nop. DO_ONCE() guarantees type safety of
* arguments!
*
* Not that the following is not equivalent ...
*
* DO_ONCE(func, arg);
* DO_ONCE(func, arg);
*
* ... to this version:
*
* void foo(void)
* {
* DO_ONCE(func, arg);
* }
*
* foo();
* foo();
*
* In case the one-time invocation could be triggered from multiple
* places, then a common helper function must be defined, so that only
* a single static key will be placed there!
*/
STATIC KEYS
简单的说,如果你代码对性能很敏感,而且大多数情况下分支路径是确定的,可以考虑使用Static Keys。Static Keys可以代替使用普通变量进行分支判断,目的是用来优化频繁使用if-else判断的问题,这里涉及到指令分支预取的一下问题。简单地说,现代cpu都有预测功能,变量的判断有可能会造成硬件预测失败,影响流水线性能。虽然有likely和unlikely,但还是会有小概率的预测失败。
下面使用例子说明一下:
//定义一个Static Keys,并且默认这个值是false。
DEFINE_STATIC_KEY_FALSE(key);…
//代码使用Static Keys代替普通变量进行判断,static_branch_unlikely是一个宏,展开后不会有真正的判断,而是直接执行false分支,即 do likely code。
if (static_branch_unlikely(&key))
do unlikely code //被编译到其他段
else
do likely code… //被编译到紧接代码段
这样的好处是,上述代码的性能和没有分支判断的性能差不多,具体可能只差一个nop指令的执行时间。 当然,如果某种情况发生了,需要改变分支的执行路径,可以调用下面的接口:
static_branch_enable(&key);
执行static_branch_enable(&key)后,底层通过gcc提供的goto功能,再结合c代码编写的动态更改内存功能,就可以让使用key的代码从执行false分支变为执行true分支。当然这个更改代价是比较昂贵的,不是所有情况都适用。可以改变分支的函数参照如下
#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)
GOTO
gcc4.5
提供了一个特性用于嵌入式汇编,那就是asm goto,其实这个特性没有什么神秘之处,就是在嵌入式汇编中go to到c代码的label,其最简单的用法如下(来自gcc的文档):
int frob(int x)
{
int y;
asm goto (“frob %%r5, %1; jc %l[error]; mov (%2), %%r5”
: : “r”(x), “r”(&y) : “r5”, “memory” : error);
return y;
error:
return -1;
}
按照原理来说"asm goto"其实就是在outputs,inputs,registers-modified之外提供了嵌入式汇编的第四个“:”,后面可以跟一系列的c语言的label,然后你可以在嵌入式汇编中goto到这些label中一个。然而使用"asm goto"可以巧妙地将"运行时修改载入内存的二进制代码”规范化,就是说你只需要调用一个统一的接口宏,编译器就将你想实现的东西给实现了,要不然代码写起来会很麻烦,这点上,编译器不嫌麻烦。具体为什么要动态修改二级制代码的原因还是与前面介绍的指令分支预取有关,为了极大的优化系统性能。
JUMP TABLE
jump_la加粗样式ble屏蔽不同体系更改机器代码的不同,向上提供一个统一接口。不同体系会提供给jump_lable一个体系相关的实现。
jump_lable的实现原理很简单,就是通过替换内存中机器代码的"nop"空指令为跳转指令,或者替换机器代码的跳转指令为“nop”空指令,实现分支的切换.
// arch/x86/include/asm/jump_label.h
static __always_inline bool arch_static_branch(struct static_key *key, bool branch)
{
asm goto("1: nop\n\t"
".pushsection __jump_table, \"aw\"\n\t"
".align 3\n\t"
".quad 1b, %l[l_yes], %c0\n\t"
".popsection\n\t"
: : "i"(&((char *)key)[branch]) : : l_yes);
return false;
l_yes:
return true;
}
static __always_inline bool arch_static_branch_jump(struct static_key *key, bool branch)
{
asm goto("1: b %l[l_yes]\n\t"
".pushsection __jump_table, \"aw\"\n\t"
".align 3\n\t"
".quad 1b, %l[l_yes], %c0\n\t"
".popsection\n\t"
: : "i"(&((char *)key)[branch]) : : l_yes);
return false;
l_yes:
return true;
}
// 编译完的伪代码大概这样,可以看出并没有分支跳转
static __always_inline bool arch_static_branch(struct static_key *key, bool branch)
{
nop/jump l_yes; // 是nop还是jump l_yes取决于 key
return false;
l_yes:
return true;
}
static __always_inline bool arch_static_branch_jump(struct static_key *key, bool branch)
{
nop/jump l_yes; // 是nop还是jump l_yes取决于 key
return false;
l_yes:
return true;
}
__do_once_done
void __do_once_done(bool *done, struct static_key_true *once_key,
unsigned long *flags)
__releases(once_lock)
{
*done = true;
spin_unlock_irqrestore(&once_lock, *flags);
once_disable_jump(once_key);
}
EXPORT_SYMBOL(__do_once_done
这个函数实在执行完第一次—do_once_start后需要修改代码段将jump修改成nop,最后还是调用了static_branch_disable(work->key); 具体修改代码段的代码追溯后到arch_jump_label_transform:
void arch_jump_label_transform(struct jump_entry *entry,
enum jump_label_type type)
{
void *addr = (void *)entry->code;
u32 insn;
if (type == JUMP_LABEL_JMP) {
insn = aarch64_insn_gen_branch_imm(entry->code,
entry->target,
AARCH64_INSN_BRANCH_NOLINK);
} else {
insn = aarch64_insn_gen_nop();
}
aarch64_insn_patch_text(&addr, &insn, 1);
}
再次调用aarch64_insn_patch_text_nosync函数:
int __kprobes aarch64_insn_patch_text_nosync(void *addr, u32 insn)
{
u32 *tp = addr;
int ret;
/* A64 instructions must be word aligned */
if ((uintptr_t)tp & 0x3)
return -EINVAL;
ret = aarch64_insn_write(tp, insn);
if (ret == 0)
flush_icache_range((uintptr_t)tp,
(uintptr_t)tp + AARCH64_INSN_SIZE);
return ret;
}
最后调用到__aarch64_insn_write,进行代码段的重映射,然后将jump tabel的头地址“b ”替换成“nop”,取消映射,返回。
static int __kprobes __aarch64_insn_write(void *addr, __le32 insn)
{
void *waddr = addr;
unsigned long flags = 0;
int ret;
raw_spin_lock_irqsave(&patch_lock, flags);
waddr = patch_map(addr, FIX_TEXT_POKE0);
ret = probe_kernel_write(waddr, &insn, AARCH64_INSN_SIZE);
patch_unmap(FIX_TEXT_POKE0);
raw_spin_unlock_irqrestore(&patch_lock, flags);
return ret;
}
linux提供这种可以修改的代码段的函数,如果是用于调试还可以理解,但是如果公布的版本还是提供,是不是给一些专家提供了便捷的途径,还是那句话,"安全是一个平衡,性能和安全从来就是相对的,具体看应用的场景“。
CONFIG
如果启动功能,需要在开启线面的两个开关,目前Linux在x86上是强制开启的,ARM上还是根据需求来开启。
#if defined(CC_HAVE_ASM_GOTO) && defined(CONFIG_JUMP_LABEL)
# define HAVE_JUMP_LABEL
#endif
————————————————
版权声明:本文为优快云博主「hanzefeng」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.youkuaiyun.com/hanzefeng/article/details/82882811