System suspend in Linux is not one callback per device. It is four ordered phases — prepare, suspend, suspend_late, suspend_noirq — driven by five linked lists that devices migrate through, so the kernel always knows exactly how far it got and what it has to unwind. A wakeup arriving during the suspend or suspend_late phases does not fail a driver; it sets async_error to -EBUSY and the whole transition reverses without recording any device as the cause. This walkthrough traces that path through the power management core in Linux 7.2, from the write to /sys/power/state down to the firmware call, and names the six things 7.2 does differently from the 6.12 longterm kernel most products ship.
This article covers one layer of Linux power management: the system-wide suspend and resume path. It is not about runtime PM, which idles individual devices while the system is running, and not about power domains, cpuidle or cpufreq — those are separate machinery with their own articles. What it does cover is the sequence that runs when something writes to /sys/power/state, because that sequence is where suspend bugs on embedded boards actually occur, and because the console output it produces is unreadable until you know the phase structure behind it.
A suspend that fails and names a device that is not the problem
The reported symptom is usually this one. Suspend returns an error to the shell:
raghu@techveda.org:~$ echo mem > /sys/power/state
-bash: echo: write error: Device or resource busyEngineers then look for the failing device, and often find one named in /sys/power/suspend_stats/last_failed_dev, investigate that driver, and find nothing wrong with it. The reason is in the source. Two of the error paths in the power management core do not report a driver failure at all: they report that a wakeup event arrived while the transition was in progress. On that path the core records no device and prints no device error, because both of those happen only when a driver callback actually returns an error. The name a reader finds in last_failed_dev is left over from an earlier, unrelated failure.
Scope. Everything below about the power management core was read from the v7.2 tag — kernel/power/suspend.c, kernel/power/main.c and drivers/base/power/main.c. As of 3 October 2026, kernel.org lists 7.3-rc5 as mainline and 7.2.8 as the newest released stable kernel, with 6.18, 6.12, 6.6, 6.1, 5.15 and 5.10 as longterm. 7.2 is therefore the newest released kernel and is what this walkthrough uses. The phase structure itself is old and stable, so the structure applies to every one of those longterm series; six specific behaviours differ, and they get their own section. Where a config symbol gates something, the symbol is named.
Five lists are the whole power management state machine
The mental model worth having for suspend-time power management is not the callback names. It is the lists. drivers/base/power/main.c declares exactly five:
LIST_HEAD(dpm_list);
static LIST_HEAD(dpm_prepared_list);
static LIST_HEAD(dpm_suspended_list);
static LIST_HEAD(dpm_late_early_list);
static LIST_HEAD(dpm_noirq_list);Every device registered with the core sits on dpm_list, and the comment above the declaration explains the one property the whole design depends on:
/*
* The entries in the dpm_list list are in a depth first order, simply
* because children are guaranteed to be discovered after parents, and
* are inserted at the back of the list on discovery.
*
* Since device_pm_add() may be called with a device lock held,
* we must never try to acquire a device lock while holding
* dpm_list_mutex.
*/Because children always appear after their parents, walking a list forwards is parent-before-child and walking it backwards is child-before-parent. That single invariant supplies the ordering without the core recording anything further: the three suspend callback phases take devices from the tail of their source list, so a child is suspended before the parent it depends on, and the three resume phases take them from the head. dpm_prepare() is the one that runs the other way, walking dpm_list forwards.
Each phase then moves devices from one list to the next as it completes them. dpm_suspend() takes devices off dpm_prepared_list and puts them on dpm_suspended_list; dpm_suspend_late() moves them to dpm_late_early_list; dpm_suspend_noirq() moves them to dpm_noirq_list. The migration is how the core keeps that record. At any instant, which list a device is on says precisely which callbacks have already run for it, so the resume path knows exactly what to undo without keeping a separate record. When a phase aborts, the code does not have to reason about it either — it splices the remainder across in one move:
if (READ_ONCE(async_error)) {
dpm_async_suspend_complete_all(&dpm_prepared_list);
/*
* Move all devices to the target list to resume them
* properly.
*/
list_splice_init(&dpm_prepared_list, &dpm_suspended_list);
break;
}Devices that were never suspended are moved onto the suspended list anyway, so that the resume walk visits them and their state is consistent afterwards. Note that this splice is 7.2 behaviour: 6.12 breaks out of the loop and leaves the unvisited devices on the prepared list, so it is a seventh difference on top of the six below.
The four phases exist because device dependencies are not all the same kind. prepare only asks drivers whether a transition is acceptable and stops new probes. suspend is the ordinary case, running with interrupts enabled and sleeping allowed. suspend_late runs after that, for work that must happen once the ordinary callbacks are done. suspend_noirq runs with device interrupts disabled, which is the only point at which a driver can touch hardware without its own handler racing it. Each phase has its own member in struct dev_pm_ops, and the core picks the right one from the transition type:
case PM_EVENT_SUSPEND:
return ops->suspend_noirq;
case PM_EVENT_RESUME:
return ops->resume_noirq;The call chain from /sys/power/state down to the firmware
A write to /sys/power/state lands in state_store(), which decodes the label and calls pm_suspend(). Note what pm_suspend() does with the result, because it is where the counters you later read come from:
pr_info("suspend entry (%s)\n", mem_sleep_labels[state]);
error = enter_state(state);
dpm_save_errno(error);
pr_info("suspend exit\n");Those two lines bracket every suspend attempt in the log, so suspend entry and suspend exit are the outermost markers to grep for. The label in the parentheses is mem_sleep_labels[state], and state_store() translates a write of mem through mem_sleep_current, which defaults to PM_SUSPEND_TO_IDLE. On a board whose suspend_ops does not validate PM_SUSPEND_MEM, writing mem therefore logs suspend entry (s2idle), not suspend entry (deep). enter_state() then takes system_transition_mutex with a trylock, which is why a second concurrent suspend attempt returns -EBUSY immediately rather than queueing:
if (!mutex_trylock(&system_transition_mutex))
return -EBUSY;From there the sequence is: sync filesystems if /sys/power/sync_on_suspend allows it, then suspend_prepare() to run the PM notifiers and freeze user processes, then suspend_devices_and_enter(). That function does the device work in two halves:
error = dpm_suspend_start(PMSG_SUSPEND);
if (error) {
pr_err("Some devices failed to suspend, or early wake event detected\n");
goto Recover_platform;
}Read that error string carefully, because it covers two different cases. “Some devices failed to suspend, or early wake event detected” is the core telling you it cannot distinguish the two cases at this level. dpm_suspend_start() is simply dpm_prepare() followed by dpm_suspend(), with the allocation mask tightened in between so that no driver callback can trigger filesystem or block I/O while devices are being suspended:
int dpm_suspend_start(pm_message_t state)
{
ktime_t starttime = ktime_get();
int error;
error = dpm_prepare(state);
if (error)
dpm_save_failed_step(SUSPEND_PREPARE);
else {
pm_restrict_gfp_mask();
error = dpm_suspend(state);
}
dpm_show_time(starttime, state, error, "start");
return error;
}The second half is suspend_enter(), and its structure is worth reading as a whole because the unwind is encoded in the labels rather than in any cleanup function:
error = dpm_suspend_late(PMSG_SUSPEND);
if (error) {
pr_err("late suspend of devices failed\n");
goto Platform_finish;
}
...
error = dpm_suspend_noirq(PMSG_SUSPEND);
if (error) {
pr_err("noirq suspend of devices failed\n");
goto Platform_early_resume;
}
...
if (state == PM_SUSPEND_TO_IDLE) {
s2idle_loop();
goto Platform_wake;
}
error = pm_sleep_disable_secondary_cpus();
if (error || suspend_test(TEST_CPUS))
goto Enable_cpus;
arch_suspend_disable_irqs();
BUG_ON(!irqs_disabled());
system_state = SYSTEM_SUSPEND;
error = syscore_suspend();
if (!error) {
*wakeup = pm_wakeup_pending();
if (!(suspend_test(TEST_CORE) || *wakeup)) {
...
error = suspend_ops->enter(state);
...
}
...
}Several things in that fragment are the answers to questions people ask about this path. The branch on PM_SUSPEND_TO_IDLE is where the two paths part inside suspend_enter(): s2idle never offlines the secondary CPUs, never disables interrupts at the architecture level, never calls syscore_suspend() and never calls suspend_ops->enter(), it just pushes every CPU into the idle loop and waits. A platform suspend does all four. s2idle does still use a platform op table of its own, s2idle_ops, through platform_suspend_prepare_noirq() and platform_resume_noirq(). The BUG_ON(!irqs_disabled()) is not merely a defensive check either — the code after it runs with no way to recover, so the kernel stops here rather than continue in a state it did not expect. And suspend_ops->enter(state) is the last line of kernel C code that runs before the firmware call; on arm64 with PSCI that is where control passes out of Linux.
The goto labels unwind in reverse order of what succeeded, and each pairs with the call directly beneath it: Enable_cpus → pm_sleep_enable_secondary_cpus(), Platform_wake → platform_resume_noirq() then dpm_resume_noirq(), Platform_early_resume → platform_resume_early(), Devices_early_resume → dpm_resume_early(), Platform_finish → platform_resume_finish(). Each entry point into that chain resumes exactly the phases that had completed, which is the other half of what the five lists provide.
Where a wakeup aborts the transition, and why power management reports -EBUSY
This is the mechanism behind the symptom at the top, and it is the single most misread part of the power management core, so it is worth quoting exactly. Exactly two functions check for a pending wakeup before touching the device: device_suspend(), which runs in the suspend phase, and device_suspend_late(), which runs in the suspend_late phase. Neither device_prepare() nor device_suspend_noirq() has such a check. This is device_suspend():
if (pm_wakeup_pending()) {
dev->power.direct_complete = false;
WRITE_ONCE(async_error, -EBUSY);
goto Complete;
}The version in device_suspend_late() is the same three lines without the direct_complete assignment. The check sets the shared async_error to -EBUSY and returns. The per-phase loop sees a non-zero async_error, splices the remaining devices across, and the phase returns that error — which propagates out through dpm_suspend_start(), suspend_devices_and_enter(), enter_state() and finally to the write() that started it. That is the Device or resource busy the shell printed.
Two consequences follow, and both are practical. First, every -EBUSY that originates at one of those two checks is a wakeup, not a driver fault. Second, and less obvious, the core records nothing about a device on this path. dpm_save_failed_dev() and pm_dev_err() are both called only from inside an if (error) block that follows the driver callback, and the wakeup check jumps past that block with error still zero. So last_failed_dev keeps whatever name an earlier genuine failure left there, and nothing is printed for this suspend at all. A stale name is worse than a wrong one, because it looks current. The core does record step-level information that is reliable — each phase calls dpm_save_failed_step() with its own constant, giving you prepare, suspend, suspend_late or suspend_noirq in /sys/power/suspend_stats/last_failed_step. Trust the step before you trust the device name.
The other abort point is earlier and newer. In 7.2 the filesystem sync itself can refuse to proceed:
while (!pm_fs_sync_completed()) {
if (pm_wakeup_pending())
return -EBUSY;
wait_event_timeout(pm_fs_sync_wait, pm_fs_sync_completed(),
PM_FS_SYNC_WAKEUP_RESOLUTION);
}pm_sleep_fs_sync() runs the sync on an ordered workqueue and polls for wakeups every five jiffies while it waits. If one arrives, suspend is abandoned before the notifiers run and before a single process is frozen. On a device that suspends and resumes frequently this is a visible behaviour change, and it is the reason a 7.x board can report a failed suspend with nothing at all in the device logs.
What Linux 7.2 changed in power management, and what your longterm kernel does instead
These six differences in the power management core were read by comparing the v7.2 and v6.12 trees directly, and they are in addition to the abort splice noted above. They matter because most shipping products are on a longterm kernel, so the source a reader has open may not match the source described in newer documentation.
1. Filesystems are frozen, not just synced. 7.2 adds filesystems_freeze(filesystem_freeze_enabled) to suspend_prepare(), with matching filesystems_thaw() calls in the failure path and in suspend_finish(). It is controlled at runtime by /sys/power/freeze_filesystems and defaults to off (filesystem_freeze_enabled = false). 6.12 has neither the call nor the file.
2. The sync can now fail the suspend. 6.12 calls ksys_sync_helper(), which returns nothing and cannot abort anything. 7.2 calls pm_sleep_fs_sync() and acts on the result with goto Unlock. A wakeup during the sync aborts the transition on 7.2 and does not on 6.12.
3. The console functions and one tracepoint were renamed. suspend_console() and resume_console() became console_suspend_all() and console_resume_all(). The rename reached the trace output: the tracepoint string "resume_console" in 6.12 is "console_resume_all" in 7.2. Any script that greps the suspend_resume tracepoint for resume_console silently matches nothing on a 7.x kernel.
4. The allocation mask is restricted later. 6.12 wraps pm_restrict_gfp_mask() and pm_restore_gfp_mask() around the whole of suspend_devices_and_enter() inside enter_state(). 7.2 moves the restriction into dpm_suspend_start(), between dpm_prepare() and dpm_suspend(), so prepare callbacks now run with the normal mask.
5. The device PM watchdog warns before it panics. This is the most useful change for anyone debugging a stuck driver. In 6.12 the handler fires once:
static void dpm_watchdog_handler(struct timer_list *t)
{
struct dpm_watchdog *wd = from_timer(wd, t, timer);
dev_emerg(wd->dev, "**** DPM device timeout ****\n");
show_stack(wd->tsk, NULL, KERN_EMERG);
panic("%s %s: unrecoverable failure\n",
dev_driver_string(wd->dev), dev_name(wd->dev));
}In 7.2 it fires twice. At the warning timeout it prints a stack and keeps going; only at the main timeout does it panic:
time_left = dpm_watchdog_timeout - dpm_watchdog_warning_timeout;
dev_warn(wd->dev, "**** DPM device timeout after %u seconds; %u seconds until panic ****\n",
dpm_watchdog_warning_timeout, time_left);
show_stack(wd->tsk, NULL, KERN_WARNING);
wd->fatal = true;
mod_timer(timer, jiffies + HZ * time_left);Both timeouts are now tunable at runtime through kernel.dpm_watchdog_timeout_secs and kernel.dpm_watchdog_warning_timeout_secs, where 6.12 had only the compile-time CONFIG_DPM_WATCHDOG_TIMEOUT. Both are tunable downwards only: the panic timeout is clamped to CONFIG_DPM_WATCHDOG_TIMEOUT, which Kconfig itself caps at 120, and the warning timeout is clamped to the live panic timeout. There is also a dpm_watchdog_all_cpu_backtrace module parameter that dumps every CPU, on the panic pass only. The defaults keep the old behaviour: DPM_WATCHDOG_TIMEOUT defaults to 120 seconds and DPM_WATCHDOG_WARNING_TIMEOUT defaults to the same value, and the Kconfig help states that when they are equal the warning never happens and the system jumps straight to panic. Setting the warning lower is what turns a bare panic into a usable stack trace plus time to react. The whole feature needs CONFIG_DPM_WATCHDOG, which depends on PM_DEBUG, PSTORE and EXPERT.
6. Asynchronous suspend starts with the leaves. 7.2 adds a pass before each phase’s main loop that starts the devices with no dependents first:
/*
* Start processing "async" leaf devices upfront so they don't need to
* wait for the "sync" devices they don't depend on.
*/
list_for_each_entry_reverse(dev, &dpm_prepared_list, power.entry) {
dpm_clear_async_state(dev);
if (dpm_leaf_device(dev))
dpm_async_with_cleanup(dev, async_suspend);
}6.12 has no leaf pass; it only dispatches asynchronously as it walks. The practical effect is shorter suspend and resume times on boards with wide device trees, and it is why timings measured on a 6.x kernel do not carry over. Asynchronous suspend can be turned off entirely with pm_async=off on the command line or by writing 0 to /sys/power/pm_async, which is the first thing to try when a 7.x board suspends unreliably and a 6.x board did not.
Checking the power management path on your own board
These run in order and each rules something out. Driver-side work below this level is covered in our Linux device drivers training.
Step 1 — turn on the core’s own debug messages. The phase timings and most of the interesting core messages go through pm_pr_dbg(), which prints only when pm_debug_messages is on and a sleep transition is in progress:
raghu@techveda.org:~$ echo 1 > /sys/power/pm_debug_messages
raghu@techveda.org:~$ echo 1 > /sys/power/pm_print_timesWith both on you get a dpm_show_time() line for each of the four device phases, plus one each for the start and end groupings, in the form noirq suspend of devices complete after 12.345 msecs, plus a per-device timing line for every callback. The phase word is aborted instead of complete when that phase returned an error, which is the fastest way to see which phase stopped.
Step 2 — read the step, not the device. The step is recorded reliably; the device name is not always meaningful, as shown above:
raghu@techveda.org:~$ cd /sys/power/suspend_stats && grep . success fail last_failed_step last_failed_errno last_failed_dev
success:12
fail:3
last_failed_step:suspend_noirq
last_failed_errno:-16
last_failed_dev:1-0036The values above are the expected form, not a capture from a specific board. -16 is -EBUSY. Two further facts about these files are in the source and worth knowing: only the last two failures are kept, because REC_FAILED_NUM is 2, and the device name field is char[40], so a longer name is truncated. The last_hw_sleep, total_hw_sleep and max_hw_sleep files are hidden unless the platform is ACPI with low-power S0 advertised, so on an Arm board they will normally be absent.
Step 3 — bisect the phases without suspending. /sys/power/pm_test makes the core stop at a chosen point, wait, and come back. The accepted values come straight from pm_tests[]: none, core, processors, platform, devices, freezer.
raghu@techveda.org:~$ echo devices > /sys/power/pm_test
raghu@techveda.org:~$ echo mem > /sys/power/stateRead the result carefully. TEST_DEVICES is checked immediately after dpm_suspend_start(), so it covers prepare and suspend only, while TEST_PLATFORM is checked after dpm_suspend_late() and dpm_suspend_noirq() have both run. If devices completes and platform does not, the failure is in suspend_late, suspend_noirq, or the platform’s own prepare_late and prepare_noirq callbacks — which still includes drivers. No pm_test level reaches suspend_ops->enter() at all: even core short-circuits it. Note also the restriction in enter_state(): for s2idle only none, freezer, devices and platform are accepted, and the deeper levels return -EAGAIN with a warning, because there is nothing to test there.
Step 4 — find out what woke the board. If the step is a wakeup abort rather than a driver error, the wakeup source is the thing to chase:
raghu@techveda.org:~$ cat /sys/power/pm_wakeup_irq
raghu@techveda.org:~$ grep -H . /sys/class/wakeup/*/active_count | grep -v ':0$'pm_wakeup_irq returns -ENODATA when nothing has been recorded, which is itself the answer: no wakeup IRQ was attributed, so look for a driver error instead.
Step 5 — make a stuck driver produce a stack instead of a panic. With CONFIG_DPM_WATCHDOG built in on a 7.x kernel, lower the warning timeout well below the panic timeout:
raghu@techveda.org:~$ sysctl -w kernel.dpm_watchdog_warning_timeout_secs=10
raghu@techveda.org:~$ sysctl -w kernel.dpm_watchdog_timeout_secs=120A driver that hangs now prints **** DPM device timeout after 10 seconds; 110 seconds until panic **** with the stuck task’s stack, and you have the backtrace without the machine panicking. On 6.12 this is not available and the first symptom is the panic.
Wrong turns that follow from an incomplete model
Each of these is sound reasoning from a power management model that is missing one fact.
“last_failed_dev names the broken driver.” Attractive because the kernel went to the trouble of recording a device name, so it must mean something. The missing fact is the wakeup abort path: when pm_wakeup_pending() returns true, the core writes -EBUSY into the shared async_error and jumps straight past the block that would have recorded a device, because that block runs only when a driver callback returned an error. Nothing is recorded, so the file still holds a name from an earlier, unrelated failure. The name is a real device, which is what makes it convincing, but it has nothing to do with this suspend. Check last_failed_errno first: -16 points at a wakeup, a driver-returned code points at the driver.
“The suspend callbacks run in the order the devices appear in sysfs.” Attractive because sysfs is the view of the device tree everyone has. The missing fact is that ordering comes from dpm_list, whose order is registration order, which is depth-first only as a side effect of children being discovered after parents. Device links change the effective order again, through dpm_wait_for_suppliers() and dpm_wait_for_consumers(). A driver that assumes a particular peer has already suspended is relying on something the core never guarantees.
“Adding a suspend_noirq callback is a safe way to make suspend more reliable.” Attractive because the noirq phase is documented as the point at which a driver can touch hardware without its own handler racing it, which is true. The missing fact is what runs before it, one phase earlier. device_suspend_late() calls pm_runtime_disable(dev), with an explicit comment that runtime PM operations on the device will fail until the matching pm_runtime_enable() in device_resume_early(). Runtime PM is therefore already disabled for the suspend_late callback as well as the suspend_noirq one. Code in either that calls pm_runtime_get_sync(), directly or through a helper, fails there and nowhere else — which is why it passes review and breaks on the board, and why moving the call from suspend_noirq into suspend_late does not fix it.
“s2idle and deep suspend are the same path with a different final call.” Attractive because both are reached by writing to /sys/power/state and both suspend all devices. The missing fact is how early they diverge. In suspend_enter(), s2idle branches to s2idle_loop() and jumps straight to Platform_wake, which means it never offlines secondary CPUs, never calls arch_suspend_disable_irqs(), never calls syscore_suspend() and never calls suspend_ops->enter(). A board where s2idle works and deep suspend does not shows that the fault is in one of those four, not in device suspend.
“Our suspend timings regressed when we moved to 7.x, so something got slower.” Attractive because the numbers are worse and the kernel is the only thing that changed. The missing fact is the leaf-first asynchronous pass added in 7.2, which changes which callbacks overlap and therefore which device appears responsible for the time. Per-device numbers from pm_print_times are not comparable across that change. Compare the per-phase dpm_show_time() totals instead, and set pm_async=off on both kernels if you need a like-for-like figure.
Key takeaways
- Suspend-time power management is four phases —
prepare,suspend,suspend_late,suspend_noirq— and five lists that devices migrate through; the list a device is on is the record of what has run for it. - The three suspend callback phases walk their source list from the tail, giving child-before-parent ordering from registration order alone, and the three resume phases walk from the head.
dpm_prepare()is the exception: it walksdpm_listforwards, parent first. - A pending wakeup, checked in
device_suspend()anddevice_suspend_late()only, setsasync_errorto-EBUSYand reverses the whole transition. - Trust
last_failed_step. On a wakeup abort no device is recorded at all, solast_failed_devcan hold a stale name from an earlier failure; only the last two failures are kept and names are truncated at 40 characters. - s2idle diverges from deep suspend inside
suspend_enter()and skips CPU offlining, IRQ disabling,syscore_suspend()andsuspend_ops->enter()entirely. - On 7.2 the filesystem sync can abort a suspend, filesystems can be frozen via
/sys/power/freeze_filesystems, the console tracepoint isconsole_resume_all, and the DPM watchdog can warn before it panics. An aborting phase also splices its remaining devices forward instead of breaking out of the loop. None of that is true of 6.12. /sys/power/pm_testbisects the phases without ever reaching the firmware, and is the quickest way to separate a driver problem from a platform problem.
Verification checklist
Run these after a change you believe fixed a power management problem. Each is an observable.
cat /sys/power/suspend_stats/faildoes not increase across ten suspend and resume cycles.cat /sys/power/suspend_stats/last_failed_stepis unchanged from before the test run.- With
pm_debug_messageson, every phase line indmesgreadscompleteand none readsaborted. dmesg | grep -c "suspend entry"equalsdmesg | grep -c "suspend exit".dmesg | grep -E "failed to (suspend|resume)"returns nothing.dmesg | grep "not prepared for power transition"returns nothing. That string is thedev_info()dpm_prepare()emits when a driver’spreparecallback returns an error other than-EAGAIN.echo platform > /sys/power/pm_testthen a suspend completes and returns, confirming every device phase succeeds without the firmware call.dmesg | grep "DPM device timeout"returns nothing after a full cycle.- Ten suspend and resume cycles with
pm_async=offleave/sys/power/suspend_stats/successhigher by the same amount as ten cycles with it on.
Related reading on TECH VEDA
- Runtime PM: Why Your Device Never Suspends, and How to Find Out — the other part of device power management, for devices idling while the system is up rather than being suspended with it.
- Linux genpd Power Domains: Why a Rail Won’t Power Off? — what happens below the per-device callbacks when a whole rail has to drop.
- From start_kernel() to Userspace: A Line-by-Line Walkthrough — the companion source walkthrough, following the boot path with the same method.
- Capturing a Kernel Crash Dump with kdump on ARM64 — for the case where the DPM watchdog does panic and you need the dump it was designed to leave behind.
Frequently asked questions
What are the four device suspend phases in Linux?prepare, suspend, suspend_late and suspend_noirq, in that order, each with its own member in struct dev_pm_ops. Devices move from dpm_list through dpm_prepared_list, dpm_suspended_list, dpm_late_early_list and dpm_noirq_list as each phase completes them.
Why does writing to /sys/power/state return Device or resource busy?
Most often because a wakeup event was pending. device_suspend() and device_suspend_late() both check pm_wakeup_pending() and set the shared async_error to -EBUSY, which aborts the whole transition. On Linux 7.2 the filesystem sync in pm_sleep_fs_sync() can also return -EBUSY for the same reason, before any device is touched.
Can I trust last_failed_dev to name the driver that blocked suspend?
No. On a wakeup abort the core records no device at all, because dpm_save_failed_dev() runs only when a driver callback returns an error, so the file keeps a stale name from an earlier failure. Read last_failed_errno and last_failed_step first; -16 means -EBUSY and points at a wakeup rather than a driver.
How is suspend-to-idle different from deep suspend in the source?
In suspend_enter(), suspend-to-idle calls s2idle_loop() and jumps to the wake label, so it never offlines secondary CPUs, never calls arch_suspend_disable_irqs(), never calls syscore_suspend() and never calls suspend_ops->enter(). Deep suspend does all four.
What changed in the suspend path between the 6.12 longterm kernel and 7.2?
Seven, counting the abort splice. Filesystems can now be frozen through /sys/power/freeze_filesystems; the filesystem sync can abort a suspend; suspend_console() and resume_console() became console_suspend_all() and console_resume_all(), renaming one tracepoint string; the allocation mask is restricted later, inside dpm_suspend_start(); the DPM watchdog can warn before it panics and both timeouts are sysctls; and asynchronous suspend now starts with leaf devices. The seventh is that an aborting phase splices its remaining devices onto the next list instead of simply breaking out of the loop.
Further reading
kernel/power/suspend.cin Linux 7.2 — enter_state, suspend_devices_and_enter, suspend_enter, s2idle_loopdrivers/base/power/main.cin Linux 7.2 — the five device lists, the four phases and the DPM watchdogkernel/power/main.cin Linux 7.2 — the /sys/power attributes, suspend_stats and pm_sleep_fs_synckernel/power/Kconfigin Linux 7.2 — DPM_WATCHDOG, DPM_WATCHDOG_TIMEOUT and DPM_WATCHDOG_WARNING_TIMEOUT- Kernel documentation — System Sleep States and Device Power Management Basics
- Kernel release index — kernel.org for the current mainline, stable and longterm versions




