IPI (Inter-Processor Interrupt)

IPI(Inter-Processor Interrupt)는 SMP 시스템에서 한 CPU가 다른 CPU에게 보내는 특수한 인터럽트(Interrupt)입니다. x86 APIC/ICR 아키텍처를 기반으로 스케줄러(Scheduler) 리밸런싱, TLB 캐시 일관성(Cache Coherency), 원격 함수 호출(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에서 콜백(Callback) 함수를 실행하는 범용 API입니다.
  • TLB Flush IPI — 페이지 테이블(Page Table) 변경 시 관련 CPU들의 TLB를 무효화(Invalidation)합니다.
  • 성능 영향 — IPI 왕복 비용은 0.5~5us이며, 대규모 SMP에서 IPI storm은 심각한 병목(Bottleneck)이 됩니다.

단계별 이해

  1. APIC 하드웨어 이해
    Local APIC와 ICR 레지스터(Register) 구조를 파악합니다. 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 캐시 일관성, 원격 함수 호출, 타이머(Timer) 동기화 등 CPU 간 협조가 필요한 거의 모든 작업에 IPI가 관여합니다.

SMP 시스템에서 각 CPU는 독립적으로 명령을 실행하지만, 공유 자원(메모리, 페이지 테이블, 스케줄러 데이터 등)에 대한 일관성을 유지하려면 CPU 간 조율이 필수적입니다. IPI는 이러한 조율을 위한 가장 저수준의 하드웨어 메커니즘으로, 소프트웨어 폴링(Polling)이나 공유 메모리 플래그 방식보다 즉각적인 반응을 보장합니다. 수신 CPU는 현재 실행 중인 작업을 인터럽트하고 즉시 IPI 핸들러를 실행하므로, 지연(Latency)이 마이크로초 이하로 매우 짧습니다.

SMP 시스템에서 IPI의 역할 CPU 0 Local APIC Task A 실행 중 CPU 1 Local APIC Task B 실행 중 CPU 2 Local APIC idle 상태 CPU N Local APIC 커널 코드 실행 중 주요 IPI 유형과 사용처 Reschedule IPI 스케줄러 리밸런싱 TIF_NEED_RESCHED 설정 Call Function IPI 원격 CPU에서 함수 실행 smp_call_function API TLB Flush IPI 페이지 테이블 변경 동기화 INVLPG / CR3 reload 특수 IPI NMI, INIT-SIPI-SIPI IRQ Work, Reboot 디바이스 인터럽트 발생원: 외부 하드웨어 (NIC, 디스크, USB...) 경로: 디바이스 → I/O APIC → Local APIC → CPU 벡터: 0x20~0xEF (동적 할당) 목적: I/O 완료 통지, 이벤트 알림 IPI (Inter-Processor Interrupt) 발생원: CPU 자체 (ICR 레지스터 기록) 경로: CPU → Local APIC → Bus → 대상 APIC 벡터: 0xF0~0xFF (시스템 예약, 고정) 목적: CPU 간 협조, 동기화, 제어
IPI는 외부 디바이스가 아닌 CPU 자체가 발생시키는 인터럽트로, 시스템 예약 벡터(0xF0~0xFF)를 사용하여 디바이스 인터럽트와 충돌하지 않습니다.

IPI의 주요 특성을 정리하면 다음과 같습니다:

특성디바이스 인터럽트IPI
발생원 외부 하드웨어 (NIC, 블록 디바이스 등) CPU 자체 (ICR 레지스터 기록)
전달 경로 디바이스 → I/O APIC → Local APIC Local APIC → System Bus → 대상 Local APIC
벡터 범위 0x20~0xEF (동적 할당) 0xF0~0xFF (시스템 예약)
대상 선택 I/O APIC가 라우팅 (IRQ Affinity) 송신 CPU가 ICR로 직접 지정
일반적 지연 수 us ~ 수십 us 0.5~5 us (CPU 간 거리에 따라)
주 용도 I/O 완료 통지 CPU 간 협조, 동기화

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 지연(Latency) 시간이 크게 줄어듭니다. 최신 시스템에서는 x2APIC가 기본입니다.

리눅스 IPI 벡터 유형

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

IPI 벡터벡터 번호용도핸들러(Handler)
RESCHEDULE_VECTOR 0xFD 대상 CPU의 스케줄러를 깨워 태스크(Task) 재배치(Relocation) 유도 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는 디바이스 인터럽트와 충돌하지 않음 */
x86 인터럽트 벡터 공간 (0x00~0xFF) CPU 예외 디바이스 인터럽트 (0x20~0xEF) IPI/시스템 (0xF0~0xFF) 0x00 0x20 0xF0 0xFF 시스템 예약 벡터 상세 (0xF0~0xFF) 0xFF SPURIOUS 가짜 인터럽트 0xFE ERROR_APIC APIC 오류 0xFD RESCHEDULE 가장 빈번 0xFC CALL_FUNC 다중 CPU 호출 0xFB CALL_SINGLE 단일 CPU 호출 0xFA REBOOT 재부팅 정지 0xF7 PLATFORM UV/Xen 전용 0xF6 IRQ WORK 일반적인 IPI 빈도 비교 (상대적) Reschedule (RES) ~50% TLB Flush (TLB) ~30% Call Function (CAL) ~15% 기타 (IRQ Work 등) ~5%
x86 벡터 공간에서 시스템 예약 영역(0xF0~0xFF)은 IPI 전용으로 분리되어 디바이스 인터럽트와 충돌하지 않습니다. 일반적인 워크로드에서 Reschedule IPI가 전체 IPI의 약 절반을 차지합니다.

Reschedule IPI

Reschedule IPI는 커널에서 가장 빈번하게 발생하는 IPI입니다. 한 CPU에서 태스크의 우선순위(Priority)가 변경되었거나, 로드 밸런서가 태스크를 다른 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 핸들러 자체는 거의 아무 작업도 하지 않습니다.

Reschedule IPI 전체 흐름 CPU 0 (발신) CPU 1 (수신) wake_up_process(task_on_cpu1) resched_curr(rq_of_cpu1) set_tsk_need_resched(curr) smp_send_reschedule(cpu1) RESCHEDULE_VECTOR (0xFD) sysvec_reschedule_ipi() ack_APIC_irq() scheduler_ipi() — 거의 빈 함수 인터럽트 리턴 경로 TIF_NEED_RESCHED → schedule() 핵심: IPI 핸들러는 작업 없이 인터럽트 자체가 목적
Reschedule IPI의 핵심은 대상 CPU에서 인터럽트를 발생시키는 것 자체이며, 인터럽트 리턴 시 TIF_NEED_RESCHED 플래그를 확인하여 schedule()을 호출합니다.

Reschedule IPI가 트리거되는 주요 경로는 다음과 같습니다:

트리거 경로함수설명
태스크 깨우기 try_to_wake_up() sleeping 태스크가 깨어날 때, 대상 CPU의 현재 태스크보다 우선순위가 높으면 IPI 전송
로드 밸런싱 load_balance() CPU 간 부하 불균형 시 태스크를 이동한 후 대상 CPU에 IPI 전송
우선순위 변경 check_preempt_curr() 새 태스크가 현재 태스크보다 높은 우선순위를 가지면 IPI로 선점 요청
RT 태스크 push push_rt_task() RT 스케줄러가 태스크를 다른 CPU로 밀어낼 때 IPI 전송
NUMA 밸런싱 task_numa_migrate() NUMA 노드 간 태스크 이동 후 대상 CPU에 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하므로, 데드락에 주의해야 합니다.

smp_call_function_single() CSD 큐 처리 흐름 CPU 0 (발신) CPU 1 (수신) CSD 구조체 준비 (func, info, flags) __smp_call_single_queue(cpu1, &csd) per-CPU call_single_queue llist (lock-free linked list) CSD 삽입 CALL_FUNCTION_SINGLE_VECTOR IPI 전송 IPI (0xFB) sysvec_call_function_single() flush_smp_call_function_queue() CSD 순회: csd->func(csd->info) 인터럽트 컨텍스트에서 실행! csd_unlock() — CSD_FLAG_LOCK 해제 wait=1인 경우 csd_lock_wait(&csd) — spin-wait 완료 통지 wait=0인 경우 즉시 리턴 (비동기 실행) smp_call_function_single() → 단일 CPU (0xFB) smp_call_function() → 전체 CPU (0xFC) on_each_cpu_mask() → 지정 CPU (0xFC)
CSD(Call Single Data)는 lock-free llist를 통해 대상 CPU의 per-CPU 큐에 삽입됩니다. IPI 수신 후 핸들러가 큐를 순회하며 콜백을 실행하고, wait=1이면 발신 측이 CSD lock 해제를 spin-wait합니다.

smp_call_function API 패밀리의 선택 기준을 정리합니다:

API대상IPI 벡터용도
smp_call_function_single() 특정 CPU 1개 0xFB 특정 CPU에서만 실행해야 하는 작업 (per-CPU 캐시 flush 등)
smp_call_function() 현재 CPU 제외 전체 0xFC 전체 CPU 동기화 (global TLB flush, 모듈 언로드 등)
on_each_cpu() 현재 CPU 포함 전체 0xFC 현재 CPU도 포함해야 하는 경우
on_each_cpu_mask() 지정 cpumask 0xFC/0xFB 관련 CPU만 대상 (TLB flush 최적화)
on_each_cpu_cond() 조건 만족 CPU 0xFC/0xFB 런타임 조건 기반 필터링
smp_call_function_single_async() 특정 CPU 1개 0xFB 항상 비동기, 호출자가 CSD 관리

TLB Flush IPI

페이지 테이블이 변경되면 해당 매핑(Mapping)을 사용하는 모든 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할 페이지(Page) 수가 많으면 전체 TLB flush(CR3 reload)로 전환합니다. 또한 lazy TLB 모드에서는 커널 스레드(Kernel Thread)처럼 유저 매핑이 불필요한 경우 TLB flush를 지연시킬 수 있습니다.

IRQ Work IPI

IRQ Work는 NMI나 하드 인터럽트(Hardirq) 컨텍스트처럼 일반적인 작업 큐(Workqueue)를 사용할 수 없는 상황에서도 안전하게 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);
}
IRQ Work IPI 처리 흐름 NMI / Hard IRQ 컨텍스트 직접 콘솔 출력 불가 sleep/schedule 불가 workqueue 사용 불가 irq_work_queue(work) per-CPU IRQ Work 리스트 raised_list (즉시 실행) lazy_list (다음 tick 실행) IRQ_WORK_LAZY 플래그로 구분 Self-IPI 전송 arch_irq_work_raise() IRQ_WORK_VECTOR (0xF6) lazy 아님 원격 CPU IPI irq_work_queue_on(work, cpu) IRQ Work 실행 경로 sysvec_irq_work() — Self-IPI 수신 irq_work_run() → raised_list 순회 work->func(work) 실행 timer tick — irq_work_tick() lazy_list 순회 (LAZY 작업) work->func(work) 실행 주요 사용처 printk() 콘솔 flush perf 이벤트 오버플로우 RCU 콜백 가속 timer 재프로그래밍 ftrace 버퍼 flush
IRQ Work는 NMI처럼 일반 작업 큐를 사용할 수 없는 컨텍스트에서 안전하게 작업을 예약합니다. raised_list의 작업은 self-IPI로 즉시, lazy_list의 작업은 다음 timer tick에서 실행됩니다.
ℹ️

IRQ Work vs Tasklet vs Workqueue: IRQ Work는 NMI에서도 안전하게 사용할 수 있는 유일한 deferred 실행 메커니즘입니다. Tasklet은 softirq 컨텍스트에서 실행되어 NMI에서 사용 불가하고, Workqueue는 프로세스(Process) 컨텍스트에서 실행됩니다. IRQ Work의 핵심은 llist_add()가 lock-free이므로 NMI와 같은 재진입 컨텍스트에서도 안전하다는 점입니다.

SMP 부팅 시 IPI 활용

멀티코어 시스템의 부팅 과정(Boot Process)에서 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 간 통신의 기본이지만 상당한 오버헤드(Overhead)를 수반합니다. 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 캐시(Cache) 공유 시 더 빠름
다른 소켓(Socket) 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 처리에 시간을 소모하면 실제 작업 처리량(Throughput)이 급격히 감소합니다. perf stat -e irq_vectors:call_function_entry로 IPI 빈도를 모니터링하고, on_each_cpu_cond()이나 cpumask 기반 API로 대상 CPU를 최소화하세요.

IPI 모니터링과 디버깅(Debugging)

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로 프로세스(Process)를 특정 노드에 바인딩하여 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비트 단일 기록으로 원자적(Atomic) 전송이 가능하며, 이로 인해 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 엔트리에 프로세스별 태그를 부여합니다. 이를 통해 컨텍스트 스위칭(Context Switching) 시 전체 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를 덤프(Dump)합니다. 이는 대상 CPU가 인터럽트를 오래 비활성화하거나, 하드 락업(hard lockup)에 빠진 경우를 진단하는 데 유용합니다. sysctl -w kernel.csd_lock_timeout=10으로 임계값을 조정할 수 있습니다.

IPI 성능 분석 상세

IPI의 지연 시간(latency)은 CPU 토폴로지(Topology), 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 라우팅(Routing)을 사용하여 대상 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 이벤트를 실시간(Real-time)으로 집계하고 히스토그램으로 시각화할 수 있습니다.
# 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 보장
커널 패닉(Kernel Panic) 위험: IPI 콜백 함수에서 예외(page fault 등)가 발생하면 커널 패닉으로 이어질 수 있습니다. IPI 핸들러는 인터럽트 컨텍스트에서 실행되므로, 유저 공간 메모리 접근(copy_from_user 등)이나 페이지 폴트(Page Fault)를 유발할 수 있는 작업은 반드시 피해야 합니다. 필요한 데이터는 IPI 전송 전에 커널 메모리에 복사해 두고, 콜백에서는 해당 커널 메모리만 참조하세요.

IPI와 메모리 순서 보장(Ordering)

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으로 보호되지 않은 구조체(Struct)를 여러 CPU가 읽고 쓸 때는 READ_ONCE/WRITE_ONCE + smp_mb 계열 조합을 명시적으로 사용하세요.

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

가상화에서는 IPI 경로가 추가 계층을 통과합니다. 게스트 커널이 APIC ICR을 기록해도 실제 물리 CPU 인터럽트로 바로 가지 않고, VM-exit 혹은 APICv/posted-interrupt 경로를 거쳐 하이퍼바이저(Hypervisor)가 중개합니다. 이때 불필요한 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 공유 패턴 변화워크로드 스레드(Thread) 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'

APIC 아키텍처 진화: 8259 PIC에서 x2APIC까지

IPI 메커니즘을 깊이 이해하려면 x86 인터럽트 컨트롤러(Interrupt Controller)가 어떤 경로를 거쳐 현재의 APIC 구조에 도달했는지를 먼저 파악해야 합니다. 각 세대는 IPI 전송 방식, 대상 CPU 수, 지연 시간에 근본적인 변화를 가져왔습니다.

x86 인터럽트 컨트롤러 진화 타임라인 8259 PIC (1981) 단일 CPU 전용 IRQ 0-15 (Master+Slave) I/O 포트 0x20/0xA0 IPI 불가 82489DX APIC (1993) 최초 SMP 지원 Local APIC + I/O APIC 전용 APIC Bus IPI 가능 (8비트 ID) xAPIC (P6/Pentium 4) 시스템 버스 사용 MMIO 0xFEE00000 ICR 32비트×2 최대 255 CPU x2APIC (Nehalem+) MSR 기반 접근 MSR 0x800-0x83F ICR 64비트 단일 기록 최대 2³² CPU 세대별 IPI 특성 비교 항목 82489DX xAPIC x2APIC ICR 접근 방식 APIC Bus 직접 MMIO (2회 기록) MSR (1회 WRMSR) APIC ID 폭 8비트 (0-255) 8비트 (0-255) 32비트 (0-4G) ICR Write 비용 ~300-500ns ~150-300ns ~50-100ns ICR Write 원자성 비원자적 비원자적 (2회 기록) 원자적 (1회 기록) 가상화 지원 없음 에뮬레이션 필요 APICv/AVIC 가속 Cluster Addressing 없음 Flat/Cluster 모드 Cluster 기본 내장 리눅스 커널 APIC 모드 감지 흐름 CPUID.01H:ECX[21] x2APIC? Yes IA32_APIC_BASE[10] x2APIC Enable 설정 x2apic_mode = 1 MSR 기반 IPI No xAPIC fallback MMIO 기반 IPI KVM/Hyper-V 환경: x2APIC 강제 활성화 (APICv)
/* arch/x86/kernel/apic/apic.c — APIC 모드 감지 및 초기화 */

/* x2APIC 감지 및 활성화 */
void __init check_x2apic(void)
{
    if (cpu_has(X86_FEATURE_X2APIC)) {
        /* CPUID.01H:ECX[21] = 1 → x2APIC 지원 */
        u64 msr;
        rdmsrl(MSR_IA32_APICBASE, &msr);

        if (msr & X2APIC_ENABLE) {
            /* 이미 x2APIC 모드 (BIOS/하이퍼바이저가 활성화) */
            x2apic_mode = 1;
        } else {
            /* x2APIC 활성화: MMIO → MSR 전환
             * 주의: xAPIC → x2APIC 전환은 되돌릴 수 없음
             * (하드 리셋 필요) */
            msr |= X2APIC_ENABLE;
            wrmsrl(MSR_IA32_APICBASE, msr);
            x2apic_mode = 1;
        }
    }
}

/* x2APIC 모드에서의 IPI 전송 — 단일 WRMSR */
static void x2apic_send_IPI(int cpu, int vector)
{
    u32 dest = per_cpu(x86_cpu_to_apicid, cpu);
    u64 cfg = ((u64)dest << 32) | APIC_DM_FIXED | vector;
    /* 원자적 64비트 MSR 기록 → race condition 없음 */
    native_x2apic_icr_write(cfg, 0);
}

/* xAPIC 모드에서의 IPI 전송 — 2회 MMIO 기록 필요 */
static void xapic_send_IPI(int cpu, int vector)
{
    u32 dest = per_cpu(x86_cpu_to_apicid, cpu);
    /* 반드시 ICR_HIGH를 먼저 기록! (ICR_LOW 기록이 전송 트리거)
     * 이 두 기록 사이에 다른 CPU가 ICR을 건드리면 race 발생
     * → xAPIC에서는 ICR 접근에 lock이 필요 */
    native_apic_mem_write(APIC_ICR2, dest << 24);
    native_apic_mem_write(APIC_ICR, APIC_DM_FIXED | vector);
}

/* apic 드라이버 구조체 — 아키텍처별 IPI 전송 함수 연결 */
struct apic {
    void (*send_IPI)(int cpu, int vector);
    void (*send_IPI_mask)(const struct cpumask *mask, int vector);
    void (*send_IPI_allbutself)(int vector);
    void (*send_IPI_self)(int vector);
    /* ... */
};

/* xAPIC → x2APIC 전환 타이밍:
 * 1. BIOS에서 이미 x2APIC 활성화 (일반적)
 * 2. 또는 커널 early boot에서 check_x2apic() → enable
 * 3. KVM 게스트에서는 하이퍼바이저가 강제 활성화
 *    (APICv/AVIC 가속을 위해 x2APIC 필수) */
APIC 세대도입 시기ICR 접근APIC ID 비트IPI 전송 비용최대 CPU 수
8259 PIC IBM PC (1981) 해당 없음 해당 없음 IPI 불가 1
82489DX APIC Pentium (1993) 전용 APIC Bus 8비트 ~300-500ns 15
xAPIC P6/Pentium 4 MMIO 2회 기록 8비트 ~150-300ns 255
x2APIC Nehalem (2008) MSR 1회 WRMSR 32비트 ~50-100ns 2³² (이론상)
xAPIC ICR Race Condition: xAPIC에서 ICR은 두 개의 32비트 레지스터(ICR_HIGH: 0xFEE00310, ICR_LOW: 0xFEE00300)로 분할되어 있습니다. ICR_LOW 기록이 실제 IPI 전송을 트리거하므로, 반드시 ICR_HIGH를 먼저 설정해야 합니다. 만약 CPU A가 ICR_HIGH를 기록한 후 ICR_LOW를 기록하기 전에 CPU B가 ICR_HIGH를 다른 값으로 덮어쓰면, CPU A의 IPI가 잘못된 대상으로 전송됩니다. 따라서 xAPIC 모드에서는 ICR 접근에 spinlock이 필요합니다. x2APIC에서는 단일 64비트 WRMSR로 이 문제가 완전히 해결됩니다.

NMI IPI와 특수 경로

일반 Fixed Delivery IPI 외에도, NMI(Non-Maskable Interrupt) 모드의 IPI는 대상 CPU의 인터럽트 마스킹 상태와 무관하게 강제 전달됩니다. 커널은 hard lockup 감지, panic 전파, 원격 backtrace 수집 등 최후 수단으로 NMI IPI를 사용합니다.

NMI IPI 전달 경로와 사용 시나리오 발신 CPU ICR: Delivery Mode = NMI ICR 비트 [10:8] = 100 (NMI Delivery) 벡터 필드 무시됨 → NMI 전용 진입점 사용 대상 CPU cli/IRQF 상태 무시 exc_nmi() → nmi_handle() NMI 핸들러 체인 순회 NMI IPI 사용 시나리오 Hard Lockup Watchdog perf NMI 기반 감지 hrtimer 미응답 → NMI 발생 watchdog_overflow_callback() is_hardlockup() → panic/backtrace Panic NMI Broadcast panic() → crash_smp_send_stop() 일반 IPI 응답 없으면 NMI로 전환 apic_send_IPI_allbutself(NMI_VECTOR) 모든 CPU를 강제 정지시킨 후 kdump 원격 Backtrace 수집 SysRq-l, /proc/sysrq-trigger arch_trigger_cpumask_backtrace() NMI IPI → nmi_cpu_backtrace() 각 CPU 레지스터+스택 덤프 NMI IPI vs Fixed IPI 비교 특성 Fixed IPI (일반) NMI IPI 마스킹 가능 여부 cli/local_irq_disable로 차단 가능 차단 불가 (Non-Maskable) 벡터 지정 ICR Vector 필드 사용 (0x00-0xFF) Vector 필드 무시, NMI 진입점 사용 중첩(Nesting) 우선순위 기반 중첩 가능 NMI 처리 중 재진입 차단 (IRET까지) 사용 빈도 높음 (Reschedule, TLB flush 등) 낮음 (panic, watchdog, 디버깅) 오버헤드 낮음 (~0.5-2us 왕복) 높음 (NMI 진입/탈출 비용, IST 전환) 안전성 일반 커널 코드 안전 NMI-safe 함수만 호출 가능
/* NMI IPI 관련 커널 코드 */

/* 1. Panic 시 NMI broadcast — crash_smp_send_stop() */
static void crash_smp_send_stop(void)
{
    static int cpus_stopped;

    if (atomic_inc_return(&cpus_stopped) != 1)
        return;

    /* 1단계: 일반 REBOOT_VECTOR IPI 시도 */
    smp_send_stop();

    /* 2단계: 응답하지 않는 CPU에 NMI IPI 전송
     * 인터럽트를 비활성화한 채 멈춘 CPU를 강제로 깨움 */
    if (num_online_cpus() > 1) {
        apic_send_IPI_allbutself(NMI_VECTOR);
        mdelay(1000);  /* 1초 대기 후 강제 진행 */
    }
}

/* 2. NMI 핸들러 체인 — nmi_handle() */
static int nmi_handle(unsigned int type, struct pt_regs *regs)
{
    struct nmi_desc *desc = nmi_to_desc(type);
    struct nmiaction *a;
    int handled = 0;

    /* NMI 핸들러 체인 순회
     * perf, watchdog, backtrace, kgdb 등 등록된 핸들러 실행 */
    list_for_each_entry_rcu(a, &desc->head, list) {
        int res = a->handler(type, regs);
        handled |= res;
    }
    return handled;
}

/* 3. 원격 backtrace 수집 — SysRq-l */
void arch_trigger_cpumask_backtrace(const cpumask_t *mask,
                                     int exclude_cpu)
{
    /* 대상 CPU들에 NMI IPI 전송 */
    apic->send_IPI_mask(mask, NMI_VECTOR);
    /* 각 CPU의 NMI 핸들러에서 nmi_cpu_backtrace()가 호출되어
     * 레지스터 상태와 커널 스택 트레이스를 출력 */
}

/* NMI 핸들러에서 사용 가능한 함수 제약:
 * - printk(): 조건부 가능 (printk_safe 컨텍스트)
 * - kmalloc(): 금지 (sleep 가능 + slab 락 충돌)
 * - spin_lock(): 금지 (외부 락과 데드락 위험)
 * - raw_spin_lock(): 조건부 가능 (nmi_enter() 이후)
 * - 페이지 폴트: 금지 (이중 NMI 위험) */
NMI 재진입(Reentry) 위험: x86에서 NMI 핸들러 실행 중 다른 NMI가 도착하면, 기본적으로 현재 NMI가 IRET으로 리턴할 때까지 보류됩니다. 그러나 NMI 핸들러 내에서 예외(page fault, #GP 등)가 발생하면 예외 처리의 IRET이 NMI 차단을 풀어버려, 보류된 NMI가 예외 핸들러 복귀 전에 진입하는 NMI nesting 문제가 발생합니다. 이를 방지하기 위해 리눅스 커널은 NMI 전용 IST(Interrupt Stack Table) 스택을 사용하고, NMI 진입 시 IDT를 교체하는 등의 보호 메커니즘을 적용합니다.

RISC-V IPI: ACLINT/IMSIC SWI

RISC-V 아키텍처에서 IPI는 SBI(Supervisor Binary Interface) 호출 또는 ACLINT(Advanced Core Local Interruptor) MSWI 레지스터를 통해 구현됩니다. x86의 APIC나 ARM의 GIC에 해당하는 표준 인터럽트 컨트롤러로 PLIC/APLIC(외부 인터럽트)와 ACLINT/IMSIC(소프트웨어 인터럽트)를 사용합니다.

RISC-V IPI 아키텍처: SBI → ACLINT/IMSIC Linux Kernel (S-mode) smp_cross_call(cpumask) ipi_mux_send(cpu, IPI_*) SBI/MMIO 경로 1: SBI 기반 (M-mode 경유) sbi_send_ipi(mask) ecall → M-mode SBI 펌웨어 (OpenSBI) MSWI 레지스터 기록 장점: 구현 간단, 모든 RISC-V 플랫폼 지원 단점: ecall 오버헤드 (~1-5us) 경로 2: ACLINT SSWI (S-mode 직접) SSWI MMIO 기록 M-mode 우회 가능 대상 hart의 SSIP 비트 세트 장점: ecall 없이 직접 전송 (~100-500ns) 요구: ACLINT SSWI 확장 지원 필요 하드웨어 계층: ACLINT (Advanced Core Local Interruptor) MSWI (M-mode SWI) SSWI (S-mode SWI) MTIMER (M 타이머) STIMECMP IMSIC 3 아키텍처 IPI 비교 x86 ARM RISC-V APIC ICR (MMIO/MSR) GIC SGI (SysReg) ACLINT SSWI/SBI ecall 벡터 0xF0-0xFF 고정 SGI 0-15 동적 할당 SSIP 비트 1개 (다중화)
/* arch/riscv/kernel/smp.c — RISC-V IPI 구현 */

/* RISC-V IPI 유형 (비트 위치로 다중화) */
enum ipi_message_type {
    IPI_RESCHEDULE,       /* 비트 0: 스케줄러 리밸런싱 */
    IPI_CALL_FUNC,        /* 비트 1: smp_call_function */
    IPI_CPU_STOP,         /* 비트 2: CPU 정지 */
    IPI_CPU_CRASH_STOP,   /* 비트 3: crash 정지 */
    IPI_IRQ_WORK,         /* 비트 4: IRQ Work */
    IPI_TIMER,            /* 비트 5: Timer broadcast */
    IPI_MAX,
};

/* RISC-V는 하드웨어 IPI 인터럽트가 1개(SSIP)뿐이므로
 * per-CPU 비트마스크로 IPI 유형을 다중화 (ipi_mux) */
static DEFINE_PER_CPU(unsigned long, ipi_data);

/* IPI 전송 */
static void ipi_mux_send(unsigned int cpu,
                          unsigned int ipi)
{
    /* 대상 CPU의 ipi_data 비트마스크에 유형 비트 설정 */
    atomic_or(BIT(ipi), &per_cpu(ipi_data, cpu));
    /* 메모리 배리어 */
    smp_mb__after_atomic();

    /* 하드웨어 IPI 전송 (SBI 또는 ACLINT) */
    __ipi_send_mask(cpumask_of(cpu));
    /* SBI 경로: sbi_send_ipi(hartmask)
     * ACLINT 경로: writel(1, sswi_base + 4*hartid) */
}

/* IPI 수신 핸들러 */
static irqreturn_t ipi_mux_handler(int irq, void *data)
{
    unsigned long ipis;

    /* 원자적으로 비트마스크 읽고 클리어 */
    ipis = atomic_xchg(&this_cpu_read(ipi_data), 0);

    /* 각 비트에 대응하는 핸들러 호출 */
    if (ipis & BIT(IPI_RESCHEDULE))
        scheduler_ipi();
    if (ipis & BIT(IPI_CALL_FUNC))
        generic_smp_call_function_interrupt();
    if (ipis & BIT(IPI_CPU_STOP))
        ipi_stop();
    if (ipis & BIT(IPI_IRQ_WORK))
        irq_work_run();
    if (ipis & BIT(IPI_TIMER))
        tick_receive_broadcast();

    return IRQ_HANDLED;
}

/* SBI vs ACLINT 성능 비교:
 * SBI (ecall 경유):   ~2-5us per IPI (M-mode 전환 비용)
 * ACLINT SSWI (직접): ~0.2-1us per IPI (MMIO 기록만)
 * IMSIC MSI-IPI:      ~0.1-0.5us (MSI 기반, 가장 빠름) */
RISC-V IPI의 독특한 특성 — 소프트웨어 다중화(Multiplexing): x86은 벡터 번호(0xFD, 0xFC 등)로 IPI 유형을 하드웨어 레벨에서 구분하고, ARM도 SGI 0-15로 최대 16개를 구분합니다. 반면 RISC-V는 하드웨어 소프트웨어 인터럽트가 SSIP 비트 하나뿐이므로, 커널이 per-CPU 비트마스크(ipi_data)를 사용하여 IPI 유형을 소프트웨어로 다중화합니다. 이로 인해 하나의 인터럽트 진입에서 여러 IPI 유형을 한꺼번에 처리할 수 있어, 높은 IPI 부하에서 오히려 효율적일 수 있습니다. IMSIC(Incoming MSI Controller)가 도입되면 MSI 기반으로 더 세밀한 하드웨어 구분이 가능해집니다.

Timer Broadcast IPI

CPU가 깊은 C-state(C3 이상)에 진입하면 로컬 APIC 타이머가 정지하는 하드웨어가 있습니다. 이때 다른 CPU(보통 C0 상태의 CPU)가 타이머 만료를 대신 감지하여 IPI로 깨워주는 것이 Timer Broadcast 메커니즘입니다. NOHZ(tickless) 커널에서는 이 IPI가 유휴 CPU의 전력 절약과 반응 시간 사이의 균형을 맞추는 핵심 역할을 합니다.

Timer Broadcast IPI: 깊은 C-state에서의 타이머 복구 CPU 0 (C0 — 활성) HPET/ACPI PM Timer 활성 tick_broadcast_handler() 만료 시간 감시 CPU 1 (C3 — 깊은 절전) Local APIC Timer 정지 MWAIT C3 (전력 절약) next_event: +50ms CPU 2 (C6 — 최대 절전) Local APIC Timer 정지 MWAIT C6 (전력 최소) next_event: +200ms IPI Timer Broadcast 시퀀스 CPU 1/2: cpuidle 진입 broadcast 마스크에 등록 HPET 타이머 만료 CPU 0에서 IRQ 발생 broadcast_handler() 만료 CPU에 IPI 전송 CPU 1/2 깨어남 tick 재개 + 작업 처리 최신 대안: TSC-deadline 모드 IA32_TSC_DEADLINE MSR 기반 Local APIC Timer C-state에서도 타이머 유지 (하드웨어 보장) → Broadcast IPI 불필요 → IPI 오버헤드 제거 ARAT (Always Running APIC Timer) CPUID.06H:EAX[2] = 1 (ARAT 지원) C-state 진입 시에도 Local APIC Timer 계속 동작 Sandy Bridge 이후 대부분 지원
/* kernel/time/tick-broadcast.c — Timer Broadcast 핵심 */

/* Broadcast 등록: CPU가 깊은 C-state 진입 시 호출 */
void tick_broadcast_enter(void)
{
    struct clock_event_device *bc;
    int cpu = smp_processor_id();

    /* 현재 CPU를 broadcast 대상 마스크에 추가 */
    cpumask_set_cpu(cpu, tick_broadcast_mask);

    /* 이 CPU의 다음 타이머 이벤트 시간을
     * broadcast 디바이스에 등록 */
    bc = tick_broadcast_device.evtdev;
    if (bc->next_event > this_cpu_read(tick_cpu_device.next_event))
        tick_broadcast_set_event(bc,
            this_cpu_read(tick_cpu_device.next_event));
}

/* Broadcast 핸들러: 활성 CPU에서 타이머 만료 시 호출 */
static void tick_handle_oneshot_broadcast(struct clock_event_device *dev)
{
    struct cpumask *mask = tick_broadcast_mask;
    ktime_t now = ktime_get();
    int cpu;

    /* 만료 시간이 지난 CPU들을 찾아 IPI 전송 */
    for_each_cpu(cpu, mask) {
        if (tick_broadcast_expired(cpu, now)) {
            cpumask_set_cpu(cpu, tick_broadcast_pending);
        }
    }

    /* 대기 중인 CPU들에 CALL_FUNCTION IPI 전송 */
    tick_broadcast_ipi(tick_broadcast_pending);
    /* 각 CPU는 IPI로 깨어나 local tick을 재개 */
}

/* C-state에서 복귀 시 broadcast 해제 */
void tick_broadcast_exit(void)
{
    int cpu = smp_processor_id();
    cpumask_clear_cpu(cpu, tick_broadcast_mask);
    /* Local APIC Timer 재활성화 */
}

/* ARAT/TSC-deadline 감지: broadcast 불필요 판단 */
/* CPUID.06H:EAX[2] (ARAT) = 1 이면
 * Local APIC Timer가 C-state에서도 동작하므로
 * broadcast 메커니즘을 사용하지 않음 → IPI 절감 */
Timer Broadcast IPI 최적화:
  • ARAT 확인: grep arat /proc/cpuinfo로 Always Running APIC Timer 지원 확인. Sandy Bridge 이후 대부분 지원
  • TSC-deadline: grep tsc_deadline /proc/cpuinfo로 TSC-deadline 모드 확인. 지원 시 broadcast IPI 자동 비활성
  • nohz_full: nohz_full= 커널 파라미터로 지정된 CPU는 tick을 완전히 멈추므로 broadcast IPI 대상에서 제외
  • cpuidle 거버너: menu 거버너가 C-state 깊이를 결정하며, 너무 깊은 C-state는 broadcast IPI 지연을 유발
  • 진단: /proc/interrupts의 LOC(Local Timer) 대비 RES/CAL 비율로 broadcast 빈도 추정

CPU Stop IPI와 Panic/Reboot 경로

커널 panic, 시스템 reboot, kexec 전환 시 모든 CPU를 안전하게 정지시키는 것이 필수입니다. REBOOT_VECTOR(0xFA) IPI와 NMI IPI를 조합하여, 먼저 정상적인 정지를 시도하고, 응답하지 않는 CPU는 NMI로 강제 정지합니다.

CPU Stop 시퀀스: Panic/Reboot/Kexec 경로 1단계: 정상적인 CPU 정지 시도 panic() / reboot() smp_send_stop() REBOOT_VECTOR(0xFA) IPI 1초 대기 mdelay(1000) 수신 CPU: sysvec_reboot() → local_irq_disable() + hlt 무한 루프 2단계: 미응답 CPU 강제 정지 (NMI IPI) 아직 살아있는 CPU 확인 남은 CPU? Yes NMI IPI (Delivery=NMI) 강제 HLT No (모두 정지) NMI 핸들러: nmi_shootdown_cpus() → crash_save_cpu(regs) → halt 3단계: 최종 동작 (BSP만 실행) Reboot 경로 machine_restart() → ACPI reset / Triple fault → EFI ResetSystem() Panic 경로 kdump_nmi_shootdown_cpus() → crash_save_cpu() 각 CPU 레지스터 → machine_kexec() 크래시 커널 부팅 Kexec 경로 kernel_kexec() → machine_shutdown() AP 정지 → relocate_kernel() 새 커널로 점프
/* arch/x86/kernel/smp.c — CPU Stop IPI 구현 */

/* REBOOT_VECTOR 수신 핸들러 */
DEFINE_IDTENTRY_SYSVEC(sysvec_reboot)
{
    ack_APIC_irq();
    local_irq_disable();  /* 더 이상의 인터럽트 차단 */
    set_cpu_online(smp_processor_id(), false);
    stop_this_cpu(NULL);  /* HLT 무한 루프 진입 */
}

static void stop_this_cpu(void *dummy)
{
    local_irq_disable();
    disable_local_APIC();  /* Local APIC 비활성화 */

    /* 무한 HLT 루프 — 이 CPU는 더 이상 코드를 실행하지 않음 */
    for (;;) {
        native_halt();  /* HLT 명령어 — 인터럽트 대기 상태 */
        /* NMI가 올 수 있으므로 HLT 이후 다시 루프 */
    }
}

/* smp_send_stop() — 모든 CPU 정지 시퀀스 */
void smp_send_stop(void)
{
    unsigned long timeout;

    if (num_online_cpus() > 1) {
        /* 1단계: 일반 REBOOT_VECTOR IPI */
        apic_send_IPI_allbutself(REBOOT_VECTOR);

        /* 최대 1초 대기 — 모든 CPU가 정지할 때까지 */
        timeout = jiffies + HZ;
        while (num_online_cpus() > 1 &&
               time_before(jiffies, timeout))
            mdelay(1);
    }

    /* 2단계: 아직 살아있는 CPU가 있으면 NMI */
    if (num_online_cpus() > 1) {
        pr_emerg("Stopping %d CPUs via NMI\n",
                 num_online_cpus() - 1);
        apic_send_IPI_allbutself(NMI_VECTOR);
    }
}

/* kdump 전용: NMI로 모든 CPU 레지스터 저장 후 정지 */
void kdump_nmi_shootdown_cpus(void)
{
    /* NMI 핸들러에서 crash_save_cpu(regs, cpu) 호출
     * → /proc/vmcore에서 각 CPU의 마지막 상태 확인 가능 */
    nmi_shootdown_cpus(kdump_nmi_callback);
}
Panic-on-CPU와 IPI 타이밍: panic()이 NMI 컨텍스트에서 발생하면, 다른 CPU에 보내는 일반 REBOOT_VECTOR IPI가 전달되지 않을 수 있습니다(대상 CPU도 NMI 처리 중이거나 인터럽트 비활성 상태). 이 경우 NMI IPI만이 유일한 수단이며, kernel.panic_on_nmi_watchdog=1crash_kexec_post_notifiers=1 설정으로 crash dump 수집 신뢰성을 높일 수 있습니다. 대규모 시스템에서는 모든 CPU가 NMI에 응답하는 데 수 초가 걸릴 수 있으므로, panic_timeout 값을 충분히 설정하세요.

PREEMPT_RT에서의 IPI 변환

PREEMPT_RT 커널에서는 결정론적(deterministic) 응답 시간을 보장하기 위해 IPI 관련 처리 방식이 변경됩니다. 일반적인 IPI 핸들러는 하드 인터럽트 컨텍스트에서 실행되지만, PREEMPT_RT는 가능한 많은 작업을 스레드화(threaded)하여 우선순위 역전(Priority Inversion)을 방지합니다.

일반 커널 vs PREEMPT_RT: IPI 처리 경로 비교 일반 커널 (PREEMPT_DYNAMIC) 하드 IRQ 컨텍스트 모든 IPI 핸들러가 여기서 실행 sysvec_call_function_single() flush_smp_call_function_queue() 콜백 func(info) 실행 (hard IRQ에서) 문제점: 1. 콜백이 길면 해당 CPU의 모든 작업 지연 2. 우선순위 역전: 저우선순위 태스크의 IPI가 고우선순위 RT 태스크를 차단 3. spinlock 보유 중 IPI → latency spike 4. local_irq_disable() 구간에서 IPI 보류 → 최악 지연(WCET) 예측 불가 PREEMPT_RT 커널 하드 IRQ: 최소 처리만 ACK + CSD 큐 확인 + wake up 스레드 컨텍스트 (preemptible) IRQ 스레드에서 콜백 실행 콜백 func(info) 실행 (스레드에서) 개선점: 1. RT 태스크가 IPI 콜백보다 높은 우선순위 가능 2. spinlock → rt_mutex 변환으로 우선순위 상속(PI) 적용 3. local_irq_disable() → preempt_disable() (IRQ 자체는 차단 안 함) → 최악 지연 ~50-100us 보장 가능
/* PREEMPT_RT에서의 IPI 관련 변환 요약 */

/* 1. spinlock_t → rt_mutex 변환
 *    IPI 콜백에서 spinlock을 사용하는 경우,
 *    PREEMPT_RT에서는 rt_mutex로 변환되어
 *    우선순위 상속(Priority Inheritance)이 적용됨 */

/* 일반 커널 */
spin_lock(&my_lock);    /* 실제 spinlock: busy-wait */
/* 작업 */
spin_unlock(&my_lock);

/* PREEMPT_RT 커널 — 동일 코드가 다르게 컴파일됨 */
spin_lock(&my_lock);    /* → rt_mutex_lock(): sleep 가능! */
/* 작업 (preemptible) */
spin_unlock(&my_lock);  /* → rt_mutex_unlock() */

/* 2. 진짜 spinlock이 필요한 경우 raw_spinlock_t 사용 */
raw_spin_lock(&hw_lock);  /* PREEMPT_RT에서도 실제 spinlock */
/* 하드웨어 레지스터 접근 등 */
raw_spin_unlock(&hw_lock);

/* 3. IPI 핸들러 중 PREEMPT_RT에서도 hard IRQ에서 실행되는 것들:
 *    - Reschedule IPI: 항상 hard IRQ (최소 비용)
 *    - NMI IPI: 당연히 hard IRQ
 *    - Timer broadcast: hard IRQ
 *
 *    스레드화되는 것들:
 *    - smp_call_function 콜백 (일부)
 *    - IRQ Work (LAZY 모드) */

/* 4. local_irq_disable() 변환 */
local_irq_disable();   /* 일반: 실제 CLI
                        * RT: preempt_disable() + migrate_disable()
                        * → IPI 전달은 차단하지 않음! */

local_irq_enable();    /* 일반: 실제 STI
                        * RT: preempt_enable() */

/* RT에서 실제로 IRQ를 차단하려면: */
raw_local_irq_disable();  /* 실제 CLI — 매우 짧은 구간에서만! */
PREEMPT_RT에서의 IPI 지연 측정:
  • cyclictest -p99 -m -t4로 스케줄링 지연을 측정하면 간접적으로 IPI 응답 시간을 포함합니다
  • PREEMPT_RT에서 목표 WCET(Worst-Case Execution Time)은 일반적으로 50-100us이며, IPI 처리 시간은 이 범위 내에 포함됩니다
  • trace-cmd record -e irq -e sched -e ipi로 IPI와 스케줄링 이벤트의 상관관계를 분석하세요
  • RT 워크로드에서는 isolcpus=irqaffinity=로 RT CPU에 대한 불필요한 IPI를 최소화하세요

IPI와 C-state/전력 비용

IPI는 CPU 간 통신뿐만 아니라 전력 소비에도 영향을 미칩니다. 깊은 C-state(C3/C6)에서 잠든 CPU를 IPI로 깨우면, C-state 탈출 지연(exit latency)이 추가되고, 깊은 절전에서 복귀하는 과정에서 상당한 에너지가 소모됩니다. 대규모 데이터센터에서는 불필요한 IPI에 의한 C-state 깨우기(Wakeup)가 전체 서버 전력의 수 퍼센트를 차지할 수 있습니다.

C-state별 IPI Wakeup 비용과 전력 영향 C-state 깊이 → IPI 수신 지연 및 에너지 비용 증가 C0 (Active) IPI 즉시 수신 지연: 0ns C1 (Halt) 클럭 게이팅 탈출: ~1us C1E (Enhanced) 전압 강하 탈출: ~10us C3 (Sleep) L1/L2 캐시 플러시 탈출: ~50-100us C6 (Deep Sleep) 코어 전원 차단 탈출: ~100-200us C-state별 IPI 응답 비용 상세 C-state 탈출 지연 IPI 왕복 에너지 비용 캐시 상태 APIC Timer C0 0ns ~0.5-1us 없음 (이미 활성) 핫 캐시 동작 중 C1 ~1us ~1-2us ~0.1uJ 핫 캐시 동작 중 C1E ~10us ~10-15us ~1uJ (전압 복구) 핫 캐시 동작 중 C3 ~50-100us ~50-105us ~5-10uJ (캐시 복구) L1/L2 콜드 정지 가능 C6 ~100-200us ~100-205us ~10-50uJ (전원 복구) 전체 콜드 정지 ※ 수치는 Intel Xeon Scalable (Ice Lake) 기준 대략값. 실제 하드웨어에 따라 다름
# C-state와 IPI의 상호작용 진단

# 1. C-state 사용 현황 확인
cat /sys/devices/system/cpu/cpu*/cpuidle/state*/name
cat /sys/devices/system/cpu/cpu*/cpuidle/state*/usage
cat /sys/devices/system/cpu/cpu*/cpuidle/state*/time
# state0: POLL  state1: C1  state2: C1E  state3: C6

# 2. C-state 잔류 시간 vs IPI 빈도 상관관계
turbostat --show Core,CPU,Busy%,Bzy_MHz,IRQ,C1%,C6% -- sleep 10
# Core CPU  Busy% Bzy_MHz IRQ   C1%   C6%
#  0    0   15.2  3200   4523  12.3  72.5  ← C6 깊은 절전 많음
#  1    4   89.1  3800  12345   8.7   2.2  ← 거의 활성 상태

# 3. 불필요한 IPI wakeup 식별
# C6 비율이 높은 CPU의 RES/CAL 카운터가 높으면
# → idle CPU를 깨우는 불필요한 IPI 존재
perf stat -e 'power:cpu_idle' \
          -e 'irq_vectors:reschedule_entry' \
          --per-cpu -a -- sleep 10

# 4. C-state 깊이 제한으로 IPI 지연 축소 (트레이드오프)
# C6 비활성화 (탈출 지연 감소, 전력 소비 증가)
echo 1 > /sys/devices/system/cpu/cpu*/cpuidle/state3/disable

# 또는 커널 파라미터로 C-state 제한
# intel_idle.max_cstate=2  (C1E까지만 허용)
# processor.max_cstate=2   (ACPI C-state 제한)

# 5. idle=poll로 C-state 완전 비활성 (최저 지연, 최고 전력)
# 이 경우 IPI 수신 지연은 최소지만 전력 소비가 크게 증가
전력 vs 지연 트레이드오프: 데이터센터에서 서버당 연간 전력 비용이 수백 달러에 달하는 환경에서, C6 비활성화는 서버당 10-30W 추가 전력(연간 $20-50)을 의미합니다. 반면 지연 민감 워크로드(금융 거래, 실시간 시스템)에서는 C6 탈출의 100-200us 지연이 치명적입니다. isolcpus=로 RT CPU만 C-state를 제한하고, 나머지 CPU는 깊은 절전을 허용하는 하이브리드 전략이 일반적입니다.

IPI 관련 커널 설정 옵션 총정리

IPI의 동작 방식, 디버깅, 성능 최적화에 영향을 미치는 주요 커널 설정 옵션(Kconfig)과 부팅 파라미터를 종합 정리합니다.

카테고리설정 옵션설명기본값
APIC 모드 CONFIG_X86_X2APIC x2APIC 지원 활성화 (MSR 기반 IPI) Y
x2apic_phys (부팅 파라미터) x2APIC Physical 모드 강제 (기본: Cluster) 비활성
nox2apic (부팅 파라미터) x2APIC 비활성화, xAPIC 모드 사용 비활성
TLB 최적화 CONFIG_X86_PCID PCID 지원 (TLB flush IPI 감소) Y
CONFIG_X86_INVLPGB AMD INVLPGB 지원 (IPI 없는 TLB 무효화) Y (AMD)
nopcid (부팅 파라미터) PCID 비활성화 (디버깅 용도) 비활성
디버깅 CONFIG_CSD_LOCK_WAIT_DEBUG smp_call_function CSD 락 타임아웃 감지 N
kernel.csd_lock_timeout (sysctl) CSD 락 타임아웃 임계값 (초) 5
CONFIG_HARDLOCKUP_DETECTOR NMI 기반 hard lockup 감지 Y
SMP/NUMA CONFIG_SMP SMP 지원 (IPI 메커니즘 활성화) Y
CONFIG_NR_CPUS 최대 CPU 수 (IPI 마스크 크기 결정) 8192
CONFIG_NUMA NUMA 인식 (cross-node IPI 비용 최적화) Y
Tickless/전력 CONFIG_NO_HZ_FULL 완전 tickless (지정 CPU의 timer IPI 제거) Y
nohz_full= (부팅 파라미터) tickless 대상 CPU 지정 비활성
isolcpus= (부팅 파라미터) 스케줄러에서 격리 (Reschedule IPI 감소) 비활성
RT CONFIG_PREEMPT_RT PREEMPT_RT (IPI 콜백 스레드화) N
CONFIG_IRQ_FORCED_THREADING 모든 IRQ 스레드화 (IPI 핸들러 포함) N
가상화 CONFIG_KVM_INTEL KVM Intel: APICv/Posted Interrupt 지원 M
CONFIG_KVM_AMD KVM AMD: AVIC 지원 M
# 현재 시스템의 IPI 관련 설정 확인

# 1. APIC 모드 확인
dmesg | grep -i "apic\|x2apic"
# [    0.008000] x2apic: enabled by BIOS
# [    0.008000] Switched to x2apic ops

# 2. PCID 지원 확인
grep pcid /proc/cpuinfo | head -1
# flags : ... pcid ...

# 3. CSD Lock 디버깅 설정
zgrep CSD_LOCK /proc/config.gz
sysctl kernel.csd_lock_timeout

# 4. NUMA 토폴로지 확인
numactl --hardware
lscpu | grep -E "Socket|Core|Thread|NUMA"

# 5. Tickless/isolation 설정 확인
cat /proc/cmdline | tr ' ' '\n' | grep -E 'nohz|isolcpus|rcu_nocbs'

# 6. cpuidle 상태 확인
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/name
cat /sys/devices/system/cpu/cpu0/cpuidle/state*/latency

# 7. ARAT/TSC-deadline 지원 확인
grep -E "arat|tsc_deadline" /proc/cpuinfo | head -1

# 8. 가상화 환경에서 APICv 상태
# KVM 호스트:
cat /sys/module/kvm_intel/parameters/enable_apicv
# AMD:
cat /sys/module/kvm_amd/parameters/avic
운영 환경별 권장 설정:
  • 범용 서버: x2APIC + PCID 기본 활성, CSD_LOCK_WAIT_DEBUG=y 권장
  • 지연 민감 (금융/게임): isolcpus= + nohz_full= + intel_idle.max_cstate=1
  • 대규모 NUMA (256+ 코어): numabalancing=disable, 워크로드별 cpuset 분리
  • 가상화 호스트: x2APIC 필수, APICv/AVIC 활성, vCPU-pCPU 1:1 핀닝
  • PREEMPT_RT: isolcpus=managed_irq, irqaffinity=, C-state 제한

참고자료

커널 공식 문서

LWN.net 기사

커널 소스 (Bootlin Elixir)

하드웨어 아키텍처 매뉴얼

기술 블로그 및 발표

추적 및 디버깅 도구

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

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