동기화 기법 (Synchronization)

커널 동기화 프리미티브 — spinlock, mutex, rwlock, semaphore, seqlock, wait queue, completion의 특성과 사용법을 비교합니다.

관련 표준: C11 Memory Model (ISO/IEC 9899:2011, 원자적 연산/메모리 순서), LKMM (Linux Kernel Memory Model) — 커널 동기화 프리미티브의 이론적 기반이 되는 메모리 모델 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

동기화가 필요한 이유

커널은 진정한 병렬 실행 환경입니다. SMP 시스템에서 여러 CPU가 동시에 같은 자료구조에 접근할 수 있고, 인터럽트나 선점으로 인해 단일 CPU에서도 레이스 컨디션이 발생합니다. Linux 커널은 다양한 동기화 프리미티브를 제공하여 critical section을 보호합니다.

Spinlock

spinlock은 가장 기본적인 커널 동기화 메커니즘입니다. 락을 획득할 수 없으면 CPU에서 busy-wait(spinning) 합니다. 슬립이 불가능한 인터럽트 컨텍스트에서 사용할 수 있는 유일한 잠금입니다.

#include <linux/spinlock.h>

DEFINE_SPINLOCK(my_lock);

/* 프로세스 컨텍스트에서만 */
spin_lock(&my_lock);
/* critical section */
spin_unlock(&my_lock);

/* 인터럽트 비활성화 + 잠금 (IRQ handler와 공유 시) */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
/* critical section */
spin_unlock_irqrestore(&my_lock, flags);

/* Bottom half 비활성화 + 잠금 */
spin_lock_bh(&my_lock);
/* critical section */
spin_unlock_bh(&my_lock);

spinlock을 보유한 상태에서는 절대로 슬립하면 안 됩니다. kmalloc(GFP_KERNEL), mutex_lock(), copy_from_user() 등 슬립 가능한 함수를 호출하면 deadlock이 발생합니다.

Mutex

mutex는 프로세스 컨텍스트 전용 잠금입니다. 락을 획득할 수 없으면 태스크를 슬립(sleep) 시키므로 CPU를 낭비하지 않습니다.

#include <linux/mutex.h>

DEFINE_MUTEX(my_mutex);

mutex_lock(&my_mutex);           /* 획득 (슬립 가능) */
/* critical section — can sleep here */
mutex_unlock(&my_mutex);

/* 시그널 인터럽트 가능 잠금 */
if (mutex_lock_interruptible(&my_mutex))
    return -ERESTARTSYS;

/* 비블로킹 시도 */
if (!mutex_trylock(&my_mutex))
    return -EBUSY;

Reader-Writer Lock

읽기 작업이 쓰기보다 훨씬 빈번할 때 사용합니다. 여러 reader가 동시에 접근 가능하지만, writer는 독점 접근합니다.

/* Spinlock 기반 rwlock */
DEFINE_RWLOCK(my_rwlock);

read_lock(&my_rwlock);
/* read-only access */
read_unlock(&my_rwlock);

write_lock(&my_rwlock);
/* exclusive access */
write_unlock(&my_rwlock);

/* Mutex 기반 rw_semaphore (슬립 가능) */
DECLARE_RWSEM(my_rwsem);

down_read(&my_rwsem);   /* shared read */
up_read(&my_rwsem);

down_write(&my_rwsem);  /* exclusive write */
up_write(&my_rwsem);

Seqlock

seqlock은 writer 우선 reader-writer 락입니다. reader는 잠금 없이 읽되, 시퀀스 번호를 검사하여 읽는 동안 writer가 수정했는지 확인합니다. 충돌 시 reader가 재시도합니다.

DEFINE_SEQLOCK(my_seq);

/* Writer (exclusive) */
write_seqlock(&my_seq);
/* modify shared data */
write_sequnlock(&my_seq);

/* Reader (lock-free, retry on conflict) */
unsigned int seq;
do {
    seq = read_seqbegin(&my_seq);
    /* read shared data into local vars */
} while (read_seqretry(&my_seq, seq));

Wait Queue (대기 큐)

Wait queue는 커널에서 이벤트 기반 대기를 구현하는 핵심 메커니즘입니다. 특정 조건이 만족될 때까지 태스크를 슬립 상태(TASK_INTERRUPTIBLE 또는 TASK_UNINTERRUPTIBLE)로 전환하고, 조건이 충족되면 깨웁니다. 커널 전역에서 I/O 완료 대기, 버퍼 가용 대기, 디바이스 준비 대기 등에 광범위하게 사용됩니다.

자료구조와 초기화

Wait queue는 wait_queue_head_t(대기 큐 헤드)와 wait_queue_entry_t(개별 대기 엔트리)로 구성됩니다.

#include <linux/wait.h>

/* 정적 초기화 */
DECLARE_WAIT_QUEUE_HEAD(my_wq);

/* 동적 초기화 */
struct wait_queue_head my_wq;
init_waitqueue_head(&my_wq);

/* 내부 구조 (include/linux/wait.h) */
struct wait_queue_head {
    spinlock_t      lock;       /* 대기 큐 보호용 spinlock */
    struct list_head head;      /* 대기 엔트리 연결 리스트 */
};

struct wait_queue_entry {
    unsigned int     flags;      /* WQ_FLAG_EXCLUSIVE 등 */
    void            *private;    /* 보통 current (task_struct *) */
    wait_queue_func_t func;     /* 깨우기 콜백 (기본: default_wake_function) */
    struct list_head entry;      /* 대기 큐 연결 */
};

wait_event 매크로 계열

wait_event 매크로는 조건(condition)이 참이 될 때까지 자동으로 슬립/깨우기/재검사를 처리합니다. 수동으로 schedule()을 호출하는 것보다 안전하고 간결합니다.

/* 기본: TASK_UNINTERRUPTIBLE — 시그널에 의해 깨어나지 않음 */
wait_event(wq, condition);

/* TASK_INTERRUPTIBLE — 시그널 수신 시 -ERESTARTSYS 반환 */
int ret = wait_event_interruptible(wq, condition);
if (ret)
    return -ERESTARTSYS;  /* 시그널에 의해 깨어남 */

/* 타임아웃: 조건 충족 시 남은 jiffies, 타임아웃 시 0 반환 */
unsigned long remaining;
remaining = wait_event_timeout(wq, condition, msecs_to_jiffies(5000));
if (!remaining) {
    pr_warn("timeout waiting for condition\n");
    return -ETIMEDOUT;
}

/* interruptible + timeout 조합 */
remaining = wait_event_interruptible_timeout(wq, condition,
                                              msecs_to_jiffies(3000));
if (remaining == 0)
    return -ETIMEDOUT;       /* 타임아웃 */
if (remaining < 0)
    return -ERESTARTSYS;     /* 시그널 */

/* TASK_KILLABLE — SIGKILL만 응답 (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) */
ret = wait_event_killable(wq, condition);
💡

wait_event 선택 가이드: 사용자 공간 요청 처리 경로에서는 wait_event_interruptible을 사용하세요 (Ctrl+C 응답 필수). 커널 내부 동기화(kthread 간 통신 등)에서는 wait_event이 적합합니다. 하드웨어 대기에는 항상 wait_event_timeout 계열을 사용하여 무한 대기를 방지하세요.

wake_up 계열 함수

조건을 변경한 후 반드시 대기 큐의 waiter들을 깨워야 합니다.

/* 모든 waiter 깨우기 */
wake_up(&wq);                /* TASK_NORMAL (INTERRUPTIBLE + UNINTERRUPTIBLE) */
wake_up_all(&wq);            /* 모든 waiter (exclusive 포함) */

/* TASK_INTERRUPTIBLE waiter만 깨우기 */
wake_up_interruptible(&wq);
wake_up_interruptible_all(&wq);

/* 주의: 조건 변경과 wake_up 사이의 순서가 중요! */
/* 올바른 패턴: */
shared_flag = 1;             /* ① 조건을 먼저 변경 */
smp_wmb();                    /* ② 메모리 배리어 (필요 시) */
wake_up(&wq);                /* ③ 그 다음 깨우기 */

wake_up()은 exclusive waiter를 하나만 깨우고 non-exclusive waiter는 모두 깨웁니다. 모든 waiter를 깨우려면 wake_up_all()을 사용하세요. wake_up_interruptible()TASK_INTERRUPTIBLE 상태의 waiter만 깨우므로, wait_event()(TASK_UNINTERRUPTIBLE)로 대기 중인 태스크는 깨우지 않습니다.

실전 예제: 디바이스 드라이버에서의 Wait Queue

#include <linux/wait.h>
#include <linux/sched.h>

struct my_device {
    wait_queue_head_t  read_wq;       /* 읽기 대기 큐 */
    spinlock_t         lock;
    char               buf[256];
    size_t             data_len;      /* 0이면 데이터 없음 */
    bool               disconnected;
};

/* 읽기: 데이터가 올 때까지 대기 */
static ssize_t my_read(struct file *filp, char __user *ubuf,
                       size_t count, loff_t *ppos)
{
    struct my_device *dev = filp->private_data;
    int ret;

    /* non-blocking 모드 처리 */
    if ((filp->f_flags & O_NONBLOCK) && !dev->data_len)
        return -EAGAIN;

    /* 데이터가 준비되거나 연결 해제될 때까지 대기 */
    ret = wait_event_interruptible(dev->read_wq,
                dev->data_len > 0 || dev->disconnected);
    if (ret)
        return -ERESTARTSYS;

    if (dev->disconnected)
        return -ENODEV;

    spin_lock(&dev->lock);
    count = min(count, dev->data_len);
    if (copy_to_user(ubuf, dev->buf, count)) {
        spin_unlock(&dev->lock);
        return -EFAULT;
    }
    dev->data_len = 0;
    spin_unlock(&dev->lock);

    return count;
}

/* 인터럽트 핸들러: 데이터 수신 시 대기자 깨우기 */
static irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_device *dev = data;

    spin_lock(&dev->lock);
    dev->data_len = read_hw_fifo(dev->buf, sizeof(dev->buf));
    spin_unlock(&dev->lock);

    wake_up_interruptible(&dev->read_wq);  /* 대기자 깨우기 */

    return IRQ_HANDLED;
}

Exclusive Wait (Thundering Herd 방지)

다수의 태스크가 같은 이벤트를 기다릴 때 wake_up()으로 모두 깨우면 Thundering Herd 문제가 발생합니다. 하나의 태스크만 실제로 작업을 수행하고 나머지는 다시 슬립하므로 CPU를 낭비합니다. WQ_FLAG_EXCLUSIVE 플래그로 exclusive waiter를 등록하면 wake_up()이 exclusive waiter를 하나만 깨웁니다.

/* exclusive waiter 등록 (수동 방식) */
DEFINE_WAIT(wait);
prepare_to_wait_exclusive(&wq, &wait, TASK_INTERRUPTIBLE);

while (!condition) {
    schedule();
    prepare_to_wait_exclusive(&wq, &wait, TASK_INTERRUPTIBLE);
}
finish_wait(&wq, &wait);

/* exclusive waiter가 있을 때의 wake_up 동작: */
/*   wake_up()         → non-exclusive 전부 + exclusive 1개 */
/*   wake_up_all()     → exclusive 포함 전부 깨움 */
/*   wake_up_nr(&wq, n) → non-exclusive 전부 + exclusive n개 */

/* 대표적 사용 예: accept() 시스템 콜 */
/* 여러 스레드가 listen socket에서 accept() 대기 시 */
/* exclusive wait으로 연결 1개당 스레드 1개만 깨움 */

수동 Wait Queue 사용 패턴

wait_event 매크로로 충분하지 않은 복잡한 경우(조건 검사 중 락 획득/해제, 커스텀 깨우기 로직 등)에는 수동으로 wait queue를 조작합니다.

/* 수동 패턴: 정확한 순서가 중요! */
DEFINE_WAIT(wait);

for (;;) {
    /* ① 대기 큐에 등록 + 태스크 상태 변경 */
    prepare_to_wait(&wq, &wait, TASK_INTERRUPTIBLE);

    /* ② 조건 검사 (락 보호 하에) */
    spin_lock(&lock);
    if (condition_met) {
        spin_unlock(&lock);
        break;
    }
    spin_unlock(&lock);

    /* ③ 시그널 확인 (interruptible인 경우) */
    if (signal_pending(current)) {
        ret = -ERESTARTSYS;
        break;
    }

    /* ④ 슬립 — wake_up()이 호출될 때까지 */
    schedule();
}
/* ⑤ 대기 큐에서 제거 + 태스크 상태 TASK_RUNNING 복구 */
finish_wait(&wq, &wait);

/* 주의: ①과 ② 사이에서 wake_up()이 호출될 수 있지만 */
/* prepare_to_wait()이 상태를 설정하므로 깨우기를 놓치지 않음 */
/* (lost wakeup 방지가 이 패턴의 핵심) */

Lost Wakeup 버그: 조건 검사와 schedule() 사이에서 다른 CPU가 조건을 변경하고 wake_up()을 호출하면, 대기 큐에 아직 등록되지 않아 깨우기를 놓칠 수 있습니다. 반드시 prepare_to_wait()으로 대기 큐 등록 후에 조건을 검사하세요. wait_event 매크로를 사용하면 이 순서를 자동으로 보장합니다.

Wait Queue와 poll/select/epoll

사용자 공간의 poll()/select()/epoll() 시스템 콜은 내부적으로 wait queue를 사용합니다. 드라이버의 poll 파일 오퍼레이션은 poll_wait()로 대기 큐를 VFS poll 테이블에 등록합니다.

static __poll_t my_poll(struct file *filp,
                        struct poll_table_struct *pt)
{
    struct my_device *dev = filp->private_data;
    __poll_t mask = 0;

    /* 대기 큐를 poll 테이블에 등록 (슬립하지 않음!) */
    poll_wait(filp, &dev->read_wq, pt);
    poll_wait(filp, &dev->write_wq, pt);

    /* 현재 상태 확인 */
    spin_lock(&dev->lock);
    if (dev->data_len > 0)
        mask |= EPOLLIN | EPOLLRDNORM;   /* 읽기 가능 */
    if (dev->buf_space > 0)
        mask |= EPOLLOUT | EPOLLWRNORM;  /* 쓰기 가능 */
    if (dev->disconnected)
        mask |= EPOLLHUP;                 /* 연결 종료 */
    spin_unlock(&dev->lock);

    return mask;
}

/* 상태 변경 시 대기자(및 epoll)에게 알림 */
wake_up_interruptible(&dev->read_wq);  /* epoll에도 자동 전달됨 */

Wait Queue 내부 동작

Wait Queue 동작 흐름 Task A: wait_event() prepare_to_wait() condition == false? Yes schedule() → SLEEP wake_up wait_queue_head_t spinlock + list_head → entry A (Task A) → entry B (Task B) → entry C (excl.) Task B: 조건 변경 condition = true wake_up(&wq) Task A: RUNNING 복귀 finish_wait(): 큐에서 제거 + RUNNING
wait_event / wake_up의 내부 동작 흐름

Completion

Completion은 wait queue를 기반으로 구축된 일회성 이벤트 알림 메커니즘입니다. "작업이 완료되었다"라는 단순한 시그널링에 최적화되어 있으며, wait queue보다 간결한 API를 제공합니다. 커널 스레드 시작/종료 동기화, 펌웨어 로딩 완료, DMA 전송 완료, 디바이스 프로브 완료 등에 널리 사용됩니다.

초기화와 내부 구조

#include <linux/completion.h>

/* 정적 초기화 (done = 0, 미완료 상태) */
DECLARE_COMPLETION(my_comp);

/* 동적 초기화 */
struct completion my_comp;
init_completion(&my_comp);

/* 재사용을 위한 재초기화 (done 카운터만 0으로 리셋) */
reinit_completion(&my_comp);

/* 내부 구조 (include/linux/completion.h) */
struct completion {
    unsigned int          done;   /* 완료 카운터 (0=미완료, >0=완료) */
    struct swait_queue_head wait;  /* simple wait queue */
};

Completion은 내부적으로 swait_queue_head(simple wait queue)를 사용합니다. 일반 wait queue와 달리 커스텀 콜백이 없고 FIFO 순서로만 깨우므로 오버헤드가 더 낮습니다. done 카운터 덕분에 complete()wait_for_completion()보다 먼저 호출되어도 정상 동작합니다 (lost wakeup 없음).

대기 API

/* 기본: TASK_UNINTERRUPTIBLE (무한 대기) */
wait_for_completion(&my_comp);

/* 타임아웃: 남은 jiffies 반환 (0이면 타임아웃) */
unsigned long remaining;
remaining = wait_for_completion_timeout(&my_comp,
                                         msecs_to_jiffies(5000));
if (!remaining) {
    pr_err("operation timed out\n");
    return -ETIMEDOUT;
}

/* 시그널 인터럽트 가능: -ERESTARTSYS 반환 시 시그널 수신 */
int ret = wait_for_completion_interruptible(&my_comp);
if (ret)
    return -ERESTARTSYS;

/* interruptible + timeout 조합 */
long result = wait_for_completion_interruptible_timeout(
                    &my_comp, msecs_to_jiffies(3000));
if (result == 0)
    return -ETIMEDOUT;
if (result < 0)
    return -ERESTARTSYS;

/* SIGKILL만 응답 (TASK_KILLABLE) */
ret = wait_for_completion_killable(&my_comp);

/* 비블로킹 완료 확인 (슬립하지 않음) */
if (try_wait_for_completion(&my_comp)) {
    /* 이미 완료됨 */
} else {
    /* 아직 미완료 */
}

/* 완료 여부 확인만 (done 카운터 소비하지 않음) */
if (completion_done(&my_comp))
    pr_info("already completed\n");

완료 알림 (시그널링)

/* 대기자 1개만 깨우기 (done 카운터 1 증가) */
complete(&my_comp);

/* 모든 대기자 깨우기 (done = UINT_MAX로 설정) */
complete_all(&my_comp);

/* complete()와 complete_all()의 차이: */
/*   complete()    → done++, waiter 1개 깨움 */
/*                   반복 호출로 여러 waiter를 순차적으로 깨울 수 있음 */
/*   complete_all() → done = UINT_MAX, 모든 waiter 깨움 */
/*                    이후 wait_for_completion()은 즉시 반환 */
/*                    재사용 시 반드시 reinit_completion() 필요 */

실전 사용 예제

/* 예제 1: 커널 스레드 시작 동기화 */
struct my_context {
    struct completion started;
    struct completion stopped;
    bool should_stop;
    /* ... */
};

static int my_kthread(void *data)
{
    struct my_context *ctx = data;

    /* 초기화 완료 후 생성자에게 알림 */
    complete(&ctx->started);

    while (!ctx->should_stop) {
        /* ... 작업 수행 ... */
    }

    /* 종료 알림 */
    complete(&ctx->stopped);
    return 0;
}

/* 생성자 */
init_completion(&ctx->started);
init_completion(&ctx->stopped);
kthread_run(my_kthread, ctx, "my-worker");
wait_for_completion(&ctx->started);  /* 스레드 초기화 완료 대기 */

/* 종료 요청 */
ctx->should_stop = true;
wait_for_completion(&ctx->stopped);  /* 스레드 종료 대기 */

/* ─────────────────────────────────── */
/* 예제 2: DMA 전송 완료 대기 */
struct dma_op {
    struct completion done;
    dma_addr_t addr;
    int status;
};

static void dma_callback(void *param)
{
    struct dma_op *op = param;
    op->status = 0;
    complete(&op->done);  /* DMA 완료 알림 */
}

/* DMA 시작 */
reinit_completion(&op->done);
start_dma_transfer(op->addr, dma_callback, op);

/* 완료 대기 (타임아웃 포함) */
if (!wait_for_completion_timeout(&op->done,
                                  msecs_to_jiffies(1000))) {
    pr_err("DMA transfer timed out\n");
    abort_dma_transfer(op);
    return -ETIMEDOUT;
}

/* ─────────────────────────────────── */
/* 예제 3: 펌웨어 로딩 완료 대기 */
static int my_probe(struct platform_device *pdev)
{
    struct completion fw_done;
    init_completion(&fw_done);

    /* 비동기 펌웨어 요청 */
    request_firmware_nowait(THIS_MODULE, true,
        "my_fw.bin", &pdev->dev, GFP_KERNEL,
        &fw_done, fw_loaded_callback);

    /* 최대 30초 대기 */
    if (!wait_for_completion_interruptible_timeout(
            &fw_done, msecs_to_jiffies(30000))) {
        dev_err(&pdev->dev, "firmware load timed out\n");
        return -ETIMEDOUT;
    }
    return 0;
}

Wait Queue vs Completion 비교

항목Wait QueueCompletion
용도조건 기반 반복 대기일회성 이벤트 알림
조건 검사매크로가 condition을 반복 검사내부 done 카운터 자동 관리
재사용별도 처리 불필요reinit_completion() 필요
Lost wakeup올바른 패턴 필수done 카운터로 자동 방지
Exclusive 대기지원 (WQ_FLAG_EXCLUSIVE)미지원
poll/epoll 연동가능 (poll_wait())불가
대표 사용처디바이스 I/O, 소켓, 프로세스 대기kthread 동기화, DMA, 펌웨어 로딩

동기화 프리미티브 비교

동기화 메커니즘 선택 가이드 인터럽트 컨텍스트인가? Yes spinlock_irqsave No 짧은 critical section? Yes spinlock No mutex
동기화 메커니즘 선택 흐름
프리미티브슬립인터럽트재귀용도
spinlock불가가능불가짧은 critical section, IRQ handler
mutex가능불가불가긴 critical section, 프로세스 컨텍스트
rw_semaphore가능불가불가읽기 다수, 쓰기 소수
seqlockreader 불가가능불가writer 우선, 간단한 데이터
RCUreader 불가가능가능읽기 최적화, 포인터 교체
atomicN/A가능N/A카운터, 단일 변수
wait_queue가능불가N/A조건 기반 이벤트 대기
completion가능불가N/A일회성 완료 알림
💡

lockdep은 커널의 런타임 잠금 의존성 검사 도구입니다. CONFIG_LOCKDEP을 활성화하면 잠금 순서 위반(잠재적 deadlock)을 자동으로 감지하여 경고합니다.

lockdep: 잠금 의존성 검증

lockdep은 런타임에 잠금 획득 순서를 추적하여 잠재적 교착 상태를 탐지합니다:

/* lockdep이 감지하는 문제들 */

/* 1. AB-BA 교착 (lock ordering violation) */
/* CPU 0: lock(A) → lock(B) */
/* CPU 1: lock(B) → lock(A)  → DEADLOCK */

/* 2. 재귀 잠금 */
/* lock(A) → lock(A)  → DEADLOCK */

/* 3. IRQ 안전성 위반 */
/* 프로세스: lock(A) */
/* IRQ handler: lock(A)  → DEADLOCK */
/* → lock_irqsave(A) 사용해야 함 */

/* lockdep_assert 매크로 (디버깅 보조) */
lockdep_assert_held(&my_lock);       /* 잠금 보유 확인 */
lockdep_assert_not_held(&my_lock);   /* 잠금 미보유 확인 */
lockdep_assert_held_write(&my_rwsem); /* 쓰기 잠금 확인 */

RT Mutex (우선순위 상속)

RT mutex는 우선순위 역전(priority inversion) 문제를 해결합니다. 낮은 우선순위 태스크가 잠금을 보유 중이면, 대기 중인 높은 우선순위 태스크의 우선순위를 일시적으로 상속합니다:

#include <linux/rtmutex.h>

DEFINE_RT_MUTEX(my_rt_mutex);

rt_mutex_lock(&my_rt_mutex);
/* critical section */
rt_mutex_unlock(&my_rt_mutex);

/* PREEMPT_RT 커널에서는 일반 mutex/spinlock도 */
/* 내부적으로 rt_mutex 기반으로 동작합니다. */

Wait/Wound Mutex (ww_mutex)

여러 잠금을 동시에 획득해야 할 때 교착 상태를 자동으로 회피합니다. GPU 드라이버(DRM/GEM)에서 버퍼 객체 잠금에 사용됩니다:

#include <linux/ww_mutex.h>

static DEFINE_WW_CLASS(my_ww_class);

struct ww_acquire_ctx ctx;
ww_acquire_init(&ctx, &my_ww_class);

/* 여러 잠금 획득 시도 */
ret = ww_mutex_lock(&obj_a->lock, &ctx);
ret = ww_mutex_lock(&obj_b->lock, &ctx);
if (ret == -EDEADLK) {
    /* 교착 감지: 잠금 해제 후 contended 잠금 먼저 획득 */
    ww_mutex_unlock(&obj_a->lock);
    ww_mutex_lock_slow(&obj_b->lock, &ctx);
    /* obj_a 재시도 */
}

ww_acquire_done(&ctx);
/* ... critical section ... */
ww_mutex_unlock(&obj_b->lock);
ww_mutex_unlock(&obj_a->lock);
ww_acquire_fini(&ctx);

잠금 순서 규칙

규칙설명
일관된 순서여러 잠금 획득 시 항상 같은 순서 유지
중첩 잠금spin_lock_nested(&lock, SINGLE_DEPTH_NESTING)
IRQ 안전IRQ handler와 공유하는 잠금은 _irqsave 사용
잠금 계층상위 → 하위 순서 (예: inode lock → page lock)
최소 범위critical section을 가능한 짧게 유지

CONFIG_DEBUG_LOCK_ALLOCCONFIG_PROVE_LOCKING을 개발 커널에서 반드시 활성화하세요. lockdep의 오버헤드는 개발 시에만 존재하며, 프로덕션에서는 비활성화합니다. lockdep 경고는 "잠재적" 교착 상태이므로 즉시 수정해야 합니다.

동기화 관련 주요 버그 사례

커널 개발에서 동기화 버그는 가장 디버깅하기 어려운 문제 중 하나입니다. 재현이 어렵고, 증상이 원인과 멀리 떨어져 나타나며, 특정 타이밍이나 CPU 수에서만 발생하기도 합니다. 이 섹션에서는 실제로 발생했던 주요 동기화 버그 패턴과 그 탐지/예방 방법을 살펴봅니다.

ABBA 데드락 패턴과 실제 사례

ABBA 데드락은 두 개 이상의 잠금을 서로 다른 순서로 획득할 때 발생하는 교착 상태입니다. CPU 0이 Lock A를 획득한 후 Lock B를 기다리고, 동시에 CPU 1이 Lock B를 획득한 후 Lock A를 기다리면 두 CPU 모두 영원히 진행할 수 없습니다.

실제 사례 — inode lock과 mmap_lock 순서 역전: mm/ 서브시스템에서 page fault 경로는 mmap_lock을 먼저 획득한 후 파일 시스템의 inode lock을 획득하지만, 일부 ioctl 경로에서는 inode lock을 먼저 획득한 후 사용자 공간 버퍼 접근 시 mmap_lock이 필요해지는 상황이 발생했습니다. 이 순서 역전은 수천 개의 동시 접근이 있는 프로덕션 환경에서만 교착 상태를 유발했습니다.

lockdep은 실제 데드락이 발생하기 전에 lock dependency graph의 순환을 탐지합니다. 잠금 획득 순서를 방향 그래프로 기록하고, 새로운 잠금 의존성이 추가될 때마다 순환이 생기는지 검사합니다.

/* lockdep이 출력하는 전형적인 ABBA 데드락 경고 메시지 */

/*
 ======================================================
 WARNING: possible circular locking dependency detected
 6.8.0-rc1 #1 Not tainted
 ------------------------------------------------------
 process_A/1234 is trying to acquire lock:
 ffff888012345678 (&mm->mmap_lock){++++}-{3:3}, at: do_page_fault+0x1a2/0x520

 but task is already holding lock:
 ffff888087654321 (&inode->i_rwsem){++++}-{3:3}, at: ext4_ioctl+0x15c/0x1100

 which lock already depends on the new lock.

 the existing dependency chain (in reverse order) is:

 -> #1 (&inode->i_rwsem){++++}-{3:3}:
        lock_acquire+0xd1/0x2d0
        down_read+0x3e/0x160
        ext4_map_blocks+0x8c/0x620
        filemap_fault+0x28f/0x8a0

 -> #0 (&mm->mmap_lock){++++}-{3:3}:
        lock_acquire+0xd1/0x2d0
        down_read+0x3e/0x160
        do_page_fault+0x1a2/0x520

 other info that might help us debug this:
  Possible unsafe locking scenario:

        CPU0                    CPU1
        ----                    ----
   lock(&inode->i_rwsem);
                                lock(&mm->mmap_lock);
                                lock(&inode->i_rwsem);
   lock(&mm->mmap_lock);

                    *** DEADLOCK ***
 */

lockdep의 핵심은 lock_class_key입니다. 같은 타입의 모든 잠금 인스턴스는 하나의 클래스로 묶이며, 클래스 간의 의존성만 추적합니다. 동일 클래스의 잠금을 중첩 획득해야 하는 경우 (예: 디렉터리 트리에서 부모 inode → 자식 inode 순서) lock nesting subclass를 사용하여 lockdep에게 구분을 알려야 합니다.

/* lock_class_key: 잠금 클래스 정의 */
static struct lock_class_key my_lock_key;

/* 동적 초기화 시 잠금 클래스 등록 */
spin_lock_init(&obj->lock);
lockdep_set_class(&obj->lock, &my_lock_key);

/* nesting subclass: 같은 타입 잠금의 중첩 획득을 허용 */
/* 예: 부모 inode lock (subclass 0) → 자식 inode lock (subclass 1) */
mutex_lock(&parent->i_mutex);                          /* subclass 0 (기본) */
mutex_lock_nested(&child->i_mutex, I_MUTEX_CHILD);    /* subclass 1 */

/* 커널에서 정의된 inode mutex subclass 상수들 */
enum inode_i_mutex_lock_class {
    I_MUTEX_NORMAL,     /* 일반 파일/디렉터리 */
    I_MUTEX_PARENT,     /* 부모 디렉터리 (rename 등) */
    I_MUTEX_CHILD,      /* 자식 디렉터리 */
    I_MUTEX_XATTR,      /* 확장 속성 */
    I_MUTEX_NONDIR2,    /* 두 번째 비디렉터리 */
    I_MUTEX_PARENT2,    /* 두 번째 부모 (cross-dir rename) */
};
💡

잠금 순서 규칙 문서화: 복잡한 서브시스템에서는 잠금 순서를 소스 코드 주석이나 Documentation/에 명시적으로 기록하세요. 예를 들어 VFS의 Documentation/filesystems/directory-locking.rst는 디렉터리 연산에서의 inode lock 획득 순서를 상세히 규정하고 있습니다. CONFIG_PROVE_LOCKINGCONFIG_DEBUG_LOCK_ALLOC은 개발 커널에서 항상 활성화하여 잠재적 ABBA 패턴을 조기에 발견하세요.

Sleep-in-atomic 컨텍스트 버그

atomic 컨텍스트(spinlock 보유, 인터럽트 비활성화, preemption 비활성화)에서 sleep 가능 함수를 호출하면 시스템이 교착 상태에 빠지거나 스케줄러가 손상됩니다. 이 버그는 코드 리뷰만으로는 놓치기 쉬우며, 특정 실행 경로에서만 발생하기도 합니다.

전형적 패턴 — spinlock 내 GFP_KERNEL 할당: spin_lock()으로 잠금을 획득한 상태에서 kmalloc(GFP_KERNEL)을 호출하면, 메모리 부족 시 커널이 직접 회수(direct reclaim)를 시도하고 이는 I/O 대기를 포함하므로 sleep합니다. spinlock은 선점을 비활성화하므로 다른 태스크가 CPU를 점유할 수 없어 데드락이 발생합니다.

/* 잘못된 코드: spinlock 보유 중 sleep 가능 함수 호출 */
spin_lock(&my_lock);

/* BUG: GFP_KERNEL은 sleep 가능 — atomic 컨텍스트에서 금지! */
buf = kmalloc(4096, GFP_KERNEL);

/* BUG: copy_from_user()는 page fault로 sleep 가능 */
copy_from_user(buf, ubuf, len);

/* BUG: mutex_lock()은 contention 시 sleep */
mutex_lock(&other_mutex);

spin_unlock(&my_lock);

/* 올바른 코드: atomic 컨텍스트에서는 GFP_ATOMIC 사용 */
spin_lock(&my_lock);
buf = kmalloc(4096, GFP_ATOMIC);  /* sleep하지 않음, 실패 가능 */
if (!buf) {
    spin_unlock(&my_lock);
    return -ENOMEM;
}
/* ... */
spin_unlock(&my_lock);

might_sleep() 매크로는 디버그 빌드에서 현재 컨텍스트가 atomic인 경우 경고를 출력합니다. 많은 커널 API 내부에 이미 삽입되어 있으며, 커스텀 sleep 가능 함수에도 추가하는 것이 좋습니다.

/* might_sleep() — 디버그 빌드에서 atomic 컨텍스트 검사 */
void my_blocking_function(void)
{
    /* 이 함수가 sleep할 수 있음을 선언 */
    might_sleep();

    /* ... sleep 가능한 작업 수행 ... */
    mutex_lock(&some_mutex);
    /* ... */
    mutex_unlock(&some_mutex);
}

/* might_sleep()의 디버그 빌드 구현 (kernel/sched/core.c) */
/*
 * CONFIG_DEBUG_ATOMIC_SLEEP 활성화 시:
 *   - preempt_count() != 0 이면 경고 (spinlock, BH, IRQ disabled 등)
 *   - in_atomic() 검사
 *   - 스택 트레이스 출력
 *
 * 커널 로그 출력 예:
 * BUG: sleeping function called from invalid context at kernel/locking/mutex.c:580
 * in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 1234, name: my_process
 * preempt_count: 1 (preempt_disable)
 * Call Trace:
 *   dump_stack+0x6d/0x88
 *   ___might_sleep+0x100/0x170
 *   mutex_lock+0x1c/0x40
 *   my_blocking_function+0x28/0x60
 *   my_spinlock_holder+0x44/0x80   <-- 여기서 spinlock 보유 중
 */
💡

CONFIG_DEBUG_ATOMIC_SLEEP 활성화: 개발 커널에서 CONFIG_DEBUG_ATOMIC_SLEEP=y를 설정하면 might_sleep()이 실제로 검사를 수행합니다. 프로덕션 커널에서는 이 옵션이 비활성화되어 might_sleep()은 빈 매크로로 컴파일됩니다. 또한 CONFIG_PROVE_LOCKING은 lockdep 기반으로 sleep-in-atomic을 더욱 정밀하게 탐지합니다.

RCU와 스핀락 혼용 시 문제

RCU(Read-Copy-Update)는 reader 측의 오버헤드를 극도로 낮추는 동기화 메커니즘이지만, 잘못된 사용 패턴은 미묘하고 치명적인 버그를 유발합니다. 특히 non-preemptible RCU(CONFIG_PREEMPT_NONE, CONFIG_PREEMPT_VOLUNTARY)에서 rcu_read_lock() 구간은 선점이 비활성화되므로 sleep 가능 함수를 호출할 수 없습니다.

rcu_read_lock() 내에서의 sleep: non-preemptible RCU 구성에서 rcu_read_lock()rcu_read_unlock() 사이에서 sleep하면 grace period가 완료되지 않아 메모리 누수 또는 use-after-free가 발생합니다. rcu_read_lock()은 preempt_disable()의 래퍼이므로, sleep은 다른 태스크의 실행을 방해하여 RCU 콜백 처리를 무기한 지연시킵니다.

/* 잘못된 코드: rcu_dereference() 없이 RCU 보호 포인터 직접 접근 */
struct my_data __rcu *global_ptr;

rcu_read_lock();
/* BUG: 컴파일러가 포인터 읽기를 재배치하거나 */
/*       추측 실행으로 인한 stale 값 참조 가능 */
struct my_data *p = global_ptr;       /* 잘못됨! */
do_something(p->field);
rcu_read_unlock();

/* 올바른 코드: rcu_dereference()로 접근 */
rcu_read_lock();
struct my_data *p = rcu_dereference(global_ptr);
if (p)
    do_something(p->field);
rcu_read_unlock();

/* rcu_dereference()는 READ_ONCE() + 의존성 배리어를 포함하여 */
/* 컴파일러와 CPU의 재배치를 방지합니다 */

sleep이 필요한 RCU read-side critical section에서는 일반 RCU 대신 SRCU(Sleepable RCU)를 사용해야 합니다. SRCU는 reader 측에서 sleep을 허용하지만, 도메인별로 별도의 srcu_struct를 관리해야 하며 오버헤드가 더 높습니다.

/* SRCU: sleep 가능한 RCU read-side critical section */
#include <linux/srcu.h>

DEFINE_SRCU(my_srcu);

/* reader: sleep 가능 */
int idx = srcu_read_lock(&my_srcu);
/* sleep 가능한 작업 수행 가능 */
mutex_lock(&some_mutex);
/* ... */
mutex_unlock(&some_mutex);
srcu_read_unlock(&my_srcu, idx);

/* updater: grace period 대기 */
synchronize_srcu(&my_srcu);

/* 일반 RCU를 써야 할 곳에서 SRCU를 쓰지 않은 버그 패턴: */
/*   1. notifier chain에서 sleep 가능 콜백 등록 시 */
/*   2. 파일시스템 콜백에서 I/O 대기가 필요한 경우 */
/*   3. 네트워크 필터 훅에서 사용자 공간 통신이 필요한 경우 */

sparse 정적 분석 도구는 __rcu 어노테이션을 통해 RCU 보호 포인터의 잘못된 사용을 컴파일 타임에 탐지합니다.

/* sparse __rcu 어노테이션으로 정적 분석 */
struct my_struct {
    struct data __rcu *rcu_ptr;    /* RCU 보호 포인터로 표시 */
    struct data *normal_ptr;       /* 일반 포인터 */
};

/* sparse 검사 실행: */
/* make C=1 CF="-D__CHECK_ENDIAN__" drivers/my_driver.o */

/* sparse가 경고하는 패턴들: */
/*   - __rcu 포인터를 rcu_dereference() 없이 직접 읽기 */
/*   - rcu_dereference()로 읽은 값을 __rcu 포인터에 대입 */
/*   - rcu_assign_pointer() 없이 __rcu 포인터에 직접 쓰기 */
/*   - 잘못된 컨텍스트에서 __rcu 포인터 접근 */
💡

RCU 사용 체크리스트: (1) reader 측에서 sleep이 필요하면 SRCU를 사용하세요. (2) RCU 보호 포인터는 반드시 rcu_dereference() 계열 매크로로 접근하세요. (3) 포인터 갱신은 반드시 rcu_assign_pointer()를 사용하세요. (4) 모든 RCU 보호 포인터에 __rcu sparse 어노테이션을 붙이고 make C=1로 정적 분석을 수행하세요. (5) CONFIG_PROVE_RCU=y를 활성화하여 런타임에 잘못된 RCU 사용을 탐지하세요.

우선순위 역전 (Priority Inversion) 실제 사례

우선순위 역전은 높은 우선순위의 태스크가 낮은 우선순위 태스크가 보유한 잠금을 기다리는 동안, 중간 우선순위 태스크가 낮은 우선순위 태스크의 실행을 선점하여 간접적으로 높은 우선순위 태스크를 무기한 차단하는 현상입니다. 이는 실시간 시스템에서 치명적인 deadline miss를 유발합니다.

우선순위 역전 시나리오: 낮은 우선순위 태스크 L이 mutex를 보유한 상태에서, 높은 우선순위 태스크 H가 같은 mutex를 요청합니다. H는 L이 mutex를 해제할 때까지 대기하지만, 중간 우선순위 태스크 M이 L을 선점하여 L의 실행을 지연시킵니다. 결과적으로 H는 M보다 낮은 우선순위로 실행되는 것과 같은 효과가 발생합니다. Mars Pathfinder (1997)의 시스템 리셋이 이 문제로 발생한 대표적 사례입니다.

/* 일반 mutex: 우선순위 역전 가능 */
DEFINE_MUTEX(shared_resource);

/* 태스크 L (낮은 우선순위, nice=19) */
mutex_lock(&shared_resource);
/* ... 긴 작업 수행 중 ... */
/* 태스크 M (중간 우선순위)이 L을 선점! */
/* 태스크 H (RT 우선순위)가 mutex 대기 — 무기한 지연됨 */
mutex_unlock(&shared_resource);

/* rt_mutex: 우선순위 상속(Priority Inheritance)으로 역전 방지 */
#include <linux/rtmutex.h>

DEFINE_RT_MUTEX(rt_shared_resource);

/* 태스크 L이 rt_mutex를 보유하고 있을 때 */
/* 태스크 H가 rt_mutex를 요청하면: */
/*   → L의 우선순위가 H의 우선순위로 일시적 상승 */
/*   → M이 L을 선점할 수 없음 */
/*   → L이 빠르게 critical section 완료 후 mutex 해제 */
/*   → L의 우선순위 원래대로 복구, H가 진행 */

rt_mutex_lock(&rt_shared_resource);
/* critical section — L의 우선순위가 대기자 중 최고로 상승됨 */
rt_mutex_unlock(&rt_shared_resource);

PREEMPT_RT 패치셋(RT 커널)에서는 커널의 동기화 동작이 근본적으로 변경됩니다. 일반 spinlock_t가 내부적으로 rt_mutex 기반의 sleeping lock으로 변환되어, 우선순위 상속이 자동으로 적용됩니다. 이는 실시간 응답성을 크게 향상시키지만 기존 코드에 영향을 미칩니다.

/* PREEMPT_RT에서의 spinlock 변환 */

/* 일반 커널: spinlock_t = raw spinlock (busy-wait, preempt 비활성화) */
/* PREEMPT_RT: spinlock_t = rt_mutex 기반 sleeping lock */

/* 따라서 PREEMPT_RT에서 spinlock_t는: */
/*   - sleep 가능 (우선순위 상속 적용) */
/*   - 인터럽트 컨텍스트에서 사용 불가! */
/*   - preemption을 비활성화하지 않음 */

/* 진짜 busy-wait이 필요한 경우 raw_spinlock_t 사용 */
static DEFINE_RAW_SPINLOCK(hw_lock);

/* raw_spinlock_t는 PREEMPT_RT에서도 진짜 spinlock */
/* 하드웨어 레지스터 접근, 인터럽트 핸들러 등에서 사용 */
raw_spin_lock_irqsave(&hw_lock, flags);
/* 하드웨어 레지스터 접근 — 매우 짧은 critical section */
raw_spin_unlock_irqrestore(&hw_lock, flags);

/* PREEMPT_RT 호환 코드 작성 가이드라인: */
/*   1. spinlock_t: 일반적인 커널 자료구조 보호 (RT에서 sleep 가능) */
/*   2. raw_spinlock_t: 하드웨어, 스케줄러, 타이머 등 핵심 경로만 */
/*   3. local_lock_t: per-CPU 데이터 보호 (RT 호환) */
/*   4. spin_lock 보유 중 sleep 가능 함수 호출이 RT에서 허용됨 */
/*      (단, raw_spin_lock 보유 중에는 여전히 불가) */
💡

실시간 시스템 개발 시 주의점: (1) 실시간 태스크 간 공유 자원은 반드시 rt_mutex 또는 PREEMPT_RT 환경의 spinlock_t(자동 PI 적용)를 사용하세요. (2) raw_spinlock_t는 critical section이 수 마이크로초 이하인 경우에만 사용하며, 절대로 긴 작업에 사용하지 마세요. (3) PREEMPT_RT 커널에서는 spin_lock()이 sleep할 수 있으므로, 인터럽트 핸들러에서는 raw_spin_lock()만 사용하세요. (4) cyclictest, rt-tests 도구로 latency를 측정하여 우선순위 역전이 발생하지 않는지 검증하세요. (5) /proc/sys/kernel/sched_rt_runtime_ussched_rt_period_us로 RT 스로틀링 정책을 조정하세요.

동기화 관련 주요 취약점 사례

동기화 메커니즘의 결함은 데이터 레이스, 데드락, Use-After-Free 등 다양한 형태로 나타나며, 재현이 어렵고 탐지가 늦어 오랜 기간 잠복하는 특성이 있습니다. 커널에서 실제로 발생한 주요 동기화 버그 사례를 분석합니다.

futex 서브시스템 취약점

CVE-2014-3153 (Towelroot) — futex requeue Priority Inheritance UAF (CVSS 7.8):

futex_requeue()에서 PI(Priority Inheritance) futex의 waiter를 non-PI futex로 requeue할 때, rt_mutex 소유권 전이 과정에서 Use-After-Free가 발생합니다. Android 루팅 도구 "Towelroot"로 악용되어 2014년 대부분의 Android 기기에 영향을 미쳤습니다.

/* CVE-2014-3153: futex PI requeue 결함 */

/*
 * futex_requeue()의 정상 동작:
 *   futex A에서 대기 중인 waiter를 futex B로 이동
 *
 * 취약점:
 *   1. FUTEX_CMP_REQUEUE로 PI futex waiter를 non-PI futex로 이동
 *   2. rt_mutex의 top_waiter 변경 → 이전 waiter의 task_struct 참조 유지
 *   3. 이전 waiter가 종료되어 task_struct 해제
 *   4. rt_mutex가 해제된 task_struct 접근 → UAF
 *
 * 수정: PI와 non-PI futex 간 requeue를 명시적으로 금지
 *       FUTEX_CMP_REQUEUE_PI만 PI futex 간 requeue 허용
 */

/* kernel/futex.c — 수정 코드 */
static int futex_requeue(..., int requeue_pi) {
    /* PI futex → non-PI futex requeue 차단 */
    if (requeue_pi) {
        /* FUTEX_CMP_REQUEUE_PI: 양쪽 모두 PI여야 함 */
        if (!futex_cmpxchg_enabled)
            return -ENOSYS;
    }
    ...
}
CVE-2021-22555 — Netfilter 스택 버퍼 OOB 쓰기 (잠금 우회):

Netfilter의 xt_compat_target_from_user()에서 스택 버퍼 범위 밖 쓰기가 가능합니다. 이 취약점 자체는 동기화 문제가 아니지만, 익스플로잇 과정에서 msg_msg 구조체의 잠금 메커니즘을 조작하여 커널 힙 레이아웃을 제어하는 기법이 사용됩니다. 동기화 프리미티브가 악용될 수 있음을 보여주는 사례입니다.

RCU 관련 버그 패턴

RCU 사용 시 반복되는 버그 패턴:

1. Grace Period 이전 해제: kfree()를 직접 호출하는 대신 kfree_rcu()call_rcu()를 사용해야 합니다. RCU read-side critical section에서 접근 중인 객체를 즉시 해제하면 UAF 발생
2. rcu_dereference() 누락: RCU로 보호되는 포인터를 직접 역참조하면 컴파일러 최적화에 의해 불완전한 데이터를 읽을 수 있음. 반드시 rcu_dereference() 사용
3. RCU read-side에서 sleep: 일반 RCU(rcu_read_lock()) 내에서는 sleep 불가. sleep이 필요하면 srcu_read_lock()(SRCU) 사용
4. RCU Stall: RCU 콜백이 장기간 실행되거나 read-side critical section이 지나치게 길면 RCU stall 발생 → softlockup이나 시스템 정지

/* RCU 올바른 사용 vs 잘못된 사용 */

/* 잘못된 패턴: 즉시 해제 → UAF 가능 */
spin_lock(&list_lock);
list_del_rcu(&entry->list);
spin_unlock(&list_lock);
kfree(entry);  /* BUG: 다른 CPU의 rcu_read_lock() 구간에서 접근 중일 수 있음 */

/* 올바른 패턴: RCU grace period 대기 후 해제 */
spin_lock(&list_lock);
list_del_rcu(&entry->list);
spin_unlock(&list_lock);
kfree_rcu(entry, rcu_head);  /* grace period 후 자동 해제 */

/* 또는 명시적 콜백 */
call_rcu(&entry->rcu_head, my_rcu_free_callback);

/* 포인터 역참조: rcu_dereference() 필수 */
rcu_read_lock();
p = rcu_dereference(global_ptr);  /* 올바른 역참조 */
/* p = global_ptr;  ← BUG: 컴파일러 최적화로 불완전한 데이터 읽기 가능 */
if (p)
    do_something(p);
rcu_read_unlock();

Lockdep이 탐지한 실제 커널 버그들

lockdep의 실전 활용:

lockdep은 커널 개발에서 가장 강력한 동기화 버그 탐지 도구입니다. 실제로 lockdep이 발견한 주요 버그 패턴:

inode lock + mmap_lock 순서 역전: 파일시스템 코드에서 inode lock → mmap_lock 순서로 획득하는 경로와, 페이지 폴트에서 mmap_lock → inode lock 순서로 획득하는 경로가 공존하여 ABBA 데드락 발생 (ext4, XFS 등에서 반복 발견)
IRQ-safe / IRQ-unsafe 혼용: 같은 lock을 프로세스 컨텍스트에서 spin_lock()으로, IRQ 핸들러에서 spin_lock()으로 사용하여 데드락 → lockdep이 경고
nested lock 미표기: 같은 유형의 lock을 여러 개 동시에 잡을 때 spin_lock_nested()를 사용하지 않으면 lockdep이 false positive 경고 → lockdep_set_class()로 해결

/* lockdep 활성화 및 디버깅 */
CONFIG_LOCKDEP=y
CONFIG_PROVE_LOCKING=y       /* 잠금 순서 검증 */
CONFIG_LOCK_STAT=y           /* 잠금 통계 수집 */
CONFIG_DEBUG_LOCK_ALLOC=y    /* 잠금 할당 디버깅 */

/* lockdep 경고 예시 */
/*
 * ======================================================
 * WARNING: possible circular locking dependency detected
 * ------------------------------------------------------
 * task/1234 is trying to acquire lock:
 *  (&inode->i_rwsem){++++}-{3:3}, at: ext4_file_write_iter
 *
 * but task already holds lock:
 *  (&mm->mmap_lock){++++}-{3:3}, at: do_page_fault
 *
 * Chain: mmap_lock -> i_rwsem (ABBA with i_rwsem -> mmap_lock)
 * ======================================================
 */