IPI (Inter-Processor Interrupt) 심화

IPI(Inter-Processor Interrupt)는 SMP 시스템에서 한 CPU가 다른 CPU에게 보내는 특수한 인터럽트입니다. x86 APIC/ICR 아키텍처를 기반으로 스케줄러 리밸런싱, TLB 캐시 일관성, 원격 함수 호출(smp_call_function), SMP 부팅 시퀀스, IRQ Work 등 CPU 간 협조 메커니즘의 전 영역을 소스 코드 수준에서 상세히 분석합니다.

관련 표준: Intel SDM Vol. 3 (APIC, ICR, IPI Delivery), AMD64 Architecture Programmer's Manual Vol. 2, ACPI (MADT/APIC 테이블) — IPI 하드웨어 인터페이스의 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 인터럽트Softirq/Hardirq 문서를 먼저 읽으세요. IPI는 인터럽트 하위 시스템 위에서 동작하므로, IDT 벡터 처리와 APIC 구조를 먼저 이해해야 합니다.
일상 비유: IPI는 사무실에서 동료에게 보내는 긴급 메모와 비슷합니다. 각자 작업하던 직원(CPU)에게 "지금 당장 이 일을 처리해 달라"고 종이를 건네는 것처럼, 한 CPU가 다른 CPU에게 즉시 반응해야 할 요청을 보내는 메커니즘입니다.

핵심 요약

  • APIC + ICR — x86에서 IPI는 Local APIC의 ICR(Interrupt Command Register) 기록으로 전송됩니다.
  • 벡터 분류 — Reschedule(0xFD), Call Function(0xFC/0xFB), Reboot(0xFA), IRQ Work(0xF6) 등 목적별 전용 벡터를 사용합니다.
  • Reschedule IPI — 가장 빈번한 IPI로, 대상 CPU의 스케줄러에 재스케줄링을 요청합니다.
  • smp_call_function — 원격 CPU에서 콜백 함수를 실행하는 범용 API입니다.
  • TLB Flush IPI — 페이지 테이블 변경 시 관련 CPU들의 TLB를 무효화합니다.
  • 성능 영향 — IPI 왕복 비용은 0.5~5us이며, 대규모 SMP에서 IPI storm은 심각한 병목이 됩니다.

단계별 이해

  1. APIC 하드웨어 이해
    Local APIC와 ICR 레지스터 구조를 파악합니다. xAPIC(MMIO)와 x2APIC(MSR) 차이를 이해합니다.
  2. IPI 벡터 맵 확인
    irq_vectors.h에 정의된 시스템 예약 벡터(0xF0~0xFF)와 각 벡터의 목적을 학습합니다.
  3. 주요 IPI 흐름 추적
    Reschedule IPI, Call Function IPI, TLB Flush IPI의 송신-수신 경로를 코드로 따라갑니다.
  4. 성능 모니터링
    /proc/interrupts의 RES/CAL/TLB 카운터와 perf/ftrace 도구로 IPI 빈도를 진단합니다.

IPI 개요

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

x86 IPI 전송 메커니즘

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

x86 IPI 전송 아키텍처 CPU 0 (발신) Local APIC ICR (0xFEE00300) Vector | Dest | Mode System Bus / APIC Bus xAPIC: ICR write → Bus | x2APIC: MSR write → Direct ICR Write CPU 1 (수신) Local APIC IRR / ISR Vector Pending → In-Service ICR (Interrupt Command Register) 구조 — 64비트 Vector [7:0] Delivery [10:8] Dest Mode [11] Level [14] Trigger [15] Shorthand [19:18] Dest [63:32] Delivery Mode (ICR bits [10:8]) 000: Fixed 001: Lowest Priority 010: SMI 100: NMI 101: INIT 110: Start-up (SIPI) Shorthand: 00=No, 01=Self, 10=All Including Self, 11=All Excluding Self
IRR(Interrupt Request Register): 대기 중인 인터럽트 벡터를 저장하는 APIC 레지스터. ISR(In-Service Register): 현재 CPU가 처리 중인 인터럽트 벡터를 저장. IRQ 신호 수신 시 IRR 비트가 세트되고, CPU가 처리를 시작하면 해당 비트가 ISR로 이동.
/* arch/x86/kernel/apic/apic.c — IPI 전송의 핵심 */

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

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

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

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

리눅스 IPI 벡터 유형

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

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

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

Reschedule IPI

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

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

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

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

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

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

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

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

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

smp_call_function API 상세

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

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

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

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

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

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

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

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

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

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

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

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

    put_cpu();
    return 0;
}

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

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

TLB Flush IPI

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

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

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

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

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

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

    put_flush_tlb_info();
}

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

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

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

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

IRQ Work IPI

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

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

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

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

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

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

    return true;
}

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

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

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

SMP 부팅 시 IPI 활용

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

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

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

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

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

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

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

IPI 성능 특성과 최적화

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

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

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

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

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

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

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

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

IPI 모니터링과 디버깅

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

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

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

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

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

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

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

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

IPI 성능 튜닝 체크리스트:

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

ICR 레지스터 구조 상세

ICR(Interrupt Command Register)은 Local APIC에서 IPI를 전송하기 위한 핵심 레지스터입니다. xAPIC 모드에서는 MMIO 주소 0xFEE00300(하위 32비트)과 0xFEE00310(상위 32비트)에 매핑되며, x2APIC 모드에서는 MSR 0x830에 64비트로 통합됩니다. 각 비트 필드의 정확한 의미를 이해하는 것이 IPI 메커니즘 분석의 기초입니다.

ICR (Interrupt Command Register) 비트 필드 상세 상위 32비트 (xAPIC: ICR_HIGH 0xFEE00310 / x2APIC: MSR 0x830 [63:32]) Destination Field [63:32] — 대상 APIC ID (xAPIC: [63:56] 8비트, x2APIC: 전체 32비트) 하위 32비트 (xAPIC: ICR_LOW 0xFEE00300 / x2APIC: MSR 0x830 [31:0]) Vector [7:0] 8비트 Delivery Mode [10:8] 3비트 Dest Mode [11] 1비트 Status [12] R/O Level [14] 1비트 Trigger [15] 1비트 Destination Shorthand [19:18] — 00: No Shorthand | 01: Self | 10: All Including Self | 11: All Excluding Self Delivery Mode 상세 (비트 [10:8]) 000: Fixed 지정 벡터 전달 001: Lowest Pri 최저 우선순위 CPU 010: SMI 시스템 관리 인터럽트 100: NMI 마스크 불가 인터럽트 101: INIT 프로세서 초기화 110: Start-up (SIPI) AP 부팅용, 리얼모드 진입 주소 인코딩 Destination Mode (비트 [11]) 0: Physical Mode APIC ID로 직접 지정 (1:1 매핑) 1: Logical Mode LDR/DFR 기반 그룹 대상 지정 Level / Trigger (비트 [14:15]) Level: 0=De-assert, 1=Assert INIT de-assert에서 0 사용 Trigger: 0=Edge, 1=Level IPI는 항상 Edge 트리거 사용
/* arch/x86/include/asm/apicdef.h — ICR 비트 필드 정의 */

/* Delivery Mode (비트 [10:8]) */
#define APIC_DM_FIXED       0x00000   /* 000: Fixed — 지정 벡터 전달 */
#define APIC_DM_LOWEST      0x00100   /* 001: Lowest Priority */
#define APIC_DM_SMI         0x00200   /* 010: SMI */
#define APIC_DM_NMI         0x00400   /* 100: NMI */
#define APIC_DM_INIT        0x00500   /* 101: INIT */
#define APIC_DM_STARTUP     0x00600   /* 110: Start-up (SIPI) */

/* Destination Mode (비트 [11]) */
#define APIC_DEST_PHYSICAL  0x00000   /* 0: Physical Mode */
#define APIC_DEST_LOGICAL   0x00800   /* 1: Logical Mode */

/* Level / Trigger (비트 [14:15]) */
#define APIC_INT_LEVELTRIG  0x08000   /* Level triggered */
#define APIC_INT_ASSERT     0x04000   /* Assert level */

/* Destination Shorthand (비트 [19:18]) */
#define APIC_DEST_NOSHORT   0x00000   /* 00: No Shorthand */
#define APIC_DEST_SELF      0x40000   /* 01: Self */
#define APIC_DEST_ALLINC    0x80000   /* 10: All Including Self */
#define APIC_DEST_ALLBUT    0xC0000   /* 11: All Excluding Self */

/* ICR 값 조립 예시 — CPU 3에 벡터 0xFD(Reschedule) 전송 */
u32 icr_low  = APIC_DM_FIXED | APIC_DEST_PHYSICAL | 0xFD;
u32 icr_high = (3 << 24);  /* xAPIC: APIC ID를 [31:24]에 배치 */

/* x2APIC에서는 64비트 통합 기록 */
u64 icr = ((u64)3 << 32) | APIC_DM_FIXED | 0xFD;
wrmsrl(MSR_X2APIC_ICR, icr);
xAPIC vs x2APIC ICR 접근 차이: xAPIC에서는 ICR이 두 개의 32비트 MMIO 레지스터로 분할되어 있어 반드시 ICR_HIGH를 먼저 기록하고 ICR_LOW를 나중에 기록해야 합니다(ICR_LOW 기록이 IPI 전송을 트리거). x2APIC에서는 MSR 0x830에 64비트 단일 기록으로 원자적 전송이 가능하며, 이로 인해 race condition 위험이 제거되고 지연 시간이 단축됩니다. 또한 x2APIC는 32비트 APIC ID를 지원하여 256개 이상의 CPU를 대상으로 IPI를 보낼 수 있습니다.

TLB Flush 최적화 심화

TLB flush IPI는 SMP 시스템에서 가장 빈번하게 발생하는 IPI 유형 중 하나이며, 성능에 미치는 영향도 큽니다. 커널은 PCID, lazy TLB, INVLPGB 등 다양한 최적화 기법을 활용하여 TLB flush IPI의 빈도와 비용을 줄입니다.

TLB Flush 최적화 경로: flush_tlb_mm → native_flush_tlb_others flush_tlb_mm_range(mm, start, end) flush 범위 확인 범위 > 임계값 전체 flush 범위 <= 임계값 INVLPG 범위 mm_cpumask(mm) 확인: 대상 CPU 결정 PCID 지원? 지원 PCID별 선택적 무효화 미지원 CR3 전체 reload 대상 CPU lazy TLB? 커널 스레드 실행 중 IPI 생략 유저 프로세스 활성 IPI 전송 필요 native_flush_tlb_others(cpumask, info) smp_call_function_many_cond() → IPI 전송

PCID (Process Context Identifiers)

PCID는 Intel Haswell 이후 프로세서에서 지원하는 기능으로, TLB 엔트리에 프로세스별 태그를 부여합니다. 이를 통해 컨텍스트 스위칭 시 전체 TLB flush 없이 프로세스별 TLB 엔트리를 선택적으로 무효화할 수 있습니다:

/* arch/x86/mm/tlb.c — PCID 기반 TLB 관리 */

/* PCID는 12비트(0-4095) 중 커널은 6개 슬롯을 순환 사용 */
#define TLB_NR_DYN_ASIDS  6

struct tlb_state {
    struct mm_struct *loaded_mm;
    u16 loaded_mm_asid;          /* 현재 로드된 PCID */
    u16 next_asid;               /* 다음 할당할 PCID */
    struct {
        struct mm_struct *mm;
        unsigned long generation;
    } ctxs[TLB_NR_DYN_ASIDS];   /* PCID ↔ mm 매핑 테이블 */
};

/* CR3에 PCID를 포함하여 로드 — NOFLUSH 비트로 TLB 보존 */
static inline void write_cr3_pcid(unsigned long cr3, u16 pcid,
                                    bool noflush)
{
    if (noflush)
        cr3 |= X86_CR3_PCID_NOFLUSH;  /* 비트 63: TLB flush 억제 */
    cr3 |= pcid;                       /* 비트 [11:0]: PCID 값 */
    write_cr3(cr3);
}

/* PCID 덕분에 컨텍스트 스위칭 시:
 * 1. 이전 mm의 TLB 엔트리가 보존됨 (PCID 태그)
 * 2. 새 mm이 최근에 사용한 PCID가 있으면 NOFLUSH로 전환
 * 3. 캐시 히트율 향상 → TLB miss 및 flush IPI 감소 */

Lazy TLB 모드

Lazy TLB는 커널 스레드가 실행 중일 때 유저 공간 TLB flush를 지연시키는 최적화입니다. 커널 스레드는 유저 공간 매핑을 사용하지 않으므로, TLB flush IPI를 즉시 처리할 필요가 없습니다:

/* arch/x86/mm/tlb.c — Lazy TLB 처리 */

/* 커널 스레드로 전환 시 lazy TLB 모드 진입 */
void switch_mm_irqs_off(struct mm_struct *prev,
                        struct mm_struct *next,
                        struct task_struct *tsk)
{
    if (!next && test_thread_flag(TIF_LAZY_MMU_UPDATES)) {
        /* 커널 스레드: loaded_mm을 유지하지만
         * mm_cpumask에서 현재 CPU를 제거하여
         * TLB flush IPI 수신 대상에서 제외 */
        cpumask_clear_cpu(smp_processor_id(),
                          mm_cpumask(real_prev));
        /* 이후 유저 프로세스로 복귀 시
         * mm_cpumask에 재추가 + 필요 시 TLB flush 수행 */
    }
}

/* Lazy TLB 효과:
 * - 커널 스레드 실행 중인 CPU에 TLB flush IPI를 보내지 않음
 * - 대규모 SMP에서 커널 스레드가 많을수록 효과 큼
 * - 유저 프로세스로 복귀 시점에 한 번만 flush 수행 */

INVLPGB (AMD 전용)

AMD Zen 3 이후 프로세서에서 지원하는 INVLPGB 명령어는 IPI 없이 다른 CPU의 TLB를 직접 무효화할 수 있는 혁신적인 기능입니다:

/* INVLPGB: Invalidate TLB Entries with Broadcast
 * AMD Zen 3+ 전용 — IPI 없이 하드웨어 브로드캐스트로 TLB 무효화
 *
 * 장점:
 * 1. IPI 왕복 비용 완전 제거 (수 us → 수십 ns)
 * 2. 대상 CPU 개수에 무관한 고정 비용
 * 3. 대상 CPU의 인터럽트 오버헤드 없음
 *
 * 사용 조건:
 * - CPUID Fn8000_0008_EBX[INVLPGB] 비트 확인
 * - 커널 CONFIG_X86_INVLPGB 활성화 필요 */

/* INVLPGB 명령어 인코딩 */
static inline void invlpgb_flush_single(unsigned long addr,
                                          u16 asid)
{
    /* RAX: 가상 주소 + 플래그
     * ECX: ASID (PCID에 해당)
     * EDX: 추가 매개변수 */
    asm volatile("invlpgb"
                 : : "a"(addr), "c"(asid), "d"(0)
                 : "memory");
    /* TLBSYNC: INVLPGB 완료 대기 (필수) */
    asm volatile("tlbsync" : : : "memory");
}
TLB Flush 최적화 요약:
  • PCID — 프로세스별 TLB 태깅으로 컨텍스트 스위칭 시 불필요한 flush 제거 (Intel Haswell+)
  • Lazy TLB — 커널 스레드 실행 중인 CPU를 flush 대상에서 제외
  • Batch flushtlb_gather_mmu()로 여러 PTE 변경을 모아 단일 IPI로 처리
  • INVLPGB — IPI 없이 하드웨어 브로드캐스트로 원격 TLB 무효화 (AMD Zen 3+)
  • Huge page — 2MB/1GB 페이지로 TLB 엔트리 수 감소, flush 빈도 저하

smp_call_function 내부 구현 상세

smp_call_function 계열 API는 커널에서 가장 널리 사용되는 IPI 인터페이스입니다. 내부적으로 per-CPU call_single_queue, CSD(Call Single Data) 락, 그리고 flush 메커니즘이 복잡하게 얽혀 있습니다.

smp_call_function 내부 구현: CSD Queue 기반 Sender CPU smp_call_function_single(cpu, func, info, wait) CSD 구조체 초기화 csd.func = func, csd.info = info csd_lock(&csd) CSD_FLAG_LOCK 설정 __smp_call_single_queue(cpu, &csd) llist_add to call_single_queue CALL_FUNCTION_SINGLE IPI Target CPU per-CPU call_single_queue (llist) sysvec_call_function_single() IDT entry → ack_APIC_irq() flush_smp_call_function_queue() llist_del_all → 모든 CSD 순회 csd->func(csd->info) 콜백 함수 실행 csd_unlock(csd) CSD_FLAG_LOCK 해제 wait=1: spin-wait 종료 csd_lock_wait(&csd) wait=1: CSD_FLAG_LOCK 해제될 때까지 spin-wait call_single_queue: per-CPU lock-free llist (단방향 연결 리스트) Sender: llist_add() (lock-free 삽입) → IPI 전송 Receiver: llist_del_all() (원자적 추출) → 순회하며 func() 호출 → csd_unlock()
/* kernel/smp.c — smp_call_function 내부 구현 상세 */

/* per-CPU call_single_queue: lock-free 단방향 연결 리스트 */
DEFINE_PER_CPU_ALIGNED(struct llist_head, call_single_queue);

/* CSD 노드 구조 — 큐의 기본 단위 */
struct __call_single_node {
    struct llist_node lentry;
    unsigned int src;      /* 발신 CPU 번호 */
    unsigned int dst;      /* 수신 CPU 번호 */
    union {
        unsigned int u_flags;
        atomic_t a_flags;     /* CSD_FLAG_LOCK 등 원자적 플래그 */
    };
};

#define CSD_FLAG_LOCK       0x01  /* CSD가 처리 중 — 재사용 방지 */
#define CSD_FLAG_SYNCHRONOUS 0x02 /* 동기 호출 — 완료 대기 필요 */

/* __smp_call_single_queue: CSD를 대상 CPU 큐에 삽입 */
static void __smp_call_single_queue(int cpu,
                                     struct llist_node *node)
{
    /* lock-free llist_add: cmpxchg로 원자적 삽입
     * 반환값이 true면 큐가 비어 있었음 → IPI 필요 */
    if (llist_add(node, per_cpu_ptr(&call_single_queue, cpu)))
        send_call_function_single_ipi(cpu);
        /* 큐에 이미 항목이 있었으면 IPI 생략 가능 —
         * 수신 CPU가 이전 IPI 처리 중 새 항목도 함께 처리 */
}

/* flush_smp_call_function_queue: 수신 CPU에서 큐 처리 */
static void flush_smp_call_function_queue(bool warn_cpu_offline)
{
    struct llist_head *head;
    struct llist_node *entry;

    head = this_cpu_ptr(&call_single_queue);
    entry = llist_del_all(head);  /* 원자적으로 전체 추출 */
    entry = llist_reverse_order(entry);  /* FIFO 순서로 정렬 */

    /* 각 CSD 순회하며 콜백 실행 */
    llist_for_each_entry_safe(csd, csd_next, entry, node.lentry) {
        csd->func(csd->info);
        csd_unlock(csd);  /* CSD_FLAG_LOCK 해제 → sender의 wait 종료 */
    }
}
CSD Lock 디버깅 (CONFIG_CSD_LOCK_WAIT_DEBUG): csd_lock_wait()에서 대상 CPU의 응답이 sysctl csd_lock_timeout(기본 5초)을 초과하면 커널이 WARNING을 출력하고 대상 CPU의 backtrace를 덤프합니다. 이는 대상 CPU가 인터럽트를 오래 비활성화하거나, 하드 락업(hard lockup)에 빠진 경우를 진단하는 데 유용합니다. sysctl -w kernel.csd_lock_timeout=10으로 임계값을 조정할 수 있습니다.

IPI 성능 분석 상세

IPI의 지연 시간(latency)은 CPU 토폴로지, APIC 모드, 메모리 아키텍처에 따라 크게 달라집니다. 정확한 IPI 비용을 측정하고 병목을 분석하는 방법을 상세히 살펴봅니다.

IPI 성능 측정 흐름: Sender Latency와 Receiver Overhead Sender CPU Receiver CPU T0: ICR Write 시작 rdtsc() T1: ICR Write 완료 ICR Write 50~300ns Bus/Interconnect 전달 T2: 인터럽트 수신 T3: IDT 핸들러 진입 Receiver Overhead 200~800ns T4: func(info) 실행 T5: csd_unlock / EOI 완료 통지 (CSD_FLAG_LOCK 해제) T6: csd_lock_wait 종료 rdtsc() 전체 왕복: 0.5~5us 같은 다이: 0.5~1us | 같은 소켓: 1~2us | 다른 소켓 (NUMA): 2~5us | 다른 노드 (QPI/UPI): 3~8us

IPI Latency 측정 방법

/* IPI 왕복 지연 시간 측정 커널 모듈 예제 */

#include <linux/module.h>
#include <linux/smp.h>
#include <linux/ktime.h>

static ktime_t ipi_start, ipi_end;
static volatile int ipi_done;

static void ipi_callback(void *info)
{
    /* 수신 CPU에서 실행 — 최소한의 작업만 수행 */
    ipi_done = 1;
}

static void measure_ipi_latency(int target_cpu)
{
    int i;
    s64 total_ns = 0;
    int iterations = 10000;

    for (i = 0; i < iterations; i++) {
        ipi_done = 0;
        smp_wmb();

        ipi_start = ktime_get();
        smp_call_function_single(target_cpu,
                                  ipi_callback, NULL, 1);
        ipi_end = ktime_get();

        total_ns += ktime_to_ns(
            ktime_sub(ipi_end, ipi_start));
    }

    pr_info("IPI latency to CPU %d: avg %lld ns\n",
            target_cpu, total_ns / iterations);
}

/* 실행 결과 예시 (Intel Xeon Platinum 8380):
 * 같은 CCX(Core Complex):  avg  620 ns
 * 같은 소켓 다른 CCX:      avg 1240 ns
 * 다른 소켓 (NUMA hop):    avg 3180 ns */

NUMA 토폴로지와 IPI 비용

토폴로지 관계평균 IPI 왕복원인최적화 방법
같은 코어 (SMT/HT) ~300-500ns L1/L2 캐시 공유 가장 빠름, 추가 최적화 불필요
같은 다이, 다른 코어 ~500-1200ns L3 캐시 경유 cpuset으로 동일 다이 바인딩
같은 소켓, 다른 다이 ~1000-2000ns 소켓 내부 인터커넥트 NUMA-aware 메모리 배치
다른 소켓 (NUMA) ~2000-5000ns QPI/UPI 인터커넥트 numactl --cpubind, 교차 소켓 IPI 최소화
다른 NUMA 노드 (4+ 소켓) ~3000-8000ns 멀티 홉 인터커넥트 프로세스/메모리 동일 노드 제한
# NUMA 토폴로지에 따른 IPI 영향 측정

# 1. NUMA 토폴로지 확인
numactl --hardware
# available: 2 nodes (0-1)
# node 0 cpus: 0 1 2 3 4 5 6 7
# node 1 cpus: 8 9 10 11 12 13 14 15
# node distances:
# node   0   1
#   0:  10  21
#   1:  21  10

# 2. perf로 IPI 이벤트와 NUMA 관계 분석
perf stat -e 'irq_vectors:call_function_entry' \
          -e 'irq_vectors:call_function_single_entry' \
          --per-socket -a -- sleep 10

# 3. /proc/interrupts로 소켓별 IPI 분포 확인
# CAL 카운터가 원격 소켓 CPU에서 높으면
# cross-NUMA IPI가 빈번하다는 의미

# 4. 프로세스를 특정 NUMA 노드에 바인딩하여 IPI 최소화
numactl --cpubind=0 --membind=0 ./application

ARM IPI: GICv3 SGI

ARM 아키텍처에서 IPI는 GIC(Generic Interrupt Controller)의 SGI(Software Generated Interrupt)를 통해 구현됩니다. x86의 APIC/ICR에 해당하는 역할을 GICv3의 ICC_SGI1_EL1 시스템 레지스터가 담당합니다.

ARM GICv3 SGI (Software Generated Interrupt) 전달 경로 CPU 0 (Sender) GIC CPU Interface ICC_SGI1_EL1 SGI ID + TargetList GIC Distributor (GICD) SGI 라우팅 결정 INTID 0-15: SGI (소프트웨어 생성) Affinity 기반 대상 CPU 선택 CPU 1 (Target) GIC CPU Interface ICC_IAR1_EL1 인터럽트 ID 읽기 SGI IRQ 리눅스 커널 IPI 추상화 계층 smp_cross_call() gic_ipi_send_mask() gic_send_sgi() ARM IPI 유형 (SGI ID 매핑) SGI 0: IPI_RESCHEDULE SGI 1: IPI_CALL_FUNC SGI 2: IPI_CPU_STOP SGI 3: IPI_IRQ_WORK SGI 4: IPI_TIMER (NOHZ) SGI 5: IPI_CPU_BACKTRACE x86 APIC vs ARM GIC 비교 전송 레지스터: x86: ICR (0xFEE00300) ARM: ICC_SGI1_EL1 (sys reg) 인터럽트 ID 범위: x86: 벡터 0xF0-0xFF (고정) ARM: SGI 0-15 (동적 할당)
/* drivers/irqchip/irq-gic-v3.c — GICv3 SGI 기반 IPI 구현 */

/* ARM IPI 유형 정의 */
enum ipi_msg_type {
    IPI_RESCHEDULE,          /* SGI 0: 스케줄러 리밸런싱 */
    IPI_CALL_FUNC,           /* SGI 1: smp_call_function */
    IPI_CPU_STOP,            /* SGI 2: CPU 정지 (panic/reboot) */
    IPI_IRQ_WORK,            /* SGI 3: IRQ Work */
    IPI_TIMER,               /* SGI 4: NOHZ tick broadcast */
    IPI_CPU_BACKTRACE,       /* SGI 5: 디버그 backtrace 수집 */
    NR_IPI,
};

/* GICv3 SGI 전송 — ICC_SGI1_EL1 레지스터 기록 */
static void gic_send_sgi(u64 cluster_id, u16 tlist,
                          unsigned int irq)
{
    u64 val;
    /* ICC_SGI1_EL1 레지스터 구조:
     * [55:48] Aff3, [39:32] Aff2, [23:16] Aff1
     * [15:0]  TargetList (비트마스크)
     * [27:24] INTID (SGI 번호 0-15) */
    val = (cluster_id & 0xff00ff0000ffULL) |
          ((u64)tlist << 0) |
          ((u64)irq << 24);
    gic_write_sgi1r(val);  /* MSR ICC_SGI1_EL1, val */
}

/* 커널 IPI 추상화 — 아키텍처 독립적 인터페이스 */
static void gic_ipi_send_mask(struct irq_data *d,
                               const struct cpumask *mask)
{
    int cpu;
    for_each_cpu(cpu, mask) {
        u64 cluster_id = MPIDR_TO_SGI_CLUSTER_ID(
            cpu_logical_map(cpu));
        u16 tlist = 1 << MPIDR_TO_SGI_RS(
            cpu_logical_map(cpu));
        gic_send_sgi(cluster_id, tlist, d->hwirq);
    }
}

/* ARM IPI 수신 핸들러 */
static void handle_IPI(int ipinr)
{
    switch (ipinr) {
    case IPI_RESCHEDULE:
        scheduler_ipi();   /* x86과 동일한 경로 */
        break;
    case IPI_CALL_FUNC:
        generic_smp_call_function_interrupt();
        break;
    case IPI_CPU_STOP:
        local_cpu_stop();
        break;
    case IPI_IRQ_WORK:
        irq_work_run();
        break;
    }
}
ARM IPI vs x86 IPI 핵심 차이: ARM의 GICv3는 SGI를 INTID 0-15로 할당하여 최대 16개의 소프트웨어 인터럽트를 지원합니다. x86은 벡터 0xF0-0xFF 범위의 고정 벡터를 사용합니다. 또한 ARM은 MPIDR(Multiprocessor Affinity Register) 기반의 Affinity 라우팅을 사용하여 대상 CPU를 클러스터 단위로 지정할 수 있으며, 이는 big.LITTLE과 같은 이기종 멀티코어 구조에 적합합니다. 리눅스 커널은 smp_cross_call() 추상화를 통해 아키텍처별 차이를 투명하게 처리합니다.

IPI 디버깅 심화

IPI 관련 문제는 시스템 전체의 안정성과 성능에 영향을 미치므로, 체계적인 디버깅 방법이 중요합니다. 여기서는 ftrace, perf, /proc 인터페이스를 활용한 고급 디버깅 기법을 다룹니다.

/proc/interrupts IPI 카운터 상세

# /proc/interrupts IPI 카운터 상세 분석
cat /proc/interrupts | head -1 && cat /proc/interrupts | grep -E "RES|CAL|TLB|IRQ|LOC"
#            CPU0       CPU1       CPU2       CPU3
# LOC:   15234567   14987234   15123456   15045678   Local timer interrupts
# RES:    1245832    1189244    1023444    1145678   Rescheduling interrupts
# CAL:     432109     398234     445312     412890   Function call interrupts
# TLB:     234567     198432     245123     213456   TLB shootdowns

# 분석 포인트:
# 1. RES 카운터가 LOC 대비 높으면 → 태스크 마이그레이션 빈번
# 2. CAL 카운터가 갑자기 증가 → smp_call_function 호출 급증
# 3. TLB 카운터가 특정 CPU에 편중 → 해당 CPU의 mm 변경 빈번
# 4. CPU 간 카운터 편차가 크면 → 워크로드 불균형

# 초 단위 IPI 증가율 측정 스크립트
while true; do
    grep "CAL" /proc/interrupts | awk '{sum=0; for(i=2;i<=NF-1;i++) sum+=$i; print strftime("%H:%M:%S"), "CAL total:", sum}'
    sleep 1
done

ftrace IPI 이벤트 추적

# ftrace로 IPI 발생 원인과 흐름 추적

# 1. IPI 관련 tracepoint 목록 확인
ls /sys/kernel/debug/tracing/events/ipi/
# ipi_raise  ipi_entry  ipi_exit

# 2. IPI raise 이벤트 활성화 (누가 IPI를 보내는지)
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo > /sys/kernel/debug/tracing/trace
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_raise/enable
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_exit/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace

# 출력 예시:
# migration/0-12 [000] ipi_raise: target_mask=2 (Function call)
# <idle>-0      [001] ipi_entry: (Function call)
# <idle>-0      [001] ipi_exit:  (Function call)

# 3. function_graph 트레이서로 IPI 핸들러 호출 체인 추적
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo sysvec_call_function_single > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 2
cat /sys/kernel/debug/tracing/trace
# 출력: sysvec_call_function_single → flush_smp_call_function_queue
#       → flush_tlb_func → __flush_tlb_one_user (INVLPG)

# 4. perf record로 IPI 핸들러 프로파일링
perf record -e 'irq_vectors:call_function_entry' \
            -e 'irq_vectors:call_function_exit' \
            --call-graph dwarf -a -- sleep 10
perf report
BPF를 활용한 고급 IPI 모니터링: bpftrace를 사용하면 IPI 이벤트를 실시간으로 집계하고 히스토그램으로 시각화할 수 있습니다.
# bpftrace로 IPI 지연 시간 히스토그램 수집
bpftrace -e '
tracepoint:ipi:ipi_entry {
    @start[cpu] = nsecs;
}
tracepoint:ipi:ipi_exit /@start[cpu]/ {
    @ipi_latency_ns = hist(nsecs - @start[cpu]);
    delete(@start[cpu]);
}
interval:s:10 { exit(); }
'
# 출력 예시:
# @ipi_latency_ns:
# [64, 128)    45 |@@@@                    |
# [128, 256)  312 |@@@@@@@@@@@@@@@@@@@@@@@@|
# [256, 512)  198 |@@@@@@@@@@@@@@@@        |
# [512, 1K)    87 |@@@@@@@@                |
# [1K, 2K)     23 |@@                      |
# [2K, 4K)      5 |                        |

일반적인 실수와 주의사항

IPI를 사용하거나 IPI 관련 코드를 작성할 때 흔히 발생하는 실수들과 그 해결 방법을 정리합니다.

데드락 (Deadlock)

/* 실수 1: IPI 콜백에서 sleep 가능 함수 호출 → 데드락 */

/* 잘못된 코드 */
static void bad_ipi_callback(void *info)
{
    /* IPI 핸들러는 인터럽트 컨텍스트에서 실행됨!
     * mutex_lock은 sleep 가능 → BUG: scheduling while atomic */
    mutex_lock(&my_mutex);        /* 금지! */
    kmalloc(1024, GFP_KERNEL);    /* 금지! GFP_KERNEL은 sleep 가능 */
    msleep(100);                   /* 금지! */
}

/* 올바른 코드 */
static void good_ipi_callback(void *info)
{
    spin_lock(&my_spinlock);      /* spin_lock은 안전 */
    kmalloc(1024, GFP_ATOMIC);    /* GFP_ATOMIC은 안전 */
    spin_unlock(&my_spinlock);
}

/* 실수 2: 교차 IPI 데드락 */

/* CPU 0이 CPU 1에 wait=1로 IPI를 보내고,
 * 동시에 CPU 1이 CPU 0에 wait=1로 IPI를 보내면
 * 양쪽 모두 상대의 응답을 기다리며 데드락 발생 */

/* CPU 0: */
smp_call_function_single(1, func_a, NULL, 1);  /* wait=1 */
/* CPU 1 (동시에): */
smp_call_function_single(0, func_b, NULL, 1);  /* wait=1 → 데드락! */

/* 해결: wait=0 사용하거나, 한 방향으로만 동기 호출 */
smp_call_function_single(1, func_a, NULL, 0);  /* wait=0: 비동기 */

IPI Storm 방지

/* 실수 3: 루프에서 반복적인 smp_call_function 호출 → IPI storm */

/* 잘못된 코드 — 페이지마다 개별 TLB flush IPI */
for (i = 0; i < num_pages; i++) {
    unmap_page(pages[i]);
    flush_tlb_page(vma, pages[i]->addr);
    /* 매 반복마다 IPI 전송 → num_pages * num_cpus 개의 IPI! */
}

/* 올바른 코드 — batch flush */
tlb_gather_mmu(&tlb, mm);
for (i = 0; i < num_pages; i++) {
    unmap_page(pages[i]);
    tlb_remove_page(&tlb, pages[i]);
}
tlb_finish_mmu(&tlb);  /* 모든 변경 후 단 한 번의 IPI */

/* 실수 4: on_each_cpu() 남용 */

/* 잘못된 코드 — 모든 CPU에 불필요하게 IPI */
on_each_cpu(update_something, NULL, 1);
/* 128코어 시스템에서 127개의 IPI 발생! */

/* 올바른 코드 — 필요한 CPU만 대상으로 */
on_each_cpu_mask(&affected_cpus, update_something, NULL, 1);
/* 또는 조건부 실행 */
on_each_cpu_cond(needs_update, update_something, NULL, 1);

과도한 TLB Flush 회피

/* 실수 5: 공유 메모리 워크로드에서 과도한 TLB shootdown */

/* 문제 상황: 다수의 프로세스가 같은 메모리를 mmap/munmap
 * → 매번 mm_cpumask의 모든 CPU에 TLB flush IPI
 *
 * 해결 방법:
 * 1. huge page 사용 — TLB 엔트리 수 감소
 *    mmap(NULL, size, prot, MAP_HUGETLB | MAP_HUGE_2MB, ...)
 *
 * 2. madvise(MADV_FREE) — 즉시 unmap 대신 지연 해제
 *    madvise(addr, len, MADV_FREE);
 *
 * 3. 프로세스를 같은 NUMA 노드에 바인딩
 *    → cross-socket TLB flush IPI 제거
 *
 * 4. 메모리 풀링 — 빈번한 mmap/munmap 대신
 *    사전 할당된 메모리 풀 사용 */

/* 진단: TLB shootdown 빈도 모니터링 */
/* perf stat -e 'tlb:tlb_flush' -a -- sleep 10
 * 또는 /proc/vmstat의 nr_tlb_remote_flush 카운터 확인 */

IPI 관련 주의사항 요약 테이블

실수 유형증상해결 방법
IPI 콜백에서 sleep 가능 함수 호출 BUG: scheduling while atomic GFP_ATOMIC, spin_lock 등 원자적 API만 사용
교차 IPI 데드락 시스템 정지 (hard lockup) wait=0 비동기 호출, 또는 단방향 IPI 구조
루프 내 반복 IPI 전송 IPI storm, CPU 사용률 급증 batch flush (tlb_gather_mmu 등)
on_each_cpu() 남용 불필요한 대규모 IPI on_each_cpu_mask/on_each_cpu_cond 사용
과도한 TLB shootdown TLB 카운터 폭증, 지연 증가 huge page, PCID, lazy TLB, INVLPGB 활용
인터럽트 비활성화 상태에서 IPI 대기 CSD lock timeout WARNING critical section 최소화, local_irq_enable 보장
커널 패닉 위험: IPI 콜백 함수에서 예외(page fault 등)가 발생하면 커널 패닉으로 이어질 수 있습니다. IPI 핸들러는 인터럽트 컨텍스트에서 실행되므로, 유저 공간 메모리 접근(copy_from_user 등)이나 페이지 폴트를 유발할 수 있는 작업은 반드시 피해야 합니다. 필요한 데이터는 IPI 전송 전에 커널 메모리에 복사해 두고, 콜백에서는 해당 커널 메모리만 참조하세요.

IPI와 메모리 순서 보장

IPI는 단순한 "알림"이 아니라 CPU 간 메모리 가시성(memory visibility)을 맞추는 동기화 지점으로도 사용됩니다. 핵심은 데이터 기록이 먼저, IPI 전송이 나중, 그리고 수신 CPU는 IPI 수신 후 데이터 관측 순서를 강제하는 것입니다. 이를 위해 커널은 smp_wmb(), smp_rmb(), smp_mb()와 CSD 플래그의 원자적 상태 전이를 결합합니다.

IPI + 메모리 배리어로 데이터 가시성 보장 CPU 0 (Sender) 1) shared_data 필드 업데이트 2) smp_wmb()로 store 순서 고정 3) CSD 큐 삽입 + IPI 전송 4) wait=1이면 csd_lock_wait() 5) unlock 관측 후 후속 로직 실행 CPU 1 (Receiver) A) IDT 진입 후 IPI 핸들러 실행 B) smp_rmb()/implicit barrier C) shared_data 읽고 콜백 실행 D) csd_unlock()로 완료 신호 게시 CALL_FUNCTION_SINGLE IPI CSD unlock 관측
/* IPI 전후 메모리 순서 예시: 공유 데이터 게시 후 원격 CPU 실행 */

struct ipi_payload {
    u64 seq;
    u64 flags;
    u64 ptr_val;
};

static struct ipi_payload shared_payload;

static void remote_consume_payload(void *info)
{
    struct ipi_payload *p = info;
    /* 수신 측에서 payload 필드 관측 */
    READ_ONCE(p->seq);
    READ_ONCE(p->flags);
    READ_ONCE(p->ptr_val);
}

void send_payload_ipi(int cpu, u64 seq, u64 flags, u64 ptr)
{
    /* 1) 공유 데이터 게시 */
    WRITE_ONCE(shared_payload.seq, seq);
    WRITE_ONCE(shared_payload.flags, flags);
    WRITE_ONCE(shared_payload.ptr_val, ptr);

    /* 2) 데이터 store가 IPI보다 뒤로 밀리지 않도록 보장 */
    smp_wmb();

    /* 3) 원격 CPU에 동기 IPI 전송 */
    smp_call_function_single(cpu, remote_consume_payload,
                             &shared_payload, 1);
}
핵심 규칙: "shared state를 쓰고 IPI를 보낸다"면 반드시 순서 보장을 고려해야 합니다. lock으로 보호되지 않은 구조체를 여러 CPU가 읽고 쓸 때는 READ_ONCE/WRITE_ONCE + smp_mb 계열 조합을 명시적으로 사용하세요.

가상화 환경의 IPI (KVM/Hypervisor)

가상화에서는 IPI 경로가 추가 계층을 통과합니다. 게스트 커널이 APIC ICR을 기록해도 실제 물리 CPU 인터럽트로 바로 가지 않고, VM-exit 혹은 APICv/posted-interrupt 경로를 거쳐 하이퍼바이저가 중개합니다. 이때 불필요한 VM-exit이 많으면 IPI 지연이 급증합니다.

Guest IPI 전달: APIC 에뮬레이션 vs APICv/Posted Interrupt 경로 1: 전통적 에뮬레이션 (비용 큼) Guest vCPU0 ICR write VM-exit KVM APIC emul Guest vCPU1 inject 경로 2: APICv/Posted Interrupt (비용 낮음) Guest vCPU0 ICR write VT-x APICv assist Posted-Interrupt Guest vCPU1 바로 수신
/* arch/x86/kvm/lapic.c 계열 흐름 요약 */

/* 게스트가 ICR을 쓸 때 KVM이 가로채어 대상 vCPU에 IPI 주입 */
int kvm_apic_send_ipi(struct kvm_lapic *apic, u32 icr_low,
                      u32 icr_high)
{
    struct kvm *kvm = apic->vcpu->kvm;
    struct kvm_lapic_irq irq;

    /* ICR 비트 디코딩: 벡터, delivery mode, destination */
    irq.vector = icr_low & 0xff;
    irq.delivery_mode = (icr_low >> 8) & 0x7;
    irq.dest_id = icr_high >> 24;

    /* 대상 vCPU 선택 후 인터럽트 주입 */
    return kvm_irq_delivery_to_apic(kvm, apic, &irq, NULL);
}

/* 성능 관점:
 * - APICv 비활성: ICR write마다 VM-exit 가능
 * - APICv 활성: 일부 경로에서 VM-exit 없이 하드웨어 전달 */
가상화 튜닝 포인트: 대규모 vCPU VM에서 RES/CAL/TLB IPI가 많다면, 호스트의 VT-x APICv/AMD AVIC 지원 여부와 활성 상태를 먼저 확인하세요. NUMA를 무시한 vCPU 핀닝은 가상 IPI 비용을 더 악화시킵니다.

대규모 코어에서 IPI 확장 전략

64코어 이상 서버에서는 "기능이 맞다"보다 "브로드캐스트를 줄였는가"가 중요합니다. 같은 코드라도 대상 CPU 마스크 설계에 따라 IPI량이 수십 배 차이 납니다. 실무에서는 대상 축소, 배치 처리, 비동기화 3가지를 기본 축으로 최적화합니다.

Broadcast 지양, 조건부 대상화, 배치 처리 나쁜 패턴 작은 이벤트마다 on_each_cpu(wait=1) 모든 CPU에 동기 IPI 페이지 단위 flush_tlb_page 반복 TLB IPI 폭증 교차 소켓 무차별 IPI NUMA latency 증가 결과: RES/CAL/TLB 카운터 급등, tail latency 악화 좋은 패턴 on_each_cpu_mask/on_each_cpu_cond 실제 영향 CPU만 타겟 tlb_gather_mmu + tlb_finish_mmu 배치 flush로 IPI 횟수 축소 소켓/LLC 로컬 우선 배치 cross-node IPI 최소화 결과: IPI/lock 대기 감소, 처리량/지연 안정화
/* 대상 CPU 최소화 패턴 예시 */

static bool needs_remote_update(int cpu, void *info)
{
    struct update_ctx *ctx = info;
    /* 실제 해당 mm을 사용하는 CPU + online CPU만 허용 */
    if (!cpu_online(cpu))
        return false;
    if (!cpumask_test_cpu(cpu, ctx->active_mask))
        return false;
    return true;
}

static void do_remote_update(void *info)
{
    /* 인터럽트 컨텍스트 안전 작업만 수행 */
}

void run_scaled_ipi_update(struct update_ctx *ctx)
{
    /* 전체 CPU 브로드캐스트 대신 조건부 실행 */
    on_each_cpu_cond(needs_remote_update, do_remote_update, ctx, 1);
}

실전 측정 실험실

IPI 튜닝은 추측이 아니라 계측으로 진행해야 합니다. 아래 절차는 "수정 전 기준선"과 "수정 후 개선치"를 재현 가능하게 비교하기 위한 실험 템플릿입니다.

IPI 튜닝 실험 절차 (Baseline → Change → Verify) 1) Baseline /proc/interrupts perf/ftrace 수집 2) Hotspot 식별 CAL/TLB 급증 지점 호출 스택 확인 3) 코드 변경 대상 CPU 축소 배치 flush 적용 4) 재계측 동일 부하 재실행 증감률 비교 판정 기준 예시 CAL/sec 30% 이상 감소, TLB/sec 20% 이상 감소, p99 latency 악화 없음 CSD timeout 경고 0건, workload throughput 유지 또는 증가
# 1) 기준선 수집: 30초 동안 IPI 이벤트
perf stat -e 'irq_vectors:reschedule_entry' \
          -e 'irq_vectors:call_function_entry' \
          -e 'irq_vectors:call_function_single_entry' \
          -e 'irq_vectors:irq_work_entry' \
          -a -- sleep 30

# 2) /proc/interrupts 스냅샷 비교
grep -E "RES|CAL|TLB|IRQ_WORK" /proc/interrupts > /tmp/ipi.before
# 튜닝 적용 후
sleep 30
grep -E "RES|CAL|TLB|IRQ_WORK" /proc/interrupts > /tmp/ipi.after
diff -u /tmp/ipi.before /tmp/ipi.after

# 3) ftrace로 원인 함수 확인
echo 0 > /sys/kernel/debug/tracing/tracing_on
echo > /sys/kernel/debug/tracing/trace
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_raise/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/debug/tracing/tracing_on
tail -n 80 /sys/kernel/debug/tracing/trace
실험 팁: IPI 카운터는 워크로드 민감도가 높으므로, CPU 주파수 governor, NUMA 바인딩, 프로세스 수를 고정한 동일 조건에서 반복 측정해야 합니다. 단일 실행 결과보다 최소 5회 반복 평균을 기준으로 판단하세요.

CPU Hotplug/Affinity와 IPI 경로 안정성

운영 중 CPU online/offline, cpuset 변경, IRQ affinity 재배치는 IPI 대상 마스크를 바꿉니다. 특히 대규모 코어에서 hotplug 이벤트가 잦으면 on_each_cpu_mask() 계열 호출의 비용 편차가 커지고, 잠깐의 마스크 불안정 구간에서 work가 재시도되는 현상이 나타날 수 있습니다.

CPU Hotplug 시 IPI 대상 마스크 변화 Step 1 cpu_online_mask 기준 fanout Step 2 hotplug 이벤트 발생 Step 3 대상 마스크 재계산/재시도 운영 체크포인트 1) housekeeping CPU와 nohz_full CPU 분리 유지 2) hotplug 자동화 스크립트에서 IRQ affinity 재적용 순서 고정 3) TLB/CAL 급증 시 cpuset 재배치와 동시 발생 여부 확인
현상가능 원인점검 포인트
CAL IPI 순간 급증CPU online 직후 마스크 재동기화/proc/interrupts 시계열, hotplug 로그
TLB IPI 편차 확대cpuset 이동과 mm 공유 패턴 변화워크로드 스레드 pinning 정책
지연 꼬리(p99) 증가housekeeping CPU 과부하ksoftirqd, irqbalance, nohz_full 설정

IPI와 RCU/stop_machine 상호작용

커널의 전역 동기화 경로(예: synchronize_rcu(), 일부 stop_machine 기반 작업)는 CPU 간 상태 수렴을 위해 IPI와 밀접하게 연결됩니다. 일반 경로 IPI 튜닝이 잘 되어도, 전역 동기화 구간이 겹치면 지연 꼬리가 갑자기 늘어날 수 있습니다.

전역 동기화 경로에서의 IPI 영향 워크로드 스레드 일반 IPI 트래픽 RCU grace period / stop_machine 전역 상태 수렴 대기 IPI 지연 꼬리 증가 p99/p999 악화 운영 대응 1) RCU stall 경고와 IPI 급증 타임라인을 함께 분석 2) 대규모 코어에서 hotplug/affinity 변경 배치 작업 시간대 분리 3) latency 민감 CPU의 housekeeping 오염 최소화
# RCU/스케줄링/IPI 지표를 함께 본다
dmesg | grep -Ei 'rcu|stall|soft lockup'
grep -E "RES|CAL|TLB|IRQ_WORK" /proc/interrupts
cat /sys/kernel/debug/tracing/trace_pipe | grep -E 'ipi_|sched_switch|rcu'

IPI와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.

권장 학습 순서: 인터럽트Softirq/HardirqIPI (Inter-Processor Interrupt)CPU 토폴로지 순서로 보면 인터럽트 기반 CPU 간 협조 메커니즘이 자연스럽게 연결됩니다.