전체 흐름
softirq는 종류별 작업을 현재 CPU의 pending 비트로 표시하고, 허용되는 문맥에서 해당 vector action을 처리한다. IRQ handler가 항상 곧바로 softirq를 실행하는 일렬 경로는 아니다.
두 번째와 세 번째 칸은 대체 진입 경로다. kernel/softirq.c:760; kernel/softirq.c:713.
| 상황 | pending 이후 | 항상 성립하지 않는 것 |
|---|---|---|
| hard IRQ에서 raise | IRQ 복귀 시 중첩·pending을 확인하고 invoke_softirq를 시도 | 그 IRQ가 무조건 같은 스택에서 즉시 action 실행 |
| 일반 문맥에서 raise | 직접 wakeup_softirqd가 가능; BH 재활성화 경로도 별개 | raise_softirq 자체가 action 호출 |
| ksoftirqd | 남은 pending이 있을 때 thread가 drain 시도 | 모든 softirq가 처음부터 ksoftirqd에서 실행 |
근거: kernel/softirq.c:760, kernel/softirq.c:713, kernel/softirq.c:1055. CONFIG_PREEMPT_RT와 forced IRQ threading 설정은 호출 문맥을 바꿀 수 있다.
소스 코드 위치
먼저 확인할 Linux 6.18.37 LTS 소스 파일
Linux 6.18.37 LTS: kernel/softirq.c
Linux 6.18.37 LTS: include/linux/interrupt.h
Linux 6.18.37 LTS: net/core/dev.c
설명: 진입 함수가 있는 파일과 구조체 정의, 아키텍처별 연결 코드, 콜백 구현을 함께 확인한다. 여러 파일을 대조해야 호출 관계와 각 코드의 역할을 알 수 있다.
대표 코드
handle_softirqs(bool ksirqd) · pending 처리 루프
static void handle_softirqs(bool ksirqd)
{
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;
int softirq_bit;
/*
* Mask out PF_MEMALLOC as the current task context is borrowed for the
* softirq. A softirq handled, such as network RX, might set PF_MEMALLOC
* again if the socket is related to swapping.
*/
current->flags &= ~PF_MEMALLOC;
pending = local_softirq_pending();
softirq_handle_begin();
in_hardirq = lockdep_softirq_start();
account_softirq_enter(current);
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);
local_irq_enable();
h = softirq_vec;
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1;
vec_nr = h - softirq_vec;
prev_count = preempt_count();
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
h->action();
trace_softirq_exit(vec_nr);
if (unlikely(prev_count != preempt_count())) {
pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
vec_nr, softirq_to_name[vec_nr], h->action,
prev_count, preempt_count());
preempt_count_set(prev_count);
}
h++;
pending >>= softirq_bit;
}
if (!IS_ENABLED(CONFIG_PREEMPT_RT) && ksirqd)
rcu_softirq_qs();
local_irq_disable();
pending = local_softirq_pending();
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd();
}
account_softirq_exit(current);
lockdep_softirq_end(in_hardirq);
softirq_handle_end();
current_restore_flags(old_flags, PF_MEMALLOC);
}
확인 사항: v6.18.37은 __do_softirq()가 handle_softirqs(false)를 부르고, ksoftirqd는 handle_softirqs(true)를 부른다. 같은 action 루프에 진입해도 호출 문맥이 다르다.
raise_softirq
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
/*
* If we're in an interrupt or softirq, we're done
* (this also catches softirq-disabled code). We will
* actually run the softirq once we return from
* the irq or softirq.
*
* Otherwise we wake up ksoftirqd to make sure we
* schedule the softirq soon.
*/
if (!in_interrupt() && should_wake_ksoftirqd())
wakeup_softirqd();
}
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
void __raise_softirq_irqoff(unsigned int nr)
{
lockdep_assert_irqs_disabled();
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
확인 사항: raise_softirq()는 IRQ 상태를 저장·복원한다. __raise_softirq_irqoff()는 종류별 pending bit를 세우고, raise_softirq_irqoff()는 실행 문맥에 따라 ksoftirqd를 깨울 수 있다.
함수별 분석
raise_softirq()는 IRQ 상태를 보존하고 pending 비트를 세운다. handle_softirqs()는 pending을 읽고 지운 뒤 종류별 action을 호출한다. producer와 consumer의 역할을 거꾸로 읽지 않는다.
__raise_softirq_irqoff()가 1UL << nr을 이 CPU의 pending mask에 OR.!in_interrupt()와 pending 조건일 때 invoke_softirq() 진입.실행 방식은 CONFIG_PREEMPT_RT·force_irqthreads·스택 설정에 좌우된다. kernel/softirq.c:713; kernel/softirq.c:1055.
| 함수 | 상태 변경 | 아닌 것 |
|---|---|---|
| raise_softirq_irqoff | pending bit OR, 문맥에 따라 ksoftirqd 깨움 | vector action 자체 실행 |
| handle_softirqs(bool ksirqd) | pending snapshot clear 뒤 action dispatch, 새 pending 재검사 | 새 pending의 유일한 producer |
| invoke_softirq | IRQ 복귀에서 직접 처리 또는 daemon wakeup 선택 | 모든 하드 IRQ의 무조건 후속 단계 |
구조체와 필드
pending 비트마스크는 처리할 softirq 종류를 나타낸다. 작업 데이터 전체를 담는 큐가 아니며, vector의 action 포인터 및 실행 문맥과 구분한다.
or_softirq_pending(1UL << nr); 새 작업 종류를 표시.action 등록은 open_softirq(), 종류별 실행은 h->action() 형태다. kernel/softirq.c:579.
| 요소 | 역할 | 관찰 포인트 |
|---|---|---|
| pending mask | 현재 CPU의 대기 중인 종류 | raise에서 OR, drain에서 snapshot과 clear |
| softirq_vec | 종류 번호 → action 연결 | open_softirq가 등록; action은 drain이 호출 |
| BH 상태 | 해당 CPU의 bottom-half 실행 제약 | local_bh_disable은 local_irq_disable과 별개 |
| ksoftirqd | 지연된 처리에 쓰는 CPU별 kernel thread | 실행 문맥을 trace로 구분 |
실행 단계와 상태 변화
처리 시작 때 pending snapshot을 읽고 mask를 비운다. callback이 실행되는 동안 같은 종류가 다시 raise될 수 있으므로 새 pending을 별도로 재검사한다.
- snapshotlocal_softirq_pending을 읽음
- clear → actionpending mask를 0으로, IRQ enable 뒤 vector별 action
- 새 pendingIRQ disable 후 다시 읽어 새 요청 확인
- restart / wake시간·resched·재시도 한계를 보고 반복 또는 ksoftirqd wake
2ms/jiffies와 MAX_SOFTIRQ_RESTART=10은 공정성을 위한 반복 제한이며 단일 callback의 엄격한 실행시간 보증은 아니다. kernel/softirq.c:579.
| 조건 | 다음 동작 | 주의 |
|---|---|---|
| 새 pending 없음 | 이번 drain 종료 | 최초 snapshot만 처리한 것과 구분 |
| 새 pending + time_before + !need_resched + 재시도 남음 | restart로 다시 snapshot 처리 | 재시도 횟수와 jiffies 조건을 함께 검사 |
| 새 pending + 한계 도달/스케줄 필요 | wakeup_softirqd | 그 자체가 packet drop이나 작업 소실은 아님 |
| 중첩 중 IRQ 복귀 | 즉시 drain 조건 불충족 | pending을 남겨 가능한 문맥에서 처리 |
v6.18.37 근거: kernel/softirq.c:579; kernel/softirq.c:713. RT/forced-thread 구성이면 context 선택이 달라진다.
불변 조건과 실패 사례
softirq의 핵심 객체가 외부에 공개된 뒤에는 마지막 참조가 사라질 때까지 해제 경로가 callback, timer, IRQ, worker와 경합하지 않아야 한다.
다른 CPU나 하위 계층이 변경된 필드를 읽는다면 잠금과 메모리 배리어가 접근 순서를 어떻게 보장하는지 확인한다. 참조 횟수와 RCU로 관리하는 객체 수명도 별도로 확인한다.
중간 단계에서 실패하면 이미 확보한 자원을 의존 관계에 맞춰 해제해야 한다. goto 레이블이 많다면 각 분기에 도달했을 때 확보한 자원을 표로 정리한다.
임베디드 보드를 처음 구동할 때는 패닉 대신 로그 중단, 시간 초과, 장치 초기화 지연, 과도한 인터럽트 같은 증상으로 문제가 드러날 수 있다.
softirq가 항상 ksoftirqd에서 돈다고 착각함
local_bh_disable을 interrupt disable로 오해함
budget 제한을 packet drop 원인과 혼동함
계측과 검증
softirq_raise와 softirq_entry/exit는 요청과 실제 action 실행을 분리해 보여준다. 같은 CPU의 사건 순서와 실행 task를 함께 확인한다.
| 관찰 | 보이는 것 | 단독으로 알 수 없는 것 |
|---|---|---|
| irq:softirq_raise | 종류별 pending 요청 시점 | action이 곧바로 실행됐는지 |
| irq:softirq_entry / exit | vector callback 실행의 시작과 끝 | 그 전에 어느 하드 IRQ가 유일한 원인인지 |
| /proc/softirqs | CPU별 종류별 실행 누적 횟수 | 개별 지연 또는 drop 원인 |
| function graph / perf | __do_softirq·handle_softirqs·ksoftirqd 호출 문맥 | pending mask의 매 변화 |
tracepoint 호출 위치: kernel/softirq.c:760, kernel/softirq.c:579. 실제 이벤트와 함수 추적 가용성은 설정에 따른다.
# tracefs가 이미 마운트되어 있고 instances 기능을 지원할 때만 실행
sh <<'SH'
tr=/sys/kernel/tracing
[ -d "$tr/instances" ] || { echo 'tracefs instances unavailable' >&2; exit 1; }
instance="$tr/instances/dujin_softirq_$$"
mkdir "$instance" || exit 1
cleanup() {
for event in softirq_raise softirq_entry softirq_exit; do
enable="$instance/events/irq/$event/enable"
[ -e "$enable" ] && printf '0\n' > "$enable"
done
rmdir "$instance"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
for event in softirq_raise softirq_entry softirq_exit; do
enable="$instance/events/irq/$event/enable"
[ -e "$enable" ] || { echo "missing irq:$event" >&2; exit 1; }
printf '1\n' > "$enable" || exit 1
done
cat "$instance/trace_pipe" # Ctrl-C로 종료하면 만든 인스턴스를 정리
SH추가 확인 사항
- softirq 의 state machine을 네 단계로 줄였을 때, 실제 코드에서 빠지는 intermediate state는 무엇인가?
- softirq 의 fast path가 생략한 검사는 어느 init path 또는 slow path에서 보증되는가?
- 실험으로 확인한다면 'softirq가 항상 ksoftirqd에서 돈다고 착각함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?
- 실험으로 확인한다면 'local_bh_disable을 interrupt disable로 오해함' 문제를 어떤 tracepoint와 counter로 분리할 수 있는가?