4、CPU瓶颈定位:CPU使用率过高、上下文切换频繁、中断风暴、缓存命中率低的分析方法与案例
大家好,我是老李。做嵌入式这些年,我见过太多系统跑着跑着就“卡死”了。你敲个命令,半天没反应。或者明明CPU频率挺高,吞吐量就是上不去。这时候,八成是CPU出了状况。
CPU瓶颈,说白了就是它忙不过来了。但“忙”也分好几种。是纯粹的计算量太大?还是频繁地在任务间跳来跳去?又或者被中断给淹没了?甚至,它可能一直在“假忙”——等数据从内存里搬过来。
今天,我就把这四种最常见的CPU瓶颈,掰开揉碎了讲清楚。每个问题我都会给出定位方法和实战案例。
核心观点:CPU瓶颈不只是“占用率高”这么简单。上下文切换、中断、缓存,这三兄弟经常联手搞事情。你得学会区分它们。
4.1 CPU使用率过高——别被数字骗了
很多人一看到CPU 100%就慌了。其实,你得先搞清楚:是用户态(user)高,还是内核态(sys)高?
用户态高,说明你的业务代码在疯狂计算。比如死循环、复杂的数学运算、或者低效的算法。
内核态高,问题就复杂了。可能是系统调用太频繁,比如频繁读写文件、创建线程。也可能是驱动有bug,或者锁竞争激烈。
我个人习惯先用 top 看一眼。然后按 1 看每个核的负载。如果某个核跑满了,其他核闲着,那大概率是单线程瓶颈。
我的小技巧:用 perf top -g 看热点函数。它能直接告诉你,CPU到底在哪个函数里“烧”时间。我在项目中遇到过,一个字符串处理函数因为没做缓存,每次都要重新计算,直接把CPU干到了90%。
案例:一个死循环引发的血案
有一次,同事说他的程序跑着跑着就卡住了。我上去一看,CPU 100%。用 perf top 一看,热点全在一个叫 process_data() 的函数里。
# perf top -g
Samples: 1M of event 'cycles', Event count (approx.): 850000000000
Overhead Shared Object Symbol
45.20% my_app [.] process_data
12.30% [kernel] [k] _raw_spin_lock
8.50% my_app [.] main_loop
我让他把代码发过来。一看,好家伙,一个 while(1) 循环里,条件判断写错了,永远为真。这就是典型的用户态CPU飙升。
修复很简单,把条件改对就行。但定位过程,靠的就是 perf 这把快刀。
4.2 上下文切换频繁——系统在“空转”
上下文切换,就是CPU从一个任务切到另一个任务。每次切换都要保存和恢复寄存器、刷新TLB。这本身就有开销。
如果切换太频繁,CPU大部分时间都在“做家务”,而不是干正事。你想想看,这效率能高吗?
怎么看?用 vmstat 1。关注 cs(context switch)这一列。如果每秒切换几万次甚至几十万次,那肯定有问题。
# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 123456 7890 45678 0 0 10 20 3000 45000 15 85 0 0 0
你看这个例子,cs 高达 45000,sy(内核态)占了85%。这说明系统大部分时间在内核里做切换。用户态只占了15%。
注意:上下文切换高,不一定就是坏事。如果系统里确实有几百个线程在并行工作,切换高是正常的。关键要看 us 和 sy 的比例。如果 sy 远高于 us,那就要警惕了。
我曾经在一个网络服务器项目里,发现上下文切换高达每秒10万次。用 pidstat -w 一查,是一个日志线程在疯狂地写文件。每次写一行,就触发一次系统调用,然后切换出去。后来改成批量写入,切换次数直接降到了5000。
4.3 中断风暴——硬件在“敲门”
中断,是硬件通知CPU“我有事”的方式。比如网卡收到一个包,就会触发一个中断。
正常情况下,中断频率是可控的。但如果某个硬件出了问题,或者驱动配置不对,中断可能会像潮水一样涌来。这就是“中断风暴”。
CPU被中断淹没,根本没时间处理正经任务。系统会变得极其卡顿,甚至完全无响应。
定位方法很简单:看 /proc/interrupts。
# cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 100 200 150 180 IO-APIC 2-edge timer
1: 10 5 8 7 IO-APIC 1-edge i8042
29: 5000000 5000000 5000000 5000000 PCI-MSI 512000-edge eth0
看到 eth0 这一行了吗?每个CPU都处理了500万个中断。这频率太高了。正常情况下一秒钟几千个就算多了。
为什么会这样?可能是网卡开启了“中断合并”功能,但没生效。或者驱动有bug,导致每个小包都触发一次中断。
我的建议:对于高速网络设备,一定要开启 NAPI 或者中断合并。把多个小包合并成一个中断,能大幅降低CPU负载。我在一个视频流服务器上,光靠调整中断合并参数,就把CPU使用率从80%降到了30%。
4.4 缓存命中率低——CPU在“等米下锅”
这个最隐蔽。CPU使用率不高,上下文切换也不多,但系统就是慢。为什么?因为CPU在等数据。
现代CPU有三级缓存:L1、L2、L3。如果数据在L1里,CPU一个时钟周期就能拿到。如果不在,就要去内存拿,那得等几十甚至上百个时钟周期。
缓存命中率低,意味着CPU大部分时间都在“空等”。你看到的CPU使用率,其实是在“等”的过程中消耗的。
怎么查?用 perf stat。
# perf stat -e cache-references,cache-misses,cycles,instructions ./my_app
Performance counter stats for './my_app':
100,000,000 cache-references
30,000,000 cache-misses # 30.00% of all cache refs
1,000,000,000 cycles
500,000,000 instructions # 0.50 insn per cycle
0.500000000 seconds time elapsed
看到 cache-misses 了吗?30%的缺失率,太高了。正常应该在5%以下。指令每周期(IPC)只有0.5,说明CPU有一半的时间在等数据。
为什么会这样?通常是数据局部性差。比如你遍历一个链表,每个节点在内存里都是随机分布的。CPU每次都要去内存里拿下一个节点,缓存根本帮不上忙。
优化思路:把链表换成数组。或者用内存池,把相关数据放在连续的内存区域。我优化过一个哈希表,把链式存储改成开放寻址法,缓存命中率从60%提到了95%,性能翻了一倍。
4.5 综合案例:一个“慢”系统的完整诊断
好了,我们来看一个综合案例。假设你有一个嵌入式网关,跑着跑着,网络吞吐量突然掉了一半。
第一步:看全局
用 top 看CPU使用率。发现 sy 占了60%,us 只有20%。这不对劲。
第二步:看上下文切换
用 vmstat 1 看 cs。发现每秒切换8万次。太高了。
第三步:看中断
用 cat /proc/interrupts 看网卡中断。发现每个CPU每秒处理了2万个中断。这频率有点高,但还不算风暴。
第四步:看缓存
用 perf stat 跑一下。发现缓存缺失率高达25%。IPC只有0.4。
诊断结论:问题出在数据布局上。网络数据包的处理函数,访问了多个分散在内存中的数据结构。导致缓存频繁失效。同时,因为数据不在缓存里,处理速度慢,导致上下文切换升高。
优化方案:把相关的数据结构打包到一个连续的内存块里。比如,把 sk_buff 和它的私有数据放在一起。优化后,缓存缺失率降到了8%,IPC提升到了1.2。上下文切换也降到了1万次以下。
你看,很多时候瓶颈不是单一的。它们会互相影响。你得学会从全局看问题,用工具一层层剥开。
最后说一句:工具只是辅助。真正重要的是你的思路。遇到CPU瓶颈,先别急着优化代码。先搞清楚:是计算密集?还是IO密集?还是内存访问密集?方向对了,努力才有意义。