
서비스 · 프로세스 관리 14 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
점검 스크립트나 백업 스크립트는 임시 파일과 락 파일을 만든다. 스크립트가 끝까지 잘 돌면 마지막 줄에서 지우면 되지만, 누군가 Ctrl+C를 누르거나 systemctl stop으로 종료하면 정리 코드가 실행되지 않고 파일이 남는다. 남은 임시 파일에는 수집한 계정 목록이나 설정 값 같은 민감한 정보가 들어 있을 수도 있다.
이번 글에서는 셸 스크립트의 시그널 핸들러인 trap으로 어떤 경로로 끝나든 정리되는 스크립트 를 만들고, trap이 동작하지 않는 예외를 확인한다.
| 형태 | 의미 |
|---|---|
trap '명령' SIGNAL... | 시그널 도착 시 명령 실행 (핸들러 등록) |
trap '' SIGNAL | 시그널 무시 (SIG_IGN) |
trap - SIGNAL | 기본 동작으로 복원 |
trap -p | 현재 설정 출력 |
trap -l | 시그널 목록 |
| 이름 | 실행 시점 | 용도 |
|---|---|---|
EXIT | 셸이 종료될 때 (정상·exit·시그널 종료 모두) | 정리 작업의 핵심 |
ERR | 명령이 0이 아닌 종료 코드를 낼 때 | 실패 위치 기록 |
DEBUG | 각 명령 실행 전 | 디버깅·감사 |
RETURN | 함수·source 종료 시 | — |
| 경우 | 이유 |
|---|---|
| SIGKILL, SIGSTOP | 커널이 핸들러 등록을 허용하지 않는다 |
| 시작할 때 이미 무시된 시그널 | 비대화형 bash는 진입 시 무시된 시그널을 trap하거나 복원할 수 없다 |
| 전원 차단·커널 패닉 | 프로세스가 실행될 기회가 없다 |
| 포그라운드 명령 실행 중 | bash는 실행 중인 명령이 끝난 뒤에 trap을 실행한다. 긴 명령은 cmd & wait $! 패턴으로 |

job.sh 시작
TMP 생성 → trap 등록 (EXIT, INT/TERM, HUP 무시)
sleep 60 & wait $! ← wait 는 시그널이 오면 즉시 반환된다
kill -TERM 1005
→ wait 중단 → TERM 트랩: "[trap] INT/TERM 수신"; exit 130
→ exit 가 EXIT 트랩 호출 → cleanup: rm -f $TMP
→ 종료 코드 130
sleep 60을 포그라운드로 실행하면 bash는 sleep이 끝날 때까지(최대 60초) TERM 트랩을 미룬다. & wait $! 패턴 을 쓰면 wait가 시그널에 즉시 반응해 트랩이 바로 실행된다. systemd가 스크립트 서비스를 90초 기다렸다가 KILL하는 흔한 원인이 바로 이것이다.
# 1) 정리 트랩이 있는 스크립트
cat > ~/job.sh <<'EOF'
#!/bin/bash
TMP=$(mktemp /tmp/job.XXXXXX)
cleanup() { rm -f "$TMP"; echo "[trap] 임시파일 $TMP 삭제 (신호: $1)"; }
trap 'cleanup EXIT' EXIT
trap 'echo "[trap] INT/TERM 수신"; exit 130' INT TERM
trap '' HUP
echo "PID=$$ TMP=$TMP"; trap -p
sleep 60 & wait $!
EOF
chmod +x ~/job.sh
~/job.sh & sleep 0.5; J=$!
kill -HUP $J; ps -o pid,stat,cmd -p $J # 무시되어 계속 실행
kill -TERM $J; wait $J; echo "종료 코드=$?"; ls /tmp/job.*
# 2) (Ubuntu) trap 이 없으면 임시 파일이 남는다
bash -c 'T=$(mktemp); echo "TMP=$T"; sleep 30' & sleep 0.5; kill $!
ls -l /tmp/tmp.* | tail -2
# 3) ERR 트랩으로 실패 위치 기록
bash -c 'set -E; trap '\''echo "[ERR] 줄 $LINENO 실패: $BASH_COMMAND (코드 $?)"'\'' ERR; ls /nonexistent; echo "계속 진행"'


텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ cat > ~/job.sh <<'EOF'
> #!/bin/bash
> TMP=$(mktemp /tmp/job.XXXXXX)
> cleanup() { rm -f "$TMP"; echo "[trap] 임시파일 $TMP 삭제 (신호: $1)"; }
> trap 'cleanup EXIT' EXIT
> trap 'echo "[trap] INT/TERM 수신"; exit 130' INT TERM
> trap '' HUP
> echo "PID=$$ TMP=$TMP"; trap -p
> sleep 60 & wait $!
> EOF
[analyst@rocky9-lab ~]$ chmod +x ~/job.sh
[analyst@rocky9-lab ~]$ ~/job.sh & sleep 0.5; J=$!
PID=1005 TMP=/tmp/job.jEtypS
trap -- 'cleanup EXIT' EXIT
trap -- '' SIGHUP
trap -- '' SIGINT
trap -- '' SIGQUIT
trap -- 'echo "[trap] INT/TERM 수신"; exit 130' SIGTERM
[analyst@rocky9-lab ~]$ kill -HUP $J; sleep 0.3; ps -o pid,stat,cmd -p $J
PID STAT CMD
1005 S+ /bin/bash /home/analyst/job.sh
[analyst@rocky9-lab ~]$ kill -TERM $J; wait $J; echo "종료 코드=$?"; ls /tmp/job.* 2>&1
[trap] INT/TERM 수신
[trap] 임시파일 /tmp/job.jEtypS 삭제 (신호: EXIT)
종료 코드=130
ls: cannot access '/tmp/job.*': No such file or directory
analyst@ubuntu-lab:~$ bash -c 'T=$(mktemp); echo "TMP=$T"; sleep 30' & sleep 0.5; kill $!; wait $! 2>/dev/null; ls -l /tmp/tmp.* | tail -2
TMP=/tmp/tmp.HgfdCpDfLx
-rw------- 1 analyst analyst 0 Sep 24 11:50 /tmp/tmp.HgfdCpDfLx
analyst@ubuntu-lab:~$ bash -c 'set -E; trap '\''echo "[ERR] 줄 $LINENO 실패: $BASH_COMMAND (코드 $?)"'\'' ERR; ls /nonexistent; echo "계속 진행"'
ls: cannot access '/nonexistent': No such file or directory
[ERR] 줄 1 실패: ls /nonexistent (코드 2)
계속 진행
| 관찰 | 의미 |
|---|---|
trap -p에 EXIT, HUP(''), TERM 트랩 표시 | 등록한 트랩이 적용되었다 |
trap -- '' SIGINT, trap -- '' SIGQUIT | 코드에서 INT 트랩을 걸었는데 무시('')로 표시 된다. 백그라운드로 실행된 비대화형 스크립트는 INT·QUIT이 시작부터 무시 상태라 trap이 적용되지 않았다 |
HUP 후에도 S+ 유지 | trap '' HUP으로 무시되었다 |
| TERM → "[trap] INT/TERM 수신" → "[trap] 임시파일 ... 삭제" | TERM 트랩이 exit 130을 호출했고, exit가 EXIT 트랩을 실행해 정리했다 |
| 종료 코드 130 | TERM 트랩에서 지정한 값. EXIT 트랩은 종료 코드를 바꾸지 않는다 |
ls /tmp/job.* → 없음 | 임시 파일이 확실히 정리되었다 |
Ubuntu: trap 없는 스크립트를 kill → /tmp/tmp.HgfdCpDfLx 남음 | 정리 코드가 없으면 시그널 종료 시 파일이 그대로 남는다 |
[ERR] 줄 1 실패: ls /nonexistent (코드 2) 후 "계속 진행" | ERR 트랩은 실패를 기록 할 뿐 스크립트를 멈추지 않는다. 멈추려면 set -e 또는 트랩 안에서 exit |
실무 스크립트를 Ctrl+C로 멈출 수 있어야 한다면 포그라운드로 실행 하거나, 스크립트 첫 부분에서 INT를 명시적으로 다루는 대신 TERM 중심으로 종료를 설계한다. systemd·cron은 종료 시 TERM을 보낸다.
| 주제 | 내용 |
|---|---|
| 민감한 임시 파일 | 점검 결과·비밀번호 해시·설정 사본이 /tmp에 남으면 다른 사용자가 읽을 수 있다. mktemp(권한 600) + EXIT 트랩으로 반드시 정리한다 |
| 예측 가능한 임시 파일명 | /tmp/backup.tmp처럼 고정된 이름은 공격자가 심볼릭 링크를 미리 만들어 다른 파일을 덮어쓰게 할 수 있다. 항상 mktemp 사용 |
| 트랩 악용 | 공격 스크립트도 trap '' HUP INT TERM으로 종료 시그널을 무시해 버틴다. 이런 프로세스의 SigIgn에는 HUP·INT·TERM 비트가 모두 켜져 있다 (11편) |
| DEBUG 트랩 | trap '...' DEBUG는 실행되는 모든 명령을 가로챌 수 있다. 공유 프로필(/etc/profile.d/)에 심어진 DEBUG 트랩은 명령 탈취·키로깅 에 악용될 수 있다 |
[Detection] /etc/profile.d/ 에 새 파일, 내용에 "trap ... DEBUG"
↓
[확인] grep -rn "trap" /etc/profile /etc/profile.d/ /etc/bash.bashrc ~/.bashrc 2>/dev/null
stat /etc/profile.d/<file> ← 생성·변경 시각
↓
[판단] 모든 로그인 셸에서 입력 명령을 외부로 보내는 코드인가?
↓
[Response] 파일 격리·삭제, 변경 시각 전후 로그인 기록 조사, 영향 계정 비밀번호 변경
| 점검 항목 | 명령 |
|---|---|
| 셸 초기화 파일의 trap | grep -rn 'trap' /etc/profile* /etc/bash* /home/*/.bash* |
| TERM을 무시하는 프로세스 | /proc/*/status의 SigIgn 비트 14(SIGTERM)를 해석 — 11편 방식, 49편에서 스크립트로 자동화 |
| 남은 임시 파일 | find /tmp /var/tmp -user root -mmin +60 -type f |
| 실수 | 결과 | 예방 |
|---|---|---|
| 정리 코드를 스크립트 마지막 줄에만 둠 | 중간 종료 시 실행 안 됨 | trap cleanup EXIT |
| 긴 명령을 포그라운드로 실행 | 트랩이 명령 종료까지 지연 → systemd 타임아웃 | cmd & wait $! |
| trap 문자열을 큰따옴표로 작성 | 변수가 등록 시점 에 확장됨 | 작은따옴표로 작성 |
| INT/TERM 트랩에서 exit 누락 | 트랩 실행 후 스크립트가 계속 진행 | 트랩 끝에 exit N |
| 백그라운드 실행 스크립트에 INT 트랩 | 적용되지 않음 (실측) | TERM 중심 설계, trap -p로 확인 |
[ ] trap EXIT 로 임시 파일을 정리하는 스크립트를 작성했다
[ ] trap '' HUP 으로 HUP 이 무시되는 것을 확인했다
[ ] TERM 트랩 → exit → EXIT 트랩 순서를 확인했다
[ ] trap -p 로 백그라운드 스크립트의 INT 가 무시 상태인 것을 확인했다
[ ] trap 이 없는 스크립트는 임시 파일이 남는 것을 확인했다
[ ] ERR 트랩으로 실패 줄과 명령을 기록했다
trap '명령' SIGNAL은 셸 스크립트의 시그널 핸들러다. ''는 무시, -는 복원이다.& wait $! 패턴으로 실행해야 트랩이 즉시 동작한다.다음 글 「15. 프로세스와 파일 디스크립터 (lsof)」 에서는 프로세스가 연 파일을 확인하는 lsof를 다룬다. 특히 삭제했는데 디스크 용량이 줄지 않는 장애를 재현하고, 삭제된 파일을 /proc/PID/fd로 찾아 비우거나 복구하는 방법을 실습한다.