인터럽트 우선순위와 선점 제어 방식
인터럽트 동시 발생 시 처리 순서를 정하는 우선순위 체계와 네스팅, 역전 해결 방식, 아키텍처별 구현을 정리한다.
2026-08-14 · 최초 발행 2026-01-06
동시에 들어온 인터럽트의 처리 순서
인터럽트 우선순위는 여러 인터럽트가 같은 시점에 발생했거나 ISR 실행 중 새 인터럽트가 도착했을 때, 무엇을 먼저 처리할지 정하는 메커니즘이다. 중요도와 긴급도를 기준으로 하드웨어 또는 소프트웨어가 처리 순서를 관리한다.
높은 우선순위의 인터럽트는 낮은 우선순위 작업을 선점할 수 있다. 이 특성은 치명적 오류에 빠르게 대응하고, 시스템의 응답성과 예측 가능한 동작을 유지하는 데 쓰인다. NMI는 항상 선점하며, 일반적인 우선순위는 타이머, 디스크, 네트워크, 키보드 순으로 낮아진다.
우선순위가 없다면 동시 이벤트를 FIFO로 다루게 되어 중요한 이벤트가 뒤로 밀릴 수 있다. 하드웨어 고장과 데이터 손실 위험에 대응하고, 실시간 처리의 마감 시간을 지키기 위해 우선순위가 필요하다. 다만 낮은 우선순위도 결국 실행되어야 하므로 기아를 막는 타임아웃 메커니즘과, Critical Section에만 한정한 짧은 인터럽트 비활성화가 함께 요구된다.
고정 우선순위와 동적 조정
고정 우선순위는 인터럽트마다 순위를 설계 단계에서 정하고 런타임에는 변경하지 않는 방식이다. 단순하고 예측 가능하며 하드웨어 구현이 쉬운 반면, 실행 중인 상황 변화에는 유연하게 대응하기 어렵다.
// 인터럽트 우선순위 정의
#define IRQ_PRIORITY_CRITICAL 0 // 최고
#define IRQ_PRIORITY_TIMER 1
#define IRQ_PRIORITY_DISK 2
#define IRQ_PRIORITY_NETWORK 3
#define IRQ_PRIORITY_KEYBOARD 4 // 최저
동적 우선순위는 시스템 상태에 맞춰 런타임에 순위를 조정하는 소프트웨어 방식이다. 대기 시간이 길어진 인터럽트의 우선순위를 높여 기아 현상을 줄이고 워크로드 변화에 대응할 수 있지만, 복잡도와 오버헤드가 늘어나며 동작 예측은 어려워진다.
// 대기 시간에 따른 우선순위 상승
void adjust_interrupt_priority(int irq) {
if (wait_time[irq] > THRESHOLD) {
priority[irq] += PRIORITY_BOOST;
}
}
일반적인 우선순위 체계에서는 NMI, Machine Check, Double Fault, Debug Exception이 가장 높은 위치에 놓인다. 그 다음은 Timer Interrupt, Power Failure, Memory Parity Error이며, Disk I/O·Network I/O·Serial Port가 중간 수준을 이룬다. Keyboard, Mouse, Sound Card는 저우선순위에 배치된다.
하드웨어와 소프트웨어가 나누는 역할
PIC에서는 IRQ0이 최고 우선순위이고 IRQ7이 최저 우선순위이며, 고정 우선순위 체계를 따른다. APIC는 벡터 번호로 우선순위를 정하고 높은 벡터 번호가 높은 우선순위를 뜻하며 동적 조정도 가능하다.
// APIC 우선순위 레지스터
void set_interrupt_priority(int vector, int priority) {
uint32_t tpr = read_msr(IA32_TPR);
tpr = (tpr & ~0xFF) | (priority << 4);
write_msr(IA32_TPR, tpr);
}
소프트웨어가 관리하는 우선순위 모델의 예로 Windows IRQL이 있다. IRQL은 0 (PASSIVE) ~ 31 (HIGH_LEVEL) 범위를 사용한다. Linux는 Softirq에서 HI_SOFTIRQ를 최고 우선순위로 두고 NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, TIMER_SOFTIRQ, TASKLET_SOFTIRQ 등을 관리한다.
// Windows IRQL
KIRQL old_irql;
KeRaiseIrql(DISPATCH_LEVEL, &old_irql);
// Critical Section
critical_code();
KeLowerIrql(old_irql);
하이브리드 방식에서는 외부 인터럽트 우선순위를 하드웨어가 처리하고, Bottom Half의 우선순위는 소프트웨어가 관리한다. 이렇게 2단계로 나누면 빠른 인터럽트 처리와 후속 작업의 조정이 가능하다.
// Top Half (하드웨어 우선순위)
irqreturn_t my_interrupt(int irq, void *dev_id) {
// 하드웨어 우선순위로 실행
schedule_softirq(MY_SOFTIRQ);
return IRQ_HANDLED;
}
// Bottom Half (소프트웨어 우선순위)
void my_softirq_handler(struct softirq_action *action) {
// 소프트웨어 우선순위로 실행
process_data();
}
ISR 실행 중 발생하는 상위 인터럽트
네스티드 인터럽트는 ISR이 수행되는 동안 더 높은 우선순위의 인터럽트가 발생해 현재 ISR을 잠시 멈추고, 새 ISR을 실행한 뒤 원래 실행으로 돌아오는 방식이다. 새 인터럽트의 우선순위가 현재 우선순위보다 높고, 인터럽트가 활성화되어 있으며 마스킹되지 않아야 한다.
하드웨어가 자동으로 네스팅을 지원할 수 있다.
void timer_isr() {
// 타이머 처리 중...
// 하드웨어가 자동으로 NMI 허용
// NMI 발생 시 선점됨
// 타이머 처리 계속...
}
소프트웨어가 컨텍스트 저장과 상위 인터럽트 허용을 직접 제어하는 방법도 있다.
void low_priority_isr() {
save_context();
// 높은 우선순위 인터럽트 허용
enable_higher_interrupts();
// 처리
process_interrupt();
// 인터럽트 비활성화
disable_interrupts();
restore_context();
}
네스팅이 일어나면 각 ISR의 컨텍스트가 스택에 순서대로 쌓인다.
Stack Growth (Bottom → Top)
+------------------------+
| Main Program Context |
+------------------------+
| Low Priority ISR |
+------------------------+
| Mid Priority ISR |
+------------------------+
| High Priority ISR | <- Current SP
+------------------------+
스택 사용량을 통제하지 않으면 네스팅이 깊어질수록 오버플로우 위험이 커진다. 최대 네스팅 깊이를 제한하는 방식으로 이를 방지할 수 있다.
// 최대 네스팅 깊이 제한
#define MAX_NESTED_INTERRUPTS 5
static int nested_count = 0;
void interrupt_entry() {
if (nested_count >= MAX_NESTED_INTERRUPTS) {
// 더 이상 네스팅 불가
disable_interrupts();
}
nested_count++;
// ISR 실행
nested_count--;
}
공유 자원이 만드는 우선순위 역전
우선순위 역전은 높은 우선순위 태스크가 낮은 우선순위 태스크가 보유한 공유 자원을 기다리는 상황이다. 이때 중간 우선순위 태스크가 낮은 우선순위 태스크를 선점하면, 높은 우선순위 태스크의 대기 시간은 더 길어져 예측하기 어려워진다.
전형적인 흐름은 Low Priority Task가 자원을 Lock한 뒤, High Priority Task가 같은 자원을 요청하면서 시작된다. High Priority Task는 Low Priority Task의 완료를 기다리고, Medium Priority Task가 Low Priority Task를 선점하면 High Priority Task도 Medium Priority Task가 끝날 때까지 대기한다.
// Low Priority Task
void low_priority_task() {
lock(resource);
// 자원 사용 중...
// Medium Priority Task에 선점됨!
// High Priority Task는 계속 대기
}
// Medium Priority Task
void medium_priority_task() {
// Low Priority 선점하여 실행
long_computation();
}
// High Priority Task
void high_priority_task() {
lock(resource); // Low Priority가 해제할 때까지 대기
// ...
}
우선순위 상속에서는 Lock을 보유한 태스크가 대기 중인 태스크의 우선순위를 상속하고, 자원을 해제하면 원래 우선순위로 돌아간다.
void lock(resource_t *res) {
if (res->locked) {
// 현재 소유자에게 우선순위 상속
if (current_task->priority > res->owner->priority) {
res->owner->effective_priority = current_task->priority;
}
wait(res);
}
res->locked = true;
res->owner = current_task;
}
void unlock(resource_t *res) {
res->locked = false;
// 원래 우선순위로 복귀
res->owner->effective_priority = res->owner->base_priority;
}
우선순위 천장 프로토콜은 자원별로 우선순위 천장을 설정하고, Lock을 잡는 즉시 해당 천장 우선순위로 올린다.
void lock_with_ceiling(resource_t *res) {
raise_priority(res->ceiling_priority);
res->locked = true;
}
void unlock_with_ceiling(resource_t *res) {
res->locked = false;
restore_priority();
}
x86, ARM, RISC-V의 우선순위 모델
x86의 PIC (8259)는 마스터 8개 IRQ 라인과 슬레이브 8개 IRQ 라인을 사용하며, IRQ0이 최고이고 IRQ15가 최저다. APIC에는 TPR (Task Priority Register)이 있고 4비트 우선순위 클래스와 4비트 서브 우선순위를 사용한다.
// APIC TPR 설정
void set_task_priority(int priority) {
// priority: 0 (최저) ~ 15 (최고)
uint32_t tpr = priority << 4;
write_msr(IA32_X2APIC_TPR, tpr);
}
ARM의 GIC (Generic Interrupt Controller)는 256단계 우선순위 (0 ~ 255)를 제공하며, 낮은 숫자가 높은 우선순위를 뜻한다. 그룹 우선순위와 서브 우선순위도 사용한다. FIQ는 Fast Interrupt로서 고정 최고 우선순위이고, IRQ는 일반 인터럽트로 우선순위를 설정할 수 있다.
// GIC 우선순위 설정
void set_interrupt_priority(int irq, int priority) {
volatile uint8_t *reg = GIC_DIST_BASE + GIC_DIST_PRI + irq;
*reg = priority;
}
RISC-V에서는 mip (Machine Interrupt Pending)와 mie (Machine Interrupt Enable)를 기준으로 고정된 우선순위 순서를 사용한다. External Interrupt, Software Interrupt, Timer Interrupt 순이다.
// RISC-V 인터럽트 처리
void machine_trap_handler(void) {
uint64_t cause = read_csr(mcause);
uint64_t pending = read_csr(mip);
// 우선순위 순서대로 확인
if (pending & MIP_MEIP) {
handle_external_interrupt();
} else if (pending & MIP_MSIP) {
handle_software_interrupt();
} else if (pending & MIP_MTIP) {
handle_timer_interrupt();
}
}
운영체제가 인터럽트 문맥을 관리하는 방법
Linux에서는 Hardware IRQ가 최고 우선순위, Softirq가 중간 우선순위, Process Context가 최저 우선순위를 갖는다. Softirq의 실행 순서는 다음과 같이 관리된다.
enum {
HI_SOFTIRQ=0, // 최고
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, // 최저
NR_SOFTIRQS
};
Windows는 PASSIVE_LEVEL부터 HIGH_LEVEL까지 IRQL을 정의하며, Device IRQLs는 3-26 범위에 있다.
#define PASSIVE_LEVEL 0
#define LOW_LEVEL 0
#define APC_LEVEL 1
#define DISPATCH_LEVEL 2
// Device IRQLs: 3-26
#define PROFILE_LEVEL 27
#define CLOCK_LEVEL 28
#define IPI_LEVEL 29
#define POWER_LEVEL 30
#define HIGH_LEVEL 31 // 최고
IRQL을 올린 뒤 Critical Section을 수행하고, 완료 후 이전 레벨로 복원할 수 있다.
KIRQL old_irql;
// IRQL 상승
KeRaiseIrql(DISPATCH_LEVEL, &old_irql);
// 높은 IRQL에서 실행
SpinLock(...);
critical_section();
SpinUnlock(...);
// IRQL 복원
KeLowerIrql(old_irql);
FreeRTOS에서는 configMAX_SYSCALL_INTERRUPT_PRIORITY가 시스템 콜을 호출할 수 있는 최고 우선순위를 나타내며, 더 높은 우선순위에서는 OS API를 호출할 수 없다. VxWorks는 256단계 우선순위를 사용하고 0이 최고, 255가 최저이며 인터럽트와 태스크 우선순위를 분리한다.
응답성을 유지하기 위한 제어 기법
유사한 우선순위의 인터럽트를 그룹으로 묶고 그룹 내부에서는 FIFO로 처리하면 구현을 단순화할 수 있다.
#define PRIORITY_CRITICAL 0
#define PRIORITY_HIGH 1
#define PRIORITY_NORMAL 2
#define PRIORITY_LOW 3
struct interrupt_queue {
struct list_head queues[4];
};
Critical Section에서는 모든 인터럽트를 막는 대신 낮은 우선순위만 선택적으로 마스킹할 수 있다. 고우선순위 인터럽트의 처리는 계속 허용하면서 보호 구간을 수행하는 방식이다.
// Critical Section 동안 낮은 우선순위만 마스킹
void mask_lower_interrupts(int threshold) {
for (int i = threshold + 1; i < MAX_IRQS; i++) {
mask_irq(i);
}
}
void critical_section() {
mask_lower_interrupts(PRIORITY_HIGH);
// 고우선순위 인터럽트는 여전히 가능
critical_code();
unmask_all_interrupts();
}
긴 작업은 Bottom Half로 넘기고 그 성격에 맞는 실행 경로를 선택할 수 있다. 긴급 작업은 Softirq, 일반 작업은 Tasklet, 낮은 우선순위 작업은 Work Queue를 사용한다.
// 긴급: Softirq
void urgent_bottom_half() {
raise_softirq(HI_SOFTIRQ);
}
// 일반: Tasklet
void normal_bottom_half() {
tasklet_schedule(&my_tasklet);
}
// 낮음: Work Queue
void low_priority_bottom_half() {
schedule_work(&my_work);
}