能否从eBPF Map获取用户态char*字符串内容?技术咨询
问题描述
我的eBPF程序旨在追踪sched_wakeup等调度相关tracepoint的数据,需要获取触发时所在的cgroup。我尝试通过bpf_get_current_task()->cgroups->subsys->cgroup->kn->name拿到char类型的cgroup名称,然后用一个输出型eBPF Map传递给Golang主程序——Map的key为u32类型(存储PID),value为char类型(存储cgroup名称)。但Map返回的是类似0x00000000的地址值,Golang程序中用unsafe.Pointer接收后,通过*(*string)(cgroupName)打印仅输出空值。请问这是因为地址属于内核空间或BPF栈导致用户态无法访问,还是整体思路存在问题?
bpf.c
#include "vmlinux.h" #include "bpf_helpers.h" #include "bpf_core_read.h" struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 10240); __type(key, u32); __type(value, char *); } pid_cgroup_name SEC(".maps"); SEC("tp/sched/sched_wakeup") int handle__sched_wakeup(struct sched_wakeup_tp_args *ctx) { struct task_struct *task = (void *)bpf_get_current_task(); return trace_enqueue(task); } static __always_inline int trace_enqueue(struct task_struct *task) { u32 pid; struct css_set *cgroups; struct cgroup_subsys_state *subsys[14]; struct cgroup *cg; struct kernfs_node *kn; char *cgroup_name; bpf_core_read(&cgroups, sizeof(cgroups), &task->cgroups); bpf_core_read(&subsys, sizeof(subsys), &cgroups->subsys); bpf_core_read(&cg, sizeof(cg), &subsys[1]->cgroup); bpf_core_read(&kn, sizeof(kn), &cg->kn); bpf_core_read(&cgroup_name,sizeof(cgroup_name),&kn->name); if (!cgroup_name) return 0; bpf_core_read(&pid, sizeof(pid), &task->tgid); bpf_map_update_elem(&pid_cgroup_name, &pid, &cgroup_name, 0); return 0; }
main.go
package main import ( "C" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/rlimit" "log" "time" ) // $BPF_CLANG and $BPF_CFLAGS are set by the Makefile. //go:generate go run github.com/cilium/ebpf/cmd/bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS bpf bpf.c -- -I../headers -I../csl-headers func main() { // Allow the current process to lock memory for eBPF resources. if err := rlimit.RemoveMemlock(); err != nil { log.Fatal(err) } // Load pre-compiled programs and maps into the kernel. objs := bpfObjects{} if err := loadBpfObjects(&objs, nil); err != nil { log.Fatalf("loading objects: %v", err) } defer objs.Close() tpWakeup, err := link.Tracepoint("sched", "sched_wakeup", objs.HandleSchedWakeup, nil) if err != nil { log.Fatalf("opening tracepoint: %s", err) } defer tpWakeup.Close() ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() log.Println("Waiting for events..") for range ticker.C { mapIterator := objs.PidCgroupName.Iterate() var pid uint32 var cgroupName unsafe.Pointer for mapIterator.Next(&pid, &cgroupName) { log.Printf("get pid %v for cgroup name: %s", pid, *(*string)(cgroupName)) } } }
运行输出
2023/03/08 16:20:11 get pid 12345 for cgroup name:
问题分析与解决方法
核心问题原因
你遇到的问题本质是内核空间地址无法被用户态直接访问:
kn->name指向的是内核内存中的字符串,你将这个内核地址存入Map后,用户态Golang程序拿到的只是一个无权限访问的内核空间指针,自然读不到有效内容。- 另外,BPF程序的栈是临时资源,即使你存储栈上字符串的地址,栈帧销毁后地址也会失效,但这里的核心矛盾还是内核地址的访问权限问题。
修正思路
不能直接存储字符串指针,需要将内核中的cgroup名称拷贝到用户态可访问的内存区域,常用两种实现方式:
方式1:用固定长度数组作为Map的value
修改bpf.c中的Map定义,将value改为固定大小的char数组,在BPF程序中用bpf_core_read_str将内核字符串拷贝到数组中:
// 修改Map定义 struct { __uint(type, BPF_MAP_TYPE_HASH); __uint(max_entries, 10240); __type(key, u32); __type(value, char[64]); // 用固定长度数组存储cgroup名称 } pid_cgroup_name SEC(".maps"); // 更新trace_enqueue函数的拷贝逻辑 static __always_inline int trace_enqueue(struct task_struct *task) { u32 pid; char cgroup_name[64] = {0}; // 定义数组存储名称 struct css_set *cgroups; struct cgroup_subsys_state *subsys_css; struct cgroup *cg; struct kernfs_node *kn; // 简化指针读取,避免拷贝整个subsys数组 bpf_core_read(&cgroups, sizeof(cgroups), &task->cgroups); bpf_core_read(&subsys_css, sizeof(subsys_css), &cgroups->subsys[1]); bpf_core_read(&cg, sizeof(cg), &subsys_css->cgroup); bpf_core_read(&kn, sizeof(kn), &cg->kn); // 将内核字符串拷贝到数组,最多拷贝63个字符(留1位存终止符) bpf_core_read_str(cgroup_name, sizeof(cgroup_name), &kn->name); bpf_core_read(&pid, sizeof(pid), &task->tgid); // 更新Map,直接存储数组内容 bpf_map_update_elem(&pid_cgroup_name, &pid, cgroup_name, 0); return 0; }
同时修改Golang代码中的value接收逻辑,用数组接收并转成字符串:
import "bytes" // 需要导入bytes包 // ... for range ticker.C { mapIterator := objs.PidCgroupName.Iterate() var pid uint32 var cgroupName [64]byte for mapIterator.Next(&pid, &cgroupName) { // 去掉数组末尾的空字符,转成有效字符串 name := string(cgroupName[:bytes.IndexByte(cgroupName[:], 0)]) log.Printf("get pid %v for cgroup name: %s", pid, name) } }
方式2:用BPF_PERF_EVENT_ARRAY发送事件
如果cgroup名称长度不固定,或需要实时传递数据,更适合用perf buffer:
- 在bpf.c中定义包含PID和cgroup名称的结构体,用
bpf_perf_event_output将事件发送到用户态。 - Golang程序中通过perf buffer接收事件,解析结构体中的字符串内容。
这种方式更灵活,支持动态长度数据,但实现复杂度稍高。
额外注意点
- 内核版本兼容性:
task_struct->cgroups、css_set->subsys等结构可能随内核版本变化,建议持续使用BTF和bpf_core_read保证兼容性。 - subsys数组索引:
subsys[1]在不同内核版本可能对应不同的cgroup控制器(比如早期版本对应cpu控制器),若要获取进程所属顶层cgroup,可能需要遍历subsys数组或使用task->cgroup(部分内核版本直接提供)。
内容的提问来源于stack exchange,提问作者54vault
相关产品推荐
相关产品推荐

