"프로세스는 독립된 메모리, 스레드는 공유 메모리" — 여기서 한 발 더 들어가면, 왜 공유하는지, 그래서 뭐가 좋고 뭐가 위험한지가 보입니다.

스레드가 Heap을 공유한다는 사실 하나가, 동기화 문제부터 컨텍스트 스위칭 비용 차이까지 전부 설명합니다.


프로세스 (Process)

프로세스는 실행 중인 프로그램 입니다. 프로그램이 디스크에 있는 코드라면, 프로세스는 그걸 메모리에 올려서 실행하는 인스턴스입니다.

프로세스의 메모리 구조

각 프로세스는 독립된 메모리 공간을 가집니다.

PLAINTEXT
[높은 주소]
┌─────────────┐
│   Stack     │  ← 지역 변수, 함수 호출 정보 (위에서 아래로 증가)
│     ↓       │
│             │
│     ↑       │
│   Heap      │  ← 동적 할당 (malloc, new) (아래서 위로 증가)
├─────────────┤
│   BSS       │  ← 초기화되지 않은 전역 변수
├─────────────┤
│   Data      │  ← 초기화된 전역 변수
├─────────────┤
│   Text      │  ← 실행 코드 (기계어)
└─────────────┘
[낮은 주소]

각 프로세스가 이 구조를 독립적으로 가집니다. A 프로세스가 B 프로세스의 메모리를 직접 읽을 수 없습니다.

프로세스 상태

PLAINTEXT
          생성


  ┌──── Ready ←──── I/O 완료
  │       │
  │    dispatch
  │       │
  │       ▼
  └── Running ────► Waiting (I/O 요청)


       Terminated
상태설명
ReadyCPU를 기다리는 상태
RunningCPU에서 실행 중
WaitingI/O 완료를 기다리는 상태

스레드 (Thread)

스레드는 ** 프로세스 내의 실행 단위 **입니다. 하나의 프로세스에 여러 스레드가 있을 수 있습니다.

스레드가 공유하는 것 / 안 하는 것

PLAINTEXT
프로세스
┌──────────────────────────────┐
│  Code  │  Data  │   Heap    │  ← 모든 스레드가 공유
├──────────────────────────────┤
│ Stack₁ │ Stack₂ │  Stack₃   │  ← 스레드마다 별도
│ PC₁    │ PC₂    │  PC₃      │  ← 스레드마다 별도
│ Reg₁   │ Reg₂   │  Reg₃     │  ← 스레드마다 별도
└──────────────────────────────┘
공유별도
Code 영역Stack
Data 영역Program Counter (PC)
Heap 영역Register
파일 디스크립터

Heap을 공유 하기 때문에 스레드 간 데이터 전달이 쉽습니다. 동시에, 같은 데이터를 여러 스레드가 수정하면 문제가 생깁니다. 이게 동기화 문제 의 근원입니다.


프로세스 vs 스레드

프로세스스레드
메모리독립공유 (Code, Data, Heap)
생성 비용높음낮음
컨텍스트 스위칭느림 (메모리 맵 전환)빠름 (레지스터, 스택만)
안정성하나가 죽어도 다른 프로세스에 영향 없음하나가 죽으면 전체 프로세스 사망
통신IPC 필요 (파이프, 소켓 등)공유 메모리로 바로 가능

컨텍스트 스위칭 (Context Switching)

CPU가 하나의 프로세스/스레드에서 다른 것으로 전환하는 과정입니다.

과정

  1. 현재 실행 중인 프로세스의 상태 저장 (레지스터, PC, 스택 포인터 → PCB에 저장)
  2. 다음 프로세스의 ** 상태 복원** (PCB에서 레지스터, PC 등을 로드)
  3. ** 메모리 맵 전환** (프로세스 전환 시 — TLB 플러시 발생)

왜 컨텍스트 스위칭이 비싼가

  • ** 직접 비용 **: 레지스터 저장/복원, 메모리 맵 전환
  • ** 간접 비용 **: CPU 캐시, TLB가 무효화됨 → 전환 직후 캐시 미스 폭증

스레드 간 전환이 프로세스 간 전환보다 빠른 이유는, ** 메모리 맵을 바꿀 필요가 없어서** TLB 플러시가 발생하지 않기 때문입니다.


주의할 점

Heap 공유가 만드는 동기화 지옥

스레드가 Heap을 공유한다는 건 양날의 검입니다. 데이터 전달은 쉽지만, 동시에 같은 데이터를 수정하면 Race Condition 이 발생합니다. count++ 한 줄이 사실은 read-modify-write 3단계라서, 두 스레드가 동시에 실행하면 값이 하나만 증가하는 문제가 생깁니다.

이 때문에 뮤텍스, 세마포어 같은 동기화 도구가 필요하고, 잘못 쓰면 데드락까지 이어집니다.

fork() 후 exec() 없이 작업하면

fork() 후 자식 프로세스에서 exec()을 호출하지 않으면, 부모의 메모리를 그대로 들고 갑니다. COW 덕분에 바로 복사하지는 않지만, 자식이 메모리를 수정하기 시작하면 복사 비용이 발생합니다. 특히 Java처럼 힙이 큰 프로세스에서 fork하면 메모리 사용량이 급증할 수 있습니다.

스레드 하나의 크래시 = 프로세스 전체 사망

멀티스레드 환경에서 하나의 스레드가 세그멘테이션 폴트를 내면, 같은 프로세스의 **모든 스레드가 함께 죽습니다 **. Chrome이 탭마다 별도 프로세스를 쓰는 이유가 바로 이것입니다. 안정성이 중요한 서비스라면 프로세스 격리를 고려해야 합니다.


멀티프로세스 vs 멀티스레드

언제 뭘 선택할까

** 멀티프로세스 **: 안정성이 중요할 때

  • Chrome 브라우저: 각 탭이 별도 프로세스 → 하나가 죽어도 다른 탭은 정상
  • Nginx: worker process 방식

** 멀티스레드 **: 성능과 자원 공유가 중요할 때

  • 웹 서버의 요청 처리: 각 요청을 스레드로 처리 (Tomcat)
  • 게임: 렌더링 스레드, 물리 스레드, 네트워크 스레드

PCB (Process Control Block)

OS가 프로세스를 관리하기 위해 유지하는 자료구조입니다.

  • PID (프로세스 ID)
  • 프로세스 상태 (Ready, Running, Waiting)
  • PC (Program Counter) — 다음에 실행할 명령어 주소
  • 레지스터 값들
  • 메모리 관련 정보 (페이지 테이블 포인터)
  • 스케줄링 정보 (우선순위, CPU 사용 시간)
  • I/O 상태

사용자 수준 스레드 vs 커널 수준 스레드

사용자 수준 스레드커널 수준 스레드
관리 주체라이브러리 (사용자 공간)OS 커널
전환 비용낮음 (시스템 콜 불필요)높음 (커널 개입)
블로킹하나가 블록되면 전체 블록하나만 블록

Java의 스레드는 ** 커널 수준 스레드 **와 1:1 매핑됩니다. Java 21의 Virtual Thread는 사용자 수준에 가까운 경량 스레드입니다.


fork()의 동작 원리

Unix 계열에서 새 프로세스를 만드는 시스템 콜입니다. 현재 프로세스를 ** 복제 **합니다.

C
pid_t pid = fork();

if (pid == 0) {
    // 자식 프로세스
    printf("I am child\n");
} else {
    // 부모 프로세스
    printf("I am parent, child PID: %d\n", pid);
}

fork() 후에는 부모와 자식이 ** 같은 코드 **를 실행하지만, 반환값이 달라서 분기됩니다. Copy-on-Write(COW) 기법으로 실제 메모리 복사는 필요할 때만 합니다.


정리

항목프로세스스레드
메모리독립 (Code, Data, Heap, Stack 전부 별도)Code, Data, Heap 공유. Stack, PC, 레지스터만 독립
생성 비용높음 (주소 공간 복제)낮음 (스택만 할당)
컨텍스트 스위칭느림 (TLB 플러시 발생)빠름 (TLB 플러시 불필요)
안정성하나가 죽어도 다른 프로세스 무관하나가 죽으면 전체 프로세스 사망
통신IPC 필요 (파이프, 소켓 등)공유 메모리로 바로 가능 (동기화 필수)
동기화불필요 (격리됨)필수 (Race Condition 위험)
대표 사례Chrome 탭, Nginx workerTomcat 요청 처리, 게임 렌더링
댓글 로딩 중...