Locking / RCU · Linux v6.6 · source + diagram note

mutex

태스크 소유권, 선택적 회전, 대기·handoff를 소스에서 구분한다.

전체 흐름

일반 mutex_lock()은 태스크 문맥에서 소유권을 얻는다. 빠른 경로가 활성인 설정에서는 경합이 없을 때 원자적 cmpxchg로 끝나고, 실패할 때만 optimistic spinning 또는 대기 목록의 sleep 경로를 시도한다. CONFIG_DEBUG_LOCK_ALLOC 설정에서는 바깥 fast 경로 대신 공통 함수로 바로 들어간다. mutex_unlock()은 대기자가 있는지에 따라 빠른 해제 또는 wake/handoff를 수행한다.

그림 1. 획득 경로의 조건부 분기 (v6.6, !CONFIG_PREEMPT_RT)
  1. fast path (설정 의존)!CONFIG_DEBUG_LOCK_ALLOC에서 owner == 0이면 cmpxchg-acquire로 current를 기록. 실패 시 공통 경로.
  2. midpath: 선택적 spinCONFIG_MUTEX_SPIN_ON_OWNER이고 소유자가 실행 중이며 재스케줄이 급하지 않을 때 시도. 실패 시 대기 경로.
  3. slowpath: wait/schedulewait_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_RTacquire 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.cmutex_lock() fast path, __mutex_lock_common(), mutex_unlock()과 slow unlock
include/linux/mutex.howner, wait_lock, wait_list, 조건부 osq 필드
kernel/locking/rtmutex.cPREEMPT_RT 기반 잠금의 별도 핵심 구현; 일반 mutex의 다음 단계가 아님
include/trace/events/lock.hlock:contention_begin/end와 LCB_F_MUTEX 플래그

대표 코드

__mutex_lock_common

원본 코드: kernel/locking/mutex.c:569-741 · v6.6 · 로컬 원문
__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

원본 코드: kernel/locking/mutex.c:537-544 · v6.6 · 로컬 원문
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을 수행한다는 기존 설명은 맞지 않는다.

그림 2. unlock의 두 갈래: wake와 명시적 handoff
전제 A가 소유 중, B가 wait_list의 선두 waiter. owner low bits에는 WAITERS/HANDOFF/PICKUP 상태를 담을 수 있다.
일반 wakeA가 owner를 release로 비우고 B를 깨운다. B는 다시 trylock한다. 그 사이 다른 태스크가 먼저 얻을 수 있으므로 wake 자체는 소유권 예약이 아니다.
HANDOFF 요청이 있을 때A가 __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 pickupww_ctx == NULL이면 일반 mutex; signal 상태는 호출 API에 따라 다름
mutex_unlock() fast!CONFIG_DEBUG_LOCK_ALLOC에서 owner가 정확히 current일 때 cmpxchg-release로 0waiter 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의 관계다.

그림 3. 일반 struct mutex의 필드 역할 (v6.6)
owner + flag bitsowner task와 WAITERS/HANDOFF/PICKUP. 포인터 값을 사용할 때 flag를 마스킹.
wait_lock + wait_listraw spinlock이 waiter 목록과 wake/handoff 순서를 짧게 직렬화.
osq (조건부)CONFIG_MUTEX_SPIN_ON_OWNER의 spinner MCS queue. sleeping waiter 목록과 별개.

ww_ctx는 위 세 필드 중 하나가 아니다. include/linux/mutex.h · kernel/locking/mutex.c:212.

요소코드에서의 역할읽을 때 주의
ownercurrent task pointer; 해제 시 release, 획득 시 acquireflag 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가 자동으로 소유자가 되는 것은 아니다.

그림 4. A·B 두 태스크의 경합 타임라인
  1. A 보유A의 fast acquire 성공 → owner=A. B의 fast 시도는 실패.
  2. B 대기B는 조건부 spin에 실패하면 wait_list에 들어가고 schedule_preempt_disabled()로 기다린다.
  3. A 해제·B 재개A는 wake 또는 요청된 handoff를 수행. B는 재시도나 PICKUP을 거쳐 소유권을 확인한 후 반환.

이것은 가능한 한 경합 예시다. midpath가 성공하면 B의 sleep은 없다. kernel/locking/mutex.c:569.

관찰 상태원본의 조건다음 결과
owner=0uncontended fast cmpxchg 성공current owner; 잠들지 않음
owner=AB의 fast 획득 실패, 선택적 optimistic spinspin 중 얻으면 즉시 반환; 아니면 목록 경로
WAITERS + wait_listB가 목록에 등록되고 여전히 획득 실패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에서 대신 unlockowner와 debug 검사; 문맥 요구사항
보유 중 객체 수명 유지보호받는 객체와 mutex 자체를 경합 중 조기 해제초기화·보유·해제 경로의 호출자 책임
wake와 handoff 분리깨웠으므로 바로 지정 task가 ownerMUTEX_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가 없는 빌드라면 실행하지 않는다. 실제 보드에서 실행한 결과가 아니다.

독립 tracefs 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 후 cleanup

event 정의: 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 오류·반환을 같은 상태표로 섞지 않았는가?