ARTICLE DETAIL

资讯详情

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

Linux DPM框架:系统睡眠中的设备电源管理基石

Linux DPM框架:系统睡眠中的设备电源管理基石 干了这么多年Linux功耗管理越到后面越觉得真正拉开差距的往往不是某个驱动怎么调而是系统里有条不紊的秩序。系统睡眠这个场景尤其明显一觉睡下去板子上所有设备都要在这几十毫秒内进入约定状态有一个设备不配合轻则睡眠失败重则唤醒后摄像头烧掉、存储分区读不出来。而这套秩序的维护者就是今天要聊的DPM框架Device Power Management设备电源管理。DPM是整个系统睡眠流程里的核心转运层。它不负责具体某款芯片怎么断电而是负责“什么时候断、按什么顺序断、断了之后如何分级恢复”。这个框架对任何做嵌入式Linux、Android底层或者服务器功耗优化的人都值得吃透。文章会先从设计动机讲起再拆链表结构、回调阶段和状态迁移最后给出实际排查睡眠问题的三板斧。希望你看完能直接拿这套思路去分析自己的板子。1. DPM框架解决什么问题1.1 为什么“按顺序”关设备这么难外行看系统睡眠觉得无非是CPU进WFI、DDR自刷新外围设备断电而已。但一台真实设备上主控、存储、Wi-Fi/蓝牙模组、电源IC、传感器之间盘根错节存储依赖eMMC控制器控制器依赖时钟和稳压器Wi-Fi模组的唤醒引脚又挂在某个GPIO上。如果你直接粗暴断电轻则唤醒信号传不进来重则设备正在写Flash时被掐电数据损坏甚至固件丢失。所以DPM框架的第一个使命就是把“依赖关系”转换成可执行的顺序。挂起时先让依赖方停止再让被依赖方停止唤醒时严格反过来。例如SD卡控制器和SD卡本身睡眠时一定是先让SD卡停止访问再关控制器的时钟和电源。唤醒则必须先把控制器时钟打开再去初始化卡。这套顺序在Linux里并不是靠驱动自觉遵守而是靠DPM框架的链表遍历次序和父子关系强制保证的。1.2 DPM在整个功耗子系统里的位置很多初学者会把DPM和Runtime PM运行时电源管理混在一起。准确说Runtime PM管的是设备“不用时自动关闭”的动态过程比如USB设备空闲3秒自动挂起DPM管的是系统级睡眠这种全局状态切换。在系统睡眠时DPM会强制把所有设备纳入管理不管它平时是否开了Runtime PM。从代码层次看Linux功耗管理大致分三层cpuidle管CPU空闲cpufreq管CPU频率DPM则管设备群的整体下沉。再往下还有syscore机制负责在设备全部停掉之后处理时钟、电源域等最基础的“最后一批”节点。DPM框架主要实现在drivers/base/power/main.c是Linux通用PM核心PM Core最主要的工作模块之一。1.3 为什么拆成多个“小阶段”而不是一把梭DPM不是一次性把所有设备挂起而是设计了多个小阶段最典型的是suspend、suspend_late、suspend_noirq。这背后的原因很实在不同阶段系统对中断、时钟、电源的可用性不同设备的可操作空间也不同。阶段中断可用性驱动此时能做什么suspend中断可用正常向设备发睡眠命令、保存上下文、停止DMAsuspend_late大部分中断可用调度受限处理需要关闭门控、清理总线的设备suspend_noirq中断基本掩蔽中断控制器冻结前最后一批设备配置唤醒源、复位相关逻辑设计成多阶段还有一个好处唤醒时可以按严格的逆序恢复。最先挂起的设备最后恢复最后挂起的设备最先恢复。这样能保证任何设备恢复运行时它所依赖的底层基础已经就绪。在深度睡眠S3suspend-to-RAM和冻结S2idle两种模式下这套框架是同一套只是平台固件参与深度不同。2. 硬核机制全局链表、回调矩阵与状态迁移2.1 DPM那几张全局链表到底在管什么看DPM代码首先接触的就是全局链表。它们不是简单的“所有设备列表”而是设备睡眠状态的“分组标签”。设备挂起过程其实就是不停把自己从当前链表摘下来挂到下一张链表上。内核里定义的核心链表有这几条LIST_HEAD(dpm_list); LIST_HEAD(dpm_prepared_list); LIST_HEAD(dpm_suspended_list); LIST_HEAD(dpm_late_early_list); LIST_HEAD(dpm_noirq_list);dpm_list系统里所有注册设备的完整名单。设备注册时通过device_add()挂进来睡眠期间的遍历都以这张表为起点。dpm_prepared_list已经完成prepare阶段、允许进入睡眠的设备。dpm_suspended_list完成常规suspend阶段、停在第一级睡眠状态的设备。dpm_late_early_list完成late_suspend的设备或者唤醒时已完成early_resume的设备看当前方向。dpm_noirq_list完成noirq挂起的设备。唤醒时这批设备会最先被恢复。每次从dpm_list上摘节点、挂到下一张链表都对应一次回调调用。如果某步失败DPM会启动回滚把已经挂到深处列表里的设备重新唤回正常状态。这个“摘下来、挂上去”的迁移动作就是整个框架最核心的运转方式。2.2 dev_pm_ops回调矩阵怎么选驱动侧提供的电源管理回调定义在struct dev_pm_ops里。常规睡眠、休眠到磁盘hibernation、系统关机这三种场景会映射到不同的回调组合。场景挂起阶段回调唤醒阶段回调普通睡眠S3/S2idlesuspend、suspend_late、suspend_noirqresume_noirq、resume_early、resume休眠到磁盘时保存镜像freeze、freeze_late、freeze_noirqthaw_noirq、thaw_early、thaw休眠到磁盘后真正断电poweroff、poweroff_late、poweroff_noirqrestore_noirq、restore_early、restore并不是每个设备都需要实现全部回调。很多驱动只实现suspend和resume框架会把其它阶段自动跳过。设备也可以选择在prepare阶段返回-EBUSY主动拒绝进入睡眠。子系统级的PM回调优先级高于驱动自身回调这是内核一贯的“子系统统管、驱动补充”思路。2.3 状态迁移中的辅助变量设备在睡眠过程中有几个关键标志位排查问题时经常要跟它们打交道dev-power.is_suspended设备当前处于挂起状态。dev-power.direct_complete允许设备跳过部分睡眠流程前提是它没有任何唤醒依赖。dev-power.may_async允许异步睡眠。dev-power.wakeup_path该设备是否处于唤醒路径上决定唤醒源是否需要保持。这些标志位会在链表迁移前后被设置或清除。调试时如果发现某个设备没有执行预期回调第一件事就是查这几个标志有没有被异常置位。比如direct_complete被置位后设备直接不走suspend这在很多老旧驱动上会引发“明明睡眠了数据却丢了”的奇怪问题。3. 一次完整睡眠中DPM经历的全过程3.1 从echo mem到DPM接管用户空间通常执行echo mem /sys/power/state触发睡眠。这个动作最终走到kernel/power/suspend.c里的suspend_devices_and_enter()。这个函数会依次组织DPM各阶段、平台固件睡眠调用以及唤醒后的DPM恢复。大致调用链如下pm_suspend() → enter_state() → suspend_devices_and_enter() → dpm_suspend_start() // 内部包含 prepare suspend suspend_late → 平台固件进入睡眠ACPI/ATF相关操作 → dpm_suspend_noirq() → 处理器真正进入低功耗态 → dpm_resume_noirq() → dpm_resume_end() // 内部包含 resume_early resume complete这里有个容易误读的点并非所有平台都在同一个位置执行dpm_suspend_noirq。有的平台在关闭非引导CPU之后才调用有的则在平台固件入口之前。理解这个差异对阅读具体平台代码很有帮助。3.2 三个挂起批次每次翻转一批链表的顺序DPM把设备挂起分批进行每批结束对应的全局链表内容就“翻转”一次。第一轮从dpm_list出发反向遍历逐个执行prepare和suspend。为什么反向因为dpm_list是设备注册顺序父设备先注册子设备后注册。反向遍历可以保证后注册的子设备先挂起符合“先挂起依赖方”的原则。第二轮执行dpm_suspend_late此时设备已经离开dpm_suspended_list进入dpm_late_early_list。第三轮再执行dpm_suspend_noirq设备进入dpm_noirq_list。唤醒时反过来先从dpm_noirq_list正序遍历恢复再逐级回到dpm_list。3.3 异步睡眠是怎么保证不乱序的现代内核为了提高睡眠速度引入了异步挂起。设备在may_async标志允许时不一定严格按照链表顺序等待而是由工作队列并发执行。为了不破坏依赖关系DPM引入了dpm_wait()机制一个设备开始挂起前会等待它依赖的父设备或消费者设备到达指定状态没有依赖关系的设备才真正并行。这意味着设备睡眠完成次序不再严格等于链表顺序但依赖关系始终被保证。驱动开发者在写异步睡眠代码时不要自己另搞一套并发控制直接用框架的async_schedule()相关接口否则很容易跟DPM的等待逻辑打架。3.4 唤醒流程的“逆向恢复”逻辑唤醒流程是挂起的严格镜像。从唤醒中断到达开始内核先恢复noirq阶段的设备再恢复late_early阶段的设备最后恢复常规resume设备并执行complete。这样设计的原因也很简单一个设备要工作它依赖的控制器、时钟、电源域必须先准备好。比如网卡唤醒系统后内核第一件事就是恢复中断控制器和时钟框架接下来才轮到网卡驱动去查唤醒原因。这里有一个很值得推荐的实践设备驱动在resume回调里不要急着访问硬件先用dev_pm_ops里的resume_early把最必要的时钟和电源恢复再把复杂初始化丢给resume。这样能显著降低唤醒阶段的时序风险。4. 挂起失败排查三板斧日志、pm_test 与 ftrace4.1 先看 dmesg失败现场都写在里面设备睡眠失败时内核打印的日志含金量极高。最常见的格式是[ 123.456] PM: dpm_run_callback(): usb_dev_suspend0x0/0x1c returns -110 [ 123.456] PM: Device 1-2: failed to suspend: error -110 [ 123.456] PM: Some devices failed to suspend, or early wake event detected看到这类日志第一件事就是去查对应驱动在usb_dev_suspend里为什么会返回-110超时。多数原因是设备正在传输数据或者固件没有在预期时间内回ACK。遇到这种问题先检查该设备是否存在未完成的异步IO或DMA。还有一个容易被忽略的点如果系统开启CONFIG_PM_DEBUG可以通过调试接口打开更细粒度的DPM日志echo enabled /sys/power/pm_debug_messages开启之后核心会打印每个设备的挂起、跳过、错误详情。这对定位“哪个设备拖慢了整个睡眠流程”非常有用。4.2 用 /sys/power/pm_test 把睡眠拆成舞台剧很多板子睡眠失败问题并不在DPM本身而是平台固件那一层。为了区分内核提供了pm_test接口。它的原理是在睡眠流程的不同位置主动打断模拟“走到某一步就立即唤醒”从而把问题范围缩小。echo core /sys/power/pm_test # 只做核心流程不真正掉电 echo process /sys/power/pm_test echo devices /sys/power/pm_test echo platform /sys/power/pm_test echo none /sys/power/pm_test我习惯按“platform → devices → process → core”从外到内逐级测试。如果platform模式就失败说明平台固件和内核对接有问题如果devices模式失败才轮到去查DPM里的具体设备。这个接口极大减少了“唤醒后谁也没日志”的排查黑盒。注意测试完毕务必恢复成none否则真实睡眠流程会一直被打断。4.3 用 ftrace 看设备睡眠时序当问题指向DPM本身比如“有个设备睡眠特别慢”或者“两个设备顺序错了”我最常用的工具是ftrace。跟踪suspend_resume事件可以直接看到设备在睡眠与唤醒时的时间戳。cd /sys/kernel/tracing echo 0 tracing_on echo 1000 buffer_size_kb echo suspend_resume set_event echo 1 tracing_on sleep 5 echo mem /sys/power/state echo 0 tracing_on cat trace | grep dpm\|suspend_time通过打印出来的时间戳能非常直观地看到哪个设备耗时最多。我遇到过某个触摸屏驱动睡眠要花800ms整个睡眠流程被它拖到1秒以上。换用ftrace一眼锁定后来发现是该驱动在suspend里做了无意义的固件刷新等待。4.4 唤醒失败与唤醒源排查唤醒问题的典型特征是“睡眠成功了但怎么都唤不醒”。这时DPM已经不是主角要重点看唤醒源配置。检查这些信息的快捷路径cat /sys/kernel/debug/wakeup_sources这个文件会列出所有注册的唤醒源、激活次数和最近一次唤醒时间。如果一个设备本应能唤醒系统但该表里没有它说明设备驱动没有正确注册唤醒源。另一类问题是唤醒中断被错误路由设备虽然注册了唤醒源但中断控制器在睡眠时把它屏蔽了。这种问题往往要用irq相关的trace去追。4.5 常见问题速查表症状可能原因建议动作某个设备failed to suspend -110设备忙、DMA未停止检查异步IO强制停流后再睡眠睡眠耗时过长某设备suspend回调等待太久用ftrace锁定时序热点唤醒后外设无响应恢复顺序错误或未恢复电源域检查resume_early是否恢复时钟/电源direct_complete跳过回调导致状态丢失设备被错误标记为可直接完成检查设备依赖与dev-power.direct_complete唤醒中断到达但系统不醒唤醒源未注册或中断路由问题查wakeup_sources和中断控制器配置pm_test恢复不了真实睡眠测试模式未关闭置为none5. 我在实际项目中反复踩过的几个坑5.1 不看电源域先把时钟关了早年调试一个MMC设备睡眠问题驱动在suspend里关闭了时钟但电源域还在供电。结果唤醒后寄存器全读回来为0驱动以为自己被复位了重新初始化一遍本来20ms的唤醒硬是拖到300ms。DPM框架只负责调设备顺序它并不知道某个时钟对某个电源域意味着什么。真正了解硬件约束、确保时钟和电源域关闭顺序合理的还是驱动自己。所以写睡眠回调前先把SoC的电源域拓扑画清楚。5.2 异步挂起的“假并发”问题有段时间我为了让睡眠更快给多个设备开启了async_suspend。结果发现每次睡眠都会随机出现某个设备丢唤醒事件。查到最后原因是两个设备共享同一个电源域异步挂起后它们的时序不再严格同步一个设备把电源域关了另一个设备还在发命令。框架的异步机制保证的是“依赖顺序”不保证“同一电源域内的兄弟设备同步”。遇到共享电源域的设备建议要么关闭异步挂起要么在驱动里自己加同步锁。5.3 唤醒源没勾选整晚白睡另一个深刻的教训某次用户反馈设备功耗异常高拿示波器一测系统根本没进底层睡眠。查日志发现睡眠流程走到平台固件阶段就返回了原因是有一个触摸屏把enable_irq_wake注册在了驱动加载阶段但在睡眠前被Runtime PM自动关闭了电源域导致唤醒中断物理上已经无法到达。这个问题看wakeup_sources根本查不出来因为唤醒源还在只是电源域没了。最后是在驱动的suspend_noirq阶段重新确保唤醒中断对应的电源域保持供电才算解决。5.4 调试DPM的一个心法最后分享一个经验之谈DPM框架本身已经很成熟绝大多数问题都出在驱动对硬件状态的理解上。遇到诡异问题不要急着怀疑框架先问三个问题设备当前在哪个链表电源域和时钟是否满足当前阶段的要求唤醒中断路径上有没有东西被提前掐断。把这三件事捋清楚90%的睡眠问题都能定位到具体驱动。我自己跟踪这个系列的时候每次重读main.c里的链表操作都会有新的体会。DPM看着复杂说到底就是一套“先挂起的先恢复、后挂起的先挂起”的秩序管理。你觉得它简单时往往说明某个依赖在你没注意的地方埋了雷。
返回列表