ARTICLE DETAIL

资讯详情

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

Linux Runtime PM 深度解析:引用计数、状态机与驱动集成实战

Linux Runtime PM 深度解析:引用计数、状态机与驱动集成实战 1. Runtime PM 到底在解决什么问题第一次接触runtime pm的人十有八九会被它那一堆回调函数和引用计数绕晕。我在带新人的时候最常听到的抱怨就是设备明明已经没人用了为什么功耗还是下不去。这个问题恰恰就是 runtime pm 存在的意义。先说结论runtime pm 是 Linux 内核功耗子系统里负责设备级动态电源管理的那一层。它要干的事情很朴素——当某个设备空闲时自动把它挂起省电当有人要用它时再自动唤醒。整个过程对驱动开发者透明对上层应用也透明。为什么需要这么一层因为现代 SoC 上挂的外设太多了。屏幕、摄像头、WiFi、蓝牙、SD 卡、各种传感器如果每个设备都一直保持上电状态待机功耗会非常难看。但你又不能简单粗暴地一刀切因为有些设备随时可能被唤醒有些设备挂起恢复的代价很高。runtime pm 就是在这中间做权衡的那套机制。它和系统级的 suspend/resume 是两码事。系统级 suspend 是整个机器进入低功耗状态用户能感知到睡眠runtime pm 是单个设备层面的用户完全无感。你可以理解为系统 suspend 是整栋楼熄灯runtime pm 是某个房间没人就关灯。这套机制的核心数据结构是struct dev_pm_info它嵌在struct device里面。也就是说每一个 device 天生就带着 runtime pm 的能力用不用是另一回事。这个设计很关键后面讲引用计数的时候会反复用到。适合谁来读这篇如果你在做嵌入式 Linux 驱动开发尤其是涉及电源管理、外设驱动、SoC 平台适配那 runtime pm 是绕不过去的。如果你只是做应用层开发了解一下概念就够了不用深挖回调实现。我下面会从框架设计讲到实操细节尽量让不同基础的人都能拿到东西。2. 核心设计思路与关键数据结构拆解2.1 为什么用引用计数而不是简单的开关很多人第一反应是设备空闲就挂起有人用就唤醒搞个布尔标志不就行了实际上不行因为一个设备可能同时被多个使用者引用。举个实际例子一个 I2C 控制器上挂了温度传感器和 EEPROM 两颗芯片。温度传感器在周期性采样EEPROM 偶尔被读写。如果只用布尔标志温度传感器采样完把设备挂起EEPROM 正在读写就被打断了。引用计数就是为了解决这种多方共享的问题——只要还有任何一个使用者持有引用设备就不能挂起。内核里的实现是dev-power.usage_count类型是atomic_t。每次pm_runtime_get加一pm_runtime_put减一。当计数归零并且没有其他阻止条件时才会触发挂起流程。这里有个容易踩的坑usage_count是原子操作但判断是否挂起的逻辑不是原子的。所以内核用了dev-power.lock自旋锁来保护状态机。你在写驱动的时候不需要直接操作这个锁但要知道它的存在因为它决定了回调函数的执行上下文。2.2 struct dev_pm_info 里到底装了什么struct dev_pm_info是 runtime pm 的核心我把它里面和 runtime 相关的字段挑出来说字段作用备注usage_count引用计数原子变量get/put 操作的对象child_count活跃子设备计数父设备挂起前要确保子设备都挂起disable_depth禁用深度大于 0 表示 runtime pm 被禁用runtime_error错误标志回调返回错误时置位runtime_status当前状态RPM_ACTIVE / RPM_SUSPENDED 等runtime_auto自动挂起开关用户空间可通过 sysfs 控制request_pending待处理请求有异步请求排队时置位irq_safe中断上下文安全标记回调能否在原子上下文调用runtime_status是个枚举取值包括RPM_INVALID、RPM_ACTIVE、RPM_RESUMING、RPM_SUSPENDED、RPM_SUSPENDING。状态机就是围绕这几个值转的。disable_depth这个字段值得单独说。它是个计数器不是布尔值。为什么因为可能有多个地方同时想禁用 runtime pm比如系统正在做 suspend 流程同时某个驱动也想临时禁用。用计数就能保证所有禁用方都释放后才真正启用。2.3 回调函数的职责划分runtime pm 给驱动提供了三个核心回调定义在struct dev_pm_ops里runtime_suspend设备空闲时调用负责把设备挂起runtime_resume设备被唤醒时调用负责恢复设备runtime_idle设备空闲但还没挂起时调用可以做延迟挂起这三个回调不是必须全部实现。如果你只实现 suspend 和 resumeidle 走默认逻辑也行。但我要提醒一句runtime_idle的默认行为是直接调用runtime_suspend如果你的设备需要延迟一段时间再挂起比如等某个操作彻底完成就必须自己实现 idle 回调。回调的执行上下文也有讲究。默认情况下这些回调在进程上下文执行可以睡眠。但如果你设置了pm_runtime_irq_safe回调就可能在中断上下文执行这时候就不能睡眠了。这个标志要慎用用错了会导致难以排查的死锁。3. 状态机与引用计数的实操细节3.1 状态流转的完整路径runtime pm 的状态机看起来复杂但核心路径就两条挂起和唤醒。挂起路径是这样的设备引用计数归零runtime_idle被调用如果有然后进入runtime_suspend。如果 suspend 成功状态变成RPM_SUSPENDED如果失败状态回到RPM_ACTIVE并且runtime_error置位。唤醒路径反过来有人调用pm_runtime_get如果设备处于挂起状态触发runtime_resume成功后状态变成RPM_ACTIVE。这里有个细节很多人忽略pm_runtime_get是异步的。它只是增加引用计数并标记需要唤醒实际的 resume 操作可能稍后才执行。如果你需要确保设备已经唤醒得用pm_runtime_get_sync它会等待 resume 完成。我见过不少驱动在中断处理里调用pm_runtime_get_sync然后抱怨系统卡死。原因就是中断上下文不能睡眠而get_sync会等待等待过程可能睡眠。这种场景要么用pm_runtime_get不等待要么把操作挪到工作队列里。3.2 引用计数的配对原则引用计数最怕的就是不配对。get 了不 put设备永远不挂起put 多了计数变成负数内核会报 warning。我总结了几条实操原则第一谁 get 谁 put。在函数入口 get在函数所有返回路径上都要 put。C 语言里多个 return 很容易漏建议用goto统一清理。第二跨函数传递要小心。如果 A 函数 get 了设备然后调用 B 函数B 函数里又 get 一次那 B 返回后要 putA 返回后也要 put。这种嵌套引用是合法的但很容易搞混。第三错误路径也要 put。很多人只在成功路径 put忘了错误分支。结果就是设备在出错后永远不挂起功耗下不去。内核提供了pm_runtime_get_sync和pm_runtime_put_sync的配对也有pm_runtime_get和pm_runtime_put的配对。sync 版本会等待操作完成非 sync 版本只是标记。选择哪个取决于你是否需要立即生效。3.3 自动挂起与手动控制的切换runtime_auto这个标志控制设备是否允许自动挂起。默认值是 true意思是引用计数归零后自动挂起。用户空间可以通过 sysfs 的control文件修改这个值。# 查看当前控制模式 cat /sys/devices/.../power/control # 设置为 on 表示禁止自动挂起 echo on /sys/devices/.../power/control # 设置为 auto 表示允许自动挂起 echo auto /sys/devices/.../power/control调试的时候这个接口很有用。比如你怀疑某个设备挂起导致问题可以临时设成 on看问题是否消失。但要注意这只是调试手段产品里不能依赖用户空间去控制。还有一个pm_runtime_forbid和pm_runtime_allow的内核接口效果类似但供驱动内部使用。forbid会增加disable_depthallow会减少。这两个接口在驱动初始化阶段常用比如设备还没准备好接受挂起时先 forbid准备好后再 allow。4. 驱动中集成 runtime pm 的完整流程4.1 初始化阶段的准备工作在驱动 probe 函数里集成 runtime pm第一步是初始化。内核提供了pm_runtime_enable来启用但在调用它之前通常要先设置一些标志。典型的初始化顺序是这样的static int my_driver_probe(struct platform_device *pdev) { struct device *dev pdev-dev; int ret; /* 1. 先禁止自动挂起等设备完全初始化后再允许 */ pm_runtime_forbid(dev); /* 2. 初始化硬件 */ ret my_hw_init(dev); if (ret) return ret; /* 3. 启用 runtime pm */ pm_runtime_enable(dev); /* 4. 设置设备为活跃状态因为刚初始化完设备是开着的 */ pm_runtime_set_active(dev); /* 5. 允许自动挂起 */ pm_runtime_allow(dev); /* 6. 标记最后一次使用触发空闲检查 */ pm_runtime_mark_last_busy(dev); pm_runtime_put_autosuspend(dev); return 0; }这个顺序不是随便定的。先 forbid 是为了防止在硬件还没初始化完的时候就被挂起。pm_runtime_set_active很重要它告诉框架设备现在是开着的否则框架可能以为设备是挂起状态后续的 resume 逻辑就乱了。pm_runtime_mark_last_busy配合 autosuspend 使用它记录最后一次使用的时间戳。pm_runtime_put_autosuspend会减少引用计数并且如果计数归零会延迟一段时间再挂起而不是立即挂起。4.2 autosuspend 延迟机制的参数选择autosuspend 是 runtime pm 里非常实用的一个特性。它解决的是设备频繁短时间使用的场景。比如一个传感器每 100ms 采样一次如果每次采样完立即挂起下次采样又立即唤醒挂起唤醒的开销可能比省下的电还多。延迟时间通过pm_runtime_set_autosuspend_delay设置单位是毫秒pm_runtime_set_autosuspend_delay(dev, 200); pm_runtime_use_autosuspend(dev);200ms 意味着设备空闲 200ms 后才挂起。这个值怎么选我的经验是看设备的挂起恢复开销和使用频率。如果挂起恢复很快比如几微秒延迟可以设小一点50ms 到 100ms。如果挂起恢复很慢比如需要重新配置寄存器、重新锁相环延迟就要设大几百毫秒甚至一秒。还有一个pm_runtime_autosuspend_expiration可以查询延迟是否到期。调试的时候可以用它来确认 autosuspend 是否按预期工作。4.3 回调函数的实现要点runtime_suspend和runtime_resume的实现有几个要点。第一保存和恢复上下文。挂起前要保存设备寄存器状态恢复时要写回去。但不是所有寄存器都需要保存只保存那些会丢失的。具体哪些会丢失看硬件手册。第二处理时钟和电源域。挂起时通常要关时钟、关电源域。恢复时反过来。这里要注意顺序关的时候先关时钟再关电源开的时候先开电源再开时钟。顺序错了硬件可能不工作。第三返回值处理。回调返回 0 表示成功负数表示失败。失败时框架会把状态设回 active并置位runtime_error。如果你返回错误要确保设备处于可用状态否则后续操作会出问题。一个典型的 suspend 回调长这样static int my_runtime_suspend(struct device *dev) { struct my_device *mydev dev_get_drvdata(dev); /* 保存寄存器 */ my_save_registers(mydev); /* 关时钟 */ clk_disable_unprepare(mydev-clk); /* 关电源域 */ regulator_disable(mydev-power); return 0; }resume 回调就是反过来的操作。注意 resume 里如果任何一步失败要回滚已经做的操作否则设备状态不一致。5. 常见问题排查与避坑经验5.1 设备不挂起的排查思路设备不挂起是最常见的问题。排查步骤我一般这么走先看usage_count是不是零。通过 sysfs 的runtime_usage文件可以看cat /sys/devices/.../power/runtime_usage如果不是零说明有地方 get 了没 put。这时候可以用pm_runtime_get的调用栈来定位或者用 ftrace 跟踪pm_runtime_get和pm_runtime_put的调用。如果usage_count是零但还不挂起看disable_depthcat /sys/devices/.../power/disable_depth大于零说明被禁用了。检查是不是有地方调用了pm_runtime_forbid没allow或者系统 suspend 流程还没结束。再看runtime_statuscat /sys/devices/.../power/runtime_status如果是suspended那其实已经挂起了你可能看错了设备。如果是active结合前面的计数和禁用深度继续查。还有一个容易忽略的点父设备的child_count。如果子设备还活跃父设备不会挂起。检查设备树里的父子关系确认子设备是否都挂起了。5.2 挂起后无法唤醒的定位比不挂起更严重的是挂起后醒不过来。这种问题通常出在 resume 回调或者唤醒源配置上。先确认唤醒源是否使能。很多设备挂起后需要配置一个唤醒中断否则外部事件来了也唤不醒。这个配置通常在 suspend 回调里做检查有没有漏。再看 resume 回调的返回值。如果 resume 失败设备状态会停在RPM_RESUMING或者回到RPM_SUSPENDED后续访问就会失败。用 dmesg 看有没有相关错误日志。还有一种情况是 resume 回调里访问了还没准备好的资源。比如时钟还没开就去读寄存器结果总线挂死。这种要看 resume 里的操作顺序确保依赖的资源先恢复。我踩过的一个坑是在 suspend 里关了电源域但 resume 里忘了重新使能结果寄存器读写全部失败。后来养成了习惯suspend 和 resume 成对写写完对照检查一遍。5.3 中断上下文使用的限制前面提过pm_runtime_irq_safe这里展开说。设置这个标志后runtime pm 的回调可以在中断上下文调用但代价是回调里不能睡眠。什么情况需要这个标志典型的是设备的中断处理里需要访问设备寄存器而设备可能处于挂起状态。如果不设置 irq_safe中断里调用pm_runtime_get会触发 resumeresume 可能睡眠中断上下文睡眠就是灾难。但设置 irq_safe 后你的 suspend/resume 回调就不能用可能睡眠的函数了。clk_prepare、regulator_enable、mutex_lock这些都不能用。只能用clk_enable非 prepare 版本、spin_lock这些。我的建议是能不用 irq_safe 就不用。如果中断里确实需要访问设备考虑把操作挪到工作队列或者 threaded irq 里。这样回调可以在进程上下文执行限制少很多。5.4 常见问题速查表现象可能原因排查方法设备不挂起引用计数不为零查 runtime_usage跟踪 get/put设备不挂起被禁用查 disable_depth设备不挂起子设备活跃查 child_count 和子设备状态挂起后无法唤醒唤醒源未配置检查 suspend 里的唤醒配置挂起后无法唤醒resume 失败查 dmesg 和 runtime_status中断里调用卡死回调睡眠检查是否设置 irq_safe计数变负数put 多于 get跟踪调用栈检查错误路径autosuspend 不生效延迟未设置查 autosuspend_delay 和 use_autosuspend6. 调试工具与实战技巧6.1 用 ftrace 跟踪 runtime pm 调用ftrace 是排查 runtime pm 问题的利器。内核里有个pm_runtime的 tracepoint可以跟踪所有 get/put/suspend/resume 事件。启用方法# 挂载 debugfs mount -t debugfs none /sys/kernel/debug # 启用 runtime pm 事件 echo 1 /sys/kernel/debug/tracing/events/power/enable # 开始跟踪 echo 1 /sys/kernel/debug/tracing/tracing_on # 查看结果 cat /sys/kernel/debug/tracing/trace输出里能看到每个设备的操作序列包括调用者。如果发现某个设备频繁 suspend/resume说明 autosuspend 延迟设小了或者有地方在频繁 get/put。我一般会配合trace-cmd工具用录制一段时间后分析。特别是排查设备不挂起的时候看 trace 里最后一次 put 之后有没有 suspend 事件一目了然。6.2 sysfs 接口的实用技巧每个设备的 runtime pm 状态都在/sys/devices/.../power/下面。除了前面提到的control、runtime_status、runtime_usage、disable_depth还有几个有用的runtime_active_time设备处于活跃状态的总时间毫秒runtime_suspended_time设备处于挂起状态的总时间runtime_active_kids活跃子设备数量async异步 suspend/resume 开关runtime_active_time和runtime_suspended_time可以用来评估电源管理效果。如果活跃时间远大于挂起时间说明设备大部分时间都在工作要么是使用频率高要么是挂起有问题。写脚本批量检查所有设备的状态也很实用for dev in /sys/devices/*/power/runtime_status; do status$(cat $dev) if [ $status active ]; then echo $dev: $status fi done这个脚本能列出所有活跃设备快速定位哪些设备没挂起。6.3 系统 suspend 与 runtime pm 的交互系统 suspend 的时候runtime pm 会被临时禁用。具体流程是系统 suspend 开始时内核遍历所有设备对活跃设备调用pm_runtime_resume确保它们处于活跃状态然后调用系统级的 suspend 回调。系统 resume 后再恢复 runtime pm。这个交互有个坑如果你的驱动在系统 suspend 回调里依赖 runtime pm 的状态可能会出错。因为系统 suspend 期间 runtime pm 是禁用的pm_runtime_get不会触发 resume。正确的做法是系统 suspend/resume 回调里直接操作硬件不要依赖 runtime pm。runtime pm 的回调只在设备级动态管理时用。还有一点系统 suspend 前会调用pm_runtime_disable这会增加disable_depth。如果你的驱动在 suspend 回调里检查disable_depth会看到非零值这是正常的。7. 性能与功耗的权衡实践7.1 挂起恢复开销的测量要优化功耗先得知道挂起恢复的开销。测量方法是在 suspend 和 resume 回调里打时间戳然后算差值。static ktime_t suspend_time; static int my_runtime_suspend(struct device *dev) { suspend_time ktime_get(); /* ... */ } static int my_runtime_resume(struct device *dev) { ktime_t now ktime_get(); s64 delta ktime_to_us(ktime_sub(now, suspend_time)); dev_info(dev, resume took %lld us\n, delta); /* ... */ }实测下来简单的设备挂起恢复可能只要几十微秒复杂的设备比如需要重新加载固件的可能要几毫秒甚至几十毫秒。知道开销后autosuspend 延迟就可以合理设置。经验公式是延迟时间 挂起恢复开销 × 2。比如开销 1ms延迟至少设 2ms。但实际还要考虑使用频率如果设备每秒用一次延迟设 2ms 意味着大部分时间都在挂起省电效果明显。7.2 不同场景下的策略选择不是所有设备都适合激进的电源管理。我一般分三类第一类使用频繁且挂起开销小的设备比如 GPIO 控制器、简单的 I2C 设备。这类可以设较小的 autosuspend 延迟甚至不用 autosuspend直接引用计数归零就挂起。第二类使用不频繁但挂起开销大的设备比如摄像头、显示屏。这类要设较大的延迟避免频繁挂起恢复。有些场景下甚至考虑不用 runtime pm改用系统级 suspend。第三类随时可能被唤醒的设备比如触摸屏、传感器。这类要配置好唤醒源挂起后能快速响应。autosuspend 延迟要小保证响应速度。7.3 功耗数据的采集与分析评估 runtime pm 效果最终要看功耗数据。采集方法有几种硬件层面用功耗分析仪测整机电流。这是最准的但需要设备支持。软件层面用runtime_active_time和runtime_suspended_time估算。如果挂起时间占比高功耗通常就低。还有内核的powertop工具可以分析哪些设备在阻止系统进入低功耗状态。虽然它主要针对系统级 suspend但对 runtime pm 也有参考价值。我一般会做对比测试关闭 runtime pm 跑一段时间记录功耗开启 runtime pm 再跑对比差异。差异明显说明配置有效差异不明显就要查是不是有设备没挂起。8. 从框架到实战的几点体会runtime pm 这套机制刚看代码的时候觉得绕用熟了会发现设计得很精巧。引用计数解决多方共享状态机保证转换有序autosuspend 平衡功耗和性能。每个设计都有它的道理。我在实际项目里踩过的坑大部分不是框架本身的问题而是使用方式不对。引用计数不配对、回调里睡眠、唤醒源没配、autosuspend 延迟拍脑袋定这些都是人为因素。框架给了工具怎么用还是看人。有个经验分享给刚接触的人先在简单的设备上练手比如一个 GPIO 或者 I2C 设备把 get/put、suspend/resume 的流程跑通用 ftrace 看状态变化。等熟悉了再上复杂的设备。上来就搞摄像头、显示屏这种很容易被各种细节淹没。还有一点runtime pm 的调试信息一定要打开。内核配置里CONFIG_PM_DEBUG和CONFIG_PM_ADVANCED_DEBUG打开后sysfs 接口更全日志更详细。生产版本可以关掉省空间但开发阶段一定要开。最后说个实际场景我之前做一个电池供电的设备待机功耗一直下不去。用 ftrace 查了半天发现是一个传感器驱动在 probe 里 get 了设备但没 put。改完之后待机功耗直接降了一半。这种问题不看 trace 很难发现因为代码逻辑看起来没问题就是漏了一个 put。runtime pm 不是银弹它解决的是设备级动态电源管理。系统级功耗优化还要配合 CPU idle、DVFS、系统 suspend 这些机制。但把 runtime pm 用好了是功耗优化的基础。
返回列表