☰
Linux内核功耗管理核心:深入理解PM Core分层设计与实践
2026/10/8 6:07:46 网站建设 项目流程

先说一个我自己的亲身经历。前两年调一块板子,外设驱动都注册得好好的,但一跑 suspend 测试就怪事频出:有的设备睡眠后叫不醒,有的设备 resume 回来直接报错,还有一次整机睡下去就再也起不来了。一开始我以为是某个 GPIO 配置的问题,顺着设备树和驱动查了两天,最后才发现问题出在根本没人关心的地方——驱动没用标准的 PM 回调,而是自己搞了一套“野路子”电源控制。那一刻我才真正意识到,Linux 内核的功耗子系统,尤其是 PM Core 这一层,绝不是一个可以“用到再查”的库,而是一套从设计理念上就规定了“谁负责什么、什么时候管、怎么上报”的完整框架。这篇文章我想以 PM Core 为切入点,把 Linux 内核功耗子系统的分层设计从头到尾聊透,包括它的边界、核心机制、实践姿势和我在项目里踩过的坑。

这个内容适合三类人:一是嵌入式 Linux 开发者,特别是要调低功耗、搞睡眠唤醒的;二是内核驱动开发者,想搞清楚 probe、suspend、resume 这些函数到底是怎么被调起来的;三是纯粹想深入内核的人,可以用 PM Core 作为理解内核“框架思维”的一个极佳切片。我不打算把代码逐行抄一遍,而是讲清楚设计意图和推演过程,配合关键代码结构,让你看完之后能直接拿去指导自己的驱动和项目。

1. 先搞清楚功耗子系统到底管了哪些事

很多人一说“Linux 功耗管理”,第一反应就是电源键按下去之后系统睡眠。其实这只是很小的一部分。功耗子系统真正要解决的事情可以拆成几大类:运行时设备功耗管理(runtime PM)、系统级睡眠与唤醒(suspend/resume/hibernate)、CPU 与设备频率调节(cpufreq / devfreq)、空闲状态选择(cpuidle)、以及电源域(power domain)的开关和电压调节。这些功能从表面看散落在不同目录、不同框架里,但它们都遵循同一个底层逻辑:在“性能”和“功耗”之间找一个动态平衡点,并且让这个平衡点可以被上层策略、用户空间甚至硬件事件灵活驱动。

PM Core 就是这一整套体系里的“核心骨架”。它的代码主体在 drivers/base/power/ 目录下,最核心的文件是 main.c、runtime.c、wakeup.c、sysfs.c、qos.c 等。它不直接操作哪个硬件,也不直接决定调频策略,而是定义了一套通用的、与具体硬件无关的机制:设备生命周期与电源状态之间的映射、睡眠/唤醒流程的编排、通知机制的转发、以及暴露给用户空间的控制接口。一句话概括,PM Core 是“地基中的地基”,没有它,其他功耗组件全是散沙。

这里面的“分层设计”,本质上是在解决一个非常现实的工程问题:功耗管理涉及的范围太大,从 CPU 微架构到 USB 外设,从系统启动到休眠唤醒,如果所有逻辑都搅在一起,这个子系统会迅速变成一个无法维护的黑洞。分层的意义在于:每一层只对上一层负责,只依赖下一层的接口,层与层之间通过约定好的 callback 和状态机通信。这样,新增一个电源域控制器不会影响驱动层,更换一个 SoC 平台也不会导致所有外设驱动重写。

1.1 功耗相关的“子系统”其实是多张网的叠加

要理解 PM Core 的设计,先得把 Linux 内核里的功耗相关代码块从“功能矩阵”的角度重新组织一遍。按管理对象分,可以分为 CPU 侧和 Device 侧;按时机分,可以分为“运行时”和“系统睡眠时”;按职责分,又可以分为“策略制定者”和“策略执行者”。如果把这些维度画成一张表,你会看到这样一幅图景:

维度CPU 侧Device 侧
运行时cpufreq(调频)、cpuidle(空闲)runtime PM、devfreq、PM QoS
系统睡眠suspend/resume 流程、CPU hotplug 中的下线dpm_suspend / dpm_resume、wakeup 事件
策略决策governor(ondemand、schedutil 等)驱动自身逻辑、电源域控制器、平台 PM 回调
基础机制时钟、中断、mm 的 freezePM Core 的各类 callback 与状态机

PM Core 在表中并不是最上层的“策略层”,也不是最底层的“硬件接触层”,而是横贯中间的“机制层”。举个例子,runtime PM 框架在 drivers/base/power/runtime.c 里定义了一套 rpm_suspend、rpm_resume 的通用执行流程,但这个流程本身不关心你的设备是 Wi-Fi 芯片还是 eMMC 控制器,它只负责状态迁移、引用计数、延迟操作等共性部分。而“这个设备能不能关”、“关了之后怎么再开”,则由设备驱动实现自己的 runtime_suspend / runtime_resume 回调,或者由它所属的电源域来回答。

很多初学者容易犯一个认知错误,就是把 PM Core 等价于“电源管理 API 集合”,以为用对函数就行。其实 PM Core 更重要的是一种顺序编排和状态记账。它时刻知道每个设备当前处于什么状态(active、suspended、resuming、disabled),知道设备之间的依赖关系(parent 与 child,domain 与 device),知道哪些 wakeup 事件正在发生,并在此基础上调度一条安全的电源切换路径。没有这套“记账系统”,任何高层策略都只是空中楼阁。

1.2 PM Core 在整个框架里的坐标

为了把分层设计讲透,我用一个项目中的实际场景来定位 PM Core。假设你在做一个带电池的物联网设备,跑着 Linux,有一颗应用处理器、一块触摸屏、一个 Wi-Fi 模块和一个环境传感器。系统运行中有两种典型需求:一种是触摸屏 10 秒没操作就让它进低功耗,但 Wi-Fi 还要保持连接,这叫“运行时设备级功耗管理”;另一种是整个系统要进 suspend,内存保持自刷新,等按键唤醒,这叫“系统级睡眠”。

第一种需求落到内核里,驱动会调用 pm_runtime_put_sync,让触摸屏进入 runtime suspend,Wi-Fi 驱动则保持 runtime resume。这些 API 的背后,就是 PM Core 的 runtime PM 机制。第二种需求则复杂得多:上层触发 mem/suspend 后,内核开始按依赖关系逐设备调用 suspend 回调,这个过程由 PM Core 的 dpm_suspend 来统筹,设备的 suspend 顺序由 dev_pm_ops 里的 callback 和设备的父子拓扑、dpm_list 顺序共同决定。

可以看到,PM Core 扮演的是“交通调度员”:它不决定车往哪开,但决定了哪辆车先走、哪辆车后走、哪条路要先封闭、哪条路能继续通行。它向下通过 resume/suspend 回调与设备驱动打交道,向电源域和平台 PM 下发指令;向上则提供 PM QoS、wakeup source 等接口给策略层使用。这种“承上启下”的位置,决定了它必须把通用机制和具体策略严格分开,这也正是分层设计的核心动机。

2. PM Core 的骨架:dev_pm_ops 与三套回调体系

前面讲过 PM Core 不碰具体硬件,那它到底靠什么和设备驱动打交道?答案是 struct dev_pm_ops,这是整个 PM Core 对外最重要的数据结构。每个设备(struct device)背后,都挂着一份 dev_pm_ops,里面包含了许多函数指针,驱动按照约定填充这些指针,PM Core 在合适的时机调用它们。理解这个结构,就抓住了 PM Core 的面板。

dev_pm_ops 的定义在 include/linux/pm.h 中,大致可以分为三套回调体系。第一套是系统睡眠/休眠相关:suspend、suspend_late、resume、resume_early,以及 hibernation 使用的 freeze、thaw、poweroff、restore。第二套是 runtime PM 相关:runtime_suspend、runtime_resume、runtime_idle。第三套是电源域智能管理相关:prepare、complete 等用于全局流程前/后准备工作的回调。我先用表格把这些回调梳理清楚,再逐个展开。

回调体系主要回调触发时机职责
系统睡眠/休眠prepare / suspend / suspend_late系统进入睡眠的不同阶段完成设备自身的睡眠准备、关闭硬件
系统唤醒resume_early / resume / complete系统唤醒的恢复阶段重新初始化硬件、恢复工作状态
运行时电源管理runtime_suspend / runtime_resume / runtime_idle设备运行期间动态开关在不影响系统整体时单独控制设备功耗
全局钩子freeze / thaw / poweroff / restore休眠镜像的保存与恢复配合 hibernation 的镜像流程

这里有一个很容易混淆的点:suspend 和 runtime_suspend 功能上相似,都是把设备切到低功耗状态,但语义完全不同。suspend 是在整个系统进入睡眠的背景下执行的,内存可能很快就不再响应,设备的中断处理也会逐渐停掉;而 runtime_suspend 是在系统仍在正常运行、CPU 仍在跑代码的情况下,单独把某一个设备关掉。这种区别直接体现在实现上:你可以在 runtime_suspend 里比较放心地使用锁、延时、I2C 传输,但在 suspend 后期(比如 suspend_late 阶段)再做这些操作就可能出问题,因为系统里的不少机制已经逐步冻结了。

另一个需要理清的概念是 callback 的注册位置。驱动的 dev_pm_ops 可以定义在设备驱动本身,也可以用通用框架提供的现成实现。比如 Linux 的 I2C 子系统、SPI 子系统、USB 子系统,它们的 bus 层往往会提供一套默认的 PM 回调处理流程,驱动只需要填充个别函数。这种设计也是“分层”的体现:通用逻辑放在 bus 层,特殊逻辑放在驱动层,PM Core 则负责在合适的时机把调用链串起来。

2.1 系统睡眠的回调链与调用顺序

系统睡眠的回调链,是理解 PM Core 分层设计最好的切入点。Linux 进入 suspend 时,PM Core 会启动一个固定的调用序列。简化地说,首先是 dpm_prepare,遍历设备列表调用每个设备的 prepare 回调;然后是 dpm_suspend,调用 suspend 回调;再是 dpm_suspend_late,调用 suspend_late。这个过程设计成“从下往上”的依赖关系:子设备先挂起,父设备后挂起,因为父设备往往是子设备工作的基础。比如一个 I2C 控制器是触摸屏的父设备,必须等触摸屏 suspend 完成之后,I2C 控制器才能安全 suspend。

等到唤醒时,顺序反过来:dpm_resume_early 先调用 resume_early,再是 dpm_resume 调用 resume,最后 dpm_complete 调用 complete。这种“LIFO”式的栈式恢复,保证了设备唤醒顺序与依赖方向一致,避免出现“子设备已经 resume、父设备还没 resume”的尴尬局面。

实话说,这个顺序并不只是“压栈出栈”这么简单。内核用的是 dpm_list,这是把所有设备加入的一个链表,链表节点的顺序由设备注册顺序、parent-child 关系、以及 device 的 PM 优先级共同决定。在 device_register 时,内核会调用 device_pm_init 把设备挂到 dpm_list 中;而 suspend 时则是从链表尾部开始遍历。项目中如果发现设备挂起顺序不对,优先检查设备之间的 parent 关系是否在 device tree 或驱动代码里正确建立,而不是急着改代码。

2.2 runtime PM 的状态机与引用计数

如果说系统睡眠是“一键全关”,runtime PM 就是“随时单点开关”。runtime PM 的核心是一个状态机,设备只有三个状态:active(运行)、suspended(已挂起)、suspending(正在挂起)。驱动和内核的其他部分通过 pm_runtime_get / pm_runtime_put 系列 API 来增加或减少设备的“使用计数”。当计数从 0 变 1 时,设备会从 suspended 转 active;从 1 变 0 时,设备进入 suspending,驱动执行 runtime_suspend 后变为 suspended。

这套机制的巧妙之处在于:它把“谁在用这个设备”的语义,抽象成了一个简单的计数。如果显示控制器正在刷屏,它的使用计数至少是 1,系统就不会去 suspend 它;如果计数值降到 0,说明暂时没人用,PM Core 就可以安全地把它关掉。相比直接“按需开关”,这种多引用计数的模型在多驱动共享同一设备时特别重要——两个驱动同时打开同一个音频编解码器,必须等两个都释放后,才能关掉它。

我在实践中最常用的几个 API 组合如下:

  • pm_runtime_get_sync:同步增加引用计数,确保设备在 active 状态才返回。
  • pm_runtime_put_sync:同步减少引用计数,如果计数值回到 0,则同步执行 runtime suspend。
  • pm_runtime_get_noresume:只增加计数,但不会主动唤醒设备,适合某些不希望因计数变化而引发硬件操作的中断上下文场景。
  • pm_runtime_put_noidle:只减少计数,不触发实际挂起,适合需要延迟处理的场景。

很多驱动在设计时会纠结:到底用哪个 API 组合?我的一般原则是,在可能阻塞的进程上下文里,用同步版本,逻辑清晰、容易排查;在中断或原子上下文里,用异步版本,配合 pm_runtime_autosuspend 机制来延迟实际挂起,避免频繁开关带来的性能损耗。autosuspend 是 runtime PM 里一个很值得讲的设计,它允许设备在计数归零后不是立刻断电,而是等待一个可配置的延迟时间,如果期间有人再次使用就取消挂起。这对触摸屏、Wi-Fi 这类“可能很快又要用”的设备非常友好。

2.3 PM Core 与电源域(power domain)的协调

PM Core 只是“通用框架”,它并不要求所有设备都必须直接通过自己的操作去开关电源。很多设备挂在一个 power domain 下,由电源域控制器统一管理它们的供电、时钟或复位。Linux 内核里,电源域有两种风格:一种是比较古老的“平台级 PM 回调”(platform_suspend_ops 之类),另一种是基于设备树 generic power domain(genpd)。PM Core 跟 genpd 的配合方式是:设备在 dev_pm_ops 之外还可以关联一个 power domain,PM Core 在处理设备睡眠时,会调用该电源域的相应回调,由电源域去操作实际的硬件开关,而不是由设备驱动直接碰寄存器。

这种设计的价值,在复杂 SoC 上体现得尤其充分。比如一颗手机 SoC,摄像头、ISP、显示控制器通常共享一个 ISP 电源域,这个域的开关逻辑非常敏感——时序、电压、隔离要求都很苛刻。如果每个设备驱动都自己去写电源域开关逻辑,一旦 SoC 改版,所有驱动都要跟着改。有了 genpd,开关逻辑收敛在电源域驱动里,设备驱动只需要订阅这个域,PM Core 在合适时机触发电源域状态迁移。这就是“分层”带来的回旋余地:每一层只要保证接口稳定,内部的改动不会造成连锁效应。

有个地方需要特别注意:设备挂到电源域之后,它的 runtime PM 回调顺序是“先电源域,后设备驱动”,也就是电源域先上电,然后设备驱动再初始化硬件。反过来,断电时是设备驱动先 suspend,电源域再断电。如果你在驱动里直接操作的是属于电源域控制的寄存器,却没有等电源域完成上电,就会踩到访问未供电寄存器的坑,表现出来就是设备初始化时随机失败、寄存器读回全 F。这类问题非常隐蔽,定位时往往要先查设备树里的 power-domains 属性和 genpd 的 status。

3. 细节深挖:wakeup 机制、notifier 与 sysfs 接口

PM Core 不只是回调的分发器,它还提供了几个关键的横向机制,这些机制把设备、内核策略、用户空间贯穿在一起。wakeup 机制就是其中之一。

3.1 wakeup source 与 wakeup event:谁叫醒了系统?

系统进入 suspend 后,理论上一切静默,但总有那么几个设备还能把系统唤醒,比如电源键、RTC 闹钟、网络包(WoL)。PM Core 为这类场景引入了 struct wakeup_source,用来跟踪“当前是否有 wakeup event 正在发生”以及“设备是否有能力唤醒系统”。

每个 wakeup_source 都挂在一个设备上,子系统的代码通过 __pm_stay_awake 和 __pm_wakeup_event 等接口来声明事件的到来和结束。内核在真正进入睡眠之前,会检查所有 wakeup_source 的 active 状态;如果发现有事件正在处理,就延迟睡眠。这样做的好处是避免“刚睡又被唤醒”的抖动,也保证系统不会在中断处理到一半的时候丢掉设备的状态。

拿 RTC 举个例子:驱动注册了一个 RTC 设备作为 wakeup source,定时器时间到了之后,硬件产生中断,中断处理函数调用 __pm_wakeup_event(rtc_ws, 500),表示有一个持续 500ms 的唤醒事件。PM Core 会记录这个事件,并将其作为“系统不应该立刻再次 suspend”的依据。500ms 是“防抖窗口”,给上层应用留出处理时间,事件超时后内核才允许再次睡眠。

这里有一个实践中的关键配置:系统中的 wakeup source 应当保持“少而精”。我有一次给客户调功耗,发现系统 suspend 后每秒钟醒一次,查了整整一天,最后用 /sys/kernel/debug/wakeup_sources 发现是触摸屏的 wakeup source 一直在上报事件。原因也很简单:触摸屏的 IRQ 配置成任意触摸都会唤醒,而触摸屏本身又不在盖合状态下,环境轻微振动就不断触发事件。解决方案是在驱动里根据系统状态(比如盖子是否合上)动态启用/禁用 wakeup,而不是让 wakeup source 一直开着。

3.2 PM notifier:当睡眠流程走到一半时,怎么通知其他人

PM notifier 是 PM Core 提供的一套广播机制,允许内核里与设备无直接关系的模块在 suspend/resume 流程的特定节点得到通知。比如用户空间要冻住、网络栈要准备断线、文件系统要准备同步,这些逻辑不适合放在设备驱动的 PM 回调里,但又需要在系统睡眠前后做点事情。notifier 机制就是为它们准备的。

内核里使用 register_pm_notifier 注册回调,回调里处理 PM_HIBERNATION_PREPARE、PM_SUSPEND_PREPARE、PM_POST_SUSPEND 等事件。这些事件顺序固定,在 PM Core 的 dpm_suspend 之前或之后触发。设计者必须清楚自己的模块逻辑应该挂在哪个点。如果模块需要做的事情依赖设备已经 suspend,那就不能挂在 PM_SUSPEND_PREPARE,因为此时设备回调可能还没执行完。

我印象最深的一次是用 notifier 处理“suspend 前关闭 Wi-Fi 热点”的逻辑。由于 Wi-Fi 驱动本身有 runtime PM,但它不属于标准的系统睡眠设备链,必须在 PM_SUSPEND_PREPARE 阶段主动关闭热点,否则 suspend 流程会被驱动内部的锁阻塞。这其实是 notifier 机制最常见的价值:把不属于设备树范畴的系统级策略,在 PM Core 的固定节点上以可预期的方式执行。

3.3 sysfs 与 debugfs:用户的抓手

PM Core 在 sysfs 和 debugfs 下提供了两个层次的接口。sysfs 面向用户空间普通操作,/sys/power/state 可以查看或触发系统睡眠状态;/sys/power/wakeup_count 用于用户空间参与 wakeup 事件协商;/sys/devices/.../power/control 控制 runtime PM 是 auto 还是 on;/sys/devices/.../power/runtime_status 查看设备的 runtime PM 状态。基本每个设备都有这几个文件,它们是排查功耗问题时的“第一现场”。

debugfs 则提供更细的眼神工具:/sys/kernel/debug/wakeup_sources 能列出每个 wakeup_source 的活跃时间与事件统计;/sys/kernel/debug/pm_print_times 打开后,每次 PM 回调执行都会打印时间戳,用于分析睡眠/唤醒耗时。还有一个很有价值的是 /sys/power/pm_test,可以把睡眠流程终止在特定阶段,用来定位是哪一步卡住了。

给新手一个建议:排查任何功耗相关问题,先养成习惯看一下这几个文件。比如设备明明没在工作,/sys/devices/.../power/runtime_status 却一直显示 active,那基本可以断定有驱动在用 pm_runtime_get 之后忘了递减。而如果调试一个“系统无法睡眠”的问题,先看 wakeup_count,再看 /sys/kernel/debug/wakeup_sources,大概率能定位到是哪个 wakeup source 在捣乱。这种排查路径比瞎猜和打点要高效太多。

4. 实操视角:从驱动代码看 PM Core 的真实用法

理论讲再多,不如直接上手写代码有感觉。这一节我用一个虚构但非常典型的嵌入式设备驱动,完整走一遍 PM Core 相关代码的填充、注册、状态转换和调试过程。

4.1 定义 dev_pm_ops 并正确填充

假设设备是一颗环境传感器,通过 I2C 连接,内核驱动框架是工业 IIO 子系统。它支持运行时关闭,也支持系统睡眠。最标准的写法是先把 dev_pm_ops 定义出来:

static int sensor_runtime_suspend(struct device *dev) { struct sensor_data *data = dev_get_drvdata(dev); // 关闭传感器内部采样,写入寄存器 regmap_write(data->regmap, SENSOR_CTRL, 0); // 可以关闭供电、断开时钟等 return 0; } static int sensor_runtime_resume(struct device *dev) { struct sensor_data *data = dev_get_drvdata(dev); // 重新初始化传感器,等待内部时钟稳定 regmap_write(data->regmap, SENSOR_CTRL, SENSOR_ENABLE); usleep_range(1000, 2000); return 0; }

这里有一点需要特别强调的是:runtime_suspend 回调里做的操作,必须保证“从挂起到重新恢复”是幂等的。也就是说,运行两次都会得到相同结果,而且任何时候恢复到 active 状态都能正常工作。很多驱动出事就出在这里——只在 probe 里做了初始化,但 runtime resume 没有完整恢复硬件状态,导致系统 sleep 后再唤醒设备就异常。

接着定义系统睡眠用到的回调。由于传感器比较简单,且是基于 I2C 的,通常可以直接复用 runtime 回调,但更严谨的做法是分开处理,因为系统睡眠时不需要改变传感器的运行状态,只需要确保 I2C 控制器在睡眠前还有机会访问外设:

static int sensor_suspend(struct device *dev) { // 通常可以调用 pm_runtime_force_suspend,或者直接关闭设备 return pm_runtime_force_suspend(dev); } static int sensor_resume(struct device *dev) { return pm_runtime_force_resume(dev); } static const struct dev_pm_ops sensor_pm_ops = { SET_SYSTEM_SLEEP_PM_OPS(sensor_suspend, sensor_resume) SET_RUNTIME_PM_OPS(sensor_runtime_suspend, sensor_runtime_resume, NULL) };

SET_SYSTEM_SLEEP_PM_OPS 和 SET_RUNTIME_PM_OPS 是内核提供的宏,它们会“按条件填充”结构体成员。在 CONFIG_PM_SLEEP 开启时才填充睡眠相关函数,在 CONFIG_PM 开启时才填充 runtime 函数,这样可以在不开启 PM 配置时保证结构体不产生无用指针,节省空间同时又避免编译错误。这是一个很典型的“内核工匠精神”的体现——每一处宏封装背后都带着配置裁剪的考虑。

4.2 在 probe 与 remove 中挂接 runtime PM

定义好 PM ops 之后,要在 probe 里完成设备和 PM Core 的“挂钩”,包括初始化 runtime PM、使能 autosuspend、以及把设备置为 active。下面是我常用的模板:

static int sensor_probe(struct i2c_client *client) { struct sensor_data *data; struct device *dev = &client->dev; int ret; data = devm_kzalloc(dev, sizeof(*data), GFP_KERNEL); if (!data) return -ENOMEM; i2c_set_clientdata(client, data); >ls /sys/bus/i2c/devices/2-0048/power/ cat /sys/bus/i2c/devices/2-0048/power/runtime_status cat /sys/bus/i2c/devices/2-0048/power/control echo on > /sys/bus/i2c/devices/2-0048/power/control echo auto > /sys/bus/i2c/devices/2-0048/power/control cat /sys/bus/i2c/devices/2-0048/power/autosuspend_delay_ms

如果驱动运行正常,不访问设备时 2 秒后 runtime_status 会变成 suspended;一旦应用层打开设备节点读数,status 马上变成 active。这种“动态开关”的验证,甚至在不用示波器的情况下就能初步确认功耗策略生效。你还可以通过修改 autosuspend_delay_ms 来调节“空闲多久进入低功耗”的阈值,这在调电池续航时特别有用。

我试过的真实场景是:某个传感器芯片每次唤醒需要 10ms,而应用层每隔 2 秒读一次数据,如果 autosuspend 延迟设成 100ms,设备会在每次采样之间反复开关,累计功耗反而更大,而且唤醒延迟可能影响采样时间戳。把延迟改成 1 秒后,芯片在两次采样之间一直保持 active,但省去了反复上电的冲击功耗,整体电流反而降低。这个参数需要实测,不能拍脑袋。

4.4 一个完整 debug 的实用脚本

我觉得一个对排查功耗问题极其趁手的脚本,是持续监控系统里所有设备的 runtime PM 状态变化。你可以用 shell 实现一个非常轻量的版本:

while true; do now=$(date +%s) for dir in /sys/devices/platform/*/power /sys/bus/i2c/devices/*/power; do [ -f "$dir/runtime_status" ] || continue devname=$(dirname "$dir") status=$(cat "$dir/runtime_status") if [ "$status" != "suspended" ]; then echo "$now $devname $status" fi done sleep 1 done

跑一段时间之后,把输出按设备聚合,基本能看出哪些设备长时间没有进入 suspend,然后顺着这些设备去检查对应驱动的引用计数。这比手动一个个 cat 效率高得多。当然如果板子上有 perftool 或者 ftrace,用 tracepoint rpm_suspend/rpm_resume 更精准,但 shell 脚本在资源受限的目标机上更好使。

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

讲完用法,必须聊一聊我踩过和帮别人排过的坑。PM Core 的代码本身经过大量内核版本的锤炼,逻辑相对稳定,项目里出问题,绝大多数是“使用方式不合法”导致的。下面这些问题,我几乎每年都会在不同项目里遇到。

5.1 设备 suspend/resume 顺序颠倒导致的启动失败

有一次在调试一个带 MIPI 摄像头模组的板卡时,系统进入 suspend 正常,但 resume 时摄像头驱动报了 I/O 错误。跟踪代码发现,摄像头的 I2C 控制器 parent 是 platform bus 上的某个控制器,而这个控制器依赖另一个 PMIC 电源域供电。问题根源在于设备树里电源域层级没有正确表达依赖关系,导致 PM Core 按照错误的 dpm_list 顺序执行了 resume:摄像头先 resume,它的 I2C 控制器还没恢复,访问自然失败。

这种情况排查起来,首先看内核日志里 dpm_suspend/dpm_resume 的调用顺序,再对照设备树中的 power-domains 属性和 parent 节点。修正方案是在设备树中补上 power-domains 引用,并把依赖关系理顺。这个案例告诉我一个原则:设备树不只是“描述硬件”,它也在定义 PM 顺序,而 PM Core 的分层设计把顺序责任分摊给了设备树和 bus 层,驱动自身要做的只是“如实上报依赖”。

5.2 runtime PM 引用泄漏:状态显示 active 但设备根本没在用

这是嵌入式开发中出现频率最高的一类 PM 故障。症状很典型:设备已经空闲,/sys/bus/.../power/runtime_status 却始终是 active,电流居高不下。通常原因是驱动在某个分支里调用 pm_runtime_get_sync 后,早退路径忘了 pm_runtime_put。在 probe、ioctl、中断线程中都很常见。

我在一个音频驱动里抓到过这种问题:某次 ioctl 传参非法时,函数在获得 pm_runtime_get_sync 之后直接 return -EINVAL,把引用计数永久泄漏了。修复很简单,在早退路径前加 pm_runtime_put_sync 即可。但更值得学习的是:内核提供了 CONFIG_PM_RUNTIME_SELFTEST 之类的手段做压力测试,实践中也能通过 ftrace 里的 rpm_get 和 rpm_put 事件对比来快速定位是哪次调用漏了。建议每个驱动在合入之前,都做一轮“长时间空闲 + 随机访问”的稳定性测试,专门观察 runtime_status 是否会卡在 active。

5.3 suspend 时“睡不下去”:最后时刻被某个设备挡住

系统触发 suspend 后,日志停在某个设备上不再前进,常见的两个原因:一是设备的 suspend 回调里做了阻塞操作,等待某个资源,但资源被其他没有被 suspend 的进程占用;二是 runtime PM 和 system sleep 发生死锁——驱动的 suspend 回调里调用了 pm_runtime_get_sync,期望设备恢复 active,但此时 runtime PM 已经被系统睡眠流程禁用。

这种问题一旦发生,日志往往不直观,因为不是每次都稳定复现,可能与负载、中断时机相关。我的排查套路是:打开 ftrace 的 power 相关事件,如 power/suspend_resume、power/device_start_suspend,确认卡在哪个回调上;然后结合 /sys/kernel/debug/wakeup_sources 确认是否有事件持续存在;最后再查这个设备的回调里是否使用了不安全的锁或函数。多数情况下,最终原因都在驱动的某个“自认为没问题”的细节里,比如在 suspend 回调里尝试获取 mutex,而那个 mutex 正被一个尚未冻结的进程持有。

5.4 autosuspend 设置不当导致的功耗不降反升

还有一个容易被忽略的工程问题: autosuspend 延迟设置不合理。我在一个 IoT 项目里发现,设备明明处于不活跃状态,但功耗曲线呈现周期性的“尖刺+回落”。用示波器抓电流后发现,Wi-Fi 模块每隔几秒就经历一次“断电-上电”循环,原因是 autosuspend 延迟设成了 100ms,而系统的网络协议栈保持每 200ms 有一次极短暂的报文活动,导致 Wi-Fi 永远在“刚要睡就被叫醒”和“醒来后发现没事又准备睡”之间来回折腾,每次开关的功耗远大于保持 active 的开销。

这其实是 runtime PM 的一个经典工程权衡:延迟太长会浪费设备完全空闲后的漏电,太短又会引发频繁开关。更合理的做法是让驱动了解“设备断开到重新打开的代价”,比如保存/恢复上下文的时间、启动时间、冲击电流时间,按照这些参数的下限设定 autosuspend delay。如果拿不准,宁可设置得长一点,也不要设置得过于激进,功耗可以接受,但稳定性和响应时间受影响就得不偿失。

5.5 睡眠唤醒事件丢失:wakeup_count 的使用姿势

最后分享一个用户空间常见问题。使用 /sys/power/state 触发睡眠时,会有一种情况:系统刚进入睡眠,结果 wakeup 事件来了,唤醒之后用户空间的唤醒处理逻辑却没有收到预期事件。内核能提供的服务是:把 wakeup event 记录在 wakeup source 中,并通过 /sys/power/wakeup_count 暴露给用户空间。标准的用户空间睡眠握手流程是这样:

# 读取当前 wakeup_count cat /sys/power/wakeup_count # 把这个值写回,表示“我认可这次睡眠,并且会处理后续唤醒” echo $count > /sys/power/wakeup_count # 如果写入成功,再真正触发睡眠 echo mem > /sys/power/state

为什么要有“写回”这一步?这是内核和用户空间之间的一种防竞态协商:内核在睡眠前检查 wakeup_count,用户空间在检查后到真正睡眠前,如果又有新的 wakeup 事件进来,内核会拒绝这次睡眠,避免“边睡边醒”。很多嵌入式产品中,休眠由自研的 power manager 服务触发,如果它不遵守这种握手流程,就可能丢掉底部半唤醒的按键事件。解决办法是严格按照内核文档里的推荐流程实现,切不可简化成“直接 echo mem”。这个细节看起来小,但在产品的开关机、按键唤醒、来电唤醒场景里,直接影响用户体验。

6. 顺着 PM Core 往下走:下一站该去哪

PM Core 本身已经把“机制”这层做得很完善,但内核功耗子系统的全貌远不止如此。从设备侧往上看,是 cpufreq 和 cpuidle 在处理器侧做动态调频与空闲状态选择;往旁边看,是 PM QoS 在协调“期望性能”和“可接受的功耗”之间的关系;往深了看,是各平台/SoC 厂商实现的 suspend_ops 和 psci 底层操作。

如果你打算继续深入这个方向,我的建议是:先把 dev_pm_ops 的每个回调都在实际驱动里走通,再用 ftrace 实地核对一遍 runtime PM 的事件流,接着再去看 gpiolib、regulator、clock framework 如何与 PM Core 协作,最后才是去啃 cpufreq 和 cpuidle。直接一上来就读 cpuidle 底层实现,反而容易在一片汇编代码里迷失方向。PM Core 是整个功耗框架最好的入口,因为它的接口最通用、调试手段最丰富、问题可观察性也最强,真正搞懂了它的分层设计,再看任何其他子系统,你都会有一种“骨架已经搭好”的踏实感。

在第一篇里我用大量篇幅讲了 PM Core 的分层骨架和几个核心机制,系统睡眠流程的完整时序、runtime PM 的精细化控制、以及 PM QoS 的深入用法,都还没有展开。后面我会按“从机制到策略”的顺序,先把系统 suspend/resume 的完整调用链逐行走一遍,然后专门写一篇 runtime PM 的进阶实践,把 autosuspend、dev_pm_qos 和复杂场景下的组合用法都放进去。内核功耗这块内容太多,一篇讲完注定是蜻蜓点水,拆成系列慢慢聊,才是更务实的方式。

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

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

立即咨询