에듀테크 프로젝트 대시보드
Unix 시그널 (Signals)
터미널에서 Ctrl+C를 누르면 왜 프로그램이 멈출까? 운영체제가 프로세스에게 "지금 이런 일이 생겼어"라고 보내는 비동기 알림이 바로 시그널이다. 하드웨어 인터럽트의 소프트웨어 버전이라고 볼 수 있다.
아래는 무한 루프를 도는 프로세스 PID 4021이다. 버튼으로 시그널을 보내고, 핸들러 설치·블록(마스킹) 옵션을 바꿔 가며 반응을 관찰해 보자.
SIGKILL(9)·SIGSTOP(19)는 핸들러를 설치해도, 블록해도 무조건 먹힌다. ② SIGINT를 블록해 두면 그 시그널은 사라지지 않고 pending으로 쌓였다가, 블록을 풀면 그때 전달된다. ③ 같은 시그널이 여러 번 쌓여도 표준 시그널은 한 번으로 합쳐진다(coalescing).
| 이름 | 번호 | 기본 동작 | 잡을 수 있나 | 보통 언제 |
|---|---|---|---|---|
| SIGHUP | 1 | 종료 | O | 터미널 연결 끊김 · 데몬 설정 다시 읽기 관례 |
| SIGINT | 2 | 종료 | O | 터미널에서 Ctrl+C |
| SIGQUIT | 3 | 종료 + 코어덤프 | O | Ctrl+\ · 디버깅용 스냅샷 |
| SIGKILL | 9 | 즉시 종료 | X | kill -9 · 최후의 수단 |
| SIGSEGV | 11 | 종료 + 코어덤프 | O | 잘못된 메모리 접근(널 포인터 등) |
| SIGUSR1/2 | 10 / 12 | 종료 | O | 개발자가 원하는 용도로 자유롭게 |
| SIGPIPE | 13 | 종료 | O | 읽는 쪽이 닫힌 파이프에 write |
| SIGTERM | 15 | 종료 | O | kill 기본값 · "정리하고 나가" |
| SIGSTOP | 19 | 일시 정지 | X | 강제로 얼리기 |
| SIGTSTP | 20 | 일시 정지 | O | 터미널에서 Ctrl+Z |
| SIGCONT | 18 | 재개 | O | 멈춘 프로세스 다시 실행 (fg/bg) |
| SIGCHLD | 17 | 무시 | O | 자식 프로세스가 끝났을 때 → wait()로 거둬야 좀비 방지 |
SIGKILL과 SIGSTOP만 핸들러 등록·블록·무시가 전부 불가능하다. 커널이 "관리자가 프로세스를 반드시 통제할 수 있어야 한다"는 이유로 특별 취급한다.
커널은 프로세스마다 두 개의 비트 집합을 들고 있다.
시그널이 도착하면 pending에 표시만 해 둔다. 실제 처리는 프로세스가 커널 모드에서 유저 모드로 돌아오는 순간(시스템 콜 종료·인터럽트 처리 후 등)에 이뤄진다. 이때 pending AND (NOT blocked)인 시그널이 있으면 → 핸들러로 점프하거나 기본 동작을 실행한다.
malloc() 도중에 SIGINT가 오면, 핸들러 안에서 다시 malloc()을 부를 경우 자료구조가 깨질 수 있다(재진입 문제). 그래서 핸들러 안에서는 async-signal-safe한 함수(write, _exit 등)만 써야 하고, 보통은 volatile sig_atomic_t 플래그 하나만 세우고 즉시 빠져나온다.
signal() vs 권장 sigaction()// 플래그만 세우고 빠져나오는 안전한 핸들러 static volatile sig_atomic_t stop_flag = 0; void on_sigterm(int sig) { stop_flag = 1; } // write()만 허용, printf X int main(void) { struct sigaction sa = {0}; sa.sa_handler = on_sigterm; sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; // 끊긴 시스템 콜 자동 재시작 sigaction(SIGTERM, &sa, NULL); while (!stop_flag) { /* 일하기 */ } cleanup(); // 루프 밖에서 안전하게 정리 return 0; }
signal 모듈import signal, sys, time def handler(signum, frame): print(f"\n{signal.Signals(signum).name} 받음 → 정리 후 종료") sys.exit(0) signal.signal(signal.SIGINT, handler) # Ctrl+C signal.signal(signal.SIGTERM, handler) # kill while True: time.sleep(1)
kill과 trap# 시그널 보내기 (kill = "시그널 보내기", 꼭 죽이는 건 아님) kill -TERM 4021 # = kill 4021 (기본 SIGTERM) kill -STOP 4021 # 얼리기 kill -CONT 4021 # 녹이기 kill -9 4021 # SIGKILL, 안 통할 때 최후의 수단 kill -l # 시그널 목록 보기 # 스크립트에서 종료 시 임시파일 청소 trap 'rm -f /tmp/work.$$; exit' INT TERM EXIT
kill(), 예외(SIGSEGV), 터미널(Ctrl+C)이 발생sigaction 테이블로 분기
구조가 거의 대칭이다. 그래서 시그널을 "프로세스를 위한 가상 인터럽트"라고 부른다. 하드웨어 인터럽트 페이지와 프로세스 vs 스레드도 함께 보면 좋다.
wait()로 종료 상태를 거두지 않으면 프로세스 표에 <defunct>로 남는다.yes | head처럼 파이프 뒤쪽이 먼저 닫히면 앞쪽은 SIGPIPE로 조용히 죽는다. 서버 코드는 보통 이걸 무시(SIG_IGN)하고 write의 EPIPE 에러로 처리한다.signalfd나 실시간 시그널(SIGRTMIN+n)을 쓴다.