
做内核功耗控制的人多半都遇到过这种场景音频播放突然卡顿、视频掉帧、某个外设 DMA 传输超时排查半天发现是 CPU 睡得太深醒来时黄花菜都凉了。最初期的解决方案非常粗暴——直接 disable idle让 CPU 保持“清醒”代价就是功耗报表惨不忍睹。后来 Linux 内核引入了 PM QoS framework也就是 Power Management Quality of Service 框架专门用来协调“谁需要 CPU 多快响应”和“CPU 到底能不能睡深、跑多快”之间的矛盾。这个系列讲功耗子系统到了第十二篇我就把 PM QoS 这个框架从数据结构、API、调用链到排查手段完整梳理一遍把那些网上不太好找的细节一次说清楚。这篇内容适合正在看内核功耗代码的人也适合做嵌入式 Linux 驱动开发、遇到外设延迟和性能问题需要定位根因的工程师。看完你至少能回答这三个问题PM QoS 到底在管什么、约束是怎么聚合的、以及为什么一个没释放的 request 会让整机功耗崩掉。1. PM QoS 框架到底解决了什么问题1.1 功耗管理里的“需求侧”与“供应侧”Linux 内核的功耗管理简单说就是一套“供需关系”供应侧是 CPU idle、CPU freq、设备 runtime PM 这些“执行者”它们决定 CPU 睡多深、跑多快、设备能不能挂起。需求侧则是各种各样的驱动、子系统、用户态程序它们对“CPU 最短多久能响应我”有明确预期。问题在于需求侧的声音是嘈杂的。声卡驱动希望 DMA 延迟不超过 200us网络驱动希望收包延迟尽量低触摸屏驱动希望按下到响应不超过 100ms。如果没有一个统一机制每个驱动就只能用自己的一套土办法去影响供应侧最常见的土办法就是“干脆不让 CPU 进深睡眠”。结果单个驱动看没什么问题多个驱动叠在一起CPU 浅睡都不敢进功耗直线上升。PM QoS 就是为这个“需求表达”设计的标准渠道。它定义了几类可量化的约束任何子系统都可以把自己的要求注册进去框架负责聚合、仲裁再把最终的有效值通知给供应侧。比如 CPU idle 收到“最大可接受延迟是 200us”的通知后会自动排除那些唤醒延迟超过 200us 的 C-state用浅睡眠替代深睡眠既不吵到需求方又能尽量节能。1.2 框架在系统中的位置把 PM QoS 放进整个系统里看它处于一个很典型的“中介层”位置需求者驱动声卡、网卡、存储控制器等、设备子系统、用户态程序核心框架PM QoS 核心模块pm_qos.c / dev_pm_qos.c负责约束请求的注册、更新、删除和聚合供应者cpuidle governor、cpufreq governor、设备 runtime PM 框架这个分工其实很像公司内部的会议室预订系统。各个部门提需求需要多大的会议室、几点到几点行政统一汇总最后按“最紧迫的需求”优先安排。PM QoS 也一样它不自己决定 CPU 睡不睡、跑多快它只负责把需求收集起来算出一个“当前必须满足的最严格条件”然后通知执行方。执行方拿到约束后怎么翻译是他们自己的事。提示理解 PM QoS 有个关键心法——“约束请求”是主动注册的“生效值”是被动算出来的。驱动只需要管好自己那份请求不需要关心系统里其他地方发生了什么。2. 核心数据结构与约束模型拆解2.1 pm_qos_constraints 结构体PM QoS 全局约束的核心数据结构是pm_qos_constraints定义在include/linux/pm_qos.h里。这个结构体是整个框架的基石字段不算多但每个都值得细看struct pm_qos_constraints { struct plist_head list; s32 target_value; s32 default_value; enum pm_qos_type type; struct blocking_notifier_head blocking_notifier; struct pm_qos_notifier_head atomic_notifier; };逐个说。list是一个 priority list优先级链表所有注册到这个类别的约束请求都会挂在这里。为什么用 plist 而不是普通 list因为每次聚合都要找到“最严格”的那个值plist 天然按优先级排序查最值就是 O(1) 操作不需要每次遍历整个链表排序。target_value就是当前生效的聚合值是供应侧真正会去读的那个数。default_value是没有任何请求时退回的默认值通常表示“无约束”比如对 DMA 延迟来说就是最大值等于不限制。type决定了聚合方向这个后文单独讲。最后是两个通知链blocking_notifier和atomic_notifier。这是给供应侧订阅变更通知用的。之所以要分两套是因为有些调用者比如 cpufreq 的某些路径会在原子上下文里执行不能睡眠只能跑 atomic notifier而像 cpuidle 这种可以睡眠的路径用 blocking notifier 更安全、开销也更小。2.2 全局约束的三种类别全局 PM QoS 一共有三个约束类别定义是一个枚举enum pm_qos_class { PM_QOS_CPU_DMA_LATENCY, PM_QOS_NETWORK_LATENCY, PM_QOS_ONLINE_LATENCY, PM_QOS_NUM_CLASSES, };PM_QOS_CPU_DMA_LATENCY是最常用、也是历史上最重要的一类它表示“CPU 对 DMA 请求的最大响应延迟”。数值越小说明 CPU 必须越快响应也就是不能睡太深。声卡、网卡这类有 DMA 的设备驱动基本都会用到它。PM_QOS_NETWORK_LATENCY是网络子系统用来表达收包延迟要求的。网络对延迟非常敏感如果一个 CPU 在深睡眠里磨蹭几百毫秒才醒来处理收包TCP 重传都发生了。所以网络栈在特定场景下也会注册这个约束。PM_QOS_ONLINE_LATENCY是后来加入的主要用在 CPU 热插拔场景它表示“把一个 CPU 从 offline 变为 online 的最大可接受延迟”。在某些功耗策略里CPU 会被动态 offline但有些驱动不能等太久这时候就通过这一类约束限制“别把 CPU 拔得太彻底”。这三类都维护在全局的pm_qos_array[]里每个元素就是一个pm_qos_constraints。驱动注册约束时传的是类别 ID框架内部自动找到对应的 constraints 结构体去操作。2.3 聚合规则为什么“最严格”才是有效值PM QoS 聚合的核心规则是取所有请求中最严格的那个作为target_value。但“最严格”的定义取决于type。PM_QOS_MIN类型请求值越大越严格所以聚合时取最大值PM_QOS_MAX类型请求值越小越严格所以聚合时取最小值就拿PM_QOS_CPU_DMA_LATENCY来说它的语义是“最大可容忍延迟”所以它其实是PM_QOS_MIN类型里的反向表述。假设声卡驱动注册了 100us网卡驱动注册了 200us那聚合出来的target_value就是 100us因为系统必须满足最严格的 100us 要求200us 的要求自然也被覆盖了。这个设计的好处很明显如果系统里只有几个宽松要求CPU 还是可以睡到较深的 C-state只要有一个严格请求冒出来大家就都得“让路”。从调度角度说这是“短板效应”的硬件化——木桶能装多少水取决于最短的那块板。static void pm_qos_update_target(struct pm_qos_constraints *c, struct plist_node *node, enum pm_qos_req_action action, int value) { ... /* 计算新的 target_value */ if (c-type PM_QOS_MIN) new_target plist_last(c-list)-prio; else new_target plist_first(c-list)-prio; ... /* 只有 target_value 真的变了才通知订阅者 */ if (new_target ! c-target_value) { c-target_value new_target; blocking_notifier_call_chain(c-blocking_notifier, ...); atomic_notifier_call_chain(c-atomic_notifier, ...); } }注意这段代码里的一个关键优化只有当聚合值target_value真的发生变化时才触发通知。如果新加一个请求但并没有“刷新最严格记录”那供应侧根本不需要被惊动这是 PM QoS 做了大量性能优化的地方。3. 全局 PM QoS 的 API 与使用细节3.1 三件套 APIadd / update / remove内核态使用 PM QoS 最经典的三个接口是void pm_qos_add_request(struct pm_qos_request *req, int pm_qos_class, s32 value); void pm_qos_update_request(struct pm_qos_request *req, s32 new_value); void pm_qos_remove_request(struct pm_qos_request *req);使用模式很固定。驱动在合适的位置定义一个struct pm_qos_request变量先add注册运行期间如果需求变化就update结束或卸载时remove。注意update_request传入的 request 必须此前已经 add 过否则内核会 hu 得很欢快。还有个配套接口pm_qos_request(qos_class)用来读取当前生效的聚合值。cpuidle 这类供应侧就是靠它获取约束的。稍微提一下旧接口。早期内核里用的是pm_qos_requirement()之类的函数直接传类别 ID 和值比较原始。现在推荐的做法是“一个请求一个结构体”这样框架可以追踪到具体是谁在持有约束调试时也能看清责任。3.2 通知机制的使用细节供应侧想要感知约束变化需要注册 notifier。PM QoS 提供两个注册接口int pm_qos_add_notifier(int pm_qos_class, struct notifier_block *notifier); int pm_qos_add_atomic_notifier(int pm_qos_class, struct notifier_block *notifier);这里有个使用上的坑。blocking notifier的回调函数允许睡眠可以做复杂操作比如重新计算 C-state 等级表但它不能被用在硬中断或持有自旋锁的上下文里。atomic notifier则必须保证回调函数里不能有任何可能睡眠的调用。实际内核代码里cpuidle 的menu governor通常是注册 blocking notifier 的因为它需要更新 governor 内部的延迟参数表这个操作不轻量。而某些 arch 层面的频率调整路径会用 atomic notifier因为它可能在中断上下文中被触发。如果你的驱动只需要“被动等待通知”不需要主动查值那直接在 notifier 回调里读取pm_qos_request(class)拿最新值就行。但千万别在 atomic notifier 的回调里干重活比如打印、加锁、分配内存内核会直接报 scheduling while atomic 的错。3.3 用户态接口 /dev/cpu_dma_latencyPM QoS 最有趣的用户态接口是/dev/cpu_dma_latency。这个设备文件的语义是打开它写入一个延迟值约束就开始生效只要文件不关闭约束就一直存在文件关闭时内核自动把约束移除。这个“关闭即释放”的设计非常巧妙因为用户态程序经常因为崩溃、被 kill 等原因退出如果约束要靠显式释放很容易泄漏。而通过 fd 的生命周期来绑定约束进程退出时内核自动清理 fd约束自然消失不会留下“僵尸约束”。实际操作中想在用户态禁止 CPU 进入深睡眠可以这样#include stdio.h #include fcntl.h #include unistd.h int main(void) { int fd open(/dev/cpu_dma_latency, O_WRONLY); if (fd 0) { perror(open); return 1; } /* 0 表示最大可接受延迟为 0即 CPU 必须极速响应 */ int latency 0; write(fd, latency, sizeof(latency)); /* 保持进程不退出约束就一直生效按任意键退出 */ getchar(); close(fd); return 0; }很多音频播放器、实时应用会用这个技巧在播放期间写 0让 CPU 保持快速响应播放结束或进程退出后自动释放。功耗优化的同事如果发现整机 C-state 一直进不了深睡第一件事就是查有没有人开着这个 fd。注意向/dev/cpu_dma_latency写入的值是“最大容忍延迟”不是“需要的最小性能”。写 0 代表极端敏感写一个很大的数比如 1000000基本等于不设约束。4. per-device PM QoS 与设备场景4.1 为什么需要 per-device 约束全局 PM QoS 的问题在于粒度太粗。一个 USB 网卡对延迟敏感并不意味着系统里每一个 CPU 都必须响应它一个 GPU 的 DMA 需求也不该限制另一个与它无关的 CPU。尤其在多核 SoC 上不同设备挂在不同的总线域上它们的延迟敏感度只跟自己的中断亲和性相关。于是内核引入了 per-device PM QoS也就是struct device_pm_qos。它挂在每个struct device的power.qos字段上允许驱动对单个设备注册约束精度从“全局”细化到“设备”。在 ARM SoC 平台上这个机制配合 runtime PM 使用非常普遍。4.2 dev_pm_qos 接口与约束类型per-device PM QoS 的核心 API 长这样int dev_pm_qos_add_request(struct device *dev, struct dev_pm_qos_request *req, enum dev_pm_qos_req_type type, s32 value); int dev_pm_qos_update_request(struct dev_pm_qos_request *req, s32 new_value); int dev_pm_qos_remove_request(struct dev_pm_qos_request *req); s32 dev_pm_qos_read_value(struct device *dev, enum dev_pm_qos_req_type type);约束类型主要有这么几种类型含义典型场景DEV_PM_QOS_RESUME_LATENCY设备从挂起状态恢复的最大可接受延迟触摸屏、传感器要求 resume 不能太慢DEV_PM_QOS_LATENCY设备对任意操作的最大响应延迟外设 DMA 与中断响应DEV_PM_QOS_FLAGS一组特殊标志位禁用 runtime PM 的某些状态DEV_PM_QOS_FREQ设备工作频率下限较新内核引入某些设备必须保持最低频率才能正常工作用得最多的还是DEV_PM_QOS_RESUME_LATENCY。它的逻辑是runtime PM 在决定要不要把一个设备挂起之前会算一下“真要恢复它得花多少时间”如果恢复时间超过了驱动注册的约束值那就干脆别挂起了。这是一种非常优雅的“自动权衡”不需要驱动自己维护一个状态机只需声明“我能等多长时间”框架自动帮你决策。4.3 sysfs 接口与用户态调试per-device PM QoS 在 sysfs 里有对应节点路径一般是/sys/devices/.../power/pm_qos_resume_latency_us /sys/devices/.../power/pm_qos_latency_tolerance_us往pm_qos_resume_latency_us里写数字就等于注册了一个用户态的约束。写0表示恢复延迟必须为 0设备不许挂起写一个很大的值或max表示没有约束。读取该文件能看到当前生效的聚合值。这个节点对调试特别有用。比如你怀疑某个设备挂起后恢复太慢可以用脚本临时给它写一个小值观察 runtime PM 是否还继续挂起它以此验证恢复延迟估算是否准确。4.4 与 runtime PM 的协作机制per-device PM QoS 之所以能发挥威力是因为它和 runtime PM 框架深度绑定。runtime PM 在__rpm_suspend前会调用dev_pm_qos_read_value(dev, DEV_PM_QOS_RESUME_LATENCY)检查约束如果约束值小于预计恢复延迟就直接否决本次挂起。这意味着驱动开发者不需要再手动“在关键时刻禁止 runtime PM”只要把约束声明好框架会自动在这两者之间找平衡。我在实际项目中用这个机制解决过一个棘手问题某款触控芯片平时功耗要求必须挂起但一旦进入画板模式唤醒延迟必须小于 2ms。如果没有 per-device QoS就得写一堆状态判断代码改成在切换画板模式时dev_pm_qos_update_request一下整个流程干净多了。5. PM QoS 如何影响 CPU 频率与 idle 状态5.1 cpuidle 的联动机制PM QoS 对 CPU idle 的影响是最直观的。cpuidle 的menu governor在选择下一个 C-state 时会读pm_qos_request(PM_QOS_CPU_DMA_LATENCY)拿当前约束值然后排除所有唤醒延迟超过这个值的 C-state。举个例子假设 CPU 有这么几档 C-stateC-state唤醒延迟功耗C110us较高C2100us中等C61000us极低如果 PM QoS 约束值是 200us那 C6 会被直接排除governor 最多只能选 C2。如果约束值是 50us连 C2 都不能选只能在 C1 待着。如果约束值非常大比如没有约束那 governor 就会按自己的策略大胆冲向 C6。这个联动过程其实是“两阶段”的第一阶段某个驱动通过pm_qos_update_request更新约束触发 notifier第二阶段cpuidle 收到通知后重新计算可用的 C-state 范围。在 menu governor 的代码里你会看到latency_req这个变量的来源就是 PM QoS 的 CPU_DMA_LATENCY 聚合值。5.2 cpufreq 的联动机制PM QoS 影响 cpufreq 主要在频率下限层面。某些设备对 CPU 性能有最低要求cpufreq驱动会从 PM QoS 频率约束中读取聚合值把它作为 governor 选择最低频率的下限。经典场景是视频播放解码器要求 CPU 至少跑到 1GHz 才能保证帧率如果系统没有 PM QoS 频率约束governor 可能会为了省电把频率压到 800MHz结果视频掉帧。这类约束在新内核里被纳入DEV_PM_QOS_FREQ或全局的 frequency qos 中。driver 更新约束后cpufreq governor 会收到通知把policy-min临时提上去约束释放后policy-min再回落到默认值。5.3 实际场景对照音频、视频、触摸把这些机制串起来看一个真实场景音频播放时声卡驱动注册PM_QOS_CPU_DMA_LATENCY约束为 200us。之后哪怕系统进入空闲状态cpuidle 也只允许它睡到 C2而不会进 C6。这就是为什么很多手机在插着耳机播放音乐时CPU 的 C-state 深度肉眼可见地变浅也是为什么有些优化方案一旦误操作比如写死一个极小延迟值整机功耗会瞬间冒出几百毫瓦的额外开销。触摸屏场景则相反它更多用 per-device 的 resume latency 约束。触摸屏 suspend 后需要快速唤醒如果恢复时间超过约束值runtime PM 就不让它睡。这种约束粒度非常细不会影响 CPU 的整体睡眠深度。我做过一个项目测试人员在播放 4K 视频时发现 CPU 频率波动剧烈怀疑 cpufreq 策略有问题。最后排查发现是某个音频驱动在视频播放期间还一直挂着 50us 的 DMA latency 约束cpuidle 不敢睡深导致 CPU 忙等进而拉高频率。问题不在 cpufreq而在“约束不该在同一时间被无脑持有”。提示排查功耗问题一定要记住“PM QoS 约束会造成连锁反应”一个小的延迟约束先限制 C-state再影响 idle 时间占比最后拉高频率和功耗。表面现象在 cpufreq根因却在 PM QoS。6. 排查技巧与常见问题实录6.1 怎么看到底谁注册了约束PM QoS 最大的痛点就是“找不到谁在持有约束”。内核里没有一个开箱即用的cat /proc/pm_qos接口但这不代表没法查。第一招用 tracepoint。内核在 PM QoS 更新的路径上打了 tracepoint事件名是pm_qos_update在events/power目录下# 挂载 tracefs mount -t tracefs nodev /sys/kernel/debug/tracing # 打开 pm_qos_update 事件 echo 1 /sys/kernel/debug/tracing/events/power/pm_qos_update/enable # 查看输出 cat /sys/kernel/debug/tracing/trace用法与常见问题# 全局PM QoS类别的可能字段pm_qos_class, value, action echo pm_qos_class 0 value 200 /sys/kernel/debug/tracing/events/power/pm_qos_update/filter echo 1 /sys/kernel/debug/tracing/events/power/pm_qos_update/enable字段名在不同内核版本里略有差异可以先cat /sys/kernel/debug/tracing/events/power/pm_qos_update/format确认可用字段。filter 生效后再结合stacktrace选项就能看到是哪个调用路径在修改约束。第二招用 ftrace 的 function_graph 跟踪pm_qos_update_request的调用栈。这个对定位内核态的“真凶”非常直接echo function_graph /sys/kernel/debug/tracing/current_tracer echo pm_qos_update_request /sys/kernel/debug/tracing/set_graph_function echo 1 /sys/kernel/debug/tracing/tracing_on第三招直接从/dev/cpu_dma_latency文件句柄下手。用lsof或查/proc/*/fd看看有没有进程一直开着这个设备。很多用户态程序导致的约束泄漏通过这个方式能一击命中。6.2 常见错误约束忘了释放PM QoS 使用中最常见的问题就是约束没有释放。典型场景有两个第一个是驱动在 probe 阶段pm_qos_add_request但 remove 或 error path 里忘了pm_qos_remove_request。这个还好治代码 review 能发现。麻烦的是第二个场景驱动在运行中临时update_request把约束改严了但某个分支提前 return 没恢复原值。这类问题在正常流程里测不出来只有特定状态触发时才会导致系统“不合时宜地保持高性能/低性能”。所以我的经验是所有 PM QoS request 的回退逻辑必须放在函数出口统一处理别在中间分支里散落几个恢复点。如果一个函数有两处以上需要改约束优先考虑用 goto cleanup 统一收口。6.3 常见错误延迟语义理解反了另一个高频坑是把约束值的方向搞反尤其刚接触 PM QoS 的人很容易踩。PM_QOS_CPU_DMA_LATENCY的值表示“可容忍的最大延迟”值越小约束越严。有人认为“写个 10000 就能让 CPU 更快响应”这完全是反的——10000 表示你能容忍 10ms 的响应时间CPU 可以放心大胆睡。为了不再混淆记住一句话延迟类约束里数字越小越“作”。凡是想要高性能、低延迟就写小值想要省电、允许睡深就写大值。6.4 排查思路速查表现象可能原因定位手段CPU 长时间深睡外设经常超时DMA 延迟约束缺失或值太大查/dev/cpu_dma_latency当前值、tracepoint 看更新CPU 从不进深睡功耗偏高有驱动/用户态进程持有过小延迟约束查/proc/*/fd中 cpu_dma_latency、ftrace 查调用栈某设备频繁被恢复功耗波动resume latency 约束过严runtime PM 无法挂起查/sys/devices/.../power/pm_qos_resume_latency_us某设备挂起后唤醒太慢resume latency 约束被放开runtime PM 选择了深度挂起用 dev_pm_qos_read_value 确认聚合值频率始终压不下去频率 QoS 约束被某驱动锁住查 cpufreq 的 QoS 相关 debugfs 节点这套速查表基本覆盖了我在项目里遇到过的绝大多数 PM QoS 相关问题。关键是先确定约束值“当前是多少”再顺着 tracepoint 找“谁最后改的”最后再用 ftrace 回溯“为什么改”。三层定位法走下来基本没有找不到的责任方。7. 从旧实现到新框架PM QoS 演进脉络回头看 PM QoS 的发展挺有意思。最早的实现是pm_qos_params.c用一套相对简单的参数模型接口也比较粗糙。后来重构出pm_qos.c和include/linux/pm_qos.h才有了今天全局约束 per-device 约束的双层结构。再往后per-device PM QoS 慢慢吸收设备电源管理的各种场景加入了 latency tolerance、resume latency甚至频率约束。这个演进的核心驱动其实是“粒度”的不断细化。最早只能表达“全系统 DMA 延迟”后来能表达“某个设备恢复多快”再后来能把“最低频率”也纳入 QoS 体系。配合 ARM 平台的 EASEnergy-Aware Scheduling等新机制PM QoS 在功耗和性能之间的调度角色越来越重。对于做产品的人来说掌握这套框架不仅是看懂 cpuidle 和 cpufreq 的前置条件也是以后排查各种“玄学性能问题”的基础功。我个人在实际项目中最后悔的一件事就是早期把 PM QoS 当成“万能性能开关”来用需求不明确就随手挂个超严约束结果整机功耗和心理预期严重不符。后来教训多了才明白PM QoS 的本质是“需求声明”不是“命令下发”。每个驱动都应该老老实实声明自己真正需要多少延迟余量而不是拍脑袋写个 0 上去。最稳妥的做法是在产品开发阶段就把所有约束注册点统一审计一遍配合 tracepoint 做一轮长时间抓取确认每个约束的存在周期都符合预期。这套框架本身不复杂但用好了是功耗优化的利器用不好就是性能问题的泥潭。