iT邦幫忙

2025 iThome 鐵人賽

DAY 10
0

如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]

前言

eBPF 的追蹤功能是其最強大的特性之一,能夠讓我們深入系統內核,監控和分析系統行為。不僅如此,學習使用 eBPF 進行動態追蹤也有助於我們之後使用 sched_ext 開發排程器。
今天我們將全面學習 eBPF 的各種追蹤機制,包括 kprobe、uprobe、tracepoint、fentry/fexit 等技術。

通過本篇文章,您將學會:

  • 各種追蹤技術的原理和應用場景
  • 如何選擇適合的追蹤方式
  • 實戰開發系統追蹤工具

eBPF 追蹤技術概覽

追蹤技術分類

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/Kretprobe 深度剖析

基本原理

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

進階 kprobe 技巧

// 條件追蹤:只追蹤特定程序
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 靜態追蹤

Tracepoint 基礎

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 高效能追蹤

基本使用

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

Uprobe 使用者空間追蹤

基本 Uprobe

// 追蹤 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;
}

追蹤 Go 程式

筆者補充
前面有提到 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;
}

實戰演練:追蹤自定義的 kernel module

筆者在 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

感興趣的筆者可以點擊上方連結閱讀完整文章。

除錯與問題排查

1. 檢查追蹤點可用性

# 檢查 tracepoint 可用性
ls /sys/kernel/debug/tracing/events/sched/

# 檢查 kprobe 可用性
grep "do_sys_openat2" /proc/kallsyms

# 檢查 fentry 支援
bpftool feature | grep fentry

2. 追蹤程式除錯

// 使用 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

3. 效能分析

# 檢查 eBPF 程式統計
bpftool prog show
bpftool prog dump xlated id <ID>

# 檢查 Map 使用情況
bpftool map show
bpftool map dump id <ID>

總結

通過本篇文章,我們全面學習了 eBPF 追蹤技術:

  1. 掌握了各種追蹤機制:kprobe、tracepoint、fentry/fexit、uprobe
  2. 理解了技術選擇原則:穩定性、效能、靈活性的權衡
  3. 實作了完整的追蹤系統:多類型事件統一處理

這些追蹤技術是 eBPF 應用的核心,為後續學習調度器開發奠定了堅實基礎。

參考資源


上一篇
eBPF CO-RE
下一篇
eBPF skeleton
系列文
30 篇文帶你用 eBPF 與 Golang 打造 Linux Scheduler12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言