3、Linux内核功耗管理框架:CPU Freq系统、CPU Idle系统、Device PM框架、Runtime PM机制

功耗管理这件事,说白了就是一句话:在性能与省电之间找到那个最舒服的平衡点。我做了这么多年嵌入式,见过太多产品因为功耗没处理好,要么发热严重被用户骂,要么续航太短被市场淘汰。

Linux内核的功耗管理,不是靠某一个模块单打独斗。它是一套组合拳。今天我们就把这套组合拳拆开来看——CPU Freq、CPU Idle、Device PM、Runtime PM。这四个东西,你搞明白了,功耗调优就入门了。

核心认知:CPU Freq管“跑多快”,CPU Idle管“歇多久”,Device PM管“设备怎么睡”,Runtime PM管“睡到啥程度”。四者协同,缺一不可。
Linux内核功耗管理四大子系统关系图 Linux内核功耗管理四大子系统 功耗管理核心 CPU Freq 动态调频/调压 CPU Idle 空闲状态管理 Device PM 设备电源管理 Runtime PM 运行时电源管理 协同工作流程 ① 系统空闲 → CPU Idle选择C-State深度睡眠 ② 任务到来 → CPU Freq根据负载调频到合适P-State ③ 设备空闲 → Runtime PM自动挂起未使用的外设 ④ 系统休眠 → Device 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 待机/轻负载
我的经验:调P-State时别只看频率。电压每降100mV,功耗能降15%-20%。但电压降太多会导致系统不稳定。我一般会留5%-10%的余量。

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
踩坑提醒:我曾经在一款ARM平台上,把scaling_min_freq设得太高,结果待机功耗多了200mW。后来发现是某个驱动在空闲时频繁唤醒CPU,导致CPU无法降频。这个问题查了我整整两天。

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为例,流程是这样的:

  1. 冻结用户空间进程:停止所有用户态任务。
  2. 暂停设备:按设备的依赖关系,依次调用驱动的suspend()回调。
  3. 关闭非引导CPU:只保留一个CPU用于唤醒。
  4. 进入睡眠状态:系统整体进入低功耗模式。
  5. 唤醒:外部中断触发,反向执行上述步骤。

这里面最容易出问题的就是第二步。如果某个驱动的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,
};
我曾经踩过的坑:一个Wi-Fi驱动的suspend回调里,忘记释放DMA缓冲区。结果系统休眠时,DMA还在传输数据,直接导致系统崩溃。后来我养成了一个习惯——suspend回调里一定要确保所有硬件活动都停止了。

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);
我的建议:自动休眠延迟不要设得太短。我曾经设了200ms,结果设备频繁休眠唤醒,功耗反而更高。一般设1-5秒比较合理,具体要看设备的使用模式。

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,所有设备都进入低功耗模式。

你看,这四个机制各司其职,又相互配合。少了任何一个,功耗都很难做到最优。

总结一句话:CPU Freq管性能,CPU Idle管空闲,Device PM管系统休眠,Runtime PM管设备级休眠。把这四个玩转了,你的嵌入式Linux功耗管理就入门了。