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

lockdep

lock class, dependency graph, selftest.

전체 흐름: 초기화와 검증은 별도 경로

lockdep은 실제로 관찰한 lock class 순서와 IRQ 사용 맥락을 기록하여 잠재적인 데드락을 경고합니다. 여기서 다루는 lockdep_init() 발췌는 부팅 시 한도와 메모리 사용량을 출력합니다. 의존성 edge를 추가하거나 실제 잠금을 획득하는 함수가 아닙니다.

그림 1. lockdep_init()과 런타임 획득 검증의 분리
부팅 초기화
lockdep_init()구성 상한·메모리 사용량을 로그에 출력
여기서 종료실제 lock 획득 또는 graph edge 삽입 없음
런타임 주석
lock_acquire()lockdep_map과 문맥을 전달
__lock_acquire()class/held stack 확인 후 의존성 검증

두 행은 연속 호출 흐름이 아닙니다. lockdep_init()과 lock_acquire()를 원문에서 나란히 비교합니다.

표 1. lock instance와 class의 차이
구분예시lockdep이 보는 것
instance서로 다른 inode에 내장된 동일 종류의 잠금 두 개개별 lockdep_map과 획득 기록
class두 잠금에 적용되는 같은 잠금 규칙key/subclass가 구분하는 논리적 분류와 class 간 의존성
edge A → BA class를 보유한 채 B class 획득 시도실제 두 instance가 영원히 맞물렸다는 뜻이 아닌 관찰된 순서

커널 lockdep 설계 문서는 동일 class에 다수의 instance가 있을 수 있으며, 검증 단위는 class라고 설명합니다.

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: kernel/locking/lockdep.c
Linux v6.6: include/linux/lockdep.h
Linux v6.6: kernel/locking/lockdep_proc.c

설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.

대표 코드

lockdep_init

원본 코드: kernel/locking/lockdep.c:6478-6515 · v6.6 · 로컬 원문
void __init lockdep_init(void)
{
	printk("Lock dependency validator: Copyright (c) 2006 Red Hat, Inc., Ingo Molnar\n");

	printk("... MAX_LOCKDEP_SUBCLASSES:  %lu\n", MAX_LOCKDEP_SUBCLASSES);
	printk("... MAX_LOCK_DEPTH:          %lu\n", MAX_LOCK_DEPTH);
	printk("... MAX_LOCKDEP_KEYS:        %lu\n", MAX_LOCKDEP_KEYS);
	printk("... CLASSHASH_SIZE:          %lu\n", CLASSHASH_SIZE);
	printk("... MAX_LOCKDEP_ENTRIES:     %lu\n", MAX_LOCKDEP_ENTRIES);
	printk("... MAX_LOCKDEP_CHAINS:      %lu\n", MAX_LOCKDEP_CHAINS);
	printk("... CHAINHASH_SIZE:          %lu\n", CHAINHASH_SIZE);

	printk(" memory used by lock dependency info: %zu kB\n",
	       (sizeof(lock_classes) +
		sizeof(lock_classes_in_use) +
		sizeof(classhash_table) +
		sizeof(list_entries) +
		sizeof(list_entries_in_use) +
		sizeof(chainhash_table) +
		sizeof(delayed_free)
#ifdef CONFIG_PROVE_LOCKING
		+ sizeof(lock_cq)
		+ sizeof(lock_chains)
		+ sizeof(lock_chains_in_use)
		+ sizeof(chain_hlocks)
#endif
		) / 1024
		);

#if defined(CONFIG_TRACE_IRQFLAGS) && defined(CONFIG_PROVE_LOCKING)
	printk(" memory used for stack traces: %zu kB\n",
	       (sizeof(stack_trace) + sizeof(stack_trace_hash)) / 1024
	       );
#endif

	printk(" per task-struct memory footprint: %zu bytes\n",
	       sizeof(((struct task_struct *)NULL)->held_locks));
}

lockdep_init_task()와 lockdep_init()은 다른 함수다. 아래는 전역 초기화 진입점의 실제 본문이며, 이 버전에서 출력하는 크기·설정 정보를 볼 수 있다. task별 held_locks 초기화나 전체 의존성 그래프 구축을 이 발췌가 수행한다고 해석하지 않는다.

__lock_acquire

원본 코드: kernel/locking/lockdep.c:4988-5167 · v6.6 · 로컬 원문
__lock_acquire 함수 전체 (180줄)
static int __lock_acquire(struct lockdep_map *lock, unsigned int subclass,
			  int trylock, int read, int check, int hardirqs_off,
			  struct lockdep_map *nest_lock, unsigned long ip,
			  int references, int pin_count, int sync)
{
	struct task_struct *curr = current;
	struct lock_class *class = NULL;
	struct held_lock *hlock;
	unsigned int depth;
	int chain_head = 0;
	int class_idx;
	u64 chain_key;

	if (unlikely(!debug_locks))
		return 0;

	if (!prove_locking || lock->key == &__lockdep_no_validate__)
		check = 0;

	if (subclass < NR_LOCKDEP_CACHING_CLASSES)
		class = lock->class_cache[subclass];
	/*
	 * Not cached?
	 */
	if (unlikely(!class)) {
		class = register_lock_class(lock, subclass, 0);
		if (!class)
			return 0;
	}

	debug_class_ops_inc(class);

	if (very_verbose(class)) {
		printk("\nacquire class [%px] %s", class->key, class->name);
		if (class->name_version > 1)
			printk(KERN_CONT "#%d", class->name_version);
		printk(KERN_CONT "\n");
		dump_stack();
	}

	/*
	 * Add the lock to the list of currently held locks.
	 * (we dont increase the depth just yet, up until the
	 * dependency checks are done)
	 */
	depth = curr->lockdep_depth;
	/*
	 * Ran out of static storage for our per-task lock stack again have we?
	 */
	if (DEBUG_LOCKS_WARN_ON(depth >= MAX_LOCK_DEPTH))
		return 0;

	class_idx = class - lock_classes;

	if (depth && !sync) {
		/* we're holding locks and the new held lock is not a sync */
		hlock = curr->held_locks + depth - 1;
		if (hlock->class_idx == class_idx && nest_lock) {
			if (!references)
				references++;

			if (!hlock->references)
				hlock->references++;

			hlock->references += references;

			/* Overflow */
			if (DEBUG_LOCKS_WARN_ON(hlock->references < references))
				return 0;

			return 2;
		}
	}

	hlock = curr->held_locks + depth;
	/*
	 * Plain impossible, we just registered it and checked it weren't no
	 * NULL like.. I bet this mushroom I ate was good!
	 */
	if (DEBUG_LOCKS_WARN_ON(!class))
		return 0;
	hlock->class_idx = class_idx;
	hlock->acquire_ip = ip;
	hlock->instance = lock;
	hlock->nest_lock = nest_lock;
	hlock->irq_context = task_irq_context(curr);
	hlock->trylock = trylock;
	hlock->read = read;
	hlock->check = check;
	hlock->sync = !!sync;
	hlock->hardirqs_off = !!hardirqs_off;
	hlock->references = references;
#ifdef CONFIG_LOCK_STAT
	hlock->waittime_stamp = 0;
	hlock->holdtime_stamp = lockstat_clock();
#endif
	hlock->pin_count = pin_count;

	if (check_wait_context(curr, hlock))
		return 0;

	/* Initialize the lock usage bit */
	if (!mark_usage(curr, hlock, check))
		return 0;

	/*
	 * Calculate the chain hash: it's the combined hash of all the
	 * lock keys along the dependency chain. We save the hash value
	 * at every step so that we can get the current hash easily
	 * after unlock. The chain hash is then used to cache dependency
	 * results.
	 *
	 * The 'key ID' is what is the most compact key value to drive
	 * the hash, not class->key.
	 */
	/*
	 * Whoops, we did it again.. class_idx is invalid.
	 */
	if (DEBUG_LOCKS_WARN_ON(!test_bit(class_idx, lock_classes_in_use)))
		return 0;

	chain_key = curr->curr_chain_key;
	if (!depth) {
		/*
		 * How can we have a chain hash when we ain't got no keys?!
		 */
		if (DEBUG_LOCKS_WARN_ON(chain_key != INITIAL_CHAIN_KEY))
			return 0;
		chain_head = 1;
	}

	hlock->prev_chain_key = chain_key;
	if (separate_irq_context(curr, hlock)) {
		chain_key = INITIAL_CHAIN_KEY;
		chain_head = 1;
	}
	chain_key = iterate_chain_key(chain_key, hlock_id(hlock));

	if (nest_lock && !__lock_is_held(nest_lock, -1)) {
		print_lock_nested_lock_not_held(curr, hlock);
		return 0;
	}

	if (!debug_locks_silent) {
		WARN_ON_ONCE(depth && !hlock_class(hlock - 1)->key);
		WARN_ON_ONCE(!hlock_class(hlock)->key);
	}

	if (!validate_chain(curr, hlock, chain_head, chain_key))
		return 0;

	/* For lock_sync(), we are done here since no actual critical section */
	if (hlock->sync)
		return 1;

	curr->curr_chain_key = chain_key;
	curr->lockdep_depth++;
	check_chain_key(curr);
#ifdef CONFIG_DEBUG_LOCKDEP
	if (unlikely(!debug_locks))
		return 0;
#endif
	if (unlikely(curr->lockdep_depth >= MAX_LOCK_DEPTH)) {
		debug_locks_off();
		print_lockdep_off("BUG: MAX_LOCK_DEPTH too low!");
		printk(KERN_DEBUG "depth: %i  max: %lu!\n",
		       curr->lockdep_depth, MAX_LOCK_DEPTH);

		lockdep_print_held_locks(current);
		debug_show_all_locks();
		dump_stack();

		return 0;
	}

	if (unlikely(curr->lockdep_depth > max_lockdep_depth))
		max_lockdep_depth = curr->lockdep_depth;

	return 1;
}

확인 사항: __lock_acquire()는 먼저 class cache 또는 등록 경로를 찾고, 현재 task의 held_locks 슬롯을 채운 다음 문맥·usage·chain을 검증한다. 보통의 성공 경로에서 검증 뒤에 curr_chain_key와 lockdep_depth를 갱신한다. 이 함수의 이름은 실제 잠금 명령의 성공 여부를 뜻하지 않는다. trylock, sync, 검증 비활성화는 별도 분기다.

함수별 분석

lockdep_init()은 진입 체인에서 제외합니다. 락 API의 lockdep 주석 경로는 lock_acquire()가 시작하고, 검증 가능할 때 __lock_acquire()가 class와 task별 보유 목록을 다룹니다.

그림 2. 일반 lockdep 획득 주석의 실제 순서
lock_acquire()문맥 보존·재귀 방지
class 선택class_cache 또는 register_lock_class
held_lock 슬롯instance·class_idx·문맥 비트 기록
validate_chain()새 chain이면 역순·IRQ 충돌 검사
depth 증가검증을 통과한 일반 획득 기록

held_locks 슬롯 채움은 lockdep_depth++보다 먼저 일어납니다. validate_chain()이 실패하면 일반 성공 경로로 진행하지 않습니다. 고정 버전 __lock_acquire().

표 2. 함수마다 맡은 역할
위치입력·조건변경/판단주요 예외
lockdep_init()부팅 초기화한도·메모리 크기 출력이 발췌에서 class/edge/held stack 생성 없음
lock_acquire()lockdep_map과 획득 속성추적 hook, 검증 가능 시 IRQ 상태 보존 후 내부 호출debug_locks/recursion 조건에서 조기 반환
__lock_acquire()class/subclass·task depthclass 조회, held 슬롯 작성, usage와 chain 검사trylock·sync·비활성 검증은 다른 처리
validate_chain()held lock과 chain key새 chain이면 재귀·역순·IRQ 사용 문제 확인기존 chain은 cache로 재검증 생략 가능

초기화 · 공개 주석 함수 · chain 검증.

구조체와 의존성 그래프

lockdep_map은 lock instance의 검증 메타데이터입니다. 그 key/subclass가 class를 고르고, task의 held_locks는 지금 보유 중이라고 주석된 잠금과 class index를 기록합니다. 그래프의 edge는 class 사이의 관찰된 순서입니다.

그림 3. A→B 관찰 뒤 B→A가 발견되는 경우
관찰된 class 순서
A classB class

task의 held stack에서 A를 보고 B를 획득 주석하면 A→B 후보를 검사합니다.

나중에 반대 순서가 나타나면
B classA class

새 edge가 기존 graph로 되돌아가는지 확인해 잠재적 순환을 경고합니다.

이 그림은 배타적 잠금의 대표 사례입니다. 독자에게 실제 교착이 발생했다고 말하지 않습니다. reader 종류·중첩 주석 등은 검증 규칙을 바꿉니다. lockdep 설계 · 고정 버전 소스.

표 3. 코드에서 추적할 대상
대상담는 정보흔한 오독
lockdep_mapinstance의 key·class cacheinstance 한 개가 class 한 개와 영구히 같은 객체라는 해석
lock_class동일 규칙의 class와 usage/의존성 기록개별 잠금의 획득 상태라는 해석
held_lock현재 task의 class_idx, instance, 문맥·read·trylock 속성전역 graph의 edge 자체라는 해석
curr_chain_key현재 보유 chain의 hash충돌 검사가 필요 없는 절대적 증거라는 해석

검증 분기와 상태 변화

모든 잠금 획득에서 새 그래프 edge가 생기는 것은 아닙니다. trylock은 보유 stack 유지와 신규 의존성 기록을 다르게 다루고, 이미 캐시한 chain은 재검사를 생략할 수 있습니다. 새 chain에서만 검사·추가 분기를 탑니다.

그림 4. 추적·캐시·새 chain의 분기
검증 비활성debug_locks / 검증 제외 경로는 확인 범위를 줄임
trylock 또는 기존 chainheld 기록은 가능; 새 edge 검사 생략 가능
새 chaincheck_deadlock + check_prevs_add
검증 통과일반 경로에서 depth/chain key 갱신

네 칸은 조건에 따른 서로 다른 결과를 비교합니다. 모두 연속 실행되는 단계가 아닙니다. validate_chain()의 조건과 __lock_acquire()의 조기 반환을 함께 보아야 합니다. 검증 코드.

표 4. 대표 분기의 결과
조건검증 처리해석
debug_locks == 0내부 검증 조기 반환그 실행의 경고 부재는 안전 증명이 아님
trylock 또는 check == 0validate_chain()에서 새 dependency 추가 생략held stack 처리 자체와 구별
이미 알려진 chain keychain cache hit로 무거운 검증 재사용새 class edge 검사와 구별
새 chain, 위험한 역순/IRQ 조합재귀·경로·IRQ 사용 규칙 검사 후 경고 가능잠재적 교착 패턴을 발견; 실제 교착 관찰과 다름
통과한 일반 획득 주석chain key 보관 후 lockdep_depth++lock 하드웨어 획득 명령 자체가 아님

검증의 전제와 실패 사례

관찰 범위

lockdep은 실제 실행에서 만난 잠금 class와 순서를 누적합니다. 실행되지 않은 경로, 비활성화된 검증, 잘못된 annotation은 경고 부재로 안전을 증명할 수 없습니다.

순서 역전

A→B를 기록한 뒤 B→A가 가능한 경로가 새로 나타나면 class 그래프에 순환 가능성이 생깁니다. 경고는 미래 교착 가능성이지 이미 멈춘 두 CPU의 증거가 아닙니다.

IRQ 문맥

hardirq/softirq 안전 class와 IRQ 허용 상태에서 쓰인 class의 의존성은 별도 사용 규칙을 검사합니다. 단순한 A↔B 도식만으로 모든 경고를 설명할 수 없습니다.

class 수명

동적 key를 쓰면 register/unregister 및 재사용 시점을 확인합니다. 해제된 class key가 남거나 잘못된 class로 재등록되면 그래프 해석이 어긋날 수 있습니다.

lockdep 설계의 class·IRQ·dependency 규칙 · 고정 버전 구현.

계측과 검증

먼저 대상 커널에 lockdep 검증이 포함됐는지 확인합니다. 경고가 있다면 class 이름, held lock 목록, 취득 위치와 과거 의존성 경로를 함께 읽습니다. 전역 tracefs 설정을 변경하던 예제는 제거했습니다.

표 5. 읽기 중심의 증거와 한계
증거확인할 사실한계
커널 설정의 CONFIG_PROVE_LOCKINGclass 의존성 증명이 빌드에 포함되는지설정만으로 모든 실행 경로를 통과한 것은 아님
lockdep 경고와 스택 trace새 edge, 역순 또는 IRQ 문맥 문제의 보고 지점실제 deadlock 발생 시각/CPU 간 대기 상황과 동일하지 않음
/proc/lockdep · /proc/lockdep_stats지원 빌드에서 class/edge 및 누적 통계설정과 procfs 가용성에 따라 없을 수 있음
lock_acquire() trace hookannotation 진입 지점검증 성공/실제 lock 소유를 뜻하지 않음

읽기 전용 예: grep -E 'CONFIG_PROVE_LOCKING|CONFIG_LOCKDEP' /boot/config-$(uname -r) (배포판의 config 경로가 있을 때), cat /proc/lockdep (존재할 때). 원래의 current_tracer·set_ftrace_filter 쓰기와 공유 trace_pipe 읽기는 이 글에 필요하지 않습니다.

추가 확인 사항

  • 경고의 두 lock은 서로 다른 instance인가, 서로 다른 class인가? key와 subclass를 확인했는가?
  • 새 class edge가 실제 실행 순서 어디에서 처음 관찰됐고, 반대 경로는 어느 stack trace에서 나왔는가?
  • 그 획득은 trylock, nested annotation, read lock 중 어느 방식인가?
  • 해당 커널에서 CONFIG_PROVE_LOCKING과 procfs 계측을 사용할 수 있는가?