ARTICLE DETAIL

资讯详情

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

Linux runtime pm 深度解析:引用计数、状态机与驱动接入实战

Linux runtime pm 深度解析:引用计数、状态机与驱动接入实战 1. 从一次待机电流超标说起runtime pm 到底在管什么前阵子帮朋友看一块嵌入式板子现象很典型系统跑起来功能都正常但只要一进待机整板电流就比规格书标称的高出一大截电池撑不住。抓了几天波形、翻了一堆寄存器最后定位到的不是硬件漏电而是几个外设的时钟和电源域压根没在空闲时被关掉——驱动里 suspend/resume 回调写得挺全可运行时那套动态管理根本没接上。这个问题逼着我把 Linux 内核功耗子系统里的runtime pm从头到尾捋了一遍也就是这篇要聊的东西。先把概念摆正。Linux 的电源管理大致分两条线一条是系统级睡眠system suspend / hibernation整机进低功耗所有设备跟着走一遍 suspend 回调另一条就是runtime pm它管的是系统还在正常运行时单个设备在“空闲”和“活跃”之间来回切换。你可以把它理解成每个设备自带的一个小开关没人用它的时候自动把时钟关掉、电源域降下来一旦有人要用立刻唤醒。它和系统级睡眠最大的区别在于——粒度是单个设备触发时机是运行中而不是整机挂起。为什么需要这么一套东西因为现代 SoC 上挂的外设太多了屏幕、摄像头、WiFi、各种传感器、存储控制器如果全都等到整机睡眠才省电那系统正常跑着的时候功耗就白白浪费了。runtime pm 的价值就在于把省电这件事下沉到“设备空闲的每一刻”。对做嵌入式、做移动端、做任何对功耗敏感的 Linux 产品的人来说这套机制是绕不开的基本功。这篇内容适合谁看如果你已经写过字符设备驱动、知道probe和remove大概怎么回事但对pm_runtime_get_sync和pm_runtime_get的区别、对runtime_status那几个状态到底怎么流转一直模模糊糊那这篇就是给你准备的。我会从struct device里那几个和 pm 相关的字段讲起把 PM Core 的调用链、引用计数机制、和系统级睡眠的交互、以及实际调试时最容易踩的坑一条条拆开。全程按“为什么这么设计”来讲而不是干巴巴列 API。2. struct device 里藏着的 runtime pm 家当要理解 runtime pm第一步得知道它的状态存在哪。答案就在每个设备都有的struct device里。这个结构体是内核设备模型的基石runtime pm 的所有运行时信息都挂在它身上不需要额外分配一个“pm 对象”。这种设计的好处是设备生命周期和 pm 状态天然绑定设备没了pm 状态自然也就没了。2.1 三个核心字段power、links、parent打开include/linux/device.h和 runtime pm 直接相关的字段主要有这么几组。第一组是struct dev_pm_info power这是个内嵌的结构体里面装着当前电源状态、是否可以被 runtime 管理、引用计数、回调函数指针等等可以说是 runtime pm 的“主控台”。第二组是struct dev_pm_domain *pm_domain指向设备所属的电源域很多 SoC 会把一组设备的电源管理打包成一个 domain 统一处理。第三组是struct device *parent这个字段本身不是为 pm 设计的但 runtime pm 会利用设备树的父子关系做级联管理——子设备活跃时父设备不能被 runtime 挂起。dev_pm_info里面又细分了一堆字段我挑几个最关键的enum rpm_status runtime_status当前运行时状态取值有RPM_ACTIVE、RPM_RESUMING、RPM_SUSPENDED、RPM_SUSPENDING。注意后两个是过渡态表示正在切换过程中。int runtime_error上一次 resume 失败留下的错误码这个字段在排查“设备起不来”时特别有用。atomic_t usage_count引用计数这是 runtime pm 的核心机制后面单独讲。unsigned int disable_depth禁用深度大于 0 表示 runtime pm 被临时关掉了。unsigned int runtime_auto是否允许自动挂起对应 sysfs 里的control文件。const struct dev_pm_ops *ops指向驱动的 pm 回调集合runtime_suspend、runtime_resume、runtime_idle都在里面。2.2 为什么状态要放在 device 而不是驱动私有数据里这里有个设计上的取舍值得说。理论上驱动完全可以自己在私有结构体里维护一个“我是不是空闲”的标志但内核选择把它统一放进struct device原因是PM Core 需要在不了解具体驱动的前提下做统一调度。比如设备树里的父子级联、比如 sysfs 暴露的power/目录、比如和系统级睡眠的协调这些都需要一个全局可见的状态源。如果状态散落在各个驱动里PM Core 就没法统一管理了。提示调试时可以直接cat /sys/devices/.../power/runtime_status看当前状态cat .../power/control看是 auto 还是 on这两个文件是排查 runtime pm 问题的第一入口。2.3 设备树里的父子关系如何影响 pm设备树天然是一棵树struct device的parent指针就来自这棵树。runtime pm 利用这个关系实现了一个很重要的约束只要有一个子设备处于 active父设备就不能进入 suspended。这个约束是通过引用计数实现的——子设备 resume 的时候会顺带把父设备的计数加一。这样设计是为了保证电源域的正确性如果父设备代表一个电源域子设备还在工作父域当然不能关。理解这一点很关键因为很多“明明设备空闲了却挂不下去”的问题根因就是某个子设备或者某个没释放的引用把父设备顶住了。后面讲排查的时候会再回到这里。3. 引用计数runtime pm 的心脏runtime pm 最核心、也最容易用错的东西就是引用计数。它决定了设备什么时候该被挂起、什么时候必须保持活跃。搞懂它runtime pm 就懂了一大半。3.1 usage_count 的加减逻辑usage_count是一个原子计数初始值通常是 1表示设备刚 probe 完是活跃的。每次调用pm_runtime_get系列函数计数加一每次调用pm_runtime_put系列函数计数减一。只有当计数减到 0 时PM Core 才会尝试去挂起设备。换句话说计数大于 0 就意味着“有人正在用这个设备别动它”。这个模型非常直观谁要用设备谁就 get 一下用完了put 一下。多个使用者可以同时持有引用互不干扰。比如一个 I2C 控制器可能同时被触摸屏和传感器驱动使用两边各自 get/put只要还有一方在用控制器就不会被挂起。3.2 get 和 get_sync 的区别别再用错这是新手最容易混淆的地方我见过太多驱动里随手写pm_runtime_get然后直接访问寄存器结果偶发挂死。区别在于pm_runtime_get/pm_runtime_get_noresume只增加计数不等待设备真正 resume。如果设备当前是 suspended调用后它只是被“标记为需要活跃”实际 resume 是异步的。pm_runtime_get_sync增加计数并且同步等待设备完成 resume返回时设备一定处于 active 状态。所以规则很简单如果你 get 之后马上就要访问硬件寄存器必须用get_sync。用get的话设备可能还没上电你访问寄存器就是访问一片未初始化的地址轻则读到垃圾数据重则总线挂死。/* 正确访问硬件前用 sync 版本 */ ret pm_runtime_get_sync(dev); if (ret 0) { pm_runtime_put_noidle(dev); return ret; } /* 此时设备已 active可以安全读写寄存器 */ val readl(dev-base REG_CTRL); /* 用完释放 */ pm_runtime_put(dev);注意上面错误处理里的pm_runtime_put_noidle。当get_sync失败时计数其实已经被加过了必须用_noidle版本减回去否则计数泄漏设备永远挂不下去。这个细节很多人不知道是实打实的坑。3.3 put 的几种变体和 idle 触发put 系列也有讲究pm_runtime_put计数减一如果减到 0会触发一次 idle 检查可能异步挂起设备。pm_runtime_put_sync计数减一并同步等待挂起完成。pm_runtime_put_autosuspend计数减一但走 autosuspend 延迟挂起路径不会立即挂。pm_runtime_put_noidle只减计数不触发 idle 检查专门用于错误回滚。什么时候用哪个一般用完设备用pm_runtime_put或pm_runtime_put_autosuspend就够了。put_sync用在需要确保设备立刻下电的场景比如你要手动控制上电时序。noidle基本只出现在错误处理路径里。3.4 autosuspend给设备一个“冷静期”有些设备频繁地被短暂使用比如每次读一个传感器值就 get/put 一次。如果每次 put 都立刻挂起那 resume 的开销时钟稳定、寄存器重配可能比省下的电还多。这时候就要用autosuspend。机制是这样的调用pm_runtime_use_autosuspend(dev)开启再用pm_runtime_set_autosuspend_delay(dev, delay_ms)设置延迟。之后每次pm_runtime_put_autosuspendPM Core 不会立即挂起而是启动一个延迟定时器等delay_ms毫秒内没有新的 get才真正挂起。如果这期间又有 get定时器取消设备保持活跃。/* 在 probe 里配置 */ pm_runtime_set_autosuspend_delay(dev, 200); /* 200ms 冷静期 */ pm_runtime_use_autosuspend(dev); pm_runtime_set_active(dev); pm_runtime_enable(dev); /* 使用后 */ pm_runtime_mark_last_busy(dev); /* 更新最后活跃时间 */ pm_runtime_put_autosuspend(dev);pm_runtime_mark_last_busy这个调用容易被忽略它的作用是刷新“最后活跃时间戳”让延迟从这一刻重新算起。如果你 put 之前不 mark延迟可能从更早的时间点算设备会提前挂起。注意autosuspend 的延迟值不是拍脑袋定的。要结合设备的 resume 耗时来算——延迟至少应该大于 resume 时间否则省电收益会被频繁唤醒抵消。我一般先用 100~500ms 试再根据实测电流曲线调。4. PM Core 的调用链一次 get_sync 背后发生了什么光知道 API 怎么用还不够真正排查问题时你得知道调用链走到哪一步了。这一节把pm_runtime_get_sync到驱动回调的完整路径捋一遍。4.1 从 pm_runtime_get_sync 到 __pm_runtime_resumepm_runtime_get_sync是个内联包装最终会调到__pm_runtime_resume(dev, RPM_GET_PUT)。这个函数做几件事先检查disable_depth如果 runtime pm 被禁用了直接返回然后原子地增加usage_count接着判断当前状态如果已经是 active 就直接返回否则进入 resume 流程。resume 流程的核心是rpm_resume。它会先把状态置为RPM_RESUMING然后调用__rpm_callback最终走到驱动的runtime_resume回调。如果设备有 parent还会先确保 parent 是 active 的——这就是前面说的级联约束。4.2 runtime_resume 回调里该做什么驱动的runtime_resume回调职责很明确把设备从低功耗状态恢复到可工作状态。典型操作包括使能时钟、打开电源域、恢复寄存器上下文、重新初始化总线。注意这里不要做太重的初始化因为 runtime resume 可能很频繁重活应该放在 probe 里做一次。static int mydev_runtime_resume(struct device *dev) { struct mydev *d dev_get_drvdata(dev); int ret; ret clk_prepare_enable(d-clk); if (ret) return ret; /* 恢复关键寄存器 */ mydev_restore_regs(d); return 0; }对应的runtime_suspend就是反过来保存上下文、关时钟、断电源。这两个回调必须成对、可重入因为 PM Core 可能在任何时候调用它们。4.3 runtime_idle 的角色runtime_idle是个容易被忽略的回调。当usage_count减到 0 时PM Core 先调用runtime_idle而不是直接 suspend。这个回调给了驱动一个“决定要不要挂起”的机会。默认行为不实现该回调是直接触发 suspend。有些驱动会在这里判断设备是否真的空闲或者启动一个延迟。大多数情况下你不需要实现runtime_idle用 autosuspend 就够了。但如果你有特殊的空闲判断逻辑可以在这里做。4.4 状态机全貌把状态流转画成一张表更清楚当前状态触发动作目标状态说明RPM_ACTIVEput 且计数归零RPM_SUSPENDING进入挂起流程RPM_SUSPENDINGsuspend 回调成功RPM_SUSPENDED挂起完成RPM_SUSPENDINGsuspend 回调失败RPM_ACTIVE回滚记录 runtime_errorRPM_SUSPENDEDgetRPM_RESUMING进入恢复流程RPM_RESUMINGresume 回调成功RPM_ACTIVE恢复完成RPM_RESUMINGresume 回调失败RPM_SUSPENDED回滚记录 runtime_error这张表在排查“状态卡在 RESUMING”这类问题时特别有用。如果你看到runtime_status一直是RPM_RESUMING基本可以断定 resume 回调里卡住了多半是在等某个锁或者某个时钟没起来。5. runtime pm 和系统级睡眠的交接runtime pm 不是孤立存在的它必须和系统级睡眠suspend to RAM 那套协调好。这两者的关系如果没理清很容易出现“系统睡眠时设备状态错乱”的问题。5.1 系统睡眠时 runtime pm 怎么处理当整机要进入系统级睡眠时PM Core 会遍历所有设备对每个设备调用系统级的suspend回调。但这里有个前提如果设备当前是 runtime active 的系统级 suspend 之前需要先把它 runtime 挂起或者至少保证状态一致。内核的处理方式是在系统 suspend 流程中会先调用pm_runtime_resume确保设备处于已知状态然后再走系统级 suspend。反过来系统 resume 之后设备不会自动回到 runtime suspended而是保持 active等下一次 idle 再挂起。5.2 为什么有些驱动要区分 runtime 和 system 回调dev_pm_ops里有两套回调runtime_suspend/resume和suspend/resume系统级。很多简单驱动会让它们指向同一个函数但严格来说两者语义不同runtime 回调设备空闲时调用可能非常频繁要求快速、轻量。system 回调整机睡眠时调用频率低可以做更彻底的省电操作比如完全断电。如果一个设备在 runtime suspend 时只是关时钟而在 system suspend 时需要彻底断电那就必须分开实现。混用会导致要么 runtime 太耗电要么 system 恢复太慢。5.3 一个真实的交接 bug我遇到过一个案例某驱动在runtime_suspend里把电源域关了但系统级suspend回调里又去访问了这个电源域下的寄存器结果系统睡眠时直接挂死。根因就是没理清两套回调的执行顺序——系统 suspend 之前设备可能已经被 runtime 挂起、电源已断此时再访问寄存器就是访问死区。修复方式是在系统级suspend回调开头先pm_runtime_get_sync把设备唤醒操作完再 put。或者更规范的做法是让系统级回调不依赖硬件状态只做纯软件的状态保存。提示判断一个驱动是否处理好了交接看它在系统级 suspend/resume 里有没有考虑 runtime 状态。如果完全没有pm_runtime_*调用多半有隐患。6. 调试 runtime pm 的实战套路理论讲完落到实操。runtime pm 的问题往往表现为“设备该睡不睡”或者“该醒不醒”排查起来需要一套系统的方法。6.1 先看 sysfs再看计数第一步永远是 sysfs。/sys/devices/.../power/目录下有这几个关键文件runtime_status当前状态active 还是 suspended。runtime_usage当前 usage_count 的值。runtime_active_kids有多少子设备是 active 的。controlauto 或 onon 表示禁止 runtime 挂起。autosuspend_delay_ms当前 autosuspend 延迟。如果runtime_status是 active 但设备明明没人用先看runtime_usage。如果它大于 0说明有引用没释放去找谁 get 了没 put。如果等于 0 但还是 active看runtime_active_kids可能是子设备顶住了。6.2 用 ftrace 追调用链sysfs 只能看结果要看过程得上 ftrace。内核的 pm 子系统有专门的 tracepoint# 开启 pm runtime 相关 trace echo 1 /sys/kernel/debug/tracing/events/power/enable cat /sys/kernel/debug/tracing/trace_pipe你会看到pm_runtime_resume、pm_runtime_suspend、pm_runtime_idle这些事件的完整调用记录包括设备名、耗时、调用者。排查“谁在频繁唤醒设备”时这个输出直接告诉你答案。6.3 常见问题对照表现象可能原因排查方向设备一直 activeusage_count 泄漏检查 get/put 是否配对错误路径是否漏 put设备一直 active子设备顶住看 runtime_active_kids逐层往下查设备一直 activecontrol 是 on检查是否有人写了 on或 disable_depth 0设备频繁 resumeautosuspend 延迟太短调大 delay或检查是否有人频繁 getresume 失败时钟/电源未就绪看 runtime_error检查 resume 回调状态卡在 RESUMINGresume 回调死锁用 ftrace 看卡在哪个函数6.4 一个计数泄漏的定位过程说个我实际踩的坑。某驱动在 probe 里调了pm_runtime_get_sync但 remove 路径里忘了 put导致设备卸载后计数还挂着。表现是设备明明已经 remove 了父设备的runtime_active_kids还是 1整个电源域关不掉。定位方法是打开 ftrace过滤这个设备的 pm 事件发现只有 resume 没有 suspend。再回头看代码probe 里 get 了但错误分支和 remove 里都没 put。修复就是在 remove 和所有错误分支补上pm_runtime_put_noidle或pm_runtime_put_sync。这个坑的教训是get 和 put 必须成对出现在所有代码路径上包括错误路径。写驱动时我习惯把 get 放在函数开头然后用 goto 统一处理错误确保每条路径都会 put。7. 把 runtime pm 接进驱动的完整姿势最后把前面所有东西串起来讲一个驱动从零接入 runtime pm 的标准流程。这套流程我用了很多次基本可以照抄。7.1 probe 里的初始化顺序顺序很重要错了会导致状态不一致static int mydev_probe(struct platform_device *pdev) { struct device *dev pdev-dev; struct mydev *d; int ret; d devm_kzalloc(dev, sizeof(*d), GFP_KERNEL); if (!d) return -ENOMEM; platform_set_drvdata(pdev, d); /* 1. 先做硬件初始化此时设备是 active 的 */ ret mydev_hw_init(d); if (ret) return ret; /* 2. 配置 autosuspend */ pm_runtime_set_autosuspend_delay(dev, 200); pm_runtime_use_autosuspend(dev); /* 3. 标记当前为 active因为硬件已经初始化好了 */ pm_runtime_set_active(dev); /* 4. 最后使能 runtime pm */ pm_runtime_enable(dev); return 0; }关键点pm_runtime_set_active必须在pm_runtime_enable之前调用。因为 enable 之后 PM Core 会认为设备处于 suspended 状态默认如果实际硬件是 active 的就会状态不一致。先 set_active 告诉内核“我现在是活的”再 enable状态才对得上。7.2 remove 里的清理remove 要保证设备被正确挂起计数清零static int mydev_remove(struct platform_device *pdev) { struct device *dev pdev-dev; pm_runtime_disable(dev); pm_runtime_set_suspended(dev); pm_runtime_put_noidle(dev); mydev_hw_deinit(dev_get_drvdata(dev)); return 0; }pm_runtime_disable会阻止后续的 runtime 操作set_suspended把状态归位put_noidle清掉 probe 时可能残留的引用。这三步做完设备才算干净地退出。7.3 运行时使用的标准模板在真正干活的函数里模式是固定的static int mydev_do_something(struct device *dev) { int ret; ret pm_runtime_get_sync(dev); if (ret 0) { pm_runtime_put_noidle(dev); return ret; } /* 访问硬件 */ ret mydev_access_hw(dev); pm_runtime_mark_last_busy(dev); pm_runtime_put_autosuspend(dev); return ret; }这个模板覆盖了 90% 的场景。记住三件事get_sync 后判返回值、访问完 mark_last_busy、用 put_autosuspend 释放。7.4 几个反复踩的坑第一个坑是在中断上下文里用 get_sync。get_sync会睡眠等待中断上下文不能睡眠会直接报错。中断里要用pm_runtime_get异步版或者干脆在中断上半部只做标记下半部再处理。第二个坑是忘记处理 resume 失败。get_sync返回负值表示 resume 失败此时设备不可用必须回滚计数并返回错误。我见过驱动忽略返回值直接访问寄存器结果 resume 失败时访问了未上电的硬件。第三个坑是autosuspend 和手动 suspend 混用。如果开了 autosuspend就不要再手动调pm_runtime_suspend两者会打架。统一走 put_autosuspend 让 PM Core 调度。第四个坑是父设备没使能 runtime pm。子设备 resume 时会尝试唤醒父设备如果父设备没 enable整个级联就断了。确保设备树路径上所有设备都正确接入了 runtime pm。7.5 验证是否真的省电了代码写完不代表就省电了。验证方法是让设备进入空闲用电流表或者 SoC 内部的功耗计数器看实际电流有没有降下来。如果代码逻辑都对但电流没降可能是时钟没真正关、电源域没断或者有其他设备在偷偷唤醒。我一般会配合 ftrace 看一段时间内 suspend/resume 的次数。如果次数远高于实际使用频率说明 autosuspend 延迟太短或者有异常唤醒源需要进一步查。8. 写在最后的一点个人体会runtime pm 这套机制刚接触时觉得 API 挺多、状态挺绕但用熟之后会发现它的设计其实很克制——核心就是引用计数加状态机剩下的都是围绕这两样东西的配套。真正难的不是记住 API而是理解“什么时候该 get、什么时候该 put、状态什么时候会变”这三件事背后的因果。我自己最大的教训是早期写驱动时把 runtime pm 当成可选项觉得不接也能跑。结果就是设备功耗一直下不来等到产品要过功耗测试才回头补那时候改动面已经很大了。所以如果你现在正在写新驱动建议从第一版就把 runtime pm 接进去哪怕一开始只是最简单的 get_sync/put 配对也比后面补要省事得多。另外提一句不同内核版本的 runtime pm 实现细节有差异比如 autosuspend 的默认行为和 tracepoint 的名字在 4.x 和 5.x、6.x 之间都有变化。看代码时以你实际用的内核版本为准别拿网上的老文章硬套。遇到状态对不上先cat runtime_status和runtime_usage再上 ftrace基本没有查不出来的问题。
返回列表