테마
Chapter 03. 프로세스와 스레드
운영체제가 관리하는 두 가지 핵심 실행 단위인 **프로세스(Process)**와 **스레드(Thread)**를 깊이 있게 살펴본다. CPU와 메모리라는 전산 자원이 어떻게 할당되는지, 프로세스의 생성과 상태 전이, 문맥 교환(Context Switching), 그리고 멀티스레딩(Multithreading)까지 다룬다.
3.1 전산 자원의 두 축: CPU와 메모리
컴퓨터의 핵심 자원
컴퓨터가 프로그램을 실행할 때 사용하는 핵심 자원은 딱 두 가지다.
| 자원 | 역할 | 할당 단위 |
|---|---|---|
| CPU (Central Processing Unit) | 명령어를 해석하고 연산을 수행 | 스레드(Thread) 단위로 사용 |
| RAM (Random Access Memory) | 실행 중인 프로그램의 데이터를 저장 | 프로세스(Process) 단위로 할당 |
이 두 자원의 할당 방식 차이가 프로세스와 스레드를 이해하는 핵심 열쇠다.
자원 할당의 원칙
운영체제는 다음과 같은 원칙으로 자원을 관리한다.
- **가상 메모리 공간(Virtual Memory Space, VMS)**은 프로세스 단위로 할당된다.
- CPU 실행 시간은 스레드 단위로 배분된다.
- Windows: 스레드를 스케줄링 단위로 사용
- Unix/Linux: 전통적으로 프로세스를 스케줄링 단위로 사용 (현대 리눅스에서는 스레드도 경량 프로세스로 취급)
- **권한(접근 제어)**은 프로세스에게 부여된다. 그 안의 스레드들은 프로세스의 권한을 공유한다.
프로세스 간 메모리 격리
가상 메모리 공간은 프로세스마다 완전히 독립적이다. 프로세스 A가 사용하는 메모리 공간에 프로세스 B는 절대 접근할 수 없다. 이것이 운영체제가 보장하는 **메모리 보호(Memory Protection)**다.
반면, 같은 프로세스 안의 스레드들은 그 프로세스의 가상 메모리 공간을 자유롭게 공유한다. 코드 영역, 데이터 영역, 힙(Heap) 영역 모두 함께 사용할 수 있다.
비유: 집과 사람
프로세스와 스레드의 관계를 집과 거기 사는 사람에 비유하면 이해가 쉽다.
| 비유 | 실제 | 설명 |
|---|---|---|
| 집 | 프로세스 (Process) | 독립적인 공간을 가진다. 다른 집 사람이 마음대로 들어올 수 없다. |
| 사는 사람 | 스레드 (Thread) | 집 안의 거실, 주방, 화장실을 자유롭게 사용한다. |
| 집 열쇠/권한 | 접근 제어 (Access Control) | 집에 부여된 권한이고, 거기 사는 사람들이 공유한다. |
| 집 주소 | 가상 메모리 공간 (VMS) | 각 집마다 고유한 주소가 있고, 다른 집과 구별된다. |
핵심: CPU는 스레드가 사용하고, 메모리는 프로세스가 소유한다. 프로세스는 독립된 집이고, 스레드는 그 집에 사는 사람이다.
3.2 프로세스 제어 블록 (PCB - Process Control Block)
PCB란?
운영체제가 프로세스를 관리하기 위해 사용하는 **자료구조(Data Structure)**다. 프로세스 하나가 생성될 때마다 OS 커널 내부에 해당 프로세스의 PCB가 하나 만들어진다.
PCB에는 프로세스의 모든 관리 정보가 담겨 있다. 마치 회사에서 직원 한 명당 인사 카드를 만들어 관리하는 것과 같다.
PCB에 포함되는 정보
| 항목 | 영문 | 설명 |
|---|---|---|
| 프로세스 ID | PID (Process ID) | 양의 정수. 프로세스를 식별하는 고유한 번호. 시스템 전체에서 유일하다. |
| 프로그램 카운터 | PC (Program Counter) | 현재 실행 중인(또는 다음에 실행할) 기계어 명령의 메모리 주소. CPU가 "지금 어디까지 읽었는지"를 기록한다. |
| 레지스터 정보 | Register Set | CPU 레지스터들의 상태를 백업해둔 값. 문맥 교환 시 저장/복원에 사용된다. |
| 프로세스 상태 | Process State | 현재 상태 (Ready, Running, Waiting 등) |
| 메모리 관련 정보 | Memory Info | 가상 메모리 공간의 시작 주소, 페이지 테이블 정보 등 |
| 스케줄링 정보 | Scheduling Info | 우선순위(Priority), CPU 사용 시간 등 |
| I/O 상태 정보 | I/O Status | 열린 파일 목록, 할당된 I/O 장치 정보 |
프로세스 메모리 구조 (Memory Layout)
프로세스에게 할당된 가상 메모리 공간은 다음과 같은 영역으로 나뉜다.
| 영역 | 저장 내용 | 특징 |
|---|---|---|
| Stack | 지역 변수, 함수 매개변수, 복귀 주소 | 함수 호출 시 자동 생성, 리턴 시 자동 해제. 위에서 아래로 성장. |
| Heap | malloc(), new로 동적 할당한 메모리 | 프로그래머가 직접 할당/해제 관리. 아래에서 위로 성장. |
| Data(Static) | 전역 변수, 정적 변수, 문자열 상수 | 프로그램 시작 시 할당, 종료 시 해제. BSS(미초기화) + Data(초기화) 세그먼트. |
| Code(Text) | 컴파일된 기계어 명령 코드 | 읽기 전용(Read-Only). 실행 중 변경 불가. |
Stack과 Heap이 서로 반대 방향으로 성장하는 이유는, 한정된 메모리 공간을 최대한 효율적으로 사용하기 위해서다. 두 영역이 같은 방향으로 자라면, 한쪽이 남아도 다른 쪽이 부족할 수 있다. 반대 방향이면 남은 공간을 양쪽이 공유한다.
TCB (Thread Control Block)
PCB가 프로세스를 관리하는 자료구조라면, **TCB(Thread Control Block)**는 스레드를 관리하는 자료구조다.
TCB에는 스레드 고유의 정보만 들어간다.
| PCB (프로세스 고유) | TCB (스레드 고유) |
|---|---|
| PID | 스레드 ID (TID) |
| 가상 메모리 공간 정보 | 프로그램 카운터 (PC) |
| 열린 파일 목록 | 레지스터 상태 |
| 접근 권한 정보 | 스레드 고유 스택 포인터 |
| 프로세스 상태 | 스레드 상태 (Ready/Running/Waiting) |
같은 프로세스에 속한 스레드들은 PCB의 메모리 정보, 파일 정보, 권한 정보를 공유하고, TCB의 PC, 레지스터, 스택만 독립적으로 유지한다.
3.3 프로세스 상태 전이
프로세스의 라이프사이클
프로세스는 생성부터 종료까지 여러 **상태(State)**를 거친다. 이 상태 변화를 **상태 전이(State Transition)**라고 부른다.
| 상태 | 영문 | 설명 |
|---|---|---|
| 생성 | New | 프로세스가 막 생성된 상태. PCB 할당, 메모리 적재 등 초기화 진행 중. |
| 준비 | Ready | 실행 준비 완료. CPU만 할당받으면 바로 실행할 수 있는 상태. |
| 실행 | Running | CPU를 할당받아 실제로 명령을 수행 중인 상태. |
| 대기 | Waiting (Blocked) | I/O 요청 등으로 CPU를 사용할 수 없어 기다리는 상태. |
| 완료 | Terminated | 실행이 끝난 상태. 자원 회수 대기 중. |
Ready Queue와 디스패치(Dispatch)
Ready Queue는 CPU 할당을 기다리는 프로세스들이 줄을 서 있는 대기열(Queue) 자료구조다.
**디스패치(Dispatch)**란 OS 스케줄러가 Ready Queue에서 프로세스 하나를 선택해서 꺼내 CPU에 할당하는 동작을 말한다.
- 디스패처(Dispatcher) = 운행원, 관리원이라는 뜻
- "줄 서 있는 사람들 중에서 한 명을 골라 보내는 것"
비유: 마치 허준 드라마의 명대사 **"줄을 서시오!"**처럼, 프로세스들은 CPU를 사용하기 위해 줄(Queue)을 서야 한다. OS가 순서대로(또는 우선순위에 따라) 하나씩 불러낸다.
시분할 시스템 (Time-Sharing System)
현대 컴퓨터에는 보통 4~16개의 CPU 코어가 있다. 하지만 동시에 실행되어야 하는 스레드는 수천 개에 달한다.
예를 들어 2000개의 스레드가 8개의 코어를 사용해야 한다면?
운영체제는 시분할(Time-sharing) 방식으로 해결한다.
- 각 스레드에게 아주 짧은 시간(Time Quantum, 보통 수 ms ~ 수십 ms)만큼 CPU를 할당한다.
- 시간이 다 되면 강제로 CPU를 회수하고, 다음 스레드에게 넘긴다.
- 이 전환이 매우 빠르게 일어나므로, 인간의 눈에는 모든 프로그램이 동시에 실행되는 것처럼 보인다.
입출력과 대기 상태
프로세스가 I/O(입출력) 요청을 하면 어떻게 될까? 두 가지 방식이 있다.
| 방식 | 설명 | 프로세스 상태 |
|---|---|---|
| Blocking I/O | I/O 요청 후 완료될 때까지 기다린다 | Running -> Waiting 상태로 전환 |
| Non-blocking I/O | I/O 요청만 보내고 바로 돌아온다 | Running 상태 유지 |
Blocking I/O에서는 I/O가 완료될 때까지 프로세스가 Waiting 상태에 머무르므로 CPU를 사용하지 않는다. I/O가 끝나면 다시 Ready 상태로 돌아가 Queue에 합류한다.
Sleep vs Suspend
프로세스가 Ready Queue에서 빠지는 경우가 두 가지 더 있다. **자발적 이탈(Sleep)**과 **강제 이탈(Suspend)**이다.
Sleep (휴식 상태) - "나 좀 잘게"
Sleep은 프로세스가 스스로 일정 시간 동안 CPU를 사용하지 않겠다고 선언하는 것이다.
sleep(10ms) // "10밀리초 동안 쉴게"동작 과정:
- 프로세스가
sleep(10ms)호출 - Ready Queue에서 자발적으로 이탈
- 10ms가 지나면 Ready Queue 맨 뒤에 다시 합류
- 자기 차례가 올 때까지 다시 대기
실제 대기 시간 = 지정 시간(10ms) + 알파(alpha)
이 "알파"는 Ready Queue 앞에 줄 서 있는 프로세스들이 처리되는 시간이다. 큐에 몇 개의 프로세스가 있는지, 각각 얼마나 CPU를 사용하는지에 따라 달라지므로 예측이 불가능하다.
이 예측 불가능한 "알파" 값을 이용해 **난수(Random Number)**를 생성하는 기법도 있을 정도다. 그만큼 시스템 상황에 따라 변동이 크다.
Suspend (보류 상태) - "너 좀 비켜봐"
Suspend는 프로세스 외부의 요인에 의해 강제로 Queue에서 빠지는 것이다.
발생 원인:
- 스왑(Swap) 발생: 메모리 부족으로 OS가 프로세스를 디스크로 내보냄
- 오류 발생: 프로세스에 문제가 생겨 OS가 강제 중단
- 관리자 개입: 시스템 관리자가 의도적으로 프로세스를 중단
Suspend된 프로세스도 재진입 시 Queue 맨 뒤에 합류하므로, 역시 "알파" 시간이 발생한다.
| 구분 | Sleep | Suspend |
|---|---|---|
| 주체 | 프로세스 자신 (자발적) | OS 또는 외부 (강제적) |
| 비유 | "나 좀 잘게" | "너 좀 비켜봐" |
| 원인 | 프로그램 내 sleep() 호출 | 메모리 부족, 오류, 관리자 개입 |
| 재진입 | 지정 시간 후 Ready Queue 맨 뒤 | 원인 해소 후 Ready Queue 맨 뒤 |
프로세스 상태 전이 다이어그램
3.4 문맥 교환 (Context Switching)
문맥(Context)이란?
프로세스의 실행 흐름 상태 전체를 **문맥(Context)**이라고 한다. 구체적으로는 다음 정보를 포함한다.
- 프로그램 카운터(PC): 어디까지 실행했는지
- CPU 레지스터 값들: 연산 중간 결과
- 프로세스 상태: Ready, Running 등
- 메모리 관리 정보: 페이지 테이블 등
문맥 교환이란?
한 프로세스에서 다른 프로세스로 CPU 제어권이 넘어가는 과정을 **문맥 교환(Context Switching)**이라고 한다.
시분할 시스템에서 타임 퀀텀(Time Quantum)이 만료되면, OS는 현재 프로세스를 중단하고 다음 프로세스를 실행해야 한다. 이때 반드시 문맥 교환이 발생한다.
문맥 교환의 과정
단계별로 정리하면 다음과 같다.
| 단계 | 동작 | 설명 |
|---|---|---|
| 1단계 | 현재 프로세스 상태 저장 | 프로세스 A의 PC, 레지스터 값을 A의 PCB에 저장 |
| 2단계 | 다음 프로세스 선택 | OS 스케줄러가 Ready Queue에서 프로세스 B를 선택 (디스패치) |
| 3단계 | 새 프로세스 상태 복원 | 프로세스 B의 PCB에서 PC, 레지스터 값을 CPU에 로드 |
| 4단계 | 실행 시작 | 프로세스 B가 이전에 멈춘 지점부터 실행 재개 |
문맥 교환의 비용
문맥 교환은 **순수 오버헤드(Overhead)**다. 교환이 일어나는 동안에는 어떤 프로세스도 유용한 작업을 하지 않는다. 그래서 문맥 교환이 너무 자주 일어나면 시스템 성능이 떨어진다.
비유: 고속도로에서 차선 변경을 생각해보자. 차선을 바꾸는 동안에는 앞으로 나아가지 못하고, 핸들을 돌리고 미러를 확인하는 데 시간을 소비한다. 차선 변경(문맥 교환)을 너무 자주 하면, 정작 목적지까지 가는 속도(실제 연산)는 느려진다. 하지만 차선 변경 없이는 여러 차(프로세스)가 도로(CPU)를 공유할 수 없다.
3.5 프로세스 생성: fork() vs exec()
프로세스 생성이 복잡한 이유
새 프로세스를 생성하려면 다음 작업이 필요하다.
- 가상 메모리 공간(VMS) 할당 - 페이지 테이블 생성, 메모리 매핑
- PCB 생성 - 프로세스 관리 정보 초기화
- 실행 파일 로딩 - 디스크에서 코드/데이터를 메모리에 적재
- 스택/힙 초기화 - 실행에 필요한 메모리 영역 설정
이 모든 작업을 처음부터 하는 것은 매우 비용이 큰 작업이다.
부모-자식 관계
프로세스 생성에는 항상 부모-자식 관계가 존재한다.
| 용어 | 설명 |
|---|---|
| 부모 프로세스 (Parent Process) | 새 프로세스를 만드는 원래 프로세스 |
| 자식 프로세스 (Child Process) | 새로 만들어진 프로세스 |
실행 파일 구조
프로그램이 디스크에 저장되어 있을 때의 형식은 운영체제마다 다르다.
| OS | 형식 | 정식 명칭 |
|---|---|---|
| Windows | PE | Portable Executable |
| Unix/Linux | ELF | Executable and Linkable Format |
두 형식 모두 기본 구조는 비슷하다: 헤더(Header) + 섹션(Section)(Code/Text, Data 등)
fork() - 통째로 복사
fork()는 부모 프로세스를 통째로 복사하여 새 프로세스를 만드는 시스템 콜이다.
복사되는 것:
- 가상 메모리 공간 (VMS) 전체
- PCB (새 PID가 부여됨)
- 환경변수
- 열린 파일 디스크립터
- 코드, 데이터, 힙, 스택 영역 전부
결과: 프로세스가 2개 존재하게 된다 (부모 + 자식).
fork() 호출 전: [부모 프로세스] 1개
fork() 호출 후: [부모 프로세스] + [자식 프로세스] = 2개왜 처음부터 새로 만들지 않고 복사하는가? 이미 존재하는 프로세스를 복사하는 것이 처음부터 새로 VMS를 구성하고, PCB를 세팅하고, 실행 파일을 로딩하는 것보다 훨씬 빠르기 때문이다. 현대 OS는 COW(Copy-On-Write) 기법을 사용하여 실제 메모리 복사를 최대한 지연시키기도 한다.
exec() - 덮어쓰기
exec()는 기존 프로세스의 메모리 공간을 새로운 프로그램의 코드로 덮어쓰는 시스템 콜이다.
핵심: VMS와 PCB를 새로 만들지 않는다. 기존 것을 재활용한다.
결과: 프로세스가 1개 그대로다 (부모가 새 프로그램으로 변신).
exec() 호출 전: [부모 프로세스(프로그램 A 실행 중)] 1개
exec() 호출 후: [같은 프로세스(프로그램 B로 교체됨)] 1개
exec()는fork()보다 훨씬 효율적이다. 메모리를 복사할 필요 없이, 코드/데이터 영역만 새 프로그램으로 교체하면 되기 때문이다.
fork() + exec() 조합
Unix/Linux에서 새 프로그램을 실행할 때 가장 일반적인 패턴은 **fork() 후 exec()**이다.
fork()로 자식 프로세스 생성 (부모 복사)- 자식 프로세스에서
exec()로 새 프로그램 코드를 로딩 - 부모 프로세스는
wait()로 자식이 끝나기를 대기
Windows: CreateProcess()
Windows에서는 fork() + exec()를 따로 하지 않고, CreateProcess() 한 번의 호출로 새 프로세스를 생성한다.
- CreateProcess() = fork + exec을 한 번에 수행
- WaitForSingleObject() = Unix의
wait()에 해당. 자식 프로세스 종료 대기.
fork vs exec 비교 흐름도
| 항목 | fork() | exec() | CreateProcess() (Windows) |
|---|---|---|---|
| 동작 | 부모를 통째로 복사 | 코드를 덮어쓰기 | fork + exec 한 번에 |
| 결과 프로세스 수 | 2개 (부모 + 자식) | 1개 (같은 프로세스) | 2개 (부모 + 자식) |
| VMS | 복사 생성 | 재활용 | 새로 생성 |
| PCB | 복사 생성 (새 PID) | 재활용 (같은 PID) | 새로 생성 |
| 효율성 | exec보다 무거움 | fork보다 가벼움 | 중간 |
| 종료 대기 | wait() | - | WaitForSingleObject() |
3.6 멀티스레딩 (Multithreading)
멀티스레딩이란?
하나의 프로세스 안에 여러 스레드가 존재하는 것을 **멀티스레딩(Multithreading)**이라고 한다.
공유하는 것:
- 가상 메모리 공간(VMS) 전체: 코드(Code), 데이터(Data), 힙(Heap) 영역
- 열린 파일, 권한 정보
독립적인 것:
- 각자의 스택(Stack)
- 각자의 프로그램 카운터(PC)
- 각자의 레지스터 상태
멀티프로세싱 vs 멀티스레딩
| 항목 | 멀티프로세싱 (Multi-Processing) | 멀티스레딩 (Multi-Threading) |
|---|---|---|
| 메모리 | 각 프로세스가 독립 VMS 보유 | 하나의 VMS를 스레드들이 공유 |
| 자원 효율 | 메모리 중복 → 낭비 | 메모리 공유 → 효율적 |
| 안전성 | 프로세스 간 격리 → 안전 | 공유 자원 접근 → 동기화 문제 |
| 통신 | IPC(Inter-Process Communication) 필요 → 복잡 | 공유 메모리로 직접 통신 → 간단 |
| 생성 비용 | VMS 새로 할당 → 비용 큼 | 스택만 추가 할당 → 비용 작음 |
| 장애 격리 | 하나가 죽어도 다른 프로세스 영향 없음 | 하나가 죽으면 전체 프로세스 종료 위험 |
비유: 오피스텔 vs 한 집
이 차이를 주거 형태에 비유하면 직관적으로 이해할 수 있다.
멀티프로세싱 = 오피스텔 각방
[101호 - 사람 A] [102호 - 사람 B] [103호 - 사람 C]
화장실 O 화장실 O 화장실 O
부엌 O 부엌 O 부엌 O
냉장고 O 냉장고 O 냉장고 O- 각자 독립된 공간과 독립된 시설을 가진다.
- 비용이 많이 들지만(방마다 화장실, 부엌, 냉장고), 서로 방해하지 않는다.
- 101호에 문제가 생겨도 102호, 103호는 영향 없다.
멀티스레딩 = 한 집에 여러 사람
[집 한 채]
사람 A, 사람 B, 사람 C
공유 화장실 1개
공유 부엌 1개
공유 냉장고 1개- 하나의 공간에 여러 사람이 산다.
- 비용이 적게 들지만(시설 공유), 경쟁과 충돌이 발생한다.
- 화장실을 동시에 사용하려 하면? → 병목현상(Bottleneck)
- 냉장고에서 내 음식을 다른 사람이 먹으면? → 데이터 무결성 문제
- 한 사람이 가스밸브를 잘못 열면 집 전체가 위험 → 장애 전파
결론: 멀티스레딩은 자원을 아낄 수 있지만, 공유 자원에 대한 동기화(Synchronization) 문제를 반드시 해결해야 한다. 이것이 바로 운영체제에서 동기화와 **교착 상태(Deadlock)**를 배우는 이유다.
핵심 정리
주요 용어 정리
| 용어 | 영문 | 설명 |
|---|---|---|
| 프로세스 | Process | 실행 중인 프로그램. 독립된 가상 메모리 공간(VMS)을 소유한다. |
| 스레드 | Thread | 프로세스 안의 실행 단위. CPU 스케줄링의 기본 단위이며 프로세스의 메모리를 공유한다. |
| PCB | Process Control Block | OS가 프로세스를 관리하기 위한 자료구조. PID, PC, 레지스터, 메모리 정보 등을 포함한다. |
| TCB | Thread Control Block | 스레드 관리용 자료구조. TID, PC, 레지스터, 스택 포인터를 포함한다. |
| Ready Queue | Ready Queue | CPU 할당을 기다리는 프로세스들의 대기열. |
| 디스패치 | Dispatch | Ready Queue에서 프로세스를 선택해 CPU에 할당하는 동작. |
| 문맥 교환 | Context Switching | 실행 중인 프로세스를 중단하고 다른 프로세스로 CPU 제어를 넘기는 과정. |
| 시분할 | Time-Sharing | CPU 시간을 작은 단위로 쪼개어 여러 프로세스에게 돌아가며 할당하는 방식. |
| fork() | fork() | 부모 프로세스를 통째로 복사하여 자식 프로세스를 생성하는 시스템 콜. |
| exec() | exec() | 기존 프로세스의 코드를 새 프로그램으로 덮어쓰는 시스템 콜. |
| Sleep | Sleep | 프로세스가 자발적으로 일정 시간 CPU 사용을 포기하는 것. |
| Suspend | Suspend | 외부 요인에 의해 프로세스가 강제로 중단되는 것. |
| 멀티프로세싱 | Multi-Processing | 여러 프로세스가 독립적인 VMS를 가지고 동시에 실행되는 것. |
| 멀티스레딩 | Multi-Threading | 하나의 프로세스 안에서 여러 스레드가 VMS를 공유하며 실행되는 것. |
5줄 요약
- CPU는 스레드 단위, 메모리는 프로세스 단위로 할당된다. 프로세스는 독립된 집이고 스레드는 그 안에 사는 사람이다.
- **PCB(Process Control Block)**는 OS가 프로세스를 관리하는 핵심 자료구조이며, 프로세스의 메모리는 Stack, Heap, Data, Code 네 영역으로 나뉜다.
- 프로세스는 생성 -> 준비 -> 실행 -> 대기 -> 완료 상태를 거치며, Ready Queue에서 디스패치를 통해 CPU를 할당받고, Sleep(자발적)이나 Suspend(강제적)로 Queue에서 이탈할 수 있다.
- **문맥 교환(Context Switching)**은 CPU 제어권이 프로세스 간 전환되는 과정으로, PCB에 상태를 저장/복원하며 순수 오버헤드가 발생한다. **fork()**는 복사, **exec()**는 덮어쓰기 방식으로 프로세스를 생성한다.
- 멀티스레딩은 자원 효율적이지만 공유 자원에 대한 동기화 문제가 발생하고, 멀티프로세싱은 안전하지만 자원이 낭비된다. 이 트레이드오프를 이해하는 것이 핵심이다.