Completion (완료 변수)
커널의 일회성 완료 신호 동기화 프리미티브인 completion을 분석합니다. struct completion의 done 카운터와 swait_queue_head 내부 구조, 초기화·대기·완료 신호 API 패밀리, complete() 단일 웨이크업과 complete_all() 브로드캐스트의 차이, 타임아웃·인터럽트(Interrupt) 가능·killable 대기 변형, reinit_completion 재사용 패턴, 모듈 로딩·펌웨어(Firmware) 요청·워커 스레드(Thread)·디바이스 프로빙 동기화 실전 사용 사례, PREEMPT_RT 영향, 안티패턴과 디버깅(Debugging)까지 포괄합니다.
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가지 변형이 있어 상황에 맞게 선택합니다.
단계별 이해
- 완료 신호 개념 파악
mutex/spinlock은 상호 배제(Mutual Exclusion)를 위한 것이고, completion은 "완료 이벤트 전달"을 위한 것임을 구분합니다. - done 카운터 상태 머신 이해
done 값이 0 → N → UINT_MAX로 변화하는 과정을 추적합니다. - API 패밀리 분류
초기화(3종) · 대기(8종) · 완료 신호(2종) API를 기능별로 분류합니다. - 재사용 패턴 숙지
reinit_completion vs init_completion의 차이를 정확히 파악합니다. - 실전 패턴 적용
모듈 로딩, 펌웨어 로드, 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이 도입된 배경이 바로 이 혼용 문제입니다.
| 특성 | semaphore | completion |
|---|---|---|
| 의도 | 자원 카운팅 (N개 동시 접근) | 일회성 완료 이벤트 전달 |
| 소유권 | down()한 측이 up() 해야 함 (논리적) | 생산자가 complete(), 소비자가 wait() |
| 재사용 | 자동 (카운터 증감) | 명시적 reinit_completion() 필요 |
| 의미 명확성 | 범용적이라 의도 파악 어려움 | 이름만으로 "완료 대기" 의도 전달 |
| PREEMPT_RT | sleeping lock으로 변환 가능 | swait 기반으로 RT에서도 예측 가능 |
Completion vs Wait Queue 비교
completion은 내부적으로 wait queue를 사용하지만, API 수준에서 중요한 차이가 있습니다. wait queue는 범용 대기 메커니즘이고, completion은 "완료 이벤트"에 특화된 고수준 래퍼입니다.
| 특성 | wait_queue | completion |
|---|---|---|
| 상태 추적 | 조건 변수 외부 관리 | done 카운터 내장 |
| 경합(Contention) 조건 | signal-before-wait 직접 처리 | done 카운터가 자동 처리 |
| 깨움 대상 | 조건 만족 시 전체/하나 | complete():하나, complete_all():전체 |
| 재사용 | 조건 함수로 자연스러운 재사용 | reinit_completion() 명시 호출 |
| 사용 패턴 | 복잡한 조건 대기 | 단순 완료 이벤트 |
| 내부 구현 | wait_queue_head_t + func 콜백(Callback) | swait_queue_head + done 정수 |
complete()가 wait_for_completion()보다 먼저 호출되어도 done이 1로 설정되어, 이후 wait_for_completion()이 즉시 반환됩니다.
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-1 | N번 완료 — 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; /* 리스트 연결 */
};
초기화 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
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() | jiffies | SIGKILL만 | 남은 jiffies, 0, -ERESTARTSYS |
wait_for_completion_state() | 무한 | state 지정 | 0 또는 -ERESTARTSYS |
try_wait_for_completion() | 즉시 | N/A | true(완료)/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;
}
완료 신호 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() — 1:1 생산자-소비자 패턴. 예: DMA 완료, 특정 요청의 응답 대기.
- complete_all() — 1:N 브로드캐스트 패턴. 예: 모듈 로딩 완료를 여러 스레드에 알림, 디바이스 초기화 완료 통지.
done 카운터 동작 원리
done 카운터는 completion의 핵심 상태 머신입니다. 생산자와 소비자의 호출 순서에 관계없이 정확하게 이벤트를 전달합니다.
상태 전이 규칙
| 연산 | done 변화 | 대기자 처리 | 예외 |
|---|---|---|---|
init_completion() | → 0 | wait queue 재초기화 | 대기자 있으면 손실! |
reinit_completion() | → 0 | wait 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, 다시 사용 가능 */
타임아웃 처리 패턴
타임아웃 변형은 무한 대기를 방지하는 가장 기본적인 안전 장치입니다. 하드웨어 응답이 보장되지 않는 상황에서 필수입니다.
/* 패턴: 타임아웃 대기 + 에러 처리 */
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) 상태 | 반응하는 시그널 | 용도 |
|---|---|---|---|
_interruptible | TASK_INTERRUPTIBLE | 모든 시그널 (SIGINT, SIGTERM 등) | 사용자 공간 요청 |
_killable | TASK_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;
}
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);
}
}
모듈 로딩 동기화 패턴
커널 모듈(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은 내부적으로 사용됨 */
}
워커 스레드 동기화
커널 스레드(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를 반환하면, 커널의 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_t | swait_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);
실전 사용 패턴
커널 소스에서 흔히 발견되는 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 관련 문제를 진단하는 방법입니다.
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_RT | swait_queue 동작 | RT 커널에서 raw_spinlock 유지 (sleeping lock 아님) |
CONFIG_LOCKDEP | DECLARE_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 → 타임아웃 만료 */
}
전체 실행 흐름 단계별 분석
| 단계 | 함수 | 동작 | 락 상태 |
|---|---|---|---|
| 1 | wait_for_completion | 진입, might_sleep 검사 | 없음 |
| 2 | wait_for_common | raw_spin_lock_irq 획득 | locked |
| 3 | do_wait_for_common | x->done fast path 체크 | locked |
| 4 | __prepare_to_swait | 대기 큐에 현재 태스크 등록 | locked |
| 5 | __set_current_state | 태스크 상태를 TASK_UNINTERRUPTIBLE로 설정 | locked |
| 6 | raw_spin_unlock_irq | 락 해제, 인터럽트 복원 | unlocked |
| 7 | schedule_timeout | 슬립 — 다른 태스크로 전환 | unlocked |
| 8 | (웨이크업) | complete()에 의해 재개 | unlocked |
| 9 | raw_spin_lock_irq | 락 재획득 | locked |
| 10 | 루프 조건 체크 | x->done && timeout 확인 | locked |
| 11 | __finish_swait | 대기 큐에서 제거 | locked |
| 12 | done-- | done 카운터 소비 (UINT_MAX 아닌 경우) | locked |
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에는 불필요) */
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_irqsave | IRQ 핸들러에서 호출 가능 |
wait_for_common() | raw_spin_lock_irq | 프로세스 컨텍스트 전용 (IRQ 항상 활성) |
try_wait_for_completion() | raw_spin_lock_irqsave | atomic 컨텍스트 허용 |
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;
}
커널 내 실제 사용 사례
| 패턴 | 커널 서브시스템 | 구현 위치 |
|---|---|---|
| Fan-in | 블록 I/O 배리어 | block/blk-flush.c — flush 요청 N개 완료 후 진행 |
| Fan-in | CPU hotplug | kernel/cpu.c — 오프라인 CPU 대기 |
| Fan-out | 모듈 로딩 | kernel/module/main.c — init 완료 후 모든 대기자 해제 |
| Fan-out | 펌웨어 로드 | drivers/base/firmware_loader — 로드 완료 브로드캐스트 |
| Barrier | kthread 파킹 | kernel/kthread.c — 모든 스레드 파킹 후 진행 |
| Barrier | 워크큐 flush | kernel/workqueue.c — 워크 항목 완료 동기화 |
atomic_dec_and_test와 complete()의 조합은 매우 일반적입니다. 하지만 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); */
}
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_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–120 | 32 (LP64) | done(4) + padding(4) + swait_queue_head(24) |
semaphore | ~150–200 | 32 (LP64) | lock + count + wait_list |
wait_queue (직접 사용) | ~100–150 | 24 (LP64) | 커스텀 콜백 지원 → 간접 호출 오버헤드 |
completion (fast path) | ~15–25 | — | done≠0, 슬립 없이 즉시 반환 |
swait vs 일반 wait_queue 오버헤드
| 항목 | swait_queue | wait_queue |
|---|---|---|
| 웨이크업 콜백 | 없음 (직접 wake_up_process) | 있음 (함수 포인터 호출) |
| 노드 크기 | 16 bytes | 32 bytes |
| exclusive wakeup | 미지원 | 지원 |
| poll/epoll 통합 | 미지원 | 지원 |
| RT 호환성 | 완전 | 제한적 |
| 웨이크업 지연 | 예측 가능 | 콜백에 따라 가변 |
선택 가이드
| 시나리오 | 최적 선택 | 근거 |
|---|---|---|
| 일회성 완료 신호 | completion | 의도 명확, swait으로 가볍고 RT 호환 |
| 리소스 카운팅 (N개 허용) | semaphore | count 기반 자연스러운 리소스 관리 |
| 조건부 대기 (커스텀 조건) | wait_queue | wait_event 매크로(Macro)로 임의 조건 표현 |
| 상호 배제 | mutex | 소유권 + 우선순위 상속(Priority Inheritance) + lockdep |
| poll/epoll 통합 | wait_queue | swait은 미지원, 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을 사용하지 않고 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_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 대기 불가, 콜백으로만 완료 처리 |
-EINPROGRESS → crypto_req_done → complete() 패턴은 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.7 | 2001 | completion 최초 도입 | semaphore 기반 완료 대기를 대체 |
| v2.6.11 | 2005 | wait_for_completion_timeout 추가 | 타임아웃 기반 대기 지원 |
| v2.6.11 | 2005 | wait_for_completion_interruptible 추가 | 시그널 인터럽트 가능 대기 |
| v2.6.25 | 2008 | wait_for_completion_killable 추가 | SIGKILL만 수신 가능한 대기 |
| v2.6.27 | 2008 | try_wait_for_completion 추가 | 비블로킹 done 확인 |
| v2.6.27 | 2008 | completion_done 추가 | done 상태 확인 (소비 없음) |
| v3.12 | 2013 | DECLARE_COMPLETION_ONSTACK lockdep 연동 | 스택 completion 수명 검증 |
| v3.17 | 2014 | wait_for_completion_io 추가 | I/O 대기 시간(Latency) 통계 분리 |
| v4.13 | 2017 | wait_queue → swait_queue 전환 | RT 호환성 확보, 구조 간소화 |
| v5.4 | 2019 | lockdep complete_acquire/release 추가 | completion 의존성 추적 강화 |
| v5.7 | 2020 | lockdep_assert_RT_in_threaded_ctx | RT에서 complete_all 컨텍스트 검증 |
| v5.15 | 2021 | PREEMPT_RT 메인라인 통합 시작 | swait 기반 completion이 RT 표준 |
| v6.1 | 2022 | lockdep 어노테이션 정리 | completion 내부 락 의존성 정확도 향상 |
| v6.8 | 2024 | wait_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개 */
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 변환 규칙이 포함되어 있습니다.
참고 자료
커널 공식 문서
- Completions — "wait for completion" barrier APIs — completion API 공식 가이드
- Driver Basics — 드라이버에서 completion 사용 패턴
- Lock types and their rules — completion 내부 raw_spinlock의 잠금 규칙
LWN.net 심층 기사
- The new mutex (2006) — semaphore(count=0)에서 completion으로의 전환 배경
- The mutex API (2013) — completion과 mutex의 역할 차이 (이벤트 vs 상호배제)
- Rethinking the wait queue (2018) — completion의 wait queue 기반 구현과 swait 비교
학술 자료 및 외부 참고
- Paul McKenney — "Is Parallel Programming Hard?" — 이벤트 기반 동기화 패턴
- 커널 소스:
include/linux/completion.h,kernel/sched/completion.c - 펌웨어 로딩:
drivers/base/firmware_loader/main.c— completion 사용의 대표 사례 - 모듈 언로드:
kernel/module/main.c— module exit completion 패턴
관련 문서
completion과 관련된 다른 동기화 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.