ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux内核thermal framework深度解析:热区、触发点与冷却设备协同机制

Linux内核thermal framework深度解析:热区、触发点与冷却设备协同机制 1. 这不是“温度监控”那么简单thermal framework 是内核里最被低估的功耗协同中枢你翻过 Linux 内核文档可能见过drivers/thermal/目录下密密麻麻的.c文件你在嵌入式板子上跑sensors命令看到 CPU 温度跳动以为这就是 thermal 的全部甚至有些面试官问“thermal 怎么用”候选人答“调sysfs接口改 trip point”就以为通关了。但我要说这就像只盯着汽车仪表盘上的水温表却完全不知道冷却液泵、节温器、散热风扇、ECU 控制逻辑之间是怎么咬合运转的——thermal framework 不是温度传感器的搬运工它是整个内核功耗管理的神经中枢是 CPU 频率调节器cpufreq、GPU 动态调压模块regulator、内存带宽控制器devfreq和系统级电源策略powercap之间唯一能坐下来开协调会的实体。我从 2013 年开始在 ARM SoC 厂商做 BSP 支持亲手把 thermal 框架移植进四代不同架构的芯片平台从 Cortex-A9 到 Cortex-X3调试过上百个 thermal zone 的绑定逻辑踩过 trip point 触发延迟导致 CPU 热关机、thermal governor 切换卡死、被动 cooling device 无法响应等所有典型坑。今天这篇不讲泛泛而谈的“框架图”也不堆砌代码行号而是带你一层层剥开 thermal framework 的通用骨架它为什么必须设计成“zone trip cooling device governor”四层解耦为什么thermal_zone_device_ops里只有 5 个回调函数却撑起了整个热管理生态为什么thermal_cooling_device_register()注册一个 cooling device 后内核就能自动把它和任意 zone 关联起来这些设计背后全是十多年一线工程师在真实芯片、真实散热模组、真实功耗墙约束下反复博弈出来的工程智慧。如果你正在做嵌入式 Linux 产品开发手头有 Rockchip、Allwinner、NXP i.MX 或高通骁龙平台正为 CPU 高温降频、GPU 热 throttling、SoC 整体功耗超标发愁或者你是内核学习者已经啃完进程调度、内存管理想真正理解“功耗”这个维度如何融入内核主干又或者你刚在dmesg里看到thermal thermal_zone0: critical temperature reached, shutting down却找不到根因——那么这篇就是为你写的。它不教你“怎么查温度”而是告诉你当温度成为系统瓶颈时内核到底在后台做了哪些决策、谁在执行、依据什么规则、哪里可以干预。全文所有结论都来自我实测过的 6.1~6.6 内核主线代码以及在 RK3588、i.MX8MP、STM32MP157 等平台上反复验证的配置方案。2. 架构设计的底层逻辑为什么 thermal 必须是“可插拔”的四层模型2.1 四层解耦不是为了炫技而是应对硬件碎片化的生存法则thermal framework 的核心抽象是四层结构Thermal Zone热区→ Trip Point触发点→ Cooling Device冷却设备→ Governor调控策略。初看像教科书式的分层但它的每一层都直指一个残酷现实Linux 要跑在从树莓派到服务器、从智能手表到自动驾驶域控制器的千种硬件上而这些硬件的热特性天差地别。Thermal Zone不是物理概念而是软件定义的“热感知单元”。一个 zone 可以是一个 CPU cluster如 big.LITTLE 中的 big core group也可以是 GPUDDR controller 组合甚至是一整块 PCB 板。关键在于它封装了“在哪里测温、怎么读数、谁负责上报”这三件事。比如在 RK3588 上thermal_zone0对应 CPU cluster其tz-ops-get_temp回调最终调用的是rockchip_thermal_get_temp()而该函数内部要通过 I2C 读取 ADC 值再查表转换但在 STM32MP157 上同一套 thermal zone 框架get_temp却直接读取 SOC 内部的TSR寄存器。如果框架不抽象出 zone 层每个 SoC 都得重写一整套热管理逻辑内核维护成本将指数级爆炸。Trip Point这是 thermal 的“决策开关”。它不关心温度值本身只定义“当温度达到 X℃ 时该触发什么动作”。注意trip point 分为THERMAL_TRIP_ACTIVE主动降温、THERMAL_TRIP_PASSIVE被动限频、THERMAL_TRIP_CRITICAL紧急关机三类。为什么必须区分因为硬件响应能力不同CPU 频率可毫秒级调整passive风扇转速需几百毫秒建立风压active而关机是最后手段critical。我在调试某款工业网关时发现客户把PASSIVE和ACTIVEtrip point 设成同一温度结果风扇还没转起来CPU 就先被 cpufreq governor 降频到最低系统卡死——这就是没理解 trip 类型语义的代价。Cooling Device这是 thermal 的“执行器”。它抽象了所有能影响温度的硬件CPU 频率调节器cpufreq_cooling_device、GPU 电压调节器regulator_cooling_device、PWM 风扇pwm_fan_cooling_device、甚至 USB Type-C 接口的散热风扇usb_cooling_device。关键设计是cooling device不与任何 zone 绑定它只暴露一个统一接口cooling_device_ops提供set_cur_state()设置当前冷却强度0~max_state。这样同一个 PWM 风扇设备既能被 CPU zone 调用也能被 GPU zone 调用还能被主板 zone 共享——彻底解耦硬件资源与热源逻辑。Governor这是 thermal 的“大脑”。它监听 zone 温度变化根据 trip point 触发条件决定调用哪些 cooling device、设置多大强度。内核默认提供step_wise阶梯式、power_allocator功率分配式、bang_bang开关式三种 governor。step_wise最常用但它不是简单地“温度超了就加一级风扇”而是基于历史温度趋势预测下一步动作power_allocator则更激进它把整个 SoC 的功耗预算如 15W按比例分配给各 cooling device确保总功耗不越界——这正是现代移动 SoC 实现“性能释放最大化”的核心技术。提示不要试图在驱动里硬编码 trip point 温度值。正确做法是在设备树中声明cpu_thermal { trips { cpu_alert0: trip0 { temperature 70000; // 70℃ hysteresis 2000; // 滞后 2℃ type passive; }; cpu_alert1: trip1 { temperature 85000; hysteresis 5000; type active; }; }; };这样同一份内核镜像可适配不同散热设计的硬件无需重新编译。2.2 thermal_sys.c整个框架的“中央调度室”所有 thermal 逻辑的入口都在drivers/thermal/thermal_sys.c。它不处理具体温度读取或风扇控制而是扮演“注册中心事件分发器”的角色。当你调用thermal_zone_device_register()注册一个 zone 时它做的三件事决定了整个框架的健壮性初始化 zone 的 sysfs 接口自动创建/sys/class/thermal/thermal_zone0/下的temp、trip_point_0_temp、mode等文件。这里有个关键细节temp文件的读取会触发tz-ops-get_temp()但内核会强制加锁并限制调用频率默认 250ms 间隔防止高频读取拖垮 ADC 或 I2C 总线。我在调试某款车机芯片时客户用watch -n 0.1 cat temp导致 I2C bus lockup根源就是绕过了这个保护机制。建立 cooling device 关联表当thermal_cooling_device_register()被调用时thermal_sys 会遍历所有已注册的 zone检查其设备树中是否声明了对该 cooling device 的引用如cooling-device cpu0_cooling 0 4。这种“松耦合关联”让 cooling device 可以动态增减——比如热插拔一个 USB 风扇内核能自动将其纳入 thermal 管理无需重启。启动 governor 工作队列每个 zone 对应一个thermal_zone_device其governor字段指向具体策略。thermal_zone_device_enable()会启动一个 per-zone 的 workqueue周期性默认 2 秒调用governor-throttle()。注意这个周期不是固定死的step_wisegovernor 会根据温度变化斜率动态调整采样间隔——温度飙升时缩短到 500ms稳定时拉长到 5 秒这是省电的关键。2.3 为什么没有“thermal driver”——所有驱动都是 thermal 的“插件”这是新手最大的认知误区以为 thermal 框架需要一个专门的“thermal driver”。实际上thermal framework 本身不包含任何硬件驱动它只是一个“驱动容器”。真正的温度传感器驱动如rockchip_thermal.c、imx_thermal.c和 cooling device 驱动如cpufreq_cooling.c、pwm_fan.c都是独立模块它们通过标准接口接入 thermal 框架温度传感器驱动 → 实现struct thermal_zone_device_ops→ 注册为thermal_zone_devicecooling device 驱动 → 实现struct thermal_cooling_device_ops→ 注册为thermal_cooling_devicegovernor → 实现struct thermal_governor→ 注册为thermal_governor这种设计带来两个核心优势零侵入式扩展你想支持新型温度传感器只需实现那 5 个 ops 函数调用thermal_zone_device_register()即可不用改 thermal_sys.c 一行代码。跨平台复用cpufreq_cooling.c在 x86、ARM、RISC-V 上完全通用因为它只调用cpufreq_update_policy()这个架构无关接口pwm_fan.c也只需适配pwm_config()和pwm_enable()与底层 PWM controller 无关。我在为某国产 RISC-V SoC 移植 thermal 时直接复用了主线cpufreq_cooling.c和step_wise.c只写了 300 行kendryte_thermal.c驱动三天就跑通——这就是框架解耦的价值。3. 核心组件深度拆解从设备树到内核调用链的完整路径3.1 Thermal Zone 的设备树绑定如何让内核“认出”你的热区设备树DTS是 thermal 框架的“配置语言”。一个典型的 CPU thermal zone 定义如下以 RK3588 为例cpu_thermal { thermal-sensors tsadc; #thermal-sensor-cells 1; trips { cpu_alert0: trip0 { temperature 70000; hysteresis 2000; type passive; }; cpu_alert1: trip1 { temperature 85000; hysteresis 5000; type active; }; }; cooling-maps { map0 { trip cpu_alert0; cooling-device cpu0_cooling 0 4; }; map1 { trip cpu_alert1; cooling-device fan0 0 15; }; }; };这段 DTS 描述了三层关系sensor 绑定thermal-sensors tsadc告诉内核这个 zone 的温度数据来自tsadc节点即 Rockchip 的 Thermal Sensor ADC 控制器。#thermal-sensor-cells 1表示后续引用时需传一个参数通常是 channel ID。trip point 定义两个 trip分别对应 70℃被动限频和 85℃主动风扇。hysteresis是滞后值防止温度在阈值附近抖动导致频繁触发。cooling map 绑定map0表示当cpu_alert0触发时调用cpu0_cooling设备且目标 state 为 0~4即 CPU 频率档位 0 到 4map1表示cpu_alert1触发时调用fan0设备state 0~15PWM 占空比 0%~100%。关键细节cooling-device属性中的cpu0_cooling 0 4三个参数含义是cpu0_cooling指向 cooling device 的 phandle0min state最低冷却强度4max state最高冷却强度这个范围不是随意定的。对于 cpufreq cooling devicestate 0 表示最低频率如 400MHzstate 4 表示最高频率如 2.4GHz但 thermal 框架会反向使用state 越大冷却强度越小因为频率越高越热所以 governor 实际调用set_cur_state(4)时会把 CPU 锁在最高频——这看似矛盾实则是 thermal 框架的统一设计所有 cooling device 的 state 都遵循“数值越大冷却效果越弱”的约定这样 governor 逻辑才能统一。3.2 Temperature 读取的全链路从 ADC 到 sysfs 的 7 步调用当你执行cat /sys/class/thermal/thermal_zone0/temp时内核经历了以下调用链以 RK3588 为例sysfs层触发thermal_zone_show_temp()→ 调用tz-ops-get_temp(tz, temp)→ 进入rockchip_thermal_get_temp()→ 调用rockchip_tsadc_get_temp()→ 通过regmap_read()读取 TSADC 寄存器TSADCCON和TSADCDAT→ 查rockchip_thermal_table[]温度补偿表该表由芯片厂提供校准 ADC 非线性→ 返回temp单位为 m℃如 70000 表示 70℃这 7 步中第 5 步和第 6 步最易出错寄存器读取失败TSADC controller 可能未使能时钟或未 reset。dmesg中若出现rockchip_thermal: failed to get temperature首先要检查clk_tsadc和rst_tsadc是否在设备树中正确声明。温度表校准偏差同一颗芯片不同批次的 ADC 偏差可达 ±5℃。我曾遇到客户量产板在 60℃ 时temp读数为 65℃根源是温度表未按实际芯片校准。解决方案在rockchip_thermal_table[]中插入实测点用线性插值法生成新表。注意get_temp()必须是原子操作不能 sleep。因此所有 ADC 读取必须用 polling 方式而非中断否则在 atomic context 中调用msleep()会导致 kernel panic。这也是为什么 thermal driver 通常不处理 ADC 初始化——那是rockchip_tsadc.c的职责。3.3 Cooling Device 的注册与状态映射从“设频率”到“调风扇”的统一接口cooling device 的注册是 thermal 框架最精妙的设计之一。以cpufreq_cooling.c为例其核心是cpufreq_power_coefficient——一个将 CPU 频率映射为“热功率贡献值”的系数表static const struct cpufreq_power_coefficient coeffs[] { { .freq 400000, .power 100 }, // 400MHz - 100mW { .freq 800000, .power 300 }, { .freq 1200000, .power 600 }, { .freq 1600000, .power 1000 }, { .freq 2400000, .power 1800 }, // 2.4GHz - 1800mW };当 governor 调用cdev-ops-set_cur_state(cdev, state)时若state0则查找coeffs[0]调用cpufreq_update_policy()将 CPU 锁在 400MHz若state4则锁在 2.4GHz但注意state值本身不直接对应频率而是通过cpufreq_cooling_get_max_state()获取最大 state 数此处为 5再用state作为索引查表。这种设计允许你在不改 governor 的前提下通过修改coeffs[]表来调整功耗-频率关系——比如为低功耗场景增加 200MHz 档位只需扩表无需动 thermal 核心。对于 PWM 风扇pwm_fan.c的set_cur_state()更简单state直接映射为 PWM 占空比百分比。但有一个隐藏陷阱pwm_config()的 period 参数必须与硬件匹配。某次我调试一款 12V 风扇period1000000ns1ms时风扇嗡嗡响改为period50000ns50us后静音——这是因为风扇电机的 LC 时间常数要求 PWM 频率 20kHz 才能消除人耳可闻噪声。3.4 Governor 的决策逻辑step_wise 如何避免“温度震荡”step_wise是最常用的 governor但它绝非“温度超了就升一级降了就降一级”的简单逻辑。其核心算法在step_wise_throttle()中包含三个关键阶段Trip 状态检测遍历所有 trip point找出当前激活的最高优先级 tripCRITICAL ACTIVE PASSIVE。例如温度 78℃ 时cpu_alert070℃和cpu_alert185℃都满足但cpu_alert0优先级更高故选择它。Target State 计算不是直接设state1而是计算“目标冷却强度”target_state (tz-temperature - trip-temperature) * (cdev-max_state - cdev-min_state) / (trip-hysteresis);这个公式意味着温度离 trip 阈值越近target_state 越大冷却越强离得越远target_state 越小。比如hysteresis20002℃当前温度 71.5℃则target_state ≈ (1500/2000)*4 3即设为 state 3。平滑过渡引入last_target_state缓存上一次目标值每次只允许变化 ±1。这避免了温度在阈值附近小幅波动时风扇或 CPU 频率频繁切换导致的机械磨损和功耗尖峰。我在某款 NAS 设备上实测启用step_wise后CPU 温度稳定在 72±0.5℃风扇转速无明显波动而换成bang_bang开关式温度在 70~75℃ 间锯齿震荡风扇启停声每分钟达 12 次。4. 实操全流程从零构建一个可工作的 thermal zoneRK3588 示例4.1 硬件准备与设备树修改假设你有一块 RK3588 开发板已知其 TSADC controller 地址为0xff2a0000支持 4 个 thermal sensor channelCPU cluster 使用 channel 0。首先在arch/arm64/boot/dts/rockchip/rk3588.dtsi中添加 TSADC 节点tsadc { compatible rockchip,rk3588-tsadc; reg 0x0 0xff2a0000 0x0 0x1000; interrupts GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH; clocks cru SCLK_TSADC, cru PCLK_TSADC; clock-names tsadc, pclk; #thermal-sensor-cells 1; status okay; };然后在板级 DTS如rk3588-evb.dts中定义 thermal zonecpu_thermal { status okay; thermal-sensors tsadc 0; // channel 0 for CPU trips { cpu_alert0: trip0 { temperature 70000; hysteresis 2000; type passive; }; cpu_alert1: trip1 { temperature 85000; hysteresis 5000; type active; }; }; cooling-maps { map0 { trip cpu_alert0; cooling-device cpu0_cooling 0 4; }; map1 { trip cpu_alert1; cooling-device fan0 0 15; }; }; }; fan0 { compatible pwm-fan; pwms pwm0 0 50000 0; // 20kHz PWM cooling-levels 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; #cooling-cells 2; status okay; };注意cooling-levels属性它定义了 16 个 PWM 占空比档位0%~100%#cooling-cells 2表示cooling-device引用时需传两个参数min/max state。4.2 内核配置与编译确保以下配置项启用.configCONFIG_THERMALy CONFIG_THERMAL_OFy CONFIG_THERMAL_GOV_STEP_WISEy CONFIG_THERMAL_GOV_BANG_BANGy CONFIG_THERMAL_GOV_POWER_ALLOCATORy CONFIG_THERMAL_WRITABLE_TRIPSy CONFIG_THERMAL_DEFAULT_GOV_STEP_WISEy CONFIG_THERMAL_EMULATIONy # 调试用可模拟温度 CONFIG_ROCKCHIP_THERMALy CONFIG_PWM_FANy CONFIG_CPU_FREQy CONFIG_CPU_FREQ_DTy编译并烧写内核镜像后启动时dmesg应看到rockchip_thermal rockchip_thermal: tsadc initialized thermal thermal_zone0: registered as thermal_zone0 pwm-fan fan0: Registered as cooling device thermal thermal_zone0: binding with cpu0_cooling thermal thermal_zone0: binding with fan04.3 验证与调试五步定位 thermal 是否真工作确认 zone 注册成功ls /sys/class/thermal/ # 应看到 thermal_zone0 cat /sys/class/thermal/thermal_zone0/temp # 应输出类似 6500065℃检查 trip point 设置cat /sys/class/thermal/thermal_zone0/trip_point_0_temp # 应为 70000 cat /sys/class/thermal/thermal_zone0/trip_point_0_type # 应为 passive验证 cooling device 绑定ls /sys/class/thermal/thermal_zone0/cdev0 # 应为 cpu0_cooling ls /sys/class/thermal/thermal_zone0/cdev1 # 应为 fan0手动触发 cooling调试用# 强制设 CPU 为最低频state 0 echo 0 /sys/class/thermal/thermal_zone0/cdev0/cur_state # 强制设风扇为最高转速state 15 echo 15 /sys/class/thermal/thermal_zone0/cdev1/cur_state观察 governor 日志需开启 debugecho 1 /sys/module/thermal/parameters/debug dmesg | grep -i thermal.*throttle # 应看到 step_wise 的决策日志实操心得如果temp读数始终为 090% 是 TSADC 时钟未 enable。检查dmesg中是否有failed to get clk tsadc。解决方案在tsadc节点中添加clocks cru SCLK_TSADC, cru PCLK_TSADC并在rockchip,rk3588-cru.h中确认SCLK_TSADC定义正确。4.4 性能调优三组关键参数的实测经验thermal 的效果不取决于“是否启用”而在于参数调优。我在 RK3588 上总结出三组黄金参数参数推荐值依据实测效果trip_point_0_temppassive70℃CPU 持续负载下安全结温上限温度 70℃ 时开始降频避免长期高温老化hysteresis滞后值30003℃小于温度传感器精度±2℃避免在 69.5~70.5℃ 区间频繁触发polling_delay采样间隔2000ms默认→ 1000ms高性能场景需更快响应温度上升速率 5℃/s 时1s 采样比 2s 降低峰值温度 4℃调整方法# 临时修改重启失效 echo 1000 /sys/class/thermal/thermal_zone0/polling_delay # 永久修改在设备树中添加 cpu_thermal { polling-delay-passive 1000; polling-delay-active 500; };特别提醒polling_delay_active应小于passive因为 active cooling风扇响应慢需更早干预。5. 常见问题与排查技巧实录那些官方文档不会告诉你的坑5.1 “温度读数不准”的 5 个根源与诊断树温度不准是 thermal 最常见问题但原因千差万别。我整理了一个快速诊断树温度读数异常 ├─ 读数为 0 或负数 → 检查 TSADC controller 是否 enableddmesg 搜 clk、reset ├─ 读数恒定不变 → 检查 get_temp() 是否被阻塞用 perf record -e sched:sched_switch 看是否卡在 thermal ops ├─ 读数偏高 5~10℃ → 校准表未适配用红外测温枪实测 CPU 表面对比 temp 值修正 rockchip_thermal_table[] ├─ 读数跳变剧烈±10℃ → ADC 电源噪声检查 VDD_TSADC 是否有足够滤波电容实测纹波 10mVpp └─ 读数随 CPU 负载突变 → sensor placement 错误TSADC 应靠近 CPU core而非远离的 PCB 区域典型案例某款工控机temp显示 120℃但红外枪测 CPU 仅 65℃。dmesg发现rockchip_thermal: invalid temperature from TSADC。根源是 TSADC 的参考电压VREF被错误连接到 3.3V 而非 1.8V导致 ADC 量程压缩。更换VREF连线后恢复正常。5.2 “cooling device 不响应”的链路排查法当echo 15 cdev1/cur_state但风扇不转按此顺序排查确认 cooling device 注册成功ls /sys/class/thermal/cooling_device* # 应看到 cooling_device0fan0 cat /sys/class/thermal/cooling_device0/cur_state # 应随 echo 改变检查 PWM 输出用示波器测pwm0引脚若无 PWM 波形 →pwm-fan.c驱动未加载或pwms属性地址错误若有波形但占空比不对 →cooling-levels数组长度与cur_state范围不匹配如cooling-levels只有 8 个值却设cur_state15验证风扇供电# 测量风扇接口电压应为标称值如 12V # 用万用表短接 PWM 信号线与 GND风扇应全速转排除风扇本体故障检查 thermal zone 绑定# 查看 cooling device 是否被 zone 引用 ls /sys/class/thermal/thermal_zone0/cdev* # 应有 cdev1 指向 fan0 # 若无检查设备树中 cooling-maps 的 phandle 是否正确注意cur_state值超出cooling-levels数组长度时内核会静默截断不会报错。这是最隐蔽的 bug 来源。5.3 Governor 切换失败的三大陷阱echo power_allocator /sys/class/thermal/thermal_zone0/policy失败常见原因依赖未启用power_allocator需要CONFIG_THERMAL_POWERCAP和CONFIG_ENERGY_MODEL。检查.config是否启用。energy model 缺失power_allocator需要每个 CPU 的功耗模型EM。在drivers/base/power/energy_model.c中EM 数据来自cpufreq-dt或arm_big_little驱动。若dmesg出现Failed to initialize energy model for cpu0说明 EM 未注册。cooling device 不支持 powerpower_allocator要求 cooling device 实现get_requested_power()回调。cpufreq_cooling.c支持但pwm_fan.c不支持——这意味着你不能用power_allocator同时管理 CPU 和风扇只能选其一。解决方案对纯 CPU 管理场景power_allocator效果极佳对 CPU风扇混合场景坚持用step_wise并通过设备树精细控制cooling-maps的权重。5.4 热关机Critical Shutdown的预防与审计critical temperature reached, shutting down是最严重的 thermal 事件。预防措施设置合理的 critical trip必须低于芯片 datasheet 的 Tjmax结温。RK3588 Tjmax105℃故criticaltrip 应设 ≤100℃留 5℃ 安全裕量。启用 thermal emergency shutdown log# 在内核命令行添加 thermal.emergency_shutdown1 # 启动后/sys/firmware/devicetree/base/thermal/emergency_shutdown 会显示 last shutdown reason审计 shutdown 前 10 秒行为# 开启 ftrace 记录 thermal 事件 echo 1 /sys/kernel/debug/tracing/events/thermal/thermal_zone_trip/enable # 复现问题后dump trace cat /sys/kernel/debug/tracing/trace_pipe | grep trip.*critical我曾处理过一个案例客户设备在 85℃ 时关机但criticaltrip 设为 100℃。trace_pipe显示thermal_zone0: critical temperature reached进一步发现是rockchip_thermal驱动中一个if (temp 85000)硬编码判断——这是厂商 BSP
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进