인터럽트 분류와 마스킹·벡터 기반 처리 방식

인터럽트 발생 원인부터 마스커블·NMI·벡터 인터럽트의 차이, x86·ARM·MIPS 구현과 마스킹 제어를 정리한다.

2026-08-14 · 최초 발행 2026-01-06

인터럽트가 들어오는 경로

인터럽트는 CPU가 현재 실행하던 흐름을 멈추고 특정 처리 루틴으로 전환하게 만드는 사건이다. 발생 원인을 기준으로 보면 외부 하드웨어, 소프트웨어 요청, 내부 예외로 나뉜다. 이 구분은 어떤 핸들러가 필요한지뿐 아니라, 처리 지연을 어디까지 허용할지 판단하는 기준이 된다.

외부 하드웨어 인터럽트에는 I/O 장치의 작업 완료와 타이머 이벤트가 포함된다. 키보드 입력, 마우스 이동이나 클릭, 디스크 읽기·쓰기 완료, 네트워크 패킷 수신이 여기에 해당한다. 시스템 타이머(PIT, APIC Timer), RTC(Real-Time Clock) 알람, Watchdog Timer 만료도 CPU에 처리를 요청한다. 전원 버튼과 리셋 버튼, 온도 센서 임계값, 배터리 부족 같은 외부 이벤트도 인터럽트 발생 요인이 될 수 있다.

// 타이머 인터럽트 예시
void timer_interrupt_handler(void) {
    // 타이머 틱 증가
    system_tick++;

    // 스케줄러 호출
    schedule();
}

소프트웨어 인터럽트는 프로그램이 커널 기능을 요청하거나 의도적으로 트랩을 발생시킬 때 나타난다. 사용자 프로그램의 커널 서비스 요청은 System Call에 해당하며, x86에서는 int 0x80, syscall, ARM에서는 svc #0가 사용된다. 디버그 브레이크포인트인 int 3, 오버플로우 검사인 into, 의도적인 예외 발생도 Trap의 예다.

// Linux system call (read)
ssize_t read(int fd, void *buf, size_t count) {
    ssize_t ret;
    asm volatile(
        "mov $0, %%rax\n"      // syscall number (read)
        "mov %1, %%rdi\n"      // fd
        "mov %2, %%rsi\n"      // buf
        "mov %3, %%rdx\n"      // count
        "syscall\n"            // Software Interrupt
        "mov %%rax, %0\n"
        : "=r"(ret)
        : "r"((long)fd), "r"(buf), "r"(count)
        : "rax", "rdi", "rsi", "rdx"
    );
    return ret;
}

CPU가 명령을 수행하는 과정에서도 인터럽트성 예외가 발생한다. 프로그램 오류에는 Division by Zero, Invalid Opcode, General Protection Fault, Stack Overflow가 있고, 메모리 관리에서는 Page Fault, Segmentation Fault, Access Violation이 대표적이다. Single Step과 Breakpoint는 디버그 목적의 예외다.

// Page Fault 핸들러
void page_fault_handler(struct pt_regs *regs) {
    unsigned long address = read_cr2();  // Fault 발생 주소

    if (is_valid_address(address)) {
        // 페이지 할당
        allocate_page(address);
        // 명령어 재실행
    } else {
        // Segmentation Fault
        send_signal(SIGSEGV);
    }
}
인터럽트발생요인외부하드웨어소프트웨어내부예외I/O장치타이머외부이벤트SystemCallTrap메모리에러프로그램에러

마스커블 인터럽트는 임계 구역에서 제어한다

Maskable Interrupt는 소프트웨어로 비활성화할 수 있는 인터럽트다. x86에서는 IF 플래그, ARM에서는 I 비트를 통해 제어하며 일반적인 I/O 인터럽트가 여기에 속한다. 공유 데이터를 갱신하는 임계 구역에서는 일시적으로 인터럽트를 막아 실행 중인 코드가 인터럽트 핸들러와 충돌하지 않도록 할 수 있다.

x86의 전역 마스킹은 clisti로 수행한다.

; 인터럽트 비활성화
cli     ; Clear Interrupt Flag

; Critical Section
; ...

; 인터럽트 활성화
sti     ; Set Interrupt Flag
// C 함수로 래핑
void disable_interrupts(void) {
    asm volatile("cli" ::: "memory");
}

void enable_interrupts(void) {
    asm volatile("sti" ::: "memory");
}

마스킹 이전의 플래그 상태를 저장해 두었다가 복원하면 호출 전 상태를 유지할 수 있다.

unsigned long save_flags_cli(void) {
    unsigned long flags;
    asm volatile(
        "pushfq\n"
        "popq %0\n"
        "cli\n"
        : "=r"(flags)
        :: "memory"
    );
    return flags;
}

void restore_flags(unsigned long flags) {
    asm volatile(
        "pushq %0\n"
        "popfq\n"
        :: "r"(flags)
        : "memory"
    );
}

공유 카운터와 버퍼를 함께 갱신해야 한다면 다음처럼 플래그를 저장한 뒤 임계 구역을 실행하고 복원할 수 있다.

void update_shared_data(void) {
    unsigned long flags = save_flags_cli();

    // 인터럽트 비활성화된 상태
    shared_counter++;
    shared_buffer[index] = data;

    restore_flags(flags);
}

여러 하드웨어 레지스터를 원자적으로 설정해야 하는 경우에도 같은 방식이 쓰인다.

void configure_device(void) {
    disable_interrupts();

    // 여러 레지스터를 원자적으로 설정
    device->reg1 = value1;
    device->reg2 = value2;
    device->reg3 = value3;

    enable_interrupts();
}

NMI는 마스킹 대상이 아닌 예외 경로다

Non-Maskable Interrupt(NMI)는 소프트웨어로 비활성화할 수 없고, 최고 우선순위로 처리되는 치명적 시스템 이벤트를 위한 인터럽트다. cli로도 마스킹할 수 없으며 하드웨어 고장을 감지하는 경로로 사용된다.

메모리 패리티 에러, 버스 에러, 전원 공급 문제, 과열(Thermal Trip)은 NMI를 유발할 수 있는 하드웨어 에러다. Watchdog Timer, 수동 NMI 버튼, 시스템 관리 모드(SMM) 진입도 관련 시스템 이벤트로 제시된다.

// NMI 핸들러
void nmi_handler(struct pt_regs *regs) {
    unsigned char reason = inb(NMI_REASON_PORT);

    if (reason & NMI_REASON_SERR) {
        printk("Hardware error detected!\n");
        panic("System bus error");
    }

    if (reason & NMI_REASON_IOCHK) {
        printk("I/O channel check\n");
        // 복구 시도
    }
}

NMI가 발생하면 현재 작업은 즉시 중단되고 NMI 핸들러가 실행된다. 복구할 수 없는 상태라면 시스템을 중단할 수 있다. 핸들러가 다시 진입하는 상황을 막으려면 처리 상태를 별도로 관리해야 한다.

static atomic_t nmi_active = ATOMIC_INIT(0);

void nmi_handler(struct pt_regs *regs) {
    // 재진입 방지
    if (atomic_inc_return(&nmi_active) != 1) {
        // 이미 NMI 처리 중
        atomic_dec(&nmi_active);
        return;
    }

    // NMI 처리
    handle_nmi(regs);

    atomic_dec(&nmi_active);
}

NMI는 시스템 덤프를 유발하는 디버깅 용도로도 사용할 수 있다.

// NMI를 이용한 시스템 덤프
void trigger_crash_dump(void) {
    // NMI 발생
    outb(0x00, 0x70);
    outb(0x01, 0x71);
}
인터럽트마스커블넌마스커블CLI로비활성화일반I/O비활성화불가치명적에러

벡터 테이블로 핸들러를 찾는 방식

Vectored Interrupt는 인터럽트마다 고유한 번호인 벡터를 부여하고, 그 번호로 핸들러 주소를 직접 찾는 구조다. 폴링처럼 모든 장치를 차례로 확인하지 않아도 되므로 빠르게 디스패치할 수 있고, 여러 핸들러를 분리해 확장할 수 있다. Non-Vectored 방식은 단일 핸들러에서 폴링이 필요하지만, 벡터 방식은 해당 핸들러를 직접 호출한다.

x86에서는 IDT(Interrupt Descriptor Table)가 벡터와 핸들러 정보를 연결한다.

struct idt_entry {
    uint16_t offset_low;   // 핸들러 주소 하위
    uint16_t selector;     // 코드 세그먼트
    uint8_t  ist;          // Interrupt Stack Table
    uint8_t  type_attr;    // 타입 및 속성
    uint16_t offset_mid;   // 중간
    uint32_t offset_high;  // 상위
    uint32_t zero;
} __attribute__((packed));

struct idt_entry idt[256];  // 256개 벡터

벡터 번호는 예외와 외부 인터럽트를 구분하는 데도 사용된다.

// x86 벡터 할당
#define VECTOR_DIVIDE_ERROR    0
#define VECTOR_DEBUG           1
#define VECTOR_NMI             2
#define VECTOR_BREAKPOINT      3
#define VECTOR_OVERFLOW        4
// ...
#define VECTOR_PAGE_FAULT      14
// ...
#define VECTOR_TIMER           32
#define VECTOR_KEYBOARD        33
#define VECTOR_DISK            46
void set_interrupt_gate(int vector, void (*handler)(void)) {
    idt[vector].offset_low = (uint64_t)handler & 0xFFFF;
    idt[vector].selector = KERNEL_CS;
    idt[vector].ist = 0;
    idt[vector].type_attr = 0x8E;  // Present, DPL=0, Interrupt Gate
    idt[vector].offset_mid = ((uint64_t)handler >> 16) & 0xFFFF;
    idt[vector].offset_high = ((uint64_t)handler >> 32);
    idt[vector].zero = 0;
}

// 사용
set_interrupt_gate(VECTOR_TIMER, timer_interrupt);
set_interrupt_gate(VECTOR_KEYBOARD, keyboard_interrupt);

폴링 핸들러는 어떤 장치가 요청을 발생시켰는지 순서대로 검사한다.

void single_interrupt_handler(void) {
    if (check_timer_interrupt()) {
        handle_timer();
    } else if (check_keyboard_interrupt()) {
        handle_keyboard();
    } else if (check_disk_interrupt()) {
        handle_disk();
    }
    // 모든 장치 확인 필요 (O(n))
}

벡터 방식에서는 하드웨어가 벡터를 제공하고 CPU가 해당 IDT 항목에서 핸들러를 직접 호출한다.

// 하드웨어가 벡터 제공
// CPU가 IDT[vector]에서 핸들러 직접 호출 (O(1))
항목 폴링 벡터
핸들러 찾기 O(n) 폴링 O(1) 직접
지연 시간 높음 낮음
확장성 나쁨 좋음
구현 간단 복잡

아키텍처마다 다른 인터럽트 경로

x86에서 마스커블 인터럽트는 INTR 핀으로 전달되며 IF 플래그로 제어한다. PIC/APIC가 이를 관리한다. NMI는 NMI 핀을 사용하고 벡터 2번에 고정되며 IF 플래그를 무시한다.

// 0-31: 예외 및 시스템 예약
// 32-255: 외부 인터럽트

void setup_x86_interrupts(void) {
    set_trap_gate(0, divide_error);
    set_trap_gate(1, debug);
    set_intr_gate(2, nmi);
    set_system_gate(3, int3);
    // ...
    set_intr_gate(32, timer_interrupt);
    set_intr_gate(33, keyboard_interrupt);
}

ARM은 IRQ를 Maskable Interrupt로, FIQ를 더 높은 우선순위의 Fast Interrupt로 구분한다. NMI는 일부 ARM 구현에서 지원한다. CPSR의 I 비트와 F 비트는 각각 IRQ와 FIQ의 마스킹에 사용된다.

// IRQ 비활성화 (CPSR I 비트)
void disable_irq(void) {
    asm volatile(
        "mrs r0, cpsr\n"
        "orr r0, r0, #0x80\n"  // I 비트 설정
        "msr cpsr_c, r0\n"
        ::: "r0"
    );
}

// FIQ 비활성화 (CPSR F 비트)
void disable_fiq(void) {
    asm volatile(
        "mrs r0, cpsr\n"
        "orr r0, r0, #0x40\n"  // F 비트 설정
        "msr cpsr_c, r0\n"
        ::: "r0"
    );
}

ARM 벡터 테이블은 각 예외와 인터럽트에 대응하는 핸들러 주소를 로드한다.

.global vectors
vectors:
    ldr pc, reset_handler_addr
    ldr pc, undefined_handler_addr
    ldr pc, swi_handler_addr
    ldr pc, prefetch_abort_handler_addr
    ldr pc, data_abort_handler_addr
    nop
    ldr pc, irq_handler_addr
    ldr pc, fiq_handler_addr

MIPS에서는 Status 레지스터의 IE 비트와 IM 필드로 마스킹을 제어한다. General Exception은 0x180에 배치되며, 인터럽트 원인은 Cause 레지스터로 구분한다.

void mips_interrupt_handler(void) {
    uint32_t cause = read_c0_cause();
    uint32_t pending = (cause & read_c0_status() & ST0_IM) >> 8;

    if (pending & CAUSEF_IP7) {
        handle_timer_interrupt();
    } else if (pending & CAUSEF_IP2) {
        handle_external_interrupt();
    }
}

전역·개별·우선순위 마스킹

전역 마스킹은 모든 마스커블 인터럽트를 비활성화한다. Linux 스타일의 local_irq_savelocal_irq_restore는 플래그 저장과 복원을 함께 수행한다.

// Linux 스타일
unsigned long flags;
local_irq_save(flags);      // 인터럽트 비활성화 + 플래그 저장

// Critical Section
critical_code();

local_irq_restore(flags);   // 플래그 복원
#define local_irq_save(flags) \
    do { \
        flags = save_flags_cli(); \
    } while (0)

#define local_irq_restore(flags) \
    restore_flags(flags)

PIC(8259)는 IRQ별 마스킹을 제공한다.

void mask_irq(int irq) {
    uint16_t port;
    uint8_t value;

    if (irq < 8) {
        port = PIC1_DATA;
    } else {
        port = PIC2_DATA;
        irq -= 8;
    }

    value = inb(port) | (1 << irq);
    outb(port, value);
}

void unmask_irq(int irq) {
    uint16_t port;
    uint8_t value;

    if (irq < 8) {
        port = PIC1_DATA;
    } else {
        port = PIC2_DATA;
        irq -= 8;
    }

    value = inb(port) & ~(1 << irq);
    outb(port, value);
}

APIC에서도 벡터별로 마스킹 상태를 설정하거나 해제할 수 있다.

void apic_mask_irq(int vector) {
    uint32_t *lvt = (uint32_t*)(APIC_BASE + APIC_LVT0 + vector * 4);
    *lvt |= APIC_LVT_MASKED;
}

void apic_unmask_irq(int vector) {
    uint32_t *lvt = (uint32_t*)(APIC_BASE + APIC_LVT0 + vector * 4);
    *lvt &= ~APIC_LVT_MASKED;
}

우선순위를 기준으로 낮은 우선순위의 인터럽트만 마스킹할 수도 있다.

void mask_below_priority(int priority) {
    // TPR (Task Priority Register) 설정
    write_apic(APIC_TASKPRI, priority << 4);
}

코드 경계에서 마스킹을 다루는 예

인터럽트를 비활성화한 상태에서 스핀락을 획득하면, 로컬 인터럽트 핸들러와 공유 데이터가 충돌하는 상황을 막을 수 있다.

typedef struct {
    volatile int locked;
} spinlock_t;

void spin_lock_irqsave(spinlock_t *lock, unsigned long *flags) {
    *flags = save_flags_cli();  // 인터럽트 비활성화

    while (__sync_lock_test_and_set(&lock->locked, 1)) {
        // Busy-wait
        cpu_relax();
    }
}

void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) {
    __sync_lock_release(&lock->locked);
    restore_flags(flags);  // 인터럽트 복원
}

PIC 초기화 중에는 먼저 모든 인터럽트를 마스킹하고, 벡터 오프셋과 마스터·슬레이브 연결을 설정한 뒤 필요한 IRQ만 언마스크한다.

void init_pic(void) {
    // 모든 인터럽트 마스킹
    outb(PIC1_DATA, 0xFF);
    outb(PIC2_DATA, 0xFF);

    // PIC 초기화
    outb(PIC1_COMMAND, ICW1_INIT | ICW1_ICW4);
    outb(PIC2_COMMAND, ICW1_INIT | ICW1_ICW4);

    // 벡터 오프셋 설정
    outb(PIC1_DATA, 0x20);  // IRQ 0-7 → 벡터 32-39
    outb(PIC2_DATA, 0x28);  // IRQ 8-15 → 벡터 40-47

    // 마스터/슬레이브 연결
    outb(PIC1_DATA, 4);
    outb(PIC2_DATA, 2);

    // 8086 모드
    outb(PIC1_DATA, ICW4_8086);
    outb(PIC2_DATA, ICW4_8086);

    // 필요한 인터럽트만 언마스크
    unmask_irq(IRQ_TIMER);
    unmask_irq(IRQ_KEYBOARD);
}

NMI 워치독은 마지막 NMI 시점을 기록하고, 지정한 시간 안에 갱신되지 않으면 시스템 lockup으로 처리한다.

static unsigned long last_nmi_timestamp = 0;

void nmi_watchdog_tick(void) {
    last_nmi_timestamp = jiffies;
}

void check_nmi_watchdog(void) {
    if (jiffies - last_nmi_timestamp > WATCHDOG_TIMEOUT) {
        printk("NMI watchdog timeout!\n");
        panic("System lockup detected");
    }
}

응답성을 해치지 않는 운영 원칙

마스킹은 필요한 임계 구역에만 적용해야 한다. 인터럽트 비활성화 시간을 최소화하고, Lock-free 알고리즘과 Read-Copy-Update(RCU)를 고려할 수 있다. 긴 계산을 인터럽트가 막힌 상태에서 수행하는 것과 필요한 공유 데이터 갱신만 그 구간에 두는 방식의 차이는 다음 코드로 확인할 수 있다.

// 나쁜 예: 긴 시간 인터럽트 비활성화
void bad_example(void) {
    disable_interrupts();
    long_computation();  // 수 밀리초
    enable_interrupts();
}

// 좋은 예: 필요한 부분만
void good_example(void) {
    prepare_data();  // 인터럽트 활성화 상태

    disable_interrupts();
    update_shared_data();  // 수 마이크로초
    enable_interrupts();

    post_process();  // 인터럽트 활성화 상태
}

NMI 핸들러에는 최소한의 처리만 남기고, 무거운 작업은 별도 컨텍스트로 넘긴다.

void nmi_handler(struct pt_regs *regs) {
    // 최소한의 처리만
    nmi_count++;

    if (is_critical_error()) {
        set_flag_for_later_processing();
    }

    // 빠른 복귀
}

// 별도 컨텍스트에서 무거운 작업
void nmi_bottom_half(void) {
    if (nmi_flag) {
        handle_critical_error();
        nmi_flag = 0;
    }
}

자주 발생하는 인터럽트와 드물게 발생하는 인터럽트의 벡터 배치, 그리고 벡터 공유 여부도 디스패치 구조의 일부다.

// 자주 발생하는 인터럽트를 낮은 번호에
#define VECTOR_TIMER       32  // 자주 발생
#define VECTOR_NETWORK     33
#define VECTOR_DISK        34

// 드물게 발생하는 인터럽트를 높은 번호에
#define VECTOR_USB         100
#define VECTOR_SOUND       101

MSI(Message Signaled Interrupts)를 사용하면 장치별 고유 벡터를 할당할 수 있다.

// MSI (Message Signaled Interrupts) 사용
void setup_msi(struct pci_dev *dev, int vector) {
    // 각 장치에 고유 벡터 할당
    pci_enable_msi(dev);
    pci_write_config_dword(dev, PCI_MSI_ADDRESS, msi_address);
    pci_write_config_dword(dev, PCI_MSI_DATA, vector);
}
인터럽트운영체제마스커블 인터럽트NMI벡터 인터럽트