Linux Hibernation Driver PM Guide
Hands-on companion to Lecture 4 of the Linux Kernel Power Management series, part of Ravi’s free Linux kernel development course: arming an RTC wakealarm, running a real Linux hibernation cycle through /sys/power/disk, and wiring struct dev_pm_ops into a working platform driver skeleton.
This is the hands-on half of the previous lecture on Linux hibernation and device power management. Where the theory lecture explained why a hibernation image has to live on swap and walked through the full struct dev_pm_ops callback contract, this lecture puts real commands in front of you: arming an RTC wakealarm before a suspend-to-RAM test, driving a complete Linux hibernation cycle through /sys/power/disk in reboot mode with realistic kernel log output at every stage, and finally building a small original platform driver, ep_hibernate_demo, that declares its own dev_pm_ops using SET_SYSTEM_SLEEP_PM_OPS and shows exactly where that structure plugs into a driver’s .driver.pm field. Everything here is meant to be typed on a real machine or virtual machine you are comfortable rebooting, as part of Ravi’s free Linux kernel development course.
Key Terms In This Lecture
What You Will Learn
By the end of this lecture you will be able to:
Prerequisites
- Root access on a Linux machine or VM with
CONFIG_HIBERNATIONenabled and a swap area at least as large as your in-use memory. - A working out-of-tree kernel module build environment (kernel headers matching your running kernel,
make, a C compiler). - Comfort with
dmesg,insmod/modprobe, and reading sysfs attributes as root. - Having read the companion theory lecture, Linux Hibernation And Device PM, is required, not optional; this lecture assumes you already know why a hibernation image lives on swap, what each
/sys/power/diskoption does conceptually, and the full shape ofstruct dev_pm_opscovered there.
Step 1: Confirm Your RTC And Arm A Wakealarm
Every hands-on sleep-state test in this series starts the same way: find a wakeup source you control before you ever write to /sys/power/state. Start by confirming which RTC device your board actually exposes, then check whether a wakealarm is already armed:
$ ls /sys/class/rtc/
rtc0
$ cat /sys/class/rtc/rtc0/wakealarm
$ cat /sys/class/rtc/rtc0/since_epoch
1785980400
An empty wakealarm read confirms nothing is currently scheduled. since_epoch gives you the RTC’s own notion of the current time in seconds, which is useful if you ever want to arm an absolute alarm instead of a relative one. For a first test, a relative alarm is simpler and safer:
# echo +30 > /sys/class/rtc/rtc0/wakealarm
# cat /sys/class/rtc/rtc0/wakealarm
1785980430
The kernel echoes back the absolute epoch time it computed for the alarm, which is worth reading back every time as a sanity check; a stale or misparsed write here is a common reason a suspend test never wakes up on its own. With the alarm confirmed, trigger suspend-to-RAM and watch the machine come back thirty seconds later without any input from you:
# echo mem > /sys/power/state
[ 842.112034] PM: suspend entry (deep)
[ 842.118221] Filesystems sync: 0.004 seconds
[ 842.140552] Freezing user space processes
[ 842.141890] Freezing user space processes completed (elapsed 0.001 seconds)
[ 842.142100] OOM killer disabled.
[ 842.142310] Freezing remaining freezable tasks
[ 842.143001] Freezing remaining freezable tasks completed (elapsed 0.001 seconds)
[ 842.150233] ep_hibernate_demo ep_hibernate_demo.0: ep_hibernate_demo: suspend callback fired, quiescing device
[ 842.201455] ACPI: EC: interrupt blocked
[ 842.250010] PM: suspend devices took 0.098 seconds
[ 842.251200] ACPI: Preparing to enter system sleep state S3
[ 842.300000] PM: suspend exit
...
[ 872.301102] ACPI: Waking up from system sleep state S3
[ 872.410221] ep_hibernate_demo ep_hibernate_demo.0: ep_hibernate_demo: resume callback fired, wakeup_count=1
[ 872.512004] Restarting tasks ... done.
[ 872.520117] PM: suspend exit
That gap between the two timestamps, roughly thirty seconds, is your RTC alarm doing its job. The ep_hibernate_demo log lines are produced by the demo driver built later in this lecture; if you have not built it yet, you will simply see the same suspend/resume sequence without those two lines.
Step 2: Choose And Confirm A Hibernation Mode
Before triggering a real Linux hibernation cycle, read /sys/power/disk to see which post-image-save behavior your kernel currently has selected:
$ cat /sys/power/disk
[platform] shutdown reboot suspend test_resume
For a first hands-on test, reboot mode is the most instructive choice, because it lets you watch the entire resume path happen immediately, in the same terminal session, without physically power-cycling anything. Select it by writing the string:
# echo reboot > /sys/power/disk
$ cat /sys/power/disk
platform shutdown [reboot] suspend test_resume
The brackets move to confirm the selection took effect. This is the one command in this whole lecture worth double-checking before you proceed, since the alternative modes behave very differently: shutdown powers the board off and leaves you needing a physical or remote power switch to bring it back, and test_resume never actually touches power at all.
Step 3: Run The Hibernation Cycle And Read The Log
With reboot mode selected, trigger hibernation the same way you triggered suspend-to-RAM, only this time through the disk string:
# sync
# echo disk > /sys/power/state
The kernel log during image creation is longer than a suspend-to-RAM cycle, because it now includes the memory snapshot and the write-out to swap:
[ 1204.001100] PM: hibernation: hibernation entry
[ 1204.005221] Filesystems sync: 0.010 seconds
[ 1204.030552] Freezing user space processes
[ 1204.031890] Freezing user space processes completed (elapsed 0.001 seconds)
[ 1204.032100] OOM killer disabled.
[ 1204.033210] Freezing remaining freezable tasks completed (elapsed 0.002 seconds)
[ 1204.034301] PM: hibernation: Basic memory bitmaps created
[ 1204.201455] PM: hibernation: Preallocating image memory
[ 1204.980112] PM: hibernation: Allocated 184320 kbytes in 0.78 seconds (236.31 MB/s)
[ 1205.001233] Freezing remaining freezable tasks completed (elapsed 0.001 seconds)
[ 1205.002344] ep_hibernate_demo ep_hibernate_demo.0: ep_hibernate_demo: suspend callback fired, quiescing device
[ 1205.100221] PM: hibernation: Creating image
[ 1206.512004] PM: hibernation: Image created (184320 pages copied)
[ 1206.601330] PM: hibernation: Writing image
[ 1209.812004] PM: hibernation: Image saving done
[ 1209.900112] PM: hibernation: Wrote 184320 kbytes in 3.20 seconds (57.60 MB/s)
[ 1209.950004] reboot: Restarting system
At that last line the board actually reboots. What you see next is the restore kernel booting like any cold boot, finding the saved image in swap, and handing off to the image kernel:
[ 0.000000] Linux version 6.x (build@host) ...
[ 2.104332] PM: hibernation: Marking nosave pages
[ 4.512211] PM: hibernation: resuming from hibernation
[ 4.601102] PM: hibernation: Loading hibernation image
[ 7.802214] PM: hibernation: Image loading done
[ 7.900331] PM: hibernation: Basic memory bitmaps freed
[ 8.001402] ep_hibernate_demo ep_hibernate_demo.0: ep_hibernate_demo: resume callback fired, wakeup_count=1
[ 8.102214] Restarting tasks ... done.
[ 8.110002] PM: hibernation: hibernation exit
The machine you land in is the exact same session you hibernated, right down to open terminal scrollback, because that is precisely what a Linux hibernation image guarantees to preserve.
RTC Wakealarm And Hibernation Test Flow On One Board
Building The ep_hibernate_demo Platform Driver
The whole point of this driver is to make one thing concrete: where exactly a dev_pm_ops table plugs into a real driver structure, and what actually shows up in dmesg when it does. It is intentionally minimal, a platform driver with no real hardware behind it, registering its own platform device so it can be tested with nothing more than insmod.
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/pm.h>
#include <linux/slab.h>
struct ep_hibernate_demo_priv {
int wakeup_count;
};
static int ep_hibernate_demo_suspend(struct device *dev)
{
struct ep_hibernate_demo_priv *priv = dev_get_drvdata(dev);
dev_info(dev, "ep_hibernate_demo: suspend callback fired, quiescing device\n");
priv->wakeup_count = priv->wakeup_count; /* nothing to save for this demo */
return 0;
}
static int ep_hibernate_demo_resume(struct device *dev)
{
struct ep_hibernate_demo_priv *priv = dev_get_drvdata(dev);
priv->wakeup_count++;
dev_info(dev, "ep_hibernate_demo: resume callback fired, wakeup_count=%d\n",
priv->wakeup_count);
return 0;
}
/* suspend/freeze/poweroff all point at the same function, and
* resume/thaw/restore all point at the same function, which is
* correct here because this demo device has no real hardware
* state that behaves differently between suspend-to-RAM and
* hibernation. */
static const struct dev_pm_ops ep_hibernate_demo_pm_ops = {
SET_SYSTEM_SLEEP_PM_OPS(ep_hibernate_demo_suspend, ep_hibernate_demo_resume)
};
static int ep_hibernate_demo_probe(struct platform_device *pdev)
{
struct ep_hibernate_demo_priv *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
platform_set_drvdata(pdev, priv);
dev_info(&pdev->dev, "ep_hibernate_demo: probed, ready for PM testing\n");
return 0;
}
static void ep_hibernate_demo_remove(struct platform_device *pdev)
{
dev_info(&pdev->dev, "ep_hibernate_demo: removed\n");
}
static struct platform_driver ep_hibernate_demo_driver = {
.probe = ep_hibernate_demo_probe,
.remove = ep_hibernate_demo_remove,
.driver = {
.name = "ep_hibernate_demo",
.pm = &ep_hibernate_demo_pm_ops,
},
};
static struct platform_device *ep_hibernate_demo_pdev;
static int __init ep_hibernate_demo_init(void)
{
int ret;
ret = platform_driver_register(&ep_hibernate_demo_driver);
if (ret)
return ret;
ep_hibernate_demo_pdev = platform_device_register_simple(
"ep_hibernate_demo", -1, NULL, 0);
if (IS_ERR(ep_hibernate_demo_pdev)) {
platform_driver_unregister(&ep_hibernate_demo_driver);
return PTR_ERR(ep_hibernate_demo_pdev);
}
return 0;
}
static void __exit ep_hibernate_demo_exit(void)
{
platform_device_unregister(ep_hibernate_demo_pdev);
platform_driver_unregister(&ep_hibernate_demo_driver);
}
module_init(ep_hibernate_demo_init);
module_exit(ep_hibernate_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("EmbeddedPathashala minimal dev_pm_ops demo driver");
Look specifically at the .driver member of ep_hibernate_demo_driver: the .pm field there is a plain pointer to a const struct dev_pm_ops, and it is the only place this driver connects to the whole system sleep machinery. Nothing about probe(), remove(), or the driver’s name changes because it now participates in power management; the PM core discovers ep_hibernate_demo_pm_ops purely by walking dev->driver->pm at suspend and resume time, exactly the field the theory lecture identified inside struct device_driver.
Build and load it like any out-of-tree module, then confirm the probe log before running a PM test:
$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod ep_hibernate_demo.ko
$ dmesg | tail -n 2
[ 1180.204112] ep_hibernate_demo ep_hibernate_demo.0: ep_hibernate_demo: probed, ready for PM testing
From here, repeat Step 1 or Step 3 above; the two dev_info() lines from this driver’s suspend and resume callbacks will now show up inline with the rest of the kernel’s sleep-transition log, exactly as they did in the annotated logs earlier in this lecture.
Two Ways To Wire Up dev_pm_ops: A Quick Comparison
| Approach | What it fills in | When to use it |
|---|---|---|
| Manual struct initialization | Only the exact callbacks you list, e.g. just .suspend and .resume | Driver genuinely needs different behavior for hibernation freeze/poweroff vs suspend-to-RAM suspend |
| SET_SYSTEM_SLEEP_PM_OPS(suspend_fn, resume_fn) | .suspend, .freeze, .poweroff all point to suspend_fn; .resume, .thaw, .restore all point to resume_fn | Device handling is identical across suspend-to-RAM and hibernation, which is true for most simple drivers, including ep_hibernate_demo |
Common Mistakes And Troubleshooting
- Forgetting to read /sys/power/disk back after writing to it. A typo in the mode string is silently ignored by some shells’ echo; always confirm the brackets moved before triggering the actual transition.
- Choosing shutdown mode on hardware with no remote power control. If you cannot physically reach the power button, use reboot or test_resume for early testing instead.
- Arming the RTC wakealarm with a relative time that has already passed by the time /sys/power/state is written. Leave enough margin, at least fifteen to thirty seconds, between arming the alarm and triggering suspend.
- Only implementing .suspend and .resume by hand and expecting hibernation to call them. Without SET_SYSTEM_SLEEP_PM_OPS or explicit .freeze/.poweroff/.restore entries, a hand-populated dev_pm_ops table leaves those fields NULL, and the PM core simply treats a NULL callback as nothing to do for that phase.
- Not checking dmesg immediately after insmod. If the ep_hibernate_demo_probe log line never appears, the platform_device registration failed silently and no PM callback will ever fire, no matter how correct the suspend/resume code is.
- Rebuilding the module against the wrong kernel headers. A dev_pm_ops layout mismatch between the headers used to build the module and the running kernel is a classic source of an insmod that succeeds but a driver that behaves strangely under PM stress.
Best Practices
- Always arm and verify an RTC wakealarm before the first sleep-state test on any board you cannot reach physically.
- Test hibernation in reboot mode first, before ever trying shutdown mode, so you can watch the full resume log in real time.
- Use test_resume mode while iterating on ep_hibernate_demo’s own callbacks; it is far faster than a real reboot cycle for catching an obvious bug.
- Keep dev_info() messages in every PM callback while developing a driver; they are the single fastest way to confirm which phase actually ran, and in what order, during a real transition.
- Prefer SET_SYSTEM_SLEEP_PM_OPS whenever suspend-to-RAM and hibernation handling are genuinely identical, rather than hand-populating six near-duplicate function pointers.
Summary And Conclusion
This lecture turned the previous lecture’s Linux hibernation theory into commands you can actually run: arming an RTC wakealarm and watching a suspend-to-RAM cycle wake itself up, selecting reboot mode through /sys/power/disk and reading a full hibernation cycle’s kernel log from image creation through the restore kernel’s handoff, and finally building ep_hibernate_demo, an original platform driver whose entire contribution to power management is a four-line dev_pm_ops table built with SET_SYSTEM_SLEEP_PM_OPS and one pointer assignment, .driver.pm, that connects it to the kernel’s sleep machinery. If you worked through every command here, you now have a driver on disk that logs its own suspend and resume callbacks during a real Linux hibernation cycle, which is exactly the foundation the next lecture in this free Linux kernel development course builds on when it extends dev_pm_ops with real runtime PM logic: usage counters, autosuspend delays, and the runtime_suspend/runtime_resume/runtime_idle callbacks only introduced at survey level so far.
Frequently Asked Questions
Why does my board have only rtc0 and not rtc1 like some tutorials show?
The number of RTC devices under /sys/class/rtc/ depends entirely on your platform; a typical PC or single-board computer exposes exactly one, rtc0, backed by either a hardware RTC chip or an emulated one from the firmware. Multiple RTCs only appear on boards with more than one physical RTC source, which is uncommon outside specialized hardware.
What happens if I write a mode to /sys/power/disk that my kernel does not support?
The write fails with an error such as EINVAL, and the currently selected mode, shown in brackets when you read the file, does not change. Always cat /sys/power/disk first to see the exact list of modes your running kernel actually offers before writing to it.
Do I need CONFIG_PM or CONFIG_PM_SLEEP enabled to build ep_hibernate_demo?
You need CONFIG_PM_SLEEP enabled in the kernel you are building against for the suspend and resume paths to actually be exercised. The module itself still compiles without it, but SET_SYSTEM_SLEEP_PM_OPS produces callbacks that the PM core will simply never call if sleep support is not compiled in.
Why does ep_hibernate_demo’s resume callback run twice if I do a suspend-to-RAM test followed by a hibernation test?
Each transition is independent; ep_hibernate_demo_resume() runs once per resume, so a suspend-to-RAM cycle followed later by a separate hibernation cycle produces two separate log lines, and wakeup_count increments each time because the driver’s private data survives both because it lives in ordinary kernel memory that neither transition frees.
Can I skip building ep_hibernate_demo and just test hibernation on my existing hardware drivers?
Yes, hibernation exercises every loaded driver’s dev_pm_ops automatically; you do not need ep_hibernate_demo for hibernation itself to work. The demo driver exists purely so you have one small, fully understood piece of code to correlate against the kernel log, rather than trying to interpret PM behavior from drivers you did not write.
Is it safe to leave .driver.pm NULL on a platform driver?
Yes, a NULL .driver.pm simply means the PM core has nothing device-specific to call for that driver during a system sleep transition; the device is still frozen and thawed as part of the general freezer, it just does not get a chance to do custom suspend or resume work. Many simple drivers with no real low-power behavior leave it NULL deliberately.
Why use platform_device_register_simple instead of a device tree entry for this demo?
platform_device_register_simple lets the module register its own platform device purely in software, with no device tree, ACPI table, or board file changes required, which is exactly what a self-contained teaching demo needs. Real hardware drivers almost always rely on device tree or ACPI matching instead.
Ready For Real Runtime PM?
You have now armed an RTC wakealarm, run a full Linux hibernation cycle through /sys/power/disk, and built ep_hibernate_demo, a working platform driver whose dev_pm_ops table plugs straight into .driver.pm. Continue this free Linux kernel development course into real runtime PM implementation next.
Back To Hibernation Theory Next: Runtime PM Implementation