2、CPU频率调度基础:CPUFreq框架核心概念、governor工作原理、用户态与内核态调频策略
2.1 CPUFreq框架:内核里的“变速箱”
咱们先聊聊CPUFreq这个框架。说白了,它就是内核里的一套机制,专门用来动态调整CPU的运行频率和电压。
你想想看,CPU不可能永远跑在最高频。看电影的时候,CPU负载很低,跑高频就是浪费电。打游戏或者编译代码的时候,负载上来了,频率就得跟上。CPUFreq干的就是这个活——根据负载动态调节频率。
我个人习惯把CPUFreq理解成汽车的变速箱。低负载时挂低档(低频),省油;高负载时挂高档(高频),给足动力。内核里这套框架,就是负责自动换挡的。
核心数据结构
CPUFreq框架里有两个关键结构体:
- cpufreq_policy:每个CPU或每组CPU(policy)对应一个策略实例。里面记录了当前频率范围、governor、调频延迟等信息。
- cpufreq_driver:硬件相关的驱动层。负责真正去写寄存器、改变PLL配置。
我在项目中遇到过一个问题:某款SoC的CPUFreq驱动注册时,policy的初始化顺序没处理好,导致系统启动时频率一直锁在最低档。排查了半天,发现是驱动里target回调函数返回了错误码。嗯,这种坑踩过一次就记住了。
2.2 Governor工作原理:谁在决定频率?
Governor,翻译过来就是“调控器”。它决定了“什么时候该升频,什么时候该降频”。
内核里默认提供了好几种governor,咱们挑几个重点的聊:
| Governor名称 | 工作方式 | 适用场景 |
|---|---|---|
| performance | 始终跑在最高频率 | 追求极致性能,不在乎功耗 |
| powersave | 始终跑在最低频率 | 省电优先,性能无所谓 |
| userspace | 用户态程序手动设置频率 | 需要精细控制频率的场景 |
| ondemand | 根据CPU负载动态调频 | 通用场景,兼顾性能和功耗 |
| conservative | 类似ondemand,但调频更平滑 | 对频率抖动敏感的场景 |
| schedutil | 基于调度器负载信息调频 | 现代内核推荐,响应更快 |
ondemand的工作原理:它每隔一段时间(采样周期)检查CPU的负载。如果负载超过某个阈值(默认95%),就立刻升到最高频。如果负载降下来了,就逐步降频。
我曾经调试过一个功耗问题:平板电脑看视频时功耗偏高。查了一圈,发现是ondemand的采样周期设得太短(10ms),导致CPU频繁升频降频。改成50ms后,功耗降了15%。
小技巧:调频延迟(transition_latency)这个参数很关键。如果硬件切换频率需要100us,那governor的采样周期就不能小于这个值。否则调频还没完成,下一次采样又来了,系统会乱套。
2.3 用户态调频策略:把控制权交给应用
用户态调频,说白了就是让应用程序自己决定CPU跑多快。内核提供了一套sysfs接口,用户态程序可以直接读写。
常用的接口路径在/sys/devices/system/cpu/cpuX/cpufreq/下面:
# 查看当前频率
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
# 设置governor为userspace
echo userspace > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# 手动设置频率(需要先切换到userspace)
echo 1200000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
为什么需要用户态调频?我遇到过这样一个场景:某个音频处理应用,对CPU频率的稳定性要求极高。如果内核自动调频,频率忽高忽低,音频就会出现卡顿。解决方案就是:应用启动时,通过userspace governor把频率锁定在一个合适的值,处理完再释放。
注意:用户态调频虽然灵活,但风险也大。如果应用写了一个超出硬件支持范围的频率值,驱动会直接返回错误。更严重的是,如果应用忘记释放频率控制权,系统可能一直跑在高频,导致功耗失控。
2.4 内核态调频策略:schedutil的崛起
内核态调频,最典型的就是schedutil governor。它是调度器(scheduler)和CPUFreq的“联姻产物”。
传统governor(比如ondemand)有个问题:它只能看到CPU的“平均负载”,看不到每个任务的实时需求。举个例子:一个任务突然被唤醒,ondemand可能要等到下一个采样周期才能发现负载变化,响应延迟至少几十毫秒。
schedutil就不一样了。它直接从调度器获取每个CPU的运行队列信息,包括:
- 当前CPU的利用率(utilization)
- 任务的预估需求(task demand)
- CPU的容量(capacity)
有了这些信息,schedutil可以做到“按需调频”,而且是实时的。调度器每次更新任务状态时,都会触发频率调整。
我记得在某个ARM64服务器项目上,从ondemand切换到schedutil后,数据库查询的P99延迟降低了30%。原因很简单:schedutil能更快地响应突发负载,CPU频率提前拉起来了。
schedutil的核心公式(简化版):
next_freq = current_freq * (util / capacity_max)
其中util是当前CPU利用率,capacity_max是CPU的最大算力。这个公式保证了频率和负载成比例变化。
2.5 避坑指南:调频策略选择建议
讲了这么多,到底该用哪个策略?我根据自己的经验,给几个建议:
- 服务器场景:优先用schedutil。响应快,能效比高。内核版本4.7以上都支持。
- 嵌入式/移动设备:ondemand或者conservative。如果对功耗敏感,可以试试conservative,调频更平滑。
- 实时性要求高的场景:用userspace,手动锁定频率。避免内核调频带来的延迟抖动。
- 调试阶段:先用performance governor排除调频干扰,定位问题后再切回动态调频。
我曾经犯过一个错误:在某个IoT设备上直接用了performance governor,结果电池续航从两天缩水到半天。后来改成ondemand,配合适当的采样周期,续航恢复到一天半。嗯,选对governor真的很重要。
调试小工具:cpupower命令可以查看和设置CPUFreq参数。比如cpupower frequency-info能列出所有支持的频率和governor。我调试时经常用它。
好了,这一章的内容就到这里。CPUFreq框架是Linux电源管理的基石,理解了它,后面讲CPU idle和热管理就会轻松很多。下一章咱们聊聊CPU idle框架,看看CPU空闲时是怎么省电的。