분석 범위: Linux v6.18.37의 arm64 GICv3 드라이버, 일반 물리 SPI의 Group 1 IRQ 경로입니다. LPI·가상화 전달·pseudo-NMI·threaded IRQ의 차이는 별도로 구분합니다. 이 버전은 고정된 분석 기준이지 최신 보안 버전이라는 뜻이 아닙니다.
아래 그림은 소스를 해석해 그린 흐름도이며 실제 보드의 측정 결과가 아닙니다.
1. 알림 전달과 처리 완료를 분리하기
GIC는 어떤 장치가 CPU의 주의를 요청하는지 정리하는 인터럽트 컨트롤러입니다. 장치 상태 레지스터의 원인을 해소하는 일은 드라이버가 맡습니다. GIC에 완료를 알렸다고 장치의 수신 데이터나 오류 원인이 사라지지는 않습니다.
- 장치IRQ 원인 발생상태 비트 또는 FIFO 등
- Distributor · GICDSPI 라우팅enable·우선순위·대상 PE
- CPU exceptiongic_handle_irq중단된 문맥 상태로 helper 선택
- CPU interface · ICC_*IAR 읽기선택된 helper가 INTID acknowledge
- irqdomainINTID 매핑hwirq → irq_desc
- Linux IRQflow handlerirqaction·장치 드라이버 호출
드라이버의 장치 원인 처리와 GIC의 EOIR/DIR 완료는 서로 다른 동작입니다. EOImode 1의 priority drop은 Linux action handler 진입 전에 수행됩니다.
| 구성요소 | 역할 | 이 글에서의 구분 |
|---|---|---|
| Distributor / GICD | SPI 설정과 라우팅 | 초기화·affinity 경로와 IRQ 수신 경로는 다릅니다. |
| Redistributor / GICR | CPU별 SGI/PPI 및 LPI 관련 상태 | GICR_WAKER는 Redistributor의 sleep/wake handshake에 사용합니다. |
| CPU interface / ICC_* | acknowledge, 우선순위, EOI | IRQ 수신의 IAR, 완료의 EOIR/DIR가 여기에 속합니다. |
2. 초기화와 IRQ 진입은 다른 경로
gic_init_bases()는 domain과 컨트롤러 기반 설정을 준비하는 초기화 함수입니다. 같은 파일의 set_handle_irq(gic_handle_irq)가 아키텍처 IRQ 진입에서 사용할 함수를 등록합니다. CPU redistributor를 깨우는 gic_enable_redist()를 매 IRQ마다 실행하는 것으로 읽으면 안 됩니다.
Redistributor 전원 상태 제어
drivers/irqchip/irq-gic-v3.c:367-399 · 로컬 원문
static void gic_enable_redist(bool enable)
{
void __iomem *rbase;
u32 val;
int ret;
if (gic_data.flags & FLAGS_WORKAROUND_GICR_WAKER_MSM8996)
return;
rbase = gic_data_rdist_rd_base();
val = readl_relaxed(rbase + GICR_WAKER);
if (enable)
/* Wake up this CPU redistributor */
val &= ~GICR_WAKER_ProcessorSleep;
else
val |= GICR_WAKER_ProcessorSleep;
writel_relaxed(val, rbase + GICR_WAKER);
if (!enable) { /* Check that GICR_WAKER is writeable */
val = readl_relaxed(rbase + GICR_WAKER);
if (!(val & GICR_WAKER_ProcessorSleep))
return; /* No PM support in this redistributor */
}
ret = readl_relaxed_poll_timeout_atomic(rbase + GICR_WAKER, val,
enable ^ (bool)(val & GICR_WAKER_ChildrenAsleep),
1, USEC_PER_SEC);
if (ret == -ETIMEDOUT) {
pr_err_ratelimited("redistributor failed to %s...\n",
enable ? "wakeup" : "sleep");
}
}enable 값에 따라 WAKER의 ProcessorSleep 상태를 바꾸고 ChildrenAsleep 응답을 기다립니다. 이 코드는 대기 중인 INTID를 반환하지 않습니다. 반면 아래의 CPU interface helper는 IAR를 읽어 INTID를 돌려줍니다.
CPU interface의 IAR 읽기
arch/arm64/include/asm/arch_gicv3.h:35-42 · 로컬 원문
static inline u64 gic_read_iar_common(void)
{
u64 irqstat;
irqstat = read_sysreg_s(SYS_ICC_IAR1_EL1);
dsb(sy);
return irqstat;
}최종 gic_read_iar() wrapper에는 CPU erratum에 따른 구현 선택도 있습니다. 그림의 IAR 읽기는 공통 의미를 표현한 것이며 모든 CPU가 동일한 명령열을 실행한다고 가정하지 않습니다.
3. 함수 이름별로 실제 책임 나누기
gic_handle_irq: 진입 당시 IRQ 상태로 분기
drivers/irqchip/irq-gic-v3.c:964-970 · 로컬 원문
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
if (unlikely(gic_supports_nmi() && !interrupts_enabled(regs)))
__gic_handle_irq_from_irqsoff(regs);
else
__gic_handle_irq_from_irqson(regs);
}interrupts_enabled(regs)는 중단된 문맥의 상태를 봅니다. pseudo-NMI를 지원하고 그 문맥에서 일반 IRQ가 막혀 있었다면 irqsoff 경로를 사용합니다. 그 밖에는 irqson 경로를 택합니다.
irqson: acknowledge한 뒤 IRQ와 NMI를 구분
drivers/irqchip/irq-gic-v3.c:904-926 · 로컬 원문
static void __gic_handle_irq_from_irqson(struct pt_regs *regs)
{
bool is_nmi;
u32 irqnr;
irqnr = gic_read_iar();
is_nmi = gic_rpr_is_nmi_prio();
if (is_nmi) {
nmi_enter();
__gic_handle_nmi(irqnr, regs);
nmi_exit();
}
if (gic_prio_masking_enabled()) {
gic_pmr_mask_irqs();
gic_arch_enable_irqs();
}
if (!is_nmi)
__gic_handle_irq(irqnr, regs);
}gic_read_iar()에서 IRQ 번호를 얻은 뒤 running priority로 NMI 여부를 판정합니다. NMI라면 nmi_enter()/nmi_exit()로 감싸고, 일반 IRQ라면 __gic_handle_irq()로 전달합니다. 따라서 helper의 발췌를 단순히 gic_handle_irq 함수 본문이라고 표기하면 잘못입니다.
IRQ/NMI helper: special INTID와 매핑 실패 처리
drivers/irqchip/irq-gic-v3.c:862-891 · 로컬 원문
static bool gic_irqnr_is_special(u32 irqnr)
{
return irqnr >= 1020 && irqnr <= 1023;
}
static void __gic_handle_irq(u32 irqnr, struct pt_regs *regs)
{
if (gic_irqnr_is_special(irqnr))
return;
gic_complete_ack(irqnr);
if (generic_handle_domain_irq(gic_data.domain, irqnr)) {
WARN_ONCE(true, "Unexpected interrupt (irqnr %u)\n", irqnr);
gic_deactivate_unhandled(irqnr);
}
}
static void __gic_handle_nmi(u32 irqnr, struct pt_regs *regs)
{
if (gic_irqnr_is_special(irqnr))
return;
gic_complete_ack(irqnr);
if (generic_handle_domain_nmi(gic_data.domain, irqnr)) {
WARN_ONCE(true, "Unexpected pseudo-NMI (irqnr %u)\n", irqnr);
gic_deactivate_unhandled(irqnr);
}
}1020~1023은 일반 장치 IRQ처럼 domain에 전달하지 않습니다. 정상 IRQ에서는 complete_ack 뒤에 domain 처리를 요청합니다. 매핑이 없거나 descriptor 처리가 실패한 경우 경고만 남기는 것이 아니라 gic_deactivate_unhandled()로 컨트롤러 상태도 정리합니다.
4. INTID에서 드라이버까지
generic_handle_domain_irq: domain과 hwirq로 descriptor 찾기
kernel/irq/irqdesc.c:712-727 · 로컬 원문
/**
* generic_handle_domain_irq - Invoke the handler for a HW irq belonging
* to a domain.
* @domain: The domain where to perform the lookup
* @hwirq: The HW irq number to convert to a logical one
*
* Returns: 0 on success, or -EINVAL if conversion has failed
*
* This function must be called from an IRQ context with irq regs
* initialized.
*/
int generic_handle_domain_irq(struct irq_domain *domain, unsigned int hwirq)
{
return handle_irq_desc(irq_resolve_mapping(domain, hwirq));
}
EXPORT_SYMBOL_GPL(generic_handle_domain_irq);동일한 숫자라도 INTID와 Linux IRQ 번호는 같은 공간이 아닙니다. Device Tree의 SPI 번호 또한 GIC의 전체 INTID와 차이가 있습니다. 이 드라이버의 DT 변환은 SPI에 32를 더합니다. 예를 들어 DT SPI 번호 40은 INTID 72가 되지만, Linux IRQ가 반드시 72라는 뜻은 아닙니다.
- Device TreeSPI 40DT interrupt specifier의 SPI 번호
- GIC hwirqINTID 72gic_irq_domain_translate(): 40 + 32
- Linux irqdomainLinux IRQ / virq매핑된 irq_desc의 번호이며 72로 고정되지 않음
DT 번호 변환은 IRQ 등록·할당 때의 관계이며 매 IRQ마다 DT를 다시 읽는다는 뜻이 아닙니다. PPI·ESPI·LPI에는 각기 다른 변환 규칙이 있습니다. 일반 SPI의 irq_desc는 handle_fasteoi_irq로 처리하고, 등록된 irqaction의 primary handler를 호출하거나 요청 구성에 따라 IRQ thread를 깨웁니다.
handle_fasteoi_irq: 상태 확인, 이벤트 처리, EOI
kernel/irq/chip.c:736-774 · 로컬 원문
void handle_fasteoi_irq(struct irq_desc *desc)
{
struct irq_chip *chip = desc->irq_data.chip;
guard(raw_spinlock)(&desc->lock);
/*
* When an affinity change races with IRQ handling, the next interrupt
* can arrive on the new CPU before the original CPU has completed
* handling the previous one - it may need to be resent.
*/
if (!irq_can_handle_pm(desc)) {
if (irqd_needs_resend_when_in_progress(&desc->irq_data))
desc->istate |= IRQS_PENDING;
cond_eoi_irq(chip, &desc->irq_data);
return;
}
if (!irq_can_handle_actions(desc)) {
mask_irq(desc);
cond_eoi_irq(chip, &desc->irq_data);
return;
}
kstat_incr_irqs_this_cpu(desc);
if (desc->istate & IRQS_ONESHOT)
mask_irq(desc);
handle_irq_event(desc);
cond_unmask_eoi_irq(desc, chip);
/*
* When the race described above happens this will resend the interrupt.
*/
if (unlikely(desc->istate & IRQS_PENDING))
check_irq_resend(desc, false);
}
EXPORT_SYMBOL_GPL(handle_fasteoi_irq);descriptor lock을 잡은 상태에서 IRQ가 처리 가능한지 확인합니다. disable 상태나 action 부재 등으로 처리할 수 없으면 mask/조건부 EOI 경로로 나갑니다. 정상 경로는 통계를 갱신하고, oneshot이면 mask한 뒤 이벤트를 처리합니다. 마지막의 cond_unmask_eoi_irq()가 thread 및 oneshot 상태에 맞춰 EOI/unmask를 결정하므로, 모든 IRQ가 같은 시점에 unmask된다고 단정할 수 없습니다.
5. EOI는 한 가지 완료가 아니다
인터럽트에는 CPU가 처리 중인 우선순위를 낮추는 동작과 컨트롤러의 active 상태를 해제하는 동작이 있습니다. EOImode에 따라 두 동작이 합쳐지거나 분리됩니다.
EOImode 1에서는 handler 진입 전 priority drop
drivers/irqchip/irq-gic-v3.c:846-852 · 로컬 원문
static inline void gic_complete_ack(u32 irqnr)
{
if (static_branch_likely(&supports_deactivate_key))
write_gicreg(irqnr, ICC_EOIR1_EL1);
isb();
}irqchip의 EOI callback과 mode 1 deactivate
drivers/irqchip/irq-gic-v3.c:719-748 · 로컬 원문
static void gic_eoi_irq(struct irq_data *d)
{
write_gicreg(irqd_to_hwirq(d), ICC_EOIR1_EL1);
isb();
if (gic_arm64_erratum_2941627_needed(d)) {
/*
* Make sure the GIC stream deactivate packet
* issued by ICC_EOIR1_EL1 has completed before
* deactivating through GICD_IACTIVER.
*/
dsb(sy);
gic_poke_irq(d, GICD_ICACTIVER);
}
}
static void gic_eoimode1_eoi_irq(struct irq_data *d)
{
/*
* No need to deactivate an LPI, or an interrupt that
* is is getting forwarded to a vcpu.
*/
if (irqd_to_hwirq(d) >= 8192 || irqd_is_forwarded_to_vcpu(d))
return;
if (!gic_arm64_erratum_2941627_needed(d))
gic_write_dir(irqd_to_hwirq(d));
else
gic_poke_irq(d, GICD_ICACTIVER);
}| 일반 물리 SPI | EOImode 0 | EOImode 1 |
|---|---|---|
| IAR acknowledge 이후 | active 상태로 처리 | active 상태로 처리 |
| gic_complete_ack | ISB | EOIR1에 써 priority drop 후 ISB |
| irqchip 완료 callback | EOIR1로 priority drop과 deactivate | 일반 SPI는 DIR로 deactivate |
| 주의할 예외 | erratum 2941627이면 EOIR1 뒤 GICD_ICACTIVER도 사용 | LPI·vCPU 전달 IRQ는 이 callback에서 DIR하지 않음; erratum 2941627이면 DIR 대신 GICD_ICACTIVER |
두 경우 모두 장치 자체의 원인 해소를 대신하지 않습니다. level-triggered IRQ에서 드라이버가 원인을 남겨 두면 active 해제 후 다시 pending될 수 있습니다. 상태 레지스터가 W1C인지, 읽기로 clear되는지 등은 해당 장치 명세를 따라야 합니다.
- 장치원인 미해소level 입력이 계속 활성
- GIC 완료active 해제EOImode 0: EOIR1
EOImode 1: DIR - GIC 상태다시 pending 가능입력 level이 유지됨
- 대상 CPU조건부 재수신enable·mask·priority 조건에 따름
일반 동기식 primary handler에서 장치 원인이 남은 예시이며, erratum 2941627 우회가 필요 없는 호스트 처리 SPI를 가정합니다. threaded/oneshot IRQ에서는 EOI가 thread 완료를 뜻하지 않으며 unmask 시점도 다릅니다. 예외의 GICD_ICACTIVER 사용은 위 표와 소스에서 구분합니다.
6. 예외 경로와 동시성에서 지킬 조건
| 조건 | 소스에서의 대응 | 잘못된 해석 |
|---|---|---|
| IRQs-off 문맥의 pseudo-NMI | IAR 읽기 전에 PMR를 일반 IRQ 차단 수준으로 낮춤 | IAR를 먼저 읽고 나중에 종류만 구분하면 충분하다는 해석 |
| domain dispatch 실패 | gic_deactivate_unhandled() | WARN 출력으로 완료 처리가 끝났다는 해석 |
| threaded/oneshot IRQ | threads_oneshot, mask 상태, chip flag에 따라 EOI/unmask 결정 | primary handler 반환 즉시 모든 IRQ가 unmask된다는 해석 |
| affinity 이동과 처리 경합 | IRQS_PENDING 및 check_irq_resend() | CPU 이동 중에는 동일 IRQ 경합이 불가능하다는 해석 |
irqsoff 경로의 긴 주석은 중요한 실패 사례를 설명합니다. NMI가 사라진 사이 일반 IRQ를 acknowledge하면 NMI 문맥에서 일반 IRQ를 처리할 수 있습니다. 그래서 gic_read_pmr → gic_pmr_mask_irqs → isb → gic_read_iar → gic_write_pmr 순서가 필요합니다.
irqsoff의 PMR 보호 구간
drivers/irqchip/irq-gic-v3.c:937-962 · 로컬 원문
static void __gic_handle_irq_from_irqsoff(struct pt_regs *regs)
{
u64 pmr;
u32 irqnr;
/*
* We were in a context with IRQs disabled. However, the
* entry code has set PMR to a value that allows any
* interrupt to be acknowledged, and not just NMIs. This can
* lead to surprising effects if the NMI has been retired in
* the meantime, and that there is an IRQ pending. The IRQ
* would then be taken in NMI context, something that nobody
* wants to debug twice.
*
* Until we sort this, drop PMR again to a level that will
* actually only allow NMIs before reading IAR, and then
* restore it to what it was.
*/
pmr = gic_read_pmr();
gic_pmr_mask_irqs();
isb();
irqnr = gic_read_iar();
gic_write_pmr(pmr);
__gic_handle_nmi(irqnr, regs);
}7. 레지스터 쓰기 없이 먼저 관측하기
먼저 /proc/interrupts에서 장치 이름·Linux IRQ 번호·CPU별 증가량을 봅니다. 테스트 장치에 통제된 작업을 주기 전후의 차이를 비교합니다. 아래 예는 추적 방법이며 이 글에서 실제 장비 결과를 측정한 것은 아닙니다.
cat /proc/interrupts
# trace-cmd가 설치되고 tracefs 권한이 있는 시험 장비에서:
sudo trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit sleep 5
trace-cmd report이 event 쌍은 Linux action handler 주변을 관찰합니다. IAR 읽기부터의 전체 하드웨어 지연이나 threaded handler 전체 실행 시간을 직접 재는 것이 아닙니다. function tracing은 available_filter_functions에서 실제 사용 가능한 함수부터 확인해야 합니다. inline helper가 항상 trace 가능한 것도 아닙니다.
운영 장비에서 devmem으로 IAR/EOIR/DIR 또는 GIC MMIO를 시험 삼아 읽거나 쓰지 않습니다. acknowledge/완료·mask 설정을 바꾸면 커널이 관리하는 상태와 불일치하거나 이벤트를 잃을 수 있습니다.
8. 근거와 남은 검증 범위
코드 발췌는 고정 소스의 행 구간으로 생성하고 파일 SHA-256을 검사합니다. GICv2, ITS 전체 수명주기, GICv4 직접 주입, 실제 SoC의 전원 도메인·erratum 조합은 이 글에서 전수 검증하지 않았습니다.