ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux功耗子系统分层设计:PM Core与设备驱动契约解析

Linux功耗子系统分层设计:PM Core与设备驱动契约解析 1. 从一次待机功耗异常说起PM Core 到底管什么几年前接手一个 ARM 平台的工控板项目整机在待机状态下比预期多耗了将近 80mW。硬件同事反复测了电源树确认各路 DCDC 的静态电流都正常最后问题落到软件侧。我那时候对 Linux 功耗子系统的理解还停留在调调cpuidle的 governor、改改opp表的水平结果用ftrace抓了一遍suspend流程才发现设备在进入系统级休眠之前有一批驱动压根没走runtime PM的引用计数释放导致PM Core认为设备仍然活跃整个system suspend被推迟了好几百毫秒期间 CPU 一直卡在较高的频率上。那次排查让我意识到一件事功耗问题很少是某一个驱动写错了而是整个分层框架里某一层的契约没被遵守。Linux 的功耗子系统Power Management Subsystem之所以设计成分层结构本质上就是为了让策略和机制解耦——上层决定什么时候省电下层决定怎么省电中间由 PM Core 做调度和仲裁。这个分层如果理解不透你调任何单个参数都是盲人摸象。这篇内容面向的是已经能看懂基本内核代码、做过驱动开发或者系统移植的工程师尤其是做嵌入式、移动端、工控设备的同行。我会从 PM Core 这个中枢切入把整个功耗框架的分层设计拆开讲清楚每一层负责什么、层与层之间的接口是什么、为什么这么分、以及在实际项目里怎么顺着这个分层去定位问题。关键词里的 Linux、PM Core、功耗子系统、内核、分层设计基本就是本文的主线。需要先说明的是本文涉及的代码结构以主流内核版本5.x 到 6.x 区间为参考不同版本在细节上会有差异但分层思想是稳定的。我不会贴大段源码而是把关键结构体和调用关系讲明白方便你对照自己手上的代码去看。2. PM Core 在功耗框架里的坐标它既不是策略也不是驱动2.1 一个容易被误解的定位很多人第一次接触drivers/base/power/这个目录时会以为 PM Core 就是功耗功能的实现。其实不是。PM Core 更像是一个仲裁者和协调者它自己不决定该不该休眠也不直接操作硬件寄存器它做的是三件事维护设备与电源管理相关的状态dev-power这一坨结构提供统一的回调调用框架dev_pm_ops里的suspend、resume、runtime_suspend等在系统级电源状态切换时按正确的顺序遍历设备树调用各驱动的回调。换句话说PM Core 是规则制定者 流程调度者真正的省电动作是驱动和平台代码干的。理解这一点非常关键因为它决定了你排查问题的方向如果功耗没降下去先别怀疑 PM Core先看驱动有没有正确实现回调、有没有正确管理引用计数。2.2 分层视角下的四个角色把整个功耗框架按职责纵向切开大致是这么四层层次代表组件核心职责策略层cpuidlegovernor、cpufreqgovernor、用户空间接口决定何时进入低功耗状态框架层PM Coredrivers/base/power/、device_pm、dpm_list仲裁、排序、统一回调入口平台/SoC 层platform_suspend_ops、soc_ops、PSCI、ACPI提供怎么进入的底层操作设备驱动层各dev_pm_ops实现保存/恢复上下文、关闭时钟和电源域这四层里PM Core 处在中间向上给策略层提供设备状态信息向下给驱动层规定回调契约。它不越界但它是所有跨层交互的必经之路。你调echo mem /sys/power/state的时候最终触发的那条调用链就是穿过这四层的。2.3 为什么必须分层一个反例假设没有分层让每个驱动自己决定什么时候关电、什么时候休眠会发生什么最直接的问题是顺序。一个 USB 控制器要休眠必须先让它下面的 PHY 进入低功耗再关控制器的时钟而 PHY 的电源又可能来自某个 PMICPMIC 的 I2C 通信又依赖某个 GPIO 控制器还活着。这些依赖关系如果让驱动各自为政几乎必然出现先关了电源后面还想通信的死锁。分层之后PM Core 通过dpm_list这个全局设备链表按照先子后父的顺序做 suspend、反序做 resume把顺序问题集中管理。这就是分层的价值把复杂的全局约束收敛到一个地方让每个驱动只需要关心自己那一小块。3. 设备模型与 dpm_listPM Core 的调度骨架3.1 dpm_list 是怎么排出来的PM Core 的核心数据结构之一是dpm_list它是一个全局链表记录了所有参与电源管理的设备。这个链表不是随便排的它的顺序由设备在设备树/设备模型中的父子关系和注册顺序共同决定。设备注册时device_pm_add()会把它挂到dpm_list的尾部。但真正决定 suspend 顺序的是dpm_list在系统 suspend 前会被dpm_prepare()和dpm_suspend()重新整理。核心规则是子设备排在父设备前面。这样 suspend 时从链表头往尾走就是先挂起子设备、再挂起父设备resume 时反过来先恢复父设备、再恢复子设备。这个规则听起来简单但实际项目里出问题往往就出在父子关系没建对。比如一个 I2C 从设备如果它的parent没有正确指向 I2C 控制器PM Core 就不知道它俩的依赖关系suspend 顺序就可能错乱表现为resume 时 I2C 通信超时。3.2 设备电源状态与运行时状态的区别这里必须区分两组概念很多新手会混淆系统级电源状态system suspend如mem、standby、freeze影响的是整机运行时电源状态runtime PM影响的是单个设备与整机状态无关。PM Core 对这两套流程都有支持但走的是不同的路径。系统级 suspend 走dpm_suspend()系列运行时 PM 走pm_runtime_*()系列。两者共享dev_pm_ops里的回调但触发时机和上下文完全不同。注意runtime PM的回调runtime_suspend/runtime_resume可能在中断上下文之外被调用而系统级 suspend 的回调在 suspend 线程里执行两者的约束不一样。写驱动时不能假设它们可以互换。3.3 引用计数runtime PM 的命门runtime PM靠引用计数usage_count来决定设备是否可以进入低功耗。每次pm_runtime_get()让计数加一pm_runtime_put()减一减到零且没有其他阻塞条件时PM Core 才会尝试调用runtime_suspend。我踩过的最典型的坑就是引用计数泄漏某个错误处理分支里pm_runtime_get_sync()之后直接return忘了put。结果设备永远忙永远不休眠。这种问题在功能测试时完全看不出来只有测功耗才会暴露。排查手段是看/sys/kernel/debug/pm_genpd/或者各设备的runtime_status和runtime_usage节点。4. dev_pm_ops驱动与 PM Core 之间的契约4.1 回调集合的全貌dev_pm_ops是驱动向 PM Core 注册的回调集合主要成员包括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 (*runtime_suspend)(struct device *dev); int (*runtime_resume)(struct device *dev); int (*runtime_idle)(struct device *dev); };这一堆回调不是随便调的它们对应系统 suspend 的不同阶段。prepare在最早调用用来做可能失败的前置检查suspend是主回调suspend_late在中断关闭后调用用来做最后的硬件操作resume_early和resume是恢复路径的对应阶段。4.2 为什么要有 prepare 和 completeprepare的存在是为了解决一个现实问题有些操作可能失败而失败必须发生在不可回退的动作之前。比如一个存储设备如果它还有未刷新的数据suspend 前必须先把数据落盘这个动作可能失败磁盘错误所以放在prepare里失败了就中止整个 suspend 流程。complete则是prepare的配对在 resume 完成后调用用来做收尾。这种准备-执行-收尾的三段式设计是内核里处理可能失败的长流程的常见模式理解了这个模式你看其他子系统也会觉得眼熟。4.3 回调返回值的语义回调的返回值不是随便返回的PM Core 会根据返回值决定流程走向返回0成功继续返回负数失败中止当前流程并回退某些回调返回1有特殊含义比如表示已处理跳过后续。我见过有驱动在runtime_suspend里返回-EBUSY来表示我现在不能休眠这本身没错但如果这个-EBUSY是因为引用计数没释放导致的那就会陷入永远不休眠的循环。所以返回错误码之前一定要想清楚这个错误是不是暂时的、会不会自愈。5. 系统级 suspend 的完整调用链拆解5.1 从写 sysfs 到进入 suspend用户空间echo mem /sys/power/state之后内核侧的流程大致是state_store()解析字符串找到对应的sleep_state调用pm_suspend()进入enter_state()suspend_prepare()阶段冻结进程、同步文件系统、调用dpm_prepare()suspend_devices_and_enter()阶段调用dpm_suspend()、关闭中断、调用平台suspend_ops-enter()唤醒后走dpm_resume()和dpm_complete()恢复。这条链里PM Core 负责的是第 3、4 步里的dpm_*部分平台相关的suspend_ops由 SoC 代码提供。5.2 dpm_suspend 内部做了什么dpm_suspend()会遍历dpm_list对每个设备调用device_suspend()后者再根据设备是否支持 runtime PM、是否有dev_pm_ops决定调用哪个回调。这里有个细节如果设备已经处于 runtime suspended 状态系统 suspend 时可能跳过它的suspend回调因为 PM Core 认为它已经省电了。这个优化在大多数情况下是对的但如果驱动的runtime_suspend和system suspend做的事情不一样就可能出问题。5.3 平台层的 enter 回调真正让 SoC 进入低功耗状态的是platform_suspend_ops-enter()。在 ARM 平台上这通常最终走到 PSCI 的CPU_SUSPEND或者 SoC 自己的电源管理固件接口。这一层是机制的最底层PM Core 不关心它怎么实现只要求它返回后系统能正常恢复。提示调试 suspend 问题时如果卡在enter()之后回不来基本可以判定是平台层或固件的问题跟 PM Core 和驱动无关。这时候要看的是 SoC 的电源状态配置和唤醒源设置。6. 顺着分层定位问题三个真实场景的排查思路6.1 场景一系统 suspend 被无限推迟现象是echo mem之后系统迟迟不进入休眠。排查顺序应该是从上层往下层先看/sys/power/state是否可写、/sys/power/pm_test是否被设成了测试模式用ftrace的pm_*事件看dpm_suspend卡在哪个设备找到设备后看它的runtime_status和usage_count判断是不是引用计数没释放如果是回到驱动代码找pm_runtime_get和put的配对。这个顺序的逻辑是PM Core 的调度是确定的卡住一定是某个设备的回调没返回或返回了错误所以定位到设备就成功了一半。6.2 场景二resume 后设备不工作这类问题通常是 resume 顺序或上下文恢复不完整导致的。重点检查设备的parent关系是否正确决定 resume 顺序resume回调里是否恢复了所有必要的寄存器是否依赖了某个还没恢复的父设备比如 I2C 控制器。我遇到过一次 resume 后触摸屏失灵最后发现是触摸屏驱动的resume里调用了 I2C 读但 I2C 控制器的resume排在它后面。修正方法是在设备树里把触摸屏的parent正确指向 I2C 控制器让 PM Core 排出正确顺序。6.3 场景三runtime PM 导致的功能异常runtime PM 最坑的地方是它会在你意想不到的时候关掉设备。比如某个驱动在初始化时pm_runtime_put之后设备进入 runtime suspend时钟被关结果后续某个异步操作访问寄存器就挂了。这类问题的排查要点是确认所有访问硬件的地方都在pm_runtime_get_sync的保护范围内。这不是 PM Core 的锅是驱动没有遵守 runtime PM 的使用契约。7. 分层设计带来的扩展性与它的代价7.1 扩展性新平台和新设备怎么接入分层设计最大的好处是接入成本低。一个新的 SoC 要支持 suspend只需要实现platform_suspend_ops一个新的设备要支持 runtime PM只需要实现dev_pm_ops里的几个回调。PM Core 不用改策略层也不用改。这种对扩展开放、对修改关闭的设计是内核能支持这么多平台和设备的基础。你在做产品移植时大部分工作其实是填回调而不是改框架。7.2 代价调试复杂度上升分层的代价是问题可能出现在任何一层而现象往往表现在另一层。功耗降不下去可能是策略层 governor 选错了也可能是驱动没实现回调还可能是设备树父子关系错了。你必须有分层定位的意识才能高效排查。我的经验是先确定问题属于哪一层再深入那一层。判断方法很简单——看现象是策略不对该省电时没省、机制失效想省电但省不了还是顺序错误省电动作执行顺序不对。这三类问题分别对应策略层、驱动层和 PM Core 层。7.3 一个实用的分层检查清单现象优先排查层关键检查点待机功耗偏高驱动层各设备 runtime_status、时钟/电源域是否关闭无法进入 suspendPM Core 层dpm_list 顺序、引用计数、回调返回值进入后立即唤醒平台层唤醒源配置、中断屏蔽频率不降策略层cpufreq governor、opp 表resume 后异常驱动层 PM Core父子关系、上下文恢复完整性这张表不是万能的但它能帮你在面对一个模糊的功耗问题时快速缩小范围。8. 我在实际项目里总结的几条经验第一条永远先看引用计数再看回调实现。runtime PM 的问题里十有八九是引用计数没配对而不是回调写错了。养成在驱动里成对写get/put的习惯错误分支也要记得put。第二条设备树的父子关系不是可选项。很多人觉得parent只是逻辑上的从属其实它直接决定 PM Core 的调度顺序。做新板子时我会专门花时间核对每个设备的parent是否指向了它真正依赖的总线控制器。第三条用pm_test分级验证。/sys/power/pm_test可以让你只跑到某个阶段就返回比如设成devices就只做设备 suspend 不做平台 enter。这在定位是设备层还是平台层的问题时非常有用能省掉大量反复重启的时间。第四条别忽略prepare阶段。很多 suspend 失败其实是prepare返回了错误但因为错误信息不够明显被忽略了。打开CONFIG_PM_DEBUG和CONFIG_PM_SLEEP_DEBUG能看到更详细的日志。第五条功耗优化是系统工程不是单点调参。我见过团队花大力气调 cpuidle 的 residency结果发现真正的大头是某个外设的时钟没关。先做全局的功耗分解用电流探头或者 SoC 内部的功耗计数器找到大头再动手比盲目调参数高效得多。这套分层框架理解透了之后你会发现它不只适用于功耗。内核里很多子系统都是类似的策略-框架-平台-驱动四层结构比如时钟框架、 regulator 框架、pinctrl 框架。把 PM Core 这一套吃透再看其他框架会有种原来都是同一个套路的感觉。后续如果要做更细的 runtime PM 调优或者 cpuidle governor 定制也都是在这个分层基础上往下钻。
返回列表