인터럽트 (Interrupts)

하드웨어 인터럽트 처리, Top/Bottom Half 아키텍처, softirq, tasklet, workqueue, threaded IRQ, NMI(Non-Maskable Interrupt)를 상세히 다룹니다.

이 문서는 인터럽트가 CPU 예외 벡터에 진입한 뒤 irq_descirq_chip 계층을 거쳐 핸들러(Handler)가 실행되고, 이후 Bottom Half로 작업이 분리되는 전체 경로를 소스 코드 관점에서 설명합니다. 단순한 개념 소개를 넘어 공유 IRQ와 affinity, MSI/MSI-X, IPI, NMI, PREEMPT_RT 환경의 threaded IRQ 동작 차이, 그리고 /proc/interrupts·ftrace·perf를 이용한 병목(Bottleneck) 분석 절차까지 포함해 실제 드라이버 개발과 운영 장애 대응에 바로 적용할 수 있도록 구성했습니다.

관련 표준: Intel SDM Vol. 3 (Interrupt and Exception Handling), ARM ARM (Exception Model) — 커널 인터럽트 처리가 준수하는 하드웨어 아키텍처 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 커널 아키텍처프로세스(Process) 관리 문서를 먼저 읽으세요. 비동기 이벤트 처리 주제는 문맥 전환(Context Switch)과 지연(Latency) 실행 경로를 정확히 구분해야 하므로, 프로세스 실행 문맥을 먼저 잡아야 합니다.

핵심 요약

  • 인터럽트 — 하드웨어가 CPU에 비동기적으로 이벤트를 알리는 메커니즘입니다.
  • IDT — Interrupt Descriptor Table. 인터럽트 번호를 핸들러 함수에 매핑(Mapping)합니다.
  • Top Half — 인터럽트 발생 즉시 실행. 빠르게 최소한의 작업만 수행합니다.
  • Bottom Half — 나중에 지연 실행. softirq, tasklet, workqueue 세 가지 메커니즘이 있습니다.
  • threaded IRQ — 인터럽트 핸들러를 커널 스레드(Kernel Thread)에서 실행하여 선점(Preemption) 가능하게 만듭니다.

단계별 이해

  1. 인터럽트 발생 — 키보드 키를 누르면 키보드 컨트롤러가 IRQ 라인을 통해 CPU에 알립니다.

    CPU는 현재 레지스터(Register)를 저장하고 IDT에서 핸들러 주소를 찾아 점프합니다.

  2. Top Half 실행 — 핸들러에서 긴급한 작업(디바이스 레지스터 읽기, ACK 보내기)만 수행합니다.

    인터럽트가 비활성화된 상태이므로 최대한 빠르게 끝내야 합니다.

  3. Bottom Half 예약 — 나머지 작업(데이터 처리, 프로토콜 스택 호출 등)을 Bottom Half로 위임합니다.

    softirq(고성능, 정적), tasklet(간편), workqueue(슬립(Sleep) 가능) 중 선택합니다.

  4. 확인cat /proc/interrupts로 각 IRQ의 발생 횟수와 핸들러를 확인할 수 있습니다.

    cat /proc/softirqs로 softirq 타입별 처리 횟수를 볼 수 있습니다.

인터럽트 개요

인터럽트는 하드웨어가 CPU에 비동기적으로 이벤트를 알리는 메커니즘입니다. CPU는 현재 실행 중인 코드를 중단하고, 인터럽트 핸들러를 실행한 뒤, 원래 코드로 복귀합니다.

인터럽트 유형

x86 IDT (Interrupt Descriptor Table) 아키텍처

x86 아키텍처에서 인터럽트와 예외는 IDT(Interrupt Descriptor Table)를 통해 처리됩니다. IDT는 256개 엔트리로 구성되며, 각 엔트리는 게이트 디스크립터(Gate Descriptor)로 해당 벡터의 핸들러 주소를 가리킵니다. IDTR 레지스터가 IDT의 베이스(Base) 주소와 크기를 보관합니다.

x86 IDT 구조: IDTR → IDT → Gate Descriptor → Handler IDTR 레지스터 Base + Limit (10B) IDT (256 엔트리) #0 Divide Error (Fault) #6 Invalid Opcode (Fault) #8 Double Fault (Abort) #13 General Protection #14 Page Fault (Fault) #15-31 (Reserved) #32-127 Device IRQ #128 (0x80) Legacy syscall #239 Local APIC Timer #251-254 IPI 벡터 #255 Spurious IRQ Gate Descriptor (16B) Segment Selector (CS) + Offset (RIP) Type: Interrupt Gate / Trap Gate DPL + Present + IST Index Interrupt Gate: RFLAGS.IF 자동 클리어 Handler (entry_64.S) Interrupt Gate: IF 클리어 (인터럽트 비활성) | Trap Gate: IF 유지 (인터럽트 허용)
x86 IDT 구조: IDTR 레지스터가 가리키는 256-엔트리 테이블과 게이트 디스크립터 상세

x86 예외 벡터 테이블 (Vector 0-31)

벡터약어이름타입설명
0#DEDivide ErrorFault0으로 나누기 또는 결과 오버플로
1#DBDebugFault/Trap하드웨어 브레이크포인트, 단일 스텝
2-NMIInterruptNon-Maskable Interrupt
3#BPBreakpointTrapINT 3 명령 (디버거 사용)
4#OFOverflowTrapINTO 명령 시 오버플로
5#BRBOUND Range ExceededFaultBOUND 명령 범위 초과
6#UDInvalid OpcodeFault잘못된 명령어
7#NMDevice Not AvailableFaultFPU/SIMD 명령, CR0.EM 또는 TS 설정
8#DFDouble FaultAbort예외 처리 중 예외 발생 (IST 사용)
10#TSInvalid TSSFaultTSS 세그먼트 오류
11#NPSegment Not PresentFault세그먼트 P 비트 클리어
12#SSStack-Segment FaultFault스택 세그먼트 오류
13#GPGeneral ProtectionFault권한 위반, 잘못된 메모리 접근
14#PFPage FaultFault페이지 미매핑, 권한 위반 (CR2에 주소)
16#MFx87 FPU ErrorFaultx87 부동소수점 예외
17#ACAlignment CheckFault정렬 위반 (EFLAGS.AC + CR0.AM)
18#MCMachine CheckAbort하드웨어 오류 (IST 사용)
19#XMSIMD Floating-PointFaultSSE/AVX 예외
20#VEVirtualization ExceptionFaultEPT 위반 (가상화)
21#CPControl ProtectionFaultCET(Control-flow Enforcement) 위반
Fault vs Trap vs Abort: Fault는 원인 명령을 재실행합니다 (Page Fault 처리 후 같은 명령 재시도). Trap은 원인 명령 다음 명령부터 실행합니다 (Breakpoint 후 계속 진행). Abort는 정확한 원인 위치를 알 수 없어 복구가 불가능합니다 (Double Fault).
/* arch/x86/kernel/idt.c — IDT 설정 (간략화) */
static const struct idt_data def_idts[] = {
    /* 예외 벡터 */
    INTG(0,   asm_exc_divide_error),           /* #DE */
    ISTG(1,   asm_exc_debug, IST_INDEX_DB),     /* #DB: IST3 사용 */
    ISTG(2,   asm_exc_nmi, IST_INDEX_NMI),      /* NMI: IST2 사용 */
    SYSG(3,   asm_exc_int3),                    /* #BP: 유저 접근 허용 */
    INTG(6,   asm_exc_invalid_op),              /* #UD */
    ISTG(8,   asm_exc_double_fault, IST_INDEX_DF), /* #DF: IST1 */
    INTG(13,  asm_exc_general_protection),      /* #GP */
    INTG(14,  asm_exc_page_fault),              /* #PF */
    ISTG(18,  asm_exc_machine_check, IST_INDEX_MCE), /* #MC: IST4 */
    /* ... */
};

/*
 * INTG: Interrupt Gate — DPL 0, IF 클리어 (인터럽트 비활성)
 * SYSG: System Gate — DPL 3, 유저 공간에서 INT 명령으로 호출 가능
 * ISTG: IST Gate — IST 스택 인덱스 지정
 */
void __init idt_setup_early_traps(void)
{
    idt_setup_from_table(idt_table, def_idts,
                         ARRAY_SIZE(def_idts), false);
    load_idt(&idt_descr);  /* LIDT 명령으로 IDTR 설정 */
}

ARM 예외 모델 (Exception Model)

ARM64에서는 IDT 대신 예외 벡터 테이블(Exception Vector Table)을 사용합니다. VBAR_EL1(Vector Base Address Register) 레지스터가 테이블의 시작 주소를 가리키며, 예외 유형과 발생 소스(Source)에 따라 128바이트 간격의 오프셋으로 벡터를 선택합니다.

ARM64 예외 벡터 테이블 (VBAR_EL1) VBAR_EL1 벡터 테이블 베이스 주소 예외 벡터 테이블 (4 × 4 = 16 벡터, 각 128B) Synchronous IRQ FIQ SError EL1t (SP_EL0) +0x000 +0x080 +0x100 +0x180 EL1h (SP_EL1) +0x200 el1h_irq +0x280 +0x300 +0x380 EL0 (AArch64) el0_sync +0x400 el0_irq +0x480 +0x500 +0x580 EL0 (AArch32) +0x600 +0x680 +0x700 +0x780 커널 IRQ: EL1h IRQ (+0x280) 또는 EL0 IRQ (+0x480) 각 벡터 공간: 128바이트 (32개 명령어) — 보통 즉시 핸들러로 분기 el0_sync: 시스템 콜, Data Abort, Instruction Abort 등 구분 필요
ARM64 예외 벡터 테이블: 예외 소스(EL1/EL0)와 유형(Sync/IRQ/FIQ/SError)에 따른 16개 벡터
// arch/arm64/kernel/entry.S — 예외 벡터 테이블 정의 (간략화)

/*
 * 벡터 테이블은 2KB 정렬, 각 벡터는 128B 간격
 * VBAR_EL1에 이 테이블 주소를 설정
 */
    .align  11  // 2KB 정렬
SYM_CODE_START(vectors)
    /* Current EL with SP_EL0 (EL1t) — 거의 사용하지 않음 */
    kernel_ventry   1, t, 64, sync        // Synchronous
    kernel_ventry   1, t, 64, irq         // IRQ
    kernel_ventry   1, t, 64, fiq         // FIQ
    kernel_ventry   1, t, 64, error       // SError

    /* Current EL with SP_ELx (EL1h) — 커널 실행 중 예외 */
    kernel_ventry   1, h, 64, sync        // Synchronous
    kernel_ventry   1, h, 64, irq         // IRQ ← 커널 모드 인터럽트
    kernel_ventry   1, h, 64, fiq         // FIQ
    kernel_ventry   1, h, 64, error       // SError

    /* Lower EL, AArch64 (EL0) — 유저 프로세스 예외 */
    kernel_ventry   0, t, 64, sync        // Synchronous (syscall 등)
    kernel_ventry   0, t, 64, irq         // IRQ ← 유저 모드 인터럽트
    kernel_ventry   0, t, 64, fiq         // FIQ
    kernel_ventry   0, t, 64, error       // SError

    /* Lower EL, AArch32 — 32비트 호환 */
    kernel_ventry   0, t, 32, sync
    kernel_ventry   0, t, 32, irq
    kernel_ventry   0, t, 32, fiq
    kernel_ventry   0, t, 32, error
SYM_CODE_END(vectors)
x86 IDT vs ARM 예외 벡터 비교: x86 IDT는 256개의 가변 크기 게이트 디스크립터로 각 벡터가 직접 핸들러 주소를 포함합니다. ARM은 16개의 고정 128바이트 벡터 슬롯(Slot)을 사용하며, 벡터 코드 내에서 예외 유형(ESR_EL1)을 읽어 세부 분기합니다. x86은 벡터 번호로 인터럽트를 구분하지만, ARM은 GIC에서 인터럽트 번호를 ACK 레지스터로 읽어야 합니다.

Top Half / Bottom Half 아키텍처

인터럽트 핸들러에서 긴 작업을 수행하면 다른 인터럽트를 차단하여 시스템 응답성이 저하됩니다. Linux는 이를 해결하기 위해 인터럽트 처리를 두 단계로 분리합니다:

Top Half / Bottom Half 아키텍처 Hardware IRQ Top Half (Hard IRQ) 최소 작업, 인터럽트 비활성 스케줄링 softirq 고속, Per-CPU tasklet softirq 기반, 동적 workqueue 프로세스 컨텍스트 threaded IRQ 커널 스레드 인터럽트 컨텍스트 (슬립 불가) 프로세스 컨텍스트 (슬립 가능) ← 낮은 지연시간 높은 유연성 →
인터럽트 처리의 Top/Bottom Half 분리와 Bottom Half 메커니즘 비교

인터럽트 핸들러 등록

#include <linux/interrupt.h>

/* IRQ 핸들러 등록 */
int request_irq(
    unsigned int irq,              /* IRQ number */
    irq_handler_t handler,         /* Top half handler */
    unsigned long flags,            /* IRQF_SHARED, IRQF_ONESHOT, etc. */
    const char *name,               /* /proc/interrupts에 표시되는 이름 */
    void *dev_id                    /* shared IRQ 구분용 */
);

/* Threaded IRQ 등록 (Top + Bottom half) */
int request_threaded_irq(
    unsigned int irq,
    irq_handler_t handler,         /* Top half (hardirq context) */
    irq_handler_t thread_fn,       /* Bottom half (thread context) */
    unsigned long flags,
    const char *name,
    void *dev_id
);

/* 핸들러 반환 값 */
/* IRQ_NONE     - 이 디바이스의 인터럽트가 아님 (shared IRQ) */
/* IRQ_HANDLED  - 정상 처리 완료 */
/* IRQ_WAKE_THREAD - bottom half 스레드 깨우기 */
코드 설명
  • request_irq()include/linux/interrupt.h에 정의된 인터럽트 핸들러 등록 함수입니다. 내부적으로 request_threaded_irq()thread_fn=NULL로 호출합니다. irq는 Linux 가상 IRQ 번호, handler는 hardirq 컨텍스트에서 실행되는 top half 콜백, flagsIRQF_SHARED 등의 동작 옵션, dev_id는 공유 IRQ에서 핸들러를 구분하는 쿠키입니다.
  • request_threaded_irq()kernel/irq/manage.c에 구현된 핵심 등록 함수입니다. handler(top half)가 IRQ_WAKE_THREAD를 반환하면 thread_fn(bottom half)이 전용 커널 스레드(irq/N-name)에서 실행됩니다. I2C/SPI처럼 슬립이 필요한 버스 디바이스 드라이버에서 필수적입니다.
  • 반환 값IRQ_NONE은 공유 IRQ에서 자신의 인터럽트가 아님을 알리고, IRQ_HANDLED는 처리 완료를 의미합니다. IRQ_WAKE_THREAD는 top half에서만 반환하며, threaded handler를 깨우도록 커널에 요청합니다.

IRQ 핸들러 예제

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

    /* Read interrupt status register */
    status = ioread32(dev->regs + IRQ_STATUS);
    if (!(status & MY_IRQ_MASK))
        return IRQ_NONE;  /* Not our interrupt */

    /* Acknowledge interrupt */
    iowrite32(status, dev->regs + IRQ_ACK);

    /* Schedule bottom half */
    tasklet_schedule(&dev->tasklet);

    return IRQ_HANDLED;
}
코드 설명
  • status 레지스터 읽기ioread32()로 디바이스의 인터럽트 상태 레지스터를 읽습니다. 공유 IRQ 환경에서는 이 값으로 자신의 인터럽트인지 판별해야 합니다. MY_IRQ_MASK에 해당하지 않으면 IRQ_NONE을 반환하여 커널이 체인의 다음 핸들러를 호출하도록 합니다.
  • 인터럽트 ACKiowrite32()로 상태 레지스터에 쓰기하여 인터럽트를 확인(Acknowledge) 처리합니다. ACK 없이 반환하면 레벨 트리거 인터럽트에서 인터럽트 스톰이 발생합니다.
  • tasklet_schedule()bottom half 처리를 위해 태스크릿을 스케줄링합니다. top half(hardirq 컨텍스트)에서는 최소한의 작업만 수행하고, 시간이 걸리는 데이터 처리는 softirq/tasklet/workqueue 등 bottom half로 위임하는 것이 핵심 패턴입니다. 호출 체인: do_IRQ()handle_irq()handle_irq_event() → 이 핸들러.

IRQ 생명주기

IRQ 핸들러의 등록과 해제는 리소스 관리의 핵심입니다. request_irq()/free_irq() 패턴과 managed 리소스 버전을 이해해야 합니다.

IRQF 플래그

플래그설명사용 시나리오
IRQF_SHAREDIRQ 라인을 여러 디바이스가 공유PCI 레거시 인터럽트
IRQF_ONESHOTthreaded handler 완료까지 IRQ를 마스킹request_threaded_irq() 필수
IRQF_TRIGGER_RISING상승 엣지 트리거GPIO 인터럽트
IRQF_TRIGGER_FALLING하강 엣지 트리거GPIO 인터럽트
IRQF_TRIGGER_HIGH하이 레벨 트리거레벨 감지 디바이스
IRQF_TRIGGER_LOW로우 레벨 트리거레벨 감지 디바이스
IRQF_NO_SUSPENDsuspend 중에도 인터럽트 수신웨이크업 소스
IRQF_NOBALANCINGirqbalance에 의한 이동 방지고정 affinity 필요

Managed IRQ 등록

/* 기본 패턴: request_irq + free_irq */
ret = request_irq(irq, my_handler, IRQF_SHARED, "mydev", priv);
if (ret)
    return ret;
/* ... 드라이버 동작 ... */
free_irq(irq, priv);  /* 반드시 같은 dev_id로 해제 */

/* Managed 리소스 패턴: device 해제 시 자동 free */
ret = devm_request_irq(&pdev->dev, irq, my_handler,
                       IRQF_SHARED, "mydev", priv);
if (ret)
    return ret;
/* free_irq() 호출 불필요 — 디바이스 해제 시 자동 처리 */

/* IRQ 제어 */
disable_irq(irq);       /* 동기적: 진행 중인 핸들러 완료 대기 */
disable_irq_nosync(irq);/* 비동기: 즉시 반환 */
enable_irq(irq);        /* IRQ 재활성화 */
synchronize_irq(irq);   /* 진행 중인 핸들러 완료 대기 */
코드 설명
  • request_irq() + free_irq()kernel/irq/manage.c에 구현된 기본 IRQ 등록/해제 패턴입니다. free_irq() 호출 시 반드시 등록할 때 사용한 것과 동일한 dev_id를 전달해야 합니다. 공유 IRQ에서 dev_id는 핸들러 체인에서 특정 핸들러를 식별하는 키 역할을 합니다.
  • devm_request_irq()managed 리소스(devres) 버전으로, 디바이스가 해제될 때 free_irq()를 자동 호출합니다. 에러 경로에서 수동 해제를 빼먹는 실수를 방지하므로 최신 드라이버에서 권장됩니다.
  • disable_irq() vs disable_irq_nosync()disable_irq()는 현재 실행 중인 핸들러가 완료될 때까지 블로킹하므로 인터럽트 컨텍스트에서 호출하면 데드록이 발생합니다. 인터럽트 핸들러 내부에서는 disable_irq_nosync()를 사용하고, 필요 시 synchronize_irq()로 별도 동기화합니다.

인터럽트 진입/복귀 경로 상세

인터럽트가 발생하면 CPU는 하드웨어 수준에서 현재 상태를 저장하고, 아키텍처별 진입 코드(Entry Code)를 거쳐 커널의 공통 인터럽트 처리 경로에 도달합니다. 이 과정을 정확히 이해하면 인터럽트 지연시간(Latency) 분석과 디버깅에 큰 도움이 됩니다.

x86 인터럽트 진입 경로

x86에서 인터럽트가 발생하면 CPU는 IDT에서 게이트 디스크립터(Gate Descriptor)를 찾아 해당 진입점으로 점프합니다. 커널은 entry_64.Sidtentry 매크로로 레지스터를 저장하고, C 핸들러를 호출합니다.

x86 인터럽트 진입 경로 (Hardware → Handler) HW IRQ 발생 IDT 벡터 조회 entry_64.S idtentry pt_regs 저장 인터럽트 스택 전환 irq_enter_rcu() common_interrupt() handle_irq(irq_desc) irqaction 체인 IRQ Handler 실행 irq_exit_rcu() softirq 확인/실행 IRET (복귀) pt_regs: RIP, CS, RFLAGS, RSP, SS + 범용 레지스터 전체 커널 6.x: do_IRQ() 대신 common_interrupt() → handle_irq() 경로 사용
x86 인터럽트 진입부터 핸들러 실행, softirq 확인, 복귀까지의 전체 경로
/* arch/x86/entry/entry_64.S — idtentry 매크로 (간략화) */

/*
 * idtentry: 인터럽트/예외 진입 매크로
 * CPU가 자동으로 SS, RSP, RFLAGS, CS, RIP를 스택에 push한 상태에서 시작
 */
.macro idtentry sym do_sym has_error_code:req
SYM_CODE_START(\sym)
    /* error_code가 없으면 더미 push */
    .if \has_error_code == 0
        pushq   $-1              /* 더미 error code */
    .endif

    /* 범용 레지스터 저장 → pt_regs 구조체 완성 */
    pushq   %rdi
    pushq   %rsi
    pushq   %rdx
    pushq   %rcx
    pushq   %rax
    pushq   %r8
    pushq   %r9
    pushq   %r10
    pushq   %r11
    pushq   %rbx
    pushq   %rbp
    pushq   %r12
    pushq   %r13
    pushq   %r14
    pushq   %r15

    movq    %rsp, %rdi           /* 첫 번째 인자: pt_regs 포인터 */
    call    \do_sym              /* C 핸들러 호출 */
    jmp     error_return        /* 복귀 경로 */
SYM_CODE_END(\sym)
.endm
/* arch/x86/kernel/irq.c — common_interrupt (커널 6.x) */
DEFINE_IDTENTRY_IRQ(common_interrupt)
{
    struct pt_regs *old_regs = set_irq_regs(regs);
    struct irq_desc *desc;
    int vector = (u8)error_code;  /* 벡터 번호 */

    /* RCU 및 인터럽트 상태 진입 */
    irq_enter_rcu();

    desc = __this_cpu_read(vector_irq[vector]);
    if (likely(!IS_ERR_OR_NULL(desc))) {
        handle_irq(desc, regs);  /* irq_desc의 handle_irq 콜백 */
    } else {
        ack_APIC_irq();
        if (desc == VECTOR_UNUSED)
            pr_emerg_ratelimited("Spurious interrupt vector %d\n", vector);
    }

    /* 인터럽트 상태 복귀 + softirq 확인 */
    irq_exit_rcu();
    set_irq_regs(old_regs);
}

ARM64 인터럽트 진입 경로

ARM64에서 IRQ가 발생하면 CPU는 VBAR_EL1에 설정된 예외 벡터 테이블(Exception Vector Table)의 해당 오프셋으로 점프합니다. EL0(유저)에서 발생한 경우와 EL1(커널)에서 발생한 경우 서로 다른 벡터를 사용합니다.

// arch/arm64/kernel/entry.S — 예외 벡터 엔트리 (간략화)

/*
 * EL1에서 IRQ 발생 시 진입점
 * SP_EL1 사용, 현재 커널 코드 실행 중에 인터럽트 발생
 */
SYM_CODE_START_LOCAL(el1h_64_irq)
    kernel_entry 1          // 레지스터 저장 (x0-x30, sp, pc, pstate)
    mov     x0, sp                // pt_regs 포인터를 첫 번째 인자로
    bl      el1_interrupt        // C 핸들러 호출
    kernel_exit 1           // 레지스터 복원 + eret
SYM_CODE_END(el1h_64_irq)

/*
 * EL0에서 IRQ 발생 시 진입점
 * 유저 프로세스 실행 중에 인터럽트 발생
 */
SYM_CODE_START_LOCAL(el0t_64_irq)
    kernel_entry 0          // 유저 레지스터 저장
    mov     x0, sp
    bl      el0_interrupt
    b       ret_to_user          // 유저 복귀 (시그널 확인 포함)
SYM_CODE_END(el0t_64_irq)
/* arch/arm64/kernel/irq.c — GIC 인터럽트 ACK 흐름 */
static void __gic_handle_irq(u32 irqstat, struct pt_regs *regs)
{
    u32 irqnr = irqstat & GICC_IAR_INT_ID_MASK;

    /* GIC에서 인터럽트 번호 읽기 (ACK) */
    if (likely(irqnr > 15 && irqnr < 1020)) {
        /* SPI(Shared Peripheral Interrupt) — irq_domain으로 매핑 */
        handle_domain_irq(gic_data.domain, irqnr, regs);
    } else if (irqnr < 16) {
        /* SGI(Software Generated Interrupt) — IPI 처리 */
        handle_IPI(irqnr, regs);
    }
    /* irqnr == 1023: spurious interrupt (무시) */
}

irq_enter() / irq_exit() 내부

irq_enter()irq_exit()는 인터럽트 처리의 시작과 끝을 커널에 알리는 핵심 함수입니다. preempt_count의 HARDIRQ 비트를 조작하여 인터럽트 컨텍스트(Interrupt Context) 상태를 추적하고, irq_exit()에서는 pending softirq를 확인하여 실행합니다.

/* kernel/softirq.c — irq_enter/irq_exit 내부 (간략화) */

void irq_enter_rcu(void)
{
    /*
     * preempt_count의 HARDIRQ 비트를 증가
     * → in_hardirq() == true
     * → 슬립 가능 함수 호출 시 BUG 감지
     */
    __irq_enter_raw();

    /* idle 상태였으면 tick 재시작 */
    if (is_idle_task(current) && !in_interrupt())
        tick_irq_enter();

    /* IRQ 시간 통계 시작 */
    account_irq_enter_time(current);
}

void irq_exit_rcu(void)
{
    /* IRQ 시간 통계 종료 */
    account_irq_exit_time(current);

    /* HARDIRQ 카운트 감소 */
    preempt_count_sub(HARDIRQ_OFFSET);

    /*
     * 핵심: HARDIRQ + SOFTIRQ가 모두 0이고
     * pending softirq가 있으면 즉시 실행
     */
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();  /* → __do_softirq() 또는 ksoftirqd 깨움 */

    /* 선점 검사: preempt_count == 0이면 재스케줄링 가능 */
    tick_irq_exit();
}
preempt_count 구조: preempt_count는 32비트 정수로, 비트 영역별로 다른 의미를 가집니다: 비트 0-7 = 선점 비활성 카운트, 비트 8-15 = softirq 카운트, 비트 16-19 = hardirq 카운트, 비트 20 = NMI. in_interrupt()는 hardirq + softirq + NMI 비트가 하나라도 설정되면 true를 반환합니다.

인터럽트 스택 아키텍처

인터럽트 핸들러는 프로세스의 커널 스택(Kernel Stack)이 아닌 별도의 인터럽트 스택(Interrupt Stack)에서 실행됩니다. 이는 스택 오버플로(Stack Overflow) 방지와 인터럽트 처리의 독립성을 보장합니다.

Per-CPU 인터럽트 스택 구조 (x86_64) 커널 스택 (THREAD_SIZE) thread_info 프로세스 실행 프레임 syscall 프레임 ... 16KB (기본) IRQ 발생 Hardirq 스택 (Per-CPU, 16KB) irq_enter_rcu() IRQ handler irq_exit_rcu() IRQ_STACK_SIZE IST 스택들 (Per-CPU, 전용) IST1: Double Fault (8KB) IST2: NMI (8KB) IST3: Debug (8KB) IST4: MCE (8KB) 치명적 예외 전용 Softirq 스택 (Per-CPU, 16KB) __do_softirq() softirq handler irq_exit에서 전환 irq_exit → softirq x86_64: 4종류의 Per-CPU 스택으로 안정성과 격리를 보장
x86_64 Per-CPU 스택 구조: 커널 스택, Hardirq 스택, IST 스택, Softirq 스택

x86 IST (Interrupt Stack Table)

IST(Interrupt Stack Table)는 x86_64에서 치명적 예외(Critical Exception)를 위한 전용 스택입니다. TSS(Task State Segment)에 최대 7개의 IST 포인터를 설정할 수 있으며, IDT 게이트 디스크립터에서 IST 인덱스를 지정하면 해당 예외 발생 시 무조건 지정된 스택으로 전환됩니다.

IST 인덱스용도크기설명
IST1Double Fault (#DF)8KB스택 오버플로 시에도 안전하게 패닉 처리
IST2NMI8KB마스크 불가 인터럽트 전용, 중첩(Nesting) 방지
IST3Debug (#DB)8KB하드웨어 브레이크포인트, 단일 스텝
IST4MCE (Machine Check)8KB하드웨어 오류 보고, 독립 스택 필수
/* arch/x86/include/asm/irq_stack.h — 인터럽트 스택 전환 (간략화) */

/*
 * call_on_irqstack: 현재 스택에서 Per-CPU hardirq 스택으로 전환하여 함수 실행
 * 커널 스택 소비를 최소화하고 스택 오버플로 위험을 줄임
 */
#define call_on_irqstack(func, asm_call)            \
{                                                       \
    register void *tos asm("r11");               \
                                                        \
    tos = __this_cpu_read(hardirq_stack_ptr);      \
                                                        \
    asm volatile(                                    \
        "movq   %%rsp, (%[tos])     \n"             \
        "movq   %[tos], %%rsp       \n"             \
        asm_call                                        \
        "popq   %%rsp               \n"             \
        :                                               \
        : [tos] "r" (tos), [func] "m" (func)        \
        : "memory"                                    \
    );                                                  \
}

ARM64 인터럽트 스택

ARM64에서도 Per-CPU 인터럽트 스택을 사용합니다. EL1에서 IRQ가 발생하면 IRQ_STACK_SIZE(기본 16KB) 크기의 전용 스택으로 전환한 후 핸들러를 실행합니다. x86의 IST와 달리 별도의 예외 전용 스택은 없으며, 모든 인터럽트가 단일 IRQ 스택을 공유합니다.

/* arch/arm64/kernel/irq.c — ARM64 IRQ 스택 전환 */

/* Per-CPU IRQ 스택 선언 */
DEFINE_PER_CPU(unsigned long *, irq_stack_ptr);

static void ____do_IRQ(struct pt_regs *regs)
{
    /*
     * 인터럽트 스택으로 전환하여 handle_arch_irq 실행
     * 커널 스택 소비를 방지하고
     * 중첩 인터럽트로 인한 오버플로를 예방
     */
    call_on_irq_stack(regs, handle_arch_irq);
}

/* init 시점에 Per-CPU 스택 할당 */
static void init_irq_stacks(void)
{
    int cpu;
    for_each_possible_cpu(cpu) {
        unsigned long *p = (unsigned long *)__get_free_pages(
            GFP_KERNEL, IRQ_STACK_ORDER);
        per_cpu(irq_stack_ptr, cpu) = p + IRQ_STACK_SIZE / sizeof(long);
    }
}

softirq

softirq는 커널에 정적으로 정의된 10가지 Bottom Half 메커니즘입니다. irq_exit()에서 pending 비트를 확인하여 실행되며, 같은 softirq가 여러 CPU에서 동시 실행될 수 있어 Per-CPU 데이터를 활용합니다. 부하가 높으면 ksoftirqd 커널 스레드로 위임됩니다.

상세 문서: softirq 타입 테이블, open_softirq()/raise_softirq() API, __do_softirq() 내부, ksoftirqd 생명주기, Per-CPU 동시성, 선점 모드별 동작은 Softirq & Hardirq 페이지(Page)에서 다룹니다.

tasklet

tasklet은 softirq(TASKLET_SOFTIRQ, HI_SOFTIRQ) 위에 구축된 동적 Bottom Half 메커니즘입니다. 같은 tasklet은 절대 병렬 실행되지 않는 직렬화(Serialization)를 보장하지만, deprecated 추세이므로 새 코드에서는 workqueue 또는 threaded IRQ를 사용하세요.

상세 문서: tasklet_struct 내부 필드, 상태 머신, Per-CPU 리스트, tasklet_schedule() 내부, HI_SOFTIRQ vs TASKLET_SOFTIRQ, PREEMPT_RT 호환성, workqueue/threaded IRQ 마이그레이션 가이드는 Tasklet 페이지에서 다룹니다.

workqueue

workqueue는 Bottom Half 작업을 커널 스레드(프로세스 컨텍스트)에서 실행합니다. 슬립이 가능하므로 mutex 획득, 메모리 할당(GFP_KERNEL) 등이 가능하며, 새로운 Bottom Half 메커니즘 선택 시 기본 권장입니다.

상세 문서: CMWQ 아키텍처, worker pool, alloc_workqueue() 플래그, work item 생명주기, 취소/flush 패턴, 디버깅(Debugging), best practices는 Workqueue (CMWQ) 페이지에서 다룹니다.

Threaded IRQ

Threaded IRQ는 인터럽트의 Bottom Half를 전용 커널 스레드에서 실행합니다. 프로세스 컨텍스트의 장점(슬립, mutex, GFP_KERNEL)을 가지면서도 workqueue보다 지연이 적습니다. PREEMPT_RT 커널에서는 모든 인터럽트 핸들러가 자동으로 threaded로 전환됩니다.

/* Threaded IRQ 등록 */
ret = request_threaded_irq(irq,
    my_hardirq_handler,   /* top half: 빠른 ACK, IRQ_WAKE_THREAD 반환 */
    my_threaded_handler,  /* bottom half: 커널 스레드에서 실행 */
    IRQF_ONESHOT,         /* 스레드 완료까지 IRQ 마스킹 유지 */
    "mydev", priv);

/* Top half: 최소 작업만 수행 */
static irqreturn_t my_hardirq_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = ioread32(dev->regs + IRQ_STATUS);

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

    /* ACK 인터럽트 */
    iowrite32(status, dev->regs + IRQ_ACK);
    dev->irq_status = status;

    return IRQ_WAKE_THREAD;  /* bottom half 스레드 깨우기 */
}

/* Bottom half: 스레드 컨텍스트 (슬립 가능!) */
static irqreturn_t my_threaded_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    mutex_lock(&dev->lock);
    /* I2C/SPI 통신, 대량 데이터 처리 등 가능 */
    process_data(dev, dev->irq_status);
    mutex_unlock(&dev->lock);

    return IRQ_HANDLED;
}

/* handler가 NULL이면 top half 없이 스레드만 실행 */
/* 이 경우 IRQF_ONESHOT 필수 (자동 unmask 방지) */
ret = request_threaded_irq(irq, NULL, my_threaded_handler,
    IRQF_ONESHOT | IRQF_TRIGGER_FALLING, "mydev", priv);
코드 설명
  • request_threaded_irq()kernel/irq/manage.c__setup_irq()에서 setup_irq_thread()를 호출하여 irq/N-name 형식의 전용 커널 스레드를 생성합니다. 이 스레드는 SCHED_FIFO 정책의 RT 우선순위 50으로 실행됩니다.
  • my_hardirq_handler()hardirq 컨텍스트에서 실행되는 top half입니다. 인터럽트 상태 확인, ACK 처리만 수행하고 IRQ_WAKE_THREAD를 반환하여 bottom half 스레드를 깨웁니다. 이 함수는 가능한 짧게 유지해야 합니다.
  • my_threaded_handler()스레드 컨텍스트에서 실행되므로 mutex_lock(), I2C/SPI 전송 등 슬립이 가능한 API를 사용할 수 있습니다. 일반 hardirq 핸들러에서는 불가능한 작업입니다.
  • IRQF_ONESHOTthreaded handler가 완료될 때까지 IRQ 라인을 마스킹 상태로 유지합니다. 레벨 트리거 인터럽트에서 이 플래그 없이 사용하면 top half가 무한 반복 호출되어 시스템이 멈춥니다. handler=NULL인 경우 커널이 IRQF_ONESHOT 누락을 감지하고 등록을 거부합니다.
ℹ️

IRQF_ONESHOT이 필요한 이유: threaded handler가 완료되기 전에 같은 인터럽트가 다시 발생하면 top half가 반복 호출됩니다. 레벨 트리거 인터럽트에서 이는 무한 루프를 유발합니다. IRQF_ONESHOT은 threaded handler 완료까지 IRQ 라인을 마스킹하여 이를 방지합니다.

컨텍스트 비교

특성Hard IRQsoftirq/taskletworkqueue
슬립 가능불가불가가능
GFP_KERNEL불가불가가능
mutex불가불가가능
spinlockspin_lock_irqsavespin_lock_bhspin_lock
선점다른 IRQ만Hard IRQ만완전 선점 가능
지연 시간최소낮음중간

성능 비교

인터럽트 처리 메커니즘마다 레이턴시와 처리량(Throughput)이 다릅니다. 다음은 실제 벤치마크 결과를 기반으로 한 성능 특성 비교입니다.

레이턴시 벤치마크

각 메커니즘의 평균/최악 레이턴시를 측정한 결과입니다 (x86_64, 3.5 GHz CPU 기준):

메커니즘평균 레이턴시최악 레이턴시처리량 (ops/sec)적합한 용도
Hard IRQ (top half)0.8 μs2.1 μs1.2M긴급 처리, 최소 작업만
softirq2.5 μs8.3 μs950K네트워크 RX/TX, 블록 I/O
tasklet3.1 μs12.4 μs850K직렬화 필요한 bottom half
workqueue (bound)15.2 μs78.5 μs320K슬립 가능, CPU 특정 작업
workqueue (unbound)22.7 μs125 μs180K긴 처리 시간, 블로킹 I/O
Threaded IRQ4.8 μs35.2 μs520KRT 시스템, 복잡한 처리
💡

측정 방법: ftrace의 function_graph tracer와 perf stat를 사용하여 측정. 레이턴시는 이벤트 발생부터 핸들러 완료까지의 시간이며, 처리량은 초당 완료 가능한 인터럽트 수입니다.

Threaded IRQ vs 전통적 IRQ 성능 비교

Threaded IRQ는 인터럽트 핸들러를 커널 스레드에서 실행하여 선점 가능하게 만듭니다. RT 시스템에서는 레이턴시 예측성이 향상되지만, 일반 시스템에서는 오버헤드(Overhead)가 있습니다:

워크로드전통적 IRQThreaded IRQ차이권장
네트워크 패킷(Packet) 처리 (10GbE)850K pps720K pps-15%전통적 IRQ
블록 I/O (NVMe)620K IOPS580K IOPS-6%전통적 IRQ
USB 입력 디바이스95 μs68 μs (jitter)jitter 감소Threaded IRQ
Audio (실시간(Real-time))최악 250 μs최악 85 μs-66%Threaded IRQ
GPIO 이벤트12 μs18 μs+50%전통적 IRQ
RT 시스템 (PREEMPT_RT)N/A예측 가능-Threaded IRQ 필수
# Threaded IRQ 레이턴시 측정
# IRQ 스레드 확인 (이름 패턴: irq/N-handler)
ps aux | grep 'irq/'

# 특정 IRQ 스레드의 우선순위 확인
chrt -p $(pgrep 'irq/16-')

# cyclictest로 IRQ 스레드 선점 레이턴시 측정
cyclictest -p 90 -m -n -i 200 -l 100000

# ftrace로 threaded IRQ 실행 시간 측정
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo irq_thread > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace

CPU Affinity 영향 분석

인터럽트를 특정 CPU에 고정하면 캐시(Cache) 지역성이 향상되지만, 부하가 집중될 수 있습니다:

설정평균 처리 시간L1 캐시 적중률CPU 사용률 분산장점
기본 (irqbalance)3.2 μs87%균등자동 밸런싱, CPU 활용 최적화
단일 CPU 고정2.4 μs96%불균등 (90% on CPU0)캐시 지역성, 예측 가능
NUMA 노드 고정2.7 μs92%노드 내 균등메모리 지역성, 확장성
CPU 쌍 (SMT)2.9 μs90%쌍 내 균등L1/L2 공유, 하이퍼스레딩 활용
# IRQ affinity 설정 및 성능 측정 예제

# 1. 현재 affinity 확인
cat /proc/irq/16/smp_affinity
#   출력 예: 00000001 (CPU0만)

# 2. CPU0-3에 분산 (비트마스크 0x0F)
echo 0f > /proc/irq/16/smp_affinity

# 3. 캐시 적중률 측정 (perf stat)
perf stat -e L1-dcache-loads,L1-dcache-load-misses -a -I 1000

# 4. IRQ 처리 시간 프로파일링
perf record -e irq:irq_handler_entry,irq:irq_handler_exit -ag
perf script | grep -A 1 'irq_handler_entry'

# 5. NUMA 인식 affinity 설정
#    NUMA 노드 0의 CPU에만 IRQ 할당
numactl --hardware  # 노드 구성 확인
# 노드 0이 CPU 0-7이라면:
echo ff > /proc/irq/16/smp_affinity  # 0xFF = CPU 0-7
⚠️

주의: 고빈도 인터럽트를 단일 CPU에 고정하면 해당 CPU가 인터럽트 처리에만 집중하여 일반 프로세스 성능이 저하될 수 있습니다. mpstat -P ALL 1로 CPU별 %irq 사용률을 모니터링하세요. 일반적으로 한 CPU의 IRQ 시간이 30%를 넘지 않도록 유지하는 것이 좋습니다.

벤치마크 방법론

인터럽트 성능을 정확히 측정하려면 다음 도구와 기법을 사용합니다:

# 1. ftrace function_graph로 정밀 타이밍 측정
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-abstime
echo do_IRQ > /sys/kernel/debug/tracing/set_graph_function
cat /sys/kernel/debug/tracing/trace

# 2. perf로 인터럽트 카운트 및 오버헤드 측정
perf stat -e 'irq:*' -a sleep 10

# 3. 실시간 인터럽트 분포 확인
watch -n 1 'cat /proc/interrupts | head -20'

# 4. 특정 IRQ 핸들러 프로파일링
perf probe --add 'handle_irq_event_percpu'
perf record -e probe:handle_irq_event_percpu -ag
perf report

Generic IRQ 프레임워크 (genirq)

Linux의 genirq 프레임워크는 모든 아키텍처에 통일된 인터럽트 관리 인터페이스를 제공합니다:

/* 인터럽트 디스크립터 (IRQ 번호별 관리 구조체) */
struct irq_desc {
    struct irq_data       irq_data;
    struct irqaction      *action;     /* 핸들러 체인 */
    unsigned int          status_use_accessors;
    unsigned int          depth;       /* disable 중첩 카운트 */
    const struct irq_chip *irq_chip;   /* HW 제어 함수 */
    struct irq_domain     *domain;
    cpumask_var_t         irq_common_data.affinity;
};

/* irq_chip: 인터럽트 컨트롤러 추상화 */
struct irq_chip {
    .name       = "GICv3",
    .irq_mask   = gic_mask_irq,     /* IRQ 마스킹 */
    .irq_unmask = gic_unmask_irq,   /* IRQ 언마스킹 */
    .irq_eoi    = gic_eoi_irq,      /* End of Interrupt */
    .irq_set_type = gic_set_type,   /* 엣지/레벨 트리거 */
    .irq_set_affinity = gic_set_affinity,
};
코드 설명
  • struct irq_descinclude/linux/irqdesc.h에 정의된 인터럽트 디스크립터로, 각 Linux IRQ 번호마다 하나씩 존재합니다. actionstruct irqaction 링크드 리스트의 헤드로, 공유 IRQ에서 여러 핸들러가 체인으로 연결됩니다. depthdisable_irq() 중첩 호출 횟수를 추적하며 0이 되어야 IRQ가 활성화됩니다.
  • struct irq_chipinclude/linux/irq.h에 정의된 인터럽트 컨트롤러 추상화 인터페이스입니다. irq_mask/irq_unmask는 개별 IRQ를 마스킹/언마스킹하고, irq_eoi는 End-of-Interrupt 신호를 컨트롤러에 전송합니다. 플로우 핸들러(handle_fasteoi_irq 등)가 이 콜백들을 호출하여 하드웨어를 제어합니다. 호출 경로: handle_irq()handle_fasteoi_irq()chip->irq_eoi().
  • irq_set_affinityirq_chip의 콜백으로, kernel/irq/manage.cirq_set_affinity_locked()에서 호출됩니다. GICv3의 경우 GICD_IROUTER 레지스터를 설정하여 인터럽트를 특정 CPU로 라우팅합니다.

IRQ Domain

IRQ domain은 하드웨어 IRQ 번호를 Linux의 가상 IRQ 번호로 매핑합니다. 여러 인터럽트 컨트롤러(Interrupt Controller)가 계층적으로 연결되는 환경을 지원합니다. 상세한 IRQ domain API와 아키텍처는 IRQ 도메인 페이지를 참고하세요.

/* IRQ domain 생성 (인터럽트 컨트롤러 드라이버) */
struct irq_domain *domain;
domain = irq_domain_add_linear(node, nr_irqs,
    &my_domain_ops, priv);

/* 하드웨어 IRQ → Linux virq 매핑 */
unsigned int virq = irq_create_mapping(domain, hwirq);

/* 계층적 IRQ domain (GIC → GPIO controller 등) */
domain = irq_domain_add_hierarchy(parent_domain,
    0, nr_irqs, node, &child_ops, priv);

MSI/MSI-X (Message Signaled Interrupts)

MSI는 PCI 디바이스가 메모리 쓰기로 인터럽트를 발생시키는 메커니즘입니다. 전용 IRQ 라인이 필요 없어 확장성이 뛰어납니다:

/* MSI-X 활성화 (여러 인터럽트 벡터) */
int nr_vecs = pci_alloc_irq_vectors(pdev,
    1,          /* 최소 벡터 수 */
    max_vecs,   /* 최대 벡터 수 */
    PCI_IRQ_MSIX | PCI_IRQ_MSI);

/* 개별 벡터의 Linux IRQ 번호 얻기 */
int irq = pci_irq_vector(pdev, vector_index);
request_irq(irq, my_handler, 0, "mydev", priv);

/* 해제 */
pci_free_irq_vectors(pdev);
방식특징벡터 수
Legacy IRQ물리 IRQ 라인, 공유 가능1
MSI메모리 쓰기 기반최대 32
MSI-X독립적 벡터, CPU affinity 지원최대 2048

인터럽트 Affinity

# 인터럽트를 특정 CPU에 고정
echo 4 > /proc/irq/42/smp_affinity    # CPU 2 (bitmask: 0100)

# affinity 목록 형식
echo 0-3 > /proc/irq/42/smp_affinity_list  # CPU 0~3

# irqbalance 데몬이 자동으로 분산
# /proc/interrupts로 현재 분포 확인
/* 커널에서 IRQ affinity 설정 */
struct cpumask mask;
cpumask_set_cpu(2, &mask);
irq_set_affinity(irq, &mask);

/* managed affinity: 커널이 자동 분산 (MSI-X용) */
struct irq_affinity affd = {
    .pre_vectors  = 1,   /* admin queue 전용 */
    .post_vectors = 0,
};
pci_alloc_irq_vectors_affinity(pdev, min, max,
    PCI_IRQ_MSIX | PCI_IRQ_AFFINITY, &affd);

irqbalance 데몬

irqbalance는 하드웨어 인터럽트를 CPU 코어 간에 자동으로 분산시키는 유저스페이스 데몬입니다. 주기적으로 /proc/interrupts/proc/stat를 읽어 인터럽트 부하를 측정하고, /proc/irq/<N>/smp_affinity를 통해 재분배합니다. NUMA 토폴로지(Topology), CPU 캐시 계층, 전력 관리 힌트까지 고려하여 최적의 affinity를 결정합니다.

분산 알고리즘과 정책

irqbalance는 CPU 토폴로지를 트리 구조로 모델링하여 인터럽트를 분산합니다:

irqbalance 내부 토폴로지 트리 (2-소켓, 8코어) NUMA Node 0 / Package 0 Cache Domain 0 CPU 0 CPU 1 Cache Domain 1 CPU 2 CPU 3 NUMA Node 1 / Package 1 Cache Domain 2 CPU 4 CPU 5 Cache Domain 3 CPU 6 CPU 7 irqbalance 분산 단계 (로드 기반) ① NUMA 레벨: 디바이스 NUMA 근접성 기반 배치 ② Package → Cache Domain → CPU 순으로 세분화 (IRQ 부하 분산)

irqbalance는 세 가지 분산 정책(hint policy)을 지원합니다:

정책설명적용 대상
HINT_EXACT드라이버가 설정한 affinity hint를 그대로 사용MSI-X capable NIC (RSS 큐별 고정)
HINT_SUBSEThint를 참고하되 부하에 따라 부분 이동일반적인 PCI 디바이스
HINT_IGNOREhint 무시, 순수 부하 기반 분산--hintpolicy=ignore 옵션 사용 시
💡

드라이버가 irq_set_affinity_hint()로 설정한 힌트는 irqbalance의 분산 결정에 영향을 줍니다. 고성능 NIC 드라이버(ixgbe, mlx5 등)는 RSS 큐별로 최적의 CPU를 힌트로 제공하며, irqbalance는 이를 존중합니다.

전력 인식 모드 (Power-aware Mode)

irqbalance는 기본적으로 전력 효율을 고려합니다. 시스템 부하가 낮을 때는 인터럽트를 최소한의 CPU 패키지에 집중시켜 유휴 패키지가 깊은 C-state에 진입할 수 있도록 합니다:

# 전력 인식 모드 (기본값) — 유휴 패키지 절전
irqbalance --powerthresh=2  # 분류 threshold (기본: 2)

# 성능 모드 — 전력 무시, 순수 부하 분산
irqbalance --foreground --powerthresh=0

# C-state 관점:
# 전력 인식 ON:  유휴 CPU → C3/C6 진입 → 전력 절감
# 전력 인식 OFF: 모든 CPU에 분산 → C1에서 대기 → 레이턴시 감소

설정과 운영

# 서비스 관리
systemctl status irqbalance
systemctl enable --now irqbalance

# 현재 분산 상태를 사람이 읽기 쉬운 형태로 확인
# irqbalance 1.4+ 에서 --debug 모드
irqbalance --foreground --debug 2&>1 | head -50

# 주요 설정 파일: /etc/sysconfig/irqbalance 또는 /etc/default/irqbalance
# IRQBALANCE_ONESHOT=yes      — 한 번만 분산 후 종료 (부팅 시 초기 배치용)
# IRQBALANCE_BANNED_CPUS=0x0c — CPU 2,3을 분산 대상에서 제외 (bitmask)
# IRQBALANCE_BANNED_CPULIST=2,3 — CPU 목록으로 제외 (1.8+)
# IRQBALANCE_ARGS="--hintpolicy=exact --powerthresh=0"
# 주요 커맨드라인 옵션
irqbalance \
  --hintpolicy=exact \    # exact|subset|ignore — 드라이버 hint 정책
  --powerthresh=0 \       # 0=성능 모드, 높을수록 공격적 절전
  --banirq=42 \           # 특정 IRQ를 분산 대상에서 제외
  --banscript=/path \     # 동적 제외 판단 스크립트
  --policyscript=/path \  # 커스텀 분산 정책 스크립트
  --deepestcache=2 \      # 분산 단위 캐시 레벨 (1=L1, 2=L2, 3=L3)
  --journal \             # systemd journal로 로그 출력
  --interval=10           # 재분산 주기 (초, 기본: 10)

특정 IRQ 격리(Isolation)와 제외

실시간 워크로드나 DPDK 같은 전용 CPU가 필요한 환경에서는 irqbalance로부터 특정 IRQ나 CPU를 격리해야 합니다:

# 방법 1: 특정 IRQ를 irqbalance에서 제외
# /etc/sysconfig/irqbalance 또는 커맨드라인
irqbalance --banirq=42 --banirq=43

# 방법 2: 특정 CPU를 irqbalance에서 제외
# CPU 4-7을 실시간 전용으로 격리
IRQBALANCE_BANNED_CPULIST=4-7

# 방법 3: 커널 부트 파라미터로 CPU 격리 (isolcpus)
# GRUB_CMDLINE_LINUX="isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7"
# irqbalance는 isolcpus를 자동으로 인식하여 해당 CPU 제외

# 방법 4: 커널 드라이버에서 IRQF_NOBALANCING 플래그
/* 드라이버에서 irqbalance 이동 방지 */
request_irq(irq, my_handler,
    IRQF_NOBALANCING | IRQF_NO_THREAD,
    "my_realtime_dev", dev);

/* affinity hint 설정 — irqbalance가 참고 */
static struct cpumask hint_mask;
cpumask_set_cpu(2, &hint_mask);
irq_set_affinity_hint(irq, &hint_mask);

/* 드라이버 종료 시 hint 해제 */
irq_set_affinity_hint(irq, NULL);

irqbalance vs 수동 Affinity 설정

기준irqbalance 자동수동 smp_affinity
NUMA 인식자동 (토폴로지 감지)관리자가 직접 계산
부하 적응10초 주기 재분산정적 (reboot 시 초기화)
NIC RSS 최적화hint_policy=exact 사용set_irq_affinity 스크립트
실시간 워크로드banirq/banned_cpus로 제외직접 제어 (결정론적)
CPU 핫플러그(Hotplug)자동 재배치(Relocation)수동 재설정 필요
디버깅 용이성동적 변경으로 추적 어려움고정되어 추적 쉬움
적합한 환경범용 서버, 클라우드HPC, 실시간, DPDK, 저지연 트레이딩
⚠️

irqbalance와 수동 affinity를 동시에 사용하면 충돌합니다. 수동으로 smp_affinity를 설정해도 irqbalance가 다음 주기에 덮어씁니다. 수동 설정이 필요한 IRQ는 반드시 --banirq로 제외하거나, irqbalance를 비활성화하세요.

네트워크 인터럽트 분산 최적화

고성능 네트워크 환경에서는 irqbalance만으로 충분하지 않을 수 있습니다. NIC의 RSS(Receive Side Scaling), RPS(Receive Packet Steering), RFS(Receive Flow Steering)와 조합하여 최적화합니다:

# 1. NIC RSS 큐 수 확인 (MSI-X 인터럽트 수)
ethtool -l eth0
# Channel parameters for eth0:
# Pre-set maximums:
#   Combined:    64
# Current hardware settings:
#   Combined:    8

# 2. RSS 큐별 인터럽트 확인
grep eth0 /proc/interrupts
#  128:  1234567        0        0        0  eth0-TxRx-0
#  129:        0  2345678        0        0  eth0-TxRx-1
#  130:        0        0  3456789        0  eth0-TxRx-2
#  ...

# 3. irqbalance가 RSS 큐를 NUMA-local CPU에 배치하는지 확인
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    echo "IRQ $irq → CPU mask: $(cat /proc/irq/$irq/smp_affinity_list)"
done

# 4. NIC NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# 0  ← irqbalance는 이 NUMA 노드의 CPU에 우선 배치

# 5. RPS로 소프트웨어 분산 보충 (RSS 큐가 부족할 때)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
💡

10Gbps 이상 NIC에서는 irqbalance --hintpolicy=exact를 사용하고, NIC 드라이버의 set_irq_affinity 스크립트(Intel ixgbe/ice, Mellanox mlx5 등에 포함)로 초기 배치 후 irqbalance가 유지하도록 하는 것이 권장됩니다.

모니터링과 트러블슈팅

# irqbalance 동작 상태 확인
# 소켓 기반 인터페이스 (irqbalance 1.4+)
echo settings | socat - UNIX-CONNECT:/var/run/irqbalance/irqbalance1234.sock
echo setup | socat - UNIX-CONNECT:/var/run/irqbalance/irqbalance1234.sock

# 인터럽트 분포 실시간 모니터링
watch -n 1 'cat /proc/interrupts | head -5; echo "---"; grep eth0 /proc/interrupts'

# 인터럽트 비율 변화 측정 (초당 발생 수)
# 방법: 1초 간격으로 /proc/interrupts 차이 계산
sar -I ALL 1 5   # sysstat 패키지 필요

# perf로 인터럽트 핫스팟 분석
perf stat -e irq:irq_handler_entry -a sleep 10
perf record -e irq:irq_handler_entry -ag sleep 10
perf report --sort comm,dso,symbol

# 트러블슈팅 체크리스트
# 1. irqbalance가 실행 중인가?
pidof irqbalance || echo "irqbalance is NOT running"

# 2. 특정 IRQ가 한 CPU에 고정되어 있는가?
cat /proc/irq/128/effective_affinity_list

# 3. 드라이버가 IRQF_NOBALANCING을 설정했는가?
cat /proc/irq/128/actions   # nobalancing 플래그 확인

# 4. affinity가 변경 가능한가? (일부 인터럽트는 고정)
echo 3 > /proc/irq/128/smp_affinity  # "Permission denied" → managed irq
ℹ️

커널 4.x 이후 managed_irq 인터럽트(주로 MSI-X blk-mq, NVMe)는 커널이 직접 affinity를 관리합니다. irqbalance는 이러한 인터럽트를 자동으로 건너뜁니다. /proc/irq/<N>/effective_affinity로 실제 적용된 affinity를 확인할 수 있습니다.

실전 디바이스 드라이버 예제

이론을 넘어 실제 디바이스 드라이버에서 인터럽트를 어떻게 처리하는지 end-to-end 예제를 통해 학습합니다.

UART 드라이버: IRQ부터 Bottom Half까지

UART 드라이버는 인터럽트 기반 I/O의 전형적인 예입니다. 데이터 수신 시 인터럽트가 발생하고, top half에서 하드웨어 레지스터를 읽은 뒤, bottom half에서 tty 레이어로 데이터를 전달합니다:

/* drivers/tty/serial/my_uart.c */
#include <linux/serial_core.h>
#include <linux/interrupt.h>
#include <linux/tty_flip.h>

struct my_uart_port {
    struct uart_port port;
    void __iomem *base;
    int irq;
    struct tasklet_struct rx_tasklet;
    unsigned char rx_buffer[256];
    int rx_count;
};

/* Top Half: 인터럽트 핸들러 */
static irqreturn_t my_uart_irq(int irq, void *dev_id)
{
    struct my_uart_port *up = dev_id;
    u32 status;

    /* 1. 인터럽트 원인 확인 */
    status = readl(up->base + UART_STATUS);
    if (!(status & UART_INT_PENDING))
        return IRQ_NONE;  /* 공유 IRQ: 우리 인터럽트 아님 */

    /* 2. 긴급 처리: 하드웨어 FIFO overflow 방지 */
    if (status & UART_RX_READY) {
        int count = 0;
        while (readl(up->base + UART_STATUS) & UART_RX_READY) {
            up->rx_buffer[count++] = readl(up->base + UART_DATA);
            if (count >= sizeof(up->rx_buffer))
                break;  /* 버퍼 오버플로 방지 */
        }
        up->rx_count = count;

        /* 3. Bottom Half 예약 */
        tasklet_schedule(&up->rx_tasklet);
    }

    /* 4. 인터럽트 클리어 (하드웨어 종속) */
    writel(status, up->base + UART_INT_CLEAR);

    return IRQ_HANDLED;
}

/* Bottom Half: Tasklet에서 실행 */
static void my_uart_rx_tasklet(unsigned long data)
{
    struct my_uart_port *up = (struct my_uart_port *)data;
    struct tty_port *tport = &up->port.state->port;
    int i;

    /* TTY 레이어로 데이터 전달 (슬립 불가) */
    for (i = 0; i < up->rx_count; i++) {
        if (!tty_insert_flip_char(tport, up->rx_buffer[i], TTY_NORMAL))
            break;  /* TTY 버퍼 가득 참 */
    }

    /* TTY 플립 버퍼 처리 예약 */
    tty_flip_buffer_push(tport);

    up->rx_count = 0;
}

/* 드라이버 초기화 */
static int my_uart_probe(struct platform_device *pdev)
{
    struct my_uart_port *up;
    int ret;

    up = devm_kzalloc(&pdev->dev, sizeof(*up), GFP_KERNEL);
    if (!up)
        return -ENOMEM;

    /* 1. 리소스 획득 */
    up->base = devm_platform_ioremap_resource(pdev, 0);
    up->irq = platform_get_irq(pdev, 0);

    /* 2. Tasklet 초기화 */
    tasklet_init(&up->rx_tasklet, my_uart_rx_tasklet,
                 (unsigned long)up);

    /* 3. IRQ 등록 */
    ret = request_irq(up->irq, my_uart_irq,
                      IRQF_SHARED, "my_uart", up);
    if (ret) {
        tasklet_kill(&up->rx_tasklet);
        return ret;
    }

    /* 4. UART 포트 등록 */
    ret = uart_add_one_port(&my_uart_driver, &up->port);
    if (ret) {
        free_irq(up->irq, up);
        tasklet_kill(&up->rx_tasklet);
    }

    return ret;
}

/* 드라이버 정리 */
static int my_uart_remove(struct platform_device *pdev)
{
    struct my_uart_port *up = platform_get_drvdata(pdev);

    uart_remove_one_port(&my_uart_driver, &up->port);
    free_irq(up->irq, up);
    tasklet_kill(&up->rx_tasklet);  /* 대기 중인 tasklet 완료 대기 */

    return 0;
}
💡

핵심 패턴: Top half는 하드웨어 레지스터만 읽고(최소 작업), bottom half(tasklet)에서 복잡한 처리를 수행합니다. tasklet_kill()은 제거 전 반드시 호출하여 진행 중인 tasklet이 완료되도록 보장해야 합니다.

네트워크 카드: NAPI + Interrupt Coalescing

고속 네트워크 카드는 패킷마다 인터럽트를 발생시키면 CPU가 인터럽트 처리에 압도됩니다. NAPI(New API)는 인터럽트와 폴링(Polling)을 혼합하여 효율을 높입니다:

/* drivers/net/ethernet/my_netdev.c */
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/interrupt.h>

struct my_netdev_priv {
    struct napi_struct napi;
    void __iomem *regs;
    int irq;
};

/* Top Half: 인터럽트 핸들러 (매우 짧음) */
static irqreturn_t my_netdev_irq(int irq, void *dev_id)
{
    struct net_device *ndev = dev_id;
    struct my_netdev_priv *priv = netdev_priv(ndev);
    u32 status;

    status = readl(priv->regs + REG_INT_STATUS);
    if (!status)
        return IRQ_NONE;

    if (status & INT_RX_DONE) {
        /* 1. 인터럽트 비활성화 (하드웨어 레벨) */
        writel(0, priv->regs + REG_INT_ENABLE);

        /* 2. NAPI 폴링 예약 (softirq에서 실행) */
        if (napi_schedule_prep(&priv->napi))
            __napi_schedule(&priv->napi);
    }

    writel(status, priv->regs + REG_INT_CLEAR);
    return IRQ_HANDLED;
}

/* NAPI poll 함수: softirq 컨텍스트에서 실행 */
static int my_netdev_poll(struct napi_struct *napi, int budget)
{
    struct my_netdev_priv *priv = container_of(napi, struct my_netdev_priv, napi);
    int work_done = 0;

    /* 패킷을 budget 개수만큼 처리 (공평성 보장) */
    while (work_done < budget) {
        struct sk_buff *skb;
        u32 status = readl(priv->regs + REG_RX_STATUS);

        if (!(status & RX_PKT_READY))
            break;  /* 더 이상 패킷 없음 */

        /* 패킷 수신 및 프로토콜 스택 전달 */
        skb = my_netdev_receive_packet(priv);
        if (skb) {
            napi_gro_receive(napi, skb);  /* GRO: 패킷 병합 최적화 */
            work_done++;
        }
    }

    /* 처리 완료: 인터럽트 재활성화 */
    if (work_done < budget) {
        napi_complete_done(napi, work_done);
        writel(INT_RX_DONE, priv->regs + REG_INT_ENABLE);
    }

    return work_done;
}

/* Interrupt Coalescing: 여러 이벤트를 묶어 인터럽트 감소 */
static void my_netdev_set_coalesce(struct my_netdev_priv *priv)
{
    /* 64개 패킷 또는 100μs 중 먼저 도달하는 조건에 인터럽트 */
    writel(64, priv->regs + REG_INT_COALESCE_COUNT);
    writel(100, priv->regs + REG_INT_COALESCE_USEC);
}
ℹ️

NAPI 동작 원리: 첫 번째 패킷에서 인터럽트가 발생하면, 이후 인터럽트를 비활성화하고 폴링 모드로 전환합니다. 패킷이 없으면 다시 인터럽트 모드로 복귀합니다. 이렇게 하여 고부하 시 인터럽트 횟수를 획기적으로 줄입니다(10GbE에서 초당 수백만 개 → 수만 개).

DMA + Interrupt 조합

DMA 엔진과 인터럽트를 함께 사용하는 실제 패턴입니다. DMA 전송 완료 시 인터럽트가 발생합니다:

/* drivers/dma/my_dma_driver.c */
#include <linux/dmaengine.h>
#include <linux/interrupt.h>

struct my_device {
    struct dma_chan *dma_chan;
    dma_addr_t dma_addr;
    void *cpu_addr;
    size_t size;
    struct completion dma_complete;
};

/* DMA 완료 콜백: softirq 또는 tasklet 컨텍스트 */
static void my_dma_callback(void *param)
{
    struct my_device *dev = param;

    /* DMA 완료 신호 */
    complete(&dev->dma_complete);
}

/* DMA 전송 시작 */
static int my_device_dma_transfer(struct my_device *dev,
                                    dma_addr_t src, size_t len)
{
    struct dma_async_tx_descriptor *desc;
    dma_cookie_t cookie;

    /* 1. DMA 전송 기술자 준비 */
    desc = dmaengine_prep_dma_memcpy(dev->dma_chan, dev->dma_addr,
                                        src, len, DMA_PREP_INTERRUPT);
    if (!desc)
        return -ENOMEM;

    /* 2. 완료 콜백 설정 */
    desc->callback = my_dma_callback;
    desc->callback_param = dev;

    /* 3. DMA 전송 제출 */
    cookie = dmaengine_submit(desc);
    if (dma_submit_error(cookie))
        return -EIO;

    /* 4. DMA 시작 */
    dma_async_issue_pending(dev->dma_chan);

    /* 5. 완료 대기 (슬립 가능한 컨텍스트에서만) */
    if (!wait_for_completion_timeout(&dev->dma_complete,
                                       msecs_to_jiffies(5000))) {
        dev_err(dev->dev, "DMA timeout\n");
        return -ETIMEDOUT;
    }

    return 0;
}

/* 또 다른 패턴: 인터럽트 핸들러에서 직접 DMA 상태 확인 */
static irqreturn_t my_device_irq_dma(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = readl(dev->regs + DMA_STATUS);

    if (status & DMA_COMPLETE) {
        /* DMA 완료: 캐시 일관성 보장 */
        dma_sync_single_for_cpu(dev->dev, dev->dma_addr,
                                  dev->size, DMA_FROM_DEVICE);

        /* 완료 처리 (예: workqueue로 전달) */
        schedule_work(&dev->process_work);

        writel(DMA_COMPLETE, dev->regs + DMA_STATUS);  /* 클리어 */
        return IRQ_HANDLED;
    }

    if (status & DMA_ERROR) {
        dev_err(dev->dev, "DMA error: 0x%x\n", status);
        writel(DMA_ERROR, dev->regs + DMA_STATUS);
        return IRQ_HANDLED;
    }

    return IRQ_NONE;
}
⚠️

DMA 캐시 일관성(Cache Coherency): DMA 전송 전후로 dma_sync_* 함수를 반드시 호출해야 합니다. CPU 캐시와 DMA 메모리 간 불일치를 방지하기 위함입니다. 특히 ARM 같은 아키텍처에서는 누락 시 데이터 손상이 발생할 수 있습니다.

예제 패턴 비교

드라이버 유형Top HalfBottom Half주요 고려사항
UART (저속)FIFO 읽기Tasklet → TTYFIFO overflow 방지, 직렬 처리
네트워크 (고속)인터럽트 비활성화NAPI 폴링 (softirq)인터럽트 최소화, GRO 최적화
블록 I/O상태 확인Workqueue슬립 가능, 긴 처리 시간
DMA 기반완료 확인Callback 또는 Workqueue캐시 일관성, 타임아웃 처리
실시간 (Audio)Threaded IRQN/A레이턴시 예측성, 우선순위(Priority)

IPI (Inter-Processor Interrupt)

IPI(Inter-Processor Interrupt)는 SMP 시스템에서 한 CPU가 다른 CPU에게 보내는 특수한 인터럽트입니다. 스케줄러(Scheduler) 밸런싱(reschedule IPI), TLB 캐시 일관성(TLB flush IPI), 원격 함수 호출(smp_call_function) 등 CPU 간 협조가 필요한 거의 모든 작업에 IPI가 관여합니다.

상세 문서: x86 ICR/APIC 아키텍처, IPI 벡터 유형, reschedule IPI, TLB flush IPI, smp_call_function API, IPI 성능 분석, SMP 부팅 시 IPI 활용은 IPI 페이지에서 다룹니다.

NMI (Non-Maskable Interrupt)

NMI는 CPU의 마스킹 메커니즘(cli/local_irq_disable())으로 비활성화할 수 없는 특수한 인터럽트입니다. 하드웨어 오류 감지, 커널 교착상태(hardlockup) 탐지, 성능 프로파일링(Profiling), 디버거 진입 등 크리티컬한 용도에 사용됩니다.

상세 문서: NMI 소스, x86 NMI 아키텍처, hardlockup watchdog, PMU NMI, NMI 핸들링 제약, NMI 디버깅은 NMI 페이지에서 다룹니다.

IRQ 디버깅

인터럽트 관련 문제는 타이밍에 민감하여 재현이 어렵습니다. 다음 도구들로 체계적으로 진단할 수 있습니다.

/proc/interrupts 해석

# 인터럽트 카운터 확인
cat /proc/interrupts
#            CPU0       CPU1       CPU2       CPU3
#   0:         45          0          0          0  IR-IO-APIC   2-edge    timer
#  16:      12345          0          0          0  IR-IO-APIC  16-fastedge  ahci[0]
# 142:          0    8765432          0          0  IR-PCI-MSI-X  0-edge    nvme0q1
#
# 열 해석: IRQ번호, CPU별 카운트, 컨트롤러, 트리거 유형, 디바이스명
# 특정 CPU에 카운트가 집중되면 affinity 조정 필요

# softirq 카운터 확인
cat /proc/softirqs
#                     CPU0       CPU1       CPU2       CPU3
#       HI:            5          0          0          0
#     TIMER:      1234567    1234568    1234569    1234570
#    NET_TX:         1234         45          0          0
#    NET_RX:      5678901      12345          0          0
# NET_RX가 한 CPU에 집중되면 RPS/RFS 또는 RSS 설정 필요

ftrace IRQ 트레이싱

# IRQ 핸들러 실행 시간 추적
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable

# softirq 실행 추적
echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_exit/enable

# 결과 확인
cat /sys/kernel/debug/tracing/trace
# irq_handler_entry: irq=16 name=ahci[0]
# irq_handler_exit:  irq=16 ret=handled
# softirq_entry:     vec=3 [action=NET_RX]
# softirq_exit:      vec=3 [action=NET_RX]

# irqsoff tracer: 인터럽트 비활성 최대 시간 측정
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/tracing_max_latency

트러블슈팅

인터럽트 관련 문제는 간헐적이고 타이밍에 민감하여 진단이 어렵습니다. 다음은 일반적인 문제와 체계적인 해결 방법입니다.

일반적인 문제와 해결책

문제증상원인해결 방법
IRQ 불균형 한 CPU만 100% 사용률 모든 인터럽트가 CPU0에 집중 irqbalance 시작, 또는 수동 affinity 분산
Interrupt Storm 시스템 응답 없음, %irq 80%+ 하드웨어 오류, 잘못된 드라이버 해당 IRQ 비활성화, 하드웨어 점검
공유 IRQ 경쟁 IRQ_NONE 경고, 성능 저하 여러 디바이스가 같은 IRQ 공유 MSI/MSI-X 사용, 또는 디바이스 재배치
레이턴시 스파이크 간헐적 지연, 타임아웃 긴 인터럽트 핸들러, 선점 불가 구간 Threaded IRQ 전환, ftrace로 핫스팟 분석
Lost Interrupt 디바이스 타임아웃, I/O 정지 인터럽트 마스크 누락, 하드웨어 버그 폴링 모드 전환, 드라이버 패치(Patch)
NMI Watchdog 타임아웃 NMI watchdog: BUG: soft lockup 인터럽트 비활성 구간 너무 김(>20초) 긴 루프에 cond_resched() 추가
Spurious IRQ irq N: nobody cared 커널 로그 공유 IRQ에서 모든 핸들러가 IRQ_NONE 반환 드라이버 핸들러 수정, IRQF_SHARED 확인

IRQ 불균형 진단 및 해결

한 CPU에 인터럽트가 집중되면 병목이 발생합니다. 다음 절차로 진단하고 해결합니다:

# 1. CPU별 인터럽트 분포 확인
mpstat -P ALL 1 5
#   %irq 컬럼이 한 CPU에서만 높으면 불균형

# 2. 특정 IRQ가 어느 CPU에서 처리되는지 확인
watch -n 1 'cat /proc/interrupts | grep -E "16:|CPU"'

# 3. irqbalance가 실행 중인지 확인
systemctl status irqbalance

# 4. 수동 분산 (irqbalance 중지 후)
systemctl stop irqbalance

# IRQ 16을 CPU 0-3에 분산 (비트마스크 0x0F)
echo 0f > /proc/irq/16/smp_affinity

# 5. 효과 검증 (5초 동안 모니터링)
sar -I ALL 1 5
💡

자동 vs 수동: 일반 서버는 irqbalance에 맡기는 것이 좋습니다. 단, 실시간 워크로드나 DPDK 같은 전용 환경에서는 수동으로 affinity를 고정하여 예측 가능성을 높입니다.

Interrupt Storm 감지 및 완화

Interrupt storm은 초당 수백만 개의 인터럽트가 발생하여 시스템이 마비되는 현상입니다:

# 1. Interrupt storm 감지
#    특정 IRQ의 카운트가 초당 수만 개 이상 증가하는지 확인
watch -d -n 1 'cat /proc/interrupts | head -20'

# 2. 문제 IRQ 식별
#    예: IRQ 19번이 폭증
cat /proc/interrupts | grep '^ *19:'
#    19:  123456789   0   0   0   IO-APIC  19-fasteoi   eth0

# 3. 긴급 완화: 해당 IRQ 비활성화 (디바이스 사용 중단)
echo 0 > /proc/irq/19/smp_affinity
#    또는 디바이스 드라이버 언로드
rmmod e1000e

# 4. 근본 원인 분석
dmesg | grep -i 'irq 19'
#    하드웨어 오류, 잘못된 드라이버 설정 확인

# 5. APIC 에러 확인 (하드웨어 이슈)
grep -i apic /var/log/kern.log
/* 커널 레벨 interrupt storm 감지 (kernel/irq/spurious.c) */
/* 100ms 동안 100,000번 이상 인터럽트 발생 시 자동 비활성화 */

static void note_interrupt(struct irq_desc *desc, irqreturn_t action_ret)
{
    if (action_ret == IRQ_NONE) {
        desc->irqs_unhandled++;
        if (desc->irqs_unhandled > 100000) {
            printk(KERN_WARNING "irq %d: nobody cared\n", desc->irq);
            __report_bad_irq(desc, action_ret);
            desc->istate |= IRQS_SPURIOUS_DISABLED;
        }
    }
}

공유 IRQ 디버깅

공유 IRQ 환경에서는 여러 디바이스가 같은 인터럽트 라인을 사용합니다. 핸들러가 제대로 구현되지 않으면 문제가 발생합니다:

# 1. 공유 IRQ 확인
cat /proc/interrupts | awk '$NF ~ /-edge|fasteoi/ {print}'
#    여러 디바이스가 같은 줄에 나열되면 공유

# 2. Spurious IRQ 경고 확인
dmesg | grep 'nobody cared'
#    출력 예: irq 19: nobody cared (try booting with the "irqpoll" option)

# 3. irqpoll 옵션으로 부팅 (긴급 회피)
#    /etc/default/grub에 추가:
#    GRUB_CMDLINE_LINUX="irqpoll"
#    이후 update-grub && reboot

# 4. MSI/MSI-X로 전환 (근본 해결)
lspci -vvv | grep -A 10 'Ethernet'
#    Capabilities: [MSI] 또는 [MSI-X] 확인

# 드라이버에서 MSI 활성화 여부 확인
cat /sys/class/net/eth0/device/msi_bus
#    1이면 MSI 사용 중
⚠️

공유 IRQ 핸들러 규칙: IRQF_SHARED 플래그를 사용하는 핸들러는 반드시 자신의 디바이스에서 발생한 인터럽트인지 확인하고, 아니면 IRQ_NONE을 반환해야 합니다. 그렇지 않으면 다른 디바이스의 인터럽트를 가로채게 됩니다.

레이턴시 스파이크 분석

간헐적인 레이턴시 급증은 인터럽트 핸들러가 너무 오래 실행되거나, 선점이 지연되는 경우 발생합니다:

# 1. 인터럽트 비활성 구간 최대 시간 측정
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 10
cat /sys/kernel/debug/tracing/tracing_max_latency
#    출력 예: 523 (단위: μs) → 523μs 동안 인터럽트 비활성

# 트레이스 확인 (어디서 오래 걸렸는지)
cat /sys/kernel/debug/tracing/trace | head -50

# 2. 특정 함수의 실행 시간 측정
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo do_IRQ > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-duration
cat /sys/kernel/debug/tracing/trace

# 3. cyclictest로 실시간 레이턴시 측정 (RT 시스템)
cyclictest -p 95 -m -n -i 200 -l 10000
#    Max 레이턴시가 100μs 이상이면 문제

# 4. perf로 인터럽트 핸들러 프로파일링
perf record -e 'irq:*' -ag sleep 10
perf report --stdio | grep -A 20 'do_IRQ'

# 5. 긴 인터럽트 비활성 구간 찾기
echo 'irqsoff' > /sys/kernel/debug/tracing/current_tracer
echo 500 > /sys/kernel/debug/tracing/tracing_thresh  # 500μs 이상만
cat /sys/kernel/debug/tracing/trace

NMI Watchdog 타임아웃 대응

NMI watchdog은 CPU가 20초 이상 선점되지 않으면 soft lockup을 감지합니다:

# 1. NMI watchdog 로그 확인
dmesg | grep -i 'watchdog\|soft lockup'
#    출력 예: NMI watchdog: BUG: soft lockup - CPU#2 stuck for 22s!

# 2. 스택 트레이스에서 문제 함수 식별
#    로그에 Call Trace가 포함되어 있음

# 3. 문제 함수에 선점 포인트 추가
/* 문제 코드: 긴 루프에서 선점 불가 */
for (i = 0; i < 1000000; i++) {
    process_item(i);  /* 20초 이상 소요 가능 */
}

/* 수정: 주기적으로 선점 허용 */
for (i = 0; i < 1000000; i++) {
    process_item(i);
    if (i % 1000 == 0)
        cond_resched();  /* 필요 시 스케줄링 허용 */
}
# 4. NMI watchdog 임계값 조정 (임시 회피)
sysctl -w kernel.watchdog_thresh=30  # 기본 10초 → 30초

# 5. NMI watchdog 비활성화 (디버깅 목적만)
echo 0 > /proc/sys/kernel/nmi_watchdog

진단 체크리스트

인터럽트 문제를 체계적으로 진단하는 8단계 체크리스트:

#!/bin/bash
# 인터럽트 진단 스크립트

echo "=== 1. 인터럽트 분포 확인 ==="
mpstat -P ALL 1 3 | grep -E 'CPU|Average'

echo -e "\n=== 2. 고빈도 IRQ 식별 ==="
cat /proc/interrupts | awk 'NR==1 || $2 > 100000 {print}'

echo -e "\n=== 3. Spurious IRQ 확인 ==="
dmesg | grep -i 'nobody cared\|spurious' | tail -10

echo -e "\n=== 4. irqbalance 상태 ==="
systemctl is-active irqbalance
cat /proc/irq/default_smp_affinity

echo -e "\n=== 5. MSI/MSI-X 사용 여부 ==="
lspci -vvv | grep -i 'msi' | head -10

echo -e "\n=== 6. 인터럽트 비활성 최대 시간 ==="
cat /sys/kernel/debug/tracing/tracing_max_latency 2>/dev/null || echo "ftrace 비활성"

echo -e "\n=== 7. NMI watchdog 이벤트 ==="
dmesg | grep -i 'nmi.*watchdog\|soft lockup' | tail -5

echo -e "\n=== 8. CPU별 IRQ 처리 시간 ==="
sar -I SUM 1 3
ℹ️

성능 이슈 우선순위: (1) Interrupt storm → 시스템 마비 위험, 즉시 조치. (2) IRQ 불균형 → 처리량 저하, irqbalance로 해결. (3) 레이턴시 스파이크 → 실시간성 문제, threaded IRQ 고려. (4) 공유 IRQ → 성능 하락, MSI/MSI-X 전환.

IRQ 플로우 핸들러 상세

Linux genirq 프레임워크는 인터럽트 컨트롤러의 하드웨어 특성에 따라 다른 플로우 핸들러(flow handler)를 사용합니다. 플로우 핸들러는 인터럽트가 하드웨어에서 발생한 후 실제 디바이스 핸들러(irqaction)를 호출하기까지의 흐름을 제어합니다. 각 핸들러는 마스킹, ACK, EOI(End of Interrupt) 타이밍이 다릅니다.

IRQ 플로우 핸들러 체인 비교 handle_level_irq (레벨 트리거) mask() + ack() handle_irq_event() unmask() 핸들러 실행 중 마스킹 → 같은 IRQ 재진입 방지 handle_edge_irq (엣지 트리거) ack() handle_irq_event() IRQS_PENDING 확인 재처리 (pending) 실행 중 도착한 엣지를 PENDING 비트로 기록 handle_fasteoi_irq (GIC/APIC EOI) handle_irq_event() eoi() 핸들러 먼저 실행 후 EOI만 전송 (가장 빠름) 현대 인터럽트 컨트롤러 (GICv3, xAPIC, x2APIC) 선택 기준 GPIO, I2C 레벨 디바이스 irq_set_handler(handle_level_irq) GPIO 엣지, 레거시 ISA irq_set_handler(handle_edge_irq) GIC, APIC, MSI/MSI-X irq_set_handler(handle_fasteoi_irq)
세 가지 주요 IRQ 플로우 핸들러의 실행 순서와 마스킹 전략 비교

플로우 핸들러 코드 분석

/* kernel/irq/chip.c — handle_level_irq (레벨 트리거) */
void handle_level_irq(struct irq_desc *desc)
{
    struct irq_chip *chip = desc->irq_data.chip;

    raw_spin_lock(&desc->lock);
    mask_ack_irq(desc);        /* ① 마스크 + ACK (레벨 유지 방지) */

    if (!irq_may_run(desc))
        goto out;

    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
        desc->istate |= IRQS_PENDING;
        goto out;
    }

    handle_irq_event(desc);    /* ② 핸들러 체인 실행 */

    cond_unmask_irq(desc);     /* ③ 언마스크 (재진입 허용) */
out:
    raw_spin_unlock(&desc->lock);
}

/* handle_edge_irq (엣지 트리거) — 핸들러 실행 중 도착한 엣지 감지 */
void handle_edge_irq(struct irq_desc *desc)
{
    raw_spin_lock(&desc->lock);
    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

    if (!irq_may_run(desc)) {
        desc->istate |= IRQS_PENDING;
        mask_ack_irq(desc);
        goto out;
    }

    chip->irq_ack(&desc->irq_data);  /* ① ACK만 (마스크 안 함) */

    do {
        if (unlikely(!desc->action)) {
            mask_irq(desc);
            goto out;
        }

        if (unlikely(desc->istate & IRQS_PENDING)) {
            if (!irqd_irq_disabled(&desc->irq_data) &&
                irqd_irq_masked(&desc->irq_data))
                unmask_irq(desc);
        }

        handle_irq_event(desc);      /* ② 핸들러 실행 */

    } while ((desc->istate & IRQS_PENDING) &&  /* ③ 실행 중 재도착 확인 */
            !irqd_irq_disabled(&desc->irq_data));
out:
    raw_spin_unlock(&desc->lock);
}

/* handle_fasteoi_irq — 현대 컨트롤러용 (GIC, APIC) */
void handle_fasteoi_irq(struct irq_desc *desc)
{
    struct irq_chip *chip = desc->irq_data.chip;

    raw_spin_lock(&desc->lock);

    if (!irq_may_run(desc))
        goto out;

    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

    if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
        desc->istate |= IRQS_PENDING;
        mask_irq(desc);
        goto out;
    }

    handle_irq_event(desc);    /* ① 바로 핸들러 실행 (마스크 없음!) */

out:
    chip->irq_eoi(&desc->irq_data);  /* ② EOI만 전송 */
    raw_spin_unlock(&desc->lock);
}
코드 설명
  • handle_level_irq()kernel/irq/chip.c에 구현된 레벨 트리거 플로우 핸들러입니다. 레벨 인터럽트는 신호가 유지되는 동안 계속 발생하므로, 핸들러 실행 전에 반드시 mask_ack_irq()로 마스킹해야 합니다. 처리 후 cond_unmask_irq()로 다시 활성화합니다. 호출 경로: common_interrupt()handle_irq()handle_level_irq()handle_irq_event() → 등록된 핸들러 체인.
  • handle_edge_irq()엣지 트리거 플로우 핸들러입니다. 엣지 인터럽트는 순간적 신호이므로 마스킹 없이 ACK만 수행합니다. 핵심은 do-while 루프로, 핸들러 실행 중 도착한 새로운 엣지를 IRQS_PENDING 비트로 감지하여 재처리합니다. 이 루프가 없으면 실행 중 도착한 인터럽트를 놓칠 수 있습니다.
  • handle_fasteoi_irq()GIC, APIC 등 현대 인터럽트 컨트롤러용 플로우 핸들러입니다. 마스킹 없이 바로 핸들러를 실행하고, 완료 후 chip->irq_eoi()만 호출하므로 오버헤드가 최소입니다. MSI/MSI-X도 이 핸들러를 사용합니다. 대부분의 현대 시스템에서 기본 플로우 핸들러입니다.

플로우 핸들러 비교

특성handle_level_irqhandle_edge_irqhandle_fasteoi_irq
트리거 유형레벨 (High/Low)엣지 (Rising/Falling)컨트롤러 의존
마스킹핸들러 전 mask, 후 unmask필요 시만 mask마스킹 없음
ACK 타이밍mask와 동시핸들러 전없음 (EOI 대체)
EOI없음없음핸들러 후
재진입 처리마스크로 차단PENDING 비트 루프컨트롤러 위임
오버헤드중간 (mask/unmask)높음 (재확인 루프)최소 (EOI만)
사용 하드웨어GPIO 레벨, I2CGPIO 엣지, ISAGIC, APIC, MSI
실무 팁: 인터럽트 컨트롤러 드라이버를 작성할 때 irq_set_chip_and_handler()로 플로우 핸들러를 지정합니다. 대부분의 현대 SoC는 handle_fasteoi_irq를 사용하며, GPIO 컨트롤러만 handle_level_irq/handle_edge_irq를 사용합니다.

GIC 아키텍처 (ARM)

GIC(Generic Interrupt Controller)는 ARM 아키텍처의 표준 인터럽트 컨트롤러입니다. GICv2에서 시작하여 GICv3/v4로 발전하면서 수천 개의 인터럽트와 가상화(Virtualization)를 지원합니다. GIC는 Distributor, Redistributor, CPU Interface 세 가지 주요 컴포넌트로 구성됩니다.

GICv3 아키텍처 상세 인터럽트 소스 SPI (0-987) PPI (16-31) SGI (0-15) LPI (8192+) MSI (ITS 경유) FIQ/IRQ ITS Interrupt Translation Distributor (GICD) GICD_CTLR (전역 enable) GICD_ISENABLERn (개별 enable) GICD_IROUTERn (affinity 라우팅) Redistributor 0 (GICR) GICR_WAKER, GICR_PENDBASER LPI pending table, config table Redistributor 1 (GICR) Per-CPU PPI/SGI 관리 LPI pending table, config table Redistributor N ... CPU Interface 0 ICC_IAR1_EL1 (ACK) ICC_EOIR1_EL1 (EOI) CPU Interface 1 System register 접근 (GICv3: MMIO 제거) CPU Core 0 CPU Core 1 GICv3: System register 기반 CPU Interface (MMIO 제거) + ITS로 LPI/MSI 지원 GICv4: vLPI 직접 주입 (가상화 가속)
GICv3 아키텍처: Distributor, Redistributor, CPU Interface, ITS 구성

GIC 인터럽트 유형

유형ID 범위범위설명
SGI (Software Generated Interrupt)0-15Per-CPUIPI 용도, GICD_SGIR로 생성
PPI (Private Peripheral Interrupt)16-31Per-CPUCPU 타이머(Timer), PMU 등 CPU 전용 인터럽트
SPI (Shared Peripheral Interrupt)32-1019전역일반 디바이스 인터럽트, affinity 설정 가능
LPI (Locality-specific Peripheral Interrupt)8192+전역GICv3 전용, ITS 경유, MSI/MSI-X
/* drivers/irqchip/irq-gic-v3.c — GICv3 초기화 */
static int gic_init_bases(void __iomem *dist_base,
                          struct redist_region *rdist_regs,
                          u32 nr_redist_regions,
                          u64 redist_stride)
{
    u32 typer;

    /* Distributor 설정 */
    typer = readl_relaxed(dist_base + GICD_TYPER);
    gic_data.rdists.gicd_typer = typer;

    gic_data.irq_nr = GICD_TYPER_SPIS(typer);  /* SPI 개수 */
    if (gic_data.irq_nr > 1020)
        gic_data.irq_nr = 1020;

    /* GICv3 우선순위 그룹 */
    gic_data.prio_bits = GICD_TYPER_NUM_LPIS(typer);

    /* Distributor enable: Group0 + Group1 */
    writel_relaxed(GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A |
                   GICD_CTLR_ENABLE_G1, dist_base + GICD_CTLR);

    /* SPI 라우팅: Affinity Routing Enable (ARE) */
    gic_dist_config(dist_base, gic_data.irq_nr, NULL);

    return 0;
}

/* GICv3 CPU Interface: System register 접근 */
static void gic_cpu_sys_reg_init(void)
{
    /* ICC_SRE_EL1: System Register Enable */
    gic_write_sre(ICC_SRE_EL1_SRE);

    /* ICC_PMR_EL1: Priority Mask (모든 우선순위 허용) */
    gic_write_pmr(DEFAULT_PMR_VALUE);

    /* ICC_CTLR_EL1: EOI mode 설정 */
    gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop);

    /* ICC_IGRPEN1_EL1: Group 1 인터럽트 enable */
    gic_write_grpen1(1);
}
GICv2 vs GICv3 차이: GICv2는 CPU Interface가 MMIO 기반이라 접근 오버헤드가 있습니다. GICv3는 System Register(ICC_*_EL1)로 전환하여 ACK/EOI 속도가 향상됩니다. 또한 GICv3의 Affinity Routing(ARE)은 64비트 MPIDR 기반으로 최대 256개 CPU를 지원하며, ITS를 통해 PCIe MSI/MSI-X를 LPI로 변환합니다.

APIC 아키텍처 (x86)

x86 시스템은 APIC(Advanced Programmable Interrupt Controller) 아키텍처를 사용합니다. 각 CPU에 내장된 LAPIC(Local APIC)과 I/O 장치의 인터럽트를 관리하는 I/O APIC으로 구성됩니다. 현대 x86에서는 xAPIC/x2APIC 모드를 지원합니다.

기술 문서: APIC 아키텍처에 대한 종합적인 내용은 APIC 페이지에서 다룹니다. LAPIC 레지스터, I/O APIC RTE, x2APIC, APIC Timer, MSI/MSI-X, APICv/AVIC 가상화, 커널 struct apic 서브시스템을 상세히 설명합니다.
x86 APIC 아키텍처 상세 인터럽트 소스 PCI 디바이스 USB/SATA NIC (MSI-X) NVMe (MSI) HPET 타이머 키보드/마우스 I/O APIC (IOAPIC) Redirection Table (24 entries) Vector: 0-255 Dest: LAPIC ID Trigger: Edge/Level Mask: Enable/Disable MSI/MSI-X (메모리 쓰기) Address: 0xFEE00000 + (APIC ID << 12) Data: Vector | Delivery Mode I/O APIC 우회 → LAPIC 직접 전달 System Bus / Interconnect LAPIC 0 (CPU 0) ISR (In-Service) IRR (Request) TPR (Priority) ICR (IPI 전송) LVT Timer LVT Perf/Thermal EOI: ISR 비트 클리어 x2APIC: MSR 접근 LAPIC 1 (CPU 1) ISR/IRR/TPR ICR (IPI 전송) LVT entries Error/CMCI IPI LAPIC N CPU Core 0 CPU Core 1 xAPIC: MMIO (0xFEE00000) | x2APIC: MSR 접근 (rdmsr/wrmsr) - 10배 빠른 EOI
x86 APIC 아키텍처: I/O APIC, LAPIC, MSI 경로와 주요 레지스터

LAPIC 주요 레지스터

레지스터오프셋(Offset)x2APIC MSR설명
APIC ID0x0200x802LAPIC 고유 식별자
TPR (Task Priority)0x0800x808현재 CPU 우선순위 임계값
EOI0x0B00x80BEnd of Interrupt (0 쓰기로 완료)
ISR (In-Service)0x100-0x1700x810-0x817현재 처리 중인 인터럽트 비트맵(Bitmap)
IRR (Request)0x200-0x2700x820-0x827대기 중인 인터럽트 비트맵
ICR (Interrupt Command)0x300-0x3100x830IPI 전송 레지스터
LVT Timer0x3200x832로컬 타이머 인터럽트 설정
LVT Performance0x3400x834성능 카운터 오버플로 인터럽트
/* arch/x86/kernel/apic/apic.c — LAPIC 초기화 */
void setup_local_APIC(void)
{
    unsigned int value;

    /* TPR: 모든 인터럽트 허용 (우선순위 0) */
    apic_write(APIC_TASKPRI, 0);

    /* Spurious Interrupt Vector: APIC enable + vector 0xFF */
    value = apic_read(APIC_SPIV);
    value &= ~APIC_VECTOR_MASK;
    value |= APIC_SPIV_APIC_ENABLED;
    value |= SPURIOUS_APIC_VECTOR;    /* 0xFF */
    apic_write(APIC_SPIV, value);

    /* LVT 설정: Timer, LINT0, LINT1, Error, PMI */
    apic_write(APIC_LVT0, APIC_DM_EXTINT);   /* LINT0: 외부 8259 */
    apic_write(APIC_LVT1, APIC_DM_NMI);      /* LINT1: NMI */
    apic_write(APIC_LVTERR, ERROR_APIC_VECTOR);
}

/* x2APIC 모드 활성화 (MSR 기반 접근) */
void enable_x2apic(void)
{
    u64 msr;

    rdmsrl(MSR_IA32_APICBASE, msr);
    if (!(msr & X2APIC_ENABLE)) {
        msr |= X2APIC_ENABLE;
        wrmsrl(MSR_IA32_APICBASE, msr);
    }
    /* 이제 apic_read/write가 MSR 사용 */
    /* xAPIC MMIO 대비 EOI가 ~10배 빠름 */
}

/* I/O APIC Redirection Table 엔트리 설정 */
static void ioapic_write_entry(int apic, int pin,
                                struct IO_APIC_route_entry e)
{
    /* 64비트 엔트리: vector, delivery, dest, trigger, mask */
    io_apic_write(apic, 0x10 + 2 * pin, *((u32 *)&e));
    io_apic_write(apic, 0x11 + 2 * pin, *((u32 *)&e + 1));
}

Threaded IRQ 실행 흐름 상세

Threaded IRQ는 인터럽트 처리의 bottom half를 전용 커널 스레드(irq/N-name)에서 실행합니다. request_threaded_irq()를 호출하면 커널이 자동으로 kthread_create()로 IRQ 스레드(Thread)를 생성합니다. 이 스레드는 SCHED_FIFO 정책으로 실행되며, RT 우선순위를 가집니다.

Threaded IRQ 실행 흐름 시간 Hard IRQ Context hardirq handler() return IRQ_WAKE_THREAD IRQ 마스킹 (IRQF_ONESHOT) wake_up_process() IRQ Thread (SCHED_FIFO) wait thread_fn() (슬립/mutex 가능) unmask IRQ wait IRQ 언마스크 스케줄러 현재 태스크 실행 중... preempt IRQ 스레드 실행 (RT 우선순위) resume 원래 태스크 재개 PREEMPT_RT 커널에서의 차이 1. 모든 인터럽트가 자동으로 threaded 변환 (IRQF_NO_THREAD 제외) 2. softirq도 스레드화 → ksoftirqd에서만 실행 3. IRQ 스레드 우선순위: chrt -f -p <priority> $(pgrep 'irq/N-name') 로 조정 가능
Threaded IRQ 실행 흐름: hardirq에서 IRQ_WAKE_THREAD 후 전용 스레드에서 bottom half 실행
/* kernel/irq/manage.c — IRQ 스레드 생성 */
static int setup_irq_thread(struct irqaction *new,
                            unsigned int irq, bool secondary)
{
    struct task_struct *t;
    struct sched_param param = {
        .sched_priority = MAX_USER_RT_PRIO / 2,  /* 기본 RT 우선순위: 50 */
    };

    /* 스레드 생성: irq/N-handler_name */
    t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
                       new->name);
    if (IS_ERR(t))
        return PTR_ERR(t);

    /* SCHED_FIFO 정책, RT 우선순위 설정 */
    sched_setscheduler_nocheck(t, SCHED_FIFO, &param);

    /* CPU affinity를 IRQ의 affinity와 동기화 */
    set_bit(IRQTF_AFFINITY, &new->thread_flags);

    new->thread = t;
    return 0;
}

/* IRQ 스레드 메인 루프 */
static int irq_thread(void *data)
{
    struct irqaction *action = data;

    while (!irq_wait_for_interrupt(action)) {  /* 인터럽트 대기 */
        irq_thread_check_affinity(action->irq, action);

        action->thread_fn(action->irq, action->dev_id);  /* 실제 처리 */

        if (test_and_clear_bit(IRQTF_ONESHOT, &action->thread_flags))
            irq_finalize_oneshot(action->irq, action);  /* unmask */
    }

    return 0;
}
코드 설명
  • setup_irq_thread()kernel/irq/manage.c에서 __setup_irq() 호출 시 threaded handler를 위한 커널 스레드를 생성합니다. kthread_create()irq/N-name 형식의 스레드를 만들고, sched_setscheduler_nocheck()SCHED_FIFO 정책과 RT 우선순위 50(MAX_USER_RT_PRIO/2)을 설정합니다. IRQTF_AFFINITY 비트를 설정하여 IRQ affinity 변경 시 스레드도 함께 이동하도록 합니다.
  • irq_thread()IRQ 스레드의 메인 루프입니다. irq_wait_for_interrupt()에서 top half의 IRQ_WAKE_THREAD 반환을 대기하고, 깨어나면 thread_fn()을 실행합니다. 매 반복마다 irq_thread_check_affinity()로 CPU 바인딩을 갱신합니다.
  • irq_finalize_oneshot()IRQF_ONESHOT 모드에서 threaded handler 완료 후 IRQ를 언마스킹합니다. 이 시점에서야 비로소 같은 IRQ 라인의 새로운 인터럽트가 다시 전달됩니다. 호출 체인: top half(IRQ_WAKE_THREAD) → irq_thread()thread_fn()irq_finalize_oneshot() → unmask.
PREEMPT_RT에서의 동작: CONFIG_PREEMPT_RT 커널에서는 IRQF_NO_THREAD 플래그가 없는 모든 인터럽트가 자동으로 threaded로 변환됩니다. 이때 hardirq 핸들러도 스레드 컨텍스트에서 실행되므로, spin_lock()이 rt_mutex로 변환되어 슬립 가능해집니다. 타이머 인터럽트와 IPI는 IRQF_NO_THREAD로 보호되어 항상 hardirq 컨텍스트에서 실행됩니다.

IRQ Affinity 및 밸런싱 상세

IRQ affinity는 특정 인터럽트를 어떤 CPU에서 처리할지 결정합니다. 올바른 affinity 설정은 캐시 지역성, NUMA 최적화, 부하 분산(Load Balancing)의 핵심입니다. Linux는 /proc/irq/N/smp_affinity 인터페이스와 irqbalance 데몬으로 이를 관리합니다.

IRQ Affinity 및 밸런싱 구조 유저스페이스 irqbalance 데몬 /proc/irq/N/smp_affinity smp_affinity_list effective_affinity 커널 API irq_set_affinity() irq_set_affinity_hint() pci_alloc_irq_vectors _affinity() managed affinity irq_desc->irq_common_data.affinity (cpumask) irq_chip->irq_set_affinity() 컨트롤러별 구현 (GIC: GICD_IROUTERn, APIC: Redirection Table) CPU 0 NIC q0, Timer CPU 1 NIC q1, SATA CPU 2 NIC q2, USB CPU 3 NIC q3, NVMe Affinity 설정 모범 사례 1. NUMA 인식: NIC IRQ를 NIC의 NUMA 노드 CPU에 배치 → 메모리 접근 지역성 2. MSI-X RSS: 큐별 IRQ를 개별 CPU에 1:1 매핑 → 록 경쟁 제거 3. 격리: 실시간 CPU는 isolcpus + IRQF_NOBALANCING으로 인터럽트 차단 4. 모니터링: /proc/irq/N/effective_affinity로 실제 적용된 CPU 확인
IRQ Affinity 설정 경로: 유저스페이스/커널 API에서 실제 CPU 배정까지
/* kernel/irq/manage.c — irq_set_affinity 내부 */
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);
    struct irq_desc *desc = irq_data_to_desc(data);
    int ret;

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

    /* online CPU만 대상으로 필터링 */
    if (cpumask_intersects(mask, cpu_online_mask))
        ret = chip->irq_set_affinity(data, mask, force);
    else
        ret = -EINVAL;

    if (ret == IRQ_SET_MASK_OK || ret == IRQ_SET_MASK_OK_DONE) {
        cpumask_copy(desc->irq_common_data.affinity, mask);
        irq_set_thread_affinity(desc);  /* threaded IRQ 스레드도 이동 */
    }

    return ret;
}

/* MSI-X managed affinity: 커널이 자동 최적 분산 */
static void irq_spread_init_one(struct cpumask *irqmsk,
                                 struct cpumask *nmsk, int cpus_per_vec)
{
    /* NUMA 노드별로 균등 분배 */
    int cpu, assigned = 0;

    for_each_cpu(cpu, nmsk) {
        cpumask_set_cpu(cpu, irqmsk);
        if (++assigned >= cpus_per_vec)
            break;
    }
}

MSI/MSI-X 인터럽트 전달 경로 상세

MSI(Message Signaled Interrupts)는 PCI 디바이스가 전용 IRQ 라인 대신 메모리 쓰기 트랜잭션(Transaction)으로 인터럽트를 발생시키는 메커니즘입니다. MSI-X는 MSI의 확장으로 최대 2048개의 독립적인 벡터를 지원하며, 각 벡터에 개별 CPU affinity를 설정할 수 있습니다.

MSI/MSI-X 인터럽트 전달 경로 PCI/PCIe 디바이스 MSI Capability Addr + Data (1-32 벡터) MSI-X Table (BAR 매핑) Entry[N]: Addr + Data + Ctrl (최대 2048 벡터) Memory Write TLP PCIe Root Complex Address Decode: 0xFEE00000-0xFEEFFFFF → LAPIC 주소 영역 DMAR/IOMMU 인터럽트 리맵 LAPIC (target CPU) IRR 비트 설정 → ISR로 전환 → IDT[vector] 호출 → do_IRQ() MSI Address/Data 인코딩 (x86) Message Address (32/64-bit) [31:20] = 0xFEE (고정) [19:12] = Destination APIC ID [3] = RH, [2] = DM (Logical/Physical) Message Data (16-bit) [7:0] = Vector Number (32-255) [10:8] = Delivery Mode (Fixed/LP) [14] = Level, [15] = Trigger Mode Legacy IRQ (비교) 1. 디바이스 → IRQ 라인 → I/O APIC 2. I/O APIC → System Bus → LAPIC 3. 공유 IRQ 문제 (PCI INTA-INTD) 4. 벡터 제한, affinity 제한 MSI/MSI-X (장점) 1. 디바이스 → 메모리 쓰기 → LAPIC 직접 2. I/O APIC 우회 (지연 감소) 3. 전용 벡터 (공유 불필요) 4. 2048 벡터, 개별 CPU affinity
MSI/MSI-X: PCI 디바이스에서 메모리 쓰기를 통한 인터럽트 전달과 Address/Data 인코딩
/* drivers/pci/msi/msi.c — MSI-X 벡터 할당 */
int pci_alloc_irq_vectors_affinity(struct pci_dev *dev,
                                    unsigned int min_vecs,
                                    unsigned int max_vecs,
                                    unsigned int flags,
                                    struct irq_affinity *affd)
{
    /* MSI-X 우선 시도, 실패 시 MSI fallback */
    if (flags & PCI_IRQ_MSIX) {
        int vecs = __pci_enable_msix_range(dev, NULL,
                    min_vecs, max_vecs, affd, flags);
        if (vecs > 0)
            return vecs;
    }

    if (flags & PCI_IRQ_MSI) {
        int vecs = __pci_enable_msi_range(dev,
                    min_vecs, max_vecs, affd);
        if (vecs > 0)
            return vecs;
    }

    /* Legacy fallback */
    if (flags & PCI_IRQ_LEGACY) {
        if (min_vecs == 1)
            return 1;
    }

    return -ENOSPC;
}

/* MSI-X 테이블 엔트리 구조 (PCI Spec) */
struct msi_msg {
    u32 address_lo;    /* [31:20]=0xFEE, [19:12]=APIC ID */
    u32 address_hi;    /* 64-bit 모드용 */
    u32 data;          /* [7:0]=Vector, [10:8]=Delivery */
};

/* MSI-X 테이블 엔트리 쓰기 */
void __pci_write_msi_msg(struct msi_desc *entry,
                          struct msi_msg *msg)
{
    void __iomem *base = entry->mask_base;
    int off = entry->msi_attrib.entry_nr * PCI_MSIX_ENTRY_SIZE;

    writel(msg->address_lo, base + off + PCI_MSIX_ENTRY_LOWER_ADDR);
    writel(msg->address_hi, base + off + PCI_MSIX_ENTRY_UPPER_ADDR);
    writel(msg->data, base + off + PCI_MSIX_ENTRY_DATA);
}
NVMe와 MSI-X: NVMe 드라이버는 큐 쌍(QP)마다 MSI-X 벡터를 할당하고, PCI_IRQ_AFFINITY 플래그로 커널이 NUMA 토폴로지를 고려하여 자동 분산합니다. 이로써 각 CPU가 자신의 큐에서 발생하는 인터럽트를 로컬에서 처리하여 캐시 경쟁을 최소화합니다.

인터럽트 비활성화 계층

Linux 커널은 여러 수준에서 인터럽트를 비활성화할 수 있습니다. 각 계층은 다른 범위와 의미를 가지며, 올바른 수준을 선택하는 것이 중요합니다. 잘못된 수준의 비활성화는 데드록이나 인터럽트 분실을 유발할 수 있습니다.

인터럽트 비활성화 계층 구조 Level 1: CPU 전역 (local_irq_disable / local_irq_save) 범위: 현재 CPU의 모든 인터럽트 비활성화 | 영향: cli/sti 명령 (x86) 또는 DAIF 마스크 (ARM) 용도: 인터럽트 컨텍스트와 공유하는 Per-CPU 데이터 보호 | 주의: NMI는 비활성화 불가 Level 2: 개별 IRQ (disable_irq / disable_irq_nosync) 범위: 특정 IRQ 번호만 비활성화 (모든 CPU) | 영향: irq_desc->depth 카운터 증가 용도: 드라이버 리소스 정리 시 | disable_irq()는 실행 중인 핸들러 완료 대기 (동기) Level 3: Bottom Half (local_bh_disable / local_bh_enable) 범위: 현재 CPU의 softirq/tasklet 비활성화 | 영향: preempt_count의 softirq 비트 용도: 프로세스 컨텍스트에서 softirq와 공유 데이터 보호 | 하드웨어 인터럽트는 허용 Level 4: Suspend/Wake (irq_set_irq_wake / enable_irq_wake) 범위: 시스템 suspend 중 특정 IRQ의 웨이크업 능력 | 영향: irq_desc->wake_depth 용도: 전원 키, WoL(Wake-on-LAN), RTC 알람 등 | IRQF_NO_SUSPEND 플래그와 결합 Level 5: 하드웨어 마스킹 (irq_chip->irq_mask / irq_unmask) 범위: 인터럽트 컨트롤러 레벨에서 특정 IRQ 마스킹 | 영향: 하드웨어 레지스터 직접 조작 용도: 플로우 핸들러 내부, IRQF_ONESHOT 처리 | GIC: GICD_ISENABLERn, APIC: Redirection Table 중첩 안전: local_irq_save/restore(flags), disable_irq/enable_irq는 depth 카운터 기반 주의: local_irq_disable/enable 쌍이 맞지 않으면 커널 패닉 또는 인터럽트 영구 비활성화
인터럽트 비활성화의 5가지 계층: CPU 전역부터 하드웨어 마스킹까지

인터럽트 비활성화 API 상세

/* === Level 1: CPU 전역 인터럽트 비활성화 === */
unsigned long flags;

/* 방법 1: flags에 현재 상태 저장 후 비활성화 (중첩 안전) */
local_irq_save(flags);
/* ... 인터럽트 비활성 구간 (최소한으로!) ... */
local_irq_restore(flags);  /* 이전 상태 복원 */

/* 방법 2: 무조건 비활성/활성 (중첩 불안전 — 권장하지 않음) */
local_irq_disable();
/* ... */
local_irq_enable();

/* === Level 2: 개별 IRQ 비활성화 === */
disable_irq(irq);           /* 동기: 진행 중인 핸들러 완료 대기 */
/* 주의: 인터럽트 컨텍스트에서 호출하면 데드록! */
enable_irq(irq);

disable_irq_nosync(irq);    /* 비동기: 즉시 반환 */
synchronize_irq(irq);       /* 필요시 별도 동기화 */
enable_irq(irq);

/* === Level 3: Bottom Half 비활성화 === */
local_bh_disable();
/* softirq/tasklet이 실행되지 않는 구간 */
/* 하드웨어 인터럽트는 여전히 발생함! */
local_bh_enable();

/* === Level 4: Wake 설정 === */
enable_irq_wake(irq);   /* suspend 중 이 IRQ로 깨어남 */
/* ... 시스템 suspend ... */
disable_irq_wake(irq);  /* resume 후 해제 */

/* === 스핀락과 인터럽트 조합 === */
spin_lock_irq(&lock);         /* local_irq_disable + spin_lock */
spin_unlock_irq(&lock);       /* spin_unlock + local_irq_enable */

spin_lock_irqsave(&lock, flags);    /* 가장 안전 (중첩 가능) */
spin_unlock_irqrestore(&lock, flags);

spin_lock_bh(&lock);          /* local_bh_disable + spin_lock */
spin_unlock_bh(&lock);        /* spin_unlock + local_bh_enable */
코드 설명
  • Level 1: local_irq_save()/local_irq_restore()현재 CPU의 모든 인터럽트를 비활성화합니다. local_irq_save()는 이전 플래그 상태를 저장하므로 중첩 호출에 안전합니다. local_irq_disable()/local_irq_enable()은 중첩 불안전하여 이미 비활성 상태에서 enable을 호출하면 의도치 않게 인터럽트를 활성화합니다. PREEMPT_RT 커널에서 local_irq_disable()은 실제 인터럽트가 아닌 선점만 비활성화합니다.
  • Level 2: disable_irq()/enable_irq()특정 IRQ 라인만 비활성화합니다. disable_irq()는 진행 중인 핸들러 완료를 대기하므로 인터럽트 컨텍스트에서 호출하면 자기 자신을 기다리는 데드록이 발생합니다. 인터럽트 핸들러 내부에서는 반드시 disable_irq_nosync()를 사용해야 합니다.
  • Level 3: local_bh_disable()/local_bh_enable()softirq와 tasklet 실행만 차단합니다. 하드웨어 인터럽트는 여전히 발생하므로 softirq와 공유하는 데이터를 보호할 때 사용합니다. 프로세스 컨텍스트에서 spin_lock_bh()가 이를 자동으로 처리합니다.
  • spin_lock_irqsave()인터럽트 핸들러와 데이터를 공유하는 가장 안전한 패턴입니다. local_irq_save() + spin_lock()을 결합하여 인터럽트 비활성화와 스핀락 획득을 원자적으로 수행합니다. 이전 인터럽트 상태를 flags에 저장하므로 중첩 호출에도 안전합니다.

비활성화 수준 선택 가이드

시나리오권장 API이유
인터럽트 핸들러와 공유 데이터spin_lock_irqsave()인터럽트 비활성 + 스핀락(Spinlock)
softirq와 공유 데이터 (프로세스)spin_lock_bh()BH만 비활성 (더 넓은 창)
드라이버 제거 시 IRQ 정리disable_irq() + free_irq()핸들러 완료 보장
인터럽트 핸들러 내부에서 IRQ 제어disable_irq_nosync()데드록 방지 (비동기)
전원 관리(Power Management) 웨이크업 소스enable_irq_wake()suspend 중 인터럽트 수신
짧은 Per-CPU 임계 구간local_irq_save()최소 오버헤드, 중첩 안전
치명적 실수: disable_irq()를 인터럽트 핸들러 내부에서 호출하면 데드록이 발생합니다. disable_irq()는 현재 실행 중인 핸들러가 완료될 때까지 대기하는데, 바로 그 핸들러 안에서 호출하면 자기 자신을 기다리게 됩니다. 인터럽트 컨텍스트에서는 반드시 disable_irq_nosync()를 사용하세요.

PREEMPT_RT 인터럽트 처리

CONFIG_PREEMPT_RT 커널은 인터럽트 처리 모델을 근본적으로 변경합니다. 거의 모든 인터럽트 핸들러가 스레드화되어 선점 가능해지며, 이를 통해 결정론적 레이턴시를 달성합니다.

주요 변경 사항

항목일반 커널 (PREEMPT)PREEMPT_RT
인터럽트 핸들러hardirq 컨텍스트스레드 컨텍스트 (IRQF_NO_THREAD 제외)
softirq인터럽트 복귀 시 실행ksoftirqd에서만 실행 (스레드화)
spin_lock()선점 비활성rt_mutex 변환 (슬립 가능)
raw_spin_lock()= spin_lock진짜 스핀락 (비선점(Non-preemptive))
local_irq_disable()인터럽트 비활성선점만 비활성 (인터럽트 허용)
raw_local_irq_disable()= local_irq_disable진짜 인터럽트 비활성
최악 레이턴시수 ms수십 us (결정론적)
/* PREEMPT_RT에서의 인터럽트 핸들러 강제 스레드화 */
/* kernel/irq/manage.c */
static int irq_setup_forced_threading(struct irqaction *new)
{
#ifdef CONFIG_IRQ_FORCED_THREADING
    /* IRQF_NO_THREAD 플래그가 없으면 강제 스레드화 */
    if (!force_irqthreads())
        return 0;

    if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
        return 0;

    /* 기존 handler를 thread_fn으로 이동 */
    new->thread_fn = new->handler;
    new->handler = irq_default_primary_handler;  /* IRQ_WAKE_THREAD 반환 */
    new->flags |= IRQF_ONESHOT;
#endif
    return 0;
}

/* PREEMPT_RT에서 spin_lock → rt_mutex 변환 */
/* include/linux/spinlock_rt.h */
#define spin_lock(lock)     rt_spin_lock(lock)    /* 슬립 가능! */
#define spin_unlock(lock)   rt_spin_unlock(lock)

/* 진짜 스핀이 필요한 곳: raw_spin_lock 사용 */
#define raw_spin_lock(lock)  __raw_spin_lock(lock)  /* 비선점 */
# PREEMPT_RT 커널에서 IRQ 스레드 우선순위 확인 및 조정
ps -eo pid,cls,rtprio,comm | grep 'irq/'
#    PID CLS RTPRIO COMMAND
#     42  FF     50 irq/16-ahci
#     43  FF     50 irq/17-eth0
#     44  FF     50 irq/18-snd_hda

# 특정 IRQ 스레드 우선순위 조정
chrt -f -p 90 43   # eth0 IRQ 스레드를 RT 우선순위 90으로

# cyclictest로 RT 레이턴시 측정
cyclictest --mlockall --priority=99 --interval=200 --loops=100000
# Min:      1 Act:    3 Avg:    2 Max:       12  (PREEMPT_RT)
# Min:      1 Act:   15 Avg:    8 Max:      523  (일반 커널)

# RT 커널 커맨드라인 최적화
# isolcpus=2-3 nohz_full=2-3 rcu_nocbs=2-3
# → CPU 2-3을 RT 전용으로 격리

인터럽트 가상화 (Interrupt Virtualization)

가상화 환경에서 게스트(Guest) OS의 인터럽트를 효율적으로 처리하는 것은 성능의 핵심입니다. 전통적인 트랩-에뮬레이션(Trap-and-Emulate) 방식은 매 인터럽트마다 VM Exit가 발생하여 큰 오버헤드를 유발합니다. 최신 하드웨어는 인터럽트를 게스트에 직접 주입(Direct Injection)하여 VMM 개입을 최소화합니다.

인터럽트 가상화 — 전통 방식 vs 직접 주입 전통 방식 (Trap + Emulate) Hardware IRQ VM Exit (비용 큼) VMM: 인터럽트 에뮬레이션 VM Entry (비용 큼) Guest IRQ Handler 직접 주입 (APICv / GICv4) Hardware IRQ HW가 직접 Guest에 주입 VM Exit 없음 Guest IRQ Handler 직접 주입은 인터럽트당 수천 사이클의 VM Exit 오버헤드를 제거합니다
전통적 트랩-에뮬레이션 방식(좌)과 하드웨어 직접 주입 방식(우) 비교

APICv / Posted Interrupts (x86)

Intel VT-x의 APICv(Advanced Programmable Interrupt Controller Virtualization)는 가상 APIC 접근을 하드웨어가 직접 처리합니다. Posted Interrupts 메커니즘은 게스트가 실행 중일 때 VM Exit 없이 인터럽트를 직접 전달합니다.

/* arch/x86/kvm/vmx/posted_intr.c — Posted Interrupt 처리 핵심 */
static void vmx_deliver_posted_interrupt(struct kvm_vcpu *vcpu, int vector)
{
    struct vcpu_vmx *vmx = to_vmx(vcpu);
    struct pi_desc *pi_desc = &vmx->pi_desc;

    /* PIR(Posted Interrupt Request)에 벡터 비트 설정 */
    if (pi_test_and_set_pir(vector, pi_desc))
        return;  /* 이미 pending */

    /* Outstanding Notification 비트 설정 */
    if (pi_test_and_set_on(pi_desc))
        return;  /* 이미 notification pending */

    /* vCPU가 실행 중이면 notification IPI 전송 → VM Exit 없이 전달 */
    if (vcpu_is_running(vcpu))
        apic_send_IPI_mask(get_cpu_mask(vcpu->cpu),
                           POSTED_INTR_VECTOR);
}

GICv4 vLPI 직접 주입 (ARM)

ARM GICv4(Generic Interrupt Controller v4)는 가상 LPI(Locality-specific Peripheral Interrupt)를 게스트 OS에 직접 전달합니다. 물리 디바이스의 인터럽트가 하이퍼바이저(Hypervisor) 개입 없이 vPE(Virtual Processing Element)로 라우팅됩니다.

/* drivers/irqchip/irq-gic-v4.c — vLPI 매핑 */
int its_map_vlpi(int irq, struct its_vlpi_map *map)
{
    /*
     * 물리 디바이스 IRQ를 가상 LPI에 매핑
     * → GIC ITS가 하드웨어 수준에서 Guest vPE에 직접 전달
     * → VM Exit 없이 Guest IRQ handler 실행
     */
    return its_send_mapvi(its_dev, map->vpe,
                          irq, map->vintid);
}

가상화 인터럽트 방식 비교

특성Trap + EmulateAPICv / Posted InterruptsGICv4 vLPI
VM Exit 발생매 인터럽트마다없음 (게스트 실행 중)없음 (vPE 스케줄 중)
하드웨어 요구기본 VT-x / ARM VirtIntel APICvARM GICv4
인터럽트 지연시간수천 사이클수백 사이클수백 사이클
디바이스 패스스루에뮬레이션 필요IOMMU IR + PIITS + vLPI 매핑
게스트 비실행 시해당 없음VM Exit → 큐잉(Queueing)Doorbell → 하이퍼바이저
커널 지원KVM 기본KVM + kvm_intelKVM + irq-gic-v4

인터럽트 디버깅

/proc/interrupts 심층 분석

# /proc/interrupts 전체 해석
cat /proc/interrupts
#            CPU0       CPU1       CPU2       CPU3
#   0:         45          0          0          0  IR-IO-APIC  2-edge     timer
#   1:          3          0          0          0  IR-IO-APIC  1-edge     i8042
#   8:          0          0          0          0  IR-IO-APIC  8-edge     rtc0
#  16:       8521       2341        456        123  IR-IO-APIC 16-fasteoi  ahci[0]
# 142:    4523678          0          0          0  IR-PCI-MSI 524288-edge nvme0q0
# 143:          0    3456789          0          0  IR-PCI-MSI 524289-edge nvme0q1
# 144:          0          0    2345678          0  IR-PCI-MSI 524290-edge nvme0q2
# NMI:       1234       1234       1234       1234  Non-maskable interrupts
# LOC:  987654321  876543210  765432109  654321098  Local timer interrupts
# RES:    1234567    1234568    1234569    1234570  Rescheduling interrupts
# CAL:      12345      12346      12347      12348  Function call interrupts
# TLB:     456789     456790     456791     456792  TLB shootdowns
#
# 해석 포인트:
# - IR-: Interrupt Remapping (VT-d/IOMMU) 활성
# - PCI-MSI: MSI/MSI-X 사용 중
# - edge/fasteoi: 플로우 핸들러 유형
# - NMI 카운트가 CPU마다 다르면: perf 프로파일링 중
# - RES 높으면: 스케줄러 IPI 과다 (태스크 마이그레이션 빈번)
# - TLB 높으면: 메모리 매핑 변경 빈번 (mmap/munmap)

# 인터럽트 변화율 측정 스크립트
prev=$(cat /proc/interrupts)
sleep 1
curr=$(cat /proc/interrupts)
diff <(echo "$prev") <(echo "$curr") | grep '^[<>]' | head -20

ftrace IRQ 이벤트

# 1. 모든 IRQ 관련 트레이스포인트 확인
ls /sys/kernel/debug/tracing/events/irq/
# irq_handler_entry  irq_handler_exit
# softirq_entry      softirq_exit      softirq_raise

# 2. IRQ 핸들러 실행 시간 측정 (function_graph)
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo handle_irq_event_percpu > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-abstime
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-proc
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -50

# 3. 특정 IRQ만 필터링 (trigger 사용)
echo 'irq==16' > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/filter
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable

# 4. irqsoff tracer: 인터럽트 비활성 최대 구간 추적
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency  # 리셋
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 워크로드 실행...
cat /sys/kernel/debug/tracing/tracing_max_latency
# 출력: 523 (μs) → 가장 긴 인터럽트 비활성 구간
cat /sys/kernel/debug/tracing/trace  # 어디서 발생했는지 스택 확인

# 5. perf + BPF로 인터럽트 핸들러 히스토그램
perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -ag sleep 10
perf script | awk '/irq_handler_entry/ {start=$4} /irq_handler_exit/ {print $4-start}'
디버깅 팁: trace-cmd 도구를 사용하면 ftrace를 더 편리하게 활용할 수 있습니다. trace-cmd record -e irq -e softirq로 기록하고 trace-cmd report로 분석합니다. kernelshark GUI 도구와 결합하면 타임라인에서 인터럽트 흐름을 시각적으로 확인할 수 있습니다.

흔한 실수와 안티패턴

인터럽트 관련 코드에서 자주 발생하는 실수와 올바른 패턴을 정리합니다. 커널 패닉(Kernel Panic), 데드락(Deadlock), 데이터 손상의 원인이 되는 안티패턴을 사전에 인지하면 디버깅 시간을 크게 줄일 수 있습니다.

안티패턴문제점올바른 패턴
인터럽트 컨텍스트에서 슬립 스케줄러 호출 → BUG: scheduling while atomic workqueue 또는 threaded IRQ 사용
disable_irq()를 IRQ 핸들러 내부에서 호출 자기 자신의 완료를 대기 → 데드락 disable_irq_nosync() 사용
threaded IRQ에서 IRQF_ONESHOT 누락 하드웨어 IRQ 재활성 시점 불명확 → IRQ 폭주(Storm) 항상 IRQF_ONESHOT 플래그 설정
request_irq() 반환값 무시 등록 실패 시 NULL 핸들러 → 크래시(Crash) 반환값 검사 후 에러 경로 처리
인터럽트 컨텍스트에서 GFP_KERNEL 슬립 가능 할당 → BUG GFP_ATOMIC 사용
free_irq() / tasklet_kill() 누락 모듈 제거 후 dangling 핸들러 → Use-After-Free cleanup 경로에서 반드시 해제
IRQ 핸들러와 드라이버 제거 사이 경쟁(Race) 핸들러가 이미 해제된 메모리 접근 free_irq()synchronize_irq() 또는 devm_request_irq() 사용
spin_lock()을 IRQ와 공유 데이터에 사용 IRQ가 같은 락 획득 시도 → 데드락 spin_lock_irqsave() 사용

실수 1: 인터럽트 컨텍스트에서 슬립

잘못된 코드: 인터럽트 핸들러에서 kmalloc(GFP_KERNEL)이나 mutex_lock() 등 슬립 가능 함수를 호출하면 BUG: scheduling while atomic이 발생합니다.
/* ❌ 잘못된 패턴: 인터럽트 핸들러에서 슬립 가능 함수 호출 */
static irqreturn_t bad_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    void *buf;

    buf = kmalloc(4096, GFP_KERNEL);  /* 💥 BUG: 슬립 가능! */
    mutex_lock(&dev->lock);            /* 💥 BUG: 슬립 가능! */
    copy_to_user(ubuf, kbuf, len);     /* 💥 BUG: page fault 가능! */
    return IRQ_HANDLED;
}
/* ✅ 올바른 패턴: GFP_ATOMIC 사용, spinlock 사용 */
static irqreturn_t good_handler(int irq, void *dev_id)
{
    struct my_dev *dev = dev_id;
    void *buf;

    buf = kmalloc(4096, GFP_ATOMIC);   /* ✅ 슬립 불가 할당 */
    if (!buf)
        return IRQ_HANDLED;

    spin_lock(&dev->slock);             /* ✅ spinlock (비슬립) */
    /* 최소한의 작업만 수행 */
    spin_unlock(&dev->slock);
    return IRQ_HANDLED;
}

실수 2: IRQ 핸들러에서 disable_irq() 데드락

/* ❌ 잘못된 패턴: IRQ 핸들러 내에서 disable_irq() */
static irqreturn_t bad_handler(int irq, void *dev_id)
{
    disable_irq(irq);  /* 💥 데드락: 자기 자신의 완료를 대기 */
    /* ... */
    enable_irq(irq);
    return IRQ_HANDLED;
}
/* ✅ 올바른 패턴: disable_irq_nosync() 사용 */
static irqreturn_t good_handler(int irq, void *dev_id)
{
    disable_irq_nosync(irq);  /* ✅ 비동기: 즉시 반환 */
    /* 하위 작업 스케줄링 */
    schedule_work(&dev->work);
    return IRQ_HANDLED;
}

/* workqueue에서 enable_irq() 호출 */
static void deferred_work(struct work_struct *work)
{
    /* 프로세스 컨텍스트에서 처리 */
    enable_irq(dev->irq);
}

실수 3: threaded IRQ에서 IRQF_ONESHOT 누락

/* ❌ 잘못된 패턴: IRQF_ONESHOT 없이 threaded IRQ 등록 */
request_threaded_irq(irq, NULL, my_thread_fn,
                     IRQF_TRIGGER_LOW,  /* 💥 ONESHOT 없음 → IRQ 재활성 시점 불명 */
                     "mydev", dev);
/* ✅ 올바른 패턴: IRQF_ONESHOT 설정 */
request_threaded_irq(irq, NULL, my_thread_fn,
                     IRQF_TRIGGER_LOW | IRQF_ONESHOT,  /* ✅ 스레드 완료까지 IRQ 마스크 */
                     "mydev", dev);

실수 4: spin_lock vs spin_lock_irqsave 혼동

/* ❌ 잘못된 패턴: IRQ와 공유 데이터에 일반 spin_lock 사용 */
/* 프로세스 컨텍스트 */
spin_lock(&dev->lock);    /* 여기서 IRQ 발생 시... */
dev->counter++;
spin_unlock(&dev->lock);

/* IRQ 핸들러 */
static irqreturn_t handler(int irq, void *dev_id)
{
    spin_lock(&dev->lock);   /* 💥 데드락: 같은 CPU에서 lock 이미 보유 */
    dev->counter++;
    spin_unlock(&dev->lock);
    return IRQ_HANDLED;
}
/* ✅ 올바른 패턴: 프로세스 컨텍스트에서 irqsave, 핸들러에서 일반 lock */
unsigned long flags;

/* 프로세스 컨텍스트 */
spin_lock_irqsave(&dev->lock, flags);  /* ✅ IRQ 비활성 + lock */
dev->counter++;
spin_unlock_irqrestore(&dev->lock, flags);

/* IRQ 핸들러 (이미 IRQ 비활성 상태) */
static irqreturn_t handler(int irq, void *dev_id)
{
    spin_lock(&dev->lock);   /* ✅ IRQ 컨텍스트에서는 일반 lock 충분 */
    dev->counter++;
    spin_unlock(&dev->lock);
    return IRQ_HANDLED;
}

실수 5: 드라이버 제거 시 경쟁 조건

/* ❌ 잘못된 패턴: 리소스 해제 순서 문제 */
static void bad_remove(struct pci_dev *pdev)
{
    kfree(dev->buffer);     /* 💥 핸들러가 아직 buffer 사용 중일 수 있음 */
    free_irq(dev->irq, dev);
    kfree(dev);
}
/* ✅ 올바른 패턴: IRQ 먼저 해제 → 동기화 → 리소스 해제 */
static void good_remove(struct pci_dev *pdev)
{
    /* 1. 새 인터럽트 수신 중단 */
    free_irq(dev->irq, dev);       /* ✅ 핸들러 완료 대기 포함 */

    /* 2. Bottom Half 완료 대기 */
    cancel_work_sync(&dev->work);  /* ✅ workqueue 완료 대기 */
    tasklet_kill(&dev->tlet);      /* ✅ tasklet 완료 대기 */

    /* 3. 이제 안전하게 리소스 해제 */
    kfree(dev->buffer);
    kfree(dev);
}

실수 6: request_irq() 반환값 미확인

/* ❌ 잘못된 패턴: 반환값 무시 */
request_irq(irq, my_handler, 0, "mydev", dev);
/* 등록 실패해도 계속 진행 → 인터럽트 미처리 */
/* ✅ 올바른 패턴: 에러 처리 */
int ret = request_irq(irq, my_handler, 0, "mydev", dev);
if (ret) {
    dev_err(&pdev->dev, "IRQ %d 등록 실패: %d\n", irq, ret);
    goto err_free;
}
/* 또는 devm_request_irq()로 관리형 사용 */

참고자료

커널 공식 문서

커널 소스 코드

LWN.net 기사

서적

외부 자료

인터럽트 처리와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.