[ 글의 목적: 리눅스 OS 에서 process 를 kill (IPC 중 signal 방식) 할때 SIGTERM, SIGKILL 동작에 대한 기록 ]
습관적으로
pkill -9 ...
을 때리다가 어떻게 OS 는 process를 "safe" 한 방식으로 죽일 수 있을까 부터 시작해 UNIX/Linux 의 Signal IPC 까지 올라가서 러프하게 정리한 글이다.
한 번 상상해보자. python 으로 작성한
a.py
에while True
가 있어도,Ctrl + C
한 방이면KeyboardInterrupt
발생하면서 stop 되고 결국 프로세스는 죽는다. 이 흐름을 정말 A to Z 까지 알고 있는가? 이게 왜 중요한가? 우린 OS 의 "프로세스" 와 "쓰레드" 의 생명주기와 IPC 는 기억하지만, 이 흐름은 놓치고 있는 경우가 많다.(근데 나도 몰랐다.)
KeyboardInterrupt
이어서..위에 얘기한 흐름을 다시 정리해보자면, 우리는 shell 에서 python 을 실행한다. python a.py
와 같이.
해당 a.py
는 python
이라는 S/W 를 기반으로 runtime 이 구성되고, python interpreter
가 실행되며 foreground 프로세스로 실행 한다.
이 상태에서 Ctrl + C를 누르면, 터미널은 이를 특수 제어 문자(ETX - End of Text, 0x03
)로 인식한다. (이는 python 에서 인식하는게 아니라 OS 에서 인식한다!!)
터미널은 이 키 입력(0x03)을 감지하면 foreground에서 실행 중인 프로세스(예: Python)에 SIGINT
시그널을 보낸다. 이건 POSIX 표준이자 리눅스/유닉스/macOS에서 동일하게 작동하는 규칙 이다.
이때 Python 인터프리터는 자체 등록한 SIGINT
핸들러를 통해 KeyboardInterrupt
예외를 발생시킨다.
Python은 인터프리터(ex - CPython 구현체)가 시작될 때 signal
모듈을 통해 자체적으로 SIGINT
에 대한 핸들러를 기본으로 등록해 두고 있다.
이 예시외에도 nginx
같은 S/W level 의 web-server process 를 죽일때, gunicorn
이나 apache & tomcat
으로 돌아가는 spring server 를 죽일때도 비슷한 흐름이다.
이제부터 Signal
에 대해서 좀 더 자세하게 알아보자. 가장 먼저 종료 signal 의 대표적인 예시인 SIGTERM
과 SIGKILL
차이점은 아래와 같다.
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 → Removed | Running/Waiting → Zombie → Removed |
자식 프로세스 처리 | 일반적으로 전파되지 않음 | 애플리케이션 설정에 따라 자식에게도 전파 가능 | 자식 프로세스 포함 전체 강제 종료 (kill -9 트리 구조 시) |
비동기성 | 처리 핸들링 가능 (시그널 핸들러 등록) | 비동기적으로 처리 가능 | 즉시 처리됨, 핸들링 불가 |
SIGTERM
은 "정중한 종료 요청"으로, 프로세스에게 "작업을 마무리하고 종료해달라"는 의미이며, SIGKILL
은 "즉각적인 강제 종료 명령"으로 프로세스에게 어떤 기회도 주지 않고 즉시 종료시킨다.파이프(Pipe)와 명명된 파이프(Named Pipe)
|
연산자를 사용한 파이프라인메시지 큐(Message Queue)
msgget()
, msgsnd()
, msgrcv()
시스템 콜 사용공유 메모리(Shared Memory)
세마포어(Semaphore)
소켓(Socket)
시그널(Signal)
신호 이름 | 번호 | 설명 | 기본 동작 | 원인 |
---|---|---|---|---|
SIGHUP | 1 | 행업(Hangup) | 종료 | 제어 터미널이 종료될 때 |
SIGINT | 2 | 인터럽트 | 종료 | Ctrl+C 키 입력 |
SIGQUIT | 3 | 종료 및 코어 덤프 | 코어 덤프와 함께 종료 | Ctrl+\ 키 입력 |
SIGILL | 4 | 잘못된 명령어 | 코어 덤프와 함께 종료 | 잘못된 CPU 명령 실행 |
SIGTRAP | 5 | 트랩 | 코어 덤프와 함께 종료 | 디버깅용 트랩 |
SIGABRT | 6 | 중단 | 코어 덤프와 함께 종료 | abort() 함수 호출 |
SIGFPE | 8 | 부동 소수점 예외 | 코어 덤프와 함께 종료 | 0으로 나누기 등 연산 오류 |
SIGKILL | 9 | 강제 종료 | 종료 (무시 불가) | 관리자 권한으로 강제 종료 |
SIGUSR1 | 10 | 사용자 정의 1 | 종료 | 사용자/애플리케이션 정의 |
SIGSEGV | 11 | 세그멘테이션 오류 | 코어 덤프와 함께 종료 | 잘못된 메모리 참조 |
SIGUSR2 | 12 | 사용자 정의 2 | 종료 | 사용자/애플리케이션 정의 |
SIGPIPE | 13 | 파이프 깨짐 | 종료 | 닫힌 파이프에 쓰기 시도 |
SIGALRM | 14 | 알람 | 종료 | alarm() 함수로 설정된 타이머 만료 |
SIGTERM | 15 | 종료 | 종료 | kill 명령의 기본값 |
SIGCHLD | 17 | 자식 상태 변경 | 무시 | 자식 프로세스가 종료되거나 중지될 때 |
SIGCONT | 18 | 계속 실행 | 중단된 프로세스 재개 | 중단된 프로세스를 계속 실행 |
SIGSTOP | 19 | 중지 | 프로세스 중지 (무시 불가) | 프로세스 중지 |
SIGTSTP | 20 | 터미널 중지 | 프로세스 중지 | Ctrl+Z 키 입력 |
SIGTTIN | 21 | 터미널 입력 | 프로세스 중지 | 백그라운드 프로세스가 터미널에서 읽기 시도 |
SIGTTOU | 22 | 터미널 출력 | 프로세스 중지 | 백그라운드 프로세스가 터미널에 쓰기 시도 |
SIGUSR1
로 재미있는 걸 해볼 수 있지 않을까?SIGTERM
과 SIGKILL
의 차이정확하겐 POSIX 표준에 정의된 시그널 관련 시스템 콜과 라이브러리 함수를 해당 레포에서 직접 확인할 수 있다. - https://github.com/torvalds/linux?tab=readme-ov-file
시그널 전송 과정
kill PID
를 실행하면, 커널의 kill()
시스템 콜이 호출된다.시그널 전달 및 큐잉
task_struct
내의 시그널 마스크와 핸들러 정보를 확인한다.SIGTERM
시그널이 해당 프로세스의 시그널 큐(sigqueue)에 추가된다.컨텍스트 스위칭과 시그널 처리
SIGTERM
이 발견되면, 커널은 다음을 확인한다:sigprocmask()
로 블록했는지)sigaction()
으로 설정)핸들러 실행
SIGTERM
핸들러가 등록되어 있다면close()
)프로세스 종료
exit()
시스템 콜을 호출하여 정상 종료한다.좀비 프로세스 정리
wait()
또는 waitpid()
를 호출하면, 자식의 종료 상태를 회수하고 프로세스 테이블 엔트리가 완전히 제거된다.wait()
를 호출하지 않으면, 좀비 프로세스가 남게 된다.시그널 전송 과정:
kill -9 PID
를 실행하면, 역시 kill()
시스템 콜이 호출된다.특수 처리
SIGKILL
은 특별한 시그널로 커널은 이를 프로세스의 시그널 큐에 넣지 않는다.강제 종료 프로세스:
do_exit()
커널 함수가 호출되어 프로세스 종료 절차를 진행한다:EXIT_ZOMBIE
로 변경한다.SIGCHLD
시그널을 보낸다.사용자 공간 코드 실행 없음
SIGKILL
은 프로세스의 사용자 공간 코드가 실행될 기회를 전혀 주지 않는다.SIGTERM
call 이 더 안전한 종료 접근법이다!!좀비 프로세스와 후속 처리:
SIGTERM
과 마찬가지로, 프로세스는 부모가 wait()
를 호출할 때까지 좀비 상태로 남는다.실행 컨텍스트
SIGTERM
: 프로세스의 사용자 공간 코드(시그널 핸들러)가 실행된다.SIGKILL
: 전적으로 커널 공간에서 처리되며, 사용자 코드는 실행되지 않는다.리소스 정리
SIGTERM
: 애플리케이션이 자신의 리소스(임시 파일, 네트워크 연결, 데이터베이스 트랜잭션 등)를 정리할 수 있다.SIGKILL
: 애플리케이션 수준의 리소스 정리가 불가능하며, 커널 수준의 리소스만 정리된다.실행 시간
SIGTERM
: 핸들러 실행과 정리 과정으로 인해 종료에 시간이 걸릴 수 있다.SIGKILL
: 즉시 종료되어 지연이 최소화된다.안전성
SIGTERM
: 정상적인 종료 절차를 통해 데이터 무결성을 보장할 가능성이 높다.SIGKILL
: 데이터 손실이나 불일치, 네트워크 연결 문제 등을 야기할 수 있다.데이터 무결성 보장
클라이언트 영향 최소화
분산 시스템 일관성
SIGTERM
핸들러를 직접 추가해서 처리하는 방향으로 접근해보자!Kubernetes의 종료 프로세스
SIGTERM
을 받은 후 terminationGracePeriodSeconds
동안 기다린다 (기본 30초).SIGKILL
을 보내 강제 종료한다.Systemd의 종료 프로세스
TimeoutStopSec
설정으로 종료 타임아웃을 지정 (기본 90초).SIGKILL
을 보낸다.Docker의 종료 프로세스
docker stop
명령은 컨테이너에 SIGTERM을 보낸다.SIGKILL
을 보낸다.docker stop --time=<seconds>
옵션으로 타임아웃 조정 가능.적절한 타임아웃 설정 전략
그러니 이제
SIGTERM
을 위해kill PID
을 기본으로 사용하되, 경우에 따라kill -9 PID
를 사용하는게 어떨까?to. 스스로에게...
# 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
#!/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]
SIGKILL
의 남용이 초래할 수 있는 문제들!데이터 일관성 문제
리소스 누수
분산 시스템 문제
실제 사례 분석
다중 계층 애플리케이션
리더-팔로워 시스템
장기 실행 작업이 있는 시스템
데이터베이스 종료
man 7 signal
man 2 kill
man 2 sigaction
man 3 signal
kernel/signal.c
include/linux/signal.h