3、CPU空闲管理(CPU Idle):框架、策略与调试

说到CPU空闲管理,我脑子里第一个蹦出来的场景,是几年前调试一款物联网网关时的经历。那设备功耗死活降不下来,电池撑不过两天。查来查去,发现CPU压根就没好好“睡觉”——它总是在浅睡眠和唤醒之间反复横跳。说白了,就是CPU Idle没配置对。

今天咱们就聊聊这个。CPU Idle,就是CPU没事干的时候怎么省电。你想想看,CPU跑起来功耗可能几瓦甚至几十瓦,但空闲时如果能进入深度睡眠,功耗能降到毫瓦级。这中间的差距,就是咱们要啃的硬骨头。

3.1 CPU Idle框架:谁在管CPU睡觉?

Linux内核里,CPU Idle框架是个挺精巧的设计。它不直接操作硬件,而是提供了一套抽象层。我习惯把它理解成“睡眠管家”——上层Governor决定睡多深,底层驱动负责怎么睡。

框架的核心组件就这几个:

  • Governor(策略器):根据系统负载,选一个合适的睡眠状态
  • Driver(驱动):跟硬件打交道,执行具体的睡眠指令
  • Governor核心层:把策略和驱动粘起来

嗯,这里要注意,CPU Idle框架和CPU Freq(调频)框架是两码事。CPU Freq管的是“干活时用多大力”,CPU Idle管的是“不干活时怎么歇”。两者配合好了,才能实现真正的节能。

3.2 C-State与P-State:别搞混了

这两个概念,我见过太多人搞混了。包括我自己刚入行时也犯过糊涂。

C-State,全称是CPU Idle State,也就是CPU的空闲状态。C0是正常运行,C1是暂停(HALT),C2是停止时钟,C3是深度睡眠...数字越大,睡得越沉,唤醒延迟也越大。

P-State,全称是Performance State,也就是CPU的性能状态。P0是最高频率,P1、P2依次降频。它管的是“干活时的速度”,不是“睡觉时的深度”。

我打个比方你就明白了:

  • C-State:你晚上睡觉,是浅睡、深睡还是雷打不动?
  • P-State:你白天干活,是快跑、慢走还是原地踏步?

两者独立,但互相影响。比如你进了C6深度睡眠,醒来后P-State可能还得重新恢复。我在项目中就遇到过,深度睡眠后恢复频率太慢,导致响应延迟超标。

关键点:C-State是空闲管理,P-State是性能管理。调试时一定要分清楚你在调哪个。

3.3 Governor策略:menu、ladder、teo

Linux内核里,CPU Idle的Governor主要有三个。我一个个说。

3.3.1 menu Governor(默认策略)

这是大多数ARM平台和x86平台的默认选择。它基于一个预测模型——根据历史空闲时间,预测下一次空闲会持续多久。预测准了,就选深度睡眠;预测不准,就选浅睡眠。

menu Governor的核心逻辑,说白了就是“赌一把”。它用了一个叫“平均指数”的东西来平滑预测。我个人觉得,它在负载变化不剧烈的场景下表现很好。但如果你做的是实时系统,那menu可能会让你头疼——它偶尔会预测失误,导致唤醒延迟超标。

3.3.2 ladder Governor(阶梯策略)

这个Governor比较“老实”。它不会预测,而是按阶梯一步步来。先试C1,不行再试C2,再不行试C3。每次空闲都从最浅的状态开始,如果发现空闲时间够长,就逐渐加深。

ladder的好处是稳定,不会出现预测失误。坏处是反应慢——从浅睡到深睡需要时间,可能还没到深睡,CPU就被唤醒了。我建议在实时性要求高的场景用ladder,比如工业控制器。

3.3.3 teo Governor(定时器事件优化)

teo是后来加入的,全称是Timer Events Oriented。它比menu更聪明一点——它不光看历史,还看未来的定时器事件。如果下一个定时器在100ms后触发,那它就知道可以放心睡个80ms的深度睡眠。

teo在有些场景下比menu省电,但也不是万能的。我记得有一次,teo在某个SoC上反而导致功耗升高,原因是那个SoC的深度睡眠唤醒开销太大,teo频繁进出深度睡眠,得不偿失。

我的建议

  • 通用场景:用menu,省心
  • 实时场景:用ladder,稳定
  • 低功耗场景:试试teo,但要做对比测试

3.4 驱动实现:怎么让CPU真正睡过去?

Governor只是“动嘴”,真正“动手”的是驱动。CPU Idle驱动需要实现两个核心回调:

  • enter:进入某个C-State
  • exit:从C-State唤醒

驱动代码通常放在 drivers/cpuidle/ 目录下。我拿一个简单的ARM平台驱动举例:

static int arm_enter_idle(struct cpuidle_device *dev,
                          struct cpuidle_driver *drv, int index)
{
    // 进入C-State前,保存必要的上下文
    // 调用WFI(Wait For Interrupt)指令
    asm volatile("wfi" : : : "memory");
    // 醒来后,恢复上下文
    return index;
}

static struct cpuidle_driver arm_idle_driver = {
    .name = "arm_idle",
    .states = {
        { .name = "WFI", .desc = "Wait For Interrupt", .exit_latency = 1 },
        { .name = "C2",  .desc = "Clock Gating",       .exit_latency = 10 },
        { .name = "C3",  .desc = "Power Gating",       .exit_latency = 100 },
    },
    .safe_state_index = 0,
};

这里有个坑,我当年踩过。驱动里定义的 exit_latency(唤醒延迟)一定要准确。你写1微秒,但实际硬件要10微秒,那Governor就会做出错误决策。我曾经就因为一个SoC的C3状态唤醒延迟写错了,导致系统频繁进入C3,结果响应延迟飙升,用户投诉说触摸屏卡顿。

避坑指南

  • 一定要实测每个C-State的唤醒延迟,别信数据手册
  • 注意不同C-State的功耗节省,别为了省1mW导致性能下降10%
  • 有些SoC的C-State之间有依赖关系,比如进C3前必须先进C2

3.5 调试手段:怎么知道CPU睡没睡好?

调试CPU Idle,我常用的工具就这几个:

工具/方法 作用 使用场景
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/time 查看每个C-State的累计时间 快速判断CPU在哪个状态待得久
cpuidle_info (tracepoint) 跟踪每次进入/退出C-State 分析空闲行为是否异常
powertop 查看系统整体功耗和空闲统计 对比不同Governor的效果
示波器/逻辑分析仪 测量硬件信号(如CLK、PWR_OK) 验证驱动是否正确执行了硬件操作

我个人最常用的是tracepoint。你可以在内核里打开 CONFIG_CPU_IDLE_TRACE,然后用 trace-cmd 抓取:

# trace-cmd record -e cpuidle:* sleep 10
# trace-cmd report

输出会告诉你,每次空闲时CPU进了哪个状态,待了多久,被什么事件唤醒。我曾经靠这个抓到一个bug——某个外设驱动频繁发送IPI(核间中断),导致CPU每2ms就被唤醒一次,根本进不了深度睡眠。

3.6 实战经验:一个真实的调优案例

最后分享一个我自己的案例。某款智能音箱,待机功耗要求低于50mW。但实测有80mW。查了一圈,发现CPU Idle配置有问题。

问题出在:

  1. 默认Governor是menu,但menu在负载极低时反而会频繁进出C1/C2
  2. 驱动里C3的唤醒延迟写的是50us,实际硬件要200us

我的解决方案:

  • 把Governor换成teo,因为teo对定时器事件更敏感,适合这种“大部分时间空闲,偶尔处理音频”的场景
  • 修正驱动里的 exit_latency,从50us改成200us
  • 增加一个 demotion 机制:如果连续3次从C3唤醒后空闲时间都很短,就自动降级到C2

改完后,待机功耗降到了45mW。嗯,虽然过程折腾,但结果还不错。

好了,CPU Idle这块就聊到这儿。记住一句话:让CPU好好睡觉,比让它拼命干活更重要。下一节咱们聊聊CPU Freq,也就是怎么让CPU“干活时更有劲”。