Process / Syscall · Linux v6.6 · source + diagram note

signal

신호 생성과 pending, 전달 선택, arm64 사용자 핸들러와 sigreturn.

전체 흐름

신호를 보내는 것과 사용자 핸들러를 실행하는 것은 서로 다른 경계다. v6.6에서 생성 경로는 신호를 무시하거나 pending에 넣고 깨울 스레드를 찾는다. 실제 선택은 대상 태스크가 신호를 처리하는 시점에 이뤄지며, arm64의 native 사용자 핸들러는 사용자 모드로 돌아가기 전에 별도 프레임을 만든다.

그림 1. 생성에서 arm64 사용자 복귀까지의 네 경계
생성: __send_signal_locked()prepare_signal / private·shared pending / complete_signal
선택: get_signal()blocked mask, 동기 신호, private→shared dequeue, action 판단
arm64 native handlerdo_signal → handle_signal → setup_rt_frame
restorer → rt_sigreturnSA_RESTORER 또는 vDSO trampoline을 거쳐 사용자 sigframe의 마스크·레지스터·altstack을 복원한다.

점선은 모든 신호가 핸들러로 간다는 뜻이 아니다. 무시·기본 동작·ptrace·blocked 상태면 흐름이 달라진다. compat 태스크는 별도 frame 경로다.

근거: v6.6 생성, 선택과 action, arm64 사용자 복귀.

소스 코드 위치

먼저 확인할 Linux v6.6 소스 파일

원본 코드:
Linux v6.6: kernel/signal.c
Linux v6.6: arch/arm64/kernel/signal.c
Linux v6.6: include/linux/signal.h

설명: 공통 생성·pending·action 선택은 kernel/signal.c, arm64 native sigframe 설치·sigreturn은 arch/arm64/kernel/signal.c에서 확인한다. include/linux/signal.h는 신호 집합 관련 선언을 대조할 때 참고한다.

대표 코드

아래 두 상자는 원문과 일치하지만 원래 제목과 실제 범위가 달랐다. 첫 상자는 group-stop 처리와 큐 레코드 할당 함수의 시작, 둘째는 prepare_signal()의 끝과 wants_signal()이다. 실제 get_signal()과 arm64 do_signal()은 별도 원문을 봐야 한다.

group-stop 처리 → task_join_group_stop() → __sigqueue_alloc() 앞부분

원본 코드: kernel/signal.c, 379–418행
	if (!WARN_ON_ONCE(sig->group_stop_count == 0))
		sig->group_stop_count--;

	/*
	 * Tell the caller to notify completion iff we are entering into a
	 * fresh group stop.  Read comment in do_signal_stop() for details.
	 */
	if (!sig->group_stop_count && !(sig->flags & SIGNAL_STOP_STOPPED)) {
		signal_set_stop_flags(sig, SIGNAL_STOP_STOPPED);
		return true;
	}
	return false;
}

void task_join_group_stop(struct task_struct *task)
{
	unsigned long mask = current->jobctl & JOBCTL_STOP_SIGMASK;
	struct signal_struct *sig = current->signal;

	if (sig->group_stop_count) {
		sig->group_stop_count++;
		mask |= JOBCTL_STOP_CONSUME;
	} else if (!(sig->flags & SIGNAL_STOP_STOPPED))
		return;

	/* Have the new thread join an on-going signal group stop */
	task_set_jobctl_pending(task, mask | JOBCTL_STOP_PENDING);
}

/*
 * allocate a new signal queue record
 * - this may be called without locks if and only if t == current, otherwise an
 *   appropriate lock must be held to stop the target task from exiting
 */
static struct sigqueue *
__sigqueue_alloc(int sig, struct task_struct *t, gfp_t gfp_flags,
		 int override_rlimit, const unsigned int sigqueue_flags)
{
	struct sigqueue *q = NULL;
	struct ucounts *ucounts = NULL;

확인 사항: 이 발췌는 group-stop 카운트와 새 스레드의 group-stop 참가, sigqueue 레코드 할당의 시작만 보인다. 신호 pending 삽입과 실제 사용자 핸들러 전달은 이 코드에 없다.

prepare_signal() 끝 → wants_signal()

원본 코드: kernel/signal.c, 963–1002행(첫 빈 행 포함)

		if (why) {
			/*
			 * The first thread which returns from do_signal_stop()
			 * will take ->siglock, notice SIGNAL_CLD_MASK, and
			 * notify its parent. See get_signal().
			 */
			signal_set_stop_flags(signal, why | SIGNAL_STOP_CONTINUED);
			signal->group_stop_count = 0;
			signal->group_exit_code = 0;
		}
	}

	return !sig_ignored(p, sig, force);
}

/*
 * Test if P wants to take SIG.  After we've checked all threads with this,
 * it's equivalent to finding no threads not blocking SIG.  Any threads not
 * blocking SIG were ruled out because they are not running and already
 * have pending signals.  Such threads will dequeue from the shared queue
 * as soon as they're available, so putting the signal on the shared queue
 * will be equivalent to sending it to one such thread.
 */
static inline bool wants_signal(int sig, struct task_struct *p)
{
	if (sigismember(&p->blocked, sig))
		return false;

	if (p->flags & PF_EXITING)
		return false;

	if (sig == SIGKILL)
		return true;

	if (task_is_stopped_or_traced(p))
		return false;

	return task_curr(p) || !task_sigpending(p);
}

확인 사항: wants_signal()은 그룹 신호를 가져갈 가능성이 있는 스레드를 깨우기 위한 후보 검사다. 최종 dequeue와 handler 선택은 get_signal() 등 뒤 경로에서 일어난다.

함수별 분석

생성 호출이 성공해도 즉시 사용자 핸들러가 실행되는 것은 아니다. __send_signal_locked()는 무시·이미 pending인 비실시간 신호·큐 할당 실패 등의 결과를 구분하고, 필요하면 pending 비트를 세운 뒤 complete_signal()로 깨울 스레드를 찾는다.

그림 2. v6.6 생성 경로에서 pending이 생기는 조건
__send_signal_locked(sig, t, type)sighand->siglock 아래 prepare_signal() 검사
무시 / 이미 pendingpending 삽입 생략prepare_signal() false 또는 legacy_queue() true.
실시간 큐 오버플로조건부 -EAGAIN특정 siginfo의 큐 확보 실패면 비트 설정 전에 종료.
진행pending 비트 설정PIDTYPE_PID는 t->pending, 그 외는 shared_pending.
complete_signal() → 깨울 스레드 후보wants_signal()은 blocked·PF_EXITING·stopped 등을 살피지만, 여기서 사용자 handler를 실행하거나 실제 dequeue하지 않는다.

trace_signal_generate()는 공통 반환 지점에 있어 ignored·already pending·overflow도 result 값과 함께 기록한다.

경계핵심 코드구분
생성 검사prepare_signal(), legacy_queue()생성 이벤트가 새 pending 삽입을 보장하지 않음.
pending 대상type == PIDTYPE_PID ? t->pending : shared_pending개별 스레드와 thread group의 신호를 구분.
깨우기complete_signal(), wants_signal()후보 스레드 선택이지 handler 완료가 아님.
실제 선택get_signal()에서 dequeue·action 평가대상 태스크의 처리 시점.

구조체와 필드

pending 큐, 차단 마스크, action은 서로 다른 결정에 관여한다. dequeue_signal()은 current->blocked를 적용해 해당 태스크의 private pending을 먼저, 그다음 thread group의 shared pending을 살핀다. get_signal()은 그보다 앞서 동기 신호도 확인한다.

그림 3. 두 pending 큐가 blocked mask와 만나는 자리

task_struct.pending

PIDTYPE_PID로 보낸 스레드 전용 pending.

/proc status: SigPnd는 해당 스레드의 pending 비트다.

signal_struct.shared_pending

thread group 대상으로 보낸 공유 pending.

/proc status: ShdPnd는 그룹 공유 pending 비트다.

차단되지 않은 신호를 선택동기 신호를 먼저 확인한 뒤 private → shared 순서로 dequeue. blocked는 “무시”가 아니라 선택 보류에 관여한다.
sighand->action[signr - 1]SIG_IGN, SIG_DFL 또는 사용자 handler를 판단한다. 이 단계가 pending 저장 자체와 같지 않다.
필드v6.6에서 하는 일관찰·주의
t->pending스레드 전용 큐·비트.SigPnd; 새 비트가 항상 sigqueue 레코드를 뜻하지는 않음.
t->signal->shared_pending그룹 공유 큐·비트.ShdPnd; complete_signal()은 깨울 후보를 탐색.
current->blockeddequeue에서 선택 가능한 신호를 거름.SigBlk; 나중에 unblock되면 action이 달라졌을 수 있음.
sighand->action[]무시·기본 동작·사용자 handler 선택.SigIgn/SigCgt; 커널 action 검사와 대조.

근거: private→shared dequeue, 동기 신호·blocked·action.

실행 단계와 상태 변화

arm64의 do_notify_resume()는 사용자 모드 복귀 작업 플래그에서 do_signal()을 부른다. get_signal()이 사용자 handler를 반환한 경우에만 native 태스크는 setup_rt_frame()을 거친다. 기본 정지·종료 동작이나 무시된 신호에는 사용자 sigframe이 만들어지지 않는다.

그림 4. action 선택과 arm64 native handler·sigreturn
get_signal(): signr + actiontrace_signal_deliver는 action 처리 직전의 기록점.
SIG_IGN다음 신호 검사사용자 handler·frame 없음.
SIG_DFL기본 동작ignore / stop / fatal 등 조건에 따라 진행.
사용자 handlerksig 반환 → handle_signal()native 태스크는 setup_rt_frame().
사용자 rt_sigframe이전 레지스터·마스크·altstack 등을 사용자 스택에 저장.
regs->pc = handlerregs[30] = SA_RESTORER 또는 vDSO sigtramp.
rt_sigreturn()frame 검증 후 sigmask·레지스터·altstack 복원.

frame 설치 실패 또는 잘못된 복귀 frame은 별도 오류 경로다. compat 태스크는 compat setup 함수를 사용하므로 이 native 그림의 ABI를 그대로 적용하지 않는다.

조건kernel/signal.carm64 native 결과
차단·pending 없음get_signal()이 선택하지 않음.handler frame 없음; 저장 마스크 복원 등의 복귀 처리.
SIG_IGN / 기본 동작무시하고 계속 검사하거나 기본 정지·종료 등 처리.사용자 handler frame 경로가 아님.
사용자 handlerksig.ka로 action 반환.setup_rt_frame() 성공 뒤 사용자 모드에서 실행.
handler 반환restorer가 syscall을 시작.rt_sigreturn()이 사용자 frame을 검증·복원.
frame 실패signal_setup_done(failed)에서 강제 SIGSEGV.handler 정상 실행·복귀로 세지 않음.

근거: frame·restorer 설정, do_notify_resume/do_signal/handle_signal, rt_sigreturn.

불변 조건과 실패 사례

생성 ≠ 실행

trace_signal_generate()는 ignored·already pending·overflow도 result로 기록한다. pending 생성과 handler 실행을 한 숫자로 세지 않는다.

blocked ≠ ignored

차단 중인 신호를 바로 무시한다고 단정하면 안 된다. sig_ignored()는 blocked/real_blocked인 신호를 무시 대상으로 보지 않고, dequeue는 mask로 선택을 보류한다.

선택 ≠ 사용자 handler

trace_signal_deliver()는 get_signal()의 ptrace 검사 뒤, action을 처리하기 전에 기록된다. 이후 SIG_IGN/default 분기는 사용자 handler를 실행하지 않을 수 있고, 앞선 ptrace 분기에서 신호가 소거되면 이 기록점까지 오지 않는다.

sigframe ABI

native arm64 frame은 사용자 스택에 놓인다. compat 경로, SA_RESTORER/vDSO trampoline, 잘못된 frame의 오류 처리를 각각 구분한다.

주의

wants_signal()의 깨우기 후보를 최종 수신 스레드로 확정하지 않는다.

주의

rt_sigreturn()은 커널 스택이 아니라 사용자 sigframe에서 상태를 복구한다.

계측과 검증

고정 원문에서 이벤트 위치를 먼저 확인한다. trace_signal_generate()는 __send_signal_locked()의 공통 반환점에서 result와 함께 호출된다. trace_signal_deliver()는 get_signal()에서 action 평가 전에 찍히므로 사용자 handler 완료를 뜻하지 않는다.

관찰점말할 수 있는 것말할 수 없는 것
signal_generate생성 시도와 ignored/already pending/overflow 등 결과.새 pending 삽입 또는 handler 실행 보장.
signal_deliver선택된 신호가 action 검사 위치에 도달.사용자 frame 설치·handler 완료·sigreturn 성공.
/proc/<pid>/status관찰 순간의 SigPnd/ShdPnd/SigBlk/SigIgn/SigCgt.짧은 상태 전이의 전체 기록이나 handler 횟수.
읽기 전용 확인:
# tracefs가 이미 제공되는 테스트 환경에서 이벤트 형식만 확인
cat /sys/kernel/tracing/events/signal/signal_generate/format
cat /sys/kernel/tracing/events/signal/signal_deliver/format
# 살아 있는 실험 대상 PID로 교체
cat /proc/<pid>/status

기록이 필요하면 격리된 실험 환경에서 해당 이벤트를 선택하고 결과 코드·pending 상태·handler 관찰을 별도로 대조한다. 이 읽기 절차는 공유 tracer와 filter를 변경하지 않는다.

추가 확인 사항

  • 보낸 신호가 개별 스레드의 pending인지 그룹의 shared_pending인지, 그리고 기존 비실시간 pending과 중복되는지?
  • complete_signal()이 깨우려는 스레드와 실제 get_signal()에서 dequeue한 스레드가 같은지?
  • 차단 마스크, ptrace, SIG_IGN/SIG_DFL/사용자 action 중 어느 분기에서 멈추거나 진행하는지?
  • arm64 native인지 compat인지, frame 설치와 restorer/rt_sigreturn 복원이 실제로 완료됐는지?