构建并运行 eBPF 应用 - Part 2

在上一篇文章中,我们用 C 语言创建了一个 eBPF 程序,以了解某个进程使用 CPU 的时间。这些数据随后被存储在 BPF HashMap 中。但这是一个不断更新的短期存储位置,数据的寿命很短……我们该如何利用这些数据呢?

这就是用户空间程序的用武之地。用户空间程序不在内核空间运行,但可以附加到 eBPF 程序并访问 BPF HashMap。

现在让我们来看看如何用 Golang 编写用户空间程序。

Bpf2go

在使用 Golang 时,有一个很好用的工具叫做 bpf2go。这个工具可以帮助我们将之前编写的 C 代码编译成 eBPF 字节码。此外,它还能创建 Golang 函数和结构的骨架,以便我们将其接口到代码中,从而节省大量时间。

Step 1: 创建 gen.go 文件

1
2
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 生成后,你将得到以下文件:

1
2
3
4
5
6
7
8
$ 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 的注释如下:

1
2
// 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,并首先取消资源限制:

1
2
3
4
// 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 的函数完成的。我们需要创建一个变量来存储该函数调用的输出。

1
2
3
4
5
6
// 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 程序中,我们指定了以下内容:

1
SEC("tracepoint/sched/sched_switch")

因此,我们知道我们的程序挂接到了 sched 命名空间中的跟踪点 sched_switch。这可以转化为以下内容:

1
2
3
4
5
6
// 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 交互。

1
2
3
4
5
6
7
// used as HashMap Key
type processtimeKeyT struct{ Pid uint32 }
// used as HashMap Value
type processtimeValT struct {
StartTime uint64
ElapsedTime uint64
}

这与我们在 C 程序中使用的两种类型相关:

1
2
3
4
5
6
7
8
9
// 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,查看是否有存储的值。
因此,我们可以使用它们来编写我们的循环逻辑:

1
2
3
4
5
6
7
8
9
10
11
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 可以不断更新,因此我们需要不断重新读取它)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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 是一项值得关注的技术。它在网络、可观测性和安全性方面能够发挥重要作用。了解基本原理是第一步,但要深入研究的东西还有很多。可以期待后续的文章分享👀。


-------------The End-------------

本文标题:构建并运行 eBPF 应用 - Part 2

文章作者:cloud sjhan

发布时间:2024年07月15日 - 15:07

最后更新:2024年07月15日 - 15:07

原始链接:https://cloudsjhan.github.io/2024/07/15/构建并运行-eBPF-应用-Part-2/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

cloud sjhan wechat
subscribe to my blog by scanning my public wechat account
坚持原创技术分享,您的支持将鼓励我继续创作!
0%
;