IRQ / Time · Linux 6.18.37 LTS · source + diagram note

softirq

최신 LTS 기준: kernel.org longterm 6.18.37 기준으로 파일 위치와 링크를 맞춘 원본 코드 분석 노트입니다. softirq vector, ksoftirqd, bottom half.

전체 흐름

softirq는 종류별 작업을 현재 CPU의 pending 비트로 표시하고, 허용되는 문맥에서 해당 vector action을 처리한다. IRQ handler가 항상 곧바로 softirq를 실행하는 일렬 경로는 아니다.

그림 1. pending 설정과 실행 문맥의 분기 (v6.18.37)
raiseraise_softirq_irqoff가 이 CPU의 종류별 pending 비트를 OR
IRQ 복귀중첩이 끝나고 pending이면 invoke_softirq를 검토
그 밖의 진입BH 재활성화·명시적 do_softirq·ksoftirqd도 가능
vector 실행pending snapshot을 비운 뒤 action 호출; 남은 비트 재검사

두 번째와 세 번째 칸은 대체 진입 경로다. kernel/softirq.c:760; kernel/softirq.c:713.

상황pending 이후항상 성립하지 않는 것
hard IRQ에서 raiseIRQ 복귀 시 중첩·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 처리 루프

원본 코드: Linux v6.18.37 kernel/softirq.c 579–652행 전체
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

원본 코드: Linux v6.18.37 kernel/softirq.c 760–791행 전체
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의 역할을 거꾸로 읽지 않는다.

그림 2. raise와 drain은 별도의 작업 (v6.18.37)
producer: raise_softirq__raise_softirq_irqoff()가 1UL << nr을 이 CPU의 pending mask에 OR.
consumer: handle_softirqs(bool ksirqd)pending snapshot을 비운 뒤 softirq_vec의 해당 action을 실행.
hard IRQ 복귀!in_interrupt()와 pending 조건일 때 invoke_softirq() 진입.
ksoftirqd직접 실행을 미루거나 반복 상한에 걸렸을 때 남은 pending 처리를 시도.

실행 방식은 CONFIG_PREEMPT_RT·force_irqthreads·스택 설정에 좌우된다. kernel/softirq.c:713; kernel/softirq.c:1055.

함수상태 변경아닌 것
raise_softirq_irqoffpending bit OR, 문맥에 따라 ksoftirqd 깨움vector action 자체 실행
handle_softirqs(bool ksirqd)pending snapshot clear 뒤 action dispatch, 새 pending 재검사새 pending의 유일한 producer
invoke_softirqIRQ 복귀에서 직접 처리 또는 daemon wakeup 선택모든 하드 IRQ의 무조건 후속 단계

구조체와 필드

pending 비트마스크는 처리할 softirq 종류를 나타낸다. 작업 데이터 전체를 담는 큐가 아니며, vector의 action 포인터 및 실행 문맥과 구분한다.

그림 3. pending·vector·실행 문맥은 서로 다른 축
per-CPU pending maskor_softirq_pending(1UL << nr); 새 작업 종류를 표시.
softirq_vec[nr].action해당 종류의 callback을 등록하고 drain loop에서 호출.
BH disable count현재 CPU의 bottom-half 실행 가능 상태에 관여; hard IRQ 마스크와 동의어가 아님.
ksoftirqd CPU threadsoftirq를 나중에 처리하는 한 실행 경로; pending 자체나 vector가 아님.

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을 별도로 재검사한다.

그림 4. drain의 반복·양보 조건 (v6.18.37)
  1. snapshotlocal_softirq_pending을 읽음
  2. clear → actionpending mask를 0으로, IRQ enable 뒤 vector별 action
  3. 새 pendingIRQ disable 후 다시 읽어 새 요청 확인
  4. 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 선택이 달라진다.

불변 조건과 실패 사례

lifetime

softirq의 핵심 객체가 외부에 공개된 뒤에는 마지막 참조가 사라질 때까지 해제 경로가 callback, timer, IRQ, worker와 경합하지 않아야 한다.

ordering

다른 CPU나 하위 계층이 변경된 필드를 읽는다면 잠금과 메모리 배리어가 접근 순서를 어떻게 보장하는지 확인한다. 참조 횟수와 RCU로 관리하는 객체 수명도 별도로 확인한다.

오류 복구

중간 단계에서 실패하면 이미 확보한 자원을 의존 관계에 맞춰 해제해야 한다. goto 레이블이 많다면 각 분기에 도달했을 때 확보한 자원을 표로 정리한다.

bring-up symptom

임베디드 보드를 처음 구동할 때는 패닉 대신 로그 중단, 시간 초과, 장치 초기화 지연, 과도한 인터럽트 같은 증상으로 문제가 드러날 수 있다.

주의

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 / exitvector callback 실행의 시작과 끝그 전에 어느 하드 IRQ가 유일한 원인인지
/proc/softirqsCPU별 종류별 실행 누적 횟수개별 지연 또는 drop 원인
function graph / perf__do_softirq·handle_softirqs·ksoftirqd 호출 문맥pending mask의 매 변화

tracepoint 호출 위치: kernel/softirq.c:760, kernel/softirq.c:579. 실제 이벤트와 함수 추적 가용성은 설정에 따른다.

tracefs 예시 (권한 있는 셸, 독립 인스턴스):
# 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로 분리할 수 있는가?