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

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

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

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

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

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

핵심 요약

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

단계별 이해

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

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

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

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

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

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

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

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

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

Softirq 심화: open_softirq 등록, raise_softirq 변형, __do_softirq 내부, ksoftirqd 생명주기, Per-CPU 동시성, 선점 모드별 동작.
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 스레드
지연시간최소낮음중간낮음~중간
동적 생성불가 (정적)가능가능가능
PREEMPT_RTksoftirqd로 이동비호환정상 동작정상 동작
우선순위 제어불가불가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 뮤텍스 기반으로 변경
  • 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
호출 오버헤드~100ns~1-5us~1-10us~1-5us
스케줄링 지연거의 없음컨텍스트 스위치컨텍스트 스위치 + 마이그레이션컨텍스트 스위치
SMP 확장성뛰어남 (Per-CPU)좋음 (Per-CPU)좋음 (NUMA-aware)보통 (Per-IRQ)
캐시 친화성높음높음 (같은 CPU)보통보통
우선순위 역전가능 (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 메커니즘별 호출 오버헤드와 스케줄링 지연 비교

관련 커널 설정

CONFIG 옵션설명기본값
CONFIG_PREEMPT_NONE선점 없음, 서버 최적화서버 defconfig
CONFIG_PREEMPT_VOLUNTARY자발적 선점, 데스크톱 기본데스크톱 defconfig
CONFIG_PREEMPT완전 선점선택
CONFIG_PREEMPT_RT실시간 선점 (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) 패치가 적용되면 각 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 사용 시 자주 발생하는 실수와 올바른 접근 방법을 비교합니다.

❌ 실수 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 순서데드락/메모리 누수 방지

성능 최적화 가이드

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: 타이머 + tasklet 조합

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

/* 실전 패턴: 폴링 기반 디바이스 */
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 인터럽트부터 소켓 버퍼 전달까지의 흐름입니다.

네트워크 드라이버 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 사용 시 발생하기 쉬운 데드락 및 우선순위 역전 패턴과 방지 방법입니다.

패턴 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) 완료 콜백이 인터럽트 컨텍스트에서 호출되므로, 무거운 처리는 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 완료 통지 -> 대기 중인 프로세스 깨움 */
}

문제 해결 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 - 성능 병목 분석
  • CONFIG_DEBUG_ATOMIC_SLEEP - atomic context 검증

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