プロセスを目で見る
한국어 원문으로 표시합니다.
목표
운영체제 책에서 읽은 것들을 직접 만들어 봅니다. 좀비, 고아, 가상 메모리, 열린 파일 — 전부 이 파드 안에서 몇 줄로 만들 수 있습니다.
여기서 배우는 것은 나중에 장애를 볼 때 그대로 쓰입니다. "디스크가 안 비어요", "메모리를 왜 이렇게 많이 쓰죠", "파드가 안 죽어요" 의 답이 전부 여기 있습니다.
볼 곳
리눅스는 커널 상태를 파일로 보여 줍니다.
ps -eo pid,ppid,stat,comm # 프로세스 목록
ls /proc/<PID>/task # 그 프로세스의 스레드들
ls -l /proc/<PID>/fd # 열어 둔 파일들
cat /proc/<PID>/status # 메모리를 포함한 상태 전부
/proc 은 진짜 디스크가 아니라 커널이 만들어 주는 화면입니다.
백그라운드로 띄울 때
파이썬 출력이 안 보이면 flush=True 를 빼먹은 것입니다.
print(os.getpid(), flush=True)
단계
- 프로세스 계보 →
01-tree.txt - 스레드 →
02-threads.txt - 좀비 →
03-zombie.txt - 고아와 PID 1 →
04-orphan.txt - VSZ 와 RSS →
05-memory.txt - 지운 파일 →
06-deleted.txt - TERM 과 KILL →
07-signal.txt - 정리 →
08-notes.md
참고
5단계와 6단계는 숫자를 둘 다 남겨야 합니다. 전후 비교가 그 단계의 전부이기 때문입니다.
누가 누구를 낳았나
지금 파드에서 도는 프로세스의 부모–자식 관계를 뽑아 01-tree.txt 에 남기세요. 자기 셸의 PID 와 그 부모도 함께 적습니다.
ps -eo pid,ppid,stat,comm. 내 셸은 echo $$, 그 부모는 ps -o ppid= -p $$.
모든 프로세스에는 부모가 있습니다. 부모가 자식을 fork 로 만들고, 자식이 끝나면 부모가 wait 로 결과를 거둡니다. 이 두 문장이 다음 세 단계의 전부입니다.
스레드는 어디에 있나
스레드를 4개 만드는 프로그램을 띄우고, ps 에는 프로세스가 하나로 보이지만 실제로는 여럿이라는 것을 02-threads.txt 에 보이세요.
파이썬이면 threading.Thread 4개면 됩니다. print(os.getpid(), flush=True) 로 PID 를 찍으세요(flush=True 를 빼면 출력이 안 보입니다).
확인은 ls /proc/<PID>/task | wc -l — 메인 1개 + 스레드 4개 = 5가 나옵니다. 리눅스에서 스레드는 메모리를 공유하는 태스크일 뿐이고, 커널이 보기에 프로세스와 크게 다르지 않습니다.
좀비를 만든다
자식이 먼저 끝났는데 부모가 거두지 않는 상태를 만들어 ps 에서 Z 로 보이는 것을 03-zombie.txt 에 남기세요.
os.fork() 로 자식을 만들고 자식은 os._exit(0), 부모는 wait() 를 안 부르고 자고 있으면 됩니다.
좀비는 메모리를 쓰지 않습니다. 끝났다는 사실과 종료 코드만 남아 있는 자리입니다. 그런데 PID 는 차지하므로, 쌓이면 새 프로세스를 못 만듭니다.
부모가 먼저 죽으면
부모가 자식보다 먼저 죽게 만들고, 그 자식의 PPID 가 무엇으로 바뀌는지 04-orphan.txt 에 남기세요. 이 파드의 PID 1 이 무엇인지도 함께.
ps -o ppid= -p <자식PID> 로 확인합니다. 부모를 잃은 프로세스는 PID 1 에게 넘겨집니다(재부모화).
그리고 ps -o comm= -p 1 을 보세요. 컨테이너의 PID 1 은 대개 여러분의 애플리케이션이지 init 이 아닙니다. init 이 아니면 넘겨받은 좀비를 거두지 않습니다 — 컨테이너에서 좀비가 쌓이는 이유이고, --init 이나 tini 를 쓰는 이유입니다.
예약한 메모리와 실제로 쓴 메모리
300MB 를 예약만 하고 그중 80MB 만 실제로 건드리는 프로그램을 띄워, VSZ 와 RSS 가 어떻게 다른지 05-memory.txt 에 남기세요.
mmap.mmap(-1, 300*1024*1024) 로 예약하고, 건드리기 전후의 ps -o vsz=,rss= -p <PID> 를 둘 다 남기세요.
bytearray(300*1024*1024) 는 안 됩니다 — 파이썬이 0으로 채우면서 이미 다 건드려 버립니다.
VSZ 는 주소를 잡아 둔 것, RSS 는 실제로 물리 메모리가 붙은 것입니다. 300MB 예약에 RSS 는 10MB 도 안 됩니다.
지웠는데 용량이 안 돌아온다
64MB 파일을 만들어 연 채로 지운 뒤, df 로 용량이 돌아오지 않는 것을 보이고 프로세스를 죽이면 돌아오는 것까지 06-deleted.txt 에 남기세요.
dd if=/dev/zero of=big.bin bs=1M count=64 로 만들고, 파이썬으로 open() 한 뒤 os.remove() 하고 자면 됩니다.
증거는 여기 있습니다 — ls -l /proc/<PID>/fd/ 에 big.bin (deleted) 로 남습니다.
파일 이름을 지우는 것과 데이터를 지우는 것은 다릅니다. 마지막 참조(이름이든 열린 fd 든)가 사라져야 블록이 반환됩니다. 로그를 지웠는데 디스크가 안 비는 사고의 원인이 정확히 이것입니다.
TERM 과 KILL 의 차이
SIGTERM 을 받아 처리하되 죽지는 않는 프로그램을 만들어, TERM 에는 살아남고 KILL 에는 죽는 것을 07-signal.txt 에 남기세요.
signal.signal(signal.SIGTERM, 핸들러). 확인은 kill -TERM <PID> 뒤 kill -0 <PID>(살아 있으면 성공), 그다음 kill -KILL <PID>.
TERM 은 부탁이고 KILL 은 통보입니다. TERM 은 프로그램이 받아서 정리할 기회를 주고, KILL 은 커널이 그냥 없앱니다 — 그래서 뒷정리를 못 합니다. 쿠버네티스가 종료할 때 TERM 을 먼저 보내고 terminationGracePeriodSeconds 를 기다렸다가 KILL 하는 이유입니다.
세 가지를 정리한다
08-notes.md 에 세 줄 이상. 좀비가 무엇이고 왜 생기는지, VSZ 와 RSS 의 차이, 지운 파일의 용량이 언제 돌아오는지.
본문에 좀비, RSS, 참조 가 들어가야 합니다.