IPI (Inter-Processor Interrupt) 심화
IPI(Inter-Processor Interrupt)는 SMP 시스템에서 한 CPU가 다른 CPU에게 보내는 특수한 인터럽트입니다. x86 APIC/ICR 아키텍처를 기반으로 스케줄러 리밸런싱, TLB 캐시 일관성, 원격 함수 호출(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에서 콜백 함수를 실행하는 범용 API입니다.
- TLB Flush IPI — 페이지 테이블 변경 시 관련 CPU들의 TLB를 무효화합니다.
- 성능 영향 — IPI 왕복 비용은 0.5~5us이며, 대규모 SMP에서 IPI storm은 심각한 병목이 됩니다.
단계별 이해
- APIC 하드웨어 이해
Local APIC와 ICR 레지스터 구조를 파악합니다. 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 캐시 일관성, 원격 함수 호출, 타이머 동기화 등 CPU 간 협조가 필요한 거의 모든 작업에 IPI가 관여합니다.
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 지연 시간이 크게 줄어듭니다. 최신 시스템에서는 x2APIC가 기본입니다.
리눅스 IPI 벡터 유형
x86 리눅스 커널은 여러 종류의 IPI 벡터를 정의하며, 각각 고유한 목적을 가집니다. 이 벡터들은 arch/x86/include/asm/irq_vectors.h에 정의되어 있습니다:
| IPI 벡터 | 벡터 번호 | 용도 | 핸들러 |
|---|---|---|---|
| RESCHEDULE_VECTOR | 0xFD | 대상 CPU의 스케줄러를 깨워 태스크 재배치 유도 | sysvec_reschedule_ipi() |
| CALL_FUNCTION_VECTOR | 0xFC | 원격 CPU에서 콜백 함수 실행 요청 | sysvec_call_function() |
| CALL_FUNCTION_SINGLE_VECTOR | 0xFB | 특정 단일 CPU에서 콜백 함수 실행 | sysvec_call_function_single() |
| REBOOT_VECTOR | 0xFA | 리부트 시 모든 CPU 정지 요청 | sysvec_reboot() |
| IRQ_WORK_VECTOR | 0xF6 | NMI-safe한 deferred 작업 실행 (printk 등) | sysvec_irq_work() |
| X86_PLATFORM_IPI_VECTOR | 0xF7 | 플랫폼 특화 IPI (UV, Xen 등) | sysvec_x86_platform_ipi() |
/* arch/x86/include/asm/irq_vectors.h */
#define SPURIOUS_APIC_VECTOR 0xFF
#define ERROR_APIC_VECTOR 0xFE
#define RESCHEDULE_VECTOR 0xFD
#define CALL_FUNCTION_VECTOR 0xFC
#define CALL_FUNCTION_SINGLE_VECTOR 0xFB
#define REBOOT_VECTOR 0xFA
#define IRQ_WORK_VECTOR 0xF6
#define X86_PLATFORM_IPI_VECTOR 0xF7
/* IPI 벡터 범위: 0xF0 ~ 0xFF (시스템 예약)
* 일반 디바이스 인터럽트 벡터: 0x20 ~ 0xEF
* 이 분리로 IPI는 디바이스 인터럽트와 충돌하지 않음 */
Reschedule IPI
Reschedule IPI는 커널에서 가장 빈번하게 발생하는 IPI입니다. 한 CPU에서 태스크의 우선순위가 변경되었거나, 로드 밸런서가 태스크를 다른 CPU로 이동시키려 할 때, 대상 CPU의 스케줄러에게 재스케줄링을 요청합니다:
/* kernel/sched/core.c — 리스케줄 IPI 발생 과정 */
/* resched_curr(): 현재 CPU 또는 원격 CPU에 재스케줄 요청 */
void resched_curr(struct rq *rq)
{
struct task_struct *curr = rq->curr;
int cpu;
if (test_tsk_need_resched(curr))
return; /* 이미 재스케줄 표시됨 */
cpu = cpu_of(rq);
if (cpu == smp_processor_id()) {
set_tsk_need_resched(curr);
set_preempt_need_resched();
return; /* 로컬 CPU면 TIF_NEED_RESCHED 플래그만 설정 */
}
/* 원격 CPU인 경우 IPI로 알림 */
set_tsk_need_resched(curr);
smp_send_reschedule(cpu); /* → RESCHEDULE_VECTOR IPI 전송 */
}
/* arch/x86/kernel/smp.c — Reschedule IPI 수신 핸들러 */
DEFINE_IDTENTRY_SYSVEC(sysvec_reschedule_ipi)
{
ack_APIC_irq();
__inc_irq_stat(irq_resched_count);
trace_reschedule_entry(RESCHEDULE_VECTOR);
scheduler_ipi(); /* 실제 동작: 별도 작업 없음. TIF_NEED_RESCHED가 */
/* 이미 설정되어 있어 인터럽트 복귀 시 schedule() 호출됨 */
trace_reschedule_exit(RESCHEDULE_VECTOR);
}
/* scheduler_ipi()는 대부분 빈 함수임:
* 실제 재스케줄은 인터럽트 리턴 경로에서
* TIF_NEED_RESCHED 플래그를 확인하여 수행됨 */
Reschedule IPI의 최적화: Reschedule IPI는 "빈 메시지"에 가깝습니다. 중요한 것은 대상 CPU에서 인터럽트가 발생했다는 사실 자체이며, 인터럽트 리턴 경로에서 TIF_NEED_RESCHED 플래그를 확인하여 schedule()을 호출합니다. 따라서 IPI 핸들러 자체는 거의 아무 작업도 하지 않습니다.
smp_call_function API 상세
커널에서 다른 CPU에 함수 실행을 요청하는 가장 일반적인 메커니즘입니다. 내부적으로 CALL_FUNCTION_VECTOR 또는 CALL_FUNCTION_SINGLE_VECTOR IPI를 사용합니다:
/* kernel/smp.c — smp_call_function 핵심 구조 */
/* CSD (Call Single Data): 원격 함수 호출의 기본 단위 */
struct __call_single_data {
struct __call_single_node node;
smp_call_func_t func; /* 실행할 함수 포인터 */
void *info; /* 함수에 전달할 인자 */
};
/* 특정 CPU 하나에서 함수 실행 */
int smp_call_function_single(int cpu,
smp_call_func_t func,
void *info,
int wait);
/* 현재 CPU를 제외한 모든 온라인 CPU에서 함수 실행 */
void smp_call_function(smp_call_func_t func,
void *info,
int wait);
/* 현재 CPU 포함 모든 CPU에서 함수 실행 */
void on_each_cpu(smp_call_func_t func,
void *info,
int wait);
/* 특정 CPU 마스크에 속한 CPU들에서 함수 실행 */
void on_each_cpu_mask(const struct cpumask *mask,
smp_call_func_t func,
void *info,
int wait);
/* 조건부: 각 CPU에서 condition 함수가 true인 경우만 실행 */
void on_each_cpu_cond(smp_cond_func_t cond_func,
smp_call_func_t func,
void *info,
int wait);
/* smp_call_function_single() 내부 동작 흐름 */
int smp_call_function_single(int cpu, smp_call_func_t func,
void *info, int wait)
{
struct __call_single_data csd;
int this_cpu;
this_cpu = get_cpu(); /* preemption 비활성화 */
if (cpu == this_cpu) {
/* 대상이 현재 CPU면 직접 실행 */
local_irq_disable();
func(info);
local_irq_enable();
} else {
/* CSD를 대상 CPU의 call_single_queue에 삽입 */
csd.func = func;
csd.info = info;
__smp_call_single_queue(cpu, &csd.node);
/* → CALL_FUNCTION_SINGLE_VECTOR IPI 전송 */
if (wait)
csd_lock_wait(&csd); /* 완료될 때까지 spin-wait */
}
put_cpu();
return 0;
}
/* 수신 측: call_single_queue에서 CSD를 꺼내 실행 */
DEFINE_IDTENTRY_SYSVEC(sysvec_call_function_single)
{
ack_APIC_irq();
__inc_irq_stat(irq_call_count);
generic_smp_call_function_single_interrupt();
/* → per-CPU call_single_queue의 모든 CSD를 순회하며 func(info) 호출 */
}
주의: smp_call_function*()에 전달하는 콜백 함수는 인터럽트 컨텍스트에서 실행됩니다. 따라서 sleep 가능한 함수(kmalloc(GFP_KERNEL), mutex_lock() 등)를 호출하면 안 됩니다. wait=1로 호출하면 원격 CPU의 실행 완료까지 현재 CPU가 spin-wait하므로, 데드락에 주의해야 합니다.
TLB Flush IPI
페이지 테이블이 변경되면 해당 매핑을 사용하는 모든 CPU의 TLB(Translation Lookaside Buffer)를 무효화해야 합니다. 이 과정에서 IPI가 핵심적인 역할을 합니다:
/* arch/x86/mm/tlb.c — TLB flush IPI 흐름 */
/* 1. 페이지 테이블 변경 시 TLB flush 요청 */
void flush_tlb_mm_range(struct mm_struct *mm,
unsigned long start, unsigned long end,
unsigned int stride_shift, bool freed_tables)
{
struct flush_tlb_info *info;
/* 현재 CPU의 TLB 먼저 flush */
info = get_flush_tlb_info(mm, start, end, stride_shift,
freed_tables, 0);
if (mm == this_cpu_read(cpu_tlbstate.loaded_mm)) {
flush_tlb_func(info); /* 로컬 TLB flush */
}
/* mm을 사용 중인 다른 CPU들에게 IPI 전송 */
if (cpumask_any_but(mm_cpumask(mm),
smp_processor_id()) < nr_cpu_ids) {
flush_tlb_multi(mm_cpumask(mm), info);
/* → smp_call_function_many_cond()로 IPI 전송 */
}
put_flush_tlb_info();
}
/* 2. 수신 측: IPI를 받은 CPU에서 TLB 무효화 수행 */
static void flush_tlb_func(void *info)
{
struct flush_tlb_info *f = info;
struct mm_struct *loaded_mm = this_cpu_read(cpu_tlbstate.loaded_mm);
if (loaded_mm != f->mm) {
return; /* 이 CPU에서 해당 mm을 사용하지 않으면 무시 */
}
if (f->end == TLB_FLUSH_ALL) {
/* 전체 TLB flush — CR3 reload */
count_vm_tlb_event(NR_TLB_LOCAL_FLUSH_ALL);
__flush_tlb_all();
} else {
/* 범위 지정 TLB flush — INVLPG 사용 */
unsigned long addr;
for (addr = f->start; addr < f->end;
addr += 1UL << f->stride_shift) {
__flush_tlb_one_user(addr); /* INVLPG */
}
}
}
TLB Flush 최적화: 커널은 불필요한 IPI를 줄이기 위해 여러 최적화를 적용합니다. mm_cpumask()를 통해 해당 메모리 매핑을 실제로 사용 중인 CPU만 대상으로 하며, flush할 페이지 수가 많으면 전체 TLB flush(CR3 reload)로 전환합니다. 또한 lazy TLB 모드에서는 커널 스레드처럼 유저 매핑이 불필요한 경우 TLB flush를 지연시킬 수 있습니다.
IRQ Work IPI
IRQ Work는 NMI나 하드 인터럽트 컨텍스트처럼 일반적인 작업 큐를 사용할 수 없는 상황에서도 안전하게 deferred 작업을 예약할 수 있는 메커니즘입니다. 대표적인 사용처는 NMI 컨텍스트에서의 printk() 출력, perf 이벤트 처리, RCU 콜백 가속 등입니다:
/* kernel/irq_work.c — IRQ Work 핵심 구조 */
struct irq_work {
struct __call_single_node node;
void (*func)(struct irq_work *);
/* flags: IRQ_WORK_LAZY — 다음 tick까지 지연 가능
* IRQ_WORK_HARD_IRQ — hard IRQ 컨텍스트에서 실행
* IRQ_WORK_PENDING — 큐에 대기 중 */
};
/* IRQ Work 예약 — NMI 컨텍스트에서도 안전 */
bool irq_work_queue(struct irq_work *work)
{
if (!irq_work_claim(work))
return false; /* 이미 큐에 있음 */
__irq_work_queue_local(work);
/* 현재 CPU의 per-CPU raised_list 또는 lazy_list에 삽입 */
if (!(work->node.type & IRQ_WORK_LAZY))
arch_irq_work_raise(); /* IRQ_WORK_VECTOR self-IPI 전송 */
return true;
}
/* 원격 CPU에 IRQ Work 예약 */
bool irq_work_queue_on(struct irq_work *work, int cpu)
{
if (!irq_work_claim(work))
return false;
/* smp_call_function_single_async()를 사용하여
* 대상 CPU에 IRQ_WORK_VECTOR IPI 전송 */
__smp_call_single_queue(cpu, &work->node);
return true;
}
/* 사용 예: NMI 컨텍스트에서 printk 트리거 */
static struct irq_work printk_wake_work;
void printk_trigger_flush(void)
{
/* NMI에서 직접 콘솔 출력 불가 → IRQ Work로 지연 */
irq_work_queue(&printk_wake_work);
}
SMP 부팅 시 IPI 활용
멀티코어 시스템의 부팅 과정에서 BSP(Bootstrap Processor)가 AP(Application Processor)를 깨울 때 특수한 IPI 시퀀스를 사용합니다:
/* arch/x86/kernel/smpboot.c — AP 초기화 IPI 시퀀스 */
/* Intel MP 사양에 따른 INIT-SIPI-SIPI 시퀀스 */
static int wakeup_secondary_cpu_via_init(int phys_apicid,
unsigned long start_eip)
{
/* 1단계: INIT IPI — AP를 리셋 상태로 전환 */
apic_icr_write(APIC_INT_LEVELTRIG | APIC_INT_ASSERT |
APIC_DM_INIT, phys_apicid);
udelay(200); /* 200us 대기 */
/* INIT de-assert */
apic_icr_write(APIC_INT_LEVELTRIG | APIC_DM_INIT,
phys_apicid);
udelay(10000); /* 10ms 대기 (Intel 사양 요구) */
/* 2단계: SIPI (Startup IPI) x 2회
* start_eip: AP가 리얼모드에서 시작할 물리 주소
* 4KB 정렬된 주소의 상위 8비트를 벡터로 인코딩 */
for (int j = 1; j <= 2; j++) {
apic_icr_write(APIC_DM_STARTUP |
(start_eip >> 12), phys_apicid);
udelay(300); /* 300us 대기 */
}
/* AP는 start_eip(trampoline 코드)에서 리얼모드로 시작 →
* 보호모드 → 롱모드 전환 후 start_secondary()로 진입 */
return 0;
}
/* AP 시작 후 호출되는 진입점 */
static void start_secondary(void *unused)
{
cpu_init(); /* GDT, IDT, TSS 초기화 */
x86_cpuinit.setup_percpu_clockev();
apic_ap_setup(); /* Local APIC 설정 */
set_cpu_online(smp_processor_id(), true);
cpu_startup_entry(CPUHP_AP_ONLINE_IDLE); /* idle 루프 진입 */
}
IPI 성능 특성과 최적화
IPI는 CPU 간 통신의 기본이지만 상당한 오버헤드를 수반합니다. 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 캐시 공유 시 더 빠름 | |
| 다른 소켓 CPU간 | ~2-5us | NUMA 인터커넥트 경유 | |
/* 커널의 IPI 최적화 기법들 */
/* 1. Batch TLB flush: 여러 페이지 변경을 모아서 한 번에 IPI 전송 */
tlb_gather_mmu(&tlb, mm);
/* ... 여러 PTE 변경 ... */
tlb_finish_mmu(&tlb); /* 모든 변경 후 한 번만 IPI */
/* 2. Lazy TLB: 커널 스레드에서는 유저 TLB flush 생략 */
/* mm_cpumask에서 커널 스레드만 실행 중인 CPU는 제외 */
/* 3. PCID (Process Context Identifiers): CR3 변경 시
* 전체 TLB flush 대신 PCID별 무효화로 범위 축소 */
/* 4. Reschedule IPI 회피: idle CPU에 IPI 대신
* ttwu_queue_wakelist()로 원격 wakeup 큐 사용 */
if (cpus_share_cache(smp_processor_id(), cpu) ||
!cpu_is_idle(cpu)) {
/* 같은 LLC 또는 실행 중 → 직접 enqueue + IPI */
} else {
/* 다른 LLC의 idle CPU → wakelist에 추가, IPI 최소화 */
__ttwu_queue_wakelist(p, cpu, wake_flags);
}
/* 5. Multi-target IPI: 가능한 경우 broadcast shorthand 사용
* ICR shorthand=11 (All Excluding Self)로 단일 기록으로
* 모든 CPU에 동시 IPI → CPU 수에 무관한 고정 비용 */
IPI Storm 주의: 대규모 NUMA 시스템(수백 코어)에서 빈번한 전체 TLB flush나 smp_call_function() 호출은 IPI storm을 유발할 수 있습니다. 모든 CPU가 IPI 처리에 시간을 소모하면 실제 작업 처리량이 급격히 감소합니다. perf stat -e irq_vectors:call_function_entry로 IPI 빈도를 모니터링하고, on_each_cpu_cond()이나 cpumask 기반 API로 대상 CPU를 최소화하세요.
IPI 모니터링과 디버깅
IPI 관련 성능 문제를 진단하기 위한 도구와 방법입니다:
# /proc/interrupts에서 IPI 통계 확인
# RES: Reschedule IPI, CAL: Call Function IPI,
# TLB: TLB shootdown IPI
grep -E "RES|CAL|TLB|IRQ_WORK" /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# RES: 1245832 1189244 1023444 1145678 Rescheduling interrupts
# CAL: 432109 398234 445312 412890 Function call interrupts
# TLB: 234567 198432 245123 213456 TLB shootdowns
# 일정 간격으로 IPI 증가량 모니터링
watch -n1 "grep -E 'RES|CAL|TLB' /proc/interrupts"
# perf로 IPI 이벤트 트레이싱
perf stat -e 'irq_vectors:reschedule_entry' \
-e 'irq_vectors:call_function_entry' \
-e 'irq_vectors:call_function_single_entry' \
-a -- sleep 10
# ftrace로 IPI 발생 원인 추적
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_raise/enable
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/ipi/ipi_exit/enable
cat /sys/kernel/debug/tracing/trace_pipe
# <...>-1234 [000] smp_call_function_single: target=2 callback=flush_tlb_func
/* 커널 내부에서 IPI 디버깅 */
/* per-CPU IPI 통계 카운터 (arch/x86/kernel/irq.c) */
__inc_irq_stat(irq_resched_count); /* RES 카운터 */
__inc_irq_stat(irq_call_count); /* CAL 카운터 */
__inc_irq_stat(irq_tlb_count); /* TLB 카운터 */
/* CSD lock 디버깅 — smp_call_function이 너무 오래 걸릴 때 */
/* CONFIG_CSD_LOCK_WAIT_DEBUG 활성화 시
* CSD lock 대기가 임계값 초과하면 경고 출력 */
#ifdef CONFIG_CSD_LOCK_WAIT_DEBUG
/* 기본 임계값: 약 5초 (sysctl csd_lock_timeout) */
/* 대상 CPU가 응답하지 않으면 backtrace 덤프 */
csd_lock_wait(csd); /* 타임아웃 시 WARNING + 대상 CPU 스택 출력 */
#endif
/* /proc/interrupts 출력 구조 */
/*
* IPI 항목별 의미:
* RES — Reschedule IPI: 스케줄러가 태스크 이동 시 발생
* 높은 값 → 빈번한 태스크 마이그레이션 (NUMA 밸런싱 확인)
*
* CAL — Call Function IPI: smp_call_function* 호출
* 높은 값 → 빈번한 원격 함수 호출 (TLB flush 포함)
*
* TLB — TLB Shootdowns: 메모리 매핑 변경으로 인한 TLB 무효화
* 높은 값 → mmap/munmap/mprotect 빈번, 공유 메모리 워크로드
*/
IPI 성능 튜닝 체크리스트:
/proc/interrupts의 RES/CAL/TLB 카운터를 주기적으로 모니터링- TLB shootdown이 과다하면
CONFIG_X86_PCID(PCID)와 huge page 사용을 검토 - Reschedule IPI가 과다하면
sched_migration_cost_ns튜닝으로 태스크 마이그레이션 빈도 조절 - NUMA 시스템에서는
numactl --cpubind로 프로세스를 특정 노드에 바인딩하여 cross-socket IPI 최소화 CONFIG_CSD_LOCK_WAIT_DEBUG=y로 느린 IPI 응답 감지 활성화
ICR 레지스터 구조 상세
ICR(Interrupt Command Register)은 Local APIC에서 IPI를 전송하기 위한 핵심 레지스터입니다. xAPIC 모드에서는 MMIO 주소 0xFEE00300(하위 32비트)과 0xFEE00310(상위 32비트)에 매핑되며, x2APIC 모드에서는 MSR 0x830에 64비트로 통합됩니다. 각 비트 필드의 정확한 의미를 이해하는 것이 IPI 메커니즘 분석의 기초입니다.
/* 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 엔트리에 프로세스별 태그를 부여합니다. 이를 통해 컨텍스트 스위칭 시 전체 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를 덤프합니다. 이는 대상 CPU가 인터럽트를 오래 비활성화하거나, 하드 락업(hard lockup)에 빠진 경우를 진단하는 데 유용합니다. sysctl -w kernel.csd_lock_timeout=10으로 임계값을 조정할 수 있습니다.
IPI 성능 분석 상세
IPI의 지연 시간(latency)은 CPU 토폴로지, 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 등)이나 페이지 폴트를 유발할 수 있는 작업은 반드시 피해야 합니다. 필요한 데이터는 IPI 전송 전에 커널 메모리에 복사해 두고, 콜백에서는 해당 커널 메모리만 참조하세요.
IPI와 메모리 순서 보장
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 계열 조합을 명시적으로 사용하세요.
가상화 환경의 IPI (KVM/Hypervisor)
가상화에서는 IPI 경로가 추가 계층을 통과합니다. 게스트 커널이 APIC ICR을 기록해도 실제 물리 CPU 인터럽트로 바로 가지 않고, VM-exit 혹은 APICv/posted-interrupt 경로를 거쳐 하이퍼바이저가 중개합니다. 이때 불필요한 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 공유 패턴 변화 | 워크로드 스레드 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'
관련 문서
IPI와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.