
做内核功耗这块的工程师应该都有同感Linux的thermal framework在各子系统里算得上“门槛最友好、理解最绕”的一个。说友好是因为它的抽象层次非常清晰sensor、cooling device、governor三者分工明确不像cpufreq那样牵一发动全身说绕是因为一旦涉及设备树里的trip point、cooling map、bind_params这些概念再叠加不同governor的行为差异很容易把思路带偏。我在实际接触thermal framework之前一直把它理解为“温度过高就降频”直到自己动手把一套温控策略从设备树一直打通到cooling device回调才发现这套框架的真正价值在于它把“感知热量”“判定风险”“执行降温”这条链路标准化了。不管你的硬件是手机SoC、车载域控制器还是服务器CPU只要按照框架的约定去注册和描述内核就能替你完成大部分温控调度工作。这篇文章想把thermal framework的通用架构串讲一遍重点放在框架的角色模型、设备树描述方式、关键数据结构以及governor的行为差异上。适合那些已经在阅读内核源码、准备做温控策略定制或者单纯想把功耗子系统整体补全的读者。1. 先从问题本质说起thermal framework到底在解决什么在深入代码之前我建议先花两分钟想明白一个问题芯片厂商为什么不直接在硬件里做温控非要操作系统插一脚答案其实很朴素——硬件只能做“断电保护”级别的兜底但真正的性能与温度平衡必须由软件根据场景动态决策。硬件过热保护通常是一个固定阈值到了就断电或者强制降频没到就完全不管。但用户手机上同时跑着游戏和充电时系统可能希望温度到70度就开始限制充电电流到80度才限制CPU频率而外接散热背夹时系统可能希望延迟降频。这种动态的分级策略是硬件做不了的而thermal framework就是给内核提供这套“分级策略”的执行框架。我用一个生活类比帮初学者建立直觉thermal framework就像一套中央空调管理系统。温度传感器是遍布房间的探头负责上报“当前温度”空调内机和风阀是降温设备负责执行“加大制冷”或“停止制热”而坐在中控室的管理员就是governor它根据探头数据决定要不要下发指令、下多大力度。Linux把这三个角色硬性拆开目的就是让这三层可以独立替换。你可以换掉传感器而不动降温策略也可以在同一套硬件上更换governor而不用改设备树。从这个角度再看代码很多设计选择就顺理成章了。框架层通过thermal_zone_device抽象一个热区一个热区绑定一个或多个温度传感器通过thermal_cooling_device抽象一个降温执行器通过thermal_governor抽象决策算法。热区负责收集温度并把它交给governorgovernor根据当前温度与各trip point的关系决定“该不该动作”最后通过binding关系把动作落到具体的cooling device上。整条数据流清晰到可以画在一张餐巾纸上。还有一个容易忽视的出发点这套框架设计时兼顾了“设备树描述”和“驱动注册”两种路径。老式驱动可以在代码里手动创建thermal zone新式驱动则倾向于把zone的拓扑放在设备树里驱动只负责注册sensor和cooling device。这个双轨设计导致阅读代码时会看到两套接口并存后面我会单独把设备树路径讲清楚。理解了“为什么需要它”再回头看各个结构体就会觉得每个字段都有它的位置。接下来我按实际注册顺序拆解三大核心抽象。2. 三大核心抽象thermal zone、cooling device与governor2.1 thermal_zone_device热区的“信息中枢”struct thermal_zone_device是整个框架里最核心的结构体它代表一个被监控的热区域。一个SoC上通常会有多个zoneCPU cluster一个、GPU一个、充电IC一个、电池一个。每个zone有自己的传感器、自己的trip points、自己的governor策略偏好。这个结构体里值得先记牢的几个字段struct thermal_zone_device { struct device dev; struct idr idr; /* ID分配 */ struct list_head thermal_instances; struct thermal_zone_device_ops *ops; /* zone操作回调 */ struct thermal_zone_params *tzp; /* governor相关参数 */ struct thermal_attr *trip_type_attrs; enum thermal_device_mode mode; /* enabled/disabled */ int temperature; int last_temperature; int passive_delay; int polling_delay; struct thermal_governor *governor; struct thermal_trip trips[]; /* trip点数组 */ };ops里最重要的三个回调是get_temp、get_trip_temp和get_trend。get_temp由传感器驱动实现返回当前温度get_trip_temp返回某个trip point的阈值get_trend返回温度变化趋势上升/下降/稳定governor决策时会用到。passive_delay和polling_delay决定轮询周期没有主动中断时内核按这个周期周期性读取温度如果驱动支持中断上报比如lm_sensors系列很多芯片支持THERMAL_TRIP_HOT中断到达阈值时会主动触发通知。关于热区数量我见过有人把每个CPU核心单独建一个zone结果governor要同时管理8个zonecooling device的绑定关系乱成一团。实际上多数平台用一个cluster级zone就够了因为硅片的温度传感器采样区域本来就有限拆得过细既增加轮询开销又容易造成各zone策略互相打架。2.2 thermal_cooling_device执行降温的“受控负载”thermal_cooling_device抽象所有“可以被削弱以降温”的设备。CPU调频器、GPU调频器、风扇、充电电流限制器、显示屏背光都属于cooling device。它的核心是ops-get_max_state和ops-set_cur_state两个回调static int cpufreq_cooling_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) { /* 返回可用的调频档位数量 - 1 */ } static int cpufreq_cooling_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) { /* 将频率限制到对应档位 */ }max_state通常是“降频档位数减一”一个支持20档频率的CPUmax_state可能就是19。state越大降温力度越强性能削弱越多。这套约定让governor不需要知道具体设备是什么只需要说“把state从0调到2”风扇还是CPU都会照做。我提醒一点cooling device的state语义在不同驱动里并不完全统一。有的驱动state 0就是最大性能有的驱动state 0反而是最大散热能力对风扇而言。查看一个陌生cdev的行为时不要只看max_state一定要打开驱动确认set_cur_state里state增大时是增强散热还是削弱功能。这类不对称导致的问题在联调时特别容易踩。2.3 thermal_governor温控策略的“大脑”governor是纯软件模块不直接接触硬件它的职责是根据zone的温度、温度趋势、trip point的触发状态计算出“要不要调整某个cooling device的state”。Linux主线常见的有四个step_wise按步进方式逐级调整温度高过trip就升一档state降到阈值以下就降一档适合大多数场景。power_allocator基于PID控制器的IPAIntelligent Power Allocator适合需要精细功耗分配的移动SoC场景。fair_share按权重比例分配各cdev的散热义务使用场景较少。user_space把决策权交给用户态内核只上报温度。governor在thermal framework里是一个链表中的节点通过thermal_governor_register注册。每个热区可以在设备树或用thermal_zone_params指定使用哪个governor也可以在sysfs里动态切换thermal_zoneX/policy。我个人的理解是governor的设计把“策略”和“机制”彻底分开了。机制部分是固定的——轮询温度、记录trip状态、调用binding关系策略部分完全看governor的算法。所以当你觉得系统温控行为不合理时第一步不是改设备树阈值而是确认当前用的是哪个governor它在这种场景下天生会怎么表现。后面有一章我会专门展开step_wise和power_allocator的行为差异。三大抽象理解完之后热区、执行器、决策者都有了但还缺最后一块拼图——它们如何被组装到一起。答案在设备树和注册接口里。3. 设备树描述与驱动注册一条链路怎么串起来在现代嵌入式Linux开发中90%的thermal拓扑都在设备树里描述。这样做的好处是不用重新编译内核就能调整阈值和绑定关系。很多团队做散热调优时改dts、重编dtb、重启验证一套流程下来比改驱动快得多。3.1 设备树里的thermal node一个典型的thermal zone节点是这样写的thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive 250; /* 被动冷却时的轮询周期(ms) */ polling-delay 1000; /* 无被动冷却时的轮询周期(ms) */ thermal-sensors tsens0 1; /* 绑定的传感器 */ trips { cpu_alert0: cpu-alert0 { temperature 85000; /* 85度 */ hysteresis 2000; /* 回差2度 */ type passive; /* 被动触发 */ }; cpu_crit: cpu-crit { temperature 105000; /* 105度 */ hysteresis 0; type critical; /* 临界触发 */ }; }; cooling-maps { map0 { trip cpu_alert0; cooling-device cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT, cpu1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT; }; map1 { trip cpu_crit; cooling-device fan0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT; }; }; }; };逐段解释一下关键字段polling-delay-passive当zone里有passive类型trip被触发后内核切换成更短的轮询周期这个值就是250ms。没触发时用polling-delay的1000ms。这个“触发后加速采样”的设计是节省功耗和响应速度之间的经典折中。thermal-sensors字符串格式是phandle sensor_idsensor_id是传感器在该控制器里的通道号。比如tsens0 1表示tsens0控制器上的第1路传感器。不写sensor_id默认取第0路。trips里的hysteresis是回差值防止温度在阈值附近反复横跳导致cooling device频繁抖动。85度触发83度解除比没有回差的时候稳定得多。type支持passive、active、hot、critical四种。hot通常用于通知用户态准备关机critical由内核直接触发关机或重启。需要特别注意critical类型的处理不经过governor而是直接在框架层做紧急动作。cooling-maps里的cooling-device属性是三元组phandle min_state max_state。THERMAL_NO_LIMIT即0和MAX_STATE表示不限制这个cdev的调节范围。你完全可以把某个cdev锁定在state 2到state 5之间这在多zone共用同一个cdev的场景下非常好用。3.2 驱动侧的注册接口设备树描述好了驱动侧要把对应的sensor和cooling device“填”进框架里。新的API推荐使用devm_thermal_of_zone_registerstatic int my_sensor_probe(struct platform_device *pdev) { struct thermal_zone_device *tz; struct thermal_zone_device_ops *ops devm_kzalloc(pdev-dev, sizeof(*ops), GFP_KERNEL); ops-get_temp my_sensor_get_temp; ops-get_trend my_sensor_get_trend; tz devm_thermal_of_zone_register(pdev-dev, 0, data, ops); if (IS_ERR(tz)) return PTR_ERR(tz); return 0; }这里第二个参数是sensor id必须和dts里thermal-sensors tsens0 1中的1一致。devm_thermal_of_zone_register会自动去寻找设备树中引用这个phandle和id的thermal-zone把ops填进zone的驱动回调里。cooling device的注册同样简单static int my_fan_probe(struct platform_device *pdev) { struct thermal_cooling_device *cdev; struct thermal_cooling_device_ops *ops devm_kzalloc(pdev-dev, sizeof(*ops), GFP_KERNEL); ops-get_max_state fan_get_max_state; ops-get_cur_state fan_get_cur_state; ops-set_cur_state fan_set_cur_state; cdev devm_thermal_of_cooling_device_register(pdev-dev, my_fan, fan_priv, ops); return PTR_ERR_OR_ZERO(cdev); }字符串my_fan是cdev的名字会在sysfs里显示为cooling_device0的type。如果你的cdev需要在dts里被引用节点里要配#cooling-cells 2这两个cell就是上面提到的min_state和max_statefan0: fan { compatible my,fan-ctrl; #cooling-cells 2; };我在实际项目中遇到过一个问题sensor驱动加载顺序如果晚于thermal zone的解析设备树里绑定关系可能会注册失败。虽然新接口用了devm_但依赖的sensor phandle如果没有先probethermal_of_zone_register会因为找不到传感器而返回-EPROBE_DEFER。这时候不要慌这是正常的延迟探针机制只要驱动本身支持probe deferral系统会在sensor就绪后自动重试。3.3 两种注册路径的历史包袱前面提过框架有“双轨制”这里展开说一下。老接口thermal_zone_device_register需要驱动自己把trip points以数组形式传给框架比较繁琐新接口thermal_zone_of_sensor_register和devm_thermal_of_zone_register改为从设备树读取trips和cooling maps。内核新版本里thermal_zone_device_register的调用者正逐步向of-based路径迁移。阅读源码时在thermal_core.c里你会看到两套非常相似的注册流程。我建议新手优先吃透of-based路径因为现在绝大多数平台都走这条。如果你做的是纯x86服务器、没有设备树的环境才需要回头研究老接口。4. 热区的灵魂trip point、斜率与被动冷却4.1 trip point分级和生效顺序trip point是热区的灵魂——它定义了“热事件”的等级。内核根据当前温度与所有trip阈值的比较结果维护一个“已触发级别”的概念。从低到高依次为normal - active散热片开启- passive被动限制性能- hot - critical。你可能会奇怪为什么active反而比passive级别低这其实是散热手段的语义约定。active对应主动散热设备风扇、空调触发它是“加大散热”不牺牲性能passive对应被动散热手段降频、限流触发它意味着系统准备牺牲性能。所以正常情况下系统先尝试主动散热不够再降性能。理解这个顺序很重要因为不同governor对同一组trip point的反应差别很大而trip的type字段决定了它在框架层走哪条处理路径。critical级别的trip触发后thermal_zone_critical会直接调用orderly_poweroff或panic这个过程不经过governor也不经过cooling device。hot级别则触发thermal_zone_device_update让用户态有机会介入。4.2 斜率slope和偏移offset——容易出错的校准每个sensor驱动都可以通过thermal_zone_params描述温度和ADC原始值之间的线性关系。最常见的是slope和offset两个参数关系式是temperature毫摄氏度 raw_value * slope offset不校准slope和offset时内核默认按1:1关系直接使用sensor上报值。但实际硬件中sensor离发热源的距离不同、封装导热系数不同、系统误差不同上报值可能与真实结温偏差很大。我见过一个平台sensor上报温度比实际结温低12度导致critical trip形同虚设。校准后的斜率修正确实能解决一部分误差但要注意slope和offset的校正在不同厂商的sensor驱动里使用方式不同有的驱动直接把它们写进设备树扩展属性里有的驱动通过thermal_zone_params传给框架。一定要先看驱动的get_temp实现确认它到底怎么使用这两个参数再决定调谁。4.3 被动冷却的真正含义passivetrip触发后框架会把zone的passive标志置位同时将轮询周期从polling-delay切换到polling-delay-passive。这一步的目的是“加速感知”当系统已经进入被动降温状态说明热风险正在聚集此时保持每秒采样一次可能不够及时收紧到250ms能更快响应温度回落。但这个机制有个隐藏代价高频轮询本身会唤醒CPU带来额外的功耗开销。如果passivetrip触发频繁且轮询周期设得太小可能出现“为了降温反而增加了额外的发热”的负优化。我在调试一个车载设备时把polling-delay-passive设成100ms结果温控效果的改善微乎其微功耗却涨了一截。经验建议被动轮询周期通常在200~500ms之间不要低于100ms。5. governor工作机制拆解step_wise 与 power_allocator5.1 step_wise简单但不粗暴step_wise是全志、瑞芯微等平台最常见的governor它按“当前温度处于哪个trip级别”来决定上下调整cdev的state。它的核心是thermal_zone_trip_update里对每个trip做遍历static void step_wise_consider(struct thermal_zone_device *tz, struct thermal_trip *trip, struct thermal_instance *instance) { if (trip-temperature tz-temperature) { /* 温度超过该trip阈值考虑上调 */ if (instance-target instance-upper) instance-target instance-target 1; /* 步进加一 */ } else { /* 温度低于阈值考虑下调 */ if (instance-target instance-lower) instance-target instance-target - 1; /* 步进减一 */ } }实际代码比这复杂一些加入了trend的判断——温度趋势还在快速上升时即使刚超过trip点也可以直接跳到更高级别温度趋势下降时则尽量保持当前level防止频繁波动。但核心思想就是一次只动一档不贪心。step_wise最大的优势是行为可预测。调一档看温度变化不行再调一档整个曲线很平滑。它的劣势也很明显升温很快时逐级上调可能跟不上温度爬升速度必须依赖合理的trip间距和合理的轮询周期。如果你的平台温升速率很快比如大核突然满载step_wise反应可能偏慢此时可以考虑把trip点之间的间距调小让多级降温更快触发。5.2 power_allocatorPID式的精细管理power_allocatorIPA是thermal框架里最“聪明”的governor。它不是简单根据当前温度越没越阈值来动作而是把温度偏差当前温度与目标温度之差当作PID控制器的输入输出一个“允许功耗值”再把这个功耗分配权重分摊给各个cooling device。IPA的核心参数有sustainable_power系统在稳态下能持续散热的功耗瓦数。k_po、k_pu、k_iPID控制器的比例、积分增益。trip_point它只使用一个passive类型的trip作为“目标温度”比如80度。温度超过80PID输出负向修正削减允许功耗低于80逐步恢复功耗。我在移动SoC平台上体验过IPA的效果在视频播放场景下step_wise会把频率一档一档往下降直到源温度回落到阈值以下帧率波动明显而IPA会提前预算功耗把CPU和GPU的功耗一起压缩帧率虽然整体降了一点但波动小很多。代价是需要调PID参数k_pu太大容易震荡sustainable_power估得太离谱会让governor完全失效。如果你是新接触IPA我建议先在几个固定场景下记录sustainable_power的估计值再跑一次阶跃负载实验观察温度响应曲线看看有没有过冲和振荡。PID参数调优没有捷径日志里看thermal_zoneX/temp和cdev的cur_state变化曲线是最直观的方法。5.3 governor的运行时切换每个zone都暴露一个sysfs节点policy可以在系统运行时切换governor# 查看当前governor cat /sys/class/thermal/thermal_zone0/policy # 切换为step_wise echo step_wise /sys/class/thermal/thermal_zone0/policy这个特性在联调阶段非常有用可以先在用户态暴力切换验证不同算法行为再决定把哪个governor写死到代码里。注意不是所有governor都能在所有zone上正常工作比如power_allocator就需要tzp-sustainable_power等参数已经被正确初始化否则切过去之后cdev可能完全不动。6. 调试实战sysfs接口、模拟温度与常见问题排查6.1 thermal_zoneX目录里有什么/sys/class/thermal/thermal_zoneX下的主要节点/sys/class/thermal/thermal_zone0/ ├── mode # enabled/disabled可以手动禁用温控 ├── policy # 当前governor ├── temp # 当前温度单位毫摄氏度 ├── type # zone类型如cpu-thermal ├── trip_point_0_temp ├── trip_point_0_type ├── trip_point_1_temp ├── trip_point_1_type └── ueventtrips的个数和设备树里的trips节点一一对应。判断trip是否触发可以直接看temp和trip_point_X_temp的相对关系。mode写成disabled可以让整个zone失效——系统重启前不会再做任何温控动作这招在排查“是不是温控导致性能异常”时特别好用测试完务必记得恢复。6.2 用emul_temp模拟温度大多数thermal sensor驱动支持emul_temp节点用于模拟温度输入。它的存在价值极大可以在不加热硬件的情况下验证governor行为和cdev联动。# 先把温度模拟到90度 echo 90000 /sys/class/thermal/thermal_zone0/emul_temp # 观察对应cooling device的state变化 cat /sys/class/thermal/cooling_device0/cur_state # 恢复正常 echo 30000 /sys/class/thermal/thermal_zone0/emul_temp用这个技巧我可以在板子上反复验证“85度触发降频一档95度触发降频两档回落到83度恢复一档”的完整链路而不用真的去加热芯片。注意emul_temp需要sensor驱动显式实现set_emul_temp回调不是所有驱动都有。没有的话只能靠热风枪或大电流负载调试效率差很多。6.3 常见问题与排查思路结合我在项目里遇到的三个高频问题给出排查路径1. cdev完全不动作但温度已经超过trip阈值先查两件事一是cooling-maps里的绑定关系是否配了trip引用的phandle是否正确二是确认该zone的governor是不是user_space——用户态governor不会自动调整cdev需要应用层写入如果没写自然一动不动。我在调试一个智能硬件时发现在设备树里配了冷却映射但没配任何governor默认就成了user_space现象就是“温度爆了但风扇不动”。2. cdev动作了但温度持续不降优先怀疑cooling device的降温能力不足或state上限不够。检查cur_state是否已经达到max_state如果已经封顶要么加大cdev能力比如风扇转速上限要么降低功率源头比如限制整机功耗。还有一种可能是传感器位置离发热源太远导致反馈滞后降温动作已经做了但感知不到。这种情况除了传感器物理位置调整只能靠调大hysteresis让系统不那么敏感避免反复触发。3.thermal_of_zone_register返回-EPROBE_DEFER本质是依赖的sensor或cdev还没注册。排查方法很简单cat /sys/class/thermal/thermal_zone*/type看看已注册的zone列表对比设备树里期望的zone数是否一致。如果在sensor驱动probe成功后zone仍然没出现用/sys/kernel/debug/device_component或dmesg看具体报错——多数情况是thermal_sensor的phandle解析失败比如设备树里写错了sensor id。6.4 调温控策略时的记录习惯最后分享一个工作习惯每次调温控参数我都会把dts改动、负载场景、温度曲线、cdev行为一并记录。因为温控调优的验证周期很长改一个阈值可能要跑一整轮压测才发现问题如果只记“把阈值从85改到90”两周后根本想不起来当时为什么这么改。我一般会在dts的注释里写清楚修改日期和动机比如“2024-03-12游戏场景下85度触发导致帧率抖动上调到88度待观察充电场景”。这套记录方式的回报体现在下一次团队接手时——散热调优的坑很多不是技术深而是历史包袱和上下文丢失叠加出来的。7. 从本文出发建议的下一步阅读路径如果你刚把这篇文章读完我建议按照以下顺序继续深入先打开drivers/thermal/thermal_core.c把zone的注册、更新流程走一遍特别关注thermal_zone_device_update这个函数它是整个框架的“心跳入口”。然后看thermal_zone_trip_update理解trip触发后如何遍历cdev并调用governor的回调。接着选一个你当前平台在用的sensor驱动比如qcom-spmi-temp-alarm、rockchip_thermal对照设备树描述理解sensor id和trip的映射关系。最后再回到step_wise.c逐行读配合emul_temp实测基本就能把整个框架从“抽象概念”内化成“肌肉记忆”。如果项目里涉及IPA也不要急着上全部PID参数建议先在用户态用policy节点切换验证再加trace_thermal事件和应用层日志辅助分析逐步积累原始数据。温控调优从来不缺理论缺的是一手日志和可靠的复现方法。就我个人经验来看thermal framework的复杂不在于某个单独的机制而在于它同时横跨了设备树解析、驱动注册、策略调度、sysfs接口、紧急动作五个层面。一次只吃透一个层面比试图一次读完所有代码要有效得多。希望这篇架构梳理能给你接下来的源码阅读提供一张可靠的地图。