한 줄 정의

실행 중인 프로그램의 인스턴스. 운영체제가 자원(메모리, 파일 디스크립터, 시그널 등)을 할당하는 기본 단위.

쉽게 말하면

프로그램은 디스크에 저장된 실행 파일(바이너리)이고, 프로세스는 그 프로그램을 메모리에 올려서 실제로 CPU가 실행하고 있는 상태입니다. 같은 프로그램을 3번 실행하면 3개의 독립된 프로세스가 생기며, 각각 자기만의 메모리 공간, 파일 테이블, 시그널 핸들러를 갖기 때문에 하나가 죽어도 나머지에는 영향이 없습니다.

왜 이걸 알아야 하는가?

  • 장애 격리 설계의 기본 단위: 마이크로서비스를 왜 프로세스(컨테이너)로 분리하는가? 프로세스 격리를 이해하지 못하면 “왜 모놀리스를 쪼개는지” 설명할 수 없습니다.
  • OOM/크래시 분석: OutOfMemoryError가 발생했을 때 해당 JVM 프로세스의 Heap, Stack, Metaspace 구조를 모르면 heap dump를 해석할 수 없습니다. “Heap이 프로세스 전체 공유”라는 사실을 알아야 OOM이 왜 전체 애플리케이션을 위험에 빠뜨리는지 이해할 수 있습니다.
  • 컨테이너/Docker의 근본 원리: Docker 컨테이너는 결국 프로세스 격리(namespaces + cgroups)입니다. 프로세스가 뭔지 모르면 컨테이너가 왜 VM보다 가벼운지, 왜 격리 수준이 다른지 설명할 수 없습니다.

왜 이렇게 설계했는가?

  • 해결하는 문제: 여러 프로그램을 동시에 실행하면서 서로 간섭하지 않도록 **격리(isolation)**하는 것입니다.
  • 이게 없다면: 모든 프로그램이 물리 메모리를 직접 공유하기 때문에, 프로그램 A가 프로그램 B의 메모리를 덮어쓸 수 있고 하나가 죽으면 전체 시스템이 크래시됩니다. 실제로 초기 DOS가 이런 구조였습니다.
  • 핵심 설계 원칙: 각 프로세스에 **가상 주소 공간(Virtual Address Space)**을 부여해서 “나만의 메모리 전체를 쓰고 있다”는 착각을 만들고, 실제 물리 메모리 매핑은 OS의 MMU(Memory Management Unit)가 처리합니다.

어떻게 동작하는가?

프로세스 생성 과정 (Linux 기준)

  1. fork(): 부모 프로세스의 복제본을 생성합니다. 이때 실제로 메모리를 복사하지 않고 COW(Copy-On-Write) 방식으로 페이지 테이블만 복사하며, 쓰기가 발생하는 시점에 해당 페이지만 실제로 복사됩니다.
  2. exec(): fork된 자식 프로세스의 메모리를 새 프로그램의 코드/데이터로 교체합니다. 기존 메모리는 버리고 새 Text/Data/BSS를 로드합니다.
  3. PCB 할당: 커널이 task_struct (Linux) 구조체를 생성하며, 여기에 PID, 상태, 레지스터 컨텍스트, 메모리 맵, 열린 파일 목록 등이 담깁니다.

PCB (Process Control Block)에 담기는 것

  • PID: 프로세스 식별자
  • 프로세스 상태: new, ready, running, waiting, terminated
  • Program Counter: 다음에 실행할 명령어 주소
  • CPU 레지스터: 컨텍스트 스위칭 때 저장/복원할 레지스터 값들
  • 메모리 관리 정보: 페이지 테이블 베이스 레지스터, 세그먼트 정보
  • I/O 상태: 열린 파일 디스크립터 테이블, 소켓 정보
  • 스케줄링 정보: 우선순위, 스케줄링 큐 포인터

프로세스 종료

  • 정상 종료: exit() 시스템 콜을 호출하면 커널이 메모리, 파일 디스크립터 등 자원을 회수합니다.
  • 좀비 프로세스: 자식이 종료됐지만 부모가 wait()를 호출하지 않은 상태입니다. PCB는 남아있고 자원은 회수된 채로 ps에서 Z 상태로 표시됩니다.
  • 고아 프로세스: 부모가 먼저 종료된 경우로, init(PID 1) 또는 systemd가 새 부모가 되어 wait()을 대신 호출해줍니다.

시각화

프로세스 메모리 구조

영역내용비고
Stack함수 호출 프레임, 지역변수, 리턴 주소높은 주소 → 낮은 주소 (↓)
(여유 공간)Stack과 Heap 사이의 빈 공간고갈 시 StackOverflow / OOM
Heapmalloc/new로 동적 할당낮은 주소 → 높은 주소 (↑)
BSS초기화되지 않은 전역/static 변수0으로 초기화됨
Data초기화된 전역/static 변수
Text컴파일된 기계어 명령어읽기 전용, 공유 가능

프로세스 생명주기

stateDiagram-v2
    [*] --> New: fork() / exec()
    New --> Ready: PCB 할당 완료
    Ready --> Running: 스케줄러 dispatch
    Running --> Ready: 타이머 인터럽트
    Running --> Waiting: I/O, lock, sleep()
    Waiting --> Ready: I/O 완료, lock 획득
    Running --> Terminated: exit() / SIGKILL
    Terminated --> [*]: wait() → 자원 회수

fork + exec 과정

sequenceDiagram
    participant P as 부모 프로세스
    participant K as 커널
    participant C as 자식 프로세스

    P->>K: fork()
    K->>K: PCB 복제, 페이지 테이블 복사 (COW)
    K->>C: 자식 생성 (리턴값 = 0)
    K->>P: 자식 PID 리턴
    C->>K: exec()
    K->>K: 메모리를 새 프로그램으로 교체
    K->>C: main()부터 실행

정리

기준프로세스스레드
메모리독립 가상 주소 공간Code/Data/Heap 공유, Stack만 독립
생성 비용높음 (PCB, 페이지 테이블, TLB flush)낮음 (Stack + TCB)
격리 수준강함 (하나 죽어도 무관)약함 (segfault → 전체 죽음)
통신 방식IPC (커널 경유, 느림)공유 메모리 (빠름, 동기화 필수)
컨텍스트 스위칭무거움 (TLB flush, 캐시 cold)가벼움 (TLB 유지)

실무에서 만난 사례

  • 크롬 브라우저: 탭마다 별도 프로세스를 사용하기 때문에 하나의 탭이 크래시해도 다른 탭은 살아있습니다. 격리의 대표적인 사례이며, 탭당 메모리 사용량이 높은 이유이기도 합니다.
  • 마이크로서비스: 서비스마다 독립 프로세스(컨테이너)로 구성되어, 하나의 서비스가 OOM으로 죽어도 다른 서비스에 영향이 없습니다. 프로세스 격리가 곧 장애 격리인 셈입니다.
  • Spring Boot: JVM 프로세스 하나가 애플리케이션 전체이고, 그 안에서 요청 처리는 스레드 단위로 이루어집니다. StackOverflowError는 해당 스레드만 죽지만, OutOfMemoryError는 Heap이 프로세스 전체 공유이므로 JVM 전체가 위험해집니다.

관련 개념

출처

  • Abraham Silberschatz, “Operating System Concepts” Chapter 3
  • Robert Love, “Linux Kernel Development” Chapter 3