Threaded IRQ (스레드화 인터럽트(Interrupt))

request_threaded_irq() 기반 스레드화 인터럽트 핸들러(Handler)를 심층 분석합니다. 전통적 Top/Bottom Half 모델의 한계를 극복하기 위해 도입된 threaded IRQ의 설계 동기, hardirq handler와 thread_fn의 역할 분리, IRQ_ONESHOT 시맨틱, force_irqthreads 메커니즘, devm managed API, nested IRQ, PREEMPT_RT 통합을 커널 소스 기반으로 분석하고, I2C/SPI/GPIO/PCIe 실전 드라이버 패턴과 기존 드라이버 마이그레이션 가이드를 제공합니다.

전제 조건: 인터럽트, Softirq & Hardirq, Bottom Half 선택 가이드 문서를 먼저 읽으세요. Threaded IRQ는 기존 인터럽트 처리 모델(Top Half/Bottom Half)의 연장선에 있으므로, hardirq 컨텍스트와 프로세스(Process) 컨텍스트의 차이를 먼저 이해해야 합니다.
일상 비유: Threaded IRQ는 응급실 접수 시스템과 비슷합니다. 환자가 도착하면(인터럽트 발생) 접수 담당자(hardirq handler)가 즉시 증상을 확인하고 번호표를 발급합니다(IRQ_WAKE_THREAD). 실제 진료(thread_fn)는 전담 의사(커널 스레드(Kernel Thread))가 차례대로 처리합니다. 접수 담당자는 빠르게 다음 환자를 받을 수 있고, 의사는 충분한 시간을 들여 진료할 수 있습니다.

핵심 요약

  • request_threaded_irq() — hardirq handler와 thread_fn을 분리하여 등록하는 API. handler는 빠른 ACK/확인만, thread_fn은 프로세스 컨텍스트에서 실행됩니다.
  • IRQ_WAKE_THREAD — hardirq handler가 반환하는 값으로, "커널 스레드를 깨워서 나머지 작업을 처리하라"는 신호입니다.
  • IRQ_ONESHOT — thread_fn 완료까지 인터럽트 라인을 마스킹 상태로 유지합니다. Level-triggered 인터럽트에서 IRQ storm 방지에 필수입니다.
  • force_irqthreads — 커널 cmdline threadirqs 또는 PREEMPT_RT에서 모든 인터럽트를 강제로 스레드화합니다.
  • devm_request_threaded_irq() — 디바이스 생명주기에 맞춰 자동으로 IRQ를 해제하는 managed API입니다.

단계별 이해

  1. 전통 모델의 한계 파악
    hardirq에서 모든 작업을 처리하면 인터럽트 비활성화 시간이 길어지고, Bottom Half(tasklet/workqueue)는 우선순위(Priority) 제어가 어렵습니다.
  2. 스레드화 분리 모델 이해
    hardirq handler는 최소한의 ACK만 수행하고 IRQ_WAKE_THREAD를 반환하면, 전용 커널 스레드(irq/N-name)가 나머지를 처리합니다.
  3. ONESHOT 시맨틱 학습
    Level-triggered 인터럽트에서 왜 thread_fn 완료까지 인터럽트를 마스킹해야 하는지 이해합니다.
  4. 실전 API 적용
    devm_request_threaded_irq()로 드라이버를 작성하고, regmap_irq 등 프레임워크와 통합하는 패턴을 익힙니다.
  5. PREEMPT_RT 통합 확인
    force_irqthreads가 기존 request_irq() 기반 드라이버에 미치는 영향을 확인하고, RT 환경에서의 우선순위 튜닝을 학습합니다.
커널 버전: 이 문서는 Linux 6.x 기준으로 작성되었습니다. Threaded IRQ는 2.6.30 (2009)에 도입되어 안정적으로 사용 가능합니다. 관련 소스: kernel/irq/manage.c, kernel/irq/irqdesc.c, include/linux/interrupt.h.

Threaded IRQ 개요와 동기

전통적인 리눅스 인터럽트 처리 모델은 Top Half(hardirq)와 Bottom Half(softirq, tasklet, workqueue)로 나뉩니다. hardirq는 인터럽트를 비활성화한 상태에서 실행되므로 가능한 짧아야 하고, 나머지 작업은 Bottom Half로 위임합니다. 이 모델은 수십 년간 잘 작동했지만, 몇 가지 근본적인 한계가 있습니다.

전통 모델의 한계

문제설명영향
긴 hardirq 시간I2C/SPI 등 슬로우 버스(Bus) 디바이스는 hardirq에서 레지스터(Register) 접근에 수백 us 소요다른 인터럽트 지연(Latency), 시스템 응답성 저하
우선순위 역전(Priority Inversion)softirq/tasklet은 고정 우선순위, 중요도와 무관하게 실행 순서 결정실시간(Real-time) 워크로드에서 예측 불가능한 지연
슬립(Sleep) 불가hardirq/softirq/tasklet 컨텍스트에서 슬립 가능 함수 호출 금지mutex, kmalloc(GFP_KERNEL), I2C 전송 등 사용 불가
PREEMPT_RT 비호환hardirq 컨텍스트는 선점(Preemption) 불가, 실시간 스케줄링 방해결정론적 지연 시간 보장 불가능

Threaded IRQ가 해결하는 문제

Thomas Gleixner가 설계한 Threaded IRQ 모델은 인터럽트 처리를 두 단계로 명확히 분리합니다:

역사

버전연도변경 사항
2.6.302009request_threaded_irq() 도입 (Thomas Gleixner)
2.6.352010devm_request_threaded_irq() 추가
3.12011threadirqs 커널 cmdline 파라미터 도입
4.x2015+PREEMPT_RT 기본 force threading 통합
5.152021PREEMPT_RT 메인라인 머지 시작
6.x2023+PREEMPT_RT 완전 메인라인 통합
전통 IRQ vs Threaded IRQ 실행 경로 비교 전통 모델 (Top/Bottom Half) hardirq (긴 처리) softirq / tasklet 인터럽트 비활성 구간 (길다) 문제: 슬립 불가, 우선순위 고정, 긴 비활성 Threaded IRQ 모델 hardirq (짧은 ACK) IRQ_WAKE_THREAD irq/N-name 스레드 프로세스 컨텍스트, 슬립 가능 인터럽트 비활성 (짧습니다) 실행 타임라인 전통: hardirq + bottom half (긴 비활성) 스레드: ACK thread_fn (선점 가능) Threaded IRQ 장점 1. 짧은 hardirq 구간 (다른 IRQ 빠른 응답) 2. thread_fn에서 슬립 가능 (mutex, I2C) 3. RT 우선순위 설정 가능 (chrt) 4. PREEMPT_RT와 자연스러운 통합 5. 선점 가능한 인터럽트 처리 코드 흐름 request_threaded_irq(irq, handler, thread_fn, flags, name, dev) IRQ 발생 -> handler() -> IRQ_WAKE_THREAD -> wake_up_process(irq_thread) -> thread_fn()

request_threaded_irq() API 상세

request_threaded_irq()는 threaded IRQ 등록의 핵심 함수입니다. kernel/irq/manage.c에 구현되어 있으며, 기존 request_irq()는 이 함수의 thread_fn=NULL 래퍼입니다.

함수 시그니처

/* include/linux/interrupt.h */
extern int request_threaded_irq(
    unsigned int       irq,          /* IRQ 번호 */
    irq_handler_t      handler,      /* hardirq 핸들러 (NULL 가능) */
    irq_handler_t      thread_fn,    /* 스레드 함수 (NULL 가능) */
    unsigned long      irqflags,     /* IRQF_* 플래그 */
    const char        *devname,      /* /proc/interrupts에 표시될 이름 */
    void              *dev_id       /* 핸들러에 전달될 인자 (공유 IRQ 시 식별자) */
);

/* handler 반환 값 */
typedef enum irqreturn {
    IRQ_NONE        = (0 << 0),  /* 이 디바이스의 인터럽트가 아님 */
    IRQ_HANDLED     = (1 << 0),  /* 처리 완료, 스레드 깨우지 않음 */
    IRQ_WAKE_THREAD = (1 << 1),  /* 스레드를 깨워서 thread_fn 실행 */
} irqreturn_t;

파라미터 상세 설명

파라미터설명주의 사항
irqIRQ 번호. platform_get_irq(), gpiod_to_irq(), pci_irq_vector() 등으로 획득유효하지 않은 번호 시 -EINVAL
handlerhardirq 컨텍스트에서 실행. 빠른 ACK, 상태 확인 수행NULL이면 기본 핸들러(irq_default_primary_handler) 사용. IRQF_SHARED 시 NULL 불가
thread_fn커널 스레드(프로세스 컨텍스트)에서 실행. 슬립 가능NULL이면 일반 request_irq()와 동일. handler와 thread_fn 둘 다 NULL이면 에러
irqflagsIRQF_ONESHOT, IRQF_SHARED, IRQF_NO_THREAD 등 조합handler=NULL + IRQF_ONESHOT 미설정 시 경고/에러
devname/proc/interrupts에 표시되는 이름, 스레드(Thread) 이름에도 사용NULL 불가, 고유한 이름 권장
dev_id핸들러에 전달되는 쿠키. IRQF_SHARED 시 고유 비NULL 값 필수공유 IRQ에서 free_irq() 시 이 값으로 핸들러 식별

handler가 NULL인 경우

handler를 NULL로 전달하면, 커널은 자동으로 irq_default_primary_handler()를 사용합니다. 이 기본 핸들러는 단순히 IRQ_WAKE_THREAD를 반환합니다:

/* kernel/irq/manage.c */
static irqreturn_t irq_default_primary_handler(int irq, void *dev_id)
{
    return IRQ_WAKE_THREAD;
}

/* 주의: handler=NULL + thread_fn만 사용할 때
 * 반드시 IRQF_ONESHOT을 설정해야 합니다!
 * 그렇지 않으면 커널이 경고를 출력하고 -EINVAL 반환 */
필수 규칙: handler가 NULL이고 thread_fn만 사용할 때는 반드시 IRQF_ONESHOT을 설정해야 합니다. ONESHOT 없이 기본 핸들러를 사용하면 hardirq가 즉시 인터럽트를 재활성화하는데, thread_fn이 원인을 제거하기 전에 같은 인터럽트가 다시 발생하여 IRQ storm이 발생합니다.

기본 사용 패턴

/* 패턴 1: hardirq handler + thread_fn */
static irqreturn_t my_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* 이 디바이스의 인터럽트인지 확인 (공유 IRQ 시 필수) */
    if (!(ioread32(dev->regs + STATUS) & IRQ_PENDING))
        return IRQ_NONE;

    /* 인터럽트 ACK (하드웨어에 처리 시작 알림) */
    iowrite32(IRQ_ACK, dev->regs + STATUS);

    return IRQ_WAKE_THREAD;
}

static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* 프로세스 컨텍스트: 슬립 가능! */
    mutex_lock(&dev->lock);
    process_data(dev);
    mutex_unlock(&dev->lock);

    return IRQ_HANDLED;
}

/* probe에서 등록 */
ret = request_threaded_irq(irq, my_hardirq, my_thread_fn,
                           IRQF_ONESHOT, "my-device", dev);

/* 패턴 2: handler=NULL (기본 핸들러 사용) */
ret = request_threaded_irq(irq, NULL, my_thread_fn,
                           IRQF_ONESHOT, "my-sensor", dev);

반환값과 에러 코드

반환값의미
0성공
-EINVAL잘못된 파라미터 (handler=NULL + thread_fn=NULL, ONESHOT 미설정 등)
-EBUSYIRQ 이미 사용 중 (비공유)
-ENOMEM메모리 할당 실패

IRQF 플래그 상세

인터럽트 플래그는 include/linux/interrupt.h에 정의되어 있으며, threaded IRQ의 동작을 세밀하게 제어합니다. 올바른 플래그 조합은 안정적인 인터럽트 처리의 핵심입니다.

플래그설명threaded IRQ 영향
IRQF_SHARED0x00000080여러 디바이스가 같은 IRQ 라인 공유handler 필수 (NULL 불가). 자기 디바이스 인터럽트인지 확인 필요
IRQF_ONESHOT0x00002000thread_fn 완료까지 IRQ 라인 마스킹 유지handler=NULL 시 필수. Level-triggered에서 IRQ storm 방지
IRQF_NO_THREAD0x00010000force_irqthreads에서도 스레드화 금지타이머(Timer), IPI 등 반드시 hardirq에서 실행해야 하는 인터럽트용
IRQF_TRIGGER_RISING0x00000001상승 에지 트리거GPIO 인터럽트에서 주로 사용
IRQF_TRIGGER_FALLING0x00000002하강 에지 트리거TRIGGER_RISING과 OR 조합 가능
IRQF_TRIGGER_HIGH0x00000004하이 레벨 트리거ONESHOT 필수 (level-triggered)
IRQF_TRIGGER_LOW0x00000008로우 레벨 트리거ONESHOT 필수 (level-triggered)
IRQF_NO_AUTOEN0x00004000등록 후 자동 활성화 안 함초기화 완료 후 수동으로 enable_irq() 호출
IRQF_NOBALANCING0x00000800IRQ 밸런싱에서 제외특정 CPU에 고정해야 하는 인터럽트
IRQF_NO_SUSPEND0x00004000시스템 서스펜드 시에도 활성 유지웨이크업 소스 인터럽트
IRQF_COND_ONESHOTv6.4+공유 라인에서 조건부 ONESHOT 요청기존 등록자가 IRQF_ONESHOT을 쓰지 않았어도 공유 등록이 가능하도록 완화. 첫 등록자만 ONESHOT을 강제 필요할 때 사용
IRQF_NO_DEBUGv6.5+spurious/poll 검사에서 제외노이즈성 장치를 디버그 모드에서 무시
IRQF_NO_AUTOEN_SHARED 관용구공유 + NO_AUTOEN 조합여러 장치가 같은 라인을 공유하고 개별 enable 타이밍이 다를 때

IRQF_COND_ONESHOT 상세 (v6.4+)

전통적으로 같은 IRQ 라인에 여러 드라이버가 등록될 때, 모든 등록자가 IRQF_ONESHOT 설정을 일치시켜야 했습니다. 특히 level-triggered 공유 라인에서는 ONESHOT이 필수인데, 레거시 드라이버가 먼저 ONESHOT 없이 등록해 둔 상태라면 이후 새 드라이버가 ONESHOT을 요구하며 등록하면 -EBUSY로 실패했습니다.

IRQF_COND_ONESHOT(v6.4 병합)은 이 경우에 대한 완화책입니다. "ONESHOT이 필요하지만, 이미 non-ONESHOT 등록자가 있다면 우리도 양보(Yield)하고 등록을 허용한다"는 의미로, 실제 동작은 첫 등록자의 설정에 따릅니다.

/* 실전 사용: 공유 라인에서 신규 등록이 공존하도록 허용 */
static int my_secondary_driver_probe(...)
{
    /*
     * - 단독 라인이면 ONESHOT으로 등록 (표준 동작)
     * - 공유 + 기존 등록자가 non-ONESHOT이면 ONESHOT 없이 등록
     * - 커널은 내부적으로 첫 등록자 설정을 따라감
     */
    return request_threaded_irq(irq, primary_handler, thread_fn,
                                IRQF_SHARED | IRQF_COND_ONESHOT,
                                dev_name(dev), dev);
}
주의: IRQF_COND_ONESHOT을 지정했더라도 non-ONESHOT으로 등록된 경우, 해당 드라이버의 thread_fn이 race 상태에 취약할 수 있습니다. thread_fn이 실행 중에 추가 인터럽트가 들어올 수 있으므로, 드라이버 자체에서 disable_irq_nosync()/enable_irq()나 상태 머신으로 재진입을 방어해야 합니다. 이 플래그는 "공존을 위한 타협책"이며, 설계 가능하면 단독 라인에 ONESHOT으로 가는 것이 원칙입니다.

플래그 조합 규칙

/* 규칙 1: handler=NULL이면 ONESHOT 필수 */
request_threaded_irq(irq, NULL, thread_fn,
    IRQF_ONESHOT, name, dev);         /* OK */
request_threaded_irq(irq, NULL, thread_fn,
    0, name, dev);                    /* FAIL: -EINVAL */

/* 규칙 2: IRQF_SHARED이면 handler 필수 */
request_threaded_irq(irq, my_handler, thread_fn,
    IRQF_SHARED | IRQF_ONESHOT, name, dev);  /* OK */
request_threaded_irq(irq, NULL, thread_fn,
    IRQF_SHARED | IRQF_ONESHOT, name, dev);  /* 주의: handler=NULL + SHARED 시 문제 */

/* 규칙 3: Level-triggered이면 ONESHOT 강력 권장 */
request_threaded_irq(irq, handler, thread_fn,
    IRQF_TRIGGER_LOW | IRQF_ONESHOT, name, dev);  /* OK */

IRQ_ONESHOT

IRQF_ONESHOT은 threaded IRQ에서 가장 중요한 플래그입니다. 이 플래그가 설정되면 hardirq handler 진입 시 인터럽트 라인이 마스킹되고, thread_fn이 완료된 후에야 언마스킹됩니다.

Level-Triggered에서 ONESHOT이 필수인 이유

Level-triggered 인터럽트는 인터럽트 라인이 활성 상태를 유지하는 한 계속 인터럽트를 발생시킵니다. 디바이스가 인터럽트를 발생시키면 CPU가 이를 감지하고 핸들러를 실행하지만, 핸들러가 끝나고 인터럽트가 재활성화되는 시점에 디바이스가 여전히 인터럽트 라인을 assert하고 있으면 즉시 다시 인터럽트가 발생합니다.

Threaded IRQ에서 hardirq handler는 단순히 IRQ_WAKE_THREAD만 반환하고, 실제 인터럽트 원인 제거(데이터 읽기, 상태 클리어 등)는 thread_fn에서 합니다. 따라서 ONESHOT 없이는:

  1. hardirq handler 실행 (IRQ_WAKE_THREAD 반환)
  2. 인터럽트 재활성화 (thread_fn 시작 전!)
  3. 디바이스가 여전히 assert 중 -> 즉시 재발생
  4. 1-3 반복 -> IRQ storm
IRQ Storm 실제 사례: I2C 터치스크린 드라이버에서 ONESHOT 없이 level-triggered 인터럽트를 사용하면, 터치 이벤트 발생 시 thread_fn이 I2C 전송(수 ms)을 완료하기 전에 수천 번의 인터럽트가 발생하여 시스템이 응답 불능 상태가 됩니다.
IRQF_ONESHOT 시퀀스 다이어그램 IRQ 라인 hardirq thread_fn 마스크 상태 IRQ 발생 라인 활성 (assert) ACK IRQ 마스킹 유지 (ONESHOT) WAKE thread_fn 실행 (데이터 읽기, 상태 클리어) 원인 제거 완료 unmask ONESHOT 없이 Level-Triggered 사용 시 (IRQ Storm) 1. hardirq: ACK + IRQ_WAKE_THREAD 반환 2. 인터럽트 즉시 재활성화 (thread_fn 시작 전) 3. 디바이스가 여전히 assert -> 즉시 재발생 -> 반복 -> 시스템 마비! 해결: IRQF_ONESHOT 설정 -> thread_fn 완료까지 마스킹 유지

Edge-Triggered에서의 ONESHOT

Edge-triggered 인터럽트에서는 ONESHOT이 필수는 아닙니다. 에지 트리거는 신호 전이(상승/하강) 시에만 인터럽트가 발생하므로, 라인이 활성 상태를 유지해도 반복 발생하지 않습니다. 그러나 thread_fn 실행 중에 발생한 새 인터럽트를 놓치지 않기 위해 ONESHOT을 사용하는 것이 안전한 관행입니다.

ONESHOT 마스킹 타임라인 상세

ONESHOT의 전체 마스킹 생명주기를 시간축으로 상세하게 표현합니다. 인터럽트 발생부터 마스킹, hardirq 처리, 스레드 웨이크업, thread_fn 실행, 인터럽트 원인 제거, 그리고 최종 언마스킹까지의 모든 단계를 포함합니다:

ONESHOT 마스킹 전체 타임라인 시간 t0 IRQ 발생 t1 mask t2 ACK 완료 t3 thread 시작 t4 원인 제거 t5 unmask IRQ 라인 디바이스가 IRQ 라인 assert (LOW 유지) deassert (원인 제거됨) 마스크 상태 unmasked masked (ONESHOT: thread_fn 완료까지) unmasked hardirq handler() IRQ_WAKE_THREAD 스케줄러 ctx switch thread_fn I2C 전송 / 데이터 읽기 / 상태 클리어 (슬립 가능) finalize unmask() wake_up_process() irq_finalize_oneshot() ONESHOT 보호 구간: 이 동안 같은 IRQ가 재발생하지 않음
ONESHOT 타이밍 핵심: t0(IRQ 발생)부터 t5(unmask)까지가 ONESHOT의 보호 구간입니다. 이 구간에서 인터럽트 라인이 마스킹되어 있으므로, 디바이스가 여전히 인터럽트를 assert하더라도(Level-triggered) CPU에 전달되지 않습니다. thread_fn이 t4에서 인터럽트 원인을 제거한 후, irq_finalize_oneshot()이 t5에서 안전하게 언마스킹합니다.

irq_thread 커널 스레드 내부

threaded IRQ를 등록하면 커널은 전용 커널 스레드를 생성합니다. 이 스레드의 메인 루프와 스케줄링 특성을 분석합니다.

스레드 생성 과정

/* kernel/irq/manage.c — __setup_irq() 내부 */
if (new->thread_fn && !nested) {
    struct task_struct *t;

    t = kthread_create(irq_thread, new,
                       "irq/%d-%s", irq, new->name);
    if (IS_ERR(t))
        return PTR_ERR(t);

    /* RT 우선순위 50으로 설정 (SCHED_FIFO) */
    sched_set_fifo(t);

    /* IRQ affinity와 동일하게 스레드 affinity 설정 */
    get_task_struct(t);
    new->thread = t;

    set_bit(IRQTF_AFFINITY, &new->thread_flags);
}

스레드 이름 규칙

생성된 스레드는 irq/N-name 형식의 이름을 가집니다:

$ ps -eo pid,cls,rtprio,comm | grep irq/
   85  FF    50 irq/9-acpi
  132  FF    50 irq/16-ahci
  207  FF    50 irq/27-i2c-touch
  341  FF    50 irq/136-xhci_hcd

irq_thread() 메인 루프

/* kernel/irq/manage.c — irq_thread() */
static int irq_thread(void *data)
{
    struct irqaction *action = data;
    struct irq_desc *desc = irq_to_desc(action->irq);

    irq_thread_check_affinity(desc, action);

    while (!irq_wait_for_interrupt(action)) {
        irqreturn_t action_ret;

        irq_thread_check_affinity(desc, action);

        action_ret = irq_thread_fn(desc, action);

        if (action_ret == IRQ_HANDLED)
            atomic_inc(&desc->threads_handled);

        wake_threads_waitq(desc);
    }

    return 0;
}

/* irq_wait_for_interrupt(): 스레드를 재운 뒤 IRQTF_RUNTHREAD가
 * 설정될 때까지 대기. hardirq handler가 IRQ_WAKE_THREAD를 반환하면
 * 이 플래그를 설정하고 스레드를 깨움 */

irq_wait_for_interrupt() 상세 분석

irq_wait_for_interrupt()는 IRQ 스레드의 핵심 대기 함수입니다. 이 함수는 스레드를 TASK_INTERRUPTIBLE 상태로 전환하고, hardirq handler가 IRQTF_RUNTHREAD 플래그를 설정할 때까지 대기합니다.

/* kernel/irq/manage.c — irq_wait_for_interrupt() */
static int irq_wait_for_interrupt(struct irqaction *action)
{
    /* 루프: kthread_should_stop() 확인으로 스레드 종료 지원 */
    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);

        /* IRQTF_RUNTHREAD 플래그가 설정되어 있으면 즉시 반환 */
        if (test_and_clear_bit(IRQTF_RUNTHREAD,
                              &action->thread_flags)) {
            __set_current_state(TASK_RUNNING);
            return 0;  /* 성공: thread_fn 실행 필요 */
        }

        /* 스레드 종료 요청 확인 (free_irq에서 호출) */
        if (kthread_should_stop()) {
            __set_current_state(TASK_RUNNING);
            return -1;  /* 종료: 메인 루프 탈출 */
        }

        /* 스케줄링 포인트: 여기서 실제로 슬립 */
        schedule();
    }
}
동작 원리: hardirq handler가 IRQ_WAKE_THREAD를 반환하면, 커널의 __irq_wake_thread() 함수가 호출됩니다. 이 함수는 IRQTF_RUNTHREAD 비트를 설정하고 wake_up_process()로 스레드를 깨웁니다. 깨어난 스레드는 irq_wait_for_interrupt()에서 플래그를 확인하고, test_and_clear_bit()으로 원자적(Atomic)으로 플래그를 클리어한 뒤 thread_fn 실행으로 진행합니다.

__irq_wake_thread() 상세

hardirq handler가 IRQ_WAKE_THREAD를 반환한 후, 커널이 스레드를 깨우는 과정을 분석합니다:

/* kernel/irq/manage.c — __irq_wake_thread() */
void __irq_wake_thread(struct irq_desc *desc,
                        struct irqaction *action)
{
    /* 이미 IRQTF_RUNTHREAD가 설정되어 있으면 스킵
     * (이전 인터럽트의 thread_fn이 아직 실행 중) */
    if (test_bit(IRQTF_RUNTHREAD, &action->thread_flags))
        return;

    /* ONESHOT: threads_oneshot 비트마스크에 자신의 비트 설정
     * 이 비트가 모두 클리어될 때까지 IRQ 라인은 마스킹 유지 */
    if (desc->istate & IRQS_ONESHOT)
        atomic_or(action->thread_mask,
                  &desc->threads_oneshot);

    /* 스레드 실행 플래그 설정 */
    set_bit(IRQTF_RUNTHREAD, &action->thread_flags);

    /* 스레드를 깨움: TASK_INTERRUPTIBLE -> TASK_RUNNING */
    wake_up_process(action->thread);
}
irq_thread() 메인 루프 상세 실행 흐름 kthread_create(irq_thread, ...) irq_thread_check_affinity() [초기] irq_wait_for_interrupt(action) TASK_INTERRUPTIBLE로 슬립 return -1 (종료) kthread_should_stop() IRQTF_RUNTHREAD 감지 irq_thread_check_affinity() irq_thread_fn(desc, action) action->thread_fn() + irq_finalize_oneshot() atomic_inc(threads_handled) + wake_threads_waitq() while 루프 hardirq 컨텍스트 handler() -> IRQ_WAKE_THREAD -> __irq_wake_thread() -> set_bit(IRQTF_RUNTHREAD) -> wake_up_process(thread) wake synchronize_irq() 대기자가 wake_threads_waitq에서 깨어남

thread_flags 비트 필드 상세

플래그설정 시점클리어 시점역할
IRQTF_RUNTHREADBIT(0)__irq_wake_thread()irq_wait_for_interrupt()스레드 실행 요청
IRQTF_WARNEDBIT(1)스퓨리어스 감지 시-경고 중복 방지
IRQTF_AFFINITYBIT(2)affinity 변경 시irq_thread_check_affinity()CPU 마이그레이션 필요
IRQTF_FORCED_THREADBIT(3)irq_setup_forced_threading()-강제 스레드화 표시
IRQTF_READYBIT(4)첫 번째 wait 진입-스레드 준비 완료
irq_thread() 상태 머신 SLEEPING irq_wait_for_interrupt() RUNNING irq_thread_fn() AFFINITY check_affinity() IRQTF_RUNTHREAD 완료 대기 복귀 스케줄링 속성 정책: SCHED_FIFO (실시간) 우선순위: 50 (sched_set_fifo로 설정) affinity: IRQ affinity와 동기화 (irq_thread_check_affinity) 이름: irq/N-devname (ps, top에서 확인 가능) 변경: chrt -f -p PRIO PID (우선순위 조절 가능)

irq_thread_fn() 래퍼 동작

/* kernel/irq/manage.c */
static irqreturn_t irq_thread_fn(struct irq_desc *desc,
                                  struct irqaction *action)
{
    irqreturn_t ret;

    ret = action->thread_fn(action->irq, action->dev_id);

    if (ret == IRQ_HANDLED)
        atomic_inc(&desc->threads_handled);

    irq_finalize_oneshot(desc, action);

    return ret;
}

/* irq_finalize_oneshot()가 ONESHOT 인터럽트의 unmask를 담당 */

force_irqthreads 메커니즘

force_irqthreads는 기존 request_irq()로 등록된 인터럽트도 강제로 스레드화하는 메커니즘입니다. PREEMPT_RT 커널의 핵심 기능이며, 비RT 커널에서도 threadirqs 부트 파라미터로 활성화할 수 있습니다.

활성화 방법

# 방법 1: 커널 cmdline 파라미터
GRUB_CMDLINE_LINUX="threadirqs"

# 방법 2: PREEMPT_RT 커널 (기본 활성화)
# CONFIG_PREEMPT_RT=y이면 force_irqthreads=true

# 확인 방법
cat /proc/cmdline | grep threadirqs
cat /sys/kernel/realtime   # 1이면 RT 커널

내부 동작: irq_setup_forced_threading()

/* kernel/irq/manage.c */
static void irq_setup_forced_threading(struct irqaction *new)
{
    if (!force_irqthreads)
        return;

    /* IRQF_NO_THREAD가 설정된 인터럽트는 스레드화하지 않음 */
    if (new->flags & IRQF_NO_THREAD)
        return;

    /* 이미 threaded_irq이면 건너뜀 */
    if (new->handler == irq_default_primary_handler)
        return;

    /* handler를 thread_fn으로 이동하고,
     * handler를 irq_default_primary_handler로 교체 */
    new->thread_fn = new->handler;
    new->handler = irq_default_primary_handler;

    /* ONESHOT 설정 (안전한 unmask 보장) */
    new->flags |= IRQF_ONESHOT;
}

스레드화에서 제외되는 인터럽트

인터럽트 유형플래그제외 이유
타이머 인터럽트IRQF_NO_THREAD스케줄러(Scheduler) tick, 시간 유지에 필수
IPI (Inter-Processor Interrupt)IRQF_NO_THREADCPU간 즉각 통신 필요
NMI별도 경로마스킹 불가 인터럽트
IRQF_PERCPU내부 로직per-CPU 인터럽트는 이미 경합(Contention) 없음

성능 영향

트레이드오프: force_irqthreads 활성화 시 모든 인터럽트가 커널 스레드 경유로 처리되므로, 스레드 생성/웨이크업 오버헤드(Overhead)(약 2~5us)가 추가됩니다. 그러나 인터럽트 비활성 시간이 극적으로 줄어 전체 시스템 응답성은 개선되며, 특히 실시간 워크로드에서 최악의 경우(worst-case) 지연이 크게 줄어듭니다.

force_irqthreads 소스 코드 심층 분석

force_irqthreads 변수가 어떻게 설정되고, 인터럽트 등록 경로에서 어떻게 활용되는지 커널 소스를 단계별로 추적합니다:

/* kernel/irq/manage.c -- force_irqthreads 변수 정의 */
#ifdef CONFIG_IRQ_FORCED_THREADING
__read_mostly bool force_irqthreads;
EXPORT_SYMBOL_GPL(force_irqthreads);

/* 커널 부트 파라미터 "threadirqs" 처리 */
static int __init setup_forced_irqthreading(char *arg)
{
    force_irqthreads = true;
    return 0;
}
early_param("threadirqs", setup_forced_irqthreading);
#endif

/* PREEMPT_RT에서의 자동 설정:
 * CONFIG_PREEMPT_RT 커널에서는 force_irqthreads가
 * 컴파일 타임에 true로 고정됩니다.
 * include/linux/interrupt.h:
 *   #define force_irqthreads() (true)
 * 실제로는 매크로로 정의되어 변수 접근 오버헤드도 없습니다 */

강제 스레드화 변환 과정 상세

드라이버가 request_irq(irq, my_handler, 0, "dev", dev)로 등록한 경우, force_irqthreads 환경에서 내부적으로 어떻게 변환되는지 분석합니다:

변환 전:

action->handler
my_handler
action->thread_fn
NULL
action->flags
0

irq_setup_forced_threading() 변환 후:

action->handler
irq_default_primary_handler
action->thread_fn
my_handler (handler가 thread_fn으로 이동)
action->flags
IRQF_ONESHOT (ONESHOT 자동 추가)

결과적으로 my_handler가 프로세스 컨텍스트에서 실행됩니다.

force_irqthreads 변환 경로 결정 트리 irq_setup_forced_threading(action) force_irqthreads == true? No 변환 없음 Yes IRQF_NO_THREAD 설정? Yes 변환 안 함 (타이머/IPI) No handler + thread_fn 둘 다 존재? Yes Secondary Action 생성 No handler만 존재: 핵심 변환 수행 thread_fn = handler (handler를 thread_fn으로 이동) handler = irq_default_primary_handler (기본 핸들러) flags |= IRQF_ONESHOT (안전한 unmask 보장)

RT 커널의 spinlock 변환

PREEMPT_RT에서 force_irqthreads가 안전하게 동작하는 핵심 이유는 spinlock의 변환에 있습니다. RT 커널에서 spinlock_trt_mutex로 변환되어 슬립이 가능해집니다:

/* RT 커널에서의 spinlock 변환 개념
 *
 * 일반 커널:
 *   spinlock_t -> 실제 스핀 (busy wait)
 *   spin_lock() 중 슬립 -> BUG
 *
 * PREEMPT_RT:
 *   spinlock_t -> rt_mutex (슬립 가능한 mutex)
 *   spin_lock() -> rt_mutex_lock() (슬립 가능!)
 *   Priority Inheritance 자동 지원
 *
 * raw_spinlock_t -> 실제 스핀 (RT에서도 변환 안 됨)
 *   irq_desc->lock 등 내부 잠금에만 사용 */

/* 예: 기존 드라이버의 handler가 spinlock을 사용하는 경우 */
static irqreturn_t my_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    spin_lock(&dev->lock);
    /* 일반 커널: 스핀 (원자적, 슬립 불가)
     * RT 커널: rt_mutex_lock (슬립 가능, PI 지원)
     * -> force_irqthreads로 스레드화되어도 안전합니다 */
    dev->status = ioread32(dev->regs + STATUS);
    spin_unlock(&dev->lock);

    return IRQ_HANDLED;
}
raw_spinlock_t 주의: raw_spinlock_t는 RT 커널에서도 변환되지 않는 진짜 스핀락(Spinlock)입니다. 커널 내부의 최소한의 크리티컬 섹션(IRQ 디스크립터 잠금(Lock), 스케줄러 잠금 등)에만 사용됩니다. 드라이버에서는 특별한 이유가 없는 한 raw_spinlock_t 대신 spinlock_t를 사용해야 합니다.

Managed IRQ (devm API)

devm_request_threaded_irq()는 디바이스 리소스 관리(devres) 프레임워크와 통합된 IRQ 등록 API입니다. 드라이버 probe에서 등록하면 remove 시 자동으로 해제됩니다.

API 시그니처

/* include/linux/interrupt.h */
extern int devm_request_threaded_irq(
    struct device    *dev,         /* 디바이스 구조체 */
    unsigned int     irq,
    irq_handler_t    handler,
    irq_handler_t    thread_fn,
    unsigned long    irqflags,
    const char      *devname,
    void            *dev_id
);

/* 편의 래퍼: thread_fn=NULL */
static inline int devm_request_irq(
    struct device *dev, unsigned int irq,
    irq_handler_t handler, unsigned long irqflags,
    const char *devname, void *dev_id)
{
    return devm_request_threaded_irq(dev, irq, handler,
                                      NULL, irqflags, devname, dev_id);
}

자동 해제 순서

devres는 LIFO(Last-In-First-Out) 순서로 리소스를 해제합니다. 따라서 probe에서 IRQ를 먼저 등록하고 다른 리소스를 나중에 등록하면, remove 시 다른 리소스가 먼저 해제된 후 IRQ가 해제됩니다:

static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    int irq, ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    /* devm로 등록: remove 시 자동 해제 */
    ret = devm_request_threaded_irq(&pdev->dev, irq,
                                     my_hardirq, my_thread_fn,
                                     IRQF_ONESHOT,
                                     dev_name(&pdev->dev), dev);
    if (ret)
        return ret;

    /* 추가 초기화 (devm 기반이므로 에러 시 자동 정리) */
    dev->regs = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(dev->regs))
        return PTR_ERR(dev->regs);  /* IRQ도 자동 해제됨 */

    platform_set_drvdata(pdev, dev);
    return 0;
}

/* remove 함수: devm 사용 시 비어있을 수 있음 */
static void my_remove(struct platform_device *pdev)
{
    /* devm이 모든 리소스를 자동 해제 */
}
devres 생명주기: probe/remove에서의 자동 해제 probe() — 리소스 등록 순서 1. devm_kzalloc 메모리 할당 2. devm_request_threaded_irq IRQ 등록 3. devm_ioremap_resource MMIO 매핑 4. devm_clk_get 클럭 획득 ▼ devres 스택 (LIFO) remove() — 자동 해제 순서 (역순) 1st: clk_put 클럭 해제 2nd: iounmap MMIO 해제 3rd: free_irq IRQ 해제 + 스레드 종료 대기 4th: kfree 메모리 해제 에러 경로에서도 동일하게 동작 probe 중 3단계에서 에러 발생 → 이미 등록된 1, 2단계 리소스 자동 역순 해제 (goto 에러 처리 불필요)
모범 사례: 새로운 드라이버에서는 항상 devm_request_threaded_irq()를 사용하세요. 에러 경로에서의 수동 정리 코드를 제거하고, 리소스 누수 버그를 방지합니다. 기존 드라이버의 request_irq()/free_irq() 쌍도 가능하면 devm으로 전환하는 것을 권장합니다.

devm IRQ 생명주기 상세

devm_request_threaded_irq()의 내부 구현과 리소스 관리 메커니즘을 상세히 분석합니다:

/* kernel/irq/devres.c — devm_request_threaded_irq() 내부 */
int devm_request_threaded_irq(struct device *dev,
    unsigned int irq, irq_handler_t handler,
    irq_handler_t thread_fn, unsigned long irqflags,
    const char *devname, void *dev_id)
{
    struct irq_devres *dr;
    int rc;

    /* devres 엔트리 할당 (IRQ 번호와 dev_id 저장) */
    dr = devres_alloc(devm_irq_release,
                       sizeof(*dr), GFP_KERNEL);
    if (!dr)
        return -ENOMEM;

    /* 실제 IRQ 등록 */
    rc = request_threaded_irq(irq, handler, thread_fn,
                              irqflags, devname, dev_id);
    if (rc) {
        devres_free(dr);
        return rc;
    }

    dr->irq = irq;
    dr->dev_id = dev_id;
    devres_add(dev, dr);  /* devres 스택에 추가 */

    return 0;
}

/* devres 해제 콜백: device_release_driver() 시 자동 호출 */
static void devm_irq_release(struct device *dev, void *res)
{
    struct irq_devres *dr = res;

    /* free_irq()는 다음을 수행합니다:
     * 1. IRQ 비활성화
     * 2. 실행 중인 hardirq handler 완료 대기
     * 3. 실행 중인 thread_fn 완료 대기
     * 4. 커널 스레드 종료 (kthread_stop)
     * 5. irqaction 구조체 해제 */
    free_irq(dr->irq, dr->dev_id);
}
devm IRQ 생명주기: 등록부터 해제까지 probe() 호출 devm_kzalloc() devres[0]: 메모리 devm_ioremap() devres[1]: MMIO devm_request_threaded_irq() devres[2]: IRQ + 커널 스레드 생성 devm_clk_get() devres[3]: 클럭 정상 동작 중 IRQ 발생 -> hardirq handler -> IRQ_WAKE_THREAD -> irq/N-name 스레드 -> thread_fn remove() 또는 probe 에러 시: devres LIFO 해제 1st: clk_put() devres[3] 해제 2nd: free_irq() devres[2] 해제 + 스레드 종료 대기 3rd: iounmap() devres[1] 해제 4th: kfree() devres[0] 해제 free_irq() 내부 동작 상세 1. IRQ 라인 비활성화 (새 인터럽트 수신 중단) 2. synchronize_irq(): 실행 중인 hardirq handler 완료 대기 3. kthread_stop(): IRQ 스레드에 종료 신호 전달 + thread_fn 완료 대기 + 스레드 종료 4. irqaction 구조체 메모리 해제

devm_free_irq(): 수동 조기 해제

드라이버가 remove 전에 IRQ를 먼저 해제해야 하는 경우, devm_free_irq()를 사용할 수 있습니다. 이 함수는 devres 스택에서도 해당 엔트리를 제거하므로 이중 해제(Double Free)가 발생하지 않습니다:

/* 런타임에 IRQ를 일시적으로 해제해야 하는 경우 */
void my_runtime_reconfigure(struct my_device *dev)
{
    /* 수동 해제 (devres에서도 제거됨) */
    devm_free_irq(dev->dev, dev->irq, dev);

    /* 하드웨어 재구성 */
    reconfigure_hardware(dev);

    /* 새로운 설정으로 다시 등록 */
    devm_request_threaded_irq(dev->dev, dev->new_irq,
        my_new_hardirq, my_new_thread_fn,
        IRQF_ONESHOT, "dev-reconfig", dev);
}

공유 인터럽트에서의 Threaded IRQ

PCI 레거시 인터럽트(INTx)는 여러 디바이스가 같은 IRQ 라인을 공유합니다. 공유 인터럽트에서 threaded IRQ를 사용할 때는 추가 규칙이 적용됩니다.

핵심 규칙

/* 공유 인터럽트 threaded IRQ 예제 */
static irqreturn_t shared_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = ioread32(dev->regs + IRQ_STATUS);

    /* 자기 디바이스의 인터럽트가 아니면 IRQ_NONE */
    if (!(status & MY_IRQ_PENDING))
        return IRQ_NONE;

    /* 인터럽트 ACK (다른 디바이스에 영향 없도록) */
    iowrite32(MY_IRQ_ACK, dev->regs + IRQ_STATUS);

    /* 상태 저장 후 스레드로 위임 */
    dev->irq_status = status;
    return IRQ_WAKE_THREAD;
}

static irqreturn_t shared_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    mutex_lock(&dev->lock);
    handle_interrupt(dev, dev->irq_status);
    mutex_unlock(&dev->lock);

    return IRQ_HANDLED;
}

ret = devm_request_threaded_irq(&pdev->dev, irq,
    shared_hardirq, shared_thread_fn,
    IRQF_SHARED | IRQF_ONESHOT,
    "my-pci-dev", dev);
공유 인터럽트(Shared IRQ)에서의 Threaded IRQ 시퀀스 IRQ 라인 Dev A handler Dev B handler Thread A Thread B ONESHOT 마스크 IRQ 발생 확인→WAKE 확인→NONE IRQ 마스킹 유지 (threads_oneshot 비트마스크) WAKE thread_fn A 실행 (데이터 처리) 비트 클리어 모든 비트 0 → unmask 공유 IRQ + ONESHOT 동작 요약 1. 각 handler가 순차 호출 (Dev A → Dev B), 자기 것이면 IRQ_WAKE_THREAD, 아니면 IRQ_NONE 2. 모든 thread_fn 완료 후 threads_oneshot == 0 이 될 때만 unmask (irq_finalize_oneshot)

Nested Threaded IRQ

Nested threaded IRQ는 I2C/SPI와 같은 슬로우 버스에 연결된 디바이스의 인터럽트를 처리하는 특수한 패턴입니다. 이 경우 부모 IRQ 컨트롤러 자체가 이미 threaded IRQ로 동작하므로, 자식 IRQ도 부모의 스레드 컨텍스트에서 실행됩니다.

Nested IRQ의 동작 원리

GPIO expander가 I2C 버스에 연결된 경우를 예로 들면:

  1. GPIO expander의 인터럽트 출력이 SoC의 GPIO 핀에 연결
  2. SoC GPIO 인터럽트는 threaded IRQ로 처리 (I2C 접근이 필요하므로)
  3. threaded handler 내에서 I2C를 통해 expander의 인터럽트 상태를 읽음
  4. handle_nested_irq()를 호출하여 자식 IRQ를 부모 스레드에서 처리
/* I2C GPIO expander의 threaded handler (부모) */
static irqreturn_t expander_irq_thread(int irq, void *dev_id)
{
    struct expander *exp = dev_id;
    unsigned long pending;
    int child_irq;

    /* I2C로 인터럽트 상태 레지스터 읽기 (슬립 가능) */
    pending = i2c_smbus_read_byte_data(exp->client, REG_INT_STATUS);

    for_each_set_bit(child_irq, &pending, exp->ngpio) {
        /* 자식 IRQ를 부모 스레드 컨텍스트에서 직접 호출 */
        handle_nested_irq(irq_find_mapping(exp->domain, child_irq));
    }

    return IRQ_HANDLED;
}

/* 자식 IRQ의 nested 속성 설정 */
static void expander_irq_setup(struct expander *exp)
{
    int i;
    for (i = 0; i < exp->ngpio; i++) {
        int virq = irq_create_mapping(exp->domain, i);
        irq_set_nested_thread(virq, 1);
        irq_set_noprobe(virq);
    }
}
Nested IRQ 체인 (I2C GPIO Expander) SoC GIC Primary IRQ SoC GPIO Threaded IRQ I2C Bus 슬로우 버스 GPIO Expander Nested IRQ 터치스크린 센서 실행 흐름 1. SoC GPIO에 인터럽트 발생 -> hardirq handler: IRQ_WAKE_THREAD 2. irq/N-gpio 스레드에서 I2C로 expander 상태 읽기 (슬립 OK) 3. handle_nested_irq()로 자식 IRQ 처리 -> 자식 handler가 부모 스레드에서 직접 실행

Nested IRQ 계층 구조 상세

실제 임베디드 시스템에서는 여러 단계의 인터럽트 계층이 형성됩니다. 예를 들어, SoC의 GIC가 SoC GPIO 컨트롤러에 연결되고, GPIO가 I2C GPIO expander에 연결되며, expander의 각 핀에 최종 디바이스(터치스크린, 센서, 버튼)가 연결됩니다. 각 계층에서 threaded IRQ와 nested IRQ의 조합이 어떻게 동작하는지 분석합니다:

다단계 Nested IRQ 계층 구조 Level 0 hardirq GIC (Generic Interrupt Controller) hwirq 0~1023, hardirq 컨텍스트 Level 1 threaded SoC GPIO 컨트롤러 (gpiolib) irq_domain, chained_irq 또는 threaded PMIC IRQ 컨트롤러 (I2C) regmap_irq, threaded IRQ Level 2 nested I2C GPIO Expander PCA953x (nested) 가속도 센서 BMI160 (threaded) 충전기 VBUS detect 온도 센서 Overtemp RTC 알람 Level 3 nested 터치스크린 근접 센서 인터럽트 처리 흐름 (터치스크린 예시) 1. GIC (hardirq) GPIO 핀의 전기적 변화 감지 -> SoC GPIO IRQ handler 호출 2. SoC GPIO (threaded) irq/N-gpio 스레드에서 I2C로 PCA953x 상태 읽기 3. PCA953x (nested) handle_nested_irq()로 터치스크린 IRQ 호출 (같은 스레드에서) 4. 터치스크린 (nested) I2C로 터치 좌표 읽기 -> input_report_abs() -> input_sync() 핵심: Level 2, 3의 nested handler들은 별도 커널 스레드를 생성하지 않습니다. 모두 Level 1의 irq/N-gpio 스레드 컨텍스트에서 순차적으로 실행됩니다. 이는 슬로우 버스(I2C) 접근이 필요한 디바이스 계층에서 스레드 폭발을 방지합니다.

handle_nested_irq() 구현 상세

handle_nested_irq()는 부모의 thread_fn 컨텍스트에서 자식 IRQ를 직접 처리합니다. 별도의 커널 스레드를 생성하지 않으며, 자식의 action->thread_fn을 현재 컨텍스트에서 직접 호출합니다:

/* kernel/irq/chip.c — handle_nested_irq() */
void handle_nested_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq);
    struct irqaction *action;
    irqreturn_t action_ret;

    might_sleep();  /* 프로세스 컨텍스트 확인 */

    raw_spin_lock_irq(&desc->lock);
    desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

    action = desc->action;
    if (unlikely(!action || irqd_irq_disabled(&desc->irq_data))) {
        desc->istate |= IRQS_PENDING;
        raw_spin_unlock_irq(&desc->lock);
        return;
    }

    kstat_incr_irqs_this_cpu(desc);
    irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
    raw_spin_unlock_irq(&desc->lock);

    /* 자식의 thread_fn을 현재 스레드에서 직접 호출 */
    action_ret = IRQ_NONE;
    for_each_action_of_desc(desc, action)
        action_ret |= action->thread_fn(action->irq,
                                         action->dev_id);

    /* 정리: INPROGRESS 클리어 */
    raw_spin_lock_irq(&desc->lock);
    irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
    raw_spin_unlock_irq(&desc->lock);
}
nested vs threaded 선택: nested IRQ를 사용할지 일반 threaded IRQ를 사용할지는 irq_set_nested_thread()로 결정합니다. nested로 설정하면 별도 커널 스레드가 생성되지 않아 리소스를 절약할 수 있지만, 모든 자식 IRQ가 부모의 스레드에서 순차적으로 처리되므로 처리 시간이 긴 자식이 있으면 다른 자식의 처리가 지연됩니다.

Threaded IRQ vs 다른 Bottom Half 비교

인터럽트 처리의 지연 작업(deferred work)을 구현하는 여러 메커니즘이 있습니다. 각각의 특성과 threaded IRQ가 최선인 상황을 비교합니다.

기준Threaded IRQWorkqueueTaskletSoftirq
실행 컨텍스트프로세스 (커널 스레드)프로세스 (kworker)Softirq (인터럽트)Softirq (인터럽트)
슬립 가능가능가능불가불가
우선순위 제어RT 우선순위 (chrt)nice 또는 WQ_HIGHPRI불가 (고정)불가 (고정)
PREEMPT_RT 호환완벽양호제거 예정스레드화됨
지연 시간스레드 웨이크업 (2~5us)kworker 스케줄링softirq 지연즉시 (softirq)
IRQ와의 결합자동 (request_threaded_irq)수동 (queue_work)수동 (tasklet_schedule)수동 (raise_softirq)
구현 복잡도낮음 (API가 통합)중간낮음높음
Bottom Half 선택 결정 트리 인터럽트 후속 작업 필요? 슬립 필요? (I2C, mutex, 할당) Yes 전용 스레드 필요? Yes Threaded IRQ No Workqueue No 극한 저지연 필요? Yes Softirq No Threaded IRQ

결정 기준

Threaded IRQ를 선택해야 할 때:
  • 인터럽트 처리에서 슬립이 필요한 경우 (I2C/SPI 전송, mutex, GFP_KERNEL 할당)
  • 인터럽트 처리의 RT 우선순위를 제어해야 할 경우
  • PREEMPT_RT 커널 호환이 필요한 경우
  • 기존 hardirq + workqueue 패턴을 단순화하고 싶은 경우
  • 인터럽트별 전용 스레드가 필요한 경우 (workqueue는 공유 스레드풀)

PREEMPT_RT에서의 Threaded IRQ

PREEMPT_RT 커널에서 threaded IRQ는 핵심적인 역할을 합니다. RT 커널은 force_irqthreads=true를 기본으로 설정하여, IRQF_NO_THREAD가 아닌 모든 인터럽트를 스레드화합니다.

RT 커널에서의 인터럽트 처리 구조

PREEMPT_RT 인터럽트 처리 스택 하드웨어 인터럽트 (최소한의 hardirq) ACK + IRQ_WAKE_THREAD (IRQF_NO_THREAD 제외) IRQ 스레드 (irq/N-name, SCHED_FIFO) RT 우선순위로 스케줄링, 선점 가능, 슬립 가능 RT 애플리케이션 SCHED_FIFO/RR, 높은 우선순위 일반 프로세스 SCHED_OTHER, CFS RT 우선순위 설정 예 chrt -f -p 90 $(pgrep irq/27-i2c) # I2C 터치: 높은 우선순위 chrt -f -p 50 $(pgrep irq/16-ahci) # AHCI: 기본 우선순위 chrt -f -p 95 $(pgrep my-rt-app) # RT 앱: IRQ보다 높게 설정 가능

우선순위 역전 방지와 PI

RT 커널에서 spinlock_t는 rt_mutex로 변환되며, rt_mutex는 Priority Inheritance(PI)를 지원합니다. IRQ 스레드가 rt_mutex를 대기할 때, 보유자의 우선순위가 자동으로 부스팅되어 우선순위 역전을 방지합니다.

# RT 커널에서 IRQ 스레드 우선순위 확인 및 변경
$ ps -eo pid,cls,rtprio,comm | grep "irq/"
   85  FF    50 irq/9-acpi
  132  FF    50 irq/16-ahci
  207  FF    50 irq/27-i2c-touch

# 중요한 인터럽트의 우선순위 올리기
$ chrt -f -p 80 207

# IRQ affinity 설정 (특정 CPU에 바인딩)
$ echo 2 > /proc/irq/27/smp_affinity

# cyclictest로 인터럽트 지연 측정
$ cyclictest -t1 -p 98 -i 1000 -l 10000
T: 0 Min:      1 Act:    1 Avg:    1 Max:      12

RT 커널 튜닝 가이드

항목설정효과
IRQ 스레드 우선순위chrt -f -p PRIO PID중요한 인터럽트 우선 처리
IRQ affinity/proc/irq/N/smp_affinity특정 CPU에 바인딩으로 캐시(Cache) 효율 향상
CPU 격리(Isolation)isolcpus=2,3RT 전용 CPU 확보
커널 타이머nohz_full=2,3격리된 CPU에서 tick 제거
RCU 콜백(Callback)rcu_nocbs=2,3RCU 콜백 오프로딩(Offloading)

PREEMPT_RT 메인라인 병합 이후의 변화 (v6.12)

v6.12에서 PREEMPT_RT 핵심 기능이 메인라인에 병합되면서, 이전에 rt-patches를 적용해야 사용할 수 있었던 threaded IRQ 거동이 주류 커널에서도 동일하게 동작합니다. 드라이버 코드 변경은 필요 없지만, 우선순위와 지연 프로파일이 다음과 같이 정돈되었습니다.

튜닝 포인트: RT 메인라인이 기본이 된 v6.12+에서는 "RT 패치 적용 여부 확인" 과정이 불필요해졌습니다. 대신 CONFIG_PREEMPT_RT=y 여부, 커널 커맨드라인의 threadirqs, /sys/kernel/debug/sched_featuresPREEMPT_RT 지표만 확인하면 됩니다.

force_irqthreads 성능 오버헤드 최신 측정

# x86_64 Icelake 서버, Linux 6.12 PREEMPT_RT 빌드 기준
# (cyclictest + tracer 기반 내부 벤치)

구간                                    v5.15-RT    v6.12 RT (mainline)
--------------------------------------  ----------  -------------------
hardirq 진입~ACK                         ~350ns      ~300ns
IRQ_WAKE_THREAD 반환~스레드 런큐 진입   ~1.2μs      ~0.6μs
런큐 → 스레드 실행 시작                 ~0.8μs      ~0.7μs
thread_fn 첫 실행까지 총 지연           ~2.3μs      ~1.5μs
최악 cyclictest max (3600s)             ~25μs       ~18μs

수치는 측정 환경 의존적이지만, PREEMPT_RT 메인라인 병합으로 전반적인 IRQ → 스레드 경로가 개선된 경향을 보여줍니다. 개별 드라이버가 큰 영향을 받는 경우는 드물며, 주로 다수의 IRQ가 동시에 발생하는 고-IOPS 환경에서 누적 효과가 나타납니다.

기존 드라이버 마이그레이션 가이드

기존 드라이버를 threaded IRQ 패턴으로 전환하는 세 가지 주요 마이그레이션 시나리오를 다룹니다.

패턴 1: request_irq() -> request_threaded_irq()

/* === 변경 전: hardirq에서 모든 작업 수행 === */
static irqreturn_t old_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = ioread32(dev->regs + STATUS);

    if (!(status & IRQ_PENDING))
        return IRQ_NONE;

    iowrite32(IRQ_ACK, dev->regs + STATUS);
    process_data(dev);  /* 시간이 걸리는 작업 */
    return IRQ_HANDLED;
}
request_irq(irq, old_handler, 0, "my-dev", dev);

/* === 변경 후: hardirq + thread_fn 분리 === */
static irqreturn_t new_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = ioread32(dev->regs + STATUS);

    if (!(status & IRQ_PENDING))
        return IRQ_NONE;

    iowrite32(IRQ_ACK, dev->regs + STATUS);
    dev->saved_status = status;
    return IRQ_WAKE_THREAD;
}

static irqreturn_t new_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    process_data(dev);
    return IRQ_HANDLED;
}
request_threaded_irq(irq, new_hardirq, new_thread_fn,
                     IRQF_ONESHOT, "my-dev", dev);

패턴 2: hardirq + workqueue -> Threaded IRQ

/* === 변경 전: hardirq에서 workqueue 스케줄링 === */
static irqreturn_t old_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    iowrite32(IRQ_ACK, dev->regs + STATUS);
    queue_work(dev->wq, &dev->work);  /* workqueue에 위임 */
    return IRQ_HANDLED;
}

static void old_work_fn(struct work_struct *work)
{
    struct my_device *dev = container_of(work, struct my_device, work);
    mutex_lock(&dev->lock);
    slow_operation(dev);
    mutex_unlock(&dev->lock);
}

/* === 변경 후: 단일 threaded IRQ로 통합 === */
static irqreturn_t new_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    iowrite32(IRQ_ACK, dev->regs + STATUS);
    return IRQ_WAKE_THREAD;
}

static irqreturn_t new_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    mutex_lock(&dev->lock);
    slow_operation(dev);
    mutex_unlock(&dev->lock);
    return IRQ_HANDLED;
}
/* workqueue, work_struct, INIT_WORK 모두 제거 가능 */

패턴 3: Tasklet -> Threaded IRQ

/* === 변경 전: tasklet 사용 === */
static void old_tasklet_fn(struct tasklet_struct *t)
{
    struct my_device *dev = from_tasklet(dev, t, tasklet);
    process_data(dev);  /* 주의: 슬립 불가! */
}

static irqreturn_t old_handler(int irq, void *dev_id)
{
    iowrite32(IRQ_ACK, dev->regs + STATUS);
    tasklet_schedule(&dev->tasklet);
    return IRQ_HANDLED;
}

/* === 변경 후: threaded IRQ (tasklet, DECLARE_TASKLET 제거) === */
static irqreturn_t new_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    process_data(dev);  /* 이제 슬립 가능! */
    return IRQ_HANDLED;
}

마이그레이션 체크리스트

#확인 항목설명
1hardirq handler에서 슬립 함수 호출이 없는가?ACK, 상태 읽기, 마스킹만 수행
2IRQF_ONESHOT이 설정되어 있는가?handler=NULL 시 필수, level-triggered 시 강력 권장
3공유 IRQ에서 handler가 자기 디바이스를 확인하는가?IRQ_NONE 반환 경로 확인
4hardirq handler와 thread_fn 간 데이터 동기화가 안전한가?volatile/atomic 또는 spin_lock 사용
5devm API로 전환했는가?에러 경로 자동 정리
6workqueue/tasklet 관련 코드를 제거했는가?불필요한 구조체(Struct), 초기화 코드 정리
7PREEMPT_RT에서 테스트했는가?force_irqthreads 환경에서 검증

I2C/SPI 디바이스 드라이버 실전

I2C/SPI 디바이스는 threaded IRQ의 가장 대표적인 사용 사례입니다. 슬로우 버스를 통한 레지스터 접근은 수백 us에서 수 ms가 소요되므로 hardirq에서 처리할 수 없습니다.

regmap_irq: 자동 IRQ chip 생성

regmap_irq 프레임워크는 레지스터 기반 인터럽트 컨트롤러(Interrupt Controller)를 자동으로 구성합니다. PMIC, 코덱, 센서 허브 등 복잡한 인터럽트 구조를 가진 디바이스에 매우 유용합니다.

/* PMIC 드라이버의 regmap_irq 설정 예 */
static const struct regmap_irq pmic_irqs[] = {
    REGMAP_IRQ_REG(0, 0, BIT(0)),  /* VBUS detect */
    REGMAP_IRQ_REG(1, 0, BIT(1)),  /* Overtemp */
    REGMAP_IRQ_REG(2, 0, BIT(2)),  /* Low battery */
    REGMAP_IRQ_REG(3, 1, BIT(0)),  /* Button press */
};

static const struct regmap_irq_chip pmic_irq_chip = {
    .name           = "pmic",
    .irqs           = pmic_irqs,
    .num_irqs       = ARRAY_SIZE(pmic_irqs),
    .num_regs       = 2,
    .status_base    = PMIC_INT_STATUS,
    .mask_base      = PMIC_INT_MASK,
    .ack_base       = PMIC_INT_ACK,
};

static int pmic_probe(struct i2c_client *client)
{
    struct pmic_data *pmic;
    int ret;

    pmic->regmap = devm_regmap_init_i2c(client, &pmic_regmap_config);

    /* regmap_irq가 threaded IRQ를 자동 등록 */
    ret = devm_regmap_add_irq_chip(&client->dev, pmic->regmap,
                                    client->irq,
                                    IRQF_ONESHOT | IRQF_TRIGGER_LOW,
                                    0, &pmic_irq_chip,
                                    &pmic->irq_data);
    if (ret)
        return ret;

    /* 개별 IRQ를 자식 디바이스에 전달 */
    pmic->charger_irq = regmap_irq_get_virq(pmic->irq_data, 0);
    pmic->thermal_irq = regmap_irq_get_virq(pmic->irq_data, 1);

    return 0;
}

터치스크린 드라이버 예제

/* I2C 터치스크린: handler=NULL, thread_fn만 사용 */
static irqreturn_t touch_thread_fn(int irq, void *dev_id)
{
    struct touch_device *ts = dev_id;
    u8 buf[10];

    /* I2C 전송 (슬립 가능, 수백 us) */
    i2c_smbus_read_i2c_block_data(ts->client, REG_TOUCH, 10, buf);

    input_report_abs(ts->input, ABS_X, get_x(buf));
    input_report_abs(ts->input, ABS_Y, get_y(buf));
    input_report_key(ts->input, BTN_TOUCH, get_touch(buf));
    input_sync(ts->input);

    return IRQ_HANDLED;
}

ret = devm_request_threaded_irq(&client->dev, client->irq,
    NULL, touch_thread_fn,
    IRQF_ONESHOT | IRQF_TRIGGER_LOW,
    "touch-irq", ts);

GPIO Interrupt와 Threaded IRQ

GPIO 인터럽트는 임베디드 시스템에서 버튼, 센서, 외부 디바이스 연결에 광범위하게 사용됩니다. GPIO subsystem과 threaded IRQ의 결합 패턴을 살펴봅니다.

/* GPIO 인터럽트 + threaded IRQ 예제 */
static int sensor_probe(struct platform_device *pdev)
{
    struct sensor_data *data;
    struct gpio_desc *irq_gpio;
    int irq, ret;

    data = devm_kzalloc(&pdev->dev, sizeof(*data), GFP_KERNEL);

    /* GPIO 디스크립터 가져오기 */
    irq_gpio = devm_gpiod_get(&pdev->dev, "irq", GPIOD_IN);
    if (IS_ERR(irq_gpio))
        return PTR_ERR(irq_gpio);

    /* GPIO -> IRQ 번호 변환 */
    irq = gpiod_to_irq(irq_gpio);
    if (irq < 0)
        return irq;

    /* threaded IRQ 등록 (하강 에지에서 트리거) */
    ret = devm_request_threaded_irq(&pdev->dev, irq,
        NULL, sensor_irq_thread,
        IRQF_ONESHOT | IRQF_TRIGGER_FALLING,
        "sensor-data-ready", data);
    if (ret)
        return ret;

    data->irq = irq;
    platform_set_drvdata(pdev, data);
    return 0;
}

static irqreturn_t sensor_irq_thread(int irq, void *dev_id)
{
    struct sensor_data *data = dev_id;
    int val;

    /* 프로세스 컨텍스트: I2C/SPI 접근 가능 */
    val = i2c_smbus_read_word_data(data->client, REG_DATA);
    if (val < 0)
        return IRQ_HANDLED;

    data->last_value = val;
    complete(&data->data_ready);
    return IRQ_HANDLED;
}

Debounce와 Threaded IRQ

GPIO 버튼의 바운싱 문제는 소프트웨어 디바운싱으로 해결하며, threaded IRQ에서 msleep()을 사용하여 안정화 시간을 기다릴 수 있습니다:

static irqreturn_t button_thread_fn(int irq, void *dev_id)
{
    struct button_data *btn = dev_id;

    /* 디바운스: 20ms 대기 후 상태 확인 */
    msleep(20);

    int state = gpiod_get_value_cansleep(btn->gpio);
    input_report_key(btn->input, btn->keycode, state);
    input_sync(btn->input);

    return IRQ_HANDLED;
}
GPIO Interrupt + Threaded IRQ 처리 흐름 외부 디바이스 센서/버튼 GPIO핀 GPIO 컨트롤러 에지/레벨 감지 hwirq IRQ 도메인 gpiod_to_irq() virq GIC / APIC 인터럽트 컨트롤러 CPU hardirq 진입 소프트웨어 처리 흐름 hardirq handler irq_default_primary_handler IRQ_WAKE_THREAD irq/N-sensor 스레드 thread_fn (프로세스 컨텍스트) I2C/SPI 통신 레지스터 읽기 (슬립) 결과 처리 complete() devm_gpiod_get(dev, "irq", GPIOD_IN) → gpiod_to_irq(gpio) → devm_request_threaded_irq(dev, irq, NULL, fn, IRQF_ONESHOT|IRQF_TRIGGER_*, ...) 디바운스: gpiod_set_debounce(gpio, us) (HW 지원 시) 또는 thread_fn에서 msleep() (SW 디바운스) 주의: gpiod_get_value()는 hardirq 안전, gpiod_get_value_cansleep()은 thread_fn에서만 사용 (I2C GPIO expander) IRQ 번호 획득: platform_get_irq() (DT), gpiod_to_irq() (GPIO), pci_irq_vector() (PCI)

PCIe 디바이스의 Threaded IRQ

PCIe 디바이스에서 threaded IRQ를 사용하는 패턴은 MSI/MSI-X 사용 여부에 따라 달라집니다.

MSI/MSI-X + Threaded IRQ

static int pci_dev_probe(struct pci_dev *pdev,
                          const struct pci_device_id *id)
{
    struct my_pci_dev *dev;
    int ret;

    ret = pcim_enable_device(pdev);
    if (ret)
        return ret;

    /* MSI 벡터 할당 */
    ret = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSI | PCI_IRQ_MSIX);
    if (ret < 0)
        return ret;

    /* MSI는 edge-triggered이므로 ONESHOT 불필요하지만
     * 안전을 위해 설정하는 것도 괜찮음 */
    ret = devm_request_threaded_irq(&pdev->dev,
        pci_irq_vector(pdev, 0),
        my_pci_hardirq, my_pci_thread,
        0,  /* MSI: ONESHOT 선택적, SHARED 불필요 */
        "my-pci", dev);

    return ret;
}

static irqreturn_t my_pci_hardirq(int irq, void *dev_id)
{
    struct my_pci_dev *dev = dev_id;

    /* DMA 완료 상태 확인 */
    if (!(ioread32(dev->mmio + DMA_STATUS) & DMA_DONE))
        return IRQ_NONE;

    /* 인터럽트 비활성화 (재발생 방지) */
    iowrite32(0, dev->mmio + IRQ_ENABLE);

    return IRQ_WAKE_THREAD;
}

static irqreturn_t my_pci_thread(int irq, void *dev_id)
{
    struct my_pci_dev *dev = dev_id;

    /* DMA 버퍼 처리 (프로세스 컨텍스트) */
    process_dma_buffer(dev);

    /* 인터럽트 재활성화 */
    iowrite32(IRQ_ENABLE_ALL, dev->mmio + IRQ_ENABLE);

    return IRQ_HANDLED;
}

PCI INTx 공유 인터럽트

레거시 PCI INTx를 사용하는 경우, 반드시 IRQF_SHARED를 설정하고 handler에서 자기 디바이스의 인터럽트인지 확인해야 합니다. MSI/MSI-X로 전환하면 이 제약이 없어집니다.

PCIe IRQ 유형별 Threaded IRQ 비교 INTx (레거시 PCI) Dev A Dev B Dev C 공유 IRQ 라인 IRQF_SHARED | IRQF_ONESHOT, handler 필수 MSI / MSI-X 벡터 0 벡터 1 벡터 2 전용 IRQ 전용 IRQ 전용 IRQ SHARED 불필요, Edge-triggered, ONESHOT 선택적 유형별 Threaded IRQ 설정 비교 항목 INTx MSI MSI-X IRQF_SHARED 필수 불필요 불필요 IRQF_ONESHOT 필수 (level) 선택적 (edge) 선택적 (edge) handler 구현 필수 (디바이스 확인) 선택적 (NULL 가능) 선택적 (NULL 가능) 벡터 수 1 (공유) 1~32 1~2048

동기화와 잠금(Lock) 패턴

threaded IRQ에서는 hardirq handler와 thread_fn이 서로 다른 컨텍스트에서 실행되므로, 공유 데이터 접근 시 적절한 동기화가 필요합니다.

hardirq handler와 thread_fn 간 동기화

시나리오동기화 방법설명
hardirq -> thread_fn 데이터 전달구조체 필드 + WRITE_ONCE/READ_ONCEhardirq에서 저장, thread_fn에서 읽기 (순서 보장(Ordering))
thread_fn 간 공유 데이터mutexthread_fn은 프로세스 컨텍스트이므로 mutex 사용 가능
hardirq + thread_fn 동시 접근spin_lock_irqsave()hardirq 컨텍스트에서도 안전
카운터 업데이트atomic_t잠금 없이 원자적 연산(Atomic Operation)

synchronize_irq() vs disable_irq()

/* synchronize_irq(): 현재 실행 중인 handler + thread_fn 완료 대기 */
synchronize_irq(dev->irq);
/* 이 시점에서 handler와 thread_fn이 실행 중이지 않음을 보장 */
cleanup_resources(dev);

/* disable_irq(): IRQ 비활성화 + 실행 중인 handler/thread_fn 완료 대기 */
disable_irq(dev->irq);
/* IRQ 비활성화됨 + 진행 중이던 처리 완료됨 */
modify_shared_data(dev);
enable_irq(dev->irq);

/* disable_irq_nosync(): IRQ만 비활성화, 완료 대기 안 함 */
disable_irq_nosync(dev->irq);  /* hardirq에서 사용 가능 */
hardirq / thread_fn 동기화 패턴 hardirq handler spin_lock(&dev->lock) OK WRITE_ONCE(dev->status, val) OK mutex_lock() 금지! thread_fn spin_lock_irqsave(&dev->lock) OK mutex_lock(&dev->mutex) OK READ_ONCE(dev->status) OK 데이터 동기화 규칙 hardirq + thread_fn 공유: spin_lock_irqsave() 사용 (hardirq가 IRQ를 비활성화하므로) thread_fn + thread_fn 공유: mutex 사용 가능 (둘 다 프로세스 컨텍스트)

데드락 시나리오와 해결

데드락 위험: thread_fn에서 mutex를 보유한 상태에서 disable_irq()를 호출하고, hardirq handler가 같은 mutex를 시도하면 데드락이 발생합니다. hardirq handler에서는 mutex를 사용하지 마세요 (spin_lock만 가능).

디버깅(Debugging) 기법

threaded IRQ 관련 문제를 진단하는 실전 기법입니다.

/proc/interrupts 해석

$ cat /proc/interrupts
           CPU0       CPU1
  9:         45          0   IO-APIC   9-fasteoi   acpi
 16:      12847          0   IO-APIC  16-fasteoi   ahci[0000:00:1f.2]
 27:       8923          0   IO-APIC  27-fasteoi   i2c-touch

# 컬럼: IRQ번호 / CPU별 카운트 / 컨트롤러 / 트리거+타입 / 이름
# fasteoi: ACK 방식 (irq chip 의존)
# 카운트가 빠르게 증가하면 IRQ storm 의심

/proc/irq/N/ 디렉토리

$ ls /proc/irq/27/
affinity_hint  effective_affinity      smp_affinity
actions        effective_affinity_list smp_affinity_list
chip_name      hwirq                  spurious
node           type

$ cat /proc/irq/27/spurious
count 0
unhandled 0
last_unhandled 0 ms

$ cat /proc/irq/27/actions
i2c-touch

$ cat /proc/irq/27/type
edge

ftrace를 이용한 추적

# 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 스레드 추적 (available_events에서 확인)
$ cat /sys/kernel/debug/tracing/trace
# irq/27-i2c-to-207  [001] d..  123.456: irq_handler_entry: irq=27 name=i2c-touch
# irq/27-i2c-to-207  [001] d..  123.456: irq_handler_exit: irq=27 ret=handled

# function_graph로 상세 호출 추적
$ echo irq_thread_fn > /sys/kernel/debug/tracing/set_graph_function
$ echo function_graph > /sys/kernel/debug/tracing/current_tracer

일반적인 오류와 해결

증상원인해결
IRQ storm (카운트 폭증)Level-triggered + ONESHOT 미설정IRQF_ONESHOT 추가
"nobody cared" 메시지공유 IRQ에서 모든 handler가 IRQ_NONE 반환handler에서 자기 디바이스 인터럽트 확인 로직 점검
"irq N: nobody cared (try booting with irqpoll)"Spurious 인터럽트 100,000회 초과하드웨어 ACK 로직 확인, IRQ 라인 연결 점검
thread_fn이 실행되지 않음handler가 IRQ_HANDLED만 반환handler에서 IRQ_WAKE_THREAD 반환하도록 수정
-EINVAL 반환handler=NULL + ONESHOT 미설정IRQF_ONESHOT 추가 또는 handler 구현
시스템 응답 지연thread_fn 처리 시간이 너무 김thread_fn 최적화, 또는 작업을 workqueue로 추가 분할

성능 분석

Threaded IRQ의 오버헤드와 성능 특성을 정량적으로 분석합니다.

오버헤드 구성 요소

단계지연 시간 (일반)지연 시간 (RT)설명
하드웨어 인터럽트 전달~1us~1us인터럽트 컨트롤러 -> CPU
hardirq handler 실행1~5us1~5usACK + IRQ_WAKE_THREAD
스레드 웨이크업2~5us2~5uswake_up_process() + 스케줄링
컨텍스트 스위칭(Context Switching)1~3us1~3us현재 태스크(Task) -> irq 스레드
thread_fn 시작4~13us (총합)4~13us (총합)인터럽트 발생부터 thread_fn 진입까지
Threaded IRQ 지연 분석 파이프라인 HW IRQ ~1us hardirq 1~5us wake_up 2~5us ctx switch 1~3us thread_fn 드라이버 의존 총 오버헤드: 4~13us (thread_fn 시작까지) 성능 측정 도구 cyclictest: 인터럽트 지연 최대/평균/최소값 측정 (RT 워크로드) ftrace (irq_handler_entry/exit): 개별 핸들러 실행 시간 측정 perf stat -e irq:irq_handler_*: 통계적 인터럽트 처리 분석

cyclictest 성능 비교

# 비RT 커널 (threadirqs 미사용)
$ cyclictest -t4 -p 98 -i 1000 -l 100000
T: 0 Min:      1 Act:    2 Avg:    3 Max:      87

# 비RT 커널 (threadirqs 사용)
$ cyclictest -t4 -p 98 -i 1000 -l 100000
T: 0 Min:      1 Act:    2 Avg:    4 Max:      42

# PREEMPT_RT 커널
$ cyclictest -t4 -p 98 -i 1000 -l 100000
T: 0 Min:      1 Act:    1 Avg:    2 Max:      12
해석: threadirqs 활성화 시 평균 지연은 약간 증가하지만, 최악의 경우(Max) 지연이 크게 감소합니다. PREEMPT_RT에서는 최악의 경우 지연이 12us로, 결정론적인 실시간 응답을 보장합니다.

Threaded vs Non-threaded 오버헤드 비교

threaded IRQ와 기존 non-threaded 방식의 오버헤드를 시나리오별로 비교합니다:

시나리오Non-threaded (기존)Threaded IRQ차이 분석
MMIO 디바이스 (빠른 ACK)hardirq 1~3ushardirq 1us + 스레드 4~8us총 지연 증가. 그러나 인터럽트 비활성 구간이 1us로 감소
I2C 센서 (슬로우 버스)hardirq 100~500us (위험!)hardirq 1us + 스레드 100~500us인터럽트 비활성 구간 극적 감소. 다른 IRQ 응답성 향상
공유 IRQ (PCI INTx)handler 체인 순차 실행handler 체인 + thread_fn 병렬thread_fn들이 독립 실행. 느린 디바이스가 다른 디바이스 지연시키지 않음
높은 인터럽트 빈도 (10k/s+)softirq 처리 효율적스레드 웨이크업 오버헤드 누적처리량(Throughput) 위주라면 non-threaded가 유리. 지연 위주라면 threaded 유리
PREEMPT_RT 환경결정론적 지연 불가최악 지연 12us 이하실시간 요구사항에서는 threaded가 유일한 선택

perf를 이용한 인터럽트 처리 시간 측정

# 인터럽트 핸들러 실행 시간 통계 수집
$ perf stat -e irq:irq_handler_entry,irq:irq_handler_exit \
  -a sleep 10
Performance counter stats for 'system wide':
         12,847      irq:irq_handler_entry
         12,847      irq:irq_handler_exit
      10.001234 seconds time elapsed

# 특정 IRQ의 핸들러 실행 시간 히스토그램
$ perf record -e irq:irq_handler_entry -e irq:irq_handler_exit \
  -a --filter 'irq==27' sleep 5
$ perf script | head -20

# bpftrace로 thread_fn 실행 시간 측정
$ bpftrace -e '
kprobe:irq_thread_fn { @start[tid] = nsecs; }
kretprobe:irq_thread_fn /@start[tid]/ {
  @us = hist((nsecs - @start[tid]) / 1000);
  delete(@start[tid]);
}'

# ftrace로 개별 핸들러 지연 시간 추적
$ 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_pipe | grep "irq=27"
Threaded vs Non-threaded: 인터럽트 비활성 구간 비교 시간 Non-threaded (I2C 센서) 인터럽트 비활성 구간 (100~500us) hardirq에서 I2C 전송 + 데이터 처리 전부 수행 IRQ X 대기 IRQ Y 대기 IRQ Z 대기 Threaded IRQ (I2C 센서) ACK 1us irq/N-sensor 스레드 (100~500us, 선점 가능) 프로세스 컨텍스트: I2C 전송 + 데이터 처리 IRQ X 처리 IRQ Y 처리 IRQ Z 처리 핵심 차이 Non-threaded: 인터럽트 비활성 100~500us -> 다른 모든 IRQ가 지연됨 Threaded: 인터럽트 비활성 1us -> 다른 IRQ 즉시 응답, thread_fn은 선점 가능

커널 설정 옵션

Threaded IRQ와 관련된 주요 커널 설정 옵션들입니다.

설정기본값설명
CONFIG_IRQ_FORCED_THREADINGy (대부분 distro)force_irqthreads 기능 활성화 (threadirqs cmdline 지원)
CONFIG_PREEMPT_RTn완전한 RT 선점 모델, force_irqthreads 기본 활성화
CONFIG_PREEMPT_DYNAMICy (6.x)런타임에 선점 모델 전환 가능
CONFIG_GENERIC_IRQ_DEBUGFSn/sys/kernel/debug/irq/ 디버그 인터페이스
CONFIG_DEBUG_SHIRQn공유 IRQ 디버깅 (free_irq 시 가짜 인터럽트 발생)
CONFIG_PROVE_LOCKINGnlockdep: 잠금 의존성 검증 (IRQ 안전성 포함)

threadirqs 커널 cmdline 파라미터

# GRUB 설정에서 추가
GRUB_CMDLINE_LINUX="threadirqs"

# 또는 부팅 시 GRUB 편집 (임시)
linux /vmlinuz ... threadirqs

# 적용 확인
$ dmesg | grep -i thread
[    0.000000] Forcing IRQ threading

권장 설정 매트릭스

환경CONFIG_PREEMPT_RTthreadirqsIRQ 스레드 우선순위 조정비고
일반 서버nn불필요기본 설정, 처리량(Throughput) 우선
지연 민감 서버ny선택적최악의 지연 감소
임베디드 (비RT)ny권장I2C/SPI 디바이스 안정성
임베디드 (RT)y자동필수실시간 응답 보장
산업 제어y자동필수결정론적 응답 시간
오디오 워크스테이션y자동권장오디오 언더런 방지

내부 구현: __setup_irq() 핵심 경로

request_threaded_irq()가 호출되면 내부적으로 __setup_irq()가 실행됩니다. 이 함수의 threaded IRQ 관련 핵심 경로를 분석합니다.

/* kernel/irq/manage.c — request_threaded_irq() 핵심 흐름 */
int request_threaded_irq(unsigned int irq,
    irq_handler_t handler, irq_handler_t thread_fn,
    unsigned long irqflags, const char *devname, void *dev_id)
{
    struct irqaction *action;
    struct irq_desc *desc;

    desc = irq_to_desc(irq);
    if (!desc)
        return -EINVAL;

    /* handler와 thread_fn 둘 다 NULL이면 에러 */
    if (!handler && !thread_fn)
        return -EINVAL;

    /* handler=NULL이면 기본 핸들러 사용 */
    if (!handler) {
        if (!thread_fn)
            return -EINVAL;
        handler = irq_default_primary_handler;
    }

    /* irqaction 구조체 할당 및 초기화 */
    action = kzalloc(sizeof(*action), GFP_KERNEL);
    action->handler = handler;
    action->thread_fn = thread_fn;
    action->flags = irqflags;
    action->name = devname;
    action->dev_id = dev_id;

    /* 핵심: __setup_irq()에서 스레드 생성, 검증, 등록 */
    return __setup_irq(irq, desc, action);
}
request_threaded_irq() 내부 흐름 파라미터 검증 handler/thread_fn NULL 체크 irqaction 할당 kzalloc + 초기화 __setup_irq() 핵심 등록 로직 __setup_irq() 내부 단계 1. force_threading 확인 irq_setup_forced_threading() 2. 스레드 생성 kthread_create(irq_thread) 3. RT 우선순위 설정 sched_set_fifo() 4. SHARED 호환 검증 플래그/트리거 일치 확인 5. action 체인 추가 desc->action 리스트에 연결 6. 스레드 시작 wake_up_process(thread) force_irqthreads 활성 시: handler -> thread_fn 이동, ONESHOT 자동 설정

핵심 자료 구조

Threaded IRQ를 이해하려면 관련 자료 구조의 관계를 파악해야 합니다.

/* include/linux/irqdesc.h */
struct irq_desc {
    struct irqaction    *action;        /* 핸들러 체인 (linked list) */
    struct irq_data      irq_data;       /* IRQ chip, hwirq 등 */
    unsigned int         irq_count;      /* 인터럽트 발생 횟수 */
    unsigned int         depth;          /* enable/disable 중첩 카운트 */
    unsigned int         threads_handled; /* 스레드 처리 완료 횟수 */
    unsigned int         threads_active;  /* 현재 활성 스레드 수 */
    wait_queue_head_t    wait_for_threads; /* synchronize_irq() 대기 */
    /* ... */
};

/* include/linux/interrupt.h */
struct irqaction {
    irq_handler_t       handler;        /* hardirq 핸들러 */
    irq_handler_t       thread_fn;      /* 스레드 함수 */
    struct task_struct *thread;          /* 커널 스레드 task */
    unsigned long       thread_flags;    /* IRQTF_RUNTHREAD 등 */
    unsigned long       thread_mask;     /* ONESHOT sync 마스크 */
    void               *dev_id;         /* 디바이스 식별자 */
    unsigned int        irq;            /* IRQ 번호 */
    unsigned int        flags;          /* IRQF_* 플래그 */
    const char         *name;           /* 이름 */
    struct irqaction   *next;           /* 공유 IRQ: 다음 action */
};
IRQ 자료 구조 관계도 irq_desc action -> (chain) irq_data depth, threads_active wait_for_threads irqaction handler (hardirq) thread_fn thread (task_struct *) thread_flags dev_id, flags, name next -> (shared chain) task_struct irq/N-name SCHED_FIFO, prio=50 irq_thread() 메인루프 action thread irqaction (공유) handler2, thread_fn2 thread2, dev_id2 next -> NULL next

ONESHOT 구현 상세: irq_finalize_oneshot()

ONESHOT 인터럽트의 unmask 타이밍은 irq_finalize_oneshot() 함수가 제어합니다. 공유 IRQ에서 여러 thread_fn이 모두 완료되어야 unmask하는 로직을 분석합니다.

/* kernel/irq/manage.c */
static void irq_finalize_oneshot(struct irq_desc *desc,
                                  struct irqaction *action)
{
    if (!(desc->istate & IRQS_ONESHOT) ||
        action->handler == irq_forced_secondary_handler)
        return;

again:
    chip_bus_lock(desc);
    raw_spin_lock_irq(&desc->lock);

    /* thread_mask에서 자신의 비트 클리어 */
    if (!test_and_clear_bit(action->thread_mask,
                            &desc->threads_oneshot)) {
        raw_spin_unlock_irq(&desc->lock);
        chip_bus_sync_unlock(desc);
        goto again;
    }

    /* 모든 thread_fn이 완료되었는지 확인 */
    if (desc->threads_oneshot == 0) {
        /* 모두 완료: unmask! */
        desc->istate &= ~IRQS_MASKED;
        irq_unmask(desc);
    }

    raw_spin_unlock_irq(&desc->lock);
    chip_bus_sync_unlock(desc);
}
threads_oneshot 비트마스크: 공유 IRQ에서 각 action은 고유한 비트를 할당받습니다. hardirq handler가 IRQ_WAKE_THREAD를 반환하면 해당 비트가 설정되고, thread_fn이 완료되면 클리어됩니다. 모든 비트가 클리어될 때만 unmask됩니다.

에러 처리 패턴

Threaded IRQ에서 발생할 수 있는 에러 상황과 올바른 처리 패턴을 정리합니다.

thread_fn에서의 에러 처리

static irqreturn_t robust_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    int ret;

    /* I2C/SPI 통신 에러 처리 */
    ret = regmap_read(dev->regmap, REG_STATUS, &dev->status);
    if (ret) {
        dev_err(dev->dev, "IRQ: 상태 읽기 실패: %d\n", ret);
        /* 에러여도 IRQ_HANDLED 반환 (IRQ_NONE이면 spurious 카운트 증가) */
        return IRQ_HANDLED;
    }

    /* 데이터 처리 */
    if (dev->status & ERROR_BIT) {
        dev_warn(dev->dev, "디바이스 에러 감지: 0x%x\n", dev->status);
        my_device_reset(dev);
    } else {
        process_normal_data(dev);
    }

    return IRQ_HANDLED;
}

/* 안전한 서스펜드/리줌 패턴 */
static int my_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    /* IRQ 비활성화 + 진행 중인 thread_fn 완료 대기 */
    disable_irq(mydev->irq);
    /* 이 시점에서 thread_fn이 확실히 실행 중이지 않음 */
    save_device_state(mydev);
    return 0;
}

static int my_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    restore_device_state(mydev);
    enable_irq(mydev->irq);
    return 0;
}

IRQ Affinity와 스레드 Affinity

Threaded IRQ에서 IRQ affinity를 설정하면 스레드의 CPU affinity도 자동으로 동기화됩니다.

# IRQ 27을 CPU 2에 바인딩
$ echo 4 > /proc/irq/27/smp_affinity

# 확인: IRQ 스레드도 CPU 2로 이동
$ taskset -p $(pgrep "irq/27")
pid 207's current affinity mask: 4

# managed_irq: 디바이스 드라이버가 affinity를 관리
# irq_set_affinity_hint()로 힌트 제공
# 커널이 자동으로 최적 배치

NUMA 최적화

/* PCIe 디바이스의 NUMA 노드에 IRQ 바인딩 */
static int setup_numa_irq(struct pci_dev *pdev)
{
    int node = dev_to_node(&pdev->dev);
    const struct cpumask *mask;

    if (node != NUMA_NO_NODE) {
        mask = cpumask_of_node(node);
        irq_set_affinity_hint(pdev->irq, mask);
    }

    return 0;
}

Managed IRQ와 자동 affinity

최신 멀티큐 디바이스(NVMe, 네트워크 카드)는 managed IRQ를 사용하여 커널이 자동으로 IRQ를 CPU에 최적 배치합니다:

/* managed IRQ: 커널이 affinity를 자동 관리 */
struct irq_affinity affd = {
    .pre_vectors  = 1,    /* admin queue */
    .post_vectors = 1,    /* polling queue */
};

/* pci_alloc_irq_vectors_affinity()로 벡터 할당 시
 * IRQ_NO_BALANCING + managed 속성이 자동 설정됨 */
ret = pci_alloc_irq_vectors_affinity(pdev,
    min_vecs, max_vecs,
    PCI_IRQ_MSIX | PCI_IRQ_AFFINITY,
    &affd);

/* 결과: 각 벡터가 자동으로 다른 CPU에 배치됨
 * IRQ N → CPU 0, IRQ N+1 → CPU 1, ...
 * 스레드 affinity도 자동으로 동기화
 * /proc/irq/N/effective_affinity에서 확인 가능 */

Affinity 변경 시 스레드 마이그레이션

IRQ affinity가 변경되면, IRQ 스레드도 자동으로 해당 CPU로 마이그레이션됩니다. 이 과정은 irq_thread_check_affinity()에서 처리됩니다:

# IRQ affinity 변경 과정 관찰
$ echo 4 > /proc/irq/27/smp_affinity   # CPU 2로 변경
$ cat /proc/irq/27/effective_affinity   # 실제 적용된 affinity
4

# 스레드 마이그레이션 확인
$ taskset -p $(pgrep "irq/27")
pid 207's current affinity mask: 4      # CPU 2로 이동 완료

# irq_set_affinity_notifier()로 변경 알림 수신 가능
# 드라이버가 affinity 변경에 대응해야 할 때 사용
managed vs 수동 affinity: managed IRQ(PCI_IRQ_AFFINITY)는 사용자가 /proc/irq/N/smp_affinity를 수동 변경할 수 없습니다. 커널이 CPU 온/오프라인 이벤트에 따라 자동으로 재배치(Relocation)합니다. 수동 affinity가 필요하면 managed 옵션 없이 벡터를 할당해야 합니다.

웨이크업 인터럽트와 Threaded IRQ

시스템 서스펜드 상태에서 특정 인터럽트가 시스템을 깨울 수 있어야 하는 경우, 웨이크업 인터럽트로 설정합니다.

/* 웨이크업 가능한 threaded IRQ 설정 */
static int wakeup_probe(struct platform_device *pdev)
{
    int irq = platform_get_irq(pdev, 0);

    /* 웨이크업 소스로 등록 */
    device_init_wakeup(&pdev->dev, true);

    devm_request_threaded_irq(&pdev->dev, irq,
        NULL, wakeup_thread_fn,
        IRQF_ONESHOT | IRQF_TRIGGER_LOW | IRQF_NO_SUSPEND,
        "wakeup-key", pdev);

    /* 서스펜드 시 IRQ를 웨이크업 소스로 설정 */
    dev_pm_set_wake_irq(&pdev->dev, irq);

    return 0;
}

/* 서스펜드 콜백에서 웨이크업 활성화 */
static int wakeup_suspend(struct device *dev)
{
    if (device_may_wakeup(dev))
        enable_irq_wake(dev->irq);
    return 0;
}

static int wakeup_resume(struct device *dev)
{
    if (device_may_wakeup(dev))
        disable_irq_wake(dev->irq);
    return 0;
}
웨이크업 인터럽트 서스펜드/리줌 시퀀스 시간 ▼ 정상 동작 중 IRQ 활성, thread_fn 실행 가능 서스펜드 진입 (pm_suspend) 1. suspend 콜백: disable_irq() + save_state() 진행 중인 thread_fn 완료 대기 후 IRQ 비활성화 2. enable_irq_wake(irq) 웨이크업 소스로 설정 (인터럽트 컨트롤러에 전달) 시스템 서스펜드 상태 (S3/S2idle) CPU 정지, 웨이크업 소스 IRQ만 활성 (IRQF_NO_SUSPEND) 웨이크업 이벤트 발생! 리줌 시작 3. disable_irq_wake(irq) 4. restore_state() + enable_irq() 필수 API device_init_wakeup(dev, true) dev_pm_set_wake_irq(dev, irq) IRQF_NO_SUSPEND (서스펜드 시에도 활성) 주의사항 IRQF_NO_SUSPEND + IRQF_ONESHOT 조합 시 thread_fn이 서스펜드 중 실행 불가 → 주의

Secondary Actions (이중 핸들러)

커널은 내부적으로 force_irqthreads 환경에서 기존 handler를 "primary" thread와 "secondary" thread로 분리할 수 있습니다. 이는 handler가 원래 hardirq와 softirq 양쪽 모두에서 작업을 수행하던 경우에 발생합니다.

/* kernel/irq/manage.c — secondary action 생성 */
if (new->handler && new->thread_fn) {
    /* handler: 원래 hardirq 부분 유지
     * thread_fn: 원래 softirq/bottom-half 부분 */

    if (force_irqthreads && new->handler != irq_default_primary_handler) {
        /* force threading 시:
         * handler -> primary thread로 이동
         * thread_fn -> secondary thread 생성 */
        struct irqaction *secondary;
        secondary = kzalloc(sizeof(*secondary), GFP_KERNEL);
        secondary->handler = irq_forced_secondary_handler;
        secondary->thread_fn = new->thread_fn;
        new->secondary = secondary;
    }
}
force_irqthreads에서의 Secondary Actions 생성 request_threaded_irq(irq, my_handler, my_thread_fn, ...) 일반 커널 (force_irqthreads=false) my_handler (hardirq) WAKE my_thread_fn (스레드) 스레드 1개: irq/N-name RT 커널 (force_irqthreads=true) irq_default_primary_handler (hardirq) WAKE Primary Thread my_handler (원래 hardirq → 스레드화) WAKE Secondary Thread my_thread_fn (원래 thread_fn 유지) 스레드 2개: primary + secondary
Secondary Actions 발생 조건: Secondary actions는 request_threaded_irq()에서 handler와 thread_fn을 둘 다 제공한 경우에만 발생합니다. handler=NULL이거나, handler가 이미 irq_default_primary_handler인 경우에는 Secondary actions가 생성되지 않습니다. 일반 커널에서는 전혀 사용되지 않으며, force_irqthreads 환경에서만 내부적으로 자동 생성됩니다.

패턴 정리: Threaded IRQ 사용 가이드

드라이버 개발자를 위한 Threaded IRQ 패턴을 유형별로 정리합니다.

디바이스 유형handlerthread_fn플래그예시
I2C/SPI 센서NULL데이터 읽기ONESHOT | TRIGGER_*BMP280, BME680
I2C 터치스크린NULL좌표 읽기 + 입력 보고ONESHOT | TRIGGER_LOWFT5x06, Goodix
PMIC (regmap)regmap_irqregmap_irqONESHOT | TRIGGER_LOWMAX77686, AXP20x
PCIe NIC상태 확인 + ACK패킷(Packet) 처리(MSI: 0)네트워크 카드
PCIe INTx (공유)상태 확인 필수데이터 처리SHARED | ONESHOT레거시 PCI
GPIO 버튼NULL디바운스 + 입력ONESHOT | TRIGGER_*gpio-keys
GPIO expander 자식nestednested(부모가 관리)PCA953x
DMA 컨트롤러완료 ACK버퍼(Buffer) 처리 + 콜백0 또는 ONESHOTDMA engine
경험 법칙:
  • 슬로우 버스(I2C/SPI) 디바이스: handler=NULL, IRQF_ONESHOT 필수
  • MMIO 디바이스: handler에서 빠른 ACK, thread_fn에서 나머지
  • MSI/MSI-X: ONESHOT 선택적 (edge-triggered)
  • 공유 INTx: handler 필수, IRQF_SHARED
  • 의심스러우면 IRQF_ONESHOT을 추가하세요. 안전합니다.

흔한 실수와 안티패턴

#실수결과해결
1handler=NULL + ONESHOT 미설정-EINVAL 반환, 등록 실패IRQF_ONESHOT 추가
2Level-triggered + ONESHOT 미설정IRQ storm, 시스템 마비IRQF_ONESHOT 추가
3thread_fn에서 IRQ_NONE 반환spurious 카운트 증가, IRQ 비활성화 위험IRQ_HANDLED 반환
4IRQF_SHARED + handler=NULL자기 디바이스 인터럽트 확인 불가handler 구현 필수
5hardirq handler에서 mutex 사용스케줄링 불가 컨텍스트에서 슬립 시도spin_lock 사용 또는 thread_fn으로 이동
6free_irq() 없이 드라이버 언로드dangling 핸들러, 커널 패닉(Kernel Panic)devm API 사용
7thread_fn에서 disable_irq() 호출자기 자신의 thread_fn 완료를 대기하며 데드락disable_irq_nosync() 사용
8IRQF_NO_AUTOEN 후 enable_irq() 누락인터럽트가 영원히 비활성화초기화 완료 후 enable_irq() 호출

API 레퍼런스 요약

함수설명컨텍스트
request_threaded_irq()threaded IRQ 등록프로세스
devm_request_threaded_irq()managed threaded IRQ 등록프로세스
free_irq()IRQ 해제 (스레드 종료 대기)프로세스
devm_free_irq()managed IRQ 수동 해제프로세스
synchronize_irq()진행 중인 handler/thread_fn 완료 대기프로세스
disable_irq()IRQ 비활성화 + 완료 대기프로세스
disable_irq_nosync()IRQ 비활성화 (완료 대기 안 함)모든 컨텍스트
enable_irq()IRQ 활성화모든 컨텍스트
enable_irq_wake()웨이크업 소스로 설정프로세스
disable_irq_wake()웨이크업 소스 해제프로세스
irq_set_affinity_hint()IRQ affinity 힌트 설정프로세스
handle_nested_irq()nested IRQ 처리 (부모 스레드에서)스레드
irq_set_nested_thread()IRQ를 nested 모드로 설정프로세스

실제 커널 드라이버 사례 분석

실제 리눅스 커널에서 threaded IRQ를 사용하는 대표적인 드라이버들을 분석합니다.

PMIC: MAX77686 (drivers/mfd/max77686-irq.c)

삼성 Exynos 플랫폼의 PMIC으로, I2C 연결입니다. regmap_irq를 사용하여 다수의 인터럽트 소스를 관리합니다:

/* 간략화된 MAX77686 IRQ 설정 */
ret = regmap_add_irq_chip(max77686->regmap, max77686->irq,
                           IRQF_ONESHOT | IRQF_TRIGGER_LOW |
                           IRQF_SHARED,
                           0, &max77686_irq_chip,
                           &max77686->irq_data);
/* 결과: I2C를 통한 모든 인터럽트 상태 읽기/마스킹이
 * threaded handler에서 자동으로 처리됨 */

터치 컨트롤러: Goodix (drivers/input/touchscreen/goodix_ts_core.c)

/* Goodix 터치 컨트롤러: handler=NULL 패턴 */
ret = devm_request_threaded_irq(&ts->client->dev,
    ts->client->irq, NULL, goodix_ts_irq_handler,
    IRQF_ONESHOT, "goodix_ts", ts);

/* thread_fn에서 I2C로 터치 데이터 읽기 + 입력 이벤트 보고 */

NVMe (drivers/nvme/host/pci.c)

NVMe는 MSI-X를 사용하지만, 특정 경우 threaded IRQ 패턴을 활용합니다:

/* NVMe: 컴플리션 큐 처리를 위한 threaded IRQ */
ret = pci_request_irq(pdev, nr,
    nvme_irq,          /* hardirq: CQ doorbell 확인 */
    nvme_irq_check,    /* thread: 컴플리션 큐 처리 */
    nvmeq, "nvme%dq%d", ...);

GPIO Keys (drivers/input/keyboard/gpio_keys.c)

GPIO Keys 드라이버는 GPIO 기반 버튼 입력을 처리하는 대표적인 threaded IRQ 사용 사례입니다. 디바운싱과 웨이크업 기능을 모두 지원합니다:

/* GPIO Keys: 하드웨어 디바운스 미지원 시 소프트웨어 디바운스 + threaded IRQ */
static irqreturn_t gpio_keys_irq_isr(int irq, void *dev_id)
{
    struct gpio_button_data *bdata = dev_id;
    struct input_dev *input = bdata->input;

    BUG_ON(irq != bdata->irq);

    spin_lock_irqsave(&bdata->lock, flags);

    if (!bdata->key_pressed) {
        if (bdata->button->wakeup)
            pm_wakeup_event(bdata->input->dev.parent, 0);

        input_event(input, EV_KEY, *bdata->code, 1);
        input_sync(input);

        if (!bdata->release_delay) {
            input_event(input, EV_KEY, *bdata->code, 0);
            input_sync(input);
        }
    }

    spin_unlock_irqrestore(&bdata->lock, flags);
    return IRQ_HANDLED;
}

/* 등록: ONESHOT + 트리거 타입 조합 */
ret = request_any_context_irq(bdata->irq,
    gpio_keys_irq_isr, irqflags, desc, bdata);
/* request_any_context_irq(): hardirq/threaded 자동 선택
 * GPIO가 I2C expander 뒤에 있으면 → nested (threaded)
 * GPIO가 SoC 직접 연결이면 → hardirq
 * 반환값으로 어떤 컨텍스트가 선택되었는지 알 수 있음 */

IIO (Industrial I/O) 센서 트리거

IIO 프레임워크의 센서 드라이버는 데이터 준비 인터럽트를 threaded IRQ로 처리하여 고속 센서 데이터를 수집합니다:

/* IIO 센서: data-ready 인터럽트 → threaded IRQ → 버퍼 push */
static irqreturn_t bmp280_trigger_handler(int irq, void *p)
{
    struct iio_poll_func *pf = p;
    struct iio_dev *indio_dev = pf->indio_dev;
    struct bmp280_data *data = iio_priv(indio_dev);
    s32 adc_temp, adc_press;

    /* I2C/SPI로 센서 데이터 읽기 (프로세스 컨텍스트에서 슬립 가능) */
    mutex_lock(&data->lock);
    regmap_bulk_read(data->regmap, BMP280_REG_PRESS_MSB,
                     data->buf, BMP280_DATA_LEN);
    mutex_unlock(&data->lock);

    /* IIO 버퍼에 push */
    iio_push_to_buffers_with_timestamp(indio_dev,
                                        data->buf, pf->timestamp);
    iio_trigger_notify_done(indio_dev->trig);
    return IRQ_HANDLED;
}

/* IIO 트리거 + threaded IRQ 등록 */
ret = devm_iio_triggered_buffer_setup(&client->dev, indio_dev,
    iio_pollfunc_store_time,   /* hardirq: 타임스탬프만 저장 */
    bmp280_trigger_handler,    /* thread_fn: 데이터 읽기 + push */
    NULL);

request_any_context_irq() — 자동 컨텍스트 선택

일부 드라이버는 GPIO가 SoC에 직접 연결되었는지, I2C GPIO expander 뒤에 있는지에 따라 hardirq 또는 threaded 모드가 결정됩니다. request_any_context_irq()는 이를 자동으로 판단합니다:

/* include/linux/interrupt.h */
extern int request_any_context_irq(
    unsigned int irq,
    irq_handler_t handler,
    unsigned long flags,
    const char *name,
    void *dev_id);

/* 반환값:
 *   IRQC_IS_HARDIRQ  — hardirq 컨텍스트로 등록됨
 *   IRQC_IS_NESTED   — nested threaded로 등록됨 (I2C/SPI 뒤)
 *   음수             — 에러
 *
 * 내부적으로 irq_settings_is_nested_thread(desc)를 확인하여
 * nested이면 자동으로 threaded IRQ로 등록 */

debugfs 인터페이스

CONFIG_GENERIC_IRQ_DEBUGFS=y를 활성화하면 /sys/kernel/debug/irq/에 상세한 IRQ 정보가 제공됩니다.

# debugfs IRQ 정보 확인
$ ls /sys/kernel/debug/irq/irqs/
0  1  2  3  4  5  6  7  8  9  10  ...

$ cat /sys/kernel/debug/irq/irqs/27
handler:  0xffffffff81234560 [my_hardirq]
thread_fn:  0xffffffff81234580 [my_thread_fn]
flags:    0x00002080 (SHARED|ONESHOT)
thread:   irq/27-my-dev (pid 207)
type:     level-low
chip:     GICv3
hwirq:    47
domain:   /interrupt-controller@...
affinity: 0-3
effective: 2

# IRQ 도메인 트리 확인
$ cat /sys/kernel/debug/irq/domains/list

IRQ 디버깅 워크플로우

Threaded IRQ 문제 진단 워크플로우 Threaded IRQ 문제 발생 IRQ storm (카운트 폭증) 1. cat /proc/interrupts (카운트 확인) 2. IRQF_ONESHOT 추가 → Level-triggered + ONESHOT 미설정이 원인 thread_fn 미실행 1. handler 반환값 확인 (ftrace) 2. IRQ_HANDLED만 반환하고 있지 않은지 3. ps | grep irq/ (스레드 존재 확인) → IRQ_WAKE_THREAD 반환 필요 등록 실패 (-EINVAL) 1. handler=NULL + ONESHOT 미설정? 2. handler=NULL + thread_fn=NULL? → IRQF_ONESHOT 추가 또는 handler 구현 주요 진단 도구 및 명령어 실시간 모니터링: watch -n1 cat /proc/interrupts | grep IRQ_NAME ftrace 추적: echo irq_thread_fn > set_graph_function && echo function_graph > current_tracer 스레드 상태: ps -eo pid,cls,rtprio,stat,wchan,comm | grep irq/ spurious 확인: cat /proc/irq/N/spurious (unhandled 카운트 증가 시 문제) perf 분석: perf stat -e irq:irq_handler_entry,irq:irq_handler_exit -a sleep 10

실전 디버깅: IRQ Storm 복구

# 1단계: IRQ storm 감지
$ watch -n1 'cat /proc/interrupts | head -5'
# 특정 IRQ의 카운트가 초당 수천~수만 씩 증가하면 storm

# 2단계: 해당 IRQ 임시 비활성화 (긴급)
$ echo 1 > /proc/irq/27/disable   # 주의: 관련 디바이스 동작 중단

# 3단계: IRQ 정보 수집
$ cat /proc/irq/27/type           # edge vs level 확인
$ cat /proc/irq/27/actions        # 어떤 드라이버인지 확인
$ cat /proc/irq/27/spurious       # unhandled 카운트 확인

# 4단계: dmesg에서 관련 메시지 확인
$ dmesg | grep -i "irq 27\|nobody cared\|irq.*disabled"

# 5단계: 드라이버 수정 후 재활성화
$ echo 0 > /proc/irq/27/disable   # 또는 드라이버 reload

/proc/interrupts와 /proc/irq/N/ 심층 분석

인터럽트 디버깅에서 가장 기본이 되는 /proc/interrupts/proc/irq/N/ 디렉토리의 각 항목을 상세히 해석합니다.

/proc/interrupts 상세 해석

$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
  0:         23          0          0          0   IO-APIC   2-edge      timer
  1:       1247          0          0          0   IO-APIC   1-edge      i8042
  8:          1          0          0          0   IO-APIC   8-edge      rtc0
  9:         45          0          0          0   IO-APIC   9-fasteoi   acpi
 16:      12847          0          0          0   IO-APIC  16-fasteoi   ahci[0000:00:1f.2]
 27:       8923          0          0          0   IO-APIC  27-fasteoi   i2c-touch
 40:          0          0      45678          0   PCI-MSI  524288-edge  nvme0q0
 41:          0          0          0      23456   PCI-MSI  524289-edge  nvme0q1
NMI:        234        231        229        228   Non-maskable interrupts
LOC:     987654     876543     765432     654321   Local timer interrupts
RES:      12345      11234      10123       9012   Rescheduling interrupts
ERR:          0
MIS:          0

# 필드 해석:
#   IRQ 번호 | CPU별 인터럽트 발생 횟수 | 인터럽트 칩 | hwirq-트리거타입 | 디바이스명
#
# 트리거 타입 해석:
#   edge     — 에지 트리거 (상승/하강)
#   fasteoi  — EOI(End Of Interrupt) 기반 ACK (level-triggered에 주로 사용)
#   level    — 레벨 트리거
#
# 비정상 징후:
#   - 특정 IRQ 카운트가 초당 수천~수만: IRQ storm 의심
#   - unbalanced: 한 CPU에만 모든 카운트 집중 (affinity 확인)
#   - ERR/MIS 증가: 하드웨어 문제 의심

/proc/irq/N/ 디렉토리 상세

# /proc/irq/27/ 전체 항목 해석
$ ls -la /proc/irq/27/
total 0
dr-xr-xr-x 2 root root 0  i2c-touch/   # action 디렉토리
-r--r--r-- 1 root root 0  actions        # 등록된 핸들러 이름
-r--r--r-- 1 root root 0  affinity_hint  # 드라이버가 설정한 힌트
-r--r--r-- 1 root root 0  chip_name      # IRQ 칩 이름
-r--r--r-- 1 root root 0  effective_affinity      # 실제 적용된 affinity
-r--r--r-- 1 root root 0  effective_affinity_list  # 리스트 형식
-r--r--r-- 1 root root 0  hwirq          # 하드웨어 IRQ 번호
-r--r--r-- 1 root root 0  node           # NUMA 노드
-rw-r--r-- 1 root root 0  smp_affinity        # CPU 비트마스크
-rw-r--r-- 1 root root 0  smp_affinity_list   # CPU 리스트
-r--r--r-- 1 root root 0  spurious       # 스퓨리어스 IRQ 정보
-r--r--r-- 1 root root 0  type           # 트리거 유형

# 각 항목 상세 확인
$ cat /proc/irq/27/actions
i2c-touch                    # 등록된 디바이스 이름

$ cat /proc/irq/27/chip_name
IO-APIC                      # 인터럽트 컨트롤러 칩

$ cat /proc/irq/27/hwirq
27                           # 하드웨어 IRQ 번호 (virq와 다를 수 있음)

$ cat /proc/irq/27/type
level                        # level 또는 edge

$ cat /proc/irq/27/spurious
count 8923                   # 총 인터럽트 횟수
unhandled 0                  # 핸들러가 처리하지 않은 횟수
last_unhandled 0 ms          # 마지막 미처리 시점
# unhandled/count 비율이 99.9%를 넘으면 IRQ 자동 비활성화!

$ cat /proc/irq/27/effective_affinity
4                            # CPU 2 (비트마스크)

$ cat /proc/irq/27/effective_affinity_list
2                            # CPU 2 (리스트 형식)

# affinity 변경
$ echo 4 > /proc/irq/27/smp_affinity   # CPU 2에 고정
$ echo 0-3 > /proc/irq/27/smp_affinity_list  # CPU 0~3 허용

인터럽트 모니터링 스크립트

threaded IRQ 관련 문제를 실시간으로 모니터링하는 실전 스크립트를 소개합니다:

# 1. 특정 IRQ의 초당 발생 횟수 추적
$ while true; do
    prev=$(awk '/27:/ {print $2+$3+$4+$5}' /proc/interrupts)
    sleep 1
    curr=$(awk '/27:/ {print $2+$3+$4+$5}' /proc/interrupts)
    echo "IRQ 27: $((curr - prev))/sec"
  done

# 2. IRQ 스레드 상태와 CPU 사용률 확인
$ ps -eo pid,cls,rtprio,psr,%cpu,stat,wchan:20,comm | grep "irq/"
  207  FF    50   2  0.3 S    irq_wait_for_inter irq/27-i2c-touch
  341  FF    50   0  1.2 S    irq_wait_for_inter irq/136-xhci_hcd
# stat 컬럼: S=슬립(대기), R=실행 중, D=IO 대기
# wchan: irq_wait_for_interrupt면 정상 대기 상태

# 3. thread_fn이 실행 중인지 확인 (R 상태 빈도)
$ for i in $(seq 1 100); do
    ps -eo stat,comm | grep "irq/27" | head -1
    usleep 10000
  done | sort | uniq -c
# R(running)이 자주 나타나면 thread_fn 실행 빈도가 높음

# 4. 스퓨리어스 IRQ 실시간 감시
$ watch -n5 'for d in /proc/irq/*/spurious; do
    irq=$(echo $d | cut -d/ -f4)
    unhandled=$(grep unhandled $d | awk "{print \$2}")
    [ "$unhandled" -gt 0 ] && echo "IRQ $irq: unhandled=$unhandled"
  done'

실전 버그와 함정 (Bugs and Pitfalls)

실제 커널 드라이버 개발에서 발생하는 threaded IRQ 관련 버그들을 구체적인 코드와 함께 분석합니다. 각 버그는 실제 커널 메일링 리스트나 버그 리포트에서 보고된 패턴을 기반으로 합니다.

버그 1: thread_fn에서 disable_irq() 데드락

/* 잘못된 코드: 데드락 발생! */
static irqreturn_t bad_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* BUG: disable_irq()는 실행 중인 thread_fn 완료를 대기합니다.
     * 그런데 지금 이 코드가 바로 그 thread_fn입니다!
     * -> 자기 자신의 완료를 기다리므로 영원히 블록됩니다. */
    disable_irq(dev->irq);  /* DEADLOCK! */

    reconfigure_device(dev);

    enable_irq(dev->irq);
    return IRQ_HANDLED;
}

/* 올바른 코드: disable_irq_nosync() 사용 */
static irqreturn_t good_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* disable_irq_nosync()는 완료를 대기하지 않습니다.
     * thread_fn 내부에서 안전하게 사용할 수 있습니다. */
    disable_irq_nosync(dev->irq);

    reconfigure_device(dev);

    enable_irq(dev->irq);
    return IRQ_HANDLED;
}

버그 2: hardirq와 thread_fn 간 경쟁 조건(Race Condition)

/* 잘못된 코드: 경쟁 조건 발생 */
static irqreturn_t bad_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* BUG: dev->status를 보호 없이 쓰기 */
    dev->status = ioread32(dev->regs + STATUS);
    return IRQ_WAKE_THREAD;
}

static irqreturn_t bad_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* BUG: ONESHOT 미설정 + 새 인터럽트 발생 시
     * hardirq가 dev->status를 덮어쓸 수 있습니다!
     * thread_fn이 이전 status를 처리하는 도중 값이 변경됩니다. */
    process_data(dev->status);
    return IRQ_HANDLED;
}

/* 올바른 코드: WRITE_ONCE/READ_ONCE 사용 + ONESHOT */
static irqreturn_t good_hardirq(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;

    /* WRITE_ONCE로 원자적 쓰기 보장 */
    WRITE_ONCE(dev->status, ioread32(dev->regs + STATUS));
    return IRQ_WAKE_THREAD;
}

static irqreturn_t good_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = READ_ONCE(dev->status);

    process_data(status);
    return IRQ_HANDLED;
}
/* + IRQF_ONESHOT으로 등록하여 근본적으로 경쟁 방지 */

버그 3: thread_fn에서 IRQ_NONE 반환

/* 잘못된 코드: thread_fn에서 IRQ_NONE 반환 */
static irqreturn_t bad_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    int ret;

    ret = i2c_smbus_read_byte_data(dev->client, REG_STATUS);
    if (ret < 0) {
        /* BUG: IRQ_NONE을 반환하면 커널이 이 인터럽트를
         * "처리되지 않음"으로 기록합니다.
         * 이것이 누적되면 (100,000회) 커널이 해당 IRQ를
         * 자동으로 비활성화하고 "nobody cared" 메시지를 출력합니다! */
        return IRQ_NONE;  /* 위험! */
    }

    process_data(dev, ret);
    return IRQ_HANDLED;
}

/* 올바른 코드: 에러여도 IRQ_HANDLED 반환 */
static irqreturn_t good_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    int ret;

    ret = i2c_smbus_read_byte_data(dev->client, REG_STATUS);
    if (ret < 0) {
        dev_err_ratelimited(&dev->client->dev,
            "IRQ: status read failed: %d\n", ret);
        /* thread_fn은 이미 "자기 디바이스의 인터럽트"라고 확인된 후
         * 호출됩니다. I/O 에러가 발생해도 IRQ_HANDLED를 반환해야
         * spurious 카운트가 증가하지 않습니다. */
        return IRQ_HANDLED;
    }

    process_data(dev, ret);
    return IRQ_HANDLED;
}
IRQ_NONE vs IRQ_HANDLED 규칙: IRQ_NONE오직 hardirq handler에서만, "이 인터럽트는 내 디바이스의 것이 아니다"를 의미할 때 사용해야 합니다(공유 IRQ). thread_fn은 이미 hardirq가 "내 디바이스의 인터럽트"라고 판단한 후 호출되므로, 항상 IRQ_HANDLED를 반환해야 합니다.

버그 4: 서스펜드/리줌 시 IRQ 레이스

/* 잘못된 코드: 서스펜드 시 thread_fn과의 레이스 */
static int bad_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    /* BUG: disable_irq() 없이 하드웨어 상태를 변경하면
     * thread_fn이 동시에 실행 중일 수 있습니다.
     * thread_fn이 레지스터에 접근하는 도중 디바이스가
     * 파워 다운되면 bus error가 발생합니다! */
    power_down_device(mydev);
    return 0;
}

/* 올바른 코드: disable_irq로 thread_fn 완료 보장 */
static int good_suspend(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    /* disable_irq()는 다음을 보장합니다:
     * 1. 새 인터럽트가 발생하지 않음
     * 2. 실행 중인 thread_fn이 완료될 때까지 대기
     * 이후 안전하게 하드웨어 상태를 변경할 수 있습니다. */
    disable_irq(mydev->irq);
    power_down_device(mydev);
    return 0;
}

static int good_resume(struct device *dev)
{
    struct my_device *mydev = dev_get_drvdata(dev);

    power_up_device(mydev);
    reinit_device(mydev);
    enable_irq(mydev->irq);  /* 디바이스 준비 후 IRQ 재활성화 */
    return 0;
}

버그 5: devm 리소스 등록 순서 오류

/* 잘못된 코드: IRQ를 나중에 등록 -> MMIO 먼저 해제됨 */
static int bad_probe(struct platform_device *pdev)
{
    /* 1. MMIO 먼저 등록 */
    dev->regs = devm_ioremap_resource(&pdev->dev, res);

    /* 2. IRQ 나중에 등록 */
    ret = devm_request_threaded_irq(&pdev->dev, irq,
        my_hardirq, my_thread_fn, IRQF_ONESHOT, "dev", dev);

    /* BUG: remove 시 devres LIFO로 해제됩니다.
     * IRQ가 나중에 등록되었으므로 먼저 해제되는 것은 맞지만,
     * free_irq()가 thread_fn 완료를 대기하는 동안
     * thread_fn이 dev->regs에 접근하면...
     * dev->regs는 아직 유효합니다. (이 경우는 안전)
     *
     * 그러나 만약 아래와 같이 순서가 바뀌면: */

    /* 3. 추가 리소스 등록 (thread_fn이 의존) */
    dev->buffer = devm_kzalloc(&pdev->dev, BUF_SIZE, GFP_KERNEL);

    /* remove 시: buffer 먼저 해제 -> IRQ 해제 (thread_fn 대기)
     * -> thread_fn이 이미 해제된 buffer에 접근할 위험!
     *
     * 해결: IRQ는 의존하는 모든 리소스 이후에 등록하거나,
     * 명시적으로 disable_irq()를 remove에서 먼저 호출 */
    return 0;
}

/* 올바른 코드: IRQ가 의존하는 리소스를 먼저 등록 */
static int good_probe(struct platform_device *pdev)
{
    /* 1. 메모리 할당 */
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    dev->buffer = devm_kzalloc(&pdev->dev, BUF_SIZE, GFP_KERNEL);

    /* 2. MMIO 매핑 */
    dev->regs = devm_ioremap_resource(&pdev->dev, res);

    /* 3. IRQ를 마지막에 등록 -> LIFO로 먼저 해제됨
     * free_irq()가 thread_fn 완료를 대기한 후,
     * 나머지 리소스가 안전하게 해제됩니다. */
    ret = devm_request_threaded_irq(&pdev->dev, irq,
        my_hardirq, my_thread_fn, IRQF_ONESHOT, "dev", dev);

    return ret;
}

버그 6: force_irqthreads를 고려하지 않은 handler

/* 잠재적 문제: handler에서 GFP_ATOMIC 할당 */
static irqreturn_t legacy_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    struct data_entry *entry;

    /* 일반 커널: hardirq에서 실행 -> GFP_ATOMIC 필수
     * RT 커널 (force_irqthreads): 스레드에서 실행
     * -> GFP_KERNEL도 사용 가능하지만...
     * -> 두 환경 모두에서 동작해야 하므로 GFP_ATOMIC 유지
     *
     * 문제: GFP_ATOMIC은 할당 실패율이 높습니다.
     * force_irqthreads 환경에서는 불필요한 제약입니다. */
    entry = kmalloc(sizeof(*entry), GFP_ATOMIC);
    if (!entry)
        return IRQ_HANDLED;  /* 데이터 손실! */

    process_and_queue(dev, entry);
    return IRQ_HANDLED;
}

/* 개선: threaded IRQ로 마이그레이션하면 GFP_KERNEL 사용 가능 */
static irqreturn_t improved_thread_fn(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    struct data_entry *entry;

    /* 프로세스 컨텍스트: GFP_KERNEL 사용 가능
     * -> 메모리 부족 시 재시도/대기 가능 -> 할당 실패율 극히 낮음 */
    entry = kmalloc(sizeof(*entry), GFP_KERNEL);
    if (!entry)
        return IRQ_HANDLED;

    process_and_queue(dev, entry);
    return IRQ_HANDLED;
}

버그 유형별 체크리스트

#버그 유형dmesg 메시지진단 방법해결
1thread_fn 데드락hung task, soft lockupecho t > /proc/sysrq-trigger 스택 덤프(Dump)disable_irq_nosync() 사용
2hardirq/thread 레이스데이터 깨짐, 간헐적 오류KCSAN, lockdep 활성화WRITE_ONCE/READ_ONCE + ONESHOT
3IRQ_NONE 스퓨리어스irq N: nobody cared/proc/irq/N/spurious 확인thread_fn에서 IRQ_HANDLED 반환
4서스펜드 레이스bus error, MMIO fault서스펜드/리줌 스트레스 테스트disable_irq() 후 상태 변경
5devm 순서 오류use-after-free, kasanKASAN 활성화 + remove 테스트IRQ를 마지막에 등록
6GFP_ATOMIC 실패할당 실패 로그메모리 압박 테스트threaded IRQ + GFP_KERNEL

regmap_irq 통합 패턴 상세

regmap_irq 프레임워크는 I2C/SPI 디바이스의 레지스터 기반 인터럽트 컨트롤러를 자동으로 관리합니다. 내부적으로 threaded IRQ를 사용하여 슬로우 버스 접근을 처리하며, 드라이버 개발자는 레지스터 맵만 정의하면 됩니다.

regmap_irq 내부 동작

/* regmap_irq 프레임워크의 내부 thread_fn
 * (drivers/base/regmap/regmap-irq.c) */
static irqreturn_t regmap_irq_thread(int irq, void *d)
{
    struct regmap_irq_chip_data *data = d;
    bool handled = false;
    int i;

    /* 1. I2C/SPI를 통해 상태 레지스터 읽기 (슬립 가능) */
    for (i = 0; i < data->chip->num_regs; i++)
        regmap_read(data->map,
                    data->chip->status_base + (i * data->reg_stride),
                    &data->status_buf[i]);

    /* 2. 마스크 적용 (마스킹된 인터럽트 제외) */
    for (i = 0; i < data->chip->num_regs; i++)
        data->status_buf[i] &= ~data->mask_buf[i];

    /* 3. 각 활성 인터럽트에 대해 handle_nested_irq() 호출 */
    for (i = 0; i < data->chip->num_irqs; i++) {
        if (data->status_buf[data->chip->irqs[i].reg_offset] &
            data->chip->irqs[i].mask) {
            handle_nested_irq(
                irq_find_mapping(data->domain, i));
            handled = true;
        }
    }

    /* 4. ACK 레지스터에 쓰기 (인터럽트 클리어) */
    if (data->chip->ack_base)
        for (i = 0; i < data->chip->num_regs; i++)
            if (data->status_buf[i])
                regmap_write(data->map,
                    data->chip->ack_base + (i * data->reg_stride),
                    data->status_buf[i]);

    return handled ? IRQ_HANDLED : IRQ_NONE;
}

regmap_irq 전체 통합 예제

PMIC과 같은 복합 디바이스에서 regmap_irq를 사용하여 여러 서브 디바이스에 인터럽트를 분배하는 전체 패턴입니다:

/* 1단계: 인터럽트 맵 정의 */
static const struct regmap_irq my_codec_irqs[] = {
    [CODEC_IRQ_HEADPHONE]  = REGMAP_IRQ_REG(0, 0, BIT(0)),
    [CODEC_IRQ_MICROPHONE] = REGMAP_IRQ_REG(1, 0, BIT(1)),
    [CODEC_IRQ_OVERTEMP]   = REGMAP_IRQ_REG(2, 0, BIT(2)),
    [CODEC_IRQ_SHORT]      = REGMAP_IRQ_REG(3, 1, BIT(0)),
};

/* 2단계: IRQ 칩 구성 */
static const struct regmap_irq_chip my_codec_irq_chip = {
    .name            = "my-codec",
    .irqs            = my_codec_irqs,
    .num_irqs        = ARRAY_SIZE(my_codec_irqs),
    .num_regs        = 2,
    .status_base     = CODEC_REG_IRQ_STATUS,
    .mask_base       = CODEC_REG_IRQ_MASK,
    .ack_base        = CODEC_REG_IRQ_ACK,
    .mask_invert     = false,
    .init_ack_masked = true,
};

/* 3단계: probe에서 등록 */
static int my_codec_probe(struct i2c_client *client)
{
    struct my_codec *codec;

    codec->regmap = devm_regmap_init_i2c(client, &codec_regmap_cfg);

    /* regmap_irq가 내부적으로 devm_request_threaded_irq() 호출 */
    ret = devm_regmap_add_irq_chip(&client->dev, codec->regmap,
        client->irq,
        IRQF_ONESHOT | IRQF_TRIGGER_LOW,
        0, &my_codec_irq_chip, &codec->irq_data);

    /* 4단계: 개별 인터럽트를 서브 드라이버에서 사용 */
    codec->hp_irq = regmap_irq_get_virq(codec->irq_data,
                                         CODEC_IRQ_HEADPHONE);

    /* 서브 드라이버가 개별 IRQ를 threaded로 등록 */
    ret = devm_request_threaded_irq(&client->dev, codec->hp_irq,
        NULL, headphone_detect_handler,
        IRQF_ONESHOT, "hp-detect", codec);

    return 0;
}
regmap_irq의 장점: regmap_irq를 사용하면 드라이버 개발자가 직접 I2C/SPI를 통한 인터럽트 상태 읽기, 마스킹, ACK 로직을 구현할 필요가 없습니다. 레지스터 주소와 비트 위치만 정의하면 프레임워크가 모든 것을 처리합니다. 또한 irq_domain을 자동으로 생성하여 서브 디바이스가 각각 독립적인 IRQ를 사용할 수 있게 합니다.

참고자료

커널 공식 문서

커널 소스 코드

LWN.net 기사

서적 및 교육 자료

컨퍼런스 발표 및 외부 자료

Threaded IRQ와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.