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
/*
 * 레거시 API → CMWQ 대응 (마이그레이션 가이드)
 *
 * create_workqueue(name)
 *   → alloc_workqueue(name, WQ_MEM_RECLAIM, 1)
 *     (Per-CPU, max_active=1, rescuer 보장)
 *
 * create_singlethread_workqueue(name)
 *   → alloc_ordered_workqueue(name, WQ_MEM_RECLAIM)
 *     (전역 순서 보장, rescuer 보장)
 *
 * create_freezable_workqueue(name)
 *   → alloc_workqueue(name, WQ_FREEZABLE | WQ_MEM_RECLAIM, 1)
 *
 * 참고: 레거시 API는 v5.x에서 완전히 제거됨
 */

CMWQ 이전에는 드라이버마다 create_workqueue()로 전용 workqueue를 만드는 것이 일반적이었고, 이로 인해 시스템에 수백 개의 kworker 스레드가 생성되는 문제가 있었습니다. CMWQ는 worker pool을 중앙 집중 관리하여 이 문제를 해결했습니다.

CMWQ 아키텍처 개요

Concurrency Managed Workqueue (CMWQ)는 Linux 2.6.36에서 도입된 현대적 workqueue 아키텍처입니다. 기존의 singlethread/multithread workqueue를 대체하여, 커널이 worker pool을 중앙 관리하고 동시성을 자동 제어합니다.

CMWQ의 핵심 설계 원칙:

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

CMWQ 아키텍처 상세

CMWQ의 핵심은 workqueue_struct, pool_workqueue, worker_pool, worker 네 가지 구조체(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 실행
/*
 * 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 스레드 생성과 소멸

/*
 * create_worker(): 새 kworker 스레드 생성
 *
 * 호출 조건:
 *   - worklist에 pending work가 있지만 nr_running == 0
 *   - manager worker가 maybe_create_worker()에서 판단
 *
 * 이름 규칙:
 *   Bound:   kworker/CPU:ID     (예: kworker/0:2)
 *            kworker/CPU:IDH    (highpri, 예: kworker/0:1H)
 *   Unbound: kworker/uPOOL:ID   (예: kworker/u8:3)
 */
static struct worker *create_worker(struct worker_pool *pool)
{
    struct worker *worker;

    worker = alloc_worker(pool->node);
    worker->pool = pool;
    worker->id = pool->worker_ida++;

    /* kthread 생성 */
    if (pool->cpu >= 0)
        worker->task = kthread_create_on_node(
            worker_thread, worker, pool->node,
            "kworker/%d:%d%s", pool->cpu, worker->id,
            pool->attrs->nice < 0 ? "H" : "");
    else
        worker->task = kthread_create_on_node(
            worker_thread, worker, pool->node,
            "kworker/u%d:%d", pool->id, worker->id);

    /* Bound pool: CPU에 고정 */
    if (pool->cpu >= 0)
        kthread_bind(worker->task, pool->cpu);

    worker_enter_idle(worker);
    wake_up_process(worker->task);
    return worker;
}

/*
 * idle_worker_timeout: idle worker 소멸 타이머
 *
 * IDLE_WORKER_TIMEOUT (300초) 동안 활동 없으면 소멸
 * 단, 풀에 최소 1개 idle worker는 항상 유지 (min_idle = 1)
 */
static void idle_worker_timeout(struct timer_list *t)
{
    struct worker_pool *pool = from_timer(pool, t, idle_timer);

    /* too_many_workers(): nr_idle > 2 && (nr_idle-2)*MAX_IDLE_WORKERS_RATIO >= nr_busy */
    while (too_many_workers(pool)) {
        struct worker *worker = list_last_entry(
            &pool->idle_list, struct worker, entry);
        destroy_worker(worker);
    }
}

Rescuer 스레드

/*
 * Rescuer Thread: WQ_MEM_RECLAIM 워크큐의 안전장치
 *
 * 메모리 부족으로 새 kworker 스레드를 생성할 수 없을 때,
 * rescuer가 대신 work를 처리하여 데드락을 방지합니다.
 *
 * 동작 흐름:
 * 1. mayday_timer 만료 → send_mayday() 호출
 * 2. rescuer 스레드가 깨어남
 * 3. 해당 pool의 worklist에서 work를 가져와 실행
 * 4. pool의 정상 worker가 복구되면 다시 대기
 *
 * 주의: rescuer는 workqueue당 1개만 존재하므로
 *       동시에 많은 work를 처리할 수 없음
 *       → 최소한의 진행(forward progress)만 보장
 */
static int rescuer_thread(void *__rescuer)
{
    struct worker *rescuer = __rescuer;
    struct workqueue_struct *wq = rescuer->rescue_wq;

    for (;;) {
        set_current_state(TASK_IDLE);
        /* mayday 시그널 대기 */
        if (list_empty(&wq->maydays))
            schedule();
        /* pool의 worklist에서 work 실행 */
        process_scheduled_works(rescuer);
    }
}

alloc_workqueue() API

/* workqueue 생성 */
struct workqueue_struct *alloc_workqueue(
    const char *fmt,          /* 이름 형식 (printf 스타일) */
    unsigned int flags,       /* WQ_* 플래그 조합 */
    int max_active,           /* Per-CPU 최대 동시 실행 work 수 */
    ...                       /* fmt 인자 */
);

/* 예제 */
struct workqueue_struct *my_wq;
my_wq = alloc_workqueue("my_driver_wq",
    WQ_UNBOUND | WQ_MEM_RECLAIM, 0);
코드 설명

include/linux/workqueue.h에 선언된 workqueue 생성 API입니다. 내부적으로 alloc_workqueue()alloc_and_link_pwqs()apply_workqueue_attrs() 호출 체인을 거칩니다.

  • fmtworkqueue 이름으로, /sys/kernel/debug/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)에서 사용하는 WQ에 필수
 *   - 파일시스템, 블록 I/O, 스왑 관련 work에 반드시 설정
 *
 * WQ_FREEZABLE (bit 2):
 *   - 시스템 suspend(freeze) 시 work 처리를 중단
 *   - try_to_freeze_tasks()에서 workqueue를 동결
 *   - resume 시 자동으로 처리 재개
 *   - 사용자 공간 요청 처리, PM 관련 작업에 사용
 *
 * WQ_SYSFS (bit 9):
 *   - /sys/devices/virtual/workqueue/<name>/ 디렉토리 생성
 *   - 런타임에 cpumask, max_active, nice 변경 가능
 *   - 프로덕션 환경 튜닝에 유용
 *
 * WQ_POWER_EFFICIENT (bit 7):
 *   - wq_power_efficient 커널 파라미터 활성 시 WQ_UNBOUND로 동작
 *   - 비활성 시 일반 bound workqueue로 동작
 *   - 전력 효율이 중요한 모바일/임베디드 환경
 */
플래그비트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()
/*
 * 시스템 workqueue 선택 가이드:
 *
 * schedule_work(&work)
 *   → system_wq에 큐잉
 *   → 대부분의 드라이버에서 이것으로 충분
 *   → 짧은 작업, 다른 서브시스템과 간섭 최소
 *
 * queue_work(system_highpri_wq, &work)
 *   → 지연시간이 중요한 작업 (인터럽트 후처리 등)
 *   → nice=-20 worker에서 실행 → 일반 work보다 우선
 *
 * queue_work(system_long_wq, &work)
 *   → 장시간 실행될 수 있는 작업
 *   → system_wq와 같은 pool이지만 의미적으로 분리
 *   → 주의: WQ_CPU_INTENSIVE가 아니므로 concurrency 관리에 영향
 *
 * queue_work(system_unbound_wq, &work)
 *   → CPU에 고정되지 않아야 하는 작업
 *   → NUMA 로컬리티 활용, 스케줄러 자유 배치
 *
 * 전용 workqueue 생성이 필요한 경우:
 *   - flush/cancel 시 다른 서브시스템에 영향 없어야 할 때
 *   - 특수 플래그 조합 필요 (WQ_UNBOUND | WQ_MEM_RECLAIM 등)
 *   - max_active 제한으로 동시성 제어 필요
 *   - /sys/kernel/debug/workqueue에서 독립 모니터링 필요
 *   - WQ_SYSFS로 런타임 튜닝 필요
 */

Work Item 생명주기

work_struct는 커널의 비동기 실행 단위입니다. 하나의 work item은 Idle → Pending → Running → Idle의 생명주기를 거치며, 중간에 취소(cancel)되거나 동기화(flush)될 수 있습니다. 아래 다이어그램은 전체 상태 전이를 보여줍니다.

Work Item 생명주기: 상태 전이 다이어그램 IDLE 어떤 큐에도 없음 다시 큐잉 가능 PENDING WORK_STRUCT_PENDING set worklist에 대기 중 RUNNING PENDING 클리어 current_work = this queue_work() schedule_work() kworker dequeue 콜백 완료 → IDLE 복귀 실행 중 재큐잉 (PENDING set → 완료 후 재실행) 중복 큐잉 → no-op Cancel 동작 PENDING 상태 즉시 dequeue return true RUNNING 상태 완료 대기 (블록킹) return false Flush 동작 flush_work() 특정 work 완료 대기 후 반환 flush_workqueue() 해당 WQ의 모든 pending work 대기 drain_workqueue() 전부 소진 + 차단 핵심: 하나의 work_struct는 동시에 하나의 workqueue에만 속할 수 있음. 서로 다른 WQ에 같은 work 큐잉 불가.
/*
 * Work Item 상태 전이:
 *
 * [Idle]                        work이 어떤 workqueue에도 없는 상태
 *   │
 *   │ queue_work() / schedule_work()
 *   ▼
 * [Pending]                     worklist에 대기 중
 *   WORK_STRUCT_PENDING 비트 set
 *   │
 *   │ kworker가 dequeue
 *   ▼
 * [Running]                     worker가 콜백 실행 중
 *   PENDING 클리어, current_work = this
 *   │
 *   │ 콜백 완료
 *   ▼
 * [Idle]                        다시 큐잉 가능
 *
 * 핵심 규칙:
 * - PENDING인 work를 다시 queue하면 no-op (중복 방지)
 * - Running 중에 queue하면 PENDING이 set되어 완료 후 재실행
 * - 서로 다른 workqueue에 같은 work를 queue할 수 없음
 */

/* work_struct 내부 */
struct work_struct {
    atomic_long_t data;        /* flags + pool_workqueue 포인터 */
    struct list_head entry;     /* worklist 연결 */
    work_func_t func;           /* 콜백 함수 */
};
코드 설명

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

INIT_WORK / INIT_DELAYED_WORK 매크로(Macro) 분석

/*
 * INIT_WORK(): work_struct 초기화 매크로
 *
 * 반드시 queue_work() 전에 호출해야 함
 * 정적 초기화는 DECLARE_WORK() 사용
 */
INIT_WORK(&my_work, my_work_handler);
/* 내부 동작:
 *   1. work->data = WORK_STRUCT_NO_POOL (어떤 pool에도 없음)
 *   2. INIT_LIST_HEAD(&work->entry)  (연결 리스트 초기화)
 *   3. work->func = my_work_handler  (콜백 함수 등록)
 */

/* 정적 초기화 (글로벌/파일 스코프) */
static DECLARE_WORK(my_global_work, my_global_handler);
/* 컴파일 시점에 초기화, 모듈 로드 즉시 사용 가능 */

/* Delayed Work 초기화 */
INIT_DELAYED_WORK(&my_dwork, my_delayed_handler);
/* 내부: INIT_WORK + timer_setup(&dwork->timer, delayed_work_timer_fn) */

static DECLARE_DELAYED_WORK(my_global_dwork, my_global_delayed_handler);

queue_work() vs schedule_work()

/* schedule_work(): system_wq에 큐잉 (편의 함수) */
static inline bool schedule_work(struct work_struct *work)
{
    return queue_work(system_wq, work);
}

/* queue_work(): 특정 workqueue에 큐잉 */
bool queue_work(struct workqueue_struct *wq,
               struct work_struct *work);

/* queue_work_on(): 특정 CPU에 큐잉 */
bool queue_work_on(int cpu, struct workqueue_struct *wq,
                   struct work_struct *work);

/* 반환값: true = 새로 큐잉됨, false = 이미 pending */

/* 전용 workqueue 사용 vs system_wq 기준:
 *
 * system_wq 사용 (schedule_work):
 *   - 짧은 작업, 다른 work와 간섭 적음
 *   - 대부분의 드라이버에서 적합
 *
 * 전용 workqueue 생성:
 *   - flush/cancel 시 다른 서브시스템에 영향 없어야 할 때
 *   - 특수 플래그 필요 (WQ_UNBOUND, WQ_MEM_RECLAIM 등)
 *   - max_active 제어 필요
 *   - /sys/kernel/debug/workqueue에서 독립 모니터링 필요
 */
코드 설명

include/linux/workqueue.h에 정의된 work 큐잉 API들입니다. 내부 호출 체인: queue_work()queue_work_on()__queue_work()insert_work()wake_up_process().

  • schedule_work()system_wq에 큐잉하는 편의 함수(convenience wrapper)입니다. 대부분의 드라이버에서 짧은 비동기 작업에 충분하며, 전용 workqueue 생성 오버헤드를 피할 수 있습니다.
  • queue_work()특정 workqueue에 work를 큐잉합니다. 내부적으로 WORK_STRUCT_PENDING 비트를 test-and-set하여 중복 큐잉을 방지하고, 현재 CPU의 pool_workqueue를 통해 해당 worker pool의 worklist에 삽입합니다.
  • queue_work_on()특정 CPU의 worker pool에 직접 큐잉합니다. 캐시 지역성이 중요한 경우나 인터럽트 핸들러에서 동일 CPU의 bottom-half 처리를 위해 사용합니다.
  • 반환값true는 새로 큐잉되었음을, false는 이미 pending 상태여서 큐잉하지 않았음을 의미합니다. 반환값을 확인하여 work 중복 실행을 방지하는 로직에 활용할 수 있습니다.

Ordered 및 Delayed Workqueue

Workqueue의 실행 모드는 크게 Per-CPU (Bound), Unbound, Ordered로 나뉩니다. 각각의 동작 차이를 아래 다이어그램에서 비교합니다.

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

Ordered Workqueue

/* Ordered Workqueue: 큐잉 순서대로 하나씩 실행 */
struct workqueue_struct *owq;
owq = alloc_ordered_workqueue("my_ordered", 0);

/*
 * 특성:
 * - max_active = 1 → 동시에 하나의 work만 실행
 * - 큐잉 순서 보장 (FIFO)
 * - 내부적으로 __WQ_ORDERED 플래그 + unbound
 *
 * 사용 시나리오:
 * - 상태 머신 이벤트 처리 (순서 중요)
 * - 파일시스템 로그/저널 쓰기
 * - 하드웨어 초기화 시퀀스
 *
 * 주의: ordered wq는 WQ_UNBOUND를 암시적으로 포함
 *       → CPU 마이그레이션 가능 (특정 CPU 고정 아님)
 */

Delayed Work

/* Delayed Work: 지정 시간 후 실행 */
struct delayed_work {
    struct work_struct work;
    struct timer_list timer;
    struct workqueue_struct *wq;
    int cpu;
};

/* 초기화 */
INIT_DELAYED_WORK(&dev->dwork, my_delayed_handler);

/* 큐잉: delay jiffies 후 실행 */
queue_delayed_work(my_wq, &dev->dwork,
    msecs_to_jiffies(100));    /* 100ms 후 */

/* 시스템 workqueue에 큐잉 */
schedule_delayed_work(&dev->dwork,
    msecs_to_jiffies(500));    /* 500ms 후 */

/* mod_delayed_work(): 이미 pending인 delayed work의 타이머 변경 */
mod_delayed_work(my_wq, &dev->dwork,
    msecs_to_jiffies(200));    /* 기존 타이머 취소 + 200ms로 재설정 */
/* 반환값: true = 기존 pending work를 변경, false = 새로 큐잉 */

/* 즉시 실행으로 변경 */
mod_delayed_work(my_wq, &dev->dwork, 0);
/* delay=0이면 가능한 빨리 실행 */
코드 설명

include/linux/workqueue.h에 정의된 delayed_work 구조체와 관련 API입니다.

  • delayed_work 구조체work_struct를 내장(embed)하고 timer_list를 추가한 확장 구조체입니다. 타이머 만료 시 delayed_work_timer_fn() 콜백이 내부 work_struct를 실제 worklist에 큐잉합니다.
  • queue_delayed_work()지정된 delay(jiffies 단위) 후에 work를 큐잉합니다. 내부적으로 delay == 0이면 queue_work()를 직접 호출하고, 그렇지 않으면 add_timer_on()으로 타이머를 설정합니다. msecs_to_jiffies()로 밀리초를 jiffies로 변환합니다.
  • mod_delayed_work()이미 pending인 delayed work의 타이머를 변경합니다. 기존 타이머를 취소하고 새 delay로 재설정하는 원자적(atomic) 연산입니다. 폴링(polling) 주기 동적 조정이나 debounce 패턴에 유용합니다.
  • schedule_delayed_work()system_wq에 delayed work를 큐잉하는 편의 함수입니다. 주기적 작업(periodic work)에서 콜백 끝에 schedule_delayed_work()를 재호출하는 자기 재큐잉(self-requeuing) 패턴이 일반적입니다.

Delayed Work 내부 타이머(Timer) 연동

/*
 * delayed_work 내부 동작:
 *
 * queue_delayed_work(wq, &dwork, delay)
 *   │
 *   ├─ delay == 0?
 *   │   ├─ YES → queue_work(wq, &dwork.work)  // 즉시 큐잉
 *   │   └─ NO  → __queue_delayed_work():
 *   │             1. dwork->wq = wq
 *   │             2. dwork->cpu = current_cpu
 *   │             3. timer_setup(&dwork->timer, delayed_work_timer_fn)
 *   │             4. add_timer_on(&dwork->timer, cpu)
 *   │             // 타이머 만료 시 콜백:
 *   │
 *   ▼
 * delayed_work_timer_fn() (타이머 만료 시 호출)
 *   │
 *   ├─ WORK_STRUCT_DELAYED 클리어
 *   └─ __queue_work(dwork->cpu, dwork->wq, &dwork->work)
 *       // 이 시점에서 일반 work와 동일하게 처리
 *
 * 핵심:
 * - delayed_work = work_struct + timer_list + wq 포인터
 * - 타이머가 만료되면 일반 work로 전환하여 큐잉
 * - cancel_delayed_work()는 del_timer() + (선택적) cancel_work()
 * - mod_delayed_work()는 del_timer() + queue_delayed_work() 원자적 수행
 */

/* 주기적 작업 패턴 (자기 재큐잉) */
static void periodic_handler(struct work_struct *work)
{
    struct delayed_work *dwork = to_delayed_work(work);
    struct my_device *dev = container_of(dwork,
        struct my_device, periodic_work);

    /* 주기적 작업 수행 */
    do_periodic_check(dev);

    /* shutting_down 체크 후 자기 재큐잉 */
    if (!dev->shutting_down)
        queue_delayed_work(dev->wq, dwork,
            msecs_to_jiffies(1000));  /* 1초마다 */
}

ordered vs max_active=1의 차이

설명 요약:
  • alloc_ordered_workqueue() 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 콜백 외부)
/* 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가 가장 자연스러운 선택
/*
 * RT 커널에서 WQ_BH workqueue의 실행 모델:
 *
 * 일반 커널:
 *   WQ_BH work → __do_softirq() 내에서 실행
 *   → 인터럽트 비활성화 상태, 선점 불가
 *   → local_bh_disable() 영역 안
 *
 * PREEMPT_RT:
 *   WQ_BH work → ksoftirqd/N 스레드 내에서 실행
 *   → 프로세스 컨텍스트 (스레드화된 softirq)
 *   → 선점 가능, 슬립 가능 (단, API 제약은 유지)
 *   → 우선순위 설정 가능: chrt -f -p 50 $(pidof ksoftirqd/0)
 *
 * 중요: WQ_BH work의 콜백은 여전히 softirq 규약을 따라야 함
 *   - spin_lock_bh() 사용 금지 (softirq 재진입 아님)
 *   - 슬립 API 호출 금지 (RT에서 가능하더라도 이식성 위해)
 *   - GFP_KERNEL 할당 금지 (GFP_ATOMIC만)
 */

RT 환경 Best Practices

규칙이유구현 방법
RT 태스크에서 flush_work() 회피 kworker가 SCHED_NORMAL이므로 우선순위 역전 발생 struct completion + wait_for_completion() 사용
중요 work에 WQ_HIGHPRI 사용 nice=-20으로 실행되어 일반 태스크보다 우선 alloc_workqueue("name", WQ_HIGHPRI, 0)
raw_spinlock vs spinlock 구분 RT에서 spinlock은 슬립 가능 → 예상치 못한 컨텍스트 전환 hardirq 핸들러 내: raw_spinlock_t, work 콜백 내: spinlock_t 허용
WQ_BH 콜백에서 블로킹 API 사용 금지 RT에서 기술적으로 가능하지만 비-RT 호환성 깨짐 블로킹 필요 시 일반 workqueue로 릴레이
kworker 우선순위는 cgroup으로 관리 kworker pid가 동적 변경됨 systemd slice 또는 cgroup v2 cpu.max 활용
workqueue.watchdog 활성화 RT에서 우선순위 문제로 work 교착 가능성 증가 workqueue.watchdog_thresh=10 (기본 30초보다 짧게)
⚠️

RT 우선순위 역전(Priority Inversion) 주의: 높은 우선순위 RT 태스크(Task)가 flush_work()로 work 완료를 대기할 때, kworker는 SCHED_NORMAL이므로 중간 우선순위 태스크에 선점될 수 있습니다. 해결: WQ_HIGHPRI 사용, 또는 RT 태스크에서 flush_work() 대신 다른 동기화 메커니즘 사용.

💡

RT 이식성 팁: RT 커널에서도 정상 동작하게 하려면, workqueue가 가장 안전한 Bottom Half 메커니즘입니다. softirq/tasklet은 RT에서 스레드화되면서 기대와 다른 지연 시간을 보일 수 있지만, workqueue는 본래부터 프로세스 컨텍스트이므로 변화가 적습니다.

일반적인 버그 패턴

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

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

스코프별 성능 특성

/*
 * Affinity Scope 선택 가이드:
 *
 * 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 생성과 소멸

/*
 * manage_workers() — Worker Pool의 핵심 관리 로직
 *
 * 호출 시점: worker_thread()에서 POOL_MANAGER_ACTIVE가 아닐 때
 * 관리자 역할을 맡은 worker가 실행
 *
 * 판단 기준:
 *   1. need_more_worker(): worklist 비어있지 않고 nr_running == 0
 *      → maybe_create_worker() 호출
 *   2. too_many_workers(): idle worker 과다
 *      → idle_timer에 의해 정리 (별도 타이머)
 */

static bool manage_workers(struct worker *worker)
{
    struct worker_pool *pool = worker->pool;

    if (pool->flags & POOL_MANAGER_ACTIVE)
        return false;

    pool->flags |= POOL_MANAGER_ACTIVE;

    /* worker 생성 필요 여부 판단 */
    maybe_create_worker(pool);

    pool->flags &= ~POOL_MANAGER_ACTIVE;
    return true;
}

static void maybe_create_worker(struct worker_pool *pool)
{
restart:
    /* nr_idle가 0이면 worker 생성 시도 */
    while (!may_start_working(pool)) {
        if (create_worker(pool) || !need_to_create_worker(pool))
            break;

        /* 생성 실패 — 잠시 후 재시도 */
        schedule_timeout_interruptible(CREATE_COOLDOWN);
        if (!need_to_create_worker(pool))
            break;
        goto restart;
    }
}

pool->lock contention 분석

/*
 * pool->lock 경쟁 지점:
 *
 * 1. queue_work() → insert_work() — work 삽입 시
 * 2. worker_thread() → process_one_work() — work 꺼내기 시
 * 3. manage_workers() — worker 생성/소멸 판단 시
 * 4. flush/cancel 경로 — 동기화 대기 시
 *
 * 경쟁 완화 설계:
 * - Bound pool: CPU별 독립 pool → 다른 CPU와 lock 경쟁 없음
 * - Unbound pool: 같은 NUMA 노드의 work가 같은 pool에 들어감
 * - try_to_grab_pending(): lock 없이 먼저 시도 (낙관적)
 *
 * 높은 contention 발생 시:
 * - unbound pool을 NUMA 노드별로 분리 (affinity scope = numa)
 * - 커스텀 workqueue로 분리하여 pool 공유 해소
 * - WQ_UNBOUND 대신 per-CPU workqueue 고려
 */
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와 함께 사용할 때 가장 효과적입니다.

메모리 압박/회수 경로에서의 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 시 작업 동결

/*
 * 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)
 *
 * 검사 주기: pool->watchdog_ts를 주기적으로 확인
 * - work가 완료되면 watchdog_ts 갱신
 * - watchdog_ts가 임계값 이상 경과하면 BUG 보고
 *
 * 출력 예시:
 * BUG: workqueue lockup - pool cpus=0 node=0 flags=0x0
 *      nice=0 stuck for 32s!
 */

/* 부트 파라미터로 임계값 설정 */
/* kernel cmdline: workqueue.watchdog_thresh=60 (60초) */
/* 런타임: /sys/module/workqueue/parameters/watchdog_thresh */

CPU Hotplug와 Worker 마이그레이션

CPU Hotplug: 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 (각 단계별 콜백)
/*
 * 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 활성화결정적 지연, 교착 감지

참고자료

커널 공식 문서

LWN.net 기사

커널 소스 코드

발표 및 컨퍼런스

서적

블로그 및 튜토리얼

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