☰
性能之巅 导读(六):CPU
2026/9/28 21:25:15 网站建设 项目流程

本文是《性能之巅》(Systems Performance,第 2 版)第 6 章的导读。本书是系统性能领域的经典,本系列逐章导读,把书的核心概念讲清楚。

工厂

上一章讲了重点企业——应用程序。现在,该进工厂了。

CPU 就是城市的工厂——它把指令加工成结果,驱动整座城市运转。但工厂也是最容易被误诊的地方:工厂忙不一定有问题,工厂闲也可能藏着大麻烦。

这一章我们分两部分讲:先搞清楚工厂是怎么运转的(理论),再看怎么给工厂做检查(实操)。


一、工厂是怎么运转的

从一颗芯片说起

你有没有想过,你电脑里那块指甲盖大小的芯片,到底是怎么干活的?

想象你走进一座工厂。这座工厂不大,但里面有几间独立车间。每间车间都能独立接活、独立干活。你看到的"8 核 16 线程",说的就是:一个物理芯片上,有 8 间车间,每间车间里挤了 2 个工人。

但等等——一间车间里坐两个工人,他们不打架吗?

会。但工厂的设计者算过一笔账:工人大部分时间其实在等物料。物料从仓库送过来,可能要等几百个时钟周期。与其让工人干坐着,不如趁他等物料的时候,让他去操作另一台机器。这就是 Intel 的Hyper-Threading(超线程)技术。

所以,操作系统看到的是 16 个"工位",但真正干活的是 8 间车间。两个工人共享同一间车间的资源——如果两个人同时都在做重活,就会互相抢工具、抢物料架,反而拖慢彼此。所以超线程带来的性能提升不是 2 倍,通常只有1.2 到 1.3 倍。

那问题来了:工人具体是怎么干活的?

工人是怎么干活的

工人干活,靠的是指令。你可以把一条指令想象成一张加工单——上面写着"把这两个数加起来"“从仓库取一个物料”“把结果放到成品区”。

一张加工单的执行,分五步:

取指 → 译码 → 执行 → 访存 → 写回
  • 取指:工人去公告板拿下一张加工单
  • 译码:看懂这张单子要干什么
  • 执行:真正动手加工
  • 访存:如果加工需要物料,去物料架取
  • 写回:把加工好的成品放到指定位置

每一步至少花一个时钟周期。其中访存最慢——如果物料不在手边,可能得等几十甚至几百个周期。

但工人不是一张一张单子傻等的。现代工厂有流水线——第一条指令在"执行"的时候,第二条在"译码",第三条在"取指",同时进行。理想情况下,每个时钟周期就能完成一条指令。

但流水线最怕两件事:

第一,分支预测失败。加工单上可能写着"如果物料是红色的,就做 A;否则做 B"。工人在译码阶段就得猜——到底该做 A 还是 B?猜对了,流水线继续;猜错了,整条流水线清空重来,前面做的全白费。

第二,缓存未命中。工人执行到"访存"这一步,发现物料不在手边的架子上,得去车间仓库找;车间仓库也没有,得去中央仓库;中央仓库还没有,得去外面调货——每多一层,就多等几十到几百个周期。

这就是为什么缓存如此重要。

物料架:为什么缓存这么重要

工人旁边有几层物料架,越近越快,越远越慢:

层级比喻大小访问延迟
L1手边的小架子几十 KB1–3 个周期
L2车间里的工具柜几百 KB 到几 MB10–20 个周期
L3车间共用的仓库几十 MB30–50 个周期
主内存中央仓库几十 GB100–300 个周期
磁盘外部仓库TB 级慢几个数量级

你可能会想:几百个周期而已,能有多慢?

让我们算一笔账。假设 CPU 主频是 3 GHz,一个周期约 0.33 纳秒。如果访问 L1 需要 3 个周期,那是 1 纳秒。如果访问主内存需要 200 个周期,那是66 纳秒——差了 66 倍。

更直观的对比:如果 L1 访问相当于你伸手拿桌上的水杯(1 秒),那主内存访问相当于你走到楼下便利店买瓶水(66 秒)。而磁盘访问相当于你开车去另一个城市买水(几小时)。

最要命的是:缓存命中率从 99% 掉到 90%,性能可能腰斩。因为那 1% 的"没命中"要去访问慢速内存,而 CPU 在等的时候什么都干不了。

那工厂怎么知道自己"忙不忙""效率高不高"呢?

工厂的"效率指标"

评价工厂不能只看"忙不忙",要看产出效率。

第一个指标是IPC(每周期指令数)——平均每个时钟周期加工了多少条指令。

  • IPC 高(比如 1.5):工人在高效加工
  • IPC 低(比如 0.2):工人大部分时间在等物料

第二个指标是利用率——工厂有多少时间在运转。

第三个指标是饱和度——多少工人在排队等上工。

你可能会觉得:利用率越高越好?

恰恰相反。一个反直觉的事实是:利用率 100% 但 IPC 只有 0.2,这是浪费——工人在忙着等物料。反过来,利用率 50% 但 IPC 1.5,这是好事——工厂一半时间在高效加工,另一半时间留给突发任务。

这就像一家餐厅:如果桌子永远坐满(利用率 100%),但每桌客人都在等菜(IPC 低),那这家餐厅的效率其实很差。如果一半桌子空着(利用率 50%),但每桌客人来了就上菜(IPC 高),那才是好餐厅。

那如果多个企业都要用工厂,谁先上工呢?

工厂的"调度"

当多个企业都要用工厂时,调度器决定谁先上工。

工作负载分两类:

  • CPU 密集型:计算型任务——科学计算、机器学习。它们一旦上工,就一直占用机器。
  • I/O 密集型:等 I/O 的任务——Web 服务器、文件服务器。它们干一小会儿,就去等 I/O 了。

调度策略是:优先让 I/O 密集型任务先跑。因为它们跑一小会儿就去等 I/O 了,能让出机器给别的任务。而 CPU 密集型任务可以等——它们不急。

现代 Linux 默认用CFS(完全公平调度器)——按"虚拟运行时间"排序,谁运行时间少谁优先。还有RT(实时调度)优先级最高,但风险是:如果 RT 任务进入死循环,整个工厂会卡死。


二、怎么给工厂做检查

理论讲完了,现在看实操。工厂出问题时,你怎么查?

uptime:看一眼"负载"

$uptime9:04pm up268day(s),10:16,2users, load average:7.76,8.32,8.60

三个数字分别是 1 分钟、5 分钟、15 分钟的平均负载。

比较三个数字能看出趋势:

  • 1 分钟 > 15 分钟:负载在上升——问题可能加剧
  • 1 分钟 < 15 分钟:负载在下降——问题可能过去了
  • 三个差不多:负载平稳

注意:Linux 的负载平均值包含等待 I/O 的线程(TASK_UNINTERRUPTIBLE),不是纯 CPU 负载。这是它和 Solaris 的一个区别——看到高负载时,要确认到底是 CPU 还是 I/O。

类比:uptime 像看一眼候工区有多少工人——快速判断"忙不忙"。

vmstat:全厂"仪表盘"

$vmstat1procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpdfreebuff cache si so bi boincs us syidwa st150045173270588866628001104338219700150045096870588866628000612106429697228000150045066070588866632000096129327228000

关键列:

  • r:运行队列长度——大于车间数就饱和
  • us:用户态工厂时间
  • sy:内核态工厂时间
  • id:空闲
  • wa:等 I/O——工厂空闲但等 I/O
  • st:被偷走的时间——虚拟机专属

类比:vmstat 像工厂仪表盘的总览屏——流水线速度、产量、等料时间、设备利用率,一眼全看到。

mpstat:逐个车间看

$ mpstat-PALL1CPU %usr %nice %sys %iowait %irq %soft %steal %guest %idle all32.160.0061.810.000.000.000.000.006.03032.000.0064.000.000.000.000.000.004.00132.320.0059.600.000.000.000.000.008.08

用途:发现"单车间热点"——一个车间 100%,其他车间闲着,说明线程扩展性有问题。

%steal是虚拟机专属:宿主把 CPU 时间偷去给了别的租户。

类比:mpstat 像分别看每个车间的运转情况——发现具体哪一块出问题。

sar:看历史

sar前面章节介绍过,关键是它能看历史数据。

CPU 相关选项:

  • -u:工厂使用率
  • -P ALL:每个车间
  • -q:运行队列
$ sar-u13Linux5.3.0(bgregg)02/01/20 _x86_64_(2CPU)18:00:32 CPU %user %nice %system %iowait %steal %idle18:00:33 all32.160.0061.810.000.006.03

类比:sar 像以前的运营报告——能看"三天前流水线速度多少"。

ps:看每个企业

$psauxUSERPID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root10.00.0237721948? Ss20120:04 /sbin/init... web1172196.50.163811652108pts/1 Rl+ 01:373:33nodeproxy.js

关键列:

  • %CPU:平均工厂使用率(进程生命周期内的平均)
  • TIME:累计工厂时间

注意:%CPU是平均值,不是当前值。要看当前值,用top。

类比:ps 像看每个企业的"生产记录"——这个企业总共用了多少工厂资源。

top:实时监控屏

$toptop- 01:38:11 up63days,1:17,2users, load average:1.57,1.81,1.77Tasks:256total,2running,254sleeping,0stopped,0zombie Cpu(s):2.0%us,3.6%sy,0.0%ni,94.2%id,0.0%wa,0.0%hi,0.2%si,0.0%st PIDUSERPR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND11721web200623m 50m4984R930.10:59.50node

用途:实时找"最忙的企业"。

注意:top自己会消耗工厂资源——它要读所有进程的/proc文件。如果top自己出现在 CPU 消耗榜上,说明系统进程多、开销大。

类比:top 像实时监控屏——谁在满负荷、谁在偷懒,一目了然。

pidstat:按进程拆 CPU 时间

$ pidstat1PID %usr %system %guest %CPU CPU Command781597.032.970.00100.0011gzip78140.001.980.001.983tar

top只能告诉你哪个进程忙,pidstat把时间拆成用户态和内核态:

  • %usr:用户态时间
  • %system:内核态时间
  • %CPU:总共占用

用途:一个gzip进程 97% 用户态时间——说明它在做压缩计算;一个tar进程大部分是内核态——说明它在做 I/O。

类比:pidstat 像把每个企业的工时拆成"实际加工时间"和"办手续时间"。

time / ptime:给单个命令计时

$timecksumubuntu-19.10-live-server-amd64.iso1044945083883949568ubuntu-19.10-live-server-amd64.iso real 0m5.590s user 0m2.776s sys 0m0.359s

三个时间:

  • real:真实经过的时间(墙钟时间)
  • user:用户态 CPU 时间
  • sys:内核态 CPU 时间

如果real远大于user + sys,说明进程在等 I/O——比如第一次运行时文件没缓存。

类比:time 像给一个具体任务掐表——总共花了多久,其中真正干活多久,办手续多久。

turbostat / showboost:看 CPU 频率

CPU 并不是一直跑在最高频率——它有P-state(性能状态)和C-state(电源状态)。频率会动态调整,省电时降频,负载高时升频(Turbo Boost)。

# turbostatCore CPU Avg_MHz Busy% Bzy_MHz TSC_MHz - -972.7036092112001283.4437002112

用途:确认 CPU 是否在跑最高频率。如果发现频率很低,可能 BIOS 里节能设置把频率压住了,性能就不是 CPU 的错,是配置的错。

类比:turbostat 像看工厂机器有没有开到最高档——档位没开满,再好的机器也发挥不出来。

pmcarch / tlbstat:看微架构效率

这两个工具是 PMC(性能监控计数器)的封装,能看到 CPU 底层的效率细节。

# pmcarchK_CYCLES K_INSTR IPC BR_RETIRED BR_MISPRED BMR% LLCREF LLCMISS LLC%96163187871663130.91197309949256791872993.4465659745417431379973.45

关键列:

  • IPC:每周期指令数——效率指标
  • BMR%:分支预测失败率
  • LLC%:最后一级缓存命中率
# tlbstat -C0 1K_CYCLES K_INSTR IPC DTLB_WALKS ITLB_WALKS K_DTLBCYC DTLB% ITLB%28757932760510.10897094966586230278791327.4022.63

DTLB%和ITLB%是地址转换占用 CPU 周期的比例。如果这个数字很高,说明 TLB(地址转换缓存)没命中,每次都要走慢路径。

注意:这两个工具依赖具体 CPU 型号,不一定在所有环境都能用。但思路通用:用 PMC 看 CPU 底层效率,判断瓶颈是计算还是访存。

类比:pmcarch/tlbstat 像给工厂机器做微观体检——不是看它忙不忙,是看它内部零件有没有卡顿。

perf:工厂分析的"主力"

perf是 Linux 官方性能工具,也是 CPU 分析的首选工具。它能做三件大事:采样、计数、追踪。

第一,CPU 剖析(采样)——找出 CPU 时间花在哪:

# 全系统采样栈,99 Hz,30 秒perf record-F99-a-g--sleep30perf report--stdio

输出能看到每个函数的占比,比top更深入——top只能告诉你哪个进程忙,perf能告诉你进程里哪个函数忙。

生成火焰图:

perf script--header>out.stacksgitclone https://github.com/brendangregg/FlameGraphcdFlameGraph ./stackcollapse-perf.pl<../out.stacks|./flamegraph.pl--hash>out.svg

第二,IPC 统计——判断是"忙但低效"还是"忙且高效":

$ perfstatgzipwords209,911,358 cycles288,321,441 instructions# 1.37 insn per cycle

IPC = 1.37 说明效率不错。如果 IPC < 0.2,说明工人在等物料——大概率是内存瓶颈。

第三,调度延迟——看线程等 CPU 等了多久:

# 记录调度事件 10 秒perf sched record --sleep10perf sched latency

输出会显示每个任务的平均调度延迟和最大调度延迟。

注意:perf sched record数据量大——10 秒可能产生 125 MB 的perf.data,因为调度事件非常频繁。生产环境慎用。

类比:perf 像工厂的"全能侦探工具"——既能拍 X 光(剖析),又能测内耗(调度延迟),又能算效率(IPC)。

profile:perf 的轻量替代

profile是 BCC 工具,和perf record类似,但更轻量——它在内核里聚合栈信息,只把唯一栈和计数传给用户态,不用先写perf.data再处理。

# profile 10Sampling at49Hertz of all threads by user + kernel stackfor10secs. finish_task_switch __sched_text_start schedule... - mysqld(5187)151

用途:快速看 CPU 栈分布。输出最后一行是进程名、PID、采样次数。

生成火焰图:

profile-af10>out.stacks ./flamegraph.pl--hash<out.stacks>out.svg

类比:profile 像轻便版 X 光机——不用搬大设备,随时拍一张。

cpudist:每次上工多久

# cpudist 10 1usecs:count distribution0->1:3618650|****************************************|2->3:2704935|*****************************|4->7:421179|****|8->15:99416|*|

用途:看线程每次在 CPU 上待多久。

如果线程跑得又短又碎(大量 <10 μs),说明工厂竞争严重——线程频繁被切出去,缓存永远不热。

注意:这个工具需要 BCC(bcc-tools包)。

类比:cpudist 像看每个工人每次上工能干多久——10 μs 就被叫走,说明车间太挤了。

runqlat:线程等 CPU 等了多久

# runqlat 10 1usecs:count distribution0->1:9017|*****2->3:7188|****...65536->131071:88|<-- 这些是"长等"的线程

用途:看线程从"可运行"到"真正上工"等了多久。

runqlat比cpudist更有意义——因为调度延迟是线程真正感受到的等待。如果大量线程等了几十毫秒才上工,说明 CPU 严重过载。

注意:runqlat采样频繁(每次上下文切换),生产环境要评估开销。轻量替代是runqlen(定时采样)。

类比:runqlat 像看工人在候工区等了多久才被叫上工。

runqlen:运行队列有多长

# runqlen 10 1runqlen:count distribution0:1824|****************************************|1:158|***

用途:定时采样运行队列长度,开销极低。

比runqlat更温和——runqlat每次上下文切换都记录,runqlen只是每秒采样几次。适合长期监控。

类比:runqlen 像定时看一眼候工区排了多长的队。

softirqs / hardirqs:中断开销

中断是硬件和内核之间的"插话"——网卡收到包、磁盘完成 I/O,都会打断 CPU。

# softirqs 10 1SOFTIRQ TOTAL_usecs net_tx9rcu751sched3431timer5542tasklet11368net_rx12225
# hardirqs 10 1HARDIRQ TOTAL_usecs nvme0q235ens5-Tx-Rx-05922

用途:看中断把 CPU 时间吃掉了多少。如果net_rx或ens5-Tx-Rx-0很高,说明网络流量大;如果nvme0q1很高,说明磁盘 I/O 频繁。

注意:这些中断开销不会出现在普通 CPU 剖析里——因为中断处理不可被采样器打断,所以要用专门的工具看。

类比:softirqs/hardirqs 像统计工厂被"临时插单"打断了多久——这些时间看不见,但真实存在。

bpftrace:自定义追踪

前面讲的都是"现成工具",bpftrace是让你自己写工具。

比如,统计所有内核函数调用次数:

bpftrace-e'kprobe:attach* { @[probe] = count(); }'

比如,记录vfs_read()耗时分布:

bpftrace-e'k:vfs_read { @ts[tid] = nsecs; } kr:vfs_read /@ts[tid]/ { @ = hist(nsecs - @ts[tid]); delete(@ts[tid]); }'

用途:现成工具不够用时,用bpftrace写一个。CPU 相关的常见场景:统计调度函数、追踪特定内核路径、测自定义延迟。

类比:bpftrace 像工厂里可以自己组装检测设备——想要什么指标,自己搭一个。


三、可视化与调优

可视化:CPU 分析最有用的是火焰图——它把成千上万条栈样本压缩成一张图,宽度代表时间占比,一眼看出哪个函数最耗 CPU。perf和profile都能生成。

另一个是利用率热图——把每个 CPU 的利用率按时间画成热图,能看出负载均衡问题。

调优:CPU 调优主要是三件事。

第一,优先级。nice调静态优先级,chrt调调度策略。

$nice-n19command# 低优先级运行$ chrt-bcommand# 批处理调度

第二,CPU 绑定。把进程绑到特定 CPU,提高缓存命中率。

$ taskset-pc7-1010790# PID 10790 只跑在 CPU 7-10

第三,资源控制。用 cgroups 限制 CPU 配额,或者用 exclusive cpusets 给进程独占 CPU。

# 给进程组限制 CPU 带宽echo50000>/sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us

注意:CPU 调优最大的赢往往不是调参数,而是消除不必要的工作——用剖析找到最热的代码路径,优化它。


四、总结

  1. 不要看 CPU 忙不忙,要看效率高不高——利用率、IPC、缓存命中率一起看
  2. 不要让 CPU 空转等内存——不是"等 I/O",是"等物料"
  3. 缓存决定性能上限——缓存命中率高,工厂就快;缓存命中率低,工厂就等。
  4. 调度延迟往往比工厂占用更值得关注——用runqlat看调度延迟,用cpudist看每次工作时长。如果线程跑得"又短又碎",说明工厂竞争严重。
  5. 多核该用就用,但别盲目堆——超线程、锁争抢都可能让"多核"变成负担

下一章:讲内存——也就是"办公桌"。工厂讲完了,接下来看工人旁边的办公桌。办公桌不够用,工厂也干不了活。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询