Linux Thermal Sleep States Hands-On
Companion hands-on lecture to Linux Thermal And Sleep States, part of Ravi’s free Linux device drivers course: real sysfs commands plus an original ep_thermal_zone demo driver.
The previous lecture built the mental model for the Linux thermal framework and the four system sleep states. This one is entirely hands-on: you will walk the real thermal sysfs tree, query and change /sys/power/state and /sys/power/mem_sleep, and then write and load an original, minimal driver, ep_thermal_zone, that registers itself with the current thermal core using the thermal_zone_device_register_with_trips() API and shows up under /sys/class/thermal/ just like any real sensor driver would.
What You Will Learn
By the end of this lecture you will be able to:
Prerequisites
- Read the companion explanation lecture, Linux Thermal And Sleep States, before starting here; this lecture assumes you already know what a trip point, cooling device, and sleep state are.
- A Linux system (or VM) where you have root access and can build and load kernel modules.
- Kernel headers matching your running kernel, and
CONFIG_THERMALenabled. - Basic C and a working
gcc/ kernel build toolchain, as covered earlier in this free Linux device drivers course.
Exploring The Thermal sysfs Tree
Start by listing what the running kernel has already registered. Output naturally varies by platform; this example is from a typical x86 laptop with an ACPI thermal zone, a package temperature sensor, and two cooling devices:
$ ls /sys/class/thermal/
cooling_device0 cooling_device1 thermal_zone0 thermal_zone1
Reading Zone Type, Temperature, And Policy
$ cat /sys/class/thermal/thermal_zone0/type
x86_pkg_temp
$ cat /sys/class/thermal/thermal_zone0/temp
52000
$ cat /sys/class/thermal/thermal_zone1/type
acpitz
$ cat /sys/class/thermal/thermal_zone1/temp
47000
$ cat /sys/class/thermal/thermal_zone1/mode
enabled
$ cat /sys/class/thermal/thermal_zone1/policy
step_wise
$ cat /sys/class/thermal/thermal_zone1/available_policies
step_wise fair_share user_space power_allocator
The temp value is always in millidegrees Celsius, so 47000 means 47.0°C. policy shows which governor is currently driving this zone, and available_policies lists every governor compiled into the kernel that this zone could switch to by writing its name into policy.
Reading Trip Points
$ cat /sys/class/thermal/thermal_zone1/trip_point_0_type
passive
$ cat /sys/class/thermal/thermal_zone1/trip_point_0_temp
85000
$ cat /sys/class/thermal/thermal_zone1/trip_point_0_hyst
2000
$ cat /sys/class/thermal/thermal_zone1/trip_point_1_type
critical
$ cat /sys/class/thermal/thermal_zone1/trip_point_1_temp
105000
Here, the zone throttles passively at 85°C with 2°C of hysteresis before the response clears, and forces an emergency shutdown at 105°C. A quick way to dump every trip point at once is:
$ grep -H . /sys/class/thermal/thermal_zone1/trip_point_*
/sys/class/thermal/thermal_zone1/trip_point_0_temp:85000
/sys/class/thermal/thermal_zone1/trip_point_0_type:passive
/sys/class/thermal/thermal_zone1/trip_point_0_hyst:2000
/sys/class/thermal/thermal_zone1/trip_point_1_temp:105000
/sys/class/thermal/thermal_zone1/trip_point_1_type:critical
Cooling Device Attributes And Statistics
$ cat /sys/class/thermal/cooling_device0/type
Processor
$ cat /sys/class/thermal/cooling_device0/max_state
8
$ cat /sys/class/thermal/cooling_device0/cur_state
0
$ cat /sys/class/thermal/cooling_device0/stats/total_trans
14
$ cat /sys/class/thermal/cooling_device0/stats/time_in_state_ms
state0 1832441
state1 920
state2 0
...
max_state and cur_state are cooling-device-defined integers, not temperatures; a processor cooling device’s states typically map to progressively lower DVFS operating points. The stats/ directory tracks how many transitions occurred and how long the device has spent in each throttle state, which is invaluable when investigating unexpected performance drops.
Querying And Changing System Sleep State
/sys/power/state
$ cat /sys/power/state
freeze mem disk standby
Any of these strings can be written by root to trigger the corresponding transition. Suspend-to-idle is the safest one to experiment with first since it needs no platform-specific wakeup wiring:
# echo freeze > /sys/power/state
The shell blocks until a wakeup source (keyboard, RTC alarm, network interrupt if enabled as a wakeup device, and so on) brings the system back, at which point the command returns and the prompt reappears normally.
/sys/power/mem_sleep
$ cat /sys/power/mem_sleep
[s2idle] deep
To force a true suspend-to-RAM transition instead of the s2idle default shown above, select deep first, then write mem:
# echo deep > /sys/power/mem_sleep
$ cat /sys/power/mem_sleep
s2idle [deep]
# echo mem > /sys/power/state
Using An RTC Wakeup Alarm To Test Suspend-To-RAM Safely
Testing suspend-to-RAM over a remote shell is risky, since the network interface itself gets suspended. A local RTC alarm gives a predictable, hands-free wakeup:
$ ls /sys/class/rtc/
rtc0
$ cat /sys/class/rtc/rtc0/wakealarm
# echo +30 > /sys/class/rtc/rtc0/wakealarm
# echo mem > /sys/power/state
[ ... console is silent for roughly 30 seconds ... ]
$
An empty read from wakealarm means no alarm is currently armed. Writing +30 arms it thirty seconds in the future; the system should resume on its own around that mark.
Building An Original Demo: ep_thermal_zone
To see the current registration API in action, here is a small, original demo driver, prefixed ep_, that registers a simulated thermal zone with two trip points using thermal_zone_device_register_with_trips(), the API current mainline kernels expect (older tutorials that show a plain thermal_zone_device_register() call with per-trip governor callbacks are describing a superseded interface).
/* ep_thermal.c - minimal original demo thermal zone driver */
#include <linux/module.h>
#include <linux/thermal.h>
#include <linux/err.h>
#define EP_TZ_NAME "ep_thermal_zone"
#define EP_POLL_MS 1000
#define EP_PASSIVE_MS 2000
/* Simulated sensor reading, in millidegrees Celsius */
static int ep_sim_temp_mC = 45000;
static int ep_get_temp(struct thermal_zone_device *tzd, int *temp)
{
*temp = ep_sim_temp_mC;
return 0;
}
static struct thermal_zone_device_ops ep_tz_ops = {
.get_temp = ep_get_temp,
};
static const struct thermal_trip ep_trips[] = {
{
.type = THERMAL_TRIP_PASSIVE,
.temperature = 70000, /* 70.0 C */
.hysteresis = 2000, /* 2.0 C */
},
{
.type = THERMAL_TRIP_CRITICAL,
.temperature = 95000, /* 95.0 C */
.hysteresis = 0,
},
};
static struct thermal_zone_device *ep_tzd;
static int __init ep_thermal_init(void)
{
ep_tzd = thermal_zone_device_register_with_trips(EP_TZ_NAME,
ep_trips,
ARRAY_SIZE(ep_trips),
NULL, /* devdata */
&ep_tz_ops,
NULL, /* tzp */
EP_PASSIVE_MS,
EP_POLL_MS);
if (IS_ERR(ep_tzd)) {
pr_err("ep_thermal: failed to register thermal zone (%ld)\n",
PTR_ERR(ep_tzd));
return PTR_ERR(ep_tzd);
}
pr_info("ep_thermal: registered '%s' with %zu trip points\n",
EP_TZ_NAME, ARRAY_SIZE(ep_trips));
return 0;
}
static void __exit ep_thermal_exit(void)
{
thermal_zone_device_unregister(ep_tzd);
pr_info("ep_thermal: unregistered '%s'\n", EP_TZ_NAME);
}
module_init(ep_thermal_init);
module_exit(ep_thermal_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("ep_thermal: minimal demo thermal zone driver");
A matching cooling device, also original and prefixed ep_, shows the other half of the API, thermal_cooling_device_register():
/* ep_cooling.c - minimal original demo cooling device driver */
#include <linux/module.h>
#include <linux/thermal.h>
#include <linux/err.h>
#define EP_COOL_NAME "ep_cooling_device"
#define EP_COOL_MAX_STATE 4
static unsigned long ep_cool_state;
static int ep_get_max_state(struct thermal_cooling_device *cdev,
unsigned long *state)
{
*state = EP_COOL_MAX_STATE;
return 0;
}
static int ep_get_cur_state(struct thermal_cooling_device *cdev,
unsigned long *state)
{
*state = ep_cool_state;
return 0;
}
static int ep_set_cur_state(struct thermal_cooling_device *cdev,
unsigned long state)
{
if (state > EP_COOL_MAX_STATE)
return -EINVAL;
ep_cool_state = state;
pr_info("ep_cooling: throttle level now %lu/%d\n",
state, EP_COOL_MAX_STATE);
return 0;
}
static struct thermal_cooling_device_ops ep_cooling_ops = {
.get_max_state = ep_get_max_state,
.get_cur_state = ep_get_cur_state,
.set_cur_state = ep_set_cur_state,
};
static struct thermal_cooling_device *ep_cdev;
static int __init ep_cooling_init(void)
{
ep_cdev = thermal_cooling_device_register(EP_COOL_NAME, NULL,
&ep_cooling_ops);
if (IS_ERR(ep_cdev)) {
pr_err("ep_cooling: failed to register cooling device (%ld)\n",
PTR_ERR(ep_cdev));
return PTR_ERR(ep_cdev);
}
pr_info("ep_cooling: registered '%s', max_state=%d\n",
EP_COOL_NAME, EP_COOL_MAX_STATE);
return 0;
}
static void __exit ep_cooling_exit(void)
{
thermal_cooling_device_unregister(ep_cdev);
pr_info("ep_cooling: unregistered '%s'\n", EP_COOL_NAME);
}
module_init(ep_cooling_init);
module_exit(ep_cooling_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("ep_cooling: minimal demo cooling device driver");
A minimal Makefile for both modules, built out-of-tree against your running kernel’s headers:
obj-m += ep_thermal.o
obj-m += ep_cooling.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Building And Loading The Demo
$ make
CC [M] ep_thermal.o
MODPOST Module.symvers
CC [M] ep_thermal.mod.o
LD [M] ep_thermal.ko
CC [M] ep_cooling.o
CC [M] ep_cooling.mod.o
LD [M] ep_cooling.ko
# insmod ep_thermal.ko
# insmod ep_cooling.ko
$ dmesg | tail -n 4
[ 1523.914021] ep_thermal: registered 'ep_thermal_zone' with 2 trip points
[ 1523.918877] ep_cooling: registered 'ep_cooling_device', max_state=4
Verifying The New Zone And Cooling Device From sysfs
$ ls /sys/class/thermal/ | grep -E "thermal_zone|cooling_device"
cooling_device0
cooling_device1
cooling_device2
thermal_zone0
thermal_zone1
thermal_zone2
$ cat /sys/class/thermal/thermal_zone2/type
ep_thermal_zone
$ cat /sys/class/thermal/thermal_zone2/temp
45000
$ cat /sys/class/thermal/thermal_zone2/trip_point_0_type
passive
$ cat /sys/class/thermal/thermal_zone2/trip_point_0_temp
70000
$ cat /sys/class/thermal/thermal_zone2/trip_point_1_type
critical
$ cat /sys/class/thermal/thermal_zone2/trip_point_1_temp
95000
$ cat /sys/class/thermal/cooling_device2/type
ep_cooling_device
$ cat /sys/class/thermal/cooling_device2/max_state
4
$ cat /sys/class/thermal/cooling_device2/cur_state
0
Note that ep_thermal_zone and ep_cooling_device come up unbound to each other, since neither was described in a device tree cooling-maps node and the demo does not call an explicit binding function. In a real platform driver you would either describe the trip-to-cooling-device relationship in the device tree, or bind them programmatically; this skeleton intentionally stays minimal so the registration APIs themselves stay easy to read.
Unloading The Demo
# rmmod ep_cooling
# rmmod ep_thermal
$ dmesg | tail -n 2
[ 1601.220154] ep_cooling: unregistered 'ep_cooling_device'
[ 1601.224309] ep_thermal: unregistered 'ep_thermal_zone'
Common Mistakes And Troubleshooting
- Permission denied writing to /sys/power/state or /sys/power/mem_sleep. These writes require root; use
sudoor a root shell. - “Invalid argument” writing to /sys/power/state. The string you wrote is not currently listed by
cat /sys/power/stateon this platform; standby and hibernation, in particular, are not guaranteed to be present. - SSH session appears to hang after
echo mem > /sys/power/state. This is expected: the network interface itself gets suspended. Test suspend-to-RAM and hibernation from a local console, or arm an RTC wakeup alarm first as shown above. - insmod fails with “Unknown symbol thermal_zone_device_register_with_trips”. Usually means
CONFIG_THERMALis not built in or the thermal core module has not been loaded yet on a kernel where it is modular. - Trip points never seem to fire in a real driver. Almost always a units bug:
get_temp()must return millidegrees Celsius, not raw ADC counts or degrees. - Kernel warning or oops on the second insmod/rmmod cycle. Usually means the thermal zone or cooling device was not unregistered in the module’s exit function, leaking the kobject.
Best Practices
- Always check
dmesgimmediately after everyinsmodandrmmodwhile developing a thermal driver. - Keep a second terminal running
dmesg -wwhile testing suspend and resume so you can see exactly where a hang occurs. - Script repetitive sysfs reads with
grep -H . trip_point_*instead of catting each attribute by hand. - Cycle
insmod/rmmodseveral times during development to catch reference-counting and double-registration bugs early. - Arm an RTC wakeup alarm before testing deeper sleep states so you are never locked out of a headless or remote machine.
Summary And Conclusion
You have now driven the Linux thermal framework from the command line: listing zones and cooling devices, reading trip points and governor policy, and pulling cooling device statistics, all through plain sysfs. You have also exercised the system sleep sysfs interface, /sys/power/state and /sys/power/mem_sleep, including a safe RTC-based way to test suspend-to-RAM. Finally, the original ep_thermal_zone and ep_cooling_device demo drivers showed the current thermal_zone_device_register_with_trips() and thermal_cooling_device_register() APIs end to end, from module load through sysfs verification to clean unload. With both the thermal framework and sleep states now concrete, the next lecture in this free Linux device drivers course goes deep into hibernation’s snapshot-and-restore internals.
Frequently Asked Questions
How do I find which thermal_zoneN corresponds to my newly loaded driver?
Grep the type attribute across every zone: grep -H . /sys/class/thermal/thermal_zone*/type. The zone whose type matches the name you passed into the registration call is yours.
Why does insmod fail with an unknown symbol error for thermal_zone_device_register_with_trips?
This almost always means the thermal core is not present in the running kernel, either because CONFIG_THERMAL is disabled or, on kernels where the core itself is modular, because that module has not been loaded yet.
Is it safe to test suspend-to-RAM over an SSH session?
Not reliably. The network interface is suspended along with everything else, so your session will drop and may not reconnect automatically on resume. Prefer a local console, or arm an RTC wakeup alarm first so the machine returns on its own.
What temperature unit does get_temp() need to return?
Millidegrees Celsius, as a plain integer. 45.0°C must be returned as 45000, not 45 and not a raw sensor register value.
How do I confirm my cooling device is actually bound to a trip point?
Check for a cdevN symlink and matching cdevN_trip_point attribute under the thermal zone directory. If they are absent, as they are in this lecture’s minimal demo, the cooling device has not been bound, either through a device tree cooling-map or an explicit binding call.
Why did my write to /sys/power/state fail with “invalid argument”?
The state string you wrote is not currently supported on this platform. Always cat /sys/power/state first and only write one of the strings actually listed there.
Keep Building Your Linux Kernel Power Management Skills
You just took the Linux thermal framework and sleep states from theory to a running, original driver. Next in this free Linux device drivers course: hibernation internals, snapshot images, and the restore-kernel handoff.
Review The Concepts Lecture Next: Hibernation Deep Dive