暗色模式

perf 性能剖析:从 perf stat 到采样报告定位 CPU 热点

技术教程
2026-09-08
6
0
本文要点
  • perf 是 Linux 内核自带的采样式性能剖析器,回答的是「CPU 时间到底花在哪个函数」:perf stat 做一次性的计数器统计,perf record/perf report 按固定频率采样并在事后生成热点榜单
  • ⚠️ Ubuntu 大坑一:apt install linux-tools-generic 装的是源里最新内核的工具包。云镜像若内核已升级未重启,运行内核与工具版本不一致,/usr/bin/perf 这个版本检查 wrapper 会直接报 WARNING: perf not found for kernel 6.8.0-136(实测踩中)——装匹配运行内核的精确版本包 linux-tools-6.8.0-136-generic 即愈
  • ⚠️ Ubuntu 大坑二:默认 kernel.perf_event_paranoid=4(比内核文档常见默认 2 更严),非特权用户完全无法采样;按需 sysctl -w kernel.perf_event_paranoid=1 并写入 /etc/sysctl.d/ 持久化
  • ⚠️ 云环境现实:无 PMU 透传的云虚机上 cycles/instructions 等硬件事件一律 <not supported>(实测如此),但软件事件 cpu-clock 可用——perf record 显式 -e cpu-clock 即可完成热点定位
  • 采样式剖析的本质是「以频率换位置」:默认 4000Hz 对运行 1.16 秒的程序采到 4600+ 个样本,报告里 busy 66.72% / spin 33.19% 与演示程序两个循环 2:1 的工作量设定精确吻合
  • 演示程序用 gcc -O2 -fno-inline -g 编译:关掉内联才能保留函数调用边界,热点才能归因到具体函数(内联会把一切抹进 main
  • 全部命令在 Ubuntu 24.04.4 云服务器(内核 6.8.0-136,2 vCPU)上真实执行,截图即真实输出;演示程序与安装的工具包在演示结束后已全部清理

perf 性能剖析:从 perf stat 到采样报告定位 CPU 热点

服务器 CPU 飙到 100% 时,top 能告诉你「哪个进程」,strace 能告诉你「调了哪些系统调用」,但更常见的问题是:这个进程内部,时间到底花在了哪个函数上?是某个循环写得低效,还是被第三方库拖死?这时要用 perf——Linux 内核自带的采样式性能剖析器,配合 Linux 系统实时监控 里的常规观测工具,形成「先看负载、再抓热点」的完整排查链。

perf 的两种典型用法贯穿全文:

  • perf stat:运行一条命令,结束时给出计数器统计——跑了多久、多少次上下文切换、多少条指令(硬件计数器可用时);
  • perf record + perf report:按固定频率采样运行中的程序,事后生成「哪个函数占了多少 CPU」的热点榜单。

实测环境是一台 Ubuntu 24.04.4 云服务器(2 vCPU,内核 6.8.0-136-generic)。这篇文章本身也是一次真实排障记录:安装阶段就连续踩中了 Ubuntu 的两个知名坑,外加一个云环境现实,正好全部是搜索量最高的问题。

安装:第一个坑(wrapper 版本检查)

Ubuntu 的 perf 装在 linux-tools-* 包里,官方推荐直接用 meta 包:

apt-get install -y linux-tools-generic

装完立刻验证版本,结果直接翻车:

perf --version

perf 版本不匹配报错

WARNING: perf not found for kernel 6.8.0-136——perf 明明装了,却说「找不到」?这个报错是 Ubuntu 的经典坑:/usr/bin/perf 其实是一个版本检查 wrapper,它会去 /usr/lib/linux-tools-<内核版本>/ 下找与当前运行内核精确匹配的 perf 二进制。而我们装的 linux-tools-generic 跟随的是源里最新内核(6.8.0-139)的工具包,这台云镜像的内核升级后一直没重启,运行的还是 6.8.0-136——版本对不上,wrapper 直接罢工。

先确认工具确实装上了,只是版本目录不同:

ls /usr/lib/linux-tools-6.8.0-139/
acpidbg
bpftool
cpupower
intel-speed-select
lib
libperf-jvmti.so
perf
rtla
turbostat
usbip
usbipd
x86_energy_perf_policy

perf 就在里面。长期用不能老指全路径,标准解法是安装与运行内核精确匹配的版本包(云服务器不能随便重启时尤其适用;另一个思路是重启进入新内核,让两边对齐):

apt-get install -y linux-tools-6.8.0-136-generic

再验证:

perf --version
perf version 6.8.12

干净了。顺带说明:演示结束后这套工具包已随清理步骤卸载,服务器恢复原状。

第二个坑:Ubuntu 默认 paranoid=4

版本问题解决后还差一道权限门槛。Ubuntu 为了安全把 perf_event_paranoid 默认设为 4(内核文档里的常见默认是 2,Ubuntu 更严),先看当前值:

sysctl -n kernel.perf_event_paranoid
4

这个值的含义是数字越大限制越严:-1 完全放开;0 起非特权用户逐步失去 raw tracepoint、CPU 范围事件、内核采样等能力;2 以上连内核态采样都被禁。在默认值 4 下,普通用户跑 perf stat 会被直接拒绝。本文演示以 root 操作不受此限,但服务器上若要让多个同事账号都能做性能排查,把限制放到 1 就够了——既放开非特权用户的用户态采样,又保留了对内核采样的约束:

sysctl -w kernel.perf_event_paranoid=1
kernel.perf_event_paranoid = 1

sysctl -w 只对本次开机有效,写进 /etc/sysctl.d/ 才能持久化:

echo 'kernel.perf_event_paranoid=1' > /etc/sysctl.d/99-perf.conf

准备一个「吃 CPU」的演示程序

为了让热点可复现、可对照,先写一个纯计算的小程序:两个函数各自做不同轮次的长整型乘加运算,main 轮流调用。volatile 防止编译器把「结果没用上」的循环整体优化掉:

cat > burn.c <<'CEOF'
#include <stdio.h>
#include <stdlib.h>

volatile unsigned long sink = 1;

unsigned long busy(unsigned long x) {
    for (long i = 0; i < 200000000L; i++)
        x = x * 1103515245UL + 12345;
    return x;
}

unsigned long spin(unsigned long x) {
    for (long i = 0; i < 100000000L; i++)
        x = x * 6364136223846793005UL + 1442695040888963407UL;
    return x;
}

int main(int argc, char **argv) {
    long rounds = argc > 1 ? atol(argv[1]) : 3;
    for (long r = 0; r < rounds; r++) {
        sink = busy(sink);
        sink = spin(sink);
    }
    printf("burn done: %lu\n", sink);
    return 0;
}
CEOF

编译时注意两个选项:-O2 模拟真实性能优化;-fno-inline 关掉内联——内联会把函数边界抹掉,采样报告里就只剩一个巨大的 main,热点无法归因。-g 带上调试信息,后续 perf annotate 能看到源码级注释:

gcc -O2 -fno-inline -g -o burn burn.c

直接跑一遍确认可用,耗时约 1.1 秒:

./burn
burn done: 2142834971516577025

perf stat:先看整次运行的计数器

perf stat 会在命令结束后汇总一组计数器:

perf stat ./burn

perf stat 计数器输出

逐行解读:

  • 1139.27 msec task-clock + 0.999 CPUs utilized:整个进程吃满了约 1 个 CPU 核心(这台机器共 2 vCPU),是纯 CPU 密集任务的特征——程序期间没有等待 IO、没有睡大觉;
  • context-switches 只有 10 次、cpu-migrations 1 次:进程几乎没被抢占、没在核心间迁移,调度开销可以忽略;
  • <not supported> cycles / instructions / branches:这是云环境的第一现实——这台虚机没有透传硬件 PMU(Performance Monitoring Unit),perf 拿不到硬件计数器。在有 PMU 的实体机或透传了 PMU 的虚机上,这几行会给出真实数值,还能进一步算出 IPC(每时钟周期指令数)。云上排查不必纠结,软件事件够用;
  • 1.139923300 seconds time elapsed:真实墙钟时间,与 ./burn 的裸跑耗时一致;user 1.1387s / sys 0.001s 说明时间几乎全花在用户态计算上。

perf record + report:采样定位热点函数

stat 回答「整体表现如何」,record 回答「时间具体花在哪」。-e cpu-clock 显式指定软件事件(对应上文云虚机无 PMU 的现实),-o perf.data 指定采样数据文件:

perf record -e cpu-clock -o perf.data ./burn
burn done: 2142834971516577025
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.189 MB perf.data (4625 samples) ]

4625 samples——程序跑了 1.16 秒,perf 默认按 4000Hz 采样,每秒在运行位置「咔嚓」4000 次,攒下 4600 多个现场样本。样本越多,统计越接近真相;代价只是磁盘和一点写入开销。

perf report --stdio 直接输出报告可能很长(上百行),先重定向到文件,再截取前 25 行查看:

perf report --stdio -i perf.data > perf-hotspot.txt
head -25 perf-hotspot.txt

perf report 热点榜单

这就是采样式剖析的经典输出:

  • 66.72% burn busy33.19% burn spin:CPU 时间几乎全在两个函数里。二者开销比约 2:1,而 burn.cbusy 循环 2 亿次、spin 循环 1 亿次——与工作量设定精确吻合。这说明热点定位是可信的:占比就是真实的时间去向;
  • 内核侧采样([kernel.kallsyms])只有 0.02% 的杂音:程序纯用户态计算,没有系统调用热点,符合预期;
  • Overhead该符号占全部采样的比例Command/Shared Object/Symbol 给出进程、模块与函数名;[.] 表示用户态符号,[k] 表示内核态。

采样报告里排在榜首的那个函数,就是优化的第一目标——它优化 10%,整体就能快近 7%。

小结

perf 的日常排障闭环就三步:stat 看整体、record 采样、report 看榜单。过程中最容易卡住的反而不是用法,而是环境:wrapper 版本不匹配先装精确版本包、paranoid 限制按需放行到 1、云虚机没有 PMU 就显式用 cpu-clock 软件事件。想深入调用链时,给 record-g 参数即可采集调用图(输出呈树状展开,适合定位「谁调了慢函数」);进一步还能用 perf annotate 看单条指令级热区。这些采样参数的细节与内核其他运行时参数一脉相承,可配合《Linux sysctl 内核参数调优》理解;而「压测工具各测什么」的横向对照,可参考 fio 磁盘基准HTTP 压测。本文演示用的 burn 程序、采样文件与安装的工具包在验证结束后已全部清理,服务器保持干净状态。

发表评论

暂无评论,快来抢沙发吧!