Wait Queue (대기 큐)

커널에서 특정 조건이 충족될 때까지 태스크(Task)를 슬립(Sleep)시키고, 조건이 만족되면 깨우는 핵심 메커니즘인 Wait Queue를 분석합니다. wait_queue_head_t/wait_queue_entry_t 자료구조, WQ_FLAG 플래그, wait_event 매크로(Macro) 패밀리의 내부 구현, wake_up 함수 패밀리, Exclusive/Non-exclusive 웨이크업과 Thundering Herd 방지, poll/select/epoll 연결, Simple Wait Queue(swait), Lost Wakeup 방지 패턴, PREEMPT_RT 영향까지 포괄합니다.

전제 조건: 동기화 기법, Spinlock, 프로세스(Process) 관리 문서를 먼저 읽으세요. Wait Queue는 spinlock을 내부적으로 사용하고, 스케줄러(Scheduler)와 긴밀하게 상호작용합니다.
일상 비유: Wait Queue는 병원 대기실과 같습니다. 환자(태스크)는 대기 명단에 이름을 올리고 잠을 잡니다. 의사(이벤트 소스)가 "다음 환자" 하고 부르면(wake_up) 깨어납니다. Exclusive 웨이크업은 한 명만 호명하는 것이고, Non-exclusive는 대기실 전체에 방송하는 것입니다.

핵심 요약

  • 조건 대기 프리미티브 — 조건이 거짓이면 태스크를 TASK_INTERRUPTIBLE/UNINTERRUPTIBLE 상태로 전환하여 슬립시키고, 조건이 참이 되면 깨웁니다.
  • 이중 자료구조wait_queue_head_t(spinlock + 이중 연결 리스트(Linked List))가 대기열 헤드, wait_queue_entry_t가 개별 대기자 엔트리입니다.
  • 매크로 패밀리wait_event/wait_event_interruptible/wait_event_timeout/wait_event_killable 등 조건+상태 조합으로 다양한 변형을 제공합니다.
  • Thundering Herd 방지WQ_FLAG_EXCLUSIVE 플래그로 하나의 대기자만 깨우는 exclusive 웨이크업을 지원합니다.
  • VFS poll 연결poll_wait()가 Wait Queue에 콜백(Callback)을 등록하여 select/poll/epoll 멀티플렉싱을 구현합니다.

단계별 이해

  1. 슬립/웨이크업 기본 개념
    busy-wait 대신 CPU를 양보(Yield)하는 블로킹 동기화의 필요성을 이해합니다.
  2. 자료구조 파악
    wait_queue_head_twait_queue_entry_t의 필드와 연결 관계를 분석합니다.
  3. wait_event 매크로 내부 추적
    조건 검사 → 상태 설정 → schedule() → 조건 재검사 루프의 흐름을 따라갑니다.
  4. wake_up 경로 분석
    대기 리스트 순회, 콜백 함수 호출, exclusive/non-exclusive 구분을 이해합니다.
  5. 실전 패턴과 함정 숙지
    Lost Wakeup, Thundering Herd, 시그널(Signal) 처리 등 실전 문제를 학습합니다.
커널 소스 위치: include/linux/wait.h, kernel/sched/wait.c에 핵심 구현이 있습니다. Simple Wait Queue는 include/linux/swait.h, kernel/sched/swait.c를 참고하세요.

이론적 배경: 조건 대기 메커니즘

Wait Queue는 조건 변수(condition variable)의 커널 구현입니다. POSIX pthread_cond_wait()와 유사한 개념이지만, 커널의 특수 요구사항(인터럽트(Interrupt) 컨텍스트 구분, 시그널 처리, 선점(Preemption) 모델 호환)에 맞게 설계되었습니다.

조건 동기화의 기본 원리

조건 동기화에서는 두 가지 역할이 존재합니다:

역할동작커널 매핑(Mapping)
Waiter (대기자)조건이 거짓이면 슬립, 참이 되면 깨어남wait_event() 호출 태스크
Waker (깨우는 자)조건을 참으로 만든 뒤 대기자를 깨움wake_up() 호출 코드

핵심 불변식은 다음과 같습니다:

/* 조건 동기화 불변식 */
1. 대기자는 반드시 대기열에 등록한 후 조건을 검사해야 합니다.
2. 깨우는 자는 반드시 조건을 설정한 후 wake_up을 호출해야 합니다.
3. 이 순서가 뒤바뀌면 Lost Wakeup이 발생합니다.
조건 동기화 시퀀스 — Wait Queue 기본 흐름 Waiter (태스크 A) Waker (태스크 B / IRQ) 1. 대기열에 등록 2. 조건 검사 (거짓) 3. TASK_INTERRUPTIBLE SLEEP 4. 조건 = 참 설정 5. wake_up() 호출 try_to_wake_up() 6. 조건 재검사 (참) 7. 대기열에서 제거 → 진행
대기자가 먼저 대기열에 등록 → 조건 검사 → 슬립, 깨우는 자가 조건 설정 → wake_up 순서가 핵심입니다

Sleep-Wait vs Busy-Wait 비교

특성Busy-Wait (spinlock)Sleep-Wait (wait queue)
CPU 사용대기 중 100% 사용대기 중 CPU 양보
적합 대기 시간(Latency)수 μs 이하수 μs ~ 무한
인터럽트 컨텍스트사용 가능사용 불가 (schedule 필요)
오버헤드(Overhead)없음 (루프)컨텍스트 스위치 ~5,000 cycles
선점선점 비활성화 필요선점 가능

wait_queue_head_t / wait_queue_entry_t 구조

Wait Queue는 두 가지 핵심 자료구조로 구성됩니다. 헤드(wait_queue_head_t)가 대기열 전체를 관리하고, 엔트리(wait_queue_entry_t)가 개별 대기자를 표현합니다.

/* include/linux/wait.h */

struct wait_queue_head {
    spinlock_t       lock;       /* 대기 리스트 보호용 spinlock */
    struct list_head  head;       /* 대기자 이중 연결 리스트 */
};
typedef struct wait_queue_head  wait_queue_head_t;

typedef int (*wait_queue_func_t)(struct wait_queue_entry *wq_entry,
                                  unsigned mode, int flags, void *key);

struct wait_queue_entry {
    unsigned int      flags;      /* WQ_FLAG_* 플래그 */
    void             *private;    /* 보통 current (struct task_struct *) */
    wait_queue_func_t func;       /* 웨이크업 콜백 함수 */
    struct list_head   entry;      /* head의 리스트에 연결 */
};
typedef struct wait_queue_entry  wait_queue_entry_t;
wait_queue_head_t / wait_queue_entry_t 메모리 레이아웃 wait_queue_head_t spinlock_t lock list_head head next → / prev → wait_queue_entry_t #1 flags: 0 private: task_A func: default_wake_function entry: list_head next → / prev → wait_queue_entry_t #2 flags: WQ_FLAG_EXCLUSIVE private: task_B func: default_wake_function entry: list_head next → / prev → 핵심 포인트: 1. head의 list_head와 각 entry의 list_head가 이중 연결 리스트로 순환 연결됩니다. 2. Non-exclusive 엔트리가 리스트 앞에, Exclusive 엔트리가 뒤에 추가됩니다. 3. spinlock이 대기 리스트의 추가/제거/순회를 보호합니다.
wait_queue_head_t의 list_head를 중심으로 wait_queue_entry_t들이 이중 연결 리스트로 연결됩니다

기본 웨이크업 콜백: default_wake_function

/* kernel/sched/wait.c */
int default_wake_function(struct wait_queue_entry *wq_entry,
                          unsigned mode, int wake_flags, void *key)
{
    return try_to_wake_up(wq_entry->private, mode, wake_flags);
}

/* private 필드에 저장된 task_struct를 try_to_wake_up()으로
   TASK_RUNNING 상태로 전환하고 런큐에 enqueue합니다. */

WQ_FLAG_* 플래그 분석

wait_queue_entry_tflags 필드는 대기자의 동작 방식을 제어합니다.

플래그설명
WQ_FLAG_EXCLUSIVE0x01Exclusive 웨이크업 대상. wake_up()은 이 플래그가 설정된 대기자 중 하나만 깨웁니다.
WQ_FLAG_WOKEN0x02wake_up에 의해 깨어났음을 표시. wait_woken() 패턴에서 spurious wakeup 방지용입니다.
WQ_FLAG_BOOKMARK0x04대기 리스트 순회 중 재시작(Reboot) 지점 북마크. 긴 리스트에서 lock 해제 후 순회를 이어가는 용도입니다.
WQ_FLAG_CUSTOM0x08커스텀 func을 사용하는 엔트리. autoremove_wake_function이 아닌 사용자 정의 콜백입니다.
WQ_FLAG_DONE0x10처리 완료. completion 구현에서 완료 상태를 표시하는 데 사용됩니다.
WQ_FLAG_PRIORITY0x20리스트 앞에 삽입. 일반 엔트리보다 우선 깨워야 하는 대기자에 사용합니다.
/* include/linux/wait.h — 플래그 정의 */
#define WQ_FLAG_EXCLUSIVE   0x01
#define WQ_FLAG_WOKEN       0x02
#define WQ_FLAG_BOOKMARK    0x04
#define WQ_FLAG_CUSTOM      0x08
#define WQ_FLAG_DONE        0x10
#define WQ_FLAG_PRIORITY    0x20
BOOKMARK 플래그 용도: __wake_up_common()이 긴 대기 리스트를 순회할 때, spinlock을 잠시 해제하고 다시 잡아야 하는 경우 순회 위치를 기억하는 더미 엔트리를 삽입합니다. 이 엔트리에 WQ_FLAG_BOOKMARK를 설정하여 실제 대기자와 구분합니다.

초기화 API

Wait Queue 헤드와 엔트리를 초기화하는 다양한 방법이 있습니다.

헤드 초기화

/* 정적 초기화 — 전역/파일 스코프 */
static DECLARE_WAIT_QUEUE_HEAD(my_wq);
/* → wait_queue_head_t my_wq = __WAIT_QUEUE_HEAD_INITIALIZER(my_wq); */

/* 동적 초기화 — 구조체 멤버, kmalloc 등 */
wait_queue_head_t wq;
init_waitqueue_head(&wq);
/* spin_lock_init() + INIT_LIST_HEAD() + lockdep 등록 */

엔트리 초기화

/* 스택 위에 정적 선언 */
DEFINE_WAIT(wait);
/* → wait_queue_entry_t wait = {
       .private = current,
       .func    = autoremove_wake_function,
       .entry   = LIST_HEAD_INIT(wait.entry),
   }; */

/* 또는 수동 초기화 */
wait_queue_entry_t wait;
init_waitqueue_entry(&wait, current);
/* .flags = 0, .private = current, .func = default_wake_function */

/* 커스텀 콜백 초기화 */
init_waitqueue_func_entry(&wait, my_wake_func);
/* .flags = 0, .private = NULL, .func = my_wake_func */
DEFINE_WAIT vs init_waitqueue_entry: DEFINE_WAITautoremove_wake_function을 콜백으로 설정하여, 깨어나면 자동으로 대기 리스트에서 제거됩니다. init_waitqueue_entrydefault_wake_function을 사용하며, 수동으로 remove_wait_queue를 호출해야 합니다.

wait_event 매크로 패밀리

가장 일반적으로 사용되는 Wait Queue API입니다. 조건을 인자로 받아 조건이 참이 될 때까지 슬립합니다.

매크로태스크 상태반환값특징
wait_event(wq, cond)TASK_UNINTERRUPTIBLEvoid시그널 무시. 조건 충족까지 무조건 대기
wait_event_interruptible(wq, cond)TASK_INTERRUPTIBLE0 또는 -ERESTARTSYS시그널로 깨어날 수 있음
wait_event_killable(wq, cond)TASK_KILLABLE0 또는 -ERESTARTSYS치명적 시그널(SIGKILL)만 수신
wait_event_timeout(wq, cond, timeout)TASK_UNINTERRUPTIBLE남은 jiffies (0이면 타임아웃)최대 대기 시간 제한
wait_event_interruptible_timeout(wq, cond, to)TASK_INTERRUPTIBLE남은 jiffies, 0, -ERESTARTSYS시그널 + 타임아웃
wait_event_freezable(wq, cond)TASK_INTERRUPTIBLE0freezer 호환 (suspend)
wait_event_idle(wq, cond)TASK_IDLEvoidload average에 기여하지 않음
wait_event 매크로 선택 가이드 시그널 처리 필요? No 타임아웃 필요? No wait_event() Yes wait_event_timeout() Yes SIGKILL만? No (모든 시그널) wait_event_interruptible() Yes wait_event_killable() 특수 변형 wait_event_idle(): TASK_IDLE — load average 미포함 | wait_event_freezable(): freezer 호환 (suspend/hibernate) wait_event_lock_irq(): 외부 spinlock과 함께 사용 | wait_event_cmd(): sleep/wake 전후 커스텀 명령 실행 반환값 주의 _timeout 변형: 남은 jiffies 반환 (0 = 타임아웃). _interruptible 변형: 시그널 시 -ERESTARTSYS 반환. 주의: _timeout의 반환값 0은 "타임아웃 됨"이지 "성공"이 아닙니다. 조건이 참이면 항상 > 0.
시그널 처리 여부와 타임아웃 필요 여부로 적절한 wait_event 매크로를 선택합니다

사용 예시

/* 기본 사용: 조건이 참이 될 때까지 무조건 대기 */
wait_event(my_wq, data_ready != 0);

/* 시그널 처리: 사용자 프로세스에서 권장 */
int ret = wait_event_interruptible(my_wq, data_ready != 0);
if (ret == -ERESTARTSYS)
    return -ERESTARTSYS;  /* 시그널에 의해 중단됨 */

/* 타임아웃: 최대 5초 대기 */
unsigned long remaining = wait_event_timeout(my_wq, done, 5 * HZ);
if (remaining == 0) {
    pr_warn("timeout waiting for completion\n");
    return -ETIMEDOUT;
}

wake_up 함수 패밀리

대기열에서 태스크를 깨우는 함수들입니다. 대기자의 태스크 상태 마스크와 exclusive 여부에 따라 동작이 달라집니다.

함수상태 마스크Exclusive 깨우기(Wakeup)설명
wake_up(wq)TASK_NORMALexclusive 1개모든 non-exclusive + exclusive 1개
wake_up_interruptible(wq)TASK_INTERRUPTIBLEexclusive 1개INTERRUPTIBLE 상태만 대상
wake_up_all(wq)TASK_NORMAL전부모든 대기자를 깨움
wake_up_interruptible_all(wq)TASK_INTERRUPTIBLE전부INTERRUPTIBLE인 모든 대기자
wake_up_nr(wq, nr)TASK_NORMALnr개non-exclusive 전부 + exclusive nr개
wake_up_interruptible_nr(wq, nr)TASK_INTERRUPTIBLEnr개위와 동일, INTERRUPTIBLE만
/* include/linux/wait.h — wake_up 매크로 정의 */
#define wake_up(x)                  __wake_up(x, TASK_NORMAL, 1, NULL)
#define wake_up_interruptible(x)    __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
#define wake_up_all(x)              __wake_up(x, TASK_NORMAL, 0, NULL)
#define wake_up_nr(x, nr)           __wake_up(x, TASK_NORMAL, nr, NULL)

/* nr 인자의 의미:
   nr > 0: non-exclusive 전부 + exclusive nr개 깨움
   nr == 0: exclusive 포함 모든 대기자 깨움 (wake_up_all) */
__wake_up_common() 내부 순회 알고리즘 대기 리스트 순회 순서: HEAD non-excl #1 WAKE non-excl #2 WAKE exclusive #1 WAKE exclusive #2 SKIP wake_up(wq) = __wake_up(wq, TASK_NORMAL, nr_exclusive=1, NULL) → non-exclusive 전부 깨운 후, exclusive는 1개만 깨우고 중단 __wake_up_common() 의사코드: list_for_each_entry_safe(curr, next, &wq_head->head, entry) { if (curr->flags & WQ_FLAG_BOOKMARK) continue; ret = curr->func(curr, mode, wake_flags, key); /* func()이 try_to_wake_up() 호출 → 태스크 RUNNING 전환 */ if (ret < 0) break; if ((curr->flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive) break; /* exclusive 카운트 소진 → 중단 */ }
Non-exclusive 대기자를 모두 깨운 후, exclusive 대기자는 nr_exclusive 개수만큼만 깨우고 순회를 중단합니다

Exclusive vs Non-exclusive 웨이크업

Wait Queue의 핵심 설계 결정 중 하나는 누구를 깨울 것인가입니다. Non-exclusive는 모든 대기자를 깨우고, Exclusive는 선택적으로 하나만 깨웁니다.

삽입 순서와 리스트 레이아웃

/* kernel/sched/wait.c */
void add_wait_queue(wait_queue_head_t *wq_head,
                     wait_queue_entry_t *wq_entry)
{
    wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    __add_wait_queue(wq_head, wq_entry);  /* 리스트 앞에 추가 */
    spin_unlock_irqrestore(&wq_head->lock, flags);
}

void add_wait_queue_exclusive(wait_queue_head_t *wq_head,
                              wait_queue_entry_t *wq_entry)
{
    wq_entry->flags |= WQ_FLAG_EXCLUSIVE;
    spin_lock_irqsave(&wq_head->lock, flags);
    __add_wait_queue_entry_tail(wq_head, wq_entry);  /* 리스트 뒤에 추가 */
    spin_unlock_irqrestore(&wq_head->lock, flags);
}
삽입 위치의 의미: Non-exclusive가 앞, Exclusive가 뒤에 삽입됩니다. 순회는 앞에서 시작하므로, non-exclusive가 항상 먼저 깨어나고, exclusive 카운트가 소진되면 나머지 exclusive는 깨우지 않습니다.
특성Non-exclusiveExclusive
깨우기 범위항상 모두 깨움nr_exclusive 개수만
리스트 위치앞 (head)뒤 (tail)
전형적 사용poll/epoll, 일반 이벤트 통지accept(), I/O 대기
Thundering Herd발생 가능방지
APIadd_wait_queue()add_wait_queue_exclusive()

Thundering Herd 문제와 해결

Thundering Herd는 하나의 이벤트에 의해 다수의 대기자가 동시에 깨어나지만, 실제로는 한 태스크만 작업을 수행할 수 있어 나머지가 모두 다시 슬립하는 비효율을 말합니다.

Thundering Herd: Non-exclusive vs Exclusive Non-exclusive (Thundering Herd) 이벤트: 새 연결 도착 wake_up_all() → 10개 태스크 모두 깨움 T1 WIN T2 T3 T4 ... → T2~T10: 조건 검사 실패, 다시 슬립 9번의 불필요한 컨텍스트 스위치! 비용 = N × (wakeup + schedule + sleep) = 10 × ~10,000 cycles = ~100,000 cycles 낭비 Exclusive (해결) 이벤트: 새 연결 도착 wake_up() → exclusive 1개만 깨움 T1 WIN T2 zzz T3 zzz T4 zzz ... → T2~T10: 슬립 유지, 깨어나지 않음 불필요한 컨텍스트 스위치 0! 비용 = 1 × (wakeup + schedule) = ~10,000 cycles = 90% 절감
Non-exclusive 웨이크업은 모든 대기자를 깨워 불필요한 스케줄링을 유발하고, Exclusive 웨이크업은 하나만 깨워 이를 방지합니다

커널 내 Thundering Herd 방지 사례

서브시스템Wait Queue방지 기법
TCP accept()sk->sk_wqWQ_FLAG_EXCLUSIVE + SO_REUSEPORT
Pipe readpipe->rd_waitExclusive 웨이크업
블록 I/Oblk_mq_wait_queueExclusive 태그 대기
epollep->wqEPOLLEXCLUSIVE 플래그

wait_event 매크로 내부 구현

wait_event 매크로가 어떻게 안전한 조건 대기 루프를 구성하는지 추적합니다.

/* include/linux/wait.h — 단순화된 wait_event 구현 */
#define wait_event(wq_head, condition)                          \
do {                                                             \
    might_sleep();                                                 \
    if (condition)                                                \
        break;                 /* Fast path: 조건이 이미 참 */  \
    __wait_event(wq_head, condition);                             \
} while (0)

#define __wait_event(wq_head, condition)                         \
    (void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, \
                        0, 0, schedule())

#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({                                                                 \
    __label__ __out;                                              \
    struct wait_queue_entry __wq_entry;                            \
    long __ret = ret;                                              \
                                                                   \
    init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
    for (;;) {                                                     \
        long __int = prepare_to_wait_event(                      \
                         &wq_head, &__wq_entry, state);          \
        if (condition)                                             \
            break;             /* 조건 충족 → 탈출 */       \
        if (___wait_is_interruptible(state) && __int) {            \
            __ret = __int;                                         \
            goto __out;         /* 시그널 수신 → 중단 */     \
        }                                                          \
        cmd;                   /* schedule() 호출 → 슬립 */    \
    }                                                              \
    finish_wait(&wq_head, &__wq_entry);                          \
__out:  __ret;                                                     \
})
wait_event() 내부 실행 흐름도 might_sleep() — 원자적 컨텍스트 경고 condition 참? Yes (fast path) 즉시 반환 No init_wait_entry() — 스택에 엔트리 초기화 prepare_to_wait_event() — 대기열 등록 + 상태 설정 condition 참? Yes break → finish_wait No 시그널 수신? Yes (interruptible) -ERESTARTSYS No schedule() CPU 양보, 슬립 깨어나면 루프 반복 finish_wait() — 대기열 제거
wait_event 매크로는 fast path 조건 검사 → 대기열 등록 → 조건 재검사 → schedule() 루프를 반복합니다

prepare_to_wait_event() 핵심 동작

/* kernel/sched/wait.c */
long prepare_to_wait_event(wait_queue_head_t *wq_head,
                           wait_queue_entry_t *wq_entry, int state)
{
    unsigned long flags;
    long ret = 0;

    spin_lock_irqsave(&wq_head->lock, flags);
    /* 시그널 체크: interruptible 상태에서 pending signal 있으면 중단 */
    if (signal_pending_state(state, current)) {
        list_del_init(&wq_entry->entry);  /* 대기열에서 제거 */
        ret = -ERESTARTSYS;
    } else {
        if (list_empty(&wq_entry->entry)) {
            if (wq_entry->flags & WQ_FLAG_EXCLUSIVE)
                __add_wait_queue_entry_tail(wq_head, wq_entry);
            else
                __add_wait_queue(wq_head, wq_entry);
        }
        set_current_state(state);  /* 태스크 상태 변경 (메모리 배리어 포함) */
    }
    spin_unlock_irqrestore(&wq_head->lock, flags);
    return ret;
}
핵심 순서 보장(Ordering): set_current_state()에는 smp_store_mb()가 포함되어 있습니다. 이 배리어는 대기열 등록이 조건 검사보다 먼저 다른 CPU에 보이도록 보장합니다. 이 순서가 Lost Wakeup을 방지하는 핵심입니다.

커스텀 대기 함수 작성

wait_event 매크로가 제공하지 않는 복잡한 대기 패턴이 필요할 때, 직접 대기 루프를 작성할 수 있습니다.

/* 커스텀 대기 함수 — 수동 패턴 */
int my_custom_wait(wait_queue_head_t *wq, struct my_data *data)
{
    DEFINE_WAIT(wait);  /* autoremove_wake_function 사용 */
    int ret = 0;

    for (;;) {
        prepare_to_wait(wq, &wait, TASK_INTERRUPTIBLE);

        if (data->ready)  /* 조건 검사 */
            break;

        if (signal_pending(current)) {
            ret = -ERESTARTSYS;
            break;
        }

        /* 추가 로직: 타임아웃, 리소스 해제 등 */
        spin_unlock(&data->lock);  /* 외부 lock 해제 */
        schedule();                 /* 슬립 */
        spin_lock(&data->lock);    /* 깨어나면 lock 재획득 */
    }
    finish_wait(wq, &wait);
    return ret;
}
DEFINE_WAIT + autoremove: autoremove_wake_function()default_wake_function()을 호출한 뒤 list_del_init()으로 대기 리스트에서 자동 제거합니다. 따라서 루프 반복 시 prepare_to_wait()이 다시 등록합니다.

wait_woken() 패턴

/* 콜백 기반 대기 — wait_woken() + woken_wake_function */
int my_callback_wait(wait_queue_head_t *wq)
{
    DEFINE_WAIT_FUNC(wait, woken_wake_function);
    add_wait_queue(wq, &wait);

    while (!data_ready) {
        wait_woken(&wait, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
        /* WQ_FLAG_WOKEN이 설정되었을 때만 깨어남
           → spurious wakeup 방지 */
        if (signal_pending(current))
            break;
    }
    remove_wait_queue(wq, &wait);
    return 0;
}

poll/select/epoll과 Wait Queue 연결

VFS의 poll 파일 오퍼레이션은 Wait Queue를 통해 구현됩니다. poll_wait()가 파일의 Wait Queue에 콜백을 등록하고, 이벤트 발생 시 콜백이 select/poll/epoll을 깨웁니다.

/* 드라이버 poll 구현 예시 */
static __poll_t my_poll(struct file *filp,
                         struct poll_table_struct *wait)
{
    struct my_dev *dev = filp->private_data;
    __poll_t mask = 0;

    /* 1. wait queue에 poll 콜백 등록 */
    poll_wait(filp, &dev->read_wq, wait);
    poll_wait(filp, &dev->write_wq, wait);

    /* 2. 현재 이벤트 상태 반환 */
    if (dev->read_avail > 0)
        mask |= EPOLLIN | EPOLLRDNORM;
    if (dev->write_space > 0)
        mask |= EPOLLOUT | EPOLLWRNORM;

    return mask;
}

/* 데이터 도착 시: wake_up으로 poll 대기자 깨움 */
wake_up_interruptible(&dev->read_wq);
epoll과 Wait Queue 연결 구조 유저 프로세스 epoll_wait() struct eventpoll ep->wq (대기열) ep->rdllist (준비 리스트) VFS f_op->poll() poll_wait() 호출 → ep_ptable_queue_proc 디바이스 Wait Queue dev->read_wq / write_wq eppoll_entry wait.func = ep_poll_callback → rdllist에 추가 웨이크업 경로 (데이터 도착 시) wake_up(dev->read_wq) ep_poll_callback() rdllist에 epi 추가 wake_up(ep->wq) epoll_wait() 반환 → 유저 프로세스 실행
epoll은 디바이스의 Wait Queue에 ep_poll_callback을 등록하고, 이벤트 발생 시 콜백 체인을 통해 유저 프로세스를 깨웁니다

Simple Wait Queue (swait)

Simple Wait Queue (swait)는 일반 Wait Queue의 경량 대안입니다. NMI 안전성이 필요하거나, exclusive/커스텀 콜백이 불필요한 경우에 사용합니다.

특성wait_queue (일반)swait (단순)
Lockspinlock_traw_spinlock_t
NMI 안전아니오예 (raw_spinlock)
Exclusive 지원아니오
커스텀 콜백예 (func 포인터)아니오 (고정 동작)
BOOKMARK아니오
PREEMPT_RTspinlock → sleepingraw_spinlock 유지
구조체(Struct) 크기더 큼 (func 포인터)더 작음
주요 사용처범용 대기completion, VFIO, KVM
/* include/linux/swait.h */
struct swait_queue_head {
    raw_spinlock_t   lock;    /* NMI-safe raw spinlock */
    struct list_head  head;    /* 대기자 리스트 */
};

struct swait_queue {
    struct task_struct *task;  /* 대기 태스크 (private 대신 직접 참조) */
    struct list_head    node;  /* 리스트 연결 */
};

/* API */
DECLARE_SWAIT_QUEUE_HEAD(name);
init_swait_queue_head(&swq);
swake_up_one(&swq);         /* 하나만 깨움 (항상) */
swake_up_all(&swq);         /* 전부 깨움 */
swait_event(swq, condition);
swait_event_interruptible(swq, condition);
swait_event_timeout(swq, condition, timeout);
completion과 swait: 커널의 struct completion은 내부적으로 swait_queue_head를 사용합니다. 이는 completion이 RT 커널에서도 예측 가능한 동작을 보장하기 위함입니다.

Lost Wakeup 방지 패턴

Lost Wakeup은 조건 동기화에서 가장 치명적인 버그입니다. 깨우는 자의 wake_up이 대기자의 슬립보다 먼저 실행되어, 대기자가 영원히 깨어나지 못하는 상황입니다.

Lost Wakeup: 잘못된 순서 vs 올바른 순서 BUG: Lost Wakeup 발생 Waiter (CPU 0) Waker (CPU 1) if (!condition) ↑ 선점 발생! condition = true; wake_up(&wq); → 대기열 비어있음, 무효 set_current_state(...); add_wait_queue(...); schedule(); → 영원히 슬립! (Lost Wakeup) 원인: 조건 검사와 대기열 등록 사이에 wake_up이 끼어들었고, 이미 조건은 참인데 대기자는 이를 모르고 schedule()에 진입 올바른 순서 (wait_event 패턴) Waiter (CPU 0) Waker (CPU 1) add_wait_queue(...); set_current_state(...); ↑ smp_store_mb() 포함 if (!condition) schedule(); condition = true; wake_up(&wq); 경우 1: wake_up이 schedule() 전에 도착 → try_to_wake_up()이 RUNNING으로 설정 → schedule()이 즉시 반환 (이미 RUNNING) 경우 2: 조건 검사 시점에 이미 참 → if (!condition)이 거짓 → schedule() 스킵 → 어느 경우든 Lost Wakeup 불가!
올바른 순서: 대기열 등록 → set_current_state(배리어) → 조건 검사 → schedule(). wait_event 매크로가 이 순서를 자동으로 보장합니다.

깨우는 측의 규칙

/* 올바른 Waker 패턴 */
spin_lock(&data_lock);
data_ready = 1;           /* 1. 조건 설정 (store) */
spin_unlock(&data_lock);
wake_up(&wq);              /* 2. 깨우기 (반드시 조건 설정 후!) */

/* 잘못된 Waker 패턴 — 조건 설정 전에 wake_up */
wake_up(&wq);              /* BAD: 대기자가 조건=false를 볼 수 있음 */
data_ready = 1;            /* Lost Wakeup 위험! */

PREEMPT_RT 영향

PREEMPT_RT 커널에서 spinlock_t는 sleeping lock(rt_mutex 기반)으로 변환됩니다. 이는 Wait Queue 내부의 spinlock에도 영향을 미칩니다.

구성요소일반 커널PREEMPT_RT
wait_queue_head_t.lockspinlock_t (busy-wait)sleeping lock (rt_mutex)
swait_queue_head.lockraw_spinlock_traw_spinlock_t (변경 없음)
wake_up 원자적(Atomic) 컨텍스트가능일반 wait queue: 불가, swait: 가능
completionwait_queue 기반swait 기반 → NMI/hardirq 안전
RT 커널 주의: hardirq 핸들러(Handler)에서 일반 wake_up()을 호출하면 RT 커널에서 sleeping lock을 원자적 컨텍스트에서 잡으려는 BUG가 발생합니다. hardirq에서 깨우기가 필요하면 swake_up_one()을 사용하거나, threaded IRQ로 전환하세요.

실전 사용 패턴

패턴 1: 파이프 읽기 대기

/* fs/pipe.c — 파이프 읽기 (단순화) */
static ssize_t pipe_read(struct kiocb *iocb, struct iov_iter *to)
{
    struct pipe_inode_info *pipe = iocb->ki_filp->private_data;

    __pipe_lock(pipe);
    for (;;) {
        if (!pipe_empty(pipe->head, pipe->tail))
            break;  /* 데이터 있음 → 읽기 진행 */

        if (!pipe->writers) {  /* 모든 writer 닫힘 → EOF */
            ret = 0;
            break;
        }

        if (filp->f_flags & O_NONBLOCK) {  /* 논블로킹 */
            ret = -EAGAIN;
            break;
        }

        __pipe_unlock(pipe);
        /* 외부 락 해제 후 대기, 깨어나면 락 재획득 */
        wait_event_interruptible(pipe->rd_wait,
            !pipe_empty(pipe->head, pipe->tail) || !pipe->writers);
        __pipe_lock(pipe);
    }
    /* 데이터 복사... */
}

패턴 2: 소켓(Socket) 대기 (sk_sleep)

/* net/core/sock.c — 소켓 수신 대기 (단순화) */
static int sock_wait_data(struct sock *sk, long *timeo)
{
    DEFINE_WAIT(wait);

    prepare_to_wait(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);
    sk_set_bit(SOCKWQ_ASYNC_WAITDATA, sk);

    if (skb_queue_empty(&sk->sk_receive_queue))
        *timeo = schedule_timeout(*timeo);

    sk_clear_bit(SOCKWQ_ASYNC_WAITDATA, sk);
    finish_wait(sk_sleep(sk), &wait);
    return 0;
}

패턴 3: 블록 I/O 태그 대기

/* block/blk-mq-tag.c — 블록 I/O 태그 할당 대기 (단순화) */
static int bt_get(struct blk_mq_alloc_data *data, ...)
{
    struct sbq_wait_state *ws;
    DEFINE_SBQ_WAIT(wait);

    ws = bt_wait_ptr(bt, data->hctx);
    for (;;) {
        sbitmap_prepare_to_wait(bt, ws, &wait, TASK_UNINTERRUPTIBLE);
        tag = __blk_mq_get_tag(data, bt);
        if (tag != BLK_MQ_NO_TAG)
            break;
        blk_mq_tag_busy(data->hctx);
        io_schedule();    /* I/O 대기로 스케줄링 */
    }
    sbitmap_finish_wait(bt, ws, &wait);
    return tag;
}

안티패턴

#안티패턴문제해결
1조건 검사 전에 schedule()Lost Wakeup: 조건이 이미 참인데 슬립wait_event 매크로 사용 (항상 조건 먼저 검사)
2wake_up 후에 조건 설정Lost Wakeup: 대기자가 깨어나서 조건 검사 시 아직 거짓조건 설정 → wake_up 순서 엄수
3인터럽트 컨텍스트에서 wait_eventschedule() 호출 → BUG: scheduling while atomic인터럽트에서는 슬립 불가. bottom half로 위임
4wait_event에서 시그널 미처리사용자 프로세스가 SIGKILL에도 응답 않음 (unkillable)wait_event_interruptible 또는 _killable 사용
5finish_wait / remove_wait_queue 누락메모리 누수, dangling pointer모든 탈출 경로에서 cleanup 보장
6Non-exclusive로 다수 대기Thundering HerdWQ_FLAG_EXCLUSIVE 또는 EPOLLEXCLUSIVE
7set_current_state 없이 schedule()이미 TASK_RUNNING이라 schedule()이 즉시 반환, busy-loopprepare_to_wait*가 자동 설정
/* 안티패턴 #1: 조건 검사 없이 바로 슬립 */
set_current_state(TASK_INTERRUPTIBLE);
schedule();  /* BAD: 조건 검사 안 함 → Lost Wakeup 위험 */

/* 안티패턴 #4: 사용자 프로세스에서 UNINTERRUPTIBLE */
wait_event(my_wq, resource_available);
/* BAD: SIGKILL을 보내도 프로세스가 안 죽음 → D state
   사용자 프로세스에서는 _interruptible 또는 _killable 사용! */
Wait Queue 코드 리뷰 체크리스트 필수 확인 항목 1. wait_event 매크로를 사용하는가? 2. 사용자 프로세스에서 _interruptible 사용? 3. 조건이 wake_up 전에 설정되는가? 4. 프로세스 컨텍스트에서만 호출하는가? 5. finish_wait/remove_wait_queue 호출? 6. 다수 대기 시 exclusive 검토? 7. 타임아웃 반환값 정확히 처리? 8. RT 커널 호환성 고려? 9. lockdep 경고 없는가? 흔한 실수 set_current_state 후 조건 검사 누락 wake_up 전에 조건 미설정 시그널 처리 없는 _interruptible 사용 spinlock 보유 중 schedule() 호출 에러 경로에서 finish_wait 누락 timeout=0 전달 (즉시 반환, 무한이 아님) hardirq에서 일반 wake_up (RT 깨짐) Non-exclusive accept() (thundering herd) 스택 변수 wait_entry 후 return (use-after-free)
Wait Queue 사용 시 반드시 확인해야 할 항목과 흔한 실수 목록

디버깅(Debugging)

hung task 감지

TASK_UNINTERRUPTIBLE 상태로 장시간 대기하는 태스크는 hung task detector가 감지합니다.

/* kernel/hung_task.c — CONFIG_DETECT_HUNG_TASK */
/* 기본 120초간 TASK_UNINTERRUPTIBLE이면 경고 출력 */
/* /proc/sys/kernel/hung_task_timeout_secs로 조정 */

/* 커널 로그 예시: */
/* INFO: task kworker/0:1:1234 blocked for more than 120 seconds. */
/*       Not tainted 6.x.y */
/* "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this */
/* Call Trace: */
/*  schedule+0x... */
/*  wait_event+0x... */

wchan과 /proc/PID/wchan

# 프로세스가 어디서 대기 중인지 확인
$ cat /proc/1234/wchan
pipe_read

# 모든 D-state 프로세스 나열
$ ps aux | awk '$8 ~ /D/'
root  1234  0.0  0.1  ... D  ...  pipe_read

# /proc/PID/stack으로 전체 콜스택 확인
$ cat /proc/1234/stack
[<0>] pipe_read+0x1a0/0x300
[<0>] vfs_read+0x128/0x1c0
[<0>] ksys_read+0x6c/0xf0

ftrace로 Wait Queue 이벤트 추적

# sched_switch 이벤트로 태스크 상태 전환 추적
$ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_switch/enable
$ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

# 특정 프로세스만 필터링
$ echo "prev_pid == 1234 || next_pid == 1234" > \
  /sys/kernel/debug/tracing/events/sched/sched_switch/filter

# 결과 확인
$ cat /sys/kernel/debug/tracing/trace
#           TASK-PID    CPU#  TIMESTAMP  FUNCTION
          bash-1234   [001]  123.456:  sched_switch: prev_state=S ==> next_pid=0
       kworker-5678   [001]  123.789:  sched_wakeup: comm=bash pid=1234 target_cpu=001

SysRq-W: 대기 중인 태스크 덤프(Dump)

# Alt+SysRq+W 또는:
$ echo w > /proc/sysrq-trigger

# dmesg에서 TASK_UNINTERRUPTIBLE인 모든 태스크의 콜스택 출력
# wait_event에서 블로킹된 태스크를 빠르게 식별하는 데 유용

성능 특성

작업비용 (cycles, 대략)설명
wait_event fast path~50조건이 이미 참: 매크로 검사만
add_wait_queue~200spinlock 획득 + 리스트 삽입 + 해제
schedule() + context switch~5,000–10,000레지스터(Register) 저장, 런큐(Runqueue) 조작, TLB
wake_up (1 waiter)~1,000–3,000spinlock + try_to_wake_up + enqueue
wake_up_all (N waiters)~N × 2,000N개 태스크 각각 try_to_wake_up
swait vs wait_queue~10–20% 빠름함수 포인터 호출 생략, 더 작은 구조체
Wait Queue 경합 수준별 성능 영향 지연시간 (cycles) 동시 대기자 수 1 ~5K 10 ~20K ~7K 100 ~200K ~12K 1000 ~2M ~30K wake_up_all (Non-exclusive) wake_up (Exclusive)
대기자가 많을수록 wake_up_all의 비용은 선형으로 증가하지만, exclusive 웨이크업은 거의 일정합니다

최적화 팁

/* 대기자 존재 확인 후 wake_up (최적화) */
if (waitqueue_active(&dev->wq))
    wake_up(&dev->wq);

/* 주의: waitqueue_active()는 lock 없이 리스트를 확인하므로
   정확한 결과를 보장하지 않습니다.
   "불필요한 wake_up 호출을 대부분 제거"하는 힌트로만 사용하세요.
   안전성은 wake_up 자체가 보장합니다. */

커널 설정

설정기본값설명
CONFIG_DETECT_HUNG_TASKyTASK_UNINTERRUPTIBLE 장기 블로킹 감지. /proc/sys/kernel/hung_task_timeout_secs로 임계값 조정 (기본 120초)
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT120hung task 감지 기본 타임아웃 (초)
CONFIG_PREEMPT_RTnRT 커널. wait_queue_head_t의 spinlock이 sleeping lock으로 변환
CONFIG_DEBUG_MUTEXESn디버그 빌드에서 슬립 가능 잠금(Lock) 관련 경고 강화
CONFIG_PROVE_LOCKINGnlockdep 활성화. wait_queue spinlock의 잠금 순서 검증
CONFIG_SCHED_DEBUGy/sys/kernel/debug/sched/에 스케줄러 통계 노출
CONFIG_SCHEDSTATSywait time, run time 등 스케줄링 통계 수집
관련 커널 설정 관계도 PREEMPT_RT 영향 wait_queue spinlock → sleeping lock 변환 DETECT_HUNG_TASK UNINTERRUPTIBLE 장기 대기 감지 + 콜스택 출력 PROVE_LOCKING lockdep: wq->lock 잠금 순서 검증 + 교착 조기 감지 SCHEDSTATS wait/run time 통계
PREEMPT_RT는 wait_queue의 spinlock 동작을 변경하고, DETECT_HUNG_TASK과 PROVE_LOCKING은 디버깅을 지원합니다

wait_event 매크로 전개 소스 분석

wait_event 매크로는 3단계로 전개됩니다. 최상위 wait_event__wait_event___wait_event로 확장되며, 최종적으로 prepare_to_wait_event/finish_wait를 호출하는 for 루프를 생성합니다. 각 단계의 역할을 커널 소스 기반으로 분석합니다.

1단계: wait_event → might_sleep + fast path

/* include/linux/wait.h — 1단계 전개 */
#define wait_event(wq_head, condition)                          \
do {                                                             \
    might_sleep();                                               \
    if (condition)                                                \
        break;                                                   \
    __wait_event(wq_head, condition);                             \
} while (0)

/* might_sleep()의 역할:
   - CONFIG_DEBUG_ATOMIC_SLEEP 활성화 시 원자적 컨텍스트(hardirq, softirq,
     spinlock 보유 등)에서 호출 시 경고 + 콜스택 출력
   - preempt_count()를 검사하여 선점 비활성화 여부를 판단
   - 디버그 빌드에서만 동작, 릴리스에서는 no-op */

2단계: __wait_event → ___wait_event 파라미터 바인딩

/* include/linux/wait.h — 2단계: 변형별 파라미터 전달 */
#define __wait_event(wq_head, condition)                         \
    (void)___wait_event(wq_head, condition,                       \
        TASK_UNINTERRUPTIBLE, 0, 0, schedule())

#define __wait_event_interruptible(wq_head, condition)           \
    ___wait_event(wq_head, condition,                             \
        TASK_INTERRUPTIBLE, 0, 0, schedule())

#define __wait_event_killable(wq_head, condition)                \
    ___wait_event(wq_head, condition,                             \
        TASK_KILLABLE, 0, 0, schedule())

#define __wait_event_timeout(wq_head, condition, timeout)        \
    ___wait_event(wq_head, condition,                             \
        TASK_UNINTERRUPTIBLE, 0, timeout,                          \
        __ret = schedule_timeout(__ret))

/* 각 변형이 전달하는 파라미터:
   state     : TASK_UNINTERRUPTIBLE / INTERRUPTIBLE / KILLABLE
   exclusive : 0 (일반) 또는 WQ_FLAG_EXCLUSIVE
   ret       : 0 (무한) 또는 timeout jiffies
   cmd       : schedule() 또는 schedule_timeout() */

3단계: ___wait_event 핵심 루프

/* include/linux/wait.h — 3단계: 실제 대기 루프 생성 */
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({                                                                   \
    __label__ __out;                                                \
    struct wait_queue_entry __wq_entry;                              \
    long __ret = ret;       /* timeout 초기값 또는 0 */              \
                                                                     \
    init_wait_entry(&__wq_entry,                                    \
                    exclusive ? WQ_FLAG_EXCLUSIVE : 0);              \
    for (;;) {                                                       \
        /* (A) 대기열 등록 + 태스크 상태 설정 */                       \
        long __int = prepare_to_wait_event(                        \
                        &wq_head, &__wq_entry, state);              \
                                                                     \
        /* (B) 조건 재검사 — 배리어 후이므로 최신값 확인 */             \
        if (condition)                                               \
            break;                                                   \
                                                                     \
        /* (C) 시그널 검사 — interruptible 상태에서만 유효 */          \
        if (___wait_is_interruptible(state) && __int) {              \
            __ret = __int;         /* -ERESTARTSYS */               \
            goto __out;                                               \
        }                                                            \
                                                                     \
        /* (D) 슬립 — schedule() 또는 schedule_timeout() */           \
        cmd;                                                         \
    }                                                                \
    finish_wait(&wq_head, &__wq_entry);                            \
__out:  __ret;                                                       \
})

/* init_wait_entry() 내부:
   .private = current
   .func    = autoremove_wake_function
   .flags   = exclusive_flag
   .entry   = LIST_HEAD_INIT()                */
___wait_event 매크로 전개 단계 1단계: wait_event() might_sleep() fast path 조건 검사 2단계: __wait_event() state, exclusive, ret, cmd 파라미터 바인딩 3단계: ___wait_event() 실제 for(;;) 루프 생성 ___wait_event 전개 결과 (for 루프) (A) prepare_to_wait_event: spinlock + list_add + set_current_state (B) condition 검사: 참이면 break → finish_wait (C) 시그널 검사: interruptible + pending → goto __out (-ERESTARTSYS) (D) cmd 실행: schedule() 또는 schedule_timeout() → 슬립 깨어나면 반복 finish_wait: TASK_RUNNING 복원 + list_del_init
wait_event 매크로는 3단계 전개를 거쳐 prepare_to_wait_event → 조건검사 → 시그널검사 → schedule() 루프를 생성합니다

finish_wait() 소스 분석

/* kernel/sched/wait.c */
void finish_wait(wait_queue_head_t *wq_head,
                 wait_queue_entry_t *wq_entry)
{
    unsigned long flags;

    __set_current_state(TASK_RUNNING);
    /*
     * __set_current_state()는 배리어 없는 버전.
     * schedule()에서 복귀한 직후이므로 이미 RUNNING이거나,
     * break로 탈출했을 때만 여기 도달 → 배리어 불필요.
     */

    if (!list_empty_careful(&wq_entry->entry)) {
        spin_lock_irqsave(&wq_head->lock, flags);
        list_del_init(&wq_entry->entry);
        spin_unlock_irqrestore(&wq_head->lock, flags);
    }
}

/* list_empty_careful()은 lock 없이 리스트 비어있는지 확인.
   autoremove_wake_function이 이미 제거했을 수 있으므로,
   불필요한 spinlock 획득을 피하는 최적화입니다. */
참고: autoremove_wake_function이 대기자를 깨울 때 이미 list_del_init을 호출하므로, finish_waitlist_empty_careful 체크가 대부분 참이 되어 spinlock을 잡지 않습니다. 이것이 DEFINE_WAIT + autoremove 조합의 성능 이점입니다.

__wake_up 소스 분석

wake_up()은 매크로로 __wake_up()을 호출하고, 내부적으로 __wake_up_common_lock()__wake_up_common()으로 이어집니다. bookmark 최적화와 nr_exclusive 로직을 소스 레벨에서 추적합니다.

__wake_up() 진입점(Entry Point)

/* kernel/sched/wait.c */
void __wake_up(wait_queue_head_t *wq_head,
               unsigned int mode, int nr_exclusive, void *key)
{
    __wake_up_common_lock(wq_head, mode, nr_exclusive, 0, key);
}

static void __wake_up_common_lock(wait_queue_head_t *wq_head,
    unsigned int mode, int nr_exclusive, int wake_flags, void *key)
{
    unsigned long flags;
    wait_queue_entry_t bookmark;

    bookmark.flags = 0;
    bookmark.private = NULL;
    bookmark.func = NULL;
    INIT_LIST_HEAD(&bookmark.entry);

    do {
        spin_lock_irqsave(&wq_head->lock, flags);
        nr_exclusive = __wake_up_common(wq_head, mode,
                            nr_exclusive, wake_flags, key, &bookmark);
        spin_unlock_irqrestore(&wq_head->lock, flags);
    } while (bookmark.flags & WQ_FLAG_BOOKMARK);
}

/* bookmark 루프의 의미:
   __wake_up_common()이 긴 리스트 순회 중 spinlock을 잠시 해제해야 할 때,
   현재 위치를 bookmark에 기록하고 반환합니다.
   외부 루프가 lock을 다시 잡고 bookmark 위치부터 순회를 이어갑니다. */

__wake_up_common() 핵심 순회

/* kernel/sched/wait.c */
static int __wake_up_common(
    wait_queue_head_t *wq_head, unsigned int mode,
    int nr_exclusive, int wake_flags, void *key,
    wait_queue_entry_t *bookmark)
{
    wait_queue_entry_t *curr, *next;

    lockdep_assert_held(&wq_head->lock);

    if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
        curr = list_next_entry(bookmark, entry);
        list_del(&bookmark->entry);
        bookmark->flags = 0;
    } else {
        curr = list_first_entry(&wq_head->head,
                    wait_queue_entry_t, entry);
    }

    list_for_each_entry_safe_from(curr, next,
                                  &wq_head->head, entry) {
        unsigned flags = curr->flags;
        int ret;

        if (flags & WQ_FLAG_BOOKMARK)
            continue;

        ret = curr->func(curr, mode, wake_flags, key);
        /* func는 보통 default_wake_function 또는
           autoremove_wake_function
           → try_to_wake_up() 호출 */

        if (ret < 0)
            break;  /* 콜백이 음수 반환 → 순회 중단 */

        if ((flags & WQ_FLAG_EXCLUSIVE) &&
            !--nr_exclusive)
            break;  /* exclusive 카운트 소진 → 중단 */
    }

    return nr_exclusive;
}
__wake_up_common: bookmark 기반 순회 1차 순회 (spin_lock 보유) wq #1 wq #2 wq #3 BOOKMARK 삽입 try_to_wake_up()이 오래 걸릴 수 있어 spinlock 보유 시간을 제한하기 위해 bookmark 삽입 후 lock 해제 spin_unlock → spin_lock 2차 순회 (bookmark 위치부터 재개) BOOKMARK 제거 wq #4 wq #5 완료 bookmark 다음 위치부터 순회 재개 func() → try_to_wake_up() 흐름 1. p->state & mode 검사 2. TASK_RUNNING으로 전환 3. 타겟 CPU 선택 (select_task_rq) 4. enqueue_task (런큐 삽입) 5. 필요 시 resched IPI 전송 반환값: 1 = 성공 (태스크 깨움) 0 = 실패 (상태 불일치)
bookmark 최적화로 긴 대기 리스트에서 spinlock 보유 시간을 제한하고, try_to_wake_up이 실제 태스크 깨우기를 수행합니다

autoremove_wake_function() 분석

/* kernel/sched/wait.c */
int autoremove_wake_function(struct wait_queue_entry *wq_entry,
                             unsigned mode, int sync, void *key)
{
    int ret = default_wake_function(wq_entry, mode, sync, key);

    if (ret)
        list_del_init_careful(&wq_entry->entry);
    /* 깨우기 성공 시 대기 리스트에서 즉시 제거.
       list_del_init_careful은 list_empty_careful과 쌍으로 동작하여
       finish_wait에서 불필요한 spinlock 획득을 방지합니다. */

    return ret;
}
nr_exclusive 동작 정리: wake_up()은 nr_exclusive=1로 호출됩니다. 순회 중 non-exclusive 엔트리는 nr_exclusive를 감소시키지 않으므로 모두 깨웁니다. exclusive 엔트리를 만나면 깨우고 --nr_exclusive가 0이 되어 순회를 중단합니다. 결과: 모든 non-exclusive + exclusive 1개.

epoll 심층: eventpoll과 wait queue 연결

epoll은 Wait Queue의 가장 정교한 사용자 중 하나입니다. ep_poll_callback이 디바이스의 Wait Queue와 eventpoll을 연결하고, rdllist(ready list)와 ovflist(overflow list)로 이벤트를 효율적으로 관리합니다.

epoll 핵심 자료구조와 WQ 연결

/* fs/eventpoll.c */
struct eventpoll {
    spinlock_t        lock;       /* rdllist/ovflist 보호 */
    struct mutex       mtx;        /* epoll_ctl 직렬화 */
    wait_queue_head_t wq;         /* epoll_wait() 대기열 */
    wait_queue_head_t poll_wait;  /* epoll 자체가 poll 대상일 때 */
    struct list_head   rdllist;    /* 준비된 epitem 리스트 */
    struct rb_root_cached rbr;     /* 감시 대상 epitem 레드블랙 트리 */
    struct epitem     *ovflist;    /* lock 보유 중 오버플로 리스트 */
};

struct epitem {
    struct rb_node        rbn;      /* rbr 트리 노드 */
    struct list_head      rdllink;   /* rdllist 연결 */
    struct epoll_filefd   ffd;       /* (file *, fd) 쌍 */
    struct list_head      pwqlist;   /* eppoll_entry 리스트 */
    struct eventpoll     *ep;        /* 소유 eventpoll */
    struct epoll_event    event;     /* 유저 지정 이벤트 마스크 */
};

struct eppoll_entry {
    struct eppoll_entry  *next;     /* 다음 eppoll_entry */
    struct epitem        *base;     /* 소유 epitem */
    wait_queue_entry_t   wait;     /* 디바이스 WQ에 등록되는 엔트리 */
    wait_queue_head_t   *whead;    /* 등록된 WQ 헤드 */
};

ep_poll_callback() 소스 분석

/* fs/eventpoll.c — 디바이스 WQ 콜백 */
static int ep_poll_callback(wait_queue_entry_t *wait,
    unsigned mode, int sync, void *key)
{
    struct eppoll_entry *pwq = container_of(
        wait, struct eppoll_entry, wait);
    struct epitem *epi = pwq->base;
    struct eventpoll *ep = epi->ep;
    __poll_t pollflags = (__poll_t)(unsigned long)key;

    spin_lock_irqsave(&ep->lock, flags);

    /* 이벤트 마스크 검사 */
    if (pollflags && !(pollflags & epi->event.events))
        goto out_unlock;

    /* ovflist 활성 시 오버플로 리스트에 추가 */
    if (READ_ONCE(ep->ovflist) != EP_UNACTIVE_PTR) {
        if (epi->next == EP_UNACTIVE_PTR) {
            epi->next = READ_ONCE(ep->ovflist);
            WRITE_ONCE(ep->ovflist, epi);
        }
        goto out_unlock;
    }

    /* rdllist에 추가 (아직 없으면) */
    if (!ep_is_linked(epi))
        list_add_tail(&epi->rdllink, &ep->rdllist);

    /* epoll_wait 대기자 깨우기 */
    if (waitqueue_active(&ep->wq))
        wake_up(&ep->wq);

out_unlock:
    spin_unlock_irqrestore(&ep->lock, flags);
    return 1;
}
epoll 다층 Wait Queue 연결 구조 epoll_wait(epfd) 유저 프로세스 슬립 대기 struct eventpoll ep->wq ep->rdllist ep->rbr (레드블랙) ep->ovflist: rdllist 순회 중 도착한 이벤트의 임시 리스트 struct epitem (fd당 1개) rdllink → rdllist 연결 pwqlist → eppoll_entry 리스트 event.events: EPOLLIN | EPOLLOUT | ... struct eppoll_entry wait.func = ep_poll_callback 디바이스 WQ dev->read_wq 등록 wake_up ep_poll_callback rdllist에 epi 추가 rdllist 추가 + wake_up(ep->wq)
epoll은 eppoll_entry를 통해 디바이스 WQ에 콜백을 등록하고, 이벤트 발생 시 ep_poll_callback이 rdllist에 추가 후 epoll_wait 대기자를 깨웁니다

Level Trigger vs Edge Trigger

특성Level Trigger (기본)Edge Trigger (EPOLLET)
이벤트 보고조건이 유지되는 동안 반복 보고상태 변화 시 1회만 보고
rdllist 동작epoll_wait 반환 후 다시 rdllist에 추가반환 후 rdllist에서 제거, 재추가 없음
구현ep_send_events에서 poll 재호출poll 재호출 없이 즉시 삭제
사용 주의안전, 데이터 유실 위험 낮음EAGAIN까지 읽어야 데이터 유실 방지
/* fs/eventpoll.c — ep_send_events_proc (단순화) */
if (!(epi->event.events & EPOLLET)) {
    /* Level Trigger: rdllist에 다시 추가하여
       다음 epoll_wait에서도 이벤트가 보고되도록 함 */
    list_add_tail(&epi->rdllink, &ep->rdllist);
    ep_pm_stay_awake(epi);
}
주의: EPOLLEXCLUSIVE는 여러 epoll 인스턴스가 동일한 fd를 감시할 때 Thundering Herd를 방지합니다. EPOLLET과 조합하면 단 하나의 epoll 인스턴스만 이벤트를 수신하며, 그 인스턴스의 epoll_wait만 깨어납니다.

io_uring과 wait queue

io_uring은 submission queue(SQ)와 completion queue(CQ)를 유저-커널 공유 메모리로 구현하되, CQ에 완료 이벤트가 없을 때의 대기에 Wait Queue를 활용합니다. io_cqring_wait()task_work 기반 완료 통지를 분석합니다.

io_cqring_wait() 대기 경로

/* io_uring/io_uring.c — CQ 대기 (단순화) */
static int io_cqring_wait(struct io_ring_ctx *ctx,
                           int min_events, ...)
{
    struct io_wait_queue iowq;

    /* Fast path: CQ에 이미 충분한 이벤트가 있으면 즉시 반환 */
    if (io_cqring_events(ctx) >= min_events)
        return 0;

    /* 대기 구조체 초기화 */
    init_waitqueue_func_entry(&iowq.wq, io_wake_function);
    iowq.wq.private = current;
    iowq.nr_timeouts = atomic_read(&ctx->cq_timeouts);

    do {
        /* task_work 처리 — 완료된 I/O 결과를 CQ에 반영 */
        if (io_run_task_work())
            continue;

        prepare_to_wait_exclusive(&ctx->cq_wait,
            &iowq.wq, TASK_INTERRUPTIBLE);

        if (io_cqring_events(ctx) >= min_events)
            break;

        if (signal_pending(current)) {
            ret = -EINTR;
            break;
        }

        schedule();
    } while (1);

    finish_wait(&ctx->cq_wait, &iowq.wq);
    return ret;
}

SQPOLL 모드와 대기 전략

모드Wait Queue 사용깨우기 메커니즘지연(Latency)시간
기본 모드ctx->cq_wait에서 대기I/O 완료 → io_cq_wake~수 μs (context switch)
SQPOLL커널 스레드(Kernel Thread)가 SQ 폴링(Polling)SQ doorbell (mmio write)~수백 ns (no syscall)
SQPOLL idlesqd->wait에서 슬립io_uring_enterwake_upidle 후 깨우기 시 ~수 μs
io_uring Completion 경로와 Wait Queue 유저 프로세스 io_uring_enter() IORING_ENTER_GETEVENTS io_cqring_wait() ctx->cq_wait에서 슬립 I/O 제출 (SQ) io_submit_sqes → 커널 I/O I/O 완료 (IRQ/softirq) bio_endio → io_complete_rw CQE 기록 io_fill_cqe → 공유 CQ 링 wake_up SQPOLL 모드 커널 스레드가 SQ 폴링 syscall 없이 제출 가능 idle 시 sqd->wait에서 슬립 SQ 항목 추가 시 깨어남 task_work 기반 완료 통지 (최적 경로) io_req_task_complete → task_work_add → TWA_SIGNAL → 태스크 복귀 시 CQE 처리 context switch 없이 완료 이벤트를 태스크에 전달 — 최소 지연
io_uring은 CQ 대기에 wait queue를 사용하고, task_work으로 context switch 없이 완료를 전달하는 최적 경로도 제공합니다
참고: io_uringio_wake_functiondefault_wake_function과 다르게 CQ 이벤트 수를 확인하여 불필요한 깨우기를 방지합니다. min_events가 충족되지 않으면 깨우기를 거부(ret=0)하여 spurious wakeup을 줄입니다.

메모리 순서: prepare_to_wait 배리어

Wait Queue의 정확성은 메모리 순서 보장에 달려 있습니다. set_current_state()smp_store_mb()try_to_wake_up()의 배리어가 어떻게 Lost Wakeup을 방지하는지 분석합니다.

대기자 측 배리어: set_current_state

/* include/linux/sched.h */
#define set_current_state(state_value)                            \
    smp_store_mb(current->__state, (state_value))

/* smp_store_mb()는 WRITE + 풀 메모리 배리어:
   1. current->__state에 state_value를 저장 (store)
   2. 이전의 모든 메모리 접근이 완료됨을 보장 (mb)
   3. 이후의 메모리 접근이 이 store 이전에 실행되지 않음을 보장

   중요: 대기열 등록(list_add)이 set_current_state 이전에 완료되고,
         조건 검사(load)가 set_current_state 이후에 실행되도록 보장 */

/* 대비: __set_current_state()는 배리어 없음 — finish_wait()에서 사용 */
#define __set_current_state(state_value)                          \
    WRITE_ONCE(current->__state, (state_value))

깨우는 측 배리어: try_to_wake_up

/* kernel/sched/core.c — try_to_wake_up() 내부 배리어 (단순화) */
static int try_to_wake_up(struct task_struct *p,
                           unsigned int state, int wake_flags)
{
    /*
     * smp_mb__after_spinlock()이 다음을 보장:
     *
     * [대기자 CPU]                    [깨우는 CPU]
     * list_add(&wq, &head);         condition = true;
     * set_current_state(TASK_*);     try_to_wake_up():
     *   smp_store_mb()                 raw_spin_lock(p->pi_lock);
     *     STORE state                  smp_mb__after_spinlock();
     *     FULL BARRIER                 if (p->state & state)
     * if (!condition)                    p->state = TASK_RUNNING;
     *     schedule();
     *
     * 이 배리어 쌍이 보장하는 것:
     * - 깨우는 측이 condition=true를 저장한 후 state를 읽을 때,
     *   대기자의 state 변경이 보입니다.
     * - 대기자가 state를 변경한 후 condition을 읽을 때,
     *   깨우는 측의 condition=true가 보입니다.
     */

    raw_spin_lock_irqsave(&p->pi_lock, flags);
    smp_mb__after_spinlock();

    if (!(p->__state & state))
        goto unlock;

    /* TASK_RUNNING으로 전환, 런큐에 삽입 */
    WRITE_ONCE(p->__state, TASK_RUNNING);
    /* ... enqueue_task ... */
}
메모리 배리어로 Lost Wakeup 방지 대기자 CPU (Waiter) STORE: list_add(wq_entry, &head) smp_store_mb (FULL BARRIER) STORE: current->state = TASK_* LOAD: condition (최신값 보장) 경우 A: condition=true 확인 → break (schedule 안 함) 경우 B: condition=false → schedule() → state 변경이 try_to_wake_up에 보임 → RUNNING 전환 → 깨어남 깨우는 CPU (Waker) STORE: condition = true wake_up() → try_to_wake_up() spin_lock(p->pi_lock) smp_mb__after_spinlock (BARRIER) LOAD: p->state (최신값 보장) state가 TASK_RUNNING이 아니면: → RUNNING으로 전환 + 런큐 삽입 state가 이미 RUNNING이면: → 이미 깨어있으므로 무시 배리어 대칭
대기자의 smp_store_mb와 깨우는 측의 smp_mb__after_spinlock이 대칭 배리어 쌍을 형성하여 Lost Wakeup을 원천 방지합니다
주의: __set_current_state()는 배리어가 없으므로 조건 검사 전에 사용하면 안 됩니다. finish_wait()에서 TASK_RUNNING 복원에만 사용하는 것이 안전합니다 (이 시점에는 이미 깨어났으므로 순서가 중요하지 않습니다).

벤치마크: wakeup 지연과 처리량(Throughput)

Wait Queue의 실제 성능 특성을 정량적으로 측정합니다. wakeup 지연시간, 단일 vs 일괄 깨우기 비용, exclusive vs non-exclusive 오버헤드를 비교합니다.

Wakeup 지연 측정 방법

/* 커널 모듈 벤치마크: wake_up 지연 측정 */
static u64 measure_wakeup_latency(void)
{
    u64 t1, t2;
    DECLARE_WAIT_QUEUE_HEAD(bench_wq);
    struct task_struct *sleeper;

    /* 대기 태스크 생성 (별도 kthread) */
    sleeper = kthread_run(bench_sleeper_fn, &bench_wq, "bench");
    msleep(10);  /* 슬립 진입 대기 */

    /* 깨우기 지연 측정 */
    t1 = ktime_get_ns();
    wake_up(&bench_wq);
    /* sleeper 측에서 ktime_get_ns()로 t2 기록 */

    return t2 - t1;  /* 나노초 단위 지연 */
}

/* 대기자 kthread */
static int bench_sleeper_fn(void *data)
{
    wait_queue_head_t *wq = data;

    wait_event(*wq, kthread_should_stop() || woken);
    t2 = ktime_get_ns();
    return 0;
}

측정 결과 (x86_64, 4GHz, PREEMPT 커널)

시나리오지연 (μs)Cycles비고
wake_up → 같은 CPU (캐시(Cache) hot)~1.5~6,000IPI 불필요
wake_up → 다른 CPU (같은 소켓)~3.5~14,000IPI + L3 캐시 전송
wake_up → 다른 소켓 (NUMA)~8.0~32,000IPI + 인터커넥트 지연
wake_up_all, 10 waiters~15~60,00010× try_to_wake_up
wake_up_all, 100 waiters~180~720,000spinlock 경합(Contention) 증가
wake_up, 1 exclusive (100 대기 중)~3.5~14,0001개만 깨움, 일정 비용
swake_up_one (1 waiter)~1.2~4,800raw_spinlock + 간소 경로
wait_event fast path (조건 참)~0.01~50조건 검사만, 슬립 없음
Wakeup 지연 비교 (로그 스케일, μs) 지연시간 (μs) 0.01 1 5 15 180 fast path swake 1.2μs same CPU 1.5μs cross CPU 3.5μs NUMA 8μs all ×10 15μs all ×100 180μs excl 3.5μs exclusive 웨이크업은 대기자 수에 관계없이 일정한 비용을 유지합니다
wake_up_all의 비용은 대기자 수에 선형 비례하지만, exclusive 웨이크업은 대기자 수와 무관하게 일정합니다
실전 지침: 대기자가 10개 이상이 될 수 있고 하나만 처리할 수 있는 이벤트라면 반드시 exclusive 웨이크업을 사용하세요. 100개 대기자 기준으로 wake_up_all 대비 50배 이상 비용 절감이 가능합니다.

kprobe/tracepoint 기반 모니터링

Wait Queue 관련 문제를 진단하려면 런타임 모니터링이 필수입니다. 커널 tracepoint, kprobe, bpftrace를 활용한 Wait Queue 모니터링 기법을 다룹니다.

sched:sched_wakeup tracepoint

# sched_wakeup 트레이스포인트 활성화
$ echo 1 > /sys/kernel/debug/tracing/events/sched/sched_wakeup/enable

# 특정 커널 함수에서의 wakeup만 필터링
$ echo 'comm == "my_daemon"' > \
  /sys/kernel/debug/tracing/events/sched/sched_wakeup/filter

# 트레이스 결과 확인
$ cat /sys/kernel/debug/tracing/trace
# sched_wakeup: comm=my_daemon pid=1234 prio=120 target_cpu=002
#   <...>-5678 [001] 123.456789: sched_wakeup: ...

# wakeup 소스 추적 (function_graph)
$ echo function_graph > /sys/kernel/debug/tracing/current_tracer
$ echo __wake_up > /sys/kernel/debug/tracing/set_graph_function
$ cat /sys/kernel/debug/tracing/trace
#  1)               |  __wake_up() {
#  1)               |    __wake_up_common_lock() {
#  1)   0.850 us    |      __wake_up_common();
#  1)   1.200 us    |    }
#  1)   1.450 us    |  }

kprobe로 __wake_up 인자 추적

# kprobe 등록: __wake_up의 인자 확인
$ echo 'p:wq_wakeup __wake_up wq=%di mode=%si nr=%dx' > \
  /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/wq_wakeup/enable

# kretprobe: prepare_to_wait_event 반환값 추적
$ echo 'r:ptw_ret prepare_to_wait_event ret=$retval' > \
  /sys/kernel/debug/tracing/kprobe_events
$ echo 1 > /sys/kernel/debug/tracing/events/kprobes/ptw_ret/enable

# 정리
$ echo > /sys/kernel/debug/tracing/kprobe_events

bpftrace: wakeup 지연 히스토그램

# bpftrace로 wakeup 지연 분포 측정
$ bpftrace -e '
tracepoint:sched:sched_wakeup
{
    @start[args->pid] = nsecs;
}

tracepoint:sched:sched_switch
/args->prev_state == 0 && @start[args->next_pid]/
{
    $latency = nsecs - @start[args->next_pid];
    @usecs = hist($latency / 1000);
    delete(@start[args->next_pid]);
}

END { clear(@start); }
'

# 출력 예시:
# @usecs:
# [1, 2)    1234 |@@@@@@@@@@@@@@@@@@@@
# [2, 4)    5678 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# [4, 8)    3456 |@@@@@@@@@@@@@@@@@@@@@@@@@@@
# [8, 16)    987 |@@@@@@@@@
# [16, 32)   234 |@@
# [32, 64)    45 |
Wait Queue 모니터링 도구 선택 가이드 무엇을 확인하고 싶은가? 어디서 대기 중인가? /proc/PID/wchan, stack 얼마나 오래 대기? schedstats, bpftrace hist 누가 깨우는가? sched_wakeup tracepoint cat /proc/PID/wchan cat /proc/PID/stack echo w > /proc/sysrq-trigger bpftrace sched_switch hist /proc/schedstat perf sched latency sched:sched_wakeup trace kprobe on __wake_up function_graph tracer 장기 블로킹 감지 CONFIG_DETECT_HUNG_TASK + /proc/sys/kernel/hung_task_timeout_secs
목적에 따라 적절한 모니터링 도구를 선택합니다: 위치 확인은 wchan/stack, 지연 측정은 bpftrace/schedstat, 원인 추적은 tracepoint/kprobe

perf sched으로 스케줄링 지연 분석

# perf sched record로 스케줄링 이벤트 수집
$ perf sched record -- sleep 10

# 태스크별 wakeup 지연 통계
$ perf sched latency
 -----------------------------------------------------------------
  Task           |   Runtime ms | Switches | Avg delay ms |
 -----------------------------------------------------------------
  my_daemon:1234 |     50.234   |     150  |    2.345     |
  kworker/0:1    |     12.456   |      80  |    0.567     |

# 타임라인 시각화
$ perf sched timehist
          time    cpu  task name      wait time  sch delay
  123.456789 [001] my_daemon:1234     5.234 ms    0.012 ms
  123.462035 [001] <idle>            0.000 ms    0.000 ms
참고: perf sched latency의 "Avg delay"는 태스크가 runnable 상태(TASK_RUNNING)가 된 후 실제로 CPU에서 실행될 때까지의 스케줄링 지연입니다. Wait Queue에서의 대기 시간과는 다릅니다. Wait Queue 대기 시간을 측정하려면 bpftrace로 sched_wakeup에서 sched_switch까지의 시간을 측정해야 합니다.

서브시스템: pipe wait queue

파이프는 Wait Queue의 전형적인 사용 사례입니다. 읽기/쓰기 각각 별도의 Wait Queue(rd_wait/wr_wait)를 사용하며, splice와의 연동에서 Wait Queue가 어떻게 활용되는지 분석합니다.

pipe_inode_info의 Wait Queue 구조

/* include/linux/pipe_fs_i.h */
struct pipe_inode_info {
    struct mutex          mutex;       /* 파이프 전체 보호 */
    wait_queue_head_t    rd_wait;     /* 읽기 대기열 */
    wait_queue_head_t    wr_wait;     /* 쓰기 대기열 */
    unsigned int         head;        /* 쓰기 위치 */
    unsigned int         tail;        /* 읽기 위치 */
    unsigned int         max_usage;   /* 최대 버퍼 수 */
    unsigned int         ring_size;   /* 링 버퍼 크기 */
    unsigned int         readers;     /* 활성 reader 수 */
    unsigned int         writers;     /* 활성 writer 수 */
    struct pipe_buffer   *bufs;        /* 파이프 버퍼 배열 */
};

pipe_read() 대기 흐름

/* fs/pipe.c — pipe_read (핵심 대기 로직 추출) */
static ssize_t pipe_read(struct kiocb *iocb,
                          struct iov_iter *to)
{
    struct pipe_inode_info *pipe = iocb->ki_filp->private_data;
    bool was_full, wake_next_reader = false;

    mutex_lock(&pipe->mutex);
    for (;;) {
        unsigned int head = smp_load_acquire(&pipe->head);
        unsigned int tail = pipe->tail;

        if (!pipe_empty(head, tail)) {
            /* 데이터 있음 — 복사 후 tail 전진 */
            was_full = pipe_full(head, tail, pipe->max_usage);
            /* ... copy_page_to_iter ... */
            smp_store_release(&pipe->tail, tail + 1);

            if (was_full)
                wake_next_reader = true;
            continue;
        }

        /* 파이프 비어있음 */
        if (!pipe->writers) {
            break;  /* 모든 writer 종료 → EOF */
        }
        if (filp->f_flags & O_NONBLOCK) {
            ret = -EAGAIN;
            break;
        }

        /* 뮤텍스 해제 → writer 깨우기 → 대기 → 뮤텍스 재획득 */
        mutex_unlock(&pipe->mutex);
        if (was_full)
            wake_up_interruptible_sync_poll(
                &pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);
        wait_event_interruptible_exclusive(
            pipe->rd_wait,
            pipe_readable(pipe));
        mutex_lock(&pipe->mutex);
    }
    mutex_unlock(&pipe->mutex);

    /* writer 깨우기 */
    if (was_full)
        wake_up_interruptible_sync_poll(
            &pipe->wr_wait, EPOLLOUT | EPOLLWRNORM);

    return ret;
}
Exclusive 사용 이유: pipe_read에서 wait_event_interruptible_exclusive를 사용하는 이유는 여러 프로세스가 동일 파이프에서 읽기를 시도할 때 Thundering Herd를 방지하기 위함입니다. 데이터가 도착하면 하나의 reader만 깨워서 실제로 데이터를 읽게 합니다.

pipe_write() 대기와 reader 깨우기

/* fs/pipe.c — pipe_write 대기 패턴 (핵심) */
static ssize_t pipe_write(struct kiocb *iocb,
                           struct iov_iter *from)
{
    /* ... 데이터 기록 시도 ... */

    if (pipe_full(head, tail, pipe->max_usage)) {
        /* 파이프 가득 참 — reader 깨우고 대기 */
        wake_up_interruptible_sync_poll(
            &pipe->rd_wait, EPOLLIN | EPOLLRDNORM);

        /* _sync 변형: 현재 CPU에서 깨운 태스크를 즉시
           스케줄하지 않음. writer가 뮤텍스를 해제한 후
           깨운 태스크가 실행되도록 최적화 */

        wait_event_interruptible_exclusive(
            pipe->wr_wait,
            pipe_writable(pipe));
    }

    /* 데이터 기록 후 reader 깨우기 */
    wake_up_interruptible_sync_poll(
        &pipe->rd_wait, EPOLLIN | EPOLLRDNORM);
}

splice와 Wait Queue

/* fs/splice.c — splice_to_pipe의 대기 패턴 */
/* splice는 pipe 버퍼를 직접 조작하므로
   동일한 rd_wait/wr_wait을 통해 대기합니다.
   zero-copy 전송에서도 Wake Queue 메커니즘은 동일:
   - 소스 fd → pipe 버퍼 참조 추가 (page 공유)
   - pipe → 타겟 fd로 전송
   - 양쪽 모두 pipe의 WQ로 동기화 */

/* splice 대기 흐름:
   1. splice_to_pipe: pipe 가득 참 → wr_wait 대기
   2. splice_from_pipe: pipe 비어있음 → rd_wait 대기
   3. 각 단계에서 상대방 WQ를 wake_up */
상황대기 WQ깨우는 WQ깨우는 시점
read, 파이프 비어있음rd_waitwr_wait읽기 후 공간 확보 시
write, 파이프 가득 참wr_waitrd_wait쓰기 전 reader 깨우기
splice to pipe, 가득 참wr_waitrd_waitpipe 소비 후
splice from pipe, 비어있음rd_waitwr_waitpipe에 데이터 추가 후
close(write fd)rd_waitwriter 수 감소 시 reader에 EOF 통지

서브시스템: block I/O completion

블록 I/O 서브시스템은 Wait Queue를 두 가지 용도로 사용합니다: I/O 요청의 완료 대기와 태그 할당 대기입니다. blk-mq의 태그 기반 대기와 bio completion 경로를 분석합니다.

bio completion과 Wait Queue

/* block/bio.c — 동기 I/O 완료 대기 */
static void submit_bio_wait_endio(struct bio *bio)
{
    complete(bio->bi_private);
    /* completion은 내부적으로 swait_queue 사용 */
}

int submit_bio_wait(struct bio *bio)
{
    DECLARE_COMPLETION_ONSTACK_MAP(done, bio->bi_disk->lockdep_map);

    bio->bi_private = &done;
    bio->bi_end_io = submit_bio_wait_endio;
    bio->bi_opf |= REQ_SYNC;

    submit_bio(bio);
    wait_for_completion(&done);
    /* → swait_event(done.wait, done.done) */

    return blk_status_to_errno(bio->bi_status);
}

blk-mq 태그 할당 대기

/* block/blk-mq-tag.c — 태그 할당 대기 */
static int blk_mq_get_tag(struct blk_mq_alloc_data *data)
{
    struct blk_mq_tags *tags = data->hctx->tags;
    struct sbitmap_queue *bt = &tags->bitmap_tags;
    struct sbq_wait_state *ws;
    DEFINE_SBQ_WAIT(wait);
    unsigned int tag;

    tag = __blk_mq_get_tag(data, bt);
    if (tag != BLK_MQ_NO_TAG)
        return tag;  /* Fast path: 태그 즉시 할당 */

    /* Slow path: 태그 부족 → 대기 */
    ws = bt_wait_ptr(bt, data->hctx);
    /* ws는 sbitmap의 wait state 배열에서 하나를 선택.
       여러 대기열을 사용하여 경합을 분산합니다. */

    for (;;) {
        sbitmap_prepare_to_wait(bt, ws, &wait,
                                TASK_UNINTERRUPTIBLE);
        tag = __blk_mq_get_tag(data, bt);
        if (tag != BLK_MQ_NO_TAG)
            break;

        blk_mq_tag_busy(data->hctx);
        io_schedule();
        /* io_schedule()은 schedule()과 유사하지만
           io_context->nr_batch_requests를 조정하여
           I/O 스케줄러에 힌트를 제공합니다. */
    }
    sbitmap_finish_wait(bt, ws, &wait);
    return tag;
}

/* 태그 해제 시 대기자 깨우기 */
void blk_mq_put_tag(struct blk_mq_tags *tags,
                     unsigned int tag)
{
    sbitmap_queue_clear(&tags->bitmap_tags, tag,
                        raw_smp_processor_id());
    /* sbitmap_queue_clear 내부에서
       sbq_wake_up → wake_up_nr이 대기자를 깨웁니다.
       해제된 태그 수만큼 대기자를 깨우는 최적화 */
}
블록 I/O 태그 대기와 completion 경로 submit_bio() blk_mq_get_tag() 태그 할당 시도 성공 I/O 디스패치 태그 없음 sbq_wait_state에서 io_schedule() 대기 (TASK_UNINTERRUPTIBLE) HW 완료 blk_mq_complete_rq softirq/irq context blk_mq_put_tag() sbitmap clear + sbq_wake_up wake_up_nr bio->bi_end_io() → complete() (swait) submit_bio_wait() wait_for_completion() complete() → swake_up_one sbitmap_queue는 여러 sbq_wait_state로 대기자를 분산하여 spinlock 경합을 줄이는 최적화된 Wait Queue입니다
블록 I/O는 태그 할당 대기(sbitmap WQ)와 bio 완료 대기(completion/swait) 두 가지 경로에서 Wait Queue를 사용합니다

요청 큐 혼잡도 대기

/* 블록 I/O 큐가 혼잡할 때의 대기 */
static void blk_io_schedule(void)
{
    /* io_schedule()은 schedule()과 동일하지만
       in_iowait 플래그를 설정하여 I/O 대기 시간을
       별도로 집계합니다. (vmstat의 wa% 컬럼) */
    set_current_state(TASK_UNINTERRUPTIBLE);
    io_schedule();
    __set_current_state(TASK_RUNNING);
}

/* /proc/stat의 iowait 시간에 기여:
   io_schedule → delayacct_blkio_start/end → iowait 집계 */
참고: io_schedule()schedule()의 I/O 특화 버전입니다. 태스크의 in_iowait 플래그를 설정하여 /proc/stat의 iowait 시간에 기여합니다. 이를 통해 시스템 모니터링 도구(top, vmstat 등)에서 I/O 대기 시간을 정확하게 보고할 수 있습니다.

커널 버전별 진화

Wait Queue는 Linux 초기부터 존재했지만 각 주요 버전에서 기능 확장, 성능 최적화, 새로운 대기 메커니즘이 추가되었습니다. 핵심 변화를 시간순으로 정리합니다.

버전별 주요 변화

버전변화설명
v2.6.x기본 wait_event 매크로wait_event/wait_event_interruptible/wait_event_timeout 표준화. 이전에는 수동 대기 루프만 존재
v2.6.25TASK_KILLABLE 도입wait_event_killable 추가. SIGKILL만 수신하여 unkillable D-state 문제를 완화
v3.11WQ_FLAG_WOKENwait_woken()/woken_wake_function() 추가. 콜백 기반 대기에서 spurious wakeup 방지
v3.15WQ_FLAG_BOOKMARK긴 대기 리스트 순회 시 spinlock 보유 시간을 제한하는 bookmark 최적화
v4.6Simple Wait Queue (swait)swait_queue_head 도입. raw_spinlock 기반, PREEMPT_RT 호환, completion에 채택
v4.9EPOLLEXCLUSIVEepoll에 exclusive 웨이크업 지원. 여러 epoll 인스턴스의 Thundering Herd 방지
v4.13prepare_to_wait_event 통합prepare_to_wait/prepare_to_wait_exclusiveprepare_to_wait_event로 통합. 시그널 검사를 내부에서 처리
v4.18wait_event_idleTASK_IDLE 상태 대기. load average에 기여하지 않는 유휴 대기용
v5.1io_uring 도입io_cqring_wait에서 Wait Queue 활용. task_work 기반 비동기 완료 통지
v5.3wait_var_event변수 주소 기반 대기. Wait Queue 헤드 없이 해시 테이블(Hash Table)로 대기. page writeback 등에 사용
v5.7WQ_FLAG_PRIORITY리스트 앞에 삽입하여 우선 깨워야 하는 대기자 지원
v5.14IORING_SETUP_SQPOLL 개선SQPOLL idle 시 Wait Queue 기반 깊은 슬립 전환
v6.1+CLASS_WAIT 타입lockdep의 wait_type 분류 강화. 원자적 컨텍스트에서의 잘못된 대기를 더 정밀하게 감지

wait_var_event: 해시(Hash) 기반 대기 (v5.3+)

/* include/linux/wait_bit.h — 변수 주소 기반 대기 */
#define wait_var_event(var, condition)                            \
do {                                                             \
    might_sleep();                                               \
    if (condition)                                                \
        break;                                                   \
    __wait_var_event(var, condition);                             \
} while (0)

/* var의 주소를 해시하여 전역 해시 테이블에서 WQ 헤드를 찾음 */
wait_queue_head_t *__var_waitqueue(void *p)
{
    return bit_wait_table +
           hash_ptr(p, __WAIT_BIT_KEY_NR);
}

/* 사용 예시: page writeback 완료 대기 */
wait_var_event(&page->flags,
    !PageWriteback(page));

/* 깨우기 */
wake_up_var(&page->flags);

/* 장점: 별도 wait_queue_head_t를 할당할 필요 없음
   단점: 해시 충돌 시 불필요한 깨우기 발생 가능 */

설계 철학의 진화

시대접근 방식특징
v2.4 이전수동 대기 루프sleep_on()/interruptible_sleep_on() — Lost Wakeup에 취약, 2010년대에 완전 제거
v2.6~v3매크로 표준화wait_event 패밀리로 안전한 패턴을 강제. 수동 루프 사용 감소
v4~v5경량화 + 특수화swait로 RT 호환성 확보, EPOLLEXCLUSIVE로 확장성 개선, io_uring으로 syscall 오버헤드 제거
v6+정밀 디버깅lockdep wait_type 분류, 원자적 컨텍스트 감지 강화, 성능 카운터 통합
주의: sleep_on()interruptible_sleep_on()은 본질적으로 Lost Wakeup에 취약한 API였습니다 (조건 검사와 대기열 등록이 원자적이지 않았음). 커널 v3.15 이후 완전히 제거되었으며, 현대 코드에서는 절대 사용하면 안 됩니다. 항상 wait_event 매크로를 사용하세요.

참고 자료

Wait Queue의 설계, API, 사용 패턴에 대한 공식 문서 및 참고 자료입니다.

커널 공식 문서

LWN.net 심층 기사

학술 자료 및 외부 참고

Wait Queue와 관련된 다른 동기화/스케줄링 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.