Spinlock (스핀락)

커널의 가장 기본적인 busy-wait 동기화 프리미티브인 spinlock을 심층 분석합니다. Test-and-Set에서 ticket lock, MCS lock을 거쳐 현재의 qspinlock에 이르는 진화 과정을 추적하고, 32비트 상태 인코딩·queued_spin_lock_slowpath 3단계·PV qspinlock·CNA NUMA 확장의 내부 구현을 커널 소스 기반으로 분석합니다. x86/ARM64/RISC-V 아키텍처별 원자적(Atomic) 명령어 차이, PREEMPT_RT에서의 spinlock_t→rt_mutex 변환, lock contention 프로파일링(Profiling)까지 포괄합니다.

전제 조건: 동기화 기법, Atomic 연산, 메모리 배리어(Memory Barrier) 문서를 먼저 읽으세요. spinlock은 원자적 연산(Atomic Operation)과 메모리 순서 보장(Ordering) 위에 구축되므로, 이들의 기본 개념을 먼저 이해해야 합니다.
일상 비유: spinlock은 회전문(turnstile)과 같습니다. 한 번에 한 사람만 통과할 수 있고, 나머지는 문 앞에서 제자리 걸음(busy-wait)을 합니다. ticket lock은 번호표 발급기를 추가한 것이고, qspinlock은 대기자마다 개인 화면에 "차례입니다" 신호를 보내는 스마트 대기열입니다.

핵심 요약

  • Busy-wait 잠금(Lock) — 락을 얻을 수 없으면 CPU에서 루프를 돌며 대기합니다. 슬립(Sleep)이 불가능한 인터럽트(Interrupt) 컨텍스트에서 유일한 선택입니다.
  • 3세대 진화 — Test-and-Set(불공정, O(N²)) → Ticket(FIFO, O(N)) → qspinlock/MCS(로컬 스피닝, O(1)).
  • 32비트 압축 — qspinlock은 locked·pending·tail을 32비트 워드 하나에 인코딩하여 fast/pending/MCS 3단계를 자동 선택합니다.
  • PREEMPT_RT 분리spinlock_t는 RT에서 sleeping lock으로 변환되고, raw_spinlock_t만 진정한 busy-wait을 유지합니다.
  • 아키텍처 고유 — x86 LOCK CMPXCHG+PAUSE, ARM64 LDAXR/STXR+WFE/SEV, RISC-V LR/SC+AMO로 각각 구현됩니다.

단계별 이해

  1. 상호 배제(Mutual Exclusion) 기초 이해
    임계 영역(critical section)과 레이스 조건의 관계를 정확히 파악합니다.
  2. 원자적 연산 매커니즘 파악
    CAS(Compare-And-Swap), TAS(Test-And-Set), LL/SC(Load-Linked/Store-Conditional) 등 하드웨어 원자적 명령어를 이해합니다.
  3. 세대별 구현 비교
    각 세대의 문제점이 다음 세대 설계를 어떻게 이끌었는지 추적합니다.
  4. qspinlock 상태 머신 추적
    32비트 워드의 비트 필드 변화를 따라가며 fast/pending/MCS 경로를 이해합니다.
  5. 실전 API와 디버깅(Debugging) 적용
    올바른 API 선택, lock contention 분석, lockdep 활용법을 익힙니다.
관련 표준: Anderson, T. E. "The Performance of Spin Lock Alternatives for Shared-Memory Multiprocessors" (1990) — 초기 스핀락(Spinlock) 성능 비교의 기초. Mellor-Crummey, J. M. & Scott, M. L. "Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors" (1991) — MCS lock 원논문. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

이론적 배경: 상호 배제와 Busy-Wait

스핀락은 상호 배제(mutual exclusion) 문제의 가장 직접적인 하드웨어 기반 해법입니다. 운영체제 이론에서 상호 배제는 Dijkstra(1965)의 semaphore, Lamport(1974)의 bakery algorithm, Peterson(1981)의 2-프로세스(Process) 알고리즘 등으로 발전했지만, 커널 수준에서는 하드웨어 원자적 명령어를 활용한 busy-wait 방식이 가장 효율적입니다.

상호 배제의 4대 요건

요건정의spinlock 보장 방식
Mutual Exclusion임계 영역에 최대 1개 스레드(Thread)만 진입원자적 CAS/XCHG로 단일 변수 배타적 설정
Progress임계 영역이 비어있으면 진입 보장CAS 성공 시 즉시 진입, 실패 시 재시도
Bounded Waiting유한 시간 내 진입 보장TAS: 미보장(불공정), ticket/MCS: FIFO 보장
No AssumptionsCPU 속도/개수에 무관하드웨어 원자성에만 의존

Busy-Wait vs Sleep: 선택 기준

spinlock의 busy-wait은 대기 시간(Latency) 동안 CPU 사이클을 소모하지만, 컨텍스트 스위칭(Context Switching) 비용이 없습니다. 이 트레이드오프를 정량적으로 분석하면:

/* 비용 모델 (cycles 단위, 대략적 수치) */

스핀 1회 반복:              ~5 cycles (PAUSE/WFE 포함)
mutex 슬립 + 웨이크업:    ~5,000–10,000 cycles (스케줄러 호출 + 컨텍스트 스위치)

/* 손익 분기점 (break-even point) */
임계 영역 보유 시간 < ~1,000 cycles → spinlock이 유리
임계 영역 보유 시간 > ~1,000 cycles → mutex가 유리

/* 커널 spinlock의 전형적 보유 시간: 10~500 cycles */
/* → busy-wait이 합리적인 선택 */
Busy-Wait(spinlock) vs Sleep(mutex) 비용 모델 임계 영역 보유 시간 (cycles) 총 비용 (cycles) spinlock (비용 ∝ 보유시간) mutex (고정 오버헤드 + 슬립) ~1,000 cycles 손익 분기점 spinlock 유리 영역 mutex 유리 영역 커널 spinlock 전형 (10~500)
임계 영역이 짧을수록 spinlock(busy-wait)이 유리하고, 길어질수록 mutex(sleep)가 유리합니다

스핀락 진화 역사: TAS → Ticket → MCS → qspinlock

Linux 커널의 스핀락은 30년 이상에 걸쳐 네 세대를 거쳤습니다. 각 세대는 이전 세대의 근본적 문제를 해결하면서 새로운 설계 제약을 도입했습니다.

Linux 커널 스핀락 4세대 진화 ~2.0 1세대: Test-and-Set xchg(lock, 1) 모든 CPU가 같은 변수 polling 캐시 트래픽 O(N²), 불공정 단순하지만 확장 불가 2.6.25 2세대: Ticket Lock next/owner 카운터 쌍 FIFO 순서 보장 캐시 트래픽 O(N) 공정하지만 NUMA 비효율 4.2 3세대: qspinlock MCS 큐 + 32비트 압축 로컬 변수에서 spinning 캐시 트래픽 O(1) NUMA 친화적, 현재 기본 5.14+ 4세대: CNA Lock NUMA 노드별 큐 분리 같은 노드 우선 전달 크로스-노드 전송 최소화 4+ 소켓에서 큰 효과 캐시 트래픽 비교 (N CPU 경합 시) TAS: O(N²) 모든 CPU가 매 루프마다 캐시 라인 무효화 유발 unlock 시에도 N개 CPU가 동시에 CAS 재시도 Ticket: O(N) 모든 CPU가 owner 변수 polling (읽기만) unlock 시 owner++ → N개 캐시 라인 무효화 qspinlock: O(1) 각 CPU가 자기 로컬 MCS 노드에서 spin unlock 시 다음 대기자의 locked만 설정 (1회) CNA: O(1) + NUMA 같은 NUMA 노드 대기자에게 우선 전달 크로스-노드 캐시 라인 전송 빈도 대폭 감소 N=128 → ~16,384 inv. N=128 → 128 inv. N=128 → 1 inv. N=128 → 1 inv. (로컬)
스핀락 4세대 진화: 핵심 지표는 unlock 시 캐시 라인(Cache Line) 무효화(invalidation) 횟수

1세대: Test-and-Set (TAS)

가장 원시적인 스핀락으로, 단일 변수에 대한 원자적 교환(xchg)으로 구현됩니다.

/* 1세대 Test-and-Set 스핀락 (개념 코드) */
typedef struct {
    volatile int locked;
} tas_spinlock_t;

static inline void tas_lock(tas_spinlock_t *lock)
{
    while (xchg(&lock->locked, 1) != 0)
        cpu_relax();   /* PAUSE/yield hint */
}

static inline void tas_unlock(tas_spinlock_t *lock)
{
    smp_store_release(&lock->locked, 0);
}

/* 문제점:
 * 1. 모든 대기 CPU가 매 루프마다 xchg 실행 → 캐시 라인 배타적 소유권 경쟁
 * 2. unlock 시 캐시 라인 무효화 → 모든 대기 CPU가 동시에 xchg 재시도
 * 3. 순서 보장 없음 → 특정 CPU가 영원히 획득 못할 수 있음 (starvation)
 */
⚠️

TTAS(Test-and-Test-and-Set) 개선: TAS의 캐시(Cache) 트래픽 문제를 완화하기 위해 while (lock->locked || xchg(&lock->locked, 1)) 패턴이 제안되었습니다. 먼저 읽기(shared 상태)로 확인하고, 해제된 것 같을 때만 xchg를 시도합니다. 그러나 unlock 시의 thundering herd 문제(모든 대기 CPU가 동시에 xchg 시도)는 여전히 존재합니다.

2세대: Ticket Spinlock

Ticket spinlock은 번호표(next/owner) 방식으로 FIFO 순서를 보장합니다. Linux 2.6.25(2008)에서 x86에 도입되었습니다.

/* 2세대 Ticket Spinlock (Linux 2.6.25~4.1) */
typedef struct {
    union {
        u32 slock;
        struct {
            u16 owner;   /* 현재 서비스 중인 번호 */
            u16 next;    /* 다음 발급할 번호 */
        };
    };
} ticket_spinlock_t;

static inline void ticket_lock(ticket_spinlock_t *lock)
{
    /* 번호표 발급: next를 원자적으로 증가 */
    u16 ticket = atomic_fetch_add(1 << 16, &lock->slock) >> 16;

    /* 내 번호가 호출될 때까지 owner를 polling */
    while (READ_ONCE(lock->owner) != ticket)
        cpu_relax();
}

static inline void ticket_unlock(ticket_spinlock_t *lock)
{
    /* owner++ → 다음 번호 대기자가 루프 탈출 */
    lock->owner++;   /* 이 write가 모든 대기 CPU의 캐시 라인을 무효화 */
}

/* 장점: FIFO 공정성, starvation 불가
 * 단점: unlock 시 owner 변경 → N개 대기 CPU 모두 캐시 라인 invalidation
 *       4소켓 NUMA에서 심각한 성능 저하 (O(N) 캐시 트래픽) */

3세대: MCS Lock (이론)

Mellor-Crummey와 Scott(1991)가 제안한 MCS lock은 각 대기자가 자신의 로컬 변수에서 spinning하는 설계로, 캐시 트래픽을 O(1)로 줄였습니다. 그러나 원래의 MCS lock은 포인터 크기(64비트)의 잠금 변수를 요구하여, Linux 커널의 sizeof(spinlock_t) == 4 제약과 충돌했습니다. 이 문제를 해결한 것이 qspinlock입니다.

/* MCS Lock 순수 구현 (Mellor-Crummey & Scott, 1991) — 개념 코드 */

struct mcs_node {
    struct mcs_node *volatile next;  /* 다음 대기자 포인터 */
    volatile int locked;             /* 0=차례 도래, 1=대기중 */
};

/* MCS lock 변수: 큐의 tail을 가리키는 포인터
 * → sizeof(mcs_lock_t) == 8 (64비트) — 커널 제약 위반!
 * 이것이 qspinlock이 tail을 16비트로 압축한 이유 */
typedef struct mcs_node *mcs_lock_t;

static inline void mcs_lock(mcs_lock_t *lock, struct mcs_node *me)
{
    struct mcs_node *prev;

    me->next = NULL;
    me->locked = 1;       /* 초기: 대기 상태 */

    /* 원자적으로 tail을 자신으로 교체, 이전 tail 반환 */
    prev = xchg(lock, me);

    if (prev != NULL) {
        /* 이전 대기자가 있음 → 큐에 자신을 연결 */
        prev->next = me;

        /* ★ 핵심: 자기 로컬 변수에서 spinning
         * → 캐시 라인 경합 O(1) (자기 노드만 polling)
         * → ticket lock의 O(N) 글로벌 변수 polling과 대비 */
        while (READ_ONCE(me->locked))
            cpu_relax();
    }
    /* prev == NULL → 큐가 비어있었음 → 즉시 획득 */
}

static inline void mcs_unlock(mcs_lock_t *lock, struct mcs_node *me)
{
    struct mcs_node *next = READ_ONCE(me->next);

    if (!next) {
        /* 내가 마지막 대기자일 수 있음 → CAS로 tail 클리어 시도 */
        if (cmpxchg(lock, me, NULL) == me)
            return;   /* 성공: 큐 비움 */

        /* CAS 실패: 새 대기자가 xchg로 등록했지만
         * 아직 prev->next = me를 완료하지 않음 → 대기 */
        while (!(next = READ_ONCE(me->next)))
            cpu_relax();
    }

    /* 다음 대기자를 깨움: locked를 0으로 설정
     * → 다음 대기자의 spin 루프 탈출 */
    smp_store_release(&next->locked, 0);
}

/* 사용 예시:
 * mcs_lock_t my_lock = NULL;
 * struct mcs_node my_node;              // 호출자가 노드 제공
 * mcs_lock(&my_lock, &my_node);
 * // 임계 영역
 * mcs_unlock(&my_lock, &my_node);
 *
 * 장점: O(1) 캐시 트래픽, FIFO 공정성
 * 단점: sizeof(lock) == 8, 호출자가 노드 관리 필요
 *       → qspinlock은 per-CPU 노드 + 32비트 tail 인코딩으로 해결
 */
MCS Lock → qspinlock 압축 변환 순수 MCS Lock lock 변수: 포인터 (64비트) tail 노드 주소 직접 저장 노드 A (CPU 0) next → B locked = 0 노드 B (CPU 3) next → NULL locked = 1 (spin) 노드는 호출자가 스택/힙에 할당 sizeof(lock) = 8 (커널 ABI 위반) 압축 qspinlock lock 변수: 32비트 워드 tail=(cpu+1|idx) | pending | locked per-CPU qnodes[0] next → qnodes[0]@CPU3 locked = 0 per-CPU qnodes[0] next → NULL locked = 1 (spin) 노드는 per-CPU 정적 배열 (동적 할당 불필요) sizeof(lock) = 4 (커널 ABI 호환) qspinlock의 3가지 핵심 혁신 1. tail 포인터를 (cpu+1)<<18 | idx<<16으로 16비트 인코딩 → 포인터 제거 2. MCS 노드를 per-CPU 정적 배열(qnodes[4])에 배치 → 동적 할당 제거 3. pending 비트 추가 → 2-CPU 경합 시 MCS 큐 없이 처리 (fast-pending)
qspinlock은 MCS의 포인터 기반 큐를 정수 인코딩 + per-CPU 노드로 변환하여 4바이트에 압축합니다

데이터 구조 분석

qspinlock 구조체(Struct)

/* include/asm-generic/qspinlock_types.h */
typedef struct qspinlock {
    union {
        atomic_t val;           /* 32비트 전체 상태 (원자적 접근) */
        struct __raw_tickets {
#ifdef __LITTLE_ENDIAN
            u8  locked;          /* byte 0: bit[0]   — 잠금 보유 여부 */
            u8  pending;         /* byte 1: bit[8]   — pending 대기자 */
#else
            u16 locked_pending;  /* 빅엔디안: 상위 16비트 */
#endif
            u16 tail;            /* byte 2-3: bit[16:31] — MCS 큐 tail */
        };
    };
} arch_spinlock_t;

/* 비트 레이아웃 (리틀 엔디안, 32비트 word) */
/*
 *  31                    16 15     8 7      0
 *  ┌────────────────────┬─────────┬─────────┐
 *  │     tail (16)      │pending(8)│locked(8)│
 *  │ [cpu+1 | idx]      │  0 or 1 │ 0 or 1  │
 *  └────────────────────┴─────────┴─────────┘
 *
 *  tail 인코딩: (cpu_id + 1) << _Q_TAIL_CPU_OFFSET | idx << _Q_TAIL_IDX_OFFSET
 *  - cpu+1: 0은 "큐 없음"을 의미하므로 +1 오프셋
 *  - idx: 0~3 (일반/softirq/hardirq/NMI 중첩 수준)
 */

MCS 노드 구조체

/* kernel/locking/mcs_spinlock.h */
struct mcs_spinlock {
    struct mcs_spinlock *next;   /* 다음 대기자 노드 포인터 */
    int locked;                   /* 0=대기중, 1=차례 도달 (로컬 spin 변수) */
    int count;                    /* 중첩 깊이 카운터 */
};

/* kernel/locking/qspinlock.c */
struct qnode {
    struct mcs_spinlock mcs;
};

/* Per-CPU MCS 노드 배열: 4개 중첩 수준 지원 */
static DEFINE_PER_CPU_ALIGNED(struct qnode, qnodes[4]);
/*
 * qnodes[0]: 프로세스 컨텍스트
 * qnodes[1]: softirq 컨텍스트
 * qnodes[2]: hardirq 컨텍스트
 * qnodes[3]: NMI 컨텍스트
 *
 * DEFINE_PER_CPU_ALIGNED: 캐시 라인 정렬
 * → 각 CPU의 노드가 자기 로컬 캐시 라인에 위치
 * → false sharing 방지
 */

spinlock_t 래퍼 구조체

/* include/linux/spinlock_types.h */
typedef struct spinlock {
    union {
        struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
        struct lockdep_map dep_map;   /* lockdep 추적 정보 */
#endif
    };
} spinlock_t;

typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;        /* 아키텍처별 qspinlock */
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned int magic, owner_cpu;
    void *owner;                     /* 디버그: 현재 보유자 */
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} raw_spinlock_t;

/* 크기 비교:
 * 일반 빌드:   sizeof(spinlock_t) == 4  (32비트 qspinlock만)
 * DEBUG 빌드:  sizeof(spinlock_t) == 24~40 (디버그 필드 추가)
 * PREEMPT_RT:  sizeof(spinlock_t) == sizeof(rt_mutex) (~40 bytes)
 */
spinlock_t 타입 계층 (일반 vs PREEMPT_RT) 일반 커널 (CONFIG_PREEMPT_RT=n) spinlock_t raw_spinlock_t arch_spinlock_t (qspinlock) 4 bytes — busy-wait preempt_disable() 포함 PREEMPT_RT 커널 spinlock_t rt_mutex (sleeping lock) ~40 bytes — sleep 가능 우선순위 상속 지원 preemptible raw_spinlock_t → qspinlock RT에서도 진정한 busy-wait 유지
일반 커널에서 spinlock_t은 qspinlock으로 컴파일되지만, PREEMPT_RT에서는 rt_mutex 기반 sleeping lock으로 변환됩니다

API 전체 레퍼런스

초기화 API

API용도비고
DEFINE_SPINLOCK(name)정적 spinlock 선언 + 초기화전역/파일 스코프
spin_lock_init(&lock)동적 spinlock 초기화kmalloc 등으로 할당 후
DEFINE_RAW_SPINLOCK(name)정적 raw_spinlock 선언 + 초기화RT 환경에서도 busy-wait 필요 시
raw_spin_lock_init(&lock)동적 raw_spinlock 초기화스케줄러(Scheduler)/타이머(Timer) 등 핵심 경로

Lock/Unlock API

Lock APIUnlock APIpreemptIRQBH사용 상황
spin_lock()spin_unlock()비활성화프로세스 컨텍스트 전용, IRQ와 공유 안 할 때
spin_lock_bh()spin_unlock_bh()비활성화비활성화softirq/tasklet과 공유할 때
spin_lock_irq()spin_unlock_irq()비활성화비활성화비활성화IRQ와 공유, 진입 시 IRQ 활성 확정일 때
spin_lock_irqsave()spin_unlock_irqrestore()비활성화비활성화비활성화IRQ와 공유, IRQ 상태 불확실할 때 (가장 안전)
spin_trylock()spin_unlock()비활성화비블로킹 시도 (실패 시 0 반환)
spin_trylock_bh()spin_unlock_bh()비활성화비활성화softirq 컨텍스트에서 비블로킹
spin_trylock_irq()spin_unlock_irq()비활성화비활성화비활성화IRQ 컨텍스트에서 비블로킹 (드묾)

상태 조회 API

API반환용도
spin_is_locked(&lock)bool잠금 여부 확인 (잠금 없이), assert/디버깅용
spin_is_contended(&lock)bool경합(Contention) 발생 여부 (대기자 존재)
assert_spin_locked(&lock)lockdep 없이도 잠금 보유 검증 (BUG_ON 발생)
lockdep_assert_held(&lock)lockdep 기반 잠금 보유 검증 (경고 출력)

raw_spin_* 대응 API

모든 spin_* API에는 대응하는 raw_spin_* 버전이 존재합니다. 일반 커널에서는 동일하게 동작하지만, PREEMPT_RT에서는 raw_spin_*만 진정한 busy-wait을 수행합니다.

/* raw_spin_* API — PREEMPT_RT에서도 busy-wait 유지 */
raw_spin_lock(&lock);
raw_spin_lock_irqsave(&lock, flags);
raw_spin_trylock(&lock);
raw_spin_unlock(&lock);
raw_spin_unlock_irqrestore(&lock, flags);

/* 사용 기준: raw_spin_*은 다음 경우에만 사용
 * - 스케줄러 핵심 경로 (runqueue lock)
 * - 타이머/hrtimer 인프라
 * - 인터럽트 컨트롤러 (GIC, APIC)
 * - NMI 핸들러
 * - 하드웨어 레지스터 접근 (짧은 I/O)
 * 커널 전체에서 ~100개 미만만 존재
 */

API 매크로 전개 체인 상세

커널의 spinlock API는 5단계 매크로 래핑으로 구성됩니다. 이 계층 구조는 lockdep 통합, preemption 제어, 디버그 검증, 아키텍처 추상화를 각 레이어에서 분리하기 위한 설계입니다.

spin_lock() 전개 경로

/* ===== Layer 1: 사용자 API — include/linux/spinlock.h ===== */
static inline void spin_lock(spinlock_t *lock)
{
    raw_spin_lock(&lock->rlock);
}
/* spinlock_t → raw_spinlock_t 변환
 * 일반 커널: 동일 구조체 (inline 전개)
 * PREEMPT_RT: spinlock_t는 rt_mutex, raw_spinlock_t만 qspinlock */

/* ===== Layer 2: raw API — include/linux/spinlock.h ===== */
#define raw_spin_lock(lock) \
    _raw_spin_lock(lock)

/* ===== Layer 3: SMP/UP 분기 — kernel/locking/spinlock.c ===== */
#ifdef CONFIG_SMP
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
    __raw_spin_lock(lock);
}
EXPORT_SYMBOL(_raw_spin_lock);
#else
/* UP(단일 CPU): preempt_disable()만 수행, 실제 lock 생략 */
#define _raw_spin_lock(lock) \
    __LOCK(lock)   /* → preempt_disable() */
#endif

/* ===== Layer 4: preemption + lockdep — include/linux/spinlock_api_smp.h ===== */
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    preempt_disable();                /* ① 선점 비활성화 */
    spin_acquire(&lock->dep_map,
                 0, 0, _RET_IP_);  /* ② lockdep: 잠금 획득 기록 */
    LOCK_CONTENDED(lock,
        do_raw_spin_trylock,
        do_raw_spin_lock);            /* ③ 실제 lock (아래 참조) */
}
/*
 * LOCK_CONTENDED 매크로:
 * - CONFIG_LOCK_STAT=n: do_raw_spin_lock(lock) 직접 호출
 * - CONFIG_LOCK_STAT=y: trylock 먼저 시도, 실패 시 contention 통계 기록 후 lock
 *
 * spin_acquire(): lockdep에 이 lock의 의존성 그래프 노드를 추가
 * → 순환 의존성(데드락) 검출의 핵심
 */

/* ===== Layer 5: 아키텍처별 구현 — include/linux/spinlock.h ===== */
static inline void do_raw_spin_lock(raw_spinlock_t *lock)
{
#ifdef CONFIG_DEBUG_SPINLOCK
    __spin_lock_debug(lock);          /* 디버그: magic/owner 검증 */
#else
    arch_spin_lock(&lock->raw_lock);  /* → queued_spin_lock() */
#endif
}

/* arch_spin_lock()은 아키텍처별로 정의:
 * x86/ARM64/RISC-V: queued_spin_lock() (qspinlock)
 * → fast path: atomic_try_cmpxchg_acquire()
 * → slow path: queued_spin_lock_slowpath()
 */
spin_lock() 5단계 매크로 전개 체인 spin_lock(lock) 사용자 API (spinlock_t → raw) _raw_spin_lock(lock) SMP/UP 분기점 (UP: preempt_disable만) __raw_spin_lock(lock) ① preempt_disable() ② spin_acquire(lockdep) ③ LOCK_CONTENDED do_raw_spin_lock(lock) DEBUG: magic/owner 검증 + arch 호출 arch_spin_lock → queued_spin_lock() atomic_try_cmpxchg_acquire (fast) / slowpath (slow) Layer 1: spinlock.h Layer 2: spinlock.c Layer 3: spinlock_api_smp.h Layer 4: spinlock.h Layer 5: qspinlock.h spin_unlock(): arch_spin_unlock (queued_spin_unlock) → spin_release(lockdep) → preempt_enable()
각 레이어가 하나의 관심사(preemption, lockdep, debug, arch)를 담당하여 유지보수성을 확보합니다

초기화 매크로 전개

/* include/linux/spinlock_types.h */
#define DEFINE_SPINLOCK(x) \
    spinlock_t x = __SPIN_LOCK_UNLOCKED(x)

#define __SPIN_LOCK_UNLOCKED(lockname) \
    (spinlock_t) { .rlock = __RAW_SPIN_LOCK_UNLOCKED(lockname) }

#define __RAW_SPIN_LOCK_UNLOCKED(lockname) \
    (raw_spinlock_t) {                               \
        .raw_lock = __ARCH_SPIN_LOCK_UNLOCKED,       \
        SPIN_DEBUG_INIT(lockname)                     \
        SPIN_DEP_MAP_INIT(lockname)                   \
    }

/* __ARCH_SPIN_LOCK_UNLOCKED:
 * qspinlock: { .val = ATOMIC_INIT(0) }  — 모든 비트 0 = FREE
 *
 * SPIN_DEBUG_INIT: (CONFIG_DEBUG_SPINLOCK 시)
 *   .magic = SPINLOCK_MAGIC,
 *   .owner = SPINLOCK_OWNER_INIT,  // (void *)-1
 *   .owner_cpu = -1
 *
 * SPIN_DEP_MAP_INIT: (CONFIG_DEBUG_LOCK_ALLOC 시)
 *   .dep_map = { .name = #lockname, .wait_type_inner = ... }
 */

/* 동적 초기화: spin_lock_init() */
#define spin_lock_init(lock)                            \
do {                                                    \
    static struct lock_class_key __key;              \
    __raw_spin_lock_init((raw_spinlock_t *)lock,     \
                         #lock, &__key,              \
                         LD_WAIT_CONFIG);            \
} while (0)

/* __raw_spin_lock_init 구현: */
void __raw_spin_lock_init(raw_spinlock_t *lock,
                         const char *name,
                         struct lock_class_key *key,
                         short inner)
{
#ifdef CONFIG_DEBUG_SPINLOCK
    lock->magic = SPINLOCK_MAGIC;         /* 0xdead4ead */
    lock->owner = SPINLOCK_OWNER_INIT;    /* (void *)-1 */
    lock->owner_cpu = -1;
#endif
    lock->raw_lock = (arch_spinlock_t)__ARCH_SPIN_LOCK_UNLOCKED;
    lockdep_init_map_wait(&lock->dep_map, name, key, 0, inner);
}

spin_lock_irqsave() 전개 경로

/* include/linux/spinlock.h */
#define spin_lock_irqsave(lock, flags) \
    raw_spin_lock_irqsave(spinlock_check(lock), flags)

/* spinlock_check(): 타입 안전성 검증 — spinlock_t*를 받아 raw_spinlock_t* 반환 */
static inline raw_spinlock_t *spinlock_check(spinlock_t *lock)
{
    return &lock->rlock;
}

/* include/linux/spinlock_api_smp.h */
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
    unsigned long flags;

    local_irq_save(flags);       /* ① IRQ 상태 저장 + 비활성화
                                  *    x86: pushfq; cli
                                  *    ARM64: mrs x0, daif; msr daifset, #2 */
    preempt_disable();            /* ② 선점 비활성화 (preempt_count++) */
    spin_acquire(&lock->dep_map,
                 0, 0, _RET_IP_); /* ③ lockdep 기록 */
    LOCK_CONTENDED(lock,
        do_raw_spin_trylock,
        do_raw_spin_lock);         /* ④ 실제 arch lock */
    return flags;
}

/* spin_unlock_irqrestore() 전개: */
static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock,
                                                 unsigned long flags)
{
    spin_release(&lock->dep_map, _RET_IP_); /* lockdep 해제 */
    do_raw_spin_unlock(lock);                /* arch unlock */
    local_irq_restore(flags);                /* IRQ 상태 복원
                                               *   x86: popfq
                                               *   ARM64: msr daif, x0 */
    preempt_enable();                         /* 선점 재활성화 + 재스케줄링 체크 */
}

spin_lock_bh() / spin_lock_irq() 전개 경로

/* spin_lock_bh: softirq(bottom half) 비활성화 + lock */
static inline void __raw_spin_lock_bh(raw_spinlock_t *lock)
{
    __local_bh_disable_ip(_RET_IP_,
                          SOFTIRQ_LOCK_OFFSET); /* ① softirq 비활성화
                                                  *   preempt_count += SOFTIRQ_LOCK_OFFSET
                                                  *   softirq 실행 차단 + 선점 비활성화 */
    spin_acquire(&lock->dep_map,
                 0, 0, _RET_IP_);               /* ② lockdep */
    LOCK_CONTENDED(lock,
        do_raw_spin_trylock,
        do_raw_spin_lock);                      /* ③ arch lock */
}

/* spin_unlock_bh: unlock + softirq 재활성화 */
static inline void __raw_spin_unlock_bh(raw_spinlock_t *lock)
{
    spin_release(&lock->dep_map, _RET_IP_);
    do_raw_spin_unlock(lock);
    __local_bh_enable_ip(_RET_IP_,
                         SOFTIRQ_LOCK_OFFSET); /* softirq 재활성화
                                                  *   pending softirq 있으면 즉시 실행 */
}

/* spin_lock_irq: IRQ 비활성화 + lock (flags 저장 안 함) */
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
    local_irq_disable();               /* ① IRQ 무조건 비활성화
                                        *   flags 저장 안 함 → _irqsave보다 빠름
                                        *   진입 시 IRQ 활성 상태 확실할 때만 사용 */
    preempt_disable();                 /* ② 선점 비활성화 */
    spin_acquire(&lock->dep_map,
                 0, 0, _RET_IP_);    /* ③ lockdep */
    LOCK_CONTENDED(lock,
        do_raw_spin_trylock,
        do_raw_spin_lock);              /* ④ arch lock */
}

/* spin_unlock_irq: IRQ 무조건 재활성화 */
static inline void __raw_spin_unlock_irq(raw_spinlock_t *lock)
{
    spin_release(&lock->dep_map, _RET_IP_);
    do_raw_spin_unlock(lock);
    local_irq_enable();                /* IRQ 무조건 활성화 */
    preempt_enable();
}

spin_trylock() 내부 구현

/* include/linux/spinlock.h */
static inline int spin_trylock(spinlock_t *lock)
{
    return raw_spin_trylock(&lock->rlock);
}

/* include/linux/spinlock_api_smp.h */
static inline int __raw_spin_trylock(raw_spinlock_t *lock)
{
    preempt_disable();                        /* 선점 비활성화 */
    if (do_raw_spin_trylock(lock)) {
        spin_acquire(&lock->dep_map,
                     0, 1, _RET_IP_);         /* 3번째 인자 1 = trylock */
        return 1;                              /* 성공 */
    }
    preempt_enable();                         /* 실패: 선점 복원 */
    return 0;                                  /* 실패 */
}

/* do_raw_spin_trylock → arch_spin_trylock → queued_spin_trylock */
static inline int queued_spin_trylock(struct qspinlock *lock)
{
    int val = atomic_read(&lock->val);

    /* 먼저 읽기로 확인 — 이미 잠겨있으면 CAS 시도 안 함
     * (캐시 라인을 Shared 상태로 유지, Modified로 승격 안 함) */
    if (unlikely(val))
        return 0;    /* 이미 잠김 → 실패 */

    /* CAS: 0(FREE) → _Q_LOCKED_VAL(1) */
    return likely(atomic_try_cmpxchg_acquire(
                      &lock->val, &val, _Q_LOCKED_VAL));
}
/* trylock의 핵심: slowpath에 절대 진입하지 않음
 * → 논블로킹 보장, 실패 시 즉시 반환
 * → contention이 심한 환경에서 polling 대안으로 활용 */

spin_is_locked() / spin_is_contended() 내부

/* include/asm-generic/qspinlock.h */
static inline int queued_spin_is_locked(struct qspinlock *lock)
{
    /*
     * 하위 8비트(locked 바이트) 또는 상위 비트(pending/tail) 중
     * 하나라도 설정되어 있으면 "잠김" 상태
     * → pending 대기자가 있는 경우도 "잠김"으로 판정
     */
    return atomic_read(&lock->val);
}

static inline int queued_spin_is_contended(struct qspinlock *lock)
{
    /*
     * tail 필드가 0이 아니면 MCS 큐에 대기자가 있음
     * → 2명 이상 경합 중 (locked + pending은 최대 2명, tail은 3명+)
     */
    return atomic_read(&lock->val) & ~_Q_LOCKED_MASK;
}

/* spin_is_locked()과 spin_is_contended()는 잠금 없이 읽기만 수행
 * → 결과는 순간적 스냅샷이므로, 즉시 무효화될 수 있음
 * → assert/디버깅 용도로만 사용, 제어 흐름 판단에는 부적합 */

/* assert_spin_locked: 디버그 검증 */
static inline void assert_spin_locked(spinlock_t *lock)
{
    BUG_ON(!spin_is_locked(lock));
    /* 잠금 미보유 시 커널 패닉 — lockdep보다 저비용이지만 거친 검증 */
}

/* lockdep_assert_held: lockdep 기반 정밀 검증 */
#define lockdep_assert_held(lock) \
    lockdep_assert(lockdep_is_held(lock) != LOCK_STATE_NOT_HELD)
/* lockdep이 현재 태스크의 held_locks 배열에서 이 lock을 검색
 * → assert_spin_locked보다 정확 (다른 CPU가 보유 중인 경우 구분 가능) */
💡

spin_lock_irq() vs spin_lock_irqsave() 선택 기준: spin_lock_irq()flags 저장/복원이 없어 pushfq/popfq 명령어 1쌍을 절약합니다(x86 기준 ~5 cycles). 하지만 호출 시점에 IRQ가 이미 비활성화되어 있으면, spin_unlock_irq()가 IRQ를 강제 활성화하여 예상치 못한 인터럽트를 유발합니다. 규칙: IRQ 상태가 확실하면 _irq, 불확실하면 _irqsave.

qspinlock 32비트 상태 인코딩 상세

qspinlock의 핵심 혁신은 MCS 큐의 개념을 단 32비트에 압축한 것입니다. 이 제약은 sizeof(spinlock_t) == 4를 유지해야 하는 커널 ABI 호환성 요구에서 비롯됩니다.

/* include/asm-generic/qspinlock.h — 비트 필드 상수 */
#define _Q_SET_MASK(type)         ((((1U) << _Q_ ## type ## _BITS) - 1)\
                                   << _Q_ ## type ## _OFFSET)

#define _Q_LOCKED_OFFSET    0
#define _Q_LOCKED_BITS      8
#define _Q_LOCKED_MASK      _Q_SET_MASK(LOCKED)    /* 0x000000FF */
#define _Q_LOCKED_VAL       (1U << _Q_LOCKED_OFFSET)  /* 0x00000001 */

#define _Q_PENDING_OFFSET   8
#define _Q_PENDING_BITS     8
#define _Q_PENDING_MASK     _Q_SET_MASK(PENDING)   /* 0x0000FF00 */
#define _Q_PENDING_VAL      (1U << _Q_PENDING_OFFSET) /* 0x00000100 */

#define _Q_TAIL_IDX_OFFSET  16
#define _Q_TAIL_IDX_BITS    2     /* 0~3: 중첩 수준 */
#define _Q_TAIL_CPU_OFFSET  18
#define _Q_TAIL_CPU_BITS    14    /* 최대 16,383 CPU 지원 */
#define _Q_TAIL_MASK        _Q_SET_MASK(TAIL)

/* tail 인코딩/디코딩 */
static inline u32 encode_tail(int cpu, int idx)
{
    /* cpu+1: 0은 "큐 없음" 표시 */
    return (u32)(cpu + 1) << _Q_TAIL_CPU_OFFSET |
           idx << _Q_TAIL_IDX_OFFSET;
}
qspinlock 32비트 워드 상세 레이아웃 31 18 17 16 15 8 7 0 tail_cpu (14비트) cpu_id + 1 (0=큐 없음) idx 2비트 pending (8비트) 0 or 1 locked (8비트) 0 or 1 _Q_TAIL_MASK (bit 16:31) 상태 예시 (val 값) 0x00000000 — FREE (잠금 해제) 0x00000001 — LOCKED (보유자 1명, 대기자 없음) 0x00000101 — LOCKED + PENDING (대기자 1명) 0x00050101 — LOCKED + PENDING + TAIL (MCS 큐 활성)
32비트 하나에 locked(8) + pending(8) + tail_idx(2) + tail_cpu(14) = 32비트를 압축하여 sizeof(spinlock_t)==4 유지

Fast Path: 경합 없는 즉시 획득

qspinlock의 fast path는 단일 원자적 CAS 1회로 잠금을 획득합니다. 커널 스핀락의 대다수 획득은 경합 없이 이루어지므로, 이 경로의 효율성이 전체 성능을 좌우합니다.

/* include/asm-generic/qspinlock.h */
static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
    int val = 0;

    /* Fast path: val이 0(FREE)이면 1(LOCKED)로 설정 — 1회 CAS */
    if (likely(atomic_try_cmpxchg_acquire(&lock->val, &val,
                                            _Q_LOCKED_VAL)))
        return;

    /* CAS 실패: 누군가 이미 보유 중 → slowpath 진입 */
    queued_spin_lock_slowpath(lock, val);
}

/* Fast path 성능:
 * - x86:   LOCK CMPXCHG 1회 → ~20 cycles
 * - ARM64: LDAXR/STXR 루프 1회 → ~15 cycles
 * - 경합 없는 경우 추가 비용 0 (slowpath 미진입)
 *
 * likely() 힌트: 컴파일러가 fast path를 인라인 코드에,
 * slowpath를 별도 함수(noinline)로 배치 → I-cache 효율 극대화
 */
/* Unlock — fast path (atomic store) */
static __always_inline void queued_spin_unlock(struct qspinlock *lock)
{
    /*
     * smp_store_release: locked 바이트를 0으로 설정
     * - release 의미론: 임계 영역의 모든 메모리 접근이 이 store 전에 완료
     * - 바이트 단위 store로 pending/tail 비트를 건드리지 않음
     */
    smp_store_release(&lock->locked, 0);
}

queued_spin_lock_slowpath() 3단계 워크스루

fast path CAS가 실패하면 queued_spin_lock_slowpath()에 진입합니다. 이 함수는 경합 수준에 따라 3단계로 분기하며, 각 단계는 이전 단계보다 무거운 메커니즘을 사용합니다.

queued_spin_lock_slowpath() — 3단계 분기 흐름 slowpath 진입 (CAS 실패) val==LOCKED? (pending=0,tail=0) Yes Phase 1: Pending 경로 대기자 1명 (보유자 + 나) ① pending 비트 설정 (CAS) ② locked 비트 polling (release 대기) ③ locked=1 설정 + pending=0 해제 비용: CAS 2회 + polling No Phase 2: MCS 큐 진입 대기자 2명+ (pending 이미 설정됨) ① per-CPU MCS 노드 할당 ② xchg_tail로 큐 tail에 등록 ③ 이전 노드의 next에 자신 연결 ④ 자기 node.locked에서 spin 비용: CAS + xchg + 로컬 spin Phase 3: MCS Head → spinlock 획득 큐 맨 앞에 도달 (node.locked=1로 통지받음) ① locked+pending 비트 모두 0이 될 때까지 polling ② locked=1 설정 + tail 정리 (CAS) ③ 뒤에 대기자 있으면 next.locked=1로 깨움 잠금 획득 완료 (return)
경합 수준에 따라 Phase 1(경량) 또는 Phase 2→3(MCS 큐) 경로를 선택합니다

Phase 1: Pending 경로 상세

/* queued_spin_lock_slowpath() — Phase 1: Pending 경로 */
if (val == _Q_LOCKED_VAL) {
    /* 조건: locked=1, pending=0, tail=0
     * → 보유자 1명만 있고 다른 대기자 없음
     * → MCS 큐 없이 pending 비트로 경량 처리 */

    /* Step 1: pending 비트 설정 */
    if (atomic_try_cmpxchg_acquire(&lock->val, &val,
                                    val | _Q_PENDING_VAL)) {

        /* Step 2: locked 비트가 풀릴 때까지 spinning
         * atomic_cond_read_acquire: val을 반복 읽다가
         * locked 비트가 0이 되면 acquire 배리어와 함께 반환 */
        atomic_cond_read_acquire(&lock->val,
            !(VAL & _Q_LOCKED_MASK));

        /* Step 3: locked 비트 획득 + pending 비트 해제
         * 바이트 단위 store로 원자적 처리 */
        clear_pending_set_locked(lock);
        return;
    }
    /* CAS 실패: 다른 CPU가 먼저 pending 설정 → Phase 2로 */
}

/* Phase 1의 존재 이유:
 * 커널 스핀락 경합의 ~90%가 2개 CPU 사이에서 발생
 * → MCS 노드 할당 없이 CAS 2회 + polling으로 완료
 * → Phase 2 대비 ~30% 빠름 */

Phase 2: MCS 큐 진입 상세

/* queued_spin_lock_slowpath() — Phase 2: MCS 큐 진입 */

/* Step 1: per-CPU MCS 노드 할당 */
node = this_cpu_ptr(&qnodes[0].mcs);
idx = node->count++;    /* 중첩 수준 추적 */
tail = encode_tail(smp_processor_id(), idx);
node = grab_mcs_node(node, idx);
node->locked = 0;
node->next = NULL;

/* Step 2: 큐 tail에 자신을 원자적으로 등록 */
old = xchg_tail(lock, tail);

/* Step 3: 이전 대기자가 있으면 연결 */
if (old & _Q_TAIL_MASK) {
    prev = decode_tail(old);
    /* 이전 노드의 next에 자신을 연결 */
    WRITE_ONCE(prev->next, node);

    /* Step 4: 자기 노드의 locked 필드에서 spinning
     * ★ 핵심: 자기 캐시 라인에서만 spin → O(1) 캐시 트래픽
     * 이전 대기자가 unlock 시 node->locked=1 설정으로 깨움 */
    arch_mcs_spin_lock_contended(&node->locked);
}

/* 여기 도달 = MCS 큐의 head 위치 → Phase 3로 */

Phase 3: MCS Head에서 spinlock 획득

/* queued_spin_lock_slowpath() — Phase 3: spinlock 획득 */

/* MCS 큐 head에서 locked+pending 비트가 모두 0이 될 때까지 대기
 * (보유자와 pending 대기자가 모두 완료해야 차례) */
val = atomic_cond_read_acquire(&lock->val,
    !(VAL & _Q_LOCKED_PENDING_MASK));

/* locked 비트 획득 + tail 정리 */
for (;;) {
    /* 뒤에 대기자가 있는지 확인 */
    if (val & _Q_TAIL_MASK) {
        /* 대기자 있음: locked만 설정, tail 유지 */
        atomic_try_cmpxchg_relaxed(&lock->val, &val,
                                    val | _Q_LOCKED_VAL);
    } else {
        /* 마지막 대기자: tail 클리어 + locked 설정 */
        atomic_try_cmpxchg_relaxed(&lock->val, &val,
                                    _Q_LOCKED_VAL);
    }
    break;
}

/* 다음 대기자를 MCS 큐에서 깨움 */
next = READ_ONCE(node->next);
if (next)
    arch_mcs_spin_unlock_contended(&next->locked);
/* next->locked = 1 → 다음 대기자의 Phase 2 spin 루프 탈출 */

/* MCS 노드 반환 */
node->count--;

slowpath 핵심 헬퍼 함수 상세

queued_spin_lock_slowpath()에서 사용하는 헬퍼 함수들은 각각 정교한 비트 조작과 메모리 순서 의미론을 캡슐화합니다. 이 함수들의 내부를 이해해야 slowpath의 상태 전이를 정확히 추적할 수 있습니다.

clear_pending_set_locked()

/* include/asm-generic/qspinlock.h
 * Pending 경로 완료 시 호출: pending=0 + locked=1을 원자적으로 설정 */
static inline void clear_pending_set_locked(struct qspinlock *lock)
{
    /*
     * 상태 전이: 0x0000_0100 (pending=1, locked=0)
     *         → 0x0000_0001 (pending=0, locked=1)
     *
     * 하위 16비트(locked+pending)를 단일 원자적 쓰기로 갱신
     * → 두 필드를 따로 수정하면 경쟁 조건 발생 가능
     */
    struct __raw_tickets *t = (struct __raw_tickets *)lock;

    WRITE_ONCE(t->locked_pending, _Q_LOCKED_VAL);
    /*
     * 왜 atomic이 아닌 WRITE_ONCE인가?
     * 이 시점에서 이 lock word의 하위 16비트를 수정할 수 있는 것은
     * 자기 자신뿐 (pending 소유자). tail 필드는 상위 16비트에 있어
     * 바이트/워드 단위 접근이 독립적.
     *
     * 단, 아키텍처가 16비트 자연 정렬 store를 원자적으로 보장해야 함
     * → x86, ARM64, RISC-V 모두 보장
     */
}

/* 대안 구현 (바이트 단위 접근 불가 아키텍처): */
static inline void clear_pending_set_locked(struct qspinlock *lock)
{
    atomic_add(-_Q_PENDING_VAL + _Q_LOCKED_VAL, &lock->val);
    /* -0x100 + 0x001 = -0xFF → pending 클리어 + locked 설정
     * 단일 원자적 add로 두 필드를 동시에 변경 */
}

set_locked()

/* MCS 큐 head에서 spinlock을 획득할 때 호출 */
static __always_inline void set_locked(struct qspinlock *lock)
{
    /*
     * locked 바이트만 1로 설정 — pending/tail은 건드리지 않음
     *
     * 이 시점의 상태: locked=0, pending=0, tail=자신(또는 다음 대기자)
     * 목표 상태: locked=1 (나머지 유지)
     *
     * smp_store_release가 아닌 WRITE_ONCE를 사용:
     * → 이 함수 전에 이미 atomic_cond_read_acquire()로
     *   acquire 배리어가 확보됨
     */
    WRITE_ONCE(lock->locked, 1);
}

xchg_tail()

/* MCS 큐 tail에 자신을 등록하고 이전 tail 반환 */
static __always_inline u32 xchg_tail(struct qspinlock *lock,
                                      u32 tail)
{
#if defined(CONFIG_X86) || defined(CONFIG_ARM64)
    /* 최적화: tail 필드(상위 16비트)만 원자적 교환
     * → 하위 16비트(locked+pending)를 건드리지 않음
     * → 32비트 전체 CAS 대비 경합 감소 */
    u16 old = xchg_relaxed(&lock->tail,
                           tail >> _Q_TAIL_OFFSET);
    return (u32)old << _Q_TAIL_OFFSET;
#else
    /* 범용 구현: 32비트 전체에서 CAS 루프 */
    u32 old, new;
    do {
        old = atomic_read(&lock->val);
        new = (old & ~_Q_TAIL_MASK) | tail;
    } while (!atomic_try_cmpxchg_relaxed(&lock->val,
                                           &old, new));
    return old;
#endif
}

/* xchg_relaxed 사용 이유:
 * xchg_tail은 큐에 등록만 하는 단계이므로
 * acquire/release 배리어가 불필요 (이후 별도 배리어 수행)
 *
 * x86: XCHG는 내장 LOCK 의미론 → relaxed 지정해도 full barrier
 * ARM64: SWPAL → relaxed는 SWP만 사용 (배리어 제거)
 */

arch_mcs_spin_lock/unlock_contended()

/* 아키텍처별 MCS 노드 로컬 스피닝 구현 */

/* include/asm-generic/mcs_spinlock.h — 범용 구현 */
#define arch_mcs_spin_lock_contended(l)             \
do {                                                    \
    while (!(smp_load_acquire(l)))                   \
        cpu_relax();                                 \
} while (0)
/*
 * smp_load_acquire: locked 필드를 acquire 의미론으로 읽음
 * → locked=1이 관찰되면, 이전 head가 쓴 모든 데이터가 가시적
 * cpu_relax(): PAUSE(x86) / WFE(ARM64) / nop(RISC-V)
 */

#define arch_mcs_spin_unlock_contended(l)           \
    smp_store_release((l), 1)
/*
 * 다음 대기자의 locked=1로 설정
 * release 의미론: 임계 영역의 모든 수정이 이 store 전에 완료
 * → 다음 대기자가 acquire로 읽을 때 모든 수정이 가시적
 */

/* arch/arm64 최적화: WFE 기반 저전력 대기 */
static inline void arch_mcs_spin_lock_contended(int *l)
{
    int val;

    asm volatile(
    "    sevl\n"               /* Set Event Local → 첫 WFE 즉시 통과 */
    "1:  wfe\n"                /* Wait For Event → 코어 저전력 상태 */
    "    ldaxr  %w0, [%1]\n"   /* Load-Acquire Exclusive */
    "    cbz    %w0, 1b\n"     /* 아직 0 → 다시 WFE */
    : "=&r" (val)
    : "r" (l)
    : "memory");
    /* WFE 메커니즘:
     * 1. exclusive monitor가 클리어되면 WFE 탈출 (다른 CPU의 store)
     * 2. SEV 명령어 수신 시 WFE 탈출 (명시적 이벤트)
     * 3. IRQ/FIQ 발생 시 WFE 탈출
     * → PAUSE(x86)보다 훨씬 적극적인 전력 절감 */
}

/* x86: PAUSE 기반 구현 (별도 아키텍처 오버라이드 없음, 범용 사용)
 * cpu_relax() = PAUSE 명령어
 * → ~140 cycles 지연 (Skylake+)
 * → 메모리 순서 위반 감지 시 파이프라인 재실행 방지 */

atomic_cond_read_acquire()

/* include/linux/atomic/atomic-instrumented.h
 * 조건이 만족될 때까지 원자적 변수를 polling하며 대기 */

#define atomic_cond_read_acquire(v, c) \
    smp_cond_load_acquire(&(v)->counter, (c))

/* include/asm-generic/barrier.h */
#define smp_cond_load_acquire(ptr, cond_expr) \
({                                                      \
    typeof(*ptr) VAL;                                 \
    for (;;) {                                        \
        VAL = smp_load_acquire(ptr);                 \
        if (cond_expr)                                \
            break;                                   \
        cpu_relax();                                  \
    }                                                   \
    VAL;                                                 \
})

/* slowpath에서의 사용 예:
 *
 * (1) Pending 경로 — locked 해제 대기:
 *     atomic_cond_read_acquire(&lock->val,
 *             !(VAL & _Q_LOCKED_MASK));
 *     → VAL의 locked 비트가 0이 될 때까지 polling
 *     → 통과 시 acquire 배리어 확보
 *
 * (2) MCS head — locked+pending 해제 대기:
 *     atomic_cond_read_acquire(&lock->val,
 *             !(VAL & _Q_LOCKED_PENDING_MASK));
 *     → locked=0 AND pending=0이 될 때까지 대기
 *     → 보유자와 pending 대기자 모두 완료 후 진입
 *
 * ARM64 최적화: WFE 기반으로 컴파일되어 저전력 대기
 * x86: PAUSE 루프로 컴파일 */

encode_tail() / decode_tail()

/* kernel/locking/qspinlock.c */

/* tail 인코딩: (cpu+1, idx) → 16비트 정수 */
static inline u32 encode_tail(int cpu, int idx)
{
    /* cpu+1: 0은 "큐 없음"을 표시하기 위해 예약
     * → 실제 CPU 0은 tail에 1로 인코딩됨
     * idx: 0~3 (process/softirq/hardirq/NMI 중첩 수준) */
    return (u32)(cpu + 1) << _Q_TAIL_CPU_OFFSET |
           idx << _Q_TAIL_IDX_OFFSET;
}

/* tail 디코딩: 16비트 정수 → per-CPU MCS 노드 포인터 */
static inline struct mcs_spinlock *decode_tail(u32 tail)
{
    int cpu = (tail >> _Q_TAIL_CPU_OFFSET) - 1;
    int idx = (tail >> _Q_TAIL_IDX_OFFSET) &
              ((1U << _Q_TAIL_IDX_BITS) - 1);

    /* per-CPU 배열에서 해당 CPU의 해당 중첩 수준 노드 반환 */
    return per_cpu_ptr(&qnodes[idx].mcs, cpu);
}

/* grab_mcs_node: 현재 CPU의 MCS 노드 할당 */
static inline struct mcs_spinlock *grab_mcs_node(
    struct mcs_spinlock *base, int idx)
{
    return &((struct qnode *)base + idx)->mcs;
}

/* 전체 흐름 요약:
 * 1. node = this_cpu_ptr(&qnodes[0].mcs);  // per-CPU 기본 노드
 * 2. idx = node->count++;                   // 중첩 깊이 증가
 * 3. tail = encode_tail(smp_processor_id(), idx);  // 16비트 인코딩
 * 4. node = grab_mcs_node(base, idx);       // 실제 사용할 노드
 * 5. old = xchg_tail(lock, tail);           // 큐 등록
 * 6. prev = decode_tail(old);               // 이전 tail → 노드 포인터
 * 7. prev->next = node;                     // 큐 연결
 */

중첩 컨텍스트와 per-CPU MCS 노드

단일 CPU에서 프로세스 → softirq → hardirq → NMI 순으로 중첩 인터럽트가 발생하면, 각 컨텍스트가 서로 다른 스핀락의 MCS 큐에 동시에 대기할 수 있습니다. qspinlock은 이를 위해 per-CPU로 4개의 MCS 노드를 미리 할당합니다.

per-CPU qnodes[4] — 중첩 컨텍스트별 MCS 노드 배정 CPU N — per_cpu(qnodes, N) qnodes[0] 프로세스 컨텍스트 idx=0 spin_lock() 호출 시 qnodes[1] softirq 컨텍스트 idx=1 softirq 중 다른 락 경합 qnodes[2] hardirq 컨텍스트 idx=2 IRQ 핸들러 중 락 경합 qnodes[3] NMI 컨텍스트 idx=3 NMI 중 락 경합 중첩 시나리오 예시 ① 프로세스: spin_lock(&lock_A) → MCS 큐 진입 (qnodes[0] 사용) ② softirq 발생: spin_lock(&lock_B) → MCS 큐 진입 (qnodes[1] 사용) ③ hardirq 발생: spin_lock(&lock_C) → MCS 큐 진입 (qnodes[2] 사용) ④ NMI 발생: spin_lock(&lock_D) → MCS 큐 진입 (qnodes[3] 사용)
각 컨텍스트가 독립된 MCS 노드를 사용하므로 중첩 인터럽트에서도 동적 할당 없이 안전하게 동작합니다
/* 중첩 깊이 추적: node->count 필드 */
/* slowpath 진입 시: */
node = this_cpu_ptr(&qnodes[0].mcs);
idx = node->count++;
/* idx 값:
 *   0 — 첫 번째 중첩 (프로세스 또는 최외곽 컨텍스트)
 *   1 — 두 번째 중첩 (softirq가 프로세스 위에서)
 *   2 — 세 번째 중첩 (hardirq가 softirq 위에서)
 *   3 — 네 번째 중첩 (NMI가 hardirq 위에서)
 *
 * 4개 이상 중첩은 불가능:
 * - 프로세스/softirq/hardirq/NMI 각각 최대 1회
 * - NMI는 NMI를 중첩하지 않음 (하드웨어 보장)
 */

/* tail 인코딩에 idx 포함: 같은 CPU의 다른 중첩도 구분 가능 */
tail = encode_tail(smp_processor_id(), idx);

PV qspinlock: 가상화(Virtualization) 환경 최적화

가상 머신에서 vCPU는 하이퍼바이저(Hypervisor)에 의해 선점(preempt)될 수 있습니다. 스핀락 보유자 vCPU가 선점되면 다른 vCPU들이 무한히 spinning하는 Lock Holder Preemption(LHP) 문제가 발생합니다.

Lock Holder Preemption (LHP) 문제와 PV qspinlock 해결 문제: 네이티브 qspinlock + vCPU pCPU 0 (스케줄링 중) pCPU 1 (스케줄링 중) vCPU 0 (보유자) 선점됨! (pCPU에서 내려감) unlock 불가능 vCPU 1 (대기자) 무한 spinning! CPU 100% 소모 vCPU 0이 선점되어 unlock을 못 함 → vCPU 1이 영원히 spinning → pCPU 1의 물리 자원 100% 낭비 해결: PV qspinlock ① Spinning 임계값 감지 일정 횟수 spin 실패 → 보유자 선점 의심 ② pv_wait() — vCPU HALT 대기자 vCPU가 자발적으로 HALT → pCPU 해제 ③ 보유자 vCPU 재스케줄링 해제된 pCPU에 보유자 vCPU 배정 → unlock 실행 ④ pv_kick() — 대기자 깨움 unlock 후 하이퍼바이저 IPI로 HALT된 vCPU 깨움 결과: CPU 낭비 없이 데드락 방지 대기 중 pCPU를 다른 vCPU에 양보
PV qspinlock은 spinning 대신 vCPU HALT로 물리 CPU를 양보(Yield)하고, unlock 시 kick으로 깨웁니다
하이퍼바이저wait 구현kick 구현활성화
KVMkvm_hypercall(HALT)kvm_hypercall(KICK)CONFIG_PARAVIRT_SPINLOCKS
XenHYPERVISOR_sched_op(SCHEDOP_poll)HYPERVISOR_vcpu_op(VCPUOP_kick)자동 감지
Hyper-VHvCallSignalEventHvCallSignalEventCONFIG_HYPERV
⚠️

PV 오버헤드(Overhead) 주의: PV qspinlock은 네이티브 대비 ~5~15%의 오버헤드가 있습니다. vCPU 오버커밋(vCPU > pCPU)이 심할수록 PV 이점이 크지만, 1:1 핀닝 환경에서는 불필요한 하이퍼콜 비용만 추가됩니다. vCPU 오버커밋이 없는 환경에서는 no-pv-spinlock 커널 파라미터로 비활성화를 고려하세요.

CNA Lock: NUMA-aware 확장

CNA(Compact NUMA-Aware) Lock은 qspinlock의 MCS 큐를 NUMA 노드 단위로 재정렬합니다. 같은 NUMA 노드의 CPU에게 우선권을 부여하여, 노드 간 캐시 라인 전송 비용을 줄입니다.

/* kernel/locking/qspinlock_cna.h — CONFIG_NUMA_AWARE_SPINLOCKS */

struct cna_node {
    struct mcs_spinlock mcs;
    u16 numa_node;            /* 이 CPU의 NUMA 노드 ID */
    u16 real_tail;             /* secondary 큐의 실제 tail */
    u32 encoded_tail;          /* 큐 위치 인코딩 */
};

/*
 * CNA의 2개 큐:
 *
 * [Main Queue]     — 보유자와 같은 NUMA 노드의 대기자
 *   → unlock 시 이 큐에서 먼저 전달
 *   → 로컬 캐시 라인 전송 (빠름)
 *
 * [Secondary Queue] — 다른 NUMA 노드의 대기자
 *   → main queue가 비거나 임계값 초과 시 병합
 *   → 크로스-노드 캐시 라인 전송 (느림)
 *
 * 기아 방지: SHUFFLE_REDUCTION_PROB_ARG 파라미터로
 * secondary 큐 병합 빈도 조절 (기본값: 약 1/256 확률)
 */

/* NUMA 노드 비교 후 큐 배치 결정 */
static void cna_lock_handoff(struct mcs_spinlock *node,
                              struct mcs_spinlock *next)
{
    struct cna_node *cn = (struct cna_node *)node;
    struct cna_node *cnn = (struct cna_node *)next;

    if (cn->numa_node == cnn->numa_node) {
        /* 같은 NUMA 노드 → main queue 우선 전달 */
        arch_mcs_spin_unlock_contended(&next->locked);
    } else {
        /* 다른 NUMA 노드 → secondary queue로 이동 */
        cna_splice_to_secondary(cn, cnn);
    }
}
시스템 구성qspinlockqspinlock + CNA개선율
2소켓 (64코어)0.55x0.62x~13%
4소켓 (128코어)0.35x0.60x~71%
8소켓 (256코어)0.18x0.48x~167%
💡

CNA 활성화: CONFIG_NUMA_AWARE_SPINLOCKS=y (Linux 5.14+) + 런타임 numa_spinlock=on 커널 파라미터. 2소켓 이하에서는 오버헤드 대비 이점이 적으므로 4소켓 이상에서 권장합니다. 공정성(Fairness) 약화로 인한 기아(Starvation) 위험은 SHUFFLE_REDUCTION_PROB_ARG로 조절합니다.

qrwlock: Reader-Writer 스핀락

qrwlock(queued reader-writer lock)은 qspinlock을 기반으로 구현된 reader-writer 스핀락입니다. 여러 reader가 동시에 보유할 수 있고, writer는 배타적으로 보유합니다.

/* include/asm-generic/qrwlock_types.h */
typedef struct qrwlock {
    union {
        atomic_t cnts;            /* reader count + writer 상태 */
        struct {
            u8 wlocked;           /* bit[0]:   writer 보유중 */
            u8 __lstate[3];
        };
    };
    arch_spinlock_t wait_lock;   /* writer 직렬화용 qspinlock */
} arch_rwlock_t;

/* cnts 비트 인코딩:
 *   bit [0]    : _QW_LOCKED   — writer 보유중
 *   bit [1]    : _QW_WAITING  — writer 대기중 (새 reader 차단)
 *   bit [2:31] : reader count (최대 ~10억)
 */

/* Reader fast path: writer 없으면 count 증가만 */
static inline void queued_read_lock(struct qrwlock *lock)
{
    u32 cnts = atomic_add_return_acquire(_QR_BIAS, &lock->cnts);
    if (likely(!(cnts & _QW_WMASK)))
        return;    /* writer 없음 → 즉시 진입 */
    queued_read_lock_slowpath(lock);
}

/* Writer: wait_lock(qspinlock)으로 직렬화 */
static inline void queued_write_lock(struct qrwlock *lock)
{
    if (atomic_try_cmpxchg_acquire(&lock->cnts, &cnts, _QW_LOCKED))
        return;    /* reader=0, writer 없음 → 즉시 획득 */
    queued_write_lock_slowpath(lock);
}
비교rwlock_t (구형)qrwlock (현재)
reader 대기test-and-set 재시도qspinlock 기반 큐 대기
writer 대기test-and-set 재시도qspinlock MCS 큐 대기
공정성reader 편향 (writer 기아 가능)_QW_WAITING으로 새 reader 차단
캐시 트래픽O(N)O(1) (MCS 로컬 spin)

x86 아키텍처 구현

x86_64는 qspinlock의 주요 타겟 아키텍처로, LOCK 접두사 명령어와 PAUSE 힌트를 활용합니다.

; x86_64 원자적 CAS (cmpxchg)
; atomic_try_cmpxchg(&lock->val, &expected, desired)
lock cmpxchg [rdi], esi      ; LOCK 접두사: 버스 잠금 → 원자적 CAS
                              ; 성공: ZF=1, [rdi]=esi
                              ; 실패: ZF=0, eax=[rdi] (현재 값)

; x86_64 spin 대기 힌트
pause                        ; spin-wait 루프 힌트
                              ; 효과 1: 파이프라인 flush 지연 → 전력 절약
                              ; 효과 2: 메모리 순서 위반 감지 시 파이프라인 재실행 방지
                              ; 효과 3: ~140 cycles 지연 (Skylake+)

; x86_64 바이트 단위 unlock (smp_store_release)
mov byte [rdi], 0            ; locked 바이트를 0으로 설정
                              ; x86 TSO: 모든 이전 store가 먼저 visible
                              ; → 별도 배리어 불필요
ℹ️

x86 TSO(Total Store Order)의 이점: x86의 강한 메모리 모델 덕분에 unlock 시 별도의 메모리 배리어가 필요 없습니다. smp_store_release()는 x86에서 단순 MOV로 컴파일됩니다. 반면 ARM64나 RISC-V의 약한 모델에서는 명시적 release 의미론(STLR, amoswap.w.rl)이 필요합니다.

ARM64 아키텍처 구현

ARM64는 LL/SC(Load-Linked/Store-Conditional) 패턴과 WFE/SEV 이벤트 메커니즘으로 qspinlock을 구현합니다.

// ARM64 원자적 CAS (LDAXR/STXR — LL/SC 패턴)
// atomic_try_cmpxchg_acquire
1:  ldaxr   w1, [x0]         // Load-Acquire Exclusive: 읽기 + acquire 배리어
    cmp     w1, w2            // expected와 비교
    b.ne    2f                // 불일치 → 실패
    stxr    w3, w4, [x0]      // Store Exclusive: 조건부 쓰기
    cbnz    w3, 1b            // exclusive monitor 실패 → 재시도
2:

// ARM64 LSE(Large System Extension) 대안 (ARMv8.1+)
// CAS 명령어로 LL/SC 루프 대체
    casa    w1, w2, [x0]      // Compare-And-Swap Acquire: 단일 명령어 CAS
                              // → LL/SC 대비 성능 향상 (특히 경합 시)

// ARM64 spin 대기 (WFE — Wait For Event)
    sevl                      // Set Event Local: 첫 WFE가 즉시 통과하도록
1:  wfe                       // Wait For Event: 이벤트까지 코어 저전력 대기
    ldaxr   w1, [x0]         // 조건 확인
    cbnz    w1, 1b            // 아직 잠겨있으면 다시 WFE

// ARM64 깨우기 (SEV — Send Event)
    stlr    wzr, [x0]        // Store-Release: locked=0 + release 배리어
                              // exclusive monitor 클리어 → WFE 대기 코어 자동 깨움
💡

WFE vs PAUSE 비교: ARM64의 WFE는 x86의 PAUSE보다 훨씬 적극적인 전력 절감을 제공합니다. PAUSE는 단순히 파이프라인(Pipeline) 힌트인 반면, WFE는 이벤트 발생까지 코어를 저전력 상태(clock gating)로 전환합니다. SEV(Send Event) 또는 exclusive monitor 클리어가 이벤트로 작용하여 대기 코어를 깨웁니다. 이 메커니즘은 모바일/임베디드 환경에서 특히 유리합니다.

RISC-V 아키텍처 구현

RISC-V는 LR/SC(Load-Reserved/Store-Conditional)AMO(Atomic Memory Operation) 두 가지 원자적 명령어 세트를 제공합니다. Linux 5.18부터 qspinlock이 기본입니다.

# RISC-V 원자적 CAS (LR/SC 패턴)
# atomic_try_cmpxchg_acquire
1:  lr.w.aq  a3, (a0)        # Load-Reserved Acquire: 읽기 + 예약
    bne      a3, a1, 2f      # expected와 불일치 → 실패
    sc.w     a4, a2, (a0)    # Store-Conditional: 예약 유효 시 쓰기
    bnez     a4, 1b           # SC 실패 → 재시도
2:

# RISC-V AMO 기반 xchg (xchg_tail)
    amoswap.w.aq  a3, a2, (a0)   # Atomic Swap Acquire
                                   # a3 = 이전 값, (a0) = a2

# RISC-V unlock (store-release)
    amoswap.w.rl  zero, zero, (a0)  # Release: locked=0
    # 또는
    fence    rw, w                   # release fence
    sb       zero, 0(a0)             # locked 바이트 = 0

# RISC-V spin 대기 (WFI는 특권 명령 → 일반적으로 PAUSE 없음)
# → 순수 polling + fence 힌트
1:  lw       a3, 0(a0)       # locked 필드 읽기
    bnez     a3, 1b           # 아직 잠겨있으면 재시도
    # Zihintpause 확장: pause 명령어 (RISC-V ISA 2.0+)
기능x86_64ARM64RISC-V
CAS 명령어LOCK CMPXCHGLDAXR/STXR 또는 CAS(LSE)LR/SC
Swap 명령어XCHGSWP(LSE)AMOSWAP
Spin 힌트PAUSE (~140 cycles)WFE (저전력 대기)pause(Zihintpause, 선택)
Release 의미론TSO: 자동 (MOV만으로 충분)STLR (store-release).rl 접미사 또는 fence
메모리 모델TSO (강한 순서)약한 순서 (acquire/release 필요)RVWMO (약한 순서)
qspinlock 도입4.2 (기본)4.2 (기본)5.18 (기본)

PREEMPT_RT와 spinlock_t 변환

PREEMPT_RT(Real-Time) 커널에서 spinlock_t는 더 이상 busy-wait하지 않습니다. rt_mutex 기반의 sleeping lock으로 자동 변환되어, 우선순위 상속(Priority Inheritance)을 지원합니다.

/* include/linux/spinlock_rt.h — PREEMPT_RT 빌드 */

/* spinlock_t은 rt_mutex 래퍼로 재정의 */
typedef struct spinlock {
    struct rt_mutex_base lock;
    unsigned int break_lock;  /* 디버그용 */
} spinlock_t;

/* spin_lock()은 RT에서 rt_mutex_lock()으로 매핑 */
static inline void spin_lock(spinlock_t *lock)
{
    rt_mutex_lock(&lock->lock);
    /* 슬립 가능! 우선순위 상속 적용
     * - 경합 시 보유자의 우선순위를 대기자 수준으로 boosting
     * - preemptible (스케줄러 호출 가능)
     * - 인터럽트 비활성화하지 않음
     */
}

/* raw_spin_lock()만 진정한 busy-wait 유지 */
static inline void raw_spin_lock(raw_spinlock_t *lock)
{
    __raw_spin_lock(&lock->raw_lock);
    /* RT에서도 qspinlock busy-wait
     * 스케줄러, 타이머, 인터럽트 컨트롤러 등 핵심 경로 전용
     */
}
특성spinlock_t (일반)spinlock_t (PREEMPT_RT)raw_spinlock_t (RT)
대기 방식busy-waitsleep (rt_mutex)busy-wait
Sleep 허용불가가능불가
우선순위 상속없음있음 (PI)없음
Preemptible아니오아니오
IRQ 핸들러(Handler) 내사용 가능사용 가능 (IRQ 스레드화)사용 가능
크기4 bytes~40 bytes4 bytes
용도범용범용 (RT에서 자동 변환)핵심 경로만 (~100개)
ℹ️

RT에서 IRQ 핸들러 변화: PREEMPT_RT에서는 대부분의 인터럽트 핸들러가 커널 스레드(Kernel Thread)로 실행됩니다(IRQF_NO_THREAD 제외). 따라서 IRQ 핸들러 내에서도 sleeping lock(spinlock_t)을 사용할 수 있습니다. spin_lock_irq()/spin_lock_irqsave()는 RT에서 인터럽트를 실제로 비활성화하지 않고, 대신 local_lock이나 rt_mutex 경로를 사용합니다.

성능 특성과 벤치마크

4소켓 NUMA 시스템(총 128코어)에서 will-it-scalelock1 벤치마크를 기준으로 한 성능 비교입니다.

경합 시나리오test-and-setticket spinlockqspinlockqspinlock+CNA
경합 없음 (1 CPU)1.00x (기준)0.98x0.97x0.96x
경경합 (4 CPU, 같은 노드)0.85x0.90x0.95x0.95x
중경합 (16 CPU, 2노드)0.30x0.45x0.80x0.88x
고경합 (64 CPU, 4노드)0.05x0.15x0.55x0.75x
극심 경합 (128 CPU, 4노드)0.02x0.08x0.35x0.60x
ℹ️

핵심 해석: 경합 없는 단일 CPU에서는 모든 구현이 거의 동일합니다(~20 cycles). 격차는 CPU 수와 NUMA 토폴로지(Topology)가 복잡해질수록 벌어집니다. qspinlock은 ticket 대비 중경합에서 2~4배, CNA 추가 시 4~7배 더 높은 처리량(Throughput)을 보입니다. 핵심 지표: unlock당 캐시 라인 무효화 — TAS O(N²), ticket O(N), qspinlock O(1).

Fast path 비용 분석

연산x86_64 (cycles)ARM64 (cycles)비고
경합 없는 lock~20~15단일 CAS 성공
경합 없는 unlock~5~8바이트 store
lock+unlock 왕복~25~23fast path 전체
Pending path (2 CPU)~100-500~80-400대기 시간에 따라 변동
MCS path (3+ CPU)~200-2000~150-1500큐 깊이에 따라 변동

실전 사용 패턴

패턴 1: IRQ 핸들러와 데이터 공유

/* 프로세스 컨텍스트와 IRQ 핸들러가 같은 데이터 접근 */
DEFINE_SPINLOCK(dev_lock);
struct device_state {
    u32 status;
    u32 pending_irqs;
    struct list_head rx_queue;
};

/* IRQ 핸들러 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct device_state *state = dev_id;
    unsigned long flags;

    spin_lock_irqsave(&dev_lock, flags);
    state->pending_irqs++;
    /* ... 데이터 처리 ... */
    spin_unlock_irqrestore(&dev_lock, flags);

    return IRQ_HANDLED;
}

/* 프로세스 컨텍스트 — 반드시 irqsave/irqrestore 사용 */
void process_rx_data(struct device_state *state)
{
    unsigned long flags;

    spin_lock_irqsave(&dev_lock, flags);
    /* 이 구간에서 IRQ 비활성화 → IRQ 핸들러 진입 불가
     * → 같은 CPU에서 데드락 방지 */
    while (!list_empty(&state->rx_queue)) {
        /* ... */
    }
    spin_unlock_irqrestore(&dev_lock, flags);
}

패턴 2: 다중 잠금 순서 규칙

/* 다중 잠금 — 항상 동일한 순서로 획득 (ABBA 데드락 방지) */
DEFINE_SPINLOCK(lock_a);
DEFINE_SPINLOCK(lock_b);

/* 올바른 순서: 항상 A → B */
void correct_order(void)
{
    spin_lock(&lock_a);    /* 1st: A */
    spin_lock(&lock_b);    /* 2nd: B */
    /* critical section */
    spin_unlock(&lock_b);
    spin_unlock(&lock_a);
}

/* ✗ 잘못된 순서 — 데드락 위험!
 * CPU 0: lock_a → lock_b (대기)
 * CPU 1: lock_b → lock_a (대기) → ABBA 데드락!
 */
void wrong_order(void)     /* 절대 이렇게 하지 마세요! */
{
    spin_lock(&lock_b);    /* 1st: B (순서 역전!) */
    spin_lock(&lock_a);    /* 2nd: A → 데드락 가능 */
    spin_unlock(&lock_a);
    spin_unlock(&lock_b);
}

패턴 3: Per-CPU 데이터 + spinlock 조합

/* 경합 최소화: per-CPU 데이터 + 전역 집계용 spinlock */
struct percpu_stats {
    u64 packets;
    u64 bytes;
    u64 errors;
};
static DEFINE_PER_CPU(struct percpu_stats, cpu_stats);
static DEFINE_SPINLOCK(global_stats_lock);

/* Hot path: per-CPU 업데이트 — 잠금 없이 빠르게 */
void fast_update(u32 bytes)
{
    this_cpu_inc(cpu_stats.packets);
    this_cpu_add(cpu_stats.bytes, bytes);
}

/* Cold path: 전역 집계 — 모든 CPU 합산 시에만 spinlock */
void get_global_stats(struct percpu_stats *total)
{
    int cpu;
    memset(total, 0, sizeof(*total));

    spin_lock(&global_stats_lock);
    for_each_possible_cpu(cpu) {
        struct percpu_stats *s = per_cpu_ptr(&cpu_stats, cpu);
        total->packets += READ_ONCE(s->packets);
        total->bytes += READ_ONCE(s->bytes);
    }
    spin_unlock(&global_stats_lock);
}

안티패턴과 흔한 실수

안티패턴 1: spinlock 보유 중 슬립

/* ✗ 치명적 실수: spinlock 보유 중 슬립 가능 함수 호출 */
spin_lock(&my_lock);

/* 다음은 모두 슬립 가능 → 데드락! */
buf = kmalloc(1024, GFP_KERNEL);     /* ✗ GFP_KERNEL은 슬립 가능 */
copy_from_user(buf, ubuf, len);      /* ✗ 페이지 폴트 → 슬립 */
mutex_lock(&other_mutex);            /* ✗ 경합 시 슬립 */
msleep(10);                          /* ✗ 명시적 슬립 */

spin_unlock(&my_lock);

/* ✓ 올바른 대안 */
spin_lock(&my_lock);
buf = kmalloc(1024, GFP_ATOMIC);    /* ✓ GFP_ATOMIC은 슬립 불가 */
spin_unlock(&my_lock);

/* 또는: spinlock 밖에서 할당 후 안에서 사용 */
buf = kmalloc(1024, GFP_KERNEL);    /* ✓ spinlock 밖 */
spin_lock(&my_lock);
/* buf 사용 */
spin_unlock(&my_lock);

안티패턴 2: IRQ 공유 락에서 IRQ 비활성화 누락

/* ✗ 데드락 시나리오:
 * CPU 0: spin_lock(&dev_lock) 보유 중
 * CPU 0: IRQ 발생 → IRQ 핸들러에서 spin_lock(&dev_lock) 시도
 * → 같은 CPU에서 자기 자신을 기다림 → 데드락! */

/* ✗ 잘못: IRQ와 공유하는 락에 spin_lock() 사용 */
spin_lock(&dev_lock);          /* IRQ 비활성화 안 함! */
/* ... 여기서 IRQ 발생하면 데드락 ... */
spin_unlock(&dev_lock);

/* ✓ 올바름: spin_lock_irqsave() 사용 */
unsigned long flags;
spin_lock_irqsave(&dev_lock, flags);   /* ✓ IRQ 비활성화 */
/* ... IRQ 발생 불가 → 안전 ... */
spin_unlock_irqrestore(&dev_lock, flags);

안티패턴 3: spinlock 과도하게 긴 보유

/* ✗ spinlock을 오래 보유하면 다른 CPU 전체가 busy-wait */
spin_lock(&my_lock);
for (i = 0; i < 10000; i++) {
    /* 긴 루프 작업... */
    heavy_computation(data[i]);     /* ✗ 수천 cycles */
}
spin_unlock(&my_lock);

/* ✓ 개선: 배치 처리 + 잠금 범위 최소화 */
for (i = 0; i < 10000; i += 32) {
    spin_lock(&my_lock);
    for (j = i; j < min(i + 32, 10000); j++)
        batch[j - i] = data[j];      /* 짧게: 데이터 복사만 */
    spin_unlock(&my_lock);
    for (j = 0; j < 32; j++)
        heavy_computation(batch[j]);  /* ✓ 잠금 밖에서 연산 */
}

lockdep 통합 내부 구현

spinlock의 모든 lock/unlock 호출에는 lockdep(Lock Dependency Validator)의 콜백이 삽입됩니다. lockdep은 실행 시점에 잠금 의존성 그래프를 구축하고, 순환 의존성(잠재적 데드락)을 탐지합니다.

spin_lock_nested() — 동일 클래스 중첩 잠금

/* include/linux/spinlock.h */
static inline void spin_lock_nested(spinlock_t *lock,
                                     unsigned int subclass)
{
    raw_spin_lock_nested(&lock->rlock, subclass);
}

/* 내부 전개: __raw_spin_lock와 동일하지만 spin_acquire에 subclass 전달 */
static inline void __raw_spin_lock_nested(raw_spinlock_t *lock,
                                            int subclass)
{
    preempt_disable();
    spin_acquire(&lock->dep_map,
                 subclass,      /* ★ lockdep 서브클래스 지정 */
                 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

/* 사용 사례: 스케줄러 double rq locking */
raw_spin_lock(&rq1->__lock);
raw_spin_lock_nested(&rq2->__lock, SINGLE_DEPTH_NESTING);
/*
 * SINGLE_DEPTH_NESTING = 1
 * lockdep에게 "같은 클래스(rq->__lock)의 2번째 인스턴스를
 * 의도적으로 보유한다"고 알림
 * → 서브클래스 0과 1은 별도 의존성 노드로 취급
 * → ABBA 데드락 미탐지 문제 방지
 *
 * 서브클래스 없이 같은 클래스의 lock을 2개 보유하면:
 *   lockdep 경고: "possible recursive locking detected"
 */

/* 다중 서브클래스 예시: 네트워크 소켓 중첩 */
enum sock_lock_subclass {
    SLOCK_NORMAL,        /* 서브클래스 0: 일반 소켓 */
    SLOCK_LISTENER,      /* 서브클래스 1: 리스너 소켓 */
    SLOCK_TIMER,         /* 서브클래스 2: 타이머 소켓 */
};
spin_lock_nested(&sk->sk_lock.slock, SLOCK_LISTENER);

lockdep_set_class() / lockdep_set_subclass()

/* lockdep 잠금 클래스 관리 API */

/* lockdep_set_class: 런타임에 잠금 클래스 변경
 * 같은 타입의 lock이지만 다른 용도로 사용될 때 */
static struct lock_class_key my_special_key;

spin_lock_init(&my_lock);
lockdep_set_class(&my_lock, &my_special_key);
/*
 * DEFINE_SPINLOCK으로 선언하면 선언 위치 기준으로 클래스가 자동 결정
 * 하지만 동적으로 할당된 구조체 배열의 lock은 모두 같은 클래스가 됨
 * → lockdep이 배열 원소 간 중첩을 "recursive locking"으로 오해
 * → set_class로 각각 다른 클래스를 부여하여 해결
 */

/* lockdep_set_subclass: 서브클래스만 변경 (클래스는 유지) */
lockdep_set_subclass(&my_lock, 1);
/* spin_lock_nested() 호출마다 서브클래스를 지정하는 대신,
 * lock 자체에 서브클래스를 영구 설정 */

/* lockdep_set_class_and_name: 클래스 + 이름 동시 설정 */
lockdep_set_class_and_name(&inode->i_lock,
                           &inode_lock_key,
                           "&inode->i_lock");
/* /proc/lockdep에서 이 이름으로 표시됨
 * → 디버깅 시 어떤 lock인지 빠르게 식별 가능 */

lockdep 의존성 추적 메커니즘

/* include/linux/lockdep_types.h */
struct lockdep_map {
    struct lock_class_key  *key;       /* 잠금 클래스 식별자 */
    struct lock_class      *class_cache[2]; /* 최근 클래스 캐시 */
    const char            *name;       /* 잠금 이름 (디버그용) */
    short                  wait_type_outer;
    short                  wait_type_inner;
    u8                     lock_type;
};

/*
 * lockdep 동작 원리 (간략):
 *
 * 1. spin_lock(A) → lockdep이 현재 태스크의 held_locks[]에 A 추가
 * 2. spin_lock(B) → held_locks[]에 B 추가
 *    → 의존성 엣지 추가: A → B (A를 보유한 채 B 획득)
 *
 * 3. 다른 컨텍스트에서 spin_lock(B) → spin_lock(A)
 *    → 의존성 엣지: B → A
 *    → 순환 감지: A → B → A (잠재적 데드락!)
 *    → "possible circular locking dependency detected" 경고 출력
 *
 * 핵심: 실제 데드락이 발생하기 전에, 의존성 그래프만으로
 *       잠재적 데드락을 탐지 (한 번이라도 해당 순서가 실행되면)
 */

/* spin_acquire 내부 (CONFIG_PROVE_LOCKING=y): */
static inline void spin_acquire(struct lockdep_map *dep_map,
                                 int subclass, int trylock,
                                 unsigned long ip)
{
    lock_acquire(dep_map,
                 subclass,        /* 서브클래스 (nested locking) */
                 trylock,         /* 1이면 trylock → 의존성 엣지 미추가 */
                 0,               /* read: 0=exclusive, 1=shared */
                 1,               /* check: 의존성 검사 수행 여부 */
                 NULL,            /* nest_lock: 부모 lock (보통 NULL) */
                 ip);             /* 호출 위치 (backtrace용) */
}

/* spin_release 내부: */
static inline void spin_release(struct lockdep_map *dep_map,
                                  unsigned long ip)
{
    lock_release(dep_map, ip);
    /* held_locks[]에서 이 lock 제거
     * unlock 순서도 검증 (LIFO 위반 시 경고) */
}

CONFIG_DEBUG_SPINLOCK 내부 구현

CONFIG_DEBUG_SPINLOCK은 스핀락의 오용을 런타임에 탐지하는 디버그 기능입니다. 매직 넘버 검증, 소유자 추적, 재귀 잠금 탐지를 수행합니다.

/* include/linux/spinlock_types.h — DEBUG 필드 */
typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned int magic;        /* SPINLOCK_MAGIC = 0xdead4ead */
    unsigned int owner_cpu;    /* 보유자 CPU ID (-1 = 미보유) */
    void *owner;               /* 보유자 task_struct (-1 = 미보유) */
#endif
} raw_spinlock_t;

/* kernel/locking/spinlock_debug.c */
static void spin_dump(raw_spinlock_t *lock, const char *msg)
{
    printk(KERN_EMERG "BUG: spinlock %s on CPU#%d, %s/%d\n",
           msg, raw_smp_processor_id(),
           current->comm, current->pid);
    printk(KERN_EMERG " lock: %pS, .magic: %08x, "
           ".owner: %s/%d, .owner_cpu: %d\n",
           lock, lock->magic,
           lock->owner ? lock->owner->comm : "<none>",
           lock->owner ? task_pid_nr(lock->owner) : -1,
           lock->owner_cpu);
    dump_stack();
}

/* do_raw_spin_lock의 디버그 버전 */
void do_raw_spin_lock(raw_spinlock_t *lock)
{
    debug_spin_lock_before(lock);  /* 사전 검증 */
    arch_spin_lock(&lock->raw_lock);
    debug_spin_lock_after(lock);   /* 사후 기록 */
}

static inline void debug_spin_lock_before(raw_spinlock_t *lock)
{
    /* ① 매직 넘버 검증: 초기화되지 않은 lock 사용 탐지 */
    SPIN_BUG_ON(lock->magic != SPINLOCK_MAGIC, lock,
                "bad magic");
    /* magic이 0xdead4ead가 아닌 경우:
     * - memset/kfree 후 재사용 (use-after-free)
     * - spin_lock_init() 호출 누락
     * - 스택 변수의 초기화 누락 */

    /* ② 재귀 잠금 탐지: 같은 CPU에서 같은 lock 재획득 시도 */
    SPIN_BUG_ON(lock->owner == current, lock,
                "recursion");
    /* spinlock은 재귀 불가! (mutex도 마찬가지)
     * 같은 태스크가 같은 lock을 두 번 호출 → 데드락 */

    /* ③ 소유자 CPU 검증: 같은 CPU에서 다른 태스크가 보유 중? */
    SPIN_BUG_ON(lock->owner_cpu == raw_smp_processor_id(),
                lock, "cpu recursion");
}

static inline void debug_spin_lock_after(raw_spinlock_t *lock)
{
    /* lock 획득 성공 후 소유자 정보 기록 */
    lock->owner_cpu = raw_smp_processor_id();
    lock->owner = current;
}

static inline void debug_spin_unlock(raw_spinlock_t *lock)
{
    /* unlock 시 검증 */
    SPIN_BUG_ON(lock->magic != SPINLOCK_MAGIC, lock,
                "bad magic");
    SPIN_BUG_ON(!raw_spin_is_locked(lock), lock,
                "already unlocked");  /* 이중 해제 탐지 */
    SPIN_BUG_ON(lock->owner != current, lock,
                "wrong owner");      /* 보유자가 아닌 태스크가 해제 */
    SPIN_BUG_ON(lock->owner_cpu != raw_smp_processor_id(),
                lock, "wrong CPU");  /* 다른 CPU에서 해제 시도 */

    /* 소유자 정보 클리어 */
    lock->owner = SPINLOCK_OWNER_INIT;    /* (void *)-1 */
    lock->owner_cpu = -1;
}
탐지 항목검증 시점커널 메시지원인
초기화 누락lock 시도"bad magic"spin_lock_init() 미호출, use-after-free
재귀 잠금lock 시도"recursion"같은 태스크가 같은 lock 재획득
이중 해제unlock 시도"already unlocked"spin_unlock() 이중 호출
잘못된 해제자unlock 시도"wrong owner"lock 보유자가 아닌 태스크가 해제
CPU 불일치unlock 시도"wrong CPU"lock 획득 CPU와 다른 CPU에서 해제 (마이그레이션?)
⚠️

DEBUG_SPINLOCK 오버헤드: 매 lock/unlock마다 3~5회의 조건 검사와 2회의 메모리 쓰기(owner, owner_cpu)가 추가됩니다. sizeof(raw_spinlock_t)도 4바이트에서 16~24바이트로 증가합니다. 개발/테스트 빌드에서만 활성화하고, 프로덕션에서는 CONFIG_PROVE_LOCKING(lockdep)을 대신 사용하는 것이 성능-안전성 균형에 유리합니다.

디버깅과 트레이싱

Lock Stat: /proc/lock_stat

# CONFIG_LOCK_STAT=y 필요

# 경합이 가장 심한 잠금 상위 10개
sort -k2 -rn /proc/lock_stat | head -20

# 주요 컬럼:
# con-bounces   — 경합 발생 횟수
# contentions   — 경합 대기 총 횟수
# waittime-avg  — 평균 대기 시간 (ns)
# holdtime-avg  — 평균 보유 시간 (ns)
# acquisitions  — 총 획득 횟수

# 통계 리셋
echo 0 > /proc/lock_stat

perf lock: 잠금 프로파일링

# 10초간 잠금 이벤트 기록
perf lock record -a -- sleep 10

# 경합 보고서
perf lock report

# 출력 예시:
#                         Name    acquired  contended     avg wait (ns)
# &rcu_node_0/0              15234         87           1243
# &dev->queue_lock           8921        412           3567

# contention 트레이싱 (perf lock con)
perf lock con record -a -- sleep 5
perf lock con report

# bpftrace로 특정 잠금 추적
bpftrace -e '
kprobe:queued_spin_lock_slowpath {
    @contention[kstack] = count();
}
'

lockdep 통합

/* lockdep 어노테이션 — 올바른 잠금 사용 문서화 */

/* 1. 잠금 보유 확인 (런타임 검증) */
lockdep_assert_held(&my_lock);

/* 2. 잠금 미보유 확인 */
lockdep_assert_not_held(&my_lock);

/* 3. 중첩 잠금 — 같은 lock class의 다중 인스턴스 */
spin_lock_nested(&child->lock, SINGLE_DEPTH_NESTING);
/* lockdep에게 "이 lock은 parent->lock의 하위" 알림 */

/* 4. lock class 수동 지정 — 동적 할당 잠금 */
static struct lock_class_key my_class;
lockdep_set_class(&my_lock, &my_class);
# lockdep 관련 커널 옵션
CONFIG_PROVE_LOCKING=y        # 잠금 순서 위반 감지
CONFIG_LOCK_STAT=y            # /proc/lock_stat 통계
CONFIG_DEBUG_LOCK_ALLOC=y     # 잠금 할당 추적
CONFIG_DEBUG_SPINLOCK=y       # spinlock 디버그 (magic/owner)
CONFIG_DEBUG_ATOMIC_SLEEP=y   # atomic 구간에서 sleep 감지

# lockdep 상태 확인
cat /proc/lockdep_stats
cat /proc/lockdep_chains

관련 커널 설정 옵션

옵션기본값설명
CONFIG_QUEUED_SPINLOCKSy (x86, ARM64)qspinlock 활성화 (ticket lock 대체)
CONFIG_QUEUED_RWLOCKSyqrwlock 활성화
CONFIG_PARAVIRT_SPINLOCKSy (가상화)PV qspinlock 활성화
CONFIG_NUMA_AWARE_SPINLOCKSnCNA lock 활성화 (5.14+)
CONFIG_PREEMPT_RTnspinlock_t → rt_mutex 변환
CONFIG_DEBUG_SPINLOCKnspinlock 디버그 정보 (owner, magic)
CONFIG_PROVE_LOCKINGnlockdep 잠금 순서 검증
CONFIG_LOCK_STATn/proc/lock_stat 경합 통계
CONFIG_DEBUG_LOCK_ALLOCn잠금 할당/해제 추적
CONFIG_DEBUG_ATOMIC_SLEEPn원자적 구간에서 sleep 경고
💡

개발 커널 권장 옵션: CONFIG_PROVE_LOCKING=y + CONFIG_DEBUG_ATOMIC_SLEEP=y + CONFIG_DEBUG_SPINLOCK=y를 동시에 활성화하면 데드락, sleep-in-atomic, spinlock 오용을 조기에 탐지할 수 있습니다. 성능 오버헤드가 있으므로 프로덕션에서는 비활성화하세요.

스핀락과 메모리 순서 보장

spinlock의 lock/unlock은 암묵적 메모리 배리어를 포함합니다. 이는 임계 영역 내의 메모리 접근이 밖으로 "새어나가지(leak out)" 않도록 보장합니다.

/* spinlock의 암묵적 메모리 순서 보장 */

/* spin_lock()은 ACQUIRE 의미론:
 * - lock 이후의 모든 메모리 접근이 lock 이전으로 재배치되지 않음
 * - 임계 영역의 읽기/쓰기가 lock 호출 전에 시작되지 않음 보장 */
spin_lock(&lock);       /* ← ACQUIRE barrier */
  /* 임계 영역: 여기의 접근은 lock 전으로 이동 불가 */
  x = READ_ONCE(shared_data);
  WRITE_ONCE(shared_data, x + 1);
  /* 임계 영역: 여기의 접근은 unlock 후로 이동 불가 */
spin_unlock(&lock);     /* ← RELEASE barrier */
/* spin_unlock()은 RELEASE 의미론:
 * - unlock 이전의 모든 메모리 접근이 unlock 이후로 재배치되지 않음
 * - 임계 영역의 읽기/쓰기가 unlock 호출 후에 완료되지 않음 보장 */

/* 실제 구현:
 * x86:   lock은 LOCK 접두사(full barrier), unlock은 MOV(TSO가 순서 보장)
 * ARM64: lock은 LDAXR(acquire), unlock은 STLR(release)
 * RISC-V: lock은 .aq(acquire), unlock은 .rl(release)
 */
spinlock 메모리 순서 보장 (ACQUIRE/RELEASE) lock 이전 메모리 접근 spin_lock() — ACQUIRE barrier 임계 영역 (Critical Section) ↑ lock 전으로 이동 불가 ↓ unlock 후로 이동 불가 spin_unlock() — RELEASE barrier unlock 이후 메모리 접근 차단 차단
ACQUIRE 배리어는 임계 영역 접근이 lock 전으로, RELEASE 배리어는 unlock 후로 이동하는 것을 방지합니다

캐시 라인과 False Sharing

spinlock 관련 데이터 구조를 설계할 때 캐시 라인 배치가 중요합니다. 서로 다른 CPU가 접근하는 데이터가 같은 캐시 라인에 있으면 false sharing이 발생하여 성능이 크게 저하됩니다.

/* ✗ False sharing 발생 구조 */
struct bad_layout {
    spinlock_t lock;        /* 4 bytes */
    u64 hot_counter;        /* 8 bytes — lock과 같은 캐시 라인! */
    u64 other_data;         /* 같은 캐시 라인 */
};
/* lock 해제 시 hot_counter의 캐시 라인도 무효화됨 */

/* ✓ 캐시 라인 분리 */
struct good_layout {
    spinlock_t lock;
    /* 패딩으로 lock을 독립 캐시 라인에 배치 */
} ____cacheline_aligned_in_smp;

/* 또는 명시적 패딩 */
struct aligned_lock {
    spinlock_t lock;
    u8 __pad[L1_CACHE_BYTES - sizeof(spinlock_t)];
    u64 hot_counter;       /* 별도 캐시 라인 */
};

trylock 활용 패턴

spin_trylock()은 잠금 획득에 실패해도 대기하지 않고 즉시 반환합니다. 데드락 회피, 적응적 알고리즘, 폴링(Polling) 루프에서 유용합니다.

/* 패턴 1: 경합 시 대안 경로 — 잠금 회피 */
if (spin_trylock(&global_lock)) {
    /* 잠금 획득 성공: 전역 구조 업데이트 */
    update_global_stats(data);
    spin_unlock(&global_lock);
} else {
    /* 경합 중: per-CPU 버퍼에 지연 저장 */
    buffer_to_percpu(data);
}

/* 패턴 2: 역순 잠금 시도 (데드락 회피) */
spin_lock(&lock_a);
if (!spin_trylock(&lock_b)) {
    /* lock_b 획득 실패: lock_a 해제 후 올바른 순서로 재시도 */
    spin_unlock(&lock_a);
    spin_lock(&lock_b);
    spin_lock(&lock_a);
}
/* 두 잠금 모두 보유 */

/* 패턴 3: NMI/hardirq에서 안전한 접근 */
static irqreturn_t nmi_handler(int irq, void *dev)
{
    if (raw_spin_trylock(&nmi_lock)) {
        /* 획득 성공: 안전하게 처리 */
        process_nmi_data();
        raw_spin_unlock(&nmi_lock);
    }
    /* 실패: 이 NMI 데이터는 버림 (NMI에서 대기 불가) */
    return IRQ_HANDLED;
}

bit_spin_lock: 비트 단위 스핀락

bit_spin_lock()은 기존 데이터 구조의 플래그 비트 하나를 스핀락으로 활용하는 초경량 잠금입니다. 별도 spinlock_t 변수(4바이트) 없이 기존 unsigned long 필드의 특정 비트를 사용하므로, 메모리가 극도로 중요한 구조체(page, buffer_head 등)에서 사용됩니다.

/* include/linux/bit_spinlock.h */

static inline void bit_spin_lock(int bitnum, unsigned long *addr)
{
    /*
     * bitnum: 사용할 비트 번호 (0-based)
     * addr: 비트가 위치한 unsigned long 변수의 주소
     *
     * 동작: addr의 bitnum 비트를 test-and-set으로 원자적 설정
     *       이미 설정되어 있으면 해제될 때까지 spinning
     */
    preempt_disable();
#if defined(CONFIG_SMP) || defined(CONFIG_DEBUG_SPINLOCK)
    while (unlikely(test_and_set_bit_lock(bitnum, addr))) {
        preempt_enable();
        do {
            cpu_relax();
        } while (test_bit(bitnum, addr));
        preempt_disable();
    }
#endif
    __acquire(bitlock);    /* sparse 정적 분석 힌트 */
}

static inline void bit_spin_unlock(int bitnum, unsigned long *addr)
{
#ifdef CONFIG_DEBUG_SPINLOCK
    BUG_ON(!test_bit(bitnum, addr));  /* 미보유 상태에서 해제 시도 검출 */
#endif
    __release(bitlock);
    clear_bit_unlock(bitnum, addr);  /* release 의미론으로 비트 클리어 */
    preempt_enable();
}

static inline int bit_spin_trylock(int bitnum, unsigned long *addr)
{
    preempt_disable();
    if (unlikely(test_and_set_bit_lock(bitnum, addr))) {
        preempt_enable();
        return 0;    /* 실패 */
    }
    __acquire(bitlock);
    return 1;            /* 성공 */
}

static inline int bit_spin_is_locked(int bitnum, unsigned long *addr)
{
    return test_bit(bitnum, addr);
}
비교spinlock_tbit_spin_lock
크기4바이트 (별도 변수)0바이트 (기존 비트 재사용)
lockdep 지원완전 지원미지원 (의존성 추적 불가)
MCS 큐qspinlock (O(1))TAS 기반 (불공정, O(N))
PREEMPT_RTrt_mutex 변환변환 불가 (진짜 busy-wait만)
사용 사례범용page flags, buffer_head, hlist_bl
/* 커널 내 bit_spin_lock 사용 사례 */

/* 1. struct page의 PG_locked 비트 — 가장 대표적 */
bit_spin_lock(PG_locked, &page->flags);
/* ... 페이지 메타데이터 수정 ... */
bit_spin_unlock(PG_locked, &page->flags);
/* page 구조체는 시스템 메모리의 ~1.5%를 차지
 * (4KB 페이지당 64바이트 = 1/64)
 * → spinlock_t 4바이트 추가도 ~0.1% 메모리 증가
 * → 수십만 개의 page에서는 상당한 차이 */

/* 2. hlist_bl_head — 해시 테이블 버킷 단위 잠금 */
struct hlist_bl_head {
    struct hlist_bl_node *first;
    /* first 포인터의 bit 0을 잠금으로 사용!
     * 포인터는 항상 4바이트 이상 정렬 → bit 0은 항상 0
     * → 잠금용 비트로 재활용 가능 (기발한 트릭) */
};

static inline void hlist_bl_lock(struct hlist_bl_head *b)
{
    bit_spin_lock(0, (unsigned long *)&b->first);
}

/* 3. buffer_head의 BH_Uptodate_Lock */
bit_spin_lock(BH_Uptodate_Lock, &bh->b_state);
⚠️

bit_spin_lock 제한사항: lockdep 미지원으로 데드락 탐지가 불가능하고, TAS 기반이므로 높은 경합에서 공정성이 보장되지 않습니다. PREEMPT_RT에서도 sleeping lock으로 변환되지 않아 RT 호환성 문제가 있습니다. 새 코드에서는 가능하면 spinlock_t를 우선 사용하고, 구조체 크기가 극도로 중요한 경우에만 bit_spin_lock을 고려하세요.

local_lock_t: per-CPU 보호 프리미티브

local_lock_t는 Linux 5.8에서 도입된 per-CPU 데이터 보호 전용 프리미티브입니다. 일반 커널에서는 preempt_disable()/local_bh_disable()의 래퍼이지만, PREEMPT_RT에서는 per-CPU spinlock_t로 변환되어 RT 호환성을 제공합니다.

/* include/linux/local_lock.h */

/* 일반 커널: 타입만 있고 실제 잠금 변수 없음 */
typedef struct {
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
    struct task_struct *owner;
#endif
} local_lock_t;

/* PREEMPT_RT: 실제 spinlock 포함 */
typedef struct {
    spinlock_t lock;       /* RT에서는 rt_mutex 기반 */
    struct lockdep_map dep_map;
    struct task_struct *owner;
} local_lock_t;

/* 선언 매크로 */
static DEFINE_PER_CPU(local_lock_t, my_local_lock);

/* ===== 일반 커널에서의 전개 ===== */
#ifndef CONFIG_PREEMPT_RT

#define local_lock(lock)                                 \
do {                                                       \
    preempt_disable();                                    \
    __acquire(lock);                                      \
} while (0)

#define local_unlock(lock)                               \
do {                                                       \
    __release(lock);                                      \
    preempt_enable();                                     \
} while (0)

#define local_lock_irqsave(lock, flags)                   \
do {                                                       \
    local_irq_save(flags);                                \
    __acquire(lock);                                      \
} while (0)

#else

/* ===== PREEMPT_RT에서의 전개 ===== */
#define local_lock(lock)                                 \
do {                                                       \
    spin_lock(this_cpu_ptr(lock));                       \
} while (0)
/* RT: per-CPU spinlock으로 보호 → sleeping 가능, PI 지원 */

#endif
/* local_lock 사용 예시: per-CPU 통계 보호 */

static DEFINE_PER_CPU(local_lock_t, stats_lock);
static DEFINE_PER_CPU(struct cpu_stats, stats);

void update_stats(u64 value)
{
    local_lock(&stats_lock);
    /* 일반 커널: preempt_disable()만
     *   → 같은 CPU에서 선점되어 다른 태스크가 이 데이터에 접근하는 것 방지
     *   → 다른 CPU는 자기 per-CPU 데이터에 접근하므로 경합 없음
     * PREEMPT_RT: spin_lock(this_cpu_ptr(&stats_lock))
     *   → sleeping lock으로 보호, 우선순위 상속 지원 */
    this_cpu_ptr(&stats)->total += value;
    this_cpu_ptr(&stats)->count++;
    local_unlock(&stats_lock);
}

/* local_lock vs spin_lock 선택 기준:
 * - per-CPU 데이터만 보호 → local_lock (경합 없음, 최소 오버헤드)
 * - 여러 CPU가 공유하는 데이터 → spin_lock (상호 배제 필요)
 * - local_lock은 "이 CPU에서 선점/softirq를 막는다"는 의도를 명시
 *   → 단순 preempt_disable()보다 lockdep 지원 + RT 호환 */
API일반 커널PREEMPT_RT용도
local_lock()preempt_disable()spin_lock(per_cpu)프로세스 컨텍스트 per-CPU 보호
local_lock_irq()local_irq_disable()spin_lock_irq(per_cpu)IRQ와 per-CPU 데이터 공유
local_lock_irqsave()local_irq_save()spin_lock_irqsave(per_cpu)IRQ 상태 불확실 시
local_lock_bh()local_bh_disable()spin_lock_bh(per_cpu)softirq와 per-CPU 데이터 공유

spinlock과 RCU 상호작용

spinlock과 RCU는 자주 함께 사용됩니다. 일반적 패턴: 읽기 경로는 RCU로 잠금 없이, 쓰기 경로는 spinlock으로 직렬화(Serialization)합니다.

/* 전형적 RCU + spinlock 패턴 */
DEFINE_SPINLOCK(list_lock);
struct list_head my_list;

/* 읽기 경로: RCU (잠금 없이, 여러 CPU 동시 접근 가능) */
void read_list(void)
{
    struct my_entry *entry;

    rcu_read_lock();
    list_for_each_entry_rcu(entry, &my_list, node) {
        /* entry 읽기 — 잠금 없이 안전 */
        process(entry->data);
    }
    rcu_read_unlock();
}

/* 쓰기 경로: spinlock + RCU 업데이트 */
void add_entry(struct my_entry *new_entry)
{
    spin_lock(&list_lock);
    list_add_rcu(&new_entry->node, &my_list);
    spin_unlock(&list_lock);
}

void remove_entry(struct my_entry *entry)
{
    spin_lock(&list_lock);
    list_del_rcu(&entry->node);
    spin_unlock(&list_lock);

    synchronize_rcu();     /* 모든 RCU reader 완료 대기 */
    kfree(entry);          /* 이제 안전하게 해제 */
}

Lock Contention 심층 분석

스핀락 경합이 성능 병목(Bottleneck)이 되면 체계적인 분석이 필요합니다. 경합의 원인은 크게 세 가지로 분류됩니다.

원인 유형증상해결 전략
긴 임계 영역holdtime-avg 높음, waittime 비례 증가임계 영역 축소, 락 밖으로 연산 이동
높은 획득 빈도acquisitions 매우 높음, 짧은 holdtime배치 처리, per-CPU 변환, 잠금 제거
큰 경합 범위여러 CPU에서 동시 경합락 분할(splitting), per-CPU 락, RCU 전환
Lock Contention 진단 의사결정 트리 스핀락 경합 감지 holdtime 긴가? Yes 임계 영역 축소 • 연산을 락 밖으로 이동 • 데이터 복사 후 락 해제 • mutex 전환 검토 No 경합 CPU 다수? Yes 락 분할 / RCU 전환 • per-CPU 락 도입 • 읽기 경로 RCU 전환 • 해시 기반 락 분할 No 빈도 감소 • 배치 처리 • 잠금 없는 경로 추가 • trylock 기반 분기
lock contention 원인에 따라 다른 최적화 전략을 선택합니다

spinlock 대안 선택 가이드

spinlock이 적합하지 않은 상황에서의 대안을 정리합니다.

상황대안이유
임계 영역에서 슬립 필요mutexspinlock은 슬립 불가
읽기 비율 > 90%RCU 또는 rwlockspinlock은 배타적 → 읽기도 직렬화
단순 카운터/플래그atomic_t잠금 없이 원자적 연산으로 충분
CPU별 독립 데이터per-CPU 변수경합 자체를 제거
긴 임계 영역 + IRQ 공유spin_lock → mutex + softirq 분리긴 busy-wait은 시스템 응답성 저하
사용자-커널 동기화futexspinlock은 커널 내부 전용
조건 대기wait queuespinlock으로 폴링은 CPU 낭비
일회성 완료 대기completionspinlock보다 의미 명확, 슬립 가능

커널 내 주요 사용 사례

서브시스템잠금보호 대상API
스케줄러rq->__lock런큐 (per-CPU)raw_spin_lock
타이머base->lock타이머 휠 (per-CPU)raw_spin_lock_irq
네트워킹sk->sk_lock.slock소켓 상태spin_lock_bh
블록 I/Oq->queue_lock요청 큐spin_lock_irq
VFSinode->i_lockinode 메타데이터spin_lock
MMzone->lock버디 할당자(Buddy Allocator) free listspin_lock_irqsave
인터럽트desc->lockIRQ 디스크립터raw_spin_lock_irqsave

qspinlock 소스 코드 심층 워크스루

kernel/locking/qspinlock.cqueued_spin_lock_slowpath()는 약 200줄의 함수이지만, 리눅스 커널에서 가장 정교하게 최적화된 코드 중 하나입니다. 이 섹션에서는 실제 커널 소스를 한 줄씩 따라가며 상태 전이의 모든 세부사항을 분석합니다.

slowpath 진입 조건은 queued_spin_trylock()이 실패한 경우, 즉 atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL)에서 val이 0이 아니어서 CAS가 실패한 상태입니다. 이때 val에는 현재 lock word의 스냅샷이 담겨 있으며, 이 값에 따라 pending 경로와 MCS 큐 경로가 분기됩니다.

/* kernel/locking/qspinlock.c — queued_spin_lock_slowpath() 전체 흐름 */
void 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));

    /* ---- Pending 경로 시도 ---- */
    if (val == _Q_LOCKED_VAL) {
        /* 조건: locked=1, pending=0, tail=0
         * → 보유자 1명뿐이고 대기자 없음
         * → pending 비트로 "2번째 대기자" 역할 수행 */
        if (atomic_try_cmpxchg_acquire(&lock->val, &val,
                val | _Q_PENDING_VAL)) {
            /* pending 비트 획득 성공 → locked 해제 대기 */
            atomic_cond_read_acquire(&lock->val,
                    !(VAL & _Q_LOCKED_MASK));
            /* locked=0이 됨 → locked 비트 설정 + pending 클리어 */
            clear_pending_set_locked(lock);
            return;
        }
    }

    /* ---- MCS 큐 경로 ---- */
    /* Step 1: per-CPU MCS 노드 가져오기 */
    node = this_cpu_ptr(&qnodes[0].mcs);
    idx = node->count++;
    tail = encode_tail(smp_processor_id(), idx);
    node = grab_mcs_node(node, idx);

    /* 노드 초기화 — 반드시 locked=0 후 enqueue */
    WRITE_ONCE(node->locked, 0);
    WRITE_ONCE(node->next, NULL);

    /* Step 2: tail에 자신을 원자적으로 등록 (xchg) */
    old = xchg_tail(lock, tail);
    if (old & _Q_TAIL_MASK) {
        /* 이전 대기자 존재 → MCS 큐 연결 */
        prev = decode_tail(old);
        WRITE_ONCE(prev->next, node);
        /* 로컬 node->locked에서 spinning (O(1) 캐시 트래픽) */
        arch_mcs_spin_lock_contended(&node->locked);
    }

    /* Step 3: MCS 큐 head 도달 → spinlock 획득 시도 */
    val = atomic_cond_read_acquire(&lock->val,
            !(VAL & _Q_LOCKED_PENDING_MASK));

    /* locked 비트 설정 + 필요 시 tail 정리 */
    if ((val & _Q_TAIL_MASK) == tail) {
        if (atomic_try_cmpxchg_relaxed(&lock->val, &val,
                    _Q_LOCKED_VAL)) {
            /* 마지막 대기자 → tail 클리어 + locked 설정 */
            goto release;
        }
    }
    set_locked(lock);

    /* Step 4: 다음 대기자 깨우기 */
    if (!next)
        next = smp_cond_load_relaxed(&node->next,
                (VAL));
    arch_mcs_spin_unlock_contended(&next->locked);

release:
    node->count--;
}
💡

참고: pending 경로는 MCS 노드 할당·큐 연결 오버헤드를 피하는 fast-pending 최적화입니다. 대기자가 1명뿐인 일반적 경합에서는 이 경로만으로 충분하며, 실제 커널 벤치마크에서 전체 slowpath 호출의 약 70~80%가 pending 경로로 해결됩니다.

MCS 노드의 생명주기를 정리하면 다음과 같습니다:

단계동작node 상태lock word 변화
1. 할당grab_mcs_node()locked=0, next=NULL변화 없음
2. 등록xchg_tail()tail에 자신 인코딩tail 필드 업데이트
3. 연결prev->next = node큐에 연결됨변화 없음
4. 대기arch_mcs_spin_lock_contended()locked에서 spin변화 없음
5. 깨움이전 head가 next->locked=1spin 탈출변화 없음
6. 획득set_locked()spinlock 보유locked=1
7. 전달arch_mcs_spin_unlock_contended()다음 대기자 깨움변화 없음
8. 반환node->count--노드 재사용 가능변화 없음
qspinlock slowpath 상태 머신 slowpath 진입 val == LOCKED_VAL? Yes Pending 설정 CAS pending=1 locked 해제 대기 cond_read_acquire locked 획득 clear_pending_set_locked No MCS 노드 할당 grab_mcs_node(idx) 큐 등록 xchg_tail(lock, tail) 로컬 노드 스피닝 node->locked에서 spin (O(1)) 스핀락 획득 완료 lock word 상태 진입: 0x00000001 pending 설정: 0x00000101 MCS 큐 등록: 0xNNNN0101 head 대기 중: 0xNNNN0001 획득: 0x00000001 N = (cpu+1)<<18 | idx<<16 locked=bit[0:7] pending=bit[8:15] tail=bit[16:31]
slowpath는 pending 경로(왼쪽)와 MCS 큐 경로(오른쪽)로 분기되며, 각각 다른 lock word 상태를 거칩니다
⚠️

주의: pending 경로에서 atomic_try_cmpxchg_acquire()가 실패하면 MCS 큐 경로로 fallback합니다. 이 전이 구간에서 val이 변경되므로, 최신 val을 기반으로 재판단해야 합니다. 이 미묘한 경쟁 조건(Race Condition) 처리가 qspinlock 코드에서 가장 어려운 부분입니다.

queued_spin_unlock 소스 분석

queued_spin_unlock()은 놀라울 정도로 간결합니다 — 단 한 줄의 smp_store_release()로 구현됩니다. 이 단순함의 이면에는 정교한 메모리 순서 설계가 있습니다.

/* include/asm-generic/qspinlock.h */
static __always_inline void queued_spin_unlock(struct qspinlock *lock)
{
    /*
     * smp_store_release: RELEASE 의미론
     * - 이전의 모든 메모리 접근이 이 store 이전에 완료됨을 보장
     * - 임계 영역의 모든 수정이 locked=0 이전에 다른 CPU에 가시적
     *
     * locked 바이트만 0으로 설정 — pending/tail은 건드리지 않음
     * 이것이 가능한 이유: locked 필드는 바이트 단위 접근 가능
     */
    smp_store_release(&lock->locked, 0);
}

smp_store_release()가 locked 바이트만 기록하는 것이 핵심입니다. 32비트 전체가 아닌 하위 8비트만 접근하므로, pending이나 tail 필드와 원자적 충돌이 발생하지 않습니다. 이것이 가능하려면 아키텍처가 바이트 단위 store를 자연스럽게 지원해야 합니다.

아키텍처unlock 구현배리어 비용비고
x86movb $0, (%rdi)0 (TSO 자동 보장)x86 TSO 모델에서 store→store 순서가 항상 보장되므로 추가 배리어 불필요
ARM64STLRB wzr, [x0]STLR 내장Store-Release 바이트 명령어로 배리어 포함
RISC-Vsb zero, 0(a0); fence rw,wfence 1회바이트 store + release fence 조합
PowerPClwsync; stb; ...blwsync 1회lightweight sync + 바이트 store
💡

참고: x86에서 unlock이 배리어 비용 0인 이유는 TSO(Total Store Ordering) 메모리 모델 때문입니다. TSO에서는 store→store 순서가 하드웨어적으로 보장되므로, smp_store_release()는 일반 mov 명령어로 컴파일됩니다. 이로 인해 x86에서 spinlock unlock은 단 1 cycle에 수행됩니다.

/* unlock 이후 대기자 깨우기 경로 분석 */

/* Case 1: pending 대기자가 있는 경우
 * locked=0 설정 → pending 대기자의 atomic_cond_read_acquire() 탈출
 * → clear_pending_set_locked() → pending=0, locked=1 */

/* Case 2: MCS 큐 head가 대기 중인 경우
 * locked=0 설정 → head의 atomic_cond_read_acquire() 탈출
 * → set_locked() → locked=1 (tail은 head가 직접 정리) */

/* Case 3: 대기자 없는 경우
 * locked=0 설정 → lock word = 0x00000000 (FREE)
 * → 다음 lock 시도자가 fastpath trylock으로 즉시 획득 */
⚠️

주의: unlock 구현에서 atomic_set()WRITE_ONCE()를 사용하면 안 됩니다. smp_store_release()만이 올바른 RELEASE 의미론을 보장합니다. WRITE_ONCE()는 컴파일러 최적화(Compiler Optimization)만 방지하고, 하드웨어 메모리 순서는 보장하지 않습니다.

캐시라인 바운싱 심층: MESI 상태 전이

스핀락 성능의 본질은 캐시 코히어런시 프로토콜에 있습니다. MESI(Modified-Exclusive-Shared-Invalid) 프로토콜에서 스핀락 캐시 라인은 lock/unlock 시마다 상태 전이를 겪으며, 이 전이 비용이 스핀락의 실제 지연(Latency) 시간을 결정합니다.

TAS 스핀락에서 N개 CPU가 경합하면, 매 루프마다 xchg(RMW 연산)가 실행되어 캐시 라인을 Exclusive→Modified 상태로 가져가려 합니다. 이때 다른 N-1개 CPU의 캐시 라인은 Invalid로 전환되고, 다음 루프에서 다시 캐시 라인을 가져와야 합니다. 이 현상이 캐시라인 바운싱(cacheline bouncing)이며, O(N²) 버스(Bus) 트래픽의 원인입니다.

MESI 캐시 코히어런시 상태 전이 (스핀락 관점) Modified (M) dirty, exclusive 소유 Exclusive (E) clean, exclusive 소유 Shared (S) clean, 여러 CPU 공유 Invalid (I) 캐시에 없음 다른 CPU 읽기 (snoop hit) 다른 CPU RMW 로컬 쓰기 (xchg/cmpxchg) 다른 CPU 쓰기 로컬 읽기 (miss) 로컬 RMW 읽기 스핀락 동작 매핑 lock(xchg): I→M (비용 높음) spin(읽기): S→S (비용 낮음) unlock(store): M→I/S
스핀락의 lock/unlock은 MESI 상태 전이를 유발하며, 특히 I→M 전이가 가장 비용이 큽니다
MESI 전이스핀락 동작비용 (cycles)버스 트래픽
S→S (읽기 hit)ticket lock spin 루프1~4없음
I→S (읽기 miss)unlock 후 첫 spin~40 (same die)캐시 fill 1회
I→M (RMW miss)TAS lock 시도 (경합)~60 (same die)invalidate + fill
S→I (무효화)다른 CPU의 lock 획득~20snoop invalidate
M→I (무효화)보유자의 unlock~30writeback + invalidate
NUMA 토폴로지별 캐시라인 전송 지연 소켓 0 (NUMA 노드 0) Die 0 Core 0 Core 1 L3 공유 Die 1 Core 2 Core 3 L3 공유 die-to-die: ~20ns 소켓 1 (NUMA 노드 1) Die 2 Core 4 Core 5 L3 공유 Die 3 Core 6 Core 7 L3 공유 QPI/UPI 캐시라인 전송 지연 비교 (I→M 전이 기준) 같은 L3: ~10ns 다른 Die: ~20ns 다른 소켓: ~100ns 4소켓 원격: ~200ns false sharing 시 이 지연이 매 루프마다 반복 → N CPU × 지연 = 총 대기 시간
NUMA 토폴로지에서 캐시라인 전송 지연은 물리적 거리에 비례하며, 크로스-소켓 전송은 10배 이상 느립니다

False sharing은 서로 다른 데이터가 같은 캐시 라인(보통 64바이트)에 위치하여 불필요한 캐시 무효화가 발생하는 현상입니다. 스핀락에서 특히 문제가 되는 패턴을 정리합니다:

패턴문제해결
spinlock + 보호 데이터가 같은 캐시 라인spin 중에 보호 데이터의 수정이 캐시 라인 무효화____cacheline_aligned로 분리
인접한 두 spinlock이 같은 캐시 라인독립된 락의 경합이 상호 간섭패딩(Padding) 추가 또는 구조체 재배치(Relocation)
per-CPU 배열에서 인접 CPU의 락CPU 0,1의 데이터가 같은 라인에DEFINE_PER_CPU_ALIGNED 사용
/* False sharing 방지 예시 */
struct my_data {
    spinlock_t     lock;
    unsigned long  counter;
    unsigned long  flags;
} ____cacheline_aligned_in_smp;
/* 구조체 시작을 캐시 라인 경계에 정렬
 * SMP에서만 적용, UP에서는 메모리 낭비 방지 */

/* 더 공격적: 핫 필드와 콜드 필드 분리 */
struct hot_cold_split {
    /* Cacheline 1: 빈번히 수정되는 필드 */
    spinlock_t     lock;
    unsigned long  hot_counter;
    unsigned long  hot_flags;

    /* Cacheline 2: 읽기 전용 또는 드물게 수정 */
    unsigned long  config ____cacheline_aligned_in_smp;
    const char     *name;
};
⚠️

주의: ____cacheline_aligned 남용은 메모리 낭비를 초래합니다. 64바이트 캐시 라인 기준으로, 4바이트 spinlock에 60바이트 패딩이 추가됩니다. 대량으로 할당되는 구조체(inode, dentry 등)에는 프로파일링 결과를 근거로 신중하게 적용해야 합니다.

벤치마크: 처리량·지연·공정성

스핀락 구현의 성능을 객관적으로 비교하려면 locktorture(커널 내장)와 will-it-scale 벤치마크를 사용합니다. locktorture는 CONFIG_LOCK_TORTURE_TEST를 활성화한 후 모듈로 로드하며, 지정된 시간 동안 N개 스레드가 동시에 lock/unlock을 반복하여 처리량과 공정성을 측정합니다.

# locktorture 실행 예시
modprobe locktorture torture_type=spin_lock \
    nwriters_stress=16 nreaders_stress=0 \
    stat_interval=5 shutdown_secs=60

# will-it-scale 벤치마크
git clone https://github.com/antonblanchard/will-it-scale.git
cd will-it-scale
make
./lock1 -t 128 -s 10  # 128 스레드, 10초

# perf로 lock 성능 직접 측정
perf bench sched pipe -T  # 기본 스케줄러 lock 포함 벤치마크
perf stat -e cache-misses,cache-references \
    -e L1-dcache-load-misses,L1-dcache-loads \
    taskset -c 0-7 ./lock_benchmark

아래 표는 Intel Xeon Platinum 8380 (2소켓, 총 80코어) 환경에서의 대표적 벤치마크 결과입니다. lock/unlock 1회당 평균 cycles를 보여줍니다:

CPU 수TAS (cycles)Ticket (cycles)qspinlock (cycles)CNA (cycles)
1 (무경합)121277
285784545
43201809085
81,250420185160
165,100950380290
3221,0002,200780520
64 (크로스-소켓)85,0005,5001,600850
80130,0008,2002,4001,100
스핀락 구현별 성능 비교 (cycles/acquisition, log scale) 10 100 1K 10K 100K cycles/acquisition 1 4 8 16 32 64 80 경합 CPU 수 TAS Ticket qspinlock CNA
CPU 수 증가에 따른 lock 획득 비용: TAS는 지수적으로 증가하고, qspinlock/CNA는 선형에 가깝습니다
💡

참고: CNA의 우위는 크로스-소켓(64+ CPU) 환경에서 두드러집니다. 같은 소켓 내에서는 qspinlock과 차이가 작지만, 2소켓 이상에서는 NUMA 노드 간 캐시라인 전송 회피 효과로 약 40~50% 향상됩니다. 단, CNA는 strict FIFO를 희생하므로 공정성이 중요한 워크로드에서는 주의가 필요합니다.

지표TASTicketqspinlockCNA
공정성없음 (starvation 가능)FIFO (완벽)FIFO (거의)NUMA 우선 (약간 불공정)
확장성O(N²)O(N)O(1)O(1) + NUMA
메모리4B4B4B + per-CPU MCS4B + per-CPU MCS + 추가
무경합 비용~12 cycles~12 cycles~7 cycles~7 cycles
최악 지연무한 (starvation)N×cyclesN×cycles2N×cycles (워스트)

LKMM과 스핀락 메모리 순서 심층

Linux Kernel Memory Model(LKMM)은 스핀락의 ACQUIRE/RELEASE 의미론을 형식적으로 정의합니다. spin_lock()ACQUIRE 연산으로, 이후의 메모리 접근이 lock 이전으로 재배치되지 않음을 보장합니다. spin_unlock()RELEASE 연산으로, 이전의 메모리 접근이 unlock 이후로 재배치되지 않음을 보장합니다.

/* LKMM 관점의 스핀락 메모리 순서 규칙 */

/*
 * 규칙 1: ACQUIRE 순서 보장
 * spin_lock() 이전의 메모리 접근은 lock 이후로 이동할 수 있음 (단방향 배리어)
 * 하지만 lock 이후의 접근은 lock 이전으로 이동할 수 없음
 *
 * [메모리 접근 A]          ← lock 위로 이동 가능
 * spin_lock(&lock);        ← ACQUIRE 배리어
 * [메모리 접근 B]          ← lock 아래로만 가능 (올라갈 수 없음)
 * [메모리 접근 C]
 */

/*
 * 규칙 2: RELEASE 순서 보장
 * spin_unlock() 이후의 메모리 접근은 unlock 이전으로 이동할 수 있음
 * 하지만 unlock 이전의 접근은 unlock 이후로 이동할 수 없음
 *
 * [메모리 접근 D]          ← unlock 위에만 가능 (내려갈 수 없음)
 * [메모리 접근 E]
 * spin_unlock(&lock);      ← RELEASE 배리어
 * [메모리 접근 F]          ← unlock 아래로 이동 가능
 */

/*
 * 규칙 3: ACQUIRE-RELEASE 조합 = 임계 영역 격리
 * spin_lock();
 *   [임계 영역]            ← 이 영역의 접근이 밖으로 누출되지 않음
 * spin_unlock();
 *
 * 단, lock 이전과 unlock 이후의 접근끼리는 순서가 보장되지 않음!
 */

LKMM의 litmus test를 사용하면 스핀락의 메모리 순서를 형식적으로 검증할 수 있습니다:

/* Litmus test: 스핀락이 보장하는 순서 */
/* tools/memory-model/litmus-tests/ */

C spinlock-ordering

{
  x = 0;
  y = 0;
}

P0(spinlock_t *lock, int *x, int *y) {
  spin_lock(lock);
  WRITE_ONCE(*x, 1);     /* 임계 영역 내 쓰기 */
  WRITE_ONCE(*y, 1);     /* 임계 영역 내 쓰기 */
  spin_unlock(lock);
}

P1(spinlock_t *lock, int *x, int *y) {
  spin_lock(lock);
  r0 = READ_ONCE(*y);    /* 임계 영역 내 읽기 */
  r1 = READ_ONCE(*x);    /* 임계 영역 내 읽기 */
  spin_unlock(lock);
}

exists (0:r0=1 /\ 0:r1=0)
/* 결과: Never
 * 스핀락의 ACQUIRE-RELEASE가 P0의 쓰기 순서를 P1에서 관찰 가능하게 함
 * P1이 y=1을 봤다면, x=1도 반드시 봄 (x의 쓰기가 y보다 먼저) */
ACQUIRE/RELEASE 메모리 순서 타임라인 CPU 0 CPU 1 x = 1 (lock 이전 접근) spin_lock() ACQUIRE a = 10; b = 20; 임계 영역 spin_unlock() RELEASE z = 3 (unlock 이후 접근) 이 아래로만 이동 가능 이 위로만 이동 가능 happens-before (RELEASE → ACQUIRE 쌍) spin_lock() ACQUIRE r0 = a; /* 10 */ r1 = b; /* 20 */ 임계 영역 spin_unlock() RELEASE CPU 0의 RELEASE → CPU 1의 ACQUIRE 순서로 실행되면, CPU 0의 임계 영역 수정이 CPU 1에서 모두 가시적임이 보장됩니다
ACQUIRE/RELEASE 쌍이 happens-before 관계를 형성하여 임계 영역의 가시성을 보장합니다
LKMM 연산스핀락 매핑(Mapping)x86ARM64RISC-V
smp_load_acquirelock 내부 (cmpxchg_acquire)MOV (TSO 자동)LDAXRLR.aq
smp_store_releaseunlock (locked=0)MOV (TSO 자동)STLRBfence rw,w; sb
smp_mb사용 안 함 (ACQ/REL로 충분)MFENCEDMB ISHfence rw,rw
cmpxchg_acquiretrylock fastpathLOCK CMPXCHGLDAXR+STXRLR.aq+SC
💡

참고: x86의 TSO 모델에서는 smp_load_acquire()smp_store_release()가 모두 일반 MOV로 컴파일됩니다. ARM64나 RISC-V 같은 약한 메모리 모델(weakly ordered)에서만 실제 배리어 명령어가 필요합니다. 이것이 x86에서 스핀락 성능이 상대적으로 좋은 이유 중 하나입니다.

서브시스템 사례: 스케줄러 rq->__lock

커널에서 가장 hot한 스핀락은 스케줄러의 per-CPU 런큐 락 rq->__lock입니다. 모든 schedule() 호출, 태스크(Task) 깨우기(wake_up), 로드 밸런싱에서 이 락이 획득됩니다. raw_spinlock_t를 사용하며, PREEMPT_RT에서도 진정한 busy-wait을 수행합니다.

특히 태스크 마이그레이션에서는 두 개의 rq lock을 동시에 보유해야 하므로, 데드락 방지를 위한 잠금 순서 규칙이 필수적입니다. 커널은 rq의 주소 순서(낮은 주소 먼저)로 lock ordering을 강제합니다.

/* kernel/sched/core.c — schedule() 핵심 경로 */
static void __schedule(unsigned int sched_mode)
{
    struct rq *rq;
    struct task_struct *prev, *next;

    rq = cpu_rq(smp_processor_id());
    prev = rq->curr;

    /* ★ rq lock 획득 — 가장 hot한 스핀락 */
    raw_spin_lock(&rq->__lock);

    /* 다음 실행 태스크 선택 */
    next = pick_next_task(rq, prev, &rf);

    if (next != prev) {
        rq->curr = next;
        /* 컨텍스트 스위치 수행 (rq lock 보유 상태) */
        rq = context_switch(rq, prev, next, &rf);
    }

    raw_spin_unlock(&rq->__lock);
}

/* kernel/sched/core.c — double rq locking (태스크 마이그레이션) */
static void double_rq_lock(struct rq *rq1, struct rq *rq2)
{
    if (rq1 == rq2) {
        raw_spin_lock(&rq1->__lock);
        return;
    }
    /* 주소 기반 순서: 항상 낮은 주소의 rq를 먼저 lock */
    if (rq1 < rq2) {
        raw_spin_lock(&rq1->__lock);
        raw_spin_lock_nested(&rq2->__lock, SINGLE_DEPTH_NESTING);
    } else {
        raw_spin_lock(&rq2->__lock);
        raw_spin_lock_nested(&rq1->__lock, SINGLE_DEPTH_NESTING);
    }
}

/* lockdep에게 double locking이 의도적임을 알림 */
/* SINGLE_DEPTH_NESTING: 같은 클래스의 락을 2개 보유해도 데드락 아님 */
스케줄러 rq->__lock 획득 경로 schedule() wake_up() load_balance() set_cpus_allowed() raw_spin_lock(&rq->__lock) per-CPU, raw_spinlock_t 단일 rq 경로 schedule(), 로컬 wake_up raw_spin_lock(&this_rq->__lock) 경합 낮음 (per-CPU) 이중 rq 경로 (마이그레이션) load_balance, 원격 wake_up double_rq_lock(src_rq, dst_rq) 주소 순서 locking 필수 rq lock 순서 규칙 1. 항상 주소가 낮은 rq를 먼저 lock (ABBA 데드락 방지) 2. raw_spin_lock_nested()로 lockdep에 의도적 중첩 표시
rq lock은 스케줄러의 모든 주요 경로에서 획득되며, 마이그레이션 시 이중 잠금이 필요합니다
호출 경로rq lock 수보유 시간빈도
schedule()1 (로컬)~500ns (pick_next_task 포함)매우 높음 (HZ 기반)
try_to_wake_up() (로컬)1~200ns높음
try_to_wake_up() (원격)2 (src→dst)~400ns중간
load_balance()2 (busiest→this)~1us (탐색 포함)낮음 (주기적)
sched_setaffinity()2 (old→new)~300ns낮음
💡

참고: rq lock이 raw_spinlock_t인 이유는 스케줄러 자체가 sleep 메커니즘을 구현하기 때문입니다. 스케줄러 lock이 sleeping lock이면, 스케줄러가 자기 자신을 스케줄링해야 하는 순환이 발생합니다. 이것이 raw_spinlock_t가 존재하는 가장 근본적인 이유입니다.

ftrace/perf lock 프로파일링 실전

스핀락 경합(contention)은 시스템 성능의 심각한 병목이 될 수 있습니다. 커널은 다양한 프로파일링 도구를 제공하며, 문제의 심각도에 따라 적절한 도구를 선택해야 합니다.

# 1. perf lock contention — 가장 직관적인 도구
# 어떤 락에서 가장 많은 시간이 소요되는지 즉시 확인
perf lock contention -a -b -- sleep 10
# 출력 예시:
#  contended   total wait     max wait     avg wait      type   caller
#        428     51.13 ms    421.72 us    119.46 us   spinlock  _raw_spin_lock+0x30
#        156     12.87 ms    302.11 us     82.50 us   spinlock  _raw_spin_lock_irqsave+0x48

# 2. perf lock record/report — 상세 분석
perf lock record -a -- sleep 30
perf lock report --sort acquired,contended,avg_wait

# 3. /proc/lock_stat — 커널 내장 통계 (CONFIG_LOCK_STAT 필요)
echo 0 > /proc/lock_stat   # 통계 초기화
# ... 워크로드 실행 ...
cat /proc/lock_stat | head -40
# 컬럼: class name / con-bounces / contentions / waittime-min/max/total
#        / acq-bounces / acquisitions / holdtime-min/max/total

/proc/lock_stat의 출력 형식을 상세히 분석합니다:

필드의미진단 기준
con-bounces경합으로 인한 캐시 바운스 횟수높으면 hot contention
contentions실제 대기(spin)가 발생한 횟수acquisitions 대비 비율 확인
waittime-total총 대기 시간 (us)성능 영향의 직접 지표
waittime-max최대 단일 대기 시간레이턴시 스파이크 원인
acq-bounces획득 시 캐시 바운스 (비경합 포함)캐시 효율성 지표
acquisitions총 락 획득 횟수hot path 식별
holdtime-total총 보유 시간임계 영역 길이 분석
holdtime-max최대 보유 시간비정상 긴 보유 탐지
# 4. bpftrace를 이용한 실시간 lock contention 추적
bpftrace -e '
kprobe:queued_spin_lock_slowpath {
    @start[tid] = nsecs;
}
kretprobe:queued_spin_lock_slowpath /@start[tid]/ {
    $dur = nsecs - @start[tid];
    @usecs = hist($dur / 1000);
    @total_wait_us = sum($dur / 1000);
    delete(@start[tid]);
}
interval:s:5 {
    print(@usecs);
    print(@total_wait_us);
    clear(@usecs);
    clear(@total_wait_us);
}'

# 5. ftrace lock event 추적
echo 1 > /sys/kernel/debug/tracing/events/lock/contention_begin/enable
echo 1 > /sys/kernel/debug/tracing/events/lock/contention_end/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... 워크로드 실행 ...
cat /sys/kernel/debug/tracing/trace | grep contention
# 출력: timestamp, CPU, type(spinlock/mutex/rwlock), caller, flags
lock contention 프로파일링 워크플로우 1. 탐지 perf top에서 _raw_spin_lock 상위 노출 2. 식별 perf lock contention 어떤 락인지 확인 3. 정량화 /proc/lock_stat 또는 bpftrace 히스토그램 4. 콜스택 분석 perf lock contention -s 어떤 경로에서 경합? 5. 원인 진단 긴 임계 영역? 빈번한 접근? false sharing? NUMA 효과? 6. 최적화 적용 락 분할, RCU 전환, per-CPU, 임계 영역 축소 도구 선택 가이드 빠른 확인: perf lock contention -a -b -- sleep 5 상세 분석: /proc/lock_stat (holdtime/waittime 비교) 실시간 추적: bpftrace + kprobe:queued_spin_lock_slowpath 이벤트 흐름: ftrace lock:contention_begin/end
lock contention 문제 해결은 탐지→식별→정량화→콜스택 분석→원인 진단→최적화의 6단계로 진행합니다
⚠️

주의: /proc/lock_statCONFIG_LOCK_STAT=y가 필요하며, 활성화 시 모든 lock 연산에 통계 수집 오버헤드가 추가됩니다. 프로덕션 커널에서는 perf lock contention(BPF 기반)이나 ftrace의 lock 이벤트를 사용하는 것이 안전합니다.

# lock_stat 결과 해석 예시
# waittime-total이 높은 경우 → 경합이 심한 lock
# holdtime-max가 높은 경우 → 임계 영역이 긴 lock
# contentions/acquisitions 비율이 높은 경우 → 경합률이 높은 lock

# 경합률 계산:
# contention_ratio = contentions / acquisitions * 100
# > 10% → 심각한 경합, 최적화 필요
# 1~10% → 주시 필요
# < 1% → 정상

# 특정 lock만 필터링
cat /proc/lock_stat | grep rq_lock
# 또는
awk '/rq_lock/{found=1} found{print; if(/{/){c++} if(/}/){c--; if(c==0) found=0}}' \
    /proc/lock_stat

커널 버전별 진화 타임라인

리눅스 커널의 스핀락은 20년에 걸쳐 4세대를 거치며 진화했습니다. 각 세대 전환의 동기는 항상 확장성(scalability) 문제였으며, 하드웨어 규모의 증가가 소프트웨어 설계 변경을 강제했습니다.

커널 버전연도변경핵심 커밋/저자동기
~2.6.241991~2008Test-and-Set (TAS)초기 SMP 지원단순 구현, 소규모 SMP
2.6.252008Ticket spinlock (x86)Nick PigginTAS의 starvation과 O(N²) 캐시 트래픽
3.152014MCS lock 인프라Tim Chen, Waiman LongTicket의 O(N) 무효화 문제 해결을 위한 기반
4.22015qspinlock (x86 기본)Waiman Long, Peter ZijlstraMCS를 4바이트에 압축, ABI 호환
4.152018PV qspinlock 최적화Waiman Long가상화 LHP 문제 (vCPU 선점)
5.02019ARM64 qspinlock 채택Will DeaconARM 서버 확산, WFE 기반 최적화
5.62020RISC-V qspinlock 지원Palmer DabbeltRISC-V SMP 지원 본격화
5.142021CNA (Compact NUMA-Aware) lockAlex Kogan (Oracle)4소켓+ 서버 크로스-노드 트래픽
6.22023qspinlock pending 경로 개선Linus Torvalds2-CPU 경합 최적화
6.72024lock contention BPF 개선Namhyung Kimperf lock contention 도구 강화
Linux 스핀락 진화 타임라인 (2008~2024) 2008 v2.6.25 Ticket Lock FIFO 보장 2014 v3.15 MCS 인프라 로컬 스피닝 2015 v4.2 qspinlock x86 기본 2018 v4.15 PV qspinlock 가상화 최적화 2019 v5.0 ARM64 채택 WFE 기반 2021 v5.14 CNA Lock NUMA 최적화 2024 v6.7 BPF 프로파일링 perf lock 강화 확장성 개선 추이 (지원 CPU 수 대비 성능 유지) TAS ~4 CPU Ticket ~32 CPU qspinlock ~256 CPU CNA ~1024+ CPU 각 세대 전환의 동기: 이전 세대가 감당할 수 없는 CPU 수 증가
스핀락의 세대 전환은 하드웨어 규모 확대에 의해 강제되었으며, 각 세대는 이전보다 더 큰 규모를 지원합니다
💡

참고: qspinlock의 도입은 기술적으로 가장 도전적인 전환이었습니다. sizeof(spinlock_t) == 4를 유지하면서 MCS 큐를 구현해야 했기 때문입니다. Waiman Long과 Peter Zijlstra의 핵심 아이디어는 MCS 노드를 per-CPU 변수로 분리하고, lock word에는 tail 포인터를 인코딩하는 것이었습니다. 이 설계가 가능했던 이유는 스핀락 대기 중 컨텍스트 스위치가 발생하지 않으므로 per-CPU 노드가 안정적이기 때문입니다.

보안: 사이드채널과 타이밍 공격

스핀락은 busy-wait 특성상 타이밍 사이드채널 공격에 취약할 수 있습니다. 공격자가 같은 물리 머신에서 락 경합 시간을 관찰하면, 피해자의 임계 영역 실행 시간이나 데이터 접근 패턴을 추론할 수 있습니다.

주요 공격 벡터와 커널의 대응 메커니즘을 분석합니다:

공격 유형메커니즘영향완화 조치
Lock contention timing oracle경합 시간으로 임계 영역 길이 추론암호화(Encryption) 키 관련 분기 추론상수 시간 구현, 경합 시간 노이즈 추가
Spectre v1 (bounds bypass)투기적 실행(Speculative Execution)이 lock 검사 전에 임계 영역 진입경계 검사 우회array_index_nospec(), LFENCE
LLC 사이드채널LLC에서 lock cacheline 접근 패턴 관찰락 획득/해제 타이밍 노출CAT(Cache Allocation Technology)
PV lock timingVM에서 host 스핀 시간으로 다른 VM 활동 추론멀티테넌트 환경 정보 누출PV qspinlock의 halted 상태 난독화
/* Spectre v1 완화: 스핀락 보호 데이터의 안전한 접근 */

spin_lock(&lock);
/* BAD: Spectre v1 취약 — 투기적 실행이 범위 밖 읽기 가능 */
if (idx < array_size)
    val = array[idx];   /* 투기적으로 범위 밖 접근 가능 */

/* GOOD: array_index_nospec으로 투기적 범위 초과 방지 */
if (idx < array_size) {
    idx = array_index_nospec(idx, array_size);
    val = array[idx];   /* 투기적으로도 범위 내만 접근 */
}
spin_unlock(&lock);

/* 암호화 키 관련 코드에서의 상수 시간 패턴 */
spin_lock(&crypto_lock);
/* 모든 분기에서 동일 시간 소요 (data-independent timing) */
result = crypto_constant_time_compare(input, key, len);
spin_unlock(&crypto_lock);
⚠️

주의: 클라우드 환경에서 rdtsc를 이용한 정밀 타이밍 측정은 스핀락 경합 시간을 나노초 정밀도로 관찰할 수 있게 합니다. 하이퍼바이저 수준에서 rdtsc 가상화(TSC_OFFSET)와 시간 노이즈 주입이 권장되며, KVM의 tsc_khz 제한과 tsc_scaling이 이 목적으로 사용됩니다.

커널의 보안 관련 스핀락 사용 가이드라인:

원칙설명적용 예시
최소 임계 영역민감한 데이터 접근 구간을 최소화키 비교만 lock 내, 결과 처리는 밖
상수 시간 연산임계 영역 내 조건부 분기 최소화crypto_memneq() 사용
KASAN/KCSAN 연동lock 없는 접근 탐지data_race() 어노테이션
lockdep 클래스 분리보안 관련 락을 별도 클래스로 분류lockdep_set_class()
💡

참고: Linux 6.x에서는 CONFIG_MITIGATION_SPECTRE_V1 옵션이 스핀락 관련 투기적 실행 경로에 자동으로 방어 코드를 삽입합니다. 이 옵션은 대부분의 배포판 커널에서 기본 활성화되어 있습니다.

NUMA 토폴로지와 락 배치 전략

CNA(Compact NUMA-Aware) lock은 qspinlock의 MCS 큐를 NUMA 토폴로지에 맞게 재구성합니다. 핵심 아이디어는 MCS 큐를 primary 큐(현재 보유자와 같은 NUMA 노드)와 secondary 큐(다른 NUMA 노드)로 분리하는 것입니다.

/* kernel/locking/qspinlock_cna.h — CNA 핵심 로직 */

/*
 * CNA MCS 노드 확장:
 * 기존 mcs_spinlock에 NUMA 노드 ID 추가
 */
struct cna_node {
    struct mcs_spinlock mcs;
    u16                 numa_node;  /* 이 CPU의 NUMA 노드 */
    u16                 encoded_tail;
    u32                 intra_count; /* 같은 노드 연속 전달 횟수 */
};

/* unlock 시 다음 대기자 선택 로직 */
static void cna_lock_handoff(struct mcs_spinlock *node,
                              struct mcs_spinlock *next)
{
    struct cna_node *cn = (struct cna_node *)node;
    struct cna_node *cn_next = (struct cna_node *)next;

    /* 같은 NUMA 노드이면 즉시 전달 (primary 큐 유지) */
    if (cn_next->numa_node == cn->numa_node) {
        cn->intra_count++;
        arch_mcs_spin_unlock_contended(&next->locked);
        return;
    }

    /* 다른 NUMA 노드: secondary 큐로 이동
     * 단, intra_count가 임계값을 초과하면 강제 전달 (공정성) */
    if (cn->intra_count < INTRA_NODE_HANDOFF_THRESHOLD) {
        /* secondary 큐에 넣고 primary에서 다음 같은 노드 탐색 */
        cna_splice_to_secondary(cn, cn_next);
    } else {
        /* 공정성을 위해 다른 노드에도 전달 */
        cn->intra_count = 0;
        arch_mcs_spin_unlock_contended(&next->locked);
    }
}

CNA의 primary/secondary 큐 메커니즘을 도식화합니다:

CNA Lock: NUMA-aware Primary/Secondary 큐 Lock 보유자 NUMA 노드 0 (CPU 3) Primary 큐 (같은 NUMA 노드) 노드 0, CPU 5 1번째 대기자 노드 0, CPU 1 2번째 대기자 노드 0, CPU 7 3번째 대기자 우선 전달 Secondary 큐 (다른 NUMA 노드) 노드 1, CPU 12 대기 중 노드 2, CPU 20 대기 중 노드 3, CPU 35 대기 중 intra_count > threshold 시 secondary 큐 전달 CNA vs qspinlock 성능 비교 (4소켓, 총 128코어) 워크로드 qspinlock CNA 개선율 will-it-scale lock1 2,400 cycles 1,100 cycles +118% dbench (파일 I/O) 3,200 MB/s 4,100 MB/s +28%
CNA는 같은 NUMA 노드 대기자에게 우선 전달하여 크로스-노드 캐시라인 전송을 최소화합니다
배치 전략적용 대상효과커널 매크로(Macro)/API
캐시 라인 정렬hot path 구조체false sharing 방지____cacheline_aligned_in_smp
per-CPU 락독립 데이터경합 자체 제거DEFINE_PER_CPU(spinlock_t, ...)
per-NUMA 락노드별 리소스크로스-노드 경합 감소per_cpu_ptr(lock, cpu_to_node(...))
락 분할(striping)대형 해시 테이블(Hash Table)병렬성 증가버킷별 독립 락
RCU 전환읽기 우세 경로읽기 무잠금rcu_read_lock()
/* NUMA-aware 락 배치 예시 */

/* 1. per-NUMA-node 락 분할 */
struct numa_locked_data {
    spinlock_t     lock;
    unsigned long  data[64];
} ____cacheline_aligned_in_smp;

static struct numa_locked_data node_data[MAX_NUMNODES];

/* 접근 시 자기 노드의 락만 획득 */
int nid = numa_node_id();
spin_lock(&node_data[nid].lock);
node_data[nid].data[idx]++;
spin_unlock(&node_data[nid].lock);

/* 2. 해시 테이블 락 분할 (inode cache 패턴) */
#define HASH_BITS  10
#define HASH_SIZE  (1 << HASH_BITS)

static struct hlist_bl_head inode_hashtable[HASH_SIZE];
/* 각 hlist_bl_head에 내장된 bit spinlock 사용
 * → 최대 1024개 독립 락, 경합 확률 1/1024 */
💡

참고: CNA의 INTRA_NODE_HANDOFF_THRESHOLD 값은 성능과 공정성의 트레이드오프를 결정합니다. 기본값은 커널 빌드 시 CONFIG_NUMA_AWARE_SPINLOCKS와 함께 설정되며, 런타임에 /proc/sys/kernel/numa_spinlock_threshold로 조정할 수 있습니다. 값이 클수록 NUMA 지역성이 좋아지지만, 원격 노드의 starvation 위험이 증가합니다.

⚠️

주의: CNA lock은 CONFIG_NUMA_AWARE_SPINLOCKS=yNUMA=y가 모두 필요하며, 단일 소켓 시스템에서는 표준 qspinlock으로 자동 fallback합니다. 2소켓 이하 시스템에서는 CNA의 오버헤드(노드 ID 비교, secondary 큐 관리)가 이점보다 클 수 있으므로, 3소켓 이상에서만 권장됩니다.

스핀락 코드 리뷰 체크리스트

#항목확인
1임계 영역에서 슬립 가능 함수 호출 없는가?GFP_KERNEL, mutex_lock, copy_from_user 등 금지
2IRQ와 공유하는 락에 _irqsave/_irq 사용했는가?빠뜨리면 같은 CPU 데드락
3softirq와 공유하는 락에 _bh 사용했는가?softirq에서 프로세스 컨텍스트 락 접근 시
4다중 잠금 순서가 일관적인가?모든 경로에서 같은 순서 (ABBA 방지)
5임계 영역이 충분히 짧은가?수백 cycles 이내 (긴 연산은 밖으로)
6lockdep_assert_held() 어노테이션이 있는가?잠금 보유 요구 함수에 추가
7RT 커널 호환성을 고려했는가?진정한 busy-wait 필요 시 raw_spinlock_t
8False sharing 방지를 위해 캐시 라인 정렬했는가?hot path 구조체에 ____cacheline_aligned
9spinlock이 정말 필요한가? 대안은?atomic, per-CPU, RCU, mutex 등 검토
10unlock 경로가 모든 분기에서 보장되는가?에러 경로, goto 레이블에서 누락 확인

참고 자료

Spinlock 구현과 성능 분석에 대한 공식 문서, 심층 기사, 학술 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

spinlock과 관련된 다른 동기화 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.