☰
Linux Runtime PM 深度解析:引用计数、状态机与驱动落地实践
2026/10/6 6:59:25 网站建设 项目流程

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_ACTIVE,usage_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回滚,记录 error
RPM_SUSPENDEDgetRPM_RESUMING开始执行 resume 回调
RPM_RESUMING回调成功RPM_ACTIVE设备已唤醒
RPM_RESUMING回调失败RPM_SUSPENDED保持挂起,记录 error

2.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异步触发 suspend,pm_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 的初始化顺序错一步,后面全是坑。我总结了一个比较稳妥的顺序:

  1. 先pm_runtime_disable,确保初始化期间不会被意外 suspend。
  2. 完成硬件初始化,包括时钟、电源域、寄存器配置。
  3. 调用pm_runtime_set_active把状态标记为 active,因为此时硬件确实是开着的。
  4. 如果需要 autosuspend,调用pm_runtime_use_autosuspend和pm_runtime_set_autosuspend_delay。
  5. 最后pm_runtime_enable,让 runtime pm 正式生效。
  6. 如果初始化后设备应该空闲,再调用一次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,能看到完整的状态迁移过程,比盲猜高效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询