3. perf record与perf report

perf record 和 perf report 是 Linux perf 子系统中使用频率最高的一对搭档。perf record 负责在目标程序运行时采集性能数据,perf report 则负责将采集到的原始数据解析、汇总并呈现为可读的分析报告。理解它们的采样原理、参数含义以及如何生成火焰图和调用链,是进行系统性能分析的基础。

核心概念:perf record 基于硬件 PMU(Performance Monitoring Unit)或软件事件进行采样。默认模式下,它按照固定频率(如每 1ms)触发中断,记录当前 CPU 的指令指针(IP)和调用栈。perf report 则对这些样本进行统计,生成热点分布和调用关系。

3.1 采样原理:从 PMU 到样本

perf 的采样机制依赖于 CPU 内部的 PMU 计数器。当计数器溢出时,CPU 触发一个 NMI(Non-Maskable Interrupt),perf 内核子系统在中断处理函数中记录当前上下文信息。

采样过程的关键步骤:

  1. 配置计数器:perf 根据用户指定的事件(如 cycles、instructions)和采样周期(period 或 frequency)配置 PMU 寄存器。
  2. 运行目标程序:目标程序在 CPU 上正常执行,PMU 计数器同步递增。
  3. 计数器溢出:当计数器达到预设阈值时,触发 PMU 中断。
  4. 记录样本:perf 中断处理函数保存当前 RIP(指令指针)、PID、TID、CPU ID、调用栈(通过帧指针或 dwarf unwind)到内核缓冲区。
  5. 数据输出:perf record 进程定期从内核缓冲区读取数据,写入 perf.data 文件。
采样 vs 插桩:采样(Sampling)是统计性的,开销低,适合分析长时间运行的程序;插桩(Instrumentation)如 ftrace 或 kprobe 是确定性追踪,开销高但能捕获每次事件。perf record 默认使用采样模式。
perf record 采样流程 用户程序执行 PMU 计数器递增 计数器溢出? 触发 NMI 中断 是 继续执行 否 记录 RIP / 调用栈 写入内核缓冲区 perf.data 文件 采样周期可配置 -F 999:每秒 999 次 -c 100000:每 10 万事件

3.2 perf record:记录性能数据

perf record 是数据采集的入口。它的核心职责是:在目标程序运行期间,按照指定的采样频率或事件周期,将性能样本写入 perf.data 文件。

3.2.1 常用参数

参数 含义 示例
-e 指定性能事件,如 cycles、instructions、cache-misses -e cycles,instructions
-F 采样频率(Hz),每秒采集样本数 -F 99
-c 采样周期,每发生 N 次事件采集一次 -c 100000
-g 记录调用栈(call-graph) -g
-p 附加到指定 PID 的进程 -p 12345
-a 全系统采样(system-wide) -a
--call-graph 指定调用栈采集方法:fp(帧指针)或 dwarf --call-graph dwarf
-o 指定输出文件名,默认 perf.data -o myperf.data

3.2.2 典型使用场景

场景一:分析 CPU 热点

# 以 99Hz 采样 CPU 周期事件,记录调用栈
perf record -e cycles -F 99 -g -- ./myapp

# 采样 10 秒后自动停止
perf record -e cycles -F 99 -g -- sleep 10 ./myapp

场景二:分析缓存缺失

# 采集 cache-misses 事件
perf record -e cache-misses -F 99 -g -a -- sleep 30

场景三:分析指定进程

# 附加到运行中的 nginx 进程
perf record -e cycles -F 99 -g -p $(pgrep -x nginx) -- sleep 10
注意事项:
  • 采样频率过高(如 -F 10000)可能导致采样开销过大,影响目标程序行为。
  • 使用 -g 时,如果程序未编译帧指针(-fomit-frame-pointer),建议使用 --call-graph dwarf,但 dwarf 展开速度较慢。
  • 全系统采样(-a)需要 root 权限。

3.3 perf report:生成火焰图与调用链分析

perf report 读取 perf.data 文件,将原始样本解析为可读的报告。它支持多种输出格式,包括文本报告、调用链树、以及用于生成火焰图的数据。

3.3.1 基本使用

# 查看默认报告
perf report

# 以调用链形式显示
perf report -g graph

# 输出到文件
perf report -g graph --stdio > report.txt

3.3.2 调用链分析

perf report 的调用链视图展示了每个热点函数的调用路径。默认按 overhead(开销占比)排序,从高到低显示。

调用链的两种显示模式:

模式 说明 适用场景
graph 以树形结构显示每个函数的调用链,子函数缩进 快速定位热点路径
fractal 类似 graph,但每个函数只显示一次,子调用折叠 减少重复,适合大型调用链
flat 扁平显示,不展开调用链 只看函数自身开销

3.3.3 生成火焰图数据

火焰图(Flame Graph)是 Brendan Gregg 发明的可视化工具,用于直观展示 CPU 热点分布。perf 本身不直接生成 SVG 火焰图,但可以输出折叠格式的调用栈数据,供 FlameGraph 工具集使用。

生成火焰图的步骤:

  1. 采集数据:perf record -e cycles -F 99 -g -- ./myapp
  2. 生成折叠栈:perf script > out.perf
  3. 折叠调用栈:stackcollapse-perf.pl out.perf > out.folded
  4. 生成 SVG:flamegraph.pl out.folded > flame.svg
# 一键生成火焰图数据(使用 perf 内置脚本)
perf script -i perf.data --header | ./stackcollapse-perf.pl | ./flamegraph.pl > flame.svg
perf report 调用链分析示例 main (100%) parse_input (45%) process_data (40%) output_result (15%) tokenize (30%) validate (15%) compute (35%) format (5%) 调用链分析要点 • 百分比表示该函数及其子函数占用的 CPU 时间比例 • 红色节点为根调用,橙色为中间层,绿色为叶子函数 • 优化应优先关注 overhead 最高的叶子函数及其调用路径

3.4 实战案例:分析 CPU 热点并生成火焰图

以下是一个完整的实战流程,演示如何从采样到生成火焰图。

步骤 1:编写测试程序

// test_hot.c
#include <stdio.h>
#include <stdlib.h>

void hot_function() {
    volatile int i;
    for (i = 0; i < 100000000; i++);
}

void cold_function() {
    volatile int i;
    for (i = 0; i < 1000; i++);
}

int main() {
    int i;
    for (i = 0; i < 10; i++) {
        hot_function();
        cold_function();
    }
    return 0;
}

步骤 2:编译并采样

gcc -g -O0 -fno-omit-frame-pointer -o test_hot test_hot.c

# 采样 5 秒
perf record -e cycles -F 99 -g -- ./test_hot

步骤 3:查看报告

perf report -g graph --stdio

# 输出片段示例:
# 99.99%  test_hot  test_hot    [.] hot_function
#         |
#         --99.99%-- main
#                    hot_function

步骤 4:生成火焰图

# 安装 FlameGraph 工具
git clone https://github.com/brendangregg/FlameGraph.git
cd FlameGraph

# 生成折叠栈
perf script -i ../perf.data | ./stackcollapse-perf.pl > out.folded

# 生成 SVG 火焰图
./flamegraph.pl out.folded > flame.svg
火焰图解读:
  • X 轴:样本分布,不代表时间顺序;宽度越宽,表示该函数占用的 CPU 时间越多。
  • Y 轴:调用栈深度,底部是根调用,顶部是叶子函数。
  • 颜色:通常随机分配,用于区分不同函数;也可以按函数名哈希着色。
  • 鼠标悬停:显示函数名和样本占比。

3.5 常见问题与避坑

问题 原因 解决方案
perf report 显示 ?? 符号 缺少符号表或调试信息 编译时加 -g,或安装 debuginfo 包
调用链不完整 帧指针被优化掉,或 dwarf 展开失败 使用 --call-graph dwarf,或编译时加 -fno-omit-frame-pointer
采样开销过高 采样频率设置过高 降低 -F 值,或使用 -c 指定周期
perf.data 文件过大 采样时间过长或频率过高 使用 --time 限制采样时间,或增大采样周期
火焰图显示所有函数名相同 perf script 输出未正确折叠 检查 stackcollapse-perf.pl 是否处理了多线程
最佳实践:
  • 首次分析使用 -F 99(99Hz),兼顾精度和开销。
  • 始终使用 -g 记录调用链,否则无法生成火焰图。
  • 生产环境建议使用 --call-graph fp 并确保程序编译时保留帧指针。
  • perf.data 文件包含原始样本,可以离线传输到分析机器上使用 perf report 查看。