Go + eBPF实战:用cilium/ebpf库5分钟写一个系统调用追踪器(附完整代码)

张开发
2026/4/20 14:11:47 15 分钟阅读

分享文章

Go + eBPF实战:用cilium/ebpf库5分钟写一个系统调用追踪器(附完整代码)
用Go和cilium/ebpf快速构建系统调用追踪器在当今云原生和微服务架构盛行的时代系统可观测性变得前所未有的重要。传统监控工具往往存在性能开销大、功能局限等问题而eBPF技术则为我们提供了一种低开销、高灵活性的内核级观测方案。本文将带你用Go语言和cilium/ebpf库在5分钟内构建一个实用的系统调用追踪器。1. 环境准备与基础概念在开始编码前我们需要确保开发环境就绪。推荐使用Linux内核5.4及以上版本并安装以下工具sudo apt-get update sudo apt-get install -y make clang llvm go get github.com/cilium/ebpfeBPFextended Berkeley Packet Filter是一种革命性的内核技术它允许用户在不修改内核源码或加载内核模块的情况下在内核中运行沙盒程序。与传统方案相比eBPF具有以下优势低开销在内核中直接处理数据避免用户空间和内核空间的频繁切换安全性通过严格的验证器确保程序不会导致内核崩溃灵活性可应用于网络、安全、追踪等多个领域cilium/ebpf是一个纯Go语言实现的eBPF库它封装了底层系统调用提供了更友好的开发接口。与传统的BCC工具链相比它不需要在目标机器上安装LLVM等重型依赖更适合生产环境部署。2. 内核态eBPF程序编写创建一个名为bpf/syscall_trace.c的文件内容如下#include linux/bpf.h #include linux/ptrace.h #include linux/sched.h struct syscall_data { u32 pid; u64 timestamp; char comm[16]; char filename[256]; }; struct { __uint(type, BPF_MAP_TYPE_RINGBUF); __uint(max_entries, 1 24); } events SEC(.maps); SEC(tracepoint/syscalls/sys_enter_openat) int trace_openat(struct trace_event_raw_sys_enter* ctx) { struct syscall_data *event; event bpf_ringbuf_reserve(events, sizeof(*event), 0); if (!event) { return 0; } event-pid bpf_get_current_pid_tgid() 32; event-timestamp bpf_ktime_get_ns(); bpf_get_current_comm(event-comm, sizeof(event-comm)); bpf_probe_read_user_str(event-filename, sizeof(event-filename), (char *)ctx-args[1]); bpf_ringbuf_submit(event, 0); return 0; } char _license[] SEC(license) GPL;这段代码定义了一个eBPF程序它会在openat系统调用入口处触发记录进程ID、时间戳、进程名和文件名通过ring buffer将数据发送到用户空间关键点说明SEC宏定义了程序的挂载点bpf_ringbuf_reserve从环形缓冲区预留空间bpf_probe_read_user_str安全地读取用户空间字符串3. 用户态Go程序开发接下来创建main.go文件负责加载eBPF程序和读取事件package main import ( bytes encoding/binary fmt log os os/signal syscall time github.com/cilium/ebpf github.com/cilium/ebpf/link github.com/cilium/ebpf/ringbuf github.com/cilium/ebpf/rlimit ) type syscallData struct { Pid uint32 Timestamp uint64 Comm [16]byte Filename [256]byte } func main() { // 移除内存限制 if err : rlimit.RemoveMemlock(); err ! nil { log.Fatal(err) } // 加载编译好的eBPF程序 collection, err : ebpf.LoadCollection(bpf/syscall_trace.o) if err ! nil { log.Fatalf(加载eBPF集合失败: %v, err) } defer collection.Close() // 附加到tracepoint tp, err : link.Tracepoint(syscalls, sys_enter_openat, collection.Programs[trace_openat]) if err ! nil { log.Fatalf(附加tracepoint失败: %v, err) } defer tp.Close() // 设置ring buffer读取器 rd, err : ringbuf.NewReader(collection.Maps[events]) if err ! nil { log.Fatalf(创建ringbuf读取器失败: %v, err) } defer rd.Close() // 处理中断信号 sig : make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) fmt.Println(开始追踪openat系统调用按CtrlC停止...) // 事件处理循环 go func() { var event syscallData for { record, err : rd.Read() if err ! nil { if ringbuf.IsClosed(err) { return } log.Printf(读取ringbuf失败: %v, err) continue } if err : binary.Read(bytes.NewBuffer(record.RawSample), binary.LittleEndian, event); err ! nil { log.Printf(解析事件失败: %v, err) continue } timestamp : time.Unix(0, int64(event.Timestamp)) fmt.Printf([%s] PID: %d, 进程名: %s, 文件名: %s\n, timestamp.Format(time.RFC3339), event.Pid, string(event.Comm[:bytes.IndexByte(event.Comm[:], 0)]), string(event.Filename[:bytes.IndexByte(event.Filename[:], 0)]), ) } }() -sig fmt.Println(\n停止追踪...) }这段Go代码完成了以下工作移除eBPF程序的内存限制加载编译好的eBPF字节码将程序附加到sys_enter_openat跟踪点设置ring buffer读取器获取内核事件解析并格式化输出事件数据4. 编译与运行创建一个简单的Makefile来简化构建过程all: build run build: clang -O2 -target bpf -c bpf/syscall_trace.c -o bpf/syscall_trace.o go build -o syscall_tracker . run: sudo ./syscall_tracker clean: rm -f bpf/syscall_trace.o syscall_tracker现在可以执行以下命令来编译和运行程序make程序运行后会在终端输出类似以下内容[2023-08-15T14:30:2208:00] PID: 1234, 进程名: bash, 文件名: /etc/passwd [2023-08-15T14:30:2308:00] PID: 5678, 进程名: vim, 文件名: /home/user/document.txt5. 进阶优化与扩展基础版本完成后我们可以考虑以下优化方向性能优化增加事件过滤条件减少不必要的事件捕获使用perf buffer替代ring buffer以获得更好的性能实现批处理事件读取减少用户空间-内核空间切换功能扩展添加更多系统调用的追踪支持实现基于进程名的过滤添加网络输出支持将事件发送到远程服务器集成Prometheus指标导出错误处理增强添加更完善的错误处理和重试机制实现配置热加载功能添加日志轮转和归档支持一个简单的过滤功能实现示例// 在eBPF C代码中添加过滤逻辑 if (event-pid ! target_pid) { return 0; } // 在Go代码中设置过滤值 targetPid : uint32(1234) if err : collection.Maps[filter_map].Put(targetPid, uint8(1)); err ! nil { log.Fatalf(设置过滤失败: %v, err) }通过这个简单的项目我们展示了如何利用Go和cilium/ebpf快速构建实用的系统观测工具。相比传统方案这种组合提供了更好的开发体验和运行时性能非常适合云原生环境下的系统监控需求。

更多文章