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 영향까지 포괄합니다.
핵심 요약
- 조건 대기 프리미티브 — 조건이 거짓이면 태스크를 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멀티플렉싱을 구현합니다.
단계별 이해
- 슬립/웨이크업 기본 개념
busy-wait 대신 CPU를 양보(Yield)하는 블로킹 동기화의 필요성을 이해합니다. - 자료구조 파악
wait_queue_head_t와wait_queue_entry_t의 필드와 연결 관계를 분석합니다. - wait_event 매크로 내부 추적
조건 검사 → 상태 설정 → schedule() → 조건 재검사 루프의 흐름을 따라갑니다. - wake_up 경로 분석
대기 리스트 순회, 콜백 함수 호출, exclusive/non-exclusive 구분을 이해합니다. - 실전 패턴과 함정 숙지
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이 발생합니다.
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;
기본 웨이크업 콜백: 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_t의 flags 필드는 대기자의 동작 방식을 제어합니다.
| 플래그 | 값 | 설명 |
|---|---|---|
WQ_FLAG_EXCLUSIVE | 0x01 | Exclusive 웨이크업 대상. wake_up()은 이 플래그가 설정된 대기자 중 하나만 깨웁니다. |
WQ_FLAG_WOKEN | 0x02 | wake_up에 의해 깨어났음을 표시. wait_woken() 패턴에서 spurious wakeup 방지용입니다. |
WQ_FLAG_BOOKMARK | 0x04 | 대기 리스트 순회 중 재시작(Reboot) 지점 북마크. 긴 리스트에서 lock 해제 후 순회를 이어가는 용도입니다. |
WQ_FLAG_CUSTOM | 0x08 | 커스텀 func을 사용하는 엔트리. autoremove_wake_function이 아닌 사용자 정의 콜백입니다. |
WQ_FLAG_DONE | 0x10 | 처리 완료. completion 구현에서 완료 상태를 표시하는 데 사용됩니다. |
WQ_FLAG_PRIORITY | 0x20 | 리스트 앞에 삽입. 일반 엔트리보다 우선 깨워야 하는 대기자에 사용합니다. |
/* 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
__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은 autoremove_wake_function을 콜백으로 설정하여, 깨어나면 자동으로 대기 리스트에서 제거됩니다.
init_waitqueue_entry는 default_wake_function을 사용하며, 수동으로 remove_wait_queue를 호출해야 합니다.
wait_event 매크로 패밀리
가장 일반적으로 사용되는 Wait Queue API입니다. 조건을 인자로 받아 조건이 참이 될 때까지 슬립합니다.
| 매크로 | 태스크 상태 | 반환값 | 특징 |
|---|---|---|---|
wait_event(wq, cond) | TASK_UNINTERRUPTIBLE | void | 시그널 무시. 조건 충족까지 무조건 대기 |
wait_event_interruptible(wq, cond) | TASK_INTERRUPTIBLE | 0 또는 -ERESTARTSYS | 시그널로 깨어날 수 있음 |
wait_event_killable(wq, cond) | TASK_KILLABLE | 0 또는 -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_INTERRUPTIBLE | 0 | freezer 호환 (suspend) |
wait_event_idle(wq, cond) | TASK_IDLE | void | load average에 기여하지 않음 |
사용 예시
/* 기본 사용: 조건이 참이 될 때까지 무조건 대기 */
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_NORMAL | exclusive 1개 | 모든 non-exclusive + exclusive 1개 |
wake_up_interruptible(wq) | TASK_INTERRUPTIBLE | exclusive 1개 | INTERRUPTIBLE 상태만 대상 |
wake_up_all(wq) | TASK_NORMAL | 전부 | 모든 대기자를 깨움 |
wake_up_interruptible_all(wq) | TASK_INTERRUPTIBLE | 전부 | INTERRUPTIBLE인 모든 대기자 |
wake_up_nr(wq, nr) | TASK_NORMAL | nr개 | non-exclusive 전부 + exclusive nr개 |
wake_up_interruptible_nr(wq, nr) | TASK_INTERRUPTIBLE | nr개 | 위와 동일, 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) */
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 |
|---|---|---|
| 깨우기 범위 | 항상 모두 깨움 | nr_exclusive 개수만 |
| 리스트 위치 | 앞 (head) | 뒤 (tail) |
| 전형적 사용 | poll/epoll, 일반 이벤트 통지 | accept(), I/O 대기 |
| Thundering Herd | 발생 가능 | 방지 |
| API | add_wait_queue() | add_wait_queue_exclusive() |
Thundering Herd 문제와 해결
Thundering Herd는 하나의 이벤트에 의해 다수의 대기자가 동시에 깨어나지만, 실제로는 한 태스크만 작업을 수행할 수 있어 나머지가 모두 다시 슬립하는 비효율을 말합니다.
커널 내 Thundering Herd 방지 사례
| 서브시스템 | Wait Queue | 방지 기법 |
|---|---|---|
TCP accept() | sk->sk_wq | WQ_FLAG_EXCLUSIVE + SO_REUSEPORT |
| Pipe read | pipe->rd_wait | Exclusive 웨이크업 |
| 블록 I/O | blk_mq_wait_queue | Exclusive 태그 대기 |
| epoll | ep->wq | EPOLLEXCLUSIVE 플래그 |
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; \
})
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;
}
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;
}
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);
Simple Wait Queue (swait)
Simple Wait Queue (swait)는 일반 Wait Queue의 경량 대안입니다. NMI 안전성이 필요하거나, exclusive/커스텀 콜백이 불필요한 경우에 사용합니다.
| 특성 | wait_queue (일반) | swait (단순) |
|---|---|---|
| Lock | spinlock_t | raw_spinlock_t |
| NMI 안전 | 아니오 | 예 (raw_spinlock) |
| Exclusive 지원 | 예 | 아니오 |
| 커스텀 콜백 | 예 (func 포인터) | 아니오 (고정 동작) |
| BOOKMARK | 예 | 아니오 |
| PREEMPT_RT | spinlock → sleeping | raw_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);
struct completion은 내부적으로 swait_queue_head를 사용합니다. 이는 completion이 RT 커널에서도 예측 가능한 동작을 보장하기 위함입니다.
Lost Wakeup 방지 패턴
Lost Wakeup은 조건 동기화에서 가장 치명적인 버그입니다. 깨우는 자의 wake_up이 대기자의 슬립보다 먼저 실행되어, 대기자가 영원히 깨어나지 못하는 상황입니다.
깨우는 측의 규칙
/* 올바른 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.lock | spinlock_t (busy-wait) | sleeping lock (rt_mutex) |
swait_queue_head.lock | raw_spinlock_t | raw_spinlock_t (변경 없음) |
| wake_up 원자적(Atomic) 컨텍스트 | 가능 | 일반 wait queue: 불가, swait: 가능 |
| completion | wait_queue 기반 | swait 기반 → NMI/hardirq 안전 |
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 매크로 사용 (항상 조건 먼저 검사) |
| 2 | wake_up 후에 조건 설정 | Lost Wakeup: 대기자가 깨어나서 조건 검사 시 아직 거짓 | 조건 설정 → wake_up 순서 엄수 |
| 3 | 인터럽트 컨텍스트에서 wait_event | schedule() 호출 → BUG: scheduling while atomic | 인터럽트에서는 슬립 불가. bottom half로 위임 |
| 4 | wait_event에서 시그널 미처리 | 사용자 프로세스가 SIGKILL에도 응답 않음 (unkillable) | wait_event_interruptible 또는 _killable 사용 |
| 5 | finish_wait / remove_wait_queue 누락 | 메모리 누수, dangling pointer | 모든 탈출 경로에서 cleanup 보장 |
| 6 | Non-exclusive로 다수 대기 | Thundering Herd | WQ_FLAG_EXCLUSIVE 또는 EPOLLEXCLUSIVE |
| 7 | set_current_state 없이 schedule() | 이미 TASK_RUNNING이라 schedule()이 즉시 반환, busy-loop | prepare_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 사용! */
디버깅(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 | ~200 | spinlock 획득 + 리스트 삽입 + 해제 |
schedule() + context switch | ~5,000–10,000 | 레지스터(Register) 저장, 런큐(Runqueue) 조작, TLB |
wake_up (1 waiter) | ~1,000–3,000 | spinlock + try_to_wake_up + enqueue |
wake_up_all (N waiters) | ~N × 2,000 | N개 태스크 각각 try_to_wake_up |
| swait vs wait_queue | ~10–20% 빠름 | 함수 포인터 호출 생략, 더 작은 구조체 |
최적화 팁
- 대기자 수가 많으면 exclusive 사용 — accept(), 블록 I/O 태그 대기 등
- 조건이 빈번히 참이면 fast path 활용 —
wait_event매크로의 첫 번째 조건 검사가 최적화됨 - 불필요한 wake_up 호출 최소화 —
waitqueue_active()로 대기자 존재 여부를 먼저 확인 - swait로 대체 가능하면 대체 — exclusive/커스텀 콜백 불필요 시 오버헤드 감소
/* 대기자 존재 확인 후 wake_up (최적화) */
if (waitqueue_active(&dev->wq))
wake_up(&dev->wq);
/* 주의: waitqueue_active()는 lock 없이 리스트를 확인하므로
정확한 결과를 보장하지 않습니다.
"불필요한 wake_up 호출을 대부분 제거"하는 힌트로만 사용하세요.
안전성은 wake_up 자체가 보장합니다. */
커널 설정
| 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_DETECT_HUNG_TASK | y | TASK_UNINTERRUPTIBLE 장기 블로킹 감지. /proc/sys/kernel/hung_task_timeout_secs로 임계값 조정 (기본 120초) |
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT | 120 | hung task 감지 기본 타임아웃 (초) |
CONFIG_PREEMPT_RT | n | RT 커널. wait_queue_head_t의 spinlock이 sleeping lock으로 변환 |
CONFIG_DEBUG_MUTEXES | n | 디버그 빌드에서 슬립 가능 잠금(Lock) 관련 경고 강화 |
CONFIG_PROVE_LOCKING | n | lockdep 활성화. wait_queue spinlock의 잠금 순서 검증 |
CONFIG_SCHED_DEBUG | y | /sys/kernel/debug/sched/에 스케줄러 통계 노출 |
CONFIG_SCHEDSTATS | y | wait time, run time 등 스케줄링 통계 수집 |
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() */
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_wait의 list_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;
}
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;
}
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;
}
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 idle | sqd->wait에서 슬립 | io_uring_enter → wake_up | idle 후 깨우기 시 ~수 μs |
io_uring의 io_wake_function은 default_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 ... */
}
__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,000 | IPI 불필요 |
| wake_up → 다른 CPU (같은 소켓) | ~3.5 | ~14,000 | IPI + L3 캐시 전송 |
| wake_up → 다른 소켓 (NUMA) | ~8.0 | ~32,000 | IPI + 인터커넥트 지연 |
| wake_up_all, 10 waiters | ~15 | ~60,000 | 10× try_to_wake_up |
| wake_up_all, 100 waiters | ~180 | ~720,000 | spinlock 경합(Contention) 증가 |
| wake_up, 1 exclusive (100 대기 중) | ~3.5 | ~14,000 | 1개만 깨움, 일정 비용 |
| swake_up_one (1 waiter) | ~1.2 | ~4,800 | raw_spinlock + 간소 경로 |
| wait_event fast path (조건 참) | ~0.01 | ~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 |
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;
}
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_wait | wr_wait | 읽기 후 공간 확보 시 |
| write, 파이프 가득 참 | wr_wait | rd_wait | 쓰기 전 reader 깨우기 |
| splice to pipe, 가득 참 | wr_wait | rd_wait | pipe 소비 후 |
| splice from pipe, 비어있음 | rd_wait | wr_wait | pipe에 데이터 추가 후 |
| close(write fd) | — | rd_wait | writer 수 감소 시 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 큐가 혼잡할 때의 대기 */
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.25 | TASK_KILLABLE 도입 | wait_event_killable 추가. SIGKILL만 수신하여 unkillable D-state 문제를 완화 |
| v3.11 | WQ_FLAG_WOKEN | wait_woken()/woken_wake_function() 추가. 콜백 기반 대기에서 spurious wakeup 방지 |
| v3.15 | WQ_FLAG_BOOKMARK | 긴 대기 리스트 순회 시 spinlock 보유 시간을 제한하는 bookmark 최적화 |
| v4.6 | Simple Wait Queue (swait) | swait_queue_head 도입. raw_spinlock 기반, PREEMPT_RT 호환, completion에 채택 |
| v4.9 | EPOLLEXCLUSIVE | epoll에 exclusive 웨이크업 지원. 여러 epoll 인스턴스의 Thundering Herd 방지 |
| v4.13 | prepare_to_wait_event 통합 | prepare_to_wait/prepare_to_wait_exclusive를 prepare_to_wait_event로 통합. 시그널 검사를 내부에서 처리 |
| v4.18 | wait_event_idle | TASK_IDLE 상태 대기. load average에 기여하지 않는 유휴 대기용 |
| v5.1 | io_uring 도입 | io_cqring_wait에서 Wait Queue 활용. task_work 기반 비동기 완료 통지 |
| v5.3 | wait_var_event | 변수 주소 기반 대기. Wait Queue 헤드 없이 해시 테이블(Hash Table)로 대기. page writeback 등에 사용 |
| v5.7 | WQ_FLAG_PRIORITY | 리스트 앞에 삽입하여 우선 깨워야 하는 대기자 지원 |
| v5.14 | IORING_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 queues and wake events — 드라이버 API의 wait queue 섹션
- Scheduler — schedule() 호출과 태스크 상태 전환의 관계
- Lock types and their rules — wait queue 내부 spinlock의 잠금 규칙
- VFS overview — poll/select/epoll의 wait queue 기반 구현
LWN.net 심층 기사
- The wait/wound mutex (2013) — wait queue와 다른 대기 메커니즘 비교
- Rethinking the wait queue (2018) — swait(simple wait queue)의 도입 배경과 PREEMPT_RT 고려
- wait_event() macro improvements (2016) — wait_event 매크로 시리즈의 발전
- Avoiding the thundering herd — exclusive wake-up과 WQ_FLAG_EXCLUSIVE
학술 자료 및 외부 참고
- Paul McKenney — "Is Parallel Programming Hard?" — Chapter 6: Partitioning and Synchronization Design (이벤트 기반 대기)
- 커널 소스:
include/linux/wait.h,kernel/sched/wait.c,include/linux/swait.h - epoll 구현:
fs/eventpoll.c— wait queue 콜백 기반 이벤트 통지의 대표 사례
관련 문서
Wait Queue와 관련된 다른 동기화/스케줄링 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.