【SRE 实习面试】面试要点

本文档围绕你的 profiler-cpp 项目,从面试官视角梳理所有可能被问到的问题,涵盖 eBPF 性能采样、perf_event 子系统、栈回溯与符号化、C++23 工程实践、基准测试方法、系统设计等方向,每个问题附带详细答案。


1. 项目概览

Q1: 一句话介绍这个项目

答: Profiler 是一个基于 eBPF + perf_event 的 Linux 采样分析器——它将 eBPF 程序挂载到硬件/软件 perf 事件上,在内核态以可配置频率采集进程的 kernel+user 栈回溯,通过 ring buffer 推送到用户态,最终用 blazesym 库符号化为函数名并输出火焰图兼容格式。

Q2: 这个项目的架构是什么?数据流是怎样的?

答:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
内核态 (eBPF):
  perf_event 触发 (每秒freq次,每CPU独立)
    |
    v
  SEC("perf_event") int profile(void* ctx)
    ├─ bpf_get_current_pid_tgid() → 获取 TGID
    ├─ match_target() → 过滤 (tgid/pgrp/session/cgroup)
    ├─ bpf_ringbuf_reserve() → 分配事件缓冲区
    ├─ bpf_get_current_comm() → 进程名
    ├─ bpf_get_stack(ctx, kstack, ..., 0) → 内核栈
    ├─ bpf_get_stack(ctx, ustack, ..., BPF_F_USER_STACK)
    └─ bpf_ringbuf_submit() → 提交到 ring buffer
                          |
用户态 (C++23):            |   ring_buffer__poll()
                          v
  handle_event_wrapper() → EventHandler::handle()
    ├─ handle_standard() → 逐条输出 (时间戳+栈帧)
    └─ handle_fold_extend() → 聚合去重, 火焰图格式
         |
         v
  blaze::Symbolizer → blazesym C API
    └─ 地址 → 函数名 + 源文件位置 + 内联信息

关键设计点:

  • 每 CPU 一个 perf_event fd,BPF 程序在多 CPU 上并行执行,无锁
  • 内核态过滤match_target)避免无效事件浪费 ring buffer 空间
  • 用户态懒符号化——--no-symbolize 模式下只计数,用于低开销基准测试

Q3: 这个项目和 Linux 自带的 perf 工具有什么区别?

答:

维度linux perfProfiler
采样机制perf_event + perf buffer(共享内存 mmap)perf_event + BPF ring buffer
栈收集内核采样帧(frame pointer/ORC/DWARF)bpf_get_stack() 内核辅助函数
符号化perf script 离线后处理在线符号化(blazesym)或关闭符号化(RawCount)
过滤perf record -p/-tkernel 态实时过滤(tgid/pgrp/session/cgroup)
输出perf.data 二进制 + perf script/report直接输出标准/折叠文本格式
端到端 CPU 开销baseline较 perf record + perf script/report 低约 30%
纯采集 CPU 开销较低较 perf record 高(更多采集内容: comm + kernel+user stack)

我的基准测试结果:

  • collect-only(仅采集):profiler CPU 开销更高(约 -48%),因为 profiler 采集了更多数据(进程名、完整内核栈 + 用户栈)
  • end-to-end(采集 + 后处理):profiler 总开销更低(约 +30%),因为省去了 perf 的二次处理(perf script + perf report

2. eBPF 采样架构

Q4: 为什么选择 perf_event 类型的 BPF 程序,而不是 kprobe/tracepoint?

答:

BPF 程序类型触发源是否适合采样原因
SEC(“perf_event”)硬件/软件性能计数器溢出最合适基于频率/周期的采样,是 profiling 的标准方法
kprobe内核函数调用需要指定具体函数,不能做通用采样;频繁函数会产生大量事件
tracepoint内核静态跟踪点同上,不是周期性采样
XDP / TC网络包到达完全不同用途

perf_event BPF 程序的核心价值:

  1. 周期性触发——硬件 PMC(Performance Monitoring Counter)每次溢出触发一次 BPF 程序执行
  2. 统计性采样——不需要 hook 每个函数调用,低开销
  3. 与内核 perf 子系统深度集成——复用了 perf_event_open() 的过滤、多路复用等能力

Q5: eBPF 程序中如何读取内核数据结构(如 task_struct)?为什么不会崩溃?

答: 使用 BPF CO-RE(Compile Once, Run Everywhere)机制:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// profiler.bpf.c 中读取进程组 ID 的例子
static __always_inline int current_pid_nr(enum pid_type type) {
    struct task_struct* task = bpf_get_current_task_btf();
    struct signal_struct* signal = BPF_CORE_READ(task, signal);
    if (!signal) {
        return 0;
    }
    struct pid* pid = BPF_CORE_READ(signal, pids[type]);
    if (!pid) {
        return 0;
    }
    unsigned int level = BPF_CORE_READ(pid, level);
    int nr = 0;
    bpf_core_read(&nr, sizeof(nr), &pid->numbers[level].nr);
    return nr;
}

为什么安全(不崩溃):

  1. BTF (BPF Type Format): 内核编译时生成 BTF 信息,描述了所有内核结构体的字段偏移和类型
  2. BPF_CORE_READ 宏: 编译时记录偏移意图,加载时 verifier + libbpf 根据 BTF 动态修正偏移量
  3. Verifier 检查: 每个 BPF_CORE_READ 后都做了 NULL 检查(if (!signal) / if (!pid)
  4. bpf_core_read() 有错误处理: 即使是指针链中的最后一个节点,无法读取会返回错误而非崩溃

没有 CO-RE 的旧方式: 需要为每个内核版本重新编译 BPF 程序(因为结构体偏移在不同编译下可能不同)。CO-RE 解决了这个问题,编译一次,到处运行。

3. perf_event 子系统

Q6: perf_event_open() 系统调用做了什么?你的项目如何配置它?

答: perf_event_open() 是 Linux 性能计数器子系统的入口,核心逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// src/perf.cpp
perf_event_attr attr = {
    .type = sw_event ? PERF_TYPE_SOFTWARE : PERF_TYPE_HARDWARE,
    .size = sizeof(perf_event_attr),
    .config = sw_event ? PERF_COUNT_SW_CPU_CLOCK : PERF_COUNT_HW_CPU_CYCLES,
    .sample_freq = freq,  // 每秒采样频次
    .freq = 1,            // 使用频率模式(非周期模式)
};

for (int cpu = 0; cpu < cpus; ++cpu) {
    int fd = perf_event_open(&attr, pid, cpu, -1, 0);
    fds.push_back(fd);
}

关键参数解析:

参数硬件事件软件事件
.typePERF_TYPE_HARDWAREPERF_TYPE_SOFTWARE
.configPERF_COUNT_HW_CPU_CYCLESPERF_COUNT_SW_CPU_CLOCK
触发机制CPU 周期计数器溢出(精确,但有 skid)高精度定时器中断
适用场景现代 x86/ARM CPU,需要真实 PMC虚拟机、不支持硬件 PMU 的环境
开销极低(硬件计数器,几乎无开销)略高(软件中断)

为什么每 CPU 打开一个 fd?

  • perf 事件的计数和采样是 per-CPU 的物理特性——CPU0 的周期计数器只计数 CPU0 上执行的事件
  • 每个 fd 绑定到 [pid, cpu],表示 " 监视某个 pid 在某个 CPU 上的 perf 事件 "
  • 如果 pid=-1,表示监听所有进程(system-wide)

Q7: sample_freqperiod 有什么区别?频率采样是怎样工作的?

答:

  • period(周期模式): 每 N 个事件触发一次采样。例如 period=1000000 表示每 100 万个 CPU 周期采样一次——采样频率随 CPU 频率变化
  • freq(频率模式): 内核动态调整 period 使采样频率稳定在指定值。.freq=1 + .sample_freq=99 → 每秒约 99 次采样

频率模式的内部机制:

1
2
3
内核每秒重新计算 period 值:
  period = (last_period * avg_sample_rate + allowed_err) / target_freq
这样系统可以对抗 CPU 频率变化(DVFS/P-state),维持稳定的采样频率。

我们为什么使用 freq 模式?

  1. 用户界面友好——" 每秒采样 99 次 " 比 " 每 1000 万周期采样一次 " 更直观
  2. 可比较性——不同 CPU 节点间采样频率一致,基准测试有意义
  3. 稳定性——不因 CPU 降频/P-state 变化而波动

Q8: 硬件事件和软件事件在采样中有何不同?什么时候应该用软件事件?

答:

特性硬件事件 (HW_CPU_CYCLES)软件事件 (SW_CPU_CLOCK)
触发源CPU PMC(Performance Monitoring Counter)内核高精度定时器 (hrtimer)
精确度采样存在 skid(从溢出到 PC 记录有几条指令延迟)定时器中断精度(微秒级)
计数含义CPU 实际执行的周期数(排除 sleep/stolen)墙钟时间(包括被 swap out 的时间)
虚拟化支持需要 vPMU(VM 中可能不可用)始终可用
idle 时采样不在 idle 时采样会采样到 idle 进程

何时使用软件事件(项目中的 --sw-event):

  1. 虚拟化环境——VM 没有暴露 vPMU(如某些云主机)
  2. 异构 CPU——大小核架构(Intel P-core/E-core)中,硬件事件在不同核上行为差异大
  3. 调试/兼容性——硬件 PMU 某些情况下不可用
  4. 明确测量 CPU 时间而非 CPU 周期——软事件测量的是 " 占用 CPU 的时间比例 "

4. 栈回溯与收集

Q9: bpf_get_stack() 是如何工作的?内核栈和用户栈有什么区别?

答: bpf_get_stack() 是 eBPF 辅助函数,在 BPF 程序上下文中获取当前进程的调用栈。

1
2
3
4
// 内核栈 (flag=0)
event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0);
// 用户栈 (flag=BPF_F_USER_STACK)
event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK);

内核栈回溯:

  • 直接遍历内核栈帧链表(通过 frame pointer 或 ORC unwinder)
  • flag=0 表示内核栈
  • 返回写入的字节数(不是帧数)——stack_sz / sizeof(__u64) = 帧数
  • 内核态执行的代码,符号化时 PID=0(使用 kallsyms/System.map)

用户栈回溯:

  • BPF_F_USER_STACK 标志指示读取用户空间栈
  • 读取的是用户空间的栈内存地址
  • 符号化时使用目标进程的 PID(通过 /proc/pid/maps + ELF 调试信息)
  • 结果与 frame pointer/DWARF unwind 信息有关

栈深度限制:

1
#define MAX_STACK_DEPTH 128   // 最多 128 层栈帧

bpf_get_stack() 的实际返回值可能是负数(special codes),项目中使用 stack_sz > 0 判断有效性。

Q10: 为什么 bpf_get_stack() 有时返回 -1(或很小)的用户栈?怎么解决?

答:

原因:

  1. 没有 frame pointer(-fomit-frame-pointer): 默认情况下,现代编译器(gcc -O2, clang)会省略 frame pointer 以提升性能,导致栈无法回溯
  2. 尾调用优化: 编译器将函数调用替换为 jmp,导致栈帧消失
  3. JIT 代码: Java/JIT/解释器生成的代码没有 frame pointer 交织
  4. syscall 入口无用户栈帧: 在系统调用入口处,用户栈寄存器尚未保存

解决方案:

  1. 编译时保留 frame pointer: CFLAGS="-fno-omit-frame-pointer" 重新编译目标程序
  2. 使用 ORC unwinder(内核 5.12+): 内核 ORC(Oops Rewind Capability)数据可以替代 frame pointer 进行栈回溯
  3. DWARF unwind: 更精确但开销更高,需要 userspace 完成
  4. 告知用户: 项目 README 中应说明被 profile 的程序需要保留 frame pointer

本项目的现状: bpf_get_stack() 依赖内核的栈回溯能力,如果被采样进程编译时省略了 frame pointer,用户栈帧将缺失。这是一个已知限制

Q11: 什么是采样偏差(Skid)?对你的项目有什么影响?

答:

Skid(滑动): 从性能计数器溢出(触发中断)到程序计数器(PC)被记录之间,已经执行了额外的指令,导致采样的 PC 指向的不是真正触发溢出的指令。

1
2
3
真实情况:
  CPU 执行到指令 X → PMC 溢出触发中断 → 中断延迟 → 记录 PC 时指向 X+N
  中间的 N 条指令 = skid

对 profiling 的影响:

  • 热点函数识别通常不精确到具体指令,skid 影响有限
  • 但会使函数级别的统计出现偏差——特别是短函数可能被错误归因到邻近函数
  • 硬件 PEBS(Precise Event-Based Sampling)可以消除或减少 skid

本项目中:

  • 使用了简单的硬件事件(PERF_COUNT_HW_CPU_CYCLES)而非 PEBS
  • 对宏观性能分析(找出哪个函数花时间最多)来说,skid 的统计误差可接受
  • 面试官追问 " 如何改进?"——回答:perf_event_attr 中设置 .precise_ip 可以启用 PEBS

5. BPF CO-RE 与 BTF

Q12: 什么是 BTF?为什么它对 eBPF 程序很重要?

答:

BTF (BPF Type Format): 内核编译时生成的元数据格式,描述了内核中所有类型(结构体、联合体、枚举、typedef)的布局,包括每个字段的名称、类型、偏移、大小。

在 Linux 中的位置: /sys/kernel/btf/vmlinux(一个 ELF 格式文件,包含 .BTF section)

两种用法:

  1. vmlinux.h(编译时):
1
2
# cmake/FindBpfObject.cmake 中生成
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

将所有内核类型展开为 C 头文件,BPF 程序可以直接 include 使用。

  1. CO-RE 重定位(加载时):
1
BPF_CORE_READ(task, signal, pids[type], numbers[level].nr);

libbpf 在加载 eBPF 程序时,根据当前运行内核的 BTF 修正结构体字段偏移——这就是 " 一次编译,到处运行 " 的秘诀。

为什么重要:

  • 内核内部结构体在不同发行版/版本间布局可能不同
  • 没有 CO-RE 的话,需要为每个内核版本编译独立的 eBPF 程序
  • 没有 BTF 的话,BPF_CORE_READ 无法确定字段偏移

Q13: BPF_CORE_READ 宏是如何实现安全的指针链遍历的?

答: BPF_CORE_READ 展开后实际做了:

1
2
3
4
// BPF_CORE_READ(task, signal, pids[type], numbers[level].nr)
// 展开为多个 bpf_core_read 调用,每个调用会用 bpf_probe_read_kernel()
// 在内核地址空间做 safe memory access,偏移由 BTF 修正
// 每层都有 NULL 检查,避免 crash

Verifier 安全保证:

  1. 每次指针解引用前做 NULL 检查
  2. 所有内存访问都有边界检查
  3. 使用 bpf_probe_read_kernel() 代替直接内存解引用(可处理 page fault)

6. 符号化与 blazesym

Q14: 什么是符号化(Symbolization)?为什么不能直接用地址?

答: 符号化(Address-to-Symbol Resolution)将栈回溯中的内存地址转换为人类可读的函数名 + 代码位置

1
2
3
符号化前:                      符号化后:
  0xffffffff81234567             do_syscall_64+0x27 (arch/x86/entry/common.c:79)
  0x7f8a3c0010a0                main+0x50 (src/main.cpp:34)

为什么不能直接用地址:

  • ASLR(地址空间布局随机化)导致每次运行地址不同
  • 不同进程的同一函数地址不同
  • 人类理解需要函数名而非十六进制

blazesym 如何获取符号信息:

来源内核符号用户空间符号
数据源/proc/kallsyms 或 kallsyms 内建/proc/pid/maps + ELF 文件
符号信息函数名 + 偏移函数名 + 源文件: 行号: 列号 + 内联函数
APIblaze_symbolize_kernel_abs_addrs()blaze_symbolize_process_abs_addrs()

项目中 PID=0 表示内核符号源:

1
2
3
4
5
6
7
// src/blaze.h
inline auto get_symbolize_source(uint32_t pid) -> blaze::Source {
    if (pid == 0) {
        return blaze::Source{blaze_symbolize_src_kernel{...}};
    }
    return blaze::Source{blaze_symbolize_src_process{.pid = pid, ...}};
}

Q15: 为什么选择 blazesym 而不是其他符号化库?

答:

方案优点缺点
blazesym (本项目)Rust 实现高性能、支持 DWARF inline、活跃维护、C API 易集成构建依赖 Rust 工具链 + Corrosion
addr2line (binutils)随系统自带每个地址 fork 进程,慢
libunwind完整 unwinding 栈C 库,无内联函数支持
libdw (elfutils)支持 DWARF 全部特性API 复杂,体积大

blazesym 的核心优势:

  1. 批量符号化: 一次调用符号化整个栈数组,减少 IPC 开销
  2. 内联函数支持: 源码中 inlined_cnt + inlined[] 字段揭示函数是否被编译器内联
  3. 性能: Rust 实现,无 GC,零抽象开销
  4. 代码信息: 提供源文件路径、行号、列号

Q16: blazesym(Rust)是如何集成到 C++ 项目中的?

答: 使用 Corrosion(Rust-CMake 桥接工具):

1
2
3
4
5
# cmake/SetupBlazesym.cmake
FetchContent_Declare(corrosion ...)
corrosion_import_crate(MANIFEST_PATH third_party/blazesym/Cargo.toml)
# 最终生成静态库 libblazesym_c.a
add_library(blazesym STATIC IMPORTED)

完整链接链路:

1
2
3
4
5
Rust (blazesym/capi) → cargo build → libblazesym_c.a (C ABI)
C++ (src/blaze.h) → 包装 C API → blaze::Symbolizer → event.cpp
C++ (profiler) → linkage: blazesym + profiler_skel + CLI11 + spdlog

构建前提:

  1. cargo build --manifest-path third_party/blazesym/Cargo.toml(需要 Rust 工具链)
  2. 生成的 C 头文件:third_party/blazesym/capi/include/blazesym.h
  3. 生成的静态库:libblazesym_c.a

面试小加分点: 你也熟悉 Rust 生态工具的集成,说明你有跨语言工程实践能力。

7. 过滤模式设计

Q17: 四种过滤模式(tgid/pgrp/session/cgroup)的区别和使用场景?

答:

过滤模式匹配的是什么典型用途
TGID(线程组 ID)单个进程(传统 PID)Profile 单个已知的进程
PGRP(进程组 ID)属于同一进程组的所有进程make -j16 这种 fork 多子进程的场景,shell pipeline
Session(会话 ID)同一会话的所有进程(默认)bash 启动的工作负载,覆盖其所有子进程和后台进程
Cgroup(控制组 ID)同一 cgroup v2 的所有进程容器中所有进程(如 Docker container 的 cgroup)

默认使用 Session 的原因:

基准测试脚本 benchmark.py 使用 bash -lc exec cmd 启动负载——它在新的 session 中运行,profiler 用 session 模式可以自动 profile 整个工作负载进程树。

Q18: kernel 态如何实现进程组/会话过滤?(核心实现细节)

答: 通过 BTF 直接访问 task_struct 内核结构体:

1
2
3
4
5
6
7
8
9
static __always_inline int current_pid_nr(enum pid_type type) {
    struct task_struct* task = bpf_get_current_task_btf();
    struct signal_struct* signal = BPF_CORE_READ(task, signal);
    struct pid* pid = BPF_CORE_READ(signal, pids[type]);
    unsigned int level = BPF_CORE_READ(pid, level);
    int nr = 0;
    bpf_core_read(&nr, sizeof(nr), &pid->numbers[level].nr);
    return nr;
}

数据结构穿透路径:

1
2
3
task_struct → signal_struct (包含所有 pid 类型)
    └→ pids[type] → struct pid
        └→ numbers[level].nr  ← 实际进程组/会话 ID

面试官可能追问:" 为什么不直接用 getpgid() 系统调用?"

答:在内核态 BPF 中没有系统调用的概念——你无法 call syscall,只能通过 BTF 直接读取内核数据结构。而且直接读取远比系统调用快(没有上下文切换)。

Q19: cgroup 过滤模式下,用户态如何解析 cgroup ID?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/common.h - cgroup_path_for_pid()
auto cgroup_path_for_pid(pid_t pid) -> std::string {
    // 读取 /proc/pid/cgroup,找到 cgroup v2 路径(格式 "0::/path")
    while (std::getline(file, line)) {
        auto pos = line.find("::");  // cgroup v2 格式标记
        if (pos != std::string::npos) {
            return "/sys/fs/cgroup" + line.substr(pos + 2);
        }
    }
}

// cgroup_id_for_pid() - 通过 stat() 获取 inode 作为唯一标识
auto cgroup_id_for_pid(pid_t pid) -> uint64_t {
    struct stat st{};
    stat(cgroup_path_for_pid(pid).c_str(), &st);
    return st.st_ino;  // cgroup 目录的 inode 就是 cgroup ID
}

而在内核态: 直接使用 bpf_get_current_cgroup_id() 辅助函数。两端需要匹配同一个 cgroup 标识。

8. Ring Buffer 原理

Q20: BPF Ring Buffer 和传统 Perf Buffer 有什么区别?为什么选择它?

答: 项目使用 BPF_MAP_TYPE_RINGBUF(内核 5.8+):

1
2
3
4
5
// bpf/profiler.bpf.c
struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);  // 256KB 共享缓冲区
} events SEC(".maps");
特性BPF Ring BufferPerf Buffer (BPF_MAP_TYPE_PERF_EVENT_ARRAY)
数据结构单共享环形缓冲区(multi-producer, single-consumer)每 CPU 独立 ring buffer
内存开销固定大小(256KB)每 CPU 拷贝一份,内存 = 256KB × Ncpu
事件顺序全局顺序(写入时按 timestamp 排序)每 CPU 内部有序,跨 CPU 无序
APIringbuf_reserve/ringbuf_submitperf_event_output
动态大小消息原生支持(reserve 时指定大小)需要固定消息大小
丢数据检测可通过返回值检测并处理静默丢弃

为什么选择 Ring Buffer:

  1. 内存效率: 64 核机器上,256KB vs 16MB
  2. 全局顺序: 按时间排序有利于火焰图构建
  3. 显式丢数据检测: bpf_ringbuf_reserve 返回 NULL 时可以记录 lost events

Q21: ring buffer 的 poll 模型是怎样的?为什么用 100ms 超时?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/main.cpp - 主循环
while (!exiting) {
    int err = rb.poll(100);  // 最多阻塞 100ms
    if (err == -EINTR) {
        continue;  // 被信号中断,检查退出标志
    }
    if (err < 0) {
        break;     // 其他错误,退出
    }
}
  • ring_buffer__poll(100) 调用 epoll_wait() 等待事件,超时 100ms
  • 超时返回后重新检查 exiting 标志——信号响应延迟 ≤ 100ms
  • 事件处理在 poll 返回时完成(回调函数中同步符号化)
  • 设计权衡: 100ms 超时使得 Ctrl+C 响应较慢但 CPU 空转不过度

9. 输出格式与火焰图

Q22: 两种输出格式(Standard vs FoldExtend)的区别和设计意图?

答:

Standard 格式: 逐条输出每个采样事件

1
2
3
4
5
[1714567890.123456789 COMM: stress-ng-matri (pid=12345) @ CPU 3]
Kernel:
  0xffffffff81234567: do_syscall_64 @ 0xffffffff81234500 + 0x67 (arch/x86/entry/common.c:79)
Userspace:
  0x7f8a3c0010a0: main @ 0x7f8a3c000000 + 0x10a0 (src/main.cpp:34)
  • 用于单条分析——查看某次采样的完整调用栈
  • 时间戳 + CPU 号 → 可以分析 " 某个 CPU 上发生了什么 "
  • 包含 inline 函数展开 + 源文件行列信息

FoldExtend 格式(火焰图兼容): 等符号化完整个会话后,flush 时输出

1
stress-ng-matri-12345;main;intensive_calculation;matrix_multiply_[k];handle_mm_fault_[k] 42
  • 用于统计分析——" 哪些函数组合占了最多 CPU 时间 "
  • 分号分隔的栈帧(栈底到栈顶),末尾计数
  • _[k] 后缀标识内核栈帧
  • 管道到 FlameGraph 脚本生成 SVG 火焰图

FoldExtend 的去重优化:

1
2
3
4
5
6
7
// event.cpp - 使用 raw bytes 作为 key 判断是否见过相同栈
auto key = folded_key(event);  // 拼接 pid+comm+kstack+ustack 的二进制
auto cached = folded_stacks_.find(key);
if (cached != folded_stacks_.end()) {
    cached->second.count++;  // 去重,只增加计数
    return;                  // 跳过后面的符号化!
}
  • 相同栈只符号化一次,后续直接计数——大幅减少 CPU 开销
  • 等 flush 时统一输出

Q23: 火焰图是什么?怎么从你的输出生成火焰图?

答:火焰图(FlameGraph) 是 Brendan Gregg 提出的性能可视化方法。

生成流程:

1
2
3
4
5
# 1. 用 profiler 采集折叠格式数据
sudo ./build/Release/profiler -f 99 -E -p 12345 > output.folded

# 2. 使用 FlameGraph 脚本生成 SVG
./third_party/FlameGraph/flamegraph.pl output.folded > flamegraph.svg

火焰图的阅读规则:

  • X 轴宽度 = 函数占总采样数的比例(越宽 = 越热点)
  • Y 轴 = 调用栈深度(越往上 = 越深)
  • 颜色随机(无特殊含义),由 flamegraph.pl 控制

10. C++23 与工程实践

Q24: 项目中用了哪些 C++23 特性?为什么选择这些特性?

答:

C++23 特性使用位置用途
std::expected<T,E>utils.h 全局Rust 风格的 Result 错误处理
std::format / std::printlnevent.cpp类型安全的格式化输出
std::ranges / std::viewsevent.cpp管道式数据转换(join_with / to<string>
auto 返回类型推导全项目现代 C++ 风格
[[nodiscard]]event.h, common.h编译时警告未使用的返回值

std::expected(Rust Result 模式的 C++ 版):

1
2
3
4
5
6
7
8
9
// 成功时返回 Result<T>
auto init_perf_monitor(...) -> Result<std::vector<int>, libbpf_errno>;
// 使用时
auto perf_fds = init_perf_monitor(freq, args.sw_event, perf_pid);
if (!perf_fds) {
    spdlog::error("Failed to initialize perf monitor");
    return 1;
}
// 成功路径: perf_fds.value()

为什么不用异常?

  • BPF/perf 操作失败是预期内行为(权限不足、内核不支持等),不应算 " 异常 "
  • std::expected 强迫调用者处理错误,避免忽略
  • 性能——exception unwind 在热路径中开销大

Q25: RAII(Resource Acquisition Is Initialization)在项目中的体现?

答: 项目使用多个 RAII 包装器管理内核资源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ProfilerSkel - 管理 BPF 对象生命周期
struct ProfilerSkel {
    profiler_bpf* obj{nullptr};
    ProfilerSkel(...) { obj = profiler_bpf::open(); profiler_bpf::load(obj); }
    ~ProfilerSkel() { if (obj) profiler_bpf__destroy(obj); }
};

// RingBuffer - 管理 ring buffer 生命周期
struct RingBuffer {
    ring_buffer* rb;
    RingBuffer(int map_fd, ...) { rb = ring_buffer__new(map_fd, ...); }
    ~RingBuffer() { if (rb) ring_buffer__free(rb); }
};

// blaze::Syms - 管理符号化结果
struct Syms {
    const blaze_syms* syms_;
    ~Syms() { if (syms_) blaze_syms_free(syms_); }
    Syms(const Syms&) = delete;  // 禁止拷贝
};

为什么重要:

  • 即使在异常/早期返回/信号处理路径中,资源也能被正确释放
  • BPF 程序不释放会导致内核内存泄漏和 zombie BPF 程序

Q26: std::variant + Overloaded 的 visitor 模式用在哪?

答: 用于区分内核符号化和进程符号化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/blaze.h
using Source = std::variant<blaze_symbolize_src_process, blaze_symbolize_src_kernel>;

auto symbolize(Source src, const Input& input) const -> Result<Syms> {
    const blaze_syms* syms = nullptr;
    std::visit(
        Overloaded{
            [&](blaze_symbolize_src_kernel& kern_src) -> void {
                syms = blaze_symbolize_kernel_abs_addrs(symbolizer_, &kern_src, input.addrs_, input.cnt_);
            },
            [&](blaze_symbolize_src_process& proc_src) -> void {
                syms = blaze_symbolize_process_abs_addrs(symbolizer_, &proc_src, input.addrs_, input.cnt_);
            },
        },
        src);
    return syms ? Result<Syms>{syms} : Err<>(...);
}

巧妙之处: Overloaded 结构体通过多重继承把多个 lambda 的 operator() 合并在一起,实现了编译期类型派发,无虚函数开销。

11. 构建系统

Q27: 项目的构建流程是怎样的?(CMake + Conan + Corrosion + bpftool)

答:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
conan install . -s build_type=Release    <- 下载 CLI11 + spdlog 到本地
cmake --preset release                   <- 配置 CMake (gcc + C++23)
    |- FindBpfObject.cmake               <- 找到 bpftool + clang + libbpf
    |    |- bpftool btf dump -> vmlinux.h <- 生成内核类型头文件
    |    |- 提供 bpf_object()    |- SetupBlazesym.cmake               <- Fetch Corrosion -> cargo build blazesym
    |- CMakeLists.txt                    <- 定义目标
        |- bpf_object(profiler bpf/profiler.bpf.c)  <- 编译 eBPF + 生成 skeleton
        |- add_executable(profiler src/main.cpp ...) <- 编译 C++ 可执行文件

cmake --build build/Release --target _cargo-build_blazesym_c  <- 构建 Rust 库
cmake --build build/Release --target profiler                 <- 构建 profiler

bpf_object 宏做了什么:

1
2
3
4
# cmake/FindBpfObject.cmake
1. clang -target bpf -O2 -g -D__TARGET_ARCH_x86 -c bpf/profiler.bpf.c -o profiler.bpf.o
2. bpftool gen skeleton profiler.bpf.o > profiler.skel.h   <- 生成骨架 C 头文件
3. 创建 INTERFACE  profiler_skel,链接 libbpf + libelf + libz

Q28: 为什么要生成 vmlinux.h?为什么不用 kernel headers?

答:

vmlinux.h: 通过 bpftool btf dump file /sys/kernel/btf/vmlinux format c 从运行内核的 BTF 信息自动生成的单个头文件。

对比 kernel headers:

维度vmlinux.h (BTF 生成)kernel headers (linux/*.h)
来源当前运行内核的 BTF 元数据编译时指定的 kernel-devel 包
一致性100% 与运行内核一致可能版本不匹配
大小单文件,约 1MB+数百个头文件
CO-RE 支持v 天然支持x 需要手动处理

核心原因: BPF CO-RE 依赖运行内核的 BTF 进行字段重定位。vmlinux.h 确保编译时类型定义和运行时 BTF完全一致

Q29: 为什么用 Conan 管理 C++ 依赖而不是 vcpkg 或 FetchContent?

答:

方案优点缺点
Conan (本项目)CMake-presets 深度集成,二进制包缓存需要安装 conan
vcpkg微软维护,Windows/Linux 都好用CMake 集成稍弱
FetchContent无需外部包管理每次都从源码编译

Conan 适合本项目因为 CLI11 + spdlog 都是纯头文件/小库,Conan 能快速拉取预编译版本。

Q30: build/ 目录下的编译产物有哪些关键文件?

答:

  • build/vmlinux.h —— 由 bpftool 生成的完整内核类型定义
  • build/profiler.bpf.o —— eBPF 字节码目标文件(clang -target bpf 编译产物)
  • build/profiler.skel.h —— bpftool gen skeleton 生成的一体化头文件,包含加载/attach/Map 访问的 C++ 包装
  • build/profiler —— 最终可执行文件
  • build/compile_commands.json —— clangd/LSP 用的编译数据库

12. 基准测试方法论

Q31: 你的基准测试为什么分 collect-only 和 end-to-end 两组?

答: 因为两组回答不同的问题:

collect-only(纯采集开销):

1
2
3
profiler --no-symbolize -f 99   vs   perf record -F 99
-> 比较 BPF ring buffer + eBPF 程序开销 vs perf buffer 开销
-> 纯数据面开销对比

end-to-end(端到端开销):

1
2
3
profiler -E -f 99              vs   perf record + perf script + perf report
-> 比较整个分析工作流的总开销
-> 包含符号化 / 输出格式化

为什么 profiler 纯采集开销更高?

  • 采集了更多数据:comm(进程名)、完整的 kernel + user stack(perf 可能压缩栈)
  • BPF ring buffer 的 reserve/submit 机制有锁开销
  • 但 end-to-end 更低——因为省去了 perf 的后处理步骤

Q32: benchmark.py 的多轮统计是怎么做的?

答:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 每轮:
# 1. start_workload -> run_profiler -> stop_workload
# 2. start_workload -> run_perf -> stop_workload
# 对每种工具采集: user_sec, sys_sec, elapsed_sec, cpu_pct, max_rss_kb, samples

# 多轮聚合:
mean = sum(values) / len(values)
std  = sqrt(sum(x^2)/n - mean^2)
p95  = sorted(values)[ceil(0.95 * n) - 1]

# 最终输出:
cpu_overhead_improvement_vs_perf(%) = (perf_cpu_mean - profiler_cpu_mean) / perf_cpu_mean * 100

关键设计选择:

  1. 两轮使用相同 workload 但分别启动——避免 profiling 互相干扰
  2. 同等条件下对比——相同的采样频率、时长、过滤参数
  3. 多轮消除偶然因素——均值/std/p95 有统计意义
  4. 可复现性——所有参数记录到 benchmark_config.json

Q33: 基准测试中样本数(samples)不一致怎么办?能比较吗?

答: 样本数差异是正常的,原因:

  1. 硬件事件采样数依赖实际 CPU 周期消耗——perf record 和 profiler 不可能精确收集相同数量的样本(同一 workload 但两次运行有波动)
  2. 软件事件采样数更稳定,基于墙钟时间
  3. benchmark.py 的对比不是比较样本数,而是比较 CPU 时间(user_sec + sys_sec)——这才是真正的开销

统计手段: 多轮运行的 mean/std/p95 会告诉我们差异是否显著。

13. 与 Linux perf 工具对比

Q34: perf record 的整个工作流程是怎样的?和你的项目有何异同?

答:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
perf record 工作流程:
  perf record -F 99 -g -p <pid>
    |- perf_event_open() x 每CPU     <- 与你相同
    |- 内核采样: 记录 PC + callchain  <- 内核完成
    |- mmap 的 perf buffer -> 写入 perf.data    <- 与你不同 (perf buffer vs ring buffer)
    |- 采集完成后,perf.data 是二进制格式

  perf script:
    |- 读取 perf.data -> 后处理 -> 符号化输出

  perf report:
    |- 读取 perf.data -> 统计聚合 -> 交互式 TUI

关键差异:

阶段perfProfiler
栈回溯内核采样帧(有 skid,但有 PEBS 精确模式)bpf_get_stack()(BPF 辅助函数)
事件缓冲区Perf buffer(每 CPU 独立 mmap)BPF ring buffer(共享 256KB)
数据格式二进制 perf.data直接文本输出
符号化时机后处理(perf script在线(或关闭)
过滤能力pid 过滤tgid/pgrp/session/cgroup 灵活过滤
典型用途通用性能分析轻量级、可定制分析

Q35: 为什么 perf 工具仍然更常用?你的项目有什么独特价值?

答:

perf 的优势:

  1. 随内核发布——零依赖,每台 Linux 都有
  2. 功能全面——除了采样还有 stat/top/annotate/ftrace 等
  3. PEBS 精确事件——消除 skid
  4. 丰富的后处理——perf report TUI、perf diff 比较、perf annotate 汇编

我的项目的独特价值:

  1. 学习价值——深入理解 eBPF + perf_event + 符号化的完整链路
  2. 定制化——filter 比 perf 更灵活(特别是 session/cgroup)
  3. 在线符号化——采样结束即刻有结果,无需后处理
  4. 低端到端开销——对于只关心火焰图的场景比 perf 更快
  5. 火焰图原生支持——FoldExtend 格式直出

Q36: perf_event 的 mmap ring buffer 和 BPF ring buffer 在实现层面的根本差异?

答:

Perf buffer (mmap):

1
2
3
内核写入: 通过 perf_output_begin/end 写到 mmap 共享内存页
用户态读取: 通过 poll/epoll 得知有数据,然后直接读 mmap 区域(零拷贝)
问题: 每 CPU 一个独立的 mmap 环形缓冲区 -> Ncpu 个 buffer

BPF ring buffer:

1
2
3
内核写入: bpf_ringbuf_reserve 分配记录 -> 填数据 -> bpf_ringbuf_submit 提交
用户态读取: ring_buffer__poll -> epoll -> 回调函数
特点: 单个共享 buffer,按时间戳全局有序

核心差异:BPF ring buffer 提供多生产者单消费者(multi-producer single-consumer)的原子性保证,不需要 per-CPU 隔离。

14. SRE 发散题

Q37: 如果要部署到生产环境做持续 profiling,需要考虑什么?

答: 从 SRE 视角:

1. 资源开销控制:

  • 采样频率:生产环境通常 10-49Hz(而非默认 99Hz),项目默认 10Hz 是合理的
  • CPU 预算:约 1-3% 单核开销(需根据实际测量确认)
  • 内存限制:RLIMIT_MEMLOCK 设为 infinity 不合适——生产环境应该根据 Map 大小计算精确值(ring buffer 256KB + BPF 程序本身约 50KB = 约 300KB)
  • BPF 指令数限制:程序只有 137 行,远在 1M 指令限制之内

2. 数据持久化与聚合:

  • 当前所有数据存在进程内存中,需要添加定时 flush 或 Pushgateway 支持
  • 生产环境需要将折叠数据推送到 Prometheus / Loki 而非 stdout
  • 考虑采样率自适应——高负载时自动降低频率

3. 安全:

  • 需要 CAP_BPF + CAP_PERFMON(内核 5.8+ 的细粒度权限),不应以 root 运行
  • Perf 事件可能泄露其他进程的敏感信息——需要 namespace 隔离
  • /proc/kallsyms 读取限制(kernel.kptr_restrict=1 时非 root 无法读取内核符号)

4. 稳定性:

  • eBPF 程序 crash → perf 事件探测器会返回错误,用户态应处理
  • Ring buffer 满 → bpf_ringbuf_reserve 返回 NULL → 只能丢事件,应暴露 lost_events 指标
  • 长时间运行下的内存泄漏:RingBuffer, EventHandler, Symbolizer 应定期重置
  • 符号化可能非常慢(大 ELF/DWARF),需要考虑反压——丢事件 vs 阻塞

5. 可观测性:

  • 当前只输出 sample_count(--no-symbolize 模式)
  • 应增加:lost_events 计数、ring buffer 水位、符号化延迟 P50/P99

Q38: 如果采样频率调到 1000Hz,系统会出现什么问题?

答:

1. Perf 事件风暴:

  • 每秒 1000 次 x Ncpu 个事件 = 大量中断
  • 每个事件触发 BPF 程序执行(约 1-5us),可能导致中断风暴
  • 在 64 核机器上:64 x 1000 x 2us = 128ms 的 CPU 时间每秒被 profiler 吃掉(~13%)

2. Ring buffer 溢出:

  • 256KB 的 ring buffer 在高频下可能瞬间填满
  • 触发大量 bpf_ringbuf_reserve 返回 NULL(丢事件)
  • StacktraceEvent 大小 = 4 + 4 + 8 + 16 + 4 + 4 + 1024 + 1024 = 2088 字节
  • 256KB / 2KB ≈ 128 个事件就满了 —— 远不够 1000Hz x 64 CPU

3. 符号化线程跟不上:

  • 用户态 poll 回来 128 个事件,每个都需要符号化(比较慢)
  • 形成反压——内核在填,用户态在慢速消费

4. 替代方案:

  • 超大 ring buffer(如 64MB)
  • 只采样、不符号化(--no-symbolize
  • 或采样后 dump 原始数据、离线符号化

Q39: 你的 profiler 应该暴露哪些 SLI(Service Level Indicators)?

答:

SLI指标用途
采样覆盖率samples_received / expected_samples判断 ring buffer 是否溢出
符号化成功率symbolized_stacks / total_stacksblazesym 健康检查
采样延迟ring buffer 从写入到 poll 取出的延迟数据新鲜度
CPU 自开销profiler 自身消耗的 CPU 秒数是否影响业务
内存使用RSS / BPF Map 使用量内存泄漏检测
错误率perf_event_open 失败次数系统兼容性

Q40: 如果用这个工具做分布式 profiling(多节点),需要怎么扩展?

答:

  1. 时间对齐: 多节点间需要精确时间同步(PTP/NTP),否则合并火焰图时时间轴不对齐
  2. 中央聚合: 每节点 push folded 数据到 Kafka/ClickHouse → 中央查询/渲染
  3. 按节点 key 区分: FoldExtend 格式中增加 hostname/node_id —— node1;comm-pid;func1;func2 count
  4. 采样策略协调: 避免所有节点同时高频采样冲击网络
  5. 去重策略: 如果集群中同构节点很多,按节点去重和跨节点去重的粒度需要权衡

15. 陷阱题

陷阱题 1:" 你的 profiler 采样时,如果目标进程正在做系统调用,采到的是内核栈还是用户栈?"

答: bpf_get_stack() 根据 flag 不同可以同时获取两者。我的项目同时获取了内核栈(flag=0)和用户栈(flag=BPF_F_USER_STACK)。

  • 系统调用路径中:内核栈会显示 syscall_entry -> do_syscall_64 -> 具体的syscall -> ...
  • 用户栈会显示 syscall 之前的用户调用链

但存在一种情况: 如果进程正在内核空间执行(如内核线程或纯内核路径),bpf_get_stack(ctx, ustack, ..., BPF_F_USER_STACK) 可能返回空(因为当前上下文不在用户空间)。

陷阱题 2:" 如果被 profiled 程序的 ELF 被删除了(如滚动升级),符号化还能工作吗?"

答:

  • 进程仍在运行: ELF 文件的 inode 仍在内核的 page cache 中,/proc/pid/maps 仍然有效
  • blazesym 通过 /proc/pid/maps 找到每个内存映射对应的 ELF 文件路径,再从中读取符号表
  • 但如果磁盘上的 ELF 文件真的被删除了(而非被替换),blazesym 将无法打开文件,符号化会失败
  • 解决方案: blazesym 支持读取 /proc/pid/mem(进程内存)获取符号,但这更复杂

陷阱题 3:" 如果采样频率设置得很高,但目标进程在采样瞬间被调度出去了,会怎样?"

答:

  • perf_event 的 pid 参数决定监控哪个进程
  • 如果目标进程不在 CPU 上运行,perf 事件不会触发(硬件周期计数器只计在 CPU 上执行的周期)
  • 这意味着采样到的数据天然就是 " 进程实际运行时 " 的分布——这是一种隐式的有效工作时间过滤
  • 对比 SW_CPU_CLOCK 模式,它会采样到 idle 进程——需要过滤掉

陷阱题 4:"bpf_get_current_comm() 返回的进程名最多 15 字符,如果进程名更长会怎样?"

答:

  • Linux 内核的 task_struct::comm 字段固定为 16 字节(TASK_COMM_LEN=16,含末尾 \0)
  • bpf_get_current_comm() 最多拷贝 16 字节,更长的进程名会被截断
  • prctl(PR_SET_NAME) 可以设置但即使设置更长也只会存 15 个字符
  • 对 profiling 的影响:相同进程名聚合后可能混入不同线程——所以 FoldExtend 格式中我们将 comm-pid 作为栈底,保证了唯一性

陷阱题 5:" 你的 match_target 函数中为什么先取 tgid 再判断?为什么不直接用 bpf_get_current_pid_tgid() 和 target_id 比较?"

答: 确实在 TGID 模式下直接比较了 tgid。但对于 PGRP/SESSION/CGROUP 模式,tgid 不参与比较——需要额外的内核数据结构遍历来获取这些 ID。

1
2
3
4
// 优化后的逻辑:先检查 target_id == 0(无过滤),快速返回
if (target_id == 0) {
    return true;  // 大多数情况下第一个判断就通过了!
}

巧妙之处: target_id == 0 的判断放在最前面——默认无过滤模式下,这是最高频路径,单指令就完成判断。

陷阱题 6:" 如果 perf_event_open 失败(返回 -1),你的代码怎么处理?"

答:

1
2
3
4
5
// src/perf.cpp
int fd = static_cast<int>(perf_event_open(&attr, pid, cpu, -1, 0));
if (fd < 0) {
    return Err(libbpf_errno::LIBBPF_ERRNO__INTERNAL);
}
  • 任何 CPU 的 perf_event_open 失败都会导致整个 init 失败
  • init_perf_monitor 返回 Result,调用方(main.cpp)会打印错误并退出
  • 缺失的容错: 如果只是个别 CPU 打开失败(如 offline CPU),理想情况应该跳过该 CPU 继续,但当前实现会整个失败

陷阱题 7:" 你的 benchmark.py 为什么先跑 profiler 再跑 perf?反过来会有什么不同?"

答: 顺序会影响结果!

  • 先跑的工具可能更有优势——系统更冷(cache 未污染,CPU 频率未降),但也可能更劣势(首次加载 page fault)
  • benchmark.py 通过多轮交替(每轮内 fixed 顺序)来暴露这个问题
  • 更严格的设计: 应该随机顺序,或 ABBA 模式(A-B-B-A),消除顺序偏差
  • 当前设计已经比较公平——每个工具在每轮中各自启动独立的 workload

陷阱题 8:" 用户态符号化时,blazesym 读取了目标进程的内存映射(/proc/pid/maps),如果目标进程在符号化过程中结束了呢?"

答: blazesym 在符号化开始时打开 /proc/pid/maps 和对应的 ELF 文件。如果随后目标进程死亡:

  1. ELF 文件本身不受影响(只是进程结束,文件仍在磁盘)
  2. /proc/pid/maps 内容可能不可用——但 blazesym 已经读取了
  3. 如果进程被另一个同名进程替换(PID 复用),maps 内容可能是错的——PID 复用是 profiling 工具的经典问题
  4. 解决方案:记录进程的 start_time(/proc/pid/stat 第 22 字段)作为唯一标识,检测 PID 复用

面试速查卡片

核心数字:

  • 采样方式:perf_event(硬件/软件) + eBPF 频率采样
  • 默认采样频率:10 Hz
  • BPF ring buffer 大小:256 KB
  • 最大栈深度:128 帧
  • 进程名长度:16 字节(TASK_COMM_LEN)
  • 单事件大小:约 2088 字节(StacktraceEvent)
  • 每 CPU 一个 perf_event fd
  • Ring buffer Poll 超时:100 ms
  • 符号化库:blazesym (Rust, via Corrosion)
  • C++ 标准:C++23
  • BPF 程序行数:137 行
  • 构建时长:约 2-3 分钟(含 Rust 编译)

核心数据流:

1
2
perf_event trigger → profile() [eBPF] → ring buffer → poll → EventHandler
(Standard: 逐条符号化输出) / (FoldExtend: 批量去重输出)

核心设计模式:

  • RAII 管理内核资源(ProfilerSkel, RingBuffer, blaze::Syms)
  • std::expected 替代异常(Result<T,E>)
  • 内核态过滤 → 减少无效数据传输
  • 二进制 key 去重 → 相同栈只符号化一次
  • Overloaded + std::visit → 编译期多态
  • 多轮统计基准测试 → 消除偶然因素

核心系统调用链:

1
2
3
4
perf_event_open() x Ncpu → bpf_program__attach_perf_event()
    → poll loop (ring_buffer__poll) → EventHandler::handle()
    → blaze::Symbolizer::symbolize() → blazesym C API
    → handle_standard() / handle_fold_extend()

四个过滤模式对比:

模式内核实现用户态解析
TGIDpid_tgid >> 32pid 直接作为 target_id
PGRPBPF_CORE_READ(task, signal, pids[PIDTYPE_PGID], ...)getpgid(pid)
SessionBPF_CORE_READ(task, signal, pids[PIDTYPE_SID], ...)getsid(pid)
Cgroupbpf_get_current_cgroup_id()stat(/proc/pid/cgroup path).st_ino

与 perf 对比一图流:

1
2
3
4
5
6
7
8
             采集               后处理
perf:     [perf record][perf script + perf report]
          开销: 低              开销: 高
          输出: perf.data       输出: 文本/TUI

profiler: [profiler -E]  ────────────────────────┐
          开销: 中高          在线符号化           输出: 文本/Folded
          输出: 可直接消费 ←──────────────────────┘
Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计