Semaphore (세마포어)

Dijkstra의 P/V 연산에서 시작된 semaphore의 이론적 배경과 Linux 커널 구현을 분석합니다. struct semaphore의 down/up 경로, Counting Semaphore 패턴, Binary Semaphore와 Mutex의 차이, rw_semaphore의 Optimistic Spinning과 HANDOFF 메커니즘, 레거시 마이그레이션 역사, 리소스 풀 관리 패턴, PREEMPT_RT 영향까지 포괄합니다.

전제 조건: 동기화 기법, Mutex, Spinlock 문서를 먼저 읽으세요. Semaphore는 sleeping lock의 원조이며, mutex와의 차이를 이해하려면 mutex 내부 구현을 먼저 파악해야 합니다.
일상 비유: Counting Semaphore는 주차장 카운터와 같습니다. 주차장에 5대 용량이면 count=5로 초기화합니다. 차가 들어올 때마다 count를 줄이고(P/down), 나갈 때마다 count를 늘립니다(V/up). count가 0이면 입구에서 대기합니다. 누가 차를 빼는지(소유권)는 상관없습니다.

핵심 요약

  • Sleeping Lock 원조 — 1965년 Dijkstra가 고안한 P(proberen)/V(verhogen) 연산이 기원입니다. 락을 얻지 못하면 프로세스(Process)를 슬립(Sleep)시킵니다.
  • 소유권 없음 — mutex와 달리 semaphore는 소유자 추적이 없습니다. down()을 호출한 스레드(Thread)가 아닌 다른 스레드가 up()을 호출할 수 있습니다.
  • Counting 가능 — count > 1로 초기화하면 동시에 여러 스레드가 임계 영역(Critical Section)에 진입할 수 있습니다. 리소스 풀 관리에 적합합니다.
  • 레거시 추세 — 2.6.16 이후 수천 개의 semaphore가 mutex로 변환되었습니다. 새 코드에서는 mutex를 사용하고, semaphore는 특수한 경우에만 사용합니다.
  • rw_semaphore — 읽기 병렬성이 필요한 경우 rw_semaphore를 사용합니다. Optimistic Spinning, HANDOFF 등 정교한 최적화가 적용되어 있습니다.

단계별 이해

  1. P/V 이론 이해
    Dijkstra의 원래 정의와 세마포어(Semaphore)가 해결하는 동기화 문제를 파악합니다.
  2. struct semaphore 구조 파악
    count, wait_list, lock 세 필드의 역할과 상호작용을 이해합니다.
  3. down/up 경로 추적
    fast path(count > 0)와 slow path(슬립/웨이크업)의 분기를 따라갑니다.
  4. Mutex와의 차이 명확화
    소유권, 우선순위 상속(Priority Inheritance), optimistic spinning 등에서 mutex가 semaphore를 대체한 이유를 이해합니다.
  5. rw_semaphore 내부 구현 분석
    count 필드 인코딩, optimistic spinning, HANDOFF 메커니즘을 파악합니다.
관련 표준: Dijkstra, E. W. "Cooperating Sequential Processes" (1965) — semaphore의 원 논문. 종합 목록은 참고자료 -- 표준 & 규격 섹션을 참고하세요.

이론적 배경: Dijkstra의 P/V 연산

Semaphore는 1965년 네덜란드 컴퓨터 과학자 Edsger W. Dijkstra가 "Cooperating Sequential Processes"에서 제안한 동기화 프리미티브입니다. 이름은 철도 신호기(semaphore)에서 따왔으며, P와 V 연산은 네덜란드어에서 유래합니다.

P/V 연산의 어원

연산네덜란드어의미동작
Pproberen (시도하다)acquire / wait / decrementcount > 0이면 count--; 아니면 슬립
Vverhogen (증가시키다)release / signal / incrementcount++; 대기자가 있으면 깨움

형식적 정의

/* Dijkstra의 원래 정의 (의사 코드) */

P(S):                          /* proberen — 시도 */
    while (S.count <= 0)
        sleep(S.wait_queue);    /* 카운트가 0이면 대기 */
    S.count--;                   /* 카운트 감소 */

V(S):                          /* verhogen — 증가 */
    S.count++;                   /* 카운트 증가 */
    if (waiters_exist(S))
        wakeup(S.wait_queue);  /* 대기자가 있으면 깨움 */
역사적 맥락: Dijkstra는 원래 busy-wait 방식의 P/V를 제안했지만, 현대 운영체제에서는 슬립/웨이크업 기반으로 구현합니다. Linux 커널의 struct semaphore도 슬립 기반입니다.
Dijkstra P/V 연산 흐름 P(S) — proberen S.count > 0 ? Yes count-- 임계 영역 진입 No wait_list에 추가 schedule() 슬립 V()에 의해 깨어남 V(S) — verhogen count++ wait_list 비어있나? 완료 (반환) Yes 첫 대기자 wake_up No
P 연산은 카운트를 감소시키고, V 연산은 카운트를 증가시킵니다. 카운트가 0이면 P는 대기합니다

Semaphore 분류

종류초기 count용도Linux 커널 대응
Binary Semaphore1상호 배제 (단일 진입)struct semaphore (count=1) / struct mutex
Counting SemaphoreN리소스 풀 관리struct semaphore (count=N)
Reader-Writer Semaphore특수 인코딩읽기 병렬성struct rw_semaphore

Semaphore vs Mutex: 차이와 선택 기준

Linux 2.6.16에서 struct mutex가 도입된 이후, semaphore의 대부분 사용처가 mutex로 대체되었습니다. 두 프리미티브의 근본적 차이를 정확히 이해해야 올바른 선택이 가능합니다.

Semaphore vs Mutex 핵심 차이 struct semaphore count: 0..N (N >= 1) 소유권 추적: 없음 재귀 잠금: 불가 (교착 위험) 우선순위 상속: 없음 Optimistic Spinning: 없음 lockdep 검증: 제한적 다른 컨텍스트 해제: 가능 적합한 사용처: - Counting 리소스 풀 (count > 1) - 비대칭 down/up (다른 스레드 해제) - 완료 신호 (completion 패턴) - 레거시 코드 유지보수 struct mutex count: 0 또는 1 (Binary only) 소유권 추적: owner 필드 재귀 잠금: lockdep이 탐지/경고 우선순위 상속: RT에서 지원 Optimistic Spinning: osq_lock lockdep 검증: 완전 지원 다른 컨텍스트 해제: 불가 (BUG) 적합한 사용처: - 모든 새로운 상호 배제 코드 - 짧은 임계 영역 (spinning 최적화) - PREEMPT_RT 호환이 필요한 코드 - 교착 탐지가 중요한 코드
새 코드에서는 거의 항상 mutex를 사용합니다. semaphore는 counting이나 비대칭 해제가 필요한 특수 경우에만 사용합니다
기준semaphoremutex판정
소유권없음owner 추적mutex: lockdep, PI 가능
Countingcount > 1 가능Binary만semaphore: 리소스 풀
다른 컨텍스트 해제가능BUG 트리거semaphore: 비대칭 시나리오
성능 (경합(Contention) 시)즉시 슬립optimistic spinning 후 슬립mutex: 짧은 경합에 유리
PREEMPT_RT변환 없음rt_mutex로 자동 변환mutex: RT 호환
lockdep제한적완전 지원mutex: 교착 탐지
설계 원칙: "새 코드에서 semaphore를 사용하지 마세요." — 이것은 커널 커뮤니티의 공식 지침입니다. semaphore가 필요하다고 생각되면, 먼저 struct mutex (Binary), struct completion (비대칭 신호), 또는 전용 리소스 풀 API가 더 적합하지 않은지 확인하세요.

struct semaphore 분석

Linux 커널의 struct semaphore는 놀라울 정도로 단순합니다. 세 개의 필드만으로 구성됩니다.

/* include/linux/semaphore.h */

struct semaphore {
    raw_spinlock_t      lock;       /* 내부 상태 보호용 스핀락 */
    unsigned int        count;      /* 사용 가능한 리소스 수 */
    struct list_head    wait_list;  /* 대기 중인 태스크 목록 */
};
struct semaphore 메모리 레이아웃 struct semaphore raw_spinlock_t lock count/wait_list 변경 시 보호 unsigned int count 사용 가능 리소스 수 struct list_head wait_list FIFO 대기 큐 (semaphore_waiter 연결) struct semaphore_waiter struct list_head list; struct task_struct *task; bool up; up = true 이면 깨어날 차례
semaphore_waiter는 대기 중인 각 태스크(Task)를 FIFO 순서로 연결합니다

초기화 매크로(Macro)

/* 정적 초기화 */
DEFINE_SEMAPHORE(name, n);           /* count = n으로 초기화 (v6.7+) */

/* 구버전 호환 (count = 1) */
DEFINE_SEMAPHORE(name);               /* 일부 구버전에서 count = 1 */

/* 동적 초기화 */
sema_init(&sem, 5);                  /* count = 5 (Counting Semaphore) */
sema_init(&sem, 1);                  /* count = 1 (Binary Semaphore) */
sema_init(&sem, 0);                  /* count = 0 (완료 신호 패턴) */
count = 0 초기화: count를 0으로 초기화하면 completion과 유사한 "이벤트 대기" 패턴을 구현할 수 있습니다. 생산자가 up()을 호출하면 소비자의 down()이 깨어납니다. 단, 이런 용도에는 struct completion이 더 명확하고 안전합니다.

Semaphore API 레퍼런스

함수반환값인터럽트(Interrupt)설명
down(&sem)void무시카운트 감소, 0이면 무한 슬립 (TASK_UNINTERRUPTIBLE)
down_interruptible(&sem)0 또는 -EINTR시그널(Signal) 수신 시 -EINTR시그널에 의해 깨어날 수 있음
down_killable(&sem)0 또는 -EINTRSIGKILL만fatal 시그널에만 반응
down_trylock(&sem)0 또는 1해당 없음비 블로킹 시도. 실패 시 1 (주의: mutex_trylock과 반대)
down_timeout(&sem, jiffies)0 또는 -ETIME타임아웃지정 시간 내 획득 실패 시 -ETIME
up(&sem)void해당 없음카운트 증가 또는 첫 대기자 깨움
down_trylock 반환값 주의: down_trylock()은 성공 시 0, 실패 시 1을 반환합니다. 이것은 mutex_trylock()이 성공 시 1, 실패 시 0을 반환하는 것과 정반대입니다. 이 차이가 수많은 버그의 원인이 되었으며, 커널 코드 리뷰에서 빈번하게 지적됩니다.
/* 올바른 사용 예 */

/* down_interruptible — 사용자 공간 프로세스에 적합 */
if (down_interruptible(&my_sem)) {
    /* 시그널에 의해 중단됨 — 정리 후 -EINTR 반환 */
    return -ERESTARTSYS;
}
/* 임계 영역 */
up(&my_sem);

/* down_trylock — 폴링 또는 fallback 경로 */
if (down_trylock(&my_sem) == 0) {
    /* 성공적으로 획득 */
    do_work();
    up(&my_sem);
} else {
    /* 획득 실패 — 대안 경로 */
    try_alternative();
}

down() 구현 분석

down() 함수는 fast path와 slow path로 나뉩니다. fast path에서는 count를 감소시키고 즉시 반환하며, count가 0이면 slow path에서 태스크를 슬립시킵니다.

/* kernel/locking/semaphore.c — down() 구현 */

void down(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(sem->count > 0))
        sem->count--;                    /* Fast path: 즉시 획득 */
    else
        __down(sem);                    /* Slow path: 슬립 */
    raw_spin_lock_irqrestore(&sem->lock, flags);
}

/* __down() — slow path */
static noinline void __sched __down(struct semaphore *sem)
{
    __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

/* __down_common() — 모든 down 변형의 공통 구현 */
static inline int __sched __down_common(
    struct semaphore *sem,
    long state,
    long timeout)
{
    struct semaphore_waiter waiter;

    list_add_tail(&waiter.list, &sem->wait_list); /* FIFO 큐 끝에 추가 */
    waiter.task = current;
    waiter.up = false;

    for (;;) {
        if (signal_pending_state(state, current))
            goto interrupted;
        if (unlikely(timeout <= 0))
            goto timed_out;
        __set_current_state(state);
        raw_spin_unlock_irq(&sem->lock);
        timeout = schedule_timeout(timeout);    /* 슬립! */
        raw_spin_lock_irq(&sem->lock);
        if (waiter.up)                              /* up()이 설정 */
            return 0;
    }

timed_out:
    list_del(&waiter.list);
    return -ETIME;

interrupted:
    list_del(&waiter.list);
    return -EINTR;
}
down() 실행 경로 분석 down(&sem) 진입 raw_spin_lock_irqsave count > 0 ? Yes (likely) count-- (Fast Path) spin_unlock, 반환 No wait_list에 waiter 추가 schedule_timeout() 슬립 waiter.up == true ? No return 0 (획득 성공) Yes
fast path는 count를 감소시키고 즉시 반환합니다. slow path는 wait_list에 들어가 up()의 웨이크업을 기다립니다

up() 구현 분석

up()은 대기자가 없으면 count를 증가시키고, 대기자가 있으면 첫 번째 대기자를 깨웁니다. 핵심은 count를 증가시키지 않고 대기자에게 직접 전달한다는 점입니다.

/* kernel/locking/semaphore.c — up() 구현 */

void up(struct semaphore *sem)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&sem->lock, flags);
    if (likely(list_empty(&sem->wait_list)))
        sem->count++;                    /* 대기자 없음: count 증가 */
    else
        __up(sem);                      /* 대기자 있음: 첫 번째 깨움 */
    raw_spin_unlock_irqrestore(&sem->lock, flags);
}

/* __up() — 대기자 웨이크업 */
static noinline void __sched __up(struct semaphore *sem)
{
    struct semaphore_waiter *waiter;

    waiter = list_first_entry(&sem->wait_list,
                              struct semaphore_waiter, list);
    list_del(&waiter->list);         /* 대기 목록에서 제거 */
    waiter->up = true;               /* 깨어날 차례 표시 */
    wake_up_process(waiter->task);   /* 태스크 깨움 */
}
count 전달 vs 증가: 대기자가 있을 때 __up()은 count를 증가시키지 않고 직접 대기자를 깨웁니다. 이렇게 하면 깨어난 태스크와 새로 진입하는 태스크 사이의 경쟁이 없어집니다. 깨어난 태스크는 waiter.up == true를 확인하고 바로 진행합니다.
up() 실행 경로 분석 up(&sem) 진입 raw_spin_lock_irqsave wait_list 비어있나? Yes (likely) count++ (리소스 반환) spin_unlock, 반환 No list_first_entry (FIFO) waiter.up = true wake_up_process(task)
대기자가 있으면 count를 증가시키지 않고 첫 번째 대기자에게 직접 전달합니다

Counting Semaphore 패턴

Counting Semaphore는 count > 1로 초기화하여 동시에 여러 스레드가 공유 리소스에 접근할 수 있게 합니다. 전형적인 사용 사례는 리소스 풀입니다.

/* Counting Semaphore: 동시 접근 수 제한 */

#define MAX_CONNECTIONS  8

static DEFINE_SEMAPHORE(conn_sem, MAX_CONNECTIONS);

static int device_open(struct inode *inode, struct file *filp)
{
    /* 최대 8개 동시 연결 허용 */
    if (down_interruptible(&conn_sem))
        return -ERESTARTSYS;

    /* 연결 설정... count는 0~7 중 하나 */
    return 0;
}

static int device_release(struct inode *inode, struct file *filp)
{
    /* 연결 해제 → 슬롯 반환 */
    up(&conn_sem);
    return 0;
}
Counting Semaphore: 리소스 풀 관리 (count=4) 리소스 풀 초기 count = 4 Slot 1 Slot 2 Slot 3 Slot 4 사용 가능 사용 중 Thread A down() → count=1 Thread B down() → count=0 Thread C down() → SLEEP Thread D down() → SLEEP Thread A: up() Thread C 깨어남! count는 증가하지 않음 — 직접 Thread C에게 전달 Thread D는 다른 스레드가 up()할 때까지 계속 대기
count=4인 풀에서 2개가 사용 중이면 count=2, 모두 사용되면 count=0이 되어 추가 요청은 슬립합니다

Binary Semaphore와 Mutex 비교

count=1인 Binary Semaphore는 표면적으로 mutex와 동일해 보이지만, 근본적인 차이가 있습니다. 이 차이가 mutex 도입의 핵심 동기였습니다.

/* Binary Semaphore: 소유권 없음 — 다른 스레드가 해제 가능 */

static DEFINE_SEMAPHORE(hw_sem, 1);

/* IRQ 핸들러에서 해제 (인터럽트 컨텍스트) */
static irqreturn_t hw_irq_handler(int irq, void *dev)
{
    /* 하드웨어 완료 → semaphore 해제 */
    up(&hw_sem);         /* down()을 호출한 스레드가 아님! */
    return IRQ_HANDLED;
}

/* 프로세스 컨텍스트에서 획득 */
static void hw_submit_and_wait(void)
{
    start_hw_operation();
    down(&hw_sem);        /* IRQ가 up()할 때까지 대기 */
    process_result();
}

/* mutex로는 불가능: */
/* mutex_unlock()을 mutex_lock()을 호출한 스레드가 아닌   */
/* 다른 스레드/컨텍스트에서 호출하면 WARNING + BUG 발생  */
특성Binary Semaphore (count=1)struct mutex
소유권없음 — 누구나 up() 가능owner 필드로 추적
잘못된 해제 탐지불가DEBUG_MUTEXES: BUG_ON
Optimistic Spinning없음 — 즉시 슬립osq_lock으로 L1 캐시(Cache) spinning
Priority Inheritance없음PREEMPT_RT: rt_mutex 변환
lockdep기본 체크만완전한 의존성 그래프 검증
인터럽트 컨텍스트 up()가능금지 (프로세스 컨텍스트만)
sizeof~40 바이트 (64비트)~72 바이트 (lockdep 포함)

rw_semaphore 구조체(Struct)

struct rw_semaphore는 읽기 병렬성과 쓰기 배타성을 동시에 제공하는 sleeping lock입니다. 커널에서 가장 많이 사용되는 동기화 프리미티브 중 하나로, mmap_lock(이전 mmap_sem)이 대표적인 사용 사례입니다.

/* include/linux/rwsem.h — rw_semaphore 구조체 */

struct rw_semaphore {
    atomic_long_t       count;      /* 상태 인코딩 (비트 필드) */
    atomic_long_t       owner;      /* 현재 라이터 (+ 플래그 비트) */
    struct optimistic_spin_queue osq; /* MCS 기반 spinning 큐 */
    raw_spinlock_t      wait_lock;  /* wait_list 보호 */
    struct list_head    wait_list;  /* 대기 태스크 목록 */
};
rw_semaphore count 필드 비트 인코딩 (64비트) bit 63 WRITER bit 62 WAITERS bit 61 HANDOFF bits 60..8 (예비) bits 7..0 READER_BIAS (0x100 단위) 상수 정의 RWSEM_WRITER_LOCKED = (1UL << 63) 라이터가 락 보유 중 RWSEM_FLAG_WAITERS = (1UL << 62) wait_list에 대기자 있음 RWSEM_FLAG_HANDOFF = (1UL << 61) 첫 대기자에게 우선 전달 RWSEM_READER_BIAS = 0x100 리더 1명 = +0x100 RWSEM_READER_MASK = 0xFF00 리더 수 추출 마스크 RWSEM_WRITER_MASK = WRITER_LOCKED | FLAG_WAITERS | FLAG_HANDOFF 상위 3비트 예: count=0x300 → 리더 3명 활성 예: count=0x8000...0000 → 라이터 보유
count 필드 하나에 라이터 잠금(Lock), 대기자 플래그, 핸드오프, 리더 수를 모두 인코딩합니다

rw_semaphore 내부 구현

rw_semaphore의 읽기/쓰기 획득 경로는 각각 fast path, optimistic spin path, slow path의 3단계로 구성됩니다.

읽기 경로: down_read()

/* kernel/locking/rwsem.c — down_read fast path */

static inline int __down_read_trylock(struct rw_semaphore *sem)
{
    long tmp = atomic_long_read(&sem->count);

    while (!(tmp & RWSEM_READ_FAILED_MASK)) {
        /* 라이터 없고, 핸드오프 없으면 → 리더 카운트 증가 */
        if (atomic_long_try_cmpxchg_acquire(
                &sem->count, &tmp,
                tmp + RWSEM_READER_BIAS))
            return 1;  /* 성공 */
    }
    return 0;  /* 실패 → slow path */
}

/* RWSEM_READ_FAILED_MASK:
 *   RWSEM_WRITER_LOCKED | RWSEM_FLAG_WAITERS | RWSEM_FLAG_HANDOFF
 *   → 라이터가 있거나 대기자/핸드오프가 있으면 fast path 실패 */

쓰기 경로: down_write()

/* kernel/locking/rwsem.c — down_write fast path */

static inline int __down_write_trylock(struct rw_semaphore *sem)
{
    long tmp = RWSEM_UNLOCKED_VALUE;  /* 0 */

    /* count가 0(완전 비어있음)일 때만 WRITER_LOCKED 설정 */
    if (atomic_long_try_cmpxchg_acquire(
            &sem->count, &tmp,
            RWSEM_WRITER_LOCKED))
        return 1;  /* 성공 */
    return 0;  /* 실패 → optimistic spin 또는 slow path */
}
rw_semaphore 획득 3단계 경로 1단계: Fast Path CAS로 즉시 획득 시도 read: count += READER_BIAS write: count = WRITER_LOCKED 비경합: ~10ns 조건: 리더만 있을 때 read, 또는 완전 비어있을 때 write 2단계: Optimistic Spin osq_lock MCS 큐에서 spinning owner가 CPU에서 실행 중이면 곧 해제할 것으로 기대 경합: ~100ns-1us 조건: owner가 현재 CPU에서 실행 중이고, need_resched 아닐 때 3단계: Slow Path wait_list에 추가 후 슬립 TASK_UNINTERRUPTIBLE 상태 up_read/up_write가 깨움 긴 경합: ~5-10us 조건: spinning 포기 후 또는 선점 필요 시 실패 포기 mutex_lock과 동일한 패턴 writer 전용, reader는 건너뜀 HANDOFF로 starvation 방지
rw_semaphore는 mutex와 동일한 3단계 최적화를 적용합니다. Optimistic Spinning은 writer 획득 시에만 사용됩니다

rwsem Optimistic Spinning

Optimistic Spinning은 rw_semaphore의 핵심 성능 최적화입니다. 락 소유자가 현재 CPU에서 실행 중이면, 곧 해제할 것으로 "낙관적으로" 기대하고 슬립 대신 spinning합니다.

/* kernel/locking/rwsem.c — rwsem_optimistic_spin() 핵심 루프 */

static bool rwsem_optimistic_spin(struct rw_semaphore *sem)
{
    /* 1. osq_lock: MCS 큐에 진입하여 spinner 정렬 */
    if (!osq_lock(&sem->osq))
        goto done;

    for (;;) {
        struct task_struct *owner;

        /* 2. owner 확인: CPU에서 실행 중인지 체크 */
        owner = rwsem_owner_flags(sem, &flags);
        if (!(flags & RWSEM_NONSPINNABLE)) {
            if (rwsem_try_write_lock_unqueued(sem))
                break;  /* 성공! */
            if (owner && !owner_on_cpu(owner))
                goto done;  /* owner 비실행 → 포기 */
        }

        /* 3. 선점 요청 확인 */
        if (need_resched())
            goto done;  /* 선점 필요 → 포기 */

        cpu_relax();  /* PAUSE/WFE 힌트 */
    }
    osq_unlock(&sem->osq);
    return true;  /* 획득 성공 */

done:
    osq_unlock(&sem->osq);
    return false;  /* slow path로 이동 */
}
spinning 포기 조건: (1) owner가 CPU에서 내려감(sleep/preempt), (2) need_resched()가 true, (3) HANDOFF 플래그가 설정됨. 이 조건들은 spinning이 더 이상 유익하지 않을 때를 정확히 포착합니다.

rwsem HANDOFF 메커니즘

HANDOFF는 writer starvation을 방지하는 핵심 메커니즘입니다. 대기 중인 첫 번째 writer가 일정 시간 이상 기다리면 HANDOFF 플래그를 설정하여, 새로운 리더/라이터의 fast path 획득을 차단합니다.

/* HANDOFF 동작 시퀀스 */

/* 1. Writer W1이 wait_list의 첫 번째로 대기 중 */
/* 2. 새 리더들이 계속 fast path로 진입 → W1 굶주림 */
/* 3. W1이 한 번 깨어났다가 다시 슬립하면: HANDOFF 설정 */

count |= RWSEM_FLAG_HANDOFF;  /* bit 61 설정 */

/* 4. 이후 새 리더의 fast path 시도: */
/*    RWSEM_READ_FAILED_MASK에 HANDOFF 포함 → CAS 실패 */

/* 5. 기존 리더들이 모두 해제되면: */
/*    up_read()가 W1을 깨움 */
/*    W1이 HANDOFF 비트와 함께 WRITER_LOCKED 설정 */

/* 6. W1 획득 후: HANDOFF 비트 클리어 */
rwsem HANDOFF: Writer Starvation 방지 시간 HANDOFF 이전 R1: down_read ------ R2: down_read --- R3: down_read - W1: down_write (대기 중...) 리더가 끝없이 진입 → W1 굶주림 HANDOFF 설정 R2: 계속 보유 --- R4: down_read 차단! R5: down_read 차단! W1: HANDOFF 비트 설정 새 리더 fast path 차단됨 Writer 획득 R2: up_read → 마지막 리더 W1: 획득! WRITER_LOCKED 설정 HANDOFF 비트 클리어 정상 운영 재개 HANDOFF 트리거 조건과 효과 트리거: 첫 번째 대기 writer가 한 번 깨어났다가 다시 슬립 (=다른 스레드에게 빼앗김) 효과 1: RWSEM_FLAG_HANDOFF가 RWSEM_READ_FAILED_MASK에 포함 → 새 리더 fast path 차단 효과 2: optimistic spinner도 HANDOFF 비트를 보면 spinning 포기 효과 3: 기존 리더가 모두 해제되면 HANDOFF 대상 writer에게 직접 전달
HANDOFF는 writer가 한 번 깨어났다 실패한 후 설정되어 새 리더의 진입을 차단합니다

rw_semaphore API 레퍼런스

함수방향설명
down_read(&rwsem)읽기 획득다른 리더와 공존 가능, 라이터와 배타
down_read_interruptible(&rwsem)읽기 획득시그널로 중단 가능
down_read_killable(&rwsem)읽기 획득SIGKILL로만 중단 가능
down_read_trylock(&rwsem)읽기 시도비 블로킹, 성공 시 1 반환
up_read(&rwsem)읽기 해제리더 카운트 감소
down_write(&rwsem)쓰기 획득모든 리더/라이터와 배타
down_write_killable(&rwsem)쓰기 획득SIGKILL로 중단 가능
down_write_trylock(&rwsem)쓰기 시도비 블로킹, 성공 시 1 반환
up_write(&rwsem)쓰기 해제라이터 비트 클리어 + 대기자 깨움
downgrade_write(&rwsem)쓰기→읽기원자적(Atomic)으로 write lock을 read lock으로 변환
/* downgrade_write() 패턴: 쓰기 후 읽기 계속 */

down_write(&inode->i_rwsem);
/* 메타데이터 수정 (배타적) */
update_metadata(inode);

/* 수정 완료 후 읽기 모드로 전환 (다른 리더 허용) */
downgrade_write(&inode->i_rwsem);

/* 데이터 읽기 (다른 리더와 병렬) */
read_data(inode);
up_read(&inode->i_rwsem);

레거시 semaphore에서 mutex 마이그레이션

Linux 2.6.16(2006년)에서 Ingo Molnar가 struct mutex를 도입한 이후, 커널 전체에서 대규모 semaphore 교체가 진행되었습니다. 이것은 Linux 커널 역사에서 가장 큰 규모의 동기화 프리미티브 마이그레이션 중 하나입니다.

Semaphore → Mutex 마이그레이션 역사 2.6.16 mutex 도입 Ingo Molnar 소유권 + lockdep ~2000개 semaphore 존재 2.6.30+ 대량 변환 VFS: inode->i_sem → inode->i_mutex → inode->i_rwsem (4.5) 3.x~4.x 대부분 완료 남은 semaphore < 50개 대부분 counting 용도 또는 비대칭 해제 필요 6.x 현재 상태 semaphore 거의 사라짐 새 코드: mutex 사용 rw_semaphore 활발 사용 주요 마이그레이션 사례 inode->i_sem → i_mutex (2.6.16) → i_rwsem (4.5): VFS 전체 영향 mmap_sem → mmap_lock (5.8): mm_struct의 rw_semaphore 래퍼 bd_sem → bd_mutex (2.6.x): block device 접근 보호
2006년 mutex 도입 이후 약 2000개의 semaphore가 단계적으로 mutex로 변환되었습니다

마이그레이션 체크리스트

/* semaphore → mutex 변환 가능 조건 */

/* 1. count == 1 (Binary만) */
DEFINE_SEMAPHORE(my_sem, 1);        /* → DEFINE_MUTEX(my_mutex); */

/* 2. down()과 up()이 같은 컨텍스트 */
down(&my_sem);                     /* → mutex_lock(&my_mutex); */
/* 임계 영역 */
up(&my_sem);                       /* → mutex_unlock(&my_mutex); */

/* 3. 인터럽트 컨텍스트에서 up()하지 않음 */

/* 변환 불가능한 경우: */
/* - count > 1 (Counting Semaphore) */
/* - 다른 스레드/컨텍스트에서 up() */
/* - 인터럽트 핸들러에서 up() */

리소스 풀 관리 패턴

Counting Semaphore의 가장 실용적인 응용은 한정된 리소스 풀의 동시 접근 제어(Access Control)입니다.

/* 실전 패턴: DMA 채널 풀 관리 */

#define NUM_DMA_CHANNELS  4

struct dma_pool {
    struct semaphore    avail;          /* 사용 가능한 채널 수 */
    spinlock_t          pool_lock;      /* 비트맵 보호 */
    unsigned long       channel_map;    /* 채널 사용 비트맵 */
    struct dma_chan     *channels[NUM_DMA_CHANNELS];
};

static struct dma_pool pool;

static void dma_pool_init(void)
{
    sema_init(&pool.avail, NUM_DMA_CHANNELS);
    spin_lock_init(&pool.pool_lock);
    pool.channel_map = 0;
}

static struct dma_chan *dma_channel_get(void)
{
    int ch;

    /* count > 0이면 즉시 통과, 아니면 슬립 */
    if (down_interruptible(&pool.avail))
        return ERR_PTR(-ERESTARTSYS);

    spin_lock(&pool.pool_lock);
    ch = find_first_zero_bit(&pool.channel_map, NUM_DMA_CHANNELS);
    __set_bit(ch, &pool.channel_map);
    spin_unlock(&pool.pool_lock);

    return pool.channels[ch];
}

static void dma_channel_put(struct dma_chan *chan)
{
    int ch = chan_to_index(chan);

    spin_lock(&pool.pool_lock);
    __clear_bit(ch, &pool.channel_map);
    spin_unlock(&pool.pool_lock);

    up(&pool.avail);  /* 대기자 있으면 깨움 */
}

PREEMPT_RT 영향

PREEMPT_RT 커널에서 semaphore와 rw_semaphore의 동작이 크게 달라집니다. 이 차이를 이해하는 것은 RT 시스템 개발에 필수적입니다.

프리미티브일반 커널PREEMPT_RT비고
struct semaphore그대로 유지그대로 유지RT 변환 대상 아님
struct mutexsleeping lockrt_mutex로 변환PI 지원 추가
struct rw_semaphoresleeping lockrwbase_rt로 변환PI 지원 추가
raw_spinlock_tbusy-waitbusy-wait (유지)진정한 spinlock
spinlock_tbusy-waitrt_mutex로 변환sleeping lock이 됨
RT에서 semaphore 주의점: struct semaphore는 PREEMPT_RT에서도 변환되지 않습니다. 이는 semaphore가 소유권이 없어 우선순위 상속(PI)을 적용할 수 없기 때문입니다. 따라서 RT 시스템에서 semaphore의 down()은 unbounded priority inversion을 일으킬 수 있습니다. RT 코드에서는 가능한 한 struct mutex를 사용하세요.
PREEMPT_RT: 동기화 프리미티브 변환 맵 일반 커널 (PREEMPT_FULL) spinlock_t mutex rw_semaphore semaphore raw_spinlock_t PREEMPT_RT rt_mutex (PI) rt_mutex (PI) rwbase_rt (PI) semaphore (변환 없음!) raw_spinlock_t (유지) 변환 없음!
semaphore는 소유권이 없어 PI를 적용할 수 없으므로 PREEMPT_RT에서도 변환되지 않습니다

실전 사용 패턴

현재 커널에서 semaphore가 사용되는 실제 사례와 올바른 사용 패턴을 분석합니다.

패턴 1: Completion 대용 (count=0 초기화)

/* count=0으로 초기화 → 이벤트 대기 패턴 */
/* 주의: 새 코드에서는 struct completion을 사용하세요 */

static DEFINE_SEMAPHORE(firmware_loaded, 0);

/* 로더 스레드 */
static int firmware_loader(void *data)
{
    load_firmware();
    up(&firmware_loaded);     /* 완료 신호 */
    return 0;
}

/* 사용자 스레드 */
static void use_firmware(void)
{
    down(&firmware_loaded);   /* 로딩 완료까지 대기 */
    access_firmware();
}

패턴 2: Producer-Consumer (이중 세마포어)

/* 이중 세마포어: bounded buffer */

#define BUF_SIZE  16

static DEFINE_SEMAPHORE(empty_slots, BUF_SIZE);  /* 빈 슬롯 수 */
static DEFINE_SEMAPHORE(full_slots, 0);          /* 채워진 슬롯 수 */
static DEFINE_MUTEX(buf_mutex);               /* 버퍼 접근 보호 */

static void producer(void)
{
    down(&empty_slots);      /* 빈 슬롯 확보 */
    mutex_lock(&buf_mutex);
    enqueue_item();
    mutex_unlock(&buf_mutex);
    up(&full_slots);         /* 채워진 슬롯 알림 */
}

static void consumer(void)
{
    down(&full_slots);       /* 채워진 슬롯 대기 */
    mutex_lock(&buf_mutex);
    dequeue_item();
    mutex_unlock(&buf_mutex);
    up(&empty_slots);        /* 빈 슬롯 반환 */
}

커널 내 실제 사용 사례

위치타입용도
inode->i_rwsemrw_semaphoreVFS inode 메타데이터 보호 (readdir, write, truncate)
mm->mmap_lockrw_semaphore프로세스 주소 공간(Address Space) 보호 (page fault, mmap, munmap)
sb->s_umountrw_semaphore슈퍼블록(Superblock) 마운트(Mount)/언마운트 보호
tty->termios_rwsemrw_semaphoreTTY termios 설정 보호
char device semsemaphore일부 레거시 드라이버의 동시 접근 제한

안티패턴

semaphore 사용에서 흔히 발생하는 실수와 안티패턴을 분석합니다.

안티패턴 1: Binary Semaphore를 mutex 대신 사용

/* BAD: mutex가 적합한 곳에 semaphore 사용 */
static DEFINE_SEMAPHORE(my_sem, 1);

static void critical_section(void)
{
    down(&my_sem);
    /* 같은 스레드에서 down/up → mutex 사용해야 함 */
    do_work();
    up(&my_sem);
}

/* GOOD: mutex 사용 */
static DEFINE_MUTEX(my_mutex);

static void critical_section(void)
{
    mutex_lock(&my_mutex);
    do_work();
    mutex_unlock(&my_mutex);
    /* lockdep 검증, optimistic spinning, PI 지원 */
}

안티패턴 2: down_trylock 반환값 오류

/* BAD: mutex_trylock과 혼동 */
if (down_trylock(&sem)) {    /* 1 = 실패인데, 성공으로 착각! */
    do_work();                /* 락 없이 실행 → 레이스! */
    up(&sem);                 /* 이중 up → count 오염 */
}

/* GOOD: 반환값 정확히 확인 */
if (down_trylock(&sem) == 0) {   /* 0 = 성공 */
    do_work();
    up(&sem);
}

안티패턴 3: 인터럽트 컨텍스트에서 down()

/* BAD: 인터럽트 핸들러에서 down() → 슬립 불가! */
static irqreturn_t bad_handler(int irq, void *dev)
{
    down(&sem);     /* BUG: 인터럽트에서 sleep → 시스템 행 */
    do_work();
    up(&sem);
    return IRQ_HANDLED;
}

/* GOOD: down_trylock 또는 spinlock 사용 */
static irqreturn_t good_handler(int irq, void *dev)
{
    if (down_trylock(&sem) == 0) {
        do_work();
        up(&sem);
    } else {
        schedule_work(&deferred_work);  /* 워커로 위임 */
    }
    return IRQ_HANDLED;
}

안티패턴 4: up() 누락 에러 경로

/* BAD: 에러 경로에서 up() 누락 → 영구 잠금 */
static int bad_function(void)
{
    down(&sem);
    if (allocate_resource() < 0)
        return -ENOMEM;  /* up() 호출 안 함! → 데드락 */
    do_work();
    up(&sem);
    return 0;
}

/* GOOD: goto cleanup 패턴 */
static int good_function(void)
{
    int ret;

    down(&sem);
    if (allocate_resource() < 0) {
        ret = -ENOMEM;
        goto out_unlock;
    }
    do_work();
    ret = 0;
out_unlock:
    up(&sem);
    return ret;
}

디버깅(Debugging)

semaphore 관련 문제를 진단하는 도구와 기법을 정리합니다.

Hung Task 탐지

/* CONFIG_DETECT_HUNG_TASK=y */
/* TASK_UNINTERRUPTIBLE 상태로 120초 이상 머무르면 경고 */

INFO: task kworker/0:1:42 blocked for more than 120 seconds.
      Not tainted 6.8.0 #1
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:kworker/0:1     state:D stack:13456 pid:42
Call Trace:
 <TASK>
 __schedule+0x3e8/0x1150
 schedule+0x5e/0xd0
 schedule_timeout+0x118/0x150
 __down+0x5a/0x80            /* ← semaphore down에서 blocked */
 down+0x43/0x50
 my_driver_write+0x2b/0x90
 vfs_write+0x1a5/0x6d0

lockdep으로 교착 탐지

/* semaphore도 lockdep 기본 체크 적용 (제한적) */

/* CONFIG_PROVE_LOCKING=y */
/* 같은 semaphore를 두 번 down() → 자기 교착 탐지 */

============================================
WARNING: possible recursive locking detected
--------------------------------------------
process/1234 is trying to acquire lock:
 (&my_sem){+.+.}-{3:3}, at: my_function+0x20/0x60
but task is already holding lock:
 (&my_sem){+.+.}-{3:3}, at: my_function+0x20/0x60

SysRq로 blocked 태스크 확인

/* Alt+SysRq+W: TASK_UNINTERRUPTIBLE 태스크 표시 */
echo w > /proc/sysrq-trigger

/* 출력 예시 */
SysRq : Show Blocked State
  task                PC stack   pid father
kworker/0:1     D 0000000000000000 13456    42      2
 Call Trace:
  schedule_timeout+0x118/0x150
  __down+0x5a/0x80
  down+0x43/0x50

/* /proc//wchan 으로 개별 확인 */
cat /proc/42/wchan
/* 출력: __down */

ftrace로 semaphore 추적

# function_graph로 down/up 호출 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo down > /sys/kernel/debug/tracing/set_ftrace_filter
echo up >> /sys/kernel/debug/tracing/set_ftrace_filter
echo __down >> /sys/kernel/debug/tracing/set_ftrace_filter
echo __up >> /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 추적 결과 확인
cat /sys/kernel/debug/tracing/trace

커널 설정

옵션기본값설명
CONFIG_RWSEM_SPIN_ON_OWNERyrwsem optimistic spinning 활성화
CONFIG_LOCK_SPIN_ON_OWNERymutex/rwsem optimistic spinning 전제 조건
CONFIG_PROVE_LOCKINGnlockdep 교착 탐지 (개발용)
CONFIG_DEBUG_LOCK_ALLOCn잠금 할당 추적
CONFIG_LOCK_STATn/proc/lock_stat 경합 통계
CONFIG_DETECT_HUNG_TASKyTASK_UNINTERRUPTIBLE 장기 차단 탐지
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT120hung task 타임아웃 (초)
CONFIG_PREEMPT_RTnRT 패치 (mutex→rt_mutex 변환)
# /proc/lock_stat 출력 예시 (CONFIG_LOCK_STAT=y)

lock_stat version 0.4
---------------------------------------------------------------------------
                              class name    con-bounces    contentions ...
---------------------------------------------------------------------------
                         &sb->s_umount:         1234           5678 ...
                          &mm->mmap_lock:        9012          34567 ...
                        &inode->i_rwsem:          456           7890 ...

down() 소스 코드 심층 분석

앞서 down() 구현 분석에서 전체 흐름을 살펴보았습니다. 이 섹션에서는 __down_common()의 각 코드 라인을 해부하여, waiter list 관리, schedule_timeout 상호작용, 시그널/타임아웃 처리의 미묘한 동기화 문제를 분석합니다.

semaphore_waiter 구조체

/* include/linux/semaphore.h */

struct semaphore_waiter {
    struct list_head list;   /* sem->wait_list에 연결 */
    struct task_struct *task; /* 슬립 중인 태스크 */
    bool up;                   /* up()이 이 waiter를 깨웠는지 */
};

/* 핵심 설계: up 필드가 있는 이유
 * schedule_timeout()에서 깨어난 것이 up()에 의한 것인지,
 * 시그널/타임아웃에 의한 spurious wakeup인지 구분해야 합니다.
 * up()은 waiter.up = true로 설정한 후 wake_up_process()를 호출하므로
 * 깨어난 태스크는 waiter.up 값으로 정당한 wakeup인지 확인합니다. */

__down_common() 라인별 분석

/* kernel/locking/semaphore.c — __down_common() 완전 분석 */

static inline int __sched __down_common(
    struct semaphore *sem,
    long state,       /* TASK_UNINTERRUPTIBLE or TASK_INTERRUPTIBLE or TASK_KILLABLE */
    long timeout)     /* MAX_SCHEDULE_TIMEOUT or jiffies 값 */
{
    struct semaphore_waiter waiter;

    /* 1단계: waiter를 wait_list 끝에 추가 (FIFO 순서 보장)
     * raw_spinlock을 이미 보유하고 있으므로 list 조작은 안전합니다 */
    list_add_tail(&waiter.list, &sem->wait_list);
    waiter.task = current;
    waiter.up = false;

    for (;;) {
        /* 2단계: 시그널 체크 (INTERRUPTIBLE/KILLABLE 경우만 유효)
         * TASK_UNINTERRUPTIBLE이면 signal_pending_state()는 항상 false */
        if (signal_pending_state(state, current))
            goto interrupted;

        /* 3단계: 타임아웃 체크
         * MAX_SCHEDULE_TIMEOUT은 사실상 무한대이므로 여기 걸리지 않음
         * down_timeout()에서만 실질적으로 활성화됩니다 */
        if (unlikely(timeout <= 0))
            goto timed_out;

        /* 4단계: 태스크 상태 설정
         * __set_current_state()는 배리어 없는 버전 — 이미 스핀락 보호 하에 있으므로
         * set_current_state()의 smp_store_mb()가 불필요합니다 */
        __set_current_state(state);

        /* 5단계: 스핀락 해제 후 슬립
         * raw_spin_unlock_irq는 인터럽트를 복원합니다
         * 이 시점에서 up()이 실행될 수 있습니다 (레이스 윈도우) */
        raw_spin_unlock_irq(&sem->lock);

        /* schedule_timeout: 실제 슬립!
         * 반환 값은 남은 timeout jiffies (또는 0) */
        timeout = schedule_timeout(timeout);

        /* 6단계: 깨어남 → 스핀락 재획득
         * 이 시점에서 waiter.up이 true일 수도 있고 아닐 수도 있음 */
        raw_spin_lock_irq(&sem->lock);

        /* 7단계: 정당한 wakeup 확인
         * up()이 waiter.up = true를 설정했으면 세마포어 획득 성공 */
        if (waiter.up)
            return 0;
    }

timed_out:
    /* 타임아웃: wait_list에서 제거하고 -ETIME 반환
     * 아직 스핀락을 보유하고 있으므로 list_del은 안전 */
    list_del(&waiter.list);
    return -ETIME;

interrupted:
    /* 시그널 수신: wait_list에서 제거하고 -EINTR 반환
     * down_interruptible(), down_killable()에서만 도달 */
    list_del(&waiter.list);
    return -EINTR;
}

unlock-schedule 레이스 윈도우

raw_spin_unlock_irq()schedule_timeout() 사이에 레이스 윈도우가 존재합니다. 이 구간에서 up()이 실행되면 어떻게 되는지 분석합니다.

/* 레이스 시나리오:
 *
 * CPU 0 (down)                    CPU 1 (up)
 * ─────────────────               ─────────────────
 * __set_current_state(UNINTERRUPTIBLE)
 * raw_spin_unlock_irq()
 *                                 raw_spin_lock_irq()
 *                                 waiter.up = true
 *                                 wake_up_process(task)
 *                                 raw_spin_unlock_irq()
 * schedule_timeout()
 *   → try_to_wake_up()가 이미 실행됨
 *   → task 상태가 RUNNING으로 변경됨
 *   → schedule()은 즉시 반환
 * raw_spin_lock_irq()
 * waiter.up == true → return 0
 *
 * 안전한 이유:
 * __set_current_state()가 spin_unlock 이전에 호출되므로
 * up()의 wake_up_process()가 태스크를 정확히 깨울 수 있습니다.
 * schedule()이 호출되더라도 이미 RUNNING 상태이므로 즉시 반환합니다. */
참고: __set_current_state() vs set_current_state() — semaphore는 raw_spinlock 보호 하에서 상태를 설정하므로 배리어가 불필요합니다. wait queue는 스핀락(Spinlock) 없이 상태를 설정하므로 set_current_state()smp_store_mb()가 필요합니다.

down 변형별 차이

함수state 인자timeout 인자반환값
__down()TASK_UNINTERRUPTIBLEMAX_SCHEDULE_TIMEOUTvoid (항상 성공)
__down_interruptible()TASK_INTERRUPTIBLEMAX_SCHEDULE_TIMEOUT0 또는 -EINTR
__down_killable()TASK_KILLABLEMAX_SCHEDULE_TIMEOUT0 또는 -EINTR
__down_timeout()TASK_UNINTERRUPTIBLEjiffies 값0 또는 -ETIME
__down_common() 내부 상태 전이 상세 __down_common() 진입 list_add_tail (FIFO 큐 추가) waiter.task = current, up = false for (;;) 루프 signal_pending_state? goto interrupted → -EINTR Yes timeout <= 0 ? goto timed_out → -ETIME Yes schedule_timeout() 슬립 No/No 통과 waiter.up == true ? return 0 (성공) Yes No → 루프 재시작
__down_common()의 for 루프는 시그널 체크 → 타임아웃 체크 → 슬립 → wakeup 확인 순서로 반복됩니다. waiter.up이 true가 되어야만 루프를 탈출합니다
주의: down()TASK_UNINTERRUPTIBLE을 사용하므로 시그널에 반응하지 않습니다. 세마포어가 영원히 해제되지 않으면 태스크는 D 상태(TASK_UNINTERRUPTIBLE)에서 영원히 멈춥니다. 이런 경우 CONFIG_DETECT_HUNG_TASK가 120초 후 경고를 출력합니다.

rwsem 소스 코드 심층 분석

rw_semaphore의 핵심은 64비트 count 필드의 비트 인코딩과, 이를 기반으로 한 rwsem_down_read_slowpath()/rwsem_down_write_slowpath()의 정교한 상태 머신입니다.

count 비트 필드 상세 인코딩

/* kernel/locking/rwsem.c — count 비트 레이아웃 (64비트) */

/*
 * 비트 63                                            비트 0
 * ┌─────────────────────────────────────────────────────────┐
 * │ READER_COUNT (비트 8~63)  │ FLAGS (비트 1~7) │ WRITER (비트 0) │
 * └─────────────────────────────────────────────────────────┘
 *
 * RWSEM_WRITER_LOCKED  = 1 << 0        (비트 0)
 * RWSEM_FLAG_WAITERS   = 1 << 1        (비트 1)
 * RWSEM_FLAG_HANDOFF   = 1 << 2        (비트 2)
 * RWSEM_READER_BIAS    = 1 << 8        (비트 8~63 = 리더 수)
 */

#define RWSEM_WRITER_LOCKED  (1UL << 0)
#define RWSEM_FLAG_WAITERS   (1UL << 1)
#define RWSEM_FLAG_HANDOFF   (1UL << 2)
#define RWSEM_READER_BIAS    (1UL << 8)

/* 리더 수 추출 */
#define RWSEM_READER_SHIFT   8
#define rwsem_reader_count(c) ((c) >> RWSEM_READER_SHIFT)

/* Fast path 실패 조건 마스크:
 * 라이터 보유 중이거나, 대기자 있거나, 핸드오프 진행 중이면 fast path 불가 */
#define RWSEM_READ_FAILED_MASK  \
    (RWSEM_WRITER_LOCKED | RWSEM_FLAG_WAITERS | RWSEM_FLAG_HANDOFF)

rwsem_down_read_slowpath() 분석

/* kernel/locking/rwsem.c — 읽기 slow path 핵심 */

static struct rw_semaphore *
rwsem_down_read_slowpath(struct rw_semaphore *sem, long count, unsigned int state)
{
    struct rwsem_waiter waiter;
    DEFINE_WAKE_Q(wake_q);

    /* 1. 라이터가 없고 핸드오프가 없으면 리더 바이어스 시도
     * 이미 READER_BIAS를 더한 상태(fast path 시도 흔적)이므로
     * 조건이 맞으면 그대로 리더로 진입 가능 */
    if (!(count & (RWSEM_WRITER_LOCKED | RWSEM_FLAG_HANDOFF))) {
        rwsem_set_reader_owned(sem);
        lockevent_inc(rwsem_rlock_fast);
        return sem;  /* 리더 직접 획득! */
    }

    /* 2. fast path 실패 → READER_BIAS 복원 (나중에 다시 설정) */
    atomic_long_add(-RWSEM_READER_BIAS, &sem->count);

    /* 3. waiter 구성 및 wait_list 추가 */
    waiter.task = current;
    waiter.type = RWSEM_WAITING_FOR_READ;
    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);

    /* 4. WAITERS 플래그 설정 */
    rwsem_add_waiter(sem, &waiter);

    /* 5. 슬립 루프 */
    for (;;) {
        set_current_state(state);
        if (!smp_load_acquire(&waiter.task))
            break;  /* wakeup 수신! task가 NULL로 설정됨 */
        raw_spin_unlock_irq(&sem->wait_lock);
        schedule_preempt_disabled();
        raw_spin_lock_irq(&sem->wait_lock);
    }
    __set_current_state(TASK_RUNNING);
    raw_spin_unlock_irq(&sem->wait_lock);
    lockevent_inc(rwsem_rlock);
    return sem;
}
참고: rwsem의 wakeup 프로토콜은 semaphore와 다릅니다. semaphore는 waiter.up = true를 사용하고, rwsem은 waiter.task = NULL을 사용합니다. smp_load_acquire()로 읽어서 메모리 순서를 보장합니다.

rwsem_down_write_slowpath() 핵심

/* kernel/locking/rwsem.c — 쓰기 slow path 핵심 흐름 */

static struct rw_semaphore *
rwsem_down_write_slowpath(struct rw_semaphore *sem, int state)
{
    struct rwsem_waiter waiter;

    /* 1. Optimistic Spinning 시도 (CONFIG_RWSEM_SPIN_ON_OWNER)
     * owner가 CPU에서 실행 중이면 spinning으로 빠르게 획득 */
    if (rwsem_can_spin_on_owner(sem))
        if (rwsem_optimistic_spin(sem))
            return sem;  /* spinning 성공! */

    /* 2. wait_list에 추가 */
    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);
    rwsem_add_waiter(sem, &waiter);

    /* 3. 슬립 루프 — HANDOFF 처리 포함 */
    for (;;) {
        if (rwsem_try_write_lock(sem, &waiter)) {
            /* HANDOFF가 설정된 상태에서 첫 번째 waiter만 획득 가능 */
            break;
        }

        set_current_state(state);
        raw_spin_unlock_irq(&sem->wait_lock);

        /* HANDOFF 설정: 대기 시간이 RWSEM_WAIT_TIMEOUT 초과 시 */
        if (!waiter.handoff_set &&
            time_after(jiffies, waiter.timeout)) {
            atomic_long_or(RWSEM_FLAG_HANDOFF, &sem->count);
            waiter.handoff_set = true;
        }

        schedule_preempt_disabled();
        raw_spin_lock_irq(&sem->wait_lock);
    }
    __set_current_state(TASK_RUNNING);
    raw_spin_unlock_irq(&sem->wait_lock);
    return sem;
}
rwsem count 비트 필드 인코딩 (64비트) READER COUNT (비트 8~63) 예약 (3~7) H W WR 비트 63 비트 8 비트 2 비트 1 비트 0 상태 예시: UNLOCKED count = 0x0000000000000000 → 자유 상태 3 READERS count = 0x0000000000000300 → 3 * READER_BIAS(0x100) WRITER LOCKED count = 0x0000000000000001 → WRITER_LOCKED (비트 0) WRITER + WAITERS count = 0x0000000000000003 → WRITER_LOCKED | FLAG_WAITERS HANDOFF 진행 count = 0x0000000000000007 → WRITER | WAITERS | HANDOFF READ_FAILED_MASK = WRITER_LOCKED | FLAG_WAITERS | FLAG_HANDOFF (하위 3비트 중 하나라도 1이면 read fast path 실패)
rwsem의 64비트 count 필드는 상위 56비트에 리더 수, 하위 비트에 플래그를 인코딩합니다. 이 설계로 단일 atomic 연산으로 상태를 판단합니다

percpu_rw_semaphore 심층 분석

percpu_rw_semaphore는 읽기 경로를 극단적으로 최적화한 rw_semaphore 변형입니다. 각 CPU마다 독립적인 리더 카운터를 유지하여 읽기 경로에서 캐시 라인(Cache Line) 바운싱이 전혀 없습니다. 대신 쓰기 경로는 매우 비용이 큽니다.

구조체 분석

/* include/linux/percpu-rwsem.h */

struct percpu_rw_semaphore {
    struct rcu_sync rss;         /* RCU 동기화 상태 추적 */
    unsigned int __percpu *read_count;  /* per-CPU 리더 카운터 */
    struct rcuwait writer;      /* 라이터 대기 */
    wait_queue_head_t waiters;  /* 리더 대기 큐 (slow path) */
    atomic_t block;              /* 리더 차단 플래그 */
};

읽기 Fast Path: __percpu_down_read()

/* kernel/locking/percpu-rwsem.c — 읽기 fast path */

bool __percpu_down_read(struct percpu_rw_semaphore *sem, bool try)
{
    /* 1. RCU read-side critical section 진입
     * preempt_disable()과 동일 — 매우 가벼움 */
    rcu_read_lock();

    /* 2. rcu_sync가 idle 상태면 → 라이터 없음 → fast path
     * __rcu_sync_is_idle()는 단순 변수 읽기 (atomic 불필요) */
    if (likely(__rcu_sync_is_idle(&sem->rss))) {
        /* 3. per-CPU 카운터 증가
         * this_cpu_inc는 로컬 CPU 변수 조작이므로 atomic 불필요
         * 캐시 라인 바운싱 없음! */
        this_cpu_inc(*sem->read_count);
        rcu_read_unlock();

        /* 4. 카운터 증가 후 block 플래그 재확인
         * 라이터가 카운터 증가와 동시에 진입했을 수 있으므로 */
        if (likely(!atomic_read(&sem->block)))
            return true;  /* Fast path 성공! */

        /* 라이터가 진입 중 → 카운터 복원 후 slow path */
        __percpu_up_read(sem);
        return false;  /* → slow path */
    }

    rcu_read_unlock();
    return false;  /* 라이터 활동 중 → slow path */
}

쓰기 경로: percpu_down_write()

/* kernel/locking/percpu-rwsem.c — 쓰기 경로 (매우 비용이 큼) */

void percpu_down_write(struct percpu_rw_semaphore *sem)
{
    /* 1단계: rcu_sync를 "enter" 상태로 전환
     * 이후 새로운 리더는 fast path를 사용할 수 없음
     * synchronize_rcu()를 내부적으로 호출 → 매우 느림 (수 ms) */
    rcu_sync_enter(&sem->rss);

    /* 2단계: block 플래그 설정 → 새 리더 진입 차단 */
    atomic_set(&sem->block, 1);
    smp_mb();  /* block 설정이 모든 CPU에서 보이도록 */

    /* 3단계: 모든 CPU의 read_count를 합산하여 0이 될 때까지 대기
     * 기존 리더들이 모두 나갈 때까지 기다림 */
    wait_event(sem->waiters,
        readers_active_check(sem) == 0);

    /* 4단계: 모든 리더가 나감 → 라이터 독점 진입 */
}
주의: percpu_down_write()는 내부적으로 synchronize_rcu()를 호출합니다. 이는 RCU grace period를 기다리는 것으로, 수 밀리초에서 수십 밀리초가 걸릴 수 있습니다. 따라서 쓰기가 극히 드문 경우에만 percpu_rw_semaphore를 사용해야 합니다.
percpu_rw_semaphore 읽기/쓰기 경로 대비 읽기 Fast Path (99.9%) rcu_read_lock() — preempt_disable __rcu_sync_is_idle() 확인 this_cpu_inc(*read_count) rcu_read_unlock() atomic_read(&block) == 0 확인 ~5ns (캐시 라인 경합 없음) atomic 연산: 0개, 캐시 miss: 0 쓰기 경로 (극히 드물게) rcu_sync_enter() — synchronize_rcu atomic_set(&block, 1) + smp_mb() for_each_possible_cpu: sum read_count wait_event(sum == 0) — 리더 완료 대기 라이터 독점 진입 완료 ~5-50ms (synchronize_rcu 포함) RCU GP + IPI + 전 CPU 순회
percpu_rw_semaphore는 읽기를 ~5ns로 만드는 대신, 쓰기를 수십 ms로 희생합니다. mmap_lock, CPU hotplug 등 읽기가 압도적으로 많은 경로에 사용됩니다

대표 사용처

사용처변수명읽기 경로쓰기 경로
CPU hotplugcpu_hotplug_lockcpus_read_lock()cpus_write_lock()
파일 시스템 freezesb->s_writerswrite 시스템콜freeze_super()
cgroupcgroup_threadgroup_rwsemfork/exitcgroup_attach_task()
메모리 CGmemcg_oom_lock페이지(Page) 할당OOM 처리

아키텍처별 구현 차이

semaphore의 down()/up()raw_spinlock으로 보호되므로, 성능 차이는 결국 아키텍처별 atomic 연산 구현에 달려 있습니다. rw_semaphore의 fast path는 atomic_long_try_cmpxchg_acquire()를 사용하므로 아키텍처별 차이가 더 두드러집니다.

x86: LOCK CMPXCHG

/* arch/x86 — atomic_long_try_cmpxchg_acquire() 하위 구현 */

/* x86는 LOCK prefix로 bus lock 또는 cache lock을 수행합니다
 * LOCK CMPXCHG는 implicit full barrier — acquire/release 구분이 불필요
 *
 * 특징:
 * - LOCK prefix는 #LOCK 시그널로 캐시 라인 독점 보장
 * - 최신 CPU는 cache lock (캐시 라인이 L1에 있으면 bus lock 불필요)
 * - 비용: ~20 사이클 (비경합), ~100+ 사이클 (경합) */

static __always_inline bool
arch_try_cmpxchg(atomic_t *v, int *old, int new)
{
    return try_cmpxchg(&v->counter, old, new);
    /* → LOCK CMPXCHG [mem], new
     *    ZF=1이면 교환 성공 (old == *mem → *mem = new)
     *    ZF=0이면 실패 (*old = *mem 현재 값으로 갱신) */
}

ARM64: LDXR/STXR (LL/SC)

/* arch/arm64 — atomic_long_try_cmpxchg_acquire() 하위 구현 */

/* ARM64는 LL/SC (Load-Link/Store-Conditional) 패턴을 사용합니다
 * LDAXR: Load-Acquire Exclusive Register (acquire 의미론)
 * STXR:  Store Exclusive Register (조건부 저장)
 *
 * ARMv8.1부터 CAS 명령어 추가 (LSE: Large System Extensions)
 * CASAL: Compare-And-Swap with Acquire-reLease
 *
 * 차이점:
 * - acquire만 필요하면 LDAXR + STXR (release 없음)
 * - full barrier 필요하면 LDAXR + STLXR
 * - LSE CAS는 단일 명령어로 더 효율적 (대형 시스템) */

/* LL/SC 방식 (ARMv8.0) */
1: ldaxr   x0, [x1]        // Load-Acquire Exclusive
   cmp     x0, x2          // old 값과 비교
   b.ne    2f              // 다르면 실패
   stxr    w3, x4, [x1]    // Store Exclusive (조건부)
   cbnz    w3, 1b          // 실패하면 재시도
2:

/* LSE CAS 방식 (ARMv8.1+) */
   casa    x0, x4, [x1]    // Compare-And-Swap Acquire

RISC-V: AMO / LR/SC

/* arch/riscv — atomic 연산 구현 */

/* RISC-V는 두 가지 방식을 제공합니다:
 * 1. LR/SC (Load-Reserved/Store-Conditional) — ARM의 LL/SC와 유사
 * 2. AMO (Atomic Memory Operations) — amoswap, amoadd 등
 *
 * acquire/release 의미론:
 * - .aq 접미사: acquire barrier
 * - .rl 접미사: release barrier
 * - .aqrl: full barrier */

/* LR/SC 방식 */
1: lr.d.aq  a0, (a1)       // Load-Reserved with Acquire
   bne      a0, a2, 2f    // 비교 실패 → 분기
   sc.d     a3, a4, (a1)   // Store-Conditional
   bnez     a3, 1b        // SC 실패 → 재시도
2:

/* AMO 방식 (단순 덧셈 — rwsem READER_BIAS 추가에 사용) */
   amoadd.d.aq  a0, a2, (a1)  // *a1 += a2, old value → a0 (acquire)

아키텍처 비교 표

특성x86ARM64RISC-V
CAS 구현LOCK CMPXCHG (단일 명령)LDAXR/STXR 또는 CAS (LSE)LR/SC 또는 AMO
메모리 모델TSO (강한 순서)약한 순서 (acquire/release 명시)RVWMO (약한 순서)
acquire 비용무료 (TSO 암시)LDAXR의 acquire barrier.aq 접미사
release 비용무료 (TSO 암시)STLXR의 release barrier.rl 접미사
spurious 실패없음LL/SC에서 가능LR/SC에서 가능
CAS 루프 필요No (HW CAS)LL/SC: Yes, LSE: NoLR/SC: Yes, AMO: No
비경합 비용~20 사이클~15 사이클 (LSE)~20 사이클
아키텍처별 atomic CAS 구현 비교 x86 (TSO) LOCK CMPXCHG 단일 명령어, HW CAS implicit full barrier acquire = 무료 (TSO) ~20 cycles spurious 실패: 없음 ARM64 (Weak) LDAXR / STXR LL/SC 루프 또는 LSE CAS 명시적 acquire/release LDAXR = acquire barrier ~15 cycles (LSE) spurious 실패: LL/SC 가능 RISC-V (RVWMO) LR.D / SC.D LR/SC 또는 AMO 명령 .aq / .rl 접미사 amoadd.d.aq = atomic add ~20 cycles spurious 실패: LR/SC 가능
x86는 TSO 메모리 모델 덕분에 acquire가 무료이고, ARM64와 RISC-V는 명시적 배리어를 사용합니다. LSE와 AMO는 LL/SC 루프를 제거하여 대형 시스템에서 유리합니다

벤치마크: semaphore 계열 성능 비교

semaphore, mutex, rw_semaphore, percpu_rw_semaphore의 성능을 비경합(uncontended)과 경합(contended) 조건에서 비교합니다. 테스트는 커널 모듈(Kernel Module)에서 수행하며, ktime_get_ns()로 측정합니다.

벤치마크 커널 모듈

/* 벤치마크 커널 모듈 핵심 부분 */

static void bench_semaphore(void)
{
    struct semaphore sem;
    u64 start, elapsed;
    int i;

    sema_init(&sem, 1);
    start = ktime_get_ns();

    for (i = 0; i < ITERATIONS; i++) {
        down(&sem);
        up(&sem);
    }

    elapsed = ktime_get_ns() - start;
    pr_info("semaphore: %llu ns/op\n", elapsed / ITERATIONS);
}

static void bench_mutex(void)
{
    DEFINE_MUTEX(mtx);
    u64 start, elapsed;
    int i;

    start = ktime_get_ns();

    for (i = 0; i < ITERATIONS; i++) {
        mutex_lock(&mtx);
        mutex_unlock(&mtx);
    }

    elapsed = ktime_get_ns() - start;
    pr_info("mutex: %llu ns/op\n", elapsed / ITERATIONS);
}

비경합(Uncontended) 성능

프리미티브lock+unlock (ns)구현 방식비고
semaphore~45-60raw_spinlock + count 조작항상 spinlock 진입
mutex~15-25atomic CAS fast path비경합 시 spinlock 불필요
rw_semaphore (read)~20-30atomic CAS + owner 설정리더 바이어스 추가
rw_semaphore (write)~20-30atomic CAS + owner 설정mutex와 유사
percpu_rw_semaphore (read)~5-10per-CPU 카운터가장 빠름 (atomic 없음)
percpu_rw_semaphore (write)~5,000,000+synchronize_rcu극도로 느림
참고: semaphore가 mutex보다 느린 핵심 이유는 fast path에서도 raw_spinlock을 반드시 획득해야 하기 때문입니다. mutex는 비경합 시 단일 atomic CAS만으로 완료됩니다. 이것이 semaphore를 mutex로 대체한 성능적 동기입니다.

경합(Contended) 성능 — 8 CPU 동시 접근

프리미티브throughput (ops/sec)avg latency (us)tail latency P99 (us)
semaphore~800K~10~50
mutex~2.5M~3.2~15
rwsem (read-heavy 90:10)~8M~1.0~8
rwsem (write-heavy 10:90)~1.5M~5.3~25
percpu-rwsem (read-heavy)~50M~0.16~0.5
비경합 lock+unlock 비용 비교 (ns, 낮을수록 좋음) 60 45 30 15 0 ~50ns semaphore ~20ns mutex ~25ns rwsem(R) ~25ns rwsem(W) ~7ns percpu(R)
비경합 시 percpu_rw_semaphore 읽기가 가장 빠르고, semaphore가 가장 느립니다. mutex는 spinlock 없이 CAS만으로 2.5배 이상 빠릅니다

선택 가이드

요구사항추천 프리미티브이유
일반 상호 배제mutex최적의 fast path, PI 지원, lockdep
소유권 없는 동기화semaphore다른 컨텍스트에서 up() 가능
읽기 >> 쓰기rw_semaphore읽기 병렬성 + optimistic spinning
읽기 극단적 다수percpu_rw_semaphore읽기 ~5ns, 쓰기는 극히 드물어야
count > 1 리소스 풀semaphore유일하게 counting 지원

메모리 순서와 배리어

semaphore와 rw_semaphore의 정확성은 메모리 순서 보장(Ordering)에 달려 있습니다. down()ACQUIRE 의미론, up()RELEASE 의미론을 구현해야 합니다. 이 섹션에서는 각 프리미티브가 어떻게 메모리 순서를 보장하는지 분석합니다.

ACQUIRE/RELEASE 의미론

/* 메모리 순서 규칙:
 *
 * ACQUIRE (down/lock):
 *   임계 영역 내의 메모리 접근이 lock 획득 이전으로 재배치되지 않음
 *   → lock 이후의 load/store가 lock 이전으로 올라가지 않음
 *
 * RELEASE (up/unlock):
 *   임계 영역 내의 메모리 접근이 lock 해제 이후로 재배치되지 않음
 *   → lock 이전의 load/store가 unlock 이후로 내려가지 않음
 *
 * 조합 효과:
 *   CPU 0: down()  →  critical section  →  up()
 *           ACQUIRE    (순서 보장)          RELEASE
 *   CPU 1:           down()  →  critical section  →  up()
 *                    ACQUIRE    (CPU 0의 변경이 보임)
 */

semaphore의 배리어 분석

/* semaphore의 ACQUIRE/RELEASE는 raw_spinlock이 제공합니다 */

void down(struct semaphore *sem)
{
    raw_spin_lock_irqsave(&sem->lock, flags);
    /* ↑ ACQUIRE barrier 포함 (spin_lock은 ACQUIRE)
     * x86: LOCK 명령의 implicit barrier
     * ARM64: LDAXR의 acquire 의미론 */

    if (sem->count > 0)
        sem->count--;

    raw_spin_unlock_irqrestore(&sem->lock, flags);
    /* ↑ RELEASE barrier 포함 (spin_unlock은 RELEASE)
     * x86: MOV의 store buffer flush (TSO 보장)
     * ARM64: STLXR의 release 의미론 */
}

/* 문제: spin_unlock이 RELEASE를 제공하지만,
 * 이는 semaphore의 RELEASE가 아닌 spinlock의 RELEASE입니다.
 * down()에서 spin_lock이 ACQUIRE를 제공하므로
 * 전체적으로 semaphore의 ACQUIRE는 보장됩니다.
 *
 * 그러나 이 설계는 불필요한 spinlock 오버헤드를 수반합니다.
 * mutex는 atomic_long_try_cmpxchg_acquire()만으로
 * ACQUIRE를 구현하여 spinlock이 필요 없습니다. */

rw_semaphore의 배리어 분석

/* rw_semaphore fast path의 메모리 순서 */

/* 읽기 ACQUIRE:
 * atomic_long_try_cmpxchg_acquire() — 이름에 _acquire가 있음
 * → CAS 성공 시 ACQUIRE barrier 보장
 * → 임계 영역의 읽기가 lock 이전으로 재배치되지 않음 */
if (atomic_long_try_cmpxchg_acquire(
        &sem->count, &tmp,
        tmp + RWSEM_READER_BIAS))
    return 1;

/* 읽기 RELEASE (up_read):
 * atomic_long_add_return_release() — _release 접미사
 * → READER_BIAS를 빼면서 RELEASE barrier 보장 */
tmp = atomic_long_add_return_release(
        -RWSEM_READER_BIAS, &sem->count);

/* 쓰기 ACQUIRE/RELEASE도 동일한 패턴:
 * down_write: _acquire 접미사 CAS
 * up_write:   _release 접미사 atomic 연산 */

smp_mb()가 필요한 경우

/* rwsem에서 명시적 smp_mb()가 필요한 경우:
 * HANDOFF 플래그 설정 시, 다른 CPU가 즉시 볼 수 있어야 합니다 */

atomic_long_or(RWSEM_FLAG_HANDOFF, &sem->count);
/* atomic_long_or는 relaxed 의미론일 수 있음 (아키텍처 의존)
 * 따라서 필요 시 별도의 smp_mb()가 뒤따릅니다 */

/* percpu_rw_semaphore에서도:
 * block 플래그 설정 후 명시적 smp_mb()로
 * 모든 CPU에서 즉시 보이도록 보장 */
atomic_set(&sem->block, 1);
smp_mb();  /* 필수! — block 설정이 read_count 읽기보다 먼저 보여야 */
ACQUIRE/RELEASE 메모리 순서 보장 CPU 0 (Producer) data = 42 (임계 영역 이전) down() — ACQUIRE ── ACQUIRE 배리어 ────────── shared_buf[0] = data (임계 영역) ── RELEASE 배리어 ────────── up() — RELEASE 다른 작업 (임계 영역 이후) happens-before CPU 1 (Consumer) down() — ACQUIRE ── ACQUIRE 배리어 ────────── x = shared_buf[0] (== 42 보장) ── RELEASE 배리어 ────────── up() — RELEASE
CPU 0의 up() RELEASE와 CPU 1의 down() ACQUIRE 사이에 happens-before 관계가 성립합니다. 이로써 CPU 0이 임계 영역에서 쓴 데이터가 CPU 1에서 정확히 보입니다
참고: semaphore의 ACQUIRE/RELEASE는 raw_spinlock이 암시적으로 제공합니다. rw_semaphore는 atomic 연산의 _acquire/_release 접미사로 직접 제공합니다. 두 방식 모두 C11 메모리 모델의 acquire/release 의미론과 동일한 보장을 합니다.

서브시스템: mmap_lock (rw_semaphore)

mmap_lock은 리눅스 커널에서 가장 경합이 심한 rw_semaphore 중 하나입니다. 프로세스의 가상 메모리(Virtual Memory) 영역(VMA)을 보호하며, 페이지 폴트(Page Fault), mmap(), munmap(), /proc/pid/maps 읽기 등 거의 모든 메모리 연산에서 사용됩니다.

mmap_sem에서 mmap_lock으로의 전환

/* mm_struct에서의 mmap_lock 선언 변천사 */

/* v2.6 ~ v5.7: mmap_sem (struct rw_semaphore) */
struct mm_struct {
    struct rw_semaphore mmap_sem;  /* 직접 접근 */
};

/* v5.8+: mmap_lock (래퍼 함수 도입)
 * Michel Lespinasse의 패치 시리즈 (2020)
 * 목적: 향후 lock splitting/replacing을 용이하게 하기 위한 추상화 */
struct mm_struct {
    struct rw_semaphore mmap_lock;  /* 이름 변경 */
};

/* 래퍼 함수 (include/linux/mmap_lock.h) */
static inline void mmap_read_lock(struct mm_struct *mm)
{
    down_read(&mm->mmap_lock);
}
static inline void mmap_write_lock(struct mm_struct *mm)
{
    down_write(&mm->mmap_lock);
}
static inline bool mmap_read_trylock(struct mm_struct *mm)
{
    return down_read_trylock(&mm->mmap_lock);
}

mmap_lock 경합 문제

경합 경로lock 유형빈도영향
페이지 폴트 (handle_mm_fault)read_lock매우 높음멀티스레드 앱 성능 병목(Bottleneck)
mmap() / munmap()write_lock높음malloc/free 시 경합
/proc/pid/maps 읽기read_lock모니터링 시프로덕션에서 주기적 경합
mprotect() / madvise()write_lock중간JIT 컴파일러에서 빈번
brk() (힙 확장)write_lock중간malloc 구현에 따라

per-VMA lock (v6.4+)

/* v6.4+: per-VMA lock으로 mmap_lock 경합 완화
 * Suren Baghdasaryan의 패치 시리즈
 *
 * 핵심 아이디어: 페이지 폴트 시 mmap_lock read 대신
 * 해당 VMA의 per-VMA lock만 획득하여 병렬성 극대화 */

struct vm_area_struct {
    struct rw_semaphore lock;    /* per-VMA lock (v6.4+) */
    /* ... */
};

/* lock_mm_and_find_vma() — per-VMA lock 획득 흐름 */
struct vm_area_struct *
lock_mm_and_find_vma(struct mm_struct *mm,
                     unsigned long addr,
                     struct pt_regs *regs)
{
    struct vm_area_struct *vma;

    /* 1. per-VMA lock 먼저 시도 (빠른 경로) */
    if (!mmap_read_trylock(mm)) {
        /* mmap_lock 없이 VMA 검색 (RCU 보호) */
        vma = find_vma(mm, addr);
        if (vma && vma_start_read(vma))
            return vma;  /* per-VMA lock 성공! */
    }

    /* 2. 실패 시 fallback: mmap_lock read */
    mmap_read_lock(mm);
    vma = find_vma(mm, addr);
    return vma;
}
mmap_lock 진화 타임라인 v2.6 ~ v5.7 mmap_sem 직접 rw_semaphore 접근 전역 경합 병목 v5.8 ~ v6.3 mmap_lock (래퍼) 추상화 도입 lock 교체 준비 v6.4+ per-VMA lock VMA별 세분화 페이지 폴트 병렬화 멀티스레드 페이지 폴트 처리량 비교 mmap_sem: 1x (기준) mmap_lock: 1x (동일) per-VMA: ~4x 향상 per-VMA lock은 서로 다른 VMA에 대한 페이지 폴트를 완전히 병렬화합니다
mmap_lock은 커널에서 가장 경합이 심한 rw_semaphore입니다. per-VMA lock(v6.4+)은 VMA별로 잠금을 세분화하여 멀티스레드 성능을 크게 개선했습니다
참고: per-VMA lock은 mmap_lock을 완전히 대체하지 않습니다. VMA 목록 자체를 수정하는 mmap()/munmap()은 여전히 mmap_write_lock이 필요합니다. per-VMA lock은 페이지 폴트 처리에서만 mmap_lock을 우회합니다.

서브시스템: TTY 세마포어

TTY 서브시스템은 Linux 커널에서 레거시 semaphore가 아직 남아있는 대표적인 영역입니다. Line Discipline(ldisc) 관리에서 소유권 없는 동기화가 필요하기 때문에 mutex로 완전히 대체하기 어렵습니다.

tty_struct의 ldisc_sem

/* include/linux/tty.h — TTY 구조체의 동기화 필드 */

struct tty_struct {
    /* ... */
    struct ld_semaphore ldisc_sem;  /* ldisc 접근 보호 */
    struct tty_ldisc *ldisc;        /* 현재 line discipline */
    struct mutex atomic_write_lock; /* 쓰기 직렬화 */
    /* ... */
};

/* ld_semaphore — TTY 전용 rw_semaphore 변형
 * include/linux/tty_ldisc.h */
struct ld_semaphore {
    atomic_long_t count;      /* reader/writer 카운터 */
    wait_queue_head_t read_wait;  /* 리더 대기 큐 */
    wait_queue_head_t write_wait; /* 라이터 대기 큐 */
};

ldisc_sem 사용 패턴

/* drivers/tty/tty_ldisc.c — ldisc 접근 패턴 */

/* 1. ldisc 읽기 참조 획득 (read path — 매우 빈번)
 * tty_read(), tty_write(), tty_ioctl() 등에서 호출 */
struct tty_ldisc *tty_ldisc_ref_wait(struct tty_struct *tty)
{
    ldsem_down_read(&tty->ldisc_sem, MAX_SCHEDULE_TIMEOUT);
    if (tty->ldisc)
        return tty->ldisc;
    ldsem_up_read(&tty->ldisc_sem);
    return NULL;
}

/* 2. ldisc 교체 (write path — 드물지만 배타적)
 * TIOCSETD ioctl로 line discipline 변경 시 */
int tty_set_ldisc(struct tty_struct *tty, int disc)
{
    /* 모든 리더가 빠져나갈 때까지 대기 */
    tty_ldisc_lock(tty, MAX_SCHEDULE_TIMEOUT);  /* write lock */

    /* 이전 ldisc 제거, 새 ldisc 설치 */
    old_ldisc = tty->ldisc;
    tty->ldisc = new_ldisc;

    tty_ldisc_unlock(tty);  /* write unlock */
}

TTY의 레거시 semaphore 잔존 이유

특성mutex 적합?TTY 요구사항
소유권owner 추적 필수hangup에서 다른 컨텍스트가 해제해야 할 수 있음
타임아웃mutex_lock_interruptible만ldsem_down_read는 시그널+타임아웃 모두 필요
읽기 병렬성mutex는 배타적다수 리더(read/write/ioctl) 병렬 필요
hangup 처리복잡한 소유권 전이tty_hangup에서 비동기적 ldisc 교체
참고: ld_semaphore는 표준 rw_semaphore가 아닌 TTY 전용 구현입니다. 이는 TTY의 특수한 요구사항(hangup 처리, 시그널 기반 인터럽트) 때문입니다. drivers/tty/tty_ldisc.c에 구현되어 있습니다.

TTY의 기타 동기화 프리미티브

/* TTY 서브시스템의 동기화 계층 */

struct tty_struct {
    struct ld_semaphore ldisc_sem;  /* ldisc 읽기/쓰기 보호 */
    struct mutex atomic_write_lock; /* 쓰기 직렬화 (단일 writer) */
    struct mutex legacy_mutex;     /* 레거시 ioctl 보호 */
    spinlock_t flow_lock;          /* 흐름 제어 */
    spinlock_t ctrl_lock;          /* 제어 문자 */
    wait_queue_head_t write_wait;  /* 쓰기 대기 */
    wait_queue_head_t read_wait;   /* 읽기 대기 */
};

/* 잠금 순서 (lockdep 규칙):
 * 1. tty->ldisc_sem (outermost)
 * 2. tty->atomic_write_lock
 * 3. tty->legacy_mutex
 * 4. tty->ctrl_lock / flow_lock (innermost) */

커널 버전별 진화

semaphore 계열 프리미티브는 커널 역사에서 가장 많이 변화한 동기화 메커니즘 중 하나입니다. v1.0의 단순한 구현에서 v6.x의 정교한 per-VMA lock까지, 각 버전별 핵심 변화를 추적합니다.

주요 버전별 변화

버전변화핵심 커밋/패치
v1.0 (1994)struct semaphore 최초 도입아키텍처별 어셈블리(Assembly) 구현
v2.1rw_semaphore 도입읽기/쓰기 분리 잠금
v2.6.16 (2006)mutex 도입, semaphore 마이그레이션 시작Ingo Molnar의 Generic Mutex Subsystem
v2.6.25semaphore 아키텍처 독립 구현kernel/semaphore.c (아키텍처별 코드 제거)
v3.0rwsem에 owner 필드 추가Optimistic Spinning 기반 준비
v3.10rwsem Optimistic Spinning 도입Tim Chen, Waiman Long
v4.7rwsem reader-owned 상태 추적리더 spinning 최적화
v4.15rwsem HANDOFF 메커니즘 도입writer starvation 방지
v4.17rwsem count 비트 필드 재설계64비트 인코딩 (READER_BIAS)
v5.4rwsem 대규모 리팩터링Waiman Long, reader optimistic spinning
v5.8mmap_sem → mmap_lock 래퍼Michel Lespinasse
v5.14percpu_rw_semaphore 최적화rcu_sync 성능 개선
v6.4per-VMA lock 도입Suren Baghdasaryan — 페이지 폴트 병렬화
v6.7per-VMA lock 안정화userfaultfd, mremap 지원 확대
semaphore 계열 프리미티브 진화 타임라인 semaphore v1.0 도입 v2.6.25 범용화 레거시 유지 mutex v2.6.16 도입 주류 rw_semaphore v2.1 도입 v3.10 OSQ v4.15 HANDOFF v5.4 리팩터 percpu-rwsem v3.x 도입 v5.14 최적화 per-VMA lock v6.4 도입 v1.0 1994 v2.6 2003 v3.x 2011 v4.x 2015 v5.x 2019 v6.x 2022 실선: 활발한 개발, 점선: 레거시 유지 모드
semaphore는 v2.6.16 이후 레거시가 되었고, mutex가 주류가 되었습니다. rwsem은 지속적으로 최적화되어 per-VMA lock까지 발전했습니다

핵심 커밋 분석

# v2.6.16: mutex 도입 (Ingo Molnar)
commit 6053ee3b32e3 ("mutex: implement adaptive spinning")
# 영향: 수천 개의 semaphore가 mutex로 변환되기 시작

# v3.10: rwsem optimistic spinning (Tim Chen)
commit 4fc828e24896 ("rwsem: implement optimistic spinning")
# 영향: rwsem 경합 시 성능 2-3배 향상

# v4.15: rwsem HANDOFF (Waiman Long)
commit 617f3ef95177 ("locking/rwsem: Add writer HANDOFF")
# 영향: writer starvation 문제 해결

# v4.17: rwsem count 재설계 (Waiman Long)
commit a15ea1a35483 ("locking/rwsem: Rework rwsem count")
# 영향: 64비트 count로 리더/라이터 상태를 단일 atomic으로 관리

# v5.8: mmap_sem → mmap_lock (Michel Lespinasse)
commit 29a40ace841c ("mmap locking API: initial implementation")
# 영향: per-VMA lock을 위한 추상화 기반 마련

# v6.4: per-VMA lock (Suren Baghdasaryan)
commit 0b57d6ba064f ("mm: introduce per-VMA lock")
# 영향: 멀티스레드 페이지 폴트 처리량 ~75% 향상

실전 디버깅 시나리오

semaphore 관련 버그는 hung_task, lockdep splat, rwsem reader starvation 등의 형태로 나타납니다. 각 시나리오의 증상, 원인, 진단 절차를 분석합니다.

시나리오 1: down()에서 hung_task 발생

# 증상: dmesg에 hung_task 경고
INFO: task kworker/2:1:1234 blocked for more than 120 seconds.
      Not tainted 6.1.0 #1
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:kworker/2:1     state:D stack:12000 pid:1234 ppid:2 flags:0x00004000
Call Trace:
 <TASK>
 __schedule+0x2eb/0x8a0
 schedule+0x5e/0xd0
 schedule_timeout+0x98/0x160
 __down+0x5a/0x90          ← semaphore slow path에서 멈춤
 down+0x4b/0x60
 my_driver_probe+0x123/0x456  ← 드라이버 코드
 ...
 </TASK>
# 진단 절차

# 1. 어떤 semaphore에서 멈췄는지 확인
# __down의 인자(sem)를 crashdump에서 확인
crash> struct semaphore 0xffff8881234abcd0
struct semaphore {
  lock = { ... },
  count = 0,              ← count가 0 → 누군가 보유 중
  wait_list = {
    next = 0xffffc900001bfd30,  ← 대기자가 있음
    prev = 0xffffc900001bfd30
  }
}

# 2. wait_list에서 대기 중인 태스크 확인
crash> list semaphore_waiter.list -s semaphore_waiter.task 0xffffc900001bfd30
  task = 0xffff888123456780   ← 대기 중인 태스크

# 3. up()을 호출해야 할 태스크 추적
# semaphore는 owner가 없으므로 코드 분석이 필요
# → down()을 호출한 코드 경로를 역추적하여 up() 호출 위치 확인
주의: semaphore는 owner 추적이 없으므로 누가 up()을 호출해야 하는지 자동으로 알 수 없습니다. mutex였다면 mutex->owner로 즉시 확인 가능합니다. 이것이 새 코드에서 mutex를 선호하는 디버깅 관점의 이유입니다.

시나리오 2: lockdep splat 분석

# 증상: rwsem 교착 경고
======================================================
WARNING: possible circular locking dependency detected
6.1.0 #1 Not tainted
------------------------------------------------------
modprobe/5678 is trying to acquire lock:
ffff8881aaaaa000 (&mm->mmap_lock){++++}-{3:3}, at: do_mmap+0x123

but task is already holding lock:
ffff8881bbbbb000 (&sb->s_umount){.+.+}-{3:3}, at: freeze_super+0x45

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

-> #1 (&sb->s_umount){.+.+}-{3:3}:
       down_read+0x3e/0x50
       lookup_open+0x234/0x560
       ...

-> #0 (&mm->mmap_lock){++++}-{3:3}:
       down_write+0x3e/0x80
       do_mmap+0x123/0x456

# 해석:
# 경로 A: mmap_lock → s_umount (lookup_open에서 발생)
# 경로 B: s_umount → mmap_lock (freeze_super에서 시도)
# → AB / BA 교착 가능성!
#
# {++++}: 4가지 컨텍스트 모두 허용 (read-recur, read, write-recur, write)
# {.+.+}: read만 허용 (read-recur 불가, write-recur 불가)
# {3:3}: lock class의 subclass

시나리오 3: rwsem reader starvation 진단

# 증상: 시스템 전체 응답 느림, 특정 rwsem에서 다수 태스크 D 상태
# /proc/lock_stat로 경합 확인

$ cat /proc/lock_stat | grep mmap_lock
                          &mm->mmap_lock-R:  123456    234567   ...
                          &mm->mmap_lock-W:       5        10   ...

# W(writer) 경합은 적지만 R(reader) 경합이 극심
# → writer가 HANDOFF를 설정하여 모든 reader를 차단하는 경우

# 진단: ftrace로 rwsem_down_read_slowpath 추적
echo rwsem_down_read_slowpath > /sys/kernel/debug/tracing/set_ftrace_filter
echo rwsem_down_write_slowpath >> /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

# 30초 후 확인
cat /sys/kernel/debug/tracing/trace

# 결과 분석 예시:
# kworker-1234  [002] d..1  1234.567: rwsem_down_write_slowpath <-down_write
# reader-5678   [003] d..1  1234.568: rwsem_down_read_slowpath  <-down_read
# reader-5679   [004] d..1  1234.568: rwsem_down_read_slowpath  <-down_read
# ...
# → writer가 HANDOFF 설정 후 reader들이 slow path로 진입하고 있음

디버깅 체크리스트

증상의심 원인진단 도구해결책
D 상태 태스크 (hung_task)up() 미호출, 교착crash, /proc/pid/stack코드 경로 분석, mutex 전환
lockdep circular dependency잠금 순서 역전lockdep 로그 분석잠금 순서 통일, trylock 사용
rwsem reader starvationwriter HANDOFF 지속/proc/lock_stat, ftrace임계 영역 축소, per-VMA lock
softlockup on down()인터럽트 컨텍스트에서 down() 호출call trace 분석down_trylock() 또는 spinlock으로 변경
percpu-rwsem write 느림synchronize_rcu 비용perf, trace-cmd쓰기 빈도 줄이기, 배치 처리

유용한 디버깅 명령어 모음

# 1. D 상태(TASK_UNINTERRUPTIBLE) 태스크 확인
ps aux | awk '$8 ~ /D/'

# 2. 특정 태스크의 커널 스택 확인
cat /proc/<pid>/stack

# 3. lock_stat으로 경합이 심한 잠금 찾기
echo 1 > /proc/lock_stat
# (부하 발생 후)
sort -k2 -rn /proc/lock_stat | head -20

# 4. perf로 semaphore 경합 프로파일링
perf lock record -a -- sleep 10
perf lock report --sort acquired,contended,avg_wait

# 5. BPF로 down() 지연 시간 측정
bpftrace -e '
kprobe:down {
    @start[tid] = nsecs;
}
kretprobe:down /@start[tid]/ {
    @latency = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}'

# 6. crashdump에서 semaphore 상태 확인
crash> struct semaphore <addr>
crash> list semaphore_waiter.list -s semaphore_waiter.task <wait_list_addr>
참고: semaphore 디버깅이 mutex보다 어려운 이유는 (1) owner 추적 없음, (2) lockdep 지원 제한적, (3) PI(Priority Inheritance) 미지원으로 우선순위 역전(Priority Inversion) 문제를 감지할 수 없기 때문입니다. 가능하면 mutex로 전환하는 것이 디버깅 용이성에서도 유리합니다.

참고 자료

Semaphore의 역사와 커널 구현에 대한 공식 문서 및 참고 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

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