Softirq & Hardirq

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

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

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

핵심 요약

  • Hardirq (Top Half) — 하드웨어 인터럽트 핸들러로, IRQ 컨텍스트에서 최소한의 작업만 수행하고 즉시 반환합니다. 이 구간에서는 sleep과 프로세스 스케줄링이 금지됩니다.
  • Softirq (Bottom Half) — hardirq가 등록한 지연 작업을 처리하는 메커니즘으로, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, TIMER_SOFTIRQ, BLOCK_SOFTIRQ 등 10개 정적 타입이 존재합니다.
  • ksoftirqd — softirq 처리가 과도하게 지연될 때 전용 커널 스레드로 위임하여, softirq가 일반 프로세스를 무기한 차단하는 것을 방지합니다.
  • IRQ 컨텍스트 규칙 — hardirq 컨텍스트에서는 뮤텍스(Mutex) 획득, 메모리 할당(GFP_KERNEL), 유저 공간 접근이 불가하며, in_irq()/in_softirq()로 현재 컨텍스트를 판별합니다.
  • local_bh_disable/enable — softirq 실행을 지역적으로 비활성화하여 softirq 핸들러와의 경쟁 조건(Race Condition)을 방지하는 동기화 프리미티브입니다.
  • /proc/softirqs — CPU별 softirq 타입별 누적 실행 횟수를 보여주는 관측 인터페이스로, 부하 분포와 병목 분석에 활용합니다.
  • PREEMPT_RT 변환 — PREEMPT_RT 커널에서는 대부분의 softirq가 커널 스레드로 강제 위임되어, 실시간(Real-time) 태스크의 지연 시간 예측 가능성을 높입니다.

단계별 이해

  1. Top Half / Bottom Half 분리 원칙 이해 — 인터럽트 처리를 즉시 처리(hardirq)와 지연 처리(softirq/tasklet)로 나누는 이유를 파악합니다.

    hardirq는 다른 인터럽트를 차단하므로 최대한 짧게 유지해야 하며, 복잡한 처리는 softirq로 위임합니다. 이 분리가 시스템 응답성의 핵심입니다.

  2. Softirq 타입별 역할 파악NET_RX, NET_TX, TIMER, BLOCK, SCHED, RCU 등 각 softirq 타입이 담당하는 서브시스템을 확인합니다.

    /proc/softirqs를 주기적으로 읽어 어떤 타입이 어느 CPU에서 많이 실행되는지 분포를 관찰합니다.

  3. 실행 흐름 추적: irq_exit()에서 ksoftirqd까지 — hardirq 종료 시 pending softirq를 확인하고, 즉시 실행 또는 ksoftirqd 위임을 결정하는 경로를 따라갑니다.

    raise_softirq()가 비트를 설정하고, __do_softirq()가 루프를 돌며 처리하는 구조를 코드 수준에서 확인합니다.

  4. 동기화와 컨텍스트 규칙 점검 — softirq 핸들러 안에서 사용 가능한 잠금(spinlock, per-CPU 변수)과 금지 사항(sleep, mutex)을 정리합니다.

    local_bh_disable()/local_bh_enable()로 프로세스 컨텍스트에서 softirq와의 경쟁을 방지하는 패턴을 익힙니다.

  5. 성능 관측과 병목 진단/proc/softirqs, /proc/interrupts, ftracesoftirq 이벤트를 조합하여 softirq 지연과 CPU 편중을 진단합니다.

    네트워크 부하 환경에서 NET_RX_SOFTIRQ가 특정 CPU에 집중되면 RPS(Receive Packet Steering) 설정으로 분산합니다.

개요

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

역사적 배경

Bottom Half의 개념은 Linux 2.0 시절 bh_base[] 배열(32개 슬롯)에서 시작되었습니다. 이 구조는 전역 락으로 보호되어 SMP 확장성이 매우 낮았습니다. Linux 2.3에서 softirqtasklet이 도입되면서 Per-CPU 실행이 가능해졌고, 2.5에서 workqueue가 추가되어 프로세스(Process) 컨텍스트 Bottom Half를 완성했습니다. Linux 4.x 이후에는 PREEMPT_RT 패치(Patch)셋의 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(), 스레드(Thread) 핸들러(Handler)
5.x+PREEMPT_RT mainlinesoftirq 완전 스레드화, 결정적 지연시간

Hardirq vs Softirq

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

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

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

인터럽트 스택 전환 메커니즘

하드웨어 인터럽트가 발생하면 CPU는 현재 프로세스의 커널 스택에서 전용 Per-CPU IRQ 스택으로 전환합니다. 이는 인터럽트 핸들러가 현재 태스크의 스택 공간을 소비하여 스택 오버플로(Stack Overflow)를 유발하는 것을 방지합니다.

/* arch/x86/kernel/irq_64.c - Per-CPU IRQ 스택 정의 */
DEFINE_PER_CPU_PAGE_ALIGNED(struct irq_stack, irq_stack_backing_store)
    __aligned(IRQ_STACK_SIZE);

/* IRQ 스택 크기 = THREAD_SIZE (보통 16KB, x86_64) */
#define IRQ_STACK_SIZE  THREAD_SIZE

/* arch/x86/include/asm/irq_stack.h - 스택 전환 매크로 */
#define call_on_irqstack_cond(func, regs, asm_call, argconstr, ...)  \
({                                                                    \
    /* 이미 IRQ 스택에 있는지 확인 */                              \
    if (__this_cpu_read(pcpu_hot.hardirq_stack_inuse)) {            \
        func(__VA_ARGS__);  /* 이미 IRQ 스택 → 직접 호출 */       \
    } else {                                                       \
        __this_cpu_write(pcpu_hot.hardirq_stack_inuse, true);       \
        asm_call_irq_on_stack(func, __VA_ARGS__);                   \
        __this_cpu_write(pcpu_hot.hardirq_stack_inuse, false);      \
    }                                                                  \
})

x86_64에서는 추가로 IST(Interrupt Stack Table)가 존재합니다. TSS(Task State Segment)에 7개의 IST 항목을 정의할 수 있으며, NMI, Double Fault, MCE(Machine Check Exception) 등 치명적 예외는 IST를 통해 현재 스택 상태와 무관하게 안전한 스택에서 실행됩니다.

IST 인덱스이름용도스택 크기
1DOUBLEFAULT_STACKDouble FaultIRQ_STACK_SIZE
2NMI_STACKNon-Maskable InterruptIRQ_STACK_SIZE
3DEBUG_STACK디버그 예외 (#DB)IRQ_STACK_SIZE
4MCE_STACKMachine Check ExceptionIRQ_STACK_SIZE
5VC_STACKVMM Communication (SEV-ES)IRQ_STACK_SIZE
x86_64 인터럽트 스택 전환 구조 프로세스 커널 스택 thread_info + 16KB 시스템콜 핸들러 스케줄러 경로 (사용 가능 공간) thread_info (스택 바닥) Per-CPU IRQ 스택 irq_stack (16KB) do_IRQ() handle_irq_event() 드라이버 핸들러 raise_softirq() IST 스택 (x86_64) TSS에서 7개 슬롯 IST1: Double Fault IST2: NMI IST3: Debug (#DB) IST4: MCE IST5: VC (SEV-ES) HW IRQ NMI/MCE 일반 하드웨어 인터럽트는 Per-CPU IRQ 스택을, NMI/Double Fault 등 치명적 예외는 IST 스택을 사용합니다.
x86_64에서는 세 종류의 스택이 분리됩니다. 인터럽트 핸들러가 프로세스 스택을 소비하지 않아 스택 오버플로 위험이 크게 줄어듭니다.

IRQ 스택 오버플로

IRQ 스택은 16KB로 제한됩니다. Hardirq 핸들러에서 깊은 호출 체인(deep call chain)을 유발하면 IRQ 스택 오버플로가 발생하여 커널 패닉으로 이어집니다. 특히 WARN_ON()이나 printk() 같은 디버그 출력도 상당한 스택 공간을 소비하므로, 인터럽트 핸들러에서는 최소한의 작업만 수행해야 합니다.

IRQF 플래그 상세 레퍼런스

request_irq() 호출 시 전달하는 flags 매개변수는 인터럽트의 동작 방식을 세밀하게 제어합니다. 다음은 include/linux/interrupt.h에 정의된 주요 IRQF 플래그입니다.

플래그설명주요 용도
IRQF_SHARED여러 디바이스가 하나의 IRQ 라인을 공유PCI 레거시 인터럽트
IRQF_ONESHOTHardirq 후 재활성화하지 않고 threaded 핸들러 완료 후 재활성화Level-triggered + threaded IRQ
IRQF_NO_SUSPEND시스템 Suspend 중에도 IRQ 비활성화하지 않음RTC, 전원 버튼, 웨이크업(Wakeup) 소스
IRQF_NOBALANCINGirqbalance에 의한 affinity 재배치 제외Per-CPU 인터럽트, 타이머
IRQF_NO_THREADPREEMPT_RT에서도 강제 스레드화 방지시간 임계 인터럽트 (타이머)
IRQF_PERCPUPer-CPU 인터럽트로 등록 (affinity 고정)IPI, 로컬 APIC 타이머
IRQF_TRIGGER_RISING상승 에지(Rising Edge)에서 트리거GPIO 인터럽트
IRQF_TRIGGER_FALLING하강 에지(Falling Edge)에서 트리거GPIO 인터럽트
IRQF_TRIGGER_HIGH높은 레벨(High Level)에서 트리거Level-triggered 디바이스
IRQF_TRIGGER_LOW낮은 레벨(Low Level)에서 트리거Active-low 디바이스
IRQF_TIMERIRQF_NO_SUSPEND | IRQF_NO_THREAD 조합시스템 타이머
IRQF_PROBE_SHAREDIRQ 프로빙(Probing) 시 공유 허용레거시 디바이스 탐지
/* 실전 IRQF 플래그 조합 예제 */

/* 1) PCI 디바이스 공유 IRQ */
request_irq(pci_irq, my_pci_handler, IRQF_SHARED,
            "my_pci_dev", dev);

/* 2) Threaded IRQ + Level-triggered (I2C 센서 등)
 *    IRQF_ONESHOT 필수: level-triggered에서 없으면
 *    threaded handler 실행 전에 인터럽트가 재발생하여 IRQ storm */
request_threaded_irq(gpio_irq,
    NULL,          /* hardirq handler 없음 */
    sensor_thread_fn,
    IRQF_TRIGGER_LOW | IRQF_ONESHOT,
    "i2c_sensor", sensor_data);

/* 3) 웨이크업 소스 (전원 버튼) */
request_irq(pwr_irq, power_btn_handler,
            IRQF_NO_SUSPEND | IRQF_TRIGGER_FALLING,
            "power_btn", NULL);

IRQF_ONESHOT이 필수인 이유

Level-triggered 인터럽트에서 request_threaded_irq()를 사용할 때 IRQF_ONESHOT을 생략하면, hardirq 핸들러가 리턴한 즉시 인터럽트가 재활성화됩니다. 그러나 threaded 핸들러가 아직 인터럽트 원인을 제거하지 않았으므로 인터럽트가 즉시 다시 발생하여 IRQ storm이 됩니다. IRQF_ONESHOT은 threaded 핸들러 완료까지 재활성화를 지연시킵니다.

Edge Trigger vs Level Trigger

인터럽트 신호 방식은 크게 Edge-triggeredLevel-triggered로 나뉘며, 각각 다른 flow handler가 처리합니다. 이 차이는 인터럽트 유실과 IRQ storm 방지에 직접 영향을 미칩니다.

트리거 방식Flow HandlerACK 타이밍인터럽트 재활성화핵심 고려사항
Edgehandle_edge_irq()핸들러 진입 전 ACK항상 활성핸들러 실행 중 재발생하면 유실 가능
Levelhandle_level_irq()핸들러 진입 전 Mask핸들러 완료 후 Unmask핸들러가 HW 원인을 제거해야 함
FastEOIhandle_fasteoi_irq()핸들러 완료 후 EOI자동MSI/MSI-X 기본 핸들러
Simplehandle_simple_irq()없음없음소프트웨어 생성 인터럽트
Percpuhandle_percpu_irq()Per-CPU ACKPer-CPUIPI, 로컬 타이머
/* kernel/irq/chip.c - Edge vs Level 핵심 차이점 */

/* Edge-triggered: ACK 먼저 → 핸들러 → 재발생 확인 */
void handle_edge_irq(struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);
    desc->irq_data.chip->irq_ack(&desc->irq_data);  /* 즉시 ACK */

    do {
        handle_irq_event(desc);  /* 핸들러 체인 실행 */
    } while (desc->istate & IRQS_PENDING);  /* 실행 중 재발생? */

    raw_spin_unlock(&desc->lock);
}

/* Level-triggered: Mask → 핸들러 → Unmask */
void handle_level_irq(struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);
    mask_ack_irq(desc);  /* Mask + ACK (재발생 방지) */

    handle_irq_event(desc);  /* 핸들러가 HW 원인 제거 */

    cond_unmask_irq(desc);  /* Unmask (이제 안전) */
    raw_spin_unlock(&desc->lock);
}
Edge Trigger vs Level Trigger 타이밍 비교 Edge-Triggered Signal IRQ 1 IRQ 2 Handler 핸들러 실행 ACK 즉시 ACK 핸들러 실행 중 재발생 → IRQS_PENDING 플래그 특징: + ACK 즉시 → 짧은 마스크 시간 - 핸들러 중 재발생 시 유실 위험 do-while 루프로 PENDING 재확인 대표: GPIO edge, 레거시 ISA MSI/MSI-X → handle_fasteoi_irq() (메모리 쓰기 기반, 유실 없음) Level-Triggered Signal High 유지 클리어 Handler 핸들러 실행 Mask Masked (인터럽트 차단) Unmask 특징: + 인터럽트 유실 없음 - 핸들러가 HW 원인 클리어 필수 - 미클리어 시 IRQ storm 발생 대표: PCI INTx, I2C/SPI GPIO Threaded IRQ 시 IRQF_ONESHOT 필수 (threaded handler 완료까지 masked)
Edge-triggered는 신호 전환점에서, Level-triggered는 신호가 유지되는 동안 인터럽트가 활성화됩니다. 각각의 flow handler가 ACK/Mask 타이밍을 다르게 처리합니다.

Softirq 타입

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

Softirq 우선순위(Priority) 순서

번호이름용도주요 사용처
0HI_SOFTIRQ고우선순위 Tasklet드라이버 Tasklet (높은 우선순위)
1TIMER_SOFTIRQ타이머 만료커널 타이머, hrtimer
2NET_TX_SOFTIRQ네트워크 송신패킷 전송 완료 처리
3NET_RX_SOFTIRQ네트워크 수신패킷 수신 처리 (NAPI)
4BLOCK_SOFTIRQ블록 I/O블록 디바이스 I/O 완료
5IRQ_POLL_SOFTIRQIRQ 폴링(Polling)고성능 블록 디바이스 (NVMe)
6TASKLET_SOFTIRQ일반 Tasklet드라이버 Tasklet (일반)
7SCHED_SOFTIRQ스케줄러(Scheduler)로드 밸런싱, CFS
8HRTIMER_SOFTIRQ고해상도 타이머(hrtimer)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 비트맵(Bitmap) 하위 비트부터 순회합니다.

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 비트맵 순회 후 재시작(Reboot) 조건 판단 (최대 10회, 2ms)

Hardirq 핸들러 체인

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

/* 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 커널 스레드(Kernel Thread)

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 공정성(Fairness): 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를 처리합니다.

인터럽트 컨텍스트 확인

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

컨텍스트 확인 매크로

매크로의미포함 범위
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 벡터 분석

10개의 softirq 벡터는 각각 커널의 핵심 서브시스템에 전용으로 할당되어 있습니다. 각 벡터의 내부 동작을 이해하면 /proc/softirqs 분석 시 어떤 서브시스템이 병목인지 빠르게 파악할 수 있습니다.

TIMER_SOFTIRQ: 타이머 휠 상호작용

TIMER_SOFTIRQ는 커널 타이머(timer_list)의 만료 콜백을 처리합니다. 타이머 휠(Timer Wheel)은 계층적 구조로 구성되어 효율적인 만료 검사를 수행합니다.

/* kernel/time/timer.c - run_timer_softirq 핵심 경로 */
static void run_timer_softirq(struct softirq_action *h)
{
    struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);

    __run_timers(base);
}

static void __run_timers(struct timer_base *base)
{
    raw_spin_lock_irq(&base->lock);

    /* base->clk를 현재 jiffies까지 전진시키며 만료 타이머 수집 */
    while (time_after_eq(jiffies, base->clk) &&
           (base->clk < base->next_expiry)) {
        struct hlist_head heads[LVL_DEPTH];
        int levels;

        /* 타이머 휠 각 레벨에서 만료 타이머 수집 */
        levels = collect_expired_timers(base, heads);
        base->clk++;

        while (levels--) {
            expire_timers(base, heads + levels);
        }
    }
    raw_spin_unlock_irq(&base->lock);
}

static void expire_timers(struct timer_base *base,
                          struct hlist_head *head)
{
    while (!hlist_empty(head)) {
        struct timer_list *timer;
        void (*fn)(struct timer_list *);

        timer = hlist_entry(head->first, struct timer_list, entry);
        base->running_timer = timer;  /* 디버그 추적 */
        fn = timer->function;

        raw_spin_unlock_irq(&base->lock);
        call_timer_fn(timer, fn, base->clk);  /* 콜백 실행 */
        raw_spin_lock_irq(&base->lock);
    }
}

타이머 휠은 9개 레벨(LVL_DEPTH)로 구성되며, 각 레벨은 64개 버킷(Bucket)을 가집니다. 레벨이 올라갈수록 타이머 해상도가 낮아지고 먼 미래의 타이머를 관리합니다.

레벨해상도 (granularity)범위총 버킷 수
01 tick (1ms @ HZ=1000)0 ~ 63 ticks64
18 ticks64 ~ 511 ticks64
264 ticks512 ~ 4095 ticks64
3512 ticks4096 ~ 32767 ticks64
44096 ticks (~4초)~32초 ~ ~4분64
5~8점점 증가수시간 ~ 수일각 64

HRTIMER_SOFTIRQ와의 관계: 고해상도 타이머(hrtimer)는 별도의 레드-블랙 트리(Red-Black Tree)를 사용합니다. 하드웨어 hrtimer가 사용 가능하면 hardirq 컨텍스트에서 직접 콜백을 실행하고, 불가능하면 HRTIMER_SOFTIRQ로 폴백합니다. CONFIG_NO_HZ_IDLE/CONFIG_NO_HZ_FULL 설정은 유휴/격리 CPU의 타이머 틱(Tick)을 억제하여 TIMER_SOFTIRQ 빈도를 크게 줄입니다.

BLOCK_SOFTIRQ와 IRQ_POLL_SOFTIRQ: 블록 I/O 완료

BLOCK_SOFTIRQ는 블록 디바이스의 I/O 완료 처리를 담당합니다. 디바이스 드라이버가 I/O 완료를 통지하면 Per-CPU blk_cpu_done 리스트에 request가 쌓이고, softirq에서 배치로 완료 콜백을 실행합니다.

/* block/blk-softirq.c - 블록 I/O 완료 softirq */
static DEFINE_PER_CPU(struct llist_head, blk_cpu_done);

static void blk_done_softirq(struct softirq_action *h)
{
    struct llist_node *entry;

    /* Per-CPU 완료 리스트를 atomic하게 가져옴 */
    entry = llist_del_all(this_cpu_ptr(&blk_cpu_done));
    entry = llist_reverse_order(entry);

    while (entry) {
        struct request *rq;
        rq = llist_entry(entry, struct request, ipi_list);
        entry = entry->next;

        rq->q->mq_ops->complete(rq);  /* blk-mq 완료 콜백 */
    }
}

/* 드라이버에서 I/O 완료 통지 */
void blk_complete_request(struct request *rq)
{
    /* 완료를 처리할 CPU 선택 (요청을 발생시킨 CPU 우선) */
    int cpu = cyclic_submit_cpu;

    if (llist_add(&rq->ipi_list, &per_cpu(blk_cpu_done, cpu)))
        raise_softirq_irqoff(BLOCK_SOFTIRQ);
}

IRQ_POLL_SOFTIRQ는 NVMe 등 고성능 블록 디바이스를 위한 NAPI 스타일 폴링 메커니즘입니다. 기존 인터럽트 방식 대신 폴링으로 I/O 완료를 감지하여 인터럽트 오버헤드를 제거합니다.

구분BLOCK_SOFTIRQIRQ_POLL_SOFTIRQ
동작 방식인터럽트 기반 완료 통지폴링 기반 완료 감지 (NAPI 스타일)
지연시간인터럽트 오버헤드 포함인터럽트 없이 즉시 감지 가능
CPU 비용낮음 (이벤트 기반)높음 (폴링 루프)
주요 사용처SATA, 일반 NVMe고성능 NVMe, SPDK
설정기본 활성io_poll=1 마운트 옵션 또는 io_uring IOPOLL

SCHED_SOFTIRQ: CPU 간 로드 밸런싱

SCHED_SOFTIRQ는 CFS(Completely Fair Scheduler) 또는 EEVDF 스케줄러의 CPU 간 로드 밸런싱을 트리거합니다. 스케줄링 도메인(Scheduling Domain) 계층을 순회하며 부하 불균형을 감지하고 태스크를 마이그레이션합니다.

/* kernel/sched/fair.c - SCHED_SOFTIRQ 핸들러 */
static void run_rebalance_domains(struct softirq_action *h)
{
    struct rq *this_rq = this_rq();
    enum cpu_idle_type idle;

    /* 현재 CPU가 idle인지 판단 */
    idle = this_rq->idle_balance ? CPU_IDLE : CPU_NOT_IDLE;

    /* nohz 밸런싱 트리거 (유휴 CPU 대신 밸런싱) */
    nohz_idle_balance(this_rq, idle);

    /* 스케줄링 도메인 계층 순회하며 밸런싱 */
    rebalance_domains(this_rq, idle);
}

static void rebalance_domains(struct rq *rq,
                              enum cpu_idle_type idle)
{
    struct sched_domain *sd;
    int continue_balancing = 1;

    /* 아래에서 위로: SMT → MC → DIE → NUMA */
    for_each_domain(cpu_of(rq), sd) {
        if (time_after_eq(jiffies, sd->next_balance)) {
            load_balance(cpu_of(rq), rq, sd, idle,
                        &continue_balancing);
            sd->next_balance = jiffies + sd->balance_interval;
        }
    }
}
SCHED_SOFTIRQ: 스케줄링 도메인 계층 밸런싱 NUMA Domain (sd→balance_interval: 128ms) DIE Domain (64ms) DIE Domain (64ms) MC (8ms) MC (8ms) MC (8ms) MC (8ms) CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7 load_balance() DIE 레벨 밸런싱 SCHED_SOFTIRQ → trigger_load_balance() → for_each_domain(cpu, sd) { load_balance(cpu, rq, sd, idle) } /* 아래→위 순회: MC → DIE → NUMA */
SCHED_SOFTIRQ는 스케줄링 도메인 계층을 아래(MC)에서 위(NUMA)로 순회하며 로드 밸런싱을 수행합니다. 각 레벨의 balance_interval이 다릅니다.

SCHED_SOFTIRQ 과다 발생 시: /proc/softirqs에서 SCHED 카운트가 비정상적으로 높다면, NUMA 토폴로지(Topology)가 비대칭이거나 CPU 간 부하 불균형이 심한 상태입니다. /proc/schedstat에서 도메인별 밸런싱 횟수와 태스크 마이그레이션(Migration) 횟수를 확인하세요. isolcpus=domain으로 특정 CPU를 밸런싱에서 제외하면 해당 CPU의 SCHED_SOFTIRQ가 사라집니다.

RCU_SOFTIRQ: Grace Period와 콜백 처리

RCU_SOFTIRQ는 RCU(Read-Copy-Update) 서브시스템의 핵심 경로입니다. Grace period 진행 상태를 확인하고, 완료된 grace period의 콜백을 배치로 실행합니다.

/* kernel/rcu/tree.c - RCU softirq 핸들러 */
static void rcu_core_si(struct softirq_action *h)
{
    rcu_core();
}

static void rcu_core(void)
{
    struct rcu_data *rdp = this_cpu_ptr(&rcu_data);
    struct rcu_node *rnp = rdp->mynode;

    /* 1. 정지 상태(Quiescent State) 보고 */
    if (rdp->core_needs_qs) {
        rcu_report_qs_rdp(rdp);
    }

    /* 2. 완료된 grace period의 콜백 진행 */
    if (rcu_segcblist_ready_cbs(&rdp->cblist)) {
        rcu_do_batch(rdp);
    }

    /* 3. 새 grace period 시작 필요 시 처리 */
    rcu_check_gp_start_stall(rnp, rdp, rcu_jiffies_till_stall_check());
}

/* rcu_do_batch(): 콜백 배치 실행 */
static void rcu_do_batch(struct rcu_data *rdp)
{
    long bl = rdp->blimit;  /* 한 번에 처리할 콜백 수 제한 */

    while (rcu_segcblist_ready_cbs(&rdp->cblist) && bl--) {
        struct rcu_head *rhp;
        rhp = rcu_segcblist_dequeue(&rdp->cblist);

        /* RCU 콜백 실행 (kfree, 구조체 해제 등) */
        debug_rcu_head_callback(rhp);
        rhp->func(rhp);
    }
}

Per-CPU rcu_data 구조체는 세그먼트 콜백 리스트(segmented callback list)를 관리합니다. 콜백은 4단계로 분류됩니다:

세그먼트상태설명
RCU_DONE_TAIL실행 가능grace period 완료, 즉시 실행 대기
RCU_WAIT_TAIL대기 중현재 grace period 완료를 기다림
RCU_NEXT_READY_TAIL다음 대기다음 grace period에 처리될 예정
RCU_NEXT_TAIL새로 등록방금 call_rcu()로 등록됨

RCU_SOFTIRQ 과다 시 대응

/proc/softirqs에서 RCU 카운트가 비정상적으로 높거나, dmesg에 "rcu_preempt detected stalls" 메시지가 나타나면:

  • 원인: 장시간 softirq/hardirq 실행이 quiescent state 보고를 지연 → grace period 연장 → 콜백 적체
  • 확인: cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
  • 대응: rcu_nocbs=N-M 커널 파라미터로 지정 CPU의 RCU 콜백을 전용 rcuog/N kthread로 오프로드. 해당 CPU에서 RCU_SOFTIRQ가 사라집니다.

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

CPU 격리와 Softirq

지연시간에 민감한 애플리케이션(HFT, 실시간 제어, 통신 장비)은 특정 CPU를 완전히 격리하여 softirq, 스케줄러 밸런싱, RCU 콜백 등의 간섭을 제거해야 합니다. Linux는 세 가지 커널 부트 파라미터를 조합하여 이를 달성합니다.

isolcpus, nohz_full, rcu_nocbs

파라미터기능제거되는 softirq
isolcpus=managed_irq,domain,N-M스케줄러 도메인 제외, managed IRQ 밸런싱 제외SCHED_SOFTIRQ 제거
nohz_full=N-M유휴/단일 태스크 시 타이머 틱 억제 (adaptive-ticks)TIMER_SOFTIRQ 대폭 감소
rcu_nocbs=N-MRCU 콜백을 전용 rcuog/N kthread로 오프로드RCU_SOFTIRQ 제거
IRQ affinity 수동 설정/proc/irq/N/smp_affinity로 IRQ를 하우스키핑 CPU에 고정NET_RX/TX, BLOCK 제거
# 완전 격리 커널 커맨드라인 예시 (CPU 4-7 격리)
isolcpus=managed_irq,domain,4-7 nohz_full=4-7 rcu_nocbs=4-7 irqaffinity=0-3

# 격리 상태 확인
cat /sys/devices/system/cpu/nohz_full
# 출력: 4-7

cat /sys/devices/system/cpu/isolated
# 출력: 4-7

# rcuog kthread 확인 (RCU 콜백 오프로드 스레드)
ps -eo pid,psr,comm | grep rcuog
# rcuog/4, rcuog/5, rcuog/6, rcuog/7 → 하우스키핑 CPU에서 실행

# 격리 CPU에서 softirq 거의 없는지 확인
cat /proc/softirqs
#            CPU0      CPU1      CPU2      CPU3      CPU4  CPU5  CPU6  CPU7
# TIMER: 13691355  12345678  11234567  10123456        2     1     1     0
# SCHED:  8745632   7654321   6543210   5432109        0     0     0     0
# RCU:    5367890   4567890   3456789   2345678        0     0     0     0

# 격리 CPU에 애플리케이션 고정
taskset -c 4-7 ./latency_critical_app
CPU 격리: 하우스키핑 vs 격리 CPU의 Softirq 분포 하우스키핑 CPU (0-3) 모든 HW IRQ 처리 NET_RX/TX SOFTIRQ TIMER SOFTIRQ SCHED SOFTIRQ RCU SOFTIRQ BLOCK SOFTIRQ ksoftirqd/0~3, rcuog/4~7, irqbalance 커널 데몬, 시스템 서비스 실행 irqaffinity=0-3 → 모든 IRQ가 이 CPU에 집중 높은 %soft, %irq 허용 격리 CPU (4-7) 지연시간 민감 애플리케이션 taskset -c 4-7 ./app IRQ 없음 / softirq 거의 없음 타이머 틱 억제 (nohz_full) 남아 있을 수 있는 간섭: • IPI (TLB shootdown) — 완전 제거 불가 • 페이지 폴트(Page Fault) 시 커널 경로 cyclictest p99 < 10μs 달성 가능 (PREEMPT_RT + 격리 조합 시)
하우스키핑 CPU가 모든 인터럽트와 softirq를 흡수하여 격리 CPU는 최소한의 커널 간섭만 받습니다.

CPU 격리 체크리스트

  • 커널 커맨드라인: isolcpus=managed_irq,domain,N-M nohz_full=N-M rcu_nocbs=N-M irqaffinity=0-(N-1)
  • IRQ 수동 확인: for i in /proc/irq/*/smp_affinity; do echo "$i: $(cat $i)"; done
  • ksoftirqd 확인: ps -eo pid,psr,comm | grep ksoftirq — 격리 CPU의 ksoftirqd가 활성이면 softirq가 여전히 발생 중
  • 검증: cyclictest --mlockall --smp --priority=80 --interval=200 --distance=0 -D 60
  • 주의: nohz_full CPU에서 2개 이상 태스크가 실행되면 스케줄링을 위해 틱이 복원됩니다. 격리 CPU당 1개 태스크가 이상적입니다.

BPF/XDP와 Softirq 컨텍스트

XDP(eXpress Data Path)와 TC BPF(Traffic Control BPF)는 NET_RX_SOFTIRQ 경로에서 BPF 프로그램을 실행합니다. 이는 커널 네트워크 스택의 매우 초기 단계에서 패킷을 처리하여 높은 성능을 달성하지만, softirq 실행 시간에 직접 영향을 미칩니다.

모드실행 위치softirq 영향성능
XDP Native드라이버 NAPI poll 내부 (skb 할당 전)softirq 시간 증가최고 (10M+ pps)
XDP Genericnetif_receive_skb_core() (skb 할당 후)softirq 시간 증가 (Native보다 느림)높음
XDP OffloadNIC 하드웨어softirq 영향 없음최고 (CPU 무관)
TC BPF__netif_receive_skb_core() ingress/egresssoftirq 시간 증가높음
/* XDP 실행 지점 — 드라이버 NAPI poll 내부 (igb 예시) */

/* drivers/net/ethernet/intel/igb/igb_main.c (간략화) */
static int igb_clean_rx_irq(struct igb_q_vector *q_vec,
                            const int budget)
{
    while (likely(total_packets < budget)) {
        struct xdp_buff xdp;

        /* DMA 완료된 디스크립터에서 데이터 포인터 설정 */
        xdp_prepare_buff(&xdp, hard_start, headroom, size, true);

        /* ★ XDP BPF 프로그램 실행 (skb 할당 전!) */
        u32 act = bpf_prog_run_xdp(xdp_prog, &xdp);

        switch (act) {
        case XDP_PASS:
            break;     /* 정상 경로: skb 생성 후 스택 진행 */
        case XDP_DROP:
            igb_rx_buffer_flip(rx_ring, rx_buffer);
            continue;  /* 패킷 폐기: skb 미생성, 매우 빠름 */
        case XDP_TX:
            igb_xdp_xmit_back(adapter, &xdp);
            continue;  /* 같은 NIC으로 TX: 헤어핀 모드 */
        case XDP_REDIRECT:
            xdp_do_redirect(netdev, &xdp, xdp_prog);
            continue;  /* 다른 NIC/AF_XDP 소켓으로 전달 */
        }

        /* XDP_PASS: 일반 경로 진행 — skb 할당 + GRO */
        skb = igb_construct_skb(rx_ring, rx_buffer, &xdp, timestamp);
        napi_gro_receive(&q_vector->napi, skb);
    }
}

XDP의 softirq 영향 모니터링

복잡한 BPF 프로그램은 패킷당 처리 시간을 증가시켜 ksoftirqd 이관 빈도가 높아질 수 있습니다. 다음 명령으로 BPF 프로그램별 실행 시간을 확인하세요:

# BPF 프로그램별 실행 시간/횟수 확인
bpftool prog show
# 출력:  run_time_ns: 1234567890  run_cnt: 50000000
# → 평균 24.7ns/packet

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

# XDP 통계 확인
ip -s link show dev eth0 xdp
# XDP xdp0: prog_id 42  drv  actions: pass 1000 drop 50000 redirect 0

/proc/softirqs 분석 방법

/proc/softirqs는 부팅 이후 각 CPU에서 처리된 softirq 횟수를 누적 표시합니다. 이 데이터를 주기적으로 캡처하여 차이를 계산하면 실시간(Real-time) 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은 절대 병렬 실행되지 않는 직렬화(Serialization)를 보장합니다. 자세한 내용은 Tasklet 페이지를 참고하세요.
Workqueue (CMWQ): 프로세스 컨텍스트에서 실행되는 Bottom Half. 슬립(Sleep)이 가능하여 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 처리량이 한계에 도달한 것입니다.

인터럽트 코얼레싱(Interrupt Coalescing)

인터럽트 코얼레싱은 여러 이벤트를 하나의 인터럽트로 합쳐서 hardirq 빈도를 줄이는 기법입니다. 인터럽트가 줄어들면 softirq 트리거 빈도도 함께 줄어들어 CPU 오버헤드가 감소하지만, 개별 이벤트의 지연시간은 증가합니다.

하드웨어 코얼레싱 (ethtool -C)

대부분의 NIC는 하드웨어 수준의 인터럽트 코얼레싱을 지원합니다. ethtool -C로 설정합니다.

파라미터의미기본값 (일반적)효과
rx-usecsRX 인터럽트 지연 (μs)3~50높을수록 IRQ 감소, 지연 증가
rx-framesRX 프레임 수 임계값0 (비활성)높을수록 배치 크기 증가
tx-usecsTX 완료 인터럽트 지연 (μs)0~100TX 완료 배치 처리
tx-framesTX 프레임 수 임계값0 (비활성)TX 완료 배치 크기
adaptive-rxNIC이 RX 코얼레싱 자동 조정on/off부하에 따라 동적 조절
adaptive-txNIC이 TX 코얼레싱 자동 조정on/offTX 측 동적 조절
# 현재 코얼레싱 설정 확인
ethtool -c eth0
# Adaptive RX: on  TX: off
# rx-usecs: 3
# rx-frames: 0
# tx-usecs: 0
# tx-frames: 0

# 저지연 설정 (HFT, 실시간): 코얼레싱 최소화
ethtool -C eth0 rx-usecs 0 rx-frames 1 adaptive-rx off

# 고처리량 서버: 공격적 코얼레싱
ethtool -C eth0 rx-usecs 100 rx-frames 64 adaptive-rx off

# 균형 설정: NIC 자동 조정
ethtool -C eth0 adaptive-rx on adaptive-tx on

# 인터럽트 코얼레싱 효과 확인: IRQ 빈도 측정
watch -d -n 1 'grep eth0 /proc/interrupts'

소프트웨어 코얼레싱과 Busy Polling

하드웨어 코얼레싱 외에도 소프트웨어 수준에서 인터럽트-softirq 경로를 최적화하는 방법이 있습니다. 특히 Busy Polling은 softirq를 완전히 우회하는 극단적 최적화입니다.

Busy Polling은 애플리케이션 스레드가 직접 NIC을 폴링하여 패킷을 가져옵니다. IRQ → softirq → wakeup 체인을 건너뛰므로 지연시간이 극도로 낮아지지만, CPU를 전용으로 소비합니다.

/* 소켓 레벨 busy polling 설정 */
int val = 50;  /* 폴링 대기 시간 (μs) */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &val, sizeof(val));

/* SO_PREFER_BUSY_POLL: 가능하면 항상 busy poll 우선 */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &prefer, sizeof(prefer));
# 시스템 전체 busy polling 설정
sysctl -w net.core.busy_poll=50     # poll() 대기 시간 (μs)
sysctl -w net.core.busy_read=50     # read() 대기 시간 (μs)

# NAPI prefer busy poll (커널 5.13+)
echo 1 > /sys/class/net/eth0/napi_defer_hard_irqs
echo 200000 > /sys/class/net/eth0/gro_flush_timeout  # 200μs
패킷 수신 경로 비교: IRQ vs NAPI vs Busy Polling 전통적 IRQ (코얼레싱 없음) 패킷 도착 → HW IRQ NET_RX_SOFTIRQ 프로토콜 스택 처리 wakeup 수신 프로세스 지연: ~15-50μs IRQ per packet CPU 비용: 높음 처리량: 낮음 NAPI + HW 코얼레싱 N개 패킷 도착 → 1 HW IRQ NET_RX_SOFTIRQ NAPI poll: N개 배치 처리 프로토콜 스택 + wakeup 지연: ~20-100μs 1 IRQ per N packets CPU 비용: 중간 처리량: 높음 범용 서버 권장 Busy Polling 패킷 도착 (IRQ 비활성) 앱 스레드가 직접 poll() NAPI poll 직접 호출 즉시 데이터 반환 지연: ~2-5μs IRQ 없음, softirq 없음 CPU 비용: 전용 코어 처리량: 중간 HFT, DPDK-lite
코얼레싱 수준이 높아질수록 처리량은 증가하고 CPU 비용은 감소하지만, 개별 패킷의 지연시간은 증가합니다. Busy Polling은 지연시간을 극소화하지만 CPU 코어를 전용으로 사용합니다.
접근 방식지연시간CPU 비용처리량용도
코얼레싱 없음가장 낮음매우 높음 (IRQ/패킷)낮음비실용적
HW 코얼레싱중간중간높음범용 서버
Adaptive 코얼레싱가변자동 조절높음혼합 워크로드
Busy Polling매우 낮음전용 코어중간HFT, 저지연 네트워킹
DPDK / 커널 바이패스최저전용 코어(들)최고전용 네트워크 어플라이언스

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()하드 인터럽트(Hardirq) 선점 고려
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 재배치(Relocation) 필요
softirq 실행 시간perf sched, tracepointNET_RX 단일 이벤트가 길면 NAPI budget 과대 가능성
ksoftirqd 런큐(Runqueue) 대기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 소스 매핑(Mapping)
    /proc/interrupts에서 급증 IRQ를 NIC 큐/스토리지 큐와 매핑합니다.
  4. 분산 정책 교정
    IRQ affinity, RPS/RFS/XPS를 재정렬해 한 CPU 집중을 먼저 해소합니다.
  5. budget/코얼레싱 조정
    NAPI budget과 인터럽트 코얼레싱 값을 동시에 조정하며 처리량 대비 지연시간을 측정합니다.
  6. RT/격리(Isolation) 검토
    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 타입으로 먼저 분해하면 분석 시간이 크게 줄어듭니다.

do_IRQ() → handle_irq_event() 호출 체인

하드웨어 인터럽트가 발생하면 CPU는 IDT(Interrupt Descriptor Table)를 통해 아키텍처별 진입점에 도달하고, 최종적으로 등록된 핸들러를 호출합니다. x86 기준 전체 호출 체인은 다음과 같습니다.

Hardirq Dispatch 호출 체인 (x86) HW Interrupt IDT entry_point do_IRQ() generic_handle_irq() handle_fasteoi_irq() handle_edge_irq() 또는 다른 flow handler handle_irq_event() handle_irq_event_percpu() __handle_irq_event_percpu() irq_enter() → preempt_count += HARDIRQ_OFFSET raw_spin_lock → action chain 순회 action→handler(irq, dev_id) 호출
하드웨어 인터럽트 발생부터 등록된 핸들러 실행까지의 전체 호출 체인입니다. flow handler는 인터럽트 컨트롤러 타입에 따라 달라집니다.

최종 단계인 __handle_irq_event_percpu()irq_desc에 연결된 irqaction 체인을 순회하며 각 핸들러를 호출합니다. 핸들러의 반환값(irqreturn_t)에 따라 후속 처리가 결정됩니다.

/* kernel/irq/handle.c - 간소화 */
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc)
{
    irqreturn_t retval = IRQ_NONE;
    struct irqaction *action;

    /* irqaction 체인 순회 (공유 IRQ 지원) */
    for_each_action_of_desc(desc, action) {
        irqreturn_t res;

        trace_irq_handler_entry(desc->irq_data.irq, action);
        res = action->handler(desc->irq_data.irq, action->dev_id);
        trace_irq_handler_exit(desc->irq_data.irq, action, res);

        switch (res) {
        case IRQ_WAKE_THREAD:
            __irq_wake_thread(desc, action);
            /* fall through */
        case IRQ_HANDLED:
            retval |= res;
            break;
        default:
            break;
        }
    }

    return retval;
}
코드 설명
  • 3행retvalIRQ_NONE으로 초기화합니다. 어떤 핸들러도 이 인터럽트를 처리하지 않으면 이 값이 반환됩니다.
  • 7행for_each_action_of_descdesc->action 연결 리스트를 순회하는 매크로입니다. 공유 IRQ(IRQF_SHARED)에서는 여러 핸들러가 체인으로 연결됩니다.
  • 11행핸들러 함수(action->handler)를 IRQ 번호와 디바이스 식별자(dev_id)를 인자로 호출합니다. 이것이 드라이버가 request_irq()로 등록한 실제 핸들러입니다.
  • 15행IRQ_WAKE_THREAD는 threaded IRQ에서 사용됩니다. hardirq 핸들러가 빠르게 ACK만 하고, 실제 처리는 커널 스레드(action->thread_fn)에 위임합니다.
  • 16행__irq_wake_thread()action->thread 커널 스레드를 깨웁니다. 이 스레드는 request_threaded_irq()로 등록 시 자동 생성됩니다.
  • 19행IRQ_HANDLED는 이 핸들러가 인터럽트를 성공적으로 처리했음을 의미합니다. retval에 OR 연산으로 누적됩니다.
  • 22행IRQ_NONE을 반환한 핸들러는 해당 인터럽트가 자신의 디바이스에서 발생한 것이 아님을 의미합니다. 공유 IRQ에서 다음 핸들러가 시도합니다.

irqreturn_t 반환값 분석

반환값의미후속 동작
IRQ_NONE이 핸들러의 디바이스가 인터럽트를 발생시키지 않았음체인의 다음 핸들러 시도. 모든 핸들러가 IRQ_NONE이면 spurious IRQ 카운터 증가
IRQ_HANDLED인터럽트를 성공적으로 처리함정상 완료. /proc/interrupts 카운터 증가
IRQ_WAKE_THREADhardirq에서 최소 처리 후 스레드 처리 요청action->thread 커널 스레드 깨움, thread_fn() 실행

모든 핸들러가 IRQ_NONE을 반환하면 커널은 note_interrupt()를 통해 spurious interrupt를 추적합니다. 연속 99,900회 이상 spurious가 발생하면 해당 IRQ 라인이 비활성화(__report_bad_irq)됩니다.

Softirq Raise/Execute 사이클 상세

Softirq의 생명주기는 크게 두 단계로 나뉩니다: Raise(대기 등록)와 Execute(실행). 이 두 단계는 Per-CPU pending 비트맵을 매개로 비동기적으로 연결됩니다.

Raise 경로

raise_softirq()는 지정된 softirq 번호에 해당하는 비트를 Per-CPU __softirq_pending에 설정합니다. 이 함수는 hardirq 핸들러, tasklet, 타이머 콜백 등 다양한 컨텍스트에서 호출됩니다.

Execute 경로

Softirq 실행은 세 가지 경로에서 트리거됩니다:

  1. irq_exit() — hardirq 처리 완료 후 pending softirq가 있으면 __do_softirq() 호출
  2. local_bh_enable() — BH 비활성화 해제 시 pending 확인
  3. ksoftirqd — 위 두 경로에서 예산 초과 시 커널 스레드로 위임
Softirq Raise → Pending → Execute 사이클 Raise 경로 raise_softirq(nr) raise_softirq_irqoff(nr) __raise_softirq_irqoff(nr) or_softirq_pending(1 << nr) __softirq_pending (Per-CPU) bit 0..9 = 10종 softirq Execute 트리거 1. irq_exit() 2. local_bh_enable() 3. ksoftirqd 스레드 __do_softirq() pending 비트 스캔 루프 pending = local_softirq_pending() softirq_vec[i].action() 호출 재시작 ≤ 10회 & 시간 이내 예산 초과 wakeup_softirqd()
Softirq는 raise로 pending 비트를 설정하고, irq_exit() 등의 실행 지점에서 __do_softirq()가 비트맵을 스캔하여 처리합니다. 예산 초과 시 ksoftirqd로 위임됩니다.

__do_softirq() 루프 상세

__do_softirq()는 pending 비트맵을 순회하며 각 softirq 핸들러를 실행합니다. 재시작 횟수(MAX_SOFTIRQ_RESTART = 10)와 시간 제한(MAX_SOFTIRQ_TIME = 2ms)을 두어 softirq가 CPU를 독점하는 것을 방지합니다.

/* kernel/softirq.c - __do_softirq() 핵심 루프 상세 */
#define MAX_SOFTIRQ_TIME  msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART  10

asmlinkage __visible void __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;
    struct softirq_action *h;
    bool in_hardirq;
    __u32 pending;

    /* softirq 진입: preempt_count에 SOFTIRQ_OFFSET 추가 */
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);

    /* hardirq에서 왔는지 확인 (irq_exit 경로) */
    in_hardirq = lockdep_softirq_start();

    /* 현재 CPU의 pending 비트를 읽고 초기화 */
restart:
    pending = local_softirq_pending();
    set_softirq_pending(0);

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

    h = softirq_vec;

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

            kstat_incr_softirqs_this_cpu(vec_nr);
            trace_softirq_entry(vec_nr);
            h->action(h);          /* ← softirq 핸들러 실행 */
            trace_softirq_exit(vec_nr);
        }
        h++;
        pending >>= 1;
    }

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

    pending = local_softirq_pending();
    if (pending) {
        /* 재시작 횟수 남아있고, 시간 제한 내이면 재시작 */
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;

        /* 예산 초과: ksoftirqd에 위임 */
        wakeup_softirqd();
    }

    __local_bh_enable(SOFTIRQ_OFFSET);
    current->flags = old_flags;
}
코드 설명
  • 2-3행MAX_SOFTIRQ_TIME은 2ms, MAX_SOFTIRQ_RESTART는 10으로 정의됩니다. 이 두 값이 softirq의 CPU 독점을 방지하는 핵심 제한입니다.
  • 15행preempt_countSOFTIRQ_OFFSET을 추가하여 softirq 컨텍스트에 진입했음을 표시합니다. in_softirq()가 true를 반환하게 됩니다.
  • 22-23행pending 비트를 읽은 뒤 즉시 0으로 초기화합니다. 이 사이에 새로 raise된 softirq는 다음 루프에서 처리됩니다.
  • 25행softirq 핸들러 실행 중에는 인터럽트를 활성화합니다. 따라서 softirq 처리 중에도 hardirq를 받을 수 있습니다.
  • 36행h->action(h)가 실제 softirq 핸들러를 호출합니다. 예: NET_RX_SOFTIRQ → net_rx_action()
  • 44행핸들러 실행 완료 후 인터럽트를 다시 비활성화하고 pending 비트를 재확인합니다. 실행 중 새로운 softirq가 raise되었을 수 있기 때문입니다.
  • 47-49행세 가지 조건을 모두 만족해야 재시작합니다: (1) 시간 제한 미초과, (2) 스케줄링 필요 없음, (3) 재시작 횟수 남음.
  • 52행재시작 조건을 만족하지 못하면 wakeup_softirqd()로 ksoftirqd 스레드를 깨워 나머지 처리를 위임합니다.

struct irq_desc / irqaction 구조체 분석

리눅스 커널의 인터럽트 관리는 두 핵심 구조체를 중심으로 구성됩니다. struct irq_desc는 각 IRQ 번호에 대한 메타데이터를, struct irqaction은 등록된 핸들러 정보를 담습니다.

irq_desc → irqaction 체인 구조 struct irq_desc irq_data .irq, .hwirq, .chip, .domain action → (irqaction 체인) handle_irq (flow handler) status_use_accessors depth (중첩 disable 카운터) irq_count irqs_unhandled threads_handled kstat_irqs (Per-CPU 통계) name lock (raw_spinlock_t) affinity_hint percpu_enabled struct irqaction [0] handler (hardirq 핸들러) thread_fn (스레드 핸들러) thread (커널 스레드) secondary (중첩 action) irq, flags, name dev_id (식별자) percpu_dev_id next → struct irqaction [1] handler (공유 IRQ 핸들러) dev_id (다른 디바이스) flags: IRQF_SHARED next → NULL IRQF_SHARED: 공유 IRQ 체인
하나의 irq_desc에 여러 irqaction이 연결 리스트로 체인됩니다. 공유 IRQ(IRQF_SHARED)에서 각 디바이스 드라이버는 자신의 irqaction을 등록합니다.
/* include/linux/irqdesc.h - 핵심 필드 발췌 */
struct irq_desc {
    struct irq_data         irq_data;       /* IRQ 번호, HW IRQ, chip, domain */
    struct irqaction        *action;        /* 핸들러 연결 리스트 헤드 */
    irq_flow_handler_t     handle_irq;     /* flow handler (fasteoi, edge 등) */
    unsigned int           status_use_accessors; /* IRQ 상태 플래그 */
    unsigned int           depth;          /* disable 중첩 카운터 */
    unsigned int           irq_count;      /* spurious 감지용 인터럽트 총 횟수 */
    unsigned int           irqs_unhandled; /* spurious (IRQ_NONE) 횟수 */
    atomic_t               threads_handled; /* threaded 핸들러 완료 추적 */
    raw_spinlock_t         lock;           /* desc 보호 락 */
    unsigned int          *kstat_irqs;     /* Per-CPU 인터럽트 통계 */
    const char            *name;           /* /proc/interrupts 표시 이름 */
};

/* include/linux/interrupt.h - 핵심 필드 발췌 */
struct irqaction {
    irq_handler_t          handler;       /* hardirq 핸들러 (최상위 절반) */
    irq_handler_t          thread_fn;     /* threaded IRQ 핸들러 */
    struct task_struct     *thread;        /* IRQ 스레드 (자동 생성) */
    struct irqaction       *secondary;     /* 중첩 action (forced threading) */
    unsigned int           irq;           /* IRQ 번호 */
    unsigned int           flags;         /* IRQF_SHARED 등 플래그 */
    const char            *name;          /* /proc/interrupts 이름 */
    void                  *dev_id;        /* 디바이스 식별자 (공유 IRQ 구분) */
    void __percpu         *percpu_dev_id; /* Per-CPU 디바이스 ID */
    struct irqaction       *next;          /* 공유 IRQ 체인의 다음 action */
};
코드 설명
  • 3행irq_data는 IRQ 번호(가상), 하드웨어 IRQ 번호, irq_chip, irq_domain 등을 포함하는 하위 구조체입니다. 인터럽트 컨트롤러 추상화의 핵심입니다.
  • 4행action은 이 IRQ에 등록된 핸들러 체인의 헤드입니다. request_irq()로 등록할 때마다 새 irqaction이 이 체인에 추가됩니다.
  • 5행handle_irq는 flow handler로, 인터럽트 컨트롤러 종류에 따라 handle_fasteoi_irq, handle_edge_irq, handle_level_irq 등이 설정됩니다.
  • 7행depthdisable_irq()가 중첩 호출된 횟수입니다. 0이면 활성 상태이고, enable_irq()를 같은 횟수만큼 호출해야 실제로 활성화됩니다.
  • 19행handler는 hardirq 컨텍스트에서 실행되는 핸들러입니다. 인터럽트를 비활성화한 상태에서 최대한 빠르게 실행되어야 합니다.
  • 20행thread_fnrequest_threaded_irq()로 등록한 스레드 핸들러입니다. 프로세스 컨텍스트에서 실행되므로 sleep 가능한 작업을 수행할 수 있습니다.
  • 26행dev_id는 공유 IRQ에서 핸들러가 자신의 디바이스를 식별하는 데 사용됩니다. free_irq() 호출 시에도 이 값으로 어떤 핸들러를 제거할지 결정합니다.
  • 28행next 포인터로 같은 IRQ를 공유하는 다른 디바이스의 핸들러와 연결됩니다. PCI 디바이스에서 흔히 사용되는 공유 IRQ 패턴입니다.

ksoftirqd 스레드 상세

ksoftirqd는 Per-CPU 커널 스레드로, softirq 처리가 __do_softirq()의 재시작 예산을 초과했을 때 나머지 pending softirq를 처리합니다. 우선순위 SCHED_NORMAL(nice 0)로 실행되어 일반 프로세스와 CPU를 공유합니다.

/* kernel/softirq.c - ksoftirqd 메인 루프 */
static void run_ksoftirqd(unsigned int cpu)
{
    /* pending softirq가 있는지 확인 */
    ksoftirqd_run_begin();

    if (local_softirq_pending()) {
        /* __do_softirq() 호출 — 동일한 softirq 실행 루프 */
        __do_softirq();
        ksoftirqd_run_end();
        cond_resched();     /* 자발적 선점 포인트 */
        return;
    }
    ksoftirqd_run_end();
}

/* ksoftirqd를 깨우는 조건들 */
static void wakeup_softirqd(void)
{
    struct task_struct *tsk = __this_cpu_read(ksoftirqd);

    if (tsk)
        wake_up_process(tsk);
}
코드 설명
  • 2행run_ksoftirqd()는 Per-CPU 스레드의 메인 콜백입니다. smpboot 프레임워크에 의해 CPU 당 하나씩 생성되고 ksoftirqd/N으로 표시됩니다.
  • 5행ksoftirqd_run_begin()local_bh_disable()을 호출하여 softirq 재진입을 방지합니다.
  • 9행__do_softirq()를 직접 호출합니다. irq_exit() 경로와 동일한 코드를 사용하지만, 프로세스 컨텍스트에서 실행되므로 스케줄링이 가능합니다.
  • 11행cond_resched()는 자발적 선점 포인트입니다. softirq 처리 후 더 높은 우선순위 태스크가 있으면 양보합니다.
  • 20행__this_cpu_read(ksoftirqd)로 현재 CPU의 ksoftirqd 태스크를 가져옵니다. Per-CPU 변수이므로 락 없이 접근합니다.

wakeup_softirqd() 호출 조건

ksoftirqd가 깨어나는 주요 상황은 다음과 같습니다:

호출 지점조건의미
__do_softirq()재시작 10회 초과 또는 2ms 초과softirq 폭주로 인한 예산 초과
raise_softirq_irqoff()!in_interrupt() (hardirq 밖)프로세스 컨텍스트에서 softirq raise
irq_exit()pending이 있지만 이미 softirq 중첩재진입 방지를 위한 지연 처리

ksoftirqd의 실행 빈도는 /proc/softirqs에 직접 반영되지 않지만, ps -eo pid,psr,ni,comm | grep ksoftirq로 CPU 바인딩 상태와 실행 빈도를 확인할 수 있습니다. ksoftirqd가 높은 CPU 점유율을 보이면 softirq 폭주(특히 NET_RX)를 의심해야 합니다.

IRQ 스레드 마이그레이션과 어피니티

IRQ 어피니티(Affinity)는 특정 인터럽트를 어느 CPU에서 처리할지 제어하는 메커니즘입니다. 네트워크 카드의 RSS(Receive Side Scaling)나 NVMe의 다중 큐 구성에서 성능 최적화에 핵심적인 역할을 합니다.

irq_set_affinity() 경로

/* kernel/irq/manage.c - IRQ 어피니티 설정 경로 */
int irq_set_affinity(unsigned int irq, const struct cpumask *mask)
{
    struct irq_desc *desc = irq_to_desc(irq);
    unsigned long flags;

    if (!desc)
        return -EINVAL;

    raw_spin_lock_irqsave(&desc->lock, flags);
    int ret = __irq_set_affinity_locked(
        irq_desc_get_irq_data(desc), mask, false);
    raw_spin_unlock_irqrestore(&desc->lock, flags);

    return ret;
}

/* 핵심 내부 함수 */
static int __irq_set_affinity_locked(
    struct irq_data *data, const struct cpumask *mask,
    bool force)
{
    struct irq_chip *chip = irq_data_get_irq_chip(data);

    if (!chip || !chip->irq_set_affinity)
        return -EINVAL;

    /* 하드웨어 수준에서 어피니티 변경 */
    int ret = chip->irq_set_affinity(data, mask, force);

    if (ret == IRQ_SET_MASK_OK || ret == IRQ_SET_MASK_OK_DONE) {
        cpumask_copy(irq_data_get_affinity_mask(data), mask);
        irqd_set(data, IRQD_AFFINITY_SET);
    }

    return ret;
}
코드 설명
  • 2행irq_set_affinity()는 유저스페이스(/proc/irq/N/smp_affinity) 또는 커널 내부에서 호출되는 어피니티 설정 진입점입니다.
  • 4행irq_to_desc()로 IRQ 번호에 대응하는 irq_desc를 찾습니다. radix tree 또는 배열 기반 구현이 사용됩니다.
  • 10행desc->lock을 잡아 동시 접근을 방지합니다. raw_spin_lock은 PREEMPT_RT에서도 실제 스핀락으로 동작합니다.
  • 25행인터럽트 컨트롤러(irq_chip)가 irq_set_affinity 콜백을 제공해야 어피니티 변경이 가능합니다. APIC, GIC 등 대부분의 컨트롤러가 지원합니다.
  • 29행chip->irq_set_affinity()는 실제 하드웨어 레지스터(예: APIC의 Destination Register)를 프로그래밍합니다.
  • 32행성공 시 소프트웨어 마스크를 갱신하고, IRQD_AFFINITY_SET 플래그를 설정하여 사용자가 명시적으로 어피니티를 지정했음을 기록합니다.

CPU 핫플러그 시 IRQ 마이그레이션

CPU가 오프라인(offline)될 때, 해당 CPU에 할당된 모든 인터럽트는 다른 온라인 CPU로 마이그레이션되어야 합니다. irq_migrate_all_off_this_cpu()가 이 역할을 수행합니다.

/* kernel/irq/cpuhotplug.c - CPU 오프라인 시 IRQ 마이그레이션 */
void irq_migrate_all_off_this_cpu(void)
{
    unsigned int irq;
    struct irq_desc *desc;

    /* 모든 IRQ 디스크립터를 순회 */
    for_each_active_irq(irq) {
        desc = irq_to_desc(irq);

        /* Per-CPU 인터럽트는 마이그레이션 불필요 */
        if (irqd_is_per_cpu(&desc->irq_data))
            continue;

        raw_spin_lock(&desc->lock);

        /* 현재 CPU가 어피니티 마스크에 포함된 경우만 처리 */
        if (cpumask_test_cpu(smp_processor_id(),
                desc->irq_data.common->affinity)) {

            struct irq_data *d = &desc->irq_data;
            struct irq_chip *c = d->chip;
            int ret;

            /* 온라인 CPU 중에서 새 대상 선택 */
            cpumask_copy(&available, cpu_online_mask);
            cpumask_clear_cpu(smp_processor_id(), &available);

            ret = c->irq_set_affinity(d, &available, true);
            if (ret)
                pr_warn_ratelimited(
                    "IRQ%u: set affinity failed(%d)\n",
                    irq, ret);
        }

        raw_spin_unlock(&desc->lock);
    }
}
코드 설명
  • 2행이 함수는 CPU 핫플러그 경로의 CPUHP_TEARDOWN_CPU 단계에서 호출됩니다. 오프라인되는 CPU에서 직접 실행됩니다.
  • 8행for_each_active_irq()는 시스템의 모든 활성 IRQ를 순회합니다. sparse_irq가 활성화된 경우 radix tree를 탐색합니다.
  • 12행Per-CPU 인터럽트(예: 로컬 APIC 타이머)는 특정 CPU에 고정되므로 마이그레이션 대상에서 제외합니다.
  • 26-27행현재 온라인인 CPU 마스크에서 오프라인되는 자신의 CPU를 제거하여 새로운 어피니티 마스크를 구성합니다.
  • 29행force 파라미터를 true로 전달하여 사용자 설정 어피니티를 무시하고 강제 마이그레이션합니다. CPU가 오프라인되므로 선택의 여지가 없습니다.

사용자 공간에서 IRQ 어피니티를 설정하려면 /proc/irq/<IRQ번호>/smp_affinity에 16진수 CPU 마스크를 기록합니다:

# IRQ 32를 CPU 0,1에만 할당
echo 3 > /proc/irq/32/smp_affinity

# 현재 어피니티 확인
cat /proc/irq/32/smp_affinity
# 출력: 00000003

# CPU 리스트 형식으로 설정 (더 직관적)
echo 0-1 > /proc/irq/32/smp_affinity_list

참고자료

커널 공식 문서

커널 소스 코드

LWN.net 기사

서적

외부 자료