3、Linux内核功耗管理框架:CPU Freq系统、CPU Idle系统、Device PM框架、Runtime PM机制
功耗管理这件事,说白了就是一句话:在性能与省电之间找到那个最舒服的平衡点。我做了这么多年嵌入式,见过太多产品因为功耗没处理好,要么发热严重被用户骂,要么续航太短被市场淘汰。
Linux内核的功耗管理,不是靠某一个模块单打独斗。它是一套组合拳。今天我们就把这套组合拳拆开来看——CPU Freq、CPU Idle、Device PM、Runtime PM。这四个东西,你搞明白了,功耗调优就入门了。
3.1 CPU Freq系统:动态调频的艺术
CPU Freq,全称是CPU Frequency Scaling。它干的事很简单:根据当前负载,动态调整CPU的工作频率和电压。负载高就提频,负载低就降频。这玩意儿在手机、平板、IoT设备上几乎是标配。
我印象很深,有一次做一款工业平板,CPU动不动就飙到85度。后来一查,是CPU Freq的governor配错了,一直跑在performance模式。换成ondemand之后,温度直接降了15度,用户体验好了不少。
3.1.1 核心概念:P-State
P-State(Performance State)是CPU Freq的基本单位。每个P-State对应一组频率+电压的组合。频率越高,电压通常也越高,功耗自然就上去了。
| P-State | 频率 (MHz) | 电压 (mV) | 典型功耗 (mW) | 适用场景 |
|---|---|---|---|---|
| P0 | 2000 | 1200 | 3500 | 突发高负载 |
| P1 | 1500 | 1050 | 2200 | 中度负载 |
| P2 | 1000 | 900 | 1200 | 日常使用 |
| P3 | 500 | 750 | 500 | 待机/轻负载 |
3.1.2 Governor:谁来决策调频?
Governor是CPU Freq的决策者。它决定“什么时候该调频、调到哪个P-State”。Linux内核里常用的governor有这么几个:
- performance:一直跑最高频。性能拉满,功耗也拉满。适合跑分场景。
- powersave:一直跑最低频。省电,但卡顿明显。
- ondemand:负载上来立刻提频,负载下去慢慢降频。响应快,但频繁调频会带来额外开销。
- conservative:调频更平滑,升频降频都慢半拍。省电,但响应慢。
- schedutil:基于调度器的负载信息做决策。这是目前最推荐的方式,精度高、延迟低。
我个人习惯,新项目首选schedutil。它跟调度器配合得好,不会出现“明明CPU很闲却还在高频跑”的尴尬情况。
3.1.3 用户态接口
CPU Freq的配置都在/sys/devices/system/cpu/cpuX/cpufreq/下面。常用的文件:
# 查看当前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
# 设置最大/最小频率(单位:kHz)
echo 1000000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
echo 500000 > /sys/devices/system/cpu/cpu0/cpufreq/scaling_min_freq
3.2 CPU Idle系统:让CPU真正歇下来
CPU Freq管的是“跑多快”,CPU Idle管的是“歇多深”。当CPU没有任务可执行时,它会进入空闲状态。空闲状态越深,功耗越低,但唤醒延迟也越大。
3.2.1 C-State:CPU的睡眠等级
C-State是CPU Idle的核心概念。C0是运行态,C1是浅度睡眠,C2/C3是深度睡眠。不同架构的C-State定义不一样,但思路是相通的。
| C-State | 名称 | 功耗 (mW) | 唤醒延迟 (us) | 说明 |
|---|---|---|---|---|
| C0 | 运行态 | 2000 | 0 | CPU正在执行指令 |
| C1 | Halt | 800 | 1-2 | 停止指令执行,保持缓存 |
| C2 | Stop-Clock | 300 | 10-20 | 关闭部分时钟 |
| C3 | Deep Sleep | 50 | 50-100 | 关闭PLL,丢失缓存内容 |
你想想看,如果系统大部分时间都处于C0,那功耗肯定下不来。但如果你让CPU频繁进入C3,每次唤醒都要花100us,响应速度就慢了。这就是延迟与功耗的博弈。
3.2.2 空闲调度器:谁来选C-State?
Linux内核里,menu governor是默认的空闲调度器。它会根据历史空闲时间和预测模型,选择一个合适的C-State。如果预测接下来会空闲很久,就选深睡眠;如果只是短暂空闲,就选浅睡眠。
我记得有一次,一个客户反映他们的设备待机功耗偏高。我一看,CPU Idle的统计数据显示,C2的进入次数是C3的10倍。这说明空闲调度器预测不准,总是选浅睡眠。后来调整了pm_qos的延迟约束,问题就解决了。
3.2.3 查看CPU Idle状态
# 查看每个CPU的C-State统计
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/time
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage
# 禁用某个C-State(不推荐,仅调试用)
echo 1 > /sys/devices/system/cpu/cpu0/cpuidle/state3/disable
cpuidle工具或者trace-cmd抓取CPU Idle事件,可以清楚看到每个CPU在什么时间进入了哪个C-State。这对定位“谁在阻止CPU睡深”非常有帮助。
3.3 Device PM框架:设备电源管理的总指挥
CPU省电只是第一步。系统里还有一大堆外设——Wi-Fi、蓝牙、传感器、显示屏……这些设备如果不管理好,功耗照样下不来。
Device PM框架就是干这个的。它提供了一套统一的接口,让设备驱动可以注册自己的电源管理回调函数。系统休眠时,框架会按顺序调用这些回调,让设备进入低功耗模式。
3.3.1 系统休眠流程
Linux支持多种休眠方式:Suspend-to-RAM(STR)、Suspend-to-Disk(STD)、Standby等。以STR为例,流程是这样的:
- 冻结用户空间进程:停止所有用户态任务。
- 暂停设备:按设备的依赖关系,依次调用驱动的
suspend()回调。 - 关闭非引导CPU:只保留一个CPU用于唤醒。
- 进入睡眠状态:系统整体进入低功耗模式。
- 唤醒:外部中断触发,反向执行上述步骤。
这里面最容易出问题的就是第二步。如果某个驱动的suspend()实现有问题,整个系统可能就睡不下去,或者睡下去醒不来。
3.3.2 设备电源管理回调
设备驱动需要实现struct dev_pm_ops结构体:
static int my_device_suspend(struct device *dev) {
// 保存寄存器状态
// 关闭设备时钟
// 切断设备电源
return 0;
}
static int my_device_resume(struct device *dev) {
// 恢复设备电源
// 恢复时钟
// 恢复寄存器状态
return 0;
}
static const struct dev_pm_ops my_device_pm_ops = {
.suspend = my_device_suspend,
.resume = my_device_resume,
};
3.4 Runtime PM机制:设备级的按需供电
系统休眠是“一刀切”——要么全睡,要么全醒。但很多时候,我们只需要让某个设备在空闲时自动休眠,而不是等整个系统都休眠。
Runtime PM就是干这个的。它让设备在运行时,根据使用情况自动进入或退出低功耗状态。说白了,就是“用的时候供电,不用的时候断电”。
3.4.1 核心概念:引用计数
Runtime PM的核心是一个引用计数器。当设备被使用时,引用计数加1;当设备空闲时,引用计数减1。计数为0时,自动触发休眠。
// 使用设备前,增加引用计数
pm_runtime_get_sync(dev);
// 使用设备后,减少引用计数
pm_runtime_put_sync(dev);
3.4.2 驱动中的实现
驱动需要实现Runtime PM的回调:
static int my_device_runtime_suspend(struct device *dev) {
// 关闭设备电源
// 保存必要的上下文
dev_dbg(dev, "Runtime suspend\n");
return 0;
}
static int my_device_runtime_resume(struct device *dev) {
// 恢复设备电源
// 恢复上下文
dev_dbg(dev, "Runtime resume\n");
return 0;
}
static const struct dev_pm_ops my_device_pm_ops = {
.runtime_suspend = my_device_runtime_suspend,
.runtime_resume = my_device_runtime_resume,
};
3.4.3 启用Runtime PM
// 在probe函数中启用
pm_runtime_enable(dev);
pm_runtime_set_autosuspend_delay(dev, 1000); // 1秒空闲后自动休眠
pm_runtime_use_autosuspend(dev);
3.4.4 用户态接口
# 查看Runtime PM状态
cat /sys/devices/.../power/control
cat /sys/devices/.../power/runtime_status
# 强制设备进入休眠
echo auto > /sys/devices/.../power/control
# 禁止设备自动休眠
echo on > /sys/devices/.../power/control
3.5 四者协同:一个完整的功耗管理场景
说了这么多,这四个东西到底怎么配合?我举个例子:
假设你有一台平板电脑,用户正在看电子书。这时候:
- CPU Idle:CPU大部分时间处于C2或C3状态,因为看书不需要太多计算。
- CPU Freq:偶尔翻页时,CPU负载上升,schedutil governor迅速提频到P1或P0。翻页完成后,又降回P3。
- Runtime PM:触摸屏在用户停止操作5秒后,自动进入休眠。Wi-Fi在后台下载完书籍后,也自动休眠。
- Device PM:如果用户长时间不操作,系统进入Suspend-to-RAM,所有设备都进入低功耗模式。
你看,这四个机制各司其职,又相互配合。少了任何一个,功耗都很难做到最优。