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性能调优,说到底就是三件事:

  1. 调度机制:选对调度策略,别让关键任务被抢。
  2. 负载均衡:让每个核的负载尽量均匀,别出现“旱的旱死,涝的涝死”。
  3. 频率与绑定:用schedutil调频,用affinity绑核,用isolcpus隔离。

下一章,咱们聊聊内存调优。内存这块,坑比CPU还多,尤其是低内存场景下的卡顿和OOM。到时候我分享一个我踩过的“内存泄漏导致系统重启”的案例,保证让你印象深刻。