1. 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 事件的函数;
  • 持续消费事件程序;

2. 代码实现

原始代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#!/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 的程序如下:

 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
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;
}
"""

这里将详细介绍我们进行的相关调整:

  1. 定义了内核 Probe 程序与用户空间程序通信的结构体 event_data_t,包含 pidfilename 两个字段;
  2. 使用 BCC 提供的宏 BPF_PERF_OUTPUT(open_events) 完成内核中 open_events 变量的定义;
  3. trace_syscall_open 函数中,增加变量的定义 struct event_data_t evt = {}; ,需要注意的是结构体变量 evt.fname 的赋值,需要使用 eBPF 提供的帮助函数 bpf_probe_read 来帮助,这是因为内核对于非简单类型的赋值需要进行安全边界的检查,避免在内核中进行越界访问,破坏内核稳定性和安全性的保障;
  4. 最后,使用 open_events.perf_submitevent 数据发送至用户空间;

上述代码,完成了我们在内核 Probe 程序中的所有工作。用户空间 Python 程序则需要定义 event 消费函数,并使用 perf_buffer_poll 函数轮训消费即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 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 代码中,当前我们只需要定义事件处理函数,将事件与函数进行关联,然后持续轮询数据即可。

  1. 我们定义了事件处理函数 print_event,然后读取出对应的数据并生成结构数据 ,event = b["open_events"].event(data),此处不用再声明 Python 中的结构体变量,BCC 已经协助处理,否则需要我们显示定义,在一些早期的 BCC 代码中还可以看到手工转换的场景。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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
    
  2. 然后在主体函数中使用 b.perf_buffer_poll() 持续轮询即可;

运行结果如下:

1
2
3
#./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 方式,在直接编写的各种跟踪程序中,优先推荐使用这种方式进行高效的数据通信。