最近在操作系统部门实习,学习了一些 linux 内核追踪和调试的机制。其中 static key 机制是我认为比较精妙的一个,用来优化系统中大量的 if-else
判断性能问题。
linux 中有一套很强大的内核代码追踪机制,说白了就是在各种重要的内核事件发生的地方(trace point)加上一段调试函数,将相关的信息记录下来。比如在切换进程的时候多调用一个 trace_sched_switch
函数:
static inline void
这个 trace 功能绝大部分 linux 用户都用不到,只有在进行内核调试时才会用到,所以默认都是关闭的,需要的时候再手动操作 /sys/kernel/debug/tracing/events
下的文件打开。
但是即使我们在运行时关闭了 trace,内核运行到这里的时候也会多执行一个无用的 if
判断语句。当系统中的追踪点很多的时候,这些空判断对性能的影响是不可忽视的。于是 linux 中引入了 static key 解决这个问题。
用法
虽然 static key 最初是为了消除 trace point 的开销而设计的,但它本身是一个独立的机制,其他模块也可以随意使用。
// 定义一个 static key
// - 必须为全局变量
// - 必须初始化为 STATIC_KEY_INIT_FALSE 或 STATIC_KEY_INIT_TRUE
struct static_key key = STATIC_KEY_INIT_FALSE;
// 初始化为 false 时,判断必须使用 static_key_false 包装
if else
// key 的值一开始为 0,该函数把 key 的值加 1
// key 从 0 变 1 时,代码路径会更换为 do_unlikely_branch() 分支
;
// 该函数把 key 的值减 1
// key 从 1 变 0 时,代码路径会更换为 do_likely_branch() 分支
;
// 直接将 key 的值置 1
;
// 直接将 key 的值置 0
;
原理
linux 使用了 static key 的机制来消除 trace point 的运行时开销。其工作原理是:
- 系统初始化时,将所有的用 static key 判断的代码地址保存到一个单独的段
jump_table
中 - 打开 static key 时,遍历
jump_table
中所有的地址,将此处代码替换为jmp
指令 - 关闭 static key 时,遍历
jump_table
中所有的地址,将此处代码替换为nop
指令
由此可见,当我们关闭 static key 后,所有本来需要判断 static key 的地方都是一条 nop
指令,尽可能减少了对系统性能的影响。需要注意的是,打开和关闭的操作是很耗时的,因为需要遍历所有出现过判断的地方,并进行代码段指令的替换。所以 static key 不适用于需要频繁开关的场景。
实现
具体的实现依赖于不同架构的汇编指令,我模拟 linux 的 API 做了一个简易版本的 x86 实现 放在 github 上以供参考。