構建並運行 eBPF 應用 - Part 2

上一篇文章中,我們用 C 語言創建了一個 eBPF 程序,以瞭解某個進程使用 CPU 的時間。這些數據隨後被存儲在 BPF HashMap 中。但這是一個不斷更新的短期存儲位置,數據的壽命很短...... 我們該如何利用這些數據呢?

這就是用戶空間程序的用武之地。用戶空間程序不在內核空間運行,但可以附加到 eBPF 程序並訪問 BPF HashMap。

現在讓我們來看看如何用 Golang 編寫用戶空間程序。

Bpf2go

在使用 Golang 時,有一個很好用的工具叫做 bpf2go[1]。這個工具可以幫助我們將之前編寫的 C 代碼編譯成 eBPF 字節碼。此外,它還能創建 Golang 函數和結構的骨架,以便我們將其接口到代碼中,從而節省大量時間。

Step 1: 創建 gen.go 文件

package main
//go:generate go run github.com/cilium/ebpf/cmd/bpf2go processtime processtime.c

gen.go 文件中的註釋行允許我們運行 go 生成,然後使用 bpf2go 工具讀取 C 程序(在本例中,第二個標誌是 processtime.c),並輸出生成的 Golang 代碼,這些代碼將使用前綴 processtime(第一個標誌)。運行 go 生成後,你將得到以下文件:

$ tree
.
|____gen.go
|____processtime.c
|____processtime_bpfel.o
|____processtime_bpfeb.o
|____processtime_bpfel.go
|____processtime_bpfeb.go

這裏生成了四個文件。對象文件(以 .o 結尾的文件)是將加載到內核中的 eBPF 字節碼。以 Golang ext 結尾的 .go 文件是創建所有用戶空間接口的文件。

打開這兩個 Golang 文件,你還會發現每個文件的頂部都有一個註釋,說明該文件適用於哪種 CPU 架構。例如,processtime_bpfeb.go 的註釋如下:

// Code generated by bpf2go; DO NOT EDIT.
//go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64

processtime_bpfel.go 有不同的架構:這是因爲,在處理內核時,程序的編譯方式在每種架構上都有細微差別。

步驟 2:編寫用戶空間程序

我們可以開始使用 eBPF 程序了。我們將在根目錄下創建一個 main.go,並首先取消資源限制:

// Remove resource limits for kernels <5.11.
if err := rlimit.RemoveMemlock(); err != nil {
  log.Fatal("Removing memlock:", err)
}

這是因爲內核 v5.11 發生了變化,BPF 進程的可用內存過去受 RLIMIT_MEMLOCK 限制,但這一邏輯已移至內存 cgroup (memcg)。

下一步是加載 eBPF 程序。這是通過 bpf2go 工具生成的接口代碼中的一個名爲 loadProcesstimeObject 的函數完成的。我們需要創建一個變量來存儲該函數調用的輸出。

// Load the compiled eBPF ELF into the kernel.
var objs processtimeObjects
if err := loadProcesstimeObjects(&objs, nil); err != nil {
  log.Fatal("Loading eBPF objects:", err)
}
defer objs.Close()

接下來,我們需要連接到已加載的程序。這就需要知道你掛接的是什麼事件,因爲你需要指定它。在我們的 C 程序中,我們指定了以下內容:

SEC("tracepoint/sched/sched_switch")

因此,我們知道我們的程序掛接到了 sched 命名空間中的跟蹤點 sched_switch。這可以轉化爲以下內容:

// link to the tracepoint program that we loaded into the kernel
tp, err := link.Tracepoint("sched""sched_switch", objs.CpuProcessingTime, nil)
if err != nil {
  log.Fatalf("opening kprobe: %s", err)
}
defer tp.Close()

我們需要的最後一項功能是讀取存儲在 BPF HashMap 中的數據。這可以通過使用 HashMap 的鍵來查看存儲的值。在生成 Golang 代碼時,我們生成了兩種類型來幫助我們與 BPF HashMap 交互。

// used as HashMap Key
type processtimeKeyT struct{ Pid uint32 }
// used as HashMap Value
type processtimeValT struct {
  StartTime uint64
  ElapsedTime uint64
}

這與我們在 C 程序中使用的兩種類型相關:

// used as Hashmap Key
struct key_t {
__u32 pid;
};
// used as Hashmap Value
struct val_t {
__u64 start_time;
__u64 elapsed_time;
};

在我們的例子中,鍵值基本上就是進程 ID。現在,在大多數系統中,PID 的默認值介於 1 和 32767 之間,但你可以通過查看 /proc/sys/kernel/pid_max 文件來查看該值。

通過上述邏輯,我們應該可以遍歷所有潛在的 PID,並檢查 BPF HashMap,查看是否有存儲的值。因此,我們可以使用它們來編寫我們的循環邏輯:

var key processtimeKeyT
// Iterate over all PIDs between 1 and 32767 (maximum PID on linux)
// found in /proc/sys/kernel/pid_max
for i := 1; i <= 32767; i++ {
  key.Pid = uint32(i)
  // Query the BPF map
  var mapValue processtimeValT
  if err := objs.ProcessTimeMap.Lookup(key, &mapValue); err == nil {
    log.Printf("CPU time for PID=%d: %dns\n", key.Pid, mapValue.ElapsedTime)
  }
}

這段代碼將循環處理每個可用的 PID,並在我們的 BPF HashMap(由 objs.ProcessTimeMap 指定)中進行查找,如果沒有錯誤返回,將打印出值。

步驟 3. 完整代碼

最終代碼如下所示:(請注意,我有一個每秒運行一次循環的 ticker,因爲 HashMap 可以不斷更新,因此我們需要不斷重新讀取它)

package main

import (
  "C"
  "log"
  "time"

  "github.com/cilium/ebpf/link"
  "github.com/cilium/ebpf/rlimit"
)

func main() {
  // Remove resource limits for kernels <5.11.
  if err := rlimit.RemoveMemlock(); err != nil {
    log.Fatal("Removing memlock:", err)
  }

  // Load the compiled eBPF ELF and load it into the kernel.
  var objs processtimeObjects
  if err := loadProcesstimeObjects(&objs, nil); err != nil {
    log.Fatal("Loading eBPF objects:", err)
  }
  defer objs.Close()

  // link to the tracepoint program that we loaded into the kernel
  tp, err := link.Tracepoint("sched""sched_switch", objs.CpuProcessingTime, nil)
  if err != nil {
    log.Fatalf("opening kprobe: %s", err)
  }
  defer tp.Close()

  // Read loop reporting the total amount of times the kernel
  // function was entered, once per second.
  ticker := time.NewTicker(1 * time.Second)
  defer ticker.Stop()

  log.Println("Waiting for events..")
  for range ticker.C {
    var key processtimeKeyT

    // Iterate over all PIDs between 1 and 32767 (maximum PID on linux)
    // found in /proc/sys/kernel/pid_max
    for i := 1; i <= 32767; i++ {
      key.Pid = uint32(i)
      // Query the BPF map
      var mapValue processtimeValT
      if err := objs.ProcessTimeMap.Lookup(key, &mapValue); err == nil {
        log.Printf("CPU time for PID=%d: %dns\n", key.Pid, mapValue.ElapsedTime)
      }
    }
  }
}

總結

eBPF 是一項值得關注的技術。它在網絡、可觀測性和安全性方面能夠發揮重要作用。瞭解基本原理是第一步,但要深入研究的東西還有很多。可以期待後續的文章分享👀。

參考資料

[1]

bpf2go: https://pkg.go.dev/github.com/cilium/ebpf/cmd/bpf2go#section-readme

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