Lockdep (Lock Dependency Validator)

Linux 커널의 잠금(Lock) 의존성 검증기 lockdep를 분석합니다. 런타임에 잠금 획득 순서를 추적하여 잠재적 교착 상태(deadlock)를 사전에 탐지하는 메커니즘을 lock_class 개념, 의존성 그래프 구축, BFS 순환 탐지, IRQ 안전성 검증, wait_type 검사 등 내부 구현 수준에서 설명합니다. PROVE_LOCKING/LOCK_STAT 설정과 /proc 인터페이스, 실전 경고 메시지 해석과 디버깅(Debugging) 기법까지 포괄합니다.

전제 조건: 동기화 기법, Spinlock, Mutex 문서를 먼저 읽으세요. lockdep는 모든 잠금 프리미티브를 계측(instrument)하므로, 각 잠금의 기본 동작을 이해해야 합니다.
일상 비유: lockdep는 교통 경찰관과 같습니다. 교차로(잠금)를 통과하는 차량(스레드(Thread))의 순서를 기록하고, "이 순서로 교차로를 지나면 교착 상태가 발생할 수 있다"는 것을 실제 사고(deadlock)가 나기 전에 경고합니다. N개의 개별 잠금을 M개의 잠금 클래스로 축소하여, 한 번이라도 위험한 순서가 관찰되면 바로 보고합니다.

핵심 요약

  • 잠금 클래스(lock class) — 같은 소스 코드 위치에서 초기화된 모든 잠금 인스턴스를 하나의 클래스로 묶어 상태 공간을 축소합니다.
  • 의존성 그래프 — "클래스 A를 잡은 채 클래스 B를 획득"하면 A→B 간선을 추가합니다. 그래프에 순환이 생기면 잠재적 교착입니다.
  • IRQ 안전성 — 인터럽트(Interrupt) 컨텍스트에서 사용되는 잠금과 그렇지 않은 잠금 사이의 의존 관계를 추적하여, IRQ-unsafe→IRQ-safe 역전을 감지합니다.
  • wait_type 검사 — atomic 컨텍스트에서 sleep 가능 잠금(mutex 등)을 획득하려는 시도를 감지합니다.
  • 런타임 증명 — PROVE_LOCKING이 활성화되면, 실제 교착이 발생하지 않아도 잠재적 교착 경로를 보고합니다.

단계별 이해

  1. 교착 상태 이론 이해
    두 스레드가 서로 상대방이 보유한 잠금을 기다리는 ABBA 패턴을 파악합니다.
  2. lock_class 개념 파악
    동일 소스 위치의 잠금들이 하나의 클래스로 분류되는 원리를 이해합니다.
  3. 의존성 그래프 구축 추적
    lock_acquire/lock_release 후킹이 어떻게 간선을 추가하는지 따라갑니다.
  4. 순환 탐지 알고리즘 분석
    BFS로 의존성 그래프의 순환을 탐지하는 과정을 확인합니다.
  5. 경고 메시지 해석과 디버깅 적용
    실제 커널 로그의 lockdep 경고를 읽고 수정하는 방법을 익힙니다.
관련 표준: Ingo Molnar, "Runtime locking correctness validator" (lockdep design document, Documentation/locking/lockdep-design.rst). 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

이론적 배경: 잠금 의존성과 순환 감지

교착 상태(deadlock)는 Coffman 등(1971)이 정의한 4가지 필요 조건 — 상호 배제(Mutual Exclusion), 점유 대기(hold and wait), 비선점(no preemption), 순환 대기(circular wait) — 이 동시에 충족될 때 발생합니다. lockdep는 이 중 순환 대기 조건의 가능성을 런타임에 검증합니다.

ABBA 패턴: 가장 단순한 교착

두 스레드가 두 잠금 A, B를 반대 순서로 획득하면 교착이 발생할 수 있습니다:

/* Thread 1 */           /* Thread 2 */
lock(A);                  lock(B);
lock(B);  /* 대기 */     lock(A);  /* 대기 → deadlock */
unlock(B);                unlock(A);
unlock(A);                unlock(B);

lockdep는 Thread 1이 A→B 순서로 잠금을 획득하는 것을 관찰한 뒤, Thread 2가 B→A를 시도하면 — 실제 교착이 발생하지 않더라도 — 순환 의존성 경고를 발생시킵니다.

ABBA 교착 패턴 Lock A Lock B Thread 1 Thread 2 holds wants holds wants Circular Dependency = Potential Deadlock

그래프 이론 기반 접근

잠금 의존성을 방향 그래프(directed graph)로 모델링합니다. 정점(vertex)은 잠금 클래스, 간선(edge)은 "A를 보유한 상태에서 B를 획득"이라는 관계입니다. 이 그래프에 순환(cycle)이 존재하면 교착 가능성이 있습니다. lockdep는 새로운 간선이 추가될 때마다 순환 검사를 수행합니다.

그래프 요소lockdep 대응설명
정점(Vertex)lock_class같은 소스 위치에서 초기화된 잠금 그룹
간선(Edge)lock_listA→B: A를 보유한 채 B를 획득
순환(Cycle)교착 가능성A→B→C→A 같은 순환 경로
경로(Path)의존성 체인순환 보고 시 전체 경로 출력

lockdep 전체 아키텍처

lockdep 서브시스템은 kernel/locking/lockdep.c에 구현되어 있으며, 약 6000줄 이상의 코드로 구성됩니다. 핵심 데이터 구조와 동작 흐름을 정리합니다.

lockdep 아키텍처 개요 Locking API: spin_lock / mutex_lock / down_read / ... lock_acquire() lock_release() lockdep 핵심 엔진 (kernel/locking/lockdep.c) lock_class 클래스 해시 테이블 lock_list 의존성 간선 lock_chain 의존성 체인 캐시 held_lock Per-task 스택 /proc/lockdep* 경고 출력 (dmesg)

핵심 자료구조 요약

자료구조위치역할
struct lock_classinclude/linux/lockdep_types.h잠금 클래스 메타데이터 (이름, 키, 의존성 목록)
struct lock_class_keyinclude/linux/lockdep_types.h잠금 클래스 식별자 (정적 변수 주소)
struct lock_listkernel/locking/lockdep_internals.h의존성 그래프 간선 (forward/backward)
struct lock_chainkernel/locking/lockdep_internals.h의존성 체인 해시(Hash) 캐시(Cache)
struct held_lockinclude/linux/lockdep_types.h현재 태스크(Task)가 보유 중인 잠금 정보
struct lockdep_mapinclude/linux/lockdep_types.h잠금 인스턴스와 lock_class의 연결

lock_class: 잠금 클래스 개념

lockdep의 핵심 통찰은 개별 잠금 인스턴스가 아닌 잠금 클래스 단위로 의존성을 추적한다는 것입니다. 예를 들어, 시스템에 1000개의 inode가 있고 각 inode에 i_rwsem이 있다면, lockdep는 이 1000개의 잠금을 하나의 lock_class로 취급합니다.

/* include/linux/lockdep_types.h */
struct lock_class {
    struct hlist_node       hash_entry;      /* 해시 테이블 체인 */
    struct list_head        lock_entry;      /* 전역 목록 */
    struct list_head        locks_after;     /* forward 의존성: 이 클래스 후에 획득된 잠금들 */
    struct list_head        locks_before;    /* backward 의존성: 이 클래스 전에 획득된 잠금들 */
    const struct lockdep_subclass_key *key;  /* 클래스 식별 키 */
    unsigned int            subclass;       /* 서브클래스 번호 */
    unsigned int            dep_gen_id;     /* 의존성 생성 ID */
    unsigned long           usage_mask;     /* IRQ 사용 상태 비트마스크 */
    const char             *name;          /* 잠금 이름 (디버그용) */
    short                   name_version;   /* 이름 버전 */
    unsigned long           contention_point[LOCKSTAT_POINTS]; /* 경합 지점 */
    unsigned long           contending_point[LOCKSTAT_POINTS]; /* 피경합 지점 */
};
코드 설명
  • hash_entryhash_entrylock_class를 해시 테이블에 연결하는 노드입니다. 키 주소의 해시 값으로 버킷을 결정하여 O(1) 조회를 가능하게 합니다 (include/linux/lockdep_types.h).
  • locks_after / locks_beforelocks_after는 이 클래스의 잠금을 보유한 상태에서 획득된 다른 클래스의 목록(forward 의존성)이고, locks_before는 이 클래스 전에 획득된 클래스 목록(backward 의존성)입니다. 이 두 리스트가 의존성 그래프의 간선을 구성합니다.
  • keykeylock_class_key의 서브키 주소를 가리키며, 이 주소가 클래스의 고유 식별자 역할을 합니다. 정적 변수의 주소이므로 커널 수명 동안 유일합니다.
  • usage_maskusage_mask는 이 클래스의 잠금이 어떤 IRQ 컨텍스트에서 사용되었는지를 비트 플래그로 기록합니다. LOCK_USED_IN_HARDIRQ, LOCK_ENABLED_SOFTIRQ 등의 비트가 설정되며, IRQ 안전성 검증의 핵심 데이터입니다.
클래스 축소 효과: N개의 잠금 인스턴스를 M개의 클래스로 축소(N >> M)하여, 의존성 그래프의 정점 수를 극적으로 줄입니다. 일반적인 커널에서 잠금 인스턴스는 수만 개이지만, 잠금 클래스는 수천 개 수준입니다. /proc/lockdep_stats에서 현재 클래스 수를 확인할 수 있습니다.
N개 잠금 인스턴스 → M개 lock_class 잠금 인스턴스 (N개) inode[0].i_rwsem inode[1].i_rwsem inode[2].i_rwsem ... (1000개) sb->s_lock dentry->d_lock 축소 lock_class (M개) &type->i_rwsem_key (1 class) &type->s_lock_key (1 class) &type->d_lock_key (1 class)

usage_mask: IRQ 사용 상태 추적

lock_classusage_mask 필드는 해당 잠금 클래스가 어떤 컨텍스트에서 사용되었는지를 비트마스크로 기록합니다:

비트매크로(Macro)의미
0LOCK_USED_IN_HARDIRQ하드 IRQ 핸들러(Handler)에서 사용됨
1LOCK_USED_IN_SOFTIRQ소프트 IRQ에서 사용됨
2LOCK_ENABLED_HARDIRQ하드 IRQ 활성 상태에서 사용됨
3LOCK_ENABLED_SOFTIRQ소프트 IRQ 활성 상태에서 사용됨
4LOCK_USED한 번이라도 획득됨

lock_class_key와 정적 키

lock_class_key는 잠금 클래스를 식별하는 핵심 요소입니다. 잠금이 정의된 소스 코드 위치에 정적 변수로 생성되며, 이 변수의 주소가 클래스의 고유 식별자 역할을 합니다.

/* include/linux/lockdep_types.h */
struct lock_class_key {
    union {
        struct hlist_node           hash_entry;
        struct lockdep_subclass_key  subkeys[MAX_LOCKDEP_SUBCLASSES];
    };
};

/* 매크로 전개 예: DEFINE_MUTEX(my_lock) */
static struct lock_class_key __key;  /* 정적 키 — 주소가 클래스 ID */
__mutex_init(&my_lock, "my_lock", &__key);
코드 설명
  • lock_class_keylock_class_key 구조체는 잠금 클래스를 식별하는 정적 키입니다. subkeys 배열은 MAX_LOCKDEP_SUBCLASSES(기본 8)개의 서브클래스를 지원하여, 같은 종류의 잠금이라도 계층별로 구분할 수 있습니다 (include/linux/lockdep_types.h).
  • static ... __keyDEFINE_MUTEX 같은 매크로가 전개되면, 호출 사이트마다 static struct lock_class_key __key가 생성됩니다. 정적 변수이므로 주소가 프로그램 수명 동안 고유하며, 이 주소 자체가 클래스 ID로 사용됩니다.
  • __mutex_init__mutex_init()는 mutex를 초기화하면서 &__keylockdep_map.key에 저장합니다. 이후 lock_acquire() 호출 시 이 키로 lock_class를 조회하거나 새로 등록합니다.
동적 잠금 초기화 시 주의: 동적으로 생성된 잠금(예: kmalloc으로 할당된 구조체(Struct) 내의 mutex)은 mutex_init() 매크로가 호출 사이트에 정적 키를 자동 생성합니다. 반복문이나 함수 안에서 여러 잠금을 초기화하면 모두 같은 키를 공유하므로, 서로 다른 역할의 잠금이 같은 클래스로 분류될 수 있습니다. 이 경우 lockdep_set_class()로 명시적으로 다른 클래스를 지정해야 합니다.

키 등록 과정

잠금이 처음 lock_acquire()를 거칠 때, lockdep는 키 주소로 해시 테이블(Hash Table)을 검색합니다:

  1. 키 주소가 이미 등록된 lock_class에 매핑(Mapping)되어 있으면 해당 클래스를 사용합니다.
  2. 처음 보는 키이면 새로운 lock_class를 할당하고 해시 테이블에 등록합니다.
  3. lockdep_map 구조체의 class_cache에 캐싱하여 이후 조회를 가속합니다.
/* kernel/locking/lockdep.c — register_lock_class() 핵심 흐름 */
static struct lock_class *
register_lock_class(struct lockdep_map *lock,
                    unsigned int subclass, int force)
{
    struct lockdep_subclass_key *key;
    struct hlist_head *hash_head;
    struct lock_class *class;

    key = lock->key->subkeys + subclass;
    hash_head = classhashentry(key);

    /* 해시 테이블에서 기존 클래스 검색 */
    hlist_for_each_entry_rcu(class, hash_head, hash_entry) {
        if (class->key == key)
            return class;
    }
    /* 새 클래스 할당 — 정적 배열에서 가져옴 */
    class = lock_classes + nr_lock_classes++;
    /* ... 초기화 및 해시 등록 ... */
    return class;
}
lock_class_key → 해시 테이블 → lock_class classhash_table bucket[0] bucket[1] bucket[2] ... bucket[N-1] lock_class_key (&__key) hash lock_class "i_rwsem" lock_class "d_lock"

잠금 의존성 그래프 구축

lockdep는 스레드가 잠금을 획득할 때마다, 현재 보유 중인 모든 잠금과 새로 획득하는 잠금 사이에 의존성 간선(dependency edge)을 추가합니다. 이 간선은 struct lock_list로 표현됩니다.

/* kernel/locking/lockdep_internals.h */
struct lock_list {
    struct list_head        entry;
    struct lock_class       *class;        /* 대상 클래스 */
    struct lock_class       *links_to;    /* 연결된 클래스 */
    struct lock_trace       *trace;       /* 의존성이 기록된 스택 트레이스 */
    u16                     distance;    /* 재귀 깊이 */
    u8                      dep;         /* 의존성 유형 비트 */
    struct lock_list       *parent;      /* BFS 경로 추적용 */
};

간선 생성 과정

스레드가 잠금 A를 보유한 채 잠금 B를 획득하면:

  1. A의 locks_after 리스트에 B를 가리키는 lock_list를 추가합니다 (forward dependency).
  2. B의 locks_before 리스트에 A를 가리키는 lock_list를 추가합니다 (backward dependency).
  3. 이미 같은 간선이 존재하면 건너뜁니다 (중복 방지).
/* kernel/locking/lockdep.c — check_prev_add() 간선 추가 핵심 */
static int check_prev_add(
    struct task_struct *curr,
    struct held_lock *prev,     /* 기존 보유 잠금 */
    struct held_lock *next,     /* 새로 획득하는 잠금 */
    ...)
{
    /* 1. 순환 검사 (BFS) */
    ret = check_noncircular(next, prev, ...);
    if (!ret)
        return 0;  /* 순환 발견 → 경고 출력 */

    /* 2. IRQ 안전성 검사 */
    ret = check_irq_usage(curr, prev, next);

    /* 3. 간선 추가 */
    add_lock_to_list(prev_class, next_class,
                     &prev_class->locks_after, ...);
    add_lock_to_list(next_class, prev_class,
                     &next_class->locks_before, ...);
}
코드 설명
  • check_noncircular()새 간선 prev→next를 추가하기 전에 check_noncircular()로 next에서 prev로 도달 가능한지 BFS 탐색합니다. 도달 가능하면 순환(교착 가능성)이므로 경고를 출력하고 간선 추가를 중단합니다 (kernel/locking/lockdep.c).
  • check_irq_usage()IRQ 안전성을 검사합니다. prev가 IRQ 활성 상태에서 사용되고 next가 IRQ 핸들러에서 사용되는 경우, IRQ 발생 시 역전 교착이 가능하므로 이를 감지합니다.
  • add_lock_to_list() (양방향)검증 통과 후 prev_class->locks_after에 next를, next_class->locks_before에 prev를 추가하여 양방향 의존성 간선을 기록합니다. 양방향 저장은 forward/backward BFS 탐색 모두에 사용됩니다.
의존성 그래프 구축 예시 A B C D lock(A)→lock(B) lock(B)→lock(C) lock(C)→lock(D) lock(D)→lock(A)? D→A 간선 추가 시 순환 감지: A→B→C→D→A

순환 탐지 알고리즘

lockdep는 새로운 의존성 간선 A→B를 추가하기 전에, B에서 A로 도달 가능한 경로가 있는지 BFS(너비 우선 탐색)로 검사합니다. 경로가 존재하면 A→B 간선을 추가했을 때 순환이 생기므로, 잠재적 교착으로 보고합니다.

/* kernel/locking/lockdep.c — check_noncircular() */
static enum bfs_result
check_noncircular(struct held_lock *src,
                  struct held_lock *target, ...)
{
    /*
     * src의 forward 의존성을 BFS로 탐색하여
     * target에 도달 가능한지 확인
     * → 도달 가능하면 순환 존재
     */
    result = __bfs_forwards(&this, target,
                            class_equal, ...);
    if (result == BFS_RMATCH)
        print_circular_bug(...);  /* 순환 보고 */
    return result;
}
코드 설명
  • check_noncircular()새 의존성 간선 src→target을 추가할 때, target의 forward 의존성 그래프를 BFS로 탐색하여 src에 다시 도달할 수 있는지 확인합니다. 도달 가능하면 순환 의존성이 존재하므로 교착 가능 경로입니다 (kernel/locking/lockdep.c).
  • __bfs_forwards()__bfs_forwards()locks_after 리스트를 따라 너비 우선 탐색을 수행합니다. class_equal 콜백으로 각 노드가 target과 같은 클래스인지 비교합니다.
  • BFS_RMATCH탐색 결과가 BFS_RMATCH이면 순환이 발견된 것입니다. 이때 print_circular_bug()lock_listparent 포인터를 역추적하여 전체 순환 경로를 커널 로그에 출력합니다.

BFS 구현 세부사항

구현 요소설명
커널 스택 위의 lock_list 배열 (재귀 대신 반복)
방문 표시dep_gen_id 필드로 세대 번호 비교 (별도 비트맵(Bitmap) 불필요)
간선 순회forward 탐색은 locks_after, backward 탐색은 locks_before 리스트 사용
종료 조건target 클래스에 도달하면 BFS_RMATCH, 큐 소진 시 BFS_RNOMATCH
경로 복원lock_listparent 포인터를 역추적(Backtrace)하여 순환 경로 출력
성능 최적화: lockdep는 의존성 체인을 해시하여 lock_chain 캐시에 저장합니다. 이전에 검증된 체인과 동일한 잠금 순서이면 BFS를 건너뛰어 오버헤드(Overhead)를 줄입니다. /proc/lockdep_chains에서 캐시된 체인 수를 확인할 수 있습니다.

lock_acquire() 후킹 메커니즘

모든 잠금 API(spin_lock(), mutex_lock(), down_read() 등)는 실제 잠금 획득 전에 lock_acquire()를 호출하여 lockdep에 잠금 획득 의도를 알립니다. 이 함수가 lockdep의 핵심 진입점(Entry Point)입니다.

/* kernel/locking/lockdep.c */
void lock_acquire(
    struct lockdep_map *lock,  /* 잠금 인스턴스 */
    unsigned int subclass,    /* 서브클래스 번호 */
    int trylock,               /* trylock 여부 */
    int read,                  /* 읽기 잠금 여부 */
    int check,                 /* 검사 수준 */
    struct lockdep_map *nest_lock, /* 중첩 잠금 */
    unsigned long ip)        /* 호출 위치 IP */
{
    struct task_struct *curr = current;

    /* 1. lockdep 비활성화 상태이면 리턴 */
    if (unlikely(!debug_locks))
        return;

    /* 2. 재진입 방지 */
    if (unlikely(curr->lockdep_recursion))
        return;

    raw_local_irq_save(flags);
    curr->lockdep_recursion++;

    /* 3. 핵심 검증 로직 */
    __lock_acquire(lock, subclass, trylock,
                   read, check, irqs_disabled_flags(flags),
                   nest_lock, ip, 0, 0);

    curr->lockdep_recursion--;
    raw_local_irq_restore(flags);
}
코드 설명
  • debug_locks 검사lockdep 내부에서 오류가 발생하면 debug_locks가 0으로 설정되어 이후 모든 검증을 건너뜁니다. 이는 lockdep 자체의 버그로 인한 연쇄 오류를 방지합니다.
  • lockdep_recursionlockdep_recursion 카운터는 lockdep 내부 코드가 다시 잠금을 획득할 때 무한 재귀를 방지합니다. 예를 들어 lockdep가 해시 테이블을 접근하면서 내부적으로 잠금을 사용할 수 있으므로, 재진입 시 즉시 리턴합니다.
  • raw_local_irq_save / restorelockdep 검증 중에는 인터럽트를 비활성화하여 검증 로직의 원자성을 보장합니다. raw_ 접두사는 lockdep 추적 대상이 아닌 저수준 IRQ 제어 함수임을 나타냅니다.
  • __lock_acquire()실제 검증 로직은 __lock_acquire()에 위임됩니다. 이 함수가 클래스 등록, held_lock 스택 관리, 체인 캐시 조회, 의존성 그래프 검증을 모두 수행합니다 (kernel/locking/lockdep.c).

__lock_acquire() 처리 단계

__lock_acquire() 처리 흐름 1. lock_class 조회/등록 2. held_lock 스택에 push 3. wait_type 검사 4. 체인 해시 검사 5. validate_chain() → 순환/IRQ 검사 통과 경고 출력 register_lock_class() curr->held_locks[depth++] check_wait_context() lookup_chain_cache() check_prev_add()

held_lock 구조체는 현재 태스크가 보유 중인 잠금의 정보를 담습니다. task_structheld_locks 배열은 최대 MAX_LOCK_DEPTH(기본 48)개까지 중첩을 추적합니다:

/* include/linux/lockdep_types.h */
struct held_lock {
    u64                     prev_chain_key; /* 이전 체인 해시 */
    unsigned long           acquire_ip;    /* 획득 위치 IP */
    struct lockdep_map      *instance;      /* 잠금 인스턴스 */
    struct lockdep_map      *nest_lock;     /* 중첩 잠금 */
    unsigned int            class_idx:13;  /* lock_class 인덱스 */
    unsigned int            irq_context:2; /* IRQ 컨텍스트 */
    unsigned int            trylock:1;     /* trylock 호출 */
    unsigned int            read:2;        /* 읽기 잠금 */
    unsigned int            check:1;       /* 검사 활성화 */
    unsigned int            hardirqs_off:1; /* 하드 IRQ 비활성 */
    unsigned int            waittime_stamp; /* LOCK_STAT 대기 시작 */
    unsigned int            holdtime_stamp; /* LOCK_STAT 보유 시작 */
};
코드 설명
  • prev_chain_keyprev_chain_key는 이 잠금이 스택에 추가되기 직전의 체인 해시 값입니다. 잠금이 해제될 때 이 값으로 체인 키를 복원하여, 비순서 해제(out-of-order unlock) 시에도 체인 해시를 올바르게 유지합니다.
  • class_idx:13class_idx는 전역 lock_classes[] 배열의 인덱스로, 13비트이므로 최대 8,192개의 잠금 클래스를 지원합니다. 비트 필드를 사용하여 held_lock 크기를 최소화합니다.
  • irq_context:2irq_context는 이 잠금이 획득된 IRQ 컨텍스트를 기록합니다 (0: 프로세스, 1: softirq, 2: hardirq). validate_chain()에서 같은 IRQ 컨텍스트의 잠금끼리만 의존성을 추적하는 데 사용됩니다.
  • held_locks 스택held_lock 구조체는 task_struct.held_locks[] 배열에 스택처럼 저장됩니다. 최대 깊이는 MAX_LOCK_DEPTH(48)이며, 현재 태스크가 보유 중인 모든 잠금의 정보를 유지합니다 (include/linux/lockdep_types.h).

lock_release() 후킹 메커니즘

잠금 해제 시 lock_release()가 호출됩니다. lock_acquire()에 비해 단순하지만, 중요한 일관성 검사를 수행합니다.

/* kernel/locking/lockdep.c */
void lock_release(
    struct lockdep_map *lock,
    unsigned long ip)
{
    /* 1. held_lock 스택에서 해당 잠금 찾기 */
    /* 2. 스택에서 제거 (LIFO 아닌 경우 경고) */
    /* 3. 잠금 깊이(depth) 감소 */
    /* 4. LOCK_STAT: 보유 시간 기록 */
    __lock_release(lock, ip);
}

해제 시 검사 항목

검사조건경고 유형
잠금 존재 확인held_lock 스택에 해당 잠금이 없음release without acquire
교차 해제다른 태스크에서 해제 시도lock held by different task
비순서 해제LIFO 순서가 아닌 해제경고 없음 (허용되지만 기록됨)

IRQ 안전성 검증

lockdep는 잠금의 IRQ 안전성을 추적하여, 인터럽트 컨텍스트에서 발생할 수 있는 교착을 감지합니다. 핵심 규칙은 단순합니다:

IRQ 안전성 규칙: 잠금 A가 IRQ 핸들러에서 사용되면(IRQ-safe), A를 IRQ가 활성화된 상태에서 획득하는 다른 모든 잠금 B에 대해, B의 의존성 경로에 IRQ-unsafe 잠금이 있으면 안 됩니다. 그렇지 않으면 인터럽트가 B를 보유한 상태에서 A를 획득하려고 시도할 때 교착이 발생합니다.
IRQ-unsafe → IRQ-safe 교착 시나리오 프로세스 컨텍스트 spin_lock(&B); /* IRQ 활성 */ spin_lock(&A); /* B→A 의존성 (IRQ-unsafe) */ IRQ 핸들러 /* B 보유 중 인터럽트 발생 */ spin_lock(&A); /* A는 IRQ-safe 잠금 */ 교착 발생 경로 프로세스: B 보유 → A 대기 | IRQ: A 보유 시도 → B 기다림 (같은 CPU) 해결: spin_lock_irqsave(&B, flags) — B 획득 시 IRQ 비활성화

IRQ 상태 추적 메커니즘

lockdep는 각 잠금 클래스에 대해 4가지 IRQ 사용 상태를 추적합니다:

상태표기의미
ever held in hardirq+하드 IRQ 핸들러에서 획득된 적 있음
ever held with hardirq enabled-하드 IRQ 활성 상태에서 획득된 적 있음
ever held in softirq?소프트 IRQ에서 획득된 적 있음
ever held with softirq enabled.소프트 IRQ 활성 상태에서 획득된 적 있음
/* /proc/lockdep 출력 예시 */
 all lock classes:
 ---- ----  rcu_read_lock
 ..+. ..+.  &rq->__lock
 ..-- ..--  &sb->s_type->i_lock_key

wait_type 검사: sleep-in-atomic 감지

lockdep의 wait_type 검사는 잘못된 컨텍스트에서 슬립(Sleep) 가능 잠금을 사용하는 것을 방지합니다. 각 잠금은 생성 시 wait_type이 지정됩니다:

wait_type상수의미예시
LD_WAIT_FREE0어디서든 안전 (wait 없음)trylock 계열
LD_WAIT_SPIN1busy-wait, 선점(Preemption) 불가spinlock_t
LD_WAIT_CONFIG2설정에 따라 다름PREEMPT_RT의 spinlock_t
LD_WAIT_SLEEP3슬립 가능mutex, rw_semaphore
/* kernel/locking/lockdep.c — check_wait_context() */
static int check_wait_context(
    struct task_struct *curr,
    struct held_lock *next)
{
    /*
     * 현재 컨텍스트의 wait_type(innermost lock 기준)과
     * 새로 획득하려는 잠금의 wait_type을 비교
     *
     * 규칙: inner wait_type <= outer wait_type
     * 위반 시: "possible sleeping function called from"
     */
    short curr_inner = current_wait_type(curr);
    short next_outer = next->class->wait_type_outer;

    if (curr_inner < next_outer)
        return 0;  /* 위반: spin 컨텍스트에서 sleep lock 시도 */
}
일반적인 위반 사례: spin_lock()으로 spinlock을 보유한 상태에서 mutex_lock()을 호출하면, LD_WAIT_SPIN 컨텍스트에서 LD_WAIT_SLEEP 잠금을 시도하므로 경고가 발생합니다. 해결 방법은 mutex를 spinlock 전에 획득하거나, 설계를 변경하는 것입니다.

lockdep 어노테이션 (nest_lock, subclass)

커널의 일부 코드는 의도적으로 같은 클래스의 잠금을 여러 개 동시에 보유합니다 (예: inode 잠금 중첩). lockdep는 이를 "possible recursive locking"으로 오경보할 수 있으므로, 개발자는 어노테이션으로 합법적인 중첩임을 표시합니다.

서브클래스 어노테이션

/* 같은 종류의 잠금을 중첩 획득 — 서브클래스로 구분 */
mutex_lock_nested(&parent->i_mutex, I_MUTEX_PARENT);
mutex_lock_nested(&child->i_mutex, I_MUTEX_CHILD);
/*
 * I_MUTEX_PARENT = 0, I_MUTEX_CHILD = 1
 * lockdep는 subclass 0과 subclass 1을 다른 클래스로 취급
 * → recursive locking 경고 회피
 */

/* spin_lock_nested 예시 */
spin_lock_nested(&lock, SINGLE_DEPTH_NESTING);

lockdep_set_class / lockdep_set_subclass

/* 잠금에 다른 클래스 키 지정 */
static struct lock_class_key my_special_key;
lockdep_set_class(&lock->dep_map, &my_special_key);

/* 서브클래스만 변경 */
lockdep_set_subclass(&lock->dep_map, 2);

nest_lock 어노테이션

/* nest_lock: 상위 잠금을 중첩 기준으로 지정 */
mutex_lock_nest_lock(&child->i_mutex, &parent->i_mutex);
/*
 * child의 i_mutex는 parent의 i_mutex를 보유한 상태에서만
 * 획득된다고 lockdep에 알림
 */

주요 어노테이션 API 정리

API용도
lockdep_assert_held(lock)lock이 현재 보유 중인지 런타임 검증
lockdep_assert_held_write(lock)쓰기 잠금 보유 검증
lockdep_assert_not_held(lock)lock이 보유되지 않았음을 검증
lock_acquire_exclusive(lock)배타적 잠금 획득 표시
lock_acquire_shared(lock)공유 잠금 획득 표시
lockdep_set_novalidate_class(lock)해당 잠금의 검증 비활성화
lockdep_pin_lock(lock)잠금이 핀 해제 전까지 해제 불가 표시

Cross-release 의존성

일반적인 잠금은 같은 컨텍스트(함수/스레드)에서 획득과 해제가 이루어집니다. 하지만 일부 동기화 패턴은 한 컨텍스트에서 획득하고 다른 컨텍스트에서 해제합니다 — completion, 페이지(Page) 잠금 등이 그 예입니다.

cross-release의 도전

lockdep의 기본 모델은 held_lock 스택이 LIFO(또는 유사 LIFO)로 동작한다고 가정합니다. Cross-release 패턴은 이 가정을 깨뜨리므로, 일반 lockdep로는 추적이 어렵습니다.

/* Cross-release 패턴 예시: completion */
/* Thread A */
wait_for_completion(&done);  /* "잠금 획득" — 여기서 대기 */

/* Thread B */
complete(&done);             /* "잠금 해제" — 다른 스레드에서 해제 */
현재 상태: Cross-release lockdep 지원은 커널 v4.14에서 Byungchul Park에 의해 도입되었다가, 복잡성과 성능 문제로 v4.15에서 제거되었습니다. 현재는 completion 등의 cross-release 의존성은 lockdep로 추적되지 않습니다. lockdep_assert_held() 같은 수동 검증이 대안입니다.

/proc/lockdep, /proc/lockdep_stats, /proc/lockdep_chains

lockdep는 /proc 파일시스템(Filesystem)을 통해 현재 상태를 노출합니다. 디버깅과 시스템 분석에 핵심적인 인터페이스입니다.

/proc/lockdep

등록된 모든 잠금 클래스와 그 의존성을 출력합니다:

# 등록된 잠금 클래스 목록 조회
$ cat /proc/lockdep | head -20
all lock classes:
 ---- ----  rcu_read_lock
 ---- ----  rcu_callback
 ..+. ..+.  &rq->__lock
 ...+ ...+  &p->pi_lock
 ..-- ..--  &sb->s_type->i_mutex_key#2

/proc/lockdep_stats

$ cat /proc/lockdep_stats
 lock-classes:                         1423
 direct dependencies:                  8752
 indirect dependencies:                45123
 all direct dependencies:              17504
 dependency chains:                    12847
 dependency chain hlocks used:         48231
 dependency chain hlocks lost:         0
 in-hardirq chains:                    234
 in-softirq chains:                    456
 in-process chains:                    12157
 stack-trace entries:                  89432
 max locking depth:                    15
 max bfs queue depth:                  312
 debug_locks:                          1

/proc/lockdep_chains

관찰된 잠금 획득 체인(순서)을 모두 나열합니다:

$ cat /proc/lockdep_chains | head -10
all lock chains:
irq_context: 0
[ffffffc000123456] &rq->__lock
[ffffffc000789abc] &p->pi_lock

irq_context: 0
[ffffffc000234567] &sb->s_type->i_mutex_key
[ffffffc000345678] &sb->s_type->i_mutex_key#2
/proc lockdep 인터페이스 lockdep 엔진 /proc/lockdep /proc/lockdep_stats /proc/lockdep_chains 클래스 + 의존성 해시/체인 통계 획득 순서 체인

lockdep 경고 메시지 해석

lockdep 경고는 dmesg에 상세한 정보를 포함합니다. 경고 메시지의 구조를 이해하면 문제를 빠르게 진단할 수 있습니다.

경고 메시지 구조

======================================================
WARNING: possible circular locking dependency detected
6.8.0-rc1 #1 Not tainted
------------------------------------------------------
kworker/0:1/123 is trying to acquire lock:
ffff888012345678 (&sb->s_type->i_mutex_key), at: ext4_file_write_iter+0x123/0x456

but task is already holding lock:
ffff888087654321 (jbd2_handle), at: start_this_handle+0x78/0x9ab

which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

-> #1 (&sb->s_type->i_mutex_key):
       lock_acquire+0xd1/0x2e0
       down_write+0x44/0xc0
       ext4_map_blocks+0x234/0x567
       ...

-> #0 (jbd2_handle):
       lock_acquire+0xd1/0x2e0
       start_this_handle+0x78/0x9ab
       ...

other info that might help us debug this:
 Possible unsafe locking scenario:
       CPU0                    CPU1
       ----                    ----
  lock(jbd2_handle);
                               lock(&sb->s_type->i_mutex_key);
                               lock(jbd2_handle);
  lock(&sb->s_type->i_mutex_key);

                *** DEADLOCK ***

경고 읽는 법

  1. 제목: 경고 유형 (circular, inconsistent, recursive 등)
  2. 현재 시도: 어떤 태스크가 어떤 잠금을 획득하려 하는지
  3. 기존 보유: 이미 보유 중인 잠금
  4. 의존성 체인: 역순으로 보여주는 의존성 경로
  5. 교착 시나리오: 구체적인 CPU별 교착 재현 순서
  6. 스택 트레이스: 각 의존성이 기록된 코드 경로

주요 경고 유형과 해결법

1. possible circular locking dependency

가장 흔한 경고로, 잠금 획득 순서가 다른 곳에서 역전된 것을 감지합니다.

원인해결법
ABBA 순서 역전모든 코드 경로에서 동일한 잠금 획득 순서 보장(Ordering)
숨겨진 간접 의존성의존성 체인 전체 분석, 중간 잠금 제거/재설계
콜백(Callback) 내 잠금 역전콜백 설계 재검토, 작업 지연(workqueue)

2. inconsistent {SOFTIRQ-ON-W} -> {IN-SOFTIRQ-W} usage

IRQ 안전성 불일치입니다. 한 곳에서는 IRQ 활성 상태로 잠금을 잡고, 다른 곳에서는 IRQ 핸들러 안에서 같은 잠금을 잡습니다.

/* 문제 코드 */
spin_lock(&my_lock);    /* softirq 활성 상태 */
...
/* softirq 핸들러 */
spin_lock(&my_lock);    /* softirq에서 같은 잠금! */

/* 수정: softirq에서도 사용되면 _bh 변형 사용 */
spin_lock_bh(&my_lock); /* softirq 비활성화 */

3. possible recursive locking detected

같은 잠금 클래스를 중첩 획득합니다. 의도된 경우 서브클래스 어노테이션으로 해결합니다.

/* 문제: 디렉터리 inode와 파일 inode의 i_rwsem 중첩 */
inode_lock(dir);
inode_lock(inode);  /* 같은 클래스 → recursive 경고 */

/* 수정: 서브클래스로 구분 */
inode_lock(dir);                                    /* subclass 0 */
inode_lock_nested(inode, I_MUTEX_CHILD);             /* subclass 1 */

4. BUG: sleeping function called from invalid context

원자적(Atomic) 컨텍스트(spinlock 보유, IRQ 비활성 등)에서 슬립 가능 함수를 호출합니다.

lockdep 경고 유형 분류 lockdep 경고 circular locking ABBA 교착 가능성 inconsistent state IRQ 안전성 불일치 recursive locking 같은 클래스 중첩 sleep-in-atomic wait_type 위반 순서 통일 _irqsave/_bh 사용 서브클래스 지정 설계 변경 debug_locks = 0 이면 lockdep가 비활성 상태 — /proc/lockdep_stats 확인

PROVE_LOCKING 동작 원리

CONFIG_PROVE_LOCKING은 lockdep의 핵심 기능을 활성화하는 커널 설정입니다. 이 옵션이 켜지면 모든 잠금 API에 계측 코드가 삽입됩니다.

PROVE_LOCKING이 활성화하는 기능

기능설명
의존성 그래프 추적모든 잠금 획득/해제에서 의존성 간선 기록
순환 탐지새 간선 추가 시 BFS 순환 검사
IRQ 안전성 검증IRQ 컨텍스트와 잠금 상태 교차 검증
wait_type 검사sleep-in-atomic 감지
체인 캐시이전 검증 결과 캐싱으로 오버헤드 감소
스택 트레이스 기록의존성 발생 위치 저장 (경고 시 출력)
/* include/linux/lockdep.h — PROVE_LOCKING 비활성 시 */
#ifndef CONFIG_PROVE_LOCKING
#define lock_acquire(l, s, t, r, c, n, i)   do { } while (0)
#define lock_release(l, i)                  do { } while (0)
/* → 모든 lockdep 계측이 no-op이 됨 */
#endif
PROVE_LOCKING 활성화 체인 CONFIG_PROVE_LOCKING CONFIG_LOCKDEP CONFIG_DEBUG_LOCK_ALLOC 활성화되는 기능 lock_acquire/release 후킹 의존성 그래프 구축 순환 탐지 (BFS) IRQ 안전성 추적 wait_type 검사 /proc/lockdep* 인터페이스

LOCK_STAT: 잠금 경합(Lock Contention) 통계

CONFIG_LOCK_STAT는 lockdep 인프라를 활용하여 잠금 경합(contention) 통계를 수집합니다. 성능 분석에 매우 유용합니다.

/proc/lock_stat 데이터

$ cat /proc/lock_stat | head -20
lock_stat version 0.4
-------------------------------------------------------------------
                              class name    con-bounces    contentions
                              ---------    -----------    -----------
                         &rq->__lock:        51234          12045
                         &p->pi_lock:         8234           2134
              &sb->s_type->i_mutex_key:       3456            891

                              class name    waittime-min    waittime-max
                              ---------    ------------    ------------
                         &rq->__lock:           0.12         145.67
                         &p->pi_lock:           0.05          23.45

통계 컬럼 설명

컬럼설명
con-bounces캐시라인 바운스 횟수 (다른 CPU에서 잠금 이동)
contentions잠금 경합 횟수 (획득 실패 후 대기)
waittime-min/max/total/avg잠금 대기 시간(Latency) 통계 (us)
holdtime-min/max/total/avg잠금 보유 시간 통계 (us)
acquisitions총 잠금 획득 횟수
# LOCK_STAT 초기화 (통계 리셋)
$ echo 0 > /proc/lock_stat
성능 분석 활용: waittime-avg가 높은 잠금은 경합(Contention)이 심한 것입니다. holdtime-avg가 높으면 임계 영역(Critical Section)이 너무 넓은 것입니다. 두 수치를 조합하면 잠금 최적화 대상을 빠르게 식별할 수 있습니다. perf lock 명령과 함께 사용하면 더 효과적입니다.

lockdep 자체 디버깅

lockdep 자체가 비활성화되거나 오동작할 때의 진단 방법입니다.

debug_locks가 꺼지는 경우

lockdep는 내부 일관성 오류를 감지하면 스스로를 비활성화합니다(debug_locks = 0). 이 상태가 되면 이후 모든 검증이 중단됩니다.

# debug_locks 상태 확인
$ cat /proc/lockdep_stats | grep debug_locks
 debug_locks:                          1    # 1 = 정상, 0 = 비활성

# lockdep가 꺼진 원인은 dmesg에서 확인
$ dmesg | grep -i "lockdep\|DEBUG_LOCKS_WARN"

비활성화 주요 원인

원인메시지대응
클래스 수 초과BUG: MAX_LOCKDEP_KEYS too low!MAX_LOCKDEP_KEYS 증가 (컴파일 타임)
체인 수 초과BUG: MAX_LOCKDEP_CHAINS too low!MAX_LOCKDEP_CHAINS 증가
간선 수 초과BUG: MAX_LOCKDEP_ENTRIES too low!잠금 수 감소 또는 한계 증가
스택 깊이 초과BUG: held lock nesting exceededMAX_LOCK_DEPTH 확인 (기본 48)
내부 오류DEBUG_LOCKS_WARN_ON커널 버그 보고
lockdep 비활성화 진단 흐름 debug_locks == 0 확인 dmesg에서 원인 메시지 검색 리소스 부족 → 한계 증가 내부 오류 → 커널 버그 보고 재부팅 후 lockdep 재활성화 (런타임 복구 불가)

lockdep 오버헤드

lockdep는 디버깅 도구이므로 런타임 오버헤드가 존재합니다. 프로덕션 커널에서는 일반적으로 비활성화됩니다.

오버헤드 분석

항목오버헤드설명
메모리수 MB ~ 수십 MBlock_class 배열, 의존성 목록, 체인 캐시, 스택 트레이스 저장
CPU (첫 번째 관찰)BFS 비용새로운 의존성 간선 추가 시 순환 검사
CPU (체인 캐시 히트)해시 비교이전 검증 체인과 동일하면 BFS 건너뜀
per-lock_acquire수백 ns ~ 수 us클래스 조회 + held_lock push + 체인 해시
부팅 시간10~30% 증가초기 의존성 그래프 구축

오버헤드 경감 기법

프로덕션 권장: CONFIG_PROVE_LOCKING은 개발/테스트 커널에서만 활성화하세요. CI/CD 파이프라인(Pipeline)에서 CONFIG_PROVE_LOCKING=y로 테스트 실행하고, 배포 커널에서는 비활성화하는 것이 일반적인 패턴입니다.

관련 커널 설정

CONFIG 옵션기본값설명
CONFIG_LOCKDEPnlockdep 기본 인프라 (직접 선택 불가, PROVE_LOCKING이 선택)
CONFIG_PROVE_LOCKINGn의존성 그래프, 순환 탐지, IRQ 안전성 활성화
CONFIG_LOCK_STATn잠금 경합 통계 (/proc/lock_stat)
CONFIG_DEBUG_LOCK_ALLOCn잠금 할당/해제 추적
CONFIG_DEBUG_LOCKDEPnlockdep 자체 디버깅 (추가 assertion)
CONFIG_LOCKDEP_CROSSRELEASE제거됨cross-release 추적 (v4.15에서 제거)
lockdep Kconfig 의존성 CONFIG_PROVE_LOCKING selects selects CONFIG_LOCKDEP CONFIG_DEBUG_LOCK_ALLOC CONFIG_LOCK_STAT (LOCKDEP에 의존) CONFIG_DEBUG_LOCKDEP (LOCKDEP에 의존) .config: CONFIG_PROVE_LOCKING=y → LOCKDEP + DEBUG_LOCK_ALLOC 자동 선택
# 개발/테스트 커널 권장 설정
CONFIG_PROVE_LOCKING=y       # 의존성 추적 + 순환 탐지
CONFIG_LOCK_STAT=y           # 경합 통계
CONFIG_DEBUG_LOCKDEP=y       # lockdep 내부 디버깅
CONFIG_DEBUG_ATOMIC_SLEEP=y  # might_sleep() 검사 강화
CONFIG_LOCKDEP_CHAINS_BITS=16 # 체인 해시 크기 (기본 16)

# 프로덕션 커널 — lockdep 비활성
# CONFIG_PROVE_LOCKING is not set
# CONFIG_LOCK_STAT is not set

__lock_acquire()/validate_chain() 소스 분석

__lock_acquire()는 lockdep의 가장 핵심적인 내부 함수로, 약 200줄의 복잡한 제어 흐름을 가집니다. 이 함수가 호출되면 잠금 클래스 등록부터 의존성 그래프 검증까지 전 과정이 수행됩니다.

__lock_acquire() 전체 경로

/* kernel/locking/lockdep.c — __lock_acquire() 핵심 흐름 */
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)
{
    struct task_struct *curr = current;
    struct lock_class *class;
    struct held_lock *hlock;
    int depth, chain_head;

    /* ① lock_class 조회 또는 신규 등록 */
    class = register_lock_class(lock, subclass, 0);
    if (!class)
        return 0;

    /* ② depth 한계 검사: MAX_LOCK_DEPTH(48) 초과 여부 */
    depth = curr->lockdep_depth;
    if (depth >= MAX_LOCK_DEPTH) {
        debug_locks_off();
        return 0;
    }

    /* ③ held_lock 초기화 및 스택에 push */
    hlock = curr->held_locks + depth;
    hlock->class_idx = class_idx(class);
    hlock->acquire_ip = ip;
    hlock->trylock = trylock;
    hlock->read = read;
    hlock->irq_context = task_irq_context(curr);

    /* ④ wait_type 검사 (sleep-in-atomic 감지) */
    if (!check_wait_context(curr, hlock))
        return 0;

    /* ⑤ 이전 체인 키로 체인 해시 계산 */
    hlock->prev_chain_key = chain_key;
    chain_key = iterate_chain_key(chain_key,
                    hlock_id(hlock));

    /* ⑥ 체인 캐시 조회 → 히트이면 BFS 건너뜀 */
    if (lookup_chain_cache(curr, hlock, chain_key))
        return 1; /* 이전 검증 통과 */

    /* ⑦ 새로운 체인 → validate_chain() 호출 */
    if (!validate_chain(curr, hlock, chain_head, chain_key))
        return 0;

    curr->lockdep_depth++;
    return 1;
}

validate_chain() 내부

validate_chain()은 새로운 의존성 체인이 관찰될 때 호출됩니다. 현재 보유 중인 잠금들과 새 잠금 사이의 관계를 검증합니다.

/* kernel/locking/lockdep.c — validate_chain() */
static int validate_chain(
    struct task_struct *curr,
    struct held_lock *hlock,
    int chain_head, u64 chain_key)
{
    /*
     * held_locks 스택의 각 이전 잠금에 대해
     * check_prev_add()로 의존성 간선 검증
     */
    for (i = curr->lockdep_depth - 1; i >= 0; i--) {
        struct held_lock *prev = curr->held_locks + i;

        /* prev→hlock 의존성 검증 */
        if (!check_prev_add(curr, prev, hlock, ...))
            return 0;

        /* 같은 IRQ 컨텍스트에서만 의존성 추적 */
        if (prev->irq_context != hlock->irq_context)
            break;
    }

    /* 새 체인 해시 등록 */
    add_chain_cache(curr, hlock, chain_key);
    return 1;
}

check_prev_add() 상세

check_prev_add()는 prev→next 의존성 간선을 추가하기 전에 3가지 핵심 검증을 수행합니다.

검증 단계함수검사 내용실패 시
1. 순환 검사check_noncircular()next→prev 도달 가능 여부 (BFS)circular dependency 경고
2. IRQ 안전성check_irq_usage()IRQ-safe/unsafe 혼용 검사unsafe lock order 경고
3. 중복 간선check_redundant()이미 도달 가능한 경로 존재 여부간선 추가 건너뜀 (최적화)
/* kernel/locking/lockdep.c — check_prev_add() 핵심 */
static int check_prev_add(
    struct task_struct *curr,
    struct held_lock *prev,
    struct held_lock *next, ...)
{
    struct lock_class *prev_class = hlock_class(prev);
    struct lock_class *next_class = hlock_class(next);

    /* trylock은 의존성 간선을 생성하지 않음 */
    if (next->trylock)
        return 1;

    /* 동일 클래스 → 재귀 잠금 검사 */
    if (prev_class == next_class)
        return check_deadlock(curr, next);

    /* ① 순환 검사: next의 forward deps에서 prev에 도달? */
    ret = check_noncircular(next, prev, ...);
    if (ret == BFS_RMATCH)
        return 0;  /* 순환 감지! */

    /* ② IRQ 안전성 검사 */
    if (!check_irq_usage(curr, prev, next))
        return 0;

    /* ③ 중복 간선(redundant) 검사 → 이미 prev→next 경로 있으면 스킵 */
    ret = check_redundant(prev, next);
    if (ret == BFS_RMATCH)
        return 1;  /* 이미 간선 존재, 추가 불필요 */

    /* 간선 추가: prev_class→next_class */
    add_lock_to_list(prev_class, next_class,
                     &prev_class->locks_after, ...);
    add_lock_to_list(next_class, prev_class,
                     &next_class->locks_before, ...);
    return 1;
}
__lock_acquire() → validate_chain() 전체 검증 흐름 __lock_acquire() 진입 ① register_lock_class(lock, subclass) ② held_lock 초기화 + depth push ③ check_wait_context() — wait_type 검사 ④ iterate_chain_key() → 체인 해시 계산 체인 캐시 히트? Yes BFS 건너뜀 ✓ No ⑤ validate_chain() check_noncircular() check_irq_usage() check_redundant() 통과 → add_lock_to_list() → 간선 추가 classhash_table MAX_LOCK_DEPTH=48 hlock_id(hlock)
그림 13. __lock_acquire()에서 validate_chain()까지의 전체 검증 경로. 체인 캐시 히트 시 BFS를 건너뛰어 성능을 최적화합니다.
참고: check_prev_add()는 held_locks 스택의 모든 이전 잠금에 대해 호출되지 않습니다. 같은 IRQ 컨텍스트 내에서만 의존성을 추적합니다. prev->irq_context != hlock->irq_context이면 순회를 중단합니다. 이는 인터럽트 경계에서의 의존성은 별도의 IRQ 안전성 검사로 처리하기 때문입니다.

BFS 순환 탐지 구현 분석

lockdep의 순환 탐지는 __bfs() 함수를 기반으로 합니다. 이 함수는 의존성 그래프에서 너비 우선 탐색을 수행하며, forward(locks_after) 방향과 backward(locks_before) 방향 모두 지원합니다.

__bfs() 핵심 구현

/* kernel/locking/lockdep.c — __bfs() */
static enum bfs_result __bfs(
    struct lock_list *source_entry,
    void *data,
    bool (*match)(struct lock_list *, void *),
    bool forward,
    struct lock_list **target_entry)
{
    struct lock_list *entry;
    struct lock_list *lock;
    struct list_head *head;
    struct circular_queue *cq = &__bfs_queue;
    unsigned int bfs_visited = 0;

    /* 큐 초기화 */
    __cq_init(cq);
    __cq_enqueue(cq, source_entry);

    while (__cq_dequeue(cq, &lock)) {
        /* 방문 카운트 제한: MAX_CIRCULAR_QUEUE_SIZE */
        if (++bfs_visited >= MAX_CIRCULAR_QUEUE_SIZE)
            return BFS_EQUEUEFULL;

        /* forward: locks_after, backward: locks_before */
        head = forward ?
            &hlock_class(lock)->locks_after :
            &hlock_class(lock)->locks_before;

        /* 인접 노드 순회 */
        list_for_each_entry(entry, head, entry) {
            /* 세대 번호로 방문 체크 (비트맵 불필요) */
            if (!lock_accessed(entry))
                continue;

            /* 매칭 콜백으로 타겟 확인 */
            if (match(entry, data)) {
                *target_entry = entry;
                return BFS_RMATCH;
            }

            /* parent 포인터 설정 (경로 역추적용) */
            entry->parent = lock;
            __cq_enqueue(cq, entry);
        }
    }

    return BFS_RNOMATCH;
}
코드 설명
  • __bfs() 시그니처__bfs()는 lockdep의 범용 너비 우선 탐색 함수입니다. source_entry에서 시작하여 match 콜백이 참을 반환하는 노드를 찾습니다. forward 매개변수로 탐색 방향을 결정합니다 (kernel/locking/lockdep.c).
  • circular_queueBFS 큐는 Per-CPU 정적 할당 원형 큐(circular_queue)를 사용합니다. 잠금 컨텍스트에서 동적 메모리 할당이 불가능하므로 고정 크기(MAX_CIRCULAR_QUEUE_SIZE = 4096)를 사용하며, 큐가 가득 차면 BFS_EQUEUEFULL을 반환합니다.
  • forward 분기forward가 참이면 locks_after 리스트를 순회하고(순환 감지용), 거짓이면 locks_before 리스트를 순회합니다(IRQ 역전 감지용). 같은 BFS 코드로 양방향 탐색을 모두 처리합니다.
  • lock_accessed()lock_accessed()dep_gen_id 세대 번호로 이미 방문한 노드인지 확인합니다. 별도의 비트맵 대신 세대 번호를 증가시키는 방식이므로, BFS 시작 시 초기화 비용이 O(1)입니다.
  • parent 포인터lock_listparent를 현재 노드로 설정하여 BFS 경로를 기록합니다. 순환이 발견되면 parent를 역추적하여 A→B→C→A 같은 전체 순환 경로를 커널 로그에 출력할 수 있습니다.

circular_queue 자료구조

BFS에 사용되는 큐는 정적으로 할당된 원형 큐입니다. 동적 메모리 할당이 잠금 컨텍스트에서 불가능하므로, 고정 크기 배열을 사용합니다.

/* kernel/locking/lockdep.c */
#define MAX_CIRCULAR_QUEUE_SIZE  4096

struct circular_queue {
    struct lock_list *element[MAX_CIRCULAR_QUEUE_SIZE];
    unsigned int front;
    unsigned int rear;
};

/* Per-CPU BFS 큐 (재진입 방지) */
static DEFINE_PER_CPU(struct circular_queue, bfs_queue);

Forward vs Backward 탐색

탐색 방향사용 함수사용 목적간선 방향
Forward__bfs_forwards()순환 탐지 (check_noncircular)locks_after 리스트 순회
Backward__bfs_backwards()IRQ 안전성 검사locks_before 리스트 순회
Forward__bfs_forwards()중복 간선 검사 (check_redundant)이미 도달 가능한 경로 확인

경로 복원과 순환 보고

순환이 탐지되면 print_circular_bug()가 호출됩니다. 각 lock_list 노드의 parent 포인터를 역추적하여 전체 순환 경로를 복원합니다.

/* 순환 경로 출력 예시 */
/*
 * Chain exists of:
 *   &A → &B → &C → &A
 *
 * Possible unsafe locking scenario:
 *       CPU0                    CPU1
 *       ----                    ----
 *  lock(&A);
 *                               lock(&C);
 *                               lock(&A);  ← deadlock
 *  lock(&B);
 */
BFS 순환 탐지: A→B→C→A 순환 검출 A B C locks_after locks_after 새 간선 C→A (순환 발생!) BFS 순환 탐색 과정 (C→A 추가 시도 시) 1. source = A (새 간선의 target) 2. A의 locks_after 순회 → B 발견 → 큐에 추가 3. B의 locks_after 순회 → C 발견 → C == source의 class → BFS_RMATCH (순환!)
그림 14. BFS 순환 탐지. C→A 간선을 추가하기 전에 A에서 forward BFS로 C에 도달 가능한지 검사합니다. A→B→C 경로가 발견되면 순환으로 보고합니다.
주의: MAX_CIRCULAR_QUEUE_SIZE(4096)를 초과하면 BFS가 BFS_EQUEUEFULL을 반환하고 lockdep가 비활성화됩니다. 이는 매우 복잡한 잠금 체계를 가진 대규모 서브시스템(네트워킹 등)에서 드물게 발생할 수 있습니다. /proc/lockdep_stats에서 max bfs queue depth를 모니터링하세요.

IRQ 안전성 검증 소스 분석

lockdep의 IRQ 안전성 검증은 check_irq_usage()를 통해 수행됩니다. 잠금이 인터럽트 컨텍스트에서 사용되는지(IRQ-safe) 아닌지(IRQ-unsafe)를 추적하고, 두 타입 간의 의존성 역전을 감지합니다.

IRQ 사용 플래그

lock_class는 IRQ 컨텍스트에서의 사용 여부를 비트 마스크로 추적합니다.

/* include/linux/lockdep_types.h — usage bits */
enum lock_usage_bit {
    LOCK_USED_IN_HARDIRQ,          /* hardirq에서 사용됨 */
    LOCK_USED_IN_SOFTIRQ,          /* softirq에서 사용됨 */
    LOCK_USED_IN_HARDIRQ_READ,     /* hardirq에서 읽기 잠금 */
    LOCK_USED_IN_SOFTIRQ_READ,     /* softirq에서 읽기 잠금 */
    LOCK_ENABLED_HARDIRQ,          /* hardirq 활성 상태에서 사용 */
    LOCK_ENABLED_SOFTIRQ,          /* softirq 활성 상태에서 사용 */
    LOCK_ENABLED_HARDIRQ_READ,     /* hardirq 활성 + 읽기 잠금 */
    LOCK_ENABLED_SOFTIRQ_READ,     /* softirq 활성 + 읽기 잠금 */
    LOCK_USED,                     /* 한 번이라도 사용됨 */
    LOCK_USAGE_STATES,             /* 총 상태 수 */
};

check_irq_usage() 검증 로직

/* kernel/locking/lockdep.c — check_irq_usage() 핵심 */
static int check_irq_usage(
    struct task_struct *curr,
    struct held_lock *prev,
    struct held_lock *next)
{
    /*
     * prev를 hardirq-unsafe로, next를 hardirq-safe로
     * 사용한 기록이 있으면 → 위험한 잠금 순서
     *
     * 시나리오: 프로세스 컨텍스트에서 L1→L2 순서로 잠금
     *   - L2가 hardirq에서도 사용되면 (USED_IN_HARDIRQ)
     *   - L1이 hardirq 활성 상태에서 사용되면 (ENABLED_HARDIRQ)
     *   → hardirq 발생 시: L2를 먼저 잡으려 하지만
     *     프로세스 컨텍스트가 L1을 잡고 L2를 기다릴 수 있음
     */

    /* Hardirq 검사 */
    if (!check_usage(curr, prev, next,
                     LOCK_USED_IN_HARDIRQ,
                     LOCK_ENABLED_HARDIRQ,
                     "hard"))
        return 0;

    /* Softirq 검사 */
    if (!check_usage(curr, prev, next,
                     LOCK_USED_IN_SOFTIRQ,
                     LOCK_ENABLED_SOFTIRQ,
                     "soft"))
        return 0;

    return 1;
}

static int check_usage(
    struct task_struct *curr,
    struct held_lock *prev,
    struct held_lock *next,
    enum lock_usage_bit bit_backwards,
    enum lock_usage_bit bit_forwards,
    const char *irqclass)
{
    /* backward 탐색: prev의 선행자 중 IRQ-safe 잠금이 있는가? */
    ret = find_usage_backwards(prev, bit_backwards, ...);

    /* forward 탐색: next의 후행자 중 IRQ-unsafe 잠금이 있는가? */
    ret = find_usage_forwards(next, bit_forwards, ...);

    /* 둘 다 발견되면 → IRQ 역전 보고 */
    if (backwards_match && forwards_match)
        return print_irq_inversion_bug(...);

    return 1;
}

mark_lock_irq(): 사용 비트 설정

잠금이 처음 특정 IRQ 컨텍스트에서 사용될 때 mark_lock_irq()가 호출되어 usage 비트를 설정합니다.

/* kernel/locking/lockdep.c */
static int mark_lock_irq(
    struct task_struct *curr,
    struct held_lock *this,
    enum lock_usage_bit new_bit)
{
    /* 이미 설정된 비트이면 빠른 리턴 */
    if (hlock_class(this)->usage_mask & (1 << new_bit))
        return 1;

    /* 새 비트 설정 */
    hlock_class(this)->usage_mask |= (1 << new_bit);

    /* 사용 위치 스택 트레이스 저장 */
    save_trace(hlock_class(this)->usage_traces + new_bit);

    /* 역방향 의존성 검사: 새 비트와 충돌하는 사용이 있는지 */
    return check_irq_usage_aggregate(curr, this);
}
코드 설명
  • check_irq_usage()두 잠금 prev→next 간의 IRQ 안전성을 검증합니다. prev의 backward 의존성 중 IRQ-safe(IRQ 핸들러에서 사용) 잠금이 있고, next의 forward 의존성 중 IRQ-unsafe(IRQ 활성 상태에서 사용) 잠금이 있으면 IRQ 역전 교착이 가능합니다 (kernel/locking/lockdep.c).
  • check_usage() 양방향 탐색find_usage_backwards()는 prev의 locks_before 그래프를 BFS로 탐색하여 LOCK_USED_IN_HARDIRQ 비트가 설정된 클래스를 찾고, find_usage_forwards()는 next의 locks_after 그래프에서 LOCK_ENABLED_HARDIRQ 비트가 설정된 클래스를 찾습니다.
  • Hardirq / Softirq 이중 검사hardirq와 softirq에 대해 각각 독립적으로 검사합니다. hardirq는 softirq보다 우선순위가 높으므로, softirq 컨텍스트에서 사용되는 잠금도 별도의 역전 시나리오가 존재합니다.
  • mark_lock_irq()잠금이 처음 특정 IRQ 컨텍스트에서 사용될 때 usage_mask에 해당 비트를 설정하고 스택 트레이스를 저장합니다. 이미 설정된 비트이면 빠르게 리턴하여 오버헤드를 최소화합니다.

IRQ 역전 시나리오

시나리오프로세스(Process) 컨텍스트IRQ 컨텍스트결과
안전spin_lock_irqsave(&L)spin_lock(&L)IRQ 비활성화로 보호됨
위험spin_lock(&L)spin_lock(&L)deadlock: 프로세스가 L 잡고 IRQ 발생 시
역전lock(A); lock(B);lock(B);A가 IRQ-unsafe, B가 IRQ-safe → 역전
IRQ 안전성 검증: check_irq_usage() 흐름 lock_class usage_mask 비트 USED_IN_HARDIRQ — hardirq에서 잠금 획득 USED_IN_SOFTIRQ — softirq에서 잠금 획득 ENABLED_HARDIRQ — hardirq 활성 상태에서 획득 ENABLED_SOFTIRQ — softirq 활성 상태에서 획득 + 각 비트의 READ 변형 (총 8비트 + LOCK_USED) check_irq_usage(prev, next) find_usage_backwards(prev) prev 선행자 중 IRQ-safe 잠금 탐색 find_usage_forwards(next) next 후행자 중 IRQ-unsafe 잠금 탐색 양쪽 매치? Yes No 검증 통과 IRQ 역전 교착 시나리오 CPU 0 (프로세스): spin_lock(&A); spin_lock(&B); ← IRQ 발생! CPU 0 (hardirq): spin_lock(&B); ← deadlock! A = ENABLED_HARDIRQ (IRQ-unsafe) B = USED_IN_HARDIRQ (IRQ-safe) 해결: spin_lock_irqsave(&A) 사용
그림 15. IRQ 안전성 검증 흐름. backward 탐색으로 IRQ-safe 잠금을, forward 탐색으로 IRQ-unsafe 잠금을 찾아 역전을 감지합니다.
참고: lockdep의 IRQ 안전성 검증은 간접 의존성도 추적합니다. A→B→C 체인에서 A가 IRQ-unsafe이고 C가 IRQ-safe이면, A→B 간선 추가 시점에서는 문제가 없어도 B→C 간선 추가 시 backward 탐색으로 A를 발견하여 경고합니다.

잠금 체인 해싱과 캐싱

lockdep의 성능은 체인 캐시에 크게 의존합니다. 매 lock_acquire()마다 BFS를 수행하면 오버헤드가 심각해지므로, 이전에 검증된 잠금 획득 순서(체인)를 해시하여 캐시합니다. 동일한 체인이 다시 관찰되면 검증을 건너뜁니다.

체인 키 계산: iterate_chain_key()

/* kernel/locking/lockdep.c — 체인 키 계산 */
static inline u64 iterate_chain_key(
    u64 key, u64 id)
{
    /*
     * 해시 함수: Jenkins의 mix 함수 변형
     * key = 이전 체인 키
     * id  = hlock_id(hlock) — 잠금 클래스+읽기/쓰기+IRQ 컨텍스트 인코딩
     */
    key = key * ITERATOR_HASH_MULT;
    key += id;
    key = (key >> 16) ^ key;
    return key;
}

/* hlock_id: 잠금 클래스 인덱스 + 컨텍스트 정보를 결합 */
static inline u64 hlock_id(
    struct held_lock *hlock)
{
    return (u64)hlock->class_idx |
           ((u64)hlock->read << 13);
}

chainhash_table 구조

/* kernel/locking/lockdep.c */
#define CHAINHASH_BITS       (CONFIG_LOCKDEP_CHAINS_BITS) /* 기본 16 */
#define CHAINHASH_SIZE       (1UL << CHAINHASH_BITS)
#define CHAINHASH_MASK       (CHAINHASH_SIZE - 1)

static struct hlist_head chainhash_table[CHAINHASH_SIZE];

/* lock_chain: 캐시된 의존성 체인 */
struct lock_chain {
    struct hlist_node   entry;     /* 해시 체인 */
    u64                 chain_key; /* 체인 해시 값 */
    int                 depth;     /* 체인 깊이 */
    int                 base;      /* chain_hlocks 인덱스 */
};

체인 캐시 조회

단계동작복잡도
1. 해시 계산chain_key & CHAINHASH_MASKO(1)
2. 버킷 탐색hlist_for_each_entrychain_key 비교O(k) — 충돌 체인 길이
3. 히트체인 키 일치 → BFS 건너뜀
4. 미스validate_chain() → BFS 수행 → 새 체인 등록O(V+E)
/* kernel/locking/lockdep.c — lookup_chain_cache() */
static int lookup_chain_cache(
    struct task_struct *curr,
    struct held_lock *hlock,
    u64 chain_key)
{
    struct hlist_head *hash_head;
    struct lock_chain *chain;

    hash_head = chainhash_table +
                (chain_key & CHAINHASH_MASK);

    hlist_for_each_entry(chain, hash_head, entry) {
        if (chain->chain_key == chain_key) {
            /* 캐시 히트: 이전 검증 결과 재사용 */
            return 1;
        }
    }

    /* 캐시 미스: validate_chain() 필요 */
    return 0;
}

해시 충돌 처리

체인 키는 64비트이므로 충돌 확률이 극히 낮지만, CHAINHASH_SIZE(65536)개의 버킷에 분산됩니다. 충돌 시 연결 리스트(hlist)로 체인링됩니다.

체인 해시 테이블과 캐싱 구조 체인 키 누적 계산 key₀ = INITIAL_CHAIN_KEY key₁ = iterate(key₀, A) key₂ = iterate(key₁, B) key₃ = iterate(key₂, C) key & MASK chainhash_table[65536] [0] [h] [h+1] ... [N-1] chain₁ /proc/lockdep_stats 관련 지표 lock chains: 12345 (현재 캐시된 체인 수) max lock chain depth: 15 (가장 깊은 체인) chain lookup hits: 9876543 (캐시 히트 횟수) 체인 캐시 성능 영향 캐시 히트율 > 99%: 대부분의 lock_acquire()가 해시 비교만으로 완료 캐시 미스 시: BFS 비용 O(V+E) 발생, 새 체인 등록 후 향후 히트로 전환 MAX_LOCKDEP_CHAINS (기본 65536) 초과 시 lockdep 비활성화
그림 16. 체인 해시 테이블 구조. 잠금 획득 순서를 누적 해시하여 chainhash_table에서 조회합니다. 히트율이 99%를 넘어 BFS 비용을 대부분 회피합니다.
주의: MAX_LOCKDEP_CHAINS 한계에 도달하면 "BUG: MAX_LOCKDEP_CHAINS too low!" 메시지와 함께 lockdep가 비활성화됩니다. CONFIG_LOCKDEP_CHAINS_BITS를 증가시켜 한계를 늘릴 수 있지만, 메모리 사용량도 비례하여 증가합니다.

Cross-release 의존성 심층

일반적인 잠금은 같은 컨텍스트에서 획득하고 해제합니다. 그러나 completion, wait_for_completion(), 일부 비동기 패턴에서는 한 컨텍스트에서 잠금(또는 동기화 대기)을 시작하고, 다른 컨텍스트에서 해제(완료 신호)하는 cross-release 패턴이 발생합니다.

Cross-release 추적의 역사

커널 버전상태설명
v4.14도입Byungchul Park의 cross-release lockdep 패치 (CONFIG_LOCKDEP_CROSSRELEASE)
v4.15제거복잡도 대비 이점 부족으로 Linus가 revert, 유지보수 부담
현재미지원cross-release 의존성은 lockdep로 추적 불가

Cross-release가 감지 못하는 교착

/* Cross-release 교착 패턴: lockdep가 감지하지 못함 */

/* Thread A */
mutex_lock(&lock_X);
wait_for_completion(&comp);  /* Thread B의 complete() 대기 */
mutex_unlock(&lock_X);

/* Thread B */
mutex_lock(&lock_X);        /* Thread A가 잡고 있음 → 대기 */
complete(&comp);            /* 여기 도달 못함 → deadlock */
mutex_unlock(&lock_X);
주의: 위 패턴에서 lockdep는 wait_for_completion()을 잠금으로 인식하지 않으므로, lock_X → comp → lock_X 순환을 감지하지 못합니다. 이 유형의 교착은 코드 리뷰와 timeout 기반 방어로 대응해야 합니다.

수동 어노테이션 기법

cross-release 의존성을 lockdep에 알리기 위해 lockdep_map을 수동으로 사용할 수 있습니다.

/* Cross-release 의존성을 lockdep에 수동 표현 */
static DEFINE_LOCKDEP_MAP(comp_map,
    "comp_dep", &comp_key, 0);

/* 대기 측 (Thread A) */
lock_acquire(&comp_map, 0, 0, 0, 1,
             NULL, _THIS_IP_);
wait_for_completion(&comp);
lock_release(&comp_map, _THIS_IP_);

/* 완료 측 (Thread B) — lock_release로 모델링 */
/* 주의: cross-context이므로 lockdep_map의
 * lock/unlock 쌍이 다른 태스크에서 수행되어
 * lock_release에서 "not held" 경고 발생 가능 */

Cross-release 대안 전략

전략설명적용 대상
timeout 사용wait_for_completion_timeout()으로 무한 대기 방지모든 completion 사용처
잠금 순서 문서화주석으로 cross-release 의존성 명시서브시스템 내부
lockdep 서브클래스관련 잠금에 서브클래스를 부여하여 간접 표현중첩 잠금
LOCKDEP_MAP_WAITlock_map_acquire()/lock_map_release()같은 태스크 내 대기
Cross-release 교착: lockdep 감지 불가 영역 Thread A lock(X) wait_for_completion(C) unlock(X) Thread B lock(X) ← blocked! complete(C) ← 도달 불가 X 보유 → B 대기 완료 불가 → A 대기 lockdep가 보는 것 Thread A: lock(X)만 추적. wait_for_completion()은 잠금이 아니므로 의존성 간선 없음. 숨겨진 의존성 (lockdep 미감지) X → completion(C) → X: 순환이지만 lockdep 그래프에 completion 노드가 없음 대응: timeout 사용, 코드 리뷰, lockdep_map 수동 어노테이션
그림 17. Cross-release 교착 패턴. Thread A가 lock(X)을 보유하고 completion을 대기하지만, Thread B가 lock(X)에 막혀 complete()를 호출할 수 없습니다. lockdep는 이 순환을 감지하지 못합니다.

lockdep 오버헤드 정량 분석

lockdep는 강력한 디버깅 도구이지만 상당한 런타임 비용을 수반합니다. 이 절에서는 메모리 사용량, CPU 오버헤드, 한계 값들을 정량적으로 분석합니다.

메모리 소비 분석

자료구조크기기본 한계총 메모리
lock_class[]~256 bytes/entryMAX_LOCKDEP_KEYS = 8192~2 MB
lock_list[]~48 bytes/entryMAX_LOCKDEP_ENTRIES = 32768~1.5 MB
lock_chain[]~24 bytes/entryMAX_LOCKDEP_CHAINS = 65536~1.5 MB
chain_hlocks[]2 bytes/entryMAX_LOCKDEP_CHAIN_HLOCKS = 262144~512 KB
stack_trace[]~8 bytes/entryMAX_LOCKDEP_STACK_TRACE_ENTRIES = 524288~4 MB
chainhash_table[]ptr/bucket65536 buckets~512 KB
Per-task held_locks[]~64 bytes/entryMAX_LOCK_DEPTH = 48 / task~3 KB/task
총계~10 MB + per-task

CPU 오버헤드 분석

경로비용빈도설명
체인 캐시 히트100~300 ns>99%해시 계산 + 비교. 대부분의 lock_acquire 경로
체인 캐시 미스1~100 us<1%BFS 순회 비용. 새로운 잠금 패턴 첫 관찰 시
새 lock_class 등록500 ns ~ 2 us드묾모듈 로드, 새 잠금 타입 첫 사용
스택 트레이스 저장1~5 us새 간선 추가 시save_stack_trace() 비용
IRQ 안전성 검사추가 BFS 비용새 간선 시backward + forward 2회 BFS

lockdep 한계 값과 모니터링

/* /proc/lockdep_stats 주요 지표 */
$ cat /proc/lockdep_stats
 lock-classes:                         1234
 direct dependencies:                  5678
 indirect dependencies:               12345
 all direct dependencies:              9012
 dependency chains:                   23456
 dependency chain hlocks:             45678
 in-hardirq chains:                      89
 in-softirq chains:                     123
 in-process chains:                   23244
 stack-trace entries:                 98765
 max locking depth:                      15
 max bfs queue depth:                   234
 chain lookup hits:                 9876543
 chain lookup misses:                 23456
 cyclic checks:                       23456
 redundant checks:                    12345
 redundant links found:                6789
 find-hierarchical-usage:              5678
 find-usage-forwards checks:           3456
 find-usage-backwards checks:          2345

벤치마크 결과 (일반적 수치)

벤치마크lockdep OFFlockdep ON오버헤드
커널 빌드 (make -j8)100%115~130%15~30%
네트워크 처리량 (iperf)100%85~92%8~15% 감소
파일시스템 I/O (fio)100%88~95%5~12% 감소
부팅 시간100%110~130%10~30% 증가
컨텍스트 스위치 (lat_ctx)100%120~150%20~50% 증가
lockdep 오버헤드 구성 요소 메모리 lock_class[] ~2MB lock_list[] ~1.5MB chains ~1.5MB stacks ~4MB CPU 핫 경로 class 조회 ~50ns 해시 계산 ~30ns 캐시 조회 ~50ns (히트) IRQ 체크 ~30ns 콜드 경로 (미스) BFS 순환 탐지 1~50us IRQ BFS (2회) 1~50us save_stack_trace() 1~5us 한계 값 MAX_LOCKDEP_KEYS = 8192 (잠금 클래스 수) MAX_LOCKDEP_ENTRIES = 32768 (의존성 간선 수) MAX_LOCKDEP_CHAINS = 65536 (체인 캐시 수) MAX_LOCK_DEPTH = 48 (per-task 중첩 깊이) MAX_CIRCULAR_QUEUE_SIZE = 4096 (BFS 큐 크기)
그림 18. lockdep 오버헤드 구성 요소. 핫 경로(캐시 히트)는 ~160ns이지만, 콜드 경로(캐시 미스)는 BFS 비용으로 수십 us까지 증가합니다.
참고: lockdep의 오버헤드는 잠금 사용 빈도에 비례합니다. 네트워크 스택(Network Stack)처럼 초당 수만 번 잠금을 획득/해제하는 서브시스템에서 오버헤드가 가장 크게 나타납니다. /proc/lockdep_statschain lookup hitsmisses 비율로 캐시 효율을 확인하세요.

서브시스템: 네트워크 스택 lockdep

네트워크 스택은 커널에서 가장 복잡한 잠금 계층 구조를 가진 서브시스템 중 하나입니다. 소켓(Socket) 잠금, 해시 테이블 잠금, NAPI 잠금 등이 다양한 컨텍스트에서 중첩됩니다.

소켓 잠금 중첩

/* include/net/sock.h — 소켓 잠금 계층 */

/*
 * 소켓 잠금 순서 (lockdep 어노테이션 기반):
 *
 * 1. sk->sk_lock.slock        (BH-disabled spinlock)
 * 2. sk->sk_lock (owner)      (소프트웨어 mutex)
 * 3. sk->sk_receive_queue.lock
 * 4. sk->sk_write_queue.lock
 */

/* 소켓 잠금 nesting 레벨 정의 */
enum sock_lock_nesting {
    SINGLE_DEPTH_NESTING = 1,
    SOCK_LOCK_NESTING    = 2,  /* 소켓 내부 중첩 */
};

주소 패밀리별 잠금 키

네트워크 스택은 주소 패밀리(AF_INET, AF_INET6, AF_UNIX 등)별로 별도의 lock_class_key를 사용합니다. 이렇게 하지 않으면 서로 다른 프로토콜의 소켓 잠금이 동일한 클래스로 분류되어 거짓 양성(false positive)이 발생합니다.

/* net/core/sock.c — 주소 패밀리별 잠금 키 */
static struct lock_class_key
    af_family_keys[AF_MAX];
static struct lock_class_key
    af_family_slock_keys[AF_MAX];
static struct lock_class_key
    af_family_kern_keys[AF_MAX];

/* sock_init_data()에서 AF별 키 할당 */
void sock_init_data(struct socket *sock,
                    struct sock *sk)
{
    int family = sk->sk_family;

    lockdep_set_class_and_name(
        &sk->sk_lock.slock,
        af_family_slock_keys + family,
        af_family_slock_key_strings[family]);

    lockdep_init_map(
        &sk->sk_lock.dep_map,
        af_family_key_strings[family],
        af_family_keys + family, 0);
}

네트워크 lockdep 과제

문제원인lockdep 대응
소켓→소켓 중첩TCP accept, splice 등에서 두 소켓 동시 잠금lock_sock_nested(sk, SINGLE_DEPTH_NESTING)
AF별 거짓 양성다른 프로토콜 소켓이 같은 lock_classaf_family_keys[] 배열
netfilter 체인conntrack, NAT 잠금의 복잡한 중첩서브클래스 어노테이션
NAPI poll 컨텍스트softirq에서 잠금 획득, IRQ 안전성 검사 트리거spin_lock_bh() 사용 필수
RCU + 소켓 잠금rcu_read_lock() 내에서 잠금 획득 순서wait_type 검사 (LD_WAIT_FREE)

주요 네트워크 lockdep 어노테이션

/* TCP 연결 수락 시 소켓 잠금 중첩 */
struct sock *inet_csk_accept(
    struct sock *sk, ...)
{
    /* 리스닝 소켓 잠금 (level 0) */
    lock_sock(sk);

    /* 새 연결 소켓 잠금 (level 1) */
    lock_sock_nested(newsk,
                     SINGLE_DEPTH_NESTING);
    ...
}

/* splice/tee 시 파이프+소켓 잠금 */
/* AF_UNIX: 두 소켓 간 교차 잠금 필요 */
static void unix_state_double_lock(
    struct sock *sk1,
    struct sock *sk2)
{
    /* 주소 순서로 잠금 획득 → ABBA 방지 */
    if (sk1 < sk2) {
        unix_state_lock(sk1);
        unix_state_lock_nested(sk2,
                    SINGLE_DEPTH_NESTING);
    } else {
        unix_state_lock(sk2);
        unix_state_lock_nested(sk1,
                    SINGLE_DEPTH_NESTING);
    }
}
네트워크 스택 잠금 계층과 lockdep 키 Socket Layer sk_lock (owner) sk_lock.slock sk_receive_queue sk_write_queue 주소 패밀리별 lock_class_key AF_INET AF_INET6 AF_UNIX AF_NETLINK AF_PACKET TCP accept 중첩 잠금 lock_sock(listen_sk); ← level 0 lock_sock_nested(new_sk, SINGLE_DEPTH_NESTING); ← level 1 → lockdep: 서로 다른 서브클래스로 허용 AF_UNIX 이중 잠금 if (sk1 < sk2): lock(sk1); ← level 0 lock_nested(sk2, 1); else: 반대 순서 → 주소 정렬로 ABBA 방지 + 서브클래스
그림 19. 네트워크 스택의 잠금 계층. 주소 패밀리별 별도 lock_class_key를 사용하고, 소켓 중첩 시 서브클래스 어노테이션으로 lockdep 경고를 억제합니다.

서브시스템: MM lockdep 어노테이션

메모리 관리(MM) 서브시스템은 lockdep와 밀접하게 연동됩니다. mmap_lock, 페이지 테이블(Page Table) 잠금, 메모리 할당기 내부 잠금이 다양한 컨텍스트에서 중첩되며, 특히 fs_reclaim 어노테이션은 메모리 부족 시 잠재적 교착을 감지합니다.

mmap_lock과 lockdep

/* include/linux/mm_types.h */
struct mm_struct {
    struct rw_semaphore mmap_lock;
    /*
     * mmap_lock은 VMA 트리를 보호하는 핵심 잠금
     *
     * 잠금 순서 규칙:
     *   mmap_lock(write) → page table lock
     *   mmap_lock(read)  → page fault 처리
     *   mmap_lock        → i_rwsem (일부 경로)
     */
};

/* mm/mmap.c — mmap_lock lockdep 어노테이션 */
static inline void mmap_write_lock(
    struct mm_struct *mm)
{
    down_write(&mm->mmap_lock);
}

/* 중첩 mmap_lock (ptrace, fork 등) */
static inline void mmap_write_lock_nested(
    struct mm_struct *mm, int subclass)
{
    down_write_nested(&mm->mmap_lock,
                      subclass);
}

fs_reclaim 어노테이션

fs_reclaim은 메모리 회수(Memory Reclaim) 경로에서의 잠금 의존성을 추적하는 lockdep 어노테이션입니다. __GFP_FS 플래그가 있는 메모리 할당은 파일시스템 잠금을 획득할 수 있으며, 이미 파일시스템 잠금을 보유한 상태에서 메모리를 할당하면 교착이 발생할 수 있습니다.

/* mm/page_alloc.c — fs_reclaim lockdep */
static inline bool __need_fs_reclaim(
    gfp_t gfp_mask)
{
    /* GFP_NOFS/GFP_NOIO → fs_reclaim 불필요 */
    if (!(gfp_mask & __GFP_FS))
        return false;
    if (current->flags & PF_MEMALLOC)
        return false;
    return true;
}

/* mm/vmscan.c — fs_reclaim 어노테이션 */
static unsigned long
__perform_reclaim(gfp_t gfp_mask, ...)
{
    /* fs_reclaim 의존성 추적 시작 */
    fs_reclaim_acquire(gfp_mask);
    /*
     * lockdep에 가상 잠금 "fs_reclaim" 획득을 알림
     * 이 시점에 이미 잡고 있는 잠금과
     * fs_reclaim 사이의 의존성이 기록됨
     */

    ret = try_to_free_pages(...);

    fs_reclaim_release(gfp_mask);
    return ret;
}

fs_reclaim 교착 시나리오

/* fs_reclaim 교착 시나리오 */

/* Thread 1: 파일시스템 작업 */
mutex_lock(&inode->i_rwsem);
/* 메모리 할당 (GFP_KERNEL → __GFP_FS 포함) */
page = alloc_page(GFP_KERNEL);
/*
 * 메모리 부족 시 kswapd가 reclaim 수행
 * → try_to_free_pages()
 * → shrink_slab()
 * → super_operations->free_inode()
 * → mutex_lock(&other_inode->i_rwsem)  ← 같은 클래스!
 * → 잠재적 교착
 */
mutex_unlock(&inode->i_rwsem);

/* lockdep 경고: i_rwsem → fs_reclaim → i_rwsem (순환) */
/* 해결: GFP_NOFS 사용 */
page = alloc_page(GFP_NOFS);

GFP 플래그와 lockdep

GFP 플래그__GFP_FS__GFP_IOfs_reclaim사용 컨텍스트
GFP_KERNELOO활성일반 프로세스 컨텍스트
GFP_NOFSXO비활성파일시스템 잠금 보유 시
GFP_NOIOXX비활성I/O 잠금 보유 시
GFP_ATOMICXX비활성인터럽트, 스핀락(Spinlock) 내부

페이지 테이블 잠금

/* include/linux/mm.h — 페이지 테이블 잠금 계층 */

/* PGD/P4D/PUD/PMD — mmap_lock 아래에서 보호 */
/* PTE — pte_lockptr()로 개별 잠금 */

/* 잠금 순서:
 * mmap_lock(write)
 *   → pmd_lock(pmd)
 *     → pte_lock(ptl)          ← page table lock
 *       → page lock (PG_locked)
 */

/* split page table lock: 각 PTE 페이지별 별도 잠금 */
#ifdef CONFIG_SPLIT_PTLOCK_CPUS
static inline spinlock_t *pte_lockptr(
    struct mm_struct *mm,
    pmd_t *pmd)
{
    return ptlock_ptr(pmd_page(*pmd));
}
#endif
MM lockdep: mmap_lock → fs_reclaim 교착 경로 프로세스 컨텍스트 i_rwsem 획득 alloc_page(GFP_KERNEL) 메모리 부족 → reclaim fs_reclaim 가상 잠금 fs_reclaim_acquire(gfp) lockdep에 가상 잠금으로 의존성 추적 Reclaim 경로 try_to_free_pages() → shrink_slab() → fs shrinker callback → i_rwsem 획득 시도 (순환!) lockdep 순환 감지 i_rwsem → fs_reclaim → i_rwsem (같은 lock_class) 해결 방법 1. GFP_NOFS 사용: __GFP_FS 제거로 fs_reclaim 방지 2. memalloc_nofs_save()/restore()
그림 20. MM lockdep의 fs_reclaim 교착 경로. i_rwsem을 보유한 채 GFP_KERNEL로 메모리를 할당하면, reclaim 경로에서 다시 i_rwsem을 획득하려는 순환이 발생합니다.
참고: 커널 5.x부터 memalloc_nofs_save()/memalloc_nofs_restore() API가 도입되어, 명시적 GFP_NOFS 대신 스코프 기반으로 FS reclaim을 비활성화할 수 있습니다. lockdep는 이 API를 인식하여 fs_reclaim 가상 잠금을 자동으로 비활성화합니다.

커널 버전별 진화

lockdep는 2006년 v2.6.18에서 Ingo Molnar에 의해 도입된 이후 지속적으로 개선되어 왔습니다. 각 주요 버전에서의 변경 사항을 추적합니다.

진화 타임라인

커널 버전시기주요 변경의의
v2.6.182006lockdep 최초 도입런타임 잠금 정확성 검증기 탄생. lock_class, BFS 순환 탐지, IRQ 안전성 검증
v2.6.282008LOCK_STAT 추가잠금 경합 통계 수집 (/proc/lock_stat)
v2.6.352010서브클래스 확장MAX_LOCKDEP_SUBCLASSES 8→MAX_LOCK_DEPTH 기반
v3.22012스택 트레이스 개선의존성 간선에 획득 위치 스택 트레이스 저장
v4.142017cross-release 도입CONFIG_LOCKDEP_CROSSRELEASE — completion 등 교차 컨텍스트 의존성
v4.152018cross-release 제거복잡도 대비 이점 부족, Linus 판단으로 revert
v5.02019wait_type 도입LD_WAIT_FREE/SPIN/SLEEP: sleep-in-atomic 컨텍스트 감지
v5.42019read-lock 의존성 강화읽기 잠금 의존성 추적 정확도 개선
v5.62020체인 해시 개선CONFIG_LOCKDEP_CHAINS_BITS 설정 가능, 대규모 시스템 지원
v5.102020lockdep_assert 매크로lockdep_assert_held(), lockdep_assert_not_held() 강화
v5.152021PREEMPT_RT lockdep 통합rt_mutex 기반 잠금에 대한 lockdep 지원 개선
v6.12022per-CPU 잠금 개선percpu rwsem, local_lock lockdep 통합
v6.42023BFS 큐 최적화circular_queue 크기 조정, 메모리 효율 개선
v6.82024lock_class 메모리 최적화lock_class 구조체 크기 축소, 비트필드 최적화
v6.12+2025~PREEMPT_LAZY 지원PREEMPT_LAZY 스케줄링 모델에 대한 lockdep 적응

wait_type 도입 (v5.0)

wait_type은 lockdep v5.0에서 도입된 중요한 기능으로, 잠금 컨텍스트의 "대기 가능 여부"를 분류합니다.

/* include/linux/lockdep_types.h — wait_type (v5.0+) */
enum lockdep_wait_type {
    LD_WAIT_INV   = 0,  /* 무효 (미설정) */
    LD_WAIT_FREE  = 1,  /* lock-free: rcu_read_lock, preempt_disable */
    LD_WAIT_SPIN  = 2,  /* spin-wait: raw_spinlock, spinlock */
    LD_WAIT_CONFIG= 2,  /* PREEMPT_RT에서 SLEEP으로 변경 가능 */
    LD_WAIT_SLEEP = 3,  /* sleep-wait: mutex, rwsem */
    LD_WAIT_MAX   = 4,
};

/*
 * 검사 규칙:
 *   LD_WAIT_FREE 컨텍스트 → LD_WAIT_SPIN/SLEEP 잠금 불가
 *   LD_WAIT_SPIN 컨텍스트 → LD_WAIT_SLEEP 잠금 불가
 *
 * 예: spin_lock() 내부에서 mutex_lock() → lockdep 경고
 */

lockdep_assert 매크로 진화

/* include/linux/lockdep.h — assertion 매크로 */

/* v2.6.18: 기본 assertion */
lockdep_assert_held(&lock);

/* v5.10+: 확장 assertion */
lockdep_assert_held_write(&lock);
lockdep_assert_held_read(&lock);
lockdep_assert_not_held(&lock);
lockdep_assert_held_once(&lock);

/* v5.15+: PREEMPT_RT 관련 */
lockdep_assert_preemption_disabled();
lockdep_assert_preemption_enabled();
lockdep_assert_in_softirq();

/* 구현: CONFIG_PROVE_LOCKING 비활성 시 no-op */
#ifdef CONFIG_PROVE_LOCKING
#define lockdep_assert_held(l) \
    WARN_ON(debug_locks && \
            !lockdep_is_held(l))
#else
#define lockdep_assert_held(l)  do { (void)(l); } while (0)
#endif
lockdep 진화 타임라인 (v2.6.18 ~ v6.x) time v2.6.18 lockdep 도입 (2006, Ingo Molnar) v2.6.28 LOCK_STAT v4.14 cross-release (도입 후 v4.15에서 제거) v5.0 wait_type (sleep-in-atomic 감지) v5.15 PREEMPT_RT (lockdep 통합) v6.x 최적화 (메모리, BFS 큐) v2.6.18 기반 기능 • lock_class / lock_class_key • BFS 순환 탐지 • IRQ 안전성 검증 • 서브클래스 / nest_lock v5.0+ 신규 기능 • wait_type 검사 • LD_WAIT_FREE/SPIN/SLEEP • lockdep_assert 강화 • 체인 해시 설정 가능 v5.15+ 최근 개선 • PREEMPT_RT lockdep • local_lock 통합 • lock_class 메모리 최적화 • PREEMPT_LAZY 지원 lockdep 발전 지표 코드 크기: ~2,000줄 (v2.6.18) → ~6,500줄 (v6.x) 지원 잠금 타입: spinlock/mutex/rwsem (v2.6.18) → +rtmutex/local_lock/percpu_rwsem (v6.x) 감지 가능 버그: ABBA순환+IRQ역전 (v2.6.18) → +sleep-in-atomic+wait_type (v6.x) 발견된 커널 버그: 수천 건 이상 (2006~현재, Ingo Molnar 추정)
그림 21. lockdep 커널 버전별 진화 타임라인. 2006년 v2.6.18 도입 이후 wait_type, PREEMPT_RT 지원 등 지속적으로 기능이 확장되었습니다.

실전: 실제 커널 버그 lockdep 보고서 분석

lockdep 경고 메시지(splat)를 읽고 해석하는 능력은 커널 개발에서 필수적입니다. 이 절에서는 실제 패턴별 lockdep 보고서를 분석하고 수정 방법을 제시합니다.

lockdep splat 구조 해석

lockdep 경고 메시지는 일정한 구조를 따릅니다. 각 구성 요소를 이해하면 빠르게 원인을 파악할 수 있습니다.

/* lockdep splat 구조 분석 */

/* ① 헤더: 경고 유형 */
======================================================
WARNING: possible circular locking dependency detected
6.1.0-rc1 #1 Not tainted
------------------------------------------------------

/* ② 트리거: 어떤 프로세스가 어떤 잠금에서 */
kworker/0:1/123 is trying to acquire lock:
ffff888012345678 (&fs_info->tree_log_mutex){+.+.}-{3:3}
                   ^                         ^^^^   ^^^
                   잠금 이름              IRQ flags  wait_type

/* ③ IRQ 사용 플래그 해석:
 *   {+.+.} = {HARDIRQ, SOFTIRQ, HARDIRQ_READ, SOFTIRQ_READ}
 *   +: USED_IN_xxx 설정
 *   -: ENABLED_xxx 설정
 *   .: 미사용
 *   ?: 알 수 없음
 */

/* ④ 현재 보유 잠금 */
but task is already holding lock:
ffff888098765432 (&fs_info->reloc_mutex){+.+.}-{3:3}

/* ⑤ 의존성 체인 */
which lock already depends on the new lock.

the existing dependency chain (in reverse order) is:

/* ⑥ 체인 내 각 간선 */
-> #2 (&fs_info->tree_log_mutex){+.+.}-{3:3}:
       lock_acquire+0xd8/0x300
       mutex_lock_nested+0x1c/0x30
       btrfs_tree_log_wq_func+0x50/0x180
       ...

-> #1 (&fs_info->ordered_extent_mutex){+.+.}-{3:3}:
       ...

-> #0 (&fs_info->reloc_mutex){+.+.}-{3:3}:
       ...

/* ⑦ 순환 경로 요약 */
Chain exists of:
  tree_log_mutex --> ordered_extent_mutex --> reloc_mutex

/* ⑧ 교착 시나리오 */
Possible unsafe locking scenario:
      CPU0                    CPU1
      ----                    ----
 lock(reloc_mutex);
                              lock(tree_log_mutex);
                              lock(ordered_extent_mutex);
 lock(tree_log_mutex);   <-- deadlock!

IRQ 사용 플래그 참조표

플래그 위치의미+-.
1번째HARDIRQhardirq에서 사용됨hardirq 활성 상태에서 사용미사용
2번째SOFTIRQsoftirq에서 사용됨softirq 활성 상태에서 사용미사용
3번째HARDIRQ_READhardirq에서 읽기 잠금hardirq 활성 + 읽기미사용
4번째SOFTIRQ_READsoftirq에서 읽기 잠금softirq 활성 + 읽기미사용

{3:3}은 wait_type을 나타냅니다. {outer:inner} 형식으로, 3은 LD_WAIT_SLEEP입니다.

패턴 1: ABBA 순환 — 잠금 순서 통일

/* 문제: 두 경로에서 잠금 순서가 다름 */

/* 경로 A: commit_transaction() */
mutex_lock(&fs_info->reloc_mutex);
mutex_lock(&fs_info->tree_log_mutex);

/* 경로 B: btrfs_tree_log_wq_func() */
mutex_lock(&fs_info->tree_log_mutex);
mutex_lock(&fs_info->reloc_mutex);  /* 순환! */

/* 수정: 잠금 순서 통일 */
/* 규칙: reloc_mutex는 항상 tree_log_mutex보다 먼저 */
/* 경로 B 수정: */
mutex_lock(&fs_info->reloc_mutex);
mutex_lock(&fs_info->tree_log_mutex);

패턴 2: IRQ 역전 — irqsave 사용

/* 문제: 프로세스 컨텍스트에서 IRQ 비활성 없이 잠금 */

/* lockdep 경고:
 * WARNING: inconsistent lock state
 * inconsistent {IN-HARDIRQ-W} -> {HARDIRQ-ON-W} usage
 */

/* 프로세스 컨텍스트 (IRQ 활성) */
spin_lock(&dev->lock);  /* ENABLED_HARDIRQ 설정 */
...
spin_unlock(&dev->lock);

/* 인터럽트 핸들러 */
spin_lock(&dev->lock);  /* USED_IN_HARDIRQ 설정 → 충돌! */
...
spin_unlock(&dev->lock);

/* 수정: 프로세스 컨텍스트에서 IRQ 비활성 */
spin_lock_irqsave(&dev->lock, flags);
...
spin_unlock_irqrestore(&dev->lock, flags);

패턴 3: sleep-in-atomic — wait_type 경고

/* 문제: spinlock 내에서 sleep 가능 잠금 획득 */

/* lockdep 경고:
 * BUG: sleeping function called from invalid context
 * in_atomic(): 1, irqs_disabled(): 0
 * ... lock type mismatch, lock(&mtx) wait type: 3
 * held lock: &slock wait type: 2
 */

spin_lock(&slock);       /* LD_WAIT_SPIN (2) */
mutex_lock(&mtx);       /* LD_WAIT_SLEEP (3) → 위반! */
mutex_unlock(&mtx);
spin_unlock(&slock);

/* 수정 방법 1: spinlock 밖에서 mutex 획득 */
mutex_lock(&mtx);
spin_lock(&slock);
...
spin_unlock(&slock);
mutex_unlock(&mtx);

/* 수정 방법 2: spinlock을 mutex로 변경 (sleep 허용 경로인 경우) */

패턴 4: 재귀 잠금 — nest_lock/subclass

/* 문제: 같은 lock_class의 잠금을 중첩 획득 */

/* lockdep 경고:
 * WARNING: possible recursive locking detected
 * kworker/123 is trying to acquire lock:
 *   (&inode->i_rwsem){+.+.}-{3:3}
 * but task is already holding lock:
 *   (&inode->i_rwsem){+.+.}-{3:3}
 */

/* 디렉토리 → 파일 i_rwsem 중첩 (rename 등) */
inode_lock(dir);
inode_lock(child);  /* 같은 lock_class → 경고 */

/* 수정: 서브클래스 사용 */
inode_lock(dir);
inode_lock_nested(child, I_MUTEX_CHILD);

/* 또는 nest_lock 사용 */
mutex_lock_nest_lock(&child->i_rwsem,
                     &dir->i_rwsem);

lockdep splat 디버깅 체크리스트

단계동작확인 사항
1경고 유형 확인circular / inconsistent lock state / recursive / wait_type
2관련 잠금 식별잠금 이름과 lock_class 확인
3의존성 체인 추적→ #N 간선 역추적, 각 획득 위치(call stack) 확인
4교착 시나리오 분석"Possible unsafe locking scenario" 섹션의 CPU0/CPU1 시나리오
5거짓 양성 판별서로 다른 인스턴스가 같은 클래스로 묶였는지 확인
6수정 방법 결정잠금 순서 통일 / irqsave / 서브클래스 / 설계 변경
7수정 검증lockdep 활성 상태로 동일 경로 재실행

흔한 거짓 양성(false positive) 사례

사례원인해결
동적 잠금 배열배열 원소마다 잠금이 있지만 같은 클래스lockdep_set_class()로 개별 키 할당
디렉토리 계층 잠금부모/자식 inode가 같은 lock_classI_MUTEX_PARENT/I_MUTEX_CHILD 서브클래스
소켓 중첩두 소켓이 같은 AF의 lock_classSINGLE_DEPTH_NESTING 서브클래스
블록 장치(Block Device) 체인dm/md 계층에서 하위 장치 잠금 중첩lockdep_set_subclass()
참고: lockdep 경고가 거짓 양성인지 확인하려면, "Possible unsafe locking scenario" 섹션의 CPU0/CPU1 시나리오가 실제로 동시에 발생 가능한지 분석하세요. 경고에 표시된 두 경로가 실제로 같은 잠금 인스턴스에서 충돌할 수 있는지가 핵심입니다. 만약 서로 다른 인스턴스라면 서브클래스 어노테이션으로 해결합니다.
주의: lockdep 경고를 lockdep_set_novalidate_class()lockdep_off()로 억제하는 것은 최후의 수단입니다. 이 방법은 실제 버그를 숨길 수 있으므로, 반드시 경고 원인을 충분히 분석한 후에만 사용하세요. 커널 메인라인에서는 이런 억제를 거의 수용하지 않습니다.

참고 자료

Lockdep 검증기의 설계, 사용법, 경고 해석에 대한 공식 문서 및 참고 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

lockdep와 관련된 다른 동기화 및 디버깅 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.