IT·리눅스/리눅스

[Linux] 시그널 - SIGTERM, SIGKILL, SIGHUP 제대로 구분하기

jmineekim 2026. 5. 13. 10:00

신입 시절에 운영 중인 배치 프로세스가 안 죽는다고 무작정 kill -9를 날렸다가, 락이 풀리지 않고 임시 파일이 그대로 남아서 다음날 새벽에 다시 출근한 적이 있습니다. 그때 처음 알았어요. kill이 그냥 "프로세스 죽이는 명령어"가 아니라, 시그널이라는 메시지를 보내는 도구이고, 어떤 시그널을 보내느냐에 따라 결과가 완전히 달라진다는 걸요.

심지어 kill이라는 이름이 좀 오해를 부르는데, 사실 이 명령어는 "프로세스에 시그널을 전달한다"가 본질입니다. 그래서 kill -SIGUSR1 같이 종료와는 전혀 상관없는 신호도 보낼 수 있어요. 오늘은 운영 환경에서 가장 자주 마주치는 세 시그널 — SIGTERM, SIGKILL, SIGHUP — 의 차이와 graceful shutdown을 한번 정리해 볼게요.

개념 / 원리

시그널(signal)은 커널이 프로세스에게 보내는 비동기 알림입니다. 회사 책상 위에 누가 메모를 놓고 가는 것과 비슷한데요, 메모 내용에 따라 하던 일을 멈출지, 잠깐 다른 일을 할지, 그냥 무시할지를 프로세스가 결정합니다.

번호 이름 의미 잡을 수 있나? 기본 동작
1 SIGHUP 터미널 끊김 / 설정 재로드 O 종료
2 SIGINT Ctrl+C O 종료
9 SIGKILL 강제 종료 X 즉시 종료
15 SIGTERM 정상 종료 요청 (kill 기본값) O 종료
19 SIGSTOP 일시정지 X 정지

핵심은 마지막 컬럼입니다. SIGKILL과 SIGSTOP만 프로세스가 가로챌 수 없어요. 나머지는 trap으로 잡아서 원하는 동작으로 바꾸거나 무시할 수 있습니다.

SIGTERM과 SIGKILL의 차이는 한 줄로 이렇습니다.

SIGTERM은 "정리하고 나가주세요"라는 정중한 요청, SIGKILL은 커널이 프로세스를 즉사시키는 강제 종료입니다.

SIGKILL을 받으면 프로세스는 자기 코드를 한 줄도 더 실행하지 못합니다. 그래서 열어둔 파일이 fsync되지 않고, DB 커넥션은 닫히지 않은 채로 끊기고, lock 파일은 그대로 남아버려요.

SIGHUP은 원래 이름 그대로 "HangUP", 즉 터미널이 끊겼다는 신호인데, 요즘은 데몬 프로세스에서 "설정 파일 다시 읽어라"는 신호로 관습적으로 쓰입니다. nginx의 nginx -s reload도 내부적으로 SIGHUP을 사용하고, syslogd나 sshd 같은 데몬도 같은 방식으로 reload를 받습니다.

프로세스가 시그널을 받았을 때 할 수 있는 행동은 세 가지입니다. 첫째, 기본 동작에 맡긴다(보통 종료). 둘째, 무시한다(SIG_IGN). 셋째, 직접 등록한 핸들러로 가로챈다. 우리가 흔히 말하는 graceful shutdown은 세 번째 방식으로 SIGTERM을 잡아서 "정리 후 종료"를 수행하는 패턴이에요.

실전 예제

bash에서 trap으로 SIGTERM을 잡아 정리 작업을 수행하는 예제입니다. 그대로 저장해서 돌려보시면 됩니다.

#!/bin/bash
# graceful.sh
cleanup() {
  echo "[$(date +%T)] 정리 작업 시작..."
  rm -f /tmp/myapp.lock
  echo "[$(date +%T)] 종료 완료"
  exit 0
}

trap cleanup SIGTERM SIGINT

echo "PID: $$"
touch /tmp/myapp.lock
while true; do
  echo "[$(date +%T)] 작업 중..."
  sleep 2
done

다른 터미널에서 시그널을 보내며 차이를 확인해 보세요.

kill -SIGTERM <PID>   # cleanup() 실행됨, 락 파일 사라짐
kill -SIGKILL <PID>   # cleanup() 실행 안 됨, /tmp/myapp.lock 그대로 남음
kill -l               # 시스템이 지원하는 시그널 전체 목록 확인

Python으로 동일한 패턴을 구현하면 이렇습니다. 웹 서버나 워커 프로세스에서 자주 쓰는 형태예요.

import os
import signal
import sys
import time

def handler(signum, frame):
    print(f"\n시그널 {signum} 수신. 정리 후 종료합니다.")
    # DB 커넥션 close, 큐 flush, 임시 파일 정리 등
    sys.exit(0)

def reload_config(signum, frame):
    print("설정 파일 다시 읽기")

signal.signal(signal.SIGTERM, handler)
signal.signal(signal.SIGINT, handler)
signal.signal(signal.SIGHUP, reload_config)

print(f"PID: {os.getpid()}")
while True:
    print("작업 중...")
    time.sleep(2)

자주 하는 실수

1. kill -9 남용. 가장 흔한 실수입니다. 일단 안 죽으면 9번부터 보내는 분들이 많은데, 그 순간 프로세스가 들고 있던 모든 정리 로직이 사라집니다. 먼저 kill <PID> (기본 SIGTERM)를 보내고, 몇 초 기다린 뒤에도 살아 있으면 그때 -9를 고려하세요.

2. systemd 타임아웃 무시. systemctl stop은 기본적으로 SIGTERM을 보낸 뒤 TimeoutStopSec(보통 기본 90초) 만큼 기다렸다가 SIGKILL로 강제 종료합니다. 정리 작업이 90초 안에 안 끝나는 서비스라면 unit 파일에서 이 값을 늘려줘야 합니다.

3. Docker PID 1 함정. 컨테이너 안에서 앱이 PID 1로 실행되면, 커널이 PID 1에는 기본 시그널 핸들러를 자동으로 붙여주지 않습니다. 즉, SIGTERM을 명시적으로 처리하지 않은 앱은 docker stop을 받아도 무시하다가 10초 뒤 SIGKILL로 죽어버려요. 핸들러를 꼭 등록하거나 tini 같은 init을 앞에 두는 게 안전합니다.

4. SIGHUP을 종료 신호로 오해. nohup이 SIGHUP을 무시하게 만드는 명령이라는 것만 보고, SIGHUP을 "끄는 시그널"로 아는 경우가 있는데, 데몬에서는 보통 reload 용도입니다. 보내기 전에 매뉴얼을 한 번 확인하세요.

5. 핸들러 안에서 무거운 작업 수행. 시그널 핸들러는 비동기로 실행되기 때문에 그 안에서 mutex 잡고 로그를 막 쓰면 데드락이 나기도 합니다. 핸들러에서는 "종료 플래그 변수만 바꾸고", 실제 정리는 메인 루프가 그 플래그를 보고 처리하는 패턴이 가장 안전합니다.

마무리

정리하면 세 줄입니다.

  • SIGTERM은 정중한 종료 요청, SIGKILL은 즉사. 운영에서는 항상 SIGTERM부터.
  • 정리 로직은 trap(bash) 또는 signal.signal()(Python)으로 등록해서 graceful shutdown을 구현한다.
  • 컨테이너·systemd 환경에서는 시그널 타임아웃과 PID 1 동작을 꼭 체크.

"Linux", "signal", "시그널", "SIGTERM", "SIGKILL", "SIGHUP", "kill", "graceful shutdown", "trap", "프로세스"

반응형