ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ebpf-go 示例程序全指南:从 kprobe 到 sched_ext 的 eBPF 实战路线图

ebpf-go 示例程序全指南:从 kprobe 到 sched_ext 的 eBPF 实战路线图 系统底层网络可观测性【免费下载链接】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点击查看免费下载本指南以仓库 examples/README.md 为主线系统梳理 ebpf-gogithub.com/cilium/ebpf官方示例集合的完整用法覆盖 kprobe、uprobe、tracepoint、cgroup、fentry、TCx、XDP、sched_ext 等全部挂载点类型并深入对应 Go 与 C 源码解释bpf2go代码生成、BPF 内核映射、perf/ringbuf 数据通路等底层机制。读完本文你将掌握每个示例的运行方式、关键 API 调用链与内核版本前提并能以此为模板搭建自己的 eBPF 观测程序。示例集整体概览examples/目录是一组演示如何使用 ebpf-go 库的完整可运行程序。每个示例都由一对文件构成一份eBPF C 程序如kprobe.c描述加载进内核的 BPF 程序与 BPF 映射一份Go 用户态程序如main.go负责把 BPF 程序加载进内核、挂载到钩子上、并从内核读取数据。C 源码文件头部都带有//go:build ignore构建标签不会被 Go 编译器编译仅供bpf2go工具链读取main.go则带有//go:build linux标签限定在 Linux 上编译运行kprobe.c。所有 C 程序统一#include common.h来自 examples/headers 目录共享SEC()、__uint/__type等 libbpf 风格宏。按挂载点类型官方示例分为八类类别示例挂载点 / 内核特性内核版本前提Kprobekprobe、kprobepin、kprobe_percpu、ringbuffer任意内核符号入口无特殊要求Uprobeuretprobe用户态二进制符号返回处无特殊要求Tracepointtracepoint_in_c、tracepoint_in_go预定义内核 tracepoint无特殊要求Cgroupcgroup_skbcgroup v2 的 egress 过滤无特殊要求Fentryfentry、tcprtt内核函数入口性能优于 kprobe5.5 及以上TCxtcxLinux TC 流量控制 ingress/egress6.6 及以上XDPxdp、xdp_live_frame网卡驱动层收包路径5.7live frame 需 5.18sched_extsched_extStructOpsMap 注册自定义调度器6.12 及以上运行示例一条命令需要 root 权限官方 README 给出的运行方式非常简洁cd ebpf/examples/ go run -exec sudo [./kprobe, ./uretprobe, ./ringbuffer, ...]关键点在于-exec sudo加载 eBPF 程序、创建 BPF 映射以及 attach 到内核钩子都需要特权因此必须借助sudo运行。-exec是go run的一个参数它告诉 Go 工具链用sudo作为最终执行二进制的前缀命令。部分示例需要额外参数xdp 与 tcx 要求第一个参数是网络接口名如go run -exec sudo ./xdp eth0xdp_live_frame 需要六个参数ifname repeat batch_size src_ip dst_ip dst_mac。各示例运行后默认每秒输出一行统计或持续打印事件按Ctrl-CSIGINT优雅退出。重新编译示例go generate 项目根 Makefile示例中的 Go 程序并不内嵌手写的汇编指令而是通过bpf2go从 C 源文件生成。每个main.go顶部都有一行go:generate指令例如//go:generate go tool bpf2go -tags linux bpf kprobe.c -- -I../headers这条指令的含义是调用 bpf2go 工具以kprobe.c为输入生成名为bpf的 Go 包对应bpf_bpfel.go/bpf_bpfeb.go文件并把-I../headers传给 clang 作为头文件搜索路径。生成的bpfObjects结构体集中了加载后的所有程序与映射句柄用户态代码据此访问objs.KprobeExecve、objs.KprobeMap等成员。官方文档说明示例由项目根目录的 Makefile 触发go generate构建Makefilemake -C ../从 Makefile 可以看到默认工具链被固定为clang-22/llvm-strip-22编译参数为-O2 -g -Wall -Werror -mcpuv2同时支持通过podman或docker容器化的 LLVM 工具链完成交叉编译生成-eb.elf大端与-el.elf小端两套对象避免本机缺少合适版本 clang 的问题。整个流程最终在examples/下产出*_bpfeb.go与*_bpfel.go两个生成文件如 counter_bpfeb.go分别对应大端/小端平台的加载代码。Kprobe 家族四种数据收集范式kprobe 是最通用的观测手段——在任意内核符号入口处触发。四个 kprobe 示例共享同一个观测目标sys_execve但展示了四种完全不同的数据收集方式是理解 BPF 映射与数据通路的理想教材。基础版kprobe 数组映射kprobe 的 C 程序定义了一个BPF_MAP_TYPE_ARRAY映射key 为u32、value 为u64、max_entries1kprobe.c。挂载在SEC(kprobe/sys_execve)上的程序每次被调用时对映射值执行__sync_fetch_and_add(valp, 1)原子自增SEC(kprobe/sys_execve) int kprobe_execve() { u32 key 0; u64 initval 1, *valp; valp bpf_map_lookup_elem(kprobe_map, key); if (!valp) { bpf_map_update_elem(kprobe_map, key, initval, BPF_ANY); return 0; } __sync_fetch_and_add(valp, 1); return 0; }Go 侧main.go的核心调用链只有三步rlimit.RemoveMemlock()解除内存锁限制 →loadBpfObjects(objs, nil)加载对象 →link.Kprobe(fn, objs.KprobeExecve, nil)挂载 kprobe。之后用time.Ticker每秒Lookup一次映射值并打印。值得注意rlimit.RemoveMemlock()自 Linux 5.11 起 memlock 限制不再是必要条件但保留该调用可兼容老内核因此几乎每个示例开头都有它。进阶一kprobepin——复用 pinned mapkprobepin 解决的是程序重启后映射数据丢失的问题。其 C 程序kprobe_pin.c在映射定义中多了__uint(pinning, LIBBPF_PIN_BY_NAME)声明该映射应按名字固定在 BPF 文件系统默认/sys/fs/bpf上。Go 侧则通过CollectionOptions.Maps.PinPath指定固定路径objs, err : loadBpfObjects(objs, ebpf.CollectionOptions{ Maps: ebpf.MapOptions{ // 若映射已存在则复用不存在则创建并 pin 到该路径 PinPath: pinPath, }, })其中pinPath path.Join(/sys/fs/bpf, sys_execve)并在挂载前用os.MkdirAll创建子目录main.go。这样即使 kprobe 程序退出重启计数也能从上次的值继续累加——这正是生产环境中跨进程共享观测状态的常用手段。进阶二kprobe_percpu——按 CPU 拆分的计数器kprobe_percpu 把映射类型换成BPF_MAP_TYPE_PERCPU_ARRAYkprobe_percpu.c。同一 key 在每个 CPU 上各有一份独立的值消除了多核并发自增时的缓存行争用。Go 侧读取方式也相应变化Lookup的目标类型从uint64变成[]uint64长度等于 CPU 数遍历即可输出每个 CPU 上的调用次数main.govar all_cpu_value []uint64 objs.KprobeMap.Lookup(mapKey, all_cpu_value) for cpuid, cpuvalue : range all_cpu_value { log.Printf(%s called %d times on CPU%v\n, fn, cpuvalue, cpuid) }进阶三ringbuffer——从计数到事件流ringbuffer 不再做计数而是把每次sys_execve调用作为一个事件推送出来。C 侧使用BPF_MAP_TYPE_RINGBUFmax_entries 124即 16MB事件结构含pid与commringbuffer.ctask_info bpf_ringbuf_reserve(events, sizeof(struct event), 0); if (!task_info) { return 0; } task_info-pid tgid; bpf_get_current_comm(task_info-comm, TASK_COMM_LEN); bpf_ringbuf_submit(task_info, 0);Go 侧用ringbuf.NewReader(objs.Events)打开读取器通过rd.Read()阻塞等待记录再用binary.Read把record.RawSample解析为 bpf2go 自动生成的bpfEvent结构main.go。读取循环里对errors.Is(err, ringbuf.ErrClosed)的判断用于处理信号触发的优雅退出。对比前两个示例ringbuffer 模式信息量更大、实时性更好是生产观测工具的推荐形态。Uprobeuretprobe 捕获用户态函数返回值uretprobe 演示了如何观测用户态二进制把 BPF 程序挂到/bin/bash中readline符号的返回处捕获其返回值用户输入的整行字符串。这是唯一一个带//go:build amd64 linux构建标签的示例main.go因为它直接操作寄存器读取返回值。Go 侧的关键 API 是link.OpenExecutable(binPath)打开 ELF 可执行文件再ex.Uretprobe(symbol, prog, nil)创建 uretprobemain.go。若你的发行版把readline放在动态库里需要先用ldd /bin/bash找到库路径并替换binPath例如/usr/lib/libreadline.so.8。C 侧展示了两个高频知识点uretprobe.cPT_REGS_RC(ctx)宏取出函数返回值在 amd64 上即ax寄存器用bpf_probe_read安全读取用户态内存bpf_perf_event_output把事件写入BPF_MAP_TYPE_PERF_EVENT_ARRAY映射。数据通路使用 perf 事件数组而非 ringbufGo 侧perf.NewReader(objs.Events, os.Getpagesize())创建读取器每次rd.Read()得到一条perf.Record解析RawSample前先检查record.LostSamples ! 0以感知环形缓冲溢出丢样本main.go。TracepointC 编程与纯 Go 汇编两种实现tracepoint 挂载在内核预定义、参数稳定的追踪点上比 kprobe 更安全不依赖内部符号实现细节。官方给出了两种实现路径的对照。tracepoint_in_c以 C 描述参数布局tracepoint_in_c 挂载到kmem/mm_page_alloctracepoint统计页面分配次数。C 侧的精髓在于按/sys/kernel/tracing/events/kmem/mm_page_alloc/format定义参数结构体——tracepoint 暴露给 BPF 程序的参数布局就由该 format 文件决定tracepoint.cstruct alloc_info { /* 前 8 字节不允许读取pad */ unsigned long pad; unsigned long pfn; unsigned int order; unsigned int gfp_flags; int migratetype; }; SEC(tracepoint/kmem/mm_page_alloc) int mm_page_alloc(struct alloc_info *info) { ... }tracepoint_in_go完全用 Go 组装 BPF 指令tracepoint_in_go 是仓库中最特别的示例——它不写一行 C直接用 Go 手写 BPF 汇编指令main.goprogSpec.Instructions asm.Instructions{ asm.Mov.Imm(asm.R2, 123), // 把常量 123 存入 FP[-8] asm.StoreMem(asm.RFP, -8, asm.R2, asm.Word), asm.LoadMapPtr(asm.R2, events.FD()), // 加载 perf event array 的 fd asm.LoadImm(asm.R3, 0xffffffff, asm.DWord), asm.Mov.Reg(asm.R4, asm.RFP), asm.Add.Imm(asm.R4, -8), asm.Mov.Imm(asm.R5, 4), asm.FnPerfEventOutput.Call(), // 调用内核 helper asm.Mov.Imm(asm.R0, 0), asm.Return(), }这段程序做的事是每次 tracepoint 触发时通过bpf_perf_event_outputhelper 向 perf 事件数组写入 4 字节常量123。与之配套的是纯 Go 构建的ebpf.ProgramSpec——它显式声明Type: ebpf.TracePoint只有 TracePoint 类型能挂到link.Tracepoint()创建的事件上以及License: GPL调用perf_event_output等 helper 必须使用 GPL 兼容许可。挂载调用为link.Tracepoint(syscalls, sys_enter_openat, prog, nil)main.go。该示例同时演示了手工创建映射ebpf.NewMap创建PerfEventArray、以asm包书写指令、以及 perf reader 的完整用法是理解 ebpf-go 底层汇编 DSL 的最佳入口。Cgroup统计 cgroup 出口流量cgroup_skb 演示把 BPF 程序挂到 cgroup v2 上作为egress 过滤器每个从该 cgroup 出去的包都会向程序传入__sk_buff指针。挂载使用link.AttachCgroupl, err : link.AttachCgroup(link.CgroupOptions{ Path: cgroupPath, Attach: ebpf.AttachCGroupInetEgress, Program: objs.CountEgressPackets, })main.go。其中cgroupPath通过解析/proc/mounts找到第一个类型为cgroup2的挂载点获得detectCgroupPath()函数逐行扫描、按fields[2] cgroup2匹配。若要在自己的机器上跑通请确保系统以 cgroup v2 挂载现代 systemd 发行版默认如此。Fentry比 kprobe 更快、更易用的内核函数入口fentry 与 kprobe 观测目标类似内核函数入口但直接利用 BTF 进行 trampoline 挂钩性能更好、参数访问更方便BTF 程序可直接读内核内存无需bpf_probe_read*前提是内核 5.5。两个示例分别演示连接跟踪与 RTT 测量。fentry追踪出站 TCP 连接fentry 挂载fentry/tcp_connect打印每个出站 IPv4 TCP 连接的进程名、地址与端口。C 程序fentry.c有两个值得注意的设计手工对齐的内核结构体程序按内核真实布局复制了struct sock_common/struct sock中会被访问的字段skc_daddr、skc_rcv_saddr、skc_dport、skc_num、skc_family并用注释明确警告不发射 CO-RE 信息、布局一旦与内核不一致程序就会出错——这是与用vmlinux.h CO-RE截然不同的兼容策略阅读时需注意区分BPF_PROG 宏int BPF_PROG(tcp_connect, struct sock *sk)是 fentry 类程序的惯例写法参数直接由宏解包程序体内可直接访问sk-__sk_common.skc_family。数据通过 ringbuf 送回用户态Go 侧按binary.BigEndian解析事件结构网络字节序字段用__be32/__be16声明挂载 API 为link.AttachTracing(link.TracingOptions{Program: objs.TcpConnect})main.go。tcprtt用 CO-RE 辅助函数读 RTTtcprtt 挂载fentry/tcp_close在连接关闭时读取srtt平滑 RTT输出Src addr / Port - Dest addr / Port / RTT表格main.go 中给出了真实运行样例输出。与 fentry 示例不同它使用 eBPF CO-RE helpers 读取内核结构不再手工复制结构体布局。IPv4 地址在 Go 侧通过net.IP转换后以十进制打印intToIP函数。该示例与 fentry 共同构成了fentry 观测 TCP 生命周期的完整套路。网络数据面TCx 与 XDPtcx内核 6.6 的现代流量控制挂载tcx 使用 Linux TCxTraffic Control with eBPF在网卡上同时统计 ingress 与 egress 包数。C 程序tcx.c别具一格——它不使用 BPF 映射而是用两个全局变量ingress_pkt_count/egress_pkt_count直接做原子自增__u64 ingress_pkt_count 0; __u64 egress_pkt_count 0; SEC(tc) int ingress_prog_func(struct __sk_buff *skb) { __sync_fetch_and_add(ingress_pkt_count, 1); return TC_ACT_OK; }Go 侧分别以ebpf.AttachTCXIngress与ebpf.AttachTCXEgress调用两次link.AttachTCXmain.go并通过 bpf2go 生成的ebpf.Variable类型objs.IngressPktCount/objs.EgressPktCount的.Get(value)方法读取全局变量值。注意该示例要求内核 6.6 及以上tcx bpf_link 的引入版本运行需指定网络接口参数。xdp按源 IP 统计收包数xdp 把程序挂到网卡驱动的收包早期路径XDP解析 IPv4 源地址并写入 LRU hash map。C 程序将每个源 IP 作为 key 累加计数Go 侧每秒用m.Iterate()遍历整个 map输出源IP 包数列表main.go。挂载 API 为link.AttachXDP(link.XDPOptions{Program: ..., Interface: iface.Index})iface由net.InterfaceByName(os.Args[1])按名字查得。示例注释提示只需修改返回值为XDP_DROP/XDP_REDIRECT即可从观测升级为丢包/转发依赖 bpf_link 的内核 5.7。xdp_live_frameXDP 活帧模式发包器xdp_live_frame 演示内核 5.18 引入的BPF_F_TEST_XDP_LIVE_FRAMES特性让 XDP 程序运行在活帧模式下把构造好的数据帧直接以XDP_TX从网卡发出用于高性能发包与测试。它的运行方式也最工程化先link.AttachXDP挂一个透传程序XdpProgPassXDP_TX 生效的前提用 Go 手工构造完整的 Ethernet IPv4 UDP 报文buildUDPPacket含 IP 校验和计算ipChecksum见 main.go通过objs.XdpProgTx.Run(ebpf.RunOptions{Data: pkt, Repeat: repeat, Flags: unix.BPF_F_TEST_XDP_LIVE_FRAMES, Context: xdpmd, BatchSize: batchSize})执行程序main.go。其中sys.XdpMd上下文提供DataEnd与IngressIfindex字段Repeat/BatchSize控制发送次数与每批帧数是理解用户态驱动内核发包路径的完整样例。sched_ext内核 6.12 的自定义调度器sched_ext 是最新的示例展示了 ebpf-go 对StructOpsMap的支持——通过link.AttachStructOps把一个sched_ext_ops映射注册为内核调度器m : objs.MinimalSched l, err : link.AttachStructOps(link.StructOpsOptions{Map: m}) if err ! nil { log.Fatalf(failed to attach sched_ext: %s, err) }main.go。程序运行后可用两条命令验证调度器状态# cat /sys/kernel/sched_ext/state enabled # cat /sys/kernel/sched_ext/root/ops miminal注意该示例的go:generate指令带有-no-global-types标志main.go告知 bpf2go 不生成全局类型且该特性要求内核版本6.12 起。它代表 eBPF 从观测/网络走向内核机制替换的前沿方向。共性模式所有示例共享的四步骨架纵观全部示例可以提炼出 ebpf-go 程序固定的四步骨架这也是把任一示例改造成自己程序的模板解除资源限制rlimit.RemoveMemlock()rlimit/rlimit_linux.go兼容老内核的 memlock 限制加载对象loadBpfObjects(objs, nil)由 bpf2go 生成底层走 elf_reader.go 的 ELF 解析与 linker.go 的指令重定位或手工ebpf.NewProgram/ebpf.NewMap挂载钩子按目标选择link.Kprobe、link.Uprobe、link.Tracepoint、link.AttachCgroup、link.AttachTracing、link.AttachXDP、link.AttachTCX、link.AttachStructOps之一返回的link.Link对象defer Close()以在退出时自动卸载消费数据计数器类用Map.Lookup轮询事件流类用perf.NewReader或ringbuf.NewReader阻塞读取并正确处理perf.ErrClosed/ringbuf.ErrClosed实现信号优雅退出。内核版本与功能对照速查功能所需内核版本出处示例kprobe / uprobe / tracepoint / cgroup_skb无特殊要求长期可用kprobe、uretprobe、tracepoint_*、cgroup_skbfentry / fexit5.5fentry、tcprttXDP bpf_link5.7xdpXDP live frameBPF_F_TEST_XDP_LIVE_FRAMES5.18xdp_live_frametcxTCX bpf_link6.6tcxsched_extStructOpsMap6.12sched_ext版本要求均可在各示例main.go的文件头注释与 README 中核实例如 xdp_live_frame/main.go、tcx/main.go、sched_ext/main.go。对照上表选择示例即可在自己的内核版本上快速验证对应能力。从示例到自有程序的三条路径结合仓库源码官方示例为开发者提供了三条可复制的落地路径最快上手复制某个main.go.c对修改go:generate行中的 C 文件名与SEC()段名重跑make -C ../触发 bpf2go 重新生成即得到新程序骨架纯 Go 实现参考 tracepoint_in_go用 asm/instruction.go 提供的指令 DSL 手写程序体无需任何 C 工具链但需自行保证指令序列通过内核 verifier跨进程状态需要持久化计数或跨程序共享数据时参考 kprobepin 的PinPathLIBBPF_PIN_BY_NAME组合把映射固定在/sys/fs/bpf下复用。无论选择哪条路径examples/目录本身examples/README.md就是最好的索引——它按挂载点类型组织、每项都标注了内核版本前提完全可以作为开发者在 ebpf-go 项目中的能力清单使用。赞分享系统底层网络可观测性【免费下载链接】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点击查看免费下载相关推荐深入理解cilium/ebpf项目示例程序eBPF开发实战指南深入理解cilium/ebpf项目示例程序eBPF开发实战指南 前言 eBPF扩展的伯克利包过滤器是一项革命性的Linux内核技术允许开发者在不修改内核系统底层网络可观测性ebpf-go 项目全解析从 2017 年的 newtools/ebpf 到 Go 生态的 eBPF 基础设施ebpf go 项目全解析从 2017 年的 newtools/ebpf 到 Go 生态的 eBPF 基础设施 本文以仓库内 docs/ebpf/about.系统底层网络可观测性ebpf-go纯Go语言实现的eBPF程序加载与操作库ebpf go纯Go语言实现的eBPF程序加载与操作库 ebpf go 是一个使用纯Go语言编写的库它提供了读取、修改和加载eBPF程序的功能并能将这些程系统底层网络可观测性上一篇毕昇JDK在devkit-pipeline中的应用Java开发者的鲲鹏迁移指南下一篇openEuler/raspberrypi项目版本管理策略解析从LTS到SP的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表