Workqueue (CMWQ) 심화
Workqueue는 리눅스 커널에서 가장 범용적인 Bottom Half 메커니즘입니다.
프로세스 컨텍스트에서 실행되므로 슬립, mutex, 메모리 할당(GFP_KERNEL)이 가능하며,
Concurrency Managed Workqueue (CMWQ) 아키텍처로 커널이 worker pool을 자동 관리합니다.
이 문서에서는 workqueue_struct, worker_pool, work_struct의
내부 구조부터 alloc_workqueue() API, 동시성 제어, flush/cancel 패턴,
ordered/delayed 워크큐, 디버깅, PREEMPT_RT 호환성까지 전 영역을 다룹니다.
이 페이지의 위치: Workqueue 심화 내용은 원래 Bottom Half 통합 문서에 포함되어 있었으나, 분량과 독립성을 고려하여 별도 페이지로 분리되었습니다. Bottom Half 메커니즘 전체 비교는 Bottom Half 선택 가이드를 참고하세요.
Workqueue 역사와 진화
리눅스 커널의 workqueue 메커니즘은 오랜 진화 과정을 거쳐 현재의 CMWQ 아키텍처에 이르렀습니다. 각 세대의 특징과 한계를 이해하면 CMWQ의 설계 동기를 더 깊이 파악할 수 있습니다.
| 세대 | 커널 버전 | 메커니즘 | 특징 / 한계 |
|---|---|---|---|
| 1세대 | 2.5.41 (2002) | keventd / task queue 대체 |
CPU별 하나의 worker 스레드 (events/N). 동시성 제어 없음, 하나의 work가 블록되면 해당 CPU의 모든 work 지연 |
| 2세대 | 2.6.x | create_workqueue() / create_singlethread_workqueue() |
multithread: CPU당 하나의 전용 스레드 생성 → N-CPU 시스템에서 N개 스레드. singlethread: 시스템에 1개 스레드. 커널 스레드 폭발 문제 |
| 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 작업 구분
- 속성 기반 매핑: workqueue 플래그에 따라 적절한 worker pool에 자동 매핑
CMWQ 아키텍처 상세
CMWQ의 핵심은 workqueue_struct, pool_workqueue, worker_pool, worker 네 가지 구조체의 관계입니다. 각 구조체의 역할과 연결 관계를 상세히 살펴봅니다.
/*
* 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 */
};
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;
};
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);
| 플래그 | 설명 | 사용 시나리오 |
|---|---|---|
WQ_UNBOUND | Per-CPU 대신 NUMA-aware unbound pool 사용 | long-running 작업, CPU 마이그레이션 허용 |
WQ_HIGHPRI | 높은 우선순위 worker pool (nice -20) 사용 | 지연시간이 중요한 작업 |
WQ_CPU_INTENSIVE | 동시성 관리에서 제외 (CPU 점유로 인한 추가 worker 생성 방지) | CPU-bound 연산 작업 |
WQ_FREEZABLE | 시스템 suspend 시 work 처리 중단 | suspend/resume과 상호작용하는 작업 |
WQ_MEM_RECLAIM | 메모리 부족 시에도 worker 생성 보장 (rescuer thread) | 메모리 회수 경로에서 사용되는 작업 |
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// 디렉토리 생성
* - 런타임에 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 | 플래그 | 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; /* 콜백 함수 */
};
INIT_WORK / INIT_DELAYED_WORK 매크로 분석
/*
* 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에서 독립 모니터링 필요
*/
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이면 가능한 빨리 실행 */
Delayed Work 내부 타이머 연동
/*
* 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 변경 방지 → 진정한 순서 보장
동기화 패턴: 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 콜백 내에서 자신이 속한 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 커널에서는 workqueue의 동작이 일부 변경됩니다:
- 모든 worker 스레드가 선점 가능: 일반 커널과 달리 모든 work가 스레드 컨텍스트에서 실행
- Worker 우선순위: WQ_HIGHPRI worker는 더 높은 RT 우선순위 가능
- BH workqueue: RT 커널에서 softirq가 스레드화되면서 WQ_BH 플래그로 BH 컨텍스트 에뮬레이션 가능
RT 우선순위 역전 주의: 높은 우선순위 RT 태스크가 flush_work()로 work 완료를 대기할 때, kworker는 SCHED_NORMAL이므로 중간 우선순위 태스크에 선점될 수 있습니다. 해결: WQ_HIGHPRI 사용, 또는 RT 태스크에서 flush_work() 대신 다른 동기화 메커니즘 사용.
RT 이식성 팁: RT 커널에서도 정상 동작하게 하려면, workqueue가 가장 안전한 Bottom Half 메커니즘입니다. softirq/tasklet은 RT에서 스레드화되면서 기대와 다른 지연 시간을 보일 수 있지만, workqueue는 본래부터 프로세스 컨텍스트이므로 변화가 적습니다.
일반적인 버그 패턴
/* 버그 1: work_struct를 포함한 구조체의 조기 해제 */
static void bad_cleanup(struct my_device *dev)
{
kfree(dev); /* dev->work가 아직 실행 중일 수 있음! */
}
/* 올바른 코드: cancel_work_sync() 후 kfree() */
/* 버그 2: work 콜백에서 자기 워크큐 flush → 데드락 */
flush_workqueue(dev->wq); /* work 콜백 안에서 호출 금지! */
/* 버그 3: 스택에 work_struct 할당 */
struct work_struct work; /* 스택 변수! */
schedule_work(&work);
/* 함수 반환 후 work가 실행되면 → 스택 손상 */
/* 버그 4: 초기화 전 큐잉 */
schedule_work(&dev->work); /* INIT_WORK 전! → 미정의 동작 */
관련 문서
- Bottom Half (Softirq, Tasklet, Workqueue) — Bottom Half 메커니즘 전체 비교, 선택 가이드, Workqueue 기초
- 인터럽트 (Interrupts) — IRQ, Top/Bottom Half, 인터럽트 컨텍스트, 핸들러 작성법
- Softirq/Hardirq 심화 — softirq 실행 흐름, ksoftirqd, 인터럽트 하위 반쪽 메커니즘 상세
- Tasklet 심화 — tasklet_struct, tasklet_schedule, deprecated 이유, 마이그레이션 가이드