
서비스 · 프로세스 관리 11 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
Part 2 「시그널·자원·세션」을 시작한다. Part 1에서 SIGHUP(로그아웃), SIGSTOP(정지), SIGCHLD(자식 종료)가 여러 번 등장했다. 이 모두가 시그널(Signal) 이다. 시그널은 프로세스에게 "무언가 일어났다"를 알리는 커널의 가장 기본적인 통신 수단이며, 프로세스를 멈추고 끝내는 모든 작업의 기반이다.
이번 글에서는 주요 시그널의 번호와 기본 동작을 정리하고, 종료 코드 128+N 규칙, 그리고 /proc/PID/status의 SigIgn·SigCgt 비트마스크로 프로세스가 어떤 시그널을 처리하도록 설정했는지 읽는 방법을 실습한다.
「리눅스 시스템 기초 34편」에서 시그널과 kill을 소개했다. 이번 글부터 13편까지 시그널을 세 편으로 나누어 깊게 다룬다.
| 특징 | 내용 |
|---|---|
| 번호 | 1~31은 표준 시그널, 34~64는 실시간 시그널(SIGRTMIN~) |
| 비동기 | 프로세스가 무엇을 하든 도착할 수 있다 |
| 정보량 | 기본적으로 "몇 번 시그널이 왔다" 는 사실뿐이다 |
| 권한 | 같은 UID의 프로세스나 root만 보낼 수 있다 |
| 예외 | SIGKILL(9)과 SIGSTOP(19) 은 무시·차단·핸들러 등록이 불가능하다 |
| 방식 | 의미 | /proc/PID/status |
|---|---|---|
| 기본 동작 (SIG_DFL) | 시그널별로 정해진 동작 (종료, 코어덤프, 정지, 무시) | 해당 비트 없음 |
| 무시 (SIG_IGN) | 도착해도 아무 일 없음 | SigIgn |
| 핸들러 (catch) | 프로그램이 등록한 함수 실행 | SigCgt |
| (차단) | 처리를 잠시 보류 (pending 상태로 대기) | SigBlk |
시그널로 종료된 프로세스의 종료 상태를 셸은 128 + 시그널 번호 로 보여 준다.
| 종료 코드 | 의미 |
|---|---|
| 130 | 128 + 2 → SIGINT (Ctrl+C) |
| 137 | 128 + 9 → SIGKILL (OOM Killer 포함, 20편) |
| 143 | 128 + 15 → SIGTERM |
systemd 로그의 status=9/KILL, 컨테이너의 Exit Code 137 모두 같은 규칙으로 읽는다.

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)를 처리한다는 뜻이다.
# 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 '... 비트를 시그널 이름으로 변환 ...'



텍스트 원본(실제 출력):
[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']
| 관찰 | 의미 |
|---|---|
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·CHLD | sshd는 HUP을 받으면 설정을 다시 읽고 재실행 하도록 핸들러를 등록해 두었다. CHLD 핸들러로 자식 세션 종료를 처리한다 |
| 주제 | 내용 |
|---|---|
| 방어 회피 | 공격자는 보안 에이전트에 SIGSTOP(정지)이나 SIGKILL을 보내 탐지를 끈다. 에이전트는 root로 실행하고, 다른 계정이 시그널을 보낼 수 없게 한다 |
| 권한 경계 | 일반 사용자는 자기 UID의 프로세스에만 시그널을 보낼 수 있다(12편 실측). root 프로세스를 kill할 수 있다면 이미 root 권한을 가진 것이다 |
| 설정 재적재 악용 | 설정 파일을 변조한 뒤 kill -HUP으로 재적재시키면 서비스 재시작 기록 없이 설정이 바뀐다. 설정 파일 무결성 감시가 필요하다 |
| 감사 | auditd로 kill 시스템 콜을 감사하면 누가 어떤 PID에 몇 번 시그널을 보냈는지 기록할 수 있다 (48편) |
[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 | 시그널이 아닌 프로그램 자체의 오류 종료 |
| 실수 | 결과 | 예방 |
|---|---|---|
모든 종료에 kill -9 | 정리 작업 없이 종료 → 락·임시파일·데이터 손상 (13편) | 먼저 TERM, 필요할 때만 KILL |
| 시그널 번호를 외워 사용 | 아키텍처마다 일부 번호가 다름 | kill -TERM처럼 이름 사용 |
kill -HUP이면 모든 데몬이 재적재된다고 가정 | HUP 핸들러가 없는 프로그램은 종료 된다 | 서비스는 systemctl reload로, 지원 여부 확인 |
| 종료 코드 137을 "프로그램 버그"로 오판 | 실제 원인은 OOM 또는 강제 종료 | 128+N 규칙으로 해석 |
| SigCgt 16진수를 그대로 비교 | 해석 오류 | 비트 → 시그널 번호 변환 |
[ ] kill -l 로 시그널 번호와 이름을 확인했다
[ ] TERM·KILL·USR1 종료 코드가 128+N 인 것을 확인했다
[ ] 핸들러를 등록한 프로세스에서 USR1 이 종료가 아닌 동작을 하는 것을 확인했다
[ ] SIG_IGN 으로 설정한 HUP 이 무시되는 것을 확인했다
[ ] SigIgn / SigCgt 비트마스크를 시그널 이름으로 해석했다
[ ] systemd 로그의 status=9/KILL 을 해석할 수 있다
/proc/PID/status의 SigIgn·SigCgt로 프로세스의 시그널 처리 설정을 확인할 수 있다.status=9/KILL 종료는 OOM 여부를 먼저 확인하고, 아니라면 방어 회피를 의심한다.다음 글 「12. kill·pkill·killall 비교」 에서는 시그널을 보내는 세 가지 도구의 대상 선택 방식 차이 를 다룬다. pkill -f가 의도하지 않은 프로세스까지 종료시키는 함정, killall의 정확한 이름 매칭, 다른 사용자 프로세스에 대한 권한 오류를 실습한다.