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/Hardirq 문서를 먼저 읽으세요. Tasklet은 softirq 위에서 동작하므로, softirq의 실행 컨텍스트와 제약 사항을 이해하는 것이 필수적입니다.
일상 비유: Tasklet은 번호표가 있는 은행 창구와 비슷합니다. 같은 번호표(같은 tasklet)를 가진 사람은 동시에 여러 창구에서 처리될 수 없지만(직렬화), 다른 번호표를 가진 사람들은 다른 창구(다른 CPU)에서 동시에 처리됩니다.

핵심 요약

  • 동적 Bottom Half — softirq와 달리 런타임에 동적으로 생성/삭제 가능. tasklet_setup()으로 초기화, tasklet_kill()로 해제.
  • 직렬화 보장TASKLET_STATE_RUN 비트로 같은 tasklet의 병렬 실행을 차단. 다른 tasklet은 다른 CPU에서 동시 실행 가능.
  • Per-CPU 리스트tasklet_vec(일반)과 tasklet_hi_vec(고우선순위) 두 개의 Per-CPU 단일 연결 리스트로 관리.
  • 상태 머신TASKLET_STATE_SCHEDTASKLET_STATE_RUN 두 비트로 Idle, Scheduled, Running, Disabled 상태 전이.
  • 새 API — 커널 5.9+에서 tasklet_setup() + from_tasklet() 조합이 타입 안전한 표준 방법.
  • deprecated 추세 — 새 코드에서는 workqueue 또는 threaded IRQ를 사용 권장. PREEMPT_RT 비호환, 슬립 불가, 부하 분산 불가 등이 이유.

단계별 이해

  1. 구조체 이해
    tasklet_structstate, count, callback 필드와 Per-CPU 리스트(tasklet_head)의 관계를 먼저 파악합니다.
  2. 상태 머신 추적
    Idle → Scheduled → Running → Idle 전이와 Disabled 상태를 포함한 전체 상태 다이어그램을 이해합니다.
  3. 스케줄링 내부
    tasklet_schedule()test_and_set_bit → Per-CPU 리스트 추가 → raise_softirq 경로를 코드 레벨에서 따라갑니다.
  4. 실행 경로 분석
    tasklet_action()에서 tasklet_trylockcount 확인 → 콜백 호출 → unlock 과정을 이해합니다.
  5. API 사용법
    tasklet_setup(), tasklet_schedule(), tasklet_disable(), tasklet_enable(), tasklet_kill()의 올바른 사용 패턴과 주의사항을 익힙니다.
  6. 마이그레이션 학습
    기존 tasklet 코드를 workqueue 또는 threaded IRQ로 전환하는 방법을 실제 코드 변환 예제로 연습합니다.
관련 표준: Tasklet은 리눅스 커널 고유의 Bottom Half 메커니즘으로, 특정 외부 표준과 직접 대응하지 않습니다. 관련 커널 소스: kernel/softirq.c, include/linux/interrupt.h. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

개요: 역사적 맥락과 현재 상태

Tasklet은 리눅스 커널 2.3 개발 주기에서 Alexey Kuznetsov에 의해 도입된 Bottom Half 메커니즘입니다. 기존의 BH(Bottom Half) 메커니즘은 전역 락으로 인해 SMP 확장성이 극히 제한적이었고, softirq는 동적 생성이 불가능하여 드라이버 개발자가 직접 사용하기 어려웠습니다. Tasklet은 이 두 가지 한계를 동시에 해결하기 위해 설계되었습니다.

Tasklet의 핵심 설계 목표는 다음 세 가지였습니다:

그러나 커널이 발전하면서 tasklet의 한계가 명확해졌습니다. 2020년 Kees Cook, Sebastian Andrzej Siewior 등의 커널 개발자들이 tasklet의 deprecation을 본격적으로 논의하기 시작했으며, 커널 5.9에서 tasklet_setup() API가 도입된 것은 기존 tasklet_init()의 타입 불안전 문제를 해결하면서도 장기적으로 workqueue/threaded IRQ로의 마이그레이션을 용이하게 하기 위함이었습니다. 2024년 현재에도 수백 개의 드라이버가 tasklet을 사용하고 있어 완전한 제거에는 시간이 걸리지만, 새 코드에서는 workqueue 또는 threaded IRQ를 사용해야 합니다.

Tasklet Deprecation Timeline:
  • 커널 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_SCHEDTASKLET_STATE_RUN 두 개의 상태 비트와 count 필드 조합으로 생명주기를 관리합니다. 아래 다이어그램은 모든 가능한 상태 전이를 보여줍니다.

Tasklet 생명주기 상태 머신 Idle state=0, count=0 Scheduled state=SCHED Running state=SCHED|RUN Disabled count > 0 Sched+Disabled state=SCHED, count > 0 tasklet_schedule() softirq 디스패치 callback 완료 → SCHED, RUN 클리어 tasklet_disable() tasklet_enable() tasklet_schedule() tasklet_enable() 재schedule → no-op 다른 CPU → 스킵 핵심 규칙 • SCHED set → schedule() 무시 • RUN set → 다른 CPU 실행 차단 • count > 0 → 실행 보류 • tasklet_kill() → Idle로 강제 전이
Tasklet 상태 전이: Idle ↔ Scheduled ↔ Running, Disabled 상태 포함

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 바인딩)
 */
Per-CPU Tasklet 리스트 구조 CPU 0 tasklet_vec 우선순위 6 tasklet_A tasklet_B tasklet_C NULL tasklet_hi_vec 우선순위 0 hi_tasklet_X NULL CPU 1 tasklet_vec 우선순위 6 tasklet_D NULL tasklet_hi_vec 우선순위 0 NULL 일반 (TASKLET_SOFTIRQ) 고우선순위 (HI)
각 CPU마다 독립적인 tasklet_vec(일반)과 tasklet_hi_vec(고우선순위) 리스트를 유지

각 CPU는 자신만의 tasklet_vectasklet_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_vectasklet_vec
스케줄 APItasklet_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);
Softirq 우선순위와 Tasklet 위치 높은 우선순위 낮은 우선순위 0: HI_SOFTIRQ tasklet_hi_schedule() 가장 먼저 실행 (오디오 등) 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-9: SCHED / HRTIMER / RCU 일반 tasklet보다 먼저 실행되는 5개 softirq
HI_SOFTIRQ(인덱스 0)는 모든 softirq보다 먼저 실행, TASKLET_SOFTIRQ(인덱스 6)는 후반에 실행
⚠️

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 필드truefalse
상태권장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_schedule() 내부 흐름 tasklet_schedule(t) test_and_set_bit (SCHED, &t->state) 이미 set 즉시 반환 새로 set local_irq_save(flags) Per-CPU tasklet_vec.tail에 추가 raise_softirq_irqoff(TASKLET_SOFTIRQ) local_irq_restore(flags) 이후 실행 시점: • Hard IRQ: irq_exit() 시 • softirq: 다음 루프 반복 • 프로세스: ksoftirqd
tasklet_schedule(): SCHED 비트 원자적 검사 후 Per-CPU 리스트 추가 및 softirq 트리거

직렬화 보장

같은 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에서만 실행됨
 */
CPU간 Tasklet 직렬화 타임라인 시간 → CPU 0 trylock 성공 callback(t) 실행 중 unlock CPU 1 trylock 실패 재스케줄 trylock 성공 callback(t) 실행 RUN bit 0 RUN = 1 (CPU 0이 보유) RUN = 1 (CPU 1이 보유) 직렬화 규칙 • 같은 tasklet: 절대 병렬 실행 불가 (RUN bit) • 다른 tasklet: 다른 CPU에서 병렬 실행 가능 • 같은 CPU: tasklet 간 중첩 불가 (순차) • trylock 실패 시 재스케줄하여 나중에 재시도 • UP 시스템: RUN bit 불필요 (항상 순차) Bottom Half 별 직렬화 비교 softirq 같은 타입도 병렬 실행 (락 필요) tasklet 같은 인스턴스 직렬화 (락 불필요) workqueue work item 단위 직렬화 threaded IRQ IRQ 라인당 직렬화
CPU 0에서 tasklet 실행 중 CPU 1의 시도가 실패하고 재스케줄되는 타임라인
메커니즘같은 인스턴스 병렬다른 인스턴스 병렬슬립 가능직렬화 방법
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_action() 실행 루프 Per-CPU tasklet_vec.head 가져오기 (head를 NULL로 리셋 - 원자적 분리) list != NULL ? NULL 완료 반환 t = list; list = list->next; tasklet_trylock(t) (RUN bit set 시도) 실패(실행 중) 재스케줄 atomic_read (&t->count) == 0? count > 0 unlock 후 재스케줄 활성 clear_bit(SCHED, &t->state) t->callback(t) // 핸들러 실행 tasklet_unlock(t) // RUN bit clear continue
tasklet_action(): Per-CPU 리스트 순회, trylock/count 검사 후 콜백 실행

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() = truein_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()로 정리 */
비교 항목taskletworkqueuethreaded IRQ
컨텍스트인터럽트 (softirq)프로세스 (kworker)프로세스 (irq/N-name)
슬립불가가능가능
지연 시간낮음중간낮음~중간
IRQ 연관간접간접직접 (1:1 대응)
우선순위 제어불가제한적 (nice)가능 (chrt)
PREEMPT_RT문제 있음호환완전 호환
추가 초기화tasklet_setup()INIT_WORK()불필요
정리 APItasklet_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 EngineDMA 전송 완료 콜백tasklet_setup()일부 workqueue 전환 완료
IrDA (적외선)데이터 수신 처리레거시 tasklet_init()서브시스템 자체 제거됨 (5.17+)
추세: 커널 커뮤니티는 tasklet을 점진적으로 제거하는 방향으로 진행 중입니다. 새 드라이버에서 tasklet_setup()을 사용하는 패치는 리뷰 시 workqueue/threaded IRQ 사용을 권장받습니다. include/linux/interrupt.h에서 tasklet API 위에 "deprecated" 주석이 추가되어 있습니다.

Bottom Half 종합 비교

리눅스 커널의 모든 Bottom Half 메커니즘을 종합적으로 비교합니다:

특성softirqtaskletworkqueuethreaded 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_RTksoftirqd 위임ksoftirqd 위임완전 호환완전 호환
취소/대기 API없음tasklet_kill()cancel_work_sync()free_irq()
타이머 연동TIMER_SOFTIRQ 자체tasklet_schedule()delayed_work없음
대표 사용처네트워크, 블록, 타이머사운드, USB, 무선파일시스템, 드라이버GPIO, I2C, SPI
새 코드 권장기존 softirq만비권장 (deprecated)권장권장

마이그레이션 결정 트리

기존 tasklet 코드를 어떤 메커니즘으로 전환할지 결정하는 흐름도입니다:

Tasklet 마이그레이션 결정 트리 기존 tasklet 코드 IRQ 핸들러와 1:1 대응? RT 커널 지원 또는 슬립 필요? threaded IRQ request_threaded_irq() 아니오 슬립/mutex 사용 필요? workqueue INIT_WORK() + schedule_work() 아니오 지연 실행 또는 타이머 연동 필요? delayed workqueue INIT_DELAYED_WORK() 아니오 CPU간 부하 분산 필요? workqueue 아니오 workqueue (기본 권장) 특별한 이유 없으면 workqueue
tasklet에서 대안 메커니즘으로 전환할 때의 의사결정 흐름도
💡

마이그레이션 원칙: 결정이 어렵다면 workqueue를 선택하세요. CMWQ(Concurrency Managed Workqueue)는 자동 동시성 관리, CPU간 부하 분산, 슬립 가능 등 tasklet의 모든 제한을 해결합니다. threaded IRQ는 하드웨어 인터럽트와 직접 연결된 Bottom Half에만 사용하는 것이 적절합니다.

필수 관련 문서: 참고 문서: