
1. Runtime PM 到底解决什么问题1.1 从手机待机耗电说起先聊一个大家都有的体验手机息屏放一晚上第二天起来发现电量掉了百分之十几。如果拆开看屏幕关了、CPU 也进了低功耗状态那电到底被谁吃了答案往往是一堆外设——摄像头模组、WiFi 芯片、触摸控制器、传感器它们明明没人用却还保持着上电和时钟静静地漏电。Runtime PMRuntime Power Management运行时电源管理就是 Linux 内核里专门收拾这类问题的一套机制。它要干的事情很朴素当一个设备暂时没人用的时候把它挂起suspend省电等下次有人要用再把它唤醒resume。注意这里的关键词是运行时——系统整体没有进入睡眠CPU 还在跑进程还在调度只是某个具体的设备被单独关掉了。这跟系统级的 suspend/resume 是两码事。系统级睡眠是整机躺平而 Runtime PM 是局部摸鱼。你可以把它理解成办公室里的工位灯整个办公室还亮着系统运行但你离开工位去开会你的台灯自动灭了设备 runtime suspend回来一坐下灯又亮了runtime resume。这个自动就是 Runtime PM 的核心价值——不需要用户手动干预由内核根据设备的使用计数自动决策。1.2 谁需要关心 Runtime PM如果你在做嵌入式 Linux 开发、手机/平板/车机的 BSP、或者任何对功耗敏感的 Linux 设备Runtime PM 是你绕不开的一环。具体来说这几类人必须吃透它驱动开发者你写的每一个外设驱动理论上都应该支持 Runtime PM否则设备永远无法进入低功耗状态。BSP/系统集成工程师你要保证整个设备的电源域划分合理设备之间的依赖关系父子、供应商-消费者配置正确。功耗优化工程师你要能看懂/sys/kernel/debug/pm_genpd、/sys/devices/.../power/下的状态定位是谁在阻止设备挂起。内核爱好者/面试准备者Runtime PM 是内核电源管理子系统的经典考点PM Core、struct device、pm_runtime_*这一套 API 是基本功。我见过太多项目驱动功能跑通了但功耗死活降不下来最后查来查去就是某个设备的 runtime PM 没配对或者引用计数泄漏导致设备永远忙。这类问题不解决产品续航直接崩盘。1.3 本文的定位这篇是Linux 内核功耗子系统系列的第七篇前面几篇分别聊了系统级睡眠、电源域genpd、OPP 等话题。这一篇专门把 Runtime PM 这个功能从头到尾梳理一遍它挂在struct device的哪个字段上、PM Core 怎么调度、驱动该怎么写、父子设备怎么联动、出问题怎么查。我不打算照搬内核文档而是按一个实际做功耗优化的人会关心的顺序来讲先讲清楚数据结构和状态机再讲驱动实操最后讲排查。看完你应该能独立给自己的驱动加上 Runtime PM并且知道出问题时从哪儿下手。2. 核心数据结构与状态机拆解2.1 struct device 里的电源管理字段Runtime PM 的所有状态都挂在struct device上。这个结构体是内核里最庞大的结构之一跟电源管理相关的字段主要有这么几个struct device { ... struct dev_pm_info power; /* 电源管理信息 */ struct dev_pm_domain *pm_domain; /* 电源域genpd 等 */ ... };真正干活的是struct dev_pm_info它定义在include/linux/pm.h里。跟 Runtime PM 直接相关的字段我挑重点列一下字段类型作用runtime_statusenum rpm_status设备当前运行时状态runtime_errorint上次 resume 失败的错误码runtime_usageatomic_t使用计数大于 0 表示忙runtime_active_kidsatomic_t处于 active 的子设备数量runtime_active_timeu64累计 active 时长统计用runtime_suspended_timeu64累计 suspended 时长统计用disable_depthint禁用深度大于 0 表示 Runtime PM 被禁用request_pendingunsigned int是否有待处理的请求deferred_resumebool是否延迟 resumeruntime_autobool是否允许自动挂起ignore_childrenbool是否忽略子设备状态no_callbacksbool是否没有回调纯计数irq_safebool回调是否可在中断上下文调用use_autosuspendbool是否启用自动挂起延迟timer_autosuspend_delayint自动挂起延迟毫秒数这里面最核心的是runtime_status、runtime_usage、disable_depth和runtime_active_kids这四个。理解了它们Runtime PM 的行为基本就懂了。2.2 五个运行时状态runtime_status是一个枚举取值有五种enum rpm_status { RPM_ACTIVE 0, /* 设备处于活动状态 */ RPM_RESUMING, /* 正在恢复中 */ RPM_SUSPENDED, /* 已挂起 */ RPM_SUSPENDING, /* 正在挂起中 */ RPM_ERROR -1, /* 出错了 */ };这五个状态构成了 Runtime PM 的状态机。设备平时在RPM_ACTIVE和RPM_SUSPENDED之间来回切换中间经过RPM_SUSPENDING和RPM_RESUMING两个过渡态。RPM_ERROR是异常态一般出现在 resume 回调返回错误之后。状态迁移的触发条件本质上是使用计数和子设备状态的变化。这里有个容易搞混的点runtime_status是结果状态而runtime_usage是决策依据。设备是否应该挂起看的是runtime_usage是否为 0、runtime_active_kids是否为 0、disable_depth是否为 0以及runtime_auto是否允许自动挂起。2.3 引用计数runtime_usage 的加减逻辑runtime_usage是 Runtime PM 的忙闲指示器。它的加减通过两个 API 完成pm_runtime_get()/pm_runtime_get_sync()计数加一表示我要用这个设备了。pm_runtime_put()/pm_runtime_put_sync()计数减一表示我用完了。当计数从 0 变成正数时如果设备当前是挂起的内核会触发 resume当计数从正数变回 0 时如果满足挂起条件内核会触发 suspend。这里有个非常关键的细节runtime_usage是原子变量但它的加减和状态迁移不是原子的。也就是说你连续调用pm_runtime_get和pm_runtime_put中间可能发生状态迁移。所以驱动里必须保证 get/put 成对出现否则计数泄漏设备永远挂不下去。我踩过最典型的坑就是在 probe 里pm_runtime_get_sync之后某条错误分支直接 return 了忘了 put。结果这个设备在整个系统生命周期里 usage 都是 1永远 active功耗下不来。这种 bug 在功能测试时完全看不出来只有测功耗才暴露。2.4 disable_depth为什么我的设备不挂起disable_depth是另一个高频背锅侠。它是一个整数大于 0 表示 Runtime PM 被禁用此时无论 usage 怎么变设备都不会挂起。static inline void pm_runtime_disable(struct device *dev) { __pm_runtime_disable(dev, true); }每次调用pm_runtime_disabledisable_depth加一调用pm_runtime_enable减一。只有减到 0Runtime PM 才真正生效。这种嵌套计数的设计是为了支持多个调用者独立控制——A 模块禁用了B 模块还能正常用只有所有人都 enable 了才恢复。常见问题驱动 probe 失败时调用了pm_runtime_disable但 remove 或重新 probe 时忘了 enable导致设备永久禁用。或者某个框架比如某些总线在初始化时 disable 了驱动没注意。2.5 父子设备的联动runtime_active_kids设备不是孤立的。一个 I2C 控制器下面挂着传感器一个 PCIe 桥下面挂着网卡这些是父子关系。Runtime PM 默认遵循一条规则父设备只有在所有子设备都挂起之后自己才能挂起。这个规则靠runtime_active_kids实现。每当一个子设备进入 active父设备的runtime_active_kids加一子设备挂起减一。父设备判断能否挂起时会检查这个计数。但有些场景下这个规则不合适。比如一个 USB 控制器它的子设备USB 设备挂起了但控制器本身可能还需要保持供电来检测热插拔。这时候可以用pm_suspend_ignore_children(dev, true)让父设备忽略子设备状态也就是设置ignore_children字段。反过来如果设备之间不是父子关系而是消费者-供应商关系比如一个设备依赖某个时钟源或 regulator那就得用设备链接device link机制这个后面实操部分再展开。3. 驱动里怎么用 Runtime PM3.1 最小可用模板给一个驱动加 Runtime PM最小骨架大概长这样static int mydrv_probe(struct platform_device *pdev) { struct device *dev pdev-dev; int ret; /* 1. 初始化硬件 */ ret mydrv_hw_init(dev); if (ret) return ret; /* 2. 使能 Runtime PM */ pm_runtime_enable(dev); /* 3. 设置自动挂起延迟可选 */ pm_runtime_set_autosuspend_delay(dev, 200); pm_runtime_use_autosuspend(dev); /* 4. 标记设备已激活允许后续自动挂起 */ pm_runtime_set_active(dev); pm_runtime_mark_last_busy(dev); pm_runtime_put_autosuspend(dev); return 0; } static int mydrv_remove(struct platform_device *pdev) { struct device *dev pdev-dev; pm_runtime_disable(dev); pm_runtime_set_suspended(dev); mydrv_hw_deinit(dev); return 0; }对应的dev_pm_opsstatic int mydrv_runtime_suspend(struct device *dev) { /* 关时钟、关电源、保存寄存器 */ clk_disable_unprepare(priv-clk); regulator_disable(priv-vdd); return 0; } static int mydrv_runtime_resume(struct device *dev) { /* 开电源、开时钟、恢复寄存器 */ regulator_enable(priv-vdd); clk_prepare_enable(priv-clk); return 0; } static const struct dev_pm_ops mydrv_pm_ops { SET_RUNTIME_PM_OPS(mydrv_runtime_suspend, mydrv_runtime_resume, NULL) };SET_RUNTIME_PM_OPS这个宏展开后就是设置runtime_suspend、runtime_resume、runtime_idle三个回调。第三个参数是 idle 回调一般传 NULL 就行除非你有特殊需求。3.2 回调里到底该做什么runtime_suspend和runtime_resume是真正省电的地方。原则很简单suspend 里关掉一切能关的resume 里恢复一切关掉的。具体包括时钟clk_disable_unprepare/clk_prepare_enable电源/稳压器regulator_disable/regulator_enable电源域如果设备在 genpd 里genpd 框架会自动处理驱动不用管引脚状态pinctrl_pm_select_sleep_state/pinctrl_pm_select_default_state寄存器上下文如果硬件掉电会丢寄存器需要保存/恢复中断某些设备挂起时需要关中断或切换唤醒源这里有个大坑回调里不能睡眠的操作要小心。默认情况下runtime PM 回调是在进程上下文执行的可以睡眠。但如果设置了pm_runtime_irq_safe(dev)回调就可能在中断上下文执行此时clk_prepare、regulator_enable这类可能睡眠的调用就不能用了。所以除非你非常确定否则不要开irq_safe。另一个坑是回调的返回值。runtime_suspend返回非 0 会导致设备进入RPM_ERROR状态后续所有 resume 都会失败。所以 suspend 里如果某个操作可能失败要么保证它不会失败要么在失败时回滚已经做的操作并返回 0假装挂起了要么就老老实实返回错误但确保能恢复。3.3 get/put 的配对艺术驱动里最考验功力的就是 get/put 的配对。我总结几条实战规则规则一谁用谁 get用完就 put。在访问硬件寄存器之前 get访问完 put。不要在整个函数开头 get、结尾 put因为中间可能有 return。规则二用pm_runtime_get_sync而不是pm_runtime_get。前者会等待 resume 完成保证你拿到设备时它一定是 active 的。后者只是加计数不等待如果你紧接着访问硬件可能设备还没 resume 完。规则三错误分支别忘了 put。这是最常见的泄漏点。建议用goto err_put这种统一出口的写法。规则四中断上下文用pm_runtime_get非 sync。因为 sync 版本会睡眠等待中断里不能用。但非 sync 版本不保证设备已 resume所以中断处理里通常只做计数实际硬件访问放到工作队列或线程化中断里。规则五probe 里的初始 get 要小心。很多驱动在 probe 里 get 一次保持设备常开直到 remove 才 put。这本身没错但如果你希望设备能自动挂起就得在 probe 末尾 put 掉。3.4 自动挂起autosuspend 机制如果每次 put 都立刻挂起设备会在频繁访问时反复 suspend/resume反而更费电因为 suspend/resume 本身有开销。autosuspend 就是解决这个问题的put 之后不立即挂起而是等一个延迟时间如果这段时间内没有新的 get才真正挂起。用法pm_runtime_set_autosuspend_delay(dev, 200); /* 200ms */ pm_runtime_use_autosuspend(dev); /* 每次访问完 */ pm_runtime_mark_last_busy(dev); pm_runtime_put_autosuspend(dev);pm_runtime_mark_last_busy会更新最后忙碌时间autosuspend 定时器从这个时间点开始算。延迟设多少合适取决于设备的访问模式。如果设备每秒被访问几十次延迟设 100~500ms 比较合适如果访问很稀疏设 0 或很小的值也行。我一般会先用一个保守值比如 200ms跑起来然后用 ftrace 看 suspend/resume 的频率再调整。如果发现 suspend 次数远多于实际访问次数说明延迟太小如果设备长时间不挂起说明延迟太大或者有计数泄漏。4. 父子设备与依赖关系处理4.1 父子联动的默认行为前面提到父设备默认要等所有子设备挂起后才能挂起。这个行为在大多数情况下是对的比如一个 SPI 控制器它的子设备SPI 从设备还在传输数据控制器当然不能挂起。但这里有个细节子设备的 active 状态是通过runtime_active_kids传递给父设备的而不是通过runtime_usage。也就是说子设备 active 不会增加父设备的 usage 计数只会增加runtime_active_kids。父设备判断能否挂起时两个条件都要满足usage 为 0 且 active_kids 为 0。这个设计的好处是父设备可以区分我自己被用了和我的子设备被用了。如果只是子设备被用父设备可以在子设备挂起后立即挂起不需要额外的延迟。4.2 ignore_children 的使用场景有些父设备不希望被子设备拖住。典型场景USB 控制器USB 设备挂起了但控制器要保持供电以检测新设备插入。I2C 控制器从设备挂起了但控制器可能还要响应其他从设备。电源域控制器它管理一堆子设备但自己不应该被子设备状态影响。用法pm_suspend_ignore_children(dev, true);设置之后父设备的ignore_children为 true判断挂起条件时就不看runtime_active_kids了。但要注意ignore_children 不是随便设的。如果父设备挂起会导致子设备无法工作那就不能设。比如一个 I2C 控制器挂起后它的从设备就没法通信了这时候设 ignore_children 就是错的。4.3 设备链接处理非父子依赖现实中的依赖关系往往不是树形的。比如一个显示控制器依赖一个时钟源但时钟源不是它的父设备。一个音频编解码器依赖一个 regulatorregulator 也不是它的父设备。一个传感器依赖一个 GPIO 控制器来产生中断。这些消费者-供应商关系用父子关系表达不了得用设备链接device link。设备链接分两种普通链接stateless只影响 Runtime PM 的挂起顺序供应商在消费者挂起前不能挂起。有状态链接stateful除了挂起顺序还要求供应商在消费者 active 时保持 active。创建链接的 API/* 在消费者驱动里创建 */ struct device_link *link; link device_link_add(consumer, supplier, DL_FLAG_PM_RUNTIME | DL_FLAG_STATELESS); if (!link) { /* 处理失败 */ }DL_FLAG_PM_RUNTIME表示这个链接参与 Runtime PM。DL_FLAG_STATELESS表示无状态链接。如果不加这个标志就是有状态链接。设备链接的引用计数由内核管理消费者驱动 remove 时链接会自动删除不用手动清理。但如果消费者驱动在运行中动态创建链接要注意在合适的时机删除。4.4 电源域genpd与 Runtime PM 的关系电源域generic power domaingenpd是比设备更高一层的电源管理抽象。一个电源域包含多个设备域内所有设备都挂起后整个域可以断电。genpd 和 Runtime PM 是协同工作的设备的 runtime suspend 回调执行完后genpd 框架会检查域内是否所有设备都挂起了如果是就关闭整个域的电源。设备 resume 时genpd 先给域上电再调用设备的 runtime resume 回调。对驱动开发者来说好消息是大部分情况下你不需要直接操作 genpd。你只要正确实现 runtime PM 回调genpd 框架会自动接管。但你需要知道设备在哪个电源域是由设备树power-domains属性或平台代码指定的。如果设备在 genpd 里runtime suspend 回调里通常不需要手动关电源genpd 会做。调试时可以通过/sys/kernel/debug/pm_genpd/查看每个域的状态和域内设备。5. 调试与问题排查实录5.1 先看 sysfs 和 debugfs排查 Runtime PM 问题第一步永远是看状态。sysfs 里每个设备都有/sys/devices/.../power/目录里面有文件含义runtime_status当前状态active/suspended/...runtime_usage使用计数runtime_active_kidsactive 子设备数runtime_enabled是否启用enabled/disabledcontrolauto/on控制是否允许自动挂起autosuspend_delay_ms自动挂起延迟比如你想知道某个 I2C 设备为什么不挂起cat /sys/bus/i2c/devices/1-0050/power/runtime_status cat /sys/bus/i2c/devices/1-0050/power/runtime_usage cat /sys/bus/i2c/devices/1-0050/power/runtime_enabled如果runtime_usage是 1说明有地方 get 了没 put如果runtime_enabled是 disabled说明disable_depth大于 0如果runtime_status是 active 但 usage 是 0可能是runtime_active_kids不为 0或者control是 on。debugfs 里还有更详细的信息cat /sys/kernel/debug/pm_genpd/pm_genpd_summary这个会列出所有电源域、域内设备、每个域的状态和子域关系。定位谁在阻止挂起非常有用。5.2 常见问题速查表我把这些年遇到的 Runtime PM 问题整理成一张表现象可能原因排查方法设备永远 activeusage 计数泄漏看runtime_usage检查 get/put 配对设备永远 activedisable_depth 0看runtime_enabled检查 enable/disable 配对设备永远 activeactive_kids 0看runtime_active_kids检查子设备设备永远 activecontrol 是 onecho auto control恢复自动suspend 回调不执行回调没注册检查dev_pm_ops是否正确赋值suspend 回调不执行设备没使能 PM检查pm_runtime_enable是否调用resume 失败回调返回错误看 dmesg检查runtime_error频繁 suspend/resumeautosuspend 延迟太小调大autosuspend_delay_ms系统睡眠失败设备 runtime PM 状态不一致检查runtime_status和系统睡眠的交互5.3 用 ftrace 追踪状态迁移sysfs 只能看当前状态要看状态迁移的历史得用 ftrace。内核里 Runtime PM 有专门的 tracepointcd /sys/kernel/debug/tracing echo 1 events/rpm/enable echo 1 tracing_on # 复现问题 cat tracerpm事件组里有rpm_suspend、rpm_resume、rpm_idle、rpm_return_int等事件能看到每次状态迁移的设备名、状态、耗时。这个对分析为什么这个设备反复挂起又恢复特别有用。我一般会配合function_graph追踪器看 suspend 回调里哪个操作最耗时echo function_graph current_tracer echo mydrv_runtime_suspend set_graph_function5.4 几个我踩过的坑坑一probe 里 set_active 的顺序。pm_runtime_set_active必须在pm_runtime_enable之前或之后答案是如果设备初始状态是 active应该先pm_runtime_set_active再pm_runtime_enable。顺序反了会导致状态不一致设备可能被误判为 suspended。坑二remove 里忘了 disable。如果 remove 时没调pm_runtime_disable而设备恰好处于 suspended 状态后续访问硬件会失败。而且如果驱动重新 probe状态会乱。坑三中断里用 sync 版本。pm_runtime_get_sync会睡眠中断上下文调用会报 scheduling while atomic。中断里只能用pm_runtime_get然后通过工作队列做实际硬件访问。坑四autosuspend 和 system suspend 的交互。系统进入 suspend 时如果设备处于 autosuspend 延迟期内内核会取消延迟并立即挂起。但如果驱动在 system suspend 回调里又调了pm_runtime_get可能导致系统 suspend 失败。这个交互比较复杂建议 system suspend 回调里不要碰 runtime PM 计数。坑五设备链接的循环依赖。如果 A 依赖 BB 又依赖 ARuntime PM 会死锁。创建链接前一定要理清依赖图确保是 DAG有向无环图。6. 从零给一个驱动加 Runtime PM 的完整流程6.1 准备工作确认硬件能力动手之前先确认三件事硬件是否支持单独关断设备有没有独立的时钟门控、电源开关如果硬件上就没法单独关Runtime PM 加了也没用。设备在哪个电源域查设备树或平台代码确认power-domains属性。依赖关系设备依赖哪些时钟、regulator、GPIO这些依赖在 suspend 时要不要一起处理6.2 分步实现第一步定义 dev_pm_ops。实现runtime_suspend和runtime_resume挂到dev_pm_ops上。第二步在 probe 里使能。按pm_runtime_enable→pm_runtime_set_active→pm_runtime_mark_last_busy→pm_runtime_put_autosuspend的顺序。第三步在硬件访问点加 get/put。每个访问寄存器的函数开头pm_runtime_get_sync结尾pm_runtime_put_autosuspend。第四步在 remove 里清理。pm_runtime_disable→pm_runtime_set_suspended。第五步测试。用 sysfs 确认设备能正常挂起和恢复用 ftrace 确认 suspend/resume 次数合理。6.3 验证清单加完之后按这个清单逐项验证[ ]runtime_enabled是 enabled[ ] 空闲时runtime_status变成 suspended[ ] 访问设备时runtime_status变回 active[ ]runtime_usage在空闲时回到 0[ ]runtime_active_kids在子设备挂起后回到 0[ ] suspend/resume 回调被正确调用ftrace 确认[ ] 系统 suspend/resume 正常[ ] 功耗实测有下降6.4 一个真实案例之前做一个传感器驱动功能都正常但整机待机功耗比预期高 20mA。查下来发现传感器一直 active。看runtime_usage是 1说明有泄漏。翻代码发现在中断处理里调了pm_runtime_get_sync但中断是边沿触发有时候一次数据上报会触发两次中断第二次中断里 get 之后因为缓冲区满直接 return 了没 put。改成在中断里只做计数、实际处理放工作队列并且用pm_runtime_get非 sync问题解决。这个案例的教训是中断上下文里的 get/put 特别容易泄漏因为中断路径多、return 点多。能用工作队列就用工作队列把 get/put 放在进程上下文里逻辑清晰得多。7. 几个容易混淆的概念澄清7.1 Runtime PM vs System Sleep这两个经常被混为一谈。简单区分Runtime PM单个设备级别的电源管理系统还在运行由设备使用情况自动触发。System Sleep整机级别的睡眠suspend-to-RAM、suspend-to-disk由用户或系统策略触发。它们的关系是系统睡眠时所有设备会先被 runtime suspend如果还没挂起然后系统进入睡眠。系统唤醒时设备按需 runtime resume。驱动里dev_pm_ops同时包含 runtime 回调和 system 回调。如果两者逻辑相同可以用SET_SYSTEM_SLEEP_PM_OPS和SET_RUNTIME_PM_OPS分别设置也可以用UNIVERSAL_DEV_PM_OPS宏统一处理。7.2 pm_runtime_get vs pm_runtime_get_syncpm_runtime_get只加计数不等待 resume 完成。可能在设备还没 resume 时返回。pm_runtime_get_sync加计数并等待 resume 完成。返回时设备一定是 active 的。进程上下文用 sync中断上下文用非 sync。这是铁律。7.3 pm_runtime_put vs pm_runtime_put_sync vs pm_runtime_put_autosuspendpm_runtime_put减计数如果计数归零则触发挂起异步。pm_runtime_put_sync减计数并等待挂起完成。pm_runtime_put_autosuspend减计数如果计数归零则启动 autosuspend 定时器延迟后挂起。一般用pm_runtime_put_autosuspend配合 autosuspend 机制。只有在需要立即挂起且不介意等待时才用 sync 版本。7.4 runtime_idle 回调runtime_idle是可选回调在设备 usage 归零、准备挂起之前调用。默认行为是直接触发 suspend。如果驱动需要自定义比如延迟挂起、检查额外条件可以实现这个回调。大部分驱动不需要实现runtime_idle用 autosuspend 就够了。只有在 autosuspend 满足不了需求时才考虑。8. 进阶话题与性能考量8.1 suspend/resume 的开销Runtime PM 不是免费的。每次 suspend/resume 都有开销关时钟、关电源、保存恢复寄存器、可能还有固件交互。如果设备访问频繁频繁 suspend/resume 反而增加总功耗。判断标准如果 suspendresume 的能耗大于空闲时保持 active 的能耗就不该频繁挂起。这个平衡点因硬件而异需要实测。autosuspend 延迟就是用来找这个平衡点的。延迟设得太小频繁挂起设得太大空闲时还在耗电。我一般会做一组实验固定访问频率测不同延迟下的总功耗选最低点。8.2 唤醒源配置设备挂起后如果需要它能唤醒系统比如按键、网卡收到包要配置唤醒源device_set_wakeup_capable(dev, true); device_set_wakeup_enable(dev, true); enable_irq_wake(irq);注意device_set_wakeup_enable和enable_irq_wake是两回事前者告诉电源管理框架这个设备能唤醒系统后者告诉中断控制器这个中断能唤醒 CPU。两个都要配。8.3 与 CPU 空闲、调频的协同Runtime PM 只管设备CPU 本身的功耗由 CPUIdle 和 CPUFreq 管。三者协同才能达到最优功耗。比如一个视频解码场景解码器 active 时CPU 可能空闲此时 CPU 进深睡眠解码器保持工作解码器空闲时它自己挂起CPU 也空闲。调试整机功耗时要同时看这三块。/sys/kernel/debug/pm_genpd/、/sys/kernel/debug/cpuidle/、/sys/kernel/debug/opp/都是常用入口。8.4 多核与并发Runtime PM 的计数是原子的但状态迁移有锁保护dev-power.lock。多核并发调用 get/put 是安全的但要注意不要在持锁的情况下调用可能睡眠的 PM 回调。中断上下文和进程上下文同时操作同一个设备时用非 sync 版本避免睡眠。如果设备被多个 CPU 共享确保 get/put 的配对在所有路径上都正确。9. 写在最后的一点经验Runtime PM 这套机制看文档觉得简单真上手写驱动才发现坑都在细节里。我做了这么多年功耗优化最大的体会是Runtime PM 的问题九成出在计数和状态的一致性上。功能测试测不出来只有功耗测试和长时间稳定性测试才暴露。所以我的建议是给驱动加 Runtime PM 时一定要配套做三件事一是用 sysfs 定期检查runtime_usage和runtime_status二是用 ftrace 记录状态迁移分析挂起频率是否合理三是做长时间至少 24 小时的稳定性测试看有没有计数缓慢泄漏。另外别指望一次就调对。autosuspend 延迟、ignore_children、设备链接这些参数都需要根据实际硬件和访问模式反复调整。先让功能跑通再优化功耗最后做稳定性验证这个顺序不能乱。如果你在排查时发现某个设备死活不挂起先别急着改代码去/sys/kernel/debug/pm_genpd/pm_genpd_summary看一眼十有八九能直接定位到是哪个设备或哪个域在阻止。这个文件我用了无数次比任何调试手段都直接。