在计算机的世界里, 程序运行的每一纳秒都在上演着无数微观事件: CPU 的指令执行、缓存的命中与失效、内存的分配与回收……这些事件如同宇宙中的暗物质, 虽然看不见摸不着, 却主宰着程序的性能命运。
早期的开发者们如同在迷雾中前行, 只能依赖粗糙的计时器和日志猜测性能瓶颈。直到 Perf 的出现——它像一台精密的显微镜, 将程序运行的每一个细节放大到可见的维度;又像一台时光机, 能回放代码执行的每一帧画面。
Perf 诞生于 Linux 内核社区, 最初由 Ingo Molnar 等人推动, 现已成为 Linux 性能分析的瑞士军刀。它直接与硬件性能计数器(Performance Monitoring Unit, PMU)对话, 能捕捉从 CPU 指令、缓存失效到系统调用的全维度数据。
简介
perf list 是 Linux 性能分析工具 perf 中的一个命令, 用于列出当前系统支持的硬件和软件性能监控事件。这些事件可以用于性能分析、调优和调试。perf 是 Linux 内核提供的一个强大的性能分析工具, 支持 CPU、内存、I/O 等各方面的性能监控。
perf list 命令会列出系统支持的以下类型的事件:
-
硬件事件(Hardware Events): 由 CPU 的性能监控单元(PMU, Performance Monitoring Unit)提供。 例如: CPU 周期、指令数、缓存命中/失效、分支预测等。
-
软件事件(Software Events): 由 Linux 内核提供的基于软件的性能事件。 例如: 上下文切换、页错误、CPU 迁移等。
-
内核 PMU 事件(Kernel PMU Events): 特定于内核的性能监控事件。例如: CPU 缓存事件、TLB 事件等。
-
Tracepoint 事件(Tracepoint Events): 内核中静态定义的跟踪点, 用于监控内核和用户空间的行为。例如: 系统调用、调度事件、文件系统操作等。
-
动态探针事件(Dynamic Tracing Events): 通过 kprobes 和 uprobes 动态插入的探针事件。例如: 监控内核函数或用户空间函数的调用。
perf 有多个子命令, 我们可以根据 perf -h
查看其所有子命令, 以下是常用的子命令介绍:
子命令 | 功能描述 | 使用场景 |
---|---|---|
record |
记录程序的性能数据(如 CPU 使用率、函数调用栈等), 生成 perf.data 文件。 |
用于事后分析程序的性能瓶颈, 支持生成火焰图(Flame Graph)。 |
report |
解析 perf.data 文件, 展示性能分析结果(如热点函数、调用栈等)。 |
查看 perf record 记录的性能数据, 定位 CPU 热点或函数调用关系。 |
top |
实时显示系统中或某个进程的 CPU 使用率最高的函数。 | 实时监控程序的性能瓶颈, 类似 top 命令, 但精确到函数级别。 |
stat |
统计程序的整体性能指标(如 CPU 周期、缓存命中率、分支预测错误等)。 | 快速评估程序的整体性能, 适合初步性能分析。 |
trace |
追踪程序的系统调用和信号事件。 | 分析程序的系统调用行为, 定位 I/O 或内核相关问题。 |
annotate |
将性能数据映射到源代码或汇编代码, 显示热点代码行。 | 深入分析热点函数的代码行, 定位具体的性能瓶颈。 |
list |
列出当前系统支持的硬件和软件性能事件。 | 查看可监控的性能事件(如 CPU 周期、缓存失效等)。 |
probe |
动态插入探针(kprobe 或 uprobe), 用于跟踪内核或用户空间函数。 | 动态监控特定函数或代码行的执行情况。 |
实验前准备
我们准备一个 golang 应用程序将用于后续 perf 实验. 当然, 你也可以使用你习惯的语言编写 :)
// 执行 `go build -o app` 编译成二进制包
package main
import (
"net/http"
"time"
)
func main() {
ch := make(chan struct{})
go func() {
for {
_, _ = http.NewRequest(http.MethodGet, "https://github.com", nil)
count()
time.Sleep(time.Second * 2)
println("ok")
}
}()
<-ch
}
func count() {
var sum int
for i := 0; i < 100_000_000; i++ {
sum += i
}
}
子命令实践
perf list
perf list 是 perf 工具中的一个非常实用的命令, 它的作用是列出当前系统支持的所有硬件和软件性能事件。这些性能事件可以用于后续的性能分析工作, 如性能计数、热点分析等。 通过执行 perf list, 你可以查看当前系统中支持的所有硬件和软件性能事件。这些事件包括 CPU 性能计数(如 CPU 周期、指令计数等)、内存事件(如缓存未命中)、硬件事件(如分支预测错误)以及软件事件(如上下文切换)。
支持自定义事件: perf list 不仅列出内核预定义的硬件和软件事件, 还可以通过自定义性能事件来扩展, 帮助进行特定领域的分析。
我们可以执行 perf list --help
查看命令描述
$ perf list --help
perf list [<options>] [hw|sw|cache|tracepoint|pmu|sdt|metric|metricgroup|event_glob]
-d, --desc # 打印额外的事件描述。--no-desc 表示不打印。
-v, --long-desc # 打印更长的事件描述, 提供更详细的信息。
--debug # 启用调试输出。
--deprecated # 打印已弃用的事件。
--details # 打印关于性能事件名称和内部使用的表达式的详细信息。
我们可以查看当前 linux kernal 版本支持的 tracepoint
$ perf list tracepoint --details
List of pre-defined events (to be used in -e):
alarmtimer:alarmtimer_cancel [Tracepoint event]
alarmtimer:alarmtimer_fired [Tracepoint event]
alarmtimer:alarmtimer_start [Tracepoint event]
alarmtimer:alarmtimer_suspend [Tracepoint event]
amd_cpu:amd_pstate_perf [Tracepoint event]
avc:selinux_audited [Tracepoint event]
block:block_bio_backmerge [Tracepoint event]
block:block_bio_bounce [Tracepoint event]
block:block_bio_complete [Tracepoint event]
block:block_bio_frontmerge [Tracepoint event]
block:block_bio_queue [Tracepoint event]
block:block_bio_remap [Tracepoint event]
block:block_dirty_buffer [Tracepoint event]
block:block_getrq [Tracepoint event]
block:block_plug [Tracepoint event]
block:block_rq_complete [Tracepoint event]
block:block_rq_insert [Tracepoint event]
block:block_rq_issue [Tracepoint event]
block:block_rq_merge [Tracepoint event]
block:block_rq_remap [Tracepoint event]
block:block_rq_requeue [Tracepoint event]
block:block_split [Tracepoint event]
block:block_touch_buffer [Tracepoint event]
block:block_unplug [Tracepoint event]
bpf_test_run:bpf_test_finish [Tracepoint event]
bpf_trace:bpf_trace_printk [Tracepoint event]
bridge:br_fdb_add [Tracepoint event]
bridge:br_fdb_external_learn_add [Tracepoint event]
bridge:br_fdb_update [Tracepoint event]
bridge:fdb_delete [Tracepoint event]
btrfs:__extent_writepage [Tracepoint event]
btrfs:add_delayed_data_ref [Tracepoint event]
btrfs:add_delayed_ref_head [Tracepoint event]
btrfs:add_delayed_tree_ref [Tracepoint event]
......
我们也可以查看 io enter 相关的 syscall 系统调用
$ perf list tracepoint --details | grep -e "syscalls:sys_enter_io_.*"
syscalls:sys_enter_io_cancel [Tracepoint event]
syscalls:sys_enter_io_destroy [Tracepoint event]
syscalls:sys_enter_io_getevents [Tracepoint event]
syscalls:sys_enter_io_pgetevents [Tracepoint event]
syscalls:sys_enter_io_setup [Tracepoint event]
syscalls:sys_enter_io_submit [Tracepoint event]
syscalls:sys_enter_io_uring_enter [Tracepoint event]
syscalls:sys_enter_io_uring_register [Tracepoint event]
syscalls:sys_enter_io_uring_setup [Tracepoint event]
perf trace
perf trace 是 perf 工具的一部分, 用于实时跟踪和显示进程的系统调用、信号事件、以及其他与内核交互的事件。它提供了类似于 strace
的功能, 但在性能分析的上下文中, 它具有更低的开销, 并能与 perf 工具的其他功能集成使用。
perf trace 的作用:
- 跟踪系统调用: perf trace 可以跟踪进程发出的系统调用(如
read()
,write()
,open()
等), 帮助分析进程与操作系统之间的交互。 - 跟踪信号事件: 可以跟踪信号的发送与接收, 帮助诊断进程的信号处理和信号处理程序的行为。
- 性能分析: 通过结合其他 perf 命令, perf trace 可帮助定位性能瓶颈, 分析系统调用的开销、延迟和频率等。
- 实时监控: perf trace 提供实时的系统调用跟踪, 允许开发人员在程序执行过程中对其行为进行实时分析。
我们可以执行 perf trace --help
查看命令描述. 稍微有点多 :)
$ perf trace --help
Usage: perf trace [<options>] [<command>]
or: perf trace [<options>] -- <command> [<options>]
or: perf trace record [<options>] [<command>]
or: perf trace record [<options>] -- <command> [<options>]
-a, --all-cpus # 系统范围的收集,监控所有 CPU
-C, --cpu <cpu> # 指定要监控的 CPU 列表
-D, --delay <n> # 在程序启动后等待 n 毫秒再开始测量
-e, --event <event> # 事件或系统调用选择器。使用 'perf list' 查看可用的事件
-f, --force # 强制执行,不会报错,直接执行
-F, --pf <all|maj|min> # 跟踪页面故障(Pagefaults),可以选择跟踪所有页面故障、重大页面故障或轻微页面故障
-G, --cgroup <name> # 只监控指定 cgroup 名称中的事件
-i, --input <file> # 分析指定文件中的事件
-m, --mmap-pages <pages> # 设置 mmap 数据页的数量
-o, --output <file> # 设置输出文件名
-p, --pid <pid> # 跟踪指定进程 ID(PID)中的事件
-s, --summary # 仅显示系统调用摘要及其统计信息
-S, --with-summary # 显示所有系统调用以及带有统计信息的摘要
-t, --tid <tid> # 跟踪指定线程 ID(TID)中的事件
-T, --time # 显示完整的时间戳,而不是相对于开始的时间
-u, --uid <user> # 跟踪指定用户(UID)的进程
-v, --verbose # 增加输出的详细信息
--call-graph <record_mode[,record_size]> # 设置并启用调用图(堆栈链/回溯):
# record_mode: 调用图记录模式(如 fp、dwarf、lbr)
# record_size: 如果记录模式是 'dwarf',则为最大堆栈记录大小(字节)
# 默认:8192(字节)
# 默认:fp
--comm # 显示线程的 COMM 名称,紧跟其 ID 后
--duration <float> # 仅显示持续时间大于 N.M 毫秒的事件
--errno-summary # 显示每个系统调用的 errno 统计信息,与 -s 或 -S 一起使用
--expr <expr> # 要跟踪的系统调用/事件列表
--failure # 仅显示失败的系统调用
--filter <filter> # 事件过滤器
--filter-pids <CSV list of pids> # 按内核过滤 PID 列表
--kernel-syscall-graph # 显示系统调用退出路径中的内核调用链
--libtraceevent_print # 使用 libtraceevent 打印跟踪点的参数
--map-dump <BPF map> # 定期转储指定的 BPF map
--max-events <n> # 设置要打印的最大事件数,达到后退出
--max-stack <n> # 设置调用链解析时的最大堆栈深度,超过此深度的部分将被忽略,默认值:8192
--min-stack <n> # 设置调用链解析时的最小堆栈深度,低于此深度的部分将被忽略
--no-inherit # 子任务不会继承计数器
--print-sample # 打印 PERF_RECORD_SAMPLE PERF_SAMPLE_ 信息,供调试使用
--proc-map-timeout <n> # 每个线程的 proc mmap 处理超时,单位为毫秒
--sched # 显示调度器阻塞事件
--show-on-off-events # 显示开关事件,与 --switch-on 和 --switch-off 一起使用
--sort-events # 在处理事件之前对事件进行排序,若事件顺序错误时使用
--switch-off <event> # 在该事件发生后停止考虑该事件
--switch-on <event> # 在该事件发生后开始考虑该事件
--syscalls # 跟踪系统调用
--tool_stats # 显示工具的统计信息
一个最佳实践: 后续我们需要查看应用的系统调用却又不知道从何下手时, 我们可以先执行 perf trace -p <PID>
查看应用当前的行为, 再通过 -e [event,]
过滤出指定的 perf event 事件
还记得文章开头准备的程序和上一小节的内容吗? 此时就派上用场了.
此时我们想看 app 应用执行过程中 connect
的系统调用:
- 确定
connect
的系统调用 tracepoint, 可以通过perf list tracepoint | grep connect
查阅 - 执行
perf trace
检测应用
$ perf list tracepoint | grep connect
interconnect:icc_set_bw [Tracepoint event]
interconnect:icc_set_bw_end [Tracepoint event]
# 我们就查看 enter_connect 吧!
syscalls:sys_enter_connect [Tracepoint event]
syscalls:sys_exit_connect [Tracepoint event]
xdp:mem_connect [Tracepoint event]
xdp:mem_disconnect [Tracepoint event]
# 可以通过 -p 参数附加到指定进程
# 当然也可以使用 perf trace -e connect ./app
$ perf trace -e syscalls:sys_enter_connect ./app
0.000 app/440315 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 110)
0.070 app/440315 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 110)
3.960 app/440315 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16)
4.224 app/440317 syscalls:sys_enter_connect(fd: 6, uservaddr: { .family: UNSPEC }, addrlen: 16)
35.464 :440319/440319 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 28)
35.539 :440319/440319 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 28)
35.576 :440319/440319 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16)
35.628 :440319/440319 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16)
35.817 :440319/440319 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16)
针对列字段我们进行解释
- 第一列 => 时间戳: 这是事件的时间戳,表示事件发生的时间,单位是毫秒(ms)。
- 第二列 => 进程标识: app 是执行该进程的可执行文件的名称(或者进程的标识符)。440315 是该进程的 PID(进程 ID),即此时正在执行该 connect 系统调用的进程的标识符。
- 第三列 => 事件类型以及参数 (syscalls:sys_enter_connect):这是 perf 跟踪的系统调用事件类型,在这个例子中是 sys_enter_connect,表示 connect() 系统调用的进入事件。sys_enter_connect 是指当系统开始执行 connect 系统调用时的入口事件。
- fd: 传递给 connect() 系统调用的套接字文件描述符(fd)
- uservaddr: 指向套接字连接的地址的指针
- addrlen: uservaddr 结构的长度,单位是字节。
更进一步我们想看 app 应用执行过程中 openat
的系统调用, 并且想知道其调用链关系
# 通过 --call-graph fp 指定基于帧指针回溯调用栈(适用于大部分操作系统)
$ perf trace -e openat --call-graph fp ./app
......
13.016 ( 0.015 ms): app/461673 openat(dfd: CWD, filename: 0xcfae0, flags: RDONLY|CLOEXEC) = 3
internal/runtime/syscall.Syscall6 (/code/golang/cook/app)
syscall.Syscall6 (/code/golang/cook/app)
syscall.openat (/code/golang/cook/app)
os.open (/code/golang/cook/app)
os.openFileNolog.func1 (/code/golang/cook/app)
os.openFileNolog (/code/golang/cook/app)
os.OpenFile (/code/golang/cook/app)
main.catFile (/code/golang/cook/app) # <== 回溯到我们在 main.go 中调用的函数栈帧(逆向回溯)
main.main (/code/golang/cook/app)
runtime.main (/code/golang/cook/app)
runtime.goexit.abi0 (/code/golang/cook/app)
--call-graph
是啥?perf trace 的
--call-graph
选项用于启用 调用图(call graph)记录,允许你跟踪系统调用(或其他事件)的调用链。通过这个选项,你可以看到不仅是每个事件本身,还能了解这个事件是如何被调用的,具体的调用路径是什么。它非常有助于深入分析程序的执行路径,帮助定位性能瓶颈。该选项有两个主要的参数:
- record_mode:定义调用图的记录模式。
- fp:基于 帧指针(Frame Pointer) 的调用图。适用于大多数系统,可以通过帧指针回溯调用栈. (这是默认行为)
- dwarf:基于 DWARF 调试信息 的调用图。需要可用的调试符号信息(比如通过 -g 编译时保留的符号)。可以提供更精确的调用栈。
- lbr:基于 硬件性能计数器(Last Branch Record) 的调用图。利用硬件支持的功能记录函数的调用路径,能够捕获更底层的细节,但支持的硬件较少。
- record_size:这个参数只有在选择了 dwarf 作为 record_mode 时才有意义,用于指定记录栈大小的最大字节数。默认值是 8192 字节(即 8KB)
perf top
perf top 用于实时显示系统中耗费 CPU 时间的函数或代码热点。它是性能分析的一个非常有用的工具,能够快速识别系统中的性能瓶颈。
具有以下特点:
- 实时查看热点: 周期性地采样 CPU 的执行状态,并输出当前占用 CPU 时间最多的函数、代码路径或者符号.
- 内核和用户空间的分析: perf top 不仅能够分析用户空间程序,还能够分析内核空间的函数(如系统调用),这对于调试内核性能问题尤为重要。
- 低开销分析: perf top 的采样方法是基于硬件性能计数器的,具有较低的开销,因此可以在生产环境中长期运行而不影响系统性能。
- 可视化分析: perf top 会输出一个类似于 top 命令的动态实时界面,可以按 CPU 使用率、函数名、模块等维度进行排序,便于开发者直观地看到代码性能瓶颈。
我们可以执行 perf top --help
查看命令描述, 这个命令的帮助信息也稍微有点多 :)
$ perf top --help
Usage: perf top [<options>]
-a, --all-cpus # 从所有 CPU 进行系统范围的数据收集
-b, --branch-any # 采样所有被执行的分支
-c, --count <n> # 事件采样周期
-C, --cpu <cpu> # 要监控的 CPU 列表
-d, --delay <n> # perf top 界面的刷新频率
-D, --dump-symtab # 转储用于分析的符号表
-E, --entries <n> # 显示此数量的函数
-e, --event <event> # 事件选择器, 可以过滤 `perf list` 列出的 events
-f, --count-filter <n> # 仅显示事件次数超过此值的函数
-F, --freq <freq or 'max'> # perf top 每秒采样频率配置.(-F 1000 代表每秒采样 1000次, -F max 使用系统能够支持的最大采样频率)
-g # 启用调用图记录和显示
-G, --cgroup <name> # 仅监控指定 cgroup 名称中的事件
-i, --no-inherit # 子任务不继承计数器
-j, --branch-filter <branch filter mask> # 分支堆栈过滤模式
-K, --hide_kernel_symbols # 隐藏内核态的程序符号
-k, --vmlinux <file> # vmlinux 文件路径
-M, --disassembler-style <disassembler style> # 指定反汇编风格(例如,-M intel 表示 intel 语法)
-m, --mmap-pages <pages> mmap 数据页数量
-n, --show-nr-samples # 显示样本数量列
-p, --pid <pid> # attach 到指定的 PID 上分析
-r, --realtime <n> # 以该 RT SCHED_FIFO 优先级收集数据
-s, --sort <key[,key2...]> # 按键排序: pid, comm, dso, symbol, parent, cpu, srcline......
-t, --tid <tid> # 在指定线程 ID 上分析事件
-U, --hide_user_symbols # 隐藏用户态的程序符号
-u, --uid <user> # 分析指定用户的事件
-v, --verbose # 显示更详细的信息(如显示计数器打开错误等)
-w, --column-widths <width[,width...]> # 不自动调整列宽,使用这些固定的列宽值
-z, --zero # 在更新时清除历史数据
--all-cgroups # 记录 cgroup 事件
--asm-raw # 显示汇编指令的原始编码(默认)
--call-graph <record_mode[,record_size],print_type,threshold[,print_limit],order,sort_key[,branch]>
# 设置并启用调用图(堆栈链/回溯):
record_mode: 调用图记录模式(fp|dwarf|lbr)
record_size: 如果记录模式是 'dwarf',最大堆栈记录大小(<字节>)默认值:8192(字节)
print_type: 调用图打印样式(graph|flat|fractal|folded|none)
threshold: 调用图包含的最小阈值(<百分比>)
print_limit: 最大调用图条目数(<数字>)
order: 调用图顺序(caller|callee)
sort_key: 调用图排序键(function|address)
branch: 将最后的分支信息包含到调用图中(branch)
value: 调用图值(percent|period|count)
默认值:fp,graph,0.5,caller,function
--children # 累积子任务的调用链并显示总的开销
以上是常用的
<options>
, 感兴趣的可以自行去了解一下~
现在我们准备一个程序用于被 perf top
进行性能分析:
// go build .
package main
func main() {
go HandlerConnect()
select {}
}
//go:noinline
func HandlerConnect() {
for {
HandlerQuery()
}
}
//go:noinline
func HandlerQuery() {}
需要加入
//go:noinline
避免编译器将我们的函数内联优化了, 以导致找不到对应的符号
此时我们将其运行起来, 然后尝试使用 perf top 分析该程序
# 以 4000Hz 频率收集进程 17068 的信息, 每 1s 同步到 screen 上,
# 隐藏内核的符号, 并展示其调用链
$ perf top -F 4000 -K -d 1 -g -p 17068

此时可以按下 h
键, 显示帮助详情
按键 | 功能描述 |
---|---|
k | 放大并查看内核符号(只显示内核的符号) |
a | 注释当前符号(尝试显示该函数的源代码并标注出热点) |
C | 折叠所有调用链 |
L | 更改百分比限制, 换句话说用于过滤执行百分比的项 |
M | 切换显示的采样状态,包括每秒采样的次数 |
Enter | 选择当前行的详细信息 |
Space | 选择当前行,显示更多详细信息 |
+ | 展开当前函数的调用链(显示调用栈) |
e | 展开/折叠当前函数的调用链(显示或隐藏调用栈) |
c | 展开/折叠当前函数的调用链(显示或隐藏调用栈,等同于 e ) |
… | … |
我们可以针对 Self 列执行热点最高的行按下 Enter 键进入下一级选择:
# 对 main.HandlerConnect 函数进行注释.
# 选择这个选项后,perf 会尝试显示该函数的源代码并标注出热点(即最消耗 CPU 时间的行)
# 这种方式通常用于深入了解函数内部的性能瓶颈, 帮助开发者优化代码。需要 `符号信息` 和 `源代码可用` 才能显示注释
- Annotate main.HandlerConnect
# 放大并查看 perf 动态共享对象(DSO)
- Zoom into perf DSO (use the 'k' hotkey to zoom directly into the kernel)
# 展开 main.HandlerConnect 的调用链
- Expand [main.HandlerConnect] callchain (one level, same as '+' hotkey, use 'e'/'c' for the whole main level entry)
# 查看程序的内存映射信息, 你可以了解程序在内存中的布局
- Browse map details
- Exit
那么我们选择第一项(等同于在 perf top screen 页面按下 a
键), 对 smain.HandlerConnect
函数进行标注查看其热点汇编代码
可以看到 perf top 提示我们其热点汇编代码, 那么我们可以执行以下命令进一步分析
# 查看其汇编代码上下20行的上下文
$ go tool objdump -S perf | grep "CALL main.HandlerQuery" -A 20 -B 20

注意: 我们需要确保编译后程序的符号表和调试信息没有被剥离!
可以通过
$file <binary>
查看详情:如果其输出:
application: ......, not stripped
代表调试信息未被剥离