Mutex (뮤텍스)

커널의 핵심 sleeping lock인 mutex의 내부 구현을 심층 분석합니다. struct mutex의 owner 필드 플래그 비트 인코딩, Fast Path(단일 CAS 즉시 획득)·Mid Path(Optimistic Spinning과 osq_lock MCS 큐)·Slow Path(슬립(Sleep)과 웨이크업) 3단계 경로를 추적하고, HANDOFF 메커니즘에 의한 starvation 방지, mutex_waiter 대기 리스트 관리, mutex_unlock 경로 분석을 다룹니다. Priority Inversion과 PI Chain 연결, PREEMPT_RT에서의 mutex 변환, CONFIG_DEBUG_MUTEXES와 lockdep 디버깅(Debugging), 실전 사용 패턴과 안티패턴까지 커널 소스 기반으로 분석합니다.

전제 조건: 동기화 기법, Atomic 연산, Spinlock 문서를 먼저 읽으세요. mutex는 atomic CAS 연산과 spinlock 기반 optimistic spinning 위에 구축되므로, 이들의 기본 개념을 먼저 이해해야 합니다.
일상 비유: mutex는 열쇠가 하나인 화장실과 같습니다. 사용하려면 열쇠(lock)를 가져가야 하고, 열쇠가 없으면 대기실 의자에 앉아 잠을 잡니다(sleep). 안에 있던 사람이 나오면서 대기열 첫 번째 사람을 깨워줍니다. spinlock은 "문 앞에서 뛰며 기다리는 것"이라면, mutex는 "벨을 누르고 의자에서 쉬며 기다리는 것"입니다. 다만, 문 앞을 지나가다가 열쇠가 꽂혀있으면(fast path) 줄도 서지 않고 바로 들어갈 수 있고, 안의 사람이 곧 나올 것 같으면 잠깐 서서 기다리기도 합니다(optimistic spinning).

핵심 요약

  • Sleeping Lock -- 락을 얻을 수 없으면 태스크(Task)를 슬립 상태로 전환하여 CPU를 양보(Yield)합니다. 프로세스(Process) 컨텍스트에서만 사용 가능합니다.
  • 3단계 획득 -- Fast Path(단일 CAS로 즉시 획득), Mid Path(osq_lock 기반 optimistic spinning), Slow Path(wait_list에 추가 후 sleep).
  • owner 인코딩 -- owner 필드 상위 비트에 task_struct 포인터, 하위 3비트에 WAITERS/HANDOFF/PICKUP 플래그를 저장합니다.
  • HANDOFF 방지 -- 대기자가 2번 연속 깨어났는데도 획득 실패하면 HANDOFF 플래그를 설정하여 소유권을 직접 이전합니다.
  • 단일 소유자 -- 한 번에 하나의 태스크만 mutex를 보유할 수 있으며, 반드시 소유자가 직접 unlock해야 합니다.

단계별 이해

  1. Sleeping Lock 개념 파악
    spinlock(busy-wait)과 달리, mutex는 대기 중 CPU를 양보하는 sleeping lock입니다. 임계 영역(Critical Section)이 길거나 슬립 가능 함수를 호출해야 할 때 사용합니다.
  2. struct mutex 구조 이해
    owner, wait_lock, wait_list, osq 필드의 역할과 상호 관계를 파악합니다.
  3. 3단계 경로 추적
    Fast Path(CAS), Mid Path(optimistic spin), Slow Path(sleep) 각각의 진입 조건과 동작을 이해합니다.
  4. HANDOFF와 공정성(Fairness)
    starvation을 방지하는 HANDOFF 메커니즘이 어떻게 동작하는지 파악합니다.
  5. 실전 패턴과 디버깅
    올바른 사용 패턴, 흔한 실수, lockdep을 활용한 디버깅 방법을 익힙니다.
관련 표준: Mellor-Crummey, J. M. & Scott, M. L. "Algorithms for Scalable Synchronization on Shared-Memory Multiprocessors" (1991) -- MCS lock 원논문, osq_lock의 기초. Sha, L., Rajkumar, R. & Lehoczky, J. P. "Priority Inheritance Protocols: An Approach to Real-Time Synchronization" (1990) -- Priority Inversion 해결 이론. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.

이론적 배경: Sleeping Lock과 상호 배제(Mutual Exclusion)

mutex(mutual exclusion)는 이름 그대로 상호 배제를 위한 가장 기본적인 sleeping lock입니다. spinlock이 락을 얻을 때까지 CPU에서 루프를 도는 busy-wait 방식인 반면, mutex는 락이 이미 점유되어 있으면 현재 태스크를 TASK_UNINTERRUPTIBLE 또는 TASK_INTERRUPTIBLE 상태로 전환하고 스케줄러(Scheduler)에 CPU를 양보합니다.

Sleeping Lock의 원리

sleeping lock의 핵심은 대기 비용을 CPU 사이클에서 컨텍스트 스위치 오버헤드(Overhead)로 교환하는 것입니다. 컨텍스트 스위치는 수천 사이클이 소요되지만, 임계 영역이 긴 경우(I/O 대기, 메모리 할당 등) CPU를 유용한 작업에 할당할 수 있어 시스템 전체 처리량(Throughput)이 향상됩니다.

/* 비용 모델 비교 */

spinlock 대기:   busy-wait 루프, 매 반복 ~5 cycles (PAUSE/WFE)
                 임계 영역 보유 시간 동안 CPU 100% 소모

mutex 대기:      sleep + wakeup ~5,000-10,000 cycles (컨텍스트 스위치 2회)
                 대기 중 CPU 0% 소모 (다른 태스크 실행)

/* 손익 분기점 */
임계 영역 < ~1,000 cycles  →  spinlock 유리 (전환 비용이 대기 비용보다 큼)
임계 영역 > ~1,000 cycles  →  mutex 유리 (CPU 양보가 이득)

/* Linux mutex: 최적화를 통해 두 영역 모두 커버 */
- Fast Path: 경합 없으면 CAS 한 번으로 획득 (~수 cycles)
- Mid Path: 짧은 경합은 optimistic spinning으로 (~spinlock 수준)
- Slow Path: 긴 경합만 실제 sleep

Linux Mutex의 계약 조건

커널 mutex는 일반적인 세마포어(Semaphore)와 달리 엄격한 규칙을 강제합니다. 이 규칙 덕분에 공격적인 최적화가 가능합니다.

규칙내용세마포어와 차이
단일 소유자한 번에 하나의 태스크만 mutex를 보유세마포어는 count > 1 가능
소유자만 해제lock한 태스크가 반드시 unlock해야 함세마포어는 다른 태스크가 up() 가능
재귀 불가같은 태스크가 두 번 lock하면 데드락일부 세마포어는 재귀 허용
프로세스 컨텍스트IRQ/softirq에서 사용 불가 (sleep 가능해야 함)동일
보유 중 exit 불가mutex 보유 상태에서 태스크 종료 금지동일
초기화 필수API로만 초기화 (memset/memcpy 금지)동일
소유자 추적의 핵심: "소유자만 해제" 규칙 덕분에 커널은 owner 필드에 현재 보유 태스크의 task_struct 포인터를 저장할 수 있고, 이를 통해 optimistic spinning에서 "owner가 현재 CPU에서 실행 중인가?"를 확인하여 spinning 여부를 결정합니다. 세마포어는 이 규칙이 없어 이 최적화가 불가능합니다.
Mutex 3단계 경로 개요 Fast Path owner == NULL ? CAS(NULL → current) 즉시 획득 (~수 cycles) Mid Path owner가 CPU에서 실행 중? osq_lock으로 스핀 직렬화 Optimistic Spinning Slow Path wait_list에 추가 schedule() → sleep 컨텍스트 스위치 발생 실패 실패 mutex_lock() mutex_unlock() Fast: CAS(current→NULL) / Slow: wake waiter

Mutex vs Spinlock 선택 기준

mutex와 spinlock은 모두 상호 배제를 제공하지만 완전히 다른 대기 전략을 사용합니다. 올바른 선택을 위한 상세 비교입니다.

기준SpinlockMutex
대기 방식Busy-wait (CPU 루프)Sleep (스케줄러 양보)
사용 컨텍스트IRQ, softirq, 프로세스프로세스 컨텍스트만
임계 영역 제약슬립 불가 (GFP_ATOMIC만)슬립 가능 (GFP_KERNEL, I/O 등)
선점(Preemption)보유 중 선점 비활성화보유 중 선점 허용
소유자 추적없음 (qspinlock은 tail로 간접)owner 필드에 task_struct 포인터
Priority Inversion대응 없음rt_mutex로 PI 지원 가능
최적 보유 시간수십~수백 cycles수백 cycles ~ 수 ms
PREEMPT_RTspinlock_t → rt_mutex로 변환이미 sleeping lock
메모리 크기4 bytes (qspinlock)32~40 bytes (아키텍처 의존)
선택 규칙: "sleep할 필요가 없고, IRQ/softirq에서 사용하며, 임계 영역이 매우 짧다"면 spinlock, 그 외에는 대부분 mutex가 적합합니다. 확신이 없으면 mutex를 먼저 시도하세요. mutex의 optimistic spinning이 짧은 경합(Contention)에서도 spinlock 수준의 성능을 제공합니다.
Lock 선택 의사결정 트리 IRQ/softirq 컨텍스트인가? Yes spinlock 사용 No 임계 영역에서 sleep 필요? Yes mutex 사용 No 보유 시간 > ~1000 cycles? Yes No Priority Inversion 방지 필요? Yes rt_mutex 사용 No mutex 또는 spinlock

struct mutex 필드 분석

struct mutexinclude/linux/mutex.h에 정의되어 있으며, 커널 빌드 설정에 따라 디버깅용 필드가 추가됩니다.

/* include/linux/mutex.h */
struct mutex {
    atomic_long_t       owner;       /* task_struct* | flags (하위 3비트) */
    raw_spinlock_t      wait_lock;   /* wait_list 보호용 spinlock */
    struct list_head    wait_list;   /* mutex_waiter 연결 리스트 (FIFO) */
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
    struct optimistic_spin_queue osq; /* MCS-like 스핀 큐 */
#endif
#ifdef CONFIG_DEBUG_MUTEXES
    const char          *name;       /* 디버깅용 이름 */
    void                *magic;      /* 매직 넘버 (corruption 탐지) */
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map  dep_map;     /* lockdep 의존성 추적 */
#endif
};
필드타입크기역할
owneratomic_long_t8B (64bit)현재 소유자 task_struct 포인터 + 하위 3비트 플래그
wait_lockraw_spinlock_t4Bwait_list 조작 보호, IRQ에서도 안전한 raw spinlock
wait_listlist_head16B대기 태스크들의 FIFO 연결 리스트(Linked List)
osqoptimistic_spin_queue4BMCS 기반 optimistic spinner 큐의 tail
크기 비교: struct mutex는 디버깅 옵션 없이 약 32바이트(64비트 아키텍처)입니다. spinlock_t(4바이트)의 8배이지만, sleeping lock의 대기 리스트와 소유자 추적에 필요한 최소 크기입니다. 수천 개의 mutex를 할당하는 서브시스템에서는 메모리 풋프린트를 고려해야 합니다.

owner 필드와 플래그 비트 인코딩

owner 필드는 mutex의 핵심입니다. 64비트 atomic_long_t 하나에 소유자 정보와 상태 플래그를 동시에 저장합니다.

/* kernel/locking/mutex.c */
#define MUTEX_FLAG_WAITERS   0x01  /* wait_list에 대기자 존재 */
#define MUTEX_FLAG_HANDOFF   0x02  /* 소유권 직접 이전 요청 */
#define MUTEX_FLAG_PICKUP    0x04  /* HANDOFF 소유권 수령 대기 */
#define MUTEX_FLAGS          0x07  /* 모든 플래그 마스크 */

/* owner 필드 구조 (64비트):
 *
 * bit 63                                  3  2  1  0
 *  |        task_struct* (정렬 보장)       |PU|HO|WA|
 *
 * task_struct는 최소 L1_CACHE_BYTES 정렬되므로
 * 하위 3비트가 항상 0 → 플래그 저장에 활용
 */

static inline struct task_struct *__mutex_owner(struct mutex *lock)
{
    return (struct task_struct *)
        (atomic_long_read(&lock->owner) & ~MUTEX_FLAGS);
}

static inline unsigned long __mutex_flags(struct mutex *lock)
{
    return atomic_long_read(&lock->owner) & MUTEX_FLAGS;
}
owner 필드 비트 인코딩 (64비트) task_struct* (bit 63 ~ 3) PU bit 2 HO bit 1 WA bit 0 63 3 0 WAITERS 대기자 존재 HANDOFF 소유권 이전 PICKUP 수령 대기 예: owner = 0xFFFF8880_12345000 | 0x01 → task=0xFFFF888012345000, WAITERS=1

플래그별 의미

플래그비트설정 시점해제 시점의미
MUTEX_FLAG_WAITERS0slow path 진입 시마지막 대기자가 획득하거나 떠날 때unlock 시 wake_up 필요함을 알림
MUTEX_FLAG_HANDOFF1대기자가 2번 깨어났는데 획득 실패 시HANDOFF 완료 시spinner에게 양보 강제
MUTEX_FLAG_PICKUP2unlock에서 HANDOFF된 waiter에게 소유권 설정 시waiter가 소유권 수령 시특정 waiter만 획득 가능
정렬 의존성: owner 필드의 하위 3비트를 플래그로 사용하는 것은 task_struct가 최소 8바이트 정렬(실제로는 L1_CACHE_BYTES, 64바이트 정렬)됨을 전제합니다. 이 정렬이 깨지면 task_struct 포인터와 플래그가 충돌하여 심각한 버그가 발생합니다.

API 전체 레퍼런스

커널은 다양한 mutex API를 제공합니다. 각 함수의 동작과 사용 조건을 정리합니다.

초기화

/* 정적 초기화 */
DEFINE_MUTEX(my_mutex);

/* 동적 초기화 */
struct mutex lock;
mutex_init(&lock);

/* mutex_init 내부:
 * - owner = 0 (unlocked, no flags)
 * - wait_list 초기화
 * - wait_lock 초기화
 * - osq 초기화
 * - lockdep: __mutex_init()으로 dep_map 등록
 */

잠금(Lock) 획득

함수설명반환값
mutex_lock(lock)TASK_UNINTERRUPTIBLE로 대기, 시그널(Signal) 무시void
mutex_lock_interruptible(lock)TASK_INTERRUPTIBLE로 대기, 시그널 수신 시 -EINTR0 또는 -EINTR
mutex_lock_killable(lock)TASK_KILLABLE로 대기, SIGKILL 시 -EINTR0 또는 -EINTR
mutex_lock_io(lock)I/O 대기로 계정 (io_schedule)void
mutex_trylock(lock)non-blocking, 즉시 성공/실패1(성공) 또는 0(실패)
mutex_lock_nested(lock, subclass)lockdep subclass 지정 (중첩 잠금 구분)void

잠금 해제

/* 해제 — 반드시 lock한 태스크에서 호출 */
mutex_unlock(&lock);

/* 상태 확인 */
bool locked = mutex_is_locked(&lock);      /* owner != 0 */

/* lockdep assertion — 현재 태스크가 보유 중인지 검증 */
lockdep_assert_held(&lock);

/* 파괴 (디버깅 빌드에서 사용 후 poison) */
mutex_destroy(&lock);
mutex_trylock 주의: mutex_trylock()은 IRQ 컨텍스트에서도 호출할 수 있지만, 이는 "lock을 가져올 수 없으면 즉시 포기"하는 경우에만 사용해야 합니다. IRQ에서 mutex_lock()을 호출하면 BUG_ON이 발생합니다.

Fast Path: 경합 없는 즉시 획득

mutex_lock()의 fast path는 mutex가 완전히 해제된 상태(owner == 0, 플래그 없음)에서 단일 CAS로 소유권을 획득하는 경로입니다. 경합이 없는 일반적인 경우에 최소 오버헤드를 제공합니다.

/* kernel/locking/mutex.c — mutex_lock() 진입점 (간략화) */

void __sched mutex_lock(struct mutex *lock)
{
    might_sleep();  /* 디버그: 슬립 불가 컨텍스트 검출 */

    /* Fast Path: CAS(0 → current) */
    if (!__mutex_trylock_fast(lock))
        __mutex_lock_slowpath(lock);
}

static __always_inline bool __mutex_trylock_fast(struct mutex *lock)
{
    unsigned long curr = (unsigned long)current;
    unsigned long zero = 0UL;

    /* atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr)
     * 성공: owner를 0에서 current로 변경, acquire 배리어
     * 실패: zero에 현재 owner 값 저장
     */
    if (atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr))
        return true;

    return false;
}

아키텍처별 CAS 구현

아키텍처CAS 명령어Acquire 배리어비고
x86-64LOCK CMPXCHG암묵적 (LOCK 접두사)TSO 모델, 추가 배리어 불필요
ARM64CASA (LSE) / LDAXR+STXRacquire suffix (-A)LSE 원자적(Atomic) 명령어가 더 효율적
RISC-VLR.W.AQ + SC.W.aq suffixLL/SC 방식, SC 실패 시 재시도
Fast Path 성능: 경합이 없는 경우 mutex_lock() + mutex_unlock()은 CAS 2회로 완료되며, x86-64에서 약 20-40 cycles입니다. 이는 spinlock의 lock+unlock 비용과 거의 동일합니다.
Fast Path: 단일 CAS 획득 owner = 0x0 (unlocked, no flags) CAS cmpxchg(0 → current) acquire semantics 성공 LOCKED owner = current 실패 owner != 0 → __mutex_lock_slowpath() Fast Path 비용: CAS 1회 (~20 cycles on x86-64)

Mid Path: Optimistic Spinning (osq_lock)

Fast Path에서 CAS가 실패하면(mutex가 이미 점유), 바로 sleep하는 대신 optimistic spinning을 시도합니다. 이 최적화의 핵심 아이디어는 "현재 소유자가 다른 CPU에서 실행 중이라면, 곧 unlock할 가능성이 높으므로 잠깐 spin하는 것이 sleep보다 효율적"이라는 것입니다.

Mid Path 진입 조건

optimistic spinning은 모든 경우에 시도되지 않습니다. 다음 조건을 모두 만족해야 합니다.

/* kernel/locking/mutex.c — __mutex_lock_common() 내부 (간략화) */

static noinline int __mutex_lock_common(...)
{
    /* Mid Path 진입 조건 검사 */
    if (__mutex_trylock(lock) ||
        mutex_optimistic_spin(lock, ww_ctx, NULL)) {
        /* CAS 또는 spinning으로 획득 성공 */
        return 0;
    }

    /* Mid Path 실패 → Slow Path */
    ...
}

static bool mutex_optimistic_spin(struct mutex *lock, ...)
{
    /* 조건 1: CONFIG_MUTEX_SPIN_ON_OWNER 활성화 */
    /* 조건 2: 선점이 비활성화 가능해야 함 */
    /* 조건 3: HANDOFF 플래그가 설정되지 않았어야 함 */

    if (!osq_lock(&lock->osq))
        goto fail;

    for (;;) {
        struct task_struct *owner;

        /* owner가 CPU에서 실행 중인지 확인 */
        owner = __mutex_owner(lock);
        if (owner && !owner_on_cpu(owner))
            break;  /* owner가 sleep/preempt → spinning 중단 */

        /* owner가 없으면(방금 unlock됨) 획득 시도 */
        if (__mutex_trylock(lock)) {
            osq_unlock(&lock->osq);
            return true;
        }

        /* 재스케줄 필요하면 spinning 중단 */
        if (need_resched())
            break;

        cpu_relax();  /* PAUSE/WFE/YIELD */
    }

    osq_unlock(&lock->osq);
fail:
    return false;
}

Spinning 중단 조건

조건이유다음 동작
owner_on_cpu(owner) == falseowner가 선점당하거나 sleep한 상태 — 곧 unlock될 가능성 낮음osq_unlock → Slow Path
need_resched() == true더 높은 우선순위(Priority) 태스크가 대기 중osq_unlock → Slow Path → schedule()
MUTEX_FLAG_HANDOFF 설정됨특정 대기자에게 소유권을 넘겨야 함spinning 포기
osq_lock() 실패다른 spinner가 이미 큐에서 대기 중곧바로 Slow Path
Mid Path: Optimistic Spinning 흐름 Fast Path CAS 실패 osq_lock() 성공? No → Slow Path Yes owner_on_cpu()? No → osq_unlock, Slow Path Yes __mutex_trylock()? Yes LOCKED (osq_unlock) No need_resched()? Yes No cpu_relax() + 재시도 Slow Path: wait_list 추가 → schedule() → sleep

OSQ (Optimistic Spin Queue) 내부 구현

OSQ는 MCS lock의 변형으로, optimistic spinning 단계에서 여러 spinner가 동시에 owner의 상태를 폴링(Polling)하는 것을 방지합니다. 각 spinner는 자신만의 로컬 변수에서 spin하여 캐시 라인(Cache Line) 경합을 최소화합니다.

/* include/linux/osq_lock.h */
struct optimistic_spin_queue {
    atomic_t tail;  /* 큐 tail, 0 = empty, >0 = CPU+1 인코딩 */
};

/* kernel/locking/osq_lock.c */
struct optimistic_spin_node {
    struct optimistic_spin_node *next;  /* 다음 노드 */
    struct optimistic_spin_node *prev;  /* 이전 노드 */
    int locked;  /* 0=spinning, 1=acquired */
    int cpu;     /* 노드 소유 CPU 번호 */
};

/* Per-CPU 노드 — 동적 할당 불필요 */
static DEFINE_PER_CPU_SHARED_ALIGNED(
    struct optimistic_spin_node, osq_node);

bool osq_lock(struct optimistic_spin_queue *lock)
{
    struct optimistic_spin_node *node = this_cpu_ptr(&osq_node);
    struct optimistic_spin_node *prev, *next;
    int curr = encode_cpu(smp_processor_id());
    int old;

    node->locked = 0;
    node->next = NULL;
    node->cpu = curr;

    /* tail에 자신을 등록, 이전 tail 값 획득 */
    old = atomic_xchg(&lock->tail, curr);
    if (old == OSQ_UNLOCKED_VAL)
        return true;  /* 큐가 비어있었음 → 즉시 획득 */

    /* 이전 노드에 자신을 연결 */
    prev = decode_cpu(old);
    WRITE_ONCE(prev->next, node);

    /* 로컬 locked 변수에서 spin (MCS 핵심!) */
    while (!READ_ONCE(node->locked)) {
        if (need_resched() || vcpu_is_preempted(prev->cpu))
            goto unqueue;
        cpu_relax();
    }
    return true;

unqueue:
    /* 큐에서 자신을 제거 (prev→next를 next로 연결) */
    ...
    return false;
}
OSQ: Per-CPU 로컬 스핀 큐 osq.tail = CPU3+1 CPU0 노드 locked = 1 mutex spin 중 CPU1 노드 locked = 0 로컬 spin 중 CPU3 노드 locked = 0 로컬 spin 중 next next MCS 핵심: 각 CPU는 자신의 per-CPU node.locked에서 spin → 캐시 라인 경합 없음 (O(1) 캐시 트래픽) osq_unlock 과정 CPU0: next->locked = 1 CPU1: spin 탈출, mutex spin 시작
MCS vs OSQ 차이: 원래 MCS lock은 하나의 lock을 완전히 대체하지만, OSQ는 mutex의 "spinner 직렬화(Serialization)"에만 사용됩니다. osq_lock을 획득한 spinner만 mutex owner를 폴링할 수 있어, 여러 CPU가 동시에 owner의 캐시 라인을 읽는 것을 방지합니다.

Slow Path: 슬립과 웨이크업

Fast Path와 Mid Path 모두 실패하면 Slow Path에 진입합니다. 태스크를 wait_list에 추가하고 schedule()을 호출하여 sleep합니다.

/* kernel/locking/mutex.c — slow path (간략화) */

static noinline int
__mutex_lock_common(struct mutex *lock, unsigned int state, ...)
{
    struct mutex_waiter waiter;
    bool first = false;
    int ret;

    /* ... Fast Path + Mid Path 실패 ... */

    raw_spin_lock(&lock->wait_lock);

    /* WAITERS 플래그 설정 (unlock이 wake를 해야 함을 알림) */
    __mutex_set_flag(lock, MUTEX_FLAG_WAITERS);

    /* wait_list에 추가 (FIFO 순서) */
    __mutex_add_waiter(lock, &waiter, &lock->wait_list);
    waiter.task = current;

    set_current_state(state);  /* TASK_UNINTERRUPTIBLE 등 */

    for (;;) {
        /* 한 번 더 trylock 시도 (unlock 직후 race window) */
        if (__mutex_trylock(lock))
            goto acquired;

        /* 시그널 체크 (interruptible 모드일 때) */
        if (signal_pending_state(state, current)) {
            ret = -EINTR;
            goto err;
        }

        raw_spin_unlock(&lock->wait_lock);
        schedule_preempt_disabled();  /* CPU 양보, sleep */

        /* 깨어남 — first waiter인지 확인 */
        first = __mutex_waiter_is_first(lock, &waiter);

        set_current_state(state);

        /* first waiter면 optimistic spinning 한 번 더 시도 */
        if (first && mutex_optimistic_spin(lock, ww_ctx, &waiter))
            break;

        raw_spin_lock(&lock->wait_lock);
    }

acquired:
    __set_current_state(TASK_RUNNING);
    __mutex_remove_waiter(lock, &waiter);

    if (list_empty(&lock->wait_list))
        __mutex_clear_flag(lock, MUTEX_FLAGS);

    raw_spin_unlock(&lock->wait_lock);
    return 0;
}
Slow Path: 태스크 상태 전이 TASK_RUNNING set_current_state() TASK_UNINTERRUPTIBLE schedule_preempt_disabled() SLEEPING CPU 양보, wait_list에서 대기 mutex_unlock() → wake_up_process() WOKEN (trylock 재시도) trylock 성공 ACQUIRED trylock 실패 재sleep
first waiter 최적화: wait_list의 첫 번째 대기자는 깨어난 후 다시 optimistic spinning을 시도합니다. 이는 깨어난 직후 다른 spinner에게 mutex를 빼앗기는 것을 줄이기 위한 것입니다. first waiter의 spinning은 osq_lock 없이 직접 수행됩니다.

HANDOFF 메커니즘: Starvation 방지

optimistic spinning은 성능에 매우 효과적이지만 공정성 문제를 야기합니다. spinner가 sleep 중인 waiter보다 항상 먼저 mutex를 획득하면, waiter는 영구적으로 starvation에 빠질 수 있습니다. HANDOFF 메커니즘이 이 문제를 해결합니다.

HANDOFF 발동 조건

/* Slow Path 내부 — first waiter가 깨어난 후 */

/* waiter가 깨어났는데 trylock 실패 → 누군가 먼저 가져감 */
if (first) {
    /* 이전에도 실패한 적 있으면 HANDOFF 설정 */
    if (woken) {
        /* 2번째 실패: HANDOFF 플래그 설정 */
        __mutex_set_flag(lock, MUTEX_FLAG_HANDOFF);
    }
    woken = true;
}

HANDOFF 동작 과정

단계동작상태 변화
1first waiter가 깨어남trylock 시도
2spinner에게 빼앗김 (1차 실패)woken = true
3다시 깨어남, 다시 실패 (2차 실패)MUTEX_FLAG_HANDOFF 설정
4현재 owner가 unlock 시 HANDOFF 감지owner를 waiter의 task|PICKUP으로 설정
5spinner는 PICKUP 감지 → spinning 포기spinner가 Slow Path로 이동
6지정된 waiter가 PICKUP 확인 후 획득PICKUP 클리어, 정상 소유
HANDOFF: Starvation 방지 시퀀스 Waiter (W) Owner (A/B) Spinner (S) sleep (wait_list) A: unlock, wake(W) S: trylock 성공 (빼앗김!) 1차 실패, woken=true S: unlock, wake(W) 2차 실패 → HANDOFF! FLAG_HANDOFF 설정 B: unlock, HANDOFF 감지 owner = W|PICKUP S2: PICKUP 감지 → 포기 W: ACQUIRED S2: → Slow Path HANDOFF 보장: 2회 연속 실패 후 공정성 강제
HANDOFF 비용: HANDOFF가 발동되면 spinner는 더 이상 mutex를 획득할 수 없으므로 처리량이 감소합니다. 그러나 이는 starvation을 방지하기 위한 필수 트레이드오프입니다. 잘 설계된 워크로드에서는 HANDOFF가 거의 발동되지 않습니다.

mutex_waiter 구조와 대기 리스트

struct mutex_waiter는 mutex의 slow path에서 대기 중인 태스크를 추적하는 스택 할당 구조체(Struct)입니다.

/* kernel/locking/mutex.c */
struct mutex_waiter {
    struct list_head    list;     /* mutex->wait_list 연결 */
    struct task_struct  *task;    /* 대기 중인 태스크 */
    struct ww_acquire_ctx *ww_ctx; /* wound-wait context (ww_mutex) */
#ifdef CONFIG_DEBUG_MUTEXES
    void                *magic;   /* 매직 넘버 */
#endif
};

/* wait_list 관리 — FIFO 순서 */
static void
__mutex_add_waiter(struct mutex *lock,
                   struct mutex_waiter *waiter,
                   struct list_head *list)
{
    /* list_add_tail: FIFO 순서로 뒤에 추가 */
    list_add_tail(&waiter->list, list);

    /* 첫 번째 waiter면 WAITERS 플래그 설정 */
    if (__mutex_waiter_is_first(lock, waiter))
        __mutex_set_flag(lock, MUTEX_FLAG_WAITERS);
}
스택 할당: mutex_waiter__mutex_lock_common()의 스택에 할당됩니다. 태스크가 sleep하면 스택 프레임(Stack Frame)이 유지되므로 별도의 힙 할당이 필요 없습니다. 이는 메모리 할당 실패 가능성을 제거하여 mutex_lock()이 항상 성공(대기)할 수 있게 합니다.

mutex_unlock() 경로 분석

mutex_unlock()도 lock과 마찬가지로 fast/slow 두 경로를 가집니다.

/* kernel/locking/mutex.c */

void __sched mutex_unlock(struct mutex *lock)
{
#ifndef CONFIG_DEBUG_MUTEXES
    /* Fast Unlock: CAS(current → 0)
     * 성공 조건: 플래그 비트가 모두 0 (대기자 없음) */
    if (__mutex_unlock_fast(lock))
        return;
#endif
    __mutex_unlock_slowpath(lock, _RET_IP_);
}

static __always_inline bool
__mutex_unlock_fast(struct mutex *lock)
{
    unsigned long curr = (unsigned long)current;

    /* CAS(current → 0): 플래그 없이 순수 current일 때만 성공
     * → release 배리어로 임계 영역 메모리 연산 완료 보장 */
    return atomic_long_try_cmpxchg_release(&lock->owner, &curr, 0UL);
}

static noinline void
__mutex_unlock_slowpath(struct mutex *lock, unsigned long ip)
{
    struct task_struct *next = NULL;
    unsigned long owner;

    raw_spin_lock(&lock->wait_lock);

    if (!list_empty(&lock->wait_list)) {
        struct mutex_waiter *waiter =
            list_first_entry(&lock->wait_list,
                             struct mutex_waiter, list);
        next = waiter->task;

        /* HANDOFF 처리: owner를 직접 waiter로 설정 */
        if (__mutex_flags(lock) & MUTEX_FLAG_HANDOFF) {
            __mutex_set_flag(lock, MUTEX_FLAG_PICKUP);
            atomic_long_set(&lock->owner,
                (unsigned long)next | MUTEX_FLAG_PICKUP);
        } else {
            /* 일반 해제: owner를 0|WAITERS로 설정 */
            atomic_long_set(&lock->owner, MUTEX_FLAG_WAITERS);
        }
    } else {
        atomic_long_set(&lock->owner, 0);
    }

    raw_spin_unlock(&lock->wait_lock);

    /* 대기자 깨우기 */
    if (next)
        wake_up_process(next);
}
mutex_unlock() 경로 분기 mutex_unlock(lock) owner == current (플래그 없음)? Yes Fast Unlock CAS(current → 0) No (flags set) Slow Unlock wait_lock + wake first waiter HANDOFF: owner = waiter|PICKUP

Priority Inversion과 PI Chain 연결

일반 struct mutex는 Priority Inversion을 처리하지 않습니다. 낮은 우선순위 태스크가 mutex를 보유하고 있을 때, 높은 우선순위 태스크가 해당 mutex를 기다리면 중간 우선순위 태스크에 의해 무기한 지연(Latency)될 수 있습니다.

Priority Inversion 문제

/* Priority Inversion 시나리오 */

태스크 우선순위: H(높음) > M(중간) > L(낮음)

1. L이 mutex 획득
2. H가 mutex_lock() 호출 → L을 기다림 (sleep)
3. M이 깨어남 → L보다 높은 우선순위로 L을 선점
4. M이 오래 실행 → L은 실행 불가 → mutex unlock 불가
5. H는 간접적으로 M에 의해 차단됨 (Priority Inversion!)

/* 해결: rt_mutex (Priority Inheritance) */

1. L이 rt_mutex 획득
2. H가 rt_mutex_lock() 호출 → L의 우선순위를 H로 부스팅
3. L이 H의 우선순위로 실행 → M보다 먼저 스케줄됨
4. L이 빠르게 unlock → H가 획득
5. L의 우선순위 복원

일반 mutex vs rt_mutex

특성mutexrt_mutex
PI 지원없음Priority Inheritance
오버헤드최소 (CAS + osq)PI chain 관리 추가
사용 사례일반 커널 코드RT 태스크, futex PI
PREEMPT_RTrt_mutex 기반으로 변환네이티브
구조체 크기~32 bytes~64 bytes
체인 지원없음다중 lock PI chain 전파
언제 rt_mutex를 사용하나? 일반 커널 코드에서는 대부분 struct mutex로 충분합니다. rt_mutex는 RT 스케줄링 정책(SCHED_FIFO/RR)을 사용하는 실시간(Real-time) 태스크 간의 동기화, 또는 PREEMPT_RT 커널에서 자동으로 사용됩니다. 더 자세한 내용은 rt_mutex 문서를 참고하세요.

PREEMPT_RT에서의 Mutex

PREEMPT_RT 패치(Patch)에서 struct mutex는 내부적으로 rt_mutex 기반으로 대체되어 Priority Inheritance를 지원하게 됩니다. 이미 sleeping lock이므로 API 변화는 없지만, optimistic spinning이 비활성화되고 PI chain 관리 비용이 추가됩니다.

RT 커널에서의 mutex 특성 변화:
  • Optimistic spinning이 비활성화됩니다 (모든 경합이 PI chain을 통해 해결)
  • mutex 보유 중에도 선점이 가능합니다 (결정론적 지연 시간)
  • PI chain 전파로 인해 lock/unlock 비용이 약간 증가합니다

spinlock_t → rt_mutex 변환 관계, raw_spinlock_t 예외, PREEMPT_RT의 전체 잠금 변환 메커니즘은 rt_mutex — PREEMPT_RT 통합에서 상세히 다룹니다.

실전 사용 패턴

기본 보호 패턴

struct my_device {
    struct mutex lock;    /* data 필드 보호 */
    int data;
    struct list_head list;
};

int device_read(struct my_device *dev, int *out)
{
    mutex_lock(&dev->lock);
    *out = dev->data;
    mutex_unlock(&dev->lock);
    return 0;
}

int device_write(struct my_device *dev, int val)
{
    mutex_lock(&dev->lock);
    dev->data = val;
    mutex_unlock(&dev->lock);
    return 0;
}

시그널 대응 패턴

int device_ioctl(struct my_device *dev, unsigned int cmd, ...)
{
    int ret;

    /* 사용자 프로세스가 Ctrl+C로 취소할 수 있는 대기 */
    ret = mutex_lock_interruptible(&dev->lock);
    if (ret)
        return ret;  /* -EINTR: 시그널로 중단됨 */

    /* 임계 영역 */
    ...

    mutex_unlock(&dev->lock);
    return 0;
}

Non-blocking 시도 패턴

/* 주기적 작업에서 lock이 가능할 때만 처리 */
void periodic_cleanup(struct my_device *dev)
{
    if (!mutex_trylock(&dev->lock))
        return;  /* 바쁘면 다음 기회에 */

    do_cleanup(dev);
    mutex_unlock(&dev->lock);
}

Scoped Guard 패턴 (6.4+)

/* cleanup.h 기반 자동 unlock (커널 6.4 이상) */
int device_complex_op(struct my_device *dev)
{
    /* guard가 스코프를 벗어나면 자동으로 mutex_unlock */
    guard(mutex)(&dev->lock);

    if (some_check_fails())
        return -EINVAL;  /* 자동 unlock */

    if (another_check())
        return -ENODEV;  /* 자동 unlock */

    do_work(dev);
    return 0;  /* 자동 unlock */
}

중첩 잠금과 lockdep subclass

/* 같은 lock class의 두 mutex를 동시에 보유해야 할 때 */
void transfer(struct account *from, struct account *to)
{
    /* 항상 포인터 주소 순서로 잠금 (ABBA 방지) */
    if (from < to) {
        mutex_lock(&from->lock);
        mutex_lock_nested(&to->lock, SINGLE_DEPTH_NESTING);
    } else {
        mutex_lock(&to->lock);
        mutex_lock_nested(&from->lock, SINGLE_DEPTH_NESTING);
    }

    /* 전송 작업 */
    from->balance -= amount;
    to->balance += amount;

    mutex_unlock(&to->lock);
    mutex_unlock(&from->lock);
}

안티패턴과 흔한 실수

IRQ 컨텍스트에서 mutex_lock

/* BUG: IRQ 핸들러에서 mutex_lock → schedule() 호출 → 커널 패닉 */
irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_device *dev = data;
    mutex_lock(&dev->lock);  /* BUG_ON(in_interrupt()) */
    ...
}

/* 해결: spinlock 사용하거나, 하반부로 작업 위임 */
irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_device *dev = data;
    spin_lock(&dev->irq_lock);  /* OK: busy-wait */
    queue_work(dev->wq, &dev->work);  /* mutex는 workqueue에서 */
    spin_unlock(&dev->irq_lock);
    return IRQ_HANDLED;
}

재귀적 잠금

/* BUG: 같은 mutex를 두 번 lock → 데드락 */
void foo(struct my_device *dev)
{
    mutex_lock(&dev->lock);
    bar(dev);  /* bar()가 다시 mutex_lock을 호출하면 데드락! */
    mutex_unlock(&dev->lock);
}

void bar(struct my_device *dev)
{
    mutex_lock(&dev->lock);  /* 데드락! foo()가 이미 보유 */
    ...
}

/* 해결: lock 보유 여부를 호출 규약으로 명확히 하거나,
 * __bar_locked() 내부 함수를 분리 */
void bar(struct my_device *dev)
{
    mutex_lock(&dev->lock);
    __bar_locked(dev);
    mutex_unlock(&dev->lock);
}

void foo(struct my_device *dev)
{
    mutex_lock(&dev->lock);
    __bar_locked(dev);  /* lock 보유 상태에서 내부 함수 호출 */
    mutex_unlock(&dev->lock);
}

ABBA 데드락

/* BUG: CPU0: lock A → lock B, CPU1: lock B → lock A → 데드락 */
void thread1(void)
{
    mutex_lock(&A);
    mutex_lock(&B);  /* CPU1이 B를 보유하고 A를 기다림 → 교착 */
    ...
}

void thread2(void)
{
    mutex_lock(&B);
    mutex_lock(&A);  /* CPU0이 A를 보유하고 B를 기다림 → 교착 */
    ...
}

/* 해결: 항상 일관된 순서 (A → B) */

장시간 보유

/* 나쁜 예: 대용량 I/O를 mutex 보유 상태에서 수행 */
mutex_lock(&lock);
vfs_read(file, buf, 1024 * 1024);  /* 수 ms ~ 수십 ms */
process_data(buf);
mutex_unlock(&lock);

/* 개선: I/O를 lock 밖으로 이동 */
vfs_read(file, buf, 1024 * 1024);  /* lock 없이 I/O */
mutex_lock(&lock);
process_data(buf);  /* 짧은 데이터 처리만 */
mutex_unlock(&lock);

디버깅: CONFIG_DEBUG_MUTEXES와 lockdep

디버깅 설정

mutex 전용 디버그 옵션은 CONFIG_DEBUG_MUTEXES=y (소유자 검증, magic 필드 검사)입니다. CONFIG_PROVE_LOCKING, CONFIG_LOCK_STAT, CONFIG_DEBUG_LOCK_ALLOC 등 공통 잠금 디버깅 설정은 lockdep — 커널 설정을 참고하세요.

CONFIG_DEBUG_MUTEXES 검사 항목

검사위반 시메시지
소유자가 아닌 태스크가 unlockBUG()"mutex not owned by task"
이미 보유 중인 mutex를 다시 lock데드락 탐지"recursive locking detected"
초기화되지 않은 mutex 사용BUG()"bad mutex magic"
해제된 mutex 사용 (mutex_destroy 후)BUG()"bad mutex magic"
IRQ 컨텍스트에서 mutex_lockBUG()"scheduling while atomic"

lockdep 활용

mutex에 lockdep_assert_held() 어노테이션을 사용하여 함수 진입 시 잠금 보유 여부를 검증할 수 있습니다. ABBA 순환 의존성, 재귀 잠금 등의 경고 형식과 해석 방법은 lockdep 경고 메시지 해석을 참고하세요.

/* lockdep annotation: 함수 진입 시 lock 보유 검증 */
void do_critical_work(struct my_device *dev)
{
    lockdep_assert_held(&dev->lock);
    /* dev->lock을 보유하지 않았으면 WARNING */
    ...
}

lock contention 프로파일링(Profiling)

CONFIG_LOCK_STAT=y 활성화 시 /proc/lock_stat에서 mutex 경합 통계를 확인할 수 있습니다. 필드 설명(con-bounces, contentions, waittime 등)과 상세 사용법은 lockdep의 /proc/lock_stat 섹션을 참고하세요. perf lock recordperf lock report로도 경합을 분석할 수 있습니다.

성능 특성과 벤치마크

경합 없는 경우 (Uncontended)

연산x86-64 (cycles)ARM64 (cycles)비고
mutex_lock (fast)~15-20~20-30단일 CAS (acquire)
mutex_unlock (fast)~10-15~15-25단일 CAS (release)
lock + unlock 합계~25-35~35-55spinlock과 유사

경합 상황 (Contended)

시나리오비용경로
짧은 경합 (owner on CPU)~100-500 cyclesMid Path: osq spinning
중간 경합 (owner preempted)~5,000-15,000 cyclesSlow Path: sleep + wakeup
긴 대기 (여러 waiter)수만 cycles 이상Slow Path + 다수 wakeup
경로별 비용 스펙트럼 비용 (cycles, 로그 스케일) 10 100 1K 10K 100K Fast Path ~25 Mid Path (osq spin) 100-500 Slow Path (sleep/wake) 5K-100K+ spinlock ref

Optimistic Spinning 효과

Ingo Molnar의 원래 패치(2013)에 따르면, optimistic spinning 도입으로 경합이 있는 워크로드에서 mutex 성능이 수십 퍼센트 향상되었습니다. 특히 파일시스템(Filesystem)의 i_mutex처럼 짧은 임계 영역에서 여러 스레드(Thread)가 경합하는 경우 효과가 큽니다.

/* osq spinning 도입 전후 비교 (개략적) */

워크로드: dbench, 32 threads, ext4

              Before (순수 sleep)    After (osq spinning)
throughput:       4,200 MB/s            5,600 MB/s  (+33%)
avg latency:      23.4 us              14.1 us      (-40%)
context switches: 1,240,000/s          380,000/s    (-69%)

Mutex 대안 선택 가이드

mutex가 항상 최선의 선택은 아닙니다. 상황에 따라 더 적합한 동기화 프리미티브가 있습니다.

상황권장 프리미티브이유
읽기 빈번, 쓰기 드묾rw_semaphore여러 reader 동시 접근 가능
읽기만 하는 경로 (락 필요 없음)RCU읽기 경로 무잠금, 쓰기 시 grace period
카운터 증감만atomic 연산단일 변수에 lock 불필요
CPU별 독립 데이터Per-CPU 변수경합 자체를 제거
완료 대기completion일회성 이벤트 알림에 특화
PI 필요rt_mutexPriority Inheritance 내장
다중 lock 교착 방지ww_mutexWound-Wait 알고리즘으로 데드락 회피
IRQ에서 보호 필요spinlocksleep 불가 컨텍스트
읽기-시퀀스 번호 기반seqlockwriter 우선, reader 재시도
동기화 프리미티브 포지셔닝 임계 영역 길이 / 경합 지속 시간 짧음 기능성 / 유연성 atomic spinlock mutex rw_semaphore ww_mutex RCU per-CPU rt_mutex seqlock

관련 커널 설정 옵션

옵션기본값설명
CONFIG_MUTEX_SPIN_ON_OWNERy (SMP)optimistic spinning 활성화. SMP + SCHEDULER 조건
CONFIG_DEBUG_MUTEXESnmutex 규칙 위반 런타임 검출 (magic 필드, 소유자 검증)
CONFIG_PREEMPT_RTnmutex를 rt_mutex 기반으로 변환 (PI 지원)
디버깅 설정: CONFIG_PROVE_LOCKING, CONFIG_LOCK_STAT, CONFIG_DEBUG_LOCK_ALLOC 등 공통 잠금 디버깅 옵션의 전체 목록과 권장 설정은 lockdep — 커널 설정을 참고하세요.
/* mutex_init() 확장 — DEBUG_MUTEXES 활성 시 */
#ifdef CONFIG_DEBUG_MUTEXES
void __mutex_init(struct mutex *lock, const char *name,
                  struct lock_class_key *key)
{
    atomic_long_set(&lock->owner, 0);
    raw_spin_lock_init(&lock->wait_lock);
    INIT_LIST_HEAD(&lock->wait_list);
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
    osq_lock_init(&lock->osq);
#endif
    lock->magic = lock;  /* 자기 참조 매직 넘버 */
    lockdep_init_map(&lock->dep_map, name, key, 0);
}

void mutex_destroy(struct mutex *lock)
{
    DEBUG_LOCKS_WARN_ON(mutex_is_locked(lock));
    lock->magic = NULL;  /* poison: 이후 사용 시 BUG */
}
#endif

Wound-Wait Mutex (ww_mutex) 개요

ww_mutex는 여러 mutex를 동시에 획득해야 할 때 데드락을 회피하는 특수 mutex입니다. GPU 드라이버(DRM/TTM)에서 버퍼(Buffer) 객체 잠금에 주로 사용됩니다.

/* Wound-Wait 알고리즘 */

/* 두 트랜잭션 T1(오래된), T2(최근)가 같은 mutex를 요청 시:
 *
 * T1이 T2가 보유한 lock 요청 → T2를 "wound" (T2가 양보)
 * T2가 T1이 보유한 lock 요청 → T2가 "wait" (T2가 대기)
 *
 * 결과: 항상 나이가 많은 트랜잭션이 우선 → 데드락 불가능
 */

struct ww_acquire_ctx ctx;
ww_acquire_init(&ctx, &my_class);

/* 여러 ww_mutex를 순서 없이 획득 시도 */
ret = ww_mutex_lock(&obj_a->lock, &ctx);
if (ret == -EDEADLK) {
    /* 양보 필요: 보유한 lock 해제 후 재시도 */
    ww_mutex_unlock(&obj_b->lock);
    ww_mutex_lock_slow(&obj_a->lock, &ctx);
    /* obj_b 다시 획득 */
}

ww_acquire_done(&ctx);
/* ... 작업 수행 ... */
ww_mutex_unlock(&obj_a->lock);
ww_mutex_unlock(&obj_b->lock);
ww_acquire_fini(&ctx);
ww_mutex 사용 사례: DRM/TTM 버퍼 관리에서 여러 GPU 버퍼 객체를 동시에 잠궈야 할 때 사용됩니다. 잠금 순서를 정적으로 결정할 수 없는 경우(객체 집합이 런타임에 결정) Wound-Wait 알고리즘이 데드락을 동적으로 회피합니다. 더 자세한 내용은 ww_mutex 문서를 참고하세요.

아키텍처별 구현 차이

mutex의 핵심 연산인 CAS(Compare-And-Swap)는 아키텍처마다 다른 명령어로 구현됩니다.

x86-64: LOCK CMPXCHG

/* x86-64: TSO 메모리 모델 — 추가 배리어 불필요 */

/* mutex_lock fast path 생성 코드 (개략) */
/* LOCK CMPXCHG [owner], current
 * - LOCK 접두사가 acquire + release 모두 제공
 * - 성공 시 ZF=1, 실패 시 ZF=0 + RAX=현재값
 */

/* mutex_unlock fast path */
/* LOCK CMPXCHG [owner], 0
 * - release semantics 자동 제공
 */

ARM64: LSE / LL/SC

/* ARM64: Weak 메모리 모델 — 명시적 배리어 필요 */

/* LSE (Large System Extensions, ARMv8.1+) */
/* CASA Xs, Xt, [Xn]  — Compare-and-Swap with Acquire */

/* LL/SC fallback (ARMv8.0) */
/* LDAXR Xt, [Xn]      — Load-Acquire Exclusive
 * STXR  Ws, Xt, [Xn]  — Store Exclusive (release는 별도)
 * CBNZ  Ws, retry      — SC 실패 시 재시도
 */

/* osq spinning에서: WFE로 전력 절약
 * - cpu_relax() → WFE (Wait For Event)
 * - osq_unlock이 SEV (Send Event) 발행
 */

RISC-V: LR/SC + AMO

/* RISC-V: Weak 메모리 모델 (RVWMO) */

/* LR.D.AQ  rd, (rs1)   — Load-Reserved with Acquire
 * SC.D     rd, rs2, (rs1)  — Store-Conditional
 * BNEZ     rd, retry   — SC 실패 시 재시도
 *
 * .aq (acquire) / .rl (release) suffix로 순서 지정
 */

/* AMO (Atomic Memory Operations) — Zamo 확장
 * AMOSWAP.D.AQ rd, rs2, (rs1)  — CAS 대용 가능
 */
아키텍처별 mutex CAS 구현 x86-64 LOCK CMPXCHG TSO: 암묵적 acquire/release PAUSE (spin hint) ARM64 CASA / LDAXR+STXR Weak: 명시적 -A/-L suffix WFE/SEV (전력 절약) RISC-V LR.D.AQ + SC.D RVWMO: .aq/.rl suffix WFI (선택적) 공통: atomic_long_try_cmpxchg_acquire/release() 아키텍처 추상화 레이어가 최적의 명령어 시퀀스 선택

Mutex 구현의 역사적 진화

Linux 커널 mutex는 여러 단계의 최적화를 거쳐 현재의 형태에 이르렀습니다.

버전년도변경효과
2.6.162006mutex 도입 (Ingo Molnar)
semaphore 대체 시작
owner 추적 가능, 디버깅 향상, 구조체 최적화
3.02011Adaptive spinning (Peter Zijlstra)경합 시 즉시 sleep 대신 owner 폴링
3.152014osq_lock MCS-like 큐 도입spinner 직렬화, 캐시(Cache) 경합 감소
4.62016owner 필드에 플래그 비트 인코딩WAITERS/HANDOFF/PICKUP 상태 압축
4.82016HANDOFF 메커니즘 추가spinner에 의한 waiter starvation 방지
5.152021PREEMPT_RT 메인라인 시작mutex → rt_mutex 기반 변환 지원
6.42023cleanup.h guard 매크로(Macro)guard(mutex) 자동 unlock 패턴

__mutex_trylock 내부 로직

slow path와 mid path에서 반복적으로 호출되는 __mutex_trylock()의 정확한 동작을 분석합니다. 이 함수는 __mutex_trylock_fast()보다 복잡하며, 플래그 비트를 고려합니다.

/* kernel/locking/mutex.c */

static inline bool __mutex_trylock(struct mutex *lock)
{
    unsigned long owner, curr = (unsigned long)current;

    owner = atomic_long_read(&lock->owner);
    for (;;) {
        unsigned long flags = __owner_flags(owner);
        unsigned long task = owner & ~MUTEX_FLAGS;

        if (task) {
            /* 이미 다른 태스크가 소유 중 */
            if (flags & MUTEX_FLAG_PICKUP)
                return false;  /* HANDOFF 대상만 획득 가능 */
            return false;
        }

        /* task == NULL: 소유자 없음, 플래그만 있을 수 있음 */
        /* CAS: (0|flags) → (current|flags)
         * 플래그를 유지하면서 소유자만 설정 */
        if (atomic_long_try_cmpxchg_acquire(
                &lock->owner, &owner, curr | flags))
            return true;

        /* CAS 실패: owner가 변경됨, 재시도 */
    }
}
플래그 보존: __mutex_trylock()은 CAS 시 기존 플래그를 보존합니다. 예를 들어 WAITERS 플래그가 설정된 상태에서 trylock이 성공하면, owner는 current | MUTEX_FLAG_WAITERS가 됩니다. 이렇게 해야 unlock 시 wait_list를 확인합니다.

메모리 순서 보장(Ordering)

mutex의 lock과 unlock은 각각 acquirerelease 시맨틱을 제공합니다. 이는 임계 영역 내의 메모리 접근이 lock 밖으로 재배치(Relocation)되지 않음을 보장합니다.

/* 메모리 순서 보장 모델 */

mutex_lock():    acquire barrier
                 ↓ (임계 영역 내 모든 load/store가 이 아래로)
                 --- critical section ---
                 ↑ (임계 영역 내 모든 load/store가 이 위로)
mutex_unlock():  release barrier

/* 구체적으로: */
CPU 0:                           CPU 1:
  mutex_lock(&m);                  mutex_lock(&m);
  WRITE(data, 42);       →→→      READ(data);  // 반드시 42
  mutex_unlock(&m);                mutex_unlock(&m);

/* acquire/release 쌍이 happens-before 관계를 형성:
 * CPU 0의 unlock(release) → CPU 1의 lock(acquire)
 * → CPU 0의 모든 이전 쓰기가 CPU 1에게 가시적 */
smp_mb() 불필요: mutex를 올바르게 사용하면 추가적인 메모리 배리어(smp_mb())는 불필요합니다. mutex의 lock/unlock이 이미 필요한 배리어를 제공합니다. 불필요한 배리어 추가는 성능 저하를 유발합니다.

주요 서브시스템의 Mutex 활용

커널의 주요 서브시스템에서 mutex가 어떻게 사용되는지 살펴봅니다.

VFS: inode->i_rwsem (구 i_mutex)

/* 과거 i_mutex → 현재 i_rwsem으로 변경되었지만,
 * VFS에서 mutex 사용 패턴의 대표적 사례 */

/* 파일 생성 시: 부모 디렉토리 inode를 보호 */
inode_lock(dir);           /* i_rwsem write lock */
error = vfs_create(mnt_userns, dir, dentry, mode, true);
inode_unlock(dir);

/* 파일 크기 변경 */
inode_lock(inode);
error = do_truncate(mnt_userns, dentry, length, ...);
inode_unlock(inode);

MM: mmap_lock

/* mm_struct의 mmap_lock (rw_semaphore이지만 mutex 패턴 참고) */

/* 페이지 폴트 처리 (읽기) */
mmap_read_lock(mm);
vma = find_vma(mm, address);
handle_mm_fault(vma, address, flags, regs);
mmap_read_unlock(mm);

/* mmap()/munmap() (쓰기) */
mmap_write_lock(mm);
ret = do_mmap(file, addr, len, prot, flags, pgoff, ...);
mmap_write_unlock(mm);

디바이스 드라이버

/* 전형적인 캐릭터 디바이스 드라이버 */
struct my_char_dev {
    struct cdev cdev;
    struct mutex lock;  /* 디바이스 상태 보호 */
    u8 buffer[4096];
    size_t data_len;
};

static ssize_t my_read(struct file *f, char __user *buf,
                        size_t count, loff_t *off)
{
    struct my_char_dev *dev = f->private_data;
    int ret;

    ret = mutex_lock_interruptible(&dev->lock);
    if (ret)
        return ret;

    if (copy_to_user(buf, dev->buffer, dev->data_len)) {
        mutex_unlock(&dev->lock);
        return -EFAULT;
    }

    mutex_unlock(&dev->lock);
    return dev->data_len;
}

전체 흐름 통합 다이어그램

mutex_lock()부터 mutex_unlock()까지의 전체 흐름을 하나의 다이어그램으로 정리합니다.

mutex_lock() → mutex_unlock() 전체 흐름 mutex_lock(&lock) CAS(0 → current)? Yes LOCKED No HANDOFF 없고, osq_lock? Yes owner_on_cpu spin trylock 성공? Yes No wait_list 추가, WAITERS 플래그 설정 schedule() → SLEEP wakeup trylock 성공? Yes No → re-sleep mutex_unlock(&lock) Fast: CAS(curr→0) Slow: wake waiter (HANDOFF: owner=waiter|PICKUP)

Mutex 변형 비교 종합표

Linux 커널에서 제공하는 mutex 계열 동기화 프리미티브를 종합 비교합니다.

특성mutexrt_mutexww_mutexsemaphorerw_semaphore
소유자 추적OOOXwriter만
PI 지원XOXXX
데드락 회피XXO (Wound-Wait)XX
Optimistic SpinOOOXO (writer)
동시 접근111NN reader / 1 writer
IRQ 안전XXXXX
크기 (64bit)~32B~64B~48B~24B~40B
PREEMPT_RT→ rt_mutex동일→ rt 기반동일동일

mutex_lock() 소스 코드 완전 분석

mutex_lock()의 내부 진입점(Entry Point)인 __mutex_lock_common()은 커널 잠금 코드에서 가장 정교하게 최적화된 함수 중 하나입니다. 이 함수는 Fast Path CAS 실패 후 호출되며, optimistic spinning과 slowpath sleep을 조율합니다. v6.x 커널 기준으로 약 200줄에 달하는 이 함수의 전체 흐름을 단계별로 추적합니다.

핵심 구조를 이해하려면 함수의 제어 흐름이 3개의 명확한 단계로 분리된다는 점을 파악해야 합니다. 첫째, __mutex_trylock()으로 재시도하는 fast re-check, 둘째, mutex_optimistic_spin()을 통한 MCS 기반 spinning, 셋째, wait_list에 자신을 추가하고 schedule()로 슬립하는 slowpath입니다.

/* kernel/locking/mutex.c — __mutex_lock_common() 전체 흐름 (v6.8 기준) */

static __always_inline int __sched
__mutex_lock_common(struct mutex *lock, unsigned int state,
                    unsigned int subclass,
                    struct lockdep_map *nest_lock,
                    unsigned long ip,
                    struct ww_acquire_ctx *ww_ctx,
                    const bool use_ww_ctx)
{
    struct mutex_waiter waiter;
    int ret;

    /* ① lockdep 검증 */
    mutex_acquire_nest(&lock->dep_map, subclass, 0, nest_lock, ip);

    /* ② Fast re-check: CAS 재시도 */
    if (__mutex_trylock(lock) ||
        mutex_optimistic_spin(lock, ww_ctx, NULL)) {
        /* 획득 성공 — lock_acquired() 호출 후 리턴 */
        lock_acquired(&lock->dep_map, ip);
        if (use_ww_ctx && ww_ctx)
            __ww_mutex_check_waiters(lock, ww_ctx);
        return 0;
    }

    /* ③ Slow Path 진입: wait_lock 획득 */
    raw_spin_lock(&lock->wait_lock);

    /* 마지막 trylock 시도 (wait_lock 보호 하에) */
    if (__mutex_trylock(lock)) {
        goto skip_wait;
    }

    /* ④ 대기 리스트 등록 */
    __mutex_add_waiter(lock, &waiter, &lock->wait_list);
    waiter.task = current;

    /* WAITERS 플래그 설정: spinner에게 대기자 존재를 알림 */
    __mutex_set_flag(lock, MUTEX_FLAG_WAITERS);

    lock_contended(&lock->dep_map, ip);
    set_current_state(state);

    /* ⑤ sleep 루프 */
    for (;;) {
        if (__mutex_trylock(lock))
            goto acquired;

        /* 시그널 확인 (interruptible/killable) */
        if (signal_pending_state(state, current)) {
            ret = -EINTR;
            goto err;
        }

        raw_spin_unlock(&lock->wait_lock);
        schedule_preempt_disabled();   /* 실제 sleep */

        /* 깨어남: HANDOFF 대기자는 첫 번째로 시도 */
        first_waiter = __mutex_waiter_is_first(lock, &waiter);
        set_current_state(state);

        /* 2번 연속 깨어났는데 실패 → HANDOFF 설정 */
        if (first_waiter && __mutex_waiter_needs_handoff(&waiter))
            __mutex_set_flag(lock, MUTEX_FLAG_HANDOFF);

        raw_spin_lock(&lock->wait_lock);
    }

acquired:
    __set_current_state(TASK_RUNNING);
    __mutex_remove_waiter(lock, &waiter);

skip_wait:
    lock_acquired(&lock->dep_map, ip);
    raw_spin_unlock(&lock->wait_lock);
    return 0;

err:
    __set_current_state(TASK_RUNNING);
    __mutex_remove_waiter(lock, &waiter);
    raw_spin_unlock(&lock->wait_lock);
    mutex_release(&lock->dep_map, ip);
    return ret;
}

이 코드에서 주목할 핵심 패턴이 있습니다. wait_lockraw_spinlock_t로, wait_list 조작을 직렬화합니다. sleep 루프에서는 raw_spin_unlock → schedule → raw_spin_lock 순서를 따르며, schedule 호출 전에 반드시 spinlock을 해제하여 다른 CPU가 mutex_unlock()에서 waiter를 깨울 수 있게 합니다.

참고: __mutex_trylock()은 CAS 기반이지만, __mutex_trylock_fast()와 달리 owner 필드의 플래그 비트를 보존합니다. owner & ~MUTEX_FLAGS가 0이면(소유자 없음) 현재 task_struct 포인터를 OR 연산으로 설정하되, 기존 플래그(WAITERS, HANDOFF, PICKUP)는 유지합니다.
__mutex_lock_common() 제어 흐름 __mutex_lock_common() 진입 ① mutex_acquire_nest (lockdep) ② trylock || optimistic_spin 성공? return 0 (획득) Yes No ③ raw_spin_lock(wait_lock) ④ add_waiter + set WAITERS flag ⑤ sleep 루프: trylock → schedule trylock 성공 acquired: 획득 signal_pending? (interruptible) err: -EINTR Yes No schedule() — CPU 양보 깨어남 → 재시도 remove_waiter raw_spin_unlock 첫 번째 대기자가 2번 연속 실패: → MUTEX_FLAG_HANDOFF 설정
__mutex_lock_common()의 전체 제어 흐름. Fast re-check → optimistic spin → sleep 루프 순서로 진행되며, HANDOFF는 starvation 방지를 담당합니다.

__mutex_trylock의 플래그 보존 CAS

__mutex_trylock()은 단순 CAS가 아니라 owner 필드의 하위 3비트 플래그를 보존하면서 소유자를 설정하는 복합 연산입니다. HANDOFF가 설정된 경우 첫 번째 대기자만 획득할 수 있도록 추가 검증을 수행합니다.

/* kernel/locking/mutex.c — __mutex_trylock() */

static bool __mutex_trylock(struct mutex *lock)
{
    unsigned long owner, curr = (unsigned long)current;

    owner = atomic_long_read(&lock->owner);
    for (;;) {
        unsigned long flags = __owner_flags(owner);
        unsigned long task = owner & ~MUTEX_FLAGS;

        /* 이미 다른 소유자가 있으면 실패 */
        if (task) {
            /* HANDOFF+PICKUP인데 자신이 대상자이면 획득 가능 */
            if (flags & MUTEX_FLAG_PICKUP) {
                if (task != curr)
                    return false;
                flags &= ~MUTEX_FLAG_PICKUP;
            } else {
                return false;
            }
        }

        /* CAS: owner를 (0|flags) → (current|flags)로 변경
         * 플래그 비트(WAITERS 등)는 보존 */
        if (atomic_long_try_cmpxchg_acquire(
                &lock->owner, &owner, curr | flags))
            return true;
    }
}
주의: __mutex_trylock()의 CAS 루프는 ABA 문제를 걱정할 필요가 없습니다. owner 필드는 task_struct 포인터(4KB 정렬)와 3비트 플래그의 조합이므로, 같은 값이 재사용될 확률은 사실상 0입니다. 그러나 루프가 지속적으로 실패하면 spinning보다 sleep이 효율적이므로, 호출자(sleep 루프)가 schedule()로 양보합니다.

mutex_unlock() 소스 상세

mutex_unlock()mutex_lock()보다 단순하지만, slowpath에서 waiter 깨우기(Wakeup)의 세부 사항은 중요합니다. Fast Path는 단일 atomic 연산으로 owner를 클리어하고, 대기자가 있으면 slowpath로 진입하여 wake_q 메커니즘을 통해 배치 웨이크업을 수행합니다.

/* kernel/locking/mutex.c — mutex_unlock() 전체 경로 */

void __sched mutex_unlock(struct mutex *lock)
{
    /* lockdep: 소유권 해제 기록 */
    mutex_release(&lock->dep_map, _RET_IP_);

    /* Fast Path: WAITERS/HANDOFF 플래그 없으면 즉시 해제 */
    if (__mutex_unlock_fast(lock))
        return;

    /* Slow Path: 대기자 존재 */
    __mutex_unlock_slowpath(lock, _RET_IP_);
}

static __always_inline bool __mutex_unlock_fast(struct mutex *lock)
{
    unsigned long curr = (unsigned long)current;

    /* CAS: owner == current(플래그 없음) → 0
     * 플래그가 설정된 경우 CAS 실패 → slowpath */
    return atomic_long_try_cmpxchg_release(
               &lock->owner, &curr, 0UL);
}

Fast Path의 CAS가 실패하는 경우는 owner 필드에 MUTEX_FLAG_WAITERS 또는 MUTEX_FLAG_HANDOFF가 설정되어 있을 때입니다. 이 경우 단순히 0으로 클리어하면 대기자를 깨우지 못하므로, slowpath가 반드시 필요합니다.

/* kernel/locking/mutex.c — __mutex_unlock_slowpath() */

static noinline void __mutex_unlock_slowpath(
    struct mutex *lock, unsigned long ip)
{
    struct task_struct *next = NULL;
    DEFINE_WAKE_Q(wake_q);
    unsigned long owner;

    raw_spin_lock(&lock->wait_lock);

    if (!list_empty(&lock->wait_list)) {
        /* 첫 번째 대기자 가져오기 */
        struct mutex_waiter *waiter =
            list_first_entry(&lock->wait_list,
                              struct mutex_waiter, list);
        next = waiter->task;

        /* HANDOFF인 경우: owner를 직접 waiter로 이전 */
        if (__mutex_waiter_is_handoff(waiter)) {
            /* owner = next | MUTEX_FLAG_PICKUP
             * waiter가 깨어나면 PICKUP 보고 즉시 획득 */
            __mutex_handoff(lock, next);
        } else {
            /* 일반 해제: owner 클리어 */
            __mutex_unlock_common_slowpath(lock);
        }

        wake_q_add(&wake_q, next);
    } else {
        /* 대기자 없음: 단순 해제 */
        __mutex_unlock_common_slowpath(lock);
    }

    raw_spin_unlock(&lock->wait_lock);
    wake_up_q(&wake_q);  /* 배치 웨이크업 */
}

wake_q 배치 웨이크업

wake_q는 여러 태스크를 한 번에 깨우기 위한 큐입니다. mutex_unlock()에서는 보통 하나의 waiter만 깨우지만, wake_q를 사용하는 이유는 raw_spin_unlock 이후에 실제 웨이크업을 수행하여 spinlock 보유 시간을 최소화하기 위함입니다.

단계동작lock->wait_lock
1raw_spin_lock(&lock->wait_lock)보유
2첫 번째 waiter 확인, HANDOFF 처리보유
3wake_q_add(&wake_q, next)보유
4raw_spin_unlock(&lock->wait_lock)해제
5wake_up_q(&wake_q) — 실제 try_to_wake_up() 호출해제
참고: HANDOFF 경로에서 __mutex_handoff()는 owner를 next | MUTEX_FLAG_PICKUP으로 설정합니다. 깨어난 waiter는 __mutex_trylock()에서 PICKUP 플래그를 발견하고, 자신의 task_struct와 일치하면 즉시 획득합니다. 이렇게 하면 다른 spinner가 가로채는 것을 방지합니다.
주의: mutex_unlock()은 반드시 mutex_lock()을 호출한 태스크에서 호출해야 합니다. CONFIG_DEBUG_MUTEXES 활성화 시 소유자 불일치가 감지되면 DEBUG_LOCKS_WARN_ON()이 발생합니다. 소유자 규칙을 위반하는 설계는 근본적으로 재구성해야 합니다.

cleanup.h 가드와 scoped mutex

Linux 6.4부터 도입된 cleanup.h 프레임워크는 GCC/Clang의 __attribute__((__cleanup__))를 활용하여 스코프 기반 자동 unlock을 제공합니다. C 언어에 RAII(Resource Acquisition Is Initialization) 패턴을 구현한 것으로, mutex의 unlock 누락 버그를 구조적으로 방지합니다.

전통적인 mutex 사용에서 가장 빈번한 버그는 에러 경로에서 mutex_unlock()을 빠뜨리는 것입니다. 복잡한 함수에서 여러 goto 레이블을 관리하거나, 조기 return 경로마다 unlock을 삽입해야 하는 부담을 guard(mutex)가 완전히 제거합니다.

/* include/linux/cleanup.h — 핵심 매크로 정의 */

/* CLASS 매크로: 타입별 생성자/소멸자 등록 */
DEFINE_CLASS(mutex,
    struct mutex *,        /* 관리 대상 타입 */
    mutex_unlock(_T),       /* 소멸자: 스코프 종료 시 호출 */
    mutex_lock(_T), _T,     /* 생성자: 스코프 진입 시 호출 */
    struct mutex *_T        /* 인자 */
);

/* guard() 매크로 확장 */
guard(mutex)(&my_lock);
/* 내부적으로 다음과 같이 확장:
 * struct mutex *__guard_mutex_ptr
 *     __attribute__((__cleanup__(mutex_unlock_cleanup)))
 *     = mutex_lock_return(&my_lock);
 */

/* scoped_guard: 블록 범위 가드 */
scoped_guard(mutex, &my_lock) {
    /* 이 블록이 끝나면 자동 unlock */
    data->value = new_value;
    data->timestamp = ktime_get();
}

guard(mutex) 실전 패턴

/* 패턴 1: 전통적 방식 — 에러 경로마다 unlock 필요 */
static int old_style(struct device *dev)
{
    int ret;

    mutex_lock(&dev->lock);

    ret = prepare_something(dev);
    if (ret)
        goto out_unlock;   /* unlock 누락 위험! */

    ret = do_operation(dev);
    if (ret)
        goto out_unlock;

    ret = finalize(dev);

out_unlock:
    mutex_unlock(&dev->lock);
    return ret;
}

/* 패턴 2: guard(mutex) — 스코프 종료 시 자동 unlock */
static int new_style(struct device *dev)
{
    guard(mutex)(&dev->lock);   /* 함수 끝에서 자동 unlock */

    int ret = prepare_something(dev);
    if (ret)
        return ret;            /* 자동 unlock! */

    ret = do_operation(dev);
    if (ret)
        return ret;            /* 자동 unlock! */

    return finalize(dev);       /* 자동 unlock! */
}

/* 패턴 3: scoped_guard — 블록 범위 제한 */
static ssize_t read_with_scoped(struct device *dev,
                                 char *buf, size_t len)
{
    size_t count;

    scoped_guard(mutex, &dev->lock) {
        count = min(len, dev->data_len);
        memcpy(buf, dev->buffer, count);
    }
    /* 여기서는 이미 unlock된 상태 */

    return count;
}

/* 패턴 4: scoped_guard + interruptible */
DEFINE_CLASS(mutex_intr,
    struct mutex *,
    if (_T) mutex_unlock(_T),
    mutex_lock_interruptible(_T) ? NULL : _T,
    struct mutex *_T
);

scoped_guard(mutex_intr, &lock) {
    /* 시그널로 중단되면 블록을 건너뜀 */
    do_work();
}
guard(mutex) 컴파일러 확장과 스코프 생명주기 소스 코드 guard(mutex)(&lock); // ... 임계 영역 ... return result; // OK return -EINVAL; // OK return -ENOMEM; // OK ← 모든 return에서 자동 unlock 컴파일러 확장 컴파일러 확장 결과 struct mutex *__ptr __cleanup(mutex_unlock_wrap) = ({ mutex_lock(&lock); &lock; }); // 변수 소멸 시 → mutex_unlock(__ptr) // GCC __attribute__((__cleanup__)) 스코프 생명주기 mutex_lock() 임계 영역 (return 가능) mutex_unlock() 자동 어떤 경로로 스코프를 벗어나든 __cleanup 소멸자가 실행됩니다
guard(mutex)는 GCC/Clang의 __cleanup 속성을 활용하여 변수 소멸 시 자동으로 mutex_unlock()을 호출합니다.
매크로동작사용 시나리오
guard(mutex)(&lock)함수 끝까지 lock 유지, 스코프 종료 시 unlock함수 전체가 임계 영역인 경우
scoped_guard(mutex, &lock) { }블록 내에서만 lock 유지임계 영역을 블록으로 제한할 때
guard(mutex_intr)(&lock)interruptible lock + 자동 unlock시그널 처리가 필요한 경우
CLASS(mutex, ptr)(&lock)명시적 변수명으로 guard 생성가드 변수를 직접 참조할 때
참고: guard() 매크로는 6.4 커널에서 도입되었으며, 6.5~6.8에서 커널 전반으로 확산되고 있습니다. 새로운 드라이버와 서브시스템 코드에서는 guard(mutex) 사용이 적극 권장됩니다. 다만, scoped_guard 블록 안에서 goto로 블록 밖으로 점프하면 소멸자가 호출되지 않으므로 주의해야 합니다.

Contention 프로파일링 실전

mutex contention은 시스템 성능의 주요 병목(Bottleneck)입니다. 커널은 perf lock, /proc/lock_stat, BPF 기반 트레이싱 등 다양한 도구를 제공하며, 각 도구는 서로 다른 수준의 세밀도와 오버헤드를 가집니다. 실전에서는 이들을 조합하여 경합 지점을 빠르게 식별하고 최적화 방향을 결정합니다.

perf lock contention

perf lock contention은 커널 6.2부터 BPF 기반으로 동작하며, 잠금 경합(Lock Contention)을 실시간으로 분석합니다. tracepoint 기반보다 오버헤드가 낮고 프로덕션 환경에서도 사용할 수 있습니다.

# perf lock contention — BPF 기반 실시간 분석
$ perf lock contention -ab sleep 5
 contended   total wait     max wait     avg wait         type   caller
      1842      45.2ms     12.3ms      24.5us        mutex   ext4_fill_super+0x1a3
       924      18.7ms      5.1ms      20.2us        mutex   do_mmap+0x8f
       456       8.3ms      2.8ms      18.2us        mutex   i915_gem_object_lock+0x42

# 스택 트레이스 포함
$ perf lock contention -abs sleep 10

# 특정 프로세스만 추적
$ perf lock contention -abp 1234 sleep 5

# 호출 스택 깊이 조절
$ perf lock contention -ab --stack-depth 8 sleep 5

/proc/lock_stat

CONFIG_LOCK_STAT=y를 활성화하면 /proc/lock_stat에서 모든 잠금의 경합 통계를 확인할 수 있습니다. 오버헤드가 상당하므로 프로덕션보다는 개발/테스트 환경에 적합합니다.

# lock_stat 활성화/초기화
$ echo 1 > /proc/sys/kernel/lock_stat
$ echo 0 > /proc/lock_stat    # 카운터 리셋

# 워크로드 실행 후 분석
$ cat /proc/lock_stat | head -30
lock_stat version 0.4
------------------------------------------------------
               class name    con-bounces  contentions  waittime-min  ...
------------------------------------------------------
          &sb->s_type->...:      12483        8921          0.42  ...
              &mm->mmap_lock:       5621        3244          0.31  ...

# 상위 경합 잠금만 추출
$ sort -k 4 -rn /proc/lock_stat | head -20

BPF/bpftrace 트레이싱

# bpftrace: mutex 경합 히트맵 (대기 시간 분포)
$ bpftrace -e '
tracepoint:lock:contention_begin /args->flags == 0x2/ {
    @start[tid] = nsecs;
}
tracepoint:lock:contention_end /args->flags == 0x2/ {
    if (@start[tid]) {
        @wait_us = hist((nsecs - @start[tid]) / 1000);
        delete(@start[tid]);
    }
}'

# 특정 mutex 주소 추적
$ bpftrace -e '
kprobe:mutex_lock {
    @addr[arg0] = count();
}
END {
    print(@addr, 10);  /* 상위 10개 mutex */
}'

# mutex 보유 시간 측정 (lock→unlock)
$ bpftrace -e '
kprobe:mutex_lock { @lock_time[tid] = nsecs; }
kprobe:mutex_unlock /@lock_time[tid]/ {
    @hold_us = hist((nsecs - @lock_time[tid]) / 1000);
    delete(@lock_time[tid]);
}'
Mutex Contention 프로파일링 도구 계층 Low Overhead perf lock contention (BPF 기반, <2% overhead) Medium Overhead bpftrace one-liners (kprobe 기반, ~5% overhead) High Overhead /proc/lock_stat (CONFIG_LOCK_STAT, ~15%) 프로덕션 안전 경합 횟수 + 대기 시간 호출 스택 추적 가능 → 초기 진단에 최적 커스텀 필터링 가능 보유 시간 히스토그램 특정 mutex 주소 추적 → 분석에 최적 모든 잠금 통계 수집 bounce 횟수 포함 lockdep class별 분류 → 전체 잠금 감사에 최적 분석 흐름: perf lock (발견) → bpftrace (상세) → lock_stat (종합 감사) 오버헤드 증가 →
3단계 프로파일링: 프로덕션에서는 perf lock으로 시작하고, 특정 잠금을 bpftrace로 분석하며, 전체 감사에 lock_stat을 사용합니다.
도구오버헤드커널 설정장점단점
perf lock contention<2%CONFIG_BPF프로덕션 안전, 스택 포함커널 6.2+ 필요
bpftrace~5%CONFIG_BPF_EVENTS유연한 커스텀 분석스크립트 작성 필요
/proc/lock_stat~15%CONFIG_LOCK_STAT포괄적 통계프로덕션 부적합
lockdep~30%CONFIG_PROVE_LOCKING데드락 탐지성능 분석 아님
ftrace mutex events~3%TRACE_EVENTS시계열 분석대용량 데이터
참고: mutex contention 분석 시 가장 먼저 확인할 것은 경합 횟수가 아니라 총 대기 시간(Latency)입니다. 경합 횟수가 높아도 대기 시간이 짧으면(optimistic spinning으로 빠르게 획득) 성능 영향이 미미할 수 있습니다. 반면 경합 횟수가 적더라도 단건 대기 시간이 수백 ms에 달하면 심각한 레이턴시 문제를 유발합니다.

벤치마크: mutex vs spinlock vs rwsem

mutex, spinlock, rw_semaphore의 성능 특성은 경합 수준, 임계 영역 길이, CPU 수에 따라 극적으로 달라집니다. locktorture 모듈과 custom 벤치마크를 사용한 측정 결과를 분석합니다. 모든 수치는 x86-64 환경(Intel Xeon, 48 코어)에서 측정되었으며, 임계 영역 내에서 변수 1개를 증가시키는 단순 워크로드 기준입니다.

Uncontended (경합 없음, 단일 스레드)

프리미티브lock+unlock 평균주요 비용
spinlock~15 nsLOCK CMPXCHG (atomic RMW)
mutex (fast path)~22 nsLOCK CMPXCHG + might_sleep 체크
rw_semaphore (read)~25 nsatomic_long_add + might_sleep
rw_semaphore (write)~28 nsLOCK CMPXCHG + reader 카운터 체크

경합이 없는 상황에서는 spinlock이 가장 빠릅니다. mutex의 fast path도 단일 CAS이지만 might_sleep() 체크와 lockdep 오버헤드로 약 7ns 추가됩니다. 이 차이는 초당 수백만 회 호출되는 hot path에서만 유의미합니다.

Contended (경합, 다중 스레드)

스레드 수spinlock (ops/s)mutex (ops/s)rwsem-read (ops/s)
166M45M40M
214M18M38M
45.2M12M36M
82.1M8.5M34M
160.9M5.8M32M
320.4M3.2M28M
480.2M1.8M24M

경합이 증가하면 spinlock의 성능이 급격히 저하됩니다. busy-wait이 CPU를 소비하므로 스레드 수가 증가할수록 캐시 라인 바운싱이 심해지기 때문입니다. mutex는 optimistic spinning(MCS 기반)이 캐시 라인 바운싱을 줄이고, spinning 실패 시 sleep하여 CPU를 양보하므로 경합 상황에서 spinlock보다 처리량이 높습니다.

경합 시 처리량 비교 (ops/sec, log scale 근사) 66M 14M 5M 2M 0.5M 0.2M 1 2 4 8 16 32 48 스레드 수 spinlock mutex rwsem (read)
스레드 수 증가에 따른 처리량 변화. spinlock은 경합 시 급격히 저하되는 반면, mutex는 optimistic spinning 덕분에 완만하게 감소합니다.

임계 영역 길이에 따른 최적 선택

임계 영역 길이최적 프리미티브이유
<100 nsspinlockcontext switch 비용이 임계 영역보다 큼
100 ns ~ 10 usmutexoptimistic spinning이 sleep보다 효율적
10 us ~ 1 msmutexsleep하여 CPU 양보가 전체 처리량 향상
>1 msmutex_interruptible긴 대기에 시그널 취소 가능성 필요
I/O 포함mutex_lock_ioI/O 대기 시간을 iowait으로 정확히 계정
주의: locktorture(CONFIG_LOCK_TORTURE_TEST)는 극단적 경합 상황을 인위적으로 생성하므로, 실제 워크로드의 경합 패턴과 다를 수 있습니다. 벤치마크 결과는 참고용이며, 실제 시스템에서는 perf lock contention으로 워크로드별 프로파일링을 수행해야 합니다.

LKMM 관점의 mutex 메모리 순서

Linux Kernel Memory Model(LKMM)은 커널 동기화 프리미티브의 메모리 순서 보장을 형식적으로 정의합니다. mutex는 LKMM에서 acquire/release 쌍으로 모델링되며, 이는 임계 영역 내의 모든 메모리 접근이 lock/unlock 경계 밖으로 재배치되지 않음을 형식적으로 보장합니다.

실제 구현에서 mutex_lock()은 atomic_long_try_cmpxchg_acquire()를 사용하여 acquire 시맨틱을, mutex_unlock()은 atomic_long_try_cmpxchg_release()를 사용하여 release 시맨틱을 제공합니다. 이 두 배리어가 결합하여 임계 영역의 모든 load/store가 가시적(visible)임을 보장합니다.

Litmus 테스트로 보는 mutex 보장

/* LKMM litmus test: mutex는 happens-before를 보장 */

C mutex-ordering

/*
 * 초기 상태:
 * x = 0; y = 0;
 * mutex M은 unlocked
 */

P0(int *x, int *y, mutex_t *M) {
    mutex_lock(M);      /* acquire */
    WRITE_ONCE(*x, 1);
    WRITE_ONCE(*y, 1);
    mutex_unlock(M);     /* release */
}

P1(int *x, int *y, mutex_t *M) {
    mutex_lock(M);      /* acquire */
    r0 = READ_ONCE(*y);
    r1 = READ_ONCE(*x);
    mutex_unlock(M);     /* release */
}

/* exists (1:r0=1 /\ 1:r1=0) -- 불가능!
 * P1이 y=1을 관찰했다면 반드시 x=1도 관찰해야 함
 * → release(P0 unlock) → acquire(P1 lock) 순서가 보장 */

형식적 순서 규칙

관계LKMM 표현의미
mutex_lock → 임계 영역acquire → polock 이후의 모든 접근은 lock 이전으로 재배치 불가
임계 영역 → mutex_unlockpo → releaseunlock 이전의 모든 접근은 unlock 이후로 재배치 불가
unlock → lock (다른 CPU)release → acquire (sw)P0의 unlock과 P1의 lock 사이에 synchronizes-with 관계
전이적 가시성hb (happens-before)위 관계의 전이적 닫힘: P0의 모든 선행 쓰기가 P1에게 가시적
LKMM: mutex acquire/release 메모리 순서 CPU 0 (P0) mutex_lock(M) [acquire] WRITE_ONCE(*x, 1) WRITE_ONCE(*y, 1) mutex_unlock(M) [release] CPU 1 (P1) mutex_lock(M) [acquire] r0 = READ_ONCE(*y) → 1 r1 = READ_ONCE(*x) → 반드시 1 mutex_unlock(M) [release] synchronizes-with (sw) release → acquire
P0의 unlock(release)과 P1의 lock(acquire) 사이에 synchronizes-with 관계가 형성되어, P0의 모든 선행 쓰기가 P1에게 가시적입니다.

LKMM에서 mutex의 메모리 순서 보장은 두 가지 핵심 속성으로 요약됩니다. 첫째, 상호 배제(mutual exclusion) — 동일 mutex의 임계 영역은 겹치지 않습니다. 둘째, 순서 보장(ordering) — 한 임계 영역의 모든 쓰기는 다음 임계 영역에서 가시적입니다. 이 두 속성이 결합하여 mutex는 순차적 일관성(sequential consistency)보다 약하지만 실용적으로 충분한 동기화를 제공합니다.

참고: mutex_trylock()도 성공 시 acquire 시맨틱을 제공합니다. 실패 시에는 어떤 메모리 순서 보장도 제공하지 않습니다. tools/memory-model/ 디렉토리에서 herd7 도구를 사용하여 LKMM litmus 테스트를 직접 실행할 수 있습니다: herd7 -conf linux-kernel.cfg litmus-tests/locking/mutex-ordering.litmus

서브시스템 사례: mmap_lock 추적

mmap_lock은 Linux 커널에서 가장 경합이 심한 잠금 중 하나입니다. mm_struct의 가상 메모리(Virtual Memory) 영역(VMA) 리스트를 보호하며, 페이지 폴트(Page Fault), mmap/munmap 시스템 콜(System Call), /proc/pid/maps 읽기 등 다양한 경로에서 접근합니다. 원래 mmap_sem이라는 세마포어였지만, v5.8에서 mmap_lock으로 이름이 변경되고 rw_semaphore로 구현됩니다.

mmap_lock의 경합은 특히 멀티스레드 애플리케이션에서 심각합니다. 페이지 폴트마다 read lock을 획득해야 하고, mmap/munmap은 write lock이 필요하므로, 메모리 집약적 워크로드에서 병목이 됩니다. 커널 개발자들은 이를 완화하기 위해 per-VMA lock, speculative page fault 등의 기술을 지속적으로 도입하고 있습니다.

mmap_lock API와 래퍼

/* include/linux/mmap_lock.h — mmap_lock 래퍼 */

/* 이름 변경 히스토리:
 * v5.8 이전: down_read(&mm->mmap_sem) / up_read(&mm->mmap_sem)
 * v5.8 이후: mmap_read_lock(mm) / mmap_read_unlock(mm) */

static inline void mmap_read_lock(struct mm_struct *mm)
{
    down_read(&mm->mmap_lock);
    __mmap_lock_trace_start_locking(mm, false);
}

static inline void mmap_write_lock(struct mm_struct *mm)
{
    down_write(&mm->mmap_lock);
    __mmap_lock_trace_start_locking(mm, true);
}

/* v6.4+: lock_mm_and_find_vma — 페이지 폴트 최적화 */
struct vm_area_struct *lock_mm_and_find_vma(
    struct mm_struct *mm,
    unsigned long addr,
    struct pt_regs *regs)
{
    /* 먼저 per-VMA lock 시도 (v6.4+) */
    vma = lock_vma_under_rcu(mm, addr);
    if (vma)
        return vma;  /* mmap_lock 없이 성공! */

    /* 실패: mmap_lock read lock으로 폴백 */
    mmap_read_lock(mm);
    vma = find_vma(mm, addr);
    return vma;
}

mmap_lock tracepoint 활용

# mmap_lock tracepoint 확인
$ ls /sys/kernel/debug/tracing/events/mmap_lock/
mmap_lock_acquire_returned  mmap_lock_start_locking

# mmap_lock 경합 추적
$ echo 1 > /sys/kernel/debug/tracing/events/mmap_lock/enable
$ cat /sys/kernel/debug/tracing/trace_pipe
  bash-1234  [003] .... 1234.567890: mmap_lock_start_locking: mm=0xffff... write=1
  bash-1234  [003] .... 1234.567920: mmap_lock_acquire_returned: mm=0xffff... write=1 success=1

# bpftrace로 mmap_lock 보유 시간 측정
$ bpftrace -e '
tracepoint:mmap_lock:mmap_lock_start_locking {
    @start[tid] = nsecs;
}
tracepoint:mmap_lock:mmap_lock_acquire_returned /@start[tid]/ {
    @wait_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'
mmap_lock 경합 경로와 per-VMA lock 최적화 Page Fault 발생 lock_vma_under_rcu() (v6.4+) 성공 VMA lock만으로 폴트 처리 실패 mmap_read_lock(mm) 경합 소스들 mmap() — write lock munmap() — write lock mprotect() — write lock fork() — write lock /proc/maps — read lock page fault — read lock find_vma() + handle_mm_fault() mmap_read_unlock(mm) mmap_lock 경합 완화 진화 v5.8: mmap_sem→mmap_lock v6.1: maple tree (RCU lookup) v6.4: per-VMA lock v6.7+: speculative fault
mmap_lock은 페이지 폴트의 주요 경합 지점입니다. v6.4부터 per-VMA lock으로 read 경로를 우회합니다.
커널 버전최적화효과
v5.8mmap_sem → mmap_lock 리네이밍tracepoint 추가, API 정리
v6.1Maple Tree로 VMA 관리 구조 변경RCU 기반 VMA lookup 가능
v6.4per-VMA lock (lock_vma_under_rcu)페이지 폴트에서 mmap_lock 우회, 75% 경합 감소
v6.7+speculative page fault 확장anonymous page에 대해 mmap_lock 없는 폴트 처리
참고: per-VMA lock은 mutex가 아닌 rw_semaphore입니다. 각 VMA마다 별도의 lock을 두어 서로 다른 VMA에 대한 페이지 폴트가 병렬로 처리됩니다. 이 설계는 "하나의 거대한 잠금 대신 세밀한 잠금"이라는 동기화의 핵심 원칙을 보여줍니다. perf lock contention으로 mmap_lock 경합을 측정하면 per-VMA lock 도입 전후의 차이를 극적으로 확인할 수 있습니다.

커널 버전별 진화

Linux 커널의 mutex 구현은 v2.6.16에서 처음 도입된 이후 지속적으로 진화해왔습니다. 단순한 sleeping lock에서 시작하여 adaptive spinning, osq_lock, HANDOFF, 그리고 최근의 cleanup.h guard까지, 각 단계는 실제 워크로드의 성능 문제를 해결하기 위한 것이었습니다.

이 진화 과정을 이해하면 왜 현재의 mutex가 이렇게 복잡한 구조를 가지게 되었는지, 그리고 각 최적화가 어떤 문제를 해결하는지를 명확히 파악할 수 있습니다.

mutex 구현 진화 타임라인 v2.6.16 (2006-03) Ingo Molnar: struct mutex 도입. 기존 semaphore(count=1)를 대체. 단순 sleep/wakeup 구조. 혁신: 소유자 추적 → lockdep 통합 → 디버깅 용이성 비약적 향상 v3.0 (2011-07) Adaptive/Optimistic Spinning 도입. owner가 CPU에서 실행 중이면 sleep 대신 spin. 효과: 높은 경합 상황에서 context switch 50~70% 감소 v3.15 (2014-06) MCS 기반 osq_lock 도입. spinner들이 로컬 변수에서 spin → 캐시 라인 바운싱 최소화. 효과: NUMA 시스템에서 spinning 오버헤드 80% 감소 v4.6 (2016-05) owner 필드에 task_struct 포인터 + 플래그 비트 인코딩. WAITERS/HANDOFF 플래그 추가. 효과: spinner와 sleeper 간의 정보 교환 효율화, starvation 방지 기반 마련 v5.0 (2019-03) HANDOFF 메커니즘 도입. 대기자가 2번 연속 깨어났는데 실패하면 소유권을 직접 이전. 효과: optimistic spinner에 의한 sleeper starvation 완전 해결 v6.4 (2023-06) cleanup.h guard(mutex) 도입. __cleanup 속성 기반 RAII 자동 unlock. 효과: unlock 누락 버그 구조적 방지, 에러 경로 코드 대폭 간소화 v6.7+ (2024~) guard(mutex) 커널 전반 확산, scoped_guard 패턴 표준화, lockdep 통합 강화
v2.6.16의 단순 sleeping lock에서 v6.x의 3단계 경로 + HANDOFF + cleanup.h guard까지, 20년에 걸친 mutex 진화.
버전변경해결한 문제커밋/패치 시리즈
v2.6.16struct mutex 도입semaphore(count=1)의 lockdep 불가Ingo Molnar 패치 시리즈
v3.0Adaptive spinning불필요한 context switchPeter Zijlstra
v3.15osq_lock (MCS)cacheline bouncing during spinJason Low, Davidlohr Bueso
v4.6owner + flags encodinglock 상태 조회의 atomicityPeter Zijlstra
v5.0HANDOFFspinner에 의한 sleeper starvationPeter Zijlstra
v6.4cleanup.h guard에러 경로 unlock 누락Peter Zijlstra, Kent Overstreet
참고: mutex의 진화는 "단순한 것에서 시작하여 실제 문제를 만날 때마다 복잡성을 추가"하는 커널 개발 철학을 잘 보여줍니다. 초기의 단순한 sleep/wakeup mutex도 기능적으로 완전했지만, 실제 워크로드에서 성능 문제가 드러날 때마다 adaptive spinning, osq_lock, HANDOFF가 순차적으로 추가되었습니다. 각 최적화는 독립적으로 검증 가능하며, CONFIG_MUTEX_SPIN_ON_OWNER 등으로 선택적 비활성화가 가능합니다.

보안: 타이밍 사이드채널

잠금 경합은 의도치 않게 타이밍 사이드채널을 형성할 수 있습니다. 공격자가 mutex_lock()의 대기 시간을 측정하면 다른 프로세스가 해당 자원을 사용하고 있는지, 얼마나 오래 임계 영역에 머무는지를 추론할 수 있습니다. 이는 특히 가상화(Virtualization) 환경과 컨테이너(Container)에서 격리(isolation)를 위협합니다.

공격 벡터

mutex 기반 타이밍 공격의 주요 벡터는 세 가지입니다. 첫째, 경합 탐지(contention detection) — 특정 시스템 콜의 latency 변화로 다른 프로세스의 활동을 추론합니다. 둘째, 임계 영역 길이 추론 — mutex 대기 시간으로 보호된 데이터의 크기나 처리 복잡도를 추정합니다. 셋째, 스케줄링 기반 추론 — optimistic spinning vs sleep 경로의 차이로 소유자의 CPU 상태를 파악합니다.

공격 벡터측정 대상추론 가능 정보위험도
경합 탐지시스템 콜 latency다른 VM/컨테이너의 자원 접근 패턴중간
임계 영역 길이mutex 대기 시간보호된 데이터 크기, 암호 키 길이높음
spinning vs sleep응답 시간 분포소유자의 CPU affinity, NUMA 노드낮음
HANDOFF 트리거2번째 wakeup 후 지연경합 패턴, 스케줄링 우선순위낮음

완화 기법

/* 완화 기법 1: 일정 시간(constant-time) 임계 영역
 * 민감한 데이터를 다루는 경우, 데이터 크기에 관계없이
 * 항상 동일한 시간이 걸리도록 구현 */

static int crypto_check_key(const u8 *key, size_t len)
{
    guard(mutex)(&crypto_lock);

    /* BAD: 조기 반환은 키 길이를 노출
     * if (len != expected_len) return -EINVAL;
     */

    /* GOOD: 항상 전체 비교 수행 */
    int result = crypto_memneq(key, expected_key, MAX_KEY_LEN);
    return result ? -EINVAL : 0;
}

/* 완화 기법 2: 격리된 잠금 사용
 * 보안 경계를 넘는 공유 잠금을 피하고,
 * 컨텍스트별 별도 잠금을 사용 */

/* BAD: 모든 사용자가 하나의 mutex 공유 */
static DEFINE_MUTEX(global_crypto_lock);

/* GOOD: 사용자별 별도 mutex */
struct crypto_ctx {
    struct mutex lock;  /* 사용자별 독립 잠금 */
    u8 key[32];
};

/* 완화 기법 3: 잡음(noise) 추가 — 극히 예외적 경우에만 */
static void sensitive_operation(void)
{
    guard(mutex)(&sensitive_lock);

    do_actual_work();

    /* 타이밍 잡음: 의도적 지연 (성능 비용 큼, 신중히 사용) */
    if (IS_ENABLED(CONFIG_SECURITY_TIMING_MITIGATIONS))
        udelay(get_random_u32_below(100));
}
주의: mutex 타이밍 사이드채널은 대부분의 워크로드에서 실질적 위험이 낮습니다. 완화 기법은 성능 비용이 크므로, 암호화(Encryption) 키 관리, 인증 코드 등 명확하게 민감한 코드에만 적용해야 합니다. 과도한 타이밍 방어는 오히려 시스템 성능을 심각하게 저하시킵니다. 가상화 환경에서의 격리는 mutex 수준보다는 하이퍼바이저(Hypervisor) 레벨(vCPU 스케줄링 격리, 캐시 파티셔닝)에서 대응하는 것이 효과적입니다.

특히 컨테이너 환경에서 주의할 점은 cgroup 관련 잠금입니다. cgroup_mutex는 모든 cgroup 계층에서 공유되므로, 한 컨테이너의 cgroup 조작이 다른 컨테이너의 cgroup 작업을 지연시킬 수 있습니다. 이는 사이드채널뿐만 아니라 DoS(서비스 거부) 벡터이기도 합니다. 커널 개발자들은 cgroup 잠금의 세밀화를 지속적으로 추진하고 있습니다.

실전 디버깅 시나리오

mutex 관련 버그는 데드락, hung task, priority inversion, unlock 누락 등 다양한 형태로 나타납니다. 각 시나리오별로 증상 식별, 진단 도구 활용, 원인 분석, 수정 방법을 구체적으로 다룹니다.

시나리오 1: hung_task 경고

태스크가 TASK_UNINTERRUPTIBLE 상태로 CONFIG_DEFAULT_HUNG_TASK_TIMEOUT(기본 120초) 이상 머무르면 hung_task 경고가 발생합니다. mutex_lock()은 기본적으로 UNINTERRUPTIBLE이므로, 장시간 경합이나 소유자의 비정상 동작 시 이 경고가 나타납니다.

/* hung_task 경고 예시 */
INFO: task kworker/0:1:1234 blocked for more than 120 seconds.
      Not tainted 6.8.0 #1
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:kworker/0:1     state:D stack:13456 pid: 1234 ppid:     2
Call Trace:
 <TASK>
 __schedule+0x3e8/0x1150
 schedule+0x5c/0xd0
 schedule_preempt_disabled+0x15/0x30
 __mutex_lock.constprop.0+0x3e8/0x750
 __mutex_lock_slowpath+0x13/0x20
 mutex_lock+0x3e/0x50
 my_driver_write+0x45/0x120   ← 여기서 블록
 vfs_write+0x1a2/0x520
 ...
# 진단 절차

# 1. 블록된 태스크와 대기 중인 mutex 확인
$ cat /proc/1234/stack
[<0>] __mutex_lock.constprop.0+0x3e8/0x750
[<0>] my_driver_write+0x45/0x120

# 2. mutex 소유자 확인 (crash 또는 /proc/lock_stat)
$ cat /proc/1234/wchan
__mutex_lock_slowpath

# 3. lockdep로 의존성 확인
$ cat /proc/lockdep_chains | grep my_driver

# 4. 모든 D 상태 태스크 조회
$ ps aux | awk '$8 ~ /D/ { print }'

# 5. SysRq로 전체 태스크 상태 덤프
$ echo w > /proc/sysrq-trigger  # blocked tasks
$ echo t > /proc/sysrq-trigger  # all tasks

시나리오 2: lockdep ABBA 데드락 감지

/* lockdep 데드락 경고 예시 */
======================================================
WARNING: possible circular locking dependency detected
6.8.0 #1 Not tainted
------------------------------------------------------
process_a/5678 is trying to acquire lock:
ffff88810a2b3c40 (&dev->lock_b){+.+.}-{3:3}, at: func_x+0x30

but task is already holding lock:
ffff88810a2b3c00 (&dev->lock_a){+.+.}-{3:3}, at: func_x+0x18

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

-> #1 (&dev->lock_b){+.+.}-{3:3}:
       lock_acquire+0xd4/0x2c0
       __mutex_lock+0x9c/0x750
       func_y+0x18/0x60     ← lock_b 먼저 획득

-> #0 (&dev->lock_a){+.+.}-{3:3}:
       lock_acquire+0xd4/0x2c0
       __mutex_lock+0x9c/0x750
       func_y+0x30/0x60     ← 그 다음 lock_a 획득 시도

/* 분석:
 * func_x: lock_a → lock_b (A→B)
 * func_y: lock_b → lock_a (B→A)
 * → ABBA 데드락 가능!
 *
 * 해결: 잠금 순서 통일 — 항상 lock_a를 먼저 획득
 */

시나리오 3: crash dump에서 mutex 상태 분석

# crash 도구로 mutex 상태 분석

# 1. mutex 구조체 확인
crash> struct mutex ffff88810a2b3c00
struct mutex {
  owner = {
    counter = 0xffff888107a52b03  ← task + flags
  },
  wait_lock = {
    raw_lock = { val = { counter = 0 } }
  },
  osq = {
    tail = { counter = 0 }
  },
  wait_list = {
    next = 0xffffc900012c7d58,  ← 대기자 있음!
    prev = 0xffffc900012c7d58
  }
}

# 2. owner 디코딩
# owner = 0xffff888107a52b03
# task_struct = owner & ~0x7 = 0xffff888107a52b00
# flags = owner & 0x7 = 0x3 = WAITERS(0x1) | HANDOFF(0x2)

crash> task_struct.comm 0xffff888107a52b00
  comm = "my_daemon\0..."   ← 현재 소유자

crash> bt 0xffff888107a52b00
PID: 9876  TASK: ffff888107a52b00  CPU: 2   COMMAND: "my_daemon"
 #0 [ffffc9000234fc80] __schedule at ffffffff81e1b8c5
 #1 [ffffc9000234fd10] schedule at ffffffff81e1bc90
 #2 [ffffc9000234fd28] io_schedule at ffffffff81e1bd50   ← I/O 대기 중!
 ...

# 3. 대기자 확인
crash> list mutex_waiter.list -s mutex_waiter.task -H 0xffff88810a2b3c10
ffff88810a2b3c10
  task = 0xffff888108b64500   ← 대기 중인 태스크
mutex 디버깅 진단 흐름도 증상 발견 어떤 증상인가? hung_task 경고 SysRq-w, /proc/PID/stack → owner가 블록된 원인 추적 lockdep 경고 의존성 체인 역추적 → 잠금 순서 통일 또는 ww_mutex 성능 저하 perf lock contention → hot mutex 식별, 분할/RCU 전환 진단 도구 crash 도구 struct mutex 덤프 owner 디코딩 lockdep 순환 의존성 탐지 /proc/lockdep_chains perf lock 경합 통계 + 스택 BPF 기반 저오버헤드 ftrace lock tracepoint 시계열 분석 bpftrace 커스텀 스크립트 히스토그램/필터 해결: 잠금 순서 통일 | 잠금 분할 | RCU 전환 | guard(mutex) 적용 | lockdep_assert_held 추가
mutex 문제는 증상(hung_task/lockdep/성능저하)에 따라 적절한 진단 도구를 선택하고, 원인별 해결 방법을 적용합니다.

시나리오 4: Priority Inversion 진단

Priority Inversion은 고우선순위 태스크가 mutex를 기다리는데, 소유자인 저우선순위 태스크가 중간 우선순위 태스크에 의해 선점당해 unlock하지 못하는 상황입니다. 일반 mutex는 PI(Priority Inheritance)를 지원하지 않으므로, 실시간 요구가 있는 시스템에서는 rt_mutex를 사용해야 합니다.

/* Priority Inversion 시나리오 */

/* 태스크 우선순위: T_high > T_mid > T_low */

/* T_low: mutex 획득 후 실행 중 */
mutex_lock(&shared_lock);
/* ... critical section ... */

/* T_high: mutex 대기 — T_low가 unlock해야 함 */
mutex_lock(&shared_lock);  /* 블록! */

/* T_mid: T_low를 선점 — T_low가 unlock 불가! */
/* → T_high는 T_mid가 끝날 때까지 기다려야 함 */
/* → 실질적으로 T_mid의 우선순위로 동작! */

/* 해결: rt_mutex 사용 → T_low의 우선순위를 T_high로 부스트 */
struct rt_mutex shared_rt_lock;
rt_mutex_init(&shared_rt_lock);

/* PREEMPT_RT 커널에서는 mutex_lock()이 자동으로
 * rt_mutex 기반으로 동작하므로 PI가 자동 적용됨 */
디버깅 체크명령어/방법확인 대상
D 상태 태스크 목록ps aux | awk '$8~/D/'블록된 태스크와 명령어
태스크 스택 확인cat /proc/PID/stack어떤 함수에서 블록되었는지
lockdep 순환 확인/proc/lockdep_chains잠금 의존성 체인
mutex owner 식별crash: struct mutex ADDR소유자 task_struct + 플래그
경합 통계perf lock contention -abs경합 횟수, 대기 시간, 스택
전체 태스크 덤프(Dump)echo t > /proc/sysrq-trigger시스템 전체 태스크 상태
RCU stall 연관dmesg | grep "rcu.*stall"mutex 블록이 RCU stall을 유발하는지
참고: 프로덕션 환경에서 mutex 문제를 사전에 방지하려면 다음 세 가지를 기본으로 활성화하세요. (1) CONFIG_PROVE_LOCKING=y — 개발/테스트 빌드에서 데드락을 사전 감지합니다. (2) CONFIG_DEBUG_MUTEXES=y — 소유자 불일치, 이중 해제(Double Free) 등을 런타임에 감지합니다. (3) lockdep_assert_held() — 잠금 보유 필수 함수에 어노테이션을 추가하여 문서화와 검증을 동시에 달성합니다.

Mutex 코드 리뷰 체크리스트

#항목확인
1프로세스 컨텍스트에서만 호출하는가?IRQ/softirq에서 mutex_lock 호출 금지
2소유자가 직접 unlock하는가?다른 태스크/스레드에서 unlock 금지
3재귀 잠금이 없는가?같은 mutex를 두 번 lock하면 데드락
4다중 mutex 순서가 일관적인가?ABBA 패턴 방지, lockdep으로 검증
5사용자 대면 API에 _interruptible 사용?Ctrl+C로 취소 가능하게
6에러 경로에서 unlock을 빠뜨리지 않았는가?goto 레이블, 조기 return 확인
7임계 영역이 불필요하게 길지 않은가?I/O, 할당을 lock 밖으로
8lockdep_assert_held() 추가했는가?lock 보유 필수 함수에 어노테이션
9guard(mutex) 사용을 고려했는가?6.4+ 커널에서 자동 unlock
10mutex가 정말 필요한가?atomic, RCU, per-CPU, spinlock 등 대안 검토

참고 자료

Mutex 설계와 구현에 대한 공식 문서, 심층 기사, 학술 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

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