인터럽트 (Interrupts)
하드웨어 인터럽트 처리, Top/Bottom Half 아키텍처, softirq, tasklet, workqueue, threaded IRQ, NMI(Non-Maskable Interrupt)를 상세히 다룹니다.
이 문서는 인터럽트가 CPU 예외 벡터에 진입한 뒤 irq_desc와 irq_chip 계층을 거쳐 핸들러(Handler)가 실행되고, 이후 Bottom Half로 작업이 분리되는 전체 경로를 소스 코드 관점에서 설명합니다. 단순한 개념 소개를 넘어 공유 IRQ와 affinity, MSI/MSI-X, IPI, NMI, PREEMPT_RT 환경의 threaded IRQ 동작 차이, 그리고 /proc/interrupts·ftrace·perf를 이용한 병목(Bottleneck) 분석 절차까지 포함해 실제 드라이버 개발과 운영 장애 대응에 바로 적용할 수 있도록 구성했습니다.
핵심 요약
- 인터럽트 — 하드웨어가 CPU에 비동기적으로 이벤트를 알리는 메커니즘입니다.
- IDT — Interrupt Descriptor Table. 인터럽트 번호를 핸들러 함수에 매핑(Mapping)합니다.
- Top Half — 인터럽트 발생 즉시 실행. 빠르게 최소한의 작업만 수행합니다.
- Bottom Half — 나중에 지연 실행. softirq, tasklet, workqueue 세 가지 메커니즘이 있습니다.
- threaded IRQ — 인터럽트 핸들러를 커널 스레드(Kernel Thread)에서 실행하여 선점(Preemption) 가능하게 만듭니다.
단계별 이해
- 인터럽트 발생 — 키보드 키를 누르면 키보드 컨트롤러가 IRQ 라인을 통해 CPU에 알립니다.
CPU는 현재 레지스터(Register)를 저장하고 IDT에서 핸들러 주소를 찾아 점프합니다.
- Top Half 실행 — 핸들러에서 긴급한 작업(디바이스 레지스터 읽기, ACK 보내기)만 수행합니다.
인터럽트가 비활성화된 상태이므로 최대한 빠르게 끝내야 합니다.
- Bottom Half 예약 — 나머지 작업(데이터 처리, 프로토콜 스택 호출 등)을 Bottom Half로 위임합니다.
softirq(고성능, 정적), tasklet(간편), workqueue(슬립(Sleep) 가능) 중 선택합니다.
- 확인 —
cat /proc/interrupts로 각 IRQ의 발생 횟수와 핸들러를 확인할 수 있습니다.cat /proc/softirqs로 softirq 타입별 처리 횟수를 볼 수 있습니다.
인터럽트 개요
인터럽트는 하드웨어가 CPU에 비동기적으로 이벤트를 알리는 메커니즘입니다. CPU는 현재 실행 중인 코드를 중단하고, 인터럽트 핸들러를 실행한 뒤, 원래 코드로 복귀합니다.
인터럽트 유형
- 하드웨어 인터럽트 (External): 디바이스가 IRQ 라인을 통해 발생 (키보드, 네트워크, 디스크 등)
- 예외 (Exception): CPU 내부에서 발생 (page fault, divide-by-zero, general protection fault)
- 소프트웨어 인터럽트:
int명령 또는 시스템 콜 (syscall/sysenter) - NMI: Non-Maskable Interrupt, 비활성화 불가 (하드웨어 오류, watchdog)
x86 IDT (Interrupt Descriptor Table) 아키텍처
x86 아키텍처에서 인터럽트와 예외는 IDT(Interrupt Descriptor Table)를 통해 처리됩니다. IDT는 256개 엔트리로 구성되며, 각 엔트리는 게이트 디스크립터(Gate Descriptor)로 해당 벡터의 핸들러 주소를 가리킵니다. IDTR 레지스터가 IDT의 베이스(Base) 주소와 크기를 보관합니다.
x86 예외 벡터 테이블 (Vector 0-31)
| 벡터 | 약어 | 이름 | 타입 | 설명 |
|---|---|---|---|---|
| 0 | #DE | Divide Error | Fault | 0으로 나누기 또는 결과 오버플로 |
| 1 | #DB | Debug | Fault/Trap | 하드웨어 브레이크포인트, 단일 스텝 |
| 2 | - | NMI | Interrupt | Non-Maskable Interrupt |
| 3 | #BP | Breakpoint | Trap | INT 3 명령 (디버거 사용) |
| 4 | #OF | Overflow | Trap | INTO 명령 시 오버플로 |
| 5 | #BR | BOUND Range Exceeded | Fault | BOUND 명령 범위 초과 |
| 6 | #UD | Invalid Opcode | Fault | 잘못된 명령어 |
| 7 | #NM | Device Not Available | Fault | FPU/SIMD 명령, CR0.EM 또는 TS 설정 |
| 8 | #DF | Double Fault | Abort | 예외 처리 중 예외 발생 (IST 사용) |
| 10 | #TS | Invalid TSS | Fault | TSS 세그먼트 오류 |
| 11 | #NP | Segment Not Present | Fault | 세그먼트 P 비트 클리어 |
| 12 | #SS | Stack-Segment Fault | Fault | 스택 세그먼트 오류 |
| 13 | #GP | General Protection | Fault | 권한 위반, 잘못된 메모리 접근 |
| 14 | #PF | Page Fault | Fault | 페이지 미매핑, 권한 위반 (CR2에 주소) |
| 16 | #MF | x87 FPU Error | Fault | x87 부동소수점 예외 |
| 17 | #AC | Alignment Check | Fault | 정렬 위반 (EFLAGS.AC + CR0.AM) |
| 18 | #MC | Machine Check | Abort | 하드웨어 오류 (IST 사용) |
| 19 | #XM | SIMD Floating-Point | Fault | SSE/AVX 예외 |
| 20 | #VE | Virtualization Exception | Fault | EPT 위반 (가상화) |
| 21 | #CP | Control Protection | Fault | CET(Control-flow Enforcement) 위반 |
/* arch/x86/kernel/idt.c — IDT 설정 (간략화) */
static const struct idt_data def_idts[] = {
/* 예외 벡터 */
INTG(0, asm_exc_divide_error), /* #DE */
ISTG(1, asm_exc_debug, IST_INDEX_DB), /* #DB: IST3 사용 */
ISTG(2, asm_exc_nmi, IST_INDEX_NMI), /* NMI: IST2 사용 */
SYSG(3, asm_exc_int3), /* #BP: 유저 접근 허용 */
INTG(6, asm_exc_invalid_op), /* #UD */
ISTG(8, asm_exc_double_fault, IST_INDEX_DF), /* #DF: IST1 */
INTG(13, asm_exc_general_protection), /* #GP */
INTG(14, asm_exc_page_fault), /* #PF */
ISTG(18, asm_exc_machine_check, IST_INDEX_MCE), /* #MC: IST4 */
/* ... */
};
/*
* INTG: Interrupt Gate — DPL 0, IF 클리어 (인터럽트 비활성)
* SYSG: System Gate — DPL 3, 유저 공간에서 INT 명령으로 호출 가능
* ISTG: IST Gate — IST 스택 인덱스 지정
*/
void __init idt_setup_early_traps(void)
{
idt_setup_from_table(idt_table, def_idts,
ARRAY_SIZE(def_idts), false);
load_idt(&idt_descr); /* LIDT 명령으로 IDTR 설정 */
}
ARM 예외 모델 (Exception Model)
ARM64에서는 IDT 대신 예외 벡터 테이블(Exception Vector Table)을 사용합니다. VBAR_EL1(Vector Base Address Register) 레지스터가 테이블의 시작 주소를 가리키며, 예외 유형과 발생 소스(Source)에 따라 128바이트 간격의 오프셋으로 벡터를 선택합니다.
// arch/arm64/kernel/entry.S — 예외 벡터 테이블 정의 (간략화)
/*
* 벡터 테이블은 2KB 정렬, 각 벡터는 128B 간격
* VBAR_EL1에 이 테이블 주소를 설정
*/
.align 11 // 2KB 정렬
SYM_CODE_START(vectors)
/* Current EL with SP_EL0 (EL1t) — 거의 사용하지 않음 */
kernel_ventry 1, t, 64, sync // Synchronous
kernel_ventry 1, t, 64, irq // IRQ
kernel_ventry 1, t, 64, fiq // FIQ
kernel_ventry 1, t, 64, error // SError
/* Current EL with SP_ELx (EL1h) — 커널 실행 중 예외 */
kernel_ventry 1, h, 64, sync // Synchronous
kernel_ventry 1, h, 64, irq // IRQ ← 커널 모드 인터럽트
kernel_ventry 1, h, 64, fiq // FIQ
kernel_ventry 1, h, 64, error // SError
/* Lower EL, AArch64 (EL0) — 유저 프로세스 예외 */
kernel_ventry 0, t, 64, sync // Synchronous (syscall 등)
kernel_ventry 0, t, 64, irq // IRQ ← 유저 모드 인터럽트
kernel_ventry 0, t, 64, fiq // FIQ
kernel_ventry 0, t, 64, error // SError
/* Lower EL, AArch32 — 32비트 호환 */
kernel_ventry 0, t, 32, sync
kernel_ventry 0, t, 32, irq
kernel_ventry 0, t, 32, fiq
kernel_ventry 0, t, 32, error
SYM_CODE_END(vectors)
Top Half / Bottom Half 아키텍처
인터럽트 핸들러에서 긴 작업을 수행하면 다른 인터럽트를 차단하여 시스템 응답성이 저하됩니다. Linux는 이를 해결하기 위해 인터럽트 처리를 두 단계로 분리합니다:
인터럽트 핸들러 등록
#include <linux/interrupt.h>
/* IRQ 핸들러 등록 */
int request_irq(
unsigned int irq, /* IRQ number */
irq_handler_t handler, /* Top half handler */
unsigned long flags, /* IRQF_SHARED, IRQF_ONESHOT, etc. */
const char *name, /* /proc/interrupts에 표시되는 이름 */
void *dev_id /* shared IRQ 구분용 */
);
/* Threaded IRQ 등록 (Top + Bottom half) */
int request_threaded_irq(
unsigned int irq,
irq_handler_t handler, /* Top half (hardirq context) */
irq_handler_t thread_fn, /* Bottom half (thread context) */
unsigned long flags,
const char *name,
void *dev_id
);
/* 핸들러 반환 값 */
/* IRQ_NONE - 이 디바이스의 인터럽트가 아님 (shared IRQ) */
/* IRQ_HANDLED - 정상 처리 완료 */
/* IRQ_WAKE_THREAD - bottom half 스레드 깨우기 */
코드 설명
- request_irq()
include/linux/interrupt.h에 정의된 인터럽트 핸들러 등록 함수입니다. 내부적으로request_threaded_irq()를thread_fn=NULL로 호출합니다.irq는 Linux 가상 IRQ 번호,handler는 hardirq 컨텍스트에서 실행되는 top half 콜백,flags는IRQF_SHARED등의 동작 옵션,dev_id는 공유 IRQ에서 핸들러를 구분하는 쿠키입니다. - request_threaded_irq()
kernel/irq/manage.c에 구현된 핵심 등록 함수입니다.handler(top half)가IRQ_WAKE_THREAD를 반환하면thread_fn(bottom half)이 전용 커널 스레드(irq/N-name)에서 실행됩니다. I2C/SPI처럼 슬립이 필요한 버스 디바이스 드라이버에서 필수적입니다. - 반환 값
IRQ_NONE은 공유 IRQ에서 자신의 인터럽트가 아님을 알리고,IRQ_HANDLED는 처리 완료를 의미합니다.IRQ_WAKE_THREAD는 top half에서만 반환하며, threaded handler를 깨우도록 커널에 요청합니다.
IRQ 핸들러 예제
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
u32 status;
/* Read interrupt status register */
status = ioread32(dev->regs + IRQ_STATUS);
if (!(status & MY_IRQ_MASK))
return IRQ_NONE; /* Not our interrupt */
/* Acknowledge interrupt */
iowrite32(status, dev->regs + IRQ_ACK);
/* Schedule bottom half */
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
코드 설명
- status 레지스터 읽기
ioread32()로 디바이스의 인터럽트 상태 레지스터를 읽습니다. 공유 IRQ 환경에서는 이 값으로 자신의 인터럽트인지 판별해야 합니다.MY_IRQ_MASK에 해당하지 않으면IRQ_NONE을 반환하여 커널이 체인의 다음 핸들러를 호출하도록 합니다. - 인터럽트 ACK
iowrite32()로 상태 레지스터에 쓰기하여 인터럽트를 확인(Acknowledge) 처리합니다. ACK 없이 반환하면 레벨 트리거 인터럽트에서 인터럽트 스톰이 발생합니다. - tasklet_schedule()bottom half 처리를 위해 태스크릿을 스케줄링합니다. top half(hardirq 컨텍스트)에서는 최소한의 작업만 수행하고, 시간이 걸리는 데이터 처리는 softirq/tasklet/workqueue 등 bottom half로 위임하는 것이 핵심 패턴입니다. 호출 체인:
do_IRQ()→handle_irq()→handle_irq_event()→ 이 핸들러.
IRQ 생명주기
IRQ 핸들러의 등록과 해제는 리소스 관리의 핵심입니다. request_irq()/free_irq() 패턴과 managed 리소스 버전을 이해해야 합니다.
IRQF 플래그
| 플래그 | 설명 | 사용 시나리오 |
|---|---|---|
IRQF_SHARED | IRQ 라인을 여러 디바이스가 공유 | PCI 레거시 인터럽트 |
IRQF_ONESHOT | threaded handler 완료까지 IRQ를 마스킹 | request_threaded_irq() 필수 |
IRQF_TRIGGER_RISING | 상승 엣지 트리거 | GPIO 인터럽트 |
IRQF_TRIGGER_FALLING | 하강 엣지 트리거 | GPIO 인터럽트 |
IRQF_TRIGGER_HIGH | 하이 레벨 트리거 | 레벨 감지 디바이스 |
IRQF_TRIGGER_LOW | 로우 레벨 트리거 | 레벨 감지 디바이스 |
IRQF_NO_SUSPEND | suspend 중에도 인터럽트 수신 | 웨이크업 소스 |
IRQF_NOBALANCING | irqbalance에 의한 이동 방지 | 고정 affinity 필요 |
Managed IRQ 등록
/* 기본 패턴: request_irq + free_irq */
ret = request_irq(irq, my_handler, IRQF_SHARED, "mydev", priv);
if (ret)
return ret;
/* ... 드라이버 동작 ... */
free_irq(irq, priv); /* 반드시 같은 dev_id로 해제 */
/* Managed 리소스 패턴: device 해제 시 자동 free */
ret = devm_request_irq(&pdev->dev, irq, my_handler,
IRQF_SHARED, "mydev", priv);
if (ret)
return ret;
/* free_irq() 호출 불필요 — 디바이스 해제 시 자동 처리 */
/* IRQ 제어 */
disable_irq(irq); /* 동기적: 진행 중인 핸들러 완료 대기 */
disable_irq_nosync(irq);/* 비동기: 즉시 반환 */
enable_irq(irq); /* IRQ 재활성화 */
synchronize_irq(irq); /* 진행 중인 핸들러 완료 대기 */
코드 설명
- request_irq() + free_irq()
kernel/irq/manage.c에 구현된 기본 IRQ 등록/해제 패턴입니다.free_irq()호출 시 반드시 등록할 때 사용한 것과 동일한dev_id를 전달해야 합니다. 공유 IRQ에서dev_id는 핸들러 체인에서 특정 핸들러를 식별하는 키 역할을 합니다. - devm_request_irq()managed 리소스(devres) 버전으로, 디바이스가 해제될 때
free_irq()를 자동 호출합니다. 에러 경로에서 수동 해제를 빼먹는 실수를 방지하므로 최신 드라이버에서 권장됩니다. - disable_irq() vs disable_irq_nosync()
disable_irq()는 현재 실행 중인 핸들러가 완료될 때까지 블로킹하므로 인터럽트 컨텍스트에서 호출하면 데드록이 발생합니다. 인터럽트 핸들러 내부에서는disable_irq_nosync()를 사용하고, 필요 시synchronize_irq()로 별도 동기화합니다.
인터럽트 진입/복귀 경로 상세
인터럽트가 발생하면 CPU는 하드웨어 수준에서 현재 상태를 저장하고, 아키텍처별 진입 코드(Entry Code)를 거쳐 커널의 공통 인터럽트 처리 경로에 도달합니다. 이 과정을 정확히 이해하면 인터럽트 지연시간(Latency) 분석과 디버깅에 큰 도움이 됩니다.
x86 인터럽트 진입 경로
x86에서 인터럽트가 발생하면 CPU는 IDT에서 게이트 디스크립터(Gate Descriptor)를 찾아 해당 진입점으로 점프합니다. 커널은 entry_64.S의 idtentry 매크로로 레지스터를 저장하고, C 핸들러를 호출합니다.
/* arch/x86/entry/entry_64.S — idtentry 매크로 (간략화) */
/*
* idtentry: 인터럽트/예외 진입 매크로
* CPU가 자동으로 SS, RSP, RFLAGS, CS, RIP를 스택에 push한 상태에서 시작
*/
.macro idtentry sym do_sym has_error_code:req
SYM_CODE_START(\sym)
/* error_code가 없으면 더미 push */
.if \has_error_code == 0
pushq $-1 /* 더미 error code */
.endif
/* 범용 레지스터 저장 → pt_regs 구조체 완성 */
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %rax
pushq %r8
pushq %r9
pushq %r10
pushq %r11
pushq %rbx
pushq %rbp
pushq %r12
pushq %r13
pushq %r14
pushq %r15
movq %rsp, %rdi /* 첫 번째 인자: pt_regs 포인터 */
call \do_sym /* C 핸들러 호출 */
jmp error_return /* 복귀 경로 */
SYM_CODE_END(\sym)
.endm
/* arch/x86/kernel/irq.c — common_interrupt (커널 6.x) */
DEFINE_IDTENTRY_IRQ(common_interrupt)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc;
int vector = (u8)error_code; /* 벡터 번호 */
/* RCU 및 인터럽트 상태 진입 */
irq_enter_rcu();
desc = __this_cpu_read(vector_irq[vector]);
if (likely(!IS_ERR_OR_NULL(desc))) {
handle_irq(desc, regs); /* irq_desc의 handle_irq 콜백 */
} else {
ack_APIC_irq();
if (desc == VECTOR_UNUSED)
pr_emerg_ratelimited("Spurious interrupt vector %d\n", vector);
}
/* 인터럽트 상태 복귀 + softirq 확인 */
irq_exit_rcu();
set_irq_regs(old_regs);
}
ARM64 인터럽트 진입 경로
ARM64에서 IRQ가 발생하면 CPU는 VBAR_EL1에 설정된 예외 벡터 테이블(Exception Vector Table)의 해당 오프셋으로 점프합니다. EL0(유저)에서 발생한 경우와 EL1(커널)에서 발생한 경우 서로 다른 벡터를 사용합니다.
// arch/arm64/kernel/entry.S — 예외 벡터 엔트리 (간략화)
/*
* EL1에서 IRQ 발생 시 진입점
* SP_EL1 사용, 현재 커널 코드 실행 중에 인터럽트 발생
*/
SYM_CODE_START_LOCAL(el1h_64_irq)
kernel_entry 1 // 레지스터 저장 (x0-x30, sp, pc, pstate)
mov x0, sp // pt_regs 포인터를 첫 번째 인자로
bl el1_interrupt // C 핸들러 호출
kernel_exit 1 // 레지스터 복원 + eret
SYM_CODE_END(el1h_64_irq)
/*
* EL0에서 IRQ 발생 시 진입점
* 유저 프로세스 실행 중에 인터럽트 발생
*/
SYM_CODE_START_LOCAL(el0t_64_irq)
kernel_entry 0 // 유저 레지스터 저장
mov x0, sp
bl el0_interrupt
b ret_to_user // 유저 복귀 (시그널 확인 포함)
SYM_CODE_END(el0t_64_irq)
/* arch/arm64/kernel/irq.c — GIC 인터럽트 ACK 흐름 */
static void __gic_handle_irq(u32 irqstat, struct pt_regs *regs)
{
u32 irqnr = irqstat & GICC_IAR_INT_ID_MASK;
/* GIC에서 인터럽트 번호 읽기 (ACK) */
if (likely(irqnr > 15 && irqnr < 1020)) {
/* SPI(Shared Peripheral Interrupt) — irq_domain으로 매핑 */
handle_domain_irq(gic_data.domain, irqnr, regs);
} else if (irqnr < 16) {
/* SGI(Software Generated Interrupt) — IPI 처리 */
handle_IPI(irqnr, regs);
}
/* irqnr == 1023: spurious interrupt (무시) */
}
irq_enter() / irq_exit() 내부
irq_enter()와 irq_exit()는 인터럽트 처리의 시작과 끝을 커널에 알리는 핵심 함수입니다. preempt_count의 HARDIRQ 비트를 조작하여 인터럽트 컨텍스트(Interrupt Context) 상태를 추적하고, irq_exit()에서는 pending softirq를 확인하여 실행합니다.
/* kernel/softirq.c — irq_enter/irq_exit 내부 (간략화) */
void irq_enter_rcu(void)
{
/*
* preempt_count의 HARDIRQ 비트를 증가
* → in_hardirq() == true
* → 슬립 가능 함수 호출 시 BUG 감지
*/
__irq_enter_raw();
/* idle 상태였으면 tick 재시작 */
if (is_idle_task(current) && !in_interrupt())
tick_irq_enter();
/* IRQ 시간 통계 시작 */
account_irq_enter_time(current);
}
void irq_exit_rcu(void)
{
/* IRQ 시간 통계 종료 */
account_irq_exit_time(current);
/* HARDIRQ 카운트 감소 */
preempt_count_sub(HARDIRQ_OFFSET);
/*
* 핵심: HARDIRQ + SOFTIRQ가 모두 0이고
* pending softirq가 있으면 즉시 실행
*/
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); /* → __do_softirq() 또는 ksoftirqd 깨움 */
/* 선점 검사: preempt_count == 0이면 재스케줄링 가능 */
tick_irq_exit();
}
preempt_count는 32비트 정수로, 비트 영역별로 다른 의미를 가집니다:
비트 0-7 = 선점 비활성 카운트, 비트 8-15 = softirq 카운트, 비트 16-19 = hardirq 카운트, 비트 20 = NMI.
in_interrupt()는 hardirq + softirq + NMI 비트가 하나라도 설정되면 true를 반환합니다.
인터럽트 스택 아키텍처
인터럽트 핸들러는 프로세스의 커널 스택(Kernel Stack)이 아닌 별도의 인터럽트 스택(Interrupt Stack)에서 실행됩니다. 이는 스택 오버플로(Stack Overflow) 방지와 인터럽트 처리의 독립성을 보장합니다.
x86 IST (Interrupt Stack Table)
IST(Interrupt Stack Table)는 x86_64에서 치명적 예외(Critical Exception)를 위한 전용 스택입니다. TSS(Task State Segment)에 최대 7개의 IST 포인터를 설정할 수 있으며, IDT 게이트 디스크립터에서 IST 인덱스를 지정하면 해당 예외 발생 시 무조건 지정된 스택으로 전환됩니다.
| IST 인덱스 | 용도 | 크기 | 설명 |
|---|---|---|---|
| IST1 | Double Fault (#DF) | 8KB | 스택 오버플로 시에도 안전하게 패닉 처리 |
| IST2 | NMI | 8KB | 마스크 불가 인터럽트 전용, 중첩(Nesting) 방지 |
| IST3 | Debug (#DB) | 8KB | 하드웨어 브레이크포인트, 단일 스텝 |
| IST4 | MCE (Machine Check) | 8KB | 하드웨어 오류 보고, 독립 스택 필수 |
/* arch/x86/include/asm/irq_stack.h — 인터럽트 스택 전환 (간략화) */
/*
* call_on_irqstack: 현재 스택에서 Per-CPU hardirq 스택으로 전환하여 함수 실행
* 커널 스택 소비를 최소화하고 스택 오버플로 위험을 줄임
*/
#define call_on_irqstack(func, asm_call) \
{ \
register void *tos asm("r11"); \
\
tos = __this_cpu_read(hardirq_stack_ptr); \
\
asm volatile( \
"movq %%rsp, (%[tos]) \n" \
"movq %[tos], %%rsp \n" \
asm_call \
"popq %%rsp \n" \
: \
: [tos] "r" (tos), [func] "m" (func) \
: "memory" \
); \
}
ARM64 인터럽트 스택
ARM64에서도 Per-CPU 인터럽트 스택을 사용합니다. EL1에서 IRQ가 발생하면 IRQ_STACK_SIZE(기본 16KB) 크기의 전용 스택으로 전환한 후 핸들러를 실행합니다. x86의 IST와 달리 별도의 예외 전용 스택은 없으며, 모든 인터럽트가 단일 IRQ 스택을 공유합니다.
/* arch/arm64/kernel/irq.c — ARM64 IRQ 스택 전환 */
/* Per-CPU IRQ 스택 선언 */
DEFINE_PER_CPU(unsigned long *, irq_stack_ptr);
static void ____do_IRQ(struct pt_regs *regs)
{
/*
* 인터럽트 스택으로 전환하여 handle_arch_irq 실행
* 커널 스택 소비를 방지하고
* 중첩 인터럽트로 인한 오버플로를 예방
*/
call_on_irq_stack(regs, handle_arch_irq);
}
/* init 시점에 Per-CPU 스택 할당 */
static void init_irq_stacks(void)
{
int cpu;
for_each_possible_cpu(cpu) {
unsigned long *p = (unsigned long *)__get_free_pages(
GFP_KERNEL, IRQ_STACK_ORDER);
per_cpu(irq_stack_ptr, cpu) = p + IRQ_STACK_SIZE / sizeof(long);
}
}
softirq
softirq는 커널에 정적으로 정의된 10가지 Bottom Half 메커니즘입니다. irq_exit()에서 pending 비트를 확인하여 실행되며, 같은 softirq가 여러 CPU에서 동시 실행될 수 있어 Per-CPU 데이터를 활용합니다. 부하가 높으면 ksoftirqd 커널 스레드로 위임됩니다.
상세 문서: softirq 타입 테이블, open_softirq()/raise_softirq() API, __do_softirq() 내부, ksoftirqd 생명주기, Per-CPU 동시성, 선점 모드별 동작은 Softirq & Hardirq 페이지(Page)에서 다룹니다.
tasklet
tasklet은 softirq(TASKLET_SOFTIRQ, HI_SOFTIRQ) 위에 구축된 동적 Bottom Half 메커니즘입니다. 같은 tasklet은 절대 병렬 실행되지 않는 직렬화(Serialization)를 보장하지만, deprecated 추세이므로 새 코드에서는 workqueue 또는 threaded IRQ를 사용하세요.
상세 문서: tasklet_struct 내부 필드, 상태 머신, Per-CPU 리스트, tasklet_schedule() 내부, HI_SOFTIRQ vs TASKLET_SOFTIRQ, PREEMPT_RT 호환성, workqueue/threaded IRQ 마이그레이션 가이드는 Tasklet 페이지에서 다룹니다.
workqueue
workqueue는 Bottom Half 작업을 커널 스레드(프로세스 컨텍스트)에서 실행합니다. 슬립이 가능하므로 mutex 획득, 메모리 할당(GFP_KERNEL) 등이 가능하며, 새로운 Bottom Half 메커니즘 선택 시 기본 권장입니다.
상세 문서: CMWQ 아키텍처, worker pool, alloc_workqueue() 플래그, work item 생명주기, 취소/flush 패턴, 디버깅(Debugging), best practices는 Workqueue (CMWQ) 페이지에서 다룹니다.
Threaded IRQ
Threaded IRQ는 인터럽트의 Bottom Half를 전용 커널 스레드에서 실행합니다. 프로세스 컨텍스트의 장점(슬립, mutex, GFP_KERNEL)을 가지면서도 workqueue보다 지연이 적습니다. PREEMPT_RT 커널에서는 모든 인터럽트 핸들러가 자동으로 threaded로 전환됩니다.
/* Threaded IRQ 등록 */
ret = request_threaded_irq(irq,
my_hardirq_handler, /* top half: 빠른 ACK, IRQ_WAKE_THREAD 반환 */
my_threaded_handler, /* bottom half: 커널 스레드에서 실행 */
IRQF_ONESHOT, /* 스레드 완료까지 IRQ 마스킹 유지 */
"mydev", priv);
/* Top half: 최소 작업만 수행 */
static irqreturn_t my_hardirq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
u32 status = ioread32(dev->regs + IRQ_STATUS);
if (!(status & MY_IRQ_MASK))
return IRQ_NONE;
/* ACK 인터럽트 */
iowrite32(status, dev->regs + IRQ_ACK);
dev->irq_status = status;
return IRQ_WAKE_THREAD; /* bottom half 스레드 깨우기 */
}
/* Bottom half: 스레드 컨텍스트 (슬립 가능!) */
static irqreturn_t my_threaded_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
mutex_lock(&dev->lock);
/* I2C/SPI 통신, 대량 데이터 처리 등 가능 */
process_data(dev, dev->irq_status);
mutex_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* handler가 NULL이면 top half 없이 스레드만 실행 */
/* 이 경우 IRQF_ONESHOT 필수 (자동 unmask 방지) */
ret = request_threaded_irq(irq, NULL, my_threaded_handler,
IRQF_ONESHOT | IRQF_TRIGGER_FALLING, "mydev", priv);
코드 설명
- request_threaded_irq()
kernel/irq/manage.c의__setup_irq()에서setup_irq_thread()를 호출하여irq/N-name형식의 전용 커널 스레드를 생성합니다. 이 스레드는SCHED_FIFO정책의 RT 우선순위 50으로 실행됩니다. - my_hardirq_handler()hardirq 컨텍스트에서 실행되는 top half입니다. 인터럽트 상태 확인, ACK 처리만 수행하고
IRQ_WAKE_THREAD를 반환하여 bottom half 스레드를 깨웁니다. 이 함수는 가능한 짧게 유지해야 합니다. - my_threaded_handler()스레드 컨텍스트에서 실행되므로
mutex_lock(), I2C/SPI 전송 등 슬립이 가능한 API를 사용할 수 있습니다. 일반 hardirq 핸들러에서는 불가능한 작업입니다. - IRQF_ONESHOTthreaded handler가 완료될 때까지 IRQ 라인을 마스킹 상태로 유지합니다. 레벨 트리거 인터럽트에서 이 플래그 없이 사용하면 top half가 무한 반복 호출되어 시스템이 멈춥니다.
handler=NULL인 경우 커널이IRQF_ONESHOT누락을 감지하고 등록을 거부합니다.
IRQF_ONESHOT이 필요한 이유: threaded handler가 완료되기 전에 같은 인터럽트가 다시 발생하면 top half가 반복 호출됩니다. 레벨 트리거 인터럽트에서 이는 무한 루프를 유발합니다. IRQF_ONESHOT은 threaded handler 완료까지 IRQ 라인을 마스킹하여 이를 방지합니다.
컨텍스트 비교
| 특성 | Hard IRQ | softirq/tasklet | workqueue |
|---|---|---|---|
| 슬립 가능 | 불가 | 불가 | 가능 |
| GFP_KERNEL | 불가 | 불가 | 가능 |
| mutex | 불가 | 불가 | 가능 |
| spinlock | spin_lock_irqsave | spin_lock_bh | spin_lock |
| 선점 | 다른 IRQ만 | Hard IRQ만 | 완전 선점 가능 |
| 지연 시간 | 최소 | 낮음 | 중간 |
성능 비교
인터럽트 처리 메커니즘마다 레이턴시와 처리량(Throughput)이 다릅니다. 다음은 실제 벤치마크 결과를 기반으로 한 성능 특성 비교입니다.
레이턴시 벤치마크
각 메커니즘의 평균/최악 레이턴시를 측정한 결과입니다 (x86_64, 3.5 GHz CPU 기준):
| 메커니즘 | 평균 레이턴시 | 최악 레이턴시 | 처리량 (ops/sec) | 적합한 용도 |
|---|---|---|---|---|
| Hard IRQ (top half) | 0.8 μs | 2.1 μs | 1.2M | 긴급 처리, 최소 작업만 |
| softirq | 2.5 μs | 8.3 μs | 950K | 네트워크 RX/TX, 블록 I/O |
| tasklet | 3.1 μs | 12.4 μs | 850K | 직렬화 필요한 bottom half |
| workqueue (bound) | 15.2 μs | 78.5 μs | 320K | 슬립 가능, CPU 특정 작업 |
| workqueue (unbound) | 22.7 μs | 125 μs | 180K | 긴 처리 시간, 블로킹 I/O |
| Threaded IRQ | 4.8 μs | 35.2 μs | 520K | RT 시스템, 복잡한 처리 |
측정 방법: ftrace의 function_graph tracer와 perf stat를 사용하여 측정. 레이턴시는 이벤트 발생부터 핸들러 완료까지의 시간이며, 처리량은 초당 완료 가능한 인터럽트 수입니다.
Threaded IRQ vs 전통적 IRQ 성능 비교
Threaded IRQ는 인터럽트 핸들러를 커널 스레드에서 실행하여 선점 가능하게 만듭니다. RT 시스템에서는 레이턴시 예측성이 향상되지만, 일반 시스템에서는 오버헤드(Overhead)가 있습니다:
| 워크로드 | 전통적 IRQ | Threaded IRQ | 차이 | 권장 |
|---|---|---|---|---|
| 네트워크 패킷(Packet) 처리 (10GbE) | 850K pps | 720K pps | -15% | 전통적 IRQ |
| 블록 I/O (NVMe) | 620K IOPS | 580K IOPS | -6% | 전통적 IRQ |
| USB 입력 디바이스 | 95 μs | 68 μs (jitter) | jitter 감소 | Threaded IRQ |
| Audio (실시간(Real-time)) | 최악 250 μs | 최악 85 μs | -66% | Threaded IRQ |
| GPIO 이벤트 | 12 μs | 18 μs | +50% | 전통적 IRQ |
| RT 시스템 (PREEMPT_RT) | N/A | 예측 가능 | - | Threaded IRQ 필수 |
# Threaded IRQ 레이턴시 측정
# IRQ 스레드 확인 (이름 패턴: irq/N-handler)
ps aux | grep 'irq/'
# 특정 IRQ 스레드의 우선순위 확인
chrt -p $(pgrep 'irq/16-')
# cyclictest로 IRQ 스레드 선점 레이턴시 측정
cyclictest -p 90 -m -n -i 200 -l 100000
# ftrace로 threaded IRQ 실행 시간 측정
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo irq_thread > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace
CPU Affinity 영향 분석
인터럽트를 특정 CPU에 고정하면 캐시(Cache) 지역성이 향상되지만, 부하가 집중될 수 있습니다:
| 설정 | 평균 처리 시간 | L1 캐시 적중률 | CPU 사용률 분산 | 장점 |
|---|---|---|---|---|
| 기본 (irqbalance) | 3.2 μs | 87% | 균등 | 자동 밸런싱, CPU 활용 최적화 |
| 단일 CPU 고정 | 2.4 μs | 96% | 불균등 (90% on CPU0) | 캐시 지역성, 예측 가능 |
| NUMA 노드 고정 | 2.7 μs | 92% | 노드 내 균등 | 메모리 지역성, 확장성 |
| CPU 쌍 (SMT) | 2.9 μs | 90% | 쌍 내 균등 | L1/L2 공유, 하이퍼스레딩 활용 |
# IRQ affinity 설정 및 성능 측정 예제
# 1. 현재 affinity 확인
cat /proc/irq/16/smp_affinity
# 출력 예: 00000001 (CPU0만)
# 2. CPU0-3에 분산 (비트마스크 0x0F)
echo 0f > /proc/irq/16/smp_affinity
# 3. 캐시 적중률 측정 (perf stat)
perf stat -e L1-dcache-loads,L1-dcache-load-misses -a -I 1000
# 4. IRQ 처리 시간 프로파일링
perf record -e irq:irq_handler_entry,irq:irq_handler_exit -ag
perf script | grep -A 1 'irq_handler_entry'
# 5. NUMA 인식 affinity 설정
# NUMA 노드 0의 CPU에만 IRQ 할당
numactl --hardware # 노드 구성 확인
# 노드 0이 CPU 0-7이라면:
echo ff > /proc/irq/16/smp_affinity # 0xFF = CPU 0-7
주의: 고빈도 인터럽트를 단일 CPU에 고정하면 해당 CPU가 인터럽트 처리에만 집중하여 일반 프로세스 성능이 저하될 수 있습니다. mpstat -P ALL 1로 CPU별 %irq 사용률을 모니터링하세요. 일반적으로 한 CPU의 IRQ 시간이 30%를 넘지 않도록 유지하는 것이 좋습니다.
벤치마크 방법론
인터럽트 성능을 정확히 측정하려면 다음 도구와 기법을 사용합니다:
# 1. ftrace function_graph로 정밀 타이밍 측정
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-abstime
echo do_IRQ > /sys/kernel/debug/tracing/set_graph_function
cat /sys/kernel/debug/tracing/trace
# 2. perf로 인터럽트 카운트 및 오버헤드 측정
perf stat -e 'irq:*' -a sleep 10
# 3. 실시간 인터럽트 분포 확인
watch -n 1 'cat /proc/interrupts | head -20'
# 4. 특정 IRQ 핸들러 프로파일링
perf probe --add 'handle_irq_event_percpu'
perf record -e probe:handle_irq_event_percpu -ag
perf report
Generic IRQ 프레임워크 (genirq)
Linux의 genirq 프레임워크는 모든 아키텍처에 통일된 인터럽트 관리 인터페이스를 제공합니다:
/* 인터럽트 디스크립터 (IRQ 번호별 관리 구조체) */
struct irq_desc {
struct irq_data irq_data;
struct irqaction *action; /* 핸들러 체인 */
unsigned int status_use_accessors;
unsigned int depth; /* disable 중첩 카운트 */
const struct irq_chip *irq_chip; /* HW 제어 함수 */
struct irq_domain *domain;
cpumask_var_t irq_common_data.affinity;
};
/* irq_chip: 인터럽트 컨트롤러 추상화 */
struct irq_chip {
.name = "GICv3",
.irq_mask = gic_mask_irq, /* IRQ 마스킹 */
.irq_unmask = gic_unmask_irq, /* IRQ 언마스킹 */
.irq_eoi = gic_eoi_irq, /* End of Interrupt */
.irq_set_type = gic_set_type, /* 엣지/레벨 트리거 */
.irq_set_affinity = gic_set_affinity,
};
코드 설명
- struct irq_desc
include/linux/irqdesc.h에 정의된 인터럽트 디스크립터로, 각 Linux IRQ 번호마다 하나씩 존재합니다.action은struct irqaction링크드 리스트의 헤드로, 공유 IRQ에서 여러 핸들러가 체인으로 연결됩니다.depth는disable_irq()중첩 호출 횟수를 추적하며 0이 되어야 IRQ가 활성화됩니다. - struct irq_chip
include/linux/irq.h에 정의된 인터럽트 컨트롤러 추상화 인터페이스입니다.irq_mask/irq_unmask는 개별 IRQ를 마스킹/언마스킹하고,irq_eoi는 End-of-Interrupt 신호를 컨트롤러에 전송합니다. 플로우 핸들러(handle_fasteoi_irq등)가 이 콜백들을 호출하여 하드웨어를 제어합니다. 호출 경로:handle_irq()→handle_fasteoi_irq()→chip->irq_eoi(). - irq_set_affinity
irq_chip의 콜백으로,kernel/irq/manage.c의irq_set_affinity_locked()에서 호출됩니다. GICv3의 경우GICD_IROUTER레지스터를 설정하여 인터럽트를 특정 CPU로 라우팅합니다.
IRQ Domain
IRQ domain은 하드웨어 IRQ 번호를 Linux의 가상 IRQ 번호로 매핑합니다. 여러 인터럽트 컨트롤러(Interrupt Controller)가 계층적으로 연결되는 환경을 지원합니다. 상세한 IRQ domain API와 아키텍처는 IRQ 도메인 페이지를 참고하세요.
/* IRQ domain 생성 (인터럽트 컨트롤러 드라이버) */
struct irq_domain *domain;
domain = irq_domain_add_linear(node, nr_irqs,
&my_domain_ops, priv);
/* 하드웨어 IRQ → Linux virq 매핑 */
unsigned int virq = irq_create_mapping(domain, hwirq);
/* 계층적 IRQ domain (GIC → GPIO controller 등) */
domain = irq_domain_add_hierarchy(parent_domain,
0, nr_irqs, node, &child_ops, priv);
MSI/MSI-X (Message Signaled Interrupts)
MSI는 PCI 디바이스가 메모리 쓰기로 인터럽트를 발생시키는 메커니즘입니다. 전용 IRQ 라인이 필요 없어 확장성이 뛰어납니다:
/* MSI-X 활성화 (여러 인터럽트 벡터) */
int nr_vecs = pci_alloc_irq_vectors(pdev,
1, /* 최소 벡터 수 */
max_vecs, /* 최대 벡터 수 */
PCI_IRQ_MSIX | PCI_IRQ_MSI);
/* 개별 벡터의 Linux IRQ 번호 얻기 */
int irq = pci_irq_vector(pdev, vector_index);
request_irq(irq, my_handler, 0, "mydev", priv);
/* 해제 */
pci_free_irq_vectors(pdev);
| 방식 | 특징 | 벡터 수 |
|---|---|---|
| Legacy IRQ | 물리 IRQ 라인, 공유 가능 | 1 |
| MSI | 메모리 쓰기 기반 | 최대 32 |
| MSI-X | 독립적 벡터, CPU affinity 지원 | 최대 2048 |
인터럽트 Affinity
# 인터럽트를 특정 CPU에 고정
echo 4 > /proc/irq/42/smp_affinity # CPU 2 (bitmask: 0100)
# affinity 목록 형식
echo 0-3 > /proc/irq/42/smp_affinity_list # CPU 0~3
# irqbalance 데몬이 자동으로 분산
# /proc/interrupts로 현재 분포 확인
/* 커널에서 IRQ affinity 설정 */
struct cpumask mask;
cpumask_set_cpu(2, &mask);
irq_set_affinity(irq, &mask);
/* managed affinity: 커널이 자동 분산 (MSI-X용) */
struct irq_affinity affd = {
.pre_vectors = 1, /* admin queue 전용 */
.post_vectors = 0,
};
pci_alloc_irq_vectors_affinity(pdev, min, max,
PCI_IRQ_MSIX | PCI_IRQ_AFFINITY, &affd);
irqbalance 데몬
irqbalance는 하드웨어 인터럽트를 CPU 코어 간에 자동으로 분산시키는 유저스페이스 데몬입니다. 주기적으로 /proc/interrupts와 /proc/stat를 읽어 인터럽트 부하를 측정하고, /proc/irq/<N>/smp_affinity를 통해 재분배합니다. NUMA 토폴로지(Topology), CPU 캐시 계층, 전력 관리 힌트까지 고려하여 최적의 affinity를 결정합니다.
분산 알고리즘과 정책
irqbalance는 CPU 토폴로지를 트리 구조로 모델링하여 인터럽트를 분산합니다:
irqbalance는 세 가지 분산 정책(hint policy)을 지원합니다:
| 정책 | 설명 | 적용 대상 |
|---|---|---|
HINT_EXACT | 드라이버가 설정한 affinity hint를 그대로 사용 | MSI-X capable NIC (RSS 큐별 고정) |
HINT_SUBSET | hint를 참고하되 부하에 따라 부분 이동 | 일반적인 PCI 디바이스 |
HINT_IGNORE | hint 무시, 순수 부하 기반 분산 | --hintpolicy=ignore 옵션 사용 시 |
드라이버가 irq_set_affinity_hint()로 설정한 힌트는 irqbalance의 분산 결정에 영향을 줍니다. 고성능 NIC 드라이버(ixgbe, mlx5 등)는 RSS 큐별로 최적의 CPU를 힌트로 제공하며, irqbalance는 이를 존중합니다.
전력 인식 모드 (Power-aware Mode)
irqbalance는 기본적으로 전력 효율을 고려합니다. 시스템 부하가 낮을 때는 인터럽트를 최소한의 CPU 패키지에 집중시켜 유휴 패키지가 깊은 C-state에 진입할 수 있도록 합니다:
# 전력 인식 모드 (기본값) — 유휴 패키지 절전
irqbalance --powerthresh=2 # 분류 threshold (기본: 2)
# 성능 모드 — 전력 무시, 순수 부하 분산
irqbalance --foreground --powerthresh=0
# C-state 관점:
# 전력 인식 ON: 유휴 CPU → C3/C6 진입 → 전력 절감
# 전력 인식 OFF: 모든 CPU에 분산 → C1에서 대기 → 레이턴시 감소
설정과 운영
# 서비스 관리
systemctl status irqbalance
systemctl enable --now irqbalance
# 현재 분산 상태를 사람이 읽기 쉬운 형태로 확인
# irqbalance 1.4+ 에서 --debug 모드
irqbalance --foreground --debug 2&>1 | head -50
# 주요 설정 파일: /etc/sysconfig/irqbalance 또는 /etc/default/irqbalance
# IRQBALANCE_ONESHOT=yes — 한 번만 분산 후 종료 (부팅 시 초기 배치용)
# IRQBALANCE_BANNED_CPUS=0x0c — CPU 2,3을 분산 대상에서 제외 (bitmask)
# IRQBALANCE_BANNED_CPULIST=2,3 — CPU 목록으로 제외 (1.8+)
# IRQBALANCE_ARGS="--hintpolicy=exact --powerthresh=0"
# 주요 커맨드라인 옵션
irqbalance \
--hintpolicy=exact \ # exact|subset|ignore — 드라이버 hint 정책
--powerthresh=0 \ # 0=성능 모드, 높을수록 공격적 절전
--banirq=42 \ # 특정 IRQ를 분산 대상에서 제외
--banscript=/path \ # 동적 제외 판단 스크립트
--policyscript=/path \ # 커스텀 분산 정책 스크립트
--deepestcache=2 \ # 분산 단위 캐시 레벨 (1=L1, 2=L2, 3=L3)
--journal \ # systemd journal로 로그 출력
--interval=10 # 재분산 주기 (초, 기본: 10)
특정 IRQ 격리(Isolation)와 제외
실시간 워크로드나 DPDK 같은 전용 CPU가 필요한 환경에서는 irqbalance로부터 특정 IRQ나 CPU를 격리해야 합니다:
# 방법 1: 특정 IRQ를 irqbalance에서 제외
# /etc/sysconfig/irqbalance 또는 커맨드라인
irqbalance --banirq=42 --banirq=43
# 방법 2: 특정 CPU를 irqbalance에서 제외
# CPU 4-7을 실시간 전용으로 격리
IRQBALANCE_BANNED_CPULIST=4-7
# 방법 3: 커널 부트 파라미터로 CPU 격리 (isolcpus)
# GRUB_CMDLINE_LINUX="isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7"
# irqbalance는 isolcpus를 자동으로 인식하여 해당 CPU 제외
# 방법 4: 커널 드라이버에서 IRQF_NOBALANCING 플래그
/* 드라이버에서 irqbalance 이동 방지 */
request_irq(irq, my_handler,
IRQF_NOBALANCING | IRQF_NO_THREAD,
"my_realtime_dev", dev);
/* affinity hint 설정 — irqbalance가 참고 */
static struct cpumask hint_mask;
cpumask_set_cpu(2, &hint_mask);
irq_set_affinity_hint(irq, &hint_mask);
/* 드라이버 종료 시 hint 해제 */
irq_set_affinity_hint(irq, NULL);
irqbalance vs 수동 Affinity 설정
| 기준 | irqbalance 자동 | 수동 smp_affinity |
|---|---|---|
| NUMA 인식 | 자동 (토폴로지 감지) | 관리자가 직접 계산 |
| 부하 적응 | 10초 주기 재분산 | 정적 (reboot 시 초기화) |
| NIC RSS 최적화 | hint_policy=exact 사용 | set_irq_affinity 스크립트 |
| 실시간 워크로드 | banirq/banned_cpus로 제외 | 직접 제어 (결정론적) |
| CPU 핫플러그(Hotplug) | 자동 재배치(Relocation) | 수동 재설정 필요 |
| 디버깅 용이성 | 동적 변경으로 추적 어려움 | 고정되어 추적 쉬움 |
| 적합한 환경 | 범용 서버, 클라우드 | HPC, 실시간, DPDK, 저지연 트레이딩 |
irqbalance와 수동 affinity를 동시에 사용하면 충돌합니다. 수동으로 smp_affinity를 설정해도 irqbalance가 다음 주기에 덮어씁니다. 수동 설정이 필요한 IRQ는 반드시 --banirq로 제외하거나, irqbalance를 비활성화하세요.
네트워크 인터럽트 분산 최적화
고성능 네트워크 환경에서는 irqbalance만으로 충분하지 않을 수 있습니다. NIC의 RSS(Receive Side Scaling), RPS(Receive Packet Steering), RFS(Receive Flow Steering)와 조합하여 최적화합니다:
# 1. NIC RSS 큐 수 확인 (MSI-X 인터럽트 수)
ethtool -l eth0
# Channel parameters for eth0:
# Pre-set maximums:
# Combined: 64
# Current hardware settings:
# Combined: 8
# 2. RSS 큐별 인터럽트 확인
grep eth0 /proc/interrupts
# 128: 1234567 0 0 0 eth0-TxRx-0
# 129: 0 2345678 0 0 eth0-TxRx-1
# 130: 0 0 3456789 0 eth0-TxRx-2
# ...
# 3. irqbalance가 RSS 큐를 NUMA-local CPU에 배치하는지 확인
for irq in $(grep eth0 /proc/interrupts | awk '{print $1}' | tr -d ':'); do
echo "IRQ $irq → CPU mask: $(cat /proc/irq/$irq/smp_affinity_list)"
done
# 4. NIC NUMA 노드 확인
cat /sys/class/net/eth0/device/numa_node
# 0 ← irqbalance는 이 NUMA 노드의 CPU에 우선 배치
# 5. RPS로 소프트웨어 분산 보충 (RSS 큐가 부족할 때)
echo ff > /sys/class/net/eth0/queues/rx-0/rps_cpus
echo 4096 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
10Gbps 이상 NIC에서는 irqbalance --hintpolicy=exact를 사용하고, NIC 드라이버의 set_irq_affinity 스크립트(Intel ixgbe/ice, Mellanox mlx5 등에 포함)로 초기 배치 후 irqbalance가 유지하도록 하는 것이 권장됩니다.
모니터링과 트러블슈팅
# irqbalance 동작 상태 확인
# 소켓 기반 인터페이스 (irqbalance 1.4+)
echo settings | socat - UNIX-CONNECT:/var/run/irqbalance/irqbalance1234.sock
echo setup | socat - UNIX-CONNECT:/var/run/irqbalance/irqbalance1234.sock
# 인터럽트 분포 실시간 모니터링
watch -n 1 'cat /proc/interrupts | head -5; echo "---"; grep eth0 /proc/interrupts'
# 인터럽트 비율 변화 측정 (초당 발생 수)
# 방법: 1초 간격으로 /proc/interrupts 차이 계산
sar -I ALL 1 5 # sysstat 패키지 필요
# perf로 인터럽트 핫스팟 분석
perf stat -e irq:irq_handler_entry -a sleep 10
perf record -e irq:irq_handler_entry -ag sleep 10
perf report --sort comm,dso,symbol
# 트러블슈팅 체크리스트
# 1. irqbalance가 실행 중인가?
pidof irqbalance || echo "irqbalance is NOT running"
# 2. 특정 IRQ가 한 CPU에 고정되어 있는가?
cat /proc/irq/128/effective_affinity_list
# 3. 드라이버가 IRQF_NOBALANCING을 설정했는가?
cat /proc/irq/128/actions # nobalancing 플래그 확인
# 4. affinity가 변경 가능한가? (일부 인터럽트는 고정)
echo 3 > /proc/irq/128/smp_affinity # "Permission denied" → managed irq
커널 4.x 이후 managed_irq 인터럽트(주로 MSI-X blk-mq, NVMe)는 커널이 직접 affinity를 관리합니다. irqbalance는 이러한 인터럽트를 자동으로 건너뜁니다. /proc/irq/<N>/effective_affinity로 실제 적용된 affinity를 확인할 수 있습니다.
실전 디바이스 드라이버 예제
이론을 넘어 실제 디바이스 드라이버에서 인터럽트를 어떻게 처리하는지 end-to-end 예제를 통해 학습합니다.
UART 드라이버: IRQ부터 Bottom Half까지
UART 드라이버는 인터럽트 기반 I/O의 전형적인 예입니다. 데이터 수신 시 인터럽트가 발생하고, top half에서 하드웨어 레지스터를 읽은 뒤, bottom half에서 tty 레이어로 데이터를 전달합니다:
/* drivers/tty/serial/my_uart.c */
#include <linux/serial_core.h>
#include <linux/interrupt.h>
#include <linux/tty_flip.h>
struct my_uart_port {
struct uart_port port;
void __iomem *base;
int irq;
struct tasklet_struct rx_tasklet;
unsigned char rx_buffer[256];
int rx_count;
};
/* Top Half: 인터럽트 핸들러 */
static irqreturn_t my_uart_irq(int irq, void *dev_id)
{
struct my_uart_port *up = dev_id;
u32 status;
/* 1. 인터럽트 원인 확인 */
status = readl(up->base + UART_STATUS);
if (!(status & UART_INT_PENDING))
return IRQ_NONE; /* 공유 IRQ: 우리 인터럽트 아님 */
/* 2. 긴급 처리: 하드웨어 FIFO overflow 방지 */
if (status & UART_RX_READY) {
int count = 0;
while (readl(up->base + UART_STATUS) & UART_RX_READY) {
up->rx_buffer[count++] = readl(up->base + UART_DATA);
if (count >= sizeof(up->rx_buffer))
break; /* 버퍼 오버플로 방지 */
}
up->rx_count = count;
/* 3. Bottom Half 예약 */
tasklet_schedule(&up->rx_tasklet);
}
/* 4. 인터럽트 클리어 (하드웨어 종속) */
writel(status, up->base + UART_INT_CLEAR);
return IRQ_HANDLED;
}
/* Bottom Half: Tasklet에서 실행 */
static void my_uart_rx_tasklet(unsigned long data)
{
struct my_uart_port *up = (struct my_uart_port *)data;
struct tty_port *tport = &up->port.state->port;
int i;
/* TTY 레이어로 데이터 전달 (슬립 불가) */
for (i = 0; i < up->rx_count; i++) {
if (!tty_insert_flip_char(tport, up->rx_buffer[i], TTY_NORMAL))
break; /* TTY 버퍼 가득 참 */
}
/* TTY 플립 버퍼 처리 예약 */
tty_flip_buffer_push(tport);
up->rx_count = 0;
}
/* 드라이버 초기화 */
static int my_uart_probe(struct platform_device *pdev)
{
struct my_uart_port *up;
int ret;
up = devm_kzalloc(&pdev->dev, sizeof(*up), GFP_KERNEL);
if (!up)
return -ENOMEM;
/* 1. 리소스 획득 */
up->base = devm_platform_ioremap_resource(pdev, 0);
up->irq = platform_get_irq(pdev, 0);
/* 2. Tasklet 초기화 */
tasklet_init(&up->rx_tasklet, my_uart_rx_tasklet,
(unsigned long)up);
/* 3. IRQ 등록 */
ret = request_irq(up->irq, my_uart_irq,
IRQF_SHARED, "my_uart", up);
if (ret) {
tasklet_kill(&up->rx_tasklet);
return ret;
}
/* 4. UART 포트 등록 */
ret = uart_add_one_port(&my_uart_driver, &up->port);
if (ret) {
free_irq(up->irq, up);
tasklet_kill(&up->rx_tasklet);
}
return ret;
}
/* 드라이버 정리 */
static int my_uart_remove(struct platform_device *pdev)
{
struct my_uart_port *up = platform_get_drvdata(pdev);
uart_remove_one_port(&my_uart_driver, &up->port);
free_irq(up->irq, up);
tasklet_kill(&up->rx_tasklet); /* 대기 중인 tasklet 완료 대기 */
return 0;
}
핵심 패턴: Top half는 하드웨어 레지스터만 읽고(최소 작업), bottom half(tasklet)에서 복잡한 처리를 수행합니다. tasklet_kill()은 제거 전 반드시 호출하여 진행 중인 tasklet이 완료되도록 보장해야 합니다.
네트워크 카드: NAPI + Interrupt Coalescing
고속 네트워크 카드는 패킷마다 인터럽트를 발생시키면 CPU가 인터럽트 처리에 압도됩니다. NAPI(New API)는 인터럽트와 폴링(Polling)을 혼합하여 효율을 높입니다:
/* drivers/net/ethernet/my_netdev.c */
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/interrupt.h>
struct my_netdev_priv {
struct napi_struct napi;
void __iomem *regs;
int irq;
};
/* Top Half: 인터럽트 핸들러 (매우 짧음) */
static irqreturn_t my_netdev_irq(int irq, void *dev_id)
{
struct net_device *ndev = dev_id;
struct my_netdev_priv *priv = netdev_priv(ndev);
u32 status;
status = readl(priv->regs + REG_INT_STATUS);
if (!status)
return IRQ_NONE;
if (status & INT_RX_DONE) {
/* 1. 인터럽트 비활성화 (하드웨어 레벨) */
writel(0, priv->regs + REG_INT_ENABLE);
/* 2. NAPI 폴링 예약 (softirq에서 실행) */
if (napi_schedule_prep(&priv->napi))
__napi_schedule(&priv->napi);
}
writel(status, priv->regs + REG_INT_CLEAR);
return IRQ_HANDLED;
}
/* NAPI poll 함수: softirq 컨텍스트에서 실행 */
static int my_netdev_poll(struct napi_struct *napi, int budget)
{
struct my_netdev_priv *priv = container_of(napi, struct my_netdev_priv, napi);
int work_done = 0;
/* 패킷을 budget 개수만큼 처리 (공평성 보장) */
while (work_done < budget) {
struct sk_buff *skb;
u32 status = readl(priv->regs + REG_RX_STATUS);
if (!(status & RX_PKT_READY))
break; /* 더 이상 패킷 없음 */
/* 패킷 수신 및 프로토콜 스택 전달 */
skb = my_netdev_receive_packet(priv);
if (skb) {
napi_gro_receive(napi, skb); /* GRO: 패킷 병합 최적화 */
work_done++;
}
}
/* 처리 완료: 인터럽트 재활성화 */
if (work_done < budget) {
napi_complete_done(napi, work_done);
writel(INT_RX_DONE, priv->regs + REG_INT_ENABLE);
}
return work_done;
}
/* Interrupt Coalescing: 여러 이벤트를 묶어 인터럽트 감소 */
static void my_netdev_set_coalesce(struct my_netdev_priv *priv)
{
/* 64개 패킷 또는 100μs 중 먼저 도달하는 조건에 인터럽트 */
writel(64, priv->regs + REG_INT_COALESCE_COUNT);
writel(100, priv->regs + REG_INT_COALESCE_USEC);
}
NAPI 동작 원리: 첫 번째 패킷에서 인터럽트가 발생하면, 이후 인터럽트를 비활성화하고 폴링 모드로 전환합니다. 패킷이 없으면 다시 인터럽트 모드로 복귀합니다. 이렇게 하여 고부하 시 인터럽트 횟수를 획기적으로 줄입니다(10GbE에서 초당 수백만 개 → 수만 개).
DMA + Interrupt 조합
DMA 엔진과 인터럽트를 함께 사용하는 실제 패턴입니다. DMA 전송 완료 시 인터럽트가 발생합니다:
/* drivers/dma/my_dma_driver.c */
#include <linux/dmaengine.h>
#include <linux/interrupt.h>
struct my_device {
struct dma_chan *dma_chan;
dma_addr_t dma_addr;
void *cpu_addr;
size_t size;
struct completion dma_complete;
};
/* DMA 완료 콜백: softirq 또는 tasklet 컨텍스트 */
static void my_dma_callback(void *param)
{
struct my_device *dev = param;
/* DMA 완료 신호 */
complete(&dev->dma_complete);
}
/* DMA 전송 시작 */
static int my_device_dma_transfer(struct my_device *dev,
dma_addr_t src, size_t len)
{
struct dma_async_tx_descriptor *desc;
dma_cookie_t cookie;
/* 1. DMA 전송 기술자 준비 */
desc = dmaengine_prep_dma_memcpy(dev->dma_chan, dev->dma_addr,
src, len, DMA_PREP_INTERRUPT);
if (!desc)
return -ENOMEM;
/* 2. 완료 콜백 설정 */
desc->callback = my_dma_callback;
desc->callback_param = dev;
/* 3. DMA 전송 제출 */
cookie = dmaengine_submit(desc);
if (dma_submit_error(cookie))
return -EIO;
/* 4. DMA 시작 */
dma_async_issue_pending(dev->dma_chan);
/* 5. 완료 대기 (슬립 가능한 컨텍스트에서만) */
if (!wait_for_completion_timeout(&dev->dma_complete,
msecs_to_jiffies(5000))) {
dev_err(dev->dev, "DMA timeout\n");
return -ETIMEDOUT;
}
return 0;
}
/* 또 다른 패턴: 인터럽트 핸들러에서 직접 DMA 상태 확인 */
static irqreturn_t my_device_irq_dma(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
u32 status = readl(dev->regs + DMA_STATUS);
if (status & DMA_COMPLETE) {
/* DMA 완료: 캐시 일관성 보장 */
dma_sync_single_for_cpu(dev->dev, dev->dma_addr,
dev->size, DMA_FROM_DEVICE);
/* 완료 처리 (예: workqueue로 전달) */
schedule_work(&dev->process_work);
writel(DMA_COMPLETE, dev->regs + DMA_STATUS); /* 클리어 */
return IRQ_HANDLED;
}
if (status & DMA_ERROR) {
dev_err(dev->dev, "DMA error: 0x%x\n", status);
writel(DMA_ERROR, dev->regs + DMA_STATUS);
return IRQ_HANDLED;
}
return IRQ_NONE;
}
DMA 캐시 일관성(Cache Coherency): DMA 전송 전후로 dma_sync_* 함수를 반드시 호출해야 합니다. CPU 캐시와 DMA 메모리 간 불일치를 방지하기 위함입니다. 특히 ARM 같은 아키텍처에서는 누락 시 데이터 손상이 발생할 수 있습니다.
예제 패턴 비교
| 드라이버 유형 | Top Half | Bottom Half | 주요 고려사항 |
|---|---|---|---|
| UART (저속) | FIFO 읽기 | Tasklet → TTY | FIFO overflow 방지, 직렬 처리 |
| 네트워크 (고속) | 인터럽트 비활성화 | NAPI 폴링 (softirq) | 인터럽트 최소화, GRO 최적화 |
| 블록 I/O | 상태 확인 | Workqueue | 슬립 가능, 긴 처리 시간 |
| DMA 기반 | 완료 확인 | Callback 또는 Workqueue | 캐시 일관성, 타임아웃 처리 |
| 실시간 (Audio) | Threaded IRQ | N/A | 레이턴시 예측성, 우선순위(Priority) |
IPI (Inter-Processor Interrupt)
IPI(Inter-Processor Interrupt)는 SMP 시스템에서 한 CPU가 다른 CPU에게 보내는 특수한 인터럽트입니다. 스케줄러(Scheduler) 밸런싱(reschedule IPI), TLB 캐시 일관성(TLB flush IPI), 원격 함수 호출(smp_call_function) 등 CPU 간 협조가 필요한 거의 모든 작업에 IPI가 관여합니다.
상세 문서: x86 ICR/APIC 아키텍처, IPI 벡터 유형, reschedule IPI, TLB flush IPI, smp_call_function API, IPI 성능 분석, SMP 부팅 시 IPI 활용은 IPI 페이지에서 다룹니다.
NMI (Non-Maskable Interrupt)
NMI는 CPU의 마스킹 메커니즘(cli/local_irq_disable())으로 비활성화할 수 없는 특수한 인터럽트입니다. 하드웨어 오류 감지, 커널 교착상태(hardlockup) 탐지, 성능 프로파일링(Profiling), 디버거 진입 등 크리티컬한 용도에 사용됩니다.
상세 문서: NMI 소스, x86 NMI 아키텍처, hardlockup watchdog, PMU NMI, NMI 핸들링 제약, NMI 디버깅은 NMI 페이지에서 다룹니다.
IRQ 디버깅
인터럽트 관련 문제는 타이밍에 민감하여 재현이 어렵습니다. 다음 도구들로 체계적으로 진단할 수 있습니다.
/proc/interrupts 해석
# 인터럽트 카운터 확인
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 0: 45 0 0 0 IR-IO-APIC 2-edge timer
# 16: 12345 0 0 0 IR-IO-APIC 16-fastedge ahci[0]
# 142: 0 8765432 0 0 IR-PCI-MSI-X 0-edge nvme0q1
#
# 열 해석: IRQ번호, CPU별 카운트, 컨트롤러, 트리거 유형, 디바이스명
# 특정 CPU에 카운트가 집중되면 affinity 조정 필요
# softirq 카운터 확인
cat /proc/softirqs
# CPU0 CPU1 CPU2 CPU3
# HI: 5 0 0 0
# TIMER: 1234567 1234568 1234569 1234570
# NET_TX: 1234 45 0 0
# NET_RX: 5678901 12345 0 0
# NET_RX가 한 CPU에 집중되면 RPS/RFS 또는 RSS 설정 필요
ftrace IRQ 트레이싱
# IRQ 핸들러 실행 시간 추적
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
# softirq 실행 추적
echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/softirq_exit/enable
# 결과 확인
cat /sys/kernel/debug/tracing/trace
# irq_handler_entry: irq=16 name=ahci[0]
# irq_handler_exit: irq=16 ret=handled
# softirq_entry: vec=3 [action=NET_RX]
# softirq_exit: vec=3 [action=NET_RX]
# irqsoff tracer: 인터럽트 비활성 최대 시간 측정
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/tracing_max_latency
트러블슈팅
인터럽트 관련 문제는 간헐적이고 타이밍에 민감하여 진단이 어렵습니다. 다음은 일반적인 문제와 체계적인 해결 방법입니다.
일반적인 문제와 해결책
| 문제 | 증상 | 원인 | 해결 방법 |
|---|---|---|---|
| IRQ 불균형 | 한 CPU만 100% 사용률 | 모든 인터럽트가 CPU0에 집중 | irqbalance 시작, 또는 수동 affinity 분산 |
| Interrupt Storm | 시스템 응답 없음, %irq 80%+ |
하드웨어 오류, 잘못된 드라이버 | 해당 IRQ 비활성화, 하드웨어 점검 |
| 공유 IRQ 경쟁 | IRQ_NONE 경고, 성능 저하 | 여러 디바이스가 같은 IRQ 공유 | MSI/MSI-X 사용, 또는 디바이스 재배치 |
| 레이턴시 스파이크 | 간헐적 지연, 타임아웃 | 긴 인터럽트 핸들러, 선점 불가 구간 | Threaded IRQ 전환, ftrace로 핫스팟 분석 |
| Lost Interrupt | 디바이스 타임아웃, I/O 정지 | 인터럽트 마스크 누락, 하드웨어 버그 | 폴링 모드 전환, 드라이버 패치(Patch) |
| NMI Watchdog 타임아웃 | NMI watchdog: BUG: soft lockup |
인터럽트 비활성 구간 너무 김(>20초) | 긴 루프에 cond_resched() 추가 |
| Spurious IRQ | irq N: nobody cared 커널 로그 |
공유 IRQ에서 모든 핸들러가 IRQ_NONE 반환 | 드라이버 핸들러 수정, IRQF_SHARED 확인 |
IRQ 불균형 진단 및 해결
한 CPU에 인터럽트가 집중되면 병목이 발생합니다. 다음 절차로 진단하고 해결합니다:
# 1. CPU별 인터럽트 분포 확인
mpstat -P ALL 1 5
# %irq 컬럼이 한 CPU에서만 높으면 불균형
# 2. 특정 IRQ가 어느 CPU에서 처리되는지 확인
watch -n 1 'cat /proc/interrupts | grep -E "16:|CPU"'
# 3. irqbalance가 실행 중인지 확인
systemctl status irqbalance
# 4. 수동 분산 (irqbalance 중지 후)
systemctl stop irqbalance
# IRQ 16을 CPU 0-3에 분산 (비트마스크 0x0F)
echo 0f > /proc/irq/16/smp_affinity
# 5. 효과 검증 (5초 동안 모니터링)
sar -I ALL 1 5
자동 vs 수동: 일반 서버는 irqbalance에 맡기는 것이 좋습니다. 단, 실시간 워크로드나 DPDK 같은 전용 환경에서는 수동으로 affinity를 고정하여 예측 가능성을 높입니다.
Interrupt Storm 감지 및 완화
Interrupt storm은 초당 수백만 개의 인터럽트가 발생하여 시스템이 마비되는 현상입니다:
# 1. Interrupt storm 감지
# 특정 IRQ의 카운트가 초당 수만 개 이상 증가하는지 확인
watch -d -n 1 'cat /proc/interrupts | head -20'
# 2. 문제 IRQ 식별
# 예: IRQ 19번이 폭증
cat /proc/interrupts | grep '^ *19:'
# 19: 123456789 0 0 0 IO-APIC 19-fasteoi eth0
# 3. 긴급 완화: 해당 IRQ 비활성화 (디바이스 사용 중단)
echo 0 > /proc/irq/19/smp_affinity
# 또는 디바이스 드라이버 언로드
rmmod e1000e
# 4. 근본 원인 분석
dmesg | grep -i 'irq 19'
# 하드웨어 오류, 잘못된 드라이버 설정 확인
# 5. APIC 에러 확인 (하드웨어 이슈)
grep -i apic /var/log/kern.log
/* 커널 레벨 interrupt storm 감지 (kernel/irq/spurious.c) */
/* 100ms 동안 100,000번 이상 인터럽트 발생 시 자동 비활성화 */
static void note_interrupt(struct irq_desc *desc, irqreturn_t action_ret)
{
if (action_ret == IRQ_NONE) {
desc->irqs_unhandled++;
if (desc->irqs_unhandled > 100000) {
printk(KERN_WARNING "irq %d: nobody cared\n", desc->irq);
__report_bad_irq(desc, action_ret);
desc->istate |= IRQS_SPURIOUS_DISABLED;
}
}
}
공유 IRQ 디버깅
공유 IRQ 환경에서는 여러 디바이스가 같은 인터럽트 라인을 사용합니다. 핸들러가 제대로 구현되지 않으면 문제가 발생합니다:
# 1. 공유 IRQ 확인
cat /proc/interrupts | awk '$NF ~ /-edge|fasteoi/ {print}'
# 여러 디바이스가 같은 줄에 나열되면 공유
# 2. Spurious IRQ 경고 확인
dmesg | grep 'nobody cared'
# 출력 예: irq 19: nobody cared (try booting with the "irqpoll" option)
# 3. irqpoll 옵션으로 부팅 (긴급 회피)
# /etc/default/grub에 추가:
# GRUB_CMDLINE_LINUX="irqpoll"
# 이후 update-grub && reboot
# 4. MSI/MSI-X로 전환 (근본 해결)
lspci -vvv | grep -A 10 'Ethernet'
# Capabilities: [MSI] 또는 [MSI-X] 확인
# 드라이버에서 MSI 활성화 여부 확인
cat /sys/class/net/eth0/device/msi_bus
# 1이면 MSI 사용 중
공유 IRQ 핸들러 규칙: IRQF_SHARED 플래그를 사용하는 핸들러는 반드시 자신의 디바이스에서 발생한 인터럽트인지 확인하고, 아니면 IRQ_NONE을 반환해야 합니다. 그렇지 않으면 다른 디바이스의 인터럽트를 가로채게 됩니다.
레이턴시 스파이크 분석
간헐적인 레이턴시 급증은 인터럽트 핸들러가 너무 오래 실행되거나, 선점이 지연되는 경우 발생합니다:
# 1. 인터럽트 비활성 구간 최대 시간 측정
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 10
cat /sys/kernel/debug/tracing/tracing_max_latency
# 출력 예: 523 (단위: μs) → 523μs 동안 인터럽트 비활성
# 트레이스 확인 (어디서 오래 걸렸는지)
cat /sys/kernel/debug/tracing/trace | head -50
# 2. 특정 함수의 실행 시간 측정
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo do_IRQ > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-duration
cat /sys/kernel/debug/tracing/trace
# 3. cyclictest로 실시간 레이턴시 측정 (RT 시스템)
cyclictest -p 95 -m -n -i 200 -l 10000
# Max 레이턴시가 100μs 이상이면 문제
# 4. perf로 인터럽트 핸들러 프로파일링
perf record -e 'irq:*' -ag sleep 10
perf report --stdio | grep -A 20 'do_IRQ'
# 5. 긴 인터럽트 비활성 구간 찾기
echo 'irqsoff' > /sys/kernel/debug/tracing/current_tracer
echo 500 > /sys/kernel/debug/tracing/tracing_thresh # 500μs 이상만
cat /sys/kernel/debug/tracing/trace
NMI Watchdog 타임아웃 대응
NMI watchdog은 CPU가 20초 이상 선점되지 않으면 soft lockup을 감지합니다:
# 1. NMI watchdog 로그 확인
dmesg | grep -i 'watchdog\|soft lockup'
# 출력 예: NMI watchdog: BUG: soft lockup - CPU#2 stuck for 22s!
# 2. 스택 트레이스에서 문제 함수 식별
# 로그에 Call Trace가 포함되어 있음
# 3. 문제 함수에 선점 포인트 추가
/* 문제 코드: 긴 루프에서 선점 불가 */
for (i = 0; i < 1000000; i++) {
process_item(i); /* 20초 이상 소요 가능 */
}
/* 수정: 주기적으로 선점 허용 */
for (i = 0; i < 1000000; i++) {
process_item(i);
if (i % 1000 == 0)
cond_resched(); /* 필요 시 스케줄링 허용 */
}
# 4. NMI watchdog 임계값 조정 (임시 회피)
sysctl -w kernel.watchdog_thresh=30 # 기본 10초 → 30초
# 5. NMI watchdog 비활성화 (디버깅 목적만)
echo 0 > /proc/sys/kernel/nmi_watchdog
진단 체크리스트
인터럽트 문제를 체계적으로 진단하는 8단계 체크리스트:
#!/bin/bash
# 인터럽트 진단 스크립트
echo "=== 1. 인터럽트 분포 확인 ==="
mpstat -P ALL 1 3 | grep -E 'CPU|Average'
echo -e "\n=== 2. 고빈도 IRQ 식별 ==="
cat /proc/interrupts | awk 'NR==1 || $2 > 100000 {print}'
echo -e "\n=== 3. Spurious IRQ 확인 ==="
dmesg | grep -i 'nobody cared\|spurious' | tail -10
echo -e "\n=== 4. irqbalance 상태 ==="
systemctl is-active irqbalance
cat /proc/irq/default_smp_affinity
echo -e "\n=== 5. MSI/MSI-X 사용 여부 ==="
lspci -vvv | grep -i 'msi' | head -10
echo -e "\n=== 6. 인터럽트 비활성 최대 시간 ==="
cat /sys/kernel/debug/tracing/tracing_max_latency 2>/dev/null || echo "ftrace 비활성"
echo -e "\n=== 7. NMI watchdog 이벤트 ==="
dmesg | grep -i 'nmi.*watchdog\|soft lockup' | tail -5
echo -e "\n=== 8. CPU별 IRQ 처리 시간 ==="
sar -I SUM 1 3
성능 이슈 우선순위: (1) Interrupt storm → 시스템 마비 위험, 즉시 조치. (2) IRQ 불균형 → 처리량 저하, irqbalance로 해결. (3) 레이턴시 스파이크 → 실시간성 문제, threaded IRQ 고려. (4) 공유 IRQ → 성능 하락, MSI/MSI-X 전환.
IRQ 플로우 핸들러 상세
Linux genirq 프레임워크는 인터럽트 컨트롤러의 하드웨어 특성에 따라 다른 플로우 핸들러(flow handler)를 사용합니다. 플로우 핸들러는 인터럽트가 하드웨어에서 발생한 후 실제 디바이스 핸들러(irqaction)를 호출하기까지의 흐름을 제어합니다. 각 핸들러는 마스킹, ACK, EOI(End of Interrupt) 타이밍이 다릅니다.
플로우 핸들러 코드 분석
/* kernel/irq/chip.c — handle_level_irq (레벨 트리거) */
void handle_level_irq(struct irq_desc *desc)
{
struct irq_chip *chip = desc->irq_data.chip;
raw_spin_lock(&desc->lock);
mask_ack_irq(desc); /* ① 마스크 + ACK (레벨 유지 방지) */
if (!irq_may_run(desc))
goto out;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
goto out;
}
handle_irq_event(desc); /* ② 핸들러 체인 실행 */
cond_unmask_irq(desc); /* ③ 언마스크 (재진입 허용) */
out:
raw_spin_unlock(&desc->lock);
}
/* handle_edge_irq (엣지 트리거) — 핸들러 실행 중 도착한 엣지 감지 */
void handle_edge_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
if (!irq_may_run(desc)) {
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out;
}
chip->irq_ack(&desc->irq_data); /* ① ACK만 (마스크 안 함) */
do {
if (unlikely(!desc->action)) {
mask_irq(desc);
goto out;
}
if (unlikely(desc->istate & IRQS_PENDING)) {
if (!irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_irq(desc);
}
handle_irq_event(desc); /* ② 핸들러 실행 */
} while ((desc->istate & IRQS_PENDING) && /* ③ 실행 중 재도착 확인 */
!irqd_irq_disabled(&desc->irq_data));
out:
raw_spin_unlock(&desc->lock);
}
/* handle_fasteoi_irq — 현대 컨트롤러용 (GIC, APIC) */
void handle_fasteoi_irq(struct irq_desc *desc)
{
struct irq_chip *chip = desc->irq_data.chip;
raw_spin_lock(&desc->lock);
if (!irq_may_run(desc))
goto out;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
mask_irq(desc);
goto out;
}
handle_irq_event(desc); /* ① 바로 핸들러 실행 (마스크 없음!) */
out:
chip->irq_eoi(&desc->irq_data); /* ② EOI만 전송 */
raw_spin_unlock(&desc->lock);
}
코드 설명
- handle_level_irq()
kernel/irq/chip.c에 구현된 레벨 트리거 플로우 핸들러입니다. 레벨 인터럽트는 신호가 유지되는 동안 계속 발생하므로, 핸들러 실행 전에 반드시mask_ack_irq()로 마스킹해야 합니다. 처리 후cond_unmask_irq()로 다시 활성화합니다. 호출 경로:common_interrupt()→handle_irq()→handle_level_irq()→handle_irq_event()→ 등록된 핸들러 체인. - handle_edge_irq()엣지 트리거 플로우 핸들러입니다. 엣지 인터럽트는 순간적 신호이므로 마스킹 없이 ACK만 수행합니다. 핵심은
do-while루프로, 핸들러 실행 중 도착한 새로운 엣지를IRQS_PENDING비트로 감지하여 재처리합니다. 이 루프가 없으면 실행 중 도착한 인터럽트를 놓칠 수 있습니다. - handle_fasteoi_irq()GIC, APIC 등 현대 인터럽트 컨트롤러용 플로우 핸들러입니다. 마스킹 없이 바로 핸들러를 실행하고, 완료 후
chip->irq_eoi()만 호출하므로 오버헤드가 최소입니다. MSI/MSI-X도 이 핸들러를 사용합니다. 대부분의 현대 시스템에서 기본 플로우 핸들러입니다.
플로우 핸들러 비교
| 특성 | handle_level_irq | handle_edge_irq | handle_fasteoi_irq |
|---|---|---|---|
| 트리거 유형 | 레벨 (High/Low) | 엣지 (Rising/Falling) | 컨트롤러 의존 |
| 마스킹 | 핸들러 전 mask, 후 unmask | 필요 시만 mask | 마스킹 없음 |
| ACK 타이밍 | mask와 동시 | 핸들러 전 | 없음 (EOI 대체) |
| EOI | 없음 | 없음 | 핸들러 후 |
| 재진입 처리 | 마스크로 차단 | PENDING 비트 루프 | 컨트롤러 위임 |
| 오버헤드 | 중간 (mask/unmask) | 높음 (재확인 루프) | 최소 (EOI만) |
| 사용 하드웨어 | GPIO 레벨, I2C | GPIO 엣지, ISA | GIC, APIC, MSI |
irq_set_chip_and_handler()로 플로우 핸들러를 지정합니다. 대부분의 현대 SoC는 handle_fasteoi_irq를 사용하며, GPIO 컨트롤러만 handle_level_irq/handle_edge_irq를 사용합니다.
GIC 아키텍처 (ARM)
GIC(Generic Interrupt Controller)는 ARM 아키텍처의 표준 인터럽트 컨트롤러입니다. GICv2에서 시작하여 GICv3/v4로 발전하면서 수천 개의 인터럽트와 가상화(Virtualization)를 지원합니다. GIC는 Distributor, Redistributor, CPU Interface 세 가지 주요 컴포넌트로 구성됩니다.
GIC 인터럽트 유형
| 유형 | ID 범위 | 범위 | 설명 |
|---|---|---|---|
| SGI (Software Generated Interrupt) | 0-15 | Per-CPU | IPI 용도, GICD_SGIR로 생성 |
| PPI (Private Peripheral Interrupt) | 16-31 | Per-CPU | CPU 타이머(Timer), PMU 등 CPU 전용 인터럽트 |
| SPI (Shared Peripheral Interrupt) | 32-1019 | 전역 | 일반 디바이스 인터럽트, affinity 설정 가능 |
| LPI (Locality-specific Peripheral Interrupt) | 8192+ | 전역 | GICv3 전용, ITS 경유, MSI/MSI-X |
/* drivers/irqchip/irq-gic-v3.c — GICv3 초기화 */
static int gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride)
{
u32 typer;
/* Distributor 설정 */
typer = readl_relaxed(dist_base + GICD_TYPER);
gic_data.rdists.gicd_typer = typer;
gic_data.irq_nr = GICD_TYPER_SPIS(typer); /* SPI 개수 */
if (gic_data.irq_nr > 1020)
gic_data.irq_nr = 1020;
/* GICv3 우선순위 그룹 */
gic_data.prio_bits = GICD_TYPER_NUM_LPIS(typer);
/* Distributor enable: Group0 + Group1 */
writel_relaxed(GICD_CTLR_ARE_NS | GICD_CTLR_ENABLE_G1A |
GICD_CTLR_ENABLE_G1, dist_base + GICD_CTLR);
/* SPI 라우팅: Affinity Routing Enable (ARE) */
gic_dist_config(dist_base, gic_data.irq_nr, NULL);
return 0;
}
/* GICv3 CPU Interface: System register 접근 */
static void gic_cpu_sys_reg_init(void)
{
/* ICC_SRE_EL1: System Register Enable */
gic_write_sre(ICC_SRE_EL1_SRE);
/* ICC_PMR_EL1: Priority Mask (모든 우선순위 허용) */
gic_write_pmr(DEFAULT_PMR_VALUE);
/* ICC_CTLR_EL1: EOI mode 설정 */
gic_write_ctlr(ICC_CTLR_EL1_EOImode_drop);
/* ICC_IGRPEN1_EL1: Group 1 인터럽트 enable */
gic_write_grpen1(1);
}
ICC_*_EL1)로 전환하여 ACK/EOI 속도가 향상됩니다. 또한 GICv3의 Affinity Routing(ARE)은 64비트 MPIDR 기반으로 최대 256개 CPU를 지원하며, ITS를 통해 PCIe MSI/MSI-X를 LPI로 변환합니다.
APIC 아키텍처 (x86)
x86 시스템은 APIC(Advanced Programmable Interrupt Controller) 아키텍처를 사용합니다. 각 CPU에 내장된 LAPIC(Local APIC)과 I/O 장치의 인터럽트를 관리하는 I/O APIC으로 구성됩니다. 현대 x86에서는 xAPIC/x2APIC 모드를 지원합니다.
LAPIC 주요 레지스터
| 레지스터 | 오프셋(Offset) | x2APIC MSR | 설명 |
|---|---|---|---|
| APIC ID | 0x020 | 0x802 | LAPIC 고유 식별자 |
| TPR (Task Priority) | 0x080 | 0x808 | 현재 CPU 우선순위 임계값 |
| EOI | 0x0B0 | 0x80B | End of Interrupt (0 쓰기로 완료) |
| ISR (In-Service) | 0x100-0x170 | 0x810-0x817 | 현재 처리 중인 인터럽트 비트맵(Bitmap) |
| IRR (Request) | 0x200-0x270 | 0x820-0x827 | 대기 중인 인터럽트 비트맵 |
| ICR (Interrupt Command) | 0x300-0x310 | 0x830 | IPI 전송 레지스터 |
| LVT Timer | 0x320 | 0x832 | 로컬 타이머 인터럽트 설정 |
| LVT Performance | 0x340 | 0x834 | 성능 카운터 오버플로 인터럽트 |
/* arch/x86/kernel/apic/apic.c — LAPIC 초기화 */
void setup_local_APIC(void)
{
unsigned int value;
/* TPR: 모든 인터럽트 허용 (우선순위 0) */
apic_write(APIC_TASKPRI, 0);
/* Spurious Interrupt Vector: APIC enable + vector 0xFF */
value = apic_read(APIC_SPIV);
value &= ~APIC_VECTOR_MASK;
value |= APIC_SPIV_APIC_ENABLED;
value |= SPURIOUS_APIC_VECTOR; /* 0xFF */
apic_write(APIC_SPIV, value);
/* LVT 설정: Timer, LINT0, LINT1, Error, PMI */
apic_write(APIC_LVT0, APIC_DM_EXTINT); /* LINT0: 외부 8259 */
apic_write(APIC_LVT1, APIC_DM_NMI); /* LINT1: NMI */
apic_write(APIC_LVTERR, ERROR_APIC_VECTOR);
}
/* x2APIC 모드 활성화 (MSR 기반 접근) */
void enable_x2apic(void)
{
u64 msr;
rdmsrl(MSR_IA32_APICBASE, msr);
if (!(msr & X2APIC_ENABLE)) {
msr |= X2APIC_ENABLE;
wrmsrl(MSR_IA32_APICBASE, msr);
}
/* 이제 apic_read/write가 MSR 사용 */
/* xAPIC MMIO 대비 EOI가 ~10배 빠름 */
}
/* I/O APIC Redirection Table 엔트리 설정 */
static void ioapic_write_entry(int apic, int pin,
struct IO_APIC_route_entry e)
{
/* 64비트 엔트리: vector, delivery, dest, trigger, mask */
io_apic_write(apic, 0x10 + 2 * pin, *((u32 *)&e));
io_apic_write(apic, 0x11 + 2 * pin, *((u32 *)&e + 1));
}
Threaded IRQ 실행 흐름 상세
Threaded IRQ는 인터럽트 처리의 bottom half를 전용 커널 스레드(irq/N-name)에서 실행합니다. request_threaded_irq()를 호출하면 커널이 자동으로 kthread_create()로 IRQ 스레드(Thread)를 생성합니다. 이 스레드는 SCHED_FIFO 정책으로 실행되며, RT 우선순위를 가집니다.
/* kernel/irq/manage.c — IRQ 스레드 생성 */
static int setup_irq_thread(struct irqaction *new,
unsigned int irq, bool secondary)
{
struct task_struct *t;
struct sched_param param = {
.sched_priority = MAX_USER_RT_PRIO / 2, /* 기본 RT 우선순위: 50 */
};
/* 스레드 생성: irq/N-handler_name */
t = kthread_create(irq_thread, new, "irq/%d-%s", irq,
new->name);
if (IS_ERR(t))
return PTR_ERR(t);
/* SCHED_FIFO 정책, RT 우선순위 설정 */
sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m);
/* CPU affinity를 IRQ의 affinity와 동기화 */
set_bit(IRQTF_AFFINITY, &new->thread_flags);
new->thread = t;
return 0;
}
/* IRQ 스레드 메인 루프 */
static int irq_thread(void *data)
{
struct irqaction *action = data;
while (!irq_wait_for_interrupt(action)) { /* 인터럽트 대기 */
irq_thread_check_affinity(action->irq, action);
action->thread_fn(action->irq, action->dev_id); /* 실제 처리 */
if (test_and_clear_bit(IRQTF_ONESHOT, &action->thread_flags))
irq_finalize_oneshot(action->irq, action); /* unmask */
}
return 0;
}
코드 설명
- setup_irq_thread()
kernel/irq/manage.c에서__setup_irq()호출 시 threaded handler를 위한 커널 스레드를 생성합니다.kthread_create()로irq/N-name형식의 스레드를 만들고,sched_setscheduler_nocheck()로SCHED_FIFO정책과 RT 우선순위 50(MAX_USER_RT_PRIO/2)을 설정합니다.IRQTF_AFFINITY비트를 설정하여 IRQ affinity 변경 시 스레드도 함께 이동하도록 합니다. - irq_thread()IRQ 스레드의 메인 루프입니다.
irq_wait_for_interrupt()에서 top half의IRQ_WAKE_THREAD반환을 대기하고, 깨어나면thread_fn()을 실행합니다. 매 반복마다irq_thread_check_affinity()로 CPU 바인딩을 갱신합니다. - irq_finalize_oneshot()
IRQF_ONESHOT모드에서 threaded handler 완료 후 IRQ를 언마스킹합니다. 이 시점에서야 비로소 같은 IRQ 라인의 새로운 인터럽트가 다시 전달됩니다. 호출 체인: top half(IRQ_WAKE_THREAD) →irq_thread()→thread_fn()→irq_finalize_oneshot()→ unmask.
CONFIG_PREEMPT_RT 커널에서는 IRQF_NO_THREAD 플래그가 없는 모든 인터럽트가 자동으로 threaded로 변환됩니다. 이때 hardirq 핸들러도 스레드 컨텍스트에서 실행되므로, spin_lock()이 rt_mutex로 변환되어 슬립 가능해집니다. 타이머 인터럽트와 IPI는 IRQF_NO_THREAD로 보호되어 항상 hardirq 컨텍스트에서 실행됩니다.
IRQ Affinity 및 밸런싱 상세
IRQ affinity는 특정 인터럽트를 어떤 CPU에서 처리할지 결정합니다. 올바른 affinity 설정은 캐시 지역성, NUMA 최적화, 부하 분산(Load Balancing)의 핵심입니다. Linux는 /proc/irq/N/smp_affinity 인터페이스와 irqbalance 데몬으로 이를 관리합니다.
/* kernel/irq/manage.c — irq_set_affinity 내부 */
int irq_set_affinity_locked(struct irq_data *data,
const struct cpumask *mask, bool force)
{
struct irq_chip *chip = irq_data_get_irq_chip(data);
struct irq_desc *desc = irq_data_to_desc(data);
int ret;
if (!chip || !chip->irq_set_affinity)
return -EINVAL;
/* online CPU만 대상으로 필터링 */
if (cpumask_intersects(mask, cpu_online_mask))
ret = chip->irq_set_affinity(data, mask, force);
else
ret = -EINVAL;
if (ret == IRQ_SET_MASK_OK || ret == IRQ_SET_MASK_OK_DONE) {
cpumask_copy(desc->irq_common_data.affinity, mask);
irq_set_thread_affinity(desc); /* threaded IRQ 스레드도 이동 */
}
return ret;
}
/* MSI-X managed affinity: 커널이 자동 최적 분산 */
static void irq_spread_init_one(struct cpumask *irqmsk,
struct cpumask *nmsk, int cpus_per_vec)
{
/* NUMA 노드별로 균등 분배 */
int cpu, assigned = 0;
for_each_cpu(cpu, nmsk) {
cpumask_set_cpu(cpu, irqmsk);
if (++assigned >= cpus_per_vec)
break;
}
}
MSI/MSI-X 인터럽트 전달 경로 상세
MSI(Message Signaled Interrupts)는 PCI 디바이스가 전용 IRQ 라인 대신 메모리 쓰기 트랜잭션(Transaction)으로 인터럽트를 발생시키는 메커니즘입니다. MSI-X는 MSI의 확장으로 최대 2048개의 독립적인 벡터를 지원하며, 각 벡터에 개별 CPU affinity를 설정할 수 있습니다.
/* drivers/pci/msi/msi.c — MSI-X 벡터 할당 */
int pci_alloc_irq_vectors_affinity(struct pci_dev *dev,
unsigned int min_vecs,
unsigned int max_vecs,
unsigned int flags,
struct irq_affinity *affd)
{
/* MSI-X 우선 시도, 실패 시 MSI fallback */
if (flags & PCI_IRQ_MSIX) {
int vecs = __pci_enable_msix_range(dev, NULL,
min_vecs, max_vecs, affd, flags);
if (vecs > 0)
return vecs;
}
if (flags & PCI_IRQ_MSI) {
int vecs = __pci_enable_msi_range(dev,
min_vecs, max_vecs, affd);
if (vecs > 0)
return vecs;
}
/* Legacy fallback */
if (flags & PCI_IRQ_LEGACY) {
if (min_vecs == 1)
return 1;
}
return -ENOSPC;
}
/* MSI-X 테이블 엔트리 구조 (PCI Spec) */
struct msi_msg {
u32 address_lo; /* [31:20]=0xFEE, [19:12]=APIC ID */
u32 address_hi; /* 64-bit 모드용 */
u32 data; /* [7:0]=Vector, [10:8]=Delivery */
};
/* MSI-X 테이블 엔트리 쓰기 */
void __pci_write_msi_msg(struct msi_desc *entry,
struct msi_msg *msg)
{
void __iomem *base = entry->mask_base;
int off = entry->msi_attrib.entry_nr * PCI_MSIX_ENTRY_SIZE;
writel(msg->address_lo, base + off + PCI_MSIX_ENTRY_LOWER_ADDR);
writel(msg->address_hi, base + off + PCI_MSIX_ENTRY_UPPER_ADDR);
writel(msg->data, base + off + PCI_MSIX_ENTRY_DATA);
}
PCI_IRQ_AFFINITY 플래그로 커널이 NUMA 토폴로지를 고려하여 자동 분산합니다. 이로써 각 CPU가 자신의 큐에서 발생하는 인터럽트를 로컬에서 처리하여 캐시 경쟁을 최소화합니다.
인터럽트 비활성화 계층
Linux 커널은 여러 수준에서 인터럽트를 비활성화할 수 있습니다. 각 계층은 다른 범위와 의미를 가지며, 올바른 수준을 선택하는 것이 중요합니다. 잘못된 수준의 비활성화는 데드록이나 인터럽트 분실을 유발할 수 있습니다.
인터럽트 비활성화 API 상세
/* === Level 1: CPU 전역 인터럽트 비활성화 === */
unsigned long flags;
/* 방법 1: flags에 현재 상태 저장 후 비활성화 (중첩 안전) */
local_irq_save(flags);
/* ... 인터럽트 비활성 구간 (최소한으로!) ... */
local_irq_restore(flags); /* 이전 상태 복원 */
/* 방법 2: 무조건 비활성/활성 (중첩 불안전 — 권장하지 않음) */
local_irq_disable();
/* ... */
local_irq_enable();
/* === Level 2: 개별 IRQ 비활성화 === */
disable_irq(irq); /* 동기: 진행 중인 핸들러 완료 대기 */
/* 주의: 인터럽트 컨텍스트에서 호출하면 데드록! */
enable_irq(irq);
disable_irq_nosync(irq); /* 비동기: 즉시 반환 */
synchronize_irq(irq); /* 필요시 별도 동기화 */
enable_irq(irq);
/* === Level 3: Bottom Half 비활성화 === */
local_bh_disable();
/* softirq/tasklet이 실행되지 않는 구간 */
/* 하드웨어 인터럽트는 여전히 발생함! */
local_bh_enable();
/* === Level 4: Wake 설정 === */
enable_irq_wake(irq); /* suspend 중 이 IRQ로 깨어남 */
/* ... 시스템 suspend ... */
disable_irq_wake(irq); /* resume 후 해제 */
/* === 스핀락과 인터럽트 조합 === */
spin_lock_irq(&lock); /* local_irq_disable + spin_lock */
spin_unlock_irq(&lock); /* spin_unlock + local_irq_enable */
spin_lock_irqsave(&lock, flags); /* 가장 안전 (중첩 가능) */
spin_unlock_irqrestore(&lock, flags);
spin_lock_bh(&lock); /* local_bh_disable + spin_lock */
spin_unlock_bh(&lock); /* spin_unlock + local_bh_enable */
코드 설명
- Level 1: local_irq_save()/local_irq_restore()현재 CPU의 모든 인터럽트를 비활성화합니다.
local_irq_save()는 이전 플래그 상태를 저장하므로 중첩 호출에 안전합니다.local_irq_disable()/local_irq_enable()은 중첩 불안전하여 이미 비활성 상태에서enable을 호출하면 의도치 않게 인터럽트를 활성화합니다.PREEMPT_RT커널에서local_irq_disable()은 실제 인터럽트가 아닌 선점만 비활성화합니다. - Level 2: disable_irq()/enable_irq()특정 IRQ 라인만 비활성화합니다.
disable_irq()는 진행 중인 핸들러 완료를 대기하므로 인터럽트 컨텍스트에서 호출하면 자기 자신을 기다리는 데드록이 발생합니다. 인터럽트 핸들러 내부에서는 반드시disable_irq_nosync()를 사용해야 합니다. - Level 3: local_bh_disable()/local_bh_enable()softirq와 tasklet 실행만 차단합니다. 하드웨어 인터럽트는 여전히 발생하므로 softirq와 공유하는 데이터를 보호할 때 사용합니다. 프로세스 컨텍스트에서
spin_lock_bh()가 이를 자동으로 처리합니다. - spin_lock_irqsave()인터럽트 핸들러와 데이터를 공유하는 가장 안전한 패턴입니다.
local_irq_save()+spin_lock()을 결합하여 인터럽트 비활성화와 스핀락 획득을 원자적으로 수행합니다. 이전 인터럽트 상태를flags에 저장하므로 중첩 호출에도 안전합니다.
비활성화 수준 선택 가이드
| 시나리오 | 권장 API | 이유 |
|---|---|---|
| 인터럽트 핸들러와 공유 데이터 | spin_lock_irqsave() | 인터럽트 비활성 + 스핀락(Spinlock) |
| softirq와 공유 데이터 (프로세스) | spin_lock_bh() | BH만 비활성 (더 넓은 창) |
| 드라이버 제거 시 IRQ 정리 | disable_irq() + free_irq() | 핸들러 완료 보장 |
| 인터럽트 핸들러 내부에서 IRQ 제어 | disable_irq_nosync() | 데드록 방지 (비동기) |
| 전원 관리(Power Management) 웨이크업 소스 | enable_irq_wake() | suspend 중 인터럽트 수신 |
| 짧은 Per-CPU 임계 구간 | local_irq_save() | 최소 오버헤드, 중첩 안전 |
disable_irq()를 인터럽트 핸들러 내부에서 호출하면 데드록이 발생합니다. disable_irq()는 현재 실행 중인 핸들러가 완료될 때까지 대기하는데, 바로 그 핸들러 안에서 호출하면 자기 자신을 기다리게 됩니다. 인터럽트 컨텍스트에서는 반드시 disable_irq_nosync()를 사용하세요.
PREEMPT_RT 인터럽트 처리
CONFIG_PREEMPT_RT 커널은 인터럽트 처리 모델을 근본적으로 변경합니다. 거의 모든 인터럽트 핸들러가 스레드화되어 선점 가능해지며, 이를 통해 결정론적 레이턴시를 달성합니다.
주요 변경 사항
| 항목 | 일반 커널 (PREEMPT) | PREEMPT_RT |
|---|---|---|
| 인터럽트 핸들러 | hardirq 컨텍스트 | 스레드 컨텍스트 (IRQF_NO_THREAD 제외) |
| softirq | 인터럽트 복귀 시 실행 | ksoftirqd에서만 실행 (스레드화) |
| spin_lock() | 선점 비활성 | rt_mutex 변환 (슬립 가능) |
| raw_spin_lock() | = spin_lock | 진짜 스핀락 (비선점(Non-preemptive)) |
| local_irq_disable() | 인터럽트 비활성 | 선점만 비활성 (인터럽트 허용) |
| raw_local_irq_disable() | = local_irq_disable | 진짜 인터럽트 비활성 |
| 최악 레이턴시 | 수 ms | 수십 us (결정론적) |
/* PREEMPT_RT에서의 인터럽트 핸들러 강제 스레드화 */
/* kernel/irq/manage.c */
static int irq_setup_forced_threading(struct irqaction *new)
{
#ifdef CONFIG_IRQ_FORCED_THREADING
/* IRQF_NO_THREAD 플래그가 없으면 강제 스레드화 */
if (!force_irqthreads())
return 0;
if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
return 0;
/* 기존 handler를 thread_fn으로 이동 */
new->thread_fn = new->handler;
new->handler = irq_default_primary_handler; /* IRQ_WAKE_THREAD 반환 */
new->flags |= IRQF_ONESHOT;
#endif
return 0;
}
/* PREEMPT_RT에서 spin_lock → rt_mutex 변환 */
/* include/linux/spinlock_rt.h */
#define spin_lock(lock) rt_spin_lock(lock) /* 슬립 가능! */
#define spin_unlock(lock) rt_spin_unlock(lock)
/* 진짜 스핀이 필요한 곳: raw_spin_lock 사용 */
#define raw_spin_lock(lock) __raw_spin_lock(lock) /* 비선점 */
# PREEMPT_RT 커널에서 IRQ 스레드 우선순위 확인 및 조정
ps -eo pid,cls,rtprio,comm | grep 'irq/'
# PID CLS RTPRIO COMMAND
# 42 FF 50 irq/16-ahci
# 43 FF 50 irq/17-eth0
# 44 FF 50 irq/18-snd_hda
# 특정 IRQ 스레드 우선순위 조정
chrt -f -p 90 43 # eth0 IRQ 스레드를 RT 우선순위 90으로
# cyclictest로 RT 레이턴시 측정
cyclictest --mlockall --priority=99 --interval=200 --loops=100000
# Min: 1 Act: 3 Avg: 2 Max: 12 (PREEMPT_RT)
# Min: 1 Act: 15 Avg: 8 Max: 523 (일반 커널)
# RT 커널 커맨드라인 최적화
# isolcpus=2-3 nohz_full=2-3 rcu_nocbs=2-3
# → CPU 2-3을 RT 전용으로 격리
인터럽트 가상화 (Interrupt Virtualization)
가상화 환경에서 게스트(Guest) OS의 인터럽트를 효율적으로 처리하는 것은 성능의 핵심입니다. 전통적인 트랩-에뮬레이션(Trap-and-Emulate) 방식은 매 인터럽트마다 VM Exit가 발생하여 큰 오버헤드를 유발합니다. 최신 하드웨어는 인터럽트를 게스트에 직접 주입(Direct Injection)하여 VMM 개입을 최소화합니다.
APICv / Posted Interrupts (x86)
Intel VT-x의 APICv(Advanced Programmable Interrupt Controller Virtualization)는 가상 APIC 접근을 하드웨어가 직접 처리합니다. Posted Interrupts 메커니즘은 게스트가 실행 중일 때 VM Exit 없이 인터럽트를 직접 전달합니다.
- VMCS Posted-Interrupt Descriptor: 256비트 PIR(Posted-Interrupt Request) 비트맵과 Outstanding Notification(ON) 비트로 구성
- Notification Vector: 호스트(Host)의 특수 IPI 벡터로, CPU가 이를 받으면 PIR에서 게스트 vIRR로 인터럽트를 병합
- 동작 흐름: 외부 인터럽트 → IOMMU가 interrupt remapping → PIR에 비트 설정 → ON 비트 set → Notification IPI → 게스트에 직접 전달
- 성능 영향: 네트워크 집약(Intensive) 워크로드에서 VM Exit 90% 이상 감소, 처리량(Throughput) 30-50% 향상
/* arch/x86/kvm/vmx/posted_intr.c — Posted Interrupt 처리 핵심 */
static void vmx_deliver_posted_interrupt(struct kvm_vcpu *vcpu, int vector)
{
struct vcpu_vmx *vmx = to_vmx(vcpu);
struct pi_desc *pi_desc = &vmx->pi_desc;
/* PIR(Posted Interrupt Request)에 벡터 비트 설정 */
if (pi_test_and_set_pir(vector, pi_desc))
return; /* 이미 pending */
/* Outstanding Notification 비트 설정 */
if (pi_test_and_set_on(pi_desc))
return; /* 이미 notification pending */
/* vCPU가 실행 중이면 notification IPI 전송 → VM Exit 없이 전달 */
if (vcpu_is_running(vcpu))
apic_send_IPI_mask(get_cpu_mask(vcpu->cpu),
POSTED_INTR_VECTOR);
}
GICv4 vLPI 직접 주입 (ARM)
ARM GICv4(Generic Interrupt Controller v4)는 가상 LPI(Locality-specific Peripheral Interrupt)를 게스트 OS에 직접 전달합니다. 물리 디바이스의 인터럽트가 하이퍼바이저(Hypervisor) 개입 없이 vPE(Virtual Processing Element)로 라우팅됩니다.
- vPE 테이블: 각 가상 CPU에 매핑된 vPE 엔트리, ITS(Interrupt Translation Service)가 DeviceID + EventID를 vLPI로 변환
- Doorbell 인터럽트: vPE가 스케줄되지 않은 상태에서 vLPI 수신 시 하이퍼바이저에 doorbell 전달
- 성능: 디바이스 패스스루(Passthrough) 시나리오에서 x86 Posted Interrupts와 유사한 수준의 오버헤드 제거
/* drivers/irqchip/irq-gic-v4.c — vLPI 매핑 */
int its_map_vlpi(int irq, struct its_vlpi_map *map)
{
/*
* 물리 디바이스 IRQ를 가상 LPI에 매핑
* → GIC ITS가 하드웨어 수준에서 Guest vPE에 직접 전달
* → VM Exit 없이 Guest IRQ handler 실행
*/
return its_send_mapvi(its_dev, map->vpe,
irq, map->vintid);
}
가상화 인터럽트 방식 비교
| 특성 | Trap + Emulate | APICv / Posted Interrupts | GICv4 vLPI |
|---|---|---|---|
| VM Exit 발생 | 매 인터럽트마다 | 없음 (게스트 실행 중) | 없음 (vPE 스케줄 중) |
| 하드웨어 요구 | 기본 VT-x / ARM Virt | Intel APICv | ARM GICv4 |
| 인터럽트 지연시간 | 수천 사이클 | 수백 사이클 | 수백 사이클 |
| 디바이스 패스스루 | 에뮬레이션 필요 | IOMMU IR + PI | ITS + vLPI 매핑 |
| 게스트 비실행 시 | 해당 없음 | VM Exit → 큐잉(Queueing) | Doorbell → 하이퍼바이저 |
| 커널 지원 | KVM 기본 | KVM + kvm_intel | KVM + irq-gic-v4 |
인터럽트 디버깅
/proc/interrupts 심층 분석
# /proc/interrupts 전체 해석
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 0: 45 0 0 0 IR-IO-APIC 2-edge timer
# 1: 3 0 0 0 IR-IO-APIC 1-edge i8042
# 8: 0 0 0 0 IR-IO-APIC 8-edge rtc0
# 16: 8521 2341 456 123 IR-IO-APIC 16-fasteoi ahci[0]
# 142: 4523678 0 0 0 IR-PCI-MSI 524288-edge nvme0q0
# 143: 0 3456789 0 0 IR-PCI-MSI 524289-edge nvme0q1
# 144: 0 0 2345678 0 IR-PCI-MSI 524290-edge nvme0q2
# NMI: 1234 1234 1234 1234 Non-maskable interrupts
# LOC: 987654321 876543210 765432109 654321098 Local timer interrupts
# RES: 1234567 1234568 1234569 1234570 Rescheduling interrupts
# CAL: 12345 12346 12347 12348 Function call interrupts
# TLB: 456789 456790 456791 456792 TLB shootdowns
#
# 해석 포인트:
# - IR-: Interrupt Remapping (VT-d/IOMMU) 활성
# - PCI-MSI: MSI/MSI-X 사용 중
# - edge/fasteoi: 플로우 핸들러 유형
# - NMI 카운트가 CPU마다 다르면: perf 프로파일링 중
# - RES 높으면: 스케줄러 IPI 과다 (태스크 마이그레이션 빈번)
# - TLB 높으면: 메모리 매핑 변경 빈번 (mmap/munmap)
# 인터럽트 변화율 측정 스크립트
prev=$(cat /proc/interrupts)
sleep 1
curr=$(cat /proc/interrupts)
diff <(echo "$prev") <(echo "$curr") | grep '^[<>]' | head -20
ftrace IRQ 이벤트
# 1. 모든 IRQ 관련 트레이스포인트 확인
ls /sys/kernel/debug/tracing/events/irq/
# irq_handler_entry irq_handler_exit
# softirq_entry softirq_exit softirq_raise
# 2. IRQ 핸들러 실행 시간 측정 (function_graph)
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo handle_irq_event_percpu > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-abstime
echo 1 > /sys/kernel/debug/tracing/options/funcgraph-proc
echo 1 > /sys/kernel/debug/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | head -50
# 3. 특정 IRQ만 필터링 (trigger 사용)
echo 'irq==16' > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/filter
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_entry/enable
echo 1 > /sys/kernel/debug/tracing/events/irq/irq_handler_exit/enable
# 4. irqsoff tracer: 인터럽트 비활성 최대 구간 추적
echo irqsoff > /sys/kernel/debug/tracing/current_tracer
echo 0 > /sys/kernel/debug/tracing/tracing_max_latency # 리셋
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 워크로드 실행...
cat /sys/kernel/debug/tracing/tracing_max_latency
# 출력: 523 (μs) → 가장 긴 인터럽트 비활성 구간
cat /sys/kernel/debug/tracing/trace # 어디서 발생했는지 스택 확인
# 5. perf + BPF로 인터럽트 핸들러 히스토그램
perf record -e irq:irq_handler_entry -e irq:irq_handler_exit -ag sleep 10
perf script | awk '/irq_handler_entry/ {start=$4} /irq_handler_exit/ {print $4-start}'
trace-cmd 도구를 사용하면 ftrace를 더 편리하게 활용할 수 있습니다. trace-cmd record -e irq -e softirq로 기록하고 trace-cmd report로 분석합니다. kernelshark GUI 도구와 결합하면 타임라인에서 인터럽트 흐름을 시각적으로 확인할 수 있습니다.
흔한 실수와 안티패턴
인터럽트 관련 코드에서 자주 발생하는 실수와 올바른 패턴을 정리합니다. 커널 패닉(Kernel Panic), 데드락(Deadlock), 데이터 손상의 원인이 되는 안티패턴을 사전에 인지하면 디버깅 시간을 크게 줄일 수 있습니다.
| 안티패턴 | 문제점 | 올바른 패턴 |
|---|---|---|
| 인터럽트 컨텍스트에서 슬립 | 스케줄러 호출 → BUG: scheduling while atomic | workqueue 또는 threaded IRQ 사용 |
disable_irq()를 IRQ 핸들러 내부에서 호출 |
자기 자신의 완료를 대기 → 데드락 | disable_irq_nosync() 사용 |
threaded IRQ에서 IRQF_ONESHOT 누락 |
하드웨어 IRQ 재활성 시점 불명확 → IRQ 폭주(Storm) | 항상 IRQF_ONESHOT 플래그 설정 |
request_irq() 반환값 무시 |
등록 실패 시 NULL 핸들러 → 크래시(Crash) | 반환값 검사 후 에러 경로 처리 |
인터럽트 컨텍스트에서 GFP_KERNEL |
슬립 가능 할당 → BUG | GFP_ATOMIC 사용 |
free_irq() / tasklet_kill() 누락 |
모듈 제거 후 dangling 핸들러 → Use-After-Free | cleanup 경로에서 반드시 해제 |
| IRQ 핸들러와 드라이버 제거 사이 경쟁(Race) | 핸들러가 이미 해제된 메모리 접근 | free_irq() 후 synchronize_irq() 또는 devm_request_irq() 사용 |
spin_lock()을 IRQ와 공유 데이터에 사용 |
IRQ가 같은 락 획득 시도 → 데드락 | spin_lock_irqsave() 사용 |
실수 1: 인터럽트 컨텍스트에서 슬립
kmalloc(GFP_KERNEL)이나 mutex_lock() 등 슬립 가능 함수를 호출하면 BUG: scheduling while atomic이 발생합니다.
/* ❌ 잘못된 패턴: 인터럽트 핸들러에서 슬립 가능 함수 호출 */
static irqreturn_t bad_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
void *buf;
buf = kmalloc(4096, GFP_KERNEL); /* 💥 BUG: 슬립 가능! */
mutex_lock(&dev->lock); /* 💥 BUG: 슬립 가능! */
copy_to_user(ubuf, kbuf, len); /* 💥 BUG: page fault 가능! */
return IRQ_HANDLED;
}
/* ✅ 올바른 패턴: GFP_ATOMIC 사용, spinlock 사용 */
static irqreturn_t good_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
void *buf;
buf = kmalloc(4096, GFP_ATOMIC); /* ✅ 슬립 불가 할당 */
if (!buf)
return IRQ_HANDLED;
spin_lock(&dev->slock); /* ✅ spinlock (비슬립) */
/* 최소한의 작업만 수행 */
spin_unlock(&dev->slock);
return IRQ_HANDLED;
}
실수 2: IRQ 핸들러에서 disable_irq() 데드락
/* ❌ 잘못된 패턴: IRQ 핸들러 내에서 disable_irq() */
static irqreturn_t bad_handler(int irq, void *dev_id)
{
disable_irq(irq); /* 💥 데드락: 자기 자신의 완료를 대기 */
/* ... */
enable_irq(irq);
return IRQ_HANDLED;
}
/* ✅ 올바른 패턴: disable_irq_nosync() 사용 */
static irqreturn_t good_handler(int irq, void *dev_id)
{
disable_irq_nosync(irq); /* ✅ 비동기: 즉시 반환 */
/* 하위 작업 스케줄링 */
schedule_work(&dev->work);
return IRQ_HANDLED;
}
/* workqueue에서 enable_irq() 호출 */
static void deferred_work(struct work_struct *work)
{
/* 프로세스 컨텍스트에서 처리 */
enable_irq(dev->irq);
}
실수 3: threaded IRQ에서 IRQF_ONESHOT 누락
/* ❌ 잘못된 패턴: IRQF_ONESHOT 없이 threaded IRQ 등록 */
request_threaded_irq(irq, NULL, my_thread_fn,
IRQF_TRIGGER_LOW, /* 💥 ONESHOT 없음 → IRQ 재활성 시점 불명 */
"mydev", dev);
/* ✅ 올바른 패턴: IRQF_ONESHOT 설정 */
request_threaded_irq(irq, NULL, my_thread_fn,
IRQF_TRIGGER_LOW | IRQF_ONESHOT, /* ✅ 스레드 완료까지 IRQ 마스크 */
"mydev", dev);
실수 4: spin_lock vs spin_lock_irqsave 혼동
/* ❌ 잘못된 패턴: IRQ와 공유 데이터에 일반 spin_lock 사용 */
/* 프로세스 컨텍스트 */
spin_lock(&dev->lock); /* 여기서 IRQ 발생 시... */
dev->counter++;
spin_unlock(&dev->lock);
/* IRQ 핸들러 */
static irqreturn_t handler(int irq, void *dev_id)
{
spin_lock(&dev->lock); /* 💥 데드락: 같은 CPU에서 lock 이미 보유 */
dev->counter++;
spin_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* ✅ 올바른 패턴: 프로세스 컨텍스트에서 irqsave, 핸들러에서 일반 lock */
unsigned long flags;
/* 프로세스 컨텍스트 */
spin_lock_irqsave(&dev->lock, flags); /* ✅ IRQ 비활성 + lock */
dev->counter++;
spin_unlock_irqrestore(&dev->lock, flags);
/* IRQ 핸들러 (이미 IRQ 비활성 상태) */
static irqreturn_t handler(int irq, void *dev_id)
{
spin_lock(&dev->lock); /* ✅ IRQ 컨텍스트에서는 일반 lock 충분 */
dev->counter++;
spin_unlock(&dev->lock);
return IRQ_HANDLED;
}
실수 5: 드라이버 제거 시 경쟁 조건
/* ❌ 잘못된 패턴: 리소스 해제 순서 문제 */
static void bad_remove(struct pci_dev *pdev)
{
kfree(dev->buffer); /* 💥 핸들러가 아직 buffer 사용 중일 수 있음 */
free_irq(dev->irq, dev);
kfree(dev);
}
/* ✅ 올바른 패턴: IRQ 먼저 해제 → 동기화 → 리소스 해제 */
static void good_remove(struct pci_dev *pdev)
{
/* 1. 새 인터럽트 수신 중단 */
free_irq(dev->irq, dev); /* ✅ 핸들러 완료 대기 포함 */
/* 2. Bottom Half 완료 대기 */
cancel_work_sync(&dev->work); /* ✅ workqueue 완료 대기 */
tasklet_kill(&dev->tlet); /* ✅ tasklet 완료 대기 */
/* 3. 이제 안전하게 리소스 해제 */
kfree(dev->buffer);
kfree(dev);
}
실수 6: request_irq() 반환값 미확인
/* ❌ 잘못된 패턴: 반환값 무시 */
request_irq(irq, my_handler, 0, "mydev", dev);
/* 등록 실패해도 계속 진행 → 인터럽트 미처리 */
/* ✅ 올바른 패턴: 에러 처리 */
int ret = request_irq(irq, my_handler, 0, "mydev", dev);
if (ret) {
dev_err(&pdev->dev, "IRQ %d 등록 실패: %d\n", irq, ret);
goto err_free;
}
/* 또는 devm_request_irq()로 관리형 사용 */
참고자료
커널 공식 문서
- Linux generic IRQ handling — 리눅스 범용 인터럽트 처리 프레임워크 공식 문서입니다.
- irq_domain — IRQ Number Mapping Library — 하드웨어 IRQ에서 Linux IRQ로의 매핑 API를 설명합니다.
- IRQ Affinity — IRQ CPU 친화성(affinity) 설정 인터페이스입니다.
- Concurrency Managed Workqueue (cmwq) — CMWQ 아키텍처와 API 사용법을 설명합니다.
- Driver Basics — request_irq / free_irq — 드라이버 API에서의 인터럽트 등록/해제 인터페이스입니다.
- Kernel Parameters —
irqpoll,irqaffinity,threadirqs등 인터럽트 관련 부트 파라미터를 확인할 수 있습니다. - MSI-HOWTO — PCI MSI/MSI-X 인터럽트 사용 방법을 설명합니다.
- x86 IRQ Remapping — x86 아키텍처의 인터럽트 리매핑(VT-d) 문서입니다.
커널 소스 코드
kernel/irq/— 범용 IRQ 처리 코어 디렉터리입니다.kernel/irq/manage.c—request_irq(),free_irq(), threaded IRQ 등의 구현입니다.kernel/irq/chip.c—irq_chip인터페이스 및 flow handler 구현입니다.kernel/irq/irqdesc.c—irq_desc할당과 관리 구현입니다.kernel/irq/irqdomain.c— IRQ domain 매핑 라이브러리 구현입니다.kernel/irq/proc.c—/proc/interrupts,/proc/irq/인터페이스 구현입니다.kernel/softirq.c— softirq, tasklet, ksoftirqd 구현입니다.kernel/workqueue.c— CMWQ(Concurrency Managed Workqueue) 구현입니다.include/linux/interrupt.h—request_irq(),irqreturn_t, softirq, tasklet 선언입니다.include/linux/irq.h—irq_chip,irq_data, flow handler 타입 정의입니다.include/linux/irqdesc.h—irq_desc구조체 정의입니다.arch/x86/kernel/irq.c— x86 아키텍처별 인터럽트 진입점입니다.arch/arm64/kernel/irq.c— ARM64 아키텍처별 인터럽트 처리입니다.drivers/irqchip/— GIC, APIC 등 인터럽트 컨트롤러 드라이버 디렉터리입니다.
LWN.net 기사
- A new generic IRQ layer (2006) — 범용 IRQ 계층 도입 배경과 설계를 설명하는 기사입니다.
- Moving interrupts to threads (2008) — 스레드화 인터럽트(threaded IRQ)의 필요성과 구현을 다룹니다.
- Threaded interrupts revisited (2010) —
request_threaded_irq()API의 발전 과정을 설명합니다. - Interrupt descriptors and irq_data (2012) —
irq_desc에서irq_data분리의 설계 근거입니다. - A survey of interrupt handling (2014) — 리눅스 인터럽트 처리 전반에 대한 개괄입니다.
- Nested interrupts and per-CPU variables (2019) — 중첩 인터럽트와 per-CPU 변수 사용 시 주의점입니다.
- Modernizing the tasklet API (2020) — tasklet API 현대화 논의와 향후 방향을 정리합니다.
- Removing tasklets (2022) — tasklet 제거 움직임과 대안(threaded IRQ, workqueue)을 설명합니다.
- IRQ affinity (2008) — IRQ CPU 친화성 설정의 원리와 사용법을 다룹니다.
서적
- Linux Kernel Development, 3rd Edition (Robert Love) — Chapter 7 "Interrupts and Interrupt Handlers", Chapter 8 "Bottom Halves and Deferring Work"가 인터럽트 처리의 핵심을 다룹니다.
- Understanding the Linux Kernel, 3rd Edition (Bovet & Cesati) — Chapter 4 "Interrupts and Exceptions"에서 x86 인터럽트 아키텍처를 상세히 설명합니다.
- Linux Device Drivers, 3rd Edition (Corbet, Rubini & Kroah-Hartman) — 온라인 전문 공개, Chapter 10 "Interrupt Handling"을 참고하세요.
- Professional Linux Kernel Architecture (Wolfgang Mauerer) — 인터럽트 디스크립터와 flow handler 내부 구현을 다룹니다.
외부 자료
- irq(7) man page —
/proc/interrupts,/proc/irq/인터페이스의 사용법을 설명합니다. - Linux Insides — Interrupts — 인터럽트 초기화부터 처리까지 소스 코드 수준에서 단계별로 추적합니다.
- Linux Kernel Labs — Interrupts — 부쿠레슈티 공과대학의 커널 인터럽트 실습 강의 자료입니다.
- Interrupt Handling in Linux (Kernel Recipes 2015) — Thomas Gleixner의 인터럽트 서브시스템 발표입니다.
- OSDev Wiki — Interrupts — 하드웨어 수준 인터럽트 메커니즘(IDT, APIC, PIC)의 기초를 설명합니다.
관련 문서
인터럽트 처리와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.