4. BCC Python接口入门:核心API与kprobe监控脚本实战
BCC(BPF Compiler Collection)不仅提供了丰富的命令行工具,还开放了一套强大的Python库接口,允许开发者用Python脚本直接编写、编译、加载和交互eBPF程序。本章将聚焦BCC Python库的核心API,并通过一个完整的kprobe监控脚本,带你从零掌握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(如哈希表、数组) | 读取内核统计计数、聚合数据 |
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脚本启动到内核事件捕获、数据回传的完整数据流:
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:快速输出内核事件数据。