ARTICLE DETAIL

资讯详情

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

Linux内核PM QoS框架:约束仲裁与功耗调优实战

Linux内核PM QoS框架:约束仲裁与功耗调优实战 1. PM QoS在功耗管理版图里的真实位置如果你在调一个经常被“神秘唤醒”的睡眠问题追着调用栈看了半天最后大概率会撞见这三个字母QoS。我在做嵌入式Linux功耗优化的这几年几乎每个平台都有人问为什么我明明在idleCPU却一直呆在C0不往下走为什么cpufreq明明设置了低档位实际频率却压不下来十个里面有八个翻到最后都是某个驱动往PM QoS里塞了一条约束或者用户空间一个fd没关。这篇是“Linux内核功耗子系统”系列里的第十二篇专门把PM QoS framework从头到尾梳一遍。PM QoS全称Power Management Quality of Service它本身不直接开关任何硬件也不负责省电它只做一件事收集系统里所有“我能容忍多大延迟”“我需要多少吞吐”的诉求算出当前最严格的有效值再通知给cpuidle、cpufreq、runtime PM这些真正做功耗决策的地方。PM QoS的价值在于它把“谁在要求什么”和“系统应该怎么响应”解耦了。在没有这套机制之前驱动通常靠“全局变量覆盖”互相打架你想让DMA延迟低于30us写一个标志位另一个驱动想允许200us又写另一个标志位。最后哪个驱动后跑哪个说了算完全没有可预测性。PM QoS把这种混乱收敛成了一个带聚合规则的“投票箱”每次有人加票、改票、撤票都会重新算出一个有效值并通知所有关心这个值的模块。这套框架分两条线一条是全局PM QoS作用于整个系统比如CPU DMA延迟、网络延迟和网络吞吐另一条是设备PM QoS挂到具体device上比如某个外设的resume latency、latency tolerance、电源关闭标志。前者在kernel/power/qos.c后者在drivers/base/power/qos.c。不管是哪条线核心数据结构都是“请求request”和“约束体constraints”后面我会逐个拆开讲清楚。理解PM QoS的时候建议先把它当成一个“约束仲裁中心”而不是某个具体的功耗模块。它和clock framework、regulator framework、runtime PM都不一样。clock framework管时钟频率开关regulator framework管电压域runtime PM管设备的挂起和恢复而PM QoS是给这些模块提供“输入信号”的你该进多深的idle你该跑多高的频率你该在多长时间内resume都要先看看PM QoS当前的有效约束值是多少。2. 核心对象pm_qos_request与pm_qos_constraints2.1 驱动侧的最小单元pm_qos_request在5.x/6.1主线里全局PM QoS请求的基本结构是struct pm_qos_request。正常情况下驱动不会自己去操作这个结构体的内部成员而是声明一个静态变量然后通过API注册进去#include linux/pm_qos.h static struct pm_qos_request g_fb_latency_req; static int fb_probe(struct platform_device *pdev) { pm_qos_add_request(g_fb_latency_req, PM_QOS_CPU_DMA_LATENCY, 100); return 0; } static void fb_remove(struct platform_device *pdev) { pm_qos_remove_request(g_fb_latency_req); }pm_qos_request内部大致是这样struct pm_qos_request { struct plist_node node; int value; enum pm_qos_class class; };value就是你投出的约束数值class指明这是哪一类约束。plist_node是它挂在约束链表上的节点。enum pm_qos_class在主线里目前包含enum pm_qos_class { PM_QOS_CPU_DMA_LATENCY 0, PM_QOS_NETWORK_LATENCY, PM_QOS_NETWORK_THROUGHPUT, PM_QOS_NUM_CLASSES };不同类别的语义不同CPU DMA延迟和网络延迟是“最大可容忍延迟”数值越小表示要求越严格网络吞吐是“最小需要带宽”数值越大表示要求越高。为什么后面这句很关键因为聚合方向就藏在“最小值还是最大值”里很多新手在这里写反过。2.2 约束体pm_qos_constraints每个全局PM QoS类对应一个pm_qos_constraints约束体它负责维护同一类下的所有请求。结构体大致包含这些成员struct pm_qos_constraints { struct plist_head list; /* 当前所有请求按数值排序 */ int target_value; /* 当前生效的聚合值 */ int default_value; /* 无请求时的默认值 */ enum pm_qos_type type; /* PM_QOS_MIN 或 PM_QOS_MAX */ struct blocking_notifier_head *notifiers; /* 数值变化时通知谁 */ };target_value就是当前系统实际采纳的约束值所有决策方通过pm_qos_request(PM_QOS_CPU_DMA_LATENCY)这类接口读到的一般就是它。当约束链表清空时target_value会退回default_value。这里要注意一个反直觉的点PM_QOS_MIN和PM_QOS_MAX描述的是“聚合算子”不是业务语义。以主线代码为例CPU DMA延迟这一类的type是PM_QOS_MIN意思是“在所有请求值里取最小值”作为有效约束。读者可以这么理解大家投的是“我最多能容忍的延迟”如果一个人能容忍100us另一个人只能容忍30us那系统必须按更严格的那个30us来执行所以取最小值。网络吞吐则反过来多个驱动都要求最小带宽为了全部满足系统必须取所有请求值的最大值。2.3 设备PM QoS的数据组织设备PM QoS不直接用pm_qos_constraints而是在drivers/base/power/qos.c里维护了一套per-device的数据。每个struct device的power.qos指针指向一个struct dev_pm_qos里面保存了resume_latency、latency_tolerance和flags三组约束。设备级请求结构体类似struct dev_pm_qos_request { enum dev_pm_qos_req_type type; /* RESUME_LATENCY / LATENCY_TOLERANCE / FLAGS */ struct list_head node; s32 data; struct device *dev; };DEV_PM_QOS_RESUME_LATENCY是最常见的设备约束驱动的写法通常是struct dev_pm_qos_request *qos_req; dev_pm_qos_add_request(qos_req, dev, DEV_PM_QOS_RESUME_LATENCY, 100);设备PM QoS还支持FLAGS类型的约束例如DEV_PM_QOS_FLAG_NO_POWER_OFF意思是只要有至少一个请求设置了该标志设备就不允许完全断电。这类标志的聚合规则和数值类不同本质是“或”的关系有一票赞成就不关。2.4 为什么用plist而不是普通链表plist的名字很直白priority list优先链表。和普通链表相比它保证节点按prio值有序排列。PM QoS需要频繁做“取当前最小/最大值”的操作plist可以让这个操作变成O(1)的插队读取而不是每次遍历整条链表。请求数量一般不多但cpuidle、cpufreq这些模块可能在热路径上反复读约束值用plist的收益主要不在单次查找而在“每次请求变更后重新采值时稳定、可预期的代价”。真正的核心在于约束体的聚合值不是每次读取时临时算出来的而是在请求插入、更新、删除时同步维护。target_value就是一个“缓存住的聚合结果”消费者在任意时刻读到的都是上次更新后的有效值。3. 一个请求的一生add / update / remove全流程3.1 添加请求时发生了什么全局PM QoS的添加入口是pm_qos_add_request。它内部会先检查请求还没有挂到链表上然后调用核心函数pm_qos_update_targetpm_qos_update_target(pm_qos_constraints[qos], req-node, PM_QOS_ADD_REQ, value);pm_qos_update_target是这套框架的中枢函数。它会做以下几件事根据node找到当前请求在plist里的位置。PM_QOS_ADD_REQ直接把节点按新值插入plist。重新计算约束体的聚合值链表为空取default_value链表非空且type为PM_QOS_MIN取plist中的最小值链表非空且type为PM_QOS_MAX取plist中的最大值。如果计算出的当前值和旧target_value不同更新target_value然后调用阻塞通知链通知所有注册了该类的消费者。返回旧值。设备PM QoS的dev_pm_qos_add_request逻辑类似但多了两步一是如果dev-power.qos还没有分配先动态分配struct dev_pm_qos二是把请求挂到对应约束的子链表里。正是这个“动态分配”导致了一个很经典的坑我放到后面讲。3.2 更新请求大部分驱动的日常操作运行过程中约束条件往往会变化。比如显示驱动在画面静止时不需要低延迟但一触发触摸就要立刻把延迟约束收紧。此时调用的是pm_qos_update_requestpm_qos_update_request(g_fb_latency_req, 30);更新和添加的区别在于动作是PM_QOS_UPDATE_REQ。pm_qos_update_target会先把旧节点从plist里摘下来再用新值重新插入随后走同样的“重新计算聚合值、比较、触发通知”流程。值得留意的是如果新值和旧值一样框架通常会直接返回不触发通知。这看起来是优化但在调试时容易让人困惑你明明调用了pm_qos_update_requesttrace里却看不到通知事件原因多半是“值没变”。3.3 删除请求释放约束驱动卸载或不再需要低延迟时pm_qos_remove_request(g_fb_latency_req);这个动作对应PM_QOS_REMOVE_REQ框架会把节点从plist删除然后重新计算聚合。删除后如果链表为空约束值会回落到default_value。设备级删除也是同样的套路但设备PM QoS在dev-power.qos已经被分配出来后删除最后一个请求并不会自动释放整个dev_pm_qos结构除非你显式调用dev_pm_qos_constraints_destroy一类的接口。也就是说即使所有请求都撤了dev-power.qos可能还在这是为了减少“每次首请求都要重新分配”的震动。3.4 锁与上下文限制全局PM QoS内部有互斥锁保护更新过程是可睡眠的。设备PM QoS同样使用mutex而且首次添加请求时可能触发kzalloc分配内存所以绝对不能在原子上下文、中断处理函数、spinlock临界区里做“第一次”dev_pm_qos_add_request。比较安全的做法是在驱动的probe阶段先把请求对象初始化并添加一次后续只做update和remove。如果驱动必须在中断里改约束最好通过workqueue延后到进程上下文再更新。很多“system_server watchdog”或者“sleeping function called from invalid context”的崩溃日志追一下栈就能看到dev_pm_qos_add_request在原子路径里躺枪。3.5 通知链消费者如何感知变化消费者不是每次都用轮询读target_value的它们通过“阻塞通知链”订阅约束变化。例如static int qos_notifier_call(struct notifier_block *nb, unsigned long val, void *ptr) { /* 这里拿到变化前后的值做状态刷新即可 */ return NOTIFY_OK; } static struct notifier_block qos_nb { .notifier_call qos_notifier_call, }; pm_qos_add_notifier(PM_QOS_CPU_DMA_LATENCY, qos_nb);因为PM QoS使用阻塞通知链回调是在进程上下文里执行的所以回调里也别做睡眠以外的重活更不能随便拿自旋锁。回调里最常见的错误是又一次调用PM QoS API导致嵌套更新、递归通知轻则多打日志重则造成锁序问题。4. 约束被谁消费cpuidle、cpufreq与运行时PM4.1 cpuidleCPU DMA延迟决定你沉多深全局PM QoS里最经典也最容易被误解的是PM_QOS_CPU_DMA_LATENCY。cpuidle的不同C状态有不同退出延迟比如C0无延迟、C1大约10us、C2约50us、C3约150us。当某个驱动或用户空间程序要求CPU DMA延迟不超过50uscpuidle governor就必须把所有退出延迟大于50us的深C状态过滤掉。所以如果你在系统里看到CPU“永远不深睡”可以先怀疑是不是有谁长期持有了一个小延迟约束。这类约束不一定来自驱动也可能来自用户空间——只要有人open了/dev/cpu_dma_latency并写入了很小的值内核就认为当前存在低延迟诉求。menu governor里对这个约束的消费方式大致是每次选择idle状态时拿一下pm_qos_request(PM_QOS_CPU_DMA_LATENCY)再用这个值去过滤候选状态。这里的数值方向很容易理解延迟约束值越小越严格过滤掉的状态越多系统越“睡不着”。4.2 cpufreq约束也会影响频率决策相比cpuidlecpufreq对PM QoS的消费弱一些但确实存在。PM_QOS_CPU_DMA_LATENCY如果被卡得很死某些平台的cpufreq驱动会限制切换频率的档位和节奏因为频率切换本身有延迟如果系统要求极低延迟频繁变频带来的调度抖动不可接受。更常见的倒是设备级PM QoS在影响带宽和电源状态。比如一些modem、网卡会请求PM_QOS_NETWORK_THROUGHPUT要求系统保证最低吞吐此时cpufreq、bus frequency governor会倾向抬高频点避免因为降频导致吞吐不达标。这个机制和音视频场景里“要稳帧率就锁频”是同一个道理只不过PM QoS把它形式化了任何模块都可以投票系统在最大值/最小值规则下自动满足最严要求。4.3 设备resume latencyaudio/video/PCIe的最爱设备PM QoS最常被消费的字段是DEV_PM_QOS_RESUME_LATENCY。一个codec如果退出低功耗需要80ms而音频驱动要求播放启动延迟不超过20ms那这个codec在播放期间就不能进入那种“需要80ms才能恢复”的低功耗状态。嵌入式平台里常见做法是音频驱动在播放前dev_pm_qos_add_request请求很小的resume latency播放结束后dev_pm_qos_update_request把值调大或直接移除让设备能重新回到深低功耗。如果驱动在播放结束时忘了更新或移除约束功耗就会一直异常我带过的项目里至少有三四次“待机掉电快”的问题最后都定位在这里。系统suspend/resume路径同样会读取设备PM QoS。某些电源域或总线控制器在准备进入低功耗前会先检查下游所有设备的resume latency约束如果当前有效值要求太严格就放弃关电避免“suspend时省了电resume时因为延迟超时被app投诉”。4.4 用户空间投票/dev/cpu_dma_latency与/dev/network_latencyPM QoS不只服务内核驱动也向用户空间开放。内核里注册了misc设备最常见的有/dev/cpu_dma_latency、/dev/network_latency。用户空间通过一个极简协议投票int fd open(/dev/cpu_dma_latency, O_WRONLY); int value 20; write(fd, value, sizeof(value)); /* 保持fd不关约束就一直生效 */ close(fd); /* 关闭即删除约束 */这个模式非常适合“临时锁定低延迟”的场景比如音视频播放器在播放期间open并写入小值退出时close。Android平台上很多功耗异常排查最后都是在/proc/pid/fd里发现某个进程一直握着/dev/cpu_dma_latency。内核这样设计的好处是进程即使崩溃fd也会被内核自动关闭约束自动撤销不会留下永久“卡死”。5. 调试PM QoStracepoint、sysfs与实战方法5.1 先用tracepoint把“谁在哪改了值”找出来主线内核为PM QoS提供了trace事件一般在/sys/kernel/tracing/events/power/下能看到pm_qos_request、pm_qos_update_request、pm_qos_update_target设备级还有dev_pm_qos_add_request、dev_pm_qos_update_request等。不同内核版本事件名略有出入先确认一下ls /sys/kernel/tracing/events/power/ | grep pm_qos临时开启echo 1 /sys/kernel/tracing/events/power/pm_qos_update_target/enable cat /sys/kernel/tracing/trace如果想看完整的变更过程推荐用trace-cmdtrace-cmd record -e power:pm_qos_* -e power:dev_pm_qos_* sleep 10 trace-cmd reportpm_qos_update_target事件的输出里通常包含约束类、动作ADD/UPDATE/REMOVE、变化前值和变化后值。通过prev_value到curr_value的跳变你能很快判断出是谁的一次更新让系统失去了深idle状态。5.2 设备级sysfs节点看当前生效值每个支持runtime PM的设备在/sys/devices/.../power/下可能带PM QoS节点比如pm_qos_resume_latency_us、pm_qos_latency_tolerance_us、pm_qos_no_power_off。直接读这些节点能看到当前设备生效的约束值cat /sys/devices/platform/soc/xxx/power/pm_qos_resume_latency_us往pm_qos_resume_latency_us写一个值相当于从用户空间给这个设备塞了一条resume latency约束。这在A/B测试中很好用先写一个很大的值再看设备行为有没有变化再写一个很小的值对比功耗和延迟。如果设备没有对应节点可能是因为设备没有初始化PM QoS数据或者内核配置没带CONFIG_PM_QOS这个宏一般在suspend/runtime PM相关配置下会打开。5.3 定位“哪个驱动在投票”从plist反推tracepoint能告诉你数值变了但不一定直接告诉你“是哪个驱动”。因为全局struct pm_qos_request里没有name字段老内核里曾经有过“具名请求”的API新框架为了省开销把这个去掉了。想反推可以分两步第一步在trace里看变化时刻的调用栈trace-cmd report -R或者带function trace能打出调用者第二步用crash工具或者kgdb直接dump对应类的plistcrash struct pm_qos_constraints pm_qos_constraints[0] crash list -H pm_qos_constraints[0].list 2 /dev/null从plist_node.prio能看到每个请求的数值再用container_of把plist_node换算回struct pm_qos_request就能拿到是哪个static变量。但这个方法对没有调试符号的现场环境不友好实操中我更常用“代码评审临时notifier打印”在怀疑的驱动里注册一个notifier或直接在pm_qos_update_target里临时加一行trace看到目标值变化时配合dump_stack()调用链立刻就很清楚了。5.4 一套可复用的检查流程如果系统“睡不下去”或“频率压不下来”我通常按这个顺序查看当前有效约束值全局的直接在trace里看target_value设备级的读sysfs节点。确认有效值是不是落在了“异常严格”区间比如CPU DMA延迟常年小于10us那基本可以断定有人锁了浅idle。打开pm_qos_update_target事件复测复现一次记住最后一次prev_value-curr_value变化的时间和调用栈。从调用栈找到驱动检查它是否在退出路径撤销了请求。如果栈里是用户空间入口就查fdls -l /proc/*/fd 2/dev/null | grep cpu_dma_latency。这个方法救过我很多次比一上来就扫全系统代码高效得多。6. 移植和调优中我踩过的几个坑6.1 第一个坑设备级Add Request不能在原子上下文首呼前文提过dev_pm_qos_add_request首次调用可能会为设备分配dev_pm_qos内存。我曾在一个USB控制器驱动里把添加逻辑放在中断返回路径上结果偶发“BUG: sleeping function called from invalid context at mm/slub.c”。这是因为第一次调用走了kzalloc而中断上下文不允许睡眠。解决方法是把请求对象放到probe阶段初始化中断里只更新数值或者用schedule_work推到进程上下文。6.2 第二个坑聚合方向和业务语义看反有人会下意识认为“PM_QOS_MIN就是最小业务需求”于是给网络吞吐这种“需要保证最小带宽”的约束也配了PM_QOS_MIN结果系统把所有请求里的最小值当成有效约束某个要求很低的请求就把整体带宽约束拉下去了。这就是“聚合算子”和“业务语义”混淆的典型事故。我列个表方便记忆约束聚合类型“更严格”的方向PM_QOS_CPU_DMA_LATENCYPM_QOS_MIN数值更小PM_QOS_NETWORK_LATENCYPM_QOS_MIN数值更小PM_QOS_NETWORK_THROUGHPUTPM_QOS_MAX数值更大DEV_PM_QOS_RESUME_LATENCYPM_QOS_MIN数值更小使用pm_qos_constraints时先确认这个类到底是“上限不能超”还是“下限不能低于”再决定聚合方向。不同内核版本里宏命名和初始化可能会有变化移植时务必打开源码确认一遍不要照抄老平台。6.3 第三个坑notifier回调里做重活PM QoS通知链是同步执行的一旦target_value变化所有订阅者的回调会在pm_qos_update_target的调用路径上立刻执行。如果在回调里又去更新别的PM QoS请求、或者拿全局锁、再做耗时的频率切换非常容易造成“通知风暴”严重时直接拖慢系统。我的习惯是回调里只记录一个flag或者把变化值写进一个cache然后通过schedule_work触发真正的策略更新把重活挪出通知链。6.4 第四个坑用户空间fd泄漏会让你睡不下去移动平台上很多多媒体服务喜欢长期open/dev/cpu_dma_latency。如果服务没有正确管理fd或者出现异常退出但阻塞式持有fd的情况CPU会一直处在高等级唤醒状态整机功耗异常。排查时不要只看内核驱动用户空间的lsof或者/proc下的fd列表也要查。反过来也有一种情况一个进程希望保持低延迟但被LMK杀掉后fd关闭约束消失系统突然就能进入深idle了行为“时好时坏”这种最容易让人误判为硬件问题。6.5 第五个坑老版本API别硬搬2.6/3.x时代的内核里PM QoS接口和现在差别非常大那时流行pm_qos_add_requirement(int class, char *name, s32 value)一类的“具名请求”用字符串name来标识请求。4.x以后改成面向对象的struct pm_qos_request不再有name字段。如果你从老BSP里直接把代码搬过来编译失败还算好的最怕的是接口名字相同、内部语义变了比如某个老接口更新时会为每次调用动态分配内存新内核里却需要你自己负责生命周期。移植PM QoS相关驱动时请直接对照目标版本的include/linux/pm_qos.h和kernel/power/qos.c不要相信网上随便搜来的老代码。如果真想给每个请求加调试标识我建议维护一个自己的“request name到指针”的映射表在add请求时登记remove时注销这样既能保留可读性又不依赖内核版本。以上这些坑基本覆盖了我在不同SoC平台上调试PM QoS时遇到的大部分问题。每次排查到最后我都会跟团队强调一句PM QoS代码量不大但它牵一发动全身任何一条不起眼的约束都可能影响整机功耗和性能。把它的数据流、通知机制和聚合方向彻底弄懂比背多少API都重要。
返回列表