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的工作流程:
- 调度器更新
util_avg(每次任务切换或周期性更新) - schedutil从调度器获取
util_avg - 根据
util_avg计算目标频率:freq = util_avg * max_freq / capacity - 调用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驱动层,看看频率到底是怎么写进硬件寄存器的。