Bottom Half 선택 가이드와 실전 패턴

Bottom Half 메커니즘(softirq, tasklet, workqueue, threaded IRQ) 중 올바른 선택 기준, 실전 패턴, 성능 최적화, 디버깅(Debugging) 가이드를 제공합니다.

이 페이지(Page)는 "슬립(Sleep) 필요 여부, 처리량(Throughput), 순서 보장(Ordering), 격리(Isolation) 수준, PREEMPT_RT 호환성" 기준으로 메커니즘을 고르는 실전 의사결정 표준을 제시합니다. 각 메커니즘의 내용은 아래 전용 페이지를 참고하세요.

관련 표준: (내부 구현으로 외부 표준 없음) Linux 커널 내부 설계 패턴입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
ℹ️

이 페이지는 인터럽트(Interrupt) 페이지의 Bottom Half 기초 내용을 바탕으로, 메커니즘 선택과 실전 패턴에 집중합니다. 각 메커니즘의 내부 구현는 전용 페이지를 참고하세요.

전제 조건: 인터럽트동기화 기법 문서를 먼저 읽으세요. 비동기 이벤트 처리 주제는 문맥 전환(Context Switch)과 지연(Latency) 실행 경로를 정확히 구분해야 하므로, IRQ와 deferred work 경계를 먼저 잡아야 합니다.

핵심 요약

  • softirq — 커널에 정적 등록되는 고성능 BH. 네트워킹, 블록 I/O 등에서 사용. per-CPU로 병렬 실행됩니다.
  • tasklet — softirq 위에 구현된 간편 메커니즘. 같은 tasklet은 동시에 하나의 CPU에서만 실행됩니다.
  • workqueue — 프로세스(Process) 컨텍스트에서 실행. 슬립 가능하여 I/O, 잠금(Lock) 획득 등이 가능합니다.
  • ksoftirqd — softirq 부하가 높을 때 처리를 인계받는 per-CPU 커널 스레드(Kernel Thread)입니다.

단계별 이해

  1. BH가 필요한 이유 — Top Half에서 오래 걸리는 작업을 하면 다른 인터럽트가 차단됩니다.

    BH로 지연하면 인터럽트를 다시 활성화하고 나중에 안전하게 처리할 수 있습니다.

  2. softirq 이해 — 10개 고정 타입(NET_TX, NET_RX, BLOCK, TIMER 등). 새로 추가하려면 커널 소스를 수정해야 합니다.

    같은 softirq가 여러 CPU에서 동시에 실행될 수 있어 per-CPU 데이터를 사용합니다.

  3. tasklet 이해 — 드라이버에서 가장 쉽게 사용하는 BH. tasklet_schedule()로 예약합니다.

    같은 tasklet 인스턴스는 직렬화(Serialization)되어 경쟁 조건(Race Condition) 걱정이 줄어듭니다.

  4. 선택 기준 — 슬립이 필요하면 workqueue, 고성능이 필요하면 softirq, 간단한 지연 처리는 tasklet을 사용합니다.

    최근에는 tasklet 대신 threaded IRQ나 workqueue를 권장하는 추세입니다.

각 Bottom Half 메커니즘의 내부 구현, API 상세, 디버깅 기법은 전용 페이지에서 다룹니다.

Softirq: open_softirq 등록, raise_softirq 변형, __do_softirq 내부, ksoftirqd 생명주기, Per-CPU 동시성, 선점(Preemption) 모드별 동작.
Softirq & Hardirq 페이지로 이동 →
Tasklet: Per-CPU 리스트, HI/NORMAL 비교, 상태 머신, tasklet_schedule 내부, 직렬화 보장, disable/enable, kill, PREEMPT_RT 동작, deprecation, workqueue/threaded IRQ 마이그레이션.
Tasklet 페이지로 이동 →
Workqueue (CMWQ): CMWQ 아키텍처, worker pool, alloc_workqueue API, max_active, 시스템 워크큐, work 생명주기, ordered/delayed, cancel/flush, 디버깅, best practices.
Workqueue (CMWQ) 페이지로 이동 →

아래부터는 메커니즘 선택 기준과 공통 패턴을 다룹니다.

Bottom Half 선택 가이드

결정 매트릭스

기준SoftirqTaskletWorkqueueThreaded IRQ
실행 컨텍스트인터럽트인터럽트프로세스프로세스
슬립 가능불가불가가능가능
동시성같은 타입 병렬같은 인스턴스 직렬max_active 제어Per-IRQ 스레드(Thread)
지연시간최소낮음중간낮음~중간
동적 생성불가 (정적)가능가능가능
PREEMPT_RTksoftirqd로 이동비호환정상 동작정상 동작
우선순위(Priority) 제어불가불가nice 값RT 우선순위 가능
사용 권장커널 내부만deprecated기본 선택IRQ Bottom Half용
메모리 할당GFP_ATOMIC만GFP_ATOMIC만GFP_KERNEL 가능GFP_KERNEL 가능
mutex불가불가가능가능

결정 흐름도

Bottom Half 선택 흐름도 IRQ 핸들러의 bottom half? Yes No (비동기 작업) Workqueue schedule_work() 슬립 필요? (mutex, I2C, 메모리) Yes Threaded IRQ request_threaded_irq() No PREEMPT_RT 지원 필요? Yes Threaded IRQ or Workqueue No 초저지연 필수? Yes Workqueue (WQ_HIGHPRI) (softirq는 커널 내부 전용) No Workqueue schedule_work()
Bottom Half 메커니즘 선택 흐름도 (드라이버 개발자 관점)

고급 선택 결정 트리

위 흐름도보다 더 세부적인 결정 기준을 포함한 고급 결정 트리입니다. 지연 허용 범위, 순서 보장, 메모리 할당 모드, CPU 바인딩 여부까지 고려합니다.

Bottom Half 고급 선택 결정 트리 슬립(blocking)이 필요한가? Yes No IRQ 핸들러의 BH인가? Yes Threaded IRQ request_threaded_irq() RT 우선순위 가능 No 순서 보장 필요한가? Yes Ordered Workqueue alloc_ordered_workqueue() max_active=1, FIFO 순서 No Standard Workqueue alloc_workqueue() / system_wq WQ_MEM_RECLAIM 권장 초저지연(<1us) 필수? Yes 커널 내부 서브시스템? Yes Softirq open_softirq() 커널 소스 수정 필요 No (드라이버) WQ_HIGHPRI alloc_workqueue(WQ_HIGHPRI) 높은 우선순위 워커 No PREEMPT_RT 호환? Yes Threaded IRQ IRQF_ONESHOT RT 안전, 선점 가능 No 선택 시 고려사항 요약 Threaded IRQ: IRQ BH + sleep 가능 + RT 호환 (권장) Workqueue: 범용 BH, GFP_KERNEL, mutex 사용 가능 (기본 선택) WQ_HIGHPRI / Ordered: 우선순위/순서 보장 필요 시 Softirq: 커널 내부 전용 (NET_RX, BLOCK, TIMER 등) Tasklet: deprecated, threaded IRQ나 workqueue로 전환 권장 GFP_ATOMIC만 가능 = softirq/tasklet | GFP_KERNEL 가능 = workqueue/threaded IRQ Per-CPU 분산 = softirq | 직렬화 보장 = ordered WQ | RT 우선순위 = threaded IRQ PREEMPT_RT 미지원 = softirq(강제이동), tasklet(비호환) | 지원 = WQ, threaded IRQ
고급 선택 결정 트리 - 지연시간, 순서 보장, RT 호환성까지 고려

PREEMPT_RT 영향

설명 요약:
  • PREEMPT_RT에서의 Bottom Half 변화:
  • Softirq:
  • 모든 softirq가 ksoftirqd에서 실행 (선점 가능)
  • irq_exit()에서 직접 실행하지 않음
  • local_bh_disable()이 preempt_disable()로 변경되지 않음
  • → RT 뮤텍스(Mutex) 기반으로 변경
  • Tasklet:
  • PREEMPT_RT에서 문제 유발
  • 인터럽트 컨텍스트 가정 코드가 호환되지 않음
  • 커널 커뮤니티에서 제거 진행 중
  • Workqueue:
  • 정상 동작 (이미 프로세스 컨텍스트)
  • RT 우선순위 설정 가능
  • Threaded IRQ:
  • 정상 동작 (이미 스레드 기반)
  • SCHED_FIFO 우선순위로 실행
  • chrt 명령으로 IRQ 스레드 우선순위 조정 가능
  • Spinlock:
  • spin_lock()이 rt_mutex로 변경 (슬립 가능!)
  • raw_spin_lock()만 진짜 스핀 (사용 최소화)
  • spin_lock_irqsave() → sleeping lock + local_irq_save

성능 특성 비교

특성SoftirqWorkqueue (bound)Workqueue (unbound)Threaded IRQ
호출 오버헤드(Overhead)~100ns~1-5us~1-10us~1-5us
스케줄링 지연거의 없음컨텍스트 스위치컨텍스트 스위치 + 마이그레이션컨텍스트 스위치
SMP 확장성뛰어남 (Per-CPU)좋음 (Per-CPU)좋음 (NUMA-aware)보통 (Per-IRQ)
캐시(Cache) 친화성높음높음 (같은 CPU)보통보통
우선순위 역전(Priority Inversion)가능 (RT 제외)PI 없음PI 없음PI 지원

실행 컨텍스트 비교 다이어그램

각 Bottom Half 메커니즘이 실행되는 컨텍스트를 시각적으로 비교합니다. 인터럽트 컨텍스트(softirq/tasklet)와 프로세스 컨텍스트(workqueue/threaded IRQ)의 차이를 이해하는 것이 올바른 선택의 핵심입니다.

Bottom Half 실행 컨텍스트 비교 CPU 타임라인 Hardirq Top Half IRQ 비활성화, 선점 불가, 슬립 불가 GFP_ATOMIC만 가능, 최소한의 작업만 수행 Softirq do_softirq() IRQ 활성화, 선점 불가 (BH disabled), 슬립 불가 Per-CPU 병렬 실행, 같은 타입 여러 CPU에서 동시 실행 Tasklet tasklet_action() softirq 위에서 실행, 같은 인스턴스 직렬화 보장 deprecated: threaded IRQ / workqueue로 전환 권장 --- 인터럽트 / 프로세스 경계 --- ksoftirqd softirq 대리 실행 커널 스레드 (프로세스 컨텍스트), softirq 과부하 시 인계 PREEMPT_RT에서 모든 softirq 처리 담당, nice 0 Workqueue kworker process_one_work() 커널 스레드 (프로세스 컨텍스트), 슬립 가능, mutex/GFP_KERNEL 가능 CMWQ worker pool, max_active 제어, nice 조정 가능 Threaded IRQ irq_thread() handler 전용 커널 스레드 (Per-IRQ), 슬립 가능, RT 우선순위 설정 가능 SCHED_FIFO 기본, chrt로 우선순위 조정, PREEMPT_RT 완전 호환 시간 (실행 지연 증가 방향)
Bottom Half 실행 컨텍스트 비교 - 인터럽트 컨텍스트 vs 프로세스 컨텍스트

지연시간/처리량 시각화

각 메커니즘의 호출 오버헤드와 처리 지연시간을 시각적 막대 그래프로 비교합니다. 실제 측정값은 하드웨어와 커널 설정에 따라 다르지만, 상대적인 크기를 이해하는 데 유용합니다.

Bottom Half 성능 비교 (호출 오버헤드 vs 스케줄링 지연) 메커니즘 지연시간 (대수 스케일, 단위: ns/us) 0 100ns 1us 5us 10us 20us+ Softirq ~100ns (호출 오버헤드) ~거의 없음 (스케줄링 지연) Tasklet ~130ns (호출 오버헤드) ~50ns (직렬화 대기 가능) WQ (bound) ~1-5us (호출 오버헤드) ~1-5us (컨텍스트 스위치) WQ (unbound) ~1-10us (호출) + 마이그레이션 Threaded IRQ ~1-5us (호출 오버헤드) ~1-5us (스레드 웨이크업) 호출 오버헤드 스케줄링 지연 * 실측값은 CPU, 부하, 커널 설정(PREEMPT 모드)에 따라 변동됩니다.
Bottom Half 메커니즘별 호출 오버헤드와 스케줄링 지연 비교

__do_softirq() 내부 실행 흐름

__do_softirq()는 리눅스 커널에서 모든 softirq를 실제로 디스패치(Dispatch)하는 핵심 함수입니다. 인터럽트 반환 시점(irq_exit())이나 local_bh_enable() 호출 시점에 pending softirq가 있으면 이 함수가 호출됩니다. 내부 동작을 정확히 이해하면 softirq 기반 BH의 지연 특성과 ksoftirqd 위임 조건을 파악할 수 있습니다.

실행 흐름 다이어그램

__do_softirq() 내부 실행 흐름 (kernel/softirq.c) __do_softirq() 진입 irq_exit() 또는 local_bh_enable() 1. 초기화 end = jiffies + MAX_SOFTIRQ_TIME (2ms) max_restart = MAX_SOFTIRQ_RESTART (10) 2. softirq 컨텍스트 설정 __local_bh_disable(SOFTIRQ_OFFSET) in_serving_softirq() = true 3. pending = local_softirq_pending() 현재 CPU의 softirq pending 비트마스크 읽기 4. local_irq_enable() 인터럽트 재활성화 (softirq 처리 중 IRQ 가능) 5. softirq 핸들러 실행 루프 while (softirq_bit = ffs(pending)) h = softirq_vec + bit; h→action(h); 호출 HI→TIMER→NET_TX→NET_RX→BLOCK→IRQ_POLL→TASKLET→SCHED→HRTIMER→RCU 6. local_irq_disable() + 재확인 pending = local_softirq_pending() 재읽기 pending && --max_restart && !time_after(jiffies,end)? Yes 재시작 No 아직 pending 남아있는가? Yes wakeup_softirqd() ksoftirqd에 위임 No 완료 BH enable 핵심: 최대 2ms 또는 10회 재시작까지 처리 → 초과 시 ksoftirqd 커널 스레드로 위임 softirq 처리 중에도 인터럽트는 활성화 → 새 인터럽트 수신 가능
__do_softirq() 내부 실행 흐름 - 최대 2ms/10회 재시작 제한과 ksoftirqd 위임

커널 소스 레벨 분석

/* kernel/softirq.c - __do_softirq() 핵심 로직 (간소화) */
asmlinkage __visible void __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;  /* 2ms */
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;  /* 10 */
    struct softirq_action *h;
    bool in_hardirq;
    __u32 pending;
    int softirq_bit;

    /*
     * 현재 태스크에 PF_MEMALLOC 설정
     * softirq 핸들러에서 메모리 할당 시 emergency pool 사용 가능
     */
    current->flags &= ~PF_MEMALLOC;

    /* pending 비트마스크 읽기 + 초기화 */
    pending = local_softirq_pending();

    softirq_handle_begin();  /* __local_bh_disable(SOFTIRQ_OFFSET) */
    in_hardirq = lockdep_softirq_start();
    account_softirq_enter(current);

restart:
    /* pending 비트마스크 초기화 (새 softirq는 다음 라운드에서 처리) */
    set_softirq_pending(0);

    local_irq_enable();  /* ★ 인터럽트 재활성화 */

    h = softirq_vec;  /* softirq 핸들러 배열 시작 */

    while ((softirq_bit = ffs(pending))) {
        unsigned vec_nr = softirq_bit - 1;

        h += vec_nr;
        /* 실제 softirq 핸들러 호출 */
        trace_softirq_entry(vec_nr);
        h->action(h);  /* ★ 핵심: 등록된 핸들러 실행 */
        trace_softirq_exit(vec_nr);

        h++;
        pending >>= softirq_bit;
    }

    local_irq_disable();  /* 인터럽트 비활성화 후 재확인 */

    pending = local_softirq_pending();
    if (pending) {
        /* 재시작 조건: 횟수 남음 AND 시간 남음 AND softirqd 아님 */
        if (time_before(jiffies, end) &&
            !need_resched() &&
            --max_restart)
            goto restart;

        /* 조건 미충족: ksoftirqd로 위임 */
        wakeup_softirqd();
    }

    account_softirq_exit(current);
    lockdep_softirq_end(in_hardirq);
    softirq_handle_end();  /* __local_bh_enable(SOFTIRQ_OFFSET) */
    current->flags = old_flags;
}

호출 경로 분석

__do_softirq()는 세 가지 경로에서 호출됩니다.

호출 경로트리거 조건실행 컨텍스트빈도
irq_exit()__irq_exit_rcu()hardirq 반환 시 pending softirq 존재인터럽트 반환 직후가장 흔함
local_bh_enable()BH 재활성화 시 pending softirq 존재프로세스 컨텍스트빈번함
ksoftirqd 스레드wakeup_softirqd()에 의해 깨움커널 스레드 (프로세스 컨텍스트)부하 높을 때

ksoftirqd 위임 조건과 영향

ksoftirqd 위임은 시스템 성능에 중요한 영향을 미칩니다. softirq가 직접 실행되면 인터럽트 반환 시점에 즉시 처리되지만, ksoftirqd로 위임되면 스케줄러를 거쳐야 하므로 지연이 증가합니다.

ksoftirqd 위임 조건 (하나라도 충족 시):
  • 시간 초과: softirq 처리 시작 후 2ms(MAX_SOFTIRQ_TIME) 경과
  • 재시작 횟수 초과: 10번(MAX_SOFTIRQ_RESTART) 재시작 완료
  • 선점 요청: need_resched()가 true (더 높은 우선순위 태스크 대기)

영향: ksoftirqd는 SCHED_OTHER (nice 0)로 실행되므로, RT 태스크보다 낮은 우선순위입니다. 네트워크 트래픽 폭주 시 패킷 처리 지연이 급증할 수 있습니다.

/* ksoftirqd 커널 스레드 메인 루프 (간소화) */
static int ksoftirqd_should_run(unsigned int cpu)
{
    return local_softirq_pending();
}

static void run_ksoftirqd(unsigned int cpu)
{
    ksoftirqd_run_begin();
    if (local_softirq_pending()) {
        __do_softirq();  /* 동일한 함수 호출 */
        ksoftirqd_run_end();
        cond_resched();  /* 다른 태스크에게 CPU 양보 기회 */
        return;
    }
    ksoftirqd_run_end();
}

/* ksoftirqd 과부하 진단 */
/* /proc/net/softnet_stat 3번째 열(time_squeeze)이 증가하면
 * NET_RX softirq가 시간/budget 초과로 ksoftirqd에 위임되고 있음 */

softirq_vec 타입별 핸들러 매핑

인덱스softirq 타입핸들러 함수등록 위치용도
0HI_SOFTIRQtasklet_hi_action()softirq_init()고우선순위 tasklet
1TIMER_SOFTIRQrun_timer_softirq()init_timers()커널 타이머 처리
2NET_TX_SOFTIRQnet_tx_action()net_dev_init()네트워크 송신 완료
3NET_RX_SOFTIRQnet_rx_action()net_dev_init()네트워크 수신 (NAPI)
4BLOCK_SOFTIRQblk_done_softirq()blk_softirq_init()블록 I/O 완료
5IRQ_POLL_SOFTIRQirq_poll_softirq()irq_poll_setup_cpu()IRQ 폴링 완료
6TASKLET_SOFTIRQtasklet_action()softirq_init()일반 tasklet 실행
7SCHED_SOFTIRQrun_rebalance_domains()init_sched_fair_class()스케줄러 부하 분산
8HRTIMER_SOFTIRQhrtimer_run_softirq()hrtimers_init()고해상도 타이머
9RCU_SOFTIRQrcu_core_si()rcu_init()RCU 콜백 처리
실행 순서: softirq는 인덱스 0(HI_SOFTIRQ)부터 9(RCU_SOFTIRQ)까지 순서대로 검사됩니다. ffs()(find first set bit)로 가장 낮은 비트부터 처리하므로, HI_SOFTIRQ가 가장 먼저, RCU_SOFTIRQ가 가장 나중에 처리됩니다. 단, 이 순서는 우선순위가 아니라 단순한 처리 순서이며, 각 핸들러의 실행 시간에 따라 후순위 softirq의 지연이 발생할 수 있습니다.

관련 커널 설정

CONFIG 옵션설명기본값
CONFIG_PREEMPT_NONE선점 없음, 서버 최적화서버 defconfig
CONFIG_PREEMPT_VOLUNTARY자발적 선점, 데스크톱 기본데스크톱 defconfig
CONFIG_PREEMPT완전 선점선택
CONFIG_PREEMPT_RT실시간(Real-time) 선점 (6.12+)선택
CONFIG_WQ_WATCHDOGworkqueue 정체 감시y
CONFIG_WQ_POWER_EFFICIENT_DEFAULT전력 효율 workqueue 기본 활성화n (노트북에서 y 권장)
CONFIG_IRQ_FORCED_THREADING모든 IRQ를 강제 스레드화n

PREEMPT_RT에서의 Bottom Half 변환 상세

PREEMPT_RT(Real-Time) 패치(Patch)가 적용되면 각 Bottom Half 메커니즘의 동작이 크게 변합니다. 아래 다이어그램은 일반 커널과 RT 커널에서 각 메커니즘의 실행 경로 변환을 보여줍니다.

PREEMPT_RT에서의 Bottom Half 변환 흐름 일반 커널 (PREEMPT) RT RT 커널 (PREEMPT_RT) Softirq irq_exit() 에서 직접 실행 인터럽트 컨텍스트, 선점 불가 강제 이동 ksoftirqd 스레드 프로세스 컨텍스트, 선점 가능 irq_exit()에서 직접 실행 안 함 Tasklet softirq TASKLET_SOFTIRQ 위에서 실행 인터럽트 컨텍스트 가정 코드 비호환 (제거 진행중) 비호환 / 제거 대상 인터럽트 컨텍스트 가정이 깨짐 workqueue/threaded IRQ로 전환 필수 Workqueue kworker 스레드 (프로세스 컨텍스트) 슬립 가능, mutex 사용 가능 변경 없음 Workqueue (동일) 정상 동작, RT 우선순위 설정 가능 WQ_HIGHPRI로 높은 우선순위 가능 Threaded IRQ 전용 커널 스레드 (SCHED_FIFO) RT 우선순위 기본 지원 변경 없음 Threaded IRQ (동일) SCHED_FIFO, chrt로 우선순위 조정 RT 최적: 결정적 지연시간 spin_lock() 진짜 스핀락, 선점 비활성화 슬립 불가 rt_mutex로 변환 rt_mutex (sleeping) 슬립 가능한 뮤텍스로 변환 raw_spin_lock()만 진짜 스핀 local_bh_disable() softirq 실행 억제 preempt_count 증가 per-CPU lock local_lock (sleeping) RT에서 per-CPU local_lock 사용 preempt_disable로 변경 안 됨 PREEMPT_RT 전환 요약 안전: workqueue, threaded IRQ (변경 없이 정상 동작) 주의: softirq (ksoftirqd로 이동, 지연 증가 가능), spin_lock (rt_mutex로 변환) 위험: tasklet (비호환, 제거 대상) - 반드시 threaded IRQ 또는 workqueue로 마이그레이션
PREEMPT_RT 패치 적용 시 각 메커니즘의 변환 흐름

PREEMPT_RT 마이그레이션 코드 패턴

tasklet에서 threaded IRQ로의 전환 과정을 단계별로 보여줍니다.

/* ===== 변환 전: tasklet 기반 드라이버 ===== */
struct my_device {
    struct tasklet_struct rx_tasklet;
    spinlock_t lock;
    void __iomem *regs;
};

static irqreturn_t my_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = readl(dev->regs + IRQ_STATUS);

    if (!(status & MY_IRQ_MASK))
        return IRQ_NONE;

    writel(status, dev->regs + IRQ_ACK);
    tasklet_schedule(&dev->rx_tasklet);  /* PREEMPT_RT 비호환! */
    return IRQ_HANDLED;
}

static void my_tasklet_func(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, rx_tasklet);
    unsigned long flags;

    spin_lock_irqsave(&dev->lock, flags);
    process_rx_data(dev);
    spin_unlock_irqrestore(&dev->lock, flags);
}

/* ===== 변환 후: threaded IRQ 기반 ===== */
struct my_device {
    /* tasklet 제거 */
    spinlock_t lock;
    void __iomem *regs;
};

static irqreturn_t my_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = readl(dev->regs + IRQ_STATUS);

    if (!(status & MY_IRQ_MASK))
        return IRQ_NONE;

    writel(status, dev->regs + IRQ_ACK);
    return IRQ_WAKE_THREAD;  /* threaded handler 호출 */
}

static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    spin_lock(&dev->lock);  /* RT에서 rt_mutex로 안전하게 변환됨 */
    process_rx_data(dev);
    spin_unlock(&dev->lock);

    return IRQ_HANDLED;
}

/* probe에서 등록 */
request_threaded_irq(irq, my_hardirq, my_thread_fn,
                    IRQF_ONESHOT, "my_device", dev);
PREEMPT_RT 마이그레이션 체크리스트:
  • tasklet_schedule() 호출 제거, IRQ_WAKE_THREAD 반환으로 변경
  • spin_lock_irqsave()spin_lock() 으로 단순화 (threaded context에서 불필요)
  • request_irq()request_threaded_irq() 로 변경
  • IRQF_ONESHOT 플래그 추가 (threaded handler 완료까지 IRQ 마스킹)
  • RT 우선순위가 필요하면 chrt -f -p <priority> <irq_thread_pid> 로 설정

성능 최적화 가이드

Bottom Half는 시스템 전체 성능에 큰 영향을 미칩니다. 효율적인 사용 방법을 소개합니다.

softirq 실행 시간 제한

/* __do_softirq() 내부 - 최대 2ms 또는 10번 재시작 제한 */
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;  /* 2ms */
int max_restart = MAX_SOFTIRQ_RESTART;  /* 10 */

/* 성능 최적화: softirq 핸들러는 2ms 이내에 완료해야 함 */
static void my_softirq_action(struct softirq_action *h)
{
    struct sk_buff *skb;
    int budget = 64;  /* 한 번에 처리할 패킷 수 제한 */

    while ((skb = dequeue_packet()) && --budget > 0) {
        process_packet(skb);
    }

    /* 남은 작업이 있으면 다시 스케줄 */
    if (has_pending_packets())
        raise_softirq(NET_RX_SOFTIRQ);
}

tasklet 병합으로 오버헤드 감소

/* ❌ 비효율적: 매 인터럽트마다 tasklet 스케줄 */
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    tasklet_schedule(&my_tasklet);  /* 매번 스케줄 */
    return IRQ_HANDLED;
}

/* ✅ 효율적: pending 상태면 스케줄 생략 */
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    atomic_inc(&dev->pending_count);

    /* 이미 스케줄되어 있으면 중복 스케줄 안 함 */
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &my_tasklet.state))
        tasklet_schedule(&my_tasklet);

    return IRQ_HANDLED;
}

static void my_tasklet_func(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, tasklet);
    int count = atomic_xchg(&dev->pending_count, 0);

    /* 여러 인터럽트를 한 번에 처리 */
    process_batch(dev, count);
}

workqueue 동시성 튜닝

/* ❌ 기본 workqueue - 제한된 동시성 */
schedule_work(&my_work);  /* system_wq 사용 */

/* ✅ 커스텀 workqueue - 동시성 제어 */
struct workqueue_struct *my_wq;

/* CPU-bound 작업 - UNBOUND로 CPU 간 이동 허용 */
my_wq = alloc_workqueue("my_wq",
                        WQ_UNBOUND | WQ_HIGHPRI,
                        4);  /* max_active = 4 */

/* I/O-bound 작업 - 높은 동시성 허용 */
my_wq = alloc_workqueue("io_wq",
                        WQ_MEM_RECLAIM,
                        256);  /* 많은 동시 실행 허용 */

/* 성능 측정 */
u64 start = ktime_get_ns();
flush_workqueue(my_wq);
u64 elapsed = ktime_get_ns() - start;
pr_info("Workqueue flush took %llu ns\n", elapsed);

NAPI와의 통합 최적화

/* 네트워크 드라이버 최적화 패턴 */
static int my_napi_poll(struct napi_struct *napi, int budget)
{
    struct my_device *dev = container_of(napi, struct my_device, napi);
    int work_done = 0;

    /* budget만큼만 처리 (softirq 시간 제한 준수) */
    while (work_done < budget) {
        struct sk_buff *skb = receive_packet(dev);
        if (!skb)
            break;

        netif_receive_skb(skb);
        work_done++;
    }

    /* 모든 패킷 처리했으면 NAPI 종료 */
    if (work_done < budget) {
        napi_complete(napi);
        enable_interrupts(dev);
    }

    return work_done;
}

성능 측정 도구

# softirq 통계 확인
cat /proc/softirqs
#                    CPU0       CPU1       CPU2       CPU3
#          HI:          0          0          0          0
#       TIMER:    5123456    4987654    5234567    5123456
#      NET_TX:      12345       9876      11234      10123
#      NET_RX:    9876543    8765432    9123456    8987654

# workqueue 통계 (debugfs)
cat /sys/kernel/debug/workqueue/workqueues
cat /sys/kernel/debug/workqueue/pool_workqueues

# ftrace로 softirq 지연 측정
echo 1 > /sys/kernel/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/tracing/events/irq/softirq_exit/enable
cat /sys/kernel/tracing/trace

# perf로 softirq 시간 분석
perf record -e irq:softirq_entry,irq:softirq_exit -a sleep 10
perf script
성능 최적화 원칙:
  • Batch 처리: 가능하면 여러 아이템을 한 번에 처리
  • Budget 제한: softirq는 2ms 이내, tasklet은 최소화
  • 적절한 선택: 빠른 처리는 softirq, 복잡한 처리는 workqueue
  • 측정 기반: 추측 대신 실제 측정 데이터로 최적화

실전 케이스 스터디

실제 드라이버와 서브시스템에서 Bottom Half를 활용하는 패턴을 분석합니다.

케이스 1: 네트워크 드라이버 (NAPI + softirq)

시나리오: 고성능 네트워크 카드의 RX 처리

/* drivers/net/ethernet/intel/e1000e/netdev.c 패턴 */

/* 1. 인터럽트 핸들러 (Top Half) */
static irqreturn_t e1000_intr(int irq, void *data)
{
    struct net_device *netdev = data;
    struct e1000_adapter *adapter = netdev_priv(netdev);
    u32 icr = er32(ICR);

    if (!icr)
        return IRQ_NONE;  /* 우리 인터럽트 아님 */

    if (icr & E1000_ICR_RXT0) {  /* RX 인터럽트 */
        /* 인터럽트 비활성화 */
        ew32(IMC, ~0);

        /* NAPI 스케줄 (softirq로 위임) */
        if (napi_schedule_prep(&adapter->napi)) {
            __napi_schedule(&adapter->napi);
        }
    }

    return IRQ_HANDLED;
}

/* 2. NAPI poll (NET_RX_SOFTIRQ에서 실행) */
static int e1000_clean(struct napi_struct *napi, int budget)
{
    struct e1000_adapter *adapter =
        container_of(napi, struct e1000_adapter, napi);
    int work_done = 0;

    /* budget만큼만 처리 */
    e1000_clean_rx_irq(adapter, &work_done, budget);

    /* 모두 처리했으면 NAPI 완료 */
    if (work_done < budget) {
        napi_complete_done(napi, work_done);

        /* 인터럽트 재활성화 */
        e1000_irq_enable(adapter);
    }

    return work_done;
}

/* 3. 실제 패킷 처리 */
static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,
                             int *work_done, int work_to_do)
{
    while (*work_done < work_to_do) {
        struct sk_buff *skb = e1000_receive_skb(adapter);
        if (!skb)
            break;

        /* 네트워크 스택으로 전달 */
        napi_gro_receive(&adapter->napi, skb);
        (*work_done)++;
    }

    return false;
}

핵심 포인트:

케이스 2: 블록 드라이버 (workqueue)

시나리오: NVMe 드라이버의 I/O 완료 처리

/* drivers/nvme/host/pci.c 패턴 */

/* 1. 인터럽트 핸들러 */
static irqreturn_t nvme_irq(int irq, void *data)
{
    struct nvme_queue *nvmeq = data;

    /* CQ에서 완료 엔트리 확인 */
    if (nvme_cqe_pending(nvmeq)) {
        /* workqueue로 완료 처리 위임 */
        queue_work(nvmeq->cq_wq, &nvmeq->cq_work);
    }

    return IRQ_HANDLED;
}

/* 2. Workqueue 핸들러 */
static void nvme_cq_work(struct work_struct *work)
{
    struct nvme_queue *nvmeq =
        container_of(work, struct nvme_queue, cq_work);

    /* 완료 큐 처리 (블로킹 가능) */
    nvme_process_cq(nvmeq);

    /* 블록 레이어에 완료 통지 */
    blk_mq_complete_request(...);
}

케이스 3: 타이머(Timer) + tasklet 조합

시나리오: 주기적 하드웨어 폴링(Polling)

/* 실전 패턴: 폴링 기반 디바이스 */
struct my_device {
    struct timer_list poll_timer;
    struct tasklet_struct poll_tasklet;
    void __iomem *regs;
};

/* 1. 타이머 콜백 (TIMER_SOFTIRQ) */
static void poll_timer_func(struct timer_list *t)
{
    struct my_device *dev = from_timer(dev, t, poll_timer);

    /* 빠른 레지스터 읽기만 수행 */
    u32 status = readl(dev->regs + STATUS_REG);

    if (status & DATA_READY) {
        /* 실제 처리는 tasklet으로 위임 */
        tasklet_schedule(&dev->poll_tasklet);
    }

    /* 다음 폴링 예약 (100ms) */
    mod_timer(&dev->poll_timer, jiffies + HZ/10);
}

/* 2. Tasklet 핸들러 */
static void poll_tasklet_func(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, poll_tasklet);

    /* 데이터 처리 (약간 더 복잡한 작업) */
    process_device_data(dev);
}

케이스 4: 지연된 리소스 정리

시나리오: RCU + workqueue로 안전한 메모리 해제

/* 실전 패턴: RCU와 workqueue 조합 */
struct my_object {
    struct rcu_head rcu;
    struct work_struct cleanup_work;
    void *large_buffer;
};

/* 1. 객체 삭제 요청 */
void delete_object(struct my_object *obj)
{
    /* RCU로 readers 보호 */
    call_rcu(&obj->rcu, object_rcu_callback);
}

/* 2. RCU 콜백 (softirq) */
static void object_rcu_callback(struct rcu_head *rcu)
{
    struct my_object *obj = container_of(rcu, struct my_object, rcu);

    /* 무거운 정리 작업은 workqueue로 위임 */
    INIT_WORK(&obj->cleanup_work, cleanup_work_func);
    schedule_work(&obj->cleanup_work);
}

/* 3. Workqueue에서 실제 정리 */
static void cleanup_work_func(struct work_struct *work)
{
    struct my_object *obj =
        container_of(work, struct my_object, cleanup_work);

    /* 슬립 가능한 정리 작업 */
    vfree(obj->large_buffer);  /* 시간이 걸릴 수 있음 */
    kfree(obj);
}

실전 네트워크 드라이버 BH 처리 경로

네트워크 드라이버에서 패킷이 수신되어 프로토콜 스택까지 전달되는 과정의 전체 Bottom Half 경로를 시각화합니다. NIC 인터럽트부터 소켓(Socket) 버퍼(Buffer) 전달까지의 흐름입니다.

네트워크 드라이버 Bottom Half 처리 경로 (RX) Hardirq Top Half ~100ns Softirq NET_RX_SOFTIRQ ~수us Protocol Stack softirq context Process Context userspace NIC Hardware DMA -> RX Ring Buffer IRQ e1000_intr() / ixgbe_msix_clean_rings() IRQ ACK + napi_schedule_prep() + __napi_schedule() raise NET_RX net_rx_action() -> napi_poll() budget=300, driver->poll() 콜백 실행 budget 소진? work >= budget -> ksoftirqd로 재스케줄 napi_gro_receive() GRO 병합 netif_receive_skb_list() RPS 활성화 시 다른 CPU의 softirq로 전달 ip_rcv() -> tcp_v4_rcv() tcp_queue_rcv() -> sk_backlog wake_up(sk->sk_wq) -> recv() XDP/eBPF 경로 (대안) bpf_prog_run_xdp() 드라이버 레벨에서 직접 처리 XDP_DROP: 즉시 폐기 (최고 성능) XDP_TX: 즉시 전송 (반사) XDP_PASS: 일반 경로 진행 네트워크 RX BH 경로 핵심 포인트 1) Top Half(hardirq): 최소 작업 - IRQ ACK + NAPI 스케줄 (~100ns) 2) Softirq(NET_RX): NAPI poll - budget 기반 패킷 처리, GRO 병합 (~수us/패킷) 3) 프로토콜 스택: IP/TCP 처리 - softirq context에서 실행, 소켓 큐에 전달 4) 고성능 대안: XDP/eBPF - 프로토콜 스택 우회, 드라이버 레벨 직접 처리
네트워크 드라이버 RX 패킷 수신 시 Bottom Half 전체 처리 경로

네트워크 드라이버 BH 핵심 코드 분석

/* net/core/dev.c - NET_RX softirq 핸들러 */
static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    unsigned long time_limit = jiffies +
        usecs_to_jiffies(READ_ONCE(netdev_budget_usecs));
    int budget = READ_ONCE(netdev_budget);  /* 기본값 300 */
    LIST_HEAD(list);
    LIST_HEAD(repoll);

    local_irq_disable();
    list_splice_init(&sd->poll_list, &list);
    local_irq_enable();

    for (;;) {
        struct napi_struct *n;

        skb_defer_free_flush(sd);

        if (list_empty(&list)) {
            if (!sd_has_rps_ipi_waiting(sd) &&
                list_empty(&repoll))
                return;
            break;
        }

        n = list_first_entry(&list, struct napi_struct, poll_list);
        budget -= napi_poll(n, &repoll);

        /* 시간 초과 또는 budget 소진 시 중단 */
        if (unlikely(budget <= 0 ||
                     time_after_eq(jiffies, time_limit))) {
            sd->time_squeeze++;
            break;
        }
    }
    /* 남은 NAPI가 있으면 softirq 재스케줄 */
    /* -> __raise_softirq_irqoff(NET_RX_SOFTIRQ) */
}

디버깅 도구와 기법

Bottom Half 관련 문제를 진단하기 위한 도구와 기법을 체계적으로 정리합니다.

/proc/softirqs 분석

# softirq 타입별, CPU별 누적 카운트 확인
cat /proc/softirqs
#                    CPU0       CPU1       CPU2       CPU3
#          HI:          2          0          0          1
#       TIMER:    5123456    4987654    5234567    5123456
#      NET_TX:      12345       9876      11234      10123
#      NET_RX:    9876543    8765432    9123456    8987654
#       BLOCK:     234567     198765     212345     207654
# IRQ_POLL:          0          0          0          0
#     TASKLET:       4567       3456       4321       3987
#       SCHED:    2345678    2234567    2456789    2345678
#     HRTIMER:      12345      11234      13456      12345
#         RCU:    3456789    3345678    3567890    3456789

# 실시간 모니터링 (1초 간격으로 변화량 관찰)
watch -d -n 1 'cat /proc/softirqs'

# CPU 간 불균형 확인 (NET_RX가 한 CPU에 몰리면 RSS 설정 필요)
# softnet_stat으로 추가 통계 확인
cat /proc/net/softnet_stat
# column 1: processed  column 2: dropped  column 3: time_squeeze

ftrace를 이용한 softirq/workqueue 추적

# ftrace 설정: softirq 이벤트 추적
cd /sys/kernel/tracing

# softirq 진입/종료 이벤트 활성화
echo 1 > events/irq/softirq_entry/enable
echo 1 > events/irq/softirq_exit/enable
echo 1 > events/irq/softirq_raise/enable

# workqueue 이벤트 추적
echo 1 > events/workqueue/workqueue_queue_work/enable
echo 1 > events/workqueue/workqueue_execute_start/enable
echo 1 > events/workqueue/workqueue_execute_end/enable

# 추적 시작
echo 1 > tracing_on
sleep 5
echo 0 > tracing_on

# 결과 확인
cat trace | head -50
# <idle>-0  [001] ..s1  1234.567890: softirq_entry: vec=3 [action=NET_RX]
# <idle>-0  [001] ..s1  1234.567925: softirq_exit:  vec=3 [action=NET_RX]
# kworker/1:0-123 [001] ....  1234.568000: workqueue_execute_start: work=...

# softirq 실행 시간 히스토그램 (function_graph 트레이서)
echo function_graph > current_tracer
echo do_softirq > set_graph_function
echo 1 > tracing_on
sleep 2
echo 0 > tracing_on
cat trace

perf를 이용한 성능 분석

# softirq 핫스팟 분석
perf record -e irq:softirq_entry -e irq:softirq_exit -a -g sleep 10
perf report

# softirq 실행 시간 분포
perf script | awk '/softirq_entry/{start=$4} /softirq_exit/{print $4-start}'

# workqueue 실행 빈도
perf stat -e workqueue:workqueue_execute_start -a sleep 10

# 스케줄링 지연 측정 (wakeup latency)
perf sched record sleep 5
perf sched latency --sort max

# IRQ 비활성화 시간 측정 (irqsoff tracer)
echo irqsoff > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace

디버깅용 커널 설정

CONFIG 옵션용도오버헤드
CONFIG_DEBUG_ATOMIC_SLEEPatomic context에서 슬립 시도 감지낮음
CONFIG_PROVE_LOCKINGlockdep: 데드락 가능성 감지높음
CONFIG_DEBUG_OBJECTS_WORKwork_struct 사용 오류 감지중간
CONFIG_WQ_WATCHDOGworkqueue 정체(stall) 감시낮음
CONFIG_SOFTIRQ_DEBUGsoftirq 디버그 정보 강화낮음
CONFIG_FTRACE함수 추적 인프라중간
CONFIG_IRQSOFF_TRACERIRQ 비활성화 시간 추적중간
CONFIG_PREEMPTIRQ_TRACEPOINTS선점/IRQ 비활성화 tracepoints낮음

데드락과 우선순위 역전 패턴

Bottom Half 사용 시 발생하기 쉬운 데드락 및 우선순위 역전 패턴과 방지 방법입니다.

spin_lock vs spin_lock_bh 데드락 시나리오 ❌ 데드락 시나리오 CPU 0 process_context() spin_lock(&lock) ✓ do_something()... ⚡ 인터럽트 발생! 프로세스 선점, softirq 실행 my_softirq() spin_lock(&lock) 영원히 대기! 같은 CPU에서 락 보유자를 선점할 수 없음 DEADLOCK: CPU 0 영구 정지 ✓ 올바른 패턴 CPU 0 process_context() spin_lock_bh(&lock) BH 비활성화 + 락 do_something()... spin_unlock_bh(&lock) BH 재활성화 + 언락 이제 softirq 실행 my_softirq() spin_lock(&lock) ✓ process_data()... 안전: BH가 락 해제 후에만 실행
spin_lock() vs spin_lock_bh() — softirq와 공유 데이터 보호 시 BH 비활성화 필수

패턴 1: softirq와 프로세스 간 spinlock 데드락

/* ❌ 데드락 시나리오:
 * CPU 0에서 process_context()가 spin_lock(&lock) 획득
 * -> 인터럽트 발생 -> softirq에서 spin_lock(&lock) 시도
 * -> 같은 CPU에서 락 보유자를 선점할 수 없어 영원히 대기 */

spinlock_t my_lock;

/* 프로세스 컨텍스트 */
void process_context(void)
{
    spin_lock(&my_lock);     /* ❌ BH 비활성화 없이 락 획득 */
    do_something();
    spin_unlock(&my_lock);
}

/* softirq 컨텍스트 */
void my_softirq(struct softirq_action *h)
{
    spin_lock(&my_lock);     /* 데드락! */
    process_data();
    spin_unlock(&my_lock);
}

/* ✅ 올바른 패턴: spin_lock_bh() 사용 */
void process_context(void)
{
    spin_lock_bh(&my_lock);  /* BH 비활성화 + 락 획득 */
    do_something();
    spin_unlock_bh(&my_lock);
}

void my_softirq(struct softirq_action *h)
{
    spin_lock(&my_lock);     /* BH에서는 spin_lock만으로 충분 */
    process_data();
    spin_unlock(&my_lock);
}

패턴 2: workqueue 중첩 flush 데드락

/* ❌ 데드락 시나리오:
 * work_A가 system_wq에서 실행 중 flush_work(&work_B) 호출
 * work_B도 system_wq에 대기 중이지만 max_active=1이면
 * work_A가 완료될 때까지 work_B 실행 불가 -> 순환 대기 */

static void work_a_func(struct work_struct *work)
{
    do_part1();
    flush_work(&work_b);  /* ❌ 같은 WQ에서 다른 work 대기 */
    do_part2();
}

/* ✅ 올바른 패턴: 별도 workqueue 사용 또는 설계 변경 */
static void work_a_func(struct work_struct *work)
{
    do_part1();
    /* work_b를 다른 workqueue에 큐잉 */
    queue_work(separate_wq, &work_b);
    /* 또는 completion 사용 */
    wait_for_completion(&work_b_done);
    do_part2();
}

패턴 3: softirq 우선순위 역전 (starvation)

/* 우선순위 역전 시나리오:
 * 1. 고우선순위 RT 태스크가 CPU에서 실행 중
 * 2. 네트워크 패킷 도착 -> NET_RX softirq 발생
 * 3. RT 태스크가 선점을 허용하지 않아 softirq 지연
 * 4. 네트워크 패킷 처리가 밀려 드롭 발생 */

/* 해결 방법 1: IRQ affinity 분리 */
/* RT 태스크 CPU와 IRQ 처리 CPU를 분리 */
# echo 0-1 > /proc/irq/<NIC_IRQ>/smp_affinity_list
# taskset -c 2-3 ./rt_application

/* 해결 방법 2: PREEMPT_RT + threaded IRQ */
/* 네트워크 IRQ 스레드 우선순위를 RT 태스크보다 높게 설정 */
# chrt -f -p 90 $(pgrep -f "irq/.*eth0")

/* 해결 방법 3: ksoftirqd 우선순위 조정 */
# chrt -f -p 50 $(pgrep ksoftirqd/0)
데드락 방지 핵심 규칙:
  • 프로세스 컨텍스트에서 softirq와 공유하는 락: 반드시 spin_lock_bh() 사용
  • hardirq와 공유하는 락: 반드시 spin_lock_irqsave() 사용
  • work 내부에서 자신이 속한 workqueue를 flush하지 않기
  • ordered workqueue에서 다른 work를 flush하지 않기
  • CONFIG_PROVE_LOCKING 활성화로 lockdep 검증 필수 실행

추가 실전 케이스: USB와 블록 I/O

USB 드라이버 BH 패턴

USB 드라이버는 URB(USB Request Block) 완료 콜백(Callback)이 인터럽트 컨텍스트에서 호출되므로, 무거운 처리는 workqueue로 위임해야 합니다.

/* USB 완료 콜백 (인터럽트 컨텍스트) */
static void usb_rx_complete(struct urb *urb)
{
    struct my_usb_dev *dev = urb->context;

    switch (urb->status) {
    case 0:  /* 성공 */
        /* 데이터를 버퍼에 복사 (빠른 작업만) */
        memcpy(dev->rx_buf + dev->rx_len,
               urb->transfer_buffer, urb->actual_length);
        dev->rx_len += urb->actual_length;

        /* 무거운 처리는 workqueue로 위임 */
        schedule_work(&dev->rx_work);
        break;
    case -ENOENT:
    case -ECONNRESET:
    case -ESHUTDOWN:
        return;  /* URB 취소됨 */
    default:
        dev_err(&dev->intf->dev, "URB error: %d\n", urb->status);
    }

    /* URB 재제출 (다음 데이터 수신) */
    usb_submit_urb(urb, GFP_ATOMIC);
}

/* workqueue 핸들러 (프로세스 컨텍스트) */
static void usb_rx_work(struct work_struct *work)
{
    struct my_usb_dev *dev = container_of(work,
                                struct my_usb_dev, rx_work);

    mutex_lock(&dev->data_mutex);  /* 슬립 가능 */
    parse_protocol(dev->rx_buf, dev->rx_len);
    deliver_to_userspace(dev);
    dev->rx_len = 0;
    mutex_unlock(&dev->data_mutex);
}

블록 I/O 완료 처리 패턴

/* 블록 I/O 완료 경로: softirq(BLOCK_SOFTIRQ) 또는 IRQ */
/* drivers/block/virtio_blk.c 패턴 */

/* 1. 인터럽트 핸들러 (Top Half) */
static irqreturn_t virtblk_irq(int irq, void *data)
{
    struct virtio_blk_vq *vq = data;

    /* 완료 처리를 위해 BLOCK softirq 스케줄 */
    blk_mq_complete_request(rq);
    return IRQ_HANDLED;
}

/* 2. blk_mq 완료 경로 선택 */
/* blk_mq_complete_request() 내부:
 *   - 같은 CPU에서 완료 가능하면 softirq로 직접 실행
 *   - 다른 CPU면 IPI 전송하여 해당 CPU의 softirq에서 완료
 *   - 목적: 캐시 친화성 최적화 */

/* 3. 완료 콜백 (softirq 또는 프로세스 컨텍스트) */
static void virtblk_done(struct request *rq)
{
    struct virtblk_req *vbr = blk_mq_rq_to_pdu(rq);

    /* 에러 처리 */
    blk_mq_end_request(rq, virtblk_result(vbr));
    /* -> I/O 완료 통지 -> 대기 중인 프로세스 깨움 */
}

블록 I/O BH 처리 경로 다이어그램

블록 I/O 요청이 디스크에 제출되고 완료 인터럽트를 거쳐 프로세스에 통지되기까지의 Bottom Half 처리 경로입니다. NVMe/virtio-blk 드라이버에서 blk-mq 레이어를 거치는 전체 흐름을 보여줍니다.

블록 I/O Bottom Half 처리 경로 Process submit_bio() Hardirq Top Half Softirq BLOCK_SOFTIRQ 또는 IPI Process 완료 통지 프로세스: read()/write() submit_bio() → blk_mq_submit_bio() → 하드웨어 큐 NVMe/virtio IRQ → driver_irq() CQ 엔트리 확인 → blk_mq_complete_request() blk_mq 완료 CPU 판단 같은 CPU BLOCK_SOFTIRQ blk_done_softirq() 다른 CPU IPI 전송 제출 CPU의 softirq로 BLOCK_SOFTIRQ 제출 CPU에서 처리 (캐시 친화) blk_mq_end_request() → bio_endio() I/O 완료 콜백 → end_io() 호출 wake_up_process() → 대기 프로세스 깨움 read()/write() 시스템 콜 반환 핵심: blk_mq는 I/O 제출 CPU에서 완료 처리 시도 → 캐시 친화성 최적화 완료 인터럽트 CPU ≠ 제출 CPU이면 IPI로 원래 CPU의 softirq에서 처리
블록 I/O 요청의 Bottom Half 처리 경로 — blk_mq의 캐시 친화적 완료 처리

문제 해결 FAQ

Bottom Half 사용 시 자주 발생하는 문제와 해결 방법입니다.

Q1: "ksoftirqd가 CPU를 100% 사용합니다"

증상: top에서 ksoftirqd/N이 높은 CPU 사용률

# CPU별 softirq 카운트 확인
watch -n1 'cat /proc/softirqs'

# 특정 softirq가 급증하는지 확인
# NET_RX가 높으면 네트워크 부하, BLOCK이 높으면 I/O 부하

원인: softirq 부하가 너무 높아 ksoftirqd로 처리 위임

해결:

# 1. 네트워크 인터럽트 분산 (RSS/RPS)
ethtool -L eth0 combined 4  # 4개 큐 사용

# 2. IRQ affinity 설정
echo 2 > /proc/irq/<IRQ번호>/smp_affinity_list  # CPU 2에 바인딩

# 3. NAPI budget 조정 (패킷 처리량 제한)
sysctl -w net.core.netdev_budget=300
sysctl -w net.core.netdev_budget_usecs=2000

Q2: tasklet이 실행되지 않습니다

디버깅:

# tasklet 상태 확인 (커널 코드)
printk("tasklet state: %lx\n", my_tasklet.state);
/* TASKLET_STATE_SCHED (0x01): 스케줄됨
   TASKLET_STATE_RUN   (0x02): 실행 중 */

# softirq 통계 확인
cat /proc/softirqs | grep -E 'HI|TASKLET'

가능한 원인:

  1. tasklet_disable() 호출됨 - tasklet_enable() 확인
  2. tasklet_kill() 호출 후 재스케줄 시도
  3. Atomic counter 오류로 state 손상

해결:

/* enable/disable 쌍 확인 */
tasklet_disable(&my_tasklet);
do_something();
tasklet_enable(&my_tasklet);  /* 반드시 호출! */

/* kill 후 재초기화 */
tasklet_kill(&my_tasklet);
tasklet_setup(&my_tasklet, my_func);  /* 재초기화 */

Q3: workqueue가 예상보다 늦게 실행됩니다

증상: schedule_work() 호출 후 수초 지연

디버깅:

# workqueue 상태 확인
cat /sys/kernel/debug/workqueue/workqueues
cat /sys/kernel/debug/workqueue/pool_workqueues

# 대기 중인 work 확인
cat /proc/PID/stack  # kworker PID

원인:

해결:

/* 전용 workqueue 생성 */
struct workqueue_struct *my_wq;

my_wq = alloc_workqueue("my_fast_wq",
                        WQ_HIGHPRI | WQ_UNBOUND,
                        0);  /* 제한 없음 */

queue_work(my_wq, &my_work);  /* system_wq 대신 사용 */

Q4: "BUG: sleeping function called from invalid context" 에러

증상: softirq/tasklet에서 슬립 함수 호출

BUG: sleeping function called from invalid context at kernel/locking/mutex.c:...
in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 0, name: swapper/0

원인: atomic context에서 블로킹 함수 호출

해결:

/* ❌ tasklet에서 mutex */
static void my_tasklet(struct tasklet_struct *t)
{
    mutex_lock(&my_mutex);  /* 에러 발생 */
    do_something();
    mutex_unlock(&my_mutex);
}

/* ✅ workqueue로 변경 */
static void my_work(struct work_struct *work)
{
    mutex_lock(&my_mutex);  /* OK */
    do_something();
    mutex_unlock(&my_mutex);
}

Q5: PREEMPT_RT에서 지연시간이 증가했습니다

증상: CONFIG_PREEMPT_RT 활성화 후 응답 시간 저하

원인: tasklet이 ksoftirqd에서 실행되며 스케줄링 지연 발생

해결:

/* tasklet을 threaded IRQ로 변경 */
int ret = request_threaded_irq(
    irq,
    my_hardirq_handler,  /* Top Half */
    my_thread_fn,         /* Threaded Bottom Half */
    IRQF_ONESHOT,
    "my_device",
    dev);

/* 스레드 우선순위 조정 */
struct sched_param param = { .sched_priority = 50 };
sched_setscheduler(current, SCHED_FIFO, ¶m);

Q6: workqueue flush 시 데드락 발생

증상: flush_workqueue()에서 멈춤

# 스택 트레이스 확인
cat /proc/PID/stack
# [<ffffffff>] flush_workqueue+0x...
# [<ffffffff>] my_cleanup+0x...

원인: work 내부에서 자신이 속한 workqueue flush

해결:

/* ❌ 데드락 패턴 */
static void my_work(struct work_struct *work)
{
    flush_workqueue(system_wq);  /* 데드락! */
}

/* ✅ 올바른 정리 순서 */
void module_exit(void)
{
    /* 1. 새 work 스케줄 중단 */
    shutdown_flag = 1;
    smp_wmb();

    /* 2. pending work 취소 */
    cancel_work_sync(&my_work);

    /* 3. workqueue flush (외부에서) */
    flush_workqueue(my_wq);

    /* 4. workqueue 파괴 */
    destroy_workqueue(my_wq);
}
디버깅 도구:
  • /proc/softirqs - softirq 통계
  • /sys/kernel/debug/workqueue/ - workqueue 상태
  • ftrace - irq:softirq_entry/exit 추적
  • perf - 성능 병목(Bottleneck) 분석
  • CONFIG_DEBUG_ATOMIC_SLEEP - atomic context 검증

Threaded IRQ

Threaded IRQ는 인터럽트 핸들러(Handler)의 Bottom Half를 전용 커널 스레드에서 실행하는 메커니즘입니다. request_threaded_irq()를 통해 hardirq 핸들러(Top Half)와 스레드 함수(Bottom Half)를 분리하여, 프로세스 컨텍스트의 이점(슬립, 잠금 획득)을 활용할 수 있습니다.

request_threaded_irq() 핵심 패턴

#include <linux/interrupt.h>

/* Top Half: 최소한의 하드웨어 확인만 수행 */
static irqreturn_t my_hardirq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* 인터럽트 소스 확인 */
    if (!my_device_irq_pending(dev))
        return IRQ_NONE;  /* 공유 IRQ: 내 장치가 아님 */

    /* 하드웨어 인터럽트 비활성화 (level-triggered 필수) */
    my_device_disable_irq(dev);

    return IRQ_WAKE_THREAD;  /* 스레드 함수 깨우기 */
}

/* Bottom Half: 프로세스 컨텍스트에서 실행 */
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* 슬립 가능! mutex, I/O 등 자유롭게 사용 */
    mutex_lock(&dev->lock);
    my_device_process_data(dev);
    mutex_unlock(&dev->lock);

    /* 하드웨어 인터럽트 재활성화 */
    my_device_enable_irq(dev);

    return IRQ_HANDLED;
}

/* 등록 */
request_threaded_irq(irq, my_hardirq_handler, my_thread_fn,
                     IRQF_ONESHOT | IRQF_SHARED,
                     "my_device", dev);

핵심 플래그와 동작

플래그의미필수 여부
IRQF_ONESHOT스레드 함수 완료 시까지 IRQ 라인 비활성화 유지level-triggered에서 필수
IRQF_SHARED여러 디바이스가 동일 IRQ 라인 공유공유 시 필수
IRQF_NO_THREADforce_irqthreads 커널 파라미터에서도 스레드화 면제타이머 등 특수 경우
IRQF_NO_AUTOEN등록 시 자동 활성화 방지 (수동 enable_irq() 필요)초기화 순서 제어
IRQF_ONESHOT 필수 시나리오: level-triggered 인터럽트에서 IRQF_ONESHOT 없이 hardirq가 IRQ_WAKE_THREAD를 반환하면, 스레드가 실행되기 전에 같은 인터럽트가 다시 발생하여 무한 루프에 빠질 수 있습니다. edge-triggered에서는 선택적이지만, 안전을 위해 항상 사용하는 것을 권장합니다.

Threaded IRQ가 기존 BH보다 나은 시나리오

시나리오Threaded IRQ 장점기존 BH (softirq/tasklet)
I2C/SPI 디바이스슬립 가능한 버스(Bus) 접근 가능불가 (atomic context)
PREEMPT_RT 환경네이티브 지원, 우선순위 제어 가능강제 스레드화 (오버헤드)
복잡한 프로토콜 처리mutex, 메모리 할당 자유spin_lock만 가능
디버깅스택 트레이스 명확, ps로 확인ksoftirqd 내부로 합쳐짐
전력 관리유휴 시 슬립, CPU 부하 최소폴링 기반 처리 경향
Threaded IRQ 실행 타임라인 HW IRQ hardirq thread IRQ line IRQ handler() IRQ_WAKE_THREAD wake thread_fn() - 슬립 가능 IRQF_ONESHOT: IRQ 라인 비활성 unmask 시간 (time)
참고: Threaded IRQ의 기본 개념과 사용법에 대한 내용은 인터럽트 문서의 Threaded IRQ 섹션을 참고하세요.

WQ_BH: Workqueue 기반 Bottom Half (6.9+)

커널 6.9부터 도입된 WQ_BH 플래그는 workqueue를 softirq 컨텍스트에서 실행하면서도 workqueue API의 편의성을 유지합니다. 이는 tasklet의 공식 대체 메커니즘으로, tasklet deprecation을 가속화하기 위해 설계되었습니다.

도입 배경

tasklet은 오랫동안 deprecation 대상이었지만, softirq 컨텍스트 실행이 필요한 드라이버에서 직접 softirq를 등록할 수 없어 대안이 없었습니다. WQ_BH는 이 문제를 해결합니다.

특성taskletWQ_BH workqueue
실행 컨텍스트softirq (TASKLET_SOFTIRQ)softirq (BH workqueue)
APItasklet_schedule()/tasklet_kill()queue_work()/cancel_work_sync()
동시성 제어같은 tasklet 직렬화work 단위 직렬화
취소tasklet_kill() (복잡한 경쟁 조건)cancel_work_sync() (명확한 의미)
디버깅제한적workqueue 디버깅 인프라 활용
PREEMPT_RT강제 스레드화통합된 스레드화 경로

system_bh_wq 사용법

#include <linux/workqueue.h>

static struct work_struct my_bh_work;

static void my_bh_work_fn(struct work_struct *work)
{
    /* softirq 컨텍스트에서 실행됨 — 슬립 불가! */
    process_pending_data();
}

/* 초기화 */
INIT_WORK(&my_bh_work, my_bh_work_fn);

/* 스케줄링 — system_bh_wq는 WQ_BH 플래그가 설정된 전역 workqueue */
queue_work(system_bh_wq, &my_bh_work);

/* 정리 */
cancel_work_sync(&my_bh_work);

tasklet에서 WQ_BH로 마이그레이션

/* === 변환 전: tasklet === */
static struct tasklet_struct my_tasklet;

static void my_tasklet_fn(unsigned long data)
{
    struct my_device *dev = (struct my_device *)data;
    process_data(dev);
}

tasklet_init(&my_tasklet, my_tasklet_fn, (unsigned long)dev);
tasklet_schedule(&my_tasklet);      /* 스케줄링 */
tasklet_kill(&my_tasklet);            /* 정리 */

/* === 변환 후: WQ_BH === */
static struct work_struct my_bh_work;

static void my_bh_work_fn(struct work_struct *work)
{
    struct my_device *dev = container_of(work, struct my_device, bh_work);
    process_data(dev);
}

INIT_WORK(&dev->bh_work, my_bh_work_fn);
queue_work(system_bh_wq, &dev->bh_work);  /* 스케줄링 */
cancel_work_sync(&dev->bh_work);          /* 정리 */
마이그레이션 핵심: tasklet_structwork_struct, tasklet_schedule()queue_work(system_bh_wq, ...), tasklet_kill()cancel_work_sync(). 콜백 시그니처가 (unsigned long)에서 (struct work_struct *)로 변경되므로 container_of() 패턴을 사용합니다.

커널 6.x Bottom Half 변경사항

커널 6.x 시리즈에서 Bottom Half 서브시스템은 상당한 변화를 겪고 있습니다. tasklet deprecation이 본격화되고, workqueue에 새로운 기능이 추가되며, per-CPU 처리 모델이 개선되고 있습니다.

주요 변경 타임라인

커널 버전변경사항영향
5.15tasklet API에 deprecation 경고 추가새 드라이버에서 tasklet 사용 자제 권고
6.3Workqueue affinity_scope 준비 작업NUMA 인식 worker 배치 기반 마련
6.5affinity_scope 도입 (system/pod/node/cpu/cache)NUMA 토폴로지(Topology) 기반 worker 배치 최적화
6.7Workqueue BH 실행 준비 (WQ_BH 프레임워크)softirq 컨텍스트 workqueue 인프라 구축
6.9WQ_BH 도입tasklet 대체 공식 메커니즘 확립
6.10+네트워킹 서브시스템 tasklet → WQ_BH 변환 진행주요 드라이버 마이그레이션 시작

Workqueue affinity_scope (6.5)

/* affinity_scope 레벨:
 *   WQ_AFFN_SYSTEM  — 시스템 전체 (기존 동작)
 *   WQ_AFFN_NODE    — NUMA 노드 단위
 *   WQ_AFFN_CPU     — 단일 CPU 단위
 *   WQ_AFFN_CACHE   — 캐시 공유 단위 (L3 등)
 *   WQ_AFFN_DFL     — 기본값 (unbound: cache, bound: cpu)
 */

/* sysfs를 통한 런타임 조정 */
/* /sys/devices/virtual/workqueue/<wq_name>/affinity_scope */

/* 코드에서 직접 설정 */
struct workqueue_struct *wq;
wq = alloc_workqueue("my_wq", WQ_UNBOUND, 0);
/* 기본적으로 WQ_AFFN_CACHE가 적용됨 */

tasklet deprecation 현황

Deprecation 진행 상태 (6.x 기준):
  • 완료: 일부 네트워크 드라이버 (e1000e, igb 일부), USB 일부, 암호화(Encryption) 드라이버
  • 진행 중: 네트워킹 코어 (NET_TX_SOFTIRQ/NET_RX_SOFTIRQ는 softirq 유지), 블루투스
  • 미정: SCSI 일부, 레거시 사운드 드라이버, 아키텍처별 코드

tasklet은 아직 완전히 제거되지 않았으며, 당분간 유지될 예정입니다. 그러나 새 코드에서는 반드시 WQ_BH 또는 workqueue를 사용해야 합니다.

Bottom Half 진화 타임라인 2.4 BH (old) 2.6 softirq + tasklet + workqueue 3.x-4.x CMWQ 도입 threaded IRQ 확산 5.x tasklet deprecation 시작 6.x WQ_BH 도입 affinity_scope old BH (2.4-) 32개 고정 슬롯 전역 잠금, 낮은 확장성 softirq/tasklet per-CPU 병렬 실행 ksoftirqd 위임 CMWQ + threaded 통합 worker pool 프로세스 컨텍스트 BH WQ_BH 시대 tasklet 단계적 제거 통합 BH 프레임워크 권장 방향: softirq(커널 전용) | workqueue/WQ_BH(드라이버) | threaded IRQ(인터럽트) tasklet: deprecated (5.15+) → WQ_BH 전환 진행 중 (6.9+)

Bottom Half 지연 벤치마크

Bottom Half 메커니즘 선택 시 가장 중요한 고려사항 중 하나는 지연(latency)과 처리량(throughput)의 트레이드오프입니다. 각 메커니즘의 실측 특성을 이해하면 적절한 선택을 할 수 있습니다.

측정 방법론

/* ftrace를 이용한 BH 지연 측정 */
# softirq 진입/종료 추적
echo 1 > /sys/kernel/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/tracing/events/irq/softirq_exit/enable

# workqueue 실행 추적
echo 1 > /sys/kernel/tracing/events/workqueue/workqueue_execute_start/enable
echo 1 > /sys/kernel/tracing/events/workqueue/workqueue_execute_end/enable

# BPF 기반 정밀 측정 (bpftrace)
bpftrace -e '
tracepoint:irq:softirq_entry { @start[tid] = nsecs; }
tracepoint:irq:softirq_exit /@start[tid]/ {
    @latency_ns = hist(nsecs - @start[tid]);
    delete(@start[tid]);
}
'

# cyclictest: 실시간 지연 측정
cyclictest --mlockall --smp -p99 -i200 -D60
/* --smp: 모든 CPU 측정, -p99: RT 우선순위 99
 * -i200: 200us 간격, -D60: 60초 실행 */

메커니즘별 벤치마크 비교

측정 환경: x86_64, 4코어, 3.5GHz, 커널 6.8, PREEMPT_DYNAMIC (voluntary), 유휴 시스템. 실제 지연은 시스템 부하, 아키텍처, PREEMPT 설정에 따라 크게 달라집니다.
메커니즘스케줄링 지연 (평균)스케줄링 지연 (p99)컨텍스트 전환 비용처리량 (ops/sec)메모리 오버헤드
softirq~0.5us~2us없음 (인터럽트 반환 시)~2,000,000최소 (per-CPU 변수)
tasklet~0.7us~3us없음 (softirq 기반)~1,500,000~64B (tasklet_struct)
WQ_BH~0.8us~4us없음 (softirq 기반)~1,200,000~128B (work_struct+WQ)
workqueue (bound)~3us~15us1회 (kworker 스케줄링)~500,000~128B + worker thread
workqueue (unbound)~5us~25us1회 + NUMA 이동 가능~400,000~128B + worker pool
threaded IRQ~4us~20us1회 (전용 스레드)~300,000~2KB (커널 스레드 스택)

처리량 vs 지연시간 트레이드오프

요구사항권장 메커니즘근거
최대 처리량 (네트워크 패킷)softirq (NAPI)컨텍스트 전환 없이 인터럽트 반환 시 즉시 실행
낮은 지연 + 슬립 필요threaded IRQ전용 스레드로 빠른 깨움, RT 우선순위 설정 가능
균형잡힌 성능workqueue (WQ_HIGHPRI)높은 우선순위 worker pool, 슬립 가능
대량 비동기 작업workqueue (WQ_UNBOUND)CPU 바운드 아닌 작업에 최적, 자동 확장
PREEMPT_RT 예측 가능성threaded IRQ결정론적 우선순위 스케줄링, 지연 보장
벤치마크 주의사항: 위 수치는 유휴 시스템 기준입니다. 부하가 높은 환경에서는 softirq가 ksoftirqd로 위임되어 지연이 크게 증가할 수 있고, workqueue는 worker pool 크기에 따라 대기 시간(Latency)이 달라집니다. 반드시 실제 워크로드로 측정하세요.

irq_work: NMI/hardirq에서 지연 실행

irq_work는 NMI(Non-Maskable Interrupt)나 hardirq 컨텍스트에서도 안전하게 콜백을 지연 실행할 수 있는 유일한 메커니즘입니다. softirq조차 NMI에서는 안전하지 않지만, irq_work는 IPI(Inter-Processor Interrupt)나 self-IPI를 통해 안전하게 작업을 위임합니다.

irq_work API

#include <linux/irq_work.h>

static void my_irq_work_handler(struct irq_work *work)
{
    /* hardirq 컨텍스트에서 실행됨 */
    /* NMI-safe가 보장되어야 하는 작업 수행 */
    do_deferred_output();
}

static struct irq_work my_work = IRQ_WORK_INIT_HARD(my_irq_work_handler);

/* NMI 또는 hardirq에서 호출 가능 */
void nmi_handler(void)
{
    /* 직접 처리하면 데드락 위험 → irq_work로 위임 */
    irq_work_queue(&my_work);
}

/* Lazy 모드: softirq 시점에 실행 (지연 허용) */
static struct irq_work lazy_work = IRQ_WORK_INIT_LAZY(my_lazy_handler);
irq_work_queue(&lazy_work);  /* 다음 tick/softirq에서 실행 */

주요 사용 시나리오

사용처이유코드 경로
printk (NMI 컨텍스트)NMI에서 직접 콘솔 출력 불가 (락 충돌)printk_deferred()irq_work_queue()
perf 이벤트 출력NMI 기반 PMU 오버플로에서 링 버퍼(Ring Buffer) 기록perf_output_end() → wakeup via irq_work
RCU 콜백 처리RCU grace period 알림을 안전하게 전파rcu_report_qs_rdp()
tick 브로드캐스트다른 CPU의 tick 재개 요청tick_broadcast_exit()
ftrace 출력트레이스 버퍼 플러시(Flush)를 안전한 컨텍스트로 위임trace_wake_up()

다른 BH 메커니즘과의 차이

특성irq_worksoftirqtaskletworkqueue
NMI 안전가능불가불가불가
실행 컨텍스트hardirqsoftirqsoftirqprocess
슬립 가능불가불가불가가능
실행 시점다음 hardirq 반환 또는 IPI인터럽트 반환 시인터럽트 반환 시스케줄러(Scheduler) 결정
CPU 지정큐잉된 CPU 또는 원격 CPUraise된 CPU스케줄된 CPUworker pool
irq_work 실행 경로 NMI / hardirq irq_work_queue() IRQ_WORK_LAZY? No (HARD) self-IPI arch_irq_work_raise() hardirq 컨텍스트 handler() 실행 Yes (LAZY) softirq 대기열 다음 tick/softirq softirq 컨텍스트 handler() 실행 HARD: 즉시 IPI로 실행 LAZY: 다음 softirq에서 실행 (전력 절약)
주의: irq_work는 hardirq 컨텍스트에서 실행되므로 슬립 불가하며, 최소한의 작업만 수행해야 합니다. 대량의 데이터 처리가 필요하면 irq_work에서 workqueue로 위임하는 2단계 패턴을 사용하세요.

Per-CPU Bottom Half 패턴

리눅스 커널의 Bottom Half 처리는 근본적으로 per-CPU 모델을 기반으로 합니다. softirq는 각 CPU에서 독립적으로 실행되고, workqueue의 bound 모드도 per-CPU worker pool을 사용합니다. 이 특성을 올바르게 활용하면 캐시 친화적이고 잠금 없는 고성능 BH 처리가 가능합니다.

Per-CPU Bottom Half 데이터 흐름 CPU 0 hardirq handler raise_softirq(NET_RX) pending 비트 설정 __softirq_pending[CPU0] = 0x08 __do_softirq() net_rx_action() 호출 this_cpu_ptr(&softnet_data) per-CPU 데이터 softnet_data (poll_list) per-CPU 통계, 큐 kworker/0:N (bound WQ) CPU 1 hardirq handler raise_softirq(NET_RX) __softirq_pending[CPU1] = 0x08 __do_softirq() net_rx_action() 호출 this_cpu_ptr(&softnet_data) per-CPU 데이터 softnet_data (poll_list) per-CPU 통계, 큐 kworker/1:N (bound WQ) 병렬 실행 같은 softirq 타입이 동시에 여러 CPU에서 실행 가능! 데이터 격리 per-CPU 데이터 접근 시 락 불필요 (local_bh_disable 충분) 공유 데이터 CPU 간 공유 데이터 접근 시 반드시 spin_lock 필요! 보호 규칙 요약 같은 CPU per-CPU 데이터: local_bh_disable() 충분 다른 CPU 데이터/전역 데이터: spin_lock 필수
Per-CPU Bottom Half 데이터 흐름 — 같은 softirq가 여러 CPU에서 독립적으로 병렬 실행

per-CPU softirq 처리의 의미

raise_softirq()가 호출된 CPU에서만 해당 softirq가 실행됩니다. 같은 유형의 softirq가 다른 CPU에서 동시에 실행될 수 있으므로, 공유 데이터에는 반드시 동기화가 필요합니다.

/* per-CPU softirq 처리 구조 (kernel/softirq.c) */
DEFINE_PER_CPU(__u32, __softirq_pending);

void __do_softirq(void)
{
    __u32 pending = local_softirq_pending();  /* 현재 CPU의 pending 비트 */
    int softirq_bit;

    while ((softirq_bit = ffs(pending))) {
        struct softirq_action *h = softirq_vec + softirq_bit - 1;
        h->action(h);  /* 해당 softirq 핸들러 실행 */
        pending >>= softirq_bit;
    }
}

per-CPU workqueue 패턴

/* Bound workqueue: per-CPU worker pool 사용 */
struct workqueue_struct *bound_wq;
bound_wq = alloc_workqueue("my_bound_wq", 0, 0);
/* WQ_UNBOUND가 없으면 per-CPU (bound) workqueue */

/* 현재 CPU의 worker에서 실행 */
queue_work(bound_wq, &my_work);

/* vs. Unbound workqueue: 시스템이 CPU 선택 */
struct workqueue_struct *unbound_wq;
unbound_wq = alloc_workqueue("my_unbound_wq", WQ_UNBOUND, 0);
/* 어떤 CPU에서든 실행될 수 있음 — NUMA 인식 배치 */

local_bh_disable/enable의 실제 동작

/* local_bh_disable(): preempt_count에 SOFTIRQ_DISABLE_OFFSET 추가 */
static inline void local_bh_disable(void)
{
    __local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

/* preempt_count 비트 레이아웃:
 * [0..7]   : preempt count (선점 깊이)
 * [8..15]  : softirq count (BH 비활성 깊이)
 * [16..19] : hardirq count (하드웨어 인터럽트 깊이)
 * [20]     : NMI
 */

/* 사용 패턴: per-CPU 데이터 보호 */
local_bh_disable();
/* 이 구간에서는 softirq/tasklet이 현재 CPU에서 실행되지 않음 */
/* per-CPU 데이터 안전하게 접근 가능 */
this_cpu_inc(my_per_cpu_counter);
local_bh_enable();
/* enable 시점에 pending softirq가 있으면 즉시 실행 */

per-CPU 안전한 BH 패턴

/* 패턴 1: per-CPU 통계 수집 (락 없음) */
DEFINE_PER_CPU(struct my_stats, pcpu_stats);

static void my_softirq_handler(struct softirq_action *a)
{
    struct my_stats *stats = this_cpu_ptr(&pcpu_stats);
    stats->packets++;  /* 안전: softirq는 per-CPU 실행 */
    stats->bytes += len;
}

/* 패턴 2: per-CPU 큐 + BH 처리 */
DEFINE_PER_CPU(struct list_head, pcpu_queue);
DEFINE_PER_CPU(spinlock_t, pcpu_lock);

/* hardirq에서: 큐에 추가 */
void irq_enqueue(struct work_item *item)
{
    spinlock_t *lock = this_cpu_ptr(&pcpu_lock);
    spin_lock(lock);
    list_add_tail(&item->node, this_cpu_ptr(&pcpu_queue));
    spin_unlock(lock);
    raise_softirq(MY_SOFTIRQ);
}

/* softirq에서: 큐 처리 */
void my_softirq_action(struct softirq_action *a)
{
    LIST_HEAD(local_list);
    spinlock_t *lock = this_cpu_ptr(&pcpu_lock);

    spin_lock(lock);
    list_splice_init(this_cpu_ptr(&pcpu_queue), &local_list);
    spin_unlock(lock);

    /* local_list 처리 — 락 없이 안전 */
    process_items(&local_list);
}
핵심 원칙: softirq와 tasklet은 per-CPU로 실행되므로, this_cpu_ptr()로 접근하는 per-CPU 데이터는 local_bh_disable()만으로 충분히 보호됩니다. 다른 CPU의 데이터에 접근할 때만 명시적 잠금이 필요합니다.

Bottom Half 중첩과 재진입

Bottom Half 메커니즘은 각각 다른 중첩(nesting)과 재진입(reentrancy) 규칙을 가집니다. 이를 정확히 이해하지 못하면 데이터 손상이나 데드락이 발생할 수 있습니다.

메커니즘별 재진입 규칙

메커니즘같은 CPU 재진입다른 CPU 동시 실행중복 큐잉
softirq불가 (같은 호출에서 재귀 금지)가능 (같은 타입 동시 실행)pending 비트 방식 (멱등)
tasklet불가불가 (TASKLET_STATE_RUN 보호)스케줄 중이면 무시
workqueue불가불가 (같은 work 동시 실행 방지)WORK_STRUCT_PENDING 보호
threaded IRQ불가불가 (스레드당 1개)해당 없음
irq_work불가 (IRQ_WORK_BUSY)불가 (같은 work)busy이면 무시

softirq 재진입 상세

/* softirq의 핵심 특성: 같은 타입이 다른 CPU에서 동시 실행 가능 */

/* CPU 0 */                         /* CPU 1 */
net_rx_action()                   net_rx_action()
  |-> process_backlog()             |-> process_backlog()
  |-> /* 공유 데이터 접근 시 */    |-> /* 동시에 실행 중! */
  |-> spin_lock(&shared_lock)       |-> spin_lock(&shared_lock)
  |-> /* ... */                      |-> /* 대기 */

/* 반면 같은 CPU에서의 softirq 중첩은 불가:
 * __do_softirq()는 in_serving_softirq() 체크로 재귀 방지
 * 단, __do_softirq() 실행 중 새 softirq가 raise되면
 * MAX_SOFTIRQ_RESTART(10)까지 반복 후 ksoftirqd로 위임 */

preempt_count와 BH 깊이 추적

preempt_count는 32비트 정수로, 현재 실행 컨텍스트를 비트 필드로 추적합니다. 이 값을 통해 커널은 현재 hardirq, softirq, 또는 선점 비활성 상태인지를 판단합니다.

preempt_count 비트 레이아웃 (32비트) NMI bit 20 1비트 HARDIRQ_COUNT bits 16-19 4비트 (최대 15중첩) SOFTIRQ_COUNT bits 8-15 8비트 (BH disable 깊이) PREEMPT_COUNT bits 0-7 8비트 (선점 disable 깊이) 31 20 19 16 15 8 7 0 컨텍스트 확인 매크로와 비트 관계 in_nmi() → NMI 비트 확인 in_irq() → HARDIRQ_COUNT > 0 in_softirq() → SOFTIRQ_COUNT > 0 in_interrupt() → HARDIRQ | SOFTIRQ in_task() → 전체가 0 in_serving_softirq() → 실행 중 플래그 local_bh_disable/enable 동작 local_bh_disable() → SOFTIRQ_COUNT += 0x200 local_bh_enable() → SOFTIRQ_COUNT -= 0x200
preempt_count 비트 레이아웃 — 커널이 현재 실행 컨텍스트를 추적하는 핵심 메커니즘
/* 컨텍스트 확인 매크로 */
in_irq()          /* hardirq 컨텍스트인가? */
in_softirq()      /* softirq 컨텍스트인가? (BH disabled 포함) */
in_serving_softirq() /* 실제 softirq 핸들러 실행 중인가? */
in_interrupt()    /* hardirq 또는 softirq 컨텍스트인가? */
in_task()         /* 프로세스 컨텍스트인가? */
in_nmi()          /* NMI 컨텍스트인가? */

/* BH disable 중첩 카운팅 */
local_bh_disable();  /* softirq_count += SOFTIRQ_DISABLE_OFFSET */
local_bh_disable();  /* 중첩 가능: softirq_count 증가 */
/* ... */
local_bh_enable();   /* softirq_count 감소 */
local_bh_enable();   /* 0이 되면 pending softirq 실행 */

중첩 시나리오별 동작 차이

시나리오동작결과
hardirq에서 softirq raisepending 비트 설정, hardirq 반환 시 실행정상 동작
softirq에서 같은 softirq raisepending 비트 설정, 현재 라운드 후 재실행MAX_SOFTIRQ_RESTART까지 반복
softirq에서 tasklet_scheduleTASKLET_SOFTIRQ에 추가현재 softirq 라운드 후 실행
tasklet에서 같은 tasklet schedule이미 RUN 상태이므로 다음 라운드로 연기안전 (직렬화 보장)
workqueue work에서 같은 work 큐잉PENDING 플래그로 거부무시됨 (중복 방지)
process 컨텍스트에서 local_bh_disablepreempt_count 증가, softirq 억제enable 시 pending 처리
흔한 실수: softirq가 다른 CPU에서 동시 실행될 수 있다는 것을 잊고 전역 데이터에 잠금 없이 접근하면 데이터 손상이 발생합니다. tasklet은 이 문제를 방지하기 위해 같은 tasklet의 동시 실행을 금지하지만, 그만큼 확장성이 떨어집니다.

Bottom Half에서의 메모리 할당

Bottom Half 컨텍스트에서의 메모리 할당은 실행 환경에 따라 사용 가능한 GFP 플래그가 제한됩니다. 잘못된 플래그 사용은 커널 경고(BUG)나 데드락을 일으킵니다.

Bottom Half 컨텍스트별 메모리 할당 결정 트리 프로세스 컨텍스트인가? Yes (workqueue/threaded IRQ) GFP_KERNEL 사용 kmalloc(size, GFP_KERNEL) 슬립 가능, direct reclaim 가능 No (softirq/tasklet/WQ_BH) 빈번한 할당인가? Yes mempool 또는 사전 할당 mempool_alloc(pool, GFP_ATOMIC) 실패 확률 최소화, per-CPU 캐시 활용 No GFP_ATOMIC 사용 kmalloc(size, GFP_ATOMIC) 슬립 불가, 실패 시 NULL 반환 할당 실패 시 복구 전략 1) schedule_work()로 workqueue 위임 2) 패킷 드롭 + 재시도 예약 GFP_KERNEL 옵션 GFP_NOIO: I/O 없는 할당 (블록 드라이버) GFP_NOFS: 파일시스템 재진입 방지
Bottom Half 컨텍스트별 메모리 할당 전략 결정 트리

컨텍스트별 허용 GFP 플래그

컨텍스트GFP_ATOMICGFP_KERNELGFP_NOIO__GFP_DIRECT_RECLAIM슬립
hardirq가능불가불가불가불가
softirq가능불가불가불가불가
tasklet가능불가불가불가불가
WQ_BH가능불가불가불가불가
workqueue가능가능가능가능가능
threaded IRQ가능가능가능가능가능

GFP_ATOMIC 사용 패턴

/* softirq/tasklet에서의 메모리 할당 */
static void my_softirq_handler(struct softirq_action *a)
{
    struct my_buffer *buf;

    /* GFP_ATOMIC: 슬립 없이 즉시 반환 (실패 가능) */
    buf = kmalloc(sizeof(*buf), GFP_ATOMIC);
    if (!buf) {
        /* 할당 실패 처리 — 반드시 대비해야 함! */
        pr_warn("BH alloc failed, deferring\n");
        schedule_work(&retry_work);  /* workqueue로 재시도 위임 */
        return;
    }
    process_with_buffer(buf);
    kfree(buf);
}

/* Slab 캐시에서의 BH 할당 */
struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_cache", sizeof(struct my_obj),
                              0, SLAB_HWCACHE_ALIGN, NULL);

/* softirq에서 */
obj = kmem_cache_alloc(my_cache, GFP_ATOMIC);
/* ... 사용 ... */
kmem_cache_free(my_cache, obj);

메모리 부족 시 BH 복구 전략

/* 전략 1: 사전 할당 풀 (mempool) */
struct mempool_s *pool;
pool = mempool_create_slab_pool(16, my_cache);
/* 최소 16개 보장 — GFP_ATOMIC에서도 안전 */

obj = mempool_alloc(pool, GFP_ATOMIC);  /* 거의 항상 성공 */

/* 전략 2: per-CPU 프리리스트 */
DEFINE_PER_CPU(struct list_head, free_list);

/* 프로세스 컨텍스트에서 미리 채움 */
void refill_free_list(void)
{
    struct my_obj *obj;
    int cpu;
    for_each_possible_cpu(cpu) {
        obj = kmalloc(sizeof(*obj), GFP_KERNEL);
        list_add(&obj->node, per_cpu_ptr(&free_list, cpu));
    }
}

/* softirq에서: 할당 없이 사용 */
obj = list_first_entry(this_cpu_ptr(&free_list), struct my_obj, node);
list_del(&obj->node);

/* 전략 3: 2단계 처리 (할당 실패 시 workqueue 위임) */
if (!buf) {
    /* 데이터를 임시 버퍼에 저장하고 workqueue에서 재처리 */
    queue_work(system_wq, &fallback_work);
}
모범 사례: BH에서 빈번한 메모리 할당이 필요하면 mempool 또는 per-CPU 프리리스트를 사용하세요. GFP_ATOMIC 할당 실패는 시스템 메모리가 부족할 때 자주 발생하며, 네트워크 패킷 폭주 상황에서 특히 위험합니다.

실시간 시스템을 위한 Bottom Half 전략

PREEMPT_RT 커널에서는 Bottom Half 메커니즘이 크게 변환됩니다. softirq와 tasklet이 강제로 스레드화되어 결정론적 스케줄링이 가능해지지만, 지연 특성과 우선순위 관리를 정확히 이해해야 합니다.

PREEMPT_RT 변환 요약

메커니즘일반 커널PREEMPT_RT 커널RT 영향
softirq인터럽트 반환 시 실행ksoftirqd 스레드에서 실행선점 가능, 우선순위 제어
taskletsoftirq 컨텍스트전용 스레드로 변환개별 우선순위 설정
local_bh_disablesoftirq 실행 억제per-CPU 락으로 변환우선순위 역전 가능
spin_lock_bhspinlock + BH disablert_mutex + BH 동기화슬립 가능, PI 지원
workqueue변경 없음변경 없음이미 프로세스 컨텍스트
threaded IRQ변경 없음변경 없음RT 네이티브

IRQ 스레드 우선순위 설정

# IRQ 스레드 목록 확인
ps -eo pid,cls,rtprio,ni,comm | grep irq/

# 특정 IRQ 스레드 우선순위 변경 (SCHED_FIFO)
chrt -f -p 90 $(pgrep -f "irq/42-my_dev")

# /proc 인터페이스로 IRQ 친화도 설정
echo 2 > /proc/irq/42/smp_affinity  # CPU 1에 고정

# 런타임에 IRQ 스레드 우선순위 확인
cat /proc/42/sched | grep policy
# policy: SCHED_FIFO, prio: 90

# ksoftirqd 우선순위 변경 (주의 필요)
chrt -f -p 50 $(pgrep "ksoftirqd/0")
# 네트워킹/타이머 softirq에 영향 — 낮추면 네트워크 지연 증가

isolcpus + nohz_full과 BH 상호작용

# 커널 부팅 파라미터 예시
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3

/* isolcpus 영향:
 * - 격리된 CPU에서는 unbound workqueue worker가 실행되지 않음
 * - 그러나 bound workqueue와 softirq는 해당 CPU에서 여전히 실행 가능
 * - IRQ를 해당 CPU로 라우팅하면 BH도 그 CPU에서 실행
 */

/* nohz_full 영향:
 * - tick 없는 실행 (최소 인터럽트)
 * - 단 softirq가 raise되면 tick이 재개됨
 * - timer softirq는 다른 CPU에서 원격 처리 가능 (6.x)
 */

/* rcu_nocbs 영향:
 * - RCU 콜백을 전용 스레드 (rcuog/rcuop)로 오프로드
 * - 격리 CPU의 BH 부하 대폭 감소
 */

/* RT 워크로드 권장 CPU 배치:
 * CPU 0-1: 일반 작업 + softirq/ksoftirqd + kworker
 * CPU 2-3: RT 작업 전용 (isolcpus)
 *          - IRQ 친화도로 인터럽트 배제
 *          - nohz_full로 tick 제거
 *          - rcu_nocbs로 RCU 오프로드
 */

실시간 워크로드별 BH 선택 가이드

워크로드지연 요구권장 BH우선순위 가이드
모션 컨트롤러< 50usthreaded IRQSCHED_FIFO 95+
오디오 처리< 1msthreaded IRQSCHED_FIFO 80-90
네트워크 패킷 처리< 5mssoftirq (RT에서 스레드화)ksoftirqd SCHED_FIFO 50
디스크 I/O 완료< 10msworkqueuekworker 기본 (SCHED_OTHER)
로그/모니터링제한 없음workqueue (WQ_UNBOUND)SCHED_OTHER, nice 10+
PREEMPT_RT 커널 BH 처리 스택 우선순위 높음 낮음 hardirq (비스레드화 — 최소한) IRQF_NO_THREAD | 타이머 인터럽트 | IPI IRQ 스레드 (SCHED_FIFO 50-99) threaded IRQ | force_irqthreads로 스레드화된 IRQ ksoftirqd (SCHED_FIFO) NET_RX/TX, BLOCK, TIMER tasklet 스레드 (RT) 개별 우선순위 설정 가능 kworker 스레드 (SCHED_OTHER) workqueue | 일반 지연 작업 RT 사용자 태스크 SCHED_FIFO/RR (응용) 일반 사용자 태스크 SCHED_OTHER RT 커널: 모든 BH가 선점 가능 → 결정론적 스케줄링
PREEMPT_RT 상세: PREEMPT_RT에서의 spinlock 변환, PI(Priority Inheritance), 전체 스레드화 모델에 대한 상세 내용은 Real-Time Linux & PREEMPT_RT 문서를 참고하세요.

일반적인 실수와 올바른 패턴

Bottom Half 사용 시 자주 발생하는 실수와 올바른 접근 방법을 비교합니다.

❌ 실수 1: softirq/tasklet에서 슬립 시도

/* 잘못된 예: softirq에서 블로킹 함수 호출 */
static void my_softirq_action(struct softirq_action *h)
{
    struct data *d;

    mutex_lock(&my_mutex);  /* ❌ softirq는 atomic context! */
    d = process_data();
    mutex_unlock(&my_mutex);
}

/* 잘못된 예: tasklet에서 msleep */
static void my_tasklet_func(struct tasklet_struct *t)
{
    msleep(100);  /* ❌ atomic context에서 슬립 불가 */
    process_data();
}

/* 올바른 예: workqueue 사용 */
static void my_work_func(struct work_struct *work)
{
    struct data *d;

    mutex_lock(&my_mutex);  /* ✓ workqueue는 프로세스 컨텍스트 */
    d = process_data();
    mutex_unlock(&my_mutex);

    msleep(100);  /* ✓ 슬립 가능 */
}

❌ 실수 2: tasklet 재진입 가정

/* 잘못된 예: tasklet이 동시 실행될 것으로 가정 */
static atomic_t counter = ATOMIC_INIT(0);

static void my_tasklet(struct tasklet_struct *t)
{
    /* ❌ 불필요한 atomic 연산 - 같은 tasklet은 직렬화됨 */
    atomic_inc(&counter);
}

/* 올바른 예: tasklet 직렬화 보장 활용 */
struct my_driver_data {
    struct tasklet_struct tasklet;
    int counter;  /* ✓ atomic 불필요 */
};

static void my_tasklet(struct tasklet_struct *t)
{
    struct my_driver_data *data = from_tasklet(data, t, tasklet);
    data->counter++;  /* ✓ 같은 tasklet은 직렬화되므로 안전 */
}

❌ 실수 3: workqueue를 atomic context로 가정

/* 잘못된 예: workqueue에서 spinlock_irqsave 남용 */
static void my_work(struct work_struct *work)
{
    unsigned long flags;

    spin_lock_irqsave(&my_lock, flags);  /* ❌ 불필요 - 이미 인터럽트 활성화 */
    process_data();
    spin_unlock_irqrestore(&my_lock, flags);
}

/* 올바른 예: 적절한 락 사용 */
static void my_work(struct work_struct *work)
{
    spin_lock(&my_lock);  /* ✓ workqueue는 인터럽트 활성화 상태 */
    process_data();
    spin_unlock(&my_lock);

    /* 또는 mutex 사용 가능 */
    mutex_lock(&my_mutex);  /* ✓ 슬립 가능 */
    slow_operation();
    mutex_unlock(&my_mutex);
}

❌ 실수 4: raise_softirq() 사용 시 인터럽트 상태 오인

/* 잘못된 예: 인터럽트 활성화 상태에서 raise_softirq_irqoff 호출 */
void my_function(void)
{
    local_irq_disable();
    do_something();
    local_irq_enable();

    raise_softirq_irqoff(NET_RX_SOFTIRQ);  /* ❌ IRQ 활성화됨! */
}

/* 올바른 예 1: 인터럽트 비활성화 상태 확인 */
void my_function(void)
{
    unsigned long flags;

    local_irq_save(flags);
    do_something();
    raise_softirq_irqoff(NET_RX_SOFTIRQ);  /* ✓ IRQ 비활성화 상태 */
    local_irq_restore(flags);
}

/* 올바른 예 2: 안전한 raise_softirq 사용 */
void my_function(void)
{
    do_something();
    raise_softirq(NET_RX_SOFTIRQ);  /* ✓ 내부에서 IRQ 제어 */
}

❌ 실수 5: workqueue flush 시 데드락

/* 잘못된 예: work 내부에서 자신을 flush */
static void my_work(struct work_struct *work)
{
    do_something();
    flush_work(work);  /* ❌ 데드락! 자기 자신을 기다림 */
}

/* 잘못된 예: 같은 workqueue에서 flush_workqueue */
static void my_work(struct work_struct *work)
{
    flush_workqueue(my_wq);  /* ❌ 같은 wq에서 실행 중 */
}

/* 올바른 예: 별도 컨텍스트에서 flush */
void cleanup_driver(void)
{
    cancel_work_sync(&my_work);  /* ✓ 외부에서 취소 대기 */
    flush_workqueue(my_wq);       /* ✓ 모든 work 완료 대기 */
}

✅ 모범 사례 체크리스트

항목설명검증 방법
컨텍스트 확인atomic vs process context 구분in_interrupt(), in_atomic()
슬립 금지softirq/tasklet에서 슬립 함수 호출 금지might_sleep() 경고 확인
최소 실행 시간softirq는 짧게, 오래 걸리면 workqueue로 위임ftrace로 실행 시간 측정
직렬화 보장tasklet의 직렬화 특성 활용불필요한 락 제거
적절한 플래그WQ_UNBOUND, WQ_HIGHPRI 등 상황에 맞게워크로드 특성 분석
정리 순서cancel → flush → destroy 순서데드락/메모리 누수 방지

드라이버 개발자 BH 선택 체크리스트

새로운 드라이버를 작성하거나 기존 드라이버를 리팩토링할 때, 아래 플로우차트와 체크리스트를 따라 최적의 Bottom Half 메커니즘을 선택하세요.

드라이버 BH 선택 플로우차트 Q1: BH에서 슬립이 필요한가? Yes Q2: IRQ 핸들러의 하반부인가? Yes Threaded IRQ request_threaded_irq() RT 최적 No Workqueue system_wq 또는 alloc_workqueue() No Q3: 초저지연(<1us) 필수? Yes Q4: 커널 서브시스템 내부? Yes Softirq 커널 소스 수정 No WQ_BH system_bh_wq No Q5: RT 지원 필수? Yes Threaded IRQ 우선순위 제어 가능 No WQ_BH tasklet 대체 레거시 tasklet 코드를 발견하면? WQ_BH (system_bh_wq)로 마이그레이션 권장 (커널 6.9+)

빠른 체크리스트

질문YesNo
BH에서 슬립(mutex, I/O 등)이 필요한가?workqueue 또는 threaded IRQ다음 질문으로
IRQ 핸들러의 하반부(Bottom Half)인가?threaded IRQ (최우선 권장)workqueue
초저지연(<1us)이 필수인가?softirq (커널 전용) 또는 WQ_BH다음 질문으로
PREEMPT_RT 지원이 필요한가?threaded IRQWQ_BH
레거시 tasklet 코드인가?WQ_BH로 마이그레이션

피해야 할 안티 패턴

/* 안티 패턴 1: 새 코드에서 tasklet 사용 */
tasklet_init(&my_tasklet, fn, data);  /* 나쁨: deprecated */
/* 올바른 대안: */
INIT_WORK(&my_work, fn);
queue_work(system_bh_wq, &my_work);  /* 좋음: WQ_BH */

/* 안티 패턴 2: softirq에서 GFP_KERNEL 사용 */
ptr = kmalloc(size, GFP_KERNEL);  /* BUG: atomic context에서 슬립 */
/* 올바른 대안: */
ptr = kmalloc(size, GFP_ATOMIC);  /* 좋음: non-blocking */

/* 안티 패턴 3: workqueue에서 초저지연 요구 */
queue_work(system_wq, &latency_critical_work);  /* 나쁨: 스케줄링 지연 */
/* 올바른 대안: */
request_threaded_irq(irq, handler, thread_fn, ...);  /* 전용 스레드 */

/* 안티 패턴 4: BH에서 장시간 실행 */
void my_softirq(struct softirq_action *a)
{
    while (has_work()) process();  /* 나쁨: 무한 루프 가능 */
}
/* 올바른 대안: 예산(budget) 기반 처리 */
void my_softirq(struct softirq_action *a)
{
    int budget = 64;
    while (has_work() && budget-- > 0)
        process();
    if (has_work())
        raise_softirq(MY_SOFTIRQ);  /* 나머지는 다음 라운드 */
}
2024년 이후 권장 사항: 새로운 드라이버 코드에서는 (1) threaded IRQ를 기본으로 고려하고, (2) 슬립이 불필요한 경우 WQ_BH를 사용하세요. tasklet은 레거시 코드 유지보수에서만 사용하고, softirq는 커널 핵심 서브시스템(네트워킹, 블록 I/O)에서만 정당화됩니다.

참고자료

커널 공식 문서

LWN.net 기사

커널 소스 코드

서적

외부 자료

Bottom Half와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.