
最早做功耗调优的时候我其实很不理解内核里为什么要把“温度管理”做成这么一大套框架。当时手里只有一块板子、一个热敏电阻和一个小风扇直觉上觉得加个阈值判断、超了温度就降频不就行了直到后来设备从单片机换到跑 Linux 的应用处理器从单纯的风扇散热变成调频调压、任务调度限制、热侵入提示一起上才意识到这绝对不是一个if (temp 85) 降频能解决的问题。这篇是 Linux 内核功耗子系统的第九篇我们把 thermal framework 的通用架构完整梳理一遍讲清楚它由哪些模块组成、数据怎么流转、治理器怎么决策、设备树怎么配置以及在调试中容易踩的坑。这套框架适合谁看做嵌入式 Linux 的 BSP 工程师、搞手机/平板功耗调优的系统软件开发者、以及想理解内核怎么统一管理“发热”这件事的底层爱好者。读完你至少能回答三个问题thermal zone、cooling device、governor 分别是什么角色温度从传感器读到之后在内核里走了一条什么样的路以及板子发热异常时该去哪个 sysfs 节点查数据、改哪个配置能快速定位问题。1. thermal framework 在设计上到底要解决什么1.1 它处在功耗管理链路的什么位置Linux 的功耗管理不是单一模块而是一套组合拳cpufreq 管 CPU 频率、cpuidle 管 idle 状态、devfreq 管设备频率、regulator 管电压、runtime PM 管设备启停这些模块负责“少干活、多省电”。thermal framework 不一样它不负责省电它负责守住温度的底线——当芯片因为高负载发热、或者散热条件恶化时thermal 是那个“踩刹车”的角色。所以看功耗管理链路时可以把 thermal 理解成一个后置的约束系统前面那些模块在尽力满足性能需求thermal 在后台实时盯温度一旦越过预设的安全线它就强制拉低性能防止硬件损坏或者用户体验崩坏。这也是为什么在移动 SoC 上thermal 的治理结果经常表现为“打游戏时帧率骤降”“快充时功率被砍”本质上都是温度约束在起作用。1.2 为什么不能用简单的阈值判断实现有人觉得温度到了 85 度就关掉风扇或者降频到了 100 度就强制复位不就完了现实情况要复杂得多。第一温度源多种多样。有 SoC 内部的热敏二极管有主板上的 NTC 热敏电阻有电源管理芯片的片上传感器每一种的读数方式、精度、更新频率都不同。它们分布在不同的地址空间由不同的驱动管理需要一个统一抽象来容纳这些差异。第二冷却手段也多。CPU 可以降频、可以调电压、可以把任务迁移到低温核心GPU 可以限制频率风扇可以多档调速充电可以限流。这些手段的“冷却强度”各不相同而且有些是渐进的、有些是突变的统一抽象之后才能让上层策略用一个标准方式操作它们。第三温度变化有惯性。芯片热起来不是瞬时的降温也不是立即生效的风扇转起来之后温度可能还在惯性上升简单阈值判断很容易出现“温度在目标点来回横跳”的震荡。我见过一个没有 hysteresis迟滞配置的板子60 度启动风扇、降到 58 度关风扇结果在 55~65 度之间持续进出风扇像得了帕金森一样抽风。这就是只做阈值判断的典型后果所以 thermal framework 才把“趋势判断”和“降温曲线”做进了治理器逻辑里。1.3 三层模型感知、决策、执行各管一段thermal framework 通用架构可以用一个三层模型来概括感知层由thermal_zone_device和底层传感器驱动构成负责回答“现在温度是多少”。决策层是thermal_governor负责回答“温度到了这个程度该让哪些设备出力、出多少力”。执行层由thermal_cooling_device构成负责回答“把风扇转速提到几档、把 CPU 频率限制到多少”。这个分层和现实世界的空调温控系统非常像。温度计是传感器温度旋钮是 trip空调主板上的控制逻辑是 governor压缩机和风机是 cooling device。你调温度旋钮控制逻辑决定压缩机转多快风机吹多大风最后温度稳定在你设定的区间里。Linux 的 thermal 框架本质上就是这么一套系统的软件化。分层的好处不用多说硬件变化可以隔离在驱动层策略变化隔离在 governor 层业务组合靠设备树描述。比如同一颗 SoC 可以用在手机上风扇都没有全指望降频也可以用在开发板上外挂一个 PWM 风扇只要设备树和 governor 配置不同内核代码几乎不用改。2. 核心数据结构与关键概念梳理2.1 thermal_zone_device热区是温度管理的“管辖范围”thermal_zone_device是整个框架最核心的对象一个热区代表一个可以被管理温度的区域。这个区域可以是一颗 CPU、一片 GPU、一块电池甚至整个机箱内部空间取决于你怎么划分。每个热区对应 sysfs 里的一个thermal_zoneX目录节点里有type、temp、mode、policy等属性。type是热区的名字比如cpu0-thermaltemp是当前温度单位毫摄氏度mode是 enable/disabled控制这个热区是否参与治理policy是当前使用的 governor 名称。热区需要实现温度读取的 ops主要包括get_temp。底层驱动通过这个回调把原始读数转换成毫摄氏度上报。不同传感器的读取方式差别非常大片上传感器可能直接读寄存器就行NTC 热敏电阻要从 ADC 通道读电压再做查表换算这些细节都被封装在驱动里对热区上层透明。static int my_sensor_get_temp(struct thermal_zone_device *tz, int *temp) { /* 读取原始值并换算成毫摄氏度 */ *temp read_sensor_raw() * 1000; return 0; } static struct thermal_zone_device_ops my_sensor_ops { .get_temp my_sensor_get_temp, };值得注意的一个点是一个热区可以注册多个 trip每个 trip 都有独立的温度阈值和类型。热区维护一个当前温度值当温度上报进来时框架会把新温度和所有 trip 做比较再根据结果触发治理器。2.2 thermal_trip触发点才是业务规则所在thermal_trip描述“温度达到什么程度时该做什么”。一个热区可以有一串 trip每个 trip 有温度值、迟滞值、类型和可选的与冷却设备的绑定关系。trip 类型在较新内核里用枚举表示THERMAL_TRIP_ACTIVE对应主动冷却适合风扇这类需要明确开关的设备。THERMAL_TRIP_PASSIVE对应被动冷却适合调频调压这类渐进式降温手段。THERMAL_TRIP_HOT表示热干预点到达后系统要做更激进的降温动作。THERMAL_TRIP_CRITICAL表示临界点到了通常直接触发关机保护。trip 的迟滞值非常关键它决定了温度回落到多少度才算“解除警报”。比如报警点是 85 度、迟滞 2 度那么温度要降到 83 度以下才会解除这个 trip 的触发状态这是防止震荡的核心设计。在 sysfs 里trip 以trip_point_0_temp、trip_point_0_type这样的节点暴露trip_point_0_temp的单位同样是毫摄氏度。用户空间可以通过修改这些节点来动态调整阈值但实际产品里更常见的做法是在设备树里写死运行时只做读取。2.3 thermal_cooling_device降温手段的抽象thermal_cooling_device把各种降温手段统一成“冷却状态”这个抽象概念。状态从 0 到max_state0 表示不干预max_state表示最大冷却强度。具体每个状态对应什么动作完全由冷却设备驱动自己定义。比如 CPU 冷却设备的每个状态对应一个频率档位状态越高允许的最高频率越低风扇冷却设备的每个状态对应一个 PWM 占空比档位GPU 冷却设备则对应不同频率和电压组合。冷却设备的 ops 有三个关键回调get_max_state返回最大冷却状态。get_cur_state返回当前冷却状态。set_cur_state设置目标冷却状态。static int fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) { *state FAN_MAX_LEVEL; return 0; } static int fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) { pwm_set_duty(fan_pwm, state * PWM_STEP); return 0; } static struct thermal_cooling_device_ops fan_cooling_ops { .get_max_state fan_get_max_state, .get_cur_state fan_get_cur_state, .set_cur_state fan_set_cur_state, };每种冷却设备都对应 sysfs 里一个cooling_deviceX目录里面有type、max_state、cur_state。我调试时经常手动写cur_state来单测冷却设备本身是否工作正常这比看整个链路简单得多。2.4 thermal_governor做决策的“大脑”governor 是 thermal framework 的策略核心它决定“当前温度下要把哪些冷却设备调到什么状态”。内核里常见的 governor 有step_wise、power_allocator、bang_bang、user_space。step_wise是阶跃式算法根据温度趋势逐级调整冷却等级是嵌入式 Linux 上最常见的默认选择。power_allocator也叫 IPA是智能功率分配算法基于温度偏差计算允许功耗预算在移动 SoC 上用得很多。bang_bang是简单的开关式控制适合风扇这种有响应的主动冷却设备。user_space把决策权完全交给用户空间的 thermal daemon。sysfs 里热区的policy属性可以查看和切换当前使用的 governor前提是目标 governor 已经通过内核配置编译进来。这个切换能力在实际调试中非常好用我可以先切到user_space手动验证冷却设备再切回step_wise验证自动治理链路。3. 一次完整的温度调节是怎么走完的3.1 温度上报与 zone update 的时机要理解整条链路最好的方式就是跟踪一次温度变化从发生到冷却设备动作的完整路径。底层传感器驱动拿到新温度后调用thermal_zone_device_update通知框架温度变化。这个函数会做几件事更新热区当前的温度值、检查所有 trip 是否被触发、然后调用当前 governor 的 throttle 回调进行决策。温度上报的时机有两种典型方式。轮询模式是通过热区配置里的polling-delay和polling-delay-passive参数由内核定时器周期触发读温度中断模式则是传感器在温度越过阈值时主动上报中断框架再去读取温度并更新。中断方案响应快但实现复杂老一点的内核和简单传感器驱动大多走轮询。轮询间隔的设置在设备树里就能配polling-delay是正常运行时的采样间隔polling-delay-passive是进入被动冷却后的采样间隔。被动状态下温度变化更敏感所以间隔通常更短。3.2 governor 决策逻辑以 step_wise 为例step_wise的设计思想是“每次只调整一级防止过冲”。它的输入是当前温度、所有 trip 的触发状态、以及趋势方向输出是每个冷却设备的目标状态。趋势方向的判断是核心。框架会比较本次温度值和上次温度值得出温度是在上升、下降还是平稳。如果温度上升step_wise会逐步调高冷却等级一次加一级如果温度下降则逐步降低冷却等级也是一次减一级。这个机制天然带缓冲不会因为短暂的温度毛刺导致冷却等级剧烈抖动。为了把这个讲明白举个具体的例子。假设一个热区有两个 trip80 度的 passive trip 和 95 度的 critical trip。CPU 冷却设备的档位是 0~4。当温度从 76 度涨到 82 度时step_wise发现越过了第一个 trip 且趋势是上升于是把冷却状态从 0 调到 1。温度继续涨到 84 度它再把状态调到 2。如果这时温度开始回落它不会立刻全部关闭而是先降到 1等到跌回 80 度以下才可能降回 0。这种逐级增减的方式很符合散热系统的物理特性。冷却动作本身有滞后性一次压满容易让温度快速跌穿目标线随后又快速反弹形成低频振荡。所以很多产品里宁可让温控曲线“钝”一点也不要整成高灵敏度的锯齿波。power_allocator的逻辑就完全不同了它更像一个控制器把温度偏差映射成功率预算再通过冷却设备把预算“分配”给各个耗电单元。这套算法适合有多核 CPU、GPU、NPU 同时参与的复杂场景能精细地决定“在这个的温度下总共能花多少瓦、谁分多少瓦”手机上的性能调度基本都离不开它。3.3 冷却请求如何作用到具体设备governor 决策完之后框架会把目标冷却状态写到对应的 cooling device 上。冷却设备的set_cur_state被调用实际降温动作发生。以cpufreq_cooling为例这是最常用的 CPU 冷却实现。它本身不直接改频率而是通过内核的 frequency QoS 机制添加一个频率上限约束。QoS 是一种请求约束框架多个子系统可以同时提出自己的频率上限最终生效的上限是所有请求里最小的那个。这意味着 thermal 的降频请求不是“一刀切”而是和其他模块的约束做协同。用户可能设置了性能模式要求最低频率不能低于某值thermal 再叠加一个上限两边取交集来满足各自需求。这个机制让 thermal 和用户空间的性能策略能共存而不是互相覆盖。风扇冷却设备则简单直白set_cur_state收到状态值后直接换算成 PWM 占空比然后写寄存器控制风扇转速。档位多的时候需要考虑风扇启停的最小占空比避免低速时驱动不足导致风扇不转。4. 设备树配置与驱动接入的实操套路4.1 device tree 中 thermal-zones 的写法在设备树里配置热区是嵌入式开发中最常见的接入方式。整个thermal-zones节点下面是各个子节点每个子节点定义一个热区。下面是一个典型的 CPU 热区配置thermal-zones { cpu0-thermal { polling-delay-passive 250; polling-delay 1000; trips { cpu_alert: cpu-alert { temperature 85000; hysteresis 2000; type passive; }; cpu_crit: cpu-crit { temperature 105000; hysteresis 0; type critical; }; }; cooling-maps { map0 { trip cpu_alert; cooling-device cpu0_cdev THERMAL_NO_LIMIT THERMAL_NO_LIMIT; }; }; }; };polling-delay-passive单位是毫秒表示进入被动冷却后的轮询间隔这里配的 250ms 意味着被动模式下每 250ms 读一次温度。polling-delay是正常模式的间隔。trip 里的temperature单位是毫摄氏度hysteresis也是毫摄氏度。temperature 85000就是 85 度hysteresis 2000是 2 度迟滞。cooling-maps是热区和冷却设备的绑定关系一个 trip 可以绑定多个冷却设备一个冷却设备也可以出现在多个 map 里。这里的THERMAL_NO_LIMIT表示冷却状态范围用满也就是不限制最小和最大值。实际中也可以写成具体的状态值来限制冷却深度比如fan0 2 5表示风扇冷却状态只在 2 到 5 档之间工作。我在实际项目里踩过一个坑在cooling-maps里引用 trip 时引用的 phandle 和被映射的冷却设备必须都已经在设备树里定义好否则解析会失败。如果树上有孤儿引用现象往往是热区注册了但没有任何冷却设备绑定成功温度飙到临界点也不触发降温。4.2 驱动侧注册 thermal zone 的代码路径设备树描述了热区的拓扑但真正把热区对象注册进内核、让框架开始工作的是驱动代码。传统方式是用thermal_zone_device_register手动注册传入 ops、trip 描述等参数。以老 API 为例大概是这样一个流程static struct thermal_zone_device_ops soc_thermal_ops { .get_temp soc_get_temp, .get_trip_type soc_get_trip_type, .get_trip_temp soc_get_trip_temp, }; struct thermal_zone_device *tz; tz thermal_zone_device_register(soc, soc_thermal_ops, NULL, 0, NULL, soc_trips, num_trips, 0);新内核在设备树路径上提供了更便捷的封装可以直接从 DT 节点解析 trip 和冷却映射信息驱动只需要提供温度读取的 ops 就可以了trip 的配置全部走设备树维护。这样代码改动更小热区的拓扑调整也不需要重新编译驱动。注册热区以后内核会自动创建对应的 sysfs 节点。你立刻能在/sys/class/thermal/下看到新的thermal_zoneX目录cat temp能读温度cat trip_point_*_temp能看到设备树里配置的阈值。4.3 冷却设备注册与绑定关系冷却设备通常由对应的驱动模块注册。CPU 冷却设备在 cpufreq 驱动初始化阶段注册GPU 冷却设备在 gpu 驱动里注册风扇冷却设备在 pwm-fan 驱动里注册。拿 CPU 为例cpufreq 驱动通过cpufreq_cooling_register注册冷却设备创建出来的 cdev 会在 sysfs 里出现名字一般是cpufreq。注册完成之前设备树里对 cpu 冷却设备的引用是无意义的这也是为什么先要有驱动加载、再有冷却设备绑定。手动注册一个自定义冷却设备也不复杂static struct thermal_cooling_device *cdev; cdev thermal_cooling_device_register(my-cooler, my_data, my_cooling_ops); if (IS_ERR(cdev)) { /* 处理注册失败 */ }绑定关系我前面说过由热区的cooling-maps决定。框架在解析热区节点时会找到 map 中引用的 cooling device phandle然后查找已经注册的同名 cdev建立关联。如果 cdev 还没注册这个 map 暂时不生效要等 cdev 注册完成后重新扫描才会绑定成功。这种动态绑定机制对驱动加载顺序有一定的容忍度但依赖框架的重扫逻辑调试时还是要留意加载次序。5. 调试方法与常见问题排查实录5.1 现场调试三板斧sysfs、tracepoint、日志调试 thermal 问题第一步永远是看一眼 sysfs 里的现状。# 查看所有热区和当前温度 cat /sys/class/thermal/thermal_zone*/type cat /sys/class/thermal/thermal_zone*/temp # 查看某个热区的 trip 配置 cat /sys/class/thermal/thermal_zone0/trip_point_*_temp cat /sys/class/thermal/thermal_zone0/trip_point_*_type # 查看冷却设备的档位和当前状态 cat /sys/class/thermal/cooling_device0/type cat /sys/class/thermal/cooling_device0/max_state cat /sys/class/thermal/cooling_device0/cur_state温度是毫摄氏度所以要除以 1000 才是摄氏度。第一次看的时候很多人被这个坑到以为板子温度是 85000 度。policy节点可以切换治理器# 查看当前使用的 governor cat /sys/class/thermal/thermal_zone0/policy # 切换到 step_wise echo step_wise /sys/class/thermal/thermal_zone0/policy手动测试冷却设备时记得先确认热区模式。如果热区处于 enabled 自动治理状态governor 会一直在后台调整cur_state你手写的值可能马上被覆盖。想手动验证时可以先写disabled到mode再操作cur_stateecho disabled /sys/class/thermal/thermal_zone0/mode echo 2 /sys/class/thermal/cooling_device0/cur_state内核还提供了 thermal 相关的 tracepoint在/sys/kernel/tracing/events/thermal/下可以找到。用 trace-cmd 或者直接往 tracefs 里写echo 1打开对应事件就能看到温度上报、trip 触发和冷却设备状态变化的实时记录。这比看日志直观得多尤其适合抓“温度震荡”“冷却不触发”这类动态问题。5.2 温度异常、冷却不生效、governor 切换失败的排错思路先说温度异常。如果temp一直是 0 或者某个固定值多半是传感器读取链路有问题。先看get_temp有没有被调用可以在驱动里加日志或者看 sensor 的 ADC 通道配置是不是错了。NTC 类型的传感器尤其要注意查表函数的分段边界我曾经见过一次查表代码在低温段返回了错误值导致 30 度以下的温度全部读到同一个负数治理器直接判定异常。如果温度有值但到了阈值不触发降温第一怀疑对象是 trip 配置。检查trip_point_*_temp和trip_point_*_type跟设备树是否一致确认温度单位有没有搞错。第二怀疑对象是 governor 没有正确工作切到step_wise或者bang_bang试试有时候是默认 governor 没被编译进内核。冷却设备不工作的情况先看 sysfs 里max_state和cur_state。如果max_state是 0说明冷却设备本身没注册好。如果cur_state有值但实际没降温效果那就是冷却设备的set_cur_state实现有问题比如 PWM 占空比配置反了、或者频率 QoS 请求被别的模块覆盖了。这类问题只有对着硬件量信号才能定位纯软件角度能做的就是把链路从前往后逐段验证。governor 切换失败报write error最常见的原因是目标 governor 没有编入内核。检查内核配置里有没有CONFIG_THERMAL_GOV_STEP_WISE、CONFIG_THERMAL_GOV_POWER_ALLOCATOR这些项。编译内核的时候经常有人只开了CONFIG_THERMAL忘了把 governor 也选上。5.3 常见问题速查表现象可能原因排查方法温度始终为 0 或固定值传感器驱动读取失败、ADC 配置错误在驱动中打印取到的原始值检查设备树节点温度越阈但不触发降温trip 配置错误、governor 未注册核对 sysfs trip 节点切换 policy 试跑冷却设备无响应cdev 未注册、绑定 map 缺失、QoS 被覆盖确认/sys/class/thermal/下 cdev 存在手动写 cur_state温度震荡、风扇反复启停hysteresis 太小或不配、step_wise 趋势误判调大 hysteresis检查轮询间隔写 policy 失败governor 未编译进内核确认 CONFIG_THERMAL_GOV_* 已开启手动写 cur_state 被还原热区处于 enabled 自动治理模式先写 disabled 到 mode再操作 cur_state热区节点不存在设备树节点解析失败、driver probe 失败查看 dmesg 中 thermal 相关打印还有一些设备树层面的坑让我印象很深。比如cooling-device里的fan0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT写法和cooling-min-state、cooling-max-state属性有时混用不同内核版本对这两种写法的解析优先级还不一样。遇到冷却映射不生效建议把设备树里两种表达检查一遍不要让它们重复定义同一个限制。6. 这个框架后续演进与我的几点体会thermal framework 在内核 6.1 之后经历了一次比较大的重构trip 从原先的“驱动自己管理一组 temp/type 属性”变成了统一的struct thermal_trip数组类型也枚举化了。新框架下驱动侧代码清爽了一些设备树到内核配置的路径更加统一。如果你手头是 5.x 的老内核读代码时要注意这些差异很多网上的示例代码在新内核上不一定能直接编译过。我个人在实际项目中最大的体会是thermal framework 的调试难点从来不在内核代码本身而在于把“温度读数 - 业务温度点 - 冷却手段”这三者之间的对应关系理清楚。很多时候板子发热异常表面上是 thermal 的问题往下一查其实是传感器位置放得不对、测到的温度和芯片实际结温差了十万八千里。再好的 governor 算法喂进去的数据是错的输出自然也是错的。另一个经验是刚开始接一个平台时不要一上来就研究power_allocator这种高级算法先用step_wisebang_bang把链路打通。确认每一条冷却映射都真实生效确认设备树里每个 trip 都符合硬件手册的电气要求再去优化温度曲线和功耗分配。我见过太多人在复杂算法里找 bug最后发现只是冷却设备绑定漏了一条。最后分享一个小技巧内核支持温度模拟你可以通过热区的 sysfs 节点或者内核配置项运行时代入一个假温度用来验证治理逻辑而不用真的拿热风枪吹板子。配合/sys/class/thermal/thermal_zone0/mode的 disabled/enabled 切换你可以非常方便地模拟“温度突然冲到 90 度看 governor 怎么响应”。这套玩法对快速验证策略和复现问题帮助很大算是做 thermal 调优的必备技能了。