4、CPU瓶颈定位:CPU使用率过高、上下文切换频繁、中断风暴、缓存命中率低的分析方法与案例

大家好,我是老李。做嵌入式这些年,我见过太多系统跑着跑着就“卡死”了。你敲个命令,半天没反应。或者明明CPU频率挺高,吞吐量就是上不去。这时候,八成是CPU出了状况。

CPU瓶颈,说白了就是它忙不过来了。但“忙”也分好几种。是纯粹的计算量太大?还是频繁地在任务间跳来跳去?又或者被中断给淹没了?甚至,它可能一直在“假忙”——等数据从内存里搬过来。

今天,我就把这四种最常见的CPU瓶颈,掰开揉碎了讲清楚。每个问题我都会给出定位方法和实战案例。

核心观点:CPU瓶颈不只是“占用率高”这么简单。上下文切换、中断、缓存,这三兄弟经常联手搞事情。你得学会区分它们。

CPU瓶颈定位 CPU使用率过高 上下文切换频繁 中断风暴 缓存命中率低 top / htop / perf top vmstat / pidstat -w /proc/interrupts / irqbalance perf stat / cachegrind

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%。

注意:上下文切换高,不一定就是坏事。如果系统里确实有几百个线程在并行工作,切换高是正常的。关键要看 ussy 的比例。如果 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 1cs。发现每秒切换8万次。太高了。

第三步:看中断

cat /proc/interrupts 看网卡中断。发现每个CPU每秒处理了2万个中断。这频率有点高,但还不算风暴。

第四步:看缓存

perf stat 跑一下。发现缓存缺失率高达25%。IPC只有0.4。

诊断结论:问题出在数据布局上。网络数据包的处理函数,访问了多个分散在内存中的数据结构。导致缓存频繁失效。同时,因为数据不在缓存里,处理速度慢,导致上下文切换升高。

优化方案:把相关的数据结构打包到一个连续的内存块里。比如,把 sk_buff 和它的私有数据放在一起。优化后,缓存缺失率降到了8%,IPC提升到了1.2。上下文切换也降到了1万次以下。

你看,很多时候瓶颈不是单一的。它们会互相影响。你得学会从全局看问题,用工具一层层剥开。


最后说一句:工具只是辅助。真正重要的是你的思路。遇到CPU瓶颈,先别急着优化代码。先搞清楚:是计算密集?还是IO密集?还是内存访问密集?方向对了,努力才有意义。