LabHub

LFCS — 리눅스 재단 시스템 관리자 · 실행 중인 시스템 운영 · 이론

프로세스, 시그널, 그리고 /proc 이 파일시스템인 이유

LabHub 에서 이어서 보기

한 줄 요약

"프로세스가 안 죽는다" 는 증상은 네 가지 서로 다른 원인에서 나온다. 시그널을 잡아 무시하고 있거나, 잡을 수 없는 상태(D)에 있거나, 시그널이 엉뚱한 대상에게 갔거나, 애초에 죽었는데 부모가 회수하지 않은 것(좀비)이다. 구분하는 방법은 전부 /proc 에 있다.

왜 이게 필요했나

배포 스크립트가 kill 을 보냈는데 프로세스가 남아 있다. 여기서 곧장 kill -9 로 가는 손이 문제다. SIGKILL 은 애플리케이션에 정리할 기회를 주지 않는다. 열린 파일의 버퍼는 플러시되지 않고, 트랜잭션은 중간에 끊기고, 락 파일은 남는다. 데이터베이스나 큐 소비자에게 SIGKILL 을 보내는 것은 전원을 뽑는 것과 같다.

올바른 절차는 언제나 두 단계다. SIGTERM 을 보내고, 유예 시간을 주고, 그래도 살아 있으면 그때 SIGKILL 이다. 컨테이너 런타임과 쿠버네티스가 하는 일도 정확히 이것이고, 유예 시간은 설정값이다.

어떻게 동작하나

상태 한 글자가 진단의 출발점이다.

| 코드 | 의미 | 실무적 함의 |
| --- | --- | --- |
| R | 실행 중이거나 실행 대기 | CPU 를 쓰거나 받으려고 줄 서 있음 |
| S | 중단 가능 대기 | 정상적인 대기. 시그널로 깨울 수 있음 |
| D | 중단 불가 대기 | SIGKILL 도 즉시 통하지 않음. 대개 디스크나 NFS |
| Z | 좀비 | 이미 죽었고 부모가 회수하지 않은 상태 |
| T | 정지됨 | SIGSTOP 또는 SIGTSTP 를 받음 |

D 가 보이면 시그널로는 해결되지 않는다. 커널이 무엇을 기다리는지를 /proc/<PID>/wchan 이나 /proc/<PID>/stack 에서 봐야 한다.

SIGKILL 과 SIGSTOP 은 잡을 수도, 차단할 수도, 무시할 수도 없다. 나머지 시그널은 전부 애플리케이션이 처리 방식을 바꿀 수 있다. 그래서 kill -TERM 이 통하지 않는 프로그램이 존재하는 것이고, 그것은 버그가 아니라 설계일 수 있다. 어떤 시그널을 잡고 있는지는 /proc/<PID>/statusSigCgt·SigBlk·SigIgn 비트마스크에 그대로 적혀 있다.

/proc 이 파일시스템인 이유. 커널의 내부 자료구조를 사용자 공간에 보여 주려면 새 시스템 콜을 잔뜩 만들거나, 이미 있는 인터페이스를 재사용해야 한다. 유닉스는 두 번째를 골랐다. 그래서 프로세스 정보는 cat 으로 읽히고, 열린 파일 디스크립터는 /proc/<PID>/fd/ 아래에 심볼릭 링크로 보이며, 그 링크를 그대로 cp 하면 삭제된 파일까지 복구된다. 리소스 한도는 /proc/<PID>/limits 에 있어서, 내 셸의 ulimit 이 아니라 그 프로세스가 실제로 받은 한도를 확인할 수 있다.

nice 와 ulimit 은 둘 다 상속된다. nice 값은 -20 에서 19 까지이고 낮을수록 우선순위가 높다. 일반 사용자는 값을 올릴 수만 있고 낮추지 못한다. ulimit 의 soft 한도는 hard 한도 아래에서 자유롭게 조정할 수 있지만, hard 한도를 올리는 것은 root 만 가능하다. 그리고 두 값 모두 자식 프로세스에 그대로 상속되기 때문에, 셸에서 한 번 낮춰 놓은 한도가 그 아래에서 뜬 모든 프로세스에 따라붙는다. "왜 이 데몬만 파일을 못 여나" 의 답이 여기 있는 경우가 많다.

현장에서 만나는 모습

저자가 정리한 systemd 문제 사례 중 세 가지가 특히 자주 나온다. 첫째, Restart=on-failure 만 걸고 재시작 제한을 두지 않으면 설정 오류로 크래시하는 서비스가 무한 재시작에 들어가 CPU 를 태우고 로그로 디스크를 채운다. 제한을 거는 StartLimitIntervalSecStartLimitBurst[Service] 가 아니라 [Unit] 섹션 지시어라, 잘못된 섹션에 넣으면 조용히 무시된다.

둘째, ExecStart= 에는 파이프와 리다이렉션이 통하지 않는다. systemd 가 셸을 거치지 않고 직접 실행하기 때문이다. 셸 기능이 필요하면 셸을 명시적으로 실행해야 하고, 로그는 표준 출력을 저널로 보내는 편이 낫다. 셋째, 종료 코드에 원인이 적혀 있다 — 203/EXEC 는 실행 경로가 틀렸거나 실행 권한이 없는 것이고, 217/USERUser= 에 적은 계정이 없는 것이다.

컨테이너 쪽 함정도 하나 있다. 명령을 셸 형식으로 쓰면 셸이 PID 1 이 되고 애플리케이션은 그 자식이 된다. 이때 SIGTERM 은 셸에게 가고 셸은 그것을 자식에게 전달하지 않으므로, 애플리케이션은 유예를 한 번도 못 받고 SIGKILL 로 죽는다.

다음 실습에서 할 것

이 실습 환경에서는 systemctl 이 동작하지 않는다. 그래서 서비스 실습은 유닛 파일을 정확히 쓰는 것에 집중한다. 섹션 구성과 실행·재시작 지시어, 타이머 유닛, /etc/cron.d 와 사용자 crontab 의 형식 차이, cron 의 짧은 PATH 문제, 로그 로테이션 설정, 그리고 드롭인 오버라이드에서 목록형 지시어를 비우는 관용구까지 다룬다. 이어지는 실습에서는 부팅 순서와 GRUB 설정을 정리하고, /proc 을 직접 읽고, 백그라운드 프로세스에 시그널을 보내 상태 변화를 확인한다.