☰
Linux Thermal框架深度解析:设备树、Governor与温控实现
2026/10/8 13:45:09 网站建设 项目流程

做内核功耗这块的工程师应该都有同感:Linux的thermal framework在各子系统里算得上“门槛最友好、理解最绕”的一个。说友好,是因为它的抽象层次非常清晰,sensor、cooling device、governor三者分工明确,不像cpufreq那样牵一发动全身;说绕,是因为一旦涉及设备树里的trip point、cooling map、bind_params这些概念,再叠加不同governor的行为差异,很容易把思路带偏。

我在实际接触thermal framework之前,一直把它理解为“温度过高就降频”,直到自己动手把一套温控策略从设备树一直打通到cooling device回调,才发现这套框架的真正价值在于:它把“感知热量”“判定风险”“执行降温”这条链路标准化了。不管你的硬件是手机SoC、车载域控制器还是服务器CPU,只要按照框架的约定去注册和描述,内核就能替你完成大部分温控调度工作。

这篇文章想把thermal framework的通用架构串讲一遍,重点放在框架的角色模型、设备树描述方式、关键数据结构以及governor的行为差异上。适合那些已经在阅读内核源码、准备做温控策略定制,或者单纯想把功耗子系统整体补全的读者。

1. 先从问题本质说起:thermal framework到底在解决什么

在深入代码之前,我建议先花两分钟想明白一个问题:芯片厂商为什么不直接在硬件里做温控,非要操作系统插一脚?答案其实很朴素——硬件只能做“断电保护”级别的兜底,但真正的性能与温度平衡必须由软件根据场景动态决策。硬件过热保护通常是一个固定阈值:到了就断电或者强制降频,没到就完全不管。但用户手机上同时跑着游戏和充电时,系统可能希望温度到70度就开始限制充电电流,到80度才限制CPU频率;而外接散热背夹时,系统可能希望延迟降频。这种动态的分级策略是硬件做不了的,而thermal framework就是给内核提供这套“分级策略”的执行框架。

我用一个生活类比帮初学者建立直觉:thermal framework就像一套中央空调管理系统。温度传感器是遍布房间的探头,负责上报“当前温度”;空调内机和风阀是降温设备,负责执行“加大制冷”或“停止制热”;而坐在中控室的管理员就是governor,它根据探头数据决定要不要下发指令、下多大力度。Linux把这三个角色硬性拆开,目的就是让这三层可以独立替换。你可以换掉传感器而不动降温策略,也可以在同一套硬件上更换governor而不用改设备树。

从这个角度再看代码,很多设计选择就顺理成章了。框架层通过thermal_zone_device抽象一个热区,一个热区绑定一个或多个温度传感器;通过thermal_cooling_device抽象一个降温执行器;通过thermal_governor抽象决策算法。热区负责收集温度并把它交给governor,governor根据当前温度与各trip point的关系决定“该不该动作”,最后通过binding关系把动作落到具体的cooling device上。整条数据流清晰到可以画在一张餐巾纸上。

还有一个容易忽视的出发点:这套框架设计时兼顾了“设备树描述”和“驱动注册”两种路径。老式驱动可以在代码里手动创建thermal zone,新式驱动则倾向于把zone的拓扑放在设备树里,驱动只负责注册sensor和cooling device。这个双轨设计导致阅读代码时会看到两套接口并存,后面我会单独把设备树路径讲清楚。

理解了“为什么需要它”,再回头看各个结构体就会觉得每个字段都有它的位置。接下来我按实际注册顺序拆解三大核心抽象。

2. 三大核心抽象:thermal zone、cooling device与governor

2.1 thermal_zone_device:热区的“信息中枢”

struct thermal_zone_device是整个框架里最核心的结构体,它代表一个被监控的热区域。一个SoC上通常会有多个zone:CPU cluster一个、GPU一个、充电IC一个、电池一个。每个zone有自己的传感器、自己的trip points、自己的governor策略偏好。

这个结构体里值得先记牢的几个字段:

struct thermal_zone_device { struct device dev; struct idr idr; /* ID分配 */ struct list_head thermal_instances; struct thermal_zone_device_ops *ops; /* zone操作回调 */ struct thermal_zone_params *tzp; /* governor相关参数 */ struct thermal_attr *trip_type_attrs; enum thermal_device_mode mode; /* enabled/disabled */ int temperature; int last_temperature; int passive_delay; int polling_delay; struct thermal_governor *governor; struct thermal_trip trips[]; /* trip点数组 */ };

ops里最重要的三个回调是get_temp、get_trip_temp和get_trend。get_temp由传感器驱动实现,返回当前温度;get_trip_temp返回某个trip point的阈值;get_trend返回温度变化趋势(上升/下降/稳定),governor决策时会用到。passive_delay和polling_delay决定轮询周期:没有主动中断时,内核按这个周期周期性读取温度;如果驱动支持中断上报(比如lm_sensors系列很多芯片支持THERMAL_TRIP_HOT中断),到达阈值时会主动触发通知。

关于热区数量,我见过有人把每个CPU核心单独建一个zone,结果governor要同时管理8个zone,cooling device的绑定关系乱成一团。实际上多数平台用一个cluster级zone就够了,因为硅片的温度传感器采样区域本来就有限,拆得过细既增加轮询开销,又容易造成各zone策略互相打架。

2.2 thermal_cooling_device:执行降温的“受控负载”

thermal_cooling_device抽象所有“可以被削弱以降温”的设备。CPU调频器、GPU调频器、风扇、充电电流限制器、显示屏背光,都属于cooling device。它的核心是ops->get_max_state和ops->set_cur_state两个回调:

static int cpufreq_cooling_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) { /* 返回可用的调频档位数量 - 1 */ } static int cpufreq_cooling_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) { /* 将频率限制到对应档位 */ }

max_state通常是“降频档位数减一”,一个支持20档频率的CPU,max_state可能就是19。state越大,降温力度越强,性能削弱越多。这套约定让governor不需要知道具体设备是什么,只需要说“把state从0调到2”,风扇还是CPU都会照做。

我提醒一点:cooling device的state语义在不同驱动里并不完全统一。有的驱动state 0就是最大性能,有的驱动state 0反而是最大散热能力(对风扇而言)。查看一个陌生cdev的行为时,不要只看max_state,一定要打开驱动确认set_cur_state里state增大时是增强散热还是削弱功能。这类不对称导致的问题,在联调时特别容易踩。

2.3 thermal_governor:温控策略的“大脑”

governor是纯软件模块,不直接接触硬件,它的职责是根据zone的温度、温度趋势、trip point的触发状态,计算出“要不要调整某个cooling device的state”。Linux主线常见的有四个:

  • step_wise:按步进方式逐级调整,温度高过trip就升一档state,降到阈值以下就降一档,适合大多数场景。
  • power_allocator:基于PID控制器的IPA(Intelligent Power Allocator),适合需要精细功耗分配的移动SoC场景。
  • fair_share:按权重比例分配各cdev的散热义务,使用场景较少。
  • user_space:把决策权交给用户态,内核只上报温度。

governor在thermal framework里是一个链表中的节点,通过thermal_governor_register注册。每个热区可以在设备树或用thermal_zone_params指定使用哪个governor,也可以在sysfs里动态切换(thermal_zoneX/policy)。

我个人的理解是:governor的设计把“策略”和“机制”彻底分开了。机制部分是固定的——轮询温度、记录trip状态、调用binding关系;策略部分完全看governor的算法。所以当你觉得系统温控行为不合理时,第一步不是改设备树阈值,而是确认当前用的是哪个governor,它在这种场景下天生会怎么表现。后面有一章我会专门展开step_wise和power_allocator的行为差异。

三大抽象理解完之后,热区、执行器、决策者都有了,但还缺最后一块拼图——它们如何被组装到一起。答案在设备树和注册接口里。

3. 设备树描述与驱动注册:一条链路怎么串起来

在现代嵌入式Linux开发中,90%的thermal拓扑都在设备树里描述。这样做的好处是不用重新编译内核就能调整阈值和绑定关系。很多团队做散热调优时,改dts、重编dtb、重启验证,一套流程下来比改驱动快得多。

3.1 设备树里的thermal node

一个典型的thermal zone节点是这样写的:

thermal-zones { cpu_thermal: cpu-thermal { polling-delay-passive = <250>; /* 被动冷却时的轮询周期(ms) */ polling-delay = <1000>; /* 无被动冷却时的轮询周期(ms) */ thermal-sensors = <&tsens0 1>; /* 绑定的传感器 */ trips { cpu_alert0: cpu-alert0 { temperature = <85000>; /* 85度 */ hysteresis = <2000>; /* 回差2度 */ type = "passive"; /* 被动触发 */ }; cpu_crit: cpu-crit { temperature = <105000>; /* 105度 */ hysteresis = <0>; type = "critical"; /* 临界触发 */ }; }; cooling-maps { map0 { trip = <&cpu_alert0>; cooling-device = <&cpu0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>, <&cpu1 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; map1 { trip = <&cpu_crit>; cooling-device = <&fan0 THERMAL_NO_LIMIT THERMAL_NO_LIMIT>; }; }; }; };

逐段解释一下关键字段:

  • polling-delay-passive:当zone里有passive类型trip被触发后,内核切换成更短的轮询周期,这个值就是250ms。没触发时用polling-delay的1000ms。这个“触发后加速采样”的设计,是节省功耗和响应速度之间的经典折中。
  • thermal-sensors:字符串格式是<&phandle sensor_id>,sensor_id是传感器在该控制器里的通道号。比如tsens0 1表示tsens0控制器上的第1路传感器。不写sensor_id默认取第0路。
  • trips里的hysteresis是回差值,防止温度在阈值附近反复横跳导致cooling device频繁抖动。85度触发,83度解除,比没有回差的时候稳定得多。
  • type支持passive、active、hot、critical四种。hot通常用于通知用户态准备关机,critical由内核直接触发关机或重启。需要特别注意:critical类型的处理不经过governor,而是直接在框架层做紧急动作。

cooling-maps里的cooling-device属性是三元组:<phandle min_state max_state>。THERMAL_NO_LIMIT即0和MAX_STATE,表示不限制这个cdev的调节范围。你完全可以把某个cdev锁定在state 2到state 5之间,这在多zone共用同一个cdev的场景下非常好用。

3.2 驱动侧的注册接口

设备树描述好了,驱动侧要把对应的sensor和cooling device“填”进框架里。新的API推荐使用devm_thermal_of_zone_register:

static int my_sensor_probe(struct platform_device *pdev) { struct thermal_zone_device *tz; struct thermal_zone_device_ops *ops = devm_kzalloc(&pdev->dev, sizeof(*ops), GFP_KERNEL); ops->get_temp = my_sensor_get_temp; ops->get_trend = my_sensor_get_trend; tz = devm_thermal_of_zone_register(&pdev->dev, 0, data, ops); if (IS_ERR(tz)) return PTR_ERR(tz); return 0; }

这里第二个参数是sensor id,必须和dts里thermal-sensors = <&tsens0 1>中的1一致。devm_thermal_of_zone_register会自动去寻找设备树中引用这个phandle和id的thermal-zone,把ops填进zone的驱动回调里。

cooling device的注册同样简单:

static int my_fan_probe(struct platform_device *pdev) { struct thermal_cooling_device *cdev; struct thermal_cooling_device_ops *ops = devm_kzalloc(&pdev->dev, sizeof(*ops), GFP_KERNEL); ops->get_max_state = fan_get_max_state; ops->get_cur_state = fan_get_cur_state; ops->set_cur_state = fan_set_cur_state; cdev = devm_thermal_of_cooling_device_register(&pdev->dev, "my_fan", fan_priv, ops); return PTR_ERR_OR_ZERO(cdev); }

字符串"my_fan"是cdev的名字,会在sysfs里显示为cooling_device0的type。如果你的cdev需要在dts里被引用,节点里要配#cooling-cells = <2>,这两个cell就是上面提到的min_state和max_state:

fan0: fan { compatible = "my,fan-ctrl"; #cooling-cells = <2>; };

我在实际项目中遇到过一个问题:sensor驱动加载顺序如果晚于thermal zone的解析,设备树里绑定关系可能会注册失败。虽然新接口用了devm_,但依赖的sensor phandle如果没有先probe,thermal_of_zone_register会因为找不到传感器而返回-EPROBE_DEFER。这时候不要慌,这是正常的延迟探针机制,只要驱动本身支持probe deferral,系统会在sensor就绪后自动重试。

3.3 两种注册路径的历史包袱

前面提过框架有“双轨制”,这里展开说一下。老接口thermal_zone_device_register需要驱动自己把trip points以数组形式传给框架,比较繁琐;新接口thermal_zone_of_sensor_register和devm_thermal_of_zone_register改为从设备树读取trips和cooling maps。内核新版本里,thermal_zone_device_register的调用者正逐步向of-based路径迁移。

阅读源码时,在thermal_core.c里你会看到两套非常相似的注册流程。我建议新手优先吃透of-based路径,因为现在绝大多数平台都走这条。如果你做的是纯x86服务器、没有设备树的环境,才需要回头研究老接口。

4. 热区的灵魂:trip point、斜率与被动冷却

4.1 trip point分级和生效顺序

trip point是热区的灵魂——它定义了“热事件”的等级。内核根据当前温度与所有trip阈值的比较结果,维护一个“已触发级别”的概念。从低到高依次为:normal -> active(散热片开启)-> passive(被动限制性能)-> hot -> critical。

你可能会奇怪:为什么active反而比passive级别低?这其实是散热手段的语义约定。active对应主动散热设备(风扇、空调),触发它是“加大散热”,不牺牲性能;passive对应被动散热手段(降频、限流),触发它意味着系统准备牺牲性能。所以正常情况下系统先尝试主动散热,不够再降性能。

理解这个顺序很重要,因为不同governor对同一组trip point的反应差别很大,而trip的type字段决定了它在框架层走哪条处理路径。critical级别的trip触发后,thermal_zone_critical会直接调用orderly_poweroff或panic,这个过程不经过governor也不经过cooling device。hot级别则触发thermal_zone_device_update让用户态有机会介入。

4.2 斜率(slope)和偏移(offset)——容易出错的校准

每个sensor驱动都可以通过thermal_zone_params描述温度和ADC原始值之间的线性关系。最常见的是slope和offset两个参数,关系式是:

temperature(毫摄氏度) = raw_value * slope + offset

不校准slope和offset时,内核默认按1:1关系直接使用sensor上报值。但实际硬件中,sensor离发热源的距离不同、封装导热系数不同、系统误差不同,上报值可能与真实结温偏差很大。我见过一个平台,sensor上报温度比实际结温低12度,导致critical trip形同虚设。校准后的斜率修正确实能解决一部分误差,但要注意:slope和offset的校正在不同厂商的sensor驱动里使用方式不同,有的驱动直接把它们写进设备树扩展属性里,有的驱动通过thermal_zone_params传给框架。一定要先看驱动的get_temp实现,确认它到底怎么使用这两个参数,再决定调谁。

4.3 被动冷却的真正含义

passivetrip触发后,框架会把zone的passive标志置位,同时将轮询周期从polling-delay切换到polling-delay-passive。这一步的目的是“加速感知”:当系统已经进入被动降温状态,说明热风险正在聚集,此时保持每秒采样一次可能不够及时,收紧到250ms能更快响应温度回落。

但这个机制有个隐藏代价:高频轮询本身会唤醒CPU,带来额外的功耗开销。如果passivetrip触发频繁且轮询周期设得太小,可能出现“为了降温反而增加了额外的发热”的负优化。我在调试一个车载设备时,把polling-delay-passive设成100ms,结果温控效果的改善微乎其微,功耗却涨了一截。经验建议:被动轮询周期通常在200~500ms之间,不要低于100ms。

5. governor工作机制拆解:step_wise 与 power_allocator

5.1 step_wise:简单但不粗暴

step_wise是全志、瑞芯微等平台最常见的governor,它按“当前温度处于哪个trip级别”来决定上下调整cdev的state。它的核心是thermal_zone_trip_update里对每个trip做遍历:

static void step_wise_consider(struct thermal_zone_device *tz, struct thermal_trip *trip, struct thermal_instance *instance) { if (trip->temperature <= tz->temperature) { /* 温度超过该trip阈值:考虑上调 */ if (instance->target < instance->upper) instance->target = instance->target + 1; /* 步进加一 */ } else { /* 温度低于阈值:考虑下调 */ if (instance->target > instance->lower) instance->target = instance->target - 1; /* 步进减一 */ } }

实际代码比这复杂一些,加入了trend的判断——温度趋势还在快速上升时,即使刚超过trip点,也可以直接跳到更高级别;温度趋势下降时,则尽量保持当前level防止频繁波动。但核心思想就是:一次只动一档,不贪心。

step_wise最大的优势是行为可预测。调一档看温度变化,不行再调一档,整个曲线很平滑。它的劣势也很明显:升温很快时,逐级上调可能跟不上温度爬升速度;必须依赖合理的trip间距和合理的轮询周期。如果你的平台温升速率很快(比如大核突然满载),step_wise反应可能偏慢,此时可以考虑把trip点之间的间距调小,让多级降温更快触发。

5.2 power_allocator:PID式的精细管理

power_allocator(IPA)是thermal框架里最“聪明”的governor。它不是简单根据当前温度越没越阈值来动作,而是把温度偏差(当前温度与目标温度之差)当作PID控制器的输入,输出一个“允许功耗值”,再把这个功耗分配权重分摊给各个cooling device。

IPA的核心参数有:

  • sustainable_power:系统在稳态下能持续散热的功耗瓦数。
  • k_po、k_pu、k_i:PID控制器的比例、积分增益。
  • trip_point:它只使用一个passive类型的trip作为“目标温度”,比如80度。温度超过80,PID输出负向修正,削减允许功耗;低于80,逐步恢复功耗。

我在移动SoC平台上体验过IPA的效果:在视频播放场景下,step_wise会把频率一档一档往下降,直到源温度回落到阈值以下,帧率波动明显;而IPA会提前预算功耗,把CPU和GPU的功耗一起压缩,帧率虽然整体降了一点,但波动小很多。代价是需要调PID参数,k_pu太大容易震荡,sustainable_power估得太离谱会让governor完全失效。

如果你是新接触IPA,我建议先在几个固定场景下记录sustainable_power的估计值,再跑一次阶跃负载实验观察温度响应曲线,看看有没有过冲和振荡。PID参数调优没有捷径,日志里看thermal_zoneX/temp和cdev的cur_state变化曲线是最直观的方法。

5.3 governor的运行时切换

每个zone都暴露一个sysfs节点policy,可以在系统运行时切换governor:

# 查看当前governor cat /sys/class/thermal/thermal_zone0/policy # 切换为step_wise echo step_wise > /sys/class/thermal/thermal_zone0/policy

这个特性在联调阶段非常有用,可以先在用户态暴力切换验证不同算法行为,再决定把哪个governor写死到代码里。注意:不是所有governor都能在所有zone上正常工作,比如power_allocator就需要tzp->sustainable_power等参数已经被正确初始化,否则切过去之后cdev可能完全不动。

6. 调试实战:sysfs接口、模拟温度与常见问题排查

6.1 thermal_zoneX目录里有什么

/sys/class/thermal/thermal_zoneX下的主要节点:

/sys/class/thermal/thermal_zone0/ ├── mode # enabled/disabled,可以手动禁用温控 ├── policy # 当前governor ├── temp # 当前温度,单位毫摄氏度 ├── type # zone类型,如cpu-thermal ├── trip_point_0_temp ├── trip_point_0_type ├── trip_point_1_temp ├── trip_point_1_type └── uevent

trips的个数和设备树里的trips节点一一对应。判断trip是否触发,可以直接看temp和trip_point_X_temp的相对关系。mode写成disabled可以让整个zone失效——系统重启前不会再做任何温控动作,这招在排查“是不是温控导致性能异常”时特别好用,测试完务必记得恢复。

6.2 用emul_temp模拟温度

大多数thermal sensor驱动支持emul_temp节点,用于模拟温度输入。它的存在价值极大:可以在不加热硬件的情况下验证governor行为和cdev联动。

# 先把温度模拟到90度 echo 90000 > /sys/class/thermal/thermal_zone0/emul_temp # 观察对应cooling device的state变化 cat /sys/class/thermal/cooling_device0/cur_state # 恢复正常 echo 30000 > /sys/class/thermal/thermal_zone0/emul_temp

用这个技巧,我可以在板子上反复验证“85度触发降频一档,95度触发降频两档,回落到83度恢复一档”的完整链路,而不用真的去加热芯片。注意:emul_temp需要sensor驱动显式实现set_emul_temp回调,不是所有驱动都有。没有的话只能靠热风枪或大电流负载,调试效率差很多。

6.3 常见问题与排查思路

结合我在项目里遇到的三个高频问题,给出排查路径:

1. cdev完全不动作,但温度已经超过trip阈值

先查两件事:一是cooling-maps里的绑定关系是否配了,trip引用的phandle是否正确;二是确认该zone的governor是不是user_space——用户态governor不会自动调整cdev,需要应用层写入,如果没写,自然一动不动。我在调试一个智能硬件时,发现在设备树里配了冷却映射但没配任何governor,默认就成了user_space,现象就是“温度爆了但风扇不动”。

2. cdev动作了但温度持续不降

优先怀疑cooling device的降温能力不足或state上限不够。检查cur_state是否已经达到max_state;如果已经封顶,要么加大cdev能力(比如风扇转速上限),要么降低功率源头(比如限制整机功耗)。还有一种可能是传感器位置离发热源太远,导致反馈滞后,降温动作已经做了但感知不到。这种情况除了传感器物理位置调整,只能靠调大hysteresis让系统不那么敏感,避免反复触发。

3.thermal_of_zone_register返回-EPROBE_DEFER

本质是依赖的sensor或cdev还没注册。排查方法很简单,cat /sys/class/thermal/thermal_zone*/type看看已注册的zone列表,对比设备树里期望的zone数是否一致。如果在sensor驱动probe成功后zone仍然没出现,用/sys/kernel/debug/device_component或dmesg看具体报错——多数情况是thermal_sensor的phandle解析失败,比如设备树里写错了sensor id。

6.4 调温控策略时的记录习惯

最后分享一个工作习惯:每次调温控参数,我都会把dts改动、负载场景、温度曲线、cdev行为一并记录。因为温控调优的验证周期很长,改一个阈值可能要跑一整轮压测才发现问题,如果只记“把阈值从85改到90”,两周后根本想不起来当时为什么这么改。我一般会在dts的注释里写清楚修改日期和动机,比如“2024-03-12:游戏场景下85度触发导致帧率抖动,上调到88度,待观察充电场景”。

这套记录方式的回报体现在下一次团队接手时——散热调优的坑,很多不是技术深,而是历史包袱和上下文丢失叠加出来的。

7. 从本文出发,建议的下一步阅读路径

如果你刚把这篇文章读完,我建议按照以下顺序继续深入:先打开drivers/thermal/thermal_core.c,把zone的注册、更新流程走一遍,特别关注thermal_zone_device_update这个函数,它是整个框架的“心跳入口”。然后看thermal_zone_trip_update,理解trip触发后如何遍历cdev并调用governor的回调。接着选一个你当前平台在用的sensor驱动(比如qcom-spmi-temp-alarm、rockchip_thermal),对照设备树描述理解sensor id和trip的映射关系。最后再回到step_wise.c逐行读,配合emul_temp实测,基本就能把整个框架从“抽象概念”内化成“肌肉记忆”。

如果项目里涉及IPA,也不要急着上全部PID参数,建议先在用户态用policy节点切换验证,再加trace_thermal事件和应用层日志辅助分析,逐步积累原始数据。温控调优从来不缺理论,缺的是一手日志和可靠的复现方法。

就我个人经验来看,thermal framework的复杂不在于某个单独的机制,而在于它同时横跨了设备树解析、驱动注册、策略调度、sysfs接口、紧急动作五个层面。一次只吃透一个层面,比试图一次读完所有代码要有效得多。希望这篇架构梳理,能给你接下来的源码阅读提供一张可靠的地图。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询