Lazy Preemption and PREEMPT_LAZY
Lazy preemption is a scheduler-driven preemption model, merged in Linux 6.13 (December 2024) and present in the 6.18 LTS kernel (released 2025-11-30), that splits the kernel’s single “reschedule” signal into two: an eager flag,
TIF_NEED_RESCHED, meaning preempt as soon as it is safe to do so (including at in-kernel preemption points), and a lazy flag,TIF_NEED_RESCHED_LAZY, meaning preempt only at the next convenient exit to user space. The idea — pushed by Thomas Gleixner and Peter Zijlstra and described in detail by Jonathan Corbet in “The long road to lazy preemption” (2024-10-18) — is to let one fully-preemptible kernel behave well for both throughput and latency: ordinarySCHED_NORMAL(fair) tasks get the lazy flag, so a CPU hog is not yanked off the processor mid-quantum (preserving cache locality and throughput), while real-time tasks still get the eager flag and preempt almost immediately. The long-term goal is to collapse the historical preemption matrix —PREEMPT_NONE/PREEMPT_VOLUNTARY/PREEMPT_FULL— down to essentially two behaviours and to retire the hundreds of scatteredcond_resched()calls that have papered over the non-preemptible models for two decades.
Uncertain
Verify: the precise merge release is Linux 6.13 (lazy preemption was not in 6.12 LTS — confirmed:
kernel/Kconfig.preemptandinclude/linux/thread_info.hat tagv6.12contain noPREEMPT_LAZY,ARCH_HAS_PREEMPT_LAZY, or_TIF_NEED_RESCHED_LAZY, whereasv6.18contains all three). Reason: the exact merge-window attribution rests on the Phoronix 6.13 report and the LWN article series, which I read; the kernel source I verified directly only at the 6.12 (absent) and 6.18 (present) endpoints, not at 6.13 itself. To resolve: confirm against thev6.13tag ofkernel/Kconfig.preempt. uncertain
Why the old model had to change
To understand lazy preemption you have to understand what it replaces. Historically the Linux kernel offered three compile-time preemption models (a fourth, PREEMPT_RT, is its own beast — see The PREEMPT_RT Real-Time Kernel), selected as a choice in kernel/Kconfig.preempt:
PREEMPT_NONE(“No Forced Preemption (Server)”) — kernel code, once entered, runs until it voluntarily yields, blocks, or returns to user space. Best throughput, worst latency.PREEMPT_VOLUNTARY(“Voluntary Kernel Preemption (Desktop)”) — adds explicit yield points (might_resched()) so long-running kernel code checks in periodically.PREEMPT/PREEMPT_FULL(“Preemptible Kernel (Low-Latency Desktop)”) — almost all kernel code is preemptible; a higher-priority task that becomes runnable can displace a task executing inside a syscall the moment it leaves a non-preemptible (preempt_disable-bracketed or spinlock-held) section.
Since Linux 5.12, PREEMPT_DYNAMIC lets a distribution build one kernel and pick none / voluntary / full at boot via the preempt= command-line parameter (or at runtime through /sys/kernel/debug/sched/preempt), implemented with static calls / static keys so the unused paths cost nothing. That solved the packaging problem but not the underlying tension, which Corbet and the Revisiting the kernel's preemption models series (LWN 944686, 2023) lay out clearly.
The deep problem is cond_resched(). In the NONE and VOLUNTARY models, the kernel cannot preempt itself spontaneously, so to avoid a long loop monopolizing a CPU, kernel developers sprinkle explicit cond_resched() calls — “if a reschedule is pending, do it now” — across hundreds of call sites in the kernel. Gleixner has called this a “hack”: it is a manual, error-prone, ad-hoc approximation of preemptibility, and getting it wrong means a latency spike (too few calls) or wasted overhead (too many). Corbet reports Gleixner’s estimate that lazy preemption would let “less than 5%” of those cond_resched() calls survive (LWN 994322).
Meanwhile, PREEMPT_FULL solves latency beautifully but hurts throughput: a SCHED_NORMAL task that has just been told it should yield gets kicked off the CPU at the very next preemption point, even if the task it is yielding to is also just an ordinary fair task that could perfectly well wait another millisecond until the next timer tick. This causes lock-holder preemption (a task is preempted while holding a lock, stalling everyone waiting on it) and trashes caches. The insight behind lazy preemption: for fairness between equals, “reschedule eventually” is good enough; only real-time urgency demands “reschedule now.”
Mental model: two flags, two urgencies
flowchart TB subgraph SETTERS["Who requests a reschedule"] RT["RT/DL task wakes,<br/>or kernel needs<br/>immediate switch"] FAIR["Fair task should yield<br/>(tick, fairer task woke,<br/>slice exhausted)"] end RT -->|"resched_curr()"| EAGER["TIF_NEED_RESCHED<br/>(eager: preempt ASAP)"] FAIR -->|"resched_curr_lazy()"| LAZY["TIF_NEED_RESCHED_LAZY<br/>(lazy: preempt at exit-to-user)"] subgraph CHECKERS["Where the flags are read"] KPRE["In-kernel preemption points<br/>(preempt_enable,<br/>IRQ return to kernel)"] UEXIT["exit_to_user_mode_loop()<br/>(syscall / IRQ return to user)"] end EAGER --> KPRE EAGER --> UEXIT LAZY -.->|"ignored in kernel"| KPRE LAZY --> UEXIT
The two-flag scheme. What it shows: an urgent reschedule (real-time wakeup, or any path that calls resched_curr()) sets the eager TIF_NEED_RESCHED, which is honoured at every preemption check — including return from an interrupt into kernel mode and every preempt_enable(). A merely-fair reschedule (the fair class calls resched_curr_lazy()) sets the lazy TIF_NEED_RESCHED_LAZY, which in-kernel preemption points deliberately ignore; it is only consumed on the way back out to user space, where preemption is always cheap and safe. The insight to take: the same fully-preemptible kernel binary can be “eager” for the tasks that need it and “lazy” for the tasks that don’t — the urgency, not a global compile-time mode, decides how fast the switch happens.
The flags in the source
In the 6.18 tree, the generic header include/linux/thread_info.h defines the lazy flag, with a fallback for architectures that have not yet wired it up:
#ifndef TIF_NEED_RESCHED_LAZY
#ifdef CONFIG_ARCH_HAS_PREEMPT_LAZY
#error "Arch needs to define TIF_NEED_RESCHED_LAZY"
#endif
#define TIF_NEED_RESCHED_LAZY TIF_NEED_RESCHED
#define _TIF_NEED_RESCHED_LAZY _TIF_NEED_RESCHED
#endifRead this carefully: if an architecture does not opt in (CONFIG_ARCH_HAS_PREEMPT_LAZY unset), the lazy flag is #defined to be the same bit as the eager flag. On such an arch, “setting the lazy flag” silently degrades to “setting the eager flag,” and lazy preemption is a no-op — every reschedule is eager. x86 does opt in; its arch/x86/include/asm/thread_info.h declares #define HAVE_TIF_NEED_RESCHED_LAZY and allocates a distinct TIF_NEED_RESCHED_LAZY thread-info bit. (At tag v6.12, none of this exists: thread_info.h has only TIF_NEED_RESCHED, confirming lazy preemption post-dates 6.12.)
The two request paths live in kernel/sched/core.c. The common worker is __resched_curr(rq, tif), which takes the bit to set as an argument:
static void __resched_curr(struct rq *rq, int tif)
{
struct task_struct *curr = rq->curr;
struct thread_info *cti = task_thread_info(curr);
int cpu;
lockdep_assert_rq_held(rq);
/*
* Always immediately preempt the idle task; no point in delaying doing
* actual work.
*/
if (is_idle_task(curr) && tif == TIF_NEED_RESCHED_LAZY)
tif = TIF_NEED_RESCHED;
if (cti->flags & ((1 << tif) | _TIF_NEED_RESCHED))
return;
...
if (cpu == smp_processor_id()) {
set_ti_thread_flag(cti, tif);
if (tif == TIF_NEED_RESCHED)
set_preempt_need_resched();
return;
}
if (set_nr_and_not_polling(cti, tif)) {
if (tif == TIF_NEED_RESCHED)
smp_send_reschedule(cpu); /* cross-CPU IPI */
}
...
}Walking the logic: the idle task is special — there is no point delaying a switch off idle, so a lazy request against the idle task is upgraded to eager. Next, if the eager bit is already set, there is nothing to add (eager dominates). Crucially, only the eager path calls set_preempt_need_resched() (which folds the flag into the per-CPU preempt_count so that preempt_enable() notices it) and only the eager path fires a cross-CPU rescheduling IPI (smp_send_reschedule). A lazy reschedule on a remote CPU sets the flag but sends no IPI — the remote CPU will notice the flag the next time it returns to user space anyway, so an interrupt would be wasted work. This is the whole point: laziness saves IPIs and avoids mid-quantum preemption.
The two public entry points:
void resched_curr(struct rq *rq) { __resched_curr(rq, TIF_NEED_RESCHED); }
void resched_curr_lazy(struct rq *rq) { __resched_curr(rq, get_lazy_tif_bit()); }get_lazy_tif_bit() returns TIF_NEED_RESCHED_LAZY only when lazy mode is actually active (dynamic_preempt_lazy() true); otherwise it returns TIF_NEED_RESCHED, so callers of resched_curr_lazy() transparently fall back to eager behaviour in none/voluntary/full modes.
Who calls which
The fair class (kernel/sched/fair.c) is the principal user of the lazy path. When the periodic tick decides the running fair task has used its slice, or a newly-woken fair task should preempt the current one, the fair class calls resched_curr_lazy() (see entity_tick → and wakeup_preempt → resched_curr_lazy(rq)). By contrast, when something urgent happens — a real-time or deadline task becomes runnable, a CPU-stop/migration request arrives — the code calls plain resched_curr() and the eager bit is set. So the division falls out naturally along the class boundary: fair-vs-fair is lazy; anything-higher-vs-fair is eager.
Where the flags are consumed: the exit-to-user loop
The lazy flag’s “convenient exit point” is the kernel’s generic exit-to-user machinery in include/linux/irq-entry-common.h. The work mask checked on every return to user space lists both flags:
#define EXIT_TO_USER_MODE_WORK \
(_TIF_SIGPENDING | _TIF_NOTIFY_RESUME | _TIF_UPROBE | \
_TIF_NEED_RESCHED | _TIF_NEED_RESCHED_LAZY | \
_TIF_PATCH_PENDING | _TIF_NOTIFY_SIGNAL | \
ARCH_EXIT_TO_USER_MODE_WORK)exit_to_user_mode_prepare() reads the thread flags and, if any of these bits are set, runs exit_to_user_mode_loop(), which calls schedule() for either resched flag. That is why the lazy flag always gets serviced at user-space boundaries — every syscall return and every interrupt that returns to user mode passes through here. What it does not do is appear in the in-kernel preemption path: a return from interrupt back into kernel mode, or a preempt_enable(), checks only the eager TIF_NEED_RESCHED (folded into preempt_count). A lazy-flagged fair task therefore keeps running through kernel code, lock sections, and tick interrupts until it naturally heads back to user space — bounded, in the worst case, by the next timer tick, at which point the scheduler can escalate (see below).
Relationship to PREEMPT_DYNAMIC: lazy as a fourth dynamic mode
Lazy preemption was wired into PREEMPT_DYNAMIC as a new boot-/run-time mode alongside none, voluntary, and full. In kernel/sched/core.c (6.18) the enum gains a member:
enum {
preempt_dynamic_undefined = -1,
preempt_dynamic_none,
preempt_dynamic_voluntary,
preempt_dynamic_full,
preempt_dynamic_lazy, /* new */
};The in-tree comment block documents exactly what each mode toggles. For lazy:
LAZY:
cond_resched <- RET0 (compiled out, like FULL)
might_resched <- RET0
preempt_schedule <- preempt_schedule
preempt_schedule_notrace <- preempt_schedule_notrace
irqentry_exit_cond_resched <- irqentry_exit_cond_resched
dynamic_preempt_lazy <- true (the distinguishing switch)
Compare with full: the only difference is that lazy flips the dynamic_preempt_lazy static key on. In other words, lazy mode is full preemption plus the static-key that makes the fair class set the lazy bit instead of the eager bit. Both compile out cond_resched() (it becomes RET0, a no-op returning 0), because in a fully-preemptible kernel those manual yield points are unnecessary — the scheduler can preempt on its own. This is the mechanical realization of “retire cond_resched().”
You can select it at boot with preempt=lazy (where the architecture supports it; sched_dynamic_mode() only accepts "lazy" under CONFIG_ARCH_HAS_PREEMPT_LAZY). Note also that the 6.18 Kconfig.preempt makes PREEMPT_NONE and PREEMPT_VOLUNTARY depend on !PREEMPT_RT, and sched_dynamic_mode() refuses none/voluntary under CONFIG_PREEMPT_RT — a real-time kernel is always at least fully preemptible.
Uncertain
Verify: whether
PREEMPT_LAZYis the default preemption model in any mainline-shipped or distribution 6.18 build, and the long-term plan to deletePREEMPT_NONE/PREEMPT_VOLUNTARYentirely. Reason: the 6.18Kconfig.preemptchoicestill listsdefault PREEMPT_NONEand all four models remain selectable; the “collapse to two models” is a stated direction (Gleixner/Corbet) not a completed removal as of 6.18. To resolve: track the preempt-modelchoiceblock across future releases. uncertain
How a lazy request escalates to eager
A natural worry: if a fair task is flagged lazy and just keeps grinding through kernel code, does it ever get preempted before it voluntarily exits to user space? Yes — via the timer tick. The scheduler’s tick handler checks whether the lazy flag has been pending and, on the next tick, escalates. In kernel/sched/core.c the tick path contains:
if (dynamic_preempt_lazy() && tif_test_bit(TIF_NEED_RESCHED_LAZY))
resched_curr(rq);So a lazy reschedule that was set during one tick interval is upgraded to an eager one at the following tick if the task is still running. This bounds the extra latency a fair task can impose on its peers to roughly one tick period (1/CONFIG_HZ, e.g. 1 ms at HZ=1000) — exactly the “preempt at the next convenient point, but no later than a tick” behaviour the model promises. The lineage here traces back to the old “PREEMPT_AUTO” proposal (LWN 961940), which was renamed and reshaped into PREEMPT_LAZY.
Common misunderstandings
- “Lazy preemption makes the kernel less preemptible.” No — a lazy kernel is built
PREEMPT_FULL-style (fully preemptible); lazy preemption only changes which flag fair-class reschedules use, deferring fair-vs-fair switches. Real-time latency is unaffected because RT/DL wakeups still use the eager flag. - “It’s just
PREEMPT_VOLUNTARYrenamed.” No. Voluntary preemption relies on explicitcond_resched()points and cannot preempt arbitrary kernel code; lazy mode compilescond_resched()out and relies on full preemptibility plus the tick to bound latency. - “The lazy flag is checked everywhere the eager one is.” No — that is the entire mechanism. The lazy flag is only evaluated at exit-to-user (and escalated at the next tick); in-kernel preemption points evaluate only the eager flag, folded into
preempt_count. - “It shipped in 6.12 LTS.” No. It is absent from the
v6.12source and merged in 6.13; it is present in 6.18 LTS.
See Also
- Kernel Preemption Models — the
none/voluntary/full/RTchoicethat lazy preemption sits alongside and aims to collapse;PREEMPT_DYNAMIClives there - The need_resched Flag and Preemption Points — the eager
TIF_NEED_RESCHEDflag,set_preempt_need_resched(), and the in-kernel preemption points that consume it - The PREEMPT_RT Real-Time Kernel — the realtime model; under RT, fair tasks still get lazy treatment but RT tasks always get the eager flag
- The EEVDF Scheduler — the fair class that calls
resched_curr_lazy()when a fair task should yield - Real-Time Scheduling SCHED_FIFO and SCHED_RR — the RT class whose wakeups set the eager flag
- Context Switch Mechanics switch_to and switch_mm — what actually happens once a reschedule flag is honoured
- Linux Process Scheduling MOC — parent map