做 Android 性能测试,CPU 使用率是最基础的指标。但很多人只会用
top看一眼,对背后的数据源和计算逻辑并不清楚。 本篇从概念讲起,先给公式、再看数据源、最后用真实数据走一遍完整计算。
一、CPU 使用率到底在度量什么
先明确一个概念:CPU 使用率衡量的是"时间片的分配比例",不是"算力的消耗比例"。
Linux 内核用一个叫 tick 的时间片来调度任务。每个 tick 大约 4~10 毫秒(取决于内核配置 CONFIG_HZ)。每个 tick 结束时,内核记录这个 tick 被分配到了哪个状态:用户态、内核态、空闲、等待 IO……
一句话定义:
CPU 使用率 = 一段时间内,非空闲 tick 占总 tick 的比例
二、核心公式(先看结论)
CPU 统计数据都是从开机开始的累积值,所以必须采两次、取差值才能算出一段时间内的使用率。
2.1 整机 CPU 使用率
第 1 次采样 → total₁, idle₁
等待 N 秒
第 2 次采样 → total₂, idle₂
Δtotal = total₂ − total₁ ← 这段时间所有核心总共产生了多少 tick
Δidle = idle₂ − idle₁ ← 其中有多少 tick 是空闲的
整机 CPU% = (Δtotal − Δidle) / Δtotal × 100
其中:
| 变量 | 含义 | 怎么得到 |
|---|---|---|
| total | 所有状态 tick 之和 | user + nice + system + idle + iowait + irq + softirq + steal |
| idle | 空闲 tick | /proc/stat 第一行的第 4 个数值字段 |
2.2 进程 CPU 使用率
第 1 次采样 → process_time₁(同时采整机 total₁)
等待 N 秒
第 2 次采样 → process_time₂(同时采整机 total₂)
Δprocess = process_time₂ − process_time₁ ← 进程自己消耗的 tick 增量
Δtotal = total₂ − total₁ ← 整机总 tick 增量(同上)
进程 CPU% = Δprocess / Δtotal × 100
其中:
| 变量 | 含义 | 怎么得到 |
|---|---|---|
| process_time | 进程累积的 CPU tick | utime + stime + cutime + cstime |
| Δtotal | 整机总 tick 增量 | 和整机公式用同一个分母 |
关键点:进程 CPU% 的分母是整机的 Δtotal,不是进程自己的数据。
含义是"这个进程在所有 CPU 资源中占了多少比例"。4 核设备上,单线程进程最高约 25%,4 线程跑满接近 100%。
2.3 公式小结
整机 CPU% = (Δtotal − Δidle) / Δtotal × 100
进程 CPU% = Δprocess / Δtotal × 100
就这两个公式,所有 CPU 采集工具(top、dumpsys cpuinfo、各种 APM SDK)底层都是这个逻辑。
三、数据源:数据从哪来
3.1 整机数据:/proc/stat 第一行
Linux 内核提供的 CPU 统计接口,所有 Android 设备都有。读取后第一行格式如下:
cpu 10132153 290696 3084719 46828483 16683 0 25195 0 0 0
跳过开头的 cpu,后面 8 个数值依次是:
| 位置 | 名称 | 含义 |
|---|---|---|
| 1 | user | 用户态时间(App 代码执行) |
| 2 | nice | 低优先级用户态时间 |
| 3 | system | 内核态时间(系统调用、驱动) |
| 4 | idle | 空闲时间(CPU 在发呆) |
| 5 | iowait | 等待 IO 的空闲时间(等磁盘/网络) |
| 6 | irq | 硬中断处理时间 |
| 7 | softirq | 软中断处理时间 |
| 8 | steal | 虚拟化被偷走的时间 |
total = 上面 8 个字段全部相加。
3.2 汇总行 vs 各核:用哪一行
/proc/stat 的完整输出中,第一行下面还有每个核心的单独数据:
cpu 10132153 290696 3084719 46828483 16683 0 25195 0 0 0 ← 汇总行(我们用这行)
cpu0 2503274 72633 771347 11709498 4180 0 6313 0 0 0 ← 核心 0
cpu1 2522508 73222 770877 11707883 4115 0 6239 0 0 0 ← 核心 1
cpu2 2505698 72460 771276 11706498 4198 0 6312 0 0 0 ← 核心 2
cpu3 2514673 72381 771219 11704604 4190 0 6331 0 0 0 ← 核心 3
汇总行 = 所有在线核心的累加。以 idle 字段验证:
cpu0.idle + cpu1.idle + cpu2.idle + cpu3.idle
= 11709498 + 11707883 + 11706498 + 11704604
= 46828483
= 汇总行的 idle ✓
所以:
| 问题 | 答案 |
|---|---|
| 需要自己加各核数据吗? | 不需要,内核已经帮我们加好了 |
| 直接用汇总行就行? | 是的,只取第一行 cpu |
| 关核了怎么办? | 关掉的核不出现在输出中,汇总行只含在线核 |
| 4 核 total 每秒增加多少? | 约 4 × HZ(HZ=100 时约 400 tick/秒) |
3.3 进程数据:/proc/{pid}/stat
读取目标进程的 stat 文件,输出是一行很长的数据:
12345 (com.example.app) S 1 12345 12345 0 -1 ... 5000 1200 0 0 ...
我们关心的是第 14~17 个字段(从 1 开始计数):
| 字段号 | 名称 | 含义 |
|---|---|---|
| 14 | utime | 进程在用户态消耗的 tick |
| 15 | stime | 进程在内核态消耗的 tick |
| 16 | cutime | 已退出子进程的用户态 tick(累积) |
| 17 | cstime | 已退出子进程的内核态 tick(累积) |
process_time = utime + stime + cutime + cstime
四、完整计算实例
用一组真实格式的数据,从头到尾走一遍。
4.1 第 1 次采样
读 /proc/stat 第一行:
cpu 10132153 290696 3084719 46828483 16683 0 25195 0 0 0
解析:
user=10132153 nice=290696 system=3084719 idle=46828483
iowait=16683 irq=0 softirq=25195 steal=0
total₁ = 10132153 + 290696 + 3084719 + 46828483 + 16683 + 0 + 25195 + 0
= 60,377,929
idle₁ = 46,828,483
同时读进程 /proc/{pid}/stat:
utime=5000 stime=1200 cutime=0 cstime=0
process_time₁ = 5000 + 1200 + 0 + 0 = 6,200
4.2 等待 10 秒,第 2 次采样
读 /proc/stat 第一行:
cpu 10132953 290720 3085019 46832083 16690 0 25210 0 0 0
解析:
total₂ = 10132953 + 290720 + 3085019 + 46832083 + 16690 + 0 + 25210 + 0
= 60,382,675
idle₂ = 46,832,083
同时读进程 /proc/{pid}/stat:
utime=5350 stime=1280 cutime=0 cstime=0
process_time₂ = 5350 + 1280 + 0 + 0 = 6,630
4.3 算差值
| 字段 | 第 1 次 | 第 2 次 | 差值(Δ) |
|---|---|---|---|
| user | 10,132,153 | 10,132,953 | 800 |
| nice | 290,696 | 290,720 | 24 |
| system | 3,084,719 | 3,085,019 | 300 |
| idle | 46,828,483 | 46,832,083 | 3,600 |
| iowait | 16,683 | 16,690 | 7 |
| irq | 0 | 0 | 0 |
| softirq | 25,195 | 25,210 | 15 |
| steal | 0 | 0 | 0 |
| total | 60,377,929 | 60,382,675 | 4,746 |
为什么 10 秒产生了 4746 个 tick?因为 4 核设备,每个核每秒约产生 HZ 个 tick(HZ≈100~120),4 × ~119 × 10 ≈ 4746。
4.4 代入公式
整机 CPU%:
Δtotal = 4,746
Δidle = 3,600
整机 CPU% = (Δtotal − Δidle) / Δtotal × 100
= (4746 − 3600) / 4746 × 100
= 1146 / 4746 × 100
≈ 24.1%
→ 过去 10 秒,4 个核心总共有 24.1% 在干活,75.9% 在空闲。
细分一下各状态占比:
user 占比 = 800 / 4746 × 100 = 16.9% ← 用户态(App 代码)
system 占比 = 300 / 4746 × 100 = 6.3% ← 内核态(系统调用)
iowait 占比 = 7 / 4746 × 100 = 0.1% ← 等 IO(几乎没有)
nice 占比 = 24 / 4746 × 100 = 0.5%
softirq占比 = 15 / 4746 × 100 = 0.3%
进程 CPU%:
Δprocess = 6630 − 6200 = 430
进程 CPU% = Δprocess / Δtotal × 100
= 430 / 4746 × 100
≈ 9.1%
→ 这个进程在过去 10 秒,消耗了整机 CPU 资源的 9.1%。
4.5 一张图看全貌
Δtotal = 4,746 tick(10 秒 × 4 核)
┌─────────────────────────────────────────────────┐
│ │
│ ┌── user = 800 ─┐ │
│ ├── nice = 24 │ │
│ ├── system = 300 ├─ 忙碌 = 1,146 (24.1%) │
│ ├── iowait = 7 │ │
│ ├── softirq = 15 │ │
│ ├── irq = 0 ┘ │
│ │ │
│ └── idle = 3,600 ── 空闲 (75.9%) │
│ │
│ 其中进程 com.example.app 消耗 430 tick = 9.1% │
└─────────────────────────────────────────────────┘
五、常见采集方式对比
除了直接读 /proc/stat,还有几种常见方式,它们底层都在用同样的公式:
| 维度 | 直接读 /proc/stat | top -n 1 | dumpsys cpuinfo |
|---|---|---|---|
| 底层原理 | 就是原始数据源 | 内部读 /proc/stat 帮你算 | 通过 Binder 向 system_server 取 |
| 数据精度 | 最高(原始 tick) | 中(top 自己算的) | 低(系统统计窗口不精确) |
| 采集开销 | 最低(2~3ms) | 中(50~200ms) | 最高(100~500ms) |
| 需要自己算 | 是 | 否 | 否 |
| 输出格式稳定 | 是(内核标准格式) | 差(各版本不同) | 中 |
| 适合高频采集 | 是 | 不推荐 | 否 |
| 适合手动看一眼 | 否(原始数字) | 是 | 是 |
建议:写采集工具 → 直接读 /proc/stat;手动快速看 → 用 top 或 dumpsys cpuinfo。
六、采集间隔与精度
6.1 间隔选择
| 间隔 | 适用场景 | 注意事项 |
|---|---|---|
| 1~2 秒 | 短时间精细分析(启动、页面切换) | ADB 采集开销占比高,建议设备端脚本 |
| 5~10 秒 | 长时间压测(推荐) | 平衡精度和开销 |
| 30~60 秒 | 粗粒度监控 | 可能漏掉短暂的 CPU 飙升 |
常用选择是 10 秒——24 小时压测产生 8640 个数据点,够做趋势分析,又不会干扰设备。
6.2 tick 精度:CONFIG_HZ
/proc/stat 里的数值单位是 tick。每个 tick 的时长取决于内核编译时的 CONFIG_HZ:
| CONFIG_HZ | 每 tick 时长 | 常见平台 |
|---|---|---|
| 100 | 10ms | 部分旧内核 |
| 250 | 4ms | MTK 平台常见 |
| 300 | 3.3ms | 部分高通平台 |
| 1000 | 1ms | 服务器/桌面 Linux |
可通过 adb shell getconf CLK_TCK 查看设备的 HZ 值。
HZ=100 时,一个运行了 5ms 的短任务可能不会被记录(不到 1 tick)。对大部分性能测试场景(间隔 ≥ 5 秒),这个精度限制可以忽略。
七、完整采集流程
把前面的内容串起来,一次完整的 CPU 采集分三步:
┌───────────────────────────────────────────────────────┐
│ Step 1:第 1 次采样 │
│ │
│ · 读 /proc/stat → 取第一行 │
│ → 解析 8 个字段 │
│ → total₁ = 8 个字段之和 │
│ → idle₁ = 第 4 个字段 │
│ │
│ · 读 /proc/{pid}/stat │
│ → 取第 14~17 字段 │
│ → process_time₁ = utime + stime + cutime + cstime │
└───────────────────────────────────────────────────────┘
│
│ 等待 N 秒(推荐 10 秒)
▼
┌───────────────────────────────────────────────────────┐
│ Step 2:第 2 次采样(同样步骤) │
│ │
│ → total₂, idle₂, process_time₂ │
└───────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────┐
│ Step 3:代入公式 │
│ │
│ Δtotal = total₂ − total₁ │
│ Δidle = idle₂ − idle₁ │
│ Δprocess = process_time₂ − process_time₁ │
│ │
│ 整机 CPU% = (Δtotal − Δidle) / Δtotal × 100 │
│ 进程 CPU% = Δprocess / Δtotal × 100 │
└───────────────────────────────────────────────────────┘
小结
| 知识点 | 一句话 |
|---|---|
| CPU% 的本质 | 非空闲 tick 占总 tick 的比例 |
| 核心公式 | 整机 (Δtotal−Δidle)/Δtotal,进程 Δprocess/Δtotal |
| 整机数据源 | /proc/stat 第一行(汇总行,不需要手动加各核) |
| 进程数据源 | /proc/{pid}/stat 第 14~17 字段 |
| total 怎么算 | 汇总行 8 个数值字段全部相加 |
| 推荐采集方式 | 直接读 /proc/stat,不用 top / dumpsys |
| 推荐采集间隔 | 10 秒(长时间压测) |
| 精度上限 | CONFIG_HZ 决定,通常 4~10ms |
掌握了这些基础,你就能看懂任何 CPU 采集工具的原理了。下一篇我们讲这个"简单公式"里藏着的 8 个坑——每个都可能让你的数据严重失真。
系列目录
- 第 1 篇:内存泄漏自动检测(上)——采集层设计
- 第 2 篇:内存泄漏自动检测(中)——检测层设计
- 第 3 篇:内存泄漏自动检测(下)——响应层设计
- 第 4 篇:Android 内存采集避坑指南
- 第 5 篇(本篇):Android CPU 使用率采集入门
- 第 6 篇(下一篇):CPU 采集的 8 个坑
我是测试工坊,专注 Android 系统级性能工程。 如果你也在做 CPU 相关的性能测试,欢迎评论区交流 👇 关注我,后续更新不迷路。