写到这里,已经是这个 Linux 内核功耗子系统系列的第九篇了。前面几篇我们聊了 cpufreq、cpuidle、PM domain 这些功耗管理的核心组件,这次终于轮到 thermal framework。说实话,thermal 这块在功耗子系统中属于"兜底"的角色——前面的组件都在想办法省电,thermal 是在省电手段失效、芯片真的热起来的时候,用降频、限流这些"粗暴"手段把温度摁回去。但恰恰因为它是最后一道防线,它的架构设计反而非常值得仔细看一遍。
这篇文章我打算从框架的整体分层讲起,把 thermal_zone_device、thermal_cooling_device、thermal_instance 这几个核心对象之间的关系理清楚,然后沿着"温度采集 -> trip 判定 -> governor 决策 -> cooling device 执行"这条链路走一遍,最后结合 device tree 配置和 sysfs 调试接口,聊几个我在实际项目中踩过的坑。适合已经对 Linux 驱动模型有基础、想系统梳理 thermal 框架的读者。
1. 为什么需要一套"通用温控框架":从散热策略到内核抽象
1.1 没有框架的年代:各家 SoC 各写各的
在 thermal framework 成熟之前,芯片厂商的温控逻辑基本是"各自为政"。高通、三星、海思各自在自家 SoC 的底层驱动里塞了一套温度监控和降频逻辑,有的放在 msm_thermal 里,有的放在 exynos_thermal 里,有的是在 cpufreq 驱动里偷偷改频率表。这样做的问题很明显:物料清单上换一颗传感器就要重写一遍策略,同一个降频算法在三个平台上被复制粘贴了三遍,而且系统工程师想从 user space 看一眼当前温度,得先搞清楚这个平台用的是哪一套私有接口。
内核社区最后给出的答案,就是把这套东西抽象成三个角色:温度传感器/温区(thermal zone)、降温手段(cooling device)、控制策略(governor)。 sensor 负责报温度,cooling device 负责执行降温动作,governor 负责在"温度超过阈值了,该降多少"这件事上做决策。三者的关系通过 thermal_instance 绑定起来,形成一张可配置的网。
1.2 thermal framework 解决的三个核心问题
第一个问题是温度数据的统一。不管底层是 NTC 热敏电阻、芯片内部的 TSENS 传感器,还是 PMIC 上报的温度,框架层面都收敛成一个 thermal_zone_device,暴露统一的 ops 接口,通过 get_temp() 回调拿到温度,单位统一为毫摄氏度(millicelsius)。上层不需要关心传感器是 I2C 的还是 SPI 的还是寄存器直读的。
第二个问题是策略与硬件解耦。governor 是纯软件层面的东西,不关心你用的是哪颗 SoC。同样是 trip 点触发,step_wise 会一步一步往下调,power_allocator 会算一个精确的功耗预算,fair_share 按权重分。SoC 厂商只需要把 zone、trip、cooling device 在 device tree 里描述清楚,选哪个策略是运行时的事。
第三个问题是执行器标准化。cpufreq 可以降频,cpuidle 可以限制进入深层 idle,devfreq 可以限带宽,风扇可以调速,这些完全不同的执行方式,统一抽象成 cooling device 的 set_cur_state/get_cur_state 接口。cooling device 的"状态"是一个 0 到 max_state 的整数,0 表示不干预,数值越大降温越激进。至于状态 3 对应的到底是 1.2GHz 还是 1.0GHz,那是各 cooling device 驱动自己翻译的事。
1.3 整体分层:传感器、策略、执行器
把整个框架从下往上分,大致是这么四层:
- 底层:传感器驱动和 cooling device 驱动,对应 thermal_zone 的 ops 和 cooling device 的 ops,它们通过注册接口挂进框架。
- 核心层:thermal core,包含 thermal_zone_device、thermal_cooling_device、thermal_instance 的生命周期管理,以及 thermal_zone_device_update() 这个核心入口。
- 策略层:governor,注册到核心层的 governor_list 里,每个 thermal zone 在运行时指定使用哪个 governor。
- 上层:sysfs 接口、netlink 事件、device tree 解析,以及用户空间的 thermal daemon。
这四层之间接口都很薄,核心层只有几十个导出函数。我个人的理解是,thermal framework 本质上就是一个"注册表 + 事件循环 + 策略插槽"的组合体,它本身不包含任何具体的温控智慧,智慧都在 governor 和驱动里。
2. 三个核心对象与一张关系网:thermal_zone、cooling_device、instance
2.1 thermal_zone_device:温区的"大脑"
每个 thermal_zone_device 在 struct 里都有一堆字段,但理解它最关键的是抓住三类信息:
struct thermal_zone_device { struct device *device; const struct thermal_zone_params *tzp; const struct thermal_zone_ops *ops; struct list_head thermal_instances; struct thermal_governor *governor; struct thermal_trip trips[]; // 或者通过 ops 动态获取 int temperature; int prev_temperature; int passive_delay; int polling_delay; ... };ops 是驱动侧需要实现的回调,最重要的三个是 get_temp()、get_trip_temp()/get_trip_type()(在 DT 支持的平台上通常不用驱动实现,直接从 DT 解析)、set_mode()。一个温区的名字(如 "cpu-thermal"、"gpu-thermal"、"battery-thermal")就是 sysfs 里的目录名,同一个 SoC 上可以有多个 zone,每个 zone 独立跑自己的策略循环。
需要注意的一个细节是,thermal zone 的温度上报有两种模式:轮询模式和中断模式。轮询模式下,内核会启动一个延迟工作项,每隔 polling_delay(被动调温时切换成 passive_delay)毫秒调一次 get_temp();中断模式下,传感器驱动在温度跨越阈值时主动调用 thermal_zone_device_update() 触发一次处理。现在的 SoC 基本都有硬件中断,但 framework 里轮询这条路径依然保留着,很多平台为了省事仍然在用。
2.2 thermal_cooling_device:降温手段的抽象
cooling device 的抽象非常简洁,核心就是三个回调:
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); };get_max_state 返回最大档位,比如 cpufreq cooling 可能支持 16 档;set_cur_state 是 governor 调度的结果,值越大冷却越强。在 cpufreq cooling 里,set_cur_state 内部会把 state 换算成频率上限,然后调用 cpufreq 接口限制最大频率;在 PWM 风扇里,state 直接对应占空比。
这里有个经常被误解的点:cooling device 的 state 和 CPU 频率不是一一对应的线性关系。cpufreq cooling 的实现里有一个频率表,state 是频率表的下标,state 越大对应的频率越低。也就是说,降温档位 0 是"不限制频率",最大档位是"锁到最低频率"。中间档位的频率分布取决于 OPP 表,可能不是均匀的。
2.3 thermal_instance:把温度和手段绑在一起的"路由表"
说实话,我第一次看 thermal_instance 这个结构时觉得它是多余的——不就是把 zone 和 cdev 绑在一起吗?实际上它才是整个框架的"路由表"。一个 zone 可以绑定多个 cdev,一个 cdev 也可以服务多个 zone,比如 GPU zone 和 CPU zone 共享一个风扇。如果不加 instance 这层中转,多对多的关系根本无法表达。
struct thermal_instance { struct thermal_zone_device *tz; struct thermal_cooling_device *cdev; struct thermal_trip trip; // 权重、类型是该 instance 自己的 unsigned long upper; // 该 instance 允许设置的上限档位 unsigned long lower; // 下限档位 unsigned long target; // governor 最近一次算出来的目标档位 u32 weight; // 用于 fair_share / power_allocator ... };每个 instance 还有自己的 upper/lower 限位。比如同一个 cpufreq cooling device,在 CPU zone 里的 upper 可能是 16(完全放开),但在 battery zone 里的 upper 可能是 8(最多只允许降到 8 档),因为电池过热时要限制 CPU,但不希望把性能压得太狠。weight 则用于多个 instance 共享同一个 cdev 时按权重分配降温幅度。
2.4 thermal_trip 与 trip 的四种类型
trip 是温控逻辑里最核心的"阈值点"。每个 thermal zone 可以有一组 trip,每个 trip 有自己的温度阈值和类型:
- active:主动散热设备介入点,通常是风扇开启的阈值。
- passive:被动散热介入点,通常是开始降频的阈值。对应 devicetree 里的 passive-delay。
- hot:达到这个点说明被动散热已经压不住了,可能会采取更激进的手段(比如通知用户空间)。
- critical:最后防线,到这个点系统要执行紧急关机(orderly_poweroff),防止硬件损坏。
内核在 __thermal_zone_device_update() 里逐个检查 trip,把当前温度与每个 trip 阈值做比较,然后调用 handle_thermal_trip() 通知 governor 做决策。每个 trip 还有一个 hysteresis(迟滞)参数,这个参数在后面讲工作循环时单独展开。
3. 温度上报与 trip 命中:thermal_zone 的工作循环
3.1 温度从哪来:get_temp 回调与轮询机制
thermal_zone_device_register() 之后,框架会根据 mode 字段决定是轮询还是中断驱动。轮询模式下,注册时如果 polling_delay 大于 0,就会 schedule 一个 delayed work(thermal_zone_device_check),这个 work 执行时调用 thermal_zone_device_update() 做一次完整巡检,然后根据当前状态重新调度自己。
拿到温度的核心路径是 thermal_zone_get_temp(),它套了一层 mutex 后调用 ops->get_temp()。驱动侧需要把裸的 ADC 值或者寄存器值换算成毫摄氏度填到参数指针里。这一步有个约定:get_temp() 返回 0 表示成功,非零会让框架认为温度读取失败。失败时,thermal_zone_device_update() 是不会继续往下跑 trip 判定和 governor 决策的,这一点是我后面要说的坑之一。
3.2 一次完整的 thermal_zone_device_update
thermal_zone_device_update() 是框架内部最重要的入口函数,逻辑大概是:
- 调 thermal_zone_get_temp() 拿当前温度,失败则跳过错峰。
- 更新 zone 的 temperature 字段,把旧值存到 prev_temperature。
- 对于每个 trip,调 thermal_zone_trip_up() / thermal_zone_trip_down() 判断温度是否跨过 trip 阈值。
- 如果发生了 trip 状态变化,或者处于持续降频状态,则调用 governor 的 throttle() 回调,传入当前温度和各 trip 状态。
- 根据 zone 状态更新下一次 poll 的 delay(passive 时用 passive_delay,平时用 polling_delay)。
这段逻辑在 5.15 之后的内核里经历过一次重构,原来在 thermal_zone_device_update() 里直接铺开的 for_each_trip 循环和 governor 调用被拆得更细,但总体流程没变。理解这个入口函数,基本就理解了 thermal 框架的运行时主干。
3.3 hysteresis(迟滞)为什么重要
hysteresis 这个概念在温度控制里非常关键,但很多第一次接触的人会忽略它。它的作用是防止温度在阈值附近反复横跳导致风扇频繁启停或者频率频繁抖动。
举个例子:trip 阈值是 85 度,没有 hysteresis 时,温度到 85.1 度触发降温,风扇开始转,温度降到 84.9 度就解除,风扇停,然后温度又升回 85.1 度……风扇就在启停之间疯狂振荡。给这个 trip 配一个 5000 毫摄氏度的 hysteresis,意味着从"未触发"到"触发"要温度升到 85 度,但从"触发"回到"未触发"要温度降到 80 度以下。这 5 度的回差区间给系统留了稳定的工作窗口。
内核里 trip 的 hysteresis 值可以从 DT 的 hystersis 属性读出来(这个拼写错误一直保留到了现在,官方文档也这么写),也可以在 sysfs 里直接改。
4. governor 决策机制:step_wise、fair_share、power_allocator
4.1 governor 在内核里到底干什么
governor 的核心就是一个 throttle() 回调,由核心层的 update 流程在 trip 状态变化时调用。governor 需要根据"当前温度是多少、哪些 trip 已经被越过、上次给每个 instance 定的 target 是多少",算出新的 target 值,然后通过 thermal_instance_set_trips() 之类的接口把 target 写回 instance,最终由核心层调 cdev->ops->set_cur_state() 落到执行器上。
每个 governor 注册时会声明自己支持的特性,比如 power_allocator 需要 zone 有足够的 power 相关参数,如果硬件描述里缺了这些参数,即使你在 policy 属性里指定了它,框架也可能会拒绝绑定或者退回到默认 governor。内核里 policy 的优先级是:DT / sysfs 里显式指定的 governor 优先,其次按 governor_list 里的优先级排序,default 是 step_wise。
4.2 step_wise:最常用也最好理解
step_wise 的思路非常朴素:维护一个 trend(温度变化趋势),温度和 trip 的关系无非是"低于 trip"、"在 trip 附近徘徊"、"刚越过 trip"、"远高于 trip"这几种,每种状态下对 instance 的 target 做一步调整。
具体来说,当温度刚越过一个 trip 时,step_wise 会把该 trip 绑定的所有 instance 的 target 从上一个状态逐步上调(target 越大降温越强),一次只调一档;当温度回落到 hysteresis 以下时,再逐步下调。它的名字里这个 "step" 就是"一档一档来"的意思。好处是不会一次性把频率拉到最低造成性能悬崖,坏处是响应速度偏慢,对于发热极快的场景可能压不住。绝大多数嵌入式平台的默认温控策略都是它。
4.3 power_allocator:功耗预算的思路
power_allocator 是目前内核里最"高级"的 governor,源自 ARM 的 IPA(Intelligent Power Allocation)方案。它的思路不是"温度高了就降一档",而是反过来:先根据当前温度和 target 温度之间的偏差,通过 PID 控制器算出一个"允许的总功耗预算",然后把这个预算按权重分配给各个 cooling device,每个 cdev 再把功耗预算映射成 state。
这套机制需要 DT 里提供充分的功率参数,包括 sustainable-power(可持续功耗)、power model 里的动态/静态功率系数等。如果这些参数缺失,power_allocator 就跑不起来。它最大的优势是能在多热源共享散热资源时做精细分配,比如 CPU 和 GPU 同时发热,谁的热量贡献大就多限制谁。缺点是参数整定(PID 系数、预测系数)很有门槛,调不好效果反而不如 step_wise。
4.4 fair_share 与 user_space
fair_share 是"按权分配"的简单策略:每个 instance 有 weight 属性,温度超过 trip 后,每个 cdev 被压低的比例与权重成正比。它比 step_wise 更平滑,但缺乏反馈闭环,实际平台里用的人不多。
user_space 则完全不做内核侧决策,只在 trip 触发时向用户空间发送 netlink 事件(也有老的 uevent 方式),由 thermal daemon 或者用户自定义的脚本决定做什么。Android 的 thermal hal 早期就大量依赖这类机制。选 user_space 意味着你要保证用户空间进程可靠运行,如果 daemon 挂了,温控就完全失效,所以关键平台一般不会把它作为唯一防线。
5. cooling device 的落地:频率、电压、风扇到底怎么被驱动
5.1 cpufreq cooling:最典型的降温执行器
cpufreq cooling 是所有 cooling device 里最常打交道的。注册入口是 cpufreq_cooling_register(),它会根据 CPU 的 freq 表和 OPP 的 dynamic power coefficient 算出每个档位对应的功耗,然后内部实例化一个 thermal_cooling_device。
set_cur_state 的实现路径大致是:state 换算成对应的最大频率 -> 调 cpufreq 接口设置 policy 的 max -> cpufreq 驱动在下次频率切换时把频率限制到这个上限以内。这实际上就是通过限制最高频率来降功耗,所以你会发现降温后 CPU 跑在"低频高占用"的状态,而不是直接 idle。这也是为什么单纯靠降频,能效往往不如 cpuidle 那种"跑完赶紧睡"的策略——但 thermal 场景下我们追求的是快速压低发热,降频依然是最直接的手段。
5.10 内核之后引入了 DTPM(Dynamic Thermal Power Management)框架,把 cpufreq cooling 的功耗建模做得更细,可以对 CPU 的功耗做更精确的预算控制,配合 power_allocator 使用。如果你想深挖功耗建模,可以从 drivers/thermal/cpufreq_cooling.c 里的 power 相关实现看起。
5.2 devfreq cooling 与 device cooling
除了 CPU,GPU、DDR、NPU 这些设备也有自己的频率控制。它们大多走 devfreq 框架,所以对应的 cooling device 就是 devfreq_cooling。注册方式类似:用 devfreq 设备的频率表和功率模型注册成 cooling device,set_cur_state 时限制 devfreq 的最大频率。比如 Mali GPU 的调频就是通过 devfreq cooling 接入 thermal 的。
另外还有 device cooling 这种更通用的封装,直接对某个 device 调 thermal_device_ops 的 set_cur_state,适用于那些没有标准 freq 管理框架的硬件。总的来说,cooling device 的 universe 就是"一切能通过限制资源来降温的手段的统称"。
5.3 fan(PWM)与 clock cooling
风扇类 cooling device 一般由 pwm-fan 驱动注册,state 直接映射到 PWM 占空比。这种 cdev 的特点是 max_state 通常很小(比如 4 到 8 档),而且它不会像 CPU 降频那样有立竿见影的温度回落,所以绑定它的 trip 往往要用 active 类型,并且 hysteresis 要设大一点,避免风扇频繁换挡产生噪音。
clock cooling 则用于那些没有 cpufreq/devfreq 管理、但可以切时钟源的模块。它的 set_cur_state 里会对某个 clock 的速率做限制。这类 cdev 在低端 SoC 上比较常见,核心逻辑其实和 cpufreq cooling 一脉相承,只是操作对象从 CPU policy 换成了 clk 框架。
6. 从配置到落地:device tree 描述与 sysfs 调试接口
6.1 device tree 里的 thermal-zones
现代 ARM 平台基本都在 DT 里描述整个温控拓扑。一个典型的 thermal-zones 节点长这样:
thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; polling-delay = <1000>; thermal-sensors = <&tsens0 0>; trips { cpu_alert0: cpu-alert0 { temperature = <85000>; hysteresis = <5000>; type = "passive"; }; cpu_crit: cpu-crit { temperature = <105000>; hysteresis = <0>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu0 0 4>; // 允许的 state 区间 [0, 4] contribution = <1024>; }; }; }; };每个 zone 节点通过 thermal-sensors 属性引用温度传感器,trips 子节点定义阈值,cooling-maps 定义"哪个 trip 触发时约束哪个 cdev、允许在什么 state 区间内活动"。cooling-device 里的两个数字是该 cdev 在本 map 内允许的 lower 和 upper 档位,contribution 则是给 fair_share 和 power_allocator 用的权重。这套语法在 Devicetree 的 thermal binding 文档里有完整说明,配置前建议仔细读一遍,尤其是 cooling-device 的 state 范围写错了会导致降温根本不起作用。
另外注意 sensors 描述里可以带传感器编号(如 <&tsens0 0>),具体含义由传感器驱动解释,比如 tsens 的通道 0 可能对应 CPU 附近的温度点,通道 1 对应 GPU。
6.2 sysfs 接口速查
系统起来后,/sys/class/thermal/ 下面会有大量目录:
- thermal_zone0、thermal_zone1... 每个温区一个目录,里面最重要的是 type(名称)、temp(当前温度,毫摄氏度)、policy(当前 governor)、mode(enabled/disabled)、trip_point_X_temp、trip_point_X_type。
- cooling_device0、cooling_device1... 每个 cdev 一个目录,里面有 type、max_state、cur_state。
- 如果存在 cooling device 和 zone 的绑定,还会出现 cooling_deviceX 的 stats 目录或者 thermal_zone 下的 cdev 关联信息(不同内核版本暴露形式不同,但 cur_state 和 max_state 一直都有)。
调试时最常用的三板斧就是:cat temp 看温度是否在跳,cat policy 看用的是哪个 governor,cat cooling_deviceN/cur_state 看 governor 有没有真的把挡位推上去。这三个值串起来,就能判断温控链路断在哪一环。
6.3 调试方法论:从 /sys/class/thermal 看问题
假设你发现设备温度已经超过 trip 阈值但频率没降,按下面顺序排查:
- 先确认 zone 的 temp 是不是真的超过 trip_point_X_temp。很多时候是传感器位置不对,读到的温度根本没到阈值。
- 确认 zone 的 mode 是 enabled,这个文件可以手动写 enabled/disabled,跑测试时经常有人误关了。
- 确认 policy 是你预期的 governor。如果 platform 的 default governor 和 DT 指定的不一致,行为会很奇怪。
- 看对应 cdev 的 cur_state。如果 cur_state 一直是 0,说明 governor 决策没下发;如果 cur_state 变了但频率没变,问题在 cdev 驱动自身。
这套排查流程看起来简单,但实际项目里 80% 的温控问题都能被它定位到。
7. 排查温控不生效的实践:我踩过的几个坑
7.1 get_temp 返回错误码导致温区直接停摆
我有一次调试平台,发现 thermal_zone 的 temp 永远是上一次的正确值,不再更新。查了半天,最后发现是传感器驱动在某个异常状态下 get_temp() 返回了 -EIO。框架这边拿到非零返回值后直接跳过了后续所有逻辑,连 delayed work 都没重新调度,等于整个温区进入了"僵尸"状态。当时我们用的内核版本在这个错误路径上既没有重试机制也没有告警,只能靠 log 里偶尔出现的一条 thermal thermal_zone0: failed to read out thermal zone 消息发现。
这个问题在较新的内核里有所改善,但依然建议驱动实现 get_temp() 时务必保证失败路径最终返回 0 或提供临时有效值,不要让错误码冒泡到框架的调度循环里。你自己的驱动里,对这种瞬时错误最好是"重试一次 + 返回上次缓存值",而不是直接返回错误。
7.2 trip 阈值与 hysteresis 配错
另一个高频问题是把 trip 的 temperature 和 hysteresis 配成绝对温度。DT 的 temperature 是毫摄氏度,85000 表示 85 度。hysteresis 是相对值,5000 表示 5 度的回差,不是 5 毫摄氏度,也不是"回到 5000 度"。这个单位问题看似低级,但真的是团队里新老同事都踩过的坑。配错之后现象很典型:温度到 85 度触发降频,降到 84.99 度就立刻恢复,然后立刻又触发,频率在两边疯狂振荡,功耗反而比不限制还差。
7.3 cooling device 的 state 一直不变
还有一个隐蔽问题:cooling-maps 里写的 upper 区间太小。比如你要允许 CPU 最多降到 4 档,但实际 max_state 是 16,你在 map 里写 cooling-device = <&cpu0 0 4>,那 governor 算出来的 target 会被 clamp 在 4 以内,怎么看 cur_state 都最多到 4。这本身是配置语义,不算 bug,但如果你误以为 state 是"绝对频率档位"就会懵。建议在调试早期直接给 map 写 0 max_state,把区间限制彻底放开,确认链路通了再逐步收紧。
7.4 governor 没选对的典型表现
最后一个坑是 governor 选择。很多平台在 DT 里没写 thermal-governor 属性,默认走 step_wise,这没问题。但如果你在内核配置里把多个 governor 编进去,而且某个 zone 的 DT 里写了 "governor = power_allocator" 却缺少 sustainable-power 属性,绑定会失败,最终 fallback 到默认 governor,而你从 sysfs 里看到的 policy 可能是 step_wise,跟预期完全不一样。这种"安静失效"最坑人,因为代码看着没问题,配置也好像写了,实际走的却是另一套逻辑。
排查这类问题,最直接的办法是打开 thermal 相关的 ftrace 事件。内核在 handle_thermal_trip() 和 thermal_zone_device_update() 等路径上有 tracepoint,可以清楚看到每次巡检时温度、trip 状态和 governor 的调用关系。配合 trace-cmd 用起来,比瞎猜高效太多。
这系列写到现在,我自己最大的体会是:thermal framework 的代码量不大,但是抽象层次非常讲究,传感器、策略、执行器三个维度切得很干净。真正难的不是读代码,而是把 device tree 里的拓扑描述、governor 的计算逻辑、cdev 驱动的实际行为这三层在脑子里串成一条完整的因果链。每次遇到温控问题,只要沿着"温度准不准 -> trip 有没有触发 -> governor 选对没 -> cdev 动没动"这条线走,基本都能收口。最后再建议一句:手里有开发板的话,把一个风扇或者可调电源接上,自己写个 sensor 驱动模拟温度变化,把 step_wise 和 power_allocator 各跑一遍,比看十篇源码分析都管用。