Workqueue (CMWQ)
Workqueue는 리눅스 커널에서 가장 범용적인 Bottom Half 메커니즘입니다.
프로세스(Process) 컨텍스트에서 실행되므로 슬립(Sleep), mutex, 메모리 할당(GFP_KERNEL)이 가능하며,
Concurrency Managed Workqueue (CMWQ) 아키텍처로 커널이 worker pool을 자동 관리합니다.
이 문서에서는 workqueue_struct, worker_pool, work_struct의
내부 구조부터 alloc_workqueue() API, 동시성 제어, flush/cancel 패턴,
ordered/delayed 워크큐, 디버깅(Debugging), PREEMPT_RT 호환성까지 전 영역을 다룹니다.
이 페이지(Page)의 위치: Workqueue 내용은 원래 Bottom Half 통합 문서에 포함되어 있었으나, 분량과 독립성을 고려하여 별도 페이지로 분리되었습니다. Bottom Half 메커니즘 전체 비교는 Bottom Half 선택 가이드를 참고하세요.
핵심 요약
- 한 줄 정의: Workqueue는 프로세스(Process) 컨텍스트에서 지연 작업을 실행하는 커널의 범용 비동기 실행 프레임워크입니다.
- 핵심 역할: 인터럽트(Interrupt) 핸들러나 atomic 컨텍스트에서 처리하기 어려운 작업(슬립, mutex 획득, 메모리 할당 등)을 안전하게 지연 실행합니다.
- 사용 이유: softirq/tasklet은 atomic 컨텍스트에서 실행되어 슬립이 불가능하지만, workqueue는 커널 스레드(Kernel Thread) 위에서 동작하므로 블로킹(Blocking) 연산이 가능합니다.
- CMWQ 아키텍처: Concurrency Managed Workqueue는 커널이 worker pool을 자동 관리하여, 드라이버 개발자가 스레드 수나 동시성을 직접 제어할 필요가 없습니다.
- 주요 구조체:
work_struct(작업 단위),workqueue_struct(작업 큐),worker_pool(스레드 풀)이 핵심 삼각 구조를 이룹니다. - 현대 커널 추세: tasklet 대체, WQ_BH(6.9+)로 softirq 영역 통합, affinity scope(6.5+)로 NUMA 최적화가 진행 중입니다.
단계별 이해
- Work 등록
드라이버가INIT_WORK()로work_struct를 초기화하고, 콜백 함수를 지정합니다. 이 콜백이 나중에 worker 스레드에서 실행될 실제 작업입니다. - Work 예약
schedule_work()또는queue_work()를 호출하면, work item이 workqueue에 연결된 worker pool의 대기 목록에 삽입됩니다. 인터럽트 컨텍스트에서도 호출 가능합니다. - Worker 스레드 깨우기
CMWQ가 worker pool에서 유휴(Idle) worker 스레드를 찾아 깨웁니다. 유휴 스레드가 없으면 동시성 수준에 따라 새 worker를 생성합니다. - 콜백 실행
Worker 스레드가 work item을 꺼내 콜백 함수를 실행합니다. 프로세스 컨텍스트이므로mutex_lock(),kmalloc(GFP_KERNEL),msleep()등 블로킹 API를 자유롭게 사용할 수 있습니다. - 완료 및 정리
콜백 실행이 끝나면 work item이 완료 상태가 됩니다.flush_work()로 완료 대기,cancel_work_sync()로 취소할 수 있으며, 드라이버 해제 시 반드시 정리해야 합니다.
Workqueue 역사와 진화
리눅스 커널의 workqueue 메커니즘은 오랜 진화 과정을 거쳐 현재의 CMWQ 아키텍처에 이르렀습니다. 각 세대의 특징과 한계를 이해하면 CMWQ의 설계 동기를 더 깊이 파악할 수 있습니다.
| 세대 | 커널 버전 | 메커니즘 | 특징 / 한계 |
|---|---|---|---|
| 1세대 | 2.5.41 (2002) | keventd / task queue 대체 |
CPU별 하나의 worker 스레드 (events/N). 동시성 제어 없음, 하나의 work가 블록되면 해당 CPU의 모든 work 지연(Latency) |
| 2세대 | 2.6.x | create_workqueue() / create_singlethread_workqueue() |
multithread: CPU당 하나의 전용 스레드 생성 → N-CPU 시스템에서 N개 스레드. singlethread: 시스템에 1개 스레드. 커널 스레드(Kernel Thread) 폭발 문제 |
| 3세대 (현재) | 2.6.36+ (2010) | CMWQ (alloc_workqueue()) |
공유 worker pool, 자동 동시성 관리, bound/unbound 분리, 플래그 기반 속성. create_workqueue()는 alloc_workqueue()의 래퍼로 전환 후 deprecated |
/*
* 레거시 API → CMWQ 대응 (마이그레이션 가이드)
*
* create_workqueue(name)
* → alloc_workqueue(name, WQ_MEM_RECLAIM, 1)
* (Per-CPU, max_active=1, rescuer 보장)
*
* create_singlethread_workqueue(name)
* → alloc_ordered_workqueue(name, WQ_MEM_RECLAIM)
* (전역 순서 보장, rescuer 보장)
*
* create_freezable_workqueue(name)
* → alloc_workqueue(name, WQ_FREEZABLE | WQ_MEM_RECLAIM, 1)
*
* 참고: 레거시 API는 v5.x에서 완전히 제거됨
*/
CMWQ 이전에는 드라이버마다 create_workqueue()로 전용 workqueue를 만드는 것이 일반적이었고, 이로 인해 시스템에 수백 개의 kworker 스레드가 생성되는 문제가 있었습니다. CMWQ는 worker pool을 중앙 집중 관리하여 이 문제를 해결했습니다.
CMWQ 아키텍처 개요
Concurrency Managed Workqueue (CMWQ)는 Linux 2.6.36에서 도입된 현대적 workqueue 아키텍처입니다. 기존의 singlethread/multithread workqueue를 대체하여, 커널이 worker pool을 중앙 관리하고 동시성을 자동 제어합니다.
CMWQ의 핵심 설계 원칙:
- 공유 worker pool: 모든 workqueue가 worker pool을 공유하여 커널 스레드 수 최소화
- 자동 동시성 관리: worker가 블록되면 새 worker를 자동 생성하여 CPU 유휴 방지
- Bound/Unbound 분리: CPU-bound 작업과 NUMA-aware unbound 작업 구분
- 속성 기반 매핑(Mapping): workqueue 플래그에 따라 적절한 worker pool에 자동 매핑
CMWQ 아키텍처 상세
CMWQ의 핵심은 workqueue_struct, pool_workqueue, worker_pool, worker 네 가지 구조체(Struct)의 관계입니다. 각 구조체의 역할과 연결 관계를 상세히 살펴봅니다.
/*
* pool_workqueue (pwq): workqueue와 worker_pool을 연결하는 중간 구조체
*
* 각 workqueue는 CPU/NUMA 노드마다 하나의 pwq를 가짐
* pwq는 해당 workqueue의 work가 특정 worker_pool에서
* 실행될 때의 상태를 추적함
*/
struct pool_workqueue {
struct worker_pool *pool; /* 연결된 worker pool */
struct workqueue_struct *wq; /* 소속 workqueue */
int nr_active; /* 현재 실행 중인 work 수 */
int max_active; /* 최대 동시 실행 수 */
struct list_head inactive_works; /* max_active 초과 시 대기 리스트 */
struct list_head pwqs_node; /* wq->pwqs 연결 */
int work_color; /* flush용 color 태그 */
int flush_color; /* flush 진행 중 color */
int refcnt; /* 참조 카운트 */
};
/*
* worker 구조체: 실제 kworker 스레드를 표현
*/
struct worker {
union {
struct list_head entry; /* idle_list 연결 */
struct hlist_node hentry; /* busy_hash 연결 */
};
struct work_struct *current_work; /* 현재 실행 중인 work */
work_func_t current_func; /* 현재 실행 함수 */
struct pool_workqueue *current_pwq; /* 현재 pwq */
struct worker_pool *pool; /* 소속 pool */
struct task_struct *task; /* kworker 태스크 */
unsigned long last_active; /* 마지막 활동 시각 (jiffies) */
unsigned int flags; /* WORKER_* 플래그 */
int id; /* kworker ID */
};
코드 설명
include/linux/workqueue.h와 kernel/workqueue.c에 정의된 핵심 중간 구조체들입니다.
- pool_workqueue
pool_workqueue(pwq)는workqueue_struct와worker_pool을 연결하는 중간 계층입니다. 각 workqueue는 CPU/NUMA 노드마다 하나의 pwq를 가지며,nr_active/max_active로 해당 pool에서의 동시 실행 수를 제어합니다.max_active를 초과하면inactive_works리스트에 대기합니다. - work_color / flush_color
flush_workqueue()구현에 사용되는 color 태깅 메커니즘입니다. flush 시점의 work와 이후 큐잉된 work를 구분하여 정확한 완료 대기를 가능하게 합니다. - worker 구조체
worker는 실제kworker커널 스레드를 표현합니다.union을 사용하여 idle 상태에서는idle_list에, busy 상태에서는busy_hash에 연결됩니다.current_work/current_func으로 현재 실행 중인 work를 추적하여flush_work()등의 동기화 API가 동작합니다. - last_active마지막 활동 시각(jiffies)을 기록하여
idle_timer(300초)에 의한 idle worker 소멸 판단에 사용됩니다.
Worker Pool 관리
Worker pool은 CMWQ의 실행 엔진입니다. Bound pool과 Unbound pool로 나뉘며, 각각의 동시성 관리 방식이 다릅니다. 아래 다이어그램은 worker의 상태 전환과 pool의 동시성 관리 메커니즘을 보여줍니다.
/*
* Worker Pool 유형:
*
* 1. Bound (Per-CPU) Pool:
* - 각 CPU에 2개: normal (nice=0) + highpri (nice=-20)
* - kworker/CPU:ID 또는 kworker/CPU:IDH (highpri)
* - 해당 CPU에서만 work 실행 → 캐시 친화적
*
* 2. Unbound Pool:
* - NUMA 노드별 생성, 속성(nice, cpumask)으로 관리
* - kworker/uPOOL:ID
* - 어떤 CPU에서든 실행 가능 → 부하 분산
* - long-running 또는 CPU-intensive 작업에 적합
*
* 동시성 관리:
* - 풀의 running worker가 모두 블록되면 새 worker 생성
* - idle worker는 일정 시간 후 소멸 (IDLE_WORKER_TIMEOUT: 300초)
* - manager worker가 pool을 관리 (worker 생성/소멸)
*/
/* kernel/workqueue.c 주요 구조체 (간략화) */
struct worker_pool {
spinlock_t lock;
int cpu; /* bound pool의 CPU, unbound는 -1 */
int node; /* NUMA 노드 */
int id;
unsigned int flags;
struct list_head worklist; /* pending work items */
int nr_workers; /* 총 worker 수 */
int nr_idle; /* idle worker 수 */
int nr_running; /* 실행 중인 worker 수 (atomic) */
struct list_head idle_list; /* idle worker 리스트 */
struct timer_list idle_timer;
struct timer_list mayday_timer;
};
코드 설명
kernel/workqueue.c에 정의된 worker_pool 구조체로, CMWQ의 실행 엔진입니다.
- cpu / nodeBound pool은
cpu에 특정 CPU 번호가 설정되고, Unbound pool은cpu = -1이며node로 NUMA 노드를 식별합니다. 각 CPU에는 normal(nice=0)과 highpri(nice=-20) 두 개의 bound pool이 존재합니다. - nr_runningCMWQ 동시성 관리의 핵심 카운터입니다. 스케줄러가
wq_worker_sleeping()/wq_worker_running()콜백을 통해 정확히 추적하며,nr_running == 0이고worklist가 비어있지 않으면 즉시 idle worker를 깨우거나 새 worker를 생성합니다. - idle_timer
IDLE_WORKER_TIMEOUT(300초) 후 idle worker를 소멸시키는 타이머입니다. 불필요한 커널 스레드를 제거하여 시스템 자원을 절약합니다. - mayday_timerworker 생성에 실패했을 때 재시도를 트리거하는 타이머입니다.
WQ_MEM_RECLAIMworkqueue에서는 이 타이머 만료 시send_mayday()를 호출하여 rescuer 스레드를 활성화합니다.
Worker 스레드 생성과 소멸
/*
* create_worker(): 새 kworker 스레드 생성
*
* 호출 조건:
* - worklist에 pending work가 있지만 nr_running == 0
* - manager worker가 maybe_create_worker()에서 판단
*
* 이름 규칙:
* Bound: kworker/CPU:ID (예: kworker/0:2)
* kworker/CPU:IDH (highpri, 예: kworker/0:1H)
* Unbound: kworker/uPOOL:ID (예: kworker/u8:3)
*/
static struct worker *create_worker(struct worker_pool *pool)
{
struct worker *worker;
worker = alloc_worker(pool->node);
worker->pool = pool;
worker->id = pool->worker_ida++;
/* kthread 생성 */
if (pool->cpu >= 0)
worker->task = kthread_create_on_node(
worker_thread, worker, pool->node,
"kworker/%d:%d%s", pool->cpu, worker->id,
pool->attrs->nice < 0 ? "H" : "");
else
worker->task = kthread_create_on_node(
worker_thread, worker, pool->node,
"kworker/u%d:%d", pool->id, worker->id);
/* Bound pool: CPU에 고정 */
if (pool->cpu >= 0)
kthread_bind(worker->task, pool->cpu);
worker_enter_idle(worker);
wake_up_process(worker->task);
return worker;
}
/*
* idle_worker_timeout: idle worker 소멸 타이머
*
* IDLE_WORKER_TIMEOUT (300초) 동안 활동 없으면 소멸
* 단, 풀에 최소 1개 idle worker는 항상 유지 (min_idle = 1)
*/
static void idle_worker_timeout(struct timer_list *t)
{
struct worker_pool *pool = from_timer(pool, t, idle_timer);
/* too_many_workers(): nr_idle > 2 && (nr_idle-2)*MAX_IDLE_WORKERS_RATIO >= nr_busy */
while (too_many_workers(pool)) {
struct worker *worker = list_last_entry(
&pool->idle_list, struct worker, entry);
destroy_worker(worker);
}
}
Rescuer 스레드
/*
* Rescuer Thread: WQ_MEM_RECLAIM 워크큐의 안전장치
*
* 메모리 부족으로 새 kworker 스레드를 생성할 수 없을 때,
* rescuer가 대신 work를 처리하여 데드락을 방지합니다.
*
* 동작 흐름:
* 1. mayday_timer 만료 → send_mayday() 호출
* 2. rescuer 스레드가 깨어남
* 3. 해당 pool의 worklist에서 work를 가져와 실행
* 4. pool의 정상 worker가 복구되면 다시 대기
*
* 주의: rescuer는 workqueue당 1개만 존재하므로
* 동시에 많은 work를 처리할 수 없음
* → 최소한의 진행(forward progress)만 보장
*/
static int rescuer_thread(void *__rescuer)
{
struct worker *rescuer = __rescuer;
struct workqueue_struct *wq = rescuer->rescue_wq;
for (;;) {
set_current_state(TASK_IDLE);
/* mayday 시그널 대기 */
if (list_empty(&wq->maydays))
schedule();
/* pool의 worklist에서 work 실행 */
process_scheduled_works(rescuer);
}
}
alloc_workqueue() API
/* workqueue 생성 */
struct workqueue_struct *alloc_workqueue(
const char *fmt, /* 이름 형식 (printf 스타일) */
unsigned int flags, /* WQ_* 플래그 조합 */
int max_active, /* Per-CPU 최대 동시 실행 work 수 */
... /* fmt 인자 */
);
/* 예제 */
struct workqueue_struct *my_wq;
my_wq = alloc_workqueue("my_driver_wq",
WQ_UNBOUND | WQ_MEM_RECLAIM, 0);
코드 설명
include/linux/workqueue.h에 선언된 workqueue 생성 API입니다. 내부적으로 alloc_workqueue() → alloc_and_link_pwqs() → apply_workqueue_attrs() 호출 체인을 거칩니다.
- fmtworkqueue 이름으로,
/sys/kernel/debug/workqueue와kworker스레드 이름에 표시됩니다. printf 형식을 지원하여alloc_workqueue("drv-%s", flags, max, name)과 같이 동적 이름을 사용할 수 있습니다. - flags
WQ_UNBOUND,WQ_MEM_RECLAIM,WQ_HIGHPRI,WQ_FREEZABLE,WQ_CPU_INTENSIVE등의 비트 조합입니다. 플래그에 따라 workqueue가 매핑되는 worker pool 유형과 동작 특성이 결정됩니다. - max_activePer-CPU(Bound) 또는 Per-NUMA(Unbound) 기준으로 동시에 실행할 수 있는 최대 work 수입니다.
0을 전달하면 기본값WQ_DFL_ACTIVE(256)이 적용됩니다.1로 설정하면 순차 실행을 보장하며, 이는alloc_ordered_workqueue()의 기본 동작입니다. - 예제
WQ_UNBOUND | WQ_MEM_RECLAIM조합은 NUMA-aware unbound pool을 사용하면서 메모리 부족 시에도 rescuer 스레드가 forward progress를 보장합니다. 블록 I/O, 파일시스템 드라이버에서 가장 흔히 사용되는 패턴입니다.
| 플래그 | 설명 | 사용 시나리오 |
|---|---|---|
WQ_UNBOUND | Per-CPU 대신 NUMA-aware unbound pool 사용 | long-running 작업, CPU 마이그레이션 허용 |
WQ_HIGHPRI | 높은 우선순위(Priority) worker pool (nice -20) 사용 | 지연시간이 중요한 작업 |
WQ_CPU_INTENSIVE | 동시성 관리에서 제외 (CPU 점유로 인한 추가 worker 생성 방지) | CPU-bound 연산 작업 |
WQ_FREEZABLE | 시스템 suspend 시 work 처리 중단 | suspend/resume과 상호작용하는 작업 |
WQ_MEM_RECLAIM | 메모리 부족 시에도 worker 생성 보장 (rescuer thread) | 메모리 회수(Memory Reclaim) 경로에서 사용되는 작업 |
WQ_SYSFS | /sys/devices/virtual/workqueue/에 제어 인터페이스 노출 | 런타임 튜닝이 필요한 workqueue |
WQ_MEM_RECLAIM: 메모리 회수 경로(reclaim path)에서 work를 큐잉하는 workqueue는 반드시 이 플래그를 설정해야 합니다. 그렇지 않으면 메모리 부족 시 worker 할당 실패로 데드락이 발생할 수 있습니다. rescuer thread가 이 상황을 방지합니다.
alloc_workqueue 플래그 상세
/*
* WQ_UNBOUND (bit 1):
* - Per-CPU pool 대신 NUMA-aware unbound pool 사용
* - kworker가 특정 CPU에 고정되지 않음 → 스케줄러가 자유롭게 배치
* - long-running 작업에 적합: bound pool의 concurrency 관리에 간섭하지 않음
* - WQ_CPU_INTENSIVE와 함께 사용 불가 (의미상 중복)
*
* WQ_HIGHPRI (bit 4):
* - nice=-20 worker pool 사용 (highpri pool)
* - 일반 worker pool(nice=0)보다 높은 스케줄링 우선순위
* - kworker 이름에 H 접미사: kworker/0:1H
* - 실시간 응답이 중요한 작업에 사용
*
* WQ_CPU_INTENSIVE (bit 5):
* - bound pool에서만 의미 있음
* - 해당 work를 concurrency 관리 대상에서 제외
* - 즉, CPU 점유로 인해 nr_running이 0이 되어도 새 worker를 생성하지 않음
* - CPU 연산이 주 작업인 경우 불필요한 worker 증식 방지
*
* WQ_MEM_RECLAIM (bit 3):
* - rescuer 스레드 생성을 보장
* - 메모리 부족으로 새 worker를 생성할 수 없을 때 rescuer가 대신 처리
* - 메모리 회수 경로(reclaim path)에서 사용하는 WQ에 필수
* - 파일시스템, 블록 I/O, 스왑 관련 work에 반드시 설정
*
* WQ_FREEZABLE (bit 2):
* - 시스템 suspend(freeze) 시 work 처리를 중단
* - try_to_freeze_tasks()에서 workqueue를 동결
* - resume 시 자동으로 처리 재개
* - 사용자 공간 요청 처리, PM 관련 작업에 사용
*
* WQ_SYSFS (bit 9):
* - /sys/devices/virtual/workqueue/<name>/ 디렉토리 생성
* - 런타임에 cpumask, max_active, nice 변경 가능
* - 프로덕션 환경 튜닝에 유용
*
* WQ_POWER_EFFICIENT (bit 7):
* - wq_power_efficient 커널 파라미터 활성 시 WQ_UNBOUND로 동작
* - 비활성 시 일반 bound workqueue로 동작
* - 전력 효율이 중요한 모바일/임베디드 환경
*/
| 플래그 | 비트 | rescuer | Pool 유형 | 주요 효과 |
|---|---|---|---|---|
WQ_UNBOUND | 1 | 선택 | Unbound | NUMA-aware, CPU 비고정, long-running 적합 |
WQ_FREEZABLE | 2 | 선택 | Any | suspend 시 동결, resume 시 재개 |
WQ_MEM_RECLAIM | 3 | 필수 | Any | rescuer 보장, reclaim 경로 데드락 방지 |
WQ_HIGHPRI | 4 | 선택 | nice=-20 | 높은 스케줄링 우선순위 |
WQ_CPU_INTENSIVE | 5 | 선택 | Bound | concurrency 관리 제외, worker 증식 방지 |
WQ_POWER_EFFICIENT | 7 | 선택 | 조건부 | 커널 파라미터로 unbound 전환 |
WQ_SYSFS | 9 | 선택 | Any | 런타임 sysfs 튜닝 인터페이스 |
max_active 동시성 제어
/*
* max_active: Per-CPU 또는 Per-NUMA 동시 실행 work item 수 제한
*
* - 0: 기본값 (WQ_DFL_ACTIVE = 256)
* - 1: 순차 실행 (alloc_ordered_workqueue)
* - N: 최대 N개 동시 실행
*
* Bound (Per-CPU) workqueue:
* max_active=4 → 각 CPU에서 최대 4개 work 동시 실행
*
* Unbound workqueue:
* max_active=4 → 각 NUMA 노드에서 최대 4개 work 동시 실행
*
* 주의: max_active는 실행 중인 work만 제한
* pending(대기 중) work 수는 무제한
*/
/* 순차 실행이 필요한 경우 */
struct workqueue_struct *ordered_wq;
ordered_wq = alloc_ordered_workqueue("my_ordered", 0);
/* = alloc_workqueue("my_ordered", __WQ_ORDERED, 1) */
시스템 Workqueue
커널은 미리 생성된 시스템 workqueue를 제공합니다. 대부분의 경우 전용 workqueue를 만들 필요 없이 시스템 workqueue를 사용합니다.
시스템 workqueue 상세 비교:
| Workqueue | 플래그 | max_active | 용도 | 편의 API |
|---|---|---|---|---|
system_wq | (기본) | 256 | 범용, 짧은 작업 | schedule_work() |
system_highpri_wq | WQ_HIGHPRI | 256 | 높은 우선순위 작업 | 직접 queue_work() |
system_long_wq | (기본) | 256 | 장시간 작업 | 직접 queue_work() |
system_unbound_wq | WQ_UNBOUND | 256 | CPU-unbound 작업 | 직접 queue_work() |
system_freezable_wq | WQ_FREEZABLE | 256 | suspend 시 중단 필요 | 직접 queue_work() |
system_power_efficient_wq | WQ_UNBOUND (조건부) | 256 | 전력 효율 최적화 | 직접 queue_work() |
/*
* 시스템 workqueue 선택 가이드:
*
* schedule_work(&work)
* → system_wq에 큐잉
* → 대부분의 드라이버에서 이것으로 충분
* → 짧은 작업, 다른 서브시스템과 간섭 최소
*
* queue_work(system_highpri_wq, &work)
* → 지연시간이 중요한 작업 (인터럽트 후처리 등)
* → nice=-20 worker에서 실행 → 일반 work보다 우선
*
* queue_work(system_long_wq, &work)
* → 장시간 실행될 수 있는 작업
* → system_wq와 같은 pool이지만 의미적으로 분리
* → 주의: WQ_CPU_INTENSIVE가 아니므로 concurrency 관리에 영향
*
* queue_work(system_unbound_wq, &work)
* → CPU에 고정되지 않아야 하는 작업
* → NUMA 로컬리티 활용, 스케줄러 자유 배치
*
* 전용 workqueue 생성이 필요한 경우:
* - flush/cancel 시 다른 서브시스템에 영향 없어야 할 때
* - 특수 플래그 조합 필요 (WQ_UNBOUND | WQ_MEM_RECLAIM 등)
* - max_active 제한으로 동시성 제어 필요
* - /sys/kernel/debug/workqueue에서 독립 모니터링 필요
* - WQ_SYSFS로 런타임 튜닝 필요
*/
Work Item 생명주기
work_struct는 커널의 비동기 실행 단위입니다. 하나의 work item은 Idle → Pending → Running → Idle의 생명주기를 거치며, 중간에 취소(cancel)되거나 동기화(flush)될 수 있습니다. 아래 다이어그램은 전체 상태 전이를 보여줍니다.
/*
* Work Item 상태 전이:
*
* [Idle] work이 어떤 workqueue에도 없는 상태
* │
* │ queue_work() / schedule_work()
* ▼
* [Pending] worklist에 대기 중
* WORK_STRUCT_PENDING 비트 set
* │
* │ kworker가 dequeue
* ▼
* [Running] worker가 콜백 실행 중
* PENDING 클리어, current_work = this
* │
* │ 콜백 완료
* ▼
* [Idle] 다시 큐잉 가능
*
* 핵심 규칙:
* - PENDING인 work를 다시 queue하면 no-op (중복 방지)
* - Running 중에 queue하면 PENDING이 set되어 완료 후 재실행
* - 서로 다른 workqueue에 같은 work를 queue할 수 없음
*/
/* work_struct 내부 */
struct work_struct {
atomic_long_t data; /* flags + pool_workqueue 포인터 */
struct list_head entry; /* worklist 연결 */
work_func_t func; /* 콜백 함수 */
};
코드 설명
include/linux/workqueue.h에 정의된 work_struct와 work item 상태 전이 모델입니다.
- data
atomic_long_t data는 멀티플렉싱 필드로, 하위 비트에WORK_STRUCT_PENDING등의 플래그를, 상위 비트에 현재 소속된pool_workqueue포인터를 저장합니다. 이 설계로 별도의 상태 변수 없이 단일 atomic 연산으로 work의 상태를 관리합니다. - entryworker pool의
worklist에 연결되는 리스트 노드입니다. Pending 상태에서만 연결되며, work 실행이 시작되면 리스트에서 제거됩니다. - funckworker 스레드가 호출할 콜백 함수 포인터입니다.
process_one_work()에서worker->current_func = work->func으로 설정한 뒤 실행합니다. - PENDING 중복 방지이미
WORK_STRUCT_PENDING인 work를 다시queue_work()하면 no-op(false 반환)입니다. Running 중에 큐잉하면 PENDING 비트가 set되어 콜백 완료 후 자동 재실행됩니다.
INIT_WORK / INIT_DELAYED_WORK 매크로(Macro) 분석
/*
* INIT_WORK(): work_struct 초기화 매크로
*
* 반드시 queue_work() 전에 호출해야 함
* 정적 초기화는 DECLARE_WORK() 사용
*/
INIT_WORK(&my_work, my_work_handler);
/* 내부 동작:
* 1. work->data = WORK_STRUCT_NO_POOL (어떤 pool에도 없음)
* 2. INIT_LIST_HEAD(&work->entry) (연결 리스트 초기화)
* 3. work->func = my_work_handler (콜백 함수 등록)
*/
/* 정적 초기화 (글로벌/파일 스코프) */
static DECLARE_WORK(my_global_work, my_global_handler);
/* 컴파일 시점에 초기화, 모듈 로드 즉시 사용 가능 */
/* Delayed Work 초기화 */
INIT_DELAYED_WORK(&my_dwork, my_delayed_handler);
/* 내부: INIT_WORK + timer_setup(&dwork->timer, delayed_work_timer_fn) */
static DECLARE_DELAYED_WORK(my_global_dwork, my_global_delayed_handler);
queue_work() vs schedule_work()
/* schedule_work(): system_wq에 큐잉 (편의 함수) */
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
/* queue_work(): 특정 workqueue에 큐잉 */
bool queue_work(struct workqueue_struct *wq,
struct work_struct *work);
/* queue_work_on(): 특정 CPU에 큐잉 */
bool queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work);
/* 반환값: true = 새로 큐잉됨, false = 이미 pending */
/* 전용 workqueue 사용 vs system_wq 기준:
*
* system_wq 사용 (schedule_work):
* - 짧은 작업, 다른 work와 간섭 적음
* - 대부분의 드라이버에서 적합
*
* 전용 workqueue 생성:
* - flush/cancel 시 다른 서브시스템에 영향 없어야 할 때
* - 특수 플래그 필요 (WQ_UNBOUND, WQ_MEM_RECLAIM 등)
* - max_active 제어 필요
* - /sys/kernel/debug/workqueue에서 독립 모니터링 필요
*/
코드 설명
include/linux/workqueue.h에 정의된 work 큐잉 API들입니다. 내부 호출 체인: queue_work() → queue_work_on() → __queue_work() → insert_work() → wake_up_process().
- schedule_work()
system_wq에 큐잉하는 편의 함수(convenience wrapper)입니다. 대부분의 드라이버에서 짧은 비동기 작업에 충분하며, 전용 workqueue 생성 오버헤드를 피할 수 있습니다. - queue_work()특정 workqueue에 work를 큐잉합니다. 내부적으로
WORK_STRUCT_PENDING비트를 test-and-set하여 중복 큐잉을 방지하고, 현재 CPU의pool_workqueue를 통해 해당 worker pool의worklist에 삽입합니다. - queue_work_on()특정 CPU의 worker pool에 직접 큐잉합니다. 캐시 지역성이 중요한 경우나 인터럽트 핸들러에서 동일 CPU의 bottom-half 처리를 위해 사용합니다.
- 반환값
true는 새로 큐잉되었음을,false는 이미 pending 상태여서 큐잉하지 않았음을 의미합니다. 반환값을 확인하여 work 중복 실행을 방지하는 로직에 활용할 수 있습니다.
Ordered 및 Delayed Workqueue
Workqueue의 실행 모드는 크게 Per-CPU (Bound), Unbound, Ordered로 나뉩니다. 각각의 동작 차이를 아래 다이어그램에서 비교합니다.
Ordered Workqueue
/* Ordered Workqueue: 큐잉 순서대로 하나씩 실행 */
struct workqueue_struct *owq;
owq = alloc_ordered_workqueue("my_ordered", 0);
/*
* 특성:
* - max_active = 1 → 동시에 하나의 work만 실행
* - 큐잉 순서 보장 (FIFO)
* - 내부적으로 __WQ_ORDERED 플래그 + unbound
*
* 사용 시나리오:
* - 상태 머신 이벤트 처리 (순서 중요)
* - 파일시스템 로그/저널 쓰기
* - 하드웨어 초기화 시퀀스
*
* 주의: ordered wq는 WQ_UNBOUND를 암시적으로 포함
* → CPU 마이그레이션 가능 (특정 CPU 고정 아님)
*/
Delayed Work
/* Delayed Work: 지정 시간 후 실행 */
struct delayed_work {
struct work_struct work;
struct timer_list timer;
struct workqueue_struct *wq;
int cpu;
};
/* 초기화 */
INIT_DELAYED_WORK(&dev->dwork, my_delayed_handler);
/* 큐잉: delay jiffies 후 실행 */
queue_delayed_work(my_wq, &dev->dwork,
msecs_to_jiffies(100)); /* 100ms 후 */
/* 시스템 workqueue에 큐잉 */
schedule_delayed_work(&dev->dwork,
msecs_to_jiffies(500)); /* 500ms 후 */
/* mod_delayed_work(): 이미 pending인 delayed work의 타이머 변경 */
mod_delayed_work(my_wq, &dev->dwork,
msecs_to_jiffies(200)); /* 기존 타이머 취소 + 200ms로 재설정 */
/* 반환값: true = 기존 pending work를 변경, false = 새로 큐잉 */
/* 즉시 실행으로 변경 */
mod_delayed_work(my_wq, &dev->dwork, 0);
/* delay=0이면 가능한 빨리 실행 */
코드 설명
include/linux/workqueue.h에 정의된 delayed_work 구조체와 관련 API입니다.
- delayed_work 구조체
work_struct를 내장(embed)하고timer_list를 추가한 확장 구조체입니다. 타이머 만료 시delayed_work_timer_fn()콜백이 내부work_struct를 실제 worklist에 큐잉합니다. - queue_delayed_work()지정된 delay(jiffies 단위) 후에 work를 큐잉합니다. 내부적으로
delay == 0이면queue_work()를 직접 호출하고, 그렇지 않으면add_timer_on()으로 타이머를 설정합니다.msecs_to_jiffies()로 밀리초를 jiffies로 변환합니다. - mod_delayed_work()이미 pending인 delayed work의 타이머를 변경합니다. 기존 타이머를 취소하고 새 delay로 재설정하는 원자적(atomic) 연산입니다. 폴링(polling) 주기 동적 조정이나 debounce 패턴에 유용합니다.
- schedule_delayed_work()
system_wq에 delayed work를 큐잉하는 편의 함수입니다. 주기적 작업(periodic work)에서 콜백 끝에schedule_delayed_work()를 재호출하는 자기 재큐잉(self-requeuing) 패턴이 일반적입니다.
Delayed Work 내부 타이머(Timer) 연동
/*
* delayed_work 내부 동작:
*
* queue_delayed_work(wq, &dwork, delay)
* │
* ├─ delay == 0?
* │ ├─ YES → queue_work(wq, &dwork.work) // 즉시 큐잉
* │ └─ NO → __queue_delayed_work():
* │ 1. dwork->wq = wq
* │ 2. dwork->cpu = current_cpu
* │ 3. timer_setup(&dwork->timer, delayed_work_timer_fn)
* │ 4. add_timer_on(&dwork->timer, cpu)
* │ // 타이머 만료 시 콜백:
* │
* ▼
* delayed_work_timer_fn() (타이머 만료 시 호출)
* │
* ├─ WORK_STRUCT_DELAYED 클리어
* └─ __queue_work(dwork->cpu, dwork->wq, &dwork->work)
* // 이 시점에서 일반 work와 동일하게 처리
*
* 핵심:
* - delayed_work = work_struct + timer_list + wq 포인터
* - 타이머가 만료되면 일반 work로 전환하여 큐잉
* - cancel_delayed_work()는 del_timer() + (선택적) cancel_work()
* - mod_delayed_work()는 del_timer() + queue_delayed_work() 원자적 수행
*/
/* 주기적 작업 패턴 (자기 재큐잉) */
static void periodic_handler(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_device *dev = container_of(dwork,
struct my_device, periodic_work);
/* 주기적 작업 수행 */
do_periodic_check(dev);
/* shutting_down 체크 후 자기 재큐잉 */
if (!dev->shutting_down)
queue_delayed_work(dev->wq, dwork,
msecs_to_jiffies(1000)); /* 1초마다 */
}
ordered vs max_active=1의 차이
alloc_ordered_workqueue()vsalloc_workqueue(..., 0, 1):- 둘 다 동시에 하나의 work만 실행하지만 중요한 차이가 있습니다:
alloc_workqueue("name", 0, 1):- bound (Per-CPU) 워크큐 → 각 CPU의 pool에서 독립적
- 즉, CPU 0에서 1개 + CPU 1에서 1개 = 동시 2개 실행 가능!
- freeze/thaw 시 max_active가 변경될 수 있음
alloc_ordered_workqueue("name", 0):- unbound 워크큐 (단일 pool) → 시스템 전체에서 하나의 work만 실행
- __WQ_ORDERED로 max_active 변경 방지 → 진정한 순서 보장(Ordering)
동기화 패턴: Cancel 및 Flush
취소 패턴
/* Work 취소: 동기적으로 완료 대기 */
bool cancel_work_sync(struct work_struct *work);
/*
* - pending이면: dequeue 후 반환 (true)
* - running이면: 완료를 기다린 후 반환 (false)
* - idle이면: 즉시 반환 (false)
* 주의: 슬립 가능! 인터럽트/atomic 컨텍스트에서 호출 불가
*/
/* Delayed Work 취소 */
bool cancel_delayed_work(struct delayed_work *dwork);
/* 비동기: 타이머만 취소, 이미 실행 중이면 대기 안 함 */
bool cancel_delayed_work_sync(struct delayed_work *dwork);
/* 동기: 타이머 취소 + 실행 중인 콜백 완료 대기 */
/* 안전한 드라이버 해제 패턴 */
static void my_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
/* 1. 더 이상 새 work가 큐잉되지 않도록 플래그 설정 */
dev->shutting_down = true;
/* 2. 모든 work 취소 (실행 중이면 완료 대기) */
cancel_work_sync(&dev->work);
cancel_delayed_work_sync(&dev->dwork);
/* 3. 이 시점에서 work 콜백이 실행되지 않음을 보장 */
}
Flush 패턴
/* flush_work(): 특정 work의 완료 대기 */
bool flush_work(struct work_struct *work);
/* pending/running work 완료를 기다림 */
/* flush_workqueue(): workqueue의 모든 pending work 완료 대기 */
void flush_workqueue(struct workqueue_struct *wq);
/* 호출 시점에 pending인 모든 work의 완료를 기다림 */
/* flush 후에 새로 큐잉된 work는 포함하지 않음 */
/* flush_scheduled_work(): system_wq flush */
void flush_scheduled_work(void);
/* 모듈 해제 시 system_wq에 큐잉된 work 정리용 */
/* drain_workqueue(): 모든 work 완료 대기 + 새 큐잉 차단 */
void drain_workqueue(struct workqueue_struct *wq);
/* destroy_workqueue() 전에 호출하여 잔여 work 처리 */
flush_workqueue() 내부 메커니즘 (color 기반): flush_workqueue()는 "color" 메커니즘으로 구현됩니다. flush 호출 시 현재 work_color를 기록하고 다음 color로 전진합니다. 새로 큐잉되는 work는 새 color를 받으므로, flush 중 새로 큐잉된 work와 기존 work를 구분할 수 있고, 여러 flush를 동시에 처리할 수 있습니다.
| 시점 | color 0 | color 1 | flush 대기 대상 |
|---|---|---|---|
| flush 호출 전 | work A, work B, work C |
없음 | 없음 |
| flush 호출 직후 | work A, work B, work C |
새 큐잉 시작 | color 0 전체 |
| 진행 중 | 실행/소진 | work D, work E |
color 0만 계속 대기 |
| 완료 시점 | 모두 완료 | 남아 있어도 무관 | flush 반환 |
Flush 데드락 주의: work 콜백(Callback) 내에서 자신이 속한 workqueue를 flush하면 데드락이 발생합니다. 또한, ordered workqueue에서 work A의 콜백이 work B를 큐잉하고 flush하면, A가 완료되어야 B가 시작되므로 역시 데드락입니다. cancel_work_sync()도 같은 주의가 필요합니다.
올바른 정리(cleanup) 패턴
/* 드라이버 제거 시 올바른 정리 순서 */
static void my_driver_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
/* 1. 새로운 work 큐잉 방지 */
dev->shutting_down = true;
/* 2. pending delayed_work의 타이머 취소 + 실행 중 work 완료 대기 */
cancel_delayed_work_sync(&dev->periodic_work);
/* 3. 일반 work 취소 + 완료 대기 */
cancel_work_sync(&dev->irq_work);
/* 4. 커스텀 워크큐 파괴 (모든 work가 완료된 후) */
if (dev->wq) {
drain_workqueue(dev->wq);
destroy_workqueue(dev->wq);
}
/* 5. 나머지 리소스 해제 */
free_irq(dev->irq, dev);
}
디버깅 및 모니터링
Workqueue 관련 문제를 진단하려면 debugfs, sysfs, ftrace tracepoint, lockdep 등 여러 도구를 활용합니다. 아래 다이어그램은 문제 유형별 디버깅 접근 경로를 보여줍니다.
debugfs 기반 모니터링
# workqueue 상태 확인
cat /sys/kernel/debug/workqueue
# 출력 예시:
# workqueue CPU POOL ACTIVE/MAX WORKERS FLAGS
# events 0 0 0/256 3
# events 1 2 0/256 2
# events_highpri 0 1 0/256 2 highpri
# my_driver_wq -1 16 2/4 3 unbound
# kworker 스레드 확인
ps aux | grep kworker
# kworker/0:0 - CPU 0 bound worker
# kworker/0:0H - CPU 0 highpri bound worker
# kworker/u8:0 - unbound worker (pool id=8)
# WQ_SYSFS가 설정된 workqueue의 런타임 설정
ls /sys/devices/virtual/workqueue/
# cpumask max_active nice
# 런타임 max_active 변경 (WQ_SYSFS 필요)
echo 8 > /sys/devices/virtual/workqueue/my_wq/max_active
cat /sys/devices/virtual/workqueue/my_wq/cpumask
# wq_watchdog: 정체된 work 탐지
# CONFIG_WQ_WATCHDOG=y + wq_watchdog_thresh_ms (기본 30초)
echo 10000 > /sys/module/workqueue/parameters/watchdog_thresh
# 10초 이상 실행 중인 work 경고
# workqueue tracepoint
echo 1 > /sys/kernel/debug/tracing/events/workqueue/workqueue_queue_work/enable
echo 1 > /sys/kernel/debug/tracing/events/workqueue/workqueue_execute_start/enable
echo 1 > /sys/kernel/debug/tracing/events/workqueue/workqueue_execute_end/enable
cat /sys/kernel/debug/tracing/trace
ftrace를 활용한 Work 실행 분석
# trace-cmd를 사용한 workqueue 이벤트 수집
trace-cmd record -e workqueue sleep 10
trace-cmd report | head -50
# 출력 예시:
# kworker/0:1 workqueue_execute_start: work struct ffff8881234 function my_work_handler
# kworker/0:1 workqueue_execute_end: work struct ffff8881234 function my_work_handler
# 특정 함수의 work 실행 시간 측정
echo 'hist:keys=function:vals=hitcount:sort=hitcount.descending' > \
/sys/kernel/debug/tracing/events/workqueue/workqueue_execute_start/trigger
cat /sys/kernel/debug/tracing/events/workqueue/workqueue_execute_start/hist
# perf를 사용한 workqueue 프로파일링
perf stat -e workqueue:workqueue_queue_work \
-e workqueue:workqueue_execute_start \
-e workqueue:workqueue_execute_end \
-a sleep 10
# lockdep으로 flush 데드락 가능성 감지
# CONFIG_PROVE_LOCKING=y 빌드 후 자동 감지
# 의심 시 dmesg에서 "possible circular locking dependency detected" 확인
dmesg | grep -i "circular\|deadlock\|workqueue"
Best Practices
| 규칙 | 권장 사항 | 이유 |
|---|---|---|
| WQ 선택 | 대부분 system_wq 사용 (schedule_work) | 불필요한 워크큐 생성은 리소스 낭비 |
| 메모리 경로 | reclaim 경로는 WQ_MEM_RECLAIM 필수 | 메모리 부족 시 worker 생성 실패 → 데드락 |
| Long-running | WQ_UNBOUND 사용 | bound pool 동시성 관리 간섭 방지 |
| CPU-heavy | WQ_CPU_INTENSIVE 사용 | 불필요한 worker 증식 방지 |
| 드라이버 해제 | cancel_*_sync() 반드시 호출 | use-after-free 방지 |
| Flush 제약 | work 콜백에서 자기 WQ flush 금지 | 자기 완료를 자기가 대기 → 데드락 |
| IRQ 컨텍스트 | queue_work() 사용 가능 | IRQ-safe, 하지만 cancel_work_sync()는 불가 (슬립) |
| Work 유일성 | 같은 work_struct를 여러 WQ에 큐잉 금지 | 하나의 work는 하나의 WQ에만 속할 수 있음 |
| 스택 할당 | 스택 변수로 work_struct 사용 금지 | 함수 반환 후 work 실행 시 스택 손상 |
| 초기화 | INIT_WORK() 후 큐잉 | 초기화 전 큐잉은 미정의 동작 |
실전 드라이버 예제
#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/platform_device.h>
struct my_device {
struct workqueue_struct *wq;
struct work_struct irq_work;
struct delayed_work monitor_work;
bool shutting_down;
int irq;
void __iomem *regs;
};
/* IRQ bottom half: 인터럽트 후처리 */
static void my_irq_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work,
struct my_device, irq_work);
/* 프로세스 컨텍스트: mutex, 메모리 할당 가능 */
mutex_lock(&dev->lock);
process_hw_data(dev);
mutex_unlock(&dev->lock);
}
/* 주기적 모니터링 */
static void my_monitor_handler(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_device *dev = container_of(dwork,
struct my_device, monitor_work);
check_device_health(dev);
if (!dev->shutting_down)
queue_delayed_work(dev->wq, dwork,
msecs_to_jiffies(5000));
}
/* IRQ 핸들러 (top half) */
static irqreturn_t my_irq_handler(int irq, void *data)
{
struct my_device *dev = data;
/* ACK 하드웨어 인터럽트 */
writel(0x1, dev->regs + IRQ_ACK);
/* bottom half로 지연: queue_work는 IRQ-safe */
queue_work(dev->wq, &dev->irq_work);
return IRQ_HANDLED;
}
/* 프로브: 초기화 */
static int my_probe(struct platform_device *pdev)
{
struct my_device *dev;
dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
/* workqueue 생성: unbound + rescuer 보장 */
dev->wq = alloc_workqueue("my_dev_wq",
WQ_UNBOUND | WQ_MEM_RECLAIM, 4);
if (!dev->wq)
return -ENOMEM;
/* work 초기화 (큐잉 전 필수!) */
INIT_WORK(&dev->irq_work, my_irq_work_handler);
INIT_DELAYED_WORK(&dev->monitor_work, my_monitor_handler);
/* 모니터링 시작 */
queue_delayed_work(dev->wq, &dev->monitor_work,
msecs_to_jiffies(5000));
return 0;
}
/* 제거: 안전한 정리 */
static void my_remove(struct platform_device *pdev)
{
struct my_device *dev = platform_get_drvdata(pdev);
/* 1. 새 work 큐잉 방지 */
dev->shutting_down = true;
/* 2. 인터럽트 해제 (새 IRQ work 방지) */
free_irq(dev->irq, dev);
/* 3. delayed work 취소 + 완료 대기 */
cancel_delayed_work_sync(&dev->monitor_work);
/* 4. 일반 work 취소 + 완료 대기 */
cancel_work_sync(&dev->irq_work);
/* 5. workqueue 파괴 */
destroy_workqueue(dev->wq);
}
PREEMPT_RT와 Workqueue
PREEMPT_RT(Real-Time) 커널은 리눅스의 결정적(Deterministic) 실시간 응답을 위한 패치셋으로, workqueue의 동작 방식에 근본적인 영향을 미칩니다. PREEMPT_RT는 Linux 5.15부터 메인라인에 점진적으로 병합되었으며, 6.x에서 대부분의 핵심 인프라가 통합되었습니다.
RT 커널에서의 Workqueue 변화
| 항목 | 일반 커널 (PREEMPT_NONE/VOLUNTARY) | PREEMPT_RT 커널 |
|---|---|---|
| Worker 선점 | 커널 코드 일부 구간에서만 선점 | 거의 모든 커널 코드에서 완전 선점 가능 |
| spinlock | 실제 spin (인터럽트 비활성화) | rt_mutex 기반 (슬립 가능, 우선순위 상속) |
| softirq 실행 | 인터럽트 컨텍스트에서 직접 실행 | 전용 커널 스레드(ksoftirqd)에서 실행 |
| WQ_BH work | softirq 컨텍스트에서 실행 | ksoftirqd 스레드 내에서 실행 (선점 가능) |
| Worker 우선순위 | SCHED_NORMAL (nice=0 또는 nice=-20) | chrt로 RT 우선순위 설정 가능 |
| local_bh_disable() | softirq 실행 억제 (경량) | Per-CPU 카운터 기반 (마이그레이션 비활성화) |
| pool->lock | raw_spinlock (비선점) | raw_spinlock 유지 (RT에서도 spin) |
RT에서의 Spinlock 변환과 Worker Pool
/*
* PREEMPT_RT의 spinlock 변환이 workqueue에 미치는 영향:
*
* 일반 커널:
* spinlock_t → 실제 spin, 인터럽트/선점 비활성화
* raw_spinlock_t → 실제 spin (RT에서도 동일)
*
* PREEMPT_RT:
* spinlock_t → rt_mutex 기반 (슬립 가능, 우선순위 상속)
* raw_spinlock_t → 실제 spin (짧은 임계 구간만)
*
* workqueue 내부에서:
* pool->lock: raw_spinlock_t → RT에서도 실제 spin (매우 짧은 구간)
* pwq->stats_lock: spinlock_t → RT에서 rt_mutex
*
* 핵심 포인트:
* - pool->lock이 raw_spinlock이므로 work 큐잉/디큐잉은 결정적
* - worker가 work 실행 중 다른 spinlock을 잡으면 RT에서 슬립 가능
* - 이때 wq_worker_sleeping()이 호출되어 pool의 nr_running 감소
* - pool이 새 worker를 깨워 동시성 유지
*/
/* kernel/workqueue.c — pool lock은 raw_spinlock */
struct worker_pool {
raw_spinlock_t lock; /* RT에서도 spin — 임계 구간 최소화 */
int cpu;
int node;
int id;
unsigned int flags;
/* ... */
};
/* RT에서 spinlock → rt_mutex 변환 예시:
* 드라이버의 work 콜백에서 spinlock_t를 잡으면
* RT에서는 슬립 가능 → wq_worker_sleeping() 트리거
*/
static void my_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
spin_lock(&dev->lock);
/* 일반 커널: spin 대기, 선점 비활성화
* PREEMPT_RT: rt_mutex 대기, 슬립 가능 → pool에서 nr_running-- */
dev->data = process_data(dev);
spin_unlock(&dev->lock);
}
Worker 스레드 우선순위 제어
/*
* RT 커널에서 kworker 우선순위 조정:
*
* 기본 kworker 우선순위:
* - normal pool: SCHED_NORMAL, nice=0
* - highpri pool: SCHED_NORMAL, nice=HIGHPRI_NICE_LEVEL (-20)
*
* RT 시스템에서 kworker에 RT 스케줄링 정책 설정:
* chrt -f -p
*
* 주의: kworker는 pool 내에서 동적으로 생성/파괴됨
* → pid가 바뀔 수 있으므로 cgroup으로 관리 권장
*/
/* 방법 1: cgroup을 이용한 kworker RT 우선순위 관리 */
# RT cgroup 생성
# mkdir /sys/fs/cgroup/cpu/rt_workers
# echo 950000 > /sys/fs/cgroup/cpu/rt_workers/cpu.rt_runtime_us
# echo > /sys/fs/cgroup/cpu/rt_workers/cgroup.procs
/* 방법 2: 전용 workqueue + WQ_HIGHPRI */
struct workqueue_struct *rt_wq;
rt_wq = alloc_workqueue("rt_critical",
WQ_HIGHPRI | WQ_MEM_RECLAIM, 1);
/* nice=-20 worker 사용 → 일반 태스크보다 높은 우선순위
* 단, 여전히 SCHED_NORMAL이므로 RT 태스크보다는 낮음 */
/* 방법 3: RT 태스크에서 flush_work() 회피 패턴 */
struct rt_safe_device {
struct work_struct work;
struct completion done; /* flush 대신 completion 사용 */
int result;
};
static void rt_work_handler(struct work_struct *work)
{
struct rt_safe_device *dev =
container_of(work, struct rt_safe_device, work);
dev->result = do_hardware_operation();
complete(&dev->done); /* RT 태스크 깨움 */
}
/* RT 태스크 측: */
init_completion(&dev->done);
queue_work(rt_wq, &dev->work);
/* flush_work() 대신 completion 대기
* → 우선순위 상속이 동작하여 역전 방지 (RT에서 completion은 rt_mutex) */
wait_for_completion(&dev->done);
RT와 WQ_BH 상호작용
/*
* RT 커널에서 WQ_BH workqueue의 실행 모델:
*
* 일반 커널:
* WQ_BH work → __do_softirq() 내에서 실행
* → 인터럽트 비활성화 상태, 선점 불가
* → local_bh_disable() 영역 안
*
* PREEMPT_RT:
* WQ_BH work → ksoftirqd/N 스레드 내에서 실행
* → 프로세스 컨텍스트 (스레드화된 softirq)
* → 선점 가능, 슬립 가능 (단, API 제약은 유지)
* → 우선순위 설정 가능: chrt -f -p 50 $(pidof ksoftirqd/0)
*
* 중요: WQ_BH work의 콜백은 여전히 softirq 규약을 따라야 함
* - spin_lock_bh() 사용 금지 (softirq 재진입 아님)
* - 슬립 API 호출 금지 (RT에서 가능하더라도 이식성 위해)
* - GFP_KERNEL 할당 금지 (GFP_ATOMIC만)
*/
RT 환경 Best Practices
| 규칙 | 이유 | 구현 방법 |
|---|---|---|
| RT 태스크에서 flush_work() 회피 | kworker가 SCHED_NORMAL이므로 우선순위 역전 발생 | struct completion + wait_for_completion() 사용 |
| 중요 work에 WQ_HIGHPRI 사용 | nice=-20으로 실행되어 일반 태스크보다 우선 | alloc_workqueue("name", WQ_HIGHPRI, 0) |
| raw_spinlock vs spinlock 구분 | RT에서 spinlock은 슬립 가능 → 예상치 못한 컨텍스트 전환 | hardirq 핸들러 내: raw_spinlock_t, work 콜백 내: spinlock_t 허용 |
| WQ_BH 콜백에서 블로킹 API 사용 금지 | RT에서 기술적으로 가능하지만 비-RT 호환성 깨짐 | 블로킹 필요 시 일반 workqueue로 릴레이 |
| kworker 우선순위는 cgroup으로 관리 | kworker pid가 동적 변경됨 | systemd slice 또는 cgroup v2 cpu.max 활용 |
| workqueue.watchdog 활성화 | RT에서 우선순위 문제로 work 교착 가능성 증가 | workqueue.watchdog_thresh=10 (기본 30초보다 짧게) |
RT 우선순위 역전(Priority Inversion) 주의: 높은 우선순위 RT 태스크(Task)가 flush_work()로 work 완료를 대기할 때, kworker는 SCHED_NORMAL이므로 중간 우선순위 태스크에 선점될 수 있습니다. 해결: WQ_HIGHPRI 사용, 또는 RT 태스크에서 flush_work() 대신 다른 동기화 메커니즘 사용.
RT 이식성 팁: RT 커널에서도 정상 동작하게 하려면, workqueue가 가장 안전한 Bottom Half 메커니즘입니다. softirq/tasklet은 RT에서 스레드화되면서 기대와 다른 지연 시간을 보일 수 있지만, workqueue는 본래부터 프로세스 컨텍스트이므로 변화가 적습니다.
일반적인 버그 패턴
Workqueue 관련 버그는 커널 패닉, 메모리 손상, 데드락 등 심각한 결과를 초래합니다. 가장 빈번한 버그 패턴과 올바른 해결법을 상세히 분석합니다.
버그 1: work_struct를 포함한 구조체의 조기 해제 (Use-After-Free)
가장 빈번하고 위험한 workqueue 버그입니다. work_struct가 아직 큐에 있거나 실행 중인 상태에서 이를 포함한 구조체를 해제하면 Use-After-Free가 발생합니다.
/* ❌ 잘못된 코드: work 실행 중 구조체 해제 */
struct my_device {
struct work_struct work;
void __iomem *regs;
int data;
};
static void my_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
dev->data = readl(dev->regs); /* dev가 이미 해제되었다면? → 커널 패닉! */
}
static void bad_remove(struct pci_dev *pdev)
{
struct my_device *dev = pci_get_drvdata(pdev);
kfree(dev); /* ❌ work가 아직 큐에 있거나 실행 중일 수 있음! */
}
/* ✅ 올바른 코드: cancel 후 해제 */
static void good_remove(struct pci_dev *pdev)
{
struct my_device *dev = pci_get_drvdata(pdev);
cancel_work_sync(&dev->work); /* 실행 중이면 완료 대기, 큐에 있으면 제거 */
kfree(dev); /* 이제 안전하게 해제 */
}
KASAN 탐지 패턴: 이 버그가 발생하면 KASAN(커널 Address Sanitizer)이 다음과 같은 로그를 출력합니다:
BUG: KASAN: slab-use-after-free in my_work_handler+0x28/0x80
Read of size 4 at addr ffff888012345678 by task kworker/0:1/234
디버깅: CONFIG_KASAN=y 빌드로 재현하면 해제 위치(free backtrace)까지 출력됩니다.
버그 2: 스택에 work_struct 할당
work_struct는 worker 스레드가 나중에 접근하는 구조체이므로, 함수 스택에 할당하면 함수 반환 후 스택 프레임이 재사용될 때 메모리 손상이 발생합니다.
/* ❌ 잘못된 코드: 스택에 work_struct 할당 */
static int bad_function(void)
{
struct work_struct work; /* 스택 변수! */
INIT_WORK(&work, my_handler);
schedule_work(&work);
return 0;
/* 함수 반환 → 스택 프레임 해제 → kworker가 실행 시 → 스택 손상 */
}
/* ✅ 올바른 코드: 힙에 할당하거나 구조체 멤버로 포함 */
/* 패턴 A: 구조체 멤버 (가장 일반적) */
struct my_context {
struct work_struct work;
int param;
};
/* 패턴 B: 동적 할당 (일회성 work) */
static void onetime_handler(struct work_struct *work)
{
struct my_context *ctx = container_of(work, struct my_context, work);
do_something(ctx->param);
kfree(ctx); /* 콜백 내에서 해제 (자기 자신) */
}
static int good_function(int param)
{
struct my_context *ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx) return -ENOMEM;
ctx->param = param;
INIT_WORK(&ctx->work, onetime_handler);
schedule_work(&ctx->work);
return 0;
}
버그 3: Work 콜백에서 자기 Workqueue Flush (데드락)
work 콜백 함수 내에서 자신이 속한 workqueue를 flush하면 자기 자신의 완료를 기다리는 형태가 되어 교착(Deadlock)이 발생합니다.
/* ❌ 잘못된 코드: 콜백 내 자기 workqueue flush → 데드락 */
static struct workqueue_struct *my_wq;
static void my_work_func(struct work_struct *work)
{
do_first_part();
/* 이전 work들이 모두 완료될 때까지 대기하고 싶음 */
flush_workqueue(my_wq); /* ❌ 자기 자신도 이 workqueue에 속함!
* flush는 모든 pending work 완료를 대기
* 자기 자신의 완료도 대기 → 영원히 블록 */
do_second_part();
}
/* ✅ 올바른 패턴: 2단계 work로 분리 */
static void phase1_work(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work1);
do_first_part();
queue_work(my_wq, &dev->work2); /* 2단계 work 큐잉 */
}
static void phase2_work(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work2);
do_second_part(); /* phase1 완료 후 자연스럽게 실행됨 */
}
/* ✅ 또는: flush_work()로 특정 work만 대기 (자기 자신 제외) */
static void my_work_func_fixed(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
flush_work(&dev->other_work); /* ✅ 다른 work는 flush 가능 */
do_combined_work();
}
버그 4: 교차 Flush 데드락 (A↔B)
/* ❌ 잘못된 코드: 두 workqueue 간 교차 flush → 데드락 */
/* work_a의 콜백 (wq_a에서 실행) */
static void work_a_func(struct work_struct *work)
{
flush_workqueue(wq_b); /* wq_b 완료 대기 */
}
/* work_b의 콜백 (wq_b에서 실행) */
static void work_b_func(struct work_struct *work)
{
flush_workqueue(wq_a); /* wq_a 완료 대기 → 데드락! */
}
/*
* 시나리오:
* CPU 0: work_a 실행 → flush_workqueue(wq_b) → wq_b 완료 대기
* CPU 1: work_b 실행 → flush_workqueue(wq_a) → wq_a 완료 대기
* → 서로 대기 → 교착
*
* CONFIG_LOCKDEP=y 에서 탐지됨:
* ============================================
* WARNING: possible recursive locking detected
* ============================================
*/
/* ✅ 올바른 패턴: flush 대신 completion 또는 단방향 의존성 */
버그 5: 초기화 전 큐잉 / 이중 초기화
/* ❌ 버그 5a: INIT_WORK 전에 schedule_work 호출 */
struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
schedule_work(&dev->work); /* ❌ work.func == NULL → 미정의 동작 */
INIT_WORK(&dev->work, handler); /* 너무 늦음! */
/* ❌ 버그 5b: 큐잉된 상태에서 INIT_WORK 재호출 */
schedule_work(&dev->work);
/* ... work가 아직 pending 상태 ... */
INIT_WORK(&dev->work, new_handler); /* ❌ 큐잉 상태 리셋 → 이중 큐잉 가능 */
/* ✅ 올바른 순서 */
struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
INIT_WORK(&dev->work, handler); /* 반드시 먼저 초기화 */
schedule_work(&dev->work); /* 그 다음 큐잉 */
/* 핸들러 변경이 필요하면: cancel 후 재초기화 */
cancel_work_sync(&dev->work);
INIT_WORK(&dev->work, new_handler); /* 안전하게 재초기화 */
schedule_work(&dev->work);
버그 6: Atomic 컨텍스트에서 Flush/Cancel_sync
/* ❌ 잘못된 코드: 인터럽트 핸들러에서 flush */
static irqreturn_t my_irq_handler(int irq, void *data)
{
struct my_device *dev = data;
cancel_work_sync(&dev->work); /* ❌ 인터럽트 컨텍스트에서 슬립!
* cancel_work_sync()는 work 완료까지 대기
* → 인터럽트 컨텍스트에서 슬립 → BUG */
schedule_work(&dev->work);
return IRQ_HANDLED;
}
/* ✅ 올바른 코드: atomic에서는 cancel_work() (non-sync) 사용 */
static irqreturn_t my_irq_handler_fixed(int irq, void *data)
{
struct my_device *dev = data;
/* cancel_work(): 큐에서 제거 시도만, 대기하지 않음 */
cancel_work(&dev->work); /* ✅ non-blocking */
schedule_work(&dev->work);
return IRQ_HANDLED;
}
/*
* flush/cancel API의 컨텍스트 제약:
*
* | API | Atomic ctx | Process ctx | Work 콜백 내 |
* |------------------------|:----------:|:-----------:|:----------:|
* | schedule_work() | ✅ | ✅ | ✅ |
* | cancel_work() | ✅ | ✅ | ✅ |
* | cancel_work_sync() | ❌ | ✅ | ⚠️ |
* | flush_work() | ❌ | ✅ | ⚠️ |
* | flush_workqueue() | ❌ | ✅ | ❌ |
* | destroy_workqueue() | ❌ | ✅ | ❌ |
*
* ⚠️ = 자기 자신 외의 work에 대해서만 안전
*/
버그 7: Delayed Work 재무장(Re-arm) 경쟁
/* ❌ 잘못된 코드: 주기적 delayed work에서 cancel 누락 */
static void periodic_handler(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_device *dev = container_of(dwork, struct my_device, dwork);
do_periodic_check(dev);
schedule_delayed_work(&dev->dwork, HZ); /* 1초 후 재스케줄 */
}
static void bad_remove(struct my_device *dev)
{
cancel_delayed_work(&dev->dwork); /* ❌ non-sync!
* 타이머가 이미 만료되어 work가 실행 중이면?
* → cancel은 실패 (이미 실행 중)
* → 실행 중인 work가 schedule_delayed_work() 재호출
* → dev 해제 후에 다시 실행됨! */
kfree(dev);
}
/* ✅ 올바른 코드: cancel_delayed_work_sync() 사용 */
static void good_remove(struct my_device *dev)
{
/* 방법 1: sync 버전으로 실행 완료까지 대기 + 재무장 방지 플래그 */
dev->shutting_down = true; /* work 콜백에서 확인 */
cancel_delayed_work_sync(&dev->dwork);
kfree(dev);
}
/* 개선된 주기적 핸들러: shutdown 플래그 확인 */
static void periodic_handler_fixed(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_device *dev = container_of(dwork, struct my_device, dwork);
do_periodic_check(dev);
if (!dev->shutting_down)
schedule_delayed_work(&dev->dwork, HZ);
}
버그 탐지 도구
| 도구 | 탐지 대상 | 커널 설정 | 오버헤드 |
|---|---|---|---|
| KASAN | Use-After-Free, 스택 접근 | CONFIG_KASAN=y | ~2x 메모리, ~2x CPU |
| LOCKDEP | 데드락, 교차 flush, 락 역순 | CONFIG_LOCKDEP=y | ~3x 부팅 시간 |
| PROVE_LOCKING | 잠재적 데드락 경로 | CONFIG_PROVE_LOCKING=y | LOCKDEP 포함 |
| DEBUG_OBJECTS | 미초기화 work, 이중 초기화 | CONFIG_DEBUG_OBJECTS_WORK=y | 경미 |
| WQ_WATCHDOG | work 장기 미처리 (교착 의심) | CONFIG_WQ_WATCHDOG=y | 거의 없음 |
개발 중 권장 설정: CONFIG_KASAN=y, CONFIG_LOCKDEP=y, CONFIG_DEBUG_OBJECTS_WORK=y를 모두 활성화하면 위의 대부분의 버그를 초기에 탐지할 수 있습니다. 프로덕션에서는 CONFIG_WQ_WATCHDOG=y만 유지하세요.
WQ_BH: Bottom Half Workqueue (6.9+)
Linux 6.9에서 도입된 WQ_BH 플래그는 workqueue를 softirq 컨텍스트에서 실행할 수 있게 만드는 혁신적 변경입니다. 이는 오랜 숙원이었던 tasklet/softirq 기반 Bottom Half 처리를 workqueue 프레임워크로 통합하기 위한 핵심 기반입니다.
도입 배경
기존 tasklet은 여러 구조적 문제가 있었습니다:
- 직렬화(Serialization) 제약: 같은 tasklet은 시스템 전체에서 하나만 실행 — 멀티코어 확장성 부족
- API 불일치: workqueue와 완전히 다른 API — 코드 유지보수 부담
- 취소/동기화 미비:
cancel_work_sync()같은 안전한 취소 메커니즘 부재 - PREEMPT_RT 문제: RT 커널에서 스레드화 시 예측 불가능한 지연
WQ_BH는 workqueue의 풍부한 API와 관리 인프라를 유지하면서도 softirq 수준의 낮은 지연을 제공합니다.
시스템 BH Workqueue
/* kernel/workqueue.c — 시스템 BH workqueue 선언 */
struct workqueue_struct *system_bh_wq __read_mostly;
struct workqueue_struct *system_bh_highpri_wq __read_mostly;
EXPORT_SYMBOL_GPL(system_bh_wq);
EXPORT_SYMBOL_GPL(system_bh_highpri_wq);
/* 초기화 — init_workqueues()에서 */
system_bh_wq = alloc_workqueue("events_bh", WQ_BH, 0);
system_bh_highpri_wq = alloc_workqueue("events_bh_highpri",
WQ_BH | WQ_HIGHPRI, 0);
| 시스템 WQ | 플래그 | 용도 |
|---|---|---|
system_bh_wq | WQ_BH | 일반 softirq 수준 Bottom Half 처리 (tasklet 대체) |
system_bh_highpri_wq | WQ_BH | WQ_HIGHPRI | 높은 우선순위 softirq Bottom Half (hi-tasklet 대체) |
tasklet에서 WQ_BH로 마이그레이션
/* === 기존: tasklet 방식 === */
static void my_tasklet_handler(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
/* Bottom Half 처리 */
}
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, 0);
/* IRQ 핸들러에서 */
tasklet_schedule(&my_tasklet);
/* === 변환: WQ_BH 방식 === */
static void my_bh_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, bh_work);
/* 동일한 Bottom Half 처리 */
}
/* 초기화에서 */
INIT_WORK(&dev->bh_work, my_bh_handler);
/* IRQ 핸들러에서 */
queue_work(system_bh_wq, &dev->bh_work);
WQ_BH 제약사항
| 항목 | 일반 Workqueue | WQ_BH Workqueue |
|---|---|---|
| 실행 컨텍스트 | 프로세스 컨텍스트 (kworker) | softirq 컨텍스트 |
| 슬립 가능 | 가능 (mutex, msleep 등) | 불가 — BUG 발생 |
| 메모리 할당 | GFP_KERNEL 사용 가능 | GFP_ATOMIC만 허용 |
| max_active | 설정 가능 | 무시됨 (softirq에서 직렬 실행) |
| flush/cancel | 완전 지원 | cancel_work_sync() 사용 가능 |
| delayed_work | 지원 | 지원 (타이머 만료 후 softirq에서 실행) |
| PREEMPT_RT | 프로세스 컨텍스트 유지 | softirq 스레드에서 실행 |
WQ_BH workqueue의 work 콜백 안에서는 절대 슬립하면 안 됩니다. mutex_lock(), msleep(), kmalloc(GFP_KERNEL) 등을 호출하면 커널 BUG가 발생합니다. softirq 컨텍스트라는 점을 항상 기억하세요.
void (*)(unsigned long)에서 void (*)(struct work_struct *)로 변경됩니다. container_of() 패턴으로 디바이스 구조체에 접근하세요. 콜백 내에서 슬립하는 코드가 없는지 반드시 확인하세요.
Affinity Scope (6.5+)
Linux 6.5에서 도입된 Affinity Scope는 unbound workqueue의 worker가 실행될 CPU 범위를 세밀하게 제어하는 메커니즘입니다. 캐시(Cache) 지역성과 처리량(Throughput) 사이의 균형을 워크로드 특성에 맞춰 조절할 수 있습니다.
스코프 유형
| 스코프 | 범위 | 특성 | 적합한 워크로드 |
|---|---|---|---|
cpu | 단일 CPU | 최대 캐시 지역성, 최소 처리량 | L1 캐시 민감 작업 |
smt | SMT 형제 CPU | L1/L2 공유 활용 | 하이퍼스레딩 활용 작업 |
cache | LLC 공유 CPU | LLC 캐시 지역성 | 대부분의 I/O 작업 (기본값) |
numa | NUMA 노드 | 메모리 지역성 | 메모리 집약 작업 |
system | 전체 시스템 | 최대 처리량, 지역성 없음 | CPU 부하 분산(Load Balancing) 필요 시 |
default | 커널 기본값 | 현재 cache | 특별한 요구 없을 때 |
API와 sysfs 인터페이스
/* 커널 API — alloc_workqueue()에서 affinity scope 지정 */
enum wq_affn_scope {
WQ_AFFN_CPU, /* 단일 CPU */
WQ_AFFN_SMT, /* SMT 형제 */
WQ_AFFN_CACHE, /* LLC 캐시 공유 */
WQ_AFFN_NUMA, /* NUMA 노드 */
WQ_AFFN_SYSTEM, /* 전체 시스템 */
WQ_AFFN_DFL, /* 기본값 (= cache) */
};
/* workqueue 생성 시 affn_scope 설정 (6.5+) */
struct workqueue_struct *wq;
wq = alloc_workqueue("my_wq", WQ_UNBOUND, 0);
/* 기본 scope는 WQ_AFFN_DFL (= cache) */
# sysfs를 통한 런타임 affinity scope 확인 및 변경
# (WQ_SYSFS 플래그가 설정된 workqueue만 가능)
# 현재 affinity scope 확인
cat /sys/devices/virtual/workqueue/writeback/affinity_scope
# 출력 예: cache
# affinity scope를 numa로 변경
echo numa > /sys/devices/virtual/workqueue/writeback/affinity_scope
# 시스템 전체 기본값 확인/변경
cat /sys/devices/virtual/workqueue/cpumask
# 0000ffff (16-CPU 시스템)
스코프별 성능 특성
/*
* Affinity Scope 선택 가이드:
*
* cache (기본값, 권장):
* - LLC 캐시를 공유하는 CPU 그룹 내에서 work 실행
* - 대부분의 워크로드에서 좋은 균형
* - 예: 4-코어 CCX에서 work가 해당 CCX 내 코어로 분산
*
* numa:
* - 대규모 메모리 할당/접근 패턴에 적합
* - NUMA 원격 접근 페널티 회피
* - 예: 대용량 파일 I/O, 페이지 캐시 작업
*
* system:
* - 부하가 불균형할 때 전체 CPU에 분산
* - 캐시 지역성 포기 대신 최대 처리량 확보
* - 예: 네트워크 패킷 처리, 병렬 암호화
*
* cpu/smt:
* - 매우 작은 work item에서 캐시 오염 최소화
* - 처리량이 제한될 수 있으므로 신중히 사용
*/
smt 스코프는 cpu와 동일하게 동작합니다. /sys/devices/system/cpu/cpu0/topology/에서 토폴로지 정보를 확인할 수 있습니다.
스코프별 성능 비교 (참고 수치)
다음은 대표적인 2-소켓 NUMA 서버(16코어/소켓, LLC 4개)에서의 스코프별 상대 성능입니다. 워크로드에 따라 최적 스코프가 달라지므로 반드시 실측이 필요합니다.
| 스코프 | Work 크기 | 캐시 히트율 | 처리량(상대) | 지연시간(상대) | 권장 시나리오 |
|---|---|---|---|---|---|
cpu | 작은 work (<1KB 데이터) | ~95% | 0.4x | 1.0x (최소) | L1 핫 데이터 반복 접근 |
smt | 작은~중간 | ~90% | 0.6x | 1.1x | SMT 형제 간 L1/L2 공유 활용 |
cache | 중간 (4-64KB) | ~75% | 1.0x (기준) | 1.5x | 기본값: 대부분의 I/O, 네트워크 |
numa | 큰 work (메모리 집약) | ~50% | 1.3x | 2.0x | 대용량 파일 I/O, 페이지 작업 |
system | 가변적 | ~30% | 1.6x (최대) | 3.0x | 극대 병렬 처리 (암호화, 압축) |
실측 방법: perf stat -e cache-misses,LLC-load-misses으로 캐시 미스율을 측정하고, bpftrace로 work 실행 지연시간을 히스토그램으로 확인하세요. scope 변경 후 동일 벤치마크로 비교하면 최적 scope를 찾을 수 있습니다.
Unbound Workqueue NUMA 배치
Unbound workqueue는 특정 CPU에 바인딩되지 않는 worker pool을 사용합니다. NUMA 시스템에서 이 pool의 배치 정책은 성능에 중대한 영향을 미칩니다. CMWQ는 NUMA 노드별로 worker pool을 분리하여 메모리 지역성을 보장합니다.
NUMA 노드별 풀 할당 정책
/*
* Unbound Worker Pool NUMA 배치:
*
* alloc_workqueue(WQ_UNBOUND) 호출 시:
* 1. workqueue_attrs (nice, cpumask, affn_scope) 결정
* 2. 각 NUMA 노드에 대해 wq_calc_node_cpumask() 호출
* 3. 노드의 online CPU와 workqueue cpumask의 교집합 계산
* 4. 교집합이 비어있으면 → wq_cpumask 전체를 사용 (fallback)
* 5. 교집합이 있으면 → 해당 노드 전용 pool_workqueue 생성
* 6. 같은 attrs를 가진 pool이 이미 존재하면 공유 (refcount 증가)
*/
/* kernel/workqueue.c — NUMA 노드별 cpumask 계산 */
static bool wq_calc_node_cpumask(
const struct workqueue_attrs *attrs,
int node, int cpu_going_down,
cpumask_t *cpumask)
{
/* 해당 NUMA 노드의 online CPU 마스크 */
if (!cpumask_and(cpumask, cpumask_of_node(node),
attrs->cpumask))
goto use_dfl;
/* cpu_going_down 제외 (hotplug 처리) */
if (cpu_going_down >= 0)
cpumask_clear_cpu(cpu_going_down, cpumask);
/* affinity scope에 따른 추가 필터링 (6.5+) */
if (wq_affn_scope_valid(attrs->affn_scope))
apply_affn_scope(cpumask, attrs);
if (cpumask_empty(cpumask))
goto use_dfl;
return true;
use_dfl:
cpumask_copy(cpumask, attrs->cpumask);
return false;
}
WQ_UNBOUND + __WQ_ORDERED 상호작용
/*
* Ordered Workqueue와 NUMA:
*
* alloc_ordered_workqueue()는 내부적으로:
* alloc_workqueue(name, WQ_UNBOUND | __WQ_ORDERED, 1)
*
* __WQ_ORDERED 워크큐는 NUMA 분산을 하지 않습니다:
* - max_active = 1 → 항상 하나의 work만 실행
* - 단일 pool_workqueue만 사용 (NUMA 노드별 분리 없음)
* - 순서 보장이 NUMA 지역성보다 중요
*
* 주의: ordered + NUMA 최적화가 필요하면
* 직접 per-node ordered workqueue를 생성해야 함
*/
struct workqueue_struct *ordered_wq;
ordered_wq = alloc_ordered_workqueue("my_ordered", 0);
/* 내부: WQ_UNBOUND | __WQ_ORDERED, max_active=1 */
대규모 NUMA 시스템 최적화
numa로 설정하여 원격 NUMA 접근을 최소화하세요. /sys/devices/virtual/workqueue/*/cpumask로 런타임 조정이 가능합니다.
# 4-NUMA 노드 시스템에서 unbound workqueue 최적화
# 각 NUMA 노드의 CPU 확인
lscpu | grep NUMA
# NUMA node0 CPU(s): 0-15
# NUMA node1 CPU(s): 16-31
# NUMA node2 CPU(s): 32-47
# NUMA node3 CPU(s): 48-63
# writeback workqueue의 affinity scope를 numa로 설정
echo numa > /sys/devices/virtual/workqueue/writeback/affinity_scope
# 특정 workqueue를 특정 NUMA 노드로 제한
echo 0000ffff > /sys/devices/virtual/workqueue/my_wq/cpumask
# Node 0 (CPU 0-15)에서만 실행
Worker Pool 내부 상세
worker_pool은 CMWQ의 실행 엔진입니다. 각 pool은 자체적으로 worker 스레드를 관리하며, 동시성 수준을 자동으로 조절합니다. 이 섹션에서는 pool의 내부 필드, worker 상태 전이, manager 역할을 상세히 분석합니다.
worker_pool 구조체 상세
/* kernel/workqueue.c — worker_pool 전체 필드 (v6.x) */
struct worker_pool {
raw_spinlock_t lock; /* pool 보호 잠금 */
int cpu; /* bound pool: CPU 번호, unbound: -1 */
int node; /* NUMA 노드 ID */
int id; /* 풀 고유 ID (I: 열에 표시) */
unsigned int flags; /* POOL_MANAGER_ACTIVE 등 */
struct list_head worklist; /* 대기 중인 work_struct 리스트 */
int nr_workers; /* 전체 worker 수 */
int nr_idle; /* idle worker 수 */
/* 핵심: 동시성 관리 카운터 */
int nr_running; /* 현재 실행 중인 worker 수 */
/* Bound pool: scheduler 콜백으로 정확히 추적 */
/* nr_running == 0 && worklist 비어있지 않으면 */
/* → 즉시 idle worker 깨움 또는 새 worker 생성 */
struct list_head idle_list; /* idle worker 리스트 (LRU 순서) */
struct timer_list idle_timer; /* 300초 후 idle worker 소멸 */
struct timer_list mayday_timer;/* worker 생성 실패 시 재시도 */
struct ida worker_ida; /* worker ID 할당자 */
struct workqueue_attrs *attrs; /* unbound: nice, cpumask, scope */
struct hlist_node hash_node; /* unbound pool 해시 테이블 */
int refcnt; /* 참조 카운트 (공유 pool) */
struct rcu_head rcu; /* RCU 콜백 (안전 해제) */
};
Manager 역할: worker 생성과 소멸
/*
* manage_workers() — Worker Pool의 핵심 관리 로직
*
* 호출 시점: worker_thread()에서 POOL_MANAGER_ACTIVE가 아닐 때
* 관리자 역할을 맡은 worker가 실행
*
* 판단 기준:
* 1. need_more_worker(): worklist 비어있지 않고 nr_running == 0
* → maybe_create_worker() 호출
* 2. too_many_workers(): idle worker 과다
* → idle_timer에 의해 정리 (별도 타이머)
*/
static bool manage_workers(struct worker *worker)
{
struct worker_pool *pool = worker->pool;
if (pool->flags & POOL_MANAGER_ACTIVE)
return false;
pool->flags |= POOL_MANAGER_ACTIVE;
/* worker 생성 필요 여부 판단 */
maybe_create_worker(pool);
pool->flags &= ~POOL_MANAGER_ACTIVE;
return true;
}
static void maybe_create_worker(struct worker_pool *pool)
{
restart:
/* nr_idle가 0이면 worker 생성 시도 */
while (!may_start_working(pool)) {
if (create_worker(pool) || !need_to_create_worker(pool))
break;
/* 생성 실패 — 잠시 후 재시도 */
schedule_timeout_interruptible(CREATE_COOLDOWN);
if (!need_to_create_worker(pool))
break;
goto restart;
}
}
pool->lock contention 분석
/*
* pool->lock 경쟁 지점:
*
* 1. queue_work() → insert_work() — work 삽입 시
* 2. worker_thread() → process_one_work() — work 꺼내기 시
* 3. manage_workers() — worker 생성/소멸 판단 시
* 4. flush/cancel 경로 — 동기화 대기 시
*
* 경쟁 완화 설계:
* - Bound pool: CPU별 독립 pool → 다른 CPU와 lock 경쟁 없음
* - Unbound pool: 같은 NUMA 노드의 work가 같은 pool에 들어감
* - try_to_grab_pending(): lock 없이 먼저 시도 (낙관적)
*
* 높은 contention 발생 시:
* - unbound pool을 NUMA 노드별로 분리 (affinity scope = numa)
* - 커스텀 workqueue로 분리하여 pool 공유 해소
* - WQ_UNBOUND 대신 per-CPU workqueue 고려
*/
CPU Intensive Work
WQ_CPU_INTENSIVE 플래그가 설정된 workqueue에서 실행되는 work는 CMWQ 동시성 관리에서 특별 취급됩니다. 이 work가 오래 실행되어도 같은 pool의 다른 work 실행을 방해하지 않습니다.
동작 원리
/*
* WQ_CPU_INTENSIVE 동작 원리:
*
* 일반 workqueue:
* - worker가 work 실행 시 pool->nr_running에 포함
* - nr_running > 0이면 새 worker를 깨우지 않음
* - 즉, 하나의 work가 오래 실행되면 다른 work 지연
*
* WQ_CPU_INTENSIVE:
* - worker가 work 실행 시작 시 nr_running에서 제외
* - pool->nr_running--; → 다른 work를 위한 worker 활성화 가능
* - work 완료 시 다시 nr_running++
*
* 즉, CPU-intensive work는 "보이지 않는 worker"가 되어
* 다른 work의 실행을 블록하지 않습니다.
*/
/* kernel/workqueue.c — process_one_work() 내부 */
static void process_one_work(struct worker *worker,
struct work_struct *work)
{
struct pool_workqueue *pwq = get_work_pwq(work);
bool cpu_intensive = pwq->wq->flags & WQ_CPU_INTENSIVE;
/* CPU-intensive: 동시성 카운터에서 제외 */
if (cpu_intensive)
worker_clr_flags(worker, WORKER_NOT_RUNNING);
/* 이후 pool->nr_running은 이 worker를 세지 않음 */
/* work 콜백 실행 */
worker->current_func = work->func;
worker->current_func(work);
/* CPU-intensive: 다시 동시성 카운터에 포함 */
if (cpu_intensive)
worker_set_flags(worker, WORKER_NOT_RUNNING);
}
코드 설명
kernel/workqueue.c의 process_one_work()로, kworker가 work를 하나 꺼내 실행하는 핵심 함수입니다. 전체 호출 체인: worker_thread() → process_one_work() → worker->current_func(work).
- get_work_pwq()
work->data의 상위 비트에서pool_workqueue포인터를 추출합니다. 이를 통해 해당 work가 어떤 workqueue에서 왔는지,WQ_CPU_INTENSIVE등의 플래그를 확인합니다. - WQ_CPU_INTENSIVE 처리CPU-intensive work는 실행 시작 시
worker_clr_flags(worker, WORKER_NOT_RUNNING)을 호출하여pool->nr_running카운터에서 자신을 제외합니다. 이로 인해 pool은 이 worker를 "보이지 않는" 상태로 취급하여, 다른 pending work를 위한 idle worker를 깨울 수 있습니다. - current_func 실행
worker->current_func = work->func으로 설정한 뒤 콜백을 직접 호출합니다. 이 필드는flush_work()에서 특정 work가 현재 실행 중인지 확인하는 데 사용됩니다. - nr_running 복원콜백 완료 후 CPU-intensive work는
worker_set_flags(worker, WORKER_NOT_RUNNING)으로 다시 동시성 카운터에 포함됩니다. 이 대칭적 처리가 pool의 동시성 수준을 정확히 유지합니다.
스케줄러(Scheduler) 연동: wq_worker_sleeping/running
/*
* 스케줄러는 kworker 스레드가 슬립/깨어남을 workqueue에 알립니다.
* 이를 통해 pool이 동시성 수준을 정확히 추적합니다.
*/
/* kernel/sched/core.c에서 호출 */
void wq_worker_sleeping(struct task_struct *task)
{
struct worker *worker = kthread_data(task);
struct worker_pool *pool = worker->pool;
/* nr_running 감소 */
if (atomic_dec_and_test(&pool->nr_running) &&
!list_empty(&pool->worklist))
/* nr_running이 0이 되면 idle worker 깨움 */
wake_up_worker(pool);
}
void wq_worker_running(struct task_struct *task)
{
struct worker *worker = kthread_data(task);
if (!worker_test_flags(worker, WORKER_NOT_RUNNING))
atomic_inc(&worker->pool->nr_running);
}
사용 사례
| 서브시스템 | 용도 | 이유 |
|---|---|---|
| Crypto | 대량 암호화(Encryption)/해시(Hash) 작업 | AES-XTS 전체 디스크 암호화 등 오래 실행 |
| Compression | zstd/lz4 압축/해제 | Btrfs 투명 압축, zswap 등 |
| Filesystem | ext4 lazy init, XFS CIL push | 초기화/로그 작업이 수백 ms 소요 |
| RAID/dm | stripe 계산, mirror 복구 | 대용량 I/O 패턴 처리 |
WQ_CPU_INTENSIVE를 설정하세요. 그렇지 않으면 같은 pool의 짧은 work들이 불필요하게 지연됩니다. 단, 이 플래그는 WQ_UNBOUND와 함께 사용할 때 가장 효과적입니다.
메모리 압박/회수 경로에서의 Workqueue
WQ_MEM_RECLAIM 플래그는 메모리 회수 경로에서 workqueue가 데드락 없이 동작하도록 보장하는 핵심 메커니즘입니다. 이 플래그가 설정되면 workqueue에 전용 rescuer 스레드가 생성됩니다.
문제: 메모리 할당 실패와 데드락
/*
* 메모리 압박 시나리오:
*
* 1. 블록 I/O writeback 경로가 work를 큐잉
* 2. work 실행을 위해 새 kworker 스레드 필요
* 3. kthread_create()가 kmalloc()을 호출 → 메모리 부족!
* 4. 메모리 회수를 위해 dirty 페이지 writeback 필요
* 5. writeback은 work 실행에 의존 → 데드락!
*
* 해결: WQ_MEM_RECLAIM → rescuer 스레드
* rescuer는 부팅 시 미리 생성되어 메모리 할당 없이 work 처리 가능
*/
rescuer_thread() 상세 동작
/* kernel/workqueue.c — rescuer 스레드 핵심 루프 */
static int rescuer_thread(void *__rescuer)
{
struct worker *rescuer = __rescuer;
struct workqueue_struct *wq = rescuer->rescue_wq;
bool should_stop;
set_user_nice(current, RESCUER_NICE_LEVEL); /* 높은 우선순위 */
repeat:
set_current_state(TASK_IDLE);
should_stop = kthread_should_stop();
/* mayday 리스트 순회 */
spin_lock_irq(&wq_mayday_lock);
while (!list_empty(&wq->maydays)) {
struct pool_workqueue *pwq;
pwq = list_first_entry(&wq->maydays,
struct pool_workqueue, mayday_node);
list_del_init(&pwq->mayday_node);
spin_unlock_irq(&wq_mayday_lock);
/* 해당 pool에 임시 합류하여 work 처리 */
worker_attach_to_pool(rescuer, pwq->pool);
process_scheduled_works(rescuer);
worker_detach_from_pool(rescuer);
spin_lock_irq(&wq_mayday_lock);
}
spin_unlock_irq(&wq_mayday_lock);
if (should_stop)
return 0;
schedule();
goto repeat;
}
코드 설명
kernel/workqueue.c의 rescuer_thread()로, WQ_MEM_RECLAIM workqueue의 안전장치(safety net)입니다. 호출 경로: mayday_timer 만료 → send_mayday() → wake_up_process(rescuer->task).
- RESCUER_NICE_LEVELrescuer 스레드는 높은 스케줄링 우선순위로 실행되어 메모리 부족 상황에서도 빠르게 work를 처리할 수 있습니다. 부팅 시 미리 생성되므로 메모리 할당 없이 즉시 동작합니다.
- maydays 리스트 순회
wq->maydays는 구출이 필요한pool_workqueue들의 리스트입니다. rescuer는 이 리스트를 순회하며 각 pool에 임시로 합류(worker_attach_to_pool)하여 worklist의 work를 직접 실행합니다. - worker_attach_to_pool / worker_detach_from_poolrescuer가 특정 pool에 합류하여 work를 처리한 뒤 분리되는 패턴입니다. rescuer는 workqueue당 1개만 존재하므로 여러 pool을 순차적으로 방문하여 최소한의 전진 진행(forward progress)을 보장합니다.
- schedule() 대기모든 mayday를 처리한 후
TASK_IDLE상태로 슬립합니다. 다음mayday_timer만료 시 다시 깨어나 구출을 반복합니다.kthread_should_stop()체크로 workqueue 소멸 시 안전하게 종료합니다.
실전 시나리오: 블록 I/O writeback
| 단계 | 상황 | 동작 |
|---|---|---|
| 1 | dirty 페이지 다수 발생 | writeback work가 큐잉됨 |
| 2 | 모든 worker가 I/O 대기 중 | pool에 idle worker 없음 |
| 3 | 새 worker 생성 시도 | kthread_create() → kmalloc() 실패 |
| 4 | mayday_timer 만료 | send_mayday() → rescuer 깨움 |
| 5 | rescuer가 pool에 합류 | writeback work 직접 실행 |
| 6 | dirty 페이지 정리됨 | 메모리 확보 → 정상 worker 생성 가능 |
WQ_MEM_RECLAIM workqueue의 work 콜백에서 GFP_KERNEL 할당을 사용하면 메모리 회수 재진입이 발생할 수 있습니다. 메모리 회수 경로의 work에서는 GFP_NOIO 또는 GFP_NOFS를 사용하세요.
전력 관리와 Workqueue
Workqueue는 전력 관리(PM) 서브시스템과 밀접하게 상호작용합니다. suspend/resume 경로, CPU hotplug, idle 자원 회수까지 workqueue의 PM 통합 메커니즘을 상세히 분석합니다.
WQ_FREEZABLE: suspend 시 작업 동결
/*
* WQ_FREEZABLE 워크큐:
*
* 시스템 suspend (pm_suspend) 시:
* 1. freeze_workqueues_begin() 호출
* 2. WQ_FREEZABLE 워크큐의 모든 pwq에 max_active = 0 설정
* 3. 새 work는 큐잉되지만 실행되지 않음 (동결 상태)
* 4. resume 시 thaw_workqueues() → max_active 복원 → 누적된 work 실행
*
* 사용 시점:
* - suspend 중에 하드웨어 접근이 위험한 work
* - 사용자 공간 의존 작업 (이미 frozen)
* - 파일시스템 동기화 작업
*/
/* WQ_FREEZABLE workqueue 생성 예시 */
struct workqueue_struct *my_wq;
my_wq = alloc_workqueue("my_freezable",
WQ_FREEZABLE | WQ_MEM_RECLAIM, 0);
/* 시스템 제공 freezable workqueue */
extern struct workqueue_struct *system_freezable_wq;
extern struct workqueue_struct *system_freezable_power_efficient_wq;
wq_watchdog: worker pool 교착 감지
/*
* Workqueue Watchdog (CONFIG_WQ_WATCHDOG):
*
* pool에 큐잉된 work가 지정 시간 이상 처리되지 않으면 경고 출력.
* 기본 임계값: 30초 (workqueue.watchdog_thresh=30)
*
* 검사 주기: pool->watchdog_ts를 주기적으로 확인
* - work가 완료되면 watchdog_ts 갱신
* - watchdog_ts가 임계값 이상 경과하면 BUG 보고
*
* 출력 예시:
* BUG: workqueue lockup - pool cpus=0 node=0 flags=0x0
* nice=0 stuck for 32s!
*/
/* 부트 파라미터로 임계값 설정 */
/* kernel cmdline: workqueue.watchdog_thresh=60 (60초) */
/* 런타임: /sys/module/workqueue/parameters/watchdog_thresh */
CPU Hotplug와 Worker 마이그레이션
/*
* CPU Hotplug 시 Workqueue 동작 상세:
*
* CPU offline:
* 1. workqueue_offline_cpu() 호출
* 2. 해당 CPU의 bound pool에서 pending work를 unbound pool로 마이그레이션
* 3. worker 스레드는 해당 CPU에서 unbind
* 4. 진행 중인 work는 완료까지 허용 (drain)
*
* CPU online:
* 1. workqueue_online_cpu() 호출
* 2. bound pool 재활성화
* 3. unbound pool의 cpumask 갱신 (새 CPU 포함)
* 4. worker 스레드 재생성 (필요 시)
*
* 주의: ordered workqueue는 마이그레이션 중에도 순서 보장
*/
/* CPU hotplug 콜백 등록 (커널 내부) */
static int workqueue_prepare_cpu(unsigned int cpu);
static int workqueue_online_cpu(unsigned int cpu);
static int workqueue_offline_cpu(unsigned int cpu);
/* 드라이버에서 CPU hotplug를 고려한 workqueue 사용 패턴 */
static void my_work_handler(struct work_struct *work)
{
int cpu = smp_processor_id();
/* bound workqueue: 이 함수가 실행되는 CPU가 바뀔 수 있음!
* CPU offline → drain → 다른 CPU에서 재실행 가능
* Per-CPU 데이터 접근 시 주의 필요 */
pr_debug("running on CPU %d\n", cpu);
}
/* CPU 고정이 필요한 경우: unbound WQ + cpumask 제어 */
struct workqueue_struct *pinned_wq;
pinned_wq = alloc_workqueue("pinned", WQ_UNBOUND | WQ_SYSFS, 0);
/* sysfs에서 cpumask 설정:
* echo 0f > /sys/devices/virtual/workqueue/pinned/cpumask
* → CPU 0-3에서만 실행 */
WQ_POWER_EFFICIENT 플래그
/*
* WQ_POWER_EFFICIENT:
*
* workqueue.power_efficient 커널 파라미터가 활성화되면:
* WQ_POWER_EFFICIENT 워크큐 → WQ_UNBOUND로 동작
* → 스케줄러가 idle CPU를 깨우지 않고 busy CPU에서 실행
* → CPU가 C-state에 머물 수 있어 전력 절약
*
* 커널 cmdline: workqueue.power_efficient=1
* (또는 CONFIG_WQ_POWER_EFFICIENT_DEFAULT=y)
*
* 주의: 지연 시간이 증가할 수 있음 (busy CPU 대기)
*/
struct workqueue_struct *pe_wq;
pe_wq = alloc_workqueue("my_pe_wq",
WQ_POWER_EFFICIENT | WQ_MEM_RECLAIM, 0);
/* 시스템 제공 power-efficient workqueue */
extern struct workqueue_struct *system_power_efficient_wq;
workqueue.power_efficient=1을 설정하면 시스템 전체의 WQ_POWER_EFFICIENT workqueue가 unbound로 전환되어 전력 소비를 줄일 수 있습니다. 서버 환경에서는 지연 시간 증가를 주의하세요.
sysfs 인터페이스 상세
WQ_SYSFS 플래그가 설정된 workqueue는 /sys/devices/virtual/workqueue/ 아래에 디렉터리를 노출합니다. 이를 통해 운영 중인 시스템에서 workqueue 파라미터를 동적으로 조정할 수 있습니다.
디렉터리 구조
# WQ_SYSFS 플래그가 설정된 workqueue만 노출됨
ls /sys/devices/virtual/workqueue/
# writeback events_power_efficient crypto ...
# 각 workqueue 디렉터리의 파일들
ls /sys/devices/virtual/workqueue/writeback/
# affinity_scope cpumask max_active nice numa per_cpu power uevent
per-workqueue 설정 파라미터
| 파일 | 권한 | 설명 | 기본값 |
|---|---|---|---|
max_active | rw | 동시 실행 가능한 최대 work 수 | 256 (또는 WQ_MAX_ACTIVE) |
nice | rw | worker 스레드의 nice 값 (-20~19) | 0 (highpri: -20) |
cpumask | rw | unbound worker가 실행 가능한 CPU 마스크 | 전체 CPU |
numa | ro | NUMA 인식 여부 | 1 (unbound) |
affinity_scope | rw | affinity 스코프 (6.5+) | cache |
per_cpu | ro | per-CPU (bound) 여부 | workqueue 유형에 따라 |
운영 환경 동적 튜닝 사례
# 사례 1: writeback workqueue의 동시성 제한
# SSD에서 과도한 writeback으로 인한 I/O 폭주 방지
echo 8 > /sys/devices/virtual/workqueue/writeback/max_active
# 기본값 256 → 8로 제한
# 사례 2: 특정 workqueue를 특정 CPU 그룹에 격리
# CPU 0-3은 앱 전용, CPU 4-7은 커널 work 전용
echo f0 > /sys/devices/virtual/workqueue/writeback/cpumask
# 0xf0 = CPU 4-7
# 사례 3: workqueue worker 우선순위 조정
# 백그라운드 정리 작업의 우선순위를 낮춤
echo 10 > /sys/devices/virtual/workqueue/events_power_efficient/nice
# 사례 4: 전체 상태 확인 스크립트
for wq in /sys/devices/virtual/workqueue/*/; do
name=$(basename "$wq")
max=$(cat "$wq/max_active" 2>/dev/null)
nice=$(cat "$wq/nice" 2>/dev/null)
scope=$(cat "$wq/affinity_scope" 2>/dev/null)
echo "$name: max_active=$max nice=$nice scope=$scope"
done
debugfs 디버그 정보
# /sys/kernel/debug/workqueue (CONFIG_DEBUG_WQ 필요)
# 모든 workqueue의 내부 상태 덤프
mount -t debugfs none /sys/kernel/debug 2>/dev/null
cat /sys/kernel/debug/workqueue
# 출력 예시:
# workqueue flags dfl act max
# writeback U 8 8 256
# events — 256 256 256
# events_highpri H 256 256 256
# events_bh B 0 0 0
WQ_SYSFS 플래그를 설정해야 합니다. 시스템 workqueue(system_wq 등)는 대부분 이 플래그가 설정되어 있지만, 커스텀 workqueue는 명시적으로 추가해야 합니다: alloc_workqueue("my_wq", WQ_SYSFS | WQ_UNBOUND, 0)
Workqueue 트레이싱
workqueue 관련 문제 진단에는 다양한 트레이싱 도구를 활용할 수 있습니다. tracepoint, ftrace, perf, BPF 기반 분석까지 체계적으로 살펴봅니다.
Workqueue Tracepoint
# 사용 가능한 workqueue tracepoint 확인
ls /sys/kernel/debug/tracing/events/workqueue/
# workqueue_activate_work
# workqueue_execute_end
# workqueue_execute_start
# workqueue_queue_work
# 모든 workqueue 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/workqueue/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 출력 예시:
# kworker/0:1-28 workqueue_execute_start: work struct ffff8881234 function flush_to_ldisc
# kworker/0:1-28 workqueue_execute_end: work struct ffff8881234 function flush_to_ldisc
| Tracepoint | 발생 시점 | 주요 필드 |
|---|---|---|
workqueue_queue_work | work가 큐잉될 때 | workqueue 이름, work 주소, 요청 CPU, 함수명 |
workqueue_activate_work | work가 활성화될 때 | work 주소 (max_active 제한 초과 시 지연 후 활성화) |
workqueue_execute_start | work 콜백 시작 | work 주소, 콜백 함수명 |
workqueue_execute_end | work 콜백 완료 | work 주소, 콜백 함수명 |
ftrace로 work function 실행 시간 측정
# function_graph 트레이서로 특정 work function 추적
cd /sys/kernel/debug/tracing
# function_graph 트레이서 설정
echo function_graph > current_tracer
# 특정 함수만 추적 (예: writeback 관련)
echo wb_workfn > set_graph_function
# 추적 시작
echo 1 > tracing_on
# ... (writeback 발생 대기) ...
echo 0 > tracing_on
cat trace
# 출력 예시:
# 3) | wb_workfn() {
# 3) 0.245 us | wb_do_writeback();
# 3) 0.890 us | }
BPF 기반 workqueue 지연 분석
/* bpftrace 원라이너: work 큐잉~실행 지연 히스토그램 */
/* bpftrace -e '
tracepoint:workqueue:workqueue_queue_work {
@start[args->work] = nsecs;
}
tracepoint:workqueue:workqueue_execute_start {
if (@start[args->work]) {
@latency_us = hist((nsecs - @start[args->work]) / 1000);
delete(@start[args->work]);
}
}
END { clear(@start); }
' */
/* bpftrace: 가장 오래 실행되는 work function Top 10 */
/* bpftrace -e '
tracepoint:workqueue:workqueue_execute_start {
@start[tid] = nsecs;
@func[tid] = args->function;
}
tracepoint:workqueue:workqueue_execute_end {
if (@start[tid]) {
@duration[ksym(@func[tid])] =
hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
delete(@func[tid]);
}
}
' */
/proc/workqueues 해석 가이드
# 실시간 workqueue 상태 확인
cat /proc/workqueues
# 출력 형식:
# CPU POOL NICE FLAGS IDLE ACT REF NAME
# 0 0 0 - 2 0 4 events
# 0 1 -20 - 1 0 3 events_highpri
# * 4 0 U 3 0 8 writeback
# 필드 설명:
# CPU: 숫자=bound, *=unbound
# POOL: worker_pool ID
# NICE: 스레드 우선순위 (-20=highpri, 0=normal)
# FLAGS: U=unbound, H=highpri, B=bh
# IDLE: idle worker 수
# ACT: active (실행 중) work 수
# REF: 참조 카운트
실전 디버깅: kworker CPU 100%
# 문제: kworker/0:1이 CPU 100% 점유
# 원인 진단 절차:
# 1. 어떤 kworker가 문제인지 확인
top -b -n1 | grep kworker
# PID USER PR NI %CPU COMMAND
# 1234 root 20 0 99.8 kworker/0:1
# 2. 해당 kworker가 실행 중인 함수 확인
cat /proc/1234/stack
# 또는
echo l > /proc/sysrq-trigger # 모든 CPU의 backtrace
# 3. ftrace로 해당 worker의 실행 함수 추적
echo 1 > /sys/kernel/debug/tracing/events/workqueue/workqueue_execute_start/enable
echo "common_pid == 1234" > /sys/kernel/debug/tracing/events/workqueue/workqueue_execute_start/filter
cat /sys/kernel/debug/tracing/trace_pipe
# 어떤 work function이 반복 실행되는지 확인
# 4. perf로 함수별 CPU 사용량 프로파일링
perf top -p 1234 -g
# 또는 record + report
perf record -p 1234 -g -- sleep 5
perf report
/proc/PID/stack과 ftrace를 조합하면 대부분 원인을 특정할 수 있습니다.
커널 설정 총정리
Workqueue 관련 커널 설정은 빌드 시(Kconfig)와 런타임(커널 파라미터/sysfs)으로 나뉩니다. 프로덕션 환경에서의 권장 설정을 포함하여 정리합니다.
빌드 시 설정 (Kconfig)
| 설정 | 기본값 | 설명 | 권장 |
|---|---|---|---|
CONFIG_WQ_WATCHDOG_THRESH | 30 | workqueue watchdog 임계값 (초). 0이면 비활성화 | 프로덕션: 30, 디버깅: 10 |
CONFIG_WQ_POWER_EFFICIENT_DEFAULT | n | WQ_POWER_EFFICIENT 워크큐를 기본적으로 unbound로 전환 | 모바일/노트북: y, 서버: n |
CONFIG_WQ_VERBOSE | n | workqueue 디버그 메시지 활성화 | 개발: y, 프로덕션: n |
CONFIG_LOCKDEP | n | Lock dependency 추적 (flush 데드락 감지) | 개발: y, 프로덕션: n |
CONFIG_DEBUG_OBJECTS_WORK | n | work_struct 생명주기 추적 | 개발: y |
CONFIG_WQ_CPU_INTENSIVE_REPORT | n | CPU-intensive work 자동 감지 리포트 | 개발: y |
런타임 설정 (커널 파라미터)
| 파라미터 | 설정 방법 | 설명 |
|---|---|---|
workqueue.watchdog_thresh | 커널 cmdline, sysfs | watchdog 임계값 (초). 0=비활성화 |
workqueue.power_efficient | 커널 cmdline | 1=WQ_POWER_EFFICIENT를 unbound로 전환 |
workqueue.disable_numa | 커널 cmdline | 1=NUMA 인식 비활성화 (디버깅용) |
workqueue.default_affinity_scope | 커널 cmdline | 기본 affinity scope 설정 (6.5+) |
sysfs 런타임 조정
# watchdog 임계값 변경 (런타임)
echo 60 > /sys/module/workqueue/parameters/watchdog_thresh
# 시스템 전체 unbound workqueue cpumask
cat /sys/module/workqueue/parameters/cpu_intensive_thresh_us
# CPU-intensive 판단 임계값 (마이크로초)
# per-workqueue 파라미터 (WQ_SYSFS인 경우)
echo 16 > /sys/devices/virtual/workqueue/writeback/max_active
echo cache > /sys/devices/virtual/workqueue/writeback/affinity_scope
프로덕션 환경 권장 설정 체크리스트
CONFIG_WQ_WATCHDOG_THRESH=30— 교착 상태(Deadlock) 조기 감지- 서버:
workqueue.power_efficient=0— 지연 시간 최소화 - 모바일:
workqueue.power_efficient=1+CONFIG_WQ_POWER_EFFICIENT_DEFAULT=y CONFIG_LOCKDEP=n— 프로덕션에서 성능 오버헤드(Overhead) 제거- NUMA 서버: affinity scope를
numa또는cache로 설정 - I/O 집약: writeback
max_active를 스토리지 큐 깊이에 맞춰 조정
# 프로덕션 서버 초기화 스크립트 예시
#!/bin/bash
# workqueue 프로덕션 튜닝
# watchdog 활성화
echo 30 > /sys/module/workqueue/parameters/watchdog_thresh
# NUMA 서버: writeback을 numa scope로
if [ -d /sys/devices/virtual/workqueue/writeback ]; then
echo numa > /sys/devices/virtual/workqueue/writeback/affinity_scope 2>/dev/null
fi
# 모든 WQ_SYSFS workqueue 상태 로깅
for wq in /sys/devices/virtual/workqueue/*/; do
name=$(basename "$wq")
max=$(cat "$wq/max_active" 2>/dev/null || echo "N/A")
logger "workqueue $name: max_active=$max"
done
| 환경 | 핵심 설정 | 이유 |
|---|---|---|
| 고성능 서버 | power_efficient=0, affinity_scope=cache | 지연 최소화, 캐시 활용 |
| NUMA 서버 | affinity_scope=numa, NUMA별 cpumask | 원격 메모리 접근 최소화 |
| 데스크톱/노트북 | power_efficient=1 | 배터리 수명 연장 |
| 임베디드/IoT | power_efficient=1, 낮은 max_active | 최소 자원 사용 |
| RT 시스템 | WQ_BH 대신 일반 WQ, watchdog 활성화 | 결정적 지연, 교착 감지 |
참고자료
커널 공식 문서
- Concurrency Managed Workqueue (cmwq) — kernel.org — CMWQ 공식 설계 문서로, 워커 풀(Worker Pool) 아키텍처와 플래그(Flag) 의미를 설명합니다
- Workqueue API Reference — kernel.org — alloc_workqueue, queue_work, flush_workqueue 등 공식 API 레퍼런스입니다
- Workqueue Sysfs Interface — kernel.org — /sys/devices/virtual/workqueue/ 아래 sysfs 튜닝 인터페이스를 설명합니다
LWN.net 기사
- Concurrency-managed workqueues (LWN, 2010) — Tejun Heo가 설계한 CMWQ의 초기 제안과 동기를 다룬 핵심 기사입니다
- Working on workqueues (LWN, 2009) — 기존 워크큐의 문제점과 CMWQ 도입 배경을 정리한 기사입니다
- The workqueue API changes in 3.7 (LWN, 2012) — alloc_workqueue 플래그 변경 및 WQ_UNBOUND 개선 사항을 다룹니다
- Workqueue items of interest (LWN, 2016) — 워크큐 디버깅 개선과 watchdog 기능 추가를 다룬 기사입니다
- BH workqueues (LWN, 2023) — WQ_BH 플래그 도입으로 tasklet을 워크큐로 대체하는 방안을 소개합니다
- Workqueue affinity scopes (LWN, 2023) — Unbound 워크큐의 affinity_scope 매개변수와 NUMA 최적화를 설명합니다
커널 소스 코드
- kernel/workqueue.c — 워크큐 핵심 구현체로, worker_thread 루프와 풀 관리 로직이 포함되어 있습니다
- include/linux/workqueue.h — work_struct, delayed_work, workqueue_struct 등 핵심 자료 구조와 매크로 정의입니다
- kernel/workqueue_internal.h — worker, worker_pool 등 내부 자료 구조 정의입니다
- Documentation/core-api/workqueue.rst — 커널 소스 트리 내 CMWQ 문서 원본입니다
발표 및 컨퍼런스
- Tejun Heo — CMWQ Design Notes — CMWQ 설계자인 Tejun Heo의 원본 설계 문서입니다
- Kernel Recipes 2018 — Workqueue: Pair your work items! (Tejun Heo) — 워크큐 활용 패턴과 주의사항을 다룬 발표입니다
서적
- Linux Kernel Development, 3rd Edition (Robert Love, Addison-Wesley, 2010) — Chapter 8 "Bottom Halves and Deferring Work"에서 워크큐의 기본 개념과 사용법을 설명합니다
- Linux Device Drivers, 3rd Edition (Corbet, Rubini, Kroah-Hartman, O'Reilly, 2005) — Chapter 7 "Time, Delays, and Deferred Work"에서 워크큐 API를 다룹니다
- Understanding the Linux Kernel, 3rd Edition (Bovet & Cesati, O'Reilly, 2005) — Chapter 4 "Interrupts and Exceptions"에서 지연 처리 메커니즘을 설명합니다
- Professional Linux Kernel Architecture (Wolfgang Mauerer, Wrox, 2008) — 워크큐 내부 구현과 스케줄링 상호작용을 상세히 다룹니다
- Linux Kernel Networking: Implementation and Theory (Rami Rosen, Apress, 2014) — 네트워크 서브시스템에서의 워크큐 활용 사례를 포함합니다
블로그 및 튜토리얼
- EmbeTronicX — Workqueue in Linux Kernel — 워크큐 기본 사용법과 예제 코드를 단계별로 설명합니다
- Kernel Docs Mirror — Concurrency Managed Workqueue — kernel.org 문서의 미러 버전으로, 오프라인 참고에 유용합니다
관련 문서
- Bottom Half (Softirq, Tasklet, Workqueue) — Bottom Half 메커니즘 전체 비교, 선택 가이드, Workqueue 기초
- 인터럽트 (Interrupts) — IRQ, Top/Bottom Half, 인터럽트 컨텍스트, 핸들러(Handler) 작성법
- Softirq/Hardirq — softirq 실행 흐름, ksoftirqd, 인터럽트 하위 반쪽 메커니즘 상세
- Tasklet — tasklet_struct, tasklet_schedule, deprecated 이유, 마이그레이션 가이드
- Threaded IRQ — 스레드화된 인터럽트 핸들러, request_threaded_irq, IRQF_ONESHOT