테마
프로세스와 스레드 2
프로세스와 스레드에서 본 프로세스(process)가 어떻게 만들어지는지, 그리고 한 프로세스 안에서 실행 흐름을 여러 개 돌리는 스레드(thread)를 이어서 다룬다. 운영체제는 프로세스마다 메모리를 따로 떼어 주어 서로를 보호하고, 한 프로세스 안의 스레드들은 그 메모리를 함께 쓴다. 이 "나눠 주느냐, 함께 쓰느냐"의 차이가 격리·통신 비용·전환 비용의 차이를 모두 만든다.
프로세스 만들기: fork와 exec
유닉스 계열 운영체제에서는 두 시스템 콜(system call)을 조합해 새 프로그램을 실행한다.
fork()는 호출한 프로세스를 거의 그대로 복제해 자식 프로세스를 만든다. 부모에게는 자식의 PID(Process ID, 프로세스 식별 번호)가, 자식에게는 0이 반환되어 둘이 다른 길로 갈 수 있다. 메모리를 실제로 전부 복사하지는 않는다. 부모와 자식이 같은 물리 메모리를 공유하다가 한쪽이 쓰는 순간 그 페이지(운영체제가 메모리를 관리하는 같은 크기 조각, 보통 4KB)만 복사하는 쓰기 시 복사(copy-on-write)를 써서 비용을 줄인다.exec()계열은 지금 프로세스의 메모리를 새 프로그램으로 통째로 갈아 끼운다. PID는 그대로다.
자식이 끝나면 부모가 wait()로 종료 상태를 거둬 가야 한다. 거둬 가기 전까지 커널(kernel)은 부모가 읽을 수 있도록 자식의 PID와 종료 정보를 지우지 않고 남겨 두기 때문이다. 부모가 거둬 가지 않으면 자식은 실행은 끝났지만 프로세스 표에 이름만 남은 좀비 프로세스(zombie process)가 된다. 반대로 부모가 먼저 끝난 자식은 고아 프로세스(orphan process)가 되고, 리눅스에서는 부팅 때 커널이 가장 먼저 실행하는 PID 1번 init 같은 프로세스가 새 부모가 되어 거둬 간다.
스레드: 프로세스 안의 실행 흐름
스레드는 한 프로세스 안에서 독립적으로 실행되는 흐름이다. 같은 프로세스의 스레드들은 주소 공간(address space, 프로세스가 쓸 수 있는 메모리 주소의 범위)을 공유하므로, 한 스레드가 힙(heap)에 만든 객체를 다른 스레드가 포인터(pointer) 하나로 바로 읽을 수 있다. 대신 각자 따로 실행되려면 "어디까지 실행했는지"와 "함수 호출 기록"은 각자 가져야 한다. 앞의 것은 레지스터(register) 값에, 뒤의 것은 스택(stack)에 담긴다.
| 스레드끼리 공유 | 스레드마다 따로 |
|---|---|
| 코드·데이터·힙 영역 | 스택 |
| 열린 파일 등 프로세스 자원 | 레지스터 값(프로그램 카운터 포함) |
| PID | 스레드 ID |
같은 프로세스 안의 스레드 사이 문맥 교환(context switch)은 주소 공간을 바꾸지 않는다. 그래서 프로세스 사이 전환보다 저장·복원할 것이 적고, 캐시 메모리(cache memory)에 올라온 공유 데이터도 계속 쓸 수 있어 싸다. 주소 변환 결과를 담아 둔 TLB(Translation Lookaside Buffer, 변환 색인 버퍼)도 비우지 않아도 된다. 반면 메모리를 함께 쓰기 때문에 두 스레드가 같은 변수를 동시에 고치면 결과가 실행 순서에 따라 달라지는 경쟁 상태(race condition)가 생긴다. 이를 막는 방법은 동기화에서 다룬다.
멀티프로세스와 멀티스레드
일을 여러 실행 흐름에 나누는 방법은 두 가지다. 프로세스를 여러 개 띄우는 멀티프로세스(multiprocessing)와, 한 프로세스 안에 스레드를 여러 개 두는 멀티스레드(multithreading)다.
| 기준 | 멀티프로세스 | 멀티스레드 |
|---|---|---|
| 메모리 | 프로세스마다 분리 | 한 주소 공간을 공유 |
| 장애 전파 | 한 프로세스가 죽어도 다른 프로세스는 산다 | 한 스레드의 치명적 오류가 프로세스 전체를 죽인다 |
| 데이터 주고받기 | 운영체제가 제공하는 프로세스 간 통신이 필요하다 | 같은 메모리를 바로 읽고 쓴다(대신 동기화 필요) |
| 생성·전환 비용 | 크다 | 작다 |
스레드가 잘못된 메모리에 접근하는 등 치명적 오류를 내면, 운영체제는 그 주소 공간을 함께 쓰는 프로세스 전체를 종료한다. 생성 비용도 다르다. 프로세스를 만들려면 커널이 주소 공간과 페이지 테이블(page table)을 새로 준비해야 하지만, 스레드는 스택과 레지스터 저장 공간만 만들면 된다.
크롬 같은 브라우저는 사이트별로 웹 페이지를 해석해 화면에 그리는 렌더링 프로세스를 나눠, 한 페이지가 죽거나 공격당해도 다른 사이트에 번지지 않게 한다. 격리를 위해 비용을 치르는 선택이다. 반대로 많은 웹 서버와 Java 애플리케이션 서버는 요청마다 스레드를 배정해, 싼 생성 비용과 쉬운 데이터 공유를 택한다.
면접 포인트
- "프로세스와 스레드의 차이는?" → 자원 할당 단위 vs 실행 단위. 스레드는 코드·데이터·힙을 공유하고 스택·레지스터만 따로 가진다.
- "
fork()는 메모리를 전부 복사하나요?" → 아니다. 쓰기 시 복사로 물리 페이지를 공유하다가 한쪽이 쓰는 페이지만 복사한다. 새 프로그램으로 갈아 끼우는 건exec()다. - "좀비 프로세스란?" → 실행은 끝났지만 부모가
wait()로 거둬 가지 않아 프로세스 표에 남은 자식. 고아 프로세스는 부모가 먼저 끝난 자식이다. - "스레드 전환이 왜 더 싼가요?" → 주소 공간을 바꾸지 않아 저장·복원이 적고, 캐시 메모리와 TLB 내용이 유지된다.
- "멀티스레드의 단점은?" → 동기화 필요, 한 스레드의 오류가 프로세스 전체로 번짐, 디버깅이 어렵다.
- 스택까지 공유한다고 말하면 틀린다. 스택은 스레드마다 따로다.
