深入理解Linux内核CPUFreq与Runtime PM:从调频到设备挂起的功耗优化 📅 发布时间:2026/9/15 7:01:57 👁 浏览次数: 1. 立项背景为什么 Linux 内核把省电当成一等公民在嵌入式、服务器和数据中心里摸过一段时间内核的人都会对“功耗”这两个字有越来越深的敬畏。CPU 频率往上拉一档芯片温度可能直接顶到降频线某个外设忘了关整机待机时间缩水一半。Linux 内核的能源管理正是围绕“别浪费”这三个字展开的CPUFreq 负责让 CPU 按需跑快或跑慢Runtime PM 负责让硬件设备在空闲时真正休眠、在需要时及时醒来。这两个机制合在一起构成了系统运行期间最核心的省电路径也往往是做功耗优化的第一站。这篇整理会从框架设计和内核源码角度把 CPUFreq 和 Runtime PM 的协作方式、配置手段、调试方法和踩坑记录体系化地捋一遍适合正在做嵌入式驱动、终端功耗优化或者单纯想弄懂cpufreq和 Runtime PM 到底是什么的开发者参考。1.1 能耗问题为什么越来越绕不开先看几个真实的场景。手机待机一晚掉电超过 5%板子上一颗 I2C 触摸芯片明明不用了却始终维持在 активный 状态服务器跑同样的业务负载某几台机器的整机功耗就是比其他机器高一截。这类问题最终排查下来大多不是硬件本身耗电而是软件没有把硬件放到该去的低功耗状态。Linux 内核这套能源管理框架说白了就是把“硬件何时该省电、省到什么程度、如何快速恢复”这几个问题用一套可配置、可观测、可扩展的机制管起来。CPUFreq 管的是处理器运行快慢Runtime PM 管的是设备工作状态两者虽然层级不同但目标一致用最低的能耗完成当前的工作。CPUFreq 做的是时间维度上的缩放——同样一段代码跑慢了省电但延迟变高Runtime PM 做的是空间维度上的关断——设备不参与当前任务时直接切断时钟或电源。很多刚接触的同事会混淆“CPU 降频”和“设备休眠”实际上它们是两个独立子系统但又会在系统级功耗优化中互相配合。CPU 降频后任务执行时间变长外设空闲窗口变短Runtime PM 的挂起机会反而减少这就是为什么必须把两者放在一起看而不是单独调某一项。1.2 从“调频率”到“控设备”的演进路线Linux 内核在早期版本里CPU 调频主要靠用户态工具写 MSR 或平台寄存器没有一个统一的抽象层。2.6 时代开始引入cpufreq框架把调频策略从驱动里抽出来形成了 governor 和 driver 解耦的结构。到了 3.x 后期到 4.x内核又用schedutilgovernor 把调度器的负载信号直接接入调频决策让调频从“周期采样负载”进化到“随任务到达即时响应”。这套演进背后其实是在回答一个问题到底以什么样的时间粒度和信息源来决定 CPU 频率才能做到既省电又不损失性能。Runtime PM 的合并则要晚得多直到 2.6.32 左右才正式进入主线。它的核心思想很朴素设备在运行期间不该一直全速工作而是由驱动或子系统在设备空闲时主动发起挂起suspend用到时再恢复resume。注意它和系统级的 suspend/resume 不是一回事。系统睡眠是整机进入 S3/S4 这类状态而 Runtime PM 是单个设备在系统继续运行时悄悄地休眠。理解这个区别后面看代码才不会晕。CPUFreq 和 Runtime PM 从两个维度共同构成了 Linux 运行时的能源管理基础这也是我决定把这两个主题放在一篇文章里讲清楚的原因。2. CPUFreq处理器频率调整的入口CPUFreq 是接触 Linux 能源管理时最先碰到的子系统因为它的接口太显眼了/sys/devices/system/cpu/cpu*/cpufreq/下面一帮文件随便 cat 一下就能看到当前频率、策略、可用频点。但真正要把它用明白需要知道核心层的几个角色如何配合。2.1 核心组件怎么协作CPUFreq 框架从高到低可以拆成四层核心层、governor 层、driver 层和硬件层。核心层负责管理 policy策略对象维护 sysfs 接口并且在调频动作发生时通过通知链告诉其他模块“频率要变了”。governor 决定“要不要调、调到多少”它只关心负载和策略不碰硬件。driver 层才真正操作寄存器或 ACPI 接口把目标频率落到硬件上。整个链路看起来像这样调度器或定时器产生负载信息governor 根据负载计算目标频率核心层校验频率合法性driver 落硬件最后更新 policy 的状态。policy 是这里容易被忽略的一个概念。多核处理器里同属一个调频域的 CPU 会共用一个 policy也就是说它们必须运行在同一个频率上。比如大小核架构中大核簇和小核簇各自独立调频但簇内的几个核不能分开调频。你在 sysfs 里看到的是每个 CPU 一个目录但写scaling_setspeed的时候实际生效范围是整个 policy。判断哪些 CPU 在同一个 policy 里可以看/sys/devices/system/cpu/cpu*/cpufreq/related_cpus这个文件会把同域 CPU 列全。四层结构里最容易出问题的其实是核心层和 driver 层的边界。核心层负责的是“目标频率 → 合法的硬件频率”的转换这里涉及设备树或 ACPI 提供的频率表。ARM 平台常见的做法是在 DTS 里定义 operating-points-v2 节点频率表由cpufreq-dt驱动解析x86 平台则走 ACPI 的 P-state 或 CPPC 接口由intel_pstate或acpi-cpufreq驱动处理。无论哪种平台只要scaling_driver文件里能看到正确的驱动名就说明核心层和 driver 层已经打通。2.2 Governor 选型与调参实战Governor 是调频策略的决策者选错了 governor后续优化事倍功半。老牌 governor 有performance、powersave、userspace、ondemand、conservative现代内核还加入了schedutil。我把它们放在一起对比一下方便你按场景选型Governor决策依据频率调整方式适用场景performance无固定最高频延迟敏感、基准测试powersave无固定最低频深度待机、低负载常驻userspace用户态写入手动指定频率调试、实验室验证ondemand周期采样 CPU 负载突变式升频老内核、负载模式简单conservative周期采样 CPU 负载逐级升降频平滑调频优先的场景schedutil调度器的利用率信号跟随任务到达快速调整现代内核的默认推荐ondemand和conservative有个共同问题它们依赖定时器定期采样负载负载采样周期一般在上百毫秒级别遇到突发任务响应会慢半拍。schedutil把调频决策直接接到调度器的 PELTPer-Entity Load Tracking信号上每个调度周期都能看到 CPU 利用率的变化升频基本能做到和任务到来同步。从 4.7 合并以来它已经成为大多数发行版和嵌入式 BSP 的默认选项。实际调参时最常用的是这几个 sysfs 接口# 先看当前 CPU 支持哪些 governor cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_governors # 切换 governor echo schedutil /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor # 查看可用频率和当前频率 cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_available_frequencies cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq如果你用userspacegovernor 做实验写完scaling_setspeed之后一定要去scaling_cur_freq确认实际频率是否变化。有时候硬件会因为电压或温度限制无法立刻跳到目标频点此时scaling_cur_freq会停在中间值这是正常现象不代表接口坏了。另一个容易踩的坑是热管理子系统会动态压低频率上限你明明写了个高频scaling_max_freq却已经被 thermal 框架悄悄改掉了所以排查“频率上不去”的时候先看一眼scaling_max_freq和温度节点。2.3 驱动对接与 ACPI/DT 联动在 ARM 嵌入式平台上CPUFreq 大多通过设备树里的 operating-points-v2 节点描述频率和电压表。一个典型的 OPP 表长这样cpu0_opp_table: opp-table-0 { compatible operating-points-v2; opp-shared; opp00 { opp-hz /bits/ 64 1000000000; opp-microvolt 1000000; }; opp01 { opp-hz /bits/ 64 1500000000; opp-microvolt 1100000; }; opp02 { opp-hz /bits/ 64 2000000000; opp-microvolt 1250000; }; }; cpu0 { operating-points-v2 cpu0_opp_table; };opp-shared这个属性值得单独说一句。它表示该表对应的频点被多个 CPU 共享也就是说这一簇 CPU 必须同频运行。不带这个属性的 OPP 表则暗示每个 CPU 可以独立调频。写设备树时如果这里搞错轻则调频不生效重则导致同一簇的 CPU 频率不一致触发 cache coherency 相关的问题。cpufreq-dt驱动会读取这个 OPP 表并通过调节电压和频率来完成调频。实际传输到硬件时一般由 PMIC 或 clock controller 芯片完成电压切换内核侧只需要调用 regulator 和 clk 框架的接口。这也是为什么做 ARM 平台调频时经常要同时排查 clk 树和 regulator 配置是否准确频率改了电压没跟上芯片可能直接死机。x86 平台则不太一样。intel_pstate驱动在支持 HWPHardware P-state的 CPU 上会把大部分调频决策交给硬件内部逻辑内核只负责给出性能偏好在不支持 HWP 或使用intel_pstatepassive参数时intel_pstate会退化为类似acpi-cpufreq的工作模式由内核通过 MSR 写入目标 P-state。理解这些差异对排查服务器上“CPU 频率不稳定”的问题很有帮助。3. Runtime PM设备级的运行时电源管理如果说 CPUFreq 调节的是处理器这根“大动脉”Runtime PM 管的就是全身各处的“毛细血管”。一个系统里 CPU 拼命省电结果一颗 sensor 芯片始终醒着待机功耗照样下不来。Runtime PM 的价值就是把省电动作细化到每个设备上。3.1 运行时挂起/恢复的状态机Runtime PM 的核心是一个针对每个设备的状态机外加一个引用计数。设备只能处于两个稳定状态ACTIVE工作和 SUSPENDED挂起中间还会经历 SUSPENDING 和 RESUMING 两个瞬态。为了让你直观理解我用一个“会议室灯”来类比每次有人进会议室就调用一次pm_runtime_get灯的引用计数加一灯必须亮着每次有人离开就调用pm_runtime_put计数减一当计数减到零并不立刻关灯而是等一个延时autosuspend delay确认没人再进来后才执行关灯动作。这个“延时关灯”的机制就是为了避免频繁有人进出时灯不断闪烁。引用计数和状态的关系是理解 Runtime PM 的关键。pm_runtime_get会让计数器加一如果设备处于 SUSPENDED则同步或异步触发 resumepm_runtime_put让计数器减一减到零后触发挂起。这个“减到零不等于马上挂起”的设定特别重要它把“设备空闲”和“设备真正断电”解耦了给了驱动一个机会去判断是否需要延迟挂起。状态机的转换必须由驱动和框架配合完成。驱动在 probe 时通常会先pm_runtime_set_active把设备的初始状态标记为 ACTIVE然后调用pm_runtime_enable开启运行时管理。之后每次硬件真正进入低功耗框架都会回调驱动的runtime_suspend函数每次恢复则回调runtime_resume。这两个回调函数是驱动开发者主要需要实现的逻辑通常在里面做关/开时钟、关/开电源、保存/恢复寄存器的操作。3.2 核心 API 与回调机制Runtime PM 的 API 看着多实际上可以按“获取/释放”和“同步/异步”两个维度分成四类。我整理了一张速查表API行为注意事项pm_runtime_get_sync(dev)引用计数加一同步完成唤醒不能在原子上下文调用pm_runtime_get(dev)引用计数加一异步唤醒返回值只代表请求是否成功pm_runtime_put_sync(dev)引用计数减一同步挂起要小心死锁和深度递归pm_runtime_put(dev)引用计数减一调度异步挂起常用搭配 autosuspendpm_runtime_get_if_in_use(dev)只在设备已被使用时才加计数适用于非阻塞检查真正的挂起和恢复动作由struct dev_pm_ops中的三个回调完成static const struct dev_pm_ops mydev_pm_ops { SET_RUNTIME_PM_OPS(mydev_runtime_suspend, mydev_runtime_resume, mydev_runtime_idle) };runtime_idle回调和runtime_suspend的区别是初学者最容易搞混的地方。框架在引用计数归零后会先调用runtime_idle驱动在这个回调里可以决定“立即挂起”还是“再等等”。如果什么都不做框架就默认暂不挂起直到 autosuspend 延时到点再尝试真正挂起。所以想用 autosuspend一定要在runtime_idle里调用pm_runtime_suspend或者干脆使用pm_runtime_put_autosuspend配合pm_runtime_mark_last_busy这套惯用法。3.3 与驱动框架的集成要点写一个带 Runtime PM 支持的驱动最难的不是回调逻辑而是把 API 放到正确的位置。根据我改过的不少驱动集成时有几个关键点最容易出错。probe 函数里的顺序问题排第一。正确的典型流程是static int mydev_probe(struct platform_device *pdev) { struct device *dev pdev-dev; int ret; /* 初始化硬件让设备处于可用状态 */ ret mydev_init_hw(dev); if (ret) return ret; /* 先告诉框架设备当前是 active再使能 runtime PM */ pm_runtime_set_active(dev); pm_runtime_enable(dev); /* 设置 autosuspend 延迟并启用 autosuspend */ pm_runtime_set_autosuspend_delay(dev, 1000); pm_runtime_use_autosuspend(dev); /* 打开设备并保持引用确保使用中有用 */ pm_runtime_get_sync(dev); mydev_start_work(dev); /* 工作完成释放引用允许延时挂起 */ pm_runtime_mark_last_busy(dev); pm_runtime_put_autosuspend(dev); return 0; }pm_runtime_set_active必须在pm_runtime_enable之前调用否则框架会认为设备初始状态未知后续引用计数管理会变得不可预期。另一个容易被忽视的是pm_runtime_use_autosuspend必须在设置延迟之后调用顺序反了会导致 autosuspend 不生效。我在 review 代码时见过好几次这种顺序错误症状都是设备永远不进入SUSPENDED。remove 函数里则要注意先停掉业务逻辑再pm_runtime_disable最后做硬件清理。如果顺序反了可能在pm_runtime_disable之后还有线程尝试pm_runtime_get直接触发使用已释放资源的路径。从 5.1 内核开始devm_pm_runtime_enable可以用设备管理机制自动关闭运行时 PM但用它的时候仍要保证 remove 回调里不会再次手动调用pm_runtime_disable否则会报“disable 次数不平衡”的警告。Runtime PM 还会和系统睡眠流程交互。在系统级 suspend 时框架会先尝试对启用 runtime PM 的设备做一遍 runtime suspend恢复时再对应恢复。这意味着如果驱动把硬件状态保存在runtime_suspend里系统睡眠恢复后也必须从runtime_resume里恢复不能想当然地依赖suspend/resume回调。这两个回调经常被驱动开发者漏写导致从休眠唤醒后设备工作异常。4. 实操从内核配置到驱动验证前面讲完原理这一节进入实战。我会按“内核选项 → 观测手段 → 驱动示例”的顺序给你一条完整可复现的路径。4.1 内核配置要点要让 CPUFreq 和 Runtime PM 真正工作内核配置里有几个关键选项需要确认。先说要编译进内核的选项再看它们各自起什么作用。CPUFreq 侧核心配置是CONFIG_CPU_FREQ。现代内核通常还会打开CONFIG_CPU_FREQ_GOV_SCHEDUTIL来使用schedutil以及平台相关的驱动选项比如 ARM 平台上的CONFIG_CPUFREQ_DT。如果你需要调试调频信息可以打开CONFIG_CPU_FREQ_STAT这样 sysfs 里会出现各频点累计驻留时间的统计。x86 平台上还需要确认CONFIG_X86_INTEL_PSTATE是编译为模块还是直接编入内核。Runtime PM 侧配置要稍微留意一下内核版本差异。较老的内核里有独立的CONFIG_PM_RUNTIME选项但在后续版本中它被并入CONFIG_PM所以新内核里通常只需要保证CONFIG_PM打开即可。如果做调试建议同时打开CONFIG_PM_DEBUG和CONFIG_PM_ADVANCED_DEBUG前者提供基础的 power sysfs 调试信息后者会在内核日志里输出更多状态转换细节。一个常见的误区是把 CPUFreq 和 CPUIdle 搞混。CPUFreq 控制运行频率CPUIgle 控制 idle 深度两者都需要独立配置CONFIG_CPU_IDLE对应 idle 框架CONFIG_CPU_IDLE_GOV_MENU对应菜单式 governor。做系统级功耗优化时这两套机制要同时关注否则会出现“CPU 频率降下去了但 idle state 进不去”的尴尬情况。4.2 使用 tracepoint 和 debugfs 观测调功耗和调试其他内核功能最大的区别是现象往往不可见必须依赖观测工具。我最常用的两个手段是 ftrace 的 power tracepoint 和 debugfs 里的 runtime 状态。ftrace 能直接看调频事件和 Runtime PM 状态切换。先开 tracepoint再跑负载最后看 trace 输出cd /sys/kernel/tracing echo 0 tracing_on echo trace echo power:cpufreq_frequency_change set_event echo power:rpm_suspend set_event echo power:rpm_resume set_event echo 1 tracing_on # 这里跑一段真实负载比如编译或压测 make -j4 2/dev/null sleep 10 echo 0 tracing_on cat trace | head -80输出里能看到每一次频率切换的目标频点以及每一个设备的 suspend/resume 事件。如果 trace 里出现某个设备频繁 resume/suspend 交替说明 autosuspend 延迟写得偏小或者驱动在使用期间不断调用 get/put导致设备在“假忙”和“真忙”之间来回横跳。用来观察 Runtime PM 当前状态更直观的是 sysfs 和 debugfs。每个设备的 power 目录下都有运行时信息cat /sys/devices/platform/serial8250/power/runtime_status cat /sys/devices/platform/serial8250/power/runtime_usage cat /sys/devices/platform/serial8250/power/controlruntime_status显示设备当前是active还是suspendedruntime_usage是引用计数control则是on或auto。这里的control很容易被忽略如果它是on表示该设备被强制保持 ACTIVE即使引用计数归零也不挂起。只有在auto模式下Runtime PM 才会根据引用计数自由切换状态。如果平台启用了 genpdGeneric PM Domain还可以通过 debugfs 查看域内各设备的挂起情况cat /sys/kernel/debug/pm_genpd/pm_genpd_summary这个文件会把每个 power domain 及其子设备的状态、挂起时间、使用计数列得一清二楚。遇到“整域无法挂起”的问题先看这个摘要基本能定位到是哪一个设备拖住了整个域。4.3 一个简单的 Runtime PM 驱动示例下面我给一个最小可运行的 platform 驱动示例它注册了一个虚拟设备用 Runtime PM 管理一个“假硬件”的时钟开关。这段代码可以直接抄进自己的实验驱动里改一改跑起来。#include linux/module.h #include linux/platform_device.h #include linux/pm_runtime.h #include linux/delay.h static int mydev_runtime_suspend(struct device *dev) { dev_info(dev, runtime suspend: clock off\n); /* 这里放关闭时钟、关闭电源、保存寄存器的代码 */ return 0; } static int mydev_runtime_resume(struct device *dev) { dev_info(dev, runtime resume: clock on\n); /* 这里放打开时钟、打开电源、恢复寄存器的代码 */ return 0; } static const struct dev_pm_ops mydev_pm_ops { SET_RUNTIME_PM_OPS(mydev_runtime_suspend, mydev_runtime_resume, NULL) }; static int mydev_probe(struct platform_device *pdev) { struct device *dev pdev-dev; pm_runtime_set_active(dev); pm_runtime_enable(dev); pm_runtime_set_autosuspend_delay(dev, 2000); pm_runtime_use_autosuspend(dev); /* 模拟一次使用拿引用、做事、释放 */ pm_runtime_get_sync(dev); dev_info(dev, hardware work start\n); msleep(100); dev_info(dev, hardware work done\n); pm_runtime_mark_last_busy(dev); pm_runtime_put_autosuspend(dev); return 0; } static int mydev_remove(struct platform_device *pdev) { struct device *dev pdev-dev; pm_runtime_disable(dev); return 0; } static struct platform_driver mydev_driver { .probe mydev_probe, .remove mydev_remove, .driver { .name mydev, .pm mydev_pm_ops, }, }; module_platform_driver(mydev_driver); MODULE_LICENSE(GPL);装载这个驱动后dmesg里会依次出现hardware work start、hardware work done然后大约 2 秒后出现runtime suspend。如果看不到最后一条说明 autosuspend 没有生效或引用计数没有归零。这种最小示例特别适合用来验证自己对 Runtime PM 状态机的理解也适合在向同事解释“为什么设备没休眠”时快速建立对照实验。5. 常见问题与排查技巧实录理论讲完代码也跑了接下来是目前项目里最值钱的部分真正排查问题时的经验和教训。5.1 CPUFreq 频点不生效的排查思路遇到“明明设置了 frequencyscaling_cur_freq 却不变”的情况先不要怀疑内核代码有 bug按下面顺序逐层排查。第一步确认驱动和 governor 是否加载正确cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_driver和scaling_governor都要有内容。第二步看频率范围是否被改动scaling_min_freq、scaling_max_freq是否正常。尤其是scaling_max_freqthermal 框架在温度过高时会直接把它压低这是最常见的“频率上不去”原因。第三步如果用的是userspacegovernor检查scaling_setspeed写入的 度值必须是scaling_available_frequencies里列出的合法频点。第四步看是否被 cgroup 或进程的 CPU quota 限制这种限制会让负载看起来不高governor 自然不愿意升频。服务器场景还要额外注意intel_pstate的行为。如果设置了intel_pstatedisable系统会退回acpi-cpufreq调频延迟和精度会有差别。而 HWP 模式下硬件自行决定具体 P-state内核侧scaling_cur_freq只是一个参考值可能随时跳动不必过分担心。5.2 设备无法进入低功耗状态这是 Runtime PM 最经典的问题症状是runtime_status始终是active或者偶尔进入suspended又立刻被唤醒。先看runtime_usage如果这个值不是 0说明有某个调用者一直没有释放引用。你在内核日志里搜pm_runtime_get再配合 tracepoint 能看到每次都谁在调用 get。如果没有明显的驱动 bug可以试着把设备的control从on改成autoecho auto /sys/devices/platform/xxx/power/control这里要特别提醒control显示on是设备被强制保持 active 的信号很多 BSP 在 probe 阶段会默认把control设成on如果驱动代码里没有显式改成auto设备永远不可能 runtime suspend。遇到这种问题先查驱动设备模型里的runtime_auto标志是否被清除再查平台代码里是不是写了pm_runtime_forbid。另一个隐蔽的原因是 device link。如果你的设备和一个始终 active 的设备建立了DL_FLAG_RPM_ACTIVE链接即使自己的引用计数归零也会因为链接另一端处于 active 而无法挂起。优先检查/sys/devices/.../device_link/下面是否存在这种强制约束。5.3 唤醒延迟过高频繁挂起反而更耗电Runtime PM 用得好是省电用不好就是负优化。我见过最典型的反模式是驱动把 autosuspend 延迟设成 0设备每次空闲就立刻挂起结果业务抖动一来又马上唤醒。频繁的 suspend/resume 不仅增加延迟还会额外消耗功耗因为唤醒时往往要重新初始化时钟和电源开销比一直保持 active 还大。解决办法是把autosuspend_delay调大比如 1 到 3 秒让短时空闲不触发挂起。同时结合pm_runtime_mark_last_busy记录最后一次使用时间这样即使设备在一个长时间任务中偶尔出现空闲窗口也不会在任务结束前反复挂起。判断当前设置是否合理用 ftrace 数一下一段稳定负载里rpm_suspend和rpm_resume的事件次数就行如果一分钟内超过几十次基本可以确定 autosuspend 参数需要重新设计。5.4 调试工具速查表最后把调试手段汇总成一张速查表方便现场排查时快速对照排查目标工具/路径关键信息CPUFreq 当前状态/sys/devices/system/cpu/cpu*/cpufreq/governor、频率范围、驱动名CPUFreq 调频事件/sys/kernel/tracing/events/power/cpufreq_frequency_change旧频率、新频率、CPU 编号设备 runtime 状态/sys/devices/.../power/runtime_statusactive、suspended设备 runtime 引用计数/sys/devices/.../power/runtime_usage非零说明设备正被占用强制启用/禁用 runtime PM/sys/devices/.../power/controlon 强制 activeauto 自动管理Runtime PM 状态切换事件/sys/kernel/tracing/events/power/rpm_suspend等设备名、返回错误码唤醒源统计/sys/kernel/debug/wakeup_sources哪些中断/设备在阻止挂起电源域状态/sys/kernel/debug/pm_genpd/pm_genpd_summary域内设备挂起时长和当前状态这里想再多说一句wakeup_sources是排查“系统睡不下去”的好帮手但它统计的是系统级 suspend 的唤醒源。如果设备在运行态无法进入 runtime suspend问题往往不在 wakeup source而在引用计数和 device link。两类唤醒源要分开看不要混在一起排查。我自己做功耗优化这么多年最大的体会是能源管理的问题很少是某一个 API 的文档读得不够而是状态机和引用计数之间的关系没有在调试时被严格对待。CPUFreq 相对直观调频策略不对看 sysfs 和 trace 就能定位Runtime PM 则需要你去推理“谁在什么时候获取了引用、为什么没有释放”。先把观测手段搭好再动代码效率会高很多。希望这篇整理能帮你把这两套机制串起来以后遇到功耗问题少走几段弯路。