如何用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 程序。

相关文章
|
2月前
|
缓存 弹性计算 API
用 Go 快速开发一个 RESTful API 服务
用 Go 快速开发一个 RESTful API 服务
|
8天前
|
Kubernetes Go 持续交付
一个基于Go程序的持续集成/持续部署(CI/CD)
本教程通过一个简单的Go程序示例,展示了如何使用GitHub Actions实现从代码提交到Kubernetes部署的CI/CD流程。首先创建并版本控制Go项目,接着编写Dockerfile构建镜像,再配置CI/CD流程自动化构建、推送Docker镜像及部署应用。此流程基于GitHub仓库,适用于快速迭代开发。
25 3
|
8天前
|
Kubernetes 持续交付 Go
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
创建一个基于Go程序的持续集成/持续部署(CI/CD)流水线
|
6天前
|
IDE Go 数据处理
Go to Learn Go之第一个Go程序
Go to Learn Go之第一个Go程序
11 0
|
2月前
|
JSON 中间件 Go
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
本文详细介绍了如何在Go项目中集成并配置Zap日志库。首先通过`go get -u go.uber.org/zap`命令安装Zap,接着展示了`Logger`与`Sugared Logger`两种日志记录器的基本用法。随后深入探讨了Zap的高级配置,包括如何将日志输出至文件、调整时间格式、记录调用者信息以及日志分割等。最后,文章演示了如何在gin框架中集成Zap,通过自定义中间件实现了日志记录和异常恢复功能。通过这些步骤,读者可以掌握Zap在实际项目中的应用与定制方法
go语言后端开发学习(四) —— 在go项目中使用Zap日志库
|
2月前
|
Linux Shell Go
如何构建和安装 Go 程序
如何构建和安装 Go 程序
32 1
|
2月前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
2月前
|
JSON 缓存 监控
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
Viper 是一个强大的 Go 语言配置管理库,适用于各类应用,包括 Twelve-Factor Apps。相比仅支持 `.ini` 格式的 `go-ini`,Viper 支持更多配置格式如 JSON、TOML、YAML
go语言后端开发学习(五)——如何在项目中使用Viper来配置环境
|
2月前
|
存储 缓存 安全
|
2月前
|
编译器 Go 开发者
下一篇
无影云桌面