LabHub
배우기 러닝패스 코스

Operating Systems

See a Process With Your Own Eyes

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

운영체제 책에서 읽은 것들을 직접 만들어 봅니다. 좀비, 고아, 가상 메모리, 열린 파일 — 전부 이 파드 안에서 몇 줄로 만들 수 있습니다.

여기서 배우는 것은 나중에 장애를 볼 때 그대로 쓰입니다. "디스크가 안 비어요", "메모리를 왜 이렇게 많이 쓰죠", "파드가 안 죽어요" 의 답이 전부 여기 있습니다.

볼 곳

리눅스는 커널 상태를 파일로 보여 줍니다.

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)

단계

  1. 프로세스 계보 → 01-tree.txt
  2. 스레드 → 02-threads.txt
  3. 좀비 → 03-zombie.txt
  4. 고아와 PID 1 → 04-orphan.txt
  5. VSZ 와 RSS → 05-memory.txt
  6. 지운 파일 → 06-deleted.txt
  7. TERM 과 KILL → 07-signal.txt
  8. 정리 → 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 만 실제로 건드리는 프로그램을 띄워, VSZRSS 가 어떻게 다른지 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, 참조 가 들어가야 합니다.