構建並運行 eBPF 應用 - Part 1

本文將介紹如何使用 C 和 Golang 編寫第一個 eBPF 程序。我們將在第一部分介紹實際的 eBPF 程序,在第二部分介紹用戶空間應用程序。

準備工作

本文開發所運行的操作系統是:

OS: Ubuntu 22.04
Linux Header Version: 6.5.0–14-generic

還通過 apt 安裝了一些依賴項:

sudo apt-get -y install libbpf bpfcc-tools

完成 ebpf 的代碼除了需要 Go 的知識之外,讀者對 C 語言編程需要熟悉。

什麼是 eBPF[1]?

有許多博客 / 網站對 eBPF 進行了深入探討(請查看資源部分),但爲了簡單起見,我們姑且認爲 eBPF 是一種在不修改內核源代碼的情況下使用模塊擴展 Linux 內核的方法。

筆者認爲 eBPF 就是是內核的一個鉤子,允許在內核空間運行邏輯。

User Space vs Kernel Space

當我們談論內核空間時,通常指的是操作系統。這是一個特權區域,可以完全訪問硬件和軟件資源。當我們談論用戶空間時,這裏通常是你運行日常程序(如谷歌瀏覽器)的地方。用戶空間的訪問權限是有限制的。

選擇要掛鉤的事件

這裏給大家推薦一個學習上手開發 eBPF 的好的項目:kepler[2]Kepler 的一項工作是通過 CPU 計劃切換,計算每個進程(由 PID 標識)使用 CPU 的時間。

CPU 調度 [3] 是指在正在執行的進程之間進行切換,以便更好地利用處理能力(當一個進程受阻時,CPU 會暫停處理該進程,並切換到另一個進程)。

因此,如果我們想複製這一功能,我們可以這樣做:

這樣我們就能大致估算出每個流程需要多少時間,同時要記住,一個流程會被安排多次。

瞭解 event 的格式

在 BPF 事件中,每個事件在運行函數時都會包含一些稱爲 "上下文" 的內容。這些上下文實質上就是事件發出的信息。我們需要定義一個 C 結構來保存這些信息,但首先,我們需要獲得該結構的格式。我們可以通過運行下面的代碼來實現:

$ sudo cat /sys/kernel/debug/tracing/events/sched/sched_switch/format
name: sched_switch
ID: 327
format:
field:unsigned short common_type; offset:0; size:2; signed:0;
field:unsigned char common_flags; offset:2; size:1; signed:0;
field:unsigned char common_preempt_count; offset:3; size:1; signed:0;
field:int common_pid; offset:4; size:4; signed:1;
field:char prev_comm[16]; offset:8; size:16; signed:0;
field:pid_t prev_pid; offset:24; size:4; signed:1;
field:int prev_prio; offset:28; size:4; signed:1;
field:long prev_state; offset:32; size:8; signed:1;
field:char next_comm[16]; offset:40; size:16; signed:0;
field:pid_t next_pid; offset:56; size:4; signed:1;
field:int next_prio; offset:60; size:4; signed:1;
print fmt: "prev_comm=%s prev_pid=%d prev_prio=%d prev_state=%s%s ==> next_comm=%s next_pid=%d next_prio=%d", REC->prev_comm, REC->prev_pid, REC->prev_prio, (REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1)) ? __print_flags(REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1)"|"{ 0x00000001, "S" }{ 0x00000002, "D" }{ 0x00000004, "T" }{ 0x00000008, "t" }{ 0x00000010, "X" }{ 0x00000020, "Z" }{ 0x00000040, "P" }{ 0x00000080, "I" }) : "R", REC->prev_state & (((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) ? "+" : "", REC->next_comm, REC->next_pid, REC->next_prio

要處理的信息相當多,但爲了簡單起見,我們可以說,在這個用例中,我們不需要關心任何以 common_ 爲前綴的字段。這樣我們就有了以下字段:

char prev_comm[16];
pid_t prev_pid;
int prev_prio;
long prev_state;
char next_comm[16];
pid_t next_pid;
int next_prio;

然後我們就可以利用這些信息創建下面的 C 結構:

struct sched_switch_args {
  char prev_comm[16];
  int prev_pid;
  int prev_prio;
  long prev_state;
  char next_comm[16];
  int next_pid;
  int next_prio;
};

現在,首先要注意的是,我將 pid_t 類型改爲 int。這是因爲 pid_t 的底層類型是 int。我們可以使用 pid_t 類型,但需要依賴 sys/types.h,而在本例中我們並不需要。有關這方面的更多信息,可以閱讀這篇文章 [4]。

創建 BPF Map

要從內核空間收集數據並在用戶空間訪問這些數據,我們需要使用一種叫做 BPF 映射的東西。BPF 映射是推送到用戶空間的數據結構。在本例中,我們將使用基於 PID 的哈希表類型。這需要我們創建三個結構,即:

struct key_t {
  // This is the process ID
  // which we will use to identify
  // in the hash map
  __u32 pid;
};
struct val_t {
  // used to understand the start time of the process
  __u64 start_time;
  // used to store the elapsed time of the process
  __u64 elapsed_time;
};
struct {
  // The type of BPF map we are creating
  __uint(type, BPF_MAP_TYPE_HASH);
  // specifying the type to be used for the key
  __type(key, struct key_t);
  // specifying the type to be used as the value
  __type(value, struct val_t);
  // max amount of entries to store in the map
  __uint(max_entries, 10240);
  // name of the map as well as a section macro
  // from the bpf lib to designate this type
  // as a BPF map
} process_time_map SEC(".maps");

我已經添加了註釋,解釋這些結構中每一行的作用。

創建 eBPF 函數

最後一步是創建 eBPF 函數。爲此,我們需要一個 eBPF 程序。這是一個 C 語言函數,帶有一些宏標識,這樣我們就可以使用先前定義的類型進行交互,例如:

SEC("tracepoint/sched/sched_switch")
int cpu_processing_time(struct sched_switch_args *ctx) {
  // get the current time in ns
  __u64 ts = bpf_ktime_get_ns();
  // we need to check if the process is in our map
  struct key_t prev_key = {
    .pid = ctx->prev_pid,
  };
  struct val_t *val = bpf_map_lookup_elem(&process_time_map, &prev_key);
  // if the previous PID does not exist it means that we just started
  // watching or we missed the start somehow
  // so we ignore it for now
  if (val) {
  // Calculate and store the elapsed time for the process and we reset the
  // start time so we can measure the next cycle of that process
    __u64 elapsed_time = ts - val->start_time;
    struct val_t new_val = {.start_time = ts, .elapsed_time = elapsed_time};
    bpf_map_update_elem(&process_time_map, &prev_key, &new_val, BPF_ANY);
    return 0;
  };
  // we need to check if the next process is in our map
  // if it's not we need to set initial time
  struct key_t next_key = {
  .pid = ctx->next_pid,
  };
  struct val_t *next_val = bpf_map_lookup_elem(&process_time_map, &prev_key);
  if (!next_val) {
    struct val_t next_new_val = {.start_time = ts};
    bpf_map_update_elem(&process_time_map, &next_key, &next_new_val, BPF_ANY);
    return 0;
  }
  return 0;
}

下面的宏指定了該函數要連接的事件。

SEC("tracepoint/sched/sched_switch")

這一行就是我們查找 BPF 映射數據的方法。我們使用一個唯一的鍵,並將其傳遞給 bpf_map_lookup_elem 函數,該函數將返回一個 val_t 類型的值(我們之前定義的)。如果該鍵下沒有值,該函數將返回 NULL,請注意我們需要將 BPF 映射類型作爲 &process_time_map 傳遞。

struct val_t *val = bpf_map_lookup_elem(&process_time_map, &prev_key);

這一行是我們向 BPF 地圖添加數據的過程。我們將傳遞鍵(本例中爲 &prev_key)和鍵值(&new_val),後者將把該值存儲到 BPF 映射中。請再次注意,我們傳遞的是映射類型。BPF_ANY 用於將鍵更新爲新值,或者在鍵不存在時創建新值(參見文檔)。

bpf_map_update_elem(&process_time_map, &prev_key, &new_val, BPF_ANY);

這樣,我們就完成了功能,不過,我們還需要在代碼中添加最後一行:

char _license[] SEC("license") = "Dual MIT/GPL";

由於 eBPF 是以 GPL 許可的,這意味着所有集成軟件也需要與 GPL 兼容。如果沒有這一行,就無法將代碼加載到內核中。因此,我們的最終代碼片段如下(我添加了需要包含的 C 頭文件):

#include <linux/sched.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <stddef.h>

#ifndef TASK_COMM_LEN
#define TASK_COMM_LEN 16
#endif

struct key_t {
  __u32 pid;
};

struct val_t {
  __u64 start_time;
  __u64 elapsed_time;
};

struct {
  __uint(type, BPF_MAP_TYPE_HASH);
  __type(key, struct key_t);
  __type(value, struct val_t);
  __uint(max_entries, 10240);
} process_time_map SEC(".maps");

// this is the structure of the sched_switch event
struct sched_switch_args {
  char prev_comm[TASK_COMM_LEN];
  int prev_pid;
  int prev_prio;
  long prev_state;
  char next_comm[TASK_COMM_LEN];
  int next_pid;
  int next_prio;
};

SEC("tracepoint/sched/sched_switch")
int cpu_processing_time(struct sched_switch_args *ctx) {
  // get the current time in ns
  __u64 ts = bpf_ktime_get_ns();
  // we need to check if the process is in our map
  struct key_t prev_key = {
    .pid = ctx->prev_pid,
  };
  struct val_t *val = bpf_map_lookup_elem(&process_time_map, &prev_key);
  // if the previous PID does not exist it means that we just started
  // watching or we missed the start somehow
  // so we ignore it for now
  if (val) {
  // Calculate and store the elapsed time for the process and we reset the
  // start time so we can measure the next cycle of that process
    __u64 elapsed_time = ts - val->start_time;
    struct val_t new_val = {.start_time = ts, .elapsed_time = elapsed_time};
    bpf_map_update_elem(&process_time_map, &prev_key, &new_val, BPF_ANY);
    return 0;
  };
  // we need to check if the next process is in our map
  // if it's not we need to set initial time
  struct key_t next_key = {
    .pid = ctx->next_pid,
  };
  struct val_t *next_val = bpf_map_lookup_elem(&process_time_map, &prev_key);
  if (!next_val) {
    struct val_t next_new_val = {.start_time = ts};
    bpf_map_update_elem(&process_time_map, &next_key, &next_new_val, BPF_ANY);
    return 0;
  }
  return 0;
}

char _license[] SEC("license") = "Dual MIT/GPL";

這樣,我們就完成了 BPF 內核側的程序。在本系列的下一篇文章中,我將介紹用 GO 語言編寫用戶空間程序,並使用名爲 bpf2go[5] 的工具來幫助我們實現程序綁定。

參考資料

[1]

what-is-ebpf: https://ebpf.io/what-is-ebpf/#what-is-ebpf

[2]

kepler github: https://github.com/sustainable-computing-io/kepler/tree/main

[3]

cpu-scheduling: https://www.studytonight.com/operating-system/cpu-scheduling

[4]

glibc-223: https://ftp.gnu.org/old-gnu/Manuals/glibc-2.2.3/html_node/libc_554.html#:~:text=The%20pid_t%20data%20type%20is,Function%3A%20pid_t%20getppid%20%28void%29

[5]

bpf2go: https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go

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