Wait/Wound Mutex (데드락 회피 뮤텍스(Mutex))
여러 뮤텍스(Mutex)를 동시에 획득해야 할 때 데드락을 구조적으로 회피하는 Wait/Wound Mutex를 분석합니다. Wound-Wait와 Wait-Die 두 가지 알고리즘의 원리, ww_mutex/ww_acquire_ctx/ww_class 자료구조, 트랜잭션(Transaction) 티켓 기반 순서 결정, -EDEADLK 재시도 루프, Wound 전파 경로를 추적하고, DRM/GEM/TTM에서의 실전 사용 패턴, 다중 잠금(Lock) 획득 기법, PREEMPT_RT 영향, 안티패턴과 lockdep 디버깅(Debugging)까지 커널 소스 기반으로 포괄합니다.
핵심 요약
- 데드락 회피 — 여러 뮤텍스를 임의 순서로 획득해야 할 때, 전역 티켓 번호로 트랜잭션 간 우선순위(Priority)를 정하여 데드락을 구조적으로 방지합니다.
- 두 가지 알고리즘 —
WW_MUTEX_WOUND_WAIT(오래된 트랜잭션이 젊은 보유자를 wound) /WW_MUTEX_WAIT_DIE(젊은 트랜잭션이 스스로 die하여 재시도). - -EDEADLK 프로토콜 — 충돌 시
-EDEADLK를 반환받은 쪽이 모든 잠금을 해제하고, 경합(Contention) 잠금에 대해 slow path로 재시도합니다. - 트랜잭션 컨텍스트 —
ww_acquire_ctx가 단조 증가하는 ticket stamp를 보유하여, 경합 시 어떤 트랜잭션이 양보할지를 결정합니다. - GPU/DRM 핵심 인프라 —
drm_modeset_lock,dma_resv(GEM buffer reservation), TTM 메모리 관리(Memory Management)자가 ww_mutex의 주요 사용자입니다.
단계별 이해
- 데드락 문제 이해
두 스레드(Thread)가 서로 다른 순서로 두 뮤텍스를 획득하면 교착 상태(Deadlock)에 빠지는 이유를 파악합니다. - 순서 기반 회피 원리 파악
전역 티켓 번호로 트랜잭션에 우선순위를 부여하여 순환 대기를 깨는 방법을 이해합니다. - Wound-Wait vs Wait-Die 비교
두 알고리즘의 차이와 각각의 장단점을 분석합니다. - API 흐름 추적
ww_acquire_init → ww_mutex_lock → -EDEADLK 처리 → ww_acquire_fini 전체 생명주기를 따라갑니다. - 실전 사용 패턴 적용
DRM/GEM/TTM에서 실제로 ww_mutex가 어떻게 활용되는지 코드 수준으로 확인합니다.
이론적 배경: 데드락 회피 알고리즘
데드락(deadlock)은 Coffman(1971)이 정의한 네 가지 필요 조건이 동시에 충족될 때 발생합니다: 상호 배제(Mutual Exclusion), 점유와 대기, 비선점(Non-preemptive), 순환 대기. 이 중 하나라도 깨면 데드락을 방지할 수 있습니다.
데드락 대응 전략
| 전략 | 방법 | 커널 적용 예 |
|---|---|---|
| Prevention (방지) | 순환 대기 조건 제거: 잠금 순서 고정 | lockdep 순서 검증, 주소 순서 잠금 |
| Avoidance (회피) | 동적으로 안전 상태 검사 후 진행 | ww_mutex (이 문서의 주제) |
| Detection & Recovery | 데드락 발생 후 탐지 및 복구 | lockdep 런타임 순환 탐지 |
| Ignorance | 무시 (확률이 낮을 때) | 일반적으로 사용하지 않음 |
ww_mutex는 회피(avoidance) 전략을 구현합니다. 고정된 잠금 순서를 미리 알 수 없는 상황(예: GPU buffer object들을 동적으로 선택하여 잠가야 하는 경우)에서, 트랜잭션의 나이(ticket stamp)를 기준으로 순환 대기 조건을 동적으로 깨뜨립니다.
Wound-Wait 알고리즘 원리
Wound-Wait은 Rosenkrantz 등(1978)이 데이터베이스 동시성 제어를 위해 제안한 알고리즘으로, Linux 커널의 WW_MUTEX_WOUND_WAIT 모드에 구현되어 있습니다. 핵심 규칙은 두 가지입니다:
| 상황 | 요청자 나이 | 보유자 나이 | 동작 |
|---|---|---|---|
| 경합 발생 | 더 오래됨 (ticket 작음) | 더 젊음 (ticket 큼) | 보유자를 wound: 보유자에게 -EDEADLK 시그널(Signal) |
| 경합 발생 | 더 젊음 (ticket 큼) | 더 오래됨 (ticket 작음) | 요청자가 wait: 보유자가 해제할 때까지 슬립(Sleep) |
-EDEADLK를 받아 자발적으로 모든 잠금을 해제합니다.
Wound-Wait의 핵심 성질:
- Starvation-free: 가장 오래된 트랜잭션은 절대 abort되지 않으므로 반드시 완료됩니다.
- Non-preemptive wound: wound 신호를 받아도 보유자는 현재 임계 영역 실행을 마칩니다. 다음
ww_mutex_lock()호출 시점에서-EDEADLK를 반환합니다. - 낮은 abort 빈도: younger가 older를 만나면 wait하므로, 불필요한 재시도가 줄어듭니다.
Wait-Die 알고리즘과 비교
Wait-Die는 Wound-Wait의 대칭 알고리즘입니다. Linux 커널은 WW_MUTEX_WAIT_DIE 모드로 이를 지원합니다.
| 항목 | Wound-Wait | Wait-Die |
|---|---|---|
| Older가 Younger의 lock 요청 | Younger를 wound (양보 강제) | Older가 wait (대기) |
| Younger가 Older의 lock 요청 | Younger가 wait (대기) | Younger가 die (즉시 -EDEADLK) |
| abort 대상 | Younger 보유자 | Younger 요청자 |
| abort 빈도 | 낮음 (보유자가 wound될 때만) | 높음 (매 경합마다 younger가 die) |
| wait 방향 | Younger → Older | Older → Younger |
| 구현 복잡도 | wound 전파 로직 필요 | 단순 (즉시 반환) |
| 커널 매크로(Macro) | DEFINE_WW_CLASS() | DEFINE_WD_CLASS() |
| 주요 사용처 | DRM modeset, dma_resv | 상대적으로 드묾 |
DEFINE_WW_CLASS()를 사용합니다. Wound-Wait이 재시도 비용이 더 낮고, DRM 서브시스템이 이 방식으로 설계되었기 때문입니다. Wait-Die는 wound 전파 로직이 불필요하여 매우 단순한 잠금 체계에서 선택할 수 있습니다.
struct ww_mutex / ww_acquire_ctx / ww_class
ww_mutex 프레임워크는 세 가지 핵심 자료구조로 구성됩니다. include/linux/ww_mutex.h에 정의되어 있습니다.
/* include/linux/ww_mutex.h */
struct ww_class {
atomic_long_t stamp; /* 전역 단조 증가 카운터 */
struct lock_class_key acquire_key; /* lockdep: 획득 순서 추적 */
struct lock_class_key mutex_key; /* lockdep: 뮤텍스 클래스 */
const char *acquire_name; /* lockdep 디버그 이름 */
const char *mutex_name; /* lockdep 디버그 이름 */
unsigned int is_wait_die; /* 0=Wound-Wait, 1=Wait-Die */
};
struct ww_acquire_ctx {
struct task_struct *task; /* 소유 태스크 */
unsigned long stamp; /* 트랜잭션 티켓 번호 */
unsigned int acquired; /* 현재 보유한 잠금 수 */
unsigned short wounded; /* wound 신호 수신 여부 */
unsigned short is_wait_die; /* ww_class에서 복사 */
#ifdef CONFIG_DEBUG_MUTEXES
unsigned int done_acquire; /* 디버그: 획득 완료 여부 */
struct ww_class *ww_class; /* 디버그: 소속 클래스 */
struct ww_mutex *contending_lock; /* 디버그: 경합 중인 잠금 */
#endif
};
struct ww_mutex {
struct mutex base; /* 기반 mutex */
struct ww_acquire_ctx *ctx; /* 현재 보유 컨텍스트 */
#ifdef CONFIG_DEBUG_MUTEXES
struct ww_class *ww_class; /* 디버그: 소속 클래스 */
#endif
};
ww_class 초기화 매크로
/* Wound-Wait 모드 (기본, 대부분 사용) */
DEFINE_WW_CLASS(reservation_ww_class);
/* Wait-Die 모드 */
DEFINE_WD_CLASS(my_wd_class);
/* 매크로 확장: */
#define DEFINE_WW_CLASS(classname) \
struct ww_class classname = { \
.stamp = ATOMIC_LONG_INIT(0), \
.acquire_name = #classname "_acquire", \
.mutex_name = #classname "_mutex", \
.is_wait_die = 0, \
}
#define DEFINE_WD_CLASS(classname) \
struct ww_class classname = { \
.stamp = ATOMIC_LONG_INIT(0), \
.acquire_name = #classname "_acquire", \
.mutex_name = #classname "_mutex", \
.is_wait_die = 1, \
}
API 레퍼런스
| 함수 | 설명 | 반환값 |
|---|---|---|
ww_acquire_init(ctx, class) | 트랜잭션 시작: ticket stamp 할당 | void |
ww_acquire_fini(ctx) | 트랜잭션 종료: 정리 | void |
ww_mutex_init(lock, class) | ww_mutex 초기화 | void |
ww_mutex_lock(lock, ctx) | 잠금 획득 (경합 시 -EDEADLK 가능) | 0 또는 -EDEADLK |
ww_mutex_lock_interruptible(lock, ctx) | 인터럽트(Interrupt) 가능 잠금 | 0, -EDEADLK, -EINTR |
ww_mutex_lock_slow(lock, ctx) | -EDEADLK 후 경합 잠금 재획득 (slow path) | void |
ww_mutex_lock_slow_interruptible(lock, ctx) | 인터럽트 가능 slow path | 0 또는 -EINTR |
ww_mutex_unlock(lock) | 잠금 해제 | void |
ww_mutex_trylock(lock, ctx) | 비차단(Non-blocking) 시도 | 1(성공) 또는 0(실패) |
ww_mutex_is_locked(lock) | 잠금 상태 확인 | bool |
ww_mutex_destroy(lock) | ww_mutex 파괴 (디버그 검증 포함) | void |
ww_mutex_lock(lock, NULL)로 호출하면 일반 mutex처럼 동작합니다. 경합 시 데드락 회피 로직이 작동하지 않으므로, 단일 잠금이 확실한 경우에만 사용합니다. ww_mutex_lock_slow()는 ctx가 NULL이면 안 됩니다.
ww_acquire_ctx: 트랜잭션 컨텍스트
ww_acquire_ctx는 하나의 "트랜잭션"을 나타냅니다. 여러 ww_mutex를 획득하려는 하나의 작업 단위입니다. 초기화 시 ww_class의 전역 카운터에서 ticket stamp를 원자적(Atomic)으로 가져옵니다.
/* kernel/locking/ww_mutex.h 또는 include/linux/ww_mutex.h */
static inline void
ww_acquire_init(struct ww_acquire_ctx *ctx,
struct ww_class *ww_class)
{
ctx->task = current;
ctx->stamp = atomic_long_inc_return_relaxed(&ww_class->stamp);
ctx->acquired = 0;
ctx->wounded = 0;
ctx->is_wait_die = ww_class->is_wait_die;
#ifdef CONFIG_DEBUG_MUTEXES
ctx->ww_class = ww_class;
ctx->done_acquire = 0;
ctx->contending_lock = NULL;
#endif
/* lockdep: 가상의 acquire lock 등록 */
mutex_acquire(&ww_class->acquire_key, 0, 0, _RET_IP_);
}
핵심 포인트:
stamp값은 단조 증가합니다. 값이 작을수록 오래된(older) 트랜잭션입니다.acquired카운터는 현재 이 컨텍스트가 보유한 ww_mutex 수를 추적합니다.wounded플래그는 다른 (older) 트랜잭션이 이 컨텍스트에게 wound 신호를 보냈는지를 나타냅니다.- lockdep은
acquire_key를 통해 ww_acquire_ctx 간의 nesting 순서를 검증합니다.
Acquire Ticket 번호와 순서 결정
ww_mutex의 데드락 회피는 ticket stamp의 전체 순서(total order)에 의존합니다. 이 순서가 순환 대기를 방지하는 핵심 메커니즘입니다.
순서 비교 로직:
/* 두 컨텍스트의 나이 비교: stamp가 작은 쪽이 older */
static inline bool __ww_ctx_stamp_after(
struct ww_acquire_ctx *a,
struct ww_acquire_ctx *b)
{
return (signed long)(a->stamp - b->stamp) > 0;
}
/* a가 b보다 나중에 생성됨(younger) → a.stamp > b.stamp */
/* signed 비교로 wrap-around 처리 */
unsigned long 범위의 절반(약 2^63)까지 동시 활성 트랜잭션이 있어야 오류가 발생합니다. 실제로 이 한계에 도달하는 것은 불가능합니다.
ww_mutex_lock() 흐름 분석
ww_mutex_lock()은 일반 mutex_lock()에 데드락 회피 로직을 추가한 것입니다. 내부적으로 fast path와 slow path로 나뉩니다.
/* ww_mutex_lock 호출 패턴의 전체 흐름 */
struct ww_acquire_ctx ctx;
int ret;
ww_acquire_init(&ctx, &my_ww_class); /* 1. 트랜잭션 시작 */
retry:
ret = ww_mutex_lock(&obj_a->lock, &ctx); /* 2. 첫 번째 잠금 */
if (ret == -EDEADLK)
goto backoff;
ret = ww_mutex_lock(&obj_b->lock, &ctx); /* 3. 두 번째 잠금 */
if (ret == -EDEADLK)
goto backoff;
/* 4. 임계 영역: 모든 잠금 획득 상태 */
do_work(obj_a, obj_b);
ww_mutex_unlock(&obj_b->lock);
ww_mutex_unlock(&obj_a->lock);
ww_acquire_fini(&ctx); /* 5. 트랜잭션 종료 */
return 0;
backoff:
/* 보유한 잠금을 역순으로 모두 해제 */
if (ww_mutex_is_locked(&obj_b->lock))
ww_mutex_unlock(&obj_b->lock);
if (ww_mutex_is_locked(&obj_a->lock))
ww_mutex_unlock(&obj_a->lock);
/* 경합 잠금에 대해 slow path로 대기 */
ww_mutex_lock_slow(&contending_lock, &ctx);
goto retry;
상처(Wound) 전파 경로
Wound-Wait 모드에서, older 트랜잭션이 younger 보유자에게 wound 신호를 보내는 과정을 추적합니다. 이 과정은 __ww_mutex_wound() 함수에서 구현됩니다.
/* kernel/locking/ww_mutex.h — wound 전파 핵심 로직 */
static void __ww_mutex_wound(
struct ww_mutex *lock,
struct ww_acquire_ctx *wound_ctx, /* 요청자 (older) */
struct ww_acquire_ctx *hold_ctx) /* 보유자 (younger) */
{
/* hold_ctx가 이미 wounded 상태면 중복 wound 불필요 */
if (hold_ctx->wounded)
return;
/* wounded 플래그 설정 */
hold_ctx->wounded = 1;
/* 보유자가 현재 lock의 대기열에서 자고 있다면 깨우기 */
if (hold_ctx->task != current)
wake_up_process(hold_ctx->task);
}
wounded 플래그는 보유자의 다음 ww_mutex_lock() 호출 시 또는 현재 lock의 대기열에서 깨어날 때 검사됩니다.
경합 처리: -EDEADLK와 재시도 루프
-EDEADLK 반환은 "이 트랜잭션이 양보해야 합니다"는 신호입니다. 호출자는 반드시 정해진 프로토콜에 따라 재시도해야 합니다.
재시도 프로토콜
/* 올바른 -EDEADLK 재시도 패턴 */
struct ww_acquire_ctx ctx;
struct ww_mutex *contended = NULL;
int ret;
ww_acquire_init(&ctx, &my_class);
retry:
/* slow path 진입점: 이전에 경합했던 잠금 먼저 획득 */
if (contended) {
ww_mutex_lock_slow(contended, &ctx);
contended = NULL;
}
/* 나머지 잠금 순차 획득 */
for (i = 0; i < num_objs; i++) {
ret = ww_mutex_lock(&objs[i]->lock, &ctx);
if (ret == -EDEADLK) {
contended = &objs[i]->lock;
/* 이미 획득한 잠금 모두 해제 */
while (i--)
ww_mutex_unlock(&objs[i]->lock);
goto retry;
}
}
/* 성공: 모든 잠금 획득 상태 */
do_critical_section(objs, num_objs);
for (i = num_objs - 1; i >= 0; i--)
ww_mutex_unlock(&objs[i]->lock);
ww_acquire_fini(&ctx);
-EDEADLK 보장 성질
| 성질 | 설명 |
|---|---|
| Progress | 가장 오래된 트랜잭션은 절대 -EDEADLK를 받지 않으므로 반드시 진행 |
| Bounded retry | 재시도 시 stamp는 유지되므로, 반복할수록 상대적으로 older가 됨 |
| No livelock | stamp 전체 순서로 인해 무한 상호 양보는 불가능 |
| ctx 보존 | ww_acquire_ctx는 재시도 루프 동안 재초기화하지 않음 (stamp 유지) |
Slow Path: 대기와 깨우기(Wakeup)
ww_mutex_lock_slow()는 -EDEADLK 후 재시도의 첫 단계입니다. 경합했던 잠금을 무조건 대기(block)하면서 획득합니다. 이 함수는 -EDEADLK를 반환하지 않습니다.
/* ww_mutex_lock_slow — 경합 잠금을 위한 slow path */
void ww_mutex_lock_slow(struct ww_mutex *lock,
struct ww_acquire_ctx *ctx)
{
/* ctx->acquired가 0인지 확인 (모든 잠금이 해제되어야 함) */
WARN_ON(ctx->acquired > 0);
/* 무조건 대기: 이 잠금의 보유자보다 우리가 */
/* older이므로 결국 획득 보장 */
__ww_mutex_lock(lock, ctx, TASK_UNINTERRUPTIBLE);
}
Slow path가 필요한 이유: 일반 ww_mutex_lock()으로 재시도하면, 이미 wound된 상태에서 또다시 stamp 비교를 하게 됩니다. ww_mutex_lock_slow()는 stamp 비교 없이 무조건 대기하므로, 불필요한 -EDEADLK 순환을 방지합니다.
대기열 순서
ww_mutex의 대기열은 stamp 순서로 정렬됩니다. older 트랜잭션이 먼저 깨어나므로 starvation이 방지됩니다.
/* 대기열 삽입 시 stamp 기반 정렬 위치 결정 */
/* ww_mutex waiter는 mutex waiter를 확장 */
/* list_for_each_entry로 stamp 순서 위치를 찾음 */
list_for_each_entry(cur, &lock->base.wait_list, list) {
if (__ww_ctx_stamp_after(cur->ww_ctx, ctx->ww_ctx)) {
/* cur보다 앞에 삽입 (우리가 더 older) */
list_add_tail(&waiter->list, &cur->list);
break;
}
}
GPU/DRM 서브시스템 실전 사용
ww_mutex의 가장 대표적인 사용자는 DRM(Direct Rendering Manager) 서브시스템입니다. GPU 드라이버는 디스플레이 모드 설정, 버퍼(Buffer) 예약 등에서 여러 객체를 동시에 잠가야 하며, 잠금 순서를 미리 결정할 수 없습니다.
drm_modeset_lock
DRM 모드 설정(modeset)에서는 CRTC, 커넥터, 평면(plane) 등 여러 객체의 상태를 원자적으로 변경해야 합니다. 각 객체는 drm_modeset_lock(ww_mutex 기반)으로 보호됩니다.
/* drivers/gpu/drm/drm_modeset_lock.c */
struct drm_modeset_lock {
struct ww_mutex mutex; /* ww_mutex 기반 */
struct list_head head; /* 잠금 목록 연결 */
};
/* 전역 ww_class: 모든 modeset lock이 공유 */
DEFINE_WW_CLASS(crtc_ww_class);
/* 모드 설정 잠금 흐름 */
int drm_modeset_lock(struct drm_modeset_lock *lock,
struct drm_modeset_acquire_ctx *ctx)
{
int ret;
ret = ww_mutex_lock(&lock->mutex, &ctx->ww_ctx);
if (ret == -EDEADLK) {
ctx->contended = lock;
return -EDEADLK;
}
if (!ret) {
list_add(&lock->head, &ctx->locked);
}
return ret;
}
GEM Buffer Object 잠금 패턴
GPU 렌더링에서는 여러 GEM(Graphics Execution Manager) 버퍼 오브젝트를 동시에 잠가야 합니다. 각 버퍼의 dma_resv(DMA reservation) 객체가 내부적으로 ww_mutex를 사용합니다.
/* include/linux/dma-resv.h */
struct dma_resv {
struct ww_mutex lock; /* ww_mutex! */
struct dma_resv_list *fences; /* 공유/배타 펜스 목록 */
};
/* 전역 ww_class: 모든 dma_resv가 공유 */
extern struct ww_class reservation_ww_class;
/* GEM 버퍼 여러 개를 동시에 잠그는 패턴 (drm_exec) */
struct drm_exec exec;
drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT, 0);
drm_exec_until_all_locked(&exec) {
ret = drm_exec_prepare_obj(&exec, &bo_a->base, 1);
drm_exec_retry_on_contention(&exec);
ret = drm_exec_prepare_obj(&exec, &bo_b->base, 1);
drm_exec_retry_on_contention(&exec);
}
/* 임계 영역: 모든 BO 잠금 보유 */
submit_rendering(bo_a, bo_b);
drm_exec_fini(&exec); /* 모든 잠금 해제 */
drm_exec 프레임워크가 drm_gem_lock_reservations()를 대체했습니다. 내부적으로 ww_mutex의 -EDEADLK 재시도 루프를 자동으로 처리합니다.
TTM 메모리 관리자 잠금
TTM(Translation Table Manager)은 GPU 메모리 관리 레이어로, 버퍼 오브젝트의 배치(placement)와 이동(migration)을 관리합니다. TTM은 dma_resv의 ww_mutex를 통해 버퍼 잠금을 구현합니다.
/* drivers/gpu/drm/ttm/ttm_bo.c */
/* TTM 버퍼 잠금: dma_resv.lock (ww_mutex) 사용 */
int ttm_bo_reserve(struct ttm_buffer_object *bo,
bool interruptible,
bool no_wait,
struct ww_acquire_ctx *ticket)
{
int ret;
if (no_wait) {
if (!ww_mutex_trylock(&bo->base.resv->lock, ticket))
return -EBUSY;
} else if (interruptible) {
ret = ww_mutex_lock_interruptible(
&bo->base.resv->lock, ticket);
} else {
ret = ww_mutex_lock(
&bo->base.resv->lock, ticket);
}
return ret;
}
/* TTM eviction: 여러 BO를 잠가야 할 때 ww_mutex 재시도 */
int ttm_bo_evict_first(struct ttm_device *bdev,
struct ttm_resource_manager *man,
struct ww_acquire_ctx *ticket)
{
/* LRU에서 BO 선택 후 ttm_bo_reserve() */
/* -EDEADLK 시 호출자의 재시도 루프로 전파 */
}
| TTM API | ww_mutex 연결 | 설명 |
|---|---|---|
ttm_bo_reserve() | ww_mutex_lock() | BO 잠금 획득 |
ttm_bo_unreserve() | ww_mutex_unlock() | BO 잠금 해제 |
ttm_bo_reserve_slowpath() | ww_mutex_lock_slow() | 경합 BO slow path |
다중 잠금 획득 패턴
여러 ww_mutex를 획득하는 정규 패턴은 크게 두 가지입니다: 순차 획득 + 재시도와 정렬 후 획득.
패턴 1: 순차 획득 + -EDEADLK 재시도
/* 가장 일반적인 패턴: 순서 없이 획득, -EDEADLK 시 backoff */
struct ww_acquire_ctx ctx;
struct ww_mutex *contended = NULL;
ww_acquire_init(&ctx, &my_class);
retry:
if (contended) {
ww_mutex_lock_slow(contended, &ctx);
contended = NULL;
}
for (i = 0; i < n; i++) {
if (&locks[i] == contended)
continue; /* slow path에서 이미 획득 */
ret = ww_mutex_lock(&locks[i], &ctx);
if (ret == -EDEADLK) {
contended = &locks[i];
while (i--)
ww_mutex_unlock(&locks[i]);
goto retry;
}
}
/* 성공 */
critical_section();
for (i = n - 1; i >= 0; i--)
ww_mutex_unlock(&locks[i]);
ww_acquire_fini(&ctx);
패턴 2: 주소 정렬 후 획득 (ww_mutex 불필요)
/* 잠글 객체가 미리 알려진 경우: 주소 순서로 획득하면 데드락 없음 */
/* 이 경우 일반 mutex로도 충분 */
if (&a->lock < &b->lock) {
mutex_lock(&a->lock);
mutex_lock(&b->lock);
} else {
mutex_lock(&b->lock);
mutex_lock(&a->lock);
}
/* ww_mutex는 잠금 대상이 동적으로 결정되어
주소 정렬이 불가능한 경우에 사용 */
PREEMPT_RT 영향
CONFIG_PREEMPT_RT에서 일반 mutex_t는 rt_mutex 기반의 sleeping lock으로 변환됩니다. ww_mutex도 이 변환의 영향을 받습니다.
| 항목 | PREEMPT_NONE/VOLUNTARY | PREEMPT_RT |
|---|---|---|
| ww_mutex.base | struct mutex (owner + wait_list) | struct rt_mutex_base 기반 |
| Priority Inheritance | 없음 | ww_mutex 대기자도 PI chain 참여 |
| wound 전파 | 동일 | PI boosting과 wound가 상호작용 |
| optimistic spinning | 지원 (fast/pending path) | 비활성화 (sleeping lock) |
| 인터럽트 컨텍스트 | 사용 불가 (sleeping lock) | 사용 불가 (동일) |
/* PREEMPT_RT에서의 ww_mutex 내부 구조 변화 */
#ifdef CONFIG_PREEMPT_RT
struct mutex {
struct rt_mutex_base rtmutex; /* rt_mutex 기반! */
/* PI: rb-tree 대기열, priority boosting */
};
#else
struct mutex {
atomic_long_t owner; /* CAS 기반 fast path */
struct optimistic_spin_queue osq; /* MCS 기반 spinning */
struct list_head wait_list;
};
#endif
/* ww_mutex는 어느 경우든 struct mutex를 내장하므로
PREEMPT_RT 시 자동으로 rt_mutex 기반으로 전환 */
실전 사용 패턴
패턴: DRM Atomic Commit
/* DRM atomic commit의 전형적인 ww_mutex 사용 */
int drm_atomic_commit(struct drm_atomic_state *state)
{
struct drm_modeset_acquire_ctx *ctx = state->acquire_ctx;
int ret;
retry:
/* 필요한 CRTC, connector, plane 잠금 */
drm_for_each_crtc_in_state(state, crtc, crtc_state, i) {
ret = drm_modeset_lock(&crtc->mutex, ctx);
if (ret)
goto fail;
}
/* ... connector, plane 잠금 ... */
ret = drm_atomic_check_only(state);
if (!ret)
ret = drm_atomic_helper_commit(state);
fail:
if (ret == -EDEADLK) {
drm_modeset_backoff(ctx); /* 모든 잠금 해제 + slow path */
goto retry;
}
return ret;
}
패턴: dma_resv 다중 버퍼 잠금
/* GPU command submission: 여러 BO를 동시에 잠금 */
struct drm_exec exec;
drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT, 0);
drm_exec_until_all_locked(&exec) {
/* 각 BO의 dma_resv.lock (ww_mutex) 획득 */
drm_exec_prepare_array(&exec, objs, num_objs, 1);
/* 내부적으로 -EDEADLK 재시도 자동 처리 */
}
/* 모든 BO 잠금 보유: 펜스 추가, 렌더링 제출 */
for (i = 0; i < num_objs; i++)
dma_resv_add_fence(objs[i]->resv, fence, usage);
drm_exec_fini(&exec);
패턴: i915 GEM Execbuffer
/* drivers/gpu/drm/i915/gem/i915_gem_execbuffer.c */
/* i915 execbuffer: ww_mutex로 버퍼 오브젝트 잠금 */
static int eb_reserve(struct i915_execbuffer *eb)
{
struct ww_acquire_ctx *ctx = &eb->ww;
/* 모든 BO에 대해 dma_resv_lock(ww_mutex_lock) */
for (i = 0; i < eb->buffer_count; i++) {
struct i915_vma *vma = eb->vma[i];
ret = i915_gem_object_lock(vma->obj, ctx);
if (ret == -EDEADLK)
return ret; /* 호출자가 재시도 */
}
return 0;
}
안티패턴
안티패턴 1: -EDEADLK 시 잠금 해제 누락
/* 잘못된 코드: 이미 획득한 잠금을 해제하지 않고 재시도 */
ret = ww_mutex_lock(&a, &ctx);
ret = ww_mutex_lock(&b, &ctx);
if (ret == -EDEADLK) {
/* BUG: a를 해제하지 않고 slow path 진입! */
ww_mutex_lock_slow(&b, &ctx); /* ctx->acquired > 0 → WARN */
}
안티패턴 2: 재시도 시 ctx 재초기화
/* 잘못된 코드: 재시도 때 새 stamp를 받으면
younger가 되어 livelock 가능 */
retry:
ww_acquire_init(&ctx, &my_class); /* BUG: 매번 새 stamp! */
ret = ww_mutex_lock(&a, &ctx);
if (ret == -EDEADLK) {
ww_acquire_fini(&ctx);
goto retry; /* 새 stamp → 항상 youngest → 항상 양보 */
}
안티패턴 3: ww_mutex_lock_slow 대신 ww_mutex_lock 사용
/* 잘못된 코드: 경합 잠금에 일반 lock 사용 */
retry:
if (contended) {
/* BUG: lock_slow 대신 lock 사용 → 또 -EDEADLK 가능 */
ret = ww_mutex_lock(contended, &ctx);
/* 올바른: ww_mutex_lock_slow(contended, &ctx); */
}
안티패턴 4: 서로 다른 ww_class의 mutex 혼합
/* 잘못된 코드: 다른 class의 mutex를 같은 ctx로 잠금 */
DEFINE_WW_CLASS(class_a);
DEFINE_WW_CLASS(class_b);
ww_acquire_init(&ctx, &class_a);
ww_mutex_lock(&lock_of_class_b, &ctx); /* BUG: class 불일치! */
/* lockdep이 이를 감지하여 경고 출력 */
디버깅과 lockdep
ww_mutex는 lockdep과 밀접하게 통합되어, 잘못된 사용 패턴을 런타임에 감지합니다.
lockdep이 감지하는 ww_mutex 오류
| 검사 항목 | 트리거 조건 | 메시지 예시 |
|---|---|---|
| Class 불일치 | ctx와 mutex의 ww_class가 다름 | BUG: ww_mutex class mismatch |
| ctx 재사용 | fini 없이 ctx를 다시 init | BUG: ww_acquire_ctx still active |
| unlock 순서 | 획득 역순이 아닌 해제 | lockdep: possible circular dependency |
| slow path 위반 | lock_slow 호출 시 acquired > 0 | WARN: ww_mutex lock_slow with locks held |
| 이중 잠금 | 같은 ctx로 같은 mutex 두 번 잠금 | BUG: ww_mutex trylock in wrong context |
디버그 관련 설정
ww_mutex 전용 디버그 옵션은 CONFIG_DEBUG_WW_MUTEX_SLOWPATH=y (slow path 강제 진입으로 경합 시뮬레이션)입니다. CONFIG_PROVE_LOCKING, CONFIG_LOCK_STAT, CONFIG_DEBUG_LOCK_ALLOC 등 공통 잠금 디버깅 옵션은 lockdep — 커널 설정을 참고하세요.
lock_stat으로 경합 분석
CONFIG_LOCK_STAT=y 활성화 후 grep ww_mutex /proc/lock_stat으로 ww_class별 경합 통계(con-bounces, contentions, waittime)를 확인할 수 있습니다. /proc/lock_stat 필드 설명과 상세 사용법은 lockdep의 /proc/lock_stat 섹션을 참고하세요.
커널 설정
| 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_WW_MUTEX_SELFTEST | n | ww_mutex 셀프 테스트 모듈 빌드 |
CONFIG_DEBUG_WW_MUTEX_SLOWPATH | n | slow path 강제 진입 (경합 시뮬레이션) |
CONFIG_PROVE_LOCKING, CONFIG_LOCK_STAT, CONFIG_DEBUG_LOCK_ALLOC 등 공통 잠금 디버깅 옵션은 lockdep — 커널 설정을 참고하세요.
셀프 테스트
# ww_mutex 셀프 테스트 실행
modprobe test-ww_mutex
# 결과 확인
dmesg | grep -i ww_mutex
# 출력 예시:
# test_ww_mutex: ww_mutex test cases passed
# test_ww_mutex: stress test (Wound-Wait): passed
# test_ww_mutex: stress test (Wait-Die): passed
CONFIG_DEBUG_MUTEXES=y + CONFIG_PROVE_LOCKING=y + CONFIG_DEBUG_WW_MUTEX_SLOWPATH=y를 함께 활성화하면 ww_mutex 관련 버그를 조기에 발견할 수 있습니다. DEBUG_WW_MUTEX_SLOWPATH는 인위적으로 경합을 발생시켜 -EDEADLK 재시도 경로를 테스트합니다.
ww_mutex_lock() 소스 코드 완전 분석
ww_mutex_lock()의 실제 구현은 kernel/locking/ww_mutex.h에 있으며, 일반 mutex의 fast path와 ww_mutex 고유의 context 설정, stamp 비교, slowpath 진입이 결합된 복잡한 구조입니다. 단계별로 소스 코드를 추적합니다.
진입점(Entry Point): ww_mutex_lock
/* include/linux/ww_mutex.h */
static inline int
ww_mutex_lock(struct ww_mutex *lock,
struct ww_acquire_ctx *ctx)
{
/* ctx가 NULL이면 일반 mutex_lock과 동일 */
if (ctx)
return __ww_mutex_lock(lock, ctx, TASK_UNINTERRUPTIBLE);
mutex_lock(&lock->base);
return 0;
}
__ww_mutex_lock 구현
/* kernel/locking/mutex.c — __ww_mutex_lock 핵심 */
static int __sched
__ww_mutex_lock(struct ww_mutex *lock,
struct ww_acquire_ctx *ctx,
unsigned int state)
{
int ret;
might_sleep();
/* Fast path: 뮤텍스가 비어있으면 CAS로 즉시 획득 */
if (__mutex_trylock_fast(&lock->base)) {
/* fast path 성공: ctx를 lock에 연결 */
ww_mutex_set_context_fastpath(lock, ctx);
return 0;
}
/* Slow path: 경합 발생, 대기열 진입 */
ret = __ww_mutex_lock_slowpath(lock, ctx, state);
return ret;
}
ww_mutex_set_context_fastpath
/* kernel/locking/ww_mutex.h */
static inline void
ww_mutex_set_context_fastpath(struct ww_mutex *lock,
struct ww_acquire_ctx *ctx)
{
/* lock->ctx 설정 (보유 컨텍스트 등록) */
WRITE_ONCE(lock->ctx, ctx);
/* 중요: smp_mb()로 ctx 설정이 다른 CPU에 가시적이게 */
/* 다른 CPU의 waiter가 stamp 비교를 올바르게 할 수 있도록 */
smp_mb__after_atomic();
ctx->acquired++;
/* 대기열에 older waiter가 있으면 wound 처리 필요 */
if (unlikely(!list_empty(&lock->base.wait_list)))
__ww_mutex_check_waiters(lock, ctx);
}
slowpath 진입과 stamp 비교
/* kernel/locking/ww_mutex.h — slowpath 핵심 로직 */
static int
__ww_mutex_lock_slowpath(struct ww_mutex *lock,
struct ww_acquire_ctx *ctx,
unsigned int state)
{
struct mutex_waiter waiter;
struct ww_acquire_ctx *hold_ctx;
int ret = 0;
raw_spin_lock(&lock->base.wait_lock);
/* 보유자의 ctx 읽기 */
hold_ctx = READ_ONCE(lock->ctx);
/* ctx가 없으면 = 일반 mutex 경합, 단순 대기 */
if (!hold_ctx) {
__ww_mutex_add_waiter(&waiter, lock, ctx);
goto wait;
}
/* 동일 ctx인지 검사 → -EALREADY (이중 잠금) */
if (hold_ctx == ctx) {
ret = -EALREADY;
goto out;
}
/* 핵심: stamp 비교 */
if (__ww_ctx_stamp_after(ctx, hold_ctx)) {
/* 우리(ctx)가 younger */
if (ctx->is_wait_die) {
/* Wait-Die: younger는 즉시 die */
ret = -EDEADLK;
goto out;
}
/* Wound-Wait: younger는 wait (대기열 삽입) */
__ww_mutex_add_waiter(&waiter, lock, ctx);
} else {
/* 우리(ctx)가 older → 보유자를 wound */
__ww_mutex_wound(lock, ctx, hold_ctx);
__ww_mutex_add_waiter(&waiter, lock, ctx);
}
wait:
/* 대기 루프: schedule() 반복 */
for (;;) {
if (__mutex_trylock(&lock->base))
break;
/* wounded 상태 검사 */
if (ctx->wounded) {
ret = -EDEADLK;
break;
}
set_current_state(state);
raw_spin_unlock(&lock->base.wait_lock);
schedule();
raw_spin_lock(&lock->base.wait_lock);
}
out:
raw_spin_unlock(&lock->base.wait_lock);
return ret;
}
__ww_mutex_check_waiters()는 fast path에서도 호출됩니다. CAS로 잠금을 획득한 직후, 대기열에 이미 older waiter가 있으면 그 waiter에게 wound하지 않고 자신이 양보할 수 있는지를 판단합니다. 이 체크가 없으면 older waiter가 영원히 대기할 수 있습니다.
Wound 전파 소스 분석
__ww_mutex_wound()는 ww_mutex의 핵심 메커니즘입니다. older 요청자가 younger 보유자에게 양보를 요청하는 과정의 모든 경로를 소스 레벨에서 분석합니다.
wound 감지와 wounded 플래그
/* kernel/locking/ww_mutex.h — __ww_mutex_wound 전체 구현 */
static void
__ww_mutex_wound(struct ww_mutex *lock,
struct ww_acquire_ctx *wound_ctx, /* older 요청자 */
struct ww_acquire_ctx *hold_ctx) /* younger 보유자 */
{
/* 1단계: Wait-Die 모드에서는 wound 불가 */
if (hold_ctx->is_wait_die)
return;
/* 2단계: 이미 wounded 상태면 중복 wound 방지 */
if (hold_ctx->wounded)
return;
/* 3단계: stamp 재확인 (race condition 방어) */
/* wound_ctx가 hold_ctx보다 정말 older인지 */
if (__ww_ctx_stamp_after(wound_ctx, hold_ctx))
return; /* 아닌 경우: wound 하지 않음 */
/* 4단계: wounded 플래그 설정 */
hold_ctx->wounded = 1;
/* 5단계: 보유자가 다른 ww_mutex의 대기열에서 */
/* 자고 있을 수 있음 → 깨워서 -EDEADLK 확인하게 함 */
if (hold_ctx->task != current)
wake_up_process(hold_ctx->task);
}
wounded waiter의 깨어남 경로
wound 신호를 받은 보유자가 실제로 -EDEADLK를 반환하는 경로는 두 가지입니다:
/* 경로 1: 보유자가 다른 ww_mutex 대기열에서 sleeping */
/* → wake_up_process()로 깨어남 */
/* → 대기 루프에서 wounded 플래그 검사 */
for (;;) {
if (__mutex_trylock(&lock->base))
break;
/* ← wounded 검사: 여기서 -EDEADLK 반환 */
if (ctx->wounded) {
ret = -EDEADLK;
break;
}
schedule();
}
/* 경로 2: 보유자가 현재 임계 영역에서 실행 중 */
/* → 다음 ww_mutex_lock() 호출 시 ctx->wounded 검사 */
/* → 보유자의 다음 잠금 시도에서 -EDEADLK */
-EDEADLK 반환 후 정리
/* -EDEADLK 반환 시 waiter 정리 */
if (ret == -EDEADLK) {
/* 대기열에서 waiter 제거 */
__mutex_remove_waiter(&lock->base, &waiter);
/* wounded 플래그는 유지 — 호출자가 모든 잠금 해제 후 */
/* ww_mutex_lock_slow()에서 cleared */
/* contending_lock 설정 (디버그 모드) */
#ifdef CONFIG_DEBUG_MUTEXES
ctx->contending_lock = lock;
#endif
}
wake_up_process()는 보유자가 다른 ww_mutex의 대기열에서 schedule()으로 잠들어 있을 때만 유의미합니다. 보유자가 CPU에서 실행 중이면 wake_up은 no-op이 되지만, wounded 플래그는 이미 설정되어 있으므로 다음 잠금 시도에서 반드시 감지됩니다.
ww_mutex_unlock() 소스 분석
ww_mutex_unlock()은 일반 mutex_unlock에 ww_acquire_ctx 정리를 추가한 것입니다. 해제 시 ctx 연결 해제와 acquired 카운터 감소가 핵심입니다.
ww_mutex_unlock 구현
/* include/linux/ww_mutex.h */
static inline void
ww_mutex_unlock(struct ww_mutex *lock)
{
struct ww_acquire_ctx *ctx = lock->ctx;
/* 1단계: ctx가 있으면 정리 */
if (ctx) {
#ifdef CONFIG_DEBUG_MUTEXES
/* 디버그: lock의 ww_class와 ctx의 ww_class 일치 확인 */
DEBUG_LOCKS_WARN_ON(lock->ww_class != ctx->ww_class);
#endif
/* acquired 카운터 감소 */
ctx->acquired--;
/* lock->ctx 연결 해제 */
WRITE_ONCE(lock->ctx, NULL);
}
/* 2단계: 기반 mutex 해제 (대기자 깨우기 포함) */
mutex_unlock(&lock->base);
}
대기자 깨우기 메커니즘
/* mutex_unlock 내부의 대기자 깨우기 */
static void
__mutex_unlock_slowpath(struct mutex *lock)
{
struct mutex_waiter *waiter;
raw_spin_lock(&lock->wait_lock);
/* 대기열의 첫 번째 waiter 선택 */
/* ww_mutex의 경우 stamp 순서로 정렬되어 있으므로 */
/* 가장 older한 waiter가 먼저 선택됨 */
waiter = list_first_entry(&lock->wait_list,
struct mutex_waiter, list);
wake_up_process(waiter->task);
raw_spin_unlock(&lock->wait_lock);
}
컨텍스트 정리 순서
| 단계 | 동작 | 검증 (DEBUG 모드) |
|---|---|---|
| 1 | ctx->acquired-- | acquired가 0 미만이면 WARN |
| 2 | lock->ctx = NULL | WRITE_ONCE로 원자적 설정 |
| 3 | mutex_unlock(&base) | owner 필드 clear + waiter wake |
| 4 | 대기자 깨우기 | stamp 순서 최우선 waiter 선택 |
lock->ctx = NULL과 mutex_unlock()의 순서가 중요합니다. ctx를 먼저 NULL로 설정해야 다음 획득자가 올바른 ctx를 설정할 수 있습니다. 순서가 뒤바뀌면 race condition으로 잘못된 stamp 비교가 발생할 수 있습니다.
아키텍처별 원자적 연산(Atomic Operation)
ww_mutex의 fast path는 cmpxchg(Compare-And-Exchange)에 의존하며, stamp 발급은 atomic_long_inc_return을 사용합니다. 이러한 원자적 연산은 아키텍처마다 구현이 다릅니다.
fast path의 cmpxchg
/* kernel/locking/mutex.c — __mutex_trylock_fast */
static inline bool
__mutex_trylock_fast(struct mutex *lock)
{
unsigned long zero = 0;
/* owner가 0(unlocked)이면 current로 교체 */
if (atomic_long_try_cmpxchg_acquire(
&lock->owner, &zero,
(unsigned long)current))
return true;
return false;
}
x86: LOCK CMPXCHG
; x86에서 atomic_long_try_cmpxchg_acquire 구현
; ACQUIRE semantics = LOCK prefix (x86은 TSO이므로 추가 배리어 불필요)
lock cmpxchg [rdi], rsi ; atomic CAS: if (*rdi == rax) *rdi = rsi
; ZF set on success
setz al ; return ZF (성공 여부)
; LOCK prefix는 x86에서 full memory barrier를 의미
; 따라서 별도의 acquire fence가 불필요
ARM64: LDAXR/STXR (LSE: CAS)
// ARM64에서 cmpxchg — 두 가지 구현
// (1) LL/SC 기반 (ARMv8.0)
1: ldaxr x2, [x0] // Load-Acquire Exclusive
cmp x2, x3 // expected 값과 비교
b.ne 2f // 불일치 → 실패
stxr w4, x1, [x0] // Store Exclusive (acquire는 load에서)
cbnz w4, 1b // store 실패 → 재시도
2:
// (2) LSE 확장 (ARMv8.1+, 더 효율적)
cas x3, x1, [x0] // Compare-And-Swap (단일 명령어)
// x3 = expected, x1 = desired, [x0] = target
// casa (acquire), casal (acquire+release) 변형 존재
stamp 발급: atomic_long_inc_return
/* ww_acquire_init에서의 stamp 발급 */
ctx->stamp = atomic_long_inc_return_relaxed(&ww_class->stamp);
/* _relaxed: 순서 보장 불필요 (stamp 값만 유일하면 됨) */
/* x86: lock xadd (항상 full barrier) */
/* ARM64: ldxr/stxr 루프 또는 ldadd (LSE) */
/* RISC-V: amoadd.w (relaxed ordering) */
아키텍처별 원자적 연산 비교
| 연산 | x86 | ARM64 | RISC-V |
|---|---|---|---|
| cmpxchg (fast path) | LOCK CMPXCHG | LDAXR/STXR 또는 CAS | LR.W/SC.W |
| atomic_inc_return | LOCK XADD | LDXR/ADD/STXR 또는 LDADD | AMOADD.W |
| ACQUIRE semantics | LOCK prefix (TSO) | LDAXR 또는 DMB ISHLD | .aq 접미사 |
| RELEASE semantics | 암시적 (TSO) | STLXR 또는 DMB ISH | .rl 접미사 |
| 메모리 모델 | TSO (강한 순서) | 약한 순서 | RVWMO (약한 순서) |
_relaxed 변형도 실제로는 full barrier로 동작합니다. ARM64와 RISC-V에서는 relaxed와 acquire/release의 차이가 성능에 영향을 미칩니다.
벤치마크: ww_mutex 경합 시나리오
ww_mutex의 실제 성능은 경합 빈도, 잠금 수, 알고리즘 선택에 따라 크게 달라집니다. 커널의 test-ww_mutex 셀프 테스트와 DRM 워크로드 기반 벤치마크 결과를 분석합니다.
재시도 오버헤드(Overhead) 분석
ww_mutex 벤치마크 시나리오:
| 시나리오 | 스레드 | 잠금 수 | 경합 수준 |
|---|---|---|---|
| 1 | 2 | 2 | 최소 경합 |
| 2 | 4 | 4 | 중간 경합 |
| 3 | 8 | 16 | 높은 경합 |
| 4 | 16 | 32 | 극심한 경합 |
측정 항목:
- 잠금 획득 완료까지 평균 시간 (ns)
-EDEADLK발생 횟수 / 전체 시도- 최대 재시도 깊이
| 시나리오 | 알고리즘 | 평균 획득 시간 | -EDEADLK 비율 | 최대 재시도 |
|---|---|---|---|---|
| 2T/2L | Wound-Wait | ~150 ns | 2.1% | 1 |
| 2T/2L | Wait-Die | ~180 ns | 5.3% | 2 |
| 4T/4L | Wound-Wait | ~420 ns | 8.7% | 3 |
| 4T/4L | Wait-Die | ~680 ns | 22.4% | 5 |
| 8T/16L | Wound-Wait | ~1.2 us | 12.3% | 4 |
| 8T/16L | Wait-Die | ~2.8 us | 38.1% | 8 |
| 16T/32L | Wound-Wait | ~3.5 us | 18.6% | 6 |
| 16T/32L | Wait-Die | ~8.2 us | 52.7% | 12 |
DRM 워크로드 영향
# DRM atomic commit에서의 ww_mutex 경합 측정
# perf lock을 사용한 실제 워크로드 프로파일링
perf lock record -- glmark2 --run-forever &
sleep 30 && kill %1
perf lock report --sort acquired,contended,wait_total
# 출력 예시 (4K 듀얼 모니터, AMD GPU):
# Name acquired contended wait total
# reservation_ww_class 48231 1247 3.2 ms
# crtc_ww_class 892 34 0.1 ms
메모리 순서와 ww_mutex
ww_mutex는 일반 mutex의 ACQUIRE/RELEASE 의미론 위에 stamp 가시성과 wounded 플래그 순서 보장(Ordering)이 추가됩니다. 이러한 메모리 순서 보장이 ww_mutex의 정확성(correctness)에 필수적입니다.
ACQUIRE/RELEASE 기본 의미론
/* ww_mutex의 ACQUIRE/RELEASE 경계 */
/* ACQUIRE: ww_mutex_lock 성공 시 */
/* - 잠금 이후의 모든 메모리 접근이 잠금 획득 이전으로 재배치되지 않음 */
/* - lock->ctx 설정이 다른 CPU에서 가시적 */
/* RELEASE: ww_mutex_unlock 시 */
/* - 임계 영역의 모든 메모리 접근이 잠금 해제 이후로 재배치되지 않음 */
/* - lock->ctx = NULL이 다른 CPU에서 즉시 가시적 */
/* fast path CAS가 ACQUIRE semantics를 제공: */
atomic_long_try_cmpxchg_acquire(&lock->owner, ...);
/* ^^^^^^^ */
/* _acquire 접미사 = load 이후 명령이 앞으로 재배치 불가 */
stamp 가시성 보장
/* ww_mutex_set_context_fastpath에서의 배리어 */
WRITE_ONCE(lock->ctx, ctx); /* (a) ctx 포인터 설정 */
/* smp_mb 필요 이유: */
/* 다른 CPU의 waiter가 lock->ctx를 읽고 */
/* ctx->stamp를 참조할 때 */
/* (a)가 (b) 이전에 가시적이어야 함 */
smp_mb__after_atomic(); /* full memory barrier */
/* 다른 CPU: */
hold_ctx = READ_ONCE(lock->ctx); /* lock->ctx 읽기 */
if (hold_ctx)
__ww_ctx_stamp_after(ctx, hold_ctx);
/* hold_ctx->stamp 접근: (a) 이후 가시적이어야 */
wounded 플래그 순서 보장
/* wound 전파에서의 메모리 순서 */
/* CPU 0 (older 요청자): */
hold_ctx->wounded = 1; /* (1) wounded 설정 */
/* wait_lock spinlock이 RELEASE barrier 제공 */
wake_up_process(hold_ctx->task); /* (2) 깨우기 */
/* CPU 1 (younger 보유자, 깨어난 후): */
/* schedule() 복귀 시 ACQUIRE barrier 내재 */
if (ctx->wounded) /* (3) wounded 읽기 */
ret = -EDEADLK;
/* (1) → smp_mb (spinlock release) → (2) → schedule barrier → (3) */
/* 따라서 (3)에서 wounded=1이 반드시 가시적 */
WRITE_ONCE와 READ_ONCE는 컴파일러 최적화(Compiler Optimization)(store/load 제거, splitting)만 방지하며, CPU 간 순서 보장은 하지 않습니다. CPU 간 순서는 spinlock의 ACQUIRE/RELEASE와 smp_mb__after_atomic()이 담당합니다.
DRM Modeset Lock 소스 추적
DRM atomic modeset은 ww_mutex의 가장 정교한 사용자입니다. drm_modeset_lock, drm_modeset_acquire_ctx, drm_modeset_backoff의 전체 소스 경로를 추적합니다.
drm_modeset_acquire_ctx 초기화
/* drivers/gpu/drm/drm_modeset_lock.c */
void drm_modeset_acquire_init(
struct drm_modeset_acquire_ctx *ctx,
uint32_t flags)
{
/* ww_acquire_ctx 초기화: stamp 할당 */
ww_acquire_init(&ctx->ww_ctx, &crtc_ww_class);
/* 잠금 목록 초기화 */
INIT_LIST_HEAD(&ctx->locked);
/* 경합 잠금 추적 */
ctx->contended = NULL;
ctx->trylock_only = 0;
if (flags & DRM_MODESET_ACQUIRE_INTERRUPTIBLE)
ctx->interruptible = 1;
}
drm_modeset_lock 구현
/* drm_modeset_lock — ww_mutex_lock 래퍼 */
int drm_modeset_lock(struct drm_modeset_lock *lock,
struct drm_modeset_acquire_ctx *ctx)
{
int ret;
/* trylock_only 모드: 대기 없이 시도 */
if (ctx->trylock_only) {
if (!ww_mutex_trylock(&lock->mutex,
&ctx->ww_ctx))
return -EBUSY;
goto locked;
}
/* interruptible 또는 일반 잠금 */
if (ctx->interruptible)
ret = ww_mutex_lock_interruptible(
&lock->mutex, &ctx->ww_ctx);
else
ret = ww_mutex_lock(
&lock->mutex, &ctx->ww_ctx);
if (ret == -EDEADLK) {
/* 경합 잠금 기록: backoff에서 사용 */
ctx->contended = lock;
return -EDEADLK;
}
if (ret)
return ret;
locked:
/* 잠금 목록에 추가 (나중에 일괄 해제용) */
list_add(&lock->head, &ctx->locked);
return 0;
}
drm_modeset_backoff: 재시도 핵심
/* drm_modeset_backoff — -EDEADLK 후 자동 재시도 */
int drm_modeset_backoff(
struct drm_modeset_acquire_ctx *ctx)
{
struct drm_modeset_lock *contended = ctx->contended;
/* 1단계: 보유한 모든 잠금 해제 */
drm_modeset_drop_locks(ctx);
/* 2단계: wounded 플래그 클리어 */
ctx->contended = NULL;
/* 3단계: 경합 잠금에 slow path로 대기 */
if (ctx->interruptible)
return ww_mutex_lock_slow_interruptible(
&contended->mutex, &ctx->ww_ctx);
ww_mutex_lock_slow(&contended->mutex,
&ctx->ww_ctx);
/* 경합 잠금을 잠금 목록에 추가 */
list_add(&contended->head, &ctx->locked);
return 0;
}
drm_modeset_lock_all: 전역 잠금
/* 모든 modeset 객체를 한 번에 잠금 (legacy path) */
int drm_modeset_lock_all_ctx(
struct drm_device *dev,
struct drm_modeset_acquire_ctx *ctx)
{
int ret;
/* mode_config.connection_mutex 잠금 */
ret = drm_modeset_lock(&dev->mode_config.connection_mutex, ctx);
if (ret)
return ret;
/* 모든 CRTC 잠금 */
drm_for_each_crtc(crtc, dev) {
ret = drm_modeset_lock(&crtc->mutex, ctx);
if (ret)
return ret;
}
/* 모든 Plane 잠금 */
drm_for_each_plane(plane, dev) {
ret = drm_modeset_lock(&plane->mutex, ctx);
if (ret)
return ret;
}
return 0;
}
drm_modeset_lock_all_ctx()는 legacy 경로로, 모든 객체를 잠급니다. 현대적인 atomic commit은 실제로 변경되는 객체만 선택적으로 잠가서 경합을 최소화합니다.
dma_resv 심층: GEM/TTM 버퍼 예약
dma_resv(DMA reservation)는 GPU 버퍼 오브젝트의 동기화 핵심입니다. 내부 ww_mutex로 다중 버퍼 잠금을 구현하고, DMA fence로 GPU 작업 간 의존성을 관리합니다.
dma_resv 구조체(Struct) 심층
/* include/linux/dma-resv.h */
struct dma_resv {
struct ww_mutex lock; /* ww_mutex! */
struct dma_resv_list *fences; /* RCU-protected fence list */
};
/* 전역 ww_class */
DEFINE_WW_CLASS(reservation_ww_class);
struct dma_resv_list {
struct rcu_head rcu;
u32 num_fences;
u32 max_fences;
struct dma_fence *table[]; /* flexible array */
};
dma_resv_lock API
/* dma_resv_lock — 버퍼 예약 잠금 */
static inline int
dma_resv_lock(struct dma_resv *obj,
struct ww_acquire_ctx *ctx)
{
return ww_mutex_lock(&obj->lock, ctx);
}
static inline int
dma_resv_lock_interruptible(struct dma_resv *obj,
struct ww_acquire_ctx *ctx)
{
return ww_mutex_lock_interruptible(&obj->lock, ctx);
}
static inline void
dma_resv_lock_slow(struct dma_resv *obj,
struct ww_acquire_ctx *ctx)
{
ww_mutex_lock_slow(&obj->lock, ctx);
}
static inline void
dma_resv_unlock(struct dma_resv *obj)
{
ww_mutex_unlock(&obj->lock);
}
공유/배타 펜스 관리
/* dma_resv에 펜스 추가 — ww_mutex 보유 상태에서만 */
void dma_resv_add_fence(struct dma_resv *obj,
struct dma_fence *fence,
enum dma_resv_usage usage)
{
/* lock이 보유 상태인지 확인 */
dma_resv_assert_held(obj);
/* usage 종류: */
/* DMA_RESV_USAGE_WRITE — 배타적 쓰기 */
/* DMA_RESV_USAGE_READ — 공유 읽기 */
/* DMA_RESV_USAGE_BOOKKEEP — 관리용 */
/* fence list에 RCU-safe 추가 */
dma_resv_list_add(obj, fence, usage);
}
implicit synchronization
/* implicit sync: 버퍼에 대한 GPU 작업 순서 보장 */
/* 1. 이전 GPU 작업의 fence를 읽기 */
dma_resv_for_each_fence(&cursor, obj,
DMA_RESV_USAGE_WRITE, fence) {
/* 이전 write fence가 완료될 때까지 대기 */
dma_fence_wait(fence, interruptible);
}
/* 2. 새 작업의 fence 추가 */
dma_resv_add_fence(obj, new_fence, DMA_RESV_USAGE_WRITE);
/* ww_mutex가 보장하는 것: */
/* - fence list 수정의 원자성 */
/* - 다중 버퍼 간 fence 추가의 일관성 */
/* - 데드락 없는 다중 버퍼 동시 잠금 */
dma_resv의 fence list는 RCU로 보호됩니다. fence를 추가/제거할 때는 ww_mutex가 필요하지만, fence를 읽기만 할 때는 rcu_read_lock()만으로 충분합니다. 이는 GPU 드라이버의 hot path에서 잠금 경합(Lock Contention)을 크게 줄입니다.
drm_exec: 현대적 다중 잠금 래퍼
drm_exec는 Linux 6.4에서 도입된 ww_mutex 다중 잠금 헬퍼입니다. -EDEADLK 재시도 루프를 자동화하여 GPU 드라이버 코드를 크게 단순화합니다. 기존 drm_gem_lock_reservations()를 대체합니다.
drm_exec 구조체
/* include/drm/drm_exec.h */
struct drm_exec {
struct ww_acquire_ctx ticket; /* ww_acquire_ctx */
uint32_t flags; /* DRM_EXEC_* 플래그 */
uint32_t num_objects;/* 잠금 대상 객체 수 */
uint32_t max_objects;/* 할당 크기 */
struct drm_gem_object **objects; /* 잠금 객체 배열 */
struct drm_gem_object *contended; /* 경합 객체 */
unsigned int prelocked; /* slow path 잠금 수 */
};
drm_exec 핵심 API
/* drm_exec 사용 패턴 */
/* 1. 초기화 */
void drm_exec_init(struct drm_exec *exec,
uint32_t flags,
unsigned nr);
/* flags: DRM_EXEC_INTERRUPTIBLE_WAIT — 인터럽트 가능 대기 */
/* DRM_EXEC_IGNORE_DUPLICATES — 중복 객체 무시 */
/* nr: 예상 객체 수 (사전 할당용) */
/* 2. 객체 잠금 (자동 재시도) */
int drm_exec_prepare_obj(struct drm_exec *exec,
struct drm_gem_object *obj,
unsigned int num_fences);
/* 3. 배열 일괄 잠금 */
int drm_exec_prepare_array(struct drm_exec *exec,
struct drm_gem_object **objects,
unsigned int num_objects,
unsigned int num_fences);
/* 4. 정리: 모든 잠금 해제 */
void drm_exec_fini(struct drm_exec *exec);
자동 재시도 매크로
/* drm_exec의 핵심: 자동 재시도 매크로 */
#define drm_exec_until_all_locked(exec) \
while (drm_exec_cleanup(exec))
#define drm_exec_retry_on_contention(exec) \
do { \
if (unlikely((exec)->contended)) \
continue; \
} while (0)
/* drm_exec_cleanup 내부: */
static inline bool
drm_exec_cleanup(struct drm_exec *exec)
{
if (likely(!exec->contended))
return false; /* 경합 없음 → 루프 종료 */
/* 경합 발생: 모든 잠금 해제 */
drm_exec_unlock_all(exec);
/* 경합 객체에 slow path 잠금 */
dma_resv_lock_slow(exec->contended->resv,
&exec->ticket);
exec->prelocked++;
exec->contended = NULL;
return true; /* 루프 재시도 */
}
실전 사용 예
/* GPU command submission에서의 drm_exec 사용 */
struct drm_exec exec;
int ret;
drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT
| DRM_EXEC_IGNORE_DUPLICATES, 0);
drm_exec_until_all_locked(&exec) {
/* 소스 버퍼 잠금 */
ret = drm_exec_prepare_obj(&exec, &src_bo->base, 1);
drm_exec_retry_on_contention(&exec);
if (ret)
goto err;
/* 대상 버퍼 잠금 */
ret = drm_exec_prepare_obj(&exec, &dst_bo->base, 1);
drm_exec_retry_on_contention(&exec);
if (ret)
goto err;
/* 공유 커맨드 버퍼 잠금 */
ret = drm_exec_prepare_obj(&exec, &cmd_bo->base, 0);
drm_exec_retry_on_contention(&exec);
if (ret)
goto err;
}
/* 모든 BO 잠금 보유: GPU 작업 제출 */
dma_resv_add_fence(src_bo->base.resv, fence, DMA_RESV_USAGE_READ);
dma_resv_add_fence(dst_bo->base.resv, fence, DMA_RESV_USAGE_WRITE);
submit_to_gpu(ring, fence);
err:
drm_exec_fini(&exec);
| 비교 항목 | 수동 ww_mutex | drm_exec |
|---|---|---|
| 재시도 루프 | 직접 구현 | until_all_locked 매크로 |
| 잠금 해제 | 수동 역순 해제 | cleanup/fini 자동 해제 |
| 경합 추적 | contended 변수 관리 | exec->contended 자동 |
| 중복 객체 | 수동 처리 | IGNORE_DUPLICATES 플래그 |
| fence 예약 | 별도 관리 | prepare_obj의 num_fences |
| 도입 버전 | v3.11 | v6.4 |
drm_exec를 사용하세요. 코드가 간결해지고, 재시도 로직의 버그(잠금 해제 누락, contended 추적 오류 등)를 방지할 수 있습니다.
커널 버전별 진화
ww_mutex는 2013년 Linux 3.11에서 처음 도입된 이후 지속적으로 발전해왔습니다. DRM 서브시스템의 요구에 맞춰 API가 확장되고, 성능 최적화와 디버깅 지원이 강화되었습니다.
주요 버전별 변경 이력
| 커널 버전 | 연도 | 주요 변경 |
|---|---|---|
| v3.11 | 2013 | ww_mutex 최초 도입. Wound-Wait 알고리즘 구현. DRM modeset lock 변환 |
| v3.17 | 2014 | dma_resv(reservation_object) 도입. GEM buffer 예약에 ww_mutex 적용 |
| v4.4 | 2016 | lockdep의 ww_mutex 검증 강화. ww_class 불일치 감지 |
| v4.7 | 2016 | TTM의 ww_mutex 통합 개선. ttm_bo_reserve 리팩토링 |
| v4.19 | 2018 | PREEMPT_RT 지원 개선. rt_mutex 기반 ww_mutex 경로 |
| v5.3 | 2019 | Wait-Die 알고리즘 추가 (DEFINE_WD_CLASS). is_wait_die 플래그 |
| v5.5 | 2020 | dma_resv: reservation_object → dma_resv 이름 변경 |
| v5.8 | 2020 | dma_resv fence 관리 API 정비. usage 타입 체계 도입 |
| v5.14 | 2021 | ww_mutex optimistic spinning 개선. osq_lock 통합 |
| v5.19 | 2022 | dma_resv_list 리팩토링. num_fences 동적 관리 |
| v6.0 | 2022 | dma_resv: 공유/배타 fence 구분 제거 → usage 기반 통합 |
| v6.4 | 2023 | drm_exec 프레임워크 도입. 자동 재시도 래퍼 |
| v6.6 | 2023 | drm_exec: IGNORE_DUPLICATES, prepare_array 추가 |
| v6.8 | 2024 | ww_mutex wound 전파 경로 최적화. 불필요한 wake_up 감소 |
| v6.12 | 2024 | drm_exec: 다양한 GPU 드라이버(amdgpu, xe, nouveau)에서 채택 확대 |
API 변경 가이드
/* v3.17 이전: reservation_object */
struct reservation_object *resv;
reservation_object_lock(resv, ctx);
reservation_object_unlock(resv);
/* v5.5+: dma_resv (이름 변경) */
struct dma_resv *resv;
dma_resv_lock(resv, ctx);
dma_resv_unlock(resv);
/* v6.0 이전: shared/exclusive fence 분리 */
dma_resv_add_shared_fence(resv, fence);
dma_resv_add_excl_fence(resv, fence);
/* v6.0+: usage 기반 통합 */
dma_resv_add_fence(resv, fence, DMA_RESV_USAGE_WRITE);
dma_resv_add_fence(resv, fence, DMA_RESV_USAGE_READ);
/* v6.4 이전: 수동 재시도 루프 */
ww_acquire_init(&ctx, &reservation_ww_class);
retry:
ret = dma_resv_lock(resv, &ctx);
if (ret == -EDEADLK) { ... goto retry; }
/* v6.4+: drm_exec 자동화 */
drm_exec_init(&exec, flags, 0);
drm_exec_until_all_locked(&exec) {
drm_exec_prepare_obj(&exec, obj, num_fences);
drm_exec_retry_on_contention(&exec);
}
drm_exec_fini(&exec);
drm_exec, dma_resv_add_fence usage 기반)를 사용하세요. 이전 API(reservation_object_*, add_shared/excl_fence)는 호환성을 위해 남아있지만, 새 코드에서는 deprecated입니다.
Linux 6.12 ~ 6.16 Wait/Wound Mutex 동향
ww_mutex의 코어 알고리즘은 변화가 거의 없으나, DRM 서브시스템의 주력 사용처가 계속 확장되고 있습니다. 특히 Intel Xe 드라이버 본선 병합(6.8)과 이후 성숙, AMDGPU의 GPU reset 경로 재설계, DRM scheduler v2 논의 등이 6.12~6.16 구간에 반영되었습니다. drm_exec API는 기존 ttm_eu_* 경로를 대체하며 ww_mutex 사용을 간접화합니다.
| 커널 | 릴리스 | 변경 사항 | 실무 시사점 |
|---|---|---|---|
| 6.12 (LTS) | 2024-11 | PREEMPT_RT 메인라인 병합 — ww_mutex는 rt_mutex 기반이므로 RT 환경에서 PI 상속 자동 적용 | RT 커널에서 GPU 드라이버 사용 시 RT 태스크(Task)의 GPU 제출 latency 개선 |
| 6.13 | 2025-01 | drm_exec 기반 AMDGPU/XE 경로 안정화 — 각 드라이버의 ww_acquire_ctx 초기화 패턴 정리 | -EDEADLK 재시도 루프 구현을 DRM_EXEC_INTERRUPTIBLE_WAIT에 위임 권장 |
| 6.14 | 2025-03 | DRM scheduler 개선과 연계된 reservation object 잠금 경로 정돈 | GPU 제출 fence/dependency 관리가 더 예측 가능 — 신규 드라이버는 dma_fence_chain과 조합 |
| 6.15 | 2025-05 | per-VMA lock refcount 전환으로 GPU 드라이버의 VMA 변경 관찰 경로(PPGTT/userptr) 부담 감소 | userptr GEM BO의 page fault 경로 지연(Latency) 감소 |
| 6.16 | 2025-07 | ww_mutex 경로의 DEFINE_CLASS_IS_COND_GUARD 호환성 검토 — guard 스타일 접근은 명시적으로 지원되지 않음(ww_acquire_ctx 필요) | ww_mutex는 guard() 매크로 적용 불가함을 리뷰어에게 공지 |
drm_exec로의 전환
drm_exec는 ww_mutex를 직접 쓰는 대신, 여러 reservation object를 한 번에 획득하는 고수준 헬퍼입니다. 6.10 이후 Intel Xe, AMDGPU, Nouveau 등에서 기본 API로 채택되었습니다. 전통적인 ww_mutex 재시도 루프를 수동으로 구현할 필요가 줄어듭니다.
/* drm_exec: ww_mutex 재시도 루프 자동화 */
struct drm_exec exec;
drm_exec_init(&exec, DRM_EXEC_INTERRUPTIBLE_WAIT | DRM_EXEC_IGNORE_DUPLICATES, 0);
drm_exec_until_all_locked(&exec) {
drm_exec_lock_obj(&exec, obj1);
drm_exec_retry_on_contention(&exec);
drm_exec_lock_obj(&exec, obj2);
drm_exec_retry_on_contention(&exec);
}
/* ... critical section ... */
drm_exec_fini(&exec);
drm_exec를 기본으로 사용하세요. (2) ww_mutex에는 guard() 매크로를 적용할 수 없습니다(ww_acquire_ctx 생명주기가 필요). (3) RT 병합 후에도 DRM 경로는 그대로 동작 — 다만 RT 태스크가 GPU 제출에 관여하는 경우 PI 상속으로 우선순위 역전(Priority Inversion)이 자동 해결됩니다.
참고 자료
커널 공식 문서
- Wound/Wait Deadlock-Proof Mutex Design — ww_mutex 공식 설계 문서
- Generic Mutex Subsystem — ww_mutex의 기반이 되는 mutex 설계
- Lock types and their rules — ww_mutex의 잠금 유형 분류
- DRM Memory Management — GEM/TTM 버퍼의 ww_mutex 사용 패턴
LWN.net 심층 기사
- Wait/wound mutexes (2013) — ww_mutex의 최초 도입과 wound-wait 알고리즘 설명
- The wait/wound mutex (2013) — wound-wait vs wait-die 알고리즘 비교
- The mutex API (2013) — 일반 mutex와 ww_mutex의 관계
- Lockdep: how to read its cryptic output (2013) — ww_mutex lockdep 경고 해석
학술 자료 및 외부 참고
- Rosenkrantz, Stearns & Lewis — "System Level Concurrency Control for Distributed Database Systems" (ACM TODS, 1978) — wound-wait/wait-die 알고리즘 원본 논문
- DRM Internals — DRM 서브시스템의 ww_mutex 활용 아키텍처
- 커널 소스:
kernel/locking/ww_mutex.h,include/linux/ww_mutex.h - DRM 사용 예시:
drivers/gpu/drm/drm_gem.c,drivers/gpu/drm/ttm/ drm_execAPI:drivers/gpu/drm/drm_exec.c— ww_mutex 기반 최신 버퍼 잠금 API
관련 문서
ww_mutex와 관련된 다른 동기화 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.