本文地址:https://www.ebpf.top/post/passive-linux-stack-sampler-ebpf

1. 背景

传统的 Linux 性能分析工具(如众所周知的 perf),主要依赖于内核预置的跟踪点(kprobe/tracepoints)和性能监控单元(PMU)等进行事件采样。Perf 工具通过周期性中断 CPU 来捕获调用栈和性能计数器,完成性能运行数据的采集,以此定位热点代码和系统瓶颈。该方式与内核紧密集成,开销相对较低,但其灵活性和深度受限于预先定义的钩子点,难以应对一些复杂或自定义的性能分析场景。

img

随着 eBPF 技术则带来了更强大的内核可编程能力,可无需修改内核源码或重启系统前提下,允许用户安全地编写程序并加载到内核中运行。基于 eBPF 的 profile 等工具通过 perf 事件进行定时采样,通过内核中运行的 eBPF 程序可通过特定过滤条件完成原始/预处理的栈调用信息通过高效的 perf-event/ring-buffer 结构高效传递到用户空间,此后用户空间工具进行符号化解析和格式化暂时,可高效完成详细的性能分析结果的呈现。

更进一步,随着 eBPF 技术的完善和增强,像工具 xstack 展现了 eBPF 在性能分析模式上的创新。xstack 基于 eBPF 任务迭代(task iterators)机制,实现了从 “被动采样” 到 “主动遍历” 的转变。xstack 工具可以直接扫描系统中运行的任务队列,按需获取系统内所有进程的调用栈快照,避免了在高频路径上插桩(如 kprobe/tracepoint 等)带来的开销。这种方式特别适合需要周期性获取系统全局进程状态全景的场景,为性能分析提供了新的视角和手段。

本文会从 xstack 使用方式和整体工作原理、源码实现等维度进行展开介绍,希望对基于任务迭代器搜集栈调用机制有个完整的熟悉。首先,我们还是从工具使用开始。

2. xstack 工具简介

xstack 工具运行最低内核版本为 Linux 5.18(依赖辅助函数 bpf_copy_from_user_task())。其在内核空间实现按过滤条件进行内核/用户空间调用栈采集和上报功能,采集用户空间栈信息依赖于程序编译支持栈帧指针展开(-fno-omit-frame-pointe),栈地址的可视化解析则是在用户空间采用了 libbpf/blzesym 库。

工具需要进行源码编译,代码可从 0xtools/xstack 目录中下载,README 文件 提供了安装和编译相关的说明。

2.1 构建

前置依赖:

  • Linux Kernel 5.18+,主要是使用的 bpf_copy_from_user_task() 辅助函数;
  • 开发工具:gcc, make, clang, llvm
  • Rust/Cargo,主要是符号解析库 BlazeSym 依赖;

在 Ubuntu 22.x or 24.x 操作系统上的安装命令如下:

1
$sudo apt install make gcc pkg-config libbpf-dev libbpf-tools clang llvm libbfd-dev libelf1 libelf-dev zlib1g-dev rustc cargo

完成基础依赖包安装后,可使用以下命令直接编译源码,生成可执行程序:

1
2
3
4
5
6
$ git clone https://github.com/tanelpoder/0xtools && cd 0xtools/xstack
# Clone libbpf and blazesym submodules
$ git submodule update --init --recursive

$ make
$./xstack --help

2.2 工具使用方式

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ ./xstack --help

USAGE: xstack -a | -p PID | -t TID [-F HZ] [-i NUM]

EXAMPLES:
  xstack -a           # Sample all tasks continuously
  xstack -p 1234      # Sample process 1234 and its threads
  xstack -t 5678      # Sample only thread 5678
  xstack -a -F 10     # Sample all tasks at 10 Hz
  xstack -a -i 100    # Sample all tasks for 100 iterations
  xstack -p $$ -F 5 -i 25  # Sample shell at 5 Hz for 5 seconds

  -a, --all                  Sample all tasks/threads
  -F, --freq=HZ              Sampling frequency in Hz (default: 1)
  -i, --iterations=NUM       Number of sampling iterations (default: infinite)
  -p, --pid=PID              Filter by process ID (TGID)
  -q, --quiet                Suppress CSV header output
  -r, --reverse-stack        Reverse stack trace order (innermost first)
  -t, --tid=TID              Filter by thread ID (PID)
  -?, --help                 Give this help list
      --usage                Give a short usage message
  -V, --version              Print program version

命令参数按功能分为目标选择采样控制输出调整三大类:

目标参数

用于指定采样对象,必须且只能选择其中一种,用于精准定位需要分析的进程或线程。

  • -a--all):适用于系统级性能监控,需全面分析整机任务的栈调用,例如排查系统级瓶颈或资源竞争问题。

  • -p PID--pid=PID):针对特定进程(如高 CPU 占用的应用程序)进行监控,同时覆盖其所有线程,适合单进程级问题定位,例如分析服务进程的调用链耗时。

  • -t TID--tid=TID):聚焦单个线程(如多线程程序中的异常线程),支持线程级别的精确堆栈采样,例如定位线程死锁或高频调度问题。

采样参数

用于控制采样的频率和时长,确定数据采集的强度与范围。

  • -F HZ--freq=HZ):默认每秒采样 1 次,若需捕捉高频事件(如瞬时性能波动),可提高频率(如 -F 10 表示每秒采样 10 次);低频采样(如 -F 0.5)则适用于长期趋势分析。
  • -i NUM--iterations=NUM):默认无限采样(需手动终止),指定迭代次数后可自动停止。例如 -i 60 -F 1 表示采样 60 秒, -i 100 -F 2 表示采样 50 秒。

输出参数

用于优化堆栈数据的展示格式,适配不同的分析需求。

  • -q--quiet):当需将输出结果直接导入数据分析工具(如 Excel、Python 脚本)时,可抑制表头以简化数据处理流程。
  • -r--reverse-stack):默认堆栈展示顺序为从外层到内层函数,启用此参数后改为内层函数优先,便于快速定位调用链的起点(如异常函数的直接调用者)。

2.3 实现命令行火焰图

xstack 实现了堆栈采集和符号解析展示,配合 Rust 语言编写的开源工具 flamelens,可实现命令行界面的火焰图展示,效果还是比较酷。如下图所示:

xstack

3. xstack 实现原理

3.1 工具实现原理

xstack 实现无侵入采样的核心技术基石是内核提供的 eBPF 可睡眠任务迭代器(sleepable task iterators)。该迭代机制允许 eBPF 程序在遍历系统进程/线程任务时安全进入睡眠状态,从而避免了传统同步迭代器可能引发的死锁风险,尤其适用于需要长时间遍历或处理大量任务的场景。与直接挂载内核函数(如do_exitschedule)的 kprobe 方案相比,eBPF 任务迭代器的接口兼容性由内核维护团队保障,其稳定性不受内核版本变更影响,可有效规避因内核函数签名修改导致的程序失效问题,显著降低跨版本维护成本。

在任务遍历的实现上,xstack 通过 eBPF 任务迭代器按进程/线程ID(PID/TID)精准过滤目标任务,可避免全系统扫描带来的性能开销。其核心优势在于无侵入性—整个过程无需向系统关键路径注入任何跟踪点(tracepoints)、kprobes 或 perf 事件,仅通过内核提供的迭代器接口被动读取任务状态,从根本上消除了对应用进程的直接干扰。

为实现对用户态栈的安全采集,xstack 采用 bpf_copy_from_user_task() 辅助函数。该函数专为跨进程内存访问设计,通过内核态权限安全读取目标线程的用户空间数据,解决了传统方法中因用户态内存地址有效性或权限问题导致的访问失败风险。结合帧指针(fp寄存器)的辅助,xstack 可在有帧指针的环境下同时提取内核态栈(通过内核栈指针)与用户态栈,完整还原调用链信息。这种双栈采集能力不仅确保了数据完整性,更通过内存安全机制避免了采样过程对目标进程的稳定性影响。

综上,eBPF 可睡眠任务迭代器与 bpf_copy_from_user_task() 的组合,构建了一套兼顾稳定性、安全性与无侵入性的采样框架:前者通过内核原生接口保障跨版本兼容与遍历安全,后者通过专用辅助函数解决用户态内存访问权限问题,最终实现了对系统线程状态及栈的主动式采集,为高性能、低干扰的 Linux 堆栈采样提供了技术支撑。

在对其核心原理介绍后,我们在下一个章节通过源码维度进行分析。

4. 实现源码解析

源码解析部分我们会通过内核空间 eBPF 代码和用户空间代码两部分展开,完整的精简在文章附录,有兴趣可阅读。

4.1 内核空间源码

内核空间的代码 eBPF 代码主要为任务代码器便利时针对单个任务调用处理的主要逻辑。eBPF 代码在文件 xstack.bpf.c 实现,核心逻辑分为三个部分:

  • 过滤规则配置:首先通过配置 map(config_map)定义过滤规则,支持按进程 ID 或线程 ID 筛选目标任务,可排除空闲内核线程;

    1
    2
    3
    4
    5
    6
    7
    
    // Configuration map
    struct {
        __uint(type, BPF_MAP_TYPE_ARRAY);
        __uint(max_entries, 1);
        __type(key, __u32);
        __type(value, struct filter_config);
    } config_map SEC(".maps");
    
  • 用户空间通信 Ring Buffer 结构对象:通信 events 结构使用 Ring Buffer 结构高效存储采集的事件数据,包含任务基础信息和堆栈内容;

    1
    2
    3
    4
    
    struct {
        __uint(type, BPF_MAP_TYPE_RINGBUF);
        __uint(max_entries, 8 * 1024 * 1024);   // 8MB
    } events SEC(".maps");
    
  • 目标任务的调用栈采集和上报:栈的采集函数为 dump_task(), 程序类型为 iter.s/task,栈采集分为内核空间和用户空间调用栈获取。 利用 bpf_get_task_stack() 辅助函数直接捕获内核栈,而对于用户栈,由于 BPF 辅助函数在非当前任务上下文中的限制,程序采用手动栈展开方案,在 x86_64 架构下通过帧指针链(RBP)逐帧读取返回地址(RIP),使用 bpf_copy_from_user_task() 实现跨任务用户栈回溯。dump_task() 核心流程如下:

    • 步骤1:判断过滤模式,检查目标任务是否符合采集条件,不符合则返回;

    • 步骤2:分配 Ring Buffer 事件对象,填充进程任务的 tgid、pid、state 和 comm 信息;

    • 步骤3:使用 bpf_get_task_stack() 辅助函数获取内核空间堆栈;

    • 步骤4:获取用户空间栈调用,基于 x86_64 或 arm64 调用机制进行遍历,依赖 bpf_copy_from_user_task() 辅助函数;

    • 步骤5:使用 bpf_ringbuf_submit() 函数提交 event 事件信息到用户空间;

3.2 用户空间核心代码

作为采集工具的控制端,主要负责管理 eBPF 程序的运行周期、处理采集数据并符号化输出。用户空间代码在文件 xstack.c 中实现,

核心功能组分为程序初始化工作和循环读取数据并处理两个模块,介绍如下:

  • 程序整体初始化工作
    • 采集模式、采集频率等参数初始化;
    • eBPF 程序读取和加载管理;
    • 将预设的过滤配置加载到内核空间 map 结构中,并创建 bpf_link 对象管理 eBPF 程序生命周期;
    • 使用 ring_buffer__new() 函数创建 ring_buffer 对象,并设置事件处理回调函数 handle_event()
    • 如果符号解析使用 BLAZESYM ,则使用函数 blaze_symbolizer_new_opts() 完成初始化;
  • 按照采样间隔在整体循环采集主循环逻辑
    • 判断采集是否结束,如触发终止条件则退出;
    • 通过 bpf_iter_create() 创建内核任务迭代器,在每个任务上触发内核中的 dump_task() 函数执行,按需数据写入到对应 ring_buffer 对象;
    • 使用函数 ring_buffer__poll() 持续读取 ring_buffer 中的数据;
      • 读取到的数据调用 handle_event() 函数进行处理;
        • handle_event() 函数完成内核空间和用户空间栈地址转换为可读符号化,最终输出时间戳、任务信息和符号化栈;
    • 基于采集频率计算下一轮采集的间隔,并持续进入到循环采集流程;

5. 总结

本文针对零侵入主动性能数据探测工具 xstack 的构建、使用、原理和源码实现进行了整体介绍,在高版本内核中如有类似场景的需求,可直接使用或定制,该工具代码相对简单清晰,也可作为我们学习编写 eBPF 工具的参考样例。

6. 代码附录

6.1 eBPF 核心代码

dump_task() 函数代码逻辑如下所示(部分代码进行了精简):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/* Sleepable task iterator is needed for reading userspace memory of other tasks */
SEC("iter.s/task")
int dump_task(struct bpf_iter__task *ctx)
{
    struct task_struct *task = ctx->task;
    
    // 步骤1:判断过滤模式,检查进程是否符合采集 filter_mode == 0 表示采集所有任务
    __u32 key = 0;
    struct filter_config *cfg = bpf_map_lookup_elem(&config_map, &key);
    __u32 pid = task->pid;
    __u32 tgid = task->tgid;
    
    if (cfg->filter_mode == 1) {  // Filter by TGID (process)
        if (tgid != cfg->target_tgid)
            return 0;
    } else if (cfg->filter_mode == 2) {  // Filter by PID (thread)
        if (pid != cfg->target_pid)
            return 0;
    }

    __u32 state = task->__state;    

    // do not emit IDLE kernel threads
    if ((task->flags & PF_KTHREAD) && (state & TASK_IDLE))
        return 0;
    
    // 步骤2:分配 ringbuf 事件对象,填充进程 tgid、pid、state 和 comm 信息
    struct stack_event *event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
    
    event->pid = pid;
    event->tgid = tgid;
    event->state = state;

    bpf_probe_read_kernel_str(&event->comm, sizeof(event->comm), task->comm);
    
    // 步骤3:使用 bpf_get_task_stack 辅助函数获取内核空间堆栈
    event->kstack_sz = bpf_get_task_stack(task, event->kstack, 
                                          sizeof(event->kstack), 0);
    if (event->kstack_sz < 0)
        event->kstack_sz = 0;
    else
        event->kstack_sz /= sizeof(__u64);  // Convert bytes to number of entries
    
    // Get userspace stack - need manual unwinding when reading it from other task contexts
    // bpf_get_task_stack with BPF_F_USER_STACK only works for current task in a TP context
    event->ustack_sz = 0;
    
    // 步骤4:获取用户空间进程堆栈,基于 x86_64 或 arm64 实现遍历,主要依赖 bpf_copy_from_user_task 辅助函数
    // Don't try to manually unwind kernel threads as we have a BPF helper for that
    if (!(task->flags & PF_KTHREAD)) {
        // Get the current stack pointer from task's pt_regs
        struct pt_regs *regs = (struct pt_regs *)bpf_task_pt_regs(task);
        if (regs) {
            // Architecture-specific frame pointer unwinding
            #if defined(__TARGET_ARCH_x86)
                // x86_64 stack frame layout:
                // struct stack_frame {
                //     void *rbp;  // next frame pointer
                //     void *rip;  // return address
                // };
                __u64 fp = BPF_CORE_READ(regs, bp);  // Frame pointer (RBP)
                __u64 sp = BPF_CORE_READ(regs, sp);  // Stack pointer (RSP)
                
                for (int i = 0; i < MAX_STACK_DEPTH; i++) {
                    if (!fp || fp < sp || fp > sp + 0x100000) {
                        // Basic sanity check: fp should be above sp and within reasonable range
                        break;
                    }
                    
                    // Read the stack frame
                    __u64 next_fp, ret_addr;
                    if (bpf_copy_from_user_task(&next_fp, sizeof(next_fp), 
                                                (void *)fp, task, 0) < 0) {
                        break;
                    }
                    if (bpf_copy_from_user_task(&ret_addr, sizeof(ret_addr), 
                                                (void *)(fp + 8), task, 0) < 0) {
                        break;
                    }
                    
                    // Store the return address using loop index (verifier-safe)
                    if (i < MAX_STACK_DEPTH) {
                        event->ustack[i] = ret_addr;
                        event->ustack_sz = i + 1;
                    }
                    
                    // Move to next frame
                    fp = next_fp;
                }
            #elif defined(__TARGET_ARCH_arm64)
          			/* ARM64 架构省略 */ 
            #endif
        }
    }
    
    bpf_ringbuf_submit(event, 0);
    return 0;
}

6.2 用户空间核心代码

核心代码分析如下,如无兴趣可效果。

基于用户空间定时驱动采集的主循环逻辑如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  struct ring_buffer *rb = ring_buffer__new(bpf_map__fd(skel->maps.events),
                           handle_event, NULL, NULL); /*回调函数 handle_event() */

	while (running) {
        if (args.iterations > 0 && iteration >= args.iterations) {
            break;
        }

        struct timespec start_time;
        clock_gettime(CLOCK_MONOTONIC, &start_time);

        int iter_fd = bpf_iter_create(link_fd);
        int ret = read(iter_fd, buf, 1);

        while (ring_buffer__poll(rb, 0) > 0) {
            // Keep processing until no more events
        }

        close(iter_fd);

        // Calculate time spent and adjust sleep
        struct timespec end_time;
        clock_gettime(CLOCK_MONOTONIC, &end_time);

        long elapsed_ns = (end_time.tv_sec - start_time.tv_sec) * 1000000000L +
                         (end_time.tv_nsec - start_time.tv_nsec);

        long sleep_ns = interval_ns - elapsed_ns;
        if (sleep_ns > 0) {
            struct timespec sleep_time = {
                .tv_sec = sleep_ns / 1000000000L,
                .tv_nsec = sleep_ns % 1000000000L,
            };
            nanosleep(&sleep_time, NULL);
        }

        iteration++;
    }

数据核心处理函数 handle_event() 代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Ring buffer callback
static int handle_event(void *ctx, void *data, size_t data_sz)
{
    struct stack_event *e = data;
    struct timespec ts;
  
    clock_gettime(CLOCK_REALTIME, &ts);
    char timestamp[64];
    strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", localtime(&ts.tv_sec));
    snprintf(timestamp + strlen(timestamp), sizeof(timestamp) - strlen(timestamp),
             ".%06ld", ts.tv_nsec / 1000);

    // 调用 symbolize_stack 函数对地址符号化处理
    char *ksyms = symbolize_stack(e->kstack, e->kstack_sz, e->pid, true);
    char *usyms = symbolize_stack(e->ustack, e->ustack_sz, e->pid, false);

    // Print CSV values: timestamp,tid,tgid,comm,state,ustack,kstack
    printf("%s|%u|%u|%s|%s|%s|%s\n",
           timestamp,
           e->pid,
           e->tgid,
           e->comm,
           state_to_str(e->state),
           usyms ? usyms : "[no_ustack]",
           ksyms ? ksyms : "[no_kstack]"
        );

    if (ksyms) free(ksyms);
    if (usyms) free(usyms);

    return 0;
}

符号化处理 symbolize_stack() 的函数逻辑如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
static char *symbolize_stack(__u64 *addrs, int count, pid_t pid, bool is_kernel)
{
#ifdef USE_BLAZESYM
    if (!symbolizer || count <= 0)
        return strdup(is_kernel ? "" : "[no_ustack]");

    static char symbuf[65536];  // Large buffer for symbols
    symbuf[0] = '\0';

    const struct blaze_syms *syms = NULL;

    if (is_kernel) {
        struct blaze_symbolize_src_kernel src = {
            .type_size = sizeof(src),
        };
        syms = blaze_symbolize_kernel_abs_addrs(symbolizer, &src,
                                                (const uintptr_t *)addrs, count);
    } else {
        struct blaze_symbolize_src_process src = {
            .type_size = sizeof(src),
            .pid = pid,
        };
        syms = blaze_symbolize_process_abs_addrs(symbolizer, &src,
                                                 (const uintptr_t *)addrs, count);
    }

    if (!syms || syms->cnt == 0) {
        if (syms) blaze_syms_free(syms);
        return strdup(is_kernel ? "[no_ksymbols]" : "[no_usymbols]");
    }

    char *ptr = symbuf;
    size_t remaining = sizeof(symbuf) - 1;

    // Iterate in reverse order if requested
    if (reverse_stack) {
        for (int i = count - 1; i >= 0; i--) {
            if (i < count - 1) {
                if (remaining > 1) {
                    *ptr++ = ';';
                    remaining--;
                }
            }

            if (i < syms->cnt) {
                const struct blaze_sym *sym = &syms->syms[i];
                if (sym->name && sym->name[0]) {
                    int written = snprintf(ptr, remaining, "%s+0x%lx",
                                         sym->name, sym->offset);
                    if (written > 0 && written < remaining) {
                        ptr += written;
                        remaining -= written;
                    }
                } else {
                    int written = snprintf(ptr, remaining, "0x%llx", addrs[i]);
                    if (written > 0 && written < remaining) {
                        ptr += written;
                        remaining -= written;
                    }
                }
            } else {
                int written = snprintf(ptr, remaining, "0x%llx", addrs[i]);
                if (written > 0 && written < remaining) {
                    ptr += written;
                    remaining -= written;
                }
            }
        }
    } else { 
      /*正序*/
    }

    *ptr = '\0';
    blaze_syms_free(syms);
    return strdup(symbuf);
}