한양과학기술고등학교

에듀테크 프로젝트 대시보드

Unix 시그널 (Signals)

Unix 시그널 (Signals)

터미널에서 Ctrl+C를 누르면 왜 프로그램이 멈출까? 운영체제가 프로세스에게 "지금 이런 일이 생겼어"라고 보내는 비동기 알림이 바로 시그널이다. 하드웨어 인터럽트의 소프트웨어 버전이라고 볼 수 있다.

시그널이란? — 커널이 프로세스에게 전달하는 아주 작은 메시지(번호 하나). 프로세스가 무슨 코드를 실행하고 있든 흐름을 끊고 미리 등록해 둔 핸들러로 잠깐 뛰어갔다 온다. 핸들러를 등록하지 않았다면 시그널마다 정해진 기본 동작(대부분 "프로세스 종료")이 실행된다.

1. 프로세스에 시그널 보내보기

아래는 무한 루프를 도는 프로세스 PID 4021이다. 버튼으로 시그널을 보내고, 핸들러 설치·블록(마스킹) 옵션을 바꿔 가며 반응을 관찰해 보자.

./worker   PID 4021
🙂
RUNNING
CPU에서 루프 실행 중
대기 중(pending) 시그널: 없음
관찰 포인트 — ① SIGKILL(9)·SIGSTOP(19)는 핸들러를 설치해도, 블록해도 무조건 먹힌다. ② SIGINT를 블록해 두면 그 시그널은 사라지지 않고 pending으로 쌓였다가, 블록을 풀면 그때 전달된다. ③ 같은 시그널이 여러 번 쌓여도 표준 시그널은 한 번으로 합쳐진다(coalescing).

2. 자주 쓰는 시그널과 기본 동작

이름번호기본 동작잡을 수 있나보통 언제
SIGHUP1종료O터미널 연결 끊김 · 데몬 설정 다시 읽기 관례
SIGINT2종료O터미널에서 Ctrl+C
SIGQUIT3종료 + 코어덤프OCtrl+\ · 디버깅용 스냅샷
SIGKILL9즉시 종료Xkill -9 · 최후의 수단
SIGSEGV11종료 + 코어덤프O잘못된 메모리 접근(널 포인터 등)
SIGUSR1/210 / 12종료O개발자가 원하는 용도로 자유롭게
SIGPIPE13종료O읽는 쪽이 닫힌 파이프에 write
SIGTERM15종료Okill 기본값 · "정리하고 나가"
SIGSTOP19일시 정지X강제로 얼리기
SIGTSTP20일시 정지O터미널에서 Ctrl+Z
SIGCONT18재개O멈춘 프로세스 다시 실행 (fg/bg)
SIGCHLD17무시O자식 프로세스가 끝났을 때 → wait()로 거둬야 좀비 방지

SIGKILL과 SIGSTOP만 핸들러 등록·블록·무시가 전부 불가능하다. 커널이 "관리자가 프로세스를 반드시 통제할 수 있어야 한다"는 이유로 특별 취급한다.

3. 시그널은 어떻게 전달될까

커널은 프로세스마다 두 개의 비트 집합을 들고 있다.

시그널이 도착하면 pending에 표시만 해 둔다. 실제 처리는 프로세스가 커널 모드에서 유저 모드로 돌아오는 순간(시스템 콜 종료·인터럽트 처리 후 등)에 이뤄진다. 이때 pending AND (NOT blocked)인 시그널이 있으면 → 핸들러로 점프하거나 기본 동작을 실행한다.

왜 "비동기"가 위험할까? — 핸들러는 아무 때나 코드 중간에 끼어든다. malloc() 도중에 SIGINT가 오면, 핸들러 안에서 다시 malloc()을 부를 경우 자료구조가 깨질 수 있다(재진입 문제). 그래서 핸들러 안에서는 async-signal-safe한 함수(write, _exit 등)만 써야 하고, 보통은 volatile sig_atomic_t 플래그 하나만 세우고 즉시 빠져나온다.

4. 코드로 보기

C — 옛날 방식 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;
}

Python — 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

5. 하드웨어 인터럽트 vs 시그널

하드웨어 인터럽트

CPU 하드웨어 → 커널에게
키보드·타이머·디스크 등 장치가 발생
ISR(인터럽트 서비스 루틴)이 커널 모드에서 실행
인터럽트 벡터 테이블로 분기

시그널

커널 → 프로세스에게
다른 프로세스의 kill(), 예외(SIGSEGV), 터미널(Ctrl+C)이 발생
핸들러가 유저 모드에서 실행
프로세스별 sigaction 테이블로 분기

구조가 거의 대칭이다. 그래서 시그널을 "프로세스를 위한 가상 인터럽트"라고 부른다. 하드웨어 인터럽트 페이지와 프로세스 vs 스레드도 함께 보면 좋다.

6. 실전에서 자주 만나는 함정