如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
eBPF 的追蹤功能是其最強大的特性之一,能夠讓我們深入系統內核,監控和分析系統行為。不僅如此,學習使用 eBPF 進行動態追蹤也有助於我們之後使用 sched_ext 開發排程器。
今天我們將全面學習 eBPF 的各種追蹤機制,包括 kprobe、uprobe、tracepoint、fentry/fexit 等技術。
通過本篇文章,您將學會:
eBPF 追蹤技術
├── 核心空間追蹤
│ ├── kprobe/kretprobe - 動態核心函數追蹤
│ ├── tracepoint - 靜態追蹤點
│ ├── fentry/fexit - 高效能函數入口/出口追蹤
│ ├── raw_tracepoint - 原始追蹤點
│ └── perf_event - 效能事件追蹤
├── 使用者空間追蹤
│ ├── uprobe/uretprobe - 使用者程式函數追蹤
│ └── USDT - 使用者定義靜態追蹤點
└── 網路追蹤
├── socket filter - Socket 過濾追蹤
├── TC (Traffic Control) - 流量控制追蹤
└── XDP - 數據包處理追蹤
筆者補充
uretprobe 在一些情況下是不可用的,例如:golang 會動態的增減 goroutine 的 stack 大小,所以使用 uretprobe 對 golang 撰寫的特定函式時,就有可能導致程式 crash。詳細資訊請參考:https://www.cnxct.com/golang-uretprobe-tracing/
技術 | 穩定性 | 效能 | 靈活性 | 使用場景 |
---|---|---|---|---|
tracepoint | 高 | 高 | 中 | 穩定的系統監控 |
kprobe | 中 | 中 | 高 | 動態函數分析 |
fentry/fexit | 高 | 最高 | 高 | 高效能追蹤 |
uprobe | 中 | 低 | 高 | 應用程式分析 |
kprobe 通過在目標函數入口插入斷點,當函數被調用時觸發 eBPF 程式:
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
// 追蹤系統調用統計
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, u64);
} syscall_count SEC(".maps");
// 函數執行時間統計
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 10240);
__type(key, u32);
__type(value, u64);
} function_time SEC(".maps");
// 追蹤 sys_openat
SEC("kprobe/do_sys_openat2")
int kprobe_openat_entry(struct pt_regs *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 ts = bpf_ktime_get_ns();
// 記錄函數進入時間
bpf_map_update_elem(&function_time, &pid, &ts, BPF_ANY);
// 取得檔案路徑參數
int dfd = (int)PT_REGS_PARM1(ctx);
struct filename *filename = (struct filename *)PT_REGS_PARM2(ctx);
if (filename) {
char path[256];
BPF_CORE_READ_STR(path, sizeof(path), filename, name);
bpf_printk("PID %d opening: %s", pid, path);
}
return 0;
}
// 追蹤 sys_openat 返回
SEC("kretprobe/do_sys_openat2")
int kretprobe_openat_exit(struct pt_regs *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 *start_ts = bpf_map_lookup_elem(&function_time, &pid);
if (start_ts) {
u64 end_ts = bpf_ktime_get_ns();
u64 duration = end_ts - *start_ts;
bpf_printk("PID %d openat duration: %llu ns", pid, duration);
// 清理時間記錄
bpf_map_delete_elem(&function_time, &pid);
// 更新系統調用計數
u64 *count = bpf_map_lookup_elem(&syscall_count, &pid);
if (count) {
(*count)++;
} else {
u64 initial = 1;
bpf_map_update_elem(&syscall_count, &pid, &initial, BPF_ANY);
}
}
// 取得返回值
long ret = PT_REGS_RC(ctx);
if (ret < 0) {
bpf_printk("PID %d openat failed: %ld", pid, ret);
}
return 0;
}
// 條件追蹤:只追蹤特定程序
SEC("kprobe/vfs_read")
int kprobe_vfs_read(struct pt_regs *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 只追蹤特定 PID
if (pid != TARGET_PID) {
return 0;
}
struct file *file = (struct file *)PT_REGS_PARM1(ctx);
size_t count = (size_t)PT_REGS_PARM3(ctx);
// 取得檔案資訊
struct dentry *dentry = BPF_CORE_READ(file, f_path.dentry);
if (dentry) {
char filename[64];
BPF_CORE_READ_STR(filename, sizeof(filename), dentry, d_name.name);
bpf_printk("Reading %zu bytes from %s", count, filename);
}
return 0;
}
// 參數過濾:只追蹤特定檔案類型
SEC("kprobe/vfs_open")
int kprobe_vfs_open(struct pt_regs *ctx)
{
struct path *path = (struct path *)PT_REGS_PARM1(ctx);
struct dentry *dentry = BPF_CORE_READ(path, dentry);
if (dentry) {
char filename[64];
BPF_CORE_READ_STR(filename, sizeof(filename), dentry, d_name.name);
// 只追蹤 .log 檔案
int len = bpf_probe_read_str(filename, sizeof(filename),
BPF_CORE_READ(dentry, d_name.name));
if (len > 4 &&
filename[len-4] == '.' &&
filename[len-3] == 'l' &&
filename[len-2] == 'o' &&
filename[len-1] == 'g') {
bpf_printk("Opening log file: %s", filename);
}
}
return 0;
}
Tracepoint 是核心中預定義的靜態追蹤點,效能高且穩定:
// 追蹤程序排程事件
SEC("tp/sched/sched_switch")
int trace_sched_switch(struct trace_event_raw_sched_switch *ctx)
{
// 取得切換資訊
u32 prev_pid = ctx->prev_pid;
u32 next_pid = ctx->next_pid;
char prev_comm[16], next_comm[16];
__builtin_memcpy(prev_comm, ctx->prev_comm, sizeof(prev_comm));
__builtin_memcpy(next_comm, ctx->next_comm, sizeof(next_comm));
bpf_printk("CPU switch: %s(%d) -> %s(%d)",
prev_comm, prev_pid, next_comm, next_pid);
return 0;
}
// 追蹤記憶體分配
SEC("tp/kmem/kmalloc")
int trace_kmalloc(struct trace_event_raw_kmalloc *ctx)
{
size_t bytes_req = ctx->bytes_req;
size_t bytes_alloc = ctx->bytes_alloc;
void *ptr = ctx->ptr;
if (bytes_req > 4096) { // 只追蹤大分配
bpf_printk("Large kmalloc: req=%zu alloc=%zu ptr=%p",
bytes_req, bytes_alloc, ptr);
}
return 0;
}
// 追蹤網路接收
SEC("tp/net/netif_receive_skb")
int trace_netif_receive_skb(struct trace_event_raw_netif_receive_skb *ctx)
{
char name[16];
__builtin_memcpy(name, ctx->name, sizeof(name));
bpf_printk("Network RX on %s: len=%d", name, ctx->len);
return 0;
}
// 通用系統調用進入追蹤
SEC("tp/raw_syscalls/sys_enter")
int trace_sys_enter(struct trace_event_raw_sys_enter *ctx)
{
u64 syscall_id = ctx->id;
u32 pid = bpf_get_current_pid_tgid() >> 32;
// 只追蹤特定系統調用
switch (syscall_id) {
case 2: // open
case 257: // openat
case 3: // close
case 0: // read
case 1: // write
bpf_printk("PID %d syscall %lld", pid, syscall_id);
break;
default:
return 0;
}
return 0;
}
// 系統調用退出追蹤
SEC("tp/raw_syscalls/sys_exit")
int trace_sys_exit(struct trace_event_raw_sys_exit *ctx)
{
long ret = ctx->ret;
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (ret < 0) {
bpf_printk("PID %d syscall failed: %ld", pid, ret);
}
return 0;
}
fentry/fexit 是最新的追蹤技術,提供最佳效能:
// 函數入口追蹤
SEC("fentry/do_unlinkat")
int BPF_PROG(fentry_unlinkat, int dfd, struct filename *name)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
char filename[64];
BPF_CORE_READ_STR(filename, sizeof(filename), name, name);
bpf_printk("PID %d unlinking: %s", pid, filename);
return 0;
}
// 函數退出追蹤
SEC("fexit/do_unlinkat")
int BPF_PROG(fexit_unlinkat, int dfd, struct filename *name, long ret)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (ret == 0) {
char filename[64];
BPF_CORE_READ_STR(filename, sizeof(filename), name, name);
bpf_printk("PID %d successfully unlinked: %s", pid, filename);
} else {
bpf_printk("PID %d unlink failed: %ld", pid, ret);
}
return 0;
}
// 追蹤複雜結構體參數
SEC("fentry/tcp_sendmsg")
int BPF_PROG(fentry_tcp_sendmsg, struct sock *sk, struct msghdr *msg, size_t size)
{
// 取得 socket 資訊
u16 family = BPF_CORE_READ(sk, __sk_common.skc_family);
u16 sport = BPF_CORE_READ(sk, __sk_common.skc_num);
u16 dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
if (family == AF_INET) {
u32 saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
u32 daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
bpf_printk("TCP send: %pI4:%d -> %pI4:%d, size=%zu",
&saddr, sport, &daddr, dport, size);
}
return 0;
}
// 追蹤網路接收
SEC("fentry/tcp_recvmsg")
int BPF_PROG(fentry_tcp_recvmsg, struct sock *sk, struct msghdr *msg,
size_t len, int flags, int *addr_len)
{
u16 sport = BPF_CORE_READ(sk, __sk_common.skc_num);
u16 dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
bpf_printk("TCP recv: port %d<-%d, len=%zu", sport, dport, len);
return 0;
}
// 追蹤 libc 函數
SEC("uprobe/libc.so.6:malloc")
int uprobe_malloc(struct pt_regs *ctx)
{
size_t size = (size_t)PT_REGS_PARM1(ctx);
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (size > 1024) { // 只追蹤大分配
bpf_printk("PID %d malloc large: %zu bytes", pid, size);
}
return 0;
}
SEC("uretprobe/libc.so.6:malloc")
int uretprobe_malloc(struct pt_regs *ctx)
{
void *ptr = (void *)PT_REGS_RC(ctx);
u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("PID %d malloc returned: %p", pid, ptr);
return 0;
}
// 追蹤 free
SEC("uprobe/libc.so.6:free")
int uprobe_free(struct pt_regs *ctx)
{
void *ptr = (void *)PT_REGS_PARM1(ctx);
u32 pid = bpf_get_current_pid_tgid() >> 32;
if (ptr) {
bpf_printk("PID %d freeing: %p", pid, ptr);
}
return 0;
}
筆者補充
前面有提到 golang 開發的程式無法順利使用 uretprobe,不過 uprobe 仍是沒問題的。
// 追蹤 Go runtime
SEC("uprobe//usr/bin/myapp:runtime.mallocgc")
int uprobe_go_malloc(struct pt_regs *ctx)
{
u64 size = (u64)PT_REGS_PARM1(ctx);
u32 pid = bpf_get_current_pid_tgid() >> 32;
bpf_printk("Go PID %d mallocgc: %llu bytes", pid, size);
return 0;
}
// 追蹤 Go GC
SEC("uprobe//usr/bin/myapp:runtime.gcStart")
int uprobe_go_gc_start(struct pt_regs *ctx)
{
u32 pid = bpf_get_current_pid_tgid() >> 32;
u64 ts = bpf_ktime_get_ns();
bpf_printk("Go PID %d GC start at %llu", pid, ts);
return 0;
}
筆者在 Debug gtp5g kernel module using stacktrace and eBPF 一文中詳細的說明如何使用 eBPF 追蹤自定義的 kernel module,效果如下:
sudo cat /sys/kernel/debug/tracing/trace_pipe
<...>-236797 [009] b.s21 6141919.013036: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=236797, TGID=236797, CPU=9
<idle>-0 [009] b.s31 6141920.013210: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141921.014070: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141922.013615: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141923.013975: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141924.014871: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141925.013730: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
kubelite-3377186 [009] b.s21 6141926.013908: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=3377186, TGID=3376931, CPU=9
<idle>-0 [009] b.s31 6141927.014084: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141928.015013: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141929.014465: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141930.014600: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141931.013976: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141932.014142: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
<idle>-0 [009] b.s31 6141933.014331: bpf_trace_printk: gtp5g_xmit_skb_ipv4: PID=0, TGID=0, CPU=9
感興趣的筆者可以點擊上方連結閱讀完整文章。
# 檢查 tracepoint 可用性
ls /sys/kernel/debug/tracing/events/sched/
# 檢查 kprobe 可用性
grep "do_sys_openat2" /proc/kallsyms
# 檢查 fentry 支援
bpftool feature | grep fentry
// 使用 bpf_printk 除錯
SEC("kprobe/do_sys_openat2")
int debug_kprobe(struct pt_regs *ctx)
{
bpf_printk("kprobe triggered: PID=%d", bpf_get_current_pid_tgid() >> 32);
return 0;
}
# 檢視 debug 輸出
cat /sys/kernel/debug/tracing/trace_pipe
# 檢查 eBPF 程式統計
bpftool prog show
bpftool prog dump xlated id <ID>
# 檢查 Map 使用情況
bpftool map show
bpftool map dump id <ID>
通過本篇文章,我們全面學習了 eBPF 追蹤技術:
這些追蹤技術是 eBPF 應用的核心,為後續學習調度器開發奠定了堅實基礎。