Softirq & Hardirq
Linux 커널의 하드웨어 인터럽트(Hardirq)와 소프트웨어 인터럽트(Softirq)를 심층 분석합니다. 인터럽트 컨텍스트, Softirq 메커니즘, ksoftirqd, Tasklet, NAPI, 성능 최적화까지 종합적으로 다룹니다.
특히 "어떤 작업을 Hardirq에서 끝내고 어떤 작업을 Softirq로 넘겨야 하는가"를 판단할 수 있도록 실행 컨텍스트 제약, 지연 시간 예산, per-CPU 병렬성, ksoftirqd/N로의 인계 조건을 구체적으로 설명합니다. 네트워크 경로의 NET_RX_SOFTIRQ 적체, CPU 사용률 급등, tail latency 증가 같은 현장을 기준으로 모니터링 지표와 튜닝 순서를 함께 제시하여 성능 문제를 재현 가능하게 분석할 수 있도록 했습니다.
핵심 요약
- 핵심 객체 — 이 문서의 중심이 되는 자료구조/API를 먼저 파악합니다.
- 실행 경로 — 요청이 들어와 처리되고 종료되는 흐름을 확인합니다.
- 병목 지점 — 지연이나 처리량 저하가 발생하는 구간을 점검합니다.
- 동기화 지점 — 경합과 경쟁 조건이 생길 수 있는 구간을 구분합니다.
- 운영 포인트 — 관측 지표와 튜닝 항목을 함께 확인합니다.
단계별 이해
- 구성요소 확인
핵심 자료구조와 주요 API를 먼저 식별합니다. - 요청 흐름 추적
입력부터 완료까지의 호출 경로를 순서대로 따라갑니다. - 예외 경로 점검
실패 처리, 재시도, 타임아웃 등 경계 조건을 확인합니다. - 성능/안정성 점검
잠금 경합, 큐 적체, 병목 지점을 측정하고 조정합니다.
개요
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가 추가되어 프로세스 컨텍스트 Bottom Half를 완성했습니다. Linux 4.x 이후에는 PREEMPT_RT 패치셋의 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(), 스레드 핸들러 |
| 5.x+ | PREEMPT_RT mainline | softirq 완전 스레드화, 결정적 지연시간 |
Hardirq vs Softirq
| 구분 | Hardirq (Top Half) | Softirq (Bottom Half) |
|---|---|---|
| 트리거 | 하드웨어 인터럽트 | 소프트웨어 (raise_softirq) |
| 실행 시점 | 즉시 (선점 가능) | Hardirq 종료 후, idle 진입 시, ksoftirqd |
| 인터럽트 차단 | 로컬 CPU 인터럽트 비활성화 | 인터럽트 활성화 상태 |
| 스케줄링 | 불가 (sleep 금지) | 불가 (프로세스 컨텍스트 아님) |
| 실행 시간 | 매우 짧음 (수 μs) | 상대적으로 김 (수십 μs ~ ms) |
| 재진입 | 불가 (같은 IRQ) | 가능 (다른 CPU에서 동시 실행) |
| 대표 작업 | 하드웨어 ACK, 데이터 읽기 | 패킷 처리, 타이머 콜백, 블록 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()불가- 긴 루프나 복잡한 연산 금지
위반 시 커널 패닉이나 데드락 발생 가능!
상세 문서: IRQ 핸들러 등록(request_irq), genirq 프레임워크, GIC/APIC 인터럽트 컨트롤러, IRQ 도메인 계층 등의 상세 내용은 인터럽트 심화 페이지를 참조하세요.
Softirq 타입
Linux는 정적으로 10가지 Softirq 타입을 정의합니다.
Softirq 우선순위 순서
| 번호 | 이름 | 용도 | 주요 사용처 |
|---|---|---|---|
| 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 폴링 | 고성능 블록 디바이스 (NVMe) |
| 6 | TASKLET_SOFTIRQ | 일반 Tasklet | 드라이버 Tasklet (일반) |
| 7 | SCHED_SOFTIRQ | 스케줄러 | 로드 밸런싱, CFS |
| 8 | HRTIMER_SOFTIRQ | 고해상도 타이머 | 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 구조체를 통해 등록된 핸들러 체인을 순회합니다.
/* 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 커널 스레드
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 공정성: Softirq가 CPU를 독점하지 못하게 함
- 우선순위: SCHED_NORMAL (nice 0) - 일반 프로세스와 동등
- 스케줄링: CFS에 의해 스케줄링되어 다른 프로세스에게도 CPU 할당
ksoftirqd CPU 사용률이 높다면 Softirq 부하가 과도하다는 신호입니다 (특히 NET_RX/TX).
ksoftirqd 활성화 조건
ksoftirqd가 깨어나는 경우는 세 가지입니다. 각 경로는 wakeup_softirqd()를 호출하여 해당 CPU의 ksoftirqd 스레드를 TASK_RUNNING으로 전환합니다.
인터럽트 컨텍스트 확인
현재 코드가 어떤 컨텍스트에서 실행 중인지 확인하는 매크로입니다.
컨텍스트 확인 매크로
| 매크로 | 의미 | 포함 범위 |
|---|---|---|
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 심화
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 태스크보다 낮게 설정하되, 너무 낮으면 네트워크 패킷 처리가 지연됩니다. SCHED_FIFO 우선순위 49 정도가 일반적입니다.
- cyclictest로 최악 지연시간을 측정하여 softirq 영향을 확인하세요.
- threaded IRQ:
request_threaded_irq()를 사용하면 hardirq 핸들러도 스레드화되어 완전한 선점이 가능합니다.
/proc/softirqs 분석 방법
/proc/softirqs는 부팅 이후 각 CPU에서 처리된 softirq 횟수를 누적 표시합니다. 이 데이터를 주기적으로 캡처하여 차이를 계산하면 실시간 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은 절대 병렬 실행되지 않는 직렬화를 보장합니다.
자세한 내용은 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 처리량이 한계에 도달한 것입니다.
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() | 하드 인터럽트 선점 고려 |
| 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 재배치 필요 |
| softirq 실행 시간 | perf sched, tracepoint | NET_RX 단일 이벤트가 길면 NAPI budget 과대 가능성 |
| ksoftirqd 런큐 대기 | 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 소스 매핑
/proc/interrupts에서 급증 IRQ를 NIC 큐/스토리지 큐와 매핑합니다. - 분산 정책 교정
IRQ affinity, RPS/RFS/XPS를 재정렬해 한 CPU 집중을 먼저 해소합니다. - budget/코얼레싱 조정
NAPI budget과 인터럽트 코얼레싱 값을 동시에 조정하며 처리량 대비 지연시간을 측정합니다. - RT/격리 검토
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