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시스템 서스펜드 시에도 활성 유지웨이크업 소스 인터럽트

플래그 조합 규칙

/* 규칙 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을 사용하는 것이 안전한 관행입니다.

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

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으로 전환하는 것을 권장합니다.

공유 인터럽트에서의 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가 부모 스레드에서 직접 실행

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)

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

기존 드라이버를 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 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 온/오프라인 이벤트에 따라 자동으로 재배치합니다. 수동 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

참고자료

커널 공식 문서

커널 소스 코드

LWN.net 기사

서적 및 교육 자료

컨퍼런스 발표 및 외부 자료

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