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 간 협조 메커니즘의 전 영역을 소스 코드 수준에서 상세히 분석합니다.
핵심 요약
- 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)이 됩니다.
단계별 이해
- APIC 하드웨어 이해
Local APIC와 ICR 레지스터(Register) 구조를 파악합니다. xAPIC(MMIO)와 x2APIC(MSR) 차이를 이해합니다. - IPI 벡터 맵 확인
irq_vectors.h에 정의된 시스템 예약 벡터(0xF0~0xFF)와 각 벡터의 목적을 학습합니다. - 주요 IPI 흐름 추적
Reschedule IPI, Call Function IPI, TLB Flush IPI의 송신-수신 경로를 코드로 따라갑니다. - 성능 모니터링
/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)이 마이크로초 이하로 매우 짧습니다.
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를 발생시킵니다:
/* 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는 디바이스 인터럽트와 충돌하지 않음 */
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가 트리거되는 주요 경로는 다음과 같습니다:
| 트리거 경로 | 함수 | 설명 |
|---|---|---|
| 태스크 깨우기 | 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 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를 줄이기 위해 여러 최적화를 적용합니다. 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 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 루프 진입 */
}
IPI 성능 특성과 최적화
IPI는 CPU 간 통신의 기본이지만 상당한 오버헤드(Overhead)를 수반합니다. IPI 하나의 왕복 비용은 수백 나노초에서 수 마이크로초에 이르며, 대규모 SMP 시스템에서는 IPI storm이 심각한 성능 병목이 될 수 있습니다:
| 항목 | xAPIC | x2APIC | 비고 |
|---|---|---|---|
| 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 메커니즘 분석의 기초입니다.
/* 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);
TLB Flush 최적화
TLB flush IPI는 SMP 시스템에서 가장 빈번하게 발생하는 IPI 유형 중 하나이며, 성능에 미치는 영향도 큽니다. 커널은 PCID, lazy TLB, INVLPGB 등 다양한 최적화 기법을 활용하여 TLB flush 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");
}
- PCID — 프로세스별 TLB 태깅으로 컨텍스트 스위칭 시 불필요한 flush 제거 (Intel Haswell+)
- Lazy TLB — 커널 스레드 실행 중인 CPU를 flush 대상에서 제외
- Batch flush —
tlb_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 메커니즘이 복잡하게 얽혀 있습니다.
/* 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_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 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 시스템 레지스터가 담당합니다.
/* 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;
}
}
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
# 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 보장 |
copy_from_user 등)이나 페이지 폴트(Page Fault)를 유발할 수 있는 작업은 반드시 피해야 합니다. 필요한 데이터는 IPI 전송 전에 커널 메모리에 복사해 두고, 콜백에서는 해당 커널 메모리만 참조하세요.
IPI와 메모리 순서 보장(Ordering)
IPI는 단순한 "알림"이 아니라 CPU 간 메모리 가시성(memory visibility)을 맞추는 동기화 지점으로도 사용됩니다. 핵심은 데이터 기록이 먼저, IPI 전송이 나중, 그리고 수신 CPU는 IPI 수신 후 데이터 관측 순서를 강제하는 것입니다. 이를 위해 커널은 smp_wmb(), smp_rmb(), smp_mb()와 CSD 플래그의 원자적 상태 전이를 결합합니다.
/* 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);
}
READ_ONCE/WRITE_ONCE + smp_mb 계열 조합을 명시적으로 사용하세요.
가상화(Virtualization) 환경의 IPI (KVM/Hypervisor)
가상화에서는 IPI 경로가 추가 계층을 통과합니다. 게스트 커널이 APIC ICR을 기록해도 실제 물리 CPU 인터럽트로 바로 가지 않고, VM-exit 혹은 APICv/posted-interrupt 경로를 거쳐 하이퍼바이저(Hypervisor)가 중개합니다. 이때 불필요한 VM-exit이 많으면 IPI 지연이 급증합니다.
/* 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 없이 하드웨어 전달 */
대규모 코어에서 IPI 확장 전략
64코어 이상 서버에서는 "기능이 맞다"보다 "브로드캐스트를 줄였는가"가 중요합니다. 같은 코드라도 대상 CPU 마스크 설계에 따라 IPI량이 수십 배 차이 납니다. 실무에서는 대상 축소, 배치 처리, 비동기화 3가지를 기본 축으로 최적화합니다.
/* 대상 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 튜닝은 추측이 아니라 계측으로 진행해야 합니다. 아래 절차는 "수정 전 기준선"과 "수정 후 개선치"를 재현 가능하게 비교하기 위한 실험 템플릿입니다.
# 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
CPU Hotplug/Affinity와 IPI 경로 안정성
운영 중 CPU online/offline, cpuset 변경, IRQ affinity 재배치는 IPI 대상 마스크를 바꿉니다.
특히 대규모 코어에서 hotplug 이벤트가 잦으면 on_each_cpu_mask() 계열 호출의 비용 편차가 커지고,
잠깐의 마스크 불안정 구간에서 work가 재시도되는 현상이 나타날 수 있습니다.
| 현상 | 가능 원인 | 점검 포인트 |
|---|---|---|
| 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 튜닝이 잘 되어도,
전역 동기화 구간이 겹치면 지연 꼬리가 갑자기 늘어날 수 있습니다.
# 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 수, 지연 시간에 근본적인 변화를 가져왔습니다.
/* 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³² (이론상) |
NMI IPI와 특수 경로
일반 Fixed Delivery IPI 외에도, NMI(Non-Maskable Interrupt) 모드의 IPI는 대상 CPU의 인터럽트 마스킹 상태와 무관하게 강제 전달됩니다. 커널은 hard lockup 감지, panic 전파, 원격 backtrace 수집 등 최후 수단으로 NMI IPI를 사용합니다.
/* 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 위험) */
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(소프트웨어 인터럽트)를 사용합니다.
/* 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 기반, 가장 빠름) */
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의 전력 절약과 반응 시간 사이의 균형을 맞추는 핵심 역할을 합니다.
/* 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 절감 */
- 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로 강제 정지합니다.
/* 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);
}
kernel.panic_on_nmi_watchdog=1과 crash_kexec_post_notifiers=1 설정으로 crash dump 수집 신뢰성을 높일 수 있습니다. 대규모 시스템에서는 모든 CPU가 NMI에 응답하는 데 수 초가 걸릴 수 있으므로, panic_timeout 값을 충분히 설정하세요.
PREEMPT_RT에서의 IPI 변환
PREEMPT_RT 커널에서는 결정론적(deterministic) 응답 시간을 보장하기 위해 IPI 관련 처리 방식이 변경됩니다. 일반적인 IPI 핸들러는 하드 인터럽트 컨텍스트에서 실행되지만, PREEMPT_RT는 가능한 많은 작업을 스레드화(threaded)하여 우선순위 역전(Priority Inversion)을 방지합니다.
/* 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 — 매우 짧은 구간에서만! */
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의 상호작용 진단
# 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 수신 지연은 최소지만 전력 소비가 크게 증가
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 제한
참고자료
커널 공식 문서
- SMP Support — The Linux Kernel documentation — smp_call_function 계열 API 공식 문서입니다
- IRQ — The Linux Kernel documentation — 인터럽트 서브시스템 전반의 공식 문서입니다
- x86 Topology — The Linux Kernel documentation — x86 CPU 토폴로지와 APIC ID 매핑을 설명합니다
- Kernel Parameters —
isolcpus,nohz_full,rcu_nocbs등 IPI 관련 부트 파라미터를 참고할 수 있습니다 - RCU — The Linux Kernel documentation — RCU grace period과 IPI의 관계를 설명합니다
- TLB — The Linux Kernel documentation — TLB flush IPI의 배경이 되는 TLB 관리 정책을 다룹니다
LWN.net 기사
- Improving kernel timers — smp_call_function과 cross-CPU 호출의 개선 논의입니다
- The full tickless CPU — nohz_full에서 tick IPI를 제거하는 설계를 설명합니다
- Reworking TLB shootdowns — TLB flush IPI의 성능 문제와 배치(batch) 최적화를 다룹니다
- Lazy TLB shootdowns — Lazy TLB invalidation으로 불필요한 IPI를 줄이는 기법을 설명합니다
- Reschedule IPI and idle CPUs — idle CPU에 대한 reschedule IPI 최적화를 다룹니다
- Multi-function IPI — 여러 IPI를 하나로 합쳐 오버헤드를 줄이는 접근을 설명합니다
- Per-CPU interrupts and IPIs — per-CPU 인터럽트 관점에서 IPI 처리를 분석합니다
- smp_call_function and CSD lock debugging — CSD lock 타임아웃 디버깅 패치를 소개합니다
커널 소스 (Bootlin Elixir)
- kernel/smp.c —
smp_call_function_single(),smp_call_function_many()등 IPI 기반 cross-CPU 호출의 핵심 구현입니다 - kernel/irq_work.c —
irq_work큐잉과 self-IPI 메커니즘을 구현합니다 - arch/x86/kernel/smp.c — x86 reschedule, call function, reboot IPI 핸들러를 포함합니다
- arch/x86/kernel/apic/apic.c — Local APIC 초기화, ICR 쓰기, IPI 전송의 하드웨어 인터페이스입니다
- arch/x86/kernel/apic/ipi.c — x86 IPI 전송 경로(
apic->send_IPI_*)를 구현합니다 - arch/x86/include/asm/apic.h — APIC 레지스터 오프셋, ICR 필드 매크로를 정의합니다
- arch/x86/mm/tlb.c — TLB flush IPI의 전송과 수신 핸들러를 구현합니다
- arch/arm64/kernel/smp.c — ARM64 IPI 핸들러(
IPI_RESCHEDULE,IPI_CALL_FUNC등)를 구현합니다 - drivers/irqchip/irq-gic-v3.c — ARM GICv3 SGI(Software Generated Interrupt) 기반 IPI 전송을 구현합니다
- include/linux/smp.h —
smp_call_function계열 API 선언과call_single_data구조체를 정의합니다
하드웨어 아키텍처 매뉴얼
- Intel 64 and IA-32 Architectures Software Developer's Manual — Volume 3A, Chapter 11 (Advanced Programmable Interrupt Controller)에서 ICR, LVT, IPI 전송 메커니즘을 상세히 설명합니다
- ARM Generic Interrupt Controller Architecture Specification (GICv3/v4) — SGI(Software Generated Interrupt)를 통한 IPI 전송과 Affinity Routing을 설명합니다
- ARM Architecture Reference Manual (Armv8-A) — System Register를 통한 ICC_SGI 접근과 PE 간 인터럽트 전달을 다룹니다
- RISC-V ACLINT Specification — RISC-V의 MSWI(Machine Software Interrupt) 기반 IPI 메커니즘을 정의합니다
기술 블로그 및 발표
- Linux Kernel Conference: Reducing IPI Overhead — Ottawa Linux Symposium에서 발표된 IPI 오버헤드 감소 기법입니다
- TLB Shootdowns: Challenges and Opportunities — Linux Plumbers Conference 2017에서 TLB shootdown IPI의 현황과 최적화 방향을 논의합니다
- Brendan Gregg — CPU Utilization is Wrong — IPI stall이 CPU 사용률 측정에 미치는 영향을 포함하여 CPU 메트릭의 한계를 설명합니다
- Andi Kleen — Scaling Performance Monitoring — NMI IPI 기반 PMU 샘플링과 대규모 시스템에서의 성능 모니터링 확장을 다룹니다
- USENIX OSDI '18: Optimizing the TLB Shootdown Algorithm with Page Access Tracking — TLB shootdown IPI 빈도를 줄이기 위한 학술 연구입니다
추적 및 디버깅 도구
- ftrace — The Linux Kernel documentation —
irq:ipi_entry,irq:ipi_exittracepoint로 IPI 이벤트를 추적할 수 있습니다 - perf wiki —
perf stat -e ipi:*로 IPI 발생 빈도와 지연을 측정하는 방법을 안내합니다 - BPF Performance Tools (Brendan Gregg) — eBPF 기반 IPI 지연 히스토그램 측정 기법을 포함합니다
관련 문서
IPI와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.