01 · QUESTION
무엇을 확인할 것인가
IPI payload는 hardware가 운반하는가, shared memory에 놓이는가? target CPU가 요청을 보았다고 판단하는 completion point는 어디인가?
IPI hardware는 vector 또는 doorbell을 전달한다. callback 함수와 인자는 kernel/smp.c의 대상 CPU별 call_single_queue에 놓인다. llist_add()가 빈 큐에 첫 노드를 넣으면 IPI를 보내고, 수신 CPU가 큐를 분리해 callback을 실행한다.
CSD 큐에 게시, 가상 IPI reason 표시, 물리 doorbell, callback 실행, csd_unlock() 완료를 각각 구분한다. 동기 smp_call_function_many(..., wait=true)는 각 대상 CSD의 unlock을 기다리므로 IRQ 비활성 상태나 의존 lock을 잡은 상태를 피해야 한다.
Tipi = Tqueue + Tdoorbell + Tinterrupt_off_target + Thandler + Tcallback. target이 IRQ를 오래 막으면 sender의 synchronous wait도 같은 시간만큼 늘어난다.02 · CONTRACT
공통 규약과 아키텍처별 구현
| architecture | 핵심 mechanism | 실패 형태 | 확인할 상태 |
|---|---|---|---|
| arm64 | GIC SGI와 logical CPU affinity encoding | MPIDR affinity encoding이나 cpu logical map이 틀리면 다른 PE에 SGI가 가거나 target이 영원히 응답하지 않는다. | SGI INTID, target MPIDR, ipi reason counter, call_single queue와 completion cpumask를 확인한다. |
| x86-64 | local APIC ICR 또는 x2APIC MSR vector delivery | offline CPU나 stale APIC ID를 target으로 삼으면 sender가 completion을 기다리며 멈춘다. vector 충돌은 잘못된 system handler 호출로 이어진다. | APIC ID, ICR destination/vector, vector statistics, target irq-off duration과 call queue head를 본다. |
| RISC-V | SBI send_ipi, supervisor software interrupt 또는 IMSIC IPI | SBI 경로의 CPU→hart ID 오류 또는 SSIP 처리 누락, IMSIC 경로의 IPI EIID/TOPEI 처리 오류, 가상 IPI 비트 소비 누락은 callback 대기를 멈추게 할 수 있다. | CPU→hart ID, ipi_mux_cpu.bits, SBI 경로의 SIP.SSIP, IMSIC 경로의 TOPEI EIID, 대상 CSD unlock을 경로별로 확인한다. |
03 · DIAGRAMS
세 그림으로 먼저 읽기
arm64
- mechanism
- GIC SGI와 logical CPU affinity encoding
- state
- sender가 target affinity를 ICC_SGI1R_EL1에 써 SGI를 보내고
do_handle_IPI()가 IPI_RESCHEDULE, CALL_FUNC, CPU_STOP, TIMER 등 reason을 분기한다. payload queue는 cache-coherent memory에 있다. - checkpoint
- SGI INTID, target MPIDR, ipi reason counter, call_single queue와 completion cpumask를 확인한다.
x86-64
- mechanism
- local APIC ICR 또는 x2APIC MSR vector delivery
- state
- sender는 destination APIC ID와 vector를 ICR에 기록한다. target의 system-vector IDT handler가 call-function 또는 reschedule 코드를 실행하고 per-CPU queue를 비운다.
- checkpoint
- APIC ID, ICR destination/vector, vector statistics, target irq-off duration과 call queue head를 본다.
RISC-V
- mechanism
- SBI send_ipi, supervisor software interrupt 또는 IMSIC IPI
- state
arch/riscv/kernel/smp.c는 reason별ipi_desc로 가상 IPI를 보낸다. IPI-Mux가 대상 CPU의bits를 설정한 뒤 SBI는 SSIP를, IMSIC은 IPI EIID를 울린다. SBI handler는 SSIP를 지우고, IMSIC handler는 TOPEI를 읽어 acknowledge한다. 두 경로 모두ipi_mux_process()로 reason별 Linux IRQ를 처리한다.- checkpoint
- CPU→hart ID,
ipi_mux_cpu.bits, SBI 경로의 SIP.SSIP, IMSIC 경로의 TOPEI EIID, 대상 CSD unlock을 경로별로 확인한다.
call_single_queue의 CSD에 함수와 인자를 게시한다. 빈 큐에 첫 노드가 들어갈 때만 call-function IPI를 보내며, 수신 CPU가 큐를 떼어 callback을 실행한다.smp.c: ipi_desc[IPI_CALL_FUNC]reason별 Linux IRQ를 __ipi_send_mask()에 전달한다. callback 포인터가 IPI에 실리지는 않는다.ipi_mux_cpu.bitsatomic_fetch_or_release()로 reason을 게시하고, 수신 측 atomic_fetch_andnot()가 활성 비트를 소비한다.sbi_send_ipi(), IMSIC은 IMSIC_IPI_ID MMIO 쓰기다. 어느 쪽도 CSD 자체를 운반하지 않는다.csd_unlock() 뒤 csd_lock_wait()가 끝나야 완료된다.ipi_desc[IPI_CALL_FUNC] → ipi_mux_cpu.bits release 게시→새로 pending이고 enabled일 때만 물리 IPIsbi_send_ipi(cpu) → SSIP→sbi_ipi_handle()이 CSR_IP의 software-pending 비트를 IE_SIE 마스크로 clear→ipi_mux_process()writel(IMSIC_IPI_ID) → 외부 인터럽트→csr_swap(CSR_TOPEI, 0)으로 IPI EIID claim/ack→ipi_mux_process(); SSIP clear 없음handle_IPI() → CSD_TYPE_SYNC callback→callback 뒤 csd_unlock() release→sender의 csd_lock_wait() acquire 종료04 · SOURCE
Linux 6.18.37 원본 코드와 주요 구문 해설
소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.
arm64 · Linux 6.18.37
GIC SGI와 logical CPU affinity encoding
sender가 target affinity를 ICC_SGI1R_EL1에 써 SGI를 보내고 do_handle_IPI()가 IPI_RESCHEDULE, CALL_FUNC, CPU_STOP, TIMER 등 reason을 분기한다. payload queue는 cache-coherent memory에 있다.
원본 코드: arch/arm64/kernel/smp.c:950-1040
950 __ipi_send_single(get_ipi_desc(cpu, IPI_KGDB_ROUNDUP), cpu);
951 }
952}
953#endif
954
955/*
956 * Main handler for inter-processor interrupts
957 */
958static void do_handle_IPI(int ipinr)
959{
960 unsigned int cpu = smp_processor_id();
961
962 if ((unsigned)ipinr < NR_IPI)
963 trace_ipi_entry(ipi_types[ipinr]);
964
965 switch (ipinr) {
966 case IPI_RESCHEDULE:
967 scheduler_ipi();
968 break;
969
970 case IPI_CALL_FUNC:
971 generic_smp_call_function_interrupt();
972 break;
973
974 case IPI_CPU_STOP:
975 case IPI_CPU_STOP_NMI:
976 if (IS_ENABLED(CONFIG_KEXEC_CORE) && crash_stop) {
977 ipi_cpu_crash_stop(cpu, get_irq_regs());
978 unreachable();
979 } else {
980 local_cpu_stop(cpu);
981 }
982 break;
983
984#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
985 case IPI_TIMER:
986 tick_receive_broadcast();
987 break;
988#endif
989
990#ifdef CONFIG_IRQ_WORK
991 case IPI_IRQ_WORK:
992 irq_work_run();
993 break;
994#endif
995
996 case IPI_CPU_BACKTRACE:
997 /*
998 * NOTE: in some cases this _won't_ be NMI context. See the
999 * comment in arch_trigger_cpumask_backtrace().
1000 */
1001 nmi_cpu_backtrace(get_irq_regs());
1002 break;
1003
1004 case IPI_KGDB_ROUNDUP:
1005 kgdb_nmicallback(cpu, get_irq_regs());
1006 break;
1007
1008 default:
1009 pr_crit("CPU%u: Unknown IPI message 0x%x\n", cpu, ipinr);
1010 break;
1011 }
1012
1013 if ((unsigned)ipinr < NR_IPI)
1014 trace_ipi_exit(ipi_types[ipinr]);
1015}
1016
1017static irqreturn_t ipi_handler(int irq, void *data)
1018{
1019 unsigned int ipi = (irq - ipi_irq_base) % nr_ipi;
1020
1021 do_handle_IPI(ipi);
1022 return IRQ_HANDLED;
1023}
1024
1025static void smp_cross_call(const struct cpumask *target, unsigned int ipinr)
1026{
1027 trace_ipi_raise(target, ipi_types[ipinr]);
1028 arm64_send_ipi(target, ipinr);
1029}
1030
1031static bool ipi_should_be_nmi(enum ipi_msg_type ipi)
1032{
1033 if (!system_uses_irq_prio_masking())
1034 return false;
1035
1036 switch (ipi) {
1037 case IPI_CPU_STOP_NMI:
1038 case IPI_CPU_BACKTRACE:
1039 case IPI_KGDB_ROUNDUP:
1040 return true;주요 구문 해설
원본 91줄은 그대로 표시하며, 검토한 57개 구문에 설명을 붙였습니다.
__ipi_send_single(get_ipi_desc(cpu, IPI_KGDB_ROUNDUP), cpu);지정 CPU에 KGDB roundup IPI를 보냅니다. 다른 CPU들도 디버거 정지 처리에 참여하도록 해당 CPU용 IPI 디스크립터를 골라 단일 대상으로 전송합니다.
#endif939행에서 시작한 선택 구간을 마칩니다. KGDB는 커널을 원격 디버거로 조사하는 기능입니다. 활성 디버깅에 참여할 다른 온라인 CPU들을 모으기 위해 IPI_KGDB_ROUNDUP을 보내는 구현을 이 설정에서만 포함합니다.
static void do_handle_IPI(int ipinr)IPI 종류 번호를 받아 현재 CPU에서 그 요청을 처리하는 공통 함수입니다. 재스케줄·함수 실행·정지·디버깅 요청을 서로 다른 처리기로 연결합니다.
unsigned int cpu = smp_processor_id();IPI를 받은 현재 CPU 번호를 얻습니다. CPU 정지·crash 처리와 진단 메시지에 수신 CPU의 식별자를 넘깁니다.
if ((unsigned)ipinr < NR_IPI)번호가 IPI 이름 표의 범위 안일 때만 진입 trace를 남깁니다. unsigned 변환은 음수 입력도 범위 밖으로 취급하게 하며, 범위 밖 번호는 아래 default에서 진단합니다.
trace_ipi_entry(ipi_types[ipinr]);유효한 IPI 종류의 처리 시작을 trace에 기록합니다. 송신 시점과 수신 처리 시간을 연결해 관측하기 위한 이벤트입니다.
switch (ipinr) {수신한 IPI의 목적에 따라 처리기를 고릅니다. 같은 CPU 간 인터럽트라도 스케줄링 요청과 CPU 정지 요청은 후속 동작이 다릅니다.
case IPI_RESCHEDULE:원격 CPU가 스케줄링을 다시 검토해 달라고 요청한 경우입니다. scheduler_ipi가 현재 CPU의 스케줄링 필요 상태를 반영합니다.
scheduler_ipi();need_resched, nohz wakeup 같은 scheduler 요청을 target CPU에서 처리한다.
break;재스케줄 통지 처리를 마쳤으므로 다른 IPI 처리기로 이어지지 않고 switch를 끝냅니다.
case IPI_CALL_FUNC:다른 CPU가 이 CPU에서 실행하도록 큐에 넣은 함수를 처리하는 IPI입니다. 공통 SMP 호출 큐 처리기로 넘깁니다.
generic_smp_call_function_interrupt();shared call-function queue를 꺼내 등록된 callback과 argument를 실행한다.
break;대기 중인 SMP 함수 호출 처리를 마치고 종류별 분기를 끝냅니다.
case IPI_CPU_STOP:일반 CPU 정지 요청을 받는 분기입니다. 다음의 NMI 정지 요청과 같은 정지 본문을 사용합니다.
case IPI_CPU_STOP_NMI:일반 IRQ 마스크를 넘어 전달할 수 있도록 요청한 CPU 정지 IPI입니다. 전달 우선순위는 달라도 정지 시 crash 상태를 남길지는 아래에서 따로 결정합니다.
if (IS_ENABLED(CONFIG_KEXEC_CORE) && crash_stop) {kexec 기반 커널 전환을 지원하고 현재 요청이 crash 정지일 때만 덤프용 CPU 상태를 남기는 경로를 택합니다. 그 외에는 일반 CPU 정지 처리를 수행합니다.
ipi_cpu_crash_stop(cpu, get_irq_regs());crash 정지 요청이면 현재 IRQ 레지스터와 CPU 번호를 넘겨 crash 상태를 저장하고 CPU를 멈춥니다. 덤프용 커널이 동작하는 동안 이 CPU가 원래 커널 실행을 계속하지 않게 합니다.
unreachable();앞의 crash 정지 함수에서 이 지점으로 돌아오지 않는다는 사실을 컴파일러에 알립니다. CPU를 새로 정지시키는 함수가 아니라 도달 불가능한 제어 흐름을 표시하는 매크로입니다.
} else {crash 상태 저장이 필요한 정지 요청이 아니면 현재 CPU를 일반 정지 경로로 보냅니다.
local_cpu_stop(cpu);일반 정지 요청을 받은 CPU의 실행을 정지 경로로 보냅니다. crash 상태 저장이 필요한 경우와 구분하여 다른 CPU의 종료 요청에 응답합니다.
break;CPU 정지 종류의 처리를 끝내는 switch 경계입니다. 정상적인 정지 경로는 원래 실행으로 돌아오지 않습니다.
#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST타이머 broadcast는 로컬 타이머 대신 다른 CPU의 타이머 통지를 받아 tick 처리를 이어가는 기능입니다. 켜지면 IPI_TIMER 수신을 tick_receive_broadcast에 연결하고, 꺼지면 이 분기 자체가 없습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
case IPI_TIMER:다른 CPU가 대신 전달한 타이머 tick 요청입니다. 로컬 타이머를 사용할 수 없는 상황에서도 tick 처리로 이어집니다.
tick_receive_broadcast();다른 CPU가 방송한 타이머 이벤트를 이 CPU에서 처리합니다. 로컬 타이머를 쓸 수 없는 idle 상태 등의 tick 전달을 이어받는 경로입니다.
break;방송 타이머 이벤트를 처리한 뒤 다른 IPI 종류로 진행하지 않습니다.
#endif984행에서 시작한 선택 구간을 마칩니다. 타이머 broadcast는 로컬 타이머 대신 다른 CPU의 타이머 통지를 받아 tick 처리를 이어가는 기능입니다. 켜지면 IPI_TIMER 수신을 tick_receive_broadcast에 연결하고, 꺼지면 이 분기 자체가 없습니다.
#ifdef CONFIG_IRQ_WORKirq_work는 원자적 문맥 등에서 요청한 짧은 작업을 나중에 IRQ 문맥에서 실행하는 장치입니다. 켜진 빌드에만 IPI_IRQ_WORK 수신 시 대기 작업을 실행하는 분기를 포함합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
case IPI_IRQ_WORK:인터럽트 문맥에서 실행하도록 예약한 irq_work를 처리하라는 요청입니다. 현재 CPU의 작업 큐를 실행합니다.
irq_work_run();이 CPU에 대기 중인 irq_work 콜백을 실행합니다. 인터럽트 문맥에서 실행하도록 예약한 작업을 IPI로 깨워 처리합니다.
break;irq_work 처리를 마치고 종류별 분기에서 나갑니다.
#endif990행에서 시작한 선택 구간을 마칩니다. irq_work는 원자적 문맥 등에서 요청한 짧은 작업을 나중에 IRQ 문맥에서 실행하는 장치입니다. 켜진 빌드에만 IPI_IRQ_WORK 수신 시 대기 작업을 실행하는 분기를 포함합니다.
case IPI_CPU_BACKTRACE:현재 CPU의 스택 추적을 요청하는 IPI입니다. 고착되거나 오래 실행 중인 CPU의 실행 위치를 다른 CPU에서 조사할 때 사용합니다.
nmi_cpu_backtrace(get_irq_regs());IPI가 중단시킨 레지스터 문맥을 기준으로 이 CPU의 스택 추적을 출력합니다. 이름에 NMI가 들어가도 이 arm64 경로가 항상 NMI 문맥인 것은 아닙니다.
break;스택 추적 요청에 응답한 뒤 IPI 분기를 끝냅니다.
case IPI_KGDB_ROUNDUP:KGDB가 다른 CPU들도 디버거 정지에 참여하게 하는 roundup 요청입니다. 수신 CPU의 번호와 중단된 레지스터를 디버거에 넘깁니다.
kgdb_nmicallback(cpu, get_irq_regs());현재 CPU 번호와 IRQ 레지스터를 KGDB의 정지 콜백에 넘깁니다. 여러 CPU가 동시에 실행하는 동안 디버거가 일관된 정지 상태를 갖도록 참여시킵니다.
break;KGDB의 CPU 정지 콜백에서 돌아온 뒤 다른 IPI를 추가로 처리하지 않습니다.
default:정의된 처리 종류와 맞지 않는 번호는 이 경로로 들어옵니다. 임의의 동작을 실행하지 않고 수신 CPU와 잘못된 번호를 로그에 남깁니다.
pr_crit("CPU%u: Unknown IPI message 0x%x\n", cpu, ipinr);지원하지 않는 IPI 번호를 수신했음을 CPU 번호와 함께 오류 로그에 기록합니다. 알 수 없는 번호를 임의의 IPI 처리기로 실행하지 않습니다.
break;알 수 없는 IPI의 오류 보고를 마친 뒤 switch를 끝냅니다.
if ((unsigned)ipinr < NR_IPI)유효한 IPI 번호에 대해서만 종료 trace를 남깁니다. 앞의 진입 이벤트와 짝을 맞추면서 이름 표의 범위를 벗어난 접근을 피합니다.
trace_ipi_exit(ipi_types[ipinr]);유효한 IPI 종류의 처리 종료를 trace에 남깁니다. 앞의 entry 이벤트와 짝을 이뤄 처리 구간을 나타냅니다.
static irqreturn_t ipi_handler(int irq, void *data)IRQ 코어가 호출하는 per-CPU IPI 처리 함수입니다. Linux IRQ 번호를 IPI 종류로 바꿔 공통 처리기에 넘긴 뒤 처리 완료를 돌려줍니다.
unsigned int ipi = (irq - ipi_irq_base) % nr_ipi;Linux IRQ 번호에서 IPI 영역의 시작을 빼고 종류 수로 나눈 나머지를 구합니다. IRQ 영역 안에서 반복되는 배치도 같은 IPI 종류 번호로 환산됩니다.
do_handle_IPI(ipi);환산한 종류 번호를 실제 IPI 처리기에 넘깁니다. IRQ 번호 해석과 재스케줄·정지 등의 동작 처리를 나눈 구조입니다.
return IRQ_HANDLED;이 IRQ를 해당 IPI 처리기가 처리했음을 IRQ 코어에 알립니다. 요청된 콜백의 임의 반환값을 전달하는 의미는 아닙니다.
static void smp_cross_call(const struct cpumask *target, unsigned int ipinr)target의 CPU 집합에 ipinr 종류의 IPI를 보내는 공통 송신 함수입니다. trace를 남긴 뒤 arm64 전송 연산을 호출합니다.
trace_ipi_raise(target, ipi_types[ipinr]);어느 CPU 집합에 어떤 종류의 IPI를 요청하는지 trace에 기록합니다. 실제 인터럽트 전송 직전의 관측 지점입니다.
arm64_send_ipi(target, ipinr);target CPU 마스크에 ipinr 종류의 IPI를 전송합니다. 아래 아키텍처 전송 연산이 각 대상 CPU의 IPI 처리 경로를 깨웁니다.
static bool ipi_should_be_nmi(enum ipi_msg_type ipi)지정한 IPI를 일반 IRQ보다 높은 우선순위의 NMI 방식으로 설치할지 결정합니다. 시스템의 우선순위 마스킹 지원과 IPI 목적을 함께 봅니다.
if (!system_uses_irq_prio_masking())GIC 우선순위 마스킹을 사용하지 않는 시스템이면 pseudo-NMI로 IPI를 구분할 수 없으므로 아래의 종류 검사 없이 거짓을 반환합니다.
return false;이 시스템에서는 해당 IPI를 NMI로 취급하지 않음을 반환합니다. CPU 정지 등 요청 자체를 금지하는 것은 아닙니다.
switch (ipi) {우선순위 마스킹을 사용할 수 있을 때, 일반 IRQ가 막힌 CPU에도 도달해야 하는 IPI 종류를 고릅니다.
case IPI_CPU_STOP_NMI:NMI 방식의 CPU 정지는 일반 IRQ가 막혀 있어도 정지 요청을 전달할 필요가 있으므로 참을 반환하는 그룹에 넣습니다.
case IPI_CPU_BACKTRACE:CPU 스택 추적도 IRQ를 막고 고착된 CPU를 진단할 수 있도록 NMI 대상 그룹에 넣습니다.
case IPI_KGDB_ROUNDUP:KGDB의 다른 CPU 정지 요청도 일반 IRQ 마스크에 막히지 않게 NMI 대상 그룹에 포함합니다.
return true;위 세 종류는 이 시스템에서 NMI 방식의 IPI로 구성해야 한다고 반환합니다.
x86-64 · Linux 6.18.37
local APIC ICR 또는 x2APIC MSR vector delivery
sender는 destination APIC ID와 vector를 ICR에 기록한다. target의 system-vector IDT handler가 call-function 또는 reschedule 코드를 실행하고 per-CPU queue를 비운다.
원본 코드: arch/x86/kernel/smp.c:249-299
249{
250 apic_eoi();
251 trace_reschedule_entry(RESCHEDULE_VECTOR);
252 inc_irq_stat(irq_resched_count);
253 scheduler_ipi();
254 trace_reschedule_exit(RESCHEDULE_VECTOR);
255}
256
257DEFINE_IDTENTRY_SYSVEC(sysvec_call_function)
258{
259 apic_eoi();
260 trace_call_function_entry(CALL_FUNCTION_VECTOR);
261 inc_irq_stat(irq_call_count);
262 generic_smp_call_function_interrupt();
263 trace_call_function_exit(CALL_FUNCTION_VECTOR);
264}
265
266DEFINE_IDTENTRY_SYSVEC(sysvec_call_function_single)
267{
268 apic_eoi();
269 trace_call_function_single_entry(CALL_FUNCTION_SINGLE_VECTOR);
270 inc_irq_stat(irq_call_count);
271 generic_smp_call_function_single_interrupt();
272 trace_call_function_single_exit(CALL_FUNCTION_SINGLE_VECTOR);
273}
274
275static int __init nonmi_ipi_setup(char *str)
276{
277 smp_no_nmi_ipi = true;
278 return 1;
279}
280
281__setup("nonmi_ipi", nonmi_ipi_setup);
282
283struct smp_ops smp_ops = {
284 .smp_prepare_boot_cpu = native_smp_prepare_boot_cpu,
285 .smp_prepare_cpus = native_smp_prepare_cpus,
286 .smp_cpus_done = native_smp_cpus_done,
287
288 .stop_other_cpus = native_stop_other_cpus,
289#if defined(CONFIG_CRASH_DUMP)
290 .crash_stop_other_cpus = kdump_nmi_shootdown_cpus,
291#endif
292 .smp_send_reschedule = native_smp_send_reschedule,
293
294 .kick_ap_alive = native_kick_ap,
295 .cpu_disable = native_cpu_disable,
296 .play_dead = native_play_dead,
297
298 .send_call_func_ipi = native_send_call_func_ipi,
299 .send_call_func_single_ipi = native_send_call_func_single_ipi,주요 구문 해설
원본 51줄은 그대로 표시하며, 검토한 35개 구문에 설명을 붙였습니다.
apic_eoi();local APIC의 in-service bit를 내려 같은 또는 낮은 priority vector가 다시 전달될 수 있게 한다.
trace_reschedule_entry(RESCHEDULE_VECTOR);재스케줄 IPI 벡터의 처리 시작을 trace에 기록합니다. 스케줄러 통지에 든 시간을 이후 exit 이벤트와 비교할 수 있게 합니다.
inc_irq_stat(irq_resched_count);현재 CPU의 재스케줄 IPI 수신 통계를 증가시킵니다. 이 줄은 실행 태스크를 선택하는 동작이 아니라 IPI 발생 횟수의 계수입니다.
scheduler_ipi();다른 CPU가 요청한 재스케줄 상태를 이 CPU의 스케줄러에 반영합니다. IPI 처리기 안에서 무조건 태스크를 바로 바꾸는 의미는 아닙니다.
trace_reschedule_exit(RESCHEDULE_VECTOR);재스케줄 IPI 처리가 끝났음을 trace에 기록합니다.
DEFINE_IDTENTRY_SYSVEC(sysvec_call_function)함수 실행 요청용 시스템 벡터의 IDT 진입 함수를 정의합니다. 매크로가 공통 인터럽트 진입·이탈 처리를 붙이고 본문은 APIC 확인과 SMP 호출 큐 처리를 수행합니다.
apic_eoi();local APIC의 in-service bit를 내려 같은 또는 낮은 priority vector가 다시 전달될 수 있게 한다.
trace_call_function_entry(CALL_FUNCTION_VECTOR);여러 CPU에 보낸 함수 실행 요청 IPI의 처리 시작을 trace에 남깁니다.
inc_irq_stat(irq_call_count);함수 실행 요청 IPI의 CPU별 수신 횟수를 증가시킵니다. 뒤의 공통 SMP 처리기가 실제 대기 콜백을 실행합니다.
generic_smp_call_function_interrupt();target CPU의 shared call queue를 처리하고 각 callback 완료를 publish한다.
trace_call_function_exit(CALL_FUNCTION_VECTOR);함수 실행 요청 IPI 처리의 종료 이벤트를 기록합니다. 바로 앞 공통 처리기가 콜백 큐를 처리한 뒤의 시점입니다.
DEFINE_IDTENTRY_SYSVEC(sysvec_call_function_single)단일 CPU 대상으로 보내는 함수 실행 IPI의 시스템 벡터 처리기를 정의합니다. 다중 대상 호출과 별도 벡터·trace 이름을 쓰지만 대기 함수 큐를 처리한다는 목적은 같습니다.
apic_eoi();local APIC의 in-service bit를 내려 같은 또는 낮은 priority vector가 다시 전달될 수 있게 한다.
trace_call_function_single_entry(CALL_FUNCTION_SINGLE_VECTOR);단일 CPU 대상 함수 실행 IPI의 진입 이벤트를 기록합니다. 다중 대상 벡터와 구분해 추적합니다.
inc_irq_stat(irq_call_count);단일 CPU 대상 함수 실행 IPI도 함수 호출 IPI 통계에 더합니다. 통계 갱신이며 콜백 호출 자체는 다음 줄입니다.
generic_smp_call_function_single_interrupt();현재 CPU에 큐로 전달된 SMP 함수 실행 요청을 처리합니다. 발신 CPU가 지정한 콜백과 데이터를 수신 CPU 문맥에서 실행하는 핵심 단계입니다.
trace_call_function_single_exit(CALL_FUNCTION_SINGLE_VECTOR);단일 CPU 대상 함수 실행 IPI의 처리가 끝났음을 trace에 기록합니다.
static int __init nonmi_ipi_setup(char *str)부팅 명령줄의 nonmi_ipi 옵션을 해석하는 초기화 함수입니다. 문자열 인자는 받지만 별도 값 없이 옵션이 나타났다는 사실로 정책을 켭니다.
smp_no_nmi_ipi = true;NMI IPI를 사용하지 않는 정책을 전역 상태에 기록합니다. 이후 다른 CPU 정지 등의 경로가 이 부팅 옵션을 참고하게 됩니다.
return 1;부팅 옵션 해석기에 이 옵션을 처리했다고 알립니다. 일반 함수의 성공 코드 0과 달리 __setup 처리 함수에서는 1이 소비 완료를 뜻합니다.
__setup("nonmi_ipi", nonmi_ipi_setup);부팅 명령줄의 nonmi_ipi 옵션을 nonmi_ipi_setup 함수와 연결해 등록합니다. 이 줄이 실행될 때 IPI를 보내는 것이 아니라 옵션 해석기에 처리 함수를 제공하는 선언형 매크로입니다.
struct smp_ops smp_ops = {x86 SMP 공통 코드가 사용할 동작 표를 정의합니다. 아래 멤버들은 지금 호출하는 코드가 아니라 각 작업에 연결할 native 함수 주소입니다.
.smp_prepare_boot_cpu = native_smp_prepare_boot_cpu,부트 CPU의 SMP 초기 준비 요청을 native 구현에 연결합니다. 부트 CPU가 온라인 CPU 관리에 참여할 기본 상태를 마련하는 지점입니다.
.smp_prepare_cpus = native_smp_prepare_cpus,보조 CPU들을 시작하기 전의 시스템 준비를 native_smp_prepare_cpus에 맡깁니다. AP 기동에 필요한 공통 자원을 준비하는 동작입니다.
.smp_cpus_done = native_smp_cpus_done,SMP CPU 준비를 마친 뒤 실행할 마무리 동작을 등록합니다. CPU 기동 전 준비와 완료 후 정리를 서로 다른 콜백으로 분리합니다.
.stop_other_cpus = native_stop_other_cpus,다른 CPU들을 멈추라는 요청에 native 정지 구현을 사용하도록 지정합니다. 시스템 종료·재시작처럼 다른 CPU의 실행을 끝내야 할 때 사용합니다.
#if defined(CONFIG_CRASH_DUMP)crash dump 지원은 시스템 오류 뒤 진단용 커널이 메모리를 조사할 수 있도록 준비하는 기능입니다. 켜지면 다른 CPU를 NMI로 멈추는 kdump 전용 동작을 SMP 함수 표에 등록합니다. 일반 CPU 정지 함수와 목적이 다른 항목입니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
.crash_stop_other_cpus = kdump_nmi_shootdown_cpus,crash 상황에서 다른 CPU의 상태를 확보하고 정지시키는 작업을 NMI 기반 kdump 구현에 연결합니다. 일반 정지와 달리 덤프에 필요한 CPU 상태 수집이 포함됩니다.
#endif289행에서 시작한 선택 구간을 마칩니다. crash dump 지원은 시스템 오류 뒤 진단용 커널이 메모리를 조사할 수 있도록 준비하는 기능입니다. 켜지면 다른 CPU를 NMI로 멈추는 kdump 전용 동작을 SMP 함수 표에 등록합니다. 일반 CPU 정지 함수와 목적이 다른 항목입니다.
.smp_send_reschedule = native_smp_send_reschedule,원격 CPU에 재스케줄 IPI를 보내는 동작을 native APIC 전송 함수에 연결합니다.
.kick_ap_alive = native_kick_ap,보조 CPU를 초기 실행으로 깨우는 작업을 native_kick_ap에 맡깁니다. 이미 실행 중인 CPU의 일반 재스케줄 요청과 다른 CPU 기동 작업입니다.
.cpu_disable = native_cpu_disable,CPU hotplug에서 현재 CPU를 오프라인으로 내릴 준비를 native_cpu_disable에 연결합니다. CPU를 더 이상 정상 작업 대상으로 사용하지 않도록 정리합니다.
.play_dead = native_play_dead,오프라인이 된 CPU가 대기·정지 상태에 머무르는 마지막 동작을 native_play_dead에 연결합니다.
.send_call_func_ipi = native_send_call_func_ipi,여러 CPU로 함수 실행 요청을 전송하는 IPI 동작을 등록합니다. 수신 CPU들은 각자 전달받은 콜백 큐를 처리합니다.
.send_call_func_single_ipi = native_send_call_func_single_ipi,지정한 CPU 하나로 함수 실행 IPI를 보내는 native 동작을 등록합니다.
RISC-V · Linux 6.18.37
SBI send_ipi, supervisor software interrupt 또는 IMSIC IPI
arch/riscv/kernel/smp.c는 reason별 ipi_desc로 가상 IPI를 보낸다. IPI-Mux가 대상 CPU의 bits를 설정한 뒤 SBI는 SSIP를, IMSIC은 IPI EIID를 울린다. SBI handler는 SSIP를 지우고, IMSIC handler는 TOPEI를 읽어 acknowledge한다. 두 경로 모두 ipi_mux_process()로 reason별 Linux IRQ를 처리한다.
원본 코드: arch/riscv/kernel/smp.c:124-200
124
125#ifdef CONFIG_IRQ_WORK
126void arch_irq_work_raise(void)
127{
128 send_ipi_single(smp_processor_id(), IPI_IRQ_WORK);
129}
130#endif
131
132static irqreturn_t handle_IPI(int irq, void *data)
133{
134 unsigned int cpu = smp_processor_id();
135 int ipi = irq - ipi_virq_base;
136
137 switch (ipi) {
138 case IPI_RESCHEDULE:
139 scheduler_ipi();
140 break;
141 case IPI_CALL_FUNC:
142 generic_smp_call_function_interrupt();
143 break;
144 case IPI_CPU_STOP:
145 ipi_stop();
146 break;
147 case IPI_CPU_CRASH_STOP:
148 ipi_cpu_crash_stop(cpu, get_irq_regs());
149 break;
150 case IPI_IRQ_WORK:
151 irq_work_run();
152 break;
153#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
154 case IPI_TIMER:
155 tick_receive_broadcast();
156 break;
157#endif
158 case IPI_CPU_BACKTRACE:
159 nmi_cpu_backtrace(get_irq_regs());
160 break;
161 case IPI_KGDB_ROUNDUP:
162 kgdb_nmicallback(cpu, get_irq_regs());
163 break;
164 default:
165 pr_warn("CPU%d: unhandled IPI%d\n", cpu, ipi);
166 break;
167 }
168
169 return IRQ_HANDLED;
170}
171
172void riscv_ipi_enable(void)
173{
174 int i;
175
176 if (WARN_ON_ONCE(!ipi_virq_base))
177 return;
178
179 for (i = 0; i < nr_ipi; i++)
180 enable_percpu_irq(ipi_virq_base + i, 0);
181}
182
183void riscv_ipi_disable(void)
184{
185 int i;
186
187 if (WARN_ON_ONCE(!ipi_virq_base))
188 return;
189
190 for (i = 0; i < nr_ipi; i++)
191 disable_percpu_irq(ipi_virq_base + i);
192}
193
194bool riscv_ipi_have_virq_range(void)
195{
196 return (ipi_virq_base) ? true : false;
197}
198
199void riscv_ipi_set_virq_range(int virq, int nr)
200{주요 구문 해설
원본 77줄은 그대로 표시하며, 검토한 53개 구문에 설명을 붙였습니다.
#ifdef CONFIG_IRQ_WORKirq_work를 지원하면 대기 중인 짧은 작업을 IRQ 문맥에서 실행하도록 현재 hart에 IPI_IRQ_WORK를 보낼 함수를 만듭니다. 기능이 없으면 이 알림 함수는 필요하지 않습니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
void arch_irq_work_raise(void)예약된 irq_work를 인터럽트 문맥에서 처리하도록 깨우는 아키텍처 함수입니다. RISC-V에서는 현재 CPU 자신에게 IPI를 보내 구현합니다.
send_ipi_single(smp_processor_id(), IPI_IRQ_WORK);현재 CPU 자신에게 irq_work용 IPI를 보냅니다. 예약한 작업을 인터럽트 문맥에서 실행하도록 로컬 CPU를 깨우는 방식입니다.
#endif125행에서 시작한 선택 구간을 마칩니다. irq_work를 지원하면 대기 중인 짧은 작업을 IRQ 문맥에서 실행하도록 현재 hart에 IPI_IRQ_WORK를 보낼 함수를 만듭니다. 기능이 없으면 이 알림 함수는 필요하지 않습니다.
static irqreturn_t handle_IPI(int irq, void *data)IRQ 코어가 전달한 IPI를 종류별로 처리하는 함수입니다. Linux IRQ 번호에서 종류를 구한 뒤 스케줄러·SMP 호출·정지·디버깅 처리로 나눕니다.
unsigned int cpu = smp_processor_id();IPI를 처리 중인 CPU의 논리 번호를 얻습니다. crash 처리와 디버거 통지, 알 수 없는 IPI 로그에 사용합니다.
int ipi = irq - ipi_virq_base;IPI 영역의 시작 IRQ를 빼서 수신 IRQ를 종류 번호로 환산합니다. 연속된 IRQ 영역에 각 IPI 종류를 하나씩 배치한 구조를 사용합니다.
switch (ipi) {수신한 IPI 종류에 맞는 작업을 선택합니다. 현재 hart에서 처리해야 할 서로 다른 원격 요청들을 하나의 IRQ 처리 함수로 모읍니다.
case IPI_RESCHEDULE:원격 CPU의 재스케줄 통지입니다. 현재 CPU의 스케줄링 필요 상태를 공통 스케줄러에 반영합니다.
scheduler_ipi();재스케줄 IPI를 공통 스케줄러 처리로 넘깁니다. 원격 CPU가 요청한 스케줄링 필요 상태를 현재 CPU에 반영합니다.
break;재스케줄 통지를 처리하고 IPI 종류 선택을 끝냅니다.
case IPI_CALL_FUNC:공통 SMP call queue를 target hart에서 실행하는 reason이다.
generic_smp_call_function_interrupt();현재 CPU에 전달된 SMP 함수 실행 요청 큐를 처리합니다. 다른 CPU가 실행을 요청한 콜백이 이 CPU 문맥에서 호출됩니다.
break;다른 CPU가 맡긴 함수 실행 요청을 처리한 뒤 다음 IPI 분기로 이어지지 않게 합니다.
case IPI_CPU_STOP:target hart를 멈추고 crash/stop synchronization에 참여시킨다.
ipi_stop();CPU 정지 IPI에 응답하여 현재 hart를 정지 경로로 보냅니다. 요청 이후 원래 커널 작업을 계속 수행하지 않게 합니다.
break;일반 CPU 정지 요청의 분기를 끝내는 위치입니다. 정상적인 정지 함수는 현재 hart의 원래 작업을 재개하지 않습니다.
case IPI_CPU_CRASH_STOP:crash 덤프를 위한 CPU 정지 요청입니다. 현재 hart의 레지스터 상태를 덤프에 남기고 멈추는 별도 경로를 선택합니다.
ipi_cpu_crash_stop(cpu, get_irq_regs());crash 정지 요청이면 현재 CPU와 중단된 레지스터 상태를 crash 처리에 넘깁니다. 덤프에 필요한 상태를 남기고 원래 실행을 멈추는 경로입니다.
break;crash 정지 요청에 해당하는 분기를 끝냅니다. 일반 IPI 작업으로 이어지지 않도록 분리한 경계입니다.
case IPI_IRQ_WORK:현재 CPU의 irq_work 큐를 처리하라는 요청입니다. 미리 예약해 둔 작업을 IRQ 문맥에서 실행합니다.
irq_work_run();현재 CPU에 대기 중인 irq_work를 인터럽트 문맥에서 실행합니다. IPI는 예약 작업의 실행을 시작시키는 통지 수단입니다.
break;irq_work 처리가 끝나면 종류별 분기를 벗어납니다.
#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST타이머 broadcast가 필요한 구성에서는 IPI_TIMER를 받아 tick 처리기에 전달합니다. 자신의 로컬 타이머를 사용할 수 없는 CPU도 다른 CPU가 보낸 타이머 통지를 처리할 수 있게 합니다. 이 조건은 빌드 전처리 단계에서 포함할 코드를 선택하며 런타임 조건 분기가 아닙니다.
case IPI_TIMER:방송 tick을 받는 분기입니다. 다른 CPU가 전달한 타이머 이벤트를 현재 CPU의 tick 처리로 연결합니다.
tick_receive_broadcast();다른 CPU가 전달한 broadcast tick을 수신 처리합니다. 로컬 clockevent를 쓰지 못하는 상황에서도 타이머 작업을 이어가게 합니다.
break;방송 타이머 처리를 마치고 다른 IPI 종류로 넘어가지 않습니다.
#endif153행에서 시작한 선택 구간을 마칩니다. 타이머 broadcast가 필요한 구성에서는 IPI_TIMER를 받아 tick 처리기에 전달합니다. 자신의 로컬 타이머를 사용할 수 없는 CPU도 다른 CPU가 보낸 타이머 통지를 처리할 수 있게 합니다.
case IPI_CPU_BACKTRACE:다른 CPU가 현재 hart의 스택 추적을 요청한 경우입니다. 중단된 레지스터를 토대로 실행 위치를 출력합니다.
nmi_cpu_backtrace(get_irq_regs());현재 IRQ 레지스터를 기준으로 CPU 스택 추적 요청에 응답합니다. IPI로 다른 hart의 실행 위치를 진단할 때 사용합니다.
break;스택 추적 요청의 처리를 마치고 IPI 분기를 끝냅니다.
case IPI_KGDB_ROUNDUP:KGDB가 여러 CPU의 실행을 멈추고 디버깅 상태를 맞추기 위한 roundup 요청입니다.
kgdb_nmicallback(cpu, get_irq_regs());CPU 번호와 중단된 레지스터를 KGDB의 roundup 콜백에 넘깁니다. 디버거가 다른 hart들을 정지시키고 상태를 수집할 수 있게 합니다.
break;KGDB 콜백에서 돌아오면 종류별 분기를 종료합니다.
default:알려진 IPI 종류와 일치하지 않을 때 진입합니다. 수신 CPU와 처리할 수 없는 번호를 경고하는 경로입니다.
pr_warn("CPU%d: unhandled IPI%d\n", cpu, ipi);처리할 수 없는 IPI 번호를 수신 CPU 번호와 함께 경고합니다. 정의되지 않은 IPI를 정상 요청으로 간주하지 않습니다.
break;알 수 없는 IPI의 경고를 끝낸 뒤 공통 반환으로 이동합니다.
return IRQ_HANDLED;수신 IRQ를 이 IPI 처리기가 처리했음을 IRQ 코어에 반환합니다. 종류별 처리 결과를 사용자 공간으로 전달하는 반환값은 아닙니다.
void riscv_ipi_enable(void)현재 hart에서 등록된 IPI IRQ들을 사용할 수 있게 하는 함수입니다. CPU 기동 시 원격 요청을 받을 준비에 사용합니다.
int i;등록된 IPI 종류들을 차례로 활성화할 반복 인덱스입니다. CPU 번호가 아니라 IPI 영역 안의 종류 번호를 셉니다.
if (WARN_ON_ONCE(!ipi_virq_base))IPI IRQ 영역이 아직 등록되지 않았다면 한 번 경고하고 활성화를 중단합니다. 시작 번호가 없는 상태에서 엉뚱한 IRQ를 켜지 않도록 합니다.
return;IPI 영역이 없어 활성화할 수 없으므로 아무 IRQ도 건드리지 않고 반환합니다.
for (i = 0; i < nr_ipi; i++)등록된 종류 수만큼 반복하면서 현재 hart의 각 IPI IRQ를 켭니다. 영역의 시작 번호에 i를 더해 실제 IRQ를 얻습니다.
enable_percpu_irq(ipi_virq_base + i, 0);현재 CPU에서 각 IPI용 per-CPU IRQ를 활성화합니다. CPU 기동 시 다른 CPU의 재스케줄·함수 실행 요청 등을 받을 준비를 합니다.
void riscv_ipi_disable(void)현재 hart에서 등록된 IPI IRQ들을 끄는 함수입니다. CPU를 오프라인으로 내릴 때 원격 요청 수신을 중단합니다.
int i;비활성화할 IPI 종류를 하나씩 가리키는 반복 인덱스입니다.
if (WARN_ON_ONCE(!ipi_virq_base))IPI IRQ 영역이 없는 비정상 순서라면 경고하고 중단합니다. 초기화되지 않은 번호로 IRQ 해제 동작을 수행하지 않게 합니다.
return;등록되지 않은 IPI 영역은 비활성화할 수 없으므로 바로 반환합니다.
for (i = 0; i < nr_ipi; i++)각 IPI 종류에 대응하는 현재 hart의 per-CPU IRQ를 하나씩 비활성화합니다.
disable_percpu_irq(ipi_virq_base + i);현재 CPU에서 각 IPI용 per-CPU IRQ를 비활성화합니다. CPU 종료 과정에서 더 이상 해당 IPI 처리로 진입하지 않게 합니다.
bool riscv_ipi_have_virq_range(void)IPI용 Linux IRQ 영역이 등록됐는지를 다른 초기화 코드에 알려주는 조회 함수입니다.
return (ipi_virq_base) ? true : false;IPI 영역 시작 번호가 0이 아니면 등록된 것으로 보고 참을 반환합니다. 아직 등록 전의 0이면 거짓입니다.
void riscv_ipi_set_virq_range(int virq, int nr)IPI IRQ 영역의 시작 번호와 개수를 등록하고 각 종류의 per-CPU 처리기를 설치합니다. 등록 후 부트 CPU의 IPI를 즉시 활성화합니다.
05 · WORKED EXAMPLE
숫자로 검산하기
동기식 8-CPU call-function의 tail 예시
7개 target에 callback을 보내며 6개는 3us 안에 처리하지만 한 CPU가 700us 동안 IRQ를 막는다고 가정한다.
- publicationsender가 대상 7개의 CSD를 각 CPU 큐에 넣고, 큐가 비어 있던 CPU만 IPI mask에 포함한다.
- parallelism빠른 6개 target의 완료는 전체 대기 시간을 18us로 합산시키지 않는다. 병렬 처리이므로 가장 느린 target이 지배한다.
- tail마지막 CPU가 700us 내내 IRQ를 막고 이어 callback 자체가 3us 걸리면, 추가 doorbell·entry 시간을 빼도 대기는 약 703us 이상이다.
- deadlocksender가 target이 잡으려는 lock을 쥔 채 wait하면 703us가 아니라 무한 대기가 된다.
결론703us는 이 가정에서의 하한에 가까운 값이다. 유한한 상한을 주장하려면 가장 느린 대상의 IRQ 차단, doorbell·handler·callback 지연과 lock 의존성의 상한이 모두 필요하다.
06 · DEEP DIVE
주요 동작 상세 분석
공통 커널 코드와 아키텍처별 함수의 역할 구분
IPI hardware는 vector 또는 doorbell을 전달한다. callback 함수와 인자는 kernel/smp.c의 대상 CPU별 call_single_queue에 놓인다. llist_add()가 빈 큐에 첫 노드를 넣으면 IPI를 보내고, 수신 CPU가 큐를 분리해 callback을 실행한다.
CSD 큐에 게시, 가상 IPI reason 표시, 물리 doorbell, callback 실행, csd_unlock() 완료를 각각 구분한다. 동기 smp_call_function_many(..., wait=true)는 각 대상 CSD의 unlock을 기다리므로 IRQ 비활성 상태나 의존 lock을 잡은 상태를 피해야 한다.
arm64: GIC SGI와 logical CPU affinity encoding
sender가 target affinity를 ICC_SGI1R_EL1에 써 SGI를 보내고 do_handle_IPI()가 IPI_RESCHEDULE, CALL_FUNC, CPU_STOP, TIMER 등 reason을 분기한다. payload queue는 cache-coherent memory에 있다.
queue write가 SGI MMIO/system-register write보다 먼저 보이도록 barrier가 필요하고 receiver는 interrupt entry 뒤 queue를 읽는다. 디버깅할 때는 SGI INTID, target MPIDR, ipi reason counter, call_single queue와 completion cpumask를 확인한다.
x86-64: local APIC ICR 또는 x2APIC MSR vector delivery
sender는 destination APIC ID와 vector를 ICR에 기록한다. target의 system-vector IDT handler가 call-function 또는 reschedule 코드를 실행하고 per-CPU queue를 비운다.
ICR delivery status, queue store ordering과 APIC EOI가 맞아야 다음 IPI가 정상 전달된다. NMI-based stop은 일반 maskable IPI와 별도 경계다. 디버깅할 때는 APIC ID, ICR destination/vector, vector statistics, target irq-off duration과 call queue head를 본다.
RISC-V: SBI send_ipi, supervisor software interrupt 또는 IMSIC IPI
arch/riscv/kernel/smp.c는 reason별 ipi_desc로 가상 IPI를 보낸다. IPI-Mux가 대상 CPU의 bits를 설정한 뒤 SBI는 SSIP를, IMSIC은 IPI EIID를 울린다. SBI handler는 SSIP를 지우고, IMSIC handler는 TOPEI를 읽어 acknowledge한다. 두 경로 모두 ipi_mux_process()로 reason별 Linux IRQ를 처리한다.
ipi_mux_send_mask()의 atomic_fetch_or_release()가 reason 비트를 게시하고, 수신 측 ipi_mux_process()의 atomic_fetch_andnot()가 활성 비트만 소비한다. 이미 pending인 비트에는 중복 물리 IPI를 보내지 않는다. 디버깅할 때는 CPU→hart ID, ipi_mux_cpu.bits, SBI 경로의 SIP.SSIP, IMSIC 경로의 TOPEI EIID, 대상 CSD unlock을 경로별로 확인한다.
객체를 사용하고 해제하는 주체와 시점을 확인한다
동기 호출의 CSD와 인자는 대상 callback 뒤 csd_unlock()이 실행될 때까지 살아 있어야 한다. 비동기 CSD_TYPE_ASYNC 경로는 func/info를 로컬 변수로 복사하고 csd_unlock()을 한 뒤 콜백을 실행한다. 따라서 CSD 재사용 가능 시점과 콜백 완료는 다르며, info가 가리키는 데이터의 수명은 별도로 동기화해야 한다.
주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.
최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다
Tipi = Tqueue + Tdoorbell + Tinterrupt_off_target + Thandler + Tcallback. target이 IRQ를 오래 막으면 sender의 synchronous wait도 같은 시간만큼 늘어난다.
평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.
07 · FAILURE
실패를 어떤 증거로 나눌 것인가
| 분류 | 관찰되는 결과 | 첫 확인값 |
|---|---|---|
| arm64 | MPIDR affinity encoding이나 cpu logical map이 틀리면 다른 PE에 SGI가 가거나 target이 영원히 응답하지 않는다. | SGI INTID, target MPIDR, ipi reason counter, call_single queue와 completion cpumask를 확인한다. |
| x86-64 | offline CPU나 stale APIC ID를 target으로 삼으면 sender가 completion을 기다리며 멈춘다. vector 충돌은 잘못된 system handler 호출로 이어진다. | APIC ID, ICR destination/vector, vector statistics, target irq-off duration과 call queue head를 본다. |
| RISC-V | SBI 경로의 CPU→hart ID 오류 또는 SSIP 처리 누락, IMSIC 경로의 IPI EIID/TOPEI 처리 오류, 가상 IPI 비트 소비 누락은 callback 대기를 멈추게 할 수 있다. | CPU→hart ID, ipi_mux_cpu.bits, SBI 경로의 SIP.SSIP, IMSIC 경로의 TOPEI EIID, 대상 CSD unlock을 경로별로 확인한다. |
08 · LAB
재현과 계측 절차
- 각 CPU에 의도적으로 다른 irq-off 시간을 넣고
smp_call_function_many()completion 분포를 측정한다. - CPU hotplug와 동시에 IPI를 보내 logical mask에서 offline CPU가 제거되는 지점을 추적한다.
- 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
- 소스만 읽고 끝내지 않고 최종
vmlinux의objdump -dr,readelf -SW결과로 선택된 alternative와 section 배치를 확인한다.
09 · REFERENCES
원문 위치
- arm64arch/arm64/kernel/smp.c:950-1040
- x86-64arch/x86/kernel/smp.c:249-299
- RISC-V reasonarch/riscv/kernel/smp.c:106-218
- 가상 IPI-Muxkernel/irq/ipi-mux.c:51-160
- SBI 수신arch/riscv/kernel/sbi-ipi.c:18-75
- IMSIC 수신irq-riscv-imsic-early.c:36-133
- CSD 완료kernel/smp.c:351-595
Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.