1. 정의 / 결론
Signal은 리눅스에서 실행 중인 프로세스에 비동기적으로 전달되는 제어 이벤트다.
프로세스는 한 번 시작되면 자기 코드 흐름에 따라 계속 실행되지만, 운영체제는 필요할 때 그 실행 흐름 바깥에서 개입해야 한다. 사용자가 직접 중단을 요청할 수도 있고, 셸이 작업을 멈추거나 재개할 수도 있으며, 시스템이나 서비스 매니저가 정상 종료를 요청할 수도 있다. 이때 사용되는 핵심 메커니즘이 바로 Signal이다.
많은 사람이 Signal을 단순히 “프로세스 죽이는 기능” 정도로 이해하지만, 그건 너무 좁은 해석이다. Signal의 본질은 명령 실행이 아니라 이미 실행 중인 프로세스의 상태를 외부에서 바꾸는 메커니즘에 있다. 즉, 프로그램을 시작하는 인터페이스가 아니라, 시작된 프로그램의 생명주기와 상태 전이를 제어하는 인터페이스다.
예를 들어 Ctrl + C를 눌렀을 때 프로그램이 멈추는 것도 Signal 때문이고, kill 명령으로 종료를 요청하는 것도 Signal 때문이다. 백그라운드 작업을 중단했다가 다시 재개하는 Job Control 역시 Signal과 직접 연결된다. 따라서 Signal은 단순한 종료 도구가 아니라, 프로세스 제어, 서비스 운영, 배치 중단, 컨테이너 종료, 셸 작업 제어까지 이어지는 리눅스 실행 구조의 핵심이다.
핵심을 한 문장으로 정리하면 이렇다.
Signal은 실행 중인 프로세스에 외부에서 제어 의도를 전달하는 운영체제 수준의 표준 인터페이스다.
이 개념을 정확히 이해하면 kill -9를 무작정 쓰는 습관에서 벗어날 수 있고, SIGTERM과 SIGINT의 차이도 자연스럽게 이해된다. 더 나아가 Docker, Kubernetes, systemd, 셸 스크립트의 trap, graceful shutdown 같은 실무 개념도 한 번에 연결된다.
2. 핵심 요약
Signal은 프로세스 간 제어를 위한 이벤트 전달 구조다.
리눅스에서는 프로세스를 단순히 “실행 중이냐 아니냐”로만 다루지 않는다. 현재 foreground에 있는지, background로 돌아가는지, 중단되었는지, 종료 요청을 받았는지 같은 상태 변화가 중요하다. Signal은 이런 상태 변화를 외부에서 전달하는 표준 수단이다.
대표적으로 SIGINT는 사용자 인터럽트다. 보통 터미널에서 Ctrl + C를 입력하면 foreground 프로세스에 전달된다. SIGTERM은 정상 종료 요청이다. 즉시 강제로 죽이는 것이 아니라, 프로세스에게 “이제 종료를 준비하라”는 의미를 전달한다. 반대로 SIGKILL은 프로세스에게 선택권을 주지 않는다. 커널이 직접 제거한다.
여기서 가장 중요한 차이는 이것이다.
Signal은 기본적으로 강제 종료 구조가 아니라 협의 기반 종료 구조다.
즉, 운영체제가 프로세스를 무조건 삭제하는 것이 아니라, 먼저 종료 요청이나 상태 전이 요청을 보내고, 프로세스가 이를 받아 적절히 정리하도록 설계되어 있다. 이 구조 덕분에 애플리케이션은 다음과 같은 처리를 할 수 있다.
- 열린 파일 닫기
- 로그 flush
- 네트워크 커넥션 정리
- 락 해제
- 임시 파일 삭제
- 체크포인트 저장
- 트랜잭션 롤백 또는 커밋 정리
실무에서 Signal을 이해해야 하는 이유도 여기에 있다.
프로세스를 죽이는 것 자체는 쉽다. 하지만 안전하게 종료시키는 것은 다르다. 데이터 일관성, 리소스 정리, 재기동 안정성까지 생각하면 Signal은 필수 개념이다.
간단히 정리하면 다음과 같다.
SIGINT -> 사용자 인터럽트 (Ctrl + C)
SIGTERM -> 정상 종료 요청
SIGKILL -> 강제 종료
SIGSTOP -> 일시 정지
SIGCONT -> 재개
하지만 이름만 외우는 것으로는 부족하다. 중요한 것은 각 신호가 어떤 의도를 담고 있고, 프로세스가 그 신호를 어떻게 처리하거나 처리하지 못하는지, 그리고 이것이 셸과 운영 환경에서 어떻게 연결되는지를 함께 이해하는 것이다.
3. 왜 필요한가
프로세스는 한 번 실행되면 자신의 코드 흐름에 따라 계속 동작한다.
예를 들어 긴 배치 작업, 서버 프로세스, 무한 루프를 도는 데몬, 파일을 복사하는 명령, 로그를 계속 읽는 프로세스는 시작 이후 스스로 종료 조건에 도달하기 전까지 계속 실행된다. 문제는 현실의 시스템에서는 이렇게 실행 중인 프로그램을 외부에서 제어해야 하는 상황이 반드시 발생한다는 점이다.
가장 쉬운 예는 사용자의 직접 중단이다. 어떤 명령이 너무 오래 걸리거나 잘못 실행되었다면 사용자는 당연히 중간에 멈추고 싶어진다. 또 시스템 관점에서는 서비스 재배포, 서버 종료, 컨테이너 교체, 배치 취소, 리소스 회수 같은 이유로 실행 중인 프로세스에 종료를 요청해야 한다. 이때 단순히 “즉시 제거”만 가능하다면 문제는 매우 커진다.
왜냐하면 프로세스는 실행 도중 다양한 상태를 들고 있기 때문이다. 메모리만 쓰는 것이 아니라 파일을 열고 있을 수 있고, DB 트랜잭션 안에 있을 수 있으며, 소켓 연결을 유지하고 있을 수 있고, 임시 파일을 만들고 있을 수 있다. 강제로 제거하면 종료는 되지만, 정상적인 종료는 아니다. 이 차이는 실제 운영에서 매우 크다.
예를 들어 이런 문제가 생길 수 있다.
- 쓰는 중이던 파일이 반쯤 기록된 상태로 남는다
- 락 파일이 제거되지 않아 다음 실행이 막힌다
- DB 작업이 중간에 끊겨 데이터 일관성이 깨진다
- 워커 프로세스가 처리 중인 작업 상태를 저장하지 못한다
- 로그가 flush되지 않아 원인 추적이 어려워진다
즉, “죽는다”와 “정상 종료한다”는 전혀 다른 문제다.
Signal은 이 문제를 해결하기 위해 존재한다.
프로세스에 무조건 제거 명령을 내리는 것이 아니라, 먼저 종료 요청 또는 상태 변경 요청을 보낸다. 그러면 프로세스는 그 요청을 받아 cleanup을 수행하고, 필요한 정리를 마친 뒤 스스로 종료할 수 있다.
구조적으로 보면 차이는 분명하다.
기존 방식: 강제 종료
-> 커널이 즉시 프로세스 제거
-> cleanup 기회 없음
Signal 방식: 종료 요청
-> 프로세스가 신호 수신
-> 내부 정리 수행
-> 정상 종료
이 차이 때문에 Signal은 단순히 종료 수단이 아니라 프로세스 생명주기 제어 인터페이스가 된다.
사용자는 중단할 수 있고, 셸은 작업 상태를 바꿀 수 있으며, 서비스 매니저는 graceful shutdown을 시도할 수 있고, 컨테이너 런타임은 일정 시간 정상 종료를 기다린 뒤 최종적으로 강제 종료할 수 있다. 즉, 현대 리눅스 시스템 운영은 거의 전부 Signal 위에서 동작한다고 봐도 과장이 아니다.
4. 예제
예제 1. SIGINT — Ctrl + C로 현재 작업 중단
가장 익숙한 Signal은 SIGINT다.
터미널에서 어떤 명령을 실행한 뒤 Ctrl + C를 누르면 보통 그 작업이 중단된다. 많은 사용자가 이걸 단순한 “터미널 취소 단축키” 정도로 생각하지만, 실제로는 터미널이 foreground 프로세스 그룹에 SIGINT를 보내는 구조다.
sleep 100
이 상태에서 Ctrl + C를 누르면 현재 실행 중인 sleep 프로세스는 SIGINT를 받고 종료된다.
결과는 단순하다.
프로세스가 즉시 중단된다
왜 이렇게 되는가.
터미널은 사용자 입력을 받아 제어 문자를 해석한다. Ctrl + C는 단순 텍스트 입력이 아니라 인터럽트 요청으로 처리된다. 그리고 그 요청은 현재 foreground에서 실행 중인 프로세스에 SIGINT로 전달된다. 따라서 이 동작은 셸 문법이 아니라 터미널 + 프로세스 그룹 + Signal 구조의 결과다.
언제 쓰는가.
사용자가 직접 실행 중인 작업을 멈추고 싶을 때 가장 기본적으로 사용한다. 빌드, 테스트, 무한 루프, 오래 걸리는 조회, 잘못된 명령 실행 등 거의 모든 인터랙티브 환경에서 사용된다.
추가로 알아둘 점도 있다.
모든 프로세스가 SIGINT를 똑같이 처리하는 것은 아니다. 기본 동작은 종료지만, 프로그램이 직접 handler를 등록하면 다른 동작을 할 수 있다. 예를 들어 한 번은 경고만 출력하고, 두 번째 입력에서 종료하거나, 현재 작업을 안전하게 정리하고 종료하도록 만들 수 있다.
예제 2. SIGTERM으로 정상 종료 요청
SIGTERM은 가장 중요한 종료 신호다.
이 신호의 핵심은 “즉시 제거”가 아니라 “종료를 요청한다”는 점이다. 그래서 서비스 종료, 배치 중단, 컨테이너 중지 같은 상황에서 가장 먼저 사용된다.
kill -15 <PID>
혹은 숫자를 생략하고 더 읽기 쉽게 쓸 수도 있다.
kill -TERM <PID>
결과는 보통 이렇다.
프로세스가 종료되지만, 종료 전에 내부 cleanup 로직이 실행될 수 있다
왜 이렇게 되는가.SIGTERM은 처리 가능한 Signal이다. 즉, 프로세스가 이 신호를 받으면 기본 동작에 따라 종료될 수도 있고, 직접 정의한 handler를 통해 종료 전 로직을 수행할 수도 있다. 이 덕분에 애플리케이션은 단순 종료가 아니라 graceful shutdown을 구현할 수 있다.
예를 들어 서버 애플리케이션이라면 다음 같은 흐름을 만들 수 있다.
SIGTERM 수신
-> 신규 요청 받지 않음
-> 진행 중 요청 처리 마무리
-> DB 커넥션 정리
-> 로그 flush
-> 종료
언제 쓰는가.
서비스 종료, 서버 재시작, 배치 취소, 컨테이너 종료, 운영 자동화 등 “정상 종료”가 필요한 거의 모든 상황에서 사용한다. 실무에서는 kill -9보다 훨씬 먼저 고려해야 하는 기본 선택지다.
예제 3. SIGKILL로 강제 종료
SIGKILL은 협의가 아니라 집행이다.
이 신호는 프로세스에게 정리 기회를 주지 않는다. 커널이 직접 프로세스를 제거하며, 프로세스는 이를 무시할 수도 없고 handler를 등록할 수도 없다.
kill -9 <PID>
또는
kill -KILL <PID>
결과는 단순하다.
프로세스가 즉시 종료된다
왜 이렇게 되는가.SIGKILL은 시스템 안정성을 위해 존재하는 예외적 강제 수단이다. 프로세스가 비정상 상태로 멈췄거나, SIGTERM을 무시하거나, cleanup handler에서 다시 멈춰버리거나, 사용자 공간에서 복구가 불가능한 경우에 최종적으로 사용한다.
하지만 이 방식은 대가가 크다.
프로세스는 종료 전에 아무것도 할 수 없다. 따라서 메모리 정리는 커널이 하더라도, 사용자 공간 수준의 cleanup은 전혀 수행되지 않는다. 예를 들어 임시 파일, 애플리케이션 락, 중간 저장 상태, 외부 시스템과의 정합성 같은 것은 보장되지 않는다.
언제 쓰는가.SIGTERM으로 종료되지 않는 프로세스를 최후 수단으로 제거할 때만 사용해야 한다. 실무에서는 “항상 kill -9부터”가 아니라, TERM -> 대기 -> KILL 순서가 원칙이다.
예제 4. SIGSTOP / SIGCONT로 일시 정지와 재개
Signal은 종료만 위한 것이 아니다.
프로세스를 잠시 멈추거나 다시 재개할 수도 있다. 이 기능은 Job Control, 디버깅, 리소스 제어에서 매우 중요하다.
kill -STOP <PID>
kill -CONT <PID>
결과는 다음과 같다.
프로세스가 일시 정지되었다가 다시 실행된다
왜 이렇게 되는가.SIGSTOP은 프로세스 실행을 정지시키고, SIGCONT는 다시 계속 실행하도록 만든다. SIGSTOP은 SIGKILL과 비슷하게 프로세스가 무시할 수 없다. 즉, 사용자 코드가 “나는 멈추기 싫다”라고 거부할 수 없다. 반대로 SIGCONT는 중단된 프로세스를 재개시킨다.
이 구조는 셸의 Job Control과 직접 연결된다.
예를 들어 Ctrl + Z를 누르면 foreground 작업이 중단되고, 셸은 이를 stopped 상태의 job으로 관리한다. 이후 bg, fg 같은 명령으로 재개할 수 있다. 즉, Job Control은 독립된 마법이 아니라 Signal 기반 상태 전이 구조다.
언제 쓰는가.
리소스를 잠시 양보해야 할 때, 디버깅 중 실행을 멈출 때, 셸 작업 제어를 할 때, 운영 중 특정 프로세스를 일시 중단해야 할 때 사용된다.
예제 5. trap으로 Signal 처리
셸 스크립트에서도 Signal을 직접 다룰 수 있다.
이때 사용하는 것이 trap이다. trap은 특정 Signal을 받았을 때 어떤 명령을 실행할지 정의한다. 즉, 기본 동작 대신 사용자 정의 종료 로직을 만들 수 있다.
#!/bin/bash
trap "echo 'cleanup'; exit" SIGTERM
while true; do
sleep 1
done
이 스크립트를 실행한 뒤 외부에서 SIGTERM을 보내면 다음처럼 동작한다.
cleanup
그리고 종료한다.
왜 이렇게 되는가.trap이 SIGTERM 수신 시 실행할 handler 역할을 하기 때문이다. 따라서 스크립트는 단순히 죽지 않고, 종료 전에 필요한 동작을 수행할 수 있다. 실제로는 메시지 출력보다 다음 같은 작업이 더 중요하다.
- 임시 파일 삭제
- 락 파일 제거
- 백그라운드 자식 프로세스 정리
- 체크포인트 저장
- 종료 로그 기록
좀 더 실전적인 예제를 보면 이해가 쉽다.
#!/bin/bash
LOCK_FILE="/tmp/myjob.lock"
TMP_FILE="/tmp/myjob.tmp"
cleanup() {
echo "[INFO] cleanup start"
rm -f "$LOCK_FILE" "$TMP_FILE"
echo "[INFO] cleanup done"
exit 0
}
trap cleanup SIGINT SIGTERM
touch "$LOCK_FILE"
touch "$TMP_FILE"
while true; do
echo "working..."
sleep 2
done
이 스크립트는 SIGINT나 SIGTERM을 받으면 정리 작업을 한 뒤 종료한다.
이런 패턴은 실무에서 매우 흔하다. Signal을 무시한 스크립트는 종료는 되더라도 운영 흔적을 남기고, 다음 실행을 막고, 장애 분석을 어렵게 만든다.
5. 실무 적용
1) 서비스 종료 처리
서버 애플리케이션은 대부분 “정상 종료”가 중요하다.
예를 들어 API 서버가 요청을 처리 중인데 운영자가 무조건 프로세스를 제거해 버리면, 현재 처리 중인 요청은 중간 상태로 끝날 수 있다. DB 트랜잭션이 걸려 있을 수 있고, 외부 시스템에 이미 일부 요청이 나갔을 수도 있으며, 응답은 실패했지만 내부 상태는 반쯤 반영되었을 수도 있다. 즉, 단순 종료는 장애를 더 크게 만든다.
그래서 서버 프로세스는 보통 SIGTERM을 받아 graceful shutdown을 수행하도록 구현한다. 자바라면 대표적으로 shutdown hook을 등록한다.
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("Shutdown hook triggered");
// 커넥션 종료, 큐 flush, 리소스 정리 등
}));
이 구조의 효과는 분명하다.
운영자가 서비스 종료를 요청하면 프로세스는 즉시 사라지는 것이 아니라, 먼저 종료 절차로 진입한다. 신규 요청 수신을 멈추고, 진행 중인 작업을 마무리하며, 리소스를 정리하고, 로그를 남기고 종료한다. 이게 바로 서비스 운영에서 말하는 graceful shutdown이다.
Spring Boot, Netty, Nginx, 여러 애플리케이션 서버가 모두 이 개념 위에 서 있다.
즉, Signal을 이해하지 못하면 graceful shutdown도 피상적으로밖에 이해할 수 없다.
2) 배치 작업 중단 처리
배치 작업은 한 번 실행되면 오래 걸리는 경우가 많다.
수집, 변환, 집계, 학습, 파일 처리, 대량 적재 같은 작업은 수 분에서 수 시간까지 이어질 수 있다. 이때 중간에 작업을 중단해야 하는 상황은 흔하다. 운영자가 잘못된 입력을 발견할 수도 있고, 상위 스케줄러가 취소를 요청할 수도 있으며, 유지보수로 인해 시스템이 내려갈 수도 있다.
문제는 배치를 무조건 강제 종료하면 재시작이 어렵다는 점이다.
어디까지 처리했는지 알 수 없고, 이미 반영된 데이터와 아직 반영되지 않은 데이터를 구분하기 어렵고, 임시 산출물이 애매한 상태로 남을 수 있다. 그래서 배치는 SIGTERM 수신 시 checkpoint를 남기고 안전하게 종료하도록 만드는 경우가 많다.
예를 들면 이런 흐름이다.
SIGTERM 수신
-> 현재 처리 위치 저장
-> 임시 상태 flush
-> 작업 중단 로그 기록
-> 종료
이렇게 해두면 다음 실행 시 이어서 처리할 수 있다.
실무에서 “재시작 가능성”은 단순 편의가 아니라 운영 안정성 그 자체다. 특히 대용량 처리에서는 처음부터 다시 돌리는 비용이 매우 크기 때문에, Signal 기반 종료 설계가 중요하다.
3) 컨테이너 종료 처리
Docker와 Kubernetes에서는 Signal을 모르면 종료 동작을 제대로 이해할 수 없다.
컨테이너를 멈춘다고 해서 런타임이 처음부터 SIGKILL을 보내는 것은 아니다. 일반적으로는 먼저 SIGTERM을 보내고, 정해진 grace period 동안 정상 종료를 기다린다. 그 시간 안에 종료되지 않으면 그제서야 SIGKILL을 보낸다.
구조는 보통 이렇다.
컨테이너 종료 요청
-> PID 1 프로세스에 SIGTERM 전달
-> grace period 대기
-> 종료 안 되면 SIGKILL
이 설계의 의미는 명확하다.
컨테이너 안에서 실행 중인 애플리케이션에게 먼저 정리 기회를 주는 것이다. 예를 들어 웹 서버라면 요청 수신을 멈추고 진행 중 요청을 처리해야 하고, 워커라면 현재 작업을 체크포인트로 남겨야 하며, 메시지 소비자라면 offset commit이나 safe stop이 필요할 수 있다.
여기서 흔한 문제가 하나 있다.
컨테이너 안의 PID 1 프로세스가 Signal을 제대로 전달받지 못하거나 처리하지 않는 경우다. 예를 들어 shell wrapper를 잘못 써서 실제 애플리케이션이 Signal을 받지 못하면, graceful shutdown이 동작하지 않는다. 그래서 Dockerfile의 ENTRYPOINT 설계, init 프로세스 사용 여부, exec form 사용 같은 것이 실무적으로 중요해진다.
즉, 컨테이너 운영은 결국 Signal 전달 경로 설계 문제이기도 하다.
4) 헬스체크 및 프로세스 제어 자동화
운영 환경에서는 프로세스를 시작하는 것만큼 상태를 제어하는 것도 중요하다.
헬스체크 실패, 비정상 상태 감지, 리소스 초과, 일정 시간 이상 무응답 같은 조건이 생기면 프로세스를 재시작하거나 중단해야 할 수 있다. 이때 Signal은 자동화 시스템이 프로세스를 제어하는 가장 기본적인 인터페이스다.
예를 들어 워커 프로세스가 응답은 없지만 아직 완전히 hang 상태는 아닐 때, 바로 SIGKILL을 쓰기보다 SIGTERM으로 종료를 요청하고 일정 시간 대기할 수 있다. 일시 중단이 필요하면 SIGSTOP, 다시 실행하면 SIGCONT를 사용할 수도 있다. 즉, Signal은 운영 자동화의 제어 레버 역할을 한다.
이런 구조는 단순히 프로세스를 죽이는 것보다 훨씬 세밀하다.
운영 시스템은 종료, 중단, 재개, 재기동, 대기 시간, 실패 시 강제 제거 같은 정책을 조합할 수 있다. 그리고 이 모든 흐름은 결국 Signal 위에서 움직인다.
6. 흔한 실수
실수 1. kill = 강제 종료라고 생각하는 경우
가장 흔한 오해다.
많은 사용자가 kill <PID>를 보면 이름 때문에 무조건 “즉시 죽인다”라고 생각한다. 하지만 기본 kill은 강제 종료가 아니다. 별도 옵션을 주지 않으면 기본적으로 SIGTERM을 보낸다.
kill <PID>
이 명령은 사실상 다음과 같다.
kill -TERM <PID>
결과적으로 프로세스가 바로 죽지 않을 수도 있다.
왜냐하면 이건 커널이 강제로 제거하는 것이 아니라 종료 요청이기 때문이다. 프로세스가 현재 정리 작업을 수행하거나, handler를 통해 다른 동작을 하거나, 심지어 무시할 수도 있다.
따라서 kill이라는 이름에 끌려 “왜 안 죽지?”라고 반응하는 것은 구조를 잘못 이해한 것이다.
올바른 해석은 이것이다.
kill 명령은 프로세스를 죽이는 명령이 아니라, Signal을 보내는 명령이다.이 차이를 이해해야 이후의 모든 Signal 동작이 자연스럽게 연결된다.
실수 2. SIGKILL을 항상 사용하는 경우
문제가 생기면 곧바로 kill -9부터 치는 습관은 위험하다.
이 방식은 당장은 빠르고 확실해 보인다. 하지만 그 확실함은 cleanup 기회를 아예 제거하는 방식으로 얻는 것이다. 즉, 문제를 해결하는 것이 아니라 단지 프로세스를 보이지 않게 만들 뿐이다.
kill -9 <PID>
이 명령은 강력하지만, 동시에 가장 거칠다.
프로세스는 종료 전에 아무런 정리도 하지 못한다. 따라서 로그 flush, 상태 저장, 락 해제, 네트워크 종료, 임시 파일 삭제, 자식 프로세스 정리 같은 것을 기대할 수 없다.
올바른 순서는 보통 이렇다.
kill -TERM <PID>
sleep 5
kill -KILL <PID> # 여전히 살아있을 때만
즉, 먼저 정상 종료를 요청하고, 필요한 시간 동안 기다린 뒤, 최후 수단으로만 강제 종료해야 한다.
이 순서를 무시하면 “잘 종료는 되는데 가끔 데이터가 꼬인다”, “락 파일이 남는다”, “재기동 후 이상 상태가 보인다” 같은 문제를 스스로 만드는 셈이다.
실수 3. Signal을 단순 종료 기능으로만 보는 경우
Signal은 상태 전이 구조다.
그런데 많은 설명이 종료 예제에만 집중하다 보니, 사용자는 Signal을 “중단 신호 몇 개” 정도로만 받아들인다. 하지만 실제로는 foreground, background, stopped 상태와 직접 연결되어 있고, Job Control과 함께 봐야 전체 구조가 보인다.
예를 들어 다음처럼 백그라운드로 작업을 보냈다고 하자.
command &
이후 사용자는 단순히 “뒤에서 돈다”라고 생각하기 쉽다. 하지만 실제 셸은 job을 관리하고 있고, 필요하면 SIGSTOP으로 멈추고 SIGCONT로 재개할 수 있으며, foreground로 다시 가져올 수도 있다. 즉, 실행 상태 제어의 핵심은 Signal이다.
Signal을 상태 전이 구조로 이해하지 못하면, Ctrl + C, Ctrl + Z, bg, fg, kill, jobs가 따로따로 보인다.
하지만 이들을 하나의 구조로 보면 훨씬 명확해진다.
Ctrl + C->SIGINTCtrl + Z-> stop 관련 Signalbg-> background 재개fg-> foreground 전환kill-> 특정 Signal 전송
즉, 셸 작업 제어는 전부 Signal 기반이다.
실수 4. trap 없이 종료 처리하는 경우
셸 스크립트는 생각보다 자주 운영 스크립트 역할을 한다.
배치 실행, 파일 처리, 백업, 동기화, 락 관리, 임시 디렉토리 생성, 자식 프로세스 실행 같은 작업을 맡는다. 그런데 많은 스크립트가 종료 시 cleanup을 전혀 고려하지 않는다.
예를 들어 이런 스크립트가 있다고 하자.
#!/bin/bash
touch /tmp/my.lock
while true; do
sleep 1
done
이 스크립트가 종료되더라도 락 파일은 그대로 남는다.
이후 다음 실행이 “이미 실행 중”으로 오판할 수 있다. 임시 파일도 마찬가지고, 백그라운드로 띄운 자식 프로세스도 마찬가지다. 즉, 종료를 고려하지 않은 스크립트는 항상 운영 흔적을 남긴다.
이 문제를 해결하는 기본 도구가 trap이다.
trap 'rm -f /tmp/my.lock; exit' SIGINT SIGTERM EXIT
이렇게 해두면 스크립트 종료 시점에 cleanup을 강제할 수 있다.
작은 차이처럼 보이지만, 실무에서는 이런 차이가 반복 실행 안정성과 장애 복구성을 결정한다.
실수 5. SIGTERM과 SIGINT를 같은 것으로 보는 경우
둘 다 “종료 비슷한 것”처럼 보여서 같은 취급을 하는 경우가 많다.
하지만 의도는 다르다. SIGINT는 보통 사용자 인터럽트다. 즉, 인터랙티브 환경에서 사용자가 지금 실행 중인 작업을 끊고 싶다는 뜻이다. 반면 SIGTERM은 시스템이나 운영 도구가 정상 종료를 요청하는 의미에 가깝다.
이 차이를 무시하고 두 신호를 완전히 동일하게 처리하면, 사용자 취소와 시스템 종료 요청을 구분할 수 없어진다. 예를 들어 배치 작업에서는 사용자 취소는 즉시 중단하되, 시스템 종료 요청은 checkpoint를 남기고 종료하고 싶을 수 있다. 웹 서버에서도 터미널 인터럽트와 서비스 종료 요청의 의미는 다를 수 있다.
따라서 둘은 비슷해 보여도 운영 의도가 다르다는 점을 기억해야 한다.
Signal 이름만 아는 것보다 중요한 것은 누가, 어떤 맥락에서, 왜 보냈는가를 이해하는 것이다.
7. 관련 개념
Signal을 제대로 이해하려면 몇 가지 주변 개념도 함께 연결해야 한다.
먼저 kill은 Signal을 보내는 명령어다.
이름 때문에 오해를 부르지만, 본질은 프로세스 종료 명령이 아니라 신호 전송 명령이다. 기본값이 SIGTERM일 뿐이고, 원하는 다른 Signal을 보낼 수도 있다.
kill -TERM <PID>
kill -KILL <PID>
kill -STOP <PID>
kill -CONT <PID>
다음으로 Job Control은 프로세스 상태 관리 구조다.
foreground, background, stopped 상태는 셸이 job 단위로 관리하며, 이 상태 전이는 Signal 없이는 설명할 수 없다. 즉, jobs, fg, bg, Ctrl + Z, Ctrl + C는 전부 Signal 기반 동작이다.
PID는 Signal의 대상 식별자다.
프로세스는 커널 안에서 PID로 식별되고, Signal은 결국 특정 PID 또는 프로세스 그룹을 향해 전달된다. 따라서 ps, pgrep, /proc, 프로세스 그룹 개념까지 연결하면 Signal의 전달 대상을 더 정확하게 이해할 수 있다.
daemon은 Signal 기반으로 제어되는 백그라운드 프로세스다.
대부분의 데몬이나 서비스 프로세스는 SIGTERM으로 종료되고, 필요하면 설정 재적용이나 재시작 관련 Signal도 받는다. 즉, 서비스 운영은 사실상 “적절한 Signal을 보내고, 적절히 처리하게 만드는 구조”다.
실제로 Signal 목록을 확인하고 싶다면 다음 명령을 쓸 수 있다.
kill -l
출력 예시는 시스템마다 조금 다를 수 있지만 대체로 다음과 비슷하다.
1) SIGHUP 2) SIGINT 3) SIGQUIT 9) SIGKILL 15) SIGTERM
19) SIGSTOP 18) SIGCONT ...
또 특정 프로세스 상태를 확인하려면 이런 명령이 자주 쓰인다.
ps -ef | grep myapp
ps -o pid,ppid,stat,cmd -p <PID>
여기서 STAT 값에 T가 보이면 stopped 상태일 수 있다. 즉, Signal에 의해 정지된 상태 여부를 추정하는 데도 도움이 된다.
8. 더 깊이 보기
Signal은 단순한 이벤트처럼 보이지만, 실제로는 커널이 관리하는 비동기 제어 구조다.
프로세스는 자기 코드 흐름에 따라 CPU를 쓰며 실행되지만, 운영체제는 언제든 외부에서 상태 변경 요청을 넣을 수 있다. 사용자 입력, 다른 프로세스의 제어 요청, 커널 내부 조건, 터미널 이벤트 등 다양한 원인으로 Signal이 발생한다.
핵심 흐름은 보통 다음과 같다.
1. Signal 발생
- 사용자 입력
- 다른 프로세스의 kill 호출
- 커널 내부 이벤트
- 터미널 제어
2. 커널이 대상 프로세스(또는 프로세스 그룹)에 전달
3. 대상 프로세스가 처리
- 등록된 handler 실행
- 무시
- default action 수행
여기서 중요한 점은 Signal이 “마치 즉시 인터럽트 코드가 실행되는 것처럼” 보이지만, 실제 처리는 그렇게 단순하지 않다는 것이다. 커널은 프로세스에 Signal이 pending 상태임을 표시하고, 적절한 시점에 프로세스가 그것을 처리하도록 한다. 즉, 개념적으로는 비동기 이벤트지만, 실제 handler 실행은 프로세스 컨텍스트 안에서 이루어진다.
이 지점 때문에 Signal handler 설계는 생각보다 까다롭다.
C 같은 저수준 환경에서는 handler 안에서 무엇을 해도 되는지 제한이 있고, reentrancy나 async-signal-safe 함수 문제도 생긴다. 고수준 언어나 프레임워크에서는 이 복잡도를 일부 감춰주지만, 본질적으로는 “아무 때나 들어올 수 있는 외부 제어 이벤트”라는 점이 달라지지 않는다.
또한 모든 Signal이 동일하지 않다.
가장 중요한 구분은 처리 가능한 Signal과 무시 불가능한 Signal이다.
처리 가능한 Signal의 대표 예:
SIGTERMSIGINTSIGHUPSIGUSR1SIGUSR2
이들은 보통 handler를 등록하거나 무시하거나 기본 동작을 따를 수 있다.
무시 불가능한 대표 Signal:
SIGKILLSIGSTOP
이 둘은 사용자 코드가 거부할 수 없다.
이 구분이 존재하는 이유는 시스템 안정성 때문이다. 만약 모든 Signal을 프로세스가 무시할 수 있다면, 비정상 프로세스를 운영체제가 통제할 방법이 없어진다. 따라서 최소한의 강제 제어 수단은 커널이 반드시 보장해야 한다.
이제 Signal을 셸 구조와 함께 다시 보면 더 선명해진다.
사용자가 터미널에서 명령을 실행하면 셸은 프로세스를 만들고 foreground로 두거나 background로 보낸다. 사용자가 Ctrl + C를 누르면 foreground job에 인터럽트가 전달된다. Ctrl + Z를 누르면 정지 관련 Signal이 전달된다. bg와 fg는 정지된 job을 재개하고 전환한다. 즉, 셸이 하는 작업 제어 대부분은 프로세스 생성과 Signal 전달의 조합이다.
컨테이너 환경도 마찬가지다.
Docker나 Kubernetes가 프로세스를 종료할 때도 본질은 Signal 전달이다. systemd가 서비스를 정지할 때도 그렇다. 프로세스 매니저, 오케스트레이터, 셸, 사용자 입력, 운영 자동화 스크립트까지 모두 동일한 인터페이스를 공유한다. 이게 Signal 구조가 가진 강력함이다.
결론적으로 Signal은 다음 세 가지를 동시에 만족시키는 구조다.
- 프로세스 제어
- 시스템 안정성
- 운영 자동화
즉, 단순히 “프로세스에 뭔가 보내는 기능”이 아니라, 유닉스 계열 운영체제가 실행 중인 프로그램을 다루는 방식 자체를 구성하는 핵심 메커니즘이다.
9. 정리
Signal은 리눅스에서 실행 중인 프로세스를 외부에서 제어하기 위한 핵심 메커니즘이다.
프로세스는 자기 코드 흐름에 따라 계속 실행되지만, 현실의 시스템은 그 흐름 바깥에서 중단, 종료, 재개, 상태 변경 요청을 넣어야 한다. Signal은 바로 그 요청을 전달하는 운영체제 수준의 표준 인터페이스다.
SIGINT, SIGTERM, SIGKILL은 모두 종료와 관련되어 보이지만 역할은 다르다.SIGINT는 사용자 인터럽트이고, SIGTERM은 정상 종료 요청이며, SIGKILL은 강제 제거다. 여기서 가장 중요한 것은 강제 종료가 기본이 아니라 제어 가능한 종료가 기본 구조라는 점이다. 즉, 먼저 요청하고, 정리 기회를 주고, 마지막에만 강제 제거한다.
이 관점을 잡으면 여러 실무 개념이 한 번에 연결된다.
셸에서 Ctrl + C가 왜 동작하는지, kill이 왜 기본적으로 강제 종료가 아닌지, trap이 왜 필요한지, Job Control이 왜 Signal과 연결되는지, Docker와 Kubernetes가 왜 먼저 SIGTERM을 보내는지가 모두 자연스럽게 이해된다.
실전에서 기억해야 할 최소 원칙은 분명하다.
# 1. 정상 종료를 먼저 시도한다
kill -TERM <PID>
# 2. 종료를 기다린다
# 3. 끝까지 안 죽을 때만 강제 종료한다
kill -KILL <PID>
그리고 스크립트나 서버 프로세스를 작성할 때는 종료를 “언젠가 끝난다”가 아니라 “외부에서 Signal로 종료 요청이 올 수 있다”는 전제 위에서 설계해야 한다.
그래야 임시 파일, 락, 트랜잭션, 커넥션, 로그, 체크포인트 같은 실제 운영 문제가 제어 가능해진다.
결국 Signal의 핵심은 이것이다.
Signal은 프로세스를 죽이는 기술이 아니라, 실행 중인 프로세스를 통제 가능한 방식으로 다루게 만드는 구조다.
이 개념을 기준으로 보면 리눅스의 프로세스 제어 흐름은 훨씬 단순해진다.
프로세스는 실행되고, 셸과 시스템은 Signal로 상태를 바꾸며, 프로그램은 그 신호를 받아 적절히 반응한다. Job Control, 프로세스 구조, 컨테이너 종료, 운영 자동화까지 이어지는 전체 흐름도 결국 이 원리 위에 서 있다.