How PREEMPT_RT Redefines local_lock: From preempt_disable to Per-CPU Sleepable Locks

PREEMPT_RT refactors local_lock from “local preemption disabling” into a “per-CPU sleepable lock.” The primary goals are to shorten non-preemptible sections, improve real-time responsiveness, and preserve per-CPU data protection semantics. Keywords: PREEMPT_RT, local_lock, rt_mutex.

Technical Specification Snapshot

Parameter Description
Domain Linux real-time kernel / PREEMPT_RT
Core object local_lock
Implementation language C
Related license GPLv2 (a common Linux kernel license)
Source Adapted from a Blog园 technical article
Series size 8 articles in the PREEMPT_RT series
Related mechanisms preempt_disable, migrate_disable, spin_lock, rt_mutex
Stars Not provided in the original article
Core dependencies Linux kernel locking primitives, per-CPU data structures, PREEMPT_RT patch

PREEMPT_RT Redefines the Boundaries of Kernel Locking

In the Linux kernel, locks are more than mutual exclusion tools. They also determine whether code may sleep, whether preemption is allowed, and whether synchronization can cross CPU boundaries. One of PREEMPT_RT’s key efforts is to redraw these semantic boundaries.

The original material groups locks into three categories: sleeping locks, CPU-local locks, and spinlocks. This classification matters because local_lock sits exactly at the point where “local protection” and “real-time scheduling” conflict most directly.

Lock Semantics Differ Between Non-RT and RT Kernels

In a standard kernel, spinlock_t often implies busy-waiting and implicit preemption disabling. In PREEMPT_RT, however, regular spinlock_t and rwlock_t become sleeping locks backed by rt_mutex.

That means only raw_spinlock_t and bit spinlocks retain native spinning semantics. At the same time, local_lock is no longer just a lightweight way to “disable preemption locally.” It becomes a real synchronization primitive with actual lock behavior.

/* Non-RT kernel: protect the current CPU critical section by disabling preemption */
#ifndef CONFIG_PREEMPT_RT
#define __local_lock(lock)                 \
    do {                                    \
        preempt_disable(); /* Prevent the current task from being preempted */ \
        local_lock_acquire(this_cpu_ptr(lock)); \
    } while (0)

#define __local_unlock(lock)               \
    do {                                    \
        local_lock_release(this_cpu_ptr(lock)); \
        preempt_enable();  /* Re-enable preemption */     \
    } while (0)
#else
#define __local_lock(__lock)               \
    do {                                    \
        migrate_disable(); /* Prevent the task from migrating to another CPU */ \
        spin_lock(this_cpu_ptr((__lock))); /* Lock the current CPU's corresponding lock */ \
    } while (0)

#define __local_unlock(__lock)             \
    do {                                    \
        spin_unlock(this_cpu_ptr((__lock))); /* Release the per-CPU lock */ \
        migrate_enable(); /* Restore migration ability */ \
    } while (0)
#endif

This code directly shows the core shift in local_lock under PREEMPT_RT: from “state control” to “lock object control.”

The local_lock Implementation Change Serves Stronger Real-Time Guarantees

In a standard kernel, local_lock fundamentally depends on preempt_disable(). It does not establish a lock contention relationship that is visible across CPUs. Instead, it protects the critical section on the current CPU by ensuring that the task cannot be switched out.

This approach has low overhead, but the tradeoff is obvious: as long as the critical section is still running, the scheduler cannot preempt the current task. For real-time systems that care about bounded worst-case latency, this increases tail latency.

PREEMPT_RT Uses Sleepable Locks to Shrink Non-Preemptible Sections

The PREEMPT_RT version changes this to migrate_disable() + spin_lock(this_cpu_ptr(...)). Two signals matter here.

First, migrate_disable() only prevents task migration; it does not fully disable scheduling preemption. Second, under RT, spin_lock() ultimately maps to rt_mutex, so lock waiting becomes sleepable instead of continuous busy-waiting.

void update_local_state(struct local_lock *lock)
{
    local_lock(lock);          /* Acquire the local lock: may sleep under RT */
    /* Core critical section: safely access current CPU private data */
    // do_something_with_this_cpu_data();
    local_unlock(lock);        /* Release the lock and restore migration */
}

This pseudocode shows that the call pattern barely changes, but the execution semantics have shifted from “prevent preemption” to “allow scheduling, but protect with a lock.”

This Redesign Preserves the Consistency of the Per-CPU Data Model

The goal of local_lock is not global cross-CPU synchronization. It exists to protect per-CPU data structures. In a non-RT kernel, this works because the task remains pinned to the current execution flow. In an RT kernel, the system must additionally ensure that the task does not migrate to another CPU, which is why migrate_disable() is introduced.

This step is critical. If a task migrates while holding the lock, the local data and lock instance referenced by this_cpu_ptr() would no longer match, and the protection relationship would break.

Under RT, local_lock Behaves More Like a Constrained Per-CPU Lock

You can think of local_lock under PREEMPT_RT as “a per-CPU spinlock_t plus a migration-disable constraint.” It still primarily serves local data access, but it now carries lock semantics that can block, contend, and participate in scheduling.

As a result, developers can no longer treat it as simple syntactic sugar for preempt_disable(). In particular, in interrupt context, atomic context, or any path that must not sleep, you must revalidate whether the calling context is safe.

/* Incorrect assumption: treat local_lock as a pure preemption-disabling tool */
void wrong_usage(void)
{
    // Under PREEMPT_RT, this assumption may fail
    // because local_lock may take the sleepable lock path
}

This illustrative code highlights an important migration risk: when porting to PREEMPT_RT, the most dangerous issue is often not an API change, but lingering assumptions about the old semantics.

Developers Should Treat local_lock as a Real-Time-Friendly Semantic Adaptation Layer

From an engineering perspective, PREEMPT_RT does not mechanically “replace every lock.” Instead, it preserves interface stability where possible while changing the underlying behavior so hot kernel paths become more preemptible.

That is exactly where local_lock provides value. Upper-layer code often requires little or no rewriting, while the implementation underneath moves from preemption disabling to an rt_mutex-style lock, significantly reducing interference between long critical sections and real-time scheduling.

Additional Notes About the Image

WeChat sharing prompt AI Visual Insight: This image is an animated sharing prompt for the page. It does not show kernel structures, lock contention paths, or scheduling timelines, so it adds no direct technical insight to the local_lock implementation analysis.

FAQ

1. Why does PREEMPT_RT no longer implement local_lock with preempt_disable()?

Because preempt_disable() creates non-preemptible sections and increases latency for real-time tasks. PREEMPT_RT switches to a sleepable lock path to reduce the amount of time scheduling is blocked.

2. Can local_lock still protect per-CPU data under PREEMPT_RT?

Yes, but the mechanism changes. It uses migrate_disable() to prevent task migration, then combines that with a per-CPU lock object to preserve consistent access to local data.

3. What is the difference between local_lock and raw_spinlock_t under PREEMPT_RT?

Under RT, local_lock usually follows sleepable lock semantics and is more real-time-friendly. raw_spinlock_t preserves native spinning and atomic semantics, making it suitable for low-level paths that absolutely must not sleep.

Core Summary: This article focuses on how Linux PREEMPT_RT redesigns local_lock, explaining how it evolves from the non-RT model of “disabling preemption/interrupts” into a sleepable lock based on per-CPU spinlock_t, and analyzing the impact of that change on real-time behavior, migration control, and lock semantics.