커널 스레드 (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-2024 (pthread API 비교 참조) — 유저 공간 스레드와 대비되는 커널 스레드의 동작을 이해할 수 있습니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
관련 문서: 프로세스(Process) 관리(task_struct, fork/exec), 프로세스 스케줄러(Scheduler)(CFS, sched_class), Workqueue(CMWQ, kworker), Bottom Half(ksoftirqd)를 함께 참고하세요.
전제 조건: 프로세스 스케줄러와 프로세스 문서를 먼저 읽으세요. 실행 단위 관리 주제는 태스크(Task) 상태 전이와 큐 정책이 핵심이므로, 스케줄링 기준과 wakeup 경로를 먼저 이해해야 합니다.
일상 비유: 이 개념은 건물 관리실의 상주 전담 직원과 비슷합니다. 외부 손님 요청을 기다리지 않고 내부 운영을 계속 맡는 직원처럼, 커널 스레드는 유저 프로세스와 별개로 커널 내부 작업을 전담합니다.

핵심 요약

  • 커널 스레드 — mm == NULL인 task_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)

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

시스템 계층에서의 위치

커널 스레드는 항상 커널 공간(Kernel Space)에서만 실행됩니다. 유저 프로세스가 시스템 콜(System Call)을 통해 커널로 진입하는 것과 달리, 커널 스레드는 처음부터 커널 공간에 존재합니다:

유저 공간 (User Space) 유저 프로세스 nginx, bash, systemd task_struct.mm != NULL — 유저 주소 공간 보유 외부 요청은 시스템 콜로만 커널에 전달됨 시스템 콜(System Call) / 예외(Exception) 경계 시스템 콜로 진입 커널 공간 (Kernel Space) 커널 스레드 [kswapd0] [kworker/0:1] [kthreadd] (PID 2) mm == NULL — 유저 공간 없음 경계를 넘지 않음 메모리 관리 vmalloc, kmalloc 페이지 테이블 파일시스템 ext4, btrfs NFS, overlayfs 인터럽트 IRQ, softirq 디바이스 드라이버 하드웨어 추상화(Hardware Abstraction Layer) 경계 하드웨어: CPU / 물리 메모리(RAM) / I/O 장치
시스템 계층에서 커널 스레드의 위치 — 커널 스레드는 처음부터 커널 공간에만 존재하며 시스템 콜 경계를 넘지 않습니다 (개념 개요)

핵심 구분: 커널 스레드(대괄호 표기)는 유저 공간 주소를 보유하지 않으므로(mm == NULL), 시스템 콜을 통해 커널에 진입할 필요가 없습니다. 항상 커널 모드에서 실행되므로 컨텍스트 스위칭(Context Switching) 비용이 유저 ↔ 커널 전환보다 낮습니다.

왜 커널 스레드가 필요한가

커널 내부의 장기 실행 작업을 처리하는 방법들과 각각의 한계를 비교합니다:

방식 슬립 가능 뮤텍스(Mutex) 사용 커널 자원 직접 접근 제약
인터럽트 핸들러 X X O 원자적 컨텍스트, 매우 짧아야 함
Softirq / tasklet X X O 원자적 컨텍스트(Atomic Context)
유저 공간 데몬 O O X 시스템 콜 오버헤드, 커널 내부 직접 접근 불가
커널 스레드 O O O 유저 공간 접근 불가 (mm == NULL)

커널 스레드가 필요한 대표적인 시나리오:

유저 프로세스와의 속성 비교:

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

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

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

mm과 active_mm의 관계

유저 프로세스 task_struct mm mm_struct — 유저 페이지 테이블 CR3 전환 물리 메모리 — 유저 페이지 물리 메모리 — 커널 직접 매핑 copy_from_user() 정상 동작 커널 스레드 task_struct mm = NULL 유저 페이지 테이블 없음 active_mm — 이전 태스크의 mm 빌림 커널 주소 공간만 접근 copy_from_user() 호출 시 Oops kthread_use_mm() 사용 시 — 예외적으로 유저 공간 접근 가능 task_struct mm = 빌린 mm_struct CR3 전환 유저 공간 접근 가능
커널 스레드 메모리 모델 — mm은 NULL이고 active_mm만 빌려 Lazy TLB로 CR3 전환을 회피하며, kthread_use_mm()를 쓰면 예외적으로 유저 공간에 접근합니다 (개념 개요)

kthread_use_mm() / kthread_unuse_mm()

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

사용 시 반드시 mmget()/mmput()으로 참조 카운트(Reference Count)를 관리해야 하며, 프로세스가 종료되어 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 번호 부여 */

내부 생성 흐름

호출자 (Caller) kthreadd kthread_create() kthread_create_on_node() 호출 kthread_create_info 할당 kthread_create_list에 추가 wake_up_process(kthreadd_task) kthreadd 깨움 kthreadd() 실행 리스트에서 create 꺼냄 wait_for_completion_killable() 이 지점에서 블럭 create_kthread(create) kernel_thread(kthread, ...) kthread() 래퍼 create->result = current; complete(done) 대기 해제 create->result인 task_struct 반환
kthread_create() 내부 흐름 — 호출자는 생성 정보를 리스트에 넣고 대기하며, kthreadd가 실제 태스크를 생성한 뒤 complete로 호출자를 깨웁니다 (상세 구현, kernel/kthread.c 기준)
/* 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 레이스가 발생합니다.

Wakeup-miss 레이스 타임라인

아래 다이어그램은 잘못된 순서와 올바른 순서에서 두 스레드(T: 워커, W: 깨우기 담당)의 실행 흐름을 비교합니다:

잘못된 순서 — wakeup-miss 레이스 발생 T (워커) W (깨우기) ① 조건 확인 check = false T는 RUNNING ③ set_current_state TASK_INTERRUPTIBLE 이벤트를 이미 놓침 ④ schedule() 깨울 수 없어 영원히 슬립 ② wake_up(T) T가 RUNNING이라 wake_up 무시됨 wake_up이 무시된 뒤 schedule()로 넘어가 영원히 깨어나지 못한다 올바른 순서 — set_current_state()를 먼저 호출 T (워커) W (깨우기) ① set_current_state TASK_INTERRUPTIBLE 슬립 준비 완료 ② 조건 확인 check = false 이후 schedule() ③ wake_up(T) INTERRUPTIBLE이므로 wake_up 성공 ④ 깨어남 → 처리 wake_up 성공 정상적으로 처리 상태를 먼저 바꿔 두었으므로 wake_up이 정상 반영되고 즉시 깨어난다
Wakeup-miss 레이스 타임라인 — set_current_state() 순서가 잘못되면 조건 확인과 schedule() 사이의 wake_up을 놓쳐 영원히 슬립합니다 (개념 개요)

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

kthread_stop_put()은 kthread_stop()과 free_kthread_struct()을 하나로 합친 편의 API입니다. 스레드 종료 후 struct kthread 메타데이터를 즉시 해제하여 메모리 누수를 방지합니다:

/* include/linux/kthread.h */
int kthread_stop_put(struct task_struct *k);

/* 사용 예시: stop + 구조체 해제를 한 번에 */
kthread_stop_put(my_thread);

/* 위는 아래와 동등하지만 더 안전함 */
kthread_stop(my_thread);
free_kthread_struct(my_thread);

kthread_stop vs kthread_stop_put: kthread_stop()만 호출하면 struct kthread가 task_struct와 함께 해제되지만, 명시적으로 free_kthread_struct()를 호출하거나 kthread_stop_put()을 사용하면 메모리 회수가 더 예측 가능해집니다. 모듈 언로드 시에는 kthread_stop_put()을 권장합니다.

kthread_bind() / kthread_bind_mask() / kthread_affine_preferred()

커널 스레드의 CPU 어피니티(Affinity)를 제어하는 세 가지 API가 있습니다:

/* include/linux/kthread.h */

/* 단일 CPU에 강제 바인딩 — 미실행 상태에서만 호출 가능 */
void kthread_bind(struct task_struct *k, unsigned int cpu);

/* cpumask로 강제 바인딩 — 미실행 상태에서만 호출 가능 */
void kthread_bind_mask(struct task_struct *k, const struct cpumask *mask);

/* 실행 중인 스레드에 선호 어피니티 적용 — 런타임에도 가능 */
int kthread_affine_preferred(struct task_struct *p, const struct cpumask *mask);
API 호출 시기 강제 여부 주요 용도
kthread_bind() 미실행 전 (TASK_UNINTERRUPTIBLE/PARKED) 강제 고정 per-CPU 스레드, 인터럽트 핸들러 대체
kthread_bind_mask() 미실행 전 강제 고정 (다중 CPU) NUMA 노드별 스레드, housekeeping 집합
kthread_affine_preferred() 런타임 (실행 중에도 가능) 선호 힌트 (사용자 덮어쓰기 가능) hotplug 재적용, 동적 부하 분산

kthread_bind() 호출 시기: kthread_bind()/kthread_bind_mask()는 스레드가 아직 첫 스케줄링을 하지 않은 상태(TASK_UNINTERRUPTIBLE)에서만 호출해야 합니다. 이미 실행 중인 스레드에 호출하면 WARN_ON()이 발생합니다. 실행 중인 스레드의 어피니티를 변경하려면 kthread_affine_preferred()나 set_cpus_allowed_ptr()을 사용하세요.

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 TASK_UNINTERRUPTIBLE RUNNING TASK_RUNNING STOPPED TASK_DEAD UNINTERRUPTIBLE TASK_UNINTERRUPTIBLE INTERRUPTIBLE TASK_INTERRUPTIBLE PARKED TASK_PARKED wake_up_process() kthread_run() kthread_stop() should_stop() schedule() wake_up() I/O 대기 I/O 완료 kthread_park() kthread_unpark() threadfn() 반환 후 do_exit() — 단방향 전이
커널 스레드 상태 전환 — CREATED에서 RUNNING으로 기동한 뒤 스케줄 경로로 슬립하고, park로 CPU 핫플러그에 대기하거나 stop으로 종료됩니다 (개념 개요)
상태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:* 등이 있습니다. 이들은 캐시 지역성(Cache Locality) 극대화와 잠금 경합(Lock Contention) 완화를 위해 특정 CPU에 바인딩되며, smpboot 프레임워크가 이 스레드들의 생명주기를 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 0 CPU 1 CPU N ksoftirqd/N softirq 처리 ksoftirqd/0 ksoftirqd/1 ksoftirqd/N migration/N SCHED_FIFO prio 99 migration/0 migration/1 migration/N kworker/N:0 workqueue 워커 kworker/0:0 kworker/1:0 kworker/N:0 kworker/N:1H 고우선순위 워커 kworker/0:1H kworker/1:1H kworker/N:1H cpuhp/N 핫플러그 콜백 cpuhp/0 cpuhp/1 cpuhp/N idle/N PID 0 상주 스레드 idle/0 idle/1 idle/N 모든 CPU에 동일하게 하나씩 생성되며, 스레드 이름의 숫자는 CPU 번호를 뜻합니다
per-CPU 커널 스레드 배치 구조 — 각 CPU에 softirq 처리, 마이그레이션, workqueue 워커, 핫플러그 콜백, idle 스레드가 하나씩 배치됩니다 (개념 개요)

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 핫플러그 이벤트 ONLINE OFFLINE smpboot 코어 smpboot_register_percpu_thread() smpboot_thread_fn() 메인 루프 콜백 함수들 예: ksoftirqd 등록 thread_fn / should_run / comm Per-CPU 스레드들 [thread/0] [thread/1] [thread/N] 콜백 등록 생성/관리 per-CPU 스레드 상태 흐름 CPU ONLINE create / setup RUNNING CPU OFFLINE park / cleanup PARKED 핫플러그 드라이버를 직접 작성하지 않아도 스레드 생명주기가 자동 관리됩니다
smpboot 프레임워크 구조도 — CPU 온라인 시 스레드를 생성해 실행하고, 오프라인 시 park해 두었다가 재기동합니다 (개념 개요)
자동 핫플러그 관리: smpboot 프레임워크를 사용하면 CPU online/offline 시 per-CPU 스레드를 수동으로 관리할 필요가 없습니다. smpboot_register_percpu_thread()만 호출하면, 커널이 CPU 핫플러그 이벤트에 자동으로 대응하여 스레드를 생성/park/unpark/정리합니다.

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의 커널 스레드 간섭을 최소화할 수 있습니다.

CPU Hotplug 연동 상세 (CPU Hotplug Integration)

Per-CPU 커널 스레드는 CPU 핫플러그 이벤트와 밀접하게 연동됩니다. 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 핫플러그에 따른 스레드 상태 전이 CPU_DOWN 스레드 아직 없음 TASK_PARKED 스레드는 살아 있음 실행 중 thread_fn() 수행 CPU online + CPUHP_CREATE_THREADS kthread_unpark() CPU offline: kthread_park() smpboot_thread_fn() 내부 루프 TASK_INTERRUPTIBLE 로 상태 설정 kthread_should_park() No ht->thread_should_run() Yes thread_fn() Yes kthread_parkme() No schedule() 세 경로 모두 완료 후 루프 처음으로 복귀합니다
CPU Hotplug 상태 머신 — park 상태에서는 스레드를 파괴하지 않고 TASK_PARKED로 대기시켜 재온라인 시 빠르게 재개합니다 (상세 구현, kernel/smpboot.c 기준)
/* 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 콜백(Callback) 함수

콜백 호출 시점 용도
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 vs 대안 선택 가이드

커널 내부 작업 처리에는 다양한 메커니즘이 있습니다. 아래 결정 트리를 참고하여 상황에 맞는 방식을 선택하세요:

Yes 선택지 커널 내부 작업 처리 방식 선택 커널 문맥에서 처리해야 할 작업이 있다 인터럽트로 즉시 처리 가능하고 매우 짧은 작업인가? 각 CPU별로 독립적으로 실행해야 하는 작업인가? 독립적인 작업 항목을 병렬로 처리해야 하는가? 특정 스레드에서 작업을 직렬화하고 지연(delay) 작업이 필요한가? 장기 실행 루프 / 직접 슬립 / 커널 자원에 직접 접근이 필요한가? Threaded IRQ request_threaded_irq() Per-CPU kthread smpboot_register _percpu_thread() Workqueue schedule_work() CMWQ kthread_worker kthread_queue _work() Yes No Yes No Yes No Yes No Yes kthread kthread_run() kthread_create() No 타이머 / hrtimer timer_setup(), hrtimer_init() 모든 질문에 No라면 슬립 없이 주기적으로 실행되는 짧은 작업이므로 타이머를 씁니다
kthread vs 대안 선택 결정 트리 — 인터럽트 응답성, CPU별 독립성, 병렬성, 직렬화와 지연 실행, 장기 실행 루프 순으로 좁혀 나갑니다 (개념 개요)
메커니즘 슬립 직렬화 CPU 친화성 주요 사용처
kthread O O (자체 루프) 선택 가능 kswapd, kjournald, 드라이버 전용 스레드
kthread_worker O O (큐 직렬화) 선택 가능 지연 작업이 필요한 직렬 처리
Workqueue (CMWQ) O X (병렬 가능) unbound/bound 일반 비동기 작업, 드라이버 지연 처리
Per-CPU kthread O O (CPU별) 고정 (per-CPU) ksoftirqd, kcompactd, CPU별 처리
Threaded IRQ O O (IRQ별) 선택 가능 인터럽트 처리 후처리, 드라이버 ISR
타이머 / hrtimer X N/A 제한적 짧은 주기 작업, 타임아웃

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) 공유 풀 사용으로 효율적 전용 스레드로 리소스 점유

성능 특성 비교

두 메커니즘의 구조적 차이에서 도출되는 정성적 특성 차이는 다음과 같습니다. 절대 수치는 CPU 모델, 워크로드 크기, 스레드 풀 상태, NUMA 배치에 따라 크게 달라지므로 특정 측정값으로 일반화하지 않습니다.

특성workqueue (CMWQ)kthread_worker구조적 이유
실행 스레드워커 풀 스레드 (풀 크기에 따라 병렬)전용 스레드 1개 (항상 순차 실행)CMWQ는 max_active로 병렬도를 조절하고, kthread_worker는 워커당 스레드가 하나뿐
병렬 실행가능불가 (큐잉 순서대로 직렬)struct kthread_worker가 task 하나만 보유
지연 예측 가능성풀 경합에 따라 가변다른 작업에 선점되지 않아 안정전용 스레드라 공유 경합이 없음
스케줄링 정책제한적 (풀 관리 특성상)전용 스레드라 자유롭게 변경 가능worker->task에 직접 sched_setscheduler() 적용
CPU 고정queue_work_on() 등 CPUs 지정 가능생성 시 CPU 지정 또는 kthread_bind()양쪽 모두 가능하나 kthread 쪽이 더 단순
sllep 허용워커 풀 정책에 따라 제약전용 스레드에서 직접 제어CMWQ는 메모리 복구 워크큐와의 상호작용을 내부에서 관리
메모리 오버헤드풀 스택을 워커가 공유전용 스택 + struct kthread + kthread_worker워커당 독립 스택이 필요
적합한 작업짧고 다발적인 작업순서가 중요하거나 RT 정책이 필요한 작업위 구조적 제약의 직접적 결과

실측 시 권장 사항: 두 메커니즘의 절대 지연을 비교하려면 kuryr나perf stat 같은 벤치마크 도구로 측정을 자동화하고, CPU 모델·커널 버전·워크로드 크기·반복 횟수를 반드시 함께 기록하세요. 측정 환경이 명시되지 않은 수치는 비교 근거로 사용할 수 없습니다.

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

Linux 6.14 이후의 kthread API 정리

버전 기준: 이 문서는 커널 7.x를 기준으로 합니다(kernel.org stable 릴리스 정보 기준). 아래 두 절은 6.14와 6.15 개발 사이클에서 발생한 변경을 다루고, 그 이후 메인라인에 누적된 상태는 현재 메인라인의 kthread API 절에서 정리합니다.

Linux 6.14 개발 사이클에서 kthread 서브시스템은 수년간 누적된 중복 API를 정리하고 Per-CPU 스레드 생성 경로를 단일화하는 대규모 리팩터링(Refactoring)을 겪었습니다. 이 섹션은 신규 심볼과 의미 변경을 요약해, 신규 드라이버 작성 시 권장되는 패턴과 이전 심볼의 매핑 관계를 정리합니다.

kthread_run_worker() / kthread_run_worker_on_cpu()

기존에는 워커 스레드를 완전히 가동하려면 kthread_create_worker()로 생성한 뒤 내부 task에 대해 별도로 wake_up_process()를 호출해야 했습니다. Linux 6.14에서 추가된 kthread_run_worker()와 kthread_run_worker_on_cpu()는 "생성 + 기동"을 한 번에 수행하여, 기존 kthread_run()·kthread_run_on_cpu()와 대칭되는 워커 버전 API를 제공합니다.

/* include/linux/kthread.h (Linux 6.14+) */

/* 단일 스레드: 생성 + 자동 기동 */
struct task_struct *kthread_run(int (*fn)(void *), void *data,
                               const char *namefmt, ...);

/* 워커: 생성 + 자동 기동 (6.14 신규) */
struct kthread_worker *kthread_run_worker(unsigned int flags,
                                           const char *namefmt, ...);

/* CPU 바인딩 워커: 생성 + 자동 기동 (6.14 신규) */
/* 정적 인라인 함수이며 가변 인자를 받지 않습니다. */
static inline struct kthread_worker *
kthread_run_worker_on_cpu(int cpu,
                                                  unsigned int flags,
                                                  const char namefmt[]);
/* 구 패턴 (6.13까지) */
struct kthread_worker *w = kthread_create_worker(0, "my-worker");
if (IS_ERR(w))
    return PTR_ERR(w);
wake_up_process(w->task);       /* 별도 기동 필요 */

/* 신 패턴 (6.14+) */
struct kthread_worker *w = kthread_run_worker(0, "my-worker");
if (IS_ERR(w))
    return PTR_ERR(w);
/* 곧바로 kthread_queue_work() 가능 */

호환성: kthread_create_worker()는 그대로 유지되며, 스레드 상태 초기화가 끝난 뒤에 외부에서 별도로 기동하고 싶을 때(예: cpumask 조정, RT 우선순위 설정 후 기동) 여전히 유용합니다. 일반적인 경우에는 kthread_run_worker*()를 사용하는 것이 실수(기동 누락)를 줄입니다.

kthread_create_on_cpu() / kthread_create_worker_on_cpu() 통합

Linux 6.14 이전에는 Per-CPU 스레드와 Per-CPU 워커 생성 경로가 서로 다른 affinity 초기화 시퀀스를 사용해, 생성 시점과 실제 kthreadd가 포크를 수행하는 시점 사이에 발생하는 hotplug 이벤트에 대한 취약성이 서로 달랐습니다. 6.14의 정리로 두 경로 모두 지연 affinity 적용을 공유합니다. 호출 시점에는 대상 CPU 번호만 기록해 두고, 스레드 컨텍스트가 처음 깨어날 때 kthread::preferred_affinity를 사용해 실제 set_cpus_allowed_ptr()을 호출합니다.

이 변경의 관측 가능한 효과는 다음과 같습니다.

/* Per-CPU 스레드 생성 — 6.14 이후 권장 흐름 */
for_each_online_cpu(cpu) {
    struct task_struct *t;
    t = kthread_create_on_cpu(per_cpu_fn, data, cpu, "pc-%u");
    if (IS_ERR(t))
        goto err_cleanup;

    /* preferred affinity는 이미 설정됨 — 추가 cpumask 조작 불요 */
    wake_up_process(t);         /* 첫 실행 시 바인딩 실제 적용 */
}

preferred_affinity 의미론

6.14에서 도입된 preferred_affinity 필드는 struct kthread 내부 구조체 중 하나로, 다음 두 경우에 사용됩니다.

  1. 지연 Per-CPU 바인딩: kthread_create_on_cpu() 계열 호출에서 대상 CPU를 기록해 두고, 첫 스케줄링 시 적용합니다.
  2. Hotplug 재적용: 해당 CPU가 offline되었다가 다시 online될 때, 힌트 마스크를 기준으로 자동 재바인딩을 시도합니다. 기존에는 드라이버가 CPU Hotplug 콜백에서 직접 set_cpus_allowed_ptr()을 호출해야 했습니다.

주의: preferred_affinity는 선호 힌트이므로, 사용자가 taskset으로 명시적으로 cpumask를 변경하면 덮어씁니다. 완전히 고정해야 하는 스레드(예: 인터럽트 핸들러 대체 스레드)는 반드시 kthread_bind()와 PF_NO_SETAFFINITY 플래그를 함께 사용해야 합니다.

use_mm → kthread_use_mm 이름 정리

2025년 8월 이후에는 kernel/fork.c를 포함한 여러 소스에서 레거시 심볼 use_mm()/unuse_mm()의 주석과 문서가 kthread_use_mm()/kthread_unuse_mm()로 정리되었습니다. 실제 함수 심볼 자체는 이미 과거에 이름이 변경되었지만, 오래된 주석이 남아 있던 것을 정리하는 작업입니다. 드라이버 측 영향은 없지만, 소스 코드 검색 시 두 이름 모두를 고려해야 합니다.

Linux 6.15 이후의 kthread 관련 변경사항

Linux 6.15는 6.14에서 완성된 kthread 친화성(Affinity) 통합 위에서 추가적인 개선사항을 도입하였습니다. 핵심 kthread API 자체의 대규모 변경보다는 kthread를 활용하는 서브시스템 수준의 개선과 스케줄러 통합에 집중되어 있습니다.

sched_ext 이벤트 카운터와 kthread 인식 개선

커널 6.15부터: sched_ext(BPF 기반 스케줄러 확장) 서브시스템에 코어 이벤트 카운터(Core Event Counter)가 구현되어, BPF 스케줄러 프로그램이 커널 스레드를 포함한 모든 태스크(Task)에 대한 enqueue/dequeue/context-switch 이벤트를 집계할 수 있게 되었습니다.

또한 전역 유휴 CPU 마스크가 NUMA 노드별로 분할되어 커널 스레드가 배치될 CPU를 선택할 때 원격 NUMA 노드를 불필요하게 방문하는 빈도가 줄어들었습니다. 이는 kswapd, kcompactd 등 NUMA 인식이 중요한 커널 스레드의 성능 향상에 기여합니다.

/* sched_ext BPF kfunc: NUMA 노드 수 조회 */
/* 전통적 bpf_xxx 헬퍼가 아니라 kfunc(Kernel Function) 분류입니다. */
int nr_nodes = scx_bpf_nr_node_ids();
/* nr_nodes를 기반으로 per-node idle 마스크 순회 가능 */

용어 구분: sched_ext 확장이 노출하는 scx_bpf_* 심볼은 BPF_PROG_TYPE_STRUCT_OPS 프로그램에서 호출되는 kfunc(Kernel Function)입니다. bpf_map_lookup_elem() 같은 전통적 헬퍼(Helper)와 구분되므로, BPF 프로그램 작성 시 호출 가능한 프로그램 타입을 혼동하지 않도록 주의하세요.

CONFIG_SCHED_DEBUG 무조건 활성화와 kthread 관측성

커널 6.15부터: 기존에 CONFIG_SCHED_DEBUG=y를 설정해야 컴파일되던 스케줄러 디버그 인터페이스가 모든 빌드에서 기본 제공됩니다. 이 저장소가 현재 다루는 커널(master)에서는 kernel/sched/debug.c 안에 CONFIG_SCHED_DEBUG 조건부 컴파일이 남아 있지 않으며, 해당 파일은 debugfs_create_dir("sched", NULL) 아래 파일들을 그대로 등록합니다.

이로 인해 커널 스레드 모니터링에 다음 인터페이스를 별도 커널 옵션 없이 사용할 수 있습니다.

인터페이스 활용
/sys/kernel/debug/sched/debug 런큐(Runqueue)별 실행 가능 태스크 목록과 vruntime, deadline, slice, sum_exec_runtime, 전환 횟수 출력
/sys/kernel/debug/sched/features 스케줄러 기능 플래그 런타임 조정
/sys/kernel/debug/sched/preempt 선점(Preempt) 모드 동적 전환
/sys/kernel/debug/sched/numa_balancing/ NUMA 자동 밸런싱 스캔 주기와 크기 튜닝
/proc/[pid]/sched 개별 커널 스레드 스케줄링 통계 (컨텍스트 스위치 수, 평균 실행 시간 등)
# 커널 스레드별 스케줄링 통계 — pid는 pgrep/ps로 직접 확인
cat /proc/$(pgrep kswapd0)/sched

# 런큐(Runqueue)별 실행 가능 태스크 목록 (커널 스레드 포함)
cat /sys/kernel/debug/sched/debug | grep -E 'kswapd|kworker|ksoftirqd'

경로 주의: 과거 커널 문서와 일부 온라인 문서에서흔히 보이는 /proc/sched_debug 경로는 현재 커널에 존재하지 않습니다. 런큐 덤프(Dump)는 /sys/kernel/debug/sched/debug로, 태스크 단위 덤프는 /proc/<pid>/sched로 확인해야 합니다. debugfs는 CONFIG_DEBUG_FS=y로 빌드한 커널에 마운트(Mount)되어 있어야 하며, 접근 시 권한 elevation이 필요합니다.

Lazy Preemption과 커널 스레드 상호작용

Lazy Preemption은 CONFIG_PREEMPT_LAZY=y로 켜는 선점 모델입니다. 아키텍처가 지원 여부를 HAVE_TIF_NEED_RESCHED_LAZY 심볼로 알리며, x86은 이를 정의합니다.

Lazy Preemption은 CFS 태스크의 선점 요청을 틱 경계까지 지연시키지만, 커널 스레드가 SCHED_FIFO 또는 SCHED_RR 정책으로 동작하는 경우에는 즉각 선점이 적용됩니다. 일반 커널 스레드(SCHED_NORMAL)는 Lazy Preemption의 영향을 받아 짧은 연산 후 불필요하게 선점되는 빈도가 줄어들어 캐시 효율성이 향상될 수 있습니다.

실시간 커널 스레드 선택: I/O 완료 처리나 타이머 응답 등 낮은 지연이 필요한 커널 스레드에는 sched_setscheduler(task, SCHED_FIFO, &param)로 RT 정책을 부여하여 Lazy Preemption 영향을 받지 않도록 합니다. 일반 백그라운드 작업 스레드는 SCHED_IDLE로 낮춰 시스템 부하를 줄입니다.

현재 메인라인의 kthread API

6.15 이후 kthread 서브시스템은 API를 크게 뒤엎기보다 정밀화하는 방향으로 발전했습니다. 이 절은 현재 메인라인 커널의 include/linux/kthread.h와 kernel/kthread.c에 실제로 존재하는 심볼과 동작을 기준으로 정리합니다.

지연 affinity 재작업의 최종 형태

6.14에서 시작된 Per-CPU kthread affinity 통합은 이후 struct kthread에 affinity_node(전역 연결 리스트 노드)와 preferred_affinity(CPU 마스크) 두 필드를 추가하며 완성되었습니다. 현재 동작은 다음과 같습니다.

/* kernel/kthread.c — 선호 affinity를 힌트로 등록 */
int kthread_affine_preferred(struct task_struct *p,
                              const cpumask *mask)
{
    struct kthread *kthread = to_kthread(p);

    WARN_ON_ONCE(kthread->preferred_affinity);
    kthread->preferred_affinity = kzalloc_obj(struct cpumask);
    if (!kthread->preferred_affinity)
        return -ENOMEM;

    cpumask_copy(kthread->preferred_affinity, mask);

    /* 전역 리스트에 연결해 CPU offline/online을 추적 */
    mutex_lock(&kthread_affinity_lock);
    list_add_tail(&kthread->affinity_node, &kthread_affinity_list);
    mutex_unlock(&kthread_affinity_lock);

    /* 필요 시 즉시 적용 */
    if (force || k->preferred_affinity || k->node != NUMA_NO_NODE) {
        struct cpumask *cpus = kthread->preferred_affinity;
        ...
    }
    return 0;
}

강제 바인딩과 선호의 차이: kthread_bind()/kthread_bind_mask()는 set_cpus_allowed_force()를 호출하고 PF_NO_SETAFFINITY를 설정해 사용자 개입을 차단합니다. 반면 kthread_affine_preferred()는 힌트만 남기므로 사용자가 taskset으로 cpumask를 바꾸면 덮어씁니다. 둘 중 무엇이 필요한지 먼저 결정해야 합니다. 또한 두 API 모두 WARN_ON_ONCE(kthread->started)로 기동된 스레드에 대한 오용을 경고합니다.

housekeeping 스레드 재구성

kthreads_update_housekeeping()이 CPU 온라인/오프라인 시점에 housekeeping 성격의 per-CPU 스레드(kworker 계열, khugepaged, kcompactd 등)를 일괄 재구성합니다. kthread.c는 이 함수에서 affinity_node 리스트를 순회하며 오프라인 CPU에 묶인 스레드를 정리하고 마이그레이션 대상이 되는 스레드를 재배치(Relocation)합니다.

# per-CPU housekeeping 스레드와 CPU 바인딩 상태 확인
for t in /sys/devices/system/cpu/cpu*/thread_siblings/*/topology/thread_siblings_list; do
  echo "$t"; cat "$t"
done

# 커널 스레드별 실제 CPU 목록
taskset -pc $(pgrep -f 'kworker/0')

안전성 개선 API

API 해결하는 문제 사용 시점
kthread_stop_put(k) kthread_stop() 대기 후 태스크 참조를 함께 해제 스레드 생성 시 get_task_struct()로 참조를 얻은 경우
kthread_probe_data(k) 종료 중인 스레드에 copy_from_kernel_nofault()로 안전하게 접근 다른 스레드에서 종료 여부를 모르는 kthread의 data를 읽을 때
kthread_should_stop_or_park() stop과 park 비트를 한 번에 검사 두 제어를 모두 받는 워커 스레드의 분기 지점
kthread_freezable_should_stop(bool *was_frozen) 필요 시 refrigerator에 들어가 kthread_stop()과 교착 회피 freeze 가능한 장시간 실행 스레드
kthread_use_mm() / kthread_unuse_mm() 커널 스레드에서 유저 mm_struct를 임시 사용 유저 메모리를 직접 순회해야 하는 드라이버 (GUP 계열)

커널 스레드 이름 잘림

task_struct.comm는 TASK_COMM_LEN(16바이트, NUL 포함)으로 제한되기 때문에 numactl-kernel-hugepages 같은 긴 이름은 잘립니다. 현재 커널은 이를 위해 struct kthread::full_name에 전체 이름을 따로 보관하고, get_kthread_comm()가 이를 우선 반환합니다.

# ps는 잘린 이름을 보여줍니다
ps -eo pid,comm | grep -E '^\s*[0-9]+ numactl'

# 전체 이름은 dmesg 로그에서 확인
dmesg | grep -i 'numactl-kernel-hugepages'

실무 팁: 진단 로그에 커널 스레드 이름을 남길 때는 %s로 current->comm를 찍는 대신, 필요한 경우 get_kthread_comm()로 전체 이름을 확보해 기록하세요. 그래야 16바이트 경계에서 잘려 식별이 실패하는 로그를 피할 수 있습니다.

주요 커널 스레드 종합

부팅 직후 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 저널링(Journaling) (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 파일시스템(Filesystem)의 저널 커밋을 담당합니다. 블록 디바이스별 1개씩 생성되며(jbd2/sda1-8 등), 트랜잭션의 원자적(Atomic) 커밋을 보장합니다.

/* 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 스레드가 동시에 실행되면 다른 모든 태스크가 선점(Preemption)되어, 전역 상태를 안전하게 변경할 수 있습니다. 이는 모듈 로드/언로드, CPU hotplug 등에 활용됩니다.

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

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

스케줄링 클래스 적용

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

/* 커널 스레드에 실시간 우선순위 설정 */
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 시스템에서 원격 노드 메모리 접근은 로컬 노드보다 체감상 확실히 느립니다. 정확한 배율은 interconnect 대역폭(Bandwidth), 아키텍처, 페이지 크기에 따라 크게 달라지므로 특정 배율을 일반화할 수 없습니다. 커널 스레드의 task_struct, 커널 스택, 그리고 작업 데이터를 동일 노드에 배치하면 캐시 히트율이 향상되고 메모리 접근 지연(Latency)을 줄일 수 있습니다. 특히 kswapd처럼 대량의 메모리 메타데이터를 다루는 스레드에서는 이 최적화가 중요합니다.

BH Workqueue

최근 커널에서는 tasklet 대체를 위해 BH (Bottom-Half) Workqueue가 도입되었습니다. WQ_BH 플래그를 준 workqueue는 워커 스레드가 아니라 큐잉한 CPU의 softirq 컨텍스트에서 작업을 실행합니다. 전용 커널 스레드를 새로 만들지 않고, tasklet이 제공하던 softirq 실행 지점을 workqueue의 flush/cancel 메커니즘과 결합한 형태입니다.

허용 플래그: include/linux/workqueue.h는 __WQ_BH_ALLOWS = WQ_BH | WQ_HIGHPRI | WQ_PERCPU로 BH workqueue에 결합 가능한 플래그를 제한합니다. 즉 WQ_UNBOUND, WQ_FREEZABLE, WQ_MEM_RECLAIM, WQ_CPU_INTENSIVE 등은 WQ_BH와 함께 지정할 수 없습니다.

BH Workqueue vs Tasklet

특성 Tasklet (Legacy) BH Workqueue
컨텍스트 softirq (BH) softirq (BH)
실행 대상 softirq 벡터별 직렬화 큐잉한 CPU의 BH 컨텍스트에서 큐잉 순서대로 실행
flush/cancel 제한적 workqueue API로 완전한 flush/cancel 지원
재스케줄 tasklet_schedule() queue_work() / 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 struct work_struct highpri_work;

static int __init bh_init(void)
{
    INIT_WORK(&my_bh_work, bh_work_handler);
    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 생성 */
/* WQ_BH는 WQ_HIGHPRI / WQ_PERCPU와만 조합할 수 있습니다. */
struct workqueue_struct *my_bh_wq;
struct workqueue_struct *my_bh_pc_wq;

my_bh_wq = alloc_workqueue("my-bh-wq",
                         WQ_BH,          /* BH 플래그 필수 */
                         0);  /* max_active: 0이면 기본값 */

/* per-CPU BH workqueue가 필요한 경우 */
my_bh_pc_wq = alloc_workqueue("my-bh-pcwq",
                            WQ_BH | WQ_PERCPU | WQ_HIGHPRI,
                            1);  /* CPU당 동시 실행 1개 */

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

흔한 실수: alloc_workqueue(..., WQ_UNBOUND | WQ_BH, ...)처럼 WQ_UNBOUND를 WQ_BH와 함께 지정하면 workqueue 생성 단계에서 거부됩니다. BH workqueue는 이미 큐잉한 CPU의 softirq 컨텍스트에서 실행되므로 스레드 풀 배치가 불필요하며, 언바운드/바운드 구분이 적용되지 않습니다.

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

성능 튜닝 및 모니터링

커널 스레드 성능 튜닝

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

/* 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.worker_private 포인터를 통해 접근합니다:

/* kernel/kthread.c (최신 커널 기준) */
struct kthread {
    unsigned long            flags;          /* KTHREAD_SHOULD_STOP 등 */
    unsigned int             cpu;            /* per-CPU kthread의 대상 CPU */
    unsigned int             node;           /* NUMA 노드 할당 정보 */
    int                      started;        /* 첫 wake-up 후 시작 여부 */
    int                      result;         /* 스레드 함수 반환값 */
    int                      (*threadfn)(void *);
    void                     *data;
    struct completion       parked;         /* park/unpark 동기화 */
    struct completion       exited;         /* stop/exit 동기화 */
#ifdef CONFIG_BLK_CGROUP
    struct cgroup_subsys_state *blkcg_css;  /* blkcg 연동 */
#endif
    char                     *full_name;    /* truncation-safe 전체 이름 */
    struct task_struct      *task;         /* 역참조 백포인터 */
    struct list_head        affinity_node;  /* affinity_list 연결용 */
    struct cpumask          *preferred_affinity; /* 지연 어피니티 적용용 */
};

/* 플래그 비트 — IS_PARKED는 없음! */
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는 존재하지 않음 — park 상태는 completion으로 추적 */
};

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

/* 헤더에서 내부 연결 확인 (lock-free) */
static inline struct kthread *tsk_is_kthread(struct task_struct *p)
{
    if (p->flags & PF_KTHREAD)
        return p->worker_private;
    return NULL;
}

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. 슬립
 * 을 안전하게 조합합니다 */

디버깅

커널 스레드 식별하기

# 모든 커널 스레드 나열 (대괄호 표기)
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");

모듈 빌드 및 테스트 방법

위 예제들을 실제로 빌드하고 커널에 로드하여 동작을 확인하는 단계별 절차입니다:

1단계: Makefile 작성

# Makefile
obj-m := example_kthread.o

KDIR ?= /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

all:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

2단계: 빌드

# 커널 헤더 패키지가 필요합니다 (Ubuntu: linux-headers-$(uname -r))
sudo apt install linux-headers-$(uname -r)   # Ubuntu/Debian
# sudo dnf install kernel-devel             # Fedora/RHEL

make

3단계: 로드 및 모니터링

# 터미널 1: 커널 로그 실시간 모니터링
dmesg -w

# 터미널 2: 모듈 로드
sudo insmod example_kthread.ko

# 커널 스레드 확인 (대괄호 표기)
ps aux | grep '\[example'

# /proc을 통한 스레드 정보 확인
cat /proc/$(pgrep example_kthread)/status

# 스택 추적 (CONFIG_STACKTRACE 필요)
cat /proc/$(pgrep example_kthread)/stack

4단계: 언로드 및 정리

# 모듈 언로드 (module_exit → kthread_stop 호출)
sudo rmmod example_kthread

# 로드된 모듈 목록 확인
lsmod | grep example

# 커널 로그에서 스레드 종료 메시지 확인
dmesg | tail -10

QEMU 가상 환경 추천: 커널 모듈 개발은 호스트 커널을 위험에 노출시킬 수 있습니다. 개발 환경 설정 문서에 소개된 QEMU + GDB 환경에서 실험하면 커널 패닉(Kernel Panic)이 발생해도 호스트에 영향을 주지 않습니다.

커널 스레드 스택 제약 (Kernel Thread Stack Constraints)

커널 스레드는 제한된 크기의 스택에서 실행됩니다. 유저 공간 스레드(pthread)가 기본 8MB 스택을 갖는 것과 달리, 커널 스레드 스택은 아키텍처별로 훨씬 작습니다:

아키텍처 THREAD_SIZE (4 KiB 페이지 기준) 사용 가능 깊이 비고
x86_64 16 KiB (4페이지) 16 KiB보다 작음 (프레임·레지스터 영역 잠복) CONFIG_THREAD_INFO_IN_TASK 시 struct thread_info가 스택 프레임에 병합
x86_32 8 KiB (2페이지) 8 KiB보다 작음 TOP_OF_KERNEL_STACK_PADDING이 프레임 크기만큼 추가됨
arm64 16 KiB (4페이지) 16 KiB보다 작음 THREAD_SIZE가 PAGE_SIZE 기준이므로 16K/64K 페이지 커널에서는 그에 비례해 커짐
RISC-V 16 KiB (4페이지) 16 KiB보다 작음 x86_64와 동일한 기본값

확인 방법: 특정 커널의 실제 값은 include/asm-generic/thread_info.h와 아키텍처별 THREAD_SIZE_ORDER 정의에서 확인하거나, 부팅 후 cat /proc/*/stack(CONFIG_STACKTRACE 필요)로 태스크별 최대 스택 깊이를 관찰할 수 있습니다.

/* THREAD_SIZE는 아키텍처마다 아래 형태로 정의됩니다 (x86_64, 4K 페이지) */
/* ORDER는 지수(exponent)입니다. 2를 PAGE_SIZE만큼 왼쪽으로 시프트 = 4페이지 */
#define THREAD_SIZE_ORDER    (2)              /* 2 << 12bit = 4페이지 */
#define THREAD_SIZE          (PAGE_SIZE << THREAD_SIZE_ORDER)  /* 4096 << 2 = 16384 = 16 KiB */

CONFIG_VMAP_STACK — 가상 매핑 스택

Linux 4.9부터 CONFIG_VMAP_STACK=y(기본값)로 커널 스택이 가상 주소 공간에 매핑됩니다. 이 설정의 핵심 이점은 스택 오버플로(Stack Overflow) 감지입니다:

# 커널 설정 확인
grep CONFIG_VMAP_STACK /boot/config-$(uname -r)
# CONFIG_VMAP_STACK=y  ← 기본 활성화

# 커널 스택 사용량 상한 (x86)
cat /proc/sys/kernel/stackdetector   # 기능 지원 여부
cat /proc/$(pgrep kworker/0)/stack | head   # 태스크별 현재 스택 프레임

스택 관련 흔한 실수

커널 스레드 함수에서 스택을 많이 사용하는 패턴은 위험합니다:

/* 위험: 커널 스택에 큰 로컬 배열 */
static int bad_thread_fn(void *data)
{
    char buf[8192];      /* x86_64 16K 스택의 절반! */
    struct big_struct s; /* 큰 구조체도 위험 */
    ...
}

/* 올바른 방법: 힙(Heap) 할당 사용 */
static int good_thread_fn(void *data)
{
    char *buf = kmalloc(8192, GFP_KERNEL);
    if (!buf)
        return -ENOMEM;

    /* 작업 수행 */

    kfree(buf);
    return 0;
}

/* 또는 vmalloc()으로 큰 버퍼 할당 */
char *buf = vmalloc(64 * 1024);  /* 64KB — kmalloc 한계 초과 시 */

스택 오버플로 증상: 커널 스택이 넘치면 CONFIG_VMAP_STACK가 활성화된 경우 즉시 커널 패닉이 발생합니다. 활성화되지 않은 경우 메모리 덮어쓰기(Corruption)가 조용히 발생하여 디버깅이 매우 어렵습니다. CONFIG_KASAN이나 CONFIG_STACK_VALIDATION을 함께 활성화하면 초기 탐지에 도움이 됩니다.

호출 깊이 제약

커널 함수 호출 체인이 깊어지면 스택이 누적됩니다. 특히 재귀(Recursion) 호출은 커널에서 매우 위험합니다:

주의사항 및 버그 패턴

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)

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

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

기본 자격 증명 (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() 사용, 직접 접근 자제
네트워크모든 소켓(Socket) 접근 가능필요한 CAP_만 설정
SELinuxkernel_t 컨텍스트별도 도메인 정의 가능
seccomp미적용 (커널 모드)해당 없음

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

API 레퍼런스 요약

아래 표는 현재 메인라인 커널의 include/linux/kthread.h에 실제 선언된 심볼을 기준으로 정리한 것입니다. 시그니처 표기는 실제 헤더 정의와 일치하며, 매크로와 정적 인라인 함수(Inline Function)를 구분했습니다.

생성 및 수명 주기

함수 / 매크로 종류 설명
kthread_create(fn, data, namefmt, ...) 매크로 현재 NUMA 노드에 커널 스레드 생성. NUMA_NO_NODE를 넣어 kthread_create_on_node()를 호출하며, 반환된 스레드는 stopped 상태이므로 별도로 깨워야 합니다.
kthread_run(fn, data, namefmt, ...) 매크로 생성과 즉시 기동(wake_up_process())을 합친 래퍼. 실패 시 ERR_PTR(-ENOMEM)을 반환합니다.
kthread_create_on_node(fn, data, node, namefmt, ...) 함수 NUMA 노드를 지정해 생성. node는 태스크와 스택 구조체의 메모리 할당 위치에 영향을 줍니다. 치명적 시그널에 의한 wait_for_completion_killable() 중단 시 ERR_PTR(-EINTR)을 반환할 수 있습니다.
kthread_create_on_cpu(fn, data, cpu, namefmt) 함수 CPU를 지정해 생성. cpu는 아직 online이 아니어도 지정할 수 있습니다.
kthread_run_on_cpu(fn, data, cpu, namefmt) 정적 인라인 CPU 지정 생성과 기동을 합친 헬퍼. namefmt는 "name.*%u" 형태로 제한되며 CPU 번호가 채워집니다.
kthread_bind(p, cpu) 함수 생성 직후 stopped 상태인 스레드를 특정 CPU에 강제 바인딩합니다. 한 번이라도 기동된 스레드에 적용하면 WARN_ON_ONCE()이 발생합니다.
kthread_bind_mask(p, mask) 함수 kthread_bind()의 마스크 버전. PF_NO_SETAFFINITY를 설정해 이후 사용자 개입을 차단합니다.
kthread_affine_preferred(p, mask) 함수 강제 바인딩이 아닌 선호 힌트(preferred_affinity)를 등록합니다. CPU hotplug로 offline되었다가 online으로 복귀할 때 힌트를 기준으로 재바인딩됩니다.
kthread_set_per_cpu(k, cpu) 함수 KTHREAD_IS_PER_CPU 비트를 설정해 per-CPU 스레드로 취급합니다.
kthread_is_per_cpu(k) 함수 per-CPU 스레드 여부를 반환합니다.
kthread_complete_and_exit(comp, code) 함수 completion을 완료시키고 반환 코드code로 종료합니다. 반환하지 않으며, 모듈 해제 전에 이 함수를 호출하면 안전하게 슬립하변합니다.
kthread_do_exit(kthread, result) 함수 종료 시 result를 기록하고 전역 affinity 리스트에서 제거합니다.
kthread_exit(result) 매크로 do_exit(result)로 확장되는 매크로입니다.
kthreads_update_housekeeping() 함수 CPU online/offline 시 housekeeping per-CPU 스레드들을 재구성합니다.

정지와 park 제어

함수 설명
kthread_should_stop() KTHREAD_SHOULD_STOP 비트를 검사합니다. kthread_stop()이 호출되면 true를 반환하고 스레드가 깨어납니다.
kthread_should_park() KTHREAD_SHOULD_PARK 비트를 검사합니다. kthread_park()가 호출되면 true를 반환합니다.
kthread_should_stop_or_park() 두 비트 중 하나라도 설정되어 있는지 한 번에 검사합니다. 두 제어를 함께 받는 스레드에서 분기를 줄여 줍니다.
kthread_freezable_should_stop(bool *was_frozen) freezable 스레드용 종료 검사입니다. 필요 시 refrigerator에 들어가므로 kthread_stop()과의 교착을 피할 수 있습니다. 실제 freeze 발생 여부를 was_frozen로 반환합니다.
kthread_stop(k) 종료를 요청하고 스레드가 실제로 종료될 때까지 동기적으로 대기합니다. 반환값은 threadfn이 돌려준 정수값입니다.
kthread_stop_put(k) kthread_stop()과 동일하게 대기한 뒤 태스크 참조를 함께 해제하는 변형입니다.
kthread_park(k) / kthread_unpark(k) 스레드를 TASK_PARKED 상태로 만들거나 다시 깨웁니다. 스레드는 살아 있으므로 park 사이클이 반복됩니다.
kthread_parkme() 현재 스레드 자신이 park 상태로 전환합니다. set_special_state(TASK_PARKED)로 대기 중 wakeup과 직렬화하여 store-store 경쟁을 피합니다.

kthread_worker / kthread_work

함수 / 매크로 종류 설명
KTW_FREEZABLE 플래그 1 << 0. 워커 스레드를 suspend 시 freeze 대상으로 등록합니다.
kthread_create_worker(flags, namefmt, ...) 매크로 NUMA_NO_NODE로 kthread_create_worker_on_node()를 호출합니다.
kthread_create_worker_on_node(flags, node, namefmt, ...) 함수 NUMA 노드를 지정해 워커를 생성합니다.
kthread_create_worker_on_cpu(cpu, flags, namefmt) 함수 CPU를 지정해 워커를 생성합니다.
kthread_run_worker(flags, namefmt, ...) 매크로 워커 생성과 worker->task 기동을 하나로 합칩니다.
kthread_run_worker_on_cpu(cpu, flags, namefmt) 정적 인라인 CPU 지정 워커의 생성과 기동을 하나로 합칩니다. 가변 인자를 받지 않습니다.
kthread_init_worker(worker) 매크로 정적 워커를 초기화합니다. lockdep 키를 워커 이름에서 자동 생성합니다.
kthread_queue_work(worker, work) 함수 작업을 큐에 넣습니다. 이미 큐잉 중이면 false를 반환합니다.
kthread_queue_delayed_work(worker, dwork, delay) 함수 지연 실행 작업을 큐에 넣습니다. 타이머는 TIMER_IRQSAFE로 설정됩니다.
kthread_mod_delayed_work(worker, dwork, delay) 함수 이미 예약된 지연 작업의 실행 시각을 변경합니다.
kthread_flush_work(work) / kthread_flush_worker(worker) 함수 특정 작업 또는 워커의 전체 큐가 빌 때까지 대기합니다. 워커 flush는 내부에 flush용 작업을 큐에 넣고 completion으로 기다립니다.
kthread_cancel_work_sync(work) 함수 작업을 취소하고, 실행 중이었다면 완료될 때까지 동기적으로 대기합니다.
kthread_cancel_delayed_work_sync(dwork) 함수 지연 작업을 취소합니다. 지연 작업은 아직 실행되지 않았기 때문에 짧게 끝납니다.
kthread_destroy_worker(worker) 함수 워커를 비우고 정리합니다. 지연 작업 처리는 호출자 책임입니다.
kthread_worker_fn(worker_ptr) 함수 워커 스레드의 본체 함수입니다. 큐를 확인하고, 비어 있으면 슬립하며, 작업이 있으면 순차 실행합니다.

상태 조회 및 컨텍스트 분리

함수 / 매크로 설명
tsk_is_kthread(p) PF_KTHREAD 플래그를 확인해 struct kthread *를 반환합니다. 커널 스레드가 아니면 NULL을 반환합니다.
kthread_func(k) 스레드 함수 포인터를 반환합니다. 커널 스레드가 아니면 NULL을 반환합니다.
kthread_data(k) 생성 시 전달한 data 포인터를 반환합니다.
kthread_probe_data(k) 스레드 종료 중에도 안전하게 동작하는 관대 버전입니다. 접근 불가 시 NULL을 반환합니다.
get_kthread_comm(buf, buf_size, tsk) comm가 잘린 경우를 대비해 전체 이름(full_name)을 반환합니다.
kthread_use_mm(mm) / kthread_unuse_mm(mm) 커널 스레드에서 유저 mm_struct를 임시로 사용합니다. mm_struct 헤더에 선언되어 있습니다.
kthread_associate_blkcg(css) / kthread_blkcg() CONFIG_BLK_CGROUP에서만 컴파일됩니다. 커널 스레드의 블록 I/O를 특정 cgroup에 귀속시키거나 조회합니다.

버전 확인: 표의 API 일부는 특정 커널 버전에서 추가되었습니다. kthread_run_worker(), kthread_run_worker_on_cpu()는 6.14, kthread_affine_preferred()와 kthread_stop_put()는 이후 릴리스에서 추가되었습니다. 구형 커널을 지원해야 하는 드라이버는 LINUX_VERSION_CODE 검사로 분기해야 합니다.

함수 / 매크로 설명 헤더
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 동작을 상세히 분석합니다.

1. 큐잉과 순차 실행 작업 제출자 kthread_queue_work() worker->lock 하에서 큐잉 이미 큐잉됨·취소 중이면 false list_add_tail() worker->work_list (FIFO) work_A (head) work_B work_C (tail) dequeue 워커 스레드 (전용 1개) kthread_worker_fn() 메인 루프 ① 슬립 상태로 진입 후 종료 요청 확인 ② 큐가 비면 schedule()로 슬립 ③ dequeue 후 current_work에 기록 큐에 새 작업이 들어오면 wake_up_process(worker->task)로 깨움 단, current_work가 있으면 이미 실행 중이므로 깨우지 않음 2. kthread_work 생명주기와 flush / cancel 동작 kthread_queue_work() list_first_entry() func() 반환 후 다음 반복에서 current_work가 NULL로 갱신 IDLE work->node 미연결, current_work 아님 QUEUED work->node가 work_list에 연결 EXECUTING worker->current_work == work kthread_cancel_work_sync() QUEUED — 큐에서 제거하고 true 반환 EXECUTING — canceling++ 후 완료 대기, false IDLE — 제거할 것이 없어 즉시 false canceling 카운터가 재큐잉을 막습니다 kthread_flush_work() QUEUED / EXECUTING — 완료까지 대기 IDLE — 이미 끝나 있어 즉시 반환 큐 뒤에 flush용 작업을 삽입하고 completion 완료를 기다립니다
kthread_worker 큐 처리 흐름과 kthread_work 생명주기 — kthread_queue_work()가 work_list 뒤에 넣고, 전용 스레드가 head에서 하나씩 꺼내 순차 실행하며, cancel과 flush는 상태별로 다르게 동작합니다 (상세 구현, kernel/kthread.c 기준)

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);
        raw_spin_lock_irq(&worker->lock);
        worker->task = NULL;
        raw_spin_unlock_irq(&worker->lock);
        return 0;
    }

    work = NULL;
    raw_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;
    raw_spin_unlock_irq(&worker->lock);

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

    try_to_freeze();
    cond_resched();
    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_PARK 플래그와 completion parked의 조합으로 동기화합니다. 별도의 KTHREAD_IS_PARKED 플래그는 존재하지 않습니다.

1. kthread_park / kthread_unpark 상태 전이 kthread_park() kthread_parkme() RUNNING SHOULD_PARK = 0 TASK_RUNNING PARK_REQUESTED SHOULD_PARK = 1 kthread_should_park() 확인 PARKED SHOULD_PARK = 1 TASK_PARKED kthread_unpark() — SHOULD_PARK 해제 + wake_up_state(TASK_PARKED) per-CPU 스레드는 __kthread_bind()로 원래 CPU에 다시 바인딩 2. CPU 핫플러그 연동 — park만 하고 스레드를 파괴하지 않는다 CPU Offline CPUHP_AP_SMPBOOT_THREADS teardown 1. smpboot_park_threads(cpu) 2. smpboot_park_thread() → kthread_park(tsk) 3. SHOULD_PARK 설정, complete(parked) 4. 스레드는 TASK_PARKED로 대기, 유지됨 CPU Online CPUHP_AP_SMPBOOT_THREADS startup 1. smpboot_unpark_threads(cpu) 2. smpboot_unpark_thread() → kthread_unpark(tsk) 3. __kthread_bind()로 CPU 재바인딩 4. SHOULD_PARK 해제 후 스레드 복귀 CPU가 다시 온라인으로 복귀 — 재생성 없이 스레드가 그대로 재개됩니다
kthread parking 상태 머신과 CPU 핫플러그 연동 시퀀스 — SHOULD_PARK 플래그와 completion parked로 동기화하며 별도의 PARKED 플래그는 쓰지 않습니다 (상세 구현, kernel/kthread.c와 kernel/smpboot.c, kernel/cpu.c 기준)

park/unpark 내부 구현

/* kernel/kthread.c - park/unpark 내부 구현 (주석 축약, master 기준) */
int kthread_park(struct task_struct *k)
{
    struct kthread *kthread = to_kthread(k);

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

    /* 이미 park 요청이 있으면 중복 park를 거부한다 */
    if (WARN_ON_ONCE(test_bit(KTHREAD_SHOULD_PARK, &kthread->flags)))
        return -EBUSY;

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

    /* 자기 자신을 park하는 경로는 깨우기와 대기를 건너뛴다 */
    if (k != current) {
        /* 슬립 중이면 깨움 — should_park()를 확인하게 함 */
        wake_up_process(k);

        /* __kthread_parkme()이 complete() — TASK_PARKED 진입 직전 */
        wait_for_completion(&kthread->parked);

        /* schedule()까지 끝나 런큐에서 빠질 때까지 대기 */
        WARN_ON_ONCE(!wait_task_inactive(k, TASK_PARKED));
    }

    return 0;
}

/* __kthread_parkme() — 내부 함수, 스레드 자신이 호출 */
static void __kthread_parkme(struct kthread *self)
{
    for (;;) {
        /* TASK_PARKED는 특수 상태 — pending wakeup과 충돌 방지 */
        set_special_state(TASK_PARKED);
        if (!test_bit(KTHREAD_SHOULD_PARK, &self->flags))
            break;

        /* schedule() 진입 전 선점 금지 — wait_task_inactive() 보장 */
        preempt_disable();
        complete(&self->parked);
        schedule_preempt_disabled();
        preempt_enable();
    }
    __set_current_state(TASK_RUNNING);
}

/* kthread_parkme() — 외부 API (__kthread_parkme 래퍼) */
void kthread_parkme(void)
{
    __kthread_parkme(to_kthread(current));
}

/* kthread_unpark() — 외부에서 호출 */
void kthread_unpark(struct task_struct *k)
{
    struct kthread *kthread = to_kthread(k);

    /* SHOULD_PARK가 설정되어 있지 않으면 무시 */
    if (!test_bit(KTHREAD_SHOULD_PARK, &kthread->flags))
        return;

    /* per-CPU 스레드는 다시 CPU에 바인딩 */
    if (kthread_is_per_cpu(kthread))
        __kthread_bind(k, kthread->cpu, TASK_PARKED);

    /* SHOULD_PARK 해제 — __kthread_parkme 루프 탈출 조건 */
    clear_bit(KTHREAD_SHOULD_PARK, &kthread->flags);

    /* TASK_PARKED 상태에서 깨움 */
    wake_up_state(k, TASK_PARKED);
}

기준 판본: master의 kernel/kthread.c를 옮겼습니다. kthread_park()의 -EBUSY 가드와 k != current 분기, wait_task_inactive() 대기는 v5.10 판본에서도 동일하게 존재하므로, 오래된 커널에서도 동작 계약이 바뀌지 않습니다.

축약한 부분: 원문의 여러 줄 영문 설명 주문만 한국어 한 줄로 줄였습니다. __kthread_parkme()에서 set_special_state(TASK_PARKED)가 pending wakeup과 직렬화되는 이유, 그리고 schedule_preempt_disabled() 앞뒤의 선점 제어가 kthread_park()의 wait_task_inactive() 대기 시간을 늘리지 않으려는 목적은 코드의 주석에 요약했습니다. 실행 흐름과 반환값은 원본과 동일합니다.

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

kthread_create_on_node() 코드 워크스루

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

호출자 kthreadd 새 태스크 1. kthread_create_on_node() 인자 정리 후 내부 함수로 2. __kthread_create_on_node() kmalloc_obj로 create 할당 3. create->threadfn / data / node done, full_name 채움 4. list_add_tail(&create->list, &kthread_create_list) 5. wake_up_process(kthreadd_task) wait_for_completion_killable() 이 지점에서 블럭 6. create_kthread(create) kernel_thread(kthread, create, full_name) CLONE_FS | CLONE_FILES | SIGCHLD 7. copy_process() + PF_KTHREAD 새 task_struct 생성 8. kthread() 래퍼 진입 create->result = current 9. complete(&create->done) 호출자 깨움 complete()로 호출자가 깨어나 create->result인 task_struct를 받습니다
kthread_create_on_node() 코드 워크스루 — 호출자가 정보를 등록하고 블럭된 사이 kthreadd가 태스크를 만들며, 새 태스크가 complete로 결과를 전달합니다 (상세 구현, kernel/kthread.c 기준)

__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) 생성 직렬화로 경합(Contention) 방지, (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);
    /* 도달하지 않음 */
}

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

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

호출 체인 전체 흐름

호출자 컨텍스트 — 드라이버 코드 1. kthread_create_on_node() include/linux/kthread.h 가변 인자를 va_list로 모아 내부 함수로 전달 2. __kthread_create_on_node() kernel/kthread.c 리스트에 넣고 kthreadd를 깨운 뒤 대기 kthreadd 컨텍스트 — kernel/kthread.c 3. kthreadd() → create_kthread() kthread_create_list 맨 앞에서 꺼냄 호출자는 이 시점부터 블럭 상태 4. kernel_thread() → kernel_clone() kernel/fork.c CLONE_FS, CLONE_FILES, SIGCHLD 로 복제 새 태스크 컨텍스트 — 새 태스크가 처음 실행되는 지점 5. kthread() 래퍼 create->result = current 후 complete(done) __kthread_parkme() 으로 park 진입 6. threadfn(data) 드라이버가 등록한 스레드 함수 반환하면 kthread_exit() 로 종료 threadfn이 반환하면 kthread_exit()로 종료되고, 대기하던 호출자가 complete()로 깨어나 create->result인 task_struct를 받습니다
kthread 생성 호출 체인 — 생성 요청은 kthreadd를 거쳐 실제 태스크로 만들어지고, 새 태스크가 kthread 래퍼를 거쳐 드라이버가 등록한 threadfn을 실행합니다 (상세 구현, kernel/kthread.c와 kernel/fork.c 기준)

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

struct kthread는 커널 스레드 한 개당 하나씩 할당되는 내부 메타데이터 구조체입니다. set_kthread_struct()가 kzalloc_obj()로 힙에 할당한 뒤 task_struct의 worker_private 포인터에 연결하므로, 태스크 스택 위에 놓이는 구조체가 아닙니다. to_kthread(task) 인라인 함수가 이 포인터를 반환하며, PF_KTHREAD 플래그가 없는 태스크에 대해서는 WARN_ON이 발생합니다.

/* kernel/kthread.c - struct kthread (master) */
struct kthread {
    unsigned long   flags;
    unsigned int    cpu;
    unsigned int    node;
    int             started;
    int             result;
    int             (*threadfn)(void *);
    void           *data;
    struct completion parked;
    struct completion exited;
#ifdef CONFIG_BLK_CGROUP
    struct cgroup_subsys_state *blkcg_css;
#endif
    /* To store the full name if task comm is truncated. */
    char            *full_name;
    struct task_struct *task;
    struct list_head affinity_node;
    struct cpumask     *preferred_affinity;
};

enum KTHREAD_BITS {
    KTHREAD_IS_PER_CPU = 0,
    KTHREAD_SHOULD_STOP,
    KTHREAD_SHOULD_PARK,
};

버전 차이: 위 정리는 현재 메인라인(7.3-rc) 기준입니다. 구형 커널의 struct kthread에는 node, started, full_name, task, affinity_node, preferred_affinity 필드가 없었습니다. preferred_affinity와 affinity_node은 Per-CPU kthread 지연 바인딩 재작업으로 추가되었고, full_name은 커널 스레드 이름이 TASK_COMM_LEN(16바이트)에 잘릴 때 전체 이름을 보관하기 위해 추가되었습니다.

코드 설명
  • flagsenum KTHREAD_BITS의 비트 위치를 그대로 사용합니다. KTHREAD_SHOULD_STOP이 설정되면 kthread_should_stop()이 true를 반환하고, KTHREAD_SHOULD_PARK는 kthread_park()가 설정하여 kthread_parkme()에서 대기하도록 만듭니다. KTHREAD_IS_PER_CPU는 smpboot가 관리하는 per-CPU 스레드 구분용입니다.
  • cpu / nodeper-CPU 바인딩 대상 CPU 번호와 메모리 할당 NUMA 노드입니다. kthread_bind()가 cpu를, kthread_create_on_node()의 node 인자가 힌트가 되어 tsk_fork_get_node()를 통해 채워집니다.
  • started스레드가 한 번이라도 기동되었는지 기록합니다. kthread_bind()와 kthread_bind_mask()는 실행 중인 스레드에 적용하면 안 되므로 WARN_ON_ONCE(kthread->started)로 사용자를 경고합니다.
  • resultthreadfn이 반환한 정수값을 저장합니다. kthread_stop()의 반환값이 되므로 음수 에러 코드 전달에 활용됩니다.
  • threadfn / data실제 스레드 함수와 인자입니다. kthread() 래퍼가 complete(done)을 호출한 뒤 로컬 복사본으로 threadfn을 호출합니다. create_info가 해제된 이후에도 안전하게 참조하기 위한 설계입니다.
  • parked / exitedparked는 스레드가 실제로 TASK_PARKED 상태에 진입했음을 알리는 completion이며 kthread_park()가 이를 대기합니다. exited는 종료 완료 신호로 kthread_stop()이 대기합니다. set_kthread_struct()는 p->vfork_done = &kthread->exited로 두어 kernel_thread() 경로에서도 같은 completion이 재사용되도록 합니다.
  • blkcg_css블록 I/O cgroup의 서브시스템 상태입니다. CONFIG_BLK_CGROUP 설정 시에만 포함되며, kthread_associate_blkcg()로 커널 스레드의 블록 I/O를 특정 cgroup에 귀속시킬 때 사용합니다.
  • full_namecomm가 잘려도 전체 이름을 보관하는 백업 문자열입니다. get_kthread_comm()가 full_name이 있으면 그것을, 없으면 tsk->comm을 반환합니다. free_kthread_struct()에서 해제됩니다.
  • task소유 task_struct 역참조입니다. 워커 스레드의 경우 kthread_worker->task와 별개로, kthread 자체에서 자기 태스크로 되돌아갈 때 사용합니다.
  • affinity_node / preferred_affinity지연 affinity 적용을 위한 전역 연결 리스트 노드와 CPU 마스크입니다. kthread_fetch_affinity()가 스레드가 처음 깨어날 때 이 마스크를 실제 cpus_allowed에 반영하고, CPU가 offline되었다가 online으로 돌아오면 힌트 마스크 기준으로 재바인딩을 시도합니다. kthread_do_exit()에서 kthread_affinity_lock 아래 리스트에서 제거하며 마스크를 해제합니다.

struct kthread_worker / kthread_work — 필드 상세

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

/* 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_list와 current_work 접근을 직렬화하는 raw 스핀락(Spinlock)입니다. kthread_queue_work()와 kthread_worker_fn() 양측에서 반드시 이 잠금(Lock)을 획득해야 합니다.
  • work_list큐잉된 kthread_work 항목의 연결 리스트(Linked List)입니다. 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가 어느 워커에 큐잉되어 있는지 역참조(Dereference)합니다. 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가 실행되지 않아, 지연 민감 태스크의 방해를 방지합니다.
  • 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)
{
    static const struct sched_param param = { .sched_priority = 0 };
    struct kthread_create_info *create = _create;
    int (*threadfn)(void *data) = create->threadfn;
    void *data                  = create->data;
    struct completion *done;
    struct kthread *self;
    int ret;

    self = to_kthread(current);

    /* done을 원자적으로 교환 — 호출자가 치명적 시그널로 죽은 경우 NULL */
    done = xchg(&create->done, NULL);
    if (!done) {
        kfree(create->full_name);
        kfree(create);
        kthread_exit(-EINTR);
    }

    self->full_name = create->full_name;
    self->threadfn = threadfn;
    self->data = data;

    /* kthreadd의 스케줄링 정책/CPUMask를 기본값으로 복원 */
    sched_setscheduler_nocheck(current, SCHED_NORMAL, &param);

    /* 호출자에게 task_struct 포인터 전달 + 완료 통지 */
    __set_current_state(TASK_UNINTERRUPTIBLE);
    create->result = current;
    preempt_disable();
    complete(done);
    schedule_preempt_disabled();
    preempt_enable();

    self->started = 1;  /* 첫 wake-up 완료 표시 */

    /* kthread_bind()나 kthread_affine_preferred()가 호출되지 않았다면
     * housekeeping CPU에 대한 기본 어피니티 적용 */
    if (!(current->flags & PF_NO_SETAFFINITY) && !self->preferred_affinity)
        kthread_affine_node();

    ret = -EINTR;
    if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
        cgroup_kthread_ready();
        __kthread_parkme(self);  /* park 요청 처리 (CPU hotplug 연동) */
        ret = threadfn(data);
    }
    kthread_exit(ret);
}
코드 설명
  • xchg(&create->done, NULL)done 포인터를 원자적으로 교환합니다. 호출자가 치명적 시그널로 죽어서 이미 xchg를 호출했다면 NULL이 반환되며, 이 경우 정리 후 바로 종료합니다.
  • self->full_name/threadfn/data 할당create_info는 complete(done) 직후 호출자에 의해 kfree됩니다. 따라서 struct kthread 내부 필드에 필요한 값들을 사전에 복사합니다.
  • sched_setscheduler_nocheck(SCHED_NORMAL)kthreadd가 상속받은 스케줄링 정책과 CPUMask를 기본값으로 복원합니다. kthreadd의 속성이 변경되었더라도 새 스레드는 항상 SCHED_NORMAL 우선순위 0으로 시작합니다.
  • preempt_disable()/complete/schedule_preempt_disabled/preempt_enablecomplete() 후 schedule() 진입 사이에 선점이 발생하면 호출자의 wait_task_inactive()가 지연됩니다. 선점 금지로 두 동작이 연속적으로 실행되도록 보장합니다.
  • self->started = 1첫 번째 wake-up이 완료됨을 표시합니다. kthread_bind()/kthread_affine_preferred()는 이 플래그가 설정되기 전(즉, 스레드가 아직 슬립 중일 때)에만 호출 가능합니다.
  • kthread_affine_node()kthread_bind()나 kthread_affine_preferred()가 호출되지 않은 경우, housekeeping CPU 집합에 대한 기본 어피니티를 자동으로 적용합니다.
  • __kthread_parkme(self)threadfn 호출 전 park 요청을 처리합니다. CPU가 오프라인 상태였던 per-CPU 스레드가 온라인되면서 처음 깨어난 경우에 park/unpark 동기화를 거칩니다.
  • 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;
    raw_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 동기화를 위해 기록 */
    raw_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로 구현한 것은 컴파일러 최적화(Compiler Optimization)를 위한 것이 아니라, TASK_INTERRUPTIBLE 상태 설정이 루프 최상단에서 이루어지도록 하여 wakeup-miss 레이스를 방지하기 위함입니다.

kthread_worker_fn() 실행 흐름

워커 종료 return 0 외부: kthread_queue_work() 큐에 작업 추가 repeat: 루프 시작 set_current_state() (TASK_INTERRUPTIBLE) kthread_should_stop() 종료 요청 확인 work_list에서 work 추출 work->func(work) 현재 작업 실행 Yes 큐잉 schedule()로 슬립 work == NULL 일 때 work == NULL try_to_freeze()와 cond_resched()를 거친 뒤 repeat로 복귀 current_work는 다음 반복에서 새 값으로 덮어써집니다
kthread_worker_fn() 실행 사이클 — 큐에 작업이 있으면 실행하고, 없으면 슬립하며, 종료 요청이 오면 워커를 정리하고 반환합니다 (상세 구현, kernel/kthread.c 기준)

Freezer 연동

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

Suspend 경로 1. pm_suspend() 호출 2. freeze_processes() — 유저 프로세스 동결 3. freeze_kernel_threads() — freezable kthread 동결 4. 각 freezable 스레드에 TIF_FROZEN 설정 5. __refrigerator()에서 FROZEN 상태로 슬립 6. 모든 스레드 동결 완료 → suspend_enter() thaw될 때까지 TASK_UNINTERRUPTIBLE Resume 경로 1. 하드웨어 인터럽트로 깨어남 2. 디바이스 드라이버 resume 콜백 3. thaw_kernel_threads() — kthread 해동 4. __thaw_task(): FROZEN 플래그 해제 5. wake_up_state(TASK_UNINTERRUPTIBLE) 6. thaw_processes() → 유저 프로세스 해동 해동 후 TASK_RUNNING으로 복귀 동결된 커널 스레드는 TASK_UNINTERRUPTIBLE로 대기하므로 suspend 중 디스크에 접근해 데이터가 손상되지 않습니다 freezable 스레드는 kthread_freezable_should_stop(), wait_event_freezable() 등 freeze 안전 API를 사용해야 합니다
Suspend/Resume 과정에서의 커널 스레드 freezer 동작 — suspend 시 동결해 I/O를 멈추고, resume 시 플래그를 해제해 작업을 재개합니다 (개념 개요)

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()라는 커널 내부 플래그 기반의 제어 메커니즘을 사용합니다.

유저 프로세스 시그널 1. SIGTERM, SIGKILL, SIGUSR1 등 수신 2. signal_pending() — sighandler 호출 3. do_signal() — 유저 핸들러 실행 시그널 기반 제어 (표준 POSIX) kill, sigaction, signal 등 커널 스레드 제어 1. ignore_signals() — 모든 시그널 차단 2. KTHREAD_SHOULD_STOP — 종료 요청 3. KTHREAD_SHOULD_PARK — 일시 중지 요청 플래그 기반 제어 (커널 내부) kthread_stop(), kthread_park() 등 대조 kthread 제어 메커니즘 흐름 외부 호출자 set_bit() KTHREAD_SHOULD_STOP + wake_up_process() 스레드 깨어남 kthread_should_stop() 확인 후 루프 탈출 kthread_exit()로 종료 시그널과 달리 비동기 인터럽트 없이 협력적(cooperative)으로 동작합니다
유저 프로세스 시그널과 커널 스레드 제어 비교 — 커널 스레드는 시그널을 무시하고 플래그 설정과 깨우기로 종료와 일시 중지를 요청합니다 (개념 개요)

시그널 차단 메커니즘

/* 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_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 kthread 종료 sched_kthread_stop kthread_work 실행 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 상태와 통계 확인 이벤트 필터링으로 필요한 신호만 골라 수집하면 측정 오버헤드를 줄일 수 있습니다
커널 스레드 모니터링 도구 체계 — 커널의 트레이스포인트와 동적 계측 이벤트를 유저 공간의 ftrace, perf, bpftrace, proc 인터페이스가 수집합니다 (개념 개요)

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가 오버헤드가 낮으므로 더 적합합니다.

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

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

비동기 작업이 필요합니다 softirq / BH workqueue 작업이 슬립 없이 끝나야 함 No 작업이 슬립(블록)할 수 있는가? Yes workqueue (CMWQ) 워커 풀이 병렬 처리 No 전용 스레드가 필요한가? Yes workqueue (CMWQ) 유지 직전 질문에서 No가 나온 경우 별도 스레드·hotplug 불필요 순차 실행 / RT 우선순위 필요? No Yes kthread_worker 전용 스레드에서 순차 실행 Yes per-CPU + hotplug 관리 필요? No smpboot 프레임워크 CPU 핫플러그 자동 관리 Yes 마지막 질문에 No라면 앞선 workqueue (CMWQ) 선택을 그대로 사용합니다 슬립 가능 여부와 CPU 고정 필요 여부가 가장 먼저 결정할 기준입니다
커널 비동기 작업 메커니즘 선택 가이드 — 슬립 가능 여부부터 전용 스레드, 순차 실행과 실시간 우선순위, per-CPU 핫플러그 관리까지 좁혀 나갑니다 (개념 개요)

메커니즘 종합 비교

특성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_queue_delayed_work() delay (jiffies) 지정 delayed_work_list에 등록 add_timer()로 타이머 시작 타이머 만료 delayed_work_list에서 제거 work_list로 이동 wake_up_process() 취소 kthread_cancel_delayed_work_sync() 타이머 취소 + delayed_work_list에서 제거 실행 워커 스레드에서 work->func(work) 실행 전용 스레드 하나에서 순차 처리 kthread_mod_delayed_work()로 이미 등록한 지연 시간을 바꿀 수 있습니다 func() 안에서 kthread_queue_delayed_work()를 다시 호출하면 주기적 실행이 됩니다 workqueue의 schedule_delayed_work()와 유사하지만 전용 스레드에서 실행됨이 보장됩니다
kthread_delayed_work의 타이머 기반 큐잉 흐름 — delay는 jiffies 단위이며 만료 시 work_list로 옮겨져 워커 스레드에서 실행됩니다 (개념 개요)

사용 사례별 권장 패턴

사용 사례권장 메커니즘이유
네트워크 패킷(Packet) 수신 처리softirq (NAPI)지연 최소화, 높은 처리량(Throughput) 필요
디바이스 드라이버 초기화 작업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)커널 내장, 자동 관리
파일시스템 저널 커밋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 기사

커널 소스 코드

서적

필수 관련 문서:
보조 관련 문서: