一般而言,如果一个 BPF 程序通过了 BPF 验证器的考验,就万事大吉了。极少数情况下我们需要对程序进行调试,BPF 的调试手段比较少,一般我们会采用打日志的方式。
bpf_trace_printk
是 BPF 提供的一个辅助函数,为调试而生,其功能就是打日志。我们可以通过如下命令查看其输出:
$ cat /sys/kernel/debug/tracing/trace_pipe
受限于 BPF 的实现,最多只能有三个格式参数(一个辅助函数最多只能接受五个参数),有着如下签名:
int bpf_trace_printk(const char *fmt, int fmt_size, ...)
BCC 中提供了 bpf_trace_printk
的封装,省去了 fmt_size
参数,但这不意味着你可以突破三个格式参数的限制:
int bpf_trace_printk(const char *fmt, ...)
libbpf 中提供了 bpf_printk
宏,也是对 bpf_trace_printk
的封装。如果仔细观察它的实现,你会发现,它允许你传递超过三个格式参数(使用了一个取巧的方式,传递数组作为参数),前提是你的内核要足够新,否则你会得到如下错误:
17: (85) call unknown#177
invalid func unknown#177
177 是另一个辅助函数 bpf_trace_vprintk
的编号。所以,准确地说,bpf_printk
是 bpf_trace_printk
和 bpf_trace_vprintk
的封装。
通常在开发的时候,我们会直接采用 bpf_printk
,因为它提供了更好的封装。更好的封装意味着:
fmt_size
参数fmt
字符串的定义默认使用 static
修饰,这意味着不占用宝贵的 BPF 栈空间(前提是内核支持 BPF Global Data,否则需要定义 BPF_NO_GLOBAL_DATA
)无论是 bpf_trace_printk
还是 bpf_printk
,我们一般只会在调试的时候使用,因为日志输出不容易查看和采集,而且 trace_pipe
是共享的。在生产环境中,我们通常要为日志设置开关,要方便采集和读取,所以通常会为需要输出日志的地方定义事件,事件可以用一个结构体来描述,然后把事件输出到 BPF perfbuf 或者 BPF ringbuf 中,在用户态程序中去采集处理。至于日志开关,这里卖个关子,欢迎评论区交流。