4. CPU调频与调压:cpufreq驱动框架、governor策略与DVFS原理
CPU调频调压,说白了就是让芯片在「跑得快」和「吃得少」之间找个平衡点。我入行那会儿,调频还是个挺粗糙的活——要么全速跑,要么睡大觉。现在不一样了,内核里有一套完整的cpufreq框架,配合各种governor策略,再加上硬件层面的DVFS(动态电压频率调整),已经能做到毫秒级的精细调节。
这一章,咱们就把这套体系拆开看看。
4.1 cpufreq驱动框架:谁在管这件事?
cpufreq是Linux内核里负责CPU频率和电压管理的核心子系统。它分三层:
- 核心层(cpufreq core):提供统一的接口,给上层governor和下层驱动用。
- Governor层:决定「什么时候调、调到多少」。后面会细讲。
- 驱动层(cpufreq driver):直接操作硬件寄存器,真正改变频率和电压。
我习惯把这三层比作「老板-经理-工人」的关系。Governor是老板,定策略;驱动是工人,干苦力;核心层就是那个传话的HR。
关键数据结构:cpufreq_policy
每个CPU(或每组共享电压域的CPU)对应一个policy。它记录了当前频率范围、可用频率表、governor指针等信息。调频调压的所有决策,都围绕这个policy展开。
驱动层最核心的接口是target_index()回调。内核传一个频率索引过来,驱动就去查表,然后写硬件寄存器。嗯,这里要注意:很多SoC的CPU和GPU共享同一个电压域,调CPU频率时GPU那边也得跟着变——我在项目中踩过这个坑,后面会讲。
4.2 Governor策略:五种调频哲学
Linux内核目前支持5种governor,每种代表一种调频思路。咱们一个一个看。
| Governor | 策略 | 适用场景 | 我的评价 |
|---|---|---|---|
| performance | 始终最高频 | 跑分、低延迟要求 | 简单粗暴,功耗爆炸 |
| powersave | 始终最低频 | 极致省电、后台任务 | 省电但卡顿明显 |
| ondemand | 负载突变时跳频 | 传统手机、桌面 | 响应快,但频繁跳频 |
| conservative | 负载渐变时缓调 | 服务器、嵌入式 | 平滑,但反应慢 |
| schedutil | 基于调度器负载 | 现代内核首选 | 精准,延迟低 |
4.2.1 performance & powersave
这两个是「极端派」。performance把频率锁死在最高档,powersave锁死在最低档。说实话,实际产品里很少直接用——除非你在跑benchmark,或者做深度休眠前的准备。
我记得有一次帮客户调一个IoT网关,他们用了powersave,结果MQTT消息延迟从50ms飙到800ms。后来换成schedutil,问题就解决了。你想想看,省电和省性能之间,得有个度。
4.2.2 ondemand
ondemand是经典策略。它每隔一段时间(采样周期,默认10ms)检查CPU负载,如果负载超过阈值(默认95%),就立刻跳到最高频;如果负载降下来,再逐步降频。
这个策略的问题在于「跳变太猛」。负载一上来就满频,负载一下去就降频,来回折腾。我在一个多媒体项目里遇到过:播放视频时CPU负载在40%~60%之间波动,ondemand让频率在1.2GHz和1.8GHz之间反复横跳,功耗反而比一直跑1.5GHz还高。
避坑指南: 我曾经在某个ARM平台上发现ondemand的采样周期设得太短(1ms),导致CPU几乎每毫秒都在调频,功耗增加了15%。后来把采样周期调到20ms,配合负载阈值从95%降到80%,效果好了很多。
4.2.3 conservative
conservative是ondemand的「温和版」。它不会一下子跳到最高频,而是逐级上调。比如当前频率是1.0GHz,负载高了,它先升到1.2GHz,再观察,不够再升到1.4GHz。
这种策略适合对响应速度要求不高的场景,比如后台数据采集、日志写入。但说实话,现在用conservative的人越来越少了——schedutil做得更好。
4.2.4 schedutil(现代首选)
schedutil是Linux 4.7引入的,基于调度器(scheduler)的负载信息来做调频决策。它不像ondemand那样靠采样周期去猜,而是直接从调度器拿到每个CPU的实时利用率(utilization)。
为什么说它好?因为调度器知道每个任务占用了多少CPU时间,schedutil根据这个数据,用PELT(Per-Entity Load Tracking)算法算出「未来需要多少算力」,然后精确调频。
我个人的经验是:schedutil在大多数场景下都能比ondemand省电10%~20%,同时响应延迟更低。尤其是跑交互式应用(比如触控、UI渲染),schedutil的平滑度明显更好。
小技巧: 如果你的内核版本支持schedutil(4.7+),建议默认就用它。但要注意,schedutil依赖调度器的负载跟踪,如果系统里跑了很多短生命周期线程(比如频繁fork的脚本),负载跟踪会不准,这时候可以回退到ondemand。
4.3 DVFS原理:电压和频率为什么必须一起调?
DVFS(Dynamic Voltage and Frequency Scaling)不是简单的「降频就省电」。这里有个物理常识:功耗 ≈ 电容 × 电压² × 频率。
你看,电压是平方项,影响最大。但频率和电压是绑定的——频率越高,需要的电压也越高(因为要保证信号在时钟周期内稳定传输)。所以调频的同时必须调压,否则芯片会出错。
每个SoC都有一个频率-电压表(OPP,Operating Performance Point),比如:
1.8 GHz → 1.15 V
1.5 GHz → 1.05 V
1.2 GHz → 0.95 V
0.8 GHz → 0.85 V
DVFS的核心就是:根据负载,选一个合适的OPP,然后同时写PLL(锁相环)改频率、写PMIC(电源管理芯片)改电压。
这里有个关键点:升频时先升压再升频,降频时先降频再降压。顺序搞反了,芯片就会因为电压不足而crash。我在一个项目里就遇到过——驱动工程师把顺序写反了,结果每次升频都死机,查了两天才找到原因。
4.4 实战:如何配置cpufreq
在嵌入式Linux里,配置cpufreq通常通过sysfs接口。下面是我常用的操作流程:
- 查看当前governor:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor - 切换governor:
echo schedutil > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor - 查看可用频率:
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies - 手动设置频率(仅限userspace governor):
echo 1200000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed
如果你用的是设备树(DT),可以在cpu节点里指定OPP表:
cpu0: cpu@0 {
compatible = "arm,cortex-a53";
operating-points = <
1800000 1150000
1500000 1050000
1200000 950000
800000 850000
>;
};
嗯,这里要注意:不同SoC的OPP表差异很大,同一颗芯片在不同温度下也可能需要不同的电压。有些SoC还支持AVS(Adaptive Voltage Scaling),能在出厂时根据芯片体质微调电压——这个后面讲功耗校准时会细说。
4.5 一张图看懂cpufreq体系
下面这张SVG图,把cpufreq的层级关系、governor策略和DVFS流程串起来了。建议你仔细看看,尤其是箭头方向——它代表了「谁驱动谁」。
4.6 调频调压的常见坑
最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路。
- 坑1:共享电压域。有些SoC的CPU和GPU共享一路电压,调CPU频率时GPU那边也得跟着调。我遇到过只调CPU不调GPU,结果GPU在高负载下电压不足,导致画面撕裂。
- 坑2:瞬态响应。频率切换不是瞬间完成的,PLL锁定需要几十微秒。如果切换太频繁,这段时间CPU是「空转」的,反而浪费功耗。
- 坑3:温度补偿。高温下芯片漏电流增大,同样的频率可能需要更高的电压。有些SoC有热敏二极管,驱动里会根据温度微调OPP表——这个在低温环境下尤其重要。
- 坑4:中断上下文调频。cpufreq的调频操作可能会睡眠(比如I2C写PMIC),不能在中断上下文里直接调。我见过有人在中断里调频,结果内核直接panic。
我的建议: 如果你刚开始调cpufreq,先别急着改代码。用sysfs把各个governor都试一遍,配合perf stat和powertop看功耗和性能数据。数据说话,比拍脑袋靠谱得多。
好了,这一章的内容就到这里。cpufreq框架和DVFS原理是功耗调优的基石,理解了这些,后面讲suspend/resume、idle管理、热管理时,你就能更清楚地知道「为什么这么设计」了。