Ray Book
운영체제 기초

프로세스와 스레드

실행의 단위, 프로세스 메모리 구조, 스레드 공유 모델, 컨텍스트 스위칭을 시각화합니다

csosprocessthreadcontext-switch

프로그램에서 프로세스로

코드를 작성하고 빌드하면 디스크에 실행 파일이 생깁니다. 이것은 아직 프로그램 입니다. 이 파일을 실행하면 OS가 메모리에 로드하고 CPU가 명령어를 실행하기 시작합니다. 이 실행 중인 인스턴스가 프로세스입니다.

프로그램은 정적인 코드의 집합이고, 프로세스는 그 코드가 실행되는 동적인 실체입니다. 같은 프로그램을 두 번 실행하면 두 개의 독립된 프로세스가 생깁니다.

프로그램 (디스크)          프로세스 (메모리)
실행 파일       -- 실행 -->  독립된 주소 공간 + CPU 시간
정적 코드                   동적 실행 상태

프로세스 메모리 구조

OS가 프로세스에 할당하는 메모리는 네 개의 영역으로 나뉩니다.

영역저장 내용특징
Code (Text)기계어 명령어읽기 전용, 변경 불가
Data전역 변수, static 변수프로세스 생존 동안 유지
Heap동적 할당 메모리낮은 주소에서 위로 성장
Stack지역 변수, 리턴 주소높은 주소에서 아래로 성장

Heap과 Stack은 서로 반대 방향 으로 자랍니다. Heap이 위로, Stack이 아래로 성장하여 가용 메모리를 양쪽에서 효율적으로 사용합니다. 둘이 만나면 메모리 부족 (Stack Overflow 또는 Heap Overflow)이 발생합니다.

// JavaScript로 비유하면
let globalCounter = 0;           // Data 영역 (전역)

function calculate(n) {          // Code 영역 (함수 코드)
  const local = n * 2;           // Stack 영역 (지역 변수)
  const obj = { value: local };  // Heap 영역 (객체)
  return obj;
}

아래 시각화에서 프로세스 메모리 구조를 단계별로 확인하세요.

프로세스 메모리 구조
프로세스 메모리
Code (Text)
실행할 기계어 명령어
Data
전역 변수, static 변수
Heap
동적 할당 메모리
Stack
지역 변수, 리턴 주소
프로세스(Process)는 실행 중인 프로그램입니다. OS는 프로그램을 메모리에 로드할 때 독립된 주소 공간을 할당합니다. 이 공간은 Code, Data, Heap, Stack 네 영역으로 나뉩니다.

스레드, 실행의 최소 단위

프로세스 안에는 최소 하나의 실행 흐름이 존재합니다. 이것이 스레드입니다.

스레드의 핵심은 공유와 독립의 균형 입니다.

  • 공유 : Code, Data, Heap (같은 프로세스의 메모리)
  • 독립 : Stack, Program Counter (PC), 레지스터

Heap을 공유하기 때문에 스레드 간 데이터 교환이 빠릅니다. 별도의 IPC (Inter-Process Communication) 없이 같은 메모리 주소를 읽고 쓸 수 있습니다. 하지만 이것은 동시에 위험이기도 합니다, 여러 스레드가 같은 데이터를 동시에 수정하면 경쟁 조건 (Race Condition)이 발생합니다.

각 스레드가 독립된 Stack을 가지는 이유는 함수 호출 흐름이 스레드마다 다르기 때문 입니다. 스레드 A가 foo() 안에서 bar()를 호출하는 동안, 스레드 B는 baz()를 실행할 수 있습니다.

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

두 가지 병렬 처리 방식을 비교합니다.

항목멀티프로세스멀티스레드
메모리 공유없음 (완전 격리)Code, Data, Heap 공유
통신 비용높음 (IPC: 파이프, 소켓)낮음 (공유 메모리)
안정성하나가 죽어도 다른 프로세스 무관하나가 죽으면 전체 프로세스 영향
컨텍스트 스위칭 비용높음 (주소 공간 교체)낮음 (같은 주소 공간)
동기화 복잡도낮음 (공유 자원 없음)높음 (Lock, Mutex 필요)

멀티프로세스 는 격리가 필요할 때 유리합니다. 한 프로세스가 크래시해도 다른 프로세스에 영향을 주지 않습니다.

멀티스레드 는 성능이 중요할 때 유리합니다. 공유 메모리 덕분에 통신이 빠르고, 스레드 생성/전환 비용이 프로세스보다 적습니다.

컨텍스트 스위칭

CPU 코어 하나는 한 번에 하나의 스레드만 실행합니다. 여러 스레드가 동시에 실행되는 것처럼 보이는 것은 OS 스케줄러가 매우 빠르게 실행 스레드를 교체하기 때문입니다. 이 교체 과정이 컨텍스트 스위칭 입니다.

컨텍스트 스위칭의 세 단계:

1. SAVE    현재 스레드의 PC, 레지스터, SP를 TCB에 저장
2. SWITCH  스케줄러가 다음 실행할 스레드를 선택
3. RESTORE 선택된 스레드의 저장된 상태를 CPU에 복원

TCB(Thread Control Block)는 각 스레드의 상태를 저장하는 자료구조입니다. 프로세스 단위로는 PCB (Process Control Block)가 같은 역할을 합니다.

컨텍스트 스위칭 비용이 발생하는 이유:

  • 직접 비용 : 레지스터 저장/복원, 스케줄러 실행 시간
  • 간접 비용 : CPU 캐시(L1, L2) 무효화, TLB (Translation Lookaside Buffer) 플러시

특히 프로세스 간 전환은 페이지 테이블까지 교체해야 하므로 TLB 전체가 무효화됩니다. 스레드 간 전환 은 같은 주소 공간이므로 TLB를 유지할 수 있어 상대적으로 가볍습니다.

실무에서의 연결

이론적인 개념이 실제 개발에서 어떻게 나타나는지 살펴봅니다.

Chrome 브라우저 = 멀티프로세스 아키텍처 : 각 탭은 독립된 렌더러 프로세스에서 실행됩니다. 한 탭이 크래시해도 다른 탭에 영향을 주지 않습니다. Chrome의 작업 관리자 (Shift + Esc)에서 각 탭의 프로세스와 메모리 사용량을 확인할 수 있습니다.

Web Worker = OS 스레드 : new Worker()로 생성하는 Web Worker는 실제 OS 스레드 위에서 실행됩니다. 메인 스레드와 Heap을 공유하지 않고 postMessage로 데이터를 복사해 전달합니다. 이것은 스레드의 공유 메모리 문제를 원천 차단하기 위한 설계입니다. SharedArrayBuffer를 사용하면 제한적으로 공유 메모리를 사용할 수 있지만, Atomics API로 직접 동기화해야 합니다.

Node.js worker_threads : Node.js의 worker_threads 모듈도 OS 스레드를 사용합니다. SharedArrayBuffer로 메모리를 공유하거나, MessagePort로 메시지를 전달합니다. CPU 집약적 작업을 메인 이벤트 루프에서 분리할 때 유용합니다.

Java/C#의 Thread vs Go의 Goroutine : Java의 전통적인 Platform Thread는 OS 스레드에 1:1 매핑됩니다. Java 21 (2023) 부터는 Go의 goroutine과 유사한 M:N 모델의 Virtual Thread도 지원합니다. Go의 goroutine은 여러 goroutine을 소수의 OS 스레드에 매핑하는 M:N 모델을 사용합니다. goroutine의 컨텍스트 스위칭은 OS가 아닌 Go 런타임이 관리하므로 비용이 훨씬 적습니다.

다음 단계

이 글에서는 프로세스와 스레드의 기본 구조를 살펴봤습니다. 다음 글에서는 CPU 스케줄링 알고리즘 , OS가 어떤 기준으로 다음 실행할 프로세스/스레드를 선택하는지를 다루겠습니다.