Softirq & Hardirq
Linux 커널의 하드웨어 인터럽트(Hardirq)와 소프트웨어 인터럽트(Softirq)를 심층 분석합니다. 인터럽트 컨텍스트, Softirq 메커니즘, ksoftirqd, Tasklet, NAPI, 성능 최적화까지 종합적으로 다룹니다.
특히 "어떤 작업을 Hardirq에서 끝내고 어떤 작업을 Softirq로 넘겨야 하는가"를 판단할 수 있도록 실행 컨텍스트 제약, 지연(Latency) 시간 예산, per-CPU 병렬성, ksoftirqd/N로의 인계 조건을 구체적으로 설명합니다. 네트워크 경로의 NET_RX_SOFTIRQ 적체, CPU 사용률 급등, tail latency 증가 같은 현장을 기준으로 모니터링 지표와 튜닝 순서를 함께 제시하여 성능 문제를 재현 가능하게 분석할 수 있도록 했습니다.
핵심 요약
- Hardirq (Top Half) — 하드웨어 인터럽트 핸들러로, IRQ 컨텍스트에서 최소한의 작업만 수행하고 즉시 반환합니다. 이 구간에서는 sleep과 프로세스 스케줄링이 금지됩니다.
- Softirq (Bottom Half) — hardirq가 등록한 지연 작업을 처리하는 메커니즘으로,
NET_TX_SOFTIRQ,NET_RX_SOFTIRQ,TIMER_SOFTIRQ,BLOCK_SOFTIRQ등 10개 정적 타입이 존재합니다. - ksoftirqd — softirq 처리가 과도하게 지연될 때 전용 커널 스레드로 위임하여, softirq가 일반 프로세스를 무기한 차단하는 것을 방지합니다.
- IRQ 컨텍스트 규칙 — hardirq 컨텍스트에서는 뮤텍스(Mutex) 획득, 메모리 할당(
GFP_KERNEL), 유저 공간 접근이 불가하며,in_irq()/in_softirq()로 현재 컨텍스트를 판별합니다. - local_bh_disable/enable — softirq 실행을 지역적으로 비활성화하여 softirq 핸들러와의 경쟁 조건(Race Condition)을 방지하는 동기화 프리미티브입니다.
- /proc/softirqs — CPU별 softirq 타입별 누적 실행 횟수를 보여주는 관측 인터페이스로, 부하 분포와 병목 분석에 활용합니다.
- PREEMPT_RT 변환 — PREEMPT_RT 커널에서는 대부분의 softirq가 커널 스레드로 강제 위임되어, 실시간(Real-time) 태스크의 지연 시간 예측 가능성을 높입니다.
단계별 이해
- Top Half / Bottom Half 분리 원칙 이해 — 인터럽트 처리를 즉시 처리(hardirq)와 지연 처리(softirq/tasklet)로 나누는 이유를 파악합니다.
hardirq는 다른 인터럽트를 차단하므로 최대한 짧게 유지해야 하며, 복잡한 처리는 softirq로 위임합니다. 이 분리가 시스템 응답성의 핵심입니다.
- Softirq 타입별 역할 파악 —
NET_RX,NET_TX,TIMER,BLOCK,SCHED,RCU등 각 softirq 타입이 담당하는 서브시스템을 확인합니다./proc/softirqs를 주기적으로 읽어 어떤 타입이 어느 CPU에서 많이 실행되는지 분포를 관찰합니다. - 실행 흐름 추적: irq_exit()에서 ksoftirqd까지 — hardirq 종료 시 pending softirq를 확인하고, 즉시 실행 또는
ksoftirqd위임을 결정하는 경로를 따라갑니다.raise_softirq()가 비트를 설정하고,__do_softirq()가 루프를 돌며 처리하는 구조를 코드 수준에서 확인합니다. - 동기화와 컨텍스트 규칙 점검 — softirq 핸들러 안에서 사용 가능한 잠금(spinlock, per-CPU 변수)과 금지 사항(sleep, mutex)을 정리합니다.
local_bh_disable()/local_bh_enable()로 프로세스 컨텍스트에서 softirq와의 경쟁을 방지하는 패턴을 익힙니다. - 성능 관측과 병목 진단 —
/proc/softirqs,/proc/interrupts,ftrace의softirq이벤트를 조합하여 softirq 지연과 CPU 편중을 진단합니다.네트워크 부하 환경에서
NET_RX_SOFTIRQ가 특정 CPU에 집중되면 RPS(Receive Packet Steering) 설정으로 분산합니다.
개요
Linux 커널은 인터럽트 처리를 상반부(Top Half)와 하반부(Bottom Half)로 분리합니다. 이 분리는 초기 Unix 시스템에서부터 이어진 설계 원칙으로, 하드웨어 응답성과 복잡한 처리 로직을 동시에 만족시키기 위한 핵심 아키텍처입니다.
역사적 배경
Bottom Half의 개념은 Linux 2.0 시절 bh_base[] 배열(32개 슬롯)에서 시작되었습니다. 이 구조는 전역 락으로 보호되어 SMP 확장성이 매우 낮았습니다. Linux 2.3에서 softirq와 tasklet이 도입되면서 Per-CPU 실행이 가능해졌고, 2.5에서 workqueue가 추가되어 프로세스(Process) 컨텍스트 Bottom Half를 완성했습니다. Linux 4.x 이후에는 PREEMPT_RT 패치(Patch)셋의 mainline 통합이 진행되면서 softirq의 스레드화가 본격적으로 논의되었습니다.
| 커널 버전 | Bottom Half 변천 | 특징 |
|---|---|---|
| 2.0 | BH (Bottom Half) | 전역 배열 32슬롯, 전역 락, SMP 병목 |
| 2.3 | Softirq + Tasklet | Per-CPU 실행, 10개 고정 벡터, 동적 tasklet |
| 2.5 | Workqueue | 프로세스 컨텍스트, sleep 가능 |
| 2.6.30+ | Threaded IRQ | request_threaded_irq(), 스레드(Thread) 핸들러(Handler) |
| 5.x+ | PREEMPT_RT mainline | softirq 완전 스레드화, 결정적 지연시간 |
Hardirq vs Softirq
| 구분 | Hardirq (Top Half) | Softirq (Bottom Half) |
|---|---|---|
| 트리거 | 하드웨어 인터럽트 | 소프트웨어 (raise_softirq) |
| 실행 시점 | 즉시 (선점(Preemption) 가능) | Hardirq 종료 후, idle 진입 시, ksoftirqd |
| 인터럽트 차단 | 로컬 CPU 인터럽트 비활성화 | 인터럽트 활성화 상태 |
| 스케줄링 | 불가 (sleep 금지) | 불가 (프로세스 컨텍스트 아님) |
| 실행 시간 | 매우 짧음 (수 μs) | 상대적으로 김 (수십 μs ~ ms) |
| 재진입 | 불가 (같은 IRQ) | 가능 (다른 CPU에서 동시 실행) |
| 대표 작업 | 하드웨어 ACK, 데이터 읽기 | 패킷(Packet) 처리, 타이머(Timer) 콜백(Callback), 블록 I/O |
인터럽트 처리 흐름
Hardirq (하드웨어 인터럽트)
Hardirq는 하드웨어가 CPU에 신호를 보내 즉시 실행되는 인터럽트 핸들러입니다.
Hardirq 핸들러 등록
/* include/linux/interrupt.h */
int request_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
/* 예제: UART 드라이버 */
static irqreturn_t uart_interrupt(int irq, void *dev_id)
{
struct uart_port *port = dev_id;
unsigned int status;
/* 1. 하드웨어 상태 읽기 */
status = readl(port->membase + UART_STATUS);
/* 2. 최소한의 처리 */
if (status & UART_RX_READY) {
char ch = readl(port->membase + UART_DATA);
uart_insert_char(port, ch); /* 버퍼에 저장만 */
}
/* 3. Softirq로 실제 처리 위임 */
tasklet_schedule(&port->tasklet);
return IRQ_HANDLED;
}
Hardirq 제약사항
Hardirq 컨텍스트에서는 다음이 절대 금지됩니다:
sleep(),schedule()호출 불가mutex_lock()사용 불가 (spinlock만 가능)kmalloc(GFP_KERNEL)불가 (GFP_ATOMIC만 가능)copy_to_user()/copy_from_user()불가- 긴 루프나 복잡한 연산 금지
위반 시 커널 패닉(Kernel Panic)이나 데드락 발생 가능!
상세 문서: IRQ 핸들러 등록(request_irq), genirq 프레임워크, GIC/APIC 인터럽트 컨트롤러(Interrupt Controller), IRQ 도메인 계층 등의 상세 내용은 인터럽트 페이지(Page)를 참조하세요.
인터럽트 스택 전환 메커니즘
하드웨어 인터럽트가 발생하면 CPU는 현재 프로세스의 커널 스택에서 전용 Per-CPU IRQ 스택으로 전환합니다. 이는 인터럽트 핸들러가 현재 태스크의 스택 공간을 소비하여 스택 오버플로(Stack Overflow)를 유발하는 것을 방지합니다.
/* arch/x86/kernel/irq_64.c - Per-CPU IRQ 스택 정의 */
DEFINE_PER_CPU_PAGE_ALIGNED(struct irq_stack, irq_stack_backing_store)
__aligned(IRQ_STACK_SIZE);
/* IRQ 스택 크기 = THREAD_SIZE (보통 16KB, x86_64) */
#define IRQ_STACK_SIZE THREAD_SIZE
/* arch/x86/include/asm/irq_stack.h - 스택 전환 매크로 */
#define call_on_irqstack_cond(func, regs, asm_call, argconstr, ...) \
({ \
/* 이미 IRQ 스택에 있는지 확인 */ \
if (__this_cpu_read(pcpu_hot.hardirq_stack_inuse)) { \
func(__VA_ARGS__); /* 이미 IRQ 스택 → 직접 호출 */ \
} else { \
__this_cpu_write(pcpu_hot.hardirq_stack_inuse, true); \
asm_call_irq_on_stack(func, __VA_ARGS__); \
__this_cpu_write(pcpu_hot.hardirq_stack_inuse, false); \
} \
})
x86_64에서는 추가로 IST(Interrupt Stack Table)가 존재합니다. TSS(Task State Segment)에 7개의 IST 항목을 정의할 수 있으며, NMI, Double Fault, MCE(Machine Check Exception) 등 치명적 예외는 IST를 통해 현재 스택 상태와 무관하게 안전한 스택에서 실행됩니다.
| IST 인덱스 | 이름 | 용도 | 스택 크기 |
|---|---|---|---|
| 1 | DOUBLEFAULT_STACK | Double Fault | IRQ_STACK_SIZE |
| 2 | NMI_STACK | Non-Maskable Interrupt | IRQ_STACK_SIZE |
| 3 | DEBUG_STACK | 디버그 예외 (#DB) | IRQ_STACK_SIZE |
| 4 | MCE_STACK | Machine Check Exception | IRQ_STACK_SIZE |
| 5 | VC_STACK | VMM Communication (SEV-ES) | IRQ_STACK_SIZE |
IRQ 스택 오버플로
IRQ 스택은 16KB로 제한됩니다. Hardirq 핸들러에서 깊은 호출 체인(deep call chain)을 유발하면 IRQ 스택 오버플로가 발생하여 커널 패닉으로 이어집니다. 특히 WARN_ON()이나 printk() 같은 디버그 출력도 상당한 스택 공간을 소비하므로, 인터럽트 핸들러에서는 최소한의 작업만 수행해야 합니다.
IRQF 플래그 상세 레퍼런스
request_irq() 호출 시 전달하는 flags 매개변수는 인터럽트의 동작 방식을 세밀하게 제어합니다. 다음은 include/linux/interrupt.h에 정의된 주요 IRQF 플래그입니다.
| 플래그 | 설명 | 주요 용도 |
|---|---|---|
IRQF_SHARED | 여러 디바이스가 하나의 IRQ 라인을 공유 | PCI 레거시 인터럽트 |
IRQF_ONESHOT | Hardirq 후 재활성화하지 않고 threaded 핸들러 완료 후 재활성화 | Level-triggered + threaded IRQ |
IRQF_NO_SUSPEND | 시스템 Suspend 중에도 IRQ 비활성화하지 않음 | RTC, 전원 버튼, 웨이크업(Wakeup) 소스 |
IRQF_NOBALANCING | irqbalance에 의한 affinity 재배치 제외 | Per-CPU 인터럽트, 타이머 |
IRQF_NO_THREAD | PREEMPT_RT에서도 강제 스레드화 방지 | 시간 임계 인터럽트 (타이머) |
IRQF_PERCPU | Per-CPU 인터럽트로 등록 (affinity 고정) | IPI, 로컬 APIC 타이머 |
IRQF_TRIGGER_RISING | 상승 에지(Rising Edge)에서 트리거 | GPIO 인터럽트 |
IRQF_TRIGGER_FALLING | 하강 에지(Falling Edge)에서 트리거 | GPIO 인터럽트 |
IRQF_TRIGGER_HIGH | 높은 레벨(High Level)에서 트리거 | Level-triggered 디바이스 |
IRQF_TRIGGER_LOW | 낮은 레벨(Low Level)에서 트리거 | Active-low 디바이스 |
IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD 조합 | 시스템 타이머 |
IRQF_PROBE_SHARED | IRQ 프로빙(Probing) 시 공유 허용 | 레거시 디바이스 탐지 |
/* 실전 IRQF 플래그 조합 예제 */
/* 1) PCI 디바이스 공유 IRQ */
request_irq(pci_irq, my_pci_handler, IRQF_SHARED,
"my_pci_dev", dev);
/* 2) Threaded IRQ + Level-triggered (I2C 센서 등)
* IRQF_ONESHOT 필수: level-triggered에서 없으면
* threaded handler 실행 전에 인터럽트가 재발생하여 IRQ storm */
request_threaded_irq(gpio_irq,
NULL, /* hardirq handler 없음 */
sensor_thread_fn,
IRQF_TRIGGER_LOW | IRQF_ONESHOT,
"i2c_sensor", sensor_data);
/* 3) 웨이크업 소스 (전원 버튼) */
request_irq(pwr_irq, power_btn_handler,
IRQF_NO_SUSPEND | IRQF_TRIGGER_FALLING,
"power_btn", NULL);
IRQF_ONESHOT이 필수인 이유
Level-triggered 인터럽트에서 request_threaded_irq()를 사용할 때 IRQF_ONESHOT을 생략하면, hardirq 핸들러가 리턴한 즉시 인터럽트가 재활성화됩니다. 그러나 threaded 핸들러가 아직 인터럽트 원인을 제거하지 않았으므로 인터럽트가 즉시 다시 발생하여 IRQ storm이 됩니다. IRQF_ONESHOT은 threaded 핸들러 완료까지 재활성화를 지연시킵니다.
Edge Trigger vs Level Trigger
인터럽트 신호 방식은 크게 Edge-triggered와 Level-triggered로 나뉘며, 각각 다른 flow handler가 처리합니다. 이 차이는 인터럽트 유실과 IRQ storm 방지에 직접 영향을 미칩니다.
| 트리거 방식 | Flow Handler | ACK 타이밍 | 인터럽트 재활성화 | 핵심 고려사항 |
|---|---|---|---|---|
| Edge | handle_edge_irq() | 핸들러 진입 전 ACK | 항상 활성 | 핸들러 실행 중 재발생하면 유실 가능 |
| Level | handle_level_irq() | 핸들러 진입 전 Mask | 핸들러 완료 후 Unmask | 핸들러가 HW 원인을 제거해야 함 |
| FastEOI | handle_fasteoi_irq() | 핸들러 완료 후 EOI | 자동 | MSI/MSI-X 기본 핸들러 |
| Simple | handle_simple_irq() | 없음 | 없음 | 소프트웨어 생성 인터럽트 |
| Percpu | handle_percpu_irq() | Per-CPU ACK | Per-CPU | IPI, 로컬 타이머 |
/* kernel/irq/chip.c - Edge vs Level 핵심 차이점 */
/* Edge-triggered: ACK 먼저 → 핸들러 → 재발생 확인 */
void handle_edge_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
desc->irq_data.chip->irq_ack(&desc->irq_data); /* 즉시 ACK */
do {
handle_irq_event(desc); /* 핸들러 체인 실행 */
} while (desc->istate & IRQS_PENDING); /* 실행 중 재발생? */
raw_spin_unlock(&desc->lock);
}
/* Level-triggered: Mask → 핸들러 → Unmask */
void handle_level_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
mask_ack_irq(desc); /* Mask + ACK (재발생 방지) */
handle_irq_event(desc); /* 핸들러가 HW 원인 제거 */
cond_unmask_irq(desc); /* Unmask (이제 안전) */
raw_spin_unlock(&desc->lock);
}
Softirq 타입
Linux는 정적으로 10가지 Softirq 타입을 정의합니다.
Softirq 우선순위(Priority) 순서
| 번호 | 이름 | 용도 | 주요 사용처 |
|---|---|---|---|
| 0 | HI_SOFTIRQ | 고우선순위 Tasklet | 드라이버 Tasklet (높은 우선순위) |
| 1 | TIMER_SOFTIRQ | 타이머 만료 | 커널 타이머, hrtimer |
| 2 | NET_TX_SOFTIRQ | 네트워크 송신 | 패킷 전송 완료 처리 |
| 3 | NET_RX_SOFTIRQ | 네트워크 수신 | 패킷 수신 처리 (NAPI) |
| 4 | BLOCK_SOFTIRQ | 블록 I/O | 블록 디바이스 I/O 완료 |
| 5 | IRQ_POLL_SOFTIRQ | IRQ 폴링(Polling) | 고성능 블록 디바이스 (NVMe) |
| 6 | TASKLET_SOFTIRQ | 일반 Tasklet | 드라이버 Tasklet (일반) |
| 7 | SCHED_SOFTIRQ | 스케줄러(Scheduler) | 로드 밸런싱, CFS |
| 8 | HRTIMER_SOFTIRQ | 고해상도 타이머(hrtimer) | hrtimer 콜백 |
| 9 | RCU_SOFTIRQ | RCU | RCU 콜백 처리 |
Softirq 정의
/* include/linux/interrupt.h */
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
};
/* kernel/softirq.c */
struct softirq_action {
void (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS];
Softirq 메커니즘
Softirq는 raise_softirq()로 스케줄링되고 do_softirq()로 실행됩니다. 핵심 구조는 Per-CPU irq_stat.__softirq_pending 비트맵과 softirq_vec[] 핸들러 배열의 조합입니다.
10종 Softirq 벡터 상세
각 softirq 벡터는 커널의 특정 서브시스템에 전용으로 할당되어 있으며, 초기화 시 open_softirq()로 등록됩니다. 새로운 softirq 추가는 커널 서브시스템 수준의 결정이며, 드라이버에서는 tasklet이나 workqueue를 사용해야 합니다.
| 벡터 | 핸들러 함수 | 등록 위치 | 주요 동작 |
|---|---|---|---|
HI_SOFTIRQ | tasklet_hi_action() | kernel/softirq.c | 고우선순위 tasklet 큐 순회, 드라이버 레거시 코드 |
TIMER_SOFTIRQ | run_timer_softirq() | kernel/time/timer.c | 타이머 휠 순회, 만료 콜백 실행 |
NET_TX_SOFTIRQ | net_tx_action() | net/core/dev.c | 송신 완료 큐 정리, sk_buff 해제 |
NET_RX_SOFTIRQ | net_rx_action() | net/core/dev.c | NAPI poll_list 순회, 패킷 수신 처리 |
BLOCK_SOFTIRQ | blk_done_softirq() | block/blk-softirq.c | 블록 I/O 완료 콜백, request 해제 |
IRQ_POLL_SOFTIRQ | irq_poll_softirq() | lib/irq_poll.c | IRQ 폴링 기반 고성능 블록 처리 (NVMe) |
TASKLET_SOFTIRQ | tasklet_action() | kernel/softirq.c | 일반 우선순위 tasklet 큐 순회 |
SCHED_SOFTIRQ | run_rebalance_domains() | kernel/sched/fair.c | CPU 간 로드 밸런싱, CFS 밸런싱 |
HRTIMER_SOFTIRQ | hrtimer_run_softirq() | kernel/time/hrtimer.c | 고해상도 타이머 콜백 (softirq 모드) |
RCU_SOFTIRQ | rcu_core_si() | kernel/rcu/tree.c | RCU 콜백 배치 처리, grace period 진행 |
Softirq 스케줄링
/* kernel/softirq.c */
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
/* 현재 hardirq 컨텍스트가 아니면 즉시 처리 시도 */
if (!in_interrupt())
wakeup_softirqd();
}
void __raise_softirq_irqoff(unsigned int nr)
{
/* CPU별 pending 비트 설정 */
or_softirq_pending(1UL << nr);
}
Softirq 실행 (__do_softirq)
/* kernel/softirq.c */
asmlinkage void __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
__u32 pending;
restart:
/* Pending softirq 비트 읽기 */
pending = local_softirq_pending();
set_softirq_pending(0);
local_irq_enable(); /* ← 인터럽트 활성화! */
h = softirq_vec;
do {
if (pending & 1) {
unsigned int vec_nr = h - softirq_vec;
trace_softirq_entry(vec_nr);
h->action(h); /* ← Softirq 핸들러 호출 */
trace_softirq_exit(vec_nr);
}
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
/* 새 pending이 생겼고 시간/반복 제한 내면 재실행 */
pending = local_softirq_pending();
if (pending && time_before(jiffies, end) && --max_restart)
goto restart;
/* 여전히 pending이 남았으면 ksoftirqd 깨우기 */
if (pending)
wakeup_softirqd();
}
Softirq 폭주 방지
Softirq는 무한 루프를 방지하기 위해 다음 제한을 둡니다:
MAX_SOFTIRQ_TIME= 2ms (2 jiffies)MAX_SOFTIRQ_RESTART= 10회
이 제한에 도달하면 ksoftirqd에 위임하여 프로세스 컨텍스트에서 처리합니다. 이렇게 하면 Softirq가 CPU를 독점하지 못하게 합니다.
Hardirq 핸들러 체인
하드웨어 인터럽트가 발생하면 CPU는 인터럽트 벡터 테이블을 통해 커널의 인터럽트 엔트리 포인트에 진입합니다. 이후 irq_desc 구조체(Struct)를 통해 등록된 핸들러 체인을 순회합니다.
/* kernel/irq/irqdesc.c - irq_desc 구조체 (핵심 필드) */
struct irq_desc {
struct irq_data irq_data; /* 하드웨어 IRQ 정보 */
struct irqaction *action; /* 핸들러 체인 (linked list) */
struct irq_chip *irq_chip; /* 인터럽트 컨트롤러 ops */
irq_flow_handler_t handle_irq; /* 흐름 핸들러 (level/edge) */
unsigned int irq_count; /* 인터럽트 발생 횟수 */
unsigned int depth; /* disable 깊이 */
const char *name; /* 디바이스 이름 */
};
/* kernel/irq/handle.c - 핸들러 체인 실행 */
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc)
{
struct irqaction *action;
irqreturn_t retval = IRQ_NONE;
/* 공유 IRQ: 모든 핸들러를 순회 */
for_each_action_of_desc(desc, action) {
irqreturn_t res;
res = action->handler(desc->irq_data.irq, action->dev_id);
switch (res) {
case IRQ_WAKE_THREAD:
/* threaded handler 깨우기 */
irq_wake_thread(desc, action);
break;
case IRQ_HANDLED:
retval |= res;
break;
}
}
return retval;
}
irq_exit() → invoke_softirq() 전환
하드웨어 인터럽트 처리가 완료되면 irq_exit()에서 pending softirq를 확인하고 실행합니다. 이것이 Hardirq에서 Softirq로의 전환 지점입니다.
/* kernel/softirq.c */
void irq_exit(void)
{
preempt_count_sub(HARDIRQ_OFFSET); /* hardirq 카운트 감소 */
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); /* softirq 실행 시작 */
tick_irq_exit();
}
static inline void invoke_softirq(void)
{
#ifdef CONFIG_IRQ_FORCED_THREADING
if (force_irqthreads()) {
/* PREEMPT_RT: ksoftirqd에서 처리 */
wakeup_softirqd();
return;
}
#endif
if (!force_irqthreads() || !__this_cpu_read(ksoftirqd)) {
__do_softirq(); /* 인라인 실행 */
} else {
wakeup_softirqd();
}
}
ksoftirqd 커널 스레드(Kernel Thread)
ksoftirqd는 CPU당 하나씩 존재하며, Softirq 부하가 높을 때 처리를 대신합니다.
ksoftirqd 메인 루프
/* kernel/softirq.c */
static int ksoftirqd(void *__bind_cpu)
{
set_current_state(TASK_INTERRUPTIBLE);
while (!kthread_should_stop()) {
if (!local_softirq_pending())
schedule(); /* Pending 없으면 sleep */
set_current_state(TASK_RUNNING);
while (local_softirq_pending()) {
__do_softirq(); /* Softirq 처리 */
cond_resched(); /* 스케줄링 포인트 */
}
set_current_state(TASK_INTERRUPTIBLE);
}
return 0;
}
ksoftirqd 프로세스 확인
ps aux | grep ksoftirqd
# root 5 0.0 0.0 0 0 ? S Jan01 0:12 [ksoftirqd/0]
# root 15 0.0 0.0 0 0 ? S Jan01 0:08 [ksoftirqd/1]
# root 20 0.0 0.0 0 0 ? S Jan01 0:05 [ksoftirqd/2]
# ...
ksoftirqd의 역할
- CPU 공정성(Fairness): Softirq가 CPU를 독점하지 못하게 함
- 우선순위: SCHED_NORMAL (nice 0) - 일반 프로세스와 동등
- 스케줄링: CFS에 의해 스케줄링되어 다른 프로세스에게도 CPU 할당
ksoftirqd CPU 사용률이 높다면 Softirq 부하가 과도하다는 신호입니다 (특히 NET_RX/TX).
ksoftirqd 활성화 조건
ksoftirqd가 깨어나는 경우는 세 가지입니다. 각 경로는 wakeup_softirqd()를 호출하여 해당 CPU의 ksoftirqd 스레드를 TASK_RUNNING으로 전환합니다.
인터럽트 컨텍스트 확인
현재 코드가 어떤 컨텍스트에서 실행 중인지 확인하는 매크로(Macro)입니다.
컨텍스트 확인 매크로
| 매크로 | 의미 | 포함 범위 |
|---|---|---|
in_irq() | Hardirq 컨텍스트 | 하드웨어 인터럽트 핸들러 |
in_softirq() | Softirq 컨텍스트 | Softirq 핸들러, BH 비활성화 구간 |
in_interrupt() | 인터럽트 컨텍스트 | Hardirq + Softirq + NMI |
in_task() | 프로세스 컨텍스트 | 일반 프로세스, 커널 스레드 |
구현
/* include/linux/preempt.h */
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
#define in_task() (!in_interrupt() && !(current->flags & PF_EXITING))
/* 사용 예 */
void my_function(void)
{
if (in_interrupt()) {
/* 인터럽트 컨텍스트 - sleep 금지 */
spin_lock(&my_lock);
} else {
/* 프로세스 컨텍스트 - mutex 사용 가능 */
mutex_lock(&my_mutex);
}
}
preempt_count 구조
인터럽트 컨텍스트 확인 매크로(in_irq(), in_softirq(), in_interrupt() 등)는 모두 preempt_count의 비트 필드를 검사합니다. 이 32비트 카운터는 PREEMPT(비트 0-7), SOFTIRQ(비트 8-15), HARDIRQ(비트 16-19), NMI(비트 20) 영역으로 나뉘어 현재 CPU의 실행 컨텍스트를 인코딩합니다. 비트 필드 구조, 각 매크로의 동작 원리, 선점 모델 비교 등 상세 내용은 preempt_count 문서를 참조하세요.
local_bh_disable/enable 메커니즘
프로세스 컨텍스트에서 softirq 핸들러와 공유 데이터를 보호하려면 local_bh_disable()/local_bh_enable()을 사용합니다. 이 API는 preempt_count의 SOFTIRQ 비트를 조작하여 현재 CPU에서 softirq 실행을 억제합니다. (preempt_count 비트 필드 구조와 in_softirq() 매크로의 동작 원리는 preempt_count 문서에서 다룹니다.)
/* kernel/softirq.c */
void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
/* preempt_count에 SOFTIRQ_DISABLE_OFFSET 추가 */
__preempt_count_add(cnt);
barrier();
}
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
/* 주의: preempt_count 감소 전에 pending softirq 확인 */
__preempt_count_sub(cnt);
if (unlikely(!in_interrupt() && local_softirq_pending())) {
/* BH 활성화 시점에 pending softirq가 있으면 즉시 처리 */
do_softirq();
}
preempt_check_resched();
}
/* 사용 예: 프로세스 컨텍스트에서 softirq 보호 */
void my_data_access(void)
{
local_bh_disable();
/* 이 구간에서는 현재 CPU에서 softirq가 실행되지 않음 */
/* 다른 CPU의 softirq는 여전히 실행 가능 → 공유 데이터면 spinlock 추가 */
shared_data->counter++;
local_bh_enable(); /* pending softirq가 있으면 여기서 처리됨 */
}
/* 동기화 계층 정리 */
/*
* spin_lock_bh() = spin_lock() + local_bh_disable()
* spin_unlock_bh()= spin_unlock() + local_bh_enable()
*
* 용도: 프로세스 컨텍스트 ↔ softirq 간 공유 데이터 보호
* (다른 CPU의 softirq도 차단하려면 lock이 필요)
*/
Per-CPU Softirq Pending 비트맵
각 CPU는 독립적인 __softirq_pending 비트맵을 가집니다. 이 Per-CPU 설계 덕분에 softirq raise/check 시 락이 불필요합니다.
/* arch/x86/include/asm/hardirq.h */
typedef struct {
unsigned int __softirq_pending; /* 10비트 사용 (NR_SOFTIRQS=10) */
unsigned int __nmi_count;
unsigned int apic_timer_irqs;
/* ... */
} irq_cpustat_t;
DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);
/* Per-CPU pending 비트 조작 API */
#define local_softirq_pending() \
__this_cpu_read(irq_stat.__softirq_pending)
#define set_softirq_pending(x) \
__this_cpu_write(irq_stat.__softirq_pending, (x))
#define or_softirq_pending(x) \
__this_cpu_or(irq_stat.__softirq_pending, (x))
각 Softirq 벡터 분석
10개의 softirq 벡터는 각각 커널의 핵심 서브시스템에 전용으로 할당되어 있습니다. 각 벡터의 내부 동작을 이해하면 /proc/softirqs 분석 시 어떤 서브시스템이 병목인지 빠르게 파악할 수 있습니다.
TIMER_SOFTIRQ: 타이머 휠 상호작용
TIMER_SOFTIRQ는 커널 타이머(timer_list)의 만료 콜백을 처리합니다. 타이머 휠(Timer Wheel)은 계층적 구조로 구성되어 효율적인 만료 검사를 수행합니다.
/* kernel/time/timer.c - run_timer_softirq 핵심 경로 */
static void run_timer_softirq(struct softirq_action *h)
{
struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
__run_timers(base);
}
static void __run_timers(struct timer_base *base)
{
raw_spin_lock_irq(&base->lock);
/* base->clk를 현재 jiffies까지 전진시키며 만료 타이머 수집 */
while (time_after_eq(jiffies, base->clk) &&
(base->clk < base->next_expiry)) {
struct hlist_head heads[LVL_DEPTH];
int levels;
/* 타이머 휠 각 레벨에서 만료 타이머 수집 */
levels = collect_expired_timers(base, heads);
base->clk++;
while (levels--) {
expire_timers(base, heads + levels);
}
}
raw_spin_unlock_irq(&base->lock);
}
static void expire_timers(struct timer_base *base,
struct hlist_head *head)
{
while (!hlist_empty(head)) {
struct timer_list *timer;
void (*fn)(struct timer_list *);
timer = hlist_entry(head->first, struct timer_list, entry);
base->running_timer = timer; /* 디버그 추적 */
fn = timer->function;
raw_spin_unlock_irq(&base->lock);
call_timer_fn(timer, fn, base->clk); /* 콜백 실행 */
raw_spin_lock_irq(&base->lock);
}
}
타이머 휠은 9개 레벨(LVL_DEPTH)로 구성되며, 각 레벨은 64개 버킷(Bucket)을 가집니다. 레벨이 올라갈수록 타이머 해상도가 낮아지고 먼 미래의 타이머를 관리합니다.
| 레벨 | 해상도 (granularity) | 범위 | 총 버킷 수 |
|---|---|---|---|
| 0 | 1 tick (1ms @ HZ=1000) | 0 ~ 63 ticks | 64 |
| 1 | 8 ticks | 64 ~ 511 ticks | 64 |
| 2 | 64 ticks | 512 ~ 4095 ticks | 64 |
| 3 | 512 ticks | 4096 ~ 32767 ticks | 64 |
| 4 | 4096 ticks (~4초) | ~32초 ~ ~4분 | 64 |
| 5~8 | 점점 증가 | 수시간 ~ 수일 | 각 64 |
HRTIMER_SOFTIRQ와의 관계: 고해상도 타이머(hrtimer)는 별도의 레드-블랙 트리(Red-Black Tree)를 사용합니다. 하드웨어 hrtimer가 사용 가능하면 hardirq 컨텍스트에서 직접 콜백을 실행하고, 불가능하면 HRTIMER_SOFTIRQ로 폴백합니다. CONFIG_NO_HZ_IDLE/CONFIG_NO_HZ_FULL 설정은 유휴/격리 CPU의 타이머 틱(Tick)을 억제하여 TIMER_SOFTIRQ 빈도를 크게 줄입니다.
BLOCK_SOFTIRQ와 IRQ_POLL_SOFTIRQ: 블록 I/O 완료
BLOCK_SOFTIRQ는 블록 디바이스의 I/O 완료 처리를 담당합니다. 디바이스 드라이버가 I/O 완료를 통지하면 Per-CPU blk_cpu_done 리스트에 request가 쌓이고, softirq에서 배치로 완료 콜백을 실행합니다.
/* block/blk-softirq.c - 블록 I/O 완료 softirq */
static DEFINE_PER_CPU(struct llist_head, blk_cpu_done);
static void blk_done_softirq(struct softirq_action *h)
{
struct llist_node *entry;
/* Per-CPU 완료 리스트를 atomic하게 가져옴 */
entry = llist_del_all(this_cpu_ptr(&blk_cpu_done));
entry = llist_reverse_order(entry);
while (entry) {
struct request *rq;
rq = llist_entry(entry, struct request, ipi_list);
entry = entry->next;
rq->q->mq_ops->complete(rq); /* blk-mq 완료 콜백 */
}
}
/* 드라이버에서 I/O 완료 통지 */
void blk_complete_request(struct request *rq)
{
/* 완료를 처리할 CPU 선택 (요청을 발생시킨 CPU 우선) */
int cpu = cyclic_submit_cpu;
if (llist_add(&rq->ipi_list, &per_cpu(blk_cpu_done, cpu)))
raise_softirq_irqoff(BLOCK_SOFTIRQ);
}
IRQ_POLL_SOFTIRQ는 NVMe 등 고성능 블록 디바이스를 위한 NAPI 스타일 폴링 메커니즘입니다. 기존 인터럽트 방식 대신 폴링으로 I/O 완료를 감지하여 인터럽트 오버헤드를 제거합니다.
| 구분 | BLOCK_SOFTIRQ | IRQ_POLL_SOFTIRQ |
|---|---|---|
| 동작 방식 | 인터럽트 기반 완료 통지 | 폴링 기반 완료 감지 (NAPI 스타일) |
| 지연시간 | 인터럽트 오버헤드 포함 | 인터럽트 없이 즉시 감지 가능 |
| CPU 비용 | 낮음 (이벤트 기반) | 높음 (폴링 루프) |
| 주요 사용처 | SATA, 일반 NVMe | 고성능 NVMe, SPDK |
| 설정 | 기본 활성 | io_poll=1 마운트 옵션 또는 io_uring IOPOLL |
SCHED_SOFTIRQ: CPU 간 로드 밸런싱
SCHED_SOFTIRQ는 CFS(Completely Fair Scheduler) 또는 EEVDF 스케줄러의 CPU 간 로드 밸런싱을 트리거합니다. 스케줄링 도메인(Scheduling Domain) 계층을 순회하며 부하 불균형을 감지하고 태스크를 마이그레이션합니다.
/* kernel/sched/fair.c - SCHED_SOFTIRQ 핸들러 */
static void run_rebalance_domains(struct softirq_action *h)
{
struct rq *this_rq = this_rq();
enum cpu_idle_type idle;
/* 현재 CPU가 idle인지 판단 */
idle = this_rq->idle_balance ? CPU_IDLE : CPU_NOT_IDLE;
/* nohz 밸런싱 트리거 (유휴 CPU 대신 밸런싱) */
nohz_idle_balance(this_rq, idle);
/* 스케줄링 도메인 계층 순회하며 밸런싱 */
rebalance_domains(this_rq, idle);
}
static void rebalance_domains(struct rq *rq,
enum cpu_idle_type idle)
{
struct sched_domain *sd;
int continue_balancing = 1;
/* 아래에서 위로: SMT → MC → DIE → NUMA */
for_each_domain(cpu_of(rq), sd) {
if (time_after_eq(jiffies, sd->next_balance)) {
load_balance(cpu_of(rq), rq, sd, idle,
&continue_balancing);
sd->next_balance = jiffies + sd->balance_interval;
}
}
}
SCHED_SOFTIRQ 과다 발생 시: /proc/softirqs에서 SCHED 카운트가 비정상적으로 높다면, NUMA 토폴로지(Topology)가 비대칭이거나 CPU 간 부하 불균형이 심한 상태입니다. /proc/schedstat에서 도메인별 밸런싱 횟수와 태스크 마이그레이션(Migration) 횟수를 확인하세요. isolcpus=domain으로 특정 CPU를 밸런싱에서 제외하면 해당 CPU의 SCHED_SOFTIRQ가 사라집니다.
RCU_SOFTIRQ: Grace Period와 콜백 처리
RCU_SOFTIRQ는 RCU(Read-Copy-Update) 서브시스템의 핵심 경로입니다. Grace period 진행 상태를 확인하고, 완료된 grace period의 콜백을 배치로 실행합니다.
/* kernel/rcu/tree.c - RCU softirq 핸들러 */
static void rcu_core_si(struct softirq_action *h)
{
rcu_core();
}
static void rcu_core(void)
{
struct rcu_data *rdp = this_cpu_ptr(&rcu_data);
struct rcu_node *rnp = rdp->mynode;
/* 1. 정지 상태(Quiescent State) 보고 */
if (rdp->core_needs_qs) {
rcu_report_qs_rdp(rdp);
}
/* 2. 완료된 grace period의 콜백 진행 */
if (rcu_segcblist_ready_cbs(&rdp->cblist)) {
rcu_do_batch(rdp);
}
/* 3. 새 grace period 시작 필요 시 처리 */
rcu_check_gp_start_stall(rnp, rdp, rcu_jiffies_till_stall_check());
}
/* rcu_do_batch(): 콜백 배치 실행 */
static void rcu_do_batch(struct rcu_data *rdp)
{
long bl = rdp->blimit; /* 한 번에 처리할 콜백 수 제한 */
while (rcu_segcblist_ready_cbs(&rdp->cblist) && bl--) {
struct rcu_head *rhp;
rhp = rcu_segcblist_dequeue(&rdp->cblist);
/* RCU 콜백 실행 (kfree, 구조체 해제 등) */
debug_rcu_head_callback(rhp);
rhp->func(rhp);
}
}
Per-CPU rcu_data 구조체는 세그먼트 콜백 리스트(segmented callback list)를 관리합니다. 콜백은 4단계로 분류됩니다:
| 세그먼트 | 상태 | 설명 |
|---|---|---|
RCU_DONE_TAIL | 실행 가능 | grace period 완료, 즉시 실행 대기 |
RCU_WAIT_TAIL | 대기 중 | 현재 grace period 완료를 기다림 |
RCU_NEXT_READY_TAIL | 다음 대기 | 다음 grace period에 처리될 예정 |
RCU_NEXT_TAIL | 새로 등록 | 방금 call_rcu()로 등록됨 |
RCU_SOFTIRQ 과다 시 대응
/proc/softirqs에서 RCU 카운트가 비정상적으로 높거나, dmesg에 "rcu_preempt detected stalls" 메시지가 나타나면:
- 원인: 장시간 softirq/hardirq 실행이 quiescent state 보고를 지연 → grace period 연장 → 콜백 적체
- 확인:
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata - 대응:
rcu_nocbs=N-M커널 파라미터로 지정 CPU의 RCU 콜백을 전용rcuog/Nkthread로 오프로드. 해당 CPU에서 RCU_SOFTIRQ가 사라집니다.
Softirq
softirq는 커널에 정적으로 컴파일되는 Bottom Half 메커니즘으로, 10개의 고정 타입이 존재합니다. 네트워킹, 블록 I/O, 타이머 등 성능이 극도로 중요한 서브시스템에서 사용됩니다. 새로운 softirq를 추가하는 것은 커널 서브시스템 수준의 결정이며, 드라이버에서는 사용하지 않습니다.
open_softirq() 등록 API
softirq 핸들러는 커널 초기화 시 open_softirq()로 등록됩니다. 각 softirq 타입에 대해 하나의 핸들러만 존재하며, 런타임에 변경할 수 없습니다:
/* kernel/softirq.c */
static struct softirq_action softirq_vec[NR_SOFTIRQS];
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
/* 각 서브시스템의 초기화 코드에서 등록 */
open_softirq(NET_TX_SOFTIRQ, net_tx_action); /* net/core/dev.c */
open_softirq(NET_RX_SOFTIRQ, net_rx_action); /* net/core/dev.c */
open_softirq(TASKLET_SOFTIRQ, tasklet_action); /* kernel/softirq.c */
open_softirq(HI_SOFTIRQ, tasklet_hi_action); /* kernel/softirq.c */
open_softirq(TIMER_SOFTIRQ, run_timer_softirq); /* kernel/time/timer.c */
open_softirq(SCHED_SOFTIRQ, run_rebalance_domains); /* kernel/sched/fair.c */
open_softirq(RCU_SOFTIRQ, rcu_core_si); /* kernel/rcu/tree.c */
raise_softirq() vs raise_softirq_irqoff()
softirq를 트리거하려면 pending 비트를 설정해야 합니다. 두 가지 API가 존재합니다:
/* 일반적인 사용: 인터럽트 상태를 자동으로 저장/복원 */
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags); /* IRQ 비활성화 + 플래그 저장 */
raise_softirq_irqoff(nr);
local_irq_restore(flags); /* 이전 IRQ 상태 복원 */
}
/* 이미 IRQ가 비활성화된 상태에서 사용 (top half 내부 등) */
void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr); /* or_softirq_pending(1 << nr) */
/* 인터럽트 컨텍스트가 아니면 ksoftirqd를 깨움 */
if (!in_interrupt())
wakeup_softirqd();
}
/* 사용 예: hard IRQ 핸들러 내부 (이미 IRQ 비활성 상태) */
static irqreturn_t my_handler(int irq, void *dev)
{
/* ... 최소 작업 ... */
raise_softirq_irqoff(NET_RX_SOFTIRQ); /* 효율적 */
return IRQ_HANDLED;
}
__do_softirq() 내부 구현
__do_softirq()는 softirq의 핵심 실행 루프입니다. pending 비트맵을 순회하며 등록된 핸들러를 호출하되, starvation 방지를 위한 제한이 존재합니다:
/* kernel/softirq.c - 핵심 실행 루프 (간략화) */
#define MAX_SOFTIRQ_TIME msecs_to_jiffies(2) /* 최대 2ms */
#define MAX_SOFTIRQ_RESTART 10 /* 최대 10회 재시작 */
asmlinkage __visible void __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
__u32 pending;
pending = local_softirq_pending(); /* Per-CPU pending 비트 읽기 */
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
restart:
set_softirq_pending(0); /* pending 클리어 */
local_irq_enable(); /* IRQ 재활성화 */
h = softirq_vec;
while (pending) {
if (pending & 1) {
h->action(h); /* softirq 핸들러 호출 */
}
h++;
pending >>= 1;
}
local_irq_disable();
pending = local_softirq_pending(); /* 새로 발생한 softirq 확인 */
/* 재시작 조건: pending 있고, 횟수/시간 제한 이내 */
if (pending && --max_restart &&
time_before(jiffies, end))
goto restart;
/* 제한 초과: ksoftirqd에 위임 */
if (pending)
wakeup_softirqd();
__local_bh_enable(SOFTIRQ_OFFSET);
}
starvation 방지 메커니즘: softirq 처리가 2ms를 초과하거나 10번 재시작하면 ksoftirqd로 위임됩니다. 이는 softirq가 일반 프로세스를 기아(starvation) 상태로 만드는 것을 방지합니다. 네트워크 부하가 높을 때 ksoftirqd의 CPU 사용량이 증가하는 이유입니다.
ksoftirqd 생명주기
ksoftirqd는 Per-CPU 커널 스레드로, softirq 처리의 폴백 경로를 담당합니다:
/* kernel/softirq.c - ksoftirqd 메인 루프 (간략화) */
static int ksoftirqd_should_run(unsigned int cpu)
{
return local_softirq_pending();
}
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable();
if (local_softirq_pending()) {
__do_softirq();
local_irq_enable();
cond_resched(); /* 다른 태스크에 CPU 양보 */
return;
}
local_irq_enable();
}
/*
* ksoftirqd 특성:
* - Per-CPU: ksoftirqd/0, ksoftirqd/1, ...
* - 스케줄링 정책: SCHED_NORMAL (nice 0)
* - 깨어나는 조건:
* 1. __do_softirq()에서 시간/횟수 제한 초과
* 2. 인터럽트 비활성 상태에서 raise_softirq() 호출
* 3. local_bh_enable()에서 pending softirq 발견
* - ksoftirqd는 일반 프로세스와 동일한 우선순위로 스케줄링됨
* → 높은 부하에서 softirq 지연 발생 가능 (의도된 동작)
*/
Per-CPU 동시성 모델
softirq의 가장 중요한 특성은 같은 softirq 타입이 여러 CPU에서 동시에 실행될 수 있다는 점입니다. 이는 높은 성능을 제공하지만, 공유 데이터에 대한 동기화가 필수입니다:
/*
* Softirq 동시성 규칙:
*
* 1. 같은 softirq가 여러 CPU에서 동시 실행 가능
* → NET_RX_SOFTIRQ: CPU0과 CPU1에서 동시 실행 가능
* → Per-CPU 데이터 사용으로 락 최소화
*
* 2. 같은 CPU에서는 softirq가 중첩되지 않음
* → softirq 실행 중 동일 CPU의 다른 softirq는 대기
*
* 3. Hard IRQ만 softirq를 선점 가능
* → softirq 실행 중 동일 CPU의 프로세스는 실행 불가
*
* 4. 동기화 패턴:
* - softirq 간: spin_lock() (preemption은 이미 비활성)
* - softirq + 프로세스: spin_lock_bh() (프로세스 쪽)
* - softirq + hard IRQ: spin_lock_irq() (softirq 쪽)
*/
/* 예: 네트워크 수신 softirq의 Per-CPU 데이터 활용 */
DEFINE_PER_CPU(struct softnet_data, softnet_data);
static void net_rx_action(struct softirq_action *h)
{
/* Per-CPU 데이터 접근 → 락 불필요 */
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
struct list_head *list = &sd->poll_list;
while (!list_empty(list)) {
/* NAPI polling ... */
}
}
선점 모드별 동작
| 선점 모드 | softirq 실행 위치 | ksoftirqd 역할 | 특징 |
|---|---|---|---|
PREEMPT_NONE | irq_exit() 직후 | 제한 초과 시 폴백 | 서버 워크로드 최적화, 높은 처리량 |
PREEMPT_VOLUNTARY | irq_exit() 직후 | 제한 초과 시 폴백 | 데스크톱 기본, 약간의 응답성 향상 |
PREEMPT_FULL | irq_exit() 직후 | 제한 초과 시 폴백 | 완전 선점, 실시간성 향상 |
PREEMPT_RT | ksoftirqd에서만 | 모든 softirq 처리 | 결정적 지연시간, softirq도 선점 가능 |
PREEMPT_RT에서는 모든 softirq가 ksoftirqd 스레드에서 실행되므로, softirq도 일반 스레드처럼 선점되고 우선순위 조정이 가능합니다. 이를 통해 결정적(deterministic) 지연시간을 보장하지만, 처리량은 감소합니다.
PREEMPT_RT에서의 Softirq 처리
PREEMPT_RT(Real-Time) 커널에서는 softirq의 동작이 근본적으로 변경됩니다. 모든 softirq가 ksoftirqd 커널 스레드에서 실행되어, softirq 핸들러도 선점 가능한 일반 스레드 컨텍스트에서 동작합니다. 이를 통해 결정적(deterministic) 지연시간을 보장합니다.
/* PREEMPT_RT에서의 주요 변환 */
/* 1. spin_lock → rt_mutex로 변환 */
/* → softirq 핸들러도 sleep 가능 (priority inheritance 지원) */
/* 2. local_bh_disable() → Per-CPU 락으로 변환 */
/* → softirq 직렬화를 preempt_count 대신 실제 락으로 구현 */
/* 3. ksoftirqd 우선순위 조정 */
/* chrt -f -p 50 $(pgrep -f "ksoftirqd/0")
* → ksoftirqd를 SCHED_FIFO로 전환하여 softirq 지연 감소 */
/* 4. 개별 softirq 스레드 (일부 RT 패치) */
/* softirq 유형별로 별도 스레드를 생성하여
* NET_RX와 TIMER의 우선순위를 독립적으로 조정 가능 */
PREEMPT_RT 운영 팁
- ksoftirqd 우선순위: RT 태스크(Task)보다 낮게 설정하되, 너무 낮으면 네트워크 패킷 처리가 지연됩니다. SCHED_FIFO 우선순위 49 정도가 일반적입니다.
- cyclictest로 최악 지연시간을 측정하여 softirq 영향을 확인하세요.
- threaded IRQ:
request_threaded_irq()를 사용하면 hardirq 핸들러도 스레드화되어 완전한 선점이 가능합니다.
CPU 격리와 Softirq
지연시간에 민감한 애플리케이션(HFT, 실시간 제어, 통신 장비)은 특정 CPU를 완전히 격리하여 softirq, 스케줄러 밸런싱, RCU 콜백 등의 간섭을 제거해야 합니다. Linux는 세 가지 커널 부트 파라미터를 조합하여 이를 달성합니다.
isolcpus, nohz_full, rcu_nocbs
| 파라미터 | 기능 | 제거되는 softirq |
|---|---|---|
isolcpus=managed_irq,domain,N-M | 스케줄러 도메인 제외, managed IRQ 밸런싱 제외 | SCHED_SOFTIRQ 제거 |
nohz_full=N-M | 유휴/단일 태스크 시 타이머 틱 억제 (adaptive-ticks) | TIMER_SOFTIRQ 대폭 감소 |
rcu_nocbs=N-M | RCU 콜백을 전용 rcuog/N kthread로 오프로드 | RCU_SOFTIRQ 제거 |
| IRQ affinity 수동 설정 | /proc/irq/N/smp_affinity로 IRQ를 하우스키핑 CPU에 고정 | NET_RX/TX, BLOCK 제거 |
# 완전 격리 커널 커맨드라인 예시 (CPU 4-7 격리)
isolcpus=managed_irq,domain,4-7 nohz_full=4-7 rcu_nocbs=4-7 irqaffinity=0-3
# 격리 상태 확인
cat /sys/devices/system/cpu/nohz_full
# 출력: 4-7
cat /sys/devices/system/cpu/isolated
# 출력: 4-7
# rcuog kthread 확인 (RCU 콜백 오프로드 스레드)
ps -eo pid,psr,comm | grep rcuog
# rcuog/4, rcuog/5, rcuog/6, rcuog/7 → 하우스키핑 CPU에서 실행
# 격리 CPU에서 softirq 거의 없는지 확인
cat /proc/softirqs
# CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
# TIMER: 13691355 12345678 11234567 10123456 2 1 1 0
# SCHED: 8745632 7654321 6543210 5432109 0 0 0 0
# RCU: 5367890 4567890 3456789 2345678 0 0 0 0
# 격리 CPU에 애플리케이션 고정
taskset -c 4-7 ./latency_critical_app
CPU 격리 체크리스트
- 커널 커맨드라인:
isolcpus=managed_irq,domain,N-M nohz_full=N-M rcu_nocbs=N-M irqaffinity=0-(N-1) - IRQ 수동 확인:
for i in /proc/irq/*/smp_affinity; do echo "$i: $(cat $i)"; done - ksoftirqd 확인:
ps -eo pid,psr,comm | grep ksoftirq— 격리 CPU의 ksoftirqd가 활성이면 softirq가 여전히 발생 중 - 검증:
cyclictest --mlockall --smp --priority=80 --interval=200 --distance=0 -D 60 - 주의:
nohz_fullCPU에서 2개 이상 태스크가 실행되면 스케줄링을 위해 틱이 복원됩니다. 격리 CPU당 1개 태스크가 이상적입니다.
BPF/XDP와 Softirq 컨텍스트
XDP(eXpress Data Path)와 TC BPF(Traffic Control BPF)는 NET_RX_SOFTIRQ 경로에서 BPF 프로그램을 실행합니다. 이는 커널 네트워크 스택의 매우 초기 단계에서 패킷을 처리하여 높은 성능을 달성하지만, softirq 실행 시간에 직접 영향을 미칩니다.
| 모드 | 실행 위치 | softirq 영향 | 성능 |
|---|---|---|---|
| XDP Native | 드라이버 NAPI poll 내부 (skb 할당 전) | softirq 시간 증가 | 최고 (10M+ pps) |
| XDP Generic | netif_receive_skb_core() (skb 할당 후) | softirq 시간 증가 (Native보다 느림) | 높음 |
| XDP Offload | NIC 하드웨어 | softirq 영향 없음 | 최고 (CPU 무관) |
| TC BPF | __netif_receive_skb_core() ingress/egress | softirq 시간 증가 | 높음 |
/* XDP 실행 지점 — 드라이버 NAPI poll 내부 (igb 예시) */
/* drivers/net/ethernet/intel/igb/igb_main.c (간략화) */
static int igb_clean_rx_irq(struct igb_q_vector *q_vec,
const int budget)
{
while (likely(total_packets < budget)) {
struct xdp_buff xdp;
/* DMA 완료된 디스크립터에서 데이터 포인터 설정 */
xdp_prepare_buff(&xdp, hard_start, headroom, size, true);
/* ★ XDP BPF 프로그램 실행 (skb 할당 전!) */
u32 act = bpf_prog_run_xdp(xdp_prog, &xdp);
switch (act) {
case XDP_PASS:
break; /* 정상 경로: skb 생성 후 스택 진행 */
case XDP_DROP:
igb_rx_buffer_flip(rx_ring, rx_buffer);
continue; /* 패킷 폐기: skb 미생성, 매우 빠름 */
case XDP_TX:
igb_xdp_xmit_back(adapter, &xdp);
continue; /* 같은 NIC으로 TX: 헤어핀 모드 */
case XDP_REDIRECT:
xdp_do_redirect(netdev, &xdp, xdp_prog);
continue; /* 다른 NIC/AF_XDP 소켓으로 전달 */
}
/* XDP_PASS: 일반 경로 진행 — skb 할당 + GRO */
skb = igb_construct_skb(rx_ring, rx_buffer, &xdp, timestamp);
napi_gro_receive(&q_vector->napi, skb);
}
}
XDP의 softirq 영향 모니터링
복잡한 BPF 프로그램은 패킷당 처리 시간을 증가시켜 ksoftirqd 이관 빈도가 높아질 수 있습니다. 다음 명령으로 BPF 프로그램별 실행 시간을 확인하세요:
# BPF 프로그램별 실행 시간/횟수 확인
bpftool prog show
# 출력: run_time_ns: 1234567890 run_cnt: 50000000
# → 평균 24.7ns/packet
# softirq 시간 프로파일링
perf stat -e irq:softirq_entry,irq:softirq_exit -a sleep 5
# XDP 통계 확인
ip -s link show dev eth0 xdp
# XDP xdp0: prog_id 42 drv actions: pass 1000 drop 50000 redirect 0
/proc/softirqs 분석 방법
/proc/softirqs는 부팅 이후 각 CPU에서 처리된 softirq 횟수를 누적 표시합니다. 이 데이터를 주기적으로 캡처하여 차이를 계산하면 실시간(Real-time) softirq 부하를 파악할 수 있습니다.
# 1초 간격으로 softirq 처리량 변화 확인
watch -d -n 1 'cat /proc/softirqs'
# 특정 시간 동안의 softirq 증가량 측정
cat /proc/softirqs > /tmp/softirq_before
sleep 10
cat /proc/softirqs > /tmp/softirq_after
diff /tmp/softirq_before /tmp/softirq_after
# perf로 softirq 실행 시간 프로파일링
perf stat -e irq:softirq_entry,irq:softirq_exit -a sleep 5
# ftrace로 개별 softirq 실행 추적
echo 1 > /sys/kernel/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/tracing/events/irq/softirq_exit/enable
cat /sys/kernel/tracing/trace_pipe
Softirq 문제 진단 패턴
| 증상 | 확인 지표 | 원인 | 대응 |
|---|---|---|---|
| 특정 CPU %soft 높음 | mpstat -P ALL, /proc/softirqs | IRQ affinity 불균형 | irqbalance 또는 수동 smp_affinity 조정 |
| NET_RX 카운트 폭증 | /proc/softirqs NET_RX 행 | 네트워크 패킷 폭주 | NAPI budget 조정, RPS/RFS, 멀티큐 NIC |
| ksoftirqd CPU 점유 | top/htop에서 ksoftirqd | softirq 부하 초과 | 원인 softirq 식별 후 해당 서브시스템 튜닝 |
| TIMER 과다 | /proc/softirqs TIMER 행 | 고빈도 타이머 사용 | 타이머 병합, NO_HZ 설정 |
| RCU 지연 | /proc/softirqs RCU 행 | RCU 콜백 적체 | rcu_nocbs 설정, RCU offloading |
| tail latency 증가 | cyclictest, perf sched | softirq 선점으로 태스크 지연 | PREEMPT_RT, CPU isolation |
# CPU별 softirq 불균형 빠른 확인 스크립트
awk 'NR>1 {
name=$1
total=0
for(i=2;i<=NF;i++) total+=$i
printf "%-20s total=%10d\n", name, total
}' /proc/softirqs
# 출력 예:
# HI: total= 3
# TIMER: total= 13691355
# NET_TX: total= 386418
# NET_RX: total= 32839506 ← 가장 높은 부하
# BLOCK: total= 116046
# IRQ_POLL: total= 0
# TASKLET: total= 28934
# SCHED: total= 8745632
# RCU: total= 5367890
관련 Bottom Half 메커니즘
Softirq 위에 구현되거나 관련된 메커니즘들에 대한 상세 문서 안내입니다.
TASKLET_SOFTIRQ, HI_SOFTIRQ) 위에 구현된 동적 Bottom Half. 같은 tasklet은 절대 병렬 실행되지 않는 직렬화(Serialization)를 보장합니다.
자세한 내용은 Tasklet 페이지를 참고하세요.
NET_RX_SOFTIRQ를 활용합니다.
자세한 내용은 NAPI 페이지를 참고하세요.
모니터링
Softirq와 Hardirq 활동을 모니터링하는 방법입니다.
/proc/softirqs
cat /proc/softirqs
# CPU0 CPU1 CPU2 CPU3
# HI: 2 1 0 0
# TIMER: 3456789 3234567 3012345 2987654
# NET_TX: 123456 98765 87654 76543
# NET_RX: 9876543 8765432 7654321 6543210 ← 높음!
# BLOCK: 45678 34567 23456 12345
# RCU: 1234567 1123456 1012345 987654
mpstat으로 softirq 비율 확인
mpstat -P ALL 1
# CPU %usr %nice %sys %iowait %irq %soft %idle
# 0 5.2 0.0 10.3 1.2 0.5 25.8 57.0 ← soft 25%!
# 1 3.1 0.0 8.7 0.8 0.3 18.2 68.9
/proc/interrupts
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 45: 12345678 11234567 10123456 9012345 IR-PCI-MSI 524288-edge eth0
# 125: 123 234 345 456 IR-PCI-MSI 1-edge nvme0q1
성능 튜닝
Softirq 부하를 조정하고 최적화하는 방법입니다.
NAPI Budget 조정
# 한 번의 NAPI poll에서 처리할 최대 패킷 수
sysctl -w net.core.netdev_budget=600 # 기본값: 300
# NAPI poll이 소비할 수 있는 최대 시간 (μs)
sysctl -w net.core.netdev_budget_usecs=8000 # 기본값: 2000
IRQ Affinity 설정
# IRQ 45번을 CPU 2-3에만 할당
echo "c" > /proc/irq/45/smp_affinity # 0xc = 0b1100 = CPU 2,3
# irqbalance 비활성화 (수동 설정 시)
systemctl stop irqbalance
RPS/RFS (네트워크)
# RPS (Receive Packet Steering): 패킷 처리를 여러 CPU로 분산
echo "f" > /sys/class/net/eth0/queues/rx-0/rps_cpus # 0xf = CPU 0-3
# RFS (Receive Flow Steering): 애플리케이션 CPU로 패킷 전달
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
echo 2048 > /sys/class/net/eth0/queues/rx-0/rps_flow_cnt
높은 %soft 의 의미
mpstat에서 %soft가 지속적으로 20% 이상이면:
- 네트워크 부하: NAPI budget 증가, 멀티큐 NIC 사용, RPS/RFS 활성화
- 타이머 폭주: 불필요한 타이머 제거, hrtimer → low-res timer
- 블록 I/O: io_uring 사용, polling 모드 활성화
ksoftirqd CPU 사용률이 높다면 Softirq 처리량이 한계에 도달한 것입니다.
인터럽트 코얼레싱(Interrupt Coalescing)
인터럽트 코얼레싱은 여러 이벤트를 하나의 인터럽트로 합쳐서 hardirq 빈도를 줄이는 기법입니다. 인터럽트가 줄어들면 softirq 트리거 빈도도 함께 줄어들어 CPU 오버헤드가 감소하지만, 개별 이벤트의 지연시간은 증가합니다.
하드웨어 코얼레싱 (ethtool -C)
대부분의 NIC는 하드웨어 수준의 인터럽트 코얼레싱을 지원합니다. ethtool -C로 설정합니다.
| 파라미터 | 의미 | 기본값 (일반적) | 효과 |
|---|---|---|---|
rx-usecs | RX 인터럽트 지연 (μs) | 3~50 | 높을수록 IRQ 감소, 지연 증가 |
rx-frames | RX 프레임 수 임계값 | 0 (비활성) | 높을수록 배치 크기 증가 |
tx-usecs | TX 완료 인터럽트 지연 (μs) | 0~100 | TX 완료 배치 처리 |
tx-frames | TX 프레임 수 임계값 | 0 (비활성) | TX 완료 배치 크기 |
adaptive-rx | NIC이 RX 코얼레싱 자동 조정 | on/off | 부하에 따라 동적 조절 |
adaptive-tx | NIC이 TX 코얼레싱 자동 조정 | on/off | TX 측 동적 조절 |
# 현재 코얼레싱 설정 확인
ethtool -c eth0
# Adaptive RX: on TX: off
# rx-usecs: 3
# rx-frames: 0
# tx-usecs: 0
# tx-frames: 0
# 저지연 설정 (HFT, 실시간): 코얼레싱 최소화
ethtool -C eth0 rx-usecs 0 rx-frames 1 adaptive-rx off
# 고처리량 서버: 공격적 코얼레싱
ethtool -C eth0 rx-usecs 100 rx-frames 64 adaptive-rx off
# 균형 설정: NIC 자동 조정
ethtool -C eth0 adaptive-rx on adaptive-tx on
# 인터럽트 코얼레싱 효과 확인: IRQ 빈도 측정
watch -d -n 1 'grep eth0 /proc/interrupts'
소프트웨어 코얼레싱과 Busy Polling
하드웨어 코얼레싱 외에도 소프트웨어 수준에서 인터럽트-softirq 경로를 최적화하는 방법이 있습니다. 특히 Busy Polling은 softirq를 완전히 우회하는 극단적 최적화입니다.
Busy Polling은 애플리케이션 스레드가 직접 NIC을 폴링하여 패킷을 가져옵니다. IRQ → softirq → wakeup 체인을 건너뛰므로 지연시간이 극도로 낮아지지만, CPU를 전용으로 소비합니다.
/* 소켓 레벨 busy polling 설정 */
int val = 50; /* 폴링 대기 시간 (μs) */
setsockopt(fd, SOL_SOCKET, SO_BUSY_POLL, &val, sizeof(val));
/* SO_PREFER_BUSY_POLL: 가능하면 항상 busy poll 우선 */
int prefer = 1;
setsockopt(fd, SOL_SOCKET, SO_PREFER_BUSY_POLL, &prefer, sizeof(prefer));
# 시스템 전체 busy polling 설정
sysctl -w net.core.busy_poll=50 # poll() 대기 시간 (μs)
sysctl -w net.core.busy_read=50 # read() 대기 시간 (μs)
# NAPI prefer busy poll (커널 5.13+)
echo 1 > /sys/class/net/eth0/napi_defer_hard_irqs
echo 200000 > /sys/class/net/eth0/gro_flush_timeout # 200μs
| 접근 방식 | 지연시간 | CPU 비용 | 처리량 | 용도 |
|---|---|---|---|---|
| 코얼레싱 없음 | 가장 낮음 | 매우 높음 (IRQ/패킷) | 낮음 | 비실용적 |
| HW 코얼레싱 | 중간 | 중간 | 높음 | 범용 서버 |
| Adaptive 코얼레싱 | 가변 | 자동 조절 | 높음 | 혼합 워크로드 |
| Busy Polling | 매우 낮음 | 전용 코어 | 중간 | HFT, 저지연 네트워킹 |
| DPDK / 커널 바이패스 | 최저 | 전용 코어(들) | 최고 | 전용 네트워크 어플라이언스 |
NAPI 수신 경로
NET_RX_SOFTIRQ 부하의 대부분은 NAPI poll 루프에서 발생합니다. 패킷 한 개의 이동 경로를 정확히 보면 병목 지점을 빠르게 찾을 수 있습니다.
드라이버 IRQ부터 프로토콜 스택까지
/* net/core/dev.c 중심 경로 (간략화) */
static irqreturn_t igb_msix_ring(int irq, void *data)
{
struct igb_q_vector *q_vector = data;
/* 하드웨어 인터럽트 마스크 + NAPI 스케줄 */
if (napi_schedule_prep(&q_vector->napi)) {
igb_irq_disable(q_vector);
__napi_schedule(&q_vector->napi);
}
return IRQ_HANDLED;
}
void __napi_schedule(struct napi_struct *n)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
list_add_tail(&n->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
int budget = netdev_budget;
while (budget > 0 && !list_empty(&sd->poll_list)) {
struct napi_struct *n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
int work = n->poll(n, min(budget, n->weight));
budget -= work;
}
}
락 설계와 동기화 패턴
softirq 코드는 컨텍스트별 락 선택이 잘못되면 즉시 데드락이나 심각한 지연으로 이어집니다. 아래 표는 실무에서 가장 자주 쓰는 조합입니다.
| 경합 주체 | 권장 락 | 프로세스 측 보호 | softirq 측 보호 | 주의점 |
|---|---|---|---|---|
| 프로세스 ↔ softirq | spin_lock_bh() | spin_lock_bh() | spin_lock() | 프로세스 쪽에서 BH 차단 필수 |
| softirq ↔ hardirq | spin_lock_irqsave() | 해당 없음 | spin_lock_irqsave() | 하드 인터럽트(Hardirq) 선점 고려 |
| CPU 간 softirq | spin_lock() 또는 Per-CPU | 해당 없음 | spin_lock() | 가능하면 Per-CPU로 락 제거 |
| 프로세스 전용 경로 | mutex | mutex_lock() | 사용 금지 | softirq/hardirq에서 절대 사용 금지 |
/* 잘못된 예: softirq가 접근하는 큐에 mutex 사용 */
struct my_queue {
struct list_head head;
struct mutex lock; /* softirq 경로에서 사용 불가 */
};
void bad_enqueue_from_softirq(struct my_queue *q, struct packet *p)
{
mutex_lock(&q->lock); /* BUG: sleep 가능 API */
list_add_tail(&p->node, &q->head);
mutex_unlock(&q->lock);
}
/* 올바른 예: 프로세스와 softirq 공유 큐 */
struct good_queue {
struct list_head head;
spinlock_t lock;
};
void enqueue_from_process(struct good_queue *q, struct packet *p)
{
spin_lock_bh(&q->lock);
list_add_tail(&p->node, &q->head);
spin_unlock_bh(&q->lock);
}
void enqueue_from_softirq(struct good_queue *q, struct packet *p)
{
spin_lock(&q->lock);
list_add_tail(&p->node, &q->head);
spin_unlock(&q->lock);
}
지연시간 타임라인 분석
tail latency는 대부분 "인터럽트 폭주 → softirq 재시작 반복 → ksoftirqd 이관" 구간에서 악화됩니다. 이벤트 타임라인을 단위 시간으로 나눠 보면 원인 구분이 쉬워집니다.
지연시간 계측 지표
| 지표 | 수집 방법 | 해석 기준 |
|---|---|---|
| softirq 실행 횟수 | /proc/softirqs 차분 | CPU별 편차가 크면 affinity 재배치(Relocation) 필요 |
| softirq 실행 시간 | perf sched, tracepoint | NET_RX 단일 이벤트가 길면 NAPI budget 과대 가능성 |
| ksoftirqd 런큐(Runqueue) 대기 | sched:sched_wakeup/switch | RT 태스크가 과도하면 softirq 지연 발생 |
| 애플리케이션 p99/p999 | 서비스 내부 지표 | softirq 폭주 시 분산이 급격히 커짐 |
실전 트러블슈팅 플레이북
운영 중 장애 상황에서 즉시 적용할 수 있는 점검 순서입니다. 핵심은 원인 축을 빠르게 좁히는 것입니다.
- 증상 고정
mpstat -P ALL 1,top -H로 어느 CPU에서%soft와ksoftirqd/N가 치솟는지 확인합니다. - softirq 타입 식별
동일 구간의/proc/softirqs차분으로 NET_RX/TIMER/RCU 중 어떤 축이 급증하는지 확인합니다. - IRQ 소스 매핑(Mapping)
/proc/interrupts에서 급증 IRQ를 NIC 큐/스토리지 큐와 매핑합니다. - 분산 정책 교정
IRQ affinity, RPS/RFS/XPS를 재정렬해 한 CPU 집중을 먼저 해소합니다. - budget/코얼레싱 조정
NAPI budget과 인터럽트 코얼레싱 값을 동시에 조정하며 처리량 대비 지연시간을 측정합니다. - RT/격리(Isolation) 검토
tail latency 요구가 높으면 PREEMPT_RT 또는 CPU isolation을 적용합니다.
# 1) CPU별 softirq 상태 10초 캡처
for i in $(seq 1 10); do
date +%T
cat /proc/softirqs
sleep 1
done > /tmp/softirq-sample.log
# 2) 인터럽트 상위 소스 확인
cat /proc/interrupts | sort -nrk2 | head -n 20
# 3) ksoftirqd 스케줄 지연 확인
perf sched record -a -- sleep 10
perf sched latency
# 4) tracepoint 기반 softirq 실행 추적
echo 1 > /sys/kernel/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/tracing/events/irq/softirq_exit/enable
sleep 5
cat /sys/kernel/tracing/trace > /tmp/softirq-trace.txt
do_IRQ() → handle_irq_event() 호출 체인
하드웨어 인터럽트가 발생하면 CPU는 IDT(Interrupt Descriptor Table)를 통해 아키텍처별 진입점에 도달하고, 최종적으로 등록된 핸들러를 호출합니다. x86 기준 전체 호출 체인은 다음과 같습니다.
최종 단계인 __handle_irq_event_percpu()는 irq_desc에 연결된 irqaction 체인을 순회하며 각 핸들러를 호출합니다. 핸들러의 반환값(irqreturn_t)에 따라 후속 처리가 결정됩니다.
/* kernel/irq/handle.c - 간소화 */
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc)
{
irqreturn_t retval = IRQ_NONE;
struct irqaction *action;
/* irqaction 체인 순회 (공유 IRQ 지원) */
for_each_action_of_desc(desc, action) {
irqreturn_t res;
trace_irq_handler_entry(desc->irq_data.irq, action);
res = action->handler(desc->irq_data.irq, action->dev_id);
trace_irq_handler_exit(desc->irq_data.irq, action, res);
switch (res) {
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action);
/* fall through */
case IRQ_HANDLED:
retval |= res;
break;
default:
break;
}
}
return retval;
}
코드 설명
- 3행
retval을IRQ_NONE으로 초기화합니다. 어떤 핸들러도 이 인터럽트를 처리하지 않으면 이 값이 반환됩니다. - 7행
for_each_action_of_desc는desc->action연결 리스트를 순회하는 매크로입니다. 공유 IRQ(IRQF_SHARED)에서는 여러 핸들러가 체인으로 연결됩니다. - 11행핸들러 함수(
action->handler)를 IRQ 번호와 디바이스 식별자(dev_id)를 인자로 호출합니다. 이것이 드라이버가request_irq()로 등록한 실제 핸들러입니다. - 15행
IRQ_WAKE_THREAD는 threaded IRQ에서 사용됩니다. hardirq 핸들러가 빠르게 ACK만 하고, 실제 처리는 커널 스레드(action->thread_fn)에 위임합니다. - 16행
__irq_wake_thread()가action->thread커널 스레드를 깨웁니다. 이 스레드는request_threaded_irq()로 등록 시 자동 생성됩니다. - 19행
IRQ_HANDLED는 이 핸들러가 인터럽트를 성공적으로 처리했음을 의미합니다.retval에 OR 연산으로 누적됩니다. - 22행
IRQ_NONE을 반환한 핸들러는 해당 인터럽트가 자신의 디바이스에서 발생한 것이 아님을 의미합니다. 공유 IRQ에서 다음 핸들러가 시도합니다.
irqreturn_t 반환값 분석
| 반환값 | 의미 | 후속 동작 |
|---|---|---|
IRQ_NONE | 이 핸들러의 디바이스가 인터럽트를 발생시키지 않았음 | 체인의 다음 핸들러 시도. 모든 핸들러가 IRQ_NONE이면 spurious IRQ 카운터 증가 |
IRQ_HANDLED | 인터럽트를 성공적으로 처리함 | 정상 완료. /proc/interrupts 카운터 증가 |
IRQ_WAKE_THREAD | hardirq에서 최소 처리 후 스레드 처리 요청 | action->thread 커널 스레드 깨움, thread_fn() 실행 |
모든 핸들러가 IRQ_NONE을 반환하면 커널은 note_interrupt()를 통해 spurious interrupt를 추적합니다. 연속 99,900회 이상 spurious가 발생하면 해당 IRQ 라인이 비활성화(__report_bad_irq)됩니다.
Softirq Raise/Execute 사이클 상세
Softirq의 생명주기는 크게 두 단계로 나뉩니다: Raise(대기 등록)와 Execute(실행). 이 두 단계는 Per-CPU pending 비트맵을 매개로 비동기적으로 연결됩니다.
Raise 경로
raise_softirq()는 지정된 softirq 번호에 해당하는 비트를 Per-CPU __softirq_pending에 설정합니다. 이 함수는 hardirq 핸들러, tasklet, 타이머 콜백 등 다양한 컨텍스트에서 호출됩니다.
Execute 경로
Softirq 실행은 세 가지 경로에서 트리거됩니다:
irq_exit()— hardirq 처리 완료 후 pending softirq가 있으면__do_softirq()호출local_bh_enable()— BH 비활성화 해제 시 pending 확인ksoftirqd— 위 두 경로에서 예산 초과 시 커널 스레드로 위임
__do_softirq() 루프 상세
__do_softirq()는 pending 비트맵을 순회하며 각 softirq 핸들러를 실행합니다. 재시작 횟수(MAX_SOFTIRQ_RESTART = 10)와 시간 제한(MAX_SOFTIRQ_TIME = 2ms)을 두어 softirq가 CPU를 독점하는 것을 방지합니다.
/* kernel/softirq.c - __do_softirq() 핵심 루프 상세 */
#define MAX_SOFTIRQ_TIME msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART 10
asmlinkage __visible void __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART;
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
/* softirq 진입: preempt_count에 SOFTIRQ_OFFSET 추가 */
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
/* hardirq에서 왔는지 확인 (irq_exit 경로) */
in_hardirq = lockdep_softirq_start();
/* 현재 CPU의 pending 비트를 읽고 초기화 */
restart:
pending = local_softirq_pending();
set_softirq_pending(0);
local_irq_enable(); /* 인터럽트 재활성화 */
h = softirq_vec;
while (pending) {
if (pending & 1) {
unsigned int vec_nr = h - softirq_vec;
int prev_count;
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
h->action(h); /* ← softirq 핸들러 실행 */
trace_softirq_exit(vec_nr);
}
h++;
pending >>= 1;
}
local_irq_disable(); /* 인터럽트 비활성화 후 pending 재확인 */
pending = local_softirq_pending();
if (pending) {
/* 재시작 횟수 남아있고, 시간 제한 내이면 재시작 */
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
/* 예산 초과: ksoftirqd에 위임 */
wakeup_softirqd();
}
__local_bh_enable(SOFTIRQ_OFFSET);
current->flags = old_flags;
}
코드 설명
- 2-3행
MAX_SOFTIRQ_TIME은 2ms,MAX_SOFTIRQ_RESTART는 10으로 정의됩니다. 이 두 값이 softirq의 CPU 독점을 방지하는 핵심 제한입니다. - 15행
preempt_count에SOFTIRQ_OFFSET을 추가하여 softirq 컨텍스트에 진입했음을 표시합니다.in_softirq()가 true를 반환하게 됩니다. - 22-23행pending 비트를 읽은 뒤 즉시 0으로 초기화합니다. 이 사이에 새로 raise된 softirq는 다음 루프에서 처리됩니다.
- 25행softirq 핸들러 실행 중에는 인터럽트를 활성화합니다. 따라서 softirq 처리 중에도 hardirq를 받을 수 있습니다.
- 36행
h->action(h)가 실제 softirq 핸들러를 호출합니다. 예: NET_RX_SOFTIRQ →net_rx_action() - 44행핸들러 실행 완료 후 인터럽트를 다시 비활성화하고 pending 비트를 재확인합니다. 실행 중 새로운 softirq가 raise되었을 수 있기 때문입니다.
- 47-49행세 가지 조건을 모두 만족해야 재시작합니다: (1) 시간 제한 미초과, (2) 스케줄링 필요 없음, (3) 재시작 횟수 남음.
- 52행재시작 조건을 만족하지 못하면
wakeup_softirqd()로 ksoftirqd 스레드를 깨워 나머지 처리를 위임합니다.
struct irq_desc / irqaction 구조체 분석
리눅스 커널의 인터럽트 관리는 두 핵심 구조체를 중심으로 구성됩니다. struct irq_desc는 각 IRQ 번호에 대한 메타데이터를, struct irqaction은 등록된 핸들러 정보를 담습니다.
/* include/linux/irqdesc.h - 핵심 필드 발췌 */
struct irq_desc {
struct irq_data irq_data; /* IRQ 번호, HW IRQ, chip, domain */
struct irqaction *action; /* 핸들러 연결 리스트 헤드 */
irq_flow_handler_t handle_irq; /* flow handler (fasteoi, edge 등) */
unsigned int status_use_accessors; /* IRQ 상태 플래그 */
unsigned int depth; /* disable 중첩 카운터 */
unsigned int irq_count; /* spurious 감지용 인터럽트 총 횟수 */
unsigned int irqs_unhandled; /* spurious (IRQ_NONE) 횟수 */
atomic_t threads_handled; /* threaded 핸들러 완료 추적 */
raw_spinlock_t lock; /* desc 보호 락 */
unsigned int *kstat_irqs; /* Per-CPU 인터럽트 통계 */
const char *name; /* /proc/interrupts 표시 이름 */
};
/* include/linux/interrupt.h - 핵심 필드 발췌 */
struct irqaction {
irq_handler_t handler; /* hardirq 핸들러 (최상위 절반) */
irq_handler_t thread_fn; /* threaded IRQ 핸들러 */
struct task_struct *thread; /* IRQ 스레드 (자동 생성) */
struct irqaction *secondary; /* 중첩 action (forced threading) */
unsigned int irq; /* IRQ 번호 */
unsigned int flags; /* IRQF_SHARED 등 플래그 */
const char *name; /* /proc/interrupts 이름 */
void *dev_id; /* 디바이스 식별자 (공유 IRQ 구분) */
void __percpu *percpu_dev_id; /* Per-CPU 디바이스 ID */
struct irqaction *next; /* 공유 IRQ 체인의 다음 action */
};
코드 설명
- 3행
irq_data는 IRQ 번호(가상), 하드웨어 IRQ 번호, irq_chip, irq_domain 등을 포함하는 하위 구조체입니다. 인터럽트 컨트롤러 추상화의 핵심입니다. - 4행
action은 이 IRQ에 등록된 핸들러 체인의 헤드입니다.request_irq()로 등록할 때마다 새irqaction이 이 체인에 추가됩니다. - 5행
handle_irq는 flow handler로, 인터럽트 컨트롤러 종류에 따라handle_fasteoi_irq,handle_edge_irq,handle_level_irq등이 설정됩니다. - 7행
depth는disable_irq()가 중첩 호출된 횟수입니다. 0이면 활성 상태이고,enable_irq()를 같은 횟수만큼 호출해야 실제로 활성화됩니다. - 19행
handler는 hardirq 컨텍스트에서 실행되는 핸들러입니다. 인터럽트를 비활성화한 상태에서 최대한 빠르게 실행되어야 합니다. - 20행
thread_fn은request_threaded_irq()로 등록한 스레드 핸들러입니다. 프로세스 컨텍스트에서 실행되므로 sleep 가능한 작업을 수행할 수 있습니다. - 26행
dev_id는 공유 IRQ에서 핸들러가 자신의 디바이스를 식별하는 데 사용됩니다.free_irq()호출 시에도 이 값으로 어떤 핸들러를 제거할지 결정합니다. - 28행
next포인터로 같은 IRQ를 공유하는 다른 디바이스의 핸들러와 연결됩니다. PCI 디바이스에서 흔히 사용되는 공유 IRQ 패턴입니다.
ksoftirqd 스레드 상세
ksoftirqd는 Per-CPU 커널 스레드로, softirq 처리가 __do_softirq()의 재시작 예산을 초과했을 때 나머지 pending softirq를 처리합니다. 우선순위 SCHED_NORMAL(nice 0)로 실행되어 일반 프로세스와 CPU를 공유합니다.
/* kernel/softirq.c - ksoftirqd 메인 루프 */
static void run_ksoftirqd(unsigned int cpu)
{
/* pending softirq가 있는지 확인 */
ksoftirqd_run_begin();
if (local_softirq_pending()) {
/* __do_softirq() 호출 — 동일한 softirq 실행 루프 */
__do_softirq();
ksoftirqd_run_end();
cond_resched(); /* 자발적 선점 포인트 */
return;
}
ksoftirqd_run_end();
}
/* ksoftirqd를 깨우는 조건들 */
static void wakeup_softirqd(void)
{
struct task_struct *tsk = __this_cpu_read(ksoftirqd);
if (tsk)
wake_up_process(tsk);
}
코드 설명
- 2행
run_ksoftirqd()는 Per-CPU 스레드의 메인 콜백입니다. smpboot 프레임워크에 의해 CPU 당 하나씩 생성되고ksoftirqd/N으로 표시됩니다. - 5행
ksoftirqd_run_begin()은local_bh_disable()을 호출하여 softirq 재진입을 방지합니다. - 9행
__do_softirq()를 직접 호출합니다. irq_exit() 경로와 동일한 코드를 사용하지만, 프로세스 컨텍스트에서 실행되므로 스케줄링이 가능합니다. - 11행
cond_resched()는 자발적 선점 포인트입니다. softirq 처리 후 더 높은 우선순위 태스크가 있으면 양보합니다. - 20행
__this_cpu_read(ksoftirqd)로 현재 CPU의 ksoftirqd 태스크를 가져옵니다. Per-CPU 변수이므로 락 없이 접근합니다.
wakeup_softirqd() 호출 조건
ksoftirqd가 깨어나는 주요 상황은 다음과 같습니다:
| 호출 지점 | 조건 | 의미 |
|---|---|---|
__do_softirq() | 재시작 10회 초과 또는 2ms 초과 | softirq 폭주로 인한 예산 초과 |
raise_softirq_irqoff() | !in_interrupt() (hardirq 밖) | 프로세스 컨텍스트에서 softirq raise |
irq_exit() | pending이 있지만 이미 softirq 중첩 | 재진입 방지를 위한 지연 처리 |
ksoftirqd의 실행 빈도는 /proc/softirqs에 직접 반영되지 않지만, ps -eo pid,psr,ni,comm | grep ksoftirq로 CPU 바인딩 상태와 실행 빈도를 확인할 수 있습니다. ksoftirqd가 높은 CPU 점유율을 보이면 softirq 폭주(특히 NET_RX)를 의심해야 합니다.
IRQ 스레드 마이그레이션과 어피니티
IRQ 어피니티(Affinity)는 특정 인터럽트를 어느 CPU에서 처리할지 제어하는 메커니즘입니다. 네트워크 카드의 RSS(Receive Side Scaling)나 NVMe의 다중 큐 구성에서 성능 최적화에 핵심적인 역할을 합니다.
irq_set_affinity() 경로
/* kernel/irq/manage.c - IRQ 어피니티 설정 경로 */
int irq_set_affinity(unsigned int irq, const struct cpumask *mask)
{
struct irq_desc *desc = irq_to_desc(irq);
unsigned long flags;
if (!desc)
return -EINVAL;
raw_spin_lock_irqsave(&desc->lock, flags);
int ret = __irq_set_affinity_locked(
irq_desc_get_irq_data(desc), mask, false);
raw_spin_unlock_irqrestore(&desc->lock, flags);
return ret;
}
/* 핵심 내부 함수 */
static 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);
if (!chip || !chip->irq_set_affinity)
return -EINVAL;
/* 하드웨어 수준에서 어피니티 변경 */
int ret = chip->irq_set_affinity(data, mask, force);
if (ret == IRQ_SET_MASK_OK || ret == IRQ_SET_MASK_OK_DONE) {
cpumask_copy(irq_data_get_affinity_mask(data), mask);
irqd_set(data, IRQD_AFFINITY_SET);
}
return ret;
}
코드 설명
- 2행
irq_set_affinity()는 유저스페이스(/proc/irq/N/smp_affinity) 또는 커널 내부에서 호출되는 어피니티 설정 진입점입니다. - 4행
irq_to_desc()로 IRQ 번호에 대응하는irq_desc를 찾습니다. radix tree 또는 배열 기반 구현이 사용됩니다. - 10행
desc->lock을 잡아 동시 접근을 방지합니다.raw_spin_lock은 PREEMPT_RT에서도 실제 스핀락으로 동작합니다. - 25행인터럽트 컨트롤러(irq_chip)가
irq_set_affinity콜백을 제공해야 어피니티 변경이 가능합니다. APIC, GIC 등 대부분의 컨트롤러가 지원합니다. - 29행
chip->irq_set_affinity()는 실제 하드웨어 레지스터(예: APIC의 Destination Register)를 프로그래밍합니다. - 32행성공 시 소프트웨어 마스크를 갱신하고,
IRQD_AFFINITY_SET플래그를 설정하여 사용자가 명시적으로 어피니티를 지정했음을 기록합니다.
CPU 핫플러그 시 IRQ 마이그레이션
CPU가 오프라인(offline)될 때, 해당 CPU에 할당된 모든 인터럽트는 다른 온라인 CPU로 마이그레이션되어야 합니다. irq_migrate_all_off_this_cpu()가 이 역할을 수행합니다.
/* kernel/irq/cpuhotplug.c - CPU 오프라인 시 IRQ 마이그레이션 */
void irq_migrate_all_off_this_cpu(void)
{
unsigned int irq;
struct irq_desc *desc;
/* 모든 IRQ 디스크립터를 순회 */
for_each_active_irq(irq) {
desc = irq_to_desc(irq);
/* Per-CPU 인터럽트는 마이그레이션 불필요 */
if (irqd_is_per_cpu(&desc->irq_data))
continue;
raw_spin_lock(&desc->lock);
/* 현재 CPU가 어피니티 마스크에 포함된 경우만 처리 */
if (cpumask_test_cpu(smp_processor_id(),
desc->irq_data.common->affinity)) {
struct irq_data *d = &desc->irq_data;
struct irq_chip *c = d->chip;
int ret;
/* 온라인 CPU 중에서 새 대상 선택 */
cpumask_copy(&available, cpu_online_mask);
cpumask_clear_cpu(smp_processor_id(), &available);
ret = c->irq_set_affinity(d, &available, true);
if (ret)
pr_warn_ratelimited(
"IRQ%u: set affinity failed(%d)\n",
irq, ret);
}
raw_spin_unlock(&desc->lock);
}
}
코드 설명
- 2행이 함수는 CPU 핫플러그 경로의
CPUHP_TEARDOWN_CPU단계에서 호출됩니다. 오프라인되는 CPU에서 직접 실행됩니다. - 8행
for_each_active_irq()는 시스템의 모든 활성 IRQ를 순회합니다. sparse_irq가 활성화된 경우 radix tree를 탐색합니다. - 12행Per-CPU 인터럽트(예: 로컬 APIC 타이머)는 특정 CPU에 고정되므로 마이그레이션 대상에서 제외합니다.
- 26-27행현재 온라인인 CPU 마스크에서 오프라인되는 자신의 CPU를 제거하여 새로운 어피니티 마스크를 구성합니다.
- 29행
force파라미터를true로 전달하여 사용자 설정 어피니티를 무시하고 강제 마이그레이션합니다. CPU가 오프라인되므로 선택의 여지가 없습니다.
사용자 공간에서 IRQ 어피니티를 설정하려면 /proc/irq/<IRQ번호>/smp_affinity에 16진수 CPU 마스크를 기록합니다:
# IRQ 32를 CPU 0,1에만 할당
echo 3 > /proc/irq/32/smp_affinity
# 현재 어피니티 확인
cat /proc/irq/32/smp_affinity
# 출력: 00000003
# CPU 리스트 형식으로 설정 (더 직관적)
echo 0-1 > /proc/irq/32/smp_affinity_list
참고자료
커널 공식 문서
- Linux generic IRQ handling — 리눅스 범용 IRQ 처리 프레임워크 공식 문서입니다
- IRQ Domain Hierarchy — IRQ 도메인 계층 구조와 하드웨어 인터럽트 번호 매핑을 설명합니다
- RCU Requirements — RCU_SOFTIRQ의 설계 요구사항과 softirq 컨텍스트에서의 동작을 다룹니다
- NAPI — NET_RX_SOFTIRQ 기반 NAPI 폴링 메커니즘 공식 문서입니다
- Lock types and their rules — hardirq/softirq/process 컨텍스트별 락 사용 규칙을 정리합니다
- RT-mutex subsystem with PI support — PREEMPT_RT에서 softirq 스레드화와 관련된 RT-mutex를 설명합니다
커널 소스 코드
kernel/softirq.c— softirq 코어 구현 (__do_softirq,raise_softirq,open_softirq, ksoftirqd)include/linux/interrupt.h— softirq 열거형,in_interrupt(),in_softirq()매크로 정의입니다include/linux/hardirq.h— hardirq 카운터,irq_enter()/irq_exit()매크로를 정의합니다include/linux/preempt.h— preempt_count 비트 필드 레이아웃 (HARDIRQ_OFFSET, SOFTIRQ_OFFSET)을 정의합니다kernel/irq/handle.c—handle_irq_event()와 IRQ 핸들러 실행 경로입니다kernel/irq/chip.c— IRQ 칩 드라이버 (level/edge/fasteoi 핸들러)입니다kernel/irq/manage.c—request_irq(), threaded IRQ, IRQ 어피니티 관리입니다net/core/dev.c—net_rx_action(),__napi_schedule()등 NET_RX_SOFTIRQ 핸들러입니다block/blk-mq.c— BLOCK_SOFTIRQ를 사용하는 블록 I/O 완료 경로입니다kernel/rcu/tree.c— RCU_SOFTIRQ를 발생시키는 Tree RCU 구현입니다
LWN.net 기사
- Software interrupts and realtime (2012) — softirq의 역사와 PREEMPT_RT에서의 스레드화 문제를 심층 분석합니다
- Toward a better softirq model (2019) — softirq 모델 개선 논의와 ksoftirqd 제한 시간 문제를 다룹니다
- Softirqs and per-CPU variables (2020) — softirq에서 per-CPU 변수 사용 시 발생하는 미묘한 경합 조건을 분석합니다
- Closing the softirq debate (2023) — softirq 처리 횟수 제한과 ksoftirqd 전환 정책의 공정성 논쟁을 정리합니다
- Reworking NAPI (2016) — NET_RX_SOFTIRQ 기반 NAPI의 busy-polling 개선안을 설명합니다
- Generic interrupt flow handling — IRQ 흐름 핸들러(level, edge, fasteoi)의 커널 내부 동작을 해설합니다
- Moving interrupts to threads (2008) — Thomas Gleixner의 threaded IRQ 도입 배경과 hardirq 최소화 전략을 소개합니다
서적
- Understanding the Linux Kernel, 3rd Edition (Daniel P. Bovet, Marco Cesati) — Chapter 4 "Interrupts and Exceptions"에서 hardirq 처리를, Chapter 4.7 "Softirqs and Tasklets"에서 softirq 메커니즘을 상세히 설명합니다
- Linux Kernel Development, 3rd Edition (Robert Love) — Chapter 7 "Interrupts and Interrupt Handlers"와 Chapter 8 "Bottom Halves and Deferring Work"가 softirq/hardirq 경계를 명확히 구분합니다
- Linux Device Drivers, 3rd Edition (Jonathan Corbet, Alessandro Rubini, Greg Kroah-Hartman) — Chapter 10 "Interrupt Handling"에서 인터럽트 컨텍스트의 제약과 bottom-half 설계를 다룹니다
- Professional Linux Kernel Architecture (Wolfgang Mauerer) — Chapter 14 "Kernel Activities"에서 softirq 벡터별 동작과 ksoftirqd 스케줄링 정책을 분석합니다
외부 자료
- Monitoring and Tuning the Linux Networking Stack: Receiving Data — NET_RX_SOFTIRQ 경로를 패킷 수신 관점에서 커널 소스 레벨까지 추적합니다
- Linux Insides: Interrupts — x86 인터럽트 초기화부터 softirq 처리까지 단계별로 해설합니다
- Approaching Real-Time Linux (OLS 2006, Clark Williams) — PREEMPT_RT 패치에서 softirq 스레드화가 실시간 지연시간에 미치는 영향을 측정합니다