전체 흐름: 초기화와 검증은 별도 경로
lockdep은 실제로 관찰한 lock class 순서와 IRQ 사용 맥락을 기록하여 잠재적인 데드락을 경고합니다. 여기서 다루는 lockdep_init() 발췌는 부팅 시 한도와 메모리 사용량을 출력합니다. 의존성 edge를 추가하거나 실제 잠금을 획득하는 함수가 아닙니다.
두 행은 연속 호출 흐름이 아닙니다. lockdep_init()과 lock_acquire()를 원문에서 나란히 비교합니다.
| 구분 | 예시 | lockdep이 보는 것 |
|---|---|---|
| instance | 서로 다른 inode에 내장된 동일 종류의 잠금 두 개 | 개별 lockdep_map과 획득 기록 |
| class | 두 잠금에 적용되는 같은 잠금 규칙 | key/subclass가 구분하는 논리적 분류와 class 간 의존성 |
edge A → B | A 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
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
__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별 보유 목록을 다룹니다.
held_locks 슬롯 채움은 lockdep_depth++보다 먼저 일어납니다. validate_chain()이 실패하면 일반 성공 경로로 진행하지 않습니다. 고정 버전 __lock_acquire().
| 위치 | 입력·조건 | 변경/판단 | 주요 예외 |
|---|---|---|---|
lockdep_init() | 부팅 초기화 | 한도·메모리 크기 출력 | 이 발췌에서 class/edge/held stack 생성 없음 |
lock_acquire() | lockdep_map과 획득 속성 | 추적 hook, 검증 가능 시 IRQ 상태 보존 후 내부 호출 | debug_locks/recursion 조건에서 조기 반환 |
__lock_acquire() | class/subclass·task depth | class 조회, held 슬롯 작성, usage와 chain 검사 | trylock·sync·비활성 검증은 다른 처리 |
validate_chain() | held lock과 chain key | 새 chain이면 재귀·역순·IRQ 사용 문제 확인 | 기존 chain은 cache로 재검증 생략 가능 |
구조체와 의존성 그래프
lockdep_map은 lock instance의 검증 메타데이터입니다. 그 key/subclass가 class를 고르고, task의 held_locks는 지금 보유 중이라고 주석된 잠금과 class index를 기록합니다. 그래프의 edge는 class 사이의 관찰된 순서입니다.
task의 held stack에서 A를 보고 B를 획득 주석하면 A→B 후보를 검사합니다.
새 edge가 기존 graph로 되돌아가는지 확인해 잠재적 순환을 경고합니다.
이 그림은 배타적 잠금의 대표 사례입니다. 독자에게 실제 교착이 발생했다고 말하지 않습니다. reader 종류·중첩 주석 등은 검증 규칙을 바꿉니다. lockdep 설계 · 고정 버전 소스.
| 대상 | 담는 정보 | 흔한 오독 |
|---|---|---|
lockdep_map | instance의 key·class cache | instance 한 개가 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에서만 검사·추가 분기를 탑니다.
네 칸은 조건에 따른 서로 다른 결과를 비교합니다. 모두 연속 실행되는 단계가 아닙니다. validate_chain()의 조건과 __lock_acquire()의 조기 반환을 함께 보아야 합니다. 검증 코드.
| 조건 | 검증 처리 | 해석 |
|---|---|---|
debug_locks == 0 | 내부 검증 조기 반환 | 그 실행의 경고 부재는 안전 증명이 아님 |
trylock 또는 check == 0 | validate_chain()에서 새 dependency 추가 생략 | held stack 처리 자체와 구별 |
| 이미 알려진 chain key | chain cache hit로 무거운 검증 재사용 | 새 class edge 검사와 구별 |
| 새 chain, 위험한 역순/IRQ 조합 | 재귀·경로·IRQ 사용 규칙 검사 후 경고 가능 | 잠재적 교착 패턴을 발견; 실제 교착 관찰과 다름 |
| 통과한 일반 획득 주석 | chain key 보관 후 lockdep_depth++ | lock 하드웨어 획득 명령 자체가 아님 |
검증의 전제와 실패 사례
lockdep은 실제 실행에서 만난 잠금 class와 순서를 누적합니다. 실행되지 않은 경로, 비활성화된 검증, 잘못된 annotation은 경고 부재로 안전을 증명할 수 없습니다.
A→B를 기록한 뒤 B→A가 가능한 경로가 새로 나타나면 class 그래프에 순환 가능성이 생깁니다. 경고는 미래 교착 가능성이지 이미 멈춘 두 CPU의 증거가 아닙니다.
hardirq/softirq 안전 class와 IRQ 허용 상태에서 쓰인 class의 의존성은 별도 사용 규칙을 검사합니다. 단순한 A↔B 도식만으로 모든 경고를 설명할 수 없습니다.
동적 key를 쓰면 register/unregister 및 재사용 시점을 확인합니다. 해제된 class key가 남거나 잘못된 class로 재등록되면 그래프 해석이 어긋날 수 있습니다.
계측과 검증
먼저 대상 커널에 lockdep 검증이 포함됐는지 확인합니다. 경고가 있다면 class 이름, held lock 목록, 취득 위치와 과거 의존성 경로를 함께 읽습니다. 전역 tracefs 설정을 변경하던 예제는 제거했습니다.
| 증거 | 확인할 사실 | 한계 |
|---|---|---|
커널 설정의 CONFIG_PROVE_LOCKING | class 의존성 증명이 빌드에 포함되는지 | 설정만으로 모든 실행 경로를 통과한 것은 아님 |
| lockdep 경고와 스택 trace | 새 edge, 역순 또는 IRQ 문맥 문제의 보고 지점 | 실제 deadlock 발생 시각/CPU 간 대기 상황과 동일하지 않음 |
/proc/lockdep · /proc/lockdep_stats | 지원 빌드에서 class/edge 및 누적 통계 | 설정과 procfs 가용성에 따라 없을 수 있음 |
lock_acquire() trace hook | annotation 진입 지점 | 검증 성공/실제 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 계측을 사용할 수 있는가?