요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
===========================
How realtime kernels differ
===========================
:Author: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
Preface
=======
With forced-threaded interrupts and sleeping spin locks, code paths that
previously caused long scheduling latencies have been made preemptible and
moved into process context. This allows the scheduler to manage them more
effectively and respond to higher-priority tasks with reduced latency.
The following chapters provide an overview of key differences between a
PREEMPT_RT kernel and a standard, non-PREEMPT_RT kernel.
Locking
=======
Spinning locks such as spinlock_t are used to provide synchronization for data
structures accessed from both interrupt context and process context. For this
reason, locking functions are also available with the _irq() or _irqsave()
suffixes, which disable interrupts before acquiring the lock. This ensures that
the lock can be safely acquired in process context when interrupts are enabled.
However, on a PREEMPT_RT system, interrupts are forced-threaded and no longer
run in hard IRQ context. As a result, there is no need to disable interrupts as
part of the locking procedure when using spinlock_t.
For low-level core components such as interrupt handling, the scheduler, or the
timer subsystem the kernel uses raw_spinlock_t. This lock type preserves
traditional semantics: it disables preemption and, when used with _irq() or
_irqsave(), also disables interrupts. This ensures proper synchronization in
critical sections that must remain non-preemptible or with interrupts disabled.
Execution context
=================
Interrupt handling in a PREEMPT_RT system is invoked in process context through
the use of threaded interrupts. Other parts of the kernel also shift their
execution into threaded context by different mechanisms. The goal is to keep
execution paths preemptible, allowing the scheduler to interrupt them when a
higher-priority task needs to run.
Below is an overview of the kernel subsystems involved in this transition to
threaded, preemptible execution.
Interrupt handling
------------------
All interrupts are forced-threaded in a PREEMPT_RT system. The exceptions are
interrupts that are requested with the IRQF_NO_THREAD, IRQF_PERCPU, or
IRQF_ONESHOT flags.
The IRQF_ONESHOT flag is used together with threaded interrupts, meaning those
registered using request_threaded_irq() and providing only a threaded handler.
Its purpose is to keep the interrupt line masked until the threaded handler has
completed.
If a primary handler is also provided in this case, it is essential that the
handler does not acquire any sleeping locks, as it will not be threaded. The
handler should be minimal and must avoid introducing delays, such as
busy-waiting on hardware registers.
Soft interrupts, bottom half handling
-------------------------------------
Soft interrupts are raised by the interrupt handler and are executed after the
handler returns. Since they run in thread context, they can be preempted by
other threads. Do not assume that softirq context runs with preemption
disabled. This means you must not rely on mechanisms like local_bh_disable() in
process context to protect per-CPU variables. Because softirq handlers are
preemptible under PREEMPT_RT, this approach does not provide reliable
synchronization.
If this kind of protection is required for performance reasons, consider using
local_lock_nested_bh(). On non-PREEMPT_RT kernels, this allows lockdep to
verify that bottom halves are disabled. On PREEMPT_RT systems, it adds the
necessary locking to ensure proper protection.
Using local_lock_nested_bh() also makes the locking scope explicit and easier
for readers and maintainers to understand.
per-CPU variables
-----------------
Protecting access to per-CPU variables solely by using preempt_disable() should
be avoided, especially if the critical section has unbounded runtime or may
call APIs that can sleep.
If using a spinlock_t is considered too costly for performance reasons,
consider using local_lock_t. On non-PREEMPT_RT configurations, this introduces
no runtime overhead when lockdep is disabled. With lockdep enabled, it verifies
that the lock is only acquired in process context and never from softirq or
hard IRQ context.
On a PREEMPT_RT kernel, local_lock_t is implemented using a per-CPU spinlock_t,
which provides safe local protection for per-CPU data while keeping the system
preemptible.
Because spinlock_t on PREEMPT_RT does not disable preemption, it cannot be used
to protect per-CPU data by relying on implicit preemption disabling. If this
inherited preemption disabling is essential and if local_lock_t cannot be used
due to performance constraints, brevity of the code, or abstraction boundaries
within an API then preempt_disable_nested() may be a suitable alternative. On
non-PREEMPT_RT kernels, it verifies with lockdep that preemption is already
disabled. On PREEMPT_RT, it explicitly disables preemption.
Timers
------
By default, an hrtimer is executed in hard interrupt context. The exception is
timers initialized with the HRTIMER_MODE_SOFT flag, which are executed in
softirq context.
On a PREEMPT_RT kernel, this behavior is reversed: hrtimers are executed in
softirq context by default, typically within the ktimersd thread. This thread
runs at the lowest real-time priority, ensuring it executes before any
SCHED_OTHER tasks but does not interfere with higher-priority real-time
threads. To explicitly request execution in hard interrupt context on
PREEMPT_RT, the timer must be marked with the HRTIMER_MODE_HARD flag.
Memory allocation
-----------------
The memory allocation APIs, such as kmalloc() and alloc_pages(), require a
gfp_t flag to indicate the allocation context. On non-PREEMPT_RT kernels, it is
necessary to use GFP_ATOMIC when allocating memory from interrupt context or
from sections where preemption is disabled. This is because the allocator must
not sleep in these contexts waiting for memory to become available.
However, this approach does not work on PREEMPT_RT kernels. The memory
allocator in PREEMPT_RT uses sleeping locks internally, which cannot be
acquired when preemption is disabled. Fortunately, this is generally not a
problem, because PREEMPT_RT moves most contexts that would traditionally run
with preemption or interrupts disabled into threaded context, where sleeping is
allowed.
What remains problematic is code that explicitly disables preemption or
interrupts. In such cases, memory allocation must be performed outside the
critical section.
This restriction also applies to memory deallocation routines such as kfree()
and free_pages(), which may also involve internal locking and must not be
called from non-preemptible contexts.
IRQ work
--------
The irq_work API provides a mechanism to schedule a callback in interrupt
context. It is designed for use in contexts where traditional scheduling is not
possible, such as from within NMI handlers or from inside the scheduler, where
using a workqueue would be unsafe.
On non-PREEMPT_RT systems, all irq_work items are executed immediately in
interrupt context. Items marked with IRQ_WORK_LAZY are deferred until the next
timer tick but are still executed in interrupt context.
On PREEMPT_RT systems, the execution model changes. Because irq_work callbacks
may acquire sleeping locks or have unbounded execution time, they are handled
in thread context by a per-CPU irq_work kernel thread. This thread runs at the
lowest real-time priority, ensuring it executes before any SCHED_OTHER tasks
but does not interfere with higher-priority real-time threads.
The exception are work items marked with IRQ_WORK_HARD_IRQ, which are still
executed in hard interrupt context. Lazy items (IRQ_WORK_LAZY) continue to be
deferred until the next timer tick and are also executed by the irq_work/
thread.
RCU callbacks
-------------
RCU callbacks are invoked by default in softirq context. Their execution is
important because, depending on the use case, they either free memory or ensure
progress in state transitions. Running these callbacks as part of the softirq
chain can lead to undesired situations, such as contention for CPU resources
with other SCHED_OTHER tasks when executed within ksoftirqd.
To avoid running callbacks in softirq context, the RCU subsystem provides a
mechanism to execute them in process context instead. This behavior can be
enabled by setting the boot command-line parameter rcutree.use_softirq=0. This
setting is enforced in kernels configured with PREEMPT_RT.
Spin until ready
================
The "spin until ready" pattern involves repeatedly checking (spinning on) the
state of a data structure until it becomes available. This pattern assumes that
preemption, soft interrupts, or interrupts are disabled. If the data structure
is marked busy, it is presumed to be in use by another CPU, and spinning should
eventually succeed as that CPU makes progress.
Some examples are hrtimer_cancel() or timer_delete_sync(). These functions
cancel timers that execute with interrupts or soft interrupts disabled. If a
thread attempts to cancel a timer and finds it active, spinning until the
callback completes is safe because the callback can only run on another CPU and
will eventually finish.
On PREEMPT_RT kernels, however, timer callbacks run in thread context. This
introduces a challenge: a higher-priority thread attempting to cancel the timer
may preempt the timer callback thread. Since the scheduler cannot migrate the
callback thread to another CPU due to affinity constraints, spinning can result
in livelock even on multiprocessor systems.
To avoid this, both the canceling and callback sides must use a handshake
mechanism that supports priority inheritance. This allows the canceling thread
to suspend until the callback completes, ensuring forward progress without
risking livelock.
In order to solve the problem at the API level, the sequence locks were extended
to allow a proper handover between the the spinning reader and the maybe
blocked writer.
Sequence locks
--------------
Sequence counters and sequential locks are documented in
Documentation/locking/seqlock.rst.
The interface has been extended to ensure proper preemption states for the
writer and spinning reader contexts. This is achieved by embedding the writer
serialization lock directly into the sequence counter type, resulting in
composite types such as seqcount_spinlock_t or seqcount_mutex_t.
These composite types allow readers to detect an ongoing write and actively
boost the writer’s priority to help it complete its update instead of spinning
and waiting for its completion.
If the plain seqcount_t is used, extra care must be taken to synchronize the
reader with the writer during updates. The writer must ensure its update is
serialized and non-preemptible relative to the reader. This cannot be achieved
using a regular spinlock_t because spinlock_t on PREEMPT_RT does not disable
preemption. In such cases, using seqcount_spinlock_t is the preferred solution.
However, if there is no spinning involved i.e., if the reader only needs to
detect whether a write has started and not serialize against it then using
seqcount_t is reasonable.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
서문
1-18SPDX 라이선스 식별자는 GPL-2.0입니다.
Realtime kernel의 차이점
저자: Sebastian Andrzej Siewior <bigeasy@linutronix.de>
서문
강제 threaded interrupt와 sleep 가능한 spin lock을 사용하면서, 과거에 긴 scheduling latency를 만들던 code path를 preempt 가능하게 바꾸고 process context로 옮겼습니다. 따라서 scheduler가 이 path를 더 효과적으로 관리하고 높은 priority task에 더 짧은 latency로 응답할 수 있습니다.
다음 장에서는 `PREEMPT_RT` kernel과 표준 non-`PREEMPT_RT` kernel 사이의 주요 차이점을 개괄합니다.
Locking 의미론
19-37Locking
`spinlock_t` 같은 spinning lock은 interrupt context와 process context 양쪽에서 접근하는 data structure를 동기화하는 데 사용합니다. 그래서 lock을 얻기 전에 interrupt를 비활성화하는 `_irq()` 또는 `_irqsave()` suffix의 locking function도 제공합니다. 이를 통해 interrupt가 활성화된 process context에서도 lock을 안전하게 얻을 수 있습니다.
하지만 `PREEMPT_RT` system에서는 interrupt를 강제로 thread화하여 더 이상 hard IRQ context에서 실행하지 않습니다. 따라서 `spinlock_t`를 사용할 때 locking 절차의 일부로 interrupt를 끌 필요가 없습니다.
Interrupt 처리, scheduler, timer subsystem 같은 low-level core component에는 kernel이 `raw_spinlock_t`를 사용합니다. 이 lock type은 전통적인 의미론을 유지하여 preemption을 끄고, `_irq()` 또는 `_irqsave()`와 함께 사용하면 interrupt도 끕니다. 이로써 non-preemptible 상태나 interrupt 비활성 상태를 유지해야 하는 critical section을 올바르게 동기화합니다.
Execution context와 interrupt 처리
38-67Execution context
`PREEMPT_RT` system의 interrupt 처리는 threaded interrupt를 통해 process context에서 호출됩니다. Kernel의 다른 부분도 여러 메커니즘으로 실행을 threaded context로 옮깁니다. 목표는 실행 path를 preempt 가능하게 유지하여 더 높은 priority task가 실행되어야 할 때 scheduler가 이를 중단할 수 있게 하는 것입니다.
아래에서는 threaded, preemptible execution으로 전환하는 데 관련된 kernel subsystem을 개괄합니다.
Interrupt 처리
`PREEMPT_RT` system의 모든 interrupt는 강제로 thread화합니다. 단, `IRQF_NO_THREAD`, `IRQF_PERCPU`, `IRQF_ONESHOT` flag로 요청한 interrupt는 예외입니다.
`IRQF_ONESHOT` flag는 `request_threaded_irq()`로 등록하고 threaded handler만 제공하는 threaded interrupt와 함께 사용합니다. 이 flag의 목적은 threaded handler가 완료될 때까지 interrupt line을 masked 상태로 유지하는 것입니다.
이 경우 primary handler도 제공한다면 그 handler는 thread화되지 않으므로 sleep 가능한 lock을 절대 얻으면 안 됩니다. Handler는 최소한으로 유지해야 하며 hardware register를 busy-wait하는 것과 같은 지연을 만들지 않아야 합니다.
Soft interrupt와 bottom half 처리
68-87Soft interrupt와 bottom half 처리
Soft interrupt는 interrupt handler가 발생시키고 handler가 반환한 뒤 실행합니다. Thread context에서 실행되므로 다른 thread가 preempt할 수 있습니다. Softirq context가 preemption disabled 상태로 실행된다고 가정해서는 안 됩니다.
따라서 process context에서 per-CPU variable을 보호하려고 `local_bh_disable()` 같은 메커니즘에 의존하면 안 됩니다. `PREEMPT_RT`에서 softirq handler는 preempt 가능하므로 이 방식은 신뢰할 수 있는 동기화를 제공하지 않습니다.
성능상 이유로 이러한 보호가 필요하면 `local_lock_nested_bh()`를 검토하십시오. Non-`PREEMPT_RT` kernel에서는 bottom half가 비활성화되었는지 lockdep이 확인할 수 있고, `PREEMPT_RT` system에서는 적절한 보호에 필요한 locking을 추가합니다.
`local_lock_nested_bh()`를 사용하면 locking scope도 명시적으로 드러나 reader와 maintainer가 이해하기 쉬워집니다.
per-CPU variable 보호
88-113per-CPU variable
Critical section의 실행 시간이 제한되지 않거나 sleep 가능한 API를 호출할 수 있다면 특히, `preempt_disable()`만으로 per-CPU variable 접근을 보호하는 방식을 피해야 합니다.
성능상 `spinlock_t`의 비용이 지나치게 크다고 판단되면 `local_lock_t`를 검토하십시오. Non-`PREEMPT_RT` configuration에서 lockdep을 끄면 runtime overhead가 없습니다. Lockdep을 켜면 이 lock을 process context에서만 얻고 softirq 또는 hard IRQ context에서는 얻지 않는지 검증합니다.
`PREEMPT_RT` kernel에서 `local_lock_t`는 per-CPU `spinlock_t`로 구현되며, system의 preempt 가능성을 유지하면서 per-CPU data에 안전한 local protection을 제공합니다.
`PREEMPT_RT`의 `spinlock_t`는 preemption을 끄지 않으므로 암묵적인 preemption 비활성화에 기대어 per-CPU data를 보호할 수 없습니다. 이 상속된 비활성화가 꼭 필요하지만 성능 제약, 짧은 code, API abstraction boundary 때문에 `local_lock_t`를 사용할 수 없다면 `preempt_disable_nested()`가 적절한 대안일 수 있습니다.
Non-`PREEMPT_RT` kernel에서는 `preempt_disable_nested()`가 preemption이 이미 꺼졌는지 lockdep으로 검증하고, `PREEMPT_RT`에서는 preemption을 명시적으로 끕니다.
Timer 실행 context
114-127Timer
기본적으로 hrtimer는 hard interrupt context에서 실행합니다. `HRTIMER_MODE_SOFT` flag로 초기화한 timer는 예외로, softirq context에서 실행합니다.
`PREEMPT_RT` kernel에서는 이 동작이 반대입니다. Hrtimer는 기본적으로 softirq context, 대개 `ktimersd` thread 안에서 실행됩니다. 이 thread는 가장 낮은 real-time priority로 실행되어 모든 `SCHED_OTHER` task보다 먼저 실행되지만 priority가 더 높은 real-time thread를 방해하지 않습니다.
`PREEMPT_RT`에서 hard interrupt context 실행을 명시적으로 요청하려면 timer에 `HRTIMER_MODE_HARD` flag를 표시해야 합니다.
Memory 할당과 해제
128-151Memory 할당
`kmalloc()`과 `alloc_pages()` 같은 memory allocation API는 allocation context를 나타내는 `gfp_t` flag를 요구합니다. Non-`PREEMPT_RT` kernel에서는 interrupt context 또는 preemption disabled section에서 memory를 할당할 때 `GFP_ATOMIC`을 사용해야 합니다. 이 context에서는 allocator가 memory를 기다리며 sleep하면 안 되기 때문입니다.
그러나 이 방식은 `PREEMPT_RT` kernel에서 동작하지 않습니다. `PREEMPT_RT`의 memory allocator는 내부적으로 sleep 가능한 lock을 사용하며, preemption이 꺼진 상태에서는 이 lock을 얻을 수 없습니다.
다행히 `PREEMPT_RT`는 전통적으로 preemption 또는 interrupt disabled 상태에서 실행하던 context 대부분을 sleep 가능한 threaded context로 옮기므로 일반적으로 문제가 되지 않습니다.
명시적으로 preemption이나 interrupt를 끄는 code는 여전히 문제가 됩니다. 이 경우 memory allocation을 critical section 밖에서 수행해야 합니다.
이 제한은 내부 locking이 개입할 수 있는 `kfree()`와 `free_pages()` 같은 memory deallocation routine에도 적용됩니다. 이러한 routine도 non-preemptible context에서 호출하면 안 됩니다.
IRQ work 실행 model
152-174IRQ work
`irq_work` API는 interrupt context에서 callback을 예약하는 메커니즘입니다. NMI handler 내부나 scheduler 안처럼 전통적인 scheduling이 불가능하고 workqueue 사용이 안전하지 않은 context를 위해 설계되었습니다.
Non-`PREEMPT_RT` system에서는 모든 `irq_work` item을 interrupt context에서 즉시 실행합니다. `IRQ_WORK_LAZY`가 표시된 item은 다음 timer tick까지 미루지만 여전히 interrupt context에서 실행합니다.
`PREEMPT_RT` system에서는 실행 model이 달라집니다. `irq_work` callback이 sleep 가능한 lock을 얻거나 제한 없이 오래 실행될 수 있으므로 per-CPU `irq_work` kernel thread의 thread context에서 처리합니다. 이 thread는 가장 낮은 real-time priority로 실행되어 모든 `SCHED_OTHER` task보다 먼저 실행되지만 더 높은 priority의 real-time thread를 방해하지 않습니다.
`IRQ_WORK_HARD_IRQ`가 표시된 work item은 예외로, 계속 hard interrupt context에서 실행합니다. Lazy item인 `IRQ_WORK_LAZY`는 계속 다음 timer tick까지 미뤄지며 `irq_work/` thread가 실행합니다.
RCU callback 실행 context
175-188RCU callback
기본적으로 RCU callback은 softirq context에서 호출됩니다. 사용 사례에 따라 memory를 해제하거나 state transition의 진행을 보장하므로 이 callback의 실행은 중요합니다. 하지만 softirq chain의 일부로 실행하면 `ksoftirqd` 안에서 다른 `SCHED_OTHER` task와 CPU resource를 두고 경쟁하는 등 바람직하지 않은 상황이 생길 수 있습니다.
RCU subsystem은 callback을 softirq context 대신 process context에서 실행하는 메커니즘을 제공합니다. Boot command-line parameter `rcutree.use_softirq=0`을 설정하면 이 동작을 활성화할 수 있으며, `PREEMPT_RT`로 구성한 kernel에서는 이 설정을 강제합니다.
Spin until ready와 priority inheritance
189-218준비될 때까지 spin하기
"spin until ready" pattern은 data structure를 사용할 수 있을 때까지 그 상태를 반복해서 확인하며 spin합니다. 이 pattern은 preemption, soft interrupt 또는 interrupt가 비활성화되었다고 가정합니다. Data structure가 busy라면 다른 CPU가 사용 중인 것으로 간주하고, 그 CPU가 진행함에 따라 spin이 결국 성공해야 합니다.
`hrtimer_cancel()`과 `timer_delete_sync()`가 예입니다. 이 function은 interrupt 또는 soft interrupt를 끈 상태에서 실행하는 timer를 취소합니다. Thread가 active timer를 취소하려 할 때 callback 완료까지 spin해도 안전한데, callback은 다른 CPU에서만 실행할 수 있고 결국 끝나기 때문입니다.
하지만 `PREEMPT_RT` kernel의 timer callback은 thread context에서 실행됩니다. Timer를 취소하려는 높은 priority thread가 timer callback thread를 preempt할 수 있습니다. Affinity 제약으로 scheduler가 callback thread를 다른 CPU로 migrate할 수 없으므로 multiprocessor system에서도 spin이 livelock을 일으킬 수 있습니다.
이를 피하려면 취소 측과 callback 측 모두 priority inheritance를 지원하는 handshake 메커니즘을 사용해야 합니다. 그러면 취소 thread가 callback 완료까지 suspend되어 livelock 없이 forward progress를 보장할 수 있습니다.
API level에서 이 문제를 해결하기 위해 sequence lock을 확장하여 spinning reader와 block될 수 있는 writer 사이에 적절한 handover가 가능하도록 했습니다.
Sequence lock 확장
219-242Sequence lock
Sequence counter와 sequential lock은 `Documentation/locking/seqlock.rst`에 설명되어 있습니다.
Writer와 spinning reader context에 올바른 preemption 상태를 보장하도록 interface를 확장했습니다. Writer serialization lock을 sequence counter type에 직접 포함하며, 그 결과 `seqcount_spinlock_t`나 `seqcount_mutex_t` 같은 composite type이 만들어졌습니다.
이 composite type을 사용하면 reader가 진행 중인 write를 감지하고 완료될 때까지 spin하는 대신 writer의 priority를 능동적으로 높여 update를 마치도록 도울 수 있습니다.
일반 `seqcount_t`를 사용한다면 update 중 reader와 writer를 동기화할 때 각별히 주의해야 합니다. Writer는 reader에 대해 update가 직렬화되고 non-preemptible임을 보장해야 합니다. `PREEMPT_RT`의 일반 `spinlock_t`는 preemption을 끄지 않으므로 이를 달성할 수 없으며, 이런 경우 `seqcount_spinlock_t`가 권장 해법입니다.
반면 spin이 전혀 없고 reader가 write 시작 여부만 감지하면 되며 write와 직렬화할 필요가 없다면 `seqcount_t`를 사용해도 합리적입니다.
요약과 해설
differences.rst:1-242`PREEMPT_RT`는 interrupt와 여러 kernel 작업을 thread context로 옮기고 `spinlock_t`를 sleep 가능하게 만들어 긴 non-preemptible 구간을 줄입니다. 반면 `raw_spinlock_t`는 전통적인 hard IRQ 의미론을 유지합니다.
Softirq와 timer, `irq_work`, RCU callback이 thread context에서 실행될 수 있으므로 preemption disabled 상태를 암묵적으로 기대하는 per-CPU 보호, memory allocation, spin-wait pattern을 다시 설계해야 합니다.
`local_lock_t`, `HRTIMER_MODE_HARD`, `IRQ_WORK_HARD_IRQ`, process-context RCU, priority inheritance가 가능한 composite sequence counter는 realtime 환경에서 latency와 forward progress를 함께 보장하는 핵심 수단입니다.