3. CPU调度基础
Android 的 CPU 调度建立在 Linux 内核调度器之上,但又被厂商和 Android 框架深度定制。理解调度,不能只记住“谁先运行”,而要抓住一条主线:调度器在“响应速度、吞吐量、公平性、功耗”之间做取舍。本节把调度器目标、调度实体、调度类、时间片、优先级、抢占、负载均衡和实时调度这些基础概念串成一张可操作的认知地图。
一、调度器的四个核心目标
调度器的本质是一个资源分配器:CPU 核数有限,可运行任务却可能很多。它要在毫秒甚至微秒级做出决策,通常同时追求以下目标:
- 响应性:前台应用、输入事件、动画渲染需要低延迟,触摸后必须在 16.6ms 内完成一帧。
- 吞吐量:后台编译、文件扫描、AI 推理希望单位时间完成更多工作。
- 公平性:同等优先级的任务应获得相近的 CPU 时间,避免饥饿。
- 能效:把任务尽量放在小核或低频率上完成,减少大核唤醒和迁移带来的功耗。
这四个目标天然冲突。例如,为了响应性把前台线程全部塞进大核,功耗会迅速上升;为了省电把任务压在小核,前台滑动就可能掉帧。因此 Android 的调度策略往往是“按场景动态加权”,而不是固定规则。
二、调度实体与调度类:谁被调度,按什么规则调度
调度实体(scheduling entity)是调度器实际参与调度的对象。在 Linux 中,它可以是进程、线程,也可以是控制组(cgroup)中的任务组。Android 常通过 cgroup 把任务划分为 top-app、foreground、background 等组,再对组整体分配 CPU 配额。
调度类(scheduling class)决定“用哪套规则来选任务”。Linux 内核按优先级从高到低依次检查多个调度类:
| 调度类 | 典型策略 | 适用对象 | 特点 |
|---|---|---|---|
| stop_sched_class | 停止调度 | CPU 热插拔、停机 | 最高优先级,用于强制停止核心 |
| dl_sched_class | SCHED_DEADLINE | 有截止时间的实时任务 | 按 runtime/deadline/period 约束 |
| rt_sched_class | SCHED_FIFO / SCHED_RR | 音频、显示、中断线程 | 实时优先级,可抢占普通任务 |
| fair_sched_class | SCHED_NORMAL / SCHED_BATCH | 绝大多数应用线程 | CFS 完全公平调度,按权重分配 |
| idle_sched_class | SCHED_IDLE | 空闲任务 | 仅在 CPU 无事可做时运行 |
Android 中,普通应用线程大多落在 fair_sched_class,音频和显示关键线程常被提升到 rt_sched_class。理解这一点,就能解释为什么“后台线程再忙,也不该卡住音频播放”。
三、时间片、优先级与抢占
时间片是任务一次连续运行的时间额度。CFS 并不使用固定时间片,而是用“虚拟运行时间”来追踪每个任务应得的 CPU 份额:权重越高,虚拟时间增长越慢,实际获得的 CPU 越多。nice 值从 -20 到 19,对应权重从高到低。
优先级分为两类:普通任务的 nice 值,以及实时任务的 RT 优先级(1~99)。RT 优先级数值越大,优先级越高。实时任务一旦就绪,通常可以立即抢占正在运行的普通任务,这就是抢占。
抢占让系统具备低延迟能力,但也带来风险:如果实时线程长时间占用 CPU,普通任务会被饿死,甚至触发 watchdog。因此 Android 对 RT 线程有严格的配额和审计机制。
关键判断:一个任务“卡”,先看它属于哪个调度类。如果是 fair 类,问题多半在权重、CPU 配额或负载;如果是 RT 类,则要检查是否长时间运行、是否缺少 throttling。
四、负载均衡与实时调度基本概念
负载均衡解决“有的核忙死、有的核闲着”的问题。调度器周期性计算各 CPU 的负载,把任务从繁忙核迁移到空闲核。Android 的 EAS(Energy Aware Scheduling)会结合 CPU 算力和功耗模型,选择“能完成任务且最省电”的核,而不是单纯找最闲的核。
实时调度则强调确定性:任务必须在截止时间前完成。SCHED_FIFO 没有时间片,同优先级按先进先出;SCHED_RR 给同优先级任务轮流分配时间片;SCHED_DEADLINE 用 runtime、deadline、period 三个参数描述任务需求。Android 的音频混音线程、显示合成线程常使用 RT 策略,以保证周期性任务不被普通任务拖延。
五、案例:一次滑动掉帧的调度排查
假设用户反馈“列表滑动偶发卡顿”。按调度视角排查,可以这样走:
- 抓取 trace,确认掉帧帧的 CPU 上正在运行什么任务。
- 查看渲染线程是否被 RT 线程或高优先级任务抢占。
- 检查 top-app cgroup 的 CPU 配额是否被后台任务挤占。
- 观察是否发生频繁任务迁移,导致缓存失效和额外功耗。
- 确认 EAS 是否把渲染线程放到了算力不足的小核。
# 查看线程调度策略与优先级
chrt -p <tid>
# 查看 cgroup 中任务分布
cat /dev/cpuset/top-app/tasks | wc -l
# 抓取调度相关 trace
atrace -c -t 5 sched freq idle am wm
如果发现渲染线程长期处于 fair 类且 nice 值不低,可以考虑通过 cgroup 提升其 CPU 配额,或由厂商调度器将其临时提升到 RT。但要注意,提升优先级只是手段,根因可能是锁竞争、IO 阻塞或过度绘制。
六、常见误区与复盘要点
误区一:“优先级越高越好”。RT 线程过多会导致普通任务饥饿,系统整体响应反而变差。
误区二:“负载均衡就是把任务平均分到每个核”。EAS 更关注能效,有时让大核多承担一点反而更省电。
误区三:“时间片固定不变”。CFS 用权重和虚拟时间动态分配,nice 值改变会直接改变 CPU 份额。
复盘时建议固定三个问题:任务属于哪个调度类?它应得多少 CPU 份额?当前系统是否在响应性、吞吐和功耗之间做出了合理取舍?把这三个问题问清楚,调度问题就不再是“玄学”,而是一条可以量化、可以验证的工程链路。