
1. 先从功耗这件事说起分层到底在分什么做嵌入式Linux开发的朋友应该都有过这种经历产品明明进入了待机状态可电池还是一路往下掉或者 suspend 以后系统根本睡不下去dmesg 里刷出一堆报错再或者连着调试器一切正常一拔掉电源就立刻被某个莫名其妙的中断唤醒。这些问题的根子大多不在驱动本身而是对 Linux 内核功耗框架的理解还停留在“调用一个接口就行”的层面。我最早接触 Linux 功耗管理时也是一头扎进 dev_pm_ops 里改回调改到后面发现睡眠流程根本不是我想的那样。真正让我把思路理顺的是反过来从 PM Core 入手把整个功耗框架的层次先看懂。PM Core 不是一个具体的驱动也不是某个 sysfs 节点而是内核功耗管理的中枢逻辑它负责把用户空间、驱动层、平台层和硬件之间的协调整合起来定义好“什么时候能睡”“睡到什么程度”“醒来以后怎么恢复”。这篇文章就想把我对 PM Core 和这套分层框架的理解完整梳理一遍特别是那些在代码里不太显眼、但实际排查问题特别关键的部分。先说分层到底解决了什么问题。功耗管理涉及的面太宽了CPU 要调频调压外设要进入低功耗模式总线要挂起内存要自刷新系统级还要决定整个 SoC 睡到哪一级。如果所有逻辑都堆在驱动里或者都交给某一个子系统那基本上没法维护。内核的做法是拆成几个层次每层只关心自己的职责通过标准接口向上提供服务、向下操作硬件。从整体看这套分层大致是这样的最上层是用户空间通过 /sys/power/state、autosleep、systemd 等发起睡眠请求中间是 PM Core统筹 suspend/resume 流程维护设备列表管理唤醒源再往下是驱动层与总线层实现 dev_pm_ops各自处理设备的电源状态最底层是平台相关代码操作SoC的电源管理控制器、时钟、电源域真正把硬件切到低功耗模式。PM Core 就在中间这一层当“总调度”。它不关心某个具体传感器怎么睡眠但它决定这个传感器应该在什么时候、以什么顺序去执行睡眠。理解了这个调度逻辑再看驱动代码会有一种豁然开朗的感觉。2. PM Core 的职责边界与核心数据结构2.1 PM Core 到底管哪一段很多人把 PM Core 理解成“睡眠流程的代码”这个说法不太准确。PM Core 管的是流程的组织不是流程本身。它做的最核心的一件事情就是把系统级别的睡眠动作拆解成一系列有序的阶段然后协调所有注册进来的设备、总线和平台回调依次执行。具体来讲PM Core 包含这几块核心内容睡眠状态的统一抽象与管理设备列表的构建与遍历dpm_listsuspend/resume 回调的调用顺序控制唤醒源与 wakeup_count 的管理电源管理通知链PM notifier chain与 runtime PM 的衔接。也就是说驱动只需要告诉 PM Core“我在这个阶段要做什么”剩下的时序、并发、依赖关系都由 PM Core 统一处理。这有点像调度器之于进程你写业务代码时不用管 CPU 核怎么分配只管提交任务就行。眼尖的人会发现我这里用的是“分量挺重”的这几块每一块单拎出来都可以写一篇文章。而实际上PM Core 的代码集中在 kernel/power/ 目录下 main.c、suspend.c、suspend_test.c、wakeup_reason.c、wakeup.c 这些文件共同构成了它的骨架。2.2 数据结构把复杂的流程变成一张表PM Core 能实现对设备的统一管理靠的是两个关键数据结构一个是 dev_pm_ops描述设备在电源状态切换时的回调函数另一个是 dev_pm_info嵌入在 struct device 内部记录设备当前的电源状态和唤醒信息。先看 dev_pm_ops。这个结构体你可能见过很多次但真正值得留意的是它里面回调的语义划分struct dev_pm_ops { int (*prepare)(struct device *dev); void (*complete)(struct device *dev); int (*suspend)(struct device *dev); int (*resume)(struct device *dev); int (*freeze)(struct device *dev); int (*thaw)(struct device *dev); int (*poweroff)(struct device *dev); int (*restore)(struct device *dev); int (*suspend_late)(struct device *dev); int (*resume_early)(struct device *dev); int (*runtime_suspend)(struct device *dev); int (*runtime_resume)(struct device *dev); ... };这里有个容易混淆的点suspend 和 freeze、poweroff 都差不多为什么要有三套回调因为系统睡眠和休眠、关机对不同设备的期望状态不一样。睡眠时设备可能不需要完全断电休眠时可能要保存上下文并让内存自刷新关机时则要尽量把设备放在最低功耗状态。PM Core 会根据不同的睡眠目标调用不同组合的回调而不是一套走到底。dev_pm_info 则是运行时信息的载体里面有几个字段非常关键flags记录电源状态标志位比如 DPM_FLAG_SUSPEND_VALID 表示设备支持系统睡眠usage_countruntime PM 的使用计数wakeup表示设备是否是一个有效的唤醒源timer用于 runtime PM 的自动休眠调度。这些字段的意思只有在看完整个 suspend 流程后才会真正理解。先说个最简单的想看一个设备能不能唤醒系统直接去 /sys/class/xxx/device/wakeup/ 目录下面看 enable 文件这个就是 wakeup 字段导出的用户视图。2.3 dpm_list设备顺序的艺术PM Core 内部维护了一个全局链表 dpm_list所有注册到内核的设备都会挂在这个链表上。每次系统 suspend 时PM Core 按 dpm_list 的顺序依次调用每个设备的 suspend 回调resume 时则反过来按逆序调用。为什么要强调顺序因为设备之间存在依赖关系。一个简单的例子触摸屏控制器I2C设备依赖 I2C 控制器本身如果先让 I2C 控制器睡眠了触摸屏这边再去写寄存器就写不进去了甚至会触发总线错误。反过来resume 的时候如果先唤醒外设再唤醒总线那外设一上来就要访问还没恢复的总线同样会出问题。dpm_list 的顺序由设备注册的顺序决定但这个“顺序”不是绝对的。内核提供了一些机制来修正依赖比如 device_links设备互联功能可以让 PM Core 在遍历 dpm_list 时考虑设备之间的 producer/consumer 关系。这也是我从代码里读出来的一个重要信息现代内核更推荐用 device_links 来显式声明依赖而不是靠注册顺序碰运气。实操建议在你的驱动里如果设备之间有明确的先后关系比如电源管理芯片要先于消费者设备恢复不要依赖注册顺序用 device_links 或直接检查总线状态。否则同一份代码换一个平台可能就出问题。3. 从用户态到硬件一次 suspend 的完整旅程3.1 用户空间怎么发起一次睡眠在嵌入式设备上最常见的触发方式是写 /sys/power/state:echo mem /sys/power/state这个 mem 不是内存的意思而是指 suspend-to-RAM对应内核里的 PM_SUSPEND_MEM 状态。在 sysfs 的 show 回调里你会看到当前系统支持的睡眠状态列表一般包括 freeze、standby、mem。Freeze 对应 suspend-to-idle即 CPU 进入 idle loop但整个系统并不真正断电内存保持活跃只是把用户空间冻结了。这些状态的定义在 include/linux/suspend.h 里是一个枚举类型typedef int __bitwise suspend_state_t; #define PM_SUSPEND_ON ((__force suspend_state_t) 0) #define PM_SUSPEND_TO_IDLE ((__force suspend_state_t) 1) #define PM_SUSPEND_STANDBY ((__force suspend_state_t) 2) #define PM_SUSPEND_MEM ((__force suspend_state_t) 3) #define PM_SUSPEND_MAX ((__force suspend_state_t) 4)注意standby 和 mem 是平台相关的ARM 平台上这两种状态的具体实现差异可能很大有的 SoC 上 standby 实际就是打个断点级别的浅睡有的 SoC 甚至直接复用 mem 的路径。做移植时一定要看平台代码里的 suspend_ops 实现别只看用户空间的配置。在较新的系统上systemd 通常通过 logind 来发起睡眠它内部还是会走到写 /sys/power/state 这一步只不过在写之前会先处理一些用户态业务比如锁屏、通知应用。如果你的目标是排查“为什么系统睡不下去”建议先绕过 systemd直接手动 echo 到 sysfs可以排除很多干扰项。3.2 PM Core 接单以后发生了什么一旦用户空间写入 /sys/power/state内核就会调用到 kernel/power/suspend.c 里的 enter_state() 函数。这个函数做事情非常有条理检查当前状态是否合法调用 suspend_prepare冻结进程、挂起非引导 CPU调用 suspend_devices_and_enter核心睡眠流程恢复阶段如果睡眠过程中发生唤醒则按逆序恢复设备和进程。其中 suspend_devices_and_enter 是整套流程的灵魂。它会先调用 dpm_suspend_start()也就是对 dpm_list 里的设备做“准备”阶段prepare 回调然后进入 dpm_suspend()真正把每个设备 suspend。这一块给驱动开发者的提醒是你的驱动在 suspend 回调里应该做哪些事很多人一上来就在 suspend 里关时钟、关电源这其实不恰当。标准做法是prepare 阶段挂起设备可能产生的异步事务suspend 阶段停止设备活动保存必要的上下文suspend_late 阶段关闭中断、停用 DMA把设备放到最深的省电状态。对应的resume 路径就反过来走 resume_early、resume、complete。这里有个非常容易踩的坑如果设备处于 runtime PM 的 suspended 状态系统 suspend 时 PM Core 不会重复调用它的 suspend 回调而是根据 dev_pm_info 里的状态直接跳过。这就可能导致你原本期望在 suspend 时保存的寄存器上下文没有保存。经验之谈如果设备在 runtime suspend 后出现过寄存器丢失或者状态错乱先检查两件事——第一runtime_suspend 和系统 suspend 的保存操作是否共用一套函数第二是否在系统 suspend 前强制 resume 了设备。很多平台的解决方法是在 prepare 回调里直接调用 pm_runtime_resume()让设备回到 active 状态再走统一的 suspend 路径。3.3 平台层与 CPU 的最后一跳设备全部 suspend 之后PM Core 会调用 platform_suspend_ops 里的 suspend_ops-enter()。到这个时候系统只剩 CPU 还在跑但所有外部设备都已经睡了。平台相关的代码会根据目标睡眠状态配置 SoC 的电源管理单元最终执行 wfi/wfe 指令让 CPU 进入等待状态。唤醒源一旦触发CPU 会从中断入口醒来再经过底层汇编代码恢复执行。这个过程里有几个细节对后人排查问题非常有用ARM 平台上CPU 醒来后要恢复 MMU 和缓存状态所以平台代码里通常会有一段专门的汇编代码来恢复 CPU 上下文这就是 sleep.S 文件存在的原因从睡眠中唤醒时中断控制器GIC的配置可能与睡眠前不一样所以 platform 代码里要负责重新初始化 GIC如果系统醒来后 USB、网络等设备不工作多半是平台层 resume 时没有正确恢复时钟或电源域。我自己的经验是梳理这个流程最好的办法不是一行行读代码而是打开 ftrace把 suspend/resume 的设备调用顺序拉出来看一遍。内核里有一个专门的事件点suspend_resume可以直接用 tracefs 来捕获完整的设备调用序列。# 开启 suspend/resume 跟踪 echo 1 /sys/kernel/tracing/events/suspend_resume/enable # 发起一次睡眠 echo mem /sys/power/state # 查看跟踪结果 cat /sys/kernel/tracing/trace这个输出里能看到每个设备是几点几分被 suspend 的花了多长时间以及有没有报错。效率比盲改驱动高太多。4. 唤醒逻辑系统凭什么能被叫醒4.1 wakeup source 与 wakeup_count设备睡了、CPU 也睡了系统还能醒来靠的是中断和唤醒源机制。这里的“唤醒源”不完全是“设备有中断”而是要经过 PM Core 记账的。任何一个设备要想阻止系统进入睡眠或者把一个已经睡下去的系统叫起来它的驱动必须调用 pm_wakeup_event() 或者通过 enable_irq_wake() 来使能对应的唤醒中断。PM Core 维护了一个全局的 wakeup_count每次系统睡眠前PM Core 会比较当前 wakeup_count 和睡眠前的值是否一致。如果不一致说明在睡眠过程中发生了新的唤醒事件那这次睡眠就被取消重新进入正常流程。这个机制主要是为了应对“一睡就被唤醒”的恶性循环。我调试过一台设备现象是每次 echo mem 后不到 3 秒就被唤醒dmesg 里也没有明显报错。最后通过读取 wakeup_count 对应的 sysfs 节点发现是触摸屏的中断一直有触发——屏虽然关了但触摸芯片的供电没断一点干扰就产生一个中断事件。解决办法是给触摸屏驱动加上 runtime PM在系统 suspend 前就把芯片放入深度睡眠模式中断不再乱触发。这类问题在嵌入式设备上太常见了。建议排查时先做好这一步cat /sys/kernel/debug/wakeup_sources这个文件会列出所有注册的唤醒源及其触发次数。如果某个设备一直非零把它找出来重点查。4.2 PM notifier 与进程级通知设备唤醒只是硬件层面的操作系统还需要通知软件栈“系统状态正在变化”。PM Core 里有一套 notifier chain驱动、子系统可以往里注册回调。当系统即将 suspend 或者刚刚 resume 时PM Core 会遍历链上所有回调把状态变化广播给各方。这个机制在处理一些特殊外设时特别有用。比如 GPS 芯片它可能在睡眠期间需要维持一个独立的电源供应。这个需求不能写在普通的 suspend 回调里因为 suspend 回调本身可能没被调用而是要通过 pm_notifier 提前申请“保持供电”。有的 PMIC 驱动也是靠这个通知来确定自己要不要进入低功耗模式的。注册方法很简单static int my_notifier_call(struct notifier_block *nb, unsigned long event, void *data) { switch (event) { case PM_SUSPEND_PREPARE: /* 睡眠前要做的事 */ break; case PM_POST_SUSPEND: /* 醒来后要做的事 */ break; } return NOTIFY_DONE; } static struct notifier_block my_nb { .notifier_call my_notifier_call, }; register_pm_notifier(my_nb);注意notifier 回调执行的时候系统可能已经冻结了大部分进程所以在回调里不能做可能睡眠的操作不能用 mutex要小心使用锁。如果在这个回调里卡住了整个睡眠流程都会被堵死。4.3 eventfd 与进程级唤醒现代 Linux 内核在功耗管理和事件通知上还有一个容易被忽略的点eventfd。它虽然不是一个专门的电源管理机制但在很多驱动中扮演着“从内核态唤醒用户态”的角色。拿 GPIO 按键来说按键中断唤醒系统后驱动要通知用户空间的进程“按键事件来了”而这个通知的载体往往是 eventfd。这类机制在 low-level 驱动里很常见内核线程在等待事件时调用 eventfd_signal()用户空间进程在 epoll 上等待同一个 eventfd一旦信号到达进程被唤醒再通过 ioctl 或 read 去获取具体数据。它和 wakeup source 的关系是中断唤醒硬件eventfd 唤醒软件。热词里有 eventfd、唤醒机制这两者经常是配合使用的。新手可能困惑的问题是既然都有中断和标准等待队列了为什么还要 eventfd核心原因是 eventfd 可以跨进程、跨驱动并且能直接接到 epoll 上机制干净简洁。你可以理解为它是内核和用户态之间的一根“唤醒电话线”——不需要定义复杂的私有协议也不需要轮询。5. 踩坑实录我调试功耗问题时的常见套路5.1 设备无法进入睡眠谁挡住了 dpm_suspend一次完整的系统睡眠包含了很多阶段任何一个设备 suspend 回调返回错误都会导致整次睡眠失败。最常见的失败形式是echo mem 后命令直接报错dmesg 里出现 “Resource busy” 或者某个驱动直接打印错误。我的排查顺序一般是这样先看 dmesg 最后 20 行看是哪个设备卡住了如果信息不足把 suspend_resume trace 打开重新睡眠一次找到卡住的那个设备检查它的 suspend 回调是否在等待某个资源。这里还有一个很隐蔽的点如果设备驱动没有实现 suspend 回调而且它的 PM 标志位显示不支持系统睡眠PM Core 默认会使用总线层的 PM 回调。总线回调存在两种情况有的总线实现的是简单的关闭操作有的总线直接返回 -EINVAL。如果你在 I2C 总线上挂了个第三方传感器传感器的驱动没实现 PM 回调同时 I2C 总线驱动也不支持那整次睡眠就失败了。遇到这种问题第一步不是改驱动而是确认驱动注册时的 pm 指针是否有效。我曾经处理过一次 AirPlay 音箱项目的睡眠失败问题现象是明确报出“i2c-msx 设备 suspend 失败”但 i2c 驱动本身看起来没问题。最后发现是音频编解码芯片的 probe 阶段没有初始化 PM 回调导致它被挂在 dpm_list 上成为一个“无 PM 能力”的设备。解决方法是给编解码芯片驱动补上 dev_pm_ops或是在设备树里设置 power-domains让 PM Core 知道它的电源域归属。5.2 睡眠过深导致的唤醒失效有些平台为了省电把 mem 状态实现得很激进内存直接进入自刷新CPU 甚至要掉电重启。这个状态下如果能唤醒还好一旦唤醒源没有正确配置设备就只能通过长按电源键强制复位了因为系统已经睡死过去了。这种问题尤其容易出现在 GPIO 唤醒的配置上。很多 SoC 的 GPIO 唤醒不是简单的 enable_irq_wake() 就完事还需要在 PM 平台代码里单独配置唤醒寄存器。比如 Rockchip 的平台上GPIO 中断要额外写 GPIO_INT_EN 和 GPIO_WAKEUP_EN 才能唤醒NXP 的 i.MX 平台则需要在 GPC 里设置 IMR 寄存器。经验总结如果你发现一个设备在浅睡眠freeze状态下可以唤醒换成 mem 就醒不了大概率不是 dev_pm_ops 的问题而是平台层唤醒源配置的问题。这个方向要尽早确认不然在驱动层反复折腾很浪费时间。另外一个值得注意的地方是 Watchdog——如果系统里使能了硬件看门狗睡眠期间看门狗还在跑但 CPU 可能因状态太深无法及时喂狗醒来后直接遇到 reboot。这种情况要确保看门狗驱动注册了 PM 回调在 suspend 时关闭或暂停在 resume 后重新启动。5.3 功耗异常runtime PM 才是大头系统级别的睡眠大多数嵌入式设备都会做但日常运行时设备功耗异常的来源反而是 runtime PM 没有用好。很多外设在“看似空闲”的时候其实还在全速运行因为驱动开发者没有把 runtime PM 使能或者 runtime PM 的超时策略设置得过于保守。runtime PM 的机制很简单设备不看用户是否打开只看自身的使用计数usage_count和闲置时间idle timeout。当使用计数降到 0并且闲置时间超过阈值后PM Core 调用 runtime_suspend 回调让设备进入低功耗状态。这里有个容易踩的“经典坑”驱动在 open 时调了 pm_runtime_get_sync但 close 时忘了 pm_runtime_put导致使用计数永远不为零。这种现象的典型特征是设备在系统 idling 时功率偏高但设备本身也不报错。看起来很难查实际上用 ftrace 也能查更直接的是看一下 /sys/devices/.../power/runtime_status 这个节点。cat /sys/devices/platform/xxx/power/runtime_status如果你的设备在这里显示 active而你又不确定它是真的在工作把它的访问计数打出来cat /sys/devices/platform/xxx/power/runtime_usage如果这个值是 1但驱动里没有主动的 PM 调用那就赶紧去 code review 吧。5.4 调试工具速查表我常用的一套组合拳折腾功耗问题不是靠猜的我把平时排查的常用手段整理成了一张表方便后面查阅目标命令或节点说明查看当前系统支持的睡眠状态cat /sys/power/state对比不同平台可用状态发起系统睡眠echo mem /sys/power/state手动触发一次完整睡眠查看唤醒源状态cat /sys/kernel/debug/wakeup_sources查看各设备唤醒计数查看设备运行状态cat /sys/devices/platform/xxx/power/runtime_status确认设备是 active/suspended查看设备 PM 使用计数cat /sys/devices/platform/xxx/power/runtime_usage排查忘记 put 的驱动抓取 suspend/resume 时序echo 1 /sys/kernel/tracing/events/suspend_resume/enable精确看到每个设备的时长查看 dmesg 中的 PM 信息dmesg | grep -i -E PMsuspend系统睡眠统计cat /sys/kernel/debug/sleep_time统计睡眠时长分布如果要进一步定位设备 suspend/resume 的耗时可以使用echo 1 /sys/kernel/debug/pm_print_times打开后内核会在每次 suspend/resume 时打印出各设备的具体耗时。延迟超过预设阈值的设备会被红字标出这对优化“系统睡眠/唤醒速度”很有用——比如 Android 平板的亮屏唤醒速度优化就经常会用到这个开关。6. 从 PM Core 延伸出去还能玩什么看完上面这些内容你应该能感受到 PM Core 在整个功耗框架里的“调度枢纽”地位。最直接的理解是当所有驱动都在 dpm_list 上排队PM Core 就是那个拿着点名册的人。它不一定知道每个设备具体怎么省电但它一定知道什么时候叫谁、按什么顺序叫叫不动的时候该怎么处理。但功耗管理远远不止 PM Core 这一层。顺着这个框架往上有 cpuidle 和 cpufreq管理 CPU 自身的闲忙往周边有 devfreq管理设备动态调频再往深了走还有 power domain电源域与 PMIC 驱动协同负责在设备级别切断整个域的供电。这一系列配合起来才能完整覆盖“CPU 层面省电”和“SoC 层面省电”两条路。我个人在实际项目里最大的体会是如果你是一个做驱动开发的工程师不用急着把 PM Core 里每个函数都看透但一定要把它拉着各模块协作的那些接口搞清楚。比如 dev_pm_ops 里的回调什么时候会被调用dpm_list 的排序对设备恢复顺序有什么影响wakeup source 的机制又是怎样影响睡眠成功的。把这些问题弄明白你在排查设备功耗异常时就不仅仅是照着文档改参数了而是能直接从系统的角度判断出问题到底出在哪一层。这套能力比背会一百个 sysfs 节点都值钱。最后再分享一个小技巧给新的开发板做功耗适配时第一件事不是在驱动里写 PM 回调而是先在干净的系统上跑一遍 suspend/resume 全流程确认平台层的 ops 能够正常把系统睡下去再叫醒。然后从一个最简单的 GPIO 唤醒源开始一点点把外设加回来。每加一个设备就观察一次功耗和唤醒是否正常。这样排查出来的问题大概率就是那个设备自己的问题不会因为多设备并发干扰而绕弯路。