Runtime PM Usage Counters and Autosuspend

Runtime PM decides when a device is idle using two atomic counters and an inactivity timer. The usage counter (dev->power.usage_count) is an atomic_t reference count of “how many code paths currently need this device powered”; the child counter (dev->power.child_count) tracks how many of the device’s children are still active. The device is allowed to power down precisely when both reach zero (runtime.c, v6.12). Drivers never touch the counters directly — they call the pm_runtime_get*() family to take a reference (and optionally resume) and the pm_runtime_put*() family to drop one (and optionally trigger an idle check). Layered on top is autosuspend: instead of suspending the instant the count hits zero, the core waits an autosuspend_delay (in milliseconds) measured from the last pm_runtime_mark_last_busy() timestamp, so a device that goes briefly idle does not thrash between power states (runtime_pm.rst, v6.12). Every one of these knobs is exposed under /sys/.../power/. The single most common runtime-PM bug — a get/put imbalance — is, in the end, an off-by-one on this reference count.

This note is pinned to Linux 6.12 LTS (released 2024-11-17), with the 6.18 LTS (2025-11-30) helper-layer changes called out explicitly because they materially change how pm_runtime_put_autosuspend() behaves. Function bodies and field names are quoted from the source at the v6.12 and v6.18 tags. The state machine these counters drive is Runtime Power Management; this note owns the counting and timing.

Mental Model: a Reference Count on “Keep Me Powered”

The usage counter is the spine. Conceptually, every time a code path is about to use the device’s hardware it takes a reference; every time it finishes it drops one. When the reference count falls to zero — and no child is holding the device up — the core runs the idle/suspend path. The counter is a plain atomic_t, so increments and decrements are lock-free; the spinlock is only taken when a transition might actually happen.

flowchart LR
  subgraph GET["take a reference (get)"]
    G1["pm_runtime_get_noresume<br/>count++ only"]
    G2["pm_runtime_get<br/>count++ · async resume"]
    G3["pm_runtime_get_sync<br/>count++ · sync resume"]
    G4["pm_runtime_resume_and_get<br/>sync resume · count++ if ok"]
  end
  subgraph PUT["drop a reference (put)"]
    P1["pm_runtime_put_noidle<br/>count-- only"]
    P2["pm_runtime_put<br/>count-- · idle check if 0"]
    P3["pm_runtime_put_sync<br/>count-- · sync idle if 0"]
    P4["pm_runtime_put_autosuspend<br/>count-- · queue autosuspend if 0"]
  end
  GET --> USE["device RPM_ACTIVE<br/>(driver touches hardware)"]
  USE --> PUT
  PUT -->|"count reaches 0<br/>and child_count == 0"| IDLE["idle/suspend path runs"]

The get/put family, grouped by what each variant does beyond changing the count. What it shows: “get” variants differ only in whether they also resume the device and whether they do it synchronously; “put” variants differ only in what they trigger when the count hits zero (nothing / idle check / synchronous idle / queued autosuspend). The insight to take: the count delta is identical across the whole family (+1 for every get, −1 for every put). Choosing a variant is about side effects — resume now vs later, suspend now vs after a delay — never about the reference count itself. That is why a leaked get and a missing put are the same bug.

The Counters in struct dev_pm_info

The fields live in struct dev_pm_info (pm.h, v6.12):

atomic_t        usage_count;     /* references held on this device */
atomic_t        child_count;     /* number of active children */
unsigned int    disable_depth:3; /* >0 means runtime PM is disabled; init = 1 */
bool            runtime_auto:1;  /* false == userspace "forbade" suspend via power/control */
bool            ignore_children:1;
bool            use_autosuspend:1;
bool            timer_autosuspends:1;
int             autosuspend_delay;  /* milliseconds; negative == never suspend */
u64             last_busy;          /* ktime_get_mono_fast_ns() of last activity */
u64             active_time;        /* accounted ns spent RPM_ACTIVE */
u64             suspended_time;     /* accounted ns spent RPM_SUSPENDED */

child_count is maintained automatically by the core: rpm_resume() does atomic_inc(&parent->power.child_count) when a child becomes active, and rpm_suspend() does atomic_add_unless(&parent->power.child_count, -1, 0) when it suspends (see Runtime Power Management). A driver almost never touches it. usage_count, by contrast, is moved by every get/put call, and getting that balance right is the driver’s job.

The Get Family

The get helpers all increment usage_count; they differ in whether (and how) they resume. From pm_runtime.h, v6.12:

  • pm_runtime_get_noresume(dev)atomic_inc(&dev->power.usage_count) and nothing else. Pure pin: bump the count without waking the device. Used when you want to prevent suspend but do not need the hardware right now (the core itself uses it to pin a parent during a child’s resume).
  • pm_runtime_get(dev)__pm_runtime_resume(dev, RPM_GET_PUT | RPM_ASYNC): increment, then queue an asynchronous resume. Returns before the device is actually up; the caller must not touch hardware yet. Safe from atomic context.
  • pm_runtime_get_sync(dev)__pm_runtime_resume(dev, RPM_GET_PUT): increment, then resume synchronously, blocking until the device is RPM_ACTIVE. Caveat: it keeps the count incremented even on error, so a caller that doesn’t carefully put on the error path leaks a reference. The header itself steers you to pm_runtime_resume_and_get() instead.
  • pm_runtime_resume_and_get(dev) — resume synchronously and increment only if the resume succeeded; on error it calls pm_runtime_put_noidle() to undo the increment and returns the error. This is the modern, leak-proof “I need the device now” call.
  • pm_runtime_get_if_in_use(dev) — increment only if the device is currently RPM_ACTIVE and its usage count is already non-zero (i.e. someone else is already using it); otherwise leave the count untouched and return 0. Returns -EINVAL if runtime PM is disabled. This is the “join an existing user without waking the device myself” call — vital in fast paths and interrupt handlers that must not trigger a resume.
  • pm_runtime_get_if_active(dev) — increment if the device is RPM_ACTIVE, regardless of the current usage count; return 0 if suspended, -EINVAL if disabled. Use it to hold an already-active device active without racing a suspend.

Both _if_* helpers are implemented by a single pm_runtime_get_conditional(dev, ign_usage_count):

static int pm_runtime_get_conditional(struct device *dev, bool ign_usage_count)
{
	...
	if (dev->power.disable_depth > 0)
		retval = -EINVAL;
	else if (dev->power.runtime_status != RPM_ACTIVE)
		retval = 0;
	else if (ign_usage_count) {          /* _if_active */
		retval = 1;
		atomic_inc(&dev->power.usage_count);
	} else                               /* _if_in_use */
		retval = atomic_inc_not_zero(&dev->power.usage_count);
	...
}

The atomic_inc_not_zero is the whole trick: _if_in_use increments only when the count was already positive, so it can never initiate a resume — it only piggybacks on an existing user.

Uncertain

Verify: the exact signature of pm_runtime_get_if_active() across releases. Reason: in older kernels this function took a second bool ign_usage_count argument; the v6.12 source I read declares it as a one-argument pm_runtime_get_if_active(struct device *dev) delegating to pm_runtime_get_conditional(dev, true). Driver code written against an older API will not compile against 6.12. To resolve: check the prototype in include/linux/pm_runtime.h at the specific tag you are building against. uncertain

The Put Family

The put helpers all decrement; they differ in what they trigger when the count reaches zero. From pm_runtime.h, v6.12:

  • pm_runtime_put_noidle(dev)atomic_add_unless(&dev->power.usage_count, -1, 0): decrement (but never below zero) and trigger nothing. The exact undo for pm_runtime_get_noresume().
  • pm_runtime_put(dev)__pm_runtime_idle(dev, RPM_GET_PUT | RPM_ASYNC): decrement; if the result is 0, queue an asynchronous idle check (which may lead to a suspend). The everyday “I’m done” call.
  • pm_runtime_put_sync(dev)__pm_runtime_idle(dev, RPM_GET_PUT): decrement; if 0, run the idle check synchronously in the caller’s context. Useful at remove/teardown where you want the suspend to complete before you return.
  • pm_runtime_put_autosuspend(dev) — decrement; if 0, schedule an autosuspend (suspend after autosuspend_delay). This is the right call for a device using autosuspend, because it defers the actual power-down past brief idle gaps.
  • pm_runtime_put_sync_suspend(dev) — decrement; if 0, suspend synchronously now, bypassing the idle callback.
  • pm_runtime_put_sync_autosuspend(dev) — decrement; if 0, set up autosuspend synchronously.

The underlying __pm_runtime_idle/suspend(dev, RPM_GET_PUT | ...) entry points first decrement the count via rpm_drop_usage_count() and return immediately if the result is still > 0 — only when the last reference drops do they engage the engine. That short-circuit is why put is cheap in the common case where other users remain.

The 6.18 change you must know about

In 6.12, pm_runtime_put_autosuspend() is just __pm_runtime_suspend(dev, RPM_GET_PUT | RPM_ASYNC | RPM_AUTO) — it does not update the last-busy timestamp. A 6.12 driver must therefore call pm_runtime_mark_last_busy(dev) itself before pm_runtime_put_autosuspend(), or the autosuspend delay is measured from a stale timestamp and the device suspends too early. The 6.12 header even carries a blunt warning that the autosuspend put helper will change and says “DO NOT USE!” about a transitional variant.

In 6.18, that consolidation has landed: pm_runtime_put_autosuspend() and pm_runtime_put_sync_autosuspend() now call pm_runtime_mark_last_busy(dev) internally (pm_runtime.h, v6.18):

/* v6.18 */
static inline int pm_runtime_put_autosuspend(struct device *dev)
{
	pm_runtime_mark_last_busy(dev);
	return __pm_runtime_put_autosuspend(dev);
}

The now-redundant explicit pm_runtime_mark_last_busy() calls were swept out of hundreds of drivers as part of the same series (LKML, 2025). The practical upshot: the same source line means different things on the two LTS lines — on 6.12 you still need the manual mark_last_busy; on 6.18 it is automatic. 6.18 also adds DEFINE_GUARD(pm_runtime_active, ...) scoped guards that pair a get_sync with an automatic put at scope exit.

Autosuspend: the Delay, the Timestamp, the Timer

Autosuspend inserts an inactivity delay between “usage count hit zero” and “actually suspend.” Three pieces cooperate:

  1. pm_runtime_use_autosuspend(dev) sets power.use_autosuspend, enabling the feature. Its undo, pm_runtime_dont_use_autosuspend(dev), clears it. (Both route through __pm_runtime_use_autosuspend().) Without this flag set, all the autosuspend put variants behave like immediate suspends and autosuspend_delay_ms reads back -EIO from sysfs.
  2. pm_runtime_set_autosuspend_delay(dev, delay) sets power.autosuspend_delay in milliseconds. A negative delay means “never runtime-suspend this device.” The implementation is subtle: changing the delay across the zero boundary while use_autosuspend is set deliberately takes or drops a usage reference so that a negative delay actually pins the device active:
static void update_autosuspend(struct device *dev, int old_delay, int old_use)
{
	int delay = dev->power.autosuspend_delay;
 
	if (dev->power.use_autosuspend && delay < 0) {       /* now forbidden */
		if (!old_use || old_delay >= 0) {
			atomic_inc(&dev->power.usage_count);     /* pin active */
			rpm_resume(dev, 0);
		}
	} else {                                             /* now allowed */
		if (old_use && old_delay < 0)
			atomic_dec(&dev->power.usage_count);     /* unpin */
		rpm_idle(dev, RPM_AUTO);
	}
}
  1. pm_runtime_mark_last_busy(dev) stamps power.last_busy with the current monotonic time: WRITE_ONCE(dev->power.last_busy, ktime_get_mono_fast_ns()). Drivers call this on every access so the “idle since” clock keeps resetting while the device is in use.

The expiration is computed by pm_runtime_autosuspend_expiration():

u64 pm_runtime_autosuspend_expiration(struct device *dev)
{
	if (!dev->power.use_autosuspend)
		return 0;
	int autosuspend_delay = READ_ONCE(dev->power.autosuspend_delay);
	if (autosuspend_delay < 0)
		return 0;
	u64 expires = READ_ONCE(dev->power.last_busy);
	expires += (u64)autosuspend_delay * NSEC_PER_MSEC;
	if (expires > ktime_get_mono_fast_ns())
		return expires;   /* still in the future -> don't suspend yet */
	return 0;             /* delay elapsed -> ok to suspend */
}

When rpm_suspend() is called with RPM_AUTO and this returns a future time, it arms dev->power.suspend_timer (an hrtimer) to fire then, with a 25% slack to coalesce wakeups, and returns without suspending. When the timer fires, pm_suspend_timer_fn() re-enters rpm_suspend() with RPM_ASYNC | RPM_AUTO. This is how a 50 ms idle gap on a device with a 100 ms autosuspend delay does not cause a suspend, but a 150 ms gap does.

Putting It Together: the Counting Lifecycle in a Driver

The whole machinery comes together in a single, conventional pattern. At probe time a driver opts the device into autosuspend and enables runtime PM; at every I/O it brackets the access with a get/put pair; the autosuspend timer does the rest. A representative skeleton (6.12-style, with the explicit mark_last_busy that 6.18 folds into the put):

/* --- probe: opt in, set a delay, enable runtime PM --- */
pm_runtime_set_autosuspend_delay(dev, 100);   /* 100 ms idle window before suspend */
pm_runtime_use_autosuspend(dev);              /* turn on the delay mechanism */
pm_runtime_set_active(dev);                   /* declare the hardware is on right now */
pm_runtime_enable(dev);                       /* disable_depth 1 -> 0: PM now live */
 
/* --- every hardware access --- */
ret = pm_runtime_resume_and_get(dev);         /* usage_count++ AND resume; leak-proof */
if (ret < 0)
	return ret;                           /* on failure the count was already undone */
do_some_register_io(dev);                     /* device guaranteed RPM_ACTIVE here */
pm_runtime_mark_last_busy(dev);               /* 6.12: reset the idle clock to "now" */
pm_runtime_put_autosuspend(dev);              /* usage_count--; arm autosuspend if 0 */
 
/* --- remove: tear the opt-in back down in reverse --- */
pm_runtime_disable(dev);                      /* disable_depth back up; stop the engine */
pm_runtime_dont_use_autosuspend(dev);

Trace the counter through one access: pm_runtime_resume_and_get() raises usage_count from 0 to 1 and (because the device was idle and suspended) drives rpm_resume() to bring it to RPM_ACTIVE; the register I/O runs against live hardware; pm_runtime_mark_last_busy() stamps last_busy = now; pm_runtime_put_autosuspend() lowers usage_count back to 0 and, finding it zero, calls pm_runtime_autosuspend_expiration() — which returns now + 100 ms, a future time — so an hrtimer is armed for 100 ms hence and the function returns without suspending. If another access arrives within that window, the cycle repeats and the timer is re-armed from the new last_busy; only after a full 100 ms of genuine inactivity does the timer fire, re-enter rpm_suspend(), and run ->runtime_suspend. On 6.18 the pm_runtime_mark_last_busy() line is redundant because pm_runtime_put_autosuspend() performs it, but writing it explicitly is harmless and keeps the driver buildable on 6.12.

The pairing rule falls out of this trace: usage_count starts and ends each access at the same value. Any path that gets without putting (or vice versa) breaks the invariant that the count returns to its baseline when no one is using the device — which is the only condition under which the device is ever allowed to power down.

The sysfs Surface

The runtime PM state is exposed per-device under /sys/.../power/ (sysfs.c, v6.12):

FileModeMeaning
controlRWauto (default) lets the device be runtime-managed; on forbids runtime suspend.
runtime_statusROactive, suspended, suspending, resuming, error, or unsupported.
autosuspend_delay_msRWthe delay in ms; only meaningful if the driver enabled autosuspend.
runtime_active_timeROtotal ms the device has spent RPM_ACTIVE.
runtime_suspended_timeROtotal ms spent RPM_SUSPENDED.
runtime_usageROthe current usage_count value (debugging aid).
runtime_active_kidsROthe current child_count value.

control is the user override that matters most. Writing on calls pm_runtime_forbid(), which sets runtime_auto = false, does atomic_inc(&dev->power.usage_count), and rpm_resume(dev, 0) — i.e. it pins the device active by holding a permanent reference and waking it if needed. Writing auto calls pm_runtime_allow(), which drops that reference and runs an idle check so the device can suspend again. This is exactly why power-management how-tos tell you to echo auto > /sys/.../power/control to enable a device’s power saving — out of the box, runtime PM may be effectively held off by a control value of on or by a driver that hasn’t called pm_runtime_allow().

runtime_status_show() reports error when power.runtime_error is set, and unsupported when disable_depth is non-zero (runtime PM disabled) — these two take precedence over the actual runtime_status enum. autosuspend_delay_ms returns -EIO on read or write if use_autosuspend is not set, so a sysfs write that fails with EIO usually means the driver simply never enabled autosuspend.

The Classic Get/Put Imbalance Bug

The defining runtime-PM bug is a reference-count imbalance. Because the count delta is uniform (+1 per get, −1 per put), the two failure directions are clean to reason about:

Leaked get (too many gets, or a missing put). Every error path that pm_runtime_get_sync()’d the device but bailed out without a matching put leaves usage_count permanently elevated. The device’s count never reaches zero, so it never runtime-suspends — silent battery drain with no error message. This is precisely why pm_runtime_resume_and_get() exists: it does the put for you on the resume-failure path, eliminating the most common leak site. Diagnose by reading /sys/.../power/runtime_usage: a count stuck above zero on a device nothing is using is the smoking gun.

Over-put (too many puts). Dropping the count more times than it was raised would, naively, take it negative and let the device suspend out from under an active user — corrupting state on the next access. The core guards against the visible damage: rpm_drop_usage_count() detects a negative result, immediately re-increments, and emits dev_warn(dev, "Runtime PM usage count underflow!\n"). That kernel-log warning is the canonical symptom of an extra put. pm_runtime_put_noidle() and the conditional decrement all use atomic_add_unless(..., -1, 0) specifically so a stray put cannot wedge the count negative.

The discipline that avoids both: pair every get with exactly one put on every exit path, prefer pm_runtime_resume_and_get() over pm_runtime_get_sync(), and use the scoped-guard helpers in 6.18+ where the put is automatic at scope exit.

See Also