2. CPUFreq核心框架:从策略到驱动的完整链路
CPUFreq子系统,说白了就是Linux内核里负责动态调频的那一套机制。我最早接触它是在做一款平板电脑的功耗优化时,那时候电池续航撑不过4小时,最后发现是CPUFreq的配置完全没起作用。嗯,从那以后我就对这个模块格外上心了。
这一章,我会带你从架构层面拆解CPUFreq。你不需要一下子记住所有细节,先理解它「为什么这么设计」更重要。
2.1 CPUFreq子系统架构:三层分离
CPUFreq的架构设计得很清晰,它把调频这件事分成了三个层次:
- 核心层(Core):负责管理策略、通知事件、提供统一的用户空间接口。说白了就是「调度中心」。
- 调频器层(Governor):决定「什么时候该调频」。比如负载高了就升频,空闲了就降频。
- 驱动层(Driver):负责「怎么调频」。比如写寄存器、配置PLL、切换电压。
我个人习惯把这三层比作「老板-经理-工人」的关系。Governor是经理,它根据负载情况做决策;Driver是工人,它执行具体的调频动作;Core则是老板,它确保整个流程合规、有序。
核心要点:CPUFreq的架构设计遵循了「策略与机制分离」的原则。Governor只关心「调不调」,Driver只关心「怎么调」。这样换一个平台,只需要重写Driver,Governor可以复用。
2.2 CPUFreq策略(Policy):调频的「游戏规则」
Policy是CPUFreq里一个非常重要的概念。它描述了一组CPU核心的调频范围、调频策略以及关联的Governor。
每个Policy对象通常对应一个或多个CPU核心(比如一个cluster)。它包含以下关键信息:
- cpuinfo.min_freq / cpuinfo.max_freq:硬件支持的频率范围,由Driver上报。
- min / max:用户或Governor设置的软限制,不能超出硬件范围。
- governor:当前绑定的调频器名称。
- related_cpus:受该Policy影响的所有CPU核心。
我在项目中遇到过一个问题:某个SoC的大小核架构,大核和小核各自有独立的Policy。但调试时发现,小核的Policy里居然包含了所有CPU,导致调频混乱。后来查代码才发现是设备树里cpu-supply节点配错了。
避坑指南:我曾经因为Policy的related_cpus配置错误,导致一个核心调频时影响了其他核心。检查设备树时一定要确认每个Policy的CPU掩码是否正确。
2.3 Governor:五种调频策略的实战选择
Linux内核目前支持五种主要的Governor。我按使用频率从高到低给你捋一遍:
| Governor | 行为特点 | 适用场景 |
|---|---|---|
| performance | 始终运行在最高频率 | 追求极致性能,不考虑功耗 |
| powersave | 始终运行在最低频率 | 后台任务、待机场景 |
| ondemand | 负载高时升频,空闲时降频 | 通用场景,兼顾性能与功耗 |
| conservative | 类似ondemand,但频率变化更平滑 | 对频率抖动敏感的场景 |
| userspace | 由用户程序直接设置频率 | 调试、特殊调优需求 |
| schedutil | 基于调度器负载信息调频 | 新内核推荐,响应更及时 |
你想想看,为什么ondemand用了这么多年还没被淘汰?因为它简单、稳定。但如果你用的是4.7以上的内核,我建议你优先考虑schedutil。它直接从调度器拿负载数据,比ondemand的采样方式更精准。
我记得有一次做视频播放的功耗优化,ondemand在播放过程中频繁升降频,导致画面卡顿。换成conservative后,频率变化平滑了,功耗反而还降了5%。嗯,有时候「慢半拍」反而是好事。
2.4 CPUFreq驱动开发:从零实现一个调频驱动
写一个CPUFreq驱动,其实没那么玄乎。核心就是实现几个回调函数:
static struct cpufreq_driver my_cpufreq_driver = {
.name = "my_cpufreq",
.init = my_cpufreq_init,
.verify = my_cpufreq_verify,
.target_index = my_cpufreq_set_target,
.get = my_cpufreq_get,
.flags = CPUFREQ_ASYNC_NOTIFICATION,
};
这几个回调的作用分别是:
- init:初始化驱动,上报支持的频率表。
- verify:验证用户设置的频率是否合法。
- target_index:核心函数,根据频率表索引设置频率。
- get:读取当前频率。
我刚开始写驱动时,犯过一个低级错误:在target_index里直接调用了clk_set_rate,但忘了先检查频率是否在支持的范围内。结果用户设置了一个不支持频率,系统直接挂了。后来我学乖了,每次调频前都先走一遍verify。
个人经验:开发CPUFreq驱动时,建议先在init里打印出完整的频率表。这样调试时能一眼看出硬件上报的频率是否合理。我曾经遇到过硬件返回的频率表是乱序的,导致调频逻辑全乱套。
2.5 频率表与电压缩放:调频背后的物理约束
调频不是单纯改个时钟频率就完事了。频率升高,电压也得跟上,否则芯片会不稳定。这就是所谓的DVFS(动态电压频率缩放)。
CPUFreq驱动里,频率表通常是一个结构体数组:
static struct cpufreq_frequency_table my_freq_table[] = {
{ .frequency = 1000000, .driver_data = 0, }, // 1GHz
{ .frequency = 1200000, .driver_data = 1, }, // 1.2GHz
{ .frequency = 1400000, .driver_data = 2, }, // 1.4GHz
{ .frequency = CPUFREQ_TABLE_END, },
};
这里的driver_data可以存放电压值、PLL配置参数等。我个人习惯把电压信息也放在这个表里,这样调频时能同时设置频率和电压,保证稳定性。
注意:频率和电压的对应关系必须严格匹配。我曾经因为电压表配错,导致芯片在1.4GHz时电压只有1.0V,结果跑几分钟就死机。后来用示波器抓了波形才发现电压纹波太大。
2.6 用户空间接口:/sys文件系统
CPUFreq通过sysfs向用户空间暴露接口。你可以在/sys/devices/system/cpu/cpuX/cpufreq/下看到这些文件:
- scaling_available_governors:列出当前可用的Governor。
- scaling_governor:设置或查看当前Governor。
- scaling_min_freq / scaling_max_freq:设置频率范围。
- cpuinfo_cur_freq:读取当前频率。
- stats/:调频统计信息。
我在调试时经常用这个命令:
echo "schedutil" > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_cur_freq
嗯,就这么简单。但要注意,有些嵌入式平台可能没有完全暴露所有接口,这取决于内核配置。
2.7 调试与优化:我的三板斧
最后分享几个我常用的调试技巧:
- 打开CPUFreq的trace事件:
echo 1 > /sys/kernel/debug/tracing/events/cpufreq/enable,然后看trace日志,能清楚看到每次调频的触发原因。 - 使用ftrace分析调频延迟:调频本身也有开销。我遇到过因为调频太频繁导致性能反而下降的情况,用ftrace抓出来一看,每秒调频上百次。
- 检查中断上下文:CPUFreq的
target_index回调可能在中断上下文中调用,所以不能睡眠。我踩过这个坑,在回调里调用了msleep,结果内核直接报scheduling while atomic。
总结一下:CPUFreq的核心是「策略与机制分离」。你作为驱动开发者,重点是把target_index实现好,保证调频的稳定性和实时性。至于什么时候调、调多少,那是Governor的事,别越界。
下一章我会讲CPUFreq的调频延迟优化和热插拔场景下的处理。说实话,那才是真正考验功底的地方。