IRQ 도메인
Linux 커널의 IRQ 도메인 인프라를 심층 분석합니다. 하드웨어 IRQ 번호를 Linux 가상 IRQ로 매핑(Mapping)하는 추상화 계층, 인터럽트 컨트롤러(Interrupt Controller) 드라이버, 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 메타데이터 구조체(Struct)
- 계층적 도메인 — 중첩 컨트롤러(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 체인을 확인합니다. - 디버깅(Debugging) 방법 습득
/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를 할당하기까지의 전체 과정입니다.
Generic IRQ Layer 전체 구조
리눅스 커널의 Generic IRQ Layer는 여러 핵심 구조체가 유기적으로 연결되어 인터럽트를 관리합니다. 아래 다이어그램은 irq_desc, irq_data, irq_chip, irq_domain, irqaction의 관계와, 계층형 도메인(Hierarchical Domain)에서의 parent_data 체인을 한눈에 보여줍니다.
위 다이어그램에서 핵심 관계를 정리하면:
- irq_desc는 virq(Linux 가상 IRQ)마다 하나씩 존재하며, 내부에
irq_data와irqaction리스트를 포함합니다. - irq_data는 해당 IRQ가 속한
irq_domain과 하드웨어 제어를 담당하는irq_chip을 참조합니다. - irqaction은
request_irq()로 등록한 핸들러 체인으로, 공유 IRQ(Shared IRQ)에서는next포인터로 연결됩니다. - 계층형 도메인에서는
irq_data→parent_data포인터가 상위 도메인의irq_data를 가리키며, MSI → ITS → GIC 같은 다단 체인을 형성합니다.
| 구조체 | 역할 | 소스 위치 |
|---|---|---|
irq_desc | virq별 최상위 디스크립터(Descriptor) — 상태, 통계, 핸들러 체인 관리 | include/linux/irqdesc.h |
irq_data | virq-hwirq 바인딩 메타데이터, chip/domain 참조 | include/linux/irq.h |
irq_chip | 인터럽트 컨트롤러 하드웨어 제어 콜백(Callback) 집합 | include/linux/irq.h |
irq_domain | hwirq→virq 매핑 인프라, revmap 관리 | include/linux/irqdomain.h |
irqaction | request_irq()로 등록된 핸들러 정보, 공유 IRQ 체인 | include/linux/interrupt.h |
irq_domain_ops | 도메인별 xlate/map/alloc/free 콜백 | include/linux/irqdomain.h |
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 */
};
코드 설명
- link전역
irq_domain_list에 연결되는 리스트 노드입니다. 커널은 이 리스트를 순회하며match()콜백으로 적절한 도메인을 검색합니다 (kernel/irq/irqdomain.c의irq_find_matching_fwspec()). - ops이 도메인의 동작을 정의하는
irq_domain_ops콜백 테이블입니다.map,xlate,alloc등 도메인 유형에 따라 필요한 콜백이 다릅니다. - host_data컨트롤러 드라이버가 자유롭게 사용하는 private 데이터 포인터입니다. GIC 드라이버는 여기에
gic_chip_data를 저장합니다. - linear_revmap[]Linear 매핑용 가변 길이 배열(Flexible Array Member)입니다. hwirq를 인덱스로 사용하여 O(1)에
irq_data를 조회합니다. 도메인 생성 시size인자로 크기가 결정됩니다. - revmap_treeTree 매핑용 radix tree입니다. hwirq가 sparse한 경우(예: MSI-X) Linear 배열 대신 사용되어 메모리를 절약합니다.
- parent계층형 도메인에서 부모 도메인을 가리킵니다. 예를 들어 MSI 도메인의 parent는 ITS 도메인, ITS의 parent는 GIC 도메인입니다. 비계층(Flat) 도메인에서는
NULL입니다. - fwnodeDevice Tree 또는 ACPI 펌웨어 노드 핸들입니다.
irq_find_matching_fwnode()에서 도메인을 검색하는 키로 사용됩니다.
irq_desc 구조체
irq_desc는 Linux 가상 IRQ(virq)마다 하나씩 할당되는 최상위 인터럽트 디스크립터(Interrupt Descriptor)입니다. 내부에 irq_data를 직접 포함하고, action 필드로 핸들러 체인(irqaction)을 관리합니다.
/* include/linux/irqdesc.h */
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data; /* 내장 irq_data (포인터가 아님) */
unsigned int __percpu *kstat_irqs; /* CPU별 인터럽트 발생 횟수 */
irq_flow_handler_t handle_irq; /* 흐름 제어 함수 (handle_level_irq 등) */
struct irqaction *action; /* 핸들러 체인 (request_irq로 등록) */
unsigned int status_use_accessors; /* IRQ 상태 플래그 */
unsigned int core_internal_state__do_not_mess_with_it;
unsigned int depth; /* disable 중첩 깊이 (0=활성) */
unsigned int irq_count; /* spurious IRQ 감지용 카운터 */
unsigned int irqs_unhandled; /* 미처리 인터럽트 카운터 */
raw_spinlock_t lock; /* per-IRQ 직렬화 락 */
const char *name; /* /proc/interrupts 표시명 */
#ifdef CONFIG_SPARSE_IRQ
struct rcu_head rcu; /* RCU 해제용 */
#endif
};
| 필드 | 설명 |
|---|---|
irq_data | 내장된 irq_data 구조체 — irq_desc_get_irq_data()로 접근 |
kstat_irqs | per-CPU 인터럽트 통계, /proc/interrupts에 표시되는 카운터 |
handle_irq | 흐름 제어 핸들러(Flow Handler) — handle_level_irq, handle_edge_irq, handle_fasteoi_irq 등 |
action | request_irq()로 등록된 irqaction 연결 리스트의 헤드 |
depth | disable_irq() 중첩 깊이, 0이면 활성 상태 |
lock | 이 IRQ 라인의 동시 접근을 보호하는 raw_spinlock |
irq_data는 irq_desc 내부에 포인터가 아닌 값(Value)으로 내장됩니다.
따라서 irq_desc의 주소에서 irq_data의 오프셋을 빼면 irq_desc를 역산할 수 있으며, 커널은 irq_data_to_desc() 헬퍼로 이를 수행합니다.
계층형 도메인의 자식 irq_data(동적 할당)는 내장이 아니라 별도 메모리에 존재하므로, irq_data_to_desc()는 최상위 irq_data에만 유효합니다.
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).
irqaction 구조체
irqaction은 request_irq() 또는 request_threaded_irq()로 등록된 인터럽트 핸들러(Interrupt Handler) 정보를 담는 구조체입니다. 공유 IRQ(Shared IRQ) 라인에서는 next 포인터로 여러 irqaction이 연결 리스트(Linked List)를 형성합니다.
/* include/linux/interrupt.h */
struct irqaction {
irq_handler_t handler; /* 하드 IRQ 핸들러 (top half) */
void *dev_id; /* 디바이스 식별자 (공유 IRQ 구분용) */
void __percpu *percpu_dev_id;
struct irqaction *next; /* 공유 IRQ 다음 핸들러 */
irq_handler_t thread_fn; /* 스레드 IRQ 핸들러 (bottom half) */
struct task_struct *thread; /* IRQ 스레드 태스크 */
struct irqaction *secondary; /* 보조 액션 (force-threaded용) */
unsigned int irq; /* virq 번호 */
unsigned int flags; /* IRQF_SHARED, IRQF_ONESHOT 등 */
unsigned long thread_flags; /* 스레드 상태 플래그 */
const char *name; /* /proc/interrupts 표시명 */
struct proc_dir_entry *dir; /* /proc/irq/N/ 엔트리 */
};
| 필드 | 설명 |
|---|---|
handler | 하드 IRQ 컨텍스트(Hard IRQ Context)에서 실행되는 핸들러. IRQ_HANDLED 또는 IRQ_WAKE_THREAD 반환 |
thread_fn | 스레드 IRQ 핸들러(Threaded IRQ Handler) — request_threaded_irq()로 등록 시 전용 커널 스레드에서 실행 |
flags | IRQF_SHARED(공유), IRQF_ONESHOT(핸들러 완료까지 재활성화 지연), IRQF_TRIGGER_* 등 |
dev_id | 공유 IRQ에서 핸들러를 구분하는 디바이스별 고유 식별자. free_irq() 시에도 사용 |
next | 동일 IRQ 라인을 공유하는 다음 irqaction 포인터 (단방향 리스트) |
name | /proc/interrupts에 표시되는 핸들러 이름 |
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);
};
코드 설명
- match()
irq_find_matching_fwspec()에서 호출됩니다.fwnode과bus_token을 기준으로 요청된 인터럽트가 이 도메인에 속하는지 판별합니다. 대부분의 드라이버는 커널 기본 구현(irq_domain_simple_ops.match)을 사용합니다. - map()비계층(Flat) 도메인에서 hwirq→virq 매핑이 생성될 때 호출됩니다. 이 콜백 안에서
irq_set_chip_and_handler()를 호출하여irq_chip과 흐름 제어 핸들러(Flow Handler)를 바인딩합니다 (include/linux/irqdomain.h). - xlate()Device Tree의
interrupts속성(intspec)을 hwirq 번호와 트리거 타입으로 변환합니다. GIC는#interrupt-cells = <3>이므로 3-cell xlate(irq_domain_xlate_twocell은 2-cell용, GIC는 자체 구현)를 사용합니다 (drivers/of/irq.c). - alloc()계층형(Hierarchy) 도메인 전용입니다.
irq_domain_alloc_irqs()에서 호출되며, 내부에서irq_domain_alloc_irqs_parent()를 통해 부모 도메인의alloc()을 재귀 호출합니다 (kernel/irq/irqdomain.c). - free()
alloc()의 역순으로 자원을 해제합니다. 계층형 도메인에서 부모 체인을 따라 역순으로 호출되어 각 계층의 할당을 정리합니다.
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 |
| 조회 시 캐시(Cache) | 배열 인덱싱 (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_domain_add_linear()과 irq_domain_add_tree()는 모두 내부적으로 __irq_domain_add()를 호출하여 도메인을 생성하고 전역 리스트에 등록합니다 (kernel/irq/irqdomain.c).
- irq_domain_add_linear
size인자만큼의linear_revmap[]배열을 할당합니다. hwirq 범위가 0부터size-1까지 연속일 때 사용하며, O(1) 조회가 가능합니다. GIC SPI(최대 1020개)나 GPIO 컨트롤러에 적합합니다. - irq_domain_add_tree
size를 0으로 전달하고 radix tree를 초기화합니다.~0(UINT_MAX)을 최대 hwirq로 설정하여 사실상 무제한 범위를 허용합니다. PCIe MSI-X처럼 hwirq 번호가 sparse한 경우에 메모리 효율적입니다. - INIT_RADIX_TREE
GFP_KERNEL플래그로 radix tree를 초기화합니다. 이후irq_domain_associate()에서 매핑 생성 시 노드가 동적으로 추가됩니다.
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 | 인터럽트 비활성화 | 컨트롤러의 마스크 레지스터(Register) 설정 |
| irq_unmask | 인터럽트 활성화 | 컨트롤러의 언마스크 레지스터 설정 |
| irq_ack | 인터럽트 핸들러(Handler) 시작 | 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;
}
코드 설명
GICv3 드라이버의 IRQ 도메인 초기화 코드입니다 (drivers/irqchip/irq-gic-v3.c). map 콜백에서 인터럽트 타입별로 다른 흐름 제어 핸들러를 설정하는 패턴을 보여줍니다.
- hw < 32: SGI/PPIhwirq 0~31은 CPU별 private 인터럽트(SGI/PPI)입니다.
irq_set_percpu_devid()로 per-CPU IRQ로 표시하고,handle_percpu_devid_irq흐름 핸들러를 설정합니다. 이 핸들러는 CPU 간 공유 없이 각 CPU에서 독립적으로 처리합니다. - else: SPIhwirq 32~1019는 공유 주변장치 인터럽트(SPI)입니다.
handle_fasteoi_irq핸들러를 사용하며, 이는 핸들러 실행 후 자동으로irq_eoi()를 호출하여 GIC에 EOI(End of Interrupt)를 전달합니다. - gic_irq_domain_ops.xlate
irq_domain_xlate_twocell은 커널 제공 헬퍼로, DT의#interrupt-cells = <2>인 경우 첫 번째 셀을 hwirq, 두 번째 셀을 트리거 타입으로 변환합니다. GICv3는 3-cell(#interrupt-cells = <3>)이므로 실제로는 자체 xlate를 사용하지만, 여기서는 단순화된 예시입니다. - irq_domain_create_linear()GIC SPI는 hwirq가 0부터 연속이므로 Linear 매핑이 최적입니다.
gic_irqs(보통 1020)개의 배열을 할당하여 O(1) 조회를 보장합니다.
GIC 인터럽트 타입
| 타입 | hwirq 범위 | 설명 |
|---|---|---|
| SGI (Software Generated) | 0-15 | IPI (Inter-Processor Interrupt) |
| PPI (Private Peripheral) | 16-31 | CPU별 타이머(Timer), 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 |
| 인터럽트 라우팅(Routing) | 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);
}
코드 설명
계층형 도메인의 alloc 재귀 호출 패턴을 보여줍니다. MSI → ITS → GIC 3단 계층 예시입니다 (kernel/irq/irqdomain.c, drivers/pci/msi/irqdomain.c).
- msi_domain_alloc()MSI 도메인의
alloc콜백입니다. 먼저irq_domain_alloc_irqs_parent()를 호출하여 부모(ITS) 도메인에 할당을 위임합니다. 부모 할당이 성공한 후에야 자신의irq_chip(pci_msi_controller)을 설정합니다. - irq_domain_alloc_irqs_parent()부모 도메인의
ops->alloc()을 재귀적으로 호출하는 헬퍼입니다. ITS 도메인은 다시 이 함수를 호출하여 GIC 도메인까지 체인이 이어집니다. 최상위 도메인(GIC)에 도달하면parent가NULL이므로 재귀가 종료됩니다. - irq_domain_set_hwirq_and_chip()각 계층에서 해당 도메인의 hwirq 번호와
irq_chip을irq_data에 설정합니다. 이로써 한 virq에 대해 계층별로 서로 다른irq_chip과 hwirq가 바인딩됩니다 (include/linux/irqdomain.h).
스택형 도메인 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 트랜잭션(Transaction)) |
| IRQ 수 | 디바이스당 1-4개 | MSI: 1-32, MSI-X: 1-2048 |
| 공유 | 공유 가능 (레벨 트리거) | 항상 독점 |
| 성능 | 낮음 (공유 시 폴링(Polling)) | 높음 (독점, 큐별 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()를 호출하면 커널 패닉(Kernel Panic)이 발생합니다.
/* 방어적 코딩 패턴 */
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_create_mapping_affinity()는 비계층(Flat) 도메인에서 hwirq→virq 매핑을 생성하는 핵심 함수입니다 (kernel/irq/irqdomain.c). 3단계로 동작합니다:
- 1단계: irq_find_mapping()이미 존재하는 매핑을 확인합니다. 동일 hwirq에 대해 중복 매핑을 방지하여, 여러 드라이버가 같은 인터럽트를 참조해도 안전합니다.
- 2단계: irq_domain_alloc_descs()새 virq 번호를 할당하고
irq_desc디스크립터를 생성합니다. 첫 번째 인자-1은 커널이 자동으로 빈 virq 번호를 선택하도록 합니다. NUMA 노드 인자(of_node_to_nid)는 디스크립터 메모리를 해당 노드에 할당합니다. - 3단계: irq_domain_associate()도메인의 revmap(Linear 배열 또는 radix tree)에 hwirq→
irq_data매핑을 저장하고,ops->map()콜백을 호출하여irq_chip과 흐름 제어 핸들러를 설정합니다. 실패 시irq_free_desc()로 할당된 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;
}
코드 설명
irq_find_mapping()은 도메인의 revmap에서 hwirq에 대응하는 virq를 조회합니다 (kernel/irq/irqdomain.c). 인터럽트 핸들러의 hot path에서 호출되므로 성능이 중요합니다.
- hwirq < domain->revmap_sizeLinear 도메인의 경우 hwirq가 배열 크기 이내이면
linear_revmap[hwirq]를 직접 인덱싱합니다. 배열 접근이므로 O(1)이며 캐시 효율이 높습니다. - rcu_dereference()RCU(Read-Copy-Update) 보호 하에 포인터를 읽습니다. 이는 매핑이 동적으로 추가/제거되는 동안에도 읽기 측에서 락 없이 안전하게 조회할 수 있게 합니다.
- radix_tree_lookup()Tree 도메인의 경우 radix tree에서 hwirq를 키로 검색합니다. O(log N) 복잡도이지만, sparse한 hwirq 공간에서는 Linear 방식보다 메모리를 크게 절약합니다.
- data->irq조회된
irq_data의irq필드가 virq 번호입니다. 매핑이 없으면 0을 반환하며, 호출자는 이를 확인하여 오류를 처리해야 합니다.
ARM GICv3 ITS (Interrupt Translation Service)
GICv3 아키텍처의 ITS(Interrupt Translation Service)는 PCIe MSI 및 기타 디바이스의 메시지 기반 인터럽트를 LPI(Locality-specific Peripheral Interrupt)로 변환하는 하드웨어 유닛입니다. ITS는 DeviceID와 EventID의 2차원 키 공간을 단일 INTID(LPI)로 매핑하며, 최종적으로 해당 LPI를 특정 CPU의 Redistributor로 라우팅합니다.
ITS 매핑 개념
ITS의 핵심은 2단계 변환입니다:
| 단계 | 입력 | 출력 | 테이블 | 설명 |
|---|---|---|---|---|
| 1단계 | DeviceID | ITT 포인터 | Device Table (DT) | BDF(Bus/Device/Function)로 디바이스별 ITT 위치 조회 |
| 2단계 | EventID | INTID + Collection | Interrupt Translation Table (ITT) | EventID를 LPI 번호와 타겟 Collection으로 변환 |
| 라우팅 | Collection ID | Target CPU | Collection Table (CT) | Collection을 특정 Redistributor(CPU)에 매핑 |
its_domain_ops 콜백
ITS 도메인은 irq_domain_ops를 구현하여 LPI 할당과 MSI 메시지 구성을 처리합니다:
/* 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,
.activate = its_irq_domain_activate,
.deactivate = its_irq_domain_deactivate,
};
/* alloc: DeviceID/EventID에 대한 LPI 할당 */
static int its_irq_domain_alloc(struct irq_domain *domain,
unsigned int virq,
unsigned int nr_irqs, void *args)
{
struct irq_fwspec *fwspec = args;
u32 dev_id = fwspec->param[0]; /* DeviceID */
u32 event_id = fwspec->param[1]; /* EventID (시작) */
/* ITS 디바이스 조회 또는 생성 */
its_dev = its_find_device(its, dev_id);
if (!its_dev)
its_dev = its_create_device(its, dev_id, nr_irqs);
/* LPI 번호 할당 및 ITT 엔트리 설정 */
for (i = 0; i < nr_irqs; i++) {
its_lpi_alloc(its_dev, event_id + i, virq + i);
irq_domain_set_hwirq_and_chip(domain, virq + i,
event_id + i, &its_irq_chip, its_dev);
}
return 0;
}
ITS Command Queue
ITS는 메모리 매핑된 커맨드 큐를 통해 제어됩니다. 드라이버가 ITS에 명령을 기록하면 ITS 하드웨어가 비동기적으로 처리합니다:
| 명령 | 코드 | 설명 | 파라미터 |
|---|---|---|---|
| MAPD | 0x08 | DeviceID → ITT 매핑 설정 | DeviceID, ITT 주소, ITT 크기 |
| MAPTI | 0x0A | EventID → INTID + Collection 매핑 | DeviceID, EventID, pINTID, CollID |
| MAPI | 0x0B | EventID → INTID 매핑 (INTID = EventID) | DeviceID, EventID, CollID |
| MAPC | 0x09 | Collection → Target Redistributor 매핑 | CollID, RDbase |
| INV | 0x0C | 캐시된 LPI 설정 무효화(Invalidation) | DeviceID, EventID |
| INVALL | 0x0D | 특정 Collection의 모든 LPI 무효화 | CollID |
| SYNC | 0x05 | Redistributor 동기화 보장 | RDbase |
| DISCARD | 0x0F | EventID 매핑 제거 | DeviceID, EventID |
/* ITS 커맨드 전송 예시 */
static void its_send_mapti(struct its_device *dev,
u32 irq_id, u32 id)
{
struct its_cmd_desc desc;
desc.its_mapti_cmd.dev = dev;
desc.its_mapti_cmd.phys_id = irq_id; /* pINTID (LPI 번호) */
desc.its_mapti_cmd.event_id = id; /* EventID */
its_send_single_command(dev->its, its_build_mapti_cmd,
&desc);
}
Device Table / ITT 구조
ITS는 두 가지 주요 메모리 테이블을 사용합니다:
| 테이블 | 인덱싱 키 | 엔트리 크기 | 할당 단위 | 설명 |
|---|---|---|---|---|
| Device Table | DeviceID | 8바이트 | Flat 또는 2-Level | DeviceID별 ITT 주소와 크기 저장 |
| ITT | EventID | ite_size (HW 정의) | 디바이스별 개별 할당 | EventID→INTID+Collection 매핑 |
| Collection Table | Collection ID | 8바이트 | ITS당 1개 | CollID→Target Redistributor 매핑 |
/* ITT 할당: 디바이스당 하나의 ITT */
static struct its_device *its_create_device(
struct its_node *its, u32 dev_id,
int nvecs)
{
struct its_device *dev;
int nr_ites; /* ITT 엔트리 수 = roundup_pow_of_two(nvecs) */
int sz; /* ITT 크기 = nr_ites * its->ite_size */
nr_ites = roundup_pow_of_two(nvecs);
sz = nr_ites * its->ite_size;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
dev->itt = itt_alloc_pool(its->numa_node, sz);
dev->nr_ites = nr_ites;
dev->device_id = dev_id;
/* MAPD 커맨드로 Device Table에 등록 */
its_send_mapd(dev, 1); /* valid=1 */
return dev;
}
Indirect=1로 설정하면 첫 번째 레벨 테이블이 두 번째 레벨 페이지(Page)를 가리키는 구조가 됩니다.
MSI Domain 계층
MSI(Message Signaled Interrupts) 도메인은 IRQ 도메인 계층 구조에서 디바이스와 인터럽트 컨트롤러 사이의 중간 변환 계층입니다. PCI MSI/MSI-X, platform-msi, device-msi 등 다양한 MSI 소스를 통합적으로 관리합니다.
MSI 계층 체인
MSI 인터럽트는 여러 도메인 계층을 거쳐 최종 인터럽트 컨트롤러에 도달합니다:
msi_domain_info / msi_domain_ops
MSI 도메인은 msi_domain_info 구조체로 설정되며, 이 안에 msi_domain_ops 콜백이 포함됩니다:
/* include/linux/msi.h */
struct msi_domain_info {
u32 flags; /* MSI_FLAG_* */
struct msi_domain_ops *ops; /* 도메인 콜백 */
struct irq_chip *chip; /* MSI irq_chip */
void *chip_data;
irq_flow_handler_t handler; /* 기본: handle_edge_irq */
void *handler_data;
const char *handler_name;
void *data; /* 플랫폼 private */
};
struct msi_domain_ops {
irq_hw_number_t (*get_hwirq)(struct msi_domain_info *, msi_alloc_info_t *);
int (*msi_init)(struct irq_domain *, struct msi_domain_info *,
unsigned int, irq_hw_number_t, msi_alloc_info_t *);
void (*msi_free)(struct irq_domain *, struct msi_domain_info *,
unsigned int);
int (*msi_prepare)(struct irq_domain *, struct device *,
int, msi_alloc_info_t *);
void (*set_desc)(msi_alloc_info_t *, struct msi_desc *);
};
MSI 타입 비교
| 속성 | PCI-MSI | Platform-MSI | Device-MSI |
|---|---|---|---|
| 소스 | PCIe 디바이스 | 비-PCI 플랫폼 디바이스 | 임의 디바이스 (범용) |
| 디스커버리 | PCI Capability | DT/ACPI 바인딩 | 드라이버 직접 요청 |
| 할당 API | pci_alloc_irq_vectors() | platform_msi_domain_alloc_irqs() | msi_domain_alloc_irqs() |
| MSI 주소/데이터 | PCI Config Space에 기록 | 드라이버가 직접 설정 | 드라이버가 직접 설정 |
| Parent Domain | ITS/IR/x86-vector | ITS/IR/x86-vector | ITS/IR/x86-vector |
| 대표 사용처 | NVMe, 네트워크 카드 | ARM SoC 내장 블록 | NTB, 특수 HW |
/* MSI parent domain 할당 흐름 (PCI 경로) */
pci_alloc_irq_vectors(pdev, min_vecs, max_vecs, PCI_IRQ_MSI | PCI_IRQ_MSIX)
→ __pci_enable_msi_range()
→ msi_domain_alloc_irqs_descs_locked(dev, MSI_DEFAULT_DOMAIN)
→ ops->msi_prepare() /* alloc_info 초기화 */
→ msi_domain_alloc_irqs_all_locked()
→ __irq_domain_alloc_irqs()
→ parent->ops->alloc() /* ITS: its_irq_domain_alloc */
→ parent->parent->ops->alloc() /* GIC: gic_irq_domain_alloc */
pci_alloc_irq_vectors()는 요청 수보다 적은 벡터를 할당할 수 있으므로, 반환값을 항상 확인하세요.
fwnode 기반 현대 API
커널 4.x 이후 IRQ 도메인 API는 of_node(Device Tree 전용)에서 fwnode_handle(DT + ACPI 통합) 기반으로 전환되었습니다. 이 통합 추상화는 동일한 인터럽트 컨트롤러 드라이버가 DT와 ACPI 환경 모두에서 동작할 수 있게 합니다.
of_node → fwnode 전환
| 구분 | 레거시 API (of_node) | 현대 API (fwnode) |
|---|---|---|
| Linear 도메인 생성 | irq_domain_add_linear() | irq_domain_create_linear() |
| Tree 도메인 생성 | irq_domain_add_tree() | irq_domain_create_tree() |
| 계층적 도메인 | irq_domain_add_hierarchy() | irq_domain_create_hierarchy() |
| 도메인 검색 | of_irq_find_parent() | irq_find_matching_fwnode() |
| 매핑 생성 | irq_create_of_mapping() | irq_create_fwspec_mapping() |
| 펌웨어 노드 참조 | struct device_node * | struct fwnode_handle * |
/* fwnode 기반 도메인 등록 예제 */
#include <linux/irqdomain.h>
static int my_intc_probe(struct platform_device *pdev)
{
struct irq_domain *domain;
struct fwnode_handle *fwnode = dev_fwnode(&pdev->dev);
int nr_irqs = 64;
/* fwnode 기반: DT/ACPI 모두 지원 */
domain = irq_domain_create_linear(fwnode, nr_irqs,
&my_domain_ops, priv);
if (!domain)
return -ENOMEM;
/* 계층적 도메인 (parent 지정) */
domain = irq_domain_create_hierarchy(
parent_domain, /* parent (GIC 등) */
0, /* flags */
nr_irqs, /* hwirq 크기 */
fwnode, /* fwnode_handle */
&my_hierarchy_ops, /* ops */
priv /* host_data */
);
return 0;
}
IRQCHIP_DECLARE 매크로(Macro)
DT 기반 인터럽트 컨트롤러는 IRQCHIP_DECLARE 매크로로 빌트인 등록합니다. 이 매크로는 __irqchip_of_table 섹션에 엔트리를 배치하여, 부팅 초기 irqchip_init()에서 자동으로 초기화됩니다:
/* IRQCHIP_DECLARE로 DT compatible 등록 */
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init);
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
/* 초기화 경로:
* start_kernel()
* → init_IRQ()
* → irqchip_init()
* → of_irq_init(__irqchip_of_table)
* → GIC, ITS 등 순서대로 init 콜백 호출
*/
/* ACPI 기반: IRQCHIP_ACPI_DECLARE */
IRQCHIP_ACPI_DECLARE(gic_v3, ACPI_MADT_TYPE_GENERIC_DISTRIBUTOR,
acpi_validate_gic_table,
ACPI_MADT_GIC_VERSION_V3,
gic_v3_acpi_init);
irq_find_matching_fwnode 도메인 검색
/* fwnode으로 도메인 검색 (DT/ACPI 공용) */
struct irq_domain *irq_find_matching_fwnode(
struct fwnode_handle *fwnode,
enum irq_domain_bus_token bus_token)
{
struct irq_domain *d;
mutex_lock(&irq_domain_mutex);
list_for_each_entry(d, &irq_domain_list, link) {
if (d->fwnode == fwnode &&
(bus_token == DOMAIN_BUS_ANY || d->bus_token == bus_token))
break;
}
mutex_unlock(&irq_domain_mutex);
return d;
}
/* bus_token 종류 */
enum irq_domain_bus_token {
DOMAIN_BUS_ANY = 0, /* 모든 도메인 */
DOMAIN_BUS_WIRED, /* 유선 인터럽트 */
DOMAIN_BUS_PCI_MSI, /* PCI MSI */
DOMAIN_BUS_PLATFORM_MSI, /* 플랫폼 MSI */
DOMAIN_BUS_IPI, /* IPI */
DOMAIN_BUS_FSL_MC_MSI, /* Freescale MC MSI */
DOMAIN_BUS_GENERIC_MSI, /* 범용 MSI */
};
irq_domain_add_linear(of_node, ...) 코드를 irq_domain_create_linear(of_node_to_fwnode(of_node), ...)로 변환하면 됩니다. of_node_to_fwnode()은 device_node를 fwnode_handle로 캐스팅하는 인라인 함수(Inline Function)입니다.
동적 IRQ 할당/해제
IRQ 도메인의 핵심 기능은 동적으로 hwirq ↔ virq 매핑을 생성/해제하는 것입니다. 레거시 고정 IRQ 체계와 달리, 현대 커널은 CONFIG_SPARSE_IRQ를 사용하여 필요한 시점에 IRQ 디스크립터를 동적으로 할당합니다.
매핑 생성 API 비교
| API | 입력 | 용도 | 호출 경로 |
|---|---|---|---|
irq_create_mapping() | domain, hwirq | 단순 hwirq→virq 매핑 | GPIO, 단순 컨트롤러 |
irq_create_fwspec_mapping() | irq_fwspec | 펌웨어 스펙 기반 매핑 | DT/ACPI 공용 경로 |
irq_domain_associate() | domain, virq, hwirq | 수동 매핑 (virq 이미 확보) | 레거시 드라이버 |
irq_create_of_mapping() | of_phandle_args | DT 전용 매핑 | of_irq_get() 경로 |
/* irq_create_mapping: 가장 간단한 매핑 생성 */
unsigned int virq = irq_create_mapping(domain, hwirq);
if (!virq) {
pr_err("failed to create mapping for hwirq %lu\n", hwirq);
return -EINVAL;
}
/* irq_create_fwspec_mapping: 펌웨어 스펙 기반 */
struct irq_fwspec fwspec = {
.fwnode = dev_fwnode(dev),
.param_count = 3,
.param = { 0, hwirq, IRQ_TYPE_LEVEL_HIGH },
};
virq = irq_create_fwspec_mapping(&fwspec);
/* irq_dispose_mapping: 매핑 해제 */
irq_dispose_mapping(virq);
/* 내부: irq_domain_disassociate() + irq_free_desc() */
sparse_irq와 동적 virq 풀
CONFIG_SPARSE_IRQ 활성 시 IRQ 디스크립터는 radix tree로 관리되며, 필요한 만큼만 할당됩니다:
/* kernel/irq/irqdesc.c */
static RADIX_TREE(irq_desc_tree, GFP_KERNEL);
/* virq 번호 할당: allocated_irqs 비트맵에서 빈 슬롯 탐색 */
static int irq_domain_alloc_descs(int virq, unsigned int cnt,
irq_hw_number_t hwirq, int node,
const struct irq_affinity_desc *affinity)
{
if (virq >= 0) /* 특정 virq 요청 시 해당 번호 시도 */
return __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE, affinity);
/* -1이면 자동 할당: NR_IRQS_LEGACY부터 탐색 */
virq = __irq_alloc_descs(-1, 0, cnt, node, THIS_MODULE, affinity);
return virq;
}
Per-CPU IRQ vs 공유 IRQ 도메인 매핑
Per-CPU 인터럽트(PPI 등)와 공유 인터럽트(SPI 등)는 도메인 내에서 다르게 취급됩니다:
/* Per-CPU IRQ 요청 (GIC PPI 등) */
request_percpu_irq(virq, handler, "timer", percpu_dev);
enable_percpu_irq(virq, IRQ_TYPE_NONE);
/* Per-CPU IRQ는 irq_desc에 percpu_dev_id가 설정됨 */
/* 각 CPU에서 개별적으로 enable/disable 가능 */
/* 공유 IRQ (SPI) - 일반 request_irq */
request_irq(virq, handler, IRQF_SHARED, "my-device", dev);
irq_dispose_mapping()을 호출하기 전에 반드시 free_irq()로 핸들러를 해제해야 합니다. 순서가 뒤바뀌면 핸들러가 해제된 매핑을 참조하여 커널 패닉이 발생할 수 있습니다.
DT vs ACPI 바인딩 비교
동일한 인터럽트 컨트롤러도 Device Tree와 ACPI에서 각기 다른 방식으로 기술됩니다. 이 섹션에서는 두 펌웨어 인터페이스의 인터럽트 바인딩 차이를 상세히 비교합니다.
Device Tree 인터럽트 바인딩
DT에서 인터럽트 컨트롤러와 클라이언트 디바이스는 다음 속성으로 연결됩니다:
/* GICv3 Device Tree 바인딩 예제 */
gic: interrupt-controller@51a00000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>; /* type, hwirq, flags */
interrupt-controller;
#address-cells = <2>;
#size-cells = <2>;
ranges;
reg = <0x0 0x51a00000 0 0x10000>, /* GICD */
<0x0 0x51b00000 0 0x200000>; /* GICR */
its: msi-controller@51b40000 {
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>;
reg = <0x0 0x51b40000 0 0x20000>;
};
};
/* 클라이언트: interrupt-parent + interrupts */
uart0: serial@30860000 {
compatible = "fsl,imx8mq-uart";
interrupt-parent = <&gic>;
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
/* GIC_SPI=0, hwirq=26, trigger=4 (level high) */
};
/* PCIe: interrupt-map으로 복잡한 매핑 */
pcie0: pcie@33800000 {
interrupt-map = <0 0 0 1 &gic GIC_SPI 122 IRQ_TYPE_LEVEL_HIGH>,
<0 0 0 2 &gic GIC_SPI 123 IRQ_TYPE_LEVEL_HIGH>;
interrupt-map-mask = <0 0 0 7>;
};
ACPI 인터럽트 바인딩
ACPI에서 인터럽트 정보는 주로 MADT(Multiple APIC Description Table), IORT, DSDT/SSDT 테이블에 기술됩니다:
/* ACPI DSDT: 디바이스 인터럽트 리소스 */
Device (COM0) {
Name (_HID, "PNP0501")
Method (_CRS, 0) {
Return (ResourceTemplate () {
/* GSI(Global System Interrupt) 기반 */
Interrupt (ResourceConsumer, Level, ActiveHigh, Exclusive) {
33 /* GSI 번호 */
}
})
}
}
/* ACPI MADT 구조 (바이너리 테이블) */
/* Type 0: Local APIC */
/* Type 1: I/O APIC - GSI Base 지정 */
/* Type 11: GIC Distributor */
/* Type 12: GIC MSI Frame */
/* Type 14: GIC ITS */
DT vs ACPI 매핑 비교 테이블
| 항목 | Device Tree | ACPI |
|---|---|---|
| 컨트롤러 기술 | interrupt-controller 속성 | MADT 서브테이블 |
| 인터럽트 셀 수 | #interrupt-cells = <N> | 고정 (GSI + flags) |
| IRQ 번호 체계 | 컨트롤러별 로컬 hwirq | GSI (Global System Interrupt) |
| 계층 참조 | interrupt-parent = <&gic> | IORT 테이블 참조 |
| 복잡 매핑 | interrupt-map 속성 | IORT ITS Group Node |
| MSI 컨트롤러 | msi-controller + #msi-cells | IORT ITS Node |
| 트리거 타입 | interrupts 셀 3번째 값 | _CRS Interrupt() 리소스 flags |
| 번호 변환 | xlate 콜백 (ops) | acpi_gsi_to_irq() |
| 파싱 함수 | of_irq_get() | acpi_irq_get() |
| 도메인 검색 | of_irq_find_parent() | irq_find_matching_fwnode() |
ACPI IORT와 ITS 연동
IORT(I/O Remapping Table)는 ACPI 환경에서 PCIe 디바이스의 MSI 라우팅 경로를 정의합니다:
/* IORT 테이블 파싱: PCIe RC → ITS 매핑 */
/* drivers/acpi/arm64/iort.c */
static int iort_get_id_mapping_index(struct acpi_iort_node *node)
{
/*
* IORT 노드 체인:
* Root Complex → ID Mapping → ITS Group
*
* ID Mapping 엔트리:
* input_base: 디바이스 ID 시작 (BDF)
* id_count: 매핑 범위
* output_base: ITS DeviceID 시작
* output_reference: ITS Group 노드 오프셋
*/
}
/* GSI → virq 변환 (ACPI 경로) */
unsigned int acpi_register_gsi(struct device *dev,
u32 gsi, int trigger, int polarity)
{
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);
}
acpi_register_gsi()가 GSI를 Linux virq로 변환합니다.
IRQ Domain 성능 분석
IRQ 도메인의 매핑 방식 선택은 시스템의 인터럽트 처리 지연(latency)에 직접 영향을 미칩니다. hwirq 수와 분포 패턴에 따라 적절한 revmap 전략을 선택해야 합니다.
Linear vs Radix 성능 상세
| 지표 | Linear | Radix Tree | 비고 |
|---|---|---|---|
| Lookup 시간 | ~3ns (L1 캐시 hit) | ~20-50ns (트리 탐색) | 캐시 상태에 크게 의존 |
| 삽입 시간 | O(1) | O(log n) + 노드 할당 | Radix는 GFP_KERNEL 할당 발생 |
| 메모리 사용 (hwirq 256) | ~2KB (256 포인터) | ~4KB (노드 오버헤드) | 소규모에서 Linear 유리 |
| 메모리 사용 (hwirq 8192) | ~64KB | ~8-16KB (실사용분만) | 대규모에서 Radix 유리 |
| 캐시 친화도(Affinity) | 높음 (연속 배열) | 낮음 (포인터 체이싱) | 실시간(Real-time) 시스템에서 중요 |
irq_resolve_mapping 최적화
/* 최적화된 매핑 조회: irq_resolve_mapping()
* irq_find_mapping()과 달리 irq_data를 직접 반환하여
* 추가 desc 조회 비용을 절감 */
struct irq_desc *irq_resolve_mapping(
struct irq_domain *domain,
irq_hw_number_t hwirq)
{
struct irq_data *data;
/* Direct hint: 이전 조회 결과 캐시 */
data = rcu_dereference(domain->mapcount ?
domain->linear_revmap[hwirq] : NULL);
if (likely(data))
return irq_data_to_desc(data);
return __irq_resolve_mapping(domain, hwirq, NULL);
}
/* 인터럽트 핸들러에서 고속 경로 */
static void gic_handle_irq(struct pt_regs *regs)
{
u32 irqnr;
while ((irqnr = gic_read_iar()) != ICC_IAR1_EL1_SPURIOUS) {
/* 고속 경로: linear revmap 직접 인덱싱 */
if (likely(irqnr > 15 && irqnr < 1020))
generic_handle_domain_irq(gic_data.domain, irqnr);
}
}
대규모 시스템 스케일링
수천 개 IRQ를 사용하는 서버 시스템에서의 고려사항:
- GICv3 LPI는 최대 2^24(약 1600만) 개까지 지원하므로 반드시 Radix Tree 사용
- NVMe 디바이스 다수 장착 시 MSI-X 벡터가 수천 개에 달함: LPI 풀 크기(
its_lpi_prop_page) 확인 필요 irq_domain_alloc_descs()의allocated_irqs비트맵(Bitmap) 스캔이 병목(Bottleneck)이 될 수 있음: NUMA 노드별 할당으로 완화- Linear 도메인의
revmap_size가 너무 크면(예: 64K+) 메모리 낭비 →irq_domain_create_tree()전환 고려
멀티 컨트롤러 토폴로지(Topology)
실제 SoC에서는 인터럽트 컨트롤러가 계층적으로 연결됩니다. GPIO 컨트롤러, MFD(Multi-Function Device) 인터럽트 컨트롤러 등이 루트 GIC/APIC에 cascaded 방식으로 연결됩니다.
Chained IRQ Handler
Chained handler는 하드웨어 인터럽트 컨텍스트에서 실행되며, 부모 컨트롤러의 IRQ 핸들러 내부에서 직접 호출됩니다:
/* Chained IRQ: GPIO 컨트롤러 예제 */
static void gpio_irq_handler(struct irq_desc *desc)
{
struct gpio_chip *gc = irq_desc_get_handler_data(desc);
struct irq_chip *chip = irq_desc_get_chip(desc);
unsigned long pending;
int offset;
chained_irq_enter(chip, desc); /* 부모 chip->irq_mask_ack() */
pending = readl(gc->reg_base + GPIO_ISR);
for_each_set_bit(offset, &pending, gc->ngpio) {
generic_handle_domain_irq(gc->irq.domain, offset);
}
chained_irq_exit(chip, desc); /* 부모 chip->irq_eoi() */
}
/* 등록: 부모 IRQ에 chained handler 설치 */
irq_set_chained_handler_and_data(parent_irq,
gpio_irq_handler, gc);
코드 설명
Chained IRQ handler는 하드웨어 인터럽트 컨텍스트(Hard IRQ Context)에서 실행되는 2차 디스패처입니다. GPIO 컨트롤러처럼 메모리 맵(MMIO) 레지스터로 즉시 접근 가능한 컨트롤러에 사용됩니다 (drivers/gpio/gpiolib.c).
- chained_irq_enter()부모
irq_chip의irq_mask_ack()또는irq_mask()+irq_ack()를 호출합니다. 부모 컨트롤러(GIC 등)에서 이 인터럽트 라인을 마스크하여 중첩 인터럽트를 방지합니다 (kernel/irq/chip.c). - generic_handle_domain_irq()GPIO 도메인에서 하위 hwirq(GPIO 오프셋)에 대응하는 virq를 찾아 해당
irq_desc->handle_irq()를 호출합니다. 이것이 GPIO 핀에 등록된 디바이스 드라이버의 핸들러를 실행하는 진입점입니다. - chained_irq_exit()부모
irq_chip의irq_eoi()또는irq_unmask()를 호출하여 인터럽트 처리 완료를 알립니다. - irq_set_chained_handler_and_data()부모 IRQ(GIC의 SPI)에 이 chained handler를 설치합니다. 설치 즉시 해당 IRQ의 흐름 핸들러가
gpio_irq_handler로 교체되므로, 기존에 등록된request_irq()핸들러와 공존할 수 없습니다.
Nested IRQ Handler
Nested handler는 I2C/SPI처럼 슬로우 버스(Bus) 뒤의 컨트롤러에 사용됩니다. IRQ 컨텍스트에서 I2C 통신은 불가능하므로, threaded IRQ로 처리합니다:
/* Nested IRQ: MFD (I2C) PMIC 인터럽트 */
static irqreturn_t pmic_irq_thread(int irq, void *data)
{
struct pmic_data *pmic = data;
unsigned int status;
/* I2C 통신으로 상태 읽기 (thread 컨텍스트) */
regmap_read(pmic->regmap, PMIC_INT_STATUS, &status);
for_each_set_bit(bit, &status, pmic->nirqs) {
handle_nested_irq(
irq_find_mapping(pmic->irq_domain, bit));
}
return IRQ_HANDLED;
}
/* 부모 IRQ: threaded로 요청 */
request_threaded_irq(parent_irq, NULL,
pmic_irq_thread, IRQF_ONESHOT, "pmic", pmic);
코드 설명
Nested IRQ handler는 I2C/SPI 버스 뒤의 컨트롤러(PMIC, MFD 등)에 사용됩니다. 슬로우 버스 통신은 슬립이 필요하므로 하드 IRQ 컨텍스트에서 실행할 수 없고, 반드시 threaded IRQ로 처리해야 합니다 (kernel/irq/manage.c).
- pmic_irq_thread()전용 커널 스레드에서 실행되므로
regmap_read()같은 I2C 통신이 가능합니다. Chained handler와 달리 슬립이 허용되는 프로세스 컨텍스트입니다. - handle_nested_irq()하위 IRQ의 핸들러(
irqaction->thread_fn)를 현재 스레드 컨텍스트에서 직접 호출합니다. Chained의generic_handle_domain_irq()와 달리, 새 스레드를 생성하지 않고 호출자의 스레드에서 중첩 실행됩니다 (kernel/irq/chip.c). - IRQF_ONESHOTthreaded handler가 완료될 때까지 부모 IRQ 라인을 재활성화하지 않습니다. 이 플래그 없이는 handler가 실행 중에 같은 인터럽트가 다시 발생하여 I2C 버스 충돌이 발생할 수 있습니다.
- handler = NULL
request_threaded_irq()의 첫 번째 핸들러(hard IRQ)를NULL로 설정하면, 커널이 자동으로irq_default_primary_handler()를 사용하여 즉시IRQ_WAKE_THREAD를 반환합니다.
Chained vs Nested 비교
| 속성 | Chained | Nested (Threaded) |
|---|---|---|
| 실행 컨텍스트 | 하드웨어 IRQ (hardirq) | 커널 스레드 (process) |
| 버스 접근 | MMIO만 가능 | I2C/SPI/느린 버스 가능 |
| 지연 | 매우 낮음 | 상대적으로 높음 |
| 하위 IRQ 디스패치(Dispatch) | generic_handle_domain_irq() | handle_nested_irq() |
| 부모 IRQ 설정 | irq_set_chained_handler() | request_threaded_irq() |
| 대표 사용처 | GPIO, 온칩 MUX | I2C PMIC, SPI MFD |
| 수면(sleep) 허용 | 불가 | 가능 |
sleep 또는 mutex_lock을 절대 호출하면 안 됩니다. hardirq 컨텍스트에서 실행되므로 원자적(atomic) 연산만 허용됩니다. I2C/SPI 버스 접근이 필요하면 반드시 Nested 방식을 사용하세요.
IRQ Domain 테스트/디버깅
IRQ 도메인 관련 문제는 커널 부팅 실패, 디바이스 프로브(Probe) 실패, 인터럽트 미전달 등의 형태로 나타납니다. 체계적인 디버깅 방법을 숙지해야 합니다.
debugfs: IRQ 도메인 정보
# IRQ 도메인 목록 확인
ls /sys/kernel/debug/irq/domains/
# 출력 예: GICv3 ITS@0x2f020000 gpio-mxc ...
# 특정 도메인 상세 정보
cat /sys/kernel/debug/irq/domains/GICv3
# name: GICv3
# size: 1020
# mapped: 87
# flags: 0x00000041
# - HIERARCHY
# - NAME_ALLOCATED
# 전체 IRQ 매핑 정보
cat /sys/kernel/debug/irq/irqs/32
# handler: handle_fasteoi_irq
# status: 0x00004000 (IRQD_IRQ_STARTED)
# domain: GICv3
# hwirq: 32
# chip: GICv3
계층 검증: irq_domain_check_hierarchy
/* 도메인이 계층적인지 확인 */
static inline bool irq_domain_is_hierarchy(
struct irq_domain *domain)
{
return domain->flags & IRQ_DOMAIN_FLAG_HIERARCHY;
}
/* 도메인 플래그 의미 */
#define IRQ_DOMAIN_FLAG_HIERARCHY (1 << 0) /* 계층적 도메인 */
#define IRQ_DOMAIN_FLAG_IPI_PER_CPU (1 << 2) /* per-CPU IPI */
#define IRQ_DOMAIN_FLAG_IPI_SINGLE (1 << 3) /* 단일 IPI */
#define IRQ_DOMAIN_FLAG_MSI (1 << 4) /* MSI 도메인 */
#define IRQ_DOMAIN_FLAG_MSI_REMAP (1 << 5) /* MSI 리매핑 */
동적 매핑 누수 감지
# IRQ 매핑 누수 감지: 등록된 핸들러 없는 virq 확인
for irq in /proc/irq/*/; do
virq=$(basename "$irq")
[ "$virq" = "default_smp_affinity" ] && continue
handlers=$(cat "/proc/irq/$virq/spurious" 2>/dev/null | grep "count")
if [ -z "$handlers" ]; then
echo "Potential leak: virq $virq"
fi
done
# /proc/interrupts에서 0 카운트 IRQ 확인
awk '$2 == 0 && NF > 2' /proc/interrupts
ftrace IRQ 도메인 트레이싱
# IRQ 핸들러 진입/종료 트레이싱
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
# IRQ 도메인 alloc/free 트레이싱 (function tracer)
echo 'irq_domain_alloc_irqs' >> /sys/kernel/debug/tracing/set_ftrace_filter
echo 'irq_domain_free_irqs' >> /sys/kernel/debug/tracing/set_ftrace_filter
echo 'irq_create_mapping_affinity' >> /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 트레이스 결과 확인
cat /sys/kernel/debug/tracing/trace
# irq_domain_alloc_irqs <-- its_irq_domain_alloc
# irq_domain_set_hwirq_and_chip
# ...
# irq_handler_entry: irq=32 name=eth0
일반적 디버깅 시나리오
| 증상 | 가능한 원인 | 진단 방법 | 해결 |
|---|---|---|---|
디바이스 프로브 실패"no irq" | DT/ACPI 인터럽트 바인딩 오류 | of_irq_get() 반환값 확인,DT 바인딩 검증 | interrupt-parent, #interrupt-cells 수정 |
| 인터럽트 미전달 | 매핑 누락 또는 도메인 불일치 | /proc/interrupts 카운터 확인,debugfs 도메인 mapped 수 | irq_create_mapping() 호출 확인 |
| spurious IRQ 다량 | 트리거 타입 불일치 (level vs edge) | cat /proc/irq/N/spurious | DT flags 또는 irq_set_irq_type() 수정 |
부팅 시 패닉"irq: no parent" | 계층적 도메인 parent 미설정 | 부트 로그 분석 | irq_domain_create_hierarchy() parent 인자 확인 |
| IRQ affinity 실패 | 도메인이 affinity 미지원 | /proc/irq/N/smp_affinity쓰기 오류 확인 | irq_chip에 .irq_set_affinity 구현 |
dmesg | grep -i irq로 IRQ 도메인 초기화 로그 확인/sys/kernel/debug/irq/domains/에서 각 도메인의 매핑 수 확인/proc/interrupts에서 인터럽트 카운터가 증가하는지 확인- ftrace로
irq_handler_entry이벤트 추적 - 계층적 도메인이면 각 계층의 alloc/activate 콜백 동작 확인
가상화(Virtualization) 환경 IRQ Domain
가상화 환경에서는 호스트와 게스트 사이의 인터럽트 전달 경로가 추가적인 계층을 형성합니다. KVM, Xen, Hyper-V 각각 다른 방식으로 IRQ 도메인을 활용합니다.
KVM irqchip: irqfd와 IRQ 라우팅
/* KVM IRQ 라우팅 테이블 설정 */
struct kvm_irq_routing_entry {
__u32 gsi; /* 게스트 GSI 번호 */
__u32 type; /* KVM_IRQ_ROUTING_IRQCHIP/MSI/... */
__u32 flags;
union {
struct kvm_irq_routing_irqchip irqchip;
struct kvm_irq_routing_msi msi;
} u;
};
/* irqfd: eventfd를 게스트 IRQ에 연결 */
struct kvm_irqfd {
__u32 fd; /* eventfd 파일 디스크립터 */
__u32 gsi; /* 게스트 GSI */
__u32 flags; /* KVM_IRQFD_FLAG_DEASSIGN 등 */
};
/* irqfd 동작 흐름:
* 1. VFIO가 물리 MSI를 eventfd로 전달
* 2. KVM의 irqfd wakeup 콜백 실행
* 3. kvm_set_irq() → vGIC/vAPIC에 주입
* 4. 게스트 진입 시 가상 인터럽트 전달 */
VFIO 인터럽트: eventfd → MSI Domain
/* VFIO PCI 디바이스의 MSI 설정 */
static int vfio_msi_set_vector_signal(
struct vfio_pci_core_device *vdev,
int vector, int fd)
{
struct eventfd_ctx *trigger;
int irq, ret;
irq = pci_irq_vector(vdev->pdev, vector);
trigger = eventfd_ctx_fdget(fd);
/* 물리 MSI IRQ에 eventfd 트리거 핸들러 연결 */
ret = request_irq(irq, vfio_msihandler, 0,
vdev->ctx[vector].name, trigger);
return ret;
}
/* 핸들러: 물리 MSI → eventfd → KVM irqfd → 게스트 */
static irqreturn_t vfio_msihandler(int irq, void *arg)
{
struct eventfd_ctx *trigger = arg;
eventfd_signal(trigger, 1);
return IRQ_HANDLED;
}
Xen Event Channel
/* Xen 이벤트 채널 → virq 매핑 */
/* drivers/xen/events/events_base.c */
static struct irq_domain *xen_evtchn_domain;
/* Xen 이벤트 채널 바인딩 */
int bind_evtchn_to_irq(evtchn_port_t evtchn)
{
int irq;
struct irq_info *info;
/* 이벤트 채널 → virq 매핑 생성 */
irq = xen_allocate_irq_dynamic();
info = xen_irq_init(irq);
info->type = IRQT_EVTCHN;
info->evtchn = evtchn;
irq_set_chip_and_handler_name(irq,
&xen_dynamic_chip, handle_edge_irq, "event");
bind_evtchn_to_cpu(evtchn, 0);
return irq;
}
Hyper-V SINT (Synthetic Interrupt)
/* Hyper-V 합성 인터럽트 소스 */
/* drivers/hv/hv.c */
enum hv_message_type {
HVMSG_NONE = 0x00000000,
HVMSG_TIMER_EXPIRED = 0x80000010,
HVMSG_VMBUS_INTERCEPT = 0x00000001,
};
/* SINT (Synthetic Interrupt Source):
* - Hyper-V는 SINT 0~15까지 16개 합성 인터럽트 소스 제공
* - VMBus 메시지, 타이머 등에 사용
* - 게스트에서 MSR로 SINT 벡터 설정
* - 호스트→게스트 인터럽트: SINT → Local APIC Vector → IDT */
/* VMBus 인터럽트 핸들러 등록 */
hv_setup_vmbus_handler(vmbus_isr);
/* 내부: ISR → tasklet → vmbus_on_msg_dpc() / vmbus_on_event() */
| 가상화 방식 | 인터럽트 메커니즘 | IRQ 도메인 역할 | 게스트 인터럽트 주입 |
|---|---|---|---|
| KVM | irqfd + vGIC/vAPIC | 호스트 IRQ domain은 그대로, KVM이 가상 IRQ 라우팅 | vCPU 진입 시 pending IRQ 주입 |
| VFIO | eventfd → irqfd | 물리 MSI domain 매핑 유지, eventfd로 게스트 전달 | irqfd wakeup → kvm_set_irq() |
| Xen | Event Channel | Xen 전용 evtchn irq_domain | 하이퍼바이저(Hypervisor) 이벤트 채널 알림 |
| Hyper-V | SINT + VMBus | hv_irq_chip + Local APIC | MSR 기반 SINT 벡터 주입 |
CONFIG_KVM_POSTED_INTR로 활성화됩니다.
커널 설정 및 운영 체크리스트
IRQ 도메인 관련 커널 설정 옵션과 운영 환경에서의 인터럽트 관리 체크리스트를 정리합니다.
커널 설정 옵션
| 설정 | 기본값 | 설명 | 의존성 |
|---|---|---|---|
CONFIG_IRQ_DOMAIN | y (자동) | IRQ 도메인 인프라 활성화 | 인터럽트 컨트롤러 드라이버 선택 시 자동 |
CONFIG_IRQ_DOMAIN_HIERARCHY | y (자동) | 계층적 도메인 지원 | GICv3, ITS, MSI 사용 시 필수 |
CONFIG_SPARSE_IRQ | y | 동적 IRQ 디스크립터 할당 | 대부분의 플랫폼에서 기본 활성 |
CONFIG_GENERIC_MSI_IRQ | y (자동) | 범용 MSI IRQ 도메인 | PCI MSI/MSI-X 사용 시 자동 |
CONFIG_IRQ_DOMAIN_DEBUG | n | debugfs에 도메인 정보 노출 | 디버깅 시 수동 활성화 |
CONFIG_GENERIC_IRQ_DEBUGFS | n | 상세 IRQ debugfs 인터페이스 | /sys/kernel/debug/irq/ 활성화 |
CONFIG_GENERIC_IRQ_IPI | y (자동) | IPI(Inter-Processor Interrupt) 지원 | SMP 시스템에서 자동 |
CONFIG_ARM_GIC_V3_ITS | y | GICv3 ITS 드라이버 | ARM64 + GICv3 시스템 |
# 현재 커널의 IRQ 도메인 관련 설정 확인
zcat /proc/config.gz 2>/dev/null | grep -E 'IRQ_DOMAIN|SPARSE_IRQ|GENERIC_MSI'
# 또는
grep -E 'IRQ_DOMAIN|SPARSE_IRQ|GENERIC_MSI' /boot/config-$(uname -r)
# 결과 예시:
# CONFIG_IRQ_DOMAIN=y
# CONFIG_IRQ_DOMAIN_HIERARCHY=y
# CONFIG_SPARSE_IRQ=y
# CONFIG_GENERIC_MSI_IRQ=y
# CONFIG_GENERIC_IRQ_DEBUGFS=y
인터럽트 모니터링
# /proc/interrupts: CPU별 인터럽트 카운터
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 32: 15423 12045 9876 11234 GICv3 26 Level uart-pl011
# 33: 0 0 0 0 GICv3 27 Level timer
# 35: 1234567 2345678 0 0 GICv3 35 Edge eth0-rx-0
# IPI0: 987654 876543 765432 654321 Rescheduling interrupts
# IPI1: 12345 23456 34567 45678 Function call interrupts
# 특정 IRQ의 affinity 확인/설정
cat /proc/irq/35/smp_affinity # 비트마스크 (예: 03 = CPU0,1)
cat /proc/irq/35/smp_affinity_list # 리스트 (예: 0-1)
cat /proc/irq/35/effective_affinity # 실제 적용된 affinity
# affinity 변경 (CPU 2,3에만 배포)
echo 0c > /proc/irq/35/smp_affinity
# affinity_hint (드라이버 권장값)
cat /proc/irq/35/affinity_hint
irqbalance 설정
# irqbalance: 자동 IRQ 분배 데몬
systemctl status irqbalance
# 특정 IRQ를 irqbalance에서 제외 (IRQBALANCE_BANNED_IRQS)
# /etc/sysconfig/irqbalance 또는 /etc/default/irqbalance
IRQBALANCE_BANNED_IRQS="32 33"
# 특정 CPU를 irqbalance에서 제외
IRQBALANCE_BANNED_CPUS="0000000c" # CPU 2,3 제외
# 정책 기반 분배 (policy script)
IRQBALANCE_ARGS="--policyscript=/etc/irqbalance/policy.sh"
# irqbalance 디버그 모드
irqbalance --foreground --debug
IRQ Storm 대응 체크리스트
| 단계 | 조치 | 명령/방법 |
|---|---|---|
| 1. 식별 | 빠르게 증가하는 IRQ 번호 확인 | watch -n1 'cat /proc/interrupts | sort -rnk2 | head' |
| 2. 원인 파악 | 해당 IRQ의 디바이스/드라이버 확인 | cat /proc/irq/N/actions |
| 3. 임시 완화 | 해당 IRQ의 affinity를 단일 CPU로 제한 | echo 01 > /proc/irq/N/smp_affinity |
| 4. 커널 자동 감지 | spurious IRQ 임계값 확인 (99,900회) | cat /proc/irq/N/spurious |
| 5. 비활성화 | 최후 수단: IRQ 비활성화 | echo 1 > /proc/irq/N/disabled |
| 6. 근본 해결 | 드라이버 버그/HW 결함 수정 | 트리거 타입, ACK 누락, 공유 IRQ 충돌 확인 |
/* 커널의 spurious IRQ 자동 감지 메커니즘 */
/* kernel/irq/spurious.c */
/* 99,900번 중 처리되지 않은 IRQ가 99,000번 이상이면 비활성화 */
if (desc->irqs_unhandled > 99000) {
__report_bad_irq(desc, action_ret);
desc->istate |= IRQS_SPURIOUS_DISABLED;
desc->depth++;
irq_disable(desc);
mod_timer(&desc->poll_timer,
jiffies + HZ/10); /* 폴링 모드 전환 */
pr_err("irq %d: nobody cared (try booting with irqpoll)\n",
desc->irq_data.irq);
}
- 실시간 모니터링:
mpstat -I ALL 1로 CPU별 인터럽트율 모니터링 - NUMA 최적화: NIC IRQ를 해당 NIC의 NUMA 노드 CPU에 고정 (
set_irq_affinity.sh) - 부팅 옵션:
irqpoll- 폴링 모드,irqfixup- misrouted IRQ 복구 시도 - RPS/RFS: 소프트웨어 기반 수신 패킷(Packet) 분배로 NIC IRQ 부하 분산(Load Balancing)
Forced IRQ Threading (PREEMPT_RT)
PREEMPT_RT 패치(Real-Time Preemption Patch)에서는 대부분의 하드웨어 인터럽트 핸들러가 커널 스레드(Kernel Thread)로 강제 변환됩니다. 이를 Forced IRQ Threading이라 하며, 인터럽트 처리를 프로세스 컨텍스트(Process Context)에서 수행함으로써 결정적 지연(Deterministic Latency)을 보장합니다. IRQ 도메인(IRQ Domain) 계층에서도 이 변환이 투명하게 적용되므로, irq_chip 콜백과 잠금(Lock) 설계 시 특별한 주의가 필요합니다.
일반 모드 vs PREEMPT_RT 비교
| 항목 | 일반 (Non-RT) | PREEMPT_RT |
|---|---|---|
| 핸들러 실행 컨텍스트 | 인터럽트 컨텍스트 (hardirq) | 프로세스 컨텍스트 (커널 스레드) |
| sleep 허용 | 불가 | 가능 (스레드 컨텍스트) |
| 우선순위 | 모든 프로세스보다 높음 | chrt로 RT 우선순위 조절 가능 |
| mutex 사용 | 불가 (spinlock만 가능) | 가능 (rt_mutex 기반 sleeping lock) |
| spinlock 동작 | 실제 spin (인터럽트 비활성화) | rt_mutex로 변환 (선점 가능) |
| irq_chip 콜백 | hardirq 컨텍스트에서 호출 | threaded 컨텍스트에서 호출 가능 |
강제 스레딩 변환 메커니즘
커널은 force_irqthreads()가 참일 때 request_irq()로 등록된 핸들러를 자동으로 스레드 핸들러로 변환합니다. 원래의 handler가 thread_fn으로 이동하고, handler는 스레드를 깨우기만 하는 irq_default_primary_handler로 교체됩니다.
/* kernel/irq/manage.c */
static int irq_setup_forced_threading(struct irqaction *new)
{
if (!force_irqthreads())
return 0;
if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
return 0;
/* hardirq handler → thread로 변환 */
new->thread_fn = new->handler;
new->handler = irq_default_primary_handler;
new->flags |= IRQF_ONESHOT;
return 0;
}
IRQ 도메인과 Forced Threading 상호작용
PREEMPT_RT 환경에서 irq_chip의 irq_mask() / irq_unmask() 등의 콜백은 스레드 컨텍스트에서 호출될 수 있습니다. 따라서 이들 콜백 내부에서 사용하는 잠금은 반드시 raw_spinlock이어야 합니다. 일반 spinlock은 PREEMPT_RT에서 rt_mutex로 변환되어 재진입 문제가 발생할 수 있습니다.
/* irq_chip 콜백이 threaded 컨텍스트에서 호출될 수 있음 */
/* PREEMPT_RT에서 irq_chip->irq_mask/unmask는
raw_spinlock으로 보호해야 함 */
static void my_chip_mask(struct irq_data *d)
{
struct my_priv *p = irq_data_get_irq_chip_data(d);
raw_spin_lock(&p->lock); /* PREEMPT_RT: raw_spinlock 필수 */
writel(BIT(d->hwirq), p->base + MASK_REG);
raw_spin_unlock(&p->lock);
}
IRQF_NO_THREAD플래그가 설정된 인터럽트는 강제 스레딩 대상에서 제외됩니다.- 타이머 인터럽트: 스케줄러 tick은 항상 hardirq 컨텍스트에서 처리되어야 합니다.
- IPI (Inter-Processor Interrupt): CPU 간 통신은 지연 없이 즉시 처리해야 합니다.
- IRQF_PERCPU: per-CPU 인터럽트도 강제 스레딩에서 제외됩니다.
irq_chip에서IRQCHIP_ONESHOT_SAFE플래그를 설정하면 IRQF_ONESHOT 없이도 스레딩이 안전함을 표시합니다.
/proc/irq/N/threads 디렉토리에서 각 IRQ에 연결된 커널 스레드 정보를 확인할 수 있습니다. ps -eo pid,cls,pri,comm | grep irq/ 명령으로 IRQ 스레드의 스케줄링 클래스와 우선순위를 조회할 수 있으며, chrt -p PID로 개별 IRQ 스레드의 RT 우선순위를 조절할 수 있습니다.
커널 소스 파일 참조
IRQ 도메인 서브시스템과 관련된 주요 커널 소스 파일입니다. 각 파일의 역할을 파악하면 코드 분석과 디버깅 시 빠르게 해당 지점을 찾을 수 있습니다.
| 파일 경로 | 설명 |
|---|---|
include/linux/irqdomain.h | IRQ 도메인 핵심 구조체 및 API 선언 |
include/linux/irq.h | irq_chip, irq_data, irq_desc 구조체 선언 |
include/linux/interrupt.h | request_irq(), irqaction, IRQF_* 플래그 |
kernel/irq/irqdomain.c | IRQ 도메인 핵심 구현 (create, map, associate) |
kernel/irq/chip.c | Flow handler 구현 (handle_level_irq, handle_fasteoi_irq 등) |
kernel/irq/manage.c | request_irq(), free_irq(), IRQ 스레딩 |
kernel/irq/irqdesc.c | irq_desc 관리, generic_handle_domain_irq() |
kernel/irq/msi.c | MSI 도메인 핵심 코드 |
kernel/irq/spurious.c | Spurious IRQ 감지 및 처리 |
kernel/irq/debugfs.c | IRQ debugfs 인터페이스 |
drivers/irqchip/irq-gic-v3.c | ARM GICv3 드라이버 |
drivers/irqchip/irq-gic-v3-its.c | GICv3 ITS 드라이버 |
drivers/irqchip/irq-sifive-plic.c | RISC-V PLIC 드라이버 |
drivers/irqchip/irq-riscv-aplic-*.c | RISC-V APLIC 드라이버 |
arch/x86/kernel/apic/io_apic.c | x86 I/O APIC 드라이버 |
arch/x86/kernel/apic/vector.c | x86 Vector 도메인 |
drivers/pci/msi/irqdomain.c | PCI MSI IRQ 도메인 |
drivers/gpio/gpiolib.c | GPIO IRQ 도메인 통합 |
Documentation/core-api/irq/irq-domain.rst에서 확인할 수 있습니다. 최신 커널 소스의 Documentation/ 디렉토리에는 IRQ 서브시스템 전반에 대한 설계 문서와 API 레퍼런스가 포함되어 있습니다.
Flow Handler 상세
인터럽트 흐름 처리기(Flow Handler)는 하드웨어 인터럽트 신호가 도착했을 때 irq_chip 콜백(mask, ack, eoi 등)을 어떤 순서로 호출할지 결정하는 핵심 디스패치 함수입니다. 각 인터럽트 컨트롤러의 특성(레벨/엣지 트리거, EOI 방식, Per-CPU 여부)에 따라 적절한 흐름 처리기가 선택되며, 이 선택이 잘못되면 인터럽트 유실이나 중복 처리가 발생합니다.
Flow Handler 비교 표
| 핸들러 | 트리거 | chip 콜백 순서 | 대표 사용처 | 핸들러 설정 함수 |
|---|---|---|---|---|
handle_level_irq | Level | mask → ack → handler → unmask | I/O APIC level, GPIO level | irq_set_handler() |
handle_edge_irq | Edge | ack → handler → (re-trigger if new edge) | Legacy PIC, GPIO edge | irq_set_handler() |
handle_fasteoi_irq | Level/Edge | handler → eoi | GIC SPI | irq_domain_set_info() |
handle_percpu_devid_irq | Per-CPU | ack → handler → eoi | GIC PPI/SGI | irq_set_percpu_devid() |
handle_simple_irq | 단순 | handler only | 소프트웨어 생성 IRQ | - |
handle_bad_irq | 오류 | WARN + count | 미설정 IRQ (기본) | - |
handle_fasteoi_irq 구현
GIC 기반 시스템에서 가장 널리 사용되는 흐름 처리기로, 핸들러 실행 후 단순히 EOI(End of Interrupt)만 전송하는 구조입니다:
/* kernel/irq/chip.c */
void handle_fasteoi_irq(struct irq_desc *desc)
{
struct irq_chip *chip = desc->irq_data.chip;
raw_spin_lock(&desc->lock);
if (!irq_may_run(desc))
goto out;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
goto out;
}
kstat_incr_irqs_this_cpu(desc);
if (desc->istate & IRQS_ONESHOT)
mask_irq(desc);
handle_irq_event(desc); /* 등록된 핸들러 실행 */
cond_unmask_eoi_irq(desc, chip); /* unmask + eoi 또는 eoi만 */
raw_spin_unlock(&desc->lock);
return;
out:
if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
chip->irq_eoi(&desc->irq_data); /* 비활성 IRQ도 EOI 필요 */
raw_spin_unlock(&desc->lock);
}
handle_edge_irq는 핸들러 실행 중 새로운 엣지 신호가 도착할 수 있기 때문에, do-while 루프로 IRQS_PENDING 플래그를 반복 확인합니다. 레벨 트리거와 달리 엣지 트리거는 신호가 순간적이므로, 이 메커니즘 없이는 핸들러 실행 중 도착한 인터럽트를 영구히 유실하게 됩니다. 핸들러 내에서 IRQS_PENDING이 설정되면 루프를 다시 돌아 handle_irq_event()를 재실행합니다.
handle_edge_irq 구현
엣지 트리거 인터럽트의 핵심은 ack 후 handler를 실행하며, 실행 중 새 엣지가 발생하면 재처리하는 do-while 루프입니다:
void handle_edge_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
if (!irq_may_run(desc)) {
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out_unlock;
}
chip->irq_ack(&desc->irq_data); /* 즉시 ACK → 새 엣지 감지 허용 */
do {
if (unlikely(!desc->action)) {
mask_irq(desc);
goto out_unlock;
}
if (unlikely(desc->istate & IRQS_PENDING)) {
if (!irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_irq(desc); /* 마스크 해제하여 재감지 */
}
desc->istate &= ~IRQS_PENDING;
handle_irq_event(desc); /* 핸들러 실행 */
} while ((desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data));
out_unlock:
raw_spin_unlock(&desc->lock);
}
RISC-V PLIC/APLIC
RISC-V 아키텍처는 인터럽트 컨트롤러를 ISA 수준에서 표준화합니다. 초기 스펙에서는 PLIC(Platform-Level Interrupt Controller)가 외부 인터럽트를 관리했으며, AIA(Advanced Interrupt Architecture) 스펙에서 APLIC(Advanced PLIC)과 IMSIC(Incoming MSI Controller)이 추가되어 MSI 기반의 확장 가능한 인터럽트 처리를 지원합니다. CLINT(Core Local Interruptor)는 타이머와 소프트웨어 인터럽트를 담당합니다.
PLIC vs APLIC vs IMSIC 비교
| 항목 | PLIC | APLIC | IMSIC |
|---|---|---|---|
| 스펙 | RISC-V Privileged Spec | RISC-V AIA | RISC-V AIA |
| 트리거 타입 | Level / Edge | Level / Edge | MSI (메모리 쓰기) |
| MSI 지원 | 미지원 | MSI 모드 가능 (IMSIC 연동) | 네이티브 MSI |
| IRQ 도메인 타입 | Linear | Linear / Hierarchy (MSI 모드) | Hierarchy |
| 최대 인터럽트 | 1024 (스펙 상한) | 1024 | 2048 per hart |
| 우선순위 | 소프트웨어 설정 (0~7) | 소프트웨어 설정 | 없음 (FIFO) |
| Device Tree compatible | sifive,plic-1.0.0, riscv,plic0 | riscv,aplic | riscv,imsic |
PLIC IRQ 도메인 코드
SiFive PLIC 드라이버는 irq_domain_ops를 통해 hwirq를 virq에 매핑하며, handle_fasteoi_irq를 기본 흐름 처리기로 설정합니다:
/* drivers/irqchip/irq-sifive-plic.c */
static const struct irq_domain_ops plic_irqdomain_ops = {
.map = plic_irqdomain_map,
.alloc = plic_irq_domain_alloc,
.free = plic_irq_domain_free,
.xlate = irq_domain_xlate_onetwocell,
};
static int plic_irqdomain_map(struct irq_domain *d,
unsigned int irq,
irq_hw_number_t hwirq)
{
struct plic_priv *priv = d->host_data;
irq_domain_set_info(d, irq, hwirq,
&plic_chip, priv,
handle_fasteoi_irq, NULL, NULL);
irq_set_affinity(irq, &priv->lmask);
return 0;
}
APLIC MSI 모드
AIA 스펙의 APLIC는 직접 배선(Direct) 모드와 MSI 모드를 모두 지원합니다. MSI 모드에서는 APLIC가 인터럽트를 IMSIC로 변환하여 전달하며, 계층적(Hierarchy) IRQ 도메인을 구성합니다:
/* drivers/irqchip/irq-riscv-aplic-msi.c */
static const struct irq_domain_ops aplic_msi_irqdomain_ops = {
.alloc = aplic_msi_irq_domain_alloc,
.free = aplic_msi_irq_domain_free,
.xlate = irq_domain_xlate_twocell,
};
/* APLIC MSI 모드 초기화 흐름:
* 1. APLIC → IMSIC 부모 도메인 참조 획득
* 2. irq_domain_create_hierarchy()로 계층 도메인 생성
* 3. sourcecfg 레지스터에 트리거 타입 설정
* 4. target 레지스터에 대상 hart/guest 설정 */
RISC-V Device Tree 예제
plic: interrupt-controller@c000000 {
compatible = "sifive,plic-1.0.0", "riscv,plic0";
#interrupt-cells = <1>;
interrupt-controller;
reg = <0x0c000000 0x4000000>;
interrupts-extended = <&cpu0_intc 11>, <&cpu0_intc 9>,
<&cpu1_intc 11>, <&cpu1_intc 9>;
riscv,ndev = <96>; /* 외부 인터럽트 소스 수 */
};
/* interrupts-extended 해석:
* cpu0_intc 11 = M-mode 외부 인터럽트 (MEI)
* cpu0_intc 9 = S-mode 외부 인터럽트 (SEI)
* 각 hart마다 M-mode, S-mode 컨텍스트를 별도 등록 */
- ARM GIC: SPI(공유)/PPI(Per-CPU)/SGI(소프트웨어) 3종 인터럽트 타입, Distributor + Redistributor + CPU Interface 3단계 계층, 1024개 SPI 지원, Affinity Routing으로 특정 hart 지정
- x86 APIC: Local APIC(Per-CPU) + I/O APIC(외부) 2단계, 벡터 기반 (0-255), MSI/MSI-X가 I/O APIC를 대체하는 추세, ACPI MADT 테이블로 토폴로지 기술
- RISC-V PLIC: 단순 우선순위 기반 중재, hart별 claim/complete 레지스터로 인터럽트 수락/완료, AIA(APLIC+IMSIC)로 진화하여 MSI 네이티브 지원과 가상화 효율성 개선
참고 자료
커널 공식 문서
- irq_domain — The IRQ Domain Mapping Library — IRQ 도메인 API의 공식 커널 문서로, 매핑 유형(linear, tree, nomap)과 계층적 도메인 사용법을 설명합니다.
- IRQ Concepts — What is an IRQ? — 하드웨어 인터럽트 번호(hwirq)와 리눅스 가상 IRQ 번호의 개념 차이를 다룹니다.
- Linux Generic IRQ Handling — 리눅스 제네릭 IRQ 서브시스템의 전체 아키텍처와 흐름 처리기(flow handler) 설계를 설명합니다.
- MSI-HOWTO — PCI MSI/MSI-X — PCI MSI/MSI-X 인터럽트의 IRQ 도메인 통합 방법과 드라이버에서의 사용법을 다룹니다.
- Device Tree Interrupt Binding — Device Tree에서 인터럽트 컨트롤러와 interrupt-cells, interrupt-parent, interrupt-map 바인딩 규칙을 설명합니다.
LWN.net 기사
- irq_domain — scalable interrupt numbering (2012) — Grant Likely가 irq_domain 서브시스템을 도입한 배경과 설계 원칙을 설명한 기사입니다.
- Hierarchical IRQ domains (2013) — 계층적 IRQ 도메인의 도입 배경과 구현 방식을 다룹니다. 부모-자식 도메인 간 alloc/free 전파 메커니즘을 설명합니다.
- Platform MSI interrupts (2016) — PCI 외 디바이스에서 MSI를 사용하기 위한 플랫폼 MSI 도메인 설계를 다룹니다.
- Per-device MSI domains (2014) — MSI 인터럽트를 IRQ 도메인 계층 구조로 통합하는 과정을 설명합니다.
- Interrupt handling with irq_chip — irq_chip 콜백 구조체와 flow handler의 관계를 깊이 있게 분석합니다.
커널 소스 (Bootlin Elixir)
- kernel/irq/irqdomain.c — IRQ 도메인 핵심 구현으로 irq_domain_create_*(), irq_create_mapping(), irq_domain_associate() 등이 정의되어 있습니다.
- include/linux/irqdomain.h — irq_domain, irq_domain_ops, irq_fwspec 구조체와 매핑 API 선언 헤더입니다.
- kernel/irq/chip.c — handle_level_irq, handle_edge_irq, handle_fasteoi_irq 등 flow handler 구현 코드입니다.
- kernel/irq/msi.c — MSI 도메인 생성, msi_domain_alloc_irqs() 등 MSI 계층 도메인 구현입니다.
- drivers/irqchip/irq-gic-v3.c — ARM GICv3 드라이버로 계층적 IRQ 도메인 활용의 대표적 구현 사례입니다.
- drivers/irqchip/irq-gic-v3-its.c — GICv3 ITS(Interrupt Translation Service) 드라이버로, MSI를 LPI로 변환하는 계층 도메인을 구현합니다.
- Documentation/core-api/irq/irq-domain.rst — 커널 트리 내 IRQ 도메인 공식 문서 원본 RST 파일입니다.
발표 및 블로그
- Porting Linux to a New SoC — IRQ Domain Setup (eLinux) — 새 SoC 포팅 시 인터럽트 컨트롤러 드라이버와 IRQ 도메인 설정 과정을 설명하는 발표 자료입니다.
- GPIO Driver — IRQ Chip Implementation — GPIO 드라이버에서 IRQ 도메인을 통합하는 방법과 gpiolib의 irq_chip 지원을 다룹니다.
- Linux Kernel Interrupt Subsystem (YouTube) — 리눅스 커널 인터럽트 서브시스템의 전체 구조와 IRQ 도메인의 역할을 시각적으로 설명하는 컨퍼런스 발표입니다.
Device Tree 바인딩
- arm,gic-v3.yaml — ARM GICv3 인터럽트 컨트롤러의 Device Tree 바인딩 스키마입니다. interrupt-cells 규칙과 ITS 서브노드 구조를 정의합니다.
- sifive,plic-1.0.0.yaml — RISC-V PLIC 인터럽트 컨트롤러의 Device Tree 바인딩으로 interrupts-extended 속성의 사용법을 명시합니다.
- riscv,aplic.yaml — RISC-V AIA APLIC 인터럽트 컨트롤러의 Device Tree 바인딩으로 Direct 모드와 MSI 모드의 속성 차이를 정의합니다.
- interrupt-controller.yaml — 인터럽트 컨트롤러 공통 바인딩 스키마로, interrupt-controller, #interrupt-cells 등 필수 속성을 정의합니다.