APIC (Advanced Programmable Interrupt Controller)

x86 시스템의 인터럽트(Interrupt) 라우팅(Routing) 핵심인 APIC 아키텍처를 종합적으로 다룹니다. 8259A PIC에서 APIC으로의 진화, LAPIC 내부 레지스터(Register), I/O APIC 리다이렉션 테이블, x2APIC MSR 인터페이스, APIC Timer, MSI/MSI-X, Intel APICv와 AMD AVIC 가상화(Virtualization) 지원, 커널 struct apic 서브시스템까지 전체 경로를 심층 분석합니다.

전제 조건: 인터럽트(Interrupt)x86 아키텍처 문서를 먼저 읽으세요. APIC은 인터럽트 전달의 핵심이므로 IDT, IRQ 개념을 이해해야 합니다.
일상 비유: APIC은 대형 회사의 전화 교환 시스템과 비슷합니다. 외부 전화(I/O APIC)가 교환대를 거쳐 적절한 부서(CPU)의 내선(LAPIC)으로 연결됩니다. x2APIC은 디지털 교환기로 업그레이드한 것이고, MSI는 직통 번호로 교환대를 거치지 않는 방식입니다.

핵심 요약

  • APIC — x86의 인터럽트 컨트롤러로, 각 CPU마다 LAPIC(Local APIC)이 내장되고, 외부 장치는 I/O APIC을 통해 인터럽트를 전달합니다.
  • xAPIC / x2APIC — xAPIC은 MMIO(0xFEE00000), x2APIC은 MSR로 레지스터에 접근하며, x2APIC은 32비트 APIC ID와 단일 MSR ICR 쓰기를 지원합니다.
  • MSI/MSI-X — I/O APIC을 우회하여 PCIe 장치가 메모리 쓰기로 직접 LAPIC에 인터럽트를 전달하는 현대적 방식입니다.
  • APICv / AVIC — Intel APICv와 AMD AVIC은 가상 머신의 인터럽트 처리를 하드웨어로 가속하여 VM Exit 오버헤드를 제거합니다.
  • struct apic — 커널은 이 드라이버 모델로 다양한 APIC 모드(flat, cluster, x2apic 등)를 추상화합니다.

단계별 이해

  1. 8259A에서 APIC으로의 진화 — 왜 APIC이 필요한지, 단일 프로세서용 PIC의 한계를 이해합니다.

    멀티프로세서 환경에서 인터럽트를 특정 CPU로 라우팅하려면 PIC으로는 불가능합니다.

  2. APIC 시스템 아키텍처 — 전체 그림: LAPIC, I/O APIC, 시스템 버스의 관계를 파악합니다.

    각 CPU에 내장된 LAPIC과 칩셋의 I/O APIC이 시스템 버스로 연결되는 구조를 학습합니다.

  3. LAPIC 내부 구조 — 핵심 레지스터(IRR, ISR, TPR, LVT)의 역할을 학습합니다.

    인터럽트 수신(IRR), 처리 중(ISR), 우선순위(TPR), 로컬 벡터 테이블(LVT)을 이해합니다.

  4. I/O APIC과 인터럽트 전달 흐름 — 외부 장치 인터럽트가 CPU에 도달하는 전체 경로를 추적합니다.

    Device에서 I/O APIC, LAPIC, IDT까지의 전체 과정을 단계별로 이해합니다.

  5. x2APIC과 MSI/MSI-X — 현대 시스템의 고속 인터페이스를 학습합니다.

    MSR 기반 x2APIC과 I/O APIC을 우회하는 MSI 메커니즘을 이해합니다.

  6. 가상화 지원 — APICv/AVIC 등은 필요에 따라 학습합니다.

    APIC Timer, APICv, AVIC 등 고급 주제는 기본 이해 후 진행합니다.

8259A → APIC 진화

초기 IBM PC/AT는 Intel 8259A PIC(Programmable Interrupt Controller) 2개를 캐스케이드하여 최대 15개의 IRQ 라인을 지원했습니다. 이 구조는 단일 프로세서 시스템에서는 충분했지만, 멀티프로세서 시스템에서는 근본적인 한계가 있었습니다.

특성8259A PICAPIC
IRQ 라인 수 15개 (마스터 8 + 슬레이브 7) I/O APIC당 24+, MSI로 사실상 무제한
CPU 지원 단일 CPU만 멀티프로세서 (APIC ID로 라우팅)
IPI 지원 불가 ICR을 통한 프로세서 간 인터럽트
우선순위(Priority) 고정 (IRQ 0 최고) 벡터 기반 프로그래머블 우선순위
인터페이스 I/O 포트 (0x20, 0xA0) MMIO(xAPIC) 또는 MSR(x2APIC)
타이머(Timer) 별도 8254 PIT 필요 LAPIC 내장 타이머 (TSC-Deadline 지원)
MSI 지원 불가 메모리 쓰기 기반 MSI/MSI-X

8259A PIC 구조

Master 8259A (I/O 0x20–0x21) Slave 8259A (I/O 0xA0–0xA1) CPU INTR Pin IRQ 0 (Timer) IRQ 1 (Kbd) IRQ 2 (Cascade) IRQ 3 (COM2) IRQ 4 (COM1) IRQ 5 IRQ 6 (FDC) IRQ 7 (LPT1) INTR CASCADE IRQ 8 (RTC) IRQ 9 IRQ 10 IRQ 11 IRQ 12 (PS/2) IRQ 13 (FPU) IRQ 14 (IDE0) IRQ 15 (IDE1)

8259A 초기화 코드 (커널 레거시 경로)

/* arch/x86/kernel/i8259.c - 8259A PIC 초기화 */
static void init_8259A(int auto_eoi)
{
    /* ICW1: 초기화 시작, ICW4 필요 */
    outb(0x11, PIC_MASTER_CMD);
    outb(0x11, PIC_SLAVE_CMD);

    /* ICW2: 벡터 오프셋 (IRQ 0→벡터 0x20, IRQ 8→벡터 0x28) */
    outb(0x20, PIC_MASTER_IMR);   /* 마스터: 벡터 32~39 */
    outb(0x28, PIC_SLAVE_IMR);    /* 슬레이브: 벡터 40~47 */

    /* ICW3: 캐스케이드 설정 */
    outb(0x04, PIC_MASTER_IMR);   /* 마스터 IRQ2에 슬레이브 */
    outb(0x02, PIC_SLAVE_IMR);    /* 슬레이브 ID = 2 */

    /* ICW4: 8086 모드, auto-EOI 옵션 */
    if (auto_eoi)
        outb(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR);
    else
        outb(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR);
    outb(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR);

    /* 모든 IRQ 마스크 (APIC 전환 전) */
    outb(0xff, PIC_MASTER_IMR);
    outb(0xff, PIC_SLAVE_IMR);
}
8259A PIC → APIC 진화 8259A PIC 15 IRQ, Single CPU I/O Port 0x20/0xA0 고정 우선순위 No IPI, No MSI i386 ~ Pentium 진화 xAPIC LAPIC + I/O APIC MMIO 0xFEE00000 8-bit APIC ID (256 CPU) IPI, MSI, Timer P6 ~ Core2 확장 x2APIC MSR 기반 접근 32-bit APIC ID (4G CPU) 64-bit ICR (단일 쓰기) Cluster 모드 Nehalem ~ MSI/MSI-X I/O APIC 우회 메모리 쓰기 → LAPIC 직접 APICv / AVIC 가상화 하드웨어 가속 VM Exit 최소화

APIC 전환 과정 (부트 시퀀스)

/* arch/x86/kernel/apic/apic.c - APIC 초기 설정 흐름
 *
 * 부트 순서:
 * 1. BIOS가 8259A PIC 모드로 시작
 * 2. 커널이 APIC 하드웨어 감지 (CPUID)
 * 3. 8259A를 마스크하고 APIC으로 전환
 * 4. x2APIC 지원 시 MSR 모드로 업그레이드
 */

void __init init_bsp_APIC(void)
{
    unsigned int value;

    /* APIC이 비활성화 상태면 건너뜀 */
    if (!boot_cpu_has(X86_FEATURE_APIC))
        return;

    /* APIC 기본 주소 확인 (MSR 0x1B) */
    value = apic_read(APIC_SPIV);

    /* Spurious Vector Register: APIC 활성화 + 벡터 0xFF */
    value &= ~APIC_VECTOR_MASK;
    value |= APIC_SPIV_APIC_ENABLED;
    value |= APIC_SPIV_FOCUS_DISABLED;
    value |= SPURIOUS_APIC_VECTOR;    /* 벡터 0xFF */

    apic_write(APIC_SPIV, value);
}

APIC 감지 및 모드 결정

/* CPUID를 통한 APIC 기능 감지 */
static bool detect_apic_mode(void)
{
    u32 eax, ebx, ecx, edx;

    /* CPUID.01H: EDX[9] = APIC 존재 여부 */
    cpuid(0x01, &eax, &ebx, &ecx, &edx);

    if (!(edx & (1 << 9))) {
        pr_err("APIC not present\n");
        return false;
    }

    /* ECX[21] = x2APIC 지원 여부 */
    if (ecx & (1 << 21))
        pr_info("x2APIC supported\n");

    /* IA32_APIC_BASE MSR (0x1B):
     *   [11]  = APIC Global Enable
     *   [10]  = x2APIC Enable
     *   [8]   = BSP Flag
     *   [12:N] = APIC Base Address (기본 0xFEE00000)
     */
    u64 msr = rdmsrl(MSR_IA32_APICBASE);
    pr_info("APIC Base: %llx, BSP: %d, Enabled: %d, x2APIC: %d\n",
            msr & 0xFFFFF000ULL,
            !!(msr & (1 << 8)),
            !!(msr & (1 << 11)),
            !!(msr & (1 << 10)));
    return true;
}

APIC 시스템 아키텍처

APIC 시스템은 LAPIC(Local APIC, 각 CPU 코어에 내장)과 I/O APIC(칩셋에 위치, 외부 장치 인터럽트 수집)으로 구성됩니다. 인터럽트는 시스템 버스(또는 ICC Bus)를 통해 I/O APIC에서 적절한 LAPIC으로 전달됩니다. 현대 시스템에서는 MSI/MSI-X를 통해 I/O APIC을 우회하는 경로도 있습니다.

APIC 시스템 아키텍처 CPU 0 LAPIC 0 ID=0, Timer, LVT IRR, ISR, TPR CPU 1 LAPIC 1 ID=1, Timer, LVT IRR, ISR, TPR CPU N LAPIC N ID=N, Timer, LVT IRR, ISR, TPR System Bus / ICC Bus I/O APIC 24 IRQ 입력 핀 RTE 리다이렉션 테이블 MMIO 0xFEC00000 MSI/MSI-X 경로 I/O APIC 우회 메모리 쓰기 → LAPIC 직접 IPI 경로 LAPIC ICR 쓰기 CPU 간 직접 전달 키보드 IRQ 1 UART IRQ 4 PCIe NIC MSI-X NVMe SSD MSI-X 범례 I/O APIC 경로 MSI 직접 경로 IPI 경로 시스템 버스 CPU/LAPIC

APIC 기본 주소 (IA32_APIC_BASE MSR)

비트필드설명
[7:0]Reserved예약
[8]BSPBootstrap Processor 플래그 (읽기 전용(Read-Only))
[9]Reserved예약
[10]EXTDx2APIC 모드 활성화 (EN=1일 때만 유효)
[11]ENAPIC 글로벌 활성화
[N:12]APIC BaseLAPIC 레지스터 기본 물리 주소 (기본: 0xFEE00000)
/* IA32_APIC_BASE MSR 읽기/쓰기 */
static u64 read_apic_base(void)
{
    return rdmsrl(MSR_IA32_APICBASE);  /* MSR 0x1B */
}

/* xAPIC → x2APIC 전환:
 * 1) EN=1, EXTD=0 (xAPIC)
 * 2) EN=1, EXTD=1 (x2APIC)
 * 주의: EN=0으로 비활성화 후 재활성화 시 RESET 필요 */
static void enable_x2apic_mode(void)
{
    u64 msr = rdmsrl(MSR_IA32_APICBASE);
    msr |= X2APIC_ENABLE;   /* bit[10] = 1 */
    wrmsrl(MSR_IA32_APICBASE, msr);
}

LAPIC 내부 구조

LAPIC(Local APIC)은 각 프로세서 코어에 내장된 인터럽트 컨트롤러입니다. 인터럽트 수신, 우선순위 판단, 벡터 전달, IPI 발신, 로컬 타이머 등 핵심 기능을 담당합니다. xAPIC 모드에서는 4KB MMIO 공간(0xFEE00000)으로 매핑(Mapping)되고, x2APIC 모드에서는 MSR로 접근합니다.

LAPIC 내부 구조 LAPIC (Local APIC) IRR (256-bit) Interrupt Request Register 수신 대기 인터럽트 비트맵 벡터 16~255 사용 가능 TMR (256-bit) Trigger Mode Register 0=Edge, 1=Level EOI 시 I/O APIC 통보 결정 ISR (256-bit) In-Service Register 현재 처리 중인 인터럽트 EOI 쓰기 시 최고 비트 클리어 우선순위 비교 후 전달 우선순위 제어 TPR (Task Priority Register) - 소프트웨어 설정 PPR (Processor Priority Register) - max(TPR, ISRV) APR (Arbitration Priority Register) - 버스 중재 수신 조건: IRR 벡터 > PPR[7:4] EOI Register 0x0B0 오프셋 쓰기 → ISR 최고 비트 클리어 Level: I/O APIC EOI 전파 SVR Spurious Vector [8]: APIC Enable [12]: Directed EOI [7:0]: Spurious Vector LVT (Local Vector Table) Timer 0x320 Thermal 0x330 PMC 0x340 LINT0 0x350 LINT1 0x360 Error 0x370 ICR (Interrupt Command Register) xAPIC: 0x300(lo) + 0x310(hi) x2APIC: MSR 0x830 (64-bit 단일) IPI 발신용: Destination + Vector + Delivery Mode 식별/설정 레지스터 ID (0x020), Version (0x030) LDR (0x0D0), DFR (0x0E0) 논리적 목적지 주소 설정 입력: System Bus (I/O APIC, MSI, IPI) → IRR 비트 세팅 → 우선순위 비교 → ISR 전달 → CPU IDT 벡터 실행 → EOI

LAPIC 레지스터 맵 (주요 항목)

오프셋(Offset)MSR (x2APIC)이름읽기/쓰기설명
0x0200x802APIC IDR/W프로세서 고유 ID (x2APIC에서 32비트)
0x0300x803VersionRAPIC 버전, Max LVT Entry
0x0800x808TPRR/WTask Priority Register
0x0900x809APRRArbitration Priority Register
0x0A00x80APPRRProcessor Priority Register
0x0B00x80BEOIWEnd of Interrupt
0x0C00x80CRRDRRemote Read Register
0x0D00x80DLDRR/WLogical Destination Register
0x0E0-DFRR/WDestination Format Register (xAPIC 전용)
0x0F00x80FSVRR/WSpurious Interrupt Vector Register
0x100~0x1700x810~0x817ISRRIn-Service Register (256-bit, 8x32)
0x180~0x1F00x818~0x81FTMRRTrigger Mode Register (256-bit)
0x200~0x2700x820~0x827IRRRInterrupt Request Register (256-bit)
0x2800x828ESRRError Status Register
0x3000x830ICR (lo)R/WInterrupt Command Register Low
0x310-ICR (hi)R/WInterrupt Command Register High (xAPIC)
0x3200x832LVT TimerR/W로컬 타이머 벡터/모드
0x3300x833LVT ThermalR/W온도 모니터 인터럽트
0x3400x834LVT PMCR/W성능 모니터 카운터 오버플로우
0x3500x835LVT LINT0R/W로컬 인터럽트 0 (보통 ExtINT)
0x3600x836LVT LINT1R/W로컬 인터럽트 1 (보통 NMI)
0x3700x837LVT ErrorR/WAPIC 내부 오류 인터럽트
0x3800x838Timer ICRR/W타이머 초기 카운트
0x3900x839Timer CCRR타이머 현재 카운트
0x3E00x83ETimer DCRR/W타이머 분주비 설정
-0x83FSelf IPIWx2APIC Self IPI (벡터만 지정)

LAPIC 레지스터 맵 시각화

LAPIC의 4KB MMIO 공간(0xFEE00000~0xFEE003FF)에 배치된 주요 레지스터 그룹을 시각화합니다. 각 레지스터는 16바이트(0x10) 경계에 정렬되며, 실제 유효 비트는 32비트(4바이트)입니다. 나머지 12바이트는 예약 영역으로, 읽기 시 0을 반환하고 쓰기는 무시됩니다.

LAPIC 4KB MMIO 레지스터 맵 (0xFEE00000 ~ 0xFEE003FF) 식별 & 우선순위 레지스터 0x020 APIC ID (R/W) 0x030 Version (R) 0x080 TPR (R/W) 0x090 APR (R) 0x0A0 PPR (R) 0x0B0 EOI (W) 0x0C0 RRD (R) 0x0D0 LDR (R/W) 0x0E0 DFR (R/W) 0x0F0 SVR (R/W) [8]=APIC Enable [12]=Suppress EOI Broadcast [7:0]=Spurious Vector DFR: xAPIC 전용 (x2APIC에서 제거) 256-bit 비트맵 레지스터 0x100~0x170 ISR (8x32-bit) In-Service Register 현재 처리 중인 인터럽트 비트맵 0x180~0x1F0 TMR (8x32-bit) Trigger Mode Register 0=Edge, 1=Level (EOI 동작 결정) 0x200~0x270 IRR (8x32-bit) Interrupt Request Register 수신 대기 인터럽트 비트맵 각 레지스터: 32비트 x 8개 = 256비트 벡터 N → 레지스터[N/32]의 비트[N%32] 명령 & 제어 레지스터 0x280 ESR (R) 0x300 ICR Low (R/W) 0x310 ICR High (R/W) ICR: IPI 발신용 x2APIC: 0x830 단일 64-bit MSR LVT (Local Vector Table): 0x320 LVT Timer (R/W) 0x330 LVT Thermal (R/W) 0x340 LVT PMC (R/W) 0x350 LVT LINT0 (R/W) 0x360 LVT LINT1 (R/W) 0x370 LVT Error (R/W) 각 LVT: Vector + Delivery Mode + Mask 타이머 레지스터 0x380 Timer ICR (Initial Count) (R/W) — 카운트 시작값 0x390 Timer CCR (Current Count) (R) — 현재 남은 카운트 0x3E0 Timer DCR (Divide Config) (R/W) — 분주비 (1~128) One-shot/Periodic: ICR 쓰기 → 카운트다운 시작 TSC-Deadline: ICR/CCR/DCR 무시, MSR 0x6E0 사용 x2APIC MSR 매핑 규칙 MSR 번호 = 0x800 + (MMIO_Offset / 0x10) 예: TPR 0x080 → MSR 0x808, EOI 0x0B0 → MSR 0x80B x2APIC 전용 레지스터: MSR 0x83F Self IPI (W) — 벡터만 지정, 최소 레이턴시 DFR (0x0E0)은 x2APIC에서 제거됨, ICR High (0x310) 통합됨 LAPIC MMIO 메모리 레이아웃 상세 각 레지스터: 16바이트(0x10) 경계 정렬, 유효 비트 32비트 (하위 4바이트) xAPIC: MMIO 접근 (uncacheable memory, UC 타입). 읽기/쓰기 모두 ~100 cycles x2APIC: MSR 접근 (RDMSR/WRMSR 명령). 읽기/쓰기 ~20 cycles (5x 성능 향상) 0x000~0x010: 예약 (접근 금지), 0x040~0x070: 예약, 0x3F0~0x3FF: 예약 IA32_APIC_BASE MSR (0x1B)의 [N:12] 비트로 기본 주소 변경 가능 (기본: 0xFEE00000)

LAPIC 레지스터 직접 읽기/쓰기

리눅스 커널에서 LAPIC 레지스터에 접근하는 방법은 xAPIC 모드와 x2APIC 모드에 따라 다릅니다. 커널은 apic_read()/apic_write() 추상화 함수를 제공하며, 실제 구현은 현재 활성화된 APIC 드라이버에 따라 MMIO 또는 MSR 접근으로 분기합니다.

/* xAPIC MMIO 기반 레지스터 접근 (arch/x86/kernel/apic/apic.c) */

/* LAPIC MMIO 가상 주소 (early_ioremap으로 매핑) */
static void __iomem *apic_mmio_base;

/* xAPIC: MMIO 읽기 */
static u32 native_apic_mem_read(u32 reg)
{
    /* UC(Uncacheable) 메모리 접근 - serializing, ~100 cycles */
    return readl(apic_mmio_base + reg);
}

/* xAPIC: MMIO 쓰기 */
static void native_apic_mem_write(u32 reg, u32 val)
{
    writel(val, apic_mmio_base + reg);
}

/* x2APIC: MSR 기반 읽기 */
static u32 native_apic_msr_read(u32 reg)
{
    u64 msr;
    u32 msr_reg = APIC_BASE_MSR + (reg >> 4);  /* 0x800 + offset/16 */

    rdmsrl(msr_reg, msr);
    return (u32)msr;
}

/* x2APIC: MSR 기반 쓰기 */
static void native_apic_msr_write(u32 reg, u32 val)
{
    u32 msr_reg = APIC_BASE_MSR + (reg >> 4);

    /* x2APIC ICR은 64비트 MSR 0x830 하나로 통합
     * hi(Destination)와 lo(Vector+Mode)를 한 번에 쓰기 */
    if (reg == APIC_ICR) {
        u64 icr_val = ((u64)__x2apic_icr_hi << 32) | val;
        wrmsrl(APIC_BASE_MSR + (APIC_ICR >> 4), icr_val);
        return;
    }

    wrmsrl(msr_reg, val);
}

/* 범용 접근 함수 (현재 모드에 따라 자동 분기) */
static inline u32 apic_read(u32 reg)
{
    return apic->read(reg);  /* struct apic의 함수 포인터 */
}

static inline void apic_write(u32 reg, u32 val)
{
    apic->write(reg, val);
}

/* 실용 예시: 현재 CPU의 APIC 상태 덤프 */
static void dump_lapic_state(void)
{
    pr_info("LAPIC State (CPU %d):\n", smp_processor_id());
    pr_info("  APIC ID:  0x%08x\n", apic_read(APIC_ID));
    pr_info("  Version:  0x%08x\n", apic_read(APIC_LVR));
    pr_info("  TPR:      0x%08x\n", apic_read(APIC_TASKPRI));
    pr_info("  PPR:      0x%08x\n", apic_read(APIC_PROCPRI));
    pr_info("  SVR:      0x%08x\n", apic_read(APIC_SPIV));
    pr_info("  ESR:      0x%08x\n", apic_read(APIC_ESR));
    pr_info("  LVT Timer:0x%08x\n", apic_read(APIC_LVTT));
    pr_info("  LVT LINT0:0x%08x\n", apic_read(APIC_LVT0));
    pr_info("  LVT LINT1:0x%08x\n", apic_read(APIC_LVT1));
    pr_info("  LVT Error:0x%08x\n", apic_read(APIC_LVTERR));
    pr_info("  Timer ICR:0x%08x\n", apic_read(APIC_TMICT));
    pr_info("  Timer CCR:0x%08x\n", apic_read(APIC_TMCCT));
    pr_info("  Timer DCR:0x%08x\n", apic_read(APIC_TDCR));
}

IRR/ISR/TMR 256-bit 비트 벡터

/* IRR, ISR, TMR은 각각 256비트(8개 32비트 레지스터)로 구성
 * 벡터 N에 대응하는 비트: 레지스터[N/32]의 비트[N%32]
 *
 * 벡터 범위:
 *   0~15:  예약 (x86 예외)
 *   16~31: 예약
 *   32~255: 사용 가능 (리눅스: 32~0xEF 일반, 0xF0~0xFF 특수)
 */

/* IRR 읽기 예시 */
static u32 read_irr_vector_range(int reg_index)
{
    /* reg_index: 0~7, 각 32비트 */
    return apic_read(APIC_IRR + reg_index * 0x10);
}

/* 모든 펜딩 인터럽트 확인 */
static void dump_irr(void)
{
    int i;
    pr_info("IRR (Interrupt Request Register):\n");
    for (i = 7; i >= 0; i--) {
        u32 val = apic_read(APIC_IRR + i * 0x10);
        if (val)
            pr_info("  IRR[%d] (vec %d-%d): 0x%08x\n",
                    i, i * 32, i * 32 + 31, val);
    }
}

TPR/PPR 우선순위 계산

/* TPR (Task Priority Register):
 *   [7:4] = Task Priority Class
 *   [3:0] = Task Priority Sub-Class
 *
 * PPR (Processor Priority Register):
 *   PPR = max(TPR, ISRV)
 *   여기서 ISRV = ISR에서 가장 높은 비트의 벡터 값
 *
 * 인터럽트 수신 조건:
 *   새 벡터의 [7:4] > PPR[7:4] 일 때만 수신
 *   같은 우선순위 클래스 내에서는 차단됨
 */

/* TPR 설정으로 저우선순위 인터럽트 차단 */
static void set_task_priority(u8 priority_class)
{
    /* priority_class: 0~15 (0이면 모든 인터럽트 수신) */
    apic_write(APIC_TASKPRI, priority_class << 4);
}

/* cr8 레지스터: 64비트 모드에서 TPR[7:4]의 바로가기
 * mov cr8, rax 는 TPR[7:4] = rax[3:0] 과 동일 */
static inline void write_cr8(u64 val)
{
    asm volatile("mov %0, %%cr8" :: "r"(val));
}

LVT 엔트리 포맷

비트필드설명
[7:0]Vector인터럽트 벡터 (16~255)
[10:8]Delivery Mode000=Fixed, 010=SMI, 100=NMI, 111=ExtINT
[12]Delivery Status0=Idle, 1=Send Pending
[13]Pin Polarity0=Active High, 1=Active Low (LINT만)
[14]Remote IRRLevel 트리거 시 EOI 대기 상태 (읽기 전용)
[15]Trigger Mode0=Edge, 1=Level (LINT만)
[16]Mask1=마스크(비활성화)
[18:17]Timer Mode00=One-shot, 01=Periodic, 10=TSC-Deadline
/* LVT 설정 예시: LINT0을 ExtINT, LINT1을 NMI로 설정 */
static void setup_lapic_lvt(void)
{
    /* LINT0: External Interrupt (8259A 호환) */
    apic_write(APIC_LVT0,
        APIC_DM_EXTINT | APIC_LVT_LEVEL_TRIGGER);

    /* LINT1: NMI */
    apic_write(APIC_LVT1,
        APIC_DM_NMI);

    /* Error: 벡터 0xFE로 에러 인터럽트 */
    apic_write(APIC_LVTERR, ERROR_APIC_VECTOR);

    /* Thermal Monitor: 벡터 지정 */
    apic_write(APIC_LVTTHMR,
        THERMAL_APIC_VECTOR | APIC_DM_FIXED);

    /* Performance Counter: 벡터 지정 */
    apic_write(APIC_LVTPC,
        LOCAL_PERF_VECTOR | APIC_DM_FIXED);
}

LAPIC 초기화 시퀀스 (setup_local_APIC)

부트 과정에서 BSP(Bootstrap Processor)와 AP(Application Processor) 모두 setup_local_APIC()을 호출하여 LAPIC을 초기화합니다. 이 함수는 SVR 활성화, LVT 설정, 우선순위 초기화, 에러 인터럽트 설정 등 핵심 초기화 작업을 수행합니다. LAPIC이 올바르게 초기화되지 않으면 인터럽트가 전달되지 않거나 시스템이 불안정해질 수 있으므로 각 단계의 순서와 의미를 정확히 이해하는 것이 중요합니다.

/* arch/x86/kernel/apic/apic.c - setup_local_APIC() 상세 분석
 *
 * 호출 시점:
 *   BSP: init_bsp_APIC() → setup_local_APIC()
 *   AP:  start_secondary() → setup_local_APIC()
 *
 * 전제 조건:
 *   - IA32_APIC_BASE[11]=1 (APIC 글로벌 활성화)
 *   - x2APIC 전환이 필요한 경우 이미 완료
 */

void setup_local_APIC(void)
{
    unsigned int value;
    int i, j, acked = 0;

    /* ===============================================
     * 1단계: 펜딩 인터럽트 클리어
     * ISR/IRR에 남아있는 인터럽트를 EOI로 제거
     * 최대 256회 시도 (모든 벡터 커버)
     * =============================================== */
    apic_write(APIC_ESR, 0);   /* ESR 클리어 */
    apic_read(APIC_ESR);

    for (i = APIC_ISR_NR - 1; i >= 0; i--) {
        value = apic_read(APIC_ISR + i * 0x10);
        for (j = 31; j >= 0; j--) {
            if (value & (1U << j)) {
                ack_APIC_irq();
                acked++;
            }
        }
    }
    if (acked)
        pr_info("CPU%d: %d pending interrupts cleared\n",
                smp_processor_id(), acked);

    /* ===============================================
     * 2단계: TPR 초기화 (모든 인터럽트 수신 허용)
     * TPR=0 → PPR은 ISR에만 의존
     * =============================================== */
    apic_write(APIC_TASKPRI, 0);

    /* ===============================================
     * 3단계: Spurious Vector Register 설정
     * [8]: APIC Software Enable = 1
     * [12]: Suppress EOI Broadcast (Directed EOI)
     * [7:0]: Spurious Vector = 0xFF
     * =============================================== */
    value = apic_read(APIC_SPIV);
    value &= ~APIC_VECTOR_MASK;
    value |= APIC_SPIV_APIC_ENABLED;

    /* Directed EOI 활성화 (Level 트리거 EOI 최적화) */
    if (lapic_has_directed_eoi())
        value |= APIC_SPIV_DIRECTED_EOI;

    value |= SPURIOUS_APIC_VECTOR;  /* 0xFF */
    apic_write(APIC_SPIV, value);

    /* ===============================================
     * 4단계: LDR/DFR 설정 (Logical Destination)
     * xAPIC Flat: DFR=0xFFFFFFFF, LDR에 비트 세팅
     * x2APIC: 하드웨어가 자동 설정 (소프트웨어 수정 불가)
     * =============================================== */
    apic->init_apic_ldr();

    /* ===============================================
     * 5단계: LVT 설정
     * LINT0: ExtINT (BSP에서만, 8259A 호환)
     * LINT1: NMI
     * Error: ERROR_APIC_VECTOR (0xFE)
     * Thermal/PMC: 커널이 별도 설정
     * Timer: calibrate_APIC_clock()에서 설정
     * =============================================== */
    value = apic_read(APIC_LVT0);
    if (smp_processor_id() == 0) {
        /* BSP LINT0: ExtINT (8259A 인터럽트 수신) */
        value = APIC_DM_EXTINT;
    } else {
        /* AP LINT0: 마스크 (AP는 8259A 사용 안 함) */
        value = APIC_LVT_MASKED;
    }
    apic_write(APIC_LVT0, value);

    /* LINT1: NMI (모든 CPU 동일) */
    apic_write(APIC_LVT1, APIC_DM_NMI);

    /* Error LVT: APIC 내부 에러 인터럽트 */
    apic_write(APIC_LVTERR, ERROR_APIC_VECTOR);

    /* ESR 최종 클리어 */
    apic_write(APIC_ESR, 0);
    apic_read(APIC_ESR);
}
초기화 순서의 중요성: setup_local_APIC()에서 SVR의 APIC Enable 비트를 설정하기 전에 펜딩 인터럽트를 클리어해야 합니다. APIC이 활성화된 상태에서 잔여 인터럽트가 있으면 즉시 CPU로 전달되어 아직 준비되지 않은 핸들러가 호출될 수 있습니다. 또한 LDR/DFR 설정은 SVR 활성화 후에 수행해야 레지스터 쓰기가 유효합니다.

EOI 처리 메커니즘

EOI(End of Interrupt)는 인터럽트 핸들러(Handler) 완료를 LAPIC에 알리는 핵심 동작입니다. EOI 레지스터에 0을 쓰면 ISR에서 가장 높은 우선순위 비트가 클리어됩니다. Level 트리거 인터럽트의 경우 I/O APIC의 Remote IRR 비트도 클리어되어야 합니다.

EOI 흐름

EOI 처리 흐름 (Edge vs Level 트리거) 1. 인터럽트 핸들러 완료 apic_write(APIC_EOI, 0) 2. ISR 최고 비트 클리어 ISR[current_vector] = 0 3. TMR 비트 확인 TMR[vector] = 0(Edge) or 1(Level)? Edge (TMR=0) 완료 추가 작업 없음 Level (TMR=1) 4. EOI 브로드캐스트 시스템 버스를 통해 I/O APIC에 전달 5. I/O APIC Remote IRR 클리어 해당 RTE의 Remote IRR = 0 → 같은 핀에서 새 인터럽트 수신 가능 Directed EOI 최적화 (SVR[12]=1) 브로드캐스트 대신 소프트웨어가 직접 I/O APIC EOI 레지스터에 쓰기
단계동작설명
1 EOI 레지스터 쓰기 인터럽트 핸들러 종료 시 apic_write(APIC_EOI, 0)
2 ISR 비트 클리어 ISR에서 현재 서비스 중인 최고 우선순위 벡터 비트 클리어
3a Edge 트리거 추가 작업 없음 (완료)
3b Level 트리거 TMR 비트가 1이면 I/O APIC에 EOI 브로드캐스트
4 I/O APIC Remote IRR 클리어 해당 RTE의 Remote IRR 비트 클리어 → 같은 핀에서 새 인터럽트 수신 가능
/* EOI 전송 - 리눅스 커널 내부 */
static inline void ack_APIC_irq(void)
{
    /* EOI 레지스터에 0을 쓰면 ISR에서 최고 비트 클리어
     * x2APIC에서는 MSR 0x80B에 wrmsr */
    apic_write(APIC_EOI, 0);
}

/* Level 트리거 EOI: I/O APIC의 Remote IRR 클리어 필요
 * Directed EOI (SVR[12]=1): 브로드캐스트 대신 특정 벡터만 EOI */
static void eoi_ioapic_pin(unsigned int apic, unsigned int pin)
{
    /* I/O APIC EOI 레지스터에 벡터 번호 쓰기 (I/O APIC v2+) */
    io_apic_eoi(apic, pin);
}

/* 커널의 irq_chip.eoi 콜백 */
static void apic_ack_edge(struct irq_data *data)
{
    irq_complete_move(data);
    irq_move_irq(data);
    ack_APIC_irq();   /* Edge: LAPIC EOI만으로 충분 */
}

static void apic_ack_level(struct irq_data *data)
{
    struct irq_cfg *cfg = irqd_cfg(data);

    irq_complete_move(data);
    irq_move_irq(data);
    ack_APIC_irq();   /* ISR 클리어 */

    /* Level: I/O APIC Remote IRR 확인 및 클리어 */
    if (irqd_is_level_type(data))
        eoi_ioapic_pin(cfg->apic, cfg->pin);
}
Directed EOI 최적화: SVR 레지스터의 비트 12(Suppress EOI Broadcast)를 설정하면, Level 트리거 EOI 시 시스템 버스를 통한 브로드캐스트 대신 소프트웨어가 직접 I/O APIC EOI 레지스터에 쓰게 됩니다. 이는 멀티 I/O APIC 시스템에서 불필요한 EOI 메시지를 줄여 성능을 향상시킵니다. 리눅스 커널은 이 기능을 기본으로 활성화합니다 (enable_directed_EOI()).

EOI가 인터럽트 처리에 미치는 영향

EOI의 타이밍은 인터럽트 처리 성능과 정확성에 중대한 영향을 미칩니다. EOI를 너무 늦게 보내면 같은 우선순위 클래스의 다른 인터럽트가 차단되고, EOI를 보내지 않으면 해당 벡터가 영구적으로 차단됩니다. 반대로 핸들러 초반에 EOI를 보내면 같은 인터럽트가 중첩 발생할 수 있습니다.

/* EOI 타이밍이 미치는 영향 분석 */

/* 1. ISR 비트와 PPR의 관계
 * EOI → ISR에서 최고 비트 클리어 → PPR 재계산
 * PPR = max(TPR[7:4], ISRV[7:4])
 * EOI 전: ISRV = 현재 처리 중 벡터 → PPR 높음 → 낮은 인터럽트 차단
 * EOI 후: ISRV = 다음 ISR 최고 또는 0 → PPR 하락 → 펜딩 인터럽트 전달 가능
 */

/* 2. 중첩 인터럽트 (Nested Interrupt) 시나리오
 * 벡터 0x80 처리 중 (ISR[0x80]=1, PPR 클래스=8)
 *   → 벡터 0x90 도착: 클래스 9 > PPR 8 → 수신 허용 (중첩!)
 *   → 벡터 0x70 도착: 클래스 7 < PPR 8 → 차단 (IRR에 펜딩)
 * 0x80 핸들러가 EOI 전송:
 *   → ISR[0x80] 클리어, ISRV = 0x90 (아직 처리 중)
 *   → 0x70은 PPR 9 때문에 여전히 차단
 * 0x90 핸들러가 EOI 전송:
 *   → ISR[0x90] 클리어, ISRV = 0
 *   → PPR = TPR → 0x70 이제 전달 가능
 */

/* 3. Level 트리거 EOI 누락 시의 문제
 * I/O APIC의 Remote IRR이 1인 채로 남으면:
 *   - 같은 핀에서 새 인터럽트 메시지 전송 불가
 *   - 장치가 인터럽트를 Assert해도 I/O APIC이 무시
 *   - 결과: 장치 응답 없음 (인터럽트 분실)
 *
 * 진단: cat /proc/interrupts에서 특정 IRQ 카운트가 멈추면 의심
 * 복구: I/O APIC RTE를 다시 쓰거나 I/O APIC EOI 레지스터에 벡터 쓰기
 */

/* 4. Directed EOI 구현 (커널 코드) */
static void enable_directed_EOI(void)
{
    u32 version = apic_read(APIC_LVR);

    /* APIC 버전 0x10+ 에서 Directed EOI 지원
     * SVR[12] = 1: EOI 브로드캐스트 억제
     * 소프트웨어가 I/O APIC EOI 레지스터(0x40)에 직접 쓰기 */
    if (GET_APIC_VERSION(version) >= 0x10) {
        u32 svr = apic_read(APIC_SPIV);
        svr |= APIC_SPIV_DIRECTED_EOI;  /* bit 12 */
        apic_write(APIC_SPIV, svr);

        pr_info("Directed EOI enabled\n");
    }
}

/* 5. I/O APIC EOI 레지스터 직접 쓰기 (Directed EOI 모드) */
static void ioapic_eoi_write(unsigned int apic, unsigned int vector)
{
    struct io_apic __iomem *io = io_apic_base(apic);

    /* IOEOI 레지스터 (오프셋 0x40)에 벡터 번호 쓰기
     * → 해당 벡터와 매칭되는 RTE의 Remote IRR 클리어 */
    writel(vector, &io->eoi);
}

I/O APIC

I/O APIC(I/O Advanced Programmable Interrupt Controller)은 칩셋(보통 사우스브리지 또는 PCH)에 위치하며, 외부 장치의 인터럽트를 수집하여 시스템 버스를 통해 적절한 LAPIC으로 전달합니다. 주요 기능은 Redirection Table Entry(RTE)를 통한 인터럽트 라우팅입니다.

I/O APIC 레지스터 접근

I/O APIC은 MMIO 기반으로 접근하며, 기본 주소는 0xFEC00000입니다. 간접 주소 방식(Index/Data)을 사용합니다.

MMIO 오프셋이름설명
0x00IOREGSELIndex Register - 접근할 레지스터 번호 지정
0x10IOWINData Register - 선택된 레지스터의 데이터 읽기/쓰기
0x40IOEOIEOI Register (I/O APIC v2+) - Level 트리거 EOI
인덱스이름설명
0x00IOAPICIDI/O APIC 식별자
0x01IOAPICVER버전 + 최대 RTE 수
0x02IOAPICARB중재 ID
0x10~0x3FIOREDTBL[0:23]Redirection Table (64-bit 엔트리 x 24)
I/O APIC RTE (Redirection Table Entry) 64-bit [63:56] Destination (xAPIC) / [63:32] (x2APIC) [55:17] Reserved [16] Mask [15] Trigger [14] RemIRR [13] Polarity [12] DeliSts [11] DestMode [10:8] Delivery Mode [7:0] Interrupt Vector (32~255) Delivery Mode [10:8] 000 = Fixed (지정 벡터 전달) 001 = Lowest Pri (최저 우선순위 CPU) 010 = SMI (시스템 관리 인터럽트) 100 = NMI (마스크 불가 인터럽트) 101 = INIT (프로세서 초기화) 111 = ExtINT (외부 인터럽트 전달) Destination Mode [11] 0 = Physical Mode → [63:56]의 APIC ID로 직접 지정 → 단일 CPU 또는 브로드캐스트(0xFF) 1 = Logical Mode → LDR/DFR 기반 그룹 전달 (Flat/Cluster)

I/O APIC 프로그래밍

/* I/O APIC 간접 접근 매크로 */
static inline u32 io_apic_read(unsigned int apic, unsigned int reg)
{
    struct io_apic __iomem *io = io_apic_base(apic);

    writel(reg, &io->index);     /* IOREGSEL에 레지스터 번호 */
    return readl(&io->data);     /* IOWIN에서 데이터 읽기 */
}

static inline void io_apic_write(unsigned int apic, unsigned int reg, u32 val)
{
    struct io_apic __iomem *io = io_apic_base(apic);

    writel(reg, &io->index);
    writel(val, &io->data);
}

/* RTE 읽기 (64비트: low + high) */
static void ioapic_read_entry(int apic, int pin,
                              struct IO_APIC_route_entry *e)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&ioapic_lock, flags);
    e->w1 = io_apic_read(apic, 0x10 + 2 * pin);      /* Low 32-bit */
    e->w2 = io_apic_read(apic, 0x10 + 2 * pin + 1);  /* High 32-bit */
    raw_spin_unlock_irqrestore(&ioapic_lock, flags);
}

/* RTE 쓰기 */
static void ioapic_write_entry(int apic, int pin,
                               struct IO_APIC_route_entry e)
{
    unsigned long flags;

    raw_spin_lock_irqsave(&ioapic_lock, flags);
    /* 주의: High 먼저 쓰기 (Low 쓰기 시 인터럽트 트리거 가능) */
    io_apic_write(apic, 0x10 + 2 * pin + 1, e.w2);
    io_apic_write(apic, 0x10 + 2 * pin, e.w1);
    raw_spin_unlock_irqrestore(&ioapic_lock, flags);
}

ACPI MADT를 통한 I/O APIC 감지

/* ACPI MADT (Multiple APIC Description Table)에서 I/O APIC 정보 파싱
 *
 * MADT Entry Type 1: I/O APIC
 *   - I/O APIC ID
 *   - I/O APIC Address (기본 0xFEC00000)
 *   - Global System Interrupt Base
 *
 * MADT Entry Type 2: Interrupt Source Override
 *   - ISA IRQ → GSI 매핑 (예: IRQ 0 → GSI 2 타이머 리다이렉트)
 */

static int __init acpi_parse_ioapic(
    union acpi_subtable_headers *header,
    const unsigned long end)
{
    struct acpi_madt_io_apic *ioapic =
        (struct acpi_madt_io_apic *)header;

    pr_info("I/O APIC: id=%d addr=0x%x gsi_base=%d\n",
            ioapic->id, ioapic->address,
            ioapic->global_irq_base);

    mp_register_ioapic(ioapic->id, ioapic->address,
                        ioapic->global_irq_base, NULL);
    return 0;
}

I/O APIC 핀-GSI-벡터 매핑

I/O APIC의 핀 번호, GSI(Global System Interrupt), 리눅스 IRQ 번호, 그리고 실제 CPU 벡터 사이의 매핑을 이해하는 것이 인터럽트 라우팅 디버깅(Debugging)의 핵심입니다.

용어범위설명
I/O APIC Pin 0~23 (per I/O APIC) 물리적 입력 핀 번호. RTE 인덱스와 동일
GSI (Global System Interrupt) 0~N (시스템 전체) ACPI 표준 인터럽트 번호. I/O APIC의 base + pin으로 계산
Linux IRQ 가상 번호 커널 내부 irq_desc 배열 인덱스. IRQ 도메인이 GSI에서 매핑
CPU Vector 32~255 실제 IDT 엔트리. per-CPU 동적 할당 (같은 벡터가 다른 CPU에서 다른 IRQ 처리 가능)
/* I/O APIC 핀 → GSI 변환 */
static unsigned int pin_to_gsi(int ioapic_idx, int pin)
{
    return mp_ioapic_gsi_routing(ioapic_idx)->gsi_base + pin;
}

/* ISA IRQ → GSI 리다이렉트 (ACPI Interrupt Source Override)
 * 일반적인 리다이렉트:
 *   ISA IRQ 0 (PIT Timer) → GSI 2 (I/O APIC pin 2)
 *   ISA IRQ 9 (ACPI SCI)  → GSI 9 (Level, Low)
 */
static int __init acpi_parse_int_src_ovr(
    union acpi_subtable_headers *header,
    const unsigned long end)
{
    struct acpi_madt_interrupt_override *ovr =
        (struct acpi_madt_interrupt_override *)header;

    pr_info("INT_SRC_OVR: bus=%d src_irq=%d gsi=%d flags=0x%x\n",
            ovr->bus, ovr->source_irq, ovr->global_irq, ovr->inti_flags);

    /* flags[1:0]: Polarity (00=Bus default, 01=Active High, 11=Active Low)
     * flags[3:2]: Trigger  (00=Bus default, 01=Edge, 11=Level) */

    mp_override_legacy_irq(ovr->source_irq, ovr->inti_flags,
                           ovr->global_irq);
    return 0;
}

I/O APIC RTE 설정 예시

/* 특정 핀에 대한 완전한 RTE 설정 예시 */
static void setup_ioapic_irq(int apic_id, int pin,
                              int vector, int dest_apicid)
{
    struct IO_APIC_route_entry entry;

    memset(&entry, 0, sizeof(entry));

    /* Delivery Mode: Fixed (000) */
    entry.delivery_mode = APIC_DELIVERY_MODE_FIXED;

    /* Destination Mode: Physical (단일 CPU 지정) */
    entry.dest_mode_logical = 0;

    /* Trigger Mode: Edge (대부분의 ISA 장치) */
    entry.is_level = 0;

    /* Pin Polarity: Active High (ISA 기본) */
    entry.active_low = 0;

    /* Mask: 활성화 (0 = unmask) */
    entry.masked = 0;

    /* Vector 및 Destination 설정 */
    entry.vector = vector;
    entry.destid_0_7 = dest_apicid & 0xFF;

    /* RTE 쓰기 (High word 먼저!) */
    ioapic_write_entry(apic_id, pin, entry);
}

/* PCI 인터럽트는 일반적으로 Level-triggered, Active-Low
 * INTA# → 핀은 ACPI _PRT (PCI Routing Table)에서 결정
 * 또는 PIRQ 라우터 (레거시 시스템)로 매핑 */

인터럽트 전달 흐름

외부 장치에서 발생한 인터럽트가 CPU의 IDT 핸들러에 도달하기까지의 전체 과정입니다. I/O APIC 경로와 MSI 경로를 모두 포함하며, LAPIC의 우선순위 판단 로직이 핵심입니다.

인터럽트 전달 흐름 (Device → IDT) 장치 IRQ 발생 I/O APIC RTE 참조 Vector + Dest MSI Write Addr + Data System Bus 메시지 전달 LAPIC 1. IRR 비트 세팅 2. Vec[7:4] > PPR[7:4]? 3. ISR 비트 세팅 4. CPU에 벡터 전달 CPU IDT[vector] 핸들러 실행 → EOI 우선순위 판단 상세 1. 인터럽트 메시지 수신 → IRR[vector] = 1 2. PPR = max(TPR[7:4], ISRV[7:4]) 계산 (ISRV = ISR에서 가장 높은 세팅 비트) 3. IRR에서 가장 높은 벡터의 [7:4] > PPR[7:4] → 수신 허용 4. IRR[vector] 클리어, ISR[vector] 세팅, CPU에 벡터 번호 전달 → IDT 핸들러 진입 5. 핸들러 완료 후 EOI → ISR[vector] 클리어 → PPR 재계산 → 대기 중인 IRR 처리

I/O APIC에서 LAPIC까지의 인터럽트 라우팅 경로

외부 장치 인터럽트가 I/O APIC 핀에 도달한 후 CPU의 IDT 핸들러에 도착하기까지의 단계별 세부 과정을 추적합니다. 각 단계에서 하드웨어가 수행하는 동작과 소프트웨어(커널)의 역할을 구분하여 이해하는 것이 인터럽트 디버깅의 핵심입니다.

I/O APIC → LAPIC 인터럽트 라우팅 상세 경로 1. 장치 IRQ 발생 물리적 IRQ 라인 Assert Edge: Low→High 전이 Level: Active 유지 2. I/O APIC RTE 조회 핀 N → RTE[N] 읽기 Mask=0 확인 (1이면 차단) Vector, Dest, Mode 추출 Trigger/Polarity 결정 3. 인터럽트 메시지 전송 시스템 버스 메시지 생성 Destination ID (물리/논리) Vector + Delivery Mode Trigger Mode + Level 4. LAPIC 수신 처리 Destination 매칭 확인 IRR[vector] 비트 세팅 TMR[vector] = Trigger Mode Level: Remote IRR 세팅 5. 우선순위 판단 PPR = max(TPR[7:4], ISRV[7:4]) IRR 최고 벡터[7:4] > PPR[7:4]? Yes → 수신, No → 대기(펜딩) ISRV: ISR 내 최고 비트 벡터 6. CPU에 벡터 전달 IRR[vector] → ISR[vector] 이동 CPU에 벡터 번호 통보 IF=1이면 즉시 IDT 진입 7. IDT 핸들러 실행 IDT[vector] → 핸들러 함수 irq_desc→handle_irq() 호출 완료 후 EOI 전송 Lowest Priority 라우팅 상세 Delivery Mode = 001 (Lowest Priority) 인 경우: 1. Destination에 포함된 CPU 그룹 식별 2. 각 LAPIC의 TPR(Task Priority) 비교 3. 가장 낮은 TPR을 가진 CPU 선택 4. 동일 TPR 시 구현 의존적 선택 (라운드 로빈) 5. 현대 CPU: Focus Processor 비활성화 시 효과적 * 리눅스: Fixed 모드 선호 (예측 가능성 향상) Physical vs Logical 라우팅 Physical (DestMode=0): Dest[7:0] = 대상 APIC ID (단일 CPU) Dest=0xFF → 브로드캐스트 (모든 CPU) Logical (DestMode=1): Flat: Dest 비트맵 AND LDR 비교 Cluster: Cluster ID 매칭 + 비트맵 여러 CPU에 동시 전달 가능 (멀티캐스트) Level 트리거 인터럽트의 Remote IRR 흐름 1. I/O APIC 핀에 Level Assert 신호 → RTE의 Remote IRR = 1 세팅 2. Remote IRR = 1인 동안 같은 핀의 새 인터럽트 메시지 전송 차단 (중복 방지) 3. CPU 핸들러 완료 → EOI 전송 → LAPIC이 시스템 버스로 EOI 브로드캐스트 4. I/O APIC이 EOI 수신 → Remote IRR = 0 클리어 → 핀에서 새 인터럽트 수신 가능 5. Directed EOI (SVR[12]=1) 사용 시: 브로드캐스트 대신 I/O APIC EOI 레지스터에 직접 벡터 쓰기 전체 경로 레이턴시: Edge 트리거 ~1μs (I/O APIC 처리 + 시스템 버스 전달 + LAPIC 판단 + IDT 진입) MSI 경로: I/O APIC 단계 생략 → 장치가 직접 시스템 버스에 인터럽트 메시지 작성 → LAPIC 수신 (~500ns 이하) Interrupt Remapping 활성 시: IOMMU가 메시지를 가로채 IRTE 변환 후 LAPIC에 전달 (추가 ~100ns)

다중 CPU Lowest Priority 인터럽트 라우팅

Lowest Priority 전달 모드는 인터럽트를 가장 유휴한(idle) CPU에 전달하여 부하를 분산합니다. 그러나 현대 리눅스 커널은 예측 가능성을 위해 주로 Fixed 모드를 사용하고, 소프트웨어(irqbalance)로 IRQ 어피니티를 관리합니다. 아래 다이어그램은 4개 CPU 환경에서 Lowest Priority 라우팅이 동작하는 과정을 보여줍니다.

다중 CPU Lowest Priority 인터럽트 라우팅 I/O APIC RTE: DeliveryMode=001 (Lowest Priority) Dest=0x0F (4CPU 전체) 시스템 버스 - 인터럽트 메시지 브로드캐스트 CPU 0 - LAPIC TPR = 0x40 (커널 스레드 실행 중) 우선순위 클래스: 4 → 선택되지 않음 CPU 1 - LAPIC TPR = 0x00 (idle 상태) 우선순위 클래스: 0 (최저) → 인터럽트 수신! CPU 2 - LAPIC TPR = 0x20 (경량 작업 중) 우선순위 클래스: 2 → 선택되지 않음 CPU 3 - LAPIC TPR = 0x80 (고우선순위 작업) 우선순위 클래스: 8 → 선택되지 않음 Lowest Priority 라우팅 동작 원리 1. I/O APIC이 RTE의 DeliveryMode=001(Lowest Priority)로 인터럽트 메시지를 시스템 버스에 전송합니다. 2. Destination에 포함된 모든 LAPIC(CPU 0-3)이 메시지를 수신하고 자신의 TPR을 비교합니다. 3. TPR이 가장 낮은 CPU 1 (TPR=0x00, idle)이 인터럽트를 수신하여 IRR에 세팅합니다. 4. 동일 TPR인 CPU가 여러 개이면 하드웨어 구현에 따라 라운드 로빈 또는 고정 선택됩니다.
Lowest Priority 모드의 현실적 한계: 현대 x86 프로세서에서 Lowest Priority 라우팅의 하드웨어 구현은 제조사와 세대에 따라 다르며, 일부 프로세서에서는 단순히 가장 낮은 APIC ID를 가진 CPU로 전달하거나 라운드 로빈이 정확하게 동작하지 않는 경우가 있습니다. 이러한 이유로 리눅스 커널은 대부분의 I/O APIC RTE를 Fixed 모드(DeliveryMode=000)로 설정하고, irqbalance 데몬(Daemon)이 주기적으로 IRQ 어피니티를 조정하여 소프트웨어 기반 부하 분산을 수행합니다. /proc/irq/N/smp_affinity를 통해 개별 IRQ의 대상 CPU를 직접 설정할 수 있습니다.

벡터 우선순위 규칙

벡터 범위우선순위 클래스리눅스 용도
0x00~0x1F0~1x86 예외 (예약)
0x20~0x2F2ISA IRQ 레거시 매핑
0x30~0xEF3~14장치 인터럽트, I/O APIC, MSI
0xF015APIC Timer
0xF1~0xF915IPI (Reschedule, Call Function 등)
0xFA15IRQ Work
0xFB15Thermal
0xFC15Threshold APIC
0xFD15CMCI
0xFE15Error APIC
0xFF15Spurious APIC
/* arch/x86/include/asm/irq_vectors.h - 리눅스 벡터 할당 */
#define FIRST_EXTERNAL_VECTOR       0x20
#define FIRST_SYSTEM_VECTOR         0xF0

/* 시스템 벡터 */
#define LOCAL_TIMER_VECTOR          0xF0
#define RESCHEDULE_VECTOR           0xF1
#define CALL_FUNCTION_VECTOR        0xF2
#define CALL_FUNCTION_SINGLE_VECTOR 0xF3
#define THERMAL_APIC_VECTOR         0xFB
#define THRESHOLD_APIC_VECTOR       0xFC
#define DEFERRED_ERROR_VECTOR       0xFD
#define ERROR_APIC_VECTOR           0xFE
#define SPURIOUS_APIC_VECTOR        0xFF

/* 벡터 할당 (동적): FIRST_EXTERNAL_VECTOR ~ FIRST_SYSTEM_VECTOR-1
 * 리눅스는 per-CPU 벡터 할당으로 같은 벡터를 다른 CPU에서 다른 IRQ에 사용 가능 */

x2APIC

x2APIC은 xAPIC의 확장으로, Intel Nehalem 이후 프로세서에서 지원됩니다. MMIO 대신 MSR(Model Specific Register)을 통해 LAPIC 레지스터에 접근하며, APIC ID가 8비트에서 32비트로 확장되어 이론적으로 4G 이상의 프로세서를 지원합니다.

xAPIC vs x2APIC 비교 xAPIC (MMIO) 접근 방식: MMIO (0xFEE00000, 4KB) APIC ID: 8-bit [최대 255 CPU] ICR: 2회 MMIO 쓰기 (hi → lo) DFR: Flat 또는 Cluster 모드 Self IPI: ICR shorthand 사용 Interrupt Remapping: 선택적 ICR 쓰기 (IPI 발신) 1. MMIO [0xFEE00310] ← Destination 2. MMIO [0xFEE00300] ← Vector+Mode (lo 쓰기 시 IPI 발신 트리거) x2APIC (MSR) 접근 방식: MSR (0x800~0x83F) APIC ID: 32-bit [최대 4G CPU] ICR: 1회 MSR 쓰기 (64-bit) Cluster 모드: 논리적 Cluster ID Self IPI: 전용 MSR 0x83F Interrupt Remapping: 필수 (VT-d) ICR 쓰기 (IPI 발신) 1. WRMSR [0x830] ← {Dest[63:32], Vec+Mode[31:0]} (단일 MSR 쓰기로 원자적 발신) Self IPI: WRMSR [0x83F] ← vector 진화

x2APIC MSR 매핑

xAPIC MMIO 오프셋x2APIC MSR레지스터비고
0x0200x802APIC ID32비트로 확장 (읽기 전용)
0x0800x808TPR동일
0x0B00x80BEOI동일 (쓰기 전용)
0x0D00x80DLDR읽기 전용 (하드웨어가 설정)
0x0E0-DFR제거됨 (Cluster 고정)
0x300+0x3100x830ICR64비트 단일 MSR로 통합
-0x83FSelf IPI신규 (벡터만 지정)

x2APIC 활성화 코드

/* arch/x86/kernel/apic/apic.c - x2APIC 활성화 */
void __init enable_x2apic(void)
{
    u64 msr;

    /* CPUID로 x2APIC 지원 확인 */
    if (!boot_cpu_has(X86_FEATURE_X2APIC)) {
        pr_info("x2APIC not supported\n");
        return;
    }

    /* Interrupt Remapping (VT-d/IOMMU) 필수 확인
     * x2APIC은 32-bit destination을 사용하므로
     * IR 없이는 보안 위험 (임의 CPU에 인터럽트 주입 가능) */
    if (!x2apic_supported_by_iommu()) {
        pr_warn("x2APIC requires Interrupt Remapping\n");
        return;
    }

    /* IA32_APIC_BASE MSR에서 x2APIC 활성화 */
    msr = rdmsrl(MSR_IA32_APICBASE);

    if (!(msr & X2APIC_ENABLE)) {
        msr |= X2APIC_ENABLE;   /* bit[10] = 1 */
        wrmsrl(MSR_IA32_APICBASE, msr);
    }

    pr_info("x2APIC enabled, APIC ID: %u\n",
            native_apic_msr_read(APIC_ID));
}

/* x2APIC Cluster 모드에서의 LDR 값
 * LDR = (Cluster ID << 16) | (1 << (APIC_ID % 16))
 * 하드웨어가 자동 설정 (소프트웨어 수정 불가) */
x2APIC과 Interrupt Remapping: x2APIC 모드에서는 Interrupt Remapping(IR)이 필수입니다. IR 없이 x2APIC을 사용하면 임의의 코드가 MSI 메모리 쓰기를 통해 특정 CPU에 인터럽트를 주입할 수 있는 보안 위험이 있습니다. Intel VT-d 또는 AMD IOMMU의 IR 기능이 DMAR/IVRS ACPI 테이블로 검출되어야 합니다.

x2APIC Cluster 모드 상세

x2APIC의 Cluster 모드는 대규모 NUMA 시스템에서 효율적인 인터럽트 라우팅을 제공합니다. 32비트 APIC ID가 Cluster ID(상위 16비트)와 Logical ID(하위 16비트)로 분할됩니다.

구성요소비트설명
Cluster ID [31:16] 클러스터 번호 (보통 NUMA 노드에 대응)
Logical ID [15:0] 클러스터 내 비트맵 (최대 16 CPU per cluster)
/* x2APIC Cluster 모드: IPI 전송 최적화
 *
 * Cluster 모드에서 같은 클러스터 내 여러 CPU에 IPI를 보낼 때
 * 단일 ICR 쓰기로 비트맵 OR 전송 가능 (브로드캐스트 최적화)
 *
 * 예: Cluster 0의 CPU 0,1,3에 IPI 전송
 *   Destination = (0 << 16) | (1<<0 | 1<<1 | 1<<3) = 0x0000000B
 */
static void x2apic_send_IPI_mask_cluster(
    const struct cpumask *mask, int vector)
{
    unsigned int cpu, this_cluster, dest;
    unsigned long flags;

    local_irq_save(flags);

    /* 클러스터별로 묶어서 전송 */
    for_each_cpu(cpu, mask) {
        u32 apicid = per_cpu(x86_cpu_to_apicid, cpu);
        this_cluster = apicid >> 16;
        dest = apicid;  /* Cluster ID + Logical ID */

        /* 같은 클러스터의 다른 CPU도 비트맵에 추가 */
        /* (실제 구현은 클러스터별 그룹핑 최적화 포함) */

        __x2apic_send_IPI_dest(dest, vector,
                               APIC_DEST_LOGICAL);
    }

    local_irq_restore(flags);
}

/* xAPIC → x2APIC 전환 시 주의사항:
 * 1. DFR 레지스터가 제거됨 (x2APIC에서는 존재하지 않음)
 * 2. LDR은 읽기 전용 (하드웨어가 APIC ID 기반으로 자동 설정)
 * 3. ICR이 64비트 단일 MSR로 통합 (두 번 쓰기 불필요)
 * 4. Self IPI 전용 MSR 추가 (벡터만 지정, 최소 레이턴시)
 * 5. xAPIC으로 역전환 불가 (RESET 필요)
 */

x2APIC vs xAPIC 레지스터 접근 비교 코드

동일한 APIC 연산을 xAPIC(MMIO)과 x2APIC(MSR) 모드에서 수행하는 코드를 비교합니다. x2APIC의 성능 이점이 어디서 오는지 구체적으로 확인할 수 있습니다.

/* ====== EOI 전송: xAPIC vs x2APIC ====== */

/* xAPIC: MMIO 쓰기 (~100 cycles)
 * Uncacheable 메모리 영역 접근 → 직렬화(Serialization) 발생 */
static inline void xapic_eoi(void)
{
    writel(0, apic_mmio_base + APIC_EOI);  /* 0xFEE000B0 */
}

/* x2APIC: MSR 쓰기 (~20 cycles)
 * WRMSR 명령어 → CPU 내부 버스 직접 접근 */
static inline void x2apic_eoi(void)
{
    wrmsrl(0x80B, 0);  /* MSR 0x80B = EOI */
}

/* ====== IPI 전송: xAPIC vs x2APIC ====== */

/* xAPIC: 2회 MMIO 쓰기 (~200 cycles)
 * 주의: High를 먼저 써야 함 (Low 쓰기 시 IPI 트리거) */
static void xapic_send_ipi(u8 dest_apicid, u8 vector)
{
    /* 1) ICR High (0x310): Destination APIC ID */
    writel((u32)dest_apicid << 24,
           apic_mmio_base + APIC_ICR2);  /* 0xFEE00310 */

    /* 2) ICR Low (0x300): Vector + Mode → IPI 발신 트리거 */
    writel(APIC_DEST_PHYSICAL | APIC_DM_FIXED | vector,
           apic_mmio_base + APIC_ICR);   /* 0xFEE00300 */

    /* 3) ICR Delivery Status 폴링 (전송 완료 대기)
     * xAPIC에서는 IPI 전송 완료를 확인해야 함 */
    while (readl(apic_mmio_base + APIC_ICR) & APIC_ICR_BUSY)
        cpu_relax();
}

/* x2APIC: 1회 MSR 쓰기 (~30 cycles, 원자적)
 * 64-bit MSR로 Destination과 Vector를 동시에 전달 */
static void x2apic_send_ipi(u32 dest_apicid, u8 vector)
{
    u64 icr = ((u64)dest_apicid << 32) |
              APIC_DEST_PHYSICAL | APIC_DM_FIXED | vector;

    /* 단일 WRMSR로 IPI 발신 (Delivery Status 폴링 불필요) */
    wrmsrl(0x830, icr);  /* MSR 0x830 = ICR */
}

/* ====== Self IPI: x2APIC 전용 최적화 ====== */

/* xAPIC: Self IPI는 ICR Shorthand 사용 (~200 cycles) */
static void xapic_self_ipi(u8 vector)
{
    writel(APIC_DEST_SELF | APIC_DM_FIXED | vector,
           apic_mmio_base + APIC_ICR);
}

/* x2APIC: 전용 Self-IPI MSR (~15 cycles!)
 * 벡터만 지정하면 자동으로 자신에게 전달 */
static void x2apic_self_ipi(u8 vector)
{
    wrmsrl(0x83F, vector);  /* MSR 0x83F = Self IPI */
}

/* ====== TPR(cr8) 접근: 공통 최적화 ====== */

/* 64비트 모드에서 CR8 레지스터는 TPR[7:4]의 바로가기
 * xAPIC/x2APIC 무관하게 가장 빠른 TPR 접근 방법 (~3 cycles) */
static inline void set_tpr_via_cr8(u8 priority_class)
{
    /* cr8 = TPR[7:4] (우선순위 클래스, 0~15) */
    asm volatile("mov %0, %%cr8" :: "r"((u64)priority_class));
}

static inline u8 get_tpr_via_cr8(void)
{
    u64 cr8;
    asm volatile("mov %%cr8, %0" : "=r"(cr8));
    return (u8)cr8;
}

x2APIC 성능 이점

연산xAPIC (MMIO)x2APIC (MSR)개선
레지스터 읽기 ~100 cycles (uncacheable MMIO) ~20 cycles (RDMSR) ~5x 빠름
레지스터 쓰기 ~100 cycles (uncacheable MMIO) ~20 cycles (WRMSR) ~5x 빠름
IPI 전송 (ICR) ~200 cycles (2x MMIO 쓰기) ~30 cycles (1x WRMSR) ~7x 빠름
Self IPI ~200 cycles (ICR shorthand) ~15 cycles (전용 MSR) ~13x 빠름
EOI ~100 cycles ~20 cycles ~5x 빠름
성능 영향: 고빈도 인터럽트(NVMe, 고속 NIC) 환경에서 x2APIC은 EOI와 IPI 오버헤드를 크게 줄여줍니다. 특히 ack_APIC_irq()는 모든 인터럽트 핸들러에서 호출되므로, 초당 수십만 회의 인터럽트를 처리하는 시스템에서 x2APIC의 5x EOI 개선은 유의미한 CPU 사이클 절감을 제공합니다.

IPI 메커니즘 상세

IPI(Inter-Processor Interrupt)는 한 CPU가 다른 CPU에게 인터럽트를 보내는 메커니즘입니다. LAPIC의 ICR(Interrupt Command Register)에 쓰기를 통해 발신하며, 리눅스 커널에서는 리스케줄링(Rescheduling), TLB 플러시(Flush), 함수 호출(Call Function), AP(Application Processor) 부팅 등에 사용됩니다.

ICR (Interrupt Command Register) 비트 필드

ICR (Interrupt Command Register) 64-bit 비트 필드 상위 32비트 (xAPIC: 0x310 / x2APIC: MSR 0x830[63:32]) [63:56] Destination Field xAPIC: 8비트 APIC ID 또는 Logical Mask [55:32] Reserved (x2APIC에서는 [63:32] 전체가 32-bit Destination) 하위 32비트 (xAPIC: 0x300 / x2APIC: MSR 0x830[31:0]) [19:18] Shorthand [17:16] Rsvd [15] Trigger [14] Level [13:12] Rsvd [11] DestMode [10:8] Del.Mode [7:0] Vector 인터럽트 벡터 번호 (Fixed/Lowest Pri에서 사용) Destination Shorthand [19:18] 00 = No Shorthand (Dest 필드 사용) 01 = Self (자신에게 IPI) 10 = All Including Self 11 = All Excluding Self Delivery Mode [10:8] 000 = Fixed → 지정 벡터 전달 001 = Lowest Pri → 최저 우선순위 CPU 선택 010 = SMI → 시스템 관리 인터럽트 011 = Reserved 100 = NMI → 마스크 불가 인터럽트 101 = INIT → INIT 신호 (AP 리셋) 110 = Start-up → SIPI (AP 시작 주소) 111 = ExtINT → 외부 인터럽트 전달

IPI 종류와 리눅스 커널 사용

IPI 종류벡터용도커널 함수
Reschedule 0xF1 대상 CPU의 스케줄러 재평가 요청 smp_send_reschedule()
Call Function 0xF2 대상 CPU에서 특정 함수 실행 (브로드캐스트) smp_call_function()
Call Function Single 0xF3 단일 CPU에서 특정 함수 실행 smp_call_function_single()
TLB Shootdown 0xF5 다른 CPU의 TLB 엔트리 무효화 flush_tlb_others()
IRQ Work 0xFA IRQ 컨텍스트에서 deferred work 실행 irq_work_queue()
INIT N/A 대상 CPU를 INIT 상태로 리셋 apic_send_init()
SIPI (Startup IPI) 시작 주소 AP를 지정 주소에서 실행 시작 apic_send_startup()

INIT-SIPI-SIPI: AP 부팅 시퀀스

멀티프로세서 시스템에서 BSP(Bootstrap Processor)가 AP(Application Processor)를 부팅할 때 사용하는 표준 시퀀스입니다. Intel MP 스펙에 정의된 이 프로토콜은 INIT IPI로 AP를 리셋하고, SIPI(Startup IPI)로 시작 주소를 전달합니다.

INIT-SIPI-SIPI AP 부팅 시퀀스 BSP (CPU 0) AP (CPU N) INIT IPI 전송 ICR: Del.Mode=101(INIT) INIT 상태 진입 (리셋, Wait-for-SIPI) 10ms 대기 SIPI #1 전송 ICR: Del.Mode=110, Vec=시작페이지 Real Mode 시작 CS:IP = VV00:0000 200μs 대기 SIPI #2 전송 (안전을 위한 재전송) 이미 실행 중이면 무시 AP 부팅 단계 1. Real Mode 진입 2. GDT/IDT 설정 3. Protected Mode 4. Long Mode (64-bit) 5. 스택 설정 6. LAPIC 초기화 7. start_secondary() 8. cpu_idle() 진입 AP 응답 대기 cpu_online 체크 cpu_online_mask 비트 세팅 AP 부팅 완료 Idle Loop 진입 전체 소요: ~10ms (INIT 대기가 대부분) | SIPI Vector: 시작 물리 주소의 상위 바이트 (예: 0x08 → 0x08000)
/* arch/x86/kernel/smpboot.c - AP 부팅 (INIT-SIPI-SIPI) */
static int wakeup_secondary_cpu_via_init(int phys_apicid,
                                         unsigned long start_eip)
{
    unsigned long send_status, accept_status;
    int num_starts, j;

    /* 1단계: INIT IPI 전송
     * ICR: Level=Assert, Trigger=Level, DeliveryMode=INIT */
    apic_icr_write(APIC_INT_LEVELTRIG | APIC_INT_ASSERT |
                    APIC_DM_INIT, phys_apicid);
    udelay(200);  /* 200μs 대기 (INIT assert) */

    /* INIT de-assert */
    apic_icr_write(APIC_INT_LEVELTRIG | APIC_DM_INIT,
                    phys_apicid);

    /* 10ms 대기 (Intel MP 스펙 요구) */
    mdelay(10);

    /* 2단계: SIPI 2회 전송 (안전을 위한 재시도) */
    num_starts = 2;
    for (j = 1; j <= num_starts; j++) {
        /* SIPI: Vector = 시작 주소 / 4KB
         * 예: start_eip=0x8000 → Vector=0x08
         *     AP는 CS:IP = 0x0800:0x0000에서 시작 */
        apic_icr_write(APIC_DM_STARTUP |
                        (start_eip >> 12), phys_apicid);

        udelay(300);  /* 300μs 대기 */

        /* Delivery Status 확인 */
        send_status = safe_apic_wait_icr_idle();
        if (send_status)
            pr_err("SIPI #%d not delivered to CPU %d\n",
                    j, phys_apicid);
    }

    return send_status ? -EIO : 0;
}

/* AP 트램폴린 코드 (Real Mode 진입점)
 * arch/x86/realmode/rm/trampoline_64.S:
 *   1. Real Mode → Protected Mode → Long Mode 전환
 *   2. 커널이 준비한 스택/GDT/IDT 로드
 *   3. start_secondary() C 함수 호출
 */

리눅스 커널 IPI 전송 코드

/* 리눅스 커널의 주요 IPI 전송 함수들 */

/* 1. Reschedule IPI: 대상 CPU의 스케줄러 재평가 */
void smp_send_reschedule(int cpu)
{
    /* TIF_NEED_RESCHED 플래그가 설정된 후 호출
     * 대상 CPU가 idle이면 깨워서 스케줄러 실행 */
    apic->send_IPI(cpu, RESCHEDULE_VECTOR);  /* 0xF1 */
}

/* 2. Call Function IPI: 대상 CPU에서 함수 실행 */
void smp_call_function_single(int cpu,
    smp_call_func_t func, void *info, int wait)
{
    /* call_single_data 큐에 함수 등록 후 IPI 전송
     * wait=1이면 대상 CPU 실행 완료까지 대기 */
    generic_exec_single(cpu, func, info, wait);
}

/* 3. TLB Shootdown: 다른 CPU의 TLB 무효화 */
static void flush_tlb_others(
    const struct cpumask *cpumask,
    const struct flush_tlb_info *info)
{
    /* 페이지 테이블 변경 후 다른 CPU의 stale TLB 제거
     * SMP 시스템에서 메모리 일관성 핵심 메커니즘 */
    apic->send_IPI_mask(cpumask, INVALIDATE_TLB_VECTOR);
}

/* 4. x2APIC Self IPI (최소 레이턴시) */
static inline void x2apic_send_IPI_self(int vector)
{
    /* 전용 Self-IPI MSR: 벡터만 지정, ~15 cycles */
    wrmsrl(APIC_SELF_IPI, vector);
}

/* 5. NMI IPI: 디버깅/프로파일링용 */
void apic_send_nmi_to_offline_cpu(unsigned int cpu)
{
    apic->send_IPI(cpu, NMI_VECTOR);
}
IPI 최적화: x2APIC 환경에서 같은 클러스터 내 여러 CPU에 IPI를 보낼 때, Logical Destination 모드의 비트맵을 OR하여 단일 ICR 쓰기로 멀티캐스트할 수 있습니다. 이는 TLB Shootdown처럼 다수 CPU에 동시 전달이 필요한 경우 성능에 큰 영향을 미칩니다. 리눅스 커널의 x2apic_send_IPI_mask_cluster()가 이 최적화를 구현합니다.

APIC Timer

LAPIC에는 프로세서별 독립 타이머가 내장되어 있습니다. 전통적인 8254 PIT이나 HPET을 대체하며, 각 CPU 코어가 독립적인 타이머 인터럽트를 받을 수 있어 멀티프로세서 환경에서 필수적입니다. 세 가지 모드를 지원합니다: One-shot, Periodic, TSC-Deadline.

APIC Timer 3가지 모드 One-shot Mode LVT Timer[18:17] = 00 Initial Count → 카운트다운 0 도달 시 인터럽트 1회 재시작: ICR에 새 값 쓰기 용도: hrtimer, tickless Periodic Mode LVT Timer[18:17] = 01 Initial Count → 카운트다운 0 도달 → 인터럽트 + 자동 리로드 주기적 인터럽트 반복 용도: 주기적 tick TSC-Deadline Mode LVT Timer[18:17] = 10 IA32_TSC_DEADLINE MSR TSC ≥ Deadline → 인터럽트 나노초 정밀도 용도: 고정밀 hrtimer 분주비 (Divide Configuration Register, 0x3E0) One-shot/Periodic: 버스 클럭을 DCR 분주비로 나눈 속도로 카운트 DCR 값: 1, 2, 4, 8, 16, 32, 64, 128 (최대 128분주) TSC-Deadline: DCR/ICR 무시, TSC 비교만 사용 (가장 정밀) CPUID.06H:EAX[2] = 1 이면 TSC-Deadline 모드 지원

APIC Timer 설정 코드

/* One-shot 모드 설정 */
static void lapic_timer_oneshot(u32 count)
{
    /* LVT Timer: One-shot 모드 + 벡터 설정 */
    apic_write(APIC_LVTT, LOCAL_TIMER_VECTOR);  /* [18:17]=00 */

    /* 분주비 설정 (16분주) */
    apic_write(APIC_TDCR, APIC_TDR_DIV_16);

    /* Initial Count 쓰기 → 카운트다운 시작 */
    apic_write(APIC_TMICT, count);
}

/* Periodic 모드 설정 */
static void lapic_timer_periodic(u32 count)
{
    apic_write(APIC_LVTT,
        LOCAL_TIMER_VECTOR | APIC_LVT_TIMER_PERIODIC);  /* [18:17]=01 */
    apic_write(APIC_TDCR, APIC_TDR_DIV_16);
    apic_write(APIC_TMICT, count);
}

/* TSC-Deadline 모드 설정 */
static void lapic_timer_tsc_deadline(u64 deadline)
{
    /* LVT Timer: TSC-Deadline 모드 */
    apic_write(APIC_LVTT,
        LOCAL_TIMER_VECTOR | APIC_LVT_TIMER_TSCDEADL);  /* [18:17]=10 */

    /* IA32_TSC_DEADLINE MSR에 목표 TSC 값 쓰기
     * TSC >= deadline 시 인터럽트 발생
     * 인터럽트 후 자동으로 0 리셋 (재설정 필요) */
    wrmsrl(MSR_IA32_TSC_DEADLINE, deadline);
}

/* 리눅스 clockevent 연동 */
static int lapic_next_deadline(unsigned long delta,
                              struct clock_event_device *evt)
{
    u64 tsc = rdtsc();
    wrmsrl(MSR_IA32_TSC_DEADLINE, tsc + ((u64)delta));
    return 0;
}

APIC Timer 캘리브레이션

One-shot/Periodic 모드에서는 APIC Timer의 클럭 속도를 먼저 측정해야 정확한 시간 간격을 설정할 수 있습니다. 커널은 부팅 시 PIT 또는 TSC를 기준으로 APIC Timer 주파수를 캘리브레이션합니다.

/* arch/x86/kernel/apic/apic.c - APIC Timer 캘리브레이션 */
static void __init calibrate_APIC_clock(void)
{
    struct clock_event_device *levt = this_cpu_ptr(&lapic_events);
    u64 tsc_perj = 0, tsc_start = 0;
    unsigned long deltaj;
    long delta, deltatsc;
    int pm_referenced = 0;

    /* TSC-Deadline 모드 사용 가능하면 캘리브레이션 불필요 */
    if (boot_cpu_has(X86_FEATURE_TSC_DEADLINE_TIMER)) {
        levt->features &= ~CLOCK_EVT_FEAT_DUMMY;
        levt->features |= CLOCK_EVT_FEAT_ONESHOT;
        levt->set_next_event = lapic_next_deadline;
        clockevents_config_and_register(levt,
            tsc_khz * 1000, 0xF, ~0UL);
        return;
    }

    /* APIC Timer를 최대 분주비로 설정하고 알려진 기간 대기 */
    apic_write(APIC_TDCR, APIC_TDR_DIV_1);
    apic_write(APIC_TMICT, ~0U);

    /* jiffies 기반 10ms 대기 */
    deltaj = jiffies_to_msecs(1);
    mdelay(deltaj);

    /* 카운트다운 양으로 주파수 계산 */
    delta = ~0U - apic_read(APIC_TMCCT);

    /* 주파수 = delta / (deltaj_ms / 1000) */
    lapic_timer_period = (delta * 1000) / deltaj;

    pr_info("APIC timer calibrated: %lu.%03lu MHz\n",
            lapic_timer_period / 1000000,
            (lapic_timer_period / 1000) % 1000);
}

clockevent 연동 구조

/* 리눅스 clockevent 장치로 등록 */
static struct clock_event_device lapic_clockevent = {
    .name           = "lapic",
    .features       = CLOCK_EVT_FEAT_PERIODIC |
                      CLOCK_EVT_FEAT_ONESHOT |
                      CLOCK_EVT_FEAT_DUMMY,
    .shift          = 32,
    .set_state_periodic = lapic_timer_set_periodic,
    .set_state_oneshot  = lapic_timer_set_oneshot,
    .set_state_shutdown = lapic_timer_shutdown,
    .set_next_event     = lapic_next_event,
    .broadcast          = lapic_timer_broadcast,
    .rating             = 100,
    .irq                = -1,
};

/* One-shot 모드 진입 */
static int lapic_timer_set_oneshot(struct clock_event_device *evt)
{
    apic_write(APIC_LVTT, LOCAL_TIMER_VECTOR);  /* [18:17]=00 */
    apic_write(APIC_TMICT, 0);  /* 카운터 정지 */
    return 0;
}

/* 다음 이벤트 설정 (hrtimer가 호출) */
static int lapic_next_event(unsigned long delta,
                            struct clock_event_device *evt)
{
    apic_write(APIC_TMICT, delta);
    return 0;
}

/* TSC-Deadline vs One-shot 선택 기준:
 *   1. CPUID.06H:EAX[2] = 1 → TSC-Deadline 지원
 *   2. TSC가 invariant (constant_tsc, nonstop_tsc)
 *   3. 두 조건 모두 만족 시 TSC-Deadline 우선 사용
 *   4. 미지원 시 One-shot + PIT/HPET 캘리브레이션 폴백
 */

APIC Timer 구현 상세

APIC Timer의 각 모드가 내부적으로 어떻게 동작하는지 세부 메커니즘을 분석합니다. 특히 TSC-Deadline 모드의 정밀도 이점과 One-shot 모드의 캘리브레이션 필요성을 이해하는 것이 타이머 관련 문제 진단에 핵심적입니다.

/* APIC Timer 내부 동작 상세 분석
 *
 * ============================================
 * One-shot 모드 내부 동작:
 * ============================================
 * 1. 소프트웨어가 LVT Timer[18:17]=00 설정
 * 2. DCR에 분주비 설정 (예: 16분주)
 * 3. ICR(Initial Count)에 값 쓰기 → 카운트다운 시작
 * 4. 카운터: APIC 버스 클럭 / 분주비 속도로 감소
 * 5. CCR(Current Count)가 0에 도달 → 인터럽트 발생
 * 6. CCR = 0에서 정지 (자동 재시작 없음)
 * 7. 재사용: ICR에 새 값 쓰기로 재시작
 *
 * 클럭 계산:
 *   타이머 주파수 = APIC_Bus_Freq / DCR
 *   인터럽트 주기 = ICR × (DCR / APIC_Bus_Freq) 초
 *   예: APIC_Bus=100MHz, DCR=16, ICR=625000
 *       → 주기 = 625000 × 16 / 100MHz = 100ms
 */

/* TSC-Deadline 모드 상세 동작 */
static void apic_timer_tsc_deadline_detailed(u64 target_ns)
{
    u64 tsc_now = rdtsc();
    u64 tsc_delta;

    /* 나노초 → TSC 틱 변환
     * tsc_khz: 캘리브레이션된 TSC 주파수 (kHz)
     * 예: tsc_khz=3000000 (3GHz), target_ns=1000000 (1ms)
     *     → delta = 3000000 * 1000000 / 1000000000 = 3000000 ticks */
    tsc_delta = div_u64((u64)tsc_khz * target_ns, 1000000ULL);

    /* LVT Timer: TSC-Deadline 모드 ([18:17]=10) */
    apic_write(APIC_LVTT,
        LOCAL_TIMER_VECTOR | APIC_LVT_TIMER_TSCDEADL);

    /* IA32_TSC_DEADLINE MSR (0x6E0)에 목표 TSC 쓰기
     *
     * 동작 원리:
     * - CPU는 매 클럭 사이클마다 TSC >= Deadline 비교
     * - 조건 만족 시 LAPIC에 타이머 인터럽트 신호
     * - ICR/CCR/DCR과 완전히 독립 (무시됨)
     * - 인터럽트 후 Deadline MSR은 자동으로 0 리셋
     *
     * 장점:
     * - 나노초 수준 정밀도 (TSC 해상도)
     * - C-state 영향 없음 (invariant TSC)
     * - 캘리브레이션 불필요 (TSC 자체가 기준)
     * - APIC 버스 클럭 의존성 없음
     */
    wrmsrl(MSR_IA32_TSC_DEADLINE, tsc_now + tsc_delta);
}

/* APIC Timer 캘리브레이션 상세 (One-shot/Periodic용)
 *
 * 문제: APIC 버스 클럭 주파수가 하드웨어마다 다르고,
 *       CPUID로 직접 조회할 수 없는 경우가 많음
 *
 * 해결: 알려진 시간 소스(PIT, PM Timer, TSC)를 기준으로 측정
 *
 * 방법 1: PIT 기반 (가장 레거시)
 *   - PIT를 10ms 간격으로 설정
 *   - APIC Timer를 최대 카운트로 시작
 *   - 10ms 후 카운트 감소량으로 주파수 계산
 *
 * 방법 2: PM Timer 기반 (더 정확)
 *   - ACPI PM Timer (3.579545 MHz 고정)를 기준 클럭으로 사용
 *   - ~100ms 측정으로 높은 정확도
 *
 * 방법 3: CPUID Leaf 0x15/0x16 (최신 CPU)
 *   - Intel Skylake+: TSC/Core Crystal 주파수 직접 제공
 *   - 캘리브레이션 불필요
 */
static void __init calibrate_apic_timer_pm(void)
{
    u32 pm_start, pm_end, pm_delta;
    u32 apic_start, apic_end, apic_delta;
    u32 pm_100ms;

    /* PM Timer: 3.579545 MHz */
    pm_100ms = 357954;  /* 100ms에 해당하는 PM 틱 */

    /* APIC Timer: 분주비 1, 최대 카운트 */
    apic_write(APIC_TDCR, APIC_TDR_DIV_1);
    apic_write(APIC_TMICT, 0xFFFFFFFF);

    /* PM Timer 시작 */
    pm_start = acpi_pm_read_early();

    /* 100ms 대기 (PM Timer 기준) */
    do {
        pm_end = acpi_pm_read_early();
        pm_delta = (pm_end - pm_start) & ACPI_PM_MASK;
    } while (pm_delta < pm_100ms);

    /* APIC 카운트 감소량 */
    apic_delta = 0xFFFFFFFF - apic_read(APIC_TMCCT);

    /* 주파수 = apic_delta / 0.1초 */
    lapic_timer_period = apic_delta * 10;  /* Hz */

    pr_info("APIC Timer calibrated via PM Timer: %lu Hz\n",
            lapic_timer_period);
}

APIC Timer와 전원 관리(Power Management) 상호작용

C-StateAPIC Timer 동작TSC-Deadline 동작영향
C1 (HALT) 정상 카운트다운 정상 비교 없음
C1E 정상 (대부분 CPU) 정상 약간의 exit latency
C3 (Sleep) 정지! (APIC 클럭 중단) TSC invariant면 정상 One-shot 기한 초과 가능
C6 (Deep Sleep) 정지 + 리셋 가능 TSC nonstop_tsc면 정상 LAPIC 레지스터 복원 필요
C3+ 상태와 APIC Timer: C3 이상 C-State에서는 APIC 버스 클럭이 중단되어 One-shot/Periodic 카운터가 멈춥니다. TSC-Deadline 모드는 invariant TSC를 사용하므로 C-State 영향을 받지 않아 tickless 커널에서 필수적입니다. 커널의 CLOCK_EVT_FEAT_C3STOP 플래그가 이 상황을 표시하며, lapic_timer_broadcast를 통해 대체 타이머(HPET)가 보조합니다.

MSI/MSI-X

MSI(Message Signaled Interrupts)와 MSI-X는 PCIe 장치가 I/O APIC을 우회하여 메모리 쓰기(Memory Write Transaction)로 직접 LAPIC에 인터럽트를 전달하는 현대적 방식입니다. 물리적인 인터럽트 라인이 필요 없으며, 벡터 수도 크게 확장됩니다.

MSI Address/Data 인코딩 Message Address (32-bit) [31:20] = 0xFEE [19:12] Destination ID [11:4] Rsvd [3] RH [2] DM [1:0] Reserved (00) Message Data (32-bit) [31:16] Reserved [15] Trigger [14] Level [13:11] Rsvd [10:8] Delivery Mode [7:0] Vector MSI PCI Configuration Space에 위치 최대 32개 벡터 (Multiple Message) 벡터: 연속 범위만 할당 가능 Destination: 단일 값 공유 마스킹: MSI-Per-Vector 옵션 BAR 사용 안 함 Capability ID: 0x05 MSI-X BAR 공간에 MSI-X Table 위치 최대 2,048개 벡터 벡터: 개별 독립 할당 가능 Destination: 벡터별 개별 설정 마스킹: 벡터별 Mask 비트 Table BAR + PBA(Pending Bit Array) BAR Capability ID: 0x11
/* MSI-X Table Entry 구조 (BAR 공간, 16바이트/엔트리) */
struct msix_entry {
    u32 msg_addr_lo;    /* [31:0]  Message Address Low */
    u32 msg_addr_hi;    /* [63:32] Message Address High (0 for 32-bit) */
    u32 msg_data;       /* [31:0]  Message Data */
    u32 vector_ctrl;    /* [0] Mask bit (1=마스크) */
};

/* 리눅스 MSI/MSI-X 설정 API */
int pci_alloc_irq_vectors(struct pci_dev *dev,
                          unsigned int min_vecs,
                          unsigned int max_vecs,
                          unsigned int flags);

/* 사용 예시 */
int nvecs = pci_alloc_irq_vectors(pdev, 1, num_cpus,
                PCI_IRQ_MSIX | PCI_IRQ_MSI | PCI_IRQ_LEGACY);
if (nvecs < 0)
    return nvecs;

/* 벡터별 IRQ 번호 조회 */
int irq = pci_irq_vector(pdev, vector_index);

/* IRQ 핸들러 등록 */
request_irq(irq, my_handler, 0, "my_device", priv);

/* 해제 */
pci_free_irq_vectors(pdev);
Interrupt Remapping과 MSI: VT-d/IOMMU의 Interrupt Remapping이 활성화되면, MSI Address/Data가 Remapping Table 인덱스로 재해석됩니다. Address[4:3] = 1(Remappable Format), Data[15:0] = IRTE 인덱스. 이를 통해 DMA 공격에 의한 인터럽트 주입을 방지합니다.

Interrupt Remapping

인터럽트 리매핑(Interrupt Remapping, IR)은 Intel VT-d 또는 AMD IOMMU가 제공하는 하드웨어 기능으로, MSI/MSI-X와 I/O APIC 인터럽트를 IOMMU의 리매핑 테이블(Remapping Table)을 통해 변환합니다. 이를 통해 DMA 공격에 의한 임의 인터럽트 주입을 방지하고, x2APIC의 32비트 Destination을 지원합니다.

Interrupt Remapping 아키텍처

Interrupt Remapping 아키텍처 PCIe 장치 MSI Write Addr + Data IOMMU (VT-d / AMD-Vi) Interrupt Remapping Table (IRT) IRTE 인덱스 조회 → 실제 Vector, Dest, Mode 결정 소스 ID 검증 (BDF 확인) 유효하지 않으면 → 차단 LAPIC 실제 인터럽트 수신 변환된 Vector/Dest 악성 DMA 위조된 MSI 쓰기 Compatibility Format (기존) Address[3:2] = 0x (non-remappable) Address[19:12] = Destination APIC ID Data[7:0] = Vector Data[10:8] = Delivery Mode → 장치가 직접 LAPIC ID/Vector 지정 → DMA 공격에 취약 Remappable Format (IR 활성) Address[4:3] = 1x (remappable 표시) Address[19:5] + Data[15:0] = IRTE Index Address[2] = SubHandle Valid IOMMU가 IRTE에서 실제 값 조회 → 장치는 인덱스만 지정 (Vector/ID 모름) → 소스 ID(BDF) 검증으로 위조 차단

IRTE (Interrupt Remapping Table Entry) 구조

IRTE (Interrupt Remapping Table Entry) 128-bit 하위 64비트 [63:0] [0] P [1] FPD [2] DM [3] RH [4] TM [7:5] Del.Mode [11:8] AVAIL [15:8] Vector [31:16] Reserved [63:32] Destination ID (32-bit) 상위 64비트 [127:64] [79:64] Source ID (Bus:Dev:Func) 장치 BDF 검증용 [81:80] SQ Source Qual. [97:82] SVT Source Valid. Type [127:98] Reserved 주요 필드: P (Present): 1이면 유효한 IRTE. 0이면 인터럽트 차단 (Blocked) FPD (Fault Processing Disable): 1이면 fault 기록 비활성화 Source ID: 장치의 PCI Bus:Device:Function. SVT/SQ와 조합하여 소스 검증 수행
/* Intel VT-d Interrupt Remapping Table Entry (IRTE) */
struct irte {
    union {
        struct {
            u64 present         : 1;   /* IRTE 유효 여부 */
            u64 fpd             : 1;   /* Fault Processing Disable */
            u64 dst_mode        : 1;   /* 0=Physical, 1=Logical */
            u64 redir_hint      : 1;   /* Redirection Hint */
            u64 trigger_mode    : 1;   /* 0=Edge, 1=Level */
            u64 dlvry_mode      : 3;   /* Delivery Mode (Fixed/LP) */
            u64 avail           : 4;   /* Available for software */
            u64 rsvd_1          : 3;
            u64 irte_mode       : 1;   /* 0=Remapped, 1=Posted */
            u64 vector          : 8;   /* Interrupt Vector */
            u64 rsvd_2          : 8;
            u64 dest_id         : 32;  /* Destination APIC ID */
        };
        u64 low;
    };
    union {
        struct {
            u64 sid             : 16;  /* Source ID (BDF) */
            u64 sq              : 2;   /* Source Qualification */
            u64 svt             : 2;   /* Source Validation Type */
            u64 rsvd_3          : 44;
        };
        u64 high;
    };
};

/* IRTE 설정 예시 */
static void setup_irte(struct irte *irte, int vector,
                      u32 dest_apicid, u16 source_bdf)
{
    memset(irte, 0, sizeof(*irte));

    irte->present = 1;
    irte->dst_mode = 0;        /* Physical */
    irte->trigger_mode = 0;    /* Edge */
    irte->dlvry_mode = 0;      /* Fixed */
    irte->vector = vector;
    irte->dest_id = dest_apicid;

    /* Source Validation: 장치 BDF 확인 */
    irte->sid = source_bdf;
    irte->svt = 1;   /* Source ID 검증 활성화 */
    irte->sq = 0;    /* 전체 BDF 일치 필요 */
}

Posted Interrupt (VT-d IR과 APICv 연동)

IRTE의 irte_mode=1로 설정하면, 외부 장치의 인터럽트가 IOMMU를 통해 직접 vCPU의 PID(Posted Interrupt Descriptor)에 기록됩니다. 이는 KVM의 Device Passthrough(장치 패스스루) 환경에서 호스트 개입 없이 게스트에 인터럽트를 전달하는 제로카피(Zero-Copy) 인터럽트 경로입니다.

흐름 단계Remapped ModePosted Mode
1. 장치 MSI 쓰기 IOMMU → IRTE 조회 IOMMU → IRTE 조회 (Posted)
2. 인터럽트 전달 IOMMU → LAPIC (호스트 벡터) IOMMU → PID PIR 비트 직접 세팅
3. 호스트 처리 KVM 핸들러 → vIRR 세팅 불필요 (하드웨어 직접)
4. 게스트 수신 VM Entry 시 주입 Guest 실행 중 직접 주입 가능
성능 VM Exit 1회 필요 VM Exit 0회 (최적)
Interrupt Remapping 없이 x2APIC 사용 시 보안 위험: x2APIC은 32비트 Destination을 사용하므로, IR 없이는 악성 장치가 MSI 메모리 쓰기를 통해 시스템의 임의 CPU에 임의 벡터의 인터럽트를 주입할 수 있습니다. 이는 커널 코드 실행 흐름 조작, 권한 상승(Privilege Escalation), DoS 공격에 악용될 수 있습니다. 리눅스 커널은 IR 미지원 시 x2APIC 활성화를 거부하며, intremap=off 옵션은 보안을 심각하게 약화시킵니다.

Interrupt Remapping 커널 구현 상세

리눅스 커널에서 Interrupt Remapping이 초기화되는 과정과 IRTE 관리 방법을 분석합니다. Intel VT-d와 AMD IOMMU의 IR 초기화 경로는 다르지만, 최종적으로 irq_remapping_ops 추상화를 통해 통합됩니다.

/* Interrupt Remapping 초기화 흐름
 *
 * 부트 시퀀스:
 * 1. ACPI DMAR/IVRS 테이블 파싱 → IOMMU 하드웨어 감지
 * 2. irq_remapping_prepare() → IR 지원 여부 확인
 * 3. irq_remapping_enable() → IR 활성화
 * 4. x2APIC 활성화 (IR이 전제 조건)
 *
 * drivers/iommu/intel/irq_remapping.c
 * drivers/iommu/amd/iommu.c
 */

/* IR 추상화 인터페이스 */
struct irq_remap_ops {
    /* 하드웨어 지원 확인 */
    int  (*supported)(void);

    /* IR 하드웨어 준비 */
    int  (*prepare)(void);

    /* IR 활성화 */
    int  (*enable)(void);

    /* IRTE 할당/해제 */
    int  (*alloc_irte)(struct irq_domain *domain,
                      unsigned int virq,
                      irq_hw_number_t hwirq);
    void (*free_irte)(unsigned int virq);

    /* MSI 메시지 구성 (Remappable Format) */
    void (*compose_msi_msg)(struct irq_data *data,
                           struct msi_msg *msg);
};

/* Intel VT-d IR 활성화 */
static int intel_enable_irq_remapping(void)
{
    struct dmar_drhd_unit *drhd;
    int setup = 0;

    /* 각 DRHD(DMA Remapping Hardware Definition) 유닛 순회 */
    for_each_drhd_unit(drhd) {
        struct intel_iommu *iommu = drhd->iommu;

        /* Interrupt Remapping Table 할당
         * 크기: 최대 64K 엔트리 (각 16바이트 = 1MB) */
        if (!iommu->ir_table) {
            iommu->ir_table = kzalloc(
                sizeof(struct ir_table), GFP_KERNEL);
            iommu->ir_table->base = alloc_pages_exact(
                IR_TABLE_SIZE, GFP_KERNEL | __GFP_ZERO);
        }

        /* IRTA(Interrupt Remapping Table Address) 레지스터 설정 */
        iommu_set_irta(iommu);

        /* Global Command 레지스터로 IR 활성화 */
        iommu_set_gcmd(iommu, DMA_GCMD_IRE);

        /* Compatibility Format 인터럽트 차단
         * (보안: 비-Remappable MSI 거부) */
        iommu_set_gcmd(iommu, DMA_GCMD_CFI);

        setup++;
    }

    if (!setup) {
        pr_err("No IOMMU supports Interrupt Remapping\n");
        return -ENODEV;
    }

    pr_info("Enabled IRQ remapping in %s mode\n",
            x2apic_enabled() ? "x2apic" : "xapic");
    return 0;
}

/* Remappable Format MSI 메시지 구성
 * IR 활성 시 MSI Address/Data가 IRTE 인덱스로 변환됨 */
static void intel_compose_remapped_msi_msg(
    struct irq_data *data, struct msi_msg *msg)
{
    u16 irte_index = data->hwirq;  /* IRTE 인덱스 */

    /* Remappable Format Address:
     * [31:20] = 0xFEE (LAPIC 주소 범위)
     * [19:5]  = IRTE Index[19:5]
     * [4]     = 1 (Remappable Format 표시)
     * [3]     = IRTE Index[15] (SubHandle Valid)
     * [2]     = 1 (Interrupt Format)
     * [1:0]   = 0 */
    msg->address_lo = MSI_ADDR_BASE_LO |
        ((irte_index & 0x7FFF) << 5) |
        MSI_ADDR_IR_INDEX1(irte_index) |
        MSI_ADDR_IR_SHV |
        MSI_ADDR_IR_EXT_INT;

    /* Remappable Format Data:
     * [15:0] = SubHandle (IRTE Index 하위 비트) */
    msg->data = (irte_index & 0xFFFF);

    msg->address_hi = MSI_ADDR_BASE_HI;
}
IR 상태 진단: dmesg | grep -i "interrupt remapping\|DMAR-IR\|IOMMU.*IR"로 IR 활성화 상태를 확인할 수 있습니다. /sys/class/iommu/ 디렉터리에서 각 IOMMU 유닛의 상세 정보를 볼 수 있으며, cat /sys/kernel/debug/iommu/intel/ir_translation_struct(디버그 빌드)로 IRTE 테이블 내용을 덤프할 수 있습니다.

Intel APICv (APIC Virtualization)

Intel APICv는 VT-x의 확장으로, 가상 머신의 APIC 접근을 하드웨어에서 직접 처리하여 VM Exit 오버헤드를 제거합니다. 주요 기능은 Virtual-APIC Page, TPR Shadow, Virtual Interrupt Delivery, Process Posted Interrupts입니다.

APICv Virtual Interrupt Delivery Guest VM APIC 접근 (MMIO/MSR) TPR/EOI/Self-IPI → Virtual-APIC Page VM Exit 없음! (하드웨어 직접 처리) Virtual-APIC Page 4KB 가상 APIC 레지스터 vTPR, vPPR, vEOI vIRR, vISR (256-bit) vICR (Guest IPI) VMCS에 물리 주소 기록 HW Interrupt Evaluation SVI (Servicing Vector) RVI (Requesting Vector) RVI[7:4] > vPPR[7:4] → Guest IDT 직접 전달 VM Exit 없이 벡터 주입 Host / KVM PID (Posted Interrupt Desc) Notification Vector 설정 PI Handler에서 PID 처리 → vIRR 업데이트 External 인터럽트 주입 경로 VMCS Controls Pin: Process Posted Interrupts Secondary: Virtual Interrupt Delivery Secondary: APIC Register Virtualization Secondary: Virtualize x2APIC Mode TPR Threshold Posted-Interrupt Notification Vector VM Entry
PID (Posted Interrupt Descriptor) 512-bit [255:0] PIR (Posted Interrupt Requests) 256-bit 벡터 비트맵 (IRR과 동일 구조) [256] ON [257] SN [511:288] NDST + NV 필드 설명: PIR [255:0]: 각 벡터에 대한 pending 비트맵 (외부에서 atomic OR로 세팅) ON [256]: Outstanding Notification - 1이면 notification IPI 발송 필요 SN [257]: Suppress Notification - 1이면 notification IPI 억제 (vCPU가 실행 중이 아닐 때) NDST [511:288]: Notification Destination (물리적 APIC ID), NV: Notification Vector
/* KVM의 APICv Posted Interrupt 처리 */
struct pi_desc {
    u32 pir[8];        /* Posted Interrupt Requests (256-bit) */
    union {
        struct {
            u16 on      : 1;  /* Outstanding Notification */
            u16 sn      : 1;  /* Suppress Notification */
            u16 rsvd    : 14;
            u8  nv;           /* Notification Vector */
            u8  rsvd2;
            u32 ndst;         /* Notification Destination */
        };
        u64 control;
    };
} __attribute__((aligned(64)));

/* Posted Interrupt 주입 흐름:
 * 1. 외부 장치 인터럽트 → KVM 핸들러
 * 2. PIR[vector] 비트 atomic OR 세팅
 * 3. ON 비트가 0이면 1로 세팅하고 notification IPI 발송
 * 4. 타겟 CPU의 PI handler가 PIR → vIRR로 병합
 * 5. Guest는 VM Exit 없이 인터럽트 수신
 */
static void vmx_deliver_posted_interrupt(struct kvm_vcpu *vcpu, int vector)
{
    struct vcpu_vmx *vmx = to_vmx(vcpu);
    struct pi_desc *pid = &vmx->pi_desc;

    /* PIR 비트 세팅 */
    if (pi_test_and_set_pir(vector, pid))
        return;  /* 이미 세팅됨 */

    /* ON 비트 세팅 + notification IPI */
    if (!pi_test_and_set_on(pid))
        apic_send_IPI(vcpu_to_pcpu(vcpu), POSTED_INTR_VECTOR);
}

AMD AVIC (Advanced Virtual Interrupt Controller)

AMD AVIC은 AMD-V(SVM)의 확장으로, Intel APICv에 대응하는 APIC 가상화 기능입니다. Backing Page, Physical/Logical APIC ID Table, Doorbell Mechanism을 사용하여 Guest의 APIC 접근을 하드웨어에서 직접 처리합니다.

AMD AVIC 아키텍처 Backing Page 4KB per vCPU 가상 APIC 레지스터 Guest MMIO 접근 → Backing Page R/W VM Exit 최소화 Physical APIC ID Table 4KB (512 엔트리) 각 엔트리: 8바이트 vCPU Running 상태 Backing Page 포인터 Guest_Phys_APIC_ID → vCPU 매핑 Doorbell Mechanism AVIC_DOORBELL MSR 호스트 → Guest 시그널 vCPU 실행 중: 직접 전달 vCPU 대기 중: 스케줄 요청 IPI 가상화의 핵심 GA Table (Guest Address Table) IOMMU IR 연동 Device → Guest 직접 전달 Passthrough 장치용 Logical APIC ID Table 4KB (512 엔트리) Logical Dest → vCPU 매핑 Flat/Cluster 모드 지원 Lowest Priority 라우팅 VMCB 설정 AVIC Enable (VMCB[0x060] bit 31) Backing Page PA Physical Table PA Logical Table PA

AVIC vs APICv 비교

기능Intel APICvAMD AVIC
가상 APIC 저장Virtual-APIC PageBacking Page
TPR 가상화TPR ShadowBacking Page TPR
인터럽트 주입Posted Interrupts (PID)Doorbell + GA Table
IPI 가상화ICR 쓰기 → HW 처리ICR → Doorbell
EOI 가상화Virtual Interrupt DeliveryEOI intercept 최적화
Passthrough IRVT-d Posted InterruptGA Table + IOMMU IR
VMCS/VMCB 필드PI Descriptor AddressBacking/Physical/Logical PA
Self IPIx2APIC Self IPI 가속Backing Page 직접 업데이트
/* KVM의 AVIC 초기화 (arch/x86/kvm/svm/avic.c) */
static int avic_init_vcpu(struct vcpu_svm *svm)
{
    struct kvm_vcpu *vcpu = &svm->vcpu;
    struct page *page;

    /* Backing Page 할당 (4KB per vCPU) */
    page = alloc_page(GFP_KERNEL_ACCOUNT | __GFP_ZERO);
    if (!page)
        return -ENOMEM;

    svm->avic_backing_page = page;

    /* VMCB에 Backing Page 물리 주소 설정 */
    svm->vmcb->control.avic_backing_page =
        page_to_phys(page) | AVIC_HPA_MASK;

    /* Physical APIC ID Table 엔트리 설정 */
    avic_update_physical_id_entry(vcpu, vcpu->vcpu_id);

    return 0;
}

/* AVIC Doorbell 전송 (IPI 가상화) */
static void avic_ring_doorbell(struct kvm_vcpu *vcpu)
{
    int cpu = READ_ONCE(vcpu->cpu);
    if (cpu != -1)
        wrmsrl(MSR_AMD64_SVM_AVIC_DOORBELL, cpu_to_apicid(cpu));
}

APICv vs AVIC 성능 비교

APIC 가상화의 핵심 목표는 VM Exit 제거입니다. 아래 표는 주요 APIC 연산별로 소프트웨어 에뮬레이션, Intel APICv, AMD AVIC의 VM Exit 발생 여부를 비교합니다.

APIC 연산SW 에뮬레이션Intel APICvAMD AVIC
TPR 읽기/쓰기VM Exit (MMIO trap)Exit 없음 (TPR Shadow)Exit 없음 (Backing Page)
EOI 쓰기VM Exit → ISR 갱신Exit 없음 (Virtual Interrupt Delivery)조건부 Exit (최적화 적용 시 생략 가능)
Self IPIVM Exit → ICR 에뮬레이션Exit 없음 (x2APIC Self-IPI 가속)Exit 없음 (Backing Page 직접 업데이트)
타 vCPU IPIVM Exit → 대상 vCPU 인터럽트 주입Exit 없음 (ICR → Posted Interrupt)VM Exit → Doorbell MSR 전송
외부 인터럽트 주입VM Exit → 소프트웨어 주입Exit 없음 (Posted Interrupt Descriptor)Doorbell + GA Table (Exit 최소화)
APIC 레지스터 읽기VM Exit (MMIO trap)Exit 없음 (Virtual-APIC Page)Exit 없음 (Backing Page)
벤치마크 측정: perf kvm stat으로 VM Exit 이벤트를 카운트하여 APICv/AVIC 효과를 정량적으로 확인할 수 있습니다.
# VM Exit 이벤트별 통계 (호스트에서 실행)
perf kvm stat record -a sleep 10
perf kvm stat report --event=vmexit

# APICv/AVIC 활성 시 APIC_ACCESS, EOI 관련 Exit가 크게 감소해야 합니다.
# AVIC에서는 IPI_WRITE Exit가 남을 수 있음 (Doorbell 기반 IPI)
/* arch/x86/kvm/svm/avic.c — AVIC Incomplete IPI 처리 */
static int avic_incomplete_ipi_interception(struct kvm_vcpu *vcpu)
{
    struct vcpu_svm *svm = to_svm(vcpu);
    u32 icrh = svm->vmcb->control.exit_info_1 >> 32;
    u32 icrl = svm->vmcb->control.exit_info_1;
    u32 id  = svm->vmcb->control.exit_info_2 >> 32;

    /* AVIC가 IPI를 완전히 처리하지 못한 경우
     * (대상 vCPU가 다른 물리 CPU에서 실행 중이거나 중지 상태)
     * KVM이 소프트웨어로 폴백하여 IPI를 전달 */
    switch (id) {
    case AVIC_IPI_FAILURE_INVALID_TARGET:
        break;
    case AVIC_IPI_FAILURE_TARGET_NOT_RUNNING:
        /* 대상 vCPU 깨우기 → Doorbell 재전송 */
        kvm_vcpu_kick(avic_get_target_vcpu(vcpu, icrl, icrh));
        break;
    }
    return 1;
}
코드 설명

AVIC는 IPI 전달을 하드웨어로 시도하지만, 대상 vCPU가 실행 중이 아닌 경우 AVIC_IPI_FAILURE_TARGET_NOT_RUNNING 이유로 VM Exit가 발생합니다. 이때 KVM이 소프트웨어로 폴백하여 대상 vCPU를 깨우고 인터럽트를 전달합니다. Intel APICv의 Posted Interrupt는 PID를 통해 비실행 vCPU에도 인터럽트를 기록할 수 있어 이 Exit가 발생하지 않는다는 점이 주요 차이입니다.

KVM LAPIC 에뮬레이션

KVM은 게스트 VM에 가상 LAPIC을 제공합니다. APICv/AVIC 미지원 시 소프트웨어 에뮬레이션으로, 지원 시 하드웨어 가속과 소프트웨어 폴백을 혼합합니다. struct kvm_lapic이 핵심 자료구조입니다.

/* arch/x86/kvm/lapic.h - KVM LAPIC 구조체 */
struct kvm_lapic {
    unsigned long base_address;       /* Guest가 보는 LAPIC 기본 주소 */
    struct kvm_io_device dev;          /* MMIO exit 처리용 */
    struct kvm_timer lapic_timer;     /* APIC 타이머 에뮬레이션 */
    u32 divide_count;                  /* 타이머 분주비 */
    struct kvm_vcpu *vcpu;            /* 소유 vCPU */
    bool sw_enabled;                   /* SVR[8] 소프트웨어 활성화 */
    bool irr_pending;                  /* IRR에 pending 인터럽트 있음 */
    bool lvt_lint0_watch;

    /* 가상 APIC 레지스터 페이지 (4KB)
     * APICv에서는 Virtual-APIC Page로 사용
     * 소프트웨어 에뮬레이션에서는 이 버퍼에서 직접 R/W */
    void *regs;

    /* 최고 ISR 벡터 캐시 (PPR 계산 최적화) */
    int highest_isr_cache;
};

/* Guest의 APIC MMIO 접근 처리 (APICv 미사용 시) */
static int apic_mmio_write(struct kvm_vcpu *vcpu,
                           struct kvm_io_device *dev,
                           gpa_t addr, int len, const void *val)
{
    u32 offset = addr & 0xFF0;
    u32 data = *(u32 *)val;

    switch (offset) {
    case APIC_EOI:
        apic_set_eoi(vcpu->arch.apic);
        break;
    case APIC_TASKPRI:
        apic_set_tpr(vcpu->arch.apic, data);
        break;
    case APIC_ICR:
        apic_send_ipi(vcpu->arch.apic, data);
        break;
    case APIC_TMICT:
        start_apic_timer(vcpu->arch.apic, data);
        break;
    /* ... 기타 레지스터 ... */
    }
    return 0;
}

/* 인터럽트 주입 가능 여부 확인 */
bool kvm_apic_has_interrupt(struct kvm_vcpu *vcpu)
{
    struct kvm_lapic *apic = vcpu->arch.apic;
    int highest_irr;

    if (!kvm_apic_present(vcpu))
        return false;

    highest_irr = apic_find_highest_irr(apic);
    if (highest_irr == -1)
        return false;

    /* PPR보다 높은 우선순위인지 확인 */
    return (highest_irr >> 4) > (kvm_apic_get_reg(apic, APIC_PROCPRI) >> 4);
}
APICv/AVIC 가속과 폴백: 하드웨어 가속이 가능한 경우(APICv/AVIC), TPR 읽기/쓰기, EOI, Self-IPI 등은 VM Exit 없이 처리됩니다. 그러나 ICR을 통한 Broadcast IPI, 특정 LVT 설정 변경, I/O APIC 관련 작업 등은 여전히 VM Exit를 유발할 수 있으며 KVM이 소프트웨어로 처리합니다.

APICv Posted Interrupt 전체 흐름 상세

APICv의 Posted Interrupt 메커니즘이 외부 장치 인터럽트를 VM Exit 없이 게스트에 전달하는 전체 흐름을 단계별로 분석합니다. 특히 Device Passthrough(VFIO) 환경에서 VT-d IR의 Posted Mode IRTE와 APICv의 PID가 연동되는 과정이 핵심입니다.

/* APICv Posted Interrupt 전체 흐름 (Device Passthrough)
 *
 * ============================================
 * 경로 1: vCPU가 실행 중인 경우 (최적 경로)
 * ============================================
 * 1. 패스스루 장치가 MSI Write 생성
 *    → Address/Data = Remappable Format (IRTE Index)
 *
 * 2. IOMMU가 MSI를 가로챔
 *    → IRTE 조회 (irte_mode=1, Posted Mode)
 *    → IRTE에서 PID 물리 주소 + Posted Vector 획득
 *
 * 3. IOMMU가 PID의 PIR[vector] 비트를 atomic OR로 세팅
 *    → PID.ON 비트가 0이면 1로 세팅
 *    → PID.NDST의 물리 CPU에 Notification Vector IPI 전송
 *
 * 4. 타겟 물리 CPU가 Notification Vector 수신
 *    → CPU가 Guest 모드(Non-Root Operation) 실행 중이면:
 *      하드웨어가 PIR → vIRR로 자동 병합
 *      Guest IDT 핸들러 직접 실행 (VM Exit 없음!)
 *
 * ============================================
 * 경로 2: vCPU가 실행 중이 아닌 경우
 * ============================================
 * 1-3. 위와 동일
 * 4. Notification Vector IPI가 호스트 핸들러에서 처리
 *    → KVM의 pi_wakeup_handler()
 *    → vCPU를 스케줄 큐에 넣고 kick
 * 5. vCPU가 VM Entry 시 PIR → vIRR 병합
 *    → Guest에서 인터럽트 처리
 *    (VM Exit 1회 발생 - vCPU 비실행 상태의 불가피한 오버헤드)
 */

/* KVM APICv 초기화 (arch/x86/kvm/vmx/vmx.c) */
static void vmx_vcpu_setup_apicv(struct kvm_vcpu *vcpu)
{
    struct vcpu_vmx *vmx = to_vmx(vcpu);

    /* VMCS Secondary Processor-Based Controls 설정 */

    /* Virtual-Interrupt Delivery: vIRR/vISR/vEOI 하드웨어 처리 */
    vmcs_set_secondary_exec(SECONDARY_EXEC_VIRTUAL_INTR_DELIVERY);

    /* APIC-Register Virtualization: 모든 APIC 레지스터 읽기 가속 */
    vmcs_set_secondary_exec(SECONDARY_EXEC_APIC_REGISTER_VIRT);

    /* Process Posted Interrupts: PID 기반 인터럽트 주입 */
    vmcs_set_pin_based(PIN_BASED_POSTED_INTR);

    /* PID(Posted Interrupt Descriptor) 주소를 VMCS에 기록 */
    vmcs_write64(POSTED_INTR_DESC_ADDR,
                  __pa(&vmx->pi_desc));

    /* Notification Vector 설정 (호스트 IDT 핸들러) */
    vmcs_write16(POSTED_INTR_NV, POSTED_INTR_VECTOR);

    /* Virtual-APIC Page 주소 */
    vmcs_write64(VIRTUAL_APIC_PAGE_ADDR,
                  page_to_phys(vcpu->arch.apic->regs_page));

    /* SVI(Servicing Vector Info) / RVI(Requesting Vector Info)
     * 하드웨어가 자동으로 관리하는 VMCS 필드
     * Guest의 인서비스/펜딩 벡터를 추적 */
    vmcs_write16(GUEST_INTR_STATUS, 0);
}

/* Posted Interrupt Notification 핸들러 (vCPU 비실행 시) */
static void pi_wakeup_handler(void)
{
    int cpu = smp_processor_id();
    struct kvm_vcpu *vcpu;

    /* 이 CPU에 바인딩된 vCPU 중 PID.ON=1인 것 찾기 */
    list_for_each_entry(vcpu, &per_cpu(wakeup_vcpus, cpu), wakeup_list) {
        struct pi_desc *pid = vcpu_to_pid(vcpu);

        if (pi_test_on(pid))
            kvm_vcpu_kick(vcpu);  /* vCPU 깨우기 */
    }
}

KVM APIC Timer 에뮬레이션

/* KVM의 APIC Timer 에뮬레이션 */
struct kvm_timer {
    struct hrtimer timer;          /* 호스트 hrtimer 사용 */
    s64 period;                     /* 주기 (나노초) */
    ktime_t target_expiration;      /* 다음 만료 시각 */
    u32 timer_mode;                 /* One-shot/Periodic/TSC-Deadline */
    u32 timer_mode_mask;
    u64 tscdeadline;                /* TSC-Deadline 목표값 */
    u64 expired_tscdeadline;
    atomic_t pending;               /* 미처리 타이머 인터럽트 수 */
    bool hv_timer_in_use;           /* HW preemption timer 사용 여부 */
};

/* Guest가 APIC Timer ICR에 쓸 때 호출 */
static void start_apic_timer(struct kvm_lapic *apic)
{
    struct kvm_timer *ktimer = &apic->lapic_timer;
    u64 ns;

    if (ktimer->timer_mode == APIC_LVT_TIMER_TSCDEADL) {
        /* TSC-Deadline: HW preemption timer 사용 시도 (VM Exit 없이 만료) */
        if (kvm_can_use_hv_timer(apic->vcpu)) {
            ktimer->hv_timer_in_use = true;
            kvm_set_hv_timer(apic->vcpu, ktimer->tscdeadline);
            return;
        }
        /* 폴백: TSC → 나노초 변환 후 hrtimer 사용 */
        ns = kvm_scale_tsc(ktimer->tscdeadline - kvm_read_l1_tsc(apic->vcpu));
    } else {
        /* One-shot/Periodic: 카운트 × 분주비 → 나노초 */
        ns = (u64)kvm_apic_get_reg(apic, APIC_TMICT) *
             APIC_BUS_CYCLE_NS * apic->divide_count;
    }

    hrtimer_start(&ktimer->timer, ktime_add_ns(ktime_get(), ns),
                  HRTIMER_MODE_ABS_HARD);
}

가상화 레벨별 APIC 처리 비교

Guest APIC 연산SW 에뮬레이션APICv/AVICVM Exit?
TPR 읽기/쓰기 MMIO exit → KVM 처리 Virtual-APIC Page 직접 SW: Yes / HW: No
EOI 쓰기 MMIO exit → vISR 클리어 Virtual Interrupt Delivery SW: Yes / HW: No
Self IPI MMIO exit → vIRR 세팅 x2APIC Self-IPI 가속 SW: Yes / HW: No
IPI (동일 VM) ICR exit → 대상 vCPU kick ICR 가속 + Doorbell/PI SW: Yes / HW: 일부
LVT 설정 MMIO exit → 레지스터 저장 MMIO exit (보통) SW: Yes / HW: Yes
Timer ICR 쓰기 MMIO exit → hrtimer 시작 MMIO exit + HW timer SW: Yes / HW: Yes (설정 시)
MSI 수신 KVM → vIRR 세팅 + kick Posted Interrupt/Doorbell SW: N/A / HW: No

KVM APIC 관련 커널 모듈(Kernel Module) 파라미터

# APICv 활성화/비활성화 확인
cat /sys/module/kvm_intel/parameters/enable_apicv
# 1 = 활성화 (기본), 0 = 비활성화

# AVIC 활성화/비활성화 확인
cat /sys/module/kvm_amd/parameters/avic
# 1 = 활성화 (기본), 0 = 비활성화

# APICv 비활성화 (디버깅/비교 테스트용)
# /etc/modprobe.d/kvm.conf:
# options kvm_intel enable_apicv=0

# Guest에서 APIC 모드 확인
dmesg | grep -i "apic\|x2apic"

# KVM 통계로 APIC VM Exit 빈도 확인
perf kvm stat live
# 또는
cat /sys/kernel/debug/kvm/*/vcpu*/stats | grep -i apic

커널 struct apic 서브시스템

리눅스 커널은 다양한 APIC 모드(flat, physical, cluster, x2apic 등)를 struct apic 드라이버 모델로 추상화합니다. 부팅 시 하드웨어와 BIOS 설정에 따라 적절한 APIC 드라이버가 선택되며, IPI 전송, APIC ID 읽기 등의 연산을 드라이버별로 구현합니다.

커널 struct apic 드라이버 계층 struct apic (추상화 인터페이스) send_IPI, read, write, init_apic_ldr, probe, ... arch/x86/include/asm/apic.h apic_flat xAPIC Flat Mode 최대 8 CPU apic_physflat Physical Flat Mode 개별 CPU 지정 x2apic_cluster x2APIC Cluster 대규모 시스템 기본 x2apic_phys x2APIC Physical KVM Guest 기본 bigsmp 레거시 >8 CPU xAPIC Physical 드라이버 선택 로직 (부트 시) x2APIC HW+IR → x2apic_cluster (기본) | CPUID/ACPI/cmdline → apic_flat/physflat/bigsmp
/* arch/x86/include/asm/apic.h - struct apic 주요 콜백 */
struct apic {
    /* 이름과 우선순위 */
    const char *name;

    /* 프로빙: 이 드라이버가 현재 HW에 적합한지 검사 */
    int  (*probe)(void);

    /* IPI 전송 */
    void (*send_IPI)(int cpu, int vector);
    void (*send_IPI_mask)(const struct cpumask *mask, int vector);
    void (*send_IPI_all)(int vector);
    void (*send_IPI_allbutself)(int vector);
    void (*send_IPI_self)(int vector);

    /* APIC ID 관련 */
    u32  (*read)(u32 reg);
    void (*write)(u32 reg, u32 val);
    u32  (*get_apic_id)(u32 id);
    u32  (*set_apic_id)(u32 id);

    /* 논리적 목적지 설정 */
    void (*init_apic_ldr)(void);
    u32  (*calc_dest_apicid)(unsigned int cpu);

    /* IRQ 어피니티 */
    int  (*cpu_mask_to_apicid)(const struct cpumask *mask,
                               struct irq_data *irqdata,
                               unsigned int *dest);

    /* 기타 */
    int (*wakeup_secondary_cpu)(int apicid, unsigned long start_eip);
};

/* 전역 APIC 드라이버 포인터 */
extern struct apic *apic;

/* 사용 예: IPI 전송 */
apic->send_IPI(cpu, RESCHEDULE_VECTOR);

드라이버 선택 과정

/* arch/x86/kernel/apic/probe_64.c - 드라이버 선택 */
void __init default_setup_apic_routing(void)
{
    /* x2APIC + Interrupt Remapping 사용 가능? */
    if (x2apic_enabled()) {
        if (x2apic_phys)
            apic_install_driver(&apic_x2apic_phys);
        else
            apic_install_driver(&apic_x2apic_cluster);
        return;
    }

    /* xAPIC: CPU 수에 따라 선택 */
    if (num_possible_cpus() <= 8)
        apic_install_driver(&apic_flat);      /* Flat Logical */
    else
        apic_install_driver(&apic_physflat);  /* Physical Flat */
}

/* 커널 명령줄 옵션:
 *   apic=debug       - APIC 디버그 로그 활성화
 *   noapic           - I/O APIC 비활성화 (8259A 사용)
 *   nolapic          - LAPIC 비활성화
 *   x2apic_phys      - x2APIC Physical 모드 강제
 */

APIC 드라이버별 특성 비교

드라이버접근 방식APIC IDDest Mode최대 CPU사용 시나리오
apic_flat xAPIC (MMIO) 8-bit Logical Flat 8 소규모 SMP 시스템
apic_physflat xAPIC (MMIO) 8-bit Physical 255 xAPIC + 8CPU 초과
x2apic_cluster x2APIC (MSR) 32-bit Logical Cluster 이론상 4G 대규모 서버 (기본)
x2apic_phys x2APIC (MSR) 32-bit Physical 이론상 4G KVM Guest, 특수 환경
bigsmp xAPIC (MMIO) 8-bit Physical 255 레거시 대규모 시스템

APIC 초기화 전체 흐름

부트 순서 (BSP):
  1. BIOS: 8259A PIC 모드로 시작, ACPI MADT 테이블 준비
  2. early_acpi_boot_init()
     └─ acpi_parse_madt() → LAPIC/I/O APIC 정보 수집
  3. init_apic_mappings()
     └─ LAPIC MMIO 매핑 (0xFEE00000)
  4. default_setup_apic_routing()
     └─ struct apic 드라이버 선택 (flat/cluster/x2apic)
  5. enable_x2apic() (지원 시)
     └─ IA32_APIC_BASE[10] = 1, IR 확인
  6. setup_local_APIC()
     └─ SVR 활성화, LVT 설정, TPR 초기화
  7. setup_IO_APIC()
     └─ RTE 프로그래밍, ISA IRQ 매핑
  8. 8259A PIC 마스크 (disable_8259A_irq)
  9. AP 부트: INIT-SIPI-SIPI → 각 AP에서 setup_local_APIC()

dmesg 출력 예시:
  [    0.000000] ACPI: LAPIC (acpi_id[0x00] lapic_id[0x00] enabled)
  [    0.000000] ACPI: IOAPIC (id[0x02] address[0xfec00000] gsi_base[0])
  [    0.000000] Using ACPI (MADT) for SMP configuration
  [    0.004000] x2apic: IRQ remapping enabled
  [    0.004000] Switched APIC routing to: x2apic_cluster

APIC ID 토폴로지

APIC ID는 단순한 프로세서 식별자가 아니라, 시스템의 물리적 토폴로지(Topology) 정보를 인코딩하고 있습니다. 소켓(Socket/Package), 코어(Core), 논리 프로세서(Thread/SMT) 계층이 APIC ID의 비트 필드에 반영되며, 커널은 이를 통해 NUMA 노드, 캐시 공유 관계, 스케줄링 도메인(Scheduling Domain)을 구성합니다.

APIC ID 비트 필드 구조

APIC ID 비트 필드 구조 (x2APIC 32-bit) Package ID 소켓/패키지 번호 Core ID 패키지 내 코어 번호 SMT ID 코어 내 하이퍼스레드 MSB LSB (bit 0) 예시: 2 Socket × 8 Core × 2 SMT = 32 논리 CPU APIC ID 구조: [Package:1비트][Core:3비트][SMT:1비트] = 5비트 사용 CPU 0 → APIC ID 0x00 = Pkg 0, Core 0, SMT 0 CPU 1 → APIC ID 0x01 = Pkg 0, Core 0, SMT 1 (하이퍼스레드 짝) CPU 2 → APIC ID 0x02 = Pkg 0, Core 1, SMT 0 CPU 16 → APIC ID 0x10 = Pkg 1, Core 0, SMT 0 (두 번째 소켓) 토폴로지 검출: CPUID Leaf 0x0B (Extended Topology) ECX[15:8] = Level Type: 1=SMT, 2=Core, (3=Module, 4=Tile, 5=Die 확장) EAX[4:0] = 해당 레벨에서 다음 레벨까지의 비트 시프트 수 EDX = 현재 논리 프로세서의 x2APIC ID EBX[15:0] = 해당 레벨에서 활성화된 논리 프로세서 수
/* CPUID를 통한 APIC 토폴로지 파싱 */
static void detect_apic_topology(void)
{
    u32 eax, ebx, ecx, edx;
    int subleaf = 0;

    /* CPUID Leaf 0x0B: Extended Topology Enumeration
     * Sub-leaf 0: SMT 레벨
     * Sub-leaf 1: Core 레벨
     * Sub-leaf 2+: Die/Module 레벨 (최신 CPU) */
    do {
        cpuid_count(0x0B, subleaf, &eax, &ebx, &ecx, &edx);

        int level_type = (ecx >> 8) & 0xFF;
        int shift = eax & 0x1F;
        int num_procs = ebx & 0xFFFF;

        if (level_type == 0)
            break;  /* 더 이상 레벨 없음 */

        pr_info("Topology level %d: type=%s shift=%d procs=%d\n",
                subleaf,
                level_type == 1 ? "SMT" :
                level_type == 2 ? "Core" :
                level_type == 5 ? "Die" : "Unknown",
                shift, num_procs);

        subleaf++;
    } while (1);

    /* x2APIC ID: edx 값 (Leaf 0x0B 어느 sub-leaf에서든 동일) */
    pr_info("This CPU x2APIC ID: 0x%x\n", edx);
}

/* APIC ID에서 토폴로지 정보 추출 */
static void parse_apic_id(u32 apicid, int smt_shift, int core_shift)
{
    int smt_id  = apicid & ((1 << smt_shift) - 1);
    int core_id = (apicid >> smt_shift) & ((1 << (core_shift - smt_shift)) - 1);
    int pkg_id  = apicid >> core_shift;

    pr_info("APIC ID 0x%x → Package=%d Core=%d SMT=%d\n",
            apicid, pkg_id, core_id, smt_id);
}

토폴로지 확인 명령

# CPU별 APIC ID 및 토폴로지 확인
lscpu -e=CPU,SOCKET,CORE,ONLINE,NODE
# CPU SOCKET CORE ONLINE NODE
#   0      0    0      y    0
#   1      0    0      y    0    ← CPU 0과 하이퍼스레드 쌍
#   2      0    1      y    0
#   ...
#  16      1    0      y    1    ← 두 번째 소켓

# APIC ID 직접 확인
grep "apicid" /proc/cpuinfo | head -8
# apicid		: 0
# apicid		: 1
# apicid		: 2
# apicid		: 3

# 토폴로지 상세 (/sys)
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
    c=$(basename $cpu)
    pkg=$(cat $cpu/topology/physical_package_id 2>/dev/null)
    core=$(cat $cpu/topology/core_id 2>/dev/null)
    siblings=$(cat $cpu/topology/thread_siblings_list 2>/dev/null)
    echo "$c: socket=$pkg core=$core ht_siblings=[$siblings]"
done

# NUMA 노드별 CPU 매핑
numactl --hardware | grep "node.*cpus"
# node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
# node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
APIC ID와 NUMA: APIC ID의 Package ID 부분이 NUMA 노드에 대응하는 것이 일반적이지만 항상 그런 것은 아닙니다. ACPI SRAT(System Resource Affinity Table)이 CPU의 APIC ID를 NUMA 노드에 매핑하며, 커널은 이를 기반으로 cpu_to_node() 매핑을 구성합니다. irqbalance는 이 토폴로지를 참고하여 인터럽트를 장치와 같은 NUMA 노드의 CPU에 할당합니다.

실전 성능 튜닝

APIC 관련 성능 튜닝은 주로 인터럽트 어피니티(Interrupt Affinity) 설정, IRQ 분배 최적화, MSI-X 벡터 할당, IPI 오버헤드 감소에 집중됩니다. 고성능 네트워킹(Networking)이나 스토리지(Storage) 워크로드에서 이러한 튜닝이 처리량(Throughput)과 레이턴시(Latency)에 큰 영향을 미칩니다.

IRQ 어피니티 설정

# IRQ 어피니티 확인
cat /proc/irq/<IRQ번호>/smp_affinity_list
cat /proc/irq/<IRQ번호>/smp_affinity     # 비트마스크 형식

# IRQ를 특정 CPU에 고정 (예: IRQ 42를 CPU 3에)
echo 3 > /proc/irq/42/smp_affinity_list

# IRQ를 CPU 그룹에 분배 (예: CPU 0-3)
echo 0-3 > /proc/irq/42/smp_affinity_list

# 비트마스크로 설정 (CPU 0,2,4,6 = 0x55)
echo 55 > /proc/irq/42/smp_affinity

# MSI-X 벡터별 어피니티 설정 (NVMe 예시)
# NVMe는 보통 CPU당 1개 큐 = 1개 MSI-X 벡터
for irq in $(grep nvme /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    echo "IRQ $irq: CPUs $(cat /proc/irq/$irq/smp_affinity_list)"
done

irqbalance 설정

옵션설명권장 시나리오
--oneshot 한 번 분배 후 종료 정적 워크로드 (NVMe, 전용 NIC)
--policyscript 사용자 정의 정책 스크립트 특정 IRQ에 커스텀 정책 적용
--banirq=N IRQ N을 재분배 대상에서 제외 수동 어피니티 설정한 IRQ 보호
--hintpolicy=exact 드라이버 어피니티 힌트를 정확히 따름 드라이버가 최적 CPU를 알 때
IRQBALANCE_BANNED_CPUS 환경 변수로 특정 CPU 제외 실시간 워크로드 CPU 격리

고성능 워크로드 튜닝 체크리스트

항목확인/설정효과
x2APIC 활성화 dmesg | grep x2apic EOI/IPI 5~13배 속도 향상
MSI-X 활성화 lspci -vvv | grep "MSI-X: Enable+" 다중 큐 장치의 병렬 인터럽트 처리
NUMA-aware IRQ 장치와 같은 NUMA 노드 CPU에 IRQ 할당 원격 메모리 접근(Remote Access) 감소
RPS/RFS 설정 /sys/class/net/*/queues/rx-*/rps_cpus 네트워크 패킷 처리 CPU 분배
CPU 격리 isolcpus= 또는 cset 지연 민감 워크로드의 인터럽트 차단
Interrupt Coalescing ethtool -C ethN rx-usecs 50 인터럽트 빈도 감소 (처리량↑, 레이턴시↑)
TSC-Deadline Timer grep tsc_deadline_timer /proc/cpuinfo 고정밀 타이머, C-state 영향 없음
Directed EOI 커널 기본 활성화 Level 트리거 EOI 브로드캐스트 제거
# === 고성능 NIC 튜닝 예시 ===

# 1. MSI-X 벡터 확인
ethtool -l eth0
# Channel parameters for eth0:
# Pre-set maximums:
#   Combined:   64
# Current hardware settings:
#   Combined:   16

# 2. 큐 수를 CPU 수에 맞춤
ethtool -L eth0 combined 16

# 3. NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# 0

# 4. 해당 NUMA 노드의 CPU에만 IRQ 할당
NODE_CPUS=$(numactl --hardware | grep "node 0 cpus:" | cut -d: -f2)
i=0
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    cpu=$(echo $NODE_CPUS | awk "{print \$$((i+1))}")
    echo $cpu > /proc/irq/$irq/smp_affinity_list
    echo "IRQ $irq → CPU $cpu"
    i=$((i+1))
done

# 5. irqbalance에서 해당 IRQ 제외
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
    IRQBALANCE_ARGS="$IRQBALANCE_ARGS --banirq=$irq"
done

# 6. Interrupt Coalescing 설정
ethtool -C eth0 rx-usecs 50 tx-usecs 50 rx-frames 64 tx-frames 64

# 7. 결과 확인: 인터럽트 분배 모니터링
watch -n 1 'grep eth0 /proc/interrupts'
KVM 환경에서의 APIC 성능: 가상화 환경에서는 APICv/AVIC 활성화 여부가 인터럽트 처리 성능에 결정적입니다. perf kvm stat live로 APIC 관련 VM Exit 빈도를 확인하고, enable_apicv=1(기본값)인지 확인하세요. Device Passthrough(VFIO) 사용 시 VT-d Posted Interrupt가 활성화되면 인터럽트가 VM Exit 없이 게스트에 직접 전달되어 네이티브에 가까운 성능을 달성합니다.

디버깅

APIC 관련 문제는 인터럽트가 전달되지 않거나 잘못된 CPU로 라우팅되는 형태로 나타납니다. /proc/interrupts, ESR(Error Status Register), 커널 트레이스포인트, dmesg 로그를 활용하여 진단합니다.

/proc/interrupts 분석

# APIC 관련 인터럽트 확인
cat /proc/interrupts | head -5
#            CPU0       CPU1       CPU2       CPU3
# 0:         22          0          0          0  IR-IO-APIC   2-edge      timer
# 1:          0          0          3          0  IR-IO-APIC   1-edge      i8042
# 8:          0          0          0          1  IR-IO-APIC   8-edge      rtc0
# 9:          0         12          0          0  IR-IO-APIC   9-fasteoi   acpi

# 키 식별자:
#   IR-IO-APIC  : Interrupt Remapping + I/O APIC 경로
#   IR-PCI-MSI  : Interrupt Remapping + MSI 경로
#   IO-APIC     : IR 없는 I/O APIC (레거시)
#   PCI-MSI     : IR 없는 MSI

# APIC 내부 인터럽트
cat /proc/interrupts | grep -E "LOC|RES|CAL|TLB|ERR|SPU"
# LOC:  Local timer interrupts (APIC Timer)
# RES:  Rescheduling interrupts (IPI)
# CAL:  Function call interrupts (IPI)
# TLB:  TLB shootdowns (IPI)
# ERR:  APIC error count
# SPU:  Spurious interrupts

# IRQ 어피니티 확인
for irq in /proc/irq/*/smp_affinity_list; do
    echo "$(dirname $irq | xargs basename): $(cat $irq)"
done

# x2APIC 모드 확인
dmesg | grep -i "x2apic\|apic\|ioapic"

ESR (Error Status Register) 분석

ESR 비트이름원인대처
[0]Send CS Error전송 시 체크섬(Checksum) 오류하드웨어 문제 의심
[1]Receive CS Error수신 시 체크섬 오류하드웨어 문제 의심
[2]Send Accept Error전송한 메시지 거부됨잘못된 APIC ID 확인
[3]Receive Accept Error수신한 메시지 거부됨APIC 비활성화 상태 확인
[4]Redirectable IPILowest Priority에서 유효하지 않은 리다이렉션라우팅 설정 확인
[5]Send Illegal Vector벡터 0~15 전송 시도벡터 번호 확인 (16~255)
[6]Receive Illegal Vector벡터 0~15 수신원본 장치 설정 확인
[7]Illegal Register Access존재하지 않는 레지스터 접근레지스터 오프셋 확인
/* ESR 읽기 (두 번 읽기 프로토콜) */
static u32 read_apic_esr(void)
{
    /* ESR은 "쓰기 후 읽기" 방식:
     * 1. ESR에 0을 쓰면 내부 에러 상태가 ESR로 래치
     * 2. ESR을 읽으면 래치된 값 반환 */
    apic_write(APIC_ESR, 0);
    return apic_read(APIC_ESR);
}

/* APIC 에러 인터럽트 핸들러 */
static void smp_error_interrupt(struct pt_regs *regs)
{
    u32 esr;

    entering_irq();
    esr = read_apic_esr();

    pr_err("APIC error on CPU%d: ESR=0x%x (%s%s%s%s%s%s%s%s)\n",
           smp_processor_id(), esr,
           (esr & 0x01) ? "Send_CS " : "",
           (esr & 0x02) ? "Recv_CS " : "",
           (esr & 0x04) ? "Send_Accept " : "",
           (esr & 0x08) ? "Recv_Accept " : "",
           (esr & 0x10) ? "Redir_IPI " : "",
           (esr & 0x20) ? "Send_Illegal " : "",
           (esr & 0x40) ? "Recv_Illegal " : "",
           (esr & 0x80) ? "Illegal_Reg " : "");

    ack_APIC_irq();
    exiting_irq();
}

커널 트레이스포인트

# APIC 관련 트레이스포인트 목록
ls /sys/kernel/debug/tracing/events/irq_vectors/
# local_timer_entry/exit    - APIC Timer 인터럽트
# reschedule_entry/exit     - Reschedule IPI
# call_function_entry/exit  - Call Function IPI
# error_apic_entry/exit     - APIC Error
# spurious_apic_entry/exit  - Spurious 인터럽트
# thermal_apic_entry/exit   - Thermal Monitor

# APIC Timer 인터럽트 트레이싱
echo 1 > /sys/kernel/debug/tracing/events/irq_vectors/local_timer_entry/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -20

# ftrace로 APIC 함수 추적
echo function > /sys/kernel/debug/tracing/current_tracer
echo 'apic_*' > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace

# perf로 APIC 관련 오버헤드 측정
perf stat -e irq_vectors:local_timer_entry \
          -e irq_vectors:reschedule_entry \
          -e irq_vectors:call_function_entry \
          -a sleep 5

bpftrace를 이용한 APIC 진단

# 레시피 1: APIC Timer 인터럽트 빈도 및 레이턴시 측정
bpftrace -e '
tracepoint:irq_vectors:local_timer_entry {
    @timer_count = count();
    @start[cpu] = nsecs;
}
tracepoint:irq_vectors:local_timer_exit / @start[cpu] / {
    $lat = (nsecs - @start[cpu]) / 1000;
    @timer_lat_us = hist($lat);
    delete(@start[cpu]);
}
interval:s:5 {
    printf("=== APIC Timer 통계 (5초) ===\n");
    printf("총 인터럽트: "); print(@timer_count);
    printf("처리 레이턴시 (μs):\n"); print(@timer_lat_us);
    clear(@timer_count); clear(@timer_lat_us);
}'

# 레시피 2: IPI 종류별 빈도 측정
bpftrace -e '
tracepoint:irq_vectors:reschedule_entry { @ipi[\"reschedule\"] = count(); }
tracepoint:irq_vectors:call_function_entry { @ipi[\"call_func\"] = count(); }
tracepoint:irq_vectors:call_function_single_entry { @ipi[\"call_single\"] = count(); }
interval:s:5 {
    printf("=== IPI 종류별 빈도 (5초) ===\n");
    print(@ipi);
    clear(@ipi);
}'

# 레시피 3: APIC Error 실시간 감시
bpftrace -e '
tracepoint:irq_vectors:error_apic_entry {
    printf("APIC ERROR on CPU %d at %llu\n", cpu, nsecs);
    @err_count = count();
}
tracepoint:irq_vectors:spurious_apic_entry {
    printf("SPURIOUS APIC on CPU %d at %llu\n", cpu, nsecs);
    @spur_count = count();
}'

# 레시피 4: EOI 레이턴시 히스토그램 (함수 프로브)
bpftrace -e '
kprobe:ack_APIC_irq {
    @eoi_start[tid] = nsecs;
}
kretprobe:ack_APIC_irq / @eoi_start[tid] / {
    @eoi_ns = hist(nsecs - @eoi_start[tid]);
    delete(@eoi_start[tid]);
}
interval:s:10 { print(@eoi_ns); clear(@eoi_ns); }'

APIC 상태 진단 스크립트

#!/bin/bash
# apic_diag.sh: APIC 관련 진단 정보 수집

echo "=== 1. APIC 모드 확인 ==="
dmesg | grep -iE "apic|x2apic|ioapic" | tail -20

echo
echo "=== 2. /proc/interrupts (상위 20줄) ==="
head -22 /proc/interrupts

echo
echo "=== 3. APIC 관련 카운터 ==="
grep -E "LOC|RES|CAL|TLB|ERR|SPU|MIS|PMI|TRM" /proc/interrupts

echo
echo "=== 4. IRQ 어피니티 (장치 IRQ만) ==="
for d in /proc/irq/*/smp_affinity_list; do
    irq=$(echo "$d" | cut -d/ -f4)
    name=""
    [ -f "/proc/irq/$irq/actions" ] && name=$(cat "/proc/irq/$irq/actions" 2>/dev/null | head -1)
    [ -z "$name" ] && continue
    echo "  IRQ $irq ($name): CPUs $(cat $d)"
done

echo
echo "=== 5. MSI/MSI-X 장치 ==="
lspci -vvv 2>/dev/null | grep -B 10 "MSI-X\|MSI:" | grep -E "^[0-9a-f]|MSI"

echo
echo "=== 6. IOMMU / Interrupt Remapping 상태 ==="
dmesg | grep -iE "DMAR|IOMMU|interrupt remapping" | tail -10

echo
echo "=== 7. CPU별 APIC ID ==="
for cpu in /sys/devices/system/cpu/cpu[0-9]*; do
    id=$(cat "$cpu/topology/core_id" 2>/dev/null)
    pkg=$(cat "$cpu/topology/physical_package_id" 2>/dev/null)
    echo "  $(basename $cpu): package=$pkg core=$id"
done

echo
echo "=== 8. APIC 에러 확인 ==="
ERR=$(grep "ERR:" /proc/interrupts | awk '{sum=0; for(i=2;i<=NF-1;i++) sum+=$i; print sum}')
SPU=$(grep "SPU:" /proc/interrupts | awk '{sum=0; for(i=2;i<=NF-1;i++) sum+=$i; print sum}')
echo "  Total APIC errors: $ERR"
echo "  Total Spurious: $SPU"
[ "$ERR" -gt 0 ] && echo "  ⚠ APIC 에러 발생 - dmesg 확인 필요"

일반적인 APIC 문제 진단 플로우

단계확인 항목명령정상 기준
1 APIC 모드 확인 dmesg | grep x2apic "x2apic enabled" 또는 "switched to apic x2apic_*"
2 I/O APIC 감지 dmesg | grep "IOAPIC" "IOAPIC[0]: apic_id N, address 0xFEC00000"
3 IR 활성화 dmesg | grep "Interrupt remapping" "Enabled IRQ remapping in x2apic mode"
4 APIC 에러 grep ERR /proc/interrupts ERR: 0
5 Spurious 인터럽트 grep SPU /proc/interrupts SPU: 0 또는 소수
6 인터럽트 분배 cat /proc/interrupts 장치 IRQ가 예상 CPU에 분배
7 MSI-X 활성화 lspci -vvv | grep MSI-X "MSI-X: Enable+"
8 타이머 동작 grep LOC /proc/interrupts 모든 CPU에서 카운트 증가

일반적인 APIC 부팅 문제와 진단

APIC 관련 부팅 문제는 인터럽트 전달 실패, 타이머 오동작, AP 부팅 실패 등 다양한 형태로 나타납니다. 아래는 현장에서 자주 발생하는 문제와 체계적 진단 방법입니다.

문제 1: APIC Timer 캘리브레이션 실패

증상원인진단해결
부팅 시 "APIC timer calibration result is negative" APIC Timer 카운터가 기대와 반대로 동작 dmesg | grep -i "calibrat" lapic_timer_c2_ok 커널 파라미터 또는 TSC-Deadline 강제
시간이 빠르거나 느리게 흐름 캘리브레이션 정확도 부족 (BIOS SMI 간섭) clocksource=tsc tsc=reliable로 비교 테스트 PM Timer 기반 캘리브레이션 또는 TSC-Deadline 모드 사용
C-state 진입 후 타이머 멈춤 C3+ 상태에서 APIC 버스 클럭 중단 processor.max_cstate=2로 C3 비활성화 후 확인 TSC-Deadline 모드 전환 (invariant TSC 필수)
# APIC Timer 캘리브레이션 문제 진단
dmesg | grep -iE "apic.*timer|calibrat|lapic|tsc"
# 정상 출력 예시:
# [    0.004000] APIC timer calibrated: 100.00 MHz
# [    0.004000] tsc: Detected 3000.000 MHz processor
# [    0.004000] tsc: Refined TSC clocksource calibration: 2999.990 MHz
# [    0.004000] Switched to clocksource tsc

# 비정상 출력 예시:
# [    0.004000] APIC timer calibration result is negative!
# [    0.004000] Using default APIC timer frequency

# TSC-Deadline 지원 확인
grep -o "tsc_deadline_timer\|constant_tsc\|nonstop_tsc" /proc/cpuinfo | sort -u

# 현재 clocksource/clockevent 확인
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
ls /sys/devices/system/clockevents/clockevent*/

문제 2: AP(Application Processor) 부팅 실패

# AP 부팅 실패 진단
dmesg | grep -iE "CPU[0-9]|smpboot|INIT|SIPI|not responding"
# 정상: "smpboot: CPU 1 is now online" 등 모든 CPU
# 비정상: "CPU 3 not responding (not an error)" 또는
#          "smpboot: Booting Node 0, Processors: #1 #2 OK #3 FAILED"

# 온라인 CPU 확인
cat /sys/devices/system/cpu/online
# 기대: 0-N (모든 CPU), 비정상: 일부 누락

# AP 부팅 실패 일반적 원인:
# 1. BIOS의 Hyper-Threading 비활성화 → APIC ID 불연속
# 2. ACPI MADT의 LAPIC 엔트리 누락/비활성화
# 3. 메모리 부족 (AP 트램폴린 코드용 low memory 없음)
# 4. microcode 문제 (구형 CPU에서 SIPI 처리 버그)
# 5. VMM의 APIC 에뮬레이션 문제 (중첩 가상화)

# ACPI MADT에서 LAPIC 엔트리 확인
dmesg | grep "ACPI.*LAPIC"
# ACPI: LAPIC (acpi_id[0x00] lapic_id[0x00] enabled)
# ACPI: LAPIC (acpi_id[0x01] lapic_id[0x01] enabled)
# ACPI: LAPIC (acpi_id[0x02] lapic_id[0x02] disabled) ← 비활성화됨!

문제 3: I/O APIC 관련 부팅 문제

증상원인진단 명령해결 파라미터
키보드/마우스 미작동 ISA IRQ의 GSI 매핑 오류 cat /proc/interrupts | head -20 acpi_sci=edge 또는 noapic
부팅 중 멈춤 (I/O APIC 초기화 단계) I/O APIC MMIO 주소 충돌 dmesg | grep "IOAPIC" noapic (8259A 폴백)
PCI 장치 인터럽트 미전달 ACPI _PRT(PCI Routing Table) 오류 dmesg | grep -i "irq\|routing" pci=routeirq 또는 BIOS 업데이트
ACPI SCI 인터럽트 폭주 Level 트리거 SCI의 EOI/Remote IRR 문제 grep "acpi" /proc/interrupts (빠른 증가) acpi_sci=edge 또는 irqpoll

문제 4: x2APIC 전환 실패

# x2APIC 전환 실패 진단
dmesg | grep -iE "x2apic|interrupt remapping|DMAR|IOMMU"

# 일반적 실패 시나리오:

# 시나리오 1: Interrupt Remapping 미지원
# "x2APIC: IRQ remapping doesn't support X2APIC mode"
# → BIOS에서 VT-d 활성화 필요

# 시나리오 2: DMAR ACPI 테이블 없음
# "DMAR: DRHD not found"
# → BIOS에 VT-d 옵션이 없는 하드웨어 (x2APIC 사용 불가)

# 시나리오 3: IOMMU 초기화 실패
# "DMAR: Failed to enable IRQ remapping"
# → BIOS 버그 또는 하드웨어 문제

# 강제 xAPIC 모드로 부팅 (디버깅용)
# 커널 파라미터: nox2apic

# VT-d/IOMMU 상태 확인
dmesg | grep -i "DMAR\|IOMMU\|VT-d"
# 정상: "DMAR: Intel(R) Virtualization Technology for Directed I/O"
#        "DMAR-IR: Enabled IRQ remapping in x2apic mode"

# x2APIC 활성 확인
rdmsr 0x1B 2>/dev/null || echo "rdmsr 도구 필요: apt install msr-tools"
# bit[10]=1 이면 x2APIC, bit[11]=1 이면 APIC 활성

문제 5: Spurious 인터럽트 과다 발생

/* Spurious 인터럽트 진단 및 처리
 *
 * 원인 분석:
 * 1. Level 트리거 인터럽트가 핸들러 진입 전 디어서트
 * 2. APIC 버스의 타이밍 레이스 컨디션
 * 3. 하드웨어 신호 글리치
 * 4. I/O APIC RTE의 잘못된 트리거 모드 설정
 *
 * 진단:
 * cat /proc/interrupts | grep SPU
 * → 소수(< 100)는 정상, 급속 증가는 문제
 *
 * 핵심 규칙: Spurious 인터럽트에 EOI를 보내면 안 됩니다!
 * ISR에 비트가 세팅되지 않았으므로 EOI는 다른 인터럽트를 잘못 클리어합니다.
 */

/* 커널의 Spurious 인터럽트 핸들러 */
static void smp_spurious_interrupt(struct pt_regs *regs)
{
    u32 v;

    entering_irq();

    /* ISR에 해당 비트가 설정되어 있는지 확인 */
    v = apic_read(APIC_ISR + (SPURIOUS_APIC_VECTOR / 32) * 0x10);

    if (v & (1 << (SPURIOUS_APIC_VECTOR % 32))) {
        /* 진짜 인터럽트 (ISR 비트 있음) → EOI 필요 */
        ack_APIC_irq();
    } else {
        /* 진짜 Spurious → EOI 보내지 않음! */
        inc_irq_stat(irq_spurious_count);
    }

    exiting_irq();
}

/* Spurious 과다 발생 시 점검 사항:
 * 1. I/O APIC RTE의 Trigger Mode 확인
 *    - PCI 장치: Level-triggered, Active-Low
 *    - ISA 장치: Edge-triggered, Active-High
 *    → 잘못된 설정은 Spurious의 주요 원인
 *
 * 2. ACPI Interrupt Source Override 확인
 *    dmesg | grep "INT_SRC_OVR"
 *    → ISA IRQ의 Polarity/Trigger 오버라이드가 올바른지 검증
 *
 * 3. 공유 인터럽트(Shared IRQ) 확인
 *    cat /proc/interrupts | grep "IR-IO-APIC.*level"
 *    → Level-triggered 공유 IRQ에서 발생하기 쉬움
 */

일반적인 실수

실수증상해결
EOI 전송 누락 같은 벡터의 인터럽트가 이후 전달되지 않음 (ISR 비트 고착) 핸들러 종료 시 반드시 ack_APIC_irq() 호출
잘못된 ICR Destination IPI가 엉뚱한 CPU로 전달되거나 분실 APIC ID와 Destination Mode(Physical/Logical) 일치 확인
x2APIC 전환 없이 MSR 접근 #GP(General Protection) 예외 발생 IA32_APIC_BASE[10]=1(EXTD) 확인 후 MSR 사용
IR 없이 x2APIC 사용 보안 취약점 (MSI 인터럽트 주입 공격 가능) VT-d/IOMMU Interrupt Remapping 필수 활성화
APIC Timer 캘리브레이션 실패 타이머 주기가 비정상, 시간 드리프트 TSC-Deadline 모드 사용 권장, PIT 기반 캘리브레이션 검증
I/O APIC RTE에 마스크 해제 누락 장치 인터럽트가 전달되지 않음 RTE[16](Mask bit)=0 확인
Level 트리거에서 Remote IRR 미해제 해당 핀에서 인터럽트 재발생 불가 I/O APIC EOI 레지스터 또는 RTE 재설정
DFR/LDR 미설정 (xAPIC Logical 모드) Logical Destination 기반 인터럽트 실패 init_apic_ldr() 호출 확인, Flat vs Cluster 모드 일치
SVR의 APIC Enable 비트 미설정 LAPIC이 모든 인터럽트를 무시 apic_write(APIC_SPIV, val | APIC_SPIV_APIC_ENABLED)
MSI-X Table의 Mask 비트 특정 MSI-X 벡터 인터럽트 미전달 vector_ctrl[0]=0 (unmask) 확인
APIC 비활성화 경고: IA32_APIC_BASE[11]=0으로 APIC을 비활성화하면 RESET 없이는 재활성화가 불가능합니다. 커널 명령줄의 nolapic 옵션은 이 비트를 건드리지 않고 소프트웨어적으로 APIC 사용을 중단합니다. BIOS에서 APIC이 비활성화된 경우 커널 부팅 시 "BIOS bug: APIC disabled" 메시지가 출력됩니다.

자주 묻는 질문 (FAQ)

Q: xAPIC과 x2APIC을 동시에 사용할 수 있는가?

아닙니다. 하나의 CPU에서 xAPIC과 x2APIC은 배타적 모드입니다. IA32_APIC_BASE MSR의 EN(bit 11)과 EXTD(bit 10) 조합으로 결정됩니다:

  • EN=0, EXTD=0: APIC 비활성화
  • EN=1, EXTD=0: xAPIC 모드 (MMIO)
  • EN=1, EXTD=1: x2APIC 모드 (MSR)
  • EN=0, EXTD=1: 잘못된 상태 (#GP 발생)

x2APIC에서 xAPIC으로 역전환은 불가능합니다. RESET만이 xAPIC으로 복귀시킵니다. 단, 멀티 소켓(Socket) 시스템에서 각 CPU가 독립적으로 모드를 결정하므로 (이론적으로) 다른 CPU가 다른 모드일 수 있으나, 실제로는 커널이 모든 CPU를 동일 모드로 설정합니다.

Q: Spurious Interrupt(벡터 0xFF)는 왜 발생하는가?

Spurious Interrupt는 LAPIC이 인터럽트 수신을 인지했으나, CPU가 ISR에 비트를 세팅하기 전에 인터럽트가 사라진 경우 발생합니다. 일반적인 원인:

  • Level 트리거 인터럽트가 핸들러 진입 전에 디어서트(de-assert)됨
  • I/O APIC과 LAPIC 사이의 레이스 컨디션
  • 하드웨어 결함 (신호 글리치)

Spurious Interrupt에 대해서는 EOI를 보내지 않아야 합니다. ISR에 비트가 세팅되지 않았으므로 EOI는 다른 인서비스 인터럽트를 잘못 클리어할 수 있습니다. 리눅스 커널은 SVR의 벡터 0xFF로 설정하고, 이 벡터의 핸들러는 카운터만 증가시킵니다.

Q: I/O APIC 없이 MSI만으로 시스템을 운영할 수 있는가?

현대 시스템에서는 대부분의 장치가 MSI/MSI-X를 사용하지만, 레거시 장치(키보드 8042, UART, RTC 등)는 여전히 I/O APIC 핀에 연결됩니다. 따라서 완전히 I/O APIC 없이 운영하기는 어렵습니다.

그러나 커널 명령줄 pci=nomsi로 MSI를 비활성화하거나, noapic으로 I/O APIC을 비활성화하여 한쪽만 사용할 수 있습니다. noapic 사용 시 I/O APIC 대신 8259A PIC 호환 모드로 폴백합니다.

Q: APICv/AVIC이 비활성화되는 경우는?

다음과 같은 상황에서 KVM은 APICv/AVIC을 비활성화하고 소프트웨어 에뮬레이션으로 폴백합니다:

  • Guest가 xAPIC Flat 이외의 모드 사용 (일부 구형 x2APIC 처리)
  • Nested Virtualization (L2 Guest)
  • AVIC에서 x2APIC 모드 Guest (AMD 구형 CPU)
  • enable_apicv=0 모듈 파라미터
  • IRQ 윈도우 관련 특수 상황
  • SEV/SEV-ES 암호화(Encryption) VM (일부 AVIC 기능 제한)

KVM은 동적으로 APICv를 활성화/비활성화할 수 있으며, kvm_request_apicv_update()로 런타임에 전환합니다. 비활성화 시 /sys/kernel/debug/kvm/*/vcpu*/apicv_inhibit에서 이유를 확인할 수 있습니다.

Q: APIC Timer와 TSC의 관계는?

APIC Timer의 One-shot/Periodic 모드는 APIC 버스 클럭(또는 코어 크리스탈 클럭)을 기반으로 카운트다운합니다. 이 클럭은 CPU 주파수 스케일링(P-state)에 영향을 받지 않지만, C3 이상의 C-state에서 중단될 수 있습니다.

반면 TSC-Deadline 모드는 TSC(Time Stamp Counter)를 기준으로 합니다. Invariant TSC(CPUID.80000007H:EDX[8]=1) 지원 시 TSC는 C-state/P-state에 관계없이 일정한 속도로 증가하므로, TSC-Deadline이 가장 안정적입니다.

커널은 constant_tscnonstop_tsc CPU 기능을 확인하고, TSC-Deadline 모드를 우선 사용합니다. cat /proc/cpuinfo | grep -o "constant_tsc\|nonstop_tsc\|tsc_deadline_timer"로 지원 여부를 확인할 수 있습니다.

커널 명령줄 APIC 관련 옵션 정리

옵션효과용도
noapic I/O APIC 비활성화 (8259A PIC 사용) I/O APIC 관련 부팅 문제 우회
nolapic LAPIC 비활성화 (PIC + PIT 폴백) LAPIC 하드웨어 오류 시 부팅
apic=debug APIC 초기화/설정 디버그 로그 활성화 APIC 설정 문제 진단
apic=verbose 상세 APIC 정보 출력 RTE/LVT 설정 확인
nox2apic x2APIC 모드 비활성화 (xAPIC 유지) x2APIC 관련 문제 우회
x2apic_phys x2APIC Physical 모드 강제 Cluster 모드 문제 시 대안
pci=nomsi MSI/MSI-X 비활성화 MSI 관련 인터럽트 문제 진단
intremap=off Interrupt Remapping 비활성화 IR 관련 부팅 문제 우회 (보안 약화)
lapic_timer_c2_ok C2에서 LAPIC 타이머 사용 허용 C2에서 타이머 정지하는 특정 BIOS 우회
clocksource=tsc TSC를 기본 클럭소스로 강제 TSC-Deadline 타이머 안정화

참고자료

커널 공식 문서

커널 소스 코드

Intel/AMD 규격 문서

LWN.net 기사

OSDev 위키

외부 기술 자료

필수 관련 문서: 참고 문서: