
1. 从一次待机功耗异常说起PM QoS 到底在管什么几年前我在调试一块嵌入式板子的待机功耗时遇到过一个很典型的现象系统明明已经进入 suspend 流程电流却始终降不到预期值比参考设计高了将近 30mA。排查了一圈时钟、电源域、外设驱动最后发现问题出在一个音频驱动上——它在某个路径里对 CPU 延迟提了一个很硬的约束导致 cpuidle 只能停在浅层状态深睡根本进不去。这个约束就是通过PM QoS framework注册进去的。这件事让我意识到功耗子系统里最容易被忽视、但排查问题时又绕不开的就是 PM QoS 这一层。它不像 cpufreq、cpuidle 那样直接干活而是站在它们背后决定谁有权限制系统能睡多深、能跑多快。你可以把它理解成功耗管理里的需求登记处各个驱动、子系统把自己的性能或延迟诉求登记进来框架负责汇总最终告诉 cpuidle、cpufreq、甚至整个系统 suspend 流程——现在到底允不允许你进入某个低功耗状态。这篇梳理面向的是已经对 Linux 功耗子系统有基本了解、正在啃内核源码或者需要定位实际功耗问题的同学。我会把 PM QoS 的框架结构、几类约束的差异、注册与更新的完整链路、以及它在 suspend/cpuidle/cpufreq 里怎么被消费一层层拆开讲。中间会穿插我自己踩过的坑比如约束值算错、notifier 没注销导致的内存泄漏、以及 resume 之后约束没恢复这类问题。看完你应该能独立判断一个功耗降不下去的场景是不是被某条 QoS 约束卡住了以及怎么顺着框架找到那个罪魁祸首。2. PM QoS 的整体框架三类约束两套机制PM QoS 不是一个单一模块它是一组接口的集合核心目标是把性能需求和功耗状态解耦。驱动不需要知道 cpuidle 有几个状态、cpufreq 有几个频点它只需要表达我需要多低的延迟或者我需要多高的吞吐剩下的交给框架去聚合。2.1 三类约束的定位差异从内核代码的组织来看PM QoS 主要提供三类约束它们服务的对象完全不同约束类型头文件主要消费者典型场景CPU 延迟约束include/linux/pm_qos.hcpuidle、suspend音频、网络中断处理要求低延迟CPU 频率约束同上cpufreq governor性能敏感任务要求最低频率设备延迟约束同上设备 runtime PM设备要求唤醒延迟上限这三类里CPU 延迟约束是最常用、也是实际排查中最常遇到的一类。它的语义是从提出请求到 CPU 响应最多能容忍多少微秒。数值越小约束越强系统就越不敢进入深睡状态。频率约束则是反过来表达我需要至少多少 kHz 的频率防止 governor 为了省电把频率压得太低。设备延迟约束相对小众它挂在设备自己的dev_pm_qos上主要影响设备的 runtime suspend 决策。很多驱动作者会忽略它但在一些对唤醒时间敏感的外设比如某些传感器、通信模组上它是必须配置的。2.2 全局约束与 per-device 约束框架在实现上分成两条线全局约束和per-device 约束。全局约束针对的是整个 CPU 子系统注册进去的值会被聚合最终形成一个系统级的有效约束。它的接口是pm_qos_add_request、pm_qos_update_request、pm_qos_remove_request这一套。注意这套接口在新版本内核里已经逐步被cpu_latency_qos_*系列替代老的PM_QOS_CPU_DMA_LATENCY这类 class 定义在慢慢退场但很多老驱动还在用读代码时两套都要认识。per-device 约束则是挂在具体设备上的接口是dev_pm_qos_add_request等。它只影响这个设备自己的电源管理行为不会污染全局。这一点很关键如果你只是想让某个设备别睡太死千万别用全局接口否则会误伤整个系统。提示判断一个驱动用的是哪套接口看它调用的是pm_qos_*还是dev_pm_qos_*。前者是全局后者是 per-device混用会导致约束范围失控。2.3 聚合逻辑为什么最严的那个说了算PM QoS 的聚合规则很简单也很符合直觉对于延迟约束取所有请求里最小值对于频率约束取最大值。因为延迟越小约束越强频率越高约束越强所以最终生效的永远是最挑剔的那个请求。这个规则带来一个很实际的后果只要有一个驱动提了很强的约束整个系统的功耗状态就会被它拖住。排查功耗问题时如果发现系统死活进不了深睡第一反应就应该是去查当前生效的 QoS 约束值是多少然后反查是谁提的。内核提供了/sys/kernel/debug/pm_qos/下的调试节点需要开CONFIG_PM_QOS_DEBUG可以直接看到每个 class 的当前值和请求列表这是排查时最省事的入口。3. 约束的注册与更新从 add_request 到 notifier 回调理解了框架定位接下来看一条约束从注册到生效的完整链路。这部分是读代码和写驱动的核心也是坑最多的地方。3.1 请求对象的生命周期一个 QoS 请求在内核里对应一个struct pm_qos_request全局或struct dev_pm_qos_requestper-device。它的生命周期必须严格遵循先 add、再 update、最后 remove的顺序。struct pm_qos_request my_req; /* 注册初始值给一个宽松的值 */ pm_qos_add_request(my_req, PM_QOS_CPU_DMA_LATENCY, PM_QOS_DEFAULT_VALUE); /* 需要约束时更新 */ pm_qos_update_request(my_req, 50); /* 要求延迟不超过 50us */ /* 不再需要时移除 */ pm_qos_remove_request(my_req);这里第一个坑就来了pm_qos_add_request的初始值不能随便给。如果你给了一个很小的值强约束那么从注册那一刻起系统就被限制了哪怕你后面才 update。正确做法是初始给PM_QOS_DEFAULT_VALUE等真正需要时再 update 成目标值。我见过有驱动直接 add 一个 0结果系统从开机起就进不了任何深睡状态查了半天才发现是初始化值写错了。3.2 update 的代价与频率控制pm_qos_update_request不是免费的。每次 update 都会触发聚合重算如果值发生变化还会通过 notifier 链通知所有订阅者比如 cpuidle 的 governor。在中断上下文或者高频路径里频繁 update会带来可观的 CPU 开销。实际经验是能批量就批量能缓存就缓存。比如一个网络驱动在收包路径上想临时提约束不要每个包都 update 一次而是判断当前值是否已经满足需求满足就跳过。很多成熟驱动会维护一个当前已生效值的本地变量只有目标值和它不同才真正调用 update。if (my_req_value ! target) { pm_qos_update_request(my_req, target); my_req_value target; }这个模式看起来简单但在高频路径上能省下大量 notifier 遍历的开销。3.3 notifier 链谁在监听约束变化约束变化后框架通过 blocking notifier 通知订阅者。cpuidle 的menugovernor、cpufreq 的某些 governor、以及 suspend 流程都会注册 notifier。当约束值变化时它们会重新评估当前允许的最深状态或最低频率。这里有个容易忽略的点notifier 回调是在持有锁的上下文里被调用的所以回调函数里不能做可能睡眠的操作也不能反过来再去调用 QoS 接口否则可能死锁。我在一个项目里见过有人在 notifier 回调里调用pm_qos_update_request直接触发自锁系统卡死。正确做法是把需要做的处理丢到 workqueue 里异步执行。3.4 remove 的必要性内存泄漏与悬挂约束pm_qos_remove_request必须在模块卸载或请求不再需要时调用。如果忘了 remove会有两个后果一是请求对象占用的内存泄漏如果是动态分配的二是这条约束会一直挂在聚合列表里永久影响系统功耗。更隐蔽的是悬挂约束驱动模块已经卸载了但请求对象还在聚合时读到的是一块已经释放或者语义上无效的内存。开了CONFIG_DEBUG_KERNEL和相关的内存调试选项后这类问题会以 use-after-free 的形式暴露出来。所以我的习惯是只要 add 了就一定在对应的 cleanup 路径里 remove并且用devm_系列接口如果适用来自动管理生命周期。4. 约束在 cpuidle、cpufreq、suspend 中的消费路径注册进去的约束最终要被消费才有意义。这一节看框架怎么把聚合后的值传递给真正的功耗决策点。4.1 cpuidle延迟约束决定能睡多深cpuidle 的每个状态都有一个exit_latency退出延迟和target_residency目标驻留时间。governor 在选择状态时会拿当前生效的 CPU 延迟约束和每个状态的exit_latency比较只有 exit_latency 小于等于约束值这个状态才允许进入。举个例子假设当前约束是 100us而某个深睡状态的 exit_latency 是 200us那这个状态就被排除了系统最多只能进到 exit_latency 小于 100us 的浅层状态。这就是为什么一个音频驱动提了 50us 的约束后系统功耗会明显上升——它把大部分深睡状态都挡在门外了。排查时可以用cpuidle的 sysfs 节点看每个状态的latency和residency再对比当前 QoS 值就能算出实际允许进入的最深状态是哪个。4.2 cpufreq频率约束如何影响 governor频率约束的消费方是 cpufreq governor。以schedutil为例它在计算目标频率时会把 QoS 频率约束作为一个下限即使负载很低频率也不会低于约束要求的值。这里要注意约束的粒度。频率约束通常以 kHz 为单位但不同平台的频点表不一样框架会把约束值向上对齐到最近的可用频点。如果你提了一个 1500000 kHz 的约束而平台频点表里只有 1400000 和 1600000那实际生效的是 1600000。这个对齐逻辑在cpufreq的policy层处理读代码时留意cpufreq_qos相关的部分。4.3 suspend约束如何阻止系统进入低功耗系统级 suspend 流程里PM QoS 也扮演了守门人的角色。如果存在很强的延迟约束suspend 流程可能会选择更浅的睡眠状态甚至直接放弃进入 suspend。具体来说suspend流程在决定进入哪个睡眠状态mem、standby等时会参考当前的 QoS 约束。如果约束要求延迟极低而某个睡眠状态的唤醒延迟超过这个值那这个状态就被排除。这在移动设备上尤其重要——用户态可能通过某个接口提了约束导致系统无法进入深度睡眠续航直接受影响。注意suspend 相关的 QoS 消费路径和 cpuidle 是分开的排查时不要只盯着 cpuidlesuspend 流程里的约束检查同样要看。5. 实战排查功耗降不下去时怎么顺藤摸瓜理论讲完回到最实际的部分。当你在真实项目里遇到系统功耗比预期高的问题怎么用 PM QoS 的视角去定位。5.1 第一步确认当前生效的约束值最直接的入口是 debugfsmount -t debugfs none /sys/kernel/debug cat /sys/kernel/debug/pm_qos/pm_qos_cpu_dma_latency如果开了CONFIG_PM_QOS_DEBUG这个节点会列出当前所有请求及其值。看到一堆请求里有一个值特别小比如 0 或者个位数那基本就是它了。如果没有 debugfs也可以通过tracefs里的power相关 tracepoint 来观察约束变化。pm_qos_update_request这类事件在powertrace 子系统里有对应的 tracepoint打开后能看到每次更新的调用栈直接定位到是哪个驱动在提约束。5.2 第二步反查请求来源拿到约束值后下一步是找到是谁提的。debugfs 节点通常会显示请求的地址但地址本身不直观。这时候有两个办法一是打开pm_qos相关的 tracepoint看更新时的调用栈栈顶就是调用者。二是直接在源码里搜pm_qos_add_request和pm_qos_update_request的调用点结合当前运行的驱动列表缩小范围。我个人的习惯是先用 tracepoint 抓一次完整的约束变化序列看看在什么操作之后约束值突然变小。比如一打开摄像头约束就变严那嫌疑就集中在 camera 相关的驱动上。5.3 第三步验证约束是否被正确释放找到来源后要确认这条约束是不是该释放时没释放。常见的情况是驱动在某个路径里 update 了强约束但对应的恢复路径没有 update 回宽松值导致约束一直挂着。验证方法是复现操作序列观察约束值的变化曲线。正常应该是操作时变严操作结束后恢复如果操作结束后值没回来那就是释放逻辑有 bug。这类问题在驱动里很常见尤其是错误处理路径——正常路径记得恢复出错路径忘了。5.4 一个真实的排查案例前面提到的音频驱动问题完整排查过程是这样的先用 debugfs 看到cpu_dma_latency的值是 50远小于正常待机时的默认值。然后打开 tracepoint发现这个值是在音频播放开始时被设置的但播放结束后没有恢复。进一步看代码发现驱动的stop路径里确实调用了 update 恢复但只在正常停止时走如果是被强制关闭比如进程被杀走的是另一条 cleanup 路径那条路径漏了恢复调用。修复很简单在 cleanup 路径里补上pm_qos_update_request恢复默认值。但定位过程花了小半天核心就是靠 debugfs 加 tracepoint 这两把工具。6. 几个容易踩的坑与经验总结最后这部分是我这些年攒下来的一些具体经验都是文档里不太会写、但实际会遇到的。6.1 初始值、恢复值、默认值别搞混PM_QOS_DEFAULT_VALUE是一个很宽松的值表示没有约束。add 的时候用它恢复的时候也用它。但有些驱动作者会自己定义一个默认值比如 100结果恢复时用的是 100 而不是PM_QOS_DEFAULT_VALUE导致系统始终带着一个 100us 的约束。这个坑很隐蔽因为 100 看起来已经够宽松了但在某些平台上它仍然会挡住最深的状态。6.2 per-device 和全局别用错再强调一次只影响单个设备的用dev_pm_qos_*需要影响整个 CPU 子系统的才用全局接口。我见过一个传感器驱动用全局接口提约束结果整个系统待机功耗翻倍就因为一个传感器的唤醒延迟需求被错误地放大到了全局。6.3 notifier 回调里别做重活notifier 回调在锁上下文里执行任何可能睡眠的操作都是禁忌。需要复杂处理的丢 workqueue。这一点在写自定义 notifier 订阅者时尤其要注意。6.4 调试选项该开就开CONFIG_PM_QOS_DEBUG和CONFIG_PM_DEBUG在开发阶段强烈建议打开。它们带来的调试节点和额外检查能帮你省下大量猜测时间。生产环境可以关掉以减少开销但开发板上一律开着。6.5 约束变化要可观测如果你的驱动会动态调整 QoS 约束建议在 update 的地方加上 tracepoint 或者至少是pr_debug把旧值、新值、调用点打出来。功耗问题往往和时序强相关没有日志的话事后很难还原当时的约束状态。PM QoS 这个框架本身不复杂难的是它在整个功耗子系统里的位置——它不直接省电但它决定了别人能不能省电。把它的聚合逻辑、注册链路、消费路径搞清楚再配上 debugfs 和 tracepoint 这两把工具大部分功耗降不下去的问题都能定位到具体的约束来源。真正花时间的往往不是理解框架而是找到那个忘了恢复约束的驱动。