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 调试与优化:我的三板斧

最后分享几个我常用的调试技巧:

  1. 打开CPUFreq的trace事件echo 1 > /sys/kernel/debug/tracing/events/cpufreq/enable,然后看trace日志,能清楚看到每次调频的触发原因。
  2. 使用ftrace分析调频延迟:调频本身也有开销。我遇到过因为调频太频繁导致性能反而下降的情况,用ftrace抓出来一看,每秒调频上百次。
  3. 检查中断上下文:CPUFreq的target_index回调可能在中断上下文中调用,所以不能睡眠。我踩过这个坑,在回调里调用了msleep,结果内核直接报scheduling while atomic

总结一下:CPUFreq的核心是「策略与机制分离」。你作为驱动开发者,重点是把target_index实现好,保证调频的稳定性和实时性。至于什么时候调、调多少,那是Governor的事,别越界。

下一章我会讲CPUFreq的调频延迟优化和热插拔场景下的处理。说实话,那才是真正考验功底的地方。