尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux PM Core 功耗管理分层设计与设备休眠机制详解

Linux PM Core 功耗管理分层设计与设备休眠机制详解 1. 从一次待机功耗异常说起为什么要啃 PM Core去年调一块嵌入式板子的时候遇到个怪事系统进入 suspend 之后万用表测出来整板还有 80 多毫安电流按经验这个数应该在个位数才对。查了两天最后发现是一个外设驱动在suspend回调里没把时钟关干净而它注册回调的方式又绕过了 PM Core 的某些检查路径。那次之后我才下决心把 Linux 功耗子系统从drivers/base/power/这一层开始完整捋一遍。这篇聊的是Linux 内核功耗子系统的第一部分聚焦PM Core也就是功耗框架的分层设计。说白了PM Core 是内核里所有和电源管理相关操作的“总调度台”它不负责具体某个硬件怎么省电而是定义了一套规则谁在什么时候可以休眠、休眠的顺序是什么、设备之间有没有依赖、系统级休眠怎么和设备级休眠串起来。搞嵌入式的、做手机/平板/车机 BSP 的、写外设驱动的只要你的代码要在低功耗场景下跑这套东西就绕不开。很多人一上来就去啃cpuidle、cpufreq、regulator这些子系统结果发现每个都跟 PM Core 有千丝万缕的关系越看越乱。我的建议是先把 PM Core 的分层骨架吃透后面再看具体 governor、具体驱动会顺很多。下面我按自己理解的顺序从设计思路到代码结构再到实操排查一层层拆。2. PM Core 到底管什么先搞清楚它的职责边界2.1 它不是“省电开关”而是“省电流程的裁判”刚接触的人容易有个误解觉得 PM Core 就是内核里管省电的模块。这个说法太粗。准确讲PM Core 管的是电源管理操作的编排而不是省电策略本身。打个比方一栋大楼晚上要熄灯锁门。PM Core 是那个拿着对讲机的保安队长他负责按顺序通知每一层、确认每个人都撤了、最后拉总闸。至于每一层具体怎么关灯、关哪个开关那是各楼层管理员也就是各个设备驱动自己的事。保安队长不关心你用的是拉线开关还是声控灯他只关心“你关完了没有”“你关的顺序对不对”“有没有人还卡在楼里”。所以 PM Core 的核心职责可以归成四块设备电源管理struct dev_pm_ops这套回调的注册与调用包括 runtime PM 和 system sleep 两条线。系统级休眠流程suspend/resume/hibernate的状态机以及设备挂起/恢复的遍历顺序。设备依赖与顺序控制通过device_link、parent-child 关系决定谁先睡谁后睡。唤醒源管理wakeup source的注册、使能、中断唤醒路径。这四块里前两块是主干后两块是保证正确性的关键。实际调试中出问题最多的恰恰是后两块——顺序错了、依赖没建对、唤醒源没使能表现就是“睡不下去”或者“睡下去醒不来”。2.2 分层设计的三个层次PM Core 的分层我习惯按“抽象层次”来切而不是按代码目录切。这样理解起来更顺层次名称关注点典型代码位置上层系统级 PM整机休眠/唤醒状态机kernel/power/中层设备级 PM单设备挂起/恢复、runtime PMdrivers/base/power/下层硬件/平台 PM具体 SoC 的电源域、时钟、寄存器drivers/soc/、arch/这个分层不是硬性隔离而是调用方向上的分层。系统级 PM 往下调用设备级 PM设备级 PM 再往下调用平台相关代码。反过来硬件的中断可以往上冒泡成唤醒事件最终影响系统级状态。为什么要这么分因为整机休眠和设备休眠的时间尺度、触发条件、失败代价完全不同。设备 runtime 休眠可能一秒钟发生几十次失败了顶多多耗点电系统 suspend 可能几分钟才一次失败了可能直接死机或者数据丢失。把这两条线放在同一层管理复杂度会爆炸。分层之后系统级 PM 只需要关心“我要睡了你们这些设备按顺序准备好”设备级 PM 关心“我自己怎么睡”各管各的。2.3 为什么是“设备树式”的遍历而不是简单列表这里有个设计细节值得单独说。PM Core 在挂起设备时不是简单地遍历一个全局设备列表而是沿着设备树的父子关系递归处理。原因很实际设备之间有天然的依赖。比如一个 I2C 控制器下面挂了温度传感器你要睡的时候肯定得先让传感器睡再让控制器睡醒的时候反过来先醒控制器再醒传感器。如果用一个扁平列表就得靠人工维护顺序几百个设备根本维护不过来。用设备树递归天然保证了“子先睡、父后睡父先醒、子后醒”的顺序。这个顺序在代码里体现得很清楚dpm_list的构建就是按设备注册顺序和父子关系排的dpm_suspend从链表尾部往前遍历先处理子设备dpm_resume从头部往后遍历。我第一次看这段代码的时候没注意方向调一个 SPI 屏的休眠问题时绕了很大弯子后来才明白方向搞反了。提示调试设备休眠顺序问题时先打印dpm_list的实际顺序比盯着代码猜要快得多。/sys/kernel/debug/devices_deferred和dpm_list相关的 debugfs 节点能直接看到当前排序。3. 核心数据结构拆解dev_pm_ops 与 device 的绑定关系3.1 dev_pm_ops设备电源管理的统一接口整个设备级 PM 的入口就是struct dev_pm_ops。这个结构体定义在include/linux/pm.h里面是一堆函数指针按场景分组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); };看着多其实分三组系统休眠组prepare/complete、suspend/resume、suspend_late/resume_early、freeze/thaw/poweroff/restore。这几组对应休眠流程的不同阶段。Runtime PM 组runtime_suspend/runtime_resume/runtime_idle。这是设备自己空闲时触发的和系统休眠独立。辅助组prepare/complete严格说属于系统休眠流程的早期和晚期钩子。新手最容易混的是suspend和runtime_suspend。记住一点系统 suspend 时runtime PM 会被临时禁用走的是suspend这条线系统正常运行时的空闲休眠走runtime_suspend。两条线不会同时跑同一个设备。3.2 回调的注册dev_pm_ops 怎么挂到 device 上驱动里通常这么写static const struct dev_pm_ops mydev_pm_ops { .suspend mydev_suspend, .resume mydev_resume, .runtime_suspend mydev_runtime_suspend, .runtime_resume mydev_runtime_resume, }; static struct platform_driver mydev_driver { .driver { .name mydev, .pm mydev_pm_ops, }, .probe mydev_probe, .remove mydev_remove, };关键在.driver.pm这个字段。设备注册时device_pm_init会把dev-pm_domain、dev-type-pm、dev-class-pm、dev-bus-pm、drivers里的pm按优先级串起来。实际调用时PM Core 会按这个优先级链依次调用直到有一个返回非零或者全部走完。这个优先级链是很多人踩坑的地方。比如你在bus-pm里写了suspend在驱动dev_pm_ops里也写了suspend两个都会被调用顺序是 bus 的先还是驱动的先取决于具体总线的实现。我见过一个 I2C 驱动bus 层的 suspend 已经把控制器时钟关了驱动层的 suspend 又去访问控制器寄存器直接挂死。所以写回调之前一定要确认这条链上都有谁。3.3 device_link设备依赖的显式表达父子关系能解决大部分顺序问题但有些依赖不是父子关系。比如一个电源管理 ICPMIC给某个外设供电这两个设备在设备树里可能是兄弟节点但外设休眠前必须确保 PMIC 还在工作。这时候就要用device_link。它允许在两个设备之间建立“消费者-供应者”关系struct device_link *link; link device_link_add(consumer, supplier, DL_FLAG_PM_RUNTIME | DL_FLAG_STATELESS);建了 link 之后PM Core 在挂起时会自动保证 supplier 比 consumer 后挂起、先恢复。这个机制在 regulator、clock、power domain 这些框架里用得很多驱动开发者一般不用手动建但排查问题时要知道它的存在。注意device_link建错了比不建更麻烦。如果 supplier 和 consumer 之间形成了环PM Core 会直接报错拒绝挂起。调试时看到device_link相关的 warning先检查依赖图有没有成环。4. 系统休眠流程从 echo mem 到设备全部就位4.1 触发路径用户空间到内核的完整链路用户敲下echo mem /sys/power/state到设备真正挂起中间经过这么几步sysfs的state_store被调用解析出目标状态mem、freeze、standby等。进入pm_suspend()先做valid_state检查确认平台支持这个状态。调用enter_state()开始正式的休眠流程。suspend_prepare()冻结用户进程、同步文件系统、给设备发prepare回调。suspend_devices_and_enter()设备挂起、平台相关休眠、进入低功耗。唤醒后反向执行resume流程。这里面第 4 步的prepare回调很关键。它的作用是让驱动在真正挂起之前做一些“可能失败”的准备工作比如申请内存、检查硬件状态。如果prepare返回错误整个休眠流程会中止并回滚不会走到不可恢复的地步。这个设计是为了让休眠尽量“可失败”而不是睡到一半发现不行。4.2 设备挂起的三个阶段设备挂起不是一步完成的分三个阶段每个阶段对应不同的回调阶段回调特点早期prepare可睡眠可失败用于准备中期suspend可睡眠常规挂起操作晚期suspend_late不可睡眠中断已关最后收尾为什么分这么细因为有些操作必须在中断还开着的时候做比如等一个 I2C 传输完成有些操作必须在中断关了之后做比如写一个不能被打断的寄存器序列。三个阶段给了驱动足够的灵活性。对应的恢复阶段是反过来的resume_early中断刚开、resume常规恢复、complete收尾。我调一个触摸屏驱动的时候suspend里发了 I2C 命令让触摸 IC 进低功耗结果偶尔失败。后来把这条命令挪到suspend_late里问题消失。原因是suspend阶段系统还在跑其他设备的挂起I2C 总线可能被占用suspend_late阶段其他设备基本都睡了总线空闲命令一次成功。这个经验说明阶段选择不是随便的要结合具体硬件的时序需求。4.3 dpm_list 的遍历方向与顺序保证前面提过dpm_list的遍历方向这里展开说。dpm_list是一个双向链表设备在device_add时被插入。插入位置不是简单的尾部追加而是根据父子关系调整保证子设备在父设备之前被处理挂起时。挂起时dpm_suspend从链表尾部向头部遍历恢复时dpm_resume从头部向尾部遍历。这个方向设计保证了挂起子设备先睡父设备后睡。恢复父设备先醒子设备后醒。如果某个设备的挂起依赖另一个非父子设备就需要device_link来调整顺序。device_link会影响dpm_list的排序把 supplier 排到 consumer 后面挂起时更晚处理。实际调试时如果怀疑顺序问题可以在dpm_suspend里加打印把每个设备的dev_name和遍历顺序打出来和硬件手册要求的顺序对比。这个方法比看代码猜快得多。5. Runtime PM设备空闲时的自动省电5.1 Runtime PM 的核心思想引用计数Runtime PM 和系统休眠最大的区别是它是自动的、细粒度的、频繁的。一个设备在系统正常运行期间如果一段时间没人用就可以自己进低功耗有人用了再自己醒过来。实现这个“有人用/没人用”判断的是引用计数。每个设备有一个usage_count驱动在访问硬件前调用pm_runtime_get_sync()增加计数访问完调用pm_runtime_put()减少计数。计数降到 0 且没有其他阻止条件时PM Core 会调用runtime_idle进而可能触发runtime_suspend。static int mydev_read(struct file *filp, char __user *buf, size_t len, loff_t *off) { struct mydev *dev filp-private_data; int ret; ret pm_runtime_get_sync(dev-dev); if (ret 0) { pm_runtime_put_noidle(dev-dev); return ret; } /* 访问硬件 */ ret mydev_hw_read(dev, buf, len); pm_runtime_put(dev-dev); return ret; }这段代码是标准写法。注意pm_runtime_get_sync失败时的处理要调用pm_runtime_put_noidle把计数还回去否则计数泄漏设备永远不休眠。这个坑我踩过表现是设备功耗一直下不来查了半天才发现是错误路径没还计数。5.2 autosuspend延迟休眠的妙用有些设备访问很频繁如果每次put之后立刻suspend再get又立刻resume频繁的开关反而比不休眠更耗电。这时候用 autosuspendpm_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_put_autosuspend不会立刻减计数到 0 触发休眠而是等delay时间。如果这期间又有get计时重置设备保持唤醒。这个机制对 I2C、SPI 这类总线设备特别有用。delay 设多少合适我的经验是先测设备从 idle 到 suspend 的耗时再测 resume 的耗时两者之和乘以 2 到 3作为初始值。比如 suspend 耗时 1msresume 耗时 2ms那 delay 设 6 到 9ms 比较合理。设太小起不到合并效果设太大省电效果打折。5.3 Runtime PM 与系统休眠的交互系统要休眠时会先把所有设备的 runtime PM 禁用pm_runtime_disable然后走系统 suspend 流程。唤醒后再重新使能。这个交互有个细节如果某个设备在系统休眠前处于 runtime suspended 状态系统 suspend 时它的suspend回调可能不会被调用因为 PM Core 认为它已经睡了。这个行为在大多数情况下是对的但有些设备在 runtime suspend 和 system suspend 时需要做不同的事。比如一个传感器runtime suspend 时只关内部时钟system suspend 时要彻底断电。这种情况下驱动需要在suspend回调里检查设备当前状态必要时先resume再按系统休眠流程处理。提示pm_runtime_status_suspended()可以查询设备当前 runtime 状态。在系统 suspend 回调里用它做分支判断是处理这种差异的标准做法。6. 唤醒源管理睡下去之后怎么醒过来6.1 wakeup source 的注册与使能设备要能唤醒系统必须注册成 wakeup sourcedevice_init_wakeup(dev, true); enable_irq_wake(irq);device_init_wakeup把设备标记为可唤醒enable_irq_wake把对应的中断线配置成唤醒源。两步缺一不可。只做第一步中断来了系统不会醒只做第二步PM Core 不知道这个设备是唤醒源可能在挂起时把它关掉。唤醒源有使能计数用户空间可以通过/sys/devices/.../power/wakeup读写。写enabled表示允许这个设备唤醒系统写disabled表示禁止。这个接口在调试时很有用可以逐个排除是哪个设备在乱唤醒。6.2 wakeup count 与唤醒统计每个 wakeup source 有一个wakeup_count记录它唤醒系统的次数。系统挂起前用户空间可以读这个值挂起后对比就知道是哪个设备触发的唤醒。# 挂起前记录 cat /sys/devices/.../power/wakeup_count # 挂起后对比 cat /sys/devices/.../power/wakeup_count如果某个设备频繁唤醒系统但业务上不需要可以把它 disable 掉。我遇到过 WiFi 模块在系统 suspend 后不断唤醒的问题最后就是通过 wakeup_count 定位到然后在不需要保持连接的场景下 disable 掉它的唤醒能力。6.3 唤醒中断的处理路径唤醒中断来了之后处理路径和普通中断不太一样中断控制器把中断送到 CPUCPU 从低功耗状态退出。中断处理程序执行但此时系统还没完全恢复。PM Core 检查中断是否来自已使能的 wakeup source。如果是记录唤醒事件继续恢复流程如果不是可能忽略或报错。这个路径里有个容易出问题的地方唤醒中断的处理程序不能做太重的操作因为此时系统还在恢复中很多资源没就绪。标准做法是在中断处理里只做最小必要的操作比如清中断标志把重活留给resume回调。7. 常见问题与排查技巧实录7.1 系统睡不下去从哪开始查系统echo mem之后没反应或者报错返回排查顺序建议这样看内核日志dmesg | tail -50PM Core 在挂起失败时会打印具体是哪个设备、哪个回调返回了错误。检查 prepare 回调prepare阶段失败最常见因为很多驱动在这里做内存申请、硬件检查。检查唤醒源如果有未处理的唤醒事件挂起系统可能拒绝休眠。看/sys/power/wakeup_count和/sys/kernel/debug/wakeup_sources。检查 device_link 环日志里如果有cycle相关 warning说明依赖图有问题。我遇到过一次系统睡不下去日志显示某个 GPIO 驱动的suspend返回-EBUSY。查下去发现是那个 GPIO 被另一个驱动持有而那个驱动没实现suspend回调PM Core 认为它还在用。解决办法是给持有方加suspend回调或者在prepare阶段释放 GPIO。7.2 睡下去醒不来唤醒源排查系统能睡但醒不来通常是唤醒源没配好确认enable_irq_wake被调用。确认/sys/devices/.../power/wakeup是enabled。确认中断控制器本身支持唤醒有些 SoC 的某些中断线不支持。用示波器量中断线确认硬件上真的有信号。有一次调试 RTC 唤醒软件配置全对但就是醒不来。最后用示波器发现 RTC 的中断线在 suspend 后被一个外部电路拉低了导致中断控制器认为中断一直有效反而屏蔽了唤醒。硬件问题软件查半天。7.3 Runtime PM 计数泄漏怎么发现和定位计数泄漏的表现是设备功耗一直下不来/sys/devices/.../power/runtime_status显示active但实际没人用。定位方法# 查看当前计数 cat /sys/devices/.../power/runtime_usage # 查看是否有未完成的 get cat /sys/kernel/debug/pm_genpd/.../...更直接的办法是在pm_runtime_get和pm_runtime_put里加打印看计数变化。如果get次数比put多就是泄漏。常见泄漏点错误路径没put。中断处理里get了但没put。多个驱动共享设备各自计数没对齐。7.4 常见问题速查表现象可能原因排查方向系统睡不下去prepare 回调失败dmesg 看具体设备系统睡不下去唤醒事件未处理wakeup_count 对比睡下去醒不来唤醒源未使能enable_irq_wake 检查睡下去醒不来中断控制器不支持查 SoC 手册功耗降不下来runtime 计数泄漏runtime_usage 检查功耗降不下来autosuspend delay 太大调整 delay 值恢复后设备异常resume 顺序错dpm_list 打印恢复后设备异常时钟/电源未恢复resume 回调检查8. 几个我踩过的坑和实操心得第一个坑是关于suspend回调里的睡眠。文档说suspend阶段可以睡眠但实际用的时候要小心如果此时系统已经冻结了用户进程某些依赖用户空间的操作比如通过 netlink 发消息会卡住。我调一个网络设备驱动时suspend里发了个 netlink 通知结果直接卡死。后来改成在prepare阶段发问题解决。第二个坑是resume回调的返回值。resume理论上不应该失败因为失败了也没法回滚。但有些驱动会在resume里做可能失败的操作返回错误。PM Core 对resume的错误处理是打印 warning 然后继续不会中止恢复流程。所以resume里的错误往往被忽略导致设备状态不一致。我的做法是resume里只做不会失败的操作可能失败的放到complete里。第三个坑是 runtime PM 和系统 PM 的锁竞争。pm_runtime_get_sync会拿dev-power.lock如果此时系统正在走 suspend 流程可能死锁。标准做法是在系统 suspend 的prepare阶段调用pm_runtime_disable确保 runtime PM 不再活动。但有些驱动忘了这一步导致偶发死锁。排查这种问题看 lockdep 报告最直接。第四个心得是关于调试工具。/sys/kernel/debug/pm_genpd/下面的节点能看到每个 power domain 的状态和引用计数调电源域问题时非常有用。/sys/kernel/debug/wakeup_sources能看到所有唤醒源的统计。这两个 debugfs 节点我基本每次调功耗问题都会看。最后说一个关于分层设计的体会。PM Core 的分层不是为了好看而是为了让每一层只关心自己该关心的事。系统级 PM 不关心具体硬件怎么省电设备级 PM 不关心整机状态机平台代码不关心设备依赖。这种分工让每一层的代码都相对简单出了问题也容易定位到具体层。我调功耗问题的经验是先确定问题出在哪一层再往下钻比一上来就翻代码高效得多。
返回列表