2026/9/27 10:33:12

使用 ebpf-go(cilium/ebpf)从零构建 eBPF Go 应用:安装、bpf2go 工作流与内核对象生命周期

使用 ebpf-go(cilium/ebpf)从零构建 eBPF Go 应用:安装、bpf2go 工作流与内核对象生命周期 系统底层网络可观测性【免费下载链接】ebpfebpf-go is a pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel.项目地址https://gitcode.com/gh_mirrors/eb/ebpf点击查看免费下载导读ebpf-go模块路径github.com/cilium/ebpf是一个纯 Go 实现的 eBPF 开发库用于读取、修改和加载 eBPF 程序并将其挂载到 Linux 内核的各种钩子上。它不依赖 C、libbpf 或标准库之外的任何第三方 Go 依赖非常适合编写自包含、可移植、能跨多种架构运行的工具。本文以官方文档首页docs/ebpf/index.md为主线结合 Getting Started 指南 与仓库内的真实示例源码完整演示从安装依赖、编写 eBPF C 程序、用 bpf2go 生成 Go 脚手架到编写加载与挂载逻辑、构建运行的端到端流程并深入讲解 memlock 资源限制与对象生命周期这两个关键概念读完即可独立开发你的第一个 eBPF 驱动型 Go 应用。项目定位为什么选择纯 Go 的 eBPF 库官方文档首页开宗明义ebpf-go 是一个用于操作 eBPF 的 Go 库。其核心优势在于不依赖 C 与 libbpf库本身的实现、加载逻辑全部使用 Go 标准库完成这让它天然适合构建自包含、可移植的二进制工具跨架构支持库对多种体系结构提供支持。从仓库内 bpf2go 生成的代码可见默认支持386、amd64、arm、arm64、loong64、mips64le、mipsle、ppc64le、riscv64、wasm等平台见 counter_bpfel.go 顶部的 build tag提供文档中心该文档旨在成为如何使用 Go 构建 eBPF 应用的集中式学习资源覆盖概念、指南与示例三层结构。需要说明的是这里的纯 Go指的是库的实现不依赖 CGO 与 libbpf而 eBPF 程序本身通常仍需要用 C或 Rust、Go eBPF 编译器编写并经 clang 编译为 BPF 字节码这一流程由配套工具 bpf2go 自动化完成。安装将 ebpf-go 加入你的 Go 模块官方文档首页给出了唯一的安装命令。在已初始化 Go 模块的目录中执行go get github.com/cilium/ebpf从仓库的 go.mod 可见当前模块版本要求go 1.25.0因此请确保本地 Go 编译器满足该版本要求。库的运行时依赖极少除标准库外仅使用golang.org/x/sys、golang.org/x/sync等少量辅助包符合自包含、可移植的定位。目标读者与前置知识文档首页明确说明了读者画像熟悉 eBPF 的基本概念与术语如 Program、Map、Hook、BPF Helper掌握 Go 工具链的基本用法能编写符合惯例的 Go 代码对 eBPF 完全陌生的读者建议先补充 eBPF 的基础知识再阅读后续指南。仓库的 Getting Started 指南 进一步列出了动手实操所需的运行环境这是运行下文示例的前提依赖说明检查/安装方式Linux 内核 5.7需要 bpf_link 支持才能使用link.AttachXDP挂载uname -rLLVM 11clang与llvm-strip用于把 C 源码编译为 BPF 目标文件clang --version部分发行版两者分属不同软件包libbpf 头文件提供__uint、__type、SEC等宏与 BPF helper 声明Debian/Ubuntu 装libbpf-devFedora 装libbpf-develLinux 内核头文件提供__u64、BPF_MAP_TYPE_ARRAY等内核定义AMD64 Debian/Ubuntu 装linux-headers-amd64Fedora 装kernel-devel。Debian 下可能还需ln -sf /usr/include/asm-generic /usr/include/asm以找到asm/types.h实战主线一个统计网卡收包数的 XDP 程序文档首页将读者导向 examples/ 目录 与 Getting Started 指南。仓库中docs/examples/getting_started/提供了完整的可直接对照源码下面按官方指南的流程逐步展开。该示例把 eBPF 程序挂到XDP 钩子上统计物理网卡收到的数据包数量。XDP 是最早一批 eBPF 应用场景的典型代表——在数据包进入内核网络栈路由、iptables/nftables 防火墙、TCP/socket 处理之前就能介入可执行过滤与修改。当然 eBPF 的能力远不止网络如今已被广泛用于追踪、系统与可观测性、安全等领域。第一步编写 eBPF C 程序 counter.c在空目录中保存 counter.c其结构将直接决定后续生成的 Go 脚手架//go:build ignore #include linux/bpf.h // 内核定义__u64、BPF_MAP_TYPE_ARRAY 等 #include bpf/bpf_helpers.h // libbpf 宏__uint、__type、SEC 与 helper 声明 struct { __uint(type, BPF_MAP_TYPE_ARRAY); // 数组型 Map __type(key, __u32); __type(value, __u64); __uint(max_entries, 1); } pkt_count SEC(.maps); // Map 定义必须放在 .maps 节 // count_packets 每次调用原子地使计数器加 1。 SEC(xdp) // 程序类型由 ELF 节名约定决定 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; // 使用 GPL helper 需声明许可逐点拆解这段代码的关键设计build tag 排除C 文件与 Go 文件放同一目录时必须用//go:build ignore之类的 tag 排除否则go build会报C source files not allowed when not using cgo or SWIG。Go 工具链可以安全忽略这些 eBPF C 源文件。头文件分工linux/bpf.h提供内核标识符bpf/bpf_helpers.hlibbpf 提供提供__uint、__type、SEC宏与 BPF helper 声明。Map 定义pkt_count是 Array 型 Map容量max_entries为 1值类型为u64。BPF 数组是预分配且清零的开箱即用无需初始化。库通过.mapsELF 节识别 Map 定义见 elf_sections.go 中的节解析逻辑。SEC 节名决定程序类型SEC(xdp)遵循 libbpf 关于 ELF 节名与程序类型对应关系的约定加载器据此得知这是 XDP 程序后续才能挂到 XDP 钩子。helper 与判空bpf_map_lookup_elem是内核提供的 BPF helper。所有 Map 查询都可能失败返回空指针BPF 校验器verifier对潜在空指针访问极其严格任何后续访问都必须以判空为前提。原子性在 SMP 系统上同一个 eBPF 程序可能在多个接收队列上被并发执行pkt_count实际上成为共享内存。因此必须用__sync_fetch_and_add这类原子操作避免脏读脏写。XDP 裁决返回XDP_PASS表示把包继续交给内核网络栈处理本程序只计数、不干预。许可证部分 helper 允许调用 GPLv2 许可的内核代码因此使用它们的程序需声明至少部分遵循 GPL。库本身是 MIT 许可示例采用Dual MIT/GPL双许可。第二步用 bpf2go 编译并生成脚手架创建 gen.go用//go:generate指令驱动 bpf2go//go:build linux package main //go:generate go tool bpf2go -tags linux counter counter.cbpf2go不仅能编译 C 程序还会生成用于加载 eBPF 程序、访问其中各对象的脚手架代码大幅减少手写样板。使用独立文件存放//go:generate指令而不是与应用逻辑混在一起是官方推荐的整洁做法。接着初始化 Go 模块并声明工具依赖确保 Go 工具链使用的 bpf2go 版本与库版本始终一致go mod init ebpf-test go mod tidy go get -tool github.com/cilium/ebpf/cmd/bpf2go然后运行go generatebpf2go 会在后台用clang把counter.c编译为counter_bpf*.o并生成两个目标文件与两个对应的 Go 源文件Compiled /home/timo/getting_started/counter_bpfel.o Stripped /home/timo/getting_started/counter_bpfel.o Wrote /home/timo/getting_started/counter_bpfel.go Compiled /home/timo/getting_started/counter_bpfeb.o Stripped /home/timo/getting_started/counter_bpfeb.o Wrote /home/timo/getting_started/counter_bpfeb.gobpfel对应小端little-endian平台bpfeb对应大端平台使程序具备跨架构可移植性。不要删除这些生成文件后续构建会用到它们——目标文件通过go:embed直接嵌入 Go 二进制见 counter_bpfel.go 末尾的_CounterBytes。生成的 counter_bpfel.go 结构非常清晰可以从源码中看到 bpf2go 为你做了什么loadCounter()从嵌入字节流解析*ebpf.CollectionSpecloadCounterObjects(obj, opts)加载 ELF 并把对象赋值到传入的结构体counterObjects/counterPrograms/counterMaps三套结构体分别对应已加载到内核的对象全集 / 仅程序 / 仅 Map均带Close()方法字段通过ebpf:count_packets、ebpf:pkt_count这类结构体 tag 与 ELF 中的对象名一一绑定。第三步编写 Go 应用 main.go在 main.go 中编写加载与挂载逻辑//go:build linux package main import ( log net os os/signal time github.com/cilium/ebpf/link github.com/cilium/ebpf/rlimit ) func main() { // 为内核 5.11 移除资源限制。 if err : rlimit.RemoveMemlock(); err ! nil { log.Fatal(Removing memlock:, err) } // 加载编译好的 eBPF ELF 并将其载入内核。 var objs counterObjects if err : loadCounterObjects(objs, nil); err ! nil { log.Fatal(Loading eBPF objects:, err) } defer objs.Close() // 退出前关闭所有 fd ifname : eth0 // 改成你机器上的网卡接口名 iface, err : net.InterfaceByName(ifname) if err ! nil { log.Fatalf(Getting interface %s: %s, ifname, err) } // 把 count_packets 挂到网卡接口上。 link, err : link.AttachXDP(link.XDPOptions{ Program: objs.CountPackets, Interface: iface.Index, }) if err ! nil { log.Fatal(Attaching XDP:, err) } defer link.Close() log.Printf(Counting incoming packets on %s.., ifname) // 周期性读取 PktCount 中的计数收到中断信号时退出。 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:, err) } log.Printf(Received %d packets, count) case -stop: log.Print(Received signal, exiting..) return } } }代码中几个值得深挖的点rlimit.RemoveMemlock()5.11 之前的内核用RLIMIT_MEMLOCK限制进程可分配的 eBPF 内存默认值较低需要显式抬高。该函数会先探测内核是否已切换到 memcg 记账5.11 起是则什么都不做否则把RLIMIT_MEMLOCK提到无穷。详见下文资源限制一节及 concepts/rlimit.md。类型安全的对象装载counterObjects是包含 nil 指针的结构体loadCounterObjects依据结构体 tag 填充字段。这消除了用字符串在 Collection 中反复查找 Map/Program 的样板代码且把查找变成编译期行为——只要某个对象不在 ELF 里对应字段就不存在应用直接编译失败从而消灭一整类运行时错误。生命周期管理defer objs.Close()与defer link.Close()确保进程退出前释放所有文件描述符与挂载关系。若 Link 未 pin 到 bpffs关闭 Link 会让程序停止执行。XDP 挂载link.AttachXDP返回一个link.Link抽象该抽象基于内核的 bpf_link 机制因此要求内核 5.7。Map 读取objs.PktCount.Lookup(uint32(0), count)读取索引 0 处的uint64与 C 侧逻辑一一对应。第四步构建与运行go build sudo ./ebpf-test预期输出加载 eBPF 程序需要 root 权限故用sudo2023/09/20 17:18:43 Counting incoming packets on eth0.. 2023/09/20 17:18:47 Received 0 packets 2023/09/20 17:18:48 Received 4 packets 2023/09/20 17:18:49 Received 11 packets 2023/09/20 17:18:50 Received 15 packets在 eth0 上产生一些流量如ping或curl计数器就会增长。迭代工作流保持生成文件同步修改 C 代码后必须重新运行 bpf2go否则 eBPF C 不会重新编译C 程序结构的变化也不会反映到 Go 脚手架中。官方推荐的迭代命令go generate go build sudo ./ebpf-test关键概念深挖资源限制RLIMIT_MEMLOCK 与 memcg创建 eBPF 对象Map、Program甚至 BTF blob都需要分配内核内存。在 concepts/rlimit.md 中说明了这部分的历史沿革5.11 之前进程可用于创建 eBPF 对象的内存量受RLIMIT_MEMLOCK限制可用ulimit -l查看。默认值往往很小不足以加载复杂的 eBPF 应用。5.11 起内核改用内存 cgroupmemcg记账管理 eBPF 对象分配被计入 cgroup 的常规内存限额可通过 cgroupfs 查询与设置——与容器的内存限制机制相同。rlimit包封装了两种行为对应仓库中的 rlimit/rlimit_linux.go导入副作用包被导入时init()会把当前进程 rlimit 降到 0、制造一次 Map 创建失败、再恢复原值以此探测内核是否支持 memcg 记账RemoveMemlock()根据探测结果仅在旧内核上把RLIMIT_MEMLOCK提升到无穷新内核上是 no-op。使用注意事项官方文档明确提示RemoveMemlock()可多次调用多入口点或 CLI 子命令场景rlimit 操作只执行一次探测过程发生在RemoveMemlock()调用之前在 5.11 之前的内核上探测期间其他并发 BPF 对象创建可能因内存不足失败其他也操作prlimit(2)的 Go 包可能干扰读写需审计代码与依赖rlimit包完全可选只是便利功能。由于init()必然创建一次 Map即使从不调用RemoveMemlock()应用启动时也会与bpf(2)交互。若不能接受可放弃该包改用 Docker 的--ulimit memlock-1或 systemd 的LimitMEMLOCKinfinity提高限制。关键概念深挖对象生命周期、fd 与 pinningconcepts/object-lifecycle.md 是排查程序意外脱离、资源泄漏等问题的进阶主题官方标注为高级话题入门阶段不必完全理解但理解它能避免大量疑难 bug。用户空间通过文件描述符引用 eBPF 对象。在 Linux 中文件描述符被广泛用作各种内核资源的引用而不只是文件。ebpf-go 的Map、Program、link.Link全部基于底层 fd 建模。与标准库os.File一致这些对象的 fd 会在 Go 对象被垃圾回收时自动关闭通常能防止资源泄漏——但副作用是如果对象在函数中创建后未被返回给调用者GC 会立刻关掉其 fd。ProgramArray程序数组用于尾调用对此尤其敏感见下文。延长对象生命周期的两种手段Pinning挂 pin在 BPF 文件系统bpffs通常挂载于/sys/fs/bpf上为 Map/Program/Link 建立一个文件关联。进程退出后 pin 仍持有引用防止对象被自动销毁删除 pinrm会移除最后的引用导致内核销毁对象。注意pin 不会跨重启持久化。典型用途是跨进程共享对象从一个进程创建并 pin Map再用bpftool map dump pinned /sys/fs/bpf/my_map从另一个进程查看。对应 API 为Map.Unpin、Program.Unpin、link.Link.Unpin。Attaching挂载把 Program 挂到钩子上本身也构成对 Program 的引用因为内核随时需要执行其指令。部分旧式 Link 类型不支持 pin通常可认为这些 Link 在 Go 应用退出后依然存在。Program Array 的坑程序数组允许程序间尾调用跳转可用于拆分超长程序且允许与 Program 形成循环依赖。为避免内核中留下无法释放的程序集合内核有一条硬性规则程序数组至少需要一个打开的 fd 或 bpffs pin。若所有用户空间/bpffs 引用都消失任何跳入该数组的尾调用都会失败而数组本身只要还有程序引用就会保持加载——这组合 GC 语义是 bug 的高发区。官方建议用CollectionSpec.LoadAndAssign它会拒绝加载无用户空间引用的程序数组若 eBPF 执行需在 Go 应用退出后继续如升级场景或短生命周期 CLI请 pin 程序数组长驻应用中始终保留对 Map 的引用注意defer m.Close()会让 Go 保留引用直到当前作用域结束。示例导航继续探索的路径文档首页将读者导向 users.md使用该库的项目列表与仓库内的 examples/ 目录。从仓库结构看示例覆盖了广泛的挂载点与场景追踪类kprobe、kprobe_percpu、kprobepin演示 pin 用法、uretprobe、fentry、tracepoint_in_c 与 tracepoint_in_go纯 Go 侧编程网络类tcprtt、tcprtt_sockops、cgroup_skb、tcx、xdp、xdp_live_frame、map_in_map新内核特性sched_ext、ringbuffer环形缓冲区从内核向用户空间传数据。这些示例均采用与本文相同的 bpf2go 工作流目录内成对出现*.c、gen.go、main.go与生成的bpf_bpfeb.go/bpf_bpfel.go可直接在支持 eBPF 的 Linux 机器上运行验证。总结至此你已完整走过一个 ebpf-go 应用的诞生过程理解纯 Go 库的定位与安装方式go get github.com/cilium/ebpf、编写带.maps与SEC(xdp)节的 eBPF C 程序、借助//go:generate go tool bpf2go一键编译并生成类型安全的 Go 脚手架、用loadCounterObjectslink.AttachXDP完成加载挂载、按迭代工作流持续开发并掌握了 memlock 资源限制与对象生命周期两个关键概念。XDP 只是 eBPF 能力的冰山一角——后续可以沿着 Getting Started 指南 与 examples/ 目录 继续探索追踪、可观测性、安全等更多应用场景。赞分享系统底层网络可观测性【免费下载链接】ebpfebpf-go is a pure-Go library to read, modify and load eBPF programs and attach them to various hooks in the Linux kernel.项目地址https://gitcode.com/gh_mirrors/eb/ebpf点击查看免费下载相关推荐Jetsnack 源码解析用 Jetpack Compose 打造自定义设计系统与高级布局动画Jetsnack 源码解析用 Jetpack Compose 打造自定义设计系统与高级布局动画 Jetsnack 是 Jetpack Compose 官方示例示例工程革命性eBPF开发库ebpf-go让内核编程像Go一样简单革命性eBPF开发库ebpf go让内核编程像Go一样简单 在当今云计算和容器化时代系统性能监控和网络可观测性变得前所未有的重要。eBPF技术作为Linux系统底层网络可观测性ebpf-go 对象生命周期深入解析文件描述符、GC 终结器与 Pin 机制ebpf go 对象生命周期深入解析文件描述符、GC 终结器与 Pin 机制 导读 本文以 cilium/ebpfebpf go官方文档《Object L系统底层网络可观测性上一篇FiftyOne 3D Visual AI 实战指南点云、网格与 3D 标注的完整数据工作流下一篇Meteor socket-stream-client 包解析ClientStream 连接抽象与 SockJS/WebSocket 传输机制创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考