☰
Linux PM Core 功耗框架分层解析与 suspend 异常排查实战
2026/10/30 3:58:30 网站建设 项目流程

1. 从一次功耗异常排查说起:PM Core 到底管什么

去年冬天调一块 ARM64 开发板,待机电流死活降不到规格书标称的 2mA,实测总在 18mA 上下晃。用示波器抓电源轨、拿热成像扫板子,硬件侧翻了个底朝天也没找出问题。最后把/sys/kernel/debug/pm_genpd/和/sys/kernel/debug/clk/clk_summary两个节点一对比,发现有个显示控制器的时钟在系统进入 suspend 之后压根没被关掉——设备驱动注册了dev_pm_ops,但它的父级电源域(genpd)没把power_off回调挂上,导致整个域一直挂在 ON 状态。这件事让我重新把 Linux 功耗子系统从 PM Core 往上捋了一遍,也促成了这篇总结。

Linux 内核功耗子系统(Power Management Subsystem)是内核里负责整机与设备级省电的一整套框架,核心入口就是PM Core(drivers/base/power/目录下那堆代码)。它要解决的问题很朴素:一块 SoC 上有几十上百个设备、十几个电源域、几百个时钟,系统要休眠时,谁先关、谁后关、谁不能关、关错了怎么回滚,这些顺序和依赖关系必须有个统一的地方来管。PM Core 就是那个“总调度”。

这篇文章适合三类人看:一是做嵌入式 Linux 驱动、经常和suspend/resume打交道的工程师;二是做系统功耗优化、需要读懂/sys/kernel/debug下各种 PM 节点的同学;三是准备内核相关面试、想把“功耗框架分层”这件事讲清楚的人。我会从分层设计的角度,把 PM Core、Device PM、genpd、Runtime PM、System Sleep 这几层的关系拆开讲,配上我实际调试时用的命令和踩过的坑。读完你应该能自己判断:一个设备不休眠,问题到底出在哪一层。

2. 功耗框架为什么要分层:一次设计取舍的复盘

2.1 不分层会怎样:一个反例

早期内核里设备省电是各驱动自己实现的,suspend回调里想关时钟就关时钟,想切 regulator 就切 regulator。听起来自由,实际是灾难。我见过一个老平台,网卡驱动在suspend里把 PHY 电源关了,但 MAC 的时钟还开着,结果 resume 时 PHY 上电时序不对,链路要 3 秒才能重新协商。更麻烦的是,两个驱动共享一个 regulator,A 驱动 suspend 时把它关了,B 驱动还在用,直接挂死。

问题的根子在于:省电操作天然有依赖和顺序。电源域有父子关系,时钟有树状结构,设备有共享资源。让每个驱动各自为政,等于把全局约束下放给局部,必然冲突。所以内核的做法是抽出一层 PM Core,把“什么时候调用哪个回调”这件事集中管起来,驱动只负责实现“我该怎么省电”,不负责“我该在什么时候省电”。

2.2 分层的边界怎么划

PM Core 的分层不是拍脑袋定的,它对应的是三种不同的省电粒度和触发时机:

层级目录/接口管什么触发时机
System Sleepkernel/power/整机 suspend/hibernate用户写/sys/power/state
Device PMdrivers/base/power/main.c设备 suspend/resume 回调编排系统睡眠或 runtime 触发
Runtime PMdrivers/base/power/runtime.c设备空闲时单独下电驱动主动pm_runtime_put
genpddrivers/base/power/domain.c电源域整体开关域内设备全部 idle
Clock/Regulatordrivers/clk、drivers/regulator具体硬件资源被上层调用

这张表是我自己捋的时候画的,它解释了一个常见困惑:为什么suspend回调里不能随便关时钟?因为时钟属于最底层资源,它的开关应该由 genpd 或 Runtime PM 根据“还有没有设备在用”来决定,而不是某个驱动在suspend里越级操作。分层设计的本质是把决策权和执行权分开:PM Core 决定顺序,驱动执行动作,genpd 管资源聚合。

2.3 分层带来的两个直接好处

第一个好处是可回滚。系统睡眠时如果第 5 个设备 resume 失败,PM Core 知道前面 4 个已经 resume 了,会按相反顺序把它们重新 suspend 回去,保证状态一致。这个逻辑在dpm_resume_noirq那一串函数里,自己写驱动是绝对做不到这么严谨的。

第二个好处是可观测。因为所有 PM 操作都过 PM Core,内核就能在/sys/kernel/debug/pm_genpd/、/sys/power/这些地方把状态暴露出来。我排查开头那个 18mA 问题,靠的就是这些节点。如果省电逻辑散在各驱动里,根本没有统一的观测点。

提示:分层不是越多越好。我见过有人给一个只有两个设备的简单平台硬套 genpd,结果多了一层间接调用,调试反而更绕。分层要匹配硬件的实际电源拓扑,SoC 有几个独立电源域就分几层,别为了“架构好看”硬加。

3. PM Core 的核心数据结构:读懂这几个结构体就懂了一半

3.1 dev_pm_ops:驱动和 PM Core 的契约

驱动要参与功耗管理,第一步就是填dev_pm_ops。这个结构体定义在include/linux/pm.h,里面是一堆回调函数指针。新手最容易懵的是:为什么有suspend、suspend_noirq、suspend_late这么多相似的?它们不是重复,而是对应系统睡眠的不同阶段。

static const struct dev_pm_ops mydev_pm_ops = { .suspend = mydev_suspend, .resume = mydev_resume, .runtime_suspend = mydev_runtime_suspend, .runtime_resume = mydev_runtime_resume, };

系统睡眠的完整阶段顺序是:prepare→suspend→suspend_late→suspend_noirq→ (硬件睡眠)→resume_noirq→resume_early→resume→complete。为什么这么设计?因为有些操作必须在中断关闭前做(比如要等一个中断确认设备已停),有些必须在中断关闭后做(比如写寄存器不能被中断打断)。_noirq后缀的就是中断已关的阶段。

我个人的经验是:大部分驱动只需要实现suspend/resume和runtime_suspend/runtime_resume四个。_late、_noirq这些留给真正有严格时序要求的设备,比如需要先停 DMA 再关时钟的控制器。乱实现_noirq回调很容易在 resume 阶段踩到“中断还没恢复就访问设备”的坑。

3.2 dev_pm_info:每个设备的状态账本

每个struct device里都内嵌了一个struct dev_pm_info,这是 PM Core 记录设备功耗状态的账本。关键字段有:

  • power_state:当前电源状态(DPM_ON、DPM_SUSPENDING、DPM_SUSPENDED等)
  • runtime_status:Runtime PM 状态(RPM_ACTIVE、RPM_SUSPENDED)
  • usage_count:Runtime PM 的引用计数
  • disable_depth:Runtime PM 是否被禁用
  • child_count:有多少子设备依赖它

usage_count和disable_depth是排查 Runtime PM 问题的核心。我遇到过设备一直不进入 runtime suspend,最后发现是某个驱动pm_runtime_get之后忘了pm_runtime_put,usage_count一直是 1。这种问题用/sys/kernel/debug/pm_genpd/看不出来,得直接读设备节点:

cat /sys/devices/platform/soc/xxx/power/runtime_status cat /sys/devices/platform/soc/xxx/power/usage_count cat /sys/devices/platform/soc/xxx/power/control

control字段是auto还是on也很关键。如果驱动调了pm_runtime_forbid,这里会显示on,设备永远不会自动挂起。

3.3 设备树里的 power-domains 属性

分层设计在设备树里的体现就是power-domains属性。一个设备挂到哪个电源域,不是驱动代码里写死的,而是设备树声明的:

gpu: gpu@ff9a0000 { compatible = "vendor,gpu"; power-domains = <&power RK3399_PD_GPU>; clocks = <&cru ACLK_GPU>; };

这样 PM Core 在设备 probe 时就能通过dev->pm_domain找到它所属的 genpd,后续 runtime suspend 时自动触发域的下电判断。这个设计的好处是硬件拓扑和驱动逻辑解耦:同一份 GPU 驱动,换个 SoC 只要改设备树,不用改代码。

注意:power-domains和clocks是两个独立维度。设备可能属于某个电源域,但它的时钟由另一个时钟控制器管。排查时两个都要看,别只盯着一个。

4. Runtime PM 与 System Sleep:两套机制怎么配合

4.1 Runtime PM 的引用计数模型

Runtime PM 的核心是引用计数。驱动要用设备时pm_runtime_get_sync,用完pm_runtime_put,计数归零且设备空闲时,PM Core 自动调用runtime_suspend。这个模型的好处是自动聚合:多个调用者共享一个设备,只要还有人在用,设备就不会被关。

/* 使用设备前 */ pm_runtime_get_sync(dev); /* 访问硬件寄存器 */ readl(dev->base + REG); /* 用完后 */ pm_runtime_put(dev);

但这里有个经典陷阱:pm_runtime_get_sync在原子上下文里不能用,因为它可能睡眠。中断处理里要用pm_runtime_get(不等待)配合pm_runtime_put_autosuspend。我踩过一次坑:在中断里调了get_sync,系统直接报scheduling while atomic。

另一个坑是autosuspend。很多驱动用pm_runtime_use_autosuspend+pm_runtime_set_autosuspend_delay做延迟挂起,避免频繁开关。延迟设多少有讲究:设太短省电效果差,设太长响应变慢。我的经验是按设备的最短空闲间隔来定,比如触摸屏设 50ms,网卡设 200ms,具体看实测。

4.2 System Sleep 如何“接管”Runtime PM

系统要整体睡眠时,Runtime PM 得先让路。PM Core 在dpm_suspend之前会调用pm_runtime_resume把所有设备唤醒到 active,然后再走系统睡眠流程。为什么?因为系统睡眠的回调假设设备处于已知状态,如果设备已经 runtime suspended,直接调suspend回调可能操作到未上电的寄存器。

这个“先唤醒再睡眠”的过程在日志里能看到:

dmesg | grep -i "pm_runtime" # 会看到 suspend 前一堆 runtime resume 的记录

理解这一点对排查“系统睡眠后设备不工作”很关键。如果某个设备在系统 resume 后状态不对,先确认它在 suspend 前是否被正确 resume 到 active,再看它的suspend回调有没有正确保存状态。

4.3 两套机制的优先级关系

场景Runtime PM 状态System Sleep 行为
设备 activeRPM_ACTIVE正常调 suspend 回调
设备已挂起RPM_SUSPENDED先 resume 再 suspend
Runtime PM 被禁用disable_depth > 0仍参与系统睡眠
设备标记 no_callbacks不参与 runtime仍参与系统睡眠

这张表说明一个容易误解的点:Runtime PM 被禁用不等于设备不参与系统睡眠。disable_depth只影响 runtime 自动挂起,系统睡眠是另一条路径。我见过有人为了“让设备不休眠”调了pm_runtime_forbid,结果系统 suspend 时设备照样被关,白忙一场。

5. genpd:电源域聚合与分层下电的实操

5.1 genpd 的工作模型

genpd(Generic Power Domain)是 PM Core 里管电源域的那一层,代码在drivers/base/power/domain.c。它的模型很简单:一个电源域包含若干设备,域内所有设备都 idle 时,域才能下电。域还有父子关系,子域全关父域才能关。

/* 典型的 genpd 注册 */ static struct generic_pm_domain gpu_pd = { .name = "GPU_PD", .power_off = gpu_pd_power_off, .power_on = gpu_pd_power_on, };

power_off回调里通常做三件事:关时钟、切 regulator、拉电源开关。注意顺序:先关时钟再切电源,反过来会有瞬间的时钟毛刺。这个顺序在power_on里要反过来:先上电再开时钟。

5.2 用 debugfs 看电源域状态

/sys/kernel/debug/pm_genpd/是我最常用的调试入口。每个域一个目录,里面有current_state、device_count、subdomain_count等文件。

# 看所有电源域状态 for d in /sys/kernel/debug/pm_genpd/*/; do echo "=== $d ===" cat $d/current_state cat $d/device_count done

current_state显示on或off。如果系统 suspend 后某个域还是on,说明域内有设备没挂起,或者域本身没实现power_off。我开头那个 18mA 问题,就是靠这个发现显示控制器所在的域一直是on。

5.3 域内设备全部 idle 才下电的验证

genpd 下电的条件是域内所有设备都 runtime suspended。验证方法:

# 看域内设备列表 ls /sys/kernel/debug/pm_genpd/GPU_PD/ # 逐个检查设备 runtime 状态 cat /sys/devices/platform/soc/gpu/power/runtime_status

如果某个设备是active,域就不会下电。这时候要顺着usage_count查是谁没put。我一般用ftrace抓pm_runtime事件:

echo 1 > /sys/kernel/debug/tracing/events/power/enable cat /sys/kernel/debug/tracing/trace_pipe

这样能看到每次pm_runtime_get/put的调用栈,谁多调了一次一目了然。

实操心得:genpd 的power_off回调里不要做耗时操作。我见过有人在里面加msleep(10)等电源稳定,结果系统 suspend 时间被拉长到几百毫秒。正确做法是用regulator_set_voltage的异步接口,或者把等待放到suspend_late阶段。

6. 常见问题与排查技巧实录

6.1 系统 suspend 失败怎么定位

suspend 失败最常见的原因是某个设备的suspend回调返回了错误码。PM Core 会打印是哪个设备:

dmesg | grep -i "suspend.*failed\|PM:.*error" # 典型输出:PM: Device xxx failed to suspend: error -16

拿到设备名后,去/sys/devices/.../power/下看它的状态,再结合驱动代码看suspend回调里哪一步可能返回-EBUSY。-EBUSY通常意味着设备还在忙,比如 DMA 没停、有 pending 的中断。

6.2 设备 resume 后不工作

这类问题八成是suspend时状态没保存好,或者resume顺序不对。排查步骤:

  1. 确认resume回调被调用了(加dev_info或看 ftrace)
  2. 确认resume里恢复了所有suspend时改动的寄存器
  3. 确认依赖的资源(时钟、regulator)在resume前已就绪

第 3 点最容易出问题。如果设备 A 依赖设备 B 的时钟,而 B 的resume排在 A 后面,A 就会访问到没时钟的寄存器。解决办法是用device_link声明依赖:

device_link_add(consumer, supplier, DL_FLAG_PM_RUNTIME);

这样 PM Core 会自动调整 resume 顺序。

6.3 常见问题速查表

现象可能原因排查命令
待机电流偏高某电源域未下电cat /sys/kernel/debug/pm_genpd/*/current_state
设备不进入 runtime suspendusage_count 不为 0cat .../power/usage_count
suspend 返回 -EBUSY设备仍忙dmesg看设备名,查驱动回调
resume 后寄存器值丢失suspend 未保存状态对比 suspend/resume 寄存器 dump
系统无法进入 suspend有 wakeup 源活跃cat /sys/kernel/debug/wakeup_sources
时钟未关闭genpd 未触发cat /sys/kernel/debug/clk/clk_summary

6.4 几个我踩过的坑

第一个坑:pm_runtime_get_sync在 probe 里调用后忘了put,设备永远 active。这个错误很隐蔽,因为功能正常,只是费电。建议 probe 里用pm_runtime_enable+pm_runtime_set_active的组合,让 PM Core 自己管初始状态。

第二个坑:genpd 的power_off回调里访问了已经被关的时钟。原因是回调顺序没理清,power_off里先关了时钟,后面又去读一个需要时钟的寄存器。正确做法是把所有寄存器操作放在关时钟之前。

第三个坑:设备树里power-domains写错了域,设备挂到了错误的电源域。这种问题编译不报错,运行时才暴露,而且现象诡异——设备能用,但省电行为不对。排查时用cat /sys/kernel/debug/pm_genpd/*/对照设备树确认。

7. 分层设计给驱动开发的启示

把 PM Core 这套分层捋清楚之后,我改驱动的方式变了。以前是“我要省电,我在 suspend 里关时钟”,现在是先问三个问题:这个设备属于哪个电源域?它的时钟和 regulator 由谁管?它和别的设备有没有共享资源?想清楚这三个问题,代码自然就写对了——驱动只管实现runtime_suspend/runtime_resume,资源聚合交给 genpd,顺序编排交给 PM Core。

这套分层还有个隐性好处:它让功耗问题变得可定位。因为每一层都有对应的 debugfs 节点,问题出在哪一层,看节点状态就能缩小范围。我现在的排查习惯是:先看/sys/power/state能不能写进去,再看/sys/kernel/debug/pm_genpd/哪个域没关,最后看具体设备的runtime_status和usage_count。三步下来,八成问题能定位。

最后分享一个我常用的验证脚本,系统 suspend 前后各跑一次,对比差异:

#!/bin/bash # pm_snapshot.sh - 抓取 PM 状态快照 echo "=== genpd ===" for d in /sys/kernel/debug/pm_genpd/*/; do echo "$(basename $d): $(cat $d/current_state)" done echo "=== clk ===" grep -E "enable_cnt" /sys/kernel/debug/clk/clk_summary | awk '$2 != 0' echo "=== wakeup ===" cat /sys/kernel/debug/wakeup_sources | awk '$6 != 0'

suspend 前跑一次存成before.txt,resume 后跑一次存成after.txt,diff一下,哪个域、哪个时钟、哪个 wakeup 源状态不对,一目了然。这个脚本帮我省了无数次手动翻节点的时间,你也可以根据自己的平台改改直接用。

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

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

立即咨询