ARTICLE DETAIL

资讯详情

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

嵌入式Linux功耗优化:PM QoS框架原理、核心数据结构与CPU idle联动实战

嵌入式Linux功耗优化:PM QoS框架原理、核心数据结构与CPU idle联动实战 1. 功耗管理的隐形裁判PM QoS 到底在管什么做嵌入式 Linux 功耗优化的朋友大概率都经历过这种场景系统待机功耗死活降不下去查了一圈 CPU idle、DVFS、时钟门控都配了最后发现是某个外设驱动在偷偷阻止系统进入低功耗状态。或者反过来某个音频播放场景下声音断断续续查来查去发现是某个模块把延迟约束卡得太死导致 CPU 频繁从低功耗状态被拽出来。这类问题的幕后主角往往就是 PM QoS framework。PM QoS全称 Power Management Quality of Service翻译过来叫电源管理服务质量。名字听着挺唬人但核心思想特别朴素系统里各个模块对功耗和性能的需求是互相冲突的有人希望越省电越好有人要求响应必须够快PM QoS 就是给这些需求提供一个统一的表达、聚合和裁决机制。它不直接控制硬件而是把各模块的诉求收集起来算出当前系统应该满足的“底线”然后让 CPU idle、DVFS、runtime PM 这些真正的功耗管理模块据此做决策。这篇文章面向的是有一定 Linux 内核基础、正在做功耗优化或者准备深入 PM 子系统的开发者。我会把 PM QoS 的框架结构、核心数据结构、注册与更新流程、通知机制、以及和 CPU idle/DVFS 的联动关系全部梳理一遍。源码基于 Linux 5.x 到 6.x 的通用实现不同版本细节可能有差异但主干逻辑是一致的。读完之后你应该能看懂内核里那些pm_qos_add_request、cpu_latency_qos_add_request到底在干什么也能自己排查“谁在阻止系统省电”这类问题。2. 框架整体设计与核心思路拆解2.1 为什么需要 PM QoS一个“众口难调”的经典问题先想清楚一个根本问题为什么不能每个驱动自己直接去设置 CPU 频率或者 idle 状态答案很简单因为需求会打架。举个例子CPU 想进 C-state 深度睡眠但网卡驱动要求中断响应延迟不能超过 50 微秒音频驱动要求 DMA 传输不能被打断而温控模块又希望 CPU 别跑太高频。如果每个模块都直接去操作硬件最后的结果就是互相覆盖谁后设置谁生效系统行为完全不可预测。PM QoS 的设计哲学就是引入一个“中间层”。每个有需求的模块不直接操作硬件而是向 PM QoS 框架提交一个约束constraint框架负责把这些约束按类型聚合计算出最终的有效值再通过通知链告诉真正的功耗管理模块。这样做的好处是需求表达和决策执行解耦聚合逻辑集中可控调试时也能清楚看到每个约束是谁提的。提示PM QoS 本身不省电它只是“需求的搬运工和仲裁者”。真正省电的是 CPU idle、DVFS、clock framework、regulator framework 这些执行者。2.2 两类约束性能型与延迟型PM QoS 把约束分成两大类这个分类是理解整个框架的钥匙。第一类是性能型约束performance constraints典型代表是 CPU 的 DMA 延迟约束和吞吐量约束。这类约束的特点是值越大代表性能要求越高。比如PM_QOS_CPU_DMA_LATENCY它表达的是“CPU 从 idle 唤醒到能处理 DMA 的最大容忍延迟”单位是微秒。你提交一个 0意思就是“一秒都不能等别让 CPU 睡太深”你提交一个 1000意思是“睡 1 毫秒以内醒来就行”。框架聚合时取的是所有请求里最严格的那个也就是最小值。第二类是延迟型约束latency constraints更准确说是“可容忍延迟”的表达。等等这里容易绕晕我换个说法。在较新的内核里PM QoS 的约束类型主要就是PM_QOS_CPU_DMA_LATENCY和PM_QOS_RESUME_LATENCY这类它们统一用“可容忍延迟”来表述值越小约束越强。框架聚合时取最小值因为所有请求里最小的那个容忍度就是系统必须满足的底线。我用一个生活类比帮你记住一群人约饭有人说“我最多等 10 分钟”有人说“我最多等 30 分钟”那这群人整体能接受的最长等待就是 10 分钟。PM QoS 取最小值就是这个道理。2.3 框架分层请求层、聚合层、通知层从代码结构上看PM QoS 可以分成三层。请求层是各个驱动和子系统调用的接口比如pm_qos_add_request()、pm_qos_update_request()、pm_qos_remove_request()以及针对 CPU 延迟的封装cpu_latency_qos_add_request()等。这一层负责把请求挂到对应的约束类型上。聚合层是框架的核心维护每种约束类型的请求链表和当前有效值。每次有请求增删改都会重新计算聚合值。聚合逻辑对性能型约束取最小值这个逻辑写在pm_qos_get_value()之类的函数里。通知层通过blocking_notifier或者原子通知链把有效值的变化广播出去。订阅者包括 CPU idle 驱动、cpuidle governor、DVFS 框架等。它们收到通知后调整自己的行为策略。这三层的关系可以用一句话概括请求层提交诉求聚合层算出底线通知层传达指令。2.4 全局约束与 per-device 约束早期 PM QoS 主要是全局的比如整个系统的 CPU DMA 延迟约束。后来随着设备电源管理精细化又引入了 per-device 的 PM QoS典型的是dev_pm_qos系列接口比如dev_pm_qos_add_request()、dev_pm_qos_update_request()。这类约束挂在具体的struct device上主要影响该设备的 runtime PM 行为比如是否允许设备进入低功耗状态、resume 延迟要求等。全局约束和 per-device 约束的区别很重要全局约束影响的是系统级决策比如 CPU idle 深度per-device 约束影响的是设备级决策比如某个设备能不能 suspend。两者用的数据结构和通知机制有相似之处但作用域完全不同。排查问题时先搞清楚是全局问题还是设备问题能省不少时间。3. 核心数据结构与关键接口解析3.1 约束类型的枚举定义PM QoS 支持的约束类型定义在include/linux/pm_qos.h里。较新内核里全局约束类型主要有enum pm_qos_type { PM_QOS_UNITIALIZED, PM_QOS_MAX, /* 取最大值聚合 */ PM_QOS_MIN, /* 取最小值聚合 */ PM_QOS_SUM /* 求和聚合 */ };而具体的约束“类别”通过enum pm_qos_flags或者约束 ID 来区分。历史上比较经典的是PM_QOS_CPU_DMA_LATENCY现在很多内核把它统一到cpu_latency_qos接口下。你在代码里看到cpu_latency_qos_add_request()底层其实就是往PM_QOS_CPU_DMA_LATENCY这个约束上挂请求。注意不同内核版本的枚举命名和数量有变化看代码时以你手上那份pm_qos.h为准别死记硬背某个版本的枚举值。3.2 请求结构体 pm_qos_request每个请求用一个struct pm_qos_request表示核心字段包括struct pm_qos_request { struct plist_node node; /* 挂到约束的请求链表上 */ int pm_qos_class; /* 属于哪个约束类别 */ s32 value; /* 请求的值 */ ... };这里用的是plist_node也就是优先链表节点。为什么用 plist 而不是普通链表因为聚合时需要快速找到最小值plist 本身按优先级排序取最小值就是取链表头O(1) 复杂度。这个设计细节体现了内核对热路径性能的考量——PM QoS 的更新可能很频繁聚合计算不能太重。3.3 约束结构体 pm_qos_constraints每种约束类型对应一个struct pm_qos_constraintsstruct pm_qos_constraints { struct plist_head list; /* 请求链表 */ s32 target_value; /* 聚合后的目标值 */ s32 default_value; /* 没有请求时的默认值 */ s32 no_constraint_value; /* 无约束时的值 */ enum pm_qos_type type; /* 聚合类型MIN/MAX/SUM */ struct blocking_notifier_head *notifiers; /* 通知链 */ };target_value是聚合结果default_value是链表为空时的兜底值no_constraint_value表示“完全没有约束”的语义值。比如 CPU DMA 延迟约束no_constraint_value通常是PM_QOS_CPU_DMA_LAT_DEFAULT_VALUE代表不限制。3.4 核心接口一览常用的全局 PM QoS 接口有这么几个接口作用典型调用场景pm_qos_add_request()添加一个请求驱动初始化时声明约束pm_qos_update_request()更新已有请求的值运行中动态调整需求pm_qos_remove_request()移除请求驱动卸载或需求消失cpu_latency_qos_add_request()添加 CPU 延迟请求的封装需要控制 CPU idle 深度cpu_latency_qos_update_request()更新 CPU 延迟请求动态调整延迟容忍度dev_pm_qos_add_request()添加 per-device 请求设备级电源管理dev_pm_qos_update_request()更新 per-device 请求设备运行态调整这些接口的返回值通常是 0 表示成功负值表示错误。pm_qos_add_request()在早期版本里可能睡眠所以不能在原子上下文调用新版本用 plist 和自旋锁保护部分路径可以在原子上下文用但保险起见初始化阶段调用最稳妥。3.5 通知链的注册与回调订阅 PM QoS 变化的模块通过pm_qos_add_notifier()注册通知块static struct notifier_block my_pm_qos_nb { .notifier_call my_pm_qos_callback, }; pm_qos_add_notifier(PM_QOS_CPU_DMA_LATENCY, my_pm_qos_nb);回调函数收到PM_QOS_UPDATE或PM_QOS_ADD/PM_QOS_REMOVE事件参数里带着新的有效值。CPU idle 驱动就是通过这种方式感知延迟约束变化的。这里有个坑通知链回调可能在原子上下文执行回调里不能睡眠不能调用可能睡眠的函数。我见过有人在回调里直接调用msleep()调试结果系统直接挂死排查半天才发现是上下文问题。4. 实操过程与核心环节实现4.1 从零写一个 PM QoS 请求模块光看接口不过瘾我带你写一个完整的内核模块演示如何添加、更新、移除 PM QoS 请求并观察它对系统的影响。先看模块代码骨架#include linux/module.h #include linux/kernel.h #include linux/pm_qos.h #include linux/cpu.h static struct pm_qos_request my_qos_req; static int __init my_qos_init(void) { /* 添加一个 CPU DMA 延迟请求容忍 100 微秒 */ pm_qos_add_request(my_qos_req, PM_QOS_CPU_DMA_LATENCY, 100); pr_info(my_qos: request added, value100us\n); return 0; } static void __exit my_qos_exit(void) { pm_qos_remove_request(my_qos_req); pr_info(my_qos: request removed\n); } module_init(my_qos_init); module_exit(my_qos_exit); MODULE_LICENSE(GPL); MODULE_DESCRIPTION(PM QoS demo module);对应的 Makefileobj-m my_qos_demo.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: make -C $(KDIR) M$(PWD) modules clean: make -C $(KDIR) M$(PWD) clean编译加载后用dmesg能看到打印。但光加载看不出效果我们得验证约束真的生效了。4.2 验证约束是否生效观察 CPU idle 状态最直接的验证方法是看 CPU 进入的 idle 状态深度。延迟约束值越小CPU 越难进入深 idle。你可以这样操作先记录基线在没加载模块时用cpuidle的 sysfs 接口或者turbostat、powertop观察 idle 状态分布。然后加载模块把请求值设成 0再观察。理论上 CPU 会大量停留在浅 idle比如 C1深 idleC3 及以上的占比会明显下降。如果你用的是带cpuidle统计的内核可以读cat /sys/devices/system/cpu/cpu0/cpuidle/state*/usage对比加载模块前后的 usage 变化。这个实验能让你直观感受到 PM QoS 的威力——一个请求就能改变整个 CPU 的睡眠行为。提示在虚拟机里做这个实验可能看不到效果因为虚拟化环境通常不暴露真实的 C-state。建议在真实硬件或者支持 cpuidle 的嵌入式板子上验证。4.3 动态更新请求值模拟场景切换实际产品里约束值往往随场景变化。比如音频播放时要求低延迟暂停后就可以放宽。我们扩展模块通过 sysfs 暴露一个接口来动态更新static ssize_t value_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t count) { s32 val; if (kstrtos32(buf, 0, val)) return -EINVAL; pm_qos_update_request(my_qos_req, val); pr_info(my_qos: updated to %d\n, val); return count; }pm_qos_update_request()内部会先移除旧值再插入新值然后触发聚合和通知。注意如果新值和旧值相同框架会做短路优化不触发通知这个细节在调试时要注意——别以为没打印就是没生效。4.4 per-device PM QoS 的实操设备级的约束用法类似但挂在struct device上static struct dev_pm_qos_request dev_qos_req; dev_pm_qos_add_request(dev, dev_qos_req, DEV_PM_QOS_RESUME_LATENCY, 5000);DEV_PM_QOS_RESUME_LATENCY表示设备 resume 的可容忍延迟单位微秒。这个值会影响 runtime PM 的决策如果设备声明的 resume 延迟很长电源管理核心可能更倾向于让它 suspend。per-device 约束的一个关键点是它和pm_runtime的联动。设备 suspend 前PM core 会检查该设备上的 QoS 约束如果约束不允许suspend 就会被拒绝。排查“设备为什么不进 suspend”时/sys/kernel/debug/pm_qos/下的调试信息如果内核开了 debugfs能直接告诉你答案。4.5 参数选择延迟值到底该设多少这是实操中最容易拍脑袋的地方。我分享一套自己的估算方法。以 CPU DMA 延迟为例这个值本质上是“CPU 从 idle 醒来处理中断的最大时间”。它取决于几个因素中断控制器的响应时间、CPU 从目标 C-state 唤醒的硬件延迟、以及软件中断处理的调度延迟。硬件手册通常会给出各 C-state 的 exit latency比如 C1 是 1 微秒C3 是 50 微秒C6 是 200 微秒。如果你要求延迟不超过 100 微秒那 C6 就不能用了因为它的 exit latency 已经 200 微秒超了。框架会把约束传给 cpuidle governorgovernor 在选择状态时过滤掉 exit latency 超过约束的 C-state。所以设值的时候先查硬件手册的 exit latency 表再留一定余量。设 100 微秒实际能用的最深状态就是 exit latency 小于 100 的那个。注意不同 SoC 的 exit latency 差异巨大别照搬别人的参数。一定要以自己平台的 datasheet 为准。5. 常见问题与排查技巧实录5.1 系统待机功耗降不下来怎么定位这是最高频的问题。排查思路我整理成一个流程第一步确认 CPU idle 是否正常工作。读/sys/devices/system/cpu/cpu0/cpuidle/state*/usage看深状态有没有被使用。如果深状态 usage 一直是 0说明有东西在阻止。第二步检查 PM QoS 约束。如果内核开了 debugfs读/sys/kernel/debug/pm_qos/下的文件能看到每种约束的当前值和请求列表。如果cpu_dma_latency的值是 0 或者很小的数那就是有模块提交了严格约束。第三步找出是谁提交的。debugfs 的请求列表通常带调用者信息如果内核配置了CONFIG_PM_QOS_DEBUG。没有的话可以用ftrace跟踪pm_qos_add_request的调用栈echo function /sys/kernel/debug/tracing/current_tracer echo pm_qos_add_request /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on # 触发场景后 cat /sys/kernel/debug/tracing/trace调用栈会直接告诉你是哪个驱动干的。5.2 常见问题速查表现象可能原因排查方法深 C-state 不进入有模块提交了低延迟约束查 debugfs pm_qosftrace 跟踪 add_request设备不 suspendper-device QoS 约束阻止查/sys/kernel/debug/pm_qos/设备相关项音频卡顿延迟约束过松CPU 睡太深检查音频驱动的 QoS 请求是否生效更新请求无反应新旧值相同被短路确认值确实变化检查返回值通知回调挂死在原子上下文睡眠检查回调函数是否调用睡眠接口模块卸载后约束残留忘记 remove_request确保 exit 函数里移除所有请求5.3 几个容易踩的坑第一个坑是忘记移除请求。模块卸载时如果不调用pm_qos_remove_request()约束会一直挂在链表上系统功耗永远降不下来。更糟的是如果模块被卸载后内存被回收链表里还挂着野指针后续遍历直接崩溃。所以 exit 函数里必须清理干净。第二个坑是在中断上下文调用可能睡眠的接口。虽然新版本 PM QoS 用了自旋锁但某些路径比如通知链回调仍然有上下文限制。保险做法是在进程上下文初始化和更新中断里只做标记延后处理。第三个坑是误解聚合方向。性能型约束取最小值这个方向别搞反。我曾经见过有人以为提交一个大值就能“提升性能”结果提交 1000000 微秒等于告诉系统“随便睡”性能反而掉了。记住延迟约束的值是“可容忍延迟”越小越严格。第四个坑是per-device 和全局约束混用。两者作用域不同全局约束影响 CPU idleper-device 影响设备 runtime PM。排查问题时先分清是哪一类别在错误的方向上浪费时间。5.4 调试技巧用 debugfs 一网打尽如果内核配置了CONFIG_PM_QOS_DEBUG和 debugfs/sys/kernel/debug/pm_qos/下会有非常详细的信息。每个约束类型一个文件内容包含当前有效值、默认值、以及所有请求的列表带请求值和调用者。这个接口在排查“谁在捣乱”时简直是神器。没有 debugfs 的话可以自己写个模块遍历约束链表打印但不推荐在生产环境这么干因为遍历时如果没加锁会有竞态。调试阶段用用可以正式排查还是靠 ftrace 和 debugfs。6. 与 CPU idle、DVFS 的联动机制6.1 CPU idle 如何消费 PM QoS 约束CPU idle 驱动在初始化时会注册 PM QoS 通知块订阅PM_QOS_CPU_DMA_LATENCY。当约束值变化时回调被触发idle 驱动更新内部的延迟限制。cpuidle governor比如 menu governor在选择 idle 状态时会读取这个限制过滤掉 exit latency 超标的状态。具体来说menu governor 的menu_select()里会调用pm_qos_read_value()或者读取缓存的约束值然后和每个 C-state 的exit_latency比较。只有exit_latency 约束值的状态才在候选集里。这个过滤是硬性的不满足直接排除。6.2 DVFS 与 PM QoS 的关系DVFS动态电压频率调整和 PM QoS 的关系稍微间接一些。PM QoS 本身不直接设置频率但它提供的延迟约束会影响 governor 的决策。比如 schedutil governor 在计算目标频率时会考虑 CPU 的唤醒延迟需求。如果延迟约束很严格governor 可能倾向于保持较高频率避免降频后响应变慢。另外per-device 的 PM QoS 约束会影响设备的 runtime PM而设备的活动状态又会影响系统负载间接影响 DVFS。这条链路比较长排查时要有耐心一层层往下查。6.3 通知链的执行顺序问题多个模块订阅同一个约束时通知链的执行顺序由注册顺序决定。这个顺序在某些场景下很关键。比如一个模块依赖另一个模块先更新状态如果顺序反了可能读到旧值。内核的blocking_notifier按注册顺序调用没有优先级机制。如果确实有顺序要求只能靠控制注册时机来保证。我遇到过一个案例某个传感器驱动和显示驱动都订阅了延迟约束显示驱动假设传感器已经更新完毕结果因为注册顺序问题读到旧值导致显示异常。解决办法是调整初始化顺序让传感器先注册。这种问题很隐蔽调试时容易怀疑到别的地方去。7. 版本演进与代码阅读建议7.1 从旧接口到新接口的迁移早期内核用的是pm_qos_add_request()配合PM_QOS_CPU_DMA_LATENCY这类枚举。后来内核引入了cpu_latency_qos_add_request()这样的封装语义更清晰。再后来per-device 的dev_pm_qos接口逐渐完善。如果你维护的是老代码看到pm_qos_add_request(req, PM_QOS_CPU_DMA_LATENCY, val)可以平滑迁移到cpu_latency_qos_add_request(req, val)。新接口内部还是操作同一个约束只是封装了一层可读性更好。7.2 读源码的切入点想深入理解 PM QoS我建议按这个顺序读源码先读include/linux/pm_qos.h把数据结构和接口声明过一遍建立整体印象。然后读kernel/power/qos.c这是全局 PM QoS 的核心实现重点看pm_qos_add_request()、pm_qos_update_target()、pm_qos_get_value()这几个函数。接着读drivers/base/power/qos.c这是 per-device 的实现。最后读drivers/cpuidle/governors/menu.c看约束是怎么被消费的。读的时候带着问题聚合逻辑在哪、通知什么时候发、锁怎么加的。把这三条线理清楚框架就通了。7.3 一个容易被忽略的细节默认值的语义default_value和no_constraint_value的区别值得单独说。default_value是请求链表为空时的有效值no_constraint_value是“无约束”的语义表示。对于 CPU DMA 延迟约束no_constraint_value通常是一个很大的数比如INT_MAX表示不限制。当所有请求都移除后有效值回到default_value如果default_value等于no_constraint_value就等于完全放开。这个细节在调试时很重要如果你看到约束值突然变成一个很大的数别慌那可能只是所有请求都被移除了系统回到了无约束状态。8. 实际项目中的经验体会我在几个嵌入式项目里深度用过 PM QoS最大的体会是它是一把双刃剑用好了省电用不好就是功耗黑洞。见过太多项目驱动开发者为了“保险”随手加一个 0 延迟的 QoS 请求结果整个系统的深睡眠全废了。所以我的第一条经验是加 QoS 请求前先问自己真的需要吗值能不能放宽。第二条经验是善用 debugfs 和 ftrace。功耗问题往往跨模块光看代码很难定位。debugfs 的 pm_qos 信息加上 ftrace 的调用栈基本能覆盖 90% 的排查场景。建议在产品开发早期就把CONFIG_PM_QOS_DEBUG打开别等到出问题才想起来。第三条经验是约束值要跟着场景走。静态设一个值往往不是最优。音频、视频、游戏这些场景对延迟的要求不同动态调整才能兼顾功耗和体验。但动态调整要注意时机别在关键路径上频繁更新否则通知链的开销也不小。最后分享一个小技巧如果你怀疑某个约束影响了功耗但又不想改代码可以临时通过 debugfs 或者自己写的调试模块把约束值改大观察功耗变化。如果功耗明显下降说明方向对了再去定位具体的请求来源。这个“先验证方向再定位源头”的思路能帮你少走很多弯路。这个框架后续还可以往 per-device 的细粒度控制方向扩展比如结合设备树的电源域信息做更精细的约束管理。不过那是另一个话题了先把全局约束和 CPU idle 这条主线吃透已经能解决大部分实际问题。
返回列表