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 최적화가 진행 중입니다.

단계별 이해

  1. Work 등록
    드라이버가 INIT_WORK()work_struct를 초기화하고, 콜백 함수를 지정합니다. 이 콜백이 나중에 worker 스레드에서 실행될 실제 작업입니다.
  2. Work 예약
    schedule_work() 또는 queue_work()를 호출하면, work item이 workqueue에 연결된 worker pool의 대기 목록에 삽입됩니다. 인터럽트 컨텍스트에서도 호출 가능합니다.
  3. Worker 스레드 깨우기
    CMWQ가 worker pool에서 유휴(Idle) worker 스레드를 찾아 깨웁니다. 유휴 스레드가 없으면 동시성 수준에 따라 새 worker를 생성합니다.
  4. 콜백 실행
    Worker 스레드가 work item을 꺼내 콜백 함수를 실행합니다. 프로세스 컨텍스트이므로 mutex_lock(), kmalloc(GFP_KERNEL), msleep() 등 블로킹 API를 자유롭게 사용할 수 있습니다.
  5. 완료 및 정리
    콜백 실행이 끝나면 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
레거시 APICMWQ 대응설명
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) 동결 가능, rescuer 보장

레거시 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 네 가지 구조체(Struct)의 관계입니다. 각 구조체의 역할과 연결 관계를 상세히 살펴봅니다.

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 실행
/* kernel/workqueue.c, kernel/workqueue_internal.h — 간략화 */

/*
 * 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.hkernel/workqueue.c에 정의된 핵심 중간 구조체들입니다.

  • pool_workqueuepool_workqueue(pwq)는 workqueue_structworker_pool을 연결하는 중간 계층입니다. 각 workqueue는 CPU/NUMA 노드마다 하나의 pwq를 가지며, nr_active/max_active로 해당 pool에서의 동시 실행 수를 제어합니다. max_active를 초과하면 inactive_works 리스트에 대기합니다.
  • work_color / flush_colorflush_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 동시성 관리: 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;
};
코드 설명

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_timerIDLE_WORKER_TIMEOUT(300초) 후 idle worker를 소멸시키는 타이머입니다. 불필요한 커널 스레드를 제거하여 시스템 자원을 절약합니다.
  • mayday_timerworker 생성에 실패했을 때 재시도를 트리거하는 타이머입니다. WQ_MEM_RECLAIM workqueue에서는 이 타이머 만료 시 send_mayday()를 호출하여 rescuer 스레드를 활성화합니다.

Worker 스레드 생성과 소멸

/* kernel/workqueue.c — 간략화 */

/*
 * 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 스레드

/* kernel/workqueue.c — 간략화 */

/*
 * 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/workqueuekworker 스레드 이름에 표시됩니다. printf 형식을 지원하여 alloc_workqueue("drv-%s", flags, max, name)과 같이 동적 이름을 사용할 수 있습니다.
  • flagsWQ_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_UNBOUNDPer-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)에서 사용하는 workqueue에 필수이며, 파일시스템, 블록 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로 동작합니다. 전력 효율이 중요한 모바일/임베디드 환경에 적합합니다.
플래그비트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 vs 전용 Workqueue 선택 가이드 Work 큐잉 필요 특수 플래그 조합 필요? (MEM_RECLAIM+UNBOUND 등) alloc_workqueue() 아니오 flush 격리 필요? (다른 서브시스템 영향 방지) 전용 workqueue 아니오 높은 우선순위? system_highpri_wq 아니오 CPU 고정 불필요? system_unbound_wq 아니오 장시간 실행? long_wq 아니오 schedule_work() !

시스템 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()
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 생성이 필요한 경우:

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 상태 전이:

  1. Idle: work이 어떤 workqueue에도 없는 상태입니다.
  2. queue_work() / schedule_work() 호출로 Pending 상태로 전환합니다. WORK_STRUCT_PENDING 비트가 set됩니다.
  3. kworker가 dequeue하면 Running 상태로 전환합니다. PENDING 비트가 클리어되고 current_work = this가 설정됩니다.
  4. 콜백이 완료되면 다시 Idle 상태로 돌아가며, 재큐잉이 가능합니다.

핵심 규칙:

/* 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 상태 전이 모델입니다.

  • dataatomic_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되어 콜백 완료 후 자동 재실행됩니다.

work_struct data 필드 비트 인코딩

work_structdata 필드는 하나의 atomic_long_t에 여러 정보를 멀티플렉싱(Multiplexing)하는 정교한 설계입니다. 하위 비트에는 상태 플래그를 저장하고, 상위 비트에는 pool_workqueue 포인터 또는 pool ID를 인코딩합니다. 이 설계로 단일 atomic 연산만으로 work의 상태 전이와 소속 추적이 가능합니다.

work_struct.data 비트 레이아웃 (64비트 시스템) pool_workqueue 포인터 / pool ID 비트 63~WORK_STRUCT_FLAG_BITS COLOR flush 태깅 PWQ 비트 2 DELAYED 비트 1 PENDING 비트 0 63 4 2 1 0 data 필드 상태별 인코딩 IDLE (초기화 직후) data = WORK_STRUCT_NO_POOL | 0 (PENDING=0, pool 정보 없음) 포인터 영역: WORK_STRUCT_NO_POOL (특수 값) PENDING=0, DELAYED=0, PWQ=0 PENDING (큐잉됨) data = pwq_ptr | color | WORK_STRUCT_PENDING 포인터 영역: pool_workqueue 주소 (정렬 보장) PENDING=1, PWQ=1, COLOR=현재 flush color RUNNING (실행 중) data = pool_id << SHIFT | color | 0 (PENDING 클리어) 포인터 영역: worker_pool ID (pwq 포인터 아님) PENDING=0, PWQ=0, COLOR 유지 (flush 추적용) RUNNING + RE-QUEUED (실행 중 재큐잉) data = pool_id << SHIFT | color | PENDING 실행 완료 후 자동으로 재큐잉됨 PENDING=1 (콜백에서 queue_work 호출 시)
/*
 * work_struct.data 비트 레이아웃 (include/linux/workqueue.h):
 *
 * 비트 0: WORK_STRUCT_PENDING
 *   - 1 = work가 큐에 있거나 큐잉 예정
 *   - 0 = work가 idle이거나 실행 중
 *   - test_and_set_bit()으로 원자적 설정 → 중복 큐잉 방지
 *
 * 비트 1: WORK_STRUCT_DELAYED
 *   - delayed_work 전용 플래그
 *   - 1 = 타이머 대기 중 (아직 worklist에 없음)
 *
 * 비트 2: WORK_STRUCT_PWQ
 *   - 1 = 상위 비트가 pool_workqueue 포인터
 *   - 0 = 상위 비트가 pool ID (실행 중 또는 idle)
 *
 * 비트 3~(WORK_STRUCT_FLAG_BITS-1): WORK_STRUCT_COLOR
 *   - flush_workqueue()의 color 태깅에 사용
 *   - WORK_NR_COLORS(16) 순환 → 4비트
 *
 * 비트 WORK_STRUCT_FLAG_BITS~63: 포인터/ID
 *   - PWQ=1: pool_workqueue 포인터 (8바이트 정렬 보장)
 *   - PWQ=0: worker_pool ID (get_work_pool() 검색용)
 *
 * 핵심 설계 이점:
 * - 단일 atomic_long_t로 상태+소속을 관리
 * - test_and_set_bit 하나로 중복 큐잉 방지
 * - 별도 lock 없이 work 상태를 추적
 */

/* 비트 조작 매크로 */
#define WORK_STRUCT_PENDING_BIT   0
#define WORK_STRUCT_DELAYED_BIT   1
#define WORK_STRUCT_PWQ_BIT       2
#define WORK_STRUCT_COLOR_SHIFT   3

/* PENDING 비트 조작 예시 */
static inline bool work_pending(struct work_struct *work)
{
    return atomic_long_read(&work->data) & WORK_STRUCT_PENDING;
}

/* queue_work_on()에서의 중복 방지 핵심 코드 */
bool queue_work_on(int cpu, struct workqueue_struct *wq,
                   struct work_struct *work)
{
    /* test_and_set_bit: PENDING이 이미 1이면 false 반환 → 큐잉 안 함 */
    if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT,
                          work_data_bits(work))) {
        __queue_work(cpu, wq, work);
        return true;
    }
    return false;
}
코드 설명

include/linux/workqueue.hkernel/workqueue.c에 정의된 work_struct.data 비트 레이아웃입니다.

  • PENDING 비트 (0)가장 중요한 비트입니다. queue_work()에서 test_and_set_bit()으로 원자적으로 설정하여 중복 큐잉을 방지합니다. process_one_work()에서 콜백 실행 직전에 클리어되어, 콜백 실행 중 같은 work를 다시 큐잉할 수 있게 합니다.
  • PWQ 비트 (2)상위 비트의 해석 방법을 결정합니다. work가 큐에 있을 때(PENDING=1)는 pool_workqueue 포인터를 저장하여 소속 workqueue와 pool을 즉시 찾을 수 있습니다. 실행 중이거나 idle일 때는 pool ID만 저장하여 메모리 효율을 높입니다.
  • COLOR 비트flush_workqueue()의 "color" 메커니즘에 사용됩니다. flush 호출 시점의 color와 이후 큐잉되는 work의 color를 다르게 태깅하여, flush가 "호출 시점까지의 work만" 대기하도록 합니다. 최대 16색(4비트) 순환으로 여러 flush를 동시에 처리할 수 있습니다.
  • 포인터 정렬pool_workqueue 구조체는 최소 8바이트 정렬이 보장되므로, 포인터의 하위 3비트는 항상 0입니다. 이 빈 비트를 플래그로 활용하는 "tagged pointer" 기법입니다.

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()

/* include/linux/workqueue.h — 간략화 */

/* 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로 나뉩니다. 각각의 동작 차이를 아래 다이어그램에서 비교합니다.

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

/* include/linux/workqueue.h — 간략화 */

/* 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초마다 */
}

Delayed Work 취소 경쟁 조건 분석

delayed_work는 타이머(Timer)와 work의 2단계 구조이므로 취소 시 미묘한 경쟁 조건(Race Condition)이 발생할 수 있습니다. 아래 다이어그램은 cancel_delayed_work() vs cancel_delayed_work_sync()의 차이와 각 시점에서의 동작을 상세히 보여줍니다.

delayed_work 취소 경쟁 조건: 타이밍별 동작 분석 시간 타이머 대기 (DELAYED) 타이머 만료 큐잉 Pending 콜백 실행 중 (Running) 완료 (Idle) 시나리오 A: 타이머 대기 중 취소 cancel 호출 cancel_delayed_work(): del_timer() 성공 → return true cancel_delayed_work_sync(): del_timer() 성공 → return true 안전 시나리오 B: 큐잉 직후 (Pending 상태) 취소 cancel 호출 cancel_delayed_work(): del_timer() 실패 + try_to_grab_pending() → return true cancel_delayed_work_sync(): 동일 + 실행 중이면 대기 → return true 안전 시나리오 C: 콜백 실행 중 취소 (위험 구간) cancel 호출 cancel_delayed_work(): 타이머 없음 + 실행 중 → return false 콜백이 schedule_delayed_work()로 재큐잉하면 → 취소 실패! cancel_delayed_work_sync(): 콜백 완료까지 대기 → return false 위험 안전 취소 API 비교 cancel_delayed_work(): 비동기 — 타이머만 취소, 실행 중인 콜백은 대기하지 않음 → 재큐잉 방지 불가 cancel_delayed_work_sync(): 동기 — 타이머 취소 + 실행 중 콜백 완료 대기 → 완전한 취소 보장 (슬립 필요)
/*
 * delayed_work 취소 패턴: 경쟁 조건 회피
 *
 * 패턴 1: shutdown 플래그 + cancel_delayed_work_sync()
 *   가장 안전하고 권장되는 패턴
 */
struct my_device {
    struct delayed_work poll_work;
    struct workqueue_struct *wq;
    atomic_t shutting_down;          /* atomic: IRQ에서도 접근 */
};

static void poll_handler(struct work_struct *work)
{
    struct delayed_work *dwork = to_delayed_work(work);
    struct my_device *dev = container_of(dwork,
        struct my_device, poll_work);

    do_poll_check(dev);

    /* atomic_read: shutdown 플래그 확인 후 재큐잉 결정 */
    if (!atomic_read(&dev->shutting_down))
        queue_delayed_work(dev->wq, dwork,
            msecs_to_jiffies(500));
}

static void my_device_stop(struct my_device *dev)
{
    /* 1. shutdown 플래그 설정 (재큐잉 방지) */
    atomic_set(&dev->shutting_down, 1);

    /* 2. sync 취소: 타이머 + 실행 중 콜백 모두 대기
     *    콜백이 실행 중이더라도, shutdown 플래그가 설정되어
     *    재큐잉하지 않으므로 cancel 이후 work가 다시 나타나지 않음 */
    cancel_delayed_work_sync(&dev->poll_work);
    /* 이 시점에서 work가 실행되지 않음을 100% 보장 */
}

/*
 * 패턴 2: disable_delayed_work_sync() 시뮬레이션
 *   flush 없이 영구적으로 비활성화 (커널 6.7+에서는 disable_work() 추가)
 */
static void my_device_suspend(struct my_device *dev)
{
    /* 재큐잉 방지 후 취소 — suspend 경로에서 안전 */
    atomic_set(&dev->shutting_down, 1);
    cancel_delayed_work_sync(&dev->poll_work);
}

static void my_device_resume(struct my_device *dev)
{
    /* 재활성화 + 재스케줄 */
    atomic_set(&dev->shutting_down, 0);
    queue_delayed_work(dev->wq, &dev->poll_work,
        msecs_to_jiffies(500));
}

/*
 * 패턴 3: mod_delayed_work()를 이용한 동적 주기 조정
 *   cancel 없이 타이머를 재설정하여 경쟁 조건 자체를 회피
 */
static void adjust_poll_interval(struct my_device *dev,
                               unsigned int new_ms)
{
    /* mod_delayed_work(): 기존 타이머 취소 + 새 delay로 재설정
     * cancel + re-queue의 원자적 수행
     * 경쟁 조건 없음 — 내부에서 lock으로 보호됨 */
    mod_delayed_work(dev->wq, &dev->poll_work,
        msecs_to_jiffies(new_ms));
}
코드 설명

주기적 delayed_work의 안전한 취소 패턴 모음입니다.

  • 패턴 1: shutdown 플래그가장 안전한 패턴입니다. atomic_t 플래그로 재큐잉을 방지한 후 cancel_delayed_work_sync()로 현재 실행 중인 콜백 완료를 대기합니다. 콜백이 플래그를 확인하므로 cancel 이후 재큐잉이 발생하지 않습니다.
  • 패턴 2: suspend/resume일시적으로 work를 비활성화했다가 재활성화하는 패턴입니다. suspend 경로에서 cancel_delayed_work_sync()를 호출하고, resume에서 다시 스케줄합니다. WQ_FREEZABLE 대신 수동으로 제어할 때 유용합니다.
  • 패턴 3: mod_delayed_work()취소 없이 주기를 변경하는 패턴입니다. mod_delayed_work()는 기존 타이머를 원자적으로 취소하고 새 delay로 재설정합니다. cancel + re-queue 사이의 경쟁 조건을 원천적으로 회피합니다.

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 변경 방지 → 진정한 순서 보장(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() / 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 콜백 외부)
/* include/linux/workqueue.h, kernel/workqueue.c — 간략화 */

/* 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 등 여러 도구를 활용합니다. 아래 다이어그램은 문제 유형별 디버깅 접근 경로를 보여줍니다.

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(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 worksoftirq 컨텍스트에서 실행ksoftirqd 스레드 내에서 실행 (선점 가능)
Worker 우선순위SCHED_NORMAL (nice=0 또는 nice=-20)chrt로 RT 우선순위 설정 가능
local_bh_disable()softirq 실행 억제 (경량)Per-CPU 카운터 기반 (마이그레이션 비활성화)
pool->lockraw_spinlock (비선점)raw_spinlock 유지 (RT에서도 spin)
RT 커널: 우선순위 역전(Priority Inversion) 시나리오 시간 → RT Task prio=10 실행 중 F flush_work() 대기 (블로킹) 재개 일반 Task prio=50 CPU 점유 (kworker보다 높은 우선순위) kworker nice=0 work 실행 선점됨 (일반 태스크에 밀림) 완료 우선순위 역전 구간 해결 방법 방법 1: WQ_HIGHPRI kworker nice=-20 방법 2: chrt로 RT 설정 kworker에 SCHED_FIFO 방법 3: flush 회피 completion/waitqueue 사용 RT 태스크(prio=10) → flush_work() → kworker(SCHED_NORMAL) 대기 → 중간 태스크가 kworker 선점 → 역전

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 상호작용

일반 커널 vs RT 커널: Bottom Half 실행 경로 일반 커널 (PREEMPT_NONE) Hardirq Softirq (atomic) WQ_BH work softirq ctx Tasklet softirq ctx kworker 일반 workqueue 프로세스 ctx 슬립 가능 슬립 불가 선점 불가 PREEMPT_RT 커널 Hardirq (최소) IRQ Thread WQ_BH work 스레드 ctx Tasklet 스레드 ctx kworker 일반 workqueue 프로세스 ctx 슬립 가능 모두 선점 가능 모두 슬립 가능 우선순위 설정 가능 RT 핵심: 모든 Bottom Half가 스레드 컨텍스트 → workqueue가 가장 자연스러운 선택
항목일반 커널PREEMPT_RT
실행 위치 __do_softirq() 내에서 실행 ksoftirqd/N 스레드 내에서 실행
컨텍스트 인터럽트 비활성화 상태, 선점 불가, local_bh_disable() 영역 프로세스 컨텍스트 (스레드화된 softirq), 선점 가능, 슬립 가능
우선순위 설정 불가 chrt -f -p 50 $(pidof ksoftirqd/0)으로 설정 가능

중요: WQ_BH work의 콜백은 여전히 softirq 규약을 따라야 합니다.

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 관련 버그는 커널 패닉, 메모리 손상, 데드락 등 심각한 결과를 초래합니다. 가장 빈번한 버그 패턴과 올바른 해결법을 상세히 분석합니다.

Workqueue 버그 패턴 분류와 심각도 수명 관리 버그 Use-After-Free, 스택 손상 동기화 버그 데드락, 경쟁 조건 초기화 버그 미초기화, 이중 초기화 설계 버그 잘못된 WQ 선택, 컨텍스트 오용 1. 구조체 조기 해제 2. 스택 work_struct 3. 콜백 내 self-flush 4. A↔B 교차 flush 5. 락 역순 (lock ordering) 6. INIT_WORK 전 큐잉 7. 이중 INIT_WORK 8. atomic ctx에서 flush 9. 잘못된 WQ 플래그 심각도 커널 패닉 / 메모리 손상 데드락 / 시스템 멈춤 미정의 동작 / 성능 저하

버그 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);
}

버그 탐지 도구

도구탐지 대상커널 설정오버헤드
KASANUse-After-Free, 스택 접근CONFIG_KASAN=y~2x 메모리, ~2x CPU
LOCKDEP데드락, 교차 flush, 락 역순CONFIG_LOCKDEP=y~3x 부팅 시간
PROVE_LOCKING잠재적 데드락 경로CONFIG_PROVE_LOCKING=yLOCKDEP 포함
DEBUG_OBJECTS미초기화 work, 이중 초기화CONFIG_DEBUG_OBJECTS_WORK=y경미
WQ_WATCHDOGwork 장기 미처리 (교착 의심)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은 여러 구조적 문제가 있었습니다:

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_wqWQ_BH일반 softirq 수준 Bottom Half 처리 (tasklet 대체)
system_bh_highpri_wqWQ_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 제약사항

항목일반 WorkqueueWQ_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 컨텍스트라는 점을 항상 기억하세요.
WQ_BH 실행 경로 vs 일반 Workqueue 실행 경로 하드웨어 인터럽트 (IRQ) queue_work(system_bh_wq, ...) softirq 실행 (동일 CPU) work 콜백 실행 (BH) 지연: 매우 낮음 (수 us) 제약: 슬립 불가, GFP_ATOMIC만 queue_work(system_wq, ...) kworker 스레드 깨움 스케줄러 컨텍스트 전환 work 콜백 실행 (kworker) 지연: 스케줄링 오버헤드 (수십~수백 us) 자유: 슬립 가능, GFP_KERNEL 허용 WQ_BH: tasklet과 동일한 지연 특성 + workqueue의 관리 인프라 (cancel/flush/sysfs)
마이그레이션 전략: tasklet을 WQ_BH로 변환할 때, 콜백 함수의 시그니처가 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 캐시 민감 작업
smtSMT 형제 CPUL1/L2 공유 활용하이퍼스레딩 활용 작업
cacheLLC 공유 CPULLC 캐시 지역성대부분의 I/O 작업 (기본값)
numaNUMA 노드메모리 지역성메모리 집약 작업
system전체 시스템최대 처리량, 지역성 없음CPU 부하 분산(Load Balancing) 필요 시
default커널 기본값현재 cache특별한 요구 없을 때
CPU 토폴로지와 Affinity Scope 대응 (2-소켓 NUMA 시스템 예시) scope=system (전체 시스템) NUMA Node 0 — scope=numa 로컬 메모리 접근 (빠름) LLC 0 (L3 캐시) scope=cache scope=smt CPU 0 scope=cpu CPU 1 scope=cpu scope=smt CPU 2 CPU 3 LLC 1 (L3 캐시) scope=cache CPU 4 CPU 5 CPU 6 CPU 7 NUMA Node 1 — scope=numa 원격 메모리 접근 (느림) LLC 2 CPU 8 CPU 9 CPU 10 CPU 11 LLC 3 CPU 12 CPU 13 CPU 14 CPU 15 메모리 (Node 0) 로컬: ~80ns 메모리 (Node 1) 로컬: ~80ns 원격: ~200ns 넓은 scope ← 처리량 우선 좁은 scope → 지역성 우선 system numa cache (기본) smt cpu

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 시스템)

스코프별 성능 특성

cache (기본값, 권장)
LLC 캐시를 공유하는 CPU 그룹 내에서 work를 실행합니다. 대부분의 워크로드에서 좋은 균형을 제공합니다. 예를 들어 4-코어 CCX에서 work가 해당 CCX 내 코어로 분산됩니다.
numa
대규모 메모리 할당/접근 패턴에 적합합니다. NUMA 원격 접근 페널티를 회피하며, 대용량 파일 I/O나 페이지 캐시 작업에 적합합니다.
system
부하가 불균형할 때 전체 CPU에 분산합니다. 캐시 지역성을 포기하는 대신 최대 처리량을 확보합니다. 네트워크 패킷 처리, 병렬 암호화 등에 사용합니다.
cpu / smt
매우 작은 work item에서 캐시 오염을 최소화합니다. 처리량이 제한될 수 있으므로 신중하게 사용해야 합니다.
CPU 토폴로지(Topology) 의존성: Affinity scope는 시스템의 CPU 토폴로지에 따라 실제 동작이 달라집니다. 예를 들어 SMT가 비활성화된 시스템에서 smt 스코프는 cpu와 동일하게 동작합니다. /sys/devices/system/cpu/cpu0/topology/에서 토폴로지 정보를 확인할 수 있습니다.

스코프별 성능 비교 (참고 수치)

다음은 대표적인 2-소켓 NUMA 서버(16코어/소켓, LLC 4개)에서의 스코프별 상대 성능입니다. 워크로드에 따라 최적 스코프가 달라지므로 반드시 실측이 필요합니다.

스코프Work 크기캐시 히트율처리량(상대)지연시간(상대)권장 시나리오
cpu작은 work (<1KB 데이터)~95%0.4x1.0x (최소)L1 핫 데이터 반복 접근
smt작은~중간~90%0.6x1.1xSMT 형제 간 L1/L2 공유 활용
cache중간 (4-64KB)~75%1.0x (기준)1.5x기본값: 대부분의 I/O, 네트워크
numa큰 work (메모리 집약)~50%1.3x2.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 */
Unbound Workqueue NUMA 배치: 2-NUMA 노드 시스템 NUMA Node 0 CPU 0 CPU 1 CPU 2 CPU 3 Local Memory (빠른 접근) Unbound Worker Pool (Node 0) cpumask: 0-3 | nice: 0 kworker/u8:0 kworker/u8:1 kworker/u8:2 pool_workqueue (Node 0 전용) NUMA Node 1 CPU 4 CPU 5 CPU 6 CPU 7 Local Memory (빠른 접근) Unbound Worker Pool (Node 1) cpumask: 4-7 | nice: 0 kworker/u9:0 kworker/u9:1 kworker/u9:2 pool_workqueue (Node 1 전용) alloc_workqueue("my_wq", WQ_UNBOUND, max_active) 원격 접근 (느림) queue_work() 호출 시 현재 CPU의 NUMA 노드에 해당하는 pool_workqueue로 라우팅 → 메모리 지역성 자동 보장 (wq_calc_node_cpumask)

대규모 NUMA 시스템 최적화

최적화 전략: 4-NUMA 이상 시스템에서는 workqueue cpumask를 적절히 제한하고, affinity scope를 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 생성과 소멸

/* kernel/workqueue.c — 간략화 */

/*
 * 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 경로 — 동기화 대기 시

경쟁 완화 설계:

높은 contention 발생 시 대응 방법:

Worker 상태 머신: 생성부터 소멸까지 전체 생명주기 CREATED create_worker() IDLE idle_list 대기 RUNNING nr_running++ BLOCKED nr_running-- DESTROYED destroy_worker() enter_idle work 도착 work 완료 sleep wakeup idle_timer (300s) worker_thread() 메인 루프 worklist 확인 work 있는가? Yes process_one_work() 콜백 실행 manage_workers() 관리 판단 sleep (idle) or 루프 반복 No → schedule() idle_list에 복귀 다음 work 대기

CPU Intensive Work

WQ_CPU_INTENSIVE 플래그가 설정된 workqueue에서 실행되는 work는 CMWQ 동시성 관리에서 특별 취급됩니다. 이 work가 오래 실행되어도 같은 pool의 다른 work 실행을 방해하지 않습니다.

WQ_CPU_INTENSIVE: nr_running 동작 비교 일반 Workqueue (문제 상황) 시간 → W-A 장시간 CPU 작업 (암호화 등) — nr_running에 포함 W-B 큐에 대기 중 (실행 불가) nr_running = 1 (항상) → 새 worker 깨우지 않음 결과: Work B가 Work A 완료까지 ~수백ms 지연 짧은 I/O work가 긴 CPU work에 의해 블록됨 WQ_CPU_INTENSIVE (해결) 시간 → W-A 장시간 CPU 작업 — nr_running에서 제외됨 W-B 즉시 실행! nr_running = 0 (W-A 제외) → idle worker 깨움 결과: Work B가 즉시 실행됨 CPU-intensive work가 다른 work를 블록하지 않음 process_one_work() 내부 동작 순서 1. work 디큐 worklist에서 꺼냄 2. CPU_INTENSIVE? → nr_running-- (보이지 않음) 3. 콜백 실행 work->func(work) 4. 콜백 완료 → nr_running++ (복원) 단계 2에서 nr_running이 0이 되면 → pool이 idle worker를 깨워 대기 중인 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.cprocess_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 전체 디스크 암호화 등 오래 실행
Compressionzstd/lz4 압축/해제Btrfs 투명 압축, zswap 등
Filesystemext4 lazy init, XFS CIL push초기화/로그 작업이 수백 ms 소요
RAID/dmstripe 계산, mirror 복구대용량 I/O 패턴 처리
언제 WQ_CPU_INTENSIVE를 사용하는가: work 콜백이 수 ms 이상 CPU를 점유할 가능성이 있으면 WQ_CPU_INTENSIVE를 설정하세요. 그렇지 않으면 같은 pool의 짧은 work들이 불필요하게 지연됩니다. 단, 이 플래그는 WQ_UNBOUND와 함께 사용할 때 가장 효과적입니다.

자동 CPU-Intensive 감지 (6.6+)

Linux 6.6부터 커널은 WQ_CPU_INTENSIVE 플래그가 설정되지 않은 workqueue에서도 콜백이 오래 실행되면 자동으로 CPU-intensive 상태로 전환합니다. 이 메커니즘은 cpu_intensive_thresh_us 파라미터로 제어됩니다.

/*
 * 자동 CPU-Intensive 감지 메커니즘 (kernel 6.6+):
 *
 * 동작 원리:
 * 1. worker가 process_one_work()에서 콜백 실행 시작
 * 2. 콜백 실행 시간이 cpu_intensive_thresh_us 초과 시
 * 3. 스케줄러 tick에서 wq_worker_tick() 콜백 호출
 * 4. 해당 worker를 자동으로 CPU-intensive로 전환
 * 5. pool->nr_running에서 제외 → 다른 work 처리 가능
 *
 * 설정:
 * /sys/module/workqueue/parameters/cpu_intensive_thresh_us
 * 기본값: 10000 (10ms)
 *
 * 0으로 설정하면 자동 감지 비활성화
 */

/* kernel/workqueue.c — wq_worker_tick() (스케줄러에서 호출) */
void wq_worker_tick(struct task_struct *task)
{
    struct worker *worker = kthread_data(task);
    struct pool_workqueue *pwq;

    if (!worker->current_func)
        return;  /* 콜백 실행 중이 아닌 경우 */

    pwq = worker->current_pwq;

    /* 이미 CPU-intensive 플래그가 설정된 경우 무시 */
    if (pwq->wq->flags & WQ_CPU_INTENSIVE)
        return;

    /* 실행 시간 확인: 임계값 초과 시 자동 전환 */
    if (worker_set_exec_deadline(worker)) {
        /* WORKER_CPU_INTENSIVE 설정 →
         * nr_running에서 제외 →
         * 다른 pending work를 위한 worker 활성화 */
        worker_set_flags(worker, WORKER_CPU_INTENSIVE);

        /* CONFIG_WQ_CPU_INTENSIVE_REPORT=y 시 경고 출력:
         * "workqueue: XXX [YYY] hogged CPU for >10000us N times,
         *  consider switching to WQ_CPU_INTENSIVE" */
    }
}
# 자동 CPU-intensive 감지 설정 (런타임)

# 현재 임계값 확인 (마이크로초)
cat /sys/module/workqueue/parameters/cpu_intensive_thresh_us
# 10000 (= 10ms 기본값)

# 임계값 변경: 5ms로 줄임 (더 적극적으로 감지)
echo 5000 > /sys/module/workqueue/parameters/cpu_intensive_thresh_us

# 비활성화: 0으로 설정
echo 0 > /sys/module/workqueue/parameters/cpu_intensive_thresh_us

# CONFIG_WQ_CPU_INTENSIVE_REPORT=y 빌드 시
# CPU-intensive로 자동 전환된 work function 확인:
dmesg | grep "hogged CPU"
# workqueue: writeback [wb_workfn] hogged CPU for >10000us 3 times,
#   consider switching to WQ_CPU_INTENSIVE
💡

자동 감지 활용: 개발 중에는 CONFIG_WQ_CPU_INTENSIVE_REPORT=y를 활성화하여 어떤 work 콜백이 자동 전환되는지 확인하세요. 반복적으로 감지되는 함수가 있으면 해당 workqueue에 WQ_CPU_INTENSIVE 플래그를 명시적으로 설정하는 것이 바람직합니다.

Workqueue 메모리 순서 보장

Workqueue는 work 큐잉부터 콜백 실행까지의 메모리 순서(Memory Ordering)를 보장합니다. 이 보장은 커널 개발자가 별도의 메모리 배리어(Memory Barrier) 없이도 안전하게 데이터를 공유할 수 있게 합니다.

Workqueue 메모리 순서 보장: 큐잉자(Producer) → 콜백(Consumer) 큐잉 측 (Producer) — CPU A 1. dev->data = new_value; (일반 store) 2. dev->config = updated; (일반 store) --- 암시적 배리어 --- 3. queue_work(wq, &dev->work); test_and_set_bit() → smp_mb__before_atomic() queue_work()가 반환된 후에는 1, 2의 store가 모든 CPU에서 콜백보다 먼저 관찰됨을 보장 happens-before 콜백 측 (Consumer) — CPU B work_handler(struct work_struct *work) --- 암시적 배리어 --- 4. val = dev->data; (new_value 보장) 5. cfg = dev->config; (updated 보장) 콜백 시작 시점에서 큐잉 이전의 모든 메모리 쓰기가 완료된 상태를 관찰함을 보장 핵심: queue_work() 전 store ≺ 콜백 내 load (happens-before 관계 성립)
보장 1: 큐잉 전 → 콜백 시작 (happens-before)
queue_work() 호출 전의 모든 메모리 쓰기는 콜백 함수 시작 시점에서 관찰 가능합니다. queue_work_on()test_and_set_bit()이 암시적 메모리 배리어를 제공하고, insert_work()wake_up_process()smp_wmb()를 포함하기 때문입니다.
보장 2: 콜백 완료 → flush/cancel 반환 (happens-before)
콜백 함수 내의 모든 메모리 쓰기는 flush_work()/cancel_work_sync() 반환 후 관찰 가능합니다. flush_work()wait_for_completion()smp_rmb()를 포함하기 때문입니다.
보장 3: ordered workqueue 순서
ordered workqueue에서 work A → work B 순서로 실행되면, A의 모든 메모리 쓰기는 B에서 관찰 가능합니다. 같은 worker 스레드에서 직렬 실행되므로 프로그램 순서(program order)가 보장됩니다.
/* 예시: 별도 배리어 없이 안전한 데이터 전달 */
struct data_transfer {
    struct work_struct work;
    char *buffer;       /* 큐잉 전 할당 */
    int length;
    int result;         /* 콜백에서 설정 */
};

/* 큐잉 측: 별도 smp_wmb() 불필요 */
static void submit_data(struct data_transfer *xfer,
                       const char *data, int len)
{
    xfer->buffer = kmemdup(data, len, GFP_KERNEL);
    xfer->length = len;
    /* queue_work()가 암시적 배리어를 제공하므로
     * buffer와 length가 콜백에서 보임을 보장 */
    queue_work(my_wq, &xfer->work);
}

/* 콜백: buffer/length가 최신값임을 보장 */
static void process_data(struct work_struct *work)
{
    struct data_transfer *xfer =
        container_of(work, struct data_transfer, work);

    /* xfer->buffer와 xfer->length는 최신값 보장 */
    xfer->result = do_transfer(xfer->buffer, xfer->length);
    kfree(xfer->buffer);
}

/* flush 측: result가 최신값임을 보장 */
static int wait_for_result(struct data_transfer *xfer)
{
    flush_work(&xfer->work);
    /* flush 반환 후 xfer->result가 최신값 보장 */
    return xfer->result;
}
코드 설명

Workqueue의 메모리 순서 보장에 대한 설명과 활용 예시입니다.

  • 보장 1: 큐잉 전 → 콜백queue_work() 내부의 test_and_set_bit()smp_mb__before_atomic()를 포함하고, insert_work()wake_up_process()가 추가 배리어를 제공합니다. 따라서 큐잉 전에 설정한 데이터는 콜백에서 안전하게 읽을 수 있습니다.
  • 보장 2: 콜백 완료 → flushflush_work()는 내부적으로 wait_for_completion()을 사용하며, 이 함수는 smp_rmb()를 포함합니다. 콜백에서 쓴 결과값은 flush 반환 후 안전하게 읽을 수 있습니다.
  • 보장 3: ordered workqueueordered workqueue는 max_active=1이므로 같은 worker 스레드에서 순차 실행됩니다. 단일 스레드의 프로그램 순서가 메모리 순서를 자동으로 보장합니다.
  • 실용적 의미커널 드라이버에서 "데이터를 설정하고 work를 큐잉하면, 콜백에서 데이터를 안전하게 읽을 수 있습니다"는 것이 핵심입니다. 별도의 smp_wmb()/smp_rmb()를 추가할 필요가 없습니다.
⚠️

보장의 한계: 메모리 순서 보장은 queue_work() → 콜백, 콜백 → flush_work() 경로에만 적용됩니다. 콜백 실행 중에 다른 CPU에서 데이터를 직접 읽으려면 별도의 동기화(lock, atomic, 배리어)가 필요합니다. 또한, cancel_work()(비동기 취소)는 완료 배리어를 제공하지 않으므로 주의하세요.

메모리 압박/회수 경로에서의 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.crescuer_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

단계상황동작
1dirty 페이지 다수 발생writeback work가 큐잉됨
2모든 worker가 I/O 대기 중pool에 idle worker 없음
3새 worker 생성 시도kthread_create() → kmalloc() 실패
4mayday_timer 만료send_mayday() → rescuer 깨움
5rescuer가 pool에 합류writeback work 직접 실행
6dirty 페이지 정리됨메모리 확보 → 정상 worker 생성 가능
WQ_MEM_RECLAIM: Rescuer 스레드 구출 경로 queue_work() worker_pool idle worker 사용 가능 정상 work 실행 완료 worker 생성 실패 OOM: kmalloc 실패 실패 mayday_timer 만료 send_mayday() Rescuer 깨움 wq->rescuer Rescuer가 work 실행 pool에 임시 합류 forward progress 보장 메모리 확보됨 정상 worker 재생성 WQ_MEM_RECLAIM 없으면: worker 생성 실패 시 work가 영원히 대기 → 데드락 블록 I/O, 파일시스템, 메모리 관리 경로에서 반드시 WQ_MEM_RECLAIM 설정
GFP 플래그와 workqueue: 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 시 작업 동결

/* kernel/workqueue.c — 간략화 */

/*
 * 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_FREEZABLE: Suspend/Resume 시 Work 동결 흐름 시간 정상 동작 동결 시작 Suspend (절전) 해동 정상 복귀 Freezable WQ work 정상 실행 max_active=0 동결: 큐잉만 가능, 실행 안 됨 max_active 복원 누적 work 처리 일반 WQ work 정상 실행 suspend 중에도 실행 계속 → 하드웨어 접근 위험! 커널 호출 freeze_workqueues_begin() pm_suspend() thaw_workqueues() 사용 시점: - suspend 중 하드웨어 레지스터 접근이 위험한 드라이버 work (PCIe 디바이스 등) - 사용자 공간 프로세스에 의존하는 작업 (이미 frozen 상태), 파일시스템 동기화 작업

wq_watchdog: worker pool 교착 감지

Workqueue Watchdog (CONFIG_WQ_WATCHDOG):

pool에 큐잉된 work가 지정 시간 이상 처리되지 않으면 경고를 출력합니다. 기본 임계값은 30초(workqueue.watchdog_thresh=30)입니다.

출력 예시:

BUG: workqueue lockup - pool cpus=0 node=0 flags=0x0
     nice=0 stuck for 32s!

임계값 설정 방법:

CPU Hotplug와 Worker 마이그레이션

CPU Hotplug: Worker 마이그레이션 흐름 CPU Offline (echo 0 > online) 1. workqueue_offline_cpu(cpu) 호출 2. bound pool에서 pending work 마이그레이션 3. worker 스레드 unbind CPU affinity 해제 → 다른 CPU에서 실행 가능 4. 진행 중 work: drain (완료 대기) 실행 중인 work는 강제 중단하지 않음 5. unbound pool에서 work 계속 처리 ordered workqueue 보장: 마이그레이션 중에도 실행 순서 유지됨 CPU Online (echo 1 > online) 1. workqueue_online_cpu(cpu) 호출 2. bound pool 재활성화 3. unbound pool cpumask 갱신 새 CPU를 cpumask에 추가 4. worker 스레드 재생성 (필요 시) 기존 idle worker 재사용 우선 5. 새 CPU에서 work 스케줄링 시작 커널 내부 Hotplug 단계: PREPARE → ONLINE → ACTIVE (각 단계별 콜백)
/* kernel/workqueue.c — 간략화 */

/*
 * 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: Bound vs Unbound 전환 power_efficient=0 (서버 기본) CPU 0 BUSY CPU 1 C-state (절전) CPU 2 C-state (절전) schedule_work() CPU 1 깨움! (C-state 탈출) Bound WQ: 현재 CPU의 pool에서 실행 → idle CPU를 깨워야 함 → 전력 소모 증가 지연: 낮음 (즉시 실행) 전력: 높음 (CPU 깨움) power_efficient=1 (모바일/노트북) CPU 0 BUSY CPU 1 C-state 유지! CPU 2 C-state 유지! schedule_work() CPU 0에서 실행 (대기) Unbound로 전환: 스케줄러가 busy CPU 선택 → idle CPU를 깨우지 않음 → 전력 절약 지연: 약간 높음 (busy CPU 대기) 전력: 낮음 (CPU 절전 유지)
/*
 * 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_activerw동시 실행 가능한 최대 work 수256 (또는 WQ_MAX_ACTIVE)
nicerwworker 스레드의 nice 값 (-20~19)0 (highpri: -20)
cpumaskrwunbound worker가 실행 가능한 CPU 마스크전체 CPU
numaroNUMA 인식 여부1 (unbound)
affinity_scoperwaffinity 스코프 (6.5+)cache
per_cpuroper-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 필수 조건: sysfs 인터페이스를 노출하려면 workqueue 생성 시 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_workwork가 큐잉될 때workqueue 이름, work 주소, 요청 CPU, 함수명
workqueue_activate_workwork가 활성화될 때work 주소 (max_active 제한 초과 시 지연 후 활성화)
workqueue_execute_startwork 콜백 시작work 주소, 콜백 함수명
workqueue_execute_endwork 콜백 완료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
kworker CPU 100%의 흔한 원인: (1) work 콜백이 자기 자신을 재큐잉하는 무한 루프, (2) 하드웨어 레지스터(Register) 폴링(Polling)이 완료되지 않는 경우, (3) 잘못된 조건 검사로 인한 busy-wait. /proc/PID/stack과 ftrace를 조합하면 대부분 원인을 특정할 수 있습니다.

커널 설정 총정리

Workqueue 관련 커널 설정은 빌드 시(Kconfig)와 런타임(커널 파라미터/sysfs)으로 나뉩니다. 프로덕션 환경에서의 권장 설정을 포함하여 정리합니다.

빌드 시 설정 (Kconfig)

설정기본값설명권장
CONFIG_WQ_WATCHDOG_THRESH30workqueue watchdog 임계값 (초). 0이면 비활성화프로덕션: 30, 디버깅: 10
CONFIG_WQ_POWER_EFFICIENT_DEFAULTnWQ_POWER_EFFICIENT 워크큐를 기본적으로 unbound로 전환모바일/노트북: y, 서버: n
CONFIG_WQ_VERBOSEnworkqueue 디버그 메시지 활성화개발: y, 프로덕션: n
CONFIG_LOCKDEPnLock dependency 추적 (flush 데드락 감지)개발: y, 프로덕션: n
CONFIG_DEBUG_OBJECTS_WORKnwork_struct 생명주기 추적개발: y
CONFIG_WQ_CPU_INTENSIVE_REPORTnCPU-intensive work 자동 감지 리포트개발: y

런타임 설정 (커널 파라미터)

파라미터설정 방법설명
workqueue.watchdog_thresh커널 cmdline, sysfswatchdog 임계값 (초). 0=비활성화
workqueue.power_efficient커널 cmdline1=WQ_POWER_EFFICIENT를 unbound로 전환
workqueue.disable_numa커널 cmdline1=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배터리 수명 연장
임베디드/IoTpower_efficient=1, 낮은 max_active최소 자원 사용
RT 시스템WQ_BH 대신 일반 WQ, watchdog 활성화결정적 지연, 교착 감지

핵심 함수 구현 분석

Workqueue의 동작을 완전히 이해하려면 세 가지 핵심 함수의 구현을 알아야 합니다: work를 큐에 넣는 __queue_work(), worker가 work를 꺼내 실행하는 process_one_work(), 그리고 worker 스레드의 메인 루프인 worker_thread()입니다. 이 섹션에서는 kernel/workqueue.c의 실제 구현을 기반으로 각 함수의 동작을 분석합니다.

__queue_work() 구현 분석

__queue_work()는 모든 work 큐잉 API(queue_work(), schedule_work() 등)의 최종 진입점입니다. 이 함수가 work를 적절한 worker pool에 삽입하고 worker를 깨우는 과정을 분석합니다.

__queue_work() 실행 흐름 __queue_work(cpu, wq, work) test_and_set_bit(WORK_STRUCT_PENDING) 이미 set → return (중복 방지) WQ_UNBOUND? pool 선택 분기 No (Bound) Per-CPU pool cpu_to_pool(cpu) Yes (Unbound) NUMA-aware pool wq→numa_pwq_tbl[node] pwq = get_pwq(cpu/node, wq) raw_spin_lock(&pool→lock) pwq→nr_active < max_active? Yes insert_work() worklist에 삽입 + wake_up No inactive_works에 대기 max_active 초과 → 보류 raw_spin_unlock(&pool→lock) 전체 경로가 IRQ-safe: 인터럽트 컨텍스트에서도 호출 가능
/*
 * kernel/workqueue.c — __queue_work() 핵심 경로 분석 (v6.x 기준)
 *
 * 호출 체인: queue_work() → queue_work_on() → __queue_work()
 */
static void __queue_work(int cpu, struct workqueue_struct *wq,
                         struct work_struct *work)
{
    struct pool_workqueue *pwq;
    struct worker_pool *last_pool, *pool;
    unsigned int work_flags;
    unsigned int req_cpu = cpu;

    /*
     * 1단계: 이미 PENDING이면 즉시 반환 (중복 큐잉 방지)
     *
     * 호출자인 queue_work_on()에서 이미 test_and_set_bit으로
     * WORK_STRUCT_PENDING을 확인함. 여기서는 WARN으로 이중 체크
     */
    WARN_ON_ONCE(!irqs_disabled());

    /*
     * 2단계: work가 이전에 속했던 pool 확인 (hot path 최적화)
     *
     * work→data에 저장된 이전 pool_id로 last_pool을 조회.
     * 같은 CPU의 같은 pool이면 lock 재획득 없이 직접 삽입 가능.
     * 이는 같은 work가 반복 큐잉되는 패턴에서 성능을 향상시킴.
     */
    last_pool = get_work_pool(work);
    if (last_pool && last_pool != pwq->pool) {
        struct worker *worker;

        raw_spin_lock(&last_pool->lock);
        worker = find_worker_executing_work(last_pool, work);

        if (worker && worker->current_pwq->wq == wq) {
            /* 같은 WQ의 worker가 이 work를 실행 중
             * → 해당 pool에 재큐잉하여 직렬화 보장 */
            pwq = worker->current_pwq;
        }
        raw_spin_unlock(&last_pool->lock);
    }

    /*
     * 3단계: 목표 pool 결정 및 lock 획득
     *
     * Bound WQ: cpu_to_node(cpu)로 해당 CPU의 pool 선택
     * Unbound WQ: wq→numa_pwq_tbl[node]로 NUMA 노드 pool 선택
     */
    if (!(wq->flags & WQ_UNBOUND)) {
        if (req_cpu == WORK_CPU_UNBOUND)
            cpu = raw_smp_processor_id();
        pwq = per_cpu_ptr(wq->cpu_pwq, cpu);
    } else {
        pwq = unbound_pwq_by_node(wq, cpu_to_node(cpu));
    }

    pool = pwq->pool;
    raw_spin_lock(&pool->lock);

    /*
     * 4단계: max_active 체크 후 삽입 또는 보류
     *
     * pwq→nr_active < pwq→max_active 이면 즉시 worklist에 삽입.
     * 초과하면 inactive_works에 보류하여 나중에 activate.
     * ordered workqueue(max_active=1)에서 순서를 보장하는 핵심 메커니즘.
     */
    if (likely(pwq->nr_active < pwq->max_active)) {
        pwq->nr_active++;
        insert_work(pwq, work, &pool->worklist, work_flags);
    } else {
        list_add_tail(&work->entry, &pwq->inactive_works);
    }

    raw_spin_unlock(&pool->lock);
}
코드 설명

kernel/workqueue.c__queue_work() 핵심 경로입니다. 실제 커널 코드는 에러 처리와 디버그 코드가 더 많지만, 핵심 로직은 위와 동일합니다.

  • PENDING 중복 체크queue_work_on()에서 test_and_set_bit(WORK_STRUCT_PENDING_BIT, &work->data)로 이미 확인하고 들어옵니다. PENDING 비트가 이미 set이면 __queue_work()까지 도달하지 않습니다.
  • last_pool 최적화work->data 상위 비트에 저장된 이전 pool ID를 조회합니다. 같은 work가 같은 workqueue에서 반복 큐잉될 때, 현재 그 work를 실행 중인 worker가 있다면 해당 pool에 재큐잉합니다. 이는 같은 work의 콜백이 동시에 여러 CPU에서 실행되는 것을 방지합니다.
  • pool 선택 분기Bound workqueue는 per_cpu_ptr()로 해당 CPU의 pool을 선택합니다. Unbound workqueue는 unbound_pwq_by_node()로 현재 CPU가 속한 NUMA 노드의 pool을 선택하여 메모리 지역성을 활용합니다.
  • insert_work()list_add_tail()로 worklist에 삽입한 뒤, wake_up_process()로 idle worker를 깨웁니다. idle worker가 없으면 manager가 나중에 새 worker를 생성합니다.
  • inactive_works 보류max_active 초과 시 inactive_works 리스트에 추가합니다. 기존 work가 완료되어 nr_active가 감소하면, pwq_activate_first_inactive()가 보류된 work를 활성화합니다.

insert_work() 구현 분석

/*
 * kernel/workqueue.c — insert_work()
 * work를 pool의 worklist에 삽입하고 idle worker를 깨우는 함수
 */
static void insert_work(struct pool_workqueue *pwq,
                        struct work_struct *work,
                        struct list_head *head,
                        unsigned int extra_flags)
{
    /* work→data에 pwq 포인터와 color 정보 기록
     * 이 정보로 나중에 flush/cancel 시 work의 소속을 추적 */
    set_work_pwq(work, pwq, extra_flags);

    /* worklist에 삽입 (tail: FIFO 순서 보장) */
    list_add_tail(&work->entry, head);

    /* 유휴 worker가 있으면 깨워서 work 처리를 시작
     * wake_up_worker()는 idle_list의 첫 번째 worker를 깨움 */
    if (__need_more_worker(pool))
        wake_up_worker(pool);
}

/* __need_more_worker(): worklist가 비어있지 않고 실행 중인 worker가 없으면 true
 * 이 조건은 bound pool의 동시성 관리 핵심:
 * "항상 최소 1개의 worker가 실행 중이어야 합니다" */
static bool __need_more_worker(struct worker_pool *pool)
{
    return !list_empty(&pool->worklist) && !pool->nr_running;
}
코드 설명

kernel/workqueue.cinsert_work()입니다. work를 worklist에 삽입하는 마지막 단계입니다.

  • set_work_pwq()work->data에 pwq 포인터를 인코딩합니다. 하위 비트는 플래그(PENDING, color 등)를 유지하면서 상위 비트에 pwq 주소를 저장합니다. 이 정보는 flush_work(), cancel_work_sync() 등에서 work의 소속 pool을 찾는 데 사용됩니다.
  • list_add_tail()FIFO 순서로 worklist 끝에 추가합니다. worker는 리스트 앞(head)에서 꺼내므로, 먼저 큐잉된 work가 먼저 실행됩니다.
  • __need_more_worker()CMWQ 동시성 관리의 핵심 조건입니다. worklist에 pending work가 있는데 실행 중인 worker(nr_running)가 0이면, idle worker를 깨워야 합니다. bound pool에서만 의미 있으며, nr_running은 스케줄러 콜백(wq_worker_sleeping(), wq_worker_running())에 의해 정확히 추적됩니다.

process_one_work() 구현 분석

process_one_work()는 worker가 worklist에서 work를 하나 꺼내 실행하는 핵심 함수입니다. work 실행 전후의 상태 관리, 동시성 추적, flush 지원을 모두 이 함수에서 처리합니다.

process_one_work() 실행 흐름 1. work dequeue list_first_entry(worklist) 2. busy_hash 등록 hash_add(busy_hash, work) 3. current 설정 current_work = work 4. PENDING 클리어 + pool unlock set_work_pool_and_clear_pending() 5. worker→current_func(work) 콜백 실행 (pool→lock 없이!) mutex, sleep, kmalloc 모두 가능 6. 정리: pool lock 재획득 current_work = NULL, busy_hash 제거 7. pwq_activate_first_inactive() 대기 중 work 활성화 + nr_active 갱신 핵심: pool→lock은 콜백 실행 전에 해제됨 → 콜백에서 블로킹 가능 busy_hash로 실행 중인 work를 추적하여 flush_work()/cancel_work_sync()가 동작
/*
 * kernel/workqueue.c — process_one_work() 핵심 경로
 * worker가 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;
    unsigned long irq_flags;

    /*
     * 1단계: busy_hash에 등록
     *
     * hash key = work 주소
     * flush_work()와 cancel_work_sync()가
     * 실행 중인 work를 찾을 때 busy_hash를 검색함
     */
    hash_add(pool->busy_hash, &worker->hentry,
             (unsigned long)work);

    /* 현재 실행 정보 기록 */
    worker->current_work = work;
    worker->current_func = work->func;
    worker->current_pwq = pwq;

    /*
     * 2단계: PENDING 비트 클리어 + pool→lock 해제
     *
     * 핵심 설계: PENDING을 클리어함으로써
     * "이 work는 이제 실행 중이지 pending이 아니다"를 표시.
     * 이 시점에서 같은 work를 다시 queue_work()하면
     * PENDING이 set되어 콜백 완료 후 재실행됨.
     *
     * pool→lock을 해제해야 콜백에서 블로킹 가능!
     */
    set_work_pool_and_clear_pending(work, pool->id);
    raw_spin_unlock_irq(&pool->lock);

    /*
     * 3단계: lockdep 주석 + 콜백 실행
     *
     * lock_map_acquire: lockdep이 work 콜백을 "잠금"으로 취급
     * → flush_work()가 이 "잠금"을 대기하므로
     * → 데드락 감지가 가능해짐
     */
    lock_map_acquire(&pwq->wq->lockdep_map);
    lock_map_acquire(&lockdep_map);

    /* 실제 콜백 실행 — 이곳이 사용자 코드 진입점 */
    trace_workqueue_execute_start(work);
    worker->current_func(work);
    trace_workqueue_execute_end(work, worker->current_func);

    lock_map_release(&lockdep_map);
    lock_map_release(&pwq->wq->lockdep_map);

    /*
     * 4단계: 정리 — pool→lock 재획득
     *
     * current_work = NULL로 설정하여 "실행 완료" 표시
     * busy_hash에서 제거
     * pwq_dec_nr_in_flight()로 nr_active 감소 + 대기 work 활성화
     */
    raw_spin_lock_irq(&pool->lock);

    hash_del(&worker->hentry);
    worker->current_work = NULL;
    worker->current_func = NULL;
    worker->current_pwq = NULL;

    /* nr_active 감소, inactive_works에서 다음 work 활성화 */
    pwq_dec_nr_in_flight(pwq, work_data);
}
코드 설명

kernel/workqueue.cprocess_one_work() 핵심 경로입니다. worker 스레드가 work item 하나를 처리하는 전체 과정을 보여줍니다.

  • busy_hash 등록pool->busy_hash에 work 주소를 key로 등록합니다. flush_work()cancel_work_sync()는 이 해시를 검색하여 "이 work가 현재 실행 중인가?"를 판단합니다. 해시 충돌은 체이닝으로 처리합니다.
  • PENDING 클리어 시점콜백 실행 전에 WORK_STRUCT_PENDING을 클리어합니다. 이 설계로 인해 콜백 실행 중에 같은 work를 queue_work()하면 PENDING 비트가 새로 set되어, 콜백 완료 후 work가 자동으로 재실행됩니다. 자기 자신을 재큐잉하는 패턴이 안전하게 동작하는 이유입니다.
  • pool→lock 해제콜백 실행 전에 pool->lock을 해제하는 것이 CMWQ의 핵심 설계입니다. lock을 들고 있으면 콜백에서 슬립할 수 없기 때문입니다. lock 없이도 busy_hashcurrent_work로 상태를 추적할 수 있습니다.
  • lockdep 통합lock_map_acquire()/lock_map_release()로 lockdep에 "이 work가 실행 중"임을 알립니다. 이를 통해 lockdep이 "work 콜백 내에서 자기 workqueue를 flush하는" 데드락을 정적으로 감지할 수 있습니다.
  • pwq_dec_nr_in_flight()pwq->nr_active를 감소시킵니다. 감소 후 inactive_works 리스트에 대기 중인 work가 있으면 pwq_activate_first_inactive()를 호출하여 하나를 worklist로 이동합니다. 이것이 max_active 기반 흐름 제어의 완결 경로입니다.

worker_thread() 메인 루프 구현 분석

worker_thread()는 모든 kworker 스레드의 진입점입니다. 이 함수의 무한 루프가 work를 꺼내 실행하고, 유휴 시 슬립하며, 동시성을 관리하는 전체 흐름을 담당합니다.

/*
 * kernel/workqueue.c — worker_thread() 메인 루프
 * 모든 kworker 스레드가 실행하는 함수
 */
static int worker_thread(void *__worker)
{
    struct worker *worker = __worker;
    struct worker_pool *pool = worker->pool;

    /* PF_WQ_WORKER 설정 → 스케줄러가 이 태스크를 특별 취급
     * wq_worker_sleeping()/wq_worker_running() 콜백 활성화 */
    worker->task->flags |= PF_WQ_WORKER;

woke_up:
    raw_spin_lock_irq(&pool->lock);

    /* worker가 소멸 대상이면 정리 후 종료 */
    if (unlikely(worker->flags & WORKER_DIE)) {
        raw_spin_unlock_irq(&pool->lock);
        worker->task->flags &= ~PF_WQ_WORKER;
        return 0;
    }

    /* idle 상태에서 벗어남 */
    worker_leave_idle(worker);

recheck:
    /*
     * 핵심 루프: worklist가 비어있지 않으면 계속 처리
     *
     * need_more_worker()가 false인 경우:
     *   - worklist가 비어있거나
     *   - 다른 worker가 이미 실행 중 (nr_running > 0)
     *   둘 중 하나이면 이 worker는 작업할 필요 없음
     *
     * keep_working()이 true인 동안:
     *   - worklist에 work가 있고
     *   - 충분한 idle worker가 있으면 계속 처리
     */
    if (!need_more_worker(pool))
        goto sleep;

    /* manager 역할 확인: worker 생성이 필요하면 관리 수행 */
    if (unlikely(!may_start_working(pool)) &&
        manage_workers(worker))
        goto recheck;

    /*
     * work 처리 루프
     *
     * worklist에서 work를 하나씩 꺼내 process_one_work() 실행.
     * keep_working() 조건: worklist가 비어있지 않고
     * idle worker가 충분하면 계속 처리.
     * 이는 하나의 worker가 여러 work를 연속 처리하여
     * 컨텍스트 스위칭 오버헤드를 줄이는 최적화.
     */
    do {
        struct work_struct *work =
            list_first_entry(&pool->worklist,
                struct work_struct, entry);

        if (likely(!(*work_data_bits(work) & WORK_STRUCT_LINKED)))
            process_one_work(worker, work);
        else
            process_scheduled_works(worker);
    } while (keep_working(pool));

    /* CPU intensive work 여부 체크 (자동 감지) */
    worker->last_active = jiffies;

sleep:
    /*
     * 슬립 진입: idle 상태로 전환
     *
     * worker_enter_idle()가 idle_list에 추가하고
     * idle_timer를 설정 (300초 후 소멸 검사).
     * schedule()로 슬립하며, 새 work가 큐잉되면
     * insert_work()의 wake_up_worker()로 깨어남.
     */
    worker_enter_idle(worker);
    __set_current_state(TASK_IDLE);
    raw_spin_unlock_irq(&pool->lock);
    schedule();
    goto woke_up;
}
코드 설명

kernel/workqueue.cworker_thread() 전체 루프 구조입니다. 모든 kworker/N:M 스레드가 이 함수를 실행합니다.

  • PF_WQ_WORKERtask->flagsPF_WQ_WORKER를 설정하면, 스케줄러가 이 태스크의 sleep/wakeup 시 wq_worker_sleeping()/wq_worker_running()을 호출합니다. 이 콜백이 pool->nr_running을 정확히 추적하여 CMWQ 동시성 관리가 가능합니다.
  • need_more_worker()worklist가 비어있지 않고 nr_running == 0일 때 true를 반환합니다. 다른 worker가 이미 실행 중이면 이 worker는 다시 idle로 돌아갑니다. 이것이 "항상 최소 1개의 실행 중인 worker를 유지"하는 동시성 관리의 핵심입니다.
  • manage_workers()idle worker가 부족하면(may_start_working() false) manager 역할을 맡아 새 worker를 생성합니다. 한 시점에 하나의 worker만 manager가 될 수 있습니다(POOL_MANAGER_ACTIVE 플래그).
  • keep_working()!list_empty(&pool->worklist) && atomic_read(&pool->nr_running) <= 1: worklist에 work가 있고, 실행 중인 worker가 자신뿐이면 계속 처리합니다. 이렇게 하면 하나의 worker가 여러 work를 배치(batch)로 처리하여 wake/sleep 오버헤드를 줄입니다.
  • TASK_IDLEidle worker는 TASK_IDLE 상태로 슬립합니다. TASK_INTERRUPTIBLE이 아닌 TASK_IDLE을 사용하여 load average에 영향을 주지 않습니다. TASK_IDLE은 커널 6.x에서 추가된 최적화입니다.

wq_worker_sleeping() / wq_worker_running(): 동시성 추적

CMWQ 동시성 관리의 비밀은 스케줄러 콜백에 있습니다. worker가 슬립하거나 깨어날 때 스케줄러가 이 콜백을 호출하여 pool->nr_running을 실시간으로 추적합니다.

/*
 * kernel/workqueue.c — 스케줄러 연동 콜백
 *
 * 이 두 함수가 CMWQ 동시성 관리의 핵심:
 * worker가 슬립(mutex_lock, msleep 등)하면 nr_running이 감소하고,
 * 0이 되면 대기 중인 work를 처리할 worker가 없으므로
 * 즉시 다른 idle worker를 깨움
 */

/* worker가 슬립할 때 스케줄러가 호출 (schedule() 직전) */
void wq_worker_sleeping(struct task_struct *task)
{
    struct worker *worker = kthread_data(task);
    struct worker_pool *pool;

    if (worker->flags & WORKER_NOT_RUNNING)
        return;  /* CPU_INTENSIVE work는 제외 */

    pool = worker->pool;

    /* nr_running 감소: 실행 중인 worker 하나 줄어듦 */
    if (atomic_dec_and_test(&pool->nr_running) &&
        !list_empty(&pool->worklist)) {
        /*
         * nr_running이 0이 되었고 worklist에 work가 있음
         * → 다른 idle worker를 즉시 깨워야 함
         *
         * 이것이 CMWQ의 핵심 동시성 보장:
         * "항상 최소 1개의 running worker가 있어야 합니다"
         */
        wake_up_worker(pool);
    }
}

/* worker가 깨어날 때 스케줄러가 호출 */
void wq_worker_running(struct task_struct *task)
{
    struct worker *worker = kthread_data(task);

    if (!(worker->flags & WORKER_NOT_RUNNING))
        atomic_inc(&worker->pool->nr_running);
}
코드 설명

kernel/workqueue.c의 스케줄러 콜백입니다. kernel/sched/core.c__schedule()에서 PF_WQ_WORKER 태스크를 감지하면 이 콜백을 호출합니다.

  • wq_worker_sleeping()work 콜백 내에서 mutex_lock(), msleep(), wait_event() 등 블로킹 API를 호출하면 schedule()이 실행됩니다. 스케줄러는 이 태스크가 PF_WQ_WORKER임을 확인하고 wq_worker_sleeping()을 호출합니다.
  • atomic_dec_and_test()nr_running을 atomic으로 감소시키고, 0이 되었는지 확인합니다. 0이 되면 이 pool에서 실행 중인 worker가 없는 의미입니다.
  • wake_up_worker()nr_running == 0이고 worklist에 work가 있으면, idle_list의 첫 번째 idle worker를 즉시 깨웁니다. 이렇게 "모든 worker가 슬립하면 즉시 대체 worker를 깨우는" 메커니즘으로, work 처리가 중단되는 것을 방지합니다.
  • WORKER_NOT_RUNNINGWQ_CPU_INTENSIVE workqueue의 work를 실행 중인 worker는 WORKER_NOT_RUNNING 플래그가 설정됩니다. 이 worker는 동시성 관리 대상에서 제외되어, 오래 실행되어도 nr_running 감소로 인한 추가 worker 깨움이 발생하지 않습니다.

try_to_grab_pending(): cancel 경로 구현

/*
 * kernel/workqueue.c — try_to_grab_pending()
 * cancel_work_sync()의 핵심 헬퍼
 *
 * work가 pending(큐에 대기 중)이면 직접 꺼내오고,
 * running(실행 중)이면 완료 대기를 위한 준비를 수행
 */
static int try_to_grab_pending(struct work_struct *work,
                               bool is_dwork,
                               unsigned long *irq_flags)
{
    struct worker_pool *pool;

    local_irq_save(*irq_flags);

    /* Fast path: PENDING 비트를 직접 클리어 시도
     * 성공하면 work가 아직 worklist에만 있었던 것
     * → 리스트에서 제거하면 취소 완료 */
    if (!test_and_set_bit(WORK_STRUCT_PENDING_BIT,
                          work_data_bits(work))) {
        /* work가 idle 상태였음 → grab 성공 (사실상 no-op) */
        return 0;
    }

    /* work가 pending 또는 running — pool을 찾아야 함 */
    pool = get_work_pool(work);
    if (!pool)
        goto fail;

    raw_spin_lock(&pool->lock);

    /* worklist에 있으면 (pending) 직접 제거 */
    if (*work_data_bits(work) & WORK_STRUCT_PENDING) {
        list_del_init(&work->entry);
        /* pwq→nr_active 감소 + inactive work 활성화 */
        pwq_dec_nr_in_flight(get_work_pwq(work),
                            *work_data_bits(work));

        /* PENDING 비트는 유지 — 호출자가 클리어
         * grab 성공: 1 반환 */
        raw_spin_unlock(&pool->lock);
        return 1;
    }

    /* running 상태 — 완료 대기가 필요
     * -EAGAIN 반환 → 호출자(cancel_work_sync)가
     * flush_work()로 완료 대기 */
    raw_spin_unlock(&pool->lock);
fail:
    local_irq_restore(*irq_flags);
    return -EAGAIN;
}
코드 설명

kernel/workqueue.ctry_to_grab_pending()입니다. cancel_work_sync()cancel_delayed_work_sync()의 핵심 구현입니다.

  • Fast pathtest_and_set_bit(PENDING)이 성공하면, work가 idle 상태(어떤 큐에도 없음)였다는 의미입니다. PENDING을 set하여 다른 큐잉을 방지하고, 바로 반환합니다. 이 경로는 lock 없이 동작하므로 매우 빠릅니다.
  • Pending 제거work가 worklist에 대기 중이면 list_del_init()로 직접 제거합니다. pwq_dec_nr_in_flight()nr_active를 갱신하여, 보류 중인 다른 work가 활성화될 수 있게 합니다.
  • Running 처리work가 현재 실행 중이면 직접 취소할 수 없습니다. -EAGAIN을 반환하면 호출자인 __cancel_work_timer()flush_work()를 호출하여 콜백 완료를 대기합니다. 이것이 cancel_work_sync()에서 "sync" 부분의 구현입니다.

실전 구현 예시

다양한 시나리오에서 workqueue를 활용하는 실전 패턴을 분석합니다. 각 예시는 실제 커널 드라이버에서 사용되는 검증된 패턴을 기반으로 합니다.

예시 1: 네트워크 드라이버 — 링크 상태 변화 처리

네트워크 드라이버에서 PHY 링크 상태 변화는 인터럽트로 감지하지만, 실제 처리(미디어 타입 변경, 속도 협상, 상위 스택 알림)는 프로세스 컨텍스트가 필요합니다. Workqueue는 이 패턴에 적합합니다.

#include <linux/netdevice.h>
#include <linux/workqueue.h>
#include <linux/phy.h>

struct my_netdev_priv {
    struct net_device *ndev;
    struct work_struct link_work;       /* 링크 변화 처리 */
    struct delayed_work stats_work;     /* 주기적 통계 수집 */
    struct workqueue_struct *wq;
    spinlock_t hw_lock;                  /* 하드웨어 레지스터 보호 */
    void __iomem *regs;
    bool running;
    unsigned int link_status;
};

/* 링크 상태 변화 work 콜백 — 프로세스 컨텍스트 */
static void my_link_work_handler(struct work_struct *work)
{
    struct my_netdev_priv *priv =
        container_of(work, struct my_netdev_priv, link_work);
    struct net_device *ndev = priv->ndev;
    unsigned int status;

    /* 프로세스 컨텍스트이므로 mutex 획득, I2C 접근 가능 */
    rtnl_lock();

    /* PHY 상태 읽기 (MDIO 버스 접근 — 슬립 가능) */
    status = phy_read_status(ndev->phydev);

    if (priv->link_status != status) {
        priv->link_status = status;

        if (status & BMSR_LSTATUS) {
            /* 링크 업: MAC 설정 변경 (속도/듀플렉스) */
            my_configure_mac(priv, ndev->phydev->speed,
                            ndev->phydev->duplex);
            netif_carrier_on(ndev);
            netdev_info(ndev, "link up %dMbps %s-duplex\n",
                       ndev->phydev->speed,
                       ndev->phydev->duplex ? "full" : "half");
        } else {
            netif_carrier_off(ndev);
            netdev_info(ndev, "link down\n");
        }
    }

    rtnl_unlock();
}

/* 주기적 통계 수집 — delayed work 재큐잉 패턴 */
static void my_stats_work_handler(struct work_struct *work)
{
    struct delayed_work *dwork = to_delayed_work(work);
    struct my_netdev_priv *priv =
        container_of(dwork, struct my_netdev_priv, stats_work);
    unsigned long flags;

    /* 하드웨어 통계 카운터 읽기 */
    spin_lock_irqsave(&priv->hw_lock, flags);
    priv->ndev->stats.rx_packets += readl(priv->regs + RX_PKT_CNT);
    priv->ndev->stats.tx_packets += readl(priv->regs + TX_PKT_CNT);
    priv->ndev->stats.rx_errors  += readl(priv->regs + RX_ERR_CNT);
    /* 하드웨어 카운터는 읽으면 자동 클리어 (Read-to-Clear) */
    spin_unlock_irqrestore(&priv->hw_lock, flags);

    /* 동작 중이면 재스케줄 */
    if (priv->running)
        queue_delayed_work(priv->wq, dwork,
            msecs_to_jiffies(1000));
}

/* PHY 인터럽트 핸들러 (top half) */
static irqreturn_t my_phy_irq(int irq, void *data)
{
    struct my_netdev_priv *priv = data;

    /* 인터럽트 ACK (atomic 컨텍스트 — MMIO만 가능) */
    writel(PHY_IRQ_CLEAR, priv->regs + PHY_IRQ_STATUS);

    /* 프로세스 컨텍스트로 지연 처리 */
    queue_work(priv->wq, &priv->link_work);
    return IRQ_HANDLED;
}

/* ndo_open: 인터페이스 활성화 시 work 시작 */
static int my_ndo_open(struct net_device *ndev)
{
    struct my_netdev_priv *priv = netdev_priv(ndev);

    priv->running = true;

    /* 통계 수집 시작: 1초 후 첫 실행 */
    queue_delayed_work(priv->wq, &priv->stats_work,
        msecs_to_jiffies(1000));

    netif_start_queue(ndev);
    return 0;
}

/* ndo_stop: 인터페이스 비활성화 시 정리 */
static int my_ndo_stop(struct net_device *ndev)
{
    struct my_netdev_priv *priv = netdev_priv(ndev);

    netif_stop_queue(ndev);

    /* 재큐잉 중단 플래그 */
    priv->running = false;

    /* 실행 중인 work 완료 대기 */
    cancel_work_sync(&priv->link_work);
    cancel_delayed_work_sync(&priv->stats_work);

    return 0;
}

/* 프로브: 초기화 */
static int my_probe(struct pci_dev *pdev,
                   const struct pci_device_id *id)
{
    struct net_device *ndev;
    struct my_netdev_priv *priv;

    ndev = alloc_etherdev(sizeof(*priv));
    priv = netdev_priv(ndev);
    priv->ndev = ndev;

    /* WQ_MEM_RECLAIM: 네트워크 I/O가 메모리 회수에 필요할 수 있음
     * max_active=0 → 기본값 256 */
    priv->wq = alloc_workqueue("my_net_%s",
        WQ_MEM_RECLAIM, 0, pci_name(pdev));
    if (!priv->wq) {
        free_netdev(ndev);
        return -ENOMEM;
    }

    INIT_WORK(&priv->link_work, my_link_work_handler);
    INIT_DELAYED_WORK(&priv->stats_work, my_stats_work_handler);
    spin_lock_init(&priv->hw_lock);

    /* ... PCI 리소스, PHY 설정, register_netdev 등 ... */
    return 0;
}
코드 설명

네트워크 드라이버에서 workqueue를 활용하는 전형적인 패턴입니다. 인터럽트(Interrupt) 핸들러(top half)에서 상태 변화를 감지하고, workqueue(bottom half)에서 실제 처리를 수행합니다.

  • link_workPHY 링크 변화 인터럽트 후 rtnl_lock()을 잡고 MDIO 버스 접근이 필요한 처리를 수행합니다. rtnl_lock()은 mutex이므로 인터럽트 컨텍스트에서는 사용 불가합니다. workqueue에서 실행하므로 안전합니다.
  • stats_workdelayed_work로 1초 간격 주기적 실행을 구현합니다. 콜백 끝에서 running 플래그를 확인하고 자기 자신을 재큐잉합니다. running = false + cancel_delayed_work_sync()로 안전하게 중단합니다.
  • WQ_MEM_RECLAIMNFS/iSCSI 등 네트워크 기반 파일시스템에서 메모리 회수 시 네트워크 I/O가 필요합니다. WQ_MEM_RECLAIM으로 rescuer 스레드를 보장하여 메모리 부족 시에도 forward progress를 유지합니다.
  • ndo_stop 정리 순서running = false를 먼저 설정하여 재큐잉을 방지한 후, cancel_*_sync()로 실행 중인 work 완료를 대기합니다. 이 순서가 바뀌면 cancel 후 재큐잉이 발생할 수 있습니다.

예시 2: 비동기 프로브(Async Probe) 패턴

디바이스 프로브(probe)에서 펌웨어(Firmware) 로딩, I2C 센서 캘리브레이션 등 시간이 오래 걸리는 작업이 있을 때, workqueue를 활용한 비동기 프로브로 부팅 시간을 단축할 수 있습니다.

#include <linux/platform_device.h>
#include <linux/firmware.h>
#include <linux/workqueue.h>
#include <linux/completion.h>

struct my_sensor {
    struct device *dev;
    struct work_struct fw_load_work;
    struct completion fw_ready;
    const struct firmware *fw;
    void __iomem *regs;
    bool fw_loaded;
    int fw_error;
};

/* 비동기 펌웨어 로딩 work */
static void my_fw_load_work(struct work_struct *work)
{
    struct my_sensor *sensor =
        container_of(work, struct my_sensor, fw_load_work);
    int ret;

    /* request_firmware()는 슬립 가능 — 프로세스 컨텍스트 필수
     * 파일시스템 접근, 유저스페이스 헬퍼 호출 가능 */
    ret = request_firmware(&sensor->fw,
                          "my_sensor_v2.bin", sensor->dev);
    if (ret) {
        dev_err(sensor->dev,
               "firmware load failed: %d\n", ret);
        sensor->fw_error = ret;
        complete(&sensor->fw_ready);
        return;
    }

    /* 펌웨어를 하드웨어에 업로드 */
    my_upload_firmware(sensor, sensor->fw);
    release_firmware(sensor->fw);

    sensor->fw_loaded = true;
    dev_info(sensor->dev, "firmware loaded successfully\n");

    /* 대기 중인 코드(open 등)에 완료 알림 */
    complete(&sensor->fw_ready);
}

/* 프로브: 빠르게 반환하고 펌웨어 로딩은 비동기로 */
static int my_sensor_probe(struct platform_device *pdev)
{
    struct my_sensor *sensor;

    sensor = devm_kzalloc(&pdev->dev, sizeof(*sensor), GFP_KERNEL);
    if (!sensor)
        return -ENOMEM;

    sensor->dev = &pdev->dev;

    /* 하드웨어 리소스 매핑 */
    sensor->regs = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(sensor->regs))
        return PTR_ERR(sensor->regs);

    init_completion(&sensor->fw_ready);
    INIT_WORK(&sensor->fw_load_work, my_fw_load_work);

    /* system_unbound_wq: CPU에 고정되지 않은 long-running 작업에 적합
     * 펌웨어 로딩은 수 초 소요될 수 있으므로 unbound가 적절 */
    queue_work(system_unbound_wq, &sensor->fw_load_work);

    platform_set_drvdata(pdev, sensor);

    /* 프로브 즉시 반환 → 다른 디바이스 프로브 진행 가능 */
    return 0;
}

/* 파일 오퍼레이션: 펌웨어 로딩 완료 후에만 사용 가능 */
static int my_sensor_open(struct inode *inode, struct file *file)
{
    struct my_sensor *sensor = /* ... */;

    /* 펌웨어 로딩 완료 대기 (최대 10초) */
    if (!wait_for_completion_timeout(&sensor->fw_ready,
            msecs_to_jiffies(10000))) {
        return -ETIMEDOUT;
    }

    if (sensor->fw_error)
        return sensor->fw_error;

    return 0;
}

static void my_sensor_remove(struct platform_device *pdev)
{
    struct my_sensor *sensor = platform_get_drvdata(pdev);

    /* 펌웨어 로딩이 진행 중이면 완료 대기 */
    cancel_work_sync(&sensor->fw_load_work);
}
코드 설명

비동기 프로브 패턴으로, 프로브 시점의 펌웨어 로딩을 workqueue로 지연하여 부팅 시간을 단축합니다.

  • request_firmware()파일시스템에서 펌웨어를 읽는 API로, 슬립이 필요합니다. 프로브 함수에서 직접 호출하면 부팅이 지연되므로, workqueue에서 비동기로 처리합니다.
  • completionstruct completion으로 펌웨어 로딩 완료를 알립니다. open() 호출 시 wait_for_completion_timeout()으로 대기하여, 펌웨어가 준비되기 전에 디바이스에 접근하는 것을 방지합니다.
  • system_unbound_wq펌웨어 로딩은 수 초 소요될 수 있는 long-running 작업이므로, bound pool의 동시성 관리에 간섭하지 않도록 system_unbound_wq를 사용합니다. 전용 workqueue를 만들 필요 없이 시스템 제공 unbound 큐를 활용합니다.
  • remove 안전성cancel_work_sync()가 펌웨어 로딩이 진행 중이면 완료를 대기합니다. devm_kzalloc()으로 할당한 메모리는 디바이스 제거 시 자동 해제되지만, work가 먼저 완료되어야 안전합니다.

예시 3: Work 배칭(Batching) 패턴

짧은 간격으로 반복되는 이벤트를 매번 처리하면 오버헤드가 큽니다. delayed_work를 활용하여 여러 이벤트를 하나의 work 실행으로 묶는(batch) 패턴을 구현할 수 있습니다.

#include <linux/workqueue.h>
#include <linux/atomic.h>

struct event_batcher {
    struct delayed_work batch_work;
    atomic_t pending_count;        /* 누적된 이벤트 수 */
    unsigned long batch_events[64]; /* 이벤트 비트맵 */
    spinlock_t lock;
    struct device *dev;
};

/* 배치 처리 콜백 — 누적된 이벤트를 한 번에 처리 */
static void batch_work_handler(struct work_struct *work)
{
    struct delayed_work *dwork = to_delayed_work(work);
    struct event_batcher *batcher =
        container_of(dwork, struct event_batcher, batch_work);
    unsigned long events[64];
    int count;

    /* 이벤트 스냅샷 획득 후 클리어 */
    spin_lock_bh(&batcher->lock);
    memcpy(events, batcher->batch_events, sizeof(events));
    memset(batcher->batch_events, 0, sizeof(batcher->batch_events));
    count = atomic_xchg(&batcher->pending_count, 0);
    spin_unlock_bh(&batcher->lock);

    if (count == 0)
        return;

    dev_dbg(batcher->dev,
           "processing batch: %d events\n", count);

    /* 비트맵에서 설정된 이벤트를 순회하며 처리 */
    for (int i = 0; i < 64; i++) {
        if (events[i])
            process_events_for_channel(batcher, i, events[i]);
    }
}

/* 이벤트 발생 시 호출 (인터럽트/atomic 컨텍스트 가능) */
static void batcher_notify_event(struct event_batcher *batcher,
                                int channel, int event_bit)
{
    unsigned long flags;

    /* 이벤트 기록: 비트맵에 누적 */
    spin_lock_irqsave(&batcher->lock, flags);
    set_bit(event_bit, &batcher->batch_events[channel]);
    spin_unlock_irqrestore(&batcher->lock, flags);

    atomic_inc(&batcher->pending_count);

    /* 10ms 디바운스(debounce): 이미 예약된 work가 있으면 무시
     * → 마지막 이벤트로부터 10ms 내 모든 이벤트가 배치됨
     *
     * mod_delayed_work()는 이미 예약된 타이머를 재설정하므로
     * "마지막 이벤트 + 10ms" 시점에 실행됨 */
    mod_delayed_work(system_wq, &batcher->batch_work,
                     msecs_to_jiffies(10));
}

/* 초기화 */
static void batcher_init(struct event_batcher *batcher,
                        struct device *dev)
{
    INIT_DELAYED_WORK(&batcher->batch_work, batch_work_handler);
    atomic_set(&batcher->pending_count, 0);
    spin_lock_init(&batcher->lock);
    batcher->dev = dev;
}

/* 정리 */
static void batcher_destroy(struct event_batcher *batcher)
{
    cancel_delayed_work_sync(&batcher->batch_work);
}
코드 설명

짧은 간격의 반복 이벤트를 효율적으로 처리하는 배칭(Batching) 패턴입니다. 네트워크 드라이버의 NAPI, 블록 디바이스의 I/O 병합과 유사한 개념입니다.

  • 비트맵 누적이벤트를 비트맵에 누적하여 "어떤 채널의 어떤 이벤트가 발생했는가"를 기록합니다. 같은 이벤트가 여러 번 발생해도 비트맵에는 한 번만 기록되므로 중복 처리를 자연스럽게 제거합니다.
  • mod_delayed_work()queue_delayed_work()와 달리, mod_delayed_work()는 이미 예약된 타이머를 새 시간으로 재설정합니다. 이벤트가 빠르게 발생하면 타이머가 계속 뒤로 밀리다가, 이벤트 폭풍(storm)이 멈추면 10ms 후에 한꺼번에 처리합니다. 이것이 디바운스(debounce) 패턴입니다.
  • 스냅샷 + 클리어배치 처리 시 lock을 잡고 이벤트 비트맵을 복사한 뒤 즉시 클리어합니다. 이렇게 하면 배치 처리 중에 발생하는 새 이벤트는 다음 배치에서 처리됩니다. lock 보유 시간을 최소화하는 일반적인 패턴입니다.

예시 4: Ordered Workqueue — 상태 머신 구현

순서가 중요한 작업(트랜잭션 로그 기록, 상태 전이 등)은 alloc_ordered_workqueue()로 생성한 ordered workqueue를 사용합니다. max_active=1로 동시 실행을 방지하여 별도의 동기화 없이 순서를 보장합니다.

#include <linux/workqueue.h>

/* 상태 머신 예: 디바이스 전원 상태 전이 */
enum power_state {
    POWER_OFF,
    POWER_STANDBY,
    POWER_ON,
    POWER_ERROR,
};

struct power_manager {
    struct workqueue_struct *state_wq;  /* ordered: 순차 실행 */
    enum power_state current_state;
    struct device *dev;
    void __iomem *regs;
};

/* 상태 전이 요청을 캡슐화하는 구조체 */
struct state_transition {
    struct work_struct work;
    struct power_manager *pm;
    enum power_state target;
    void (*callback)(int result, void *ctx);
    void *callback_ctx;
};

/* 상태 전이 work 콜백
 * ordered WQ에서 실행 → 동시에 하나만 실행됨
 * → current_state 접근에 별도 lock 불필요 */
static void state_transition_work(struct work_struct *work)
{
    struct state_transition *trans =
        container_of(work, struct state_transition, work);
    struct power_manager *pm = trans->pm;
    int ret = 0;

    dev_dbg(pm->dev, "state transition: %d → %d\n",
           pm->current_state, trans->target);

    /* 전이 유효성 검증 */
    if (!is_valid_transition(pm->current_state, trans->target)) {
        dev_warn(pm->dev, "invalid transition: %d → %d\n",
                pm->current_state, trans->target);
        ret = -EINVAL;
        goto out;
    }

    /* 하드웨어 상태 전이 수행 — 슬립 가능 */
    switch (trans->target) {
    case POWER_ON:
        ret = hw_power_on_sequence(pm);  /* msleep, regulator 등 */
        break;
    case POWER_STANDBY:
        ret = hw_enter_standby(pm);
        break;
    case POWER_OFF:
        hw_power_off(pm);
        break;
    default:
        ret = -EINVAL;
    }

    if (ret == 0)
        pm->current_state = trans->target;
    else
        pm->current_state = POWER_ERROR;

out:
    /* 완료 콜백 호출 (선택적) */
    if (trans->callback)
        trans->callback(ret, trans->callback_ctx);

    /* 동적 할당된 전이 요청 해제 */
    kfree(trans);
}

/* 비동기 상태 전이 요청 — IRQ 컨텍스트에서도 호출 가능 */
static int power_request_transition(struct power_manager *pm,
                                   enum power_state target,
                                   void (*cb)(int, void *),
                                   void *ctx)
{
    struct state_transition *trans;

    /* GFP_ATOMIC: 인터럽트 컨텍스트 가능 */
    trans = kmalloc(sizeof(*trans), GFP_ATOMIC);
    if (!trans)
        return -ENOMEM;

    trans->pm = pm;
    trans->target = target;
    trans->callback = cb;
    trans->callback_ctx = ctx;
    INIT_WORK(&trans->work, state_transition_work);

    /* ordered WQ: 큐잉 순서대로 하나씩 실행됨 */
    queue_work(pm->state_wq, &trans->work);
    return 0;
}

/* 초기화 */
static int power_manager_init(struct power_manager *pm,
                             struct device *dev)
{
    pm->dev = dev;
    pm->current_state = POWER_OFF;

    /* alloc_ordered_workqueue: max_active=1
     * → 상태 전이가 큐잉 순서대로 직렬화됨
     * WQ_MEM_RECLAIM: 메모리 부족 시에도 전이 보장 */
    pm->state_wq = alloc_ordered_workqueue("pm_state_%s",
        WQ_MEM_RECLAIM, dev_name(dev));
    if (!pm->state_wq)
        return -ENOMEM;

    return 0;
}

/* 해제 */
static void power_manager_destroy(struct power_manager *pm)
{
    /* drain: 큐에 남아있는 모든 전이 완료 후 파괴 */
    destroy_workqueue(pm->state_wq);
}
코드 설명

Ordered workqueue를 활용한 상태 머신 구현입니다. 전원 관리, 프로토콜 핸들러 등 순서가 중요한 비동기 작업에 적합합니다.

  • alloc_ordered_workqueue()max_active=1을 보장하여 한 시점에 하나의 work만 실행됩니다. 상태 전이 요청이 큐잉 순서대로 직렬화되므로, current_state 접근에 별도 mutex가 필요 없습니다.
  • 동적 work 할당각 전이 요청을 kmalloc()으로 독립적인 state_transition 구조체에 할당합니다. 정적 work_struct와 달리 여러 전이를 동시에 큐잉할 수 있습니다. 콜백 완료 후 kfree()로 해제합니다.
  • GFP_ATOMIC인터럽트 컨텍스트에서 전이를 요청할 수 있도록 GFP_ATOMIC으로 할당합니다. 예: 하드웨어 에러 인터럽트에서 POWER_ERROR 전이 요청.
  • destroy_workqueue()destroy_workqueue()는 내부적으로 drain_workqueue()를 호출하여 큐에 남아있는 모든 work를 완료한 후 워크큐를 파괴합니다. 미처리된 전이 요청이 유실되지 않습니다.

예시 5: devm 자동 정리 패턴

devm(Device-managed Resource) API와 workqueue를 함께 사용하면, 디바이스 제거 시 자동으로 workqueue를 정리할 수 있습니다. 수동 cleanup 코드의 실수를 줄이는 방어적 패턴입니다.

#include <linux/device.h>
#include <linux/workqueue.h>

/* devm으로 work_struct 자동 cancel을 위한 래퍼 */
struct devm_work_ctx {
    struct work_struct *work;
    struct delayed_work *dwork;
    struct workqueue_struct *wq;
};

/* devm 해제 액션: 디바이스 제거 시 자동 호출 */
static void devm_work_release(void *data)
{
    struct devm_work_ctx *ctx = data;

    /* work 정리 */
    if (ctx->dwork)
        cancel_delayed_work_sync(ctx->dwork);
    if (ctx->work)
        cancel_work_sync(ctx->work);
}

static void devm_wq_release(void *data)
{
    struct devm_work_ctx *ctx = data;

    if (ctx->wq)
        destroy_workqueue(ctx->wq);
}

/* devm work 등록 헬퍼: 디바이스 제거 시 자동 cancel */
static int devm_work_register(struct device *dev,
                             struct work_struct *work,
                             work_func_t func)
{
    struct devm_work_ctx *ctx;

    ctx = devm_kzalloc(dev, sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return -ENOMEM;

    INIT_WORK(work, func);
    ctx->work = work;

    return devm_add_action_or_reset(dev, devm_work_release, ctx);
}

/* 사용 예시: 프로브에서 work 등록, 별도 cleanup 불필요 */
static int my_simple_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    /* devm으로 work 등록 → remove에서 cancel 불필요 */
    ret = devm_work_register(&pdev->dev, &dev->event_work,
                            my_event_handler);
    if (ret)
        return ret;

    /* ... 나머지 초기화 ... */

    return 0;

    /* remove 함수에서 cancel_work_sync() 호출 불필요!
     * devm이 디바이스 제거 시 자동으로 처리
     *
     * 주의: devm action 순서는 등록 역순이므로,
     * work가 접근하는 리소스보다 work 해제가 먼저 실행됨 */
}
코드 설명

devm_add_action_or_reset()을 활용하여 workqueue 정리를 자동화하는 패턴입니다.

  • devm_add_action_or_reset()디바이스가 제거될 때 자동으로 호출될 콜백을 등록합니다. 등록 실패 시 즉시 해제 액션을 실행(_or_reset)하여 리소스 누수를 방지합니다.
  • 해제 순서devm 해제 순서는 등록 역순(LIFO)입니다. work를 먼저 등록하고 그 후에 work가 접근하는 리소스(예: I/O 영역)를 등록하면, 리소스가 먼저 해제되고 work가 나중에 cancel됩니다. 따라서 work 등록을 나중에 하여 cancel이 먼저 실행되게 합니다.
  • remove 불필요모든 리소스가 devm으로 관리되면 remove() 함수를 비워두거나 생략할 수 있습니다. 에러 경로에서 goto cleanup 패턴도 불필요합니다.

예시 6: RCU + Workqueue 조합 패턴

RCU(Read-Copy-Update) grace period 대기는 슬립이 필요하므로 인터럽트 컨텍스트에서 직접 수행할 수 없습니다. call_rcu()와 workqueue를 조합하면 RCU 콜백에서 블로킹 작업을 안전하게 처리할 수 있습니다.

#include <linux/rcupdate.h>
#include <linux/workqueue.h>
#include <linux/slab.h>

struct my_entry {
    struct rcu_head rcu;
    struct work_struct free_work;
    struct hlist_node node;
    void *large_buffer;       /* vfree 필요 (슬립 가능) */
    struct file *backing_file; /* fput 필요 */
    char key[64];
};

/* work 콜백: 프로세스 컨텍스트에서 안전하게 해제
 * RCU grace period 이후에 실행됨이 보장됨 */
static void my_entry_free_work(struct work_struct *work)
{
    struct my_entry *entry =
        container_of(work, struct my_entry, free_work);

    /* 슬립 가능한 해제 작업 */
    vfree(entry->large_buffer);  /* vmalloc'd memory → 슬립 가능 */
    fput(entry->backing_file);   /* 파일 참조 해제 → 슬립 가능 */
    kfree(entry);
}

/* RCU 콜백: softirq 컨텍스트에서 호출됨 → 슬립 불가
 * work를 스케줄하여 프로세스 컨텍스트로 지연 */
static void my_entry_rcu_free(struct rcu_head *rcu)
{
    struct my_entry *entry =
        container_of(rcu, struct my_entry, rcu);

    /* RCU 콜백은 softirq → schedule_work로 지연 */
    INIT_WORK(&entry->free_work, my_entry_free_work);
    schedule_work(&entry->free_work);
}

/* 엔트리 삭제 (write-side) */
static void my_remove_entry(struct my_entry *entry)
{
    /* RCU 보호 하에 리스트에서 제거 */
    hlist_del_rcu(&entry->node);

    /* grace period 후 RCU 콜백 호출
     * → RCU 콜백에서 work 스케줄
     * → work에서 실제 메모리 해제 */
    call_rcu(&entry->rcu, my_entry_rcu_free);
}

/* 읽기 경로 (read-side, lock-free) */
static struct my_entry *my_lookup(const char *key)
{
    struct my_entry *entry;

    rcu_read_lock();
    hlist_for_each_entry_rcu(entry, &my_hash_table[hash], node) {
        if (strcmp(entry->key, key) == 0) {
            rcu_read_unlock();
            return entry;
        }
    }
    rcu_read_unlock();
    return NULL;
}
코드 설명

RCU 콜백(softirq 컨텍스트)에서 슬립이 필요한 해제 작업을 workqueue로 지연하는 패턴입니다.

  • call_rcu() → work → free3단계 해제 체인입니다: (1) call_rcu()로 grace period 대기, (2) RCU 콜백에서 schedule_work()로 work 예약, (3) work 콜백에서 vfree()/fput() 실행. vfree()는 대량의 페이지를 해제할 때 TLB flush를 위해 슬립할 수 있으므로 softirq에서 직접 호출하면 안 됩니다.
  • INIT_WORK 시점INIT_WORK()를 RCU 콜백 내에서 호출합니다. rcufree_work가 같은 메모리를 공유하지 않으므로(구조체에 별도 필드) 안전합니다. 만약 rcu_headwork_struct를 union으로 공유하려면 INIT_WORK()를 RCU 콜백에서만 호출해야 합니다.
  • schedule_work()RCU 콜백은 softirq 컨텍스트이므로 queue_work()/schedule_work() 호출이 가능합니다(IRQ-safe). 전용 workqueue가 필요하지 않으므로 system_wq를 사용합니다.

예시 7: 서브시스템 워크큐 — 블록 I/O flush 패턴

블록 디바이스 드라이버에서 디스크(Disk) 캐시 flush 작업은 잠재적으로 수 초가 소요됩니다. WQ_MEM_RECLAIM | WQ_UNBOUND 조합으로 메모리 압박 환경에서도 안전하게 동작하는 패턴을 구현합니다.

#include <linux/blkdev.h>
#include <linux/workqueue.h>

struct my_blk_device {
    struct gendisk *disk;
    struct workqueue_struct *flush_wq;
    struct work_struct flush_work;
    struct work_struct error_work;
    spinlock_t queue_lock;
    struct list_head pending_ios;
    atomic_t error_count;
    void __iomem *regs;
    bool removing;
};

/* 캐시 flush work — 수 초 소요 가능 */
static void my_flush_handler(struct work_struct *work)
{
    struct my_blk_device *bdev =
        container_of(work, struct my_blk_device, flush_work);
    struct list_head batch;
    unsigned long flags;

    INIT_LIST_HEAD(&batch);

    /* pending I/O를 로컬 리스트로 이동 (lock 최소화) */
    spin_lock_irqsave(&bdev->queue_lock, flags);
    list_splice_init(&bdev->pending_ios, &batch);
    spin_unlock_irqrestore(&bdev->queue_lock, flags);

    /* 하드웨어 flush 명령 (블로킹 — DMA 완료 대기) */
    writel(FLUSH_CMD, bdev->regs + CMD_REG);
    if (my_wait_for_completion(bdev, msecs_to_jiffies(5000))) {
        /* flush 완료 — pending I/O 완료 처리 */
        complete_pending_ios(&batch, 0);
    } else {
        /* 타임아웃 — 에러로 완료 */
        complete_pending_ios(&batch, -EIO);
        atomic_inc(&bdev->error_count);
        queue_work(bdev->flush_wq, &bdev->error_work);
    }
}

/* 에러 복구 work — 디바이스 리셋 + 재초기화 */
static void my_error_handler(struct work_struct *work)
{
    struct my_blk_device *bdev =
        container_of(work, struct my_blk_device, error_work);

    dev_err(disk_to_dev(bdev->disk),
           "device error, initiating reset (%d errors)\n",
           atomic_read(&bdev->error_count));

    /* 하드웨어 리셋 시퀀스 (msleep 포함) */
    my_hw_reset(bdev);
    my_hw_reinit(bdev);

    atomic_set(&bdev->error_count, 0);
}

/* 초기화 */
static int my_blk_init(struct my_blk_device *bdev)
{
    /* WQ_MEM_RECLAIM: 블록 I/O는 메모리 회수 경로의 핵심
     *   → 메모리 부족 시에도 flush가 완료되어야 회수 진행
     *   → rescuer 스레드가 forward progress 보장
     *
     * WQ_UNBOUND: flush는 수 초 소요 → bound pool에 간섭 방지
     *
     * max_active=2: flush + error 동시 처리 허용
     *   (일반적으로 flush 1개 + error 1개)  */
    bdev->flush_wq = alloc_workqueue("my_blk_flush",
        WQ_MEM_RECLAIM | WQ_UNBOUND, 2);
    if (!bdev->flush_wq)
        return -ENOMEM;

    INIT_WORK(&bdev->flush_work, my_flush_handler);
    INIT_WORK(&bdev->error_work, my_error_handler);
    spin_lock_init(&bdev->queue_lock);
    INIT_LIST_HEAD(&bdev->pending_ios);

    return 0;
}

/* 해제 */
static void my_blk_exit(struct my_blk_device *bdev)
{
    bdev->removing = true;

    /* 실행 중인 work 완료 대기 */
    cancel_work_sync(&bdev->flush_work);
    cancel_work_sync(&bdev->error_work);

    destroy_workqueue(bdev->flush_wq);
}
코드 설명

블록 디바이스 드라이버에서 WQ_MEM_RECLAIM | WQ_UNBOUND 조합을 사용하는 패턴입니다.

  • WQ_MEM_RECLAIM 필수 이유블록 I/O flush는 메모리 회수 경로에서 호출됩니다: 더티 페이지 → writeback → 블록 I/O → flush. 메모리가 부족한 상황에서 새 worker를 kmalloc()할 수 없으면 flush가 멈추고, flush가 멈추면 메모리 회수가 멈추는 데드락이 발생합니다. WQ_MEM_RECLAIM의 rescuer 스레드가 이를 방지합니다.
  • list_splice_init()pending I/O 리스트를 로컬 변수로 옮기고 원본을 비웁니다. 이렇게 하면 lock 보유 시간을 최소화하면서도 배치 처리가 가능합니다. flush 중에 새로 도착하는 I/O는 원본 리스트에 추가됩니다.
  • max_active=2flush와 error 핸들러가 동시에 실행될 수 있도록 max_active=2로 설정합니다. flush 타임아웃으로 error_work가 큐잉되면 flush_work와 병렬 실행이 가능해야 합니다.

Work 비활성화 API (6.7+)

Linux 6.7에서 도입된 disable_work()/enable_work() API는 work를 일시적으로 비활성화하는 공식 메커니즘을 제공합니다. 이전에는 cancel_work_sync() + shutdown 플래그로 구현했던 패턴을 커널이 직접 지원합니다.

/* kernel/workqueue.c — 간략화 */

/*
 * disable_work() / enable_work() API (kernel 6.7+):
 *
 * disable_work_sync(&work):
 *   - work를 비활성화 상태로 전환
 *   - 실행 중인 콜백이 있으면 완료 대기 (sync)
 *   - 비활성화 상태에서 queue_work()를 호출해도 큐잉되지 않음
 *   - 중첩(nested) 가능: disable 횟수를 카운트
 *
 * enable_work(&work):
 *   - disable 카운터 감소
 *   - 카운터가 0이 되면 활성화 상태로 복귀
 *   - 비활성화 중 시도된 큐잉이 있으면 자동으로 큐잉됨
 *
 * disable_delayed_work_sync(&dwork):
 *   - delayed_work 버전: 타이머 + work 모두 비활성화
 *
 * enable_delayed_work(&dwork):
 *   - delayed_work 재활성화
 */

/* 사용 예시: suspend/resume에서 work 비활성화 */
struct my_device {
    struct work_struct event_work;
    struct delayed_work monitor_work;
};

static int my_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    /* 6.7+ 방식: 명시적 비활성화
     * - 실행 중인 콜백 완료 대기
     * - 이후 queue_work() 호출은 no-op
     * - enable 호출 시 자동 재활성화 */
    disable_work_sync(&mydev->event_work);
    disable_delayed_work_sync(&mydev->monitor_work);

    return 0;
}

static int my_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    /* 재활성화: disable 중 큐잉 시도가 있었으면 자동 실행 */
    enable_work(&mydev->event_work);
    enable_delayed_work(&mydev->monitor_work);

    /* 모니터링 재개 */
    schedule_delayed_work(&mydev->monitor_work,
        msecs_to_jiffies(1000));

    return 0;
}

/*
 * disable_work_sync() vs cancel_work_sync() 비교:
 *
 * | 기능                    | cancel_work_sync | disable_work_sync |
 * |------------------------|:----------------:|:-----------------:|
 * | 실행 중 콜백 대기        |       ✅         |       ✅          |
 * | 향후 큐잉 차단          |       ❌         |       ✅          |
 * | 중첩(nested) 지원       |       ❌         |       ✅          |
 * | 복원(enable) API        |       ❌         |       ✅          |
 * | 비활성화 중 큐잉 보존    |       ❌         |       ✅          |
 *
 * cancel_work_sync(): 일회성 취소 → 재사용하려면 다시 queue_work()
 * disable_work_sync(): 일시 중단 → enable_work()로 복원
 */
코드 설명

Linux 6.7에서 추가된 work 비활성화 API입니다. kernel/workqueue.c에 구현되어 있습니다.

  • disable_work_sync()cancel_work_sync()와 달리, 비활성화 상태를 유지합니다. 비활성화 중 queue_work()를 호출하면 즉시 반환(false)되지만, 큐잉 시도가 있었다는 것을 기록합니다. enable_work() 호출 시 보류된 큐잉이 자동으로 실행됩니다.
  • 중첩 disabledisable 호출 횟수를 카운트하여, 같은 횟수의 enable이 호출되어야 실제로 활성화됩니다. 여러 서브시스템이 독립적으로 비활성화를 요청할 수 있는 구조입니다.
  • suspend/resume 활용이전에는 shutting_down 플래그 + cancel_delayed_work_sync() 패턴이 필요했지만, 이제 disable_work_sync()/enable_work()으로 깔끔하게 구현할 수 있습니다. 콜백 코드에 shutdown 확인 로직이 필요 없어집니다.
ℹ️

커널 버전 호환성: disable_work() API는 Linux 6.7 이상에서만 사용 가능합니다. 이전 버전에서는 atomic_t shutting_down + cancel_delayed_work_sync() 패턴을 사용하세요. 커널 6.7 미만을 지원해야 하는 out-of-tree 모듈에서는 #if LINUX_VERSION_CODE >= KERNEL_VERSION(6,7,0)으로 분기하세요.

참고자료

커널 공식 문서

LWN.net 기사

커널 소스 코드

발표 및 컨퍼런스

서적

블로그 및 튜토리얼

필수 관련 문서: 참고 문서:
  • Softirq/Hardirq — softirq 실행 흐름, ksoftirqd, 인터럽트 하위 반쪽 메커니즘 상세
  • Tasklet — tasklet_struct, tasklet_schedule, deprecated 이유, 마이그레이션 가이드
  • Threaded IRQ — 스레드화된 인터럽트 핸들러, request_threaded_irq, IRQF_ONESHOT