리눅스 - SIGTERM 과 SIGKILL ?!, 근데 Signal 을 곁들인 (ps. kill -9 만 쓰시나요?)

정현우·2025년 3월 23일
4

Linux Basic to Advanced

목록 보기
16/17
post-thumbnail

[ 글의 목적: 리눅스 OS 에서 process 를 kill (IPC 중 signal 방식) 할때 SIGTERM, SIGKILL 동작에 대한 기록 ]

SIGTERM과 SIGKILL에 대한 심층 분석

습관적으로 pkill -9 ... 을 때리다가 어떻게 OS 는 process를 "safe" 한 방식으로 죽일 수 있을까 부터 시작해 UNIX/Linux 의 Signal IPC 까지 올라가서 러프하게 정리한 글이다.

한 번 상상해보자. python 으로 작성한 a.pywhile True 가 있어도, Ctrl + C 한 방이면 KeyboardInterrupt 발생하면서 stop 되고 결국 프로세스는 죽는다. 이 흐름을 정말 A to Z 까지 알고 있는가? 이게 왜 중요한가? 우린 OS 의 "프로세스" 와 "쓰레드" 의 생명주기와 IPC 는 기억하지만, 이 흐름은 놓치고 있는 경우가 많다. (근데 나도 몰랐다.)

  • 썸네일은 gpt 와 claude 의 콜라보다. 우측 하단의 "쾅" 이 클로드인데, 역시 튜닝의 끝은 순정인가,, 잼민이 감성..

Python 의 KeyboardInterrupt 이어서..

  1. 위에 얘기한 흐름을 다시 정리해보자면, 우리는 shell 에서 python 을 실행한다. python a.py 와 같이.

  2. 해당 a.pypython 이라는 S/W 를 기반으로 runtime 이 구성되고, python interpreter 가 실행되며 foreground 프로세스로 실행 한다.

  3. 이 상태에서 Ctrl + C를 누르면, 터미널은 이를 특수 제어 문자(ETX - End of Text, 0x03)로 인식한다. (이는 python 에서 인식하는게 아니라 OS 에서 인식한다!!)

  4. 터미널은 이 키 입력(0x03)을 감지하면 foreground에서 실행 중인 프로세스(예: Python)에 SIGINT 시그널을 보낸다. 이건 POSIX 표준이자 리눅스/유닉스/macOS에서 동일하게 작동하는 규칙 이다.

  5. 이때 Python 인터프리터는 자체 등록한 SIGINT 핸들러를 통해 KeyboardInterrupt 예외를 발생시킨다.

  6. Python은 인터프리터(ex - CPython 구현체)가 시작될 때 signal 모듈을 통해 자체적으로 SIGINT 에 대한 핸들러를 기본으로 등록해 두고 있다.

  • 이 예시외에도 nginx 같은 S/W level 의 web-server process 를 죽일때, gunicorn 이나 apache & tomcat 으로 돌아가는 spring server 를 죽일때도 비슷한 흐름이다.

  • 이제부터 Signal 에 대해서 좀 더 자세하게 알아보자. 가장 먼저 종료 signal 의 대표적인 예시인 SIGTERMSIGKILL 차이점은 아래와 같다.

SIGINT, SIGTERM, SIGKILL 비교표

  • 셋 다 프로세스를 멈추는 신호(Signal)이며, SIGTERM, SIGKILL 은 "프로세스 종료" 에 더 가깝다. 여기서는 사실 SIGTERM, SIGKILL 에 대해 더 자세히 알아보고자 한다.
구분SIGINT (2)SIGTERM (15)SIGKILL (9)
의미사용자 중단 요청 (Interrupt)정상 종료 요청 (Termination Request)강제 종료 (Forced Kill)
발생 시점주로 사용자가 Ctrl+C 입력일반적으로 kill 명령 사용 시 기본값kill -9 또는 시스템 강제 종료 시
기본 동작프로세스에 인터럽트 요청 → 정상적인 종료 처리 실행종료 핸들러 실행 기회 제공즉시 강제 종료
프로세스 대응무시 가능, 시그널 핸들러로 처리 가능무시 가능, 시그널 핸들러로 처리 가능무시 불가능 (non-maskable, non-catchable)
우선순위사용자 개입 수준일반 종료 시퀀스최우선, 무조건 종료
리소스 정리가능 (시그널 핸들러에서 정리 가능)가능 (핸들러 내부에서 리소스 정리 수행 가능)불가능 (리소스 누수 위험 있음)
커널 개입커널이 시그널 전달 → 프로세스 핸들링커널이 시그널 전달 → 프로세스 핸들링커널이 직접 종료 (컨텍스트 무시하고 즉시 종료)
사용 예시사용자가 Ctrl+C 입력kill PID (기본값)kill -9 PID
시스템 콜kill(pid, SIGINT)kill(pid, SIGTERM)kill(pid, SIGKILL)
프로세스 상태 전이Running → Signal Handling → (계속/종료)Running/Waiting → Terminating → Zombie → RemovedRunning/Waiting → Zombie → Removed
자식 프로세스 처리일반적으로 전파되지 않음애플리케이션 설정에 따라 자식에게도 전파 가능자식 프로세스 포함 전체 강제 종료 (kill -9 트리 구조 시)
비동기성처리 핸들링 가능 (시그널 핸들러 등록)비동기적으로 처리 가능즉시 처리됨, 핸들링 불가
  • SIGTERM 은 "정중한 종료 요청"으로, 프로세스에게 "작업을 마무리하고 종료해달라"는 의미이며, SIGKILL 은 "즉각적인 강제 종료 명령"으로 프로세스에게 어떤 기회도 주지 않고 즉시 종료시킨다.

1. 프로세스 간 통신(IPC)과 시그널

1) IPC의 개념과 종류

  • 프로세스 간 통신(Inter-Process Communication, IPC)은 서로 다른 프로세스가 데이터를 교환하고 동기화하는 메커니즘이다. UNIX/Linux 시스템에서 IPC의 주요 방식은 다음과 같다.
  1. 파이프(Pipe)와 명명된 파이프(Named Pipe)

    • 단방향 데이터 흐름을 제공
    • 예: | 연산자를 사용한 파이프라인
  2. 메시지 큐(Message Queue)

    • 비동기적 메시지 전달 시스템
    • msgget(), msgsnd(), msgrcv() 시스템 콜 사용
  3. 공유 메모리(Shared Memory)

    • 여러 프로세스가 동일한 메모리 영역에 접근
    • 가장 빠른 IPC 방식
  4. 세마포어(Semaphore)

    • 공유 자원에 대한 접근 제어
    • 상호 배제(mutual exclusion)를 보장
  5. 소켓(Socket)

    • 네트워크를 통한 프로세스 간 통신
    • 로컬 및 원격 통신 모두 지원
  6. 시그널(Signal)

    • 비동기적인 이벤트 알림
    • 프로세스 제어 및 예외 상황 처리에 사용

2) 시그널의 특징과 역할

  • 시그널은 UNIX/Linux 시스템에서 가장 오래된 IPC 메커니즘 중 하나로, 다음과 같은 특징이 있다.
    • 비동기성: 프로세스가 어떤 작업을 하고 있든 상관없이 전달 가능
    • 경량성: 시그널은 단순한 정수 값으로, 오버헤드가 적음
    • 이벤트 드리븐: 특정 이벤트 발생 시 프로세스에 알림을 제공
    • 제한된 정보: 시그널 번호 외에 추가 데이터를 전달하기 어려움

3) 주요 시그널 종류

신호 이름번호설명기본 동작원인
SIGHUP1행업(Hangup)종료제어 터미널이 종료될 때
SIGINT2인터럽트종료Ctrl+C 키 입력
SIGQUIT3종료 및 코어 덤프코어 덤프와 함께 종료Ctrl+\ 키 입력
SIGILL4잘못된 명령어코어 덤프와 함께 종료잘못된 CPU 명령 실행
SIGTRAP5트랩코어 덤프와 함께 종료디버깅용 트랩
SIGABRT6중단코어 덤프와 함께 종료abort() 함수 호출
SIGFPE8부동 소수점 예외코어 덤프와 함께 종료0으로 나누기 등 연산 오류
SIGKILL9강제 종료종료 (무시 불가)관리자 권한으로 강제 종료
SIGUSR110사용자 정의 1종료사용자/애플리케이션 정의
SIGSEGV11세그멘테이션 오류코어 덤프와 함께 종료잘못된 메모리 참조
SIGUSR212사용자 정의 2종료사용자/애플리케이션 정의
SIGPIPE13파이프 깨짐종료닫힌 파이프에 쓰기 시도
SIGALRM14알람종료alarm() 함수로 설정된 타이머 만료
SIGTERM15종료종료kill 명령의 기본값
SIGCHLD17자식 상태 변경무시자식 프로세스가 종료되거나 중지될 때
SIGCONT18계속 실행중단된 프로세스 재개중단된 프로세스를 계속 실행
SIGSTOP19중지프로세스 중지 (무시 불가)프로세스 중지
SIGTSTP20터미널 중지프로세스 중지Ctrl+Z 키 입력
SIGTTIN21터미널 입력프로세스 중지백그라운드 프로세스가 터미널에서 읽기 시도
SIGTTOU22터미널 출력프로세스 중지백그라운드 프로세스가 터미널에 쓰기 시도
  • SIGUSR1 로 재미있는 걸 해볼 수 있지 않을까?

2. OS 관점에서 SIGTERMSIGKILL 의 차이

정확하겐 POSIX 표준에 정의된 시그널 관련 시스템 콜과 라이브러리 함수를 해당 레포에서 직접 확인할 수 있다. - https://github.com/torvalds/linux?tab=readme-ov-file

1) SIGTERM (15) - 정상적인 종료 요청

  1. 시그널 전송 과정

    • 사용자가 kill PID를 실행하면, 커널의 kill() 시스템 콜이 호출된다.
    • 시스템 콜 인터페이스를 통해 커널 공간으로 전환된다.
    • 커널은 프로세스 테이블에서 PID에 해당하는 프로세스를 찾는다.
  2. 시그널 전달 및 큐잉

    • 커널은 해당 프로세스의 task_struct 내의 시그널 마스크와 핸들러 정보를 확인한다.
    • SIGTERM 시그널이 해당 프로세스의 시그널 큐(sigqueue)에 추가된다.
    • 프로세스의 시그널 큐는 보류 중인 시그널(pending signals)을 관리한다.
  3. 컨텍스트 스위칭과 시그널 처리

    • 다음 스케줄링 시점에 프로세스가 CPU를 획득하면, 커널은 해당 프로세스의 시그널 큐를 검사한다.
    • SIGTERM 이 발견되면, 커널은 다음을 확인한다:
      • 시그널이 블록되었는지 (프로세스가 sigprocmask()로 블록했는지)
      • 사용자 정의 핸들러가 등록되었는지 (sigaction()으로 설정)
  4. 핸들러 실행

    • 만약 SIGTERM 핸들러가 등록되어 있다면
      • 커널은 사용자 공간의 스택에 시그널 프레임을 추가한다.
      • 프로세스의 실행 컨텍스트를 시그널 핸들러로 변경한다.
      • 프로세스는 핸들러 내에서 정리 작업을 수행한다:
        • 열린 파일 디스크립터 닫기 (close())
        • 데이터베이스 커넥션 종료
        • 임시 파일 삭제
        • 로그 파일에 종료 메시지 기록
        • 자식 프로세스에게 종료 시그널 전파
    • 시그널 핸들러가 완료된 후, 프로세스는 인터럽트되었던 지점(또는 시그널 핸들러 반환 후의 지점)으로 돌아간다!
  5. 프로세스 종료

    • 핸들러 실행 후, 애플리케이션은 일반적으로 exit() 시스템 콜을 호출하여 정상 종료한다.
    • 만약 핸들러가 없거나 시그널이 무시되지 않았다면, 기본 동작(default action)인 종료가 수행된다.
    • 프로세스 종료 시 다음 과정이 진행된다:
      • 모든 스레드 종료
      • 프로세스 리소스 정리 (메모리, 파일 핸들 등)
      • 부모 프로세스에 SIGCHLD 시그널 전송
      • 프로세스 상태가 좀비(zombie) 상태로 전환
  6. 좀비 프로세스 정리

    • 부모 프로세스가 wait() 또는 waitpid()를 호출하면, 자식의 종료 상태를 회수하고 프로세스 테이블 엔트리가 완전히 제거된다.
    • 만약 부모가 wait()를 호출하지 않으면, 좀비 프로세스가 남게 된다.

2) SIGKILL (9) - 강제 종료

  1. 시그널 전송 과정:

    • 사용자가 kill -9 PID를 실행하면, 역시 kill() 시스템 콜이 호출된다.
    • 커널 공간으로 전환되어 프로세스 테이블을 검색한다.
  2. 특수 처리

    • SIGKILL 은 특별한 시그널로 커널은 이를 프로세스의 시그널 큐에 넣지 않는다.
    • 대신, 즉시 프로세스 종료 절차를 시작한다! 프로세스의 시그널 마스크나 핸들러 설정을 확인하지 않는다! (무시할 수 없는 시그널)
  3. 강제 종료 프로세스:

    • 커널은 프로세스의 모든 스레드에 대해 즉시 실행을 중단시킨다.
    • do_exit() 커널 함수가 호출되어 프로세스 종료 절차를 진행한다:
      • 프로세스의 가상 메모리 공간을 해제한다.
      • 열린 파일 디스크립터를 강제로 닫는다.
      • 시스템 V IPC 리소스를 해제한다.
      • 프로세스가 소유한 세마포어를 해제한다.
      • 프로세스 상태를 EXIT_ZOMBIE 로 변경한다.
      • 부모 프로세스에 SIGCHLD 시그널을 보낸다.
  4. 사용자 공간 코드 실행 없음

    • SIGKILL 은 프로세스의 사용자 공간 코드가 실행될 기회를 전혀 주지 않는다.
    • 이는 애플리케이션이 자신의 리소스를 정리할 수 없음을 의미한다.
    • 커널만이 프로세스의 커널 리소스(파일 핸들, 메모리 등)를 정리한다.
    • PS) 그러니까 SIGTERM call 이 더 안전한 종료 접근법이다!!
  5. 좀비 프로세스와 후속 처리:

    • SIGTERM 과 마찬가지로, 프로세스는 부모가 wait()를 호출할 때까지 좀비 상태로 남는다.
    • 부모 프로세스가 이미 종료된 경우, init 프로세스(PID 1)가 고아 프로세스를 자식으로 대려와 좀비를 정리한다.

3) 두 시그널의 핵심 차이점

  1. 실행 컨텍스트

    • SIGTERM: 프로세스의 사용자 공간 코드(시그널 핸들러)가 실행된다.
    • SIGKILL: 전적으로 커널 공간에서 처리되며, 사용자 코드는 실행되지 않는다.
  2. 리소스 정리

    • SIGTERM: 애플리케이션이 자신의 리소스(임시 파일, 네트워크 연결, 데이터베이스 트랜잭션 등)를 정리할 수 있다.
    • SIGKILL: 애플리케이션 수준의 리소스 정리가 불가능하며, 커널 수준의 리소스만 정리된다.
  3. 실행 시간

    • SIGTERM: 핸들러 실행과 정리 과정으로 인해 종료에 시간이 걸릴 수 있다.
    • SIGKILL: 즉시 종료되어 지연이 최소화된다.
  4. 안전성

    • SIGTERM: 정상적인 종료 절차를 통해 데이터 무결성을 보장할 가능성이 높다.
    • SIGKILL: 데이터 손실이나 불일치, 네트워크 연결 문제 등을 야기할 수 있다.
  • 사실 이 때문에 두 시그널을 두고 다양한 밈들이 있다.

4) 우아한 종료(Graceful Shutdown)의 중요성

  1. 데이터 무결성 보장

    • 불완전한 작업이 없도록 보장
    • 트랜잭션 완료 또는 롤백
    • 디스크 버퍼 플러시
  2. 클라이언트 영향 최소화

    • 기존 연결의 적절한 종료
    • 오류 메시지 대신 정상 응답 제공
    • 세션 데이터 보존
  3. 분산 시스템 일관성

    • 클러스터 노드 간 상태 동기화
    • 리더 선출 또는 장애 조치 트리거
    • 로드 밸런서에서 노드 제거
  • 그러니 우리가 만약 응용 프로그램을 직접 만들게 된다면, 이 "Graceful Shutdown" 을 위한 SIGTERM 핸들러를 직접 추가해서 처리하는 방향으로 접근해보자!

5) 종료 시간 제한(Timeout)의 설정

  • 대부분의 시스템에서는 우아한 종료에 시간 제한을 두는 것이 중요하다.
  1. Kubernetes의 종료 프로세스

    • Pod가 SIGTERM 을 받은 후 terminationGracePeriodSeconds 동안 기다린다 (기본 30초).
    • 시간이 초과되면 SIGKILL 을 보내 강제 종료한다.
  2. Systemd의 종료 프로세스

    • TimeoutStopSec 설정으로 종료 타임아웃을 지정 (기본 90초).
    • 타임아웃 후 SIGKILL 을 보낸다.
  3. Docker의 종료 프로세스

    • docker stop 명령은 컨테이너에 SIGTERM을 보낸다.
    • 기본적으로 10초 후 SIGKILL 을 보낸다.
    • docker stop --time=<seconds> 옵션으로 타임아웃 조정 가능.
  4. 적절한 타임아웃 설정 전략

    • 애플리케이션의 일반적인 정리 시간을 측정
    • 최악의 경우 시나리오(많은 연결, 큰 트랜잭션 등) 고려
    • 마진을 추가하되, 너무 길지 않게 설정
    • 배포 중단 시 전체 시스템 영향 고려

그러니 이제 SIGTERM 을 위해 kill PID 을 기본으로 사용하되, 경우에 따라 kill -9 PID 를 사용하는게 어떨까? to. 스스로에게...


4. shell script로 실습!

1) SIGTERM을 먼저 사용하기

  • 효과적인 프로세스 종료를 위한 최선의 방법!
  1. 단계적 접근
# 1. 먼저 SIGTERM으로 정상 종료 시도
kill <PID>

# 2. 일정 시간 대기 (5-10초)
sleep 10

# 3. 프로세스가 여전히 실행 중인지 확인
if ps -p <PID> > /dev/null; then
    echo "Process still running, sending SIGKILL..."
    kill -9 <PID>
else
    echo "Process terminated gracefully."
fi
  1. 스크립트 자동화
#!/bin/bash

terminate_with_timeout() {
    local pid=$1
    local timeout=${2:-30}  # 기본 30초 타임아웃
    
    # 프로세스가 존재하는지 확인
    if ! ps -p $pid > /dev/null; then
    echo "Process $pid does not exist."
    return 0
    fi
    
    # SIGTERM 전송
    echo "Sending SIGTERM to process $pid..."
    kill $pid
    
    # 타임아웃 내에 종료되는지 확인
    local count=0
    while ps -p $pid > /dev/null && [ $count -lt $timeout ]; do
    sleep 1
    count=$((count + 1))
    echo "Waiting for process to terminate: $count/$timeout seconds"
    done
    
    # 여전히 실행 중이면 SIGKILL 전송
    if ps -p $pid > /dev/null; then
    echo "Process still running after $timeout seconds, sending SIGKILL..."
    kill -9 $pid
    return 1
    else
    echo "Process terminated gracefully."
    return 0
    fi
}

# 사용 예: terminate_with_timeout <PID> [timeout_in_seconds]

2) 그러니, 다시 정리하는 SIGKILL을 남용하지 말아야 하는 이유!

  • 여러분들의 응용프로그램에게 SIGKILL 의 남용이 초래할 수 있는 문제들!
  1. 데이터 일관성 문제

    • 데이터베이스 트랜잭션 중단으로 인한 데이터 불일치
    • 파일 쓰기 작업 중 강제 종료로 인한 파일 손상
    • 캐시와 영구 저장소 간의 불일치 발생
  2. 리소스 누수

    • 임시 파일이 제거되지 않음
    • 공유 메모리 세그먼트가 정리되지 않음
    • 네트워크 포트가 적절히 해제되지 않음
    • 외부 리소스 락(lock)이 해제되지 않음
  3. 분산 시스템 문제

    • 클러스터에 종료 통지가 전송되지 않음
    • 리더 선출 프로세스가 비정상적으로 트리거됨
    • 다른 노드와의 통신이 정상적으로 종료되지 않음
  4. 실제 사례 분석

    • 데이터베이스 서버의 SIGKILL 종료는 WAL(Write-Ahead Log) 손상을 초래할 수 있음
    • 메시지 큐의 SIGKILL 종료는 메시지 중복 처리나 유실을 초래할 수 있음
    • 분산 락 관리자의 SIGKILL 종료는 "뇌 분할(split-brain)" 현상을 초래할 수 있음

3) 복잡한 애플리케이션의 종료 전략

  • 다중 노드 등의 복잡한 형태, 조금 더 큰 규모의 시스템에서의 안전한 종료 전략은 자식 부터 부모로 올라오거나 치명적인 것에서 덜 치명적인 순서로 접근하는게 필요하다!
  1. 다중 계층 애플리케이션

    • 종료 순서가 중요함: 클라이언트 계층 → 애플리케이션 계층 → 데이터 계층
    • 각 계층은 다른 종료 유예 시간이 필요할 수 있음
  2. 리더-팔로워 시스템

    • 팔로워 노드 먼저 종료
    • 리더 노드는 새로운 리더 선출 후 종료
    • 리더 변경 사항이 모든 노드에 전파된 후 완전 종료
  3. 장기 실행 작업이 있는 시스템

    • 진행 중인 작업의 체크포인트 생성
    • 작업 큐의 적절한 상태 저장
    • 재시작 시 중단된 지점부터 계속할 수 있는 메커니즘 구현
  4. 데이터베이스 종료

    • 트랜잭션 커밋 또는 롤백
    • 버퍼된 데이터 디스크에 쓰기
    • 체크포인트 생성
    • 메타데이터 업데이트

출처

1) 리눅스 시그널 관련 문서

2) 관련 표준 및 사양

profile
🔥 도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결의 본질” 에 몰두하는 software/product 개발자, 정현우 입니다.

0개의 댓글

관련 채용 정보