在『eBPF Talk 读者群』里讨论起 kfuncs
、kprobe
、fentry/fexit
等,聊到 kprobe.multi
与 fprobe
;我立刻学习了一下 kprobe.multi
和 fprobe
。
TL;DR kprobe.multi
的底层是 fprobe
,fprobe
的底层是 ftrace
。
P.S. kprobe.multi
最低要求 5.18 内核,起始于 bpf: Add multi kprobe link。
kprobe.multi
例子:pwru
pwru
是基于 kprobe
实现的 Linux 内核网络包跟踪工具,目前默认使用 kprobe.multi
进行 kprobe
。
使用了 kprobe.multi
后,pwru
可以一次性对多个内核函数进行 “kprobe” 并运行同一个 bpf prog。
相关代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// ${PWRU}/bpf/kprobe_pwru.c
#ifdef HAS_KPROBE_MULTI
#define PWRU_KPROBE_TYPE "kprobe.multi"
#define PWRU_HAS_GET_FUNC_IP true
#else
#define PWRU_KPROBE_TYPE "kprobe"
#define PWRU_HAS_GET_FUNC_IP false
#endif /* HAS_KPROBE_MULTI */
#define PWRU_ADD_KPROBE(X) \
SEC(PWRU_KPROBE_TYPE "/skb-" #X) \
int kprobe_skb_##X(struct pt_regs *ctx) { \
struct sk_buff *skb = (struct sk_buff *) PT_REGS_PARM##X(ctx); \
return handle_everything(skb, ctx, PWRU_HAS_GET_FUNC_IP); \
}
PWRU_ADD_KPROBE(1)
PWRU_ADD_KPROBE(2)
PWRU_ADD_KPROBE(3)
PWRU_ADD_KPROBE(4)
PWRU_ADD_KPROBE(5)
#undef PWRU_KPROBE
#undef PWRU_HAS_GET_FUNC_IP
#undef PWRU_KPROBE_TYPE
// ${PWRU}/main.go
opts := link.KprobeMultiOptions{Symbols: funcsByPos[pos]} // 一次性提供进行 "kprobe" 的多个内核函数的函数名称
kp, err := link.KprobeMulti(fn, opts)
|
kprobe.multi
源码分析
从 cilium/ebpf
Go 库看下 kprobe.multi
会使用 BPF 系统调用的哪个子命令。
1
2
3
4
|
KprobeMulti() // ${EBPF}/link/kprobe_multi.go
|-->kprobeMulti()
|-->sys.LinkCreateKprobeMulti() // ${EBPF}/internal/sys/types.go
|-->BPF(BPF_LINK_CREATE, unsafe.Pointer(attr), unsafe.Sizeof(*attr))
|
接着,看下 BPF 系统调用的 BPF_LINK_CREATE 的源代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
// ${KERNEL}/kernel/bpf/syscall.c
static int __sys_bpf(int cmd, bpfptr_t uattr, unsigned int size)
{
// ...
switch (cmd) {
case BPF_LINK_CREATE:
err = link_create(&attr, uattr);
break;
}
return err;
}
static int link_create(union bpf_attr *attr, bpfptr_t uattr)
{
// ...
switch (prog->type) {
// ...
case BPF_PROG_TYPE_KPROBE:
if (attr->link_create.attach_type == BPF_PERF_EVENT)
ret = bpf_perf_link_attach(attr, prog);
else
ret = bpf_kprobe_multi_link_attach(attr, prog);
break;
default:
ret = -EINVAL;
}
out:
if (ret < 0)
bpf_prog_put(prog);
return ret;
}
// ${KERNEL}/kernel/trace/bpf_trace.c
int bpf_kprobe_multi_link_attach(const union bpf_attr *attr, struct bpf_prog *prog)
{
// ...
if (uaddrs) {
if (copy_from_user(addrs, uaddrs, size)) {
// ...
}
} else {
// ...
err = ftrace_lookup_symbols(us.syms, cnt, addrs);
// ...
}
if (flags & BPF_F_KPROBE_MULTI_RETURN)
link->fp.exit_handler = kprobe_multi_link_handler;
else
link->fp.entry_handler = kprobe_multi_link_handler;
link->addrs = addrs;
link->cookies = cookies;
link->cnt = cnt;
// ...
err = register_fprobe_ips(&link->fp, addrs, cnt);
// ...
return err;
}
|
以上代码片段的主要处理逻辑如下:
- 将提供的内核函数名称转成函数地址。
- 设置
fprobe
entry/exit 所需的回调函数。
- 注册
fprobe
。
fprobe
源码分析
接着,直接看下 register_fprobe_ips()
的源码吧。
1
2
3
4
5
6
7
8
9
10
|
register_fprobe_ips() // ${KERNEL}/kernel/trace/fprobe.c
|-->fprobe_init() {
| if (fprobe_shared_with_kprobes(fp))
| fp->ops.func = fprobe_kprobe_handler;
| else
| fp->ops.func = fprobe_handler;
| }
|-->ftrace_set_filter_ips(&fp->ops, addrs, num, 0, 0); // ${KERNEL}/kernel/trace/ftrace.c
|-->fprobe_init_rethook(fp, num);
|-->register_ftrace_function(&fp->ops);
|
以上代码片段的主要处理逻辑如下:
- 设置
fprobe
的回调函数。
- 将需要 trace 的函数地址加入到
ftrace
中。
- 向指定函数地址进行
ftrace
。
运行 bpf prog
kprobe.multi
和 fprobe
都设置了它们的回调函数。继续看下回调函数里做了哪些处理吧。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// ${KERNEL}/kernel/trace/fprobe.c
static void fprobe_handler(unsigned long ip, unsigned long parent_ip,
struct ftrace_ops *ops, struct ftrace_regs *fregs)
{
// ...
if (fp->entry_handler)
fp->entry_handler(fp, ip, ftrace_get_regs(fregs));
if (fp->exit_handler) {
rh = rethook_try_get(fp->rethook);
if (!rh) {
fp->nmissed++;
goto out;
}
fpr = container_of(rh, struct fprobe_rethook_node, node);
fpr->entry_ip = ip;
rethook_hook(rh, ftrace_get_regs(fregs), true);
}
out:
ftrace_test_recursion_unlock(bit);
}
|
- 如果有
entry_handler
,就调用 entry_handler
函数。
- 如果有
exit_handler
,就调用 exit_handler
函数。
kprobe.multi
设置的 fprobe
的 entry_handler
和 exit_handler
都是 kprobe_multi_link_handler
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// ${KERNEL}/kernel/trace/bpf_trace.c
static void
kprobe_multi_link_handler(struct fprobe *fp, unsigned long fentry_ip,
struct pt_regs *regs)
{
struct bpf_kprobe_multi_link *link;
link = container_of(fp, struct bpf_kprobe_multi_link, fp);
kprobe_multi_link_prog_run(link, get_entry_ip(fentry_ip), regs);
}
static int
kprobe_multi_link_prog_run(struct bpf_kprobe_multi_link *link,
unsigned long entry_ip, struct pt_regs *regs)
{
struct bpf_kprobe_multi_run_ctx run_ctx = {
.link = link,
.entry_ip = entry_ip,
};
struct bpf_run_ctx *old_run_ctx;
int err;
if (unlikely(__this_cpu_inc_return(bpf_prog_active) != 1)) {
err = 0;
goto out;
}
migrate_disable();
rcu_read_lock();
old_run_ctx = bpf_set_run_ctx(&run_ctx.run_ctx);
err = bpf_prog_run(link->link.prog, regs);
bpf_reset_run_ctx(old_run_ctx);
rcu_read_unlock();
migrate_enable();
out:
__this_cpu_dec(bpf_prog_active);
return err;
}
|
以上代码片段比较简单,就是将 fprobe
回调函数传递过来的 regs
传给 bpf_kprobe_multi_link
上的 bpf prog。
小结
从 5.18 内核版本起,需要进行 kprobe
的场景都可以使用 kprobe.multi
替换。
因为 kprobe.multi
底层使用的 fprobe
on top of ftrace
进行 trace 的性能消耗比较低了,跟 fentry
/fexit
的性能消耗相差不多。这是因为 ftrace
和 fentry
/fexit
的底层都是 trampoline
技术。