리눅스 기초 · 프로세스와 신호 · 이론
시그널: 잡을 수 있는 것과 없는 것
한 줄 요약
시그널은 프로세스에게 보내는 짧은 알림이고, 그중 SIGKILL(9)과 SIGSTOP(19) 단 둘만 프로그램이 잡을 수도, 막을 수도, 무시할 수도 없다. 나머지는 전부 애플리케이션이 처리 방식을 바꿀 수 있다.
왜 이게 필요했나
"프로세스가 안 죽습니다"라는 문장은 사실 최소 네 가지 다른 상황을 뭉뚱그린 말이다.
1. kill을 보냈는데 그대로다 -> 애플리케이션이 SIGTERM을 잡아서 무시하거나, 정리 작업 중이다
2. kill -9도 안 먹는다 -> 프로세스가 D 상태(중단 불가 대기)다. 시그널은 큐에 쌓여 있고 커널에서 돌아와야 처리된다
3. 이미 죽었는데 목록에 보인다 -> 좀비다. 죽일 수 없다, 이미 죽었으니까
4. 컨테이너를 멈췄더니 데이터가 끊겼다 -> 시그널이 PID 1 인 셸에서 멈추고 앱까지 전달되지 않았다
네 상황의 대응이 전부 다르다. 그래서 "안 죽는다"에서 멈추지 않고 어느 계층에서 멈췄는지를 물어야 한다.
어떻게 동작하나
정상 종료는 항상 두 단계다. SIGTERM 을 보내 정리할 시간을 주고, 유예 시간이 지나도 살아 있으면 SIGKILL 을 보낸다. docker stop의 기본 유예가 10초, systemd 의 TimeoutStopSec이 같은 역할을 한다. 셸로는 timeout -k 10 30 명령 한 줄로 같은 정책을 만들 수 있다.
SIGKILL 이 위험한 이유는 정리 기회를 아예 주지 않기 때문이다. 버퍼가 flush 되지 않고, 트랜잭션이 중간에 끊기고, 락 파일과 임시 파일이 남는다. DB나 큐 소비자에게 SIGKILL 은 사실상 전원을 뽑는 것과 같다.
자주 쓰는 시그널의 관례도 알아 두면 좋다. 번호는 아키텍처마다 다를 수 있으니 스크립트에서는 항상 이름을 쓴다.
| 이름 | 기본 동작 | 관례적 용도 |
| --- | --- | --- |
| SIGHUP | 종료 | 설정 재적재(원래 뜻은 터미널 연결 끊김) |
| SIGINT | 종료 | Ctrl-C |
| SIGTERM | 종료 | 정중한 종료 요청 |
| SIGKILL | 종료 | 잡을 수 없음. 최후 수단 |
| SIGSTOP / SIGCONT | 정지 / 재개 | 잠시 멈췄다 다시 |
| SIGUSR1 / SIGUSR2 | 종료 | 앱이 정하기 나름(nginx 는 로그 재오픈 / 바이너리 교체) |
좀비와 고아는 짝으로 이해한다. 자식이 죽으면 커널은 종료 상태를 남겨 두고, 부모가 wait로 그것을 회수해야 목록에서 사라진다. 회수 전 상태가 좀비(Z)다. 좀비는 메모리를 거의 안 쓰지만 PID를 점유하므로 수천 개가 쌓이면 새 프로세스를 못 만드는 지경이 된다. 반대로 부모가 먼저 죽으면 자식은 고아가 되어 PID 1에 입양되고, PID 1이 대신 회수해 준다. 좀비를 없애는 확실한 방법이 "부모를 종료시키는 것"인 이유가 여기 있다.
종료 코드 규약도 기억해 둘 것. 시그널로 죽으면 종료 코드는 128 + 시그널 번호다. SIGTERM 은 143, SIGINT 는 130. CI 로그에서 143을 보면 "누군가 정중히 종료를 요청했다"로 읽으면 된다.
현장에서 만나는 모습
컨테이너 셸 형식 함정. Dockerfile 에 CMD myapp이라고 쓰면 /bin/sh -c가 PID 1이 되고 앱은 그 자식이 된다. 런타임이 보낸 SIGTERM 은 셸이 받고, 셸은 자식에게 전달하지 않는다. 유예 시간이 지나면 SIGKILL 이 cgroup 전체에 적용되어 앱은 아무 정리도 못 하고 죽는다. 해법은 exec 형식(CMD ["myapp"])이거나 init 프로세스를 두는 것이다.
pkill 의 사정거리. pkill -f 는 명령줄 전체를 패턴 매칭하므로 넓게 쓰면 엉뚱한 프로세스까지 죽인다. 반드시 같은 패턴으로 pgrep -a -f 를 먼저 실행해 목록을 눈으로 확인한 뒤 실행한다. 그리고 kill -9 -1 은 절대 실행하지 않는다 - 권한이 닿는 모든 프로세스에 SIGKILL 을 보낸다.
trap 이 늦게 도는 이유. 셸 스크립트에서 sleep 300 실행 중에 도착한 시그널은 그 명령이 끝난 뒤에 처리된다. 즉시 반응하게 하려면 sleep 300 & wait $! 형태로 쓴다.
다음 실습에서 할 것
백그라운드 프로세스를 띄워 PID와 부모 PID를 확인하고, 정지·종료·강제 종료를 각각 보내 상태가 어떻게 달라지는지 본다. SIGTERM 을 잡는 스크립트를 만들어 SIGKILL 은 잡히지 않는다는 것을 직접 증명하고, 좀비를 하나 만들어 본 뒤, 마지막에는 패턴에 맞는 프로세스를 안전하게 정리하는 스크립트를 작성한다.