2、CPU性能调优:CPU调度机制、负载均衡策略、频率调节与调频器选择、核心绑定与隔离
好,咱们进入第二个章节。CPU调优这块,说实在的,是系统性能优化的硬骨头。我早年刚接触Android系统时,总觉得CPU调度是内核的事,上层应用管不着。后来被线上问题狠狠教育了一回——某个视频App在低端机上卡成PPT,查了半天,发现是大核没被用上,小核跑满了还在那硬扛。嗯,从那以后,我再也不敢小看CPU调优了。
2.1 CPU调度机制:谁来决定下一个跑谁?
CPU调度器,说白了就是操作系统的“交通指挥员”。它决定下一个该让哪个线程上CPU跑。Android用的是CFS(Completely Fair Scheduler),完全公平调度器。名字听着挺理想,但实际用起来,坑不少。
CFS的核心思路很简单:给每个线程分配一个“虚拟运行时间”(vruntime)。谁跑得少,谁就先上CPU。它维护一棵红黑树,vruntime最小的线程在最左边,调度时直接取出来。
我个人的习惯是,先看调度器的几个关键参数:
- sched_latency_ns:调度延迟,默认6ms。这个值越小,交互响应越快,但上下文切换开销也越大。
- min_granularity_ns:最小调度粒度,默认0.75ms。线程至少跑够这个时间才会被抢走。
- wakeup_granularity_ns:唤醒抢占粒度,默认1ms。被唤醒的线程能不能抢走当前正在跑的线程,看这个。
关键点:CFS不是实时调度器。它追求的是“公平”,而不是“低延迟”。如果你有音频、触控这类实时性要求高的任务,得考虑RT调度策略(SCHED_FIFO或SCHED_RR)。
我曾经在一个直播项目中,发现推流线程偶尔会被其他后台任务抢走CPU,导致音频断断续续。查了调度日志,发现推流线程用的是SCHED_NORMAL(普通调度)。改成SCHED_FIFO,优先级设到90,问题就解决了。但注意,RT线程用多了,普通线程可能饿死,要谨慎。
2.2 负载均衡策略:别让一个核累死,其他核闲着
多核CPU最怕什么?怕负载不均。一个核跑满100%,其他核才20%。这就像团队里一个人干到死,其他人摸鱼。负载均衡就是解决这个问题的。
Android的负载均衡主要在调度域(Scheduling Domain)层面做。系统把CPU分成不同的层级:
| 层级 | 说明 | 例子 |
|---|---|---|
| MC(Multi-Core) | 同一个簇内的多个核心 | 小核簇内的4个核 |
| DIE(Die) | 同一个芯片上的不同簇 | 大核簇与小核簇之间 |
| NUMA(非一致性内存访问) | 不同内存节点 | 服务器场景,手机少见 |
负载均衡的触发时机有两个:
- 周期性均衡:每1ms左右检查一次,把任务从忙的核迁移到闲的核。
- 空闲时均衡:某个核空闲了,主动去别的核“偷”任务过来跑。
你想想看,如果一个大核簇的核都在忙,小核簇却闲着,系统会不会把小核的任务迁移到大核?会的。但反过来,大核的任务很少迁移到小核,因为大核跑得快,迁移成本高。
调优技巧:查看当前系统的负载均衡情况,可以用 /proc/schedstat 和 /proc/loadavg。我一般先看 cat /proc/loadavg,如果负载值接近CPU核心数,说明系统快饱和了。
2.3 频率调节与调频器选择:省电还是性能?这是个问题
CPU频率调节,就是动态调整CPU的运行频率。频率越高,跑得越快,但功耗也越大。Android默认用的是 cpufreq 框架,它支持多种调频器(Governor)。
常见的调频器有这些:
| 调频器 | 策略 | 适用场景 |
|---|---|---|
| performance | 一直跑最高频率 | 跑分、游戏 |
| powersave | 一直跑最低频率 | 待机、省电 |
| ondemand | 负载高时升频,负载低时降频 | 通用场景 |
| interactive | 根据最近负载平滑调频 | 交互式应用(Android 4.4之前常用) |
| schedutil | 基于调度器负载信息调频 | Android 5.0之后推荐 |
我个人强烈推荐 schedutil。为什么?因为它直接利用CFS调度器算出来的负载信息,而不是像ondemand那样自己去采样。采样总有延迟,schedutil的响应更快,也更省电。
我记得有一次,某款手机在刷微博时发热严重。查了调频器,发现用的是ondemand。ondemand的升频阈值是95%,也就是说CPU负载到95%才升频。但微博这种应用,负载是突发的,等ondemand反应过来,频率已经跟不上,用户就感觉卡。换成schedutil后,负载感知更及时,频率提前拉高,卡顿消失了,发热反而降了——因为任务跑得快,早点进空闲。
注意:调频器不是万能的。如果系统本身负载就高,调频器再智能也救不了。先优化应用逻辑,再考虑调频策略。
2.4 核心绑定与隔离:把关键任务“锁”在指定核上
核心绑定(CPU Affinity),就是强制某个线程或进程只能在指定的CPU核心上运行。隔离(CPU Isolation),则是把某些核心从调度器中“隐藏”起来,不让普通任务跑上去。
为什么要做绑定?举个例子:
- 音频处理线程,绑定到某个小核上,避免被大核的突发任务干扰。
- 渲染线程,绑定到大核上,保证渲染帧率稳定。
- 后台下载任务,绑定到小核上,别去抢大核资源。
在Android中,设置核心绑定的方式有两种:
// 方法一:通过taskset命令
taskset -p 0x0F <pid> // 绑定到CPU 0-3(小核)
// 方法二:通过sched_setaffinity系统调用
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0, &mask); // 绑定到CPU 0
CPU_SET(1, &mask); // 绑定到CPU 1
sched_setaffinity(0, sizeof(mask), &mask);
核心隔离更狠。它通过内核启动参数 isolcpus 来指定哪些核心不参与调度。比如:
isolcpus=4,5,6,7 // 把大核全部隔离
隔离后的核心,只有通过显式的 sched_setaffinity 才能使用。这通常用于实时任务或专用计算任务。
我曾经在一个车载项目中,把导航的定位计算线程绑定到隔离的大核上。这样即使其他应用卡顿,导航的定位也不会受影响。效果很明显,定位延迟从平均50ms降到了10ms以内。
避坑指南:核心绑定不要绑得太死。比如你把所有线程都绑到同一个核上,那其他核就闲着,性能反而下降。我一般建议,关键线程绑定到特定核,普通线程保持默认调度。
小结
CPU性能调优,说到底就是三件事:
- 调度机制:选对调度策略,别让关键任务被抢。
- 负载均衡:让每个核的负载尽量均匀,别出现“旱的旱死,涝的涝死”。
- 频率与绑定:用schedutil调频,用affinity绑核,用isolcpus隔离。
下一章,咱们聊聊内存调优。内存这块,坑比CPU还多,尤其是低内存场景下的卡顿和OOM。到时候我分享一个我踩过的“内存泄漏导致系统重启”的案例,保证让你印象深刻。