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의 핵심 설계 원칙:

CMWQ (Concurrency Managed Workqueue) 계층 구조 workqueue_struct 사용자가 생성/사용하는 인터페이스 (alloc_workqueue) pool_workqueue (CPU 0) workqueue ↔ worker_pool 연결 (Per-CPU) pool_workqueue (CPU 1, ...) 다른 CPU/NUMA 노드 worker_pool (normal/highpri) kworker 스레드 풀 — 공유 자원 worker_pool 다른 CPU worker_pool kworker/0:0 kworker/0:1 kworker/0:2H highpri 풀 workqueue에 queue_work() 호출 → pool_workqueue 경유 → worker_pool의 idle worker가 실행 concurrency 관리: 실행 중 worker 수가 max_active 초과 시 work 지연 unbound WQ: NUMA 로컬 worker_pool 사용, CPU affinity 없음
CMWQ 아키텍처 workqueue_struct system_wq system_highpri_wq my_wq (custom) system_unbound_wq pool_workqueue (pwq) pwq (CPU0) pwq (CPU1) pwq (NUMA 0) pwq (NUMA 1) worker_pool (공유) pool (CPU0, nice=0) pool (CPU1, nice=0) unbound pool (N0) unbound pool (N1) kworker 스레드 kworker/0:0 kworker/0:1 kworker/1:0 kworker/u8:0 kworker/u8:1 kworker/u8:2 Bound (Per-CPU) Unbound (Per-NUMA) kworker/CPU:ID (bound) | kworker/uPOOL:ID (unbound) | H suffix = highpri
CMWQ 계층: workqueue → pool_workqueue → worker_pool → kworker 스레드

CMWQ 아키텍처 상세

CMWQ의 핵심은 workqueue_struct, pool_workqueue, worker_pool, worker 네 가지 구조체의 관계입니다. 각 구조체의 역할과 연결 관계를 상세히 살펴봅니다.

CMWQ 전체 아키텍처: 구조체 관계 workqueue_struct 계층 (사용자 인터페이스) system_wq flags=0 system_highpri_wq WQ_HIGHPRI my_wq (커스텀) WQ_UNBOUND ordered_wq __WQ_ORDERED, max_active=1 pool_workqueue 계층 (연결 레이어) pwq (CPU 0) nr_active: 2 max_active: 256 pwq (CPU 1) nr_active: 1 max_active: 256 pwq (NUMA 0) unbound pool 연결 delayed_works 리스트 pwq (ordered) nr_active: 0/1 max_active=1 고정 worker_pool 계층 (공유 실행 엔진) pool (CPU 0, nice=0) nr_running=1, nr_idle=2 worklist → pending works pool (CPU 1, nice=0) nr_running=0, nr_idle=1 manager 가동 중 unbound pool (NUMA 0) cpumask: 0-3, nice=0 rescuer 스레드 대기 worker (kworker 스레드) 계층 kworker/0:0 BUSY kworker/0:1 IDLE kworker/0:2 IDLE kworker/1:0 IDLE kworker/u4:0 BUSY rescuer (u4) STANDBY 핵심 관계 요약: workqueue_struct → (1:N) pool_workqueue → (N:1) worker_pool → (1:N) worker (kworker) 여러 workqueue가 하나의 worker_pool을 공유합니다. pool_workqueue는 둘을 연결하며 nr_active/max_active를 관리합니다. Work Item 흐름 queue_work() pwq 선택 pool worklist kworker 실행
/*
 * 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 동시성 관리: Worker 상태 전환 IDLE idle_list에 대기 idle_timer 설정됨 BUSY (Running) work 콜백 실행 중 nr_running++ BLOCKED mutex/IO 대기 nr_running-- worklist에 work 도착 슬립/블록 깨어남 work 완료 DESTROYED IDLE_WORKER_TIMEOUT (300초 경과) 동시성 관리 메커니즘 정상 상태 nr_running > 0 → idle worker 대기 → 추가 worker 불필요 모든 worker 블록됨 nr_running == 0 → manager가 감지 → create_worker() 호출 Mayday (긴급) worker 생성 실패 → mayday_timer 만료 → rescuer 스레드 가동 Bound Pool: CPU별 2개 (nice=0 + nice=-20). 스케줄러 연동으로 블록 감지 Unbound Pool: NUMA 노드별. cpumask+nice 속성 조합으로 관리 (workqueue_attrs) idle_timer: 300초 후 idle worker 제거 최소 1개 idle worker는 항상 유지 mayday_timer: worker 생성 재시도 WQ_MEM_RECLAIM 시 rescuer 활성화
/*
 * 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_UNBOUNDPer-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로 동작
 *   - 전력 효율이 중요한 모바일/임베디드 환경
 */
플래그비트rescuerPool 유형주요 효과
WQ_UNBOUND1선택UnboundNUMA-aware, CPU 비고정, long-running 적합
WQ_FREEZABLE2선택Anysuspend 시 동결, resume 시 재개
WQ_MEM_RECLAIM3필수Anyrescuer 보장, reclaim 경로 데드락 방지
WQ_HIGHPRI4선택nice=-20높은 스케줄링 우선순위
WQ_CPU_INTENSIVE5선택Boundconcurrency 관리 제외, worker 증식 방지
WQ_POWER_EFFICIENT7선택조건부커널 파라미터로 unbound 전환
WQ_SYSFS9선택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_wqWQ_HIGHPRI256높은 우선순위 작업직접 queue_work()
system_long_wq(기본)256장시간 작업직접 queue_work()
system_unbound_wqWQ_UNBOUND256CPU-unbound 작업직접 queue_work()
system_freezable_wqWQ_FREEZABLE256suspend 시 중단 필요직접 queue_work()
system_power_efficient_wqWQ_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 어떤 큐에도 없음 다시 큐잉 가능 PENDING WORK_STRUCT_PENDING set worklist에 대기 중 RUNNING PENDING 클리어 current_work = this queue_work() schedule_work() kworker dequeue 콜백 완료 → IDLE 복귀 실행 중 재큐잉 (PENDING set → 완료 후 재실행) 중복 큐잉 → no-op Cancel 동작 PENDING 상태 즉시 dequeue return true RUNNING 상태 완료 대기 (블록킹) return false Flush 동작 flush_work() 특정 work 완료 대기 후 반환 flush_workqueue() 해당 WQ의 모든 pending work 대기 drain_workqueue() 전부 소진 + 차단 핵심: 하나의 work_struct는 동시에 하나의 workqueue에만 속할 수 있음. 서로 다른 WQ에 같은 work 큐잉 불가.
/*
 * 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로 나뉩니다. 각각의 동작 차이를 아래 다이어그램에서 비교합니다.

Per-CPU vs Unbound vs Ordered 동작 비교 Per-CPU (Bound) flags = 0, max_active = 256 CPU 0 pool W-A W-B CPU 1 pool W-C W-D 4개 동시 실행 가능! (CPU별 독립적 max_active) 실행 타임라인: W-A (CPU0) W-B (CPU0) W-C (CPU1) W-D Unbound WQ_UNBOUND, max_active = 4 Unbound Pool (NUMA 0) W-A W-B W-C D NUMA 노드당 max_active 적용 (CPU 마이그레이션 가능) 실행 타임라인: W-A (any) W-B W-C W-D Ordered __WQ_ORDERED, max_active = 1 Unbound Pool (단일) W-A B 대기 시스템 전체에서 하나만 실행 (FIFO 순서 보장) 실행 타임라인: W-A W-B W-C W-D 순서 보장: A → B → C → D 특징: - CPU 캐시 친화적 - CPU별 독립 max_active - 순서 보장 없음 - 짧은 작업에 최적 - 실시간 워크로드 - max_active=1도 CPU별 독립! 특징: - NUMA 로컬리티 활용 - CPU 마이그레이션 허용 - long-running 작업 적합 - 노드당 max_active 적용 - bound pool 간섭 없음 - WQ_CPU_INTENSIVE와 결합 불가 특징: - 전역 FIFO 순서 보장 - __WQ_ORDERED + unbound - max_active 변경 불가 - 상태 머신, 저널링 - 처리량 낮음 (직렬화) - freeze/thaw에도 max_active=1 유지

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() vs alloc_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() / flush_workqueue() 동기화 흐름 flush_work(&work) 흐름 호출 스레드 work 상태 확인 idle → 즉시 return false pending/running completion 대기 (슬립) 완료 후 return true flush_workqueue(wq) 흐름 (Color 기반) color=N 기록 color N+1로 전진 대기 Color N (flush 대상): W-A W-B W-C Color N+1 (새 큐잉): W-D W-E Color N 모두 완료 → flush 반환 Color N+1(W-D, W-E)은 대기 대상 아님 여러 flush 동시 진행 가능 (multi-color) Flush/Cancel 데드락 시나리오 자기 WQ flush (데드락) work 콜백 내에서 flush_workqueue(my_wq) 호출 → 자신의 완료를 자신이 대기 → 영원히 블록 Ordered WQ 의존 (데드락) work A 콜백에서 work B 큐잉 + flush_work(B) → max_active=1이므로 A 완료 전 B 시작 불가 → 데드락 교차 flush (데드락) WQ-A의 work가 WQ-B flush, WQ-B의 work가 WQ-A flush → 순환 대기 → 양쪽 모두 영원히 블록 안전한 패턴 다른 WQ의 work에서 flush → OK (의존 관계 없을 때) 모듈 exit에서 flush → OK (work 콜백 외부)
/* 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 등 여러 도구를 활용합니다. 아래 다이어그램은 문제 유형별 디버깅 접근 경로를 보여줍니다.

Workqueue 디버깅 도구와 접근 경로 문제 유형 → 진단 도구 Work 정체 (stall/hang) 지연/성능 저하 (latency) 데드락 (flush 관련) 스레드 폭발 (worker 과다 생성) debugfs /sys/kernel/debug/ workqueue 실시간 상태 확인 ftrace tracepoint workqueue_queue_work workqueue_execute_* 실행 시간 측정 lockdep CONFIG_PROVE_LOCKING flush 의존성 추적 교차 flush 감지 sysfs + ps ps aux | grep kworker /sys/devices/virtual/ 스레드 수 모니터링 wq_watchdog CONFIG_WQ_WATCHDOG watchdog_thresh 설정 trace-cmd / perf trace-cmd record -e workqueue:* crash 도구 struct worker_pool worklist 순회 분석 커널 파라미터 workqueue.power_efficient workqueue.watchdog_thresh debugfs 출력 해석 가이드 workqueue CPU POOL ACTIVE/MAX WORKERS FLAGS events 0 0 0/256 3 ← CPU 0 bound, 정상 events_highpri 0 1 0/256 2 highpri ← highpri pool (nice=-20) my_driver_wq -1 16 2/4 3 unbound ← unbound, 2/4 활성 stuck_wq -1 16 256/256 260 unbound ← 포화 상태! 조사 필요 ACTIVE/MAX가 MAX에 도달하면 새 work가 inactive_works에 쌓임 → 처리 지연 발생 WORKERS가 비정상적으로 증가하면 work 콜백의 장시간 블록 또는 데드락 의심

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-runningWQ_UNBOUND 사용bound pool 동시성 관리 간섭 방지
CPU-heavyWQ_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의 동작이 일부 변경됩니다:

⚠️

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 전! → 미정의 동작 */
필수 관련 문서: 참고 문서:
  • Softirq/Hardirq 심화 — softirq 실행 흐름, ksoftirqd, 인터럽트 하위 반쪽 메커니즘 상세
  • Tasklet 심화 — tasklet_struct, tasklet_schedule, deprecated 이유, 마이그레이션 가이드