
Runtime PM 是 Linux 内核功耗管理里最容易被低估的一块。很多人第一次接触它会觉得不就是pm_runtime_get和pm_runtime_put两个函数来回调吗能有多复杂。但真正在驱动里用过一轮之后就会发现这套机制的坑几乎全藏在什么时候可以睡什么时候不能睡引用计数为什么对不上suspend 回调里到底能不能碰寄存器这些细节里。它不像系统级的 suspend/resume 那样有明显的全局流程而是渗透在每一个设备驱动的日常操作中平时不显山不露水一旦计数失衡或者回调顺序搞错轻则设备不工作重则整个电源域锁死。这篇内容我打算把 runtime pm 从设计动机到代码落地完整梳理一遍。适合已经写过字符设备驱动、对 Linux 设备模型有基本了解、但还没系统啃过电源管理子系统的朋友。如果你正在调试一个设备明明没人用却一直耗电或者runtime suspend 之后再也唤不醒的问题那这篇基本就是给你写的。我会尽量把每个 API 背后的判断逻辑讲清楚而不是只列函数原型因为 runtime pm 的难点从来不是记住接口而是理解它在什么条件下会真正触发动作。1. runtime pm 到底在解决一个什么问题1.1 从设备常开到按需供电的转变早期的驱动模型里设备一旦 probe 成功时钟、电源域、总线接口基本就保持开启状态直到系统整体 suspend 或者驱动 remove。这种模式在 PC 上问题不大因为外设数量有限而且很多设备本来就长期在线。但到了移动端和嵌入式场景情况完全变了一个 SoC 上挂着几十个控制器屏幕、摄像头、音频编解码器、各种传感器如果全部常开待机功耗根本压不下来。Runtime PM 的核心目标就是让每个设备在没有实际业务的时候主动进入低功耗状态而且这个判断是设备自己做的不依赖全局的电源管理策略。换句话说它把要不要省电这件事从系统层面下放到了驱动层面。设备驱动通过引用计数告诉内核我现在有人用或者我现在空闲了内核根据计数决定是否调用驱动注册的 suspend 回调。这里有个关键点容易被忽略runtime pm 的 suspend 和系统 suspend 是两套独立的状态机。一个设备可以处于 runtime suspended 状态同时系统整体还在正常运行反过来系统进入 suspend 时runtime pm 的状态也会被纳入考虑。理解这两者的关系是后面不踩坑的前提。1.2 引用计数模型的设计哲学Runtime PM 用引用计数来判断设备是否空闲这个设计看起来简单但背后有明确的取舍。为什么不用最后访问时间 超时这种方案因为超时机制需要定时器而定时器本身在低功耗场景下就是负担而且超时时间很难定得合理——定短了频繁上下电影响性能定长了省电效果打折。引用计数则把判断权交给了调用方谁在用设备谁就负责 get用完了就 put。这样内核不需要猜只需要在计数归零时触发 suspend。代价是驱动开发者必须严格配对 get/put一旦漏掉一个 put设备就永远不会进入低功耗反过来多 put 一次设备可能在还被使用时就被挂起直接导致功能异常。提示runtime pm 的引用计数是 per-device 的不是全局的。每个struct device都有自己的usage_count所以排查计数问题时一定要先确认是哪个设备。1.3 runtime pm 与系统 suspend 的边界很多人会混淆这两个概念。系统 suspend比如 echo mem /sys/power/state是整个系统进入低功耗所有设备都要走一遍 suspend 流程而 runtime pm 是单个设备在系统运行期间独立进出低功耗。两者在代码路径上会交汇当系统要 suspend 时内核会先确保所有设备都 runtime resume然后再统一走系统级 suspend 回调。这个交汇点带来一个常见问题如果某个设备的 runtime pm 回调里做了耗时操作系统 suspend 时会被它拖慢更糟的是如果 runtime suspend 回调里拿了锁而系统 suspend 路径又要拿同一把锁就可能死锁。所以写 runtime pm 回调时一定要清楚它会在哪些上下文里被调用。2. 核心数据结构与状态机拆解2.1 dev_pm_info 里到底存了什么每个struct device里都有一个struct dev_pm_info成员runtime pm 的所有状态都记录在这里。这个结构体字段不少但真正需要关注的就几个usage_count引用计数get 加一put 减一归零才可能 suspend。runtime_status当前 runtime 状态取值包括RPM_ACTIVE、RPM_RESUMING、RPM_SUSPENDING、RPM_SUSPENDED。runtime_error记录 runtime resume 是否失败失败后设备会被标记为 error 状态。disable_depth禁用深度调用pm_runtime_disable会加一只有为 0 时 runtime pm 才真正工作。request_pending、work用于异步处理 resume/suspend 请求的工作队列相关字段。理解disable_depth特别重要。很多驱动在 probe 早期会先pm_runtime_disable等初始化完成再pm_runtime_enable这期间所有 get/put 都不会触发实际动作只是改计数。如果你发现调了 get 但设备没 resume第一件事就是查disable_depth是不是非零。2.2 状态迁移的完整路径Runtime PM 的状态机不算复杂但迁移条件必须记牢。设备初始状态是RPM_ACTIVEusage_count为 0 时如果调用pm_runtime_put会触发 idle 检查满足条件就进入RPM_SUSPENDING回调执行成功后变成RPM_SUSPENDED。当有新的 get 到来从RPM_SUSPENDED进入RPM_RESUMING回调成功后回到RPM_ACTIVE。这里有个细节RPM_SUSPENDING和RPM_RESUMING是过渡态表示回调正在执行。如果此时又有新的请求进来内核会把请求挂起等当前回调结束后再处理。这就是为什么在回调里再次调用 runtime pm API 要格外小心容易触发递归或者请求丢失。当前状态触发动作目标状态说明RPM_ACTIVEput 且计数归零RPM_SUSPENDING开始执行 suspend 回调RPM_SUSPENDING回调成功RPM_SUSPENDED设备已挂起RPM_SUSPENDING回调失败RPM_ACTIVE回滚记录 errorRPM_SUSPENDEDgetRPM_RESUMING开始执行 resume 回调RPM_RESUMING回调成功RPM_ACTIVE设备已唤醒RPM_RESUMING回调失败RPM_SUSPENDED保持挂起记录 error2.3 回调函数的注册与调用时机驱动通过dev_pm_ops注册 runtime pm 回调主要用到三个runtime_suspend、runtime_resume、runtime_idle。其中runtime_idle比较特殊它在计数归零但还没决定是否 suspend 时被调用驱动可以在这里做延迟决策比如启动一个定时器过一会儿再 suspend。runtime_idle如果返回 0内核会继续走 suspend 流程如果返回-EBUSY表示驱动自己接管了后续处理内核不再自动 suspend。这个机制给了驱动很大的灵活性但也容易用错——很多人以为runtime_idle返回 0 就一定会 suspend其实还要看usage_count是否仍然为 0以及是否有 pending 的 resume 请求。3. 常用 API 的语义差异与选择3.1 get/put 与 get_sync/put_sync 的区别这是最容易搞混的一组接口。pm_runtime_get是异步的它只增加计数并标记需要 resume然后立刻返回实际的 resume 操作可能在工作队列里稍后执行。而pm_runtime_get_sync会同步等待 resume 完成确保函数返回时设备已经处于 active 状态。什么时候用哪个如果后续代码马上就要访问设备寄存器必须用_sync版本否则可能访问到还没上电的设备。如果只是标记我要用这个设备了实际访问在别的地方可以用异步版本。但异步版本有个坑它不返回 resume 是否成功你得自己检查runtime_error或者用pm_runtime_get_sync的返回值。对应的 put 也一样pm_runtime_put异步触发 suspendpm_runtime_put_sync同步等待 suspend 完成。在中断上下文或者不能睡眠的场景只能用异步版本因为_sync版本会睡眠等待。3.2 自动挂起与 autosuspend 机制pm_runtime_put_autosuspend是另一个高频接口它和普通 put 的区别在于计数归零后不会立即 suspend而是等一个延迟时间由pm_runtime_set_autosuspend_delay设置。这个机制专门解决设备频繁短时间使用的场景——比如触摸屏用户连续点击时如果每次都完整走一遍 suspend/resume开销太大用 autosuspend 可以把这些操作合并。使用 autosuspend 需要先调用pm_runtime_use_autosuspend使能然后设置延迟时间。延迟时间的选择是个经验活太短起不到合并效果太长省电效果打折。一般触摸屏、传感器这类交互设备会设几十到几百毫秒具体要看业务特征。注意autosuspend 的延迟计时是从最后一次 put 开始算的如果期间又有 get计时会重置。所以它合并的是连续空闲场景不是周期性使用场景。3.3 禁止与使能disable/enable 的正确用法pm_runtime_disable会把disable_depth加一pm_runtime_enable减一。只要disable_depth非零所有 runtime pm 操作都只改计数不触发回调。这个机制主要用于两个场景一是驱动初始化期间硬件还没准备好不希望被 suspend二是驱动 remove 期间要确保设备保持 active 直到清理完成。有个常见错误是在 probe 里调了pm_runtime_enable之后忘记处理初始状态。如果设备 probe 时硬件已经是 active 的应该调用pm_runtime_set_active把状态同步过来否则内核以为设备是 suspended第一次 get 时可能不会真正 resume导致访问失败。4. 在真实驱动里落地 runtime pm 的完整流程4.1 probe 阶段的初始化顺序Runtime PM 的初始化顺序错一步后面全是坑。我总结了一个比较稳妥的顺序先pm_runtime_disable确保初始化期间不会被意外 suspend。完成硬件初始化包括时钟、电源域、寄存器配置。调用pm_runtime_set_active把状态标记为 active因为此时硬件确实是开着的。如果需要 autosuspend调用pm_runtime_use_autosuspend和pm_runtime_set_autosuspend_delay。最后pm_runtime_enable让 runtime pm 正式生效。如果初始化后设备应该空闲再调用一次pm_runtime_put或pm_runtime_put_autosuspend。这个顺序的核心逻辑是先禁用防止干扰初始化完同步状态最后使能并释放初始引用。漏掉第 3 步是最常见的错误表现为第一次访问设备时没有 resume因为内核认为它已经是 active 的。4.2 业务路径中的 get/put 配对在读写、ioctl 等业务入口标准做法是在函数开头 get结尾 put。但要注意错误路径也要 put否则一次失败就会导致计数泄漏。我见过太多驱动在goto err分支里忘了 put结果设备用几次之后就再也不省电了。对于可能睡眠的操作用_sync版本对于中断上下文用异步版本并检查返回值。如果业务逻辑里有多处访问可以在最外层 get 一次内部不再重复 get减少计数操作开销。但这样做的风险是如果内部有异步操作put 的时机要仔细设计确保所有异步操作完成后再 put。4.3 suspend/resume 回调里该做什么不该做什么runtime_suspend回调里通常做这些事保存必要的寄存器状态、关闭时钟、切断电源域、配置唤醒源。runtime_resume则相反恢复电源、使能时钟、恢复寄存器、清除唤醒状态。不该做的事也很明确不要在回调里调用可能睡眠很久的操作不要拿会被系统 suspend 路径竞争的锁不要在 suspend 回调里访问已经关闭的硬件。特别提醒一点runtime_suspend回调执行时设备的时钟可能还没关但电源域状态取决于具体实现访问寄存器前一定要确认硬件还在可访问状态。回调应该做不应该做runtime_suspend保存寄存器、关时钟、断电源、配唤醒源长时间睡眠、拿全局锁、访问已关硬件runtime_resume恢复电源、开时钟、恢复寄存器重复初始化、申请资源、调用可能失败的复杂逻辑runtime_idle延迟决策、启动定时器直接执行 suspend 逻辑、阻塞等待4.4 唤醒源配置与中断处理设备 runtime suspend 之后要靠中断唤醒。这里的关键是唤醒源必须在 suspend 之前配置好而且中断控制器要能在这个电源状态下工作。很多平台的 GPIO 中断在深度低功耗下会失效需要用专门的 wakeup 控制器。中断处理函数里通常要调用pm_runtime_get或者pm_runtime_get_sync来唤醒设备。如果中断上下文不能睡眠用异步版本然后在工作队列里做实际处理。这里有个经典问题如果中断来得太频繁设备会频繁 resume反而更耗电。解决办法是在中断处理里做聚合或者用 autosuspend 配合较长的延迟。5. 调试 runtime pm 问题的实战思路5.1 从 sysfs 和 debugfs 看状态排查 runtime pm 问题第一步永远是看状态。/sys/devices/.../power/目录下有runtime_status、runtime_usage、control等文件可以直接读出当前状态和计数。runtime_usage就是usage_count如果它一直不归零说明有地方漏了 put。更详细的信息在 debugfs 里挂载 debugfs 后可以看/sys/kernel/debug/pm_genpd/下的电源域状态以及/sys/kernel/debug/rpm/相关统计。有些平台还提供 runtime pm 的调用栈追踪能直接定位到是哪个函数拿了引用没释放。5.2 计数泄漏的定位方法计数泄漏是最常见的问题表现是设备永远不 suspend。定位方法有两种一是加日志在 get/put 里打印调用栈对比看哪个 get 没有对应的 put二是用pm_runtime_get_noresume配合手动检查这个接口只加计数不触发 resume适合在调试时标记可疑路径。还有一种隐蔽的泄漏在 probe 里 get 了但 remove 里没 put这种在设备热插拔场景才会暴露。所以写驱动时get/put 最好成对出现在同一个函数或者明确的生命周期里跨函数的引用要特别小心。5.3 resume 失败与 error 状态处理如果runtime_resume回调返回错误设备会被标记为runtime_error之后所有 get 都会直接失败。这时候设备基本就废了只能靠系统 suspend/resume 或者重新 probe 来恢复。所以 resume 回调里的操作要尽量可靠不要做可能失败的复杂逻辑。如果真的失败了排查方向是电源域是否真的上电、时钟是否使能、复位是否释放、寄存器访问是否返回错误。很多时候 resume 失败是因为 suspend 时关掉了某个依赖资源而 resume 时忘了恢复或者恢复顺序不对。5.4 与系统 suspend 的交互问题系统 suspend 时内核会先对所有设备做 runtime resume然后再走系统级 suspend。如果某个设备的 runtime resume 一直失败系统 suspend 也会失败。反过来如果系统 suspend 过程中某个设备的 runtime pm 回调被调用而此时系统已经进入不可睡眠状态就可能出问题。排查这类问题重点是看系统 suspend 的日志里有没有 runtime pm 相关的报错以及确认所有设备的 runtime pm 回调都能在系统 suspend 的上下文里安全执行。有些驱动在 runtime 回调里用了只能在进程上下文工作的接口系统 suspend 时就可能触发警告。6. 几个容易踩的坑与经验总结第一个坑是pm_runtime_get_sync在已经 active 的设备上调用会怎样。答案是它只增加计数不会重复执行 resume 回调这是符合预期的。但如果你依赖 resume 回调来做某些初始化就会落空。所以初始化逻辑不要放在 resume 回调里应该放在 probe 里。第二个坑是 autosuspend 和pm_runtime_put_sync混用。put_sync会忽略 autosuspend 延迟直接触发 suspend。如果你既想用 autosuspend 又调了put_sync延迟就白设了。正确做法是用pm_runtime_put_autosuspend或者pm_runtime_put配合 autosuspend 使能。第三个坑是电源域genpd和 runtime pm 的配合。当设备属于某个电源域时设备的 runtime suspend 可能不会真正断电而是等整个电源域都空闲才断。这时候看单个设备的状态是 suspended但电源域还是 on功耗没降下来。排查时要同时看设备和电源域的状态。第四个坑是runtime_idle回调返回值的误用。返回 0 表示我没事了你可以继续 suspend返回-EBUSY表示我自己处理。如果驱动想延迟 suspend 但又返回 0内核会立即 suspend达不到延迟效果。想延迟就用 autosuspend不要指望runtime_idle。第五个坑是在 suspend 回调里调用pm_runtime_get。这会导致递归因为 suspend 过程中计数变化会触发新的状态迁移。如果确实需要在 suspend 里唤醒自己应该用pm_runtime_get_noresume只加计数或者用工作队列异步处理。最后分享一个实用技巧在驱动里加一个模块参数控制 runtime pm 的开关调试时可以动态禁用方便对比开启和关闭时的行为差异。这个在定位到底是 runtime pm 的问题还是别的电源管理机制的问题时特别有用。另外runtime pm 的日志可以通过动态调试dynamic debug打开在drivers/base/power/runtime.c里加pr_debug或者用现成的dev_dbg能看到完整的状态迁移过程比盲猜高效得多。