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

1. 들어가며

Part 2 「시그널·자원·세션」을 시작한다. Part 1에서 SIGHUP(로그아웃), SIGSTOP(정지), SIGCHLD(자식 종료)가 여러 번 등장했다. 이 모두가 시그널(Signal) 이다. 시그널은 프로세스에게 "무언가 일어났다"를 알리는 커널의 가장 기본적인 통신 수단이며, 프로세스를 멈추고 끝내는 모든 작업의 기반이다.

이번 글에서는 주요 시그널의 번호와 기본 동작을 정리하고, 종료 코드 128+N 규칙, 그리고 /proc/PID/status의 SigIgn·SigCgt 비트마스크로 프로세스가 어떤 시그널을 처리하도록 설정했는지 읽는 방법을 실습한다.

「리눅스 시스템 기초 34편」에서 시그널과 kill을 소개했다. 이번 글부터 13편까지 시그널을 세 편으로 나누어 깊게 다룬다.


2. 핵심 개념

2-1. 시그널의 특징

특징내용
번호1~31은 표준 시그널, 34~64는 실시간 시그널(SIGRTMIN~)
비동기프로세스가 무엇을 하든 도착할 수 있다
정보량기본적으로 "몇 번 시그널이 왔다" 는 사실뿐이다
권한같은 UID의 프로세스나 root만 보낼 수 있다
예외SIGKILL(9)과 SIGSTOP(19) 은 무시·차단·핸들러 등록이 불가능하다

2-2. 프로세스의 세 가지 반응

방식의미/proc/PID/status
기본 동작 (SIG_DFL)시그널별로 정해진 동작 (종료, 코어덤프, 정지, 무시)해당 비트 없음
무시 (SIG_IGN)도착해도 아무 일 없음SigIgn
핸들러 (catch)프로그램이 등록한 함수 실행SigCgt
(차단)처리를 잠시 보류 (pending 상태로 대기)SigBlk

2-3. 종료 코드 128 + N

시그널로 종료된 프로세스의 종료 상태를 셸은 128 + 시그널 번호 로 보여 준다.

종료 코드의미
130128 + 2 → SIGINT (Ctrl+C)
137128 + 9 → SIGKILL (OOM Killer 포함, 20편)
143128 + 15 → SIGTERM

systemd 로그의 status=9/KILL, 컨테이너의 Exit Code 137 모두 같은 규칙으로 읽는다.


3. 동작 원리

시그널 전달 흐름과 세 가지 처리 방식

kill -USR1 724
  ↓ 커널: 권한 확인 → PID 724 의 pending 에 SIGUSR1 표시
  ↓ 724 가 다음에 커널 → 사용자 모드로 돌아갈 때 pending 확인
  ↓ 처리 방식 확인
     SigCgt 에 USR1 비트 → 등록된 핸들러 실행 ("설정 다시 읽기")
     SigIgn 에 비트      → 무시
     둘 다 없음           → 기본 동작 (USR1 은 "종료")

SigIgn, SigCgt는 64비트를 16진수로 표현한 값이다. 비트 번호 N-1이 시그널 N 에 대응한다. 예를 들어 0x4200은 비트 9(0x200)와 비트 14(0x4000)가 켜진 값이므로 시그널 10(SIGUSR1)과 15(SIGTERM)를 처리한다는 뜻이다.


4. 명령어 실습

# 1) 시그널 목록과 번호 변환
kill -l | head -7
kill -l 15; kill -l TERM

# 2) 시그널별 종료 코드
sleep 100 & kill -TERM $!; wait $!; echo "종료 코드=$?"
sleep 100 & kill -KILL $!; wait $!; echo "종료 코드=$?"
sleep 100 & kill -USR1 $!; wait $!; echo "종료 코드=$?"

# 3) 핸들러를 등록한 프로세스 (USR1=재적재, TERM=정리 후 종료, HUP=무시)
python3 -c 'import signal,time,sys
signal.signal(signal.SIGUSR1, lambda s,f: print("USR1 받음: 설정 다시 읽기", flush=True))
signal.signal(signal.SIGTERM, lambda s,f: (print("TERM 받음: 정리 후 종료", flush=True), sys.exit(0)))
signal.signal(signal.SIGHUP, signal.SIG_IGN)
time.sleep(30)' &
grep -E '^Sig(Blk|Ign|Cgt)' /proc/$P/status
kill -USR1 $P; kill -HUP $P; ps -o pid,stat,cmd -p $P
kill -TERM $P; wait $P; echo "종료 코드=$?"

# 4) (Ubuntu) sshd 의 SigIgn / SigCgt 비트마스크 해석
grep -E '^Sig(Ign|Cgt)' /proc/$(pgrep -xo sshd)/status
python3 -c '... 비트를 시그널 이름으로 변환 ...'

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — 시그널 목록과 기본 동작

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — 시그널 핸들러가 있는 프로세스

실제 실행 결과 — Ubuntu 24.04.5 · root@ubuntu-lab — SigCgt 비트마스크 해석하기

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

[analyst@rocky9-lab ~]$ kill -l | head -7
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
[analyst@rocky9-lab ~]$ kill -l 15; kill -l TERM
TERM
15
[analyst@rocky9-lab ~]$ sleep 100 & kill -TERM $!; wait $!; echo "종료 코드=$? (128+15)"
종료 코드=143 (128+15)
[analyst@rocky9-lab ~]$ sleep 100 & kill -KILL $!; wait $!; echo "종료 코드=$? (128+9)"
/tmp/run_11_1.sh: line 14:   687 Killed                  sleep 100
종료 코드=137 (128+9)
[analyst@rocky9-lab ~]$ sleep 100 & kill -USR1 $!; wait $!; echo "종료 코드=$? (128+10)"
/tmp/run_11_1.sh: line 17:   688 User defined signal 1   sleep 100
종료 코드=138 (128+10)
[analyst@rocky9-lab ~]$ python3 -c 'import signal,time,sys
> signal.signal(signal.SIGUSR1, lambda s,f: print("USR1 받음: 설정 다시 읽기", flush=True))
> signal.signal(signal.SIGTERM, lambda s,f: (print("TERM 받음: 정리 후 종료", flush=True), sys.exit(0)))
> signal.signal(signal.SIGHUP, signal.SIG_IGN)
> time.sleep(30)' &
[analyst@rocky9-lab ~]$ sleep 0.5; P=$!; grep -E '^Sig(Blk|Ign|Cgt)' /proc/$P/status
SigBlk:	0000000000000000
SigIgn:	0000000001001007
SigCgt:	0000000000004200
[analyst@rocky9-lab ~]$ kill -USR1 $P; sleep 0.3; kill -HUP $P; sleep 0.3; ps -o pid,stat,cmd -p $P | cut -c1-60
USR1 받음: 설정 다시 읽기
    PID STAT CMD
    724 S+   python3 -c import signal,time,sys signal.signal
[analyst@rocky9-lab ~]$ kill -TERM $P; wait $P; echo "종료 코드=$?"
TERM 받음: 정리 후 종료
종료 코드=0
root@ubuntu-lab:~# S=$(pgrep -xo sshd); grep -E '^Sig(Ign|Cgt)' /proc/$S/status
SigIgn:	0000000000001000
SigCgt:	0000000000014005
root@ubuntu-lab:~# python3 -c 'import signal,sys
> for name in ("SigIgn","SigCgt"):
>     v=int([l.split()[1] for l in open("/proc/'$S'/status") if l.startswith(name)][0],16)
>     print(name, [signal.Signals(i+1).name for i in range(64) if v>>i & 1 and i+1 < 32])'
SigIgn ['SIGPIPE']
SigCgt ['SIGHUP', 'SIGQUIT', 'SIGTERM', 'SIGCHLD']

6. 결과 해석

관찰의미
kill -l 목록에 32, 33이 없음glibc(NPTL)가 내부용으로 예약해 SIGRTMIN이 34부터 시작한다
kill -l 15 → TERM, kill -l TERM → 15번호 ↔ 이름 변환. 스크립트에서는 이름 을 쓰는 편이 안전하다
TERM → 143, KILL → 137, USR1 → 138모두 128 + 시그널 번호. 종료 코드만 보고 어떤 시그널로 죽었는지 알 수 있다
Killed, User defined signal 1 메시지셸이 자식의 종료 원인을 사람이 읽을 수 있게 출력했다. TERM은 셸이 메시지를 생략한다
python SigCgt: 0x4200비트 9·14 → SIGUSR1(10)·SIGTERM(15) 핸들러 등록
python SigIgn: 0x1001007비트 0·1·2 → HUP·INT·QUIT, 비트 12 → PIPE, 비트 24 → XFSZ 무시. HUP은 코드에서, INT·QUIT은 백그라운드 실행 규칙에서, PIPE·XFSZ는 Python 런타임이 설정했다
USR1 → "설정 다시 읽기" 출력, 프로세스 유지핸들러가 실행되고 종료되지 않았다. 데몬의 설정 재적재 가 이렇게 동작한다
HUP → 아무 반응 없이 S+ 유지무시로 설정되어 로그아웃 시에도 살아남는다 (10편 nohup과 같은 원리)
TERM → "정리 후 종료", 종료 코드 0핸들러가 정상 종료(sys.exit(0))를 선택했다. 시그널을 받았어도 종료 코드는 0일 수 있다
Ubuntu sshd: Ign=PIPE, Cgt=HUP·QUIT·TERM·CHLDsshd는 HUP을 받으면 설정을 다시 읽고 재실행 하도록 핸들러를 등록해 두었다. CHLD 핸들러로 자식 세션 종료를 처리한다

7. 보안 관점

주제내용
방어 회피공격자는 보안 에이전트에 SIGSTOP(정지)이나 SIGKILL을 보내 탐지를 끈다. 에이전트는 root로 실행하고, 다른 계정이 시그널을 보낼 수 없게 한다
권한 경계일반 사용자는 자기 UID의 프로세스에만 시그널을 보낼 수 있다(12편 실측). root 프로세스를 kill할 수 있다면 이미 root 권한을 가진 것이다
설정 재적재 악용설정 파일을 변조한 뒤 kill -HUP으로 재적재시키면 서비스 재시작 기록 없이 설정이 바뀐다. 설정 파일 무결성 감시가 필요하다
감사auditd로 kill 시스템 콜을 감사하면 누가 어떤 PID에 몇 번 시그널을 보냈는지 기록할 수 있다 (48편)

8. 보안관제 관점

[Detection]  systemd: auditd.service Main process exited, code=killed, status=9/KILL
     ↓
[해석]       status=9/KILL → SIGKILL 로 강제 종료 (종료 코드라면 137)
     ↓
[확인]       journalctl -u auditd --since "-10min"   ← 서비스 로그
             dmesg | grep -i oom                     ← OOM Killer 인가? (20편)
             journalctl _COMM=sudo --since "-10min" ← 그 시각 root 명령
     ↓
[판단]       OOM 이 아닌데 보안 서비스가 SIGKILL → 방어 회피 의심
     ↓
[Response]   서비스 재시작, 해당 시각 로그인 세션·명령 조사, 계정 점검
로그 표기해석
code=killed, status=15/TERM정상 종료 요청으로 종료
code=killed, status=9/KILL강제 종료 (사람, systemd 타임아웃, OOM Killer)
code=dumped, status=11/SEGV메모리 접근 오류로 코어덤프 — 버그 또는 익스플로잇 시도 가능성
code=exited, status=1/FAILURE시그널이 아닌 프로그램 자체의 오류 종료

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

실수결과예방
모든 종료에 kill -9정리 작업 없이 종료 → 락·임시파일·데이터 손상 (13편)먼저 TERM, 필요할 때만 KILL
시그널 번호를 외워 사용아키텍처마다 일부 번호가 다름kill -TERM처럼 이름 사용
kill -HUP이면 모든 데몬이 재적재된다고 가정HUP 핸들러가 없는 프로그램은 종료 된다서비스는 systemctl reload로, 지원 여부 확인
종료 코드 137을 "프로그램 버그"로 오판실제 원인은 OOM 또는 강제 종료128+N 규칙으로 해석
SigCgt 16진수를 그대로 비교해석 오류비트 → 시그널 번호 변환

10. 실습 체크리스트

[ ] kill -l 로 시그널 번호와 이름을 확인했다
[ ] TERM·KILL·USR1 종료 코드가 128+N 인 것을 확인했다
[ ] 핸들러를 등록한 프로세스에서 USR1 이 종료가 아닌 동작을 하는 것을 확인했다
[ ] SIG_IGN 으로 설정한 HUP 이 무시되는 것을 확인했다
[ ] SigIgn / SigCgt 비트마스크를 시그널 이름으로 해석했다
[ ] systemd 로그의 status=9/KILL 을 해석할 수 있다

11. 핵심 정리

  • 시그널은 커널이 프로세스에 보내는 번호 하나짜리 비동기 알림이다.
  • 프로세스는 기본 동작·무시·핸들러로 반응하며, SIGKILL과 SIGSTOP은 막을 수 없다.
  • 시그널로 종료되면 종료 코드는 128 + N (TERM 143, KILL 137).
  • /proc/PID/status의 SigIgn·SigCgt로 프로세스의 시그널 처리 설정을 확인할 수 있다.
  • 보안 서비스의 status=9/KILL 종료는 OOM 여부를 먼저 확인하고, 아니라면 방어 회피를 의심한다.

12. 다음 편 예고

다음 글 「12. kill·pkill·killall 비교」 에서는 시그널을 보내는 세 가지 도구의 대상 선택 방식 차이 를 다룬다. pkill -f가 의도하지 않은 프로세스까지 종료시키는 함정, killall의 정확한 이름 매칭, 다른 사용자 프로세스에 대한 권한 오류를 실습한다.


참고 자료


시리즈 이동

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

0개의 댓글