본문 바로가기
TYLER SONGBlog
블로그 목록
CS학습

PCB: 프로세스의 상태를 저장하는 커널 자료구조

PCB(Process Control Block)는 운영체제가 프로세스를 관리하기 위해 커널 메모리에 유지하는 자료구조다. 프로세스의 상태, 레지스터 값, 메모리 정보, 스케줄링 정보 등을 담고 있어 문맥 교환(context switching)과 프로세스 스케줄링의 기반이 된다. 이 글에서는 PCB의 구성 요소와 동작 원리, 문맥 교환 시 PCB가 어떻게 활

송민성6분 읽기

1. 개념

PCB(Process Control Block)는 운영체제 커널이 각 프로세스마다 하나씩 생성해서 관리하는 자료구조다. 프로세스가 생성되는 순간 커널 메모리 영역에 만들어지고, 프로세스가 종료될 때까지 그 프로세스의 모든 상태 정보를 담고 있다.

리눅스 커널에서는 task_struct라는 구조체가 이 역할을 한다. 개념적으로는 PCB라고 부르지만 실제 구현체 이름은 운영체제마다 다르다.

2. 왜 사용하는가

CPU는 한 번에 하나의 프로세스만 실행할 수 있지만(코어 기준), 사용자 입장에서는 여러 프로그램이 동시에 실행되는 것처럼 보인다. 이는 CPU가 짧은 시간 단위로 여러 프로세스를 번갈아 실행하기 때문인데, 이 과정에서 다음 문제가 발생한다.

  • 프로세스 A를 잠시 멈추고 프로세스 B를 실행한 뒤, 다시 A로 돌아왔을 때 A가 멈추기 직전 상태를 정확히 복원해야 한다.
  • 어떤 프로세스가 어떤 자원을 쓰고 있는지, 우선순위는 무엇인지, 부모-자식 관계는 어떻게 되는지 추적해야 한다.

PCB가 없다면 운영체제는 프로세스 전환 시 이전 실행 상태를 복원할 방법이 없다. PCB는 이런 문제를 해결하기 위한 저장소 역할을 한다.

3. 동작 원리

PCB에는 일반적으로 다음 정보가 담긴다.

  • 프로세스 식별자(PID): 프로세스를 구분하는 고유 번호
  • 프로세스 상태: 실행(running), 준비(ready), 대기(waiting/blocked), 종료(terminated) 등
  • 프로그램 카운터(PC): 다음에 실행할 명령어의 주소
  • CPU 레지스터 값: 범용 레지스터, 스택 포인터 등 문맥 교환 시 저장/복원 대상
  • CPU 스케줄링 정보: 우선순위, 스케줄링 큐 포인터
  • 메모리 관리 정보: 페이지 테이블, 세그먼트 테이블 주소
  • 입출력 상태 정보: 열려 있는 파일 목록, 할당된 장치 목록
  • 계정 정보: CPU 사용 시간, 프로세스 번호, 실행 제한 시간

문맥 교환(context switching)이 일어나는 과정은 다음과 같다.

  1. 타이머 인터럽트나 시스템 콜 등으로 현재 실행 중인 프로세스 A를 멈춰야 하는 시점이 온다.
  2. 커널은 A의 현재 레지스터 값, 프로그램 카운터를 A의 PCB에 저장한다.
  3. 스케줄러가 다음에 실행할 프로세스 B를 선택한다.
  4. B의 PCB에 저장되어 있던 레지스터 값, 프로그램 카운터를 CPU에 복원한다.
  5. B가 멈췄던 지점부터 실행을 재개한다.

이 전환 과정 자체는 순수한 오버헤드다. 실제 유용한 작업을 하지 않으면서 시간을 소비하기 때문에, 문맥 교환 횟수가 지나치게 많으면 시스템 전체 처리량이 떨어진다.

프로세스는 생성부터 종료까지 여러 상태를 오가며, PCB의 상태 필드가 이를 추적한다.

text
생성(new) → 준비(ready) → 실행(running) → 종료(terminated)
                ↑              ↓
                └── 대기(waiting) ←┘

4. 코드 예제

실제 커널 구조체를 그대로 다룰 수는 없으니, PCB의 개념을 이해하기 위한 단순화된 시뮬레이션 코드를 Python으로 작성한다.

python
from dataclasses import dataclass, field from enum import Enum, auto class ProcessState(Enum): NEW = auto() READY = auto() RUNNING = auto() WAITING = auto() TERMINATED = auto() @dataclass class PCB: pid: int state: ProcessState = ProcessState.NEW program_counter: int = 0 registers: dict = field(default_factory=lambda: {"AX": 0, "BX": 0, "SP": 0}) priority: int = 0 memory_base: int = 0 memory_limit: int = 0 open_files: list = field(default_factory=list) class Scheduler: """단순화한 라운드로빈 스케줄러. 실제 커널 스케줄러를 단순화한 예제다.""" def __init__(self): self.ready_queue: list[PCB] = [] self.running: PCB | None = None def add_process(self, pcb: PCB): pcb.state = ProcessState.READY self.ready_queue.append(pcb) def context_switch(self): # 1. 현재 실행 중인 프로세스의 상태를 PCB에 저장 if self.running is not None: self.running.state = ProcessState.READY self.ready_queue.append(self.running) print(f"[저장] PID={self.running.pid} PC={self.running.program_counter}") # 2. 다음 프로세스를 큐에서 꺼내 실행 상태로 전환 next_pcb = self.ready_queue.pop(0) next_pcb.state = ProcessState.RUNNING self.running = next_pcb print(f"[복원] PID={next_pcb.pid} PC={next_pcb.program_counter}") def tick(self): """CPU가 한 단위 시간만큼 실행되는 것을 흉내낸다.""" if self.running: self.running.program_counter += 1 scheduler = Scheduler() scheduler.add_process(PCB(pid=1, priority=1)) scheduler.add_process(PCB(pid=2, priority=2)) scheduler.context_switch() # PID=1 실행 시작 scheduler.tick() scheduler.tick() scheduler.context_switch() # PID=1 저장, PID=2 실행 시작 scheduler.tick() scheduler.context_switch() # PID=2 저장, PID=1 재개 (PC=2부터)

실행하면 PID=1이 문맥 교환 후 다시 실행될 때 프로그램 카운터가 멈췄던 지점(2)부터 이어진다는 것을 확인할 수 있다. 이것이 PCB가 하는 핵심 역할이다.

5. 시간 복잡도 또는 성능 특성

PCB 자체는 자료구조이므로 시간 복잡도보다는 문맥 교환 비용 관점에서 이야기하는 것이 적절하다.

  • 문맥 교환 1회에 걸리는 시간은 하드웨어와 커널 구현에 따라 다르며, 보통 수 마이크로초(µs) 단위다. 정확한 수치는 CPU 아키텍처, 캐시 상태, 레지스터 개수에 따라 달라지므로 특정 숫자를 일반화하기는 어렵다.
  • 문맥 교환 비용에는 레지스터 저장/복원 자체의 비용뿐 아니라, CPU 캐시와 TLB(Translation Lookaside Buffer)가 무효화되면서 발생하는 간접 비용도 포함된다. 이 간접 비용이 실제로는 더 크게 작용하는 경우가 많다.
  • 스레드 간 문맥 교환은 같은 프로세스 주소 공간을 공유하므로 프로세스 간 전환보다 일반적으로 비용이 낮다. 페이지 테이블을 교체할 필요가 없기 때문이다.

6. 실무 사용 사례

  • `ps`, `top` 같은 프로세스 모니터링 도구: 이 도구들이 보여주는 PID, 상태, CPU 사용률, 우선순위 정보는 모두 커널이 유지하는 PCB(리눅스에서는 task_struct)에서 읽어온 값이다.
  • `/proc` 파일시스템: 리눅스에서 /proc/[pid]/status, /proc/[pid]/stat 파일을 읽으면 해당 프로세스의 PCB 정보 일부를 텍스트 형태로 확인할 수 있다.
  • 컨테이너 기술: 도커 같은 컨테이너는 프로세스 격리를 위해 리눅스 네임스페이스(namespace)와 cgroup을 활용하는데, 이때도 근본적으로는 PCB에 담긴 프로세스 자원 정보를 기반으로 격리와 제한이 이루어진다.
  • 디버깅: 데드락이나 좀비 프로세스를 분석할 때, 프로세스 상태(state) 필드를 확인해 어떤 프로세스가 대기 상태에 멈춰 있는지 파악하는 것이 첫 단계다.
shell
# 특정 프로세스의 PCB 정보 일부를 확인하는 예 cat /proc/1234/status | grep -E "State|VmRSS|Threads"

7. 주의할 점

  • PCB는 커널 메모리 영역에 존재하며 사용자 프로세스가 직접 접근할 수 없다. 사용자 공간에서는 시스템 콜을 통해 간접적으로만 정보를 조회할 수 있다.
  • 프로세스 개수가 많아지면 PCB를 관리하는 자료구조(리스트, 해시 테이블, 트리 등) 자체의 탐색 비용도 스케줄링 성능에 영향을 준다. 리눅스는 이런 이유로 스케줄링 큐 관리에 레드-블랙 트리(CFS 스케줄러 기준)를 사용한 적이 있다.
  • 좀비 프로세스(zombie process)는 자식 프로세스가 종료되었지만 부모가 wait()를 호출하지 않아 PCB가 회수되지 않은 상태를 말한다. PCB가 계속 남아 있으면 PID 자원이 낭비되므로, 좀비 프로세스가 누적되면 시스템에 문제가 될 수 있다.
  • 스레드는 프로세스와 달리 PCB 전체를 새로 만들지 않고, 대부분의 정보(주소 공간, 열린 파일 등)를 공유하면서 스택과 레지스터 등 최소한의 정보만 별도로 관리한다. 이 차이 때문에 스레드 생성과 문맥 교환이 프로세스보다 가볍다.

8. 핵심 정리

PCB는 운영체제가 프로세스를 식별하고 관리하기 위해 유지하는 커널 자료구조로, 프로세스 상태·레지스터 값·메모리 정보·스케줄링 정보를 담는다. 문맥 교환이 일어날 때마다 현재 프로세스의 실행 상태를 PCB에 저장하고 다음 프로세스의 PCB에서 상태를 복원하는 방식으로 멀티태스킹이 구현된다. ps, top, /proc 파일시스템 같은 도구들은 결국 이 PCB 정보를 사용자에게 노출하는 창구 역할을 한다.

© 2026 Tyler Song