构建性能瓶颈分析:用 perf、top、iotop 揪出拖慢 Yocto 的真凶

Yocto 构建一次动辄几十分钟甚至几小时,卡在哪一步都不知道,确实让人头疼。我刚开始调优时也犯过傻——盲目加 CPU 核数,结果磁盘 I/O 先爆了,构建反而更慢。说白了,你得先找到瓶颈在哪,才能对症下药。

这一章我们就来聊聊怎么用 perf、top、iotop 这些工具,把 CPU、内存、磁盘 I/O 的瓶颈一个个揪出来。

构建过程的资源消耗画像

Yocto 构建不是单一任务,它分好几个阶段,每个阶段的资源消耗特点完全不同。我习惯把整个过程拆成三块来看:

阶段 主要消耗 典型特征
解析与配置(bitbake -p) CPU(单核) Python 解析 .bb/.bbclass,单进程跑满一个核
编译(do_compile) CPU(多核)+ 内存 gcc/clang 并行编译,内存吃紧时触发 OOM
打包与部署(do_package / do_image) 磁盘 I/O 大量读写 tmp/deploy,rpm/deb 打包、压缩

你想想看,如果编译阶段 CPU 利用率只有 30%,那加核也没用;如果打包阶段 iowait 飙到 50%,那换 SSD 比换 CPU 更管用。所以第一步,先搞清楚你现在卡在哪。

核心原则:先测量,再调优。不要凭感觉加资源,用数据说话。

用 top 快速定位 CPU 和内存压力

top 是我最常用的第一手工具。构建时开一个终端跑 top,按 1 看每个核的负载,按 Shift+M 按内存排序。我个人习惯这样用:

# 在构建主机上运行
top -d 2 -b | grep -E "bitbake|gcc|make|python" > /tmp/build_profile.log

重点关注几个指标:

  • %CPU:如果所有核加起来不到 80%,说明 CPU 不是瓶颈,可能是 I/O 或锁等待。
  • %MEM / RES:单个进程 RES 超过物理内存 20% 就要小心了,尤其是 gcc 进程。
  • load average:如果 load 远高于 CPU 核数,说明大量进程在等 I/O 或锁。

我记得有一次帮客户调一个嵌入式 Linux 发行版,构建时 load 飙到 60,但 CPU 利用率只有 40%。用 top 一看,大量进程处于 D 状态( uninterruptible sleep),说白了就是在等磁盘。换了一块 NVMe SSD,构建时间直接砍半。

小技巧:按 Shift+P 按 CPU 排序,按 Shift+M 按内存排序。两个视图切换着看,能快速定位是 CPU 吃紧还是内存吃紧。

用 iotop 揪出磁盘 I/O 瓶颈

top 能告诉你 iowait 高,但谁在读写磁盘?这时候 iotop 就派上用场了。iotop 类似 top,但显示的是每个进程的磁盘读写速率。

# 安装 iotop(Yocto 构建主机上)
sudo apt install iotop   # 或 sudo dnf install iotop

# 运行 iotop,只看 Yocto 相关进程
sudo iotop -o -d 2 -p $(pgrep -d',' -f "bitbake|gcc|make|python")

参数说明:

  • -o:只显示有磁盘 I/O 的进程
  • -d 2:每 2 秒刷新一次
  • -p:指定进程 PID,避免被其他进程干扰

看什么?重点关注 DISK READ 和 DISK WRITE 两列。如果某个进程持续写几百 MB/s,而你的磁盘带宽只有 500 MB/s,那它基本就是瓶颈了。

嗯,这里要注意:iotop 需要 root 权限,否则看不到进程名。我一般用 sudo iotop,然后按 O 键过滤,只显示 Yocto 相关进程。

避坑指南:我曾经在 HDD 上跑 Yocto,do_package 阶段 iotop 显示 DISK WRITE 持续 200+ MB/s,但 HDD 顺序写只有 150 MB/s。结果构建时间从 40 分钟拖到 2 小时。后来换成 SSD,问题迎刃而解。如果你还在用 HDD 做构建,强烈建议先换 SSD。

用 perf 做深度 CPU 剖析

top 和 iotop 能告诉你「谁」在消耗资源,但 perf 能告诉你「为什么」。perf 是 Linux 性能剖析的瑞士军刀,可以采样 CPU 的调用栈,找出热点函数。

# 对 bitbake 进程做 CPU 采样,持续 30 秒
sudo perf record -g -p $(pgrep -f bitbake) -- sleep 30

# 生成报告
sudo perf report -g graph --no-children

输出示例:

# 样本数:15000
# 事件:cpu-clock
#
# 开销      命令         共享对象                符号
# ........  ...........  ......................  ......................
    35.2%  bitbake       [kernel.kallsyms]      [k] _raw_spin_lock
    12.1%  bitbake       libpython3.10.so.1.0   [.] PyEval_EvalFrameDefault
     8.5%  bitbake       libc-2.31.so           [.] __GI___libc_read
     6.3%  bitbake       [kernel.kallsyms]      [k] do_syscall_64

看到 _raw_spin_lock 占 35%?说明有严重的锁竞争。这在 Yocto 解析阶段很常见——bitbake 解析数千个 .bb 文件时,全局解释器锁(GIL)和文件锁会拖慢速度。

我建议重点关注几个方向:

  • 内核锁竞争:_raw_spin_lock、mutex_lock 占比高 → 考虑减少并行任务数(BB_NUMBER_THREADS)
  • Python 解释器:PyEval_EvalFrameDefault 占比高 → 考虑用 pypy3 或优化 .bb 解析逻辑
  • 系统调用:do_syscall_64、__GI___libc_read 占比高 → 检查文件系统或缓存策略

个人经验:perf 的 -g 参数会记录调用栈,但数据量很大。我一般先跑 10 秒采样,看看热点在哪,再针对性地加长时间。另外,--no-children 可以去掉子调用聚合,只看当前函数的开销,更清晰。

用 vmstat 和 iostat 看系统整体健康度

除了进程级工具,系统级工具也不能少。vmstat 和 iostat 能给你一个全局视角。

# 每 2 秒输出一次系统状态
vmstat 2

# 输出示例
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 8  2      0 2048000 500000 8000000    0    0   500  1200 3000 5000 45 10  5 40  0

关键列:

  • r:运行队列长度。如果持续大于 CPU 核数,说明 CPU 不够用。
  • b:处于不可中断睡眠的进程数。如果持续大于 0,说明 I/O 有瓶颈。
  • wa:iowait 百分比。超过 20% 就要警惕了。
  • si / so:swap 换入换出。如果非零,说明内存不足,正在用磁盘当内存,性能会急剧下降。

iostat 更聚焦磁盘:

# 查看所有磁盘的 I/O 统计,每 2 秒一次
iostat -x 2

# 输出示例
Device            r/s     w/s     rkB/s     wkB/s  await  svctm  %util
nvme0n1         200.0   150.0   80000.0   60000.0   2.5   0.8    28.0
sda              50.0   300.0   20000.0  120000.0  15.0   6.0    90.0

看 %util:如果接近 100%,说明磁盘已经饱和。再看 await:如果远高于 svctm,说明有排队,磁盘响应慢。

实战判断口诀:

  • r 高 + wa 高 + %util 高 → I/O 瓶颈,换 SSD 或调 BB_NUMBER_THREADS
  • r 高 + wa 低 + %util 低 → CPU 瓶颈,加核或优化编译参数
  • si/so 非零 → 内存瓶颈,加内存或减少并行任务

瓶颈分析全景图

下面这张图总结了整个分析流程,我每次调优都按这个思路走:

Yocto 构建瓶颈分析流程 构建变慢?开始诊断 第一步:vmstat / iostat 看全局 第二步:top 看 CPU/内存 第二步:iotop 看磁盘 I/O 第三步:perf 采样 CPU 热点 第三步:strace 跟踪系统调用 定位瓶颈类型 → 针对性调优 CPU 瓶颈 → 调 BB_NUMBER_THREADS / 优化编译参数 I/O 瓶颈 → 换 SSD / 调 BB_NUMBER_THREADS / 用 tmpfs

实战案例:一次完整的瓶颈定位

最后分享一个我实际遇到的案例。某次构建一个带 Qt 的嵌入式镜像,总耗时 2 小时 15 分,客户嫌慢。我按上面的流程走了一遍:

  1. vmstat:r=12(8 核 CPU),wa=35%,si/so=0 → 排除内存问题,I/O 嫌疑最大
  2. iotop:do_package 阶段,DISK WRITE 持续 180 MB/s,磁盘是 SATA SSD(理论 550 MB/s)→ 磁盘没跑满,但 wa 高说明有排队
  3. perf:采样发现 do_syscall_64 和 __GI___libc_write 占 40% → 大量小文件写入导致系统调用开销大
  4. 根因:do_package 阶段会为每个包创建大量小文件(.rpm/.deb 的临时文件),文件系统 ext4 的元数据操作成了瓶颈
  5. 解决方案:把 TMPDIR 移到 tmpfs(内存盘),同时调大 BB_NUMBER_THREADS 到 12(超线程)

结果:构建时间从 2 小时 15 分降到 1 小时 10 分,接近一半。你想想看,只是换了个文件系统策略,效果就这么明显。

注意:tmpfs 方案需要足够的内存。如果内存小于 32GB,不建议把整个 TMPDIR 放 tmpfs,否则会触发 swap,得不偿失。我一般只把 tmp/deploy 或 tmp/work 放 tmpfs,具体看哪个阶段是瓶颈。

好了,这一章的内容就到这里。工具和方法都给了,剩下的就是动手去试。记住:先测量,再调优,别凭感觉加资源。