Cooling Devices and Thermal Governors
A cooling device (
struct thermal_cooling_device) is the output of the Linux thermal control loop: a piece of hardware that can be told to remove heat or generate less of it, exposed through exactly three operations — what is the deepest level you can throttle to (get_max_state), where are you now (get_cur_state), and go to this level (set_cur_state). A thermal governor is the policy that decides, every polling cycle, which level each cooling device should be at, given the zone’s temperature and trip points. The genius of the design is the cooling-state abstraction: a fan, a CPU-frequency cap, and an idle-injection engine all look identical to the governor — an integer in[0, max_state]where 0 means “do nothing” andmax_statemeans “cool as hard as you can.” Linux ships five governors —step_wise,bang_bang,fair_share,user_space, andpower_allocator(the Intelligent Power Allocator, a PID controller) — and the build default isstep_wise(perdrivers/thermal/Kconfigandthermal_core.hat v6.12).
This note covers the outputs and policy half of the thermal framework. The inputs — sensors, zones and trip points — and the high-level passive-versus-active cooling philosophy are siblings; read them alongside this one. Everything here is verified against the Linux 6.12 LTS source tree (released 2024-11-17).
Mental Model
Think of the thermal subsystem as a thermostat with a twist. A house thermostat reads one temperature and turns one furnace on or off. The Linux thermal framework reads many sensors (grouped into thermal zones), compares each against a list of temperature thresholds (trip points), and drives many actuators (cooling devices) — but the actuators are not binary furnaces. Each one exposes a staircase of throttling levels, the cooling states. The governor is the control law that, on each cycle, maps “how hot is this zone, and which trips has it crossed?” onto “what cooling state should each bound device sit at?”
flowchart TB subgraph ZONE["Thermal zone (one per sensor group)"] TEMP["current temperature<br/>(from sensor)"] TRIPS["trip points<br/>(active / passive / hot / critical)"] end GOV["Governor (.manage / .trip_crossed)<br/>step_wise | bang_bang | fair_share<br/>user_space | power_allocator"] subgraph CDEVS["Cooling devices (the actuators)"] CPUF["cpufreq_cooling<br/>state = freq cap index"] CPUI["cpuidle_cooling<br/>state = idle-inject %"] DEVF["devfreq_cooling<br/>state = GPU/devfreq cap"] FAN["fan / GPIO fan<br/>state = fan speed step"] end TEMP --> GOV TRIPS --> GOV GOV -->|"instance->target per bound cdev"| AGG["__thermal_cdev_update():<br/>target = MAX over all instances"] AGG -->|"set_cur_state(cdev, target)"| CPUF AGG --> CPUI AGG --> DEVF AGG --> FAN
Figure: the thermal control loop. What it shows: the zone supplies temperature and trip points to the governor; the governor writes a desired target cooling state into each device-zone binding (a thermal_instance); the core’s __thermal_cdev_update() then aggregates all instances bound to a given cooling device by taking the maximum requested state and calls that device’s set_cur_state. The insight to take: the governor never touches hardware directly. It only writes integer targets into bindings; the core resolves conflicts (a device shared by two zones obeys whichever zone wants it cooler) and the cooling_device_ops translate the integer into a real action — a frequency cap, a fan speed, an idle ratio.
The Cooling-State Abstraction
The entire output side of the thermal framework rests on one small operations vector. In include/linux/thermal.h at v6.12 it is:
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);
int (*get_requested_power)(struct thermal_cooling_device *, u32 *);
int (*state2power)(struct thermal_cooling_device *, unsigned long, u32 *);
int (*power2state)(struct thermal_cooling_device *, u32, unsigned long *);
};The first three callbacks are mandatory and define the universal cooling-state contract:
get_max_state(cdev, &state)— returns the highest throttle level the device supports. A cooling state is an integer in the closed interval[0, max_state]. State 0 always means “no cooling action” (full performance / fan off);max_statemeans “throttle as hard as possible” (lowest frequency / fan at full speed). The framework treats the scale as monotone: a larger state always means more cooling and less performance.get_cur_state(cdev, &state)— returns where the device currently sits. The governor reads this to decide the next step.set_cur_state(cdev, state)— commands the device to a new level. This is where the abstraction becomes concrete hardware action, and where errors (a frequency request that fails) are reported.
The last three callbacks — get_requested_power, state2power, power2state — are the power-actor API, optional and used only by the power_allocator governor (described below). A device that implements them is a “power actor”: it can report how much power it is asking for, convert a cooling state to a power figure in milliwatts, and convert a granted power budget back to the nearest achievable state. Most cooling devices implement only the first three; a fan, for example, cannot meaningfully report power.
Crucially, the governor never calls set_cur_state itself. It writes a desired value into instance->target — the per-binding target inside a struct thermal_instance, the object that links one cooling device to one trip point of one zone. The core function __thermal_cdev_update() in thermal_helpers.c then walks every instance bound to that device and picks the largest target before calling set_cur_state:
void __thermal_cdev_update(struct thermal_cooling_device *cdev)
{
struct thermal_instance *instance;
unsigned long target = 0;
/* Make sure cdev enters the deepest cooling state */
list_for_each_entry(instance, &cdev->thermal_instances, cdev_node) {
if (instance->target == THERMAL_NO_TARGET)
continue;
if (instance->target > target)
target = instance->target;
}
thermal_cdev_set_cur_state(cdev, target);
}This max-wins rule is the answer to the obvious question: what happens when a single CPU’s cpufreq_cooling device is bound to two zones (say a per-cluster zone and an SoC-wide zone), and one wants state 3 while the other wants state 5? The hotter zone wins — the device is driven to state 5 — because under-cooling a hot zone risks a critical shutdown, whereas over-cooling merely costs performance. THERMAL_NO_TARGET (-1UL) is the sentinel for “this instance has no opinion right now” and is skipped.
The Common Cooling Devices
Several drivers implement the cooling-device contract, each translating the abstract state into a different physical action.
cpufreq_cooling (cpufreq_cooling.c) is the most important on CPUs. Its max_state equals max_level, the number of frequency steps in the policy’s frequency table minus one. State 0 maps to the highest frequency (no cap) and max_state to the lowest. The translation in cpufreq_set_cur_state() is striking — it does not force a frequency; it installs a maximum-frequency QoS constraint:
static int cpufreq_set_cur_state(struct thermal_cooling_device *cdev,
unsigned long state)
{
struct cpufreq_cooling_device *cpufreq_cdev = cdev->devdata;
unsigned int frequency;
...
frequency = get_state_freq(cpufreq_cdev, state);
ret = freq_qos_update_request(&cpufreq_cdev->qos_req, frequency);
if (ret >= 0)
cpufreq_cdev->cpufreq_state = state;
return ret;
}freq_qos_update_request() lowers the ceiling that cpufreq is allowed to choose from. The active governor (ondemand) still picks the frequency within that ceiling — thermal cooling only caps it. This is why thermal throttling and frequency scaling compose cleanly: thermal sets the upper bound, the performance governor picks below it.
cpuidle_cooling (cpuidle_cooling.c) takes a different tack: it does not lower frequency, it forces the CPU to be idle for a fraction of the time via the idle-injection engine. Its max_state is always 100 — a percentage. State N means “inject idle so the cluster runs only (100-N)% of the time.” The driver’s comment makes the contract explicit: “The state 100% will make the cluster 100% … idle. A 0% injection ratio means no idle injection at all.” The run-time computation is running = (idle × 100) / ratio − idle, so at state 50 a fixed 10 ms idle slice is paired with 10 ms of run time. This is the actuator of choice when a platform has no usable per-CPU frequency knob, or as a complement when frequency capping alone cannot shed enough heat.
devfreq_cooling (devfreq_cooling.c) is the GPU/memory-bus analogue of cpufreq_cooling: it caps the frequency of a devfreq-managed device (commonly the GPU) by the same staircase-of-states idea, and it implements the full power-actor API so a GPU can participate in IPA budgeting alongside the CPUs.
Fan cooling devices (e.g. thermal/gpio-fan, pwm-fan, ACPI fans) are the canonical active actuator. State is a fan-speed step: 0 = off, max_state = full speed. A simple on/off fan has max_state == 1. Fans are the natural pairing with the bang_bang governor.
The Governors
A governor is a struct thermal_governor registering a small set of callbacks — chiefly .manage (called once per polling cycle after temperature is sampled) and/or .trip_crossed (called when a trip threshold is crossed). They are registered at boot by thermal_register_governors() walking a linker table built from THERMAL_GOVERNOR_DECLARE() (thermal_core.c).
step_wise — the build default
step_wise (gov_step_wise.c) moves each cooling device one state at a time per cycle, in the direction of the zone’s temperature trend. Its core decision is get_target_state():
if (throttle) {
if (trend == THERMAL_TREND_RAISING)
return clamp(cur_state + 1, instance->lower, instance->upper);
} else if (trend == THERMAL_TREND_DROPPING) {
if (cur_state <= instance->lower)
return THERMAL_NO_TARGET;
return instance->lower;
}Read the logic carefully: if the zone is above a trip (throttle == true) and heating (THERMAL_TREND_RAISING), step the state up by one (clamped to the instance’s [lower, upper] bounds). If it is below the trip and dropping, jump straight back to lower (releasing throttle). If it is above the trip but the trend is dropping, do nothing — let the existing throttle bring it down. This hysteresis-by-trend keeps step_wise from oscillating: it only tightens while heating and only loosens while cooling. It is the kernel default precisely because it is robust, needs no per-platform tuning (no Energy Model, no sustainable_power), and works with any cooling device that implements the three mandatory ops. The Kconfig help text says it plainly: “If in doubt, select ‘step_wise’.”
bang_bang — on/off for fans
bang_bang (gov_bang_bang.c) is two-point hysteretic control: it drives the device fully on (state 1) when the trip is crossed on the way up, and fully off (state 0) when temperature falls below trip_temp − hysteresis. The ASCII diagram in the source captures it: the fan turns on at trip_temp and does not turn off until the temperature has dropped a full hysteresis band below the trip. This is exactly what a fan wants — a fan that toggled on and off every few hundred milliseconds at the trip boundary would be audibly annoying and mechanically stressful. The hysteresis band, taken from trip->hysteresis, is what step_wise lacks and what makes bang_bang the correct governor for active fan control. Note bang_bang uses .trip_crossed (event-driven) in addition to .manage, so it reacts at the moment of crossing.
fair_share — proportional load-splitting
fair_share (gov_fair_share.c) computes each device’s target as a product of three factors, per its own comment: new_state = P3 × P2 × P1, where P1 = max_state (the device’s range), P2 = weight[i]/total_weight (the device’s relative cooling effectiveness, from platform data), and P3 = trip_level/num_trips (how deep into the trip stack the zone currently is). Concretely the code computes instance->target = (trip_level × cdev->max_state × weight) / (num_trips × total_weight). The intuition: the more trips the zone has crossed, the harder every device is pushed; and within a cycle, more-effective devices (higher weight) take proportionally more of the load. It needs platform-supplied weights to be useful and is rarely the desktop default, but it is handy when several heterogeneous coolers should share the burden.
user_space — hand it to userspace
user_space (gov_user_space.c) does no throttling at all. On each trip crossing it emits a KOBJ_CHANGE uevent carrying the zone name, temperature, trip index, and event type, and lets a userspace thermal daemon (such as thermald) decide what to do. The driver itself prints "Consider using thermal netlink events interface" — the modern replacement is the richer thermal netlink channel. This governor exists for policy that is too complex or too product-specific to live in the kernel.
power_allocator — the Intelligent Power Allocator (IPA)
power_allocator (gov_power_allocator.c), originally contributed by ARM in 2014, is the most sophisticated governor: a PID (Proportional-Integral-Derivative) controller that maintains a power budget and divvies it among power-actor cooling devices to hold the zone at a target temperature while maximizing performance. It is the governor of choice for mobile SoCs where CPU and GPU compete for a shared thermal envelope. It requires the Energy Model (its Kconfig depends on ENERGY_MODEL) and works only on cooling devices implementing the power-actor API.
IPA needs two passive trip points: the first passive trip is the switch-on temperature (the loop is dormant below it), and the last passive trip is the control temperature it regulates toward (get_governor_trips()). The PID core, pid_controller(), computes a power budget from the temperature error err = control_temp − tz->temperature:
err = control_temp - tz->temperature;
err = int_to_frac(err);
/* proportional term: k_po when overshooting, k_pu when undershooting */
p = mul_frac(err < 0 ? tz->tzp->k_po : tz->tzp->k_pu, err);
i = mul_frac(tz->tzp->k_i, params->err_integral);
... /* integrate only while err < integral_cutoff */
d = mul_frac(tz->tzp->k_d, err - params->prev_err);
power_range = p + i + d;
power_range = sustainable_power + frac_to_int(power_range); /* feed-forward */
power_range = clamp(power_range, 0, max_allocatable_power);The output is a total power budget, centered on sustainable_power (the power the zone can dissipate indefinitely at the control temperature — a feed-forward term so the controller starts from a sensible baseline rather than from zero). The proportional term uses two different gains: k_po when overshooting (err < 0, temperature above target — back off gently) and k_pu when undershooting (err >= 0, room to spare — push up aggressively, by default at twice the gain). This asymmetry — fast to grant power, slow to retract it — is what makes IPA feel responsive without thermal overshoot; the k_po/k_pu constants and the passive control loop are dissected in detail in Passive and Active Cooling. The integral term corrects long-term drift but only accumulates while the error is below integral_cutoff (no point banking positive error on an idle, cool system). Once it has a budget, divvy_up_power() splits it among actors in proportion to each one’s weighted requested power, then re-distributes any surplus (power granted beyond a device’s max) to the still-hungry actors. Each actor’s share is fed through power2state() to land on a concrete cooling state.
Configuration and Inspection
Governors and cooling states are visible and (partly) controllable through sysfs and Kconfig.
The build default is selected by a Kconfig choice in drivers/thermal/Kconfig:
choice
prompt "Default Thermal governor"
default THERMAL_DEFAULT_GOV_STEP_WISE
help
This option sets which thermal governor shall be loaded at
startup. If in doubt, select 'step_wise'.and thermal_core.h turns that into the DEFAULT_THERMAL_GOVERNOR string the core uses when a zone does not name its own governor:
#if defined(CONFIG_THERMAL_DEFAULT_GOV_STEP_WISE)
#define DEFAULT_THERMAL_GOVERNOR "step_wise"
#elif defined(CONFIG_THERMAL_DEFAULT_GOV_FAIR_SHARE)
#define DEFAULT_THERMAL_GOVERNOR "fair_share"
...At runtime, each zone exposes and accepts its governor through sysfs:
$ cat /sys/class/thermal/thermal_zone0/available_policies
step_wise fair_share user_space power_allocator
$ cat /sys/class/thermal/thermal_zone0/policy
step_wise
# switch this zone to the power allocator
$ echo power_allocator > /sys/class/thermal/thermal_zone0/policyEach cooling device appears as cooling_device[N] with the state contract directly visible:
$ cat /sys/class/thermal/cooling_device0/type # what kind of cooler
cpufreq-cpu0
$ cat /sys/class/thermal/cooling_device0/max_state # = get_max_state
7
$ cat /sys/class/thermal/cooling_device0/cur_state # = get_cur_state
0A cur_state of 0 confirms no thermal throttling is active; a non-zero value means the device is being capped. Watching cur_state climb while a zone heats is the single most direct way to confirm a governor is doing its job.
Failure Modes and Common Misunderstandings
“My CPU is stuck at low frequency.” A cpufreq-cpuN cooling device left at a non-zero cur_state holds a freq_qos max-frequency request that outlives the heat event if the governor never steps it back down — for instance if temperature sampling stalled, or if a buggy zone never reports a dropping trend so step_wise never releases. Check cooling_device*/cur_state; a persistent non-zero value with a cool sensor is the smoking gun.
Expecting power_allocator to work without an Energy Model. IPA is depends on ENERGY_MODEL and its power_allocator_bind() calls check_power_actors(), which refuses to bind and logs "%s is not a power actor" if any cooling device on the control trip lacks the power-actor API. Selecting it on a platform whose cooling devices are plain fans (no state2power) silently leaves the zone on no effective policy.
Confusing cooling state with a frequency or an RPM. The state is a dimensionless index. State 3 on one CPU’s cpufreq_cooling and state 3 on another’s may correspond to completely different frequencies because the mapping is per-device. Never compare raw states across devices.
bang_bang chatter from a missing hysteresis. If a fan’s trip point has hysteresis == 0, bang_bang degenerates to toggling on/off at the exact trip temperature — the very oscillation the governor exists to prevent. The fix is in the trip definition (give it a hysteresis band), not the governor.
Alternatives and When to Choose Them
step_wise is the safe default: no tuning, works with any cooler, gently throttles. Choose it unless you have a specific reason not to. bang_bang is for fans and other genuinely binary actuators where hysteresis matters. fair_share suits multiple heterogeneous coolers that should share load by configured weight, but needs that platform data. user_space is for product-specific policy better expressed in a daemon. power_allocator is for mobile SoCs with a shared CPU+GPU thermal budget and a calibrated Energy Model, where holding a target temperature while maximizing total performance is worth the configuration cost (sustainable_power, PID constants). The trade-off ladder runs from “zero configuration, decent behavior” (step_wise) to “heavy per-platform calibration, optimal behavior” (power_allocator).
Production Notes
On Android phones, power_allocator (IPA) is the workhorse: vendors calibrate sustainable_power (the ARM docs cite roughly 2000 mW for a 4-inch phone, 4500 mW for a 10-inch tablet) and let IPA arbitrate the CPU-vs-GPU power split under a sustained gaming load — exactly the case step_wise handles poorly because it throttles each device independently with no shared budget. On typical x86 laptops and desktops the firmware and intel_pstate/RAPL often handle the fast thermal loop in hardware, leaving the kernel thermal zones on step_wise for the slower, coarser SoC-temperature response, frequently supplemented by a userspace thermald. A recurring real-world gotcha: IPA’s PID loop “works best if there is a periodic tick” (per the power_allocator docs) — on a deeply tickless idle system the governor can be called irregularly, degrading control quality, which is one reason the passive polling delay (see Passive and Active Cooling) is kept short while a passive trip is active.
See Also
- The Thermal Framework — the parent: how zones, sensors, trips, governors, and cooling devices assemble into one subsystem
- Thermal Zones Trip Points and Sensors — the input side: where temperature and trip points come from
- Passive and Active Cooling — the two cooling philosophies, the IPA
k_po/k_puconstants, and the passive polling delay, in depth - The Energy Model EM Framework — the per-platform power model that
power_allocator(and EAS) consume - The cpufreq Subsystem —
cpufreq_coolingcaps the frequency this subsystem selects - Processor C-States —
cpuidle_coolinginjects idle, forcing CPUs into these states - Linux Power Management MOC — the map this note sits under (§7 Thermal Management)