목표: 프로세스와 스레드의 차이, PCB, 컨텍스트 스위칭, IPC 를 이해하고, 크롬의 멀티프로세스 아키텍처와 JS의 싱글 스레드 선택 이유를 OS 수준에서 설명할 수 있어야 한다.
0. "프로그램이 실행된다"는 게 뭔가 — 초심자용
0-1. 프로그램 vs 프로세스
- 프로그램(Program): 하드디스크에 저장된 파일 (
.exe,.app). 가만히 있는 레시피. - 프로세스(Process): 그 프로그램을 실행시킨 상태. OS가 메모리를 할당해주고, CPU 시간을 배정해준 활동 중인 요리사.
크롬 아이콘을 더블클릭하는 순간, OS는 디스크의 chrome.exe를 읽어 메모리에 복사하고, CPU 일부를 배정해 "움직이기 시작"하게 한다. 이게 프로세스.
0-2. 스레드는 또 뭐고
한 프로세스 안에서 여러 일을 동시에 처리하고 싶다. 예: 크롬이 페이지를 그리면서, 다운로드도 하고, 탭 간 전환도 받아야 한다. 이를 위해 프로세스 안에 실행 흐름을 여러 개 둔다 — 이게 스레드(Thread).
비유: 프로세스 = 식당, 스레드 = 식당 안의 직원. 같은 주방·같은 식재료(메모리) 를 쓰지만, 각자 다른 주문을 처리한다.
0-3. 프론트엔드 개발자와의 접점
왜 프론트가 OS 개념을 알아야 하나?
- Chrome의 탭마다 프로세스가 다르다 → 한 탭이 죽어도 다른 탭은 살아남는다 (사이트 격리, Site Isolation)
- JS는 "싱글 스레드"다 → 이 말의 진짜 의미는 이 장에서 드러남 (2-6의 토대)
- Web Worker → 브라우저에서 멀티 스레드 흉내 내기
- Node.js는 단일 프로세스 + 이벤트 루프 → 성능 특성이 전부 여기서 나옴
0-4. 이 장의 핵심 용어
| 용어 | 한 줄 의미 |
|---|---|
| 프로세스 | 실행 중인 프로그램 단위. 독립된 메모리 공간. |
| 스레드 | 프로세스 안의 실행 흐름. 메모리를 공유. |
| PCB | OS가 각 프로세스를 관리하려고 만든 "주민등록증". |
| 컨텍스트 스위칭 | CPU 하나가 여러 프로세스/스레드를 번갈아 처리하는 것. |
| IPC | 프로세스끼리 대화하는 방법 (메모리가 분리돼 있으니). |
0-5. "동시성"이라는 착각
CPU 코어가 1개여도 "동시에 여러 일이 돌아가는 것처럼" 보인다. 실제로는 아주 빠르게 번갈아 가며 처리하는 것. 인간 눈에는 동시로 보인다. 이 "번갈아 가기"의 비용이 컨텍스트 스위칭 — 공짜가 아니다. 이 장에서 그 비용을 실감하게 된다.
1. 프로세스 (Process)
실행 중인 프로그램의 인스턴스. OS가 자원을 할당하는 최소 단위.
구성
┌──────────────────────────┐
│ Text (Code) │ ← 실행할 명령어
├──────────────────────────┤
│ Data (Global) │ ← 초기화된 전역 변수
├──────────────────────────┤
│ BSS │ ← 미초기화 전역 변수
├──────────────────────────┤
│ Heap ↓↓↓ │ ← malloc, new
│ (위로 증가) │
│ │
│ (아래로 증가) │
│ Stack ↑↑↑ │ ← 함수 호출 프레임
└──────────────────────────┘
자원 소유
- PID (Process ID)
- 메모리 공간 (독립적)
- 파일 디스크립터 (열린 파일·소켓)
- 스레드 (최소 1개, 메인 스레드)
- 신호 처리기
- 환경 변수
2. 스레드 (Thread)
프로세스 안의 실행 흐름. 같은 프로세스 내 스레드는 메모리를 공유한다.
프로세스 vs 스레드
| 프로세스 | 스레드 | |
|---|---|---|
| 메모리 | 독립 | 공유 (Text/Data/Heap) |
| 생성 비용 | 크다 | 작다 |
| 통신 | IPC 필요 (비싸다) | 공유 메모리 (싸다) |
| 안정성 | 하나 죽어도 다른 것 영향 없음 | 하나 죽으면 전체 프로세스 크래시 |
| 컨텍스트 스위칭 | 비쌈 (TLB flush) | 싸다 |
스레드가 자체적으로 갖는 것
- 스택 (함수 호출 프레임)
- 레지스터 (PC, SP 등)
- 스레드 로컬 스토리지 (TLS)
3. PCB (Process Control Block)
OS 커널이 프로세스별로 관리하는 구조체.
struct task_struct { // Linux
pid_t pid;
int state; // RUNNING, READY, BLOCKED, ZOMBIE...
unsigned long priority;
struct mm_struct *mm; // 메모리 맵
struct files_struct *files; // FD 테이블
struct signal_struct *signal;
struct task_struct *parent;
struct list_head children;
struct thread_info thread; // 레지스터, 스택 포인터
// ...수백 필드
};
4. 프로세스 상태 (State Diagram)
┌─────────┐
│ New │
└────┬────┘
▼
┌─────────┐ scheduler ┌─────────┐
┌─────▶│ Ready │────────────────▶│ Running │
│ └─────────┘ preemption └────┬────┘
│ ▲ ◀──────────────── │
│ │ │ I/O wait
│ │ I/O done ▼
│ ┌─────┴────┐ ┌─────────┐
│ │ Blocked │◀───────────────│ Waiting │
│ └──────────┘ └─────────┘
│ │
│ ▼
│ ┌─────────┐
└──────────────────────────────────│ Exit │
└─────────┘
5. Context Switching
OS가 한 프로세스/스레드의 실행을 멈추고 다른 것으로 교체하는 과정.
단계
- 현재 실행 중인 프로세스의 레지스터·PC·SP 를 PCB에 저장
- 다음 실행할 프로세스의 PCB에서 레지스터 복원
- TLB flush (프로세스 간 전환 시) — 페이지 테이블이 바뀌므로
- 캐시 오염
비용
- 수 마이크로초~수십 마이크로초
- 빈번하면 전체 처리량 저하
- 스레드 간 전환은 TLB flush 불필요 → 더 쌈
트레이드오프
| 짧은 타임 슬라이스 | 긴 타임 슬라이스 |
|---|---|
| 반응성 ↑ | 처리량 ↑ |
| 컨텍스트 스위칭 오버헤드 ↑ | 공정성 ↓ |
6. 스케줄링
주요 알고리즘
| 알고리즘 | 특징 |
|---|---|
| FCFS | 먼저 온 순서. 짧은 작업이 긴 작업 뒤에 갇힘(convoy effect) |
| SJF | 짧은 작업 우선. 기아 가능 |
| Round Robin | 타임 슬라이스 순환. 반응성 좋음 |
| Priority | 우선순위 기반. 기아 방지 위해 aging |
| Multilevel Feedback Queue | 실제 OS 대부분. 우선순위 자동 조정 |
| CFS (Linux) | vruntime 기반 공정 분배. Red-Black Tree |
선점 vs 비선점
- 선점(Preemptive): OS가 강제로 빼앗음 (타이머 인터럽트)
- 비선점(Cooperative): 프로세스가 자발적으로 양보 (
yield)
7. IPC (Inter-Process Communication)
프로세스는 메모리가 격리돼 있어 전용 통신 수단이 필요.
수단
| 방법 | 특징 |
|---|---|
| Pipe | 부모-자식 단방향. 쉘의 ` |
| Named Pipe (FIFO) | 이름 있는 파이프, 임의 프로세스 간 |
| Message Queue | 커널이 관리하는 큐 |
| Shared Memory | 가장 빠름. 동기화 필요 (세마포어, 뮤텍스) |
| Socket | 네트워크 (또는 Unix Domain Socket) |
| Signal | 이벤트 알림 (SIGKILL, SIGTERM, SIGCHLD) |
| Memory-mapped File | 파일을 메모리로 매핑, 프로세스 간 공유 가능 |
FE 연관
- 크롬 브라우저의 Renderer ↔ Browser 프로세스: Mojo IPC (shared memory + socket)
- Node.js
child_process.fork(): IPC 채널로.send()/message이벤트
8. 크롬의 멀티프로세스 아키텍처
┌─────────────────────────────────────────────┐
│ Browser Process (1개) │
│ - UI, 네트워크, 저장소, 주소창 │
└─────────────────────────────────────────────┘
▲ ▲ ▲
IPC │ IPC │ IPC │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Renderer 1 │ │ Renderer 2 │ │ Renderer 3 │
│ (sandbox) │ │ (sandbox) │ │ (sandbox) │
│ - HTML 파싱 │ │ │ │ │
│ - JS 실행 │ │ │ │ │
│ - 렌더링 │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ GPU Process │ │ Utility │ │ Plugin │
└──────────────┘ └──────────────┘ └──────────────┘
왜 멀티프로세스인가
- 안정성: 탭 하나 크래시해도 브라우저 전체가 죽지 않음
- 보안: 각 Renderer는 샌드박스 → 취약점이 있어도 파일시스템 접근 불가
- 성능: 멀티코어 활용, 탭 간 GC 영향 격리
Site Isolation
- Spectre/Meltdown 대응으로 각 Origin별로 별도 Renderer
- 메모리 사용↑, 보안 격리↑
9. 왜 JavaScript는 싱글 스레드인가
역사적 이유
- 1995년 Brendan Eich가 10일 만에 설계
- DOM은 공유 자원 — 멀티 스레드면 락·경쟁 조건 폭발
대안
| 방법 | |
|---|---|
| CPU 집약 작업 | Web Worker |
| 격리된 실행 | iframe + postMessage |
| 병렬 요청 | Promise.all (I/O는 비동기) |
| 실제 병렬 계산 | SharedArrayBuffer + Atomics + Worker |
Web Worker가 DOM에 접근하지 못하는 이유
DOM은 스레드 안전하지 않다. 여러 스레드가 동시에 appendChild 하면 트리가 깨짐. JS 생태계는 처음부터 싱글 스레드 DOM을 전제로 진화했기에 이제는 바꿀 수 없다.
10. Node.js의 모델
┌──────────────────────────┐
│ Main Thread (V8 + 이벤트 루프) │
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ libuv Thread Pool │
│ (기본 4개, 최대 1024) │
│ - 파일 I/O │
│ - DNS │
│ - crypto (pbkdf2) │
│ - 압축 (zlib) │
└──────────────────────────┘
- 네트워크 I/O는 OS의 async I/O (epoll/kqueue/IOCP) 사용 → 스레드 풀 안 씀
- CPU 작업은
worker_threads모듈로 별도 스레드
11. 스레드 동기화 (이론)
공유 자원에 여러 스레드가 접근하면 경쟁 조건 발생.
원시 자료구조
| 설명 | |
|---|---|
| Mutex | 한 번에 한 스레드만 소유 |
| Semaphore | N개의 권한, P(wait) / V(signal) |
| Condition Variable | 조건 충족까지 대기, signal로 깨움 |
| Read-Write Lock | 읽기는 동시, 쓰기는 독점 |
| Spinlock | 락 획득까지 busy-wait. 짧은 임계 영역용 |
| Atomic | 락 없이 단일 연산 보장 (CAS, FAA) |
고전 문제
- Deadlock: 서로 상대가 가진 자원을 기다림 (4조건: 상호 배제, 점유 대기, 비선점, 순환 대기)
- Starvation: 낮은 우선순위가 영원히 실행 안 됨
- Priority Inversion: 높은 우선순위가 낮은 것을 기다림 (Mars Pathfinder 사례)
JS에서의 의의
JS 자체는 싱글 스레드라 이런 문제에서 대부분 자유. 단 SharedArrayBuffer 를 쓸 때 Atomics.wait/notify 로 직접 동기화 필요.
// Worker 간 공유 메모리
const sab = new SharedArrayBuffer(1024);
const view = new Int32Array(sab);
Atomics.store(view, 0, 42);
Atomics.wait(view, 0, 42); // 값이 42면 대기
Atomics.notify(view, 0, 1); // 1개 깨움
12. ⚠️ 자주 하는 실수·오해
| 오해 | 실제 |
|---|---|
| 스레드가 많을수록 빠르다 | CPU 코어보다 많으면 오히려 컨텍스트 스위칭으로 느려짐 |
| 프로세스 생성은 공짜다 | fork는 비싸다. 스레드 풀이 기본 |
| 크롬 탭 하나 = 프로세스 하나 | Site Isolation + 통합 규칙으로 달라짐 |
| Worker로 감싸면 모든 게 빨라진다 | postMessage 복사 비용, CPU 바운드만 유리 |
| Node.js는 싱글 스레드다 | JS 실행만 싱글, 내부엔 libuv 스레드 풀 |
13. 연습 문제
Q1. 프로세스와 스레드의 가장 본질적인 차이는?
정답
메모리 공간. 프로세스는 독립적 가상 메모리를 갖고, 같은 프로세스의 스레드는 Text/Data/Heap을 공유한다(스택만 분리). 그래서 스레드 간 통신은 공유 메모리로 싼 반면, 프로세스 간은 IPC로 비싸다.
Q2. 크롬이 탭마다 프로세스를 분리하는 3가지 이유는?
정답
- 안정성 — 한 탭 크래시가 전체를 죽이지 않음
- 보안 — 샌드박스로 OS 자원 접근 차단, Spectre 대응 Site Isolation
- 성능 — 멀티코어 활용, 탭별 V8 힙 분리로 GC 영향 격리
Q3. 컨텍스트 스위칭이 비싼 이유 2가지는?
정답
- 레지스터·PC 저장·복원 + TLB flush + 캐시 오염 (하드웨어 비용)
- 캐시 웜업 비용 — 새 프로세스의 데이터는 L1/L2/L3에 없어 캐시 미스가 집중적으로 발생
스레드 간 전환은 TLB flush가 없어 조금 저렴.
Q4. Node.js가 싱글 스레드라면서 파일 I/O는 어떻게 병렬로 하는가?
정답
JS 실행은 싱글 스레드지만, 내부적으로 libuv가 스레드 풀(기본 4개) 을 운영한다. 파일 I/O, DNS, crypto, zlib 같은 작업은 이 풀에서 병렬 실행되고 완료되면 콜백이 메인 스레드의 이벤트 루프에 등록된다. 네트워크 I/O는 OS의 async I/O(epoll/kqueue/IOCP)를 사용해 스레드 풀 없이 처리한다.
Q5. Web Worker가 DOM을 건드리지 못하는 이유는?
정답
DOM API는 스레드 안전하지 않기 때문. 여러 스레드가 동시에 트리를 수정하면 락 없이는 일관성을 유지할 수 없고, DOM에 락을 붙이면 성능이 심각하게 저하된다. 따라서 Worker는 메시지 전달(postMessage)로만 메인 스레드와 통신한다.
Q6. Deadlock의 4가지 필요조건은?
정답
- 상호 배제 (Mutual Exclusion): 자원을 한 번에 하나만 소유
- 점유 대기 (Hold and Wait): 자원을 갖고 다른 자원을 기다림
- 비선점 (No Preemption): 자원을 강제로 뺏을 수 없음
- 순환 대기 (Circular Wait): 프로세스들이 원형으로 자원을 기다림
하나라도 깨면 Deadlock 불가능. 보통 순환 대기를 자원 획득 순서로 깬다.
Q7. SharedArrayBuffer 는 왜 기본 비활성화 상태였다가 COOP/COEP 헤더를 요구하게 됐는가?
정답
Spectre 사이드 채널 공격 때문. SharedArrayBuffer 로 정밀 타이머를 만들 수 있어 CPU 추측 실행 취약점으로 메모리를 엿볼 수 있었다. 2018년 일시 비활성화됐다가, Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp 로 Origin을 격리된 에이전트 클러스터로 분리 해야만 다시 활성화된다. Cross-origin 리소스가 격리되므로 공격 표면이 줄어든다.
14. 체크리스트
- 프로세스와 스레드의 차이(메모리, 비용, 통신)를 설명할 수 있다
- PCB에 저장되는 정보를 안다
- 컨텍스트 스위칭의 단계와 비용을 이해한다
- IPC 수단을 상황에 따라 선택할 수 있다
- 크롬 멀티프로세스 아키텍처의 이유를 안다
- JS가 싱글 스레드인 이유를 역사·기술 양쪽으로 설명할 수 있다
- Node.js가 libuv 스레드 풀을 쓰는 지점을 안다
- Deadlock 4조건을 암기하고 있다