Process State: 프로세스의 생명주기를 결정하는 상태 전이 모델
프로세스는 생성부터 종료까지 New, Ready, Running, Waiting, Terminated 등 몇 가지 상태를 거치며, 운영체제 스케줄러는 이 상태 전이를 관리해 CPU 자원을 여러 프로세스에 분배한다. 상태 전이 규칙을 이해하면 컨텍스트 스위칭 비용, 스케줄링 정책, 좀비 프로세스 같은 실무 문제를 정확히 진단할 수 있다. 이 글에서는 상태 모
1. 개념
프로세스 상태(Process State)는 프로세스가 실행 생명주기 동안 놓일 수 있는 조건을 유한한 개수로 구분한 것이다. 대표적으로 다음 5가지 상태로 모델링한다.
- New: 프로세스가 생성되는 중이며 아직 커널이 실행 자원을 할당하지 않은 상태
- Ready: 실행할 준비는 되어 있지만 CPU를 할당받지 못해 대기 중인 상태
- Running: CPU를 할당받아 명령어를 실제로 수행 중인 상태
- Waiting(Blocked): I/O 완료, 락 획득, 시그널 수신 등 특정 이벤트를 기다리는 상태
- Terminated: 실행이 끝났지만 부모 프로세스가 종료 상태를 회수(wait)하기 전까지 커널에 잔여 정보가 남아있는 상태
각 프로세스는 PCB(Process Control Block)에 현재 상태, 레지스터 값, 프로그램 카운터, 메모리 매핑 정보를 저장하며, 상태가 바뀔 때마다 PCB가 갱신된다.
2. 왜 사용하는가
CPU는 한 순간에 하나의 코어당 하나의 명령어 흐름만 실행할 수 있다. 수십, 수백 개의 프로세스가 동시에 존재하는 상황에서 운영체제는 "지금 이 프로세스를 실행해도 되는가"를 판단해야 하는데, 이 판단 기준이 바로 상태다.
상태 구분이 없다면 스케줄러는 모든 프로세스를 매번 검사해서 실행 가능 여부를 새로 계산해야 한다. Ready 큐, Waiting 큐처럼 상태별로 자료구조를 분리해두면 스케줄러는 Ready 큐에서만 다음 실행 대상을 고르면 되므로 탐색 비용이 줄어든다.
3. 동작 원리
상태 전이는 정해진 규칙에 따라서만 일어난다.
New → Ready : fork() 등으로 생성 완료, 스케줄러 큐에 진입
Ready → Running : 스케줄러가 CPU 할당(dispatch)
Running → Ready : 타임 슬라이스 소진 또는 더 높은 우선순위 프로세스 등장(preemption)
Running → Waiting : I/O 요청, lock 대기, sleep() 호출
Waiting → Ready : I/O 완료, 이벤트 발생 인터럽트
Running → Terminated : exit() 호출 또는 시그널에 의한 종료Running에서 Waiting으로, Waiting에서 곧바로 Running으로는 전이할 수 없다. 이벤트가 끝나면 반드시 Ready를 거쳐 스케줄러의 재할당을 기다려야 한다. 이 규칙 덕분에 스케줄러는 항상 Ready 큐만 관리하면 되고, 임의의 시점에 여러 프로세스가 동시에 Running 상태가 되는 경쟁 조건을 원천적으로 차단한다.
리눅스는 이 5단계 모델을 세분화해서 실제로는 다음과 같은 상태 코드를 사용한다.
R (Running) 실행 중이거나 Ready 큐에서 대기 중
S (Interruptible Sleep) 시그널로 깨울 수 있는 대기 상태 (일반적인 I/O 대기)
D (Uninterruptible Sleep) 시그널로 깨울 수 없는 대기 상태 (디스크 I/O 등)
T (Stopped) 시그널(SIGSTOP)에 의해 정지
Z (Zombie) 종료했지만 부모가 wait()로 회수하지 않은 상태4. 코드 예제
리눅스에서 프로세스 상태는 /proc/[pid]/status 파일에서 직접 확인할 수 있다.
# 현재 셸의 PID로 상태 확인
cat /proc/$$/status | grep State
# 출력 예: State: S (sleeping)
# 전체 프로세스를 상태 코드와 함께 조회
ps -eo pid,stat,comm | head -n 10Python으로 부모-자식 프로세스 상태 전이를 직접 관찰하는 예제다. 자식 프로세스를 sleep 상태로 만든 뒤 종료시키고, 부모가 회수(wait)하기 전까지 좀비 상태가 되는 것을 확인한다.
import os
import time
import sys
pid = os.fork()
if pid == 0:
# 자식 프로세스: 즉시 종료 -> Terminated -> 부모가 회수 전까지 Zombie
sys.exit(0)
else:
# 부모 프로세스: 일부러 잠시 대기하며 자식을 좀비 상태로 방치
time.sleep(2)
with open(f"/proc/{pid}/status") as f:
for line in f:
if line.startswith("State"):
print(f"자식 프로세스 상태: {line.strip()}")
# 출력: State: Z (zombie)
break
# 회수(wait)하면 좀비가 사라지고 커널에서 완전히 제거된다
os.waitpid(pid, 0)
print("wait() 호출 후 좀비 프로세스 회수 완료")5. 시간 복잡도 또는 성능 특성
상태 전이 자체의 계산 비용보다 전이에 수반되는 컨텍스트 스위칭(Context Switching) 비용이 실무에서 중요하다.
- 상태 큐 삽입/삭제(Ready 큐, Waiting 큐 조작): 큐 구현에 따라 다르지만 연결 리스트 기반이면 O(1)
- 컨텍스트 스위칭 1회 비용: 레지스터 저장/복원, 캐시 및 TLB(Translation Lookaside Buffer) 무효화까지 포함해 일반적으로 수 마이크로초(μs) 단위. 정확한 수치는 CPU 아키텍처와 커널 버전에 따라 달라지므로 벤치마크 없이 단정할 수 없다
- 스케줄러가 다음 실행 프로세스를 고르는 비용: 리눅스 CFS(Completely Fair Scheduler)는 레드-블랙 트리 기반으로 O(log n), 여기서 n은 Ready 상태 프로세스 수
6. 실무 사용 사례
- 좀비 프로세스 누적 감지: 서버 애플리케이션이 자식 프로세스를 생성하고
wait()를 호출하지 않으면 Z 상태 프로세스가 계속 쌓여 PID 자원이 고갈될 수 있다.ps aux | grep Z로 주기적으로 점검한다. - D 상태(Uninterruptible Sleep) 모니터링: 디스크 I/O가 비정상적으로 느려지면 프로세스가 D 상태에 장시간 머무르며, 이 상태는
kill -9로도 종료되지 않는다.iostat,vmstat같은 도구로 디스크 병목을 함께 확인해야 한다. - 로드 밸런서/오토스케일러 판단 기준: Ready 상태 프로세스(또는 스레드) 수가 CPU 코어 수 대비 지속적으로 많으면 CPU 포화로 판단해 스케일 아웃 트리거로 사용한다.
7. 주의할 점
- 좀비 프로세스는 메모리를 거의 점유하지 않지만 PID 테이블 슬롯을 차지한다. 대량 생성 시
fork bomb과 유사하게 PID 고갈로 신규 프로세스 생성이 불가능해질 수 있다. - D 상태 프로세스는 시그널로 강제 종료할 수 없다. 근본 원인인 I/O 병목이나 커널 버그를 해결해야 상태가 풀린다.
- 멀티스레드 환경에서는 프로세스 단위가 아니라 스레드 단위로 상태가 관리된다(리눅스는 스레드를 경량 프로세스, LWP로 취급).
ps -eLf로 스레드별 상태를 확인해야 정확하다.
8. 핵심 정리
프로세스 상태는 New, Ready, Running, Waiting, Terminated라는 정해진 전이 규칙을 통해 스케줄러가 CPU 자원을 안전하고 효율적으로 분배하도록 돕는 모델이다. 리눅스는 이를 R, S, D, T, Z 코드로 세분화해서 노출하며, 실무에서는 특히 Z(좀비)와 D(디스크 대기) 상태를 모니터링해 자원 누수와 I/O 병목을 조기에 발견하는 것이 중요하다.