Tasklet
Tasklet은 softirq(TASKLET_SOFTIRQ, HI_SOFTIRQ) 위에 구축된 동적 Bottom Half 메커니즘입니다.
softirq와 달리 런타임에 동적으로 생성/삭제가 가능하며, 같은 tasklet은 절대 병렬 실행되지 않는 직렬화(Serialization)를 보장합니다.
이 문서에서는 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 단일 연결 리스트(Linked List)로 관리. - 상태 머신 —
TASKLET_STATE_SCHED와TASKLET_STATE_RUN두 비트로 Idle, Scheduled, Running, Disabled 상태 전이. - 새 API — 커널 5.9+에서
tasklet_setup()+from_tasklet()조합이 타입 안전한 표준 방법. - deprecated 추세 — 새 코드에서는 workqueue 또는 threaded IRQ를 사용 권장. PREEMPT_RT 비호환, 슬립(Sleep) 불가, 부하 분산(Load Balancing) 불가 등이 이유.
단계별 이해
- 구조체(Struct) 이해
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확인 → 콜백(Callback) 호출 → 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) 위에서 실행되므로, 인터럽트(Interrupt) 리턴 직후 빠르게 처리됩니다.
그러나 커널이 발전하면서 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 전환 패치(Patch)
- 현재: 새 코드 사용 금지, 기존 코드 점진적 마이그레이션 중
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 설계는 캐시(Cache) 친화적이지만, CPU간 부하 분산이 불가능하다는 단점이 있습니다.
HI_SOFTIRQ vs TASKLET_SOFTIRQ
| 속성 | HI_SOFTIRQ (인덱스 0) | TASKLET_SOFTIRQ (인덱스 6) |
|---|---|---|
| 우선순위(Priority) | 최고 (softirq 중 가장 먼저 실행) | 일반 (SCHED, HRTIMER 다음) |
| Per-CPU 리스트 | tasklet_hi_vec | tasklet_vec |
| 스케줄 API | tasklet_hi_schedule() | tasklet_schedule() |
| 핸들러(Handler) | tasklet_hi_action() | tasklet_action() |
| 실행 순서 | NET_TX/RX, BLOCK, TIMER 보다 먼저 | 대부분의 softirq 이후 |
| 용도 | 극히 낮은 지연(Latency) 필요 시 (드물게 사용) | 일반적인 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()은 타이머(Timer), 네트워크, 블록 I/O보다 먼저 실행됩니다. 과도한 사용은 이들 서브시스템의 지연을 유발합니다. 정말로 최소 지연이 필요한 경우에만 사용하세요.
tasklet_struct 필드
필드별 역할
| 필드 | 타입 | 역할 | 관리 주체 |
|---|---|---|---|
next |
struct tasklet_struct * |
Per-CPU 단일 연결 리스트의 다음 노드 | softirq 핸들러 (tasklet_action) |
state |
unsigned long |
비트 플래그: SCHED(스케줄됨), RUN(실행 중) | atomic 연산으로 조작 |
count |
atomic_t |
disable 참조 카운트 (0=활성, >0=비활성) | tasklet_disable/enable |
use_callback |
bool |
새 API(callback) vs 레거시(func) 구분 | tasklet_setup/tasklet_init |
callback |
void (*)(tasklet_struct *) |
새 API 콜백 (5.9+, from_tasklet 사용) | tasklet_setup |
func |
void (*)(unsigned long) |
레거시 콜백 (deprecated) | tasklet_init |
data |
unsigned long |
레거시 API용 인자 (캐스트 필요) | tasklet_init |
state 비트 마스크
/* include/linux/interrupt.h — state 비트 정의 */
enum {
TASKLET_STATE_SCHED, /* 비트 0: Per-CPU 리스트에 스케줄됨
* set: tasklet_schedule() → test_and_set_bit()
* clear: tasklet_action()에서 실행 완료 후
* 의미: 이미 큐에 있으면 중복 스케줄 방지 */
TASKLET_STATE_RUN, /* 비트 1: 현재 CPU에서 실행 중 (SMP 전용)
* set: tasklet_trylock()에서 실행 직전
* clear: tasklet_unlock()에서 실행 완료 후
* 의미: 같은 tasklet의 동시 실행 방지 */
};
/* 동시성 보장 메커니즘:
* 1. SCHED 비트: 같은 tasklet이 2번 큐에 들어가는 것 방지
* 2. RUN 비트: 같은 tasklet이 2개 CPU에서 동시 실행 방지
* → 드라이버는 자체 lock 없이 tasklet 콜백 작성 가능 */
직렬화 보장: 동일한 tasklet은 절대로 두 CPU에서 동시에 실행되지 않습니다. CPU A에서 실행 중일 때 CPU B가 같은 tasklet을 스케줄하면, CPU B는 TASKLET_STATE_RUN 비트를 확인하고 실행을 건너뛴 후 다음 softirq 라운드에서 재시도합니다. 이는 워크큐와 다른 점입니다 (워크큐는 동일 work를 다른 CPU에서 동시 실행 가능). 자세한 비교는 워크큐 페이지(Page)를 참고하세요.
/* 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() 매크로(Macro)와 함께 사용하여 타입 안전한 컨테이너(Container) 접근을 제공합니다:
/* ====== 새 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 비트의 원자적(Atomic) 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 스레드(Thread) 직렬화 |
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(실시간(Real-time)) 커널에서는 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) | 프로세스(Process) 컨텍스트 (ksoftirqd 스레드) |
| 선점(Preemption) 가능 여부 | 불가 (softirq 완료까지 실행) | 가능 (RT 태스크(Task)에 의해 선점됨) |
| 슬립 가능 여부 | 불가 | 여전히 불가 (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 디버깅(Debugging)
/*
* 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 + 적절한 동기화 |
| 우선순위 역전(Priority Inversion) | 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로 전환 완료 |
| 네트워크 (레거시) | 수신 패킷(Packet) 처리 | tasklet_schedule() | 대부분 NAPI로 전환 완료 |
| Crypto API | 비동기 암호화(Encryption) 완료 콜백 | 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, 무선 | 파일시스템(Filesystem), 드라이버 | GPIO, I2C, SPI |
| 새 코드 권장 | 기존 softirq만 | 비권장 (deprecated) | 권장 | 권장 |
마이그레이션 결정 트리
기존 tasklet 코드를 어떤 메커니즘으로 전환할지 결정하는 흐름도입니다:
마이그레이션 원칙: 결정이 어렵다면 workqueue를 선택하세요. CMWQ(Concurrency Managed Workqueue)는 자동 동시성 관리, CPU간 부하 분산, 슬립 가능 등 tasklet의 모든 제한을 해결합니다. threaded IRQ는 하드웨어 인터럽트와 직접 연결된 Bottom Half에만 사용하는 것이 적절합니다.
tasklet_schedule() 호출 체인 분석
Tasklet이 스케줄되어 실행되기까지의 전체 경로를 커널 소스(Source) 수준에서 추적합니다. tasklet_schedule()에서 시작하여 __tasklet_schedule_common()을 거쳐 raise_softirq_irqoff()로 softirq를 트리거하고, 최종적으로 tasklet_action()에서 콜백(Callback)이 호출되는 전체 체인(Chain)입니다.
커널 소스 호출 체인
/* include/linux/interrupt.h */
static inline void tasklet_schedule(struct tasklet_struct *t)
{
/* SCHED 비트가 이미 설정되어 있으면 중복 스케줄 방지 */
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
/* kernel/softirq.c — __tasklet_schedule() 실제 구현 */
void __tasklet_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}
/* kernel/softirq.c — 공통 스케줄링 구현 */
static void __tasklet_schedule_common(struct tasklet_struct *t,
struct tasklet_head __percpu *head,
unsigned int softirq_nr)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(head->tail) = t; /* tail 포인터가 가리키는 곳에 t 저장 */
__this_cpu_write(head->tail, &t->next); /* tail을 t->next 주소로 갱신 */
raise_softirq_irqoff(softirq_nr); /* softirq pending 비트 설정 */
local_irq_restore(flags);
}
/* kernel/softirq.c — raise_softirq_irqoff() 내부 */
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr); /* or_softirq_pending(1 << nr) */
/* 프로세스 컨텍스트에서 호출된 경우 ksoftirqd 깨움 */
if (!in_interrupt() && should_wake_ksoftirqd())
wakeup_softirqd();
}
코드 설명
- 3행
tasklet_schedule()는 인라인(inline) 함수로,test_and_set_bit()로 SCHED 비트를 원자적으로 검사합니다. 이미 set이면 즉시 반환하여 중복 큐잉을 방지합니다. - 9행
__tasklet_schedule()은tasklet_vec(Per-CPU 변수)와TASKLET_SOFTIRQ를 인자로__tasklet_schedule_common()에 위임합니다. HI 버전은tasklet_hi_vec와HI_SOFTIRQ를 사용합니다. - 16행
__tasklet_schedule_common()이 실제 작업을 수행합니다.local_irq_save()로 인터럽트를 비활성화하여 Per-CPU 리스트를 보호합니다. - 21-22행단일 연결 리스트의 tail 포인터를 이용한 O(1) 삽입입니다. tail은 마지막 노드의 next 필드 주소를 가리키므로, 간접 참조로 새 노드를 연결합니다.
- 23행
raise_softirq_irqoff()는 현재 CPU의 softirq pending 비트맵(Bitmap)에 해당 softirq 번호 비트를 설정합니다. - 30행
__raise_softirq_irqoff()는or_softirq_pending(1 << nr)로 pending 비트를 설정합니다. 이 비트는irq_exit()또는ksoftirqd에서 확인됩니다. - 33-34행프로세스 컨텍스트(Process Context)에서 호출된 경우
ksoftirqd커널 스레드(Thread)를 깨워 softirq를 처리하게 합니다. Hard IRQ 컨텍스트에서는irq_exit()시점에 자동 처리됩니다.
Per-CPU tasklet_head 구조
/* kernel/softirq.c — Per-CPU tasklet 리스트 구조 */
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail; /* 마지막 노드의 next 필드 주소 */
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
/*
* 초기화 시: head = NULL, tail = &head
* 첫 삽입: *tail(= head) = t; tail = &t->next
* 두번째: *tail(= 첫번째->next) = t; tail = &t->next
*
* tail 간접 포인터 기법으로 head/non-head 삽입을 통일 처리
*/
코드 설명
- 2-5행
tasklet_head는 head(첫 노드 포인터)와 tail(마지막 노드의 next 필드를 가리키는 이중 포인터)로 구성됩니다. 이 패턴은 리눅스 커널에서 자주 사용하는 tail-pointer 간접 참조 기법입니다. - 7-8행일반 우선순위 tasklet은
tasklet_vec, 고우선순위 tasklet은tasklet_hi_vec에 저장됩니다. 각각 CPU마다 독립적인 리스트를 유지합니다. - 11-14행빈 리스트일 때
tail = &head이므로,*tail = t는 곧head = t입니다. 노드가 있을 때tail은 마지막 노드의next주소이므로 같은 코드로 추가됩니다.
HI 경로: tasklet_hi_schedule()은 동일한 체인이지만 tasklet_hi_vec와 HI_SOFTIRQ(우선순위 0)를 사용합니다. HI_SOFTIRQ는 TASKLET_SOFTIRQ(우선순위 6)보다 먼저 실행되므로, 네트워크 드라이버(Driver) 등에서 긴급 처리가 필요할 때 사용합니다.
struct tasklet_struct 소스 분석
커널 소스에서 tasklet_struct의 각 필드가 어떻게 사용되고 상호작용하는지 분석합니다. 특히 state 필드의 SCHED/RUN 비트 상태 머신(State Machine)과 count 필드의 원자적 참조 카운트(Reference Count) 메커니즘을 중점적으로 살펴봅니다.
커널 소스 정의와 필드별 분석
/* include/linux/interrupt.h — tasklet_struct 전체 정의 */
struct tasklet_struct {
struct tasklet_struct *next; /* [1] Per-CPU 단일 연결 리스트 */
unsigned long state; /* [2] SCHED | RUN 비트 플래그 */
atomic_t count; /* [3] disable 참조 카운트 */
bool use_callback; /* [4] 신/구 API 구분 플래그 */
union {
void (*callback)(struct tasklet_struct *t); /* [5] 신 API */
void (*func)(unsigned long data); /* [6] 레거시 */
};
unsigned long data; /* [7] 레거시 API 인자 */
};
/* state 비트 정의 */
enum {
TASKLET_STATE_SCHED, /* 비트 0: 스케줄 대기 중 */
TASKLET_STATE_RUN, /* 비트 1: 현재 실행 중 (SMP 전용) */
};
/* 상태 전이 함수들 */
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);
}
/* 초기화 함수 — 신 API (5.9+) */
void tasklet_setup(struct tasklet_struct *t,
void (*callback)(struct tasklet_struct *))
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0); /* 0 = 활성 상태 */
t->callback = callback;
t->use_callback = true;
t->data = 0;
}
코드 설명
- 3행
next: Per-CPUtasklet_head단일 연결 리스트에서 다음 tasklet을 가리킵니다.tasklet_action()이 관리하며 드라이버가 직접 접근하면 안 됩니다. - 4행
state: 두 개의 비트 플래그를 사용합니다. SCHED(비트 0)는 "이미 큐에 있음"을, RUN(비트 1)은 "현재 실행 중"을 나타냅니다. 원자적 비트 연산(test_and_set_bit,clear_bit)으로만 조작합니다. - 5행
count: 원자적 정수로, 0이면 실행 가능 상태이고 1 이상이면 비활성 상태입니다.tasklet_disable()로 증가,tasklet_enable()로 감소하며, 중첩 호출을 지원합니다. - 6행
use_callback: 5.9 커널에서 도입된 플래그로,true이면 새 API(callback),false이면 레거시 API(func)를 사용합니다. - 21-23행
tasklet_trylock(): RUN 비트를 원자적으로 set하고 이전 값을 반환합니다. 이미 set이면 false(잠금 실패)를 반환하여, 같은 tasklet이 두 CPU에서 동시 실행되는 것을 방지합니다. - 25-29행
tasklet_unlock():smp_mb__before_atomic()로 메모리 배리어(Barrier)를 삽입한 후 RUN 비트를 clear합니다. 콜백 내 메모리 접근이 unlock 전에 완료됨을 보장합니다. - 35행
state = 0: 초기 상태에서 SCHED, RUN 비트 모두 clear입니다. 아직 스케줄되지 않았고 실행 중이 아닌 상태입니다. - 36행
atomic_set(&count, 0): count를 0으로 설정하여 tasklet을 활성 상태로 만듭니다.DECLARE_TASKLET()매크로도 동일하게 0으로 초기화합니다.
SCHED/RUN 비트 상태 머신
tasklet의 state 필드는 2비트로 4가지 상태를 표현하며, 각 상태 간 전이는 원자적 비트 연산에 의해 보호됩니다:
| SCHED | RUN | 상태 | 의미 |
|---|---|---|---|
| 0 | 0 | 유휴(Idle) | 큐에 없고 실행 중도 아님 |
| 1 | 0 | 대기(Pending) | Per-CPU 리스트에 있으나 아직 실행 전 |
| 1 | 1 | 실행(Running) | 콜백 실행 중 (SCHED는 실행 직전 clear) |
| 0 | 1 | 실행 중(Active) | SCHED clear 후 콜백 실행 중 |
tasklet_action() 실행 분석
tasklet_action()과 tasklet_hi_action()은 softirq 핸들러로 등록되어, softirq 처리 시점에 Per-CPU tasklet 리스트를 순회하며 각 tasklet의 콜백을 실행합니다. 실제 구현은 tasklet_action_common()에 통합되어 있습니다.
tasklet_action_common() 커널 소스
/* kernel/softirq.c — tasklet_action_common() 실제 구현 */
static void tasklet_action_common(struct softirq_action *a,
struct tasklet_head *tl_head,
unsigned int softirq_nr)
{
struct tasklet_struct *list;
/* [1단계] Per-CPU 리스트를 원자적으로 분리 (splice) */
local_irq_disable();
list = tl_head->head; /* 전체 리스트 가져오기 */
tl_head->head = NULL; /* 원본 비우기 */
tl_head->tail = &tl_head->head; /* tail 초기화 */
local_irq_enable();
/* [2단계] 분리된 리스트 순회 */
while (list) {
struct tasklet_struct *t = list;
list = list->next;
/* [3단계] RUN 비트로 직렬화 검사 */
if (tasklet_trylock(t)) {
/* [4단계] count로 disable 검사 */
if (!atomic_read(&t->count)) {
/* SCHED 비트 해제: 재스케줄 가능 상태로 */
clear_bit(TASKLET_STATE_SCHED, &t->state);
/* [5단계] 콜백 실행 */
if (t->use_callback)
t->callback(t); /* 신 API */
else
t->func(t->data); /* 레거시 API */
tasklet_unlock(t); /* RUN 비트 해제 */
continue;
}
tasklet_unlock(t); /* disable됨 → unlock */
}
/* [6단계] 실행 불가 → 재스케줄 */
local_irq_disable();
t->next = NULL;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(softirq_nr); /* 다음 softirq 라운드 예약 */
local_irq_enable();
}
}
/* softirq 핸들러 등록 */
static void tasklet_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_vec),
TASKLET_SOFTIRQ);
}
static void tasklet_hi_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_hi_vec),
HI_SOFTIRQ);
}
코드 설명
- 9-13행리스트 분리(Splice): IRQ를 비활성화한 상태에서 현재 Per-CPU 리스트의 head를 가져오고, 원본을 빈 상태로 초기화합니다. 이 과정에서 새로 스케줄되는 tasklet은 새 리스트에 추가되므로 간섭이 없습니다.
- 16-18행분리된 리스트를 순회합니다.
list->next를 미리 저장하여, 현재 tasklet 처리 중 리스트가 변경되어도 안전합니다. - 21행
tasklet_trylock():test_and_set_bit(RUN)으로 다른 CPU에서 이미 실행 중인지 확인합니다. SMP 환경에서 같은 tasklet의 동시 실행을 방지하는 핵심 메커니즘입니다. - 23행
atomic_read(&count): count가 0이 아니면(disable 상태) 실행을 건너뜁니다.tasklet_disable()로 임시 중지된 tasklet은 여기서 걸러집니다. - 25행
clear_bit(SCHED): 콜백 실행 전에 SCHED 비트를 해제합니다. 이후 콜백 내부나 외부에서tasklet_schedule()을 다시 호출할 수 있습니다. - 28-31행
use_callback플래그로 신/구 API를 구분합니다. 신 API는tasklet_struct포인터를 받고, 레거시는unsigned long data를 받습니다. - 39-44행재스케줄: trylock 실패(다른 CPU에서 실행 중) 또는 disable 상태이면, tasklet을 현재 CPU의 리스트에 다시 추가하고 softirq를 재트리거합니다. 이로 인해 다음 softirq 라운드에서 재시도됩니다.
- 48-56행
tasklet_action()과tasklet_hi_action()은 각각tasklet_vec/tasklet_hi_vec를 인자로tasklet_action_common()을 호출하는 래퍼(Wrapper)입니다.
리스트 분리(Splice) 기법의 이유
왜 리스트를 분리하는가? IRQ 비활성화 구간을 최소화하기 위함입니다. head를 가져오고 원본을 NULL로 초기화하는 것은 O(1) 연산이며, 이후의 순회와 콜백 실행은 IRQ 활성화 상태에서 수행됩니다. 만약 분리하지 않으면, 전체 순회 동안 IRQ를 비활성화해야 하므로 인터럽트 지연 시간(Latency)이 크게 증가합니다.
재스케줄 시나리오
tasklet이 즉시 실행되지 못하고 재스케줄되는 두 가지 경우:
| 시나리오 | 원인 | 재스케줄 후 동작 |
|---|---|---|
tasklet_trylock() 실패 |
다른 CPU에서 같은 tasklet 실행 중 (RUN=1) | 현재 CPU의 리스트에 다시 추가, 다음 softirq 라운드에서 재시도 |
count > 0 |
tasklet_disable()로 비활성화됨 |
동일하게 재스케줄. tasklet_enable() 후 다음 라운드에서 실행 |
tasklet_disable() / tasklet_enable() / tasklet_kill() 분석
tasklet의 활성/비활성 제어와 완전한 제거를 위한 API를 커널 소스 수준에서 분석합니다.
tasklet_disable() 구현
/* include/linux/interrupt.h — 동기식 비활성화 */
static inline void tasklet_disable(struct tasklet_struct *t)
{
tasklet_disable_nosync(t); /* count 원자적 증가 */
tasklet_unlock_wait(t); /* RUN 비트 해제 대기 */
smp_mb(); /* 전체 메모리 배리어 */
}
/* include/linux/interrupt.h — 비동기식 비활성화 */
static inline void tasklet_disable_nosync(struct tasklet_struct *t)
{
atomic_inc(&t->count); /* count++: 0→1이면 비활성화 시작 */
smp_mb__after_atomic(); /* inc 이후 메모리 배리어 */
}
/* include/linux/interrupt.h — 활성화 */
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic(); /* dec 이전 메모리 배리어 */
atomic_dec(&t->count); /* count--: 1→0이면 활성화 복귀 */
}
/* include/linux/interrupt.h — RUN 비트 대기 (SMP) */
static inline void tasklet_unlock_wait(struct tasklet_struct *t)
{
while (test_bit(TASKLET_STATE_RUN, &t->state))
cpu_relax(); /* busy-wait (spin) */
}
코드 설명
- 2-7행
tasklet_disable()는 동기식입니다. count를 증가시킨 후, 현재 실행 중인 콜백이 완료될 때까지 대기합니다. 반환 후에는 tasklet이 확실히 실행 중이 아닙니다. - 10-14행
tasklet_disable_nosync()는 비동기식입니다. count만 증가시키고 즉시 반환합니다. 콜백이 현재 실행 중일 수 있으므로, 공유 데이터 접근에 주의가 필요합니다. - 6행
smp_mb(): 전체 메모리 배리어로, disable 이후의 메모리 접근이 콜백 완료 후에 수행됨을 보장합니다. - 12행
atomic_inc(&count): 원자적 증가입니다. 여러 곳에서 disable을 호출하면 count가 누적되며, 같은 횟수만큼 enable해야 활성화됩니다. - 17-21행
tasklet_enable(): count를 원자적으로 감소시킵니다. count가 0이 되면 다음 softirq 라운드에서 tasklet이 실행될 수 있습니다. - 24-28행
tasklet_unlock_wait(): RUN 비트가 해제될 때까지 busy-wait합니다. SMP 환경에서 다른 CPU에서 실행 중인 콜백이 완료될 때까지 스핀(Spin)합니다. UP(단일 프로세서)에서는 no-op입니다.
tasklet_kill() 구현
/* kernel/softirq.c — tasklet 완전 제거 */
void tasklet_kill(struct tasklet_struct *t)
{
if (in_interrupt())
pr_notice("Attempt to kill tasklet from interrupt\n");
/* SCHED 비트가 해제될 때까지 반복 대기 */
while (test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
wait_var_event(&t->state,
!test_bit(TASKLET_STATE_SCHED, &t->state));
tasklet_unlock_wait(t); /* RUN 비트 해제 대기 */
clear_bit(TASKLET_STATE_SCHED, &t->state);
}
코드 설명
- 4-5행인터럽트 컨텍스트에서
tasklet_kill()을 호출하면 경고를 출력합니다. 이 함수는 프로세스 컨텍스트에서만 호출해야 합니다 (대기할 수 있으므로). - 8-10행
test_and_set_bit(SCHED)로 SCHED 비트를 잠급니다. 이미 set이면 (tasklet이 스케줄되어 실행 대기 중),wait_var_event()로 해제될 때까지 대기합니다. 이 루프는 tasklet이 실행 완료되어 SCHED가 clear될 때까지 반복합니다. - 12행SCHED 비트를 성공적으로 잠근 후, RUN 비트도 대기합니다. 콜백이 완전히 종료되어야 합니다.
- 13행마지막으로 SCHED 비트를 clear합니다. 이 시점에서 tasklet은 어떤 CPU의 리스트에도 없고 실행 중도 아닌 안전한 상태입니다. 이후
tasklet_struct가 포함된 메모리를 해제할 수 있습니다.
주의: tasklet_kill() 후 tasklet을 다시 스케줄하면 안 됩니다. SCHED 비트가 set된 상태에서 clear하므로, 다른 코드 경로에서 동시에 tasklet_schedule()을 호출하면 경쟁 상태(Race Condition)가 발생할 수 있습니다. 드라이버 remove() 함수에서 tasklet_kill()을 호출한 후에는 해당 tasklet을 더 이상 사용하지 않아야 합니다.
disable/enable 중첩 패턴
/* 중첩 disable/enable 예제 */
struct my_device {
struct tasklet_struct tasklet;
spinlock_t lock;
int shared_data;
};
/* 경로 A: 설정 변경 시 */
void update_config(struct my_device *dev)
{
tasklet_disable(&dev->tasklet); /* count: 0→1 (동기 대기) */
spin_lock(&dev->lock);
dev->shared_data = 42; /* tasklet과 공유하는 데이터 수정 */
spin_unlock(&dev->lock);
tasklet_enable(&dev->tasklet); /* count: 1→0 (활성화) */
}
/* 경로 B: suspend 시 */
int my_suspend(struct device *dev)
{
struct my_device *priv = dev_get_drvdata(dev);
tasklet_disable(&priv->tasklet); /* count: 0→1 또는 1→2 */
/* tasklet 확실히 중지, suspend 안전 */
return 0;
}
int my_resume(struct device *dev)
{
struct my_device *priv = dev_get_drvdata(dev);
tasklet_enable(&priv->tasklet); /* count: 2→1 또는 1→0 */
return 0;
}
코드 설명
- 11행
tasklet_disable(): count를 증가시키고 현재 실행 중인 콜백 완료를 대기합니다. 반환 후 공유 데이터를 안전하게 수정할 수 있습니다. - 15행
tasklet_enable(): count를 감소시킵니다. 0이 되면 다음 softirq 라운드에서 스케줄된 tasklet이 실행됩니다. - 22행suspend 경로에서 disable하면, 이미 경로 A에서 disable된 상태라도 count가 중첩 증가합니다 (1→2). 각 disable에 대응하는 enable이 필요합니다.
- 29행resume에서 enable하면 count가 감소합니다. 경로 A의 disable이 아직 해제되지 않았다면 count는 1로 남아 tasklet은 여전히 비활성 상태입니다.
tasklet vs workqueue vs threaded IRQ: 코드 패턴 비교와 마이그레이션
Tasklet이 deprecated된 현재, 대안인 workqueue와 threaded IRQ로의 마이그레이션(Migration) 코드 패턴을 비교합니다. 실제 커널 드라이버에서 사용되는 패턴을 기반으로 합니다.
초기화 패턴 비교
/* ========== 1. Tasklet (deprecated) ========== */
struct my_dev_tasklet {
struct tasklet_struct tasklet;
int irq;
void __iomem *regs;
};
static void my_tasklet_handler(struct tasklet_struct *t)
{
struct my_dev_tasklet *dev = from_tasklet(dev, t, tasklet);
process_data(dev); /* 슬립 불가! */
}
static irqreturn_t my_irq_handler(int irq, void *data)
{
struct my_dev_tasklet *dev = data;
ack_hw_irq(dev->regs); /* 하드웨어 IRQ 확인 */
tasklet_schedule(&dev->tasklet); /* bottom half 스케줄 */
return IRQ_HANDLED;
}
/* probe */
tasklet_setup(&dev->tasklet, my_tasklet_handler);
request_irq(dev->irq, my_irq_handler, 0, "my_dev", dev);
/* remove */
free_irq(dev->irq, dev);
tasklet_kill(&dev->tasklet);
/* ========== 2. Workqueue 마이그레이션 ========== */
struct my_dev_wq {
struct work_struct work; /* tasklet_struct → work_struct */
int irq;
void __iomem *regs;
};
static void my_work_handler(struct work_struct *w)
{
struct my_dev_wq *dev = container_of(w, struct my_dev_wq, work);
mutex_lock(&dev->mutex); /* 슬립 가능! */
process_data(dev);
mutex_unlock(&dev->mutex);
}
static irqreturn_t my_irq_handler_wq(int irq, void *data)
{
struct my_dev_wq *dev = data;
ack_hw_irq(dev->regs);
schedule_work(&dev->work); /* tasklet_schedule → schedule_work */
return IRQ_HANDLED;
}
/* probe */
INIT_WORK(&dev->work, my_work_handler); /* tasklet_setup → INIT_WORK */
request_irq(dev->irq, my_irq_handler_wq, 0, "my_dev", dev);
/* remove */
free_irq(dev->irq, dev);
cancel_work_sync(&dev->work); /* tasklet_kill → cancel_work_sync */
/* ========== 3. Threaded IRQ 마이그레이션 ========== */
struct my_dev_tirq {
/* tasklet_struct 필드 불필요! */
int irq;
void __iomem *regs;
};
/* Hard IRQ 핸들러 (Top Half) — 최소 작업만 */
static irqreturn_t my_hardirq(int irq, void *data)
{
struct my_dev_tirq *dev = data;
ack_hw_irq(dev->regs);
return IRQ_WAKE_THREAD; /* 커널이 thread_fn 깨움 */
}
/* Thread 핸들러 (Bottom Half) — 프로세스 컨텍스트 */
static irqreturn_t my_thread_fn(int irq, void *data)
{
struct my_dev_tirq *dev = data;
mutex_lock(&dev->mutex); /* 슬립 가능! */
process_data(dev);
mutex_unlock(&dev->mutex);
return IRQ_HANDLED;
}
/* probe — devm_ 접두사로 자동 해제 */
devm_request_threaded_irq(dev->dev, dev->irq,
my_hardirq, my_thread_fn,
IRQF_ONESHOT, "my_dev", dev); /* IRQ 한 번 처리 → 자동 re-enable */
/* remove — devm_이므로 free_irq() 불필요!
* tasklet_kill()도 불필요! */
코드 설명
- Tasklet 10행
from_tasklet():container_of()래퍼로,tasklet_struct포인터에서 디바이스 구조체를 역참조합니다. 5.9+ 새 API 패턴입니다. - Tasklet 11행tasklet 콜백은 softirq 컨텍스트에서 실행되므로 슬립이 불가능합니다. mutex, kmalloc(GFP_KERNEL), msleep 등을 사용할 수 없습니다.
- Workqueue 10행
container_of():from_tasklet()대신 직접container_of()를 사용합니다.work_struct포인터에서 디바이스 구조체를 역참조하는 동일한 패턴입니다. - Workqueue 11행workqueue 핸들러는 프로세스 컨텍스트에서 실행되므로 슬립이 가능합니다. mutex, 메모리 할당 등을 자유롭게 사용할 수 있습니다.
- Workqueue 20행
schedule_work():tasklet_schedule()대신 사용합니다. system_wq에 work를 큐잉하며, CMWQ가 자동으로 워커(Worker) 스레드를 관리합니다. - Workqueue 29행
cancel_work_sync():tasklet_kill()대신 사용합니다. 큐에 대기 중이거나 실행 중인 work가 완료될 때까지 대기합니다. - Threaded IRQ 13행
IRQ_WAKE_THREAD: Hard IRQ 핸들러가 이 값을 반환하면 커널이 자동으로 thread_fn을 깨웁니다. tasklet 스케줄링 코드가 불필요합니다. - Threaded IRQ 27-29행
devm_request_threaded_irq(): device-managed API로, 디바이스 해제 시 자동으로 IRQ를 해제합니다.IRQF_ONESHOT플래그는 thread_fn 완료 전까지 IRQ를 마스크(Mask)합니다. - Threaded IRQ 31행devm_ API를 사용하면
free_irq()와tasklet_kill()모두 불필요합니다. 코드가 대폭 간결해지는 핵심 장점입니다.
API 대응표
| 기능 | Tasklet (deprecated) | Workqueue | Threaded IRQ |
|---|---|---|---|
| 구조체 | tasklet_struct | work_struct | 없음 (커널 관리) |
| 초기화 | tasklet_setup() | INIT_WORK() | request_threaded_irq() |
| 스케줄 | tasklet_schedule() | schedule_work() | return IRQ_WAKE_THREAD |
| 비활성화 | tasklet_disable() | disable_work() | disable_irq() |
| 제거 | tasklet_kill() | cancel_work_sync() | free_irq() / devm 자동 |
| 역참조 | from_tasklet() | container_of() | dev_id 인자 직접 전달 |
| 실행 컨텍스트 | softirq (슬립 불가) | 프로세스 (슬립 가능) | 프로세스 (슬립 가능) |
| 직렬화 | 자동 (동일 tasklet) | 수동 (lock 필요) | 자동 (IRQF_ONESHOT) |
| PREEMPT_RT | 강제 스레드화 | 정상 동작 | 정상 동작 |
마이그레이션 체크리스트
tasklet → workqueue 마이그레이션 단계:
struct tasklet_struct→struct work_struct로 교체tasklet_setup()→INIT_WORK()로 교체from_tasklet()→container_of()로 교체tasklet_schedule()→schedule_work()로 교체tasklet_kill()→cancel_work_sync()로 교체- 콜백에서 슬립 불가 제약 해소: 필요시 mutex/GFP_KERNEL 사용 가능
- workqueue는 같은 work를 다른 CPU에서 동시 실행하지 않지만, 다른 work 간에는 동시 실행 가능하므로 공유 데이터 보호 확인
tasklet → threaded IRQ 마이그레이션 단계:
struct tasklet_struct필드 제거- Hard IRQ 핸들러에서
tasklet_schedule()→return IRQ_WAKE_THREAD - tasklet 콜백 → thread_fn으로 이동 (반환값
IRQ_HANDLED추가) request_irq()→devm_request_threaded_irq()로 교체free_irq(),tasklet_kill()제거 (devm 자동 관리)IRQF_ONESHOT플래그 추가: thread_fn 완료 전까지 IRQ 마스크
softirq_init(): 부팅 시 Tasklet 핸들러 등록
커널 부팅 과정에서 tasklet 실행 기반이 되는 softirq 핸들러가 등록됩니다. softirq_init()은 start_kernel() → softirq_init() 경로로 호출되며, TASKLET_SOFTIRQ와 HI_SOFTIRQ에 각각의 핸들러를 연결합니다.
/* kernel/softirq.c — softirq_init() */
void __init softirq_init(void)
{
int cpu;
/* 모든 CPU의 Per-CPU tasklet 리스트 초기화 */
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
/* HI_SOFTIRQ (인덱스 0) 핸들러 등록 */
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
/* TASKLET_SOFTIRQ (인덱스 6) 핸들러 등록 */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
}
/*
* 호출 체인:
*
* start_kernel() [init/main.c]
* └─ softirq_init() [kernel/softirq.c]
* ├─ Per-CPU tasklet_vec/tasklet_hi_vec 초기화
* ├─ open_softirq(HI_SOFTIRQ, tasklet_hi_action)
* └─ open_softirq(TASKLET_SOFTIRQ, tasklet_action)
*
* open_softirq()는 softirq_vec[nr].action = action 한 줄:
*/
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
/*
* softirq_vec는 NR_SOFTIRQS(10)개 엔트리 배열:
*
* softirq_vec[0].action = tasklet_hi_action ← HI_SOFTIRQ
* softirq_vec[1].action = run_timer_softirq ← TIMER
* softirq_vec[2].action = net_tx_action ← NET_TX
* ...
* softirq_vec[6].action = tasklet_action ← TASKLET_SOFTIRQ
* ...
* softirq_vec[9].action = rcu_core_si ← RCU
*
* __do_softirq()가 pending 비트맵을 순회하며
* softirq_vec[i].action()을 호출하는 구조
*/
코드 설명
- 7-12행모든 가능한 CPU에 대해
tasklet_vec과tasklet_hi_vec의 tail 포인터를 head 주소로 초기화합니다. 빈 리스트 상태에서 tail이 head를 가리키면,*tail = t로 첫 노드를 삽입할 때 자연스럽게head = t가 됩니다. - 15행
open_softirq(HI_SOFTIRQ, tasklet_hi_action): 인덱스 0번 softirq에 고우선순위 tasklet 핸들러를 등록합니다.__do_softirq()에서 가장 먼저 실행됩니다. - 18행
open_softirq(TASKLET_SOFTIRQ, tasklet_action): 인덱스 6번 softirq에 일반 tasklet 핸들러를 등록합니다. 네트워크, 블록 I/O 등 다른 softirq보다 낮은 우선순위입니다. - 33행
open_softirq()의 구현은 단순히softirq_vec배열의 action 포인터를 설정하는 한 줄입니다. 락이 필요 없는 이유는 부팅 초기 단일 CPU 환경에서 호출되기 때문입니다.
CPU Hotplug와 Tasklet 마이그레이션
CPU가 오프라인(Offline)될 때, 해당 CPU의 Per-CPU tasklet 리스트에 남아있는 tasklet은 다른 CPU로 마이그레이션되어야 합니다.
커널은 takeover_tasklets() 함수를 통해 이를 처리합니다. CPU hotplug 이벤트에 등록된 콜백으로, CPU가 DEAD 상태가 된 후 호출됩니다.
/* kernel/softirq.c — CPU 오프라인 시 tasklet 인계 */
static int takeover_tasklets(unsigned int cpu)
{
/* 오프라인 CPU의 일반 tasklet 리스트를 현재 CPU로 이전 */
local_irq_disable();
/* tasklet_vec: 일반 우선순위 tasklet 이전 */
if (&per_cpu(tasklet_vec, cpu).head) {
*__this_cpu_read(tasklet_vec.tail) =
per_cpu(tasklet_vec, cpu).head;
__this_cpu_write(tasklet_vec.tail,
per_cpu(tasklet_vec, cpu).tail);
per_cpu(tasklet_vec, cpu).head = NULL;
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
}
/* tasklet_hi_vec: 고우선순위 tasklet 이전 */
if (&per_cpu(tasklet_hi_vec, cpu).head) {
*__this_cpu_read(tasklet_hi_vec.tail) =
per_cpu(tasklet_hi_vec, cpu).head;
__this_cpu_write(tasklet_hi_vec.tail,
per_cpu(tasklet_hi_vec, cpu).tail);
per_cpu(tasklet_hi_vec, cpu).head = NULL;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
raise_softirq_irqoff(TASKLET_SOFTIRQ);
raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_enable();
return 0;
}
/*
* CPU Hotplug 등록 (커널 초기화 시):
*
* cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD,
* "softirq:dead", NULL, takeover_tasklets);
*
* CPUHP_SOFTIRQ_DEAD 단계에서 호출되며,
* 이 시점에서 오프라인 CPU는 더 이상 인터럽트를 처리하지 않으므로
* 해당 CPU의 tasklet 리스트에 새 tasklet이 추가될 수 없습니다.
*
* 동작 요약:
* 1. 오프라인 CPU의 tasklet_vec 리스트 전체를 현재 CPU에 병합
* 2. 오프라인 CPU의 tasklet_hi_vec 리스트 전체를 현재 CPU에 병합
* 3. 오프라인 CPU의 리스트를 빈 상태로 초기화
* 4. softirq 트리거 → 현재 CPU에서 이전된 tasklet 실행
*/
코드 설명
- 5행
local_irq_disable(): Per-CPU 리스트를 조작하므로 인터럽트를 비활성화합니다. 이 구간에서 현재 CPU의 리스트가 동시에 수정되는 것을 방지합니다. - 9-10행현재 CPU의
tasklet_vectail이 가리키는 위치에 오프라인 CPU의 head를 연결합니다. 이것은 두 단일 연결 리스트를 O(1)로 병합하는 기법입니다. - 11-12행현재 CPU의 tail 포인터를 오프라인 CPU의 tail 값으로 갱신합니다. 이렇게 하면 오프라인 CPU의 마지막 노드가 현재 CPU 리스트의 새 끝이 됩니다.
- 13-15행오프라인 CPU의 리스트를 빈 상태로 초기화합니다.
head = NULL,tail = &head로 설정하여 다시 온라인될 때 깨끗한 상태에서 시작합니다. - 29-30행이전된 tasklet을 실행하기 위해 두 softirq를 모두 트리거합니다. 다음 softirq 처리 시점에서 이전된 tasklet들이 현재 CPU에서 실행됩니다.
CPU Hotplug 주의사항: tasklet은 스케줄한 CPU에서 실행되는 것이 기본이지만, CPU 오프라인 시 다른 CPU로 마이그레이션됩니다. 드라이버에서 tasklet 콜백이 특정 CPU에서만 실행된다고 가정하면 안 됩니다. CPU hotplug가 가능한 환경에서는 Per-CPU 데이터 접근 시 적절한 동기화가 필요합니다.
Tasklet 내부 메모리 배리어 분석
Tasklet의 정확한 동작은 메모리 배리어(Memory Barrier)에 의존합니다. SMP 환경에서 CPU 간 메모리 가시성(Visibility)을 보장하기 위해 여러 지점에 배리어가 삽입되어 있습니다.
/*
* Tasklet 내부 메모리 배리어 지점 분석
*
* 1. tasklet_trylock() → test_and_set_bit()
* - test_and_set_bit()는 암묵적 acquire 의미를 가짐
* - 이전 CPU의 콜백 내 쓰기가 현재 CPU에서 가시적
* - 콜백 진입 전 모든 선행 쓰기를 관찰 가능
*
* 2. tasklet_unlock() → smp_mb__before_atomic() + clear_bit()
* - 콜백 내 모든 쓰기가 clear_bit() 이전에 완료됨을 보장
* - 다른 CPU의 trylock이 RUN=0을 볼 때 콜백 결과도 가시적
* - release 의미 (store-store 배리어)
*
* 3. tasklet_disable() → smp_mb()
* - count 증가 + unlock_wait 후 전체 메모리 배리어
* - disable 이후 코드에서 콜백의 모든 효과를 관찰 가능
* - 가장 강력한 배리어 (full fence)
*
* 4. tasklet_disable_nosync() → smp_mb__after_atomic()
* - count 증가 후 배리어
* - count 증가가 다른 CPU에서 즉시 가시적
* - 다음 tasklet_action() 라운드에서 count > 0 확인 보장
*
* 5. tasklet_enable() → smp_mb__before_atomic()
* - count 감소 전 배리어
* - enable 이전의 데이터 수정이 콜백에서 가시적
* - 콜백이 최신 데이터를 읽을 수 있음을 보장
*
* 6. test_and_set_bit(SCHED) in tasklet_schedule()
* - 원자적 비트 연산으로 중복 스케줄 방지
* - tasklet_schedule() 이전의 데이터 쓰기가
* 콜백 실행 시점에서 가시적임을 보장
*/
| 배리어 위치 | 배리어 유형 | 보장 내용 | 필요한 이유 |
|---|---|---|---|
tasklet_trylock() |
test_and_set_bit() (acquire) |
이전 CPU의 쓰기가 콜백 진입 전 가시 | 콜백이 최신 데이터를 읽어야 함 |
tasklet_unlock() |
smp_mb__before_atomic() + clear_bit() (release) |
콜백 내 쓰기가 unlock 전 완료 | 다음 trylock이 콜백 결과를 관찰해야 함 |
tasklet_disable() |
smp_mb() (full fence) |
disable 후 콜백 효과 전부 가시 | 공유 데이터를 안전하게 수정하기 위함 |
tasklet_disable_nosync() |
smp_mb__after_atomic() |
count 증가가 다른 CPU에서 즉시 가시 | tasklet_action()이 count > 0을 확인해야 함 |
tasklet_enable() |
smp_mb__before_atomic() |
데이터 수정이 count 감소 전 완료 | 재활성화된 콜백이 최신 데이터를 읽어야 함 |
배리어 최적화: smp_mb__before_atomic()과 smp_mb__after_atomic()은 아키텍처에 따라 no-op이 될 수 있습니다. x86에서는 lock 접두사가 붙은 원자적 명령이 이미 전체 배리어 의미를 제공하므로 추가 배리어가 불필요합니다. ARM, RISC-V 등에서는 실제 배리어 명령(dmb, fence)이 삽입됩니다.
Tasklet에서 NAPI로: 네트워크 서브시스템의 전환
네트워크 서브시스템은 tasklet의 한계를 가장 먼저 체감한 영역입니다. 초기 네트워크 드라이버는 패킷(Packet) 수신 시 Hard IRQ → tasklet_schedule()으로 Bottom Half 처리를 위임했지만, 고속 네트워크(1Gbps+)에서 인터럽트 오버헤드가 심각한 병목이 되었습니다. 이를 해결하기 위해 NAPI(New API)가 도입되었습니다.
| 특성 | Tasklet 기반 수신 | NAPI 기반 수신 |
|---|---|---|
| 인터럽트 모델 | 패킷마다 IRQ → tasklet | IRQ → 폴링(Polling) 모드 전환 |
| 인터럽트 빈도 | 패킷 수에 비례 (높음) | IRQ 1회 → 최대 budget개 일괄 처리 |
| 고속 네트워크 성능 | 인터럽트 폭풍으로 CPU 포화 | 적응적 폴링으로 CPU 효율적 사용 |
| CPU 간 분산 | 불가 (Per-CPU 고정) | RPS/RFS, aRFS로 자동 분산 |
| GRO 통합 | 불가 | NAPI 폴링 루프에서 GRO 자동 수행 |
| XDP 지원 | 불가 | NAPI 콜백에서 XDP 프로그램 직접 실행 |
| Busy Polling | 불가 | SO_BUSY_POLL 소켓 옵션으로 지원 |
| 버짓(Budget) 제어 | 없음 (전체 큐 처리) | napi_poll(budget)으로 제한 |
| 적응적 IRQ 조절 | 불가 | Adaptive Interrupt Coalescing 지원 |
| 대표 드라이버 | 초기 8139too, ne2k | 모든 현대 NIC (e1000e, ixgbe, mlx5) |
/* ====== Before: Tasklet 기반 네트워크 수신 (레거시) ====== */
static irqreturn_t legacy_nic_irq(int irq, void *dev_id)
{
struct legacy_nic *nic = dev_id;
iowrite32(IRQ_ACK, nic->regs);
tasklet_schedule(&nic->rx_tasklet); /* 매 패킷마다 호출 */
return IRQ_HANDLED;
}
static void legacy_rx_tasklet(struct tasklet_struct *t)
{
struct legacy_nic *nic = from_tasklet(nic, t, rx_tasklet);
while (rx_ring_has_data(nic)) {
struct sk_buff *skb = receive_packet(nic);
netif_rx(skb); /* 패킷을 네트워크 스택으로 전달 */
}
iowrite32(IRQ_ENABLE, nic->regs);
}
/* 문제점:
* - 10Gbps에서 초당 ~15M 패킷 → ~15M 인터럽트
* - 인터럽트 처리 오버헤드만으로 CPU 100%
* - tasklet 직렬화로 멀티큐(Multi-queue) NIC 활용 불가
*/
/* ====== After: NAPI 기반 네트워크 수신 (현대) ====== */
static irqreturn_t modern_nic_irq(int irq, void *dev_id)
{
struct modern_nic *nic = dev_id;
iowrite32(IRQ_DISABLE, nic->regs); /* IRQ 비활성화 */
napi_schedule(&nic->napi); /* 폴링 모드 진입 */
return IRQ_HANDLED;
}
static int modern_napi_poll(struct napi_struct *napi, int budget)
{
struct modern_nic *nic = container_of(napi, struct modern_nic, napi);
int work_done = 0;
while (work_done < budget && rx_ring_has_data(nic)) {
struct sk_buff *skb = receive_packet(nic);
napi_gro_receive(napi, skb); /* GRO 자동 적용 */
work_done++;
}
if (work_done < budget) {
napi_complete_done(napi, work_done); /* 폴링 종료 */
iowrite32(IRQ_ENABLE, nic->regs); /* IRQ 재활성화 */
}
return work_done;
}
/* 장점:
* - 인터럽트 1회 → budget(기본 64)개 패킷 일괄 처리
* - 10Gbps: ~15M 인터럽트 → ~234K 폴링 사이클 (64x 감소)
* - GRO로 패킷 병합 → 네트워크 스택 처리량 향상
* - 멀티큐 NIC: 큐별 NAPI → CPU별 병렬 처리
*/
NAPI와 Tasklet의 관계: NAPI 내부적으로는 NAPI_STATE_SCHED 비트를 사용하는데, 이 설계는 tasklet의 TASKLET_STATE_SCHED 비트에서 직접 영향을 받았습니다. 그러나 NAPI는 softirq(NET_RX_SOFTIRQ) 위에서 폴링 루프를 운영하는 전혀 다른 실행 모델입니다. 상세 내용은 NAPI 페이지를 참고하세요.
흔한 실수와 버그 패턴
Tasklet을 사용할 때 발생하는 대표적인 버그 패턴과 올바른 대처 방법입니다.
1. Use-After-Free: tasklet_kill() 없이 해제
/* ✗ 잘못된 코드: use-after-free */
static void bad_remove(struct pci_dev *pdev)
{
struct my_device *dev = pci_get_drvdata(pdev);
free_irq(pdev->irq, dev);
/* tasklet_kill() 누락! */
kfree(dev); /* dev 해제 → tasklet_struct도 해제됨 */
/* 위험: 이미 스케줄된 tasklet이 해제된 메모리를 참조
* → softirq에서 t->callback(t) 호출 시 커널 크래시(Crash)
* → KASAN: "use-after-free in tasklet_action_common" */
}
/* ✓ 올바른 코드 */
static void good_remove(struct pci_dev *pdev)
{
struct my_device *dev = pci_get_drvdata(pdev);
free_irq(pdev->irq, dev); /* 1. 새 IRQ 발생 방지 */
tasklet_kill(&dev->tasklet); /* 2. 스케줄된 tasklet 완료 대기 */
kfree(dev); /* 3. 이제 안전하게 해제 */
}
2. Tasklet 콜백에서 슬립
/* ✗ 잘못된 코드: softirq 컨텍스트에서 슬립 */
static void bad_tasklet_cb(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, tasklet);
mutex_lock(&dev->mutex); /* BUG: mutex는 슬립 가능 → 데드락!
* "BUG: scheduling while atomic"
* "in_atomic(): 1, irqs_disabled(): 0" */
dev->buf = kmalloc(4096, GFP_KERNEL); /* BUG: GFP_KERNEL도 슬립 가능 */
msleep(10); /* BUG: 명시적 슬립 */
mutex_unlock(&dev->mutex);
}
/* ✓ 올바른 코드 */
static void good_tasklet_cb(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, tasklet);
spin_lock(&dev->lock); /* ✓ spin_lock은 softirq에서 안전 */
dev->buf = kmalloc(4096, GFP_ATOMIC); /* ✓ GFP_ATOMIC 사용 */
spin_unlock(&dev->lock);
}
/* 또는 workqueue로 전환하여 슬립 제약을 해소 */
3. 데드락: tasklet_kill()을 인터럽트 컨텍스트에서 호출
/* ✗ 잘못된 코드: IRQ 핸들러에서 tasklet_kill() */
static irqreturn_t bad_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
if (dev->error_flag) {
tasklet_kill(&dev->tasklet); /* BUG: tasklet_kill()은 슬립!
* wait_var_event() 내부에서 schedule()
* → "scheduling while atomic" */
}
return IRQ_HANDLED;
}
/* ✓ 올바른 코드: 프로세스 컨텍스트로 위임 */
static irqreturn_t good_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
if (dev->error_flag) {
tasklet_disable_nosync(&dev->tasklet); /* ✓ 비동기 비활성화 */
schedule_work(&dev->error_work); /* ✓ workqueue에서 정리 */
}
return IRQ_HANDLED;
}
static void error_work_handler(struct work_struct *w)
{
struct my_device *dev = container_of(w, struct my_device, error_work);
tasklet_kill(&dev->tasklet); /* ✓ 프로세스 컨텍스트에서 호출 */
cleanup_device(dev);
}
4. 무한 대기: tasklet_kill() 전에 소스를 비활성화하지 않음
/* ✗ 잘못된 코드: tasklet_kill()이 영원히 반환하지 않음 */
static void bad_cleanup(struct my_device *dev)
{
tasklet_kill(&dev->tasklet); /* 데드락!
* tasklet_kill()이 SCHED 비트 클리어를 기다림
* → tasklet 실행됨 → SCHED 클리어됨
* → 그런데 IRQ 핸들러가 다시 tasklet_schedule() 호출
* → SCHED 다시 set됨 → tasklet_kill() 재진입
* → IRQ가 계속 발생 → 영원히 반복... */
free_irq(dev->irq, dev); /* 여기까지 도달 못함 */
}
/* ✓ 올바른 순서: 소스를 먼저 비활성화 */
static void good_cleanup(struct my_device *dev)
{
free_irq(dev->irq, dev); /* 1. IRQ 핸들러 제거 → 새 schedule 불가 */
tasklet_kill(&dev->tasklet); /* 2. 이미 스케줄된 것만 대기 → 정상 반환 */
}
5. disable/enable 불일치
/* ✗ 잘못된 코드: disable 2회, enable 1회 */
tasklet_disable(&dev->tasklet); /* count: 0 → 1 */
tasklet_disable(&dev->tasklet); /* count: 1 → 2 */
/* ... */
tasklet_enable(&dev->tasklet); /* count: 2 → 1 (여전히 비활성!) */
/* tasklet이 영원히 실행되지 않음 → 드라이버 기능 상실 */
/* ✓ 올바른 코드: disable/enable 횟수 일치 */
tasklet_disable(&dev->tasklet); /* count: 0 → 1 */
tasklet_disable(&dev->tasklet); /* count: 1 → 2 */
/* ... */
tasklet_enable(&dev->tasklet); /* count: 2 → 1 */
tasklet_enable(&dev->tasklet); /* count: 1 → 0 (활성!) */
6. Tasklet 콜백 내에서 자기 자신을 kill
/* ✗ 잘못된 코드: 콜백 내에서 tasklet_kill() */
static void bad_self_kill(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, tasklet);
if (dev->should_stop) {
tasklet_kill(&dev->tasklet); /* BUG: 데드락!
* tasklet_kill()이 RUN 비트 클리어를 대기
* → 하지만 현재 콜백이 실행 중이므로 RUN=1
* → 자기 자신이 완료되기를 무한 대기 */
}
}
/* ✓ 올바른 코드: 단순히 재스케줄하지 않으면 됨 */
static void good_self_stop(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, tasklet);
if (dev->should_stop)
return; /* ✓ 재스케줄 안 하면 자연스럽게 중지
* SCHED, RUN 모두 clear → Idle 상태로 돌아감 */
/* 정상 처리 ... */
tasklet_schedule(t); /* 필요 시 재스케줄 */
}
버그 패턴 요약:
- Use-After-Free:
tasklet_kill()없이kfree()→ 스케줄된 tasklet이 해제된 메모리 참조 - 슬립 in softirq: 콜백에서 mutex, GFP_KERNEL, msleep 사용 → "scheduling while atomic"
- 데드락: IRQ 컨텍스트에서
tasklet_kill()→ 프로세스 컨텍스트에서만 호출 - 무한 대기:
free_irq()전에tasklet_kill()→ 소스부터 비활성화 - 카운트 불일치: disable N번, enable M번 (N ≠ M) → tasklet 영구 비활성화 또는 예기치 않은 활성화
- 자기 kill: 콜백 내에서
tasklet_kill()→ RUN 비트 대기 데드락
Tasklet 성능 특성과 측정
Tasklet의 성능 특성을 정량적으로 이해하면 대안 메커니즘과의 비교에 도움이 됩니다.
지연시간(Latency) 비교
| 메커니즘 | 스케줄 → 콜백 시작 | 오버헤드 요인 | 측정 환경 |
|---|---|---|---|
| Tasklet | ~1-5 μs | softirq 디스패치, Per-CPU 리스트 순회 | Hard IRQ에서 schedule 시 |
| Workqueue | ~5-30 μs | kworker 스레드 웨이크업, 스케줄러 오버헤드 | system_wq, schedule_work() |
| Threaded IRQ | ~3-15 μs | IRQ 스레드 웨이크업, IRQF_ONESHOT 처리 | request_threaded_irq() |
| Softirq (직접) | ~0.5-2 μs | irq_exit()에서 직접 호출 (리스트 순회 없음) | NET_RX_SOFTIRQ 등 |
측정 주의: 위 수치는 일반적인 x86_64 서버 환경에서의 대략적인 범위입니다. 실제 값은 CPU 아키텍처, 클럭 속도, 캐시 상태, 시스템 부하, 커널 설정(PREEMPT 레벨)에 따라 크게 달라집니다. 정확한 측정에는 ftrace나 bpftrace를 사용하세요.
오버헤드 분석
/*
* Tasklet 오버헤드 구성 요소:
*
* 1. 스케줄링 오버헤드 (tasklet_schedule)
* - test_and_set_bit(SCHED): ~10-20 cycles (원자적 RMW)
* - local_irq_save/restore: ~20-40 cycles
* - 리스트 삽입: ~5-10 cycles
* - raise_softirq_irqoff: ~10-20 cycles (비트 OR)
* 합계: ~45-90 cycles (~15-30ns @ 3GHz)
*
* 2. 실행 오버헤드 (tasklet_action_common)
* - local_irq_disable + 리스트 분리: ~20-30 cycles
* - tasklet_trylock (test_and_set): ~10-20 cycles
* - atomic_read(&count): ~5-10 cycles
* - clear_bit(SCHED): ~10-20 cycles
* - 콜백 호출 (indirect call): ~5-15 cycles
* - tasklet_unlock: ~10-20 cycles
* 합계: ~60-115 cycles (~20-38ns @ 3GHz)
*
* 총 오버헤드: ~105-205 cycles (~35-68ns @ 3GHz)
* (콜백 실행 시간 제외)
*
* 비교:
* - workqueue: ~300-1000 cycles (스레드 웨이크업 포함)
* - threaded IRQ: ~200-500 cycles
* - softirq 직접: ~30-50 cycles (리스트 순회 없음)
*
* Tasklet의 낮은 오버헤드는 softirq 위에서 직접
* 실행되기 때문이며, 이것이 지연 민감 드라이버에서
* 여전히 사용되는 주요 이유입니다.
*/
성능 측정 방법
# 1. ftrace로 tasklet 스케줄→실행 지연 측정
echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/tasklet_exit/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_raise/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -50
# softirq_raise 타임스탬프와 tasklet_entry 타임스탬프 차이 = 스케줄 지연
# 2. bpftrace로 tasklet 콜백 실행 시간 분포
bpftrace -e '
tracepoint:irq:tasklet_entry {
@start[tid] = nsecs;
}
tracepoint:irq:tasklet_exit /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
interval:s:10 { exit(); }'
# 3. perf로 tasklet 관련 함수 프로파일링
perf record -g -e irq:tasklet_entry -e irq:tasklet_exit -- sleep 10
perf report --sort=comm,symbol --stdio
# 4. cyclictest와 병행하여 RT 환경 영향 측정
# tasklet 활성 상태에서의 최대 지연시간 확인
cyclictest -p 99 -t 4 -m -D 60
# 5. /proc/softirqs 기반 처리량 모니터링
# 1초 간격으로 TASKLET 증가량 계산
awk '
BEGIN { split("", prev) }
/TASKLET/ {
for(i=2; i<=NF; i++) {
if(prev[i]) printf "CPU%d: %d/s ", i-2, $i-prev[i];
prev[i]=$i;
}
printf "\n";
}' <(while true; do cat /proc/softirqs; sleep 1; done)
성능 판단 기준: tasklet의 ~1-5μs 지연은 대부분의 드라이버에서 workqueue의 ~5-30μs와 실질적으로 동일합니다. 지연시간 차이가 유의미한 경우는 오디오(period < 1ms), 고속 네트워크(line-rate 처리), 실시간 제어(PREEMPT_RT) 등 극히 제한적입니다. 대부분의 새 코드에서는 workqueue나 threaded IRQ의 추가 지연을 감수하고 유연성을 확보하는 것이 더 나은 선택입니다.
WQ_BH: Workqueue 기반 Tasklet 대체 (커널 6.9+)
커널 6.9에서 도입된 WQ_BH 플래그는 tasklet의 deprecation을 가속하는 핵심 기능입니다. 기존에 tasklet이 담당하던 "softirq 컨텍스트에서의 빠른 Bottom Half 실행"을 workqueue API로 대체할 수 있게 합니다.
/* kernel/workqueue.c — WQ_BH 워크큐 (커널 6.9+) */
/*
* WQ_BH 핵심 개념:
* - BH(Bottom Half) 컨텍스트에서 실행되는 workqueue
* - softirq 핸들러 내에서 work를 디스패치
* - tasklet과 동일한 실행 컨텍스트 (슬립 불가)
* - 하지만 workqueue의 모든 API 사용 가능:
* cancel_work_sync(), flush_workqueue(), mod_delayed_work() 등
*/
/* 시스템 BH 워크큐 (커널 제공) */
struct workqueue_struct *system_bh_wq; /* 일반 우선순위 */
struct workqueue_struct *system_bh_highpri_wq; /* 고우선순위 */
/* 사용 예시: tasklet → WQ_BH 마이그레이션 */
/* Before: tasklet */
struct my_device {
struct tasklet_struct tasklet;
};
tasklet_setup(&dev->tasklet, my_handler);
tasklet_schedule(&dev->tasklet);
tasklet_kill(&dev->tasklet);
/* After: WQ_BH workqueue */
struct my_device {
struct work_struct bh_work;
};
INIT_WORK(&dev->bh_work, my_work_handler);
queue_work(system_bh_wq, &dev->bh_work); /* softirq 컨텍스트에서 실행됨 */
cancel_work_sync(&dev->bh_work);
/*
* WQ_BH vs 일반 workqueue vs tasklet 비교:
*
* WQ_BH workqueue:
* - 실행 컨텍스트: softirq (in_softirq() = true)
* - 슬립: 불가 (tasklet과 동일)
* - API: workqueue 전체 (cancel, flush, delayed 등)
* - 부하 분산: 가능 (CMWQ)
* - PREEMPT_RT: ksoftirqd에서 실행 (tasklet과 동일)
*
* 일반 workqueue:
* - 실행 컨텍스트: 프로세스 (kworker)
* - 슬립: 가능
* - API: workqueue 전체
* - 부하 분산: 가능 (CMWQ)
* - PREEMPT_RT: 정상 동작
*
* tasklet:
* - 실행 컨텍스트: softirq
* - 슬립: 불가
* - API: schedule/kill/disable/enable만
* - 부하 분산: 불가 (Per-CPU 고정)
* - PREEMPT_RT: 문제 있음
*
* → WQ_BH는 tasklet의 "softirq 컨텍스트에서 빠른 실행"을
* 유지하면서 workqueue API의 유연성을 제공하는 이상적인 대안
*/
| 특성 | Tasklet | WQ_BH Workqueue | 일반 Workqueue |
|---|---|---|---|
| 실행 컨텍스트 | softirq | softirq | 프로세스 (kworker) |
| 슬립 가능 | 불가 | 불가 | 가능 |
| 지연시간 | ~1-5 μs | ~1-5 μs (동등) | ~5-30 μs |
| cancel/flush API | tasklet_kill()만 | cancel_work_sync(), flush_work() | 동일 |
| delayed 실행 | 불가 | queue_delayed_work() | 동일 |
| CPU간 부하 분산 | 불가 | CMWQ 자동 분산 | 동일 |
| 자동 직렬화 | 예 (같은 인스턴스) | 예 (같은 work) | 동일 |
| 커널 버전 | 2.3+ | 6.9+ | 2.6+ |
| 상태 | deprecated | 권장 (softirq 필요 시) | 권장 (기본) |
WQ_BH 마이그레이션 전략: tasklet의 낮은 지연시간이 반드시 필요한 경우(softirq 컨텍스트 실행이 핵심인 경우), 커널 6.9+에서는 system_bh_wq를 사용하세요. 대부분의 경우에는 일반 system_wq로 충분하며, 프로세스 컨텍스트의 장점(슬립 가능, mutex, GFP_KERNEL 사용)을 얻을 수 있습니다. 상세 내용은 Bottom Half 문서의 WQ_BH 섹션을 참고하세요.
참고 링크
- Kernel Documentation — Tasklet API — 커널 공식 문서의 tasklet API 설명입니다
- Kernel Documentation — Softirq 메커니즘 — softirq 기반 하위 반쪽(Bottom Half) 동작 원리를 설명합니다
- kernel/softirq.c (Bootlin Elixir) — tasklet_action(), tasklet_hi_action(), tasklet_schedule() 등 핵심 구현부입니다
- include/linux/interrupt.h (Bootlin Elixir) — tasklet_struct 정의, tasklet_setup(), tasklet_schedule() 인라인 함수 및 매크로를 포함합니다
- include/linux/tasklet.h (Bootlin Elixir) — PREEMPT_RT 환경에서의 tasklet 래퍼 구현을 포함합니다
- LWN: Eliminating tasklets (2007) — Matthew Wilcox가 제안한 초기 tasklet 제거 논의입니다. tasklet의 설계적 문제점을 분석합니다
- LWN: Modernizing the tasklet API (2020) — tasklet_setup()과 from_tasklet() 매크로 도입 등 최신 API 전환 과정을 다룹니다
- LWN: Deprecating tasklets (2023) — tasklet 공식 폐지 진행 상황과 WQ_BH 기반 workqueue 대체 방안을 설명합니다
- LWN: BH workqueues (2024) — Tejun Heo의 WQ_BH 워크큐 패치 시리즈로, softirq 컨텍스트에서 workqueue를 실행하는 tasklet 대체 메커니즘입니다
- LWN: A new approach to tasklet conversion (2012) — Thomas Gleixner의 PREEMPT_RT 환경에서 tasklet을 커널 스레드로 전환하는 접근법을 다룹니다
- LWN: Softirqs, tasklets, bottom halves (2006) — softirq와 tasklet 메커니즘의 전체 구조를 비교 분석하는 개요 글입니다
- Kernel Documentation — Unreliable Guide to Locking — softirq/tasklet 컨텍스트에서의 락 사용 규칙과 주의사항을 설명합니다
- lib/test_tasklet.c (Bootlin Elixir) — 커널 셀프테스트의 tasklet 테스트 코드입니다
- Linux Kernel Development, 3rd Edition (Robert Love, 2010) — Chapter 8 "Bottom Halves and Deferring Work"에서 tasklet 구현과 사용법을 상세히 다룹니다
- Understanding the Linux Kernel, 3rd Edition (Bovet & Cesati, 2005) — Chapter 4 "Interrupts and Exceptions"에서 tasklet 상태 머신과 스케줄링 내부를 설명합니다
- Linux Device Drivers, 3rd Edition (Corbet, Rubini & Kroah-Hartman, 2005) — Chapter 7 "Time, Delays, and Deferred Work"에서 드라이버 개발자를 위한 tasklet 활용법을 안내합니다
- Kernel Documentation — Concurrency Managed Workqueue — tasklet에서 workqueue로 마이그레이션할 때 참고할 공식 workqueue 문서입니다
관련 문서
- Bottom Half 메커니즘 — softirq, tasklet, workqueue, threaded IRQ 전체 개요
- 인터럽트 (Interrupts) — IRQ, Top/Bottom Half, softirq, tasklet, workqueue
- Softirq / Hardirq — 하드웨어 인터럽트와 소프트웨어 인터럽트 상세
- Workqueue (CMWQ) — Concurrency Managed Workqueue