인터럽트 (Interrupts)

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

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

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

핵심 요약

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

단계별 이해

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

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

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

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

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

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

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

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

인터럽트 개요

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

인터럽트 유형

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 스레드 깨우기 */

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;
}

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);   /* 진행 중인 핸들러 완료 대기 */

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 심화 페이지에서 다룹니다.

tasklet

tasklet은 softirq(TASKLET_SOFTIRQ, HI_SOFTIRQ) 위에 구축된 동적 Bottom Half 메커니즘입니다. 같은 tasklet은 절대 병렬 실행되지 않는 직렬화를 보장하지만, 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 패턴, 디버깅, 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);
ℹ️

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만완전 선점 가능
지연 시간최소낮음중간

성능 비교

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

레이턴시 벤치마크

각 메커니즘의 평균/최악 레이턴시를 측정한 결과입니다 (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 시스템에서는 레이턴시 예측성이 향상되지만, 일반 시스템에서는 오버헤드가 있습니다:

워크로드전통적 IRQThreaded IRQ차이권장
네트워크 패킷 처리 (10GbE)850K pps720K pps-15%전통적 IRQ
블록 I/O (NVMe)620K IOPS580K IOPS-6%전통적 IRQ
USB 입력 디바이스95 μs68 μs (jitter)jitter 감소Threaded IRQ
Audio (실시간)최악 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에 고정하면 캐시 지역성이 향상되지만, 부하가 집중될 수 있습니다:

설정평균 처리 시간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,
};

IRQ Domain

IRQ domain은 하드웨어 IRQ 번호를 Linux의 가상 IRQ 번호로 매핑합니다. 여러 인터럽트 컨트롤러가 계층적으로 연결되는 환경을 지원합니다. 상세한 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 토폴로지, 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 격리와 제외

실시간 워크로드나 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 핫플러그자동 재배치수동 재설정 필요
디버깅 용이성동적 변경으로 추적 어려움고정되어 추적 쉬움
적합한 환경범용 서버, 클라우드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)는 인터럽트와 폴링을 혼합하여 효율을 높입니다:

/* 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 캐시 일관성: 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레이턴시 예측성, 우선순위

IPI (Inter-Processor Interrupt)

IPI(Inter-Processor Interrupt)는 SMP 시스템에서 한 CPU가 다른 CPU에게 보내는 특수한 인터럽트입니다. 스케줄러 밸런싱(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) 탐지, 성능 프로파일링, 디버거 진입 등 크리티컬한 용도에 사용됩니다.

상세 문서: 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 정지 인터럽트 마스크 누락, 하드웨어 버그 폴링 모드 전환, 드라이버 패치
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_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로 발전하면서 수천 개의 인터럽트와 가상화를 지원합니다. 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 타이머, 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 주요 레지스터

레지스터오프셋x2APIC MSR설명
APIC ID0x0200x802LAPIC 고유 식별자
TPR (Task Priority)0x0800x808현재 CPU 우선순위 임계값
EOI0x0B00x80BEnd of Interrupt (0 쓰기로 완료)
ISR (In-Service)0x100-0x1700x810-0x817현재 처리 중인 인터럽트 비트맵
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 스레드를 생성합니다. 이 스레드는 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;
}
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 최적화, 부하 분산의 핵심입니다. 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 라인 대신 메모리 쓰기 트랜잭션으로 인터럽트를 발생시키는 메커니즘입니다. 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 */

비활성화 수준 선택 가이드

시나리오권장 API이유
인터럽트 핸들러와 공유 데이터spin_lock_irqsave()인터럽트 비활성 + 스핀락
softirq와 공유 데이터 (프로세스)spin_lock_bh()BH만 비활성 (더 넓은 창)
드라이버 제거 시 IRQ 정리disable_irq() + free_irq()핸들러 완료 보장
인터럽트 핸들러 내부에서 IRQ 제어disable_irq_nosync()데드록 방지 (비동기)
전원 관리 웨이크업 소스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진짜 스핀락 (비선점)
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 전용으로 격리

인터럽트 디버깅 심화

/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 도구와 결합하면 타임라인에서 인터럽트 흐름을 시각적으로 확인할 수 있습니다.

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