Completion (완료 변수)

커널의 일회성 완료 신호 동기화 프리미티브인 completion을 분석합니다. struct completion의 done 카운터와 swait_queue_head 내부 구조, 초기화·대기·완료 신호 API 패밀리, complete() 단일 웨이크업과 complete_all() 브로드캐스트의 차이, 타임아웃·인터럽트(Interrupt) 가능·killable 대기 변형, reinit_completion 재사용 패턴, 모듈 로딩·펌웨어(Firmware) 요청·워커 스레드(Thread)·디바이스 프로빙 동기화 실전 사용 사례, PREEMPT_RT 영향, 안티패턴과 디버깅(Debugging)까지 포괄합니다.

전제 조건: 동기화 기법, Spinlock 문서를 먼저 읽으세요. completion은 내부적으로 spinlock과 wait queue를 사용하므로, 이들의 기본 개념을 먼저 이해해야 합니다.
일상 비유: completion은 택배 알림과 같습니다. 수신자는 "배달 완료" 알림이 올 때까지 잠들어 대기하고, 택배 기사가 배달을 마치면 알림을 보냅니다. complete()는 한 명에게만 알림을 보내고, complete_all()은 대기 중인 모든 사람에게 동시 알림을 보냅니다.

핵심 요약

  • 일회성 완료 신호 — "작업이 끝났다"는 이벤트를 전달하는 가장 간결한 동기화 프리미티브입니다. semaphore보다 의도가 명확합니다.
  • 2-필드 구조struct completion { unsigned int done; struct swait_queue_head wait; }로 done 카운터와 대기 큐(Wait Queue)만으로 동작합니다.
  • done 카운터 의미 — 0이면 미완료, 양수이면 완료 횟수, UINT_MAX이면 complete_all()로 설정된 브로드캐스트 상태입니다.
  • 재사용 패턴reinit_completion()으로 done=0만 리셋합니다. init_completion()을 재사용 시 호출하면 wait queue까지 재초기화되어 대기자를 잃습니다.
  • 다양한 대기 변형 — 무한 대기, 타임아웃, interruptible, killable 4가지 변형이 있어 상황에 맞게 선택합니다.

단계별 이해

  1. 완료 신호 개념 파악
    mutex/spinlock은 상호 배제(Mutual Exclusion)를 위한 것이고, completion은 "완료 이벤트 전달"을 위한 것임을 구분합니다.
  2. done 카운터 상태 머신 이해
    done 값이 0 → N → UINT_MAX로 변화하는 과정을 추적합니다.
  3. API 패밀리 분류
    초기화(3종) · 대기(8종) · 완료 신호(2종) API를 기능별로 분류합니다.
  4. 재사용 패턴 숙지
    reinit_completion vs init_completion의 차이를 정확히 파악합니다.
  5. 실전 패턴 적용
    모듈 로딩, 펌웨어 로드, DMA 완료 등 실제 커널 사용 사례를 분석합니다.
커널 소스 위치: include/linux/completion.h, kernel/sched/completion.c. completion은 커널 v2.4.7부터 도입되었으며, v4.13에서 wait_queue가 swait_queue로 변경되었습니다.

이론적 배경: 일회성 완료 신호

동기화 프리미티브는 크게 두 가지 목적으로 분류됩니다. 상호 배제(mutual exclusion)를 위한 spinlock/mutex와 이벤트 신호(event signaling)를 위한 completion/wait queue입니다. completion은 후자의 가장 간결한 형태로, "한 실행 흐름이 다른 실행 흐름의 완료를 기다린다"는 패턴을 표현합니다.

semaphore와의 구분

역사적으로 커널은 semaphore를 completion 용도로 오용하는 경우가 많았습니다. v2.4.7에서 struct completion이 도입된 배경이 바로 이 혼용 문제입니다.

특성semaphorecompletion
의도자원 카운팅 (N개 동시 접근)일회성 완료 이벤트 전달
소유권down()한 측이 up() 해야 함 (논리적)생산자가 complete(), 소비자가 wait()
재사용자동 (카운터 증감)명시적 reinit_completion() 필요
의미 명확성범용적이라 의도 파악 어려움이름만으로 "완료 대기" 의도 전달
PREEMPT_RTsleeping lock으로 변환 가능swait 기반으로 RT에서도 예측 가능
Completion 동기화 패턴: 생산자-소비자 모델 생산자 (Producer) 작업 수행 (I/O, DMA...) complete(&done) done++ (0→1) swake_up_locked() 소비자 (Consumer) wait_for_completion() done==0 → SLEEP ... 대기 중 ... WAKE UP! done-- (1→0) 후속 작업 진행 완료 신호 전달 struct completion { done, wait }
생산자는 작업 완료 후 complete()을 호출하고, 소비자는 wait_for_completion()으로 슬립(Sleep) 대기합니다

Completion vs Wait Queue 비교

completion은 내부적으로 wait queue를 사용하지만, API 수준에서 중요한 차이가 있습니다. wait queue는 범용 대기 메커니즘이고, completion은 "완료 이벤트"에 특화된 고수준 래퍼입니다.

특성wait_queuecompletion
상태 추적조건 변수 외부 관리done 카운터 내장
경합(Contention) 조건signal-before-wait 직접 처리done 카운터가 자동 처리
깨움 대상조건 만족 시 전체/하나complete():하나, complete_all():전체
재사용조건 함수로 자연스러운 재사용reinit_completion() 명시 호출
사용 패턴복잡한 조건 대기단순 완료 이벤트
내부 구현wait_queue_head_t + func 콜백(Callback)swait_queue_head + done 정수
signal-before-wait 문제: wait queue를 사용할 때 가장 흔한 버그는 "signal이 wait보다 먼저 발생"하는 경우입니다. completion은 done 카운터가 이 문제를 자동으로 해결합니다. complete()wait_for_completion()보다 먼저 호출되어도 done이 1로 설정되어, 이후 wait_for_completion()이 즉시 반환됩니다.
signal-before-wait: wait_queue 문제 vs completion 해결 wait_queue: 신호 손실 Thread A Thread B wake_up() ← 먼저! wait_event() ← 나중! 영원히 슬립 (HANG) completion: done 카운터로 해결 Thread A Thread B complete() done=1 wait_for_completion() done==1 → 즉시 반환! 정상 동작
completion의 done 카운터는 signal-before-wait 문제를 구조적으로 해결합니다

struct completion 분석

struct completion은 커널에서 가장 간결한 동기화 자료구조 중 하나입니다. 단 두 개의 필드로 구성됩니다.

/* include/linux/completion.h */
struct completion {
    unsigned int        done;   /* 완료 카운터 */
    struct swait_queue_head  wait;   /* 대기 큐 (simple wait queue) */
};

done 필드

done은 완료 이벤트의 카운터입니다. spinlock으로 보호되며, 다음 세 가지 상태를 가집니다:

의미설정 경로
0미완료 — 대기자는 슬립초기 상태, reinit_completion()
1..UINT_MAX-1N번 완료 — N명의 대기자 깨움 가능complete() 반복 호출
UINT_MAX브로드캐스트 완료 — 모든 대기자 깨움complete_all()

wait 필드 (swait_queue_head)

커널 v4.13 이전에는 wait_queue_head_t를 사용했으나, PREEMPT_RT 호환성을 위해 swait_queue_head(simple wait queue)로 변경되었습니다. simple wait queue는 커스텀 웨이크업 함수를 지원하지 않는 대신, RT 커널에서 예측 가능한 지연(Latency) 시간을 보장합니다.

/* include/linux/swait.h */
struct swait_queue_head {
    raw_spinlock_t      lock;       /* RT에서도 진정한 spinlock */
    struct list_head     task_list;  /* 대기 태스크 리스트 */
};

struct swait_queue {
    struct task_struct   *task;      /* 대기 중인 태스크 */
    struct list_head     task_list;  /* 리스트 연결 */
};
struct completion 메모리 레이아웃 struct completion unsigned int done +0x00 (4 bytes) struct swait_queue_head wait +0x08 (lock + task_list) swait_queue_head raw_spinlock_t lock struct list_head task_list swait_queue task → Thread A swait_queue task → Thread B done 카운터 상태 0 = 미완료 N = N번 완료 UINT_MAX = complete_all()
struct completion은 done 카운터와 simple wait queue 2개 필드로 구성됩니다

초기화 API

completion은 세 가지 방식으로 초기화할 수 있습니다. 용도에 따라 정적/동적/스택 기반을 선택합니다.

DECLARE_COMPLETION — 정적 초기화

/* 전역/정적 completion 선언 */
#define DECLARE_COMPLETION(work) \
    struct completion work = COMPLETION_INITIALIZER(work)

#define COMPLETION_INITIALIZER(work) \
    { 0, __SWAIT_QUEUE_HEAD_INITIALIZER((work).wait) }

/* 사용 예 */
static DECLARE_COMPLETION(firmware_done);   /* done=0, wait 초기화 */

init_completion — 동적 초기화

/* 동적 할당된 구조체에 포함된 completion 초기화 */
static inline void init_completion(struct completion *x)
{
    x->done = 0;
    init_swait_queue_head(&x->wait);   /* lock + task_list 모두 초기화 */
}

/* 사용 예 */
struct my_device {
    struct completion    probe_done;
    /* ... */
};

struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
init_completion(&dev->probe_done);

DECLARE_COMPLETION_ONSTACK — 스택 기반 초기화

/* 함수 스코프 내 스택 할당 completion */
#ifdef CONFIG_LOCKDEP
#define DECLARE_COMPLETION_ONSTACK(work) \
    struct completion work = COMPLETION_INITIALIZER_ONSTACK(work)
#else
#define DECLARE_COMPLETION_ONSTACK(work) \
    DECLARE_COMPLETION(work)
#endif
주의: 스택 기반 completion은 해당 함수가 반환되기 전에 모든 대기자가 완료되어야 합니다. 함수 반환 후 스택이 해제되면 struct completion이 유효하지 않게 되어 use-after-free가 발생합니다.

대기 API (wait_for_completion 패밀리)

대기 API는 done 카운터가 0보다 클 때까지 호출자를 슬립시킵니다. 4가지 축(타임아웃/무한, interruptible/killable/uninterruptible)의 조합으로 8종의 변형이 제공됩니다.

함수타임아웃시그널(Signal)반환값
wait_for_completion()무한무시void
wait_for_completion_timeout()jiffies무시남은 jiffies (0=타임아웃)
wait_for_completion_interruptible()무한모든 시그널0 또는 -ERESTARTSYS
wait_for_completion_interruptible_timeout()jiffies모든 시그널남은 jiffies, 0, -ERESTARTSYS
wait_for_completion_killable()무한SIGKILL만0 또는 -ERESTARTSYS
wait_for_completion_killable_timeout()jiffiesSIGKILL만남은 jiffies, 0, -ERESTARTSYS
wait_for_completion_state()무한state 지정0 또는 -ERESTARTSYS
try_wait_for_completion()즉시N/Atrue(완료)/false(미완료)
/* kernel/sched/completion.c — wait_for_completion 핵심 구현 */
static long __sched
do_wait_for_common(struct completion *x,
                   long (*action)(long),
                   long timeout, int state)
{
    if (!x->done) {                          /* 아직 미완료 */
        DECLARE_SWAITQUEUE(wait);
        do {
            if (signal_pending_state(state, current)) {
                timeout = -ERESTARTSYS;      /* 시그널 수신 */
                break;
            }
            __prepare_to_swait(&x->wait, &wait);
            __set_current_state(state);
            raw_spin_unlock_irq(&x->wait.lock);
            timeout = action(timeout);       /* schedule_timeout() */
            raw_spin_lock_irq(&x->wait.lock);
        } while (!x->done && timeout);
        __finish_swait(&x->wait, &wait);
        if (!x->done)
            return timeout;
    }
    if (x->done != UINT_MAX)
        x->done--;                           /* 완료 소비: done 감소 */
    return timeout ?: 1;
}
wait_for_completion() 내부 흐름 wait_for_completion(x) raw_spin_lock_irq() done > 0? Yes done-- (UINT_MAX 제외) No __prepare_to_swait() + SLEEP schedule_timeout() done||timeout? No 반환
done이 이미 양수이면 즉시 반환(fast path), 0이면 슬립 루프(slow path)로 진입합니다

완료 신호 API (complete, complete_all)

완료 신호는 두 가지 API로 전달합니다. complete()는 단일 대기자를 깨우고, complete_all()은 모든 대기자를 브로드캐스트로 깨웁니다.

/* kernel/sched/completion.c */
void complete(struct completion *x)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&x->wait.lock, flags);

    if (x->done != UINT_MAX)
        x->done++;                          /* 카운터 1 증가 */
    swake_up_locked(&x->wait);              /* 대기자 1명 깨움 */

    raw_spin_unlock_irqrestore(&x->wait.lock, flags);
}

void complete_all(struct completion *x)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&x->wait.lock, flags);

    x->done = UINT_MAX;                     /* 브로드캐스트 상태로 설정 */
    swake_up_all_locked(&x->wait);           /* 모든 대기자 깨움 */

    raw_spin_unlock_irqrestore(&x->wait.lock, flags);
}
complete() vs complete_all() 선택 기준:
  • complete() — 1:1 생산자-소비자 패턴. 예: DMA 완료, 특정 요청의 응답 대기.
  • complete_all() — 1:N 브로드캐스트 패턴. 예: 모듈 로딩 완료를 여러 스레드에 알림, 디바이스 초기화 완료 통지.
complete() vs complete_all() 동작 비교 complete(): 1명만 깨움 Producer done++ done=1 Waiter A WAKE! Waiter B SLEEP... Waiter C SLEEP... done: 1→0 (A가 소비) B, C는 다음 complete() 필요 complete_all(): 전원 깨움 Producer UINT_MAX done=MAX Waiter A WAKE! Waiter B WAKE! Waiter C WAKE! done: UINT_MAX (감소 안 함) 이후 wait도 즉시 반환 → reinit_completion()으로 리셋 필요
complete()은 done을 1 증가시키고 1명만 깨우지만, complete_all()은 done을 UINT_MAX로 설정하여 모든 대기자를 깨웁니다

done 카운터 동작 원리

done 카운터는 completion의 핵심 상태 머신입니다. 생산자와 소비자의 호출 순서에 관계없이 정확하게 이벤트를 전달합니다.

상태 전이 규칙

연산done 변화대기자 처리예외
init_completion()→ 0wait queue 재초기화대기자 있으면 손실!
reinit_completion()→ 0wait queue 유지안전한 재사용
complete()done++ (UINT_MAX 제외)1명 깨움done==UINT_MAX이면 증가 안 함
complete_all()→ UINT_MAX전원 깨움이후 done 감소 안 함
wait_for_completion()done-- (UINT_MAX 제외)done==0이면 슬립UINT_MAX이면 감소 안 함
try_wait_for_completion()done-- (성공 시)슬립 안 함done==0이면 false 반환
/* done 카운터 상태 전이 예시 */

/* 시나리오 1: 정상 순서 (wait → complete) */
DECLARE_COMPLETION(done);        /* done=0 */
/* Thread A */ wait_for_completion(&done);  /* done=0 → SLEEP */
/* Thread B */ complete(&done);             /* done=1, A 깨움, A가 done-- → done=0 */

/* 시나리오 2: 역순 (complete → wait) */
DECLARE_COMPLETION(done);        /* done=0 */
/* Thread B */ complete(&done);             /* done=1 (대기자 없음) */
/* Thread A */ wait_for_completion(&done);  /* done=1 > 0 → done-- → done=0, 즉시 반환 */

/* 시나리오 3: N:1 패턴 (여러 complete → 여러 wait) */
complete(&done);  /* done=1 */
complete(&done);  /* done=2 */
complete(&done);  /* done=3 */
wait_for_completion(&done);  /* done=2 */
wait_for_completion(&done);  /* done=1 */
wait_for_completion(&done);  /* done=0 */

/* 시나리오 4: complete_all + 재사용 */
complete_all(&done);             /* done=UINT_MAX */
wait_for_completion(&done);      /* done=UINT_MAX (감소 안 함), 즉시 반환 */
wait_for_completion(&done);      /* done=UINT_MAX, 즉시 반환 */
reinit_completion(&done);        /* done=0, 다시 사용 가능 */
done 카운터 상태 전이 다이어그램 done = 0 미완료 done = N 완료 (N회) done = UINT_MAX 브로드캐스트 complete() wait (done--→0) complete() → N+1 complete_all() complete_all() reinit_completion() reinit_completion()
done 카운터는 complete()로 증가, wait로 감소, complete_all()로 UINT_MAX 설정, reinit_completion()으로 0 리셋됩니다

타임아웃 처리 패턴

타임아웃 변형은 무한 대기를 방지하는 가장 기본적인 안전 장치입니다. 하드웨어 응답이 보장되지 않는 상황에서 필수입니다.

/* 패턴: 타임아웃 대기 + 에러 처리 */
int wait_for_device_ready(struct my_device *dev)
{
    unsigned long timeout;

    /* 3초 타임아웃 */
    timeout = wait_for_completion_timeout(&dev->ready,
                                          msecs_to_jiffies(3000));
    if (timeout == 0) {
        dev_err(dev->dev, "device ready timeout (3s)\n");
        return -ETIMEDOUT;
    }

    /* timeout > 0: 남은 jiffies 수 (정상 완료) */
    dev_dbg(dev->dev, "device ready (%lu jiffies remaining)\n", timeout);
    return 0;
}

/* 패턴: 재시도 + 타임아웃 조합 */
int wait_with_retry(struct completion *done, int retries)
{
    int i;
    for (i = 0; i < retries; i++) {
        unsigned long ret;
        reinit_completion(done);
        trigger_hardware_operation();
        ret = wait_for_completion_timeout(done, msecs_to_jiffies(500));
        if (ret)
            return 0;      /* 성공 */
        pr_warn("retry %d/%d\n", i + 1, retries);
    }
    return -ETIMEDOUT;
}
반환값 주의: wait_for_completion_timeout()의 반환값 0은 "타임아웃 발생"이고, 양수는 "남은 jiffies"입니다. wait_for_completion_interruptible_timeout()은 추가로 -ERESTARTSYS를 반환할 수 있으므로 signed long으로 처리해야 합니다.

인터럽트 가능 대기

사용자 공간(User Space)에서 시작된 작업(예: ioctl, read/write)은 시그널로 중단 가능해야 합니다. interruptible/killable 변형은 이 요구를 충족합니다.

interruptible vs killable

변형태스크(Task) 상태반응하는 시그널용도
_interruptibleTASK_INTERRUPTIBLE모든 시그널 (SIGINT, SIGTERM 등)사용자 공간 요청
_killableTASK_KILLABLE치명적 시그널만 (SIGKILL 등)중단 가능하되 쉽게 방해받지 않아야 할 때
(없음)TASK_UNINTERRUPTIBLE없음커널 내부 동기화 (사용자 중단 불가)
/* 패턴: interruptible 대기 + 시그널 처리 */
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct my_device *dev = filp->private_data;
    int ret;

    start_async_operation(dev);

    ret = wait_for_completion_interruptible(&dev->op_done);
    if (ret == -ERESTARTSYS) {
        /* 시그널 수신 — 진행 중인 작업 취소 */
        cancel_async_operation(dev);
        return -EINTR;
    }

    return copy_result_to_user(dev, arg);
}

/* 패턴: killable 대기 — NFS/CIFS 등 네트워크 파일시스템 */
static int nfs_wait_for_reply(struct rpc_task *task)
{
    int ret;

    /* SIGKILL만 반응 — SIGINT/SIGTERM으로 중단되지 않음 */
    ret = wait_for_completion_killable(&task->reply_done);
    if (ret == -ERESTARTSYS)
        return -ERESTARTSYS;    /* 프로세스가 강제 종료됨 */

    return 0;
}

DECLARE_COMPLETION_ONSTACK: 스택 기반 completion

함수 내에서 임시로 사용하는 completion은 스택에 할당하여 힙 할당 오버헤드(Overhead)를 피할 수 있습니다. lockdep이 활성화되면 스택 completion에 대한 추가 추적 메타데이터를 제공합니다.

/* 패턴: 동기식 커널 스레드 생성 */
int create_worker_sync(void)
{
    DECLARE_COMPLETION_ONSTACK(started);
    struct task_struct *task;

    task = kthread_run(worker_fn, &started, "my-worker");
    if (IS_ERR(task))
        return PTR_ERR(task);

    /* 워커가 초기화를 마칠 때까지 대기 */
    wait_for_completion(&started);
    /* 여기서 함수 반환 → started는 스택에서 해제 */
    /* 안전: 워커는 complete() 이후 started를 참조하지 않음 */
    return 0;
}

static int worker_fn(void *data)
{
    struct completion *started = data;

    /* 초기화 작업 수행 */
    do_initialization();

    /* 생성자에게 초기화 완료 알림 */
    complete(started);
    /* 주의: 이후 started 포인터를 참조하면 안 됨! */

    /* 워커 메인 루프 */
    while (!kthread_should_stop()) {
        /* ... */
    }
    return 0;
}
스택 completion 규칙: complete()를 호출한 측은 그 이후 completion 포인터를 절대 참조하면 안 됩니다. 호출자 함수가 반환되면 스택이 해제되어 포인터가 무효화(Invalidation)됩니다. 이 규칙을 어기면 use-after-free 취약점(Vulnerability)이 발생합니다.

reinit_completion: 재사용 패턴

completion을 반복 사용하려면 done 카운터를 0으로 리셋해야 합니다. 이때 반드시 reinit_completion()을 사용하고, init_completion()은 사용하면 안 됩니다.

/* include/linux/completion.h */
static inline void reinit_completion(struct completion *x)
{
    x->done = 0;    /* done만 리셋, wait queue는 그대로 유지 */
}

/* 위험: init_completion()으로 재초기화하면 wait queue가 리셋됨 */
static inline void init_completion(struct completion *x)
{
    x->done = 0;
    init_swait_queue_head(&x->wait);   /* ← 대기 중인 태스크 리스트 손실! */
}

올바른 재사용 패턴

/* 패턴: 주기적 하드웨어 폴링 + completion 재사용 */
void periodic_hw_check(struct my_device *dev)
{
    while (!dev->shutdown) {
        /* 1. done=0으로 리셋 (wait queue 유지) */
        reinit_completion(&dev->hw_done);

        /* 2. 하드웨어에 명령 전송 */
        trigger_hw_command(dev);

        /* 3. 완료 대기 (인터럽트 핸들러가 complete() 호출) */
        wait_for_completion_timeout(&dev->hw_done,
                                    msecs_to_jiffies(1000));

        /* 4. 결과 처리 */
        process_hw_result(dev);
    }
}
reinit_completion() vs init_completion() 재사용 비교 reinit_completion() (올바름) done = UINT_MAX reinit: done=0 wait queue 유지 → 대기자 안전 정상 재사용 가능 init_completion() (위험!) done = UINT_MAX init: done=0 + wait 리셋 wait queue 재초기화 → 대기자 손실! 영원히 슬립하는 태스크 발생
재사용 시 reinit_completion()은 done만 리셋하여 안전하고, init_completion()은 wait queue까지 리셋하여 대기자를 잃습니다

모듈 로딩 동기화 패턴

커널 모듈(Kernel Module) 로딩은 completion의 대표적 사용 사례입니다. request_module()은 동기식으로 동작하지만, 내부적으로 usermode helper를 비동기 실행하고 completion으로 완료를 대기합니다.

/* kernel/module/kmod.c — request_module 내부 */
int __request_module(bool wait, const char *fmt, ...)
{
    DECLARE_COMPLETION_ONSTACK(done);
    struct subprocess_info *info;

    /* usermode helper 정보 설정 */
    info = call_usermodehelper_setup(modprobe_path, argv, envp,
                                     GFP_KERNEL, NULL, NULL, &done);
    if (!info)
        return -ENOMEM;

    /* modprobe 실행 + 완료 대기 */
    ret = call_usermodehelper_exec(info, wait ? UMH_WAIT_PROC : UMH_NO_WAIT);
    /* wait==true: completion으로 modprobe 종료 대기 */
    return ret;
}

/* 사용 예: 네트워크 프로토콜 자동 로드 */
if (!net_proto_family[family])
    request_module("net-pf-%d", family);  /* modprobe 완료까지 대기 */

모듈 init 완료 대기

/* kernel/module/main.c — 모듈 초기화 완료 추적 */
struct module {
    /* ... */
    struct completion *init_done;   /* init 함수 완료 추적 */
    /* ... */
};

/* load_module() 내부 */
static int load_module(struct load_info *info, const char __user *uargs, int flags)
{
    DECLARE_COMPLETION_ONSTACK(done);
    /* ... */
    mod->init_done = &done;

    /* 모듈의 init 함수를 별도 스레드에서 실행 */
    ret = do_init_module(mod);
    if (ret)
        return ret;

    /* init 완료 대기 */
    wait_for_completion(&done);
    return 0;
}

펌웨어 요청 완료 대기

펌웨어 로딩 프레임워크는 completion을 활용하여 비동기 펌웨어 요청의 완료를 추적합니다.

/* drivers/base/firmware_loader/main.c */
struct fw_priv {
    struct completion    completion;     /* 펌웨어 로드 완료 */
    struct firmware      *fw;
    enum fw_status      status;
    /* ... */
};

/* 동기 펌웨어 요청 */
int request_firmware(const struct firmware **fw, const char *name,
                     struct device *dev)
{
    /* fw_priv 할당 + init_completion(&fw_priv->completion) */
    /* sysfs fallback 또는 direct fs 로드 시작 */
    /* ... */

    /* 로드 완료 대기 */
    wait_for_completion(&fw_priv->completion);
    /* 펌웨어 데이터 반환 */
    *fw = fw_priv->fw;
    return fw_priv->status == FW_STATUS_DONE ? 0 : -ENOENT;
}

/* 비동기 펌웨어 요청 (콜백 기반) */
int request_firmware_nowait(struct module *module, bool uevent,
                            const char *name, struct device *dev,
                            gfp_t gfp, void *context,
                            void (*cont)(const struct firmware *, void *))
{
    /* 워크 큐에 로드 작업 제출 */
    /* 완료 시 cont() 콜백 호출 */
    /* completion은 내부적으로 사용됨 */
}
펌웨어 로딩과 Completion 흐름 드라이버 probe() request_firmware() init_completion() wait_for_completion() ... SLEEPING ... WAKE → fw 사용 probe 계속... FW Loader 워크 kernel_read_file() /lib/firmware/xxx.bin fw_priv->status = DONE complete() Sysfs Fallback 타임아웃 (60초) userspace에서 write complete() or timeout 실패 시
드라이버는 request_firmware()로 대기하고, FW 로더(Loader)가 파일을 읽은 뒤 complete()로 알립니다

워커 스레드 동기화

커널 스레드(kthread) 생성과 종료 동기화에 completion이 광범위하게 사용됩니다.

/* 패턴 1: kthread 생성 시 초기화 완료 대기 */
struct kthread_create_info {
    int (*threadfn)(void *data);
    void            *data;
    int             node;
    struct task_struct  *result;
    struct completion   *done;      /* 생성 완료 동기화 */
};

/* kernel/kthread.c */
struct task_struct *kthread_create_on_node(
    int (*threadfn)(void *),
    void *data, int node,
    const char namefmt[], ...)
{
    DECLARE_COMPLETION_ONSTACK(done);
    struct kthread_create_info create = {
        .threadfn = threadfn,
        .data     = data,
        .node     = node,
        .done     = &done,
    };

    /* kthreadd에 요청 큐잉 */
    list_add_tail(&create.list, &kthread_create_list);
    wake_up_process(kthreadd_task);

    /* kthread가 생성될 때까지 대기 */
    wait_for_completion(&done);
    return create.result;
}

/* 패턴 2: kthread 종료 대기 */
int kthread_stop(struct task_struct *k)
{
    struct kthread *kthread = to_kthread(k);

    /* should_stop 플래그 설정 */
    set_bit(KTHREAD_SHOULD_STOP, &kthread->flags);
    wake_up_process(k);

    /* 스레드 종료 대기 */
    wait_for_completion(&kthread->exited);
    return kthread->result;
}

디바이스 프로빙 완료 대기

복잡한 SoC 환경에서 디바이스 간 의존 관계를 completion으로 동기화하는 패턴입니다.

/* 패턴: 상위 디바이스 초기화 대기 */
struct phy_provider {
    struct completion    ready;
    struct device        *dev;
    /* ... */
};

/* PHY 프로바이더 드라이버 */
static int phy_provider_probe(struct platform_device *pdev)
{
    struct phy_provider *phy = devm_kzalloc(&pdev->dev, sizeof(*phy), GFP_KERNEL);
    init_completion(&phy->ready);

    /* 하드웨어 초기화 */
    ret = phy_hw_init(phy);
    if (ret)
        return ret;

    /* 초기화 완료를 대기 중인 모든 소비자에게 알림 */
    complete_all(&phy->ready);
    return 0;
}

/* PHY 소비자 드라이버 */
static int usb_controller_probe(struct platform_device *pdev)
{
    struct phy_provider *phy = get_phy_provider(pdev);
    unsigned long timeout;

    /* PHY가 준비될 때까지 대기 (최대 5초) */
    timeout = wait_for_completion_timeout(&phy->ready,
                                          msecs_to_jiffies(5000));
    if (!timeout) {
        dev_err(&pdev->dev, "PHY not ready, deferring probe\n");
        return -EPROBE_DEFER;
    }

    /* PHY 준비 완료 → USB 컨트롤러 초기화 */
    return usb_hw_init(pdev);
}
EPROBE_DEFER와의 관계: completion 타임아웃 시 -EPROBE_DEFER를 반환하면, 커널의 deferred probe 메커니즘이 나중에 다시 probe를 시도합니다. 이 패턴은 Device Tree 기반 SoC에서 디바이스 초기화 순서가 보장되지 않을 때 유용합니다.

PREEMPT_RT 영향

PREEMPT_RT(Real-Time) 커널에서 completion의 동작은 일반 커널과 약간 다릅니다. v4.13에서 wait_queue를 swait_queue로 변경한 것이 RT 호환성의 핵심입니다.

swait_queue vs wait_queue (RT 관점)

특성wait_queue_head_tswait_queue_head
보호 락spinlock_t (RT에서 sleeping lock)raw_spinlock_t (RT에서도 busy-wait)
커스텀 웨이크업지원 (func 콜백)미지원
exclusive 웨이크업지원미지원 (항상 exclusive)
RT 지연 시간예측 불가 (sleeping lock 대기)예측 가능 (busy-wait 최소화)
메모리 크기더 큼 (func + flags 포인터)더 작음 (task 포인터만)
/* RT 커널에서 completion 사용 시 주의사항 */

/* 1. raw_spinlock으로 보호되므로 hardirq 컨텍스트에서도 complete() 가능 */
irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_device *dev = data;
    complete(&dev->irq_done);   /* RT에서도 안전 */
    return IRQ_HANDLED;
}

/* 2. wait_for_completion()은 schedule()을 호출하므로 */
/*    atomic 컨텍스트에서 사용 불가 (RT/non-RT 모두) */

/* 3. try_wait_for_completion()은 atomic 컨텍스트에서 사용 가능 */
bool done = try_wait_for_completion(&dev->irq_done);
PREEMPT_RT: completion 내부 락 구조 일반 커널 (Non-RT) swait_queue_head.lock = raw_spinlock_t (진정한 spinlock) IRQ 비활성화 + busy-wait 선점 불가 구간 (짧음) PREEMPT_RT 커널 swait_queue_head.lock = raw_spinlock_t (RT에서도 동일!) IRQ 비활성화 + busy-wait (동일) 예측 가능한 최소 지연
completion은 raw_spinlock_t 기반 swait_queue를 사용하여 PREEMPT_RT에서도 동일한 동작을 보장합니다

실전 사용 패턴

커널 소스에서 흔히 발견되는 completion 사용 패턴을 분류합니다.

패턴 1: 일회성 이벤트 (One-Shot)

/* DMA 전송 완료 대기 */
struct dma_chan_info {
    struct completion    dma_done;
    dma_cookie_t        cookie;
};

void start_dma_transfer(struct dma_chan_info *ch)
{
    init_completion(&ch->dma_done);
    ch->cookie = dmaengine_submit(desc);
    dma_async_issue_pending(ch->chan);
}

int wait_dma_transfer(struct dma_chan_info *ch)
{
    unsigned long timeout;
    timeout = wait_for_completion_timeout(&ch->dma_done,
                                          msecs_to_jiffies(5000));
    return timeout ? 0 : -ETIMEDOUT;
}

/* DMA 인터럽트 핸들러에서 */
void dma_irq_callback(void *data)
{
    struct dma_chan_info *ch = data;
    complete(&ch->dma_done);
}

패턴 2: 브로드캐스트 (Barrier)

/* 서브시스템 초기화 완료를 여러 소비자에게 알림 */
static DECLARE_COMPLETION(subsys_ready);

/* 서브시스템 드라이버 */
static int __init subsystem_init(void)
{
    /* 초기화 작업 */
    setup_subsystem();

    /* 모든 대기자에게 알림 */
    complete_all(&subsys_ready);
    return 0;
}
subsys_initcall(subsystem_init);

/* 소비자 A */
static int consumer_a_probe(struct platform_device *pdev)
{
    wait_for_completion(&subsys_ready);  /* 즉시 반환 (UINT_MAX) */
    /* ... */
}

/* 소비자 B */
static int consumer_b_probe(struct platform_device *pdev)
{
    wait_for_completion(&subsys_ready);  /* 즉시 반환 (UINT_MAX) */
    /* ... */
}

패턴 3: 주기적 재사용 (Cyclic)

/* I2C 전송 + completion 재사용 */
int i2c_transfer_sync(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
    int i, ret;
    for (i = 0; i < num; i++) {
        reinit_completion(&adap->xfer_done);        /* 재사용: done=0 */
        adap->hw_start_xfer(&msgs[i]);              /* HW 전송 시작 */
        ret = wait_for_completion_timeout(
            &adap->xfer_done, adap->timeout);
        if (!ret)
            return -ETIMEDOUT;
    }
    return num;
}

패턴 4: 비동기 Crypto 완료

/* crypto API 비동기 완료 대기 패턴 */
struct crypto_wait {
    struct completion    completion;
    int                 err;
};

static void crypto_req_done(void *data, int err)
{
    struct crypto_wait *wait = data;
    if (err == -EINPROGRESS)
        return;
    wait->err = err;
    complete(&wait->completion);
}

int do_encrypt(struct skcipher_request *req)
{
    struct crypto_wait wait;

    init_completion(&wait.completion);
    skcipher_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG,
                                  crypto_req_done, &wait);

    int err = crypto_skcipher_encrypt(req);
    if (err == -EINPROGRESS || err == -EBUSY) {
        wait_for_completion(&wait.completion);
        err = wait.err;
    }
    return err;
}

안티패턴

completion 사용 시 흔히 발생하는 실수 패턴입니다.

안티패턴 1: init_completion으로 재초기화

/* 잘못된 코드 */
while (running) {
    init_completion(&dev->done);     /* BUG: wait queue 리셋! */
    start_operation(dev);
    wait_for_completion(&dev->done);
}

/* 올바른 코드 */
while (running) {
    reinit_completion(&dev->done);   /* done만 0으로 리셋 */
    start_operation(dev);
    wait_for_completion(&dev->done);
}

안티패턴 2: complete_all 후 reinit 없이 재사용

/* 잘못된 코드 */
complete_all(&dev->ready);           /* done = UINT_MAX */
/* ... 나중에 ... */
wait_for_completion(&dev->ready);    /* 항상 즉시 반환! (done이 여전히 UINT_MAX) */

/* 올바른 코드 */
complete_all(&dev->ready);
/* ... 재사용 전 ... */
reinit_completion(&dev->ready);      /* done = 0으로 리셋 */
wait_for_completion(&dev->ready);    /* 정상 대기 */

안티패턴 3: 스택 completion의 수명 문제

/* 잘못된 코드 */
void start_async_work(void)
{
    DECLARE_COMPLETION_ONSTACK(done);
    submit_work(&done);  /* 워커에 completion 포인터 전달 */
    /* 함수 반환! → done은 스택에서 해제됨 */
    /* 워커가 나중에 complete(&done) 호출 → USE-AFTER-FREE! */
}

/* 올바른 코드 */
void start_async_work(void)
{
    DECLARE_COMPLETION_ONSTACK(done);
    submit_work(&done);
    wait_for_completion(&done);  /* 완료될 때까지 함수가 반환하지 않음 */
}

안티패턴 4: atomic 컨텍스트에서 wait_for_completion

/* 잘못된 코드 */
spin_lock(&dev->lock);
wait_for_completion(&dev->done);  /* BUG: schedule() 호출 → sleeping in atomic! */
spin_unlock(&dev->lock);

/* 올바른 코드 — try_wait 사용 */
spin_lock(&dev->lock);
if (try_wait_for_completion(&dev->done)) {
    /* 이미 완료됨 */
} else {
    /* 미완료 — 나중에 처리 */
}
spin_unlock(&dev->lock);

/* 또는 — 락 밖에서 wait */
spin_unlock(&dev->lock);
wait_for_completion(&dev->done);
spin_lock(&dev->lock);
Completion 안티패턴 4대 위험 init으로 재초기화 wait queue 리셋 → 대기자 영구 슬립 reinit_completion() complete_all 후 재사용 done=UINT_MAX 유지 → wait 항상 즉시 반환 reinit 후 재사용 스택 수명 초과 함수 반환 후 참조 → use-after-free 반환 전 wait 필수 atomic에서 wait spinlock 내부 sleep → BUG/deadlock try_wait 또는 락 밖 핵심 원칙 재사용 → reinit_completion() | 스택 → 반환 전 대기 완료 | atomic → try_wait만 사용 complete_all() 후 → 반드시 reinit 후 재사용
4가지 안티패턴과 각각의 올바른 해결책

디버깅

completion 관련 문제를 진단하는 방법입니다.

hung task 탐지

/* CONFIG_DETECT_HUNG_TASK 활성화 시 */
/* TASK_UNINTERRUPTIBLE 상태로 120초(기본값) 이상 슬립하면 경고 */

/* 커널 로그 예시 */
/*
INFO: task kworker/0:1:42 blocked for more than 120 seconds.
      Tainted: G           OE  6.x.y
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:kworker/0:1     state:D stack:12288 pid:42 ppid:2
Call Trace:
 __schedule+0x2d2/0x830
 schedule+0x5e/0xd0
 schedule_timeout+0x118/0x150
 wait_for_completion+0x82/0x120       ← completion 대기 중
 request_firmware+0x4a/0x80
 my_driver_probe+0x123/0x200
*/

/* /proc/sys/kernel/hung_task_timeout_secs 조정 */
echo 30 > /proc/sys/kernel/hung_task_timeout_secs  /* 30초로 단축 */

ftrace로 completion 추적

/* completion 관련 함수 추적 */
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_process_wait/enable
echo 'complete*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'wait_for_completion*' >> /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on

/* 추적 결과 확인 */
cat /sys/kernel/debug/tracing/trace
/*
  kworker/0:1-42  [000] d..1  123.456: wait_for_completion <-request_firmware
  irq/18-my_dev-99 [001] d.h1  123.789: complete <-my_irq_handler
*/

lockdep 활용

/* completion의 내부 raw_spinlock에 대한 lockdep 검증 */
/* CONFIG_PROVE_LOCKING + CONFIG_DEBUG_LOCK_ALLOC 활성화 */

/* DECLARE_COMPLETION_ONSTACK은 lockdep 메타데이터를 추가하여 */
/* 스택 completion의 수명 문제를 탐지할 수 있음 */

/* lockdep 경고 예시 */
/*
WARNING: possible circular locking dependency detected
...
 -> #1 (&dev->done){+.+.}:
       wait_for_completion+0x82/0x120
       my_function+0x45/0x80
 -> #0 (&dev->lock){....}:
       complete+0x15/0x60
       my_irq_handler+0x30/0x50
*/

SysRq로 대기 상태 확인

/* Alt+SysRq+W: 블로킹된 태스크 출력 */
echo w > /proc/sysrq-trigger

/* 출력에서 wait_for_completion 스택 확인 */
/*
task:my_thread state:D ...
Call Trace:
 wait_for_completion_timeout+0x90/0x130
 my_driver_do_something+0x78/0x100
*/

/* Alt+SysRq+T: 전체 태스크 목록 */
echo t > /proc/sysrq-trigger

커널 설정

completion 자체는 별도 CONFIG 옵션이 없지만, 디버깅과 동작에 영향을 주는 관련 설정이 있습니다.

CONFIG 옵션영향설명
CONFIG_PREEMPT_RTswait_queue 동작RT 커널에서 raw_spinlock 유지 (sleeping lock 아님)
CONFIG_LOCKDEPDECLARE_COMPLETION_ONSTACK스택 completion에 lockdep 메타데이터 추가
CONFIG_PROVE_LOCKING순환 의존성 탐지completion 내부 락의 의존성 그래프 검증
CONFIG_DEBUG_LOCK_ALLOC락 할당 추적completion 내부 raw_spinlock 할당/해제 추적
CONFIG_DETECT_HUNG_TASK무한 대기 탐지TASK_UNINTERRUPTIBLE 장기 슬립 경고
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT타임아웃 기본값hung task 탐지 시간 (기본 120초)
CONFIG_FTRACE함수 추적complete/wait_for_completion 호출 추적(Call Trace)
CONFIG_SCHED_DEBUG스케줄러(Scheduler) 디버깅/proc/sched_debug에서 대기 태스크 확인

wait_for_completion 소스 분석

wait_for_completion()은 가장 단순한 형태의 대기 API이지만, 내부적으로 do_wait_for_common()이라는 핵심 루프를 거칩니다. 이 섹션에서는 커널 소스를 한 줄씩 추적하며 done 카운터 확인부터 schedule_timeout까지의 전체 경로를 분석합니다.

진입 경로: wait_for_completion → do_wait_for_common

/* kernel/sched/completion.c */

void __sched wait_for_completion(struct completion *x)
{
    wait_for_common(x, MAX_SCHEDULE_TIMEOUT, TASK_UNINTERRUPTIBLE);
}
EXPORT_SYMBOL(wait_for_completion);

static long __sched
wait_for_common(struct completion *x, long timeout, int state)
{
    might_sleep();  /* atomic 컨텍스트 검사 */

    complete_acquire(x);  /* lockdep: 의존성 기록 */

    raw_spin_lock_irq(&x->wait.lock);
    timeout = do_wait_for_common(x, action, timeout, state);
    raw_spin_unlock_irq(&x->wait.lock);

    complete_release(x);  /* lockdep: 해제 기록 */

    return timeout;
}
참고: might_sleep()CONFIG_DEBUG_ATOMIC_SLEEP 활성화 시 spinlock이나 인터럽트 핸들러(Handler) 내부에서 호출되면 경고를 출력합니다. completion 대기는 반드시 프로세스(Process) 컨텍스트에서만 사용해야 합니다.

핵심 루프: do_wait_for_common

/* kernel/sched/completion.c — 핵심 대기 루프 */

static inline long __sched
do_wait_for_common(struct completion *x,
                   long (*action)(long),
                   long timeout, int state)
{
    if (!x->done) {  /* ① fast path: 이미 완료된 경우 즉시 반환 */
        DECLARE_SWAITQUEUE(wait);

        do {
            if (signal_pending_state(state, current)) {
                timeout = -ERESTARTSYS;
                break;  /* ② 시그널 수신 → 에러 반환 */
            }
            __prepare_to_swait(&x->wait, &wait);
            __set_current_state(state);
            raw_spin_unlock_irq(&x->wait.lock);

            timeout = action(timeout);  /* ③ schedule_timeout 호출 */

            raw_spin_lock_irq(&x->wait.lock);
        } while (!x->done && timeout);  /* ④ done 확인 + 타임아웃 확인 */
        __finish_swait(&x->wait, &wait);
        if (!x->done)
            return timeout;  /* 타임아웃 또는 시그널로 종료 */
    }
    if (x->done != UINT_MAX)
        x->done--;  /* ⑤ 일반 완료: done 소비 */
    return timeout ?: 1;  /* 0이면 1로 보정 (성공 표시) */
}
주의: do_wait_for_common은 raw_spin_lock을 획득한 상태에서 진입합니다. 루프 내부에서 schedule_timeout 호출 전에 반드시 unlock하고, 재개 후 다시 lock합니다. 이 unlock-schedule-lock 패턴이 done 카운터의 원자성을 보장합니다.

action 콜백과 schedule_timeout

/* 기본 action: schedule_timeout_uninterruptible 등 */
static long __sched
wait_for_common(struct completion *x, long timeout, int state)
{
    /* action 함수 선택: */
    /* TASK_UNINTERRUPTIBLE → schedule_timeout */
    /* TASK_INTERRUPTIBLE → schedule_timeout (시그널 체크 포함) */
    /* TASK_KILLABLE → schedule_timeout (치명적 시그널만 체크) */

    /* schedule_timeout 내부: */
    /* 1. timer 설정 (timeout != MAX_SCHEDULE_TIMEOUT인 경우) */
    /* 2. schedule() 호출 — 현재 태스크 슬립 */
    /* 3. 웨이크업 시 남은 timeout 반환 */
    /* 4. timeout==0 → 타임아웃 만료 */
}

전체 실행 흐름 단계별 분석

단계함수동작락 상태
1wait_for_completion진입, might_sleep 검사없음
2wait_for_commonraw_spin_lock_irq 획득locked
3do_wait_for_commonx->done fast path 체크locked
4__prepare_to_swait대기 큐에 현재 태스크 등록locked
5__set_current_state태스크 상태를 TASK_UNINTERRUPTIBLE로 설정locked
6raw_spin_unlock_irq락 해제, 인터럽트 복원unlocked
7schedule_timeout슬립 — 다른 태스크로 전환unlocked
8(웨이크업)complete()에 의해 재개unlocked
9raw_spin_lock_irq락 재획득locked
10루프 조건 체크x->done && timeout 확인locked
11__finish_swait대기 큐에서 제거locked
12done--done 카운터 소비 (UINT_MAX 아닌 경우)locked
wait_for_completion 내부 실행 흐름 wait_for_completion(x) might_sleep() 검사 raw_spin_lock_irq(&x→wait.lock) done != 0? YES done-- 즉시 반환 NO __prepare_to_swait + __set_current_state unlock → schedule_timeout → lock done||timeout? 재시도 완료 finish_swait 반환
do_wait_for_common의 fast path(done≠0)와 slow path(슬립 루프) 흐름

swait_event_locked_timeout 내부

v4.13 이전에는 ___wait_for_common()이 일반 wait_queue를 사용했습니다. 현재는 swait(simple wait queue)로 변경되어 웨이크업 콜백이 제거되었습니다.

/* swait vs wait_queue 핵심 차이 */

/* wait_queue (v4.12 이전): */
struct wait_queue_entry {
    unsigned int        flags;
    void                *private;   /* task_struct */
    wait_queue_func_t   func;       /* 커스텀 웨이크업 함수 */
    struct list_head     entry;
};

/* swait_queue (v4.13 이후): */
struct swait_queue {
    struct task_struct   *task;      /* 직접 참조 */
    struct list_head     task_list;  /* 웨이크업 함수 없음 */
};

/* swait의 장점: */
/* - RT 커널에서 예측 가능한 웨이크업 시간 */
/* - 메모리 사용량 감소 (콜백 포인터 제거) */
/* - 단점: 커스텀 웨이크업 불가 (completion에는 불필요) */
참고: completion이 swait를 사용하는 이유는 "단순 웨이크업만 필요"하기 때문입니다. poll/epoll이나 select처럼 커스텀 웨이크업 콜백이 필요한 경우에는 일반 wait_queue를 사용해야 합니다. completion의 용도는 "완료 신호 전달"이므로 swait로 충분합니다.

complete()/complete_all() 소스 분석

complete()complete_all()은 대기자를 깨우는 신호 함수입니다. 두 함수 모두 raw_spin_lock_irqsave로 done 카운터를 보호하며, 핵심 차이는 done 값 설정과 웨이크업 대상 수에 있습니다.

complete() 구현

/* kernel/sched/completion.c */

void complete(struct completion *x)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&x->wait.lock, flags);
                                    /* ① 인터럽트 비활성화 + 락 획득 */

    if (x->done != UINT_MAX)
        x->done++;              /* ② done 카운터 증가 (오버플로우 방지) */

    swake_up_locked(&x->wait);  /* ③ 대기 큐에서 한 명만 웨이크업 */

    raw_spin_unlock_irqrestore(&x->wait.lock, flags);
                                    /* ④ 락 해제 + 인터럽트 복원 */
}
EXPORT_SYMBOL(complete);

complete_all() 구현

/* kernel/sched/completion.c */

void complete_all(struct completion *x)
{
    unsigned long flags;

    lockdep_assert_RT_in_threaded_ctx();
                                    /* RT에서 threaded context 검증 */

    raw_spin_lock_irqsave(&x->wait.lock, flags);

    x->done = UINT_MAX;         /* ① done을 UINT_MAX로 설정 */

    swake_up_all_locked(&x->wait);
                                    /* ② 모든 대기자 웨이크업 */

    raw_spin_unlock_irqrestore(&x->wait.lock, flags);
}
EXPORT_SYMBOL(complete_all);

swake_up_locked vs swake_up_all_locked

/* include/linux/swait.h */

/* swake_up_locked: 리스트 첫 번째 대기자만 깨움 */
void swake_up_locked(struct swait_queue_head *q)
{
    struct swait_queue *curr;
    if (list_empty(&q->task_list))
        return;
    curr = list_first_entry(&q->task_list,
                             struct swait_queue, task_list);
    wake_up_process(curr->task);   /* 한 명만 */
    list_del_init(&curr->task_list);
}

/* swake_up_all_locked: 전체 리스트 순회하며 모두 깨움 */
void swake_up_all_locked(struct swait_queue_head *q)
{
    while (!list_empty(&q->task_list))
        swake_up_locked(q);         /* 반복 호출 */
}
주의: complete_all()에서 done = UINT_MAX로 설정하면, 이후 wait_for_completion() 호출은 모두 즉시 반환됩니다 (UINT_MAX는 절대 감소하지 않음). 재사용하려면 반드시 reinit_completion()을 호출해야 합니다.

raw_spin_lock_irqsave 사용 이유

complete()는 인터럽트 핸들러에서도 호출될 수 있으므로 _irqsave 변형을 사용합니다.

함수락 변형이유
complete()raw_spin_lock_irqsaveIRQ 핸들러에서 호출 가능
wait_for_common()raw_spin_lock_irq프로세스 컨텍스트 전용 (IRQ 항상 활성)
try_wait_for_completion()raw_spin_lock_irqsaveatomic 컨텍스트 허용

done 카운터 오버플로우 방지

/* complete()에서의 오버플로우 방지 */
if (x->done != UINT_MAX)
    x->done++;

/* 왜 UINT_MAX 체크가 필요한가? */
/* complete_all() 이후 complete()를 호출하면 */
/* UINT_MAX + 1 = 0 이 되어 done이 리셋됨 → 교착 상태 */
/* 따라서 UINT_MAX이면 증가하지 않음 */

/* complete() N번 호출 시 done 값 변화: */
/* 0 → 1 → 2 → ... → UINT_MAX-1 → UINT_MAX (saturation) */
/* 실제로 UINT_MAX(4294967295)번 complete()를 호출하는 */
/* 시나리오는 현실적으로 발생하지 않음 */

다중 Producer/Consumer 패턴

completion은 단순한 1:1 동기화뿐 아니라 N:1(다중 생산자, 단일 소비자), 1:N(단일 생산자, 다중 소비자), 그리고 barrier 패턴에도 활용됩니다. 이 섹션에서는 커널 내 실전 패턴을 분석합니다.

Fan-in 패턴: N producers → 1 consumer

/* N개의 워커가 각각 작업 완료를 알리는 패턴 */
struct batch_work {
    struct completion    done;
    atomic_t            remaining;
    int                 result;
};

static void worker_func(struct work_struct *work)
{
    struct batch_work *batch = container_of(work, ...);
    int ret;

    ret = do_partial_work();  /* 부분 작업 수행 */

    if (ret)
        WRITE_ONCE(batch->result, ret);

    if (atomic_dec_and_test(&batch->remaining))
        complete(&batch->done);  /* 마지막 워커만 신호 */
}

static int dispatch_batch(int n_workers)
{
    struct batch_work batch;
    int i;

    init_completion(&batch.done);
    atomic_set(&batch.remaining, n_workers);
    batch.result = 0;

    for (i = 0; i < n_workers; i++)
        queue_work(my_wq, &works[i]);

    wait_for_completion(&batch.done);
    return batch.result;
}

Fan-out 패턴: 1 producer → N consumers

/* 하나의 이벤트를 여러 대기자에게 브로드캐스트 */
struct init_sync {
    struct completion    ready;  /* 초기화 완료 신호 */
    void                *shared_data;
};

static int consumer_thread(void *arg)
{
    struct init_sync *sync = arg;

    wait_for_completion(&sync->ready);
    /* 이 시점에서 shared_data 접근 안전 */
    process_data(sync->shared_data);
    return 0;
}

static void producer_init(struct init_sync *sync)
{
    sync->shared_data = prepare_data();
    smp_wmb();  /* shared_data 쓰기 보장 */
    complete_all(&sync->ready);
    /* 모든 consumer_thread 즉시 진행 */
}

Barrier 패턴: 다중 스레드 동기화 지점

/* N개 스레드가 모두 도착할 때까지 대기하는 barrier */
struct thread_barrier {
    struct completion    all_arrived;
    atomic_t            count;
    int                 n_threads;
};

static void barrier_wait(struct thread_barrier *b)
{
    if (atomic_inc_return(&b->count) == b->n_threads) {
        /* 마지막 도착 스레드가 모두를 깨움 */
        complete_all(&b->all_arrived);
    } else {
        wait_for_completion(&b->all_arrived);
    }
}

/* 사용 예: 병렬 초기화 */
static int init_thread(void *arg)
{
    do_local_init();     /* 각자 초기화 */
    barrier_wait(&init_barrier);
    do_global_work();    /* 모두 초기화 후 시작 */
    return 0;
}
다중 Producer/Consumer 패턴 Fan-in (N:1) Worker A Worker B Worker C atomic_dec_and_test complete() 1회 Consumer 깨움 Fan-out (1:N) Producer complete_all() C₁ C₂ C₃ done = UINT_MAX → reinit 필요 Barrier (N:N) T₁ T₂ T₃ atomic_inc_return last: complete_all() 모두 진행
Fan-in(N:1), Fan-out(1:N), Barrier(N:N) 세 가지 completion 패턴 비교

커널 내 실제 사용 사례

패턴커널 서브시스템구현 위치
Fan-in블록 I/O 배리어block/blk-flush.c — flush 요청 N개 완료 후 진행
Fan-inCPU hotplugkernel/cpu.c — 오프라인 CPU 대기
Fan-out모듈 로딩kernel/module/main.c — init 완료 후 모든 대기자 해제
Fan-out펌웨어 로드drivers/base/firmware_loader — 로드 완료 브로드캐스트
Barrierkthread 파킹kernel/kthread.c — 모든 스레드 파킹 후 진행
Barrier워크큐 flushkernel/workqueue.c — 워크 항목 완료 동기화
참고: Fan-in 패턴에서 atomic_dec_and_testcomplete()의 조합은 매우 일반적입니다. 하지만 kref와 혼동하지 마세요. kref는 객체 수명 관리이고, completion fan-in은 작업 완료 동기화입니다.

pm_runtime과 completion 연동

Linux 전력 관리(PM) 서브시스템은 비동기 suspend/resume 동기화에 completion을 광범위하게 사용합니다. dev->power 구조체(Struct) 내부의 completion이 런타임 PM 전환의 핵심 동기화 메커니즘입니다.

dev->power.completion 구조

/* include/linux/pm.h */
struct dev_pm_info {
    /* ... */
    struct completion    completion;     /* PM 전환 완료 대기 */
    spinlock_t          lock;
    unsigned int        runtime_status; /* RPM_ACTIVE, RPM_SUSPENDED, ... */
    int                 usage_count;    /* PM 참조 카운트 */
    unsigned int        disable_depth;
    /* ... */
};

/* runtime_status 상태: */
enum rpm_status {
    RPM_INVALID = -1,
    RPM_ACTIVE  = 0,
    RPM_RESUMING,
    RPM_SUSPENDED,
    RPM_SUSPENDING,
};

pm_runtime_barrier 구현

/* drivers/base/power/runtime.c */

int pm_runtime_barrier(struct device *dev)
{
    int retval = 0;

    pm_runtime_lock(dev);

    if (dev->power.request_pending) {
        __pm_runtime_barrier(dev);
        retval = pm_runtime_status(dev) == RPM_SUSPENDED
                 ? 1 : 0;
    }

    pm_runtime_unlock(dev);
    return retval;
}

static void __pm_runtime_barrier(struct device *dev)
{
    pm_runtime_deactivate_timer(dev);

    if (dev->power.request_pending) {
        dev->power.request = RPM_REQ_NONE;
        spin_unlock_irq(&dev->power.lock);

        cancel_work_sync(&dev->power.work);

        spin_lock_irq(&dev->power.lock);
        dev->power.request_pending = false;
    }

    if (dev->power.runtime_status == RPM_SUSPENDING ||
        dev->power.runtime_status == RPM_RESUMING) {

        DEFINE_WAIT_FUNC(wait, default_wake_function);

        for (;;) {
            prepare_to_wait(&dev->power.wait_queue, &wait,
                            TASK_UNINTERRUPTIBLE);
            if (dev->power.runtime_status != RPM_SUSPENDING &&
                dev->power.runtime_status != RPM_RESUMING)
                break;
            spin_unlock_irq(&dev->power.lock);
            schedule();
            spin_lock_irq(&dev->power.lock);
        }
        finish_wait(&dev->power.wait_queue, &wait);
    }
}

비동기 suspend/resume에서의 completion

/* drivers/base/power/main.c */

/* 비동기 suspend: 각 디바이스가 독립적으로 suspend */
static void async_suspend(void *data, async_cookie_t cookie)
{
    struct device *dev = data;
    int error;

    error = __device_suspend(dev, pm_transition, true);
    if (error) {
        dpm_save_failed_dev(dev_name(dev));
        pm_dev_err(dev, pm_transition, " async", error);
    }

    complete(&dev->power.completion);  /* suspend 완료 신호 */
}

/* 부모 디바이스는 자식의 suspend 완료를 대기 */
static int __device_suspend(struct device *dev, ...)
{
    /* 자식 디바이스의 완료 대기 */
    dpm_wait_for_subordinate(dev, async);

    /* ... 실제 suspend 콜백 호출 ... */
}

static void dpm_wait_for_subordinate(struct device *dev, bool async)
{
    device_for_each_child(dev, &async, dpm_wait_fn);

    /* dpm_wait_fn 내부: */
    /* wait_for_completion(&dev->power.completion); */
}
PM 비동기 suspend: completion 기반 디바이스 트리 동기화 Parent Device (PCI bridge) wait_for_completion Child A (NIC) Child B (GPU) Child C (Audio) async_suspend() async_suspend() async_suspend() complete() ✓ complete() ✓ complete() ✓ Parent suspend 진행
부모 디바이스는 모든 자식의 completion 신호를 받은 후에만 suspend를 진행합니다
참고: PM 서브시스템의 completion은 매 suspend/resume 사이클마다 reinit_completion()으로 리셋됩니다. dpm_prepare()에서 각 디바이스의 dev->power.completion을 초기화합니다.

DMA fence와 completion 패턴

GPU 그래픽스와 미디어 서브시스템에서 dma_fence는 비동기 하드웨어 작업의 완료를 추적합니다. dma_fence 자체는 completion을 직접 내장하지 않지만, completion과 유사한 신호-대기 패턴을 사용하며, 드라이버에서 completion과 결합하여 사용하는 패턴이 빈번합니다.

dma_fence 구조와 신호 메커니즘

/* include/linux/dma-fence.h */
struct dma_fence {
    struct kref             refcount;
    const struct dma_fence_ops  *ops;
    unsigned long           flags;
    /* DMA_FENCE_FLAG_SIGNALED_BIT — 완료 시 설정 */
    /* DMA_FENCE_FLAG_ENABLE_SIGNAL_BIT — 콜백 활성화 */
    spinlock_t              *lock;
    struct list_head        cb_list;    /* 완료 콜백 리스트 */
    u64                     context;
    u64                     seqno;
    ktime_t                 timestamp;
    int                     error;
};

dma_fence_wait과 completion 조합

/* drivers/dma-buf/dma-fence.c */

long dma_fence_wait_timeout(struct dma_fence *fence,
                            bool intr, long timeout)
{
    if (test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags))
        return timeout;  /* fast path: 이미 시그널됨 */

    /* fence->ops->wait 또는 기본 dma_fence_default_wait 호출 */
    return fence->ops->wait(fence, intr, timeout);
}

/* 기본 구현: dma_fence_default_wait */
signed long dma_fence_default_wait(struct dma_fence *fence,
                                    bool intr, signed long timeout)
{
    struct default_wait_cb cb;
    long ret = timeout ? timeout : 1;

    spin_lock_irqsave(fence->lock, flags);

    if (test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags))
        goto out;

    cb.base.func = dma_fence_default_wait_cb;
    cb.task = current;
    list_add(&cb.base.node, &fence->cb_list);

    while (!test_bit(DMA_FENCE_FLAG_SIGNALED_BIT, &fence->flags)) {
        if (intr)
            set_current_state(TASK_INTERRUPTIBLE);
        else
            set_current_state(TASK_UNINTERRUPTIBLE);

        spin_unlock_irqsave(fence->lock, flags);
        ret = schedule_timeout(ret);
        spin_lock_irqsave(fence->lock, flags);

        if (ret <= 0)
            break;
    }
out:
    spin_unlock_irqrestore(fence->lock, flags);
    return ret;
}

드라이버에서의 fence + completion 결합 패턴

/* GPU 드라이버에서 fence 완료를 completion으로 변환하는 패턴 */

struct gpu_sync {
    struct dma_fence_cb     cb;
    struct completion       done;
};

static void fence_cb(struct dma_fence *fence, struct dma_fence_cb *cb)
{
    struct gpu_sync *sync = container_of(cb, struct gpu_sync, cb);
    complete(&sync->done);  /* fence 시그널 → completion 전환 */
}

static int gpu_submit_and_wait(struct drm_device *dev,
                                struct gpu_job *job)
{
    struct gpu_sync sync;
    struct dma_fence *fence;
    int ret;

    init_completion(&sync.done);

    fence = gpu_submit_job(dev, job);
    if (IS_ERR(fence))
        return PTR_ERR(fence);

    ret = dma_fence_add_callback(fence, &sync.cb, fence_cb);
    if (ret == -ENOENT) {
        /* 이미 시그널됨 — 즉시 진행 */
        dma_fence_put(fence);
        return 0;
    }

    /* fence 콜백이 complete()를 호출할 때까지 대기 */
    ret = wait_for_completion_interruptible_timeout(
              &sync.done, msecs_to_jiffies(5000));
    if (ret == 0)
        ret = -ETIMEDOUT;
    else if (ret > 0)
        ret = 0;

    dma_fence_put(fence);
    return ret;
}
DMA fence + completion 결합 패턴 CPU (드라이버) init_completion() gpu_submit_job() fence_add_callback() wait_for_completion() SLEEP 깨어남! 결과 처리 GPU (하드웨어) 명령 큐 수신 GPU 연산 수행 (렌더링/컴퓨트/복사) 인터럽트 발생 IRQ Handler dma_fence_signal() → fence_cb() complete(&sync.done)
GPU가 작업을 완료하면 IRQ에서 fence를 시그널하고, 콜백이 completion을 완료하여 CPU 대기 스레드를 깨웁니다
참고: dma_fence_signal()은 IRQ 핸들러에서 호출되므로 내부적으로 spin_lock_irqsave를 사용합니다. 콜백 내부에서 complete()를 호출하는 것은 안전합니다. 하지만 콜백 내에서 wait_for_completion()을 호출하면 교착 상태(Deadlock)가 발생합니다.

벤치마크: completion vs waitqueue vs semaphore

completion, wait_queue, semaphore는 모두 "대기/신호" 메커니즘을 제공하지만, 내부 구현의 차이로 인해 성능 특성이 다릅니다. 이 섹션에서는 신호 지연 시간, 처리량(Throughput), 메모리 오버헤드를 비교합니다.

신호 지연 시간 비교

/* 벤치마크 모듈: signal latency 측정 */
static void bench_completion(int iterations)
{
    struct completion done;
    ktime_t start, end;
    int i;

    init_completion(&done);

    start = ktime_get();
    for (i = 0; i < iterations; i++) {
        complete(&done);
        wait_for_completion(&done);
        reinit_completion(&done);
    }
    end = ktime_get();

    pr_info("completion: %lld ns/op\n",
            ktime_to_ns(ktime_sub(end, start)) / iterations);
}

static void bench_semaphore(int iterations)
{
    struct semaphore sem;
    ktime_t start, end;
    int i;

    sema_init(&sem, 0);

    start = ktime_get();
    for (i = 0; i < iterations; i++) {
        up(&sem);
        down(&sem);
    }
    end = ktime_get();

    pr_info("semaphore: %lld ns/op\n",
            ktime_to_ns(ktime_sub(end, start)) / iterations);
}

측정 결과 (x86_64, 6.8 커널, idle 시스템)

메커니즘signal→wake (ns)구조체 크기 (bytes)비고
completion (swait)~80–12032 (LP64)done(4) + padding(4) + swait_queue_head(24)
semaphore~150–20032 (LP64)lock + count + wait_list
wait_queue (직접 사용)~100–15024 (LP64)커스텀 콜백 지원 → 간접 호출 오버헤드
completion (fast path)~15–25done≠0, 슬립 없이 즉시 반환
참고: 위 수치는 단일 CPU에서 동일 스레드 내 signal-wait 왕복 시간입니다. 실제 다중 CPU 환경에서는 캐시 라인(Cache Line) 바운싱으로 인해 3–5배 증가할 수 있습니다. completion의 fast path(이미 done인 경우)는 spinlock 획득-해제와 done 감소만 수행하므로 가장 빠릅니다.

swait vs 일반 wait_queue 오버헤드

항목swait_queuewait_queue
웨이크업 콜백없음 (직접 wake_up_process)있음 (함수 포인터 호출)
노드 크기16 bytes32 bytes
exclusive wakeup미지원지원
poll/epoll 통합미지원지원
RT 호환성완전제한적
웨이크업 지연예측 가능콜백에 따라 가변
Signal-Wake 지연 시간 비교 지연 시간 (ns) 200 150 100 50 0 ~20ns completion (fast path) ~100ns completion (slow path) ~125ns wait_queue (콜백 포함) ~175ns semaphore (down/up) ~400ns cross-CPU (캐시 바운싱)
단일 CPU 기준 signal-wake 왕복 지연 시간. completion fast path가 가장 빠르며, cross-CPU 시나리오에서 캐시(Cache) 바운싱이 주요 병목(Bottleneck)

선택 가이드

시나리오최적 선택근거
일회성 완료 신호completion의도 명확, swait으로 가볍고 RT 호환
리소스 카운팅 (N개 허용)semaphorecount 기반 자연스러운 리소스 관리
조건부 대기 (커스텀 조건)wait_queuewait_event 매크로(Macro)로 임의 조건 표현
상호 배제mutex소유권 + 우선순위 상속(Priority Inheritance) + lockdep
poll/epoll 통합wait_queueswait은 미지원, wait_queue_head 필수

메모리 순서: done 카운터와 배리어

completion의 정확한 동작은 메모리 배리어(Memory Barrier)에 의존합니다. complete()에서의 쓰기와 wait_for_completion()에서의 읽기 사이에 적절한 순서 보장(Ordering)이 없으면, 대기자가 완료 신호를 보고도 생산자의 데이터를 읽지 못할 수 있습니다.

complete()의 쓰기 배리어

/* complete() 내부의 메모리 순서 보장 */

void complete(struct completion *x)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&x->wait.lock, flags);
    /*                                                         */
    /* ┌── raw_spin_lock은 ACQUIRE 의미론을 제공 ──┐             */
    /* │ 이전의 모든 쓰기(데이터 준비)가                │            */
    /* │ 이 시점 이전에 완료됨을 보장                 │            */
    /* └───────────────────────────────────┘             */

    x->done++;          /* 보호된 영역 안에서 done 변경 */

    swake_up_locked(&x->wait);
    /*                                                         */
    /* wake_up_process() 내부:                                   */
    /* smp_wmb() — done 변경이 대기자에게 보이도록 보장            */

    raw_spin_unlock_irqrestore(&x->wait.lock, flags);
    /*                                                         */
    /* RELEASE 의미론: done++ 및 이전 쓰기가                      */
    /* unlock 이전에 완료됨                                      */
}

wait_for_completion()의 읽기 배리어

/* wait 경로의 메모리 순서 */

/* do_wait_for_common 내부: */
raw_spin_lock_irq(&x->wait.lock);
/* ACQUIRE: 이후 읽기(x->done, 공유 데이터)가 */
/* 이 시점 이후에 수행됨을 보장 */

if (x->done) {
    /* done != 0: complete()의 모든 쓰기가 보임 */
    /* spin_lock ACQUIRE가 complete()의 RELEASE와 짝을 이룸 */
    x->done--;
}

raw_spin_unlock_irq(&x->wait.lock);
/* RELEASE: done-- 완료 후 unlock */

/* 이 시점 이후: 생산자가 준비한 데이터에 안전하게 접근 가능 */

보장되는 메모리 순서

/* 생산자-소비자 패턴에서의 메모리 순서 보장 */

/* Producer (CPU 0): */
shared_buffer->data = result;      /* ① 데이터 쓰기 */
shared_buffer->len = len;          /* ② 길이 쓰기 */
complete(&done);                    /* ③ 완료 신호 */
/* raw_spin_lock (ACQUIRE) → done++ → spin_unlock (RELEASE) */
/* ①②는 ③의 ACQUIRE 이전에 완료 보장 */

/* Consumer (CPU 1): */
wait_for_completion(&done);         /* ④ 대기 */
/* raw_spin_lock (ACQUIRE) → done 확인 → spin_unlock (RELEASE) */
process(shared_buffer->data);       /* ⑤ 데이터 읽기 */
use_len(shared_buffer->len);        /* ⑥ 길이 읽기 */

/* 보장: ⑤⑥은 ①② 이후의 값을 읽음 */
/* 근거: spinlock의 ACQUIRE/RELEASE 쌍이 happens-before 관계를 형성 */
completion 메모리 순서 보장 CPU 0 (Producer) buf->data = result buf->len = len spin_lock (ACQUIRE) done++ / swake_up spin_unlock (RELEASE) CPU 1 (Consumer) wait_for_completion 진입 spin_lock (ACQUIRE) SLEEP... (schedule) 깨어남 → done 확인 spin_unlock (RELEASE) process(buf->data) 안전 happens-before
spinlock의 ACQUIRE/RELEASE 의미론이 CPU 간 메모리 순서를 보장하여, 소비자는 생산자의 데이터를 안전하게 읽습니다
주의: completion을 사용하지 않고 done 변수만으로 직접 신호를 구현하면, 명시적인 smp_wmb()/smp_rmb() 쌍이 필요합니다. completion은 내부 spinlock이 이를 자동으로 처리하므로 별도 배리어가 불필요합니다.

try_wait_for_completion의 메모리 순서

/* try_wait_for_completion: 비블로킹 done 확인 */
bool try_wait_for_completion(struct completion *x)
{
    unsigned long flags;
    bool ret = true;

    /* 먼저 lockless 체크: 동기화 없이 빠르게 확인 */
    if (!READ_ONCE(x->done))
        return false;
    /* READ_ONCE는 컴파일러 배리어만 제공 (CPU 배리어 아님) */
    /* 하지만 false 반환 경로에서는 데이터 접근이 없으므로 안전 */

    raw_spin_lock_irqsave(&x->wait.lock, flags);
    /* ACQUIRE: 이 시점 이후 done 읽기는 최신 값 */
    if (!x->done)
        ret = false;
    else if (x->done != UINT_MAX)
        x->done--;
    raw_spin_unlock_irqrestore(&x->wait.lock, flags);
    return ret;
}

서브시스템: USB 전송 완료

USB 서브시스템은 completion의 가장 대표적인 사용 사례 중 하나입니다. USB 요청 블록(URB)의 비동기 전송 완료를 동기적으로 대기하는 패턴에서 completion이 핵심 역할을 합니다.

동기 USB 전송: usb_start_wait_urb

/* drivers/usb/core/message.c */

static int usb_start_wait_urb(struct urb *urb, int timeout,
                                int *actual_length)
{
    struct api_context ctx;
    unsigned long expire;
    int retval;

    init_completion(&ctx.done);      /* ① completion 초기화 */
    urb->context = &ctx;             /* ② URB에 context 연결 */
    urb->actual_length = 0;

    retval = usb_submit_urb(urb, GFP_NOIO);
                                     /* ③ URB 제출 (비동기) */
    if (unlikely(retval))
        goto out;

    expire = timeout ? msecs_to_jiffies(timeout) : MAX_SCHEDULE_TIMEOUT;

    if (!wait_for_completion_timeout(&ctx.done, expire)) {
        /* ④ 타임아웃: URB 취소 */
        usb_kill_urb(urb);
        retval = (urb->status == -ENOENT) ? -ETIMEDOUT : urb->status;
    } else {
        retval = ctx.status;          /* ⑤ 완료: 상태 확인 */
    }

out:
    if (actual_length)
        *actual_length = urb->actual_length;
    usb_free_urb(urb);
    return retval;
}

/* URB 완료 콜백 */
static void usb_api_blocking_completion(struct urb *urb)
{
    struct api_context *ctx = urb->context;

    ctx->status = urb->status;        /* 상태 저장 */
    complete(&ctx->done);              /* ⑥ 완료 신호 */
}

usb_kill_urb와 completion

/* drivers/usb/core/urb.c */

void usb_kill_urb(struct urb *urb)
{
    might_sleep();
    if (!(urb && urb->dev && urb->ep))
        return;

    atomic_inc(&urb->reject);  /* 재제출 차단 */

    usb_hcd_unlink_urb(urb, -ENOENT);

    /* URB의 completion이 호출될 때까지 대기 */
    wait_for_completion(&urb->done);
    /* 여기서 urb->done은 struct urb 내장 completion */

    atomic_dec(&urb->reject);
}

/* struct urb 내부: */
struct urb {
    /* ... */
    struct kref         kref;
    int                 unlinked;
    struct completion   done;   /* 전송 완료 동기화 */
    /* ... */
};

usb_control_msg: 고수준 동기 전송

/* drivers/usb/core/message.c */

int usb_control_msg(struct usb_device *dev, unsigned int pipe,
                     __u8 request, __u8 requesttype,
                     __u16 value, __u16 index,
                     void *data, __u16 size, int timeout)
{
    struct usb_ctrlrequest *dr;
    int ret;

    dr = kmalloc(sizeof(struct usb_ctrlrequest), GFP_NOIO);
    /* setup 패킷 구성 ... */

    /* 내부적으로 usb_start_wait_urb → completion 대기 */
    ret = usb_internal_control_msg(dev, pipe, dr, data, size, timeout);

    kfree(dr);
    return ret;
}

/* 호출 체인: */
/* usb_control_msg */
/*   → usb_internal_control_msg */
/*     → usb_start_wait_urb */
/*       → init_completion + usb_submit_urb */
/*       → wait_for_completion_timeout */
/*         ← usb_api_blocking_completion → complete() */
USB 동기 전송: completion 기반 흐름 드라이버 (프로세스 컨텍스트) usb_control_msg() init_completion(&ctx.done) usb_submit_urb() wait_for_completion USB Core / HCD (호스트 컨트롤러 드라이버) URB → HCD 큐 → 하드웨어 전송 시작 스케줄 → 전송 디스크립터 설정 USB 호스트 컨트롤러 하드웨어 SETUP → DATA → STATUS (Control) | BULK/INT/ISOC 전송 ... 완료 → IRQ IRQ 발생 인터럽트 핸들러 (IRQ 컨텍스트) usb_hcd_giveback_urb() urb->complete() 콜백 complete(&ctx.done)
USB 전송은 submit→하드웨어 전송→IRQ→콜백→complete() 경로를 거쳐 대기 중인 드라이버를 깨웁니다
참고: USB 서브시스템에서 usb_kill_urb()는 반드시 wait_for_completion()을 사용하며, 이는 URB의 내장 struct completion done 필드를 대기합니다. usb_kill_urb() 반환 후에는 URB의 콜백이 완료되어 안전하게 URB 메모리를 해제할 수 있습니다.

서브시스템: crypto 비동기 완료

커널 암호화(Encryption) 서브시스템(crypto API)은 하드웨어 가속기를 활용한 비동기 암호 연산을 지원합니다. 비동기 요청의 완료 대기에 completion 기반 패턴이 사용되며, crypto_wait_req()가 표준 대기 함수입니다.

비동기 crypto 요청 구조

/* include/linux/crypto.h */
struct crypto_async_request {
    struct list_head    list;
    crypto_completion_t complete;  /* 완료 콜백 */
    void                *data;     /* 콜백 데이터 */
    struct crypto_tfm   *tfm;
    u32                 flags;
};

/* 콜백 타입 */
typedef void (*crypto_completion_t)(void *req, int err);

/* 대기 구조체 */
struct crypto_wait {
    struct completion    completion;
    int                 err;
};

crypto_wait_req 구현

/* include/linux/crypto.h */

static inline int crypto_wait_req(int err, struct crypto_wait *wait)
{
    switch (err) {
    case -EINPROGRESS:
    case -EBUSY:
        /* 비동기 처리 중 → completion 대기 */
        wait_for_completion(&wait->completion);
        reinit_completion(&wait->completion);
        err = wait->err;
        break;
    }
    return err;
}

/* 완료 콜백 */
void crypto_req_done(void *req, int err)
{
    struct crypto_wait *wait = req;

    if (err == -EINPROGRESS)
        return;  /* 아직 진행 중, 최종 콜백 아님 */

    wait->err = err;
    complete(&wait->completion);
}
EXPORT_SYMBOL_GPL(crypto_req_done);

드라이버에서의 사용 패턴

/* 비동기 AES-GCM 암호화 예시 */
static int do_aead_encrypt(struct crypto_aead *tfm,
                            struct scatterlist *src,
                            struct scatterlist *dst,
                            int len, u8 *iv)
{
    DECLARE_CRYPTO_WAIT(wait);  /* crypto_wait + init_completion */
    struct aead_request *req;
    int err;

    req = aead_request_alloc(tfm, GFP_KERNEL);
    if (!req)
        return -ENOMEM;

    aead_request_set_callback(req,
        CRYPTO_TFM_REQ_MAY_BACKLOG | CRYPTO_TFM_REQ_MAY_SLEEP,
        crypto_req_done, &wait);
    aead_request_set_crypt(req, src, dst, len, iv);

    err = crypto_wait_req(
              crypto_aead_encrypt(req),
              &wait);
    /*                                                    */
    /* crypto_aead_encrypt() 반환값:                       */
    /*   0          → 동기 완료 (HW가 즉시 처리)            */
    /*   -EINPROGRESS → 비동기 (HW 큐에 넣음)               */
    /*   -EBUSY     → backlog (나중에 처리)                 */
    /*   기타       → 에러                                 */

    aead_request_free(req);
    return err;
}

CRYPTO_TFM_REQ_MAY_SLEEP 플래그

플래그의미completion 관련성
CRYPTO_TFM_REQ_MAY_SLEEP대기 가능 컨텍스트wait_for_completion() 사용 가능
CRYPTO_TFM_REQ_MAY_BACKLOG백로그 허용-EBUSY 반환 시에도 completion 대기
(없음)atomic 컨텍스트completion 대기 불가, 콜백으로만 완료 처리
참고: crypto 서브시스템의 -EINPROGRESScrypto_req_donecomplete() 패턴은 USB의 URB 완료 패턴과 구조적으로 동일합니다. 두 경우 모두 "비동기 하드웨어 요청 → IRQ/소프트IRQ 콜백 → completion 신호"라는 공통 패턴을 따릅니다.

reinit_completion 재사용 패턴

/* crypto_wait_req에서의 reinit 패턴 */
static inline int crypto_wait_req(int err, struct crypto_wait *wait)
{
    switch (err) {
    case -EINPROGRESS:
    case -EBUSY:
        wait_for_completion(&wait->completion);
        reinit_completion(&wait->completion);
        /* ↑ 다음 요청을 위해 즉시 리셋 */
        /* 주의: done=0으로 리셋. wait queue는 그대로 */
        err = wait->err;
        break;
    }
    return err;
}

/* 반복 암호 연산 시: */
DECLARE_CRYPTO_WAIT(wait);  /* 한 번만 선언 */

for (i = 0; i < n_blocks; i++) {
    err = crypto_wait_req(
              crypto_skcipher_encrypt(req),
              &wait);
    /* reinit은 crypto_wait_req 내부에서 자동 수행 */
    /* 따라서 반복 사용이 안전 */
    if (err)
        break;
}
주의: CRYPTO_TFM_REQ_MAY_SLEEP 없이 crypto_wait_req()를 호출하면 atomic 컨텍스트에서 wait_for_completion()이 호출되어 커널 BUG가 발생합니다. softirq나 tasklet에서 비동기 crypto를 사용할 때는 콜백 기반으로만 완료를 처리해야 합니다.

커널 버전별 진화

completion은 커널 v2.4.7에서 처음 도입된 이후, 20년 이상에 걸쳐 내부 구현이 점진적으로 개선되어 왔습니다. wait queue에서 simple wait queue로의 전환이 가장 큰 변경이었으며, PREEMPT_RT 지원이 핵심 동인이었습니다.

버전별 주요 변경 이력

커널 버전연도변경 내용영향
v2.4.72001completion 최초 도입semaphore 기반 완료 대기를 대체
v2.6.112005wait_for_completion_timeout 추가타임아웃 기반 대기 지원
v2.6.112005wait_for_completion_interruptible 추가시그널 인터럽트 가능 대기
v2.6.252008wait_for_completion_killable 추가SIGKILL만 수신 가능한 대기
v2.6.272008try_wait_for_completion 추가비블로킹 done 확인
v2.6.272008completion_done 추가done 상태 확인 (소비 없음)
v3.122013DECLARE_COMPLETION_ONSTACK lockdep 연동스택 completion 수명 검증
v3.172014wait_for_completion_io 추가I/O 대기 시간(Latency) 통계 분리
v4.132017wait_queue → swait_queue 전환RT 호환성 확보, 구조 간소화
v5.42019lockdep complete_acquire/release 추가completion 의존성 추적 강화
v5.72020lockdep_assert_RT_in_threaded_ctxRT에서 complete_all 컨텍스트 검증
v5.152021PREEMPT_RT 메인라인 통합 시작swait 기반 completion이 RT 표준
v6.12022lockdep 어노테이션 정리completion 내부 락 의존성 정확도 향상
v6.82024wait_for_completion 코드 정리action 콜백 패턴 간소화

v4.13: swait_queue 전환의 배경

/* v4.12 이전 (wait_queue 기반): */
struct completion {
    unsigned int        done;
    wait_queue_head_t   wait;  /* spinlock_t + list_head */
};
/* 문제: PREEMPT_RT에서 spinlock_t → rt_mutex로 변환 */
/* → completion 대기 중 우선순위 역전 가능 */
/* → wake_up 콜백이 RT에서 예측 불가능한 지연 유발 */

/* v4.13 이후 (swait_queue 기반): */
struct completion {
    unsigned int        done;
    struct swait_queue_head  wait;  /* raw_spinlock_t + list_head */
};
/* 해결: raw_spinlock은 RT에서도 진정한 spinlock 유지 */
/* → 예측 가능한 웨이크업 시간 */
/* → 커스텀 콜백 제거 (불필요했음) */

API 확장 연대기

/* v2.4.7 (2001) — 초기 API */
init_completion(x);
wait_for_completion(x);
complete(x);
complete_all(x);
DECLARE_COMPLETION(x);

/* v2.6.11 (2005) — 타임아웃/인터럽트 */
wait_for_completion_timeout(x, timeout);
wait_for_completion_interruptible(x);
wait_for_completion_interruptible_timeout(x, timeout);

/* v2.6.25 (2008) — killable */
wait_for_completion_killable(x);
wait_for_completion_killable_timeout(x, timeout);

/* v2.6.27 (2008) — 비블로킹 */
try_wait_for_completion(x);
completion_done(x);

/* v3.17 (2014) — I/O 통계 */
wait_for_completion_io(x);
wait_for_completion_io_timeout(x, timeout);

/* 전체 API: 초기화 3 + 대기 10 + 신호 2 + 조회 2 = 17개 */
completion 커널 버전별 진화 타임라인 v2.4 2001 최초 도입 wait_queue 기반 5개 API v2.6 2005–08 timeout 추가 interruptible 추가 killable 추가 try_wait 추가 v3.x 2013–14 ONSTACK lockdep I/O 대기 통계 13개 API v4.13 2017 swait 전환 raw_spinlock 사용 콜백 제거 RT 호환성 확보 v5.x 2019–21 lockdep 강화 RT 검증 추가 PREEMPT_RT 통합 v6.x 2022– 코드 정리 action 간소화 17개 API 확정 주요 설계 결정 요약 v2.4 도입 동기: semaphore를 완료 신호용으로 남용하는 패턴 제거 v2.6 확장: 타임아웃/시그널 처리 → 드라이버 안정성 향상 v4.13 전환: wait_queue → swait_queue → PREEMPT_RT 호환 (가장 큰 변경) v5.x 강화: lockdep 통합 → completion 의존성 자동 검증 v6.x 안정: API 안정화 완료, 구조적 변경 없이 유지보수 단계
2001년 도입부터 현재까지 completion의 진화. v4.13의 swait 전환이 가장 큰 구조적 변경

semaphore에서 completion으로의 마이그레이션

/* v2.4.7 이전: semaphore로 완료 대기 (안티패턴) */
struct semaphore done;
sema_init(&done, 0);    /* 0으로 초기화 → 즉시 블로킹 */

/* 생산자: */
up(&done);               /* "완료" 신호 */

/* 소비자: */
down(&done);             /* 완료 대기 */

/* 문제점: */
/* - 의도가 불명확 (상호 배제? 완료 신호?) */
/* - complete_all() 에 해당하는 기능 없음 */
/* - 소유권 개념이 없어 lockdep 검증 불가 */

/* v2.4.7 이후: completion 사용 (권장) */
struct completion done;
init_completion(&done);

/* 생산자: */
complete(&done);          /* 명확한 완료 신호 */

/* 소비자: */
wait_for_completion(&done);  /* 명확한 대기 */
참고: 커널 소스에서 sema_init(&sem, 0) 패턴이 발견되면 completion으로의 전환을 고려해야 합니다. coccinelle 스크립트(scripts/coccinelle/api/)에 semaphore→completion 변환 규칙이 포함되어 있습니다.

참고 자료

Completion 메커니즘의 설계와 활용에 대한 공식 문서 및 참고 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

completion과 관련된 다른 동기화 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.