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 도메인 확장까지 단계별로 다루며, 컨트롤러 드라이버 작성 시 자주 발생하는 매핑 누락·중복 할당·해제 순서 오류를 실제 점검 항목 중심으로 정리했습니다.

전제 조건: 커널 아키텍처 문서를 먼저 읽으세요. CPU, 메모리, 인터럽트(Interrupt)의 기본 흐름을 알고 있으면 본 문서를 더 빠르게 이해할 수 있습니다.
일상 비유: 이 개념은 전화번호 국번 체계와 비슷합니다. 각 지역(인터럽트 컨트롤러)은 자체 내선번호(hwirq)를 가지지만, 전국 전화망(커널)에서 통화하려면 고유한 전화번호(virq)가 필요합니다. IRQ 도메인은 내선번호를 전국 번호로 변환하는 교환기 역할을 합니다.

핵심 요약

  • 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(다계층)

단계별 이해

  1. 구조체 파악
    irq_domain, irq_chip, irq_data 세 구조체의 관계를 먼저 이해합니다.
  2. 매핑 흐름 추적
    펌웨어 파싱(DT/ACPI) → xlate → map → virq 할당까지의 경로를 따라갑니다.
  3. 계층 구조 이해
    MSI → ITS → GIC처럼 도메인이 중첩되는 경우의 alloc/free 체인을 확인합니다.
  4. 디버깅(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 Domain3.11+parent/child 트리 구조현재 표준 (MSI, IOMMU, GPIO 통합)

Flat IRQ 시절에는 NR_IRQS를 플랫폼마다 고정값으로 정의했습니다. ARM SoC가 다양해지면서 hwirq 번호가 컨트롤러 간에 겹치는 문제가 심각해졌고, 이를 해결하기 위해 irq_domain 추상화가 도입되었습니다.

핵심 개념

개념설명예제
hwirq하드웨어 인터럽트 번호GIC의 SPI 32번
virqLinux 가상 IRQ 번호request_irq(45, ...)
IRQ 도메인hwirq → virq 매핑 관리자GIC 도메인, GPIO 도메인
irq_chip인터럽트 컨트롤러 opsmask/unmask/ack 함수
irq_datavirq-hwirq 바인딩 메타데이터chip, domain, hwirq, mask 정보
fwnode펌웨어 노드 핸들DT device_node, ACPI fwnode

hwirq → virq 매핑 흐름

Device Tree 또는 ACPI에서 인터럽트 정보를 파싱하여 virq를 할당하기까지의 전체 과정입니다.

DT / ACPI interrupts 속성 파싱 xlate() intspec → hwirq+type irq_create_mapping 중복 확인 후 진행 irq_alloc_desc virq 번호 할당 domain→ops→map() irq_chip + handler 설정 irq_domain_associate revmap에 hwirq→virq 저장 virq 반환 (완료) request_irq()로 핸들러 등록 가능 역방향 조회: irq_find_mapping(domain, hwirq) revmap(배열/radix tree)에서 virq 검색 정방향: irq_get_irq_data(virq)→hwirq irq_desc→irq_data→hwirq 필드 참조 펌웨어 파싱 변환/연결 할당 완료 하드웨어 설정

Generic IRQ Layer 전체 구조

리눅스 커널의 Generic IRQ Layer는 여러 핵심 구조체가 유기적으로 연결되어 인터럽트를 관리합니다. 아래 다이어그램은 irq_desc, irq_data, irq_chip, irq_domain, irqaction의 관계와, 계층형 도메인(Hierarchical Domain)에서의 parent_data 체인을 한눈에 보여줍니다.

irq_desc (virq별 1개, include/linux/irqdesc.h) irq_data irq (virq), hwirq *chip, *domain *parent_data *chip_data, mask irqaction handler() thread_fn() flags, name *next → irqaction (공유 IRQ) handle_irq (흐름 제어 함수) irq_chip (include/linux/irq.h) irq_ack(), irq_mask() irq_unmask(), irq_eoi() irq_set_type(), irq_set_affinity() *chip irq_domain (include/linux/irqdomain.h) *ops (xlate, map, alloc, free) revmap (linear[] / radix_tree) *parent, *fwnode, *host_data *domain MSI irq_data MSI 도메인 ITS irq_data ITS 도메인 GIC irq_data GIC 도메인 parent_data parent_data 계층형 도메인의 parent_data 체인 irq_desc irq_data irq_chip irq_domain irqaction parent 체인

위 다이어그램에서 핵심 관계를 정리하면:

구조체역할소스 위치
irq_descvirq별 최상위 디스크립터(Descriptor) — 상태, 통계, 핸들러 체인 관리include/linux/irqdesc.h
irq_datavirq-hwirq 바인딩 메타데이터, chip/domain 참조include/linux/irq.h
irq_chip인터럽트 컨트롤러 하드웨어 제어 콜백(Callback) 집합include/linux/irq.h
irq_domainhwirq→virq 매핑 인프라, revmap 관리include/linux/irqdomain.h
irqactionrequest_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.cirq_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_irqsper-CPU 인터럽트 통계, /proc/interrupts에 표시되는 카운터
handle_irq흐름 제어 핸들러(Flow Handler) — handle_level_irq, handle_edge_irq, handle_fasteoi_irq
actionrequest_irq()로 등록된 irqaction 연결 리스트의 헤드
depthdisable_irq() 중첩 깊이, 0이면 활성 상태
lock이 IRQ 라인의 동시 접근을 보호하는 raw_spinlock
irq_desc와 irq_data의 관계: irq_datairq_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 vs irq_data: irq_desc는 virq별로 하나씩 존재하는 최상위 디스크립터이며, irq_data는 그 안에 포함된 도메인별 메타데이터입니다. 계층형 도메인에서는 한 virq에 대해 여러 irq_dataparent_data 포인터로 연결됩니다 (예: MSI irq_data → ITS irq_data → GIC irq_data).

irqaction 구조체

irqactionrequest_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()로 등록 시 전용 커널 스레드에서 실행
flagsIRQF_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()에서 호출됩니다. fwnodebus_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 콜백 시퀀스

각 콜백이 호출되는 시점과 순서를 타임라인으로 나타냅니다.

시간 match() 도메인 검색 xlate() hwirq 추출 alloc() 계층 전용 map() chip/handler 설정 unmap() 매핑 해제 free() 계층 해제 비계층(Flat) 도메인 경로: match → xlate → map → (사용) → unmap alloc/free는 호출되지 않음 irq_create_mapping() 내부에서 map() 호출 계층(Hierarchy) 도메인 경로: match → xlate → alloc → (사용) → free alloc()가 부모의 alloc()을 재귀 호출 map()은 alloc() 내부에서 간접 호출 주요 콜백 역할 요약: match: fwnode으로 도메인 탐색 | xlate: DT intspec을 hwirq+type으로 변환 | map: irq_chip/handler 바인딩 alloc: 계층별 자원 할당 (부모 체인) | free: 역순 자원 해제 | unmap: 단일 도메인 매핑 제거

매핑 타입

IRQ 도메인은 여러 매핑 전략을 지원합니다.

매핑 방식 비교

타입자료구조메모리조회 속도적합한 경우
Linear배열O(N)O(1)hwirq가 0부터 연속 (GPIO, 대부분의 IC)
TreeRadix TreeO(log N)O(log N)hwirq가 sparse (PCIe MSI)
No-Map없음O(1)-hwirq == virq (레거시)
Hierarchy다계층가변가변중첩된 컨트롤러 (MSI → GIC)

메모리/성능 트레이드오프

항목LinearTree (Radix)No-Map
초기 메모리size * sizeof(irq_data *)radix node만큼0
조회 시 캐시(Cache)배열 인덱싱 (1회 접근)트리 순회 (3-4회 접근)직접 매핑
hwirq 밀도 90%+최적메모리 낭비불가 (중복 시)
hwirq 밀도 1% 미만메모리 낭비최적불가
동적 확장불가 (고정 크기)자동 확장불필요
대표 사용처GIC(SPI 1020개), GPIOMSI-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_linearsize 인자만큼의 linear_revmap[] 배열을 할당합니다. hwirq 범위가 0부터 size-1까지 연속일 때 사용하며, O(1) 조회가 가능합니다. GIC SPI(최대 1020개)나 GPIO 컨트롤러에 적합합니다.
  • irq_domain_add_treesize를 0으로 전달하고 radix tree를 초기화합니다. ~0(UINT_MAX)을 최대 hwirq로 설정하여 사실상 무제한 범위를 허용합니다. PCIe MSI-X처럼 hwirq 번호가 sparse한 경우에 메모리 효율적입니다.
  • INIT_RADIX_TREEGFP_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_affinityCPU 친화성 변경타겟 CPU 설정 (멀티코어)

도메인 생명주기

IRQ 도메인은 생성 → 전역 리스트 등록 → 매핑 사용 → 매핑 해제 → 도메인 제거의 생명주기를 거칩니다.

__irq_domain_create() kzalloc + fwnode 연결 ops, host_data 설정 __irq_domain_add() irq_domain_list에 등록 debugfs 엔트리 생성 irq_create_mapping() virq 할당 + revmap 저장 ops->map() 호출 request_irq() / 인터럽트 처리 핸들러 등록 및 IRQ 활성 상태 irq_dispose_mapping() virq 해제 + revmap 제거 irq_domain_remove() 전역 리스트에서 제거 생명주기 핵심 코드 요약 생성: domain = irq_domain_create_linear(fwnode, size, ops, data); 등록: 내부적으로 list_add(&domain->link, &irq_domain_list); 자동 수행 매핑: virq = irq_create_mapping(domain, hwirq); 해제: irq_dispose_mapping(virq); + irq_domain_remove(domain); 주의: irq_domain_remove() 전에 모든 매핑을 반드시 해제해야 합니다 (mapcount == 0 확인)

도메인 생성 내부 코드

/* 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.xlateirq_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-15IPI (Inter-Processor Interrupt)
PPI (Private Peripheral)16-31CPU별 타이머(Timer), PMU 등
SPI (Shared Peripheral)32-1019공유 디바이스 인터럽트
LPI (Locality-specific Peripheral)8192+MSI/MSI-X (GICv3 ITS)

GIC vs APIC 비교

ARM과 x86의 대표적인 인터럽트 컨트롤러를 아키텍처 수준에서 비교합니다.

ARM GICv3 SPI Devices LPI (MSI/ITS) GICD (Distributor) GICR (CPU0) GICR (CPU1) CPU 0 CPU 1 x86 APIC Legacy/IOAPIC MSI (PCIe) I/O APIC x86 Vector Domain LAPIC 0 CPU 0 LAPIC 1 CPU 1

GIC vs APIC 상세 비교

항목ARM GICv3x86 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 → MSIVector → I/O APIC, Vector → MSI
인터럽트 라우팅(Routing)affinity 비트 + GICRRTE(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 계층

PCIe Device MSI-X hwirq=5 MSI Domain (irq_domain) virq=256 할당 GIC Domain (parent) hwirq=96 (GIC SPI) virq=256 (동일한 virq 공유)

MSI 도메인은 디바이스별 hwirq를 받아 GIC의 hwirq로 변환하고, 최종적으로 하나의 virq를 할당합니다.

다계층 트리 구조

실제 ARM64 서버에서는 Device → MSI → ITS → GIC → CPU로 이어지는 4단계 이상의 도메인 계층이 형성됩니다.

NVMe (MSI-X) GPU (MSI-X) NIC (MSI-X) GPIO Controller PCI-MSI Domain msi_domain_ops (alloc/free) GPIO Domain gpio_irq_domain_ops ITS Domain (GICv3 ITS) its_domain_ops: DeviceID+EventID → LPI hwirq GIC Domain (root) gic_irq_domain_ops: SGI/PPI/SPI + LPI 통합 CPU 0 CPU 1 CPU N

계층 도메인 생성

/* 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)에 도달하면 parentNULL이므로 재귀가 종료됩니다.
  • irq_domain_set_hwirq_and_chip()각 계층에서 해당 도메인의 hwirq 번호와 irq_chipirq_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 재귀 체인
권장 여부레거시 (호환용)현재 표준
실무 지침: 새 인터럽트 컨트롤러 드라이버를 작성할 때는 항상 계층형(Hierarchy) 도메인을 사용하세요. 스택형은 GPIO 컨트롤러 등 이전 코드와의 호환을 위해서만 유지되고 있습니다.

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

인터럽트 전체 라우팅 경로

하드웨어 신호가 발생하여 최종 핸들러가 실행되기까지의 전체 경로입니다.

하드웨어 인터럽트 신호 인터럽트 컨트롤러 (GIC/APIC) hwirq 확인 + CPU로 전달 CPU 예외 벡터 진입 IRQ 스택 전환 + 레지스터 저장 generic_handle_domain_irq(domain, hwirq) irq_find_mapping()으로 virq 검색 → irq_desc 획득 Flow Handler (handle_fasteoi_irq 등) irq_chip->ack() → action handler → irq_chip->eoi() 디바이스 IRQ 핸들러 (request_irq) 하드웨어 CPU 아키텍처 IRQ Domain IRQ 코어 디바이스 드라이버

라우팅 핵심 코드

/* 인터럽트 컨트롤러 핸들러에서 호출 */
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 메서드
파싱 APIof_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 콜백 누락

증상: Device Tree에서 인터럽트를 파싱할 때 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_datairq 필드가 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단계DeviceIDITT 포인터Device Table (DT)BDF(Bus/Device/Function)로 디바이스별 ITT 위치 조회
2단계EventIDINTID + CollectionInterrupt Translation Table (ITT)EventID를 LPI 번호와 타겟 Collection으로 변환
라우팅Collection IDTarget CPUCollection Table (CT)Collection을 특정 Redistributor(CPU)에 매핑
PCIe Device DeviceID + EventID Device Table DeviceID → ITT ptr ITT EventID → INTID Collection Table CollID → CPU Redistributor CPU Core ITS 매핑 파이프라인: DeviceID → EventID → INTID → 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 하드웨어가 비동기적으로 처리합니다:

명령코드설명파라미터
MAPD0x08DeviceID → ITT 매핑 설정DeviceID, ITT 주소, ITT 크기
MAPTI0x0AEventID → INTID + Collection 매핑DeviceID, EventID, pINTID, CollID
MAPI0x0BEventID → INTID 매핑 (INTID = EventID)DeviceID, EventID, CollID
MAPC0x09Collection → Target Redistributor 매핑CollID, RDbase
INV0x0C캐시된 LPI 설정 무효화(Invalidation)DeviceID, EventID
INVALL0x0D특정 Collection의 모든 LPI 무효화CollID
SYNC0x05Redistributor 동기화 보장RDbase
DISCARD0x0FEventID 매핑 제거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);
}
ITS 성능 팁: ITS 커맨드는 메모리 기반 큐에 기록되므로 배치 처리가 효율적입니다. 다수의 MAPTI 커맨드를 전송한 후 단일 SYNC로 완료를 보장하면 커맨드 큐 오버헤드(Overhead)를 줄일 수 있습니다.

Device Table / ITT 구조

ITS는 두 가지 주요 메모리 테이블을 사용합니다:

테이블인덱싱 키엔트리 크기할당 단위설명
Device TableDeviceID8바이트Flat 또는 2-LevelDeviceID별 ITT 주소와 크기 저장
ITTEventIDite_size (HW 정의)디바이스별 개별 할당EventID→INTID+Collection 매핑
Collection TableCollection ID8바이트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;
}
2-Level Device Table: DeviceID 공간이 큰 시스템(예: 16비트 이상)에서는 flat 테이블 대신 2-Level 인덱싱을 사용합니다. GITS_BASER에서 Indirect=1로 설정하면 첫 번째 레벨 테이블이 두 번째 레벨 페이지(Page)를 가리키는 구조가 됩니다.

MSI Domain 계층

MSI(Message Signaled Interrupts) 도메인은 IRQ 도메인 계층 구조에서 디바이스와 인터럽트 컨트롤러 사이의 중간 변환 계층입니다. PCI MSI/MSI-X, platform-msi, device-msi 등 다양한 MSI 소스를 통합적으로 관리합니다.

MSI 계층 체인

MSI 인터럽트는 여러 도메인 계층을 거쳐 최종 인터럽트 컨트롤러에 도달합니다:

MSI Domain 계층 트리 PCI Device Platform Device Device MSI PCI-MSI Domain Platform-MSI Device-MSI ITS Domain (ARM) IOMMU/IR Domain (x86) GIC / APIC Root Domain

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-MSIPlatform-MSIDevice-MSI
소스PCIe 디바이스비-PCI 플랫폼 디바이스임의 디바이스 (범용)
디스커버리PCI CapabilityDT/ACPI 바인딩드라이버 직접 요청
할당 APIpci_alloc_irq_vectors()platform_msi_domain_alloc_irqs()msi_domain_alloc_irqs()
MSI 주소/데이터PCI Config Space에 기록드라이버가 직접 설정드라이버가 직접 설정
Parent DomainITS/IR/x86-vectorITS/IR/x86-vectorITS/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 */
MSI-X 벡터 수 제한: MSI-X는 디바이스당 최대 2048개 벡터를 지원하지만, 실제 할당은 시스템의 가용 LPI/벡터 풀에 의해 제한됩니다. 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_nodefwnode_handle로 캐스팅하는 인라인 함수(Inline Function)입니다.

동적 IRQ 할당/해제

IRQ 도메인의 핵심 기능은 동적으로 hwirq ↔ virq 매핑을 생성/해제하는 것입니다. 레거시 고정 IRQ 체계와 달리, 현대 커널은 CONFIG_SPARSE_IRQ를 사용하여 필요한 시점에 IRQ 디스크립터를 동적으로 할당합니다.

동적 매핑 생명주기 alloc_descs virq 번호 확보 associate hwirq↔virq 연결 activate HW 경로 활성화 request_irq 핸들러 등록 Active 인터럽트 수신 free_irq 핸들러 해제 deactivate HW 경로 해제 dispose 매핑 해제 free_descs virq 반환 생명주기: alloc → map → activate → use → deactivate → dispose → free

매핑 생성 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_argsDT 전용 매핑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 TreeACPI
컨트롤러 기술interrupt-controller 속성MADT 서브테이블
인터럽트 셀 수#interrupt-cells = <N>고정 (GSI + flags)
IRQ 번호 체계컨트롤러별 로컬 hwirqGSI (Global System Interrupt)
계층 참조interrupt-parent = <&gic>IORT 테이블 참조
복잡 매핑interrupt-map 속성IORT ITS Group Node
MSI 컨트롤러msi-controller + #msi-cellsIORT 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);
}
GSI(Global System Interrupt): ACPI의 GSI는 시스템 전체에서 고유한 인터럽트 번호입니다. x86에서는 I/O APIC의 GSI Base + pin 오프셋(Offset)으로 계산되며, ARM64에서는 GIC SPI 번호에 32를 더한 값입니다. 커널의 acpi_register_gsi()가 GSI를 Linux virq로 변환합니다.

IRQ Domain 성능 분석

IRQ 도메인의 매핑 방식 선택은 시스템의 인터럽트 처리 지연(latency)에 직접 영향을 미칩니다. hwirq 수와 분포 패턴에 따라 적절한 revmap 전략을 선택해야 합니다.

매핑 타입별 성능 특성 Linear (배열) 조회: O(1) 인덱싱 메모리: O(max_hwirq) 적합: hwirq 연속, 밀집 예: GIC SPI 0~1023 Radix Tree (트리) 조회: O(log n) 메모리: O(실제 매핑 수) 적합: hwirq 희소, 넓은 범위 예: ITS LPI 8192+ Hierarchy (계층) 조회: 부모 도메인 위임 메모리: 자체 revmap 없음 적합: 다계층 컨트롤러 예: MSI→ITS→GIC 선택 가이드 hwirq 연속 + max < 1024 → Linear | hwirq 희소 또는 max > 4096 → Radix 부모 도메인 존재 → Hierarchy | 레거시 ISA → No-map

Linear vs Radix 성능 상세

지표LinearRadix 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 방식으로 연결됩니다.

계층적 인터럽트 컨트롤러 토폴로지 GIC (Root) SPI, PPI, SGI GPIO Controller chained handler PCIe RC (MSI) hierarchy domain MFD (I2C) nested/threaded ITS LPI domain Button Sensor NVMe NIC PMIC Codec RTC IRQ CHG IRQ 3단 계층: GPIO/MFD Sub-device → MFD/GPIO → GIC

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_chipirq_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_chipirq_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 = NULLrequest_threaded_irq()의 첫 번째 핸들러(hard IRQ)를 NULL로 설정하면, 커널이 자동으로 irq_default_primary_handler()를 사용하여 즉시 IRQ_WAKE_THREAD를 반환합니다.

Chained vs Nested 비교

속성ChainedNested (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, 온칩 MUXI2C PMIC, SPI MFD
수면(sleep) 허용불가가능
Chained handler 주의사항: Chained handler 내에서는 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/spuriousDT 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 구현
디버깅 체크리스트:
  1. dmesg | grep -i irq로 IRQ 도메인 초기화 로그 확인
  2. /sys/kernel/debug/irq/domains/에서 각 도메인의 매핑 수 확인
  3. /proc/interrupts에서 인터럽트 카운터가 증가하는지 확인
  4. ftrace로 irq_handler_entry 이벤트 추적
  5. 계층적 도메인이면 각 계층의 alloc/activate 콜백 동작 확인

가상화(Virtualization) 환경 IRQ Domain

가상화 환경에서는 호스트와 게스트 사이의 인터럽트 전달 경로가 추가적인 계층을 형성합니다. KVM, Xen, Hyper-V 각각 다른 방식으로 IRQ 도메인을 활용합니다.

KVM IRQ 라우팅 경로 QEMU (Userspace) ioctl(KVM_SIGNAL_MSI) KVM (Kernel) irq routing table + irqfd vGIC / vAPIC 가상 인터럽트 컨트롤러 Guest Kernel 게스트 IRQ 도메인 irqfd (eventfd) VFIO → KVM 경로 VFIO (Passthrough) 물리 디바이스 MSI → eventfd Physical HW IRQ Domain GIC/APIC + ITS KVM: userspace → kernel irq routing → vGIC/vAPIC → guest kernel

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 도메인 역할게스트 인터럽트 주입
KVMirqfd + vGIC/vAPIC호스트 IRQ domain은 그대로, KVM이 가상 IRQ 라우팅vCPU 진입 시 pending IRQ 주입
VFIOeventfd → irqfd물리 MSI domain 매핑 유지, eventfd로 게스트 전달irqfd wakeup → kvm_set_irq()
XenEvent ChannelXen 전용 evtchn irq_domain하이퍼바이저(Hypervisor) 이벤트 채널 알림
Hyper-VSINT + VMBushv_irq_chip + Local APICMSR 기반 SINT 벡터 주입
Posted Interrupts (VT-d): Intel VT-d의 Posted Interrupt 기능은 VFIO passthrough 디바이스의 MSI를 게스트에 직접 전달합니다. 호스트 CPU 개입 없이 하드웨어가 게스트의 vAPIC에 인터럽트를 기록하므로, VM exit 오버헤드를 제거합니다. CONFIG_KVM_POSTED_INTR로 활성화됩니다.

커널 설정 및 운영 체크리스트

IRQ 도메인 관련 커널 설정 옵션과 운영 환경에서의 인터럽트 관리 체크리스트를 정리합니다.

커널 설정 옵션

설정기본값설명의존성
CONFIG_IRQ_DOMAINy (자동)IRQ 도메인 인프라 활성화인터럽트 컨트롤러 드라이버 선택 시 자동
CONFIG_IRQ_DOMAIN_HIERARCHYy (자동)계층적 도메인 지원GICv3, ITS, MSI 사용 시 필수
CONFIG_SPARSE_IRQy동적 IRQ 디스크립터 할당대부분의 플랫폼에서 기본 활성
CONFIG_GENERIC_MSI_IRQy (자동)범용 MSI IRQ 도메인PCI MSI/MSI-X 사용 시 자동
CONFIG_IRQ_DOMAIN_DEBUGndebugfs에 도메인 정보 노출디버깅 시 수동 활성화
CONFIG_GENERIC_IRQ_DEBUGFSn상세 IRQ debugfs 인터페이스/sys/kernel/debug/irq/ 활성화
CONFIG_GENERIC_IRQ_IPIy (자동)IPI(Inter-Processor Interrupt) 지원SMP 시스템에서 자동
CONFIG_ARM_GIC_V3_ITSyGICv3 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 대응 체크리스트

IRQ Storm 긴급 대응: IRQ storm은 인터럽트가 과도하게 발생하여 CPU가 인터럽트 처리에만 소모되는 상황입니다. 즉시 대응이 필요합니다.
단계조치명령/방법
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) 설계 시 특별한 주의가 필요합니다.

일반 IRQ 경로 (Non-RT) Forced Threaded 경로 (PREEMPT_RT) HW IRQ 발생 hardirq handler 실행 (인터럽트 컨텍스트) irq_return HW IRQ 발생 최소 hardirq (wake thread) irq_default_primary_handler IRQ Thread에서 handler 실행 (프로세스 컨텍스트, 선점 가능) irq_return

일반 모드 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()로 등록된 핸들러를 자동으로 스레드 핸들러로 변환합니다. 원래의 handlerthread_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_chipirq_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 — 스레딩 불가 인터럽트:
  • IRQF_NO_THREAD 플래그가 설정된 인터럽트는 강제 스레딩 대상에서 제외됩니다.
  • 타이머 인터럽트: 스케줄러 tick은 항상 hardirq 컨텍스트에서 처리되어야 합니다.
  • IPI (Inter-Processor Interrupt): CPU 간 통신은 지연 없이 즉시 처리해야 합니다.
  • IRQF_PERCPU: per-CPU 인터럽트도 강제 스레딩에서 제외됩니다.
  • irq_chip에서 IRQCHIP_ONESHOT_SAFE 플래그를 설정하면 IRQF_ONESHOT 없이도 스레딩이 안전함을 표시합니다.
IRQ 스레드 확인: /proc/irq/N/threads 디렉토리에서 각 IRQ에 연결된 커널 스레드 정보를 확인할 수 있습니다. ps -eo pid,cls,pri,comm | grep irq/ 명령으로 IRQ 스레드의 스케줄링 클래스와 우선순위를 조회할 수 있으며, chrt -p PID로 개별 IRQ 스레드의 RT 우선순위를 조절할 수 있습니다.

커널 소스 파일 참조

IRQ 도메인 서브시스템과 관련된 주요 커널 소스 파일입니다. 각 파일의 역할을 파악하면 코드 분석과 디버깅 시 빠르게 해당 지점을 찾을 수 있습니다.

파일 경로설명
include/linux/irqdomain.hIRQ 도메인 핵심 구조체 및 API 선언
include/linux/irq.hirq_chip, irq_data, irq_desc 구조체 선언
include/linux/interrupt.hrequest_irq(), irqaction, IRQF_* 플래그
kernel/irq/irqdomain.cIRQ 도메인 핵심 구현 (create, map, associate)
kernel/irq/chip.cFlow handler 구현 (handle_level_irq, handle_fasteoi_irq 등)
kernel/irq/manage.crequest_irq(), free_irq(), IRQ 스레딩
kernel/irq/irqdesc.cirq_desc 관리, generic_handle_domain_irq()
kernel/irq/msi.cMSI 도메인 핵심 코드
kernel/irq/spurious.cSpurious IRQ 감지 및 처리
kernel/irq/debugfs.cIRQ debugfs 인터페이스
drivers/irqchip/irq-gic-v3.cARM GICv3 드라이버
drivers/irqchip/irq-gic-v3-its.cGICv3 ITS 드라이버
drivers/irqchip/irq-sifive-plic.cRISC-V PLIC 드라이버
drivers/irqchip/irq-riscv-aplic-*.cRISC-V APLIC 드라이버
arch/x86/kernel/apic/io_apic.cx86 I/O APIC 드라이버
arch/x86/kernel/apic/vector.cx86 Vector 도메인
drivers/pci/msi/irqdomain.cPCI MSI IRQ 도메인
drivers/gpio/gpiolib.cGPIO IRQ 도메인 통합
커널 공식 문서: 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 처리 흐름 비교 handle_level_irq mask ack handler unmask handle_edge_irq ack handler PENDING? re-trigger handle_fasteoi_irq handler eoi (GIC에서 가장 일반적) handle_percpu_devid_irq ack handler eoi (PPI/SGI Per-CPU) 흐름 처리기 선택 기준 Level 트리거: I/O APIC, GPIO 등 → handle_level_irq Edge 트리거: Legacy PIC, 엣지 GPIO → handle_edge_irq FastEOI: GIC SPI (가장 일반적) → handle_fasteoi_irq Per-CPU: GIC PPI/SGI, 타이머 → handle_percpu_devid_irq

Flow Handler 비교 표

핸들러트리거chip 콜백 순서대표 사용처핸들러 설정 함수
handle_level_irqLevelmask → ack → handler → unmaskI/O APIC level, GPIO levelirq_set_handler()
handle_edge_irqEdgeack → handler → (re-trigger if new edge)Legacy PIC, GPIO edgeirq_set_handler()
handle_fasteoi_irqLevel/Edgehandler → eoiGIC SPIirq_domain_set_info()
handle_percpu_devid_irqPer-CPUack → handler → eoiGIC PPI/SGIirq_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);
}
Edge IRQ의 재트리거(Re-trigger) 메커니즘: 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 비교

항목PLICAPLICIMSIC
스펙RISC-V Privileged SpecRISC-V AIARISC-V AIA
트리거 타입Level / EdgeLevel / EdgeMSI (메모리 쓰기)
MSI 지원미지원MSI 모드 가능 (IMSIC 연동)네이티브 MSI
IRQ 도메인 타입LinearLinear / Hierarchy (MSI 모드)Hierarchy
최대 인터럽트1024 (스펙 상한)10242048 per hart
우선순위소프트웨어 설정 (0~7)소프트웨어 설정없음 (FIFO)
Device Tree compatiblesifive,plic-1.0.0, riscv,plic0riscv,aplicriscv,imsic
RISC-V 인터럽트 계층 구조 External Devices PCIe MSI Timer / SW IRQ PLIC / APLIC 외부 인터럽트 라우팅 IMSIC MSI 수신 컨트롤러 CLINT 타이머 / IPI Hart 0 (CPU) Hart 1 (CPU) Hart N (CPU) PLIC IMSIC CLINT

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 컨텍스트를 별도 등록 */
GIC vs APIC vs PLIC 아키텍처 비교:
  • 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 네이티브 지원과 가상화 효율성 개선

참고 자료

커널 공식 문서

LWN.net 기사

커널 소스 (Bootlin Elixir)

발표 및 블로그

Device Tree 바인딩