perf —— 采样调用栈,定位到函数/代码行的终极工具
这个工具是做什么的
top/pidstat 只能区分用户态/内核态,perf 基于内核 PMU(性能监控单元)和采样,能告诉你具体是哪个函数、哪行代码、哪条调用链在烧 CPU。是 CPU 高问题的"终审法官"。
可以回答什么问题
| 问题 | 怎么用 |
|---|---|
| CPU 主要耗在哪个函数? | perf record -g + perf report 看热点 |
| 热点函数的完整调用链? | perf report 展开 / 火焰图 |
| 每周期指令数(IPC)高不高?CPU 在有效计算还是空转? | perf stat 的 IPC = instructions / cycles |
| 缓存命中率低吗?分支预测失败多吗? | perf stat -e cache-misses,branch-misses |
| 上下文切换多不多?在迁移吗? | perf stat -e context-switches,migrations |
| 现在哪个函数最热(免落盘实时)? | perf top |
| 程序做了哪些系统调用,每次多慢? | perf trace(perf 内置,比 strace 低开销) |
数据来源
- 来源接口:内核
perf_events子系统,通过perf_event_open(2)系统调用开启事件。事件分三类:CPU 硬件 PMU(cycles/instructions/cache-misses/branch-misses 等,由 CPU 芯片提供)、软件事件(时钟、缺页、上下文切换)、内核 tracepoint(预埋在内核里的探针)。 - 采集方式:采样而非读统计值——按频率(如
-F 999)周期性中断,记录当时的指令指针和调用栈,事后按出现次数聚合出"热点"。 - 由此决定的特性:① 结果是统计意义上的热点,采样频率越高越精确、开销也越大;② 读硬件 PMU 和内核栈需要权限,受
kernel.perf_event_paranoid控制,容器里常需额外 capability;③ 能看到函数/行级信息,前提是二进制带符号(-g、别过度优化),否则栈是地址。
想深入原理——perf_event_open、PMU 采样、mmap 环形缓冲区、数据怎么落盘到
perf.data、以及用 libperf 编程——见 perf-internals。
# 安装
yum install perf # CentOS/RHEL
apt install linux-tools-common linux-tools-$(uname -r) # Debian/Ubuntu
perf --version前提条件:分析对象要带调试符号(编译加
-g),且别用高优化(-O2会内联/消除函数,栈变得难读甚至误导)。本仓库make默认-O0 -g就是为此;对比make release(-O2)你会看到热循环被优化没了。生产二进制若 strip 过符号,需额外装 debuginfo 包。
一、perf record —— 采样并落盘
perf record -g -p <PID> -- sleep 30 # 采样已运行进程 30 秒(-- sleep N 是常用控时法)
perf record -g ./cpu_demo # 从头采样一个新命令的完整生命周期
perf record -g -a -- sleep 10 # -a 采样整机所有 CPU(找不到具体 PID 时)
perf record -F 999 -g -p <PID> -- sleep 30 # -F 采样频率(Hz),999 避免和 1000Hz 时钟共振
perf record -g --call-graph dwarf -p <PID> -- sleep 10 # 用 DWARF 展开栈(见下方坑)参数:
-g:记录调用栈(否则只知道热点函数,不知道谁调用的)。-F <Hz>:每秒采样次数,越高越精细但开销越大,常用 99/199/999。-a:所有 CPU;-p:指定进程;-t:指定线程 TID。- 采样数据默认写到当前目录
perf.data。
二、perf report —— 查看热点
perf report # 交互式 TUI:↑↓ 选函数,回车展开调用链,a 看反汇编
perf report --stdio # 直接打印到终端(适合复制/脚本)
perf report -n # 显示采样次数而非仅百分比
perf report --sort comm,dso,symbol # 按 进程/动态库/符号 聚合阅读要点:
Overhead列 = 该函数占总采样的百分比,从高往下看即可。- 函数名颜色/来源:属于可执行文件/
.so的是用户态(cpu_demo 场景1 的累加循环在这里);属于[kernel.kallsyms]的是内核态(场景2 的vfs_write、ksys_write、__x64_sys_openat等 syscall 路径)。 - 回车展开可看调用链(谁调用了这个热点),自底向上定位到自己的业务代码。
三、火焰图(最直观的可视化)
需要 Brendan Gregg 的 FlameGraph 脚本:
git clone https://github.com/brendangregg/FlameGraph
perf record -F 999 -g -p <PID> -- sleep 30
perf script | FlameGraph/stackcollapse-perf.pl | FlameGraph/flamegraph.pl > flame.svg浏览器打开 flame.svg:
- X 轴 = 占比(横条越宽越热,不是时间轴,别按左右理解成时间顺序)。
- Y 轴 = 调用栈深度(下面是父调用,上面是子调用,最顶端正在执行)。
- 鼠标点某一格可放大该子树,搜索框可高亮某函数在各处的总占比。
变体:
perf script | stackcollapse-perf.pl | flamegraph.pl --color=java换配色;差分火焰图(difffolded.pl)可对比优化前后。
四、perf top —— 实时热点(免落盘)
perf top -p <PID> # 像 top,但列的是热点函数,实时滚动
perf top -g -p <PID> # 带调用链
perf top -e cache-misses # 按指定事件排热点适合"现在就卡,想立刻看谁最热",无需先 record 再 report。
五、perf stat —— 整体性能计数(不采样栈,看硬件指标)
perf stat ./cpu_demo # 跑完打印一份计数汇总
perf stat -p <PID> -- sleep 10 # 对运行中进程统计 10 秒
perf stat -e cache-misses,branch-misses,cycles,instructions ./cpu_demo
perf stat -d ./cpu_demo # -d 更详细(含 cache 层级)关键指标:
| 指标 | 含义与判读 |
|---|---|
instructions / cycles → IPC | 每周期指令数。IPC 低(<1)= 流水线常停顿:等内存、分支预测失败、依赖链长 |
cache-misses / cache-references | 缓存未命中率高 = 内存访问局部性差(如随机访问大数组),考虑改数据布局 |
branch-misses | 分支预测失败率高 = 分支太随机(如数据无序),排序数据或用无分支写法可改善 |
context-switches | 上下文切换次数,多 = 线程过多/频繁阻塞 |
page-faults | 缺页次数,major 缺页触发磁盘 IO |
经验:
us高但 IPC 也低,说明 CPU 忙在"等"(内存/分支)而非有效计算——这类问题火焰图看着热,真正病根是访存模式,perf stat才看得出。
六、perf 可观测指标全景
perf 能观测的指标分四大类:硬件 PMU 事件、软件事件、内核 Tracepoint 事件、专用子命令。执行 perf list 可以列出本机支持的全部事件。
6.1 硬件 PMU 事件 —— CPU 微架构
这些事件由 CPU 芯片的性能监控单元(PMU)提供,是理解"代码在 CPU 上真正怎么跑"的核心。不同 CPU 型号支持的事件集不同(Intel/AMD/ARM 各有差异),perf list 列出的是当前机器实际可用的。
6.1.1 核心计算指标
| 事件名 | 含义 | 典型判读 |
|---|---|---|
cycles | CPU 时钟周期数 | 跟 task-clock 结合判断主频是否正常 |
instructions | 已退休指令数 | 跟 cycles 算 IPC = instructions / cycles |
| IPC | 每周期指令数(派生值) | IPC >2 优秀,1~2 正常,<0.7 说明 CPU 经常在等 |
ref-cycles | 不受降频影响的参考周期 | 跟 cycles 对比判断是否降频(cycles < ref-cycles = 降频了) |
6.1.2 流水线停顿(Stall)分析
| 事件名 | 含义 | 判读 |
|---|---|---|
stalled-cycles-frontend | 前端停顿周期(取指不够快) | 高 → I-cache miss / 分支预测失败 / 指令 TLB miss |
stalled-cycles-backend | 后端停顿周期(执行单元等数据) | 高 → D-cache miss / 内存延迟 / 依赖链长 |
stalled-cycles-frontend / cycles | 前端停顿占比 | >30% → 取指瓶颈,改代码布局或减少间接跳转 |
stalled-cycles-backend / cycles | 后端停顿占比 | >30% → 数据瓶颈,改数据布局或预取 |
区分前端 vs 后端是性能调优的关键分叉点:前端 = 喂不饱流水线(指令侧),后端 = 流水线满了但等数据(数据侧)。
6.1.3 分支预测
| 事件名 | 含义 | 判读 |
|---|---|---|
branch-instructions | 分支指令总数 | 配合 branch-misses 算失败率 |
branch-misses | 分支预测失败次数 | 失败率 >5% 说明分支模式不规律:数据无序导致的分支、多态/虚函数太多 |
branch-loads / branch-load-misses | 间接分支(如虚函数调用)的预测 | 面向对象代码中多态调用频繁时关注 |
branch-misses / branch-instructions | 分支预测失败率(派生值) | <1% 优秀,1~3% 正常,>5% 可优化 |
6.1.4 缓存层级
| 事件名 | 含义 | 判读 |
|---|---|---|
cache-references | 所有缓存访问次数 | 配合 cache-misses 算命中率 |
cache-misses | 最后一级缓存(LLC)未命中 | 命中率 <90% → 数据布局或访问模式有问题 |
L1-dcache-loads / L1-dcache-load-misses | L1 数据缓存加载/未命中 | L1 miss 率高 → 数据步长太大或不连续 |
L1-dcache-stores / L1-dcache-store-misses | L1 数据缓存存储/未命中 | 写操作频繁时关注 |
L1-icache-loads / L1-icache-load-misses | L1 指令缓存加载/未命中 | I-cache miss 高 → 代码段太大或跳转太散 |
LLC-loads / LLC-load-misses | 最后一级缓存加载/未命中 | LLC miss = 必须访问 DRAM,延迟 ~100-300 cycles |
LLC-stores / LLC-store-misses | 最后一级缓存存储/未命中 | 写穿透场景 |
6.1.5 TLB(地址翻译缓存)
| 事件名 | 含义 | 判读 |
|---|---|---|
dTLB-loads / dTLB-load-misses | 数据 TLB 加载/未命中 | 高 → 访问的内存页太散,考虑大页(HugePage) |
iTLB-loads / iTLB-load-misses | 指令 TLB 加载/未命中 | 高 → 代码段碎片化严重 |
dTLB-stores / dTLB-store-misses | 数据 TLB 存储/未命中 | 写密集且页多时关注 |
TLB miss 的真实代价:一次 TLB miss 需要硬件 page walk 遍历页表(4 级页表 = 4 次内存访问),比 cache miss 更贵。频繁 TLB miss 是 IPC 被拉低的常见元凶之一。
6.2 软件事件 —— 内核直接计数
不依赖 CPU PMU,由内核直接计数。无需 -e 时 perf stat 默认输出其中一部分。
| 事件名 | 含义 | 判读 |
|---|---|---|
task-clock | 进程实际占用 CPU 的累计时间(ms) | 单核跑满 1 秒 = 1000;多线程可 > 墙钟时间 |
context-switches | 上下文切换次数(自愿+非自愿,全线程合计) | 单线程 >10,000/秒偏高,>50,000/秒严重;多线程需按核折算 |
cpu-migrations | 线程从一个核迁移到另一个核的次数 | 高 → 考虑绑核或 taskset |
page-faults | 缺页次数(minor-faults + major-faults) | 即 minor faults,通常无害 |
minor-faults | 轻微缺页(页已在内存,只需建页表映射) | 正常操作(如 mmap 首次访问),数量可能很大 |
major-faults | 严重缺页(需要从磁盘读取) | >0 = 访问了被 swap 出去的页或 mmap 文件未预读 |
alignment-faults | 非对齐访问异常次数 | ARM/PowerPC 上常见,x86 上极罕见(硬件自动处理) |
emulation-faults | 模拟未实现指令的次数 | 极罕见,虚拟化或特殊指令场景 |
cpu-clock | 进程的 CPU 时钟累计(纳秒) | 类似 task-clock 但精度更高 |
dummy | 占位事件(不实际计数) | 用于 perf record 的定时采样,不消耗 PMU 硬件计数器 |
6.3 内核 Tracepoint 事件 —— 精准捕获内核行为
Tracepoint 是 Linux 内核中预埋的静态探针,覆盖调度、系统调用、磁盘 IO、网络、内存管理等全部子系统。用 perf list tracepoint 查看全部;用 perf record -e <tracepoint> 采样。
6.3.1 调度类(sched:*)
| Tracepoint | 触发时机 | 能看什么 |
|---|---|---|
sched:sched_switch | 每次上下文切换 | 哪个 task 被切走、切给谁、调用栈 |
sched:sched_wakeup | 进程被唤醒 | 谁唤醒了谁、唤醒延迟 |
sched:sched_wakeup_new | 新创建的进程首次被唤醒 | fork + exec 后的第一次调度 |
sched:sched_migrate_task | 任务跨核迁移 | 从哪个 CPU 迁到哪个 CPU |
sched:sched_process_exec | exec() 调用 | 进程启动新程序 |
sched:sched_process_fork | fork() 调用 | 进程创建子进程 |
sched:sched_process_exit | 进程退出 | 进程结束 |
sched:sched_process_wait | 父进程 wait() | 等待子进程 |
sched:sched_stat_runtime | 调度统计 | 每个 task 的累计运行时间 |
sched:sched_stat_wait | 等待统计 | task 在就绪队列等待的累计时间 |
sched:sched_stat_sleep | 睡眠统计 | task 睡眠的累计时间 |
sched:sched_stat_iowait | IO 等待统计 | task 等待 IO 的时间 |
6.3.2 系统调用类(syscalls:*)
| Tracepoint | 触发时机 | 能看什么 |
|---|---|---|
syscalls:sys_enter_<name> | 进入某个系统调用 | 参数、调用方 |
syscalls:sys_exit_<name> | 退出某个系统调用 | 返回值、耗时 |
常用:
sys_enter_futex(锁争用)、sys_enter_read/sys_enter_write(IO 热点)、sys_enter_accept/sys_enter_connect(网络连接)。
6.3.3 块设备 IO(block:*)
| Tracepoint | 触发时机 | 能看什么 |
|---|---|---|
block:block_rq_issue | IO 请求发送到设备 | 哪个进程发起的 IO、扇区位置、大小 |
block:block_rq_complete | IO 请求完成 | 一次 IO 的完成事件 |
block:block_bio_queue | bio 进入队列 | IO 排队开始时间 |
block:block_bio_remap | bio 被重映射 | DM/md 等设备映射层的转发 |
结合
block_rq_issue和block_rq_complete的时间差,可以算出单次 IO 的端到端延迟。
6.3.4 内存管理(kmem:*)
| Tracepoint | 触发时机 | 能看什么 |
|---|---|---|
kmem:kmalloc / kmem:kfree | 内核内存分配/释放 | 内核 slab 分配行为 |
kmem:mm_page_alloc / kmem:mm_page_free | 页分配/释放 | buddy allocator 行为 |
kmem:mm_page_pcpu_drain | per-CPU 页缓存回收 | 内存回收触发频率 |
6.3.5 其他常用 Tracepoint
| 类别 | 关键 Tracepoint | 用途 |
|---|---|---|
| 中断 | irq:irq_handler_entry / irq:irq_handler_exit | 哪个中断处理函数耗时多久 |
| 软中断 | irq:softirq_entry / irq:softirq_exit | NET_RX / NET_TX / TIMER 等软中断行为 |
| 网络 | net:net_dev_xmit / net:napi_gro_receive_entry | 网络包收发 |
| 信号 | signal:signal_generate / signal:signal_deliver | 信号发送和递送 |
| 文件系统 | ext4:ext4_da_write_begin / ext4:ext4_da_write_end | 写操作全路径 |
| 写回 | writeback:writeback_dirty_page / writeback:writeback_written | 脏页产生到刷盘 |
| 定时器 | timer:timer_start / timer:timer_expire_entry | 内核定时器触发 |
6.4 专用子命令 —— 各司其职
perf 不只是 stat + record + report。以下是完整子命令清单:
| 子命令 | 作用 | 一句话说明 |
|---|---|---|
perf stat | 性能计数器汇总 | "这项指标是多少",不采样调用栈 |
perf record + perf report | 采样调用栈 + 热点分析 | "谁在烧 CPU",最常见的分析路径 |
perf top | 实时热点(不落盘) | "现在卡住了,立刻看什么函数最热" |
perf sched | 调度延迟/切换分析 | "哪个线程在等调度,等了多久" |
perf lock | 锁争用分析 | "哪个锁竞争最激烈" |
perf mem | 内存访问延迟定位 | "哪条指令的内存访问最慢" |
perf trace | 系统调用跟踪 | 比 strace 开销低的 syscall 追踪(基于 perf) |
perf probe | 动态插桩 | 在运行中的内核或用户程序任意位置插入探针 |
perf list | 列出所有可用事件 | 看看这台机器能采哪些指标(硬件/软件/tracepoint) |
perf script | 原始采样数据导出 | 导出成文本,供自定义分析或生成火焰图 |
perf kvm | 虚拟机性能分析 | KVM guest/host 的性能事件 |
perf bench | 微基准测试 | 测 memcpy/pipe/futex/sched 等基础操作速度 |
perf test | 自检 | 检查 perf 功能是否正常 |
perf diff | 两份 perf.data 对比 | 优化前后的差异量化 |
perf archive | 打包 perf.data + 符号 | 把采样数据搬到另一台机器分析 |
perf c2c | False Sharing 检测 | Cache line 伪共享分析(多线程性能杀手之一) |
perf buildid-cache | 管理 build-id 缓存 | 管理符号文件的缓存 |
6.5 常用组合速查
| 我想知道... | 用这个 |
|---|---|
| 进程总体硬件效率 | perf stat -d -p <PID> -- sleep 10 |
| 是取指瓶颈还是数据瓶颈 | perf stat -e stalled-cycles-frontend,stalled-cycles-backend -p <PID> -- sleep 5 |
| 缓存命中率 | perf stat -e cache-references,cache-misses -p <PID> -- sleep 5 |
| 线程各自的切换/效率 | perf stat --per-thread -p <PID> -- sleep 3 |
| 自愿 vs 非自愿切换 | pidstat -w 1 -p <PID>(perf stat 不区分,需借助 /proc 或 pidstat) |
| 缺页分 minor/major | perf stat -e minor-faults,major-faults -p <PID> -- sleep 5 |
| 降频了吗 | perf stat -e cycles,ref-cycles -p <PID> -- sleep 5(cycles < ref-cycles = 降频) |
| 每个 syscall 的开销 | perf trace -p <PID> 或 perf trace -s |
| 锁争用 | perf lock record -p <PID> -- sleep 5 && perf lock report |
| 调度延迟 | perf sched record -p <PID> -- sleep 5 && perf sched latency |
| False Sharing | perf c2c record -p <PID> -- sleep 5 && perf c2c report |
| 内存访问延迟 | perf mem record -p <PID> -- sleep 5 && perf mem report |
七、常见坑与排错
- 栈只有一层 / 一堆
[unknown]:- 没编符号或被 strip → 编译带
-g,或装对应 debuginfo。 - 编译器省略了栈帧指针(
-O2默认)→ 用--call-graph dwarf(靠 DWARF 调试信息展开,开销大但准),或重编加-fno-omit-frame-pointer。 - 新硬件可用
--call-graph lbr(用 CPU 的 LBR 硬件栈,开销小)。
- 没编符号或被 strip → 编译带
- 权限不足 / 采不到内核栈:perf 需要 root,或调低门槛
sysctl -w kernel.perf_event_paranoid=1(-1 最宽松);看内核符号还需sysctl -w kernel.kptr_restrict=0。 - 容器里用不了:需
--privileged或--cap-add SYS_ADMIN,且容器内要有 perf 和符号。 -O2下火焰图对不上源码:函数被内联,栈里看不到你写的函数名——这不是 bug,是优化的正常结果(正好用本仓库make release对比体会)。- 采样频率太高:
-F设很大在高负载机上,perf 自身开销和数据量都暴涨;一般 99~999 足够。
八、常见排查方法与分析
8.1 CPU 高 → 定位热点函数
top → 确定高 CPU 进程 PID
→ perf record -g -p <PID> -- sleep 30
→ perf report → 看 Overhead 最高的函数
→ 用户态函数 → 看调用链,定位业务代码
→ 内核态函数 → syscall 路径,用 strace -c 辅助8.2 CPU 利用率高但 IPC 低 → 假忙
perf stat -p <PID> -- sleep 10 → IPC < 0.5
→ CPU 在频繁 stall(等内存/分支预测失败)
→ perf stat -e stalled-cycles-frontend,stalled-cycles-backend -p <PID> -- sleep 5
→ frontend 高 → 指令获取瓶颈
→ backend 高 → 数据/内存瓶颈
→ perf stat -e cache-misses,cache-references -p <PID> -- sleep 5
→ cache miss 率高 → 改数据布局、减少随机访问8.3 内核态 CPU 高 → 系统调用热路径
perf record -g -p <PID> -- sleep 30
→ perf report → 看 [kernel.kallsyms] 函数
→ 结合 strace -c -p <PID> → 确认哪个 syscall
→ 若是锁相关 → perf lock record + perf lock report
→ 若是内存分配 → 减少 malloc 或使用内存池8.4 上下文切换频繁 → 调度开销
perf stat -e context-switches,migrations -p <PID> -- sleep 5
→ 切换多 + pidstat -w → 区分自愿/非自愿
→ perf sched record -- sleep 5
→ perf sched latency → 看每个 task 的调度延迟真实案例解读:一份
perf stat -p 58931 -- sleep 3的数据(75,200 次/秒切换、IPC 3.41、page-faults 仅 2)的完整分析,见 context-switch 实战篇 §5.7-B。该案例展示了"CPU 微架构指标很好,但切换极高"时如何判断根因。
8.5 进程启动慢 → 缺页分析
perf stat -e page-faults,major-faults -p <PID> -- sleep 5
→ major-faults 持续非零 → 启动阶段大量 mmap 缺页
→ 减少启动时的 lazy 内存访问,或预热九、交叉引用
- 整机 → 进程:top → pidstat(确定哪个进程需要 perf 分析)
- 系统调用:strace(sy 高时辅助定位 syscall)
- 调度分析:scheduling-observation(perf sched 深度分析)
- 微架构:cpu-microarch-overview(IPC/cache-miss/branch-miss 的硬件含义)
- 原理深入:perf-internals(perf_events 内核机制)
- 场景化排查链路:排查工具集(perf 只是"下钻"环节,先看分诊工具定方向,再回到 perf 定位函数热点)
十、其他实用子命令
perf trace -p <PID> # 类似 strace 但基于 perf,开销更低,看系统调用
perf record -e block:block_rq_issue -a -- sleep 5 # 按内核 tracepoint 事件采样(这里是块 IO 下发)
perf list # 列出本机支持的所有事件(硬件/软件/tracepoint)
perf sched record -- sleep 5; perf sched latency # 调度延迟分析FAQ
Q: perf 和 top 有什么区别? A: top 只能看进程级 CPU 占用;perf 通过 PMU 采样能定位到具体是哪个函数、哪行代码、哪条调用链在烧 CPU,是 CPU 高问题的"终审工具"。
Q: perf record -g 采样后全是 [unknown] 怎么办? A: 程序需带调试符号(编译加 -g、勿 strip)或装 debuginfo;-O2 默认省略栈帧指针,可加 --call-graph dwarf 或重编加 -fno-omit-frame-pointer。
Q: 为什么 -O2 编译后火焰图对不上源码? A: 函数被内联后栈里看不到源码里的函数名,这是优化的正常结果,不是 bug;可以用本仓库 make release 对比体会优化效果。
Q: IPC 低说明什么? A: IPC = instructions/cycles,低于 0.5 说明 CPU 大量周期在 stall(等内存、分支预测失败等),配合 stalled-cycles-frontend/backend 定位是取指还是数据瓶颈。
Q: 容器里能用 perf 吗? A: 可以但受限:需要 --privileged 或 --cap-add SYS_ADMIN,容器内要有 perf 和符号;权限门槛由 kernel.perf_event_paranoid 控制。
Q: perf 采样频率 -F 设多大合适? A: 一般 99~999Hz 足够;设太大会让 perf 自身开销和数据量暴涨,高负载机上尤其明显。
总结
perf 是 CPU 高问题的「终审工具」:perf stat 读 PMU 硬件事件计数(IPC、cache-miss、branch-miss、stalled-cycles)判断瓶颈方向,perf record -g 采样加 perf report/火焰图定位到具体函数、源码行、调用链在烧 CPU;正确用法是先用 top/pidstat 定到进程再 perf 下钻,采样全是 [unknown] 就装 debuginfo 或用 --call-graph dwarf(-O2 省略栈帧),-F 一般 99~999Hz 即可。它回答的是「CPU 时间到底花在哪段代码」,和 top 的进程级占用是下钻关系而非替代。