如何用Go开发eBPF程序

简介: 【2月更文挑战第7天】

使用Go开发eBPF程序可以通过以下三个步骤完成:


第一步,使用 C 语言开发内核态 eBPF 程序,这一步跟 libbpf 方法是完全相同的。


新建一个 hello.bpf.c 文件,然后写入内核态 eBPF 程序即可。

/* 由于我们并不需要cgo,这儿需要通过Go构建标签来排除C源文件,否则Go编译会报错 */
//go:build ignore
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
/* 定义BPF映射,用于存储网络包计数*/
struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, __u32);
    __type(value, __u64);
    __uint(max_entries, 1);
} pkt_count SEC(".maps");
/* XDP程序入口,统计网络包数量并存入BPF映射 */
SEC("xdp")
int count_packets() {
    __u32 key    = 0;
    __u64 *count = bpf_map_lookup_elem(&pkt_count, &key);
    if (count) {
        __sync_fetch_and_add(count, 1);
    }
    return XDP_PASS;
}
char __license[] SEC("license") = "Dual MIT/GPL";

这其中,

  • //go:build ignore 表示 Go 编译时忽略 C 文件;
  • pkt_count 定义了一个用于存储网络包计数的 BPF 映射;
  • SEC("xdp") 定义了 XDP 程序的入口函数 count_packets。


从这段代码你可以发现,这儿的代码跟 libbpf 方法是一样的。只有一点需要注意的是 // go:build ignore 这一行是必不可少的,它的意思是让 Go 编译时忽略 C 源码文件。由于我们只是用 C 语言开发 eBPF 程序,并不需要通过 cgo 去直接调用内核态 eBPF 程序代码,所以在编译 Go 代码时应该忽略 C 源码文件。


第二步,借助 go generate 命令,使用 cmd/bpf2go 编译 eBPF 程序,并生成 Go 语言脚手架代码。


有了 eBPF 程序代码之后,接下来就是利用 cmd/bpf2go 来编译并生成 Go 脚手架代码了。创建一个 main.go 文件,并写入如下的代码。

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

这段代码最关键的是第二句 go:generate 注解,用于在执行 go generate 时自动执行 cmd/bpf2go 命令。cmd/bpf2go 命令需要两个参数,第一个 hello 是生成文件名的前缀,而第二个参数 hello.bpf.c 就是我们第一步开发的 eBPF 程序。


在执行 go generate 命令之前,你还需要执行下面的命令,初始化一个 Go 模块,并添加对 github.com/cilium/ebpf/cmd/bpf2go 的依赖。

go mod init hello
go mod tidy
go get github.com/cilium/ebpf/cmd/bpf2go


接下来,你就可以执行 go generate 命令,编译并生成 Go 语言脚手架代码。如果一切顺利,你将看到如下输出:

$ go generate
Compiled /ebpf-apps/go/hello/hello_bpfel.o
Stripped /ebpf-apps/go/hello/hello_bpfel.o
Wrote /ebpf-apps/go/hello/hello_bpfel.go
Compiled /ebpf-apps/go/hello/hello_bpfeb.o
Stripped /ebpf-apps/go/hello/hello_bpfeb.o
Wrote /ebpf-apps/go/hello/hello_bpfeb.go

这其中,.o 文件就是编译目标文件, .go 文件就是对应的脚手架代码,而后缀 bpfel 和 bpfeb 则分别表示该文件用于小端系统和大端系统。


第三步,使用 cilium/ebpf 库配合上一步生成的脚手架代码开发用户态程序,包括 eBPF 程序加载、挂载到内核函数和跟踪点,以及通过 BPF 映射获取和打印执行结果等。


有了脚手架代码之后。可以在 main.go 里面继续添加 main() 函数,添加 eBPF 程序加载、挂载到 XDP,以及通过 BPF 映射获取和打印执行结果等执行逻辑。

// 1. 引入必要的依赖库
import (
 "log"
 "net"
 "os"
 "os/signal"
 "time"
 "github.com/cilium/ebpf/link"
 "github.com/cilium/ebpf/rlimit"
)
func main() {
 // 2. 移除内核<5.11的资源限制
 if err := rlimit.RemoveMemlock(); err != nil {
  log.Fatal("Removing memlock:", err)
 }
 // 3. 调用脚手架函数,加载编译后的 eBPF 字节码
 var objs helloObjects
 if err := loadHelloObjects(&objs, nil); err != nil {
  log.Fatal("Loading eBPF objects failure:", err)
 }
 defer objs.Close()
  // 4. 挂载 XDP 程序到网卡上
 ifname := "eth0"
 iface, err := net.InterfaceByName(ifname)
 if err != nil {
  log.Fatalf("Getting interface %s failure: %s", ifname, err)
 }
 link, err := link.AttachXDP(link.XDPOptions{
  Program:   objs.CountPackets,
  Interface: iface.Index,
 })
 if err != nil {
  log.Fatal("Attaching XDP failure:", err)
 }
 defer link.Close()
 log.Printf("Counting incoming packets on %s..", ifname)
 // 5. 定期查询并打印数据包计数(Ctrl+C退出)
 tick := time.Tick(time.Second)
 stop := make(chan os.Signal, 5)
 signal.Notify(stop, os.Interrupt)
 for {
  select {
  case <-tick:
   var count uint64
   err := objs.PktCount.Lookup(uint32(0), &count)
   if err != nil {
    log.Fatal("Map lookup failure:", err)
   }
   log.Printf("Received %d packets", count)
  case <-stop:
   log.Print("Received stop signal, exiting..")
   return
  }
 }
}

这段代码的主要逻辑跟 libbpf 方法也是类似的,所不同的只是编程语言和库函数的不同。另外,这段 Go 代码里面的 eBPF 程序名 CountPackets 和 BPF 映射名 PktCount 分别对应第一步 eBPF C 代码里面的 count_packets 和 pkt_count,这是 cmd/bpf2go 自动将 C 命名格式转换为 Go 的驼峰命名法导致的(即不使用下划线且单词首字母大写)。


代码开发完成后,你就可以编译并执行用户态的程序了。执行 go build 命令编译 Go 程序后并执行 ./hello 运行它,如果一切正常,你将看到如下的输出:

$ go build
$ ./hello
2023/12/30 14:19:49 Counting incoming packets on eth0..
2023/12/30 14:19:50 Received 9 packets
2023/12/30 14:19:51 Received 16 packets
2023/12/30 14:19:52 Received 20 packets


到这里已经使用 Go 语言成功开发并运行了第一个 eBPF 程序。

相关文章
|
4月前
|
缓存 弹性计算 API
用 Go 快速开发一个 RESTful API 服务
用 Go 快速开发一个 RESTful API 服务
|
26天前
|
开发框架 Go 计算机视觉
纯Go语言开发人脸检测、瞳孔/眼睛定位与面部特征检测插件-助力GoFly快速开发框架
开发纯go插件的原因是因为目前 Go 生态系统中几乎所有现有的人脸检测解决方案都是纯粹绑定到一些 C/C++ 库,如 OpenCV 或 dlib,但通过 cgo 调用 C 程序会引入巨大的延迟,并在性能方面产生显著的权衡。此外,在许多情况下,在各种平台上安装 OpenCV 是很麻烦的。使用纯Go开发的插件不仅在开发时方便,在项目部署和项目维护也能省很多时间精力。
|
1月前
|
Go 数据处理 开发者
Go 语言的反射机制允许程序在运行时动态检查和操作类型信息,提供极大的灵活性和扩展性
Go 语言的反射机制允许程序在运行时动态检查和操作类型信息,提供极大的灵活性和扩展性。本文探讨了反射的基本原理、主要操作、应用场景及注意事项,并通过实例展示了反射的实际应用,帮助开发者更好地理解和使用这一强大特性。
35 2
|
1月前
|
Go 数据安全/隐私保护 开发者
Go语言开发
【10月更文挑战第26天】Go语言开发
42 3
|
1月前
|
Java 程序员 Go
Go语言的开发
【10月更文挑战第25天】Go语言的开发
35 3
|
3月前
|
Kubernetes Go 持续交付
一个基于Go程序的持续集成/持续部署(CI/CD)
本教程通过一个简单的Go程序示例,展示了如何使用GitHub Actions实现从代码提交到Kubernetes部署的CI/CD流程。首先创建并版本控制Go项目,接着编写Dockerfile构建镜像,再配置CI/CD流程自动化构建、推送Docker镜像及部署应用。此流程基于GitHub仓库,适用于快速迭代开发。
84 3
|
3月前
|
Kubernetes 持续交付 Go
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
|
3月前
|
IDE Go 数据处理
Go to Learn Go之第一个Go程序
Go to Learn Go之第一个Go程序
31 0
|
4月前
|
存储 缓存 安全
|
4月前
|
编译器 Go 开发者
Go 程序中的包:定义、作用与应用指南
【8月更文挑战第31天】
102 0