1. 从一次温控翻车说起:thermal framework 到底管什么
前阵子帮朋友排查一块嵌入式板子,现象很典型:设备跑高负载任务不到三分钟,CPU 频率就被死死压在低位,性能直接腰斩,但外壳摸上去并不烫。第一反应是散热没做好,换了导热硅脂、加了风扇,问题照旧。后来把/sys/class/thermal/目录下的信息拉出来一看,才发现是 thermal zone 的温度读数被某个错误的 trip point 提前触发了降频,跟物理散热压根没关系。
这个案例几乎是我接触 Linux 功耗子系统以来,反复遇到的同一类问题:thermal framework 在后台默默工作,一旦配置或理解有偏差,表现出的症状却完全不像"温度问题"。它可能表现为性能骤降、设备莫名重启、充电变慢、风扇狂转,甚至系统卡死。而大多数人排查时根本不会第一时间想到去翻 thermal 相关的节点。
所以这篇想做的事情很明确:把 Linux 内核里 thermal framework 的通用架构从头到尾梳理一遍。不是罗列 API,而是讲清楚它由哪些部分组成、每部分承担什么职责、数据怎么流动、驱动开发者需要填哪些坑、系统调优时该盯哪些节点。关键词里的Linux 内核、thermal framework、功耗子系统、通用架构,正好对应本文的四个核心视角:内核视角、框架视角、功耗视角、架构视角。
适合谁看?如果你在做嵌入式 Linux 驱动、BSP 移植、功耗调优,或者单纯想搞明白/sys/class/thermal下面那一堆文件到底代表什么,这篇应该能帮你把散落的知识点串成一条线。如果你只是偶尔用sensors命令看看温度,那也能从里面理解到读数背后的机制,知道为什么有时候读数会"骗人"。
需要提前说明的是,thermal framework 本身是一个持续演进的子系统,不同内核版本在细节上有差异。本文梳理的是通用架构层面的东西,具体到某个版本的行为,还是要以你手上的内核源码为准。我下面提到的结构、流程、节点,都是基于常见实践和主流版本的共性总结,遇到版本差异我会尽量点出来。
2. thermal framework 的四层骨架:从传感器到执行器
要理解 thermal framework,最有效的方式不是背 API,而是先建立一张"数据流地图"。整个框架可以拆成四个层次,从上到下依次是:传感器层、thermal zone 抽象层、governor 决策层、cooling device 执行层。这四层之间通过内核内部的注册与回调机制连接,形成一个闭环。
2.1 传感器层:温度是从哪里读出来的
最底层是温度传感器。它可能来自 SoC 内部的温度传感模块(比如很多 ARM 平台集成的 TSADC)、外部的 I2C/SPI 温度芯片(如常见的 LM75、TMP102 系列)、或者通过其他子系统间接获取的温度源。这一层的核心任务是:提供一个可以被内核调用的温度读取接口。
在设备树里,传感器通常以独立节点的形式描述,然后被 thermal zone 引用。比如一个典型的 SoC 内部传感器节点会声明寄存器地址、时钟、校准参数等。驱动加载后,会向 thermal framework 注册一个thermal_zone_device,并绑定一个get_temp回调。框架每次需要温度时,就调用这个回调去实际读取硬件。
这里有个容易被忽略的点:温度读取是有开销的。如果传感器挂在慢速总线上,频繁读取会拖累系统。所以框架内部有轮询间隔(polling delay)的概念,不是每次查询都真的去读硬件。理解这一点,对后面分析"为什么温度变化有延迟"很关键。
2.2 thermal zone 抽象层:把物理区域变成内核对象
thermal zone 是整个框架的核心抽象。一个 thermal zone 代表一个"需要被热管理的物理区域",比如 CPU 集群、GPU、电池、外壳表面等。它把传感器、触发点(trip point)、冷却设备(cooling device)三者绑定在一起,形成一个完整的管理单元。
每个 thermal zone 在内核里对应一个thermal_zone_device结构,注册后会出现在/sys/class/thermal/thermal_zoneN/目录下。这个目录里的文件就是用户空间观察和控制 thermal 行为的主要入口。我列几个最关键的:
| 文件/节点 | 含义 | 典型用途 |
|---|---|---|
type | thermal zone 的类型名 | 区分是 CPU、GPU 还是电池 |
temp | 当前温度(毫摄氏度) | 实时监控 |
mode | 工作模式(enabled/disabled) | 临时关闭热管理 |
policy | 当前使用的 governor | 查看/切换策略 |
trip_point_N_type | 第 N 个触发点类型 | 查看触发条件 |
trip_point_N_temp | 第 N 个触发点温度 | 查看/调整阈值 |
cdevN_cur_state | 第 N 个冷却设备当前状态 | 查看降频档位 |
trip point 是 thermal zone 里最需要理解的概念。它本质上是"温度阈值 + 触发动作"的组合。常见的 trip 类型有passive(被动降频)、active(主动散热,比如开风扇)、critical(临界,通常触发关机)、hot(过热警告)。当温度越过某个 trip point,框架就会通知 governor 去执行对应动作。
2.3 governor 决策层:温度越线之后谁来拍板
governor 是 thermal framework 的"大脑"。它决定当温度达到某个 trip point 时,具体该怎么调整冷却设备。内核里常见的 governor 有几种,各自适用场景不同:
- step_wise:最常用也最直观。温度每越过一个档位,就把冷却设备状态调整一档,逐步加码或逐步回退。适合大多数嵌入式场景。
- power_allocator:基于功耗预算做分配,把有限的散热能力按权重分给各个 cooling device。适合多核、多设备协同的复杂平台。
- fair_share:在多个 cooling device 之间公平分配降温任务。
- bang_bang:最简单的开关式控制,温度高了就全开,低了就全关。容易震荡,一般用于风扇这类设备。
- user_space:把决策权交给用户空间,内核只负责上报。适合需要精细自定义策略的场景。
governor 的选择通过policy节点体现,也可以在设备树或内核配置里指定。选错 governor 是很多"降频行为诡异"问题的根源。比如一个本该用 step_wise 的场景用了 bang_bang,就会出现温度在阈值附近反复横跳、频率忽高忽低的现象。
2.4 cooling device 执行层:降频、关核、开风扇都归它管
cooling device 是实际执行降温动作的部件。它可以是:
- CPU 频率调节器:通过 cpufreq 降低频率或电压。
- CPU 热插拔:直接关掉部分核心。
- GPU 频率调节:降低 GPU 工作频率。
- 风扇控制器:调整风扇转速。
- 充电电流限制:降低充电功率减少发热。
- 设备节流:限制某些外设的工作强度。
每个 cooling device 注册时会声明自己的状态数量(max_state)和每个状态对应的实际效果。框架通过set_cur_state回调来调整它。状态值越大,通常代表降温力度越强。
这四层的关系可以这样理解:传感器提供"体温",thermal zone 定义"发烧标准",governor 决定"怎么退烧",cooling device 负责"实际吃药"。任何一环出问题,最终表现都是温度控制失效或性能异常。
3. 设备树里的 thermal 描述:绑定关系是怎么建立的
搞清楚了四层骨架,接下来要回答一个实操中绕不开的问题:这些组件是怎么被"组装"到一起的?答案主要藏在设备树(Device Tree)里。对于 ARM 等平台,thermal 相关的绑定关系几乎全靠设备树描述,理解这部分是 BSP 移植的基本功。
3.1 thermal zone 节点的标准写法
一个典型的 thermal zone 节点长这样(以某 ARM 平台为例,具体属性名以你的内核绑定文档为准):
thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <100>; polling-delay = <1000>; thermal-sensors = <&tsadc 0>; trips { cpu_alert0: cpu-alert0 { temperature = <70000>; hysteresis = <2000>; type = "passive"; }; cpu_crit: cpu-crit { temperature = <100000>; hysteresis = <0>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; }; }; };这里面几个关键属性值得逐个拆解:
polling-delay-passive:进入被动降温状态后的轮询间隔(毫秒)。温度已经越线了,就得盯紧点,所以这个值通常比正常轮询小。polling-delay:正常状态下的轮询间隔。设太大反应迟钝,设太小浪费 CPU。thermal-sensors:引用具体的温度传感器节点,可以引用多个。trips:定义所有触发点,每个包含温度、迟滞(hysteresis)、类型。cooling-maps:把 trip point 和 cooling device 关联起来,这是"越线后找谁降温"的映射表。
3.2 hysteresis 这个参数为什么不能随便填
hysteresis(迟滞)是我见过最容易被忽视、又最容易引发诡异现象的参数。它的作用是:当温度回落到 trip point 以下时,不是立刻解除降温,而是要再低一个 hysteresis 值才解除。
举个例子:trip point 设在 70°C,hysteresis 设 2°C。温度升到 70°C 触发降频,之后温度降到 69°C 时不会立刻恢复,要降到 68°C 以下才恢复。这样做的目的是防止温度在阈值附近反复穿越,导致降频/恢复频繁抖动。
如果 hysteresis 设成 0,或者设得太小,就会出现"温度在 70°C 上下波动,频率跟着反复横跳"的现象。用户感受到的就是性能忽好忽坏,非常难受。我一般建议 hysteresis 至少留 2°C 到 5°C 的余量,具体看散热系统的响应速度。
3.3 cooling-maps 的匹配逻辑与常见错误
cooling-maps 是把 trip 和 cooling device 绑定的地方。每个 map 条目包含一个trip引用和一个cooling-device引用。cooling-device后面的两个参数是状态范围(最小状态和最大状态),THERMAL_NO_LIMIT表示不限制。
这里常见的错误有几类:
第一类是引用了不存在的 cooling device。比如设备树里写了&cpu0,但 cpufreq 驱动没加载或者没注册成 cooling device,结果就是 trip 触发了却没人执行降温,温度继续飙升直到 critical 关机。
第二类是状态范围填反了。有的驱动状态值越大降温越强,有的相反。填错范围会导致降温动作方向错误,越降越热。
第三类是多个 trip 映射到同一个 cooling device 但状态范围重叠。这会让 governor 的决策变得混乱,不知道该用哪个档位。
排查这类问题,最直接的办法是看/sys/class/thermal/thermal_zoneN/cdevN_cur_state的值有没有随温度变化。如果温度越线了但 cur_state 一直是 0,基本可以确定映射没生效。
4. 温度越线之后:一次完整的触发链路拆解
前面讲了静态结构,这一节讲动态过程。当温度真的越过一个 trip point,内核里到底发生了什么?把这条链路走通,排查问题时就能准确定位卡在哪一环。
4.1 从轮询到 governor 被唤醒
thermal zone 注册后,框架会启动一个延迟工作队列(delayed work)来周期性读取温度。每次读取后,框架会把当前温度和所有 trip point 比对,判断是否有 trip 被穿越。
这里有个细节:框架不是简单地"温度大于阈值就触发",而是会记录上一次的 trip 状态,只有发生状态变化(从没触发到触发,或从触发到解除)时才通知 governor。这就是为什么 governor 不会每轮都被调用,也是 hysteresis 能起作用的原因。
一旦检测到 trip 状态变化,框架会调用当前 governor 的throttle回调,把 thermal zone、trip 信息传进去。governor 拿到这些信息后,开始决策。
4.2 step_wise 的决策过程
以最常用的 step_wise 为例,它的决策逻辑大致是这样的:
- 判断当前 trip 是升温触发还是降温解除。
- 如果是升温触发,找到这个 trip 关联的所有 cooling device,把每个设备的状态往上调一档(不超过 max_state)。
- 如果是降温解除,把状态往下调一档(不低于 0)。
- 如果温度继续升高,越过更高的 trip,重复上述过程,继续加档。
这个"逐步加码"的设计很符合直觉:温度越高,降温力度越大。但它也有个特点——反应是渐进的。如果散热系统响应慢,而温度上升快,可能在 governor 还没加到足够档位时,温度就已经冲到 critical 了。所以 trip point 的档位设计要留足缓冲。
4.3 cooling device 状态改变后发生了什么
governor 决定调整某个 cooling device 的状态后,框架会调用该设备的set_cur_state回调。对于 CPU 频率冷却设备来说,这个回调最终会作用到 cpufreq 子系统,限制 CPU 的最大频率。
这里要注意一个耦合关系:thermal 和 cpufreq 是两个独立子系统,通过 cooling device 接口连接。thermal 只负责说"现在需要降到第 3 档",具体降到多少频率是 cpufreq 根据 cooling device 的状态映射决定的。这个映射关系在 cpufreq 驱动里定义,不同平台不一样。
所以当你看到"温度越线了,频率确实降了,但降得不够"时,问题可能不在 thermal,而在 cpufreq 的 cooling device 状态映射上。反过来,"频率降了但温度没降下来",那可能是散热设计或 trip 档位的问题。
4.4 critical trip 的特殊处理
critical 类型的 trip 比较特殊。它不经过 governor 的常规决策流程,而是直接触发内核的热关机机制。当温度达到 critical 阈值,框架会调用thermal_zone_device_critical,最终走 orderly_poweroff 或直接触发硬件关机。
这个机制是最后一道防线,正常情况下不应该被触发。如果你的设备经常因为 critical 关机,说明前面的 passive/active 降温措施完全不够用,需要重新审视散热设计或 trip 配置。把 critical 阈值设得很高来"避免关机"是掩耳盗铃,硬件该坏还是会坏。
5. 用户空间能看到什么:sysfs 节点实战解读
对大多数开发和运维人员来说,跟 thermal framework 打交道最多的入口就是 sysfs。这一节把常用节点和它们的实际含义讲透,方便你排查问题时快速定位。
5.1 读懂 /sys/class/thermal 目录结构
进入/sys/class/thermal/,通常会看到两类目录:thermal_zoneN和cooling_deviceN。前者是热区,后者是冷却设备。每个目录下都有若干属性文件。
一个实用的排查顺序是:
- 先看
thermal_zoneN/type,确认这个 zone 管的是哪个区域。 - 看
thermal_zoneN/temp,确认当前温度读数是否合理。 - 看
thermal_zoneN/policy,确认用的哪个 governor。 - 看所有
trip_point_N_type和trip_point_N_temp,确认阈值配置。 - 看
cdevN_cur_state,确认降温设备当前档位。 - 对照
cooling_deviceN/type和cooling_deviceN/max_state,确认设备能力。
这一套走下来,基本能判断出 thermal 系统是否在正常工作。
5.2 温度读数的单位陷阱
temp节点的单位是毫摄氏度,不是摄氏度。也就是说读到 45000 表示 45°C。这个单位在脚本处理时特别容易出错,我见过不止一次有人写监控脚本时忘了除以 1000,结果阈值判断完全错乱。
同样,trip_point_N_temp也是毫摄氏度。写自动化脚本时,统一按毫摄氏度处理,最后展示时再转换,能避免很多低级错误。
5.3 临时关闭热管理的正确姿势
调试性能问题时,有时候需要临时关掉热管理,排除降频干扰。方法是往mode节点写disabled:
echo disabled > /sys/class/thermal/thermal_zone0/mode但这里必须强调:这只是调试手段,绝对不能作为长期方案。关掉热管理意味着硬件失去了保护,高负载下可能真的烧毁。我一般只在实验室环境、短时间验证时用,验证完立刻恢复:
echo enabled > /sys/class/thermal/thermal_zone0/mode5.4 用脚本做温度趋势监控
单次读温度意义不大,看趋势才有价值。下面这个简单脚本可以周期性打印所有 thermal zone 的温度,方便观察变化:
#!/bin/bash while true; do for zone in /sys/class/thermal/thermal_zone*; do type=$(cat $zone/type 2>/dev/null) temp=$(cat $zone/temp 2>/dev/null) if [ -n "$temp" ]; then printf "%-20s %d.%03d C\n" "$type" $((temp/1000)) $((temp%1000)) fi done echo "---" sleep 2 done跑起来之后,配合压力测试,就能直观看到温度爬升和降频触发的对应关系。这比盯着单个数字有用得多。
6. 移植和调优时最容易踩的几个坑
前面偏原理和结构,这一节讲实操。下面这些坑,有的是我自己踩过的,有的是帮别人排查时反复见到的,都是文档里不太会写、但实际项目中很要命的东西。
6.1 传感器读数和真实温度对不上
最常见的问题是温度读数明显偏离实际。可能的原因有几个:校准参数没配、传感器采样点离热源太远、或者读的是芯片结温而不是环境温度。
芯片结温(junction temperature)通常比外壳温度高不少,这是正常的。但如果结温读数高到离谱,比如待机就 90°C,那就要检查校准数据是否正确写入。很多 SoC 的传感器需要出厂校准值,这些值存在 efuse 或 OTP 里,驱动要负责读出来并参与计算。如果校准没做,读数就是错的。
排查方法:在已知环境温度下(比如室温 25°C),让设备待机一段时间,看读数是否接近合理范围。如果偏差超过 10°C,基本可以确定校准有问题。
6.2 trip point 档位设计不合理导致性能断崖
有些平台的 trip 档位设计得很粗,比如只有 70°C 和 100°C 两个点。结果就是温度一到 70°C,降频力度直接拉满,性能断崖式下跌。用户感受就是"用一会儿就卡"。
合理的做法是设置多个渐进档位,比如 65°C、75°C、85°C 各一档,每档降温力度递增。这样温度上升时性能是平滑下降的,而不是突然掉下去。当然档位也不是越多越好,太多会增加 governor 的决策开销,一般 3 到 5 档比较合适。
6.3 多 thermal zone 之间的相互干扰
复杂平台往往有多个 thermal zone,比如 CPU、GPU、电池各一个。如果它们的 cooling device 有重叠(比如都控制 CPU 频率),就可能出现相互干扰:CPU zone 降了频,GPU zone 觉得温度降了又恢复,结果 CPU 温度又上去。
处理这类问题,要么在设计上让各 zone 的 cooling device 尽量不重叠,要么用 power_allocator 这类能统一协调的 governor。用 step_wise 管多个重叠 zone,很容易出现"按下葫芦浮起瓢"的情况。
6.4 轮询间隔设置的两难
polling-delay设大了,温度变化反应慢,可能错过最佳降温时机;设小了,频繁读传感器增加系统开销,尤其在慢速总线上更明显。
我的经验值是:正常轮询 500ms 到 1000ms,被动降温状态下 100ms 到 200ms。这个范围在大多数平台上能兼顾响应速度和开销。如果传感器读取特别慢(比如 I2C 上挂了很多设备),可以适当放宽,但要相应地把 trip 档位设计得更保守,留足缓冲。
6.5 内核版本差异带来的行为变化
thermal framework 在不同内核版本间有过不少调整,比如 governor 的默认选择、sysfs 节点的命名、设备树绑定的属性名等。从老版本内核迁移到新版本时,这些差异可能导致原本工作的配置失效。
我的建议是:移植 thermal 配置时,一定要对照目标内核版本的绑定文档(Documentation/devicetree/bindings/thermal/)重新核对一遍,不要直接照搬老版本的设备树。尤其是 trip 类型和 cooling device 的引用方式,改动比较频繁。
7. 把 thermal 放进整个功耗子系统里看
最后想跳出 thermal 本身,聊聊它在整个 Linux 功耗子系统里的位置。这样能帮你建立更完整的认知,排查问题时也知道该往哪个方向找。
Linux 的功耗管理是个多子系统协作的体系,thermal 只是其中一环。跟它关系最密切的有这么几个:
- cpufreq:负责 CPU 频率和电压调节,是 thermal 最主要的降温执行者。
- cpuidle:负责 CPU 空闲状态管理,影响待机功耗和发热。
- devfreq:负责其他设备(GPU、内存控制器等)的频率调节,也可能作为 cooling device。
- regulator:负责电压调节,跟 cpufreq 配合实现 DVFS。
- PM QoS:提供性能约束接口,thermal 的降频决策最终会体现为对性能的约束。
它们之间的关系是:thermal 感知温度,通过 cooling device 接口去影响 cpufreq、devfreq 等执行者,执行者再通过 regulator 等底层机制真正改变硬件状态。任何一环的配置或驱动有问题,最终都会表现为热管理失效。
理解了这层关系,排查问题时思路就清晰了:温度越线但没降频,先查 cooling device 映射;降频了但温度不降,查散热和 trip 档位;温度读数本身不对,查传感器和校准。每一层都有对应的排查入口,不用盲目乱试。
thermal framework 的通用架构,说到底就是"感知—决策—执行"这个闭环在内核里的具体实现。把这四层骨架、设备树绑定、触发链路、sysfs 节点这几块吃透,绝大多数热管理相关的问题都能自己定位。剩下的就是具体平台的细节差异,那部分只能靠读源码和实测来补。