Workqueue 서브시스템 심화 (CMWQ)
리눅스 커널의 핵심 비동기 실행 인프라인 Workqueue(CMWQ) 서브시스템을 심층 분석합니다. worker pool 아키텍처, work_struct/delayed_work 자료구조, 커스텀 워크큐 생성, 동시성 관리, 디버깅 기법, 그리고 드라이버에서의 실전 활용 패턴까지 포괄적으로 다룹니다.
이 페이지는 Workqueue 서브시스템을 독립적으로 심층 분석합니다. Bottom Half 전반(softirq, tasklet 포함)에 대한 개요는 Bottom Half 심화 페이지를, 인터럽트 기초는 인터럽트 페이지를 참고하세요.
핵심 요약
- work_struct — 지연 실행할 작업을 나타내는 구조체. 콜백 함수 포인터를 포함합니다.
- worker pool — per-CPU 또는 unbound worker 스레드 풀. CMWQ가 동적으로 관리합니다.
- system_wq — 기본 시스템 워크큐.
schedule_work()로 간편하게 작업을 예약합니다. - 슬립 가능 — softirq/tasklet과 달리 workqueue는 프로세스 컨텍스트에서 실행되어 슬립할 수 있습니다.
단계별 이해
- work 생성 —
INIT_WORK(&my_work, my_handler)로 작업을 초기화합니다.핸들러 함수는
void handler(struct work_struct *work)시그니처입니다. - work 예약 —
schedule_work(&my_work)으로 시스템 워크큐에 넣거나,queue_work(my_wq, &my_work)로 커스텀 큐에 넣습니다.schedule_delayed_work()로 일정 시간 후에 실행되도록 예약할 수도 있습니다. - 실행 과정 — worker 스레드가 큐에서 work를 꺼내 핸들러를 실행합니다.
CMWQ가 동시성 수준을 자동 관리하여 CPU 사용을 최적화합니다.
- 확인 —
ps aux | grep kworker로 현재 활성 worker 스레드를 확인합니다./sys/kernel/debug/workqueue/에서 워크큐 통계를 볼 수 있습니다.
Workqueue 개요
Workqueue는 커널에서 작업을 프로세스 컨텍스트로 지연 실행(deferred execution)하기 위한 범용 인프라입니다. 인터럽트 핸들러나 softirq에서 수행하기에 부적합한 작업 -- 슬립 가능한(sleepable) 연산, 잠금(lock) 획득, 사용자 공간 접근, 시간이 오래 걸리는 처리 등 -- 을 안전하게 위임할 수 있습니다.
역사: keventd에서 CMWQ까지
Workqueue의 발전은 커널의 비동기 처리 요구사항 증가와 직접 연결됩니다:
| 시기 | 메커니즘 | 특징 | 한계 |
|---|---|---|---|
| 2.5 이전 | task queue (keventd) | 단일 스레드, 간단한 API | 병렬 처리 불가, 확장성 부족 |
| 2.5 ~ 2.6.36 | 기존 Workqueue | Per-CPU worker 스레드 | 워크큐마다 Per-CPU 스레드 생성 (메모리 낭비, PID 공간 소모) |
| 2.6.36+ | CMWQ (Concurrency Managed) | 공유 worker pool, 동시성 자동 관리 | 현재 표준 |
기존 workqueue의 핵심 문제는 create_workqueue()가 CPU 수만큼 커널 스레드를 생성했다는 점입니다. 64-CPU 시스템에서 30개의 워크큐가 있으면 1,920개의 커널 스레드가 만들어졌고, 대부분은 유휴 상태로 메모리만 소모했습니다. CMWQ는 이 문제를 공유 worker pool로 해결했습니다.
핵심 설계 원칙: CMWQ는 "워크큐는 작업의 속성(attribute)을 정의하고, worker pool은 실행을 담당한다"는 분리 원칙을 따릅니다. 워크큐는 더 이상 자체 스레드를 소유하지 않으며, 적절한 worker pool에 작업을 위임합니다.
Workqueue vs 다른 Bottom Half 메커니즘
| 특성 | Softirq | Tasklet | Workqueue |
|---|---|---|---|
| 실행 컨텍스트 | 인터럽트 | 인터럽트 | 프로세스 |
| 슬립 가능 | 불가 | 불가 | 가능 |
| 동일 work 병렬 실행 | 가능 (Per-CPU) | 불가 (직렬화) | 설정에 따라 다름 |
| 지연 실행 | 불가 | 불가 | 가능 (delayed_work) |
| CPU 친화성 | 현재 CPU | 현재 CPU | 설정 가능 |
| 사용 대상 | 커널 서브시스템 | 드라이버 (레거시) | 드라이버, 서브시스템 |
CMWQ 아키텍처
CMWQ의 핵심은 워크큐와 worker pool을 분리한 것입니다. 워크큐는 작업의 속성(우선순위, CPU 바인딩 여부 등)을 정의하고, worker pool은 실제 실행 인프라를 제공합니다.
Worker Pool 유형
시스템에는 두 가지 유형의 worker pool이 존재합니다:
/*
* Worker Pool 구조:
*
* ┌─────────────────────────────────────────────────────────────────┐
* │ Per-CPU Worker Pools │
* │ │
* │ CPU 0 CPU 1 CPU N │
* │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
* │ │ pool (nice 0)│ │ pool (nice 0)│ │ pool (nice 0)│ │
* │ │ worker 0 │ │ worker 0 │ │ worker 0 │ │
* │ │ worker 1 │ │ worker 1 │ │ worker 1 │ │
* │ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
* │ │ pool (nice -20)│ │ pool (nice -20)│ │ pool (nice -20)│ │
* │ │ worker 0 │ │ worker 0 │ │ worker 0 │ │
* │ └──────────────┘ └──────────────┘ └──────────────┘ │
* ├─────────────────────────────────────────────────────────────────┤
* │ Unbound Worker Pools │
* │ │
* │ ┌──────────────────────────────────────┐ │
* │ │ unbound pool (attrs: nice 0, cpumask)│ │
* │ │ worker 0, worker 1, ... worker N │ │
* │ └──────────────────────────────────────┘ │
* │ ┌──────────────────────────────────────┐ │
* │ │ unbound pool (attrs: nice -20, ...) │ │
* │ │ worker 0, worker 1, ... worker N │ │
* │ └──────────────────────────────────────┘ │
* └─────────────────────────────────────────────────────────────────┘
*/
Per-CPU Worker Pool: 각 CPU마다 두 개의 pool이 존재합니다. 하나는 일반 우선순위(nice 0), 다른 하나는 높은 우선순위(nice -20, WQ_HIGHPRI용)입니다. bound 워크큐의 work item은 큐잉된 CPU의 Per-CPU pool에서 실행됩니다.
Unbound Worker Pool: CPU에 바인딩되지 않은 pool입니다. WQ_UNBOUND 플래그가 설정된 워크큐의 work item을 실행합니다. 워크큐 속성(nice 값, cpumask 등)에 따라 풀이 생성되며, 동일 속성의 워크큐는 풀을 공유합니다.
핵심 자료구조 관계
/* kernel/workqueue_internal.h, kernel/workqueue.c */
/* worker_pool: 실제 실행 인프라 */
struct worker_pool {
spinlock_t lock; /* pool 보호 락 */
int cpu; /* Per-CPU pool: 바인딩된 CPU (-1이면 unbound) */
int node; /* NUMA 노드 */
int id; /* pool ID */
unsigned int flags; /* POOL_MANAGER_ACTIVE 등 */
unsigned long watchdog_ts; /* worker stall 감지용 */
struct list_head worklist; /* 대기 중인 work item 리스트 */
int nr_workers; /* 전체 worker 수 */
int nr_idle; /* 유휴 worker 수 */
struct list_head idle_list; /* 유휴 worker 리스트 */
struct list_head workers; /* 전체 worker 리스트 */
struct workqueue_attrs *attrs; /* unbound pool 속성 */
struct hash_head busy_hash[]; /* 실행 중인 work→worker 매핑 */
};
/* pool_workqueue: 워크큐 ↔ worker_pool 연결 */
struct pool_workqueue {
struct worker_pool *pool; /* 연결된 worker pool */
struct workqueue_struct *wq; /* 소속 워크큐 */
int nr_active; /* 활성 work item 수 */
int max_active; /* 최대 동시 활성 수 */
struct list_head inactive_works; /* max_active 초과 시 대기 리스트 */
};
/* workqueue_struct: 사용자가 상호작용하는 워크큐 객체 */
struct workqueue_struct {
struct list_head pwqs; /* pool_workqueue 리스트 */
struct list_head list; /* 전역 워크큐 리스트 */
unsigned int flags; /* WQ_UNBOUND, WQ_HIGHPRI 등 */
int saved_max_active; /* freezing 시 저장 */
char name[]; /* 워크큐 이름 */
};
3계층 구조: workqueue_struct(사용자 인터페이스) → pool_workqueue(연결 역할) → worker_pool(실행 인프라). 이 구조 덕분에 여러 워크큐가 동일한 worker pool을 공유할 수 있습니다.
work_struct / delayed_work
work item은 워크큐에 큐잉되는 작업의 기본 단위입니다. 두 가지 기본 타입이 있습니다.
work_struct 정의
/* include/linux/workqueue.h */
struct work_struct {
atomic_long_t data; /* 플래그 + pool_workqueue 포인터 (하위 비트는 플래그) */
struct list_head entry; /* worklist 연결 */
work_func_t func; /* 콜백 함수: void (*)(struct work_struct *) */
};
struct delayed_work {
struct work_struct work; /* 내장된 work_struct */
struct timer_list timer; /* 지연 타이머 */
struct workqueue_struct *wq; /* 타이머 만료 시 큐잉할 워크큐 */
int cpu; /* 타이머 만료 시 큐잉할 CPU */
};
data 필드의 비트 레이아웃
work_struct.data는 atomic_long_t이지만, 단순 데이터가 아니라 플래그와 pool_workqueue 포인터를 동시에 저장합니다:
/* include/linux/workqueue.h - data 필드 비트 레이아웃 */
#define WORK_STRUCT_PENDING_BIT 0 /* 큐에 대기 중 */
#define WORK_STRUCT_INACTIVE_BIT 1 /* inactive 리스트에 있음 */
#define WORK_STRUCT_PWQ_BIT 2 /* data가 pwq 포인터를 담고 있음 */
#define WORK_STRUCT_LINKED_BIT 3 /* 다음 work와 연결됨 */
#define WORK_STRUCT_COLOR_SHIFT 4 /* flush color 비트 시작 */
/*
* 상위 비트: pool_workqueue 포인터 (정렬 보장으로 하위 비트 사용 가능)
* 하위 비트: 플래그
*
* ┌─────────────────────────────────┬──────┬─┬─┬─┬─┐
* │ pool_workqueue pointer │color │L│P│I│P│
* │ (or pool ID when idle) │ │ │W│N│E│
* │ │ │ │Q│A│N│
* └─────────────────────────────────┴──────┴─┴─┴─┴─┘
* MSB LSB
*/
초기화 매크로
/* 정적 초기화 */
DECLARE_WORK(name, func); /* 정적 work_struct 선언+초기화 */
DECLARE_DELAYED_WORK(name, func); /* 정적 delayed_work 선언+초기화 */
/* 동적 초기화 */
struct work_struct my_work;
INIT_WORK(&my_work, my_work_handler);
struct delayed_work my_dwork;
INIT_DELAYED_WORK(&my_dwork, my_dwork_handler);
/* 콜백 함수 시그니처 */
static void my_work_handler(struct work_struct *work)
{
/* container_of로 부모 구조체 접근 */
struct my_device *dev = container_of(work, struct my_device, work);
/* 프로세스 컨텍스트: 슬립 가능, 뮤텍스 획득 가능 */
}
static void my_dwork_handler(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_device *dev = container_of(dwork, struct my_device, dwork);
/* ... */
}
work_struct 재사용 규칙: 동일한 work_struct는 동시에 두 번 큐잉할 수 없습니다. queue_work()는 이미 WORK_STRUCT_PENDING이 설정된 work를 무시합니다(false 반환). 동일 작업을 여러 번 큐잉해야 한다면, 각각 별도의 work_struct를 사용해야 합니다.
시스템 워크큐
커널은 사전 정의된 시스템 워크큐를 제공합니다. 대부분의 드라이버에서는 커스텀 워크큐 대신 이들을 사용하면 충분합니다:
/* kernel/workqueue.c - 시스템 워크큐 정의 */
struct workqueue_struct *system_wq; /* 범용 (Per-CPU, nice 0) */
struct workqueue_struct *system_highpri_wq; /* 높은 우선순위 (nice -20) */
struct workqueue_struct *system_long_wq; /* 장시간 실행 work용 */
struct workqueue_struct *system_unbound_wq; /* CPU 비종속 (unbound) */
struct workqueue_struct *system_freezable_wq; /* suspend 시 동결 */
struct workqueue_struct *system_power_efficient_wq; /* 전력 효율 */
| 시스템 워크큐 | 플래그 | 용도 |
|---|---|---|
system_wq | 0 | 일반적인 짧은 작업. schedule_work()의 기본 대상 |
system_highpri_wq | WQ_HIGHPRI | 지연이 중요한 작업 (nice -20 worker에서 실행) |
system_long_wq | 0 | 장시간 실행 work (concurrency 관리에서 제외하지는 않음) |
system_unbound_wq | WQ_UNBOUND | CPU 바인딩 불필요한 작업 (스케줄러가 CPU 선택) |
system_freezable_wq | WQ_FREEZABLE | suspend/hibernate 시 동결되어야 하는 작업 |
system_power_efficient_wq | WQ_UNBOUND (조건부) | workqueue.power_efficient 부트 파라미터에 따라 unbound 전환 |
편의 API (시스템 워크큐 직접 사용)
/* 시스템 워크큐(system_wq)에 직접 큐잉하는 편의 함수 */
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}
static inline bool schedule_delayed_work(struct delayed_work *dwork,
unsigned long delay)
{
return queue_delayed_work(system_wq, dwork, delay);
}
/* 사용 예: IRQ 핸들러에서 work 큐잉 */
static irqreturn_t my_irq_handler(int irq, void *data)
{
struct my_device *dev = data;
/* 최소한의 처리 후 나머지를 워크큐로 위임 */
dev->irq_status = readl(dev->regs + IRQ_STATUS);
writel(dev->irq_status, dev->regs + IRQ_ACK);
schedule_work(&dev->work); /* system_wq에 큐잉 */
return IRQ_HANDLED;
}
system_wq vs 커스텀 워크큐: 대부분의 경우 schedule_work()/schedule_delayed_work()로 시스템 워크큐를 사용하는 것으로 충분합니다. 커스텀 워크큐가 필요한 경우: (1) flush 범위를 분리하고 싶을 때, (2) 특수 플래그(WQ_MEM_RECLAIM 등)가 필요할 때, (3) ordered 실행이 필요할 때.
커스텀 워크큐 생성
alloc_workqueue() API
/* include/linux/workqueue.h */
struct workqueue_struct *alloc_workqueue(
const char *fmt, /* 워크큐 이름 (printf 포맷) */
unsigned int flags, /* WQ_* 플래그 조합 */
int max_active, /* 최대 동시 활성 work 수 (0 = 기본값) */
... /* fmt에 대한 가변 인자 */
);
/* 매크로 래퍼 */
#define alloc_ordered_workqueue(fmt, flags, args...) \
alloc_workqueue(fmt, WQ_UNBOUND | __WQ_ORDERED | (flags), 1, ##args)
/* 해제 */
void destroy_workqueue(struct workqueue_struct *wq);
WQ 플래그 상세
| 플래그 | 설명 |
|---|---|
WQ_UNBOUND | Per-CPU pool 대신 unbound pool 사용. CPU 마이그레이션이 자유롭고, NUMA 지역성 최적화 적용. 장시간 실행 work에 적합 |
WQ_HIGHPRI | 높은 우선순위(nice -20) worker pool 사용. 지연 민감한 작업에 사용 |
WQ_FREEZABLE | suspend/hibernate 진입 시 새 work 큐잉을 막고 기존 work 완료를 대기. PM 관련 드라이버에 필수 |
WQ_MEM_RECLAIM | 메모리 회수 경로에서 사용하는 워크큐. rescuer worker를 보장하여 메모리 부족 시에도 진행 가능(forward progress) |
WQ_CPU_INTENSIVE | CPU를 장시간 점유하는 work. concurrency 관리에서 제외(실행 중에도 다른 work를 동시에 처리 가능) |
__WQ_ORDERED | 내부 플래그. alloc_ordered_workqueue()에서 자동 설정. 순서 보장 |
max_active 파라미터
/*
* max_active: 워크큐당 Per-CPU/unbound pool에서 동시에 실행 가능한 work 수
*
* - 0: 기본값 사용 (WQ_DFL_ACTIVE = 256, 또는 WQ_UNBOUND_MAX_ACTIVE = CPU 수 * 4)
* - 1: 순서 보장 (ordered workqueue)
* - N: 최대 N개의 work가 동시 실행 가능
*
* 주의: max_active는 Per-CPU pool 기준입니다.
* bound 워크큐에서 max_active=2이면, 각 CPU에서 최대 2개까지 동시 실행.
* 시스템 전체로는 CPU_수 * 2개가 동시 실행될 수 있습니다.
*/
/* 예: 동시 실행 제한이 있는 워크큐 */
struct workqueue_struct *my_wq;
my_wq = alloc_workqueue("my_driver_wq",
WQ_UNBOUND | WQ_MEM_RECLAIM,
4); /* 최대 4개 동시 실행 */
if (!my_wq)
return -ENOMEM;
WQ_MEM_RECLAIM과 rescuer 스레드
메모리 회수(reclaim) 경로에서 워크큐를 사용하면 순환 의존성 위험이 있습니다. 메모리 부족으로 새 worker를 생성할 수 없는데, work 실행이 메모리를 해제하는 상황입니다. WQ_MEM_RECLAIM은 이를 rescuer 스레드로 해결합니다:
/* kernel/workqueue.c - rescuer 스레드 */
static int rescuer_thread(void *__rescuer)
{
struct worker *rescuer = __rescuer;
struct workqueue_struct *wq = rescuer->rescue_wq;
/*
* rescuer는 워크큐 생성 시 미리 할당된 전용 worker입니다.
* 일반 worker를 생성할 수 없을 때(메모리 부족) 활성화되어
* pending work를 직접 실행합니다.
*
* 이것이 forward progress guarantee입니다:
* 메모리 회수 work가 실행되어 메모리를 해제하면,
* 다시 일반 worker 생성이 가능해집니다.
*/
for (;;) {
set_current_state(TASK_IDLE);
if (need_to_create_worker(pool)) {
/* pool의 pending work를 가져와서 직접 실행 */
move_linked_works(work, &rescuer->scheduled, &n);
process_scheduled_works(rescuer);
}
schedule();
}
}
필수 규칙: 메모리 회수 경로(직접 또는 간접)에서 사용하는 워크큐에는 반드시 WQ_MEM_RECLAIM을 설정해야 합니다. 그렇지 않으면 메모리 부족 시 데드락이 발생할 수 있습니다. 블록 디바이스 드라이버, 파일시스템, 스왑 관련 코드에서 특히 주의하세요.
실전 워크큐 생성 예제
/* 예 1: 범용 드라이버 워크큐 (가장 일반적) */
wq = alloc_workqueue("mydrv", 0, 0);
/* 예 2: 순서 보장 워크큐 */
wq = alloc_ordered_workqueue("mydrv_ordered", 0);
/* 예 3: 블록 디바이스 드라이버 (메모리 회수 경로) */
wq = alloc_workqueue("myblk", WQ_MEM_RECLAIM | WQ_HIGHPRI, 0);
/* 예 4: CPU 집약적 연산 (암호화, 압축 등) */
wq = alloc_workqueue("mycrypto", WQ_UNBOUND | WQ_CPU_INTENSIVE, 0);
/* 예 5: 전원 관리 관련 드라이버 */
wq = alloc_workqueue("mypm", WQ_FREEZABLE | WQ_MEM_RECLAIM, 0);
/* 예 6: 이름에 동적 정보 포함 */
wq = alloc_workqueue("mydrv-%s", WQ_UNBOUND, 0, dev_name(dev));
/* 모든 경우 NULL 체크 필수 */
if (!wq)
return -ENOMEM;
Worker Pool 관리
동시성 관리 메커니즘
CMWQ의 "C"(Concurrency Managed)는 worker pool이 자동으로 동시성을 관리한다는 의미입니다. Per-CPU bound pool에서 동작하는 핵심 메커니즘은 다음과 같습니다:
/*
* 동시성 관리 핵심 원리:
*
* 1. worker가 work를 실행하기 시작하면 "running" 카운트 증가
* 2. worker가 슬립(I/O 대기, 뮤텍스 등)하면 "running" 카운트 감소
* 3. running 카운트가 0이 되면 → 새 worker를 깨움
* 4. 깨어난 worker가 대기 중인 work를 처리
*
* 이를 통해 CPU가 유휴 상태로 빠지는 것을 방지하면서도
* 불필요한 worker 생성을 억제합니다.
*/
/* kernel/workqueue.c - worker가 슬립할 때 호출 */
void wq_worker_sleeping(struct task_struct *task)
{
struct worker *worker = kthread_data(task);
struct worker_pool *pool;
pool = worker->pool;
/* running 카운트 감소 */
if (atomic_dec_and_test(&pool->nr_running) &&
!list_empty(&pool->worklist)) {
/* 더 이상 running worker가 없고 대기 work가 있으면 */
/* 유휴 worker를 깨움 */
wake_up_worker(pool);
}
}
/* worker가 깨어날 때 호출 */
void wq_worker_running(struct task_struct *task)
{
struct worker *worker = kthread_data(task);
if (!worker->sleeping)
return;
worker->sleeping = 0;
/* running 카운트 증가 */
atomic_inc(&worker->pool->nr_running);
}
WQ_CPU_INTENSIVE의 의미: WQ_CPU_INTENSIVE 워크큐의 work는 실행 시작 시 running 카운트에서 제외됩니다. 즉, CPU 집약적 work가 오래 실행되어도 pool의 동시성 관리가 "work가 블록되었다"고 오판하여 불필요한 worker를 깨우지 않습니다.
Worker 스레드 생명주기
/*
* Worker 스레드 상태 전이:
*
* 생성 ──→ IDLE ──→ BUSY (work 실행) ──→ IDLE
* │ │
* │ 유휴 시간 초과 │
* └──────→ 소멸 (destroy) ←───────┘
*
* 생성 조건:
* - pool에 대기 work가 있는데 running worker가 없을 때
* - manager가 need_more_worker()로 판단
*
* 소멸 조건:
* - idle_list에 IDLE_WORKER_TIMEOUT(5분) 이상 머문 worker
* - 단, pool당 최소 1개의 worker는 유지 (min_idle_workers)
*/
/* kernel/workqueue.c */
#define IDLE_WORKER_TIMEOUT (300 * HZ) /* 5분 */
static int worker_thread(void *__worker)
{
struct worker *worker = __worker;
struct worker_pool *pool = worker->pool;
woke_up:
spin_lock_irq(&pool->lock);
/* 유휴 worker 정리 대상인지 확인 */
if (too_many_workers(pool))
goto die;
/* worker가 manager 역할을 해야 하는지 확인 */
if (!need_more_worker(pool))
goto sleep;
/* manager: 필요하면 새 worker 생성 */
if (manage_workers(worker))
goto recheck;
/* 대기 중인 work 처리 */
do {
struct work_struct *work = list_first_entry(
&pool->worklist, struct work_struct, entry);
process_one_work(worker, work);
} while (keep_working(pool));
sleep:
worker_enter_idle(worker);
schedule();
goto woke_up;
die:
worker_detach_from_pool(worker);
kthread_exit(0);
}
kworker 네이밍 규칙
ps나 top에서 볼 수 있는 kworker 스레드의 이름은 다음 규칙을 따릅니다:
kworker 네이밍 패턴:
kworker/CPU:WORKER_ID Per-CPU, 일반 우선순위
kworker/CPUH:WORKER_ID Per-CPU, 높은 우선순위 (H = Highpri)
kworker/u POOL_ID:WORKER_ID Unbound pool (u = Unbound)
예시:
kworker/0:1 - CPU 0의 일반 pool, worker ID 1
kworker/3:0H - CPU 3의 highpri pool, worker ID 0
kworker/u8:2 - Unbound pool ID 8, worker ID 2
rescuer 스레드:
kworker_rescue-WORKQUEUE_NAME - 해당 워크큐의 rescuer worker
큐잉과 실행
queue_work() 계열 API
/* include/linux/workqueue.h */
/* 기본 큐잉: 현재 CPU의 pool에 큐잉 */
bool queue_work(struct workqueue_struct *wq, struct work_struct *work);
/* 특정 CPU의 pool에 큐잉 */
bool queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work);
/* 지연 큐잉: delay jiffies 후 큐잉 */
bool queue_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay);
/* 특정 CPU에 지연 큐잉 */
bool queue_delayed_work_on(int cpu, struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay);
/* 타이머가 이미 pending인 delayed_work의 지연 시간 변경 */
bool mod_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay);
/* 반환값: true = 새로 큐잉됨, false = 이미 pending 상태 */
queue_work() 내부 동작
/* kernel/workqueue.c - 큐잉 경로 (간략화) */
bool queue_work_on(int cpu, struct workqueue_struct *wq,
struct work_struct *work)
{
bool ret = false;
unsigned long flags;
local_irq_save(flags);
/* PENDING 비트를 원자적으로 설정 시도 */
if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT,
work_data_bits(work))) {
/* 성공: 아직 큐에 없었음 → 실제 큐잉 수행 */
__queue_work(cpu, wq, work);
ret = true;
}
/* 실패: 이미 PENDING → 중복 큐잉 방지 */
local_irq_restore(flags);
return ret;
}
static void __queue_work(int cpu, struct workqueue_struct *wq,
struct work_struct *work)
{
struct pool_workqueue *pwq;
/* 1. 적절한 pool_workqueue 선택 */
if (wq->flags & WQ_UNBOUND) {
/* unbound: NUMA 노드 기반 pwq 선택 */
pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
} else {
/* bound: 해당 CPU의 pwq 선택 */
pwq = per_cpu_ptr(wq->cpu_pwqs, cpu);
}
/* 2. max_active 체크 */
if (pwq->nr_active >= pwq->max_active) {
/* 초과: inactive 리스트에 대기 */
list_add_tail(&work->entry, &pwq->inactive_works);
set_work_inactive(work);
} else {
/* 3. worker pool의 worklist에 추가 */
pwq->nr_active++;
list_add_tail(&work->entry, &pwq->pool->worklist);
/* 4. 유휴 worker 깨우기 (필요시) */
wake_up_worker(pwq->pool);
}
}
process_one_work() - 실행 경로
/* kernel/workqueue.c - work 실행 핵심 함수 (간략화) */
static void process_one_work(struct worker *worker,
struct work_struct *work)
{
struct pool_workqueue *pwq = get_work_pwq(work);
struct worker_pool *pool = worker->pool;
work_func_t f = work->func;
/* 1. work를 worklist에서 제거 */
list_del_init(&work->entry);
/* 2. busy hash에 등록 (flush/cancel에서 추적용) */
hash_add(pool->busy_hash, &worker->hentry,
(unsigned long)work);
worker->current_work = work;
worker->current_func = f;
worker->current_pwq = pwq;
/* 3. PENDING 비트 클리어 (큐잉 가능하게) */
set_work_pool_and_clear_pending(work, pool->id);
/* 4. pool 락 해제 후 work 함수 호출 */
spin_unlock_irq(&pool->lock);
lockdep_start();
trace_workqueue_execute_start(work);
f(work); /* ← work 콜백 실행 (프로세스 컨텍스트) */
trace_workqueue_execute_end(work, f);
lockdep_end();
/* 5. 정리 */
spin_lock_irq(&pool->lock);
worker->current_work = NULL;
/* 6. inactive 리스트에서 다음 work 활성화 */
pwq_dec_nr_in_flight(pwq);
}
PENDING 비트 클리어 타이밍: process_one_work()에서 work 콜백을 호출하기 전에 PENDING 비트가 클리어됩니다. 이는 work 콜백 내에서 동일한 work_struct를 다시 큐잉할 수 있게 하기 위함입니다 (자기 재큐잉 패턴).
취소와 플러시
취소 API
/* work 취소: 대기 중이면 제거, 실행 중이면 완료 대기 */
bool cancel_work_sync(struct work_struct *work);
/* delayed_work 취소: 타이머 + work 모두 취소 */
bool cancel_delayed_work(struct delayed_work *dwork);
bool cancel_delayed_work_sync(struct delayed_work *dwork);
/*
* cancel_work_sync() vs cancel_delayed_work():
*
* cancel_work_sync():
* - 실행 중인 work가 있으면 완료될 때까지 블록
* - 반환 후 work가 더 이상 실행되지 않음을 보장
* - IRQ 컨텍스트에서 호출 불가 (슬립 가능)
*
* cancel_delayed_work():
* - 타이머만 취소 (비동기, non-blocking)
* - 이미 큐잉되었거나 실행 중인 work는 영향 없음
* - IRQ 컨텍스트에서 호출 가능
*
* cancel_delayed_work_sync():
* - 타이머 취소 + 실행 중 work 완료 대기
* - 가장 안전한 cleanup 방법
*/
플러시 API
/* 특정 work의 실행 완료 대기 */
void flush_work(struct work_struct *work);
void flush_delayed_work(struct delayed_work *dwork);
/* 워크큐의 모든 pending work 실행 완료 대기 */
void flush_workqueue(struct workqueue_struct *wq);
/* 워크큐의 모든 work 완료 대기 + 새 큐잉 차단 */
void drain_workqueue(struct workqueue_struct *wq);
flush_workqueue() 내부 메커니즘 (color 기반)
/*
* flush_workqueue()는 "color" 메커니즘으로 구현됩니다:
*
* 1. flush 호출 시, 현재 work_color를 기록하고 다음 color로 전진
* 2. 새로 큐잉되는 work는 새 color를 받음
* 3. 이전 color의 모든 work가 완료되면 flush 완료
*
* 이 방식의 장점:
* - flush 중에 새로 큐잉된 work를 구분할 수 있음
* - 여러 flush를 동시에 처리 가능
*
* 시퀀스 다이어그램:
*
* 시간 →
* ═══════════════════════════════════════════════
* color 0: [work A] [work B] [work C]
* ↑ flush 호출
* color 1: [work D] [work E]
* ↑ 새로 큐잉된 work
*
* flush는 color 0의 A, B, C만 기다림
* color 1의 D, E는 대상이 아님
*/
올바른 정리(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); /* 잔여 work 처리 */
destroy_workqueue(dev->wq);
}
/* 5. 나머지 리소스 해제 */
free_irq(dev->irq, dev);
/* ... */
}
/* work 콜백에서 shutting_down 확인 */
static void my_periodic_work(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);
/* 종료 중이 아니면 자기 재큐잉 */
if (!dev->shutting_down)
queue_delayed_work(dev->wq, dwork, HZ);
}
데드락 주의: work 콜백 내에서 자신이 속한 워크큐에 대해 flush_workqueue()를 호출하면 데드락이 발생합니다. work가 자신의 완료를 기다리는 순환 대기 상태가 됩니다. 마찬가지로 cancel_work_sync()를 자기 자신에 대해 호출하면 안 됩니다.
Ordered Workqueue
Ordered workqueue는 큐잉된 순서대로 work를 실행하는 것을 보장합니다. 동시에 하나의 work만 실행됩니다.
생성과 특성
/* ordered workqueue 생성 */
struct workqueue_struct *owq;
owq = alloc_ordered_workqueue("my_ordered", 0);
/* 위는 다음과 동일:
* alloc_workqueue("my_ordered", WQ_UNBOUND | __WQ_ORDERED, 1);
*
* 핵심 특성:
* - max_active = 1: 한 번에 하나의 work만 실행
* - WQ_UNBOUND: unbound pool 사용 (CPU 마이그레이션 가능)
* - __WQ_ORDERED: max_active 변경 방지 (freeze/thaw 시에도 유지)
*/
/* 사용 예: 상태 머신 구현 */
struct state_machine {
struct workqueue_struct *wq; /* ordered workqueue */
struct work_struct event_work;
struct list_head event_queue;
spinlock_t lock;
enum sm_state state;
};
/* 이벤트 핸들러: 순서 보장으로 별도의 직렬화 불필요 */
static void process_events(struct work_struct *work)
{
struct state_machine *sm = container_of(work,
struct state_machine, event_work);
/* ordered workqueue이므로 이 함수는 절대 동시 실행되지 않음 */
/* 따라서 sm->state 접근에 별도 동기화 불필요 */
handle_event(sm);
}
Ordered workqueue vs 뮤텍스: ordered workqueue는 작업 직렬화를 위해 명시적인 뮤텍스 없이도 순서 보장을 제공합니다. 특히 상태 머신, 순차적 I/O 처리, 이벤트 큐 등에 유용합니다. 다만, 동시에 하나만 실행되므로 처리량(throughput)이 제한될 수 있습니다.
ordered vs max_active=1의 차이
/*
* alloc_ordered_workqueue() vs alloc_workqueue(..., 0, 1):
*
* 둘 다 동시에 하나의 work만 실행하지만 중요한 차이가 있습니다:
*
* alloc_workqueue("name", 0, 1):
* - bound (Per-CPU) 워크큐
* - max_active=1이지만 각 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 변경 방지
* - 진정한 순서 보장
*/
PREEMPT_RT와 Workqueue
PREEMPT_RT (Real-Time) 패치가 적용된 커널에서는 workqueue의 동작이 일부 변경됩니다:
RT 커널에서의 주요 변화
/*
* PREEMPT_RT에서의 workqueue 변화:
*
* 1. 모든 worker 스레드가 선점 가능(preemptible)
* - 일반 커널: softirq 컨텍스트에서 일부 work 처리 가능
* - RT 커널: 모든 work가 스레드 컨텍스트에서 실행
*
* 2. Worker 우선순위
* - 일반 커널: kworker는 SCHED_NORMAL
* - RT 커널: WQ_HIGHPRI worker는 더 높은 RT 우선순위 가능
*
* 3. BH(Bottom Half) workqueue
* - RT 커널에서 softirq가 스레드화되면서
* - BH workqueue가 softirq 대체 가능
* - WQ_BH 플래그로 BH 컨텍스트 에뮬레이션
*/
/* RT 커널에서의 BH workqueue (커널 6.x+) */
#ifdef CONFIG_PREEMPT_RT
/* softirq 대신 BH workqueue를 사용하는 예 */
struct workqueue_struct *my_bh_wq;
my_bh_wq = alloc_workqueue("my_bh", WQ_BH | WQ_HIGHPRI, 0);
#endif
RT 이식성 팁: 드라이버 코드를 RT 커널에서도 정상 동작하게 하려면, workqueue를 사용하는 것이 가장 안전한 Bottom Half 메커니즘입니다. softirq/tasklet은 RT에서 스레드화되면서 기대와 다른 지연 시간을 보일 수 있지만, workqueue는 본래부터 프로세스 컨텍스트이므로 변화가 적습니다.
우선순위 역전 문제
/*
* RT 환경에서의 우선순위 역전 시나리오:
*
* 1. 높은 우선순위 RT 태스크가 work 완료를 대기 (flush_work)
* 2. work를 실행하는 kworker는 SCHED_NORMAL (낮은 우선순위)
* 3. 중간 우선순위 태스크가 kworker를 선점
* 4. → 높은 우선순위 태스크가 간접적으로 블록됨 (우선순위 역전)
*
* 해결 방법:
* - WQ_HIGHPRI 사용 (worker 우선순위 상승)
* - RT 태스크에서 flush_work() 대신 다른 동기화 메커니즘 사용
* - 커널 설정에서 kworker의 RT 스케줄링 정책 설정
*/
디버깅
debugfs 인터페이스
# workqueue 상태 확인
cat /sys/kernel/debug/workqueue
# 출력 예시:
# workqueue CPU nr_active max_active flags
# events 0 0 256 0x0
# events_highpri 0 0 256 0x10 (WQ_HIGHPRI)
# events_unbound 3 1 512 0x2 (WQ_UNBOUND)
# mydrv_wq 0 2 4 0x8 (WQ_MEM_RECLAIM)
# worker pool 정보
cat /proc/stat | grep cpu # CPU별 부하 확인
# kworker 스레드 목록
ps -eo pid,comm,wchan | grep kworker
# 특정 kworker가 어떤 work를 실행 중인지 확인
cat /proc/<kworker_pid>/stack
# workqueue_attrs 확인 (sysfs)
ls /sys/bus/workqueue/devices/
cat /sys/bus/workqueue/devices/writeback/cpumask
cat /sys/bus/workqueue/devices/writeback/nice
sysfs를 통한 런타임 튜닝
# unbound 워크큐의 CPU 친화성 변경
echo ff > /sys/bus/workqueue/devices/writeback/cpumask
# unbound 워크큐의 nice 값 변경
echo 5 > /sys/bus/workqueue/devices/writeback/nice
# NUMA 인식 설정
echo 1 > /sys/bus/workqueue/devices/writeback/numa
ftrace를 통한 workqueue 추적
# workqueue 이벤트 활성화
echo 1 > /sys/kernel/debug/tracing/events/workqueue/enable
# 또는 개별 이벤트만:
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
# 출력 예시:
# kworker/0:1-123 [000] workqueue_execute_start: work=0xffff... function=my_work_handler
# kworker/0:1-123 [000] workqueue_execute_end: work=0xffff... function=my_work_handler
일반적인 버그와 진단
/* 버그 1: work_struct를 포함한 구조체의 조기 해제 */
/* 잘못된 코드 */
static void bad_cleanup(struct my_device *dev)
{
kfree(dev); /* dev->work가 아직 실행 중일 수 있음! */
}
/* 올바른 코드 */
static void good_cleanup(struct my_device *dev)
{
cancel_work_sync(&dev->work); /* 완료 대기 */
cancel_delayed_work_sync(&dev->dwork);
kfree(dev); /* 이제 안전 */
}
/* 버그 2: work 콜백에서 자기 워크큐 flush */
static void deadlock_work(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
flush_workqueue(dev->wq); /* 데드락! 자기 완료를 기다림 */
}
/* 버그 3: 스택에 work_struct 할당 */
static void stack_work_bug(void)
{
struct work_struct work; /* 스택 변수! */
INIT_WORK(&work, handler);
schedule_work(&work);
/* 함수 반환 후 work가 실행되면 → 스택 손상 */
}
/* 버그 4: 초기화 전 큐잉 */
static int init_order_bug(void)
{
struct my_device *dev = kzalloc(sizeof(*dev), GFP_KERNEL);
schedule_work(&dev->work); /* INIT_WORK 전! → 미정의 동작 */
INIT_WORK(&dev->work, handler); /* 늦었음 */
}
lockdep 활용: CONFIG_LOCKDEP을 활성화하면 workqueue 관련 데드락을 조기에 감지할 수 있습니다. lockdep은 flush_work/cancel_work_sync에서의 잠금 순서 위반, 순환 의존성 등을 보고합니다. 개발 중에는 반드시 활성화하세요.
사용 패턴과 모범 사례
패턴 1: IRQ → Bottom Half 위임
가장 일반적인 패턴으로, 인터럽트 핸들러에서 최소 작업만 수행하고 나머지를 워크큐로 위임합니다:
struct my_net_device {
void __iomem *regs;
int irq;
struct work_struct rx_work;
struct workqueue_struct *wq;
u32 pending_status;
};
static irqreturn_t my_net_irq(int irq, void *data)
{
struct my_net_device *ndev = data;
/* Top Half: 최소 작업 */
ndev->pending_status = readl(ndev->regs + STATUS_REG);
writel(ndev->pending_status, ndev->regs + ACK_REG);
/* Bottom Half로 위임 */
queue_work(ndev->wq, &ndev->rx_work);
return IRQ_HANDLED;
}
static void my_net_rx_work(struct work_struct *work)
{
struct my_net_device *ndev = container_of(work,
struct my_net_device, rx_work);
/* 프로세스 컨텍스트: 슬립, 뮤텍스, 메모리 할당 가능 */
struct sk_buff *skb = netdev_alloc_skb(ndev->netdev, len);
/* ... 패킷 처리 ... */
}
패턴 2: 주기적 작업 (Self-Requeueing)
struct health_monitor {
struct delayed_work check_work;
struct workqueue_struct *wq;
bool running;
};
static void health_check(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct health_monitor *mon = container_of(dwork,
struct health_monitor, check_work);
/* 상태 점검 수행 */
check_device_health(mon);
update_statistics(mon);
/* 자기 재큐잉: 5초 후 다시 실행 */
if (mon->running)
queue_delayed_work(mon->wq, dwork, 5 * HZ);
}
/* 시작 */
static int start_monitoring(struct health_monitor *mon)
{
mon->wq = alloc_workqueue("health_mon", WQ_FREEZABLE, 0);
if (!mon->wq)
return -ENOMEM;
INIT_DELAYED_WORK(&mon->check_work, health_check);
mon->running = true;
queue_delayed_work(mon->wq, &mon->check_work, 0);
return 0;
}
/* 정지 */
static void stop_monitoring(struct health_monitor *mon)
{
mon->running = false;
cancel_delayed_work_sync(&mon->check_work);
destroy_workqueue(mon->wq);
}
패턴 3: 작업 배치 처리
struct batch_processor {
struct work_struct batch_work;
struct list_head pending_items;
spinlock_t lock;
struct workqueue_struct *wq;
};
/* 빠른 경로: 아이템 추가 (IRQ/atomic 컨텍스트에서 호출 가능) */
static void enqueue_item(struct batch_processor *bp,
struct work_item *item)
{
unsigned long flags;
spin_lock_irqsave(&bp->lock, flags);
list_add_tail(&item->list, &bp->pending_items);
spin_unlock_irqrestore(&bp->lock, flags);
/* work가 이미 pending이면 큐잉되지 않음 (중복 방지) */
queue_work(bp->wq, &bp->batch_work);
}
/* 느린 경로: 축적된 아이템 일괄 처리 */
static void process_batch(struct work_struct *work)
{
struct batch_processor *bp = container_of(work,
struct batch_processor, batch_work);
LIST_HEAD(local_list);
struct work_item *item, *tmp;
/* 리스트를 로컬로 이동 (lock 구간 최소화) */
spin_lock_irq(&bp->lock);
list_splice_init(&bp->pending_items, &local_list);
spin_unlock_irq(&bp->lock);
/* lock 없이 일괄 처리 */
list_for_each_entry_safe(item, tmp, &local_list, list) {
process_single_item(item);
list_del(&item->list);
kfree(item);
}
}
패턴 4: 모듈 전체 예제
#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/slab.h>
struct my_driver {
struct workqueue_struct *wq;
struct work_struct event_work;
struct delayed_work poll_work;
atomic_t event_count;
bool active;
};
static struct my_driver *drv;
static void event_handler(struct work_struct *work)
{
struct my_driver *d = container_of(work, struct my_driver,
event_work);
int count = atomic_read(&d->event_count);
pr_info("Processing %d events\n", count);
atomic_set(&d->event_count, 0);
}
static void poll_handler(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
struct my_driver *d = container_of(dwork, struct my_driver,
poll_work);
pr_info("Polling device status\n");
if (d->active)
queue_delayed_work(d->wq, dwork, HZ);
}
static int __init my_driver_init(void)
{
drv = kzalloc(sizeof(*drv), GFP_KERNEL);
if (!drv)
return -ENOMEM;
/* ordered + MEM_RECLAIM 워크큐 생성 */
drv->wq = alloc_ordered_workqueue("my_driver", WQ_MEM_RECLAIM);
if (!drv->wq) {
kfree(drv);
return -ENOMEM;
}
INIT_WORK(&drv->event_work, event_handler);
INIT_DELAYED_WORK(&drv->poll_work, poll_handler);
atomic_set(&drv->event_count, 0);
drv->active = true;
/* 주기적 폴링 시작 */
queue_delayed_work(drv->wq, &drv->poll_work, HZ);
pr_info("my_driver loaded\n");
return 0;
}
static void __exit my_driver_exit(void)
{
/* 정리 순서가 중요! */
drv->active = false; /* 재큐잉 방지 */
cancel_delayed_work_sync(&drv->poll_work); /* 타이머+work 취소 */
cancel_work_sync(&drv->event_work); /* work 취소 */
destroy_workqueue(drv->wq); /* 워크큐 파괴 */
kfree(drv);
pr_info("my_driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Workqueue usage example");
MODULE_AUTHOR("MINZKN");
모범 사례 요약
| 규칙 | 설명 |
|---|---|
| 시스템 워크큐 우선 사용 | 특별한 이유가 없으면 schedule_work()/schedule_delayed_work() 사용 |
| WQ_MEM_RECLAIM 설정 | 메모리 회수 경로에서 사용하는 워크큐에 필수 |
| cancel_*_sync() 호출 | 모듈 제거/장치 해제 전 반드시 호출 |
| 스택에 work 할당 금지 | work_struct는 반드시 힙 또는 전역/정적 메모리에 할당 |
| 자기 flush 금지 | work 콜백에서 자신의 워크큐를 flush하면 데드락 |
| container_of 사용 | work 콜백에서 부모 구조체 접근 시 container_of 패턴 사용 |
| ordered 워크큐 활용 | 직렬 실행이 필요하면 alloc_ordered_workqueue() 사용 |
| CPU 집약적 work 표시 | 오래 실행되는 CPU bound work에는 WQ_CPU_INTENSIVE 사용 |
API 빠른 참조
| 범주 | API | 설명 |
|---|---|---|
| 초기화 | INIT_WORK() | work_struct 동적 초기화 |
| 초기화 | INIT_DELAYED_WORK() | delayed_work 동적 초기화 |
| 초기화 | DECLARE_WORK() | work_struct 정적 선언+초기화 |
| 초기화 | DECLARE_DELAYED_WORK() | delayed_work 정적 선언+초기화 |
| 워크큐 | alloc_workqueue() | 커스텀 워크큐 생성 |
| 워크큐 | alloc_ordered_workqueue() | ordered 워크큐 생성 |
| 워크큐 | destroy_workqueue() | 워크큐 파괴 |
| 큐잉 | queue_work() | work를 워크큐에 큐잉 |
| 큐잉 | queue_delayed_work() | delayed_work를 지연 큐잉 |
| 큐잉 | schedule_work() | system_wq에 큐잉 (편의 함수) |
| 큐잉 | schedule_delayed_work() | system_wq에 지연 큐잉 |
| 큐잉 | mod_delayed_work() | pending delayed_work 지연 시간 변경 |
| 취소 | cancel_work_sync() | work 취소 + 실행 완료 대기 |
| 취소 | cancel_delayed_work() | 타이머만 취소 (비동기) |
| 취소 | cancel_delayed_work_sync() | 타이머 + work 취소 + 완료 대기 |
| 플러시 | flush_work() | 특정 work 완료 대기 |
| 플러시 | flush_workqueue() | 워크큐의 모든 work 완료 대기 |
| 플러시 | drain_workqueue() | 완료 대기 + 새 큐잉 차단 |