EN

Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇

字数统计: 4.5k阅读时长: 15 min
2022/01/21
loading

本文是 Systrace 线程 CPU 运行状态分析技巧系列的第一篇,主要分析了 Systrace 中线程的 Runnable 状态出现的原因和 Runnable 过长时的一些优化思路。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录

技术复核(2026-10-14): 本文发表于 2022 年,保留旧 Systrace 截图。Runnable 的基本分析方法仍适用,但下文已修正调度、绑核和实时优先级的泛化,并补充 Linux 6.6 EEVDF、Android 任务配置与 32 位兼容边界。新采集优先使用 Perfetto;具体策略以目标设备内核和系统配置为准。

本文目录

  1. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  2. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  3. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Runnable 状态说明

Runnable 状态在 Trace 中的显示方式

Running 篇讨论线程正在 CPU 上执行的区间。这里的 Runnable 指线程已可执行、但尚未获得 CPU 的区间;旧 Systrace 通常用蓝色显示。应以状态字段为准,不要只凭颜色判断,也不要把它等同于 Java 的 Thread.State.RUNNABLE:后者是虚拟机状态,与操作系统线程状态不是一一对应关系。参见 Java 17 状态定义。

图 1: Systrace 中 Runnable 的可视化效果展示

图 1 是历史截图:选中区间标为 Runnable,并显示 wakeup from tid: 1515。这说明有唤醒关联,不能仅据此认定 1515 就是抢占者或延迟根因。完整 Trace 未随文提供,无法从这张裁图量化整个等待过程。

图 2: 性能之巅 2 CPU 优化

图 2 是简化的调度模型。对操作系统看到的一个逻辑 CPU 而言,同一时刻只运行一个普通线程上下文;带 SMT 的物理核可能有多个逻辑 CPU。Linux 维护每 CPU 的运行队列及各调度类的数据结构,并不意味着所有任务都在一个简单 FIFO 队列里依次等待。

Runnable 只表示线程当前没有因睡眠条件而停止执行,不代表它已经持有所有 I/O、锁和信号量资源。线程被唤醒后可能还要重新竞争锁,也可能短暂运行后再次睡眠。Linux 的 TASK_RUNNING 覆盖可运行与正在运行的任务,而查看器进一步区分 Running 和 Runnable。

在 Perfetto 调度数据中,可结合 sched_switch 与 sched_waking 查看运行、换出和唤醒。R+ 表示被抢占后仍可运行;唤醒到获得 CPU 执行机会的区间也可能包含唤醒路径和迁移开销,不能全部解释成某个队列中严格的排队时间。Trace 边界和事件丢失还可能令区间不完整。

常见调度策略要分开理解:

  1. 普通公平调度:SCHED_NORMAL(用户态常称 SCHED_OTHER)下,nice 值影响相对权重。原文时代常见的 CFS 全称是 Completely Fair Scheduler,按虚拟运行时间分配 CPU,并不是固定时间片的简单轮转。上游 Linux 从 6.6 开始转向 EEVDF;Android 设备采用哪种实现,需看实际内核及厂商修改,不能只按 Android 版本推断。参见 CFS 与 EEVDF。
  2. 实时策略:SCHED_FIFO 没有普通意义的轮转时间片,SCHED_RR 在同优先级任务间轮转。符合运行条件的实时任务通常优先于普通公平类,但也受带宽限制等约束;高优先级任务阻塞、让出或被限制时,其他任务才可能获得机会。Linux 还有 SCHED_DEADLINE 等策略,不能把全系统简化成上述两类。参见 Linux 调度策略。

图 3: AOSP 渲染架构

图 3 仅作历史流程示意:RenderThread 是 CPU 线程,不是 GPU;它提交的 GPU 工作异步执行,之后还涉及 BufferQueue、fence 与 SurfaceFlinger/HWC 合成。图中的 16.6 ms 对应约 60 Hz,不是所有屏幕和帧的统一预算,也不是所有阶段都必须在同一个周期串行完成。参见 AOSP 图形架构。

关键链路上的调度延迟可能导致错过该帧截止时间,但并非每一段 Runnable 都会掉帧。先把区间与目标帧关联;Android 12 / API 31 及以上、采集包含对应数据时,可结合 FrameTimeline判断实际帧结果。

Runnable 过长的原因和优化思路

下面按七类情况排查。这些是候选原因,不是仅凭蓝色区间就能作出的诊断;系统策略、应用负载和硬件能力可能共同影响一次等待。

先选定一个确实影响启动或帧截止时间的 Runnable 区间,核对线程身份、前一状态、唤醒来源与下一次 Running。然后查看同一时段各 CPU 的工作,并检查该线程实际允许使用的 CPU、调度策略、优先级和 cgroup 限制。把「已唤醒但未运行」与「仍在等锁或 I/O」分开,之后再进入下面的原因分析。

原因 1: 优先级设置错误

应用把非关键任务设得过高,可能挤占关键工作的 CPU;把关键线程设得过低,也可能在竞争时增加等待。但普通公平类的 nice 权重不等于实时优先级,不能跨策略直接比较数字,更不能从「另一个线程正在跑」就认定它在异常抢占。

优化思路:

  1. 对照 Trace 中的调度切换与实际策略,确认竞争者和关键依赖。提高等待者优先级不一定能帮助一个低优先级的依赖线程更快完成。
  2. 应用优先安排任务期限、并发量和线程用途,再按 API 文档约束使用合适的优先级;比较代表性设备上的结果。不要把改成 FIFO/RT 当作通用修复:它需要对应权限与平台配置,还可能令其他任务饥饿。
  3. 平台若确需实时调度,应评估运行时间、阻塞依赖、实时带宽和对其他线程的影响。仅把“关键”线程改为 FIFO,不构成完整方案。

我们在实践中见到过不少应用因为设置错了优先级反而导致更卡。原因比较复杂,可能开发者所使用的机器用当时的优先级策略没问题,但是在别的厂商的调度策略下就会出现水土不兼容的情况。因此,三方应用不要只凭直觉抬高优先级。Process.setThreadPriority 是公开的 nice 调整接口,仍需遵守权限与参数限制,并验证对依赖线程的影响。

长远来看,应让有时限的工作避免排在无界的后台任务队列之后;这不意味着不能使用 worker,更不意味着把重活搬回 UI 线程。对于持续、可描述目标时长的负载,可在支持的设备上评估 Android 12 / API 31 引入的 PerformanceHintManager,向系统报告目标与实际工作时长。它提供提示,不保证指定核心或截止时间。

原因 2: 绑核不合理

CPU affinity 限制线程可以运行的逻辑 CPU 集合,不能保证提速。把线程限制在一个繁忙核心上,即使其他核心空闲,也可能增加等待。按簇绑定仍然可能过窄;普通应用通常应先让系统调度,再用证据判断是否确需限制。

检查时注意:

  1. 按设备实际拓扑识别核心,不能把 4–7 写成通用大核编号,也不能假设所有八核设备都是四大四小。
  2. 同时检查 affinity、cpuset、在线 CPU 与位宽兼容性。最终可用集合可能比请求的 mask 小,也可能随进程状态、策略或热插拔变化。
  3. 对标准 Linux sched_setaffinity,请求集合与允许集合部分重叠时,内核可以收窄到有效交集;没有允许的 CPU 时可返回 EINVAL,权限不足还可能返回 EPERM。应检查返回值和最终 mask,而不是把失败描述为必然崩溃。参见 接口定义。
  4. 即使 affinity 合理,也要检查 cgroup CPU 配额。配额耗尽后,任务可能仍可运行却被节流;其他核空闲并不自动解除这一限制。参见 CPU 带宽控制。

原因 3: 软件架构设计不合理

一个关键操作如果反复在线程间交接,就可能多次经历唤醒到运行的延迟。例如 A 把任务交给 B,等待 B 完成后再继续:A 等待 B 时通常是睡眠或阻塞,B 唤醒 A 后才进入这里讨论的 Runnable 区间,不能把整段等待都算成 Runnable。

设计的时候,别按整台手机只跑你的应用来想,别的进程也在用这些资源。线程数量本身不是结论。合理并行可以缩短总耗时;无必要的串行交接、无界任务队列和过量并发才需要优先排查。

优化思路:

  1. 用调度和 Binder、futex 或业务标记建立依赖链;唤醒关系只是线索,不等于完整业务因果。仅靠 CPU 栈采样也不能还原全部等待关系。
  2. 合并无必要的交接,缩短关键依赖,限制并发和队列积压;不要用主线程同步等待替代后台协作。
  3. 平台调度优化需要用真实依赖和跨场景结果验证,不能仅把一组线程一起提优先级。

原因 4: 应用自己或系统整体负载高导致排队的任务非常多

允许使用的 CPU 上有很多可运行任务时,调度等待可能增加。公平调度不是严格的先到先服务,不能仅按“排在后面”解释长等待。

CPU 轨道很密,可能是频繁切换,也可能是观察尺度导致。它不能证明整机使用率超过 90%。应选定时间窗口,按核心统计非 idle 的运行时间,再按所用核心数和在线时间解释分母;中断耗时与调度轨道的归属也需单独考虑。平均使用率、运行队列压力和某条线程的等待时间是不同量,低平均负载也可能伴随关键线程的短时竞争。

1.应用自身高占用

应用同时执行大量 CPU 密集任务,可能让渲染或启动所需的工作缺少及时的运行机会。线程数多不一定意味着 CPU 忙:许多线程也可能在睡眠。

优化建议:

  1. 在同一问题窗口找出本进程的高 CPU 线程,核对是否有重复计算、过期任务或失控并发。
  2. 用 simpleperf 等采样工具定位正在执行的热点,同时用调度数据区分计算与等待;采样比例不是调用次数。
  3. 限制并发,取消过期任务,把可延期工作移出关键窗口。不要用 FIFO/RT 掩盖负载问题。
  4. 区分关键和非关键任务,但不要机械地把所有后台任务绑到小核。某个后台线程也可能正提供 UI 等待的结果;应沿依赖链检查最终效果。

2.系统服务高占用

系统服务或其他应用的活动也可能占用可用 CPU。按同一时间窗口统计实际运行的线程,再结合调用链、进程身份和业务场景判断是否异常。仅看到后台进程活跃,不能断定是保活行为;音乐、导航、通话等可能有合法的持续工作。

3.平台厂家的黑科技

平台可以协调前后台资源,但必须区分机制与具体产品配置,不能把某个 ROM 的限制概括为 AOSP 的固定缺陷。

  1. cpuset 限定可用 CPU 集合,不直接代表某个固定优先级,也不保证硬件独占。子进程通常继承所属 cgroup,之后还可能被框架或服务重新分组,不能说 Java fork 的子进程一律无人管理。
  2. CPU controller 可以按内核和配置提供相对权重、配额等控制。权重不是硬上限,cpuset 也不是 CPU 使用量配额;过严配额本身可能增加 Runnable 等待。
  3. 任务配置入口:Android 10 起有 cgroup 抽象层,Android 11 起提供 SetTaskProfiles / SetProcessProfiles;Android 12 起 init 的 task_profiles 替代已弃用的 writepid 迁移方式。平台代码按实际 cgroups.json、task_profiles.json 与厂商覆盖配置工作,普通应用不能直接套用这些系统接口。参见 AOSP cgroup 文档。
  4. 缓存应用冻结:Android 11 / API 30 起支持 cached apps freezer,针对符合条件的 cached 进程,依赖内核 cgroup v2 与系统配置。它不是“所有退到后台的应用立即冻结”,也不适用于把音乐、导航等持续工作一概停止。Binder、解冻和进程关系仍需系统协调。参见 AOSP freezer 文档。
  5. 按需工作:减少不必要的系统任务与后台启动,同时保留用户正在使用的服务,并验证不同设备、温度和前后台切换场景。

原因 5: CPU 算力限制、锁频、锁核、状态异常

打个当年常见的比方:排队做核酸,窗口少了、处理慢了,队伍就容易长起来。同样的工作量,在有效 CPU 能力较低时可能需要更长时间,从而增加其他任务的等待。需要分别核查:

  1. 热限制、省电模式或场景策略是否改变了频率上限、可用核心或任务放置;不同厂商同名模式不保证相同机制。
  2. 实际频率、在线 CPU、负载与热状态是否在问题窗口对应。单独看到低频不能证明热降频,也不能直接换算为损失的算力。
  3. CPU idle 的唤醒延迟是否相关。C0/C1/C2 是常见命名示意,不能当作所有 ARM SoC 的统一状态表;应查看目标平台的 idle state 与 exit latency。参见 Linux CPU idle 文档。
  4. 较低性能设备是否无法承受相同工作量,需要降低负载或调整体验。价格、核心数量和品牌不足以单独判断能力。

应用这边,还是那句话:降低自己负载,少惹平台。优先减少不必要工作,并按热状态调整负载;平台则要验证功耗与时延的取舍。硬件损坏需要独立诊断证据,不能从一段长 Runnable 推断。更多计算耗时分析见 Running 篇。

原因 6: 调度器异常

调度实现或厂商策略可能存在问题,但“有空闲核,任务却在等待”还不足以证明异常。先排除 affinity、cpuset、位宽兼容、配额节流、核心离线等限制;再考虑迁移成本、能耗策略、中断和不可抢占区间。

频率 governor、idle governor 与任务调度不是同一个机制,应结合相应数据定位。系统开发者需要记录内核版本、策略配置、事件完整性和可重复场景,比较预期与实际放置行为,再决定是否修改调度器。

原因 7: 处理器区分执行 32 位与 64 位进程

原文提到的骁龙 8 Gen 1、天玑 9000 属于 2021–2022 年的过渡期例子,不能再称为“最近推出”。这里要关注的是非对称 AArch32 支持:某些系统只有部分核心能运行 32 位用户代码,32 位任务便可能集中到较小的 CPU 集合,而不是必然都挤在某一个核。

Linux 非对称 32 位文档说明了内核启用该兼容模式后如何限制 affinity,以及支持时的 aarch32_el0 CPU 集合。具体手机还需核对内核、系统 ABI 与实际可用 mask;64 位任务同样受前述 cpuset、affinity、配额和热策略限制,并非“畅通无阻”。

  1. 应用按 Android 64 位支持指南检查自身与第三方 Native 库,补齐适配的 64 位版本,并验证安装、加载和功能。仅改 ABI 配置不能把缺失的 32 位库变成 64 位;迁移也不保证所有负载自动加速。
  2. 平台按实际 32 位兼容能力管理任务,与应用方协作完成迁移。纯 64 位系统可能无法运行旧 32 位 Native 应用,这与“仍可运行但排队更多”是不同问题。

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 本文目录
  • Runnable 状态说明
    1. 1. Runnable 状态在 Trace 中的显示方式
  • Runnable 过长的原因和优化思路
    1. 1. 原因 1: 优先级设置错误
    2. 2. 原因 2: 绑核不合理
    3. 3. 原因 3: 软件架构设计不合理
    4. 4. 原因 4: 应用自己或系统整体负载高导致排队的任务非常多
      1. 4.1. 1.应用自身高占用
      2. 4.2. 2.系统服务高占用
      3. 4.3. 3.平台厂家的黑科技
    5. 5. 原因 5: CPU 算力限制、锁频、锁核、状态异常
    6. 6. 原因 6: 调度器异常
    7. 7. 原因 7: 处理器区分执行 32 位与 64 位进程
  • 关于我 && 博客