요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
Owner semantics, rt_mutex와 semaphore
locktypes.rst:86-130Semaphore를 제외한 대부분의 lock은 strict owner semantics를 가집니다. 획득한 task가 직접 해제해야 하며, 다른 task가 대신 unlock하면 lock 구현과 lockdep이 추적하는 소유 관계가 깨집니다. rw_semaphore reader에는 특수한 non-owner release interface가 있지만 일반 규칙의 예외로만 사용해야 합니다.
rt_mutex는 waiter의 priority를 owner에게 상속시키는 PI mutex입니다. 높은 priority task가 낮은 priority owner의 lock을 기다릴 때 owner를 boost하여 중간 priority task가 owner를 무한히 밀어내는 상황을 막습니다. 하지만 preemption 또는 IRQ가 이미 disabled인 구간을 PI가 선점할 수는 없습니다.
Counting semaphore는 owner가 없으므로 누구의 priority를 올려야 할지 결정할 수 없습니다. PREEMPT_RT도 semaphore 구현을 PI lock으로 바꾸지 않습니다. 새 코드는 serialization에는 mutex를, event 대기에는 completion을 사용하여 두 의미를 분리하는 편이 낫습니다.
rw_semaphore의 fairness와 PREEMPT_RT
locktypes.rst:132-156rw_semaphore는 여러 reader 또는 하나의 writer를 허용합니다. 일반 kernel 구현은 writer starvation을 막도록 공정성을 제공합니다. PREEMPT_RT에서는 별도의 rt_mutex 기반 구현으로 바뀌며 priority inheritance의 방향 때문에 reader와 writer의 starvation 특성이 비대칭이 됩니다.
Writer 하나는 자신을 기다리는 reader들의 priority를 상속할 수 있습니다. 반대로 여러 reader가 동시에 owner인 상태에서 높은 priority writer 하나의 priority를 모든 reader에게 정확히 전달하는 것은 불가능합니다. 따라서 선점된 저우선순위 reader가 high-priority writer를 오래 막는 경우가 남습니다.
local_lock과 per-CPU 보호 범위
locktypes.rst:158-227local_lock은 preempt_disable()이나 local_irq_disable()로 보호하던 per-CPU critical section에 이름을 붙입니다. 일반 kernel에서는 다음과 같이 기존 primitive의 wrapper이지만, 이름과 lockdep map 덕분에 어떤 데이터가 어느 local scope로 보호되는지 검증할 수 있습니다.
| local_lock API | 일반 kernel에서의 동작 |
|---|---|
| local_lock / local_unlock | preempt_disable / preempt_enable |
| local_lock_irq / local_unlock_irq | local_irq_disable / local_irq_enable |
| local_lock_irqsave / local_unlock_irqrestore | local_irq_save / local_irq_restore |
PREEMPT_RT에서는 local_lock이 per-CPU spinlock_t로 구현됩니다. 이때 lock은 task를 해당 CPU에 고정하고 동일한 per-CPU lock의 사용자를 직렬화하지만, 일반 kernel처럼 IRQ가 실제로 disabled되었다고 가정할 수 없습니다. softirq 전용 데이터에는 local_lock_nested_bh()를 사용하여 보호 범위를 명시합니다.
서로 다른 local_lock_t 두 개는 같은 보호 범위를 만들지 않습니다. 단순히 local_irq_save() 호출 두 곳을 각각 다른 local lock으로 치환하면 PREEMPT_RT에서 공통 하위 함수를 직렬화하지 못합니다.
raw_spinlock_t와 spinlock_t
locktypes.rst:229-316raw_spinlock_t는 모든 kernel 구성에서 실제 spinning lock입니다. low-level interrupt 처리, hardware register access, scheduler와 같은 core atomic code에 한정해야 합니다. 획득하면 preemption이 disabled되므로 critical section은 짧고 절대로 sleep하지 않아야 합니다.
일반 kernel의 spinlock_t는 raw_spinlock_t와 같은 의미입니다. PREEMPT_RT의 spinlock_t는 rt_mutex 기반이며 경쟁 시 task가 잠들 수 있고 preemption을 끄지 않습니다. _irq와 _irqsave suffix도 CPU의 hard IRQ 상태를 바꾸지 않습니다. 다만 migration을 막아 task가 선점되더라도 per-CPU pointer가 다른 CPU의 데이터를 가리키지 않게 합니다.
RT spinlock 획득 중 task가 block되면 kernel은 기존 task state를 saved_state에 저장하고 내부 대기 상태를 TASK_UNINTERRUPTIBLE로 바꿉니다. 다른 wakeup이 들어오면 saved_state를 TASK_RUNNING으로 바꾸며, lock wakeup이 마지막에 saved_state를 복원합니다. 이 순서가 없으면 lock 대기 중 발생한 정상 wakeup을 잃을 수 있습니다.
PREEMPT_RT의 spinlock_t는 경쟁 시 rt_mutex처럼 잠들 수 있습니다. 따라서 lock 내부 대기에는 TASK_UNINTERRUPTIBLE을 사용하되, 호출자가 미리 설정한 TASK_INTERRUPTIBLE을 saved_state에 보존해야 합니다. Lock을 얻을 수 있게 된 wakeup이 saved_state를 복원하면 호출자 관점의 task state 계약이 유지됩니다.
Signal이나 일반 event wakeup이 lock 대기 중 들어와도 실제 state를 즉시 RUNNING으로 바꾸면 아직 lock을 얻지 못한 task를 깨울 수 있습니다. 그래서 일반 wakeup은 saved_state만 RUNNING으로 갱신합니다. 나중에 lock wakeup이 이를 복원하면서 실제 wakeup도 잃지 않고 lock 획득 순서도 지킵니다.
rwlock_t와 실시간 kernel
locktypes.rst:317-340일반 kernel의 rwlock_t는 여러 spinning reader와 하나의 writer를 허용하며 spinlock suffix 규칙을 따릅니다. PREEMPT_RT에서는 rt_mutex 기반 구현으로 바뀌므로 spinlock_t의 semantic 변화가 그대로 적용됩니다.
Reader가 여러 명이면 writer의 priority를 모든 reader에게 전달할 수 없습니다. 따라서 저우선순위 reader가 선점된 상태에서 높은 priority writer가 굶을 수 있습니다. 이 특성 때문에 read-mostly 구조라는 이유만으로 rwlock_t를 선택해서는 안 되며 RCU, seqlock, rw_semaphore와 실제 access pattern을 비교해야 합니다.
PREEMPT_RT에서 local lock을 치환할 때
locktypes.rst:342-416일반 kernel에서 local_lock_irq() 뒤에 raw_spin_lock()을 잡으면 local IRQ가 이미 disabled되어 있으므로 동작합니다. PREEMPT_RT에서 local_lock_irq()는 per-CPU spinlock_t일 뿐 IRQ를 끄지 않으므로 같은 코드는 깨집니다. 양쪽 구성에서 동일하게 동작해야 한다면 local_lock_irq() 안쪽에는 regular spin_lock()을 사용합니다.
/* 일반 kernel과 PREEMPT_RT 모두에서 허용되는 조합 */
local_lock_irq(&local_lock);
spin_lock(&lock);
/* 보호 대상 접근 */
spin_unlock(&lock);
local_unlock_irq(&local_lock);
하위 함수의 계약도 lockdep_assert_irqs_disabled()가 아니라 lockdep_assert_held(&local_lock)으로 표현해야 합니다. 필요한 조건은 IRQ flag 자체가 아니라 해당 데이터의 보호 lock을 보유했다는 사실이기 때문입니다.
per-CPU pointer와 spinlock_t의 조합
locktypes.rst:417-490PREEMPT_RT의 spin_lock()은 preemptible context가 필요합니다. get_cpu_ptr()는 암묵적으로 preemption을 disable하므로 그 뒤에 spin_lock()을 호출하면 RT kernel에서 잘못된 조합이 됩니다. 데이터가 같은 CPU에 머물기만 하면 되는 경우 migrate_disable()과 this_cpu_ptr()을 사용합니다.
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
spin_unlock(&p->lock);
migrate_enable();
migrate_disable()은 CPU 이동만 막고 같은 CPU에서 다른 task가 preempt하여 동일 데이터를 재진입하는 것은 막지 않습니다. 재진입까지 막아야 한다면 local_lock을 사용해야 합니다. 일반 kernel에서는 preemption disable로, RT에서는 per-CPU spinlock으로 그 조건을 만족시킵니다.
RT에서 raw spinlock과 bit spinlock
locktypes.rst:491-525raw_spinlock_t를 보유한 구간은 실제 atomic context입니다. PREEMPT_RT의 memory allocator는 fully preemptible하므로 raw lock 안에서 kmalloc()을 호출하는 식의 코드는 실패합니다. 반면 regular spinlock_t는 RT에서 preemption을 끄지 않으므로 같은 제한이 그대로 적용되지 않습니다.
Bit spinlock은 한 비트에 lock 상태를 저장하므로 rt_mutex 구조체로 치환할 공간이 없습니다. PREEMPT_RT에서도 raw spinning semantics가 유지됩니다. 필요한 경우 사용 지점의 conditional code로 regular spinlock_t를 별도로 두어야 합니다.
Lock 중첩 순서
locktypes.rst:528-556같은 범주의 lock은 전체 lock ordering이 순환하지 않는 한 중첩할 수 있습니다. Sleeping lock은 CPU-local 또는 spinning lock 안쪽에 들어갈 수 없습니다. 반대로 CPU-local lock과 spinning lock은 sleeping lock 안쪽에 들어갈 수 있고, 실제 spinning lock은 모든 범주 안쪽의 가장 안쪽 단계에 둘 수 있습니다.
| 바깥쪽에서 안쪽 순서 | PREEMPT_RT를 포함한 type |
|---|---|
| 1 | mutex, rt_mutex, semaphore, rw_semaphore 등 sleeping lock |
| 2 | spinlock_t, rwlock_t, local_lock |
| 3 | raw_spinlock_t, bit spinlock |
PREEMPT_RT에서 spinlock_t, rwlock_t, local_lock은 sleeping 계열 구현으로 변환되므로 raw_spinlock_t를 잡은 상태에서 획득할 수 없습니다. lockdep은 RT 여부와 관계없이 이 nesting 제약 위반을 보고합니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
.. _kernel_hacking_locktypes:
==========================
Lock types and their rules
==========================
Introduction
============
The kernel provides a variety of locking primitives which can be divided
into three categories:
- Sleeping locks
- CPU local locks
- Spinning locks
This document conceptually describes these lock types and provides rules
for their nesting, including the rules for use under PREEMPT_RT.
Lock categories
===============
Sleeping locks
--------------
Sleeping locks can only be acquired in preemptible task context.
Although implementations allow try_lock() from other contexts, it is
necessary to carefully evaluate the safety of unlock() as well as of
try_lock(). Furthermore, it is also necessary to evaluate the debugging
versions of these primitives. In short, don't acquire sleeping locks from
other contexts unless there is no other option.
Sleeping lock types:
- mutex
- rt_mutex
- semaphore
- rw_semaphore
- ww_mutex
- percpu_rw_semaphore
On PREEMPT_RT kernels, these lock types are converted to sleeping locks:
- local_lock
- spinlock_t
- rwlock_t
CPU local locks
---------------
- local_lock
On non-PREEMPT_RT kernels, local_lock functions are wrappers around
preemption and interrupt disabling primitives. Contrary to other locking
mechanisms, disabling preemption or interrupts are pure CPU local
concurrency control mechanisms and not suited for inter-CPU concurrency
control.
Spinning locks
--------------
- raw_spinlock_t
- bit spinlocks
On non-PREEMPT_RT kernels, these lock types are also spinning locks:
- spinlock_t
- rwlock_t
Spinning locks implicitly disable preemption and the lock / unlock functions
can have suffixes which apply further protections:
=================== ====================================================
_bh() Disable / enable bottom halves (soft interrupts)
_irq() Disable / enable interrupts
_irqsave/restore() Save and disable / restore interrupt disabled state
=================== ====================================================
Owner semantics
===============
The aforementioned lock types except semaphores have strict owner
semantics:
The context (task) that acquired the lock must release it.
rw_semaphores have a special interface which allows non-owner release for
readers.
rtmutex
=======
RT-mutexes are mutexes with support for priority inheritance (PI).
PI has limitations on non-PREEMPT_RT kernels due to preemption and
interrupt disabled sections.
PI clearly cannot preempt preemption-disabled or interrupt-disabled
regions of code, even on PREEMPT_RT kernels. Instead, PREEMPT_RT kernels
execute most such regions of code in preemptible task context, especially
interrupt handlers and soft interrupts. This conversion allows spinlock_t
and rwlock_t to be implemented via RT-mutexes.
semaphore
=========
semaphore is a counting semaphore implementation.
Semaphores are often used for both serialization and waiting, but new use
cases should instead use separate serialization and wait mechanisms, such
as mutexes and completions.
semaphores and PREEMPT_RT
----------------------------
PREEMPT_RT does not change the semaphore implementation because counting
semaphores have no concept of owners, thus preventing PREEMPT_RT from
providing priority inheritance for semaphores. After all, an unknown
owner cannot be boosted. As a consequence, blocking on semaphores can
result in priority inversion.
rw_semaphore
============
rw_semaphore is a multiple readers and single writer lock mechanism.
On non-PREEMPT_RT kernels the implementation is fair, thus preventing
writer starvation.
rw_semaphore complies by default with the strict owner semantics, but there
exist special-purpose interfaces that allow non-owner release for readers.
These interfaces work independent of the kernel configuration.
rw_semaphore and PREEMPT_RT
---------------------------
PREEMPT_RT kernels map rw_semaphore to a separate rt_mutex-based
implementation, thus changing the fairness:
Because an rw_semaphore writer cannot grant its priority to multiple
readers, a preempted low-priority reader will continue holding its lock,
thus starving even high-priority writers. In contrast, because readers
can grant their priority to a writer, a preempted low-priority writer will
have its priority boosted until it releases the lock, thus preventing that
writer from starving readers.
local_lock
==========
local_lock provides a named scope to critical sections which are protected
by disabling preemption or interrupts.
On non-PREEMPT_RT kernels local_lock operations map to the preemption and
interrupt disabling and enabling primitives:
=============================== ======================
local_lock(&llock) preempt_disable()
local_unlock(&llock) preempt_enable()
local_lock_irq(&llock) local_irq_disable()
local_unlock_irq(&llock) local_irq_enable()
local_lock_irqsave(&llock) local_irq_save()
local_unlock_irqrestore(&llock) local_irq_restore()
=============================== ======================
The named scope of local_lock has two advantages over the regular
primitives:
- The lock name allows static analysis and is also a clear documentation
of the protection scope while the regular primitives are scopeless and
opaque.
- If lockdep is enabled the local_lock gains a lockmap which allows to
validate the correctness of the protection. This can detect cases where
e.g. a function using preempt_disable() as protection mechanism is
invoked from interrupt or soft-interrupt context. Aside of that
lockdep_assert_held(&llock) works as with any other locking primitive.
local_lock and PREEMPT_RT
-------------------------
PREEMPT_RT kernels map local_lock to a per-CPU spinlock_t, thus changing
semantics:
- All spinlock_t changes also apply to local_lock.
local_lock usage
----------------
local_lock should be used in situations where disabling preemption or
interrupts is the appropriate form of concurrency control to protect
per-CPU data structures on a non PREEMPT_RT kernel.
local_lock is not suitable to protect against preemption or interrupts on a
PREEMPT_RT kernel due to the PREEMPT_RT specific spinlock_t semantics.
CPU local scope and bottom-half
-------------------------------
Per-CPU variables that are accessed only in softirq context should not rely on
the assumption that this context is implicitly protected due to being
non-preemptible. In a PREEMPT_RT kernel, softirq context is preemptible, and
synchronizing every bottom-half-disabled section via implicit context results
in an implicit per-CPU "big kernel lock."
A local_lock_t together with local_lock_nested_bh() and
local_unlock_nested_bh() for locking operations help to identify the locking
scope.
When lockdep is enabled, these functions verify that data structure access
occurs within softirq context.
Unlike local_lock(), local_unlock_nested_bh() does not disable preemption and
does not add overhead when used without lockdep.
On a PREEMPT_RT kernel, local_lock_t behaves as a real lock and
local_unlock_nested_bh() serializes access to the data structure, which allows
removal of serialization via local_bh_disable().
raw_spinlock_t and spinlock_t
=============================
raw_spinlock_t
--------------
raw_spinlock_t is a strict spinning lock implementation in all kernels,
including PREEMPT_RT kernels. Use raw_spinlock_t only in real critical
core code, low-level interrupt handling and places where disabling
preemption or interrupts is required, for example, to safely access
hardware state. raw_spinlock_t can sometimes also be used when the
critical section is tiny, thus avoiding RT-mutex overhead.
spinlock_t
----------
The semantics of spinlock_t change with the state of PREEMPT_RT.
On a non-PREEMPT_RT kernel spinlock_t is mapped to raw_spinlock_t and has
exactly the same semantics.
spinlock_t and PREEMPT_RT
-------------------------
On a PREEMPT_RT kernel spinlock_t is mapped to a separate implementation
based on rt_mutex which changes the semantics:
- Preemption is not disabled.
- The hard interrupt related suffixes for spin_lock / spin_unlock
operations (_irq, _irqsave / _irqrestore) do not affect the CPU's
interrupt disabled state.
- The soft interrupt related suffix (_bh()) still disables softirq
handlers.
Non-PREEMPT_RT kernels disable preemption to get this effect.
PREEMPT_RT kernels use a per-CPU lock for serialization which keeps
preemption enabled. The lock disables softirq handlers and also
prevents reentrancy due to task preemption.
PREEMPT_RT kernels preserve all other spinlock_t semantics:
- Tasks holding a spinlock_t do not migrate. Non-PREEMPT_RT kernels
avoid migration by disabling preemption. PREEMPT_RT kernels instead
disable migration, which ensures that pointers to per-CPU variables
remain valid even if the task is preempted.
- Task state is preserved across spinlock acquisition, ensuring that the
task-state rules apply to all kernel configurations. Non-PREEMPT_RT
kernels leave task state untouched. However, PREEMPT_RT must change
task state if the task blocks during acquisition. Therefore, it saves
the current task state before blocking and the corresponding lock wakeup
restores it, as shown below::
task->state = TASK_INTERRUPTIBLE
lock()
block()
task->saved_state = task->state
task->state = TASK_UNINTERRUPTIBLE
schedule()
lock wakeup
task->state = task->saved_state
Other types of wakeups would normally unconditionally set the task state
to RUNNING, but that does not work here because the task must remain
blocked until the lock becomes available. Therefore, when a non-lock
wakeup attempts to awaken a task blocked waiting for a spinlock, it
instead sets the saved state to RUNNING. Then, when the lock
acquisition completes, the lock wakeup sets the task state to the saved
state, in this case setting it to RUNNING::
task->state = TASK_INTERRUPTIBLE
lock()
block()
task->saved_state = task->state
task->state = TASK_UNINTERRUPTIBLE
schedule()
non lock wakeup
task->saved_state = TASK_RUNNING
lock wakeup
task->state = task->saved_state
This ensures that the real wakeup cannot be lost.
rwlock_t
========
rwlock_t is a multiple readers and single writer lock mechanism.
Non-PREEMPT_RT kernels implement rwlock_t as a spinning lock and the
suffix rules of spinlock_t apply accordingly. The implementation is fair,
thus preventing writer starvation.
rwlock_t and PREEMPT_RT
-----------------------
PREEMPT_RT kernels map rwlock_t to a separate rt_mutex-based
implementation, thus changing semantics:
- All the spinlock_t changes also apply to rwlock_t.
- Because an rwlock_t writer cannot grant its priority to multiple
readers, a preempted low-priority reader will continue holding its lock,
thus starving even high-priority writers. In contrast, because readers
can grant their priority to a writer, a preempted low-priority writer
will have its priority boosted until it releases the lock, thus
preventing that writer from starving readers.
PREEMPT_RT caveats
==================
local_lock on RT
----------------
The mapping of local_lock to spinlock_t on PREEMPT_RT kernels has a few
implications. For example, on a non-PREEMPT_RT kernel the following code
sequence works as expected::
local_lock_irq(&local_lock);
raw_spin_lock(&lock);
and is fully equivalent to::
raw_spin_lock_irq(&lock);
On a PREEMPT_RT kernel this code sequence breaks because local_lock_irq()
is mapped to a per-CPU spinlock_t which neither disables interrupts nor
preemption. The following code sequence works perfectly correct on both
PREEMPT_RT and non-PREEMPT_RT kernels::
local_lock_irq(&local_lock);
spin_lock(&lock);
Another caveat with local locks is that each local_lock has a specific
protection scope. So the following substitution is wrong::
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_1, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_1, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_2, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_2, flags);
}
func3()
{
lockdep_assert_irqs_disabled();
access_protected_data();
}
On a non-PREEMPT_RT kernel this works correctly, but on a PREEMPT_RT kernel
local_lock_1 and local_lock_2 are distinct and cannot serialize the callers
of func3(). Also the lockdep assert will trigger on a PREEMPT_RT kernel
because local_lock_irqsave() does not disable interrupts due to the
PREEMPT_RT-specific semantics of spinlock_t. The correct substitution is::
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func3()
{
lockdep_assert_held(&local_lock);
access_protected_data();
}
spinlock_t and rwlock_t
-----------------------
The changes in spinlock_t and rwlock_t semantics on PREEMPT_RT kernels
have a few implications. For example, on a non-PREEMPT_RT kernel the
following code sequence works as expected::
local_irq_disable();
spin_lock(&lock);
and is fully equivalent to::
spin_lock_irq(&lock);
Same applies to rwlock_t and the _irqsave() suffix variants.
On PREEMPT_RT kernel this code sequence breaks because RT-mutex requires a
fully preemptible context. Instead, use spin_lock_irq() or
spin_lock_irqsave() and their unlock counterparts. In cases where the
interrupt disabling and locking must remain separate, PREEMPT_RT offers a
local_lock mechanism. Acquiring the local_lock pins the task to a CPU,
allowing things like per-CPU interrupt disabled locks to be acquired.
However, this approach should be used only where absolutely necessary.
A typical scenario is protection of per-CPU variables in thread context::
struct foo *p = get_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
This is correct code on a non-PREEMPT_RT kernel, but on a PREEMPT_RT kernel
this breaks. The PREEMPT_RT-specific change of spinlock_t semantics does
not allow to acquire p->lock because get_cpu_ptr() implicitly disables
preemption. The following substitution works on both kernels::
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
migrate_disable() ensures that the task is pinned on the current CPU which
in turn guarantees that the per-CPU access to var1 and var2 are staying on
the same CPU while the task remains preemptible.
The migrate_disable() substitution is not valid for the following
scenario::
func()
{
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
p->val = func2();
This breaks because migrate_disable() does not protect against reentrancy from
a preempting task. A correct substitution for this case is::
func()
{
struct foo *p;
local_lock(&foo_lock);
p = this_cpu_ptr(&var1);
p->val = func2();
On a non-PREEMPT_RT kernel this protects against reentrancy by disabling
preemption. On a PREEMPT_RT kernel this is achieved by acquiring the
underlying per-CPU spinlock.
raw_spinlock_t on RT
--------------------
Acquiring a raw_spinlock_t disables preemption and possibly also
interrupts, so the critical section must avoid acquiring a regular
spinlock_t or rwlock_t, for example, the critical section must avoid
allocating memory. Thus, on a non-PREEMPT_RT kernel the following code
works perfectly::
raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
But this code fails on PREEMPT_RT kernels because the memory allocator is
fully preemptible and therefore cannot be invoked from truly atomic
contexts. However, it is perfectly fine to invoke the memory allocator
while holding normal non-raw spinlocks because they do not disable
preemption on PREEMPT_RT kernels::
spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
bit spinlocks
-------------
PREEMPT_RT cannot substitute bit spinlocks because a single bit is too
small to accommodate an RT-mutex. Therefore, the semantics of bit
spinlocks are preserved on PREEMPT_RT kernels, so that the raw_spinlock_t
caveats also apply to bit spinlocks.
Some bit spinlocks are replaced with regular spinlock_t for PREEMPT_RT
using conditional (#ifdef'ed) code changes at the usage site. In contrast,
usage-site changes are not needed for the spinlock_t substitution.
Instead, conditionals in header files and the core locking implementation
enable the compiler to do the substitution transparently.
Lock type nesting rules
=======================
The most basic rules are:
- Lock types of the same lock category (sleeping, CPU local, spinning)
can nest arbitrarily as long as they respect the general lock ordering
rules to prevent deadlocks.
- Sleeping lock types cannot nest inside CPU local and spinning lock types.
- CPU local and spinning lock types can nest inside sleeping lock types.
- Spinning lock types can nest inside all lock types
These constraints apply both in PREEMPT_RT and otherwise.
The fact that PREEMPT_RT changes the lock category of spinlock_t and
rwlock_t from spinning to sleeping and substitutes local_lock with a
per-CPU spinlock_t means that they cannot be acquired while holding a raw
spinlock. This results in the following nesting ordering:
1) Sleeping locks
2) spinlock_t, rwlock_t, local_lock
3) raw_spinlock_t and bit spinlocks
Lockdep will complain if these constraints are violated, both in
PREEMPT_RT and otherwise.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
세 lock category와 owner semantics
1-95Kernel locking primitive는 sleeping lock, CPU local lock, spinning lock의 세 category로 나눌 수 있다. 이 문서는 각 lock을 개념적으로 설명하고 PREEMPT_RT를 포함한 nesting rule을 제시한다.
| Category | Lock type | 기본 규칙 |
|---|---|---|
| Sleeping | mutex, rt_mutex, semaphore, rw_semaphore, ww_mutex, percpu_rw_semaphore | Preemptible task context에서만 획득 |
| CPU local | local_lock | Non-RT에서는 preemption/IRQ disable wrapper이며 inter-CPU serialization을 제공하지 않음 |
| Spinning | raw_spinlock_t, bit spinlock | Preemption을 암묵적으로 disable |
| Non-PREEMPT_RT에서 spinning | spinlock_t, rwlock_t | raw spinning semantics |
| PREEMPT_RT에서 sleeping으로 변환 | local_lock, spinlock_t, rwlock_t | rt_mutex 기반 또는 per-CPU sleeping lock semantics |
Sleeping lock 구현이 다른 context에서 try_lock()을 허용하더라도 try_lock뿐 아니라 unlock 안전성과 debugging version까지 검토해야 한다. 다른 선택지가 없다면 모를까 sleeping lock을 비-task context에서 획득하지 않는 것이 원칙이다.
CPU에서 preemption 또는 interrupt를 disable하는 것은 순수한 CPU-local concurrency control이다. 다른 CPU와의 동시 접근을 직렬화하지 못한다.
| Spinning lock suffix | 추가 보호 |
|---|---|
| _bh() | Bottom half, 즉 soft interrupt disable/enable |
| _irq() | Interrupt disable/enable |
| _irqsave/restore() | 기존 interrupt-disabled state 저장 후 disable / 저장 state 복원 |
Semaphore를 제외한 앞의 lock type은 strict owner semantics를 갖는다. Lock을 획득한 context 또는 task가 직접 release해야 한다. rw_semaphore에는 reader를 위해 non-owner release를 허용하는 특별 interface가 있다.
rt_mutex, semaphore와 priority inheritance
98-129RT-mutex는 priority inheritance(PI)를 지원하는 mutex다. Non-PREEMPT_RT kernel에서는 preemption-disabled와 interrupt-disabled section 때문에 PI에 한계가 있다.
PREEMPT_RT에서도 PI가 preemption-disabled 또는 interrupt-disabled code region을 preempt할 수는 없다. 대신 PREEMPT_RT는 interrupt handler와 soft interrupt를 포함한 이런 region 대부분을 preemptible task context에서 실행하도록 변환한다. 이 변환 덕분에 spinlock_t와 rwlock_t를 RT-mutex로 구현할 수 있다.
Semaphore는 counting semaphore 구현이다. Serialization과 waiting 두 목적으로 자주 쓰였지만 새 use case는 mutex와 completion처럼 serialization mechanism과 wait mechanism을 분리해야 한다.
PREEMPT_RT는 semaphore 구현을 바꾸지 않는다. Counting semaphore에는 owner 개념이 없어 priority inheritance를 제공할 수 없기 때문이다. 알 수 없는 owner의 priority는 boost할 수 없다. 따라서 semaphore에서 block하면 priority inversion이 생길 수 있다.
rw_semaphore의 fairness와 PREEMPT_RT
132-155rw_semaphore는 여러 reader 또는 한 writer를 허용하는 lock mechanism이다. Non-PREEMPT_RT 구현은 fair하므로 writer starvation을 막는다. 기본적으로 strict owner semantics를 따르지만 reader용 non-owner release special-purpose interface가 있으며 kernel configuration과 무관하게 동작한다.
PREEMPT_RT는 rw_semaphore를 별도의 rt_mutex 기반 구현으로 mapping하여 fairness를 바꾼다. rw_semaphore writer는 여러 reader에게 자기 priority를 넘겨줄 수 없다. 따라서 preempt된 low-priority reader가 lock을 계속 보유하면 high-priority writer도 starvation될 수 있다.
반대 방향에서는 reader들이 writer에게 priority를 전달할 수 있다. Preempt된 low-priority writer는 lock을 release할 때까지 priority가 boost되므로 그 writer 때문에 reader가 starvation되는 일을 막는다.
local_lock의 named CPU-local scope
158-205local_lock은 preemption 또는 interrupt disable로 보호하는 critical section에 이름 있는 scope를 제공한다. Non-PREEMPT_RT에서 operation은 다음 primitive로 mapping된다.
| local_lock API | Non-PREEMPT_RT mapping |
|---|---|
| local_lock(&llock) | preempt_disable() |
| local_unlock(&llock) | preempt_enable() |
| local_lock_irq(&llock) | local_irq_disable() |
| local_unlock_irq(&llock) | local_irq_enable() |
| local_lock_irqsave(&llock) | local_irq_save() |
| local_unlock_irqrestore(&llock) | local_irq_restore() |
이름 있는 scope에는 두 장점이 있다. Lock name은 static analysis에 사용할 수 있고 보호 범위를 분명히 문서화한다. 일반 preemption·IRQ primitive는 이름과 scope가 없어 어떤 data를 보호하는지 알기 어렵다.
Lockdep를 enable하면 local_lock에 lockmap이 생겨 보호가 올바른지 검증할 수 있다. 예를 들어 preempt_disable()로 보호하는 function이 interrupt 또는 soft-interrupt context에서 불리는 오류를 찾는다. lockdep_assert_held(&llock)도 다른 locking primitive와 같은 방식으로 사용할 수 있다.
PREEMPT_RT는 local_lock을 per-CPU spinlock_t로 mapping하므로 spinlock_t의 모든 의미 변화가 local_lock에도 적용된다. Local_lock은 non-RT에서 per-CPU data structure를 보호하기 위해 preemption 또는 interrupt disable이 적절한 경우에 사용한다. RT에서는 spinlock_t semantics 때문에 preemption이나 interrupt 자체를 막는 용도로 사용할 수 없다.
CPU-local softirq scope를 명시하는 nested_bh
207-227Softirq context에서만 접근하는 per-CPU variable이라도 그 context가 non-preemptible이라 자동 보호된다고 가정해서는 안 된다. PREEMPT_RT에서는 softirq context가 preemptible이며, bottom-half-disabled section 전체를 암묵적으로 동기화하면 per-CPU big kernel lock과 같은 결과가 된다.
local_lock_t와 local_lock_nested_bh()·local_unlock_nested_bh()를 사용하면 실제 locking scope를 식별할 수 있다. Lockdep가 켜져 있으면 data structure access가 softirq context 안에서 이루어지는지 검증한다.
local_unlock_nested_bh()는 local_lock()과 달리 preemption을 disable하지 않으며 lockdep가 없을 때 overhead를 추가하지 않는다. PREEMPT_RT에서는 local_lock_t가 real lock으로 동작하고 local_unlock_nested_bh()가 data access를 직렬화하므로 local_bh_disable()에 의한 serialization을 제거할 수 있다.
raw_spinlock_t와 spinlock_t의 기본 차이
229-276raw_spinlock_t는 PREEMPT_RT를 포함한 모든 kernel에서 strict spinning lock이다. Real critical core code, low-level interrupt handling, hardware state에 안전하게 접근하기 위해 preemption 또는 interrupt disable이 반드시 필요한 곳에서만 사용한다. Critical section이 극히 작아 RT-mutex overhead를 피하려는 경우에도 제한적으로 쓸 수 있다.
spinlock_t 의미는 PREEMPT_RT 여부에 따라 달라진다. Non-PREEMPT_RT에서는 raw_spinlock_t에 mapping되어 완전히 같은 semantics를 갖는다.
PREEMPT_RT에서는 spinlock_t가 rt_mutex 기반 별도 구현으로 mapping된다. Preemption을 disable하지 않으며 spin_lock/spin_unlock의 _irq, _irqsave/_irqrestore suffix도 CPU interrupt-disabled state를 바꾸지 않는다.
_bh suffix는 여전히 softirq handler를 disable한다. Non-RT는 preemption을 disable해 이 효과를 얻지만 PREEMPT_RT는 preemption을 유지하면서 per-CPU lock으로 serialization한다. 이 lock은 softirq handler를 disable하고 task preemption에 의한 reentrancy도 막는다.
그 밖의 spinlock_t semantics는 유지된다. Lock을 가진 task는 migrate하지 않는다. Non-RT는 preemption disable로 migration을 막고 PREEMPT_RT는 migration만 disable하여 task가 preempt되더라도 per-CPU variable pointer가 유효하도록 보장한다.
PREEMPT_RT spinlock 대기와 task state 보존
278-314Spinlock 획득 전후의 task state도 모든 kernel configuration에서 보존해야 한다. Non-PREEMPT_RT는 task state를 건드리지 않는다. 하지만 PREEMPT_RT에서는 rt_mutex 기반 spinlock 획득 중 task가 block할 수 있어 state를 바꿔야 한다. Block 전에 current state를 saved_state에 저장하고 lock wakeup이 이를 복원한다.
TASK_INTERRUPTIBLE 상태에서 lock을 기다리면 scheduler block을 위해 TASK_UNINTERRUPTIBLE로 잠시 바꾼다. Lock이 가능해지면 wakeup path가 original saved_state를 되돌린다.
일반 wakeup은 보통 task state를 무조건 RUNNING으로 바꾼다. 그러나 spinlock을 기다리며 block된 task에 그렇게 하면 lock이 가능하기 전에 실행될 수 있어 안 된다. 따라서 non-lock wakeup은 실제 state가 아니라 saved_state를 RUNNING으로 바꾼다. Lock 획득이 끝나면 lock wakeup이 saved_state를 current state에 복원한다.
일반 wakeup은 task를 즉시 runnable로 만들지 않고 saved_state에 TASK_RUNNING을 기록한다. Lock wakeup이 뒤에 이 값을 복원하므로 실제 wakeup event가 사라지지 않는다.
이 two-stage state update 덕분에 실제 wakeup event가 lock wait에 가려져 유실되지 않는다.
rwlock_t와 PREEMPT_RT fairness
317-339rwlock_t는 여러 reader 또는 한 writer를 허용한다. Non-PREEMPT_RT에서는 spinning lock으로 구현되어 spinlock_t suffix rule이 적용되고 fair implementation이 writer starvation을 막는다.
PREEMPT_RT는 rwlock_t를 별도 rt_mutex 기반 구현으로 mapping한다. Spinlock_t의 모든 변화가 rwlock_t에도 적용된다.
Writer는 여러 reader에게 priority를 전달할 수 없으므로 preempt된 low-priority reader가 lock을 계속 보유해 high-priority writer를 starvation시킬 수 있다. 반대로 reader는 writer에게 priority를 전달할 수 있어 preempt된 low-priority writer는 release할 때까지 boost되고 reader starvation을 막는다.
PREEMPT_RT에서 local_lock 변환 시 주의점
342-415Non-PREEMPT_RT에서 다음 sequence는 local_lock_irq()가 interrupt를 disable하므로 기대대로 동작하며 raw_spin_lock_irq(&lock)와 같다.
local_lock_irq(&local_lock);
raw_spin_lock(&lock);
/* non-RT에서 동등 */
raw_spin_lock_irq(&lock);
PREEMPT_RT에서는 local_lock_irq()가 interrupt나 preemption을 disable하지 않는 per-CPU spinlock_t로 mapping되어 이 sequence가 깨진다. 다음처럼 일반 spin_lock을 nesting하면 RT와 non-RT 모두 올바르다.
local_lock_irq(&local_lock);
spin_lock(&lock);
또 다른 함정은 local_lock마다 고유한 protection scope가 있다는 점이다. 두 caller가 같은 func3()의 protected data에 접근하면서 서로 다른 local_lock_1과 local_lock_2를 사용하면 RT에서 두 caller를 직렬화하지 못한다. local_lock_irqsave()가 interrupt를 실제로 disable하지 않으므로 lockdep_assert_irqs_disabled()도 fail한다.
/* 잘못된 변환: 서로 다른 scope */
func1()
{
local_lock_irqsave(&local_lock_1, flags);
func3();
local_unlock_irqrestore(&local_lock_1, flags);
}
func2()
{
local_lock_irqsave(&local_lock_2, flags);
func3();
local_unlock_irqrestore(&local_lock_2, flags);
}
func3()
{
lockdep_assert_irqs_disabled();
access_protected_data();
}
올바른 변환은 두 caller가 같은 local_lock을 사용하고 callee가 lockdep_assert_held()로 그 lock을 확인하는 것이다.
func1()
{
local_lock_irqsave(&local_lock, flags);
func3();
local_unlock_irqrestore(&local_lock, flags);
}
func2()
{
local_lock_irqsave(&local_lock, flags);
func3();
local_unlock_irqrestore(&local_lock, flags);
}
func3()
{
lockdep_assert_held(&local_lock);
access_protected_data();
}
IRQ disable, per-CPU pointer와 RT spinlock nesting
417-488Non-PREEMPT_RT에서 local_irq_disable() 뒤 spin_lock()을 호출하는 sequence는 spin_lock_irq()와 같다. rwlock_t와 _irqsave suffix도 마찬가지다.
local_irq_disable();
spin_lock(&lock);
/* non-RT에서 동등 */
spin_lock_irq(&lock);
PREEMPT_RT에서는 RT-mutex가 fully preemptible context를 요구하므로 첫 sequence가 깨진다. 대신 spin_lock_irq() 또는 spin_lock_irqsave()와 대응 unlock을 사용한다. IRQ disable과 locking을 분리해야 한다면 local_lock으로 task를 CPU에 pin한 뒤 per-CPU interrupt-disabled lock 등을 획득할 수 있지만 반드시 필요한 경우에만 사용한다.
Thread context에서 per-CPU variable을 보호하는 다음 code는 non-RT에서는 맞지만 RT에서는 get_cpu_ptr()가 암묵적으로 preemption을 disable하기 때문에 p->lock을 획득할 수 없다.
struct foo *p = get_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
두 kernel에서 모두 동작하려면 migration만 disable하고 task는 preemptible하게 유지한다. migrate_disable()은 task를 current CPU에 pin하여 var1과 var2의 per-CPU access가 같은 CPU에 남게 한다.
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
그러나 migrate_disable()은 preempting task의 reentrancy를 막지 못한다. Function이 per-CPU data를 읽은 뒤 preempt될 수 있고 다른 task가 같은 data에 다시 들어올 수 있는 경우에는 local_lock을 사용해야 한다.
func()
{
struct foo *p;
local_lock(&foo_lock);
p = this_cpu_ptr(&var1);
p->val = func2();
}
Non-RT에서는 local_lock이 preemption을 disable해 reentrancy를 막고 PREEMPT_RT에서는 underlying per-CPU spinlock을 획득해 같은 보호를 제공한다.
RT에서 raw_spinlock과 bit spinlock의 atomic 제약
491-525raw_spinlock_t를 획득하면 preemption과 경우에 따라 interrupt도 disable된다. 따라서 critical section은 일반 spinlock_t 또는 rwlock_t를 추가로 획득해서는 안 되고 memory allocation 같은 operation도 피해야 한다.
다음 code는 non-PREEMPT_RT에서는 동작하지만 fully preemptible memory allocator를 truly atomic context에서 호출할 수 없는 PREEMPT_RT에서는 실패한다.
raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
PREEMPT_RT에서 일반 non-raw spinlock은 preemption을 disable하지 않으므로 해당 lock을 가진 상태에서 allocator를 호출하는 것은 허용된다.
spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
Bit 하나에는 RT-mutex를 담을 수 없어 PREEMPT_RT가 bit spinlock을 대체할 수 없다. 따라서 bit spinlock semantics는 그대로이며 raw_spinlock_t의 주의점이 모두 적용된다.
일부 bit spinlock은 사용 위치의 #ifdef code로 PREEMPT_RT에서 일반 spinlock_t로 바꾼다. 반면 spinlock_t 자체의 대체는 사용 위치를 고칠 필요가 없다. Header와 core locking implementation의 conditional이 compiler가 투명하게 substitution하도록 만든다.
Lock category nesting 순서
528-555- 같은 category의 lock끼리는 일반 lock ordering rule을 지켜 deadlock을 막는 한 자유롭게 nesting할 수 있다.
- Sleeping lock은 CPU-local lock 또는 spinning lock 안에 nesting할 수 없다.
- CPU-local lock과 spinning lock은 sleeping lock 안에 nesting할 수 있다.
- Spinning lock은 모든 lock type 안에 nesting할 수 있다.
이 제약은 PREEMPT_RT와 non-RT 모두에 적용된다. PREEMPT_RT에서는 spinlock_t와 rwlock_t category가 spinning에서 sleeping으로 바뀌고 local_lock이 per-CPU spinlock_t가 되므로 raw spinlock을 가진 상태에서 이들을 획득할 수 없다.
| Nesting level | PREEMPT_RT에서 바깥쪽부터 안쪽 순서 |
|---|---|
| 1 | Sleeping locks |
| 2 | spinlock_t, rwlock_t, local_lock |
| 3 | raw_spinlock_t, bit spinlock |
PREEMPT_RT 여부와 관계없이 이 제약을 위반하면 lockdep가 경고한다.
세 가지 lock 범주
locktypes.rst:9-83커널 lock은 sleeping lock, CPU-local lock, spinning lock의 세 범주로 나눌 수 있습니다. 이 구분은 lock을 기다릴 때 scheduler에 CPU를 넘길 수 있는지, 현재 CPU의 실행만 통제하는지, 다른 CPU가 lock을 놓을 때까지 busy-wait하는지를 기준으로 합니다.
Spinning lock의 _bh() suffix는 softirq를, _irq()는 local hard IRQ를 막습니다. _irqsave()는 이전 IRQ disable 상태를 flags에 보존한 뒤 IRQ를 막고, unlock의 _irqrestore()가 그 상태를 복구합니다. 다른 CPU의 interrupt를 끄는 기능이 아니라 현재 CPU에서 같은 lock을 다시 요청하는 interrupt가 끼어드는 것을 막는 장치입니다.