ARM GIC v3 (Generic Interrupt Controller v3)

GICv2는 아키텍처상 PE(Processing Element, 코어)를 최대 8개까지만 지원한다. 서버·모바일 SoC의 코어 수가 계속 늘어나면서 이 상한이 실제 병목이 됐고, ARM은 GICv3에서 Distributor 하나가 모든 코어를 직접 상대하던 구조를 버리고 코어마다 전용 Redistributor를 붙이는 방식으로 이 제한을 없앴다. 가상화 지원도 리스트 레지스터 기반의 제한적인 방식에서 직접 주입(direct injection) 방식으로 강화됐다. 이 글에서는 GICv3의 구성 요소와 디바이스 트리 설정, 그리고 리눅스 커널 irqchip 드라이버의 초기화 흐름을 실제 커널 소스 기준으로 정리한다.

GICv3의 구성 요소

  • Distributor (GICD) — SPI(Shared Peripheral Interrupt)처럼 시스템 전역에 걸친 인터럽트를 관리한다.
  • Redistributor (GICR) — PE마다 하나씩 붙어 SGI(Software Generated Interrupt)와 PPI(Private Peripheral Interrupt), LPI를 처리한다. GICv2에는 없던 컴포넌트다.
  • CPU 인터페이스 (GICC / 시스템 레지스터) — Redistributor가 넘긴 인터럽트를 실제 코어가 받아들이는 지점이다.
  • ITS (Interrupt Translation Service) — 메시지 시그널 인터럽트(MSI)와 LPI를 지원한다. PCIe 디바이스처럼 대량의 인터럽트를 동적으로 할당해야 하는 환경에서 필요하다.

GICv2 vs GICv3

특징GICv2GICv3
PE(코어) 수 제한최대 8개 (아키텍처 상한)PE마다 Redistributor가 붙는 구조라 8개 제한이 없음
SPI(공유 인터럽트)최대 1020개기존 1020개 + Extended SPI(ESPI) 추가 지원
Redistributor없음 (Distributor가 전담)PE마다 전용 Redistributor(GICR)
가상화 지원제한적 (리스트 레지스터 기반)direct injection 등 강화된 가상화 지원
LPI 지원지원 안 함ITS를 통해 지원

SPI 상한이 “1020개 vs 1024개”로 소개되는 자료도 있는데, 실제 커널 소스(drivers/irqchip/irq-gic-v3.c)를 보면 GICv3도 GIC_LINE_NRmin(SPI 개수, 1020)으로 계산해 기존 SPI 자체의 상한은 GICv2와 동일하게 1020개로 잡는다. GICv3가 실제로 확장한 부분은 이 1020개 위에 Extended SPI(ESPI)라는 별도 풀을 추가로 지원한다는 점이다.

디바이스 트리 설정

리눅스 커널은 GICv3 설정을 디바이스 트리에서 읽어온다. 아래는 커널 공식 바인딩 문서(Documentation/devicetree/bindings/interrupt-controller/arm,gic-v3.yaml)에 실린 예제다.

gic: interrupt-controller@2cf00000 {
  compatible = "arm,gic-v3";
  #interrupt-cells = <3>;
  #address-cells = <1>;
  #size-cells = <1>;
  ranges;
  interrupt-controller;
  reg = <0x2f000000 0x10000>,  // GICD
        <0x2f100000 0x200000>,  // GICR
        <0x2c000000 0x2000>,  // GICC
        <0x2c010000 0x2000>,  // GICH
        <0x2c020000 0x2000>;  // GICV
  interrupts = <1 9 4>;

  msi-controller;
  mbi-ranges = <256 128>;

  msi-controller@2c200000 {
    compatible = "arm,gic-v3-its";
    msi-controller;
    #msi-cells = <1>;
    reg = <0x2c200000 0x20000>;
  };
};

reg의 다섯 항목이 각각 GICD(Distributor)·GICR(Redistributor)·GICC/GICH/GICV(레거시 CPU 인터페이스 계열 — GICv2 호환 모드용)에 대응하고, 하위 노드로 붙은 msi-controller가 ITS다. Redistributor 영역이 하나뿐인 단순 구성이며, SoC에 따라 #redistributor-regions로 영역을 여러 개 나누는 구성도 쓰인다.

리눅스 커널 초기화 흐름 — irq-gic-v3.c

GICv3 드라이버는 drivers/irqchip/irq-gic-v3.c에 있고, 디바이스 트리 기반 부팅에서는 gic_of_init()이 진입점이다.

static int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
	phys_addr_t dist_phys_base;
	void __iomem *dist_base;
	struct redist_region *rdist_regs;
	struct resource res;
	u64 redist_stride;
	u32 nr_redist_regions;
	int err, i;

	dist_base = gic_of_iomap(node, 0, "GICD", &res);
	if (IS_ERR(dist_base)) {
		pr_err("%pOF: unable to map gic dist registers\n", node);
		return PTR_ERR(dist_base);
	}

	dist_phys_base = res.start;

	err = gic_validate_dist_version(dist_base);
	if (err) {
		pr_err("%pOF: no distributor detected, giving up\n", node);
		goto out_unmap_dist;
	}

	if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
		nr_redist_regions = 1;

	rdist_regs = kzalloc_objs(*rdist_regs, nr_redist_regions);
	if (!rdist_regs) {
		err = -ENOMEM;
		goto out_unmap_dist;
	}

	for (i = 0; i < nr_redist_regions; i++) {
		rdist_regs[i].redist_base = gic_of_iomap(node, 1 + i, "GICR", &res);
		if (IS_ERR(rdist_regs[i].redist_base)) {
			pr_err("%pOF: couldn't map region %d\n", node, i);
			err = -ENODEV;
			goto out_unmap_rdist;
		}
		rdist_regs[i].phys_base = res.start;
	}

	if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
		redist_stride = 0;

	gic_enable_of_quirks(node, gic_quirks, &gic_data);

	err = gic_init_bases(dist_phys_base, dist_base, rdist_regs,
			     nr_redist_regions, redist_stride, &node->fwnode);
	if (err)
		goto out_unmap_rdist;

	gic_populate_ppi_partitions(node);

	if (static_branch_likely(&supports_deactivate_key))
		gic_of_setup_kvm_info(node, nr_redist_regions);
	return 0;

out_unmap_rdist:
	for (i = 0; i < nr_redist_regions; i++)
		if (!IS_ERR_OR_NULL(rdist_regs[i].redist_base))
			iounmap(rdist_regs[i].redist_base);
	kfree(rdist_regs);
out_unmap_dist:
	iounmap(dist_base);
	return err;
}

IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);

디바이스 트리에서 Distributor(GICD)와 Redistributor(GICR) 영역을 각각 매핑하고, gic_validate_dist_version()으로 실제 하드웨어가 GICv3/v4인지 확인한 뒤 gic_init_bases()로 넘어간다. IRQCHIP_DECLARE 매크로는 이 함수를 "arm,gic-v3" compatible 문자열과 연결해, 디바이스 트리에 이 문자열이 있으면 커널이 자동으로 gic_of_init()을 호출하도록 등록한다.

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);

	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 (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_cpu_sys_reg_enable();
	gic_prio_init();
	gic_dist_init();
	gic_cpu_init();
	gic_enable_nmi_support();
	gic_smp_init();
	gic_cpu_pm_init();

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

	return 0;

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

흐름을 요약하면 다음과 같다. Distributor의 GICD_TYPER 레지스터를 읽어 SPI/ESPI 개수를 파악하고, irq_domain_create_tree()로 IRQ 도메인을 만든 뒤, gic_dist_init()/gic_cpu_init()으로 Distributor와 현재 CPU의 Redistributor를 초기화한다. 마지막으로 gic_dist_supports_lpis()가 참이면 its_init()을 호출해 MSI/LPI를 처리할 ITS를 켜고, 그렇지 않은 구형 플랫폼이면 GICv2 호환 MSI 처리기(gicv2m_init())로 대체한다.

주의사항

  • SPI 상한은 GICv2·GICv3 모두 1020개로 동일하다. GICv3의 확장은 그 위에 얹는 Extended SPI(ESPI) 지원에서 온다는 점을 “1020 vs 1024” 식으로 단순화해 오해하지 않아야 한다.
  • 디바이스 트리의 reg 순서(GICD → GICR → GICC/GICH/GICV)는 바인딩 문서에 정의된 고정 순서다. SoC 벤더 데이터시트의 레지스터 오프셋을 그대로 옮기다 순서를 뒤바꾸는 실수가 흔하다.
  • 커널 소스는 계속 바뀐다. 이 글의 gic_of_init()도 예전에는 kcalloc()으로 Redistributor 배열을 할당했지만 최신 트리에서는 kzalloc_objs() 헬퍼로 바뀌어 있었다 — 예전 블로그 글이나 오래된 커밋의 코드를 그대로 인용하기보다 그때그때 실제 소스를 확인하는 게 안전하다.
  • ITS는 모든 SoC에 있는 게 아니다. gic_dist_supports_lpis()가 거짓이면 ITS 없이 GICv2 호환 MSI 경로로 빠지므로, MSI 기반 디바이스를 붙일 계획이라면 대상 SoC가 실제로 ITS를 갖췄는지 데이터시트로 먼저 확인해야 한다.

마무리

GICv3의 핵심은 결국 “Distributor 하나가 모든 CPU를 상대하던 구조를 CPU마다 Redistributor를 두는 구조로 바꿨다”는 한 문장으로 요약된다. 이 구조 변경 덕분에 GICv2의 8코어 상한이 사라졌고, ITS를 통한 LPI/MSI 지원까지 더해지면서 코어 수가 많고 PCIe 같은 동적 인터럽트 소스가 많은 최신 SoC에 맞는 인터럽트 컨트롤러로 자리잡았다. 커널 코드 흐름 자체는 디바이스 트리 파싱 → Distributor/Redistributor 초기화 → ITS 등록의 3단계로 비교적 단순하니, 새 SoC에 GICv3를 붙일 때는 이 순서대로 디바이스 트리 값을 검증해나가면 된다.

참고

답글 남기기