首页
/ Moby 项目中的 ebpf-go(cilium/ebpf)全解析:用纯 Go 加载、编译与调试 eBPF 程序

Moby 项目中的 ebpf-go(cilium/ebpf)全解析:用纯 Go 加载、编译与调试 eBPF 程序

2026-09-07 16:23:23作者:霍妲思

本文以 Moby 仓库中 vendored 的 cilium/ebpf README 为主体,结合其源码与在容器工具链中的真实调用点,讲解 ebpf-go 库的定位、包结构、核心 API 与使用约束。读完你将掌握:ebpf-go 如何帮助长时运行进程在用户态用 Go 直接驱动内核 eBPF 对象、各子包的职责边界,以及 eBPF 程序从 ELF/指令集到内核对象再到挂钩(attach)的完整生命周期,并看到它在 Moby 生态(cgroup v2 设备过滤)中的实际落地形态。

从 Moby 仓库认识 ebpf-go:一段被 vendored 的 eBPF 工具链

Moby(moby/moby,即 Docker Engine 的开源仓库)中,github.com/cilium/ebpfv0.17.3 版本作为间接依赖被引入(见根目录 go.mod),其完整快照位于 vendor/github.com/cilium/ebpf。这与文档中的定位完全一致:ebpf-go 是一个“纯净 Go”实现、极少外部依赖、面向长时运行进程的 eBPF 用户态库——而 Docker/containerd 守护进程正是这类进程的典型代表。

值得注意的是,仓库的 vendor 快照只保留了 Moby 构建真正需要的子包:核心顶层包(map.goprog.gotypes.gocollection.goelf_reader.gosyscalls.go 等)、asm 汇编子包、btf 类型信息子包、link 挂钩子包,以及 internal/ 下的 sys(系统调用封装)、linuxkallsymskconfigtracefsunix 等底层辅助包。文档中提到的 cmd/bpf2goperfringbuffeaturesrlimitpin 属于库的完整形态,本文会一并介绍,但请留意本仓库内并非全部可见。

定位与设计意图:写给"不想碰 C"的长时进程

顶层包的自述文件 doc.go 给出了最关键的设计声明:

  • eBPF 程序是直接运行在 Linux 内核虚拟机中的小段代码,因此非常快且灵活;许多内核子系统都接受 eBPF 程序,这使开发者可以在不修改内核本身的前提下,在内核中实现高度应用定制化的逻辑。
  • 该库专为长时运行进程设计,运行时依赖只有库自身和 Linux 内核本身;eBPF 代码应预先用 clang 编译,并像其他资源一样随应用一起分发。
  • link 子包把一个已加载的程序附着到内核中的某个挂钩(hook)。
  • 资源生命周期警告:一旦丢失对 Map/Program 对象的全部引用,其底层文件描述符就会被关闭,从而可能把内核中的对应对象删除。因此要始终持有引用——典型做法是在应用退出前 defer 关闭一个 CollectionLoadAndAssign 结果对象。
  • ProgramArray 这类 map 要特别小心:当最后一个用户态或 bpffs 引用消失时,内核会擦除其内容,无论该 map 是否正被实际使用。

这决定了 ebpf-go 的工程形态:它不做内核内编译,而是充当"内核 eBPF 对象的 Go 化前端"——负责把指令序列、map 定义、程序规格送入内核,并管理这些对象的生命周期与附着关系。

包结构总览:八个子包,各自解决什么问题

文档以表格化方式列出库包含的主要包,按职责可划分为"编写 / 编译"、"加载与操纵"、"挂钩"、"数据通道"、"内核探测"与"基础设施"几组:

子包 职责(对应文档原文要点) 本仓库可见性
asm 内置一个基础汇编器,允许直接在 Go 代码中书写 eBPF 汇编指令;如果习惯用 C 写程序则非必需 可见(asm
cmd/bpf2go 编译并把 C 写的 eBPF 程序嵌入 Go 代码,同时自动生成用于加载与操纵 eBPF 程序及 map 对象的 Go 代码 上游库提供,本 vendor 快照未含
link 把 eBPF 附着到各种挂钩(cgroup、kprobe、tracepoint、XDP 等) 可见(link
perf PERF_EVENT_ARRAY map 读取数据 上游库提供,本 vendor 快照未含
ringbuf BPF_MAP_TYPE_RINGBUF map 读取数据 上游库提供,本 vendor 快照未含
features 用纯 Go 实现 bpftool feature probe 的等价物,探测内核 BPF 特性 上游库提供,本 vendor 快照未含
rlimit 提供便捷 API 提升 5.11 之前内核的 RLIMIT_MEMLOCK 限制 上游库提供,本 vendor 快照未含
btf 读取 BPF 类型格式(BPF Type Format),支撑 CO-RE 等功能 可见(btf
pin 提供在 bpffs 上操作 pin 对象的 API 上游库提供,本 vendor 快照未含

组合起来就是一条完整流水线:写程序(asm / bpf2go)→ 描述并加载(顶层包)→ 附着挂钩(link)→ 读取结果(perf / ringbuf)→ 排查兼容性(features / rlimit / btf)

从源码看,link 子包是附着能力最丰富的部分,本仓库快照中包含 kprobe.gokprobe_multi.gouprobe.gouprobe_multi.gotracepoint.gotracing.goraw_tracepoint.goperf_event.gocgroup.goxdp.gosocket_filter.gotcx.gonetkit.gonetfilter.gonetns.goiter.go 等,几乎覆盖主流 eBPF 挂钩类型,并通过 link.gosyscalls.goquery.goanchor.go 统一抽象出 Link 的创建、查询与锚定。

顶层包核心类型:从 Go 结构看内核对象模型

Map 类型:types.go 中的 MapType 枚举

types.go 定义了完整的 map 类型枚举(镜像内核 enum bpf_map_type),部分关键类型及语义如下:

  • Hash / Array:基础哈希表与数组;PerCPUHash / PerCPUArray 提供按 CPU 分摊的无锁高并发访问。
  • ProgramArray:值是 eBPF 程序 fd 的数组 map,key/value 各占 4 字节,配合 Tail Call 尾调用使用(types.go)。
  • PerfEventArray:配合 PerfEventRead / PerfEventOutput 读取寄存器中的 bpf_perf_data
  • LRUHash / LRUCPUHash:内存不足时淘汰最久未使用条目而非报错;后者把 CPU 局部性纳入 LRU 权重。
  • LPMTrie:最长前缀匹配字典树,天然适合存 IP 地址这类可按掩码归并的 key。
  • ArrayOfMaps / HashOfMaps:map 里装 map(内层 map 不允许再是 map-of-maps)。
  • Queue(FIFO)与 Stack(LIFO):BPF 程序内部的通用存储。
  • RingBuf:类似 PerfEventArray 但跨 CPU 共享。
  • BloomFilter:判断元素是否属于某集合的空间高效数据结构。
  • UserRingbuf:与 RingBuf 方向相反,用于用户态向 BPF 程序发消息。
  • 以及 DevMapSockMapCGroupStorageArena(BPF 程序与用户态共享的稀疏内存)等场景化类型。

辅助方法揭示了 map 的分类学(types.go):hasPerCPUValue() 判定是否按 CPU 存值;canStoreMap()/canStoreProgram() 分别对应 map-of-maps 与 ProgramArray;canHaveValueSize() 显示 RingBufArena 不支持 value size,而 PerfEventArray 出于历史原因 value size 只能是 0 或 4(由库稍后修正)。

程序类型与附着类型

types.go 罗列了二十余种程序类型(SocketFilterKprobeSchedCLSTracePointXDPPerfEventCGroupSKBSockOpsRawTracepointTracingStructOpsLSMNetfilter 等),对应内核 bpf_prog_type

紧随其后的 AttachType 是加载某些较新程序类型(如 CGroupSockAddr)时的必填项,用于区分允许访问的上下文;文档特别提醒:设置错误会在加载时触发 EINVALtypes.go)。其枚举覆盖 cgroup 各进出向挂钩、SkLookupXDPTraceKprobeMultiTCXNetfilterNetkit 等。此外还有 AttachFlagsPinTypePinNone/PinByName,镜像 libbpf 的 enum libbpf_pin_type)以及用于以只读/只写方式打开 pin 对象的 LoadPinOptions

程序加载参数与验证器日志

prog.go 揭示了加载程序时的"可观测性"设计:

  • 定义 ErrNotSupported(内核不支持某特性)、错误重定位(坏 CO-RE 重定位)等错误语义。
  • ProgramOptions 暴露了内核验证器日志控制:LogLevel 可参考 types.go 中的 LogLevelBranch(打印分支点验证器状态)、LogLevelInstruction(逐指令打印,Linux ≥ 5.2)、LogLevelStats(验证结束时输出错误与统计);日志缓冲区默认从 64 KiB 起步(minVerifierLogSize)并按需增长。
  • 默认行为:程序先不带验证器输出加载一次;失败后以 LogLevelBranch 与给定(或默认)日志大小重试一次,从而在出错时给出详细内核反馈。

加载流程纵深:CollectionSpec → Collection,ELF 与 Go 结构两种入口

collection.go 是库中组织批量加载的核心抽象:

  • CollectionSpeccollection.go)描述一个集合:MapsPrograms、全局变量 Variables(ELF 中声明的全局变量,加载前可自由读写,加载后修改对运行中的程序无效)、BTF 类型信息 Types 与字节序 ByteOrder;提供 Copy() 深拷贝。
  • CollectionOptions 支持用 MapReplacements 以既有 map 替换待新建的 map(要求类型、key/value 大小、max entries 与 flags 全部一致,内部会 Clone()),这是跨进程复用 map 的标准姿势;旧的 RewriteMaps 方法已被标记 Deprecatedcollection.go)。
  • 加载入口有三个层次:NewCollection(spec)NewCollectionWithOptions(spec, opts)collection.go);而 CollectionSpec.LoadAndAssign(to, opts)collection.go)会把加载出的程序与 map 按名称直接赋值到传入的结构体字段上,配合 doc.go 的告诫(defer Close),成为生成代码与手写代码共同推荐的高层 API。

程序与 map 的规格描述对象分别位于 prog.goProgramSpec,含 TypeInstructionsLicense 等)与 map.go。而 ELF 入口则在 elf_reader.go(把 clang 产出的 .o 解析为 CollectionSpec)与 elf_sections.go,真正落地系统调用的地方是 syscalls.gointernal/sys。底层借助 linker.gomarshalers.govariable.go 完成重定位与序列化。可以推断,一条典型的开发路径是:

clang 编译 eBPF(.o)  → LoadCollectionSpec(elf)  // 解析 ELF 为 CollectionSpec
                     → 修改 Spec(替换 map / 调变量)
                     → LoadAndAssign/NewCollection  // 送入内核
                     → link.AttachXxx(...)  // 附着到挂钩
                     → defer Close()  // 生命周期管理

实战案例:Moby 生态中 ebpf-go 的真实调用(cgroup v2 设备过滤器)

文档强调 ebpf-go 适用于"容器生态"长时进程——在本仓库中就能找到最直观的证据。Moby 构建链包含的 vendor/github.com/containerd/cgroups/v3/cgroup2/ebpf.go 实现了 LoadAttachCgroupDeviceFilter:它把 runc 风格生成的 asm.Instructions 包装成 ebpf.ProgramSpec(类型 ebpf.CGroupDevice),随后调用 ebpf.NewProgram 加载,再通过 link.RawAttachProgramlink/syscalls.go 中的底层封装)以 ebpf.AttachCgroupDevice 附着到 cgroup 目录 fd 上,并带上 BPF_F_ALLOW_MULTI 标志允许多个过滤器叠加,返回的 close 函数负责 RawDetachProgram

其前提条件写得很清楚:系统运行在 cgroup2 统一模式(unified mode)且内核 ≥ 4.15。真正的调度点在 vendor/github.com/containerd/cgroups/v3/cgroup2/manager.go 附近:当存在设备规则需要执行且 eBPF 设备过滤器可用时,cgroup 管理器即安装该过滤器来仲裁容器对设备文件的访问(配合 canSkipEBPFError 判断是否允许在纯 deny 规则下跳过 eBPF 而退回传统写法)。这就把文档中 asm(书写指令)、link(附着 cgroup 挂钩)、btf(如需 CO-RE)与顶层 API 串成了一个自洽的容器运行时场景——Moby 之所以引入 ebpf-go,正是为了在 cgroup v2 上用 eBPF 实现设备 cgroup 的强制策略

使用前提与运行约束

文档的 Requirements 部分明确了适用范围:

  • Go 版本:需使用上游仍支持维护的 Go 版本。
  • 内核版本:CI 针对 kernel.org LTS 版本运行;≥ 4.4 理论上可用,但已 EOL 的版本不在支持范围
  • 结合 doc.gorlimit 包说明可进一步确认:在 Linux 5.11 之前,加载 eBPF 通常需要放宽 RLIMIT_MEMLOCKrlimit 包正是为此提供便捷 API),新内核则依赖 cgroup/内存计费机制。
  • 特性探测需求(如不确定内核支持哪些 map/program/attach 类型)由 features 包承担——它相当于把 bpftool feature probe 搬进了 Go。

许可与归属

该库以 MIT 许可证发布(文档 License 一节)。其徽标 HoneyGopher(eBPF 主题吉祥物)基于 Renee French 设计的 Go gopher 形象二次创作,体现了它作为 Go 原生库的社区归属。

小结

作为 Moby 仓库中不可或缺的一环,ebpf-go 展示了纯 Go 用户态如何完整驾驭内核 eBPF:从 asm 的指令级书写、btf 的类型元数据,到顶层包的 CollectionSpec/LoadAndAssign 批量加载,再到 link 的多样挂钩与 perf/ringbuf 的数据回读,最后借由 cgroup v2 设备过滤器的实现证明其生产可用性。如果你想在自己的 Go 项目里引入内核级可编程能力,又不想引入 CGO 或 C 工具链,那么参照本文梳理的包职责、加载流程与资源生命周期约束,即可快速定位到该用哪个子包、如何组装加载链路。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.74 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
595
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.63 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
518
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
389