IRQ 도메인

Linux 커널의 IRQ 도메인 인프라를 심층 분석합니다. 하드웨어 IRQ 번호를 Linux 가상 IRQ로 매핑하는 추상화 계층, 인터럽트 컨트롤러 드라이버, Device Tree/ACPI 통합, MSI/MSI-X 처리까지 다룹니다.

핵심 목표는 "하드웨어 번호 공간을 커널 공통 IRQ 모델로 안전하게 연결하는 방법"을 이해하는 것입니다. 따라서 irq_domain 생성부터 매핑 방식(linear, radix, tree), 계층형 도메인(parent/child) 설계, 펌웨어(Device Tree, ACPI MADT) 정보 파싱, MSI 도메인 확장까지 단계별로 다루며, 컨트롤러 드라이버 작성 시 자주 발생하는 매핑 누락·중복 할당·해제 순서 오류를 실제 점검 항목 중심으로 정리했습니다.

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

핵심 요약

  • irq_domain — hwirq를 virq로 매핑하는 추상화 계층 (인터럽트 컨트롤러 1개당 1개)
  • irq_chip — 인터럽트 컨트롤러의 하드웨어 제어 콜백 (mask/unmask/ack/eoi)
  • irq_data — virq와 hwirq를 연결하는 per-IRQ 메타데이터 구조체
  • 계층적 도메인 — 중첩 컨트롤러(MSI → ITS → GIC)를 parent/child 트리로 표현
  • 매핑 타입 — Linear(배열), Tree(radix), No-Map(레거시), Hierarchy(다계층)

단계별 이해

  1. 구조체 파악
    irq_domain, irq_chip, irq_data 세 구조체의 관계를 먼저 이해합니다.
  2. 매핑 흐름 추적
    펌웨어 파싱(DT/ACPI) → xlate → map → virq 할당까지의 경로를 따라갑니다.
  3. 계층 구조 이해
    MSI → ITS → GIC처럼 도메인이 중첩되는 경우의 alloc/free 체인을 확인합니다.
  4. 디버깅 방법 습득
    /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 필드 참조 펌웨어 파싱 변환/연결 할당 완료 하드웨어 설정

irq_domain 구조체

IRQ 도메인은 struct irq_domain으로 표현됩니다.

/* include/linux/irqdomain.h */
struct irq_domain {
    struct list_head        link;
    const char              *name;
    const struct irq_domain_ops *ops;

    void                    *host_data;      /* 컨트롤러별 private 데이터 */
    unsigned int            flags;
    unsigned int            mapcount;        /* 매핑된 IRQ 수 */

    /* hwirq → virq 매핑 방식 */
    struct irq_domain_chip_generic *gc;
    struct irq_data        *linear_revmap[]; /* Linear 매핑용 */
    struct radix_tree_root  revmap_tree;     /* Radix 매핑용 */

    /* 계층적 도메인 */
    struct irq_domain      *parent;
    struct fwnode_handle   *fwnode;          /* Device Tree/ACPI */
};

irq_data 구조체

irq_data는 각 인터럽트 라인의 메타데이터를 담으며, irq_desc 안에 내장됩니다. 계층형 도메인에서는 parent_data를 통해 체인을 형성합니다.

/* include/linux/irq.h */
struct irq_data {
    u32                     mask;            /* 프리셋 비트마스크 */
    unsigned int            irq;             /* virq 번호 */
    unsigned long           hwirq;           /* hwirq 번호 */
    struct irq_common_data  *common;
    struct irq_chip         *chip;           /* 이 IRQ를 제어하는 chip */
    struct irq_domain       *domain;         /* 소속 도메인 */
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
    struct irq_data         *parent_data;    /* 부모 도메인의 irq_data */
#endif
    void                    *chip_data;      /* chip 고유 데이터 */
};
irq_desc vs irq_data: irq_desc는 virq별로 하나씩 존재하는 최상위 디스크립터이며, irq_data는 그 안에 포함된 도메인별 메타데이터입니다. 계층형 도메인에서는 한 virq에 대해 여러 irq_dataparent_data 포인터로 연결됩니다 (예: MSI irq_data → ITS irq_data → GIC irq_data).

irq_domain_ops 콜백

struct irq_domain_ops {
    int  (*match)(struct irq_domain *d, struct device_node *node,
                     enum irq_domain_bus_token bus_token);
    int  (*map)(struct irq_domain *d, unsigned int virq,
                   irq_hw_number_t hwirq);
    void (*unmap)(struct irq_domain *d, unsigned int virq);
    int  (*xlate)(struct irq_domain *d, struct device_node *node,
                    const u32 *intspec, unsigned int intsize,
                    irq_hw_number_t *out_hwirq, unsigned int *out_type);
    int  (*alloc)(struct irq_domain *d, unsigned int virq,
                    unsigned int nr_irqs, void *arg);
    void (*free)(struct irq_domain *d, unsigned int virq,
                   unsigned int nr_irqs);
};

irq_domain_ops 콜백 시퀀스

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

시간 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
조회 시 캐시배열 인덱싱 (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_chip 구조체

irq_chip은 인터럽트 컨트롤러의 하드웨어 제어 인터페이스입니다.

irq_chip 정의

/* include/linux/irq.h */
struct irq_chip {
    const char    *name;

    /* 인터럽트 마스크 제어 */
    void        (*irq_mask)(struct irq_data *data);
    void        (*irq_unmask)(struct irq_data *data);
    void        (*irq_mask_ack)(struct irq_data *data);

    /* 인터럽트 ACK */
    void        (*irq_ack)(struct irq_data *data);
    void        (*irq_eoi)(struct irq_data *data);  /* End of Interrupt */

    /* 트리거 타입 설정 */
    int         (*irq_set_type)(struct irq_data *data, unsigned int flow_type);

    /* CPU affinity */
    int         (*irq_set_affinity)(struct irq_data *data,
                                        const struct cpumask *dest,
                                        bool force);

    /* Wake-up 지원 */
    int         (*irq_set_wake)(struct irq_data *data, unsigned int on);
};

Callback 역할

함수호출 시점하드웨어 동작
irq_mask인터럽트 비활성화컨트롤러의 마스크 레지스터 설정
irq_unmask인터럽트 활성화컨트롤러의 언마스크 레지스터 설정
irq_ack인터럽트 핸들러 시작Edge-triggered: 펜딩 클리어
irq_eoi인터럽트 핸들러 종료Level-triggered: EOI 레지스터 쓰기
irq_set_type인터럽트 등록 시Edge/Level, Rising/Falling 설정
irq_set_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;
}

GIC 인터럽트 타입

타입hwirq 범위설명
SGI (Software Generated)0-15IPI (Inter-Processor Interrupt)
PPI (Private Peripheral)16-31CPU별 타이머, 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
인터럽트 라우팅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);
}

스택형 도메인 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 트랜잭션)
IRQ 수디바이스당 1-4개MSI: 1-32, MSI-X: 1-2048
공유공유 가능 (레벨 트리거)항상 독점
성능낮음 (공유 시 폴링)높음 (독점, 큐별 IRQ)
라우팅고정동적 (CPU 지정)

MSI Address/Data 레지스터

MSI 인터럽트는 PCIe 디바이스가 지정된 주소에 데이터를 기록하여 발생합니다.

/* MSI Address 레지스터 (x86) */
/* [31:20] = 0xFEE (고정, LAPIC 주소 범위) */
/* [19:12] = Destination ID (타겟 CPU의 APIC ID) */
/* [11:4]  = 예약 */
/* [3]     = RH (Redirection Hint) */
/* [2]     = DM (Destination Mode: 0=Physical, 1=Logical) */

/* MSI Data 레지스터 */
/* [15]    = Trigger Mode (0=Edge, 1=Level) */
/* [14]    = Level (0=Deassert, 1=Assert) */
/* [7:0]   = Vector 번호 (x86 IDT 인덱스) */

struct msi_msg {
    u32 address_lo;  /* 위 Address 레지스터 */
    u32 address_hi;  /* 64-bit 주소용 상위 32비트 */
    u32 data;        /* 위 Data 레지스터 */
};

MSI-X 도메인 계층 (ARM64 ITS)

ARM64에서 MSI-X는 GICv3 ITS(Interrupt Translation Service)를 통해 LPI로 변환됩니다.

/* drivers/irqchip/irq-gic-v3-its.c */
static const struct irq_domain_ops its_domain_ops = {
    .alloc  = its_irq_domain_alloc,
    .free   = its_irq_domain_free,
};

/* ITS alloc: DeviceID + EventID → LPI hwirq 매핑 */
static int its_irq_domain_alloc(struct irq_domain *domain,
                                   unsigned int virq,
                                   unsigned int nr_irqs, void *args)
{
    /* 1. LPI 번호 할당 (8192+) */
    /* 2. ITS 디바이스 테이블에 DeviceID 등록 */
    /* 3. ITT(Interrupt Translation Table)에 EventID→LPI 매핑 */
    /* 4. MAPD/MAPI/INV 커맨드로 ITS 하드웨어 설정 */
    irq_domain_set_hwirq_and_chip(domain, virq, hwirq,
                                     &its_irq_chip, its_dev);
    return 0;
}

MSI 도메인 구현

/* drivers/pci/msi/irqdomain.c */
static struct irq_chip pci_msi_controller = {
    .name           = "PCI-MSI",
    .irq_mask       = pci_msi_mask_irq,
    .irq_unmask     = pci_msi_unmask_irq,
    .irq_ack        = irq_chip_ack_parent,
    .irq_set_affinity = msi_domain_set_affinity,
};

static int pci_msi_domain_alloc_irqs(struct irq_domain *domain,
                                       struct device *dev,
                                       int nvec)
{
    struct msi_desc *desc;
    int virq, i = 0;

    for_each_msi_entry(desc, dev) {
        virq = __irq_domain_alloc_irqs(domain, -1, 1, dev_to_node(dev),
                                         desc, false, NULL);
        if (virq < 0)
            return virq;

        irq_set_msi_desc(virq, desc);
        desc->irq = virq;
        i++;
    }
    return 0;
}

GPIO IRQ 도메인

GPIO 컨트롤러는 자체 IRQ 도메인을 가지며, 각 GPIO 핀이 인터럽트 소스로 동작합니다.

gpiochip에 IRQ 도메인 연결

/* drivers/gpio/gpiolib.c */
static int gpiochip_add_irqchip(struct gpio_chip *gc,
                                   struct lock_class_key *lock_key,
                                   struct lock_class_key *request_key)
{
    struct gpio_irq_chip *girq = &gc->irq;
    struct irq_domain *domain;

    if (girq->domain) {
        /* 드라이버가 이미 도메인을 설정한 경우 */
        return 0;
    }

    if (girq->parent_domain) {
        /* 계층형: 부모 도메인(GIC 등)에 연결 */
        domain = irq_domain_create_hierarchy(
            girq->parent_domain, 0, gc->ngpio,
            gc->fwnode, girq->parent_handler ?
                &gpiochip_hierarchy_domain_ops : girq->domain_ops,
            gc);
    } else {
        /* 비계층형: 독립 Linear 도메인 */
        domain = irq_domain_create_simple(
            gc->fwnode, gc->ngpio, girq->first,
            girq->domain_ops ?: &gpiochip_domain_ops, gc);
    }

    girq->domain = domain;
    return 0;
}

GPIO IRQ 드라이버 예제

/* 간단한 GPIO 컨트롤러 IRQ 도메인 설정 */
static const struct irq_chip my_gpio_irq_chip = {
    .name           = "my-gpio",
    .irq_ack        = my_gpio_irq_ack,
    .irq_mask       = my_gpio_irq_mask,
    .irq_unmask     = my_gpio_irq_unmask,
    .irq_set_type   = my_gpio_irq_set_type,
    .flags          = IRQCHIP_IMMUTABLE,
    GPIOCHIP_IRQ_RESOURCE_HELPERS,
};

static int my_gpio_probe(struct platform_device *pdev)
{
    struct my_gpio *mg;
    struct gpio_irq_chip *girq;

    mg->gc.ngpio = 32;
    mg->gc.parent = &pdev->dev;

    /* IRQ chip 설정 */
    girq = &mg->gc.irq;
    gpio_irq_chip_set_chip(girq, &my_gpio_irq_chip);
    girq->parent_handler = my_gpio_irq_handler;
    girq->num_parents = 1;
    girq->parents = devm_kcalloc(&pdev->dev, 1,
                                    sizeof(*girq->parents), GFP_KERNEL);
    girq->parents[0] = platform_get_irq(pdev, 0);
    girq->default_type = IRQ_TYPE_NONE;
    girq->handler = handle_bad_irq;

    return devm_gpiochip_add_data(&pdev->dev, &mg->gc, mg);
}

인터럽트 전체 라우팅 경로

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

하드웨어 인터럽트 신호 인터럽트 컨트롤러 (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()를 호출하면 커널 패닉이 발생합니다.
/* 방어적 코딩 패턴 */
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_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;
}