전체 흐름
일반 mutex_lock()은 태스크 문맥에서 소유권을 얻는다. 빠른 경로가 활성인 설정에서는 경합이 없을 때 원자적 cmpxchg로 끝나고, 실패할 때만 optimistic spinning 또는 대기 목록의 sleep 경로를 시도한다. CONFIG_DEBUG_LOCK_ALLOC 설정에서는 바깥 fast 경로 대신 공통 함수로 바로 들어간다. mutex_unlock()은 대기자가 있는지에 따라 빠른 해제 또는 wake/handoff를 수행한다.
- fast path (설정 의존)
!CONFIG_DEBUG_LOCK_ALLOC에서owner == 0이면 cmpxchg-acquire로 current를 기록. 실패 시 공통 경로. - midpath: 선택적 spin
CONFIG_MUTEX_SPIN_ON_OWNER이고 소유자가 실행 중이며 재스케줄이 급하지 않을 때 시도. 실패 시 대기 경로. - slowpath: wait/schedule
wait_lock아래 재시도 후wait_list에 등록. 못 얻으면 잠들고, 깨어나 재획득 또는 handoff pickup.
각 칸에서 성공하면 보호 구역으로 들어간다. 모든 요청이 세 칸을 끝까지 방문하는 것은 아니다. kernel/locking/mutex.c:569.
| 대상 | 보호 대상 | 잠들 수 있는가 |
|---|---|---|
struct mutex | 호출자의 공유 데이터 보호 구역 | 경합 시 잠들 수 있으므로 IRQ/tasklet 등에서 사용 금지 |
wait_lock의 raw_spinlock_t | 내부 wait_list와 handoff/wakeup 순서 | 짧은 내부 임계 구역; 호출자 보호 구역 잠금이 아님 |
ww_mutex / PREEMPT_RT | acquire context의 교착 회피 / rtmutex 기반 별도 구현 | 아래 일반 mutex 그림을 그대로 적용하지 않음 |
범위: kernel/locking/mutex.c의 !CONFIG_PREEMPT_RT 구현. might_sleep()는 문맥 검사이며 실제 sleep과 다르다.
소스 코드 위치
kernel/locking/mutex.c에서 acquire, waiter 처리, unlock을 이어 읽는다. include/linux/mutex.h가 struct mutex와 API를 함께 정의한다. kernel/locking/rtmutex.c는 일반 경로의 뒤 단계가 아닌 별도 RT 구현이다.
| 원본 | 이 글에서 확인할 내용 |
|---|---|
| kernel/locking/mutex.c | mutex_lock() fast path, __mutex_lock_common(), mutex_unlock()과 slow unlock |
| include/linux/mutex.h | owner, wait_lock, wait_list, 조건부 osq 필드 |
| kernel/locking/rtmutex.c | PREEMPT_RT 기반 잠금의 별도 핵심 구현; 일반 mutex의 다음 단계가 아님 |
| include/trace/events/lock.h | lock:contention_begin/end와 LCB_F_MUTEX 플래그 |
대표 코드
__mutex_lock_common
__mutex_lock_common 함수 전체 (173줄)
__mutex_lock_common(struct mutex *lock, unsigned int state, unsigned int subclass,
struct lockdep_map *nest_lock, unsigned long ip,
struct ww_acquire_ctx *ww_ctx, const bool use_ww_ctx)
{
struct mutex_waiter waiter;
struct ww_mutex *ww;
int ret;
if (!use_ww_ctx)
ww_ctx = NULL;
might_sleep();
MUTEX_WARN_ON(lock->magic != lock);
ww = container_of(lock, struct ww_mutex, base);
if (ww_ctx) {
if (unlikely(ww_ctx == READ_ONCE(ww->ctx)))
return -EALREADY;
/*
* Reset the wounded flag after a kill. No other process can
* race and wound us here since they can't have a valid owner
* pointer if we don't have any locks held.
*/
if (ww_ctx->acquired == 0)
ww_ctx->wounded = 0;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
nest_lock = &ww_ctx->dep_map;
#endif
}
preempt_disable();
mutex_acquire_nest(&lock->dep_map, subclass, 0, nest_lock, ip);
trace_contention_begin(lock, LCB_F_MUTEX | LCB_F_SPIN);
if (__mutex_trylock(lock) ||
mutex_optimistic_spin(lock, ww_ctx, NULL)) {
/* got the lock, yay! */
lock_acquired(&lock->dep_map, ip);
if (ww_ctx)
ww_mutex_set_context_fastpath(ww, ww_ctx);
trace_contention_end(lock, 0);
preempt_enable();
return 0;
}
raw_spin_lock(&lock->wait_lock);
/*
* After waiting to acquire the wait_lock, try again.
*/
if (__mutex_trylock(lock)) {
if (ww_ctx)
__ww_mutex_check_waiters(lock, ww_ctx);
goto skip_wait;
}
debug_mutex_lock_common(lock, &waiter);
waiter.task = current;
if (use_ww_ctx)
waiter.ww_ctx = ww_ctx;
lock_contended(&lock->dep_map, ip);
if (!use_ww_ctx) {
/* add waiting tasks to the end of the waitqueue (FIFO): */
__mutex_add_waiter(lock, &waiter, &lock->wait_list);
} else {
/*
* Add in stamp order, waking up waiters that must kill
* themselves.
*/
ret = __ww_mutex_add_waiter(&waiter, lock, ww_ctx);
if (ret)
goto err_early_kill;
}
set_current_state(state);
trace_contention_begin(lock, LCB_F_MUTEX);
for (;;) {
bool first;
/*
* Once we hold wait_lock, we're serialized against
* mutex_unlock() handing the lock off to us, do a trylock
* before testing the error conditions to make sure we pick up
* the handoff.
*/
if (__mutex_trylock(lock))
goto acquired;
/*
* Check for signals and kill conditions while holding
* wait_lock. This ensures the lock cancellation is ordered
* against mutex_unlock() and wake-ups do not go missing.
*/
if (signal_pending_state(state, current)) {
ret = -EINTR;
goto err;
}
if (ww_ctx) {
ret = __ww_mutex_check_kill(lock, &waiter, ww_ctx);
if (ret)
goto err;
}
raw_spin_unlock(&lock->wait_lock);
schedule_preempt_disabled();
first = __mutex_waiter_is_first(lock, &waiter);
set_current_state(state);
/*
* Here we order against unlock; we must either see it change
* state back to RUNNING and fall through the next schedule(),
* or we must see its unlock and acquire.
*/
if (__mutex_trylock_or_handoff(lock, first))
break;
if (first) {
trace_contention_begin(lock, LCB_F_MUTEX | LCB_F_SPIN);
if (mutex_optimistic_spin(lock, ww_ctx, &waiter))
break;
trace_contention_begin(lock, LCB_F_MUTEX);
}
raw_spin_lock(&lock->wait_lock);
}
raw_spin_lock(&lock->wait_lock);
acquired:
__set_current_state(TASK_RUNNING);
if (ww_ctx) {
/*
* Wound-Wait; we stole the lock (!first_waiter), check the
* waiters as anyone might want to wound us.
*/
if (!ww_ctx->is_wait_die &&
!__mutex_waiter_is_first(lock, &waiter))
__ww_mutex_check_waiters(lock, ww_ctx);
}
__mutex_remove_waiter(lock, &waiter);
debug_mutex_free_waiter(&waiter);
skip_wait:
/* got the lock - cleanup and rejoice! */
lock_acquired(&lock->dep_map, ip);
trace_contention_end(lock, 0);
if (ww_ctx)
ww_mutex_lock_acquired(ww, ww_ctx);
raw_spin_unlock(&lock->wait_lock);
preempt_enable();
return 0;
err:
__set_current_state(TASK_RUNNING);
__mutex_remove_waiter(lock, &waiter);
err_early_kill:
trace_contention_end(lock, ret);
raw_spin_unlock(&lock->wait_lock);
debug_mutex_free_waiter(&waiter);
mutex_release(&lock->dep_map, ip);
preempt_enable();
return ret;
}확인 사항: 이 함수는 fast 재시도·선택적 optimistic spin·waiter 등록 및 재획득을 한데 담는다. 일반 mutex 호출에서는 ww_ctx가 NULL이고, 대기에서 돌아온 뒤 waiter를 제거하고 TASK_RUNNING으로 복원한 후 반환한다.
mutex_unlock
void __sched mutex_unlock(struct mutex *lock)
{
#ifndef CONFIG_DEBUG_LOCK_ALLOC
if (__mutex_unlock_fast(lock))
return;
#endif
__mutex_unlock_slowpath(lock, _RET_IP_);
}
mutex_unlock()은 설정에 따라 fast path를 시도하고, 성공하지 못하면 __mutex_unlock_slowpath()로 넘긴다. 호출한 task가 소유한 mutex를 해제하는 API이며 interrupt 문맥에서 사용할 수 없다. unlock 호출이 반환하기 전에 다른 task가 mutex 객체 자체를 해제하도록 만들어서도 안 된다.
함수별 분석
__mutex_lock_common()은 빠른 시도가 실패한 뒤에도 먼저 재시도하고, 필요하면 waiter를 등록한다. 해제할 때는 보통 owner를 비우고 선두 waiter를 깨우며, 선두 waiter가 handoff를 요청한 경우에만 특정 태스크로 소유권을 넘긴다. mutex_unlock()이 optimistic spin을 수행한다는 기존 설명은 맞지 않는다.
wait_list의 선두 waiter. owner low bits에는 WAITERS/HANDOFF/PICKUP 상태를 담을 수 있다.__mutex_handoff(lock, B)로 owner를 B|PICKUP으로 바꾼 뒤 B를 깨운다. B는 PICKUP을 해제하며 획득한다.handoff는 모든 unlock에서 발생하지 않는다. kernel/locking/mutex.c:212 · kernel/locking/mutex.c:901.
| 경로 | 조건과 상태 변화 | 확인할 것 |
|---|---|---|
__mutex_lock_common() | 재시도 → 조건부 spin → waiter 등록·schedule → 재획득/handoff pickup | ww_ctx == NULL이면 일반 mutex; signal 상태는 호출 API에 따라 다름 |
mutex_unlock() fast | !CONFIG_DEBUG_LOCK_ALLOC에서 owner가 정확히 current일 때 cmpxchg-release로 0 | waiter flag나 경쟁이 있으면 fast 해제 실패 후 slowpath |
__mutex_unlock_slowpath() | WAITERS이면 wait_lock 아래 첫 waiter를 wake; HANDOFF이면 owner를 선두 waiter로 이전 | wake와 소유권 이전을 같은 사건으로 보지 않음 |
버전별 구현 세부: v6.6은 대기 목록에서 raw_spin_lock(&lock->wait_lock)을, v6.18.37은 raw_spin_lock_irqsave()와 wake queue 복원을 사용한다. v6.18.37의 blocked-on/hung-task 보강을 v6.6 원문에 역투영하지 않는다. 이는 내부 목록 보호 방식의 차이이며 호출자 임계 구역을 raw spinlock으로 바꾼 뜻이 아니다.
구조체와 필드
owner는 태스크 포인터와 low-bit 상태를 함께 담는다. 필드가 독립적인 실행 단계는 아니다. ww_ctx는 struct mutex의 필드가 아니라 wound/wait 획득 문맥과 struct ww_mutex의 관계다.
CONFIG_MUTEX_SPIN_ON_OWNER의 spinner MCS queue. sleeping waiter 목록과 별개.ww_ctx는 위 세 필드 중 하나가 아니다. include/linux/mutex.h · kernel/locking/mutex.c:212.
| 요소 | 코드에서의 역할 | 읽을 때 주의 |
|---|---|---|
owner | current task pointer; 해제 시 release, 획득 시 acquire | flag bit를 제거한 뒤 소유 task로 해석 |
| WAITERS | 비어 있지 않은 waiter 목록 표시 | 있다고 handoff가 이미 일어난 것은 아님 |
| HANDOFF / PICKUP | 선두 waiter가 handoff 요청 / 지정 waiter가 pickup 대기 | 일반 wake와 별개인 조건부 경로 |
wait_list / wait_lock | 대기 태스크 목록 / 목록과 unlock의 동기화 | spinlock은 내부 메타데이터용 |
osq / ww_ctx | 설정된 경우 spinner 직렬화 / 별도 wound-wait 문맥 | 둘 다 일반 mutex의 상시 동일 필드가 아님 |
실행 단계와 상태 변화
시간 순서와 배타적 경로를 나눠야 한다. A가 임계 구역에 있는 동안 B가 경합해 잠들 수 있지만, A가 해제할 때 B가 자동으로 소유자가 되는 것은 아니다.
- A 보유A의 fast acquire 성공 →
owner=A. B의 fast 시도는 실패. - B 대기B는 조건부 spin에 실패하면
wait_list에 들어가고schedule_preempt_disabled()로 기다린다. - A 해제·B 재개A는 wake 또는 요청된 handoff를 수행. B는 재시도나 PICKUP을 거쳐 소유권을 확인한 후 반환.
이것은 가능한 한 경합 예시다. midpath가 성공하면 B의 sleep은 없다. kernel/locking/mutex.c:569.
| 관찰 상태 | 원본의 조건 | 다음 결과 |
|---|---|---|
owner=0 | uncontended fast cmpxchg 성공 | current owner; 잠들지 않음 |
owner=A | B의 fast 획득 실패, 선택적 optimistic spin | spin 중 얻으면 즉시 반환; 아니면 목록 경로 |
WAITERS + wait_list | B가 목록에 등록되고 여전히 획득 실패 | B가 schedule; unlock이 wake 검토 |
| HANDOFF → PICKUP | 선두 waiter가 handoff 요청했고 owner가 해제 | 지정된 waiter만 pickup; 일반 wake에는 이 보장 없음 |
| signal/kill 조건 | interruptible·killable·ww 계열의 해당 경로 | 대기 중 오류 반환·waiter 제거; 보통 mutex_lock의 성공과 구분 |
v6.6 코드에는 v6.18.37의 blocked-on/hung-task 기록을 가정하지 않는다. mutex_lock()은 void이고 획득 뒤 반환한다.
불변 조건과 실패 사례
| 규칙 | 잘못된 해석 | 원본에서 확인할 지점 |
|---|---|---|
| 같은 task가 획득·해제 | 다른 CPU/task나 IRQ에서 대신 unlock | owner와 debug 검사; 문맥 요구사항 |
| 보유 중 객체 수명 유지 | 보호받는 객체와 mutex 자체를 경합 중 조기 해제 | 초기화·보유·해제 경로의 호출자 책임 |
| wake와 handoff 분리 | 깨웠으므로 바로 지정 task가 owner | MUTEX_FLAG_HANDOFF와 PICKUP 조건 |
| 일반 mutex와 ww/RT 분리 | ww_ctx를 상시 필드로 쓰거나 RT 내부를 같은 그림으로 설명 | use_ww_ctx 분기와 CONFIG_PREEMPT_RT |
mutex는 재귀 획득을 허용하지 않고 IRQ/tasklet 문맥에서 사용할 수 없다. 여러 잠금의 순환 의존성은 lockdep과 호출 순서로 확인한다. include/linux/mutex.h.
계측과 검증
먼저 lock:contention_begin/end가 이 커널의 tracefs에 나타나는지 확인한다. begin의 MUTEX flag는 mutex 경합을 구별하지만 event 하나로 소유권 획득이나 원인 태스크가 모두 증명되지는 않는다. 주소·시각·호출 문맥을 맞춰 읽는다.
| 수단 | 확인할 것 | 한계 |
|---|---|---|
| lockdep / DEBUG_MUTEXES | 잠금 순서, 재귀 획득, 잘못된 unlock 경고 | 설정에 따라 제공 범위가 달라짐 |
lock:contention_begin/end | 경합 시작/종료와 MUTEX flag, 같은 lock 주소 | 모든 fast acquire를 표시하는 event는 아님 |
| hung-task 진단 / SysRq-w | 잠든 태스크와 stack을 관찰 | 대기 stack만으로 owner나 원인 단정 금지 |
아래 예시는 root shell에서 이미 mount된 tracefs에 본인만의 instance를 만들고 종료 시 제거한다. 전역 current_tracer, set_ftrace_filter, 공유 trace_pipe는 바꾸거나 읽지 않는다. event/instance가 없는 빌드라면 실행하지 않는다. 실제 보드에서 실행한 결과가 아니다.
# root shell; tracefs가 이미 /sys/kernel/tracing에 mount된 경우만
set -eu
tr=/sys/kernel/tracing
test -d "$tr/instances" || exit 1
inst="$tr/instances/dujin_mutex_$$"
mkdir "$inst"
cleanup() {
for ev in contention_begin contention_end; do
test -w "$inst/events/lock/$ev/enable" && printf 0 > "$inst/events/lock/$ev/enable" || :
done
rmdir "$inst"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
for ev in contention_begin contention_end; do
test -w "$inst/events/lock/$ev/enable" || exit 1
printf 1 > "$inst/events/lock/$ev/enable"
done
cat "$inst/trace_pipe" # Ctrl-C 후 cleanupevent 정의: include/trace/events/lock.h. tracefs instance는 별도 buffer와 event enable 상태를 갖는다. trace_pipe는 읽은 데이터를 소비하므로 반드시 이 instance의 파일만 읽는다.
추가 확인 사항
- 이 호출에서 fast path가 실패한 이유는 owner task, flag, 디버그 설정 중 무엇인가?
- 실제 커널에
CONFIG_MUTEX_SPIN_ON_OWNER와CONFIG_PREEMPT_RT가 어떻게 설정되어 있으며 그림의 어느 경로가 적용되는가? - 선두 waiter를 깨웠을 뿐인지,
MUTEX_FLAG_HANDOFF뒤 PICKUP을 받았는지 어떻게 구분할 것인가? - 일반 mutex 경합과
ww_mutex의 wound/wait 오류·반환을 같은 상태표로 섞지 않았는가?