
做过嵌入式或者 Android BSP 的同学大概率都有过类似经历产品经理拿着一台功耗测试仪丢下一句“灭屏后整机平均电流比竞品多了 5mA你查一下”然后转身离开。你很不服气但最后还是打开内核源码盯着 suspend/resume 的调用栈看了一下午。如果你正处于这个状态那恭喜你你已经摸到了 Linux 内核功耗子系统最核心的地带——PM Core。这篇是系列文章的第一篇我打算把“PM Core”这个听起来很玄乎的名字拆开讲清楚它到底在功耗框架里扮演什么角色。我尽量不去贴大段大段的代码注释而是从“为什么这套框架要这样设计”的角度入手带着你从 dev_pm_ops 看到 dpm_suspend再看到 wakeup_source。读完你应该能建立起一张完整的脑内地图Linux 内核的功耗管理哪些事情归 PM Core 管哪些事情其实不归它管以及你在驱动里写的那些 callback 到底是在哪一环被调用的。1. PM Core 在功耗管理版图里的准确坐标1.1 功耗管理的“三层楼”硬件特性、驱动回调、框架编排刚开始接触内核电源管理的人很容易把“功耗子系统”理解成一个巨大的、无所不包的黑盒CPUFreq、CPUIdle、Regulator、Clock、设备 Runtime PM、suspend/resume、hibernate……好像全都搅在一起。实际上Linux 内核的功耗管理是有清晰层次的我习惯把它分成三层来看。最底层是硬件特性层。这一层关心的是芯片本身能提供什么CPU 有多少个 idle stateDDR 能不能进入 self-refreshGPU 有没有 retention 模式外设寄存器能不能在时钟关断后保持上下文。这些能力由硬件 IP 决定内核能做的只是“发现”并“管理”它们对应到代码里就是 cpuidle driver、cpufreq driver、clock framework、regulator framework 这些模块。中间层是设备驱动层。每一个设备驱动都需要回答一个问题我的设备在系统睡眠前要把哪些寄存器保存下来在唤醒后要重新初始化哪些逻辑这些回答通过填充struct dev_pm_ops里的回调函数来实现。这是驱动开发者接触最多的地方。最上层就是框架编排层。也就是本文要说的 PM Core。它不关心某个具体设备的寄存器细节它只关心两件事一是“整个系统现在应该处于什么功耗状态”二是“在切换这个状态时怎么有序地通知到每一个驱动、每一个子系统”。这三层的关系很像一个剧场。演员驱动知道自己要演什么舞台设备硬件也知道自己能实现什么效果但如果没有导演PM Core拿着剧本一页一页地喊“该谁上场、该谁退场”整场戏肯定是乱套的。1.2 PM Core 的三大职责与目录对应既然叫“Core”它的职责实际上非常收敛。从代码结构上可以看得很清楚PM Core 不是某个单一文件而是分布在三个目录下的协同体系目录/文件职责典型内容include/linux/pm.h定义框架的“契约”dev_pm_ops、pm_message_t、pm_event等数据结构和宏kernel/power/全局状态机管理suspend/resume/hibernate 的入口流程、状态转换、wakeup_count 协议drivers/base/power/设备层面的 PM 机制dpm_list 管理、runtime PM 框架、wakeup source 管理、sysfs 接口第一块负责“定规矩”第二块负责“管全局”第三块负责“落实到设备”。三者合起来才是完整意义上的 PM Core。很多人读代码时只盯着kernel/power/里的main.c其实真正和设备驱动天天打交道的逻辑都在drivers/base/power/下面。1.3 什么时候你会需要关注 PM Core坦率地说如果你只是写一个普通的字符设备驱动不关心功耗你可能几年都碰不到 PM Core 的代码。但下面这几类场景你就必须把 PM Core 的机制弄明白系统休眠唤醒后某个设备工作不正常怀疑是恢复顺序或保存/恢复不完整灭屏后整机电流降不下去怀疑有设备没进入低功耗模式或者被某个 wakeup source 反复唤醒你需要在设备驱动里正确注册 runtime PM 回调并理解pm_runtime_get_sync和pm_runtime_put_sync背后的引用计数机制做 Android BSP需要调试wake_lock和autosleep的行为追踪系统为什么迟迟不进入 suspend。如果你中了其中任何一条请继续往下看。这一篇把“分层设计”的骨架搭起来后面几篇我们再逐个功能模块深入进去。2. 分层设计的灵魂在 include/linux/pm.h 那几百行2.1 dev_pm_ops设备与功耗框架之间的“合同”打开include/linux/pm.h第一眼会看到struct 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 (*freeze_late)(struct device *dev); int (*thaw_early)(struct device *dev); int (*poweroff_late)(struct device *dev); int (*restore_early)(struct device *dev); int (*suspend_noirq)(struct device *dev); int (*resume_noirq)(struct device *dev); int (*freeze_noirq)(struct device *dev); int (*thaw_noirq)(struct device *dev); int (*poweroff_noirq)(struct device *dev); int (*restore_noirq)(struct device *dev); int (*runtime_suspend)(struct device *dev); int (*runtime_resume)(struct device *dev); int (*runtime_idle)(struct device *dev); };第一次看到这么多回调很多人会头皮发麻。但注意看其实只有三大组系统睡眠相关suspend/resume、休眠镜像相关freeze/thaw、poweroff/restore、运行时电源管理相关runtime_*。每一组内部又按“时机”拆成了四个阶段普通阶段、late 阶段、noirq 阶段、以及挂在最前面的 prepare/complete。这里我想强调一个容易被忽略的设计点这些回调都是可选的。如果你的设备在系统睡眠时不需要做任何事你就什么都不用填框架在遍历设备时发现回调为空就直接跳过。这点非常重要因为很多老驱动其实根本没有 PM 概念它们的内核版本也比较老上面没有.suspend.resume回调。PM Core 要保证老驱动在引入新框架后依然能正常工作所以“回调缺省为空”是最基础、最关键的兼容性设计。2.2 为什么是 PM_EVENT_* 而不是一个布尔值在非常早期的 Linux 内核里电源管理回调是非常朴素的只有简单的suspend/resume两个函数宣传上就一个标志位。后来为什么演化出了pm_message_t和PM_EVENT_*答案是系统挂起不止一种原因。suspend to RAM挂起到内存系统保持内存供电把 CPU 和大部分外设关掉唤醒后恢复的速度比较快suspend to diskhibernate挂起到磁盘把内存镜像写到磁盘然后整机断电唤醒时需要从磁盘恢复镜像freeze冻结不进入真正低功耗状态只是把进程和设备冻结住用于创建系统镜像等场景。同样是“让设备停下来”三种场景下设备需要做的事差别很大。比如 hibernate 前设备必须把 DMA 完全停掉保证内存镜像一致性而 freeze 场景下设备往往只需要把中断和任务停掉即可。如果只靠一个布尔标志位驱动根本分不清自己到底处在哪个流程里。pm_message_t和PM_EVENT_*的存在让框架可以向设备回调传递“事件类型”驱动也可以根据事件类型走不同的处理逻辑。另外注意历史上pm_message_t曾经还承载过 event 之外的信息但现在它的核心作用就是传递事件类型。typedef struct pm_message { int event; } pm_message_t;常用的event值定义在同一个头文件里PM_EVENT_SUSPEND、PM_EVENT_RESUME、PM_EVENT_FREEZE、PM_EVENT_THAW、PM_EVENT_POWEROFF、PM_EVENT_RESTORE等。在回调里你可以这样判断static int demo_suspend(struct device *dev, pm_message_t state) { if (state.event PM_EVENT_SUSPEND) dev_dbg(dev, suspend to RAM\n); else if (state.event PM_EVENT_FREEZE) dev_dbg(dev, freeze for image\n); return 0; }不过在实战中大多数驱动不需要区分这么细因为 PM Core 在调用具体回调前已经把事件类型“翻译”成了对应的回调函数suspend 流程调.suspendfreeze 流程调.freeze驱动只有在想兼顾两类场景时才会去检查pm_message_t.event。2.3 回调分成四段prepare/suspend/late/noirq 的工程意义很多驱动开发者第一次看到.suspend、.suspend_late、.suspend_noirq时是懵的这些不都是让设备睡觉吗为什么要分三四个阶段这是理解功耗框架分层设计的一个绝佳切入口。系统睡眠是一个非常敏感的全局操作。它像一支多支部队协同撤退有人要先撤有人要后撤有人要在别人撤完后再负责放哨。如果一个驱动在中断仍然正常派发的时候做复杂操作它可以用普通.suspend如果一个驱动希望在绝大部分系统机制还在运转时完成收尾它可以用.suspend_late而一旦系统进入了“中断禁用”阶段只剩.suspend_noirq能提供极其有限的操作窗口。反过来恢复流程就是反过来的顺序resume_noirq→resume_early→resume→complete。我用自己的理解做个类比你在公司加班到很晚准备锁门走人。.prepare相当于你先把桌面收拾干净、把重要文件备份告诉保安“我快走了”.suspend相当于你关掉办公室的灯和大功率设备.suspend_late相当于你把门锁上但还在楼道里.suspend_noirq相当于你走出大楼把门禁系统也关掉——此时已经没有任何人能响应门铃了。这种分阶段设计最大的工程价值是让“对中断敏感”和“对时序敏感”的设备都能找到自己合适的操作窗口。比如 GPIO 控制器、中断控制器这类设备必须等到几乎所有设备都 suspend 之后再 sleep因为它们还要为别人服务对应的它们必须最早醒来。对于普通驱动我的建议是能用.suspend/.resume解决的绝不要往.suspend_late/.resume_early里塞逻辑。里面能做的事情少出错的代价大真需要在这个阶段跑代码的设备通常都是平台级设备普通外设驱动没有理由去凑热闹。3. 从 /sys/power/state 到 CPU 睡死一次整机睡眠的完整旅程3.1 suspend_state_t 与用户态接口的对应关系用户态触发系统挂起最常用的方法就是往/sys/power/state里写字符串echo mem /sys/power/state这个“mem”在内核里对应的是PM_SUSPEND_MEM。内核里定义了几个典型的挂起状态状态含义低功耗程度典型场景PM_SUSPEND_ON正常工作无默认PM_SUSPEND_STANDBY待机低通过空闲 CPU快速唤醒PM_SUSPEND_MEM挂起到内存高内存自刷新手机灭屏待机PM_SUSPEND_DISK挂起到磁盘极高几乎断电笔记本合盖休眠注意/sys/power/state支持哪些字符串取决于平台的suspend_ops和内核配置。有时候你写入standby会返回EINVAL那说明平台实际上没有实现这个状态。这个细节经常被测试人员当成 bug 报上来其实是预期行为。写一个自定义字符串进/sys/power/state后调用链大致是write() → state_store() → pm_suspend(state)state_store()位于kernel/power/main.c。它会先做一次合法性检查然后进入全局挂起流程。这里有个很容易被新手忽略的点pm_suspend()只有在系统没有正在挂起、也没有被其他机制阻止时才会真正执行。比如你在一个已经处于 suspend 状态的系统上再写一次mem大概率会得到一个错误。3.2 enter_state() 里最重要的握手wakeup_count 协议从/sys/power/state到设备回调真正被执行中间还有一个非常关键的保护机制叫做wakeup_count 协议。这个协议的背景是用户态写入“让我睡觉”的瞬间可能正好有人按了电源键或者有网络唤醒包到达。如果系统直接睡过去这次唤醒事件会丢失或者系统睡下去之后立刻又被唤醒造成资源浪费。为了避免这种竞态内核设计了“写之前先确认”的握手流程。标准操作是# 1. 读取当前 wakeup events 计数 cat /sys/power/wakeup_count 1024 # 2. 将该计数写回表示“基于这个时间点开始挂起” echo 1024 /sys/power/wakeup_count # 3. 如果写回成功再执行挂起 echo mem /sys/power/state如果第 2 步写回时内核发现 wakeup event 计数已经变了写回会失败返回错误。此时挂在用户态的脚本就知道“有新的唤醒事件发生”于是中止挂起流程。进入enter_state()后整个流程可以概括为enter_state() ├── 通知 PM_SUSPEND_PREPARE ├── suspend_devices_and_enter() │ ├── dpm_suspend() /* 普通设备挂起 */ │ ├── suspend_enter() /* 平台相关操作调用 suspend_ops-enter() */ │ ├── dpm_resume() /* 设备恢复 */ └── 通知 PM_POST_SUSPEND这里可以看到PM Core 把流程切成了“设备挂起”和“平台进入”两个环节。设备挂起是可移植的、与 SoC 无关的部分而真正让 CPU 进入 deep idle、让 DDR 自刷新这些硬件操作则委托给平台相关的suspend_ops。这种“平台无关 平台相关”的拆分就是分层设计在全局状态机上的体现。3.3 dpm_suspend() 的设备排序逻辑与经典坑dpm_suspend()在drivers/base/power/main.c里它做的事情本质上是遍历一个全局设备链表逐一调用每个设备的 suspend 回调。但链表顺序不是随机的它由设备的注册顺序、依赖关系和异步标志共同决定。这里有一个关键点被挂起的顺序和恢复的顺序是相反的。先挂起的设备后恢复后挂起的设备先恢复。这个规则让设备之间的依赖关系能自然满足——比如一个 MIPI 接口的传感器依赖 I2C 控制器那么 I2C 控制器应该后挂起、先恢复这样传感器在恢复时总能找到可用的 I2C 总线。真正让很多人掉坑里的是下面几种情况第一-EBUSY不代表 PM Core 出错。如果你的设备在.suspend或.prepare回调里返回-EBUSY内核会认为此刻系统不该挂起从而中止整个流程。很多驱动在“忙”的时候不该睡这个设计是刻意的但新手容易以为返回错误就是代码写错了。第二异步挂起让打印顺序乱了。内核默认可以对带DPM_FLAG_SMART_SUSPEND或异步能力的设备进行并行挂起。并行之后你看到驱动里的dev_info打印顺序和链表顺序不一致这是正常的不代表回调乱序。第三其实也是最重要的电源设备必须最后挂起、最先恢复。这不是命令而是插入链表的顺序决定的设备在注册时会被插入到 dpm_list 的合适位置power domain / regulator / clock 这类电源相关的设备通常在更晚的位置。如果你的驱动在挂起后还需要向外部设备操作但它的供电设备已经没了这就是典型的排序 bug。内核为此也提供了各种 device PM domain 机制来解决依赖问题相关内容后面专门写一篇。4. 系统级的“装睡”与“叫醒”Wakeup Source 与 Runtime PM4.1 wakeup_source让深度睡眠的设备还能对外界作出响应如果一个系统进入mem挂起CPU 停掉外设大多断电那它凭什么能被叫醒答案是某些设备仍然保持着“最低限度的检测能力”它们被称为wakeup source唤醒源。用大白话说系统中的唤醒源相当于值班室里的电话。整个公司都下班了但值班室的电话必须通着电一旦有人打进来就得把人叫回来处理。在驱动的标准做法里设备要成为唤醒源需要两步/* 1. 在 probe 里初始化 wakeup 能力 */ device_init_wakeup(dev, true); /* 2. 在需要时“持有”唤醒锁 */ __pm_stay_awake(wakeup_source); /* 3. 事情处理完后“释放”唤醒锁 */ __pm_relax(wakeup_source);wakeup_source在内核里是一个计数 标志的机制。只要还有 wakeup source 处于“active”状态系统就不允许进入 suspend或者一旦尝试进入就会被立刻拉起来。这就是为什么 Android 上wake_lock能防止系统睡着——它的底层实现就是标准的 wakeup_source。设备可以在系统已经进入 suspend 之后产生唤醒事件此时它通过中断唤醒 CPU并在 resume 流程中被识别。所以如果你排查“系统总是无法进入休眠”的问题第一件事就是看看/sys/kernel/debug/wakeup_sources里哪些 source 还处于 active 状态。这个文件列出了每个 wakeup source 的名字、活跃计数、上次活跃时间是定位功耗问题最直接的入口之一。4.2 runtime PM设备级功耗管理与 PM Core 的关系很多人会有个困惑runtime PM运行时电源管理到底算不算 PM Core 的一部分答案是算但它是独立于系统级 suspend/resume 的一套机制。系统级挂起是“所有设备一起睡”runtime PM 是“单个设备自己睡”。两者通过同一个dev_pm_ops里的回调来工作但触发方式和状态管理完全不同。runtime PM 的核心思想是一个设备在没人使用的时候可以自行进入低功耗状态一旦有人要用比如open()、发起传输就把它唤醒。内核提供了一组标准 APIpm_runtime_get_sync(dev); /* 使用前增加引用计数并唤醒 */ pm_runtime_put_sync(dev); /* 使用完减少引用计数并允许睡眠 */ pm_runtime_allow(dev); /* 启用 runtime PM */ pm_runtime_forbid(dev); /* 禁用 runtime PM */引用计数归零后框架会自动调用dev_pm_ops里的.runtime_suspend回调计数从零变正时调用.runtime_resume。这个机制和系统级 suspend 的交互关系很有讲究。设备可能正处于 runtime suspended 状态此时系统进入全局挂起PM Core 就不会再调用它的.suspend因为它已经睡了。同样系统恢复时runtime suspended 的设备也不会被.resume叫醒它会继续保持自己的低功耗状态直到有用户来用。这个设计保证了“系统挂起”和“设备运行状态”互不干扰也让驱动的逻辑能保持简单。不过它也是一把双刃剑如果你在.runtime_resume里做了很重的初始化系统级 resume 时相关的设备突然需要 IO你就得面对“设备在 runtime suspend 时被系统级唤醒”的复杂时序。4.3 autosleep 与 wake_lock移动设备待机的隐藏功臣如果你做过 Android 或者智能手表类产品你一定见过wake_lock唤醒锁和autosleep自动睡眠的概念。它们在用户态表现为/sys/power/wake_lock和/sys/power/autosleep。而在内核里它们都是建立在 PM Core 的 wakeup source 机制之上的。autosleep的思路很聪明系统不主动睡眠而是“没有活跃 wakeup source 的时候就自动挂起”。它由一个内核线程持续监控 wakeup source 的活跃状态一旦发现所有 source 都处于 inactive就自动执行 suspend如果睡眠过程中出现新的 wakeup 事件系统被唤醒后继续监控。Android 的wake_lock本质上就是把用户态的名字注册成一个 wakeup source。App 持锁时source 是 active 的系统不睡App 释放锁后source 变 inactiveautosleep 线程就会找机会让系统进入 suspend。这套配合把“应用行为”和“内核休眠决策”优雅地解耦了也是 PM Core 分层设计在用户态影响最大的一处。5. 实战代码里怎么追踪 PM Core 的行为5.1 /sys/power 下的调试接口清单纸上谈兵到此结束。下面列出我平时排查功耗问题时会用的调试接口大部分都在/sys/power/下面接口用途/sys/power/state查看/触发挂起状态/sys/power/wakeup_count唤醒计数握手协议/sys/power/wake_lock用户态创建命名 wakeup source/sys/power/wake_unlock释放用户态 wakeup source/sys/power/autosleep启用/禁用自动睡眠/sys/kernel/debug/wakeup_sources查看所有 wakeup source 状态/sys/devices/.../power/control查看/控制设备 runtime PM 策略/sys/devices/.../power/wakeup查看设备是否能用做唤醒源使用/sys/kernel/debug/wakeup_sources是定位“系统睡不着”问题的第一步。你会看到类似这样的输出name active_count active_since total_time max_time event0 0 0 0 0 gpio-keys 0 0 0 0 pm8921-rtc 0 0 0 0大部分时候问题就出在一个你没想到的设备上active_count一直不为零。看到的那一刻你就知道该去查哪个驱动了。5.2 用 ftrace 的 suspend_resume 事件观测设备回调只想在 shell 层面看设备回调顺序又不想改代码加打印那ftrace的suspend_resume事件组就是最好的工具。用法很简单# 挂载 tracefs mount -t tracefs tracefs /sys/kernel/tracing # 打开 suspend/resume 相关事件 echo 1 /sys/kernel/tracing/events/suspend_resume/enable # 开始记录 echo 1 /sys/kernel/tracing/tracing_on # 触发一次挂起 echo mem /sys/power/state # 停止记录并查看 echo 0 /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace你会在输出里看到类似下面的片段migration/0-11 [000] d... 123.456789: suspend_resume: suspend_start kthreadd-2 [000] d... 123.456801: suspend_resume: dpm_suspend_start kworker/0:1-5 [000] d... 123.456810: suspend_resume: device_suspend: platform kworker/0:1-5 [000] d... 123.456823: suspend_resume: device_suspend: I2C0 ...这里最实用的信息是device_suspend事件旁边的设备名列表它直接展示了 dpm 链表的遍历顺序。如果某个设备长时间卡住你也会在时间戳里看出异常——相邻两个设备之间隔了几百毫秒甚至更久说明前一个设备的 suspend 回调做了太多不可控的事情。5.3 从零写一个带 dev_pm_ops 的驱动来验证休眠时序理解 PM Core 最快的方法是自己写一个带有 PM 回调的驱动然后看着它的打印在休眠流程里出现。下面给一个最小化的例子注册一个平台驱动在挂起/恢复时打印日志#include linux/module.h #include linux/platform_device.h #include linux/pm.h static int demo_suspend(struct device *dev) { dev_info(dev, suspend callback called\n); return 0; } static int demo_resume(struct device *dev) { dev_info(dev, resume callback called\n); return 0; } static const struct dev_pm_ops demo_pm_ops { .suspend demo_suspend, .resume demo_resume, .suspend_late demo_suspend, .resume_early demo_resume, }; static struct platform_driver demo_driver { .driver { .name demo_pm, .pm demo_pm_ops, }, }; static int __init demo_init(void) { return platform_driver_register(demo_driver); } static void __exit demo_exit(void) { platform_driver_unregister(demo_driver); } module_init(demo_init); module_exit(demo_exit); MODULE_LICENSE(GPL);如果你的平台上有设备树节点匹配demo_pm加载模块后触发一次echo mem /sys/power/state你就能在dmesg里看到suspend callback called在哪个阶段出现。这个实验的最大价值是让你直观感受到“PM Core 遍历设备”这件事本身——你的驱动只是茫茫设备海中的一个。注意我故意同时注册了.suspend和.suspend_late你会在日志里看到它们按阶段分别触发。如果只注册.suspend内核也会默默跳过 late 阶段——这是“回调可选”设计最直接的验证。5.4 调试中容易被忽略的三件事第一件在 suspend/resume 回调里千万不要长时间自旋。这些回调运行在系统挂起/恢复的关键路径上耗时越长整机休眠/唤醒就越慢。如果回调里有 200ms 的延时产品经理立刻就会在功耗报告里发现“唤醒太慢”。把耗时操作放到.prepare里做或者干脆用异步机制去处理。第二件注意dev_info在 noirq 阶段的可用性。.suspend_noirq阶段中断已经被禁用打印可能不可靠。内核提供了一些特殊的打印函数但调试时最好避免在这个阶段做任何输出否则你可能会看到日志中途断掉误以为驱动崩溃。第三件唤醒源的中断标志不能乱设。很多设备想当唤醒源但它的中断没有被正确地注册为 wakeirq或者中断触发方式有问题。结果就是系统永远无法进入深度睡眠或者休眠后一旦有中断立刻唤醒但驱动已经来不及完整恢复。内核里有个机制叫dev_pm_set_wake_irq()专门把某个中断关联到设备的唤醒源管理上这个接口比自己在驱动里裸玩 IRQ 要安全得多。从 PM Core 的设计到/sys/power/state的一次挂起流程再到 wakeup source 和 runtime PM 的配合我希望你已经对 Linux 内核功耗子系统的骨架有了一个整体印象。这套分层设计真正的精妙之处在于它用严格的时序契约prepare/late/noirq承载了无数硬件平台的能力差异让“设备驱动”和“平台电源策略”可以独立演进——驱动只负责把自己管好平台负责把所有驱动兜起来。下一篇我会挑drivers/base/power/main.c里的dpm_suspend()展开专门聊聊设备链表排序背后的依赖规则和那些让人抓狂的边界情况。