ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Linux thermal framework 架构解析:从 thermal zone 到 governor 的完整链路

Linux thermal framework 架构解析:从 thermal zone 到 governor 的完整链路 做内核功耗和热管理这些年我越来越觉得 thermal framework 是 Linux 里最容易被忽略、但最影响体验的子系统。手机玩两局游戏就锁帧、笔记本开渲染风扇直接起飞、工业设备高温下莫名降频这些现象背后基本都是它在干活。这篇文章是功耗子系统系列的第九篇前面聊过 cpufreq、devfreq 和 PM QoS这次把 thermal framework 的通用架构完完整整梳理一遍适合想搞懂热管理骨架的驱动开发者也适合刚入内核功耗方向的人做全局认知。说它不复杂是因为核心就三个角色热区thermal zone负责量温度冷却设备cooling device负责执行降温动作governor策略器在中间做决策。想清楚这三件事后面的数据结构、设备树配置、运行流程自然就顺了。这篇文章不贴大段源码重点讲清楚设计思路、配置方法和实际中会踩的坑。1. thermal framework 到底在解决什么问题1.1 没有框架的时代热管理是怎么写的在很多老产品和自研 RTOS 上热管理往往是一段散装逻辑驱动读一次温度寄存器发现超过阈值就直接调 cpufreq 接口降频或者拉高 GPIO 开风扇。这种写法不是不能用但项目一复杂就崩。芯片上有四五个温度传感器CPU、GPU、DDR、外壳各一个每个都有一套阈值和动作代码里到处是 if-else要理清楚哪段代码管哪个传感器得翻半天。更麻烦的是产品形态一变整套逻辑就得推翻重来。手机散热能力和服务器完全不一样同一个芯片在不同产品里允许跑到的温度也不同散装代码很难把这些差异干净地隔离出来。可以说热管理从来不是一个“能跑就行”的功能它直接决定产品的稳定性、寿命和用户体验所以内核才需要一套通用框架来统一这件事。1.2 三个角色一次看明白thermal framework 的核心抽象其实非常简单就是三个角色配合角色内核数据结构实际例子核心能力温度感知thermal_zone_devicetsens、hwmon、ACPI thermal zone上报当前温度、trip 阈值执行降温thermal_cooling_devicecpufreq cooling、风扇、devfreq cooling限频、调速、降功耗决策策略thermal_governorstep_wise、power_allocator决定降多少、什么时候降这个分工很像现实中的空调系统温度传感器只负责告诉房间现在多少度空调压缩机只负责制冷人governor根据温度和目标温差决定压缩机开几档。传感器不用关心压缩机的电机怎么转压缩机也不用操心温度怎么设定人和设备之间的约定就是“目标温度”和“档位”。这套抽象带来的好处是传感器驱动只需要老老实实上报温度不用知道上层策略冷却设备只需要实现一个带档位的执行器接口策略则完全独立内核默认提供几种厂商也可以自己写一个很特别的 governor。三层解耦之后换传感器不用改策略换产品形态不用改驱动。1.3 分层带来的好处因为按角色分层一套 zone 驱动可以在多个产品上复用策略可以在运行时切换。sysfs 里 policy 节点改一下系统的温控行为就从“保守降频”变成“激进压榨”这在工程上非常实用。新硬件接入的成本也很低芯片厂商换了一个温度传感器只需要提供一个新的 get_temp 实现产品方调整温度阈值不动驱动代码改设备树就行。这个影响范围远比想象中大小到智能手表、大到机架服务器主流 Linux 设备的热管理基本都跑在这套框架上。也就是说理解了这个通用架构你看到的每个thermal_zoneX、每个cooling_deviceX、每次莫名其妙的降频都不再是黑盒。2. 核心数据结构逐一定位2.1 thermal_zone_device温度世界的“传感器本体”thermal_zone_device 是整个框架的入口对象一个 zone 通常对应一个温度传感器或者一个被监控的区域。结构体字段在不同内核版本里会有变动但核心内容很稳定struct thermal_zone_device { char type[THERMAL_NAME_LENGTH]; // 比如 cpu-thermal struct device device; struct thermal_zone_device_ops *ops; // get_temp / set_trips struct thermal_trip *trips; // 阈值点数组 int num_trips; struct thermal_governor *governor; // 当前策略 int temperature; // 最近一次采样值 int passive_delay; int polling_delay; };type 字段就是你在/sys/class/thermal/thermal_zone0/type里看到的字符串大部分版本要求它唯一所以命名别乱来。ops 里最重要的回调是 get_temp它负责把当前温度写进 temperature 字段set_trips 则用于给硬件设定温度上下限实现中断式上报而不是傻乎乎地一直轮询。polling_delay 和 passive_delay 是轮询周期前者是正常情况下的采样间隔后者是系统进入被动降温有 passive 级别 trip 被触发后的高频采样间隔。这个设计很关键后面章节会展开讲。2.2 thermal_trip触发点与四类阈值trip 是 thermal 世界的“闹钟”用来描述温度到多少度该干什么事struct thermal_trip { int temperature; // 毫摄氏度 int hysteresis; // 回差毫摄氏度 enum thermal_trip_type type; };trip 类型有四种分别对应不同的触发语义类型典型用途常见动作ACTIVE主动散热开风扇、提风扇档位PASSIVE被动散热CPU/GPU 限频、限流HOT严重过热更激进地降性能CRITICAL过温保护优雅关机或紧急停机hysteresis 也就是回差非常容易被忽略。举个例子阈值设 70 度、回差 2 度那么温度升到 70 度触发动作后必须降到 68 度才能解除。没有这个回差温度在阈值附近轻微波动就会导致动作反复触发风扇就会“抽风”。需要注意的是trip 的 temperature 单位是毫摄氏度。设备树里写 65000 就是 65 度sysfs 里读 temp 出来 105000 表示 105 度。这个单位坑了很多人后面排查章节还会提到。2.3 thermal_cooling_device降温动作的抽象cooling device 代表一个“有档位的降温执行器”。它有且只有三个核心操作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 告诉框架这个冷却设备最多能分几档get_cur_state 返回当前档位set_cur_state 让驱动把硬件的执行状态调整到指定档位。state 值越大表示冷却动作越猛、对性能的影响越大。比如一个风扇max_state 为 10state0 是停转state10 是满转cpufreq cooling device 的 max_state 等于可用频率档位数减一state 越大对应的受限频率越低。实现方需要理解的是state 的物理含义完全由驱动自己定义框架只要求“能设置档位”这个抽象契约。2.4 thermal_governor决策大脑governor 是三层抽象里最有意思的部分它从内核视角看就是一个带 name 和回调的结构体static struct thermal_governor thermal_gov_step_wise { .name step_wise, .throttle step_wise_throttle, }; thermal_register_governor(thermal_gov_step_wise);用户可以通过/sys/class/thermal/thermal_zone0/policy在运行期切换 governor前提是内核配置了对应的CONFIG_THERMAL_GOV_*选项。内核常见的内置 governor 有四个fair_share按权重把冷却资源简单分配给各个 cdevstep_wise逐级增减档位最常用、行为最可预期user_space把决策权留给用户态守护进程power_allocator基于功耗预算和 PID 控制适合移动 SoC不同 governor 的核心差异在于“温度落在哪个 trip 区间时如何设置 cdev 的 target 状态”。这也是写自定义 governor 时最需要关心的点。2.5 关系绑定instance 与 cooling-map一个 zone 可以有多个 trip每个 trip 可以挂多个 cdev一个 cdev 也可以同时服务多个 trip。两者之间的多对多关系通过 thermal_instance 这个中间对象记录。instance 里包含 trip、cdev、target、upper/lower 限位、权重等信息。设备树里的 cooling-maps 就是描述这种绑定关系的配置它声明哪种 trip 触发时调用哪个 cooling device以及档位限制范围。在老内核里这个绑定需要驱动手动调用 thermal_zone_bind_cooling_device 完成新内核里设备树解析后框架会自动完成绑定这也是为什么新驱动比老驱动看起来“清爽”很多。3. 设备树与驱动的接入实操3.1 设备树侧配置thermal-zones 节点怎么写以一套典型的 CPU 热区配置为例thermal-zones { cpu-thermal { polling-delay-passive 250; polling-delay 1000; thermal-sensors tsens0 1; trips { cpu_alert: cpu-alert { temperature 70000; hysteresis 2000; type passive; }; cpu_crit: cpu-crit { temperature 105000; hysteresis 2000; type critical; }; }; cooling-maps { cpu_alert_map { trip cpu_alert; cooling-device cpu0 0 THERMAL_NO_LIMIT; }; }; }; };polling-delay 和 polling-delay-passive 的单位是毫秒。thermal-sensors 引用温度传感器节点后面的数字是 sensor id不同厂商写法可能不同。trips 子节点里每个 trip 都有一个名字名字本身没有特殊含义但同一个 thermal zone 内的 trip 节点名不能重复。cooling-device 后面的三元组第一个是 cdev 的 phandle第二个是允许的最低 state第三个是允许的最高 state。THERMAL_NO_LIMIT表示不限制。我见过有人在这里写cpu0 0 0结果 cdev 永远被锁在 0 档等于没绑排查了半天才发现是限幅写错了。注意trip 和 cooling-map 都是可选的。如果只想做温度监控、不做主动降温控制只配 trips 不配 cooling-maps 也能跑。3.2 驱动侧注册流程从 get_temp 到 zone 上线老内核3.x/4.x里zone 驱动需要手动注册tzd thermal_zone_device_register(cpu-thermal, 2, 0x3, dev, cpu_thermal_ops, NULL, 0, 1000);第二个参数是 trip 数量第三个参数 mask 表示启用哪些 trip0x3 表示启用 trip0 和 trip1。注册之后还得逐个绑定冷却设备thermal_zone_bind_cooling_device(tzd, trip_id, cdev, THERMAL_NO_LIMIT, THERMAL_NO_LIMIT, THERMAL_WEIGHT_DEFAULT);新内核5.x 起6.x 推荐用设备树自动解析的方式tzd devm_thermal_of_zone_register(dev, sensor_id, data, cpu_thermal_ops);设备树里的 trips 和 cooling-maps 都由框架解析驱动不需要 bind。这是新老 API 之间最大的区别。thermal_zone_device_ops 里 get_temp 的返回值语义值得注意返回 0 表示成功温度写入 *temp返回 -EAGAIN 表示本次采样失败框架忽略这次结果返回 -ENODEV 表示传感器不可用框架会停止更新这个 zone。实际调试时如果 temp 节点一直不变多半就是 get_temp 根本没被正确调用。3.3 cooling device 的注册CPU、GPU、风扇三种典型CPU 是最常见的冷却设备直接复用 cpufreq 驱动struct thermal_cooling_device *cdev; cdev cpufreq_cooling_register(policy); if (IS_ERR(cdev)) return PTR_ERR(cdev);cpufreq_cooling 内部会根据可用频率表建立 state 映射state 越大代表限频越狠。GPU 和 DDR 这类挂在 devfreq 框架下的设备用 devfreq_cooling_register 注册原理类似。风扇则通常是自己写驱动实现三个回调static int fan_get_max_state(struct thermal_cooling_device *cdev, unsigned long *state) { *state 10; return 0; } static int fan_set_cur_state(struct thermal_cooling_device *cdev, unsigned long state) { /* 把 state 映射成 PWM 占空比 */ pwm_set_duty(fan-pwm, state * 100 / 10); return 0; } static struct thermal_cooling_device_ops fan_cooling_ops { .get_max_state fan_get_max_state, .get_cur_state fan_get_cur_state, .set_cur_state fan_set_cur_state, };注册入口用thermal_of_cooling_device_register(np, fan, dev, fan_cooling_ops)其中 np 是设备树里自己设备的节点。这样风扇才能被 thermal zone 的 cooling-maps 引用。3.4 sysfs 接口与基础调试手段sysfs 是理解 thermal 运行状态最直接的入口。看一下当前系统有哪些热区ls /sys/class/thermal/ cat /sys/class/thermal/thermal_zone*/type cat /sys/class/thermal/thermal_zone*/temp cat /sys/class/thermal/thermal_zone0/policytemp 读出的是毫摄氏度65000 表示 65 度。policy 可以读到当前 governor也可以写入切换。trip 信息在 thermal_zone0 目录下的trip_point_0_temp、trip_point_0_type、trip_point_0_hyst等节点里。冷却设备在/sys/class/thermal/cooling_device0/下有 type、cur_state、max_state。开发板上我最常用的一行命令watch -n 1 for z in /sys/class/thermal/thermal_zone*; do echo $z $(cat $z/type) $(cat $z/temp); done实时看所有热区的温度变化比逐个 cat 高效得多。如果某个 zone 的温度一直不更新先看这个脚本里它的输出是不是恒定不变再往下查驱动。4. 运行机制与调温策略拆解4.1 轮询更新链路thermal_zone_device_update 一锤定音thermal framework 的驱动核心是一个周期性的轮询任务调用路径大概是thermal workqueue 到点执行 thermal_zone_device_check内部调用 thermal_zone_device_update(tz, THERMAL_EVENT_UNSPECIFIED)update 里先执行 ops-get_temp 拿到当前温度遍历 trips判断当前温度落在哪个区间、哪些 trip 被触发调用 governor-throttle算出每个 instance 的目标档位对每个冷却设备执行 set_cur_state根据是否处于被动触发状态重新安排下一次轮询 delay这个流程里最值得琢磨的就是轮询 delay 的动态切换。正常工作状态下1 秒采样一次就够了一旦有 passive 类型 trip 被触发系统会切成 polling-delay-passive 指定的高频周期比如 250ms以便更快响应温度变化。这样设计是为了在“及时响应”和“降低功耗”之间取一个平衡——毕竟一直高频采样本身也是一种功耗开销。4.2 step_wise最常用的逐级调温逻辑step_wise 是内核里用得最广泛的 governor它的核心思想就是四个字小步快跑。温度升高超过阈值cdev 的 state 就加一档温度回落到阈值减 hysteresis 以下state 就减一档。它还会结合趋势trend来避免振荡趋势可以通过 zone 的 get_trend 回调获取如果没有实现框架会用前后两次采样的温度差来推断。用伪代码理解它的逻辑for each instance (trip - cdev): if 当前温度 trip.temperature: 增加 cdev state不超过 upper else if 当前温度 trip.temperature - trip.hysteresis: 减少 cdev state不低于 lower else: 保持当前 state这种策略在 CPU 调频场景下非常典型温度到了 70 度先降一档还压不住再降一档温度降下来后再慢慢恢复。它不会从全速一下掉到最低频用户体验损失小行为也可预期。缺点是对温度变化又快又大的场景收敛速度不够快这时候就要考虑 power_allocator 这类更激进的策略。4.3 power_allocator(IPA)用功率预算换温度power_allocator 也叫 IPA思路和 step_wise 完全不同。它不直接看“温度超过阈值所以降一档”而是给整个 thermal zone 设定一个可持续功耗预算sustainable_power再用 PID 控制器根据当前温度和目标温度算出“现在允许消耗多少瓦”最后把功率预算按权重分配给各个冷却设备。打个比方step_wise 像是水龙头开了关、关了开看水温行事IPA 像是先算好这个月能用多少度电再安排每个房间的空调怎么分配。后者天然适合手机这种没有风扇、只能限制 CPU/GPU 功耗的移动设备。设备树里可以配置cpu-thermal { sustainable-power 2500; trips { ... }; cooling-maps { ... }; };IPA 调参是个经验活k_p、k_i、k_d 这几个 PID 参数直接决定温控曲线。我自己的做法是先保证 sustainable-power 贴近实测稳态功耗然后把 k_d 设为 0只调 k_p 让温度波动减小最后再逐步加 k_i 消除静差。注意IPA 要正常工作冷却设备必须实现 power actor 接口get_requested_power、state2power。如果发现 policy 切到 power_allocator 后完全没反应先检查 cdev 是否实现了功耗接口而不是怀疑 governor 坏了。4.4 critical 路径与重启兜底当温度突破 critical trip 时框架会走一条独立的重保护路径。内核会调用 thermal_zone_device_critical默认行为是触发 orderly_poweroff尝试优雅关机。如果温度实在降不下来或者关机流程卡住最后就得靠硬件 watchdog 和 PMIC 的过温保护来兜底。这里必须提醒做产品的同学critical 温度不要贪高。芯片 datasheet 标称 125 度是硅片极限不是让你把临界点也设到 125 度。考虑到温度采样误差、关机流程耗时和环境因素我在实际项目中一般会留 10 度以上的裕量比如 105 度触发 critical这样即使软件关机慢一点温度也不至于突破硬件极限。5. 实战中常见的坑与排查方法5.1 温度上报异常先查这三件事温度数据是整个热管理的地基地基不准上面策略再对也没用。我调试时遇到过的典型问题temp 节点一直不变要么 poll delay 太大要么 get_temp 没实现要么 get_temp 一直返回 -EAGAIN 被框架静默忽略。温度数值明显偏大或偏小寄存器换算错误常见的是单位搞错寄存器里是 0.1 度代码却当成 1 度来上报。温度在正常区间内剧烈跳动传感器噪声大或者读到了错误的寄存器。这种问题建议先在 sensor 驱动侧做滤波比如连续采几次取平均不要一上来就调大 hysteresis。排查的时候先看 dmesg 里有没有 thermal 相关的报错再逐级确认 sysfs 的 temp 节点和驱动日志是否能对上。5.2 绑了冷却设备却不生效这是社区里被问烂的问题“温度已经到阈值了为什么 cdev 不动”建议按下面的顺序排查看 policy 是不是你预期的 governorsysfs 里 cat 一下 thermal_zone0/policy看 trip 的 type 是否符合预期比如想触发降频却配成了 active某些场景下 active 动作不会被执行看 cooling device 是否注册成功cat 一下 max_state如果返回 0 说明驱动注册失败或 get_max_state 没实现看设备树 cooling-maps 的 trip 引用是否正确trip 名字拼错是非常常见的低级错误还有一个很容易忽略的点老内核 API 需要驱动主动 bind如果 bind 回调没执行cdev 自然不动。换新内核设备树方案后这个坑少了很多但老代码里依然存在。5.3 风扇“抽风”抖动与 hysteresis风扇在临界温度附近反复开关是我调过最头疼的问题之一。现象是温度刚好在阈值附近波动风扇一会儿满转一会儿停转噪声大不说还容易把风扇电机搞坏。根因基本都是 hysteresis 太小加上温度采样本身有噪声。解决方法有三个方向把 DTS 里的 hysteresis 从 5000.5 度加大到 2000 或者 50002~5 度在 sensor 驱动里做移动平均滤波适当调大 polling-delay-passive减少频繁判断的次数。改完 DTS 记得确认热区重新解析生效有些平台要重启或者卸载重绑驱动才生效。5.4 不同内核版本的 API 差异thermal framework 这几年接口变化不小网上大量文章和代码是老版本的直接抄容易踩坑。我把常见差异总结成一张表内核版本zone 注册方式绑定方式说明3.x / 4.xthermal_zone_device_register(type, trips, mask, ...)手动 bindmask 控制 trip 启用5.x 起thermal_of_zone_register / devm_thermal_of_zone_registerDTS 自动trips 由 DTS 解析6.x推荐 devm_thermal_of_zone_registerDTS 自动接口更清晰老接口仍在最省事的做法不是死记 API而是打开你手上内核的 drivers/thermal 目录找和你的平台最接近的驱动照着它的写法来。6. 最后再说点学习心得如果你也想把 thermal 这块彻底吃透我的建议是不要一上来就啃 thermal_core.c。先找一台能读 /sys/class/thermal 的机器把每个节点的 type、temp、policy、trip 信息都 cat 一遍建立直观感受然后重点读 step_wise 的源码和 thermal_zone_device_update 的调用流程最后自己写一个简单的风扇 cdev 驱动在开发板上把“温度升高 - governor 决策 - cdev 动作”这个闭环跑通。我调过最头痛的一次是系统负载不高但风扇一直在中高档位来回摆后来发现是采样周期和负载周期性叠加导致的共振加了 hysteresis 和滤波后才稳定下来。从那以后我在 DTS 里每定义一个 trip都会顺手确认回差是否合理。读代码的时候多问一句这个 trip 是谁在建、哪个 governor 在处理、cdev 到底有没有动。把这三条线串起来thermal 这关就算真正过了。
返回列表