4. BCC Python接口入门:核心API与kprobe监控脚本实战

BCC(BPF Compiler Collection)不仅提供了丰富的命令行工具,还开放了一套强大的Python库接口,允许开发者用Python脚本直接编写、编译、加载和交互eBPF程序。本章将聚焦BCC Python库的核心API,并通过一个完整的kprobe监控脚本,带你从零掌握BCC Python编程的基本范式。

学习前提: 确保系统已安装bcc-python包(如 python3-bcc 或 bcc-devel),内核版本建议4.9+,且已开启BPF相关配置。

4.1 BCC Python库核心API概览

BCC Python库的核心是 BPF 类,它封装了从C代码编译、BPF系统调用到内核事件挂载的全流程。下表列出了最常用的API及其用途:

API / 方法 功能描述 典型使用场景
BPF(text="...") 编译并加载一段C语言编写的eBPF程序 所有BCC Python脚本的入口
attach_kprobe(event, fn_name) 将eBPF程序挂载到内核函数的入口(kprobe) 监控函数调用、参数捕获
attach_kretprobe(event, fn_name) 将eBPF程序挂载到内核函数的返回点(kretprobe) 捕获函数返回值、计算执行耗时
trace_print(fmt=None) 从内核trace_pipe读取并打印eBPF程序输出的格式化消息 快速调试、简单日志输出
perf_buffer_poll(timeout=-1) 轮询perf事件缓冲区,配合Python回调函数处理数据 高性能、结构化数据传输
get_table(name) 获取eBPF中定义的BPF_MAP(如哈希表、数组) 读取内核统计计数、聚合数据
核心流程: 编写C eBPF代码 → BPF() 编译加载 → attach_kprobe/kretprobe 挂载事件 → trace_print() 或 perf_buffer_poll() 读取数据。

4.2 第一个kprobe监控脚本:捕获 do_sys_open 调用

下面我们通过一个完整的示例,演示如何使用BCC Python库监控内核函数 do_sys_open 的每次调用,并打印出调用进程的PID、进程名以及打开的文件路径。

4.2.1 编写C语言eBPF内核态程序

在BCC中,eBPF程序以C语言字符串的形式嵌入Python脚本。以下代码定义了一个kprobe处理函数,它从 do_sys_open 的第二个参数(文件名指针)读取字符串,并通过 bpf_trace_printk 输出到内核trace管道:

#include <uapi/linux/ptrace.h>
#include <linux/fs.h>

// 定义kprobe处理函数,挂载到do_sys_open入口
int trace_open_entry(struct pt_regs *ctx, const char __user *filename, int flags)
{
    // 获取当前进程PID和进程名
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));

    // 从用户空间读取文件名(最多256字节)
    char fname[256];
    bpf_probe_read_user_str(fname, sizeof(fname), filename);

    // 输出到trace_pipe,格式:PID 进程名 文件名
    bpf_trace_printk("PID %d %s opened %s\\n", pid, comm, fname);
    return 0;
}

4.2.2 编写Python用户态加载与交互脚本

Python脚本负责编译上述C代码、挂载kprobe、并循环读取输出:

#!/usr/bin/python3
from bcc import BPF

# 1. 定义eBPF C程序(字符串形式)
bpf_text = """
#include <uapi/linux/ptrace.h>
#include <linux/fs.h>

int trace_open_entry(struct pt_regs *ctx, const char __user *filename, int flags)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));

    char fname[256];
    bpf_probe_read_user_str(fname, sizeof(fname), filename);

    bpf_trace_printk("PID %d %s opened %s\\n", pid, comm, fname);
    return 0;
}
"""

# 2. 编译并加载eBPF程序
b = BPF(text=bpf_text)

# 3. 挂载kprobe到内核函数 do_sys_open
b.attach_kprobe(event="do_sys_open", fn_name="trace_open_entry")

# 4. 输出提示信息
print("Tracing do_sys_open... Ctrl+C to exit.")
print("%-8s %-16s %s" % ("PID", "COMM", "FILENAME"))

# 5. 循环读取trace_print输出
try:
    b.trace_print(fmt="{1:<8} {2:<16} {3}")
except KeyboardInterrupt:
    print("\nDetaching...")
    exit()
注意事项:
  • bpf_trace_printk 有输出长度限制(约1024字节),且会写入内核trace缓冲区,生产环境建议使用perf buffer。
  • attach_kprobe 的第一个参数 event 必须是内核符号表中存在的函数名,可通过 cat /proc/kallsyms | grep do_sys_open 验证。
  • 运行脚本需要root权限:sudo python3 open_trace.py。

4.3 核心API深度解析

4.3.1 BPF 类:编译与加载引擎

BPF(text="...") 是BCC Python库的基石。它内部调用Clang/LLVM将C代码编译为BPF字节码,然后通过 bpf() 系统调用加载到内核。关键参数包括:

  • text:C语言源码字符串。
  • cflags:传递给Clang的编译选项列表(如 ["-I/usr/include"])。
  • debug:调试标志,可输出编译日志或BPF验证器信息。

4.3.2 attach_kprobe 与 attach_kretprobe

这两个方法用于将eBPF程序挂载到内核函数的入口和返回点。它们的参数完全一致:

参数 类型 说明
event str 内核函数名,如 "do_sys_open"
fn_name str eBPF C程序中定义的函数名
event_re str 可选,正则表达式匹配多个函数
pid int 可选,只监控指定PID的事件

4.3.3 trace_print:快速输出通道

trace_print(fmt=None) 从内核 /sys/kernel/debug/tracing/trace_pipe 读取eBPF程序通过 bpf_trace_printk 输出的内容。fmt 参数支持Python格式化字符串,用于重新排列字段。例如:

b.trace_print(fmt="{1:<8} {2:<16} {3}")

其中 {1} 对应输出中的第二个字段(PID),{2} 对应第三个字段(COMM),{3} 对应第四个字段(FILENAME)。

4.4 完整工作流程SVG图

下图展示了从Python脚本启动到内核事件捕获、数据回传的完整数据流:

BCC Python kprobe监控完整工作流程 BCC Python kprobe监控完整工作流程 Python脚本 BPF(text=...) BCC库 Clang编译 + bpf()加载 内核BPF子系统 验证器 + JIT编译 kprobe: do_sys_open attach_kprobe() eBPF程序执行 bpf_trace_printk() trace_pipe 内核trace缓冲区 trace_print() 格式化输出到终端 说明: 整个流程从Python脚本发起,经过BCC库编译、内核加载、kprobe挂载,最终在函数调用时触发eBPF程序执行,数据通过trace_pipe回传到用户态。

4.5 实战:扩展脚本——捕获返回值与执行耗时

结合 attach_kretprobe,我们可以同时监控函数的进入和返回,计算每次调用的执行时间。以下代码在上一示例基础上增加了kretprobe:

#!/usr/bin/python3
from bcc import BPF
import time

bpf_text = """
#include <uapi/linux/ptrace.h>

// 用于存储每个线程的进入时间戳
BPF_HASH(start, u32, u64);

int trace_open_entry(struct pt_regs *ctx, const char __user *filename, int flags)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 ts = bpf_ktime_get_ns();
    start.update(&pid, &ts);  // 记录进入时间

    char comm[16];
    bpf_get_current_comm(&comm, sizeof(comm));
    char fname[256];
    bpf_probe_read_user_str(fname, sizeof(fname), filename);

    bpf_trace_printk("ENTRY PID %d %s %s\\n", pid, comm, fname);
    return 0;
}

int trace_open_return(struct pt_regs *ctx)
{
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    u64 *tsp = start.lookup(&pid);
    if (tsp == 0) return 0;  // 未找到进入记录

    u64 delta = (bpf_ktime_get_ns() - *tsp) / 1000;  // 微秒
    start.delete(&pid);

    bpf_trace_printk("RETURN PID %d duration %lld us\\n", pid, delta);
    return 0;
}
"""

b = BPF(text=bpf_text)
b.attach_kprobe(event="do_sys_open", fn_name="trace_open_entry")
b.attach_kretprobe(event="do_sys_open", fn_name="trace_open_return")

print("Tracing do_sys_open with duration... Ctrl+C to exit.")
print("%-8s %-16s %-20s %s" % ("PID", "COMM", "EVENT", "DETAIL"))

try:
    b.trace_print(fmt="{1:<8} {2:<16} {3:<20} {4}")
except KeyboardInterrupt:
    print("\nDetaching...")
    exit()
关键点:
  • 使用 BPF_HASH 在kprobe和kretprobe之间共享数据(如时间戳)。
  • bpf_ktime_get_ns() 返回纳秒级时间戳,计算差值后转换为微秒输出。
  • kretprobe处理函数的参数只有 struct pt_regs *ctx,返回值通过 PT_REGS_RC(ctx) 宏获取。

4.6 常见问题与避坑指南

问题现象 可能原因 解决方案
脚本运行报错 Failed to attach kprobe 内核函数名不存在或被内核符号表隐藏 使用 grep function /proc/kallsyms 确认函数名;检查内核是否开启 CONFIG_KPROBES
trace_print 无输出 未挂载kprobe,或 bpf_trace_printk 格式错误 检查 attach_kprobe 是否成功;确认C代码中 bpf_trace_printk 格式字符串与输出字段匹配
编译错误:unknown type 缺少必要的内核头文件包含 在C代码开头添加 #include <uapi/linux/ptrace.h> 和 #include <linux/fs.h>
性能开销过高 kprobe挂载在频繁调用的函数上,且处理逻辑复杂 考虑使用tracepoint替代kprobe;减少 bpf_trace_printk 调用;使用perf buffer异步传输

4.7 本章小结

通过本章的学习,你已经掌握了BCC Python库的三大核心能力:

  • BPF类:编译加载eBPF C程序。
  • attach_kprobe / attach_kretprobe:挂载内核函数入口和返回点监控。
  • trace_print:快速输出内核事件数据。