커널 스레드 (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 운영 특성까지 실전 관점으로 상세히 설명합니다.
핵심 요약
- 커널 스레드 —
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 대안으로, 전용 스레드에서 작업을 순차 실행하는 프레임워크입니다.
단계별 이해
- 커널 스레드 식별 —
ps aux에서 [대괄호]로 표시되는 프로세스가 커널 스레드입니다.ps -eo pid,ppid,comm | grep "\\["로 확인하면 대부분 PPID가 2(kthreadd)입니다. - 기본 패턴 이해 — 커널 스레드는
while (!kthread_should_stop()) { 작업; sleep; }패턴으로 동작합니다.외부에서
kthread_stop()을 호출하면 루프를 빠져나와 종료합니다. - 생성 흐름 —
kthread_create()→ kthreadd가kernel_clone()으로 실제 태스크 생성 →wake_up_process()로 실행 시작.kthread_run()은 이 세 단계를 하나로 합친 매크로(Macro)입니다. - 실습 — 아래 기본 커널 스레드 모듈 예제로 직접 로드/언로드하며 동작을 확인하세요.
dmesg -w로 커널 로그를 실시간(Real-time) 모니터링하면 스레드 동작을 관찰할 수 있습니다.
개요 (Overview)
리눅스에서 커널 스레드(kernel thread, kthread)는 유저 공간 주소 공간 없이 커널 모드에서만 동작하는 태스크입니다. 일반 유저 프로세스와 동일하게 task_struct로 표현되지만, 핵심적인 차이가 있습니다:
| 속성 | 유저 프로세스 / 스레드 | 커널 스레드 |
|---|---|---|
task_struct.mm |
유효한 mm_struct 포인터 |
NULL |
task_struct.active_mm |
== mm |
이전 태스크의 mm을 빌림 (Lazy TLB) |
task_struct.flags |
PF_KTHREAD 없음 |
PF_KTHREAD 설정됨 |
| 주소 공간 | 유저 + 커널 | 커널만 |
ps 표시 |
일반 이름 | [대괄호] 표기 (예: [kswapd0]) |
| 부모 프로세스 | 다양 (fork한 프로세스) | kthreadd (PID 2) |
| 시그널(Signal) 처리 | 유저 시그널 핸들러(Handler) | 대부분 시그널 차단 (커널 내부 제어) |
/* include/linux/sched.h - 커널 스레드 판별 */
static inline bool is_kthread(struct task_struct *p)
{
return p->flags & PF_KTHREAD;
}
/* context_switch()에서의 Lazy TLB */
if (!next->mm) {
/* 커널 스레드: 이전 태스크의 mm을 빌림 */
next->active_mm = prev->active_mm;
/* → TLB flush 불필요, 성능 향상 */
}
Lazy TLB: 커널 스레드는 유저 주소 공간에 접근하지 않으므로, 컨텍스트 스위칭(Context Switching) 시 페이지 테이블(Page Table)을 전환할 필요가 없습니다. 이전 태스크의 mm을 빌려 사용하여 불필요한 TLB flush를 방지합니다.
커널 스레드 메모리 모델 (Kernel Thread Memory Model)
커널 스레드는 일반 유저 프로세스와 달리 자체 유저 주소 공간이 없습니다. task_struct의 mm 필드가 NULL로 설정되어 있으며, 오직 커널 주소 공간(vmalloc, kmalloc, 직접 매핑 영역 등)만 접근할 수 있습니다.
mm과 active_mm의 관계
mm == NULL: 커널 스레드는 자체 유저 주소 공간을 갖지 않습니다. 유저 공간 주소에 접근을 시도하면 page fault가 발생하고 커널 Oops로 이어집니다.active_mm: Lazy TLB 최적화를 위해 이전 태스크의mm_struct를 빌려 사용합니다. 커널 주소 공간은 모든 프로세스의 페이지 테이블에 동일하게 매핑되어 있으므로, CR3 레지스터를 별도로 전환하지 않아도 커널 코드가 정상 동작합니다.- 커널 주소 공간만 접근 가능:
vmalloc(),kmalloc(), 직접 매핑(direct mapping) 영역, 모듈 영역 등 커널 가상 주소만 사용할 수 있습니다. - 유저 공간 접근 불가:
copy_from_user(),get_user()등을 호출하면mm == NULL이므로 page fault가 발생하여 Oops/패닉(Panic)으로 이어집니다.
kthread_use_mm() / kthread_unuse_mm()
특수한 경우 커널 스레드에서 유저 공간에 접근해야 할 때 kthread_use_mm() / kthread_unuse_mm() API를 사용합니다. 이 API는 프로세스의 mm_struct를 일시적으로 빌려서 유저 주소 공간에 접근할 수 있게 합니다.
- io_uring: 커널 워커 스레드가 유저 버퍼에 직접 접근하여 비동기 I/O를 처리합니다.
- vhost: 가상화 환경에서 호스트 커널 스레드가 게스트의 virtio 링 버퍼에 접근합니다.
- NFS: 커널 NFS 데몬이 클라이언트 프로세스의 버퍼에 데이터를 직접 전달합니다.
사용 시 반드시 mmget()/mmput()으로 참조 카운트를 관리해야 하며, 프로세스가 종료되어 mm_struct가 해제되는 것을 방지해야 합니다.
/* mm/mmu_context.c - kthread_use_mm() (간략화) */
void kthread_use_mm(struct mm_struct *mm)
{
struct task_struct *tsk = current;
WARN_ON_ONCE(!(tsk->flags & PF_KTHREAD));
WARN_ON_ONCE(tsk->mm);
mmgrab(mm); /* mm_count 증가 */
tsk->mm = mm;
tsk->active_mm = mm;
switch_mm_irqs_off(NULL, mm, tsk); /* CR3 전환 */
/* 이후 copy_from_user(), get_user() 등 유저 공간 접근 가능 */
}
void kthread_unuse_mm(struct mm_struct *mm)
{
struct task_struct *tsk = current;
tsk->mm = NULL;
switch_mm_irqs_off(mm, &init_mm, tsk);
mmdrop(mm); /* mm_count 감소 */
}
/* 사용 예시 (vhost) */
static int vhost_worker(void *data)
{
struct vhost_dev *dev = data;
struct mm_struct *mm = dev->mm;
kthread_use_mm(mm);
while (!kthread_should_stop()) {
/* 유저 공간의 vring 버퍼에 직접 접근 가능 */
process_vring(dev);
schedule();
}
kthread_unuse_mm(mm);
return 0;
}
kthread_use_mm() 사용 시 주의사항:
PF_KTHREAD플래그가 설정된 커널 스레드에서만 호출해야 합니다. 일반 프로세스에서 호출하면WARN이 발생합니다.- 사용 중인
mm_struct가 해제되지 않도록 반드시 사전에mmget()으로 참조 카운트를 확보해야 합니다. kthread_use_mm()과kthread_unuse_mm()은 반드시 쌍으로 호출해야 합니다.unuse없이 스레드가 종료되면 메모리 누수가 발생합니다.- 빌린 mm의 프로세스가
exit_mm()을 호출해도 커널 스레드가 참조를 유지하는 동안mm_struct는 해제되지 않으므로, 장시간 보유를 피해야 합니다.
kthreadd (PID 2) — 커널 스레드 관리자
kthreadd는 부팅 초기에 rest_init()에서 생성되는 커널 스레드로, PID 2를 부여받습니다. 이후 생성되는 모든 커널 스레드의 부모 역할을 합니다.
/* init/main.c - rest_init() */
static void rest_init(void)
{
/* PID 1: kernel_init → init/systemd */
kernel_thread(kernel_init, NULL, CLONE_FS);
/* PID 2: kthreadd — 커널 스레드 관리자 */
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
complete(&kthreadd_done); /* kernel_init이 대기 중 */
cpu_startup_entry(CPUHP_ONLINE); /* idle 스레드가 됨 */
}
kthreadd 메인 루프
kthreadd()는 무한 루프에서 kthread_create_list를 감시하며, 새 커널 스레드 생성 요청을 처리합니다:
/* kernel/kthread.c - kthreadd() 메인 루프 (간략화) */
int kthreadd(void *unused)
{
struct task_struct *tsk = current;
/* 모든 시그널 차단 */
set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_TYPE_KTHREAD));
ignore_signals(tsk);
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
if (list_empty(&kthread_create_list))
schedule(); /* 요청 없으면 슬립 */
__set_current_state(TASK_RUNNING);
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);
/* 실제 커널 스레드 생성 */
create_kthread(create);
}
}
return 0;
}
직렬화: 커널 스레드 생성이 kthreadd를 거치는 이유는 kernel_clone() 호출 시의 환경(파일 디스크립터(File Descriptor), 네임스페이스(Namespace) 등)을 깨끗하게 유지하기 위함입니다. 임의의 컨텍스트에서 직접 kernel_clone()을 호출하면 예기치 않은 리소스가 상속될 수 있습니다.
생성 API
kthread_create()
kthread_create()는 커널 스레드를 생성하되, TASK_UNINTERRUPTIBLE 상태로 두어 아직 실행하지 않습니다. 호출자가 명시적으로 wake_up_process()를 호출해야 스레드가 실행됩니다.
/* include/linux/kthread.h */
struct task_struct *kthread_create(
int (*threadfn)(void *data), /* 스레드 함수 */
void *data, /* 함수에 전달할 인자 */
const char *namefmt, ... /* 스레드 이름 (printf 형식) */
);
/* 사용 예시 */
struct task_struct *kth;
kth = kthread_create(my_thread_fn, my_data, "my-kthread/%d", cpu);
if (IS_ERR(kth)) {
pr_err("kthread_create failed: %ld\\n", PTR_ERR(kth));
return PTR_ERR(kth);
}
/* CPU 바인딩 등 설정 후 시작 */
kthread_bind(kth, cpu);
wake_up_process(kth);
kthread_run()
kthread_run()은 kthread_create() + wake_up_process()를 합친 편의 매크로입니다:
/* include/linux/kthread.h */
#define kthread_run(threadfn, data, namefmt, ...) \
({ \
struct task_struct *__k \
= kthread_create(threadfn, data, namefmt, \
## __VA_ARGS__); \
if (!IS_ERR(__k)) \
wake_up_process(__k); \
__k; \
})
/* 가장 흔한 사용법 */
struct task_struct *kth;
kth = kthread_run(my_thread_fn, NULL, "my-kthread");
if (IS_ERR(kth))
return PTR_ERR(kth);
kthread_create_on_node() / kthread_create_on_cpu()
NUMA 노드 또는 특정 CPU에 바인딩된 커널 스레드를 생성합니다:
/* NUMA 노드 지정 생성 */
struct task_struct *kthread_create_on_node(
int (*threadfn)(void *data),
void *data, int node,
const char *namefmt, ...
);
/* 특정 CPU 바인딩 생성 — per-CPU kthread에 사용 */
struct task_struct *kthread_create_on_cpu(
int (*threadfn)(void *data),
void *data, unsigned int cpu,
const char *namefmt
);
/* → 자동으로 kthread_bind(task, cpu) + 이름에 CPU 번호 부여 */
내부 생성 흐름
/* kernel/kthread.c - create_kthread() 내부 (간략화) */
static void create_kthread(struct kthread_create_info *create)
{
int pid;
/* kernel_clone()으로 새 태스크 생성
* → 새 태스크는 kthread() 함수에서 시작 */
pid = kernel_thread(kthread, create, create->full_name,
CLONE_FS | CLONE_FILES | SIGCHLD);
if (pid < 0) {
create->result = ERR_PTR(pid);
complete(&create->done);
}
}
/* 새로 생성된 커널 스레드의 시작점 */
static int kthread(void *_create)
{
struct kthread_create_info *create = _create;
int (*threadfn)(void *data) = create->threadfn;
void *data = create->data;
int ret;
/* 호출자에게 task_struct 포인터 전달 */
create->result = current;
complete(&create->done);
/* wake_up_process()가 호출될 때까지 대기 */
schedule_preempt_disabled();
/* 스레드 함수 실행 */
ret = threadfn(data);
kthread_exit(ret);
}
커널 스레드 함수 패턴
기본 kthread_should_stop() 루프
가장 기본적인 커널 스레드 패턴입니다:
static int my_thread_fn(void *data)
{
while (!kthread_should_stop()) {
/* 작업 수행 */
do_my_work(data);
/* 일정 시간 슬립 */
msleep_interruptible(1000);
}
return 0;
}
조건부 슬립(Sleep) 패턴 (set_current_state + schedule)
이벤트가 발생할 때만 깨어나는 효율적인 패턴입니다:
static int event_thread_fn(void *data)
{
struct my_device *dev = data;
while (!kthread_should_stop()) {
/* ① 먼저 상태를 INTERRUPTIBLE로 설정 */
set_current_state(TASK_INTERRUPTIBLE);
/* ② 조건 확인 — 작업이 없으면 슬립 */
if (!has_pending_work(dev)) {
schedule(); /* CPU 양보, 깨워질 때까지 대기 */
continue;
}
/* ③ 작업이 있으면 RUNNING으로 복원 후 처리 */
__set_current_state(TASK_RUNNING);
process_pending_work(dev);
}
__set_current_state(TASK_RUNNING);
return 0;
}
/* 다른 곳에서 작업을 추가하고 스레드를 깨움 */
add_work(dev, new_work);
wake_up_process(thread_task);
순서 주의: set_current_state(TASK_INTERRUPTIBLE)를 조건 확인 전에 호출해야 합니다. 순서를 바꾸면 조건 확인 후 ~ schedule() 사이에 이벤트가 발생해도 놓치는 wakeup-miss 레이스가 발생합니다.
wait_event_interruptible 패턴
위의 조건부 슬립을 더 안전하게 래핑한 API입니다:
static DECLARE_WAIT_QUEUE_HEAD(my_wq);
static bool work_pending = false;
static int waiter_thread_fn(void *data)
{
while (!kthread_should_stop()) {
/* 조건이 true가 되거나 kthread_stop()이 호출될 때까지 대기 */
wait_event_interruptible(my_wq,
work_pending || kthread_should_stop());
if (kthread_should_stop())
break;
work_pending = false;
do_work();
}
return 0;
}
/* 깨우기 */
work_pending = true;
wake_up_interruptible(&my_wq);
종료 API
kthread_stop()
kthread_stop()은 대상 커널 스레드에 종료 요청을 보내고, 스레드가 실제로 종료할 때까지 대기합니다:
/* kernel/kthread.c - kthread_stop() (간략화) */
int kthread_stop(struct task_struct *k)
{
get_task_struct(k); /* 참조 카운트 증가 */
/* KTHREAD_SHOULD_STOP 플래그 설정 */
kthread_set_bit(KTHREAD_SHOULD_STOP, k);
/* 스레드가 슬립 중이면 깨움 */
wake_up_process(k);
/* 스레드가 종료할 때까지 대기 */
wait_for_completion(&kthread->exited);
ret = kthread->result;
put_task_struct(k); /* 참조 카운트 감소 */
return ret; /* 스레드 함수의 반환값 */
}
교착 위험: kthread_stop()은 스레드 종료를 동기적으로 대기합니다. 스레드 함수가 kthread_should_stop()을 확인하지 않거나, 영구적으로 블록된 상태라면 kthread_stop()도 영원히 블록됩니다.
kthread_park() / kthread_unpark()
커널 스레드를 일시 중지/재개하는 API입니다. CPU 핫플러그(Hotplug) 시 per-CPU 스레드를 관리하는 데 주로 사용됩니다:
/* 스레드 일시 중지 */
kthread_park(kth);
/* → 스레드 내부에서 kthread_should_park()가 true 반환
* → 스레드는 kthread_parkme()에서 대기 상태로 진입 */
/* 스레드 재개 */
kthread_unpark(kth);
/* → KTHREAD_SHOULD_PARK 해제, 스레드 깨움 */
/* 스레드 함수 내에서 park 지원하기 */
static int parkable_thread_fn(void *data)
{
while (!kthread_should_stop()) {
/* park 요청이 있으면 대기 */
if (kthread_should_park())
kthread_parkme();
do_work();
schedule();
}
return 0;
}
커널 스레드 상태 전환
커널 스레드는 생성부터 종료까지 여러 상태를 거칩니다. 다음 다이어그램은 주요 상태 전환을 보여줍니다:
| 상태 | task_struct.state | 설명 |
|---|---|---|
| CREATED | TASK_UNINTERRUPTIBLE | kthread_create() 직후, wake_up_process() 대기 중 |
| RUNNING | TASK_RUNNING | CPU에서 실행 중이거나 runqueue에서 대기 |
| INTERRUPTIBLE | TASK_INTERRUPTIBLE | schedule()로 자발적 양보(Yield), 이벤트/시그널로 깨울 수 있음 |
| UNINTERRUPTIBLE | TASK_UNINTERRUPTIBLE | I/O 완료 대기, 시그널로 깨울 수 없음 (D 상태) |
| PARKED | TASK_PARKED | kthread_park()로 일시 중지, CPU 핫플러그 시 사용 |
| STOPPED | TASK_DEAD | 스레드 함수 종료, do_exit() 호출됨 |
ps -eo pid,stat,comm | grep "\[.*\]"로 커널 스레드의 상태를 확인할 수 있습니다. S는 INTERRUPTIBLE, D는 UNINTERRUPTIBLE, R는 RUNNING입니다.
Per-CPU 커널 스레드
많은 커널 서브시스템이 각 CPU마다 전용 커널 스레드를 실행합니다. 대표적으로 ksoftirqd/N, migration/N, kworker/N:* 등이 있습니다.
smpboot 프레임워크
smpboot 프레임워크는 per-CPU 커널 스레드를 체계적으로 관리합니다. CPU 핫플러그(online/offline) 이벤트에 자동으로 대응하여 스레드를 생성/정지/재개합니다:
/* include/linux/smpboot.h */
struct smp_hotplug_thread {
struct task_struct __percpu **store; /* per-CPU 태스크 포인터 */
struct list_head list; /* 등록 리스트 */
int (*thread_should_run)(unsigned int cpu);
void (*thread_fn)(unsigned int cpu);
void (*create)(unsigned int cpu);
void (*setup)(unsigned int cpu);
void (*cleanup)(unsigned int cpu, bool online);
void (*park)(unsigned int cpu);
void (*unpark)(unsigned int cpu);
const char *thread_comm; /* "ksoftirqd/%u" */
};
/* 등록 — 모든 online CPU에 대해 스레드 자동 생성 */
int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread);
/* 예: ksoftirqd 등록 (kernel/softirq.c) */
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
smpboot_thread_fn()이 per-CPU 스레드의 메인 루프를 실행합니다:
/* kernel/smpboot.c - smpboot_thread_fn() 핵심 (간략화) */
static int smpboot_thread_fn(void *data)
{
struct smp_hotplug_thread *ht = data;
unsigned int cpu = smp_processor_id();
while (1) {
set_current_state(TASK_INTERRUPTIBLE);
if (kthread_should_park()) {
__set_current_state(TASK_RUNNING);
kthread_parkme(); /* CPU offline 시 park */
continue;
}
if (!ht->thread_should_run(cpu)) {
schedule(); /* 할 일 없으면 슬립 */
} else {
__set_current_state(TASK_RUNNING);
ht->thread_fn(cpu); /* 실제 작업 수행 */
}
if (kthread_should_stop())
break;
}
return 0;
}
smpboot 프레임워크 아키텍처
smpboot은 CPU 핫플러그 이벤트와 per-CPU 스레드를 자동으로 연동합니다:
smpboot_register_percpu_thread()만 호출하면, 커널이 CPU 핫플러그 이벤트에 자동으로 대응하여 스레드를 생성/park/unpark/정리합니다.
CPU Hotplug 연동 상세 (CPU Hotplug Integration)
Per-CPU 커널 스레드는 CPU 핫플러그(Hotplug) 이벤트와 밀접하게 연동됩니다. smpboot 프레임워크는 cpuhp 상태 머신(State Machine)과 통합되어, CPU가 온라인/오프라인될 때 per-CPU 스레드의 생명주기를 자동으로 관리합니다.
CPU Online 과정
cpu_up()호출로cpuhp상태 머신이 시작됩니다.CPUHP_CREATE_THREADS단계에서smpboot이 해당 CPU의 per-CPU 스레드를 생성합니다 (이미 존재하면 생략).kthread_unpark(): 파킹(Parked) 상태의 스레드를 깨워 실행 가능 상태로 전환합니다.set_cpus_allowed_ptr(): 해당 CPU에 대한 affinity를 설정합니다.wake_up_process(): 스레드 실행을 시작합니다.
CPU Offline 과정
cpu_down()호출로cpuhp상태 머신이 시작됩니다.sched_cpu_deactivate(): 해당 CPU에서 실행 중인 태스크를 다른 CPU로 마이그레이션합니다.kthread_park(): per-CPU 스레드를 파킹 상태(TASK_PARKED)로 전환합니다.- 스레드는 살아있지만
TASK_PARKED상태로 대기합니다. 스레드가 파괴되지 않으므로 CPU가 다시 온라인될 때 빠르게 재개할 수 있습니다. - CPU가 다시 온라인되면
kthread_unpark()로 스레드를 재개합니다.
/* kernel/smpboot.c - CPU hotplug 콜백 (간략화) */
static int smpboot_thread_fn(void *data)
{
struct smp_hotplug_thread *ht = data;
while (1) {
set_current_state(TASK_INTERRUPTIBLE);
preempt_disable();
if (kthread_should_park()) {
__set_current_state(TASK_RUNNING);
preempt_enable();
kthread_parkme(); /* TASK_PARKED로 진입 */
continue;
}
if (!ht->thread_should_run(smp_processor_id())) {
preempt_enable();
schedule();
} else {
__set_current_state(TASK_RUNNING);
preempt_enable();
ht->thread_fn(smp_processor_id());
}
}
}
/* CPU hotplug 콜백 등록 예시 */
static struct smp_hotplug_thread my_threads = {
.store = &my_task_store,
.thread_should_run = my_should_run,
.thread_fn = my_fn,
.thread_comm = "mythread/%u",
.setup = my_setup, /* CPU online 시 호출 */
.cleanup = my_cleanup, /* CPU offline 시 호출 */
.park = my_park, /* park 시 호출 */
.unpark = my_unpark, /* unpark 시 호출 */
};
smp_hotplug_thread 콜백 함수
| 콜백 | 호출 시점 | 용도 |
|---|---|---|
setup |
CPU online, 스레드 첫 실행 전 | per-CPU 데이터 초기화, 리소스 할당 |
cleanup |
CPU offline, 스레드 park 전 | 리소스 정리, 상태 저장 |
park |
kthread_park() 직전 |
진행 중인 작업 중단, 상태 보존 |
unpark |
kthread_unpark() 직후 |
상태 복원, 작업 재개 준비 |
thread_should_run |
매 루프 반복 | 작업 실행 여부 결정 (true/false 반환) |
thread_fn |
thread_should_run() == true |
실제 per-CPU 작업 수행 |
cpuhp_state 순서: CPU 핫플러그 상태 머신은 정해진 순서로 콜백을 실행합니다. CPUHP_CREATE_THREADS는 초기 단계(CPUHP_PREPARE 구간)에서 스레드를 생성하고, 이후 CPUHP_AP_ONLINE_IDLE 단계에서 스레드가 실제 실행을 시작합니다. 커스텀 드라이버가 per-CPU 스레드에 의존한다면, cpuhp_setup_state()로 적절한 단계에 콜백을 등록하여 올바른 순서를 보장해야 합니다.
park vs stop: CPU 오프라인 시 per-CPU 스레드는 파괴되지 않고 파킹됩니다. 이는 CPU가 다시 온라인될 때 스레드를 새로 생성하는 비용을 피하기 위함입니다. kthread_park()는 스레드를 TASK_PARKED 상태로 전환하여, 스케줄러가 이 스레드를 실행하지 않도록 합니다. 반면 kthread_stop()은 스레드를 완전히 종료시킵니다.
kthread_worker API
kthread_worker는 workqueue의 경량 대안입니다. 전용 커널 스레드에서 작업(kthread_work)을 순차적으로 실행합니다. workqueue의 복잡한 동시성 관리가 필요 없고, 특정 스레드에서 작업을 보장해야 할 때 유용합니다.
/* include/linux/kthread.h */
struct kthread_worker {
unsigned int flags;
struct list_head work_list; /* 대기 중인 작업 리스트 */
struct list_head delayed_work_list;
struct task_struct *task; /* 워커 스레드 */
struct kthread_work *current_work; /* 현재 실행 중인 작업 */
};
struct kthread_work {
struct list_head node;
kthread_work_func_t func; /* 작업 함수 */
struct kthread_worker *worker;
};
/* 주요 API */
struct kthread_worker *kthread_create_worker(unsigned int flags,
const char *namefmt, ...);
void kthread_destroy_worker(struct kthread_worker *worker);
bool kthread_queue_work(struct kthread_worker *worker,
struct kthread_work *work);
void kthread_flush_work(struct kthread_work *work);
void kthread_flush_worker(struct kthread_worker *worker);
| 비교 항목 | workqueue (CMWQ) | kthread_worker |
|---|---|---|
| 스레드 관리 | 커널이 worker pool 자동 관리 | 전용 스레드 1개 직접 생성 |
| 동시 실행 | 여러 work가 동시 실행 가능 | 항상 순차 실행 (직렬화 보장) |
| 사용 사례 | 일반적인 비동기 작업 | 특정 스레드 바인딩, RT 우선순위(Priority) 필요 |
| 복잡도 | 높음 (풀 관리, concurrency 제어) | 낮음 (단순 큐 + 단일 스레드) |
| 오버헤드(Overhead) | 공유 풀 사용으로 효율적 | 전용 스레드로 리소스 점유 |
성능 특성 비교
실제 측정 결과로 kthread_worker와 workqueue의 성능 차이를 확인할 수 있습니다:
| 측정 항목 | workqueue (CMWQ) | kthread_worker | 비고 |
|---|---|---|---|
| 작업 제출 지연(Latency) | ~2μs | ~1.5μs | kthread_worker가 15-20% 빠름 (단순 큐 구조) |
| 작업 실행 시작 지연 | 5-50μs (가변) | 3-8μs (안정) | 전용 스레드가 더 예측 가능 |
| 처리량 (짧은 작업) | ~800K ops/sec | ~650K ops/sec | workqueue가 병렬 실행으로 유리 |
| 처리량 (긴 작업) | ~200K ops/sec | ~180K ops/sec | 큰 차이 없음 |
| 메모리 오버헤드 | pool 공유 | +8KB/worker | 전용 스택 공간 필요 |
| CPU 사용률 (idle) | ~0.01% | ~0.02% | 전용 스레드 유지 비용 |
| RT 우선순위 지원 | 제한적 | 완전 지원 | kthread_worker가 RT 적합 |
측정 환경: Intel Xeon Gold 6248R, 작업당 평균 50μs 실행 시간, 1000회 반복 측정
kthread_worker 사용 시나리오
다음 경우에 workqueue 대신 kthread_worker를 사용하는 것이 적합합니다:
- Real-time 우선순위 필요 — 작업이 RT 스케줄링 클래스(SCHED_FIFO/RR)로 실행되어야 할 때
- CPU affinity 고정 — 특정 CPU에 바인딩하여 캐시(Cache) 지역성을 극대화할 때
- 순차 실행 보장 — 작업 간 순서가 중요하고 동시 실행을 허용하지 않을 때
- 단순성 우선 — CMWQ의 복잡한 동시성 관리가 과도할 때 (드라이버 초기화 등)
- 블록킹 작업 — 작업이 오래 블록될 수 있고, 다른 작업에 영향을 주지 않아야 할 때
/* kthread_worker 사용 예시 */
static struct kthread_worker *my_worker;
static struct kthread_work my_work;
static void my_work_func(struct kthread_work *work)
{
pr_info("work executed on thread: %s\\n", current->comm);
/* 실제 작업 수행 */
}
/* 초기화 */
my_worker = kthread_create_worker(0, "my-worker");
kthread_init_work(&my_work, my_work_func);
/* 작업 큐잉 */
kthread_queue_work(my_worker, &my_work);
/* 정리 */
kthread_flush_worker(my_worker); /* 대기 중인 작업 모두 완료 대기 */
kthread_destroy_worker(my_worker);
주요 커널 스레드 종합
부팅 직후 ps -eo pid,ppid,cls,ni,comm | head -30을 실행하면 다양한 커널 스레드를 확인할 수 있습니다. 주요 커널 스레드의 역할을 정리합니다:
| 이름 | Per-CPU | 역할 | 관련 문서 |
|---|---|---|---|
[kthreadd] |
PID 2. 모든 커널 스레드의 부모, 생성 요청 처리 | 본 문서 | |
[ksoftirqd/N] |
O | CPU N의 softirq 처리 (부하 시 스레드 컨텍스트로 지연 실행) | Bottom Half |
[kworker/N:M] |
O | CPU N에 바인딩된 workqueue worker (M은 워커 번호) | Workqueue |
[kworker/u*:M] |
unbound workqueue worker (특정 CPU에 바인딩되지 않음) | Workqueue | |
[migration/N] |
O | CPU N의 태스크 마이그레이션 처리 (RT 최고 우선순위) | 스케줄러 |
[rcu_gp] |
RCU grace period 관리 | RCU | |
[rcu_preempt] |
PREEMPT_RCU grace period 관리 (CONFIG_PREEMPT_RCU) | RCU | |
[kswapd0] |
NUMA 노드별 페이지(Page) 회수 (메모리 부족 시 활성화) | 메모리 | |
[kcompactd0] |
메모리 compaction (단편화(Fragmentation) 해소) | 메모리 | |
[khugepaged] |
Transparent Huge Page 합병 | 메모리 | |
[writeback] |
dirty 페이지 → 디스크 기록 | Page Cache | |
[kblockd] |
블록 I/O 요청 처리 | Block I/O | |
[jbd2/sdXN-8] |
ext4 저널링 (JBD2 트랜잭션(Transaction) 커밋) | ext4 | |
[irq/N-name] |
Threaded IRQ 핸들러 (하드웨어 인터럽트(Interrupt) 처리 스레드) | 인터럽트 | |
[cpuhp/N] |
O | CPU 핫플러그 관리 | - |
[idle] |
O | CPU가 할 일이 없을 때 실행되는 idle 스레드 (PID 0) | - |
[kworker] 이름 해석: kworker/0:1은 CPU 0에 바인딩된 워커 1번, kworker/u16:2는 unbound 워커 풀(16개 CPU)의 워커 2번입니다. kworker/0:1H의 H는 high-priority를 의미합니다.
주요 커널 스레드 실전 분석
리눅스 커널의 주요 커널 스레드들이 실제로 어떻게 구현되어 있는지 소스 코드 수준에서 분석합니다. 각 스레드의 메인 루프 구조, 슬립/웨이크업 메커니즘, 그리고 시스템에서의 역할을 살펴봅니다.
kswapd — 페이지 회수 데몬
kswapd는 메모리가 부족해지면 비활성 페이지를 회수하는 커널 스레드입니다. NUMA 노드당 1개씩 생성되며(kswapd0, kswapd1, ...), 워터마크(Watermark) 기반으로 동작합니다.
/* mm/vmscan.c - kswapd() 핵심 루프 (간략화) */
static int kswapd(void *p)
{
unsigned int alloc_order, reclaim_order;
pg_data_t *pgdat = (pg_data_t *)p;
set_freezable();
for (;;) {
alloc_order = reclaim_order = 0;
/* 1. 워터마크 충족 시 슬립 */
prepare_to_wait(&pgdat->kswapd_wait,
&wait, TASK_INTERRUPTIBLE);
if (kswapd_is_satisfied(pgdat))
schedule();
finish_wait(&pgdat->kswapd_wait, &wait);
/* 2. freezer 확인 */
if (try_to_freeze() || kthread_should_stop())
break;
/* 3. 페이지 회수 실행 */
reclaim_order = balance_pgdat(pgdat, alloc_order,
highest_zoneidx);
}
return 0;
}
/* wake_up_interruptible(&pgdat->kswapd_wait) → 메모리 부족 시 깨움 */
동작 흐름:
- 워터마크 기반 회수: 각 존(Zone)은
pages_min<pages_low<pages_high세 단계의 워터마크를 가집니다. 가용 페이지가pages_low이하로 떨어지면kswapd가 깨어나고,pages_high까지 회수한 후 다시 슬립합니다. - 깨움 경로:
alloc_page()→__alloc_pages_slowpath()→wakeup_kswapd()순서로 메모리 할당 실패 시kswapd를 깨웁니다. - 회수 경로:
balance_pgdat()→shrink_node()→shrink_lruvec()순서로 LRU 리스트에서 비활성 페이지를 회수합니다. - NUMA 인식: 각 NUMA 노드마다 별도의
kswapd가 실행되어 해당 노드의 메모리만 관리합니다.
jbd2 — ext4 저널 커밋 스레드
jbd2(Journaling Block Device 2)는 ext4 파일시스템의 저널 커밋을 담당합니다. 블록 디바이스별 1개씩 생성되며(jbd2/sda1-8 등), 트랜잭션의 원자적 커밋을 보장합니다.
/* fs/jbd2/journal.c - kjournald2() 핵심 루프 (간략화) */
static int kjournald2(void *arg)
{
journal_t *journal = arg;
set_freezable();
while (!kthread_should_stop()) {
if (journal->j_flags & JBD2_UNMOUNT)
break;
/* 트랜잭션 커밋 주기 (기본 5초) */
if (journal->j_commit_interval) {
wait_event_interruptible_timeout(
journal->j_wait_commit,
journal_needs_commit(journal) ||
kthread_should_stop(),
journal->j_commit_interval);
}
if (journal_needs_commit(journal))
jbd2_journal_commit_transaction(journal);
if (freezing(current))
try_to_freeze();
}
return 0;
}
주요 특징:
- 주기적 커밋: 기본 5초(
j_commit_interval) 간격으로 대기 중인 트랜잭션을 디스크에 커밋합니다. - 즉시 커밋:
fsync()호출 시j_wait_commit대기 큐를 깨워 즉시 커밋을 트리거합니다. - 트랜잭션 배리어(Barrier): 저널 커밋 시 디스크 쓰기 배리어를 사용하여 데이터 무결성을 보장합니다. 전원 장애 시에도 파일시스템 일관성이 유지됩니다.
- 언마운트 처리:
JBD2_UNMOUNT플래그가 설정되면 남은 트랜잭션을 모두 커밋하고 종료합니다.
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;
}
마이그레이션이 발생하는 주요 경우:
set_cpus_allowed_ptr(): 태스크의 CPU affinity가 변경되어 현재 CPU에서 더 이상 실행할 수 없을 때, migration 스레드가 해당 태스크를 허용된 CPU로 이동시킵니다.load_balance(): CFS 스케줄러의 로드 밸런싱 과정에서 과부하된 CPU에서 여유 있는 CPU로 태스크를 이동합니다.- CPU offline: CPU가 오프라인 상태로 전환될 때, 해당 CPU에서 실행 중인 모든 태스크를 다른 CPU로 강제 이동합니다.
migration 스레드와 stop_machine(): migration 스레드는 stop_machine() 메커니즘의 기반이 됩니다. 모든 CPU의 migration 스레드가 동시에 실행되면 다른 모든 태스크가 선점되어, 전역 상태를 안전하게 변경할 수 있습니다. 이는 모듈 로드/언로드, CPU hotplug 등에 활용됩니다.
Real-Time 커널 및 스케줄링 클래스
커널 스레드는 기본적으로 CFS(COMPLETELY_FAIR_SCHEDULER)에 의해 스케줄링되지만, 실시간 요구사항이 있는 작업에는 실시간 스케줄링 클래스를 적용할 수 있습니다.
스케줄링 클래스 적용
SCHED_FIFO와 SCHED_RR은 실시간 스케줄링 정책으로, 커널 스레드에 적용하면 우선순위에 따른 선점(Preemption) 스케줄링이 가능합니다:
/* 커널 스레드에 실시간 우선순위 설정 */
struct sched_param param;
param.sched_priority = 50; /* 1~99, 높은 값이 높은 우선순위 */
/* SCHED_FIFO: 선점 가능하면 즉시 실행, 타임 슬라이스 없음 */
sched_setscheduler(kth, SCHED_FIFO, ¶m);
/* SCHED_RR: 타임 슬라이스 기반 라운드 로빈 */
sched_setscheduler(kth, SCHED_RR, ¶m);
/* 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 인식 커널 스레드의 핵심 특징:
kthread_create_on_node():node매개변수로 NUMA 노드를 지정하면,task_struct와 커널 스택이 해당 노드의 로컬 메모리에 할당됩니다.kthread_bind_mask(): 스레드의 CPU affinity를 해당 NUMA 노드의 CPU들로 제한하여, 항상 로컬 메모리에 접근하도록 보장합니다.- kswapd: 노드별 1개로 생성되어 자동으로 NUMA를 인식합니다. 각
kswapd는 자신의 노드 메모리만 관리합니다. - kcompactd: 메모리 컴팩션 데몬으로,
kswapd와 마찬가지로 노드별 1개씩 생성됩니다.
NUMA 지역성 성능 이점: NUMA 시스템에서 원격 노드 메모리 접근은 로컬 노드 대비 1.5~3배의 지연(latency)이 발생합니다. 커널 스레드의 task_struct, 커널 스택, 그리고 작업 데이터를 동일 노드에 배치하면 캐시 히트율이 향상되고 메모리 접근 지연이 크게 줄어듭니다. 특히 kswapd처럼 대량의 메모리 메타데이터를 다루는 스레드에서는 이 최적화가 필수적입니다.
BH Workqueue
최근 커널에서는 tasklet 대체를 위해 BH (Bottom-Half) Workqueue 도입과 전환이 진행 중입니다. BH workqueue는 workqueue 프레임워크를 활용하면서 softirq 컨텍스트에서 작업을 실행합니다.
BH Workqueue vs Tasklet
| 특성 | Tasklet (Legacy) | BH Workqueue |
|---|---|---|
| 컨텍스트 | softirq (BH) | softirq (BH) |
| 동시성 | 동일 softirq 타입만 직렬화 | workqueue pool 기반 동시성 제어 |
| flush/cancel | 제한적 | 완전한 flush/cancel 지원 |
| melting | tasklet_schedule() | queue_work_on() |
| 추적성 | 제한적 | ftrace, perf 지원 |
| 상태 | deprecated | 전환 진행 중 |
/* BH Workqueue 사용 예시 (커널 설정/버전 확인 필요) */
#include <linux/workqueue.h>
static void bh_work_handler(struct work_struct *work)
{
pr_info("BH work executed in softirq context\\n");
/* 실제 작업 — atomic 컨텍스트에서 실행됨 */
}
static void bh_work_handler_highpri(struct work_struct *work)
{
pr_info("High priority BH work\\n");
}
/* 시스템 제공 BH workqueue 사용 */
static struct work_struct my_bh_work;
static int __init bh_init(void)
{
INIT_WORK(&my_bh_work, bh_work_handler);
/* 일반 BH workqueue에 큐잉 */
queue_work(system_bh_wq, &my_bh_work);
/* 고우선순위 BH workqueue */
INIT_WORK(&highpri_work, bh_work_handler_highpri);
queue_work(system_bh_highpri_wq, &highpri_work);
return 0;
}
커스텀 BH Workqueue 생성
/* 커스텀 BH workqueue 생성 */
struct workqueue_struct *my_bh_wq;
my_bh_wq = alloc_workqueue("my-bh-wq",
WQ_UNBOUND | WQ_BH, /* BH 플래그 필수 */
0); /* max_active */
/* 사용 후 파괴 */
destroy_workqueue(my_bh_wq);
tasklet → BH workqueue 마이그레이션: 신규 코드에서는 tasklet 신규 도입을 피하고 BH workqueue 또는 threaded IRQ/workqueue 패턴을 우선 검토하세요. 기존 tasklet 코드는 서브시스템별 일정에 따라 점진적으로 전환되고 있으므로, 적용 전 대상 커널 브랜치의 지원 상태를 확인해야 합니다.
성능 튜닝 및 모니터링
커널 스레드 성능 튜닝
대용량 시스템에서는 커널 스레드의 동작을 튜닝하여 성능을 최적화할 수 있습니다:
/* CPU affinity 설정 — 특정 CPU에서만 실행되도록 제한 */
cpumask_t mask;
cpumask_clear(&mask);
cpumask_set_cpu(0, &mask); /* CPU 0에만 바인딩 */
set_cpus_allowed_ptr(current, &mask);
/*nice 값 설정 — 스케줄링 우선순위 조절 */
set_user_nice(current, -10); /* 더 높은 우선순위 */
/* 커널 스레드 시작 지연 — 시스템 부하 상태 확인 후 */
if (system_state == SYSTEM_RUNNING)
kth = kthread_run(fn, data, "delayed-kthread");
else
schedule_delayed_work(&delayed_init, msecs_to_jiffies(5000));
커널 스레드 통계 확인
# 커널 스레드별 CPU 사용량 확인
ps -eo pid,ppid,comm,%cpu,%mem,psr | grep -E "\\[" | sort -k4 -rn | head -20
# 특정 커널 스레드의 상세 통계
cat /proc/<pid>/sched
# se.exec_start : 123456789.123456
# se.vruntime : 1234.567890
# se.sum_exec_runtime : 5678.901234
# se.nr_switches : 12345
# se.nr_voluntary_switches: 12000
# se.nr_involuntary_switches: 345
# se.wait_start : 0.000000
# se.sleep_start : 123456789.123456
# se.sleep_max : 1234.567890
# se.iowait_sum : 123.456789
# se.iowait_max : 12.345678
# se.nr_migrations : 1234
# kworker 스레드별 CPU 사용량 상세
cat /proc/interrupts | head -5
cat /proc/softirqs
# workqueue 통계 (Linux 4.11+)
cat /sys/kernel/debug/workqueue
perf 도구 활용
# 특정 커널 스레드 프로파일링
perf record -p <pid> -g -- sleep 10
perf report
# 커널 스레드 생성/종료 이벤트 추적
perf record -e sched:sched_kthread_stop -e sched:sched_kthread_start -a -- sleep 30
perf script
# workqueue 작업 실행 추적
perf record -e workqueue:workqueue_execute_start -e workqueue:workqueue_execute_end -a
perf report
커널 스레드 디버깅(Debugging)
커널 스레드가 응답하지 않거나 예상대로 동작하지 않을 때 사용할 수 있는 디버깅 기법들입니다:
Hung Task 탐지
# Hung task 감지 활성화 (기본 120초)
echo 120 > /proc/sys/kernel/hung_task_timeout_secs
echo 1 > /proc/sys/kernel/hung_task_warnings
# D 상태(UNINTERRUPTIBLE)에 장시간 머무는 스레드 확인
ps -eo pid,stat,wchan:30,comm | grep -E '^[[:space:]]*[0-9]+[[:space:]]+D'
# 특정 스레드의 대기 이유 확인
cat /proc/<pid>/stack
# [<0>] __schedule+0x2e0/0x970
# [<0>] schedule+0x46/0xb0
# [<0>] io_schedule+0x16/0x40
# [<0>] wait_on_page_bit+0x15e/0x250
# → 페이지 I/O 대기 중
ftrace로 스레드 추적
# 특정 커널 스레드의 함수 호출 추적
cd /sys/kernel/debug/tracing
echo 0 > tracing_on
echo function_graph > current_tracer
echo 1 > options/funcgraph-proc
echo kswapd0 > set_ftrace_pid # 또는 PID 번호
echo 1 > tracing_on
sleep 5
echo 0 > tracing_on
cat trace | head -100
# kthread 생성/종료 이벤트 추적
echo 1 > events/sched/sched_process_fork/enable
echo 1 > events/sched/sched_process_exit/enable
echo 1 > tracing_on
cat trace_pipe
데드락 탐지
# lockdep으로 락 의존성 문제 감지 (CONFIG_PROVE_LOCKING 필요)
dmesg | grep -i "possible circular locking"
# 모든 스레드의 락 정보 확인
cat /proc/lock_stat
# 특정 스레드가 대기 중인 락 확인
cat /proc/<pid>/wchan # 대기 중인 커널 함수
# SysRq로 전체 스레드 스택 덤프 (긴급 상황)
echo t > /proc/sysrq-trigger
dmesg | tail -200
디버깅 팁
| 문제 증상 | 확인 방법 | 해결 방향 |
|---|---|---|
| 스레드가 생성되지 않음 | dmesg | grep kthread | kthreadd 상태 확인, OOM 확인 |
| 스레드가 종료되지 않음 | cat /proc/<pid>/stack | kthread_should_stop() 확인 누락 의심 |
| CPU 100% 사용 | perf top -p <pid> | 무한 루프, schedule() 누락 |
| D 상태에 장시간 | /proc/<pid>/stack | I/O 대기, mutex 대기 확인 |
| Park 후 unpark 안됨 | ps에서 STAT=P 확인 | CPU 핫플러그 상태 확인 |
panic_print 파라미터를 설정하면 유용한 정보를 출력할 수 있습니다. echo 0x1ff > /proc/sys/kernel/panic_print로 모든 정보(태스크 리스트, 메모리 정보, 타이머(Timer) 등)를 출력합니다.
커널 스레드 시각화
커널 스레드 상태 시각화
# 모든 커널 스레드의 상태 시각화 (심플)
for pid in $(ls /proc/ | grep -E '^[0-9]+$'); do
if grep -q '\[.*\]' /proc/$pid/comm 2>/dev/null; then
state=$(awk '{print $3}' /proc/$pid/stat)
comm=$(cat /proc/$pid/comm)
printf "%-30s %-5s %s\n" "$comm" "$pid" "$state"
done
# kernel thread 상태 그래프 (Python 스크립트 예시)
#!/usr/bin/env python3
import os
threads = []
for pid in os.listdir('/proc'):
if not pid.isdigit(): continue
try:
with open(f'/proc/{pid}/comm') as f:
comm = f.read().strip()
if comm.startswith('[') and comm.endswith(']'):
with open(f'/proc/{pid}/stat') as s:
state = s.read().split()[2]
threads.append((comm, pid, state))
except: pass
# 상태별 분류
states = {}
for t in threads:
states.setdefault(t[2], []).append(t)
for state, lst in sorted(states.items()):
print(f"# {state}: {len(lst)} threads")
for comm, pid, _ in lst[:5]:
print(f" {comm} (pid={pid})")
Flame Graph로 커널 스레드 분석
# 커널 스레드 스택 flamegraph 생성
perf record -F 99 -a -g -- sleep 30
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > kthread.svg
# 특정 커널 스레드만 프로파일링
perf record -F 99 -p $(pgrep -f kworker/0:1) -g -- sleep 10
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > kworker0.svg
내부 구현 상세
to_kthread() / struct kthread
각 커널 스레드에는 struct kthread가 부속되며, task_struct.set_child_tid 포인터를 통해 접근합니다:
/* kernel/kthread.c */
struct kthread {
unsigned long flags; /* KTHREAD_SHOULD_STOP 등 */
unsigned int cpu; /* per-CPU kthread의 대상 CPU */
int result; /* 스레드 함수 반환값 */
int (*threadfn)(void *);
void *data;
struct completion parked;
struct completion exited;
char *full_name;
};
/* 플래그 비트 */
enum KTHREAD_BITS {
KTHREAD_IS_PER_CPU = 0, /* per-CPU 스레드 여부 */
KTHREAD_SHOULD_STOP = 1, /* kthread_stop() 요청됨 */
KTHREAD_SHOULD_PARK = 2, /* kthread_park() 요청됨 */
KTHREAD_IS_PARKED = 3, /* 현재 park 상태 */
};
/* task_struct에서 kthread 구조체 접근 */
static inline struct kthread *to_kthread(struct task_struct *k)
{
WARN_ON(!(k->flags & PF_KTHREAD));
return k->worker_private;
}
kernel_thread() → kernel_clone()
kernel_thread()는 kernel_clone()의 래퍼로, 커널 모드에서 새 태스크를 생성합니다:
/* kernel/fork.c */
pid_t kernel_thread(int (*fn)(void *), void *arg,
const char *name, unsigned long flags)
{
struct kernel_clone_args args = {
.flags = ((lower_32_bits(flags) | CLONE_VM | CLONE_UNTRACED)
& ~CSIGNAL),
.exit_signal = (lower_32_bits(flags) & CSIGNAL),
.fn = fn,
.fn_arg = arg,
.name = name,
.kthread = 1, /* PF_KTHREAD 설정 */
};
return kernel_clone(&args);
}
/* kernel_clone()은 copy_process()를 호출하여:
* 1. task_struct 할당
* 2. PF_KTHREAD 플래그 설정
* 3. mm = NULL (유저 주소 공간 없음)
* 4. 커널 스택 할당
* 5. 스케줄러에 등록
*/
동기화 및 슬립 패턴
completion 기반 패턴
커널 스레드의 시작/종료를 동기화할 때 completion을 사용합니다:
static DECLARE_COMPLETION(thread_started);
static int my_thread_fn(void *data)
{
/* 초기화 완료 시그널 */
complete(&thread_started);
while (!kthread_should_stop()) {
do_work();
msleep(100);
}
return 0;
}
/* 생성 측 */
kth = kthread_run(my_thread_fn, data, "my-thread");
wait_for_completion(&thread_started);
/* 이 시점에서 스레드 초기화가 확실히 완료됨 */
Freezable 커널 스레드
시스템 절전(suspend) 시 커널 스레드도 얼릴 수 있습니다. I/O를 수행하는 커널 스레드는 suspend 전에 정지해야 데이터 손상을 방지할 수 있습니다:
static int freezable_thread_fn(void *data)
{
/* 이 스레드는 freezable — suspend 시 동결됨 */
set_freezable();
while (!kthread_should_stop()) {
/* try_to_freeze()가 suspend 시 스레드를 정지시킴 */
try_to_freeze();
do_io_work();
/* freezable 슬립 — suspend 도중에 깨어나지 않음 */
wait_event_freezable(my_wq, has_work || kthread_should_stop());
}
return 0;
}
/* wait_event_freezable은 내부적으로:
* 1. freezing(current)이면 try_to_freeze() 호출
* 2. 조건 확인
* 3. 슬립
* 을 안전하게 조합합니다 */
디버깅 (Debugging)
커널 스레드 식별하기
# 모든 커널 스레드 나열 (대괄호 표기)
ps -eo pid,ppid,cls,ni,psr,comm | awk '$2==2 || $1==2'
# 특정 커널 스레드의 상세 정보
cat /proc/<pid>/status
# Name: kswapd0
# State: S (sleeping)
# Tgid: <pid>
# VmSize: (없음 — 커널 스레드는 유저 메모리 없음)
# 커널 스레드의 CPU affinity 확인
taskset -p <pid>
# 커널 스레드 스택 트레이스
cat /proc/<pid>/stack
디버깅 도구
# ftrace로 kthread 생성 추적
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_kthread_work_execute_start/enable
echo 1 > /sys/kernel/debug/tracing/events/sched/sched_kthread_work_queue_work/enable
cat /sys/kernel/debug/tracing/trace
# SysRq-T: 모든 태스크의 스택 덤프 (커널 스레드 포함)
echo t > /proc/sysrq-trigger
dmesg | less
# 특정 커널 스레드의 스케줄링 통계
cat /proc/<pid>/sched
# perf로 커널 스레드 프로파일링
perf top -t <pid>
/proc/<pid>/stack: CONFIG_STACKTRACE가 활성화된 커널에서 커널 스레드의 현재 호출 스택을 확인할 수 있습니다. 스레드가 어디서 슬립하고 있는지 파악하는 데 매우 유용합니다.
코드 예제 (Code Examples)
기본 커널 스레드 모듈
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/sched.h>
static struct task_struct *my_thread;
static int thread_func(void *data)
{
int count = 0;
pr_info("kthread started: pid=%d, comm=%s\\n",
current->pid, current->comm);
while (!kthread_should_stop()) {
pr_info("kthread iteration %d (cpu=%d)\\n",
count++, smp_processor_id());
msleep(2000);
}
pr_info("kthread stopping, count=%d\\n", count);
return count;
}
static int __init my_init(void)
{
pr_info("creating kernel thread\\n");
my_thread = kthread_run(thread_func, NULL, "example_kthread");
if (IS_ERR(my_thread)) {
pr_err("kthread_run failed: %ld\\n", PTR_ERR(my_thread));
return PTR_ERR(my_thread);
}
pr_info("kthread created: pid=%d\\n", my_thread->pid);
return 0;
}
static void __exit my_exit(void)
{
int ret;
if (my_thread) {
ret = kthread_stop(my_thread);
pr_info("kthread stopped, return value: %d\\n", ret);
}
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Basic Kernel Thread Example");
Per-CPU 커널 스레드
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/cpumask.h>
static struct task_struct * __percpu *per_cpu_threads;
static int per_cpu_fn(void *data)
{
unsigned int cpu = (unsigned long)data;
pr_info("per-cpu kthread on CPU %u started (pid=%d)\\n",
cpu, current->pid);
while (!kthread_should_stop()) {
if (kthread_should_park())
kthread_parkme();
pr_info("CPU %u: doing work\\n", cpu);
msleep(5000);
}
return 0;
}
static int __init percpu_init(void)
{
unsigned int cpu;
per_cpu_threads = alloc_percpu(struct task_struct *);
if (!per_cpu_threads)
return -ENOMEM;
for_each_online_cpu(cpu) {
struct task_struct *t;
t = kthread_create_on_cpu(per_cpu_fn,
(void *)(unsigned long)cpu,
cpu, "my_pcpu/%u");
if (IS_ERR(t)) {
pr_err("failed on CPU %u\\n", cpu);
continue;
}
*per_cpu_ptr(per_cpu_threads, cpu) = t;
wake_up_process(t);
}
return 0;
}
static void __exit percpu_exit(void)
{
unsigned int cpu;
for_each_online_cpu(cpu) {
struct task_struct *t = *per_cpu_ptr(per_cpu_threads, cpu);
if (t)
kthread_stop(t);
}
free_percpu(per_cpu_threads);
}
module_init(percpu_init);
module_exit(percpu_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Per-CPU Kernel Thread Example");
kthread_worker 사용 예제
#include <linux/module.h>
#include <linux/kthread.h>
static struct kthread_worker *worker;
static struct kthread_work my_work;
static struct kthread_delayed_work my_delayed_work;
static void work_func(struct kthread_work *work)
{
pr_info("work executed: thread=%s, cpu=%d\\n",
current->comm, smp_processor_id());
}
static void delayed_work_func(struct kthread_work *work)
{
pr_info("delayed work executed\\n");
/* 자기 자신을 다시 큐잉 (주기적 실행) */
kthread_queue_delayed_work(worker, &my_delayed_work,
msecs_to_jiffies(3000));
}
static int __init worker_init(void)
{
/* 워커 생성 (전용 스레드 자동 생성) */
worker = kthread_create_worker(0, "my_kworker");
if (IS_ERR(worker))
return PTR_ERR(worker);
/* 즉시 실행 작업 */
kthread_init_work(&my_work, work_func);
kthread_queue_work(worker, &my_work);
/* 지연 실행 작업 (3초 후) */
kthread_init_delayed_work(&my_delayed_work, delayed_work_func);
kthread_queue_delayed_work(worker, &my_delayed_work,
msecs_to_jiffies(3000));
return 0;
}
static void __exit worker_exit(void)
{
kthread_cancel_delayed_work_sync(&my_delayed_work);
kthread_destroy_worker(worker);
}
module_init(worker_init);
module_exit(worker_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("kthread_worker Example");
Freezable 커널 스레드
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/freezer.h>
#include <linux/wait.h>
static struct task_struct *freeze_thread;
static DECLARE_WAIT_QUEUE_HEAD(wq);
static bool do_work = false;
static int freezable_fn(void *data)
{
set_freezable();
while (!kthread_should_stop()) {
/* suspend 시 자동 동결, resume 시 자동 해동 */
wait_event_freezable(wq,
do_work || kthread_should_stop());
if (kthread_should_stop())
break;
do_work = false;
pr_info("freezable thread: doing I/O work\\n");
}
return 0;
}
static int __init freeze_init(void)
{
freeze_thread = kthread_run(freezable_fn, NULL, "freezable_kth");
return IS_ERR(freeze_thread) ? PTR_ERR(freeze_thread) : 0;
}
static void __exit freeze_exit(void)
{
kthread_stop(freeze_thread);
}
module_init(freeze_init);
module_exit(freeze_exit);
MODULE_LICENSE("GPL");
주의사항 및 버그 패턴
kthread_stop() 없이 모듈 언로드
/* 잘못된 예 — 스레드 실행 중 모듈 언로드 */
static void __exit bad_exit(void)
{
/* kthread_stop() 호출 안 함!
* → 스레드가 계속 실행되지만 코드 영역이 해제됨
* → 커널 패닉 (use-after-free) */
pr_info("goodbye\\n");
}
/* 올바른 예 */
static void __exit good_exit(void)
{
if (my_thread) {
kthread_stop(my_thread); /* 반드시 종료 대기 */
my_thread = NULL;
}
}
커널 스레드에서 유저 공간 접근
/* 잘못된 예 — 커널 스레드에서 유저 메모리 접근 */
static int bad_thread_fn(void *data)
{
char __user *ubuf = (char __user *)data;
char kbuf[64];
/* mm == NULL이므로 copy_from_user()가 실패하거나 Oops 발생!
* 커널 스레드는 유저 주소 공간이 없습니다 */
copy_from_user(kbuf, ubuf, 64); /* WRONG */
return 0;
}
/* 올바른 방법: 데이터를 커널 메모리에 복사해서 전달 */
char *kdata = kmalloc(64, GFP_KERNEL);
copy_from_user(kdata, ubuf, 64); /* 유저 컨텍스트에서 미리 복사 */
kth = kthread_run(my_fn, kdata, "my-kthread"); /* 커널 메모리 전달 */
무한 루프 / CPU 100% 점유
/* 잘못된 예 — 슬립 없는 폴링 루프 */
static int hog_thread_fn(void *data)
{
while (!kthread_should_stop()) {
/* CPU를 100% 점유! 다른 태스크 기아 발생 */
do_polling();
}
return 0;
}
/* 올바른 예 — schedule() 또는 cond_resched() 삽입 */
static int good_thread_fn(void *data)
{
while (!kthread_should_stop()) {
if (!has_work()) {
usleep_range(1000, 2000); /* 1~2ms 슬립 */
continue;
}
do_work();
cond_resched(); /* 선점 포인트 */
}
return 0;
}
kthread_create 에러 미처리
/* 잘못된 예 */
my_thread = kthread_run(fn, data, "my-kth");
wake_up_process(my_thread); /* IS_ERR일 때 크래시! */
/* 올바른 예 */
my_thread = kthread_run(fn, data, "my-kth");
if (IS_ERR(my_thread)) {
pr_err("failed: %ld\\n", PTR_ERR(my_thread));
my_thread = NULL; /* exit에서 NULL 체크 가능하도록 */
return PTR_ERR(my_thread);
}
모듈 언로드 안전: module_exit에서는 반드시 (1) kthread_stop()으로 스레드 종료를 기다리고, (2) 스레드가 사용한 리소스를 해제하세요. 스레드가 실행 중인 상태에서 모듈이 언로드되면 코드 영역이 해제되어 커널 패닉이 발생합니다.
커널 스레드 보안 고려사항 (Security Considerations)
커널 스레드는 커널 모드에서 실행되므로 사용자 공간의 보안 메커니즘(seccomp, 사용자 네임스페이스 등)이 적용되지 않습니다. 기본적으로 init_cred를 상속받아 완전한 root 권한으로 실행되기 때문에, 보안 측면에서 신중한 설계가 필요합니다.
주의: 커널 스레드는 기본적으로 root 권한으로 실행됩니다. kthreadd(PID 2)가 init_cred를 사용하므로, 이로부터 생성된 모든 커널 스레드는 CAP_FULL_SET(모든 capabilities)를 상속받습니다. 커널 모듈에서 커널 스레드를 생성할 때는 필요한 최소 권한만 설정하는 것이 보안 모범 사례입니다.
기본 자격 증명 (Default Credentials)
/* 커널 스레드의 기본 자격 증명 */
/* kthreadd가 init_cred를 사용하므로 모든 자식도 상속 */
struct cred init_cred = {
.usage = ATOMIC_INIT(4),
.uid = GLOBAL_ROOT_UID,
.gid = GLOBAL_ROOT_GID,
.cap_inheritable = CAP_FULL_SET,
.cap_permitted = CAP_FULL_SET,
.cap_effective = CAP_FULL_SET,
...
};
권한 제한 방법
prepare_kernel_cred()와 commit_creds()를 사용하면 커널 스레드의 capabilities를 필요한 최소한으로 제한할 수 있습니다.
/* 커널 스레드에서 권한 제한 */
static int secure_kthread(void *data)
{
struct cred *new_cred;
new_cred = prepare_kernel_cred(NULL);
/* capabilities 제거 */
cap_clear(new_cred->cap_effective);
cap_clear(new_cred->cap_permitted);
/* 특정 capability만 추가 */
cap_raise(new_cred->cap_effective, CAP_NET_ADMIN);
cap_raise(new_cred->cap_permitted, CAP_NET_ADMIN);
commit_creds(new_cred);
while (!kthread_should_stop()) {
/* 이제 CAP_NET_ADMIN만 가진 상태로 실행 */
do_network_work();
schedule();
}
return 0;
}
보안 측면 요약
| 보안 측면 | 설명 | 대응 |
|---|---|---|
| 기본 권한 | root (init_cred 상속) | prepare_kernel_cred()으로 제한 |
| 파일 접근 | 모든 파일 접근 가능 | vfs_kern_mount() 사용, 직접 접근 자제 |
| 네트워크 | 모든 소켓 접근 가능 | 필요한 CAP_만 설정 |
| SELinux | kernel_t 컨텍스트 | 별도 도메인 정의 가능 |
| seccomp | 미적용 (커널 모드) | 해당 없음 |
LSM(Linux Security Module) 컨텍스트: SELinux가 활성화된 시스템에서 커널 스레드는 기본적으로 kernel_t 보안 컨텍스트로 실행됩니다. AppArmor의 경우 커널 스레드는 unconfined 프로파일을 사용합니다. 보안이 중요한 커널 모듈에서는 별도의 SELinux 도메인을 정의하여 커널 스레드의 접근을 제한할 수 있습니다.
API 레퍼런스 요약
| 함수 / 매크로 | 설명 | 헤더 |
|---|---|---|
kthread_create(fn, data, fmt, ...) |
커널 스레드 생성 (미실행 상태) | linux/kthread.h |
kthread_run(fn, data, fmt, ...) |
커널 스레드 생성 + 즉시 실행 | linux/kthread.h |
kthread_create_on_node(fn, data, node, fmt, ...) |
NUMA 노드 지정 생성 | linux/kthread.h |
kthread_create_on_cpu(fn, data, cpu, fmt) |
CPU 바인딩 생성 (per-CPU) | linux/kthread.h |
kthread_bind(task, cpu) |
미실행 스레드를 특정 CPU에 바인딩 | linux/kthread.h |
kthread_should_stop() |
종료 요청 확인 (루프 조건) | linux/kthread.h |
kthread_stop(task) |
스레드 종료 요청 + 종료 대기 | linux/kthread.h |
kthread_should_park() |
일시 중지 요청 확인 | linux/kthread.h |
kthread_park(task) |
스레드 일시 중지 | linux/kthread.h |
kthread_unpark(task) |
스레드 재개 | linux/kthread.h |
kthread_parkme() |
현재 스레드를 park 상태로 전환 | linux/kthread.h |
kthread_create_worker(flags, fmt, ...) |
kthread_worker + 전용 스레드 생성 | linux/kthread.h |
kthread_queue_work(worker, work) |
작업을 워커 큐에 추가 | linux/kthread.h |
kthread_flush_work(work) |
특정 작업 완료 대기 | linux/kthread.h |
kthread_destroy_worker(worker) |
워커 + 전용 스레드 정리 | linux/kthread.h |
smpboot_register_percpu_thread(ht) |
per-CPU 스레드 프레임워크 등록 | linux/smpboot.h |
set_freezable() |
현재 스레드를 freezable로 마킹 | linux/freezer.h |
wait_event_freezable(wq, cond) |
freeze 안전한 조건부 대기 | linux/freezer.h |
kthread_worker 큐 처리
kthread_worker는 단일 스레드 기반의 작업 큐(Workqueue)로, 내부적으로 work_list에 연결된 kthread_work를 순차적으로 꺼내 실행합니다. 이 섹션에서는 큐 처리 메커니즘, 작업 생명주기, flush/cancel 동작을 상세히 분석합니다.
kthread_worker_fn() 내부 구현
kthread_worker_fn()은 워커 스레드의 메인 루프로, kthread_create_worker()가 자동으로 이 함수를 스레드 함수로 설정합니다:
/* kernel/kthread.c - kthread_worker_fn() 핵심 (간략화) */
int kthread_worker_fn(void *worker_ptr)
{
struct kthread_worker *worker = worker_ptr;
struct kthread_work *work;
repeat:
set_current_state(TASK_INTERRUPTIBLE);
if (kthread_should_stop()) {
__set_current_state(TASK_RUNNING);
spin_lock_irq(&worker->lock);
worker->task = NULL;
spin_unlock_irq(&worker->lock);
return 0;
}
work = NULL;
spin_lock_irq(&worker->lock);
if (!list_empty(&worker->work_list)) {
work = list_first_entry(&worker->work_list,
struct kthread_work, node);
list_del_init(&work->node);
}
worker->current_work = work;
spin_unlock_irq(&worker->lock);
if (work) {
__set_current_state(TASK_RUNNING);
work->func(work);
} else if (!freezing(current)) {
schedule();
}
try_to_freeze();
goto repeat;
}
flush와 cancel 의미론
kthread_flush_work()와 kthread_cancel_work_sync()는 작업 상태에 따라 다르게 동작합니다:
| API | IDLE 상태 | 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와 KTHREAD_IS_PARKED 두 플래그의 조합으로 상태를 관리합니다.
park/unpark 내부 구현
/* kernel/kthread.c - kthread_park() 내부 (간략화) */
int kthread_park(struct task_struct *k)
{
struct kthread *kthread = to_kthread(k);
if (WARN_ON(k->flags & PF_EXITING))
return -ENOSYS;
/* SHOULD_PARK 비트 설정 */
set_bit(KTHREAD_SHOULD_PARK, &kthread->flags);
/* 스레드가 슬립 중이면 깨움 — should_park()를 확인하게 함 */
wake_up_process(k);
/* 스레드가 실제로 park될 때까지 대기 */
wait_for_completion(&kthread->parked);
return 0;
}
/* kthread_parkme() — 스레드 자신이 호출 */
void kthread_parkme(void)
{
struct kthread *self = to_kthread(current);
__set_current_state(TASK_PARKED);
while (test_bit(KTHREAD_SHOULD_PARK, &self->flags)) {
if (!test_and_set_bit(KTHREAD_IS_PARKED, &self->flags))
complete(&self->parked); /* park 완료 통지 */
schedule();
__set_current_state(TASK_PARKED);
}
clear_bit(KTHREAD_IS_PARKED, &self->flags);
__set_current_state(TASK_RUNNING);
}
/* kthread_unpark() */
void kthread_unpark(struct task_struct *k)
{
struct kthread *kthread = to_kthread(k);
/* 두 플래그 모두 해제 */
clear_bit(KTHREAD_IS_PARKED, &kthread->flags);
clear_bit(KTHREAD_SHOULD_PARK, &kthread->flags);
/* completion 초기화 (다음 park 대비) */
reinit_completion(&kthread->parked);
/* TASK_PARKED에서 깨움 */
wake_up_state(k, TASK_PARKED);
}
kthread_park()는 스레드를 살린 채로 일시 중지하고, kthread_stop()은 스레드를 완전히 종료합니다. CPU 핫플러그에서 park를 사용하는 이유는, CPU가 다시 online될 때 스레드를 재생성하지 않고 빠르게 재개할 수 있기 때문입니다.
Per-CPU 커널 스레드
리눅스 커널은 각 CPU마다 전용 커널 스레드를 배치하여 CPU별 작업을 효율적으로 처리합니다. 이들은 캐시 지역성 극대화와 락 경합(Contention) 최소화를 위해 특정 CPU에 바인딩됩니다.
주요 per-CPU 스레드 역할 상세
| 스레드 | 스케줄링 | 역할 | 활성화 조건 |
|---|---|---|---|
ksoftirqd/N | SCHED_NORMAL (nice 0) | softirq가 과도하게 발생할 때 스레드 컨텍스트에서 처리. 인터럽트 컨텍스트의 지연을 방지 | __do_softirq()에서 반복 횟수 초과 시 |
migration/N | SCHED_FIFO (prio 99) | CPU 간 태스크 이동 요청 처리. stop_machine() 동기화에도 사용 | 스케줄러의 load balancing 또는 affinity 변경 시 |
kworker/N:M | SCHED_NORMAL | CPU N에 바인딩된 workqueue 작업 실행. M은 워커 번호 | queue_work()로 작업 제출 시 |
kworker/N:MH | SCHED_NORMAL (nice -20) | 고우선순위 workqueue 워커. WQ_HIGHPRI 플래그 사용 | 고우선순위 작업 제출 시 |
cpuhp/N | SCHED_NORMAL | CPU 핫플러그 콜백(Callback) 처리. CPU up/down 시 등록된 콜백 순서대로 실행 | CPU online/offline 이벤트 |
CPU affinity 관리
/* per-CPU 스레드의 CPU 바인딩 — kthread_bind()는 미실행 상태에서만 가능 */
void kthread_bind(struct task_struct *p, unsigned int cpu)
{
__kthread_bind(p, cpumask_of(cpu), TASK_UNINTERRUPTIBLE);
}
/* 실행 중 스레드의 CPU 제한 */
set_cpus_allowed_ptr(current, cpumask_of(target_cpu));
/* NUMA 인식 per-CPU 스레드 — 같은 노드에서 메모리 할당 */
struct task_struct *t;
t = kthread_create_on_node(fn, data,
cpu_to_node(cpu), /* CPU가 속한 NUMA 노드 */
"mythread/%d", cpu);
kthread_bind(t, cpu);
wake_up_process(t);
isolcpus= 부트 파라미터로 격리(Isolation)된 CPU에서는 기본적으로 unbound kworker가 스케줄링되지 않지만, per-CPU 스레드(ksoftirqd, migration 등)는 해당 CPU에 고정됩니다. nohz_full=과 함께 사용하면 격리 CPU의 커널 스레드 간섭을 최소화할 수 있습니다.
kthread_create_on_node() 코드 워크스루
커널 스레드 생성의 전체 경로를 kthread_create_on_node()부터 실제 태스크가 실행되기까지 단계별로 추적합니다.
__kthread_create_on_node() 상세
/* 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;
}
kernel_clone()을 호출하면 예기치 않은 리소스가 새 스레드에 상속될 수 있습니다.
스레드 시작 시퀀스
새로 생성된 커널 스레드의 kthread() 함수가 실행되는 과정을 더 자세히 살펴봅니다:
/* kernel/kthread.c - kthread() 시작 함수 상세 */
static int kthread(void *_create)
{
struct kthread_create_info *create = _create;
int (*threadfn)(void *data);
void *data;
int ret;
/* ① kthread 메타데이터 초기화 */
struct kthread *self = to_kthread(current);
self->threadfn = create->threadfn;
self->data = create->data;
/* ② 생성 완료를 호출자에게 통지 */
threadfn = create->threadfn;
data = create->data;
create->result = current;
complete(create->done);
/* 이 시점 이후 create를 참조하면 안 됨 — 호출자가 해제할 수 있음 */
/* ③ wake_up_process()가 호출될 때까지 대기
* kthread_run()이면 즉시 깨어남
* kthread_create()면 호출자가 수동으로 깨울 때까지 대기 */
schedule_preempt_disabled();
/* ④ 실행 — stop 요청이 이미 있으면 건너뜀 */
if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags))
ret = threadfn(data);
else
ret = -EINTR;
kthread_exit(ret);
/* 도달하지 않음 */
}
커널 소스 분석: 호출 체인 및 핵심 구조체
이 절에서는 커널 스레드 생성과 실행의 전체 호출 체인(Call Chain)을
kthread_create_on_node()부터 threadfn() 실행까지
실제 소스 코드를 기반으로 분석합니다.
호출 체인 전체 흐름
struct kthread — 커널 스레드 내부 상태
struct kthread는 각 커널 스레드의 태스크 스택 최상단에 위치하는 내부 메타데이터 구조체입니다.
to_kthread(task) 매크로(Macro)로 task_struct에서 접근합니다.
/* kernel/kthread.c - struct kthread */
struct kthread {
unsigned long flags; /* KTHREAD_SHOULD_STOP, KTHREAD_SHOULD_PARK 비트 */
unsigned int cpu; /* park/unpark 시 대상 CPU */
int (*threadfn)(void *); /* 실제 스레드 함수 포인터 */
void *data; /* threadfn에 전달되는 인자 */
struct completion parked; /* park 완료 신호 */
struct completion exited; /* 스레드 종료 신호 (kthread_stop이 대기) */
int result; /* threadfn 반환값 (kthread_stop이 수거) */
#ifdef CONFIG_BLK_CGROUP
struct cgroup_subsys_state *blkcg_css; /* 블록 I/O cgroup 상태 */
#endif
};
코드 설명
- flags
KTHREAD_SHOULD_STOP비트가 설정되면kthread_should_stop()이true를 반환합니다.KTHREAD_SHOULD_PARK비트는kthread_park()호출 시 설정되어 스레드가kthread_parkme()에서 대기하도록 유도합니다. - cpupark/unpark 시 스레드가 복귀할 CPU 번호를 저장합니다.
kthread_bind()로 설정되며, smpboot 프레임워크가 per-CPU 스레드를 관리할 때 사용합니다. - threadfn / data실제 스레드 함수와 인자를 보관합니다.
kthread()래퍼가complete(done)후 로컬 복사본으로 호출하는데, 이는create_info가 해제된 이후에도 안전하게 참조하기 위함입니다. - parkedpark 완료를 외부에 알리는 completion입니다.
kthread_park()는 스레드가 실제로TASK_PARKED상태에 진입할 때까지 이 completion을 대기합니다. - exited스레드가
kthread_exit()를 호출할 때complete()됩니다.kthread_stop()은 이 completion을 대기하여 스레드 종료를 동기적으로 확인합니다. - result
threadfn이 반환한 정수값을 저장합니다.kthread_stop()의 반환값이 됩니다. 에러 코드 전달에 활용됩니다. - blkcg_css블록 I/O cgroup 계층(Hierarchy)의 서브시스템 상태입니다.
CONFIG_BLK_CGROUP설정 시에만 포함되며, 커널 스레드의 블록 I/O를 cgroup으로 제어할 때 사용합니다.
struct kthread_worker / kthread_work — 필드 상세
kthread_worker와 kthread_work는 전용 커널 스레드 기반의 작업 큐를 구성합니다.
workqueue(CMWQ)와 달리 단일 스레드에서 순차 실행을 보장하며 실시간(Real-time) 우선순위 설정이 용이합니다.
/* include/linux/kthread.h - struct kthread_worker */
struct kthread_worker {
unsigned int flags; /* KTW_FREEZABLE 등 플래그 */
raw_spinlock_t lock; /* work_list 보호 스핀락 */
struct list_head work_list; /* 실행 대기 중인 kthread_work 큐 */
struct list_head delayed_work_list; /* 지연 실행 work 큐 (타이머 기반) */
struct task_struct *task; /* 워커 스레드 task_struct */
struct kthread_work *current_work; /* 현재 실행 중인 work (flush 동기화용) */
};
코드 설명
- flags
KTW_FREEZABLE플래그가 설정되면 suspend 시 freezer가 이 워커를 동결합니다. 기본 생성(kthread_create_worker(0, ...))은 freezable입니다. - lock
work_list와current_work접근을 직렬화하는 raw 스핀락입니다.kthread_queue_work()와kthread_worker_fn()양측에서 반드시 이 락을 획득해야 합니다. - work_list큐잉된
kthread_work항목의 연결 리스트입니다.kthread_queue_work()가 tail에 추가하고, 워커 스레드가 head에서 꺼내어 처리합니다. - delayed_work_list타이머 만료 후 실행되는
kthread_delayed_work항목의 리스트입니다. 타이머 핸들러가 만료 시work_list로 이동시킵니다. - task이 워커를 실행하는 전용 커널 스레드입니다.
kthread_create_worker()가 할당합니다.NULL이면 워커가 파괴(destroyed) 상태임을 나타냅니다. - current_work현재 실행 중인 work 포인터입니다.
kthread_flush_work()가 특정 work의 완료를 대기할 때, 이 포인터를 확인하여 해당 work가 이미 실행 중인지 판별합니다.
/* include/linux/kthread.h - struct kthread_work */
struct kthread_work {
struct list_head node; /* work_list 연결용 리스트 노드 */
kthread_work_func_t func; /* 실행할 작업 함수 포인터 */
struct kthread_worker *worker; /* 이 work가 큐잉된 워커 (NULL이면 미큐잉) */
int canceling; /* cancel 진행 중인 횟수 (atomic) */
};
코드 설명
- node
kthread_worker.work_list에 연결되는 intrusive 리스트 노드입니다. work가 큐에 없을 때는list_empty(&work->node)가true이며, 이를 통해 중복 큐잉을 방지합니다. - func워커 스레드가 호출할 함수 포인터입니다. 함수 시그니처는
void func(struct kthread_work *work)이며,container_of()로 감싸는 구조체에 접근하는 패턴이 일반적입니다. - worker이 work가 어느 워커에 큐잉되어 있는지 역참조합니다.
kthread_flush_work()와kthread_cancel_work_sync()가 해당 워커의 락을 획득하기 위해 사용합니다. 미큐잉 상태에서는NULL입니다. - canceling진행 중인 cancel 요청 횟수입니다. cancel 중에는 재큐잉(re-queue)을 방지하여 안전한 취소를 보장합니다.
kthreadd() 소스 분석
kthreadd()는 PID 2로 실행되며, kthread_create_list를 감시하다가 요청이 들어오면
create_kthread()를 호출하여 실제 커널 스레드를 생성합니다.
/* kernel/kthread.c */
int kthreadd(void *unused)
{
struct task_struct *tsk = current; /* 현재 태스크 = kthreadd 자신 */
/* 모든 CPU에서 실행 가능하도록 설정 (housekeeping CPU 집합) */
set_cpus_allowed_ptr(tsk, housekeeping_cpumask(HK_TYPE_KTHREAD));
set_mems_allowed(node_states[N_MEMORY]); /* 모든 NUMA 노드 허용 */
current->flags |= PF_NOFREEZE; /* freezer 대상 제외: kthreadd는 항상 실행 */
cgroup_init_kthreadd(); /* cgroup 초기화 */
ignore_signals(tsk); /* 모든 시그널 무시 */
for (;;) {
set_current_state(TASK_INTERRUPTIBLE); /* 슬립 준비 */
if (list_empty(&kthread_create_list))
schedule(); /* 요청 없음 → CPU 양보, 깨어날 때까지 대기 */
__set_current_state(TASK_RUNNING); /* RUNNING 복귀 */
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
create = list_entry(kthread_create_list.next,
struct kthread_create_info,
list);
list_del_init(&create->list); /* 리스트에서 분리 */
spin_unlock(&kthread_create_lock);
create_kthread(create); /* 실제 스레드 생성 */
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
return 0; /* 도달하지 않음 */
}
코드 설명
- set_cpus_allowed_ptrkthreadd가 실행될 수 있는 CPU를 housekeeping CPU 집합으로 제한합니다. isolated CPU(nohz_full)에서는 kthreadd가 실행되지 않아, 레이턴시(Latency) 민감 태스크의 방해를 방지합니다.
- PF_NOFREEZE이 플래그가 설정된 커널 스레드는 freezer 대상에서 제외됩니다. kthreadd는 suspend 중에도 다른 freezable 스레드들이 동결 요청을 받을 수 있도록 계속 실행되어야 합니다.
- ignore_signalskthreadd의 모든 시그널을 무시합니다. 생성 요청은 리스트를 통해 전달되므로 시그널이 필요 없으며, 의도치 않은 종료를 방지합니다.
- set_current_state / schedule리스트가 비어있을 때
TASK_INTERRUPTIBLE로 전환 후schedule()로 CPU를 양보합니다. 새 요청이 들어오면wake_up_process(kthreadd_task)로 깨어납니다. 이 패턴은 wakeup-miss 레이스를 방지합니다. - spin_lock / list_del_init락을 보유한 채 리스트에서 항목을 꺼낸 뒤, 락을 해제하고
create_kthread()를 호출합니다. 락을 보유한 채 스레드를 생성하면 데드락이 발생할 수 있으므로 반드시 먼저 해제해야 합니다. - create_kthread(create)내부적으로
kernel_thread(kthread, create, ...)를 호출합니다. 새 스레드는kthread()래퍼에서 시작하며, 이 함수가complete(done)으로 호출자를 깨웁니다.
kthread() 래퍼 소스 분석
kthread()는 모든 커널 스레드의 실제 진입점(Entry Point)입니다.
kernel_clone()으로 생성된 태스크가 가장 먼저 이 함수를 실행합니다.
/* kernel/kthread.c - kthread() 래퍼 (실제 커널 스레드 진입점) */
static int kthread(void *_create)
{
struct kthread_create_info *create = _create;
int (*threadfn)(void *data) = create->threadfn; /* 로컬로 복사 */
void *data = create->data;
struct completion *done = create->done;
struct kthread *self;
int ret;
/* 태스크 스택 최상단에 위치하는 kthread 메타데이터 초기화 */
self = to_kthread(current);
init_completion(&self->exited);
init_completion(&self->parked);
current->vfork_done = &self->exited; /* 종료 시 complete용 */
/* 호출자(kthread_create)에게 task_struct 포인터 전달 */
create->result = current;
complete(done); /* 이 시점 이후 create를 역참조하면 안 됨 */
/* wake_up_process()가 호출될 때까지 슬립
* kthread_run()이면 즉시 깨어남
* kthread_create()면 호출자가 wake_up_process() 호출 시까지 대기 */
schedule_preempt_disabled();
if (!test_bit(KTHREAD_SHOULD_STOP, &self->flags)) {
/* 정상 실행 경로: threadfn 호출 */
ret = threadfn(data);
} else {
/* kthread_stop()이 wake_up_process() 전에 호출된 경우 */
ret = -EINTR;
}
kthread_exit(ret);
/* 도달하지 않음: kthread_exit()는 do_exit()를 호출 */
}
코드 설명
- threadfn / data 로컬 복사
complete(done)이후create_info는 호출자가 언제든 해제할 수 있습니다. 따라서complete()전에 필요한 값을 로컬 변수에 복사하여 use-after-free를 방지합니다. - to_kthread(current)태스크 스택 최상단에서
struct kthread를 얻는 매크로입니다.task_thread_info(task)위에 위치합니다. 이 구조체는kernel_clone()이 스택을 설정할 때 이미 공간이 확보되어 있습니다. - current->vfork_done = &self->exited커널 스레드 종료 시
do_exit()가vfork_done을 통해complete()를 호출합니다.kthread_stop()은exitedcompletion을 대기하여 스레드 종료를 확인합니다. - complete(done)호출자(
__kthread_create_on_node())에서wait_for_completion_killable(done)으로 대기 중인 스레드를 깨웁니다. 이 시점에서create->result에 새 태스크 포인터가 저장되어 있습니다. - schedule_preempt_disabled()선점(Preemption)을 비활성화한 채 스케줄러를 호출하여 CPU를 양보합니다.
TASK_UNINTERRUPTIBLE상태로 잠들어,wake_up_process()호출만 이 스레드를 깨울 수 있습니다. - test_bit(KTHREAD_SHOULD_STOP)스레드가 깨어나기 전에 이미
kthread_stop()이 호출된 경우를 처리합니다. 이 경우threadfn을 실행하지 않고-EINTR을 반환합니다.
kthread_worker_fn() 소스 분석
kthread_worker_fn()은 kthread_create_worker()가 내부적으로 생성하는
전용 워커 스레드의 메인 루프입니다. work_list에서 작업을 꺼내어 순차 실행합니다.
/* kernel/kthread.c - kthread_worker_fn() */
int kthread_worker_fn(void *worker_ptr)
{
struct kthread_worker *worker = worker_ptr;
struct kthread_work *work;
WARN_ON(worker->task); /* 중복 실행 방지 */
worker->task = current;
repeat:
set_current_state(TASK_INTERRUPTIBLE); /* 슬립 준비 */
if (kthread_should_stop()) { /* stop 요청 확인 */
__set_current_state(TASK_RUNNING);
break;
}
work = NULL;
spin_lock_irq(&worker->lock);
if (!list_empty(&worker->work_list)) {
work = list_first_entry(&worker->work_list,
struct kthread_work, node);
list_del_init(&work->node); /* 큐에서 분리 */
}
worker->current_work = work; /* flush 동기화를 위해 기록 */
spin_unlock_irq(&worker->lock);
if (work) {
__set_current_state(TASK_RUNNING);
work->func(work); /* 작업 실행 */
cond_resched(); /* 선점 포인트: 다른 태스크에 양보 */
} else {
schedule(); /* 할 일 없음 → 슬립 */
}
goto repeat;
}
EXPORT_SYMBOL_GPL(kthread_worker_fn);
코드 설명
- worker->task = current워커 스레드 자신을
kthread_worker.task에 등록합니다. 이후kthread_destroy_worker()가kthread_stop(task)을 호출할 때 이 포인터를 사용합니다. - set_current_state(TASK_INTERRUPTIBLE)작업을 꺼내기 전에 먼저 상태를 변경합니다. 이 순서가 중요한데, 만약
work_list확인 후 슬립 전에 새 작업이 큐잉되더라도 wakeup 신호를 놓치지 않습니다. - list_first_entry / list_del_initFIFO 순서로 작업을 꺼냅니다.
list_del_init()으로 노드를 초기화하여, 실행 중에도 같은 work를 재큐잉할 수 있도록 합니다. - worker->current_work = work현재 실행 중인 work를 기록합니다.
kthread_flush_work(work)는 이 값을 확인하여 해당 work가 실행 완료될 때까지 대기합니다. 실행 완료 후 다음 반복에서 새 work로 갱신됩니다. - work->func(work)work 함수를 직접 호출합니다. 워커 스레드와 동일한 컨텍스트에서 실행되므로, work 함수의 스케줄링 우선순위와 CPU affinity가 워커 스레드 설정을 따릅니다.
- cond_resched()긴 작업 처리 후 자발적 선점 포인트를 제공합니다. 비선점형 커널 설정에서도 다른 고우선순위 태스크가 실행될 기회를 보장합니다.
- goto repeat무한 루프를
goto로 구현한 것은 컴파일러 최적화를 위한 것이 아니라,TASK_INTERRUPTIBLE상태 설정이 루프 최상단에서 이루어지도록 하여 wakeup-miss 레이스를 방지하기 위함입니다.
kthread_worker_fn() 실행 흐름
Freezer 연동
시스템 절전(suspend/hibernate) 시 커널의 freezer는 모든 유저 프로세스와 freezable 커널 스레드를 동결합니다. I/O를 수행하는 커널 스레드가 suspend 도중 디스크에 접근하면 데이터 손상이 발생할 수 있으므로, freezer 연동은 전원 관리(Power Management)의 핵심입니다.
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();
}
set_freezable()을 호출하지 않은 커널 스레드는 기본적으로 PF_NOFREEZE 플래그가 설정되어 suspend 시에도 동결되지 않습니다. migration/N, ksoftirqd/N 등 시스템 필수 스레드는 의도적으로 non-freezable입니다. I/O를 수행하지 않는 순수 계산 스레드도 non-freezable로 두는 것이 안전합니다.
freeze_kernel_threads() 단계에서 거의 모든 커널 스레드가 정지됩니다. 이때 동결 타임아웃(기본 20초) 내에 try_to_freeze()에 도달하지 못하는 스레드가 있으면 suspend가 취소됩니다.
커널 스레드 시그널 처리
커널 스레드는 유저 프로세스와 달리 대부분의 시그널을 무시합니다. 대신 kthread_should_stop()과 kthread_should_park()라는 커널 내부 플래그 기반의 제어 메커니즘을 사용합니다.
시그널 차단 메커니즘
/* kthreadd가 생성하는 모든 커널 스레드의 시그널 마스크 */
/* kernel/kthread.c - kthreadd() */
ignore_signals(current);
/* → sigfillset(¤t->blocked) — 모든 시그널 차단 */
/* 자식 스레드(새로 생성되는 kthread)도 이 마스크를 상속
* → 기본적으로 모든 커널 스레드는 시그널 전달 불가 */
kthread 제어 플래그
| 플래그 | 설정하는 함수 | 확인하는 함수 | 의미 |
|---|---|---|---|
KTHREAD_SHOULD_STOP | kthread_stop() | kthread_should_stop() | 스레드 종료 요청. 스레드 함수의 메인 루프 탈출 조건 |
KTHREAD_SHOULD_PARK | kthread_park() | kthread_should_park() | 스레드 일시 중지 요청. kthread_parkme() 호출 트리거 |
KTHREAD_IS_PARKED | kthread_parkme() | 내부 사용 | 현재 park 상태임을 표시. unpark 시 해제 |
KTHREAD_IS_PER_CPU | kthread_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()을 사용하는 것은 일반적으로 권장되지 않습니다. 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 트레이스포인트 활용
# 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 WQ | workqueue (CMWQ) | kthread_worker | kthread_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 |
| 사용 난이도 | 중간 | 낮음 | 중간 | 높음 | 중간 |
사용 사례별 권장 패턴
| 사용 사례 | 권장 메커니즘 | 이유 |
|---|---|---|
| 네트워크 패킷(Packet) 수신 처리 | softirq (NAPI) | 지연 최소화, 높은 처리량 필요 |
| 디바이스 드라이버 초기화 작업 | workqueue | 일회성 비동기 작업, 슬립 가능 |
| 스토리지 I/O 완료 처리 | workqueue (WQ_HIGHPRI) | 고우선순위, 시스템 관리 풀 활용 |
| DRM/GPU 명령 제출 | kthread_worker | 순차 실행 보장, RT 우선순위 필요 |
| NAND 플래시 쓰기 | kthread_worker | 전용 스레드에서 직렬화된 I/O |
| USB 이벤트 처리 | kthread_run() | 복잡한 상태 머신, 전용 루프 필요 |
| per-CPU 캐시 관리 | smpboot | CPU별 독립 처리, 자동 hotplug |
| softirq 과부하 대응 | smpboot (ksoftirqd) | 커널 내장, 자동 관리 |
| 파일시스템(Filesystem) 저널 커밋 | kthread_run() | 장시간 블록 가능, 전용 제어 루프 |
| 메모리 페이지 회수 | kthread_run() (kswapd) | 복잡한 정책, 전역 상태 관리 |
/* 패턴 1: 단순 비동기 작업 → workqueue (가장 간단) */
static void simple_work_fn(struct work_struct *work)
{
do_deferred_init();
}
static DECLARE_WORK(init_work, simple_work_fn);
schedule_work(&init_work);
/* 패턴 2: 순차 보장 → kthread_worker */
struct kthread_worker *w = kthread_create_worker(0, "serial-io");
kthread_init_work(&my_work, serial_io_fn);
kthread_queue_work(w, &my_work);
/* 패턴 3: 복잡한 상태 머신 → kthread_run() */
kthread_run(complex_state_machine_fn, dev, "my-daemon");
/* 패턴 4: per-CPU 처리 → smpboot */
static struct smp_hotplug_thread my_threads = {
.store = &per_cpu_task,
.thread_should_run = my_should_run,
.thread_fn = my_per_cpu_fn,
.thread_comm = "mythread/%u",
};
smpboot_register_percpu_thread(&my_threads);
참고자료
커널 공식 문서
- Kernel Thread API — kernel.org — kthread_run, kthread_create, kthread_stop 등 커널 스레드 공식 API 레퍼런스입니다
- Concurrency Managed Workqueue (cmwq) — kernel.org — 커널 스레드 대안으로 권장되는 CMWQ 프레임워크 공식 문서입니다
- Kernel Sysctl Parameters — kernel.org — kthread_cpus 등 커널 스레드 관련 sysctl 튜닝 매개변수를 설명합니다
LWN.net 기사
- The kthread API (LWN, 2004) — kthread_create/kthread_stop API 도입 배경과 kernel_thread() 대비 개선점을 다룬 기사입니다
- Concurrency-managed workqueues (LWN, 2010) — 커널 스레드 직접 생성 대신 워크큐를 활용하는 CMWQ 설계 동기를 설명합니다
- The kthread_worker API (LWN, 2012) — kthread_worker/kthread_work 인프라의 도입과 활용 방법을 소개합니다
- Freezing and thawing kernel threads (LWN, 2005) — 시스템 절전(suspend) 시 커널 스레드 동결(freeze) 메커니즘을 다룹니다
- kthread_should_stop() and wakeups (LWN, 2007) — 커널 스레드 종료 처리 시 kthread_should_stop()과 웨이크업 경합 문제를 설명합니다
- Bounding kernel threads to CPUs (LWN, 2015) — kthread_bind와 CPU 어피니티 설정을 통한 커널 스레드 CPU 격리 기법을 다룹니다
커널 소스 코드
- kernel/kthread.c — kthread_create_on_node, kthread_worker_fn 등 커널 스레드 핵심 구현체입니다
- include/linux/kthread.h — kthread_run, kthread_worker, kthread_work 등 핵심 매크로와 구조체 정의입니다
- include/linux/smpboot.h — smp_hotplug_thread 구조체와 smpboot_register_percpu_thread 등 per-CPU 스레드 프레임워크 정의입니다
- kernel/smpboot.c — per-CPU 커널 스레드의 hotplug 관리와 park/unpark 구현입니다
- kthread_worker_fn() — kernel/kthread.c — kthread_worker 메인 루프 구현으로, work 큐잉과 실행 흐름을 확인할 수 있습니다
서적
- Linux Kernel Development, 3rd Edition (Robert Love, Addison-Wesley, 2010) — Chapter 3 "Process Management"에서 커널 스레드 생성과 관리를 설명합니다
- Linux Device Drivers, 3rd Edition (Corbet, Rubini, Kroah-Hartman, O'Reilly, 2005) — Chapter 7 "Time, Delays, and Deferred Work"에서 커널 스레드 기반 작업 처리를 다룹니다
- Understanding the Linux Kernel, 3rd Edition (Bovet & Cesati, O'Reilly, 2005) — Chapter 3 "Processes"에서 커널 스레드의 내부 구조와 스케줄링을 상세히 설명합니다
- Professional Linux Kernel Architecture (Wolfgang Mauerer, Wrox, 2008) — 커널 스레드의 생성 과정과 kthreadd 데몬의 역할을 분석합니다
관련 문서
Kernel Threads와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.