Linux Thermal Framework内核热管理架构与实践解析

发布时间:2026/10/7 7:49:29
Linux Thermal Framework内核热管理架构与实践解析
功耗子系统的系列写到第九篇终于轮到 thermal framework 了。前面聊 cpufreq、cpuidle 的时候我反复强调过一句话功耗和发热是一枚硬币的两面省电没做好发热就会找上门发热没管住省电策略再漂亮也是空中楼阁。在真实产品里很多工程师对 thermal 的印象就是温度高了就降频但真被问到 thermal framework 怎么组织的、governor 是怎么选的、cdev 是怎么绑上去的能讲清楚的人其实不多。这篇就从通用架构入手把 thermal framework 的骨架、关键数据结构、工作流程和调试方法完整梳理一遍。内容面向正在看内核功耗代码、或者做产品热设计需要和内核层打交道的同学也适合刚接触嵌入式 Linux 想系统了解 thermal 机制的初学者。1. 整体架构与分层设计思路1.1 三层模型zone、cdev 与 governorLinux thermal framework 的核心设计可以抽象成三个角色thermal zone热区、cooling device冷却设备、thermal governor热管理策略。thermal zone 抽象的是一个带温度传感器的区域比如 CPU 封装、电池、壳温传感器的覆盖范围。每个 zone 有自己的一组 trip point温度阈值比如 60 度提示、80 度降频、95 度关机。cooling device 抽象的是任何能影响发热的手段最常见的是 cpufreq 调频、cpu idle 调 idle 状态、CPU 热插拔、风扇转速、GPU 降频等等。governor 则是决策层它根据 zone 上报的温度、trip point 的命中情况决定要不要触发 cooling device以及触发到什么档位。这套三层模型的妙处在于传感器厂商只需要把温度读上来注册成一个 zone硬件工程师只需要把风扇或者调频接口封装成 cdev策略工程师只需要写 governor 逻辑。三者通过内核提供的标准接口对接互不感知对方内部实现。我在实际项目里见过不少刚接触 thermal 的同事上来就想着温度高了我去 call cpufreq 接口把频率降下来这就是没理解框架的意图。正确做法是注册一个 cpufreq 类型的 cdev让 governor 来决定调频的时机和幅度。你现在手写代码干预频率后面换了 governor、改了温控策略代码就全废了。1.2 为什么一定要做解耦而不是直接硬编码可能有人会问一个 SoC 上就那么几个传感器几套降频策略直接写死不好吗非要用框架绕一圈我早年也这么想过直到被现实教育了。第一不同产品的 thermal 策略差异非常大。手机希望表面温度优先优先控制壳温传感器服务器希望性能优先宁可风扇狂转也尽量不降 CPU平板没有风扇只能靠调频和 idle 来压温度。同一个内核要适配这些完全不同的策略单纯的硬编码根本没法维护。框架把策略抽出来做成可配置的 governor产品工程师只需要在 device tree 里调整 trip point 和绑定关系不用动 kernel 代码。第二冷却手段本身是异构的。x86 平台有 ACPI 的 _ACx 被动冷却和 _PSL 主动冷却ARM 平台一般靠 cpufreq 和 thermal 的绑定带风扇的平台还要接 PWM 调速。如果每个平台都各自实现一套温度判断和降温逻辑内核里会出现大量重复代码。thermal framework 把温度感知和降温执行全部抽象成标准接口新平台接入的成本大幅降低。我自己在适配一个新 SoC 的时候最快的路径就是看 vendor 提供的 driver 是注册成 zone 还是 cdev然后补 device tree 配置基本两三天能把整条链路跑通。2. 核心数据结构与注册流程2.1 thermal_zone_device 里面到底装了什么先看 zone 侧的核心结构内核 include/linux/thermal.h 里struct thermal_zone_device的关键字段struct thermal_zone_device { char type[THERMAL_NAME_LENGTH]; /* zone 类型名如 cpu-thermal */ struct device device; /* 标准内核设备模型 */ struct thermal_zone_device_ops *ops; /* 温度读取、trip 操作等回调 */ struct thermal_zone_params *tzp; /* 参数governor 可能用到 */ struct list_head thermal_instances; /* 该 zone 绑定的 cdev 实例列表 */ struct idr idr; /* trip point 管理 */ int num_trips; /* trip 数量 */ int temperature; /* 当前温度单位毫摄氏度 */ int target_temperature; /* governor 选择的 target 温度 */ struct thermal_governor *governor; /* 当前生效的 governor */ ... };ops是 zone driver 必须实现的回调集合典型的有get_temp、set_trip_temp、get_trend、get_trip_type等。其中get_temp是每次轮询都会调用的函数必须保证低延迟和低开销我见过有实现里在 get_temp 里直接去 i2c 读外部传感器每次要几十毫秒直接把 thermal 轮询线程拖垮了。num_trips和 trip point 数组描述了这个 zone 的所有温度阈值。这里有个容易忽略的细节trip 和 cdev 的绑定关系是分层的一个 zone 可以挂多个 trip每个 trip 又可以绑定多个 cdev 实例。这个多对多关系是理解 thermal 框架的关键后面单独章节展开。2.2 cdev 侧的结构与回调冷却设备侧的核心结构struct thermal_cooling_device { int id; char type[THERMAL_NAME_LENGTH]; struct device device; void *devdata; const struct thermal_cooling_device_ops *ops; struct list_head thermal_instances; /* 被哪些 zone 的哪些 trip 引用 */ ... }; 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); };cdev 的接口非常简洁你告诉我能档位到几get_max_state、现在在几档get_cur_state、让我切到某一档set_cur_state。至于档位和频率怎么对应是 cpufreq 一层自己的事。cpufreq_cooling.c 里会在注册时扫描可用的频率点把从 max_freq 到 min_freq 分成若干个 statestate 0 表示最高频不降温state 越大频率越低。我第一次读 cpufreq_cooling 代码的时候最惊讶的就是降温竟然只是把 state 往上加这么简单。但背后有一套 power model 的估算逻辑IPA governor 用的cpufreq_cooling 注册时会填一个em_pdEnergy Model相关的结构。这块后面讲 power_allocator 的时候还要细说。2.3 注册路径与顺序zone 和 cdev 的注册顺序没有严格先后要求因为 thermal framework 提供了解耦的绑定机制。典型流程是这样的/* cdev 侧 */ cdev thermal_of_cooling_device_register(np, cpufreq, cpufreq_dev, cpufreq_cooling_ops); /* zone 侧一般走 device tree */ tzd thermal_zone_device_register(cpu-thermal, num_trips, mask, tz_dev, ops, tzp, passive_delay, polling_delay);在 device tree 为主的 ARM 平台thermal_of_zone_register会把 DT 里的 trip point、cooling map 一次性解析出来然后自动把 cdev 绑定到对应 trip 上。也就是说对于大多数工程师来说你不需要手动调 bind 接口只要把 DT 写对内核启动时这个关系网就建好了。这里要提醒一个坑zone 注册后 goernor 是默认值step_wise如果你想用 power_allocator要么在 DT 里指定thermal-governor属性要么在thermal_zone_params里设置governor_name字段。内核 5.x 之后的版本还有一个全局的governor选择机制会先看 zone 的偏好再看全局配置。调试的时候如果发现跑的 governor 和预期不符第一件事先查sysfs里实际生效的是谁而不是猜代码。3. trip point、绑定机制与 governor 工作逻辑3.1 trip point 的设计与状态机trip point 不只是简单阈值它还带类型。老内核里 trip type 有 4 种active主动冷却对应风扇、passive被动冷却对应降频、hot临界高温、critical致命温度直接触发系统关机或者硬件 reset。关键点是hot 和 critical 不是给 governor 用的而是给紧急路径用的。critical 触发了 thermal 会走thermal_emergency_poweroff直接强制关机hot 在触发时如果设备没有能力继续降温同样会走 emergency 路径。这层保险丝设计非常重要governor 正不正常工作是一回事温度真到危险线能不能兜底是另一回事。我遇到过某平台 vendor 把 critical trip 写进 DT 但没验证结果跑到高温时内核 panic问题排查了很久才发现是 thermal 层主动触发关机导致的。当 zone 温度穿过某个 trip point 时thermal framework 会调用thermal_zone_set_trips更新轮询区间同时通知 governor。governor 根据温度趋势上升/下降决定 cdev 的档位往哪个方向调。这里有个细节轮询不是固定间隔扫描所有 trip而是只关心当前温度附近的那两个 trip。通过set_trips把下一次需要关注的上限下限告诉传感器层温度没跨越就继续 sleep。这套机制叫 interrupt-driven trip crossing能大幅降低轮询功耗。3.2 多对多绑定thermal_instance 是怎么组织的前面提到 zone 和 cdev 是多对多的这个关系在内核里叫thermal_instance对象struct thermal_instance { int id; struct thermal_zone_device *tz; struct thermal_cooling_device *cdev; int trip; unsigned long upper, lower; unsigned long target; ... };一个thermal_instance代表某个 zone 的某个 trip 绑定了某个 cdev。每次温度穿过 tripgovernor 遍历这个 trip 上的所有实例对每个实例计算 target state。上下限upper和lower可以由 DT 的 cooling-map 配置也可以在绑定后动态修改。这个设计让同一组传感器温度可以同时驱动 CPU 降频和风扇提速也允许同一个风扇被多个温度传感器共同控制比如 CPU 温度和 GPU 温度都绑到同一个风扇 cdev 上取最高档位生效。从架构角度看这种关系没有用树状结构而是用扁平列表就是为了方便遍历和动态增删因为运行时用户态可能通过 sysfs 修改绑定关系。3.3 四种常见 governor 的取舍Linux 内核里可选的 governor 不少但实际产品里真正常用的就几种step_wise默认配置按温度趋势和 trip 状态一步一档地调整。温度上升且超过 trip 就逐级降温温度下降则逐级回暖。逻辑简单调起来直观但响应偏保守。bang_bang只有开/关两态常用于风扇。温度过 trip 开回到 hysteresis 区间关。实现最简单缺点是控制曲线粗糙风扇频繁启停。power_allocator基于功耗模型的分配器也叫 IPA。它把目标温度看成预算用 PID 控制器的思路动态计算允许功耗然后把功耗预算按 EMEnergy Model分配给各个 cooling device。控制效果平滑适合对温控要求高的移动设备。user_space内核不做决策只上报温度用户在用户态用 thermal daemon 决定怎么处理。桌面 Linux 上比较常见但嵌入式产品很少用因为多了用户态转发延迟。我个人的建议是能上 IPA 就上 IPA省事而且效果好但前提是 EM 信息得填对。IPA 算不准十个里面有八个是 EM 的 dynamic power coefficient 给的不对。# 查看当前各 zone 使用哪个 governor cat /sys/class/thermal/thermal_zone*/mode cat /sys/class/thermal/thermal_zone*/policy注意policy文件就是 governor 的名字你可以 echo 切换比如echo power_allocator policy但前提是内核编译进了 power_allocator governor。4. Device Tree 配置与绑定关系实例4.1 一份最小可用的 DT thermal 节点ARM64 平台上 thermal 配置几乎全在 DT 里。下面是一份典型的单 CPU 热区配置cpu_thermal: cpu-thermal { polling-delay-passive 250; polling-delay 1000; thermal-sensors tsens0 0; trips { cpu_alert0: cpu-alert0 { temperature 65000; hysteresis 2000; type passive; }; cpu_alert1: cpu-alert1 { temperature 85000; hysteresis 2000; type passive; }; cpu_crit: cpu-crit { temperature 105000; hysteresis 2000; type critical; }; }; cooling-maps { map0 { trip cpu_alert0; cooling-device cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT; }; map1 { trip cpu_alert1; cooling-device cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT; }; }; };polling-delay-passive是被动冷却阶段至少一个 passive trip 被触发的轮询间隔polling-delay是正常状态下的轮询间隔。这里单位都是毫秒数值需要权衡监测精度和功耗太快会让 thermal 轮询线程空转太慢会在快速升温场景下反应迟钝。我经验值是被动阶段 100-250ms、正常阶段 1000ms 比较稳妥。THERMAL_NO_LIMIT表示 cdev 上下限不限制实际使用中也可以写具体档位比如cpu0 0 4表示在这个 trip 下 cdev 只能在 0 到 4 档之间调。产品上常见做法是低温度 trip 限制低档位高温度 trip 逐步放开限制让降温梯度更平滑。4.2 binding 关系在 DT 里怎么生效从这份 DT 可以看到cooling-map 是绑定关系的声明处。map0 表示 cpu_alert0 触发了就调 cpu0 这个 cdevmap1 表示 cpu_alert1 触发了也调 cpu0。同一 cdev 出现在多个 map 中太常见了——多个 trip 控制一个 cdev这在 step_wise 下就是温度越高档位越高的阶梯策略。zones 和 cdevs 都注册完之后内核里thermal_of_build_thermal_zone会遍历 DT 的 cooling-maps为每个 trip 和 cdev pair 创建 thermal_instance。to cool down what with which device at what trip——这就是完整的关系三元组。我在给新人讲 thermal 的时候总说DT 里的 thermal 节点不是在配温度表而是在搭一张关系网。这张网决定了整个系统的温控行为。4.3 多 sensor 多 zone 的联动设计实际 SoC 往往不止一个热区。手机里有 CPU、GPU、电池、充放电 IC、壳温等多个 zone它们之间还有约束关系。比如壳温 zone 触发后不止要降频还要限制充电电流——这时可以把 PWM charger 注册成一个 cdev绑定到壳温 trip 上。还有一种常见的联动是 CPU 和 GPU 共享一个散热预算。这种情况下两个 zone 绑定同一个风扇 cdev内核取的是所有实例里请求的最高档位。这个取 max的逻辑在thermal_cdev_update函数里它遍历整个 cdev 相关的 instance 列表找 target 最大的一个。所以即使两个 zone 温度不同步风扇也总是以最激进的需求运转优先保证最热的那块区域不过温。5. sysfs 接口与用户态调试方法5.1 /sys/class/thermal 全景内核启动后thermal 框架会在 /sys/class/thermal 下暴露两类节点/sys/class/thermal/ ├── thermal_zone0/ # 第一个热区 │ ├── temp # 当前温度毫摄氏度 │ ├── policy # 当前 governor 名称 │ ├── mode # enabled/disabled │ ├── trip_point_0_temp # 第 0 个 trip 的阈值 │ ├── trip_point_0_type # 第 0 个 trip 的类型 │ └── ... ├── thermal_zone1/ ├── cooling_device0/ # 第一个冷却设备 │ ├── max_state # 最大档位 │ ├── cur_state # 当前档位 │ └── type # cdev 类型如 cpufreq └── cooling_device1/这套 sysfs 接口非常方便做快速验证。我在实验室里调的流程一般是先cat温度和 policy 确认链路通了再手动改 cur_state 确认 cdev 能动最后回到 DT 调整 trip 参数做整机验证。5.2 常用调试动作# 查看温度和档位 cat /sys/class/thermal/thermal_zone0/temp cat /sys/class/thermal/cooling_device0/cur_state # 手动强制切 cdev 档位模拟 governor 决策 echo 5 /sys/class/thermal/cooling_device0/cur_state # 切换 governor echo power_allocator /sys/class/thermal/thermal_zone0/policy # 禁用/启用某个 zone echo disabled /sys/class/thermal/thermal_zone0/mode注意手动写 cur_state 只是测试 cdev 本身是否正常不会反映真实策略效果。真正看 governor 行为要盯thermal_zone0/temp和cooling_device0/cur_state的变化曲线。我常用watch -n 1两个终端同时刷一个盯温度一个盯档位升温时档位跟得紧不紧、降温时回落得及时不及时一眼就看出来。5.3 tracing 与日志内核对 thermal 也有 tracepoint 支持。CONFIG_THERMAL_TRACE使能后可以抓# 抓 thermal 温度变化和 trip 触发事件 trace-cmd record -e thermal_temperature -e thermal_zone_triptrace 出来的信息比 dmesg 精确得多能看到每次温度轮询、每个 trip 的命中时间点非常适合排查温度都到 90 度了为何没触发降频这类问题。6. 常见问题与排查实录6.1 温度读不到或者读到 0遇到这种问题先不要怀疑 zone driver先确认传感器子系统是不是正常。检查 dmesg 里有没有传感器初始化失败的日志再用i2cdetect之类工具确认 sensor 芯片在线。实际案例中GPIO 中断没配好导致温度不更新、I2C 时钟频率太低读超时的情况非常多见。还有一点容易踩get_temp的返回值单位是毫摄氏度而 DT 里的 temperature 单位是摄氏度。有的传感器驱动天然返回整数摄氏度直接注册进去会导致温度显示放大 1000 倍。判断方法很简单cat /sys/class/thermal/thermal_zone0/temp看数值量级正常应该是 30000-100000毫摄氏度如果看到 30 或 31基本就是单元转换的问题。6.2 governor 不切换或者策略不生效最常见原因是从没检查过实际生效的 governor。可能你 DT 里写了thermal-governor power_allocator但内核没开CONFIG_THERMAL_GOV_POWER_ALLOCATOR于是回退到 step_wise。另一个高频问题是因为被动轮询没开。polling-delay-passive配置成 0 的话即使温度越过了 passive trip框架也可能不进入被动轮询模式。这就导致温度一直涨但 cdev 档位不动。排查时先确认对应的 trip 是否被命中再看cur_state有没有变化如果 trip 命中了但 cdev 没动去查binding信息。6.3 降温响应太慢或者太激进这个基本属于调参问题但参数不只是 trip 温度。step_wise 下激进通常是因为 cdev 最大档位对应频率下降幅度太大可以考虑在 DT 的 cooling-device 节点里设置更小范围的上限太慢则往往是轮询间隔过大或者 trip 的 hysteresis 太大导致温度回落要等很久才释放档位。如果用了 IPA先检查power_allocator的sustainable_power是否和实测底功耗匹配这个值的偏差会直接导致 PID 输出整体偏移。简易标定方法在待机稳定状态下读一次整机功耗通常可以作为 sustainable_power 的参考量。6.4 重启后配置丢失有用户反映 sysfs 里改好的 policy 或 cur_state重启后恢复默认。这不是 bugthermal 的策略配置本来就是运行时属性固化配置应该走 DT 或内核配置参数。这条提醒主要是为了让刚接触的同学理解 sysfs 的定位sysfs 是调试接口和运行时控制接口不是持久化配置仓库。你该修改的是 DT 里的默认值而不是保存 sysfs 的 echo 命令到开机脚本里骗自己。7. 对 thermal framework 的一点实战心得最后分享几个我这些年踩过坑后的体会。第一个体会是thermal framework 的代码阅读路径一定从 sysfs 和 device tree 开始不要一上来就扎进 governor 源码。先通过 sysfs 确认链路通不通、温度准不准、cdev 动没动再倒回去读源码你会发现自己对thermal_zone_device_update、thermal_zone_set_trips这些函数理解特别快。第二个体会是绑定关系是整个框架的灵魂。理解了 thermal_instance 和 cooling-map 的关系你就理解了 thermal framework 90% 的宏观逻辑。第三个体会是真正产品级的热管理永远不是内核单层能搞定的。内核 thermal framework 提供的是机制传感器布板位置、导热路径、策略参数这些属于系统和硬件全局的决策需要热设计工程师、驱动工程师和内核工程师一起把参数调出来。框架做得再好传感器布在错误的位置策略也是瞎忙活。这套 thermal 架构从 3.x 内核演化到现在核心抽象一直没变但细节越来越丰富。如果后面有时间我打算单独写一篇 power_allocator 的源码级拆解把 PID 参数怎么算、EM 怎么建模讲透那部分内容单独撑一篇都不过分。对 IPA 感兴趣的同学可以先去读drivers/thermal/gov_power_allocator.c配合include/linux/energy_model.h一起看比任何二手转述都来得直接。