Dynticks and NOHZ Idle
A traditional Unix kernel runs a periodic scheduling-clock tick — a timer interrupt that fires at a fixed rate,
HZtimes per second (commonly 100, 250, or 1000 Hz), on every CPU. That tick advancesjiffies, accounts CPU time, runs expired timers, and gives the scheduler a regular chance to preempt the running task. But on a CPU that is idle — sitting in the idle loop with nothing runnable — there is nothing to preempt and nothing to account, so waking itHZtimes a second is pure waste: it burns power and prevents the processor from settling into a deep low-power state. Dynamic ticks (dynticks), enabled byCONFIG_NO_HZ_IDLE, fix this by stopping the periodic tick when a CPU goes idle. On idle entry the kernel computes when the next timer is actually due and reprograms the per-CPU clock-event device for that single instant; on wake it accounts the elapsed idle time in bulk and restarts the periodic tick. A CPU in this state is said to be “dyntick-idle” or “running tickless” (no_hz.rst, v6.12). Tickless idle has been the kernel’s default tick-handling mode since the dynticks infrastructure landed in Linux 2.6.21 (April 2007) (LWN, 2007).
This note pins behaviour to Linux 6.12 LTS (released 2024-11-17) and the matching tick-sched code, per kernel.org/releases. It covers the idle case — CONFIG_NO_HZ_IDLE. The far more radical extension that stops the tick even when a CPU is busy running a single task is a separate concept; see the sibling note Full Dynticks and NOHZ_FULL.
Mental Model — Don’t Tick a CPU With Nothing To Do
The scheduling-clock tick exists for one core reason: to periodically wrest control back from a running task so the kernel can re-decide what to run, age statistics, and fire timers. That reason evaporates the moment a CPU enters its idle loop. There is no task to preempt, no vruntime to advance, no time-slice to expire. The only thing the kernel still needs from that idle CPU is to wake up when the next real event is due — the next expiring timer, the next hrtimer, an interrupt, or an inter-processor interrupt (IPI) from another CPU. Everything between now and that event is dead time, and ticking through it HZ times a second accomplishes nothing but burning energy and keeping the CPU out of deep sleep.
So the dyntick-idle bargain is simple: instead of leaving a periodic timer armed (fire every 1/HZ seconds, forever), the kernel arms a single one-shot timer for the exact instant of the next pending event, then lets the CPU sleep undisturbed until then. If the next timer is 40 milliseconds away and HZ is 1000, that replaces 40 wakeups with one.
flowchart TB subgraph PERIODIC["Periodic tick (HZ_PERIODIC)"] P0["idle"] -->|"tick"| P1["wake (do nothing)"] P1 -->|"1/HZ later"| P2["wake (do nothing)"] P2 -->|"1/HZ later"| P3["wake (do nothing)"] P3 -->|"..."| P4["real event<br/>finally arrives"] end subgraph DYNTICK["Dyntick-idle (NO_HZ_IDLE)"] D0["idle entry"] -->|"find next timer,<br/>program one-shot"| D1["deep sleep<br/>(tick stopped)"] D1 -->|"one wake"| D2["real event<br/>arrives"] end
The two regimes for an idle CPU. What it shows: under a periodic tick the CPU is dragged awake at every 1/HZ boundary to discover it still has nothing to do, until the real event finally arrives; under dyntick-idle the kernel arms a single one-shot for that real event and the CPU sleeps straight through. The insight to take: the periodic tick is wasted work on an idle CPU, and dynticks collapses a long run of empty wakeups into exactly one, which is what lets the processor reach (and stay in) a deep low-power C-state.
Mechanical Walk-through — Stopping and Restarting the Tick
The machinery lives in kernel/time/tick-sched.c. Every CPU has a per-CPU struct tick_sched (tick_cpu_sched) holding its tickless state, and the idle loop calls into this file on the way into and out of idle.
Entering idle. When the scheduler picks the idle task and the CPU begins to idle, it calls tick_nohz_idle_enter(). This is deliberately lightweight: it disables interrupts, sets the TS_FLAG_INIDLE flag on the CPU’s tick_sched, and calls tick_nohz_start_idle() to timestamp the idle entry — but it does not stop the tick yet (tick-sched.c, v6.12):
void tick_nohz_idle_enter(void)
{
struct tick_sched *ts;
lockdep_assert_irqs_enabled();
local_irq_disable();
ts = this_cpu_ptr(&tick_cpu_sched);
WARN_ON_ONCE(ts->timer_expires_base);
tick_sched_flag_set(ts, TS_FLAG_INIDLE); /* mark "in idle" */
tick_nohz_start_idle(ts); /* record idle_entrytime */
local_irq_enable();
}The actual tick stop happens slightly later, in tick_nohz_idle_stop_tick(), called once the idle governor has decided the CPU really is going to sleep (the split exists because the cpuidle governor first wants to know how long the sleep will be — see below). That function calls two collaborators: can_stop_idle_tick() to decide whether stopping is even legal, and tick_nohz_next_event() to compute when the next event is due.
Deciding whether the tick may stop. can_stop_idle_tick() is the gatekeeper. The most important early check is need_resched() — if a reschedule is already pending there is no point sleeping at all. It also refuses to stop if the CPU is offline, if the clock-event device cannot do one-shot mode (!tick_nohz_enabled / no TICK_ONESHOT), or if there is pending work that requires the tick.
Computing the next event. tick_nohz_next_event() is where the one-shot deadline is calculated. It reads the current jiffies-based time as basemono/basejiff, then asks: is anything pending that forces the tick to keep running? Specifically:
if (rcu_needs_cpu() || arch_needs_cpu() ||
irq_work_needs_cpu() || local_timer_softirq_pending()) {
next_tick = basemono + TICK_NSEC; /* keep ticking one more period */
} else {
next_tick = get_next_timer_interrupt(basejiff, basemono);
ts->next_timer = next_tick;
}If Read-Copy-Update (RCU) still needs this CPU (it has callbacks to process, so it must keep reporting quiescent states), or an architecture hook needs it, or irq_work is queued, or a timer softirq is already pending, the function keeps the tick alive for one more period rather than risk sleeping through required work. Otherwise it calls get_next_timer_interrupt(), which walks the timer wheel (and, with high-resolution timers disabled, the next hrtimer) to find the absolute time of the next pending timer (tick-sched.c, v6.12). That instant — clamped so it is never before basemono and never further out than the timekeeping subsystem’s max_deferment if this CPU owns the timekeeping duty — becomes ts->timer_expires, the deadline the one-shot will be armed for. If the next timer is within the next single tick period, the kernel decides there is no benefit in stopping and bails (no point reprogramming the hardware to fire essentially now).
Actually stopping. tick_nohz_stop_tick() consumes that deadline. It marks the timer base idle (timer_base_try_to_set_idle()) so a newly enqueued timer on another CPU will know to kick this one, hands off the do_timer() duty if this CPU was the one updating jiffies (tick_do_timer_cpu → TICK_DO_TIMER_NONE, remembering it via TS_FLAG_DO_TIMER_LAST), sets TS_FLAG_STOPPED, and finally reprograms the clock-event device for the single computed instant:
if (tick_sched_flag_test(ts, TS_FLAG_HIGHRES)) {
hrtimer_start(&ts->sched_timer, expires, HRTIMER_MODE_ABS_PINNED_HARD);
} else {
hrtimer_set_expires(&ts->sched_timer, expires);
tick_program_event(expires, 1);
}If the next event is KTIME_MAX (nothing pending at all), it simply cancels the timer outright (tick_sched_timer_cancel()) — the CPU will sleep until an external interrupt or IPI wakes it. The periodic tick is now gone; the CPU can drop into a deep idle state.
The do_timer / timekeeping handoff. One subtlety the code handles carefully: exactly one CPU at a time owns the job of advancing global jiffies_64 and wall-clock time (tick_do_timer_cpu). If the CPU that owns it goes tickless, it must relinquish the duty, or jiffies would freeze. tick_nohz_stop_tick() therefore sets tick_do_timer_cpu to TICK_DO_TIMER_NONE; whichever CPU next runs its tick timer will pick the duty back up. (Under NO_HZ_IDLE this is safe precisely because if all CPUs are idle there are no userspace processes watching the clock, so a brief jiffies stall is invisible — a point the documentation makes explicitly, and a distinction that matters greatly for the full-dynticks case.)
Waking up. When an event finally fires (the one-shot, an interrupt, or an IPI), the CPU leaves idle and the idle loop calls tick_nohz_idle_exit(). This clears TS_FLAG_INIDLE, and if the tick was stopped, calls tick_nohz_idle_update_tick() → tick_nohz_restart_sched_tick() to re-arm the periodic tick, followed by tick_nohz_account_idle_time() to fix up the books — described next.
Catching Up Jiffies and Idle Accounting on Wake
While the tick was stopped, jiffies did not advance on this CPU and no per-tick idle accounting happened. Both must be reconciled in bulk at wake time, because the periodic mechanism that normally does them one tick at a time was off.
Jiffies catch-up happens in tick_do_update_jiffies64(). Rather than incrementing by one, it computes how many tick periods elapsed since the last update and advances jiffies_64 by that whole count at once:
delta = ktime_sub(now, tick_next_period);
if (unlikely(delta >= TICK_NSEC)) {
/* Slow path for long idle sleep times */
s64 incr = TICK_NSEC;
ticks += ktime_divns(delta, incr); /* how many ticks were skipped */
last_jiffies_update = ktime_add_ns(last_jiffies_update, incr * ticks);
}
...
jiffies_64 += ticks; /* bulk advance */So if the CPU slept 40 ms with a 1 ms tick, ticks becomes ~40 and jiffies jumps by 40 in a single locked update under jiffies_lock/jiffies_seq (the seqcount that lets clock_gettime readers run lock-free). This is the “caught up in bulk on wake” behaviour: the kernel never lost track of time, it just deferred the bookkeeping.
Idle-time accounting is similarly batched. tick_nohz_account_idle_time() computes ticks = jiffies - ts->idle_jiffies and calls account_idle_ticks(ticks) to charge the whole sleep to idle time at once (skipped entirely when full virtual-time accounting vtime is in effect, which accounts continuously):
ticks = jiffies - ts->idle_jiffies;
if (ticks && ticks < LONG_MAX)
account_idle_ticks(ticks);Without this, update_process_times() — which does exactly one tick’s worth of accounting — would undercount the idle period by everything it slept through, and tools like top would show wildly wrong idle percentages.
The cpuidle Coupling — How Long Will We Sleep?
Dynticks does not just save power passively; it actively informs the cpuidle governor. Before committing to a sleep, the governor needs to know how long the CPU is expected to be idle, because deeper C-states have higher entry/exit latency and are only worth it for long sleeps. The function tick_nohz_get_sleep_length() answers this: it runs the same tick_nohz_next_event() calculation, also folds in the next hrtimer via hrtimer_next_event_without(), and returns the expected sleep duration (tick-sched.c, v6.12). The governor uses that figure to pick a C-state. This is why the entry path is split (tick_nohz_idle_enter() then later tick_nohz_idle_stop_tick()): the governor queries the predicted sleep length between the two, then the tick is stopped with the chosen one-shot deadline.
Configuration and Boot Control
CONFIG_NO_HZ_IDLE is one of three mutually exclusive choices in the kernel’s “Timer tick handling” menu (Kconfig, v6.12):
config NO_HZ_IDLE
bool "Idle dynticks system (tickless idle)"
select NO_HZ_COMMON
help
This option enables a tickless idle system: timer interrupts
will only trigger on an as-needed basis when the system is idle.
This is usually interesting for energy saving.
Most of the time you want to say Y here.It selects NO_HZ_COMMON, which in turn selects TICK_ONESHOT — the clock-event one-shot capability that the whole scheme depends on. The other two choices are HZ_PERIODIC (never stop the tick) and NO_HZ_FULL (the busy-CPU extension). NO_HZ_IDLE is the standard distribution default.
At runtime, dyntick-idle can be disabled with the boot parameter nohz=off; a NO_HZ_IDLE kernel boots with nohz=on by default (no_hz.rst, v6.12). You verify it is active by checking that idle CPUs are not accumulating local-timer interrupts in /proc/interrupts (the LOC row), or by tracing tick_stop events.
The History — Tickless Idle Arrived in 2.6.21
Dynticks was not always there. Very old kernels (1990s–early 2000s) ticked unconditionally. The clockevents + dynticks infrastructure was merged into mainline for Linux 2.6.21, whose merge window closed in February 2007 (the release shipped that April), after originating in the real-time tree and missing earlier targets (LWN, 2007). That first implementation was exactly the mechanism described above: “When the kernel enters idle mode… it checks the next pending timer event. If that event is further away than the next tick, the periodic tick is turned off altogether; instead, the timer is programmed to fire when the next event comes due” (LWN, 2007). The motivation was unambiguous: as that article put it, “An idle CPU can save quite a bit of power, but waking that CPU up 100 times (or more) per second will hurt those power savings considerably.” The 2.6.21 version was i386-only; x86-64 and ARM support followed in later releases. The documentation of the day already flagged the long-term ambitions — full tickless systems and eventual removal of jiffies — which is the lineage that later produced NO_HZ_FULL.
Failure Modes and Costs
Dyntick-idle is not free, and the documentation is candid about it (no_hz.rst, v6.12):
- More instructions on the idle path. Computing the next event, reprogramming the clock-event device, and the catch-up bookkeeping all add work to every idle entry/exit. For a workload that idles for only tens of microseconds at a time before becoming busy again, this overhead can exceed the power saved — the very scenario for which the docs say
HZ_PERIODICcan still be the right choice. - Expensive clock reprogramming. On many architectures, arming the one-shot is a costly hardware write. A workload that flickers in and out of idle thousands of times a second pays this repeatedly.
- Worse from-idle latency. Because deep C-states are now reachable, exit latency rises; the deeper the state cpuidle chose, the longer the wake. This is why aggressively real-time systems sometimes run
HZ_PERIODICor pinidle=pollto keep CPUs out of deep sleep, trading power for predictable wake latency.
A common misunderstanding is that NO_HZ_IDLE makes a busy CPU tickless. It does not — the instant a second runnable task appears (or even a single busy task under NO_HZ_IDLE), the periodic tick is running. Stopping the tick on a busy CPU is a wholly separate, much harder feature: Full Dynticks and NOHZ_FULL.
Another subtlety: the one-shot is reprogrammed only when a non-rescheduling interrupt arrives during idle. tick_nohz_stop_tick() notes it “can be called several times before tick_nohz_restart_sched_tick() is called. This happens when interrupts arrive which do not cause a reschedule” — so an idle CPU woken by, say, a network IRQ that does not need a reschedule will re-evaluate and re-stop the tick rather than fully restarting it.
Alternatives — The Three Tick Modes
The kernel offers exactly three tick-handling regimes, and choosing among them is a power-versus-jitter trade-off:
HZ_PERIODIC— never stop the tick. Right for workloads with frequent but very short idle gaps (tens of microseconds), where stopping/restarting costs more than it saves, and for the most latency-sensitive real-time setups that cannot tolerate variable from-idle wake latency.NO_HZ_IDLE(this note) — stop the tick when idle. The default; large power wins on laptops, phones, and densely virtualized hosts (the docs cite a mainframe running ~1,500 OS instances that “might find that half of its CPU time was consumed by unnecessary scheduling-clock interrupts” under a periodic tick).NO_HZ_FULL— stop the tick even when one task is running busy. For HPC and real-time busy-loop workloads, at substantial added cost. See Full Dynticks and NOHZ_FULL.
Production Notes
On battery-powered and virtualized systems the win is large and uncontroversial: the documentation states a HZ_PERIODIC device “would drain its battery very quickly, easily 2-3 times as fast as would the same device running a CONFIG_NO_HZ_IDLE=y kernel” (no_hz.rst, v6.12). This is why effectively every mainstream distribution ships NO_HZ_IDLE. The mainframe figure — up to half of CPU time lost to unnecessary ticks across many OS instances — is the canonical motivation for the highly-virtualized case, where hundreds of guests each ticking HZ times a second multiply into enormous aggregate waste on the host.
When debugging “why isn’t my CPU reaching deep C-states,” the usual culprits are timers that keep re-arming (a userspace process polling on a short interval, or a deferrable-timer that was not marked deferrable), which keep get_next_timer_interrupt() returning a near-future deadline and prevent any meaningful sleep. The LOC (local timer) row of /proc/interrupts on an otherwise-idle system is the first diagnostic: it should be nearly static on a healthy NO_HZ_IDLE machine.
See Also
- Full Dynticks and NOHZ_FULL — the extension that stops the tick on a busy CPU running a single task; depends on RCU offload and a housekeeping CPU
- The Scheduling-Clock Tick — what the periodic tick does when it is running (jiffies, accounting, scheduler reconsideration)
- Jiffies and HZ — the tick-counted time base that dynticks advances in bulk on wake
- Tick Broadcast and Deep Idle States — when a CPU’s local timer stops in deep C-states, a broadcast timer wakes it; the deep-idle counterpart to stopping the tick
- RCU and NOCB Offloaded Callbacks — why
rcu_needs_cpu()can keep an idle CPU ticking, and how offload removes that constraint - Linux Time and Timers MOC — parent map (section D, “The Tick and Tickless Operation”)
- Linux Process Scheduling MOC — the scheduler is the consumer the tick serves; an idle CPU has nothing for it to do