컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
os · 01

프로세스

OS프로세스PCB

프로세스(Process)는 실행 중인 프로그램입니다. 디스크에 저장된 정적인 프로그램이 메모리에 올라와 OS의 관리를 받는 순간 프로세스가 됩니다. OS는 각 프로세스를 PCB(Process Control Block)로 관리하며, 프로세스는 생명 주기 동안 여러 상태를 전이합니다.

핵심 개념

PCB (Process Control Block)

OS가 프로세스를 관리하기 위해 유지하는 자료구조입니다. PID, 상태, PC(Program Counter), 레지스터 값, 메모리 정보, 스케줄링 우선순위 등을 저장합니다.

핵심 개념

프로세스 상태

New(생성) → Ready(준비) → Running(실행) → Waiting(대기) → Terminated(종료)의 5가지 상태로 설명하는 것이 교재의 일반적인 모델입니다(운영체제마다 세부 상태는 다릅니다). OS 스케줄러가 Ready ↔ Running 전이를 제어합니다.

핵심 개념

컨텍스트 스위칭

CPU가 현재 프로세스의 상태를 PCB에 저장하고, 다음 프로세스의 상태를 PCB에서 복원하는 작업입니다. 순수한 오버헤드로 작용하므로 빈도를 최소화하는 것이 중요합니다.

핵심 개념

프로세스 메모리 구조

Code(텍스트) / Data(초기화된 전역·정적 변수) / BSS(미초기화 전역·정적 변수, 0으로 초기화) / Heap(동적 할당, 보통 위로 성장) / Stack(함수 호출, 보통 아래로 성장) 5개 영역으로 나눠 설명하는 것이 일반적입니다(실제 배치는 운영체제와 아키텍처에 따라 다릅니다).

핵심 개념

fork() / exec()

fork()는 부모 프로세스를 복제해 자식 프로세스를 생성합니다. exec()는 현재 프로세스 이미지를 새 프로그램으로 교체합니다. 두 조합으로 새 프로세스를 실행합니다.

핵심 개념

좀비 / 고아 프로세스

자식이 종료됐지만 부모가 wait()을 호출하지 않은 경우 좀비 프로세스가 됩니다. 부모가 먼저 종료되면 자식은 고아(Orphan)가 되어, 가장 가까운 subreaper 프로세스에 입양됩니다(지정된 subreaper가 없으면 그 PID 네임스페이스의 init, 보통 PID 1).

Ready vs Waiting — Ready는 CPU만 할당받으면 바로 실행 가능한 상태. Waiting(Blocked)은 I/O 완료·이벤트 발생 등 외부 조건을 기다리는 상태로, CPU를 줘도 실행할 수 없습니다.
디스패처(Dispatcher) — 스케줄러가 선택한 프로세스를 실제로 CPU에 올리는 모듈. 컨텍스트 스위칭 + 유저 모드 전환 + PC 점프를 수행합니다. 디스패처 지연(latency)은 컨텍스트 스위치 비용의 핵심입니다.
멀티프로그래밍 vs 멀티태스킹 — 멀티프로그래밍은 CPU 유휴 시간을 없애기 위해 여러 프로세스를 메모리에 적재. 멀티태스킹은 시분할로 짧은 퀀텀마다 전환해 동시 실행처럼 보이게 합니다.
프로세스 vs 스레드 — 프로세스는 독립된 메모리 공간(Code/Data/Heap/Stack 모두 별도). 스레드는 같은 프로세스 내에서 Code·Data·Heap을 공유하고 Stack과 레지스터를 독립적으로 가집니다.
IPC (Inter-Process Communication) — 독립된 주소 공간을 가진 프로세스 간 통신 방법. Pipe, Message Queue, Shared Memory, Socket, Signal 등이 있으며 각각 속도·복잡도 트레이드오프가 있습니다.

프로그램과 프로세스는 무엇이 다른가

프로그램은 디스크에 저장된 명령어의 모음이고, 프로세스는 그 프로그램이 실행되고 있는 상태입니다. 같은 프로그램을 두 번 실행하면 프로세스는 두 개가 됩니다. 운영체제는 프로세스마다 주소 공간, 열린 파일, CPU 레지스터 같은 실행 상태를 따로 관리하고, 리눅스 커널 소스(include/linux/sched.h)에서는 task_struct 구조체가 PID 같은 프로세스 정보를 담습니다.

숫자로 확인하기

fork를 세 번 이어서 호출하면 — 조건 없이 fork()를 연달아 3번 부르면 호출할 때마다 프로세스 수가 2배가 되어 1 → 2 → 4 → 8개, 곧 원래 프로세스를 포함해 8개(새로 생긴 것은 7개)가 됩니다.
fork의 반환값 — fork()는 부모에게 자식의 PID를, 자식에게 0을 돌려줍니다. 이 값 하나로 같은 코드가 부모와 자식으로 갈라집니다.
PID의 범위 — 리눅스는 PID가 pid_max에서 정한 상한까지 올라가며, proc_sys_kernel(5) 매뉴얼은 기본값이 32768이고 64비트 시스템에서는 2²², 곧 약 400만까지 늘릴 수 있다고 설명합니다.
주소 공간의 크기 — x86-64 리눅스(4단계 페이지 테이블)는 사용자 영역에 대략 128TB의 가상 주소를 줍니다. 47비트로 표현되는 주소 공간 전체는 2⁴⁷바이트 = 128TiB이고 그 끝이 0x00007fffffffffff입니다(맨 위 4kB는 guard hole로 비워 둡니다). 프로세스마다 이 범위를 따로 갖는 것은 가상 주소이기 때문입니다.

자주 하는 오해

프로세스와 프로그램은 같은 말이다 — 프로그램은 파일이고 프로세스는 실행 중인 인스턴스입니다. 한 프로그램에서 프로세스가 여러 개 생길 수 있습니다.
fork는 부모의 메모리를 즉시 전부 복사한다 — 개념상 복제이지만 리눅스는 쓰기 시점 복사(copy-on-write)로 구현해, 부모와 자식이 쓰기 전까지 같은 물리 페이지를 공유합니다. 리눅스 fork(2) 매뉴얼이 이를 설명합니다.
좀비 프로세스는 kill로 없앤다 — 좀비는 이미 종료된 프로세스이고 종료 상태를 부모에게 알리려고 프로세스 테이블 항목만 남은 것입니다. 부모가 wait()로 수거하거나 부모가 종료되어 새 부모가 수거해야 사라집니다.
컨텍스트 스위칭은 프로세스끼리만 일어난다 — 스레드 사이에서도 일어납니다. 다만 주소 공간을 바꿔야 하는 프로세스 사이 전환이 보통 더 비쌉니다.

면접에서 이렇게 물어요

면접 질문

프로세스와 프로그램의 차이는 무엇인가요?

프로그램은 디스크에 있는 정적인 코드이고, 프로세스는 메모리에 올라가 실행 중인 상태입니다. 프로세스는 주소 공간, PCB, 열린 파일 같은 실행 환경을 함께 가집니다.

면접 질문

PCB에는 무엇이 들어 있나요?

PID, 프로세스 상태, 프로그램 카운터와 레지스터 값, 메모리 관리 정보, 스케줄링 정보, 열린 파일과 입출력 상태 같은 정보가 들어 있습니다. 컨텍스트 스위칭 때 이 정보로 실행 상태를 저장하고 복원합니다.

면접 질문

fork와 exec은 어떻게 쓰이나요?

fork는 호출한 프로세스를 복제해 자식을 만들고, exec은 그 프로세스의 메모리를 새 프로그램으로 바꿉니다. 셸이 명령을 실행할 때 fork로 자식을 만들고 자식에서 exec을 호출하는 방식이 대표적입니다.

면접 질문

좀비 프로세스와 고아 프로세스는 어떻게 다른가요?

좀비는 종료했지만 부모가 아직 종료 상태를 수거하지 않은 자식입니다. 고아는 부모가 먼저 종료되어 부모를 잃은 살아 있는 자식이고, init이나 subreaper가 입양해 나중에 수거합니다.

실제 시스템에서는

상태 확인 — ps -o pid,ppid,stat,cmd로 프로세스의 부모와 상태를 볼 수 있고, ps 매뉴얼은 STAT의 Z가 좀비(defunct)라고 설명합니다. /proc/PID/status에서도 상태를 읽을 수 있습니다.
종료 상태 — wait(2) 매뉴얼은 WEXITSTATUS가 종료 상태의 하위 8비트를 돌려준다고 설명하므로, exit(256)은 부모에게 0으로 보입니다.
컨테이너의 PID 1 — 컨테이너에서 애플리케이션이 PID 1이 되면 그 PID 네임스페이스의 고아 프로세스를 입양하게 됩니다(pid_namespaces(7) 매뉴얼). Docker 문서는 --init 옵션이 시그널을 전달하고 프로세스를 수거하는 init을 넣어 준다고 설명합니다.
Java에서 자식 프로세스 — ProcessBuilder로 외부 프로그램을 자식 프로세스로 실행하고 Process.waitFor()로 종료를 기다립니다.

함께 보면 좋은 토픽

스레드 — 프로세스 안에서 주소 공간을 공유하는 실행 단위를 봅니다.
CPU 스케줄링 — Ready 상태의 프로세스를 고르는 방식을 봅니다.
프로세스 동기화 — 프로세스와 스레드가 공유 자원에 접근할 때의 문제를 봅니다.
시스템 콜 — fork, exec, wait가 커널로 들어가는 방식을 봅니다.
메모리 관리 — 프로세스의 주소 공간이 메모리에 놓이는 방식을 봅니다.
cgroups / namespace — 프로세스가 보는 자원 범위를 나누고 사용량을 제한하는 리눅스 커널 기능을 봅니다.
부팅 과정(BIOS→커널) — 전원을 켠 뒤 운영체제가 올라오기까지 단계별 프로그램이 이어지는 과정을 봅니다.

참고 자료

Linux fork(2) — fork(2) — Linux manual page
Linux wait(2) — wait(2) — Linux manual page
Linux proc_sys_kernel(5) — proc_sys_kernel(5) — Linux manual page
교재 — Silberschatz, Galvin, Gagne, "Operating System Concepts", 10th Edition, Chapter 3 "Processes" / Tanenbaum, Bos, "Modern Operating Systems", "Processes and Threads" 장

C — fork() / exec() / wait() 프로세스 생성

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();  // 부모 프로세스 복제 → 자식 생성

    if (pid < 0) {
        perror("fork failed");
        return 1;

    } else if (pid == 0) {
        // ── 자식 프로세스 (fork() 반환값 = 0) ──────────────
        // 상태 전이: New → Ready → Running (스케줄러 선택 시)
        printf("Child  PID=%d, Parent PID=%d\n", getpid(), getppid());

        // exec(): 현재 프로세스 이미지를 ls 로 교체
        // 성공하면 이 아래 코드는 절대 실행되지 않음
        char *args[] = { "ls", "-l", NULL };
        execvp(args[0], args);

        // exec() 실패 시에만 도달
        perror("exec failed");
        return 1;

    } else {
        // ── 부모 프로세스 (fork() 반환값 = 자식 PID) ────────
        printf("Parent PID=%d, Child PID=%d\n", getpid(), pid);

        // wait(): 자식 종료 대기 → Waiting 상태 진입
        // 호출하지 않으면 자식이 좀비(Zombie) 프로세스가 됨
        int status;
        waitpid(pid, &status, 0);

        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n", WEXITSTATUS(status));
        }
        // 부모: Waiting → Ready → Running 재진입
    }
    return 0;  // Terminated
}

fork()는 부모의 주소 공간을 복사해 자식을 생성합니다(Copy-on-Write 최적화 적용).
반환값이 0이면 자식, 양수이면 부모입니다.
exec()는 현재 프로세스 이미지를 완전히 교체하므로, fork() 직후 exec()를 호출하면 새 프로그램이 기존 PID를 유지한 채 실행됩니다.
부모가 wait() 없이 종료하면 자식은 고아(Orphan)가 되어 가장 가까운 subreaper(지정된 subreaper가 없으면 init, PID 1)에 입양되고, 자식이 종료됐는데 부모가 wait()을 호출하지 않으면 자식 PCB가 남아 좀비(Zombie)가 됩니다.

Java — ProcessBuilder로 자식 프로세스 생성

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class ProcessDemo {

    public static void main(String[] args) throws Exception {

        // ProcessBuilder = fork() + exec() 조합에 해당
        ProcessBuilder pb = new ProcessBuilder("ls", "-l");
        pb.redirectErrorStream(true);  // stderr → stdout 병합

        // start() 호출 시 자식 프로세스 생성 → New → Ready
        Process child = pb.start();

        int pid = (int) child.pid();
        System.out.println("Child PID: " + pid);

        // 자식 stdout 읽기 (부모는 Waiting 상태로 I/O 대기)
        try (var reader = new BufferedReader(
                new InputStreamReader(child.getInputStream()))) {
            reader.lines().forEach(System.out::println);
        }

        // waitFor(): reaper 스레드가 수집해 둔 종료 코드를 읽음
        // (자식 reaping 자체는 JVM의 process reaper 스레드가 담당)
        int exitCode = child.waitFor();
        System.out.println("Exit code: " + exitCode);

        // ProcessHandle로 살아있는 프로세스 정보 조회 (Java 9+)
        ProcessHandle.allProcesses()
            .filter(h -> h.info().command().isPresent())
            .limit(5)
            .forEach(h -> System.out.println(
                "PID=" + h.pid() + " CMD=" + h.info().command().orElse("?")
            ));
    }
}

Java의 ProcessBuilder는 내부적으로 OS의 fork()+exec()를 호출합니다.
JVM은 ProcessBuilder.start()로 생성한 자식마다 "process reaper" 데몬 스레드를 띄워, 그 스레드가 OS 레벨에서 자식 종료를 직접 기다립니다. 따라서 사용자 코드가 child.waitFor()를 호출하지 않아도 JVM이 자식을 reap하므로 일반적으로 좀비(Zombie) 프로세스는 남지 않습니다.
child.waitFor()는 reaper가 수집해 둔 종료 코드를 읽어 오는 동작이며, 호출을 생략했을 때의 실제 위험은 좀비가 아니라 자식 출력 스트림을 읽지 않아 파이프 버퍼가 가득 차서 발생하는 데드락과 파일 디스크립터 누수입니다.
Java 9부터 ProcessHandle API로 실행 중인 모든 프로세스를 조회하고 PID·CPU 시간·시작 시각 등 상세 정보를 얻을 수 있습니다.
JVM 자체도 하나의 OS 프로세스이므로, JVM 내부의 스레드 스케줄링은 OS 프로세스 스케줄링과 별개 계층에서 동작합니다.

Q1

프로세스의 상태 전이 중 'Ready'는 무엇을 의미하나요?

실행할 준비는 됐지만 아직 CPU를 못 받은 상태입니다.
Ready 상태는 프로세스가 실행될 준비를 마치고 CPU 스케줄러가 자신을 선택해주기를 기다리는 상태입니다.

Q2

PCB(Process Control Block)에 담기지 않는 정보는?

PCB는 프로세스 자신에 대한 정보만 담습니다.
PCB는 프로세스 ID, 상태, 프로그램 카운터, 레지스터 값 등 해당 프로세스를 관리하는 데 필요한 정보를 담습니다. 다른 사용자의 정보와는 무관합니다.

Q3

프로세스가 입출력을 요청하면 어떤 상태로 전이되나요?

입출력이 끝날 때까지 CPU를 쓸 수 없습니다.
입출력 요청처럼 즉시 끝나지 않는 작업을 기다려야 할 때 프로세스는 Waiting(Blocked) 상태로 전이되어 완료될 때까지 대기합니다.