1. 从一个真实的续航翻车案例说起
去年帮一个做嵌入式的朋友调一块工控板,硬件规格不差,四核处理器加 2GB 内存,跑的是精简过的 Linux 系统。按理说这种配置待机功耗应该能压到很低的水平,结果实测下来待机电流比预期高了将近一倍,电池撑不到标称时间的三分之二。他一开始怀疑是电源芯片选型问题,换了料还是老样子;又怀疑是某个外设没关干净,一个个拔掉测,改善有限。
最后用powertop一跑,问题浮出水面:系统里大量设备处于 active 状态,运行时的动态功耗管理几乎没起作用。换句话说,硬件该省的地方没省下来,软件层面根本没把 RPM 这套机制用起来。
这个案例我后来在好几个项目里都遇到过类似的版本。很多人对 Linux 电源管理的理解停留在"调调 CPU 调频"或者"改改 suspend 参数",但真正决定日常运行功耗的,往往是Runtime Power Management,也就是我们常说的RPM。它管的不是系统休眠这种"大动作",而是设备在系统正常运行期间,一个个"该睡就睡、该醒就醒"的细粒度管理。这篇文章就把 RPM 这套东西从头到尾拆一遍,包括它解决什么问题、内核里怎么实现的、驱动该怎么写、用户空间怎么调、以及实际调试中那些文档里不会写的坑。
如果你在做嵌入式、移动端、或者任何对功耗敏感的 Linux 产品,RPM 是绕不过去的一环。哪怕你只是想让自己的笔记本风扇少转两圈、续航多撑半小时,理解 RPM 也能帮你找到不少可以下手的地方。
2. RPM 到底在管什么:核心概念与设计动机
2.1 系统级电源管理 vs 运行时电源管理
先把概念理清楚,不然很容易和 suspend/resume 混在一起。Linux 的电源管理大致可以分成两个层面。
第一个层面是系统级电源管理,也就是 System PM。它处理的是整机状态切换,比如 suspend to RAM(挂起到内存)、suspend to disk(休眠到硬盘)、以及关机重启这些。这些操作的特点是"全局性"——整个系统一起进入低功耗状态,用户能明显感知到"机器睡了"。
第二个层面就是运行时电源管理,Runtime PM。它处理的是系统正常运行期间,单个设备在空闲时进入低功耗状态、有请求时再唤醒的过程。用户完全无感知,设备在后台自己"打盹"。比如你的 USB 控制器在没插设备时可以关掉时钟,SD 卡控制器在没有读写请求时可以降频甚至断电,I2C 控制器在总线空闲时可以停掉。
这两者的关系可以这么理解:System PM 是"全公司下班关灯锁门",RPM 是"某个员工暂时没事做就趴桌上眯一会儿,来活了立刻醒"。RPM 做得好,系统在"清醒"状态下的平均功耗就能大幅下降,这对靠电池吃饭的设备来说是生死攸关的。
2.2 RPM 的核心状态机
RPM 的核心是一个状态机,每个设备在运行时有两个基本状态:active(活跃)和suspended(挂起)。设备默认是 active 的,当它空闲一段时间后,RPM 框架会尝试把它切到 suspended;当有新的请求到来时,再把它唤醒回 active。
这里有个关键点:RPM 的 suspend 不等于硬件断电。具体设备进入什么低功耗状态,完全由驱动自己决定。有的驱动只是关掉时钟,有的会切断电源域,有的会同时保存寄存器上下文。RPM 框架只负责"什么时候该切","切成什么样"是驱动的事。
状态切换的触发条件主要有两个来源。一是驱动主动调用pm_runtime_put()之类的接口,表示"我用完了";二是空闲超时机制,设备在 active 状态持续空闲超过设定时间后,框架自动触发 suspend。唤醒则通过pm_runtime_get()系列接口,或者设备的中断处理里主动请求 resume。
2.3 为什么需要 RPM:从"全局休眠"到"按需省电"
早期 Linux 的电源管理基本只有 System PM,设备要么全开要么全关。这在台式机上问题不大,但在移动设备上就很要命——你不可能为了省电让整机频繁休眠,用户还在用呢。
RPM 的出现解决了这个矛盾。它让每个设备独立管理自己的功耗,互不干扰。CPU 在跑任务的时候,其他空闲的外设可以各自进入低功耗;某个外设被唤醒时,也不影响其他设备继续打盹。这种细粒度的管理,是移动设备能实现"全天候续航"的基础。
从设计哲学上看,RPM 体现的是**"按需供电"**的思想:不用的时候绝不浪费,要用的时候立刻响应。这个思想贯穿了整个 Linux 电源管理子系统,也是理解后续各种机制(比如 CPU idle、devfreq、regulator 动态管理)的基础。
3. 内核里的 RPM 框架:数据结构与调用链
3.1 核心数据结构
RPM 框架的核心数据结构是struct dev_pm_info,它嵌在struct device里,每个设备都有一份。这个结构里和 RPM 相关的字段主要有几个:
runtime_status:当前 RPM 状态,取值包括RPM_ACTIVE、RPM_RESUMING、RPM_SUSPENDING、RPM_SUSPENDED。runtime_error:记录最近一次 resume 是否失败。runtime_usage:使用计数,表示当前有多少个"使用者"。runtime_active_kids:处于 active 状态的子设备数量。runtime_suspended_time/runtime_active_time:统计设备在两种状态下的累计时间,调试时很有用。disable_depth:禁用深度,大于 0 时表示 RPM 被禁用。runtime_auto:是否允许自动 suspend(对应 sysfs 里的control属性)。
这些字段不是随便设的,它们共同构成了 RPM 的引用计数和状态判断逻辑。理解它们,才能看懂后面那些 API 的行为。
3.2 引用计数机制
RPM 用引用计数来决定设备能不能 suspend。核心规则是:只有当runtime_usage为 0,且所有子设备都处于 suspended 状态时,设备才允许进入 suspended。
pm_runtime_get()会把runtime_usage加一,pm_runtime_put()减一。当计数从 0 变正时,如果设备当前是 suspended,框架会触发 resume;当计数从正变 0 时,框架会启动一个延迟工作(delayed work),在空闲超时后尝试 suspend。
这里有个容易踩的坑:get 和 put 必须严格配对。多一次 put 会导致计数下溢,少一次 put 会导致设备永远无法 suspend。我在实际项目里见过因为错误处理路径漏了 put,导致某个控制器一直 active,整机功耗居高不下的情况。排查这种问题,/sys/kernel/debug/pm_genpd/和runtime_usage的值是重要线索。
3.3 父子设备的联动
RPM 支持设备树的层级关系。一个父设备只有在所有子设备都 suspended 之后,自己才能 suspend。反过来,父设备 resume 时,子设备不一定跟着 resume,但父设备 active 时子设备可以独立管理。
这个机制在 SoC 上特别重要。比如一个电源域(power domain)下面挂了多个外设,只有当所有外设都空闲了,整个电源域才能关掉。内核里的generic power domain(genpd)框架就是干这个的,它和 RPM 紧密配合,实现了"子设备全睡父设备才睡"的联动。
3.4 关键 API 一览
实际写驱动时,最常用的 RPM API 有这么几个:
| API | 作用 | 使用场景 |
|---|---|---|
pm_runtime_enable() | 启用设备的 RPM | probe 里调用 |
pm_runtime_disable() | 禁用 RPM | remove 或错误路径 |
pm_runtime_get_sync() | 增加计数并同步等待 resume 完成 | 需要立刻访问硬件前 |
pm_runtime_put_sync() | 减少计数并同步等待 suspend 完成 | 访问完硬件后 |
pm_runtime_get() | 增加计数,异步 | 中断上下文等不能睡眠的地方 |
pm_runtime_put() | 减少计数,异步 | 同上 |
pm_runtime_put_autosuspend() | 减少计数并启动自动 suspend 计时 | 希望延迟 suspend 时 |
pm_runtime_set_active() | 强制设置状态为 active | 初始化时同步状态 |
pm_runtime_set_suspended() | 强制设置状态为 suspended | 初始化时同步状态 |
pm_runtime_allow()/pm_runtime_forbid() | 允许/禁止自动 suspend | 对应 sysfs 的 control |
同步版本(_sync)会阻塞等待状态切换完成,异步版本只是改计数然后返回。在能睡眠的上下文里优先用同步版本,逻辑更清晰;在中断或原子上下文里只能用异步版本。
4. 驱动侧实操:把 RPM 接进你的设备
4.1 驱动里 RPM 的标准接入流程
给一个设备驱动接入 RPM,标准流程大致是这样的:
第一步,在 probe 函数里初始化硬件后,调用pm_runtime_set_active()把设备状态设为 active,然后pm_runtime_enable()启用 RPM。注意顺序不能反,否则可能出现状态不一致。
第二步,在需要访问硬件的地方,用pm_runtime_get_sync()确保设备处于 active,访问完用pm_runtime_put_autosuspend()释放。用 autosuspend 版本是为了让设备在空闲一小段时间后再 suspend,避免频繁的 suspend/resume 抖动。
第三步,实现struct dev_pm_ops里的runtime_suspend、runtime_resume、runtime_idle三个回调。runtime_suspend里做真正的省电操作(关时钟、断电源、存上下文),runtime_resume里做恢复操作,runtime_idle一般不用实现,框架有默认行为。
第四步,在 remove 或错误路径里,先pm_runtime_disable(),再释放资源。
4.2 runtime_suspend 和 runtime_resume 里该做什么
这两个回调是 RPM 的灵魂,写得好不好直接决定省电效果和稳定性。
runtime_suspend里通常要做的事包括:保存设备寄存器上下文(如果硬件断电会丢失)、关闭设备时钟、关闭电源域、把引脚设为低功耗状态、关闭相关的中断源(如果不需要唤醒)。顺序很重要,一般是先停数据流,再关时钟,最后断电源。
runtime_resume里则是逆操作:先上电、再开时钟、恢复寄存器、重新初始化数据流、使能中断。如果 resume 过程中任何一步失败,要返回错误码,框架会把runtime_error置位,后续的 get 调用会直接失败,避免在硬件没准备好的情况下访问。
注意:
runtime_suspend和runtime_resume里不能睡眠太久,也不能调用可能引起递归 RPM 的接口。如果确实需要等待硬件稳定,用usleep_range()而不是msleep(),前者精度更高。
4.3 autosuspend 延迟的设置
pm_runtime_put_autosuspend()配合pm_runtime_set_autosuspend_delay()使用,可以设置一个延迟时间,设备在 put 之后不会立刻 suspend,而是等这个延迟过去再 suspend。这个机制对减少抖动非常关键。
延迟设多少合适?这要看设备的访问模式。如果设备被频繁访问(比如每秒好几次),延迟设大一点(比如 100ms 到 500ms),避免每次访问都触发一轮 suspend/resume。如果设备很少被访问,延迟可以设小一点(比如 10ms 到 50ms),让省电尽快生效。
我一般会先用一个保守值(比如 200ms)跑起来,然后用 ftrace 观察 suspend/resume 的频率,再根据实际情况调整。如果发现 suspend 次数远多于实际访问次数,说明延迟太小;如果发现设备长时间 active 但实际没干活,说明延迟太大或者 put 调用有问题。
4.4 一个简化的驱动示例
下面是一个简化的 RPM 接入示例,展示核心逻辑(省略了具体硬件操作):
static int mydev_probe(struct platform_device *pdev) { struct mydev *dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; /* 硬件初始化 */ mydev_hw_init(dev); /* 设置 autosuspend 延迟 */ pm_runtime_set_autosuspend_delay(&pdev->dev, 200); pm_runtime_use_autosuspend(&pdev->dev); /* 先设为 active,再启用 RPM */ pm_runtime_set_active(&pdev->dev); pm_runtime_enable(&pdev->dev); return 0; } static int mydev_runtime_suspend(struct device *dev) { struct mydev *d = dev_get_drvdata(dev); /* 保存上下文 */ mydev_save_context(d); /* 关时钟 */ clk_disable_unprepare(d->clk); /* 关电源域 */ regulator_disable(d->vdd); return 0; } static int mydev_runtime_resume(struct device *dev) { struct mydev *d = dev_get_drvdata(dev); /* 开电源域 */ regulator_enable(d->vdd); /* 开时钟 */ clk_prepare_enable(d->clk); /* 恢复上下文 */ mydev_restore_context(d); return 0; } static const struct dev_pm_ops mydev_pm_ops = { .runtime_suspend = mydev_runtime_suspend, .runtime_resume = mydev_runtime_resume, }; static int mydev_remove(struct platform_device *pdev) { pm_runtime_disable(&pdev->dev); return 0; }这个例子里,pm_runtime_use_autosuspend()是必须的,否则pm_runtime_put_autosuspend()不会生效。很多新手会漏掉这一步,然后奇怪为什么设备 put 之后立刻 suspend 了。
5. 用户空间怎么调:sysfs 与调试接口
5.1 sysfs 里的 RPM 属性
每个设备在/sys/devices/.../power/目录下都有一组 RPM 相关的属性文件,常用的有:
control:写入auto允许自动 suspend,写入on禁止自动 suspend(强制保持 active)。runtime_status:只读,显示当前状态(active、suspended、suspending、resuming、error)。runtime_usage:只读,当前使用计数。runtime_active_kids:只读,active 的子设备数量。runtime_active_time/runtime_suspended_time:只读,累计时间(毫秒)。autosuspend_delay_ms:读写,autosuspend 延迟。
这些属性在调试时非常有用。比如你想确认某个设备到底有没有进入 suspend,直接cat runtime_status就行;想知道它 suspend 了多久,看runtime_suspended_time。
5.2 用 sysfs 手动控制设备状态
调试时经常需要手动把某个设备强制保持 active,方便观察行为。操作很简单:
# 禁止自动 suspend,强制保持 active echo on > /sys/devices/.../power/control # 恢复自动 suspend echo auto > /sys/devices/.../power/control这个操作在排查"设备 suspend 后功能异常"这类问题时特别有用。如果control设为on后问题消失,说明问题出在 suspend/resume 路径上,可以重点查那两个回调。
5.3 用 ftrace 追踪 RPM 事件
内核提供了 RPM 的 tracepoint,可以用 ftrace 追踪状态切换。启用方法:
# 启用 rpm 相关 tracepoint echo 1 > /sys/kernel/debug/tracing/events/rpm/enable # 查看追踪结果 cat /sys/kernel/debug/tracing/trace输出里会显示每个设备的runtime_suspend、runtime_resume、runtime_idle事件,以及调用者的信息。这个工具在分析"谁在阻止设备 suspend"时特别好用——你能看到每次 get/put 的调用栈,快速定位到是哪个驱动或子系统在持有引用。
5.4 powertop 与 RPM 的关系
powertop是排查功耗问题的常用工具,它的 "Device stats" 页面会列出各个设备的 RPM 状态和 suspend 比例。如果某个设备的 suspend 比例很低(比如低于 50%),说明它大部分时间都处于 active,是潜在的功耗大户。
powertop 还能给出具体的优化建议,比如"某个设备从未进入 suspend,检查驱动是否支持 RPM"。不过 powertop 的建议不能全信,它有时候会误报,最终还是要结合 ftrace 和实际测量来判断。
6. 常见问题与排查技巧实录
6.1 设备永远不 suspend 的排查思路
这是最常见的问题。设备一直 active,功耗降不下来。排查步骤我一般是这样走的:
先看runtime_status和runtime_usage。如果runtime_usage大于 0,说明有地方持有引用没释放,用 ftrace 追踪 get/put 调用栈,找到那个"只 get 不 put"的代码路径。如果runtime_usage为 0 但状态还是 active,看runtime_active_kids,如果大于 0,说明有子设备没 suspend,顺着设备树往下查。
如果这些都正常,检查control属性是不是被设成了on,或者disable_depth是不是大于 0(说明 RPM 被禁用了)。还有一种情况是runtime_error被置位了,这通常意味着之前某次 resume 失败,框架出于安全考虑不再尝试 suspend,需要先解决 resume 失败的根本原因。
6.2 suspend 后功能异常的定位方法
设备 suspend 后功能异常,比如数据丢失、通信失败、中断不响应,这类问题通常出在runtime_suspend/runtime_resume的实现上。
我的排查套路是:先把control设为on强制保持 active,确认问题消失,证明问题确实在 RPM 路径上。然后在runtime_suspend和runtime_resume里加打印,观察调用顺序和返回值。常见的原因包括:上下文保存不完整、时钟关闭顺序错误、电源域关闭后某些寄存器丢失、中断没有正确重新使能。
还有一种隐蔽的情况是竞态:设备正在 suspend 的过程中来了一个访问请求,如果驱动没有正确处理这种并发,就会出现访问已关闭硬件的情况。解决办法是在访问硬件前用pm_runtime_get_sync()确保设备已经 resume 完成,而不是只检查状态。
6.3 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 设备永不 suspend | 引用计数未归零 | ftrace 追踪 get/put |
| 设备永不 suspend | 子设备未 suspend | 检查 runtime_active_kids |
| 设备永不 suspend | RPM 被禁用 | 检查 disable_depth 和 control |
| suspend 后功能异常 | 上下文保存不完整 | 检查 runtime_suspend 实现 |
| suspend 后功能异常 | 时钟/电源顺序错误 | 检查开关顺序 |
| suspend 后功能异常 | 并发竞态 | 检查访问前的 get_sync |
| resume 失败 | 硬件未就绪 | 检查 runtime_resume 返回值 |
| 功耗反而升高 | suspend/resume 抖动 | 调整 autosuspend 延迟 |
| 系统卡顿 | suspend 时阻塞太久 | 优化 runtime_suspend 耗时 |
6.4 几个容易忽略的坑
第一个坑是初始化顺序。pm_runtime_enable()之前必须先pm_runtime_set_active(),否则框架可能认为设备处于未知状态,导致第一次 get 时行为异常。这个顺序在文档里有写,但很容易被忽略。
第二个坑是错误路径漏 put。在 probe 或运行时访问硬件的函数里,如果中间某步失败直接 return,很容易漏掉对应的 put。建议用goto统一清理,或者用devm_系列的自动管理接口。
第三个坑是autosuspend 延迟设成 0。延迟为 0 意味着 put 之后立刻 suspend,如果设备被频繁访问,会导致大量的 suspend/resume,反而增加功耗和延迟。除非设备确实很少被访问,否则不要设 0。
第四个坑是在 runtime_suspend 里调用可能睡眠的接口。RPM 的 suspend/resume 回调运行在特定的上下文中,某些接口(比如会触发其他设备 RPM 的接口)可能导致死锁。如果确实需要,用pm_runtime_get_noresume()之类的接口小心处理。
7. 从 RPM 延伸出去:相关机制与进阶方向
7.1 genpd:电源域的层级管理
前面提到过 generic power domain,它是 RPM 在 SoC 层面的重要补充。genpd 把多个设备组织成一个电源域,域内所有设备都 suspend 后,整个域可以断电。这个机制在手机 SoC 上无处不在,几乎每个外设都挂在某个电源域下面。
genpd 和 RPM 的配合方式是:genpd 注册为一个"虚拟设备",各个外设作为它的子设备。当子设备全部 suspend 后,genpd 的runtime_suspend被调用,执行真正的断电操作。理解这个联动关系,对分析 SoC 级别的功耗问题很关键。
7.2 CPU idle 与 RPM 的协同
CPU idle 是另一个层面的运行时省电机制,它让 CPU 在空闲时进入低功耗状态(C-state)。CPU idle 和 RPM 是独立的,但会相互影响:CPU 进入深度 idle 时,某些依赖 CPU 时钟的外设可能也需要相应处理;反过来,外设频繁唤醒 CPU 会阻止 CPU 进入深度 idle。
分析整机功耗时,要把 CPU idle 和 RPM 放在一起看。cpuidle的统计和 RPM 的统计结合起来,才能定位到真正的功耗瓶颈。
7.3 调试工具链的搭建
做 RPM 调试,一套顺手的工具链能省很多时间。我常用的组合是:ftrace看事件流,powertop看整体分布,/sys/kernel/debug/pm_genpd/看电源域状态,再加上一个电流表做实际测量。软件工具告诉你"发生了什么",电流表告诉你"省了多少电",两者缺一不可。
如果条件允许,建议在硬件上预留电流测量点,调试阶段能实时看到功耗变化。很多问题在软件层面看不出来,一测电流就原形毕露。
7.4 后续可以深入的方向
RPM 本身不算复杂,但它牵扯的面很广。往下可以深入 genpd 的实现、regulator 的动态管理、clock framework 的省电特性;往上可以结合系统级的 suspend 策略、热管理框架一起考虑。真正把功耗做下来,往往是多个机制协同的结果,单靠 RPM 一个机制是不够的。
我在实际项目里的体会是,RPM 的接入本身不难,难的是把每个设备的 suspend/resume 路径都调对,以及在系统层面协调好各个设备的省电策略。前者靠仔细和耐心,后者靠对整体架构的理解。这两样东西,都不是看几篇文档就能掌握的,得在真实的项目里一个坑一个坑踩出来。