커널 스레드 (Kernel Threads)

커널 스레드(kthread)는 유저 주소 공간(Address Space) 없이 커널 문맥에서 동작하는 핵심 실행 단위입니다. 이 문서는 kthreadd(PID 2) 기반 생성 흐름, kthread_create/run/stop/park 제어 API, kthread_worker 패턴, per-CPU 및 smpboot 스레드(Thread) 설계, freezer/CPU hotplug 대응, ksoftirqd/kworker/kswapd 운영 특성까지 실전 관점으로 상세히 설명합니다.

관련 표준: POSIX.1-2017 (pthread API 비교 참조) — 유저 공간 스레드와 대비되는 커널 스레드의 동작을 이해할 수 있습니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 문서: 프로세스(Process) 관리(task_struct, fork/exec), 프로세스 스케줄러(Scheduler)(CFS, sched_class), Workqueue(CMWQ, kworker), Bottom Half(ksoftirqd)를 함께 참고하세요.
전제 조건: 프로세스 스케줄러프로세스 문서를 먼저 읽으세요. 실행 단위 관리 주제는 태스크(Task) 상태 전이와 큐 정책이 핵심이므로, 스케줄링 기준과 wakeup 경로를 먼저 이해해야 합니다.

핵심 요약

  • 커널 스레드mm == NULLtask_struct. 유저 주소 공간 없이 커널 코드만 실행합니다.
  • kthreadd (PID 2) — 모든 커널 스레드의 부모. 생성 요청을 직렬화(Serialization)하여 처리합니다.
  • kthread_create / kthread_run — 커널 스레드 생성 API. kthread_run은 생성 + 즉시 깨우기(Wakeup)를 합칩니다.
  • kthread_should_stop — 스레드 함수 내 메인 루프의 종료 조건. kthread_stop()이 호출되면 true를 반환합니다.
  • kthread_worker — workqueue 대안으로, 전용 스레드에서 작업을 순차 실행하는 프레임워크입니다.

단계별 이해

  1. 커널 스레드 식별ps aux에서 [대괄호]로 표시되는 프로세스가 커널 스레드입니다.

    ps -eo pid,ppid,comm | grep "\\["로 확인하면 대부분 PPID가 2(kthreadd)입니다.

  2. 기본 패턴 이해 — 커널 스레드는 while (!kthread_should_stop()) { 작업; sleep; } 패턴으로 동작합니다.

    외부에서 kthread_stop()을 호출하면 루프를 빠져나와 종료합니다.

  3. 생성 흐름kthread_create() → kthreadd가 kernel_clone()으로 실제 태스크 생성 → wake_up_process()로 실행 시작.

    kthread_run()은 이 세 단계를 하나로 합친 매크로(Macro)입니다.

  4. 실습 — 아래 기본 커널 스레드 모듈 예제로 직접 로드/언로드하며 동작을 확인하세요.

    dmesg -w로 커널 로그를 실시간(Real-time) 모니터링하면 스레드 동작을 관찰할 수 있습니다.

개요 (Overview)

리눅스에서 커널 스레드(kernel thread, kthread)는 유저 공간 주소 공간 없이 커널 모드에서만 동작하는 태스크입니다. 일반 유저 프로세스와 동일하게 task_struct로 표현되지만, 핵심적인 차이가 있습니다:

속성 유저 프로세스 / 스레드 커널 스레드
task_struct.mm 유효한 mm_struct 포인터 NULL
task_struct.active_mm == mm 이전 태스크의 mm을 빌림 (Lazy TLB)
task_struct.flags PF_KTHREAD 없음 PF_KTHREAD 설정됨
주소 공간 유저 + 커널 커널만
ps 표시 일반 이름 [대괄호] 표기 (예: [kswapd0])
부모 프로세스 다양 (fork한 프로세스) kthreadd (PID 2)
시그널(Signal) 처리 유저 시그널 핸들러(Handler) 대부분 시그널 차단 (커널 내부 제어)
/* include/linux/sched.h - 커널 스레드 판별 */
static inline bool is_kthread(struct task_struct *p)
{
    return p->flags & PF_KTHREAD;
}

/* context_switch()에서의 Lazy TLB */
if (!next->mm) {
    /* 커널 스레드: 이전 태스크의 mm을 빌림 */
    next->active_mm = prev->active_mm;
    /* → TLB flush 불필요, 성능 향상 */
}
ℹ️

Lazy TLB: 커널 스레드는 유저 주소 공간에 접근하지 않으므로, 컨텍스트 스위칭(Context Switching) 시 페이지 테이블(Page Table)을 전환할 필요가 없습니다. 이전 태스크의 mm을 빌려 사용하여 불필요한 TLB flush를 방지합니다.

커널 스레드 메모리 모델 (Kernel Thread Memory Model)

커널 스레드는 일반 유저 프로세스와 달리 자체 유저 주소 공간이 없습니다. task_structmm 필드가 NULL로 설정되어 있으며, 오직 커널 주소 공간(vmalloc, kmalloc, 직접 매핑 영역 등)만 접근할 수 있습니다.

mm과 active_mm의 관계

유저 프로세스 task_struct mm_struct 페이지 테이블 물리 메모리 task_struct (스레드) 공유 커널 스레드 task_struct mm = NULL active_mm = (이전 태스크의 mm) ← Lazy TLB 커널 주소 공간만 접근 kthread_use_mm() 사용 시 task_struct mm = 빌린 mm_struct ← CR3 전환 유저 공간 접근 가능 (copy_from_user 등)

kthread_use_mm() / kthread_unuse_mm()

특수한 경우 커널 스레드에서 유저 공간에 접근해야 할 때 kthread_use_mm() / kthread_unuse_mm() API를 사용합니다. 이 API는 프로세스의 mm_struct를 일시적으로 빌려서 유저 주소 공간에 접근할 수 있게 합니다.

사용 시 반드시 mmget()/mmput()으로 참조 카운트를 관리해야 하며, 프로세스가 종료되어 mm_struct가 해제되는 것을 방지해야 합니다.

/* mm/mmu_context.c - kthread_use_mm() (간략화) */
void kthread_use_mm(struct mm_struct *mm)
{
    struct task_struct *tsk = current;

    WARN_ON_ONCE(!(tsk->flags & PF_KTHREAD));
    WARN_ON_ONCE(tsk->mm);

    mmgrab(mm);  /* mm_count 증가 */
    tsk->mm = mm;
    tsk->active_mm = mm;

    switch_mm_irqs_off(NULL, mm, tsk);  /* CR3 전환 */

    /* 이후 copy_from_user(), get_user() 등 유저 공간 접근 가능 */
}

void kthread_unuse_mm(struct mm_struct *mm)
{
    struct task_struct *tsk = current;

    tsk->mm = NULL;
    switch_mm_irqs_off(mm, &init_mm, tsk);

    mmdrop(mm);  /* mm_count 감소 */
}

/* 사용 예시 (vhost) */
static int vhost_worker(void *data)
{
    struct vhost_dev *dev = data;
    struct mm_struct *mm = dev->mm;

    kthread_use_mm(mm);

    while (!kthread_should_stop()) {
        /* 유저 공간의 vring 버퍼에 직접 접근 가능 */
        process_vring(dev);
        schedule();
    }

    kthread_unuse_mm(mm);
    return 0;
}
⚠️

kthread_use_mm() 사용 시 주의사항:

  • PF_KTHREAD 플래그가 설정된 커널 스레드에서만 호출해야 합니다. 일반 프로세스에서 호출하면 WARN이 발생합니다.
  • 사용 중인 mm_struct가 해제되지 않도록 반드시 사전에 mmget()으로 참조 카운트를 확보해야 합니다.
  • kthread_use_mm()kthread_unuse_mm()은 반드시 쌍으로 호출해야 합니다. unuse 없이 스레드가 종료되면 메모리 누수가 발생합니다.
  • 빌린 mm의 프로세스가 exit_mm()을 호출해도 커널 스레드가 참조를 유지하는 동안 mm_struct는 해제되지 않으므로, 장시간 보유를 피해야 합니다.

kthreadd (PID 2) — 커널 스레드 관리자

kthreadd는 부팅 초기에 rest_init()에서 생성되는 커널 스레드로, PID 2를 부여받습니다. 이후 생성되는 모든 커널 스레드의 부모 역할을 합니다.

/* init/main.c - rest_init() */
static void rest_init(void)
{
    /* PID 1: kernel_init → init/systemd */
    kernel_thread(kernel_init, NULL, CLONE_FS);

    /* PID 2: kthreadd — 커널 스레드 관리자 */
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);

    complete(&kthreadd_done);  /* kernel_init이 대기 중 */
    cpu_startup_entry(CPUHP_ONLINE);  /* idle 스레드가 됨 */
}

kthreadd 메인 루프

kthreadd()는 무한 루프에서 kthread_create_list를 감시하며, 새 커널 스레드 생성 요청을 처리합니다:

/* kernel/kthread.c - kthreadd() 메인 루프 (간략화) */
int kthreadd(void *unused)
{
    struct task_struct *tsk = current;

    /* 모든 시그널 차단 */
    set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_TYPE_KTHREAD));
    ignore_signals(tsk);

    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);
        if (list_empty(&kthread_create_list))
            schedule();  /* 요청 없으면 슬립 */
        __set_current_state(TASK_RUNNING);

        while (!list_empty(&kthread_create_list)) {
            struct kthread_create_info *create;
            create = list_entry(kthread_create_list.next,
                        struct kthread_create_info, list);
            list_del_init(&create->list);

            /* 실제 커널 스레드 생성 */
            create_kthread(create);
        }
    }
    return 0;
}
💡

직렬화: 커널 스레드 생성이 kthreadd를 거치는 이유는 kernel_clone() 호출 시의 환경(파일 디스크립터(File Descriptor), 네임스페이스(Namespace) 등)을 깨끗하게 유지하기 위함입니다. 임의의 컨텍스트에서 직접 kernel_clone()을 호출하면 예기치 않은 리소스가 상속될 수 있습니다.

생성 API

kthread_create()

kthread_create()는 커널 스레드를 생성하되, TASK_UNINTERRUPTIBLE 상태로 두어 아직 실행하지 않습니다. 호출자가 명시적으로 wake_up_process()를 호출해야 스레드가 실행됩니다.

/* include/linux/kthread.h */
struct task_struct *kthread_create(
    int (*threadfn)(void *data),  /* 스레드 함수 */
    void *data,                    /* 함수에 전달할 인자 */
    const char *namefmt, ...       /* 스레드 이름 (printf 형식) */
);

/* 사용 예시 */
struct task_struct *kth;
kth = kthread_create(my_thread_fn, my_data, "my-kthread/%d", cpu);
if (IS_ERR(kth)) {
    pr_err("kthread_create failed: %ld\\n", PTR_ERR(kth));
    return PTR_ERR(kth);
}
/* CPU 바인딩 등 설정 후 시작 */
kthread_bind(kth, cpu);
wake_up_process(kth);

kthread_run()

kthread_run()kthread_create() + wake_up_process()를 합친 편의 매크로입니다:

/* include/linux/kthread.h */
#define kthread_run(threadfn, data, namefmt, ...)         \
({                                                          \
    struct task_struct *__k                              \
        = kthread_create(threadfn, data, namefmt,          \
                          ## __VA_ARGS__);                   \
    if (!IS_ERR(__k))                                     \
        wake_up_process(__k);                              \
    __k;                                                    \
})

/* 가장 흔한 사용법 */
struct task_struct *kth;
kth = kthread_run(my_thread_fn, NULL, "my-kthread");
if (IS_ERR(kth))
    return PTR_ERR(kth);

kthread_create_on_node() / kthread_create_on_cpu()

NUMA 노드 또는 특정 CPU에 바인딩된 커널 스레드를 생성합니다:

/* NUMA 노드 지정 생성 */
struct task_struct *kthread_create_on_node(
    int (*threadfn)(void *data),
    void *data, int node,
    const char *namefmt, ...
);

/* 특정 CPU 바인딩 생성 — per-CPU kthread에 사용 */
struct task_struct *kthread_create_on_cpu(
    int (*threadfn)(void *data),
    void *data, unsigned int cpu,
    const char *namefmt
);
/* → 자동으로 kthread_bind(task, cpu) + 이름에 CPU 번호 부여 */

내부 생성 흐름

kthread_create(fn, data) kthread_create_info를 리스트에 추가 wake_up_process (kthreadd) kthreadd: create_kthread() → kernel_clone() 새 task_struct 생성 (TASK_UNINTERRUPTIBLE) complete() → 호출자에 반환 호출자: wait_for_completion()
kthread_create() 내부 흐름 — 호출자 → kthreadd → kernel_clone() → 완료 통지
/* kernel/kthread.c - create_kthread() 내부 (간략화) */
static void create_kthread(struct kthread_create_info *create)
{
    int pid;

    /* kernel_clone()으로 새 태스크 생성
     * → 새 태스크는 kthread() 함수에서 시작 */
    pid = kernel_thread(kthread, create, create->full_name,
                         CLONE_FS | CLONE_FILES | SIGCHLD);
    if (pid < 0) {
        create->result = ERR_PTR(pid);
        complete(&create->done);
    }
}

/* 새로 생성된 커널 스레드의 시작점 */
static int kthread(void *_create)
{
    struct kthread_create_info *create = _create;
    int (*threadfn)(void *data) = create->threadfn;
    void *data = create->data;
    int ret;

    /* 호출자에게 task_struct 포인터 전달 */
    create->result = current;
    complete(&create->done);

    /* wake_up_process()가 호출될 때까지 대기 */
    schedule_preempt_disabled();

    /* 스레드 함수 실행 */
    ret = threadfn(data);

    kthread_exit(ret);
}

커널 스레드 함수 패턴

기본 kthread_should_stop() 루프

가장 기본적인 커널 스레드 패턴입니다:

static int my_thread_fn(void *data)
{
    while (!kthread_should_stop()) {
        /* 작업 수행 */
        do_my_work(data);

        /* 일정 시간 슬립 */
        msleep_interruptible(1000);
    }
    return 0;
}

조건부 슬립(Sleep) 패턴 (set_current_state + schedule)

이벤트가 발생할 때만 깨어나는 효율적인 패턴입니다:

static int event_thread_fn(void *data)
{
    struct my_device *dev = data;

    while (!kthread_should_stop()) {
        /* ① 먼저 상태를 INTERRUPTIBLE로 설정 */
        set_current_state(TASK_INTERRUPTIBLE);

        /* ② 조건 확인 — 작업이 없으면 슬립 */
        if (!has_pending_work(dev)) {
            schedule();  /* CPU 양보, 깨워질 때까지 대기 */
            continue;
        }

        /* ③ 작업이 있으면 RUNNING으로 복원 후 처리 */
        __set_current_state(TASK_RUNNING);
        process_pending_work(dev);
    }
    __set_current_state(TASK_RUNNING);
    return 0;
}

/* 다른 곳에서 작업을 추가하고 스레드를 깨움 */
add_work(dev, new_work);
wake_up_process(thread_task);
⚠️

순서 주의: set_current_state(TASK_INTERRUPTIBLE)조건 확인 전에 호출해야 합니다. 순서를 바꾸면 조건 확인 후 ~ schedule() 사이에 이벤트가 발생해도 놓치는 wakeup-miss 레이스가 발생합니다.

wait_event_interruptible 패턴

위의 조건부 슬립을 더 안전하게 래핑한 API입니다:

static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static bool work_pending = false;

static int waiter_thread_fn(void *data)
{
    while (!kthread_should_stop()) {
        /* 조건이 true가 되거나 kthread_stop()이 호출될 때까지 대기 */
        wait_event_interruptible(my_wq,
            work_pending || kthread_should_stop());

        if (kthread_should_stop())
            break;

        work_pending = false;
        do_work();
    }
    return 0;
}

/* 깨우기 */
work_pending = true;
wake_up_interruptible(&my_wq);

종료 API

kthread_stop()

kthread_stop()은 대상 커널 스레드에 종료 요청을 보내고, 스레드가 실제로 종료할 때까지 대기합니다:

/* kernel/kthread.c - kthread_stop() (간략화) */
int kthread_stop(struct task_struct *k)
{
    get_task_struct(k);  /* 참조 카운트 증가 */

    /* KTHREAD_SHOULD_STOP 플래그 설정 */
    kthread_set_bit(KTHREAD_SHOULD_STOP, k);

    /* 스레드가 슬립 중이면 깨움 */
    wake_up_process(k);

    /* 스레드가 종료할 때까지 대기 */
    wait_for_completion(&kthread->exited);

    ret = kthread->result;
    put_task_struct(k);  /* 참조 카운트 감소 */
    return ret;  /* 스레드 함수의 반환값 */
}
⚠️

교착 위험: kthread_stop()은 스레드 종료를 동기적으로 대기합니다. 스레드 함수가 kthread_should_stop()을 확인하지 않거나, 영구적으로 블록된 상태라면 kthread_stop()도 영원히 블록됩니다.

kthread_park() / kthread_unpark()

커널 스레드를 일시 중지/재개하는 API입니다. CPU 핫플러그(Hotplug) 시 per-CPU 스레드를 관리하는 데 주로 사용됩니다:

/* 스레드 일시 중지 */
kthread_park(kth);
/* → 스레드 내부에서 kthread_should_park()가 true 반환
 * → 스레드는 kthread_parkme()에서 대기 상태로 진입 */

/* 스레드 재개 */
kthread_unpark(kth);
/* → KTHREAD_SHOULD_PARK 해제, 스레드 깨움 */

/* 스레드 함수 내에서 park 지원하기 */
static int parkable_thread_fn(void *data)
{
    while (!kthread_should_stop()) {
        /* park 요청이 있으면 대기 */
        if (kthread_should_park())
            kthread_parkme();

        do_work();
        schedule();
    }
    return 0;
}

커널 스레드 상태 전환

커널 스레드는 생성부터 종료까지 여러 상태를 거칩니다. 다음 다이어그램은 주요 상태 전환을 보여줍니다:

CREATED 생성됨, 대기 RUNNING 실행 중 INTERRUPTIBLE 슬립 (깨울 수 있음) PARKED 일시 중지 UNINTER- RUPTIBLE 슬립 (D 상태) STOPPED 종료됨 wake_up_process() kthread_run() schedule() set_current_state() wake_up() 이벤트 발생 I/O 대기 I/O 완료 kthread_park() should_park() kthread_unpark() kthread_stop() should_stop() return from fn 스레드 함수는 kthread_should_stop(), kthread_should_park()를 주기적으로 확인 PF_KTHREAD 플래그 설정, mm == NULL, PPID = 2 (kthreadd)
상태task_struct.state설명
CREATEDTASK_UNINTERRUPTIBLEkthread_create() 직후, wake_up_process() 대기 중
RUNNINGTASK_RUNNINGCPU에서 실행 중이거나 runqueue에서 대기
INTERRUPTIBLETASK_INTERRUPTIBLEschedule()로 자발적 양보(Yield), 이벤트/시그널로 깨울 수 있음
UNINTERRUPTIBLETASK_UNINTERRUPTIBLEI/O 완료 대기, 시그널로 깨울 수 없음 (D 상태)
PARKEDTASK_PARKEDkthread_park()로 일시 중지, CPU 핫플러그 시 사용
STOPPEDTASK_DEAD스레드 함수 종료, do_exit() 호출됨
상태 확인: ps -eo pid,stat,comm | grep "\[.*\]"로 커널 스레드의 상태를 확인할 수 있습니다. S는 INTERRUPTIBLE, D는 UNINTERRUPTIBLE, R는 RUNNING입니다.

Per-CPU 커널 스레드

많은 커널 서브시스템이 각 CPU마다 전용 커널 스레드를 실행합니다. 대표적으로 ksoftirqd/N, migration/N, kworker/N:* 등이 있습니다.

smpboot 프레임워크

smpboot 프레임워크는 per-CPU 커널 스레드를 체계적으로 관리합니다. CPU 핫플러그(online/offline) 이벤트에 자동으로 대응하여 스레드를 생성/정지/재개합니다:

/* include/linux/smpboot.h */
struct smp_hotplug_thread {
    struct task_struct __percpu  **store;       /* per-CPU 태스크 포인터 */
    struct list_head             list;         /* 등록 리스트 */
    int                          (*thread_should_run)(unsigned int cpu);
    void                         (*thread_fn)(unsigned int cpu);
    void                         (*create)(unsigned int cpu);
    void                         (*setup)(unsigned int cpu);
    void                         (*cleanup)(unsigned int cpu, bool online);
    void                         (*park)(unsigned int cpu);
    void                         (*unpark)(unsigned int cpu);
    const char                   *thread_comm;  /* "ksoftirqd/%u" */
};

/* 등록 — 모든 online CPU에 대해 스레드 자동 생성 */
int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread);

/* 예: ksoftirqd 등록 (kernel/softirq.c) */
static struct smp_hotplug_thread softirq_threads = {
    .store              = &ksoftirqd,
    .thread_should_run  = ksoftirqd_should_run,
    .thread_fn          = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",
};

smpboot_thread_fn()이 per-CPU 스레드의 메인 루프를 실행합니다:

/* kernel/smpboot.c - smpboot_thread_fn() 핵심 (간략화) */
static int smpboot_thread_fn(void *data)
{
    struct smp_hotplug_thread *ht = data;
    unsigned int cpu = smp_processor_id();

    while (1) {
        set_current_state(TASK_INTERRUPTIBLE);

        if (kthread_should_park()) {
            __set_current_state(TASK_RUNNING);
            kthread_parkme();  /* CPU offline 시 park */
            continue;
        }

        if (!ht->thread_should_run(cpu)) {
            schedule();  /* 할 일 없으면 슬립 */
        } else {
            __set_current_state(TASK_RUNNING);
            ht->thread_fn(cpu);  /* 실제 작업 수행 */
        }

        if (kthread_should_stop())
            break;
    }
    return 0;
}

smpboot 프레임워크 아키텍처

smpboot은 CPU 핫플러그 이벤트와 per-CPU 스레드를 자동으로 연동합니다:

CPU Hotplug 이벤트 ONLINE OFFLINE smpboot 코어 smpboot_register_percpu_thread() smpboot_thread_fn() 메인 루프 콜백 함수들 → create(cpu) → setup(cpu) → thread_should_run(cpu) → thread_fn(cpu) → park(cpu) → unpark(cpu) → cleanup(cpu) Per-CPU 스레드들 [thread/0] CPU 0 [thread/1] CPU 1 [thread/N] CPU N 호출 생성/관리 예시: ksoftirqd 등록 struct smp_hotplug_thread softirq_threads = { .store = &ksoftirqd, .thread_should_run = ksoftirqd_should_run, .thread_fn = run_ksoftirqd, .thread_comm = "ksoftirqd/%u" }; 스레드 상태 흐름 CPU ONLINE create/setup RUNNING CPU OFFLINE park/cleanup PARKED smpboot은 등록된 모든 스레드에 대해 CPU 이벤트를 자동 처리하여 핫플러그 관리를 단순화
자동 핫플러그 관리: smpboot 프레임워크를 사용하면 CPU online/offline 시 per-CPU 스레드를 수동으로 관리할 필요가 없습니다. smpboot_register_percpu_thread()만 호출하면, 커널이 CPU 핫플러그 이벤트에 자동으로 대응하여 스레드를 생성/park/unpark/정리합니다.

CPU Hotplug 연동 상세 (CPU Hotplug Integration)

Per-CPU 커널 스레드는 CPU 핫플러그(Hotplug) 이벤트와 밀접하게 연동됩니다. smpboot 프레임워크는 cpuhp 상태 머신(State Machine)과 통합되어, CPU가 온라인/오프라인될 때 per-CPU 스레드의 생명주기를 자동으로 관리합니다.

CPU Online 과정

  1. cpu_up() 호출로 cpuhp 상태 머신이 시작됩니다.
  2. CPUHP_CREATE_THREADS 단계에서 smpboot이 해당 CPU의 per-CPU 스레드를 생성합니다 (이미 존재하면 생략).
  3. kthread_unpark(): 파킹(Parked) 상태의 스레드를 깨워 실행 가능 상태로 전환합니다.
  4. set_cpus_allowed_ptr(): 해당 CPU에 대한 affinity를 설정합니다.
  5. wake_up_process(): 스레드 실행을 시작합니다.

CPU Offline 과정

  1. cpu_down() 호출로 cpuhp 상태 머신이 시작됩니다.
  2. sched_cpu_deactivate(): 해당 CPU에서 실행 중인 태스크를 다른 CPU로 마이그레이션합니다.
  3. kthread_park(): per-CPU 스레드를 파킹 상태(TASK_PARKED)로 전환합니다.
  4. 스레드는 살아있지만 TASK_PARKED 상태로 대기합니다. 스레드가 파괴되지 않으므로 CPU가 다시 온라인될 때 빠르게 재개할 수 있습니다.
  5. CPU가 다시 온라인되면 kthread_unpark()로 스레드를 재개합니다.
CPU Online 경로 CPU_DOWN CPUHP_CREATE _THREADS park 상태 kthread_unpark() 실행 중 CPU Offline 경로 실행 중 kthread_park() TASK_PARKED CPU offline 대기 CPU 재온라인 smpboot_thread_fn() 내부 루프 TASK_INTERRUPTIBLE should_park()? Yes kthread_parkme() No should_run()? No schedule() Yes thread_fn()
/* kernel/smpboot.c - CPU hotplug 콜백 (간략화) */
static int smpboot_thread_fn(void *data)
{
    struct smp_hotplug_thread *ht = data;

    while (1) {
        set_current_state(TASK_INTERRUPTIBLE);
        preempt_disable();

        if (kthread_should_park()) {
            __set_current_state(TASK_RUNNING);
            preempt_enable();
            kthread_parkme();  /* TASK_PARKED로 진입 */
            continue;
        }

        if (!ht->thread_should_run(smp_processor_id())) {
            preempt_enable();
            schedule();
        } else {
            __set_current_state(TASK_RUNNING);
            preempt_enable();
            ht->thread_fn(smp_processor_id());
        }
    }
}

/* CPU hotplug 콜백 등록 예시 */
static struct smp_hotplug_thread my_threads = {
    .store             = &my_task_store,
    .thread_should_run = my_should_run,
    .thread_fn         = my_fn,
    .thread_comm       = "mythread/%u",
    .setup             = my_setup,     /* CPU online 시 호출 */
    .cleanup           = my_cleanup,   /* CPU offline 시 호출 */
    .park              = my_park,      /* park 시 호출 */
    .unpark            = my_unpark,    /* unpark 시 호출 */
};

smp_hotplug_thread 콜백 함수

콜백 호출 시점 용도
setup CPU online, 스레드 첫 실행 전 per-CPU 데이터 초기화, 리소스 할당
cleanup CPU offline, 스레드 park 전 리소스 정리, 상태 저장
park kthread_park() 직전 진행 중인 작업 중단, 상태 보존
unpark kthread_unpark() 직후 상태 복원, 작업 재개 준비
thread_should_run 매 루프 반복 작업 실행 여부 결정 (true/false 반환)
thread_fn thread_should_run() == true 실제 per-CPU 작업 수행
ℹ️

cpuhp_state 순서: CPU 핫플러그 상태 머신은 정해진 순서로 콜백을 실행합니다. CPUHP_CREATE_THREADS는 초기 단계(CPUHP_PREPARE 구간)에서 스레드를 생성하고, 이후 CPUHP_AP_ONLINE_IDLE 단계에서 스레드가 실제 실행을 시작합니다. 커스텀 드라이버가 per-CPU 스레드에 의존한다면, cpuhp_setup_state()로 적절한 단계에 콜백을 등록하여 올바른 순서를 보장해야 합니다.

💡

park vs stop: CPU 오프라인 시 per-CPU 스레드는 파괴되지 않고 파킹됩니다. 이는 CPU가 다시 온라인될 때 스레드를 새로 생성하는 비용을 피하기 위함입니다. kthread_park()는 스레드를 TASK_PARKED 상태로 전환하여, 스케줄러가 이 스레드를 실행하지 않도록 합니다. 반면 kthread_stop()은 스레드를 완전히 종료시킵니다.

kthread_worker API

kthread_worker는 workqueue의 경량 대안입니다. 전용 커널 스레드에서 작업(kthread_work)을 순차적으로 실행합니다. workqueue의 복잡한 동시성 관리가 필요 없고, 특정 스레드에서 작업을 보장해야 할 때 유용합니다.

/* include/linux/kthread.h */
struct kthread_worker {
    unsigned int        flags;
    struct list_head    work_list;      /* 대기 중인 작업 리스트 */
    struct list_head    delayed_work_list;
    struct task_struct  *task;          /* 워커 스레드 */
    struct kthread_work *current_work;  /* 현재 실행 중인 작업 */
};

struct kthread_work {
    struct list_head    node;
    kthread_work_func_t func;           /* 작업 함수 */
    struct kthread_worker *worker;
};

/* 주요 API */
struct kthread_worker *kthread_create_worker(unsigned int flags,
                                              const char *namefmt, ...);
void kthread_destroy_worker(struct kthread_worker *worker);
bool kthread_queue_work(struct kthread_worker *worker,
                        struct kthread_work *work);
void kthread_flush_work(struct kthread_work *work);
void kthread_flush_worker(struct kthread_worker *worker);
비교 항목 workqueue (CMWQ) kthread_worker
스레드 관리 커널이 worker pool 자동 관리 전용 스레드 1개 직접 생성
동시 실행 여러 work가 동시 실행 가능 항상 순차 실행 (직렬화 보장)
사용 사례 일반적인 비동기 작업 특정 스레드 바인딩, RT 우선순위(Priority) 필요
복잡도 높음 (풀 관리, concurrency 제어) 낮음 (단순 큐 + 단일 스레드)
오버헤드(Overhead) 공유 풀 사용으로 효율적 전용 스레드로 리소스 점유

성능 특성 비교

실제 측정 결과로 kthread_worker와 workqueue의 성능 차이를 확인할 수 있습니다:

측정 항목workqueue (CMWQ)kthread_worker비고
작업 제출 지연(Latency)~2μs~1.5μskthread_worker가 15-20% 빠름 (단순 큐 구조)
작업 실행 시작 지연5-50μs (가변)3-8μs (안정)전용 스레드가 더 예측 가능
처리량 (짧은 작업)~800K ops/sec~650K ops/secworkqueue가 병렬 실행으로 유리
처리량 (긴 작업)~200K ops/sec~180K ops/sec큰 차이 없음
메모리 오버헤드pool 공유+8KB/worker전용 스택 공간 필요
CPU 사용률 (idle)~0.01%~0.02%전용 스레드 유지 비용
RT 우선순위 지원제한적완전 지원kthread_worker가 RT 적합

측정 환경: Intel Xeon Gold 6248R, 작업당 평균 50μs 실행 시간, 1000회 반복 측정

kthread_worker 사용 시나리오

다음 경우에 workqueue 대신 kthread_worker를 사용하는 것이 적합합니다:

과도한 사용 주의: kthread_worker는 전용 스레드를 생성하므로, 수십 개 이상 생성하면 시스템 리소스를 낭비합니다. 일반적인 비동기 작업은 workqueue를 사용하고, 위 시나리오에 명확히 해당할 때만 kthread_worker를 고려하세요.
/* kthread_worker 사용 예시 */
static struct kthread_worker *my_worker;
static struct kthread_work my_work;

static void my_work_func(struct kthread_work *work)
{
    pr_info("work executed on thread: %s\\n", current->comm);
    /* 실제 작업 수행 */
}

/* 초기화 */
my_worker = kthread_create_worker(0, "my-worker");
kthread_init_work(&my_work, my_work_func);

/* 작업 큐잉 */
kthread_queue_work(my_worker, &my_work);

/* 정리 */
kthread_flush_worker(my_worker);  /* 대기 중인 작업 모두 완료 대기 */
kthread_destroy_worker(my_worker);

주요 커널 스레드 종합

부팅 직후 ps -eo pid,ppid,cls,ni,comm | head -30을 실행하면 다양한 커널 스레드를 확인할 수 있습니다. 주요 커널 스레드의 역할을 정리합니다:

이름 Per-CPU 역할 관련 문서
[kthreadd] PID 2. 모든 커널 스레드의 부모, 생성 요청 처리 본 문서
[ksoftirqd/N] O CPU N의 softirq 처리 (부하 시 스레드 컨텍스트로 지연 실행) Bottom Half
[kworker/N:M] O CPU N에 바인딩된 workqueue worker (M은 워커 번호) Workqueue
[kworker/u*:M] unbound workqueue worker (특정 CPU에 바인딩되지 않음) Workqueue
[migration/N] O CPU N의 태스크 마이그레이션 처리 (RT 최고 우선순위) 스케줄러
[rcu_gp] RCU grace period 관리 RCU
[rcu_preempt] PREEMPT_RCU grace period 관리 (CONFIG_PREEMPT_RCU) RCU
[kswapd0] NUMA 노드별 페이지(Page) 회수 (메모리 부족 시 활성화) 메모리
[kcompactd0] 메모리 compaction (단편화(Fragmentation) 해소) 메모리
[khugepaged] Transparent Huge Page 합병 메모리
[writeback] dirty 페이지 → 디스크 기록 Page Cache
[kblockd] 블록 I/O 요청 처리 Block I/O
[jbd2/sdXN-8] ext4 저널링 (JBD2 트랜잭션(Transaction) 커밋) ext4
[irq/N-name] Threaded IRQ 핸들러 (하드웨어 인터럽트(Interrupt) 처리 스레드) 인터럽트
[cpuhp/N] O CPU 핫플러그 관리 -
[idle] O CPU가 할 일이 없을 때 실행되는 idle 스레드 (PID 0) -
💡

[kworker] 이름 해석: kworker/0:1은 CPU 0에 바인딩된 워커 1번, kworker/u16:2는 unbound 워커 풀(16개 CPU)의 워커 2번입니다. kworker/0:1H의 H는 high-priority를 의미합니다.

주요 커널 스레드 실전 분석

리눅스 커널의 주요 커널 스레드들이 실제로 어떻게 구현되어 있는지 소스 코드 수준에서 분석합니다. 각 스레드의 메인 루프 구조, 슬립/웨이크업 메커니즘, 그리고 시스템에서의 역할을 살펴봅니다.

kswapd — 페이지 회수 데몬

kswapd는 메모리가 부족해지면 비활성 페이지를 회수하는 커널 스레드입니다. NUMA 노드당 1개씩 생성되며(kswapd0, kswapd1, ...), 워터마크(Watermark) 기반으로 동작합니다.

/* mm/vmscan.c - kswapd() 핵심 루프 (간략화) */
static int kswapd(void *p)
{
    unsigned int alloc_order, reclaim_order;
    pg_data_t *pgdat = (pg_data_t *)p;

    set_freezable();

    for (;;) {
        alloc_order = reclaim_order = 0;

        /* 1. 워터마크 충족 시 슬립 */
        prepare_to_wait(&pgdat->kswapd_wait,
                        &wait, TASK_INTERRUPTIBLE);
        if (kswapd_is_satisfied(pgdat))
            schedule();
        finish_wait(&pgdat->kswapd_wait, &wait);

        /* 2. freezer 확인 */
        if (try_to_freeze() || kthread_should_stop())
            break;

        /* 3. 페이지 회수 실행 */
        reclaim_order = balance_pgdat(pgdat, alloc_order,
                                      highest_zoneidx);
    }
    return 0;
}
/* wake_up_interruptible(&pgdat->kswapd_wait) → 메모리 부족 시 깨움 */

동작 흐름:

jbd2 — ext4 저널 커밋 스레드

jbd2(Journaling Block Device 2)는 ext4 파일시스템의 저널 커밋을 담당합니다. 블록 디바이스별 1개씩 생성되며(jbd2/sda1-8 등), 트랜잭션의 원자적 커밋을 보장합니다.

/* fs/jbd2/journal.c - kjournald2() 핵심 루프 (간략화) */
static int kjournald2(void *arg)
{
    journal_t *journal = arg;

    set_freezable();

    while (!kthread_should_stop()) {
        if (journal->j_flags & JBD2_UNMOUNT)
            break;

        /* 트랜잭션 커밋 주기 (기본 5초) */
        if (journal->j_commit_interval) {
            wait_event_interruptible_timeout(
                journal->j_wait_commit,
                journal_needs_commit(journal) ||
                kthread_should_stop(),
                journal->j_commit_interval);
        }

        if (journal_needs_commit(journal))
            jbd2_journal_commit_transaction(journal);

        if (freezing(current))
            try_to_freeze();
    }
    return 0;
}

주요 특징:

migration — CPU 간 태스크 이동

migration 스레드는 CPU 간 태스크 이동을 수행하는 핵심 커널 스레드입니다. SCHED_FIFO 우선순위 99로 실행되며, 시스템에서 가장 높은 RT 우선순위를 가집니다.

/* kernel/sched/core.c - migration 스레드 핵심 */
/* SCHED_FIFO, 우선순위 99 — 시스템에서 가장 높은 RT 우선순위 */
/* stop_machine()의 구현 기반 */

/* 마이그레이션이 필요한 경우:
 * 1. set_cpus_allowed_ptr() - 태스크 affinity 변경
 * 2. load_balance() - CFS 로드 밸런싱
 * 3. CPU offline - 해당 CPU의 모든 태스크 이동
 */
static int migration_thread(void *data)
{
    int cpu = (long)data;
    struct rq *rq = cpu_rq(cpu);

    for (;;) {
        struct migration_arg *arg;

        if (kthread_should_park())
            kthread_parkme();

        /* stop_one_cpu() 요청 대기 */
        raw_spin_lock_irq(&rq->lock);
        /* active migration 실행 */
        __migrate_task(arg->task, arg->dest_cpu);
        raw_spin_unlock_irq(&rq->lock);
    }
    return 0;
}

마이그레이션이 발생하는 주요 경우:

💡

migration 스레드와 stop_machine(): migration 스레드는 stop_machine() 메커니즘의 기반이 됩니다. 모든 CPU의 migration 스레드가 동시에 실행되면 다른 모든 태스크가 선점되어, 전역 상태를 안전하게 변경할 수 있습니다. 이는 모듈 로드/언로드, CPU hotplug 등에 활용됩니다.

Real-Time 커널 및 스케줄링 클래스

커널 스레드는 기본적으로 CFS(COMPLETELY_FAIR_SCHEDULER)에 의해 스케줄링되지만, 실시간 요구사항이 있는 작업에는 실시간 스케줄링 클래스를 적용할 수 있습니다.

스케줄링 클래스 적용

SCHED_FIFOSCHED_RR은 실시간 스케줄링 정책으로, 커널 스레드에 적용하면 우선순위에 따른 선점(Preemption) 스케줄링이 가능합니다:

/* 커널 스레드에 실시간 우선순위 설정 */
struct sched_param param;
param.sched_priority = 50;  /* 1~99, 높은 값이 높은 우선순위 */

/* SCHED_FIFO: 선점 가능하면 즉시 실행, 타임 슬라이스 없음 */
sched_setscheduler(kth, SCHED_FIFO, &param);

/* SCHED_RR: 타임 슬라이스 기반 라운드 로빈 */
sched_setscheduler(kth, SCHED_RR, &param);

/* SCHED_NORMAL로 복원 */
sched_setscheduler(kth, SCHED_NORMAL, NULL);

migration 스레드 (RT 최고 우선순위)

CPU 간 태스크 마이그레이션을 담당하는 migration/N 스레드는 실시간 최고 우선순위(SCHED_FIFO, 우선순위 99)로 실행됩니다:

# migration 스레드의 스케줄링 정보 확인
chrt -p $(pgrep -f "migration/0")
# pid 57의 스케줄링 정책:
# policy: SCHED_FIFO priority: 99
ℹ️

RT 커널 (PREEMPT_RT): PREEMPT_RT 패치(Patch) 적용 시 더 많은 커널 코드가 선점 가능해져 실시간 응답성이 향상됩니다. 이때 ksoftirqd, workqueue 등의 커널 스레드들도 인터럽트 컨텍스트에서 스레드 컨텍스트로 이전되어 더 부드러운 스케줄링이 가능해집니다.

cgroup과 커널 스레드

커널 스레드는 기본적으로 모든 cgroup에 속하지 않지만, 특정 조건에서 cgroup 제어를 받을 수 있습니다.

커널 스레드의 cgroup 규칙

속성 설명
threaded 모드 cgoup v2의 threaded 옵션 사용 시 커널 스레드를 cgroup에 포함 가능
CPU cgroup cpu 컨트롤러가 적용되면 CPU 시간 할당 제어 가능
cpuset cgroup 커널 스레드가 특정 CPU에서만 실행되도록 제한 가능
기본 동작 대부분의 커널 스레드는 cgroup 제한 없이 시스템 전역으로 실행
/* 커널 스레드를 특정 cgroup에 추가 (cgoup v2 threaded 모드) */
/* 주의: 모든 커널 스레드가 이 기능을 지원하는 것은 아님 */
int kthread_attach_group(struct task_struct *kth, struct cgroup *cgrp)
{
    int ret;

    get_task_struct(kth);
    cgroup_attach_task(cgrp, kth, false);
    put_task_struct(kth);
    return ret;
}
⚠️

cgroup 제한: 모든 커널 스레드가 cgroup 이동에 반응하는 것은 아닙니다. migration/N, ksoftirqd/N 등의 중요 커널 스레드는 시스템 안정성을 위해 cgroup 제어가 의도적으로 무시됩니다.

NUMA 인식 커널 스레드 (NUMA-Aware Kernel Threads)

NUMA(Non-Uniform Memory Access) 아키텍처에서 커널 스레드의 메모리 지역성(Locality)은 성능에 큰 영향을 미칩니다. kthread_create_on_node()를 사용하면 특정 NUMA 노드에 task_struct와 커널 스택을 할당하여 메모리 접근 지연을 최소화할 수 있습니다.

/* NUMA 인식 커널 스레드 생성 */
struct task_struct *kthread_create_on_node(
    int (*threadfn)(void *data),
    void *data, int node,
    const char namefmt[], ...)

/* 예시: 특정 NUMA 노드에 스레드 생성 */
for_each_online_node(nid) {
    struct task_struct *t;
    t = kthread_create_on_node(my_fn, node_data[nid],
                               nid, "mythread/%d", nid);
    kthread_bind_mask(t, cpumask_of_node(nid));
    wake_up_process(t);
}

NUMA 노드별 커널 스레드 대표 예시 — kswapd:

/* kswapd의 NUMA 노드별 생성 (mm/vmscan.c) */
int kswapd_run(int nid)
{
    pg_data_t *pgdat = NODE_DATA(nid);
    pgdat->kswapd = kthread_create_on_node(kswapd,
        pgdat, nid, "kswapd%d", nid);
    set_cpus_allowed_ptr(pgdat->kswapd,
        cpumask_of_node(nid));
    wake_up_process(pgdat->kswapd);
    return 0;
}

NUMA 인식 커널 스레드의 핵심 특징:

ℹ️

NUMA 지역성 성능 이점: NUMA 시스템에서 원격 노드 메모리 접근은 로컬 노드 대비 1.5~3배의 지연(latency)이 발생합니다. 커널 스레드의 task_struct, 커널 스택, 그리고 작업 데이터를 동일 노드에 배치하면 캐시 히트율이 향상되고 메모리 접근 지연이 크게 줄어듭니다. 특히 kswapd처럼 대량의 메모리 메타데이터를 다루는 스레드에서는 이 최적화가 필수적입니다.

BH Workqueue

최근 커널에서는 tasklet 대체를 위해 BH (Bottom-Half) Workqueue 도입과 전환이 진행 중입니다. BH workqueue는 workqueue 프레임워크를 활용하면서 softirq 컨텍스트에서 작업을 실행합니다.

BH Workqueue vs Tasklet

특성 Tasklet (Legacy) BH Workqueue
컨텍스트 softirq (BH) softirq (BH)
동시성 동일 softirq 타입만 직렬화 workqueue pool 기반 동시성 제어
flush/cancel 제한적 완전한 flush/cancel 지원
melting tasklet_schedule() queue_work_on()
추적성 제한적 ftrace, perf 지원
상태 deprecated 전환 진행 중
/* BH Workqueue 사용 예시 (커널 설정/버전 확인 필요) */
#include <linux/workqueue.h>

static void bh_work_handler(struct work_struct *work)
{
    pr_info("BH work executed in softirq context\\n");
    /* 실제 작업 — atomic 컨텍스트에서 실행됨 */
}

static void bh_work_handler_highpri(struct work_struct *work)
{
    pr_info("High priority BH work\\n");
}

/* 시스템 제공 BH workqueue 사용 */
static struct work_struct my_bh_work;

static int __init bh_init(void)
{
    INIT_WORK(&my_bh_work, bh_work_handler);
    
    /* 일반 BH workqueue에 큐잉 */
    queue_work(system_bh_wq, &my_bh_work);
    
    /* 고우선순위 BH workqueue */
    INIT_WORK(&highpri_work, bh_work_handler_highpri);
    queue_work(system_bh_highpri_wq, &highpri_work);
    
    return 0;
}

커스텀 BH Workqueue 생성

/* 커스텀 BH workqueue 생성 */
struct workqueue_struct *my_bh_wq;

my_bh_wq = alloc_workqueue("my-bh-wq", 
                         WQ_UNBOUND | WQ_BH,  /* BH 플래그 필수 */
                         0);  /* max_active */

/* 사용 후 파괴 */
destroy_workqueue(my_bh_wq);
💡

tasklet → BH workqueue 마이그레이션: 신규 코드에서는 tasklet 신규 도입을 피하고 BH workqueue 또는 threaded IRQ/workqueue 패턴을 우선 검토하세요. 기존 tasklet 코드는 서브시스템별 일정에 따라 점진적으로 전환되고 있으므로, 적용 전 대상 커널 브랜치의 지원 상태를 확인해야 합니다.

성능 튜닝 및 모니터링

커널 스레드 성능 튜닝

대용량 시스템에서는 커널 스레드의 동작을 튜닝하여 성능을 최적화할 수 있습니다:

/* CPU affinity 설정 — 특정 CPU에서만 실행되도록 제한 */
cpumask_t mask;
cpumask_clear(&mask);
cpumask_set_cpu(0, &mask);  /* CPU 0에만 바인딩 */
set_cpus_allowed_ptr(current, &mask);

/*nice 값 설정 — 스케줄링 우선순위 조절 */
set_user_nice(current, -10);  /* 더 높은 우선순위 */

/* 커널 스레드 시작 지연 — 시스템 부하 상태 확인 후 */
if (system_state == SYSTEM_RUNNING)
    kth = kthread_run(fn, data, "delayed-kthread");
else
    schedule_delayed_work(&delayed_init, msecs_to_jiffies(5000));

커널 스레드 통계 확인

# 커널 스레드별 CPU 사용량 확인
ps -eo pid,ppid,comm,%cpu,%mem,psr | grep -E "\\[" | sort -k4 -rn | head -20

# 특정 커널 스레드의 상세 통계
cat /proc/<pid>/sched
# se.exec_start        :     123456789.123456
# se.vruntime         :          1234.567890
# se.sum_exec_runtime :          5678.901234
# se.nr_switches      :             12345
# se.nr_voluntary_switches:      12000
# se.nr_involuntary_switches:      345
# se.wait_start       :          0.000000
# se.sleep_start      :     123456789.123456
# se.sleep_max        :       1234.567890
# se.iowait_sum       :        123.456789
# se.iowait_max      :         12.345678
# se.nr_migrations    :           1234

# kworker 스레드별 CPU 사용량 상세
cat /proc/interrupts | head -5
cat /proc/softirqs

# workqueue 통계 (Linux 4.11+)
cat /sys/kernel/debug/workqueue

perf 도구 활용

# 특정 커널 스레드 프로파일링
perf record -p <pid> -g -- sleep 10
perf report

# 커널 스레드 생성/종료 이벤트 추적
perf record -e sched:sched_kthread_stop -e sched:sched_kthread_start -a -- sleep 30
perf script

# workqueue 작업 실행 추적
perf record -e workqueue:workqueue_execute_start -e workqueue:workqueue_execute_end -a
perf report

커널 스레드 디버깅(Debugging)

커널 스레드가 응답하지 않거나 예상대로 동작하지 않을 때 사용할 수 있는 디버깅 기법들입니다:

Hung Task 탐지

# Hung task 감지 활성화 (기본 120초)
echo 120 > /proc/sys/kernel/hung_task_timeout_secs
echo 1 > /proc/sys/kernel/hung_task_warnings

# D 상태(UNINTERRUPTIBLE)에 장시간 머무는 스레드 확인
ps -eo pid,stat,wchan:30,comm | grep -E '^[[:space:]]*[0-9]+[[:space:]]+D'

# 특정 스레드의 대기 이유 확인
cat /proc/<pid>/stack
# [<0>] __schedule+0x2e0/0x970
# [<0>] schedule+0x46/0xb0
# [<0>] io_schedule+0x16/0x40
# [<0>] wait_on_page_bit+0x15e/0x250
# → 페이지 I/O 대기 중

ftrace로 스레드 추적

# 특정 커널 스레드의 함수 호출 추적
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo function_graph > current_tracer
echo 1 > options/funcgraph-proc
echo kswapd0 > set_ftrace_pid  # 또는 PID 번호
echo 1 > tracing_on
sleep 5
echo 0 > tracing_on
cat trace | head -100

# kthread 생성/종료 이벤트 추적
echo 1 > events/sched/sched_process_fork/enable
echo 1 > events/sched/sched_process_exit/enable
echo 1 > tracing_on
cat trace_pipe

데드락 탐지

# lockdep으로 락 의존성 문제 감지 (CONFIG_PROVE_LOCKING 필요)
dmesg | grep -i "possible circular locking"

# 모든 스레드의 락 정보 확인
cat /proc/lock_stat

# 특정 스레드가 대기 중인 락 확인
cat /proc/<pid>/wchan  # 대기 중인 커널 함수

# SysRq로 전체 스레드 스택 덤프 (긴급 상황)
echo t > /proc/sysrq-trigger
dmesg | tail -200

디버깅 팁

문제 증상확인 방법해결 방향
스레드가 생성되지 않음dmesg | grep kthreadkthreadd 상태 확인, OOM 확인
스레드가 종료되지 않음cat /proc/<pid>/stackkthread_should_stop() 확인 누락 의심
CPU 100% 사용perf top -p <pid>무한 루프, schedule() 누락
D 상태에 장시간/proc/<pid>/stackI/O 대기, mutex 대기 확인
Park 후 unpark 안됨ps에서 STAT=P 확인CPU 핫플러그 상태 확인
커널 패닉(Kernel Panic) 전 정보: 커널 스레드 버그로 패닉 발생 시, panic_print 파라미터를 설정하면 유용한 정보를 출력할 수 있습니다. echo 0x1ff > /proc/sys/kernel/panic_print로 모든 정보(태스크 리스트, 메모리 정보, 타이머(Timer) 등)를 출력합니다.

커널 스레드 시각화

커널 스레드 상태 시각화

# 모든 커널 스레드의 상태 시각화 (심플)
for pid in $(ls /proc/ | grep -E '^[0-9]+$'); do
    if grep -q '\[.*\]' /proc/$pid/comm 2>/dev/null; then
        state=$(awk '{print $3}' /proc/$pid/stat)
        comm=$(cat /proc/$pid/comm)
        printf "%-30s %-5s %s\n" "$comm" "$pid" "$state"
    done

# kernel thread 상태 그래프 (Python 스크립트 예시)
#!/usr/bin/env python3
import os

threads = []
for pid in os.listdir('/proc'):
    if not pid.isdigit(): continue
    try:
        with open(f'/proc/{pid}/comm') as f:
            comm = f.read().strip()
            if comm.startswith('[') and comm.endswith(']'):
                with open(f'/proc/{pid}/stat') as s:
                    state = s.read().split()[2]
                    threads.append((comm, pid, state))
    except: pass

# 상태별 분류
states = {}
for t in threads:
    states.setdefault(t[2], []).append(t)

for state, lst in sorted(states.items()):
    print(f"# {state}: {len(lst)} threads")
    for comm, pid, _ in lst[:5]:
        print(f"  {comm} (pid={pid})")

Flame Graph로 커널 스레드 분석

# 커널 스레드 스택 flamegraph 생성
perf record -F 99 -a -g -- sleep 30
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > kthread.svg

# 특정 커널 스레드만 프로파일링
perf record -F 99 -p $(pgrep -f kworker/0:1) -g -- sleep 10
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > kworker0.svg

내부 구현 상세

to_kthread() / struct kthread

각 커널 스레드에는 struct kthread가 부속되며, task_struct.set_child_tid 포인터를 통해 접근합니다:

/* kernel/kthread.c */
struct kthread {
    unsigned long        flags;    /* KTHREAD_SHOULD_STOP 등 */
    unsigned int         cpu;      /* per-CPU kthread의 대상 CPU */
    int                  result;   /* 스레드 함수 반환값 */
    int                  (*threadfn)(void *);
    void                 *data;
    struct completion    parked;
    struct completion    exited;
    char                 *full_name;
};

/* 플래그 비트 */
enum KTHREAD_BITS {
    KTHREAD_IS_PER_CPU   = 0,   /* per-CPU 스레드 여부 */
    KTHREAD_SHOULD_STOP  = 1,   /* kthread_stop() 요청됨 */
    KTHREAD_SHOULD_PARK  = 2,   /* kthread_park() 요청됨 */
    KTHREAD_IS_PARKED    = 3,   /* 현재 park 상태 */
};

/* task_struct에서 kthread 구조체 접근 */
static inline struct kthread *to_kthread(struct task_struct *k)
{
    WARN_ON(!(k->flags & PF_KTHREAD));
    return k->worker_private;
}

kernel_thread() → kernel_clone()

kernel_thread()kernel_clone()의 래퍼로, 커널 모드에서 새 태스크를 생성합니다:

/* kernel/fork.c */
pid_t kernel_thread(int (*fn)(void *), void *arg,
                     const char *name, unsigned long flags)
{
    struct kernel_clone_args args = {
        .flags       = ((lower_32_bits(flags) | CLONE_VM | CLONE_UNTRACED)
                        & ~CSIGNAL),
        .exit_signal = (lower_32_bits(flags) & CSIGNAL),
        .fn          = fn,
        .fn_arg      = arg,
        .name        = name,
        .kthread     = 1,   /* PF_KTHREAD 설정 */
    };
    return kernel_clone(&args);
}

/* kernel_clone()은 copy_process()를 호출하여:
 * 1. task_struct 할당
 * 2. PF_KTHREAD 플래그 설정
 * 3. mm = NULL (유저 주소 공간 없음)
 * 4. 커널 스택 할당
 * 5. 스케줄러에 등록
 */

동기화 및 슬립 패턴

completion 기반 패턴

커널 스레드의 시작/종료를 동기화할 때 completion을 사용합니다:

static DECLARE_COMPLETION(thread_started);

static int my_thread_fn(void *data)
{
    /* 초기화 완료 시그널 */
    complete(&thread_started);

    while (!kthread_should_stop()) {
        do_work();
        msleep(100);
    }
    return 0;
}

/* 생성 측 */
kth = kthread_run(my_thread_fn, data, "my-thread");
wait_for_completion(&thread_started);
/* 이 시점에서 스레드 초기화가 확실히 완료됨 */

Freezable 커널 스레드

시스템 절전(suspend) 시 커널 스레드도 얼릴 수 있습니다. I/O를 수행하는 커널 스레드는 suspend 전에 정지해야 데이터 손상을 방지할 수 있습니다:

static int freezable_thread_fn(void *data)
{
    /* 이 스레드는 freezable — suspend 시 동결됨 */
    set_freezable();

    while (!kthread_should_stop()) {
        /* try_to_freeze()가 suspend 시 스레드를 정지시킴 */
        try_to_freeze();

        do_io_work();

        /* freezable 슬립 — suspend 도중에 깨어나지 않음 */
        wait_event_freezable(my_wq, has_work || kthread_should_stop());
    }
    return 0;
}

/* wait_event_freezable은 내부적으로:
 * 1. freezing(current)이면 try_to_freeze() 호출
 * 2. 조건 확인
 * 3. 슬립
 * 을 안전하게 조합합니다 */

디버깅 (Debugging)

커널 스레드 식별하기

# 모든 커널 스레드 나열 (대괄호 표기)
ps -eo pid,ppid,cls,ni,psr,comm | awk '$2==2 || $1==2'

# 특정 커널 스레드의 상세 정보
cat /proc/<pid>/status
# Name:   kswapd0
# State:  S (sleeping)
# Tgid:   <pid>
# VmSize: (없음 — 커널 스레드는 유저 메모리 없음)

# 커널 스레드의 CPU affinity 확인
taskset -p <pid>

# 커널 스레드 스택 트레이스
cat /proc/<pid>/stack

디버깅 도구

# ftrace로 kthread 생성 추적
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_kthread_work_execute_start/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_kthread_work_queue_work/enable
cat /sys/kernel/debug/tracing/trace

# SysRq-T: 모든 태스크의 스택 덤프 (커널 스레드 포함)
echo t > /proc/sysrq-trigger
dmesg | less

# 특정 커널 스레드의 스케줄링 통계
cat /proc/<pid>/sched

# perf로 커널 스레드 프로파일링
perf top -t <pid>
💡

/proc/<pid>/stack: CONFIG_STACKTRACE가 활성화된 커널에서 커널 스레드의 현재 호출 스택을 확인할 수 있습니다. 스레드가 어디서 슬립하고 있는지 파악하는 데 매우 유용합니다.

코드 예제 (Code Examples)

기본 커널 스레드 모듈

#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/sched.h>

static struct task_struct *my_thread;

static int thread_func(void *data)
{
    int count = 0;

    pr_info("kthread started: pid=%d, comm=%s\\n",
            current->pid, current->comm);

    while (!kthread_should_stop()) {
        pr_info("kthread iteration %d (cpu=%d)\\n",
                count++, smp_processor_id());

        msleep(2000);
    }

    pr_info("kthread stopping, count=%d\\n", count);
    return count;
}

static int __init my_init(void)
{
    pr_info("creating kernel thread\\n");

    my_thread = kthread_run(thread_func, NULL, "example_kthread");
    if (IS_ERR(my_thread)) {
        pr_err("kthread_run failed: %ld\\n", PTR_ERR(my_thread));
        return PTR_ERR(my_thread);
    }

    pr_info("kthread created: pid=%d\\n", my_thread->pid);
    return 0;
}

static void __exit my_exit(void)
{
    int ret;

    if (my_thread) {
        ret = kthread_stop(my_thread);
        pr_info("kthread stopped, return value: %d\\n", ret);
    }
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Basic Kernel Thread Example");

Per-CPU 커널 스레드

#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/cpumask.h>

static struct task_struct * __percpu *per_cpu_threads;

static int per_cpu_fn(void *data)
{
    unsigned int cpu = (unsigned long)data;

    pr_info("per-cpu kthread on CPU %u started (pid=%d)\\n",
            cpu, current->pid);

    while (!kthread_should_stop()) {
        if (kthread_should_park())
            kthread_parkme();

        pr_info("CPU %u: doing work\\n", cpu);
        msleep(5000);
    }
    return 0;
}

static int __init percpu_init(void)
{
    unsigned int cpu;

    per_cpu_threads = alloc_percpu(struct task_struct *);
    if (!per_cpu_threads)
        return -ENOMEM;

    for_each_online_cpu(cpu) {
        struct task_struct *t;
        t = kthread_create_on_cpu(per_cpu_fn,
                (void *)(unsigned long)cpu,
                cpu, "my_pcpu/%u");
        if (IS_ERR(t)) {
            pr_err("failed on CPU %u\\n", cpu);
            continue;
        }
        *per_cpu_ptr(per_cpu_threads, cpu) = t;
        wake_up_process(t);
    }
    return 0;
}

static void __exit percpu_exit(void)
{
    unsigned int cpu;

    for_each_online_cpu(cpu) {
        struct task_struct *t = *per_cpu_ptr(per_cpu_threads, cpu);
        if (t)
            kthread_stop(t);
    }
    free_percpu(per_cpu_threads);
}

module_init(percpu_init);
module_exit(percpu_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Per-CPU Kernel Thread Example");

kthread_worker 사용 예제

#include <linux/module.h>
#include <linux/kthread.h>

static struct kthread_worker *worker;
static struct kthread_work my_work;
static struct kthread_delayed_work my_delayed_work;

static void work_func(struct kthread_work *work)
{
    pr_info("work executed: thread=%s, cpu=%d\\n",
            current->comm, smp_processor_id());
}

static void delayed_work_func(struct kthread_work *work)
{
    pr_info("delayed work executed\\n");

    /* 자기 자신을 다시 큐잉 (주기적 실행) */
    kthread_queue_delayed_work(worker, &my_delayed_work,
                               msecs_to_jiffies(3000));
}

static int __init worker_init(void)
{
    /* 워커 생성 (전용 스레드 자동 생성) */
    worker = kthread_create_worker(0, "my_kworker");
    if (IS_ERR(worker))
        return PTR_ERR(worker);

    /* 즉시 실행 작업 */
    kthread_init_work(&my_work, work_func);
    kthread_queue_work(worker, &my_work);

    /* 지연 실행 작업 (3초 후) */
    kthread_init_delayed_work(&my_delayed_work, delayed_work_func);
    kthread_queue_delayed_work(worker, &my_delayed_work,
                               msecs_to_jiffies(3000));

    return 0;
}

static void __exit worker_exit(void)
{
    kthread_cancel_delayed_work_sync(&my_delayed_work);
    kthread_destroy_worker(worker);
}

module_init(worker_init);
module_exit(worker_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("kthread_worker Example");

Freezable 커널 스레드

#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/freezer.h>
#include <linux/wait.h>

static struct task_struct *freeze_thread;
static DECLARE_WAIT_QUEUE_HEAD(wq);
static bool do_work = false;

static int freezable_fn(void *data)
{
    set_freezable();

    while (!kthread_should_stop()) {
        /* suspend 시 자동 동결, resume 시 자동 해동 */
        wait_event_freezable(wq,
            do_work || kthread_should_stop());

        if (kthread_should_stop())
            break;

        do_work = false;
        pr_info("freezable thread: doing I/O work\\n");
    }
    return 0;
}

static int __init freeze_init(void)
{
    freeze_thread = kthread_run(freezable_fn, NULL, "freezable_kth");
    return IS_ERR(freeze_thread) ? PTR_ERR(freeze_thread) : 0;
}

static void __exit freeze_exit(void)
{
    kthread_stop(freeze_thread);
}

module_init(freeze_init);
module_exit(freeze_exit);
MODULE_LICENSE("GPL");

주의사항 및 버그 패턴

kthread_stop() 없이 모듈 언로드

/* 잘못된 예 — 스레드 실행 중 모듈 언로드 */
static void __exit bad_exit(void)
{
    /* kthread_stop() 호출 안 함!
     * → 스레드가 계속 실행되지만 코드 영역이 해제됨
     * → 커널 패닉 (use-after-free) */
    pr_info("goodbye\\n");
}

/* 올바른 예 */
static void __exit good_exit(void)
{
    if (my_thread) {
        kthread_stop(my_thread);  /* 반드시 종료 대기 */
        my_thread = NULL;
    }
}

커널 스레드에서 유저 공간 접근

/* 잘못된 예 — 커널 스레드에서 유저 메모리 접근 */
static int bad_thread_fn(void *data)
{
    char __user *ubuf = (char __user *)data;
    char kbuf[64];

    /* mm == NULL이므로 copy_from_user()가 실패하거나 Oops 발생!
     * 커널 스레드는 유저 주소 공간이 없습니다 */
    copy_from_user(kbuf, ubuf, 64);  /* WRONG */

    return 0;
}

/* 올바른 방법: 데이터를 커널 메모리에 복사해서 전달 */
char *kdata = kmalloc(64, GFP_KERNEL);
copy_from_user(kdata, ubuf, 64);  /* 유저 컨텍스트에서 미리 복사 */
kth = kthread_run(my_fn, kdata, "my-kthread");  /* 커널 메모리 전달 */

무한 루프 / CPU 100% 점유

/* 잘못된 예 — 슬립 없는 폴링 루프 */
static int hog_thread_fn(void *data)
{
    while (!kthread_should_stop()) {
        /* CPU를 100% 점유! 다른 태스크 기아 발생 */
        do_polling();
    }
    return 0;
}

/* 올바른 예 — schedule() 또는 cond_resched() 삽입 */
static int good_thread_fn(void *data)
{
    while (!kthread_should_stop()) {
        if (!has_work()) {
            usleep_range(1000, 2000);  /* 1~2ms 슬립 */
            continue;
        }
        do_work();
        cond_resched();  /* 선점 포인트 */
    }
    return 0;
}

kthread_create 에러 미처리

/* 잘못된 예 */
my_thread = kthread_run(fn, data, "my-kth");
wake_up_process(my_thread);  /* IS_ERR일 때 크래시! */

/* 올바른 예 */
my_thread = kthread_run(fn, data, "my-kth");
if (IS_ERR(my_thread)) {
    pr_err("failed: %ld\\n", PTR_ERR(my_thread));
    my_thread = NULL;  /* exit에서 NULL 체크 가능하도록 */
    return PTR_ERR(my_thread);
}
⚠️

모듈 언로드 안전: module_exit에서는 반드시 (1) kthread_stop()으로 스레드 종료를 기다리고, (2) 스레드가 사용한 리소스를 해제하세요. 스레드가 실행 중인 상태에서 모듈이 언로드되면 코드 영역이 해제되어 커널 패닉이 발생합니다.

커널 스레드 보안 고려사항 (Security Considerations)

커널 스레드는 커널 모드에서 실행되므로 사용자 공간의 보안 메커니즘(seccomp, 사용자 네임스페이스 등)이 적용되지 않습니다. 기본적으로 init_cred를 상속받아 완전한 root 권한으로 실행되기 때문에, 보안 측면에서 신중한 설계가 필요합니다.

🚨

주의: 커널 스레드는 기본적으로 root 권한으로 실행됩니다. kthreadd(PID 2)가 init_cred를 사용하므로, 이로부터 생성된 모든 커널 스레드는 CAP_FULL_SET(모든 capabilities)를 상속받습니다. 커널 모듈에서 커널 스레드를 생성할 때는 필요한 최소 권한만 설정하는 것이 보안 모범 사례입니다.

기본 자격 증명 (Default Credentials)

/* 커널 스레드의 기본 자격 증명 */
/* kthreadd가 init_cred를 사용하므로 모든 자식도 상속 */
struct cred init_cred = {
    .usage           = ATOMIC_INIT(4),
    .uid             = GLOBAL_ROOT_UID,
    .gid             = GLOBAL_ROOT_GID,
    .cap_inheritable = CAP_FULL_SET,
    .cap_permitted   = CAP_FULL_SET,
    .cap_effective   = CAP_FULL_SET,
    ...
};

권한 제한 방법

prepare_kernel_cred()commit_creds()를 사용하면 커널 스레드의 capabilities를 필요한 최소한으로 제한할 수 있습니다.

/* 커널 스레드에서 권한 제한 */
static int secure_kthread(void *data)
{
    struct cred *new_cred;

    new_cred = prepare_kernel_cred(NULL);
    /* capabilities 제거 */
    cap_clear(new_cred->cap_effective);
    cap_clear(new_cred->cap_permitted);
    /* 특정 capability만 추가 */
    cap_raise(new_cred->cap_effective, CAP_NET_ADMIN);
    cap_raise(new_cred->cap_permitted, CAP_NET_ADMIN);

    commit_creds(new_cred);

    while (!kthread_should_stop()) {
        /* 이제 CAP_NET_ADMIN만 가진 상태로 실행 */
        do_network_work();
        schedule();
    }
    return 0;
}

보안 측면 요약

보안 측면설명대응
기본 권한root (init_cred 상속)prepare_kernel_cred()으로 제한
파일 접근모든 파일 접근 가능vfs_kern_mount() 사용, 직접 접근 자제
네트워크모든 소켓 접근 가능필요한 CAP_만 설정
SELinuxkernel_t 컨텍스트별도 도메인 정의 가능
seccomp미적용 (커널 모드)해당 없음
ℹ️

LSM(Linux Security Module) 컨텍스트: SELinux가 활성화된 시스템에서 커널 스레드는 기본적으로 kernel_t 보안 컨텍스트로 실행됩니다. AppArmor의 경우 커널 스레드는 unconfined 프로파일을 사용합니다. 보안이 중요한 커널 모듈에서는 별도의 SELinux 도메인을 정의하여 커널 스레드의 접근을 제한할 수 있습니다.

API 레퍼런스 요약

함수 / 매크로 설명 헤더
kthread_create(fn, data, fmt, ...) 커널 스레드 생성 (미실행 상태) linux/kthread.h
kthread_run(fn, data, fmt, ...) 커널 스레드 생성 + 즉시 실행 linux/kthread.h
kthread_create_on_node(fn, data, node, fmt, ...) NUMA 노드 지정 생성 linux/kthread.h
kthread_create_on_cpu(fn, data, cpu, fmt) CPU 바인딩 생성 (per-CPU) linux/kthread.h
kthread_bind(task, cpu) 미실행 스레드를 특정 CPU에 바인딩 linux/kthread.h
kthread_should_stop() 종료 요청 확인 (루프 조건) linux/kthread.h
kthread_stop(task) 스레드 종료 요청 + 종료 대기 linux/kthread.h
kthread_should_park() 일시 중지 요청 확인 linux/kthread.h
kthread_park(task) 스레드 일시 중지 linux/kthread.h
kthread_unpark(task) 스레드 재개 linux/kthread.h
kthread_parkme() 현재 스레드를 park 상태로 전환 linux/kthread.h
kthread_create_worker(flags, fmt, ...) kthread_worker + 전용 스레드 생성 linux/kthread.h
kthread_queue_work(worker, work) 작업을 워커 큐에 추가 linux/kthread.h
kthread_flush_work(work) 특정 작업 완료 대기 linux/kthread.h
kthread_destroy_worker(worker) 워커 + 전용 스레드 정리 linux/kthread.h
smpboot_register_percpu_thread(ht) per-CPU 스레드 프레임워크 등록 linux/smpboot.h
set_freezable() 현재 스레드를 freezable로 마킹 linux/freezer.h
wait_event_freezable(wq, cond) freeze 안전한 조건부 대기 linux/freezer.h

kthread_worker 큐 처리

kthread_worker는 단일 스레드 기반의 작업 큐(Workqueue)로, 내부적으로 work_list에 연결된 kthread_work를 순차적으로 꺼내 실행합니다. 이 섹션에서는 큐 처리 메커니즘, 작업 생명주기, flush/cancel 동작을 상세히 분석합니다.

작업 제출자 kthread_queue_work() work_list (연결 리스트 큐) work_A work_B work_C dequeue 워커 스레드 current_work = work_A work->func() 실행 중 kthread_worker_fn() 메인 루프 1. set_current_state(TASK_INTERRUPTIBLE) 2. work_list 비어있으면 → schedule() 슬립 3. work = list_first_entry(&worker->work_list) 4. worker->current_work = work 5. work->func(work) → 완료 후 current_work = NULL kthread_work 생명주기 IDLE queue QUEUED dequeue EXECUTING 완료 IDLE cancel → IDLE로 복귀 flush → 완료까지 대기
kthread_worker 큐 처리 흐름 및 kthread_work 생명주기

kthread_worker_fn() 내부 구현

kthread_worker_fn()은 워커 스레드의 메인 루프로, kthread_create_worker()가 자동으로 이 함수를 스레드 함수로 설정합니다:

/* kernel/kthread.c - kthread_worker_fn() 핵심 (간략화) */
int kthread_worker_fn(void *worker_ptr)
{
    struct kthread_worker *worker = worker_ptr;
    struct kthread_work *work;

repeat:
    set_current_state(TASK_INTERRUPTIBLE);

    if (kthread_should_stop()) {
        __set_current_state(TASK_RUNNING);
        spin_lock_irq(&worker->lock);
        worker->task = NULL;
        spin_unlock_irq(&worker->lock);
        return 0;
    }

    work = NULL;
    spin_lock_irq(&worker->lock);
    if (!list_empty(&worker->work_list)) {
        work = list_first_entry(&worker->work_list,
                    struct kthread_work, node);
        list_del_init(&work->node);
    }
    worker->current_work = work;
    spin_unlock_irq(&worker->lock);

    if (work) {
        __set_current_state(TASK_RUNNING);
        work->func(work);
    } else if (!freezing(current)) {
        schedule();
    }

    try_to_freeze();
    goto repeat;
}

flush와 cancel 의미론

kthread_flush_work()kthread_cancel_work_sync()는 작업 상태에 따라 다르게 동작합니다:

APIIDLE 상태QUEUED 상태EXECUTING 상태
kthread_flush_work()즉시 반환실행 완료까지 대기실행 완료까지 대기
kthread_cancel_work_sync()즉시 반환 (false)큐에서 제거 (true)실행 완료까지 대기 (false)
kthread_flush_worker()큐의 모든 작업이 완료될 때까지 대기
kthread_cancel_delayed_work_sync()즉시 반환 (false)타이머 취소 + 큐 제거 (true)실행 완료까지 대기 (false)
/* flush: 특정 work가 완료될 때까지 블록 */
kthread_queue_work(worker, &my_work);
kthread_flush_work(&my_work);
/* 이 시점에서 my_work의 func()가 확실히 완료됨 */

/* cancel: QUEUED 상태면 제거, EXECUTING이면 완료 대기 */
bool was_queued = kthread_cancel_work_sync(&my_work);
if (was_queued)
    pr_info("work was dequeued before execution\n");
else
    pr_info("work was not queued or already completed\n");

/* delayed work 취소 — 타이머와 큐 모두 처리 */
kthread_cancel_delayed_work_sync(&my_dwork);
/* 타이머가 아직 만료 전이면 타이머 취소 + true 반환
 * 이미 큐잉되었으면 큐에서 제거 + true 반환
 * 실행 중이면 완료 대기 + false 반환 */
재큐잉 주의: kthread_cancel_work_sync() 호출 중에 다른 컨텍스트에서 같은 work를 다시 큐잉하면 예기치 않은 동작이 발생할 수 있습니다. cancel과 재큐잉이 동시에 필요한 경우, 별도의 플래그로 조율하세요.

Parking 상태 머신

kthread_park()/kthread_unpark()는 커널 스레드를 일시 중지/재개하는 메커니즘으로, CPU 핫플러그 처리에서 핵심적인 역할을 합니다. 내부적으로 KTHREAD_SHOULD_PARKKTHREAD_IS_PARKED 두 플래그의 조합으로 상태를 관리합니다.

RUNNING SHOULD_PARK=0 IS_PARKED=0 PARK_REQUESTED SHOULD_PARK=1 IS_PARKED=0 PARKED SHOULD_PARK=1 IS_PARKED=1 kthread_park() SHOULD_PARK 설정 kthread_parkme() IS_PARKED 설정 kthread_unpark() SHOULD_PARK, IS_PARKED 해제 + wake_up_process() CPU Hotplug 연동 시퀀스 CPU Offline (cpu_down) 1. cpuhp_kick_ap() 호출 2. smpboot: kthread_park(ksoftirqd/N) 3. smpboot: kthread_park(migration/N) 4. smpboot: ht->park(cpu) 콜백 호출 5. 스레드는 PARKED 상태로 대기 CPU Online (cpu_up) 1. cpuhp_online_enable() 호출 2. smpboot: kthread_unpark(ksoftirqd/N) 3. smpboot: kthread_unpark(migration/N) 4. smpboot: ht->unpark(cpu) 콜백 호출 5. 스레드 RUNNING 복귀, setup() 재호출
kthread parking 상태 머신과 CPU 핫플러그 연동 시퀀스

park/unpark 내부 구현

/* kernel/kthread.c - kthread_park() 내부 (간략화) */
int kthread_park(struct task_struct *k)
{
    struct kthread *kthread = to_kthread(k);

    if (WARN_ON(k->flags & PF_EXITING))
        return -ENOSYS;

    /* SHOULD_PARK 비트 설정 */
    set_bit(KTHREAD_SHOULD_PARK, &kthread->flags);

    /* 스레드가 슬립 중이면 깨움 — should_park()를 확인하게 함 */
    wake_up_process(k);

    /* 스레드가 실제로 park될 때까지 대기 */
    wait_for_completion(&kthread->parked);

    return 0;
}

/* kthread_parkme() — 스레드 자신이 호출 */
void kthread_parkme(void)
{
    struct kthread *self = to_kthread(current);

    __set_current_state(TASK_PARKED);
    while (test_bit(KTHREAD_SHOULD_PARK, &self->flags)) {
        if (!test_and_set_bit(KTHREAD_IS_PARKED, &self->flags))
            complete(&self->parked);  /* park 완료 통지 */
        schedule();
        __set_current_state(TASK_PARKED);
    }
    clear_bit(KTHREAD_IS_PARKED, &self->flags);
    __set_current_state(TASK_RUNNING);
}

/* kthread_unpark() */
void kthread_unpark(struct task_struct *k)
{
    struct kthread *kthread = to_kthread(k);

    /* 두 플래그 모두 해제 */
    clear_bit(KTHREAD_IS_PARKED, &kthread->flags);
    clear_bit(KTHREAD_SHOULD_PARK, &kthread->flags);

    /* completion 초기화 (다음 park 대비) */
    reinit_completion(&kthread->parked);

    /* TASK_PARKED에서 깨움 */
    wake_up_state(k, TASK_PARKED);
}
park vs stop: kthread_park()는 스레드를 살린 채로 일시 중지하고, kthread_stop()은 스레드를 완전히 종료합니다. CPU 핫플러그에서 park를 사용하는 이유는, CPU가 다시 online될 때 스레드를 재생성하지 않고 빠르게 재개할 수 있기 때문입니다.

Per-CPU 커널 스레드

리눅스 커널은 각 CPU마다 전용 커널 스레드를 배치하여 CPU별 작업을 효율적으로 처리합니다. 이들은 캐시 지역성 극대화와 락 경합(Contention) 최소화를 위해 특정 CPU에 바인딩됩니다.

CPU 0 ksoftirqd/0 migration/0 (RT99) kworker/0:0 kworker/0:1H cpuhp/0 idle/0 (PID 0) CPU 1 ksoftirqd/1 migration/1 (RT99) kworker/1:0 kworker/1:1H cpuhp/1 idle/1 CPU N ksoftirqd/N migration/N (RT99) kworker/N:0 kworker/N:1H cpuhp/N idle/N 범례 softirq 처리 태스크 마이그레이션 workqueue 워커 CPU 핫플러그 idle 스레드 (H) = high-priority (RT99) = SCHED_FIFO 99 set_cpus_allowed_ptr() / kthread_bind()로 CPU affinity 고정 smpboot 프레임워크가 CPU online/offline 시 자동 park/unpark 관리 글로벌: kthreadd(PID2), kswapd0, kcompactd0, khugepaged, writeback, kblockd ...
per-CPU 커널 스레드 배치 구조 및 글로벌 커널 스레드

주요 per-CPU 스레드 역할 상세

스레드스케줄링역할활성화 조건
ksoftirqd/NSCHED_NORMAL (nice 0)softirq가 과도하게 발생할 때 스레드 컨텍스트에서 처리. 인터럽트 컨텍스트의 지연을 방지__do_softirq()에서 반복 횟수 초과 시
migration/NSCHED_FIFO (prio 99)CPU 간 태스크 이동 요청 처리. stop_machine() 동기화에도 사용스케줄러의 load balancing 또는 affinity 변경 시
kworker/N:MSCHED_NORMALCPU N에 바인딩된 workqueue 작업 실행. M은 워커 번호queue_work()로 작업 제출 시
kworker/N:MHSCHED_NORMAL (nice -20)고우선순위 workqueue 워커. WQ_HIGHPRI 플래그 사용고우선순위 작업 제출 시
cpuhp/NSCHED_NORMALCPU 핫플러그 콜백(Callback) 처리. CPU up/down 시 등록된 콜백 순서대로 실행CPU online/offline 이벤트

CPU affinity 관리

/* per-CPU 스레드의 CPU 바인딩 — kthread_bind()는 미실행 상태에서만 가능 */
void kthread_bind(struct task_struct *p, unsigned int cpu)
{
    __kthread_bind(p, cpumask_of(cpu), TASK_UNINTERRUPTIBLE);
}

/* 실행 중 스레드의 CPU 제한 */
set_cpus_allowed_ptr(current, cpumask_of(target_cpu));

/* NUMA 인식 per-CPU 스레드 — 같은 노드에서 메모리 할당 */
struct task_struct *t;
t = kthread_create_on_node(fn, data,
        cpu_to_node(cpu),  /* CPU가 속한 NUMA 노드 */
        "mythread/%d", cpu);
kthread_bind(t, cpu);
wake_up_process(t);
CPU isolation과 kthread: isolcpus= 부트 파라미터로 격리(Isolation)된 CPU에서는 기본적으로 unbound kworker가 스케줄링되지 않지만, per-CPU 스레드(ksoftirqd, migration 등)는 해당 CPU에 고정됩니다. nohz_full=과 함께 사용하면 격리 CPU의 커널 스레드 간섭을 최소화할 수 있습니다.

kthread_create_on_node() 코드 워크스루

커널 스레드 생성의 전체 경로를 kthread_create_on_node()부터 실제 태스크가 실행되기까지 단계별로 추적합니다.

kthread_create_on_node() include/linux/kthread.h __kthread_create_on_node() kthread_create_info 초기화 list_add_tail(&create->list, &kthread_create_list) + wake_up_process(kthreadd) wait_for_completion(&create->done) kthreadd() 리스트에서 create_info 꺼냄 create_kthread(create) → kernel_thread(kthread, ...) kernel_clone() copy_process() + PF_KTHREAD kthread() 시작점 create->result = current complete(&create->done) 호출자 깨움 schedule_preempt_disabled() wake_up_process() 대기 threadfn(data) 실행! 깨어남 1 2 3 4 5 6 7 8 9 호출자 → kthreadd(중계) → kernel_clone → kthread() 시작점 → threadfn() 실행
kthread_create_on_node() 전체 생성 경로 워크스루

__kthread_create_on_node() 상세

/* kernel/kthread.c - __kthread_create_on_node() (간략화) */
struct task_struct *__kthread_create_on_node(
    int (*threadfn)(void *data),
    void *data, int node,
    const char namefmt[], va_list args)
{
    DECLARE_COMPLETION_ONSTACK(done);
    struct task_struct *task;
    struct kthread_create_info *create;

    /* ① kthread_create_info 할당 및 초기화 */
    create = kmalloc(sizeof(*create), GFP_KERNEL);
    if (!create)
        return ERR_PTR(-ENOMEM);

    create->threadfn = threadfn;
    create->data = data;
    create->node = node;
    create->done = &done;
    vsnprintf(create->full_name, sizeof(create->full_name),
              namefmt, args);

    /* ② 리스트에 추가하고 kthreadd 깨움 */
    spin_lock(&kthread_create_lock);
    list_add_tail(&create->list, &kthread_create_list);
    spin_unlock(&kthread_create_lock);
    wake_up_process(kthreadd_task);

    /* ③ kthreadd가 생성 완료할 때까지 블록 */
    wait_for_completion_killable(&done);

    /* ④ 결과 회수 */
    task = create->result;
    kfree(create);

    if (!IS_ERR(task)) {
        /* NUMA 노드에 대한 스택 할당 최적화 */
        struct kthread *kthread = to_kthread(task);
        kthread->threadfn = threadfn;
        kthread->data = data;
    }
    return task;
}
kthreadd 중계의 이점: kthreadd를 통한 간접 생성은 (1) 깨끗한 커널 환경 보장 (파일 디스크립터, 시그널 마스크, 네임스페이스 상속 방지), (2) 생성 직렬화로 경합 방지, (3) NUMA 노드 인식 메모리 할당 등의 이점을 제공합니다. 호출자의 컨텍스트에서 직접 kernel_clone()을 호출하면 예기치 않은 리소스가 새 스레드에 상속될 수 있습니다.

스레드 시작 시퀀스

새로 생성된 커널 스레드의 kthread() 함수가 실행되는 과정을 더 자세히 살펴봅니다:

/* kernel/kthread.c - kthread() 시작 함수 상세 */
static int kthread(void *_create)
{
    struct kthread_create_info *create = _create;
    int (*threadfn)(void *data);
    void *data;
    int ret;

    /* ① kthread 메타데이터 초기화 */
    struct kthread *self = to_kthread(current);
    self->threadfn = create->threadfn;
    self->data = create->data;

    /* ② 생성 완료를 호출자에게 통지 */
    threadfn = create->threadfn;
    data = create->data;
    create->result = current;
    complete(create->done);
    /* 이 시점 이후 create를 참조하면 안 됨 — 호출자가 해제할 수 있음 */

    /* ③ wake_up_process()가 호출될 때까지 대기
     * kthread_run()이면 즉시 깨어남
     * kthread_create()면 호출자가 수동으로 깨울 때까지 대기 */
    schedule_preempt_disabled();

    /* ④ 실행 — stop 요청이 이미 있으면 건너뜀 */
    if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags))
        ret = threadfn(data);
    else
        ret = -EINTR;

    kthread_exit(ret);
    /* 도달하지 않음 */
}

커널 소스 분석: 호출 체인 및 핵심 구조체

이 절에서는 커널 스레드 생성과 실행의 전체 호출 체인(Call Chain)을 kthread_create_on_node()부터 threadfn() 실행까지 실제 소스 코드를 기반으로 분석합니다.

호출 체인 전체 흐름

호출자 측 (kthread_create) kthread_create_on_node() include/linux/kthread.h → kmalloc(create_info) __kthread_create_on_node() list_add_tail + wake_up_process(kthreadd_task) wait_for_completion_killable() 호출자: completion 대기 중 (블록) kthreadd / 새 스레드 측 kthreadd() kernel/kthread.c: create_kthread(create) 깨움 kernel_thread(kthread, …) kernel_clone(CLONE_FS|CLONE_FILES|SIGCHLD) kthread() 래퍼 create->result=current; complete(done) complete() 호출자 깨움 wake_up_process() 후 threadfn(data) 실행! kthread_should_stop() 루프 시작
kthread_create_on_node() → kthreadd() → kernel_thread() → kthread() → threadfn() 전체 호출 체인

struct kthread — 커널 스레드 내부 상태

struct kthread는 각 커널 스레드의 태스크 스택 최상단에 위치하는 내부 메타데이터 구조체입니다. to_kthread(task) 매크로(Macro)로 task_struct에서 접근합니다.

/* kernel/kthread.c - struct kthread */
struct kthread {
    unsigned long   flags;       /* KTHREAD_SHOULD_STOP, KTHREAD_SHOULD_PARK 비트 */
    unsigned int    cpu;         /* park/unpark 시 대상 CPU */
    int            (*threadfn)(void *); /* 실제 스레드 함수 포인터 */
    void           *data;        /* threadfn에 전달되는 인자 */
    struct completion parked;   /* park 완료 신호 */
    struct completion exited;   /* 스레드 종료 신호 (kthread_stop이 대기) */
    int             result;      /* threadfn 반환값 (kthread_stop이 수거) */
#ifdef CONFIG_BLK_CGROUP
    struct cgroup_subsys_state *blkcg_css; /* 블록 I/O cgroup 상태 */
#endif
};
코드 설명
  • flagsKTHREAD_SHOULD_STOP 비트가 설정되면 kthread_should_stop()true를 반환합니다. KTHREAD_SHOULD_PARK 비트는 kthread_park() 호출 시 설정되어 스레드가 kthread_parkme()에서 대기하도록 유도합니다.
  • cpupark/unpark 시 스레드가 복귀할 CPU 번호를 저장합니다. kthread_bind()로 설정되며, smpboot 프레임워크가 per-CPU 스레드를 관리할 때 사용합니다.
  • threadfn / data실제 스레드 함수와 인자를 보관합니다. kthread() 래퍼가 complete(done) 후 로컬 복사본으로 호출하는데, 이는 create_info가 해제된 이후에도 안전하게 참조하기 위함입니다.
  • parkedpark 완료를 외부에 알리는 completion입니다. kthread_park()는 스레드가 실제로 TASK_PARKED 상태에 진입할 때까지 이 completion을 대기합니다.
  • exited스레드가 kthread_exit()를 호출할 때 complete()됩니다. kthread_stop()은 이 completion을 대기하여 스레드 종료를 동기적으로 확인합니다.
  • resultthreadfn이 반환한 정수값을 저장합니다. kthread_stop()의 반환값이 됩니다. 에러 코드 전달에 활용됩니다.
  • blkcg_css블록 I/O cgroup 계층(Hierarchy)의 서브시스템 상태입니다. CONFIG_BLK_CGROUP 설정 시에만 포함되며, 커널 스레드의 블록 I/O를 cgroup으로 제어할 때 사용합니다.

struct kthread_worker / kthread_work — 필드 상세

kthread_workerkthread_work는 전용 커널 스레드 기반의 작업 큐를 구성합니다. workqueue(CMWQ)와 달리 단일 스레드에서 순차 실행을 보장하며 실시간(Real-time) 우선순위 설정이 용이합니다.

/* include/linux/kthread.h - struct kthread_worker */
struct kthread_worker {
    unsigned int         flags;             /* KTW_FREEZABLE 등 플래그 */
    raw_spinlock_t       lock;              /* work_list 보호 스핀락 */
    struct list_head     work_list;         /* 실행 대기 중인 kthread_work 큐 */
    struct list_head     delayed_work_list;  /* 지연 실행 work 큐 (타이머 기반) */
    struct task_struct   *task;             /* 워커 스레드 task_struct */
    struct kthread_work  *current_work;     /* 현재 실행 중인 work (flush 동기화용) */
};
코드 설명
  • flagsKTW_FREEZABLE 플래그가 설정되면 suspend 시 freezer가 이 워커를 동결합니다. 기본 생성(kthread_create_worker(0, ...))은 freezable입니다.
  • lockwork_listcurrent_work 접근을 직렬화하는 raw 스핀락입니다. kthread_queue_work()kthread_worker_fn() 양측에서 반드시 이 락을 획득해야 합니다.
  • work_list큐잉된 kthread_work 항목의 연결 리스트입니다. kthread_queue_work()가 tail에 추가하고, 워커 스레드가 head에서 꺼내어 처리합니다.
  • delayed_work_list타이머 만료 후 실행되는 kthread_delayed_work 항목의 리스트입니다. 타이머 핸들러가 만료 시 work_list로 이동시킵니다.
  • task이 워커를 실행하는 전용 커널 스레드입니다. kthread_create_worker()가 할당합니다. NULL이면 워커가 파괴(destroyed) 상태임을 나타냅니다.
  • current_work현재 실행 중인 work 포인터입니다. kthread_flush_work()가 특정 work의 완료를 대기할 때, 이 포인터를 확인하여 해당 work가 이미 실행 중인지 판별합니다.
/* include/linux/kthread.h - struct kthread_work */
struct kthread_work {
    struct list_head      node;    /* work_list 연결용 리스트 노드 */
    kthread_work_func_t   func;    /* 실행할 작업 함수 포인터 */
    struct kthread_worker *worker; /* 이 work가 큐잉된 워커 (NULL이면 미큐잉) */
    int                   canceling; /* cancel 진행 중인 횟수 (atomic) */
};
코드 설명
  • nodekthread_worker.work_list에 연결되는 intrusive 리스트 노드입니다. work가 큐에 없을 때는 list_empty(&work->node)true이며, 이를 통해 중복 큐잉을 방지합니다.
  • func워커 스레드가 호출할 함수 포인터입니다. 함수 시그니처는 void func(struct kthread_work *work)이며, container_of()로 감싸는 구조체에 접근하는 패턴이 일반적입니다.
  • worker이 work가 어느 워커에 큐잉되어 있는지 역참조합니다. kthread_flush_work()kthread_cancel_work_sync()가 해당 워커의 락을 획득하기 위해 사용합니다. 미큐잉 상태에서는 NULL입니다.
  • canceling진행 중인 cancel 요청 횟수입니다. cancel 중에는 재큐잉(re-queue)을 방지하여 안전한 취소를 보장합니다.

kthreadd() 소스 분석

kthreadd()는 PID 2로 실행되며, kthread_create_list를 감시하다가 요청이 들어오면 create_kthread()를 호출하여 실제 커널 스레드를 생성합니다.

/* kernel/kthread.c */
int kthreadd(void *unused)
{
    struct task_struct *tsk = current;  /* 현재 태스크 = kthreadd 자신 */

    /* 모든 CPU에서 실행 가능하도록 설정 (housekeeping CPU 집합) */
    set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_TYPE_KTHREAD));
    set_mems_allowed(node_states[N_MEMORY]);  /* 모든 NUMA 노드 허용 */

    current->flags |= PF_NOFREEZE;  /* freezer 대상 제외: kthreadd는 항상 실행 */
    cgroup_init_kthreadd();          /* cgroup 초기화 */
    ignore_signals(tsk);             /* 모든 시그널 무시 */

    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE); /* 슬립 준비 */

        if (list_empty(&kthread_create_list))
            schedule();  /* 요청 없음 → CPU 양보, 깨어날 때까지 대기 */

        __set_current_state(TASK_RUNNING);  /* RUNNING 복귀 */

        spin_lock(&kthread_create_lock);
        while (!list_empty(&kthread_create_list)) {
            struct kthread_create_info *create;

            create = list_entry(kthread_create_list.next,
                                struct kthread_create_info,
                                list);
            list_del_init(&create->list);   /* 리스트에서 분리 */
            spin_unlock(&kthread_create_lock);

            create_kthread(create);          /* 실제 스레드 생성 */

            spin_lock(&kthread_create_lock);
        }
        spin_unlock(&kthread_create_lock);
    }

    return 0;  /* 도달하지 않음 */
}
코드 설명
  • set_cpus_allowed_ptrkthreadd가 실행될 수 있는 CPU를 housekeeping CPU 집합으로 제한합니다. isolated CPU(nohz_full)에서는 kthreadd가 실행되지 않아, 레이턴시(Latency) 민감 태스크의 방해를 방지합니다.
  • PF_NOFREEZE이 플래그가 설정된 커널 스레드는 freezer 대상에서 제외됩니다. kthreadd는 suspend 중에도 다른 freezable 스레드들이 동결 요청을 받을 수 있도록 계속 실행되어야 합니다.
  • ignore_signalskthreadd의 모든 시그널을 무시합니다. 생성 요청은 리스트를 통해 전달되므로 시그널이 필요 없으며, 의도치 않은 종료를 방지합니다.
  • set_current_state / schedule리스트가 비어있을 때 TASK_INTERRUPTIBLE로 전환 후 schedule()로 CPU를 양보합니다. 새 요청이 들어오면 wake_up_process(kthreadd_task)로 깨어납니다. 이 패턴은 wakeup-miss 레이스를 방지합니다.
  • spin_lock / list_del_init락을 보유한 채 리스트에서 항목을 꺼낸 뒤, 락을 해제하고 create_kthread()를 호출합니다. 락을 보유한 채 스레드를 생성하면 데드락이 발생할 수 있으므로 반드시 먼저 해제해야 합니다.
  • create_kthread(create)내부적으로 kernel_thread(kthread, create, ...)를 호출합니다. 새 스레드는 kthread() 래퍼에서 시작하며, 이 함수가 complete(done)으로 호출자를 깨웁니다.

kthread() 래퍼 소스 분석

kthread()는 모든 커널 스레드의 실제 진입점(Entry Point)입니다. kernel_clone()으로 생성된 태스크가 가장 먼저 이 함수를 실행합니다.

/* kernel/kthread.c - kthread() 래퍼 (실제 커널 스레드 진입점) */
static int kthread(void *_create)
{
    struct kthread_create_info *create = _create;
    int (*threadfn)(void *data) = create->threadfn; /* 로컬로 복사 */
    void *data                  = create->data;
    struct completion *done    = create->done;
    struct kthread *self;
    int ret;

    /* 태스크 스택 최상단에 위치하는 kthread 메타데이터 초기화 */
    self = to_kthread(current);
    init_completion(&self->exited);
    init_completion(&self->parked);
    current->vfork_done = &self->exited;  /* 종료 시 complete용 */

    /* 호출자(kthread_create)에게 task_struct 포인터 전달 */
    create->result = current;
    complete(done);  /* 이 시점 이후 create를 역참조하면 안 됨 */

    /* wake_up_process()가 호출될 때까지 슬립
     * kthread_run()이면 즉시 깨어남
     * kthread_create()면 호출자가 wake_up_process() 호출 시까지 대기 */
    schedule_preempt_disabled();

    if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
        /* 정상 실행 경로: threadfn 호출 */
        ret = threadfn(data);
    } else {
        /* kthread_stop()이 wake_up_process() 전에 호출된 경우 */
        ret = -EINTR;
    }

    kthread_exit(ret);
    /* 도달하지 않음: kthread_exit()는 do_exit()를 호출 */
}
코드 설명
  • threadfn / data 로컬 복사complete(done) 이후 create_info는 호출자가 언제든 해제할 수 있습니다. 따라서 complete() 전에 필요한 값을 로컬 변수에 복사하여 use-after-free를 방지합니다.
  • to_kthread(current)태스크 스택 최상단에서 struct kthread를 얻는 매크로입니다. task_thread_info(task) 위에 위치합니다. 이 구조체는 kernel_clone()이 스택을 설정할 때 이미 공간이 확보되어 있습니다.
  • current->vfork_done = &self->exited커널 스레드 종료 시 do_exit()vfork_done을 통해 complete()를 호출합니다. kthread_stop()exited completion을 대기하여 스레드 종료를 확인합니다.
  • complete(done)호출자(__kthread_create_on_node())에서 wait_for_completion_killable(done)으로 대기 중인 스레드를 깨웁니다. 이 시점에서 create->result에 새 태스크 포인터가 저장되어 있습니다.
  • schedule_preempt_disabled()선점(Preemption)을 비활성화한 채 스케줄러를 호출하여 CPU를 양보합니다. TASK_UNINTERRUPTIBLE 상태로 잠들어, wake_up_process() 호출만 이 스레드를 깨울 수 있습니다.
  • test_bit(KTHREAD_SHOULD_STOP)스레드가 깨어나기 전에 이미 kthread_stop()이 호출된 경우를 처리합니다. 이 경우 threadfn을 실행하지 않고 -EINTR을 반환합니다.

kthread_worker_fn() 소스 분석

kthread_worker_fn()kthread_create_worker()가 내부적으로 생성하는 전용 워커 스레드의 메인 루프입니다. work_list에서 작업을 꺼내어 순차 실행합니다.

/* kernel/kthread.c - kthread_worker_fn() */
int kthread_worker_fn(void *worker_ptr)
{
    struct kthread_worker *worker = worker_ptr;
    struct kthread_work   *work;

    WARN_ON(worker->task);              /* 중복 실행 방지 */
    worker->task = current;

repeat:
    set_current_state(TASK_INTERRUPTIBLE);  /* 슬립 준비 */

    if (kthread_should_stop()) {           /* stop 요청 확인 */
        __set_current_state(TASK_RUNNING);
        break;
    }

    work = NULL;
    spin_lock_irq(&worker->lock);
    if (!list_empty(&worker->work_list)) {
        work = list_first_entry(&worker->work_list,
                               struct kthread_work, node);
        list_del_init(&work->node);     /* 큐에서 분리 */
    }
    worker->current_work = work;        /* flush 동기화를 위해 기록 */
    spin_unlock_irq(&worker->lock);

    if (work) {
        __set_current_state(TASK_RUNNING);
        work->func(work);              /* 작업 실행 */
        cond_resched();                /* 선점 포인트: 다른 태스크에 양보 */
    } else {
        schedule();                   /* 할 일 없음 → 슬립 */
    }

    goto repeat;
}
EXPORT_SYMBOL_GPL(kthread_worker_fn);
코드 설명
  • worker->task = current워커 스레드 자신을 kthread_worker.task에 등록합니다. 이후 kthread_destroy_worker()kthread_stop(task)을 호출할 때 이 포인터를 사용합니다.
  • set_current_state(TASK_INTERRUPTIBLE)작업을 꺼내기 전에 먼저 상태를 변경합니다. 이 순서가 중요한데, 만약 work_list 확인 후 슬립 전에 새 작업이 큐잉되더라도 wakeup 신호를 놓치지 않습니다.
  • list_first_entry / list_del_initFIFO 순서로 작업을 꺼냅니다. list_del_init()으로 노드를 초기화하여, 실행 중에도 같은 work를 재큐잉할 수 있도록 합니다.
  • worker->current_work = work현재 실행 중인 work를 기록합니다. kthread_flush_work(work)는 이 값을 확인하여 해당 work가 실행 완료될 때까지 대기합니다. 실행 완료 후 다음 반복에서 새 work로 갱신됩니다.
  • work->func(work)work 함수를 직접 호출합니다. 워커 스레드와 동일한 컨텍스트에서 실행되므로, work 함수의 스케줄링 우선순위와 CPU affinity가 워커 스레드 설정을 따릅니다.
  • cond_resched()긴 작업 처리 후 자발적 선점 포인트를 제공합니다. 비선점형 커널 설정에서도 다른 고우선순위 태스크가 실행될 기회를 보장합니다.
  • goto repeat무한 루프를 goto로 구현한 것은 컴파일러 최적화를 위한 것이 아니라, TASK_INTERRUPTIBLE 상태 설정이 루프 최상단에서 이루어지도록 하여 wakeup-miss 레이스를 방지하기 위함입니다.

kthread_worker_fn() 실행 흐름

외부: kthread_queue_work() work_list tail에 추가 + wake_up 루프 시작 set_current_state(INTERRUPTIBLE) kthread_should_stop()? Yes → break (스레드 종료) work_list 비어있나? spin_lock_irq 보호 schedule() INTERRUPTIBLE 슬립 비어있음(No work) wake_up_process() 후 깨어남 → repeat work->func(work) set_current_state(RUNNING) work 있음 current_work 기록 flush_work() 동기화용 cond_resched() 자발적 선점 포인트 goto repeat
kthread_worker_fn() 내부 실행 사이클 — 큐잉 → 실행 → 슬립 → 깨어남 반복

Freezer 연동

시스템 절전(suspend/hibernate) 시 커널의 freezer는 모든 유저 프로세스와 freezable 커널 스레드를 동결합니다. I/O를 수행하는 커널 스레드가 suspend 도중 디스크에 접근하면 데이터 손상이 발생할 수 있으므로, freezer 연동은 전원 관리(Power Management)의 핵심입니다.

Suspend/Resume과 커널 스레드 freezer Suspend 경로 1. pm_suspend() 호출 2. freeze_processes() - 유저 프로세스 동결 3. freeze_kernel_threads() - freezable kthread 동결 4. 각 freezable 스레드에 TIF_SIGPENDING 설정 → 스레드가 try_to_freeze()에서 __refrigerator() 진입 5. __refrigerator(): FROZEN 상태로 슬립 thaw될 때까지 TASK_UNINTERRUPTIBLE 6. 모든 freezable 스레드 동결 완료 → suspend_enter() Resume 경로 1. 하드웨어 인터럽트로 깨어남 2. 디바이스 드라이버 resume 콜백 3. thaw_kernel_threads() - kthread 해동 4. __thaw_task(): FROZEN 플래그 해제 → wake_up_state(TASK_UNINTERRUPTIBLE) 5. 스레드 RUNNING 복귀, 작업 재개 6. thaw_processes() → 유저 프로세스 해동
Suspend/Resume 과정에서의 커널 스레드 freezer 동작

Freezer API 상세

API호출 위치역할
set_freezable()스레드 함수 초반현재 스레드를 freezable로 마킹. current->flags &= ~PF_NOFREEZE
try_to_freeze()메인 루프 내freeze 요청이 있으면 __refrigerator()로 진입하여 동결
wait_event_freezable(wq, cond)메인 루프 내try_to_freeze() + wait_event_interruptible() 조합
wait_event_freezable_timeout()메인 루프 내타임아웃 포함 freezable 대기
freezing(current)조건 확인현재 freeze 요청 중인지 확인
kthread_freezable_should_stop()메인 루프 조건try_to_freeze() + kthread_should_stop() 조합
/* freezer 연동 패턴 — I/O 스레드에서 권장 */
static int io_thread_fn(void *data)
{
    set_freezable();

    while (!kthread_should_stop()) {
        /* 방법 1: wait_event_freezable (가장 권장) */
        wait_event_freezable(wq,
            has_work || kthread_should_stop());

        if (kthread_should_stop())
            break;

        do_io_work();
    }
    return 0;
}

/* 방법 2: 수동 try_to_freeze() */
static int polling_thread_fn(void *data)
{
    set_freezable();

    while (!kthread_should_stop()) {
        /* 루프 시작마다 freeze 확인 */
        try_to_freeze();

        if (check_condition()) {
            process_data();
        }
        usleep_range(1000, 2000);
    }
    return 0;
}

/* 방법 3: kthread_freezable_should_stop() */
while (!kthread_freezable_should_stop(NULL)) {
    /* freeze + stop 동시 확인 */
    do_work();
    schedule();
}
non-freezable 스레드: set_freezable()을 호출하지 않은 커널 스레드는 기본적으로 PF_NOFREEZE 플래그가 설정되어 suspend 시에도 동결되지 않습니다. migration/N, ksoftirqd/N 등 시스템 필수 스레드는 의도적으로 non-freezable입니다. I/O를 수행하지 않는 순수 계산 스레드도 non-freezable로 두는 것이 안전합니다.
hibernate와 freezer: hibernation(S4)에서는 suspend(S3)보다 더 엄격하게 스레드를 동결합니다. 전체 메모리 이미지를 디스크에 저장해야 하므로, freeze_kernel_threads() 단계에서 거의 모든 커널 스레드가 정지됩니다. 이때 동결 타임아웃(기본 20초) 내에 try_to_freeze()에 도달하지 못하는 스레드가 있으면 suspend가 취소됩니다.

커널 스레드 시그널 처리

커널 스레드는 유저 프로세스와 달리 대부분의 시그널을 무시합니다. 대신 kthread_should_stop()kthread_should_park()라는 커널 내부 플래그 기반의 제어 메커니즘을 사용합니다.

유저 프로세스 시그널 SIGTERM, SIGKILL, SIGUSR1 ... signal_pending() → sighandler 호출 do_signal() → 유저 핸들러 실행 시그널 기반 제어 (표준 POSIX) kill, sigaction, signal 등 커널 스레드 제어 ignore_signals() → 모든 시그널 차단 KTHREAD_SHOULD_STOP → 종료 요청 KTHREAD_SHOULD_PARK → 일시 중지 요청 플래그 기반 제어 (커널 내부) kthread_stop, kthread_park 등 vs kthread 제어 메커니즘 흐름 외부 호출자 set_bit(SHOULD_STOP) + wake_up_process() 스레드 깨어남 should_stop() 확인 루프 탈출 + kthread_exit() 시그널과 달리 비동기 인터럽트 없이 협력적(cooperative)으로 동작 스레드가 should_stop()을 확인할 때까지 종료가 지연될 수 있음
유저 프로세스 시그널 vs 커널 스레드 제어 메커니즘 비교

시그널 차단 메커니즘

/* kthreadd가 생성하는 모든 커널 스레드의 시그널 마스크 */
/* kernel/kthread.c - kthreadd() */
ignore_signals(current);
/* → sigfillset(¤t->blocked) — 모든 시그널 차단 */

/* 자식 스레드(새로 생성되는 kthread)도 이 마스크를 상속
 * → 기본적으로 모든 커널 스레드는 시그널 전달 불가 */

kthread 제어 플래그

플래그설정하는 함수확인하는 함수의미
KTHREAD_SHOULD_STOPkthread_stop()kthread_should_stop()스레드 종료 요청. 스레드 함수의 메인 루프 탈출 조건
KTHREAD_SHOULD_PARKkthread_park()kthread_should_park()스레드 일시 중지 요청. kthread_parkme() 호출 트리거
KTHREAD_IS_PARKEDkthread_parkme()내부 사용현재 park 상태임을 표시. unpark 시 해제
KTHREAD_IS_PER_CPUkthread_bind()내부 사용per-CPU 스레드 여부. smpboot 프레임워크 연동
/* 올바른 제어 플래그 확인 패턴 */
static int well_behaved_fn(void *data)
{
    while (!kthread_should_stop()) {
        /* park 요청 먼저 확인 — CPU offline 대응 */
        if (kthread_should_park()) {
            kthread_parkme();
            continue;  /* unpark 후 루프 재진입 */
        }

        /* 실제 작업 */
        do_work();
        schedule();
    }
    return 0;
}

/* 시그널 허용이 필요한 특수한 경우 */
static int signal_aware_fn(void *data)
{
    /* 특정 시그널만 허용 (매우 드문 패턴) */
    allow_signal(SIGTERM);
    allow_signal(SIGKILL);

    while (!kthread_should_stop()) {
        if (signal_pending(current)) {
            flush_signals(current);
            pr_info("signal received, cleaning up\n");
            break;
        }
        do_work();
        msleep_interruptible(1000);
    }
    return 0;
}
allow_signal() 사용 주의: 커널 스레드에서 allow_signal()을 사용하는 것은 일반적으로 권장되지 않습니다. kthread_should_stop() 기반의 제어가 표준 패턴이며, 시그널 기반 제어는 nfsd, cifs 등 레거시 코드에서만 사용됩니다. 새 코드에서는 kthread_stop()이나 kthread_park()를 사용하세요.

stop vs park vs signal 비교

메커니즘용도복구 가능동기적사용 사례
kthread_stop()영구 종료불가예 (완료 대기)모듈 언로드, 서브시스템 정리
kthread_park()일시 중지가능 (unpark)예 (park 대기)CPU hotplug, 동적 재구성
allow_signal()시그널 반응가능비동기레거시 코드 (nfsd 등)
set_bit() + wakeup커스텀 플래그가능비동기드라이버별 커스텀 제어

ftrace/bpftrace 커널 스레드 모니터링

커널 스레드의 실행 패턴, 스케줄링 동작, 성능 특성을 분석하기 위해 ftrace와 bpftrace를 활용할 수 있습니다.

커널 공간 (트레이스포인트) sched_switch sched_wakeup sched_kthread_stop sched_kthread_work _execute_start/end kprobes (동적) function_graph tracer perf_events PMU 유저 공간 도구 ftrace debugfs 기반 perf record/report/sched bpftrace eBPF 원라이너 /proc 인터페이스 stack, sched, stat 함수 호출 그래프 이벤트 필터링 스케줄링 지연 CPU 프로파일 동적 트레이싱 히스토그램 스택 트레이스 스케줄링 통계 오버헤드: /proc (최소) < ftrace < perf < bpftrace (최대, 하지만 가장 유연)
커널 스레드 모니터링 도구 체계 및 트레이스포인트 연동

sched 트레이스포인트 활용

# kthread 관련 주요 트레이스포인트
ls /sys/kernel/debug/tracing/events/sched/
# sched_kthread_stop         — kthread_stop() 호출 시
# sched_kthread_stop_ret     — kthread_stop() 완료 시 (반환값 포함)
# sched_kthread_work_execute_start  — kthread_work 실행 시작
# sched_kthread_work_execute_end    — kthread_work 실행 완료
# sched_kthread_work_queue_work     — kthread_work 큐잉 시

# 특정 커널 스레드의 스케줄링 이벤트 추적
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo > trace
echo 1 > events/sched/sched_switch/enable
echo 1 > events/sched/sched_wakeup/enable

# ksoftirqd만 필터링
echo 'comm == "ksoftirqd/0"' > events/sched/sched_switch/filter
echo 1 > tracing_on
sleep 5
echo 0 > tracing_on
cat trace

# 출력 예시:
#  ksoftirqd/0-12  [000] ..s.  123.456: sched_switch: prev_comm=ksoftirqd/0
#    prev_pid=12 prev_prio=120 prev_state=S ==> next_comm=swapper/0

kthread_work 추적

# kthread_worker의 작업 실행 추적
echo 1 > events/sched/sched_kthread_work_queue_work/enable
echo 1 > events/sched/sched_kthread_work_execute_start/enable
echo 1 > events/sched/sched_kthread_work_execute_end/enable
echo 1 > tracing_on
sleep 10
echo 0 > tracing_on
cat trace

# 작업 지연 분석: queue → execute_start 사이의 시간 측정
cat trace | grep kthread_work | awk '{print $1, $4, $5}'

bpftrace 원라이너

# 커널 스레드 생성 추적
bpftrace -e 'kprobe:kthread_create_on_node {
    printf("kthread create: %s (comm=%s, pid=%d)\n",
           str(arg3), comm, pid);
}'

# kthread_stop 호출 추적 (누가 어떤 스레드를 정지시키는지)
bpftrace -e 'kprobe:kthread_stop {
    $task = (struct task_struct *)arg0;
    printf("stop kthread: pid=%d comm=%s by %s(%d)\n",
           $task->pid, $task->comm, comm, pid);
}'

# kthread_worker 작업 실행 시간 히스토그램
bpftrace -e '
tracepoint:sched:sched_kthread_work_execute_start {
    @start[tid] = nsecs;
}
tracepoint:sched:sched_kthread_work_execute_end {
    if (@start[tid]) {
        @us = hist((nsecs - @start[tid]) / 1000);
        delete(@start[tid]);
    }
}'

# 커널 스레드 웨이크업 지연 측정 (깨어나기까지 걸린 시간)
bpftrace -e '
tracepoint:sched:sched_wakeup /args->comm == "ksoftirqd/0"/ {
    @wakeup[args->comm] = nsecs;
}
tracepoint:sched:sched_switch /args->next_comm == "ksoftirqd/0"/ {
    if (@wakeup["ksoftirqd/0"]) {
        $delay = nsecs - @wakeup["ksoftirqd/0"];
        printf("ksoftirqd/0 wakeup-to-run latency: %d us\n", $delay / 1000);
        delete(@wakeup["ksoftirqd/0"]);
    }
}'

perf를 활용한 kthread 분석

# 커널 스레드의 스케줄링 지연 분석
perf sched record -- sleep 30
perf sched latency --sort max
# Task                  |   Runtime   |  Switches  |  Avg delay  |  Max delay
# ksoftirqd/0:12        |    5.234 ms |       142  |   0.045 ms  |   1.234 ms

# 특정 kworker의 함수 프로파일
perf record -p $(pgrep -f 'kworker/0:1$') -g -- sleep 10
perf report --stdio

# kthread_stop 이벤트 카운팅
perf stat -e 'sched:sched_kthread_stop' -a -- sleep 60

# off-CPU 분석 — kthread가 어디서 블록되는지
perf record -e sched:sched_switch -a --filter 'prev_comm ~ "kswapd*"' -- sleep 30
perf script
실시간 모니터링 팁: trace_pipe를 사용하면 이벤트를 실시간으로 스트리밍할 수 있습니다: cat /sys/kernel/debug/tracing/trace_pipe | grep kthread. 프로덕션 환경에서는 perf record가 오버헤드가 낮으므로 더 적합합니다.

디자인 패턴 비교 및 선택 가이드

리눅스 커널은 비동기 작업 처리를 위해 여러 메커니즘을 제공합니다. 각각의 특성을 이해하고 상황에 맞는 최적의 선택을 하는 것이 중요합니다.

커널 비동기 작업 메커니즘 선택 가이드 비동기 작업이 필요합니다 작업이 슬립(블록)할 수 있는가? 아니오 (atomic) softirq / BH workqueue 전용 스레드가 필요한가? 아니오 workqueue (CMWQ) 순차 실행 / RT 우선순위 필요? kthread_worker 전용 스레드 + 작업 큐 per-CPU + hotplug 관리 필요? smpboot 프레임워크 자동 hotplug 관리 아니오 kthread_run() 직접 스레드 관리 복잡도 증가 → softirq < workqueue < kthread_worker < kthread_run() < smpboot
커널 비동기 작업 메커니즘 선택 판단 흐름도

메커니즘 종합 비교

특성softirq / BH WQworkqueue (CMWQ)kthread_workerkthread_run()smpboot
실행 컨텍스트BH (softirq)프로세스프로세스프로세스프로세스
슬립 가능불가가능가능가능가능
스레드 관리자동 (ksoftirqd)자동 (worker pool)전용 1개수동자동 per-CPU
동시성CPU별 직렬동시 가능직렬 (보장)커스텀CPU별 직렬
CPU hotplug자동자동수동수동자동
RT 우선순위불가제한적완전 지원완전 지원완전 지원
flush/cancel제한적완전 지원완전 지원수동 구현park/unpark
메모리 오버헤드최소공유 풀+8KB/worker+8KB/thread+8KB/CPU
사용 난이도중간낮음중간높음중간
kthread_delayed_work 타이머 기반 큐잉 흐름 kthread_queue_ delayed_work() hrtimer 등록 delay jiffies 대기 만료 work_list에 추가 + wake_up_process() func() 실행 워커 스레드에서 kthread_cancel_delayed_work_sync() 타이머 취소 + 큐 제거 + 실행 완료 대기 취소 주기적 실행: func() 내에서 kthread_queue_delayed_work()를 재호출하면 주기적 타이머 동작 workqueue의 schedule_delayed_work()와 유사하지만 전용 스레드에서 실행 보장
kthread_delayed_work의 타이머 기반 큐잉 및 취소 흐름

사용 사례별 권장 패턴

사용 사례권장 메커니즘이유
네트워크 패킷(Packet) 수신 처리softirq (NAPI)지연 최소화, 높은 처리량 필요
디바이스 드라이버 초기화 작업workqueue일회성 비동기 작업, 슬립 가능
스토리지 I/O 완료 처리workqueue (WQ_HIGHPRI)고우선순위, 시스템 관리 풀 활용
DRM/GPU 명령 제출kthread_worker순차 실행 보장, RT 우선순위 필요
NAND 플래시 쓰기kthread_worker전용 스레드에서 직렬화된 I/O
USB 이벤트 처리kthread_run()복잡한 상태 머신, 전용 루프 필요
per-CPU 캐시 관리smpbootCPU별 독립 처리, 자동 hotplug
softirq 과부하 대응smpboot (ksoftirqd)커널 내장, 자동 관리
파일시스템(Filesystem) 저널 커밋kthread_run()장시간 블록 가능, 전용 제어 루프
메모리 페이지 회수kthread_run() (kswapd)복잡한 정책, 전역 상태 관리
/* 패턴 1: 단순 비동기 작업 → workqueue (가장 간단) */
static void simple_work_fn(struct work_struct *work)
{
    do_deferred_init();
}
static DECLARE_WORK(init_work, simple_work_fn);
schedule_work(&init_work);

/* 패턴 2: 순차 보장 → kthread_worker */
struct kthread_worker *w = kthread_create_worker(0, "serial-io");
kthread_init_work(&my_work, serial_io_fn);
kthread_queue_work(w, &my_work);

/* 패턴 3: 복잡한 상태 머신 → kthread_run() */
kthread_run(complex_state_machine_fn, dev, "my-daemon");

/* 패턴 4: per-CPU 처리 → smpboot */
static struct smp_hotplug_thread my_threads = {
    .store             = &per_cpu_task,
    .thread_should_run = my_should_run,
    .thread_fn         = my_per_cpu_fn,
    .thread_comm       = "mythread/%u",
};
smpboot_register_percpu_thread(&my_threads);
선택 원칙: 가능한 한 가장 단순한 메커니즘을 선택하세요. (1) 먼저 workqueue 사용을 검토하고, (2) 순차 실행이나 RT가 필요할 때만 kthread_worker를 고려하며, (3) 복잡한 상태 머신이 필요한 경우에만 직접 kthread를 생성하세요. per-CPU 스레드가 필요하면 smpboot 프레임워크를 사용하여 hotplug 관리를 자동화하세요.

참고자료

커널 공식 문서

LWN.net 기사

커널 소스 코드

서적

Kernel Threads와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.