
서비스 · 프로세스 관리 13 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
"안 죽으면 kill -9." 가장 많이 퍼진 습관이자 가장 위험한 습관이다. SIGKILL은 프로세스에게 정리할 기회를 전혀 주지 않는다. 데이터베이스는 쓰던 데이터를 버리고, 애플리케이션은 락 파일을 남기고, 임시 파일이 쌓인다. 반면 SIGTERM은 "이제 끝내 달라"는 요청이라 프로그램이 스스로 정리하고 끝낼 수 있다.
이번 글에서는 두 시그널의 차이를 락 파일 실험 으로 확인하고, systemctl stop이 TERM → 타임아웃 → KILL 순서로 서비스를 안전하게 종료하는 과정을 로그로 추적한다.
| 항목 | SIGTERM (15) | SIGKILL (9) |
|---|---|---|
| 성격 | 종료 요청 | 강제 종료 |
| 핸들러 등록 | 가능 | 불가 |
| 무시 | 가능 | 불가 |
| 정리 작업 | 가능 (flush, 락 해제, 연결 종료) | 불가 |
| 자식 프로세스 | 프로그램이 직접 정리 가능 | 자식은 고아로 남을 수 있음 |
kill 기본값 | ✅ | — |
| 종료 코드 | 143 (또는 핸들러가 정한 값) | 137 |
| 적합한 상황 | 모든 정상 종료 | TERM에 응답하지 않을 때, 악성 프로세스 |
| 설정 | 기본값 | 의미 |
|---|---|---|
ExecStop= | 없음 | 종료 시 먼저 실행할 명령 |
KillSignal= | SIGTERM | 처음 보낼 시그널 |
KillMode= | control-group | 누구에게 보낼지: cgroup 전체 / mixed / process(메인만) / none |
TimeoutStopSec= | 90초 | TERM 후 기다리는 시간 |
FinalKillSignal= | SIGKILL | 타임아웃 후 보낼 시그널 |
SendSIGKILL= | yes | 마지막 KILL 사용 여부 |

kill -TERM 1234
→ 1234 의 TERM 핸들러: lock 삭제 → exit(0) ✔ 깨끗한 종료
kill -KILL 1234
→ 커널이 즉시 1234 제거 (사용자 코드 한 줄도 실행되지 않음)
→ /tmp/worker.lock 남음 → 다음 실행 시 "이미 실행 중" 오류 가능 ✘
systemctl stop app
→ ExecStop → cgroup 전체에 SIGTERM → TimeoutStopSec 대기
→ 남은 프로세스에 SIGKILL → 결과 기록 (success / timeout)
KillMode=control-group이 기본인 이유도 중요하다. 서비스가 만든 자식 프로세스까지 모두 cgroup에 묶여 있으므로, 메인 프로세스만 죽이고 자식이 남는 일을 막는다. kill PID로 메인만 종료하면 자식은 고아가 되어 남을 수 있다.
# 1) TERM 핸들러가 있는 작업 프로그램
cat > ~/worker.py <<'EOF'
import signal,sys,time,os
LOCK="/tmp/worker.lock"
open(LOCK,"w").write(str(os.getpid()))
def stop(s,f):
os.remove(LOCK); print("TERM 수신 → lock 삭제 후 정상 종료", flush=True); sys.exit(0)
signal.signal(signal.SIGTERM, stop)
while True: time.sleep(1)
EOF
# 2) TERM 으로 종료 → lock 정리됨
python3 ~/worker.py & sleep 0.5; ls -l /tmp/worker.lock
kill -TERM $!; wait $!; echo "종료 코드=$?"; ls /tmp/worker.lock
# 3) KILL 로 종료 → lock 남음
python3 ~/worker.py & sleep 0.5
kill -KILL $!; wait $!; echo "종료 코드=$?"; ls -l /tmp/worker.lock
# 4) (root) TERM 을 무시하는 서비스와 systemd 종료 절차
cat > /etc/systemd/system/stubborn.service <<'EOF'
[Unit]
Description=Lab service that ignores SIGTERM
[Service]
ExecStart=/usr/bin/python3 -c "import signal,time; signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(3600)"
TimeoutStopSec=5
EOF
systemctl daemon-reload; systemctl start stubborn
systemctl show stubborn -p MainPID,KillMode,KillSignal,TimeoutStopUSec,FinalKillSignal
time systemctl stop stubborn
journalctl -u stubborn --no-pager -o short-precise | tail -5


텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ cat > ~/worker.py <<'EOF'
> import signal,sys,time,os
> LOCK="/tmp/worker.lock"
> open(LOCK,"w").write(str(os.getpid()))
> def stop(s,f):
> os.remove(LOCK); print("TERM 수신 → lock 삭제 후 정상 종료", flush=True); sys.exit(0)
> signal.signal(signal.SIGTERM, stop)
> while True: time.sleep(1)
> EOF
[analyst@rocky9-lab ~]$ python3 ~/worker.py & sleep 0.5; ls -l /tmp/worker.lock
-rw-r--r-- 1 analyst analyst 3 Sep 24 11:50 /tmp/worker.lock
[analyst@rocky9-lab ~]$ kill -TERM $!; wait $!; echo "종료 코드=$?"; ls /tmp/worker.lock
TERM 수신 → lock 삭제 후 정상 종료
종료 코드=0
ls: cannot access '/tmp/worker.lock': No such file or directory
[analyst@rocky9-lab ~]$ python3 ~/worker.py & sleep 0.5
[analyst@rocky9-lab ~]$ kill -KILL $!; wait $!; echo "종료 코드=$?"; ls -l /tmp/worker.lock
/tmp/run_13_1.sh: line 33: 902 Killed python3 ~/worker.py
종료 코드=137
-rw-r--r-- 1 analyst analyst 3 Sep 24 11:50 /tmp/worker.lock
[analyst@rocky9-lab ~]$ rm -f /tmp/worker.lock
[root@rocky9-lab ~]# cat > /etc/systemd/system/stubborn.service <<'EOF'
> [Unit]
> Description=Lab service that ignores SIGTERM
> [Service]
> ExecStart=/usr/bin/python3 -c "import signal,time; signal.signal(signal.SIGTERM, signal.SIG_IGN); time.sleep(3600)"
> TimeoutStopSec=5
> EOF
[root@rocky9-lab ~]# systemctl daemon-reload; systemctl start stubborn; systemctl show stubborn -p MainPID,KillMode,KillSignal,TimeoutStopUSec,FinalKillSignal
TimeoutStopUSec=5s
MainPID=952
KillMode=control-group
KillSignal=15
FinalKillSignal=9
[root@rocky9-lab ~]# time systemctl stop stubborn
real 0m5.190s
user 0m0.000s
sys 0m0.011s
[root@rocky9-lab ~]# journalctl -u stubborn --no-pager -o short-precise | tail -5
Sep 24 11:50:20.194583 rocky9-lab systemd[1]: stubborn.service: State 'stop-sigterm' timed out. Killing.
Sep 24 11:50:20.194660 rocky9-lab systemd[1]: stubborn.service: Killing process 952 (python3) with signal SIGKILL.
Sep 24 11:50:20.195829 rocky9-lab systemd[1]: stubborn.service: Main process exited, code=killed, status=9/KILL
Sep 24 11:50:20.195894 rocky9-lab systemd[1]: stubborn.service: Failed with result 'timeout'.
Sep 24 11:50:20.196338 rocky9-lab systemd[1]: Stopped Lab service that ignores SIGTERM.
| 관찰 | 의미 |
|---|---|
| TERM: "lock 삭제 후 정상 종료", 종료 코드 0, lock 없음 | 핸들러가 실행되어 정리 후 스스로 종료했다 |
KILL: Killed, 종료 코드 137, lock 남음 | 핸들러가 실행될 기회가 없었다. lock 파일에는 이미 죽은 PID가 적혀 있다 |
KillMode=control-group, KillSignal=15, FinalKillSignal=9 | systemd 기본 종료 정책 |
TimeoutStopUSec=5s | unit 파일에서 90초 기본값을 5초로 줄였다 |
time systemctl stop → real 0m5.190s | TERM을 무시해서 타임아웃 5초를 꽉 채운 뒤 종료되었다 |
State 'stop-sigterm' timed out. Killing. | TERM 단계가 시간 초과되어 KILL 단계로 넘어갔다 |
Killing process 952 (python3) with signal SIGKILL. | 남아 있던 프로세스에 SIGKILL |
code=killed, status=9/KILL, Failed with result 'timeout' | 서비스는 멈췄지만 결과는 실패(timeout) 로 기록된다. 모니터링에서 이 기록을 잡을 수 있다 |
운영 서비스에서
systemctl stop이 항상 90초씩 걸린다면 애플리케이션이 TERM을 제대로 처리하지 못한다는 뜻이다. 타임아웃을 늘리기 전에 애플리케이션의 종료 처리를 먼저 점검한다.
| 주제 | 내용 |
|---|---|
| 악성 프로세스의 TERM 무시 | 악성코드는 TERM을 무시하거나 핸들러에서 자신을 다시 실행 하기도 한다. 악성으로 판정된 프로세스는 증거 수집 후 STOP → KILL 순서로 처리한다 |
| 로그 유실 | 로그 수집기·보안 에이전트를 KILL로 죽이면 버퍼에 있던 이벤트가 전송되지 않고 사라진다. 관제 공백의 원인이 된다 |
| 무결성 | DB·파일 시스템 작업 중 KILL은 데이터 손상으로 이어져 복구 과정에서 증거가 훼손될 수 있다 |
| 흔적 | 강제 종료는 status=9/KILL로 남는다. 정기 점검 시간이 아닌데 보안 서비스에 KILL 기록이 있다면 조사한다 |
[Detection] journald: "falcon-sensor.service: Main process exited, code=killed, status=9/KILL"
↓
[분류] ① systemd 타임아웃? → "State 'stop-sigterm' timed out" 로그가 앞에 있는가
② OOM Killer? → dmesg "Killed process" (20편)
③ 사람/도구? → 위 둘 다 없음
↓
[확인 ③] 같은 시각 sudo/root 세션: journalctl _COMM=sudo --since ...
auditd kill 규칙 기록 (a1=9)
↓
[Response] 서비스 재시작 → 공백 구간 기록 → 실행 주체 조사
| 로그 조합 | 해석 |
|---|---|
stop-sigterm timed out → status=9/KILL | systemd의 정상 종료 절차 중 강제 종료 (애플리케이션 문제) |
Memory cgroup out of memory → status=9/KILL | OOM |
| KILL 기록만 단독 | 외부에서 kill -9 → 사람 또는 공격 도구 |
| 실수 | 결과 | 예방 |
|---|---|---|
처음부터 kill -9 | 데이터 손상, 락·임시파일 잔존 | TERM → 대기 → KILL |
서비스를 kill PID로 종료 | systemd는 "비정상 종료"로 보고 재시작 할 수 있다 (29편) | systemctl stop 사용 |
TimeoutStopSec를 무작정 늘림 | 재부팅·배포 지연 | 애플리케이션 종료 처리 개선 |
| KILL 후 남은 lock 파일 방치 | 서비스가 다시 시작하지 못함 | lock의 PID가 살아 있는지(kill -0) 확인 후 정리 |
KillMode=process로 변경 | 자식 프로세스가 남아 누적 | 특별한 이유가 없으면 기본값 유지 |
[ ] TERM 핸들러가 lock 을 정리하고 종료 코드 0 으로 끝나는 것을 확인했다
[ ] KILL 로 종료하면 lock 이 남고 종료 코드 137 인 것을 확인했다
[ ] systemctl show 로 KillMode, KillSignal, TimeoutStopUSec 를 확인했다
[ ] TERM 을 무시하는 서비스가 TimeoutStopSec 후 KILL 되는 것을 확인했다
[ ] journalctl 에서 stop-sigterm timed out 로그를 찾았다
[ ] status=9/KILL 의 세 가지 원인(타임아웃·OOM·외부)을 구분할 수 있다
systemctl stop은 ExecStop → cgroup 전체 TERM → TimeoutStopSec → KILL을 자동으로 수행하고 결과를 기록한다.KillMode=control-group 덕분에 서비스의 자식 프로세스까지 함께 정리된다.status=9/KILL은 타임아웃·OOM·외부 kill 중 무엇인지 로그 조합으로 구분한다.다음 글 「14. trap으로 시그널 처리하기」 에서는 셸 스크립트에서 시그널 핸들러를 등록하는 trap을 다룬다. 스크립트가 중간에 종료되어도 임시 파일을 반드시 정리 하는 패턴, trap -p로 현재 설정을 확인하는 방법, 그리고 백그라운드 실행 시 SIGINT를 trap할 수 없는 함정을 확인한다.