1. 项目概述1.1 为什么要啃Linux Thermal模块做嵌入式或者跑服务器的朋友应该都有这种体会:芯片性能越来越强,功耗密度越来越大,散热问题已经从“硬件工程师的烦恼”变成了“系统工程师的噩梦”。我自己手头负责的一块主控板,四核A72跑满负载时,外壳温度能飙到80多度,如果不加任何控制,要么触发硬件保护直接关机,要么芯片快速老化。解决这个问题的核心手段之一,就是Linux内核里的Thermal模块。Thermal模块在内核里的定位其实很纯粹:它管理“温度”、“冷却手段”、“控制策略”这三者之间的关系。你可以把它理解成一个温控管家——温度传感器负责报数(temperature),风扇、调频、调压这些执行器负责干活(cooling device),而中间那层策略逻辑(governor)决定“什么时候该让风扇转起来、CPU降到什么频率”。这三层配合好了,系统既能在性能释放和温度控制之间找到平衡,又能避免极端情况下硬件损坏。我刚开始接触这个模块时,最直观的感受是:网上资料太散了。要么是内核源码的枯燥注释,要么是某个板子改设备树的零碎记录,很少有系统性的梳理。这篇文章我想把自己这段时间的学习和调试验证过程整理出来,从框架设计、核心数据结构、设备树配置,到governor选型、常见坑排查,一条线走通。适合正在做bring-up、遇到温度控制不生效、或者想彻底搞懂thermal子系统内核机制的朋友参考。1.2 这个模块能帮你解决什么问题芯片过热降频甚至死机时,能快速定位是哪里没配置对设计产品散热策略时,知道选哪种governor、怎么设置trip point更合理看懂内核里thermal相关的sysfs输出,不再是一堆数字看了就头疼新板子bring-up阶段,能自己编写和验证设备树中的thermal-zones节点2. Linux Thermal框架的设计思路2.1 三层解耦:传感、执行、策略分开管理Thermal子系统的设计哲学不复杂,核心就是“解耦”。内核维护者把它拆成了三个独立维度:thermal zone:代表一个温度监控区域。一个芯片可以划分为多个zone,比如CPU zone、GPU zone、电池 zone,每个zone有自己的温度传感器(或者多个传感器取平均/最大值)。zone里定义了几个关键温度阈值点,叫trip point。cooling device:代表一个冷却执行器。CPU调频(cpufreq cooling)、GPU调频(devfreq cooling)、风扇转速(fan cooling)、甚至关闭某核(cpu hotplug cooling)都是冷却设备。每个cooling device有若干档位(level/state),从0开始,数值越大表示冷却能力越强。governor:连接上面两者的策略算法。它周期性地读取zone的温度,判断当前温度落在哪个trip point区间,然后对绑定的cooling device下发“调到第几档”的指令。这个分层结构最大的好处是:换传感器不用动冷却逻辑,换风扇不用改governor算法。我最早看过某厂商的私有温控实现,温度和调频策略耦合在一起,后来拆出来做适配时差点崩溃。相比之下,内核thermal模块的分层模型干净得让人舒服。2.2 热循环的完整工作流程一次完整的热控制循环大概是这样的:内核的thermal framework注册一个定时器(默认轮询间隔因governor而异,通常250ms到100ms级别),周期性触发。thermal zone获取传感器的实时温度,然后交给governor的throttle处理函数。governor对比温度与trip point的关系,计算目标冷却状态。比如step_wise governor会“一步一步”地根据趋势决定升档还是降档。通过thermal_zone_bind_cooling_device建立的绑定关系,将目标状态写到对应的cooling device。cooling device根据状态值执行具体动作。cpufreq cooling就会去调用cpufreq_set_policy调整频率上限,风扇驱动则去修改PWM占空比。这个循环里每一步都有对应的sysfs节点可以观测,所以排查问题时思路很简单:看温度读数对不对 -> 看trip point触发没触发 -> 看governor有没有发指令 -> 看cooling device有没有执行。后面我会详细拆每一步怎么验证。2.3 为什么不是简单做个“温度高了就降频”有个很自然的疑问:直接做成“温度达到90度就降频,降到80度就恢复”不行吗?当然可以,但实际产品里这样粗暴的开关式控制体验很差。风扇会反复启停,CPU频率会来回抖动,用户体验就是“电脑卡一下、好一下、再卡一下”。Governor的存在就是为了解决这个“震荡”问题。它引入了趋势预判、滞回区间、渐进调节等机制,让冷却过程平滑。这也解释了为什么内核有多个governor——不同场景需要不同的调节哲学。移动设备希望尽量延长峰值性能的持续时间,服务器希望绝对不要超过某个温度红线,风扇控制的滞后性又决定了你得提前动作。这些细微差异就体现在governor的实现细节里。3. 核心数据结构与关键接口3.1 thermal_zone_device:温度世界的终点在内核源码里找到drivers/thermal/thermal_core.h,你会看到struct thermal_zone_device这个核心结构体。挑几个关键字段说:struct thermal_zone_device { struct device *device; struct thermal_zone_dev_attr *tzp; const struct thermal_zone_params *tzp_params; struct thermal_zone_ops *ops; // 操作函数集 struct list_head thermal_instances; // 绑定的cooling device链表 struct idr idr; struct thermal_zone_trip *trips; // trip点数组 int num_trips; struct thermal_attr *trip_temp_attrs; enum thermal_device_mode mode; // enabled/disabled int temperature; // 当前温度(单位:毫摄氏度) int last_temperature; int emul_temperature; // 模拟温度,调试用 int passive_delay; // 非主动模式轮询间隔 int polling_delay; // 轮询间隔 int governor; // 当前使用的governor struct thermal_zone_device *tz; };对于驱动开发者来说,最需要关心的是thermal_zone_ops里的三个回调:struct thermal_zone_ops { int (*get_temp)(struct thermal_zone_device *, int *); int (*set_trips)(struct thermal_zone_device *, int, int); int (*change_mode)(struct thermal_zone_device *, enum thermal_device_mode); };get_temp是底层传感器驱动的唯一接口——框架不管你的传感器是I2C上的、还是寄存器直接映射的,只要你能填出一个毫摄氏度温度值就行。set_trips用来设置硬件比较器的阈值,如果传感器支持硬件中断触发的话,可以省掉轮询的开销。change_mode则用来在调试时强制启用/禁用这个zone。刚接触thermal时最容易犯的错是单位搞混。内核thermal框架里温度单位是毫摄氏度(millicelsius),也就是25度要写成25000。设备树里temperature字段的单位反而是摄氏度。这两个口径在thermal_of.c里会有转换逻辑,但你自己写驱动直接调用thermal_zone_get_temp拿到的是毫摄氏度,别拿去跟设备树里的temperature=85000直接比。3.2 thermal_cooling_device:执行端口的统一抽象冷却设备的抽象同样是一个大结构体,核心操作函数集集中在thermal_cooling_device_ops里:struct thermal_cooling_device_ops { int (*get_max_state)(struct thermal_cooling_device *, unsigned long *); int (*get_cur_state)(struct thermal_cooling_device *, unsigned long *); int (*set_cur_state)(struct thermal_cooling_device *, unsigned long); };这三个回调的含义很直观:get_max_state告诉thermal框架你支持多少档位,get_cur_state报告当前档位,set_cur_state让执行器切到指定档位。cpufreq cooling device的档位就是来自cpufreq_table中的频率档数,devfreq cooling的档位对应设备支持的工作频率数量,风扇的档位就由驱动自己定义了。注意set_cur_state是同步还是异步取决于具体驱动。有些风扇驱动里PWM设置完还需要等待转速稳定,这时如果governor频繁下发指令,驱动要对相同的state请求做去重处理。内核里很多cooling device驱动都维护了curr_state的缓存,只有当新state不同时才真正操作硬件,这也是我建议大家写自己的cooling device驱动时必须保留的一个习惯。3.3 thermal_instance与绑定关系光有zone和cooling device还不够,两者之间需要一个“接线”的环节。这个接线就是struct thermal_instance,它记录了某个cooling device被绑定到某个zone的某个trip点,并且包含两个关键属性:upper和lower,即该冷却设备在这个trip点下允许调节的档位范围。为什么需要上下限?举个实际例子:CPU调频冷却设备的档位范围是0到N,你绑到临界高温trip时可能只想让它最高降到某个中间频率,而不想直接干到最低频。这时设置upper=某档就行。设备树里cooling-device = <&cpu_cooling 0 2>这种写法,意思就是绑定时的档位范围是从0到2,可以有效限制governor的调节空间。这个设计非常实用,我也强烈建议在初始调试阶段,通过限制upper/lower来缩小governor的动作范围,这样排查问题时会容易很多。3.4 trip point:温度与动作的连接点Linux thermal框架中trip point(温度阈值点)经历了一次比较大的API调整,老的代码里是get_trip_temp、get_trip_type多个回调函数,新内核(5.x后期开始)收敛成了struct thermal_trip这样一个描述性结构:struct thermal_trip { struct list_head list; int temperature; // 触发温度(毫摄氏度) int hysteresis; // 滞回值(毫摄氏度) enum thermal_trip_type type; // passive/active/critical/hot };trip的type有四种:passive:被动冷却,通常绑定调频/调压这类手段,CPU温度超过该点会触发降频active:主动冷却,通常是风扇等外部冷却设备hot:热警告,触发系统事件通知,但还不到关机力度critical:临界温度,达到后直接触发系统关机(orderly_poweroff),防止硬件烧毁我在实际配置时,trip点的设定需要结合产品的散热结构找规律。比如某款外壳散热条件不错的盒子,我会把passive设在75度、hot设在85度、critical设在95度。但如果产品是密闭无风扇的,passive就得提前到65度左右,因为无主动散热时热惯性大,等到85度再降频就来不及了。4. 设备树中的thermal-zones配置实战4.1 一个完整的配置片段拆解设备树是thermal模块在嵌入式设备上的主战场。以最常见的配置方式为例,一段典型的thermal节点长这样:thermal_zones: thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; polling-delay = <1000>; thermal-sensors = <&tsens 0>; trips { cpu_alert0: cpu-alert0 { temperature = <75000>; hysteresis = <2000>; type = "passive"; }; cpu_alert1: cpu-alert1 { temperature = <85000>; hysteresis = <2000>; type = "hot"; }; cpu_crit: cpu-crit { temperature = <95000>; hysteresis = <0>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu_cooling 0 2>; }; map1 { trip = <&cpu_alert1>; cooling-device = <&cpu_cooling 3 6>; }; }; }; };这里有几个点值得展开讲:polling-delay-passive和polling-delay是两种模式的轮询间隔。前者在温度超过passive trip之后启用,后者是正常状态下的温度轮询间隔。为什么要分两种?因为正常状态没必要高频读取温度浪费功耗,而进入被动冷却状态后,温度变化快,需要更高频的采样以便及时调整。这个设计我很认同,调试时也可以利用它省事——把polling-delay调大,被动轮询间隔调小,温度变化就能更灵敏地反映在sysfs中。trips子节点定义温度阈值,cooling-maps子节点做绑定。map0将alert0这个trip和cpu_cooling这个冷却设备绑定,后面的0 2就是我在3.3节提到的上下限档位。thermal-sensors = <&tsens 0>指定了温度传感器。tsens是高通平台常见的温度传感器IP,其他平台可能是&temp_sensor或者&bandgap。这里第二个参数0代表传感器通道索引。4.2 冷却设备节点的写法刚才的配置里引用了cpu_cooling这个节点,它是什么?在设备树里,cpufreq冷却设备通常不需要单独写节点,而是通过cpufreq-cooling机制自动注册。有些平台为了更精细的控制会显式定义:cpu_cooling: cpu-cooling { compatible = "thermal-cooling-device"; #cooling-cells = <2>; cooling-min-level = <0>; cooling-max-level = <6>; };#cooling-cells = <2>表示这个冷却设备在bind时需要两个参数:最小档位和最大档位。这解释了为什么cooling-maps里写<&cpu_cooling 0 2>是两个值。手动写cooling设备节点的另一个原因是:当你想用cpufreq冷却,但希望暴露给thermal系统的档位数量不等于实际cpufreq表里的档位数时,可以通过这种方式做压缩映射。4.3 设备树校验和常见报错写完设备树后,我的习惯是在板子上启动后用以下方式快速验证:# 查看设备树中thermal zone的解析结果 ls /sys/class/thermal/ cat /sys/class/thermal/thermal_zone0/type # 查看内核是否成功注册了所有cooling device cat /sys/class/thermal/cooling_device0/type如果设备树解析失败,dmesg里会有类似thermal_sys: Failed to find 'trips' node或者failed to build thermal zone的提示。最常见的错误原因有两个:一是trips节点里type字符串写错(必须是passive/hot/active/critical),二是thermal-sensors引用的phandle写错或传感器驱动没有probe成功。遇到第二种情况时,先确认传感器驱动独立加载是否正常,再用cat /sys/class/thermal/thermal_zone*/temp看温度节点是否存在。另外特别提醒:不要在设备树里给同一个trip写两个相同的温度值。内核里thermal_zone_device_register会检查trip排序合法性,如果发现trip点没有按温度递增排列,会拒绝注册zone。这个坑我踩过——当时想加一个active类型的低温柔控点,结果排在了passive前面,整个zone直接没起来。5. thermal framework的初始化与注册流程5.1 从内核启动到设备就位的完整链路要理解thermal模块每个环节,有必要过一遍初始化流程。当内核启动时,thermal_core.c中的thermal_init作为子系统入口被调用。它做几件关键的事:注册thermal_class类,这样sysfs里才出现/sys/class/thermal这个目录。初始化全局的thermal_tz_list、thermal_cdev_list两个链表,分别管理所有zone和冷却设备。注册governor管理机制,加载step_wise等默认governor。注册热区的genhd和平台驱动支持,如果设备树存在thermal-zones节点,后续的of_parse_thermal_zones会逐个解析并创建zone。在设备树平台上,真正干活的是thermal_of.c。of_parse_thermal_zones函数遍历设备树中的thermal-zones节点,对每个节点执行thermal_zone_of_add_sensor和后续的thermal_zone_device_register。整个过程有个值得一提的设计:thermal zone的注册延迟。传感器驱动可能注册得比thermal框架晚,为了解决先后顺序问题,设备树解析并不是一次完成的。如果发现thermal-sensors指向的驱动还没ready,内核会把zone放到一个waiting list里,等传感器驱动probe完成并调thermal_zone_of_sensor_register时再真正完成zone的创建。这个机制导致一个很常见的排查场景:传感器驱动没加载,zone节点就不存在。5.2 governor的注册和选择逻辑governor并不是一开始就全部加载,而是通过thermal_governor_register动态注册的。内核里step_wise是默认内建的,power_allocator也是比较常用的。启动后你可以通过sysfs看到:cat /sys/class/thermal/thermal_zone0/policy这个文件显示当前zone使用的governor名称。如果需要切换,直接echo:echo "power_allocator" > /sys/class/thermal/thermal_zone0/policygovernor的选取逻辑在thermal_core.c的thermal_zone_device_set_policy中:如果手动设置,就用指定的;否则使用thermal_zone_params里的governor_name;都没有则回落到step_wise。看到这里你应该明白:governor是可以在运行时动态切换的。这在调试时是利器——比如怀疑某个zone的调节过于激进,可以直接切换到user_space,自己手动写cooling state,进行对照实验。``bash切换到user_space governor,手动控制echo "user_space" > /sys/class/thermal/thermal_zone0/policy echo 3 > /sys/class/thermal/cooling_device0/cur_state`user_space`这个governor很有意思,它相当于把调节权完全交给了用户态。你手动设置`cur_state`,thermal模块不会主动干预。这是排查具体cooling device是否正常的首选方式——可以直接绕过策略算法测执行器件。 ## 6. governor算法细节与选型心得 ### 6.1 step_wise:经典且直观的逐步调节 `step_wise`是内核默认governor,也是我建议大家最先读源码的算法。它的调节逻辑在每个轮询周期内,根据当前温度与trip point的关系和温度趋势来做决策: - 如果当前温度高于某个trip点对应的温度,且温度在上升,则冷却设备**上调一档** - 如果温度高于trip点但趋势平稳/下降,则保持当前档位 - 如果温度低于trip点减去滞回值,且温度在下降,则下调一档 源码中体现为`step_wise_throttle`函数,它遍历该zone下所有trip点,找到当前温度所处的区间和趋势方向。这里有个关键细节:**step_wise是“基于趋势”的**,而不是“基于绝对值”的。它内部使用`temperature == last_temperature`判断稳定、`temperature > last_temperature`判断上升。 这个细节在实测中很有感:如果温度传感器噪声大,读数来回抖,step_wise可能会频繁升降档。缓解办法有两个:一是增大trip的`hysteresis`值,二是调大`polling-delay-passive`让决策更慢。我自己遇到过传感器瞬间尖刺导致风扇突然满转的情况,加了滞回之后明显好转。 ### 6.2 power_allocator:为功耗预算而生 `power_allocator`是另一种完全不同的思路。它不直接看温度绝对值来触发动作,而是引入了“功耗预算”的概念:zone有一个`sustainable_power`(可持续散热功率),如果当前功耗超过这个值,就把多出来的部分作为需要削减的量,然后换算成cooling state让冷却设备执行。 用PID控制策略来描述power_allocator的内核逻辑再合适不过了。它根据当前温度与目标温度的误差计算需要调整的功耗,再加上一个前馈项(当前功耗预估),最终算出目标功率。具体的换算链路是:`estimate_power`估算当前功耗 -> `pid_controller`算出调整量 -> `power_to_state`将功率需求映射到cooling state。 这个governor在移动设备/笔记本上很常用,优点是能主动控制功耗而不只被动降温,缺点是需要配置正确的`sustainable_power`和PID参数,初期调参痛苦,参数不合适时风扇反而会振荡。如果要用它,设备树中需要: ```dts trips { cpu_alert0: cpu-alert0 { temperature = <80000>; hysteresis = <2000>; type = "passive"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu_cooling 0 6>; }; };同时还需要在zone节点中设置:cpu-thermal { sustainable-power = <2500>; // 毫瓦,表示散热系统能持续带走2.5W };下面这个初始化代码片段是用户在驱动中配置PID参数的参考方式:#include <linux/thermal.h> static struct thermal_zone_params tzp = { .governor_name = "power_allocator", }; static int __init thermal_zone_init(void) { struct thermal_zone_device *tz; tz = thermal_zone_device_register("cpu-thermal", 0, NULL, &tzp, NULL, NULL, 0, 0); if (IS_ERR(tz)) return PTR_ERR(tz); return 0; }6.3 fair_share等其他governor的适用场景除了上面两个,内核还提供fair_share、bang_bang和user_space等governor。fair_share是早期的简单权重分配算法,它按cooling device的权重比例分配散热需求,在只有单个zone、单种冷却设备时比较鸡肋。bang_bang是开关控制,适合只有开/关两种状态的风扇等设备,启停频繁的缺点很明显。user_space更像是调试工具而非常用生产方案。选型上我给的建议很简单:没有特殊理由就用step_wise,要做动态功耗控制再换power_allocator。step_wise经过了大量设备的验证,行为可预期,参数少,调起来快。power_allocator理论更优,但sustainable_power的准确性直接决定控制效果,而且这个值跟环境温度、散热条件强相关,很难一劳永逸地标定。6.4 自研governor的切入方式如果你需要实现一个非常特定的温控策略(比如结合产品外壳温度做预测控制),内核支持注册自己的governor。实现方法很直接:static struct thermal_governor my_governor = { .name = "my_governor", .throttle = my_throttle, .thermal_zone_trip_update = my_trip_update, }; thermal_governor_register(&my_governor);.throttle函数会在每个轮询周期被调用,你在这个函数里读取tz->temperature,然后遍历tz->thermal_instances列表,对每个instance调用thermal_cdev_update(cdev)设置目标状态。这个接口非常灵活,几乎可以做任何你能想象的控制逻辑。但我得提醒一句:内核对governor的调用是在打上thermal_zone->lock的情况下进行的,你的实现里不能做耗时操作,更不能在持锁状态下调用可能sleep的函数。否则你会在压力测试时碰到奇怪的调度延迟甚至死锁。7. 实际调试中的经验与踩坑记录7.1 让温度“动起来”的调试思路调试thermal最基础也最困难的事情,就是让温度可靠地上升。我的常规做法分几步:第一步,确认传感器读数是真实的:cat /sys/class/thermal/thermal_zone0/temp如果温度完全不动,先检查传感器驱动,使用i2cdetect确认i2c地址正确。第二步,用负载工具拉高温度。CPU场景我常用:# 多核满负荷小工具 stress-ng --cpu 8 --timeout 300 &GPU场景可能需要持续跑渲染负载,或简单循环跑一定负载。第三步,在负载拉起来的同时,实时观察:watch -n 1 cat /sys/class/thermal/thermal_zone0/temp watch -n 1 cat /sys/class/thermal/cooling_device0/cur_state通过同时看温度和冷却状态的变化,你能判断整套链路是不是在正常工作。如果温度涨上去了但cur_state一直是0,那就说明问题出在governor或trip判断上。7.2 一个典型的“能读到温度但没降频”排查实录有块板子出现这样的现象:温度升到90度,但CPU频率纹丝不动,系统直到critical关机。我排查的思路是:第一步查zone信息:cat /sys/class/thermal/thermal_zone0/type # 确认zone存在 cat /sys/class/thermal/thermal_zone0/trip_point_0_temp # 确认trip点存在结果正常。第二步查policy:cat /sys/class/thermal/thermal_zone0/policy输出step_wise,正常。第三步查绑定关系:cat /sys/class/thermal/thermal_zone0/cdev0_type ls /sys/class/thermal/thermal_zone0/cdev0_trip_point如果这里发现cdev0不存在,就是绑定关系没建立。再到设备树里看cooling-maps节点,很可能cooling-device引用的phandle和实际冷却设备节点对不上,或者绑定的trip节点名称跟trips里定义的不一致。还有一种很隐蔽的情况:zone注册成功了,cdev也注册了,但trip type和governor的触发逻辑不匹配。比如trip设成了hot类型,而step_wise只对passive和active类型的trip做调节,hot只上报事件不改频率。这就解释了“温度超了但就是不动”的现象。后来把hot改成passive后立刻生效。这个坑非常容易踩,因为设备树里type字段写错了不会报任何错误,只是行为不符预期。7.3 冷却状态“卡住”和风扇突变问题还有一种常见故障:风扇进入高速档后回不来。step_wise governor调节档位依赖温度降到trip_temp - hysteresis以下。如果你把hysteresis设成0,而传感器温度又在阈值附近抖动,那么不满足“低于阈值”的条件,风扇永远降不下来。解决方案就是给trip设置合理的滞回值,比如3到5度。反过来,风扇突然满转的问题则通常是传感器噪声引起的。我遇到过一次给tsens输入的电压纹波过大导致温度读数跳变20度,step_wise检测到“温度骤升”就直接把冷却档位拉满。治本的方法是修硬件滤波,治标的方法是把polling-delay-passive调大,让噪声在多个采样周期内被平均掉。这里也体现出emul_temp调试节点的价值:# 手动模拟某温度,绕过传感器实际测量 echo 90000 > /sys/class/thermal/thermal_zone0/emul_temp注意不是所有内核都打开了CONFIG_THERMAL_EMULATION,这个配置项默认常在开发板上关闭。开启后就可以用固定值来验证trip触发逻辑,这是调试策略逻辑、绕开硬件问题的最有效手段。7.4 测试环境里容易忽略的细节温度传感器离热源太远,读到的温度和芯片结温差很多。我在某块板子上对比过:红外点温枪测外壳65度,tsens报的结温已经83度,两者差了整整18度。这意味着trip点设置必须对应传感器的实际位置特性,不能照搬datasheet里的结温建议值。散热条件差异巨大。同一块板子,裸板测试时温度平衡点可能比装进外壳后低10度以上。建议在产品最终形态的散热条件下做trip点标定,裸板状态下调出来的参数装上外壳后可能过保守或过激进。多路负载并行时要让温度有足够时间达到稳态。CPU、GPU、NPU同时跑的时候,温度上升有热惯性,通常需要几分钟才到平衡点。做控制策略验证时,每次改变负载条件后至少要等3到5分钟再下结论,否则阶段性的数据会误导你。7.5 常用调试接口速查表调试目标路径/命令预期行为查看当前温度/sys/class/thermal/thermal_zone*/temp输出毫摄氏度,如65000表示65°C查看zone使用governorcat /sys/class/thermal/thermal_zone0/policy显示step_wise或power_allocator等切换governorecho "power_allocator" > policy运行时切换策略查看冷却设备档位cat /sys/class/thermal/cooling_device0/cur_state显示当前冷却档位手动控制冷却设备echo 5 > /sys/class/thermal/cooling_device0/cur_state配合user_space governor使用查看冷却设备绑定ls /sys/class/thermal/thermal_zone0/cdev0_*显示该zone绑定的cdev信息模拟温度echo 90000 > /sys/class/thermal/thermal_zone0/emul_temp需要开启CONFIG_THERMAL_EMULATION查看内核日志`dmesggrep -i thermal`7.6 内核配置选项要点menuconfig里有几个选项跟你的调试体验强相关。CONFIG_THERMAL是总开关,CONFIG_THERMAL_OF负责设备树解析支持,CONFIG_THERMAL_EMULATION就是上面说的温度模拟开关。governor相关的有CONFIG_THERMAL_DEFAULT_GOV_STEP_WISE(设置默认governor)、CONFIG_THERMAL_GOV_POWER_ALLOCATOR、CONFIG_THERMAL_GOV_USER_SPACE。调试期建议把这些governor都编译进去,运行时随时切。还要注意,有些SoC平台有自己的私有thermal驱动,比如某些平台的“TM”(Thermal Monitor)模块,它可能不走标准框架而是直接在固件里做调频控制。碰上这种平台,你首先要确认板上实际生效的是不是标准thermal框架——查看启动日志里是否出现thermal thermal_zone0: ...之类信息,如果没有,那多半是跑在隐藏的固件逻辑上,设备树配得再标准也很难生效。8. 更进一步的学习路径建议8.1 源码阅读的有效顺序很多人问我源码怎么读,我的建议顺序是从“使用层”向“框架层”推进:先读设备树绑定文档:Documentation/devicetree/bindings/thermal/thermal-zones.yaml,搞清楚节点字段含义。再读drivers/thermal/thermal_core.c中的注册函数:thermal_zone_device_register和thermal_cooling_device_register,知道注册一个zone需要满足什么条件。然后读drivers/thermal/thermal_sysfs.c:看sysfs节点怎么暴露、temp、policy、trip_point_*这些文件背后的逻辑是什么。接着读drivers/thermal/step_wise.c和drivers/thermal/gov_power_allocator.c,理解两个典型governor的决策过程。最后看具体的驱动实现,比如drivers/thermal/cpufreq_cooling.c和drivers/thermal/devfreq_cooling.c,搞清楚cooling state如何映射到调频调压。读完这个序列,整个thermal子系统的全貌就清晰了。我也推荐把自己平台的传感器驱动和风扇驱动完整读一遍,那里面有很多frame结构对不齐导致的workaround,是真实世界的实战经验。8.2 从内核到用户态的扩展视野很多产品最终不用内核governor,而是走user_space模式,在用户态用专门的热管理服务做策略。这样做的好处是策略升级不需要重编内核,坏处是用户态拿到的温度数据延迟大一些,而且服务崩溃会导致温控完全失效。如果你做的是消费级产品,建议内核态保底(至少critical关机),用户态做增强策略。这两个层面不冲突:内核thermal保证不死机,用户态策略负责性能和体验的精细平衡。内核和用户态协作的标准接口是netlink事件。通过监听NETLINK_THERMAL事件,用户态进程能收到温度越限通知。感兴趣的可以看drivers/thermal/thermal_netlink.c,里面定义了事件类型和消息结构。8.3 结合硬件设计反向理解参数最后给个硬件视角的建议。thermal参数不是软件工程师凭空定的,它应该来源于硬件实测。理想的工作流程是:硬件工程师给出散热设计目标:比如“外壳最高温度不超过45度,芯片结温不超过85度”做热测试拿到芯片在不同负载下的结温曲线、散热系统在不同风扇转速下的散热能力软件据此反推trip点:结温85度对应散热极限,那passive点就应该设在75度,留出10度余量作为响应延迟补偿动态功耗测试还能算出sustainable_power,供power_allocator使用这个流程听起来理所当然,但实际项目里软硬件经常各干各的:硬件调完结构就给软件提“帮忙看看thermal”,软件拿到板子再自己摸索试探。结果就是个折中的、没有理论依据的配置。你如果能把这条链路打通,在项目里就是不可替代的桥梁角色。我个人在几个项目里反复体会最深的一点是:thermal不像其他内核模块那样“对和错”分明,它更像是一个调优过程。同样的配置在这块板子上完美运行,换一块散热稍差的板子就可能出问题。所以别指望抄一份设备树就能解决所有问题,关键还是理解里面的每个参数是怎么影响控制环路的,然后跟你手头硬件的热特性对应起来。把这个模块吃透后,你会发现它不仅用于降温,还串联起了调频、调压、风扇控制、甚至电池充电电流控制等多个子系统。从它入手去梳理整个系统的功耗和热管理路径,是一条性价比很高的学习路线。希望这篇整理出的框架和踩坑记录,能帮你少走一些弯路。
1. 项目概述1.1 为什么要啃Linux Thermal模块做嵌入式或者跑服务器的朋友应该都有这种体会:芯片性能越来越强,功耗密度越来越大,散热问题已经从“硬件工程师的烦恼”变成了“系统工程师的噩梦”。我自己手头负责的一块主控板,四核A72跑满负载时,外壳温度能飙到80多度,如果不加任何控制,要么触发硬件保护直接关机,要么芯片快速老化。解决这个问题的核心手段之一,就是Linux内核里的Thermal模块。Thermal模块在内核里的定位其实很纯粹:它管理“温度”、“冷却手段”、“控制策略”这三者之间的关系。你可以把它理解成一个温控管家——温度传感器负责报数(temperature),风扇、调频、调压这些执行器负责干活(cooling device),而中间那层策略逻辑(governor)决定“什么时候该让风扇转起来、CPU降到什么频率”。这三层配合好了,系统既能在性能释放和温度控制之间找到平衡,又能避免极端情况下硬件损坏。我刚开始接触这个模块时,最直观的感受是:网上资料太散了。要么是内核源码的枯燥注释,要么是某个板子改设备树的零碎记录,很少有系统性的梳理。这篇文章我想把自己这段时间的学习和调试验证过程整理出来,从框架设计、核心数据结构、设备树配置,到governor选型、常见坑排查,一条线走通。适合正在做bring-up、遇到温度控制不生效、或者想彻底搞懂thermal子系统内核机制的朋友参考。1.2 这个模块能帮你解决什么问题芯片过热降频甚至死机时,能快速定位是哪里没配置对设计产品散热策略时,知道选哪种governor、怎么设置trip point更合理看懂内核里thermal相关的sysfs输出,不再是一堆数字看了就头疼新板子bring-up阶段,能自己编写和验证设备树中的thermal-zones节点2. Linux Thermal框架的设计思路2.1 三层解耦:传感、执行、策略分开管理Thermal子系统的设计哲学不复杂,核心就是“解耦”。内核维护者把它拆成了三个独立维度:thermal zone:代表一个温度监控区域。一个芯片可以划分为多个zone,比如CPU zone、GPU zone、电池 zone,每个zone有自己的温度传感器(或者多个传感器取平均/最大值)。zone里定义了几个关键温度阈值点,叫trip point。cooling device:代表一个冷却执行器。CPU调频(cpufreq cooling)、GPU调频(devfreq cooling)、风扇转速(fan cooling)、甚至关闭某核(cpu hotplug cooling)都是冷却设备。每个cooling device有若干档位(level/state),从0开始,数值越大表示冷却能力越强。governor:连接上面两者的策略算法。它周期性地读取zone的温度,判断当前温度落在哪个trip point区间,然后对绑定的cooling device下发“调到第几档”的指令。这个分层结构最大的好处是:换传感器不用动冷却逻辑,换风扇不用改governor算法。我最早看过某厂商的私有温控实现,温度和调频策略耦合在一起,后来拆出来做适配时差点崩溃。相比之下,内核thermal模块的分层模型干净得让人舒服。2.2 热循环的完整工作流程一次完整的热控制循环大概是这样的:内核的thermal framework注册一个定时器(默认轮询间隔因governor而异,通常250ms到100ms级别),周期性触发。thermal zone获取传感器的实时温度,然后交给governor的throttle处理函数。governor对比温度与trip point的关系,计算目标冷却状态。比如step_wise governor会“一步一步”地根据趋势决定升档还是降档。通过thermal_zone_bind_cooling_device建立的绑定关系,将目标状态写到对应的cooling device。cooling device根据状态值执行具体动作。cpufreq cooling就会去调用cpufreq_set_policy调整频率上限,风扇驱动则去修改PWM占空比。这个循环里每一步都有对应的sysfs节点可以观测,所以排查问题时思路很简单:看温度读数对不对 -> 看trip point触发没触发 -> 看governor有没有发指令 -> 看cooling device有没有执行。后面我会详细拆每一步怎么验证。2.3 为什么不是简单做个“温度高了就降频”有个很自然的疑问:直接做成“温度达到90度就降频,降到80度就恢复”不行吗?当然可以,但实际产品里这样粗暴的开关式控制体验很差。风扇会反复启停,CPU频率会来回抖动,用户体验就是“电脑卡一下、好一下、再卡一下”。Governor的存在就是为了解决这个“震荡”问题。它引入了趋势预判、滞回区间、渐进调节等机制,让冷却过程平滑。这也解释了为什么内核有多个governor——不同场景需要不同的调节哲学。移动设备希望尽量延长峰值性能的持续时间,服务器希望绝对不要超过某个温度红线,风扇控制的滞后性又决定了你得提前动作。这些细微差异就体现在governor的实现细节里。3. 核心数据结构与关键接口3.1 thermal_zone_device:温度世界的终点在内核源码里找到drivers/thermal/thermal_core.h,你会看到struct thermal_zone_device这个核心结构体。挑几个关键字段说:struct thermal_zone_device { struct device *device; struct thermal_zone_dev_attr *tzp; const struct thermal_zone_params *tzp_params; struct thermal_zone_ops *ops; // 操作函数集 struct list_head thermal_instances; // 绑定的cooling device链表 struct idr idr; struct thermal_zone_trip *trips; // trip点数组 int num_trips; struct thermal_attr *trip_temp_attrs; enum thermal_device_mode mode; // enabled/disabled int temperature; // 当前温度(单位:毫摄氏度) int last_temperature; int emul_temperature; // 模拟温度,调试用 int passive_delay; // 非主动模式轮询间隔 int polling_delay; // 轮询间隔 int governor; // 当前使用的governor struct thermal_zone_device *tz; };对于驱动开发者来说,最需要关心的是thermal_zone_ops里的三个回调:struct thermal_zone_ops { int (*get_temp)(struct thermal_zone_device *, int *); int (*set_trips)(struct thermal_zone_device *, int, int); int (*change_mode)(struct thermal_zone_device *, enum thermal_device_mode); };get_temp是底层传感器驱动的唯一接口——框架不管你的传感器是I2C上的、还是寄存器直接映射的,只要你能填出一个毫摄氏度温度值就行。set_trips用来设置硬件比较器的阈值,如果传感器支持硬件中断触发的话,可以省掉轮询的开销。change_mode则用来在调试时强制启用/禁用这个zone。刚接触thermal时最容易犯的错是单位搞混。内核thermal框架里温度单位是毫摄氏度(millicelsius),也就是25度要写成25000。设备树里temperature字段的单位反而是摄氏度。这两个口径在thermal_of.c里会有转换逻辑,但你自己写驱动直接调用thermal_zone_get_temp拿到的是毫摄氏度,别拿去跟设备树里的temperature=85000直接比。3.2 thermal_cooling_device:执行端口的统一抽象冷却设备的抽象同样是一个大结构体,核心操作函数集集中在thermal_cooling_device_ops里:struct thermal_cooling_device_ops { int (*get_max_state)(struct thermal_cooling_device *, unsigned long *); int (*get_cur_state)(struct thermal_cooling_device *, unsigned long *); int (*set_cur_state)(struct thermal_cooling_device *, unsigned long); };这三个回调的含义很直观:get_max_state告诉thermal框架你支持多少档位,get_cur_state报告当前档位,set_cur_state让执行器切到指定档位。cpufreq cooling device的档位就是来自cpufreq_table中的频率档数,devfreq cooling的档位对应设备支持的工作频率数量,风扇的档位就由驱动自己定义了。注意set_cur_state是同步还是异步取决于具体驱动。有些风扇驱动里PWM设置完还需要等待转速稳定,这时如果governor频繁下发指令,驱动要对相同的state请求做去重处理。内核里很多cooling device驱动都维护了curr_state的缓存,只有当新state不同时才真正操作硬件,这也是我建议大家写自己的cooling device驱动时必须保留的一个习惯。3.3 thermal_instance与绑定关系光有zone和cooling device还不够,两者之间需要一个“接线”的环节。这个接线就是struct thermal_instance,它记录了某个cooling device被绑定到某个zone的某个trip点,并且包含两个关键属性:upper和lower,即该冷却设备在这个trip点下允许调节的档位范围。为什么需要上下限?举个实际例子:CPU调频冷却设备的档位范围是0到N,你绑到临界高温trip时可能只想让它最高降到某个中间频率,而不想直接干到最低频。这时设置upper=某档就行。设备树里cooling-device = <&cpu_cooling 0 2>这种写法,意思就是绑定时的档位范围是从0到2,可以有效限制governor的调节空间。这个设计非常实用,我也强烈建议在初始调试阶段,通过限制upper/lower来缩小governor的动作范围,这样排查问题时会容易很多。3.4 trip point:温度与动作的连接点Linux thermal框架中trip point(温度阈值点)经历了一次比较大的API调整,老的代码里是get_trip_temp、get_trip_type多个回调函数,新内核(5.x后期开始)收敛成了struct thermal_trip这样一个描述性结构:struct thermal_trip { struct list_head list; int temperature; // 触发温度(毫摄氏度) int hysteresis; // 滞回值(毫摄氏度) enum thermal_trip_type type; // passive/active/critical/hot };trip的type有四种:passive:被动冷却,通常绑定调频/调压这类手段,CPU温度超过该点会触发降频active:主动冷却,通常是风扇等外部冷却设备hot:热警告,触发系统事件通知,但还不到关机力度critical:临界温度,达到后直接触发系统关机(orderly_poweroff),防止硬件烧毁我在实际配置时,trip点的设定需要结合产品的散热结构找规律。比如某款外壳散热条件不错的盒子,我会把passive设在75度、hot设在85度、critical设在95度。但如果产品是密闭无风扇的,passive就得提前到65度左右,因为无主动散热时热惯性大,等到85度再降频就来不及了。4. 设备树中的thermal-zones配置实战4.1 一个完整的配置片段拆解设备树是thermal模块在嵌入式设备上的主战场。以最常见的配置方式为例,一段典型的thermal节点长这样:thermal_zones: thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; polling-delay = <1000>; thermal-sensors = <&tsens 0>; trips { cpu_alert0: cpu-alert0 { temperature = <75000>; hysteresis = <2000>; type = "passive"; }; cpu_alert1: cpu-alert1 { temperature = <85000>; hysteresis = <2000>; type = "hot"; }; cpu_crit: cpu-crit { temperature = <95000>; hysteresis = <0>; type = "critical"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu_cooling 0 2>; }; map1 { trip = <&cpu_alert1>; cooling-device = <&cpu_cooling 3 6>; }; }; }; };这里有几个点值得展开讲:polling-delay-passive和polling-delay是两种模式的轮询间隔。前者在温度超过passive trip之后启用,后者是正常状态下的温度轮询间隔。为什么要分两种?因为正常状态没必要高频读取温度浪费功耗,而进入被动冷却状态后,温度变化快,需要更高频的采样以便及时调整。这个设计我很认同,调试时也可以利用它省事——把polling-delay调大,被动轮询间隔调小,温度变化就能更灵敏地反映在sysfs中。trips子节点定义温度阈值,cooling-maps子节点做绑定。map0将alert0这个trip和cpu_cooling这个冷却设备绑定,后面的0 2就是我在3.3节提到的上下限档位。thermal-sensors = <&tsens 0>指定了温度传感器。tsens是高通平台常见的温度传感器IP,其他平台可能是&temp_sensor或者&bandgap。这里第二个参数0代表传感器通道索引。4.2 冷却设备节点的写法刚才的配置里引用了cpu_cooling这个节点,它是什么?在设备树里,cpufreq冷却设备通常不需要单独写节点,而是通过cpufreq-cooling机制自动注册。有些平台为了更精细的控制会显式定义:cpu_cooling: cpu-cooling { compatible = "thermal-cooling-device"; #cooling-cells = <2>; cooling-min-level = <0>; cooling-max-level = <6>; };#cooling-cells = <2>表示这个冷却设备在bind时需要两个参数:最小档位和最大档位。这解释了为什么cooling-maps里写<&cpu_cooling 0 2>是两个值。手动写cooling设备节点的另一个原因是:当你想用cpufreq冷却,但希望暴露给thermal系统的档位数量不等于实际cpufreq表里的档位数时,可以通过这种方式做压缩映射。4.3 设备树校验和常见报错写完设备树后,我的习惯是在板子上启动后用以下方式快速验证:# 查看设备树中thermal zone的解析结果 ls /sys/class/thermal/ cat /sys/class/thermal/thermal_zone0/type # 查看内核是否成功注册了所有cooling device cat /sys/class/thermal/cooling_device0/type如果设备树解析失败,dmesg里会有类似thermal_sys: Failed to find 'trips' node或者failed to build thermal zone的提示。最常见的错误原因有两个:一是trips节点里type字符串写错(必须是passive/hot/active/critical),二是thermal-sensors引用的phandle写错或传感器驱动没有probe成功。遇到第二种情况时,先确认传感器驱动独立加载是否正常,再用cat /sys/class/thermal/thermal_zone*/temp看温度节点是否存在。另外特别提醒:不要在设备树里给同一个trip写两个相同的温度值。内核里thermal_zone_device_register会检查trip排序合法性,如果发现trip点没有按温度递增排列,会拒绝注册zone。这个坑我踩过——当时想加一个active类型的低温柔控点,结果排在了passive前面,整个zone直接没起来。5. thermal framework的初始化与注册流程5.1 从内核启动到设备就位的完整链路要理解thermal模块每个环节,有必要过一遍初始化流程。当内核启动时,thermal_core.c中的thermal_init作为子系统入口被调用。它做几件关键的事:注册thermal_class类,这样sysfs里才出现/sys/class/thermal这个目录。初始化全局的thermal_tz_list、thermal_cdev_list两个链表,分别管理所有zone和冷却设备。注册governor管理机制,加载step_wise等默认governor。注册热区的genhd和平台驱动支持,如果设备树存在thermal-zones节点,后续的of_parse_thermal_zones会逐个解析并创建zone。在设备树平台上,真正干活的是thermal_of.c。of_parse_thermal_zones函数遍历设备树中的thermal-zones节点,对每个节点执行thermal_zone_of_add_sensor和后续的thermal_zone_device_register。整个过程有个值得一提的设计:thermal zone的注册延迟。传感器驱动可能注册得比thermal框架晚,为了解决先后顺序问题,设备树解析并不是一次完成的。如果发现thermal-sensors指向的驱动还没ready,内核会把zone放到一个waiting list里,等传感器驱动probe完成并调thermal_zone_of_sensor_register时再真正完成zone的创建。这个机制导致一个很常见的排查场景:传感器驱动没加载,zone节点就不存在。5.2 governor的注册和选择逻辑governor并不是一开始就全部加载,而是通过thermal_governor_register动态注册的。内核里step_wise是默认内建的,power_allocator也是比较常用的。启动后你可以通过sysfs看到:cat /sys/class/thermal/thermal_zone0/policy这个文件显示当前zone使用的governor名称。如果需要切换,直接echo:echo "power_allocator" > /sys/class/thermal/thermal_zone0/policygovernor的选取逻辑在thermal_core.c的thermal_zone_device_set_policy中:如果手动设置,就用指定的;否则使用thermal_zone_params里的governor_name;都没有则回落到step_wise。看到这里你应该明白:governor是可以在运行时动态切换的。这在调试时是利器——比如怀疑某个zone的调节过于激进,可以直接切换到user_space,自己手动写cooling state,进行对照实验。``bash切换到user_space governor,手动控制echo "user_space" > /sys/class/thermal/thermal_zone0/policy echo 3 > /sys/class/thermal/cooling_device0/cur_state`user_space`这个governor很有意思,它相当于把调节权完全交给了用户态。你手动设置`cur_state`,thermal模块不会主动干预。这是排查具体cooling device是否正常的首选方式——可以直接绕过策略算法测执行器件。 ## 6. governor算法细节与选型心得 ### 6.1 step_wise:经典且直观的逐步调节 `step_wise`是内核默认governor,也是我建议大家最先读源码的算法。它的调节逻辑在每个轮询周期内,根据当前温度与trip point的关系和温度趋势来做决策: - 如果当前温度高于某个trip点对应的温度,且温度在上升,则冷却设备**上调一档** - 如果温度高于trip点但趋势平稳/下降,则保持当前档位 - 如果温度低于trip点减去滞回值,且温度在下降,则下调一档 源码中体现为`step_wise_throttle`函数,它遍历该zone下所有trip点,找到当前温度所处的区间和趋势方向。这里有个关键细节:**step_wise是“基于趋势”的**,而不是“基于绝对值”的。它内部使用`temperature == last_temperature`判断稳定、`temperature > last_temperature`判断上升。 这个细节在实测中很有感:如果温度传感器噪声大,读数来回抖,step_wise可能会频繁升降档。缓解办法有两个:一是增大trip的`hysteresis`值,二是调大`polling-delay-passive`让决策更慢。我自己遇到过传感器瞬间尖刺导致风扇突然满转的情况,加了滞回之后明显好转。 ### 6.2 power_allocator:为功耗预算而生 `power_allocator`是另一种完全不同的思路。它不直接看温度绝对值来触发动作,而是引入了“功耗预算”的概念:zone有一个`sustainable_power`(可持续散热功率),如果当前功耗超过这个值,就把多出来的部分作为需要削减的量,然后换算成cooling state让冷却设备执行。 用PID控制策略来描述power_allocator的内核逻辑再合适不过了。它根据当前温度与目标温度的误差计算需要调整的功耗,再加上一个前馈项(当前功耗预估),最终算出目标功率。具体的换算链路是:`estimate_power`估算当前功耗 -> `pid_controller`算出调整量 -> `power_to_state`将功率需求映射到cooling state。 这个governor在移动设备/笔记本上很常用,优点是能主动控制功耗而不只被动降温,缺点是需要配置正确的`sustainable_power`和PID参数,初期调参痛苦,参数不合适时风扇反而会振荡。如果要用它,设备树中需要: ```dts trips { cpu_alert0: cpu-alert0 { temperature = <80000>; hysteresis = <2000>; type = "passive"; }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu_cooling 0 6>; }; };同时还需要在zone节点中设置:cpu-thermal { sustainable-power = <2500>; // 毫瓦,表示散热系统能持续带走2.5W };下面这个初始化代码片段是用户在驱动中配置PID参数的参考方式:#include <linux/thermal.h> static struct thermal_zone_params tzp = { .governor_name = "power_allocator", }; static int __init thermal_zone_init(void) { struct thermal_zone_device *tz; tz = thermal_zone_device_register("cpu-thermal", 0, NULL, &tzp, NULL, NULL, 0, 0); if (IS_ERR(tz)) return PTR_ERR(tz); return 0; }6.3 fair_share等其他governor的适用场景除了上面两个,内核还提供fair_share、bang_bang和user_space等governor。fair_share是早期的简单权重分配算法,它按cooling device的权重比例分配散热需求,在只有单个zone、单种冷却设备时比较鸡肋。bang_bang是开关控制,适合只有开/关两种状态的风扇等设备,启停频繁的缺点很明显。user_space更像是调试工具而非常用生产方案。选型上我给的建议很简单:没有特殊理由就用step_wise,要做动态功耗控制再换power_allocator。step_wise经过了大量设备的验证,行为可预期,参数少,调起来快。power_allocator理论更优,但sustainable_power的准确性直接决定控制效果,而且这个值跟环境温度、散热条件强相关,很难一劳永逸地标定。6.4 自研governor的切入方式如果你需要实现一个非常特定的温控策略(比如结合产品外壳温度做预测控制),内核支持注册自己的governor。实现方法很直接:static struct thermal_governor my_governor = { .name = "my_governor", .throttle = my_throttle, .thermal_zone_trip_update = my_trip_update, }; thermal_governor_register(&my_governor);.throttle函数会在每个轮询周期被调用,你在这个函数里读取tz->temperature,然后遍历tz->thermal_instances列表,对每个instance调用thermal_cdev_update(cdev)设置目标状态。这个接口非常灵活,几乎可以做任何你能想象的控制逻辑。但我得提醒一句:内核对governor的调用是在打上thermal_zone->lock的情况下进行的,你的实现里不能做耗时操作,更不能在持锁状态下调用可能sleep的函数。否则你会在压力测试时碰到奇怪的调度延迟甚至死锁。7. 实际调试中的经验与踩坑记录7.1 让温度“动起来”的调试思路调试thermal最基础也最困难的事情,就是让温度可靠地上升。我的常规做法分几步:第一步,确认传感器读数是真实的:cat /sys/class/thermal/thermal_zone0/temp如果温度完全不动,先检查传感器驱动,使用i2cdetect确认i2c地址正确。第二步,用负载工具拉高温度。CPU场景我常用:# 多核满负荷小工具 stress-ng --cpu 8 --timeout 300 &GPU场景可能需要持续跑渲染负载,或简单循环跑一定负载。第三步,在负载拉起来的同时,实时观察:watch -n 1 cat /sys/class/thermal/thermal_zone0/temp watch -n 1 cat /sys/class/thermal/cooling_device0/cur_state通过同时看温度和冷却状态的变化,你能判断整套链路是不是在正常工作。如果温度涨上去了但cur_state一直是0,那就说明问题出在governor或trip判断上。7.2 一个典型的“能读到温度但没降频”排查实录有块板子出现这样的现象:温度升到90度,但CPU频率纹丝不动,系统直到critical关机。我排查的思路是:第一步查zone信息:cat /sys/class/thermal/thermal_zone0/type # 确认zone存在 cat /sys/class/thermal/thermal_zone0/trip_point_0_temp # 确认trip点存在结果正常。第二步查policy:cat /sys/class/thermal/thermal_zone0/policy输出step_wise,正常。第三步查绑定关系:cat /sys/class/thermal/thermal_zone0/cdev0_type ls /sys/class/thermal/thermal_zone0/cdev0_trip_point如果这里发现cdev0不存在,就是绑定关系没建立。再到设备树里看cooling-maps节点,很可能cooling-device引用的phandle和实际冷却设备节点对不上,或者绑定的trip节点名称跟trips里定义的不一致。还有一种很隐蔽的情况:zone注册成功了,cdev也注册了,但trip type和governor的触发逻辑不匹配。比如trip设成了hot类型,而step_wise只对passive和active类型的trip做调节,hot只上报事件不改频率。这就解释了“温度超了但就是不动”的现象。后来把hot改成passive后立刻生效。这个坑非常容易踩,因为设备树里type字段写错了不会报任何错误,只是行为不符预期。7.3 冷却状态“卡住”和风扇突变问题还有一种常见故障:风扇进入高速档后回不来。step_wise governor调节档位依赖温度降到trip_temp - hysteresis以下。如果你把hysteresis设成0,而传感器温度又在阈值附近抖动,那么不满足“低于阈值”的条件,风扇永远降不下来。解决方案就是给trip设置合理的滞回值,比如3到5度。反过来,风扇突然满转的问题则通常是传感器噪声引起的。我遇到过一次给tsens输入的电压纹波过大导致温度读数跳变20度,step_wise检测到“温度骤升”就直接把冷却档位拉满。治本的方法是修硬件滤波,治标的方法是把polling-delay-passive调大,让噪声在多个采样周期内被平均掉。这里也体现出emul_temp调试节点的价值:# 手动模拟某温度,绕过传感器实际测量 echo 90000 > /sys/class/thermal/thermal_zone0/emul_temp注意不是所有内核都打开了CONFIG_THERMAL_EMULATION,这个配置项默认常在开发板上关闭。开启后就可以用固定值来验证trip触发逻辑,这是调试策略逻辑、绕开硬件问题的最有效手段。7.4 测试环境里容易忽略的细节温度传感器离热源太远,读到的温度和芯片结温差很多。我在某块板子上对比过:红外点温枪测外壳65度,tsens报的结温已经83度,两者差了整整18度。这意味着trip点设置必须对应传感器的实际位置特性,不能照搬datasheet里的结温建议值。散热条件差异巨大。同一块板子,裸板测试时温度平衡点可能比装进外壳后低10度以上。建议在产品最终形态的散热条件下做trip点标定,裸板状态下调出来的参数装上外壳后可能过保守或过激进。多路负载并行时要让温度有足够时间达到稳态。CPU、GPU、NPU同时跑的时候,温度上升有热惯性,通常需要几分钟才到平衡点。做控制策略验证时,每次改变负载条件后至少要等3到5分钟再下结论,否则阶段性的数据会误导你。7.5 常用调试接口速查表调试目标路径/命令预期行为查看当前温度/sys/class/thermal/thermal_zone*/temp输出毫摄氏度,如65000表示65°C查看zone使用governorcat /sys/class/thermal/thermal_zone0/policy显示step_wise或power_allocator等切换governorecho "power_allocator" > policy运行时切换策略查看冷却设备档位cat /sys/class/thermal/cooling_device0/cur_state显示当前冷却档位手动控制冷却设备echo 5 > /sys/class/thermal/cooling_device0/cur_state配合user_space governor使用查看冷却设备绑定ls /sys/class/thermal/thermal_zone0/cdev0_*显示该zone绑定的cdev信息模拟温度echo 90000 > /sys/class/thermal/thermal_zone0/emul_temp需要开启CONFIG_THERMAL_EMULATION查看内核日志`dmesggrep -i thermal`7.6 内核配置选项要点menuconfig里有几个选项跟你的调试体验强相关。CONFIG_THERMAL是总开关,CONFIG_THERMAL_OF负责设备树解析支持,CONFIG_THERMAL_EMULATION就是上面说的温度模拟开关。governor相关的有CONFIG_THERMAL_DEFAULT_GOV_STEP_WISE(设置默认governor)、CONFIG_THERMAL_GOV_POWER_ALLOCATOR、CONFIG_THERMAL_GOV_USER_SPACE。调试期建议把这些governor都编译进去,运行时随时切。还要注意,有些SoC平台有自己的私有thermal驱动,比如某些平台的“TM”(Thermal Monitor)模块,它可能不走标准框架而是直接在固件里做调频控制。碰上这种平台,你首先要确认板上实际生效的是不是标准thermal框架——查看启动日志里是否出现thermal thermal_zone0: ...之类信息,如果没有,那多半是跑在隐藏的固件逻辑上,设备树配得再标准也很难生效。8. 更进一步的学习路径建议8.1 源码阅读的有效顺序很多人问我源码怎么读,我的建议顺序是从“使用层”向“框架层”推进:先读设备树绑定文档:Documentation/devicetree/bindings/thermal/thermal-zones.yaml,搞清楚节点字段含义。再读drivers/thermal/thermal_core.c中的注册函数:thermal_zone_device_register和thermal_cooling_device_register,知道注册一个zone需要满足什么条件。然后读drivers/thermal/thermal_sysfs.c:看sysfs节点怎么暴露、temp、policy、trip_point_*这些文件背后的逻辑是什么。接着读drivers/thermal/step_wise.c和drivers/thermal/gov_power_allocator.c,理解两个典型governor的决策过程。最后看具体的驱动实现,比如drivers/thermal/cpufreq_cooling.c和drivers/thermal/devfreq_cooling.c,搞清楚cooling state如何映射到调频调压。读完这个序列,整个thermal子系统的全貌就清晰了。我也推荐把自己平台的传感器驱动和风扇驱动完整读一遍,那里面有很多frame结构对不齐导致的workaround,是真实世界的实战经验。8.2 从内核到用户态的扩展视野很多产品最终不用内核governor,而是走user_space模式,在用户态用专门的热管理服务做策略。这样做的好处是策略升级不需要重编内核,坏处是用户态拿到的温度数据延迟大一些,而且服务崩溃会导致温控完全失效。如果你做的是消费级产品,建议内核态保底(至少critical关机),用户态做增强策略。这两个层面不冲突:内核thermal保证不死机,用户态策略负责性能和体验的精细平衡。内核和用户态协作的标准接口是netlink事件。通过监听NETLINK_THERMAL事件,用户态进程能收到温度越限通知。感兴趣的可以看drivers/thermal/thermal_netlink.c,里面定义了事件类型和消息结构。8.3 结合硬件设计反向理解参数最后给个硬件视角的建议。thermal参数不是软件工程师凭空定的,它应该来源于硬件实测。理想的工作流程是:硬件工程师给出散热设计目标:比如“外壳最高温度不超过45度,芯片结温不超过85度”做热测试拿到芯片在不同负载下的结温曲线、散热系统在不同风扇转速下的散热能力软件据此反推trip点:结温85度对应散热极限,那passive点就应该设在75度,留出10度余量作为响应延迟补偿动态功耗测试还能算出sustainable_power,供power_allocator使用这个流程听起来理所当然,但实际项目里软硬件经常各干各的:硬件调完结构就给软件提“帮忙看看thermal”,软件拿到板子再自己摸索试探。结果就是个折中的、没有理论依据的配置。你如果能把这条链路打通,在项目里就是不可替代的桥梁角色。我个人在几个项目里反复体会最深的一点是:thermal不像其他内核模块那样“对和错”分明,它更像是一个调优过程。同样的配置在这块板子上完美运行,换一块散热稍差的板子就可能出问题。所以别指望抄一份设备树就能解决所有问题,关键还是理解里面的每个参数是怎么影响控制环路的,然后跟你手头硬件的热特性对应起来。把这个模块吃透后,你会发现它不仅用于降温,还串联起了调频、调压、风扇控制、甚至电池充电电流控制等多个子系统。从它入手去梳理整个系统的功耗和热管理路径,是一条性价比很高的学习路线。希望这篇整理出的框架和踩坑记录,能帮你少走一些弯路。