IRQ 도메인
Linux 커널의 IRQ 도메인 인프라를 심층 분석합니다. 하드웨어 IRQ 번호를 Linux 가상 IRQ로 매핑하는 추상화 계층, 인터럽트 컨트롤러 드라이버, Device Tree/ACPI 통합, MSI/MSI-X 처리까지 다룹니다.
핵심 목표는 "하드웨어 번호 공간을 커널 공통 IRQ 모델로 안전하게 연결하는 방법"을 이해하는 것입니다. 따라서 irq_domain 생성부터 매핑 방식(linear, radix, tree), 계층형 도메인(parent/child) 설계, 펌웨어(Device Tree, ACPI MADT) 정보 파싱, MSI 도메인 확장까지 단계별로 다루며, 컨트롤러 드라이버 작성 시 자주 발생하는 매핑 누락·중복 할당·해제 순서 오류를 실제 점검 항목 중심으로 정리했습니다.
핵심 요약
- irq_domain — hwirq를 virq로 매핑하는 추상화 계층 (인터럽트 컨트롤러 1개당 1개)
- irq_chip — 인터럽트 컨트롤러의 하드웨어 제어 콜백 (mask/unmask/ack/eoi)
- irq_data — virq와 hwirq를 연결하는 per-IRQ 메타데이터 구조체
- 계층적 도메인 — 중첩 컨트롤러(MSI → ITS → GIC)를 parent/child 트리로 표현
- 매핑 타입 — Linear(배열), Tree(radix), No-Map(레거시), Hierarchy(다계층)
단계별 이해
- 구조체 파악
irq_domain,irq_chip,irq_data세 구조체의 관계를 먼저 이해합니다. - 매핑 흐름 추적
펌웨어 파싱(DT/ACPI) → xlate → map → virq 할당까지의 경로를 따라갑니다. - 계층 구조 이해
MSI → ITS → GIC처럼 도메인이 중첩되는 경우의 alloc/free 체인을 확인합니다. - 디버깅 방법 습득
/proc/interrupts,/sys/kernel/debug/irq/, ftrace 활용법을 익힙니다.
개요
IRQ 도메인(IRQ Domain)은 하드웨어 인터럽트 번호(hwirq)를 Linux 가상 IRQ 번호(virq)로 매핑하는 추상화 계층입니다.
왜 IRQ 도메인이 필요한가?
IRQ 번호 충돌 문제
SoC(System-on-Chip)는 여러 인터럽트 컨트롤러를 포함합니다:
- Main GIC: hwirq 0-255 (ARM)
- GPIO 컨트롤러: hwirq 0-31 (각 GPIO 뱅크마다)
- PCIe MSI: hwirq 0-1023 (디바이스마다)
각 컨트롤러가 독립적인 hwirq 번호를 사용하므로 번호가 겹칩니다. IRQ 도메인은 이들을 중복 없는 Linux virq 공간으로 변환합니다.
Flat IRQ에서 도메인 기반으로의 진화
리눅스 커널의 인터럽트 번호 관리는 크게 세 시대를 거쳤습니다.
| 시대 | 커널 버전 | 방식 | 한계 |
|---|---|---|---|
| Flat IRQ | ~2.6.37 | 전역 정수 배열 NR_IRQS | 컨트롤러 간 번호 충돌, 동적 확장 불가 |
| IRQ Domain 도입 | 3.1+ | 컨트롤러별 독립 번호 공간 | 계층 지원 부족 |
| Hierarchy Domain | 3.11+ | parent/child 트리 구조 | 현재 표준 (MSI, IOMMU, GPIO 통합) |
Flat IRQ 시절에는 NR_IRQS를 플랫폼마다 고정값으로 정의했습니다. ARM SoC가 다양해지면서 hwirq 번호가 컨트롤러 간에 겹치는 문제가 심각해졌고, 이를 해결하기 위해 irq_domain 추상화가 도입되었습니다.
핵심 개념
| 개념 | 설명 | 예제 |
|---|---|---|
| hwirq | 하드웨어 인터럽트 번호 | GIC의 SPI 32번 |
| virq | Linux 가상 IRQ 번호 | request_irq(45, ...) |
| IRQ 도메인 | hwirq → virq 매핑 관리자 | GIC 도메인, GPIO 도메인 |
| irq_chip | 인터럽트 컨트롤러 ops | mask/unmask/ack 함수 |
| irq_data | virq-hwirq 바인딩 메타데이터 | chip, domain, hwirq, mask 정보 |
| fwnode | 펌웨어 노드 핸들 | DT device_node, ACPI fwnode |
hwirq → virq 매핑 흐름
Device Tree 또는 ACPI에서 인터럽트 정보를 파싱하여 virq를 할당하기까지의 전체 과정입니다.
irq_domain 구조체
IRQ 도메인은 struct irq_domain으로 표현됩니다.
/* include/linux/irqdomain.h */
struct irq_domain {
struct list_head link;
const char *name;
const struct irq_domain_ops *ops;
void *host_data; /* 컨트롤러별 private 데이터 */
unsigned int flags;
unsigned int mapcount; /* 매핑된 IRQ 수 */
/* hwirq → virq 매핑 방식 */
struct irq_domain_chip_generic *gc;
struct irq_data *linear_revmap[]; /* Linear 매핑용 */
struct radix_tree_root revmap_tree; /* Radix 매핑용 */
/* 계층적 도메인 */
struct irq_domain *parent;
struct fwnode_handle *fwnode; /* Device Tree/ACPI */
};
irq_data 구조체
irq_data는 각 인터럽트 라인의 메타데이터를 담으며, irq_desc 안에 내장됩니다. 계층형 도메인에서는 parent_data를 통해 체인을 형성합니다.
/* include/linux/irq.h */
struct irq_data {
u32 mask; /* 프리셋 비트마스크 */
unsigned int irq; /* virq 번호 */
unsigned long hwirq; /* hwirq 번호 */
struct irq_common_data *common;
struct irq_chip *chip; /* 이 IRQ를 제어하는 chip */
struct irq_domain *domain; /* 소속 도메인 */
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data; /* 부모 도메인의 irq_data */
#endif
void *chip_data; /* chip 고유 데이터 */
};
irq_desc는 virq별로 하나씩 존재하는 최상위 디스크립터이며, irq_data는 그 안에 포함된 도메인별 메타데이터입니다.
계층형 도메인에서는 한 virq에 대해 여러 irq_data가 parent_data 포인터로 연결됩니다 (예: MSI irq_data → ITS irq_data → GIC irq_data).
irq_domain_ops 콜백
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token);
int (*map)(struct irq_domain *d, unsigned int virq,
irq_hw_number_t hwirq);
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
irq_hw_number_t *out_hwirq, unsigned int *out_type);
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
};
irq_domain_ops 콜백 시퀀스
각 콜백이 호출되는 시점과 순서를 타임라인으로 나타냅니다.
매핑 타입
IRQ 도메인은 여러 매핑 전략을 지원합니다.
매핑 방식 비교
| 타입 | 자료구조 | 메모리 | 조회 속도 | 적합한 경우 |
|---|---|---|---|---|
| Linear | 배열 | O(N) | O(1) | hwirq가 0부터 연속 (GPIO, 대부분의 IC) |
| Tree | Radix Tree | O(log N) | O(log N) | hwirq가 sparse (PCIe MSI) |
| No-Map | 없음 | O(1) | - | hwirq == virq (레거시) |
| Hierarchy | 다계층 | 가변 | 가변 | 중첩된 컨트롤러 (MSI → GIC) |
메모리/성능 트레이드오프
| 항목 | Linear | Tree (Radix) | No-Map |
|---|---|---|---|
| 초기 메모리 | size * sizeof(irq_data *) | radix node만큼 | 0 |
| 조회 시 캐시 | 배열 인덱싱 (1회 접근) | 트리 순회 (3-4회 접근) | 직접 매핑 |
| hwirq 밀도 90%+ | 최적 | 메모리 낭비 | 불가 (중복 시) |
| hwirq 밀도 1% 미만 | 메모리 낭비 | 최적 | 불가 |
| 동적 확장 | 불가 (고정 크기) | 자동 확장 | 불필요 |
| 대표 사용처 | GIC(SPI 1020개), GPIO | MSI-X(최대 2048) | x86 레거시 16개 |
Linear 도메인 생성
/* kernel/irq/irqdomain.c */
struct irq_domain *
irq_domain_add_linear(struct device_node *of_node,
unsigned int size,
const struct irq_domain_ops *ops,
void *host_data)
{
struct irq_domain *domain;
domain = __irq_domain_add(of_node, size, size, 0, ops, host_data);
if (!domain)
return NULL;
/* linear_revmap 배열 할당 (size 크기) */
domain->revmap_type = IRQ_DOMAIN_MAP_LINEAR;
return domain;
}
Tree 도메인 생성
struct irq_domain *
irq_domain_add_tree(struct device_node *of_node,
const struct irq_domain_ops *ops,
void *host_data)
{
struct irq_domain *domain = __irq_domain_add(of_node, 0, ~0, 0,
ops, host_data);
if (domain) {
INIT_RADIX_TREE(&domain->revmap_tree, GFP_KERNEL);
domain->revmap_type = IRQ_DOMAIN_MAP_TREE;
}
return domain;
}
irq_chip 구조체
irq_chip은 인터럽트 컨트롤러의 하드웨어 제어 인터페이스입니다.
irq_chip 정의
/* include/linux/irq.h */
struct irq_chip {
const char *name;
/* 인터럽트 마스크 제어 */
void (*irq_mask)(struct irq_data *data);
void (*irq_unmask)(struct irq_data *data);
void (*irq_mask_ack)(struct irq_data *data);
/* 인터럽트 ACK */
void (*irq_ack)(struct irq_data *data);
void (*irq_eoi)(struct irq_data *data); /* End of Interrupt */
/* 트리거 타입 설정 */
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
/* CPU affinity */
int (*irq_set_affinity)(struct irq_data *data,
const struct cpumask *dest,
bool force);
/* Wake-up 지원 */
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
};
Callback 역할
| 함수 | 호출 시점 | 하드웨어 동작 |
|---|---|---|
| irq_mask | 인터럽트 비활성화 | 컨트롤러의 마스크 레지스터 설정 |
| irq_unmask | 인터럽트 활성화 | 컨트롤러의 언마스크 레지스터 설정 |
| irq_ack | 인터럽트 핸들러 시작 | Edge-triggered: 펜딩 클리어 |
| irq_eoi | 인터럽트 핸들러 종료 | Level-triggered: EOI 레지스터 쓰기 |
| irq_set_type | 인터럽트 등록 시 | Edge/Level, Rising/Falling 설정 |
| irq_set_affinity | CPU 친화성 변경 | 타겟 CPU 설정 (멀티코어) |
도메인 생명주기
IRQ 도메인은 생성 → 전역 리스트 등록 → 매핑 사용 → 매핑 해제 → 도메인 제거의 생명주기를 거칩니다.
도메인 생성 내부 코드
/* kernel/irq/irqdomain.c - __irq_domain_create 내부 */
static struct irq_domain *
__irq_domain_create(struct fwnode_handle *fwnode,
unsigned int size,
irq_hw_number_t hwirq_max,
int direct_max,
const struct irq_domain_ops *ops,
void *host_data)
{
struct irq_domain *domain;
domain = kzalloc_node(
struct_size(domain, linear_revmap, size),
GFP_KERNEL, of_node_to_nid(to_of_node(fwnode)));
if (!domain)
return NULL;
domain->ops = ops;
domain->host_data = host_data;
domain->fwnode = fwnode_handle_get(fwnode);
domain->hwirq_max = hwirq_max;
/* fwnode에서 이름 추출 */
if (is_fwnode_irqchip(fwnode))
domain->name = kasprintf(GFP_KERNEL, "%s",
fwnode->ops->get_name(fwnode));
return domain;
}
irq_domain_remove()를 호출하기 전에 도메인에 매핑된 모든 virq를 irq_dispose_mapping()으로 해제해야 합니다. 매핑이 남아 있는 상태에서 도메인을 제거하면 WARN_ON(domain->mapcount)이 발생합니다.
Device Tree 통합
Device Tree는 하드웨어 인터럽트 연결 정보를 기술합니다.
Device Tree 예제
/* arch/arm64/boot/dts/example.dtsi */
gic: interrupt-controller@8000000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x0 0x8000000 0 0x10000>, /* GICD */
<0x0 0x80a0000 0 0xf60000>; /* GICR */
};
uart0: serial@9000000 {
compatible = "arm,pl011";
reg = <0x0 0x9000000 0x0 0x1000>;
interrupts = <GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;
/* ^타입 ^hwirq ^트리거 */
interrupt-parent = <&gic>;
};
인터럽트 파싱
/* drivers/of/irq.c */
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
{
struct of_phandle_args oirq;
/* 1. Device Tree에서 interrupts 속성 파싱 */
if (of_irq_parse_one(dev, index, &oirq))
return 0;
/* 2. IRQ 도메인 찾기 (interrupt-parent) */
/* 3. xlate 콜백으로 hwirq 추출 */
/* 4. 매핑 생성 (또는 기존 virq 반환) */
return irq_create_of_mapping(&oirq);
}
ARM GIC 예제
ARM GIC(Generic Interrupt Controller)의 IRQ 도메인 구현을 살펴봅니다.
GIC 드라이버 초기화
/* drivers/irqchip/irq-gic-v3.c */
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
irq_hw_number_t hw)
{
struct irq_chip *chip = &gic_chip;
if (hw < 32) { /* SGI/PPI */
irq_set_percpu_devid(irq);
irq_domain_set_info(d, irq, hw, chip, d->host_data,
handle_percpu_devid_irq, NULL, NULL);
} else { /* SPI */
irq_domain_set_info(d, irq, hw, chip, d->host_data,
handle_fasteoi_irq, NULL, NULL);
irq_set_probe(irq);
}
return 0;
}
static const struct irq_domain_ops gic_irq_domain_ops = {
.map = gic_irq_domain_map,
.unmap = gic_irq_domain_unmap,
.xlate = irq_domain_xlate_twocell,
};
static int gic_init_bases(void __iomem *dist_base, struct fwnode_handle *handle)
{
struct irq_domain *gic_domain;
/* IRQ 도메인 생성 (Linear, 0-1019) */
gic_domain = irq_domain_create_linear(handle, gic_irqs,
&gic_irq_domain_ops,
gic_data);
return 0;
}
GIC 인터럽트 타입
| 타입 | hwirq 범위 | 설명 |
|---|---|---|
| SGI (Software Generated) | 0-15 | IPI (Inter-Processor Interrupt) |
| PPI (Private Peripheral) | 16-31 | CPU별 타이머, PMU 등 |
| SPI (Shared Peripheral) | 32-1019 | 공유 디바이스 인터럽트 |
| LPI (Locality-specific Peripheral) | 8192+ | MSI/MSI-X (GICv3 ITS) |
GIC vs APIC 비교
ARM과 x86의 대표적인 인터럽트 컨트롤러를 아키텍처 수준에서 비교합니다.
GIC vs APIC 상세 비교
| 항목 | ARM GICv3 | x86 APIC |
|---|---|---|
| 아키텍처 | ARM64 (AArch64) | x86_64 |
| 분배기 | GICD (Distributor) | I/O APIC |
| CPU별 컨트롤러 | GICR (Redistributor) | Local APIC (LAPIC) |
| MSI 지원 | ITS (Interrupt Translation Service) | 직접 LAPIC로 메모리 쓰기 |
| IRQ 도메인 계층 | GIC → ITS → MSI | Vector → I/O APIC, Vector → MSI |
| 인터럽트 라우팅 | affinity 비트 + GICR | RTE(Redirection Table Entry) |
| 최대 IRQ 수 | SPI 1020 + LPI 무제한 | I/O APIC 24 + Vector 256/CPU |
| EOI 메커니즘 | ICC_EOIR 시스템 레지스터 | LAPIC EOI 레지스터 |
| 펌웨어 기술 | Device Tree / ACPI (GICC) | ACPI MADT |
x86 APIC 예제
x86 시스템의 APIC(Advanced Programmable Interrupt Controller) 구현입니다.
I/O APIC 도메인
/* arch/x86/kernel/apic/io_apic.c */
static struct irq_chip ioapic_chip = {
.name = "IO-APIC",
.irq_startup = startup_ioapic_irq,
.irq_mask = mask_ioapic_irq,
.irq_unmask = unmask_ioapic_irq,
.irq_ack = irq_chip_ack_parent,
.irq_eoi = ioapic_ack_level,
.irq_set_affinity = ioapic_set_affinity,
};
static int mp_irqdomain_create(int ioapic)
{
struct irq_domain *parent;
struct mp_chip_data *data = ioapic_data[ioapic];
parent = irq_remapping_get_irq_domain();
if (!parent)
parent = x86_vector_domain;
/* 계층적 도메인 생성 (I/O APIC → Vector) */
data->irqdomain = irq_domain_create_hierarchy(
parent, 0, data->nr_irqs, data->fwnode,
&mp_ioapic_irqdomain_ops, data);
return data->irqdomain ? 0 : -ENOMEM;
}
계층적 도메인
복잡한 인터럽트 경로는 여러 도메인을 계층화하여 구현합니다.
계층 구조 예제
PCIe MSI → GIC 계층
MSI 도메인은 디바이스별 hwirq를 받아 GIC의 hwirq로 변환하고, 최종적으로 하나의 virq를 할당합니다.
다계층 트리 구조
실제 ARM64 서버에서는 Device → MSI → ITS → GIC → CPU로 이어지는 4단계 이상의 도메인 계층이 형성됩니다.
계층 도메인 생성
/* kernel/irq/irqdomain.c */
struct irq_domain *
irq_domain_create_hierarchy(struct irq_domain *parent,
unsigned int flags,
unsigned int size,
struct fwnode_handle *fwnode,
const struct irq_domain_ops *ops,
void *host_data)
{
struct irq_domain *domain;
domain = __irq_domain_create(fwnode, size, ~0, 0, ops, host_data);
if (!domain)
return NULL;
domain->parent = parent; /* 부모 도메인 연결 */
domain->flags |= flags;
return domain;
}
계층별 alloc/free 체인
계층형 도메인에서 alloc은 child → parent 방향으로 재귀 호출됩니다. free는 반대 방향(parent → child)으로 수행됩니다.
/* 계층형 alloc 재귀 호출 패턴 (MSI → ITS → GIC) */
static int msi_domain_alloc(struct irq_domain *domain,
unsigned int virq,
unsigned int nr_irqs, void *arg)
{
struct irq_alloc_info *info = arg;
int ret;
/* 1. 부모 도메인(ITS)에 alloc 위임 */
ret = irq_domain_alloc_irqs_parent(domain, virq, nr_irqs, arg);
if (ret)
return ret;
/* 2. 자신의 irq_chip/handler 설정 */
irq_domain_set_hwirq_and_chip(domain, virq, info->hwirq,
&pci_msi_controller, info);
return 0;
}
/* irq_domain_alloc_irqs_parent() 내부: */
int irq_domain_alloc_irqs_parent(struct irq_domain *domain,
unsigned int virq,
unsigned int nr_irqs, void *arg)
{
if (!domain->parent)
return -ENOSYS;
/* 부모의 alloc()을 재귀 호출 */
return domain->parent->ops->alloc(domain->parent, virq, nr_irqs, arg);
}
스택형 도메인 vs 계층형 도메인
| 항목 | 스택형 (Stacked/Chained) | 계층형 (Hierarchy) |
|---|---|---|
| virq 할당 | 최하위 도메인에서만 할당 | 최상위에서 할당, 체인으로 전파 |
| irq_chip 체인 | irq_set_chained_handler() | parent_data 포인터 |
| 핸들러 호출 | chained handler가 하위 도메인 호출 | irq_chip_*_parent() 위임 |
| 대표 사용처 | GPIO (v4.x 이전) | MSI, IOMMU, ITS (v3.11+) |
| 자원 관리 | 각 도메인 독립 | alloc/free 재귀 체인 |
| 권장 여부 | 레거시 (호환용) | 현재 표준 |
MSI/MSI-X 도메인
MSI(Message Signaled Interrupts)는 메모리 쓰기를 통해 인터럽트를 전달합니다.
MSI vs 전통적 IRQ
| 항목 | Legacy IRQ (INTx) | MSI/MSI-X |
|---|---|---|
| 전달 방식 | 전용 IRQ 핀 | 메모리 쓰기 (PCIe 트랜잭션) |
| IRQ 수 | 디바이스당 1-4개 | MSI: 1-32, MSI-X: 1-2048 |
| 공유 | 공유 가능 (레벨 트리거) | 항상 독점 |
| 성능 | 낮음 (공유 시 폴링) | 높음 (독점, 큐별 IRQ) |
| 라우팅 | 고정 | 동적 (CPU 지정) |
MSI Address/Data 레지스터
MSI 인터럽트는 PCIe 디바이스가 지정된 주소에 데이터를 기록하여 발생합니다.
/* MSI Address 레지스터 (x86) */
/* [31:20] = 0xFEE (고정, LAPIC 주소 범위) */
/* [19:12] = Destination ID (타겟 CPU의 APIC ID) */
/* [11:4] = 예약 */
/* [3] = RH (Redirection Hint) */
/* [2] = DM (Destination Mode: 0=Physical, 1=Logical) */
/* MSI Data 레지스터 */
/* [15] = Trigger Mode (0=Edge, 1=Level) */
/* [14] = Level (0=Deassert, 1=Assert) */
/* [7:0] = Vector 번호 (x86 IDT 인덱스) */
struct msi_msg {
u32 address_lo; /* 위 Address 레지스터 */
u32 address_hi; /* 64-bit 주소용 상위 32비트 */
u32 data; /* 위 Data 레지스터 */
};
MSI-X 도메인 계층 (ARM64 ITS)
ARM64에서 MSI-X는 GICv3 ITS(Interrupt Translation Service)를 통해 LPI로 변환됩니다.
/* drivers/irqchip/irq-gic-v3-its.c */
static const struct irq_domain_ops its_domain_ops = {
.alloc = its_irq_domain_alloc,
.free = its_irq_domain_free,
};
/* ITS alloc: DeviceID + EventID → LPI hwirq 매핑 */
static int its_irq_domain_alloc(struct irq_domain *domain,
unsigned int virq,
unsigned int nr_irqs, void *args)
{
/* 1. LPI 번호 할당 (8192+) */
/* 2. ITS 디바이스 테이블에 DeviceID 등록 */
/* 3. ITT(Interrupt Translation Table)에 EventID→LPI 매핑 */
/* 4. MAPD/MAPI/INV 커맨드로 ITS 하드웨어 설정 */
irq_domain_set_hwirq_and_chip(domain, virq, hwirq,
&its_irq_chip, its_dev);
return 0;
}
MSI 도메인 구현
/* drivers/pci/msi/irqdomain.c */
static struct irq_chip pci_msi_controller = {
.name = "PCI-MSI",
.irq_mask = pci_msi_mask_irq,
.irq_unmask = pci_msi_unmask_irq,
.irq_ack = irq_chip_ack_parent,
.irq_set_affinity = msi_domain_set_affinity,
};
static int pci_msi_domain_alloc_irqs(struct irq_domain *domain,
struct device *dev,
int nvec)
{
struct msi_desc *desc;
int virq, i = 0;
for_each_msi_entry(desc, dev) {
virq = __irq_domain_alloc_irqs(domain, -1, 1, dev_to_node(dev),
desc, false, NULL);
if (virq < 0)
return virq;
irq_set_msi_desc(virq, desc);
desc->irq = virq;
i++;
}
return 0;
}
GPIO IRQ 도메인
GPIO 컨트롤러는 자체 IRQ 도메인을 가지며, 각 GPIO 핀이 인터럽트 소스로 동작합니다.
gpiochip에 IRQ 도메인 연결
/* drivers/gpio/gpiolib.c */
static int gpiochip_add_irqchip(struct gpio_chip *gc,
struct lock_class_key *lock_key,
struct lock_class_key *request_key)
{
struct gpio_irq_chip *girq = &gc->irq;
struct irq_domain *domain;
if (girq->domain) {
/* 드라이버가 이미 도메인을 설정한 경우 */
return 0;
}
if (girq->parent_domain) {
/* 계층형: 부모 도메인(GIC 등)에 연결 */
domain = irq_domain_create_hierarchy(
girq->parent_domain, 0, gc->ngpio,
gc->fwnode, girq->parent_handler ?
&gpiochip_hierarchy_domain_ops : girq->domain_ops,
gc);
} else {
/* 비계층형: 독립 Linear 도메인 */
domain = irq_domain_create_simple(
gc->fwnode, gc->ngpio, girq->first,
girq->domain_ops ?: &gpiochip_domain_ops, gc);
}
girq->domain = domain;
return 0;
}
GPIO IRQ 드라이버 예제
/* 간단한 GPIO 컨트롤러 IRQ 도메인 설정 */
static const struct irq_chip my_gpio_irq_chip = {
.name = "my-gpio",
.irq_ack = my_gpio_irq_ack,
.irq_mask = my_gpio_irq_mask,
.irq_unmask = my_gpio_irq_unmask,
.irq_set_type = my_gpio_irq_set_type,
.flags = IRQCHIP_IMMUTABLE,
GPIOCHIP_IRQ_RESOURCE_HELPERS,
};
static int my_gpio_probe(struct platform_device *pdev)
{
struct my_gpio *mg;
struct gpio_irq_chip *girq;
mg->gc.ngpio = 32;
mg->gc.parent = &pdev->dev;
/* IRQ chip 설정 */
girq = &mg->gc.irq;
gpio_irq_chip_set_chip(girq, &my_gpio_irq_chip);
girq->parent_handler = my_gpio_irq_handler;
girq->num_parents = 1;
girq->parents = devm_kcalloc(&pdev->dev, 1,
sizeof(*girq->parents), GFP_KERNEL);
girq->parents[0] = platform_get_irq(pdev, 0);
girq->default_type = IRQ_TYPE_NONE;
girq->handler = handle_bad_irq;
return devm_gpiochip_add_data(&pdev->dev, &mg->gc, mg);
}
인터럽트 전체 라우팅 경로
하드웨어 신호가 발생하여 최종 핸들러가 실행되기까지의 전체 경로입니다.
라우팅 핵심 코드
/* 인터럽트 컨트롤러 핸들러에서 호출 */
static void __gic_handle_irq(void)
{
u32 irqnr;
irqnr = gic_read_iar(); /* hwirq 읽기 */
if (likely(irqnr > 15 && irqnr < 1020)) {
/* SPI/PPI: 도메인을 통해 virq로 변환하여 처리 */
int err = generic_handle_domain_irq(gic_data.domain, irqnr);
if (err)
WARN_ONCE(true, "Unexpected hwirq %d\n", irqnr);
}
}
/* kernel/irq/irqdesc.c */
int generic_handle_domain_irq(struct irq_domain *domain,
unsigned int hwirq)
{
unsigned int virq = irq_find_mapping(domain, hwirq);
if (unlikely(!virq))
return -EINVAL;
generic_handle_irq(virq); /* irq_desc->handle_irq() 호출 */
return 0;
}
API 사용법
디바이스 드라이버에서 IRQ 도메인을 사용하는 주요 API입니다.
IRQ 매핑 생성
/* 드라이버 예제 */
struct my_device {
struct irq_domain *domain;
int base_virq;
};
static int my_probe(struct platform_device *pdev)
{
struct my_device *mydev;
struct device_node *np = pdev->dev.of_node;
int virq;
/* 1. IRQ 도메인 생성 */
mydev->domain = irq_domain_add_linear(np, 32,
&my_irq_domain_ops,
mydev);
/* 2. hwirq 5번을 virq로 매핑 */
virq = irq_create_mapping(mydev->domain, 5);
/* 3. 인터럽트 핸들러 등록 */
request_irq(virq, my_irq_handler, 0, "my-device", mydev);
return 0;
}
Device Tree에서 IRQ 가져오기
/* of_irq API */
int virq = of_irq_get(pdev->dev.of_node, 0); /* interrupts[0] */
if (virq < 0)
return virq;
request_irq(virq, handler, flags, name, dev);
/* platform_get_irq (더 선호됨) */
virq = platform_get_irq(pdev, 0);
if (virq < 0)
return virq;
디버깅
IRQ 도메인과 인터럽트 상태를 확인하는 방법입니다.
/proc/interrupts
cat /proc/interrupts
# CPU0 CPU1
# 1: 0 0 GIC-0 1 Edge arch_timer
# 16: 12345 8910 GIC-0 16 Level uart-pl011
# 45: 5678 0 PCI-MSI 524288 Edge nvme0q0
# ^virq ^도메인 ^hwirq
/sys/kernel/irq/
ls /sys/kernel/irq/45/
# actions affinity_hint chip_name hwirq name per_cpu_count spurious type
cat /sys/kernel/irq/45/chip_name
# PCI-MSI
cat /sys/kernel/irq/45/hwirq
# 524288
/sys/kernel/debug/irq_domain_mapping
# CONFIG_GENERIC_IRQ_DEBUGFS=y 필요
cat /sys/kernel/debug/irq_domain_mapping
# name mapped linear-max direct-max devtree-node
# GICv3 128 1024 0 /interrupt-controller@8000000
# PCI-MSI 64 0 0
/sys/kernel/debug/irq/domains/
# 각 도메인별 상세 정보 (CONFIG_GENERIC_IRQ_DEBUGFS=y)
ls /sys/kernel/debug/irq/domains/
# GICv3 PCI-MSI ITS@00000000 GPIO-bank0
cat /sys/kernel/debug/irq/domains/GICv3
# name: GICv3
# size: 1024
# mapped: 128
# flags: 0x00000041
# parent: ---
cat /sys/kernel/debug/irq/domains/PCI-MSI
# name: PCI-MSI
# size: 0
# mapped: 64
# flags: 0x00000101
# parent: ITS@00000000
ftrace를 활용한 IRQ 추적
# irq_domain 관련 함수 추적
echo 'irq_domain_*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 특정 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
# 결과 확인
cat /sys/kernel/debug/tracing/trace
# ... irq_handler_entry: irq=45 name=nvme0q0
# ... irq_handler_exit: irq=45 ret=handled
# IRQ 도메인 매핑 추적
echo 'irq_create_mapping' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'irq_dispose_mapping' >> /sys/kernel/debug/tracing/set_ftrace_filter
echo function_graph > /sys/kernel/debug/tracing/current_tracer
ACPI 통합
x86 시스템에서는 ACPI MADT(Multiple APIC Description Table)로 인터럽트를 기술합니다.
ACPI vs Device Tree
| 항목 | Device Tree (ARM/RISC-V) | ACPI (x86/ARM64) |
|---|---|---|
| 포맷 | 텍스트 .dts → 바이너리 .dtb | 바이너리 테이블 (MADT, DSDT) |
| IRQ 기술 | interrupts 속성 | MADT 엔트리, _CRS 메서드 |
| 파싱 API | of_irq_parse_one() | acpi_register_gsi() |
| 컨트롤러 | GIC, PLIC 등 | I/O APIC, GICv3 |
ACPI IRQ 등록
/* drivers/acpi/resource.c */
int acpi_register_gsi(struct device *dev, u32 gsi,
int trigger, int polarity)
{
unsigned int virq;
struct irq_fwspec fwspec;
fwspec.fwnode = acpi_gsi_domain_id;
fwspec.param[0] = gsi;
fwspec.param[1] = acpi_dev_get_irq_type(trigger, polarity);
fwspec.param_count = 2;
return irq_create_fwspec_mapping(&fwspec);
}
자주 발생하는 오류
IRQ 도메인 관련 드라이버 개발에서 자주 마주치는 오류와 해결 방법입니다.
매핑 누수 (Mapping Leak)
/proc/interrupts에 IRQ가 남아 있거나, 재로드 시 irq_create_mapping()이 다른 virq를 반환합니다.
/* 잘못된 예: remove에서 매핑 해제 누락 */
static void my_remove_BAD(struct platform_device *pdev)
{
struct my_device *mydev = platform_get_drvdata(pdev);
free_irq(mydev->virq, mydev);
/* 주의: irq_dispose_mapping() 누락! */
irq_domain_remove(mydev->domain); /* WARN_ON(mapcount) 발생 */
}
/* 올바른 예: 매핑 해제 후 도메인 제거 */
static void my_remove_GOOD(struct platform_device *pdev)
{
struct my_device *mydev = platform_get_drvdata(pdev);
int i;
for (i = 0; i < mydev->nr_irqs; i++) {
unsigned int virq = irq_find_mapping(mydev->domain, i);
if (virq) {
free_irq(virq, mydev);
irq_dispose_mapping(virq); /* 매핑 해제 */
}
}
irq_domain_remove(mydev->domain); /* 안전하게 도메인 제거 */
}
이중 해제 (Double-Free)
irq_dispose_mapping()을 두 번 호출하거나, 이미 해제된 virq에 대해 free_irq()를 호출하면 커널 패닉이 발생합니다.
/* 방어적 코딩 패턴 */
static void safe_dispose_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
unsigned int virq = irq_find_mapping(domain, hwirq);
if (virq && virq != 0) {
struct irq_data *d = irq_get_irq_data(virq);
if (d && d->domain == domain) {
irq_dispose_mapping(virq);
}
}
}
xlate 콜백 누락
irq_create_of_mapping()이 0을 반환합니다.
xlate 콜백을 설정하지 않으면 기본 irq_domain_xlate_onecell()이 사용되는데, #interrupt-cells이 2 이상이면 실패합니다.
/* 해결: #interrupt-cells에 맞는 xlate 콜백 설정 */
static const struct irq_domain_ops my_ops = {
.map = my_map,
.xlate = irq_domain_xlate_twocell, /* #interrupt-cells = <2>일 때 */
};
/* 커널 제공 xlate 헬퍼 함수들: */
/* irq_domain_xlate_onecell() — #interrupt-cells = <1> */
/* irq_domain_xlate_twocell() — #interrupt-cells = <2> */
/* irq_domain_xlate_onetwocell() — 1 또는 2셀 자동 감지 */
드라이버 개발 체크리스트
| 점검 항목 | 확인 방법 | 실패 시 증상 |
|---|---|---|
| xlate 콜백 일치 | #interrupt-cells 값과 xlate 함수 확인 | 매핑 실패 (virq = 0) |
| 매핑 해제 순서 | free_irq() → irq_dispose_mapping() → irq_domain_remove() | WARN_ON, use-after-free |
| 계층 alloc 체인 | 부모 도메인의 alloc 성공 확인 | 전체 체인 실패 |
| irq_chip 완전성 | mask/unmask/ack(또는 eoi) 모두 구현 | 인터럽트 무한 반복 |
| 동시성 보호 | shared IRQ에서 핸들러 reentrant 확인 | 데이터 손상 |
| 모듈 제거 테스트 | insmod/rmmod 반복 후 /proc/interrupts 확인 | 매핑 누수 |
IRQ Domain 내부 구현 상세
irq_create_mapping()의 내부 동작을 단계별로 분석합니다.
/* kernel/irq/irqdomain.c */
unsigned int irq_create_mapping_affinity(
struct irq_domain *domain,
irq_hw_number_t hwirq,
const struct irq_affinity_desc *affinity)
{
struct device_node *of_node = irq_domain_get_of_node(domain);
int virq;
/* 1단계: 기존 매핑 확인 (중복 방지) */
virq = irq_find_mapping(domain, hwirq);
if (virq) {
pr_debug("existing mapping on virq %d\n", virq);
return virq; /* 이미 매핑됨: 기존 virq 반환 */
}
/* 2단계: virq 디스크립터 할당 */
virq = irq_domain_alloc_descs(-1, 1, hwirq, of_node_to_nid(of_node),
affinity);
if (virq < 0)
return virq;
/* 3단계: 도메인 연결 (revmap 저장 + ops->map() 호출) */
if (irq_domain_associate(domain, virq, hwirq)) {
irq_free_desc(virq);
return 0;
}
return virq;
}
irq_find_mapping 내부
unsigned int irq_find_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)
{
struct irq_data *data;
if (hwirq < domain->revmap_size) {
/* Linear: O(1) 배열 인덱싱 */
data = rcu_dereference(domain->linear_revmap[hwirq]);
} else {
/* Tree: radix tree 조회 */
data = radix_tree_lookup(&domain->revmap_tree, hwirq);
}
return data ? data->irq : 0;
}