【性能优化】使用perf工具精准定位CPU高负载问题
1. 从“CPU爆了”到“凶手是谁”:为什么你需要perf
最近在后台收到不少朋友的私信,说自己的服务器或者嵌入式设备,跑着跑着CPU使用率就飙到90%甚至100%了。风扇呼呼转,系统卡成PPT,但一看进程列表,除了一个叫kworker或者某个自己写的服务进程占着CPU不撒手,其他信息一概不知。这时候你是不是也一头雾水?重启大法好,但治标不治本,过一阵子“高烧”又来了。
这种场景我遇到过太多次了。早期我也只会用top或者htop看看哪个进程最“凶”,但这就好比只知道“有人犯罪”,却不知道“他具体干了什么坏事”。进程只是一个执行单元,真正消耗CPU的,是它内部执行的函数、调用的系统调用、或者陷入的某个内核逻辑。这时候,你就需要一个“福尔摩斯”级别的工具,深入进程内部,把那个最耗CPU的“元凶函数”给揪出来。这个工具,就是perf。
perf是Linux内核自带的性能剖析工具,它就像给系统做的一次“全身体检”或者“动态CT扫描”。它不只能看CPU,还能分析内存、I/O、调度,但今天我们聚焦最头疼的CPU高负载问题。它的强大之处在于,可以告诉你CPU时间到底花在了哪里:是用户空间的某个字符串处理函数循环了太多次?还是内核里某个驱动在疯狂轮询一个寄存器?知道了“病灶”的具体位置,优化才能有的放矢。
很多人觉得perf是内核开发者才用的高端工具,其实不然。对于任何需要让程序跑得更快、更省资源的开发者或运维来说,它都是必备技能。接下来,我就手把手带你,用perf走完一次完整的CPU高负载问题侦破之旅。我们从安装开始,到采集数据、分析火焰图,最后定位并修改代码,整个过程就像破案一样有意思。
2. 磨刀不误砍柴工:perf的安装与快速验证
工欲善其事,必先利其器。首先我们得把perf这个“侦探”请到我们的系统里。安装方法因系统而异,但总的来说分两大类:在通用Linux服务器(如Ubuntu、CentOS)上安装,和为嵌入式设备交叉编译。
2.1 在Ubuntu/CentOS等服务器上安装
在常见的发行版上,安装perf非常简单,基本上就是几条命令的事。但有个小坑需要注意:perf工具和当前运行的内核版本是强绑定的,你必须安装和你uname -r显示的内核版本号一致的linux-tools包。
以Ubuntu为例,你可以这样操作:
# 首先更新软件包列表
sudo apt update
# 安装基础工具包
sudo apt install linux-tools-common
# 查看你当前系统的精确内核版本
uname -r
# 假设输出是 5.4.0-100-generic
# 安装对应版本的perf工具
sudo apt install linux-tools-5.4.0-100-generic
安装完成后,输入perf --version验证一下。如果看到版本信息,恭喜你,侦探已就位。
对于CentOS/RHEL系列,使用yum或dnf安装:
sudo yum install perf
# 或者
sudo dnf install perf
通常这些仓库里的perf版本会和系统内核匹配。
2.2 为嵌入式设备交叉编译perf
这是很多做嵌入式开发的朋友会遇到的情况。你的程序跑在ARM或MIPS的开发板上,但perf需要在那个环境里运行。板子资源有限,通常我们会在功能强大的x86编译机上,为ARM板子编译一个静态链接的perf工具,然后拷贝到板子上用。
怎么做呢?perf的源码就在Linux内核源码树的tools/perf/目录下。所以你需要先获取你的设备所使用的内核源码。
编译命令看起来有点长,但结构很清晰:
# 进入你的内核源码目录
cd /path/to/your/linux-kernel-src
# 执行编译,关键是指定架构和交叉编译工具链
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- perf LDFLAGS+=--static NO_LIBELF=1 V=1 WERROR=0 NO_SLANG=1 NO_GTK2=1 NO_LIBAUDIT=1 NO_LIBNUMA=1 NO_LIBPERL=1
我来解释一下这几个参数:
ARCH=arm64: 指定目标CPU架构是ARM64。CROSS_COMPILE=aarch64-linux-gnu-: 指定交叉编译工具链的前缀。你的工具链可能是arm-linux-gnueabihf-或其他,这里要替换成你自己的。LDFLAGS+=--static: 强制静态链接。这个非常重要,它会把perf依赖的所有库都打包进最终的可执行文件,这样生成的是一个独立的perf二进制文件,可以直接扔到任何同架构的板子上运行,无需担心板子上缺少动态库。- 后面一串
NO_XXX=1: 是为了禁用一些非必要的依赖(如图形界面、特定语言绑定),让编译过程更简单,生成的文件也更小。对于基础CPU分析,这些功能不需要。
编译完成后,生成的perf二进制文件就在当前目录下。用file perf命令检查一下,确认是ARM64架构的静态可执行文件,然后就可以通过scp等方式传到嵌入式设备上了。
3. 案发现场:使用perf record采集“犯罪证据”
安装好perf,我们就要开始“办案”了。第一步是现场取证,也就是在CPU高负载发生时,记录下系统的执行快照。这个命令就是perf record。
最常用、也最有效的命令格式是这样的:
sudo perf record -g -a sleep 10
别小看这短短一行,信息量巨大:
sudo: 因为perf需要访问内核的性能计数器,所以通常需要root权限。-g: 代表记录调用图(call-graph)。这是关键!它不光记录哪个函数耗时,还记录了这个函数是被谁调用的,调用链是什么。有了调用链,你才能理解上下文,知道为什么这个函数会被频繁执行。-a: 代表监控所有CPU(all CPUs)。如果你的问题只出现在某个核心,也可以指定-C 0来只监控0号CPU。sleep 10: 这是告诉perf记录多长时间。这里记录10秒。你可以根据问题发生的频率调整,如果问题是间歇性的,可能需要记录更长时间(比如60秒)。
执行这条命令后,perf会安静地运行10秒钟,在这期间,它会以极高的频率(通常每秒几千次)对所有CPU进行采样,询问“你现在正在执行哪个函数?”。10秒后,它会生成一个名为perf.data的二进制数据文件。这个文件就是我们的“现场录像”。
这里有几个实战小技巧:
- 抓准时机:最好在CPU负载已经升高的时候开始记录。你可以先用
top观察,等目标进程的CPU使用率上去后再运行perf record。 - 控制文件大小:默认采样频率很高,记录时间长可能会生成巨大的
perf.data文件。可以用-F参数控制采样频率,例如-F 99表示每秒采样99次。对于大多数问题,99Hz或499Hz已经足够清晰,又能有效控制文件大小。 - 精准监控:如果你已经知道是哪个进程(PID)有问题,可以用
-p PID来只监控该进程,这样数据更干净,分析起来更快。
4. 分析证据:perf report与火焰图可视化
拿到perf.data这个“录像带”后,接下来就是“复盘分析”。最直接的方式是使用perf report命令。
直接在采集数据的终端里输入:
perf report
这会启动一个交互式的文本界面。你会看到一个按开销百分比排序的函数列表,通常排在第一位的,就是消耗CPU最多的“嫌疑犯”。这就像原始文章里发现的fsl_scan_event函数。
在perf report界面里,你可以:
- 按上下键选择不同的函数条目。
- 按回车键可以展开该函数的调用链,看看它是被谁调用的。
- 按
a键可以注解,显示该函数的汇编代码(需要debug符号)。 - 按
q键退出。
文本界面虽然强大,但对于复杂的调用关系,看起来还是不够直观。这时候,就需要更强大的可视化工具——火焰图(Flame Graph)。
火焰图是Brendan Gregg大神推广的神器,它把perf record采集到的调用栈信息,变成一张层层堆叠的火焰状图片。Y轴表示调用栈的深度,X轴表示采样到的次数(即耗时),每个矩形代表一个函数。矩形越宽,表示这个函数(或其子函数)消耗的CPU时间越多。
生成火焰图需要几个步骤,但一旦脚本准备好,就非常方便:
# 1. 用perf script命令将perf.data转换为可读的文本格式
perf script -i perf.data > perf.script.out
# 2. 使用FlameGraph工具包中的stackcollapse-perf.pl脚本折叠调用栈
./FlameGraph/stackcollapse-perf.pl perf.script.out > perf.folded.out
# 3. 生成SVG格式的火焰图
./FlameGraph/flamegraph.pl perf.folded.out > perf_flame.svg
然后把perf_flame.svg用浏览器打开,你就能看到一张全貌图。看图秘诀是:不要看最高的火焰,而要看最宽的火焰。 最宽的那块,就是你的性能瓶颈所在。你可以用鼠标悬停在任何一块“火焰”上,会显示详细的函数名和耗时百分比。点击某个矩形可以放大查看。
我第一次用火焰图定位到一个JSON解析库里的内存拷贝函数占用过高时,那种“原来是你!”的豁然开朗感,至今难忘。它让抽象的百分比数据,变成了直观的、可交互的“热力图”。
5. 实战案例拆解:从函数到代码行的深度追踪
光知道哪个函数耗时高还不够,我们得找到具体的代码行,甚至是指令。我们结合一个类似原始文章的案例来走一遍。
假设我们通过perf report或火焰图,发现一个叫device_polling_loop的函数占用了超过60%的CPU时间。perf report里显示它属于我们自己的内核模块my_driver.ko。
第一步:确认函数和调用来源。
在perf report里展开这个函数,我们看到它的调用链是:irq_handler -> handle_interrupt -> device_polling_loop。这说明它是在中断处理路径中被调用的。
第二步:获取带调试符号的信息。
perf要显示具体的函数名和代码行,需要调试符号。对于我们自己编译的程序或内核模块,需要在编译时加上-g选项。对于系统库,可能需要安装-dbgsym或-debuginfo包(如Ubuntu的linux-image-$(uname -r)-dbgsym)。
第三步:使用perf annotate进行源码级注解。
这是perf更进阶但极其有用的功能。在perf report界面选中device_polling_loop函数,然后按a键。或者直接用命令:
perf annotate -i perf.data --symbol=device_polling_loop
如果调试信息齐全,perf会尝试显示这个函数的汇编代码,并在旁边标注每条指令的耗时百分比。你会惊讶地发现,耗时可能集中在某几条指令上,比如一个内存屏障(barrier()),或一个原子操作(atomic_inc)。
第四步:结合源码分析。
现在我们打开my_driver.c的源码,找到device_polling_loop函数。果然,里面有一个这样的循环:
static void device_polling_loop(struct my_device *dev) {
while (!(readl(dev->reg_base + STATUS_REG) & READY_FLAG)) {
// 空循环,等待设备就绪
cpu_relax(); // 一个让CPU缓口气的宏
}
// ... 处理数据
}
问题很明显了:这是一个忙等待(busy-wait)轮询。它不断地、疯狂地读取一个硬件状态寄存器,直到设备就绪。在设备慢或者出问题时,这个循环就会像脱缰野马一样吃掉整个CPU核心。
第五步:优化与验证。 原始文章的解决方案是调整轮询间隔(从4ms改为10ms)。这是一个方法,但治标不治本。更优的方案是:
- 中断驱动:让硬件在就绪时主动发起中断,CPU在此期间可以去处理其他任务,这是最理想的方式。
- 休眠轮询:如果必须轮询,在每次检查之间加入
usleep_range(100, 200)这样的微小休眠,哪怕睡眠几十微秒,也能极大地降低CPU占用。 - 使用内核延迟工作队列:将轮询任务放到一个可以调度的上下文中,而不是死循环。
修改代码后,重新编译、部署,再次运行perf record和perf report,你会看到device_polling_loop的CPU占比从60%暴跌到接近0%,优化效果立竿见影。
6. 进阶技巧与常见坑点
用了这么久perf,我也踩过不少坑,这里分享几个进阶技巧和避坑指南,能让你用得更顺手。
技巧一:关注系统调用(syscall)开销。
有时候,用户态函数本身不耗时,但它频繁调用的系统调用很耗时。可以用perf record -e 'syscalls:sys_enter_*'来追踪系统调用。我曾经用这个发现一个日志服务因为write系统调用过于频繁,且每次写入字节数太少,导致上下文切换开销巨大。合并写操作后,性能提升显著。
技巧二:使用perf stat进行宏观统计。
在优化前后,用perf stat命令跑一遍你的程序,可以对比关键的硬件事件数,比如:
perf stat -e cycles, instructions, cache-misses, branch-misses ./your_program
看看优化后,是否减少了CPU周期(cycles)、提高了每周期指令数(IPC)、降低了缓存未命中率(cache-misses)。这能从计算机体系结构层面给你优化的正反馈。
技巧三:注意符号表与栈展开。
这是最大的坑之一。有时候perf report里看到一堆[unknown]或者函数名是乱码。原因有:
- 没有调试符号。确保程序用
-g编译,或者安装对应的debug包。 - 栈被破坏,或编译器优化(如尾调用优化、帧指针省略)导致perf无法正确展开调用栈。在编译时尝试加上
-fno-omit-frame-pointer选项禁用帧指针优化,能极大提高perf采集调用链的成功率。
技巧四:区分CPU占用与CPU等待。
perf主要统计的是CPU繁忙的时间。但有时候程序慢不是因为CPU忙,而是因为在等I/O(磁盘、网络)。这时候CPU占用率看起来不高,但程序就是卡。这种情况需要用其他工具(如iotop, bcc工具集中的bitesize)来定位I/O问题。不过perf的perf record -e sched:sched_stat_blocked等事件也能辅助分析调度阻塞。
技巧五:长期监控与脚本化。
对于生产环境,你可以写一个简单的shell脚本,定期(比如每分钟)执行perf record -g -a sleep 5,并将perf.data归档。当出现问题时,就有历史数据可以回溯分析,而不是只能等到问题复现时手忙脚乱地去抓取。
perf工具生态非常丰富,除了今天讲的record/report/annotate,还有trace(追踪特定事件)、mem(分析内存)、sched(分析调度器)等子命令。掌握它,就像是获得了一本Linux系统性能的“内部调试手册”。最开始可能会被它的输出信息量吓到,但多练几次,从解决一个个具体的小问题开始,你会越来越得心应手。记住,性能优化的第一步,永远是精准测量,而perf就是你手中最强大的那把尺子。
更多推荐
所有评论(0)