ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Linux runtime pm 深度解析:从功耗异常到驱动优化

2026/10/6 15:45:40 拓冰建站 浏览量
Linux runtime pm 深度解析:从功耗异常到驱动优化 1. 从一次待机功耗异常说起runtime pm 到底在管什么前阵子帮一个做嵌入式产品的朋友排查问题设备在空闲状态下功耗始终降不下来电池续航比规格书标称的少了将近三分之一。用功耗分析仪抓了一段时间的电流曲线发现 SoC 在系统空闲时并没有进入低功耗状态几个外设的时钟也一直开着。顺着/sys/kernel/debug/pm_genpd和/sys/kernel/debug/runtime_pm一路查下去最后定位到某个 I2C 从设备的驱动在 probe 之后没有正确调用pm_runtime_put导致它的 parent 设备引用计数一直不为零整条电源域都没法关。这个问题让我意识到很多做 Linux 驱动或者系统功耗优化的同学对 runtime pm 的理解停留在“调几个 API 就行”的层面但真到功耗压不下去的时候往往不知道从哪儿下手。这篇就围绕 Linux 内核功耗子系统里的 runtime pm 功能做一次系统梳理把它的设计思路、核心数据结构、API 用法、和系统级 suspend 的关系、以及调试手段讲透。适合正在写驱动的工程师、做嵌入式功耗优化的同学以及想深入理解内核 PM Core 的读者。看完你应该能自己判断一个设备的 runtime pm 状态是否正常也能在功耗异常时快速缩小排查范围。runtime pm全称 Runtime Power Management直译就是运行时电源管理。它要解决的问题很具体设备在系统正常运行期间如果一段时间没人用就应该主动进入低功耗状态等下次要用的时候再自动唤醒。注意这里的关键词是“运行时”也就是系统没有整体休眠的情况下单个设备级别的动态电源管理。它和系统级的 suspend/resume 是两套机制但又有协作关系这个后面会专门讲。2. runtime pm 的整体设计思路与核心机制拆解2.1 为什么需要设备级动态电源管理传统的电源管理思路是系统级休眠整个系统进入 suspend所有设备一起挂起唤醒时一起恢复。这种方式在手机、平板这类交互设备上问题不大因为用户不用的时候屏幕一关整机休眠就行。但在很多场景下这套逻辑不够用。举个典型例子一个工业网关设备主控一直要保持网络连接但上面挂的 RS485 收发器、ADC 采样芯片可能几分钟才用一次。如果为了省电让整机休眠网络就断了如果整机不休眠这些外设一直供电又浪费。runtime pm 就是为这种“系统在跑但部分设备可以歇着”的场景设计的。它让每个设备独立管理自己的电源状态互不干扰。再往深一层看现代 SoC 的功耗管理是分层的。一个设备往往挂在某个电源域power domain下面电源域又可能挂在某个时钟域下面。runtime pm 的引用计数机制天然支持这种层级关系子设备都 idle 了父设备电源域才能 idle最终整个域断电。这套级联逻辑是 runtime pm 最精妙的地方也是排查功耗问题时最容易踩坑的地方。2.2 核心数据结构struct device 里的 pm 字段runtime pm 的所有状态都挂在struct device上。每个设备结构体里有一个struct dev_pm_info power成员这个结构体定义在include/linux/pm.h里是理解 runtime pm 的入口。里面几个关键字段值得单独拎出来说。power.runtime_status记录当前设备的 runtime pm 状态取值是RPM_ACTIVE或RPM_SUSPENDED。注意这个状态和硬件实际是否上电不是一回事它只是软件层面的逻辑状态。power.usage_count是引用计数谁要用这个设备就加一用完减一减到零才有机会进入 suspend。power.disable_depth是禁用深度调用pm_runtime_disable会让它加一只要不为零runtime pm 就完全不工作。power.child_count记录有多少子设备处于 active 状态这个字段是级联的核心。power.runtime_error和power.suspended_jiffies分别记录错误状态和累计 suspend 时长调试时很有用。还有一个容易忽略的字段是power.no_callbacks。如果驱动没有实现 runtime pm 回调内核会把这个标志置位表示这个设备不支持 runtime pm。很多新手写的驱动没实现回调然后调pm_runtime_get_sync发现没反应就是因为它压根没参与这套机制。2.3 状态机与引用计数的配合逻辑runtime pm 的状态迁移不是简单的 active/suspended 二选一它背后有一套带引用计数的状态机。核心规则可以概括成几条。设备初始状态是RPM_ACTIVEusage_count为 0disable_depth为 0。当有人调用pm_runtime_get或pm_runtime_get_syncusage_count加一如果之前是 suspended 就触发 resume。当调用pm_runtime_putusage_count减一减到零时如果满足条件没有子设备 active、没被禁用、没有 pending 的 resume 请求就触发 suspend。这里有个细节pm_runtime_get是异步的它只加计数不等待 resume 完成pm_runtime_get_sync会等待 resume 真正完成才返回。写驱动时如果后续代码依赖设备已经上电必须用 sync 版本否则可能访问到还没恢复的设备寄存器轻则读回垃圾值重则总线挂死。这个坑我见过太多次了。另外usage_count是带保护的内核用自旋锁保证并发安全。但引用计数的加减必须成对出现多减一次会导致计数下溢内核会打印 warning 并且状态错乱。所以驱动里每个 get 都要有对应的 put异常路径也不能漏。3. 驱动里怎么用API 用法与实操要点3.1 回调函数的实现runtime_suspend 与 runtime_resume要让一个设备支持 runtime pm驱动需要实现dev_pm_ops里的三个回调runtime_suspend、runtime_resume、runtime_idle。这三个函数在struct dev_pm_ops里定义通过SET_RUNTIME_PM_OPS宏注册。runtime_suspend在设备要进入低功耗时被调用里面做的事情通常包括保存必要的寄存器上下文、关闭设备时钟、切断设备电源如果可控、把引脚设为低功耗状态。runtime_resume反过来恢复时钟、重新初始化寄存器、把设备拉回工作状态。runtime_idle是可选的当usage_count减到零但还没决定是否 suspend 时调用驱动可以在这里做延迟决策比如判断是否真的值得 suspend。写回调时有几个硬性约束。首先runtime_suspend里不能睡眠的上下文要注意虽然 runtime pm 的回调通常运行在进程上下文可以睡眠但如果是从中断里触发的 put情况会复杂。其次回调里访问设备寄存器前要确保时钟已经打开这个顺序不能错。第三runtime_resume要能处理设备可能已经掉电的情况不能假设寄存器还是上次的值。一个常见的错误是在runtime_suspend里调用了会触发 runtime pm 的 API比如又去 get 别的设备这可能导致死锁或者状态机混乱。回调里应该只做纯粹的硬件操作不要再去操作 pm 引用计数。3.2 引用计数的正确使用姿势引用计数的使用是 runtime pm 最容易出错的地方。核心原则是谁需要使用设备谁负责 get 和 put而且必须成对。在字符设备驱动的 open 里 getrelease 里 put这是最经典的用法。在中断处理里如果中断需要访问设备可以在中断触发时 get处理完 put但要注意中断上下文不能睡眠得用pm_runtime_get而不是 sync 版本。在数据传输路径上每次发起传输前 get传输完成回调里 put这样设备在两次传输之间就能自动 suspend。有个细节值得强调pm_runtime_get_sync在设备已经被 disable 的情况下会返回错误码但计数还是会加。所以调用后要检查返回值如果失败要记得把计数还回去否则就泄漏了。我见过有驱动在 probe 失败路径上漏了 put结果设备永远无法 suspend。还有一种情况是设备之间有依赖关系。比如一个传感器挂在 I2C 总线上传感器要工作时 I2C 控制器也必须工作。这时候传感器驱动在 resume 时应该 get 它的 parentI2C 控制器在 suspend 时 put。内核的pm_runtime_get_sync会自动处理 parent 的引用但前提是 parent 也实现了 runtime pm。3.3 自动 suspend 与 autosuspend 延迟设置runtime pm 支持自动 suspend也就是usage_count减到零后自动触发 suspend。但很多时候我们不希望设备一空闲就立刻断电因为频繁的 suspend/resume 本身也有开销而且有些设备断电再上电需要较长的稳定时间。这时候就要用 autosuspend 机制。通过pm_runtime_set_autosuspend_delay设置一个延迟时间毫秒再用pm_runtime_use_autosuspend启用。这样usage_count减到零后内核会等这个延迟时间如果期间又有 get就取消 suspend。这个延迟的设置很讲究太短了省电效果差太长了响应变慢。一般根据设备的唤醒开销来定唤醒开销大的设备延迟设长一点。配合 autosuspendput 的时候要用pm_runtime_mark_last_busy更新时间戳再用pm_runtime_put_autosuspend而不是普通的 put。这样内核才知道设备最后一次使用是什么时候才能正确计算延迟。这个组合用法在 I2C、SPI 这类总线上非常常见。4. 级联与系统级协作runtime pm 的进阶机制4.1 父子设备的级联 suspend 逻辑runtime pm 的级联机制是它最强大的特性之一。当一个设备有子设备时父设备的 suspend 要等所有子设备都 suspend 之后才能进行。这个逻辑由child_count字段驱动。具体来说当子设备 resume 时内核会自动增加父设备的child_count并且如果父设备当前是 suspended会触发父设备的 resume。当子设备 suspend 时减少父设备的child_count如果减到零且父设备自己的usage_count也为零父设备才有机会 suspend。这套机制保证了电源域的层级关系子设备都歇了父设备才能歇。这个机制在设备树里体现得很明显。设备树里的power-domains属性描述了设备的电源域归属内核在初始化时会建立父子关系。调试的时候看/sys/kernel/debug/pm_genpd就能看到每个电源域下挂了哪些设备以及它们的 runtime 状态。有个坑要注意如果父设备没有实现 runtime pm 回调子设备的级联就会断掉。因为父设备不参与这套机制child_count的增减就没有意义。所以做电源域管理时父设备通常是电源域控制器必须实现完整的 runtime pm 支持。4.2 runtime pm 与系统 suspend 的交互runtime pm 和系统级 suspend 是两套机制但它们必须协作。系统要进入 suspend 时所有设备最终都要挂起不管它们当前的 runtime 状态是什么。内核的处理方式是在系统 suspend 流程里会先调用每个设备的prepare回调然后调用suspend回调。对于已经处于 runtime suspended 的设备系统 suspend 会跳过或者做特殊处理。反过来系统 resume 之后设备的 runtime 状态需要恢复。内核提供了pm_runtime_force_suspend和pm_runtime_force_resume这两个辅助函数让驱动在系统 suspend/resume 回调里同步 runtime 状态。这样系统 resume 后设备的 runtime 引用计数和状态能保持一致不会出现状态错乱。这里有个实际经验如果驱动在系统 suspend 回调里直接操作硬件而没有同步 runtime 状态系统 resume 后可能会出现设备被重复 resume 或者状态不一致的问题。正确做法是在系统 suspend 里调用pm_runtime_force_suspend在 resume 里调用pm_runtime_force_resume让内核帮你处理状态同步。4.3 电源域与时钟域的协同runtime pm 管的是设备级别的电源状态但实际硬件里还有电源域power domain和时钟域clock domain的概念。一个电源域可能包含多个设备一个时钟域也可能被多个设备共享。runtime pm 的级联机制天然适配电源域的层级但时钟域的管理需要额外注意。时钟的开关通常由clk_prepare_enable和clk_disable_unprepare控制。在 runtime pm 回调里resume 时开时钟suspend 时关时钟这是标准做法。但如果有多个设备共享一个时钟就要用引用计数来管理内核的 common clock framework 已经提供了这个能力。关键是不要在 runtime suspend 里关掉别人还在用的时钟。电源域的控制通常由 genpdgeneric power domain框架负责。genpd 会监听设备的 runtime pm 状态当域内所有设备都 suspended 时自动关闭整个域。驱动不需要直接操作电源域寄存器只要正确实现 runtime pm 回调genpd 会帮你处理。这也是为什么调试功耗问题时先看 runtime pm 状态再看 genpd 状态的原因。5. 调试与排查功耗压不下去时怎么定位5.1 debugfs 里的 runtime pm 信息怎么读内核提供了丰富的 debugfs 接口来观察 runtime pm 状态。挂载 debugfs 后/sys/kernel/debug/runtime_pm目录下有每个设备的 runtime pm 信息。/sys/kernel/debug/pm_genpd则展示电源域的层级和状态。读这些信息时重点看几个字段status是 active 还是 suspendedusage_count是多少child_count是多少disable_depth是多少。如果一个设备本该 idle 但 status 还是 active先看 usage_count如果不为零说明有地方 get 了没 put如果为零但 child_count 不为零说明有子设备还 active如果 disable_depth 不为零说明被禁用了。/sys/kernel/debug/pm_genpd里能看到每个电源域下挂的设备列表以及域本身的 on/off 状态。如果域是 on 但里面设备都 suspended可能是域的控制逻辑有问题或者有设备没正确注册到域里。5.2 常见问题速查表现象可能原因排查方向设备一直 activeusage_count 不为零有 get 没 put引用计数泄漏检查所有 get 路径的异常分支是否有 putusage_count 为零但设备不 suspenddisable_depth 不为零或 child_count 不为零检查是否调用了 pm_runtime_disable检查子设备状态设备 suspend 后立刻 resume有中断或轮询在触发 get检查中断处理里是否无条件 get检查定时器系统 suspend 后设备状态错乱系统 suspend 回调没同步 runtime 状态使用 pm_runtime_force_suspend/resume父设备无法 suspend子设备没实现 runtime pm 或没正确 put检查子设备回调实现和引用计数resume 后设备寄存器值异常resume 回调没重新初始化寄存器检查 resume 里是否恢复了所有必要寄存器5.3 几个实战踩坑记录第一个坑是中断里的 get/put 配对。有个驱动在中断处理里调用了pm_runtime_get_sync结果在中断上下文里睡眠导致内核报错。中断上下文只能用异步的pm_runtime_get而且要注意如果设备已经 suspended异步 get 只是加计数实际 resume 是异步进行的中断处理里不能假设设备已经上电。第二个坑是 autosuspend 延迟设得太短。有个传感器驱动把延迟设成了 10ms结果每次数据传输间隔稍长就 suspend下次传输又要 resume反而增加了功耗和延迟。后来改成 500ms整体功耗降下来了。这个延迟要根据实际使用模式来调不能拍脑袋。第三个坑是系统 suspend 和 runtime pm 的状态冲突。有个驱动在系统 suspend 回调里手动调用了pm_runtime_suspend结果系统 resume 后设备的 runtime 状态和实际硬件状态不一致导致后续访问失败。正确做法是用pm_runtime_force_suspend它会正确处理状态同步。第四个坑是电源域注册顺序。有个设备的电源域在设备 probe 之后才注册导致设备初始化时找不到父电源域runtime pm 级联失效。设备树里的power-domains属性必须保证在设备 probe 前电源域已经就绪这通常由 probe 顺序或者-EPROBE_DEFER机制保证。6. 从驱动到系统runtime pm 的扩展与优化思路6.1 多设备协同的功耗优化单个设备的 runtime pm 做好之后下一步是多个设备的协同优化。比如一个摄像头模组包含 sensor、CSI 控制器、ISP 三个设备它们之间有数据依赖。sensor 出图时 CSI 和 ISP 都要工作sensor 停流后 CSI 和 ISP 也应该跟着 suspend。这种场景下可以在 sensor 驱动的 stream on/off 里统一管理三个设备的引用计数或者利用设备树的父子关系让级联自动处理。更复杂的场景是多个设备共享一个电源域但使用模式不同。这时候要分析哪些设备经常一起工作哪些是独立的。如果两个设备总是一起用可以考虑合并它们的 runtime pm 管理如果独立性强就各自管理让 genpd 在域级别做最终决策。6.2 性能与功耗的平衡取舍runtime pm 的本质是在功耗和性能之间做权衡。suspend 越激进功耗越低但 resume 延迟越大。对于交互式设备resume 延迟直接影响用户体验所以 autosuspend 延迟要设得保守一些。对于后台批处理设备可以设得激进一些。还有一个取舍是 suspend 的深度。有些设备支持多种低功耗状态浅睡眠唤醒快但省电少深睡眠省电多但唤醒慢。runtime pm 的回调里可以根据使用模式选择不同的睡眠深度比如空闲时间短就浅睡眠空闲时间长就深睡眠。这需要驱动自己维护状态判断逻辑。6.3 后续可以深入的方向runtime pm 往上可以延伸到系统级的电源管理策略比如根据系统负载动态调整各设备的 autosuspend 延迟往下可以结合具体的硬件特性比如 DVFS动态电压频率调整和 runtime pm 配合在设备 idle 时不仅关时钟还降电压。对于做嵌入式产品的同学还可以研究一下 runtime pm 和热管理的关系。设备频繁 suspend/resume 会产生额外的功耗和热量如果热管理策略和 runtime pm 策略冲突可能导致设备在温度边缘反复切换状态。这个交叉领域目前资料不多但实际项目中很常见。我个人在实际项目里的体会是runtime pm 的坑大多不在 API 本身而在引用计数的管理和状态同步上。把每个 get/put 的路径画出来确保异常分支也配对基本就能解决八成的功耗异常。剩下的两成往往和硬件特性、电源域设计有关需要结合具体平台的手册和 debugfs 信息慢慢磨。调试的时候别急着改代码先把/sys/kernel/debug/runtime_pm和/sys/kernel/debug/pm_genpd的状态读明白让数据告诉你问题在哪。