Linux Thermal子系统详解:从设备树到内核架构的温控指南
Linux thermal这个名词听起来像是内核里最不起眼的一部分但如果你参与过系统Bring Up或者做过性能调优大概率已经被它支配过一轮了。我们经常会遇到设备跑着跑着温度冲上85度CPU被死死卡在低频率档位屏幕亮度、充电电流也一起被限制。这一系列“奇怪的自动降级”背后基本都是thermal模块在悄悄干活。用大白话讲Linux thermal就是内核里的“恒温器”它通过温度传感器拿到各个发热源的温度一旦越过某个阈值就去调低CPU频率、开风扇、限制充电电流直到温度回到安全区间。这篇文章我会从整体架构、内核代码路径、设备树配置、sysfs调试到真实踩坑排查完整过一遍thermal模块。适合刚接手平台Bring Up的内核工程师、做功耗发热优化的系统工程师以及想把“温控”这件事彻底搞明白的学生。1. 先搞清楚thermal在解决什么问题一条温度反馈闭环1.1 四大核心对象从传感器到散热器的链路thermal子系统不是单独一个驱动而是一整套框架。理解这套框架的关键是四个对象thermal zone温度域。一个zone可以理解为一个“发热区域”它关联了温度传感器、温度阈值点trip point和散热设备cooling device。比如CPU这一个zone传感器可能来自SoC内部的温度计散热设备就是CPUFreq所代表的“频率调节器”。trip point温度阈值点。定义“多少度开始做反应”例如75度开始轻度干预、115度panic。每个trip point有自己的类型active、passive、hot、critical。governor温控策略。决定温度越界之后具体把冷却等级cooling state调到哪一档。常见的有step_wise、fair_share、power_allocator等。cooling device执行机构。是抽象的“散热设备”可以是风扇、CPU调频、GPU降频、充电电流限制。每个cooling device有0到max_state多个冷却档位state越高散热能力越强。这个设计很像家用空调的逻辑房间是thermal zone温度传感器检测到29度超过你设定的26度trip point那么温控策略governor决定压缩机提高一档也就是让“制冷能力”从1档升到2档这就是cooling device的state变化。等到温度回落到26度以下再逐步降低档位避免频繁启停。1.2 主动散热与被动散热的差别thermal里经常听到“主动散热”和“被动散热”两个词。这里的“主动/被动”指的是冷却手段的性质不是governor的主动被动。主动散热有额外功耗的散热手段比如风扇type通常是active。被动散热通过降低自身性能来减少发热比如CPU降频、GPU限流type通常是passive。这两者经常配合使用。设备树里比较典型的设计是温度到65度先转风扇active到80度再开始CPU降频passive到115度触发临界保护。也就是说cooling map可以绑多个trip点也可以让一个trip点同时控制风扇和降频。governor负责把这些关系翻译成具体的state。理解这个闭环之后再去看内核代码就不会迷路。整个链路就是传感器上报温度zone更新温度然后governor根据trip点计算目标state最后把state写到对应的cooling device。2. 内核里的数据流从温度数值到频率被压低2.1 温度是怎么进到thermal zone的温度传感器在内核里通常不直接属于thermal模块而是挂在hwmon、IIO或者Platform设备下。比如某些SoC内部温控传感器会先注册为一个hwmon设备或者通过底层寄存器读取然后由thermal框架的驱动比如某平台thermal driver或generic thermal layer把传感器读数桥接过来。关键路径是thermal_zone_device_register()。一个zone注册时需要提供一组回调最核心的是get_temp。整个thermal框架的温度轮询就是循环调用它struct thermal_zone_device_ops { int (*get_temp)(struct thermal_zone_device *tz, int *temp); int (*set_trips)(struct thermal_zone_device *tz, int low, int high); int (*get_trip_temp)(struct thermal_zone_device *tz, int trip, int *temp); int (*get_trip_type)(struct thermal_zone_device *tz, int trip, enum thermal_trip_type *type); };get_temp不是瞬间只读一次而是按周期被thermal_zone_device_update()调用。这个周期就是设备树里的polling-delay和polling-delay-passive。这两个参数很容易被忽略但它们决定温度更新的实时性。polling-delay正常温度时的轮询间隔。polling-delay-passive当zone进入passive或active降温状态时的轮询间隔。passive状态的轮询间隔通常要更短比如正常1秒一次越阈后250毫秒一次。因为越阈后温度变化快需要更密集的采样来及时回调。如果你发现某平台温度响应很迟钝先看这两个delay是不是配大了。2.2 thermal_zone_device_update一次温度更新的完整流程每次温度更新来的时候内核并不只是读个温度就完事。__thermal_zone_device_update()的核心逻辑大致是调用get_temp拿到传感器当前温度。遍历这个zone的所有trip point判断当前温度落在哪些trip点的范围内。把温度趋势trend和越阈状态交给governor。governor根据当前温度、trip点温度、上一次的state算出新的冷却目标state。框架再调用对应cooling device的set_cur_state把目标state刷下去。这个流程里有两个容易忽略的点。一是“趋势”这个概念step_wise这类governor在温度上升和下降时使用的步进策略不同趋势判断会影响它选择“升一档”还是“保持原档”。二是trip point不是单独触发而是多个trip点同时处于不同状态governor需要找到最“热”的那一级目标。2.3 governor是怎么算出来目标state的governor是thermal框架里策略和实现分离做得最清楚的部分。内核把governor实现为一个链表每个governor有throttle和unthrottle回调。比较常用的几个Governor策略特点适用场景step_wise温度越阈后每次只升一档温度回落后再降档避免猛升猛降一般场景最常见bang_bang只有开/关两档超过阈值直接拉满低于阈值直接关风扇控制比较典型fair_share按权重把温度负载分摊到多个cooling device上多设备协同降温user_space内核不自动调只上报事件由用户态程序决定定制化策略power_allocator根据功耗预算和温度模型动态分配追求性能和功耗平衡的平台以step_wise为例它的逻辑可以简化成if (temp trip_temp) { if (trend RAISING) cur_state min(cur_state 1, max_state); else cur_state cur_state; } else { if (trend DROPPING) cur_state max(cur_state - 1, 0); }为什么step_wise要这样设计因为它希望温度接近阈值时不要一次性把状态拉满否则温度会掉得很快接着又快速回升形成“震荡”。一次只调一档可以让温度平滑地稳定下来。代价是回落到正常温度比较慢所以很多平台会配合较短的polling-delay-passive来加快采样。fair_share的逻辑又不一样。它不是看绝对温度而是根据当前负载和权重来分配冷却量。举个例子CPU和GPU两个cooling device都绑在同一个zone下CPU权重80GPU权重20。当zone温度超限fair_share会按8:2的权重把散热能力分配到两个设备上。这种方式适合多个发热源互相影响比较大的场景。power_allocator则是更偏学术的设计。它把温控看成一个“功耗分配问题”设定一个sustainable_power可持续功耗数值然后根据当前温度误差和PID参数来调整冷却状态。这个governor调起来门槛比较高因为必须先搞清楚平台的散热能力和传感器位置否则参数很难收敛。这里引用一句判断标准如果平台功耗模型不是很明确先不要上power_allocatorstep_wise能解决90%的问题。2.4 cooling device怎么真正把频率降下来cooling device的接口不多例如CPU调频通过cpufreq驱动注册struct thermal_cooling_device_ops { int (*get_max_state)(struct thermal_cooling_device *cdev, unsigned long *state); int (*get_cur_state)(struct thermal_cooling_device *cdev, unsigned long *state); int (*set_cur_state)(struct thermal_cooling_device *cdev, unsigned long state); };set_cur_state被调用时cpufreq冷却设备会把state映射到对应的频率档位。比如max_state5state2就代表频率降到第2档。不同平台map方式不同有的直接用OPPOperating Performance Point有的用频率表。这里有个关键点cooling device的state是有限的档位不是连续的频率值。比如一个CPU有1.8GHz、1.5GHz、1.2GHz、900MHz四档可用频率max_state就是3state 0对应max频率state 3对应最低频率。理解了这个关系就不会在sysfs下看到“state2”却疑惑它为什么不显示具体Hz。3. 设备树里的thermal-zones配置一份可以直接落地的配置3.1 基本结构thermal-zones节点怎么搭设备树里的thermal是另一个大块。多数ARM平台使用设备树描述thermal topology启动时由内核thermal_of层解析并注册zone。一个最基本的zone配置长这样thermal-zones { cpu0_thermal: cpu0-thermal { polling-delay-passive 250; polling-delay 1000; thermal-sensors tsens0 1; trips { cpu_alert: cpu-alert { temperature 75000; hysteresis 2000; type passive; }; cpu_crit: cpu-crit { temperature 115000; hysteresis 0; type critical; }; }; cooling-maps { map0 { trip cpu_alert; cooling-device cpu0_cool 2 4; contribution 100; }; }; }; };thermal-sensors指向一个温度传感器控制器后面的数字代表第几路。例如某个tsens控制器下面有多路0路读CPU1路读GPU。这个索引如果配错最常见的现象是温度读数永远是0或者异常大。trips下面是阈值点每个子节点代表一个trip。temperature单位是毫摄氏度75000就是75度。hysteresis是回滞区间单位也是毫摄氏度后面详细讲。type可选active、passive、hot、critical。critical类型的trip一旦触发在多数平台会直接触发内核的thermal_zone_device_critical处理可能是要求重启或着关闭电源这是最后一道防线。3.2 cooling-maps与trip的绑定逻辑cooling-maps定义了“哪个trip触发哪个cooling device的哪个档位区间”。上面的cooling-device cpu0_cool 2 4;表示这个map只允许绑定到state 2到state 4之间。也就是说即便step_wise想继续升到state 5这个map也会限制上限为4。这是一个非常实用的约束你不希望某个风扇在低温阈值下就立刻满转速就可以用区间限制。contribution是权重属性主要用于fair_share和power_allocator。它的单位是“千分比”所有cooling device的contribution加起来不一定要等于1000但比例关系会被governor使用。比如map0的contribution100如果某个zone下只有这一个cooling device那么相对权重只能算是总和中的一份。多设备协同降温时这个值很关键。cooling device本身需要声明#cooling-cells。以CPU调频为例通常是在CPU节点或者cpufreq节点上写cpu0: cpu0 { compatible arm,cortex-a53; ... #cooling-cells 2; };#cooling-cells 2代表cooling-device属性里需要两个参数第一个是最小state第二个是最大state。如果你写cpu0 2 4就是允许在state 2到state 4之间调整。3.3 hysteresis温度震荡的解药还是毒药hysteresis是设备树配置里最容易被忽视但又问题最大的参数。它表示温度低于trip点多少毫摄氏度才算真正“离开”这个trip点。举例一个trip点温度是75度hysteresis是2度。那么它在温度上升到75度时触发之后必须回落到73度以下才会解除。如果hysteresis0那么温度在74.9度和75.1度之间波动时governor会反复触发和解除冷却状态会不停地升档降档。这个现象在真实设备上表现为CPU频率一会降低一会恢复风扇一会转一会停。所以hysteresis应该根据整个系统温度波动的幅度来设定。如果实测温度在1-2度范围内抖动那hysteresis至少给到2000到3000。但也不能设太大否则温度都降到远低于阈值了冷却设备还在高档位运行白白损失性能。3.4 多sensor和coefficients不是所有温度都能直接读有些zone会绑多个传感器比如“板温zone”同时读CPU温度和环境温度。这时设备树里thermal-sensors可以是一个phandle数组。内核会取他们的加权平均或者指定策略。还有一个coefficients属性thermal-sensors tsens0 0; coefficients 1200 -500;它的语义是最终温度 传感器读数 * 1200 / 1000 - 500单位毫摄氏度。不同平台的传感器可能存在固定偏移和增益误差这个系数就是做校正用的。如果发现某一路温度比红外实测偏高或偏低优先检查这里的系数而不是一股脑改trips。4. sysfs用户态用最土的方式验证温度策略4.1 /sys/class/thermal/目录地图内核把thermal对象全部暴露在sysfs。进入/sys/class/thermal/你会看到thermal_zone0、cooling_device0这类目录。以thermal_zone0为例节点内容与作用typezone名称比如“cpu0-thermal”temp当前温度单位毫摄氏度读出来可能是65000表示65度modeenabled/disabled可以强制关闭这个zone的自动调温policy当前governor比如step_wise、power_allocatortrip_point_0_temp第0个trip点的温度阈值trip_point_0_type第0个trip点的类型cdev0绑定的第一个cooling device是一个符号链接sustainable_powerpower_allocator使用的可持续功耗参数sysfs的好处是你不用改代码、不用编译就能直接观察governor的行为。比如温度到了阈值cdev的cur_state会不会自己变。这比什么调试工具都直接。4.2 用真实负载触发一次完整的“越阈→降频→恢复”循环推荐一个最直接的验证方法先用yes /dev/null或者stress工具把CPU跑满然后观察温度变化和冷却状态。stress --cpu 4 --timeout 120 watch -n 2 cat /sys/class/thermal/thermal_zone0/temp; echo ---; cat /sys/class/thermal/cooling_device0/cur_state实测下来你会看到温度逐步爬升当温度越过trip_point_0_temp时冷却状态从0变成1温度趋势继续上涨的话再变成2、3。如果系统温度下降state又会逐步回落。这个过程能验证几件事一是温度读数是否正常二是trip点配置是否正确三是governor策略是否生效。如果温度已经超过trip点了cur_state却纹丝不动那问题要么是cooling map没绑对要么是policy根本不是自动的改了user_space。4.3 手动干预把governor换成user_space有些调试场景下我们不想让内核自动随手乱调频率比如想测试某个频点的稳定性或者想复现某个温度临界bug。这时可以把zone的policy改成user_spaceecho user_space /sys/class/thermal/thermal_zone0/policy改完之后再观察温度超阈也不会有任何自动冷却动作。然后你可以手动写cooling stateecho 3 /sys/class/thermal/cooling_device0/cur_state这个操作可以快速验证cooling device链路是否正常。如果手动写state频率能降下来那说明cdev没问题问题在governor或者trip配置。如果手动写都没反应说明lower层的cpufreq或者其他驱动没有真正响应state变化。这就是用户态thermal daemon比如简化的thermald的基本原理内核只负责暴露状态真正的控制逻辑放在用户态小程序里。自己写一个不到50行的shell脚本就能做简单的温控实验。但需要注意这种方案可靠性完全依赖用户态进程不被杀掉如果用户态崩溃系统就没有任何温度保护了生产环境不太建议长期这么干。5. 实操中会遇到的坑问题排查与快速定位5.1 温度读出来永远是0或者0xFFFFFFFF这是最常见的Bring Up问题。温度节点存在但读到的值明显不对。排查顺序一般是先确认传感器本身有没有在别的框架下注册。如果hwmon下也看不到这个传感器优先查I2C/寄存器配置。再查设备树thermal-sensors里的phandle和索引。索引指错地方回读到的寄存器是别的模块的数值自然不对。用coefficients校准。某次遇到温度整体偏差10度查了几天最后发现是sensor采样电阻的参考电压和驱动预期不一致用系数校正后就和红外实测吻合了。如果读到的是很大的值另一个可能是驱动没有判断温度有效的标志位。比如某些传感器读出来的原始数据没有经过mask直接把保留bit也拉进去了。5.2 trip点触发了但cooling device不动作这种情况也很典型。温度值明显超过trip_point_0_tempcur_state却一直是0。优先查三处coolong-maps有没有写对。trip cpu_alert必须和trips节点里的label匹配如果label写错或者引用错内核很可能在解析时静默跳过。cooling device的#cooling-cells和map里的参数个数是否匹配。如果map里写了两个数字但#cooling-cells是1解析就会失败并warning。policy是不是被谁改成了user_space。很多平台的用户态服务会在启动后强制写policy比如说某个厂商的省电服务把policy设成user_space内核governor就不会自动接管。排查命令方面dmesg | grep -i thermal是最直接的。内核在解析设备树失败时通常会有明确日志。比如thermal_sys: Failed to find trips node thermal_sys: Failed to find cooling map看到这种日志直接回设备树找问题就行了。5.3 step_wise回温慢温度在阈值附近反复横跳如果冷却状态升上去很难降回来或者温度在threshold附近震荡通常不是bug而是配置问题。回温慢step_wise设计上就是一步一降回落慢是正常的。如果你希望降得快一点要么换power_allocator要么缩短polling-delay-passive。缩短到200ms左右governor能更快感知到温度下降从而更早降档。温度震荡shopping bag现象前面说的hysteresis设太小。把hysteresis从500改成2000常常能根治。还有个容易忽略的点冷却设备的state档位之间频率差异很大。比如从1.8GHz直接掉到1.2GHz温度会瞬间下降很多接着又因为性能变差导致负载堆积温度再次上升。这不是thermal的问题而是调频档位跨度太大。这时考虑在OPP中间补一个档位让频率下降更平滑。5.4 多核/多cluster配置了多个zone温度却互相干扰现在的SoC基本都是大小核、多cluster架构。如果每个cluster一个zone你会遇到一个现象cluster0过热降频但cluster1还在满负载系统整体功耗没降温度并没有真正回落。这种情况下要么把几个cluster归到同一个zone里让所有cluster的cooling device一起参与降频要么在cooling map里把多个zone的trip都绑到同一个更大范围的cooling device上比如“系统级散热设备”。用sysfs看温度时也要留意哪个zone才是真正决定整机温度的。很多平台还有个“skin temperature” zone传感器在外壳附近那个数值对用户体验影响最大优先级应该更高。5.5 cdev的state写了没反应手动echo cur_state之后频率或者风扇转速没变化这种问题一般在cooling device的正下方驱动。对于cpufreq冷却设备最常见的坑是cpufreq驱动本身有其他限频机制比如某个电源管理策略锁定了最高频率。你设state2但另外一条路径把频率锁定在1.2GHz两者互不感知看起来就是“写了没反应”。对于风扇需要确认风扇驱动有没有注册成thermal cooling device还是只注册成普通pwm驱动。很多风扇设备在设备树里是pwm-fan它同时会注册cdev但如果节点里没加cooling-cells和#cooling-cellssysfs下就看不到对应的cdev。6. 一条更顺的学习路线和几条私藏心得6.1 我建议按这个顺序学thermal而不是从头读源码我第一次读thermal源码时直接扎进thermal_core.c结果被event、notification、power_allocator这些概念绕晕了。后来发现更高效的路子是这样先看内核文档Documentation/driver-api/thermal/把trip、zone、cdev、governor的概念过熟。去sysfs把板子上的真实目录翻一遍对应概念逐个对号入座。从设备树开始写一个简单的zone能调起来比什么都重要。用stress把温度触发一遍观察到governor的升降档行为。再回来看代码重点看step_wise.c和thermal_zone_device_update这时候基本水到渠成。如果你想深入调试强烈建议写一个假的thermal zone驱动。注册一个只返回自定义变量的get_temp回调然后通过debugfs或者sysfs去改这个变量。这样你就能在不需要真实加热的情况下快速验证governor对每个温度点和趋势会做出的反应。我最早调试governor行为时就是靠这个假zone比拿着热风枪去吹sensor效率高得多。伪驱动核心就是注册一个zone温度值每次都从外部变量取static int fake_temp 50000; static int fake_get_temp(struct thermal_zone_device *tz, int *temp) { *temp fake_temp; return 0; } static struct thermal_zone_device_ops fake_ops { .get_temp fake_get_temp, }; static struct thermal_zone_device *tz; tz thermal_zone_device_register(fake, 1, NULL, fake_ops, NULL, 0, 0, 0, 1000);但注意这只是调试工具。它没有真实trip点所以你需要配合设备树或者手动注册trip点否则governor拿不到trip数据依然不会动作。更简单的替代方案是在真实zone里直接echo修改trip_point_0_temp低到比如40000让系统“假装”过热也能触发冷却。6.2 避坑清单结合自己踩过的坑整理一份清单供参考先确认CONFIG_THERMAL、CONFIG_THERMAL_OF、对应governor的config都打开否则设备树写死也没用。polling-delay不是越小越好。频繁轮询会增加功耗温度平稳的平台设1000ms到2000ms都正常。critical trip不要乱删。很多bring up阶段为了不让板子“莫名其妙重启”把critical trip删了这非常危险一旦散热失控硬件有损坏风险。mode节点可以写disabled来临时关闭zone但记得调试完恢复。很多现场问题就是调试人员忘记恢复导致设备完全没有热保护。不要只在空载下测试温控。空载温度根本到不了阈值看不出governor行为。一定要配合stress、GPU负载等真实压力。如果平台有厂商扩展的thermal驱动注意它可能绕过generic thermal governor直接操作寄存器。这时候你用sysfs看policy只是摆设实际行为要翻厂商驱动的日志。6.3 还能往哪些方向扩展thermal模块不是孤立的。深入学习之后可以继续关注几个方向cpuidle和thermal的配合温度高了不仅要降频还可以约束进入更深idle状态来降温内核里有cooling device类型支持这种模式。powercap与功耗控制的关系RAPL这类功耗控制框架和thermal的界限不是一个概念一个管功耗预算一个管温度上限。但实际运行时两者经常联动。DT里的cooling-cells和OPP设计调频档位的分布会直接影响温控平滑度设计OPP时要考虑thermal需求。关键degradation的分析方法系统出现“热伤性能”时用/sys/class/thermal记录数据和cat /proc/stat读CPU占用率能快速定位是thermal限制还是负载本身的问题。从我自己的经验来看学thermal最有价值的一点是它把设备树、内核框架、用户态接口全部串在了一起。你不只是在学一个驱动而是在理解“系统级功耗温度管理”这条完整链路。后来我做性能调优遇到奇怪卡顿时第一反应已经不可能是看某个单独的硬件寄存器而是打开sysfs先看热状态再往下查。这个习惯帮我省掉过很多次无效的debug。