Bottom Half 선택 가이드와 실전 패턴
Bottom Half 메커니즘(softirq, tasklet, workqueue, threaded IRQ) 중 올바른 선택 기준, 실전 패턴, 성능 최적화, 디버깅(Debugging) 가이드를 제공합니다.
이 페이지(Page)는 "슬립(Sleep) 필요 여부, 처리량(Throughput), 순서 보장(Ordering), 격리(Isolation) 수준, PREEMPT_RT 호환성" 기준으로 메커니즘을 고르는 실전 의사결정 표준을 제시합니다. 각 메커니즘의 내용은 아래 전용 페이지를 참고하세요.
이 페이지는 인터럽트(Interrupt) 페이지의 Bottom Half 기초 내용을 바탕으로, 메커니즘 선택과 실전 패턴에 집중합니다. 각 메커니즘의 내부 구현는 전용 페이지를 참고하세요.
핵심 요약
- softirq — 커널에 정적 등록되는 고성능 BH. 네트워킹, 블록 I/O 등에서 사용. per-CPU로 병렬 실행됩니다.
- tasklet — softirq 위에 구현된 간편 메커니즘. 같은 tasklet은 동시에 하나의 CPU에서만 실행됩니다.
- workqueue — 프로세스(Process) 컨텍스트에서 실행. 슬립 가능하여 I/O, 잠금(Lock) 획득 등이 가능합니다.
- ksoftirqd — softirq 부하가 높을 때 처리를 인계받는 per-CPU 커널 스레드(Kernel Thread)입니다.
단계별 이해
- BH가 필요한 이유 — Top Half에서 오래 걸리는 작업을 하면 다른 인터럽트가 차단됩니다.
BH로 지연하면 인터럽트를 다시 활성화하고 나중에 안전하게 처리할 수 있습니다.
- softirq 이해 — 10개 고정 타입(NET_TX, NET_RX, BLOCK, TIMER 등). 새로 추가하려면 커널 소스를 수정해야 합니다.
같은 softirq가 여러 CPU에서 동시에 실행될 수 있어 per-CPU 데이터를 사용합니다.
- tasklet 이해 — 드라이버에서 가장 쉽게 사용하는 BH.
tasklet_schedule()로 예약합니다.같은 tasklet 인스턴스는 직렬화(Serialization)되어 경쟁 조건(Race Condition) 걱정이 줄어듭니다.
- 선택 기준 — 슬립이 필요하면 workqueue, 고성능이 필요하면 softirq, 간단한 지연 처리는 tasklet을 사용합니다.
최근에는 tasklet 대신 threaded IRQ나 workqueue를 권장하는 추세입니다.
메커니즘별 기술 문서
각 Bottom Half 메커니즘의 내부 구현, API 상세, 디버깅 기법은 전용 페이지에서 다룹니다.
Softirq & Hardirq 페이지로 이동 →
Tasklet 페이지로 이동 →
Workqueue (CMWQ) 페이지로 이동 →
아래부터는 메커니즘 선택 기준과 공통 패턴을 다룹니다.
Bottom Half 선택 가이드
결정 매트릭스
| 기준 | Softirq | Tasklet | Workqueue | Threaded IRQ |
|---|---|---|---|---|
| 실행 컨텍스트 | 인터럽트 | 인터럽트 | 프로세스 | 프로세스 |
| 슬립 가능 | 불가 | 불가 | 가능 | 가능 |
| 동시성 | 같은 타입 병렬 | 같은 인스턴스 직렬 | max_active 제어 | Per-IRQ 스레드(Thread) |
| 지연시간 | 최소 | 낮음 | 중간 | 낮음~중간 |
| 동적 생성 | 불가 (정적) | 가능 | 가능 | 가능 |
| PREEMPT_RT | ksoftirqd로 이동 | 비호환 | 정상 동작 | 정상 동작 |
| 우선순위(Priority) 제어 | 불가 | 불가 | nice 값 | RT 우선순위 가능 |
| 사용 권장 | 커널 내부만 | deprecated | 기본 선택 | IRQ Bottom Half용 |
| 메모리 할당 | GFP_ATOMIC만 | GFP_ATOMIC만 | GFP_KERNEL 가능 | GFP_KERNEL 가능 |
| mutex | 불가 | 불가 | 가능 | 가능 |
결정 흐름도
고급 선택 결정 트리
위 흐름도보다 더 세부적인 결정 기준을 포함한 고급 결정 트리입니다. 지연 허용 범위, 순서 보장, 메모리 할당 모드, CPU 바인딩 여부까지 고려합니다.
PREEMPT_RT 영향
- PREEMPT_RT에서의 Bottom Half 변화:
- Softirq:
- 모든 softirq가 ksoftirqd에서 실행 (선점 가능)
- irq_exit()에서 직접 실행하지 않음
- local_bh_disable()이 preempt_disable()로 변경되지 않음
- → RT 뮤텍스(Mutex) 기반으로 변경
- Tasklet:
- PREEMPT_RT에서 문제 유발
- 인터럽트 컨텍스트 가정 코드가 호환되지 않음
- 커널 커뮤니티에서 제거 진행 중
- Workqueue:
- 정상 동작 (이미 프로세스 컨텍스트)
- RT 우선순위 설정 가능
- Threaded IRQ:
- 정상 동작 (이미 스레드 기반)
- SCHED_FIFO 우선순위로 실행
- chrt 명령으로 IRQ 스레드 우선순위 조정 가능
- Spinlock:
- spin_lock()이 rt_mutex로 변경 (슬립 가능!)
- raw_spin_lock()만 진짜 스핀 (사용 최소화)
- spin_lock_irqsave() → sleeping lock + local_irq_save
성능 특성 비교
| 특성 | Softirq | Workqueue (bound) | Workqueue (unbound) | Threaded IRQ |
|---|---|---|---|---|
| 호출 오버헤드(Overhead) | ~100ns | ~1-5us | ~1-10us | ~1-5us |
| 스케줄링 지연 | 거의 없음 | 컨텍스트 스위치 | 컨텍스트 스위치 + 마이그레이션 | 컨텍스트 스위치 |
| SMP 확장성 | 뛰어남 (Per-CPU) | 좋음 (Per-CPU) | 좋음 (NUMA-aware) | 보통 (Per-IRQ) |
| 캐시(Cache) 친화성 | 높음 | 높음 (같은 CPU) | 보통 | 보통 |
| 우선순위 역전(Priority Inversion) | 가능 (RT 제외) | PI 없음 | PI 없음 | PI 지원 |
실행 컨텍스트 비교 다이어그램
각 Bottom Half 메커니즘이 실행되는 컨텍스트를 시각적으로 비교합니다. 인터럽트 컨텍스트(softirq/tasklet)와 프로세스 컨텍스트(workqueue/threaded IRQ)의 차이를 이해하는 것이 올바른 선택의 핵심입니다.
지연시간/처리량 시각화
각 메커니즘의 호출 오버헤드와 처리 지연시간을 시각적 막대 그래프로 비교합니다. 실제 측정값은 하드웨어와 커널 설정에 따라 다르지만, 상대적인 크기를 이해하는 데 유용합니다.
__do_softirq() 내부 실행 흐름
__do_softirq()는 리눅스 커널에서 모든 softirq를 실제로 디스패치(Dispatch)하는 핵심 함수입니다. 인터럽트 반환 시점(irq_exit())이나 local_bh_enable() 호출 시점에 pending softirq가 있으면 이 함수가 호출됩니다. 내부 동작을 정확히 이해하면 softirq 기반 BH의 지연 특성과 ksoftirqd 위임 조건을 파악할 수 있습니다.
실행 흐름 다이어그램
커널 소스 레벨 분석
/* kernel/softirq.c - __do_softirq() 핵심 로직 (간소화) */
asmlinkage __visible void __do_softirq(void)
{
unsigned long end = jiffies + MAX_SOFTIRQ_TIME; /* 2ms */
unsigned long old_flags = current->flags;
int max_restart = MAX_SOFTIRQ_RESTART; /* 10 */
struct softirq_action *h;
bool in_hardirq;
__u32 pending;
int softirq_bit;
/*
* 현재 태스크에 PF_MEMALLOC 설정
* softirq 핸들러에서 메모리 할당 시 emergency pool 사용 가능
*/
current->flags &= ~PF_MEMALLOC;
/* pending 비트마스크 읽기 + 초기화 */
pending = local_softirq_pending();
softirq_handle_begin(); /* __local_bh_disable(SOFTIRQ_OFFSET) */
in_hardirq = lockdep_softirq_start();
account_softirq_enter(current);
restart:
/* pending 비트마스크 초기화 (새 softirq는 다음 라운드에서 처리) */
set_softirq_pending(0);
local_irq_enable(); /* ★ 인터럽트 재활성화 */
h = softirq_vec; /* softirq 핸들러 배열 시작 */
while ((softirq_bit = ffs(pending))) {
unsigned vec_nr = softirq_bit - 1;
h += vec_nr;
/* 실제 softirq 핸들러 호출 */
trace_softirq_entry(vec_nr);
h->action(h); /* ★ 핵심: 등록된 핸들러 실행 */
trace_softirq_exit(vec_nr);
h++;
pending >>= softirq_bit;
}
local_irq_disable(); /* 인터럽트 비활성화 후 재확인 */
pending = local_softirq_pending();
if (pending) {
/* 재시작 조건: 횟수 남음 AND 시간 남음 AND softirqd 아님 */
if (time_before(jiffies, end) &&
!need_resched() &&
--max_restart)
goto restart;
/* 조건 미충족: ksoftirqd로 위임 */
wakeup_softirqd();
}
account_softirq_exit(current);
lockdep_softirq_end(in_hardirq);
softirq_handle_end(); /* __local_bh_enable(SOFTIRQ_OFFSET) */
current->flags = old_flags;
}
호출 경로 분석
__do_softirq()는 세 가지 경로에서 호출됩니다.
| 호출 경로 | 트리거 조건 | 실행 컨텍스트 | 빈도 |
|---|---|---|---|
irq_exit() → __irq_exit_rcu() | hardirq 반환 시 pending softirq 존재 | 인터럽트 반환 직후 | 가장 흔함 |
local_bh_enable() | BH 재활성화 시 pending softirq 존재 | 프로세스 컨텍스트 | 빈번함 |
ksoftirqd 스레드 | wakeup_softirqd()에 의해 깨움 | 커널 스레드 (프로세스 컨텍스트) | 부하 높을 때 |
ksoftirqd 위임 조건과 영향
ksoftirqd 위임은 시스템 성능에 중요한 영향을 미칩니다. softirq가 직접 실행되면 인터럽트 반환 시점에 즉시 처리되지만, ksoftirqd로 위임되면 스케줄러를 거쳐야 하므로 지연이 증가합니다.
- 시간 초과: softirq 처리 시작 후 2ms(
MAX_SOFTIRQ_TIME) 경과 - 재시작 횟수 초과: 10번(
MAX_SOFTIRQ_RESTART) 재시작 완료 - 선점 요청:
need_resched()가 true (더 높은 우선순위 태스크 대기)
영향: ksoftirqd는 SCHED_OTHER (nice 0)로 실행되므로, RT 태스크보다 낮은 우선순위입니다. 네트워크 트래픽 폭주 시 패킷 처리 지연이 급증할 수 있습니다.
/* ksoftirqd 커널 스레드 메인 루프 (간소화) */
static int ksoftirqd_should_run(unsigned int cpu)
{
return local_softirq_pending();
}
static void run_ksoftirqd(unsigned int cpu)
{
ksoftirqd_run_begin();
if (local_softirq_pending()) {
__do_softirq(); /* 동일한 함수 호출 */
ksoftirqd_run_end();
cond_resched(); /* 다른 태스크에게 CPU 양보 기회 */
return;
}
ksoftirqd_run_end();
}
/* ksoftirqd 과부하 진단 */
/* /proc/net/softnet_stat 3번째 열(time_squeeze)이 증가하면
* NET_RX softirq가 시간/budget 초과로 ksoftirqd에 위임되고 있음 */
softirq_vec 타입별 핸들러 매핑
| 인덱스 | softirq 타입 | 핸들러 함수 | 등록 위치 | 용도 |
|---|---|---|---|---|
| 0 | HI_SOFTIRQ | tasklet_hi_action() | softirq_init() | 고우선순위 tasklet |
| 1 | TIMER_SOFTIRQ | run_timer_softirq() | init_timers() | 커널 타이머 처리 |
| 2 | NET_TX_SOFTIRQ | net_tx_action() | net_dev_init() | 네트워크 송신 완료 |
| 3 | NET_RX_SOFTIRQ | net_rx_action() | net_dev_init() | 네트워크 수신 (NAPI) |
| 4 | BLOCK_SOFTIRQ | blk_done_softirq() | blk_softirq_init() | 블록 I/O 완료 |
| 5 | IRQ_POLL_SOFTIRQ | irq_poll_softirq() | irq_poll_setup_cpu() | IRQ 폴링 완료 |
| 6 | TASKLET_SOFTIRQ | tasklet_action() | softirq_init() | 일반 tasklet 실행 |
| 7 | SCHED_SOFTIRQ | run_rebalance_domains() | init_sched_fair_class() | 스케줄러 부하 분산 |
| 8 | HRTIMER_SOFTIRQ | hrtimer_run_softirq() | hrtimers_init() | 고해상도 타이머 |
| 9 | RCU_SOFTIRQ | rcu_core_si() | rcu_init() | RCU 콜백 처리 |
ffs()(find first set bit)로 가장 낮은 비트부터 처리하므로, HI_SOFTIRQ가 가장 먼저, RCU_SOFTIRQ가 가장 나중에 처리됩니다. 단, 이 순서는 우선순위가 아니라 단순한 처리 순서이며, 각 핸들러의 실행 시간에 따라 후순위 softirq의 지연이 발생할 수 있습니다.
관련 커널 설정
| CONFIG 옵션 | 설명 | 기본값 |
|---|---|---|
CONFIG_PREEMPT_NONE | 선점 없음, 서버 최적화 | 서버 defconfig |
CONFIG_PREEMPT_VOLUNTARY | 자발적 선점, 데스크톱 기본 | 데스크톱 defconfig |
CONFIG_PREEMPT | 완전 선점 | 선택 |
CONFIG_PREEMPT_RT | 실시간(Real-time) 선점 (6.12+) | 선택 |
CONFIG_WQ_WATCHDOG | workqueue 정체 감시 | y |
CONFIG_WQ_POWER_EFFICIENT_DEFAULT | 전력 효율 workqueue 기본 활성화 | n (노트북에서 y 권장) |
CONFIG_IRQ_FORCED_THREADING | 모든 IRQ를 강제 스레드화 | n |
PREEMPT_RT에서의 Bottom Half 변환 상세
PREEMPT_RT(Real-Time) 패치(Patch)가 적용되면 각 Bottom Half 메커니즘의 동작이 크게 변합니다. 아래 다이어그램은 일반 커널과 RT 커널에서 각 메커니즘의 실행 경로 변환을 보여줍니다.
PREEMPT_RT 마이그레이션 코드 패턴
tasklet에서 threaded IRQ로의 전환 과정을 단계별로 보여줍니다.
/* ===== 변환 전: tasklet 기반 드라이버 ===== */
struct my_device {
struct tasklet_struct rx_tasklet;
spinlock_t lock;
void __iomem *regs;
};
static irqreturn_t my_hardirq(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
u32 status = readl(dev->regs + IRQ_STATUS);
if (!(status & MY_IRQ_MASK))
return IRQ_NONE;
writel(status, dev->regs + IRQ_ACK);
tasklet_schedule(&dev->rx_tasklet); /* PREEMPT_RT 비호환! */
return IRQ_HANDLED;
}
static void my_tasklet_func(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, rx_tasklet);
unsigned long flags;
spin_lock_irqsave(&dev->lock, flags);
process_rx_data(dev);
spin_unlock_irqrestore(&dev->lock, flags);
}
/* ===== 변환 후: threaded IRQ 기반 ===== */
struct my_device {
/* tasklet 제거 */
spinlock_t lock;
void __iomem *regs;
};
static irqreturn_t my_hardirq(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
u32 status = readl(dev->regs + IRQ_STATUS);
if (!(status & MY_IRQ_MASK))
return IRQ_NONE;
writel(status, dev->regs + IRQ_ACK);
return IRQ_WAKE_THREAD; /* threaded handler 호출 */
}
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
spin_lock(&dev->lock); /* RT에서 rt_mutex로 안전하게 변환됨 */
process_rx_data(dev);
spin_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* probe에서 등록 */
request_threaded_irq(irq, my_hardirq, my_thread_fn,
IRQF_ONESHOT, "my_device", dev);
tasklet_schedule()호출 제거,IRQ_WAKE_THREAD반환으로 변경spin_lock_irqsave()를spin_lock()으로 단순화 (threaded context에서 불필요)request_irq()를request_threaded_irq()로 변경IRQF_ONESHOT플래그 추가 (threaded handler 완료까지 IRQ 마스킹)- RT 우선순위가 필요하면
chrt -f -p <priority> <irq_thread_pid>로 설정
성능 최적화 가이드
Bottom Half는 시스템 전체 성능에 큰 영향을 미칩니다. 효율적인 사용 방법을 소개합니다.
softirq 실행 시간 제한
/* __do_softirq() 내부 - 최대 2ms 또는 10번 재시작 제한 */
unsigned long end = jiffies + MAX_SOFTIRQ_TIME; /* 2ms */
int max_restart = MAX_SOFTIRQ_RESTART; /* 10 */
/* 성능 최적화: softirq 핸들러는 2ms 이내에 완료해야 함 */
static void my_softirq_action(struct softirq_action *h)
{
struct sk_buff *skb;
int budget = 64; /* 한 번에 처리할 패킷 수 제한 */
while ((skb = dequeue_packet()) && --budget > 0) {
process_packet(skb);
}
/* 남은 작업이 있으면 다시 스케줄 */
if (has_pending_packets())
raise_softirq(NET_RX_SOFTIRQ);
}
tasklet 병합으로 오버헤드 감소
/* ❌ 비효율적: 매 인터럽트마다 tasklet 스케줄 */
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
tasklet_schedule(&my_tasklet); /* 매번 스케줄 */
return IRQ_HANDLED;
}
/* ✅ 효율적: pending 상태면 스케줄 생략 */
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
atomic_inc(&dev->pending_count);
/* 이미 스케줄되어 있으면 중복 스케줄 안 함 */
if (!test_and_set_bit(TASKLET_STATE_SCHED, &my_tasklet.state))
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
static void my_tasklet_func(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, tasklet);
int count = atomic_xchg(&dev->pending_count, 0);
/* 여러 인터럽트를 한 번에 처리 */
process_batch(dev, count);
}
workqueue 동시성 튜닝
/* ❌ 기본 workqueue - 제한된 동시성 */
schedule_work(&my_work); /* system_wq 사용 */
/* ✅ 커스텀 workqueue - 동시성 제어 */
struct workqueue_struct *my_wq;
/* CPU-bound 작업 - UNBOUND로 CPU 간 이동 허용 */
my_wq = alloc_workqueue("my_wq",
WQ_UNBOUND | WQ_HIGHPRI,
4); /* max_active = 4 */
/* I/O-bound 작업 - 높은 동시성 허용 */
my_wq = alloc_workqueue("io_wq",
WQ_MEM_RECLAIM,
256); /* 많은 동시 실행 허용 */
/* 성능 측정 */
u64 start = ktime_get_ns();
flush_workqueue(my_wq);
u64 elapsed = ktime_get_ns() - start;
pr_info("Workqueue flush took %llu ns\n", elapsed);
NAPI와의 통합 최적화
/* 네트워크 드라이버 최적화 패턴 */
static int my_napi_poll(struct napi_struct *napi, int budget)
{
struct my_device *dev = container_of(napi, struct my_device, napi);
int work_done = 0;
/* budget만큼만 처리 (softirq 시간 제한 준수) */
while (work_done < budget) {
struct sk_buff *skb = receive_packet(dev);
if (!skb)
break;
netif_receive_skb(skb);
work_done++;
}
/* 모든 패킷 처리했으면 NAPI 종료 */
if (work_done < budget) {
napi_complete(napi);
enable_interrupts(dev);
}
return work_done;
}
성능 측정 도구
# softirq 통계 확인
cat /proc/softirqs
# CPU0 CPU1 CPU2 CPU3
# HI: 0 0 0 0
# TIMER: 5123456 4987654 5234567 5123456
# NET_TX: 12345 9876 11234 10123
# NET_RX: 9876543 8765432 9123456 8987654
# workqueue 통계 (debugfs)
cat /sys/kernel/debug/workqueue/workqueues
cat /sys/kernel/debug/workqueue/pool_workqueues
# 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
# perf로 softirq 시간 분석
perf record -e irq:softirq_entry,irq:softirq_exit -a sleep 10
perf script
- Batch 처리: 가능하면 여러 아이템을 한 번에 처리
- Budget 제한: softirq는 2ms 이내, tasklet은 최소화
- 적절한 선택: 빠른 처리는 softirq, 복잡한 처리는 workqueue
- 측정 기반: 추측 대신 실제 측정 데이터로 최적화
실전 케이스 스터디
실제 드라이버와 서브시스템에서 Bottom Half를 활용하는 패턴을 분석합니다.
케이스 1: 네트워크 드라이버 (NAPI + softirq)
시나리오: 고성능 네트워크 카드의 RX 처리
/* drivers/net/ethernet/intel/e1000e/netdev.c 패턴 */
/* 1. 인터럽트 핸들러 (Top Half) */
static irqreturn_t e1000_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct e1000_adapter *adapter = netdev_priv(netdev);
u32 icr = er32(ICR);
if (!icr)
return IRQ_NONE; /* 우리 인터럽트 아님 */
if (icr & E1000_ICR_RXT0) { /* RX 인터럽트 */
/* 인터럽트 비활성화 */
ew32(IMC, ~0);
/* NAPI 스케줄 (softirq로 위임) */
if (napi_schedule_prep(&adapter->napi)) {
__napi_schedule(&adapter->napi);
}
}
return IRQ_HANDLED;
}
/* 2. NAPI poll (NET_RX_SOFTIRQ에서 실행) */
static int e1000_clean(struct napi_struct *napi, int budget)
{
struct e1000_adapter *adapter =
container_of(napi, struct e1000_adapter, napi);
int work_done = 0;
/* budget만큼만 처리 */
e1000_clean_rx_irq(adapter, &work_done, budget);
/* 모두 처리했으면 NAPI 완료 */
if (work_done < budget) {
napi_complete_done(napi, work_done);
/* 인터럽트 재활성화 */
e1000_irq_enable(adapter);
}
return work_done;
}
/* 3. 실제 패킷 처리 */
static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,
int *work_done, int work_to_do)
{
while (*work_done < work_to_do) {
struct sk_buff *skb = e1000_receive_skb(adapter);
if (!skb)
break;
/* 네트워크 스택으로 전달 */
napi_gro_receive(&adapter->napi, skb);
(*work_done)++;
}
return false;
}
핵심 포인트:
- Top Half: 최소한의 작업 (인터럽트 비활성화, NAPI 스케줄)
- Bottom Half: softirq(NET_RX)에서 실제 패킷(Packet) 처리
- Budget 제한으로 softirq 시간 제한 준수
케이스 2: 블록 드라이버 (workqueue)
시나리오: NVMe 드라이버의 I/O 완료 처리
/* drivers/nvme/host/pci.c 패턴 */
/* 1. 인터럽트 핸들러 */
static irqreturn_t nvme_irq(int irq, void *data)
{
struct nvme_queue *nvmeq = data;
/* CQ에서 완료 엔트리 확인 */
if (nvme_cqe_pending(nvmeq)) {
/* workqueue로 완료 처리 위임 */
queue_work(nvmeq->cq_wq, &nvmeq->cq_work);
}
return IRQ_HANDLED;
}
/* 2. Workqueue 핸들러 */
static void nvme_cq_work(struct work_struct *work)
{
struct nvme_queue *nvmeq =
container_of(work, struct nvme_queue, cq_work);
/* 완료 큐 처리 (블로킹 가능) */
nvme_process_cq(nvmeq);
/* 블록 레이어에 완료 통지 */
blk_mq_complete_request(...);
}
케이스 3: 타이머(Timer) + tasklet 조합
시나리오: 주기적 하드웨어 폴링(Polling)
/* 실전 패턴: 폴링 기반 디바이스 */
struct my_device {
struct timer_list poll_timer;
struct tasklet_struct poll_tasklet;
void __iomem *regs;
};
/* 1. 타이머 콜백 (TIMER_SOFTIRQ) */
static void poll_timer_func(struct timer_list *t)
{
struct my_device *dev = from_timer(dev, t, poll_timer);
/* 빠른 레지스터 읽기만 수행 */
u32 status = readl(dev->regs + STATUS_REG);
if (status & DATA_READY) {
/* 실제 처리는 tasklet으로 위임 */
tasklet_schedule(&dev->poll_tasklet);
}
/* 다음 폴링 예약 (100ms) */
mod_timer(&dev->poll_timer, jiffies + HZ/10);
}
/* 2. Tasklet 핸들러 */
static void poll_tasklet_func(struct tasklet_struct *t)
{
struct my_device *dev = from_tasklet(dev, t, poll_tasklet);
/* 데이터 처리 (약간 더 복잡한 작업) */
process_device_data(dev);
}
케이스 4: 지연된 리소스 정리
시나리오: RCU + workqueue로 안전한 메모리 해제
/* 실전 패턴: RCU와 workqueue 조합 */
struct my_object {
struct rcu_head rcu;
struct work_struct cleanup_work;
void *large_buffer;
};
/* 1. 객체 삭제 요청 */
void delete_object(struct my_object *obj)
{
/* RCU로 readers 보호 */
call_rcu(&obj->rcu, object_rcu_callback);
}
/* 2. RCU 콜백 (softirq) */
static void object_rcu_callback(struct rcu_head *rcu)
{
struct my_object *obj = container_of(rcu, struct my_object, rcu);
/* 무거운 정리 작업은 workqueue로 위임 */
INIT_WORK(&obj->cleanup_work, cleanup_work_func);
schedule_work(&obj->cleanup_work);
}
/* 3. Workqueue에서 실제 정리 */
static void cleanup_work_func(struct work_struct *work)
{
struct my_object *obj =
container_of(work, struct my_object, cleanup_work);
/* 슬립 가능한 정리 작업 */
vfree(obj->large_buffer); /* 시간이 걸릴 수 있음 */
kfree(obj);
}
실전 네트워크 드라이버 BH 처리 경로
네트워크 드라이버에서 패킷이 수신되어 프로토콜 스택까지 전달되는 과정의 전체 Bottom Half 경로를 시각화합니다. NIC 인터럽트부터 소켓(Socket) 버퍼(Buffer) 전달까지의 흐름입니다.
네트워크 드라이버 BH 핵심 코드 분석
/* net/core/dev.c - NET_RX softirq 핸들러 */
static void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
unsigned long time_limit = jiffies +
usecs_to_jiffies(READ_ONCE(netdev_budget_usecs));
int budget = READ_ONCE(netdev_budget); /* 기본값 300 */
LIST_HEAD(list);
LIST_HEAD(repoll);
local_irq_disable();
list_splice_init(&sd->poll_list, &list);
local_irq_enable();
for (;;) {
struct napi_struct *n;
skb_defer_free_flush(sd);
if (list_empty(&list)) {
if (!sd_has_rps_ipi_waiting(sd) &&
list_empty(&repoll))
return;
break;
}
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
/* 시간 초과 또는 budget 소진 시 중단 */
if (unlikely(budget <= 0 ||
time_after_eq(jiffies, time_limit))) {
sd->time_squeeze++;
break;
}
}
/* 남은 NAPI가 있으면 softirq 재스케줄 */
/* -> __raise_softirq_irqoff(NET_RX_SOFTIRQ) */
}
디버깅 도구와 기법
Bottom Half 관련 문제를 진단하기 위한 도구와 기법을 체계적으로 정리합니다.
/proc/softirqs 분석
# softirq 타입별, CPU별 누적 카운트 확인
cat /proc/softirqs
# CPU0 CPU1 CPU2 CPU3
# HI: 2 0 0 1
# TIMER: 5123456 4987654 5234567 5123456
# NET_TX: 12345 9876 11234 10123
# NET_RX: 9876543 8765432 9123456 8987654
# BLOCK: 234567 198765 212345 207654
# IRQ_POLL: 0 0 0 0
# TASKLET: 4567 3456 4321 3987
# SCHED: 2345678 2234567 2456789 2345678
# HRTIMER: 12345 11234 13456 12345
# RCU: 3456789 3345678 3567890 3456789
# 실시간 모니터링 (1초 간격으로 변화량 관찰)
watch -d -n 1 'cat /proc/softirqs'
# CPU 간 불균형 확인 (NET_RX가 한 CPU에 몰리면 RSS 설정 필요)
# softnet_stat으로 추가 통계 확인
cat /proc/net/softnet_stat
# column 1: processed column 2: dropped column 3: time_squeeze
ftrace를 이용한 softirq/workqueue 추적
# ftrace 설정: softirq 이벤트 추적
cd /sys/kernel/tracing
# softirq 진입/종료 이벤트 활성화
echo 1 > events/irq/softirq_entry/enable
echo 1 > events/irq/softirq_exit/enable
echo 1 > events/irq/softirq_raise/enable
# workqueue 이벤트 추적
echo 1 > events/workqueue/workqueue_queue_work/enable
echo 1 > events/workqueue/workqueue_execute_start/enable
echo 1 > events/workqueue/workqueue_execute_end/enable
# 추적 시작
echo 1 > tracing_on
sleep 5
echo 0 > tracing_on
# 결과 확인
cat trace | head -50
# <idle>-0 [001] ..s1 1234.567890: softirq_entry: vec=3 [action=NET_RX]
# <idle>-0 [001] ..s1 1234.567925: softirq_exit: vec=3 [action=NET_RX]
# kworker/1:0-123 [001] .... 1234.568000: workqueue_execute_start: work=...
# softirq 실행 시간 히스토그램 (function_graph 트레이서)
echo function_graph > current_tracer
echo do_softirq > set_graph_function
echo 1 > tracing_on
sleep 2
echo 0 > tracing_on
cat trace
perf를 이용한 성능 분석
# softirq 핫스팟 분석
perf record -e irq:softirq_entry -e irq:softirq_exit -a -g sleep 10
perf report
# softirq 실행 시간 분포
perf script | awk '/softirq_entry/{start=$4} /softirq_exit/{print $4-start}'
# workqueue 실행 빈도
perf stat -e workqueue:workqueue_execute_start -a sleep 10
# 스케줄링 지연 측정 (wakeup latency)
perf sched record sleep 5
perf sched latency --sort max
# IRQ 비활성화 시간 측정 (irqsoff tracer)
echo irqsoff > /sys/kernel/tracing/current_tracer
echo 1 > /sys/kernel/tracing/tracing_on
sleep 5
echo 0 > /sys/kernel/tracing/tracing_on
cat /sys/kernel/tracing/trace
디버깅용 커널 설정
| CONFIG 옵션 | 용도 | 오버헤드 |
|---|---|---|
CONFIG_DEBUG_ATOMIC_SLEEP | atomic context에서 슬립 시도 감지 | 낮음 |
CONFIG_PROVE_LOCKING | lockdep: 데드락 가능성 감지 | 높음 |
CONFIG_DEBUG_OBJECTS_WORK | work_struct 사용 오류 감지 | 중간 |
CONFIG_WQ_WATCHDOG | workqueue 정체(stall) 감시 | 낮음 |
CONFIG_SOFTIRQ_DEBUG | softirq 디버그 정보 강화 | 낮음 |
CONFIG_FTRACE | 함수 추적 인프라 | 중간 |
CONFIG_IRQSOFF_TRACER | IRQ 비활성화 시간 추적 | 중간 |
CONFIG_PREEMPTIRQ_TRACEPOINTS | 선점/IRQ 비활성화 tracepoints | 낮음 |
데드락과 우선순위 역전 패턴
Bottom Half 사용 시 발생하기 쉬운 데드락 및 우선순위 역전 패턴과 방지 방법입니다.
패턴 1: softirq와 프로세스 간 spinlock 데드락
/* ❌ 데드락 시나리오:
* CPU 0에서 process_context()가 spin_lock(&lock) 획득
* -> 인터럽트 발생 -> softirq에서 spin_lock(&lock) 시도
* -> 같은 CPU에서 락 보유자를 선점할 수 없어 영원히 대기 */
spinlock_t my_lock;
/* 프로세스 컨텍스트 */
void process_context(void)
{
spin_lock(&my_lock); /* ❌ BH 비활성화 없이 락 획득 */
do_something();
spin_unlock(&my_lock);
}
/* softirq 컨텍스트 */
void my_softirq(struct softirq_action *h)
{
spin_lock(&my_lock); /* 데드락! */
process_data();
spin_unlock(&my_lock);
}
/* ✅ 올바른 패턴: spin_lock_bh() 사용 */
void process_context(void)
{
spin_lock_bh(&my_lock); /* BH 비활성화 + 락 획득 */
do_something();
spin_unlock_bh(&my_lock);
}
void my_softirq(struct softirq_action *h)
{
spin_lock(&my_lock); /* BH에서는 spin_lock만으로 충분 */
process_data();
spin_unlock(&my_lock);
}
패턴 2: workqueue 중첩 flush 데드락
/* ❌ 데드락 시나리오:
* work_A가 system_wq에서 실행 중 flush_work(&work_B) 호출
* work_B도 system_wq에 대기 중이지만 max_active=1이면
* work_A가 완료될 때까지 work_B 실행 불가 -> 순환 대기 */
static void work_a_func(struct work_struct *work)
{
do_part1();
flush_work(&work_b); /* ❌ 같은 WQ에서 다른 work 대기 */
do_part2();
}
/* ✅ 올바른 패턴: 별도 workqueue 사용 또는 설계 변경 */
static void work_a_func(struct work_struct *work)
{
do_part1();
/* work_b를 다른 workqueue에 큐잉 */
queue_work(separate_wq, &work_b);
/* 또는 completion 사용 */
wait_for_completion(&work_b_done);
do_part2();
}
패턴 3: softirq 우선순위 역전 (starvation)
/* 우선순위 역전 시나리오:
* 1. 고우선순위 RT 태스크가 CPU에서 실행 중
* 2. 네트워크 패킷 도착 -> NET_RX softirq 발생
* 3. RT 태스크가 선점을 허용하지 않아 softirq 지연
* 4. 네트워크 패킷 처리가 밀려 드롭 발생 */
/* 해결 방법 1: IRQ affinity 분리 */
/* RT 태스크 CPU와 IRQ 처리 CPU를 분리 */
# echo 0-1 > /proc/irq/<NIC_IRQ>/smp_affinity_list
# taskset -c 2-3 ./rt_application
/* 해결 방법 2: PREEMPT_RT + threaded IRQ */
/* 네트워크 IRQ 스레드 우선순위를 RT 태스크보다 높게 설정 */
# chrt -f -p 90 $(pgrep -f "irq/.*eth0")
/* 해결 방법 3: ksoftirqd 우선순위 조정 */
# chrt -f -p 50 $(pgrep ksoftirqd/0)
- 프로세스 컨텍스트에서 softirq와 공유하는 락: 반드시
spin_lock_bh()사용 - hardirq와 공유하는 락: 반드시
spin_lock_irqsave()사용 - work 내부에서 자신이 속한 workqueue를 flush하지 않기
- ordered workqueue에서 다른 work를 flush하지 않기
CONFIG_PROVE_LOCKING활성화로 lockdep 검증 필수 실행
추가 실전 케이스: USB와 블록 I/O
USB 드라이버 BH 패턴
USB 드라이버는 URB(USB Request Block) 완료 콜백(Callback)이 인터럽트 컨텍스트에서 호출되므로, 무거운 처리는 workqueue로 위임해야 합니다.
/* USB 완료 콜백 (인터럽트 컨텍스트) */
static void usb_rx_complete(struct urb *urb)
{
struct my_usb_dev *dev = urb->context;
switch (urb->status) {
case 0: /* 성공 */
/* 데이터를 버퍼에 복사 (빠른 작업만) */
memcpy(dev->rx_buf + dev->rx_len,
urb->transfer_buffer, urb->actual_length);
dev->rx_len += urb->actual_length;
/* 무거운 처리는 workqueue로 위임 */
schedule_work(&dev->rx_work);
break;
case -ENOENT:
case -ECONNRESET:
case -ESHUTDOWN:
return; /* URB 취소됨 */
default:
dev_err(&dev->intf->dev, "URB error: %d\n", urb->status);
}
/* URB 재제출 (다음 데이터 수신) */
usb_submit_urb(urb, GFP_ATOMIC);
}
/* workqueue 핸들러 (프로세스 컨텍스트) */
static void usb_rx_work(struct work_struct *work)
{
struct my_usb_dev *dev = container_of(work,
struct my_usb_dev, rx_work);
mutex_lock(&dev->data_mutex); /* 슬립 가능 */
parse_protocol(dev->rx_buf, dev->rx_len);
deliver_to_userspace(dev);
dev->rx_len = 0;
mutex_unlock(&dev->data_mutex);
}
블록 I/O 완료 처리 패턴
/* 블록 I/O 완료 경로: softirq(BLOCK_SOFTIRQ) 또는 IRQ */
/* drivers/block/virtio_blk.c 패턴 */
/* 1. 인터럽트 핸들러 (Top Half) */
static irqreturn_t virtblk_irq(int irq, void *data)
{
struct virtio_blk_vq *vq = data;
/* 완료 처리를 위해 BLOCK softirq 스케줄 */
blk_mq_complete_request(rq);
return IRQ_HANDLED;
}
/* 2. blk_mq 완료 경로 선택 */
/* blk_mq_complete_request() 내부:
* - 같은 CPU에서 완료 가능하면 softirq로 직접 실행
* - 다른 CPU면 IPI 전송하여 해당 CPU의 softirq에서 완료
* - 목적: 캐시 친화성 최적화 */
/* 3. 완료 콜백 (softirq 또는 프로세스 컨텍스트) */
static void virtblk_done(struct request *rq)
{
struct virtblk_req *vbr = blk_mq_rq_to_pdu(rq);
/* 에러 처리 */
blk_mq_end_request(rq, virtblk_result(vbr));
/* -> I/O 완료 통지 -> 대기 중인 프로세스 깨움 */
}
블록 I/O BH 처리 경로 다이어그램
블록 I/O 요청이 디스크에 제출되고 완료 인터럽트를 거쳐 프로세스에 통지되기까지의 Bottom Half 처리 경로입니다. NVMe/virtio-blk 드라이버에서 blk-mq 레이어를 거치는 전체 흐름을 보여줍니다.
문제 해결 FAQ
Bottom Half 사용 시 자주 발생하는 문제와 해결 방법입니다.
Q1: "ksoftirqd가 CPU를 100% 사용합니다"
증상: top에서 ksoftirqd/N이 높은 CPU 사용률
# CPU별 softirq 카운트 확인
watch -n1 'cat /proc/softirqs'
# 특정 softirq가 급증하는지 확인
# NET_RX가 높으면 네트워크 부하, BLOCK이 높으면 I/O 부하
원인: softirq 부하가 너무 높아 ksoftirqd로 처리 위임
해결:
# 1. 네트워크 인터럽트 분산 (RSS/RPS)
ethtool -L eth0 combined 4 # 4개 큐 사용
# 2. IRQ affinity 설정
echo 2 > /proc/irq/<IRQ번호>/smp_affinity_list # CPU 2에 바인딩
# 3. NAPI budget 조정 (패킷 처리량 제한)
sysctl -w net.core.netdev_budget=300
sysctl -w net.core.netdev_budget_usecs=2000
Q2: tasklet이 실행되지 않습니다
디버깅:
# tasklet 상태 확인 (커널 코드)
printk("tasklet state: %lx\n", my_tasklet.state);
/* TASKLET_STATE_SCHED (0x01): 스케줄됨
TASKLET_STATE_RUN (0x02): 실행 중 */
# softirq 통계 확인
cat /proc/softirqs | grep -E 'HI|TASKLET'
가능한 원인:
tasklet_disable()호출됨 -tasklet_enable()확인tasklet_kill()호출 후 재스케줄 시도- Atomic counter 오류로 state 손상
해결:
/* enable/disable 쌍 확인 */
tasklet_disable(&my_tasklet);
do_something();
tasklet_enable(&my_tasklet); /* 반드시 호출! */
/* kill 후 재초기화 */
tasklet_kill(&my_tasklet);
tasklet_setup(&my_tasklet, my_func); /* 재초기화 */
Q3: workqueue가 예상보다 늦게 실행됩니다
증상: schedule_work() 호출 후 수초 지연
디버깅:
# workqueue 상태 확인
cat /sys/kernel/debug/workqueue/workqueues
cat /sys/kernel/debug/workqueue/pool_workqueues
# 대기 중인 work 확인
cat /proc/PID/stack # kworker PID
원인:
- 같은 workqueue에서 다른 work이 블로킹 중
max_active제한 도달- CPU-bound workqueue에서 I/O 대기
해결:
/* 전용 workqueue 생성 */
struct workqueue_struct *my_wq;
my_wq = alloc_workqueue("my_fast_wq",
WQ_HIGHPRI | WQ_UNBOUND,
0); /* 제한 없음 */
queue_work(my_wq, &my_work); /* system_wq 대신 사용 */
Q4: "BUG: sleeping function called from invalid context" 에러
증상: softirq/tasklet에서 슬립 함수 호출
BUG: sleeping function called from invalid context at kernel/locking/mutex.c:...
in_atomic(): 1, irqs_disabled(): 0, non_block: 0, pid: 0, name: swapper/0
원인: atomic context에서 블로킹 함수 호출
해결:
/* ❌ tasklet에서 mutex */
static void my_tasklet(struct tasklet_struct *t)
{
mutex_lock(&my_mutex); /* 에러 발생 */
do_something();
mutex_unlock(&my_mutex);
}
/* ✅ workqueue로 변경 */
static void my_work(struct work_struct *work)
{
mutex_lock(&my_mutex); /* OK */
do_something();
mutex_unlock(&my_mutex);
}
Q5: PREEMPT_RT에서 지연시간이 증가했습니다
증상: CONFIG_PREEMPT_RT 활성화 후 응답 시간 저하
원인: tasklet이 ksoftirqd에서 실행되며 스케줄링 지연 발생
해결:
/* tasklet을 threaded IRQ로 변경 */
int ret = request_threaded_irq(
irq,
my_hardirq_handler, /* Top Half */
my_thread_fn, /* Threaded Bottom Half */
IRQF_ONESHOT,
"my_device",
dev);
/* 스레드 우선순위 조정 */
struct sched_param param = { .sched_priority = 50 };
sched_setscheduler(current, SCHED_FIFO, ¶m);
Q6: workqueue flush 시 데드락 발생
증상: flush_workqueue()에서 멈춤
# 스택 트레이스 확인
cat /proc/PID/stack
# [<ffffffff>] flush_workqueue+0x...
# [<ffffffff>] my_cleanup+0x...
원인: work 내부에서 자신이 속한 workqueue flush
해결:
/* ❌ 데드락 패턴 */
static void my_work(struct work_struct *work)
{
flush_workqueue(system_wq); /* 데드락! */
}
/* ✅ 올바른 정리 순서 */
void module_exit(void)
{
/* 1. 새 work 스케줄 중단 */
shutdown_flag = 1;
smp_wmb();
/* 2. pending work 취소 */
cancel_work_sync(&my_work);
/* 3. workqueue flush (외부에서) */
flush_workqueue(my_wq);
/* 4. workqueue 파괴 */
destroy_workqueue(my_wq);
}
/proc/softirqs- softirq 통계/sys/kernel/debug/workqueue/- workqueue 상태ftrace- irq:softirq_entry/exit 추적perf- 성능 병목(Bottleneck) 분석CONFIG_DEBUG_ATOMIC_SLEEP- atomic context 검증
Threaded IRQ
Threaded IRQ는 인터럽트 핸들러(Handler)의 Bottom Half를 전용 커널 스레드에서 실행하는 메커니즘입니다. request_threaded_irq()를 통해 hardirq 핸들러(Top Half)와 스레드 함수(Bottom Half)를 분리하여, 프로세스 컨텍스트의 이점(슬립, 잠금 획득)을 활용할 수 있습니다.
request_threaded_irq() 핵심 패턴
#include <linux/interrupt.h>
/* Top Half: 최소한의 하드웨어 확인만 수행 */
static irqreturn_t my_hardirq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* 인터럽트 소스 확인 */
if (!my_device_irq_pending(dev))
return IRQ_NONE; /* 공유 IRQ: 내 장치가 아님 */
/* 하드웨어 인터럽트 비활성화 (level-triggered 필수) */
my_device_disable_irq(dev);
return IRQ_WAKE_THREAD; /* 스레드 함수 깨우기 */
}
/* Bottom Half: 프로세스 컨텍스트에서 실행 */
static irqreturn_t my_thread_fn(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* 슬립 가능! mutex, I/O 등 자유롭게 사용 */
mutex_lock(&dev->lock);
my_device_process_data(dev);
mutex_unlock(&dev->lock);
/* 하드웨어 인터럽트 재활성화 */
my_device_enable_irq(dev);
return IRQ_HANDLED;
}
/* 등록 */
request_threaded_irq(irq, my_hardirq_handler, my_thread_fn,
IRQF_ONESHOT | IRQF_SHARED,
"my_device", dev);
핵심 플래그와 동작
| 플래그 | 의미 | 필수 여부 |
|---|---|---|
IRQF_ONESHOT | 스레드 함수 완료 시까지 IRQ 라인 비활성화 유지 | level-triggered에서 필수 |
IRQF_SHARED | 여러 디바이스가 동일 IRQ 라인 공유 | 공유 시 필수 |
IRQF_NO_THREAD | force_irqthreads 커널 파라미터에서도 스레드화 면제 | 타이머 등 특수 경우 |
IRQF_NO_AUTOEN | 등록 시 자동 활성화 방지 (수동 enable_irq() 필요) | 초기화 순서 제어 |
IRQF_ONESHOT 없이 hardirq가 IRQ_WAKE_THREAD를 반환하면, 스레드가 실행되기 전에 같은 인터럽트가 다시 발생하여 무한 루프에 빠질 수 있습니다. edge-triggered에서는 선택적이지만, 안전을 위해 항상 사용하는 것을 권장합니다.
Threaded IRQ가 기존 BH보다 나은 시나리오
| 시나리오 | Threaded IRQ 장점 | 기존 BH (softirq/tasklet) |
|---|---|---|
| I2C/SPI 디바이스 | 슬립 가능한 버스(Bus) 접근 가능 | 불가 (atomic context) |
| PREEMPT_RT 환경 | 네이티브 지원, 우선순위 제어 가능 | 강제 스레드화 (오버헤드) |
| 복잡한 프로토콜 처리 | mutex, 메모리 할당 자유 | spin_lock만 가능 |
| 디버깅 | 스택 트레이스 명확, ps로 확인 | ksoftirqd 내부로 합쳐짐 |
| 전력 관리 | 유휴 시 슬립, CPU 부하 최소 | 폴링 기반 처리 경향 |
WQ_BH: Workqueue 기반 Bottom Half (6.9+)
커널 6.9부터 도입된 WQ_BH 플래그는 workqueue를 softirq 컨텍스트에서 실행하면서도 workqueue API의 편의성을 유지합니다. 이는 tasklet의 공식 대체 메커니즘으로, tasklet deprecation을 가속화하기 위해 설계되었습니다.
도입 배경
tasklet은 오랫동안 deprecation 대상이었지만, softirq 컨텍스트 실행이 필요한 드라이버에서 직접 softirq를 등록할 수 없어 대안이 없었습니다. WQ_BH는 이 문제를 해결합니다.
| 특성 | tasklet | WQ_BH workqueue |
|---|---|---|
| 실행 컨텍스트 | softirq (TASKLET_SOFTIRQ) | softirq (BH workqueue) |
| API | tasklet_schedule()/tasklet_kill() | queue_work()/cancel_work_sync() |
| 동시성 제어 | 같은 tasklet 직렬화 | work 단위 직렬화 |
| 취소 | tasklet_kill() (복잡한 경쟁 조건) | cancel_work_sync() (명확한 의미) |
| 디버깅 | 제한적 | workqueue 디버깅 인프라 활용 |
| PREEMPT_RT | 강제 스레드화 | 통합된 스레드화 경로 |
system_bh_wq 사용법
#include <linux/workqueue.h>
static struct work_struct my_bh_work;
static void my_bh_work_fn(struct work_struct *work)
{
/* softirq 컨텍스트에서 실행됨 — 슬립 불가! */
process_pending_data();
}
/* 초기화 */
INIT_WORK(&my_bh_work, my_bh_work_fn);
/* 스케줄링 — system_bh_wq는 WQ_BH 플래그가 설정된 전역 workqueue */
queue_work(system_bh_wq, &my_bh_work);
/* 정리 */
cancel_work_sync(&my_bh_work);
tasklet에서 WQ_BH로 마이그레이션
/* === 변환 전: tasklet === */
static struct tasklet_struct my_tasklet;
static void my_tasklet_fn(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
process_data(dev);
}
tasklet_init(&my_tasklet, my_tasklet_fn, (unsigned long)dev);
tasklet_schedule(&my_tasklet); /* 스케줄링 */
tasklet_kill(&my_tasklet); /* 정리 */
/* === 변환 후: WQ_BH === */
static struct work_struct my_bh_work;
static void my_bh_work_fn(struct work_struct *work)
{
struct my_device *dev = container_of(work, struct my_device, bh_work);
process_data(dev);
}
INIT_WORK(&dev->bh_work, my_bh_work_fn);
queue_work(system_bh_wq, &dev->bh_work); /* 스케줄링 */
cancel_work_sync(&dev->bh_work); /* 정리 */
tasklet_struct → work_struct, tasklet_schedule() → queue_work(system_bh_wq, ...), tasklet_kill() → cancel_work_sync(). 콜백 시그니처가 (unsigned long)에서 (struct work_struct *)로 변경되므로 container_of() 패턴을 사용합니다.
커널 6.x Bottom Half 변경사항
커널 6.x 시리즈에서 Bottom Half 서브시스템은 상당한 변화를 겪고 있습니다. tasklet deprecation이 본격화되고, workqueue에 새로운 기능이 추가되며, per-CPU 처리 모델이 개선되고 있습니다.
주요 변경 타임라인
| 커널 버전 | 변경사항 | 영향 |
|---|---|---|
| 5.15 | tasklet API에 deprecation 경고 추가 | 새 드라이버에서 tasklet 사용 자제 권고 |
| 6.3 | Workqueue affinity_scope 준비 작업 | NUMA 인식 worker 배치 기반 마련 |
| 6.5 | affinity_scope 도입 (system/pod/node/cpu/cache) | NUMA 토폴로지(Topology) 기반 worker 배치 최적화 |
| 6.7 | Workqueue BH 실행 준비 (WQ_BH 프레임워크) | softirq 컨텍스트 workqueue 인프라 구축 |
| 6.9 | WQ_BH 도입 | tasklet 대체 공식 메커니즘 확립 |
| 6.10+ | 네트워킹 서브시스템 tasklet → WQ_BH 변환 진행 | 주요 드라이버 마이그레이션 시작 |
Workqueue affinity_scope (6.5)
/* affinity_scope 레벨:
* WQ_AFFN_SYSTEM — 시스템 전체 (기존 동작)
* WQ_AFFN_NODE — NUMA 노드 단위
* WQ_AFFN_CPU — 단일 CPU 단위
* WQ_AFFN_CACHE — 캐시 공유 단위 (L3 등)
* WQ_AFFN_DFL — 기본값 (unbound: cache, bound: cpu)
*/
/* sysfs를 통한 런타임 조정 */
/* /sys/devices/virtual/workqueue/<wq_name>/affinity_scope */
/* 코드에서 직접 설정 */
struct workqueue_struct *wq;
wq = alloc_workqueue("my_wq", WQ_UNBOUND, 0);
/* 기본적으로 WQ_AFFN_CACHE가 적용됨 */
tasklet deprecation 현황
- 완료: 일부 네트워크 드라이버 (e1000e, igb 일부), USB 일부, 암호화(Encryption) 드라이버
- 진행 중: 네트워킹 코어 (NET_TX_SOFTIRQ/NET_RX_SOFTIRQ는 softirq 유지), 블루투스
- 미정: SCSI 일부, 레거시 사운드 드라이버, 아키텍처별 코드
tasklet은 아직 완전히 제거되지 않았으며, 당분간 유지될 예정입니다. 그러나 새 코드에서는 반드시 WQ_BH 또는 workqueue를 사용해야 합니다.
Bottom Half 지연 벤치마크
Bottom Half 메커니즘 선택 시 가장 중요한 고려사항 중 하나는 지연(latency)과 처리량(throughput)의 트레이드오프입니다. 각 메커니즘의 실측 특성을 이해하면 적절한 선택을 할 수 있습니다.
측정 방법론
/* ftrace를 이용한 BH 지연 측정 */
# softirq 진입/종료 추적
echo 1 > /sys/kernel/tracing/events/irq/softirq_entry/enable
echo 1 > /sys/kernel/tracing/events/irq/softirq_exit/enable
# workqueue 실행 추적
echo 1 > /sys/kernel/tracing/events/workqueue/workqueue_execute_start/enable
echo 1 > /sys/kernel/tracing/events/workqueue/workqueue_execute_end/enable
# BPF 기반 정밀 측정 (bpftrace)
bpftrace -e '
tracepoint:irq:softirq_entry { @start[tid] = nsecs; }
tracepoint:irq:softirq_exit /@start[tid]/ {
@latency_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
'
# cyclictest: 실시간 지연 측정
cyclictest --mlockall --smp -p99 -i200 -D60
/* --smp: 모든 CPU 측정, -p99: RT 우선순위 99
* -i200: 200us 간격, -D60: 60초 실행 */
메커니즘별 벤치마크 비교
| 메커니즘 | 스케줄링 지연 (평균) | 스케줄링 지연 (p99) | 컨텍스트 전환 비용 | 처리량 (ops/sec) | 메모리 오버헤드 |
|---|---|---|---|---|---|
| softirq | ~0.5us | ~2us | 없음 (인터럽트 반환 시) | ~2,000,000 | 최소 (per-CPU 변수) |
| tasklet | ~0.7us | ~3us | 없음 (softirq 기반) | ~1,500,000 | ~64B (tasklet_struct) |
| WQ_BH | ~0.8us | ~4us | 없음 (softirq 기반) | ~1,200,000 | ~128B (work_struct+WQ) |
| workqueue (bound) | ~3us | ~15us | 1회 (kworker 스케줄링) | ~500,000 | ~128B + worker thread |
| workqueue (unbound) | ~5us | ~25us | 1회 + NUMA 이동 가능 | ~400,000 | ~128B + worker pool |
| threaded IRQ | ~4us | ~20us | 1회 (전용 스레드) | ~300,000 | ~2KB (커널 스레드 스택) |
처리량 vs 지연시간 트레이드오프
| 요구사항 | 권장 메커니즘 | 근거 |
|---|---|---|
| 최대 처리량 (네트워크 패킷) | softirq (NAPI) | 컨텍스트 전환 없이 인터럽트 반환 시 즉시 실행 |
| 낮은 지연 + 슬립 필요 | threaded IRQ | 전용 스레드로 빠른 깨움, RT 우선순위 설정 가능 |
| 균형잡힌 성능 | workqueue (WQ_HIGHPRI) | 높은 우선순위 worker pool, 슬립 가능 |
| 대량 비동기 작업 | workqueue (WQ_UNBOUND) | CPU 바운드 아닌 작업에 최적, 자동 확장 |
| PREEMPT_RT 예측 가능성 | threaded IRQ | 결정론적 우선순위 스케줄링, 지연 보장 |
irq_work: NMI/hardirq에서 지연 실행
irq_work는 NMI(Non-Maskable Interrupt)나 hardirq 컨텍스트에서도 안전하게 콜백을 지연 실행할 수 있는 유일한 메커니즘입니다. softirq조차 NMI에서는 안전하지 않지만, irq_work는 IPI(Inter-Processor Interrupt)나 self-IPI를 통해 안전하게 작업을 위임합니다.
irq_work API
#include <linux/irq_work.h>
static void my_irq_work_handler(struct irq_work *work)
{
/* hardirq 컨텍스트에서 실행됨 */
/* NMI-safe가 보장되어야 하는 작업 수행 */
do_deferred_output();
}
static struct irq_work my_work = IRQ_WORK_INIT_HARD(my_irq_work_handler);
/* NMI 또는 hardirq에서 호출 가능 */
void nmi_handler(void)
{
/* 직접 처리하면 데드락 위험 → irq_work로 위임 */
irq_work_queue(&my_work);
}
/* Lazy 모드: softirq 시점에 실행 (지연 허용) */
static struct irq_work lazy_work = IRQ_WORK_INIT_LAZY(my_lazy_handler);
irq_work_queue(&lazy_work); /* 다음 tick/softirq에서 실행 */
주요 사용 시나리오
| 사용처 | 이유 | 코드 경로 |
|---|---|---|
printk (NMI 컨텍스트) | NMI에서 직접 콘솔 출력 불가 (락 충돌) | printk_deferred() → irq_work_queue() |
| perf 이벤트 출력 | NMI 기반 PMU 오버플로에서 링 버퍼(Ring Buffer) 기록 | perf_output_end() → wakeup via irq_work |
| RCU 콜백 처리 | RCU grace period 알림을 안전하게 전파 | rcu_report_qs_rdp() |
| tick 브로드캐스트 | 다른 CPU의 tick 재개 요청 | tick_broadcast_exit() |
| ftrace 출력 | 트레이스 버퍼 플러시(Flush)를 안전한 컨텍스트로 위임 | trace_wake_up() |
다른 BH 메커니즘과의 차이
| 특성 | irq_work | softirq | tasklet | workqueue |
|---|---|---|---|---|
| NMI 안전 | 가능 | 불가 | 불가 | 불가 |
| 실행 컨텍스트 | hardirq | softirq | softirq | process |
| 슬립 가능 | 불가 | 불가 | 불가 | 가능 |
| 실행 시점 | 다음 hardirq 반환 또는 IPI | 인터럽트 반환 시 | 인터럽트 반환 시 | 스케줄러(Scheduler) 결정 |
| CPU 지정 | 큐잉된 CPU 또는 원격 CPU | raise된 CPU | 스케줄된 CPU | worker pool |
irq_work는 hardirq 컨텍스트에서 실행되므로 슬립 불가하며, 최소한의 작업만 수행해야 합니다. 대량의 데이터 처리가 필요하면 irq_work에서 workqueue로 위임하는 2단계 패턴을 사용하세요.
Per-CPU Bottom Half 패턴
리눅스 커널의 Bottom Half 처리는 근본적으로 per-CPU 모델을 기반으로 합니다. softirq는 각 CPU에서 독립적으로 실행되고, workqueue의 bound 모드도 per-CPU worker pool을 사용합니다. 이 특성을 올바르게 활용하면 캐시 친화적이고 잠금 없는 고성능 BH 처리가 가능합니다.
per-CPU softirq 처리의 의미
raise_softirq()가 호출된 CPU에서만 해당 softirq가 실행됩니다. 같은 유형의 softirq가 다른 CPU에서 동시에 실행될 수 있으므로, 공유 데이터에는 반드시 동기화가 필요합니다.
/* per-CPU softirq 처리 구조 (kernel/softirq.c) */
DEFINE_PER_CPU(__u32, __softirq_pending);
void __do_softirq(void)
{
__u32 pending = local_softirq_pending(); /* 현재 CPU의 pending 비트 */
int softirq_bit;
while ((softirq_bit = ffs(pending))) {
struct softirq_action *h = softirq_vec + softirq_bit - 1;
h->action(h); /* 해당 softirq 핸들러 실행 */
pending >>= softirq_bit;
}
}
per-CPU workqueue 패턴
/* Bound workqueue: per-CPU worker pool 사용 */
struct workqueue_struct *bound_wq;
bound_wq = alloc_workqueue("my_bound_wq", 0, 0);
/* WQ_UNBOUND가 없으면 per-CPU (bound) workqueue */
/* 현재 CPU의 worker에서 실행 */
queue_work(bound_wq, &my_work);
/* vs. Unbound workqueue: 시스템이 CPU 선택 */
struct workqueue_struct *unbound_wq;
unbound_wq = alloc_workqueue("my_unbound_wq", WQ_UNBOUND, 0);
/* 어떤 CPU에서든 실행될 수 있음 — NUMA 인식 배치 */
local_bh_disable/enable의 실제 동작
/* local_bh_disable(): preempt_count에 SOFTIRQ_DISABLE_OFFSET 추가 */
static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
/* preempt_count 비트 레이아웃:
* [0..7] : preempt count (선점 깊이)
* [8..15] : softirq count (BH 비활성 깊이)
* [16..19] : hardirq count (하드웨어 인터럽트 깊이)
* [20] : NMI
*/
/* 사용 패턴: per-CPU 데이터 보호 */
local_bh_disable();
/* 이 구간에서는 softirq/tasklet이 현재 CPU에서 실행되지 않음 */
/* per-CPU 데이터 안전하게 접근 가능 */
this_cpu_inc(my_per_cpu_counter);
local_bh_enable();
/* enable 시점에 pending softirq가 있으면 즉시 실행 */
per-CPU 안전한 BH 패턴
/* 패턴 1: per-CPU 통계 수집 (락 없음) */
DEFINE_PER_CPU(struct my_stats, pcpu_stats);
static void my_softirq_handler(struct softirq_action *a)
{
struct my_stats *stats = this_cpu_ptr(&pcpu_stats);
stats->packets++; /* 안전: softirq는 per-CPU 실행 */
stats->bytes += len;
}
/* 패턴 2: per-CPU 큐 + BH 처리 */
DEFINE_PER_CPU(struct list_head, pcpu_queue);
DEFINE_PER_CPU(spinlock_t, pcpu_lock);
/* hardirq에서: 큐에 추가 */
void irq_enqueue(struct work_item *item)
{
spinlock_t *lock = this_cpu_ptr(&pcpu_lock);
spin_lock(lock);
list_add_tail(&item->node, this_cpu_ptr(&pcpu_queue));
spin_unlock(lock);
raise_softirq(MY_SOFTIRQ);
}
/* softirq에서: 큐 처리 */
void my_softirq_action(struct softirq_action *a)
{
LIST_HEAD(local_list);
spinlock_t *lock = this_cpu_ptr(&pcpu_lock);
spin_lock(lock);
list_splice_init(this_cpu_ptr(&pcpu_queue), &local_list);
spin_unlock(lock);
/* local_list 처리 — 락 없이 안전 */
process_items(&local_list);
}
this_cpu_ptr()로 접근하는 per-CPU 데이터는 local_bh_disable()만으로 충분히 보호됩니다. 다른 CPU의 데이터에 접근할 때만 명시적 잠금이 필요합니다.
Bottom Half 중첩과 재진입
Bottom Half 메커니즘은 각각 다른 중첩(nesting)과 재진입(reentrancy) 규칙을 가집니다. 이를 정확히 이해하지 못하면 데이터 손상이나 데드락이 발생할 수 있습니다.
메커니즘별 재진입 규칙
| 메커니즘 | 같은 CPU 재진입 | 다른 CPU 동시 실행 | 중복 큐잉 |
|---|---|---|---|
| softirq | 불가 (같은 호출에서 재귀 금지) | 가능 (같은 타입 동시 실행) | pending 비트 방식 (멱등) |
| tasklet | 불가 | 불가 (TASKLET_STATE_RUN 보호) | 스케줄 중이면 무시 |
| workqueue | 불가 | 불가 (같은 work 동시 실행 방지) | WORK_STRUCT_PENDING 보호 |
| threaded IRQ | 불가 | 불가 (스레드당 1개) | 해당 없음 |
| irq_work | 불가 (IRQ_WORK_BUSY) | 불가 (같은 work) | busy이면 무시 |
softirq 재진입 상세
/* softirq의 핵심 특성: 같은 타입이 다른 CPU에서 동시 실행 가능 */
/* CPU 0 */ /* CPU 1 */
net_rx_action() net_rx_action()
|-> process_backlog() |-> process_backlog()
|-> /* 공유 데이터 접근 시 */ |-> /* 동시에 실행 중! */
|-> spin_lock(&shared_lock) |-> spin_lock(&shared_lock)
|-> /* ... */ |-> /* 대기 */
/* 반면 같은 CPU에서의 softirq 중첩은 불가:
* __do_softirq()는 in_serving_softirq() 체크로 재귀 방지
* 단, __do_softirq() 실행 중 새 softirq가 raise되면
* MAX_SOFTIRQ_RESTART(10)까지 반복 후 ksoftirqd로 위임 */
preempt_count와 BH 깊이 추적
preempt_count는 32비트 정수로, 현재 실행 컨텍스트를 비트 필드로 추적합니다. 이 값을 통해 커널은 현재 hardirq, softirq, 또는 선점 비활성 상태인지를 판단합니다.
/* 컨텍스트 확인 매크로 */
in_irq() /* hardirq 컨텍스트인가? */
in_softirq() /* softirq 컨텍스트인가? (BH disabled 포함) */
in_serving_softirq() /* 실제 softirq 핸들러 실행 중인가? */
in_interrupt() /* hardirq 또는 softirq 컨텍스트인가? */
in_task() /* 프로세스 컨텍스트인가? */
in_nmi() /* NMI 컨텍스트인가? */
/* BH disable 중첩 카운팅 */
local_bh_disable(); /* softirq_count += SOFTIRQ_DISABLE_OFFSET */
local_bh_disable(); /* 중첩 가능: softirq_count 증가 */
/* ... */
local_bh_enable(); /* softirq_count 감소 */
local_bh_enable(); /* 0이 되면 pending softirq 실행 */
중첩 시나리오별 동작 차이
| 시나리오 | 동작 | 결과 |
|---|---|---|
| hardirq에서 softirq raise | pending 비트 설정, hardirq 반환 시 실행 | 정상 동작 |
| softirq에서 같은 softirq raise | pending 비트 설정, 현재 라운드 후 재실행 | MAX_SOFTIRQ_RESTART까지 반복 |
| softirq에서 tasklet_schedule | TASKLET_SOFTIRQ에 추가 | 현재 softirq 라운드 후 실행 |
| tasklet에서 같은 tasklet schedule | 이미 RUN 상태이므로 다음 라운드로 연기 | 안전 (직렬화 보장) |
| workqueue work에서 같은 work 큐잉 | PENDING 플래그로 거부 | 무시됨 (중복 방지) |
| process 컨텍스트에서 local_bh_disable | preempt_count 증가, softirq 억제 | enable 시 pending 처리 |
Bottom Half에서의 메모리 할당
Bottom Half 컨텍스트에서의 메모리 할당은 실행 환경에 따라 사용 가능한 GFP 플래그가 제한됩니다. 잘못된 플래그 사용은 커널 경고(BUG)나 데드락을 일으킵니다.
컨텍스트별 허용 GFP 플래그
| 컨텍스트 | GFP_ATOMIC | GFP_KERNEL | GFP_NOIO | __GFP_DIRECT_RECLAIM | 슬립 |
|---|---|---|---|---|---|
| hardirq | 가능 | 불가 | 불가 | 불가 | 불가 |
| softirq | 가능 | 불가 | 불가 | 불가 | 불가 |
| tasklet | 가능 | 불가 | 불가 | 불가 | 불가 |
| WQ_BH | 가능 | 불가 | 불가 | 불가 | 불가 |
| workqueue | 가능 | 가능 | 가능 | 가능 | 가능 |
| threaded IRQ | 가능 | 가능 | 가능 | 가능 | 가능 |
GFP_ATOMIC 사용 패턴
/* softirq/tasklet에서의 메모리 할당 */
static void my_softirq_handler(struct softirq_action *a)
{
struct my_buffer *buf;
/* GFP_ATOMIC: 슬립 없이 즉시 반환 (실패 가능) */
buf = kmalloc(sizeof(*buf), GFP_ATOMIC);
if (!buf) {
/* 할당 실패 처리 — 반드시 대비해야 함! */
pr_warn("BH alloc failed, deferring\n");
schedule_work(&retry_work); /* workqueue로 재시도 위임 */
return;
}
process_with_buffer(buf);
kfree(buf);
}
/* Slab 캐시에서의 BH 할당 */
struct kmem_cache *my_cache;
my_cache = kmem_cache_create("my_cache", sizeof(struct my_obj),
0, SLAB_HWCACHE_ALIGN, NULL);
/* softirq에서 */
obj = kmem_cache_alloc(my_cache, GFP_ATOMIC);
/* ... 사용 ... */
kmem_cache_free(my_cache, obj);
메모리 부족 시 BH 복구 전략
/* 전략 1: 사전 할당 풀 (mempool) */
struct mempool_s *pool;
pool = mempool_create_slab_pool(16, my_cache);
/* 최소 16개 보장 — GFP_ATOMIC에서도 안전 */
obj = mempool_alloc(pool, GFP_ATOMIC); /* 거의 항상 성공 */
/* 전략 2: per-CPU 프리리스트 */
DEFINE_PER_CPU(struct list_head, free_list);
/* 프로세스 컨텍스트에서 미리 채움 */
void refill_free_list(void)
{
struct my_obj *obj;
int cpu;
for_each_possible_cpu(cpu) {
obj = kmalloc(sizeof(*obj), GFP_KERNEL);
list_add(&obj->node, per_cpu_ptr(&free_list, cpu));
}
}
/* softirq에서: 할당 없이 사용 */
obj = list_first_entry(this_cpu_ptr(&free_list), struct my_obj, node);
list_del(&obj->node);
/* 전략 3: 2단계 처리 (할당 실패 시 workqueue 위임) */
if (!buf) {
/* 데이터를 임시 버퍼에 저장하고 workqueue에서 재처리 */
queue_work(system_wq, &fallback_work);
}
mempool 또는 per-CPU 프리리스트를 사용하세요. GFP_ATOMIC 할당 실패는 시스템 메모리가 부족할 때 자주 발생하며, 네트워크 패킷 폭주 상황에서 특히 위험합니다.
실시간 시스템을 위한 Bottom Half 전략
PREEMPT_RT 커널에서는 Bottom Half 메커니즘이 크게 변환됩니다. softirq와 tasklet이 강제로 스레드화되어 결정론적 스케줄링이 가능해지지만, 지연 특성과 우선순위 관리를 정확히 이해해야 합니다.
PREEMPT_RT 변환 요약
| 메커니즘 | 일반 커널 | PREEMPT_RT 커널 | RT 영향 |
|---|---|---|---|
| softirq | 인터럽트 반환 시 실행 | ksoftirqd 스레드에서 실행 | 선점 가능, 우선순위 제어 |
| tasklet | softirq 컨텍스트 | 전용 스레드로 변환 | 개별 우선순위 설정 |
| local_bh_disable | softirq 실행 억제 | per-CPU 락으로 변환 | 우선순위 역전 가능 |
| spin_lock_bh | spinlock + BH disable | rt_mutex + BH 동기화 | 슬립 가능, PI 지원 |
| workqueue | 변경 없음 | 변경 없음 | 이미 프로세스 컨텍스트 |
| threaded IRQ | 변경 없음 | 변경 없음 | RT 네이티브 |
IRQ 스레드 우선순위 설정
# IRQ 스레드 목록 확인
ps -eo pid,cls,rtprio,ni,comm | grep irq/
# 특정 IRQ 스레드 우선순위 변경 (SCHED_FIFO)
chrt -f -p 90 $(pgrep -f "irq/42-my_dev")
# /proc 인터페이스로 IRQ 친화도 설정
echo 2 > /proc/irq/42/smp_affinity # CPU 1에 고정
# 런타임에 IRQ 스레드 우선순위 확인
cat /proc/42/sched | grep policy
# policy: SCHED_FIFO, prio: 90
# ksoftirqd 우선순위 변경 (주의 필요)
chrt -f -p 50 $(pgrep "ksoftirqd/0")
# 네트워킹/타이머 softirq에 영향 — 낮추면 네트워크 지연 증가
isolcpus + nohz_full과 BH 상호작용
# 커널 부팅 파라미터 예시
isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3
/* isolcpus 영향:
* - 격리된 CPU에서는 unbound workqueue worker가 실행되지 않음
* - 그러나 bound workqueue와 softirq는 해당 CPU에서 여전히 실행 가능
* - IRQ를 해당 CPU로 라우팅하면 BH도 그 CPU에서 실행
*/
/* nohz_full 영향:
* - tick 없는 실행 (최소 인터럽트)
* - 단 softirq가 raise되면 tick이 재개됨
* - timer softirq는 다른 CPU에서 원격 처리 가능 (6.x)
*/
/* rcu_nocbs 영향:
* - RCU 콜백을 전용 스레드 (rcuog/rcuop)로 오프로드
* - 격리 CPU의 BH 부하 대폭 감소
*/
/* RT 워크로드 권장 CPU 배치:
* CPU 0-1: 일반 작업 + softirq/ksoftirqd + kworker
* CPU 2-3: RT 작업 전용 (isolcpus)
* - IRQ 친화도로 인터럽트 배제
* - nohz_full로 tick 제거
* - rcu_nocbs로 RCU 오프로드
*/
실시간 워크로드별 BH 선택 가이드
| 워크로드 | 지연 요구 | 권장 BH | 우선순위 가이드 |
|---|---|---|---|
| 모션 컨트롤러 | < 50us | threaded IRQ | SCHED_FIFO 95+ |
| 오디오 처리 | < 1ms | threaded IRQ | SCHED_FIFO 80-90 |
| 네트워크 패킷 처리 | < 5ms | softirq (RT에서 스레드화) | ksoftirqd SCHED_FIFO 50 |
| 디스크 I/O 완료 | < 10ms | workqueue | kworker 기본 (SCHED_OTHER) |
| 로그/모니터링 | 제한 없음 | workqueue (WQ_UNBOUND) | SCHED_OTHER, nice 10+ |
일반적인 실수와 올바른 패턴
Bottom Half 사용 시 자주 발생하는 실수와 올바른 접근 방법을 비교합니다.
❌ 실수 1: softirq/tasklet에서 슬립 시도
/* 잘못된 예: softirq에서 블로킹 함수 호출 */
static void my_softirq_action(struct softirq_action *h)
{
struct data *d;
mutex_lock(&my_mutex); /* ❌ softirq는 atomic context! */
d = process_data();
mutex_unlock(&my_mutex);
}
/* 잘못된 예: tasklet에서 msleep */
static void my_tasklet_func(struct tasklet_struct *t)
{
msleep(100); /* ❌ atomic context에서 슬립 불가 */
process_data();
}
/* 올바른 예: workqueue 사용 */
static void my_work_func(struct work_struct *work)
{
struct data *d;
mutex_lock(&my_mutex); /* ✓ workqueue는 프로세스 컨텍스트 */
d = process_data();
mutex_unlock(&my_mutex);
msleep(100); /* ✓ 슬립 가능 */
}
❌ 실수 2: tasklet 재진입 가정
/* 잘못된 예: tasklet이 동시 실행될 것으로 가정 */
static atomic_t counter = ATOMIC_INIT(0);
static void my_tasklet(struct tasklet_struct *t)
{
/* ❌ 불필요한 atomic 연산 - 같은 tasklet은 직렬화됨 */
atomic_inc(&counter);
}
/* 올바른 예: tasklet 직렬화 보장 활용 */
struct my_driver_data {
struct tasklet_struct tasklet;
int counter; /* ✓ atomic 불필요 */
};
static void my_tasklet(struct tasklet_struct *t)
{
struct my_driver_data *data = from_tasklet(data, t, tasklet);
data->counter++; /* ✓ 같은 tasklet은 직렬화되므로 안전 */
}
❌ 실수 3: workqueue를 atomic context로 가정
/* 잘못된 예: workqueue에서 spinlock_irqsave 남용 */
static void my_work(struct work_struct *work)
{
unsigned long flags;
spin_lock_irqsave(&my_lock, flags); /* ❌ 불필요 - 이미 인터럽트 활성화 */
process_data();
spin_unlock_irqrestore(&my_lock, flags);
}
/* 올바른 예: 적절한 락 사용 */
static void my_work(struct work_struct *work)
{
spin_lock(&my_lock); /* ✓ workqueue는 인터럽트 활성화 상태 */
process_data();
spin_unlock(&my_lock);
/* 또는 mutex 사용 가능 */
mutex_lock(&my_mutex); /* ✓ 슬립 가능 */
slow_operation();
mutex_unlock(&my_mutex);
}
❌ 실수 4: raise_softirq() 사용 시 인터럽트 상태 오인
/* 잘못된 예: 인터럽트 활성화 상태에서 raise_softirq_irqoff 호출 */
void my_function(void)
{
local_irq_disable();
do_something();
local_irq_enable();
raise_softirq_irqoff(NET_RX_SOFTIRQ); /* ❌ IRQ 활성화됨! */
}
/* 올바른 예 1: 인터럽트 비활성화 상태 확인 */
void my_function(void)
{
unsigned long flags;
local_irq_save(flags);
do_something();
raise_softirq_irqoff(NET_RX_SOFTIRQ); /* ✓ IRQ 비활성화 상태 */
local_irq_restore(flags);
}
/* 올바른 예 2: 안전한 raise_softirq 사용 */
void my_function(void)
{
do_something();
raise_softirq(NET_RX_SOFTIRQ); /* ✓ 내부에서 IRQ 제어 */
}
❌ 실수 5: workqueue flush 시 데드락
/* 잘못된 예: work 내부에서 자신을 flush */
static void my_work(struct work_struct *work)
{
do_something();
flush_work(work); /* ❌ 데드락! 자기 자신을 기다림 */
}
/* 잘못된 예: 같은 workqueue에서 flush_workqueue */
static void my_work(struct work_struct *work)
{
flush_workqueue(my_wq); /* ❌ 같은 wq에서 실행 중 */
}
/* 올바른 예: 별도 컨텍스트에서 flush */
void cleanup_driver(void)
{
cancel_work_sync(&my_work); /* ✓ 외부에서 취소 대기 */
flush_workqueue(my_wq); /* ✓ 모든 work 완료 대기 */
}
✅ 모범 사례 체크리스트
| 항목 | 설명 | 검증 방법 |
|---|---|---|
| 컨텍스트 확인 | atomic vs process context 구분 | in_interrupt(), in_atomic() |
| 슬립 금지 | softirq/tasklet에서 슬립 함수 호출 금지 | might_sleep() 경고 확인 |
| 최소 실행 시간 | softirq는 짧게, 오래 걸리면 workqueue로 위임 | ftrace로 실행 시간 측정 |
| 직렬화 보장 | tasklet의 직렬화 특성 활용 | 불필요한 락 제거 |
| 적절한 플래그 | WQ_UNBOUND, WQ_HIGHPRI 등 상황에 맞게 | 워크로드 특성 분석 |
| 정리 순서 | cancel → flush → destroy 순서 | 데드락/메모리 누수 방지 |
드라이버 개발자 BH 선택 체크리스트
새로운 드라이버를 작성하거나 기존 드라이버를 리팩토링할 때, 아래 플로우차트와 체크리스트를 따라 최적의 Bottom Half 메커니즘을 선택하세요.
빠른 체크리스트
| 질문 | Yes | No |
|---|---|---|
| BH에서 슬립(mutex, I/O 등)이 필요한가? | workqueue 또는 threaded IRQ | 다음 질문으로 |
| IRQ 핸들러의 하반부(Bottom Half)인가? | threaded IRQ (최우선 권장) | workqueue |
| 초저지연(<1us)이 필수인가? | softirq (커널 전용) 또는 WQ_BH | 다음 질문으로 |
| PREEMPT_RT 지원이 필요한가? | threaded IRQ | WQ_BH |
| 레거시 tasklet 코드인가? | WQ_BH로 마이그레이션 | — |
피해야 할 안티 패턴
/* 안티 패턴 1: 새 코드에서 tasklet 사용 */
tasklet_init(&my_tasklet, fn, data); /* 나쁨: deprecated */
/* 올바른 대안: */
INIT_WORK(&my_work, fn);
queue_work(system_bh_wq, &my_work); /* 좋음: WQ_BH */
/* 안티 패턴 2: softirq에서 GFP_KERNEL 사용 */
ptr = kmalloc(size, GFP_KERNEL); /* BUG: atomic context에서 슬립 */
/* 올바른 대안: */
ptr = kmalloc(size, GFP_ATOMIC); /* 좋음: non-blocking */
/* 안티 패턴 3: workqueue에서 초저지연 요구 */
queue_work(system_wq, &latency_critical_work); /* 나쁨: 스케줄링 지연 */
/* 올바른 대안: */
request_threaded_irq(irq, handler, thread_fn, ...); /* 전용 스레드 */
/* 안티 패턴 4: BH에서 장시간 실행 */
void my_softirq(struct softirq_action *a)
{
while (has_work()) process(); /* 나쁨: 무한 루프 가능 */
}
/* 올바른 대안: 예산(budget) 기반 처리 */
void my_softirq(struct softirq_action *a)
{
int budget = 64;
while (has_work() && budget-- > 0)
process();
if (has_work())
raise_softirq(MY_SOFTIRQ); /* 나머지는 다음 라운드 */
}
참고자료
커널 공식 문서
- Concurrency Managed Workqueue (cmwq) — CMWQ 아키텍처, worker pool, 플래그 등 공식 문서입니다
- IRQ Domain — 인터럽트 도메인 매핑과 계층 구조를 설명합니다
- Linux generic IRQ handling — 리눅스 범용 인터럽트 처리 아키텍처 문서입니다
- What is RCU? — softirq 컨텍스트에서 사용되는 RCU 메커니즘의 공식 설명입니다
- Unreliable Guide To Locking — Bottom Half 컨텍스트 간 락킹 규칙을 다룹니다
- Reducing OS jitter due to per-cpu kthreads — ksoftirqd, kworker 등 per-CPU 커널 스레드의 CPU 격리 방법을 설명합니다
LWN.net 기사
- The new workqueue API (2012) — CMWQ(Concurrency Managed Workqueue)의 도입 배경과 설계를 설명하는 기사입니다
- Deprecating tasklets (2024) — tasklet 폐기 논의와 WQ_BH 대안의 등장을 다루는 기사입니다
- Modernizing the tasklet API (2020) — tasklet API 현대화 시도와 한계를 분석합니다
- Moving interrupts to threads (2007) — threaded IRQ의 초기 설계와 RT 커널에서의 동기를 설명합니다
- Threaded interrupts in mainline (2008) — threaded IRQ가 메인라인에 합류한 과정을 다룹니다
- Completing the conversion to BH-disabled workqueues (2019) — softirq에서 workqueue로의 전환 흐름을 정리합니다
- A bottom-half rework (2020) — Bottom Half 전체 구조 재설계 논의를 소개합니다
- Softirqs and timing (2022) — softirq 처리 타이밍과 ksoftirqd로의 위임 조건을 분석합니다
커널 소스 코드
kernel/softirq.c— softirq 핵심 구현,__do_softirq(),raise_softirq()등이 정의되어 있습니다kernel/workqueue.c— CMWQ 전체 구현, worker pool 관리,WQ_BH지원 등을 포함합니다kernel/irq/manage.c—request_threaded_irq(), IRQ 스레드 생성 등 인터럽트 관리 코드입니다include/linux/interrupt.h— softirq, tasklet, threaded IRQ 관련 주요 선언과 매크로가 정의되어 있습니다include/linux/workqueue.h— workqueue API 선언,WQ_BH,WQ_HIGHPRI등 플래그 정의입니다kernel/irq/irqdesc.c— IRQ 디스크립터 관리와 per-CPU IRQ 처리 구조입니다kernel/time/timer.c— 타이머 softirq(TIMER_SOFTIRQ) 구현을 포함합니다net/core/dev.c—NET_TX_SOFTIRQ/NET_RX_SOFTIRQ처리와 NAPI 통합 코드입니다block/blk-mq.c—BLOCK_SOFTIRQ처리와 블록 I/O 완료 경로입니다
서적
- Robert Love, Linux Kernel Development, 3rd Edition — Chapter 8 "Bottom Halves and Deferring Work"에서 softirq, tasklet, workqueue의 설계와 사용법을 체계적으로 설명합니다
- Daniel P. Bovet & Marco Cesati, Understanding the Linux Kernel, 3rd Edition — Chapter 4 "Interrupts and Exceptions"에서 Bottom Half 메커니즘의 내부 구현을 상세히 다룹니다
- Jonathan Corbet, Alessandro Rubini & Greg Kroah-Hartman, Linux Device Drivers, 3rd Edition — Chapter 7 "Time, Delays, and Deferred Work"에서 드라이버 관점의 Bottom Half 활용을 설명합니다
- Wolfgang Mauerer, Professional Linux Kernel Architecture — Chapter 14 "Kernel Activities"에서 softirq와 tasklet의 구현 세부 사항을 분석합니다
외부 자료
- Approaching Real-Time Linux (OLS 2006) — PREEMPT_RT에서 Bottom Half 스레드화의 초기 설계를 다룬 논문입니다
- Kernel Recipes 2019: Workqueue — Subtleties and improvements — Tejun Heo의 workqueue 개선 발표로, CMWQ 내부 동작을 이해하는 데 도움이 됩니다
- Linux Insides: Softirq, Tasklets and Workqueues — softirq 처리 흐름을 소스 코드 레벨에서 상세히 추적합니다
- Locking lessons: spin_lock_bh() — Bottom Half 컨텍스트에서의 스핀락 사용 규칙과
spin_lock_bh()/spin_unlock_bh()의 의미를 설명합니다 - Thomas Gleixner's Blog — RT 커널 메인테이너로서 threaded IRQ와 Bottom Half 스레드화에 대한 기술적 인사이트를 공유합니다
관련 문서
Bottom Half와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.