eBPF Talk: bpf2bpf 特性简介
文章目录
最近才了解到 eBPF 里有 bpf2bpf 这个特性,故特意学习了一番。
bpf2bpf 简介
bpf2bpf 特性要求 4.16 内核版本,参考 BPF Features by Linux Kernel Version。
在 bpf2bpf 特性出现之前,eBPF 程序都要求是由单独的一个函数构成,而在 C 代码中强制使用 always_inline
去内联其它函数。这就带来两个问题:
- eBPF ELF 文件大小会随着功能的增多而膨胀(因多个 eBPF 程序间不能复用共通的功能逻辑)
- 开发者必需将复用的(即使很小的)代码放到头文件里
而 bpf2bpf 特性就是为了解决这两问题而来。它允许多个 eBPF 程序以(C 代码层面)非内联的方式复用多个函数;这就允许开发者不比在头文件里去复用函数;而且多个 eBPF 程序复用同一个函数的时候,该复用的函数在 ELF 文件里只存在一份(独占一个 ELF section)。与此同时,bpf2bpf 特性能带来意外的好处:
- 将这些需要复用的函数分别编译成独立的 .o 文件,按需加载
- 在加载阶段即时编译(JIT)一段 eBPF 程序,去替换 C 代码里非内联的函数
bpf2bpf 实现原理
在编码阶段,需要复用的函数不再需要 always_inline
,而需要 noinline
;以此告诉编译器不要对该函数进行内联。
在编译阶段,为 noinline
的函数生成 BPF_PSEUDO_CALL
指令。
在加载阶段,确保 noinline
的函数在 eBPF 程序中。
在校验阶段,并没有将 noinline
的函数进行内联,也是确保有该函数。
在 JIT 阶段,查找函数地址,生成函数调用指令。
至此,bpf2bpf 特性的实现原理就是一次真实的函数调用,而不是伪装的函数调用。
P.S. 详细的校验过程和 JIT 过程有待进一步梳理。
bpf2bpf 与 bpf_tail_call
其实,bpf2bpf 特性跟 bpf_tail_call()
很像;一开始还以为它们是孪生兄弟。
从 C 代码层面看,它们都是非 always_inline
的。bpf2bpf 的目标函数使用 noinline
属性,而 bpf_tail_call()
的目标函数使用 SEC
属性(即调用另一段 eBPF 程序)。
从代码执行逻辑的顺序看,bpf2bpf 调用函数后能够继续往下执行,而 bpf_tail_call()
在调用后就不能再往下执行当前函数余下的代码(毕竟它就是尾调用嘛)。
bpf2bpf 例子一则
既然已学习了 bpf2bpf 特性,那就搞个 demo 验证一下;毕竟纸上得来终觉浅,绝知此事要躬行。
就以 tcp 连接事件为例吧;当前服务器向外发起 tcp 连接、获取接收 tcp 连接时,就将其中的地址端口打印出来。其中的 C 代码如下:
|
|
相比于 always_inline
的实现方式,noinline
的差别仅在于将 __always_inline
换成 __noinline
,其它地方并无差异。
效果如下:
|
|
Go 代码就不贴出来了;完整 demo 代码请查看 bpf2bpf example。
小结
在 5.2+ 内核的 Linux 系统中,可以使用命令 bpftool prog dump xlated id ${progID}
来查看已经挂载的 eBPF 程序的汇编指令,带有 C 源代码哟。${prodID}
可通过 bpftool prog list
来查询得到。
文章作者 Leon Hwang
上次更新 2022-11-16