Wait/Wound 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는 컴파일러 최적화(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입니다.
참고 자료
커널 공식 문서
- 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와 관련된 다른 동기화 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.