控制app可以使用BPF skeleton方便地提供这类变量:
struct <name> *skel = <name>__open(); if (!skel) /* handle errors */ skel->rodata->my_cfg.feature_enabled = true; skel->rodata->my_cfg.pid_to_filter = 123; if (<name>__load(skel)) /* handle errors */只读变量可以在BPF skeleton加载前在用户空间进行设置和修改。一旦加载了BPF程序,则无法在用户空间进行设置和修改。这保证BPF校验器在校验期间将这类变量视为常数,以便更好地移除无效代码。而非常量则可以在BPF skeleton加载之后的整个生命周期中(从BPF和用户空间)进行修改,这些变量可以用于交换可变的配置,状态等等。
常见的问题在运行BPF程序时可能会遇到各种问题。有时只是一个误解,有时是因为BCC和libbpf实现上的差异导致的。下面给出了一些典型的场景,可以帮助更好地进行BCC到BPF CO-RE的转换。
全局变量BPF全局变量看起来就像一个用户空间的变量:它们可以在表达式中使用,也可以更新(非const表达式),甚至可以使用它们的地址并传递到辅助函数中。但这是在BPF代码侧有效。在用户空间侧,只能通过BPF skeletob进行读取和更新。
skel->rodata 用于只读变量;
skel->bss 用于初始值为0的可变量;
skel->data 用于初始值非0的可变量。
可以在用户空间进行读取/更新,这些更新会立即反映到BPF侧。但在用户空间侧,这些变量并不是全局的,它们只是BPF skeleton的rodata、bss、或data的成员,在skeleton 加载期间进行了初始化。因此意味着在BPF代码和用户空间代码中声明完全相同的全局变量将视为完全独立的变量,在任何情况下都不会出现交集。
循环展开除非目标内核为5.3以上的版本,否则BPF代码中的所有循环都必须使用#pragma unroll标识,强制Clang进行循环展开,并消除所有可能的循环控制流:
#pragma unroll for (i = 0; i < 10; i++) { ... }如果没有循环展开,或循环没有在固定迭代之后结束,那么会返回一个"back-edge from insn X to Y"的校验器错误,即BPF校验器检测到了一个无限循环(或无法在有限次数的迭代之后结束的循环)。
辅助子程序如果使用静态辅助函数,则必须将其标记为static __always_inline(由于当前libbpf的处理限制):
static __always_inline unsigned long probe_read_lim(void *dst, void *src, unsigned long len, unsigned long max) { ... }从5.5内核开始支持非内联的全局函数,但它们具有与静态函数不同的语义和校验限制,这种情况下,最好也使用内核标记!
bpf_printk 调试BPF程序没有常规调试器可以用于设置断点,检查变量和BPF maps,以及代码的单步调试等。使用这类工具通常无法确定BPF代码的问题所在。
这种情况下,使用日志输出是最好的选择。使用bpf_printk(fmt, args...)打印输出额外的信息来理解发生的事情。该函数接受printf类的格式,最大支持3个参数。它的使用非常简单,但开销也比较大,不适合用于生产环境,因此仅适用于临时调试:
char comm[16]; u64 ts = bpf_ktime_get_ns(); u32 pid = bpf_get_current_pid_tgid(); bpf_get_current_comm(&comm, sizeof(comm)); bpf_printk("ts: %lu, comm: %s, pid: %d\n", ts, comm, pid);日志信息可以从一个特殊的/sys/kernel/debug/tracing/trace_pipe文件中读取:
$ sudo cat /sys/kernel/debug/tracing/trace_pipe ... [...] ts: 342697952554659, comm: runqslower, pid: 378 [...] ts: 342697952587289, comm: kworker/3:0, pid: 320 ...