3. Linux电源管理框架:CPUIdle、CPUFreq、Devfreq、Runtime PM、Suspend-to-RAM/Idle

电源管理,说白了就是让系统在「干活」和「歇着」之间找到平衡。我做了这么多年嵌入式,见过太多产品因为功耗问题翻车——有的电池撑不过半天,有的发热到烫手。Linux内核的电源管理框架,就是帮你解决这些问题的工具箱。

这一章,我们把这几个核心组件拆开揉碎了讲。你不需要一次性全记住,但得知道它们各自管什么、怎么配合。

3.1 CPUIdle:让CPU学会「打盹」

CPUIdle管理的是CPU空闲时的状态。当CPU没有任务可跑时,它不会傻等着,而是进入更深度的睡眠模式。

核心思路:空闲时间越长,睡得越深,省电越多。但醒来也需要时间,这是个权衡。

3.1.1 空闲状态层级

ARM架构下,CPUIdle通常有这几个状态:

状态 名称 功耗节省 唤醒延迟 典型场景
WFI 等待中断 ~30% 几微秒 短时空闲
CPU睡眠 关闭时钟 ~60% 几十微秒 中等空闲
Cluster睡眠 关闭电源域 ~90% 几百微秒 长时空闲

嗯,这里要注意:不是所有平台都支持所有状态。具体支持哪些,要看你的SoC设计。

3.1.2 调度器与CPUIdle的配合

调度器发现CPU空闲时,会调用CPUIdle框架。框架根据预测的空闲时长,选择合适的睡眠状态。

// CPUIdle核心流程(伪代码)
if (cpu_idle()) {
    predicted_time = predict_idle_duration();
    target_state = select_idle_state(predicted_time);
    enter_idle_state(target_state);
    // 醒来后继续执行
}

我的经验:曾经有个项目,CPUIdle预测不准,导致频繁进入深度睡眠又立刻唤醒。结果功耗反而比不睡还高。后来我们调整了预测算法参数,问题才解决。

3.2 CPUFreq:动态调频调压

CPUFreq管理的是CPU的运行频率和电压。任务重时跑高频,任务轻时降频降压。

3.2.1 调频策略

Linux内核提供了几种内置策略:

  • performance:一直跑最高频,性能优先
  • powersave:一直跑最低频,省电优先
  • ondemand:根据CPU负载动态调频,响应快
  • conservative:类似ondemand,但调频更平缓
  • schedutil:基于调度器负载信息,更精准

我个人习惯用schedutil。它直接拿调度器的负载数据做决策,比ondemand那种采样方式准得多。

3.2.2 调频延迟与电压切换

调频不是瞬间完成的。频率切换需要几十微秒,电压切换更慢。频繁调频反而浪费电。

// 查看当前调频策略
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

// 设置调频策略
echo "schedutil" > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor

避坑指南:我曾经在一个项目里把调频间隔设得太短,结果CPU一直在切频率,功耗反而增加了20%。后来把采样率从10ms改成50ms,问题解决。

3.3 Devfreq:设备频率管理

Devfreq是CPUFreq的「兄弟」,但它管的是外设——GPU、DDR、总线这些。

3.3.1 Devfreq与CPUFreq的对比

特性 CPUFreq Devfreq
管理对象 CPU核心 GPU、DDR、总线等
调频依据 CPU负载 设备负载/带宽
策略框架 governor governor(类似)
sysfs接口 /sys/devices/system/cpu/ /sys/class/devfreq/

3.3.2 实际应用场景

你想想看,GPU在渲染静态画面时,根本不需要跑满频率。Devfreq会根据帧率或渲染负载,自动降低GPU频率。

// 查看Devfreq设备
ls /sys/class/devfreq/

// 查看当前频率
cat /sys/class/devfreq/<device>/cur_freq

我的经验:调DDR频率要特别小心。降频虽然省电,但一旦CPU需要大量带宽,性能会断崖式下跌。我一般会留20%的余量。

3.4 Runtime PM:运行时电源管理

Runtime PM管理的是设备在运行过程中的电源状态。设备空闲时自动挂起,需要时自动唤醒。

3.4.1 核心机制

Runtime PM基于引用计数:

  • pm_runtime_get():增加引用计数,唤醒设备
  • pm_runtime_put():减少引用计数,尝试挂起
  • 自动挂起:引用计数为0且满足条件时,自动进入低功耗状态
// 驱动中启用Runtime PM
pm_runtime_enable(dev);
pm_runtime_set_autosuspend_delay(dev, 100); // 100ms延迟
pm_runtime_use_autosuspend(dev);

// 在需要时获取/释放设备
pm_runtime_get_sync(dev);
// ... 使用设备 ...
pm_runtime_mark_last_busy(dev);
pm_runtime_put_autosuspend(dev);

避坑指南:我曾经犯过一个错误——在中断处理函数里调用pm_runtime_get_sync。中断上下文不能睡眠,而Runtime PM的同步操作可能会睡眠。后来改成pm_runtime_get_noresume才搞定。

3.5 Suspend-to-RAM/Idle:系统级睡眠

这是最彻底的省电方式。整个系统进入睡眠状态,只保留内存供电(Suspend-to-RAM)或完全不保留(Suspend-to-Idle)。

3.5.1 睡眠状态对比

状态 内存供电 唤醒源 唤醒延迟 功耗
Suspend-to-Idle 是 任何中断 毫秒级 中等
Suspend-to-RAM 是 特定唤醒源 秒级 极低
Suspend-to-Disk 否 特定唤醒源 十秒级 接近零

3.5.2 唤醒源配置

不是所有设备都能唤醒系统。你需要明确指定哪些设备可以作为唤醒源。

// 查看设备是否支持唤醒
cat /sys/devices/<device>/power/wakeup

// 启用唤醒
echo "enabled" > /sys/devices/<device>/power/wakeup

// 触发睡眠
echo "mem" > /sys/power/state

核心要点:Suspend-to-RAM的功耗可以做到毫瓦级别。但唤醒时间较长,不适合需要快速响应的场景。

3.6 框架协作:一张图看懂

这几个框架不是孤立的。它们协同工作,形成一个完整的电源管理体系。

Linux电源管理框架协作关系 Linux电源管理框架协作关系 用户空间(sysfs接口) 内核电源管理框架 CPUIdle | CPUFreq | Devfreq | Runtime PM | Suspend CPU核心 P-State / C-State 外设(GPU/DDR等) 频率/电压调节 系统控制器 睡眠/唤醒控制 协作流程说明: 1. 用户通过sysfs配置策略(如调频策略、唤醒源) 2. 内核框架根据负载和空闲时间,选择最优电源状态 3. CPUFreq/Devfreq调节频率电压,CPUIdle管理空闲状态 4. Runtime PM管理设备运行时状态,Suspend控制系统级睡眠 5. 所有框架协同工作,实现从微秒级到秒级的全场景功耗优化

3.7 实战建议

说了这么多,最后给几条实在的建议:

  • 先测量,再优化:别凭感觉调参数。用功耗仪或电池监测工具,量化每个改动的影响
  • 从大到小:先搞定系统级睡眠(Suspend),再优化运行时功耗(Runtime PM),最后调CPU/设备频率
  • 注意唤醒延迟:省电和省时间,往往是一对矛盾。根据产品需求做取舍
  • 多测试边界:低功耗状态下,外设驱动容易出问题。我见过太多「睡下去醒不来」的案例

最后说一句:电源管理没有银弹。每个产品都有自己的功耗特性,多试、多测、多调,才能找到最优解。