You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

能否从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:

  1. 在bpf.c中定义包含PID和cgroup名称的结构体,用bpf_perf_event_output将事件发送到用户态。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 11:59:59