IRQ / Time · Linux v6.6 · source + diagram note

GIC

Linux v6.6 arm64 GICv3에서 초기화, 일반 Group 1 물리 SPI 수신, Linux IRQ 전달과 완료를 구분합니다.

전체 흐름

이 글은 Linux v6.6 arm64 GICv3의 일반 Group 1 물리 SPI를 중심으로 봅니다. 부팅·CPU 시작 때의 컨트롤러 준비와 매 인터럽트의 acknowledge·Linux IRQ 전달·완료를 분리해야 합니다. LPI, 가상화 전달, pseudo-NMI, threaded IRQ는 같은 그림의 기본 경로로 합치지 않습니다.

핵심 확인 사항

DT의 SPI 번호, GIC INTID, Linux IRQ 번호는 서로 다른 공간입니다. IAR가 돌려준 INTID가 irqdomain을 거쳐 irq_desc와 드라이버 action으로 연결되는 위치를 확인합니다.

오류 처리 확인

special INTID(1020~1023)는 일반 domain dispatch로 보내지 않습니다. 매핑 실패 시 gic_deactivate_unhandled()가 컨트롤러 상태를 정리하는지, CPU hotplug/PM의 Redistributor 준비가 별도 경로인지 확인합니다.

그림 1. 일반 Group 1 물리 SPI의 수신 경로
  1. 장치IRQ 원인 발생장치 상태가 level 또는 edge 신호를 만듦
  2. GICDSPI 라우팅enable·우선순위·대상 CPU 설정
  3. CPU exceptiongic_handle_irq중단된 문맥으로 helper 선택
  4. CPU interfaceIAR 읽기선택된 helper가 INTID acknowledge
  5. irqdomainINTID 매핑hwirq에서 irq_desc 찾기
  6. Linux IRQflow/action드라이버 처리와 irqchip 완료
Redistributor(GICR)는 이 SPI의 매 IRQ 통과 단계가 아닙니다.CPU별 SGI/PPI/LPI 상태와 초기화·CPU PM의 sleep/wake 경로를 별도로 추적합니다.

장치 원인 해소와 GIC의 priority drop·active 해제는 서로 다릅니다. 두 GIC 완료 동작의 시점은 EOImode에 따라 달라집니다.

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: drivers/irqchip/irq-gic-v3.c
Linux v6.6: drivers/irqchip/irq-gic-common.c
Linux v6.6: include/linux/irqchip/arm-gic-v3.h
Linux v6.6: arch/arm64/include/asm/arch_gicv3.h

설명: irq-gic-v3.c에는 초기화, irqdomain 변환, IRQ 진입과 EOI callback이 있습니다. irq-gic-common.c는 공통 irqchip 보조 구현, arm-gic-v3.h는 GICv3 레지스터와 INTID 관련 정의를 확인하는 자료입니다. CPU interface의 IAR 읽기는 arm64 helper까지 따라가야 합니다.

대표 코드

gic_init_bases

원본 코드: drivers/irqchip/irq-gic-v3.c:2003-2098 · v6.6 · 로컬 원문
gic_init_bases 함수 전체 (96줄)
static int __init gic_init_bases(phys_addr_t dist_phys_base,
				 void __iomem *dist_base,
				 struct redist_region *rdist_regs,
				 u32 nr_redist_regions,
				 u64 redist_stride,
				 struct fwnode_handle *handle)
{
	u32 typer;
	int err;

	if (!is_hyp_mode_available())
		static_branch_disable(&supports_deactivate_key);

	if (static_branch_likely(&supports_deactivate_key))
		pr_info("GIC: Using split EOI/Deactivate mode\n");

	gic_data.fwnode = handle;
	gic_data.dist_phys_base = dist_phys_base;
	gic_data.dist_base = dist_base;
	gic_data.redist_regions = rdist_regs;
	gic_data.nr_redist_regions = nr_redist_regions;
	gic_data.redist_stride = redist_stride;

	/*
	 * Find out how many interrupts are supported.
	 */
	typer = readl_relaxed(gic_data.dist_base + GICD_TYPER);
	gic_data.rdists.gicd_typer = typer;

	gic_enable_quirks(readl_relaxed(gic_data.dist_base + GICD_IIDR),
			  gic_quirks, &gic_data);

	pr_info("%d SPIs implemented\n", GIC_LINE_NR - 32);
	pr_info("%d Extended SPIs implemented\n", GIC_ESPI_NR);

	/*
	 * ThunderX1 explodes on reading GICD_TYPER2, in violation of the
	 * architecture spec (which says that reserved registers are RES0).
	 */
	if (!(gic_data.flags & FLAGS_WORKAROUND_CAVIUM_ERRATUM_38539))
		gic_data.rdists.gicd_typer2 = readl_relaxed(gic_data.dist_base + GICD_TYPER2);

	gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops,
						 &gic_data);
	gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
	if (!static_branch_unlikely(&gic_nvidia_t241_erratum)) {
		/* Disable GICv4.x features for the erratum T241-FABRIC-4 */
		gic_data.rdists.has_rvpeid = true;
		gic_data.rdists.has_vlpis = true;
		gic_data.rdists.has_direct_lpi = true;
		gic_data.rdists.has_vpend_valid_dirty = true;
	}

	if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
		err = -ENOMEM;
		goto out_free;
	}

	irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);

	gic_data.has_rss = !!(typer & GICD_TYPER_RSS);

	if (typer & GICD_TYPER_MBIS) {
		err = mbi_init(handle, gic_data.domain);
		if (err)
			pr_err("Failed to initialize MBIs\n");
	}

	set_handle_irq(gic_handle_irq);

	gic_update_rdist_properties();

	gic_dist_init();
	gic_cpu_init();
	gic_smp_init();
	gic_cpu_pm_init();

	if (gic_dist_supports_lpis()) {
		its_init(handle, &gic_data.rdists, gic_data.domain);
		its_cpu_init();
		its_lpi_memreserve_init();
	} else {
		if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
			gicv2m_init(handle, gic_data.domain);
	}

	gic_enable_nmi_support();

	return 0;

out_free:
	if (gic_data.domain)
		irq_domain_remove(gic_data.domain);
	free_percpu(gic_data.rdists.rdist);
	return err;
}

확인 사항: gic_init_bases()는 irq_domain_create_tree()로 domain을 만들고 CPU별 자료를 확보한 뒤 set_handle_irq(gic_handle_irq)를 등록합니다. 이어 gic_dist_init()와 gic_cpu_init()을 호출합니다. GICD_CTLR 변경은 gic_init_bases() 본문이 아니라 gic_dist_init() 내부에서 일어납니다. domain 또는 per-CPU 할당 실패는 out_free로 정리합니다.

gic_handle_irq

원본 코드: drivers/irqchip/irq-gic-v3.c:861-867 · v6.6 · 로컬 원문
static asmlinkage 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);
}

여기는 v6.6 GICv3의 IRQ 진입 wrapper다. interrupted context의 IRQ 상태에 따라 irqson/irqsoff helper로 분기한다. 실제 acknowledge와 dispatch는 helper에서 수행하며, 이 함수가 GICR_WAKER로 Redistributor를 깨우는 것은 아니다.

함수별 분석

GICv3의 SPI와 CPU별 SGI/PPI/LPI는 설정·전달 책임이 다릅니다. 아래 함수 경로는 일반 물리 SPI의 초기화와 IRQ 수신을 따로 나타냅니다.

gic_init_bases()는 부팅 시 domain과 handler, Distributor·CPU interface를 준비합니다. 등록된 gic_handle_irq()는 이후 IRQ 예외에서 호출되며 helper가 IAR를 읽고 INTID를 domain으로 전달합니다.

그림 2. 초기화와 IRQ 진입은 서로 다른 호출 경로

부팅·설정 경로

  1. gic_init_basesdomain 준비irq_domain_create_tree와 CPU별 자료 확보
  2. gic_init_baseshandler 등록set_handle_irq(gic_handle_irq)
  3. init 함수들컨트롤러 준비gic_dist_init·gic_cpu_init

일반 물리 SPI 수신 경로

  1. CPU exceptiongic_handle_irq중단된 문맥 상태로 helper 선택
  2. irqson helperIAR acknowledgespecial INTID는 별도 처리
  3. 일반 IRQ helperdomain dispatchgeneric_handle_domain_irq

위아래 lane 사이의 연결은 초기화가 런타임 경로를 준비한다는 뜻이며, gic_init_bases가 매 인터럽트마다 호출된다는 뜻이 아닙니다.

1. gic_init_bases

gic_init_bases()는 gic_data.domain을 생성하고 set_handle_irq(gic_handle_irq)로 진입 함수를 등록합니다. 실제 Distributor 제어 레지스터 설정은 호출된 gic_dist_init()에서 처리합니다.

v6.6 발췌에서는 domain 및 per-CPU 자료 할당, 실패 시 out_free, handler 등록, gic_dist_init()·gic_cpu_init() 호출을 이 순서로 확인합니다. Redistributor wake는 gic_cpu_init()이 호출하는 gic_enable_redist(true)의 CPU 준비 작업입니다.

구분: domain 생성·handler 등록은 부팅 설정이고, IAR acknowledge는 IRQ 수신 시 수행합니다.

2. gic_handle_irq

gic_handle_irq는 중단된 문맥의 IRQ 상태를 보고 irqson/irqsoff helper로 분기합니다. helper의 IAR acknowledge, special INTID 처리와 irqdomain 전달을 따라 읽어야 합니다. Redistributor를 깨우는 GICR_WAKER 초기화와는 별도 경로입니다.

일반 IRQ 경로에서는 irqson helper가 gic_read_iar()로 INTID를 얻고, special INTID를 제외한 뒤 generic_handle_domain_irq()로 넘깁니다. domain dispatch 실패는 gic_deactivate_unhandled()로 정리합니다. 이 경로는 Redistributor wake를 수행하지 않습니다.

구분: Redistributor wake는 CPU init/PM이고, gic_handle_irq의 IAR·dispatch는 매 IRQ 경로입니다.

함수입력상태 변경확인할 사항
gic_init_basesGICD/GICR base, firmware handledomain·per-CPU 자료 준비, handler 등록, gic_dist_init·gic_cpu_init 호출GICD_CTLR는 gic_dist_init 내부에서 바뀌는가?
gic_handle_irqIRQ 예외의 pt_regs, 중단된 문맥의 IRQ 상태irqson/irqsoff helper 선택; helper에서 IAR·domain dispatchspecial INTID와 실패한 mapping은 어떻게 처리되는가?

번호 공간과 주요 상태

DT SPI 번호는 irqdomain 설정 때 GIC INTID로 변환되고, Linux IRQ 번호는 domain mapping 결과로 정해집니다. GICD, GICR, CPU interface 레지스터는 서로 다른 책임을 가지므로 하나의 순차 필드 체인으로 읽지 않습니다.

그림 3. DT SPI 번호, GIC INTID, Linux IRQ 번호 공간
  1. Device TreeSPI 40interrupt specifier의 SPI 번호
  2. GIC hwirqINTID 72gic_irq_domain_translate: 40 + 32
  3. Linux irqdomainLinux IRQ / virqirq_desc 매핑 결과; 72로 고정되지 않음

GICD_CTLR는 Distributor 설정, GICR_WAKER는 CPU별 Redistributor sleep/wake, ICC_EOIR1_EL1·ICC_DIR_EL1은 CPU interface 완료에 관련됩니다. 서로를 순서대로 소비하는 단일 필드 체인은 아닙니다. DT 번호 변환은 IRQ 등록·할당 때의 관계이며 매 IRQ마다 DT를 다시 읽는다는 뜻이 아닙니다.

각 항목이 부팅 설정, CPU별 준비, IRQ 수신, 완료 중 어느 단계에 속하는지 구분합니다.

대상v6.6에서의 책임
GICD_CTLRDistributor의 global control. gic_init_bases()가 호출하는 gic_dist_init()에서 설정합니다.
GICR_WAKERRedistributor sleep/wake handshake. gic_cpu_init() 및 CPU PM 경로에서 다룹니다.
gic_data.domainINTID(hwirq)를 Linux irq_desc로 연결하는 irqdomain입니다.
ICC_IAR1_EL1CPU interface에서 Group 1 INTID를 acknowledge합니다. 단순 조회용 레지스터가 아닙니다.
ICC_EOIR1_EL1 / ICC_DIR_EL1priority drop 및 active 해제에 관련하며 사용 시점은 EOImode에 따릅니다.

설정과 IRQ 처리의 상태

부팅·CPU 시작에서 만든 domain과 handler는 이후 IRQ 예외가 사용합니다. 한 번의 IRQ에서는 IAR acknowledge, special INTID 판별, domain dispatch, irqchip 완료를 순서와 EOImode 조건에 따라 따로 봐야 합니다.

그림 4. 설정 상태와 인터럽트 한 번의 수신·완료를 나눈 수영레인

부팅·CPU 시작 시 준비

  1. gic_init_basesdomain 생성gic_data.domain 확보
  2. handler 등록set_handle_irq아키텍처 IRQ 진입 함수 지정
  3. 하드웨어 준비GICD·CPU initgic_dist_init와 gic_cpu_init

호스트가 직접 처리하는 일반 Group 1 물리 SPI

  1. CPU interfaceIAR acknowledgeINTID 수신
  2. GIC helpergic_complete_ackEOImode 0: ISB
    EOImode 1: EOIR1 priority drop
  3. generic IRQdomain→actionirq_desc flow handler와 드라이버
  4. irqchip callbackactive 해제mode 0: EOIR1(기본)
    mode 1: DIR(기본)

그림은 vCPU로 전달하지 않고 erratum 2941627 우회가 필요 없는 SPI의 기본 경로입니다. 우회가 필요하면 mode 0은 EOIR1 뒤 GICD_ICACTIVER로도 active를 정리하고, mode 1은 DIR 대신 GICD_ICACTIVER를 씁니다. LPI·vCPU 전달 IRQ는 mode 1 callback에서 DIR하지 않습니다. level IRQ의 장치 원인이 남으면 active 해제 후 다시 pending될 수 있습니다.

구간v6.6 경로관찰할 조건잘못된 연결
부팅 설정gic_init_bases: domain 생성·handler 등록·gic_dist_init 호출domain/per-CPU 할당 실패는 out_free로 정리gic_init_bases가 매 IRQ마다 GICD_CTLR를 쓰는 것처럼 읽기
CPU 준비gic_cpu_init → gic_enable_redist(true)GICR_WAKER의 sleep/wake 응답GICR wake를 일반 SPI의 매 IRQ 데이터 경로로 놓기
INTID 수신gic_handle_irq의 helper가 IAR 읽기1020~1023 special INTID는 일반 domain dispatch 제외IAR를 반복해도 무해한 상태 조회로 보기
Linux IRQ 전달generic_handle_domain_irq(gic_data.domain, irqnr)mapping 실패 시 gic_deactivate_unhandled()INTID와 Linux IRQ 번호가 늘 같다고 보기
GIC 완료EOImode별 EOIR1·DIR callback장치 원인 해소와 active 해제는 별개EOI가 장치 상태를 자동 clear한다고 보기
GIC 검토 순서 = 초기화와 IRQ 진입 분리 → INTID/virq 구분 → 장치 원인과 컨트롤러 완료 구분

불변 조건과 실패 사례

IAR acknowledge

IAR 읽기는 pending INTID를 acknowledge하는 동작입니다. 디버깅용 반복 조회나 devmem 실험으로 취급하지 않습니다.

세 번호 공간

DT SPI 40은 INTID 72로 변환되지만 Linux IRQ 번호는 irqdomain mapping 결과입니다. 숫자가 우연히 같아도 같은 namespace가 아닙니다.

special·매핑 실패

1020~1023 special INTID는 일반 IRQ로 전달하지 않습니다. domain dispatch 실패는 경고 뒤 gic_deactivate_unhandled()로 정리합니다.

장치 원인과 GIC 완료

EOIR/DIR은 장치 상태 레지스터의 원인을 지우지 않습니다. level IRQ에서 원인이 남으면 active 해제 후 다시 pending될 수 있습니다.

주의

일반 SPI의 GICD 라우팅과 CPU별 SGI/PPI/LPI 설정을 같은 경로로 그리지 않습니다.

주의

GICR_WAKER sleep/wake는 gic_cpu_init/CPU PM 경로이며 일반 SPI의 매 IRQ 단계가 아닙니다.

주의

장치 원인 해소와 EOImode별 priority drop·active 해제를 분리합니다.

계측과 검증

먼저 /proc/interrupts의 Linux IRQ 이름과 CPU별 증가량을 통제된 장치 작업 전후로 비교합니다. action handler의 입출구는 irq trace event로 보되, 이 수치만으로 IAR 읽기·EOIR/DIR 또는 GIC 초기화 시점을 측정했다고 말하지 않습니다.

계측 지점이 Linux handler 경계인지, CPU interface register 동작인지 구분합니다.

관찰 수단확인 가능한 범위와 한계
/proc/interruptsLinux IRQ 이름·CPU별 카운트 증가를 봅니다. IAR/EOI 시간이나 내부 init 단계는 직접 나타내지 않습니다.
irq:irq_handler_entry/exitLinux action handler 주변을 관찰합니다. 하드웨어 수신부터의 전체 지연이나 IRQ thread 전체 실행 시간은 아닙니다.
ftrace 함수 필터available_filter_functions에서 gic_handle_irq 등 실제 trace 가능한 함수를 먼저 확인합니다. inline helper를 가정하지 않습니다.
GIC 레지스터 직접 접근IAR/EOIR/DIR을 임의로 읽거나 쓰거나 devmem으로 GIC MMIO를 시험 삼아 건드리지 않습니다. acknowledge·완료·mask 상태를 바꿀 수 있습니다.
시험 장비에서의 관찰 예시:
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

추가 확인 사항

  • 이 장치의 DT SPI 번호를 GIC INTID로 바꾸면 얼마이며, Linux IRQ 번호는 어떻게 매핑되는가?
  • 이 CPU에서 일반 IRQ와 pseudo-NMI가 갈라지는 조건은 무엇인가?
  • 1020~1023 special INTID 또는 매핑 실패가 일반 irq_desc 처리와 어떻게 구분되는가?
  • EOImode 0/1의 priority drop·active 해제와 장치 원인 clear를 어떤 관찰로 분리할 수 있는가?