eBPF 入門開發實踐教程十:使用 libbpf 開發程序—bootstrap

eBPF (Extended Berkeley Packet Filter) 是 Linux 內核上的一個強大的網絡和性能分析工具。它允許開發者在內核運行時動態加載、更新和運行用戶定義的代碼。

在本教程中,我們將瞭解內核態和用戶態的 eBPF 程序是如何協同工作的。我們還將學習如何使用原生的 libbpf 開發用戶態程序,將 eBPF 應用打包爲可執行文件,實現跨內核版本分發。

libbpf 庫,以及爲什麼需要使用它

libbpf 是一個 C 語言庫,伴隨內核版本分發,用於輔助 eBPF 程序的加載和運行。它提供了用於與 eBPF 系統交互的一組 C API,使開發者能夠更輕鬆地編寫用戶態程序來加載和管理 eBPF 程序。這些用戶態程序通常用於分析、監控或優化系統性能。

使用 libbpf 庫有以下優勢:

同時,libbpf 和 BTF(BPF Type Format)都是 eBPF 生態系統的重要組成部分。它們各自在實現跨內核版本兼容方面發揮着關鍵作用。BTF(BPF Type Format)是一種元數據格式,用於描述 eBPF 程序中的類型信息。BTF 的主要目的是提供一種結構化的方式,以描述內核中的數據結構,以便 eBPF 程序可以更輕鬆地訪問和操作它們。

BTF 在實現跨內核版本兼容方面的關鍵作用如下:

結合 libbpf 和 BTF,eBPF 程序可以在各種不同版本的內核上運行,而無需爲每個內核版本單獨編譯。這極大地提高了 eBPF 生態系統的可移植性和兼容性,降低了開發和維護的難度。

什麼是 bootstrap

Bootstrap 是一個使用 libbpf 的完整應用,它利用 eBPF 程序來跟蹤內核中的 exec() 系統調用(通過 SEC("tp/sched/sched_process_exec") handle_exec BPF 程序),這主要對應於新進程的創建(不包括 fork() 部分)。此外,它還跟蹤進程的 exit() 系統調用(通過 SEC("tp/sched/sched_process_exit") handle_exit BPF 程序),以瞭解每個進程何時退出。

這兩個 BPF 程序共同工作,允許捕獲關於新進程的有趣信息,例如二進制文件的文件名,以及測量進程的生命週期,並在進程結束時收集有趣的統計信息,例如退出代碼或消耗的資源量等。這是深入瞭解內核內部並觀察事物如何真正運作的良好起點。

Bootstrap 還使用 argp API(libc 的一部分)進行命令行參數解析,使得用戶可以通過命令行選項配置應用行爲。這種方式提供了靈活性,讓用戶能夠根據實際需求自定義程序行爲。雖然這些功能使用 eunomia-bpf 工具也可以實現,但是這裏我們使用 libbpf 可以在用戶態提供更高的可擴展性,不過也帶來了不少額外的複雜度。

Bootstrap

Bootstrap 分爲兩個部分:內核態和用戶態。內核態部分是一個 eBPF 程序,它跟蹤 exec() 和 exit() 系統調用。用戶態部分是一個 C 語言程序,它使用 libbpf 庫來加載和運行內核態程序,並處理從內核態程序收集的數據。

內核態 eBPF 程序 bootstrap.bpf.c

// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2020 Facebook */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, pid_t);
    __type(value, u64);
} exec_start SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} rb SEC(".maps");

const volatile unsigned long long min_duration_ns = 0;

SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
    struct task_struct *task;
    unsigned fname_off;
    struct event *e;
    pid_t pid;
    u64 ts;

    /* remember time exec() was executed for this PID */
    pid = bpf_get_current_pid_tgid() >> 32;
    ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);

    /* don't emit exec events when minimum duration is specified */
    if (min_duration_ns)
        return 0;

    /* reserve sample from BPF ringbuf */
    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;

    /* fill out the sample with data */
    task = (struct task_struct *)bpf_get_current_task();

    e->exit_event = false;
    e->pid = pid;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    fname_off = ctx->__data_loc_filename & 0xFFFF;
    bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);

    /* successfully submit it to user-space for post-processing */
    bpf_ringbuf_submit(e, 0);
    return 0;
}

SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
    struct task_struct *task;
    struct event *e;
    pid_t pid, tid;
    u64 id, ts, *start_ts, duration_ns = 0;
    
    /* get PID and TID of exiting thread/process */
    id = bpf_get_current_pid_tgid();
    pid = id >> 32;
    tid = (u32)id;

    /* ignore thread exits */
    if (pid != tid)
        return 0;

    /* if we recorded start of the process, calculate lifetime duration */
    start_ts = bpf_map_lookup_elem(&exec_start, &pid);
    if (start_ts)
        duration_ns = bpf_ktime_get_ns() - *start_ts;
    else if (min_duration_ns)
        return 0;
    bpf_map_delete_elem(&exec_start, &pid);

    /* if process didn't live long enough, return early */
    if (min_duration_ns && duration_ns < min_duration_ns)
        return 0;

    /* reserve sample from BPF ringbuf */
    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;

    /* fill out the sample with data */
    task = (struct task_struct *)bpf_get_current_task();

    e->exit_event = true;
    e->duration_ns = duration_ns;
    e->pid = pid;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    /* send data to user-space for post-processing */
    bpf_ringbuf_submit(e, 0);
    return 0;
}

這段代碼是一個內核態 eBPF 程序(bootstrap.bpf.c),主要用於跟蹤 exec() 和 exit() 系統調用。它通過 eBPF 程序捕獲進程的創建和退出事件,並將相關信息發送到用戶態程序進行處理。下面是對代碼的詳細解釋。

首先,我們引入所需的頭文件,定義 eBPF 程序的許可證以及兩個 eBPF maps:exec_start 和 rb。exec_start 是一個哈希類型的 eBPF map,用於存儲進程開始執行時的時間戳。rb 是一個環形緩衝區類型的 eBPF map,用於存儲捕獲的事件數據,並將其發送到用戶態程序。

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"

char LICENSE[] SEC("license") = "Dual BSD/GPL";

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __uint(max_entries, 8192);
    __type(key, pid_t);
    __type(value, u64);
} exec_start SEC(".maps");

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} rb SEC(".maps");

const volatile unsigned long long min_duration_ns = 0;

接下來,我們定義了一個名爲 handle_exec 的 eBPF 程序,它會在進程執行 exec() 系統調用時觸發。首先,我們從當前進程中獲取 PID,記錄進程開始執行的時間戳,然後將其存儲在 exec_start map 中。

SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
    // ...
    pid = bpf_get_current_pid_tgid() >> 32;
    ts = bpf_ktime_get_ns();
    bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);

    // ...
}

然後,我們從環形緩衝區 map rb 中預留一個事件結構,並填充相關數據,如進程 ID、父進程 ID、進程名等。之後,我們將這些數據發送到用戶態程序進行處理。

    // reserve sample from BPF ringbuf
    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;

    // fill out the sample with data
    task = (struct task_struct *)bpf_get_current_task();

    e->exit_event = false;
    e->pid = pid;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    fname_off = ctx->__data_loc_filename & 0xFFFF;
    bpf_probe_read_str(&e->filename, sizeof(e->filename)(void *)ctx + fname_off);

    // successfully submit it to user-space for post-processing
    bpf_ringbuf_submit(e, 0);
    return 0;

最後,我們定義了一個名爲 handle_exit 的 eBPF 程序,它會在進程執行 exit() 系統調用時觸發。首先,我們從當前進程中獲取 PID 和 TID(線程 ID)。如果 PID 和 TID 不相等,說明這是一個線程退出,我們將忽略此事件。

SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
    // ...
    id = bpf_get_current_pid_tgid();
    pid = id >> 32;
    tid = (u32)id;

    /* ignore thread exits */
    if (pid != tid)
        return 0;

    // ...
}

接着,我們查找之前存儲在 exec_start map 中的進程開始執行的時間戳。如果找到了時間戳,我們將計算進程的生命週期(持續時間),然後從 exec_start map 中刪除該記錄。如果未找到時間戳且指定了最小持續時間,則直接返回。

    // if we recorded start of the process, calculate lifetime duration
    start_ts = bpf_map_lookup_elem(&exec_start, &pid);
    if (start_ts)
        duration_ns = bpf_ktime_get_ns() - *start_ts;
    else if (min_duration_ns)
        return 0;
    bpf_map_delete_elem(&exec_start, &pid);

    // if process didn't live long enough, return early
    if (min_duration_ns && duration_ns < min_duration_ns)
        return 0;

然後,我們從環形緩衝區 map rb 中預留一個事件結構,並填充相關數據,如進程 ID、父進程 ID、進程名、進程持續時間等。最後,我們將這些數據發送到用戶態程序進行處理。

    /* reserve sample from BPF ringbuf */
    e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if (!e)
        return 0;

    /* fill out the sample with data */
    task = (struct task_struct *)bpf_get_current_task();

    e->exit_event = true;
    e->duration_ns = duration_ns;
    e->pid = pid;
    e->ppid = BPF_CORE_READ(task, real_parent, tgid);
    e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));

    /* send data to user-space for post-processing */
    bpf_ringbuf_submit(e, 0);
    return 0;
}

這樣,當進程執行 exec() 或 exit() 系統調用時,我們的 eBPF 程序會捕獲相應的事件,並將詳細信息發送到用戶態程序進行後續處理。這使得我們可以輕鬆地監控進程的創建和退出,並獲取有關進程的詳細信息。

除此之外,在 bootstrap.h 中,我們還定義了和用戶態交互的數據結構:

/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2020 Facebook */
#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H

#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127

struct event {
    int pid;
    int ppid;
    unsigned exit_code;
    unsigned long long duration_ns;
    char comm[TASK_COMM_LEN];
    char filename[MAX_FILENAME_LEN];
    bool exit_event;
};

#endif /* __BOOTSTRAP_H */

用戶態,bootstrap.c

// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2020 Facebook */
#include <argp.h>
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "bootstrap.h"
#include "bootstrap.skel.h"

static struct env {
    bool verbose;
    long min_duration_ms;
} env;

const char *argp_program_version = "bootstrap 0.0";
const char *argp_program_bug_address = "<bpf@vger.kernel.org>";
const char argp_program_doc[] =
"BPF bootstrap demo application.\n"
"\n"
"It traces process start and exits and shows associated \n"
"information (filename, process duration, PID and PPID, etc).\n"
"\n"
"USAGE: ./bootstrap [-d <min-duration-ms>] [-v]\n";

static const struct argp_option opts[] = {
    { "verbose"'v', NULL, 0, "Verbose debug output" },
    { "duration"'d'"DURATION-MS", 0, "Minimum process duration (ms) to report" },
    {},
};

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
    switch (key) {
    case 'v':
        env.verbose = true;
        break;
    case 'd':
        errno = 0;
        env.min_duration_ms = strtol(arg, NULL, 10);
        if (errno || env.min_duration_ms <= 0) {
            fprintf(stderr, "Invalid duration: %s\n", arg);
            argp_usage(state);
        }
        break;
    case ARGP_KEY_ARG:
        argp_usage(state);
        break;
    default:
        return ARGP_ERR_UNKNOWN;
    }
    return 0;
}

static const struct argp argp = {
    .options = opts,
    .parser = parse_arg,
    .doc = argp_program_doc,
};

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    if (level == LIBBPF_DEBUG && !env.verbose)
        return 0;
    return vfprintf(stderr, format, args);
}

static volatile bool exiting = false;

static void sig_handler(int sig)
{
    exiting = true;
}

static int handle_event(void *ctx, void *data, size_t data_sz)
{
    const struct event *e = data;
    struct tm *tm;
    char ts[32];
    time_t t;

    time(&t);
    tm = localtime(&t);
    strftime(ts, sizeof(ts)"%H:%M:%S", tm);

    if (e->exit_event) {
        printf("%-8s %-5s %-16s %-7d %-7d [%u]",
               ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code);
        if (e->duration_ns)
            printf(" (%llums)", e->duration_ns / 1000000);
        printf("\n");
    } else {
        printf("%-8s %-5s %-16s %-7d %-7d %s\n",
               ts, "EXEC", e->comm, e->pid, e->ppid, e->filename);
    }

    return 0;
}

int main(int argc, char **argv)
{
    struct ring_buffer *rb = NULL;
    struct bootstrap_bpf *skel;
    int err;

    /* Parse command line arguments */
    err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
    if (err)
        return err;

    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Cleaner handling of Ctrl-C */
    signal(SIGINT, sig_handler);
    signal(SIGTERM, sig_handler);

    /* Load and verify BPF application */
    skel = bootstrap_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open and load BPF skeleton\n");
        return 1;
    }

    /* Parameterize BPF code with minimum duration parameter */
    skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;

    /* Load & verify BPF programs */
    err = bootstrap_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* Attach tracepoints */
    err = bootstrap_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    /* Set up ring buffer polling */
    rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
    if (!rb) {
        err = -1;
        fprintf(stderr, "Failed to create ring buffer\n");
        goto cleanup;
    }

    /* Process events */
    printf("%-8s %-5s %-16s %-7s %-7s %s\n",
           "TIME""EVENT""COMM""PID""PPID""FILENAME/EXIT CODE");
    while (!exiting) {
        err = ring_buffer__poll(rb, 100 /* timeout, ms */);
        /* Ctrl-C will cause -EINTR */
        if (err == -EINTR) {
            err = 0;
            break;
        }
        if (err < 0) {
            printf("Error polling perf buffer: %d\n", err);
            break;
        }
    }

cleanup:
    /* Clean up */
    ring_buffer__free(rb);
    bootstrap_bpf__destroy(skel);

    return err < 0 ? -err : 0;
}

這個用戶態程序主要用於加載、驗證、附加 eBPF 程序,以及接收 eBPF 程序收集的事件數據,並將其打印出來。我們將分析一些關鍵部分。

首先,我們定義了一個 env 結構,用於存儲命令行參數:

static struct env {
    bool verbose;
    long min_duration_ms;
} env;

接下來,我們使用 argp 庫來解析命令行參數:

static const struct argp_option opts[] = {
    { "verbose"'v', NULL, 0, "Verbose debug output" },
    { "duration"'d'"DURATION-MS", 0, "Minimum process duration (ms) to report" },
    {},
};

static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
    // ...
}

static const struct argp argp = {
    .options = opts,
    .parser = parse_arg,
    .doc = argp_program_doc,
};

main() 函數中,首先解析命令行參數,然後設置 libbpf 的打印回調函數 libbpf_print_fn,以便在需要時輸出調試信息:

err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
if (err)
    return err;

libbpf_set_print(libbpf_print_fn);

接下來,我們打開 eBPF 腳手架(skeleton)文件,將最小持續時間參數傳遞給 eBPF 程序,並加載和附加 eBPF 程序:

skel = bootstrap_bpf__open();
if (!skel) {
    fprintf(stderr, "Failed to open and load BPF skeleton\n");
    return 1;
}

skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;

err = bootstrap_bpf__load(skel);
if (err) {
    fprintf(stderr, "Failed to load and verify BPF skeleton\n");
    goto cleanup;
}

err = bootstrap_bpf__attach(skel);
if (err) {
    fprintf(stderr, "Failed to attach BPF skeleton\n");
    goto cleanup;
}

然後,我們創建一個環形緩衝區(ring buffer),用於接收 eBPF 程序發送的事件數據:

rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) {
    err = -1;
    fprintf(stderr, "Failed to create ring buffer\n");
    goto cleanup;
}

handle_event() 函數會處理從 eBPF 程序收到的事件。根據事件類型(進程執行或退出),它會提取並打印事件信息,如時間戳、進程名、進程 ID、父進程 ID、文件名或退出代碼等。

最後,我們使用 ring_buffer__poll() 函數輪詢環形緩衝區,處理收到的事件數據:

while (!exiting) {
    err = ring_buffer__poll(rb, 100 /* timeout, ms */);
    // ...
}

當程序收到 SIGINT 或 SIGTERM 信號時,它會最後完成清理、退出操作,關閉和卸載 eBPF 程序:

cleanup:
 /* Clean up */
 ring_buffer__free(rb);
 bootstrap_bpf__destroy(skel);

 return err < 0 ? -err : 0;
}

安裝依賴

構建示例需要 clang、libelf 和 zlib。包名在不同的發行版中可能會有所不同。

在 Ubuntu/Debian 上,你需要執行以下命令:

sudo apt install clang libelf1 libelf-dev zlib1g-dev

在 CentOS/Fedora 上,你需要執行以下命令:

sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel

編譯運行

編譯運行上述代碼:

$ make
  BPF      .output/bootstrap.bpf.o
  GEN-SKEL .output/bootstrap.skel.h
  CC       .output/bootstrap.o
  BINARY   bootstrap
$ sudo ./bootstrap 
[sudo] password for yunwei: 
TIME     EVENT COMM             PID     PPID    FILENAME/EXIT CODE
03:16:41 EXEC  sh               110688  80168   /bin/sh
03:16:41 EXEC  which            110689  110688  /usr/bin/which
03:16:41 EXIT  which            110689  110688  [0] (0ms)
03:16:41 EXIT  sh               110688  80168   [0] (0ms)
03:16:41 EXEC  sh               110690  80168   /bin/sh
03:16:41 EXEC  ps               110691  110690  /usr/bin/ps
03:16:41 EXIT  ps               110691  110690  [0] (49ms)
03:16:41 EXIT  sh               110690  80168   [0] (51ms)

總結

通過這個實例,我們瞭解瞭如何將 eBPF 程序與用戶態程序結合使用。這種結合爲開發者提供了一個強大的工具集,可以實現跨內核和用戶空間的高效數據收集和處理。通過使用 eBPF 和 libbpf,您可以構建更高效、可擴展和安全的監控和性能分析工具。

如果您希望學習更多關於 eBPF 的知識和實踐,請查閱 eunomia-bpf 的官方文檔:https://github.com/eunomia-bpf/eunomia-bpf 。您還可以訪問我們的教程代碼倉庫 https://github.com/eunomia-bpf/bpf-developer-tutorial 以獲取更多示例和完整的教程。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/460Hop9iGW8Zid-a23wvNA