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接口。下面是我常用的操作流程:

  1. 查看当前governor:cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
  2. 切换governor:echo schedutil > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
  3. 查看可用频率:cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies
  4. 手动设置频率(仅限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流程串起来了。建议你仔细看看,尤其是箭头方向——它代表了「谁驱动谁」。

cpufreq 驱动框架与 DVFS 流程图 cpufreq 驱动框架与 DVFS 流程图 用户空间 (sysfs / 调频服务) Governor 策略层 performance powersave ondemand conservative schedutil cpufreq 核心层 (cpufreq_policy) cpufreq 驱动层 (target_index / setpolicy) 硬件层:PLL (频率) + PMIC (电压) + OPP 表 升频:先升压 → 再升频 | 降频:先降频 → 再降压 DVFS 功耗公式 P ∝ C × V² × f 负载反馈 (PELT)

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管理、热管理时,你就能更清楚地知道「为什么这么设计」了。