전체 흐름: API와 구현 경계
raw_spinlock_t 경로에서 이 글의 발췌는 do_raw_spin_lock()이 arch_spin_lock()에 획득을 위임하는 지점을 보여 줍니다. 아키텍처가 qspinlock을 선택하면 그 아래에서 빠른 경로와 경합 경로가 갈라집니다.
작은 화면에서는 그림을 좌우로 이동해 읽습니다.
spinlock_t를 쓰는 일반 API는 CONFIG_PREEMPT_RT=n일 때 raw spinlock으로 매핑됩니다. PREEMPT_RT의 일반 spinlock_t는 rt_mutex 기반이므로 이 qspinlock 경로로 일반화할 수 없습니다. 고정 버전 spinlock.h · 커널 lock type 문서.
qspinlock을 쓰는 아키텍처에서 queued_spin_lock()은 0에서 locked 값으로 바꾸는 acquire 비교 교환이 성공하면 곧바로 반환합니다.
비교 교환이 실패한 경우에만 queued_spin_lock_slowpath()로 갑니다. 이 경로도 pending, queue, 가상화, 노드 부족 등으로 나뉩니다.
소스 코드 위치
먼저 확인할 Linux v6.6 소스 파일
Linux v6.6: kernel/locking/spinlock.c
Linux v6.6: kernel/locking/qspinlock.c
Linux v6.6: include/linux/spinlock.h
설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
대표 코드
do_raw_spin_lock
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
mmiowb_spin_lock();
}
이 발췌는 CONFIG_DEBUG_SPINLOCK이 꺼진 inline 경로다. 먼저 arch_spin_lock()으로 실제 잠금을 획득하고, 그 다음 mmiowb_spin_lock()으로 MMIO 쓰기 순서 추적을 시작한다. DEBUG_SPINLOCK 경로의 별도 구현이나 PREEMPT_RT의 일반 spinlock_t와 혼동하지 않는다.
queued_spin_lock_slowpath
queued_spin_lock_slowpath 함수 전체 (251줄)
void __lockfunc queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
{
struct mcs_spinlock *prev, *next, *node;
u32 old, tail;
int idx;
BUILD_BUG_ON(CONFIG_NR_CPUS >= (1U << _Q_TAIL_CPU_BITS));
if (pv_enabled())
goto pv_queue;
if (virt_spin_lock(lock))
return;
/*
* Wait for in-progress pending->locked hand-overs with a bounded
* number of spins so that we guarantee forward progress.
*
* 0,1,0 -> 0,0,1
*/
if (val == _Q_PENDING_VAL) {
int cnt = _Q_PENDING_LOOPS;
val = atomic_cond_read_relaxed(&lock->val,
(VAL != _Q_PENDING_VAL) || !cnt--);
}
/*
* If we observe any contention; queue.
*/
if (val & ~_Q_LOCKED_MASK)
goto queue;
/*
* trylock || pending
*
* 0,0,* -> 0,1,* -> 0,0,1 pending, trylock
*/
val = queued_fetch_set_pending_acquire(lock);
/*
* If we observe contention, there is a concurrent locker.
*
* Undo and queue; our setting of PENDING might have made the
* n,0,0 -> 0,0,0 transition fail and it will now be waiting
* on @next to become !NULL.
*/
if (unlikely(val & ~_Q_LOCKED_MASK)) {
/* Undo PENDING if we set it. */
if (!(val & _Q_PENDING_MASK))
clear_pending(lock);
goto queue;
}
/*
* We're pending, wait for the owner to go away.
*
* 0,1,1 -> *,1,0
*
* this wait loop must be a load-acquire such that we match the
* store-release that clears the locked bit and create lock
* sequentiality; this is because not all
* clear_pending_set_locked() implementations imply full
* barriers.
*/
if (val & _Q_LOCKED_MASK)
smp_cond_load_acquire(&lock->locked, !VAL);
/*
* take ownership and clear the pending bit.
*
* 0,1,0 -> 0,0,1
*/
clear_pending_set_locked(lock);
lockevent_inc(lock_pending);
return;
/*
* End of pending bit optimistic spinning and beginning of MCS
* queuing.
*/
queue:
lockevent_inc(lock_slowpath);
pv_queue:
node = this_cpu_ptr(&qnodes[0].mcs);
idx = node->count++;
tail = encode_tail(smp_processor_id(), idx);
trace_contention_begin(lock, LCB_F_SPIN);
/*
* 4 nodes are allocated based on the assumption that there will
* not be nested NMIs taking spinlocks. That may not be true in
* some architectures even though the chance of needing more than
* 4 nodes will still be extremely unlikely. When that happens,
* we fall back to spinning on the lock directly without using
* any MCS node. This is not the most elegant solution, but is
* simple enough.
*/
if (unlikely(idx >= MAX_NODES)) {
lockevent_inc(lock_no_node);
while (!queued_spin_trylock(lock))
cpu_relax();
goto release;
}
node = grab_mcs_node(node, idx);
/*
* Keep counts of non-zero index values:
*/
lockevent_cond_inc(lock_use_node2 + idx - 1, idx);
/*
* Ensure that we increment the head node->count before initialising
* the actual node. If the compiler is kind enough to reorder these
* stores, then an IRQ could overwrite our assignments.
*/
barrier();
node->locked = 0;
node->next = NULL;
pv_init_node(node);
/*
* We touched a (possibly) cold cacheline in the per-cpu queue node;
* attempt the trylock once more in the hope someone let go while we
* weren't watching.
*/
if (queued_spin_trylock(lock))
goto release;
/*
* Ensure that the initialisation of @node is complete before we
* publish the updated tail via xchg_tail() and potentially link
* @node into the waitqueue via WRITE_ONCE(prev->next, node) below.
*/
smp_wmb();
/*
* Publish the updated tail.
* We have already touched the queueing cacheline; don't bother with
* pending stuff.
*
* p,*,* -> n,*,*
*/
old = xchg_tail(lock, tail);
next = NULL;
/*
* if there was a previous node; link it and wait until reaching the
* head of the waitqueue.
*/
if (old & _Q_TAIL_MASK) {
prev = decode_tail(old);
/* Link @node into the waitqueue. */
WRITE_ONCE(prev->next, node);
pv_wait_node(node, prev);
arch_mcs_spin_lock_contended(&node->locked);
/*
* While waiting for the MCS lock, the next pointer may have
* been set by another lock waiter. We optimistically load
* the next pointer & prefetch the cacheline for writing
* to reduce latency in the upcoming MCS unlock operation.
*/
next = READ_ONCE(node->next);
if (next)
prefetchw(next);
}
/*
* we're at the head of the waitqueue, wait for the owner & pending to
* go away.
*
* *,x,y -> *,0,0
*
* this wait loop must use a load-acquire such that we match the
* store-release that clears the locked bit and create lock
* sequentiality; this is because the set_locked() function below
* does not imply a full barrier.
*
* The PV pv_wait_head_or_lock function, if active, will acquire
* the lock and return a non-zero value. So we have to skip the
* atomic_cond_read_acquire() call. As the next PV queue head hasn't
* been designated yet, there is no way for the locked value to become
* _Q_SLOW_VAL. So both the set_locked() and the
* atomic_cmpxchg_relaxed() calls will be safe.
*
* If PV isn't active, 0 will be returned instead.
*
*/
if ((val = pv_wait_head_or_lock(lock, node)))
goto locked;
val = atomic_cond_read_acquire(&lock->val, !(VAL & _Q_LOCKED_PENDING_MASK));
locked:
/*
* claim the lock:
*
* n,0,0 -> 0,0,1 : lock, uncontended
* *,*,0 -> *,*,1 : lock, contended
*
* If the queue head is the only one in the queue (lock value == tail)
* and nobody is pending, clear the tail code and grab the lock.
* Otherwise, we only need to grab the lock.
*/
/*
* In the PV case we might already have _Q_LOCKED_VAL set, because
* of lock stealing; therefore we must also allow:
*
* n,0,1 -> 0,0,1
*
* Note: at this point: (val & _Q_PENDING_MASK) == 0, because of the
* above wait condition, therefore any concurrent setting of
* PENDING will make the uncontended transition fail.
*/
if ((val & _Q_TAIL_MASK) == tail) {
if (atomic_try_cmpxchg_relaxed(&lock->val, &val, _Q_LOCKED_VAL))
goto release; /* No contention */
}
/*
* Either somebody is queued behind us or _Q_PENDING_VAL got set
* which will then detect the remaining tail and queue behind us
* ensuring we'll see a @next.
*/
set_locked(lock);
/*
* contended path; wait for next if not observed yet, release.
*/
if (!next)
next = smp_cond_load_relaxed(&node->next, (VAL));
arch_mcs_spin_unlock_contended(&next->locked);
pv_kick_node(lock, next);
release:
trace_contention_end(lock, 0);
/*
* release the node
*/
__this_cpu_dec(qnodes[0].mcs.count);
}확인 사항: 이 코드에서는 반환값보다 상태 변경을 먼저 확인한다. 어느 잠금을 획득한 뒤 어떤 필드를 바꾸는지, 실패 시 어느 레이블로 분기하는지, 변경된 상태를 다음 호출자가 어떤 전제로 사용하는지 추적한다.
함수별 분석
두 발췌는 무조건 연속 호출되는 두 함수가 아닙니다. do_raw_spin_lock()은 아키텍처 구현으로 넘기고, qspinlock을 사용하는 구현에서 빠른 비교 교환이 실패해야 queued_spin_lock_slowpath()에 들어갑니다.
작은 화면에서는 그림을 좌우로 이동해 읽습니다.
val=0에서 atomic_try_cmpxchg_acquire()가 성공하면 slowpath 없이 lock을 갖습니다.
queue 경쟁이 아직 없으면 pending을 세우고 소유자의 locked 해제를 기다린 뒤 clear_pending_set_locked()로 소유권을 얻습니다.
경합을 보거나 pending 경합이 생기면 per-CPU node를 준비하고 tail을 연결합니다. queue head가 locked/pending 해제를 기다립니다.
pending과 queue는 slowpath 안의 서로 다른 경로입니다. 모든 경합이 tail을 연결하지는 않습니다. generic qspinlock 진입/해제 · 고정 소스 slowpath.
| 함수 | 입력 | 상태·효과 | 전제와 확인 |
|---|---|---|---|
do_raw_spin_lock() | raw_spinlock_t * | arch_spin_lock() 호출 뒤 mmiowb_spin_lock() | DEBUG_SPINLOCK=n 인라인 경로; 실제 arch 구현은 별도 확인 |
queued_spin_lock() | struct qspinlock * | 0→locked acquire 비교 교환 성공이면 반환 | qspinlock 채택 아키텍처의 빠른 경로 |
queued_spin_lock_slowpath() | 실패 시 관찰한 val | pending 또는 queue 경로; PV·노드 부족 예외도 존재 | 고정 소스의 분기 확인 |
do_raw_spin_lock() 자체는 qspinlock의 val을 직접 읽지 않습니다. queued_spin_lock_slowpath()는 lock 내부의 locked/pending/tail 및 per-CPU queue node를 다룹니다. preempt_count는 이 함수의 구조체 필드가 아닙니다.
v6.6 발췌에서는 노드 한도 이름이 MAX_NODES, 이전 tail을 복원하는 호출이 decode_tail(old)입니다. 노드 한도를 넘는 예외 분기는 MCS node 없이 queued_spin_trylock()으로 실제 획득을 다시 시도하고, 실패한 시도 사이에 cpu_relax()를 실행합니다. 이를 일반 queue 대기 전체로 확대하지 않습니다.
잠금 word와 별도 맥락
원문의 struct qspinlock은 packed val을 locked/pending/tail로 해석합니다. 이전 그림의 qspinlock val → preempt_count → lockdep_map → irqflags는 객체 관계도 실행 순서도 아닙니다.
작은 화면에서는 그림을 좌우로 이동해 읽습니다.
위 칸은 개념상 역할을 나눈 것입니다. 고정 bit 폭이나 실제 메모리 레이아웃을 뜻하지 않습니다. qspinlock_types.h의 _Q_PENDING_BITS와 slowpath를 함께 읽습니다.
| 대상 | 읽는 법 | 경계 |
|---|---|---|
qspinlock.val | locked, pending, tail을 겹친 32비트 잠금 word | 하나의 상태 word; bit 배치는 아키텍처 설정에 의존 |
per-CPU qnodes | 경합 시 MCS형 연결 노드 | qspinlock 자체의 필드가 아님; node 부족 시 직접 회전 예외 |
preempt_count | 호출 문맥의 선점/IRQ 계수 | qspinlock word나 queued_spin_lock_slowpath()의 필드가 아님 |
dep_map · flags | lockdep 메타데이터 · irqsave 호출자의 저장 상태 | 설정/호출자가 별도로 소유; tail→dep_map→flags 순서가 아님 |
실행 단계와 상태 변화
fastpath → queue slowpath는 순차 상태가 아닙니다. 빠른 획득 성공과 경합 시 slowpath는 대안 경로이며, slowpath 안에서도 pending과 queue가 갈라집니다. 모두 소유권을 얻은 뒤에는 호출자의 임계 구역을 실행하고 대응하는 unlock으로 해제합니다.
작은 화면에서는 그림을 좌우로 이동해 읽습니다.
빠른 acquire가 성공하면 직접 임계 구역으로 진입합니다. slowpath는 실행하지 않습니다.
경합 중 pending을 세운 waiter는 이전 owner의 release를 기다려 소유권을 넘겨받습니다.
대기열 노드는 tail에 연결하고 앞 node 또는 owner를 기다립니다. queue head가 lock을 얻은 뒤 다음 node를 깨웁니다.
위 상태 전이는 원문 함수 머리의 상태도와 분기에서 추린 대표 흐름입니다. 실제 아키텍처·PV 설정과 경합 타이밍에 따라 다른 중간 상태가 있습니다.
| 상태 | 표현 | 다음 판단 | 주의 |
|---|---|---|---|
| 비어 있음 | val == 0 | 빠른 acquire 비교 교환 가능 | 다른 CPU가 선점하면 slowpath로 분기 |
| owner + pending | locked/pending 조합 | owner의 release 관찰 후 pending→locked 인계 | tail 경합이 나타나면 pending 취소 후 queue |
| 대기열 존재 | tail에 CPU/index 부호화 | per-CPU node로 앞 waiter와 연결 | node 한도·PV 경로는 별도 처리 |
| 소유권 해제 | arch unlock의 release store | locked를 지우고 다음 waiter가 acquire로 관찰 | unlock은 객체 수명 해제나 free가 아님 |
불변 조건과 설정별 예외
한 시점의 임계 구역 owner는 하나입니다. 빠른 acquire와 pending/queue 인계는 서로 다른 코드 경로로 같은 소유권을 얻습니다.
qspinlock 빠른 경로의 비교 교환은 acquire, generic unlock은 release store입니다. 이것이 잠근 객체의 수명이나 무관한 포인터 publish 전체를 자동 보장하지는 않습니다.
raw_spinlock_t 임계 구역은 sleep할 수 없습니다. irqsave는 로컬 CPU의 IRQ 상태를 저장·복원하며 CPU 간 직렬화는 lock 자체가 담당합니다.
CONFIG_DEBUG_SPINLOCK이면 표시된 인라인 do_raw_spin_lock() 대신 별도 구현을 씁니다. PREEMPT_RT의 일반 spinlock_t도 raw qspinlock 그림으로 설명하지 않습니다.
generic qspinlock acquire/unlock · spinlock.h wrapper · lock type 문서. 이 도식은 qspinlock을 사용하는 아키텍처의 대표 경로입니다.
계측과 검증
먼저 빠른 경로의 실패인지, pending 인계인지, queue 대기인지 가설을 정합니다. 이 고정 소스의 trace_contention_begin/end는 queue 경로에 있고 빠른 획득 성공 경로에는 없습니다.
| 도구 | 볼 수 있는 것 | 한계 |
|---|---|---|
| lockdep | 잠금 순서·문맥 위반 경고 | 사용 가능한 빌드 설정이 필요; 경합 시간을 측정하지 않음 |
| perf lock | 지원되는 시스템의 잠금 경합 통계 | 커널·도구·추적 설정에 따라 기록 범위가 달라짐 |
trace_contention_begin/end | 이 발췌의 queue 경로에서 경합 구간 경계 | 성공한 빠른 경로 전체를 뜻하지 않음; trace 지원 빌드 필요 |
| KCSAN | 임계 구역 밖 데이터 경쟁 징후 | 잠금 소유권·진행 상태의 완전한 증명은 아님 |
추적 실험은 대상 빌드의 지원 여부를 확인한 뒤 별도 trace instance에서 계획합니다. 전역 current_tracer, set_ftrace_filter, 공유 trace_pipe를 바꾸던 예제는 제거했습니다. 고정 소스의 경합 trace 지점.
추가 확인 사항
- 이 보드의
arch_spin_lock()은 실제로 qspinlock을 쓰는가? - 해당 호출은
raw_spinlock_t인가,CONFIG_PREEMPT_RT에서 의미가 달라지는 일반spinlock_t인가? - 관찰한 경합은 빠른 비교 교환 실패 뒤 pending에서 끝났는가, tail을 게시하는 queue까지 갔는가?
irqsave의 로컬 IRQ 보호 범위와 lock의 CPU 간 상호 배제를 호출자가 구분하는가?
cpu_relax는 락 획득을 대신하지 않습니다
위의 v6.6 qspinlock 노드 부족 경로는 queued_spin_trylock()이 실패하는 동안 cpu_relax()를 실행하고 다시 획득을 시도합니다. 락을 얻는 것은 trylock 연산이며 relax 호출이 소유권이나 acquire 순서를 만들어 주는 것은 아닙니다. CPU를 재우는 대기 큐나 운영체제 스케줄러의 양보와도 구별합니다.
같은 v6.6의 x86 구현은 rep; nop(PAUSE), arm64 구현은 yield를 사용합니다. RISC-V 구현에는 PAUSE와 compiler barrier가 있으며, __riscv_muldiv 구성에서는 먼저 지연을 위한 DIV도 있습니다. 명령 이름만 보고 모든 아키텍처가 같은 시간만큼 멈추거나 같은 전력을 절감한다고 판단하지 않습니다.
RISC-V Zihintpause 명세는 PAUSE를 자원 소비를 줄이는 힌트로 정의하며 실제 대기량은 구현에 따라 0일 수도 있습니다. FENCE 인코딩을 사용해도 이 PAUSE 자체는 메모리 순서를 부과하지 않습니다. Compiler barrier와 하드웨어 순서 보장도 같은 의미가 아닙니다. 대기 중 반복 읽기를 모두 캐시라인 소유권 이동으로 세지 말고, 락 경쟁·상태 변경·CPU 구성별로 관측해야 합니다. 여기서는 코드 경로를 대조했으며 성능이나 전력은 측정하지 않았습니다.
English summary. A spin-wait hint neither acquires a lock nor puts the task to sleep. Linux v6.6 uses architecture-specific implementations. Distinguish compiler barriers, atomic acquisition and hardware memory ordering; do not infer a fixed delay or speedup.