在上一篇文章中,我们用 C 语言创建了一个 eBPF 程序,以了解某个进程使用 CPU 的时间。这些数据随后被存储在 BPF HashMap 中。但这是一个不断更新的短期存储位置,数据的寿命很短……我们该如何利用这些数据呢?
这就是用户空间程序的用武之地。用户空间程序不在内核空间运行,但可以附加到 eBPF 程序并访问 BPF HashMap。
现在让我们来看看如何用 Golang 编写用户空间程序。
Bpf2go
在使用 Golang 时,有一个很好用的工具叫做 bpf2go。这个工具可以帮助我们将之前编写的 C 代码编译成 eBPF 字节码。此外,它还能创建 Golang 函数和结构的骨架,以便我们将其接口到代码中,从而节省大量时间。
Step 1: 创建 gen.go 文件
1 | package main |
gen.go 文件中的注释行允许我们运行 go 生成,然后使用 bpf2go 工具读取 C 程序(在本例中,第二个标志是 processtime.c),并输出生成的 Golang 代码,这些代码将使用前缀 processtime(第一个标志)。运行 go 生成后,你将得到以下文件:
1 | $ tree |
这里生成了四个文件。对象文件(以 .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 | // Load the compiled eBPF ELF into the kernel. |
接下来,我们需要连接到已加载的程序。这就需要知道你挂接的是什么事件,因为你需要指定它。在我们的 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
11var 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 | package main |
总结
eBPF 是一项值得关注的技术。它在网络、可观测性和安全性方面能够发挥重要作用。了解基本原理是第一步,但要深入研究的东西还有很多。可以期待后续的文章分享👀。