Tasklet 심화
Tasklet은 softirq(TASKLET_SOFTIRQ, HI_SOFTIRQ) 위에 구축된 동적 Bottom Half 메커니즘입니다.
softirq와 달리 런타임에 동적으로 생성/삭제가 가능하며, 같은 tasklet은 절대 병렬 실행되지 않는 직렬화를 보장합니다.
이 문서에서는 tasklet_struct의 내부 구조, 상태 머신, Per-CPU 리스트 관리, 스케줄링 내부 구현,
직렬화 보장 메커니즘, disable/enable/kill API, PREEMPT_RT 환경에서의 동작 변화,
그리고 workqueue/threaded IRQ로의 마이그레이션 방법까지 상세히 다룹니다.
핵심 요약
- 동적 Bottom Half — softirq와 달리 런타임에 동적으로 생성/삭제 가능.
tasklet_setup()으로 초기화,tasklet_kill()로 해제. - 직렬화 보장 —
TASKLET_STATE_RUN비트로 같은 tasklet의 병렬 실행을 차단. 다른 tasklet은 다른 CPU에서 동시 실행 가능. - Per-CPU 리스트 —
tasklet_vec(일반)과tasklet_hi_vec(고우선순위) 두 개의 Per-CPU 단일 연결 리스트로 관리. - 상태 머신 —
TASKLET_STATE_SCHED와TASKLET_STATE_RUN두 비트로 Idle, Scheduled, Running, Disabled 상태 전이. - 새 API — 커널 5.9+에서
tasklet_setup()+from_tasklet()조합이 타입 안전한 표준 방법. - deprecated 추세 — 새 코드에서는 workqueue 또는 threaded IRQ를 사용 권장. PREEMPT_RT 비호환, 슬립 불가, 부하 분산 불가 등이 이유.
단계별 이해
- 구조체 이해
tasklet_struct의state,count,callback필드와 Per-CPU 리스트(tasklet_head)의 관계를 먼저 파악합니다. - 상태 머신 추적
Idle → Scheduled → Running → Idle 전이와 Disabled 상태를 포함한 전체 상태 다이어그램을 이해합니다. - 스케줄링 내부
tasklet_schedule()의test_and_set_bit→ Per-CPU 리스트 추가 →raise_softirq경로를 코드 레벨에서 따라갑니다. - 실행 경로 분석
tasklet_action()에서tasklet_trylock→count확인 → 콜백 호출 → unlock 과정을 이해합니다. - API 사용법
tasklet_setup(),tasklet_schedule(),tasklet_disable(),tasklet_enable(),tasklet_kill()의 올바른 사용 패턴과 주의사항을 익힙니다. - 마이그레이션 학습
기존 tasklet 코드를 workqueue 또는 threaded IRQ로 전환하는 방법을 실제 코드 변환 예제로 연습합니다.
kernel/softirq.c, include/linux/interrupt.h.
종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
개요: 역사적 맥락과 현재 상태
Tasklet은 리눅스 커널 2.3 개발 주기에서 Alexey Kuznetsov에 의해 도입된 Bottom Half 메커니즘입니다. 기존의 BH(Bottom Half) 메커니즘은 전역 락으로 인해 SMP 확장성이 극히 제한적이었고, softirq는 동적 생성이 불가능하여 드라이버 개발자가 직접 사용하기 어려웠습니다. Tasklet은 이 두 가지 한계를 동시에 해결하기 위해 설계되었습니다.
Tasklet의 핵심 설계 목표는 다음 세 가지였습니다:
- 동적 등록: softirq와 달리 런타임에 자유롭게 생성/삭제 가능. 드라이버 모듈이 로드/언로드될 때 tasklet을 동적으로 관리할 수 있습니다.
- 자동 직렬화: 같은 tasklet 인스턴스는 동시에 하나의 CPU에서만 실행됩니다. 이를 통해 tasklet 콜백 내부에서 해당 데이터에 대한 별도의 락이 불필요합니다.
- softirq 기반 실행:
TASKLET_SOFTIRQ(인덱스 6) 또는HI_SOFTIRQ(인덱스 0) 위에서 실행되므로, 인터럽트 리턴 직후 빠르게 처리됩니다.
그러나 커널이 발전하면서 tasklet의 한계가 명확해졌습니다. 2020년 Kees Cook, Sebastian Andrzej Siewior 등의 커널 개발자들이 tasklet의 deprecation을 본격적으로 논의하기 시작했으며,
커널 5.9에서 tasklet_setup() API가 도입된 것은 기존 tasklet_init()의 타입 불안전 문제를 해결하면서도
장기적으로 workqueue/threaded IRQ로의 마이그레이션을 용이하게 하기 위함이었습니다.
2024년 현재에도 수백 개의 드라이버가 tasklet을 사용하고 있어 완전한 제거에는 시간이 걸리지만,
새 코드에서는 workqueue 또는 threaded IRQ를 사용해야 합니다.
- 커널 2.3: tasklet 도입 (BH 메커니즘 대체)
- 커널 2.5: BH 완전 제거, tasklet이 주요 동적 Bottom Half
- 커널 5.9:
tasklet_setup()+from_tasklet()신규 API - 커널 5.10+: 다수 드라이버 tasklet → workqueue 전환 패치
- 현재: 새 코드 사용 금지, 기존 코드 점진적 마이그레이션 중
Tasklet 생명주기 상태 머신
tasklet은 TASKLET_STATE_SCHED와 TASKLET_STATE_RUN 두 개의 상태 비트와 count 필드 조합으로 생명주기를 관리합니다. 아래 다이어그램은 모든 가능한 상태 전이를 보여줍니다.
Per-CPU Tasklet 리스트
tasklet은 Per-CPU 단일 연결 리스트에 저장됩니다. 일반 tasklet과 고우선순위 tasklet은 별도 리스트를 사용합니다:
/* kernel/softirq.c */
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
/* Per-CPU 리스트: 일반 tasklet (TASKLET_SOFTIRQ, 우선순위 6) */
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
/* Per-CPU 리스트: 고우선순위 tasklet (HI_SOFTIRQ, 우선순위 0) */
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
/*
* 리스트 구조 (Per-CPU):
*
* tasklet_vec.head ──▶ [tasklet_A] ──▶ [tasklet_B] ──▶ [tasklet_C] ──▶ NULL
* ↑ ↑
* 먼저 스케줄됨 tasklet_vec.tail
*
* 특징:
* - FIFO 순서: 먼저 schedule된 tasklet이 먼저 실행
* - tail 포인터로 O(1) 추가 보장
* - tasklet_schedule() 시 현재 CPU의 리스트에 추가
* - 실행도 같은 CPU에서 (Per-CPU 바인딩)
*/
각 CPU는 자신만의 tasklet_vec과 tasklet_hi_vec를 유지합니다. tasklet_schedule() 호출 시 현재 CPU의 리스트에 추가되므로, tasklet은 기본적으로 스케줄한 CPU에서 실행됩니다. 이 Per-CPU 설계는 캐시 친화적이지만, CPU간 부하 분산이 불가능하다는 단점이 있습니다.
HI_SOFTIRQ vs TASKLET_SOFTIRQ
| 속성 | HI_SOFTIRQ (인덱스 0) | TASKLET_SOFTIRQ (인덱스 6) |
|---|---|---|
| 우선순위 | 최고 (softirq 중 가장 먼저 실행) | 일반 (SCHED, HRTIMER 다음) |
| Per-CPU 리스트 | tasklet_hi_vec | tasklet_vec |
| 스케줄 API | tasklet_hi_schedule() | tasklet_schedule() |
| 핸들러 | tasklet_hi_action() | tasklet_action() |
| 실행 순서 | NET_TX/RX, BLOCK, TIMER 보다 먼저 | 대부분의 softirq 이후 |
| 용도 | 극히 낮은 지연 필요 시 (드물게 사용) | 일반적인 Bottom Half 작업 |
/* softirq 우선순위 순서 (낮은 인덱스 = 먼저 실행) */
/* 0: HI_SOFTIRQ ← tasklet_hi_schedule()로 등록된 tasklet */
/* 1: TIMER_SOFTIRQ */
/* 2: NET_TX_SOFTIRQ */
/* 3: NET_RX_SOFTIRQ */
/* 4: BLOCK_SOFTIRQ */
/* 5: IRQ_POLL_SOFTIRQ */
/* 6: TASKLET_SOFTIRQ ← tasklet_schedule()로 등록된 tasklet */
/* 7: SCHED_SOFTIRQ */
/* 8: HRTIMER_SOFTIRQ */
/* 9: RCU_SOFTIRQ */
/* 고우선순위 tasklet 사용 예 (커널 사운드 서브시스템) */
struct snd_pcm_substream {
struct tasklet_struct tasklet; /* 오디오 버퍼 처리 */
/* ... */
};
/* 오디오는 지연 민감 → HI_SOFTIRQ 사용 */
tasklet_hi_schedule(&substream->tasklet);
HI_SOFTIRQ 남용 주의: tasklet_hi_schedule()은 타이머, 네트워크, 블록 I/O보다 먼저 실행됩니다. 과도한 사용은 이들 서브시스템의 지연을 유발합니다. 정말로 최소 지연이 필요한 경우에만 사용하세요.
tasklet_struct 필드
/* include/linux/interrupt.h */
struct tasklet_struct {
struct tasklet_struct *next; /* Per-CPU 리스트의 다음 tasklet */
unsigned long state; /* TASKLET_STATE_SCHED | TASKLET_STATE_RUN */
atomic_t count; /* 0이면 활성, >0이면 비활성 (disable 카운트) */
bool use_callback; /* 새 API (callback) vs 레거시 (func) */
union {
void (*callback)(struct tasklet_struct *t); /* 새 API */
void (*func)(unsigned long data); /* 레거시 */
};
unsigned long data; /* 레거시 API용 인자 */
};
tasklet_setup() vs tasklet_init() API
커널 5.9에서 tasklet_setup()이 도입되어 기존 tasklet_init()을 대체합니다. 새 API는 from_tasklet() 매크로와 함께 사용하여 타입 안전한 컨테이너 접근을 제공합니다:
/* ====== 새 API (커널 5.9+, 권장) ====== */
#include <linux/interrupt.h>
struct my_device {
struct tasklet_struct tasklet;
u32 pending_data;
};
/* 콜백: tasklet_struct 포인터를 직접 받음 */
static void my_tasklet_cb(struct tasklet_struct *t)
{
/* from_tasklet(): container_of 래퍼 매크로 */
struct my_device *dev = from_tasklet(dev, t, tasklet);
process_data(dev->pending_data);
}
/* 초기화: 내부적으로 use_callback = true 설정 */
tasklet_setup(&dev->tasklet, my_tasklet_cb);
/* 정적 선언 매크로 */
DECLARE_TASKLET(name, callback); /* count=0 (활성) */
DECLARE_TASKLET_DISABLED(name, callback); /* count=1 (비활성) */
/* ====== 레거시 API (deprecated) ====== */
/* 콜백: unsigned long data 인자 사용 → 타입 불안전 */
static void old_tasklet_func(unsigned long data)
{
struct my_device *dev = (struct my_device *)data; /* 캐스트 필요 */
process_data(dev->pending_data);
}
/* 초기화: func + data 방식 (use_callback = false) */
tasklet_init(&dev->tasklet, old_tasklet_func,
(unsigned long)dev); /* 캐스트 필요 */
| 속성 | tasklet_setup() (새 API) | tasklet_init() (레거시) |
|---|---|---|
| 커널 버전 | 5.9+ | 2.4+ |
| 콜백 시그니처 | void (*)(struct tasklet_struct *) | void (*)(unsigned long) |
| 컨테이너 접근 | from_tasklet() (타입 안전) | (struct xxx *)data (캐스트) |
| use_callback 필드 | true | false |
| 상태 | 권장 | deprecated (기존 코드 호환용) |
마이그레이션 팁: tasklet_init() → tasklet_setup() 전환 시, from_tasklet() 매크로를 사용하면 됩니다. 이 매크로는 container_of()의 래퍼로, 첫 번째 인자에 결과 변수명, 두 번째에 tasklet 포인터, 세 번째에 구조체 내 tasklet 필드명을 지정합니다.
상태 머신 상세
tasklet은 두 개의 상태 비트로 스케줄링과 실행을 제어합니다:
/* 상태 비트 정의 */
enum {
TASKLET_STATE_SCHED, /* bit 0: 실행 대기열에 등록됨 */
TASKLET_STATE_RUN, /* bit 1: 현재 실행 중 (SMP에서만 의미) */
};
/*
* 상태 전이:
*
* [Idle] ──tasklet_schedule()──▶ [Scheduled]
* state: 0 state: SCHED
* ▲ │
* │ │ softirq 실행 시
* │ ▼
* │ [Running]
* │ state: SCHED | RUN
* │ │
* └──────── 완료 후 클리어 ◀──────────┘
* SCHED, RUN 모두 클리어
*
* 핵심 규칙:
* - SCHED가 이미 set이면 tasklet_schedule()은 no-op
* → 같은 tasklet을 여러 번 schedule해도 한 번만 실행
* - RUN이 set이면 다른 CPU에서 실행 시도 시 건너뜀
* → 같은 tasklet의 병렬 실행 방지
*/
tasklet_schedule() 내부 구현
tasklet_schedule()은 tasklet을 Per-CPU 리스트에 추가하고 TASKLET_SOFTIRQ를 트리거합니다. 핵심은 TASKLET_STATE_SCHED 비트의 원자적 test-and-set입니다:
/* kernel/softirq.c - tasklet_schedule() 구현 */
static inline void tasklet_schedule(struct tasklet_struct *t)
{
/* SCHED 비트가 이미 set이면 false 반환 → 아무것도 안 함 */
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags); /* IRQ 비활성화 (Per-CPU 리스트 보호) */
/* 현재 CPU의 tasklet_vec 리스트 tail에 추가 */
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &t->next);
raise_softirq_irqoff(TASKLET_SOFTIRQ); /* softirq 트리거 */
local_irq_restore(flags);
}
/*
* 호출 컨텍스트별 동작:
*
* 1. Hard IRQ 핸들러 (Top Half) 내부:
* - 가장 일반적인 사용 패턴
* - IRQ가 이미 비활성 상태이므로 local_irq_save는 no-op에 가까움
* - irq_exit() 시점에 TASKLET_SOFTIRQ가 실행됨
*
* 2. softirq 핸들러 내부:
* - 현재 softirq 루프의 다음 반복에서 실행
* - __do_softirq()가 pending 비트를 재확인하므로
*
* 3. 프로세스 컨텍스트:
* - raise_softirq_irqoff() → wakeup_softirqd() 호출
* - ksoftirqd에서 실행됨
*
* 4. 중복 호출:
* - SCHED 비트가 이미 set → test_and_set_bit 실패 → 즉시 반환
* - 이미 스케줄된 tasklet은 두 번 큐에 들어가지 않음
*/
직렬화 보장
같은 tasklet 인스턴스는 TASKLET_STATE_RUN 비트에 의해 절대 병렬 실행되지 않습니다. 이것이 tasklet의 가장 중요한 특성입니다.
/* include/linux/interrupt.h - tasklet 직렬화 핵심 함수 */
static inline bool tasklet_trylock(struct tasklet_struct *t)
{
return !test_and_set_bit(TASKLET_STATE_RUN, &t->state);
}
static inline void tasklet_unlock(struct tasklet_struct *t)
{
smp_mb__before_atomic();
clear_bit(TASKLET_STATE_RUN, &t->state);
}
/*
* tasklet_action() 내에서의 직렬화 로직:
*
* CPU 0 (실행 시도) CPU 1 (실행 시도)
* ───────────────── ─────────────────
* tasklet_trylock(t)
* → RUN=0 이므로 성공
* → RUN bit set tasklet_trylock(t)
* → RUN=1 이므로 실패
* t->callback(t) 실행 중... → 리스트에 다시 추가
* → 다음 softirq에서 재시도
* tasklet_unlock(t)
* → RUN bit clear
*
* 결과: 같은 tasklet t는 CPU 0에서만 실행됨
*/
| 메커니즘 | 같은 인스턴스 병렬 | 다른 인스턴스 병렬 | 슬립 가능 | 직렬화 방법 |
|---|---|---|---|---|
| softirq | 가능 (락 필요) | 가능 | 불가 | 수동 (spin_lock 등) |
| tasklet | 불가 | 가능 | 불가 | TASKLET_STATE_RUN bit |
| workqueue | 불가 (기본) | 가능 | 가능 | work item per-CPU 큐잉 |
| threaded IRQ | 불가 | 가능 | 가능 | IRQ 스레드 직렬화 |
tasklet_disable() / tasklet_enable()
/* count 기반 비활성화/활성화 */
void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t); /* atomic_inc(&t->count) */
tasklet_unlock_wait(t); /* RUN 비트 해제 대기 (SMP) */
smp_mb();
}
/* count > 0이면 tasklet 실행이 보류됨 */
/* 중첩 가능: disable 2번 → enable 2번 필요 */
void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count); /* count가 0이 되면 실행 가능 */
}
/* 사용 패턴: 임시로 tasklet 실행 중지 */
tasklet_disable(&my_tasklet);
/* 이 구간에서 tasklet과 공유하는 데이터를 안전하게 수정 */
/* tasklet이 실행 중이었다면 완료를 기다림 */
tasklet_enable(&my_tasklet);
내부 실행 경로
/* kernel/softirq.c - tasklet_action() 간략화 */
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
/* Per-CPU tasklet 리스트를 원자적으로 가져옴 */
list = __this_cpu_read(tasklet_vec.head);
__this_cpu_write(tasklet_vec.head, NULL);
while (list) {
struct tasklet_struct *t = list;
list = list->next;
/* 다른 CPU에서 실행 중인지 확인 */
if (tasklet_trylock(t)) { /* test_and_set_bit(RUN, &t->state) */
if (!atomic_read(&t->count)) { /* disable되지 않았으면 */
clear_bit(TASKLET_STATE_SCHED, &t->state);
t->callback(t); /* tasklet 핸들러 호출 */
tasklet_unlock(t); /* clear_bit(RUN, ...) */
continue;
}
tasklet_unlock(t);
}
/* 실행 불가: 리스트에 다시 추가하고 재스케줄 */
tasklet_schedule(t);
}
}
/* tasklet_hi_action()도 동일한 로직이지만 HI_SOFTIRQ(우선순위 0)에서 실행 */
tasklet_kill() 내부 동작
tasklet_kill()은 tasklet을 안전하게 비활성화하고 해제하는 API입니다. 드라이버 언로드나 디바이스 제거 시 반드시 호출해야 합니다:
/* kernel/softirq.c - tasklet_kill() 구현 (간략화) */
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
pr_notice("Attempt to kill tasklet from interrupt\\n");
/* 1단계: SCHED 비트가 클리어될 때까지 대기 */
/* → 현재 스케줄된 tasklet의 실행 완료를 보장 */
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
wait_var_event(&t->state,
!test_bit(TASKLET_STATE_SCHED, &t->state));
/* 2단계: RUN 비트가 클리어될 때까지 대기 (SMP) */
/* → 다른 CPU에서 실행 중인 tasklet 완료를 보장 */
tasklet_unlock_wait(t);
/* 3단계: SCHED 비트를 set 상태로 유지 */
/* → 이후 tasklet_schedule() 호출이 no-op이 됨 */
/* → tasklet이 다시 큐에 들어가는 것을 방지 */
}
/*
* tasklet_kill() 사용 규칙:
*
* 1. 프로세스 컨텍스트에서만 호출 (슬립 가능해야 함)
* → 인터럽트/softirq 컨텍스트에서 호출하면 데드락!
*
* 2. tasklet_kill() 후에는 tasklet_schedule()이 무시됨
* → SCHED 비트가 영구적으로 set 상태이므로
* → 재사용하려면 tasklet_setup()으로 재초기화 필요
*
* 3. 드라이버에서의 올바른 해제 순서:
*/
static void my_remove(struct pci_dev *pdev)
{
struct my_device *dev = pci_get_drvdata(pdev);
/* 1. 새 인터럽트 발생 방지 */
free_irq(dev->irq, dev);
/* 2. 이미 스케줄된 tasklet 완료 대기 + 비활성화 */
tasklet_kill(&dev->tasklet);
/* 3. 이제 안전하게 리소스 해제 */
kfree(dev->buffer);
pci_release_regions(pdev);
}
주의: tasklet_kill()을 호출하기 전에 해당 tasklet을 스케줄할 수 있는 모든 소스(IRQ 핸들러 등)를 먼저 비활성화하세요. 그렇지 않으면 tasklet_kill()이 무한히 대기할 수 있습니다.
PREEMPT_RT에서의 Tasklet
PREEMPT_RT(실시간) 커널에서는 tasklet의 동작이 크게 변경됩니다. softirq가 더 이상 인터럽트 컨텍스트에서 직접 실행되지 않으므로, tasklet도 영향을 받습니다:
/*
* PREEMPT_RT에서의 tasklet 변환:
*
* 일반 커널:
* tasklet → TASKLET_SOFTIRQ → __do_softirq() (인터럽트 컨텍스트)
*
* PREEMPT_RT:
* tasklet → TASKLET_SOFTIRQ → ksoftirqd 스레드 (프로세스 컨텍스트)
*
* 영향:
* 1. tasklet이 선점 가능해짐
* → 일반 커널에서 불가능한 우선순위 역전 발생 가능
* → RT 태스크가 tasklet에 의해 지연될 수 있음
*
* 2. ksoftirqd 우선순위에 의존
* → ksoftirqd는 SCHED_NORMAL(nice 0)
* → RT 태스크보다 항상 낮은 우선순위
* → 결정적 지연시간 보장이 어려움
*
* 3. RT 커널에서의 권장 대안:
* → threaded IRQ: 스레드별 우선순위 개별 설정 가능
* → workqueue: CMWQ의 유연한 스케줄링 활용
*/
/* PREEMPT_RT에서 tasklet 대신 threaded IRQ 사용 예시 */
/* Before: tasklet 기반 */
request_irq(irq, my_top_half, 0, "dev", priv);
/* top_half 내부에서: tasklet_schedule(&priv->tasklet); */
/* After: threaded IRQ (RT 호환) */
request_threaded_irq(irq, my_top_half, my_threaded_bottom,
IRQF_ONESHOT, "dev", priv);
/* top_half: return IRQ_WAKE_THREAD; */
/* my_threaded_bottom: 전용 커널 스레드에서 실행 */
/* → chrt으로 스레드 우선순위 조정 가능:
* chrt -f -p 50 $(pgrep irq/XX-dev)
*/
일반 커널 vs PREEMPT_RT 동작 비교
| 특성 | 일반 커널 (PREEMPT_NONE/VOLUNTARY) | PREEMPT_RT (실시간 커널) |
|---|---|---|
| 실행 컨텍스트 | 인터럽트 컨텍스트 (softirq) | 프로세스 컨텍스트 (ksoftirqd 스레드) |
| 선점 가능 여부 | 불가 (softirq 완료까지 실행) | 가능 (RT 태스크에 의해 선점됨) |
| 슬립 가능 여부 | 불가 | 여전히 불가 (API 수준 제약) |
| 우선순위 | softirq 고정 (제어 불가) | ksoftirqd 우선순위 (SCHED_NORMAL, nice 0) |
| 지연시간 결정성 | 낮음 (softirq 누적으로 변동) | 매우 낮음 (RT 태스크가 항상 우선) |
| lockdep 동작 | in_softirq() = true | in_softirq() = true (여전히) |
| spin_lock 동작 | 실제 스핀 (선점 비활성) | rt_mutex로 변환 (슬립 가능) |
| 권장 대안 | 해당 없음 | threaded IRQ (우선순위 제어) 또는 workqueue |
PREEMPT_RT 핵심 문제: RT 커널에서 tasklet은 ksoftirqd 스레드에서 실행되므로 SCHED_NORMAL 우선순위를 갖습니다. RT 태스크(SCHED_FIFO/RR)는 항상 ksoftirqd보다 먼저 실행되어, tasklet의 지연 시간이 예측 불가능해집니다. 실시간 시스템에서는 반드시 threaded IRQ로 전환하세요.
실제 커널 드라이버 사용 예제
실제 커널 소스에서 tasklet을 사용하는 대표적인 패턴입니다:
/* 예시: 네트워크 드라이버의 tasklet 기반 수신 처리 */
struct my_nic {
struct net_device *netdev;
struct tasklet_struct rx_tasklet;
spinlock_t rx_lock;
struct sk_buff_head rx_queue; /* 수신 패킷 큐 */
void __iomem *regs;
};
/* Top Half: 하드웨어 인터럽트 핸들러 */
static irqreturn_t my_nic_irq(int irq, void *dev_id)
{
struct my_nic *nic = dev_id;
u32 status = ioread32(nic->regs + IRQ_STATUS);
if (!(status & RX_IRQ_BIT))
return IRQ_NONE;
/* 인터럽트 ACK + 추가 인터럽트 마스킹 */
iowrite32(RX_IRQ_BIT, nic->regs + IRQ_ACK);
iowrite32(0, nic->regs + IRQ_MASK);
/* Bottom Half로 위임 */
tasklet_schedule(&nic->rx_tasklet);
return IRQ_HANDLED;
}
/* Bottom Half: tasklet 핸들러 */
static void my_nic_rx_tasklet(struct tasklet_struct *t)
{
struct my_nic *nic = from_tasklet(nic, t, rx_tasklet);
struct sk_buff *skb;
int budget = 64; /* 한 번에 최대 64개 패킷 처리 */
spin_lock(&nic->rx_lock);
while (budget-- > 0) {
u32 desc_status = ioread32(nic->regs + RX_DESC);
if (!(desc_status & DESC_READY))
break;
skb = netdev_alloc_skb(nic->netdev, desc_status & DESC_LEN_MASK);
if (!skb) {
nic->netdev->stats.rx_dropped++;
continue;
}
/* DMA 버퍼에서 skb로 복사 (인터럽트 컨텍스트이므로 GFP_ATOMIC) */
memcpy(skb_put(skb, desc_status & DESC_LEN_MASK),
nic->rx_buf, desc_status & DESC_LEN_MASK);
skb->protocol = eth_type_trans(skb, nic->netdev);
netif_rx(skb);
nic->netdev->stats.rx_packets++;
}
spin_unlock(&nic->rx_lock);
/* 인터럽트 재활성화 */
iowrite32(RX_IRQ_BIT, nic->regs + IRQ_MASK);
}
/* 프로브: 초기화 */
static int my_nic_probe(struct pci_dev *pdev, /* ... */)
{
struct my_nic *nic;
/* ... 할당, 매핑 ... */
tasklet_setup(&nic->rx_tasklet, my_nic_rx_tasklet);
spin_lock_init(&nic->rx_lock);
request_irq(pdev->irq, my_nic_irq, IRQF_SHARED, "my_nic", nic);
return 0;
}
/* 제거: 정리 */
static void my_nic_remove(struct pci_dev *pdev)
{
struct my_nic *nic = pci_get_drvdata(pdev);
free_irq(pdev->irq, nic); /* 1. IRQ 핸들러 해제 */
tasklet_kill(&nic->rx_tasklet); /* 2. tasklet 완료 대기 + 비활성화 */
/* 3. 나머지 리소스 해제 ... */
}
Tasklet 디버깅
/*
* Tasklet 관련 디버깅 기법:
*
* 1. /proc/softirqs - tasklet 실행 횟수 확인
* $ cat /proc/softirqs
* CPU0 CPU1 CPU2 CPU3
* HI: 0 0 0 0 ← 고우선순위 tasklet
* TIMER: 123456 98765 87654 76543
* NET_TX: 100 50 30 20
* NET_RX: 45678 34567 23456 12345
* BLOCK: 5678 4567 3456 2345
* IRQ_POLL: 0 0 0 0
* TASKLET: 12345 8765 6543 4321 ← 일반 tasklet
* SCHED: 34567 23456 12345 9876
* HRTIMER: 10 8 6 4
* RCU: 67890 56789 45678 34567
*
* 2. CPU 편향 확인
* - TASKLET 카운트가 특정 CPU에 집중되면 부하 불균형
* - tasklet은 schedule한 CPU에서만 실행되므로
* - Top Half(IRQ)의 affinity가 원인 → /proc/irq/XX/smp_affinity
*
* 3. ftrace로 tasklet 실행 추적
* $ echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_entry/enable
* $ echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_exit/enable
* $ cat /sys/kernel/debug/tracing/trace
* → tasklet 핸들러 진입/종료 시점, 실행 시간 확인
*
* 4. lockdep 경고
* - CONFIG_PROVE_LOCKING 활성화 시
* - tasklet 핸들러에서 mutex 사용 → 경고 발생
* - 인터럽트 컨텍스트에서 슬립 시도 감지
*
* 5. WARN/BUG 트리거 조건
* - tasklet_kill()을 인터럽트 컨텍스트에서 호출
* - disable된 tasklet을 kill하지 않고 모듈 언로드
* - use-after-free: tasklet_kill() 없이 구조체 해제
*/
쉘 기반 디버깅 명령
# 1. 실시간 softirq 카운트 모니터링 (1초 간격)
watch -n1 'cat /proc/softirqs | head -1; cat /proc/softirqs | grep -E "HI:|TASKLET:"'
# 2. CPU별 TASKLET 처리 횟수 편향 확인
awk '/TASKLET/ {for(i=2;i<=NF;i++) sum+=$i; for(i=2;i<=NF;i++) printf "CPU%d: %d (%.1f%%)\n",i-2,$i,$i/sum*100}' /proc/softirqs
# 3. ftrace로 tasklet 실행 추적 활성화
echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_exit/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -20
# 4. perf로 softirq/tasklet 핫스팟 분석
perf record -g -e irq:softirq_entry -e irq:softirq_exit -- sleep 5
perf report --sort=comm,dso,symbol
# 5. ksoftirqd 스레드 CPU 사용률 확인
ps -eo pid,comm,%cpu,psr | grep ksoftirqd
bpftrace를 사용한 고급 디버깅
# 1. tasklet 콜백 함수별 실행 시간 히스토그램 (마이크로초)
bpftrace -e '
tracepoint:irq:tasklet_entry {
@start[tid] = nsecs;
}
tracepoint:irq:tasklet_exit /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
# 2. tasklet 실행 빈도 - CPU별/초당 카운트
bpftrace -e '
tracepoint:irq:tasklet_entry {
@count[cpu] = count();
}
interval:s:1 {
print(@count);
clear(@count);
}'
# 3. tasklet에서 호출되는 커널 함수 스택 추적
bpftrace -e '
tracepoint:irq:tasklet_entry {
@stacks[kstack] = count();
}'
ksoftirqd CPU 사용률이 높을 때: perf top으로 어떤 softirq/tasklet이 CPU를 점유하는지 확인하세요. perf record -g -e irq:softirq_entry로 softirq별 호출 빈도와 스택 트레이스를 분석할 수 있습니다. bpftrace의 tracepoint:irq:tasklet_entry를 사용하면 실시간으로 어떤 tasklet 콜백이 얼마나 오래 실행되는지 정확히 파악할 수 있습니다.
Deprecation 이유와 대안
tasklet은 다음과 같은 이유로 deprecated 추세이며, 새 코드에서는 사용하지 않아야 합니다:
| 문제점 | 설명 | 대안 |
|---|---|---|
| 인터럽트 컨텍스트 | 슬립 불가, mutex/GFP_KERNEL 사용 불가 | workqueue (프로세스 컨텍스트) |
| PREEMPT_RT 비호환 | RT 커널에서 지연시간 문제 유발 | threaded IRQ / workqueue |
| Per-CPU 고정 | 스케줄한 CPU에서만 실행 → 부하 분산 불가 | workqueue (CMWQ가 자동 분산) |
| 제한적 동시성 | 같은 tasklet 직렬화로 SMP 확장성 부족 | workqueue + 적절한 동기화 |
| 우선순위 역전 | softirq 우선순위에 묶여 우선순위 제어 불가 | threaded IRQ (스레드 우선순위 조정) |
Tasklet → Workqueue 마이그레이션
/* ====== Before: tasklet 사용 ====== */
struct my_device {
struct tasklet_struct tasklet;
/* ... */
};
static void my_tasklet_handler(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, tasklet);
/* 슬립 불가! spin_lock만 사용 가능 */
spin_lock(&dev->lock);
process_data(dev);
spin_unlock(&dev->lock);
}
/* 초기화 */
tasklet_setup(&dev->tasklet, my_tasklet_handler);
/* IRQ 핸들러에서 */
tasklet_schedule(&dev->tasklet);
/* 해제 */
tasklet_kill(&dev->tasklet);
/* ====== After: workqueue 사용 ====== */
struct my_device {
struct work_struct work;
/* ... */
};
static void my_work_handler(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, work);
/* 슬립 가능! mutex, GFP_KERNEL 사용 가능 */
mutex_lock(&dev->mutex);
process_data(dev);
mutex_unlock(&dev->mutex);
}
/* 초기화 */
INIT_WORK(&dev->work, my_work_handler);
/* IRQ 핸들러에서 */
schedule_work(&dev->work);
/* 해제 */
cancel_work_sync(&dev->work);
Tasklet → Threaded IRQ 마이그레이션
특히 하드웨어 인터럽트와 1:1 대응하는 tasklet은 threaded IRQ로 전환하는 것이 더 자연스럽습니다:
/* ====== Before: request_irq + tasklet ====== */
static irqreturn_t my_top_half(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
iowrite32(IRQ_ACK, dev->regs + IRQ_STATUS);
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
static void my_tasklet_handler(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, tasklet);
spin_lock(&dev->lock);
process_data(dev);
spin_unlock(&dev->lock);
}
/* 등록 */
tasklet_setup(&dev->tasklet, my_tasklet_handler);
request_irq(irq, my_top_half, IRQF_SHARED, "dev", dev);
/* ====== After: request_threaded_irq ====== */
static irqreturn_t my_top_half(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
iowrite32(IRQ_ACK, dev->regs + IRQ_STATUS);
return IRQ_WAKE_THREAD; /* tasklet 대신 스레드 깨움 */
}
static irqreturn_t my_threaded_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* 프로세스 컨텍스트! mutex, GFP_KERNEL 사용 가능 */
mutex_lock(&dev->mutex);
process_data(dev);
mutex_unlock(&dev->mutex);
return IRQ_HANDLED;
}
/* 등록: tasklet_setup() + request_irq() 대신 단일 호출 */
request_threaded_irq(irq, my_top_half, my_threaded_handler,
IRQF_SHARED | IRQF_ONESHOT, "dev", dev);
/* tasklet_kill() 대신 free_irq()로 정리 */
| 비교 항목 | tasklet | workqueue | threaded IRQ |
|---|---|---|---|
| 컨텍스트 | 인터럽트 (softirq) | 프로세스 (kworker) | 프로세스 (irq/N-name) |
| 슬립 | 불가 | 가능 | 가능 |
| 지연 시간 | 낮음 | 중간 | 낮음~중간 |
| IRQ 연관 | 간접 | 간접 | 직접 (1:1 대응) |
| 우선순위 제어 | 불가 | 제한적 (nice) | 가능 (chrt) |
| PREEMPT_RT | 문제 있음 | 호환 | 완전 호환 |
| 추가 초기화 | tasklet_setup() | INIT_WORK() | 불필요 |
| 정리 API | tasklet_kill() | cancel_work_sync() | free_irq() |
마이그레이션 선택 기준: IRQ와 1:1 대응하는 Bottom Half는 threaded IRQ로 전환하세요. 여러 소스에서 트리거되거나, 지연 실행이 필요하거나, flush/cancel 같은 고급 제어가 필요하면 workqueue가 적합합니다.
커널 서브시스템 Tasklet 사용 현황
현재 리눅스 커널에서 tasklet을 사용하는 주요 서브시스템과 그 마이그레이션 상태입니다 (커널 6.x 기준):
| 서브시스템 | 용도 | API 유형 | 마이그레이션 상태 |
|---|---|---|---|
| ALSA (사운드) | PCM 기간(period) 완료 처리 | tasklet_hi_schedule() | 일부 드라이버 workqueue 전환 중 |
| USB (EHCI/XHCI) | 전송 완료 후처리 | tasklet_schedule() | 일부 threaded IRQ로 전환 완료 |
| 네트워크 (레거시) | 수신 패킷 처리 | tasklet_schedule() | 대부분 NAPI로 전환 완료 |
| Crypto API | 비동기 암호화 완료 콜백 | tasklet_setup() | 일부 workqueue 전환 논의 중 |
| SCSI (일부) | 명령 완료 처리 | tasklet_schedule() | blk-mq로 마이그레이션 진행 중 |
| 무선 (mac80211) | TX/RX 완료 처리 | tasklet_setup() | 활발히 사용 중 (마이그레이션 예정) |
| DMA Engine | DMA 전송 완료 콜백 | tasklet_setup() | 일부 workqueue 전환 완료 |
| IrDA (적외선) | 데이터 수신 처리 | 레거시 tasklet_init() | 서브시스템 자체 제거됨 (5.17+) |
tasklet_setup()을 사용하는 패치는 리뷰 시 workqueue/threaded IRQ 사용을 권장받습니다. include/linux/interrupt.h에서 tasklet API 위에 "deprecated" 주석이 추가되어 있습니다.
Bottom Half 종합 비교
리눅스 커널의 모든 Bottom Half 메커니즘을 종합적으로 비교합니다:
| 특성 | softirq | tasklet | workqueue | threaded IRQ |
|---|---|---|---|---|
| 실행 컨텍스트 | 인터럽트 (softirq) | 인터럽트 (softirq) | 프로세스 (kworker) | 프로세스 (irq/N) |
| 슬립 가능 | 불가 | 불가 | 가능 | 가능 |
| 동적 생성 | 불가 (컴파일 타임) | 가능 | 가능 | request_threaded_irq() |
| 병렬 실행 | 같은 타입도 다른 CPU에서 가능 | 같은 인스턴스 불가 | 같은 work 불가 | 같은 IRQ 불가 |
| 부하 분산 | 불가 (Per-CPU) | 불가 (Per-CPU) | 가능 (CMWQ 자동) | IRQ affinity로 제어 |
| 우선순위 제어 | 인덱스 순서 고정 | HI(0) 또는 NORMAL(6) | nice 값 (제한적) | chrt로 RT 우선순위 |
| 지연시간 | 최소 | 낮음 | 중간~높음 | 낮음~중간 |
| PREEMPT_RT | ksoftirqd 위임 | ksoftirqd 위임 | 완전 호환 | 완전 호환 |
| 취소/대기 API | 없음 | tasklet_kill() | cancel_work_sync() | free_irq() |
| 타이머 연동 | TIMER_SOFTIRQ 자체 | tasklet_schedule() | delayed_work | 없음 |
| 대표 사용처 | 네트워크, 블록, 타이머 | 사운드, USB, 무선 | 파일시스템, 드라이버 | GPIO, I2C, SPI |
| 새 코드 권장 | 기존 softirq만 | 비권장 (deprecated) | 권장 | 권장 |
마이그레이션 결정 트리
기존 tasklet 코드를 어떤 메커니즘으로 전환할지 결정하는 흐름도입니다:
마이그레이션 원칙: 결정이 어렵다면 workqueue를 선택하세요. CMWQ(Concurrency Managed Workqueue)는 자동 동시성 관리, CPU간 부하 분산, 슬립 가능 등 tasklet의 모든 제한을 해결합니다. threaded IRQ는 하드웨어 인터럽트와 직접 연결된 Bottom Half에만 사용하는 것이 적절합니다.
관련 문서
- Bottom Half 메커니즘 — softirq, tasklet, workqueue, threaded IRQ 전체 개요
- 인터럽트 (Interrupts) — IRQ, Top/Bottom Half, softirq, tasklet, workqueue
- Softirq / Hardirq — 하드웨어 인터럽트와 소프트웨어 인터럽트 상세
- Workqueue (CMWQ) — Concurrency Managed Workqueue 심화