인터럽트 (Interrupts)

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

전제 조건: 커널 아키텍처(IDT, 특권 레벨, CPU 동작 모드)를 먼저 읽으세요.
일상 비유: 인터럽트는 집중해서 일하는 중에 울리는 초인종과 같습니다. 초인종(하드웨어 인터럽트)이 울리면 하던 일을 멈추고 문을 확인합니다(Top Half). 택배라면 "일단 현관에 놓기"만 하고(최소 처리) 나중에 정리합니다(Bottom Half). NMI는 화재 경보처럼 절대 무시할 수 없는 인터럽트입니다.

핵심 요약

  • 인터럽트 — 하드웨어가 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는 커널에 정적으로 정의된 Bottom Half 메커니즘입니다. 컴파일 타임에 개수가 고정되며, Per-CPU로 실행됩니다:

Indexsoftirq용도
0HI_SOFTIRQ고우선순위 tasklet
1TIMER_SOFTIRQ타이머 처리
2NET_TX_SOFTIRQ네트워크 송신
3NET_RX_SOFTIRQ네트워크 수신
4BLOCK_SOFTIRQ블록 I/O 완료
5IRQ_POLL_SOFTIRQI/O 폴링
6TASKLET_SOFTIRQ일반 tasklet
7SCHED_SOFTIRQ스케줄러 밸런싱
8HRTIMER_SOFTIRQ고해상도 타이머
9RCU_SOFTIRQRCU 콜백 처리

softirq 실행 흐름

softirq는 하드웨어 인터럽트 핸들러(top half) 종료 직후 irq_exit()에서 실행됩니다. 만약 softirq 처리가 너무 오래 걸리면 ksoftirqd 커널 스레드로 위임됩니다.

/* softirq 실행 경로 */
/*
 * 1. 하드웨어 인터럽트 발생 → top half 실행
 * 2. top half에서 raise_softirq(NET_RX_SOFTIRQ) 등으로 softirq 마킹
 * 3. irq_exit() → __irq_exit_rcu() → invoke_softirq()
 * 4. __do_softirq() 실행:
 *    - pending 비트 확인
 *    - 최대 MAX_SOFTIRQ_RESTART(10)번 루프
 *    - 시간 제한: MAX_SOFTIRQ_TIME(2ms)
 *    - 제한 초과 시 → wakeup_softirqd() 호출
 */

/* ksoftirqd: softirq 전담 커널 스레드 */
/*
 * Per-CPU 스레드: ksoftirqd/0, ksoftirqd/1, ...
 * 다음 경우에 활성화:
 * - __do_softirq()에서 시간/횟수 제한 초과
 * - 인터럽트 비활성 상태에서 softirq가 발생
 * 우선순위: SCHED_NORMAL (nice 0)
 */

/* local_bh_disable/enable: softirq/tasklet 실행 억제 */
local_bh_disable();
/* 이 구간에서는 softirq/tasklet이 실행되지 않음 */
/* 프로세스 컨텍스트에서 softirq와 공유 데이터 보호에 사용 */
local_bh_enable();
/* enable 시점에 pending softirq가 있으면 즉시 실행 */
💡

심화 학습: __do_softirq() 내부 구현, ksoftirqd 생명주기, Per-CPU 동시성 모델, 선점 모드별 동작은 Bottom Half 심화 - Softirq 심화에서 다룹니다.

tasklet

tasklet은 softirq 위에 구축된 동적 Bottom Half 메커니즘입니다. softirq와 달리 런타임에 등록/해제가 가능하며, 같은 tasklet은 동시에 하나의 CPU에서만 실행됩니다.

#include <linux/interrupt.h>

/* 정적 선언 */
DECLARE_TASKLET(my_tasklet, my_tasklet_func);

/* 또는 동적 초기화 (새 API, 커널 5.9+) */
struct tasklet_struct my_tasklet;
tasklet_setup(&my_tasklet, my_tasklet_func);

/* tasklet 핸들러 */
static void my_tasklet_func(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, tasklet);
    /* 인터럽트 컨텍스트: 슬립 불가! */
    process_pending_data(dev);
}

/* top half에서 스케줄 */
tasklet_schedule(&my_tasklet);    /* TASKLET_SOFTIRQ (일반) */
tasklet_hi_schedule(&my_tasklet); /* HI_SOFTIRQ (고우선순위) */

/* 정리 */
tasklet_kill(&my_tasklet);  /* 실행 완료 대기 후 비활성화 */

tasklet 제어 API

/* 스케줄링 API */
tasklet_schedule(&t);           /* TASKLET_SOFTIRQ로 스케줄 */
tasklet_hi_schedule(&t);        /* HI_SOFTIRQ로 스케줄 (최고 우선순위) */

/* 비활성화/활성화 (중첩 가능) */
tasklet_disable(&t);            /* count++, 실행 중이면 완료 대기 */
tasklet_disable_nosync(&t);     /* count++, 완료를 기다리지 않음 */
tasklet_enable(&t);             /* count--, 0이 되면 실행 가능 */

/* 정리 (프로세스 컨텍스트에서만!) */
tasklet_kill(&t);               /* 실행 완료 대기 + 영구 비활성화 */

/*
 * 핵심 특성:
 * - 같은 tasklet은 절대 동시에 두 CPU에서 실행되지 않음
 * - 다른 tasklet은 다른 CPU에서 동시 실행 가능
 * - tasklet_schedule()을 여러 번 호출해도 한 번만 실행
 * - schedule한 CPU에서 실행됨 (Per-CPU 바인딩)
 * - 인터럽트 컨텍스트: mutex, GFP_KERNEL, 슬립 모두 불가
 */

tasklet은 deprecated 추세입니다. 새 코드에서는 workqueue 또는 threaded IRQ를 사용하세요. tasklet은 인터럽트 컨텍스트에서 실행되어 슬립이 불가하고, 직렬화 보장이 제한적입니다. 기존 코드의 tasklet_init()tasklet_setup()으로, tasklet 자체는 workqueue나 threaded IRQ로 전환이 진행 중입니다.

💡

심화 학습: tasklet_struct 내부 필드, 상태 머신 다이어그램, Per-CPU 리스트 구조, tasklet_schedule() 내부 구현, HI_SOFTIRQ vs TASKLET_SOFTIRQ, tasklet_kill() 동작, PREEMPT_RT 호환성, 실제 드라이버 예제, tasklet → workqueue/threaded IRQ 마이그레이션 가이드는 Bottom Half 심화 - Tasklet 심화에서 다룹니다.

workqueue

workqueue는 Bottom Half 작업을 커널 스레드(프로세스 컨텍스트)에서 실행합니다. 슬립이 가능하므로 mutex 획득, 메모리 할당(GFP_KERNEL) 등이 가능합니다.

#include <linux/workqueue.h>

/* Work 정의 */
static void my_work_handler(struct work_struct *work)
{
    struct my_device *dev = container_of(work, struct my_device, work);
    /* Can sleep here! */
    mutex_lock(&dev->lock);
    /* ... process data ... */
    mutex_unlock(&dev->lock);
}

/* 초기화 */
INIT_WORK(&dev->work, my_work_handler);

/* 스케줄 (시스템 workqueue 사용) */
schedule_work(&dev->work);

/* 지연 실행 */
schedule_delayed_work(&dev->dwork, msecs_to_jiffies(100));
💡

새로운 Bottom Half 메커니즘을 선택할 때는 workqueue를 기본으로 사용하세요. tasklet은 deprecated 추세이며, softirq는 새로운 커널 서브시스템 추가 시에만 사용합니다.

💡

심화 학습: CMWQ 아키텍처, worker pool, alloc_workqueue() 플래그, work item 생명주기, 취소/flush 패턴, 디버깅은 Bottom Half 심화 - 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만완전 선점 가능
지연 시간최소낮음중간

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 생성 (인터럽트 컨트롤러 드라이버) */
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 내부 토폴로지 트리 */
NUMA Node 0                      NUMA Node 1
├── Package 0                    ├── Package 1
│   ├── Cache Domain 0           │   ├── Cache Domain 2
│   │   ├── CPU 0                │   │   ├── CPU 4
│   │   └── CPU 1                │   │   └── CPU 5
│   └── Cache Domain 1           │   └── Cache Domain 3
│       ├── CPU 2                │       ├── CPU 6
│       └── CPU 3                │       └── CPU 7

/* 분산 단계:
   1. NUMA 노드 레벨: 디바이스의 NUMA 근접성 기반 배치
   2. 패키지 레벨: 소켓 간 균형
   3. 캐시 도메인 레벨: L2/L3 캐시 공유 그룹 내 분산
   4. CPU 레벨: 개별 코어에 최종 할당 */

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를 확인할 수 있습니다.

IPI (Inter-Processor Interrupt)

IPI(Inter-Processor Interrupt)는 SMP(Symmetric Multi-Processing) 시스템에서 한 CPU가 다른 CPU에게 보내는 특수한 인터럽트입니다. 일반적인 디바이스 인터럽트와 달리 외부 하드웨어가 아닌 CPU 자체가 발생시키며, 커널의 SMP 동작에서 핵심적인 역할을 합니다. 스케줄러 밸런싱, TLB 캐시 일관성, 원격 함수 호출, 타이머 동기화 등 CPU 간 협조가 필요한 거의 모든 작업에 IPI가 관여합니다.

x86 IPI 전송 메커니즘

x86 아키텍처에서 IPI는 Local APIC(Advanced Programmable Interrupt Controller)의 ICR(Interrupt Command Register)을 통해 전송됩니다. 커널은 ICR에 대상 CPU와 벡터 번호를 기록하여 IPI를 발생시킵니다:

x86 IPI 전송 아키텍처 CPU 0 (발신) Local APIC ICR (0xFEE00300) Vector | Dest | Mode System Bus / APIC Bus xAPIC: ICR write → Bus | x2APIC: MSR write → Direct ICR Write CPU 1 (수신) Local APIC IRR / ISR Vector Pending → In-Service ICR (Interrupt Command Register) 구조 — 64비트 Vector [7:0] Delivery [10:8] Dest Mode [11] Level [14] Trigger [15] Shorthand [19:18] Dest [63:32] Delivery Mode (ICR bits [10:8]) 000: Fixed 001: Lowest Priority 010: SMI 100: NMI 101: INIT 110: Start-up (SIPI) Shorthand: 00=No, 01=Self, 10=All Including Self, 11=All Excluding Self
/* arch/x86/kernel/apic/apic.c — IPI 전송의 핵심 */

/* xAPIC 모드: MMIO를 통한 ICR 접근 */
static void __xapic_send_IPI_dest(unsigned int dest, int vector,
                                    unsigned int delivery_mode)
{
    unsigned long cfg;
    cfg = __prepare_ICR(0, vector, delivery_mode);
    __prepare_ICR2(dest);
    /* ICR에 기록하면 APIC가 자동으로 IPI 전송 */
    native_apic_mem_write(APIC_ICR, cfg);  /* 0xFEE00300 */
}

/* x2APIC 모드: MSR을 통한 ICR 접근 (더 빠름) */
static void __x2apic_send_IPI_dest(unsigned int dest, int vector,
                                     unsigned int delivery_mode)
{
    u64 cfg = __prepare_ICR(0, vector, delivery_mode)
              | ((u64)dest << 32);
    /* 단일 MSR 기록으로 IPI 전송 — xAPIC 대비 지연 시간 감소 */
    native_x2apic_icr_write(cfg, 0);  /* MSR 0x830 */
}

/* 커널의 IPI 전송 추상화 */
static inline void apic_send_IPI_allbutself(int vector)
{
    apic->send_IPI_allbutself(vector);
}
static inline void apic_send_IPI_self(int vector)
{
    apic->send_IPI_self(vector);
}

xAPIC vs x2APIC: xAPIC는 MMIO(Memory-Mapped I/O, 0xFEE00000)를 통해 APIC 레지스터에 접근하며, ICR 기록에 두 번의 쓰기가 필요합니다(ICR_HIGH → ICR_LOW). x2APIC는 MSR(Model Specific Register)을 사용하여 단일 WRMSR 명령으로 ICR을 기록할 수 있어 IPI 지연 시간이 크게 줄어듭니다. 최신 시스템에서는 x2APIC가 기본입니다.

리눅스 IPI 벡터 유형

x86 리눅스 커널은 여러 종류의 IPI 벡터를 정의하며, 각각 고유한 목적을 가집니다. 이 벡터들은 arch/x86/include/asm/irq_vectors.h에 정의되어 있습니다:

IPI 벡터벡터 번호용도핸들러
RESCHEDULE_VECTOR 0xFD 대상 CPU의 스케줄러를 깨워 태스크 재배치 유도 sysvec_reschedule_ipi()
CALL_FUNCTION_VECTOR 0xFC 원격 CPU에서 콜백 함수 실행 요청 sysvec_call_function()
CALL_FUNCTION_SINGLE_VECTOR 0xFB 특정 단일 CPU에서 콜백 함수 실행 sysvec_call_function_single()
REBOOT_VECTOR 0xFA 리부트 시 모든 CPU 정지 요청 sysvec_reboot()
IRQ_WORK_VECTOR 0xF6 NMI-safe한 deferred 작업 실행 (printk 등) sysvec_irq_work()
X86_PLATFORM_IPI_VECTOR 0xF7 플랫폼 특화 IPI (UV, Xen 등) sysvec_x86_platform_ipi()
/* arch/x86/include/asm/irq_vectors.h */
#define SPURIOUS_APIC_VECTOR       0xFF
#define ERROR_APIC_VECTOR          0xFE
#define RESCHEDULE_VECTOR          0xFD
#define CALL_FUNCTION_VECTOR       0xFC
#define CALL_FUNCTION_SINGLE_VECTOR 0xFB
#define REBOOT_VECTOR              0xFA
#define IRQ_WORK_VECTOR            0xF6
#define X86_PLATFORM_IPI_VECTOR    0xF7

/* IPI 벡터 범위: 0xF0 ~ 0xFF (시스템 예약)
 * 일반 디바이스 인터럽트 벡터: 0x20 ~ 0xEF
 * 이 분리로 IPI는 디바이스 인터럽트와 충돌하지 않음 */

Reschedule IPI

Reschedule IPI는 커널에서 가장 빈번하게 발생하는 IPI입니다. 한 CPU에서 태스크의 우선순위가 변경되었거나, 로드 밸런서가 태스크를 다른 CPU로 이동시키려 할 때, 대상 CPU의 스케줄러에게 재스케줄링을 요청합니다:

/* kernel/sched/core.c — 리스케줄 IPI 발생 과정 */

/* resched_curr(): 현재 CPU 또는 원격 CPU에 재스케줄 요청 */
void resched_curr(struct rq *rq)
{
    struct task_struct *curr = rq->curr;
    int cpu;

    if (test_tsk_need_resched(curr))
        return;  /* 이미 재스케줄 표시됨 */

    cpu = cpu_of(rq);
    if (cpu == smp_processor_id()) {
        set_tsk_need_resched(curr);
        set_preempt_need_resched();
        return;  /* 로컬 CPU면 TIF_NEED_RESCHED 플래그만 설정 */
    }

    /* 원격 CPU인 경우 IPI로 알림 */
    set_tsk_need_resched(curr);
    smp_send_reschedule(cpu);  /* → RESCHEDULE_VECTOR IPI 전송 */
}

/* arch/x86/kernel/smp.c — Reschedule IPI 수신 핸들러 */
DEFINE_IDTENTRY_SYSVEC(sysvec_reschedule_ipi)
{
    ack_APIC_irq();
    __inc_irq_stat(irq_resched_count);
    trace_reschedule_entry(RESCHEDULE_VECTOR);
    scheduler_ipi();  /* 실제 동작: 별도 작업 없음. TIF_NEED_RESCHED가 */
                      /* 이미 설정되어 있어 인터럽트 복귀 시 schedule() 호출됨 */
    trace_reschedule_exit(RESCHEDULE_VECTOR);
}

/* scheduler_ipi()는 대부분 빈 함수임:
 * 실제 재스케줄은 인터럽트 리턴 경로에서
 * TIF_NEED_RESCHED 플래그를 확인하여 수행됨 */
💡

Reschedule IPI의 최적화: Reschedule IPI는 "빈 메시지"에 가깝습니다. 중요한 것은 대상 CPU에서 인터럽트가 발생했다는 사실 자체이며, 인터럽트 리턴 경로에서 TIF_NEED_RESCHED 플래그를 확인하여 schedule()을 호출합니다. 따라서 IPI 핸들러 자체는 거의 아무 작업도 하지 않습니다.

smp_call_function API 상세

커널에서 다른 CPU에 함수 실행을 요청하는 가장 일반적인 메커니즘입니다. 내부적으로 CALL_FUNCTION_VECTOR 또는 CALL_FUNCTION_SINGLE_VECTOR IPI를 사용합니다:

/* kernel/smp.c — smp_call_function 핵심 구조 */

/* CSD (Call Single Data): 원격 함수 호출의 기본 단위 */
struct __call_single_data {
    struct __call_single_node node;
    smp_call_func_t func;    /* 실행할 함수 포인터 */
    void *info;               /* 함수에 전달할 인자 */
};

/* 특정 CPU 하나에서 함수 실행 */
int smp_call_function_single(int cpu,
                              smp_call_func_t func,
                              void *info,
                              int wait);

/* 현재 CPU를 제외한 모든 온라인 CPU에서 함수 실행 */
void smp_call_function(smp_call_func_t func,
                        void *info,
                        int wait);

/* 현재 CPU 포함 모든 CPU에서 함수 실행 */
void on_each_cpu(smp_call_func_t func,
                  void *info,
                  int wait);

/* 특정 CPU 마스크에 속한 CPU들에서 함수 실행 */
void on_each_cpu_mask(const struct cpumask *mask,
                       smp_call_func_t func,
                       void *info,
                       int wait);

/* 조건부: 각 CPU에서 condition 함수가 true인 경우만 실행 */
void on_each_cpu_cond(smp_cond_func_t cond_func,
                       smp_call_func_t func,
                       void *info,
                       int wait);
/* smp_call_function_single() 내부 동작 흐름 */

int smp_call_function_single(int cpu, smp_call_func_t func,
                              void *info, int wait)
{
    struct __call_single_data csd;
    int this_cpu;

    this_cpu = get_cpu();  /* preemption 비활성화 */

    if (cpu == this_cpu) {
        /* 대상이 현재 CPU면 직접 실행 */
        local_irq_disable();
        func(info);
        local_irq_enable();
    } else {
        /* CSD를 대상 CPU의 call_single_queue에 삽입 */
        csd.func = func;
        csd.info = info;
        __smp_call_single_queue(cpu, &csd.node);
        /* → CALL_FUNCTION_SINGLE_VECTOR IPI 전송 */

        if (wait)
            csd_lock_wait(&csd);  /* 완료될 때까지 spin-wait */
    }

    put_cpu();
    return 0;
}

/* 수신 측: call_single_queue에서 CSD를 꺼내 실행 */
DEFINE_IDTENTRY_SYSVEC(sysvec_call_function_single)
{
    ack_APIC_irq();
    __inc_irq_stat(irq_call_count);
    generic_smp_call_function_single_interrupt();
    /* → per-CPU call_single_queue의 모든 CSD를 순회하며 func(info) 호출 */
}

주의: smp_call_function*()에 전달하는 콜백 함수는 인터럽트 컨텍스트에서 실행됩니다. 따라서 sleep 가능한 함수(kmalloc(GFP_KERNEL), mutex_lock() 등)를 호출하면 안 됩니다. wait=1로 호출하면 원격 CPU의 실행 완료까지 현재 CPU가 spin-wait하므로, 데드락에 주의해야 합니다.

TLB Flush IPI

페이지 테이블이 변경되면 해당 매핑을 사용하는 모든 CPU의 TLB(Translation Lookaside Buffer)를 무효화해야 합니다. 이 과정에서 IPI가 핵심적인 역할을 합니다:

/* arch/x86/mm/tlb.c — TLB flush IPI 흐름 */

/* 1. 페이지 테이블 변경 시 TLB flush 요청 */
void flush_tlb_mm_range(struct mm_struct *mm,
                         unsigned long start, unsigned long end,
                         unsigned int stride_shift, bool freed_tables)
{
    struct flush_tlb_info *info;

    /* 현재 CPU의 TLB 먼저 flush */
    info = get_flush_tlb_info(mm, start, end, stride_shift,
                              freed_tables, 0);

    if (mm == this_cpu_read(cpu_tlbstate.loaded_mm)) {
        flush_tlb_func(info);  /* 로컬 TLB flush */
    }

    /* mm을 사용 중인 다른 CPU들에게 IPI 전송 */
    if (cpumask_any_but(mm_cpumask(mm),
                        smp_processor_id()) < nr_cpu_ids) {
        flush_tlb_multi(mm_cpumask(mm), info);
        /* → smp_call_function_many_cond()로 IPI 전송 */
    }

    put_flush_tlb_info();
}

/* 2. 수신 측: IPI를 받은 CPU에서 TLB 무효화 수행 */
static void flush_tlb_func(void *info)
{
    struct flush_tlb_info *f = info;
    struct mm_struct *loaded_mm = this_cpu_read(cpu_tlbstate.loaded_mm);

    if (loaded_mm != f->mm) {
        return;  /* 이 CPU에서 해당 mm을 사용하지 않으면 무시 */
    }

    if (f->end == TLB_FLUSH_ALL) {
        /* 전체 TLB flush — CR3 reload */
        count_vm_tlb_event(NR_TLB_LOCAL_FLUSH_ALL);
        __flush_tlb_all();
    } else {
        /* 범위 지정 TLB flush — INVLPG 사용 */
        unsigned long addr;
        for (addr = f->start; addr < f->end;
             addr += 1UL << f->stride_shift) {
            __flush_tlb_one_user(addr);  /* INVLPG */
        }
    }
}
TLB Flush IPI 시퀀스 CPU 0 (발신) CPU 1 CPU 2 PTE 변경 (unmap) 로컬 TLB flush CALL_FUNCTION IPI flush_tlb_func() flush_tlb_func() INVLPG addr INVLPG addr 완료 ACK 실행 재개

TLB Flush 최적화: 커널은 불필요한 IPI를 줄이기 위해 여러 최적화를 적용합니다. mm_cpumask()를 통해 해당 메모리 매핑을 실제로 사용 중인 CPU만 대상으로 하며, flush할 페이지 수가 많으면 전체 TLB flush(CR3 reload)로 전환합니다. 또한 lazy TLB 모드에서는 커널 스레드처럼 유저 매핑이 불필요한 경우 TLB flush를 지연시킬 수 있습니다.

IRQ Work IPI

IRQ Work는 NMI나 하드 인터럽트 컨텍스트처럼 일반적인 작업 큐를 사용할 수 없는 상황에서도 안전하게 deferred 작업을 예약할 수 있는 메커니즘입니다. 대표적인 사용처는 NMI 컨텍스트에서의 printk() 출력, perf 이벤트 처리, RCU 콜백 가속 등입니다:

/* kernel/irq_work.c — IRQ Work 핵심 구조 */

struct irq_work {
    struct __call_single_node node;
    void (*func)(struct irq_work *);
    /* flags: IRQ_WORK_LAZY — 다음 tick까지 지연 가능
     *        IRQ_WORK_HARD_IRQ — hard IRQ 컨텍스트에서 실행
     *        IRQ_WORK_PENDING — 큐에 대기 중 */
};

/* IRQ Work 예약 — NMI 컨텍스트에서도 안전 */
bool irq_work_queue(struct irq_work *work)
{
    if (!irq_work_claim(work))
        return false;  /* 이미 큐에 있음 */

    __irq_work_queue_local(work);
    /* 현재 CPU의 per-CPU raised_list 또는 lazy_list에 삽입 */

    if (!(work->node.type & IRQ_WORK_LAZY))
        arch_irq_work_raise();  /* IRQ_WORK_VECTOR self-IPI 전송 */

    return true;
}

/* 원격 CPU에 IRQ Work 예약 */
bool irq_work_queue_on(struct irq_work *work, int cpu)
{
    if (!irq_work_claim(work))
        return false;
    /* smp_call_function_single_async()를 사용하여
     * 대상 CPU에 IRQ_WORK_VECTOR IPI 전송 */
    __smp_call_single_queue(cpu, &work->node);
    return true;
}

/* 사용 예: NMI 컨텍스트에서 printk 트리거 */
static struct irq_work printk_wake_work;

void printk_trigger_flush(void)
{
    /* NMI에서 직접 콘솔 출력 불가 → IRQ Work로 지연 */
    irq_work_queue(&printk_wake_work);
}

SMP 부팅 시 IPI 활용

멀티코어 시스템의 부팅 과정에서 BSP(Bootstrap Processor)가 AP(Application Processor)를 깨울 때 특수한 IPI 시퀀스를 사용합니다:

/* arch/x86/kernel/smpboot.c — AP 초기화 IPI 시퀀스 */

/* Intel MP 사양에 따른 INIT-SIPI-SIPI 시퀀스 */
static int wakeup_secondary_cpu_via_init(int phys_apicid,
                                          unsigned long start_eip)
{
    /* 1단계: INIT IPI — AP를 리셋 상태로 전환 */
    apic_icr_write(APIC_INT_LEVELTRIG | APIC_INT_ASSERT |
                    APIC_DM_INIT, phys_apicid);
    udelay(200);  /* 200μs 대기 */

    /* INIT de-assert */
    apic_icr_write(APIC_INT_LEVELTRIG | APIC_DM_INIT,
                    phys_apicid);
    udelay(10000);  /* 10ms 대기 (Intel 사양 요구) */

    /* 2단계: SIPI (Startup IPI) × 2회
     * start_eip: AP가 리얼모드에서 시작할 물리 주소
     * 4KB 정렬된 주소의 상위 8비트를 벡터로 인코딩 */
    for (int j = 1; j <= 2; j++) {
        apic_icr_write(APIC_DM_STARTUP |
                        (start_eip >> 12), phys_apicid);
        udelay(300);  /* 300μs 대기 */
    }

    /* AP는 start_eip(trampoline 코드)에서 리얼모드로 시작 →
     * 보호모드 → 롱모드 전환 후 start_secondary()로 진입 */
    return 0;
}

/* AP 시작 후 호출되는 진입점 */
static void start_secondary(void *unused)
{
    cpu_init();           /* GDT, IDT, TSS 초기화 */
    x86_cpuinit.setup_percpu_clockev();
    apic_ap_setup();      /* Local APIC 설정 */
    set_cpu_online(smp_processor_id(), true);
    cpu_startup_entry(CPUHP_AP_ONLINE_IDLE);  /* idle 루프 진입 */
}
INIT-SIPI-SIPI AP 부팅 시퀀스 BSP AP INIT IPI (Assert + De-assert) CPU Reset 상태 10ms 대기 SIPI #1 (vector = start_eip >> 12) Real mode 시작 300μs 대기 SIPI #2 (안전을 위한 재전송) Real → Protected → Long mode start_secondary() set_cpu_online(true)

IPI 성능 특성과 최적화

IPI는 CPU 간 통신의 기본이지만 상당한 오버헤드를 수반합니다. IPI 하나의 왕복 비용은 수백 나노초에서 수 마이크로초에 이르며, 대규모 SMP 시스템에서는 IPI storm이 심각한 성능 병목이 될 수 있습니다:

항목xAPICx2APIC비고
IPI 전송 지연 ~150–300ns ~50–100ns ICR 기록 시간
IPI 수신 지연 ~200–500ns ~150–400ns 벡터 전달 + 핸들러 진입
전체 왕복 (round-trip) ~1–5μs ~0.5–2μs 전송 → 실행 → ACK
같은 다이 CPU간 ~0.5–1μs L3 캐시 공유 시 더 빠름
다른 소켓 CPU간 ~2–5μs NUMA 인터커넥트 경유
/* 커널의 IPI 최적화 기법들 */

/* 1. Batch TLB flush: 여러 페이지 변경을 모아서 한 번에 IPI 전송 */
tlb_gather_mmu(&tlb, mm);
/* ... 여러 PTE 변경 ... */
tlb_finish_mmu(&tlb);  /* 모든 변경 후 한 번만 IPI */

/* 2. Lazy TLB: 커널 스레드에서는 유저 TLB flush 생략 */
/* mm_cpumask에서 커널 스레드만 실행 중인 CPU는 제외 */

/* 3. PCID (Process Context Identifiers): CR3 변경 시
 * 전체 TLB flush 대신 PCID별 무효화로 범위 축소 */

/* 4. Reschedule IPI 회피: idle CPU에 IPI 대신
 * ttwu_queue_wakelist()로 원격 wakeup 큐 사용 */
if (cpus_share_cache(smp_processor_id(), cpu) ||
    !cpu_is_idle(cpu)) {
    /* 같은 LLC 또는 실행 중 → 직접 enqueue + IPI */
} else {
    /* 다른 LLC의 idle CPU → wakelist에 추가, IPI 최소화 */
    __ttwu_queue_wakelist(p, cpu, wake_flags);
}

/* 5. Multi-target IPI: 가능한 경우 broadcast shorthand 사용
 * ICR shorthand=11 (All Excluding Self)로 단일 기록으로
 * 모든 CPU에 동시 IPI → CPU 수에 무관한 고정 비용 */

IPI Storm 주의: 대규모 NUMA 시스템(수백 코어)에서 빈번한 전체 TLB flush나 smp_call_function() 호출은 IPI storm을 유발할 수 있습니다. 모든 CPU가 IPI 처리에 시간을 소모하면 실제 작업 처리량이 급격히 감소합니다. perf stat -e irq_vectors:call_function_entry로 IPI 빈도를 모니터링하고, on_each_cpu_cond()이나 cpumask 기반 API로 대상 CPU를 최소화하세요.

IPI 모니터링과 디버깅

IPI 관련 성능 문제를 진단하기 위한 도구와 방법입니다:

# /proc/interrupts에서 IPI 통계 확인
# RES: Reschedule IPI, CAL: Call Function IPI,
# TLB: TLB shootdown IPI
grep -E "RES|CAL|TLB|IRQ_WORK" /proc/interrupts
#            CPU0       CPU1       CPU2       CPU3
# RES:    1245832    1189244    1023444    1145678  Rescheduling interrupts
# CAL:     432109     398234     445312     412890  Function call interrupts
# TLB:     234567     198432     245123     213456  TLB shootdowns

# 일정 간격으로 IPI 증가량 모니터링
watch -n1 "grep -E 'RES|CAL|TLB' /proc/interrupts"

# perf로 IPI 이벤트 트레이싱
perf stat -e 'irq_vectors:reschedule_entry' \
          -e 'irq_vectors:call_function_entry' \
          -e 'irq_vectors:call_function_single_entry' \
          -a -- sleep 10

# ftrace로 IPI 발생 원인 추적
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_raise/enable
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_exit/enable
cat /sys/kernel/debug/tracing/trace_pipe
# <...>-1234 [000] smp_call_function_single: target=2 callback=flush_tlb_func
/* 커널 내부에서 IPI 디버깅 */

/* per-CPU IPI 통계 카운터 (arch/x86/kernel/irq.c) */
__inc_irq_stat(irq_resched_count);     /* RES 카운터 */
__inc_irq_stat(irq_call_count);        /* CAL 카운터 */
__inc_irq_stat(irq_tlb_count);         /* TLB 카운터 */

/* CSD lock 디버깅 — smp_call_function이 너무 오래 걸릴 때 */
/* CONFIG_CSD_LOCK_WAIT_DEBUG 활성화 시
 * CSD lock 대기가 임계값 초과하면 경고 출력 */
#ifdef CONFIG_CSD_LOCK_WAIT_DEBUG
/* 기본 임계값: 약 5초 (sysctl csd_lock_timeout) */
/* 대상 CPU가 응답하지 않으면 backtrace 덤프 */
csd_lock_wait(csd);  /* 타임아웃 시 WARNING + 대상 CPU 스택 출력 */
#endif

/* /proc/interrupts 출력 구조 */
/*
 * IPI 항목별 의미:
 * RES — Reschedule IPI: 스케줄러가 태스크 이동 시 발생
 *   높은 값 → 빈번한 태스크 마이그레이션 (NUMA 밸런싱 확인)
 *
 * CAL — Call Function IPI: smp_call_function* 호출
 *   높은 값 → 빈번한 원격 함수 호출 (TLB flush 포함)
 *
 * TLB — TLB Shootdowns: 메모리 매핑 변경으로 인한 TLB 무효화
 *   높은 값 → mmap/munmap/mprotect 빈번, 공유 메모리 워크로드
 */
💡

IPI 성능 튜닝 체크리스트:

  • /proc/interrupts의 RES/CAL/TLB 카운터를 주기적으로 모니터링
  • TLB shootdown이 과다하면 CONFIG_X86_PCID(PCID)와 huge page 사용을 검토
  • Reschedule IPI가 과다하면 sched_migration_cost_ns 튜닝으로 태스크 마이그레이션 빈도 조절
  • NUMA 시스템에서는 numactl --cpubind로 프로세스를 특정 노드에 바인딩하여 cross-socket IPI 최소화
  • CONFIG_CSD_LOCK_WAIT_DEBUG=y로 느린 IPI 응답 감지 활성화

NMI (Non-Maskable Interrupt)

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

NMI 발생 원인

소스설명커널 설정
하드웨어 오류메모리 패리티 에러, 버스 에러, I/O 채널 체크BIOS/펌웨어 설정
Watchdog (hardlockup)PMU(Performance Monitoring Unit) 카운터 오버플로로 발생. CPU가 인터럽트 비활성 상태에서 일정 시간 이상 멈추면 감지CONFIG_HARDLOCKUP_DETECTOR
Performance Monitorperf 프로파일링에서 PMU 카운터 오버플로 시 NMI로 샘플 수집CONFIG_PERF_EVENTS
외부 NMI 버튼서버 하드웨어의 물리적 NMI 버튼 (디버깅 목적)
IOAPIC WatchdogI/O APIC를 통한 NMI 전달 (레거시 시스템)nmi_watchdog=1
GHES (ACPI)ACPI의 Generic Hardware Error Source를 통한 하드웨어 에러 보고CONFIG_ACPI_APEI_GHES
MCE (Machine Check)심각한 하드웨어 오류 시 MCE를 NMI로 전달 (일부 아키텍처)CONFIG_X86_MCE

x86 NMI 아키텍처

x86 NMI 처리 흐름 PMU Overflow HW Error (MCE) External NMI Local APIC NMI pin / LVT IDT Vector #2 asm_exc_nmi NMI# exc_nmi() default_do_nmi() nmi_handle() perf_event_nmi watchdog_nmi kgdb / panic NMI는 EFLAGS.IF=0 상태에서도 CPU에 전달됨 (마스킹 불가)
x86 아키텍처에서의 NMI 전달 및 처리 경로

x86에서 NMI는 IDT(Interrupt Descriptor Table)의 벡터 2번에 고정되어 있습니다. CPU가 NMI를 수신하면 현재 실행 컨텍스트와 무관하게 즉시 NMI 핸들러로 진입하며, NMI 처리 중에는 동일 CPU에서 중첩 NMI가 차단됩니다(CPU 하드웨어 메커니즘).

NMI 처리 코드 흐름

/* arch/x86/kernel/nmi.c — NMI 핸들러 등록 */
struct nmiaction {
    struct list_head  list;
    nmi_handler_t     handler;     /* 핸들러 콜백 */
    u64               max_duration;/* 최대 실행 시간 (ns) */
    unsigned long     flags;
    const char        *name;
};

/* NMI 핸들러 등록/해제 */
int register_nmi_handler(unsigned int type,
    nmi_handler_t handler, unsigned long flags,
    const char *name);
void unregister_nmi_handler(unsigned int type,
    const char *name);

/* NMI 타입 */
/* NMI_LOCAL      - 이 CPU에만 해당하는 NMI (PMU, watchdog) */
/* NMI_UNKNOWN    - 원인 불명 NMI */
/* NMI_SERR       - 시스템 에러 (PCI SERR#) */
/* NMI_IO_CHECK   - I/O 채널 체크 에러 */
/* NMI 진입점 (간략화) — arch/x86/kernel/nmi.c */
void exc_nmi(struct pt_regs *regs)
{
    /* IST(Interrupt Stack Table) 또는 전용 NMI 스택 사용 */
    nmi_enter();  /* RCU, context tracking 업데이트 */

    default_do_nmi(regs);

    nmi_exit();
}

static void default_do_nmi(struct pt_regs *regs)
{
    /* 1단계: CPU-local NMI 핸들러 체인 순회 */
    if (nmi_handle(NMI_LOCAL, regs))
        return;

    /* 2단계: I/O 체크 에러 */
    if (reason & NMI_REASON_IOCHK) {
        io_check_error(reason, regs);
        return;
    }

    /* 3단계: unknown NMI */
    unknown_nmi_error(reason, regs);
    /* unknown_nmi_panic=1 이면 여기서 panic() */
}

NMI Watchdog (Hardlockup Detector)

NMI watchdog은 CPU가 인터럽트 비활성 상태에서 장시간 멈추는 hardlockup을 감지합니다. 일반 인터럽트가 차단된 상태에서도 NMI는 전달되므로, 교착 상태에 빠진 CPU를 감지할 수 있는 유일한 메커니즘입니다.

Hardlockup Detection 메커니즘 hrtimer (Per-CPU) watchdog_timer_fn() hrtimer_interrupts++ 매 watchdog_thresh 초 PMU Counter NMI perf event overflow watchdog_overflow hrtimer_interrupts 변화 확인 카운터 변화? (비교) 정상 Yes HARDLOCKUP! panic / backtrace No hrtimer는 일반 인터럽트로 동작 → hardlockup 시 카운터 증가 불가 → NMI에서 감지
NMI watchdog의 hardlockup 탐지 원리: hrtimer(일반 인터럽트)가 멈추면 NMI에서 이를 감지
/* kernel/watchdog.c — NMI watchdog 동작 원리 */

/*
 * 1. Per-CPU hrtimer가 watchdog_thresh(기본 10초) 간격으로 실행
 *    → hrtimer_interrupts 카운터 증가
 *
 * 2. Per-CPU PMU 카운터가 주기적으로 NMI 발생
 *    → watchdog_overflow_callback() 호출
 *    → hrtimer_interrupts 변화 여부 확인
 *
 * 3. hrtimer가 실행되지 못함 = 인터럽트가 비활성 = hardlockup!
 */

/* hardlockup 감지 시 호출되는 콜백 */
static void watchdog_overflow_callback(struct perf_event *event,
    struct perf_sample_data *data, struct pt_regs *regs)
{
    if (is_hardlockup()) {
        /* hrtimer_interrupts 카운터가 변하지 않음 → hardlockup 확정 */
        pr_emerg("Watchdog detected hard LOCKUP on cpu %d\n", cpu);
        show_regs(regs);     /* 레지스터 덤프 */

        if (hardlockup_panic)
            nmi_panic(regs, "Hard LOCKUP");
    }
}
# NMI watchdog 설정
# 활성화/비활성화
echo 1 > /proc/sys/kernel/nmi_watchdog   # 활성화
echo 0 > /proc/sys/kernel/nmi_watchdog   # 비활성화 (가상화 환경에서 권장)

# watchdog 임계값 (기본 10초)
echo 30 > /proc/sys/kernel/watchdog_thresh

# hardlockup 시 panic 여부 (기본 0 = backtrace만)
echo 1 > /proc/sys/kernel/hardlockup_panic

# 커널 파라미터로 설정
# nmi_watchdog=0         NMI watchdog 비활성화
# nmi_watchdog=1         I/O APIC 기반 watchdog (레거시)
# nmi_watchdog=2         Local APIC 기반 (perf PMU)
# hardlockup_panic=1    hardlockup 시 panic

# watchdog 상태 확인
cat /proc/sys/kernel/nmi_watchdog
dmesg | grep -i "nmi watchdog"

가상화 환경 주의: VM에서는 vCPU가 호스트에 의해 스케줄링되므로 NMI watchdog이 false positive를 발생시킬 수 있습니다. KVM, VMware, Hyper-V 등의 가상화 환경에서는 nmi_watchdog=0으로 비활성화를 권장합니다. 또한 PMU 가상화가 지원되지 않는 환경에서는 자동으로 비활성화됩니다.

NMI 컨텍스트 제약사항

NMI 핸들러는 어떤 컨텍스트에서든 비동기적으로 진입할 수 있으므로, 일반 인터럽트 핸들러보다 훨씬 엄격한 제약이 적용됩니다:

동작NMI 컨텍스트이유
spinlock (일반)사용 불가NMI가 lock holder를 선점하면 self-deadlock
raw_spin_lock사용 불가 (동일)NMI 중에도 동일 CPU의 lock holder를 선점 가능
nmi_spin_lock 패턴arch_spin_trylocktrylock으로만 시도, 실패 시 즉시 포기
메모리 할당사용 불가할당자 내부 lock 보유 중일 수 있음
printk()제한적 사용 가능NMI-safe 버퍼(nmi_print_seq)에 기록 후 나중에 출력
trace_*NMI-safe 버전만perf_swevent_put 등 NMI-safe tracing API 사용
RCU read제한적 가능nmi_enter()가 RCU watching 상태를 보장
panic()nmi_panic() 사용일반 panic()은 NMI 컨텍스트에서 교착 가능
/* NMI-safe 핸들러 작성 패턴 */
static int my_nmi_handler(unsigned int type,
    struct pt_regs *regs)
{
    /* !! 절대 금지: mutex, semaphore, kmalloc, schedule !! */

    /* Per-CPU 변수는 안전 (NMI는 같은 CPU에서 중첩되지 않음) */
    this_cpu_inc(nmi_counter);

    /* atomic 연산은 안전 */
    atomic_inc(&global_nmi_count);

    /* trylock 패턴: 실패 시 포기 */
    if (raw_spin_trylock(&my_lock)) {
        /* ... critical section ... */
        raw_spin_unlock(&my_lock);
    }

    /* NMI-safe 출력 */
    nmi_panic(regs, "Fatal error detected");

    return NMI_HANDLED;  /* 또는 NMI_DONE */
}

/* 등록 예시 */
register_nmi_handler(NMI_LOCAL, my_nmi_handler,
    0, "my_nmi");

NMI 중첩과 IST

x86_64에서 NMI는 IST(Interrupt Stack Table)를 사용하여 전용 스택에서 실행됩니다. 이는 스택 오버플로 상태에서도 NMI가 안전하게 처리되도록 보장합니다.

/* arch/x86/include/asm/page_64_types.h */
/*
 * IST 스택 할당 (x86_64):
 * IST 1 — #DF (Double Fault)
 * IST 2 — NMI
 * IST 3 — #DB (Debug)
 * IST 4 — #MC (Machine Check)
 *
 * 각 Per-CPU, 크기는 보통 4KB~8KB
 */

/* NMI 중첩 문제:
 *
 * CPU는 NMI 처리 중 추가 NMI를 하드웨어적으로 차단하지만,
 * IRET 실행 시 이 차단이 해제됩니다.
 *
 * 문제 시나리오:
 * 1. NMI 진입 → IST2 스택 사용
 * 2. 인터럽트/예외 발생 → IRET으로 복귀
 * 3. IRET이 NMI 차단 해제 → 새 NMI 진입 가능
 * 4. 새 NMI도 IST2 스택 사용 → 이전 NMI 스택 덮어쓰기!
 *
 * Linux 해결: NMI 진입 직후 전용 스택으로 전환,
 * IST 스택은 트램폴린으로만 사용
 */

Unknown NMI 처리

등록된 핸들러가 처리하지 못한 NMI는 unknown_nmi_error()로 전달됩니다. 이는 하드웨어 문제를 나타낼 수 있습니다:

# Unknown NMI 발생 시 panic 설정
echo 1 > /proc/sys/kernel/unknown_nmi_panic

# 또는 커널 파라미터
# unknown_nmi_panic=1

# dmesg에서 NMI 관련 로그 확인
dmesg | grep -i nmi
# [    0.000000] NMI watchdog: Perf NMI watchdog permanently disabled
# [  123.456789] Uhhuh. NMI received for unknown reason 2d on CPU 0.
# [  123.456790] Dazed and confused, but trying to continue

# /proc/interrupts에서 NMI 카운트 확인
grep NMI /proc/interrupts
# NMI:       1234       5678       9012       3456   Non-maskable interrupts
# LOC:     123456     234567     345678     456789   Local timer interrupts

서버 환경에서의 NMI: 서버 BIOS/BMC에서 NMI 버튼 또는 IPMI 명령(ipmitool chassis power diag)으로 NMI를 수동 발생시킬 수 있습니다. 시스템이 응답하지 않을 때 unknown_nmi_panic=1과 함께 사용하면 crash dump를 생성하여 kdump로 문제를 진단할 수 있습니다.

NMI 기반 성능 프로파일링

perf는 PMU 카운터 오버플로 시 NMI를 사용하여 CPU 샘플을 수집합니다. 인터럽트 비활성 구간에서도 샘플링이 가능하므로, 일반 인터럽트 기반 프로파일링보다 정확한 결과를 제공합니다.

# NMI 기반 perf 프로파일링 (cycles 이벤트 = PMU NMI)
perf record -e cycles -g -a sleep 10

# NMI를 사용하는지 확인 (/proc/interrupts의 NMI 카운트 증가)
watch -n 1 'grep NMI /proc/interrupts'

# perf의 NMI 사용량 확인
perf stat -e 'cycles:u,cycles:k' -a sleep 5
/* perf의 NMI 핸들러 등록 (간략화) */
/* arch/x86/events/core.c */

static int perf_event_nmi_handler(unsigned int cmd,
    struct pt_regs *regs)
{
    /* PMU 카운터 오버플로 확인 */
    if (!x86_pmu_handle_irq(regs))
        return NMI_DONE;

    /* 샘플 기록: IP(Instruction Pointer), callchain 등 */
    /* perf_event_overflow() → perf_event_output() */
    return NMI_HANDLED;
}

/* NMI에서 안전하게 callchain을 수집하기 위해
 * frame pointer 또는 ORC unwinder 사용 */
💡

NMI vs. Timer 프로파일링: perf record -e cycles(NMI 기반)는 perf record -e cpu-clock(타이머 기반)보다 정밀합니다. 타이머 기반은 local_irq_disable() 구간을 관측할 수 없지만, 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