LabHub

운영체제 · 프로세스와 스레드 · 이론

프로세스 — 운영체제가 파는 격리라는 상품

LabHub 에서 이어서 보기

한 줄 요약

프로세스는 "이 프로그램은 자기만의 컴퓨터를 갖고 있다"는 착각을 운영체제가 만들어 파는 상품이고, 그 착각의 핵심은 독립된 주소 공간이다.

왜 이게 필요했나

한 대의 기계에서 여러 프로그램을 돌리려면 두 가지를 보장해야 한다. 첫째, 한 프로그램의 버그가 다른 프로그램의 메모리를 망가뜨리면 안 된다. 둘째, 프로그램이 자기가 언제 멈추고 언제 재개되는지 신경 쓰지 않아도 돼야 한다.

이 둘을 함께 제공하는 추상이 프로세스다. 각 프로세스는 0번지부터 시작하는 자기만의 가상 주소 공간을 갖고, CPU 를 잠시 빌렸다가 반납하지만 그 사실을 알아차리지 못한다.

어떻게 동작하나

운영체제는 프로세스마다 PCB(Process Control Block) 를 유지한다. 여기에 담기는 것은 대략 이렇다.

컨텍스트 스위치는 현재 프로세스의 레지스터를 PCB 에 저장하고 다음 프로세스의 것을 복원하는 일이다. 레지스터 몇십 개를 옮기는 것 자체는 짧지만, 진짜 비용은 그다음에 온다. 새 프로세스가 실행되기 시작하면 캐시와 TLB 에 남아 있던 이전 프로세스의 내용이 쓸모없어져 미스가 쏟아진다. 이 간접 비용이 직접 비용보다 훨씬 크다.

리눅스에서 프로세스를 만드는 방식도 특징적이다. fork() 는 부모를 통째로 복제하고 exec() 가 그 위에 새 프로그램을 덮어쓴다. 통째로 복제한다는 말이 무섭게 들리지만 실제로는 쓰기 시 복사(copy-on-write) 로 처리된다. 부모와 자식이 같은 물리 페이지를 공유하다가, 누군가 쓰려는 순간 그 페이지만 복사한다. 곧바로 exec() 를 호출하는 흔한 패턴에서는 복사가 거의 일어나지 않는다.

프로세스 상태 전이에서 자주 헷갈리는 것이 좀비다. 자식이 끝났는데 부모가 아직 wait() 로 종료 상태를 거두지 않으면 PCB 만 남은 좀비가 된다. 좀비는 메모리를 거의 쓰지 않지만 프로세스 테이블 항목을 차지하므로, 쌓이면 새 프로세스를 못 만든다. 반대로 부모가 먼저 죽으면 자식은 고아가 되어 init(또는 컨테이너의 1번 프로세스)에 입양된다. 컨테이너 안에서 애플리케이션을 1번 프로세스로 띄웠을 때 좀비가 쌓이는 사고가 여기서 나온다. 1번은 고아를 거둘 책임이 있는데 보통의 애플리케이션은 그 코드를 갖고 있지 않다.

현장에서 만나는 모습

ps 출력의 STAT 열에서 Z 를 보면 위 상황이다. 컨테이너 이미지에 tini 같은 작은 init 을 넣거나 런타임의 init 옵션을 켜는 것이 표준 대응이다. D 상태(중단 불가 대기)도 알아 둘 만한데, 대개 디스크나 네트워크 파일시스템 I/O 를 기다리는 중이며 이 상태의 프로세스는 kill -9 로도 즉시 죽지 않는다.

이어지는 퀴즈에서 확인할 것

fork 가 왜 비싸 보이는데 실제로는 싸게 끝나는지, 컨텍스트 스위치의 진짜 비용이 어디에 있는지 설명할 수 있는지 확인한다.