ARM GIC v3 in Linux Kernel

ARM GICv3의 컴포넌트 구성과 초기화 흐름은 별도 글에서 다뤘다. 그런데 막상 각 레지스터를 읽고 쓰는 실제 커널 코드를 보면 writel_relaxed() 한 줄로 끝나는 경우가 드물다. 하드웨어에 값을 쓴 뒤 그 상태가 실제로 반영됐는지 폴링으로 확인하거나, 명령어 배리어를 끼워 넣는 코드가 거의 항상 따라붙는다. 이 글에서는 Distributor·Redistributor·CPU 인터페이스·ITS 네 컴포넌트의 핵심 레지스터를 실제 커널 소스(drivers/irqchip/irq-gic-v3.c) 기준으로 정리하고, 왜 단순 MMIO 쓰기만으로는 부족한지 살펴본다.

Distributor 레지스터 — 인터럽트 마스크/언마스크

레지스터오프셋역할
GICD_CTLR0x0000Distributor 활성화/비활성화, 그룹별 인터럽트 활성화
GICD_TYPER0x0004지원하는 인터럽트 개수·CPU 개수 등 하드웨어 속성 조회
GICD_ISENABLER0x0100인터럽트 활성화(Set-Enable)
GICD_ICENABLER0x0180인터럽트 비활성화(Clear-Enable)
GICD_IPRIORITYR0x0400인터럽트 우선순위 설정
GICD_ITARGETSR0x0800인터럽트 타깃 CPU 설정 (GICv2 호환 모드)
GICD_ICFGR0x0C00트리거 방식 설정(레벨/엣지)

실제 인터럽트 마스크/언마스크는 gic_poke_irq()라는 공통 헬퍼를 통한다. SPI냐 PPI/SGI/LPI냐에 따라 베이스 주소 자체가 Distributor인지 Redistributor인지 갈리기 때문이다.

static void gic_poke_irq(struct irq_data *d, u32 offset)
{
	void __iomem *base;
	u32 index, mask;

	offset = convert_offset_index(d, offset, &index);
	mask = 1 << (index % 32);

	if (gic_irq_in_rdist(d))
		base = gic_data_rdist_sgi_base();
	else
		base = gic_data.dist_base;

	writel_relaxed(mask, base + offset + (index / 32) * 4);
}

static void gic_mask_irq(struct irq_data *d)
{
	gic_poke_irq(d, GICD_ICENABLER);
	if (gic_irq_in_rdist(d))
		gic_redist_wait_for_rwp();
	else
		gic_dist_wait_for_rwp();
}

static void gic_unmask_irq(struct irq_data *d)
{
	gic_poke_irq(d, GICD_ISENABLER);
}

gic_mask_irq()gic_unmask_irq()와 다른 점은 gic_redist_wait_for_rwp()/gic_dist_wait_for_rwp() 호출이다. GICD_ICENABLER에 값을 쓴다고 그 순간 인터럽트가 즉시 막히는 게 아니라서, 마스크가 확실히 걸렸는지 RWP(Register Write Pending) 비트가 꺼질 때까지 기다린 뒤에야 함수가 반환된다. 반대로 언마스크는 이 대기가 없다 — 조금 늦게 반영돼도 인터럽트를 하나 더 받는 정도로 끝나지만, 마스크가 늦게 반영되면 막았다고 착각한 인터럽트가 계속 들어올 수 있기 때문이다.

Redistributor 레지스터 — 슬립 해제

레지스터오프셋역할
GICR_CTLR0x0000GICD_CTLR와 동일 오프셋(별칭)
GICR_WAKER0x0014해당 CPU의 Redistributor 슬립 상태 제어
GICR_PROPBASER0x0070LPI 속성(property) 테이블 기본 주소
GICR_PENDBASER0x0078LPI 펜딩(pending) 테이블 기본 주소
GICR_ISENABLER00x0100GICD_ISENABLER와 동일 오프셋(별칭), PPI/SGI 활성화

부팅 시 각 CPU의 Redistributor는 잠들어 있는 상태라, gic_enable_redist()GICR_WAKER를 조작해 깨운다. 핵심 로직만 추리면 다음과 같다.

static void gic_enable_redist(bool enable)
{
	void __iomem *rbase;
	u32 val;
	int ret;

	rbase = gic_data_rdist_rd_base();

	val = readl_relaxed(rbase + GICR_WAKER);
	if (enable)
		val &= ~GICR_WAKER_ProcessorSleep;
	else
		val |= GICR_WAKER_ProcessorSleep;
	writel_relaxed(val, rbase + GICR_WAKER);

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

ProcessorSleep 비트를 지워 깨우라는 요청을 보낸 뒤, 실제로 ChildrenAsleep 비트가 그에 맞춰 바뀔 때까지 readl_relaxed_poll_timeout_atomic()으로 최대 1초간 폴링한다. 슬립 해제가 비동기 하드웨어 동작이라 값을 쓰는 즉시 완료를 보장할 수 없기 때문이다.

CPU 인터페이스 레지스터 — 인터럽트 수신과 EOI

레지스터EL역할
ICC_IAR1_EL1EL1활성화된 인터럽트 ID 조회(Acknowledge)
ICC_EOIR1_EL1EL1인터럽트 처리 완료 통지
ICC_IGRPEN1_EL1EL1Group 1 인터럽트 활성화
ICC_CTLR_EL1EL1CPU 인터페이스 제어·우선순위 설정

이 레지스터들은 MMIO가 아니라 AArch64 시스템 레지스터라서 readl_relaxed() 대신 read_gicreg()/write_gicreg() 매크로로 접근한다.

static inline u64 gic_read_iar_common(void)
{
	u64 irqstat;

	irqstat = read_sysreg_s(SYS_ICC_IAR1_EL1);
	dsb(sy);
	return irqstat;
}

static void gic_eoi_irq(struct irq_data *d)
{
	write_gicreg(irqd_to_hwirq(d), ICC_EOIR1_EL1);
	isb();
}

gic_read_iar_common()은 인터럽트 ID를 읽은 직후 dsb(sy) 배리어를 끼운다. ID를 읽는 것 자체가 해당 인터럽트를 “Acknowledge” 상태로 바꾸는 부수 효과가 있어서, 그 뒤에 이어지는 인터럽트 처리 코드가 배리어 없이 먼저 실행되면 순서가 꼬일 수 있기 때문이다. gic_eoi_irq()도 마찬가지로 EOI 신호를 보낸 뒤 isb()로 명령어 파이프라인을 정리한다. 두 함수 모두 실제로는 침입 방지 에라타 처리 분기가 더 있는데, 핵심 흐름만 추렸다.

ITS 레지스터 — MSI/LPI

레지스터오프셋역할
GITS_CTLR0x0000ITS 활성화
GITS_CBASER0x0080명령 큐(command queue) 기본 주소
GITS_CWRITER0x0088명령 큐 쓰기 포인터

ITS 구현은 drivers/irqchip/irq-gic-v3-its.c 하나에만 5,000줄이 넘게 들어 있을 만큼 복잡하다(명령 큐 기반 비동기 프로토콜로 디바이스 테이블·컬렉션을 관리한다). 다만 활성화 자체는 다른 레지스터들과 같은 패턴이다 — GITS_CTLR을 읽어 GITS_CTLR_ENABLE 비트를 세우고 다시 쓴다.

ctlr = readl_relaxed(its->base + GITS_CTLR);
ctlr |= GITS_CTLR_ENABLE;
writel_relaxed(ctlr, its->base + GITS_CTLR);

irq_chip 구조체로 엮이는 지점

지금까지 본 gic_mask_irq/gic_unmask_irq/gic_eoi_irq는 결국 irq_chip 구조체의 콜백으로 등록되어, 제네릭 IRQ 서브시스템이 이 함수들을 호출하는 구조다.

static struct irq_chip gic_chip = {
	.name			= "GICv3",
	.irq_mask		= gic_mask_irq,
	.irq_unmask		= gic_unmask_irq,
	.irq_eoi		= gic_eoi_irq,
	.irq_set_type		= gic_set_type,
	.irq_set_affinity	= gic_set_affinity,
	.irq_retrigger          = gic_retrigger,
};

드라이버를 처음 읽을 때는 이 구조체를 먼저 찾아서, 필드에 연결된 함수부터 하나씩 따라가는 순서가 전체 흐름을 파악하기에 편하다.

주의사항

  • 레지스터에 값을 쓰는 것과 하드웨어에 그 변경이 실제로 반영되는 것은 별개다. 마스크처럼 “이후 동작이 이 변경에 의존하는” 경우는 반드시 RWP 대기나 폴링으로 완료를 확인해야 한다. 언마스크처럼 늦게 반영돼도 무해한 경우만 대기 없이 넘어간다.
  • CPU 인터페이스 레지스터(ICC_*_EL1)는 일반 read_sysreg()/write_sysreg()가 아니라 GIC 전용 read_gicreg()/write_gicreg() 매크로로 접근한다. 겉보기엔 비슷해 보여도 실제 커널 소스의 명칭과 다르면 검색·grep이 안 먹히니 실제 매크로명을 확인하고 인용하는 게 안전하다.
  • GICR_WAKER_ProcessorSleep처럼 비트 필드 매크로는 대소문자가 섞인 CamelCase로 정의된 것들이 있다. 스네이크케이스로 바꿔 적으면(예: GICR_WAKER_PROCESSOR_SLEEP) 실제 헤더에 없는 이름이라 컴파일이 안 된다.
  • 이 글의 코드는 가독성을 위해 에라타 워크어라운드·에러 처리 일부를 생략한 발췌본이다. 실제로 드라이버를 수정한다면 반드시 원본 소스 전체를 확인해야 한다.

마무리

GICv3 레지스터 하나하나는 오프셋과 역할만 놓고 보면 단순하지만, 실제 드라이버 코드는 비동기 하드웨어 상태 전파를 기다리는 폴링과 명령어 배리어로 채워져 있다. 특히 마스크/언마스크처럼 겉보기엔 대칭적인 두 함수가 대기 로직 유무에서 갈리는 지점은, 레지스터 스펙만 읽어서는 알기 어렵고 실제 드라이버 코드를 봐야 드러나는 부분이다. 새 SoC에 인터럽트 관련 버그가 있다면, 우선 이 대기·배리어 로직이 하드웨어 요구사항과 맞게 동작하는지부터 의심해볼 만하다.

참고

답글 남기기