1. 项目概述:为什么一个“功耗子系统”值得从 PM Core 开始深挖?
Linux 内核的功耗管理,从来不是给笔记本电脑加个“省电模式”那么简单。它是一套贯穿硬件抽象层、驱动模型、调度策略与用户空间接口的精密协同机制——而PM Core,就是这套机制的“中央调度室”。我第一次在 ARM64 平台调试一块 SoC 的待机电流异常时,发现系统在 suspend_to_mem 状态下功耗比预期高出整整 3 倍。排查路径不是从电源芯片手册开始,而是从drivers/base/power/main.c里一行pm_runtime_set_active()调用切入,最终定位到某个 USB PHY 驱动漏掉了pm_runtime_disable()。这件事让我彻底明白:功耗问题的本质,是状态管理的精确性问题;而 PM Core,就是所有设备功耗状态流转的唯一仲裁者与记录者。
你可能正在嵌入式设备上做低功耗优化,可能在服务器场景下压测 CPU idle C-state 深度,也可能在学习内核源码时被struct dev_pm_info里十几种 flag 绕晕。无论哪种情况,绕开 PM Core 直接看 cpuidle 或 runtime pm,就像没学过加减法就去解微分方程——能跑通,但永远不知道哪一步错了。标题里强调“从 PM Core 看分层设计”,不是为了讲教科书式的架构图,而是要还原一个真实场景:当用户执行echo mem > /sys/power/state,内核内部究竟发生了多少层调用、多少次状态校验、多少个回调函数被串行或并行触发?每一层封装了什么职责?又把哪些复杂性向上屏蔽、向下暴露?这些答案,全藏在kernel/power/和drivers/base/power/这两个目录的代码组织逻辑里。
这篇文章面向三类人:一是刚读完《Linux Device Drivers》想深入内核机制的开发者;二是正为某款工控板待机功耗超标焦头烂额的嵌入式工程师;三是准备 Linux 内核面试、被问到“设备 suspend 流程中 runtime pm 和 system pm 如何协作”的求职者。我不讲抽象概念,只拆真实代码路径、画状态流转草图、标出每个关键函数的调用栈深度和锁持有情况。你不需要背下所有结构体字段,但读完后应该能对着git blame drivers/base/power/main.c说出第 872 行dpm_suspend_start()为什么必须在dpm_list上加dpm_list_lock读锁,以及这个锁和dev->power.lock之间是什么嵌套关系。
提示:本文不涉及任何具体 SoC 的 PMIC 配置,也不讲解 ACPI _PS0/_PS3 方法的解析细节。所有分析严格基于上游主线内核 v6.6+ 的通用框架,代码路径以
CONFIG_PM=y为前提。如果你的内核配置里关掉了CONFIG_PM_RUNTIME,那 runtime pm 相关章节对你就是无效信息——这恰恰印证了分层设计的价值:可裁剪、可组合、不强耦合。
2. 分层设计全景图:PM Core 不是“一层”,而是四层契约的交汇点
很多人误以为“PM Core”是一个叫pm_core.o的独立模块,其实它根本不存在编译目标。它是一组约定俗成的接口集合、一套状态机定义、一个全局设备链表(dpm_list)和若干同步原语的统称。真正的分层,体现在四个相互咬合的契约层上,每一层都向上提供抽象,向下约束实现:
2.1 第一层:设备模型层(Device Model Layer)——所有功耗管理的“注册制”入口
这是最基础的一层,也是最容易被忽略的一层。struct device里那个struct dev_pm_info power字段,就是整个功耗子系统的锚点。但关键不在字段存在,而在谁负责初始化它。答案是:device_initialize()函数,在设备结构体分配内存后立即调用。它做了三件关键事:
- 初始化
power.lock自旋锁(注意:是 spinlock,不是 mutex,因为 runtime pm 的部分路径在中断上下文执行); - 将
power.runtime_status设为RPM_ACTIVE,power.disable_depth设为 0; - 调用
pm_runtime_init(),将power.runtime_usage计数器归零,并设置power.runtime_auto标志位。
这个初始化过程决定了:任何设备,只要挂到设备模型树上,就自动具备了 runtime pm 的基本能力。你不需要显式调用pm_runtime_enable(),除非你想禁用它(比如某些 legacy 设备)。我见过太多人写驱动时,在probe()里反复调用pm_runtime_enable(),结果导致power.disable_depth被错误递增,最终pm_runtime_get_sync()失败——根源就是没理解这一层的默认契约。
注意:
power.runtime_status的初始值RPM_ACTIVE是一个“乐观假设”,它假设设备刚上电时处于工作状态。但实际硬件可能需要几百毫秒才能稳定,所以probe()结尾必须调用pm_runtime_put_sync()来触发首次 idle,否则设备会一直被 runtime pm 认为“正在使用”。
2.2 第二层:设备电源管理核心层(DPM Core Layer)——system suspend/resume 的“总控台”
drivers/base/power/main.c是这一层的绝对核心。它维护着全局的dpm_list(按设备注册顺序排列的双向链表)和dpm_prepared_list(已 prepare 完毕的设备链表),并定义了dpm_suspend()、dpm_resume()、dpm_complete()等顶层函数。这里的关键设计是状态分离:dpm_suspend()只负责下发 suspend 请求,不处理设备自身的电源切换;dpm_resume()只负责下发 resume 请求,不恢复设备寄存器。真正的硬件操作,全部交给设备驱动的.suspend()和.resume()回调。
这种分离带来两个直接好处:一是dpm_suspend()可以在持有dpm_list_lock的情况下遍历整个链表,保证 suspend 流程的原子性;二是驱动开发者只需关注自己设备的硬件特性,无需了解其他设备的状态依赖。举个例子:当 SATA 控制器 suspend 时,它必须确保所有 attached 的硬盘设备已经完成 suspend,否则可能触发 DMA 错误。这个依赖关系不是由 DPM Core 管理,而是由struct device_driver的.pm字段里的->prepare()回调来显式声明——DPM Core 只负责按拓扑顺序调用prepare(),再按逆序调用suspend()。
实操中,我常通过cat /sys/firmware/acpi/hardware_signature查看当前 ACPI 表签名,再结合dmesg | grep "dpm", 确认 suspend 流程是否卡在某个设备的prepare()阶段。如果看到dpm_run_callback: failed to prepare device,那一定是该设备驱动的->prepare()返回了非零值,此时应检查其是否正确处理了dev->power.direct_complete标志(用于 direct-complete 优化路径)。
2.3 第三层:运行时电源管理层(Runtime PM Layer)——设备级“按需供电”的执行引擎
drivers/base/power/runtime.c实现了pm_runtime_get_sync()、pm_runtime_put_autosuspend()等核心 API。它的精妙之处在于双计数器 + 状态机设计:
power.runtime_usage:引用计数,每次get加 1,put减 1;power.runtime_active_time:累计设备处于 active 状态的 jiffies;power.runtime_status:当前状态(RPM_ACTIVE/RPM_SUSPENDING/RPM_SUSPENDED/RPM_RESUMING)。
状态转换不是简单的 if-else,而是有严格守则。例如,从RPM_ACTIVE到RPM_SUSPENDING,必须满足:
power.runtime_usage == 0(无活跃引用);power.disable_depth == 0(未被禁用);power.runtime_auto == true(启用 autosuspend);power.runtime_suspended_jiffies已超dev->power.autosuspend_delay(延迟时间满足)。
这个状态机被封装在rpm_idle()函数里,而rpm_idle()又被pm_runtime_idle()和pm_request_idle()间接调用。我在调试 USB 设备时发现,autosuspend_delay默认是 -1(禁用),必须在驱动 probe 里显式调用pm_runtime_set_autosuspend_delay(dev, 2000)才能启用 2 秒自动休眠。否则,即使power.runtime_usage归零,设备也永远不会进入RPM_SUSPENDED状态。
2.4 第四层:平台特定电源管理层(Platform PM Layer)——硬件差异的“翻译官”
arch/*/kernel/pm.c和drivers/soc/下的代码属于这一层。它不定义通用接口,而是实现arch_suspend_disable_irqs()、arch_resume_enable_irqs()等 arch hook,以及platform_suspend_ops结构体。以 ARM64 为例,arch_suspend_disable_irqs()会调用gic_cpu_if_down()关闭 GIC 接口,而platform_suspend_ops的.enter()回调则指向psci_cpu_suspend(),最终通过 PSCI(ARM Power State Coordination Interface)调用 firmware 进入底层低功耗状态。
这一层的关键价值是隔离硬件细节。同一套 DPM Core 代码,可以在 x86 上通过 ACPI S-states 进入 sleep,在 ARM64 上通过 PSCI 进入 standby,在 RISC-V 上通过 SBI(Supervisor Binary Interface)进入 wfi。用户空间执行echo mem > /sys/power/state时,enter_state()函数会根据valid_state()检查结果,选择调用hibernate_basic_enter()还是suspend_enter(),而后者又会调用platform_ops->enter()。整个链条里,PM Core 层完全不知道 PSCI 是什么,它只认platform_ops这个函数指针。
3. PM Core 核心数据结构与状态流转:一张图看懂 12 个关键字段
要真正理解 PM Core,必须亲手画出struct dev_pm_info的状态流转图。这不是为了死记硬背,而是为了在crash或kdump时,能从struct device的内存布局里快速定位问题。以下是我整理的 12 个最常被误用或误解的字段,按使用频率排序:
| 字段名 | 类型 | 典型值 | 关键作用 | 常见误用 |
|---|---|---|---|---|
runtime_status | enum rpm_status | RPM_ACTIVE, RPM_SUSPENDED | 设备当前 runtime 状态 | 在中断上下文直接修改,未加power.lock |
disable_depth | int | 0, 1, 2 | runtime pm 禁用深度,0 表示启用 | pm_runtime_disable()调用次数 >pm_runtime_enable(),导致永久禁用 |
runtime_usage | atomic_t | 0, 1, 2 | runtime 引用计数 | 在probe()里get后忘记put,导致设备无法 suspend |
runtime_auto | bool | true/false | 是否启用 autosuspend | 初始化后未显式设为 true,autosuspend 不生效 |
runtime_suspended_jiffies | unsigned long | jiffies 值 | 上次进入 suspended 的时间戳 | 与autosuspend_delay计算时未考虑 jiffies wraparound |
runtime_active_jiffies | unsigned long | jiffies 值 | 累计 active 时间 | 用于计算设备平均功耗,但常被忽略 |
runtime_last_busy | unsigned long | jiffies 值 | 上次被访问的时间戳 | rpm_idle()判断 idle 时间的基准 |
runtime_autosuspend | long | -1, 2000 | autosuspend 延迟毫秒数 | 设为负数表示禁用,但很多驱动设为 0 导致立即 suspend |
direct_complete | bool | true/false | 是否走 direct-complete 优化路径 | 在prepare()里设为 true 后,suspend()必须返回 0 |
is_prepared | bool | true/false | 是否已完成 prepare 阶段 | dpm_prepare()设置,dpm_suspend()清除,用于 suspend/resume 同步 |
is_suspended | bool | true/false | 是否已完成 suspend 阶段 | dpm_suspend()设置,dpm_resume()清除,用于状态校验 |
should_wake | bool | true/false | 是否允许 wake event 唤醒设备 | USB 设备常设为 true,但 GPIO 中断设备易漏设 |
这张表背后是大量踩坑经验。比如direct_complete字段:当设备在prepare()阶段就判断出可以跳过suspend(),就设dev->power.direct_complete = true,然后dpm_suspend()会直接跳过该设备的suspend()回调。但很多驱动忘了在prepare()里返回 0,导致dpm_suspend()认为 prepare 失败,整个 suspend 流程 abort。我在调试一块 PCIe SSD 时,就是因为prepare()返回了-EBUSY,而direct_complete又设为 true,结果系统 suspend 卡死在dpm_suspend()的循环里。
另一个高频问题是runtime_last_busy的更新时机。它只在pm_runtime_mark_last_busy()里更新,而这个函数通常由设备驱动在完成一次 I/O 后手动调用。如果驱动忘了调用,rpm_idle()就会认为设备“从未被使用过”,从而在power.runtime_usage归零后立即触发 suspend——哪怕设备刚完成一个耗时 5 秒的 DMA 传输。正确的做法是在complete()回调里调用pm_runtime_mark_last_busy(dev),而不是在start_xfer()里。
4. 实操:从零跟踪一次 echo mem > /sys/power/state 的完整调用栈
理论终需落地。下面我以 ARM64 平台执行echo mem > /sys/power/state为例,逐层展开调用栈,标注每个关键节点的参数、锁状态和潜在风险点。这不是代码复读,而是带你走进内核的“手术室”。
4.1 用户空间触发:sysfs 接口的隐式约束
/sys/power/state是一个 sysfs 属性文件,其store方法指向state_store()函数(kernel/power/main.c)。这个函数第一行就是:
if (!valid_state(state)) return -EINVAL;valid_state()检查state是否在pm_states[]数组里。对于mem,它对应PM_SUSPEND_MEM。但关键在第二行:
error = enter_state(state);enter_state()是整个流程的闸门。它做的第一件事是调用suspend_prepare(),而这个函数里有一行容易被忽略的代码:
pm_prepare_console();它会保存当前 console 的状态,并在 resume 后恢复。如果你的系统没有配置CONFIG_VT_CONSOLE,或者 console 是通过earlyprintk实现的,这里就会静默失败,导致 suspend 后屏幕黑屏无法恢复——这不是 PM Core 的 bug,而是 console 子系统与 PM 的契约未对齐。
4.2 系统挂起准备:dpm_prepare() 的双重锁机制
enter_state()接下来调用dpm_prepare(PMSG_SUSPEND)。这个函数遍历dpm_list,对每个设备调用device_prepare()。device_prepare()的核心是:
mutex_lock(&dev->mutex); pm_runtime_get_noresume(dev); pm_runtime_barrier(dev);注意:这里用了mutex_lock(&dev->mutex),而不是power.lock。dev->mutex保护的是设备的整个生命周期(如 probe/remove),而power.lock只保护power字段。两者嵌套使用时,必须遵守锁顺序:先dev->mutex,再power.lock,否则会死锁。我在调试一个 USB hub 驱动时,发现它在remove()里先拿了power.lock,再试图拿dev->mutex,结果和dpm_prepare()的锁顺序冲突,导致 suspend 卡死。
pm_runtime_barrier(dev)是关键:它等待所有 pending 的 runtime pm 操作完成。这意味着,如果某个设备的pm_runtime_put_autosuspend()正在延时队列里排队,dpm_prepare()会在这里阻塞,直到延时到期。这就是为什么有时echo mem会卡住几秒钟——不是硬件慢,而是某个设备的 autosuspend 延时没到。
4.3 设备挂起执行:dpm_suspend() 的状态校验与回调分发
dpm_prepare()成功后,enter_state()调用dpm_suspend(PMSG_SUSPEND)。这个函数遍历dpm_prepared_list(注意:不是dpm_list),对每个设备调用__device_suspend()。__device_suspend()的核心逻辑是:
if (dev->power.is_suspended && dev->power.is_prepared) { /* 设备已 suspend,跳过 */ goto Complete; } if (dev->power.direct_complete) { /* 走 direct-complete 路径 */ goto Complete; } if (dev->driver && dev->driver->pm) { callback = dev->driver->pm->suspend; } else if (dev->bus && dev->bus->pm) { callback = dev->bus->pm->suspend; } if (callback) error = callback(dev);这里体现了分层设计的精髓:驱动优先,bus 次之,最后 fallback 到通用逻辑。但callback(dev)的返回值处理很严格:必须返回 0 才算成功,否则__device_suspend()会设置dev->power.is_suspended = false,并在dpm_suspend()结尾统一报错。
我在调试一块 SPI NOR Flash 时,发现它的suspend()回调里调用了spi_sync(),而spi_sync()在 suspend 上下文里会尝试获取spi_master->bus_lock,这个锁在dpm_prepare()时已被spi_master的prepare()拿走,导致死锁。解决方案是:在suspend()里改用spi_async()+ completion,或者直接返回-EBUSY让上层跳过该设备。
4.4 平台进入低功耗:platform_ops->enter() 的硬件握手
dpm_suspend()成功后,enter_state()调用platform_ops->enter(state)。对于 ARM64,这指向psci_cpu_suspend()(drivers/firmware/psci/psci.c)。这个函数会:
- 调用
psci_ops.cpu_suspend(),即psci_cpu_suspend_finisher(); - 将 CPU 状态寄存器(如
SPSR_EL1)设置为指定的 power state; - 执行
wfi(Wait For Interrupt)指令,让 CPU 进入 WFI 状态。
但wfi不是终点。PSCI firmware 收到请求后,会检查所有 CPU 的状态。如果有一个 CPU 还在运行(比如 watchdog timer 在 tick),firmware 就不会真正关闭电源域,而是让 CPU 继续wfi。这就是为什么有时dmesg显示Entering suspend state mem,但电流表读数纹丝不动——问题不在内核,而在 firmware 没有收到所有 CPU 的确认。
验证方法:在psci_cpu_suspend_finisher()里加pr_info("CPU %d entering state 0x%lx\n", smp_processor_id(), state),然后看所有 CPU 是否都打印了这条 log。如果某个 CPU 缺失,说明它的wfi被中断打断后没有重新进入,需要检查该 CPU 的中断处理是否阻塞了wfi。
5. 常见问题与排查技巧实录:那些文档里不会写的“血泪史”
5.1 问题:suspend 后无法唤醒,按键/网络无响应
现象:执行echo mem > /sys/power/state后,系统黑屏,按电源键无反应,必须长按强制重启。
排查思路:
- 首先确认唤醒源是否 enable:
cat /proc/sys/dev/wakeup查看全局 wakeup 状态;cat /sys/devices/platform/.../power/wakeup查看具体设备。 - 如果设备
wakeup文件内容是disabled,执行echo enabled > /sys/devices/platform/.../power/wakeup。 - 但更常见的是:设备 driver 的
.suspend()里调用了disable_irq(),但.resume()里忘了enable_irq()。此时dmesg会显示irq X: nobody cared。
独家技巧:在__device_suspend()里加一行pr_err("SUSPEND %s: wakeup=%d\n", dev_name(dev), device_may_wakeup(dev)),然后dmesg | grep SUSPEND,看哪些设备的wakeup返回 0。如果 USB host controller 的wakeup是 0,说明它的device_set_wakeup_enable(dev, true)没被调用,通常是因为usb_hcd_resume_root_hub()在 resume 时才设置,而 suspend 时没设置。
5.2 问题:runtime pm 导致设备频繁 suspend/resume,功耗不降反升
现象:cat /sys/devices/.../power/runtime_status在active和suspended间疯狂跳变,万用表测得平均电流比runtime pm关闭时还高。
根因分析:每次 suspend/resume 都有硬件开销(如 PHY 重初始化、PLL 重锁定),如果间隔太短,开销超过 idle 节省的功耗。
解决方案:
- 增大
autosuspend_delay:echo 5000 > /sys/devices/.../power/autosuspend(5 秒); - 使用
pm_runtime_put_noidle()替代pm_runtime_put(),避免触发 idle; - 在驱动里实现
->runtime_idle()回调,加入业务逻辑判断(如“DMA buffer 未满不 idle”)。
实操心得:我曾为一个 SDIO WiFi 模块设置autosuspend_delay=1000,结果 ping 包丢包率飙升。后来发现,WiFi firmware 在 idle 时会关闭 RF,而autosuspend_delay太小导致频繁开关 RF,引发射频干扰。最终方案是:在->runtime_idle()里检查netif_queue_stopped(),只有当网络队列空闲且无 pending packet 时才允许 idle。
5.3 问题:dpm_suspend() 卡死在某个设备,dmesg 无任何输出
现象:echo mem后系统无响应,Ctrl+Alt+F2切不到 tty,SysRq也无效。
终极排查法:
- 在
dpm_suspend()循环里加pr_emerg("DPM SUSPEND %s\n", dev_name(dev)); - 重新编译内核,启动后执行
echo mem; - 看最后一个打印的设备名,就是卡点。
典型案例:某次卡在soc:qcom,spmi-dbg设备。查看其 driver,发现suspend()里调用了regmap_read()读取 debug register,而regmap的read函数在 suspend 上下文里会尝试 acquireregmap->lock,这个 lock 在dpm_prepare()时已被spmi_master的prepare()拿走。解决方案:在suspend()里改用regmap_read_async(),或者直接返回-EBUSY。
避坑口诀:suspend/resume 回调里,禁止调用任何可能 sleep 的函数(msleep,wait_event,mutex_lock),禁止访问可能被其他 CPU 修改的共享资源(除非用spin_lock_irqsave),禁止发起新的 I/O(DMA、SPI、I2C)。
5.4 问题:系统 suspend 后,RTC 时间不准,偏差达数分钟
现象:唤醒后date显示时间比实际晚几分钟。
原理:RTC 是独立于 CPU 的硬件,但它的中断(IRQ 8)需要 CPU 处理。如果 suspend 时 RTC 中断被 mask,或者 resume 后 RTC driver 没有重新 sync 时间,就会累积误差。
验证步骤:
cat /proc/interrupts | grep rtc,看 suspend 前后 IRQ 8 的计数是否增长;cat /sys/class/rtc/rtc0/since_epoch,对比 suspend 前后值。
修复方法:
- 在 RTC driver 的
.suspend()里调用rtc_dev_suspend(); - 在
.resume()里调用rtc_dev_resume(),并手动rtc_set_time(); - 或者,启用
CONFIG_RTC_HCTOSYS,让 kernel 在 boot 时从 RTC 读取时间。
个人体会:这个问题在嵌入式设备上极其隐蔽。我曾花三天排查一块工业网关的时钟漂移,最后发现是rtc-s3cdriver 的.resume()里漏掉了s3c_rtc_setaie(),导致 RTC alarm 中断没使能,hctosys机制失效。教训是:任何带中断的外设,suspend/resume 里必须显式管理中断使能状态。
6. 分层设计的延伸思考:为什么 Linux 功耗框架能支撑从手表到超算的跨度?
回到标题的核心:“分层设计”。PM Core 的四层结构,不是为了炫技,而是为了解决一个根本矛盾:硬件多样性与软件可维护性的不可调和。一块智能手表的 MCU 只有几十 KB RAM,需要极致精简的功耗管理;而一台 AI 服务器的 CPU 有上百个 core,GPU 有数千个 SM,功耗状态组合呈指数爆炸。如果用同一套代码处理,要么手表跑不起来,要么服务器管不住。
分层设计给出了优雅解法:
- 设备模型层提供最小公约数:所有设备都有
power字段,都有runtime_status; - DPM Core 层提供流程骨架:suspend/resume 的顺序、状态校验、错误传播,与硬件无关;
- Runtime PM 层提供设备粒度控制:单个设备可独立启用/禁用,可自定义 autosuspend 延迟;
- Platform PM 层提供硬件适配:PSCI、ACPI、SBI,只是不同方言,说的都是“请把我关掉”。
这种设计让贡献者可以各司其职:SoC 厂商专注写psci_cpu_suspend(),驱动作者专注写->suspend(),系统集成者专注配置autosuspend_delay。没有人需要理解全部。
我自己在参与一个 RISC-V SoC 项目时,就深刻体会到这点。我们只需要实现sbi_suspend()调用,然后在arch/riscv/kernel/suspend.c里填platform_suspend_ops,剩下的 DPM Core、Runtime PM 全部复用上游代码。整个功耗框架的移植,只用了两天,而之前在 ARM 平台,光 PSCI firmware 适配就花了三周。
最后分享一个小技巧:当你面对一个陌生的功耗问题,不要一上来就翻 driver 代码。先执行grep -r "pm_runtime" drivers/your_device/,看驱动是否调用了 runtime pm API;再cat /sys/devices/your_device/power/下所有文件,看runtime_status、autosuspend、wakeup的值;最后dmesg | grep -i "dpm\|pm_runtime",看内核日志里的状态流转。90% 的问题,靠这三步就能定位。真正的高手,不是代码写得最多的人,而是最懂如何用分层设计“缩小问题域”的人。