1. 从一次待机功耗异常说起:PM Core 到底管什么
去年帮一个做嵌入式设备的朋友排查问题,设备在空闲状态下电流比预期高了将近 40mA,电池续航直接缩水三分之一。他一开始怀疑是 WiFi 模组没进低功耗,又怀疑是屏幕背光没关干净,折腾了快两周,最后用powertop一看,问题出在一个自己写的字符设备驱动上——那个驱动在 probe 阶段调用了pm_runtime_get_sync,但 remove 路径里忘了配对调用pm_runtime_put_sync,导致设备的 runtime PM 引用计数永远归不了零,整个 SoC 的电源域就卡在 active 状态下不来。
这件事让我意识到一个很现实的问题:很多人学 Linux 功耗管理,一上来就去啃cpuidle、cpufreq、regulator这些具体子系统,却忽略了把它们串起来的那个"总调度"——PM Core。你可以把 PM Core 理解成公司里的行政中枢:它不直接干活,但所有部门(cpuidle、cpufreq、runtime PM、system suspend)的流程、状态机、回调时机都由它来定义和协调。不理解 PM Core 的分层设计,你调功耗就是盲人摸象,出了问题只能靠猜。
这篇内容我打算把 PM Core 的分层设计讲透。适合三类人看:一是刚接触 Linux 内核功耗、被各种pm_前缀函数绕晕的初学者;二是做嵌入式或移动端、需要实际调功耗的驱动开发者;三是想理解内核子系统设计哲学、把 PM Core 当成一个架构案例来研究的内核爱好者。我会从分层结构讲起,把device、bus_type、class、dev_pm_domain、dev_pm_ops这几层的关系理清楚,再讲 runtime PM 和 system suspend 两条主线怎么在 PM Core 里统一起来,最后给一些实际调试中真正用得上的经验。
需要提前说明的是,本文基于 Linux 6.x 内核的代码结构来写,不同版本在细节上会有差异,但分层设计的核心思想是稳定的。文中涉及的具体函数名和结构体字段,建议你对照自己手上的内核源码一起看,效果最好。
2. PM Core 的四层结构:谁在什么时候被调用
2.1 为什么功耗管理需要分层
先想一个问题:如果让你设计一套功耗管理框架,你会怎么设计?最朴素的做法是在每个驱动里自己判断"现在能不能省电",然后直接操作硬件寄存器。但这样会带来几个致命问题。
第一是调用时机无法统一。系统要进入 suspend 的时候,几百个设备谁先谁后?如果每个驱动自己决定,顺序就乱了,可能出现"父设备已经断电,子设备还在访问它寄存器"的灾难。第二是状态语义不统一。A 驱动认为"空闲"是 100ms 没访问,B 驱动认为是 1s,系统层面根本没法协调。第三是跨设备依赖无法表达。一个 I2C 触摸屏要进低功耗,前提是它挂载的 I2C 控制器先进低功耗,这种父子依赖关系必须由框架来管。
PM Core 的分层设计就是为了解决这三个问题。它把功耗管理拆成四个层次,每一层负责不同的职责,层与层之间通过明确定义的接口通信。这样驱动开发者只需要关心自己那一层,框架负责协调全局。
2.2 四个层次的具体职责划分
我把这四层从下到上梳理一下,这样你理解起来会顺很多。
第一层是设备层(device layer),对应struct device里的struct dev_pm_info power字段。这一层保存的是设备自身的功耗状态,比如power.runtime_status(当前 runtime PM 状态)、power.runtime_error(runtime PM 是否出错)、power.is_suspended(是否已 suspend)等等。你可以把它理解成每个设备的"功耗档案",框架和驱动都通过读写这个档案来了解设备当前处于什么状态。
第二层是驱动层(driver layer),对应struct dev_pm_ops。这是驱动开发者最常打交道的地方,里面定义了runtime_suspend、runtime_resume、suspend、resume、freeze、thaw、poweroff、restore等一系列回调。驱动通过实现这些回调,告诉框架"当你要让我省电时,我具体该做什么"。注意,驱动只负责"做什么",不负责"什么时候做"——时机由框架决定。
第三层是总线/类/域层(bus/class/domain layer),对应struct bus_type的pm字段、struct class的pm字段,以及struct dev_pm_domain。这一层是 PM Core 分层设计里最容易被忽略、但实际最关键的一层。总线层(比如 platform_bus、pci_bus)可以定义总线级别的功耗回调,类层(比如 input class、block class)可以定义同类设备的通用功耗行为,而 PM domain 则用来表达设备之间的电源依赖关系——比如一个 SoC 上的电源域,域里所有设备都断电了,域本身才能断电。
第四层是核心层(PM Core),对应drivers/base/power/目录下的代码。这一层是"总指挥",它维护设备链表、管理状态机、决定回调调用顺序、处理并发和竞态。main.c、runtime.c、suspend.c、domain.c、qos.c这些文件都属于这一层。
这四层的关系可以用一句话概括:核心层决定时机和顺序,总线/类/域层提供中间协调,驱动层执行具体操作,设备层记录状态。理解了这个分工,你看任何 PM 相关的代码都能快速定位它属于哪一层。
2.3 回调的优先级顺序:为什么 bus 比 driver 先执行
这里有个细节很多人会搞混:当一个设备要 suspend 时,PM Core 到底先调 bus 的回调还是 driver 的回调?答案是先 bus 后 driver,resume 时反过来,先 driver 后 bus。
为什么这么设计?我用一个类比解释。假设你要关闭一栋大楼,bus 层相当于"大楼管理员",driver 层相当于"各个房间的租户"。关闭时,管理员先做准备工作(比如通知大家要关门了、切断公共区域电源),然后租户再各自收拾东西离开。开门时反过来,租户先进房间开灯开设备,管理员最后恢复公共设施。这个顺序保证了依赖关系不会出错。
具体到代码,PM Core 在device_suspend这类函数里会依次调用dev->bus->pm->suspend、dev->class->pm->suspend、dev->driver->pm->suspend(实际实现里通过pm_op宏和device_suspend的调用链完成)。如果某一层没有实现对应回调,就跳过。这个顺序是硬编码在框架里的,驱动开发者不需要也不能改变。
提示:很多驱动开发者会疑惑"我明明实现了 suspend 回调,为什么没被调用"。八成是因为你的设备没有正确注册到 bus 上,或者 bus 的 pm 回调里做了拦截。排查时先确认
dev->bus和dev->bus->pm是否为空。
3. dev_pm_ops:驱动开发者真正要写的那个结构体
3.1 结构体字段的完整拆解
struct dev_pm_ops是驱动和 PM Core 之间的契约,定义在include/linux/pm.h里。它的字段可以分成三组,我按使用频率从高到低排一下。
第一组是runtime PM 回调:runtime_suspend、runtime_resume、runtime_idle。这三个是设备运行时功耗管理的核心,后面第 4 节会详细讲。
第二组是system suspend/resume 回调:suspend、resume、freeze、thaw、poweroff、restore。这六个对应系统级睡眠的不同阶段。suspend/resume用于常规的 STR(Suspend to RAM),freeze/thaw用于 STD(Suspend to Disk,也就是 hibernation)的前半段,poweroff/restore用于 hibernation 的后半段。
第三组是早期/晚期回调:prepare、complete、suspend_noirq、resume_noirq、freeze_noirq、thaw_noirq、poweroff_noirq、restore_noirq。这些是给有特殊时序要求的设备用的,比如需要在中断关闭前后做特定操作的设备。普通驱动基本用不到。
我列个表帮你对照记忆:
| 回调组 | 字段 | 典型使用场景 |
|---|---|---|
| runtime PM | runtime_suspend/resume/idle | 运行时动态省电,如 I2C 空闲时关时钟 |
| system suspend | suspend/resume | STR 睡眠,保存/恢复设备上下文 |
| hibernation | freeze/thaw/poweroff/restore | STD 睡眠,分两阶段处理 |
| noirq 阶段 | *_noirq | 中断关闭后的特殊时序操作 |
| 早期晚期 | prepare/complete | 需要早于/晚于常规阶段的操作 |
3.2 用 SET_RUNTIME_PM_OPS 还是手写赋值
定义dev_pm_ops有两种写法。一种是手写结构体初始化,另一种是用SET_RUNTIME_PM_OPS、SET_SYSTEM_SLEEP_PM_OPS这些宏。我强烈建议用宏,原因有两个。
第一个原因是版本兼容性。内核在不同版本里给dev_pm_ops加过字段,手写初始化如果用了指定初始化器(designated initializer)还好,如果用位置初始化就会出问题。宏会帮你处理这些差异。
第二个原因是可读性。用宏写出来的代码一眼就能看出这个驱动支持哪些功耗特性。比如:
static const struct dev_pm_ops mydev_pm_ops = { SET_RUNTIME_PM_OPS(mydev_runtime_suspend, mydev_runtime_resume, NULL) SET_SYSTEM_SLEEP_PM_OPS(mydev_suspend, mydev_resume) };这段代码清楚地表明:这个驱动支持 runtime PM 和 system suspend,不支持 runtime idle。如果手写,你得在几十个字段里找哪些被赋值了,很容易看漏。
注意:
SET_SYSTEM_SLEEP_PM_OPS在配置了CONFIG_PM_SLEEP时才会展开成实际的 suspend/resume 赋值,否则展开为空。这意味着如果你的驱动依赖 system suspend 回调,一定要确认内核配置里开了CONFIG_PM_SLEEP,否则回调永远不会被调用,而且不会有任何编译警告。
3.3 回调里到底该写什么:一个真实驱动的拆解
光看结构体定义还是抽象,我拿一个典型的 I2C 传感器驱动举例,看看每个回调里实际该写什么。
runtime_suspend里通常做三件事:保存必要的寄存器上下文(如果硬件掉电会丢失)、关闭设备时钟、把设备引脚设为低功耗状态。注意顺序——必须先保存上下文再关时钟,否则你可能读不到寄存器。runtime_resume里做相反的事:恢复时钟、恢复寄存器、把引脚设回工作状态。
suspend回调(system suspend 用)比runtime_suspend更重,因为系统睡眠时供电可能完全切断。这里除了保存寄存器,还要保存那些"恢复后必须重新配置"的东西,比如中断使能状态、DMA 配置、FIFO 阈值等。resume里要完整地重新初始化设备,不能假设硬件还保持着睡眠前的状态。
这里有个经验:很多驱动在 resume 后工作不正常,根源是 resume 回调里漏了某个初始化步骤。我的做法是在 resume 回调里调用和 probe 相同的初始化函数(把 probe 里的硬件初始化部分抽成一个独立函数),这样能保证恢复后的状态和刚 probe 时一致。代价是 resume 会慢一点,但正确性优先。
4. Runtime PM:引用计数驱动的动态省电
4.1 引用计数机制的本质
Runtime PM 的核心是引用计数。每个设备有一个power.usage_count计数器,驱动在需要使用设备时调用pm_runtime_get_sync增加计数,用完调用pm_runtime_put_sync减少计数。当计数从 1 减到 0 时,PM Core 会触发runtime_idle回调,如果 idle 回调返回 0(表示可以挂起),就进一步调用runtime_suspend。
这个机制的设计意图很明确:让驱动用"声明式"的方式表达设备使用状态,而不是自己判断什么时候该省电。驱动只管说"我现在要用"和"我用完了",至于用完多久后挂起、挂起时做什么,交给框架和回调处理。这样多个驱动共享一个设备时(比如多个 I2C 从设备共享一个 I2C 控制器),引用计数能自动协调,不会出现一个驱动想挂起、另一个还在用的冲突。
我见过不少驱动开发者图省事,在 probe 里get一次就再也不put,理由是"这个设备一直在用"。这在功能上没问题,但等于放弃了 runtime PM 的省电收益。正确的做法是在每次实际访问硬件前后配对 get/put,让框架有机会在两次访问的间隙把设备挂起。
4.2 get_sync 和 get_noresume 的区别,以及什么时候用哪个
pm_runtime_get_sync和pm_runtime_get_noresume是两个容易混淆的函数。区别在于:get_sync会增加计数并且确保设备处于 resume 状态(如果当前是 suspended,会同步等待 resume 完成);get_noresume只增加计数,不保证设备被唤醒。
什么时候用get_noresume?典型场景是在中断上下文或者不能睡眠的上下文里。因为get_sync可能触发 resume 回调,而 resume 回调里可能有睡眠操作(比如 I2C 传输),在原子上下文里调用会出问题。这时候用get_noresume先占住计数,然后在可以睡眠的上下文里再调用pm_runtime_resume真正唤醒设备。
还有一个函数pm_runtime_get(不带后缀),它增加计数并安排一个异步 resume,不等待完成。这个用在"我知道设备马上要用,但不急着等它醒"的场景。实际项目里get_sync用得最多,get_noresume次之,get最少。
4.3 autosuspend:延迟挂起的艺术
如果设备频繁地 get/put,比如每秒几十次,那么每次 put 到 0 就立即 suspend、下次 get 又立即 resume,光是 suspend/resume 的开销就抵消了省电收益。这就是autosuspend机制要解决的问题。
驱动可以调用pm_runtime_set_autosuspend_delay(dev, delay_ms)设置一个延迟,然后pm_runtime_use_autosuspend(dev)启用。这样当计数减到 0 时,框架不会立即 suspend,而是等delay_ms毫秒。如果在这段时间内有新的 get,就取消挂起,设备保持 active。只有超过延迟还没有新请求,才真正 suspend。
延迟设多少合适?这取决于设备的 suspend/resume 开销和使用模式。经验值是:延迟设为 suspend+resume 总耗时的 2 到 5 倍。比如你的设备 suspend 要 1ms、resume 要 2ms,总耗时 3ms,那延迟设 6 到 15ms 比较合理。设太短省电收益小,设太长设备长时间不挂起也费电。实际调优时可以用 ftrace 跟踪runtime_suspend和runtime_resume的调用频率,根据实际访问模式来定。
提示:启用 autosuspend 后,
pm_runtime_put_sync的行为会变成"减少计数并安排延迟挂起",而不是立即挂起。如果你在某个路径上确实需要立即挂起,用pm_runtime_put_sync_suspend。这个细节坑过不少人。
5. System Suspend:从用户敲下命令到设备真正睡下
5.1 suspend 的完整阶段划分
用户执行echo mem > /sys/power/state之后,内核里发生的事远比想象中复杂。PM Core 把整个 suspend 过程分成多个阶段,每个阶段有明确的职责。
第一阶段是 prepare。这个阶段在冻结用户进程之前执行,主要做两件事:一是让驱动有机会分配内存(因为后续阶段不能分配),二是让驱动可以访问用户空间。很多驱动在这里申请一块"恢复时要用"的内存。
第二阶段是 freeze。冻结所有用户进程和内核线程(除了冻结相关的),确保没有新的硬件访问请求进来。这一步是后续所有操作的前提——如果还有进程在访问设备,suspend 就没法安全进行。
第三阶段是 suspend(常规阶段)。这是主战场,PM Core 按设备树顺序(从叶子到根)依次调用每个设备的suspend回调。注意顺序:先挂起子设备,再挂起父设备,因为子设备依赖父设备提供的资源(比如时钟、电源),父设备先挂了子设备就没法正常操作了。
第四阶段是 suspend_noirq。中断已经关闭,只剩单核在跑。这个阶段给那些"必须在中断关闭后操作"的设备用,比如需要关闭中断控制器的设备。
第五阶段是进入低功耗。调用平台相关的代码,真正让 SoC 进入低功耗状态。
resume 过程完全反过来:先 resume_noirq,再 resume,再 thaw 进程,最后 complete。整个流程的对称性很强,理解了 suspend 就理解了 resume。
5.2 设备挂起顺序为什么是"子先父后"
这个顺序问题值得单独讲,因为它是最容易出 bug 的地方。PM Core 维护了一个dpm_list,设备按依赖关系排序。排序规则是:如果一个设备是另一个设备的子节点(通过device.parent或dev_pm_domain关联),子节点排在父节点前面。
为什么?因为子设备要访问硬件,往往需要父设备提供的资源。比如一个挂在 I2C 控制器下的传感器,它要读写寄存器必须通过 I2C 控制器。如果父设备(I2C 控制器)先 suspend 了,子设备(传感器)的 suspend 回调里如果还要访问寄存器就会失败。所以必须先让子设备完成 suspend,父设备再 suspend。
反过来 resume 时,父设备必须先 resume,把资源准备好,子设备才能 resume。这个顺序是框架自动维护的,驱动开发者不需要手动排序,但必须保证设备之间的父子关系被正确建立。如果你的驱动没有正确设置device.parent或者没有通过dev_pm_domain关联,框架就排不出正确顺序,suspend 时可能随机失败。
5.3 一个真实的 suspend 失败排查过程
我遇到过一个案例:系统 suspend 时卡在某个 USB 设备上,日志显示dpm_run_callback(): usb_dev_suspend+0x0/0x20 returns -16,错误码 -16 是-EBUSY。这个错误说明 USB 设备的 suspend 回调返回了"忙"。
排查思路是这样的。首先确认是哪个 USB 设备,通过dmesg里的设备名定位到是一个 USB 网卡。然后看它的 suspend 回调为什么返回 EBUSY——通常是设备还有未完成的传输。进一步查发现,用户空间有个进程在持续 ping,导致网卡一直有数据要发。但问题是,suspend 流程里进程应该已经被 freeze 了,为什么还有传输?
最后定位到是网卡驱动自己的一个内核线程在周期性发送心跳包,这个线程没有被正确冻结。修复方法是在驱动的 suspend 回调里先停掉这个内核线程,resume 时再启动。这个案例说明:suspend 失败往往不是 PM Core 的问题,而是驱动没有正确处理自己的后台活动。排查时要从"谁在阻止设备进入低功耗"这个角度切入,而不是盯着 PM Core 的代码看。
6. PM Domain:表达设备间电源依赖的正确方式
6.1 为什么 bus 和 class 不够用
前面讲了 bus 层和 class 层可以定义功耗回调,但它们表达的是"同类设备"或"同总线设备"的共性行为,无法表达跨总线、跨类的电源依赖。举个典型例子:一个 SoC 上有多个电源域(power domain),每个域里有 CPU 核、GPU、显示控制器、各种外设。这些设备分属不同的 bus 和 class,但它们共享同一个电源开关。只有当域里所有设备都空闲了,域本身才能断电。
这种"多对一"的依赖关系,bus 和 class 都表达不了,必须引入PM Domain(struct dev_pm_domain)。PM Domain 是一个逻辑概念,代表一组共享电源资源的设备。每个设备可以通过dev->pm_domain指向它所属的域。当域里所有设备都 suspend 后,域的power_off回调被调用,真正切断电源。
6.2 genpd:通用电源域框架
内核里最常用的 PM Domain 实现是genpd(generic power domain),代码在drivers/base/power/domain.c。genpd 提供了一套完整的电源域管理机制,包括域的注册、设备的添加、引用计数、状态机等。
genpd 的核心是struct generic_pm_domain,里面有几个关键字段:power_off和power_on回调(真正操作硬件开关)、states数组(定义域支持的低功耗状态)、device_count(域内设备数)、device_links(设备间的依赖链接)。设备通过dev_pm_domain_set或者设备树里的power-domains属性关联到域。
genpd 的状态机比 runtime PM 更复杂,因为它要处理"域内部分设备活跃、部分空闲"的情况。比如域里有 3 个设备,2 个空闲 1 个活跃,域不能断电;等第 3 个也空闲了,域才能断电。这个判断由 genpd 的genpd_power_off逻辑完成,它会检查所有设备的引用计数和状态。
6.3 设备链接:表达"我依赖你"的机制
除了父子关系,设备之间还可能有更复杂的依赖,比如"设备 A 要工作,设备 B 必须处于 active 状态,但 A 不是 B 的子设备"。这种关系用device link(struct device_link)表达。
Device link 有两种方向:DL_FLAG_STATELESS表示无状态依赖(A 活跃时 B 必须活跃,但 A 空闲时 B 不一定空闲),DL_FLAG_PM_RUNTIME表示 runtime PM 层面的依赖。创建链接用device_link_add,删除用device_link_del。
实际项目里,device link 常用于表达"消费者-供应者"关系。比如一个显示控制器(消费者)依赖一个时钟控制器(供应者),显示控制器要工作时,时钟控制器必须 active。通过 device link,PM Core 在 suspend 显示控制器时会自动确保时钟控制器还活着,resume 时也会按正确顺序恢复。
注意:device link 如果创建后忘记删除,会导致设备永远无法进入低功耗,因为框架认为还有依赖存在。这是我在实际项目中踩过的坑——一个动态创建的 link 在设备移除时没清理,结果整个电源域都下不来。排查时用
ls /sys/kernel/debug/devices_deferred和cat /sys/kernel/debug/pm_genpd/*/devices能看到域内设备和链接状态。
7. 调试功耗问题的实战工具箱
7.1 用 ftrace 跟踪 PM 回调调用链
调功耗问题,ftrace 是第一利器。PM Core 里几乎所有关键函数都有 tracepoint,可以直接打开跟踪。
最常用的几个:
# 跟踪 runtime PM 的 suspend/resume 调用 echo 1 > /sys/kernel/debug/tracing/events/power/runtime_suspend/enable echo 1 > /sys/kernel/debug/tracing/events/power/runtime_resume/enable # 跟踪 system suspend 的各个阶段 echo 1 > /sys/kernel/debug/tracing/events/power/suspend_resume/enable # 查看结果 cat /sys/kernel/debug/tracing/traceruntime_suspend事件会记录设备名、耗时、返回值。如果你发现某个设备的 suspend 耗时特别长(比如超过 10ms),那它就是优化重点。suspend_resume事件会记录每个阶段的开始和结束,能帮你定位 suspend 卡在哪个阶段。
我通常还会配合function_graphtracer 看具体是哪个函数慢:
echo function_graph > /sys/kernel/debug/tracing/current_tracer echo 'mydev_runtime_suspend' > /sys/kernel/debug/tracing/set_graph_function这样能看到mydev_runtime_suspend内部每个子函数的耗时,精确定位瓶颈。
7.2 powertop 和 pm-graph 的配合使用
powertop是看整体功耗分布的工具,它能告诉你哪些设备、哪些进程在阻止系统进入低功耗。跑powertop --auto-tune可以自动调整一些可调的功耗参数,但生产环境慎用,因为它会改一些你不一定想改的设置。
pm-graph是更专业的 suspend/resume 分析工具,包含sleepgraph和bootgraph两个脚本。sleepgraph能生成 suspend/resume 的完整时间线,包括每个设备的回调耗时、内核线程活动、中断情况。用法:
sudo ./sleepgraph.py -m mem -g -o ./output生成的 HTML 报告非常直观,能一眼看出哪个设备拖慢了 suspend。我在排查一个 suspend 耗时 2 秒的问题时,就是靠 sleepgraph 定位到是一个老驱动的 resume 回调里做了 1.5 秒的延时等待。
7.3 常见问题速查表
我把实际项目中遇到的高频问题整理成表,方便你快速对照:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 设备无法进入 runtime suspend | 引用计数未归零 | 查power/usage_count和power/runtime_status |
| suspend 卡住不返回 | 某设备回调阻塞 | ftrace 看最后调用的回调 |
| resume 后设备不工作 | resume 回调漏初始化 | 对比 probe 和 resume 的初始化步骤 |
| 功耗比预期高 | 电源域未断电 | 查 genpd 状态和 device link |
| autosuspend 不生效 | 未调用 use_autosuspend | 查power/control是否为 auto |
查设备 runtime PM 状态最直接的方法是看 sysfs:
cat /sys/devices/.../power/runtime_status # active 或 suspended cat /sys/devices/.../power/control # on 或 auto cat /sys/devices/.../power/usage_count # 当前引用计数如果control是on,说明 runtime PM 被禁用了,设备永远不会自动挂起。如果usage_count不为 0,说明有驱动忘了 put。这两个是最常见的"设备不省电"原因。
8. 分层设计给驱动开发者的三条实操建议
聊了这么多框架细节,最后回到驱动开发者的视角,分享三条我在实际项目中总结的建议。
第一条:probe 里先建立依赖关系,再初始化硬件。很多驱动在 probe 里直接开始读写寄存器,但如果这个设备依赖某个电源域或者上游设备,而依赖关系还没建立,硬件访问可能失败或者行为异常。正确顺序是:先dev_pm_domain_set或通过设备树建立关联,再pm_runtime_enable,最后初始化硬件。这样框架在初始化阶段就能正确处理电源依赖。
第二条:suspend/resume 回调要幂等。框架在某些情况下可能重复调用 suspend 或 resume(比如 suspend 失败后回滚),如果你的回调不是幂等的,第二次调用就可能出错。判断幂等的方法很简单:连续调用两次 suspend,设备状态应该和调用一次一样。实现上可以在回调开头检查当前状态,如果已经是目标状态就直接返回。
第三条:善用 dev_pm_ops 的宏,但别滥用。宏能简化代码,但如果你只需要 runtime PM 不需要 system suspend,就别用SET_SYSTEM_SLEEP_PM_OPS把 suspend/resume 也定义了。定义了的回调框架就会调用,如果你的 suspend 回调里没做正确的事,反而会引入 bug。只实现你真正需要的回调,这是最稳妥的做法。
功耗调优这件事,框架给你提供了完整的工具和机制,但最终效果取决于驱动有没有正确使用这些机制。PM Core 的分层设计把"时机"和"操作"解耦,让驱动专注于"操作",这是它最优雅的地方。理解了这个设计哲学,你再看任何 PM 相关的代码,都能快速判断它属于哪一层、该由谁负责。