Softirq & Hardirq

Linux 커널의 하드웨어 인터럽트(Hardirq)와 소프트웨어 인터럽트(Softirq)를 심층 분석합니다. 인터럽트 컨텍스트, Softirq 메커니즘, ksoftirqd, Tasklet, NAPI, 성능 최적화까지 종합적으로 다룹니다.

특히 "어떤 작업을 Hardirq에서 끝내고 어떤 작업을 Softirq로 넘겨야 하는가"를 판단할 수 있도록 실행 컨텍스트 제약, 지연 시간 예산, per-CPU 병렬성, ksoftirqd/N로의 인계 조건을 구체적으로 설명합니다. 네트워크 경로의 NET_RX_SOFTIRQ 적체, CPU 사용률 급등, tail latency 증가 같은 현장을 기준으로 모니터링 지표와 튜닝 순서를 함께 제시하여 성능 문제를 재현 가능하게 분석할 수 있도록 했습니다.

전제 조건: 커널 아키텍처 문서를 먼저 읽으세요. CPU, 메모리, 인터럽트의 기본 흐름을 알고 있으면 본 문서를 더 빠르게 이해할 수 있습니다.
일상 비유: 이 개념은 작업 순서표를 따라 문제를 해결하는 과정과 비슷합니다. 핵심 용어를 먼저 잡고, 실제 동작 순서를 단계별로 확인하면 복잡한 커널 내부 동작을 안정적으로 이해할 수 있습니다.

핵심 요약

  • 핵심 객체 — 이 문서의 중심이 되는 자료구조/API를 먼저 파악합니다.
  • 실행 경로 — 요청이 들어와 처리되고 종료되는 흐름을 확인합니다.
  • 병목 지점 — 지연이나 처리량 저하가 발생하는 구간을 점검합니다.
  • 동기화 지점 — 경합과 경쟁 조건이 생길 수 있는 구간을 구분합니다.
  • 운영 포인트 — 관측 지표와 튜닝 항목을 함께 확인합니다.

단계별 이해

  1. 구성요소 확인
    핵심 자료구조와 주요 API를 먼저 식별합니다.
  2. 요청 흐름 추적
    입력부터 완료까지의 호출 경로를 순서대로 따라갑니다.
  3. 예외 경로 점검
    실패 처리, 재시도, 타임아웃 등 경계 조건을 확인합니다.
  4. 성능/안정성 점검
    잠금 경합, 큐 적체, 병목 지점을 측정하고 조정합니다.

개요

Linux 커널은 인터럽트 처리를 상반부(Top Half)와 하반부(Bottom Half)로 분리합니다. 이 분리는 초기 Unix 시스템에서부터 이어진 설계 원칙으로, 하드웨어 응답성과 복잡한 처리 로직을 동시에 만족시키기 위한 핵심 아키텍처입니다.

역사적 배경

Bottom Half의 개념은 Linux 2.0 시절 bh_base[] 배열(32개 슬롯)에서 시작되었습니다. 이 구조는 전역 락으로 보호되어 SMP 확장성이 매우 낮았습니다. Linux 2.3에서 softirqtasklet이 도입되면서 Per-CPU 실행이 가능해졌고, 2.5에서 workqueue가 추가되어 프로세스 컨텍스트 Bottom Half를 완성했습니다. Linux 4.x 이후에는 PREEMPT_RT 패치셋의 mainline 통합이 진행되면서 softirq의 스레드화가 본격적으로 논의되었습니다.

커널 버전Bottom Half 변천특징
2.0BH (Bottom Half)전역 배열 32슬롯, 전역 락, SMP 병목
2.3Softirq + TaskletPer-CPU 실행, 10개 고정 벡터, 동적 tasklet
2.5Workqueue프로세스 컨텍스트, sleep 가능
2.6.30+Threaded IRQrequest_threaded_irq(), 스레드 핸들러
5.x+PREEMPT_RT mainlinesoftirq 완전 스레드화, 결정적 지연시간

Hardirq vs Softirq

구분Hardirq (Top Half)Softirq (Bottom Half)
트리거하드웨어 인터럽트소프트웨어 (raise_softirq)
실행 시점즉시 (선점 가능)Hardirq 종료 후, idle 진입 시, ksoftirqd
인터럽트 차단로컬 CPU 인터럽트 비활성화인터럽트 활성화 상태
스케줄링불가 (sleep 금지)불가 (프로세스 컨텍스트 아님)
실행 시간매우 짧음 (수 μs)상대적으로 김 (수십 μs ~ ms)
재진입불가 (같은 IRQ)가능 (다른 CPU에서 동시 실행)
대표 작업하드웨어 ACK, 데이터 읽기패킷 처리, 타이머 콜백, 블록 I/O

인터럽트 처리 흐름

하드웨어 인터럽트 발생 [Hardirq Context] • 인터럽트 벡터 호출 • irq_handler() 실행 – 하드웨어 ACK – 데이터 읽기 (최소한) – raise_softirq() ← Softirq 스케줄링 • irq_exit() → invoke_softirq() ← Pending softirq 확인 [Softirq Context] • __do_softirq() 실행 – NET_RX_SOFTIRQ, TIMER_SOFTIRQ 등 처리 • (부하 초과 시) ksoftirqd 커널 스레드에 위임 → 프로세스 컨텍스트에서 처리 (cond_resched 가능)

Hardirq (하드웨어 인터럽트)

Hardirq는 하드웨어가 CPU에 신호를 보내 즉시 실행되는 인터럽트 핸들러입니다.

Hardirq 핸들러 등록

/* include/linux/interrupt.h */
int request_irq(unsigned int irq,
               irq_handler_t handler,
               unsigned long flags,
               const char *name,
               void *dev);

/* 예제: UART 드라이버 */
static irqreturn_t uart_interrupt(int irq, void *dev_id)
{
    struct uart_port *port = dev_id;
    unsigned int status;

    /* 1. 하드웨어 상태 읽기 */
    status = readl(port->membase + UART_STATUS);

    /* 2. 최소한의 처리 */
    if (status & UART_RX_READY) {
        char ch = readl(port->membase + UART_DATA);
        uart_insert_char(port, ch);  /* 버퍼에 저장만 */
    }

    /* 3. Softirq로 실제 처리 위임 */
    tasklet_schedule(&port->tasklet);

    return IRQ_HANDLED;
}

Hardirq 제약사항

Hardirq 컨텍스트에서는 다음이 절대 금지됩니다:

  • sleep(), schedule() 호출 불가
  • mutex_lock() 사용 불가 (spinlock만 가능)
  • kmalloc(GFP_KERNEL) 불가 (GFP_ATOMIC만 가능)
  • copy_to_user() / copy_from_user() 불가
  • 긴 루프나 복잡한 연산 금지

위반 시 커널 패닉이나 데드락 발생 가능!

상세 문서: IRQ 핸들러 등록(request_irq), genirq 프레임워크, GIC/APIC 인터럽트 컨트롤러, IRQ 도메인 계층 등의 상세 내용은 인터럽트 심화 페이지를 참조하세요.

Softirq 타입

Linux는 정적으로 10가지 Softirq 타입을 정의합니다.

Softirq 우선순위 순서

번호이름용도주요 사용처
0HI_SOFTIRQ고우선순위 Tasklet드라이버 Tasklet (높은 우선순위)
1TIMER_SOFTIRQ타이머 만료커널 타이머, hrtimer
2NET_TX_SOFTIRQ네트워크 송신패킷 전송 완료 처리
3NET_RX_SOFTIRQ네트워크 수신패킷 수신 처리 (NAPI)
4BLOCK_SOFTIRQ블록 I/O블록 디바이스 I/O 완료
5IRQ_POLL_SOFTIRQIRQ 폴링고성능 블록 디바이스 (NVMe)
6TASKLET_SOFTIRQ일반 Tasklet드라이버 Tasklet (일반)
7SCHED_SOFTIRQ스케줄러로드 밸런싱, CFS
8HRTIMER_SOFTIRQ고해상도 타이머hrtimer 콜백
9RCU_SOFTIRQRCURCU 콜백 처리
10종 Softirq 우선순위 (낮은 번호 = 높은 우선순위) 우선순위 (높음 → 낮음) 0: HI_SOFTIRQ 고우선순위 Tasklet 1: TIMER_SOFTIRQ 커널 타이머 만료 2: NET_TX_SOFTIRQ 네트워크 송신 완료 3: NET_RX_SOFTIRQ 네트워크 수신 (NAPI) 4: BLOCK_SOFTIRQ 블록 I/O 완료 5: IRQ_POLL_SOFTIRQ 고성능 블록 폴링 6: TASKLET_SOFTIRQ 일반 Tasklet 7: SCHED_SOFTIRQ 로드 밸런싱 8: HRTIMER_SOFTIRQ 고해상도 타이머 9: RCU_SOFTIRQ RCU 콜백 처리 Tasklet 기반 (0, 6) 네트워킹 (2, 3) - 가장 높은 부하 스토리지 (4, 5) 타이머 (1, 8) 스케줄러 + RCU (7, 9) __do_softirq()는 비트 0부터 순서대로 순회하므로 HI_SOFTIRQ가 항상 먼저 실행됩니다. pending 비트맵: bit: 9 8 7 6 5 4 3 2 1 0 val: 0 0 1 0 0 0 1 0 1 0
10종 softirq 벡터의 우선순위와 카테고리 분류. pending 비트맵 하위 비트부터 순회합니다.

Softirq 정의

/* include/linux/interrupt.h */
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,

    NR_SOFTIRQS
};

/* kernel/softirq.c */
struct softirq_action {
    void (*action)(struct softirq_action *);
};

static struct softirq_action softirq_vec[NR_SOFTIRQS];

Softirq 메커니즘

Softirq는 raise_softirq()로 스케줄링되고 do_softirq()로 실행됩니다. 핵심 구조는 Per-CPU irq_stat.__softirq_pending 비트맵과 softirq_vec[] 핸들러 배열의 조합입니다.

10종 Softirq 벡터 상세

각 softirq 벡터는 커널의 특정 서브시스템에 전용으로 할당되어 있으며, 초기화 시 open_softirq()로 등록됩니다. 새로운 softirq 추가는 커널 서브시스템 수준의 결정이며, 드라이버에서는 tasklet이나 workqueue를 사용해야 합니다.

벡터핸들러 함수등록 위치주요 동작
HI_SOFTIRQtasklet_hi_action()kernel/softirq.c고우선순위 tasklet 큐 순회, 드라이버 레거시 코드
TIMER_SOFTIRQrun_timer_softirq()kernel/time/timer.c타이머 휠 순회, 만료 콜백 실행
NET_TX_SOFTIRQnet_tx_action()net/core/dev.c송신 완료 큐 정리, sk_buff 해제
NET_RX_SOFTIRQnet_rx_action()net/core/dev.cNAPI poll_list 순회, 패킷 수신 처리
BLOCK_SOFTIRQblk_done_softirq()block/blk-softirq.c블록 I/O 완료 콜백, request 해제
IRQ_POLL_SOFTIRQirq_poll_softirq()lib/irq_poll.cIRQ 폴링 기반 고성능 블록 처리 (NVMe)
TASKLET_SOFTIRQtasklet_action()kernel/softirq.c일반 우선순위 tasklet 큐 순회
SCHED_SOFTIRQrun_rebalance_domains()kernel/sched/fair.cCPU 간 로드 밸런싱, CFS 밸런싱
HRTIMER_SOFTIRQhrtimer_run_softirq()kernel/time/hrtimer.c고해상도 타이머 콜백 (softirq 모드)
RCU_SOFTIRQrcu_core_si()kernel/rcu/tree.cRCU 콜백 배치 처리, grace period 진행

Softirq 스케줄링

/* kernel/softirq.c */
void raise_softirq(unsigned int nr)
{
    unsigned long flags;

    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}

inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);

    /* 현재 hardirq 컨텍스트가 아니면 즉시 처리 시도 */
    if (!in_interrupt())
        wakeup_softirqd();
}

void __raise_softirq_irqoff(unsigned int nr)
{
    /* CPU별 pending 비트 설정 */
    or_softirq_pending(1UL << nr);
}

Softirq 실행 (__do_softirq)

/* kernel/softirq.c */
asmlinkage void __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    int max_restart = MAX_SOFTIRQ_RESTART;
    struct softirq_action *h;
    __u32 pending;

restart:
    /* Pending softirq 비트 읽기 */
    pending = local_softirq_pending();
    set_softirq_pending(0);

    local_irq_enable();  /* ← 인터럽트 활성화! */

    h = softirq_vec;
    do {
        if (pending & 1) {
            unsigned int vec_nr = h - softirq_vec;

            trace_softirq_entry(vec_nr);
            h->action(h);  /* ← Softirq 핸들러 호출 */
            trace_softirq_exit(vec_nr);
        }
        h++;
        pending >>= 1;
    } while (pending);

    local_irq_disable();

    /* 새 pending이 생겼고 시간/반복 제한 내면 재실행 */
    pending = local_softirq_pending();
    if (pending && time_before(jiffies, end) && --max_restart)
        goto restart;

    /* 여전히 pending이 남았으면 ksoftirqd 깨우기 */
    if (pending)
        wakeup_softirqd();
}

Softirq 폭주 방지

Softirq는 무한 루프를 방지하기 위해 다음 제한을 둡니다:

  • MAX_SOFTIRQ_TIME = 2ms (2 jiffies)
  • MAX_SOFTIRQ_RESTART = 10회

이 제한에 도달하면 ksoftirqd에 위임하여 프로세스 컨텍스트에서 처리합니다. 이렇게 하면 Softirq가 CPU를 독점하지 못하게 합니다.

__do_softirq() 내부 루프 상세 흐름 pending = local_softirq_pending() __local_bh_disable(SOFTIRQ_OFFSET) restart: set_softirq_pending(0) local_irq_enable() ← IRQ 재활성화 while (pending) if (pending & 1) h->action(h); pending >>= 1; local_irq_disable() pending = local_softirq_pending() pending && cnt<10 && t<2ms? Yes No wakeup_softirqd() __local_bh_enable(SOFTIRQ_OFFSET)
__do_softirq() 내부 루프: pending 비트맵 순회 후 재시작 조건 판단 (최대 10회, 2ms)

Hardirq 핸들러 체인

하드웨어 인터럽트가 발생하면 CPU는 인터럽트 벡터 테이블을 통해 커널의 인터럽트 엔트리 포인트에 진입합니다. 이후 irq_desc 구조체를 통해 등록된 핸들러 체인을 순회합니다.

/* kernel/irq/irqdesc.c - irq_desc 구조체 (핵심 필드) */
struct irq_desc {
    struct irq_data       irq_data;       /* 하드웨어 IRQ 정보 */
    struct irqaction     *action;         /* 핸들러 체인 (linked list) */
    struct irq_chip      *irq_chip;       /* 인터럽트 컨트롤러 ops */
    irq_flow_handler_t    handle_irq;     /* 흐름 핸들러 (level/edge) */
    unsigned int          irq_count;      /* 인터럽트 발생 횟수 */
    unsigned int          depth;          /* disable 깊이 */
    const char           *name;           /* 디바이스 이름 */
};

/* kernel/irq/handle.c - 핸들러 체인 실행 */
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
    struct irqaction *action;
    irqreturn_t retval = IRQ_NONE;

    /* 공유 IRQ: 모든 핸들러를 순회 */
    for_each_action_of_desc(desc, action) {
        irqreturn_t res;
        res = action->handler(desc->irq_data.irq, action->dev_id);

        switch (res) {
        case IRQ_WAKE_THREAD:
            /* threaded handler 깨우기 */
            irq_wake_thread(desc, action);
            break;
        case IRQ_HANDLED:
            retval |= res;
            break;
        }
    }
    return retval;
}

irq_exit() → invoke_softirq() 전환

하드웨어 인터럽트 처리가 완료되면 irq_exit()에서 pending softirq를 확인하고 실행합니다. 이것이 Hardirq에서 Softirq로의 전환 지점입니다.

/* kernel/softirq.c */
void irq_exit(void)
{
    preempt_count_sub(HARDIRQ_OFFSET);  /* hardirq 카운트 감소 */

    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();  /* softirq 실행 시작 */

    tick_irq_exit();
}

static inline void invoke_softirq(void)
{
#ifdef CONFIG_IRQ_FORCED_THREADING
    if (force_irqthreads()) {
        /* PREEMPT_RT: ksoftirqd에서 처리 */
        wakeup_softirqd();
        return;
    }
#endif
    if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
        __do_softirq();  /* 인라인 실행 */
    } else {
        wakeup_softirqd();
    }
}
Hardirq → Softirq 전환 흐름 (irq_exit → invoke_softirq) Hardirq Context HW IRQ 발생 irq_enter() handle_irq_event_percpu() action->handler() 체인 실행 raise_softirq_irqoff(NR) irq_exit() pending? Softirq Context Yes No invoke_softirq() RT? force_irqthreads No __do_softirq() Yes wakeup_softirqd() ksoftirqd/N에 위임
Hardirq 종료 시 irq_exit()에서 pending softirq를 확인하고 실행 경로를 결정합니다. PREEMPT_RT에서는 항상 ksoftirqd로 위임합니다.

ksoftirqd 커널 스레드

ksoftirqd는 CPU당 하나씩 존재하며, Softirq 부하가 높을 때 처리를 대신합니다.

ksoftirqd 메인 루프

/* kernel/softirq.c */
static int ksoftirqd(void *__bind_cpu)
{
    set_current_state(TASK_INTERRUPTIBLE);

    while (!kthread_should_stop()) {
        if (!local_softirq_pending())
            schedule();  /* Pending 없으면 sleep */

        set_current_state(TASK_RUNNING);

        while (local_softirq_pending()) {
            __do_softirq();  /* Softirq 처리 */
            cond_resched();  /* 스케줄링 포인트 */
        }

        set_current_state(TASK_INTERRUPTIBLE);
    }
    return 0;
}

ksoftirqd 프로세스 확인

ps aux | grep ksoftirqd
# root         5  0.0  0.0      0     0 ?        S    Jan01   0:12 [ksoftirqd/0]
# root        15  0.0  0.0      0     0 ?        S    Jan01   0:08 [ksoftirqd/1]
# root        20  0.0  0.0      0     0 ?        S    Jan01   0:05 [ksoftirqd/2]
# ...

ksoftirqd의 역할

  • CPU 공정성: Softirq가 CPU를 독점하지 못하게 함
  • 우선순위: SCHED_NORMAL (nice 0) - 일반 프로세스와 동등
  • 스케줄링: CFS에 의해 스케줄링되어 다른 프로세스에게도 CPU 할당

ksoftirqd CPU 사용률이 높다면 Softirq 부하가 과도하다는 신호입니다 (특히 NET_RX/TX).

ksoftirqd 활성화 조건

ksoftirqd가 깨어나는 경우는 세 가지입니다. 각 경로는 wakeup_softirqd()를 호출하여 해당 CPU의 ksoftirqd 스레드를 TASK_RUNNING으로 전환합니다.

ksoftirqd 활성화 조건 및 실행 흐름 경로 1: __do_softirq() 10회 재시작 또는 2ms 초과 → wakeup_softirqd() 경로 2: raise_softirq() 인터럽트 컨텍스트 밖에서 호출 !in_interrupt() 일 때 경로 3: local_bh_enable() BH 활성화 시 pending 발견 do_softirq() 또는 wakeup ksoftirqd/N 스레드 SCHED_NORMAL, nice 0, Per-CPU pending? should_run() No TASK_INTERRUPTIBLE wakeup 대기 Yes run_ksoftirqd(cpu) __do_softirq() + cond_resched() 반복 cond_resched() 호출로 다른 태스크에게 CPU를 양보 → softirq가 일반 프로세스를 기아 상태로 만들지 않음
ksoftirqd는 세 가지 경로로 깨어나며, 프로세스 컨텍스트에서 softirq를 처리합니다.

인터럽트 컨텍스트 확인

현재 코드가 어떤 컨텍스트에서 실행 중인지 확인하는 매크로입니다.

컨텍스트 확인 매크로

매크로의미포함 범위
in_irq()Hardirq 컨텍스트하드웨어 인터럽트 핸들러
in_softirq()Softirq 컨텍스트Softirq 핸들러, BH 비활성화 구간
in_interrupt()인터럽트 컨텍스트Hardirq + Softirq + NMI
in_task()프로세스 컨텍스트일반 프로세스, 커널 스레드

구현

/* include/linux/preempt.h */
#define in_irq()         (hardirq_count())
#define in_softirq()      (softirq_count())
#define in_interrupt()   (irq_count())
#define in_task()         (!in_interrupt() && !(current->flags & PF_EXITING))

/* 사용 예 */
void my_function(void)
{
    if (in_interrupt()) {
        /* 인터럽트 컨텍스트 - sleep 금지 */
        spin_lock(&my_lock);
    } else {
        /* 프로세스 컨텍스트 - mutex 사용 가능 */
        mutex_lock(&my_mutex);
    }
}

preempt_count 구조

인터럽트 컨텍스트 확인 매크로(in_irq(), in_softirq(), in_interrupt() 등)는 모두 preempt_count의 비트 필드를 검사합니다. 이 32비트 카운터는 PREEMPT(비트 0-7), SOFTIRQ(비트 8-15), HARDIRQ(비트 16-19), NMI(비트 20) 영역으로 나뉘어 현재 CPU의 실행 컨텍스트를 인코딩합니다. 비트 필드 구조, 각 매크로의 동작 원리, 선점 모델 비교 등 상세 내용은 preempt_count 문서를 참조하세요.

local_bh_disable/enable 메커니즘

프로세스 컨텍스트에서 softirq 핸들러와 공유 데이터를 보호하려면 local_bh_disable()/local_bh_enable()을 사용합니다. 이 API는 preempt_count의 SOFTIRQ 비트를 조작하여 현재 CPU에서 softirq 실행을 억제합니다. (preempt_count 비트 필드 구조와 in_softirq() 매크로의 동작 원리는 preempt_count 문서에서 다룹니다.)

/* kernel/softirq.c */
void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
    /* preempt_count에 SOFTIRQ_DISABLE_OFFSET 추가 */
    __preempt_count_add(cnt);
    barrier();
}

void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
    /* 주의: preempt_count 감소 전에 pending softirq 확인 */
    __preempt_count_sub(cnt);

    if (unlikely(!in_interrupt() && local_softirq_pending())) {
        /* BH 활성화 시점에 pending softirq가 있으면 즉시 처리 */
        do_softirq();
    }
    preempt_check_resched();
}

/* 사용 예: 프로세스 컨텍스트에서 softirq 보호 */
void my_data_access(void)
{
    local_bh_disable();
    /* 이 구간에서는 현재 CPU에서 softirq가 실행되지 않음 */
    /* 다른 CPU의 softirq는 여전히 실행 가능 → 공유 데이터면 spinlock 추가 */
    shared_data->counter++;
    local_bh_enable();  /* pending softirq가 있으면 여기서 처리됨 */
}

/* 동기화 계층 정리 */
/*
 * spin_lock_bh()  = spin_lock() + local_bh_disable()
 * spin_unlock_bh()= spin_unlock() + local_bh_enable()
 *
 * 용도: 프로세스 컨텍스트 ↔ softirq 간 공유 데이터 보호
 *       (다른 CPU의 softirq도 차단하려면 lock이 필요)
 */

Per-CPU Softirq Pending 비트맵

각 CPU는 독립적인 __softirq_pending 비트맵을 가집니다. 이 Per-CPU 설계 덕분에 softirq raise/check 시 락이 불필요합니다.

/* arch/x86/include/asm/hardirq.h */
typedef struct {
    unsigned int __softirq_pending;   /* 10비트 사용 (NR_SOFTIRQS=10) */
    unsigned int __nmi_count;
    unsigned int apic_timer_irqs;
    /* ... */
} irq_cpustat_t;

DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);

/* Per-CPU pending 비트 조작 API */
#define local_softirq_pending()  \
    __this_cpu_read(irq_stat.__softirq_pending)

#define set_softirq_pending(x)  \
    __this_cpu_write(irq_stat.__softirq_pending, (x))

#define or_softirq_pending(x)   \
    __this_cpu_or(irq_stat.__softirq_pending, (x))
Per-CPU Softirq Pending 비트맵 구조 CPU 0 RCU HRT SCH TSK IPL BLK NRX NTX TMR HI = 0x20A (비트 1,3,9) CPU 1 0 0 0 0 0 0 1 0 1 0 = 0x00A (비트 1,3) CPU 2 0 0 0 0 0 0 0 0 0 0 = 0x000 (idle) CPU 3 1 0 0 0 0 0 0 0 0 0 = 0x200 (RCU만) 비트 인덱스: 비트 9(RCU) 8(HRT) 7(SCH) 6(TSK) 5(IPL) 4(BLK) 3(NRX) 2(NTX) 1(TMR) 0(HI) or_softirq_pending(1 << nr) : 해당 비트를 1로 설정 (raise) local_softirq_pending() : 전체 pending 비트맵 읽기 set_softirq_pending(0) : 처리 시작 시 전체 클리어 (__do_softirq 진입부)
각 CPU가 독립적인 pending 비트맵을 유지하므로 softirq raise/check에 락이 불필요합니다.

Softirq 심화

softirq는 커널에 정적으로 컴파일되는 Bottom Half 메커니즘으로, 10개의 고정 타입이 존재합니다. 네트워킹, 블록 I/O, 타이머 등 성능이 극도로 중요한 서브시스템에서 사용됩니다. 새로운 softirq를 추가하는 것은 커널 서브시스템 수준의 결정이며, 드라이버에서는 사용하지 않습니다.

open_softirq() 등록 API

softirq 핸들러는 커널 초기화 시 open_softirq()로 등록됩니다. 각 softirq 타입에 대해 하나의 핸들러만 존재하며, 런타임에 변경할 수 없습니다:

/* kernel/softirq.c */
static struct softirq_action softirq_vec[NR_SOFTIRQS];

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

/* 각 서브시스템의 초기화 코드에서 등록 */
open_softirq(NET_TX_SOFTIRQ, net_tx_action);   /* net/core/dev.c */
open_softirq(NET_RX_SOFTIRQ, net_rx_action);   /* net/core/dev.c */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);  /* kernel/softirq.c */
open_softirq(HI_SOFTIRQ, tasklet_hi_action);    /* kernel/softirq.c */
open_softirq(TIMER_SOFTIRQ, run_timer_softirq); /* kernel/time/timer.c */
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains); /* kernel/sched/fair.c */
open_softirq(RCU_SOFTIRQ, rcu_core_si);         /* kernel/rcu/tree.c */

raise_softirq() vs raise_softirq_irqoff()

softirq를 트리거하려면 pending 비트를 설정해야 합니다. 두 가지 API가 존재합니다:

/* 일반적인 사용: 인터럽트 상태를 자동으로 저장/복원 */
void raise_softirq(unsigned int nr)
{
    unsigned long flags;
    local_irq_save(flags);       /* IRQ 비활성화 + 플래그 저장 */
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);    /* 이전 IRQ 상태 복원 */
}

/* 이미 IRQ가 비활성화된 상태에서 사용 (top half 내부 등) */
void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);  /* or_softirq_pending(1 << nr) */
    /* 인터럽트 컨텍스트가 아니면 ksoftirqd를 깨움 */
    if (!in_interrupt())
        wakeup_softirqd();
}

/* 사용 예: hard IRQ 핸들러 내부 (이미 IRQ 비활성 상태) */
static irqreturn_t my_handler(int irq, void *dev)
{
    /* ... 최소 작업 ... */
    raise_softirq_irqoff(NET_RX_SOFTIRQ);  /* 효율적 */
    return IRQ_HANDLED;
}

__do_softirq() 내부 구현

__do_softirq()는 softirq의 핵심 실행 루프입니다. pending 비트맵을 순회하며 등록된 핸들러를 호출하되, starvation 방지를 위한 제한이 존재합니다:

/* kernel/softirq.c - 핵심 실행 루프 (간략화) */
#define MAX_SOFTIRQ_TIME    msecs_to_jiffies(2)   /* 최대 2ms */
#define MAX_SOFTIRQ_RESTART 10                    /* 최대 10회 재시작 */

asmlinkage __visible void __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    int max_restart = MAX_SOFTIRQ_RESTART;
    struct softirq_action *h;
    __u32 pending;

    pending = local_softirq_pending();   /* Per-CPU pending 비트 읽기 */

    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);

restart:
    set_softirq_pending(0);              /* pending 클리어 */
    local_irq_enable();                  /* IRQ 재활성화 */

    h = softirq_vec;
    while (pending) {
        if (pending & 1) {
            h->action(h);               /* softirq 핸들러 호출 */
        }
        h++;
        pending >>= 1;
    }

    local_irq_disable();
    pending = local_softirq_pending();   /* 새로 발생한 softirq 확인 */

    /* 재시작 조건: pending 있고, 횟수/시간 제한 이내 */
    if (pending && --max_restart &&
        time_before(jiffies, end))
        goto restart;

    /* 제한 초과: ksoftirqd에 위임 */
    if (pending)
        wakeup_softirqd();

    __local_bh_enable(SOFTIRQ_OFFSET);
}
⚠️

starvation 방지 메커니즘: softirq 처리가 2ms를 초과하거나 10번 재시작하면 ksoftirqd로 위임됩니다. 이는 softirq가 일반 프로세스를 기아(starvation) 상태로 만드는 것을 방지합니다. 네트워크 부하가 높을 때 ksoftirqd의 CPU 사용량이 증가하는 이유입니다.

ksoftirqd 생명주기

ksoftirqd는 Per-CPU 커널 스레드로, softirq 처리의 폴백 경로를 담당합니다:

/* kernel/softirq.c - ksoftirqd 메인 루프 (간략화) */
static int ksoftirqd_should_run(unsigned int cpu)
{
    return local_softirq_pending();
}

static void run_ksoftirqd(unsigned int cpu)
{
    local_irq_disable();
    if (local_softirq_pending()) {
        __do_softirq();
        local_irq_enable();
        cond_resched();         /* 다른 태스크에 CPU 양보 */
        return;
    }
    local_irq_enable();
}

/*
 * ksoftirqd 특성:
 * - Per-CPU: ksoftirqd/0, ksoftirqd/1, ...
 * - 스케줄링 정책: SCHED_NORMAL (nice 0)
 * - 깨어나는 조건:
 *   1. __do_softirq()에서 시간/횟수 제한 초과
 *   2. 인터럽트 비활성 상태에서 raise_softirq() 호출
 *   3. local_bh_enable()에서 pending softirq 발견
 * - ksoftirqd는 일반 프로세스와 동일한 우선순위로 스케줄링됨
 *   → 높은 부하에서 softirq 지연 발생 가능 (의도된 동작)
 */
Softirq 실행 흐름 Hardware IRQ Top Half (hardirq) raise_softirq() irq_exit() pending 확인 pending? No Return Yes __do_softirq() pending 순회 + 핸들러 호출 재시작? <10회 & <2ms Yes No wakeup_softirqd()
softirq 실행 흐름: irq_exit() → __do_softirq() → ksoftirqd 폴백

Per-CPU 동시성 모델

softirq의 가장 중요한 특성은 같은 softirq 타입이 여러 CPU에서 동시에 실행될 수 있다는 점입니다. 이는 높은 성능을 제공하지만, 공유 데이터에 대한 동기화가 필수입니다:

/*
 * Softirq 동시성 규칙:
 *
 * 1. 같은 softirq가 여러 CPU에서 동시 실행 가능
 *    → NET_RX_SOFTIRQ: CPU0과 CPU1에서 동시 실행 가능
 *    → Per-CPU 데이터 사용으로 락 최소화
 *
 * 2. 같은 CPU에서는 softirq가 중첩되지 않음
 *    → softirq 실행 중 동일 CPU의 다른 softirq는 대기
 *
 * 3. Hard IRQ만 softirq를 선점 가능
 *    → softirq 실행 중 동일 CPU의 프로세스는 실행 불가
 *
 * 4. 동기화 패턴:
 *    - softirq 간: spin_lock() (preemption은 이미 비활성)
 *    - softirq + 프로세스: spin_lock_bh() (프로세스 쪽)
 *    - softirq + hard IRQ: spin_lock_irq() (softirq 쪽)
 */

/* 예: 네트워크 수신 softirq의 Per-CPU 데이터 활용 */
DEFINE_PER_CPU(struct softnet_data, softnet_data);

static void net_rx_action(struct softirq_action *h)
{
    /* Per-CPU 데이터 접근 → 락 불필요 */
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    struct list_head *list = &sd->poll_list;

    while (!list_empty(list)) {
        /* NAPI polling ... */
    }
}

선점 모드별 동작

선점 모드softirq 실행 위치ksoftirqd 역할특징
PREEMPT_NONEirq_exit() 직후제한 초과 시 폴백서버 워크로드 최적화, 높은 처리량
PREEMPT_VOLUNTARYirq_exit() 직후제한 초과 시 폴백데스크톱 기본, 약간의 응답성 향상
PREEMPT_FULLirq_exit() 직후제한 초과 시 폴백완전 선점, 실시간성 향상
PREEMPT_RTksoftirqd에서만모든 softirq 처리결정적 지연시간, softirq도 선점 가능
💡

PREEMPT_RT에서는 모든 softirq가 ksoftirqd 스레드에서 실행되므로, softirq도 일반 스레드처럼 선점되고 우선순위 조정이 가능합니다. 이를 통해 결정적(deterministic) 지연시간을 보장하지만, 처리량은 감소합니다.

PREEMPT_RT에서의 Softirq 처리

PREEMPT_RT(Real-Time) 커널에서는 softirq의 동작이 근본적으로 변경됩니다. 모든 softirq가 ksoftirqd 커널 스레드에서 실행되어, softirq 핸들러도 선점 가능한 일반 스레드 컨텍스트에서 동작합니다. 이를 통해 결정적(deterministic) 지연시간을 보장합니다.

PREEMPT_RT vs 일반 커널: Softirq 실행 경로 비교 일반 커널 (PREEMPT_NONE/FULL) Hardirq 발생 irq_exit() __do_softirq() 인터럽트 컨텍스트에서 실행 특성: + 낮은 지연시간 (즉시 실행) + 높은 처리량 - 선점 불가 (비결정적 지연) - sleep/mutex 사용 불가 - RT 태스크 지연 가능 spin_lock → 실제 spin (비선점) local_bh_disable → preempt_count PREEMPT_RT 커널 Hardirq 발생 irq_exit() wakeup_softirqd() ksoftirqd/N (스레드) 프로세스 컨텍스트에서 실행 특성: + 선점 가능 (결정적 지연시간) + 우선순위 조정 가능 (chrt) - 처리량 감소 (컨텍스트 전환 비용) spin_lock → rt_mutex (선점 가능)
PREEMPT_RT에서는 모든 softirq가 ksoftirqd 스레드에서 실행되어 선점 가능하고 우선순위 조정이 가능합니다.
/* PREEMPT_RT에서의 주요 변환 */

/* 1. spin_lock → rt_mutex로 변환 */
/*    → softirq 핸들러도 sleep 가능 (priority inheritance 지원) */

/* 2. local_bh_disable() → Per-CPU 락으로 변환 */
/*    → softirq 직렬화를 preempt_count 대신 실제 락으로 구현 */

/* 3. ksoftirqd 우선순위 조정 */
/*    chrt -f -p 50 $(pgrep -f "ksoftirqd/0")
 *    → ksoftirqd를 SCHED_FIFO로 전환하여 softirq 지연 감소 */

/* 4. 개별 softirq 스레드 (일부 RT 패치) */
/*    softirq 유형별로 별도 스레드를 생성하여
 *    NET_RX와 TIMER의 우선순위를 독립적으로 조정 가능 */

PREEMPT_RT 운영 팁

  • ksoftirqd 우선순위: RT 태스크보다 낮게 설정하되, 너무 낮으면 네트워크 패킷 처리가 지연됩니다. SCHED_FIFO 우선순위 49 정도가 일반적입니다.
  • cyclictest로 최악 지연시간을 측정하여 softirq 영향을 확인하세요.
  • threaded IRQ: request_threaded_irq()를 사용하면 hardirq 핸들러도 스레드화되어 완전한 선점이 가능합니다.

/proc/softirqs 분석 방법

/proc/softirqs는 부팅 이후 각 CPU에서 처리된 softirq 횟수를 누적 표시합니다. 이 데이터를 주기적으로 캡처하여 차이를 계산하면 실시간 softirq 부하를 파악할 수 있습니다.

# 1초 간격으로 softirq 처리량 변화 확인
watch -d -n 1 'cat /proc/softirqs'

# 특정 시간 동안의 softirq 증가량 측정
cat /proc/softirqs > /tmp/softirq_before
sleep 10
cat /proc/softirqs > /tmp/softirq_after
diff /tmp/softirq_before /tmp/softirq_after

# perf로 softirq 실행 시간 프로파일링
perf stat -e irq:softirq_entry,irq:softirq_exit -a sleep 5

# 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_pipe

Softirq 문제 진단 패턴

증상확인 지표원인대응
특정 CPU %soft 높음mpstat -P ALL, /proc/softirqsIRQ affinity 불균형irqbalance 또는 수동 smp_affinity 조정
NET_RX 카운트 폭증/proc/softirqs NET_RX 행네트워크 패킷 폭주NAPI budget 조정, RPS/RFS, 멀티큐 NIC
ksoftirqd CPU 점유top/htop에서 ksoftirqdsoftirq 부하 초과원인 softirq 식별 후 해당 서브시스템 튜닝
TIMER 과다/proc/softirqs TIMER 행고빈도 타이머 사용타이머 병합, NO_HZ 설정
RCU 지연/proc/softirqs RCU 행RCU 콜백 적체rcu_nocbs 설정, RCU offloading
tail latency 증가cyclictest, perf schedsoftirq 선점으로 태스크 지연PREEMPT_RT, CPU isolation
# CPU별 softirq 불균형 빠른 확인 스크립트
awk 'NR>1 {
  name=$1
  total=0
  for(i=2;i<=NF;i++) total+=$i
  printf "%-20s total=%10d\n", name, total
}' /proc/softirqs

# 출력 예:
# HI:                 total=         3
# TIMER:              total=  13691355
# NET_TX:             total=    386418
# NET_RX:             total=  32839506  ← 가장 높은 부하
# BLOCK:              total=    116046
# IRQ_POLL:           total=         0
# TASKLET:            total=     28934
# SCHED:              total=   8745632
# RCU:                total=   5367890

Softirq 위에 구현되거나 관련된 메커니즘들에 대한 상세 문서 안내입니다.

Tasklet: softirq(TASKLET_SOFTIRQ, HI_SOFTIRQ) 위에 구현된 동적 Bottom Half. 같은 tasklet은 절대 병렬 실행되지 않는 직렬화를 보장합니다. 자세한 내용은 Tasklet 심화 페이지를 참고하세요.
Workqueue (CMWQ): 프로세스 컨텍스트에서 실행되는 Bottom Half. 슬립이 가능하여 mutex, GFP_KERNEL 등을 사용할 수 있습니다. 자세한 내용은 Workqueue (CMWQ) 심화 페이지를 참고하세요.
NAPI: 인터럽트 + 폴링 하이브리드로 네트워크 성능을 개선하는 메커니즘. NET_RX_SOFTIRQ를 활용합니다. 자세한 내용은 NAPI 심화 페이지를 참고하세요.
Bottom Half 선택 가이드: softirq, tasklet, workqueue, threaded IRQ 중 어떤 메커니즘을 선택해야 하는지에 대한 실전 가이드. 자세한 내용은 Bottom Half 선택 가이드와 실전 패턴 페이지를 참고하세요.

모니터링

Softirq와 Hardirq 활동을 모니터링하는 방법입니다.

/proc/softirqs

cat /proc/softirqs
#                    CPU0       CPU1       CPU2       CPU3
#       HI:          2          1          0          0
#    TIMER:    3456789    3234567    3012345    2987654
#   NET_TX:     123456      98765      87654      76543
#   NET_RX:    9876543    8765432    7654321    6543210  ← 높음!
#    BLOCK:      45678      34567      23456      12345
#      RCU:    1234567    1123456    1012345     987654

mpstat으로 softirq 비율 확인

mpstat -P ALL 1
# CPU    %usr   %nice    %sys %iowait    %irq   %soft  %idle
#   0    5.2     0.0    10.3     1.2     0.5    25.8   57.0   ← soft 25%!
#   1    3.1     0.0     8.7     0.8     0.3    18.2   68.9

/proc/interrupts

cat /proc/interrupts
#            CPU0       CPU1       CPU2       CPU3
#   45:  12345678   11234567   10123456    9012345   IR-PCI-MSI 524288-edge      eth0
#  125:       123        234        345        456   IR-PCI-MSI   1-edge      nvme0q1

성능 튜닝

Softirq 부하를 조정하고 최적화하는 방법입니다.

NAPI Budget 조정

# 한 번의 NAPI poll에서 처리할 최대 패킷 수
sysctl -w net.core.netdev_budget=600    # 기본값: 300

# NAPI poll이 소비할 수 있는 최대 시간 (μs)
sysctl -w net.core.netdev_budget_usecs=8000  # 기본값: 2000

IRQ Affinity 설정

# IRQ 45번을 CPU 2-3에만 할당
echo "c" > /proc/irq/45/smp_affinity  # 0xc = 0b1100 = CPU 2,3

# irqbalance 비활성화 (수동 설정 시)
systemctl stop irqbalance

RPS/RFS (네트워크)

# RPS (Receive Packet Steering): 패킷 처리를 여러 CPU로 분산
echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus  # 0xf = CPU 0-3

# RFS (Receive Flow Steering): 애플리케이션 CPU로 패킷 전달
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt

높은 %soft 의 의미

mpstat에서 %soft가 지속적으로 20% 이상이면:

  • 네트워크 부하: NAPI budget 증가, 멀티큐 NIC 사용, RPS/RFS 활성화
  • 타이머 폭주: 불필요한 타이머 제거, hrtimer → low-res timer
  • 블록 I/O: io_uring 사용, polling 모드 활성화

ksoftirqd CPU 사용률이 높다면 Softirq 처리량이 한계에 도달한 것입니다.

NAPI 수신 경로 심화

NET_RX_SOFTIRQ 부하의 대부분은 NAPI poll 루프에서 발생합니다. 패킷 한 개의 이동 경로를 정확히 보면 병목 지점을 빠르게 찾을 수 있습니다.

드라이버 IRQ부터 프로토콜 스택까지

/* net/core/dev.c 중심 경로 (간략화) */
static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;

    /* 하드웨어 인터럽트 마스크 + NAPI 스케줄 */
    if (napi_schedule_prep(&q_vector->napi)) {
        igb_irq_disable(q_vector);
        __napi_schedule(&q_vector->napi);
    }
    return IRQ_HANDLED;
}

void __napi_schedule(struct napi_struct *n)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    list_add_tail(&n->poll_list, &sd->poll_list);
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

static void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    int budget = netdev_budget;

    while (budget > 0 && !list_empty(&sd->poll_list)) {
        struct napi_struct *n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
        int work = n->poll(n, min(budget, n->weight));
        budget -= work;
    }
}
NAPI 기반 수신 경로: Hardirq 최소화 + NET_RX_SOFTIRQ 배치 처리 NIC RX Ring에 패킷 도착 DMA 완료 + MSI-X 인터럽트 Hardirq 핸들러 인터럽트 마스크 + __napi_schedule pending 비트 설정 NET_RX_SOFTIRQ irq_exit() invoke_softirq() net_rx_action() softnet_data.poll_list를 budget/시간 제한으로 순회 드라이버 napi->poll() RX 디스크립터 회수 + skb 생성 napi_gro_receive() GRO 병합 + 상위 프로토콜 전달 병목 점검 지점 1) RX ring fill/clean 균형 2) NAPI weight 대비 실제 work 3) GRO flush 빈도 4) backlog 적체(netdev_max_backlog) 5) ksoftirqd 이관 빈도 핵심 튜닝 변수 - net.core.netdev_budget / net.core.netdev_budget_usecs - 드라이버 NAPI weight, IRQ coalescing(rx-usecs/rx-frames) - RPS/RFS/XPS CPU 분산 정책과 IRQ affinity 정렬 - 멀티큐 NIC에서 queue 수와 애플리케이션 worker 수 매칭 - busy_poll/busy_read 적용 시 tail latency와 CPU 비용 동시 측정
NAPI의 목적은 hardirq 시간을 최소화하고, softirq에서 배치 처리로 처리량을 확보하는 것입니다.

락 설계와 동기화 패턴

softirq 코드는 컨텍스트별 락 선택이 잘못되면 즉시 데드락이나 심각한 지연으로 이어집니다. 아래 표는 실무에서 가장 자주 쓰는 조합입니다.

경합 주체권장 락프로세스 측 보호softirq 측 보호주의점
프로세스 ↔ softirqspin_lock_bh()spin_lock_bh()spin_lock()프로세스 쪽에서 BH 차단 필수
softirq ↔ hardirqspin_lock_irqsave()해당 없음spin_lock_irqsave()하드 인터럽트 선점 고려
CPU 간 softirqspin_lock() 또는 Per-CPU해당 없음spin_lock()가능하면 Per-CPU로 락 제거
프로세스 전용 경로mutexmutex_lock()사용 금지softirq/hardirq에서 절대 사용 금지
/* 잘못된 예: softirq가 접근하는 큐에 mutex 사용 */
struct my_queue {
    struct list_head head;
    struct mutex lock;  /* softirq 경로에서 사용 불가 */
};

void bad_enqueue_from_softirq(struct my_queue *q, struct packet *p)
{
    mutex_lock(&q->lock);   /* BUG: sleep 가능 API */
    list_add_tail(&p->node, &q->head);
    mutex_unlock(&q->lock);
}

/* 올바른 예: 프로세스와 softirq 공유 큐 */
struct good_queue {
    struct list_head head;
    spinlock_t lock;
};

void enqueue_from_process(struct good_queue *q, struct packet *p)
{
    spin_lock_bh(&q->lock);
    list_add_tail(&p->node, &q->head);
    spin_unlock_bh(&q->lock);
}

void enqueue_from_softirq(struct good_queue *q, struct packet *p)
{
    spin_lock(&q->lock);
    list_add_tail(&p->node, &q->head);
    spin_unlock(&q->lock);
}

지연시간 타임라인 분석

tail latency는 대부분 "인터럽트 폭주 → softirq 재시작 반복 → ksoftirqd 이관" 구간에서 악화됩니다. 이벤트 타임라인을 단위 시간으로 나눠 보면 원인 구분이 쉬워집니다.

1개 CPU에서 관측한 인터럽트/softirq 타임라인 예시 Hardirq 짧고 빈번한 IRQ Softirq restart 루프 반복 ksoftirqd 폴백 처리 구간 사용자 태스크 softirq 폭주 구간에서 스케줄링 지연 2ms/10회 초과 해석 포인트 1) Hardirq 폭은 작아도 빈도 과다 시 softirq backlog가 누적됩니다. 2) ksoftirqd 구간이 길수록 tail latency가 증가합니다.
타임라인 분석은 처리량 문제(평균)와 tail latency 문제(최악값)를 분리해서 보는 데 유용합니다.

지연시간 계측 지표

지표수집 방법해석 기준
softirq 실행 횟수/proc/softirqs 차분CPU별 편차가 크면 affinity 재배치 필요
softirq 실행 시간perf sched, tracepointNET_RX 단일 이벤트가 길면 NAPI budget 과대 가능성
ksoftirqd 런큐 대기sched:sched_wakeup/switchRT 태스크가 과도하면 softirq 지연 발생
애플리케이션 p99/p999서비스 내부 지표softirq 폭주 시 분산이 급격히 커짐

실전 트러블슈팅 플레이북

운영 중 장애 상황에서 즉시 적용할 수 있는 점검 순서입니다. 핵심은 원인 축을 빠르게 좁히는 것입니다.

  1. 증상 고정
    mpstat -P ALL 1, top -H로 어느 CPU에서 %softksoftirqd/N가 치솟는지 확인합니다.
  2. softirq 타입 식별
    동일 구간의 /proc/softirqs 차분으로 NET_RX/TIMER/RCU 중 어떤 축이 급증하는지 확인합니다.
  3. IRQ 소스 매핑
    /proc/interrupts에서 급증 IRQ를 NIC 큐/스토리지 큐와 매핑합니다.
  4. 분산 정책 교정
    IRQ affinity, RPS/RFS/XPS를 재정렬해 한 CPU 집중을 먼저 해소합니다.
  5. budget/코얼레싱 조정
    NAPI budget과 인터럽트 코얼레싱 값을 동시에 조정하며 처리량 대비 지연시간을 측정합니다.
  6. RT/격리 검토
    tail latency 요구가 높으면 PREEMPT_RT 또는 CPU isolation을 적용합니다.
# 1) CPU별 softirq 상태 10초 캡처
for i in $(seq 1 10); do
  date +%T
  cat /proc/softirqs
  sleep 1
done > /tmp/softirq-sample.log

# 2) 인터럽트 상위 소스 확인
cat /proc/interrupts | sort -nrk2 | head -n 20

# 3) ksoftirqd 스케줄 지연 확인
perf sched record -a -- sleep 10
perf sched latency

# 4) tracepoint 기반 softirq 실행 추적
echo 1 > /sys/kernel/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/tracing/events/irq/softirq_exit/enable
sleep 5
cat /sys/kernel/tracing/trace > /tmp/softirq-trace.txt
Softirq 장애 대응 의사결정 트리 증상: 높은 %soft 또는 ksoftirqd 급등 주요 softirq가 NET_RX 인가? Yes IRQ affinity + RPS/RFS NAPI budget 점검 No TIMER/RCU/BLOCK 서브시스템별 원인 추적 NIC 코얼레싱/큐 수 애플리케이션 worker CPU 매칭 고빈도 타이머 정리 RCU 오프로딩 블록 큐 튜닝 재측정: 처리량 + p99/p999 개선 없으면 PREEMPT_RT/격리 검토 항상 "IRQ 분산 → softirq 타입 확인 → 서브시스템 튜닝" 순서로 접근하면 시행착오를 크게 줄일 수 있습니다.
실무에서는 증상을 softirq 타입으로 먼저 분해하면 분석 시간이 크게 줄어듭니다.