ARTICLE DETAIL

资讯详情

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

Linux PM QoS 机制详解:约束聚合、通知链与功耗调优实践

Linux PM QoS 机制详解:约束聚合、通知链与功耗调优实践 1. 从一次待机功耗异常说起PM QoS 到底在管什么几年前我在调试一块嵌入式板子的待机功耗时遇到过一个很典型的现象系统明明已经进入 suspend 流程测量到的静态电流却比预期高了将近 20mA。查了一圈驱动最后发现问题出在一个音频 codec 的驱动上——它在 probe 阶段通过pm_qos_add_request注册了一个对 CPU DMA 延迟的约束但设备卸载时忘了 remove导致这个约束一直挂在全局链表上CPU 空闲管理模块始终不敢进入最深的那档 idle 状态。这件事让我意识到PM QoSPower Management Quality of Service这个框架虽然代码量不大但它是 Linux 功耗子系统里最容易被忽视、又最容易出问题的一环。它本质上是一套约束协商机制各个驱动、子系统把自己对性能的最低要求比如CPU 响应延迟不能超过多少微秒DMA 延迟不能超过多少网络吞吐不能低于多少注册进来功耗管理模块在做决策时读取这些约束在满足所有约束的前提下尽可能省电。这篇梳理面向的是已经对 Linux 电源管理有基本了解、想深入理解 PM QoS 内部机制的读者包括嵌入式驱动开发者、内核功耗调优工程师以及正在啃drivers/base/power/目录源码的同学。我会从框架的设计动机讲起把几个 QoS class 的差异、约束的聚合算法、通知链机制、以及实际使用中的坑都过一遍。文中涉及的代码路径以主流 5.x/6.x 内核为参考不同版本细节可能有出入但核心逻辑是稳定的。需要先明确一点PM QoS 不是一个省电开关它是一套约束表达与聚合的基础设施。真正做省电决策的是 cpuidle governor、cpufreq governor、runtime PM 这些消费者PM QoS 只负责把散落在各处的需求收集起来算出一个当前必须满足的边界值。理解这个定位后面很多设计就顺了。2. 约束的两种表达方式绝对值和期望值PM QoS 最核心的抽象是约束constraint而约束在框架里被分成两大类这个区分是理解整个框架的起点。2.1 绝对值约束不可逾越的硬边界绝对值约束表达的是我必须满足这个条件否则功能会出问题。比如一个需要低延迟响应的串口驱动它可能要求 CPU 从 idle 唤醒的延迟不超过 100 微秒。这种约束的特点是只要有一个请求者提出了这个要求系统就必须满足没有任何商量的余地。在代码层面绝对值约束对应的是PM_QOS_FLAG_NO_POWER_OFF这类标志以及各个 class 里维护的min或max边界。以 CPU DMA 延迟这个 class 为例它维护的是一个最大可接受延迟值所有请求者提交的值里取最小的那个就是当前系统必须满足的上限。这里有个容易混淆的点不同 class 的方向是不一样的。CPU DMA 延迟是越小越严格所以聚合时取最小值而网络吞吐这类 class 是越大越严格聚合时取最大值。框架用pm_qos_flags和具体的聚合函数来区分这两种方向读代码时要特别留意。2.2 期望值约束尽力而为的软目标期望值约束表达的是我希望达到这个水平但达不到也能接受。最典型的是 CPU 频率的期望值——某个驱动可能希望 CPU 跑在 1.2GHz 以上以获得更好的性能但如果系统因为温度或功耗限制只能跑 800MHz它也能工作只是体验差一点。期望值约束的聚合方式和绝对值不同。它不是简单地取极值而是要考虑有多少请求者、各自的权重是多少。在较新的内核里这部分逻辑和pm_qos_update_request的value语义绑定聚合结果会作为 governor 的一个参考输入而不是硬性门槛。提示判断一个约束是绝对值还是期望值最直接的方法是看它在 class 定义里注册的是哪个回调。绝对值通常走constraints相关的处理期望值则更多体现在target值的计算上。2.3 为什么要有这个区分如果所有约束都当成硬边界系统会变得极其保守——任何一个驱动提出一点点性能要求都会把整个系统钉在高功耗状态。反过来如果都当成软目标关键功能又可能因为省电而被牺牲。这个二分法其实是工程上的折中把功能正确性相关的需求和性能体验相关的需求分开管理前者严格保证后者灵活权衡。我在实际调优时经常需要判断某个驱动的约束到底该用哪种——如果一个约束去掉后功能会出错那就是绝对值如果只是变慢那就是期望值。这个判断直接决定了调优的空间有多大。3. 三类 QoS class 的差异与选择逻辑PM QoS 框架里预定义了几个 class每个 class 对应一类资源约束。理解它们的差异才能在实际开发中选对工具。3.1 CPU DMA 延迟最常用的那个PM_QOS_CPU_DMA_LATENCY是使用频率最高的 class。它约束的是 CPU 从 idle 状态唤醒、以及 DMA 操作的延迟上限。单位是微秒值越小要求越严格。这个 class 之所以常用是因为很多外设对延迟敏感音频播放要求缓冲区及时填充否则会爆音高速串口要求及时响应否则会丢数据某些传感器要求周期性采样延迟太大会导致数据失真。它的聚合逻辑是取所有请求中的最小值。也就是说只要有一个请求者要求 50 微秒整个系统的延迟上限就是 50 微秒哪怕其他请求者都只要求 1000 微秒。这个设计是合理的——延迟这种资源短板效应非常明显。3.2 网络吞吐容易被忽略的 classPM_QOS_NETWORK_THROUGHPUT约束的是网络吞吐量的下限。这个 class 在移动设备上比较常见比如某个后台同步任务要求至少 1Mbps 的吞吐否则同步会超时。它的聚合逻辑和延迟相反取所有请求中的最大值。因为吞吐是越大越好的资源最严格的要求就是最高的那个下限。实际使用中这个 class 有个坑它约束的是吞吐能力不是实际吞吐。也就是说即使当前没有网络流量只要有人注册了吞吐约束系统就不能为了省电而把网络相关模块关掉。我在一个项目里见过因为忘记 remove 这个约束导致设备待机时 WiFi 模块一直无法进入低功耗状态。3.3 flags 类约束布尔型的开关除了上面两个数值型 class还有一类是 flags 型约束比如PM_QOS_FLAG_NO_POWER_OFF。它表达的是这个设备不能被断电这种布尔需求。flags 类约束的聚合是逻辑或——只要有一个请求者设置了某个 flag这个 flag 就生效。这类约束通常用在设备热插拔、固件更新等场景需要保证设备在整个操作期间保持供电。3.4 选 class 的实操判断面对一个具体的约束需求怎么选 class我的经验是问三个问题判断维度选 CPU DMA 延迟选网络吞吐选 flags约束的是响应速度是否否约束的是数据速率否是否约束的是供电状态否否是单位是时间是否无单位是速率否是无如果三个都不匹配可能需要考虑是否要新增一个 class。不过新增 class 的成本不低要改框架代码一般项目里很少这么做优先复用现有 class。4. 约束的注册、更新与聚合一次完整的生命周期理解了 class 的分类接下来看一个约束从注册到生效的完整流程。这部分是框架的核心机制也是排查问题时最需要搞清楚的地方。4.1 请求对象的创建与注册使用 PM QoS 的第一步是创建一个请求对象。以 CPU DMA 延迟为例典型代码是这样的struct pm_qos_request my_req; pm_qos_add_request(my_req, PM_QOS_CPU_DMA_LATENCY, 100);pm_qos_add_request做几件事初始化请求对象、把它挂到对应 class 的请求链表上、然后触发一次聚合计算。第三个参数 100 表示这个请求者要求延迟不超过 100 微秒。这里有个细节值得注意请求对象pm_qos_request是调用者自己分配的结构体框架只是把它的指针挂到链表上。这意味着请求对象的生命周期必须由调用者保证——如果对象被释放了但没从链表上摘下来就会变成野指针后续聚合时访问它就会出问题。我前面提到的那个音频 codec 的 bug本质就是这个生命周期管理没做好。4.2 聚合计算的触发时机约束的聚合不是每次读取时现算的而是在约束发生变化时预先算好缓存起来。触发聚合的时机有三个新增一个请求时更新一个已有请求的值时移除一个请求时聚合结果会更新到 class 的target_value或类似的字段上消费者比如 cpuidle governor直接读这个缓存值避免每次都遍历链表。这个设计的好处是读取路径非常轻量适合在 idle 决策这种高频路径上调用。代价是每次约束变化都要做一次全链表遍历不过约束变化的频率通常很低这个代价可以接受。4.3 通知链约束变化如何传导到消费者聚合结果更新后怎么通知消费者答案是通知链notifier chain。每个 class 都维护一个 notifier 链表消费者可以注册回调在约束变化时收到通知。static int my_notifier_call(struct notifier_block *nb, unsigned long val, void *ptr) { /* val 是新的聚合值在这里调整自己的行为 */ return NOTIFY_OK; } static struct notifier_block my_nb { .notifier_call my_notifier_call, }; pm_qos_add_notifier(PM_QOS_CPU_DMA_LATENCY, my_nb);cpuidle 子系统就是通过这个机制感知延迟约束变化的当约束变严格时它会重新评估各个 idle 状态是否可用把不满足延迟要求的状态排除掉。注意notifier 回调是在约束变化的上下文里同步调用的所以回调里不能做可能睡眠的操作否则可能死锁。这个限制在调试时经常被忽略导致一些偶发的挂起问题。4.4 移除请求最容易被遗忘的一步pm_qos_remove_request是使用 PM QoS 时最容易被遗忘的一步。很多驱动在 probe 里加了请求但在 remove 或者错误处理路径里忘了移除。这个问题的隐蔽性在于忘记移除不会立刻报错约束会一直生效表现为设备明明已经不用了系统功耗却降不下来。而且如果请求对象是动态分配的对象释放后约束还在链表上就变成了 use-after-free可能引发更难排查的崩溃。我的建议是把 add 和 remove 写在同一个函数的两端或者用devm_系列的托管接口如果对应 class 支持的话让框架自动处理生命周期。如果必须手动管理在代码 review 时把这一对操作作为检查重点。5. 和 cpuidle、cpufreq 的联动约束如何真正影响功耗PM QoS 本身不做功耗决策它的价值体现在和功耗管理子系统的联动上。这部分讲清楚约束是怎么被消费的。5.1 cpuidle 如何用延迟约束筛选 idle 状态cpuidle 的核心任务是在 CPU 空闲时选择一个合适的 idle 状态。每个 idle 状态有两个关键参数进入/退出的延迟exit_latency和功耗节省量。状态越深省电越多但退出延迟越大。PM QoS 的 CPU DMA 延迟约束在这里起作用cpuidle governor 在选状态时会把 exit_latency 大于当前约束值的状态全部排除掉。比如约束是 100 微秒那么所有退出延迟超过 100 微秒的深睡状态都不能用。这个筛选逻辑保证了有低延迟需求的设备能及时得到响应代价是省电效果打折。调优时经常要在这两者之间找平衡约束放得太松延迟敏感设备出问题约束太紧省电效果上不去。5.2 cpufreq 如何参考期望值约束cpufreq 的情况稍微复杂一些。CPU DMA 延迟约束会间接影响 cpufreq因为某些频率调节策略和 idle 状态是耦合的。但更直接的是期望值约束——某些驱动通过 PM QoS 表达对 CPU 性能的期望cpufreq governor 在计算目标频率时会参考这个期望。不过要注意期望值约束对 cpufreq 的影响是建议性的不是强制的。governor 还会综合考虑负载、温度、功耗预算等因素。所以不能指望注册一个期望值约束就一定能拿到想要的频率。5.3 一个实际的联动案例假设一个场景设备正在播放音频音频驱动注册了 50 微秒的 CPU DMA 延迟约束。cpuidle 收到约束变化通知重新评估 idle 状态把退出延迟超过 50 微秒的状态标记为不可用CPU 空闲时只能进入较浅的 idle 状态唤醒延迟得到保证音频缓冲区及时填充播放不卡顿代价是待机功耗比没有约束时高一些音频播放结束后驱动移除约束cpuidle 重新评估深睡状态恢复可用功耗降下来。这就是 PM QoS 的正常工作循环。如果驱动忘了移除约束这个循环就断了——音频早就播完了CPU 却一直进不了深睡功耗白白浪费。这也是为什么我在第 1 节强调生命周期管理。6. 调试 PM QoS 问题的完整排查链路前面讲了原理这一节讲实战。PM QoS 相关的问题往往表现为功耗降不下来或者延迟敏感功能偶发异常排查起来有一定套路。6.1 第一步确认约束是否存在排查的第一步是看当前系统上有哪些活跃的约束。内核提供了 debugfs 接口# 查看 CPU DMA 延迟约束 cat /sys/kernel/debug/pm_qos/pm_qos_cpu_dma_latency/constraints # 查看网络吞吐约束 cat /sys/kernel/debug/pm_qos/pm_qos_network_throughput/constraints输出会列出所有活跃的请求及其值。如果发现某个约束的值很严格但对应的设备早就没在用了那基本可以确定是忘记 remove 的问题。需要确认内核编译时开启了CONFIG_PM_QOS_DEBUG否则这些 debugfs 节点不存在。这个配置在生产内核里经常是关的调试时要注意。6.2 第二步定位约束的来源找到可疑约束后下一步是定位它是谁注册的。debugfs 输出里通常只有请求对象的值没有调用者信息。这时候有几个办法用crash或者drgn这类工具根据请求对象的地址反查它在哪个模块的数据段里在pm_qos_add_request里临时加打印把调用栈打出来如果是可复现的问题用 ftrace 跟踪pm_qos_add_request和pm_qos_remove_request的调用我一般优先用 ftrace因为它对系统影响小而且能看到完整的调用时序。配置好 function_graph tracer过滤这两个函数跑一遍复现流程谁加了没删一目了然。6.3 第三步验证约束对 idle 的实际影响确认约束来源后还要验证它确实影响了 idle 决策。可以看 cpuidle 的统计cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage对比约束存在和不存在时各个 idle 状态的使用次数。如果约束存在时深睡状态的使用次数明显下降说明约束确实在起作用。这一步的价值在于区分约束存在但没影响和约束存在且影响了两种情况。有些约束虽然挂着但因为值比较宽松实际并不影响 idle 选择这种就不需要处理。6.4 第四步修复与回归验证定位到问题后修复通常就是补上pm_qos_remove_request。但修复后一定要做回归验证确认约束在设备卸载后确实从 debugfs 里消失了确认 idle 统计恢复到正常水平确认功耗测量值回到预期范围确认延迟敏感功能仍然正常我踩过的一个坑是补了 remove但补在了错误的分支上正常卸载路径走了错误处理路径还是漏。所以修复时要检查所有可能提前返回的路径。6.5 常见问题速查表现象可能原因排查方向待机功耗偏高约束未移除查 debugfs 约束列表延迟敏感功能偶发异常约束值太宽松查约束值是否满足需求系统崩溃在 pm_qos 相关路径请求对象生命周期问题查 add/remove 配对约束变化后行为没变notifier 未注册或注册失败查 notifier 注册返回值深睡状态完全不可用约束值过严查约束值是否合理7. 几个容易踩的坑和我的处理经验最后分享几个我在实际项目中踩过的坑都是文档里不太会写、但实际开发中很容易遇到的。7.1 在原子上下文里调用 add/removepm_qos_add_request和pm_qos_remove_request内部可能会睡眠因为要拿 mutex 保护链表。如果在原子上下文比如中断处理程序、持有 spinlock 的代码里调用会触发 scheduling while atomic 的警告甚至崩溃。我遇到过一次是在一个中断处理程序里直接调用了 add_request当时没注意跑压力测试时才暴露出来。解决办法是把约束操作挪到工作队列或者线程上下文里执行。如果确实需要在中断里表达约束可以考虑用不带睡眠的变体如果有的话或者重新设计调用时机。7.2 约束值的单位搞错CPU DMA 延迟的单位是微秒不是毫秒也不是纳秒。我见过有驱动把毫秒值直接填进去结果约束严了 1000 倍系统几乎进不了任何 idle 状态功耗飙升。这个错误的隐蔽性在于它不会报错只是行为异常。建议在代码里对约束值加个范围检查或者至少加个注释说明单位。review 时也要特别留意这个数字。7.3 多个请求对象共用一个结构体pm_qos_request对象不能被多个逻辑请求共用。有些驱动为了省事用一个全局的 request 对象在不同地方反复 add/update结果约束之间互相干扰。正确的做法是每个独立的约束需求用一个独立的 request 对象。虽然多占一点内存但逻辑清晰不会互相影响。内存这点开销在现在的系统上完全可以忽略。7.4 忽略 notifier 回调的返回值pm_qos_add_notifier会返回一个状态码表示注册是否成功。有些代码不检查返回值注册失败了也不知道结果约束变化时消费者收不到通知行为不符合预期。虽然注册失败的情况比较少见通常是内存不足但检查一下返回值是好习惯成本几乎为零。7.5 在 suspend/resume 路径里的特殊处理系统 suspend 时PM QoS 的约束处理有一些特殊逻辑。比如某些约束在 suspend 期间会被临时忽略resume 后恢复。如果驱动在 suspend 回调里操作约束要特别注意时序避免和框架的处理冲突。我的经验是除非有明确需求否则不要在 suspend/resume 回调里动 PM QoS 约束。约束的注册和移除应该和设备的正常生命周期绑定而不是和电源状态绑定。7.6 关于新增 class 的取舍有时候现有 class 都不太匹配需求会考虑新增一个。我的建议是尽量别加原因有三一是要改框架代码维护成本高二是新 class 的聚合逻辑、通知机制都要自己实现容易出 bug三是上游不一定接受后续合并内核版本会有冲突。如果确实需要先看看能不能用现有的 class 加上一些约定来表达。实在不行再考虑在驱动内部自己维护一套约束聚合逻辑不走 PM QoS 框架。这样虽然重复造轮子但至少不影响框架的稳定性。8. 写在最后的一点个人体会PM QoS 这个框架代码量不大但设计得相当精巧。它把约束表达和功耗决策解耦让各个驱动只需要关心自己的需求不用管系统怎么权衡。这个抽象层次把握得很好。我在实际使用中最大的体会是约束的生命周期管理比约束本身更重要。框架提供了机制但用不用得好取决于驱动开发者有没有把 add/remove 当成一对必须配对的操作来对待。我见过的大部分 PM QoS 问题根源都在这里。另外一个体会是调试这类问题要有耐心。功耗问题往往不是单一原因造成的PM QoS 约束可能只是其中一个因素。排查时要把约束、idle 统计、功耗测量结合起来看才能定位到真正的原因。单看任何一个维度都可能得出错误结论。如果你正在做功耗调优建议把 debugfs 里的 PM QoS 约束列表作为常规检查项在每次功耗测试前都看一眼。这个习惯帮我提前发现过好几次约束泄漏的问题省了不少排查时间。
返回列表