ARTICLE DETAIL

资讯详情

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

Linux PM Core功耗框架分层设计:从genpd到Runtime PM的功耗优化实践

Linux PM Core功耗框架分层设计:从genpd到Runtime PM的功耗优化实践 1. 从一次待机功耗异常说起功耗框架到底管什么去年帮一个做工业网关的朋友排查问题设备用的是典型的嵌入式 Linux 方案跑起来功能都正常唯独待机电流比规格书高了将近 40 毫安。硬件同事把板子翻来覆去测了个遍电源芯片、LDO、外设供电域全都排除了最后问题落到软件侧。我上去看了一眼/sys/kernel/debug/pm_genpd和/sys/power/state再顺着dmesg里的 runtime PM 日志捋了一遍发现是某个 I2C 从设备驱动在 probe 之后没有正确调用pm_runtime_put导致它所在的电源域一直没法进入低功耗状态。改了三行代码待机电流直接掉回规格以内。这件事让我意识到一个很现实的问题很多人做 Linux 功耗优化第一反应是去调 CPU 调频调压、去改cpuidle的 governor但真正卡住功耗的往往是框架层面的分层没有理顺。Linux 内核的功耗管理不是某一个单独的模块而是一整套从下到上、从硬件到策略的分层体系业内通常把它统称为PM CorePower Management Core功耗管理核心。你如果只盯着某一层使劲就像只拧水龙头却不管总阀效果有限还容易误伤。这篇内容我想聊的就是这套分层设计。核心关键词是Linux、PM Core、功耗框架、分层设计。我会从 PM Core 的整体架构讲起把每一层负责什么、层与层之间怎么交互、驱动开发者该在哪一层动手掰开揉碎说清楚。适合谁看如果你写过字符设备驱动、调过runtime PM、被System Suspend卡过、或者单纯想搞明白/sys/power下面那一堆文件到底谁在管那这篇就是给你准备的。哪怕你只是刚接触嵌入式 Linux只要跟着分层这条主线走也不会迷路。我个人的习惯是理解一个内核子系统先看它的目录结构和 Kconfig再看核心数据结构和回调链最后拿一个真实驱动走一遍流程。功耗子系统尤其适合这么干因为它的分层边界非常清晰几乎每一层都能在源码树里找到对应的目录。下面我就按这个思路展开。2. PM Core 的整体分层架构与设计动机2.1 为什么功耗管理必须分层先说一个最朴素的观察一块 SoC 上CPU 有 CPU 的功耗状态外设有外设的时钟门控总线有总线的电源域整机还有整机的休眠。这些东西的粒度和生命周期完全不一样。CPU 可能一毫秒内进出一次 idle而一个外设电源域可能几分钟才关一次整机 suspend 更是几小时才发生一回。如果把这些逻辑全塞进一个模块代码会变成一团乱麻而且任何一处改动都可能影响全局。分层设计的第一个动机就是解耦不同时间尺度和不同粒度的功耗行为。底层的时钟、电源域、稳压器只关心我现在能不能关上层的策略模块只关心现在该不该关、关到哪一级。中间用统一的框架接口隔开两边各自演进互不干扰。第二个动机是硬件差异的屏蔽。不同厂商的 SoC电源域划分、时钟树结构、稳压器控制方式千差万别。如果每个驱动都直接去操作寄存器那驱动就没法跨平台复用了。PM Core 的做法是抽象出genpdGeneric Power Domain通用电源域、clk框架、regulator框架这些中间层驱动只跟框架打交道具体怎么落到硬件由平台代码决定。第三个动机是策略与机制分离。这是内核设计的一贯哲学。机制层提供能做什么的能力策略层决定该做什么。比如cpuidle框架提供进入各级 idle 的能力而menu、teo这些 governor 决定当前该进哪一级。这样换一个 governor 就能改变整机功耗行为不用动底层机制。2.2 从下到上的四层结构把 PM Core 拆开看我习惯分成四层从下往上依次是层级名称典型组件主要职责第一层硬件抽象层clk、regulator、genpd、pinctrl直接控制时钟、电压、电源域开关第二层设备运行时层Runtime PM、Device Links管理单个设备在运行态的动态功耗第三层系统级电源层System Suspend、System Resume管理整机进入低功耗/休眠状态第四层策略与调优层cpuidle governor、cpufreq governor、PM QoS根据负载和约束决定功耗策略这四层不是严格的金字塔调用关系而是互相配合。比如 Runtime PM 在决定挂起一个设备时会去调用 genpd 把对应的电源域关掉System Suspend 在挂起整机前会先让所有设备走一遍 Runtime PM 的 suspend 流程。理解这个交叉关系是理解整个框架的关键。2.3 源码树里的对应位置光说概念容易飘落到源码上更实在。以主流内核版本为例相关代码大致分布在这些地方drivers/base/power/PM Core 的主体包括main.c系统 suspend/resume、runtime.cRuntime PM、domain.cgenpd 核心、qos.cPM QoS、clock_ops.c等。drivers/base/power/domain_governor.cgenpd 的 governor决定电源域能关到哪一级。kernel/power/系统级电源管理suspend.c、main.c、hibernate.c、qos.c等。drivers/cpuidle/cpuidle 框架和各个 governor。drivers/cpufreq/cpufreq 框架和各个 governor。include/linux/pm.h、include/linux/pm_runtime.h、include/linux/pm_domain.h核心头文件数据结构定义都在这里。我建议你打开drivers/base/power/这个目录先扫一眼文件名基本就能对上我上面说的分层。main.c管系统级runtime.c管设备级domain.c管电源域职责划分一目了然。这种目录即架构的组织方式是内核里少有的清爽设计。3. 硬件抽象层genpd、clk 与 regulator 怎么配合3.1 genpd 的核心数据结构genpd 是 PM Core 里最值得细看的一层因为它把电源域这个硬件概念抽象成了内核对象。核心结构是struct generic_pm_domain定义在include/linux/pm_domain.h。我挑几个关键字段说struct generic_pm_domain { struct dev_pm_domain domain; /* 对设备暴露的 PM domain 接口 */ struct list_head gpd_list_node; /* 挂在全局 gpd_list 上 */ struct list_head master_links; /* 作为 master 的连接 */ struct list_head slave_links; /* 作为 slave 的连接 */ struct list_head dev_list; /* 属于本域的设备和子域 */ struct gpd_timing_data *td; /* 时序约束数据 */ int (*power_off)(struct generic_pm_domain *domain); int (*power_on)(struct generic_pm_domain *domain); unsigned int state_idx; /* 当前状态索引 */ ... };power_off和power_on这两个回调是平台代码必须实现的它们直接操作硬件把电源域真正关掉或打开。而dev_list和master_links/slave_links构成了域之间的拓扑关系——一个域可以是另一个域的 master也可以是 slave这就形成了电源域的层级树。为什么要有 master/slave 关系因为真实 SoC 的电源域是有依赖的。比如一个显示子系统域它依赖顶层的 always-on 域供电同时它内部又包含 GPU 子域和显示控制器子域。关的时候必须自下而上开的时候必须自上而下。genpd 用master_links和slave_links把这种依赖关系建模出来governor 在决策时会沿着这个图遍历。3.2 电源域的注册与设备绑定平台代码注册一个电源域通常用pm_genpd_initpm_genpd_init(disp_pd, NULL, false);第三个参数is_off表示初始状态是否已关闭。注册完之后还要把设备绑定到这个域上。有两种方式一种是在设备树里通过power-domains属性声明由genpd_dev_pm_attach在设备 probe 时自动绑定另一种是在代码里显式调用dev_pm_domain_attach。设备树方式更常见写起来也直观disp: display12340000 { compatible vendor,display; power-domains pd_disp; ... };绑定之后设备的dev-pm_domain就指向了这个 genpd。之后设备走 Runtime PM 挂起时PM Core 会先调用设备自己的runtime_suspend再通过pm_domain的runtime_suspend回调去尝试关闭电源域。这个调用链是理解 genpd 的关键我在下一节会展开。3.3 clk 与 regulator 的配合关系genpd 管的是电源域开关但一个域关掉之前往往还要先关时钟、再断电压。这三者的顺序不能乱。典型的下电顺序是先 gate 时钟clk_disable再关电源域genpd power_off最后如果整个域都断电了才去 disable regulator。上电顺序反过来。为什么顺序这么重要因为如果先断电压再关时钟某些硬件模块在没电的情况下收到时钟信号可能产生不确定行为甚至漏电。反过来如果先关电源域再 gate 时钟时钟信号可能还在但模块已经没电同样有风险。所以平台代码在实现power_off回调时必须严格按这个顺序来。我见过不少新手写的power_off回调直接一句regulator_disable就完事结果时钟没关域关了但时钟还在跑功耗反而更高。正确的做法是在power_off里依次处理 clk、genpd、regulator并且用clk_prepare_enable/clk_disable_unprepare这种成对接口避免引用计数错乱。提示genpd 的power_off回调里不要做可能睡眠的操作之外的事情也不要调用会触发 Runtime PM 的接口否则容易死锁。这个回调运行在 genpd 的锁保护下能做的事情很有限。4. 设备运行时层Runtime PM 的引用计数与回调链4.1 Runtime PM 的核心思想Runtime PM 解决的是设备在系统运行期间空闲时能不能自己关掉的问题。它的核心是一个引用计数每个设备有一个usage_count当计数为 0 且没有活跃的子设备时设备就可以进入低功耗状态。这个设计的好处是驱动不需要自己判断现在有没有人在用我只需要在开始用的时候pm_runtime_get用完pm_runtime_put框架自动帮你判断能不能关。这就像图书馆的灯谁进来谁开灯最后一个人走的时候关灯不需要管理员盯着。核心接口就几个pm_runtime_get_sync(dev)增加引用计数必要时唤醒设备。pm_runtime_put(dev)减少引用计数可能触发挂起。pm_runtime_put_sync(dev)减少引用计数并同步等待挂起完成。pm_runtime_set_active(dev)/pm_runtime_set_suspended(dev)设置初始状态。pm_runtime_enable(dev)/pm_runtime_disable(dev)启用/禁用 Runtime PM。4.2 回调链是怎么走的当一个设备的引用计数降到 0PM Core 会尝试挂起它。挂起过程不是简单调一个函数而是一条回调链先调用设备所属总线的runtime_suspend比如platform_bus_type的。再调用设备驱动自己的runtime_suspend。然后调用设备所属pm_domain的runtime_suspend也就是 genpd 的挂起逻辑。genpd 在挂起时会检查这个域下所有设备是否都空闲如果是才真正执行power_off。这条链的顺序很重要设备自己的runtime_suspend先执行把设备内部状态保存好、时钟关掉然后 genpd 才去关整个域的电源。如果顺序反了域先断电设备还没来得及保存状态数据就丢了。恢复的时候顺序反过来genpd 先power_on然后设备驱动的runtime_resume最后总线的runtime_resume。4.3 一个真实的引用计数踩坑案例回到开头那个 I2C 从设备的例子。那个驱动的 probe 函数大致是这样的static int sensor_probe(struct i2c_client *client) { ... pm_runtime_enable(client-dev); pm_runtime_set_active(client-dev); ... /* 读了一次寄存器做初始化 */ sensor_read_reg(client, REG_ID); ... return 0; }问题出在pm_runtime_set_active之后没有配对pm_runtime_put。set_active会把设备标记为活跃但不会增加引用计数而 probe 里读寄存器时如果驱动内部用了pm_runtime_get_sync计数就上去了读完却没有put计数永远回不到 0设备永远不挂起它所在的电源域也就永远关不掉。修复很简单在 probe 末尾加一句pm_runtime_put(client-dev);或者在读寄存器的地方用pm_runtime_get_sync/pm_runtime_put成对包裹。这个坑的隐蔽性在于功能完全正常只有测功耗才能发现。所以我的经验是写完任何带 Runtime PM 的驱动第一件事就是去/sys/bus/.../devices/.../power/runtime_status看状态确认空闲时能变成suspended。4.4 Device Links处理设备间的依赖有些设备之间有依赖关系比如一个传感器挂在 I2C 控制器下面传感器要用I2C 控制器就必须开着。Runtime PM 单独看每个设备是判断不出来的这时候就需要 Device Links。Device Links 用device_link_add建立可以指定DL_FLAG_PM_RUNTIME让 PM 框架感知这个依赖。建立之后当消费者设备传感器活跃时供应者设备I2C 控制器会被自动保持活跃消费者挂起后供应者才可能挂起。这个机制在设备树里也能声明通过links属性或者由框架根据power-domains、parent关系自动推导。我个人的建议是能用设备树自动推导的就别手写手写容易漏而且顺序错了很难查。5. 系统级电源层System Suspend 的完整流程5.1 系统休眠的几种状态系统级电源管理管的是整机状态Linux 里通过/sys/power/state暴露给用户空间。常见的有freeze冻结进程设备不一定断电用于轻量级暂停。mem或standby挂起到内存也就是常说的 STRSuspend to RAM。disk挂起到磁盘也就是休眠Hibernate。写echo mem /sys/power/state就触发一次 STR。这个动作背后是一整套流程涉及进程冻结、设备挂起、CPU 停核、系统唤醒等步骤。5.2 挂起流程的五个阶段一次完整的 System Suspend我习惯分成五个阶段冻结用户进程和内核线程调用freeze_processes把所有可冻结的进程停住防止它们在挂起过程中搞事情。设备挂起dpm_suspend按设备树的顺序从叶子节点往根节点依次调用每个设备的suspend回调。这个顺序保证了子设备先挂起父设备后挂起。关闭非引导 CPU把除 CPU0 之外的核都下线减少功耗。进入低功耗状态调用平台相关的enter回调让 SoC 进入真正的低功耗模式。唤醒与恢复被中断唤醒后按相反顺序恢复 CPU、设备、进程。这五个阶段里第二阶段是驱动开发者最需要关心的因为你的suspend/resume回调就在这里被调用。5.3 dpm_list 的顺序为什么重要设备挂起依赖一个全局链表dpm_list这个链表的顺序由设备注册顺序和父子关系决定。核心规则是子设备必须排在父设备前面。挂起时从前往后遍历恢复时从后往前。为什么因为如果父设备比如 I2C 控制器先挂起子设备传感器再去访问它就会失败。反过来恢复时父设备必须先恢复子设备才能用。这个顺序由device_pm_add在设备注册时维护。如果驱动在运行时动态创建了设备或者手动调整了dpm_list就可能打乱顺序导致挂起失败。我遇到过一次挂起卡死最后查出来是某个驱动在suspend回调里又注册了一个新设备把链表搞乱了。所以记住一条不要在 suspend/resume 回调里注册或注销设备。5.4 唤醒源与 wakeup 计数系统挂起后得有东西能把它唤醒。这就是唤醒源wakeup source的作用。设备可以通过device_init_wakeup(dev, true)声明自己是唤醒源然后通过enable_irq_wake让中断在系统挂起时仍能触发。每个唤醒源有一个wakeup_count用户空间可以通过/sys/power/wakeup_count和/sys/power/wake_lock参与控制。这个机制是为了防止刚挂起就被唤醒的竞态用户空间先读wakeup_count确认没有待处理的唤醒事件再写回去然后才写mem触发挂起。如果这期间有唤醒事件写wakeup_count会失败挂起就不会发生。这个设计有点绕但很实用。我在做低功耗产品时经常用wakeup_count来判断到底是谁在阻止系统进入休眠比盲猜高效得多。6. 策略与调优层governor 与 PM QoS6.1 cpuidle governor 怎么选cpuidle 管的是 CPU 在空闲时进哪一级 idle。每级 idle 有exit_latency退出延迟和target_residency目标驻留时间两个关键参数。governor 的任务就是根据预测的空闲时长选一个省电又不影响性能的级别。常见的 governormenu基于历史空闲时长做预测适合大多数场景。teo更激进的预测适合交互式负载。ladder老式实现现在基本不用了。选哪个我的经验是桌面和移动设备用teo或menu服务器用menu实时性要求高的场景要谨慎因为进深 idle 的退出延迟可能影响响应。可以通过/sys/devices/system/cpu/cpuidle/current_governor查看和切换。6.2 cpufreq governor 与功耗的权衡cpufreq 管的是 CPU 频率和电压。governor 决定当前该跑多快。常见的有performance一直最高频性能最好功耗最高。powersave一直最低频省电但可能卡。ondemand负载高就升频负载低就降频响应快但可能抖动。schedutil跟调度器联动最现代的选择推荐。schedutil的好处是它直接拿调度器的负载信息不用自己采样决策更准、延迟更低。现在主流内核默认就是它。如果你的设备还在用ondemand可以考虑换过来通常能同时改善功耗和响应。6.3 PM QoS把约束显式表达出来PM QoSQuality of Service解决的是某些场景下不允许进太深的低功耗状态的问题。比如音频播放时CPU 不能进太深的 idle否则会爆音网络收包时延迟不能太高。PM QoS 有两类约束PM_QOS_CPU_DMA_LATENCY和PM_QOS_RESUME_LATENCY。驱动或用户空间可以通过/dev/cpu_dma_latency或 sysfs 接口注册约束。框架会把所有约束取最严格的那个作为当前允许的最大延迟。这个机制的价值在于它把性能要求变成了一个可量化的数字让功耗框架能自动做权衡而不是靠人拍脑袋。我在做音频设备时就是靠注册一个PM_QOS_CPU_DMA_LATENCY约束保证播放期间不进深 idle问题迎刃而解。7. 常见问题与排查技巧实录7.1 功耗问题排查速查表现象可能原因排查入口待机电流偏高某设备未挂起/sys/.../power/runtime_status系统无法进入 suspend有 wakeup 事件或约束/sys/power/wakeup_count、dmesg挂起后立即唤醒唤醒源误触发/sys/kernel/debug/wakeup_sourcesCPU 不进深 idleQoS 约束或 governor 问题/sys/devices/system/cpu/cpuidle/电源域关不掉域内设备未全部空闲/sys/kernel/debug/pm_genpd频率降不下来governor 或负载误判/sys/devices/system/cpu/cpufreq/7.2 几个我踩过的坑坑一debugfs 没挂载。很多功耗调试接口在/sys/kernel/debug/下如果没挂载 debugfs什么都看不到。挂载命令mount -t debugfs none /sys/kernel/debug坑二runtime_status 显示error。这通常意味着某个runtime_suspend回调返回了错误。去看dmesg一般会有对应的错误打印。常见原因是回调里访问了已经关掉的时钟或电源。坑三genpd 的state_idx一直是 0。说明电源域从没关过。去/sys/kernel/debug/pm_genpd看每个域的dev_list找出哪个设备还在活跃。十有八九是引用计数没配对。坑四suspend 卡在某个设备。打开CONFIG_PM_DEBUG和CONFIG_PM_SLEEP_DEBUG然后echo 1 /sys/power/pm_debug_messages再触发挂起dmesg会打印每个设备的挂起过程卡在谁那里一目了然。7.3 一个实用的调试脚本我平时排查功耗习惯先跑一遍这个脚本把关键状态一次性抓出来#!/bin/bash echo runtime status for d in /sys/bus/*/devices/*/power/runtime_status; do st$(cat $d 2/dev/null) [ $st ! active ] continue echo $d: $st done echo wakeup sources cat /sys/kernel/debug/wakeup_sources 2/dev/null | awk $60 {print} echo pm_genpd cat /sys/kernel/debug/pm_genpd 2/dev/null echo cpuidle cat /sys/devices/system/cpu/cpuidle/current_governor 2/dev/null这个脚本会列出所有还处于 active 的设备、有活跃计数的唤醒源、电源域状态和当前 idle governor。大部分功耗问题跑一遍就能缩小范围。8. 分层设计带来的扩展思路理解了 PM Core 的分层其实很多扩展就顺理成章了。比如你想给一个新的 SoC 加功耗支持只需要在硬件抽象层实现power_off/power_on回调注册 genpd剩下的 Runtime PM、System Suspend 框架会自动接管。你想换一种功耗策略只需要写一个新的 governor注册到 cpuidle 或 cpufreq 框架不用动底层。我个人在实际项目里的体会是功耗优化 80% 的工作量在理顺分层20% 才在调参数。大部分功耗问题不是参数没调好而是某一层的职责没理清导致设备该关的没关、该醒的没醒。把 genpd 的域划分对、把 Runtime PM 的引用计数配对、把 wakeup source 管好功耗自然就下来了。最后再分享一个小技巧如果你不确定某个设备该不该在空闲时挂起可以临时把它的control设成on强制它保持活跃然后对比功耗。如果功耗没变化说明这个设备本来就没进低功耗问题在别处如果功耗明显上升说明它平时是挂起的那就要去看它挂起时是否真的省电。这个强制活跃对比法在定位功耗大户时特别好用比一个个猜快得多。
返回列表