프로세스
프로세스(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).
프로그램과 프로세스는 무엇이 다른가
프로그램은 디스크에 저장된 명령어의 모음이고, 프로세스는 그 프로그램이 실행되고 있는 상태입니다. 같은 프로그램을 두 번 실행하면 프로세스는 두 개가 됩니다. 운영체제는 프로세스마다 주소 공간, 열린 파일, CPU 레지스터 같은 실행 상태를 따로 관리하고, 리눅스 커널 소스(include/linux/sched.h)에서는 task_struct 구조체가 PID 같은 프로세스 정보를 담습니다.
숫자로 확인하기
fork()를 연달아 3번 부르면 호출할 때마다 프로세스 수가 2배가 되어 1 → 2 → 4 → 8개, 곧 원래 프로세스를 포함해 8개(새로 생긴 것은 7개)가 됩니다.
fork()는 부모에게 자식의 PID를, 자식에게 0을 돌려줍니다. 이 값 하나로 같은 코드가 부모와 자식으로 갈라집니다.
pid_max에서 정한 상한까지 올라가며, proc_sys_kernel(5) 매뉴얼은 기본값이 32768이고 64비트 시스템에서는 2²², 곧 약 400만까지 늘릴 수 있다고 설명합니다.
0x00007fffffffffff입니다(맨 위 4kB는 guard hole로 비워 둡니다). 프로세스마다 이 범위를 따로 갖는 것은 가상 주소이기 때문입니다.
자주 하는 오해
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에서도 상태를 읽을 수 있습니다.
WEXITSTATUS가 종료 상태의 하위 8비트를 돌려준다고 설명하므로, exit(256)은 부모에게 0으로 보입니다.
--init 옵션이 시그널을 전달하고 프로세스를 수거하는 init을 넣어 준다고 설명합니다.
ProcessBuilder로 외부 프로그램을 자식 프로세스로 실행하고 Process.waitFor()로 종료를 기다립니다.
함께 보면 좋은 토픽
참고 자료
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'는 무엇을 의미하나요?
Q2
PCB(Process Control Block)에 담기지 않는 정보는?
Q3
프로세스가 입출력을 요청하면 어떤 상태로 전이되나요?