QUESTION
signal이 발생한 시점과 handler가 실행되는 시점은 왜 다른가?
signal은 생성되면 process-directed 또는 thread-directed pending 상태가 된다. 대상 thread가 그 signal을 block하지 않고 kernel에서 userspace로 돌아가는 지점에 도달해야 handler/default action이 실제로 적용된다.
handler는 원래 userspace stack에 sigframe을 만들고 instruction pointer를 handler로 바꾸는 방식으로 실행된다. return은 일반 함수 return만으로 끝나지 않고 rt_sigreturn trampoline이 register와 mask를 복원한다.
STRUCTURE
구조 그림
Process signal state
- shared disposition
- process pending
- SIGUSR1 siginfo
Thread A
- mask: SIGUSR1 blocked
- thread pending
- delivery 불가
Thread B
- mask: unblocked
- get_signal 선택
- user stack 사용
rt_sigframe
- saved registers
- old mask
- ucontext · return trampoline
process-directed signal은 pending queue에 들어간 뒤 해당 signal을 block하지 않은 thread 하나에 전달될 수 있다.
CALL PATH
호출 흐름
생성, pending, delivery 세 시점을 분리한다. mask는 signal을 삭제하지 않고 delivery를 미루며 standard signal은 여러 번 발생해도 하나로 합쳐질 수 있다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| kernel/signal.c | __send_signal_locked(), get_signal() | pending queue와 disposition 선택 |
| arch/x86/kernel/signal.c | arch_do_signal_or_restart(), setup_rt_frame() | userspace signal frame 구성 |
| arch/x86/entry/entry_64.S | syscall/interrupt return | userspace 복귀 전 signal 처리 연결 |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 signal_wait.c -o signal_wait01#define _POSIX_C_SOURCE 200809L
02#include <signal.h>
03#include <stdio.h>
04#include <unistd.h>
05
06int main(void)
07{
08 sigset_t set;
09 sigemptyset(&set);
10 sigaddset(&set, SIGUSR1);
11 if (sigprocmask(SIG_BLOCK, &set, NULL) < 0)
12 return 1;
13
14 printf("pid=%ld; waiting for SIGUSR1\n", (long)getpid());
15 fflush(stdout);
16
17 siginfo_t info;
18 int signo = sigwaitinfo(&set, &info);
19 if (signo < 0)
20 return 1;
21 printf("received signal=%d sender=%ld value=%d\n", signo,
22 (long)info.si_pid, info.si_value.sival_int);
23 return 0;
24}
CODE NOTES
코드 조각별 설명
sigemptyset(&set)sigset_t 내부 표현을 직접 0으로 가정하지 않고 API로 빈 집합을 만든다.
SIG_BLOCK대상 signal을 먼저 block해 default action이나 handler delivery 대신 pending queue에 머물게 한다.
fflush(stdout)stdout이 pipe로 redirect돼 line buffering이 아닐 때도 PID가 signal 전송자에게 즉시 보이도록 한다.
sigwaitinfo(&setasync handler가 아니라 동기 함수 반환으로 signal과 siginfo를 소비한다. 기다리는 signal은 호출 thread에서 block돼 있어야 한다.
info.si_value.sival_intsigqueue로 보낸 realtime/queued value를 읽을 수 있다. kill로 보낸 standard signal이면 의미 있는 payload를 기대하지 않는다.
DETAILS
세부 동작
process-directed signal의 thread 선택
kill로 process에 보낸 signal은 해당 signal을 block하지 않은 thread 중 하나에 전달될 수 있다. 특정 thread에 보내려면 pthread_kill/tgkill 계열을 사용한다.
multithread 프로그램은 초기 thread에서 signal을 block한 뒤 전용 sigwait/signalfd thread가 소비하도록 구성하면 handler 경쟁을 줄일 수 있다.
handler 안에서 호출 가능한 함수는 제한된다
malloc, printf, pthread_mutex_lock은 내부 상태를 갱신하던 도중 handler가 끼면 재진입 deadlock이나 corruption을 만들 수 있다. async-signal-safe 목록 안의 함수만 사용한다.
보통 handler는 sig_atomic_t flag 또는 self-pipe write만 수행하고 실제 정리는 main loop로 넘긴다.
standard와 realtime signal의 queue 규칙
같은 standard signal은 block 중 여러 번 발생해도 pending bit 하나로 합쳐질 수 있다. realtime signal은 순서와 payload를 가진 queue로 쌓이지만 resource limit이 있다.
event count를 잃으면 안 되는 workload를 standard signal 횟수로 표현하지 않는다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
sighand_struct | thread group이 signal disposition을 공유하며 exec에서 caught action이 reset된다 | handler, flags, mask |
sigpending | signal 생성부터 delivery/소비까지 process 또는 task에 존재한다 | signal bitmap, queued siginfo |
rt_sigframe | delivery 때 user stack에 생기고 sigreturn에서 소비된다 | ucontext, old mask, registers |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| signal 횟수 손실 | standard signal coalescing | realtime signal/eventfd 검토 |
| handler deadlock | async-signal-unsafe 함수 재진입 | handler call graph와 core stack |
| 원하는 thread가 안 받음 | process-directed delivery와 mask 구성 | /proc/PID/task/*/status SigBlk |
LAB
직접 확인
- 실행 PID에 kill -USR1을 보내 sigwaitinfo가 sender PID를 반환하는지 확인한다.
- SIGUSR1을 block한 상태로 여러 번 보내 /proc/PID/status SigPnd와 한 번의 소비를 관찰한다.
- sigqueue로 integer payload를 보내는 sender를 작성하고 realtime signal에서 여러 값의 순서를 확인한다.
./signal_waitstrace -e trace=rt_sigprocmask,rt_sigtimedwait,kill ./signal_waitPRIMARY REFERENCES