Reader-Writer Lock (읽기-쓰기 잠금(Read-Write Lock))

읽기 작업이 쓰기 작업보다 압도적으로 많은 상황에서 동시 읽기를 허용하면서도 쓰기의 배타성을 보장하는 Reader-Writer Lock을 분석합니다. rwlock_t(qrwlock)의 cnts+wait_lock 내부 구조, rw_semaphore의 Optimistic Spinning과 HANDOFF 메커니즘, percpu_rw_semaphore의 Per-CPU 카운터 설계, Writer Starvation 문제와 해결 전략, PREEMPT_RT에서의 rwbase_rt 변환까지 커널 소스 기반으로 분석합니다.

전제 조건: 동기화 기법, Spinlock, Mutex, Atomic 연산 문서를 먼저 읽으세요. Reader-Writer Lock은 spinlock과 mutex의 확장이므로, 이들의 내부 구현을 먼저 이해해야 합니다.
일상 비유: Reader-Writer Lock은 도서관 열람실과 같습니다. 여러 사람이 동시에 책을 읽을 수 있지만(Reader), 사서가 서가를 재배치(Relocation)할 때(Writer)는 모든 열람자가 나가야 합니다. 사서가 기다리는 동안에도 새 열람자가 계속 들어오면 사서는 영원히 작업을 시작할 수 없습니다 — 이것이 Writer Starvation 문제입니다.

핵심 요약

  • 동시 읽기, 배타적 쓰기 — 여러 Reader가 동시에 잠금(Lock)을 보유할 수 있지만, Writer는 단독으로만 보유합니다. 읽기 비중이 높을수록 spinlock/mutex 대비 처리량(Throughput)이 향상됩니다.
  • 3가지 변형rwlock_t(busy-wait, 인터럽트(Interrupt) 컨텍스트), rw_semaphore(sleeping, 프로세스(Process) 컨텍스트), percpu_rw_semaphore(읽기 오버헤드(Overhead) 최소화, 쓰기 비용 극대화).
  • qrwlock 구조rwlock_t는 내부적으로 qrwlock이며, 32비트 cnts 필드(Reader Count + Writer Bits)와 wait_lock(qspinlock)으로 구성됩니다.
  • Writer Starvation 방지 — qrwlock의 _QW_WAITING 비트가 새 Reader 진입을 차단하고, rwsem의 HANDOFF 메커니즘이 Writer에게 우선권을 부여합니다.
  • PREEMPT_RT 변환rwlock_t는 RT 커널에서 rwbase_rt(sleeping lock)로 변환되어 우선순위 역전(Priority Inversion)을 방지합니다.

단계별 이해

  1. Readers-Writers 문제 이해
    동시 읽기와 배타적 쓰기의 요구사항, 그리고 Reader/Writer 편향 정책의 트레이드오프를 파악합니다.
  2. rwlock_t 내부 구조 파악
    qrwlock의 cnts 비트 필드와 wait_lock 기반 대기 메커니즘을 추적합니다.
  3. rw_semaphore 슬로우패스 분석
    Optimistic Spinning, 대기 큐(Wait Queue), HANDOFF 플래그의 상호작용을 이해합니다.
  4. Writer Starvation과 해결 전략
    각 변형이 Writer 기아(Starvation)를 어떻게 완화하는지 비교합니다.
  5. 사용 패턴과 대안 선택
    rwlock vs rwsem vs seqlock vs RCU 결정 트리를 기반으로 실전에서 올바른 프리미티브를 선택합니다.
관련 표준: Courtois, Heymans & Parnas, "Concurrent Control with Readers and Writers" (1971) — Readers-Writers 문제의 원논문. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

이론적 배경: Readers-Writers 문제

Readers-Writers 문제는 Courtois, Heymans, Parnas(1971)가 정식화한 고전적 동기화 문제입니다. 공유 자원에 대해 읽기 작업(Reader)은 동시에 여럿이 수행할 수 있지만, 쓰기 작업(Writer)은 다른 모든 Reader와 Writer를 배제해야 합니다.

세 가지 변형

변형정책장점단점
1번 문제 (Reader 우선)Reader가 대기 중인 Writer보다 우선읽기 처리량 극대화Writer Starvation
2번 문제 (Writer 우선)Writer가 대기하면 새 Reader 진입 차단Writer 지연(Latency) 최소화Reader Starvation 가능
3번 문제 (공정)도착 순서 기반 FIFO기아 방지읽기 동시성 저하

Linux 커널은 변형에 따라 다른 정책을 사용합니다. rwlock_t(qrwlock)는 대기 Writer가 있으면 새 Reader를 차단하는 Writer 우선 경향을 보이며, rw_semaphore는 HANDOFF 메커니즘으로 Writer에게 명시적 우선권을 부여합니다.

Readers-Writers 상태 전이 FREE (비어있음) READ-LOCKED (N Readers) WRITE-LOCKED (1 Writer) read_lock() 마지막 read_unlock() write_lock() write_unlock() +Reader Writer 대기 (Reader들이 보유 중) Reader 경로 Writer 경로 차단된 경로
Reader-Writer Lock의 세 가지 상태: FREE, READ-LOCKED, WRITE-LOCKED 간 전이

rwlock_t vs rw_semaphore vs percpu_rw_semaphore

Linux 커널은 세 가지 Reader-Writer 동기화 프리미티브를 제공합니다. 각각 사용 가능한 컨텍스트와 성능 특성이 다릅니다.

속성rwlock_trw_semaphorepercpu_rw_semaphore
대기 방식Busy-wait (spinning)SleepingSleeping
사용 컨텍스트인터럽트, atomic프로세스 컨텍스트만프로세스 컨텍스트만
Reader 오버헤드atomic_add (전역 카운터)atomic cmpxchgPer-CPU 카운터 (거의 0)
Writer 오버헤드wait_lock + reader drainOptimistic spinning + sleepsynchronize_rcu + percpu sum (매우 비쌈)
내부 구현qrwlock (cnts + qspinlock)count + wait_list + osqrcu + __percpu unsigned int
Writer Starvation 방지_QW_WAITING 비트HANDOFF 플래그synchronize_rcu 보장
PREEMPT_RT 동작rwbase_rt (sleeping)변화 없음변화 없음
대표적 사용처tasklist_lockmmap_lock (VMA)cgroup_threadgroup_rwsem
헤더<linux/rwlock.h><linux/rwsem.h><linux/percpu-rwsem.h>
선택 기준: 인터럽트/softirq에서 사용해야 한다면 rwlock_t, 프로세스 컨텍스트에서 슬립(Sleep)이 가능하다면 rw_semaphore, 읽기 빈도가 극도로 높고 쓰기가 매우 드물면 percpu_rw_semaphore를 선택하세요.
Reader-Writer Lock 선택 결정 트리 인터럽트 컨텍스트에서 사용? rwlock_t (qrwlock) 아니오 읽기 비율 99%+ & 쓰기 극히 드묾? percpu_rw_semaphore 아니오 읽기가 쓰기보다 훨씬 많은가? rw_semaphore 아니오 mutex (읽기/쓰기 비슷) 대안 검토: seqlock(짧은 읽기, 재시도 허용) | RCU(읽기 전용 경로, 포인터)
Reader-Writer Lock 변형 선택을 위한 결정 트리

rwlock_t 내부 구조 (qrwlock)

Linux 커널 v4.0 이후 rwlock_tqrwlock(queued rwlock)으로 구현됩니다. 핵심 자료구조는 include/asm-generic/qrwlock_types.h에 정의되어 있습니다.

/* include/asm-generic/qrwlock_types.h */
typedef struct qrwlock {
    union {
        atomic_t    cnts;       /* 32비트: reader count + writer bits */
        struct {
#ifdef __LITTLE_ENDIAN
            u8      wlocked;    /* Writer locked 바이트 */
            u8      __lstate[3];
#else
            u8      __lstate[3];
            u8      wlocked;
#endif
        };
    };
    arch_spinlock_t wait_lock; /* Writer 대기 직렬화용 qspinlock */
} arch_rwlock_t;

cnts 필드 비트 레이아웃

cnts는 32비트 atomic 변수로, Reader Count와 Writer 상태 비트를 하나의 워드에 인코딩합니다.

/* include/asm-generic/qrwlock.h */
#define _QW_WAITING  0x100   /* 비트 8: Writer 대기 중 */
#define _QW_LOCKED   0x0ff   /* 비트 0-7: Writer locked (0xff) */
#define _QW_WMASK    0x1ff   /* 비트 0-8: Writer 전체 마스크 */
#define _QR_SHIFT    9       /* Reader count 시작 비트 */
#define _QR_BIAS     (1U << _QR_SHIFT)  /* 0x200: Reader 1 증가분 */
qrwlock cnts 32비트 필드 레이아웃 31 9 8 7 0 Reader Count (bits 31:9) — 23비트, 최대 ~8M readers W_WAIT bit 8 W_LOCKED (bits 7:0) 상태 예시: cnts = 0x000 FREE cnts = 0x600 3 Readers (3 << 9 = 0x600) cnts = 0x0ff Writer LOCKED cnts = 0x100 Writer WAITING
cnts의 상위 23비트는 Reader 수, 비트 8은 Writer 대기 플래그, 하위 8비트는 Writer Locked 상태

wait_lock은 내부적으로 qspinlock(arch_spinlock_t)이며, 여러 Writer가 동시에 진입할 때 직렬화(Serialization)합니다. 실제 Writer 간 경쟁은 이 spinlock에서 발생하며, Reader는 wait_lock 없이 cnts의 atomic 연산만으로 진입합니다.

rwlock_t API 전체 레퍼런스

API설명컨텍스트
rwlock_init(lock)rwlock 초기화 (동적)모든 컨텍스트
DEFINE_RWLOCK(name)rwlock 선언 + 초기화 (정적)전역/파일 스코프
read_lock(lock)Reader 획득 (선점(Preemption) 비활성화)프로세스/softirq
read_unlock(lock)Reader 해제read_lock 보유 중
read_lock_bh(lock)Reader 획득 + softirq 비활성화프로세스
read_lock_irq(lock)Reader 획득 + IRQ 비활성화프로세스
read_lock_irqsave(lock, flags)Reader 획득 + IRQ 저장/비활성화모든 컨텍스트
write_lock(lock)Writer 획득 (선점 비활성화)프로세스/softirq
write_unlock(lock)Writer 해제write_lock 보유 중
write_lock_bh(lock)Writer 획득 + softirq 비활성화프로세스
write_lock_irq(lock)Writer 획득 + IRQ 비활성화프로세스
write_lock_irqsave(lock, flags)Writer 획득 + IRQ 저장/비활성화모든 컨텍스트
read_trylock(lock)Reader 시도 (실패 시 0 반환)모든 컨텍스트
write_trylock(lock)Writer 시도 (실패 시 0 반환)모든 컨텍스트
주의: rwlock_t는 재귀적 Reader 잠금을 허용하지만, Writer 대기 중에 같은 CPU에서 Reader를 재진입하면 데드락이 발생합니다. lockdep이 이 패턴을 감지합니다.
/* 기본 사용 예시 */
static DEFINE_RWLOCK(my_rwlock);

/* Reader 경로 — 동시 진입 가능 */
void read_data(void)
{
    read_lock(&my_rwlock);
    /* 공유 데이터 읽기... */
    read_unlock(&my_rwlock);
}

/* Writer 경로 — 배타적 진입 */
void write_data(void)
{
    write_lock(&my_rwlock);
    /* 공유 데이터 수정... */
    write_unlock(&my_rwlock);
}

trylock 구현 분석

read_trylock()write_trylock()은 잠금 획득에 실패해도 대기하지 않고 즉시 반환합니다. 인터럽트 핸들러(Handler)나 데드락 회피 경로에서 필수적입니다.

queued_read_trylock

/* include/asm-generic/qrwlock.h */
static inline int queued_read_trylock(struct qrwlock *lock)
{
    int cnts;

    cnts = atomic_read(&lock->cnts);
    /* Writer가 있으면(locked 또는 waiting) 즉시 실패 */
    if (cnts & _QW_WMASK)
        return 0;

    /* Writer 없음 → Reader count 증가 시도
     * atomic_add_return이 아닌 cmpxchg 사용:
     * → 읽은 시점과 증가 시점 사이에 Writer가 진입하면 실패
     * → 실패 시 재시도하지 않고 즉시 0 반환 */
    cnts = (u32)atomic_add_return_acquire(_QR_BIAS, &lock->cnts);
    if (likely(!(cnts & _QW_WMASK)))
        return 1;  /* 성공 */

    /* atomic_add 이후 Writer가 발견됨 → 복원 */
    atomic_sub(_QR_BIAS, &lock->cnts);
    return 0;   /* 실패 */
}

/*
 * read_trylock vs read_lock 차이:
 *
 * read_lock():
 *   atomic_add → Writer 확인 → 있으면 slowpath (wait_lock에서 spin)
 *   → 반드시 락 획득 후 반환
 *
 * read_trylock():
 *   atomic_read → Writer 확인 → 있으면 즉시 0 반환
 *   → atomic_add → 다시 확인 → Writer 발견 시 복원 후 0 반환
 *   → slowpath 없음, 절대 대기하지 않음
 *
 * 반환값: 1 = 성공 (락 획득), 0 = 실패 (락 미획득)
 */

queued_write_trylock

/* include/asm-generic/qrwlock.h */
static inline int queued_write_trylock(struct qrwlock *lock)
{
    int cnts;

    cnts = atomic_read(&lock->cnts);
    /* cnts가 0이 아니면 (Reader든 Writer든) 즉시 실패 */
    if (cnts)
        return 0;

    /* cnts == 0 (아무도 없음) → cmpxchg로 _QW_LOCKED 설정 시도
     * cmpxchg가 실패하면 (경쟁에서 짐) 재시도 없이 0 반환
     *
     * write_lock()과의 차이:
     *   write_lock(): cmpxchg 실패 → slowpath (wait_lock spin + drain)
     *   write_trylock(): cmpxchg 실패 → 즉시 0 반환
     */
    return atomic_cmpxchg_acquire(&lock->cnts, 0,
        _QW_LOCKED) == 0;
}

/*
 * write_trylock의 조건이 read_trylock보다 엄격한 이유:
 *   read_trylock:  Writer만 없으면 성공 (Reader 여럿 공존 가능)
 *   write_trylock: cnts == 0이어야 성공 (Reader도 Writer도 없어야)
 *
 * 전형적 사용 패턴:
 *   if (write_trylock(&lock)) {
 *       // 배타적 수정...
 *       write_unlock(&lock);
 *   } else {
 *       // 대안 경로: 나중에 재시도 또는 다른 전략
 *   }
 */

unlock 내부 구현

read_unlock()write_unlock()은 각각 다른 메커니즘으로 구현됩니다. Reader는 atomic 감소, Writer는 바이트 스토어만으로 해제합니다.

queued_read_unlock 구현

/* include/asm-generic/qrwlock.h */
static inline void queued_read_unlock(struct qrwlock *lock)
{
    /* cnts에서 _QR_BIAS (0x200) 감소 — release 의미론
     *
     * release 의미론:
     *   임계 영역 내의 모든 메모리 접근이 이 연산 이전에 완료
     *   → Reader가 읽은 데이터가 unlock 이후에도 유효
     *
     * 아키텍처별 구현:
     *   x86:    LOCK XADD (암묵적 full barrier)
     *   ARM64:  LDXR + STLXR (STLXR이 release)
     *           또는 LSE: LDADDL (atomic add + release)
     *   RISC-V: amoadd.w.rl (.rl이 release ordering)
     *
     * 마지막 Reader가 나가면 cnts의 상위 23비트가 0이 됨
     * → Writer가 drain 대기 중이면 atomic_cond_read가 감지
     */
    atomic_sub_return_release(_QR_BIAS, &lock->cnts);
}

/*
 * Reader unlock이 Writer를 깨우는 메커니즘:
 *
 * 시나리오: Writer가 queued_write_lock_slowpath에서 drain 대기 중
 *
 * Writer: atomic_cond_read_acquire(&cnts, VAL == _QW_WAITING)
 *         → cnts를 반복 읽으며 reader count가 0이 될 때까지 spin
 *
 * 마지막 Reader: atomic_sub(_QR_BIAS)
 *         → cnts = _QW_WAITING (0x100)
 *         → Writer의 조건 VAL == _QW_WAITING이 참이 됨
 *         → ARM64: STLXR이 exclusive monitor 클리어 → WFE 해제
 *         → x86: LOCK XADD가 캐시라인 변경 → PAUSE 루프 탈출
 *
 * 명시적 wake-up 호출 없음 — atomic 연산의 캐시 일관성이 암묵적 알림
 */

queued_write_unlock 구현

/* include/asm-generic/qrwlock.h */
static inline void queued_write_unlock(struct qrwlock *lock)
{
    /* wlocked 바이트를 0으로 설정 — release 의미론
     *
     * 핵심: 32비트 atomic 연산이 아닌 바이트 스토어 사용!
     *
     * 왜 바이트 스토어로 충분한가:
     *   wlocked는 cnts union의 첫 바이트 (Little-Endian)
     *   Writer는 wlocked = 0xff로 설정하여 락을 보유
     *   wlocked = 0으로 설정하면 _QW_LOCKED 비트 전체 클리어
     *   → Reader count(상위 23비트)는 건드리지 않음
     *   → atomic RMW 불필요 → 더 효율적
     *
     * 아키텍처별 구현:
     *   x86:    MOV BYTE [lock], 0
     *           (TSO가 release 보장 → 배리어 불필요)
     *   ARM64:  STLRB wzr, [lock]
     *           (Store-Release Byte — 명시적 release)
     *   RISC-V: fence rw,w + sb zero, (lock)
     *           (fence가 release, sb가 바이트 스토어)
     */
    smp_store_release(&lock->wlocked, 0);
}

/*
 * Writer unlock 후 대기자 처리:
 *
 * 1. Reader가 slow path에서 대기 중인 경우:
 *    → atomic_cond_read_acquire(&cnts, !(VAL & _QW_LOCKED))
 *    → wlocked = 0이 되면 조건 성립 → Reader 진입
 *
 * 2. 다른 Writer가 wait_lock에서 대기 중인 경우:
 *    → wait_lock은 이미 현재 Writer가 slowpath에서 해제했음
 *    → 다음 Writer가 wait_lock 획득 → cnts 확인 → 진입 시도
 *
 * 3. Reader와 Writer 모두 대기:
 *    → wait_lock을 먼저 잡는 쪽이 진입
 *    → Reader: _QW_LOCKED 해제 확인 후 진입
 *    → Writer: cnts == 0 확인 후 진입 (Reader가 먼저 들어가면 drain 대기)
 */

IRQ 변형 내부 매핑(Mapping)

read_lock_bh(), read_lock_irq(), write_lock_irqsave() 등의 IRQ 변형은 실제로 인터럽트/softirq 제어 + 기본 lock/unlock의 조합입니다. 내부 매핑 관계를 정리합니다.

/* include/linux/rwlock.h — IRQ 변형 매크로 정의 */

/* ■ BH (Bottom Half / softirq) 변형 */
#define read_lock_bh(lock)     \
    do { local_bh_disable(); read_lock(lock); } while (0)
#define read_unlock_bh(lock)   \
    do { read_unlock(lock); local_bh_enable(); } while (0)
#define write_lock_bh(lock)    \
    do { local_bh_disable(); write_lock(lock); } while (0)
#define write_unlock_bh(lock)  \
    do { write_unlock(lock); local_bh_enable(); } while (0)

/* ■ IRQ 변형 (인터럽트 비활성화) */
#define read_lock_irq(lock)    \
    do { local_irq_disable(); read_lock(lock); } while (0)
#define read_unlock_irq(lock)  \
    do { read_unlock(lock); local_irq_enable(); } while (0)
#define write_lock_irq(lock)   \
    do { local_irq_disable(); write_lock(lock); } while (0)
#define write_unlock_irq(lock) \
    do { write_unlock(lock); local_irq_enable(); } while (0)

/* ■ IRQSAVE 변형 (인터럽트 상태 저장/복원) */
#define read_lock_irqsave(lock, flags)   \
    do { local_irq_save(flags); read_lock(lock); } while (0)
#define read_unlock_irqrestore(lock, flags) \
    do { read_unlock(lock); local_irq_restore(flags); } while (0)
#define write_lock_irqsave(lock, flags)  \
    do { local_irq_save(flags); write_lock(lock); } while (0)
#define write_unlock_irqrestore(lock, flags) \
    do { write_unlock(lock); local_irq_restore(flags); } while (0)

IRQ 변형 선택 기준

상황ReaderWriter이유
프로세스 컨텍스트만read_lock()write_lock()선점만 비활성화하면 충분
softirq에서도 같은 lock 사용read_lock_bh()write_lock_bh()softirq 재진입 방지
hardirq에서도 같은 lock 사용read_lock_irq()write_lock_irq()인터럽트가 항상 활성화 상태일 때
IRQ 상태를 모를 때read_lock_irqsave()write_lock_irqsave()중첩 인터럽트 비활성화 안전

IRQ 변형 선택의 핵심 규칙: 같은 rwlock을 인터럽트 핸들러에서도 사용한다면, 프로세스 컨텍스트에서는 반드시 IRQ를 비활성화해야 합니다.

예시로 네트워크 드라이버에서 통계(Statistics) 보호 시의 사용 패턴을 살펴봅니다.

프로세스 컨텍스트 (ethtool 통계 읽기)
read_lock_bh(&dev_stats_lock)(softirq 비활성화) → 통계 읽기 → read_unlock_bh(&dev_stats_lock)
NAPI softirq (패킷(Packet) 수신)
write_lock(&dev_stats_lock)(이미 softirq 컨텍스트) → stats->rx_packets++write_unlock(&dev_stats_lock)

local_bh_disable()가 필요한 이유는 다음과 같습니다. 프로세스 컨텍스트에서 read_lock()을 보유 중일 때 softirq가 발생하여 write_lock()을 시도하면, Reader가 같은 CPU에서 보유 중이므로 drain이 불가하여 데드락이 발생합니다.

초기화 매크로(Macro) 내부

/* include/linux/rwlock_types.h */
#define __RW_LOCK_UNLOCKED(lockname)  \
    (rwlock_t) {                      \
        .raw_lock = __ARCH_RW_LOCK_UNLOCKED, \
        RW_DEP_MAP_INIT(lockname)     \
    }

#define DEFINE_RWLOCK(x)  \
    rwlock_t x = __RW_LOCK_UNLOCKED(x)

/* 동적 초기화 — lockdep 키 자동 생성 */
#define rwlock_init(lock)  \
    do {                   \
        static struct lock_class_key __key;  \
        __rwlock_init((lock), #lock, &__key); \
    } while (0)

/* kernel/locking/spinlock_debug.c */
void __rwlock_init(rwlock_t *lock, const char *name,
                    struct lock_class_key *key)
{
    /*
     * qrwlock 초기화:
     *   cnts = 0 (FREE 상태)
     *   wait_lock = UNLOCKED (qspinlock 초기)
     *   lockdep dep_map 초기화
     *
     * __ARCH_RW_LOCK_UNLOCKED:
     *   x86:    { .cnts = ATOMIC_INIT(0), .wait_lock = ... }
     *   ARM64:  동일
     *   RISC-V: 동일
     *
     * lockdep 키:
     *   DEFINE_RWLOCK은 정적 → 변수 이름으로 키 생성
     *   rwlock_init은 동적 → __key가 static으로 고유성 보장
     *   같은 코드 위치에서 생성된 모든 rwlock은 같은 클래스
     */
    lock->raw_lock = (arch_rwlock_t)__ARCH_RW_LOCK_UNLOCKED;
    lockdep_init_map(&lock->dep_map, name, key, 0);
}

qrwlock 내부: cnts + wait_lock

qrwlock의 핵심 설계는 Reader는 cnts atomic 연산만으로 fast path를 처리하고, Writer는 wait_lock(qspinlock)으로 직렬화한 후 cnts를 조작하는 이중 구조입니다.

/* include/asm-generic/qrwlock.h — queued_read_lock() */
static inline void queued_read_lock(struct qrwlock *lock)
{
    int cnts;

    cnts = atomic_add_return_acquire(_QR_BIAS, &lock->cnts);
    if (likely(!(cnts & _QW_WMASK)))
        return;  /* Fast path: Writer 없음, 즉시 진입 */

    /* Slow path: Writer가 있으면 cnts 복원 후 대기 */
    queued_read_lock_slowpath(lock);
}

/* queued_write_lock() */
static inline void queued_write_lock(struct qrwlock *lock)
{
    if (atomic_cmpxchg_acquire(&lock->cnts, 0, _QW_LOCKED) == 0)
        return;  /* Fast path: 아무도 없으면 즉시 획득 */

    queued_write_lock_slowpath(lock);
}
qrwlock 내부 동작 흐름 Reader 경로 atomic_add(_QR_BIAS, &cnts) (cnts & _QW_WMASK) == 0 ? Fast Path 진입! 아니오 atomic_sub(_QR_BIAS) 복원 arch_spin_lock(wait_lock) Writer 해제될 때까지 spin atomic_add + unlock + 진입 Writer 경로 cmpxchg(cnts, 0, _QW_LOCKED) 성공 (cnts == 0)? 즉시 획득! 아니오 slowpath 진입 spin_lock(wait_lock) _QW_WAITING 설정 Readers drain 대기 → _QW_LOCKED 설정 → wait_lock 해제 Write Lock 획득!
Reader는 atomic_add fast path, Writer는 wait_lock 직렬화 후 reader drain 대기

Reader Fast Path: atomic_add

Reader의 fast path는 단 하나의 atomic 연산으로 구현됩니다. atomic_add_return_acquire(_QR_BIAS, &cnts)를 수행한 후, 결과의 하위 9비트(Writer 영역)가 0이면 즉시 진입합니다.

/* include/asm-generic/qrwlock.h — 간략화 */
/* Reader fast path의 핵심 */
cnts = atomic_add_return_acquire(_QR_BIAS, &lock->cnts);
if (likely(!(cnts & _QW_WMASK)))
    return;  /* Writer 없음 → 진입 (acquire 의미론 보장) */

/*
 * _QW_WMASK = 0x1ff (비트 8:0)
 * Writer가 locked(0xff) 또는 waiting(0x100)이면 이 조건 실패
 * → slow path로 진입
 *
 * 아키텍처별 구현:
 *   x86:   LOCK XADD → XADD 결과 확인
 *   ARM64: LDAXR/STXR + DMB ISH (acquire)
 *   RISC-V: amoadd.w.aq
 */

이 설계의 핵심 장점은 Writer가 없는 일반적인 경우 Reader 간 경합(Contention)이 캐시(Cache)라인 하나에서만 발생하는 것입니다. 다만 이 캐시라인이 모든 CPU에서 공유되므로, CPU 수가 많아질수록 atomic_add의 캐시 일관성(Cache Coherency) 트래픽이 성능 병목(Bottleneck)이 됩니다. 이것이 percpu_rw_semaphore가 필요한 이유입니다.

Reader Slow Path

/* kernel/locking/qrwlock.c — queued_read_lock_slowpath() 단순화 */
void queued_read_lock_slowpath(struct qrwlock *lock)
{
    /* 1. 먼저 추가한 reader count를 되돌림 */
    atomic_sub(_QR_BIAS, &lock->cnts);

    /* 2. wait_lock 획득하여 Reader도 직렬화 */
    arch_spin_lock(&lock->wait_lock);

    /* 3. Writer의 locked 비트가 해제될 때까지 spin */
    atomic_cond_read_acquire(&lock->cnts, !(VAL & _QW_LOCKED));

    /* 4. Reader count 다시 증가 + wait_lock 해제 */
    atomic_add(_QR_BIAS, &lock->cnts);
    arch_spin_unlock(&lock->wait_lock);
}
핵심 포인트: Slow path에서 Reader가 wait_lock을 잡는 이유는 Writer 해제 직후 여러 Reader가 동시에 쇄도하는 thundering herd를 방지하기 위해서입니다. wait_lock은 하나의 Reader만 cnts를 조작하게 하고, 이후 fast path로 재진입하는 Reader들은 wait_lock 없이 바로 진입합니다.

Writer 경로: wait_lock + reader drain

Writer는 Reader보다 복잡한 3단계 과정을 거칩니다.

/* kernel/locking/qrwlock.c — queued_write_lock_slowpath() 단순화 */
void queued_write_lock_slowpath(struct qrwlock *lock)
{
    /* ① wait_lock 획득 — Writer 간 직렬화 */
    arch_spin_lock(&lock->wait_lock);

    /* ② Reader가 없으면 즉시 locked 설정 시도 */
    if (!atomic_read(&lock->cnts) &&
        (atomic_cmpxchg_acquire(&lock->cnts, 0, _QW_LOCKED) == 0))
        goto unlock;

    /* ③ _QW_WAITING 비트 설정 → 새 Reader의 fast path 차단 */
    atomic_or(_QW_WAITING, &lock->cnts);

    /* ④ 기존 Reader들이 모두 나갈 때까지 spin */
    do {
        atomic_cond_read_acquire(&lock->cnts,
            VAL == _QW_WAITING);
    } while (0);

    /* ⑤ _QW_WAITING → _QW_LOCKED로 전환 */
    atomic_sub(_QW_WAITING, &lock->cnts);
    smp_store_release(&lock->wlocked, 1);

unlock:
    arch_spin_unlock(&lock->wait_lock);
}
_QW_WAITING의 역할: 이 비트가 설정되면 queued_read_lock()의 fast path 조건 !(cnts & _QW_WMASK)가 실패합니다. 따라서 새로운 Reader는 slow path로 빠져 wait_lock에서 대기하게 됩니다. 이것이 Writer Starvation을 방지하는 핵심 메커니즘입니다.

rw_semaphore 구조체(Struct) 분석

rw_semaphore는 sleeping lock으로, mutex와 유사한 대기 큐 기반 구조를 가집니다. include/linux/rwsem.h에 정의되어 있습니다.

/* include/linux/rwsem.h */
struct rw_semaphore {
    atomic_long_t       count;      /* 64비트: reader count + flags */
    atomic_long_t       owner;      /* Writer owner + flags */
    struct optimistic_spin_queue osq; /* Optimistic Spinning MCS 큐 */
    raw_spinlock_t      wait_lock;  /* 대기 큐 보호 */
    struct list_head    wait_list;  /* rwsem_waiter 리스트 */
};

count 필드 인코딩

/* kernel/locking/rwsem.c */
#define RWSEM_WRITER_LOCKED   (1UL << 0)  /* 비트 0: Writer 보유 */
#define RWSEM_FLAG_WAITERS    (1UL << 1)  /* 비트 1: 대기자 존재 */
#define RWSEM_FLAG_HANDOFF    (1UL << 2)  /* 비트 2: HANDOFF 활성 */
#define RWSEM_READER_BIAS     (1UL << 8)  /* Reader 1 증가분 */
#define RWSEM_READER_MASK     (~(RWSEM_READER_BIAS - 1))

/*
 * count 값 해석:
 *   0x000          → FREE
 *   0x001          → Writer LOCKED
 *   0x100          → 1 Reader
 *   0x300          → 3 Readers
 *   0x003          → Writer LOCKED + WAITERS 존재
 *   0x007          → Writer LOCKED + WAITERS + HANDOFF
 */
rw_semaphore 내부 구조 struct rw_semaphore count (atomic_long_t) owner (atomic_long_t) osq (optimistic_spin_queue) wait_lock (raw_spinlock_t) wait_list (list_head) count 비트 레이아웃 (64비트) bits 63:8 — Reader Count bit 2:HANDOFF bit 1:WAITERS bit 0:W_LOCKED owner 인코딩 task_struct * (정렬) | NONSPINNABLE(bit 1) | READER_OWNED(bit 0) wait_list (rwsem_waiter 리스트) Writer (HANDOFF) Reader Reader Writer FIFO 순서: 첫 번째 대기자에게 우선권 (HANDOFF)
rw_semaphore는 count + owner + osq + wait_lock + wait_list로 구성됩니다

rw_semaphore API 레퍼런스

API설명반환값/특이사항
init_rwsem(sem)rwsem 초기화 (동적)매크로, lockdep 키 생성
DECLARE_RWSEM(name)rwsem 선언 + 초기화 (정적)전역/파일 스코프
down_read(sem)Reader 획득 (인터럽트 불가)슬립 가능
down_read_interruptible(sem)Reader 획득 (시그널(Signal) 허용)0 또는 -EINTR
down_read_killable(sem)Reader 획득 (SIGKILL 허용)0 또는 -EINTR
down_read_trylock(sem)Reader 시도 (실패 시 0)슬립 안 함
up_read(sem)Reader 해제
down_write(sem)Writer 획득 (인터럽트 불가)슬립 가능
down_write_killable(sem)Writer 획득 (SIGKILL 허용)0 또는 -EINTR
down_write_trylock(sem)Writer 시도 (실패 시 0)슬립 안 함
up_write(sem)Writer 해제
downgrade_write(sem)Writer → Reader 원자적(Atomic) 전환Reader를 깨움
down_read_nested(sem, class)중첩 Reader (lockdep)중첩 클래스 지정
down_write_nested(sem, class)중첩 Writer (lockdep)중첩 클래스 지정
/* rw_semaphore 사용 예시: VFS mmap_lock 패턴 */
struct mm_struct *mm = current->mm;

/* 페이지 폴트 핸들러 — Reader 경로 */
mmap_read_lock(mm);           /* down_read(&mm->mmap_lock) */
vma = find_vma(mm, address);
/* ... VMA 탐색 ... */
mmap_read_unlock(mm);         /* up_read(&mm->mmap_lock) */

/* mmap/munmap 시스템 콜 — Writer 경로 */
mmap_write_lock(mm);          /* down_write(&mm->mmap_lock) */
/* ... VMA 생성/삭제/분할 ... */
mmap_write_unlock(mm);        /* up_write(&mm->mmap_lock) */

rwsem Fast Path 인라인 구현

down_read(), up_read(), down_write(), up_write()의 fast path는 인라인 함수(Inline Function)로 구현되어 대부분의 경우 단일 atomic 연산으로 완료됩니다. 경합이 없을 때의 오버헤드를 최소화하는 핵심 코드입니다.

down_read Fast Path

/* kernel/locking/rwsem.c */
static inline void __down_read(struct rw_semaphore *sem)
{
    long tmp;

    /* count에 RWSEM_READER_BIAS (0x100) 추가 — acquire 의미론
     *
     * 결과 분석:
     *   하위 3비트(WRITER_LOCKED | WAITERS | HANDOFF)가 0이면
     *   → Writer 없고 대기자 없음 → 즉시 Reader 진입!
     *
     * 하위 3비트가 0이 아닌 경우:
     *   bit 0 (WRITER_LOCKED): Writer 보유 중 → slowpath
     *   bit 1 (WAITERS): 대기자 존재 → slowpath (공정성)
     *   bit 2 (HANDOFF): HANDOFF 활성 → slowpath
     */
    tmp = atomic_long_fetch_add_acquire(
        RWSEM_READER_BIAS, &sem->count);

    if (unlikely(tmp & RWSEM_READ_FAILED_MASK))
        rwsem_down_read_slowpath(sem, TASK_UNINTERRUPTIBLE);
}

/*
 * RWSEM_READ_FAILED_MASK:
 *   RWSEM_WRITER_LOCKED | RWSEM_FLAG_WAITERS | RWSEM_FLAG_HANDOFF
 *   = 0x7 (하위 3비트)
 *
 * 왜 WAITERS 비트도 확인하는가?
 *   대기 중인 Writer가 있는데 새 Reader가 계속 진입하면
 *   Writer starvation 발생 → 대기자가 있으면 slowpath로 보내
 *   대기 큐에서 공정하게 순서 대기
 *
 * down_read_interruptible / down_read_killable:
 *   동일한 fast path, slowpath에서 TASK_INTERRUPTIBLE /
 *   TASK_KILLABLE로 슬립하여 시그널 수신 시 -EINTR 반환
 */

/* down_read_trylock — 비차단 시도 */
static inline int __down_read_trylock(struct rw_semaphore *sem)
{
    long tmp;

    tmp = atomic_long_read(&sem->count);
    while (!(tmp & RWSEM_READ_FAILED_MASK)) {
        /* Writer/대기자 없음 → READER_BIAS 추가 시도 */
        if (atomic_long_try_cmpxchg_acquire(
                &sem->count, &tmp,
                tmp + RWSEM_READER_BIAS)) {
            rwsem_set_reader_owned(sem);
            return 1;
        }
    }
    return 0;
}

/*
 * down_read_trylock vs down_read 차이:
 *   down_read: fetch_add로 낙관적 추가 → 실패 시 slowpath (슬립)
 *   down_read_trylock: cmpxchg 루프 → 실패 시 즉시 0 반환
 *
 * cmpxchg 루프를 사용하는 이유:
 *   fetch_add는 무조건 count를 증가시키므로,
 *   실패 시 다시 빼야 함 (비효율적)
 *   cmpxchg는 조건 확인 후 원자적으로 추가하므로
 *   실패 시 복원 불필요
 */

up_read Fast Path

/* kernel/locking/rwsem.c */
static inline void __up_read(struct rw_semaphore *sem)
{
    long tmp;

    /* count에서 RWSEM_READER_BIAS 감소 — release 의미론
     *
     * 반환값 분석:
     *   결과가 음수가 아니고 하위 비트가 0이면
     *   → 다른 Reader가 있거나 아무도 없음 → 즉시 완료
     *
     *   WAITERS 비트가 설정되어 있으면
     *   → 대기 중인 Writer/Reader가 있음
     *   → 마지막 Reader라면 깨워야 함 → slowpath
     */
    tmp = atomic_long_add_return_release(
        -RWSEM_READER_BIAS, &sem->count);

    if (unlikely((tmp & (RWSEM_LOCK_MASK | RWSEM_FLAG_WAITERS))
                 == RWSEM_FLAG_WAITERS))
        rwsem_wake(sem);
}

/*
 * up_read slowpath (rwsem_wake) 진입 조건:
 *
 *   (tmp & RWSEM_LOCK_MASK) == 0  → Writer 없고 Reader count 0
 *   (tmp & RWSEM_FLAG_WAITERS) != 0  → 대기자 존재
 *
 * 즉: "마지막 Reader가 나갔고, 대기자가 있을 때"만 rwsem_wake 호출
 *
 * rwsem_wake가 하는 일:
 *   1. wait_lock 획득
 *   2. 대기 큐 첫 번째 waiter 확인
 *   3. Writer이면 → 해당 Writer 깨움 (HANDOFF일 수 있음)
 *   4. Reader이면 → 연속된 Reader들을 모두 깨움
 *   5. wait_lock 해제
 */

down_write Fast Path

/* kernel/locking/rwsem.c */
static inline int __down_write_common(struct rw_semaphore *sem,
                                      int state)
{
    /* count가 0이면 (아무도 없음) WRITER_LOCKED로 cmpxchg
     *
     * 성공: count = RWSEM_WRITER_LOCKED (0x1) → 즉시 획득
     * 실패: Reader가 있거나 다른 Writer가 있음 → slowpath
     */
    if (unlikely(!atomic_long_try_cmpxchg_acquire(
            &sem->count, 0, RWSEM_WRITER_LOCKED)))
        return rwsem_down_write_slowpath(sem, state);

    /* fast path 성공: owner 설정 */
    rwsem_set_owner(sem);
    return 0;
}

/*
 * down_write 변형들의 fast path는 모두 동일:
 *   down_write(sem)          → state = TASK_UNINTERRUPTIBLE
 *   down_write_killable(sem) → state = TASK_KILLABLE
 *   down_write_trylock(sem)  → slowpath 없이 즉시 0 반환
 *
 * rwsem_set_owner(sem):
 *   atomic_long_set(&sem->owner, (long)current)
 *   → optimistic spinning에서 owner->on_cpu 확인에 사용
 *   → PREEMPT_RT에서 PI(Priority Inheritance) chain에 사용
 */

up_write Fast Path

/* kernel/locking/rwsem.c */
static inline void __up_write(struct rw_semaphore *sem)
{
    long tmp;

    /* owner 클리어 */
    rwsem_clear_owner(sem);

    /* count에서 WRITER_LOCKED 제거 — release 의미론
     *
     * 반환값 분석:
     *   결과가 정확히 0이면
     *   → 대기자 없음 → 즉시 완료
     *
     *   WAITERS 비트가 남아 있으면
     *   → 대기자 깨우기 필요 → rwsem_wake
     */
    tmp = atomic_long_fetch_add_release(
        -RWSEM_WRITER_LOCKED, &sem->count);

    if (unlikely(tmp & RWSEM_FLAG_WAITERS))
        rwsem_wake(sem);
}

/*
 * up_write에서 rwsem_wake 호출 시:
 *
 * 대기 큐 첫 번째가 Writer이면:
 *   → 해당 Writer 하나만 깨움
 *   → HANDOFF가 설정되어 있으면 해당 Writer에게 직접 전달
 *
 * 대기 큐 첫 번째가 Reader이면:
 *   → 연속된 모든 Reader를 한꺼번에 깨움 (batch wakeup)
 *   → 중간에 Writer가 있으면 거기서 멈춤
 *   → 예: [R, R, R, W, R] → R 3개만 깨움, W는 대기 유지
 */

struct rwsem_waiter와 대기 큐 관리

rwsem_waiter 구조체는 대기 큐의 각 항목을 표현하며, HANDOFF와 타임아웃(Timeout) 관리의 핵심입니다.

/* kernel/locking/rwsem.c */
struct rwsem_waiter {
    struct list_head    list;      /* wait_list 연결 */
    struct task_struct  *task;     /* 대기 중인 태스크 */
    enum rwsem_waiter_type type;   /* READER 또는 WRITER */
    unsigned long       timeout;   /* HANDOFF 타임아웃 시각 */
    bool                handoff_set; /* HANDOFF 플래그 설정 여부 */
};

enum rwsem_waiter_type {
    RWSEM_WAITING_FOR_WRITE,
    RWSEM_WAITING_FOR_READ,
};

/*
 * 대기 큐의 구조 예시:
 *
 * wait_list → [W1 (first, handoff_set=true)]
 *           → [R2]
 *           → [R3]
 *           → [W4]
 *           → [R5]
 *
 * W1이 깨어나면:
 *   HANDOFF가 설정되어 있으므로 W1만 락 획득 가능
 *   다른 optimistic spinner는 try_write_lock에서 실패
 *
 * 현재 Writer가 해제하면:
 *   rwsem_wake → W1을 깨움 → W1이 락 획득
 *   W1이 해제하면 → R2, R3를 한꺼번에 깨움 (W4에서 멈춤)
 *   R2, R3 모두 해제하면 → W4를 깨움
 *   W4가 해제하면 → R5를 깨움
 */

/* RWSEM_WAIT_TIMEOUT — HANDOFF 활성화 시점 */
#define RWSEM_WAIT_TIMEOUT  (4 * HZ / 1000)  /* ~4ms (4 jiffies @ HZ=1000) */
/*
 * 4ms는 대략적 가이드라인:
 *   HZ=1000 → 4 jiffies = 4ms
 *   HZ=250  → 1 jiffy = 4ms
 *   HZ=100  → 0 jiffies → 최소 1 jiffy = 10ms
 *
 * 이 시간이 지나도 락을 얻지 못하면 HANDOFF 설정
 * → optimistic spinner 차단
 * → 첫 번째 대기자에게 우선 전달
 */

rwsem_mark_wake: 대기자 깨우기(Wakeup) 로직

/* kernel/locking/rwsem.c — 단순화 */
static void rwsem_mark_wake(struct rw_semaphore *sem,
    enum rwsem_wake_type wake_type,
    struct wake_q_head *wake_q)
{
    struct rwsem_waiter *waiter, *tmp;
    long oldcount, woken = 0, adjustment = 0;

    lockdep_assert_held(&sem->wait_lock);
    waiter = rwsem_first_waiter(sem);

    /* ■ 첫 번째 대기자가 Writer인 경우 */
    if (waiter->type == RWSEM_WAITING_FOR_WRITE) {
        if (wake_type == RWSEM_WAKE_ANY) {
            /* Writer를 직접 깨움
             * → Writer가 rwsem_try_write_lock()으로 획득 시도
             * → HANDOFF 설정 시 이 Writer만 성공 가능 */
            wake_q_add(wake_q, waiter->task);
        }
        return;
    }

    /* ■ 첫 번째 대기자가 Reader인 경우
     *    → 연속된 Reader를 모두 깨움 (batch wakeup) */
    list_for_each_entry_safe(waiter, tmp,
        &sem->wait_list, list) {

        /* Writer를 만나면 멈춤 */
        if (waiter->type == RWSEM_WAITING_FOR_WRITE)
            break;

        woken++;
        list_del(&waiter->list);

        /*
         * waiter->task = NULL로 설정하여 깨움 알림
         * → smp_store_release: 임계 영역 데이터가 가시적
         * → 깨어난 Reader가 schedule()에서 복귀 시
         *   smp_load_acquire(&waiter.task)로 NULL 관찰
         */
        smp_store_release(&waiter->task, NULL);
        wake_q_add(wake_q, waiter->task);
    }

    /* Reader 수만큼 count 조정
     * (이미 slowpath에서 READER_BIAS를 추가했으므로 추가 조정) */
    adjustment = woken * RWSEM_READER_BIAS;
    if (list_empty(&sem->wait_list))
        adjustment -= RWSEM_FLAG_WAITERS;  /* 대기자 없으면 플래그 제거 */

    if (adjustment)
        atomic_long_add(adjustment, &sem->count);

    /* owner를 READER_OWNED로 설정 */
    rwsem_set_reader_owned(sem);
}

/*
 * rwsem_wake_type:
 *   RWSEM_WAKE_ANY      — Reader 또는 Writer 깨움 (up_read/up_write)
 *   RWSEM_WAKE_READERS  — Reader만 깨움 (downgrade_write 시)
 *   RWSEM_WAKE_READ_OWNED — Reader 이미 소유, 추가 Reader 깨움
 *
 * wake_q:
 *   실제 wake_up_process() 호출을 지연시키는 큐
 *   → wait_lock 보유 중에 wake_up 하면 데드락 위험
 *   → wake_q에 모아놓고 wait_lock 해제 후 한꺼번에 깨움
 *   → wake_up_q(wake_q)로 일괄 처리
 */
rwsem_mark_wake: 대기자 깨우기 로직 대기 큐 (wait_list): R1 (first) R2 R3 W4 R5 batch wakeup: R1, R2, R3 W4에서 멈춤 R1,R2,R3 모두 해제 후: W4 (first, wake) R5 W4 해제 후: R5 (wake) 연속 Reader는 한꺼번에 깨우고(batch), Writer는 하나씩 깨움 → 처리량과 공정성 균형
대기 큐에서 연속 Reader를 batch wakeup하고 Writer에서 멈추는 패턴

downgrade_write 구현 상세

/* kernel/locking/rwsem.c */
void downgrade_write(struct rw_semaphore *sem)
{
    long tmp;

    /*
     * count 원자적 변환:
     *   -RWSEM_WRITER_LOCKED + RWSEM_READER_BIAS
     *   = -1 + 0x100 = 0xFF (64비트에서)
     *
     * 이 단일 atomic 연산으로:
     *   1. Writer 비트 제거
     *   2. Reader 카운트 1 추가
     *   → Writer에서 Reader로 원자적 전환
     */
    tmp = atomic_long_fetch_add_release(
        -RWSEM_WRITER_LOCKED + RWSEM_READER_BIAS,
        &sem->count);

    /* owner를 READER_OWNED로 변경 */
    rwsem_set_reader_owned(sem);

    /* 대기 중인 Reader가 있으면 깨움 */
    if (tmp & RWSEM_FLAG_WAITERS)
        rwsem_downgrade_wake(sem);
}

/*
 * rwsem_downgrade_wake:
 *   RWSEM_WAKE_READ_OWNED 타입으로 rwsem_mark_wake 호출
 *   → 대기 큐의 연속 Reader만 깨움 (Writer는 건너뜀)
 *   → downgrade 후 현재 태스크가 Reader로 계속 보유하므로
 *     Writer는 깨워도 lock을 얻지 못함
 *
 * 전형적 사용 사례 — VFS rename:
 *   down_write(parent_dir->i_rwsem);  // 배타적 디렉터리 수정
 *   // ... rename 수행 ...
 *   downgrade_write(parent_dir->i_rwsem);  // Reader로 전환
 *   // ... 수정된 디렉터리 내용 기반으로 추가 조회 ...
 *   up_read(parent_dir->i_rwsem);
 */

rwsem Optimistic Spinning

rw_semaphore는 mutex와 마찬가지로 Optimistic Spinning(적극적 스피닝)을 지원합니다. Writer가 lock을 보유하고 있지만 해당 task가 CPU에서 실행 중(owner->on_cpu)이면, 슬립하지 않고 스피닝하며 기다립니다.

/* kernel/locking/rwsem.c — rwsem_optimistic_spin() 핵심 로직 */
static bool rwsem_optimistic_spin(struct rw_semaphore *sem,
                                    struct rwsem_waiter *waiter)
{
    /* 1. osq_lock으로 스피너 큐 진입 (MCS 기반) */
    if (!osq_lock(&sem->osq))
        goto done;

    for (;;) {
        enum owner_state owner_state;

        /* 2. owner가 CPU에서 실행 중인지 확인 */
        owner_state = rwsem_spin_on_owner(sem);
        if (owner_state != OWNER_READER &&
            owner_state != OWNER_WRITER)
            break;  /* owner가 슬립했으면 스피닝 중단 */

        /* 3. 락 시도 */
        taken = rwsem_try_write_lock_unqueued(sem);
        if (taken)
            break;

        /* 4. 선점 필요하면 스피닝 중단 */
        if (need_resched())
            break;

        cpu_relax();
    }

    osq_unlock(&sem->osq);
done:
    return taken;
}
rwsem Optimistic Spinning 흐름 down_write() 호출 cmpxchg fast path 시도 성공 획득 완료 실패 osq_lock() — MCS 큐 진입 owner->on_cpu 확인하며 spin 해제됨 스피닝 성공, 획득 스케줄 필요 / owner 슬립 slow path (슬립 대기)
Optimistic Spinning은 owner가 CPU에서 실행 중일 때 컨텍스트 스위칭(Context Switching) 없이 잠금을 획득합니다

rwsem HANDOFF와 Writer 우선 정책

rwsem에는 HANDOFF 메커니즘이 있어 대기 시간(Latency)이 긴 첫 번째 Writer에게 우선권을 부여합니다. Optimistic Spinner가 대기 큐의 첫 번째 대기자를 건너뛰고 잠금을 가로채는 것을 방지합니다.

/* kernel/locking/rwsem.c — 간략화 */
/* HANDOFF 플래그 설정 조건 */
/*
 * 1. 대기 큐의 첫 번째 waiter가 일정 횟수 이상 락 획득에 실패
 * 2. → RWSEM_FLAG_HANDOFF (bit 2) 설정
 * 3. Optimistic Spinner는 HANDOFF가 설정되면 스피닝 중단
 * 4. 락 해제 시 → 첫 번째 waiter에게 직접 HANDOFF
 */

/* kernel/locking/rwsem.c — HANDOFF 확인 */
static inline bool rwsem_try_write_lock(struct rw_semaphore *sem,
                                          struct rwsem_waiter *waiter)
{
    long count, new;

    count = atomic_long_read(&sem->count);
    do {
        bool has_handoff = !!(count & RWSEM_FLAG_HANDOFF);

        if (has_handoff) {
            /* HANDOFF가 설정되어 있으면 첫 번째 waiter만 획득 가능 */
            if (waiter != rwsem_first_waiter(sem))
                return false;
        }

        new = count | RWSEM_WRITER_LOCKED;
        new &= ~RWSEM_FLAG_HANDOFF;

        if (count & RWSEM_READER_MASK)
            return false;  /* Reader가 아직 있음 */

    } while (!atomic_long_try_cmpxchg_acquire(&sem->count,
                                                &count, new));
    return true;
}
HANDOFF vs Optimistic Spinning: 일반적으로 Optimistic Spinning이 먼저 시도되어 빠른 잠금 획득을 지원합니다. 하지만 대기 큐의 첫 번째 waiter가 오래 기다리면 HANDOFF가 활성화되어, Optimistic Spinner가 더 이상 잠금을 가로챌 수 없게 됩니다. 이 균형이 성능과 공정성(Fairness)을 모두 달성하는 핵심입니다.

Writer Starvation 문제와 해결

Writer Starvation은 Reader-Writer Lock의 근본적 문제입니다. Reader가 계속 진입하면 Writer는 영원히 잠금을 획득할 수 없습니다.

Starvation 시나리오

시간 →

CPU 0 (Writer):  [대기 .......................................]  ← Writer Starvation!
CPU 1 (Reader):  [R1 보유][해제][R3 보유][해제][R5 보유] ...
CPU 2 (Reader):  [R2 보유   ][해제][R4 보유   ][해제] ...

문제: Reader R1이 보유 중일 때 Writer가 도착하지만,
R1이 해제되기 전에 R2가 진입하고, R2가 해제되기 전에 R3가 진입...
Reader의 연속 진입으로 reader count가 0이 되는 순간이 없음

커널의 해결 메커니즘

프리미티브메커니즘효과
qrwlock_QW_WAITING 비트 설정새 Reader의 fast path가 실패하여 wait_lock에서 대기
rwsemRWSEM_FLAG_HANDOFF첫 번째 Writer waiter에게 직접 HANDOFF
rwsemRWSEM_FLAG_WAITERSOptimistic Reader가 스피닝 대신 슬립하도록 유도
percpu_rw_semaphoresynchronize_rcu()RCU grace period로 모든 Reader가 빠져나갈 때까지 보장
Writer Starvation 방지: _QW_WAITING 동작 시간 R1, R2 활성 (cnts >> 9 = 2) Writer 도착 _QW_WAITING 설정 R3 진입 시도 → 차단! R1,R2 해제 중... Writer 획득 (_QW_LOCKED) 비교: _QW_WAITING 없이 R1, R2 활성 R3 진입 가능! R4 진입 가능! Writer 영원히 대기... (Starvation!) 문제: 새 Reader가 계속 진입하여 reader count > 0 유지
_QW_WAITING 비트가 새 Reader의 진입을 차단하여 기존 Reader가 빠져나가면 Writer가 획득합니다

percpu_rw_semaphore 분석

percpu_rw_semaphore읽기 경로의 오버헤드를 거의 0으로 만들기 위해 Per-CPU 카운터를 사용합니다. 대신 쓰기 경로는 모든 CPU의 카운터를 합산해야 하므로 매우 비쌉니다.

/* include/linux/percpu-rwsem.h */
struct percpu_rw_semaphore {
    struct rcu_sync     rss;       /* RCU synchronization state */
    unsigned int __percpu *read_count; /* Per-CPU reader 카운터 */
    struct rcuwait      writer;    /* Writer 대기 */
    wait_queue_head_t   waiters;   /* 슬로우 패스 대기 큐 */
    atomic_t            block;     /* Reader 차단 플래그 */
};

/* Reader fast path — preempt_disable + per-cpu 증가만! */
void percpu_down_read(struct percpu_rw_semaphore *sem)
{
    might_sleep();
    rwsem_acquire_read(&sem->dep_map, 0, 0, _RET_IP_);

    preempt_disable();
    /*
     * rcu_sync_is_idle()가 true이면 (Writer가 없으면)
     * Per-CPU 카운터만 증가 — 전역 atomic 연산 없음!
     */
    if (likely(rcu_sync_is_idle(&sem->rss)))
        this_cpu_inc(*sem->read_count);
    else
        __percpu_down_read(sem, false);  /* slow path */
    preempt_enable();
}

/* Writer 경로 — 매우 비쌈 */
void percpu_down_write(struct percpu_rw_semaphore *sem)
{
    /* 1. RCU sync 시작 — 이후 Reader는 slow path */
    rcu_sync_enter(&sem->rss);

    /* 2. 새 Reader 차단 */
    atomic_set(&sem->block, 1);

    /* 3. RCU grace period 대기 — 진행 중인 Reader의
     *    preempt_disable 섹션이 모두 완료될 때까지 */
    synchronize_rcu();

    /* 4. 모든 Per-CPU 카운터 합산하여 0이 될 때까지 대기 */
    rcuwait_wait_event(&sem->writer,
        readers_active_check(sem));
}
비용 경고: percpu_down_write()synchronize_rcu()를 호출하며, 이는 모든 CPU에서 RCU grace period가 완료될 때까지 기다립니다. 전형적으로 수 밀리초에서 수십 밀리초가 소요됩니다. 따라서 쓰기가 빈번한 상황에서는 절대 사용하면 안 됩니다.
percpu_rw_semaphore: Per-CPU Reader 카운터 CPU 0 read_count: 3 로컬 캐시만 접근 CPU 1 read_count: 1 로컬 캐시만 접근 CPU 2 read_count: 0 CPU 3 read_count: 2 ... Writer 경로 (percpu_down_write) 1. rcu_sync_enter() 2. block = 1 (새 Reader 차단) 3. synchronize_rcu() 4. SUM(per-cpu read_count) == 0 대기 → Writer 획득 Reader: this_cpu_inc() ~2-5ns Writer: sync_rcu + percpu_sum ~ms
Reader는 Per-CPU 카운터만 수정하므로 캐시 경합이 없고, Writer는 모든 CPU 카운터를 합산합니다

커널 내 대표적 사용처

사용처변수명이유
cgroup 스레드(Thread) 그룹cgroup_threadgroup_rwsem프로세스 fork/exit가 cgroup 마이그레이션보다 압도적으로 빈번
CPU hotplugcpus_read_lock()CPU 구성 읽기가 hotplug 이벤트보다 훨씬 빈번
파일시스템(Filesystem) freezesb->s_writers일반 I/O가 freeze보다 훨씬 빈번
Memory CG chargememcg event메모리 charge가 리미트 변경보다 빈번

percpu_rw_semaphore 추가 API

percpu_rw_semaphore는 기본 lock/unlock 외에도 trylock, 상태 확인, 초기화/해제 등 다양한 API를 제공합니다.

API설명비용
DEFINE_STATIC_PERCPU_RWSEM(name)정적 선언 + 초기화컴파일 타임
percpu_init_rwsem(sem)동적 초기화 (per-cpu 할당)alloc_percpu
percpu_free_rwsem(sem)해제 (per-cpu 메모리 반환)free_percpu
percpu_down_read(sem)Reader 획득~2-5ns (fast)
percpu_up_read(sem)Reader 해제~2-5ns (fast)
percpu_down_read_trylock(sem)Reader 시도 (비차단(Non-blocking))~5ns
percpu_down_write(sem)Writer 획득~ms (sync_rcu)
percpu_up_write(sem)Writer 해제rcu_sync_exit
percpu_is_read_locked(sem)Reader 보유 여부 확인per-cpu 합산
percpu_is_write_locked(sem)Writer 보유 여부 확인~0 (atomic)
percpu_rwsem_assert_held(sem)lockdep 어서션디버그 전용
guard(percpu_read)(&sem)범위 기반 읽기 가드 — 블록 탈출 시 자동 해제 (v6.15+)Reader
guard(percpu_write)(&sem)범위 기반 쓰기 가드 — 블록 탈출 시 자동 해제 (v6.15+)Writer
scoped_guard(percpu_read_try, &sem)trylock 조건부 가드 — 획득 실패 시 블록 건너뜀 (v6.15+)Reader

커널 6.15부터: percpu_rw_semaphoreDEFINE_GUARD/DEFINE_GUARD_COND 매크로 기반 scope-based 가드(Guard)가 추가되었습니다 (Peter Zijlstra, 2025-03). guard(percpu_read), guard(percpu_write), scoped_guard(percpu_read_try)를 통해 블록 탈출 시 자동으로 잠금이 해제됩니다. 기존 rwsem에 이미 제공되던 guard(read)/guard(write)와 동일한 패턴을 percpu 변형에도 적용한 것입니다.

percpu_rw_semaphore guard 사용 예

/* v6.15+: DEFINE_GUARD 기반 scope-based 잠금
 * include/linux/percpu-rwsem.h */

/* 읽기 가드 — 블록 탈출 시 percpu_up_read 자동 호출 */
{
    guard(percpu_read)(&my_percpu_rwsem);
    /* 임계 영역 */
    value = shared_data.field;
}   /* 여기서 percpu_up_read 자동 호출 */

/* trylock 조건부 가드 */
scoped_guard(percpu_read_try, &my_percpu_rwsem) {
    /* 획득 성공 시만 진입 */
    value = shared_data.field;
}

/* 이전 방식 (v6.14 이하) */
percpu_down_read(&my_percpu_rwsem);
value = shared_data.field;
percpu_up_read(&my_percpu_rwsem);

percpu_down_read_trylock 구현

/* include/linux/percpu-rwsem.h */
static inline bool percpu_down_read_trylock(
    struct percpu_rw_semaphore *sem)
{
    bool ret = true;

    preempt_disable();
    /*
     * fast path: Writer 없으면 per-cpu 카운터만 증가
     * 동일한 rcu_sync_is_idle 확인
     */
    if (likely(rcu_sync_is_idle(&sem->rss)))
        this_cpu_inc(*sem->read_count);
    else
        ret = __percpu_down_read(sem, true);  /* try=true */
    preempt_enable();

    /*
     * try=true일 때 __percpu_down_read 동작:
     *   1. this_cpu_inc → smp_mb → block 확인
     *   2. block == 0 → 성공 (true 반환)
     *   3. block == 1 → this_cpu_dec → 실패 (false 반환)
     *   → wait_event 없이 즉시 반환 (비차단)
     *
     * percpu_down_read와의 차이:
     *   percpu_down_read: try=false → block 시 wait_event 슬립
     *   percpu_down_read_trylock: try=true → block 시 즉시 false
     */
    return ret;
}

percpu_is_read_locked / percpu_is_write_locked

/* include/linux/percpu-rwsem.h */
static inline bool percpu_is_read_locked(
    struct percpu_rw_semaphore *sem)
{
    /*
     * 모든 CPU의 per-cpu read_count를 합산하여 > 0인지 확인
     *
     * 주의: 이 함수는 정확한 스냅샷을 보장하지 않음!
     *   → 합산 도중 Reader가 진입/해제될 수 있음
     *   → lockdep 어서션이나 디버깅 목적으로만 사용
     *   → 동기화 결정의 기반으로 사용하면 안 됨
     */
    return per_cpu_sum(*sem->read_count) != 0 &&
           rcu_sync_is_idle(&sem->rss);
}

static inline bool percpu_is_write_locked(
    struct percpu_rw_semaphore *sem)
{
    /*
     * 내부 rwsem의 Writer 보유 여부 확인
     * → 단일 atomic 읽기로 충분 (per-cpu 합산 불필요)
     * → lockdep 어서션 목적
     */
    return atomic_read(&sem->block);
}

/* lockdep 어서션 예시 */
void cgroup_migrate(struct task_struct *tsk)
{
    /* cgroup_threadgroup_rwsem Writer 보유 확인 */
    percpu_rwsem_assert_held(&cgroup_threadgroup_rwsem);
    /* ... 마이그레이션 수행 ... */
}

/* percpu_init_rwsem / percpu_free_rwsem */
int percpu_init_rwsem(struct percpu_rw_semaphore *sem,
                       const char *name,
                       struct lock_class_key *key)
{
    /*
     * 1. per-cpu unsigned int 할당 (alloc_percpu)
     *    → 각 CPU마다 자체 read_count 캐시라인
     *    → 할당 실패 시 -ENOMEM 반환
     *
     * 2. rcu_sync 초기화 (rcu_sync_init)
     * 3. rcuwait 초기화 (Writer 대기용)
     * 4. wait_queue 초기화 (Reader slow path 대기용)
     * 5. block = 0 (초기: Reader 차단 없음)
     * 6. 내부 rwsem 초기화 (Writer 직렬화용)
     * 7. lockdep 키 등록
     */
    sem->read_count = alloc_percpu(unsigned int);
    if (!sem->read_count)
        return -ENOMEM;

    rcu_sync_init(&sem->rss);
    rcuwait_init(&sem->writer);
    init_waitqueue_head(&sem->waiters);
    atomic_set(&sem->block, 0);
    __init_rwsem(&sem->rw_sem, name, key);
    return 0;
}

void percpu_free_rwsem(struct percpu_rw_semaphore *sem)
{
    /* rcu_sync 정리 → 진행 중인 RCU 콜백 대기 */
    rcu_sync_dtor(&sem->rss);
    /* per-cpu 메모리 해제 */
    free_percpu(sem->read_count);
    sem->read_count = NULL;
}

down_write → downgrade_write 패턴

downgrade_write()는 Writer 잠금을 Reader 잠금으로 원자적으로 전환합니다. 이 패턴은 먼저 배타적으로 데이터를 수정한 후, 수정된 데이터를 읽기만 하는 후속 작업 동안 다른 Reader의 동시 접근을 허용할 때 유용합니다.

/* downgrade_write 사용 패턴 */
down_write(&sem);

/* 배타적으로 데이터 수정 */
data->value = new_value;
list_add(&new_entry->list, &data->list);

/* Writer → Reader 전환 (대기 중인 Reader들 깨움) */
downgrade_write(&sem);

/* 이제 Reader로서 수정된 데이터를 기반으로 추가 작업 */
result = compute_with(data);

up_read(&sem);

/*
 * 내부 구현:
 *   1. count에서 WRITER_LOCKED 클리어 + READER_BIAS 추가
 *   2. 대기 큐의 Reader들을 깨움
 *   3. owner를 READER_OWNED로 변경
 *
 * 주의: down_read → upgrade_write 함수는 존재하지 않음!
 *       이를 수동 구현하면 두 Reader가 동시에 upgrade하여 데드락 발생
 */
upgrade 경고: up_read() → down_write()를 수동으로 수행하면 TOCTTOU(Time-Of-Check-To-Time-Of-Use) 경합이 발생합니다. Reader 해제와 Writer 획득 사이에 다른 Writer가 데이터를 변경할 수 있습니다. 이 패턴이 필요하면 처음부터 down_write()를 사용하세요.

PREEMPT_RT에서의 rwlock/rwsem

CONFIG_PREEMPT_RT 커널에서 rwlock_t는 busy-wait 대신 sleeping lock(rwbase_rt)으로 변환됩니다. 이는 인터럽트 핸들러도 스레드화되어 슬립이 가능하기 때문입니다.

/* include/linux/rwlock_rt.h — PREEMPT_RT 정의 */
typedef struct {
    struct rwbase_rt rwbase;
    unsigned int magic;
} rwlock_t;

/* rwbase_rt 구조 */
struct rwbase_rt {
    struct rt_mutex_base rtmutex;  /* PI(Priority Inheritance) 지원 */
    atomic_t             readers;  /* Reader count */
};

/*
 * RT에서의 핵심 변화:
 *   - rwlock_t는 더 이상 busy-wait하지 않음
 *   - rt_mutex 기반이므로 PI(Priority Inheritance) 지원
 *   - read_lock()도 슬립 가능 → softirq에서 사용 시 주의
 *   - raw_rwlock은 존재하지 않음!
 *     (인터럽트에서 진짜 busy-wait이 필요하면 raw_spinlock 사용)
 */
Non-RT 커널PREEMPT_RT 커널비고
rwlock_t = qrwlock (busy-wait)rwlock_t = rwbase_rt (sleeping)API 동일, 동작 다름
rw_semaphore = sleepingrw_semaphore = sleeping (동일)변화 없음
read_lock_irqsave() = IRQ 비활성화read_lock_irqsave() = preempt_disable만IRQ 스레드화됨
Reader 중 선점 불가Reader 중 선점 가능결정적 지연 시간 보장
PREEMPT_RT: rwlock_t 변환 구조 Non-RT 커널 rwlock_t qrwlock busy-wait 선점 비활성화 IRQ 비활성화 가능 cnts (atomic) wait_lock (qspinlock) PI 미지원 PREEMPT_RT 커널 rwlock_t rwbase_rt sleeping lock 선점 가능 IRQ 스레드화 rt_mutex_base readers (atomic) PI(우선순위 상속) 지원 RT 변환
PREEMPT_RT에서 rwlock_t는 동일한 API를 유지하면서 내부 구현이 rwbase_rt(sleeping, PI 지원)로 변환됩니다
RT 개발 지침: RT에서 진정한 busy-wait RW lock이 필요한 경우는 극히 드뭅니다. 대부분 raw_spinlock_t(단순 배타적 잠금)로 충분합니다. rwlock_t의 RT 변환은 spinlock_trt_mutex 변환과 동일한 설계 철학입니다. 자세한 내용은 Spinlock — PREEMPT_RT를 참고하세요.

rwbase_rt 내부 구현 상세

PREEMPT_RT 커널에서 rwlock_trwbase_rt로 변환됩니다. 이 구조는 rt_mutex_base를 기반으로 하여 PI(Priority Inheritance)를 지원하는 sleeping RW lock입니다.

rwbase_rt 구조체 상세

/* kernel/locking/rwbase_rt.c */
struct rwbase_rt {
    struct rt_mutex_base rtmutex;
    /*
     * rt_mutex_base:
     *   .owner    — Writer owner (PI chain용)
     *   .wait_lock — 대기 큐 보호 spinlock
     *   .waiters  — PI-sorted 대기자 rbtree
     *
     * PI(Priority Inheritance) 동작:
     *   Writer가 락을 보유하고 있을 때,
     *   높은 우선순위 태스크가 대기하면
     *   Writer의 우선순위가 일시적으로 승격
     *   → 우선순위 역전(Priority Inversion) 방지
     */

    atomic_t readers;
    /*
     * readers 카운터:
     *   > 0  — 현재 활성 Reader 수
     *   == 0 — Reader/Writer 없음 (FREE) 또는 Writer 보유
     *
     * Non-RT qrwlock의 cnts와 유사하지만:
     *   - Writer 비트가 없음 (rtmutex.owner로 관리)
     *   - Reader는 슬립 가능 (busy-wait 아님)
     *   - atomic_add로 Reader 등록 후, rtmutex를 잠깐 잡아
     *     Writer 부재를 확인
     */
};

/*
 * PREEMPT_RT rwlock_t typedef:
 *   typedef struct {
 *       struct rwbase_rt rwbase;
 *       unsigned int     magic;   // DEBUG용 매직 넘버
 *   } rwlock_t;
 *
 * magic 필드:
 *   RWLOCK_MAGIC = 0xdeaf1eed
 *   디버그 빌드에서 초기화 안 된 rwlock 감지
 */

RT read_lock 구현

/* kernel/locking/rwbase_rt.c — 단순화 */
static int __rwbase_read_lock(struct rwbase_rt *rwb,
                               unsigned int state)
{
    struct rt_mutex_base *rtm = &rwb->rtmutex;
    int ret;

    raw_spin_lock_irq(&rtm->wait_lock);

    /*
     * ① Reader count 증가 시도
     *    Writer가 없으면 (rtm->owner == NULL) 즉시 성공
     */
    if (!rt_mutex_base_is_locked(rtm)) {
        atomic_inc(&rwb->readers);
        raw_spin_unlock_irq(&rtm->wait_lock);
        return 0;
    }

    /*
     * ② Writer 보유 중 → 대기 큐에 삽입 후 슬립
     *    PI-sorted rbtree에 삽입:
     *    → 우선순위 높은 Reader가 앞에 위치
     *    → Writer의 PI chain에 연결
     *    → Writer 우선순위가 필요 시 승격됨
     */
    ret = rt_mutex_slowlock_block(rtm, NULL, state,
                                    NULL, &waiter);

    /* ③ 깨어나면 Reader count 증가 */
    if (!ret)
        atomic_inc(&rwb->readers);

    raw_spin_unlock_irq(&rtm->wait_lock);
    return ret;
}

/*
 * Non-RT vs RT read_lock 비교:
 *
 * Non-RT (qrwlock):
 *   atomic_add_return_acquire(_QR_BIAS, &cnts)  → busy-wait
 *   → 선점 비활성화 상태에서 spin
 *   → 짧은 임계영역에 최적화
 *
 * RT (rwbase_rt):
 *   raw_spin_lock → readers++ → raw_spin_unlock  → sleeping
 *   → 선점 가능! 임계영역 중 높은 우선순위 태스크가 실행 가능
 *   → PI 지원으로 우선순위 역전 방지
 *   → 결정적 지연 시간(Deterministic Latency) 보장
 */

RT write_lock 구현

/* kernel/locking/rwbase_rt.c — 단순화 */
static int __rwbase_write_lock(struct rwbase_rt *rwb,
                                unsigned int state)
{
    struct rt_mutex_base *rtm = &rwb->rtmutex;

    /* ① rtmutex 획득 — Writer 간 직렬화 + PI 지원
     *    rt_mutex_lock은 PI chain을 통해
     *    현재 holder의 우선순위를 승격시킴
     */
    rt_mutex_slowlock(rtm, NULL, state);

    /* ② Reader drain 대기
     *    atomic_read(&rwb->readers) == 0이 될 때까지 슬립
     *
     *    Non-RT와의 핵심 차이:
     *    → Non-RT: atomic_cond_read로 busy-wait
     *    → RT: rcuwait_wait_event로 sleeping wait
     *    → 대기 중 선점 가능, CPU를 다른 태스크에 양보
     */
    rwbase_write_wait(rwb);

    return 0;
}

static void rwbase_write_wait(struct rwbase_rt *rwb)
{
    /* readers가 0이 될 때까지 sleeping wait
     * → 마지막 Reader가 read_unlock 시 깨움
     *
     * Non-RT drain과 비교:
     *   Non-RT: atomic_cond_read_acquire → PAUSE/WFE 루프
     *   RT: schedule 기반 → CPU 양보, 결정적 지연 보장
     */
    while (atomic_read(&rwb->readers))
        rcuwait_wait_event(..., !atomic_read(&rwb->readers));
}

/*
 * RT write_unlock:
 *   1. rtmutex 해제 → PI chain에서 제거
 *   2. 대기 중인 Reader/Writer 중 가장 높은 우선순위 태스크 깨움
 *   → PI-sorted이므로 가장 중요한 태스크가 먼저 실행
 *
 * RT read_unlock:
 *   1. atomic_dec(&rwb->readers)
 *   2. readers == 0이면 Writer 대기자 깨움
 *   → 마지막 Reader가 나가면 Writer에게 알림
 */

RT에서 IRQ 변형의 변환

PREEMPT_RT에서 IRQ 변형의 매핑 변화는 다음과 같습니다.

APINon-RTPREEMPT_RT
read_lock_irq(lock)local_irq_disable + qrwlockmigrate_disable + rwbase_rt
read_lock_bh(lock)local_bh_disable + qrwlockmigrate_disable + rwbase_rt

핵심 변화는 다음과 같습니다.

주의: raw_spin_lock_irq/raw_spin_lock_irqsave는 RT에서도 진짜 IRQ를 비활성화합니다. 이것이 rwbase_rt의 내부 wait_lockraw_spinlock인 이유입니다.

결과적으로 read_lock_irqsave(&lock, flags)는 RT에서 flags가 dummy이고(IRQ 상태를 저장하지 않음), migrate_disable만 수행한 뒤 rwbase_rt를 통해 sleeping lock을 획득합니다.

RT 이식 주의점: Non-RT에서 read_lock_irq() 보유 중에 다른 spinlock을 잡는 코드가 있으면, RT에서는 두 번째 spinlock도 sleeping lock이 됩니다. 이때 잠금 순서가 맞지 않으면 lockdep이 경고합니다. RT 이식 시 모든 rwlock_traw_spinlock_t 전환 필요성을 평가하세요. 실제로 인터럽트 핸들러에서 busy-wait이 필수적인 경우에만 raw_spinlock_t를 사용하고, 나머지는 RT 변환을 수용합니다.

실전 사용 패턴

tasklist_lock: 프로세스 목록 보호

/* kernel/fork.c — 프로세스 생성 시 Writer */
write_lock_irq(&tasklist_lock);
list_add_tail_rcu(&p->sibling, &p->real_parent->children);
list_add_tail_rcu(&p->tasks, &init_task.tasks);
write_unlock_irq(&tasklist_lock);

/* fs/proc/array.c — 프로세스 정보 읽기 시 Reader */
read_lock(&tasklist_lock);
task = pid_task(find_vpid(pid), PIDTYPE_PID);
if (task)
    get_task_struct(task);
read_unlock(&tasklist_lock);

mmap_lock: VMA 보호

/* mm/memory.c — 페이지 폴트 (Reader 경로, 매우 빈번) */
if (!mmap_read_trylock(mm)) {
    if (!(flags & FAULT_FLAG_RETRY_NOWAIT))
        mmap_read_lock(mm);
}
vma = find_vma(mm, address);
/* ... 페이지 폴트 처리 ... */
mmap_read_unlock(mm);

/* mm/mmap.c — mmap() 시스템 콜 (Writer 경로) */
mmap_write_lock(mm);
/* ... VMA 생성/병합/분할 ... */
mmap_write_unlock(mm);

superblock s_writers: 파일시스템 freeze

/* fs/super.c — 일반 I/O (Reader, 매우 빈번) */
sb_start_write(inode->i_sb);
/* ... 파일 쓰기 ... */
sb_end_write(inode->i_sb);

/* fs/super.c — 파일시스템 freeze (Writer, 매우 드묾) */
percpu_down_write(sb->s_writers.rw_sem + SB_FREEZE_WRITE - 1);
/* ... 파일시스템 freeze 처리 ... */

안티패턴과 흔한 실수

1. Writer 대기 중 Reader 재진입 데드락

/* 데드락 시나리오 */
read_lock(&lock);      /* CPU 0: Reader 획득 */
                         /* CPU 1: write_lock() 호출 → _QW_WAITING 설정 */
read_lock(&lock);      /* CPU 0: 재진입 시도 → _QW_WAITING 때문에 slow path */
                         /* → wait_lock에서 대기, CPU 1은 Reader drain 대기 */
                         /* → 데드락! */

/* 해결: lockdep이 이 패턴을 감지함 */
/* 또는 단일 read_lock 섹션으로 합치기 */

2. rwlock_t 보유 중 슬립

/* 잘못된 코드 */
read_lock(&lock);
kmalloc(size, GFP_KERNEL);  /* 슬립 가능! BUG! */
read_unlock(&lock);

/* 올바른 코드 옵션 1: GFP_ATOMIC 사용 */
read_lock(&lock);
kmalloc(size, GFP_ATOMIC);  /* 슬립 안 함 */
read_unlock(&lock);

/* 올바른 코드 옵션 2: rw_semaphore로 전환 */
down_read(&sem);
kmalloc(size, GFP_KERNEL);  /* OK — rwsem은 sleeping lock */
up_read(&sem);

3. Reader → Writer 업그레이드 시도

/* 데드락 시나리오 */
down_read(&sem);
/* ... 조건 확인 ... */
up_read(&sem);
down_write(&sem);      /* TOCTTOU: 이 사이에 다른 Writer가 진입 가능! */
/* ... 조건이 변경되었을 수 있음 ... */

/* 올바른 패턴: 처음부터 Writer로 진입 */
down_write(&sem);
if (!need_modify) {
    downgrade_write(&sem);  /* Writer → Reader 전환 (안전) */
    /* ... 읽기 작업 ... */
    up_read(&sem);
} else {
    /* ... 수정 작업 ... */
    up_write(&sem);
}

4. IRQ 변형 불일치

/* 잘못된 코드: 인터럽트 핸들러에서 같은 rwlock 사용 */

/* 프로세스 컨텍스트 */
read_lock(&lock);        /* IRQ 비활성화 안 함 */
/* ← 여기서 IRQ 발생! */

/* IRQ 핸들러 */
write_lock(&lock);       /* 데드락! Reader가 같은 CPU에서 보유 중 */

/* 올바른 코드 */
read_lock_irqsave(&lock, flags);  /* IRQ 비활성화 */
/* ... */
read_unlock_irqrestore(&lock, flags);

디버깅(Debugging)과 lockdep

lockdep은 rwlock/rwsem의 잘못된 사용을 컴파일 타임이 아닌 런타임에 동적으로 감지합니다.

관련 설정

CONFIG_PROVE_LOCKING=y     # lockdep 교착 감지 활성화
CONFIG_LOCK_STAT=y         # /proc/lock_stat 경합 통계
CONFIG_DEBUG_LOCK_ALLOC=y  # 잠금 할당 추적
CONFIG_DEBUG_RWSEMS=y      # rwsem 전용 디버깅

lockdep 어서션

/* rwsem 보유 상태 검증 */
lockdep_assert_held(&sem);           /* Reader 또는 Writer 보유 */
lockdep_assert_held_write(&sem);     /* Writer만 보유 */
lockdep_assert_held_read(&sem);      /* Reader만 보유 */
lockdep_assert_not_held(&sem);       /* 보유하지 않아야 함 */

/* rwlock_t 보유 상태 검증 */
lockdep_assert_held(&lock);          /* read 또는 write 보유 */
lockdep_assert_held_write(&lock);    /* write_lock 보유 */

lockdep 경고 메시지 해석

=====================================================
WARNING: possible recursive locking detected
-----------------------------------------------------
kworker/0:1/28 is trying to acquire lock:
ffff888100a5c0d0 (&sb->s_type->i_mutex_key#5){++++}-{3:3}

but task is already holding lock:
ffff888100a5c148 (&sb->s_type->i_mutex_key#5){++++}-{3:3}

{++++} → Reader(+) 4개 모드 모두 허용
{----} → 모든 컨텍스트에서 사용됨 (hardirq, softirq, reclaim, ...)
{3:3}  → wait_type (read:write)

lockdep에 대한 자세한 내용은 동시성 디버깅 문서를 참고하세요.

성능 특성과 비교

프리미티브Reader 비용Writer 비용CPU 확장성최적 시나리오
spinlock_t~10-20ns~10-20ns보통읽기/쓰기 비율 비슷, 짧은 임계영역
rwlock_t~15-30ns (atomic_add)~50-500ns (drain 대기)제한적읽기 >> 쓰기, 인터럽트 컨텍스트
rw_semaphore~20-50ns (cmpxchg)~100ns-10us보통읽기 >> 쓰기, 프로세스 컨텍스트
percpu_rw_semaphore~2-5ns (per-cpu inc)~ms (sync_rcu)우수읽기 극도로 빈번, 쓰기 극히 드묾
seqlock~5-10ns (재시도 가능)~10-20ns우수짧은 데이터 읽기, 재시도 허용
RCU~0ns (rcu_read_lock)~ms (sync_rcu)최고포인터 기반 게시, 대기 가능
Reader 비용 vs Writer 비용 (로그 스케일) Writer 비용 (로그) → Reader 비용 (로그) → ~10ns ~100ns ~10us ~1ms 0ns 5ns 20ns 50ns spinlock rwlock_t rwsem percpu_rwsem seqlock RCU 양쪽 비용 모두 낮은 영역
Reader 비용과 Writer 비용은 트레이드오프 관계: percpu_rwsem과 RCU는 Reader가 거의 무비용이지만 Writer가 매우 비쌉니다

캐시라인 경합 분석

rwlock_t의 캐시라인 경합 패턴을 분석합니다. 모든 Reader가 동일한 cnts 캐시라인에 atomic_add를 수행하므로, CPU 수가 증가할수록 캐시 일관성 트래픽이 증가합니다.

32-CPU 시스템에서의 대략적 Reader 오버헤드는 다음과 같습니다.

프리미티브오버헤드특성
rwlock_t~200ns전역 atomic 경합
percpu_rw_semaphore~5ns로컬 캐시만

perf로 측정할 수 있습니다.

perf stat -e cache-misses,cache-references \
  -p $(pgrep -f "rwlock_test") -- sleep 5

perf c2c record -p $PID -- sleep 5
perf c2c report  # false sharing/true sharing 분석

대안 선택: seqlock, RCU, percpu

Reader-Writer Lock은 항상 최선의 선택이 아닙니다. 많은 경우 seqlock이나 RCU가 더 나은 성능을 제공합니다.

조건추천 프리미티브이유
포인터 기반 데이터, Reader 대부분RCUReader 오버헤드 0, 포인터 publish-subscribe 모델
작은 데이터(8-16바이트), 재시도 허용seqlockWriter가 Reader를 차단하지 않음
단순 카운터/통계per-CPU 변수잠금 자체가 불필요
읽기 99.9%+, 쓰기 극히 드묾percpu_rw_semaphoreReader ~0 비용
인터럽트에서 RW 필요rwlock_t유일한 busy-wait RW lock
프로세스 컨텍스트, 중간 읽기/쓰기 비율rw_semaphoreOptimistic Spinning + HANDOFF
읽기/쓰기 비율 비슷mutexRW lock 오버헤드가 이점을 상쇄
경험 법칙: 읽기가 쓰기보다 10배 이상 빈번하지 않으면, Reader-Writer Lock의 추가 복잡성(Writer Starvation 위험, 더 큰 구조체, 더 복잡한 디버깅)이 성능 이점을 상쇄합니다. 확실하지 않으면 mutex부터 시작하고, 프로파일링(Profiling) 결과가 필요할 때만 RW lock으로 전환하세요.

관련 커널 설정 옵션

설정설명기본값
CONFIG_RWSEM_SPIN_ON_OWNERrwsem Optimistic Spinning 활성화y (SMP & MUTEX_SPIN_ON_OWNER)
CONFIG_QUEUED_RWLOCKSqrwlock 사용 (rwlock_t 구현)y (대부분 아키텍처)
CONFIG_PROVE_LOCKINGlockdep 교착 감지n (디버그 빌드에서 y)
CONFIG_LOCK_STAT/proc/lock_stat 경합 통계n
CONFIG_DEBUG_LOCK_ALLOC잠금 할당 추적n
CONFIG_DEBUG_RWSEMSrwsem 전용 디버깅n
CONFIG_PREEMPT_RTrwlock_t를 rwbase_rt로 변환n
CONFIG_RWSEM_GENERIC_SPINLOCK아키텍처 고유 rwsem 비활성화 (레거시)제거됨 (v5.x+)
# /proc/lock_stat 출력 예시 (rwsem 관련)
# lock_stat 활성화:
echo 1 > /proc/lock_stat

# 출력 확인:
cat /proc/lock_stat | grep -A2 "mmap_lock"

#                 contentions   waittime-min  waittime-max  waittime-total
# mmap_lock-W:           2345       0.52         125.40          8234.50
# mmap_lock-R:            123       0.10          12.30           456.78
# -W: Writer 경합, -R: Reader 경합

qrwlock 소스 코드 심층 분석

qrwlock의 내부 구현은 kernel/locking/qrwlock.cinclude/asm-generic/qrwlock.h에 걸쳐 있으며, cnts 필드의 비트 인코딩과 atomic_cond_read_acquire를 중심으로 동작합니다. 이 섹션에서는 실제 커널 소스의 세밀한 경로를 한 줄씩 추적합니다.

cnts 필드 인코딩 심층

/* include/asm-generic/qrwlock.h */
/*
 * cnts 32비트 필드의 전체 상태 전이:
 *
 * 상태                cnts 값        의미
 * ─────────────────────────────────────────
 * FREE                0x00000000     아무도 보유하지 않음
 * 1 Reader            0x00000200     _QR_BIAS (1 << 9)
 * N Readers           N << 9         최대 ~8M readers
 * Writer LOCKED       0x000000ff     _QW_LOCKED
 * Writer WAITING      0x00000100     _QW_WAITING
 * W_WAIT + 3 Readers  0x00000700     _QW_WAITING | (3 << 9)
 * W_LOCK + 0 Readers  0x000000ff     Writer 보유 중 (Reader drain 완료)
 *
 * 핵심 불변량(invariant):
 *   - Writer LOCKED (0xff)와 Reader count > 0은 동시에 성립 불가
 *   - Writer WAITING 중에도 기존 Reader는 계속 보유 가능
 *   - Writer WAITING이 설정되면 새 Reader의 fast path 실패
 */

/* cnts 조작 헬퍼 매크로 */
#define _QW_WAITING   0x100      /* bit 8 */
#define _QW_LOCKED    0x0ff      /* bits 7:0 전부 1 → 0xff */
#define _QW_WMASK     0x1ff      /* bits 8:0 → WAITING | LOCKED */
#define _QR_SHIFT     9
#define _QR_BIAS      (1U << _QR_SHIFT)  /* 0x200 */

/* wlocked 바이트 직접 접근 — union 활용 */
/*
 * Little-endian에서 wlocked는 cnts의 바이트 0에 위치
 * → smp_store_release(&lock->wlocked, 1)로 Writer locked 설정 가능
 * → 전체 32비트 cmpxchg 없이 바이트 스토어로 충분
 * → ARM64/RISC-V에서 성능 이점 (작은 단위 스토어)
 */

queued_read_lock 상세 경로

/* include/asm-generic/qrwlock.h */
static inline void queued_read_lock(struct qrwlock *lock)
{
    int cnts;

    /* ① atomic_add_return_acquire: cnts += _QR_BIAS (0x200)
     *    acquire 의미론: 이 연산 이후의 메모리 접근이
     *    이 연산 이전으로 재배치되지 않음을 보장
     *    → 임계 영역의 읽기가 락 획득 이전으로 이동 불가 */
    cnts = atomic_add_return_acquire(_QR_BIAS, &lock->cnts);

    /* ② Writer 비트 확인: _QW_WMASK = 0x1ff
     *    하위 9비트가 0이면 Writer 없음 → fast path 성공
     *    likely() 힌트: 대부분의 경우 Writer 없음 */
    if (likely(!(cnts & _QW_WMASK)))
        return;

    /* ③ Writer 존재 → slow path
     *    먼저 추가한 reader count를 되돌려야 함 */
    queued_read_lock_slowpath(lock);
}

queued_read_lock_slowpath 전체 분석

/* kernel/locking/qrwlock.c */
void queued_read_lock_slowpath(struct qrwlock *lock)
{
    /* Step 1: 낙관적으로 추가한 reader bias를 되돌림
     *         Writer가 drain 중이므로, reader count가 높으면
     *         Writer가 더 오래 기다려야 함 → 빠르게 빼줘야 */
    atomic_sub(_QR_BIAS, &lock->cnts);

    /* Step 2: wait_lock(qspinlock) 획득
     *         → Reader끼리의 thundering herd 방지
     *         → Writer 해제 직후 모든 Reader가 동시에
     *           atomic_add를 하면 캐시라인 폭풍 발생
     *         → wait_lock이 하나씩 진입하게 직렬화 */
    arch_spin_lock(&lock->wait_lock);

    /* Step 3: atomic_cond_read_acquire — Writer locked 해제 대기
     *
     * atomic_cond_read_acquire(ptr, cond):
     *   - ptr을 반복 읽으며 cond가 참이 될 때까지 대기
     *   - x86: PAUSE 루프로 구현
     *   - ARM64: WFE(Wait For Event) + LDAXR로 구현
     *     → WFE는 CPU를 저전력 대기 상태로 전환
     *     → exclusive monitor가 해제되면 SEV로 깨움
     *   - RISC-V: fence + spin 루프
     *
     * _QW_LOCKED (0xff)가 클리어될 때까지 대기
     * _QW_WAITING(0x100)은 상관없음 — Reader는 WAITING 상태에서도
     * Writer가 실제 LOCKED가 아니면 진입 가능 */
    atomic_cond_read_acquire(&lock->cnts,
        !(VAL & _QW_LOCKED));

    /* Step 4: Writer locked 해제됨 → Reader count 다시 추가
     *         + wait_lock 해제하여 다음 대기 Reader 허용 */
    atomic_add(_QR_BIAS, &lock->cnts);
    arch_spin_unlock(&lock->wait_lock);
}

queued_write_lock_slowpath 전체 분석

/* kernel/locking/qrwlock.c */
void queued_write_lock_slowpath(struct qrwlock *lock)
{
    /* ① Writer 간 직렬화 — wait_lock(qspinlock) 획득
     *    여러 Writer가 동시에 진입하면 이 spinlock에서 대기
     *    → 한 번에 하나의 Writer만 cnts를 조작 가능 */
    arch_spin_lock(&lock->wait_lock);

    /* ② 빠른 재시도: 아무도 없으면 즉시 획득
     *    atomic_read로 먼저 확인 → cmpxchg 호출 최소화 */
    if (!atomic_read(&lock->cnts) &&
        (atomic_cmpxchg_acquire(&lock->cnts, 0,
            _QW_LOCKED) == 0))
        goto unlock;

    /* ③ _QW_WAITING 비트 설정 (bit 8)
     *    핵심 메커니즘: 이 비트가 설정되면
     *    queued_read_lock()의 fast path 조건
     *    !(cnts & _QW_WMASK)가 실패함
     *    → 새 Reader는 slow path로 강제 이동
     *    → Writer starvation 방지의 핵심 */
    atomic_or(_QW_WAITING, &lock->cnts);

    /* ④ 기존 Reader drain 대기
     *    cnts == _QW_WAITING (0x100)이 될 때까지 spin
     *    → Reader count가 0이고 Writer waiting만 남은 상태
     *
     *    atomic_cond_read_acquire 사용:
     *    - 아키텍처 최적화된 busy-wait
     *    - ARM64: WFE 사용으로 전력 절약
     *    - x86: PAUSE로 파이프라인 비움 */
    atomic_cond_read_acquire(&lock->cnts,
        VAL == _QW_WAITING);

    /* ⑤ WAITING → LOCKED 전환
     *    atomic_sub로 _QW_WAITING 제거 → cnts = 0
     *    smp_store_release로 wlocked = 1 설정
     *    → 바이트 스토어이므로 전체 cmpxchg보다 효율적
     *    → release 의미론: 이전 메모리 접근이 완료된 후 저장 */
    atomic_sub(_QW_WAITING, &lock->cnts);
    smp_store_release(&lock->wlocked, 1);

unlock:
    arch_spin_unlock(&lock->wait_lock);
}

/* queued_write_unlock — Writer 해제 */
static inline void queued_write_unlock(struct qrwlock *lock)
{
    /* wlocked 바이트를 0으로 설정 (release 의미론)
     *   → 임계 영역의 모든 스토어가 이 해제 이전에 완료
     *   → Reader의 atomic_cond_read_acquire가 이를 관찰 */
    smp_store_release(&lock->wlocked, 0);
}
qrwlock cnts 상태 전이 상세 FREE cnts = 0x000 N Readers cnts = N << 9 Writer LOCKED cnts = 0x0ff W_WAITING + N Readers cnts = 0x100 | (N << 9) W_WAITING (drain 완료) cnts = 0x100 atomic_add(_QR_BIAS) read_unlock (last) cmpxchg(0, _QW_LOCKED) write_unlock Writer: atomic_or(W_WAITING) readers drain sub(W_WAIT) + wlocked=1 핵심 불변량: 1. Writer LOCKED(0xff)와 Reader count > 0은 절대 동시 성립 불가 2. W_WAITING 설정 시 새 Reader fast path 차단 → Writer starvation 방지
cnts 필드의 상태 전이: FREE ↔ Readers ↔ W_WAITING+Readers → W_WAITING → Writer LOCKED

atomic_cond_read_acquire 내부

include/linux/atomic/atomic-instrumented.hatomic_cond_read_acquire(v, cond)v를 반복적으로 읽으며 cond가 참이 될 때까지 대기합니다. VAL은 매크로 내부에서 현재 읽은 값을 참조하는 특수 변수입니다.

아키텍처별 구현 차이는 다음과 같습니다.

x86 (arch/x86/include/asm/barrier.h)
내부적으로 READ_ONCE + cpu_relax() 루프를 사용합니다. cpu_relax()PAUSE 명령어(rep; nop)로 파이프라인(Pipeline)을 비우고 ~140 사이클을 대기합니다.
ARM64 (arch/arm64/include/asm/barrier.h)
__atomic_load_n(v, __ATOMIC_RELAXED)로 값을 읽고, 조건이 충족되지 않으면 __wfe()(Wait For Event)로 CPU를 저전력 대기 상태로 전환합니다. exclusive monitor가 해당 캐시라인 변경을 감지하면 SEV(Send Event)로 WFE가 해제됩니다. 루프 종료 후 __dmb(ishld)로 acquire 배리어를 수행합니다.
RISC-V
fence + spin 루프를 사용합니다(WFE 미지원 ISA). Svvptc 확장이 있으면 WRS.STO를 사용할 수 있습니다.
성능 차이: ARM64의 WFE는 x86의 PAUSE보다 전력 효율이 높습니다. PAUSE는 ~140 사이클 동안 파이프라인을 비우지만 CPU는 활성 상태이고, WFE는 exclusive monitor가 이벤트를 발생시킬 때까지 CPU를 실제로 대기 상태로 전환합니다. 다만 WFE에서 깨어나는 지연이 PAUSE보다 길 수 있어, 짧은 스핀에서는 PAUSE가 유리할 수 있습니다.

rwsem 소스 코드 심층 분석

rw_semaphore의 핵심 슬로우패스 구현은 kernel/locking/rwsem.c에 있으며, 약 1,500줄에 달하는 복잡한 상태 머신으로 구성됩니다. count 필드의 비트 인코딩, optimistic spinning 조건, 대기 큐 관리를 깊이 분석합니다.

count 비트 필드 상세

/* kernel/locking/rwsem.c */
#define RWSEM_WRITER_LOCKED   (1UL << 0)   /* bit 0 */
#define RWSEM_FLAG_WAITERS    (1UL << 1)   /* bit 1 */
#define RWSEM_FLAG_HANDOFF    (1UL << 2)   /* bit 2 */
#define RWSEM_READER_BIAS     (1UL << 8)   /* Reader 1 증가분 */

/*
 * count 값 인코딩 상세:
 *
 *  bits 63:8  — Reader Count (양수: 활성 reader 수)
 *  bit  2     — HANDOFF: 첫 대기자에게 직접 전달
 *  bit  1     — WAITERS: 대기 큐에 waiter 존재
 *  bit  0     — WRITER_LOCKED: Writer가 보유 중
 *
 *  특수 조합:
 *  count = RWSEM_READER_BIAS → 1 reader, 대기자 없음
 *  count = RWSEM_WRITER_LOCKED | RWSEM_FLAG_WAITERS
 *        → Writer 보유 중 + 대기자 존재
 *  count = RWSEM_WRITER_LOCKED | RWSEM_FLAG_WAITERS | RWSEM_FLAG_HANDOFF
 *        → Writer 보유 중 + 대기자 존재 + HANDOFF 활성
 *  count < 0 → Reader count 오버플로 (이론상, 실제 발생 불가)
 */

/* owner 필드 인코딩 */
#define RWSEM_READER_OWNED    (1UL << 0)
#define RWSEM_NONSPINNABLE    (1UL << 1)
/*
 * owner 포인터의 하위 2비트를 플래그로 활용 (task_struct 정렬 보장):
 *   bit 0 = READER_OWNED: Reader가 보유 중 (writer spinning 차단)
 *   bit 1 = NONSPINNABLE: optimistic spinning 하지 말 것
 *
 * Writer 보유 시: owner = current task_struct *
 * Reader 보유 시: owner = RWSEM_READER_OWNED (또는 첫 reader의 주소 | READER_OWNED)
 */

rwsem_down_read_slowpath 분석

/* kernel/locking/rwsem.c — 단순화된 down_read slowpath */
static struct rw_semaphore *
rwsem_down_read_slowpath(struct rw_semaphore *sem,
                          long count, unsigned int state)
{
    struct rwsem_waiter waiter;
    long adjustment;
    bool wake = false;

    /* ① Optimistic Spinning 시도 (Writer가 CPU에서 실행 중이면)
     *    Reader도 optimistic spin 가능 (v5.0+)
     *    조건: 대기 큐가 비어있고, owner가 CPU에서 실행 중 */
    if (rwsem_can_spin_on_owner(sem)) {
        if (rwsem_optimistic_spin(sem, NULL)) {
            /* spinning 중 lock 획득 성공! */
            return sem;
        }
    }

    /* ② waiter 구조체 초기화 */
    waiter.task = current;
    waiter.type = RWSEM_WAITING_FOR_READ;
    waiter.timeout = jiffies + RWSEM_WAIT_TIMEOUT;
    waiter.handoff_set = false;

    /* ③ wait_lock 획득 후 대기 큐에 삽입 */
    raw_spin_lock_irq(&sem->wait_lock);

    /* 대기 큐 맨 앞이 Reader이고 Writer 없으면 즉시 획득 시도 */
    if (list_empty(&sem->wait_list)) {
        /* 첫 번째 대기자 → WAITERS 플래그 설정 */
        adjustment = RWSEM_FLAG_WAITERS | RWSEM_READER_BIAS;
        count = atomic_long_add_return(adjustment, &sem->count);

        if (!(count & RWSEM_LOCK_MASK)) {
            /* Writer 없음 → 즉시 깨움 */
            raw_spin_unlock_irq(&sem->wait_lock);
            return sem;
        }
    } else {
        adjustment = RWSEM_READER_BIAS;
        atomic_long_add(adjustment, &sem->count);
    }

    /* ④ 대기 큐 삽입 (FIFO 순서) */
    list_add_tail(&waiter.list, &sem->wait_list);

    /* ⑤ 슬립 루프 */
    for (;;) {
        set_current_state(state);
        if (!smp_load_acquire(&waiter.task))
            break;  /* 깨움 받음 */
        raw_spin_unlock_irq(&sem->wait_lock);
        schedule();     /* 슬립 → 컨텍스트 스위치 */
        raw_spin_lock_irq(&sem->wait_lock);
    }

    __set_current_state(TASK_RUNNING);
    raw_spin_unlock_irq(&sem->wait_lock);
    return sem;
}

rwsem_down_write_slowpath 분석

/* kernel/locking/rwsem.c — 단순화된 down_write slowpath */
static struct rw_semaphore *
rwsem_down_write_slowpath(struct rw_semaphore *sem,
                           unsigned int state)
{
    struct rwsem_waiter waiter;

    /* ① Optimistic Spinning 시도
     *    osq_lock으로 MCS 큐에 진입하여 스피닝
     *    owner가 CPU에서 실행 중이면 스피닝 계속
     *    owner가 슬립하면 스피닝 중단 → 대기 큐로 */
    if (rwsem_can_spin_on_owner(sem) &&
        rwsem_optimistic_spin(sem, NULL))
        return sem;  /* spinning 중 획득 성공 */

    /* ② waiter 준비 */
    waiter.task = current;
    waiter.type = RWSEM_WAITING_FOR_WRITE;
    waiter.timeout = jiffies + RWSEM_WAIT_TIMEOUT;
    waiter.handoff_set = false;

    raw_spin_lock_irq(&sem->wait_lock);
    list_add_tail(&waiter.list, &sem->wait_list);

    /* WAITERS 플래그 설정 */
    rwsem_set_waiters(sem);

    /* ③ 슬립 루프 */
    for (;;) {
        if (rwsem_try_write_lock(sem, &waiter))
            break;  /* 락 획득 성공 */

        raw_spin_unlock_irq(&sem->wait_lock);

        /* ④ HANDOFF 설정 조건 확인
         *    대기 시간이 RWSEM_WAIT_TIMEOUT을 초과하면
         *    HANDOFF 플래그 설정 → optimistic spinner 차단
         *    → 첫 번째 대기자에게 직접 전달 */
        if (time_after(jiffies, waiter.timeout)) {
            if (!waiter.handoff_set) {
                atomic_long_or(RWSEM_FLAG_HANDOFF,
                    &sem->count);
                waiter.handoff_set = true;
            }
        }

        set_current_state(state);
        schedule();

        raw_spin_lock_irq(&sem->wait_lock);
    }

    __set_current_state(TASK_RUNNING);
    list_del(&waiter.list);
    raw_spin_unlock_irq(&sem->wait_lock);
    return sem;
}
rwsem down_write_slowpath 상태 머신 down_write(sem) — fast path 실패 Optimistic Spinning (osq_lock + spin_on_owner) owner가 CPU에서 실행 중이면 계속 스피닝 획득 성공! owner 슬립 또는 실패 wait_list에 삽입 + WAITERS 플래그 설정 schedule() — 슬립 깨어나면 rwsem_try_write_lock() 시도 획득 성공! 실패 대기 시간 > RWSEM_WAIT_TIMEOUT ? HANDOFF 플래그 설정 → spinner 차단 재시도 HANDOFF 활성 시: optimistic spinner가 try_write_lock_unqueued() 실패 → 대기 큐 첫 번째 Writer에게 전달 RWSEM_WAIT_TIMEOUT = 4 jiffies (기본 ~4ms) — 이 시간 내에 획득하지 못하면 HANDOFF 설정
Writer slowpath: Optimistic Spinning 시도 → 실패 시 대기 큐 → HANDOFF 메커니즘으로 starvation 방지

Optimistic Spinning 조건 판단

/* kernel/locking/rwsem.c */
static bool rwsem_can_spin_on_owner(struct rw_semaphore *sem)
{
    struct task_struct *owner;
    unsigned long flags;
    bool ret = true;

    /* need_resched() → 스피닝하면 안 됨 */
    if (need_resched())
        return false;

    owner = rwsem_owner_flags(sem, &flags);

    /* NONSPINNABLE 플래그 설정됨 → 스피닝 금지 */
    if (flags & RWSEM_NONSPINNABLE)
        return false;

    /* READER_OWNED → Reader가 보유 중
     *   Reader는 스케줄 아웃될 수 있으므로
     *   스피닝이 비효율적 → false */
    if (flags & RWSEM_READER_OWNED)
        return false;

    /* owner가 있고 CPU에서 실행 중 → 스피닝 가치 있음 */
    if (owner && !owner_on_cpu(owner))
        ret = false;

    return ret;
}
NONSPINNABLE 전파: Reader가 rwsem을 보유한 채 스케줄 아웃되면 RWSEM_NONSPINNABLE 플래그가 설정됩니다. 이 플래그는 잠금이 해제될 때까지 유지되며, 이후 모든 optimistic spinner가 즉시 대기 큐로 이동합니다. 이는 Reader가 장기간 보유하는 패턴(예: 메모리 매핑 작업 중 페이지 폴트(Page Fault))에서 불필요한 스피닝 낭비를 방지합니다.

x86 아키텍처 구현

x86에서 qrwlock과 rwsem의 atomic 연산은 LOCK 접두사 명령어로 구현됩니다. LOCK은 버스(Bus) 잠금이 아니라 캐시 일관성 프로토콜(MESI/MESIF)을 통해 해당 캐시라인의 배타적 소유권을 보장합니다.

LOCK 접두사와 캐시 프로토콜

x86에서 qrwlock 핵심 연산의 어셈블리(Assembly)입니다.

atomic_add_return_acquire(_QR_BIAS, &cnts)LOCK XADD [cnts], $0x200

LOCK 접두사는 다음 역할을 합니다.

  1. 캐시라인을 Exclusive 상태로 전환합니다(MESI).
  2. read-modify-write를 원자적으로 수행합니다.
  3. 다른 코어의 읽기/쓰기를 차단하지 않습니다(버스 잠금이 아닌 캐시 잠금).
  4. Full memory barrier 효과를 가집니다(x86 TSO에서).

XADD(Exchange and Add)는 기존 값을 반환하면서 새 값을 저장하며, cmpxchg보다 효율적입니다(단일 명령어).

atomic_cmpxchg_acquire(cnts, 0, _QW_LOCKED)

mov eax, 0          ; expected = 0
mov ecx, 0xff       ; desired = _QW_LOCKED
LOCK CMPXCHG [cnts], ecx
; ZF=1이면 성공 (cnts에 0xff 저장)
; ZF=0이면 실패 (eax에 현재 cnts 값)

smp_store_release(&lock->wlocked, 1) — x86 TSO(Total Store Ordering)에서는 Store-Store 재배치가 원래 불가하므로 일반 MOV로 충분합니다(배리어 불필요). MOV BYTE [lock+0], 1로 컴파일됩니다. ARM64에서는 STLR(Store-Release)이 필요합니다.

PAUSE 명령어와 스핀 루프

/*
 * x86 cpu_relax() = PAUSE (rep; nop)
 *
 * PAUSE의 효과:
 * 1. 파이프라인 비움 (~140 사이클 지연)
 *    → 스핀 루프에서 명령어 낭비 감소
 * 2. 메모리 순서 위반(Memory Order Violation) 방지
 *    → speculative execution이 lock 변수를 과도하게 읽는 것 방지
 *    → MOB(Memory Order Buffer) flush 비용 감소
 * 3. Hyper-Threading에서 다른 논리 코어에 자원 양보
 *    → 스핀 중인 스레드가 파이프라인 자원을 독점하지 않음
 * 4. 전력 소비 감소 (P-state 전환 힌트)
 *
 * TPAUSE/UMWAIT (신규 프로세서):
 * → 지정된 시간까지 CPU를 C0.1/C0.2 대기 상태로 전환
 * → PAUSE보다 적극적인 전력 절약
 * → 현재 upstream qrwlock 구현에서는 아직 미사용
 */

/* x86 atomic_cond_read_acquire 구현 */
#define smp_cond_load_acquire(ptr, cond_expr) ({
    typeof(*ptr) VAL;
    for (;;) {
        VAL = READ_ONCE(*ptr);
        if (cond_expr)
            break;
        cpu_relax();    /* PAUSE */
    }
    VAL;
})

캐시라인 경합 패턴

qrwlock의 x86 캐시라인 경합 분석입니다. struct qrwlock은 8바이트(cnts 4B + wait_lock 4B)로 하나의 캐시라인(64B)에 완전히 수용됩니다.

Reader fast path에서 atomic XADD 시 다음과 같이 캐시라인이 이동합니다.

  1. CPU0: LOCK XADD → 캐시라인 Exclusive 획득
  2. CPU1: LOCK XADD → CPU0으로부터 캐시라인 전송 (RFO: Read For Ownership)
  3. CPU2: LOCK XADD → CPU1으로부터 캐시라인 전송

N개 CPU에서 O(N) RFO 트래픽이 발생하며, 각 RFO는 ~50-100ns(NUMA 교차 시 ~200ns+)가 소요됩니다. 이것이 percpu_rw_semaphore가 필요한 근본적 이유입니다. percpu 방식은 this_cpu_inc로 로컬 캐시라인만 수정하므로 O(1)입니다.

perf c2c로 확인할 수 있습니다.

perf c2c record -e mem-loads,mem-stores -p $PID -- sleep 5
perf c2c report --stdio
→ HITM(Hit Modified) 카운트가 높으면 true sharing 경합
x86 TSO의 이점: x86의 TSO(Total Store Ordering) 메모리 모델 덕분에 rwlock 구현에서 별도의 메모리 배리어(Memory Barrier)가 거의 필요 없습니다. Store-Store, Load-Load 재배치가 불가하므로, smp_store_release()는 단순 MOV 명령어로 컴파일됩니다. 이는 ARM64/RISC-V에서 명시적 배리어가 필요한 것과 대조됩니다.

ARM64 아키텍처 구현

ARM64는 약한 메모리 순서(Weakly Ordered) 아키텍처이므로, rwlock 구현에서 명시적 배리어와 exclusive 모니터 기반의 원자 연산이 필수적입니다. x86과 달리 모든 acquire/release 의미론을 명시적으로 인코딩해야 합니다.

Exclusive Monitor와 LDAXR/STLXR

ARM64에서 atomic_add_return_acquire(_QR_BIAS, &cnts)의 어셈블리 시퀀스는 다음과 같습니다.

  prfm  pstl1strm, [x0]     ; prefetch for store (선택적)
1:
  ldaxr w1, [x0]            ; Load-Acquire Exclusive Register
                             ; → exclusive monitor 설정
                             ; → acquire 의미론 포함
  add   w2, w1, #0x200      ; w2 = w1 + _QR_BIAS
  stxr  w3, w2, [x0]        ; Store Exclusive Register
                             ; → exclusive monitor 확인
                             ; → 성공: w3=0, 실패: w3=1
  cbnz  w3, 1b              ; 실패 시 재시도
  ; w1에 원래 cnts 값 (반환값)

Exclusive Monitor 동작: LDAXR은 해당 캐시라인에 exclusive 표시를 설정하며, 다른 코어가 같은 캐시라인을 수정하면 exclusive 표시가 클리어되어 STXR이 실패합니다. Local monitor(CPU별)와 Global monitor(버스 레벨)로 구성됩니다.

LDAXR vs LDXR: LDAXR은 Load + Acquire + Exclusive이고, LDXR은 Load + Exclusive(acquire 없음)입니다. rwlock은 acquire 의미론이 필요하므로 LDAXR을 사용합니다.

smp_store_release(&lock->wlocked, 1)은 ARM64에서 STLRB w1, [x0](Store-Release Byte)으로 구현됩니다. Store + Release 의미론으로 이전의 모든 메모리 접근이 이 스토어 이전에 완료되어, 임계 영역(Critical Section)의 데이터가 lock 해제 이전에 가시적임을 보장합니다. x86에서는 TSO 보장으로 단순 MOV로 충분합니다.

WFE/SEV 메커니즘

/*
 * ARM64에서 atomic_cond_read_acquire의 구현:
 *
 * WFE (Wait For Event):
 *   1. Event Register가 설정되어 있으면 즉시 반환 (플래그 클리어)
 *   2. 설정되어 있지 않으면 CPU를 저전력 대기 상태로 전환
 *   3. 다음 조건에서 깨어남:
 *      a. SEV/SEVL 명령어 (Send Event)
 *      b. 인터럽트 (IRQ/FIQ/SError)
 *      c. exclusive monitor 클리어 (다른 코어가 캐시라인 수정)
 *      d. 디버그 이벤트
 *
 * → x86 PAUSE보다 전력 효율적: CPU가 실제로 대기 상태
 * → 깨어나는 지연은 PAUSE(~140 사이클)보다 길 수 있음
 *    (마이크로아키텍처에 따라 ~1us 수준)
 *
 * SEV (Send Event):
 *   - 모든 코어의 Event Register를 설정
 *   - Writer unlock 시 호출 → 대기 중인 Reader WFE 해제
 *   - 커널에서 직접 호출하지 않음; STLR이 암묵적으로
 *     exclusive monitor 클리어 → WFE 자동 해제
 */

/* ARM64 qrwlock spin 루프 의사 코드 */
static inline void arm64_cond_read(atomic_t *v, int cond_mask)
{
    int val;
    for (;;) {
        val = __atomic_load_n(&v->counter, __ATOMIC_RELAXED);
        if (!(val & cond_mask))
            break;
        __wfe();   /* Wait For Event — CPU 저전력 대기 */
    }
    __dmb(ishld);  /* acquire 배리어: Inner Shareable, Load */
}
ARM64 Exclusive Monitor + WFE 기반 rwlock 스핀 CPU 0 (Writer) wlocked = 1 임계 영역 실행 중... STLRB wzr, [wlocked] CPU 1 (Reader) LDAXR cnts → Writer 존재 WFE — 저전력 대기 exclusive monitor 감시 중 CPU 2 (Reader) WFE — 대기 중 STLRB wzr → wlocked = 0 exclusive monitor 클리어 CPU 1: WFE 해제 LDAXR cnts → Writer 없음 STXR → Reader 진입! CPU 2: WFE 해제 LDAXR/STXR Reader 진입! WFE → exclusive monitor 클리어로 깨어남 → LDAXR/STXR로 원자적 Reader 등록 → DMB ishld (acquire) x86 PAUSE 루프보다 전력 효율적이지만, WFE wake-up 지연이 있을 수 있음
ARM64: Writer STLRB 해제 → exclusive monitor 클리어 → WFE 대기 중인 Reader 깨어남

DMB 배리어 종류

ARM64의 메모리 배리어 명령어 중 rwlock 관련 항목은 다음과 같습니다.

DMB (Data Memory Barrier)
DMB ISH(Inner Shareable, Full barrier), DMB ISHLD(Inner Shareable, Load-Load + Load-Store), DMB ISHST(Inner Shareable, Store-Store)가 있습니다.
DSB (Data Synchronization Barrier)
DMB보다 강력하여 명령어 실행 자체를 차단합니다. rwlock에서는 일반적으로 불필요합니다.

rwlock에서의 배리어 배치는 다음과 같습니다.

read_lock:  LDAXR → STXR → (acquire 내장)
            ┌─ 여기서부터 임계 영역 ─┐
read_unlock: atomic_sub(_QR_BIAS)  (release)
            └─ 여기까지 임계 영역 ─┘

write_lock: LDAXR/STLXR 반복 (acquire)
            ┌─ 임계 영역 ─┐
write_unlock: STLRB wzr   (release)
            └─ 임계 영역 ─┘

주요 명령어: LDAXR(Load-Acquire Exclusive — acquire + exclusive), STLXR(Store-Release Exclusive — release + exclusive), STLRB(Store-Release Byte — release, 바이트 단위).

LSE Atomics (ARMv8.1): Large System Extensions를 지원하는 프로세서에서 LDAXR/STXR 루프 대신 LDADD(atomic add), CAS(compare-and-swap), SWP(swap) 단일 명령어를 사용할 수 있습니다. qrwlock의 atomic_add_return_acquireLDADD로 컴파일되면 재시도 루프 없이 단일 명령어로 완료됩니다. 이는 높은 경합 상황에서 성능을 크게 개선합니다.

RISC-V 아키텍처 구현

RISC-V는 원자 연산을 위해 LR/SC(Load-Reserved/Store-Conditional)AMO(Atomic Memory Operation) 두 가지 메커니즘을 제공합니다. qrwlock 구현에서 두 메커니즘이 어떻게 사용되는지 분석합니다.

LR/SC 기반 원자 연산

RISC-V에서 atomic_add_return_acquire(_QR_BIAS, &cnts)는 두 가지 방법으로 구현할 수 있습니다.

방법 1: AMO 명령어 사용
  amoadd.w.aq a0, a1, (a2)   ; atomic add with acquire
  ; a0 = 원래 값 (반환), [a2] += a1
  ; .aq = acquire ordering (이후 메모리 접근 재배치 금지)
  ; .rl = release ordering (.aqrl이면 둘 다)

방법 2: LR/SC 사용
1: lr.w.aq a0, (a2)          ; Load-Reserved, acquire
   add     a1, a0, a3        ; a1 = a0 + _QR_BIAS
   sc.w    a4, a1, (a2)      ; Store-Conditional
   bnez    a4, 1b            ; 실패 시 재시도
   ; a0에 원래 cnts 값

LR/SC와 AMO의 차이점은 다음과 같습니다.

AMO
단일 명령어로 간단한 연산(add/and/or/xor/swap)을 수행합니다. qrwlock fast path에서 amoadd.w.aq를 사용하며 더 효율적입니다.
LR/SC
복잡한 연산(cmpxchg 등)이 가능하고 ABA 문제가 없습니다. qrwlock slow path에서 cmpxchg가 필요할 때 사용합니다.

LR/SC 예약 세트(Reservation Set): LR이 예약하는 메모리 범위는 구현 정의이며 최소 1 워드입니다. 다른 코어가 예약 범위 내 메모리를 수정하면 SC가 실패합니다. ARM64 exclusive monitor와 유사하지만, RISC-V는 forward progress 보장이 더 약합니다. LR과 SC 사이에 다른 메모리 접근을 넣지 말아야 하며, 스펙에서는 LR과 SC 사이 최대 16개 명령어를 권장합니다.

fence 명령어와 메모리 순서

RISC-V의 메모리 순서 명령어는 다음과 같습니다.

fence rw, rw     ; Full barrier (iorw 포함 시 fence iorw, iorw)
fence r, r       ; Load-Load barrier
fence w, w       ; Store-Store barrier
fence r, rw      ; acquire barrier 역할
fence rw, w      ; release barrier 역할

RISC-V는 ARM64보다 더 약한 기본 메모리 모델(RVWMO)을 사용합니다. Load-Load, Load-Store, Store-Store, Store-Load 재배치가 모두 가능하므로(x86은 Store-Load만 허용) 배리어가 필수적입니다.

qrwlock에서의 배리어 사용은 다음과 같습니다.

read_lock
amoadd.w.aq.aq가 acquire fence를 포함합니다.
read_unlock
amoadd.w.rl.rl이 release fence를 포함합니다. 또는 fence rw, w + amoadd.w를 사용합니다.
write_lock
lr.w.aq / sc.w — acquire 의미론. 또는 amocas.w.aq(Zacas 확장)를 사용합니다.
write_unlock
fence rw, w(release 배리어) + sb zero, (wlocked)으로 wlocked를 0으로 설정합니다.

RISC-V의 WFE 부재: RISC-V에는 ARM64의 WFE에 해당하는 표준 명령어가 없으므로 cpu_relax()가 순수 spin 루프(NOP 또는 PAUSE 힌트)입니다. Zawrs 확장이 있으면 WRS.STO/WRS.NTO를 사용할 수 있으며(wrs.sto rs1: 예약 세트가 변경될 때까지 대기, WFE와 유사), 현재 공개 구현 다수에서는 Zawrs 지원이 아직 제한적입니다.

RISC-V qrwlock 전체 시퀀스

/* RISC-V queued_read_lock 어셈블리 의사 코드 */
queued_read_lock:
    /* ① atomic_add_return_acquire(_QR_BIAS, &cnts) */
    li      a1, 0x200           /* a1 = _QR_BIAS */
    amoadd.w.aq a0, a1, (lock)   /* a0 = old cnts, [lock] += 0x200 */
    add     a0, a0, a1           /* a0 = new cnts */

    /* ② Writer 확인 */
    andi    a2, a0, 0x1ff       /* a2 = cnts & _QW_WMASK */
    bnez    a2, .Lslow           /* Writer 존재 → slow path */
    ret                          /* fast path 성공 */

.Lslow:
    /* slow path: queued_read_lock_slowpath */
    li      a1, -0x200          /* a1 = -_QR_BIAS */
    amoadd.w a0, a1, (lock)      /* reader count 복원 */
    /* ... wait_lock 획득 후 spin ... */

/* queued_read_unlock */
queued_read_unlock:
    li      a1, -0x200
    amoadd.w.rl a0, a1, (lock)   /* release: reader count 감소 */
    ret
Zacas 확장: RISC-V Zacas(Atomic Compare-and-Swap) 확장은 amocas.w/amocas.d 명령어를 추가합니다. 이를 사용하면 qrwlock의 Writer fast path(atomic_cmpxchg_acquire)가 LR/SC 루프 대신 단일 amocas.w.aq 명령어로 컴파일됩니다. 커널 v6.7부터 Zacas를 감지하여 자동으로 활용합니다.

percpu_rw_semaphore 내부 심층

percpu_rw_semaphore는 Reader fast path에서 전역 atomic 연산을 완전히 제거하여 확장성을 극대화합니다. 이 섹션에서는 fast path의 정확한 구현, RCU 동기화, slow path의 복잡한 상호작용을 소스 레벨에서 분석합니다.

Reader Fast Path: __percpu_down_read 내부

/* include/linux/percpu-rwsem.h */
static inline void percpu_down_read(struct percpu_rw_semaphore *sem)
{
    might_sleep();

    preempt_disable();
    /*
     * rcu_sync_is_idle() 확인:
     *   true  → Writer가 없음 (정상 상태)
     *          → this_cpu_inc()만으로 Reader 등록!
     *          → 전역 atomic 없음, 캐시 경합 없음
     *          → ~2-5ns (로컬 캐시 라인만 접근)
     *
     *   false → Writer가 활동 중 (rcu_sync_enter 호출됨)
     *          → __percpu_down_read() slow path 진입
     */
    if (likely(rcu_sync_is_idle(&sem->rss)))
        this_cpu_inc(*sem->read_count);
    else
        __percpu_down_read(sem, false);
    preempt_enable();
}

/* kernel/locking/percpu-rwsem.c — slow path */
bool __percpu_down_read(struct percpu_rw_semaphore *sem,
                         bool try)
{
    /*
     * Writer가 활동 중일 때의 Reader 경로:
     * ① per-cpu 카운터 먼저 증가
     * ② atomic_read(&sem->block) 확인
     *    → 0이면 Writer가 아직 Reader 차단 안 함 → 성공
     *    → 1이면 Reader 차단 중 → 카운터 복원 후 대기
     */
    this_cpu_inc(*sem->read_count);
    smp_mb();  /* read_count inc과 block 읽기 순서 보장 */

    if (likely(!atomic_read(&sem->block)))
        return true;

    /* Writer가 차단 중 → 카운터 복원 */
    this_cpu_dec(*sem->read_count);

    /* try 모드면 실패 반환 */
    if (try)
        return false;

    /* ③ Writer 완료까지 대기 큐에서 슬립
     *    Writer가 percpu_up_write() 호출하면 깨움 */
    wait_event(sem->waiters,
        !atomic_read(&sem->block));

    /* ④ Writer 완료 → 다시 per-cpu 카운터 증가 */
    this_cpu_inc(*sem->read_count);
    return true;
}

Writer 경로 상세: synchronize_rcu의 역할

/* kernel/locking/percpu-rwsem.c */
void percpu_down_write(struct percpu_rw_semaphore *sem)
{
    might_sleep();

    /* ① rcu_sync_enter: RCU sync 상태 전환
     *    rcu_sync_is_idle()가 false를 반환하게 만듦
     *    → 이후 Reader의 fast path에서 slow path로 전환
     *    → rcu_sync는 내부적으로 call_rcu/synchronize_rcu 사용
     *    → 첫 번째 Writer가 이미 enter 했으면 빠르게 반환 */
    rcu_sync_enter(&sem->rss);

    /* ② 내부 rwsem Writer 획득
     *    여러 Writer 간 직렬화 */
    down_write(&sem->rw_sem);

    /* ③ block = 1 → 새 Reader의 slow path에서도 차단
     *    smp_mb() 이후 block 설정 → 순서 보장 */
    atomic_set(&sem->block, 1);
    smp_mb();  /* block 설정이 per-cpu 합산 이전에 가시적 */

    /* ④ synchronize_rcu()
     *    핵심: 현재 preempt_disable() 섹션에 있는 Reader들이
     *    모두 preempt_enable()을 호출할 때까지 대기
     *
     *    왜 필요한가?
     *    Reader fast path: preempt_disable → this_cpu_inc → preempt_enable
     *    synchronize_rcu()는 모든 CPU가 quiescent state를 지남을 보장
     *    → preempt_disable 구간에 있던 Reader는 반드시 완료됨
     *    → 이후 per-cpu 합산 결과가 정확함을 보장 */
    synchronize_rcu();

    /* ⑤ 모든 per-cpu 카운터 합산하여 0이 될 때까지 대기
     *    readers_active_check: sum of all per-cpu read_count == 0
     *    → 슬로우 패스에서 진입한 Reader도 모두 나갈 때까지 */
    rcuwait_wait_event(&sem->writer,
        readers_active_check(sem));
}

/* percpu_up_write — Writer 해제 */
void percpu_up_write(struct percpu_rw_semaphore *sem)
{
    /* ① block = 0 → 새 Reader 허용 */
    atomic_set(&sem->block, 0);

    /* ② 대기 중인 Reader 깨움 */
    wake_up_all(&sem->waiters);

    /* ③ 내부 rwsem 해제 */
    up_write(&sem->rw_sem);

    /* ④ rcu_sync_exit: 충분한 grace period 후
     *    rcu_sync_is_idle()가 다시 true 반환
     *    → Reader fast path 복구 */
    rcu_sync_exit(&sem->rss);
}
percpu_rw_semaphore 시간축 동작 t idle rcu_sync_enter block=1 synchronize_rcu drain unlock R-A fast path (this_cpu_inc) R-B slow path (block=0 → OK) R-C 차단! 진입 Writer rcu_sync_enter block + sync_rcu wait readers 임계영역 unlock rss idle (fast path 유효) active (slow path 강제) 비용 비교 Reader fast path: ~2-5ns Writer: synchronize_rcu ~ms + percpu 합산 synchronize_rcu()는 preempt_disable 구간의 Reader 완료를 보장 → per-cpu 합산 정확성 보장
시간축으로 본 percpu_rw_semaphore: rcu_sync_enter부터 모든 reader drain까지의 과정

readers_active_check 구현

/* kernel/locking/percpu-rwsem.c */
static bool readers_active_check(struct percpu_rw_semaphore *sem)
{
    /*
     * 모든 CPU의 per-cpu read_count를 합산
     * → 0이면 모든 Reader가 나간 것
     *
     * 주의: 이 합산은 반드시 synchronize_rcu() 이후에 수행해야 함
     * 그래야 preempt_disable 구간에 있던 Reader가
     * this_cpu_inc를 완료한 상태를 보장
     *
     * percpu_counter_sum()은 smp_call_function보다 저렴하지만
     * 여전히 NR_CPUS 개의 캐시라인을 읽어야 함
     * → 128-CPU 시스템에서 ~10us 수준
     */
    if (per_cpu_sum(*sem->read_count) != 0)
        return false;
    return true;
}

/* per_cpu_sum 의사 구현 */
static inline long per_cpu_sum(unsigned int __percpu var)
{
    long sum = 0;
    int cpu;

    for_each_possible_cpu(cpu)
        sum += per_cpu(var, cpu);

    return sum;
}
주의: preempt_disable 범위: percpu_down_read()preempt_disable() 구간 내에서 this_cpu_inc()를 수행하고 즉시 preempt_enable()을 호출합니다. 따라서 Reader의 임계 영역 전체가 preempt_disable로 보호되는 것이 아닙니다. Reader는 임계 영역에서 슬립할 수 있으며, 심지어 다른 CPU로 마이그레이션될 수도 있습니다. percpu_up_read()는 호출 시점의 CPU에서 카운터를 감소시키므로, 마이그레이션되면 다른 CPU의 카운터가 감소합니다 — 이것이 per_cpu_sum이 필요한 이유입니다.

벤치마크: reader/writer 비율별 성능

Reader-Writer Lock의 성능은 읽기/쓰기 비율에 극도로 민감합니다. 이 섹션에서는 다양한 프리미티브를 Reader/Writer 비율, CPU 수, 임계 영역 크기에 따라 비교합니다.

벤치마크 방법론

커널 모듈(Kernel Module) 기반 벤치마크(locktorture 변형)의 테스트 환경은 다음과 같습니다.

테스트 환경
  • CPU: Intel Xeon Platinum 8380 (2S, 80C/160T)
  • 메모리: DDR4-3200 512GB (NUMA 2 노드)
  • 커널: v6.8 defconfig + CONFIG_LOCK_STAT=y
  • 각 테스트: 10초간 반복, ops/sec 측정
  • 임계 영역: 공유 카운터 1회 읽기/증가 (~10ns)
locktorture 파라미터
modprobe locktorture torture_type=rwsem_lock \
    nreaders_stress=79 nwriters_stress=1 \
    stat_interval=1 verbose=1

torture_type 옵션: rwsem_lock(rw_semaphore), rw_lock_lock(rwlock_t), percpu_rwsem(percpu_rw_semaphore), mutex_lock(mutex, 비교 기준)

Reader/Writer 비율별 처리량

비율 (R:W)mutexrwlock_trw_semaphorepercpu_rwsemRCU
50:5085M ops/s42M ops/s65M ops/s0.8M ops/sN/A
90:1080M ops/s68M ops/s95M ops/s12M ops/s~400M ops/s
99:178M ops/s120M ops/s180M ops/s150M ops/s~400M ops/s
99.9:0.178M ops/s150M ops/s200M ops/s380M ops/s~400M ops/s
측정 조건: 80 CPU 코어, 임계 영역 ~10ns, NUMA 2-소켓(Socket) 시스템. 실제 성능은 하드웨어, NUMA 토폴로지(Topology), 임계 영역 크기에 따라 크게 달라집니다. 위 수치는 상대적 경향을 보여주기 위한 대략적 값입니다.
Reader/Writer 비율별 처리량 비교 (80 CPU) 0 100M 200M 300M 400M ops/sec 50:50 90:10 99:1 99.9:0.1 Reader:Writer 비율 mutex rwlock_t rwsem percpu_rwsem
Reader 비율이 높아질수록 RW lock의 이점 증가; percpu_rwsem은 99.9:0.1에서 최고 성능

CPU 수별 확장성

CPU 수rwlock_t Readerrwsem Readerpercpu_rwsem Reader
1~15ns~20ns~5ns
4~25ns~30ns~5ns
16~80ns~60ns~5ns
32~200ns~120ns~5ns
64~500ns~250ns~5ns
128~1.2us~500ns~5ns
핵심 관찰: rwlock_trwsem의 Reader 비용은 CPU 수에 비례하여 증가합니다 — 모든 Reader가 동일 캐시라인에 atomic 연산을 수행하기 때문입니다. 반면 percpu_rwsem은 CPU 수에 관계없이 일정합니다. 64+ CPU 시스템에서 읽기 빈번한 워크로드라면 percpu_rwsem이 필수적입니다.

임계 영역 크기의 영향

임계 영역 크기별 성능 영향 (64 CPU, R:W = 99:1):

임계 영역rwlock_trwsempercpu_rwsem
~10ns120M ops180M ops380M ops
~100ns95M ops150M ops370M ops
~1us40M ops80M ops350M ops
~10us12M ops25M ops300M ops
~100us1.5M ops3M ops50M ops

관찰 결과는 다음과 같습니다.

  1. 임계 영역이 짧을수록 lock 오버헤드 비중이 높아 percpu_rwsem의 이점이 극대화됩니다.
  2. 임계 영역이 길면(~100us) 모든 프리미티브의 성능이 하락합니다. lock 오버헤드보다 임계 영역 자체가 병목이 됩니다.
  3. rwsem이 rwlock_t보다 나은 이유는 optimistic spinning이 Writer 전환 비용을 감소시키고, 경합 시 슬립으로 CPU 낭비를 방지하기 때문입니다.

메모리 순서와 배리어 배치

Reader-Writer Lock의 정확성은 메모리 순서 보장(Ordering)에 의존합니다. 이 섹션에서는 각 연산의 acquire/release 의미론과 아키텍처별 배리어 배치를 분석합니다.

Acquire/Release 의미론

rwlock의 메모리 순서 계약:

┌─────────────────────────────────────────────────┐
│ read_lock()/write_lock()  →  ACQUIRE            │
│   이후의 메모리 접근이 이전으로 재배치 불가       │
│                                                   │
│ read_unlock()/write_unlock() →  RELEASE           │
│   이전의 메모리 접근이 이후로 재배치 불가          │
│                                                   │
│ 결합 효과:                                        │
│   CPU A: write_lock → [store X] → write_unlock   │
│   CPU B: read_lock  → [load X]  → read_unlock    │
│   → CPU B는 반드시 CPU A의 X 갱신을 관찰         │
└─────────────────────────────────────────────────┘

이 계약이 보장하는 것은 다음과 같습니다.

  1. 임계 영역 내의 접근이 임계 영역 밖으로 유출되지 않습니다.
  2. Writer의 수정이 이후 Reader에게 가시적입니다.
  3. 여러 Reader의 읽기 결과가 일관적입니다(동일 시점의 데이터).

qrwlock의 배리어 배치

queued_read_lock
atomic_add_return_acquire(_QR_BIAS, &cnts) — ACQUIRE 의미론. x86에서는 LOCK XADD(암묵적 full barrier), ARM64에서는 LDAXR + STXR(LDAXR이 acquire), RISC-V에서는 amoadd.w.aq(.aq가 acquire)로 구현됩니다.
queued_read_unlock
atomic_sub_return_release(_QR_BIAS, &cnts) — RELEASE 의미론. x86에서는 LOCK XADD(암묵적 full barrier), ARM64에서는 LDXR + STLXR(STLXR이 release), RISC-V에서는 amoadd.w.rl(.rl이 release)로 구현됩니다.
queued_write_lock
atomic_cmpxchg_acquire(&cnts, 0, _QW_LOCKED) — ACQUIRE 의미론입니다.
queued_write_unlock
smp_store_release(&lock->wlocked, 0) — RELEASE 의미론. x86에서는 MOV(TSO가 release 보장), ARM64에서는 STLRB(Store-Release Byte), RISC-V에서는 fence rw,w + sb로 구현됩니다.
rwlock Acquire/Release 메모리 순서 CPU A (Writer) store X = 42 (임계 영역 밖) write_lock(&rwlock) ← ACQUIRE ↓ 배리어: 이하 접근이 위로 재배치 불가 임계 영역 store shared_data = 100 ↑ 배리어: 이상 접근이 아래로 재배치 불가 write_unlock(&rwlock) ← RELEASE store Y = 99 (임계 영역 밖) CPU B (Reader) read_lock(&rwlock) ← ACQUIRE 임계 영역 load shared_data → 100 보장 read_unlock(&rwlock) ← RELEASE happens-before Writer의 RELEASE → Reader의 ACQUIRE 순서로 happens-before 관계가 성립 → shared_data 가시성 보장
Writer의 write_unlock(RELEASE)과 Reader의 read_lock(ACQUIRE)이 happens-before 관계를 형성

rwsem의 배리어 배치

rwsem의 메모리 순서는 qrwlock보다 복잡합니다.

down_read (fast path)
atomic_long_fetch_add_acquire(RWSEM_READER_BIAS, &count) — ACQUIRE 의미론
up_read (fast path)
대기자가 없으면 atomic_long_add_return_release(-RWSEM_READER_BIAS, &count)(RELEASE)을 수행합니다. 대기자가 있으면 rwsem_wake()wake_up_process()를 호출하며, 깨우는 쪽에서 smp_store_release를 수행합니다.
down_write (fast path)
atomic_long_cmpxchg_acquire(&count, 0, RWSEM_WRITER_LOCKED) — ACQUIRE 의미론
up_write
atomic_long_add_return_release(-RWSEM_WRITER_LOCKED, &count)(RELEASE)을 수행합니다. 대기자가 있으면 rwsem_wake()를 호출합니다.
downgrade_write
count에서 WRITER_LOCKED를 제거하고 READER_BIAS를 추가합니다. 내부적으로 atomic_long_add(-RWSEM_WRITER_LOCKED+RWSEM_READER_BIAS)를 수행하며, 대기 중인 Reader를 깨웁니다(RELEASE 의미론 포함).

중요한 배리어 주의점은 다음과 같습니다.

  1. Owner 확인 시 smp_load_acquire: owner를 읽어 optimistic spinning 결정 시 smp_load_acquire로 읽어야 owner->on_cpu의 최신 값을 보장합니다.
  2. Waiter 깨움 시 smp_store_release: waiter->task = NULL로 설정하여 깨움을 알릴 때, release로 임계 영역의 변경 사항을 가시적으로 만듭니다.
  3. HANDOFF 플래그: atomic_long_or(RWSEM_FLAG_HANDOFF, &count)는 별도 배리어가 불필요합니다(atomic OR이 충분).
smp_rmb/smp_wmb 직접 사용 금지: rwlock/rwsem 코드 내부에서 smp_rmb()/smp_wmb()를 직접 호출하는 것은 위험합니다. 대신 smp_load_acquire()/smp_store_release() 또는 _acquire/_release 접미사가 붙은 atomic 연산을 사용해야 합니다. 이렇게 하면 x86에서는 불필요한 배리어가 생략되고, ARM64/RISC-V에서는 적절한 배리어가 삽입됩니다.

서브시스템 사례: VFS inode->i_rwsem

inode->i_rwsem은 리눅스 커널에서 가장 빈번하게 경합하는 rwsem 중 하나입니다. 파일 읽기/쓰기, 디렉터리 조회, 메타데이터 업데이트 등 거의 모든 VFS 연산이 이 잠금을 거칩니다.

i_rwsem의 역할

/* include/linux/fs.h */
struct inode {
    /* ... */
    struct rw_semaphore     i_rwsem;
    /* ... */
};

/*
 * i_rwsem 보호 대상:
 *
 * ■ 일반 파일:
 *   Reader (down_read):
 *     - read() 시스템 콜 (버퍼드 I/O)
 *     - 파일 크기 조회 (stat)
 *     - mmap 읽기 경로
 *
 *   Writer (down_write):
 *     - write() 시스템 콜 (파일 확장 시)
 *     - truncate/ftruncate
 *     - fallocate
 *     - 권한/타임스탬프 변경 (setattr)
 *
 * ■ 디렉터리:
 *   Reader (down_read):
 *     - 이름 해석 (lookup) — 경로 탐색
 *     - readdir/getdents
 *     - 디렉터리 내 stat
 *
 *   Writer (down_write):
 *     - 파일/디렉터리 생성 (create, mkdir)
 *     - 삭제 (unlink, rmdir)
 *     - 이름 변경 (rename)
 *     - 하드 링크 생성 (link)
 */

VFS 잠금 순서 규칙

VFS i_rwsem의 잠금 순서는 lockdep으로 검증되며 다음 규칙을 따릅니다.

  1. 디렉터리 i_rwsem → 자식 inode i_rwsem: 예를 들어 rename(src_dir, dst_dir)에서는 down_write(src_dir->i_rwsem)down_write(dst_dir->i_rwsem)(src < dst 순서) → down_write(victim->i_rwsem)(삭제될 파일) 순서로 획득합니다.
  2. mmap_locki_rwsem: 페이지 폴트 → 파일 읽기 경로에서 down_read(mm->mmap_lock)(이미 보유) → down_read(inode->i_rwsem) 순서를 따릅니다.
  3. i_rwsem → 페이지(Page) 잠금: write() → 페이지 캐시(Page Cache) 경로에서 down_write(inode->i_rwsem)lock_page(page) 순서를 따릅니다.

lockdep 어노테이션은 다음과 같이 매핑됩니다.

래퍼 함수실제 호출
inode_lock(inode)down_write(&inode->i_rwsem)
inode_unlock(inode)up_write(&inode->i_rwsem)
inode_lock_shared(inode)down_read(&inode->i_rwsem)
inode_unlock_shared(inode)up_read(&inode->i_rwsem)

중첩 잠금 시에는 lockdep class를 사용합니다: I_MUTEX_PARENT(부모 디렉터리), I_MUTEX_CHILD(자식 inode), I_MUTEX_NORMAL(일반 파일), I_MUTEX_XATTR(확장 속성(Extended Attribute)).

VFS i_rwsem 잠금 패턴 일반 파일 연산 read() — down_read write() — down_write stat() — down_read truncate — down_write 일반적 비율: read 90%+ / write <10% 디렉터리 연산 lookup — down_read create — down_write readdir — down_read unlink — down_write lookup 비율 극히 높음 (경로 해석 매 단계) 잠금 순서 (상위 → 하위) mmap_lock (rwsem) parent dir i_rwsem child i_rwsem rename 특수 사례: 두 디렉터리 잠금 rename(old_dir, old_name, new_dir, new_name): lock_rename(old_dir, new_dir) → inode 포인터 비교로 순서 결정 (작은 주소 먼저) → 교착 방지를 위한 일관된 잠금 순서 보장
VFS i_rwsem: 파일/디렉터리 연산별 Reader/Writer 구분과 잠금 순서 규칙

i_rwsem 경합 분석

i_rwsem 경합이 높은 시나리오는 다음과 같습니다.

  1. 다중 스레드 동시 읽기 + 간헐적 쓰기: 예를 들어 웹 서버가 정적 파일을 서빙하면서 로그 로테이션이 발생하는 경우입니다. read()down_read, 로그 로테이션의 truncate는 down_write를 사용하며, Writer가 기다리는 동안 HANDOFF가 발동할 수 있습니다.
  2. 디렉터리 집중 워크로드: /tmp에서 수천 개 파일을 생성/삭제하면, 모든 create/unlink가 동일 디렉터리의 i_rwsem Writer를 획득해야 합니다. lookup(Reader)과 create(Writer)의 경합이 발생합니다.
  3. 메일디르 패턴(Maildir++): 수천 프로세스가 동일 디렉터리에 파일을 생성하면 모든 프로세스가 디렉터리 i_rwsem Writer 대기 상태가 됩니다. 디렉터리 해싱 또는 tmpfile+linkat 패턴으로 해결할 수 있습니다.

성능 측정에는 다음 명령을 사용할 수 있습니다.

echo 1 > /proc/lock_stat
# 워크로드 실행
cat /proc/lock_stat | grep -A3 "i_rwsem"
# contentions, waittime-total, holdtime-avg 확인

perf lock record -p $PID -- sleep 10
perf lock report --sort acquired,contended,avg_wait

VFS 레벨 최적화

i_rwsem 경합을 감소시키기 위한 전략은 다음과 같습니다.

  1. 파일별 읽기 시 i_rwsem 불필요한 경우: Direct I/O 읽기는 v5.19부터 i_rwsem 없이 수행할 수 있습니다(inode_dio_begin/end로 충분). io_uringIORING_OP_READ_FIXED는 고정 버퍼(Buffer) 사용 시 i_rwsem을 회피합니다.
  2. 디렉터리 연산 최적화: 경로 해석 시 RCU-walk 모드를 사용하면 i_rwsem 없이 d_seq seqcount만 확인합니다. 실패 시에만 REF-walk(down_read)로 폴백합니다. 이것이 VFS의 가장 중요한 확장성 최적화입니다.
  3. parallel_rename (실험적): 디렉터리 내 rename을 파일 이름 해시(Hash)로 분할하여 다른 해시 버킷의 rename을 동시에 수행하는 방식입니다. 아직 mainline에 포함되지 않았습니다.

RCU-walk vs REF-walk — 경로 해석 예시로 /home/user/file.txt를 살펴보면 다음과 같습니다.

RCU-walk (fast path)
rcu_read_lock()/d_seq 확인 → homed_seq 확인 → userd_seq 확인 → file.txtrcu_read_unlock(). i_rwsem을 사용하지 않고 seqcount만 확인하며, 대부분의 lookup이 이 경로로 성공합니다.
REF-walk (fallback)
inode_lock_shared(dir)(down_read(&dir->i_rwsem)) → dir->lookup()inode_unlock_shared(dir). rename/unlink 등으로 d_seq가 변경되면 RCU-walk가 실패하여 REF-walk로 전환하며, 이때 i_rwsem을 사용합니다.
실전 팁: /proc/lock_stat에서 i_rwsem의 waittime-total이 높다면, 먼저 Writer 연산의 빈도를 확인하세요. 디렉터리 생성이 많다면 하위 디렉터리로 분산하고, 파일 쓰기가 많다면 Direct I/O 전환을 고려하세요. 대부분의 경우 코드 변경보다 워크로드 구조 변경이 더 효과적입니다.

서브시스템 사례: 네트워크 스택(Network Stack)

네트워크 스택은 rwlock_trw_semaphore를 광범위하게 사용합니다. 네트워크 디바이스(Device) 목록, 소켓 콜백(Callback), 라우팅(Routing) 테이블 등에서 읽기 경로의 성능이 핵심입니다.

dev_base_lock: 네트워크 디바이스 목록

/* net/core/dev.c */
DEFINE_RWLOCK(dev_base_lock);

/*
 * dev_base_lock 보호 대상:
 *   - 네트워크 네임스페이스의 디바이스 리스트 (dev_base_head)
 *   - dev->name, dev->ifindex 등 디바이스 속성
 *
 * Reader 사용처 (매우 빈번):
 *   - dev_get_by_name() — 이름으로 디바이스 찾기
 *   - dev_get_by_index() — 인덱스로 디바이스 찾기
 *   - /proc/net/dev 읽기
 *   - netlink 덤프 (ip link show)
 *
 * Writer 사용처 (드묾):
 *   - register_netdevice() — 디바이스 등록
 *   - unregister_netdevice() — 디바이스 해제
 *   - dev_change_name() — 인터페이스 이름 변경
 *
 * 주의: 최신 커널에서는 RCU로 대부분 대체되었지만,
 * dev_base_lock은 Writer 직렬화와 하위 호환성을 위해 유지됨
 */

/* 디바이스 등록 — Writer 경로 */
int register_netdevice(struct net_device *dev)
{
    /* ... 검증 ... */
    write_lock(&dev_base_lock);
    list_netdevice(dev);        /* 리스트에 추가 */
    write_unlock(&dev_base_lock);
    /* ... 후처리 ... */
}

/* 디바이스 조회 — Reader 경로 (RCU 대안 존재) */
struct net_device *__dev_get_by_name(
    struct net *net, const char *name)
{
    /* ASSERT_RTNL() 또는 dev_base_lock Reader 보유 확인 */
    /* 해시 테이블에서 이름으로 검색 */
    hlist_for_each_entry(dev, head, name_hlist) {
        if (!strncmp(dev->name, name, IFNAMSIZ))
            return dev;
    }
    return NULL;
}

sk_callback_lock: 소켓 콜백 보호

/* include/net/sock.h */
struct sock {
    /* ... */
    rwlock_t  sk_callback_lock;
    /* ... */
};

/*
 * sk_callback_lock은 소켓의 콜백 함수 포인터를 보호합니다:
 *   - sk->sk_data_ready    — 데이터 수신 알림
 *   - sk->sk_write_space   — 쓰기 공간 확보 알림
 *   - sk->sk_state_change  — 상태 변경 알림
 *   - sk->sk_error_report  — 에러 보고
 *
 * Reader (매우 빈번 — 패킷 수신 경로):
 *   read_lock_bh(&sk->sk_callback_lock);
 *   sk->sk_data_ready(sk);    // 콜백 호출
 *   read_unlock_bh(&sk->sk_callback_lock);
 *
 * Writer (드묾 — 콜백 변경):
 *   write_lock_bh(&sk->sk_callback_lock);
 *   sk->sk_data_ready = new_callback;
 *   write_unlock_bh(&sk->sk_callback_lock);
 *
 * BH 변형 사용 이유:
 *   패킷 수신은 softirq(NAPI/NET_RX)에서 발생
 *   프로세스 컨텍스트에서 콜백 변경 시 softirq와 경합 방지
 *
 * 사용 사례:
 *   - TLS(kTLS): 암호화 콜백으로 교체
 *   - epoll: poll 콜백 등록
 *   - splice/sendfile: 데이터 파이프 콜백
 */

/* TCP 수신 경로 — softirq에서 Reader */
void tcp_data_ready(struct sock *sk)
{
    /* softirq 컨텍스트 → _bh 불필요 (이미 softirq) */
    read_lock(&sk->sk_callback_lock);
    sk->sk_data_ready(sk);
    read_unlock(&sk->sk_callback_lock);
}

/* kTLS 콜백 교체 — 프로세스 컨텍스트에서 Writer */
int tls_set_device_offload(struct sock *sk, ...)
{
    write_lock_bh(&sk->sk_callback_lock);
    sk->sk_data_ready = tls_data_ready;
    write_unlock_bh(&sk->sk_callback_lock);
}

dev_addr_list_lock: MAC 주소 목록

net/core/dev_addr_lists.c에서는 네트워크 디바이스의 유니캐스트/멀티캐스트 주소 목록을 rwlock으로 보호합니다. netdev_hw_addr_list 구조체의 리스트와 카운트가 보호 대상입니다.

Reader (패킷 수신 시 주소 매칭)
netif_addr_lock(dev)(read_lock_bh(&dev->addr_list_lock))로 유니캐스트/멀티캐스트 주소 리스트를 탐색한 뒤, netif_addr_unlock(dev)로 해제합니다.
Writer (주소 추가/삭제 — ip link set 등)
netif_addr_lock_bh(dev)(write_lock_bh(&dev->addr_list_lock))로 dev_mc_add(dev, addr) 등을 수행한 뒤, netif_addr_unlock_bh(dev)로 해제합니다.

이 패턴은 네트워크 드라이버 작성 시 자주 등장합니다. 프로미스큐어스(Promiscuous) 모드 전환이나 VLAN 주소 추가 등에서 항상 이 rwlock을 통해 주소 목록에 접근해야 합니다.

fib 테이블: rwsem 사용

net/ipv4/fib_trie.c의 라우팅 테이블(Routing Table)(FIB)은 rcu_read_lock으로 보호되지만, 테이블 수정 시에는 RTNL lock(mutex)과 fib_info rwsem을 사용합니다.

읽기 경로 (패킷 포워딩 — 초당 수백만 회)
rcu_read_lock()fib_lookup(net, flp, &res) (RCU로 보호) → rcu_read_unlock()
쓰기 경로 (라우트 추가/삭제)
rtnl_lock() (mutex) → fib_table_insert(tb, cfg, ...)rtnl_unlock()

이는 rwlock → RCU 전환의 모범 사례입니다. 초기에는 rwlock_t로 FIB를 보호했으나, 현재는 RCU로 읽기를 보호하고 mutex로 쓰기를 직렬화합니다. 이를 통해 읽기 경로에서 lock 오버헤드를 완전히 제거하여 패킷 포워딩 성능을 극대화했습니다.

네트워크 스택의 교훈: 네트워크 스택은 rwlock에서 RCU로의 전환을 가장 적극적으로 수행한 서브시스템입니다. dev_base_lock(디바이스 목록), fib_lock(라우팅), neigh_tbl_lock(ARP/NDP)이 모두 RCU 기반으로 전환되었습니다. 새 네트워크 코드를 작성할 때는 rwlock_t보다 RCU를 먼저 고려하세요. sk_callback_lock처럼 포인터 swap이 아닌 콜백 보호가 필요한 경우에만 rwlock_t가 적절합니다.

커널 버전별 진화

Reader-Writer Lock은 20년 이상에 걸쳐 지속적으로 발전해왔습니다. 초기의 단순한 rwlock_t에서 현재의 qrwlock + rwsem HANDOFF + percpu_rw_semaphore까지, 각 버전의 핵심 변화를 추적합니다.

Reader-Writer Lock 커널 버전별 진화 v2.6 (2003-2011) rwlock_t: 아키텍처별 어셈블리 구현 (test-and-set 기반) rwsem: 아키텍처별 asm (x86: XADD 기반), CONFIG_RWSEM_GENERIC_SPINLOCK 대체 구현 문제: Writer starvation 심각, 확장성 제한 v3.0-3.15 (2011-2014) v3.0: rwsem을 C로 재작성 (Ingo Molnar) — 아키텍처 공통 코드 v3.10: rwsem에 Optimistic Spinning 추가 (mutex에서 영감) 효과: rwsem Writer 전환 비용 크게 감소, osq_lock 도입 v4.0-4.20 (2015-2018) v4.0: qrwlock 도입 (Waiman Long) — rwlock_t를 qrwlock으로 교체 v4.6: percpu_rw_semaphore 개선 — rcu_sync 기반 fast path v4.15: rwsem owner 추적 + NONSPINNABLE 플래그 효과: _QW_WAITING으로 Writer starvation 방지, Reader fast path O(1) v5.0-5.15 (2019-2021) v5.0: rwsem 전면 재작성 (Waiman Long) — count 비트 인코딩 변경 v5.4: HANDOFF 메커니즘 도입 — optimistic spinner가 대기자를 기아시키는 문제 해결 v5.15: PREEMPT_RT rwbase_rt — rwlock_t를 sleeping lock으로 변환 효과: 공정성 대폭 개선, RT 커널 rwlock 안정화 v6.0-6.8 (2022-2024) v6.2: rwsem WAIT_TIMEOUT 조정 (4 jiffies → adaptive) v6.7: VFS i_rwsem Direct I/O 읽기 시 회피, mmap_lock 범위 축소 방향: rwsem 자체 개선보다 rwsem 사용 범위 축소 (lockless, RCU 전환) v6.9+ / 미래 방향 mmap_lock → per-VMA lock 전환 (maple tree 기반) RCU-protected inode lookup 확대, lockless VFS 경로 확장 추세: 가능한 곳에서 RW lock을 lockless/RCU로 대체 진화 방향: 단순 busy-wait → queued + spinning → HANDOFF 공정성 → 사용 범위 축소 (lockless)
20년간의 진화: 성능 → 공정성 → 확장성 → 사용 범위 축소(lockless 전환)

핵심 커밋 레퍼런스

버전커밋/패치(Patch)변경 내용작성자
v3.104fc828e2rwsem에 optimistic spinning 최초 도입Waiman Long
v4.070af2f8aqrwlock: queued rwlock 도입Waiman Long
v4.1594a9717brwsem owner 추적 + NONSPINNABLEWaiman Long
v5.05dec94d4rwsem 전면 재작성: 새 count 인코딩Waiman Long
v5.4616be87frwsem HANDOFF 메커니즘 추가Waiman Long
v5.15943f0edbPREEMPT_RT: rwlock_t → rwbase_rtThomas Gleixner
v6.7다수VFS i_rwsem DIO 읽기 회피다수

설계 철학의 변화

Reader-Writer Lock의 설계 철학은 커널 버전에 따라 크게 진화해왔습니다.

1세대 (v2.6): 하드웨어 최적화
"아키텍처별 최적 어셈블리로 개별 연산을 빠르게" — 결과적으로 x86/ARM/MIPS 각각 다른 구현이 되어 유지보수가 어려웠습니다.
2세대 (v3.x): 알고리즘 혁신
"Optimistic Spinning으로 슬립 비용을 줄이자" — mutex에서 검증된 기법을 rwsem에 적용했습니다. 다만 spinning이 대기 큐 waiter를 기아시킬 수 있는 부작용이 있었습니다.
3세대 (v4.x-v5.x): 공정성 + 확장성
"qrwlock으로 Writer starvation 방지, HANDOFF로 공정성, percpu로 확장성" — 균형 잡힌 설계로 대부분의 시나리오에서 우수한 성능을 제공합니다.
4세대 (v6.x+): 사용 범위 축소
"RW lock 자체를 최적화하기보다, RW lock이 필요 없도록 자료구조를 변경" — mmap_lock → per-VMA lock, i_rwsem → RCU-walk 및 lockless DIO로 전환하는 방향입니다. 핵심 철학은 "가장 빠른 lock은 잡지 않는 lock"입니다.
Waiman Long의 기여: qrwlock, rwsem optimistic spinning, rwsem 재작성, HANDOFF 메커니즘 등 Reader-Writer Lock의 핵심 개선 대부분은 Waiman Long(Red Hat/HPE)이 주도했습니다. 그의 작업은 "스핀-투-슬립 전환 비용 최소화"와 "공정성과 처리량의 균형"이라는 두 가지 원칙에 기반합니다.

qrwlock·rwsem·percpu_rw_semaphore의 코어 구현은 비교적 안정화된 상태이지만, 2024-2025년 구간에 주변 서브시스템이 rwsem 의존도를 낮추는 방향으로 대거 리팩토링되었습니다. 특히 mm 서브시스템의 per-VMA lock refcount 전환(6.15)과 RTNL의 per-netns 분할(6.13)이 대표적입니다. PREEMPT_RT 메인라인 병합(6.12)으로 rwlock_t의 RT 세만틱스(rwbase_rt 기반 sleeping lock)도 기본 커널에서 공식화되었습니다.

커널릴리스변경 사항실무 시사점
6.12 (LTS)2024-11PREEMPT_RT 병합 — rwlock_t가 RT에서 rwbase_rt(rt_mutex 기반) sleeping lock으로 동작, raw_rwlock_t는 busy-wait 유지인터럽트 컨텍스트 reader가 있는 코드는 read_lock()raw_read_lock() 재검토
6.132025-01RTNL(Routing Netlink) 잠금을 per-network-namespace로 분할 — 글로벌 rtnl_lock() 경합 해소컨테이너(Container) 다량 생성/ip link 대량 조작 워크로드 성능 개선
6.142025-03lockless mount namespace lookup — namespace_sem의 read-heavy 경로 lockless 전환(10개 커밋)컨테이너 런타임 fork/exec 지연 감소
6.152025-05per-VMA lock을 rcuref_t 기반 refcount로 재구현 — mmap_lock rwsem writer 대기 최소화. module 로더(Loader) RCU-sched → RCU 전환page fault 핫패스에서 writer starvation 가능성 감소, page fault latency 분산
6.162025-07rwsem reader 핫패스 memory ordering 재검토 — ARM64 LSE 원자 연산 정합성 보강ARM64 서버에서 reader→writer 전환 시 관찰 가능 상태 정합성 개선

rwsem 대체 경로의 대두

6.15 이후 커널 커뮤니티에서는 read-heavy 자료구조를 rwsem보다 refcount 기반 lockless 또는 RCU 경로로 재설계하려는 흐름이 강해졌습니다. per-VMA lock이 대표 사례이며, 앞으로 dcache·inode 경로에서도 유사한 전환이 예상됩니다.

핵심 요약: (1) RT 병합(6.12) 이후 rwlock_t는 RT에서 수면 가능한 sleeping lock입니다. (2) per-VMA lock(6.15+)이 refcount 기반으로 바뀌었기 때문에 mmap_lock 관련 기존 분석은 재측정이 필요합니다. (3) 새로운 자료구조를 설계할 때 rwsem을 첫 선택으로 두지 말고, RCU/refcount/seqlock 조합이 더 적합한지 먼저 평가하세요.

참고자료

Reader-Writer Lock의 구현, 성능 분석, PREEMPT_RT 전환에 대한 참고 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

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