ebpf_perf_output 介绍
在上一篇 ”使用 ebpf 实时持续跟踪进程文件记录“ 中,我们简单介绍了使用 eBPF 跟踪文件打开记录的跟踪。为了简单演示功能,我们直接使用了 bpf_trace_printk
进行演示,正如上文所述,bpf_trace_printk
存在一些限制:
- 最大只支持 3 个参数,而且只运行一个 %s 的参数;
- 程序共享输出共享
/sys/kernel/debug/tracing/trace_pipe
文件,可能导致文件输出错乱; - 该实现方式在数据量大的时候,性能也存在一定的问题;
本文中我们将使用更加高效且提供隔离功能的 BPF_PERF_OUTPUT
机制来实现数据的传递,而且由于数据通过结构体定义的方式,也不存在参数数量和数据大小等限制。
为了使用 BPF_PERF_OUTPUT
机制,需要约定 Probe 程序和用户空间程序的通信协议。相比简单使用 bpf_trace_printk
,在内核中的 Probe 程序需要以下操作:
- 定义一个通信的结构体,用于 Probe 程序与用户空间通信程序的数据传输约定;
- 定一个用于通信的 perf_event 对象,BCC 提供了宏
BPF_PERF_OUTPUT
实现; - 内核中的 Probe 程序捕获事件,将数据按照第一步定义好的结构体填充,并将
event
事件发布;
在用户空间程序,在本文中为基于 BCC 的 Python代码:
- 定义和声明通信的结构体;(基于 BCC 的程序已经自动生成,无需再定义)
- 定义消费
event
事件的函数; - 持续消费事件程序;
代码实现
原始代码如下:
#!/usr/bin/python
from bcc import BPF
prog = """
int trace_syscall_open(struct pt_regs *ctx, const char __user *filename, int flags) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_trace_printk("%d [%s]\\n", pid, filename);
return 0;
}
"""
b = BPF(text=prog)
b.attach_kprobe(event=b.get_syscall_fnname("open"), fn_name="trace_syscall_open")
try:
b.trace_print()
except KeyboardInterrupt:
exit()
按照上述的步骤,调整后的 Probe 的程序如下:
prog = """
#include <uapi/linux/limits.h> // for NAME_MAX
// 1 define struct
struct event_data_t {
u32 pid;
char fname[NAME_MAX]; // max of filename
};
// 2. declare BPF_PERF_OUTPUT define
BPF_PERF_OUTPUT(open_events);
int trace_syscall_open(struct pt_regs *ctx, const char __user *filename, int flags) {
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 3.1 define event data and fill data
struct event_data_t evt = {};
evt.pid = pid;
bpf_probe_read(&evt.fname, sizeof(evt.fname), (void *)filename);
// bpf_trace_printk("%d [%s]\\n", pid, filename); =>
// 3.2 submit the event
open_events.perf_submit(ctx, &evt, sizeof(evt));
return 0;
}
"""
这里将详细介绍我们进行的相关调整:
- 定义了内核 Probe 程序与用户空间程序通信的结构体
event_data_t
,包含pid
和filename
两个字段; - 使用 BCC 提供的宏
BPF_PERF_OUTPUT(open_events)
完成内核中open_events
变量的定义; - 在
trace_syscall_open
函数中,增加变量的定义struct event_data_t evt = {};
,需要注意的是结构体变量evt.fname
的赋值,需要使用 eBPF 提供的辅助函数bpf_probe_read
来帮助,这是因为内核对于非简单类型的赋值需要进行安全边界的检查,避免在内核中进行越界访问,破坏内核稳定性和安全性的保障; - 最后,使用
open_events.perf_submit
将event
数据发送至用户空间;
上述代码,完成了我们在内核 Probe 程序中的所有工作。用户空间 Python 程序则需要定义 event
消费函数,并使用 perf_buffer_poll
函数轮训消费即可。
# 1.1 define process event
def print_event(cpu, data, size):
event = b["open_events"].event(data)
print("Rcv Event %d, %s"%(event.pid, event.fname))
# 1.2 loop with callback to print_event
b["open_events"].open_perf_buffer(print_event)
while True:
try:
b.perf_buffer_poll() # 2. perf poll
except KeyboardInterrupt:
exit()
在用户空间的 Python 代码中,当前我们只需要定义事件处理函数,将事件与函数进行关联,然后持续轮询数据即可。
-
我们定义了事件处理函数
print_event
,然后读取出对应的数据并生成结构数据 ,event = b["open_events"].event(data)
,此处不用再声明 Python 中的结构体变量,BCC 已经协助处理,否则需要我们显示定义,在一些早期的 BCC 代码中还可以看到手工转换的场景。 -
import ctypes class OpenEvt(ctypes.Structure): _fields_ = [ ("pid", ctypes.c_uint), ("fname", ctypes.c_char * MAX_STR_LEN), ] # event 处理函数中强制 cast 使用 # event = ct.cast(data, ct.POINTER(OpenEvt)).contents
``
-
然后在主体函数中使用
b.perf_buffer_poll()
持续轮询即可;
运行结果如下:
#./open_perf_output.py
Rcv Event 1732, /var/log/secure
Rcv Event 12846, /usr/lib64/python2.7/encodings/ascii.so
完整样例可以参考 open_perf_output.py。
3. 总结
通过我们上述代码样例,相信你已经非常熟悉了如何使用 BPF_PERF_OUTPUT 方式,在直接编写的各种跟踪程序中,优先推荐使用这种方式进行高效的数据通信。
- 原文作者:DavidDi
- **原文链接:**https://www.ebpf.top/post/ebpf_trace_file_open_perf_output/
- **版权声明:**本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。