서비스 · 프로세스 관리 13 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

"안 죽으면 kill -9." 가장 많이 퍼진 습관이자 가장 위험한 습관이다. SIGKILL은 프로세스에게 정리할 기회를 전혀 주지 않는다. 데이터베이스는 쓰던 데이터를 버리고, 애플리케이션은 락 파일을 남기고, 임시 파일이 쌓인다. 반면 SIGTERM은 "이제 끝내 달라"는 요청이라 프로그램이 스스로 정리하고 끝낼 수 있다.

이번 글에서는 두 시그널의 차이를 락 파일 실험 으로 확인하고, systemctl stop이 TERM → 타임아웃 → KILL 순서로 서비스를 안전하게 종료하는 과정을 로그로 추적한다.


2. 핵심 개념

2-1. SIGTERM과 SIGKILL 비교

항목SIGTERM (15)SIGKILL (9)
성격종료 요청강제 종료
핸들러 등록가능불가
무시가능불가
정리 작업가능 (flush, 락 해제, 연결 종료)불가
자식 프로세스프로그램이 직접 정리 가능자식은 고아로 남을 수 있음
kill 기본값✅—
종료 코드143 (또는 핸들러가 정한 값)137
적합한 상황모든 정상 종료TERM에 응답하지 않을 때, 악성 프로세스

2-2. systemd의 종료 관련 설정

설정기본값의미
ExecStop=없음종료 시 먼저 실행할 명령
KillSignal=SIGTERM처음 보낼 시그널
KillMode=control-group누구에게 보낼지: cgroup 전체 / mixed / process(메인만) / none
TimeoutStopSec=90초TERM 후 기다리는 시간
FinalKillSignal=SIGKILL타임아웃 후 보낼 시그널
SendSIGKILL=yes마지막 KILL 사용 여부

3. 동작 원리

SIGTERM vs SIGKILL, 그리고 systemctl stop 의 종료 절차

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로 메인만 종료하면 자식은 고아가 되어 남을 수 있다.


4. 명령어 실습

# 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

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — SIGTERM 은 정리 기회를 주고 SIGKILL 은 주지 않는다

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — systemd 의 종료 절차: TERM → 타임아웃 → KILL

텍스트 원본(실제 출력):

[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.

6. 결과 해석

관찰의미
TERM: "lock 삭제 후 정상 종료", 종료 코드 0, lock 없음핸들러가 실행되어 정리 후 스스로 종료했다
KILL: Killed, 종료 코드 137, lock 남음핸들러가 실행될 기회가 없었다. lock 파일에는 이미 죽은 PID가 적혀 있다
KillMode=control-group, KillSignal=15, FinalKillSignal=9systemd 기본 종료 정책
TimeoutStopUSec=5sunit 파일에서 90초 기본값을 5초로 줄였다
time systemctl stop → real 0m5.190sTERM을 무시해서 타임아웃 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을 제대로 처리하지 못한다는 뜻이다. 타임아웃을 늘리기 전에 애플리케이션의 종료 처리를 먼저 점검한다.


7. 보안 관점

주제내용
악성 프로세스의 TERM 무시악성코드는 TERM을 무시하거나 핸들러에서 자신을 다시 실행 하기도 한다. 악성으로 판정된 프로세스는 증거 수집 후 STOP → KILL 순서로 처리한다
로그 유실로그 수집기·보안 에이전트를 KILL로 죽이면 버퍼에 있던 이벤트가 전송되지 않고 사라진다. 관제 공백의 원인이 된다
무결성DB·파일 시스템 작업 중 KILL은 데이터 손상으로 이어져 복구 과정에서 증거가 훼손될 수 있다
흔적강제 종료는 status=9/KILL로 남는다. 정기 점검 시간이 아닌데 보안 서비스에 KILL 기록이 있다면 조사한다

8. 보안관제 관점

[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/KILLsystemd의 정상 종료 절차 중 강제 종료 (애플리케이션 문제)
Memory cgroup out of memory → status=9/KILLOOM
KILL 기록만 단독외부에서 kill -9 → 사람 또는 공격 도구

9. 실무에서 자주 발생하는 실수

실수결과예방
처음부터 kill -9데이터 손상, 락·임시파일 잔존TERM → 대기 → KILL
서비스를 kill PID로 종료systemd는 "비정상 종료"로 보고 재시작 할 수 있다 (29편)systemctl stop 사용
TimeoutStopSec를 무작정 늘림재부팅·배포 지연애플리케이션 종료 처리 개선
KILL 후 남은 lock 파일 방치서비스가 다시 시작하지 못함lock의 PID가 살아 있는지(kill -0) 확인 후 정리
KillMode=process로 변경자식 프로세스가 남아 누적특별한 이유가 없으면 기본값 유지

10. 실습 체크리스트

[ ] TERM 핸들러가 lock 을 정리하고 종료 코드 0 으로 끝나는 것을 확인했다
[ ] KILL 로 종료하면 lock 이 남고 종료 코드 137 인 것을 확인했다
[ ] systemctl show 로 KillMode, KillSignal, TimeoutStopUSec 를 확인했다
[ ] TERM 을 무시하는 서비스가 TimeoutStopSec 후 KILL 되는 것을 확인했다
[ ] journalctl 에서 stop-sigterm timed out 로그를 찾았다
[ ] status=9/KILL 의 세 가지 원인(타임아웃·OOM·외부)을 구분할 수 있다

11. 핵심 정리

  • SIGTERM은 정리할 기회를 주는 요청, SIGKILL은 기회를 주지 않는 강제 종료 다.
  • 종료 순서는 항상 TERM → 대기 → KILL 이다.
  • systemctl stop은 ExecStop → cgroup 전체 TERM → TimeoutStopSec → KILL을 자동으로 수행하고 결과를 기록한다.
  • KillMode=control-group 덕분에 서비스의 자식 프로세스까지 함께 정리된다.
  • status=9/KILL은 타임아웃·OOM·외부 kill 중 무엇인지 로그 조합으로 구분한다.

12. 다음 편 예고

다음 글 「14. trap으로 시그널 처리하기」 에서는 셸 스크립트에서 시그널 핸들러를 등록하는 trap을 다룬다. 스크립트가 중간에 종료되어도 임시 파일을 반드시 정리 하는 패턴, trap -p로 현재 설정을 확인하는 방법, 그리고 백그라운드 실행 시 SIGINT를 trap할 수 없는 함정을 확인한다.


참고 자료


시리즈 이동

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글