構建並運行 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