01 · QUESTION
무엇을 확인할 것인가
hardware pending bit가 Linux IRQ number와 irq_desc로 변환되고, level interrupt가 다시 들어오지 않도록 mask와 EOI를 어떤 순서로 다루는가?
irqdomain은 hardware interrupt number를 Linux virtual IRQ에 매핑한다. 각 irqchip은 필요한 mask·unmask·ack·eoi·affinity callback을 선택해 구현하고, flow handler는 edge/level 설정에 맞춰 irq_desc와 device action chain을 실행한다. ack와 eoi가 호출되는 시점은 chip·flow handler마다 다르다.
controller acknowledge로 active state를 얻는 구간, generic_handle_domain_irq가 driver handler를 호출하는 구간, EOI/deactivate 구간을 분리한다. level source는 device status를 먼저 지우지 않으면 즉시 재assert된다.
02 · CONTRACT
공통 규약과 아키텍처별 구현
| architecture | 핵심 mechanism | 실패 형태 | 확인할 상태 |
|---|---|---|---|
| arm64 | GICv3 distributor, redistributor와 ICC system register interface | spurious INTID를 정상 IRQ로 처리하거나 level source를 clear하기 전에 deactivate하면 같은 interrupt가 연속 재진입한다. | INTID, group/priority, redistributor affinity, pending/active bit, irq_desc depth와 device status를 같은 timestamp로 남긴다. |
| x86-64 | local APIC vector, IOAPIC redirection과 IDT/FRED dispatch | vector를 재사용하기 전에 이전 CPU의 in-flight interrupt를 drain하지 않으면 다른 device irq_desc를 호출할 수 있다. | APIC mode xAPIC/x2APIC, vector, destination, IOAPIC RTE, remote IRR, IRTE와 irq migration status를 본다. |
| RISC-V | S-mode local interrupt와 external APLIC/PLIC/IMSIC domain | SCAUSE code를 Linux hwirq로 잘못 변환하거나 PLIC complete를 빼면 해당 source가 막히거나 계속 pending 상태로 남는다. | hart ID·SCAUSE·SIE/SIP와 parent domain을 확인하고, PLIC이면 claim ID·threshold, APLIC direct이면 CLAIMI, IMSIC이면 TOPEI EIID를 별도로 기록한다. |
03 · DIAGRAMS
세 그림으로 먼저 읽기
arm64
- mechanism
- GICv3 distributor, redistributor와 ICC system register interface
- state
- CPU는 ICC_IAR1_EL1에서 INTID를 acknowledge하고 irqdomain을 통해 irq_desc로 보낸다. SPI는 distributor, PPI/SGI는 redistributor의 per-CPU 상태를 사용하며 EOImode에 따라 priority drop과 deactivate가 나뉜다.
- checkpoint
- INTID, group/priority, redistributor affinity, pending/active bit, irq_desc depth와 device status를 같은 timestamp로 남긴다.
x86-64
- mechanism
- local APIC vector, IOAPIC redirection과 IDT/FRED dispatch
- state
- IOAPIC pin 또는 MSI message가 destination APIC ID와 vector를 선택한다. local APIC가 vector를 accept하면 IDT 경로는 vector stub으로, FRED 경로는
fred_extint()로 진입한다. 두 경로의 일반 device IRQ는common_interrupt()로 모여 per-CPUvector_irqmapping에서 irq_desc를 찾는다. - checkpoint
- APIC mode xAPIC/x2APIC, vector, destination, IOAPIC RTE, remote IRR, IRTE와 irq migration status를 본다.
RISC-V
- mechanism
- S-mode local interrupt와 external APLIC/PLIC/IMSIC domain
- state
riscv_intc는 SCAUSE의 local cause를 per-hart domain으로 넘긴다. external device IRQ는 PLIC의 claim/complete, APLIC direct의 CLAIMI, 또는 APLIC MSI→IMSIC의 TOPEI EIID 중 실제 controller 경로에서 해석된다. APLIC direct는 IMSIC을 필수로 거치지 않는다.- checkpoint
- hart ID·SCAUSE·SIE/SIP와 parent domain을 확인하고, PLIC이면 claim ID·threshold, APLIC direct이면 CLAIMI, IMSIC이면 TOPEI EIID를 별도로 기록한다.
gic_complete_ack()의 이른 EOIR(priority drop)→handler→DIR(deactivate) 순서다. 단 ARM64 erratum 2941627 우회 시 deactivate에 GICD_ICACTIVER를 쓰고, mode 1의 LPI·vCPU forwarding에는 DIR을 생략한다.irq_ack에서 complete한다. APLIC direct는 CLAIMI 읽기가 pending을 지우며, IMSIC은 TOPEI로 claim/ack한다. 후자 두 경로에 PLIC complete/threshold를 적용하지 않는다.generic_handle_domain_irq()와 device handler→보통 ICC_EOIR1_EL1로 priority drop·deactivate; erratum 우회 시 GICD_ICACTIVER도 사용ICC_EOIR1_EL1 priority drop→device handler 실행→보통 ICC_DIR_EL1, erratum 우회 시 GICD_ICACTIVER로 deactivate; LPI·forwarded는 생략handle_fasteoi_irq()와 device handler→plic_irq_eoi()가 ID를 claim register에 써 completehandle_edge_irq()의 irq_ack에서 complete→device handler; level 경로와 완료 시점이 다름CLAIMI 읽기가 pending을 지움→domain handler 실행→APLIC irqchip의 irq_eoi는 빈 callbackCSR_TOPEI 읽기/0 쓰기로 EIID claim·ack→domain handler 실행→PLIC claim register complete 없음04 · SOURCE
Linux 6.18.37 원본 코드와 주요 구문 해설
소스 위치를 고정된 숫자로 복사하지 않고 Linux v6.18.37 tree에서 함수 선언을 다시 찾아 발췌했습니다. 원문 전체 발췌를 보존하고, 개별 해설은 근거가 있는 구문에만 붙였습니다.
arm64 · Linux 6.18.37
GICv3 distributor, redistributor와 ICC system register interface
CPU는 ICC_IAR1_EL1에서 INTID를 acknowledge하고 irqdomain을 통해 irq_desc로 보낸다. SPI는 distributor, PPI/SGI는 redistributor의 per-CPU 상태를 사용하며 EOImode에 따라 priority drop과 deactivate가 나뉜다.
원본 코드: drivers/irqchip/irq-gic-v3.c:956-1028
956 gic_pmr_mask_irqs();
957 isb();
958 irqnr = gic_read_iar();
959 gic_write_pmr(pmr);
960
961 __gic_handle_nmi(irqnr, regs);
962}
963
964static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
965{
966 if (unlikely(gic_supports_nmi() && !interrupts_enabled(regs)))
967 __gic_handle_irq_from_irqsoff(regs);
968 else
969 __gic_handle_irq_from_irqson(regs);
970}
971
972static void __init gic_dist_init(void)
973{
974 unsigned int i;
975 u64 affinity;
976 void __iomem *base = gic_data.dist_base;
977 u32 val;
978
979 /* Disable the distributor */
980 writel_relaxed(0, base + GICD_CTLR);
981 gic_dist_wait_for_rwp();
982
983 /*
984 * Configure SPIs as non-secure Group-1. This will only matter
985 * if the GIC only has a single security state. This will not
986 * do the right thing if the kernel is running in secure mode,
987 * but that's not the intended use case anyway.
988 */
989 for (i = 32; i < GIC_LINE_NR; i += 32)
990 writel_relaxed(~0, base + GICD_IGROUPR + i / 8);
991
992 /* Extended SPI range, not handled by the GICv2/GICv3 common code */
993 for (i = 0; i < GIC_ESPI_NR; i += 32) {
994 writel_relaxed(~0U, base + GICD_ICENABLERnE + i / 8);
995 writel_relaxed(~0U, base + GICD_ICACTIVERnE + i / 8);
996 }
997
998 for (i = 0; i < GIC_ESPI_NR; i += 32)
999 writel_relaxed(~0U, base + GICD_IGROUPRnE + i / 8);
1000
1001 for (i = 0; i < GIC_ESPI_NR; i += 16)
1002 writel_relaxed(0, base + GICD_ICFGRnE + i / 4);
1003
1004 for (i = 0; i < GIC_ESPI_NR; i += 4)
1005 writel_relaxed(REPEAT_BYTE_U32(dist_prio_irq),
1006 base + GICD_IPRIORITYRnE + i);
1007
1008 /* Now do the common stuff */
1009 gic_dist_config(base, GIC_LINE_NR, dist_prio_irq);
1010
1011 val = GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A | GICD_CTLR_ENABLE_G1;
1012 if (gic_data.rdists.gicd_typer2 & GICD_TYPER2_nASSGIcap) {
1013 pr_info("Enabling SGIs without active state\n");
1014 val |= GICD_CTLR_nASSGIreq;
1015 }
1016
1017 /* Enable distributor with ARE, Group1, and wait for it to drain */
1018 writel_relaxed(val, base + GICD_CTLR);
1019 gic_dist_wait_for_rwp();
1020
1021 /*
1022 * Set all global interrupts to the boot CPU only. ARE must be
1023 * enabled.
1024 */
1025 affinity = gic_cpu_to_affinity(smp_processor_id());
1026 for (i = 32; i < GIC_LINE_NR; i++)
1027 gic_write_irouter(affinity, base + GICD_IROUTER + i * 8);
1028 주요 구문 해설
원본 73줄은 그대로 표시하며, 검토한 39개 구문에 설명을 붙였습니다.
gic_pmr_mask_irqs();PMR 임계값으로 일반 IRQ를 막고 NMI 우선순위만 인정되게 합니다. IRQ가 금지된 문맥의 NMI 처리에서 일반 IRQ를 잘못 acknowledge하지 않도록 IAR 읽기 전에 제한합니다.
isb();앞서 바꾼 PMR 우선순위 마스크가 적용된 뒤 IAR을 읽도록 명령 실행을 동기화합니다. NMI 문맥에서 일반 IRQ를 잘못 승인하지 않게 하는 순서입니다.
irqnr = gic_read_iar();CPU interface에서 가장 높은 우선순위의 pending INTID를 acknowledge하며 active 상태로 전환한다.
gic_write_pmr(pmr);IAR에서 인터럽트 번호를 읽은 뒤 원래 PMR 값을 되돌립니다. 호출자가 유지하던 인터럽트 마스크 상태를 바꾸지 않고 NMI 처리를 계속합니다.
__gic_handle_nmi(irqnr, regs);읽어 온 인터럽트 번호와 예외 레지스터를 NMI 처리 경로에 넘깁니다. 앞에서 일반 IRQ를 제외한 상태로 acknowledge한 결과를 사용하는 단계입니다.
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)arm64 예외 진입 코드가 호출하는 GIC의 최상위 IRQ 처리기입니다. 중단된 레지스터 상태를 보고 일반 IRQ 진입과 IRQ-off NMI 진입을 구분합니다.
if (unlikely(gic_supports_nmi() && !interrupts_enabled(regs)))GIC가 NMI를 지원하면서 중단된 문맥의 IRQ가 꺼져 있었다면 NMI 전용 경로를 선택합니다. 일반 IRQ를 허용하던 문맥과 PMR·NMI 진입 처리 방식이 다르기 때문입니다.
__gic_handle_irq_from_irqsoff(regs);NMI를 지원하고 중단된 문맥의 IRQ가 꺼져 있으면 IRQ-off 전용 진입 경로를 사용합니다. 일반 IRQ를 NMI 문맥에서 처리하지 않도록 PMR 관리가 달라집니다.
elseNMI 전용 IRQ-off 조건이 아니면 일반 GIC 진입 함수로 진행합니다. 그 함수가 읽은 요청의 우선순위를 보고 필요한 처리를 수행합니다.
__gic_handle_irq_from_irqson(regs);NMI 전용 IRQ-off 경로에 해당하지 않으면 일반 GIC 진입 경로로 들어갑니다. 이 함수가 IAR에서 요청 번호를 읽고, NMI 지원 시스템에서는 우선순위도 확인하여 일반 IRQ와 NMI 처리를 구분합니다.
static void __init gic_dist_init(void)부팅 중 GIC distributor의 공유 인터럽트 상태를 초기화합니다. 분배기를 끄고 그룹·마스크·우선순위를 정한 뒤 다시 켜고 초기 라우팅 CPU를 지정합니다.
unsigned int i;초기화할 SPI 또는 확장 SPI 번호를 순회하는 인덱스입니다. 레지스터 하나가 여러 IRQ 설정을 담으므로 각 반복문의 증가 폭이 다릅니다.
u64 affinity;SPI의 초기 전달 CPU를 나타낼 64비트 GIC affinity 값을 보관합니다. CPU의 논리 번호를 그대로 IROUTER에 쓰는 것은 아닙니다.
void __iomem *base = gic_data.dist_base;매핑된 distributor 레지스터 영역의 시작 주소를 가져옵니다. __iomem 표시는 일반 RAM이 아닌 장치 레지스터 접근용 주소임을 나타냅니다.
u32 val;분배기를 다시 켤 때 GICD_CTLR에 쓸 32비트 제어값입니다. affinity routing·Group 1·선택적 SGI 동작 비트를 조합합니다.
writel_relaxed(0, base + GICD_CTLR);GIC distributor 제어 레지스터를 0으로 써 분배기를 끕니다. 그룹·활성 상태·우선순위를 바꾸는 동안 중간 설정으로 인터럽트를 전달하지 않게 합니다.
gic_dist_wait_for_rwp();분배기 레지스터 쓰기 처리 중 비트가 내려갈 때까지 기다립니다. 끄기 요청이 실제로 반영된 뒤 다음 초기화를 진행합니다.
for (i = 32; i < GIC_LINE_NR; i += 32)CPU별 SGI·PPI를 제외한 SPI 번호 32부터 32개씩 처리합니다. IGROUPR 한 레지스터의 32비트가 IRQ 32개의 그룹 선택을 담습니다.
writel_relaxed(~0, base + GICD_IGROUPR + i / 8);SPI 32개씩의 그룹 비트를 모두 1로 설정하여 비보안 Group 1에 둡니다. i/8은 인터럽트 번호를 32비트 그룹 레지스터의 바이트 오프셋으로 바꾼 값입니다.
for (i = 0; i < GIC_ESPI_NR; i += 32) {확장 SPI를 32개씩 순회하며 enable과 active 상태를 초기화합니다. 두 레지스터 모두 IRQ당 1비트를 사용합니다.
writel_relaxed(~0U, base + GICD_ICENABLERnE + i / 8);확장 SPI 32개 단위로 enable-clear 비트를 써 모두 비활성화합니다. 초기화 전에 남아 있을 수 있는 활성화 설정을 없앱니다.
writel_relaxed(~0U, base + GICD_ICACTIVERnE + i / 8);확장 SPI의 active-clear 비트를 써 처리 중 상태를 해제합니다. 이전 실행 환경에서 남은 active 상태가 초기 커널의 전달을 막지 않게 합니다.
for (i = 0; i < GIC_ESPI_NR; i += 32)확장 SPI의 그룹 선택을 32개씩 설정합니다. 한 번의 32비트 쓰기로 해당 범위를 비보안 Group 1로 만듭니다.
writel_relaxed(~0U, base + GICD_IGROUPRnE + i / 8);확장 SPI를 비보안 Group 1로 지정합니다. 일반 SPI를 처리하는 공통 코드 범위 밖이어서 별도로 초기화합니다.
for (i = 0; i < GIC_ESPI_NR; i += 16)확장 SPI의 트리거 형식을 16개씩 초기화합니다. ICFGR은 IRQ마다 2비트를 사용하므로 32비트 쓰기 하나에 16개가 들어갑니다.
writel_relaxed(0, base + GICD_ICFGRnE + i / 4);확장 SPI의 trigger 설정을 0으로 만들어 level-triggered 기본값으로 둡니다. 각 인터럽트가 차지하는 설정 비트 수에 맞춰 i/4 오프셋을 사용합니다.
for (i = 0; i < GIC_ESPI_NR; i += 4)확장 SPI 우선순위를 4개씩 설정합니다. IRQ당 우선순위 1바이트가 필요해 32비트 레지스터 하나에 네 IRQ가 들어갑니다.
writel_relaxed(REPEAT_BYTE_U32(dist_prio_irq),기본 우선순위 바이트를 네 번 반복한 32비트 값을 만들어 네 확장 SPI에 한 번에 씁니다.
base + GICD_IPRIORITYRnE + i);이번 네 IRQ의 확장 우선순위 레지스터 주소를 지정합니다. i가 IRQ당 한 바이트인 우선순위 배열의 바이트 오프셋이 됩니다.
gic_dist_config(base, GIC_LINE_NR, dist_prio_irq);일반 SPI 범위의 trigger·우선순위·활성 상태를 공통 GIC 초기화 함수로 설정합니다. 확장 SPI에 대해 앞에서 한 준비를 기본 SPI 범위에도 적용합니다.
val = GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A | GICD_CTLR_ENABLE_G1;비보안 affinity routing과 Group 1 전달을 활성화하는 제어 비트를 모읍니다. 뒤의 한 번의 CTLR 쓰기로 준비한 초기 설정을 사용하기 시작합니다.
if (gic_data.rdists.gicd_typer2 & GICD_TYPER2_nASSGIcap) {GIC가 active 상태 없이 SGI를 처리하는 기능을 지원하는지 확인합니다. 지원할 때만 nASSGI 요청 비트를 넣고 그렇지 않으면 기존 SGI 방식으로 둡니다.
pr_info("Enabling SGIs without active state\n");하드웨어가 SGI의 active state 생략 기능을 지원하여 사용할 예정임을 로그로 알립니다. 실제 요청 비트는 다음 줄에서 제어값에 더합니다.
val |= GICD_CTLR_nASSGIreq;SGI에 active 상태를 두지 않는 동작을 요청하는 비트를 제어값에 추가합니다. 아직 레지스터에 쓰는 줄은 아니며 최종 CTLR 값의 일부를 만듭니다.
writel_relaxed(val, base + GICD_CTLR);비보안 affinity routing과 Group 1 전달을 켜는 값을 GICD_CTLR에 기록합니다. 필요한 초기 상태를 만든 뒤 distributor를 다시 활성화합니다.
gic_dist_wait_for_rwp();distributor 활성화 설정의 레지스터 쓰기가 끝날 때까지 기다립니다. affinity routing을 전제로 하는 다음 대상 CPU 설정보다 먼저 완료되어야 합니다.
affinity = gic_cpu_to_affinity(smp_processor_id());현재 부트 CPU의 논리 번호를 GIC 라우팅용 affinity 값으로 바꿉니다. 이후 모든 SPI의 초기 전달 대상을 이 CPU로 지정합니다.
for (i = 32; i < GIC_LINE_NR; i++)각 SPI마다 라우팅 레지스터 하나를 설정합니다. 처음에는 모든 SPI가 부트 CPU로 전달되도록 하며, 이후 IRQ affinity 설정이 대상을 바꿀 수 있습니다.
gic_write_irouter(affinity, base + GICD_IROUTER + i * 8);각 SPI의 IROUTER에 부트 CPU affinity를 기록합니다. 다른 CPU와 드라이버가 준비되기 전 전역 인터럽트가 도착할 기본 CPU를 정합니다.
x86-64 · Linux 6.18.37
local APIC vector, IOAPIC redirection과 IDT/FRED dispatch
IOAPIC pin 또는 MSI message가 destination APIC ID와 vector를 선택한다. local APIC가 vector를 accept하면 IDT 경로는 vector stub으로, FRED 경로는 fred_extint()로 진입한다. 두 경로의 일반 device IRQ는 common_interrupt()로 모여 per-CPU vector_irq mapping에서 irq_desc를 찾는다.
바로 아래 apic.c:1347-1409 발췌는 APIC interrupt mode 초기화 코드다. 실제 device IRQ의 per-CPU vector_irq 조회·dispatch는 arch/x86/kernel/irq.c:248-320에서 확인한다.
원본 코드: arch/x86/kernel/apic/apic.c:1347-1409
1347 if (apic_extnmi == APIC_EXTNMI_NONE)
1348 value |= APIC_LVT_MASKED;
1349 apic_write(APIC_LVT1, value);
1350}
1351
1352static void __init apic_bsp_setup(bool upmode);
1353
1354/* Init the interrupt delivery mode for the BSP */
1355void __init apic_intr_mode_init(void)
1356{
1357 bool upmode = IS_ENABLED(CONFIG_UP_LATE_INIT);
1358
1359 switch (apic_intr_mode) {
1360 case APIC_PIC:
1361 pr_info("APIC: Keep in PIC mode(8259)\n");
1362 return;
1363 case APIC_VIRTUAL_WIRE:
1364 pr_info("APIC: Switch to virtual wire mode setup\n");
1365 break;
1366 case APIC_VIRTUAL_WIRE_NO_CONFIG:
1367 pr_info("APIC: Switch to virtual wire mode setup with no configuration\n");
1368 upmode = true;
1369 break;
1370 case APIC_SYMMETRIC_IO:
1371 pr_info("APIC: Switch to symmetric I/O mode setup\n");
1372 break;
1373 case APIC_SYMMETRIC_IO_NO_ROUTING:
1374 pr_info("APIC: Switch to symmetric I/O mode setup in no SMP routine\n");
1375 break;
1376 }
1377
1378 x86_64_probe_apic();
1379
1380 if (x86_platform.apic_post_init)
1381 x86_platform.apic_post_init();
1382
1383 apic_bsp_setup(upmode);
1384}
1385
1386static void lapic_setup_esr(void)
1387{
1388 unsigned int oldvalue, value, maxlvt;
1389
1390 if (!lapic_is_integrated()) {
1391 pr_info("No ESR for 82489DX.\n");
1392 return;
1393 }
1394
1395 if (apic->disable_esr) {
1396 /*
1397 * Something untraceable is creating bad interrupts on
1398 * secondary quads ... for the moment, just leave the
1399 * ESR disabled - we can't do anything useful with the
1400 * errors anyway - mbligh
1401 */
1402 pr_info("Leaving ESR disabled.\n");
1403 return;
1404 }
1405
1406 maxlvt = lapic_get_maxlvt();
1407 if (maxlvt > 3) /* Due to the Pentium erratum 3AP. */
1408 apic_write(APIC_ESR, 0);
1409 oldvalue = apic_read(APIC_ESR);주요 구문 해설
원본 63줄은 그대로 표시하며, 검토한 39개 구문에 설명을 붙였습니다.
if (apic_extnmi == APIC_EXTNMI_NONE)외부 NMI를 받지 않는 부팅 정책이 선택됐는지 확인합니다. 이 경우 LINT1을 NMI 전달 모드로 구성하더라도 입력을 마스크합니다.
value |= APIC_LVT_MASKED;LINT1 제어값에 마스크 비트를 추가해 외부 NMI 입력을 차단합니다. 다른 로컬 APIC 인터럽트를 모두 막는 설정은 아닙니다.
apic_write(APIC_LVT1, value);LINT1의 NMI 전달 모드와 필요 시 level-trigger·mask 비트를 로컬 APIC에 기록합니다. virtual wire 설정에서 외부 NMI 입력을 플랫폼 조건과 부팅 정책에 맞춥니다.
static void __init apic_bsp_setup(bool upmode);부트 CPU의 APIC 설정 함수 원형을 미리 선언합니다. upmode는 단일 CPU용 설정 여부를 전달하는 인자이며 이 줄은 함수 실행이 아닙니다.
void __init apic_intr_mode_init(void)부팅 때 선택된 PIC·virtual wire·symmetric I/O 방식에 맞춰 APIC를 초기화하는 함수입니다. 모드별 예외를 정한 뒤 공통 부트 CPU 설정으로 이어집니다.
bool upmode = IS_ENABLED(CONFIG_UP_LATE_INIT);단일 CPU 커널의 늦은 APIC 초기화 설정이 켜져 있으면 upmode를 참으로 시작합니다. 뒤의 apic_bsp_setup은 이 값에 따라 부트 CPU만 사용하는 APIC 준비를 추가하며, 구성 정보가 없는 virtual wire 경로에서도 이 값을 참으로 바꿉니다.
switch (apic_intr_mode) {앞서 결정한 인터럽트 전달 방식에 따라 APIC 설정을 계속할지와 단일 CPU 준비가 필요한지를 선택합니다.
case APIC_PIC:기존 8259 PIC만 사용하는 모드입니다. APIC 모드로 전환하지 않고 로그 후 반환합니다.
pr_info("APIC: Keep in PIC mode(8259)\n");선택된 인터럽트 방식이 기존 8259 PIC임을 알립니다. 바로 반환하므로 아래의 APIC 설정으로 전환하지 않습니다.
return;PIC 방식 유지를 선택했으므로 APIC 드라이버 탐지와 부트 APIC 설정을 건너뜁니다.
case APIC_VIRTUAL_WIRE:기존 PIC의 인터럽트를 로컬 APIC의 ExtINT 입력으로 받는 virtual wire 모드를 선택합니다.
pr_info("APIC: Switch to virtual wire mode setup\n");기존 PIC의 인터럽트를 로컬 APIC를 통해 받는 virtual wire 설정을 선택했음을 알립니다. 실제 설정은 switch 뒤의 공통 APIC 초기화에서 진행합니다.
break;virtual wire 선택 로그를 마친 뒤 switch 밖의 공통 APIC 준비로 진행합니다.
case APIC_VIRTUAL_WIRE_NO_CONFIG:APIC 구성 정보를 얻지 못한 virtual wire 모드입니다. 단일 CPU 기준으로 부트 APIC를 준비해야 합니다.
pr_info("APIC: Switch to virtual wire mode setup with no configuration\n");정상적인 APIC 구성 정보가 없는 virtual wire 경로임을 알립니다. 이어서 upmode를 켜 단일 CPU에 맞는 초기화를 선택합니다.
upmode = true;구성 정보가 없는 경로에서는 upmode를 켭니다. 뒤의 부트 CPU APIC 설정이 단일 CPU 초기화를 포함하도록 합니다.
break;구성 정보 없는 virtual wire의 추가 결정을 마치고 공통 APIC 초기화로 이어갑니다.
case APIC_SYMMETRIC_IO:I/O APIC가 장치 인터럽트를 CPU의 로컬 APIC로 분배하는 symmetric I/O 모드입니다.
pr_info("APIC: Switch to symmetric I/O mode setup\n");I/O APIC를 사용하는 symmetric I/O 방식으로 초기화할 예정임을 로그에 남깁니다.
break;symmetric I/O 모드 선택을 끝내고 공통 APIC 설정으로 진행합니다.
case APIC_SYMMETRIC_IO_NO_ROUTING:대칭 I/O 모드는 사용하되 SMP 라우팅 구성을 적용하지 않는 경우입니다. 이 구분을 로그에 남긴 뒤 APIC 준비를 계속합니다.
pr_info("APIC: Switch to symmetric I/O mode setup in no SMP routine\n");SMP 라우팅 구성을 사용하지 않는 symmetric I/O 경로를 선택했음을 알립니다. 로그 출력 자체가 인터럽트 라우팅을 프로그래밍하지는 않습니다.
break;라우팅 없는 symmetric I/O 분기를 마치고 공통 APIC 초기화로 이동합니다.
x86_64_probe_apic();64비트 x86 환경에 맞는 APIC 드라이버를 탐지·선택합니다. 이후 공통 APIC 호출이 실제 시스템의 APIC 방식으로 연결되도록 합니다.
if (x86_platform.apic_post_init)플랫폼 전용 APIC 후처리 함수가 등록되어 있을 때만 호출합니다. NULL이면 추가 동작 없이 부트 CPU 설정으로 진행합니다.
x86_platform.apic_post_init();플랫폼이 등록한 APIC 후처리 콜백이 있으면 실행합니다. 일반 APIC 선택만으로 처리되지 않는 플랫폼별 초기 설정을 삽입하는 지점입니다.
apic_bsp_setup(upmode);부트 CPU의 로컬 APIC와 I/O APIC를 준비하고 오류 인터럽트·레거시 IRQ 벡터 설정을 마무리합니다. upmode가 참이면 단일 CPU 환경에 필요한 부트 APIC 설정도 먼저 수행합니다.
static void lapic_setup_esr(void)로컬 APIC의 오류 상태 레지스터 ESR과 오류 인터럽트를 준비하는 함수입니다. 오래된 APIC나 ESR 사용이 금지된 플랫폼은 먼저 제외합니다.
unsigned int oldvalue, value, maxlvt;ESR 설정 전후의 오류 상태와 APIC의 최대 LVT 번호를 보관합니다. 하드웨어 조건을 판단하고 초기 오류를 진단하는 데 쓰입니다.
if (!lapic_is_integrated()) {CPU 통합형 APIC인지 확인합니다. 통합형이 아닌 82489DX에서는 지원하지 않는 ESR 초기화를 시도하지 않고 반환합니다.
pr_info("No ESR for 82489DX.\n");외장 82489DX APIC에는 ESR 처리를 적용하지 않음을 알립니다. 다음 줄에서 반환하여 지원하지 않는 오류 상태 레지스터를 접근하지 않습니다.
return;ESR을 제공하지 않는 APIC이므로 오류 상태 레지스터 설정 없이 반환합니다.
if (apic->disable_esr) {선택된 APIC 드라이버가 ESR을 끄도록 요구하는지 확인합니다. 플랫폼 문제를 피해야 하는 경우 이후 ESR 접근을 생략합니다.
pr_info("Leaving ESR disabled.\n");선택된 APIC 드라이버가 ESR 비활성화를 요구하므로 그대로 둔다는 로그입니다. 알려진 플랫폼 문제를 피하기 위해 아래의 ESR 설정을 건너뜁니다.
return;ESR 비활성화 정책을 유지하기 위해 오류 벡터 및 ESR 설정을 건너뜁니다.
maxlvt = lapic_get_maxlvt();로컬 APIC가 제공하는 최대 LVT 번호를 읽습니다. ESR에 0을 쓰기 전 하드웨어 세대에 따른 조건을 판단하는 값입니다.
if (maxlvt > 3) /* Due to the Pentium erratum 3AP. */최대 LVT 번호가 3보다 큰 APIC에서만 ESR에 0을 씁니다. Pentium의 3AP erratum 때문에 모든 APIC에 동일한 쓰기를 적용하지 않는 조건입니다.
apic_write(APIC_ESR, 0);LVT 수가 조건을 만족하면 ESR에 0을 씁니다. Pentium 3AP 오류를 고려한 조건 아래 이전 APIC 오류 상태를 정리하는 접근입니다.
oldvalue = apic_read(APIC_ESR);기존 ESR 오류 상태를 읽어 oldvalue에 저장합니다. 이후 오류 벡터 설정 뒤의 상태와 비교하고 진단할 자료입니다.
RISC-V · Linux 6.18.37
S-mode local interrupt와 external APLIC/PLIC/IMSIC domain
riscv_intc는 SCAUSE의 local cause를 per-hart domain으로 넘긴다. external device IRQ는 PLIC의 claim/complete, APLIC direct의 CLAIMI, 또는 APLIC MSI→IMSIC의 TOPEI EIID 중 실제 controller 경로에서 해석된다. APLIC direct는 IMSIC을 필수로 거치지 않는다.
원본 코드: drivers/irqchip/irq-riscv-intc.c:21-71
21
22#include <asm/hwcap.h>
23
24static struct irq_domain *intc_domain;
25static unsigned int riscv_intc_nr_irqs __ro_after_init = BITS_PER_LONG;
26static unsigned int riscv_intc_custom_base __ro_after_init = BITS_PER_LONG;
27static unsigned int riscv_intc_custom_nr_irqs __ro_after_init;
28
29static void riscv_intc_irq(struct pt_regs *regs)
30{
31 unsigned long cause = regs->cause & ~CAUSE_IRQ_FLAG;
32
33 if (generic_handle_domain_irq(intc_domain, cause))
34 pr_warn_ratelimited("Failed to handle interrupt (cause: %ld)\n", cause);
35}
36
37static void riscv_intc_aia_irq(struct pt_regs *regs)
38{
39 unsigned long topi;
40
41 while ((topi = csr_read(CSR_TOPI)))
42 generic_handle_domain_irq(intc_domain, topi >> TOPI_IID_SHIFT);
43}
44
45/*
46 * On RISC-V systems local interrupts are masked or unmasked by writing
47 * the SIE (Supervisor Interrupt Enable) CSR. As CSRs can only be written
48 * on the local hart, these functions can only be called on the hart that
49 * corresponds to the IRQ chip.
50 */
51
52static void riscv_intc_irq_mask(struct irq_data *d)
53{
54 if (IS_ENABLED(CONFIG_32BIT) && d->hwirq >= BITS_PER_LONG)
55 csr_clear(CSR_IEH, BIT(d->hwirq - BITS_PER_LONG));
56 else
57 csr_clear(CSR_IE, BIT(d->hwirq));
58}
59
60static void riscv_intc_irq_unmask(struct irq_data *d)
61{
62 if (IS_ENABLED(CONFIG_32BIT) && d->hwirq >= BITS_PER_LONG)
63 csr_set(CSR_IEH, BIT(d->hwirq - BITS_PER_LONG));
64 else
65 csr_set(CSR_IE, BIT(d->hwirq));
66}
67
68static void andes_intc_irq_mask(struct irq_data *d)
69{
70 /*
71 * Andes specific S-mode local interrupt causes (hwirq)주요 구문 해설
원본 51줄은 그대로 표시하며, 검토한 24개 구문에 설명을 붙였습니다.
#include <asm/hwcap.h>RISC-V ISA 확장 식별과 지원 여부 확인에 필요한 정의를 가져옵니다. 로컬 인터럽트 컨트롤러가 지원하는 하드웨어 기능에 맞춰 CSR 처리 방식을 고르는 근거입니다. 전처리 단계에서 헤더 내용을 포함하며, CPU가 실행 중 헤더를 호출하는 동작은 없습니다.
static struct irq_domain *intc_domain;hart 로컬 인터럽트 원인 번호와 Linux IRQ를 연결하는 domain을 보관합니다. 진입 처리기는 이 domain으로 원인 번호를 전달합니다.
static unsigned int riscv_intc_nr_irqs __ro_after_init = BITS_PER_LONG;기본 로컬 인터럽트 원인 수를 레지스터 폭인 32개 또는 64개로 시작합니다. 초기화 때 하드웨어에 맞게 조정한 뒤 __ro_after_init으로 읽기 전용이 됩니다.
static unsigned int riscv_intc_custom_base __ro_after_init = BITS_PER_LONG;기본 범위를 넘어서는 사용자 정의 로컬 인터럽트의 시작 번호입니다. 초기값은 기본 원인 수 바로 다음 번호이며 초기화 후에는 변경하지 않습니다.
static unsigned int riscv_intc_custom_nr_irqs __ro_after_init;추가로 제공되는 사용자 정의 인터럽트의 개수를 저장합니다. 초기화에서 정하고 이후에는 읽기 전용 상태로 유지합니다.
static void riscv_intc_irq(struct pt_regs *regs)일반 RISC-V 로컬 IRQ 진입 함수입니다. 저장된 cause에서 IRQ 표시 비트를 제거한 뒤 원인 번호를 IRQ domain에 전달합니다.
unsigned long cause = regs->cause & ~CAUSE_IRQ_FLAG;cause의 최상위 IRQ 표시 비트를 지워 순수한 인터럽트 원인 번호만 얻습니다. 예외인지 IRQ인지 나타내는 표시는 domain 인덱스에 포함하지 않습니다.
if (generic_handle_domain_irq(intc_domain, cause))원인 번호에 해당하는 Linux IRQ 처리를 요청하고 반환값이 오류인지 확인합니다. 성공은 0이므로 실패할 때만 아래의 빈도 제한 경고를 출력합니다.
pr_warn_ratelimited("Failed to handle interrupt (cause: %ld)\n", cause);해당 cause를 로컬 IRQ domain으로 처리하지 못하면 원인 번호를 경고합니다. 같은 실패가 반복되어도 로그가 쏟아지지 않도록 출력 빈도를 제한합니다.
static void riscv_intc_aia_irq(struct pt_regs *regs)AIA를 사용하는 hart의 로컬 IRQ 진입 함수입니다. 저장된 cause 하나만 처리하지 않고 TOPI를 반복해서 읽어 현재 최우선 요청들을 처리합니다.
unsigned long topi;TOPI에서 읽은 인터럽트 ID와 우선순위 정보를 담을 레지스터 폭의 값입니다. 0이면 현재 처리할 요청이 없습니다.
while ((topi = csr_read(CSR_TOPI)))TOPI가 0이 될 때까지 현재 최우선 인터럽트를 읽고 처리합니다. 하나를 처리한 뒤 다시 읽어 새 최우선 요청을 이어서 처리합니다.
generic_handle_domain_irq(intc_domain, topi >> TOPI_IID_SHIFT);AIA top interrupt 값에서 인터럽트 식별자를 추출해 로컬 IRQ domain의 처리기로 전달합니다. 하드웨어 원인 번호를 Linux IRQ 처리와 연결합니다.
static void riscv_intc_irq_mask(struct irq_data *d)irq_data가 가리키는 현재 hart의 로컬 IRQ를 마스크하는 함수입니다. 해당 원인의 허용 비트만 지우므로 다른 로컬 IRQ에는 영향을 주지 않습니다.
if (IS_ENABLED(CONFIG_32BIT) && d->hwirq >= BITS_PER_LONG)32비트 빌드에서 IRQ 번호가 기본 레지스터 폭 이상이면 상위 허용 레지스터 IEH를 사용합니다. 나머지 원인은 기본 IE 안의 비트로 다룹니다.
csr_clear(CSR_IEH, BIT(d->hwirq - BITS_PER_LONG));32비트 RISC-V에서 상위 번호 인터럽트의 CSR_IEH 비트를 지워 마스크합니다. BITS_PER_LONG을 빼 상위 CSR 안에서의 비트 위치로 바꿉니다.
else상위 32비트 전용 처리가 필요하지 않으면 기본 IE 레지스터에서 해당 원인 비트를 지웁니다.
csr_clear(CSR_IE, BIT(d->hwirq));해당 로컬 인터럽트 번호의 CSR_IE 비트를 지워 수신을 막습니다. 특정 원인만 비활성화하며 전체 인터럽트 허용 상태를 끄는 것은 아닙니다.
static void riscv_intc_irq_unmask(struct irq_data *d)현재 hart의 특정 로컬 IRQ 마스크를 해제하는 함수입니다. 해당 원인의 인터럽트 허용 비트를 다시 켭니다.
if (IS_ENABLED(CONFIG_32BIT) && d->hwirq >= BITS_PER_LONG)32비트에서 기본 IE에 담기지 않는 상위 IRQ인지 확인합니다. 상위 번호는 IEH, 그 외는 IE에 허용 비트를 설정합니다.
csr_set(CSR_IEH, BIT(d->hwirq - BITS_PER_LONG));32비트 RISC-V의 상위 로컬 인터럽트 허용 비트를 CSR_IEH에 설정합니다. 상위 레지스터 내부 비트 번호로 변환한 값을 사용합니다.
else기본 IE에 들어가는 IRQ이므로 IE의 해당 비트를 켜는 경로입니다.
csr_set(CSR_IE, BIT(d->hwirq));CSR_IE에서 해당 인터럽트 원인의 비트를 켭니다. 마스크했던 CPU 로컬 인터럽트가 다시 전달될 수 있게 합니다.
static void andes_intc_irq_mask(struct irq_data *d)Andes 전용 로컬 IRQ까지 포함해 마스크하는 함수입니다. 기본 원인은 IE, 256부터 시작하는 Andes 원인은 별도의 SLIE 레지스터에서 끕니다.
05 · WORKED EXAMPLE
숫자로 검산하기
level-triggered device IRQ가 종료되는 조건
device status bit가 1인 동안 line을 계속 assert하는 network device를 가정한다.
- acceptcontroller가 pending을 active로 옮기고 CPU가 INTID/vector/claim ID를 읽는다.
- servicedriver가 device status를 읽고 RX descriptor를 처리한 뒤 MMIO write로 원인을 clear한다.
- orderingposted MMIO write가 device에 도달하기 전에 controller를 deactivate하면 line이 아직 높아 재pending될 수 있다.
- finish필요한 readback/barrier 뒤 EOI/complete를 수행하고 unmask한다.
결론EOI는 device 원인을 지우는 연산이 아니다. device 상태와 controller active state 두 축이 모두 끝나야 interrupt가 종료된다.
06 · DEEP DIVE
주요 동작 상세 분석
공통 커널 코드와 아키텍처별 함수의 역할 구분
irqdomain은 hardware interrupt number를 Linux virtual IRQ에 매핑한다. 각 irqchip은 필요한 mask·unmask·ack·eoi·affinity callback을 선택해 구현하고, flow handler는 edge/level 설정에 맞춰 irq_desc와 device action chain을 실행한다. ack와 eoi가 호출되는 시점은 chip·flow handler마다 다르다.
controller acknowledge로 active state를 얻는 구간, generic_handle_domain_irq가 driver handler를 호출하는 구간, EOI/deactivate 구간을 분리한다. level source는 device status를 먼저 지우지 않으면 즉시 재assert된다.
arm64: GICv3 distributor, redistributor와 ICC system register interface
CPU는 ICC_IAR1_EL1에서 INTID를 acknowledge하고 irqdomain을 통해 irq_desc로 보낸다. SPI는 distributor, PPI/SGI는 redistributor의 per-CPU 상태를 사용하며 EOImode에 따라 priority drop과 deactivate가 나뉜다.
EOImode 0은 보통 IAR→handler→EOIR(priority drop·deactivate)이다. EOImode 1은 IAR→gic_complete_ack()의 이른 EOIR(priority drop)→handler→DIR(deactivate) 순서다. 단 ARM64 erratum 2941627 우회 시 deactivate에 GICD_ICACTIVER를 쓰고, mode 1의 LPI·vCPU forwarding에는 DIR을 생략한다. 디버깅할 때는 INTID, group/priority, redistributor affinity, pending/active bit, irq_desc depth와 device status를 같은 timestamp로 남긴다.
x86-64: local APIC vector, IOAPIC redirection과 IDT/FRED dispatch
IOAPIC pin 또는 MSI message가 destination APIC ID와 vector를 선택한다. local APIC가 vector를 accept하면 IDT 경로는 vector stub으로, FRED 경로는 fred_extint()로 진입한다. 두 경로의 일반 device IRQ는 common_interrupt()로 모여 per-CPU vector_irq mapping에서 irq_desc를 찾는다.
APIC EOI와 level-triggered IOAPIC remote IRR, interrupt remapping의 ordering이 affinity 이동 중 중복/손실을 막는다. 디버깅할 때는 APIC mode xAPIC/x2APIC, vector, destination, IOAPIC RTE, remote IRR, IRTE와 irq migration status를 본다.
RISC-V: S-mode local interrupt와 external APLIC/PLIC/IMSIC domain
riscv_intc는 SCAUSE의 local cause를 per-hart domain으로 넘긴다. external device IRQ는 PLIC의 claim/complete, APLIC direct의 CLAIMI, 또는 APLIC MSI→IMSIC의 TOPEI EIID 중 실제 controller 경로에서 해석된다. APLIC direct는 IMSIC을 필수로 거치지 않는다.
PLIC level 경로는 claim 뒤 handler와 complete write를 거치고 threshold를 사용한다. PLIC edge quirk는 irq_ack에서 complete한다. APLIC direct는 CLAIMI 읽기가 pending을 지우며, IMSIC은 TOPEI로 claim/ack한다. 후자 두 경로에 PLIC complete/threshold를 적용하지 않는다. 디버깅할 때는 hart ID·SCAUSE·SIE/SIP와 parent domain을 확인하고, PLIC이면 claim ID·threshold, APLIC direct이면 CLAIMI, IMSIC이면 TOPEI EIID를 별도로 기록한다.
객체를 사용하고 해제하는 주체와 시점을 확인한다
irqdomain mapping과 irq_desc는 device가 IRQ를 요청한 동안 유지된다. hot-unplug은 affinity 이동, synchronize_irq와 pending interrupt drain 뒤 mapping을 해제해야 한다.
주소나 레지스터 값만 확인하면 갱신되지 않은 상태를 놓칠 수 있다. 상태를 만드는 코드, 다른 코드에 전달하는 시점, 사용하는 코드, 해제하는 시점을 함께 기록한다.
최대 지연 시간은 명령 하나의 실행 시간만으로 결정되지 않는다
interrupt latency는 controller routing, priority masking, CPU interrupt-off 구간, irq_desc contention과 threaded handler wakeup으로 나뉜다. 최악값은 같은 priority의 폭주와 긴 hardirq-disabled section이 만든다.
평균값뿐 아니라 인터럽트를 비활성화한 시간, 다른 CPU의 응답 시간, 펌웨어 호출 시간, 재시도 횟수를 따로 확인해야 최대 지연의 원인을 찾을 수 있다.
07 · FAILURE
실패를 어떤 증거로 나눌 것인가
| 분류 | 관찰되는 결과 | 첫 확인값 |
|---|---|---|
| arm64 | spurious INTID를 정상 IRQ로 처리하거나 level source를 clear하기 전에 deactivate하면 같은 interrupt가 연속 재진입한다. | INTID, group/priority, redistributor affinity, pending/active bit, irq_desc depth와 device status를 같은 timestamp로 남긴다. |
| x86-64 | vector를 재사용하기 전에 이전 CPU의 in-flight interrupt를 drain하지 않으면 다른 device irq_desc를 호출할 수 있다. | APIC mode xAPIC/x2APIC, vector, destination, IOAPIC RTE, remote IRR, IRTE와 irq migration status를 본다. |
| RISC-V | SCAUSE code를 Linux hwirq로 잘못 변환하거나 PLIC complete를 빼면 해당 source가 막히거나 계속 pending 상태로 남는다. | hart ID·SCAUSE·SIE/SIP와 parent domain을 확인하고, PLIC이면 claim ID·threshold, APLIC direct이면 CLAIMI, IMSIC이면 TOPEI EIID를 별도로 기록한다. |
08 · LAB
재현과 계측 절차
- irqsoff tracer와 controller register dump를 결합해 interrupt assertion부터 handler 시작까지 최대 지연을 측정한다.
- affinity를 반복 이동하면서 MSI/vector/INTID와 irq_desc mapping이 언제 바뀌는지 추적한다.
- 동일한 workload에서 세 architecture의 tracepoint 이름, CPU 번호, PC, stack pointer와 address-space identifier를 같은 열로 기록한다.
- 소스만 읽고 끝내지 않고 최종
vmlinux의objdump -dr,readelf -SW결과로 선택된 alternative와 section 배치를 확인한다.
09 · REFERENCES
원문 위치
- arm64drivers/irqchip/irq-gic-v3.c:956-1028
- x86-64arch/x86/kernel/apic/apic.c:1347-1409
- RISC-Vdrivers/irqchip/irq-riscv-intc.c:21-71
- GIC EOI 경로irq-gic-v3.c:700-748irq-gic-v3.c:829-880
- PLIC level/edgeirq-sifive-plic.c:157-247irq-sifive-plic.c:380-408
- APLIC directirq-riscv-aplic-direct.c:42-48irq-riscv-aplic-direct.c:129-164
- IMSICirq-riscv-imsic-early.c:109-133
- x86 device dispatcharch/x86/kernel/irq.c:248-320entry_fred.c:157-175
Linux kernel source: GPL-2.0-only. 이 글의 코드 발췌는 Linux v6.18.37 원문을 기준으로 하며, 분석 문장은 해당 코드의 실행 조건과 상태 경계를 설명합니다.