운영체제는 수십에서 수백 개의 Process를 동시에 관리합니다. 각 프로세스가 어디까지 실행되었는지, 어떤 자원을 사용하고 있는지, 지금 어떤 상태에 있는지를 커널이 항상 파악하고 있어야 프로세스를 중단했다가 다시 이어서 실행할 수 있습니다. 이 모든 정보를 한곳에 모아 관리하는 자료구조가 바로 PCB(Process Control Block)입니다.
PCB는 왜 필요한가
오늘날 컴퓨터에서는 수많은 프로세스가 CPU를 번갈아 사용한다. 스케줄러가 실행 중인 프로세스를 중단하고 다른 프로세스를 실행하는 컨텍스트 스위치가 일어날 때, 커널은 중단되는 프로세스의 현재 상태를 어딘가에 저장해 두어야 한다. 나중에 다시 그 프로세스가 실행될 차례가 되면 저장해 둔 상태를 복원함으로써 마치 중단된 적이 없었던 것처럼 실행을 재개할 수 있기 때문이다.
이 역할을 하는 것이 PCBProcess Control Block다. PCB는 프로세스마다 하나씩 존재하는 자료구조로, 해당 프로세스를 통제하기 위한 모든 정보를 담고 있다. Process Descriptor(프로세스 서술자) 혹은 Task Control Block(TCB)이라 부르기도 한다.
PCBLayout.png|PCB의 대략적인 구조
PCB가 없다면 운영체제는 프로세스를 전환할 수 없고, 따라서 멀티프로그래밍multiprogramming1 자체가 불가능해진다. PCB는 운영체제가 프로세스를 하나의 관리 가능한 단위로 다루기 위한 핵심 추상화인 셈이다.
PCB의 구성 요소
PCB에 담기는 정보는 운영체제마다 세부 사항이 다르지만, 공통적으로 포함되는 핵심 요소들이 있다.
프로세스 상태

프로세스 식별자
각 프로세스를 고유하게 구분하기 위한 번호로, PIDProcess Identification Number라 부른다. 운영체제는 PID를 통해 특정 프로세스를 지목하여 시그널을 보내거나, 자원 사용량을 추적하거나, 부모-자식 관계를 관리한다.
프로그램 카운터

CPU가 다음에 실행할 명령어의 주소를 가리키는 레지스터 값이다. 컨텍스트 스위치가 일어날 때 현재 프로그램 카운터 값을 PCB에 저장하고, 프로세스가 다시 실행될 때 저장된 값을 복원한다. 이 덕분에 프로세스는 중단된 지점의 바로 다음 명령어부터 이어서 실행할 수 있다.
CPU 레지스터
프로그램 카운터 외에도 프로세스가 사용하던 범용 레지스터, 스택 포인터, 상태 레지스터 등의 값을 모두 저장해야 한다. 이들을 통칭하여 하드웨어 문맥hardware context이라 하며, 컨텍스트 스위치의 핵심인 저장/복원save/restore 작업의 대상이 된다.2
CPU 스케줄링 정보
프로세스의 우선순위priority, 스케줄링 큐에서의 위치, 스케줄링 정책(FIFO, Round Robin 등)에 관한 정보다. 스케줄러는 이 정보를 바탕으로 어떤 프로세스에게 CPU를 할당할지 결정한다.
메모리 관리 정보
프로세스가 사용하는 가상 주소 공간에 관한 정보다. 페이지 테이블의 베이스 주소, 세그먼트 테이블, 메모리 한계 레지스터 값 등이 이에 해당한다. 커널은 이 정보를 통해 프로세스가 접근할 수 있는 메모리 범위를 관리하고, 다른 프로세스의 메모리 영역을 침범하지 못하도록 보호한다.
입출력 상태 정보
프로세스에 할당된 입출력 장치 목록, 열려 있는 파일의 목록 등이 포함된다. 프로세스가 종료될 때 커널은 이 정보를 참조하여 해당 프로세스가 점유하고 있던 입출력 자원을 해제한다.
자원 사용 정보
프로세스가 CPU를 사용한 시간, 경과 시간, 메모리 사용량 등의 통계 정보다. Accounting Information이라고도 부르며, 과금accounting, 성능 분석, 자원 한도 관리에 활용된다.
태스크 리스트
운영체제는 이러한 PCB들을 하나의 리스트로 조직하여 시스템에 존재하는 모든 프로세스를 관리한다. 이 리스트를 태스크 리스트task list 또는 프로세스 리스트process list라 부른다.

프로세스가 생성되면 해당 프로세스의 PCB가 태스크 리스트에 추가되고, 프로세스가 종료되면 태스크 리스트에서 제거된다. 스케줄러는 태스크 리스트를 순회하며 실행할 프로세스를 선택하고, 커널의 다른 서브시스템도 이 리스트를 통해 특정 프로세스를 찾거나 전체 프로세스에 대한 작업(시그널 전달, 자원 회수 등)을 수행한다.

한편, 스케줄링의 효율을 위해 태스크 리스트 외에도 상태별 큐가 별도로 운영된다. 실행 준비가 완료된 프로세스는 실행 대기열ready queue에, 특정 이벤트를 기다리는 프로세스는 대기열wait queue에 각각 들어간다. 이렇게 상태별로 프로세스를 분류하면, 스케줄러가 모든 프로세스를 매번 순회하지 않고도 실행 가능한 프로세스를 빠르게 찾을 수 있다.
컨텍스트 스위치와 PCB
PCB의 존재 의의는 컨텍스트 스위치에서 가장 극명하게 드러난다. 스케줄러가 현재 실행 중인 프로세스 를 중단하고 프로세스 을 실행하기로 결정하면, 다음과 같은 일이 일어난다.
- 의 현재 하드웨어 문맥(레지스터, 프로그램 카운터 등)을 의 PCB에 저장한다.
- 의 PCB에서 이전에 저장해 둔 하드웨어 문맥을 읽어 CPU 레지스터에 복원한다.
- 이 중단된 지점부터 실행을 재개한다.
이 과정에서 PCB는 프로세스의 실행 상태를 보존하는 스냅샷 역할을 한다. PCB에 저장해야 할 정보가 많을수록 컨텍스트 스위치에 걸리는 시간이 길어지므로, PCB의 설계는 필요한 정보를 빠짐없이 담으면서도 크기를 최소화하는 균형을 맞추어야 한다. 컨텍스트 스위치의 세부 절차와 성능 비용에 대해서는 컨텍스트 스위치 포스트에서 자세히 다룬다.
Linux에서의 구현: task_struct
구조체 개요
리눅스에서 PCB에 해당하는 것은 include/linux/sched.h에 정의된 task_struct 구조체다(Linux v6.19 기준, 819번째 줄에서 시작). 이 구조체는 약 700줄이 넘는 방대한 크기(약 6~8KB)를 자랑하며, 프로세스에 대한 모든 것을 담고 있다.
struct task_struct {
/* ── 식별 ── */
pid_t pid; // 스레드 ID (커널 내부에서 고유)
pid_t tgid; // 스레드 그룹 ID (사용자가 보는 PID)
char comm[TASK_COMM_LEN]; // 실행 파일 이름
/* ── 상태 ── */
unsigned int __state; // TASK_RUNNING, TASK_INTERRUPTIBLE 등
int exit_state; // EXIT_ZOMBIE, EXIT_DEAD
unsigned int flags; // PF_EXITING, PF_KTHREAD 등
/* ── 스케줄링 ── */
int prio; // 실효 우선순위 (0-139)
unsigned int policy; // SCHED_NORMAL, SCHED_FIFO 등
const struct sched_class *sched_class; // 스케줄링 클래스 vtable
struct sched_entity se; // CFS 스케줄링 엔티티
/* ── 메모리 ── */
struct mm_struct *mm; // 사용자 주소 공간 (커널 스레드는 NULL)
struct mm_struct *active_mm; // 현재 하드웨어에 로드된 주소 공간
/* ── 파일 시스템 ── */
struct files_struct *files; // 열린 파일 서술자 테이블
struct fs_struct *fs; // 루트 디렉터리, 현재 작업 디렉터리
/* ── 프로세스 계층 ── */
struct task_struct __rcu *real_parent; // 실제 부모 프로세스
struct list_head children; // 자식 프로세스 리스트
struct list_head sibling; // 형제 프로세스 리스트
/* ── 커널 스택 / 아키텍처 ── */
struct thread_info thread_info; // 아키텍처별 저수준 정보
struct thread_struct thread; // CPU 레지스터 저장 영역
/* ... 700줄 이상의 필드 생략 ... */
};
앞서 살펴본 PCB의 개념적 구성 요소가 이 구조체 안에 대응됨을 확인할 수 있다. 프로세스 상태는 __state, 식별자는 pid와 tgid, 스케줄링 정보는 prio와 sched_class, 메모리 관리 정보는 mm, 입출력 상태 정보는 files에 각각 대응된다.
리눅스에서는 task_struct 구조체들이 원형 이중 연결 리스트를 이루며, 이것이 바로 리눅스의 태스크 리스트다.
pid와 tgid: 프로세스와 스레드의 구분
리눅스에서는 Process와 스레드를 별도의 자료구조로 구분하지 않는다. 둘 다 동일한 task_struct로 표현되며, 차이는 자원을 공유하는 범위에서 비롯된다. 이를 가능하게 하는 것이 pid와 tgid 두 필드다.
pid는 커널 내부에서 각 task_struct를 고유하게 식별하는 번호로, 스레드 단위로 할당된다. tgidThread Group ID는 같은 프로세스에 속한 모든 스레드가 공유하는 번호로, 스레드 그룹의 리더(첫 번째로 생성된 스레드)의 pid값과 동일하다.3
사용자 공간에서 getpid()를 호출하면 current->tgid가 반환되고, gettid()를 호출하면 current->pid가 반환된다. 즉 사용자가 인식하는 "프로세스 ID"는 실제로는 스레드 그룹 ID인 셈이다.
thread_info와 current
task_struct는 슬랍 할당자slab allocator4를 통해 동적으로 할당되기 때문에, 커널이 현재 실행 중인 프로세스의 task_struct에 빠르게 접근할 수 있는 방법이 필요하다. 이 역할을 담당하는 것이 thread_info 구조체와 current 매크로다.
thread_info는 task_struct의 첫 번째 멤버로 자리 잡고 있으며, 프로세스의 커널 스택에서 아키텍처별 저수준 정보에 빠르게 접근할 수 있게 한다. current 매크로는 현재 실행 중인 프로세스의 task_struct 포인터를 반환하는데, 그 구현은 아키텍처에 따라 다르다. ARM64 환경에서는 sp_el0 레지스터에 현재 프로세스의 task_struct 포인터를 직접 저장하는 방식을 사용한다.
// arch/arm64/include/asm/current.h
static __always_inline struct task_struct *get_current(void)
{
unsigned long sp_el0;
asm ("mrs %0, sp_el0" : "=r" (sp_el0));
return (struct task_struct *)sp_el0;
}
#define current get_current()
x86-64 환경에서는 Per-CPU 변수per-cpu variable5를 통해 current를 구현한다. 어느 아키텍처든 핵심은 동일하다. 커널이 현재 실행 중인 프로세스의 PCB에 의 비용으로 접근할 수 있도록 보장하는 것이다.
출처
- Silberschatz, A., Galvin, P.B. and Gagne, G. (2018) Operating System Concepts. 10th edn. Hoboken: Wiley.
- Love, R. (2010) Linux Kernel Development. 3rd edn. Upper Saddle River: Addison-Wesley.
- Bryant, R.E. and O'Hallaron, D.R. (2016) Computer Systems: A Programmer's Perspective. 3rd edn. Boston: Pearson.
- Bovet, D.P. and Cesati, M. (2005) Understanding the Linux Kernel. 3rd edn. Sebastopol: O'Reilly.
Footnotes
-
멀티프로그래밍이란 여러 프로세스를 메모리에 동시에 올려두고 CPU가 한 프로세스에서 다른 프로세스로 전환하며 실행하는 기법을 말한다. CPU 이용률을 극대화하기 위한 기본적인 운영체제 설계 원칙이다. ↩
-
저장해야 하는 레지스터의 종류와 개수는 CPU 아키텍처에 따라 다르다. x86-64 환경에서는 16개의 범용 레지스터, 플래그 레지스터, 부동소수점/SIMD 레지스터 등이 포함되며, ARM64에서는 31개의 범용 레지스터와 NEON/FP 레지스터 등이 대상이다. 자세한 내용은 컨텍스트 스위치 포스트에서 다룬다. ↩
-
이 설계 덕분에
kill(pid, sig)로 시그널을 보내면 프로세스 전체(스레드 그룹)에 전달되고,tgkill(tgid, tid, sig)를 사용하면 특정 스레드에만 전달할 수 있다./proc/<tgid>/task/<pid>/경로를 통해 개별 스레드의 정보를 확인할 수 있다. ↩ -
슬랩 할당자란 커널에서 자주 사용되는 크기의 객체를 효율적으로 할당/해제하기 위한 메모리 할당 메커니즘이다.
task_struct처럼 크기가 고정된 구조체의 생성과 소멸이 빈번한 경우, 슬랩 할당자를 사용하면 메모리 단편화를 줄이고 할당 속도를 높일 수 있다. ↩ -
Per-CPU 변수란 CPU 코어마다 독립적인 복사본을 갖는 변수다. 각 코어가 자신만의
current값을 유지하므로 락lock 없이도 안전하게 현재 프로세스에 접근할 수 있다. ↩