본문으로 건너뛰기

버그 제보

이상한 점을 알려 주시면 고칠게요. 지금 보고 있는 페이지와 브라우저 정보가 함께 담겨요.

페이지:

Computer Science

UNIX 시그널

작성 2026. 10. 14.추가
목차9개 항목
출처와 참고

팔만코딩경(42Seoul 커뮤니티 위키)의 「minitalk 에서 컴퓨터 통신 맛보기」, 「exit() atexit() _exit() 뭐가 다른지 설명할 수 있어?」, 「[운영체제] Time의 종류와 Alarm」을 바탕으로 재구성한 노트다. 「시그널 보내기」 절과 보류 시그널이 기록되는 위치는 고려사이버대학교 「운영체제이론 및 실습」(2024-1학기) 강의 내용으로 보강했다.

시그널이란

시그널(signal)은 프로세스에 "어떤 사건이 일어났다"는 사실만 비동기로 알리는 소프트웨어 인터럽트다. 하드웨어 인터럽트가 CPU의 흐름을 끊듯, 시그널은 프로세스의 흐름을 끊고 미리 정한 동작을 실행시킨다. 데이터는 거의 실어 나르지 않는다. 시그널 번호 자체가 메시지다.

보내는 쪽예
커널 (예외)잘못된 메모리 접근 → SIGSEGV, 0으로 나누기 → SIGFPE, 닫힌 파이프에 쓰기 → SIGPIPE
커널 (이벤트)자식 종료 → SIGCHLD, 타이머 만료 → SIGALRM
터미널Ctrl+C → SIGINT, Ctrl+\ → SIGQUIT, Ctrl+Z → SIGTSTP
다른 프로세스kill(pid, sig), 셸의 kill 명령

주요 시그널과 기본 동작

시그널기본 동작언제
SIGINT종료터미널 인터럽트(Ctrl+C)
SIGTERM종료정상 종료 요청. kill의 기본값. 서비스 매니저가 데몬을 멈출 때
SIGKILL종료강제 종료. 잡거나 무시할 수 없다
SIGQUIT종료 + 코어 덤프Ctrl+\
SIGSEGV·SIGBUS·SIGILL·SIGFPE종료 + 코어 덤프메모리·명령어·산술 예외
SIGABRT종료 + 코어 덤프abort(), assert 실패
SIGPIPE종료읽는 쪽이 없는 파이프·소켓에 쓰기
SIGHUP종료제어 터미널이 끊김. 데몬은 관례상 "설정 다시 읽기"로 쓴다
SIGCHLD무시자식이 종료·정지·재개
SIGALRM·SIGVTALRM·SIGPROF종료실시간·사용자 CPU 시간·전체 CPU 시간 타이머 만료
SIGSTOP정지강제 정지. 잡거나 무시할 수 없다
SIGTSTP·SIGTTIN·SIGTTOU정지Ctrl+Z, 백그라운드 프로세스의 터미널 읽기·쓰기
SIGCONT재개정지된 프로세스를 다시 실행
SIGUSR1·SIGUSR2종료프로그램이 뜻을 정해 쓰는 사용자 정의 시그널

번호는 시스템마다 다를 수 있으므로 코드에서는 항상 이름 상수를 쓴다. 전체 목록은 man 7 signal에 있다.

시그널의 일생

flowchart LR
  g["발생(generate)"] --> p["보류(pending)"]
  p -->|"시그널 마스크로 막혀 있으면 계속 보류"| p
  p --> d["전달(deliver)"]
  d --> def["기본 동작<br/>(종료·코어·정지·무시)"]
  d --> ign["무시 (SIG_IGN)"]
  d --> h["핸들러 실행"]
  h --> r["끊긴 지점으로 복귀"]
  • 발생한 시그널은 커널이 대상 프로세스의 PCB(Linux는 task_struct의 보류 집합)에 표시해 두었다가, 그 프로세스가 커널에서 사용자 모드로 돌아가는 시점에 전달한다. 보내는 쪽은 표시만 하고 바로 돌아오므로 비동기다.
  • 표준 시그널은 큐에 쌓이지 않는다. 보류 상태는 시그널마다 비트 하나라서, 같은 시그널이 처리 전에 여러 번 오면 한 번으로 합쳐진다.
  • 서로 다른 시그널이 함께 보류되면 어떤 순서로 전달될지 정해져 있지 않다.
  • 실시간 시그널(SIGRTMIN~SIGRTMAX)은 예외다. 큐에 쌓이고, 번호 순으로 전달되며, sigqueue()로 정수나 포인터 하나를 함께 보낼 수 있다.
  • 마스크는 sigprocmask()(스레드에서는 pthread_sigmask())로 바꾼다. 임계 구역 동안 막아 두었다가 풀면 그때 전달된다.

시그널 보내기

방법대상동작
셸 kill -INT 3500, kill -9 3500지정한 PID이름은 SIG를 뺀 형태나 번호로 준다. 옵션이 없으면 SIGTERM. 자주 쓰는 번호는 INT 2, QUIT 3, KILL 9, TERM 15
kill(pid, sig)지정한 PID셸 kill이 부르는 시스템 콜. 성공 0, 실패 -1. sig가 0이면 아무것도 보내지 않고 프로세스가 있는지와 권한만 확인한다
raise(sig)자기 자신단일 스레드에서는 kill(getpid(), sig)와 같다
alarm(sec)자기 자신sec초 뒤 SIGALRM을 예약한다. 다시 부르면 이전 예약을 취소하고 남은 초를 돌려주며, alarm(0)은 예약만 취소한다
abort()자기 자신SIGABRT를 보내 코어 덤프와 함께 끝낸다. SIGABRT 핸들러가 있어도 핸들러가 돌아오면 결국 종료된다
터미널 키포그라운드 프로세스 그룹Ctrl+C(SIGINT), Ctrl+\(SIGQUIT), Ctrl+Z(SIGTSTP). 멈춘 작업은 fg·bg가 SIGCONT로 되살린다

alarm은 블로킹 호출에 시간 제한을 거는 가장 간단한 방법이다. alarm(10) 뒤 read로 입력을 기다리다 10초 안에 들어오면 alarm(0)으로 예약을 지우고, 들어오지 않으면 SIGALRM의 기본 동작으로 프로세스가 끝난다. 끝내지 않고 계속하려면 SIGALRM 핸들러를 SA_RESTART 없이 등록해 read가 EINTR로 빠져나오게 한다. 타이머 계열 함수는 운영체제의 시간과 타이머에 있다.

핸들러 등록: sigaction

가장 오래된 등록 함수는 signal(signum, handler)다. handler에 함수 대신 SIG_IGN(무시)이나 SIG_DFL(기본 동작으로 되돌림)을 줄 수 있어, signal(SIGINT, SIG_IGN)이면 Ctrl+C를 눌러도 끝나지 않는다. 그러나 signal()은 시스템마다 의미가 달라(핸들러가 한 번 실행 후 기본값으로 돌아가는지 등) 이식성이 없다. sigaction()을 쓴다.

struct sigaction sa = {0};
sa.sa_sigaction = handler;          // void handler(int sig, siginfo_t *info, void *ctx)
sa.sa_flags = SA_SIGINFO | SA_RESTART;
sigemptyset(&sa.sa_mask);           // 핸들러 실행 중 추가로 막을 시그널
sigaction(SIGUSR1, &sa, NULL);
  • SA_SIGINFO: 핸들러가 siginfo_t로 보낸 프로세스의 PID(si_pid) 등을 받는다. 응답을 보낼 상대를 알 수 있다.
  • SA_RESTART: 핸들러 때문에 끊긴 read 같은 느린 시스템 콜을 자동으로 다시 시작한다. 없으면 EINTR로 실패한다.
  • 핸들러가 실행되는 동안 같은 시그널은 자동으로 막힌다.

핸들러 작성 규칙

핸들러는 프로그램의 아무 지점에나 끼어든다. malloc 한가운데서 불렸는데 핸들러가 또 malloc을 부르면 힙이 깨진다.

  • 핸들러 안에서는 async-signal-safe 함수만 부른다. write, _exit, kill, sigaction은 되고 printf, malloc, free, exit은 안 된다(man 7 signal-safety).
  • 메인 흐름과 공유하는 변수는 volatile sig_atomic_t로 선언하고, 핸들러는 플래그만 세운다. 실제 처리는 메인 루프에서 한다.
  • 핸들러가 errno를 바꿀 수 있으므로 들어갈 때 저장하고 나올 때 복원한다.

SIGCHLD, 좀비, 고아

자식이 _exit()하면 커널은 메모리와 fd를 회수하지만, 종료 상태를 담은 프로세스 테이블 항목은 부모가 거둘 때까지 남긴다. 이 상태가 좀비(zombie, ps에서 Z)다.

상황결과대응
부모가 wait·waitpid로 거둠항목 삭제, 종료 코드 전달정상
부모가 거두지 않고 살아 있음좀비가 계속 쌓여 PID 고갈SIGCHLD 핸들러에서 while (waitpid(-1, &st, WNOHANG) > 0);
부모가 먼저 죽음고아(orphan)가 되어 init(PID 1)이나 subreaper가 입양해 거둠셸·컨테이너의 PID 1은 반드시 자식을 거둬야 한다

SIGCHLD는 합쳐질 수 있으므로 핸들러에서 한 번만 waitpid하면 자식을 놓친다. 위처럼 반복해서 거둔다. SIGCHLD를 SIG_IGN으로 두면 커널이 자식을 자동으로 거둔다(대신 종료 코드를 받을 수 없다).

waitpid(pid, &status, options)는 거둘 자식을 고르고(-1이면 아무나), WNOHANG이면 기다리지 않고 바로 돌아온다. 종료 상태는 정수를 직접 비트 연산하지 말고 매크로로 읽는다.

매크로참일 때 함께 읽는 값
WIFEXITED(status)WEXITSTATUS(status): exit()에 넘긴 값의 하위 8비트(0~255)
WIFSIGNALED(status)WTERMSIG(status): 프로세스를 죽인 시그널 번호

시그널로 통신하기의 한계

SIGUSR1을 0, SIGUSR2를 1로 정하면 비트 단위로 문자열을 보낼 수 있다. 그러나 시그널은 통신용으로 설계되지 않았다.

문제원인완화
비트가 사라짐같은 시그널이 처리 전에 다시 오면 하나로 합쳐짐수신 측이 비트마다 확인 응답(ACK)을 보낸 뒤 다음 비트를 보냄
순서가 바뀜다른 시그널 간 전달 순서 미정한 번에 하나만 보내는 stop-and-wait
느림비트 하나마다 문맥 교환과 핸들러 실행대량 전송은 파이프·소켓·공유 메모리로
sleep으로 간격 두기도착을 보장하지 못하고 느리기만 함ACK 기반 흐름 제어

비트마다 ACK를 기다리는 방식은 TCP 이전의 정지-대기(stop-and-wait) ARQ와 같은 구조다. 한 번에 여러 개를 보내고 누적 ACK를 받는 슬라이딩 윈도우, 실패 지점부터 다시 보내는 Go-Back-N, 실패한 것만 다시 보내는 Selective Repeat로 효율을 높이는 것이 TCP의 흐름 제어다.

정리

  • 시그널은 데이터 없는 비동기 알림이다. SIGKILL·SIGSTOP만 잡을 수 없다.
  • 표준 시그널은 큐에 쌓이지 않고 합쳐진다. 개수를 세거나 순서에 기대는 설계는 깨진다.
  • 핸들러는 sigaction으로 등록하고, 안에서는 플래그만 세우거나 async-signal-safe 함수만 쓴다.
  • 부모는 SIGCHLD를 받으면 WNOHANG 루프로 자식을 모두 거둬 좀비를 막는다.
  • 프로세스 간 데이터 전달에는 파이프·소켓을 쓰고, 시그널은 "깨워라·멈춰라·다시 읽어라" 같은 제어에만 쓴다.

관련 노트: 프로세스와 스레드, 컴퓨터 시스템 구조와 인터럽트, 셸이 명령을 실행하는 방식, 시간과 타이머, 데몬과 systemd

원본 기록4

댓글

GitHub 계정으로 댓글을 남길 수 있어요.