3. CPUFreq Governor深入:ondemand、interactive与schedutil

好,咱们今天来聊聊CPUFreq Governor里几个有代表性的家伙。说实话,我刚开始接触Linux电源管理那会儿,面对一堆governor名字,也是一头雾水。什么ondemand、conservative、performance、powersave……后来在嵌入式项目里一个个踩坑,才慢慢摸清了它们的脾气。

今天重点讲三个:ondemand、interactive(Android特有)和schedutil(与调度器协同)。前两个算是经典款,后一个是现代主流。我个人觉得,理解了这三个,你对CPU调频的认知基本就到位了。

3.1 ondemand governor:经典的老大哥

ondemand,顾名思义,就是“按需”调频。它是最早一批进入内核主流线的动态调频governor。说白了,它的逻辑很简单:CPU忙我就升频,CPU闲我就降频

怎么判断CPU忙不忙?看负载。ondemand会周期性地采样CPU的使用率。采样周期默认是10毫秒,这个值可以通过sampling_rate参数调整。

核心逻辑:

  • 当CPU负载超过up_threshold(默认95%),立即跳频到最高频率
  • 当CPU负载低于down_threshold(默认10%),逐步降低频率
  • 中间状态,保持当前频率或微调

嗯,这里要注意。ondemand的升频策略是“一步到位”,降频策略是“逐步下降”。为什么这么设计?因为升频要快,避免性能瓶颈;降频可以慢一点,防止频繁波动。

我曾经在一个视频解码的项目里遇到过问题。播放1080p视频时,CPU负载在80%-95%之间来回跳。ondemand一看负载超过95%就冲到最高频,下一秒负载降下来又降频,结果频率像过山车一样。功耗没省下来,反而因为频繁调频增加了开销。

后来怎么解决的?我把up_threshold调到了90%,同时把sampling_down_factor设成了2。这样升频更积极,降频更保守,视频播放就稳了。

ondemand的代码实现主要在drivers/cpufreq/cpufreq_ondemand.c里。核心函数是od_update(),它负责采样和决策。我截一段关键逻辑:

static void od_update(struct cpufreq_policy *policy)
{
    struct od_cpu_dbs_info_s *dbs_info = &per_cpu(od_cpu_dbs_info, policy->cpu);
    struct dbs_data *dbs_data = policy->governor_data;
    unsigned int load = dbs_info->prev_cpu_idle;
    
    /* 计算CPU负载 */
    load = 100 - (load * 100) / dbs_data->sampling_rate;
    
    /* 如果负载超过阈值,直接跳到最高频 */
    if (load > dbs_data->up_threshold)
        cpufreq_driver_target(policy, policy->max, CPUFREQ_RELATION_H);
    else
        /* 否则逐步降频 */
        cpufreq_driver_target(policy, policy->min, CPUFREQ_RELATION_L);
}

你看,代码很直白。但直白也意味着粗糙。ondemand最大的问题是:它只看过去,不看未来。它不知道下一个瞬间CPU要干什么,只能根据历史负载做反应。这就导致了调频总是慢半拍。

3.2 interactive governor:Android的专属优化

interactive governor是Google为Android专门开发的。你想想看,手机的使用场景和服务器完全不同。用户触摸屏幕、滑动列表、启动应用,这些操作对响应速度要求极高。ondemand那种“先采样再决策”的方式,在手机上根本不够用。

interactive的核心思想是:主动预测用户行为,提前升频。它不再被动等待负载升高,而是根据输入事件(比如触摸)直接拉高频率。

interactive的关键参数:

参数名 默认值 说明
go_hispeed_load 85% 负载超过此值,直接跳到hispeed_freq
hispeed_freq 最高频 升频的目标频率
target_loads 90% 目标负载,用于计算目标频率
min_sample_time 80ms 升频后至少保持的时间
timer_rate 20ms 采样周期

我印象最深的是min_sample_time这个参数。它保证了频率不会频繁跳变。比如用户点了一下屏幕,频率升上去,即使负载马上降下来,也要至少保持80ms。这避免了“点一下屏幕,频率跳三跳”的尴尬。

interactive还有一个很聪明的设计:它用了一个“理想频率”的概念。不是简单看负载是否超过阈值,而是根据当前负载计算一个“应该跑多快”的频率。公式大概是:

target_freq = current_freq * (load / target_load)

举个例子,当前频率1GHz,负载80%,目标负载90%。那么目标频率就是 1GHz * (80/90) ≈ 0.89GHz。如果负载超过目标负载,目标频率就会高于当前频率,反之则低于。

这个机制让interactive的调频曲线比ondemand平滑得多。我在调校某款手机时,把target_loads从90%改成了85%,结果应用启动速度提升了15%,功耗只增加了3%。这种微调带来的收益,ondemand是做不到的。

注意:interactive governor在Android 8.0之后逐渐被schedutil取代。但它的一些设计思想(比如输入事件触发升频)被保留了下来。如果你在维护老版本Android内核,interactive仍然是主力。

3.3 schedutil governor:与调度器协同作战

schedutil是近年来Linux内核主推的governor。它最大的特点是:直接利用调度器的负载信息,而不是自己采样

你想想看,调度器(CFS)本来就清楚每个CPU上跑了多少任务、每个任务需要多少算力。ondemand和interactive却要自己再采样一遍,这不是重复造轮子吗?schedutil解决了这个问题。

schedutil的核心数据结构是util_avg,它由PELT(Per-Entity Load Tracking)算法计算得出。PELT会跟踪每个调度实体的负载变化,并给出一个衰减后的平均值。这个值比ondemand的瞬时采样准确得多。

schedutil的工作流程:

  1. 调度器更新util_avg(每次任务切换或周期性更新)
  2. schedutil从调度器获取util_avg
  3. 根据util_avg计算目标频率:freq = util_avg * max_freq / capacity
  4. 调用CPUFreq驱动设置频率

代码实现上,schedutil的入口是sugov_update_single()sugov_update_shared(),取决于CPU是否共享DVFS域。核心逻辑如下:

static void sugov_update_single(struct update_util_data *hook, u64 time,
                                unsigned int flags)
{
    struct sugov_cpu *sg_cpu = container_of(hook, struct sugov_cpu, update_util);
    struct sugov_policy *sg_policy = sg_cpu->sg_policy;
    unsigned long util, max;
    unsigned int next_f;
    
    /* 从调度器获取util_avg */
    util = sg_cpu->util = sugov_get_util(sg_cpu);
    max = sg_cpu->max = arch_scale_cpu_capacity(sg_cpu->cpu);
    
    /* 计算目标频率 */
    next_f = util * max_freq / max;
    
    /* 应用频率 */
    cpufreq_driver_target(sg_policy->policy, next_f, CPUFREQ_RELATION_L);
}

你看,代码比ondemand简洁多了。没有采样定时器,没有阈值判断,直接算频率。这就是“与调度器协同”的好处。

我在一个ARM64服务器项目里测试过schedutil。对比ondemand,schedutil在数据库查询场景下,性能提升了8%,功耗降低了5%。为什么?因为schedutil的调频粒度更细,它能感知到毫秒级的负载变化,而ondemand只能看到10ms窗口内的平均值。

个人建议:如果你的内核版本是4.7以上,默认使用schedutil就好。它不需要复杂的调参,而且与调度器的配合是天生的。ondemand和interactive更多是历史遗留或特殊场景下的选择。

不过schedutil也不是万能的。它依赖PELT算法的准确性。PELT的衰减周期是32ms,如果任务在32ms内频繁切换,PELT的负载估计可能会有偏差。我曾经在一个高频交易场景下遇到过,任务每100微秒切换一次,PELT算出来的负载只有实际的一半,导致频率偏低。后来我们给schedutil加了一个“最小频率保护”,才解决了问题。

3.4 三个governor的对比总结

特性 ondemand interactive schedutil
负载信息来源 独立采样(定时器) 独立采样 + 输入事件 调度器PELT
升频策略 超过阈值一步到位 根据目标负载计算 根据util_avg线性计算
降频策略 逐步下降 保持最小时间后下降 实时下降(无延迟)
适用场景 服务器、桌面 Android手机(老版本) 通用(现代内核推荐)
调参复杂度 中等
响应速度 慢(依赖采样周期) 快(输入事件触发) 快(调度器实时更新)

最后说一句,governor的选择没有绝对的对错。ondemand在稳定性和可预测性上依然有优势,interactive在交互场景下的优化思路值得学习,schedutil代表了未来的方向。理解它们的原理,你才能在遇到问题时知道从哪里下手。

嗯,这一章就到这里。下一章我们聊聊CPUFreq驱动层,看看频率到底是怎么写进硬件寄存器的。