Linux 커널 인터럽트 핸들링 C 코드 분석

ARM 아키텍처에서 예외가 발생하면 어셈블리 단계를 거쳐 최종적으로 asm_do_IRQ() 함수로 진입한다. 이 시점까지 어셈블리 코드는 인터럽트 번호(irq)를 계산하여 C 함수에 전달한다. 이 문서에서는 Linux 인터럽트 시스템의 핵심 자료구조와 C 언어 단계의 처리 흐름을 살펴본다.

핵심 자료구조

커널은 모든 인터럽트를 일관된 번호 체계로 관리하며, irq_desc[NR_IRQS] 배열을 통해 각 인터럽트 소스를 기술한다. 각 배열 요소는 하드웨어 접근 방법, 상태 래그, 그리고 사용자 등록 콜백 함수들의 연결 정보를 담고 있다.

irq_desc 구조체

include/linux/irq.h에 정의된 irq_desc는 다음과 같다:

struct irq_desc {
    irq_flow_handler_t    handle_irq;
    struct irq_chip       *chip;
    struct msi_desc       *msi_desc;
    void                  *handler_data;
    void                  *chip_data;
    struct irqaction      *action;
    unsigned int          status;
    unsigned int          depth;
    unsigned int          wake_depth;
    unsigned int          irq_count;
    unsigned int          irqs_unhandled;
    spinlock_t            lock;
    const char            *name;
} ____cacheline_internodealigned_in_smp;

각 멤버의 역할은 다음과 같다:

  • handle_irq: 해당 인터럽트의 상위 분배 함수
  • chip: 하드웨어 제어를 위한 메서드 집합
  • action: 드라이버가 등록한 실제 처리 함수들의 연결 리스트

irq_chip 구조체

하드웨어 레벨의 조작을 추상화한 구조체다:

struct irq_chip {
    const char       *name;
    unsigned int     (*startup)(unsigned int irq);
    void             (*shutdown)(unsigned int irq);
    void             (*enable)(unsigned int irq);
    void             (*disable)(unsigned int irq);
    void             (*ack)(unsigned int irq);
    void             (*mask)(unsigned int irq);
    void             (*mask_ack)(unsigned int irq);
    void             (*unmask)(unsigned int irq);
    const char       *typename;
};

irqaction 구조체

드라이버 관점의 인터트 핸들러 등록 정보를 담는다:

struct irqaction {
    irq_handler_t      handler;
    unsigned long      flags;
    cpumask_t          mask;
    const char         *name;
    void               *dev_id;
    struct irqaction   *next;
    int                irq;
    struct proc_dir_entry *dir;
};

공유 인터럽트를 지원하기 위해 next 포인터로 동일 번호의 여러 들러를 연결한다.

시스템 초기화 흐름

플랫폼 독립 초기화

init_IRQ()arch/arm/kernel/irq.c에서 인터럽트 프레임워크의 기본 상태를 설정한다:

void __init init_IRQ(void)
{
    int irq;

    for (irq = 0; irq < NR_IRQS; irq++)
        irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE;
    init_arch_irq();
}

init_arch_irq는 함수 포인터로, 대상 SoC에 맞는 초기화 루틴을 가리킨다.

SoC 특화 초기화 (S3C2440 예시)

s3c24xx_init_irq()에서는 각 인터럽트에 대해 chiphandle_irq를 연결한다:

void __init s3c24xx_init_irq(void)
{
    for (irqno = IRQ_EINT0; irqno <= IRQ_EINT3; irqno++) {
        irqdbf("registering irq %d (ext int)\n", irqno);
        set_irq_chip(irqno, &s3c_irq_eint0t4);
        set_irq_handler(irqno, handle_edge_irq);
        set_irq_flags(irqno, IRQF_VALID);
    }
}

외부 인터럽트 0~3에 대해 하드웨어 조작 함수와 엣지 트리거 핸들러를 설정한다.

static struct irq_chip s3c_irq_eint0t4 = {
    .name      = "s3c-ext0",
    .ack       = s3c_irq_ack,
    .mask      = s3c_irq_mask,
    .unmask    = s3c_irq_unmask,
    .set_wake  = s3c_irq_wake,
    .set_type  = s3c_irqext_type,
};

드라이버의 인터럽트 등록

request_irq()kernel/irq/manage.c에 있으며, 내부적으로 irqaction을 할당하고 setup_irq()를 호출한다:

int request_irq(unsigned int irq, irq_handler_t handler,
        unsigned long irqflags, const char *devname, void *dev_id)
{
    struct irqaction *act = kmalloc(sizeof(*act), GFP_ATOMIC);
    
    act->handler = handler;
    act->flags   = irqflags;
    act->name    = devname;
    act->dev_id  = dev_id;
    act->next    = NULL;
    
    return setup_irq(irq, act);
}

setup_irq()의 내부 동작

int setup_irq(unsigned int irq, struct irqaction *fresh)
{
    struct irq_desc *entry = irq_desc + irq;
    struct irqaction **pos = &entry->action;
    struct irqaction *cur = *pos;
    int is_shared = 0;

    if (cur) {
        if (!((cur->flags & fresh->flags) & IRQF_SHARED) ||
            ((cur->flags ^ fresh->flags) & IRQF_TRIGGER_MASK))
            return -EBUSY;

        while (cur->next) {
            pos = &cur->next;
            cur = *pos;
        }
        is_shared = 1;
    }

    *pos = fresh;

    if (!is_shared) {
        irq_chip_set_defaults(entry->chip);

        if (fresh->flags & IRQF_TRIGGER_MASK) {
            if (entry->chip && entry->chip->set_type)
                entry->chip->set_type(irq,
                        fresh->flags & IRQF_TRIGGER_MASK);
        }

        if (!(entry->status & IRQ_NOAUTOEN)) {
            entry->depth = 0;
            entry->status &= ~IRQ_DISABLED;
            if (entry->chip->startup)
                entry->chip->startup(irq);
            else
                entry->chip->enable(irq);
        }
    }

    fresh->irq = irq;
    return 0;
}

핵심 로직은: 공유 가능 여부 검증 → 리스트 연결 → 트리거 방식 설정 → 인터럽트 활성화 순이다.

인터럽트 해제

free_irq()dev_id로 특정 핸들러를 식별하여 제거한다:

void free_irq(unsigned int irq, void *dev_id)
{
    struct irq_desc *entry = irq_desc + irq;
    struct irqaction **prev = &entry->action;
    struct irqaction *iter;

    for (iter = *prev; iter; prev = &iter->next, iter = *prev) {
        if (iter->dev_id != dev_id)
            continue;

        *prev = iter->next;

        if (!entry->action) {
            entry->status |= IRQ_DISABLED;
            if (entry->chip->shutdown)
                entry->chip->shutdown(irq);
            else
                entry->chip->disable(irq);
        }

        kfree(iter);
        return;
    }
}

마지막 핸들러가 제거되면 하드웨어 인터럽트를 완전히 비활성화한다.

런타임 처리 경로

최상위 진입점

asm_do_IRQ()는 어블리에서 직접 호출되며, 인터럽트 컨텍스트의 시작을 알린다:

asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    struct pt_regs *saved = set_irq_regs(regs);
    struct irq_desc *desc = irq_desc + irq;

    if (irq >= NR_IRQS)
        desc = &bad_irq_desc;

    irq_enter();
    desc_handle_irq(irq, desc);
    irq_finish(irq);
    irq_exit();
    set_irq_regs(saved);
}

desc_handle_irq()는 단순히 desc->handle_irq(irq, desc)를 호출하는 인라인 함수다.

엣지 트리거 처리 예시

EINT0의 경우 handle_edge_irq()가 호출된다:

void fastcall handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
    kstat_cpu(smp_processor_id()).irqs[irq]++;

    desc->chip->ack(irq);
    desc->status |= IRQ_INPROGRESS;

    handle_IRQ_event(irq, desc->action);
}

ack()로 하드웨어 플래그를 클리어하고, handle_IRQ_event()로 등록된 모든 핸들러를 순회 실행한다:

irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *chain)
{
    irqreturn_t cumulative = IRQ_NONE;
    int ret;

    do {
        ret = chain->handler(irq, chain->dev_id);
        if (ret == IRQ_HANDLED)
            cumulative |= chain->flags;
        chain = chain->next;
    } while (chain);

    return cumulative;
}

계층형 인터럽트: 서브 IRQ 사례

S3C2440의 EINT5는 EINT4~7 그룹에 속하는 2차 인터럽트다. 상위 그룹의 핸들러가 먼저 실행된 후 실제 소스를 파악한다.

상위 분배 핸들러 등록

set_irq_chained_handler(IRQ_EINT4t7, s3c_irq_demux_extint4t7);

실제 분배 로직

static void s3c_irq_demux_extint4t7(unsigned int parent_irq,
            struct irq_desc *parent_desc)
{
    unsigned long pending = __raw_readl(S3C24XX_EINTPEND);
    unsigned long masked  = __raw_readl(S3C24XX_EINTMASK);
    unsigned long active;
    int sub;

    active = pending & ~masked;
    active &= 0xff;

    while (active) {
        sub = __ffs(active);
        active &= ~(1 << sub);
        
        int real_irq = IRQ_EINT4 + sub - 4;
        desc_handle_irq(real_irq, irq_desc + real_irq);
    }
}

이 패턴에서는 INTOFFSET으로 얻은 번호가 그룹 번호이며, 실제 핀 번호는 펜딩 레지스터를 어 재계산한다. 이후 desc_handle_irq()를 재귀적으로 호출하여 최종 핸들러로 분기한다.

처리 흐름 정리

단계함수/동작설명
1CPU 예외 벡터어셈블리에서 vector_irq로 진입
2asm_do_IRQ()C 단계 진입, 컨텍스트 설정
3desc->handle_irq()유형별 1차 분배 (엣지/레벨/체인)
4desc->chip->ack()하드웨어 인터럽트 클리어
5handle_IRQ_event()드라이버 핸들러 순회 실행
6사용자 콜백실제 디바이스 처리 로직

이 구조 덕분에 Linux는 단일 IRQ 번호 공간에서부터 복잡한 계층형 인터럽트 컨트롤러까지 일관된 추상화로 대응할 수 있다.

태그: Linux kernel ARM Interrupt irq_desc request_irq irq_chip

6월 29일 20:46에 게시됨