The Process Control Block (PCB)
Imagine an operating system juggling two hundred processes on four CPU cores. It switches between them dozens of times per second. Every time the kernel takes a process off the CPU, it must remember everything about that process. It must remember well enough to resume it later, byte for byte. It must resume as if nothing happened. The question this article answers is deceptively simple: where does the operating system keep that memory? The answer is a single, central data structure called the Process Control Block, or PCB. The PCB is the kernel's ledger entry for a process. It is a record that exists from the moment a process is created until it is destroyed. It is not a user-visible object; no application ever reads or writes a PCB directly. Yet almost every kernel service that touches processes operates by reading or updating fields inside this structure. This includes the Scheduler, the memory manager, the file system, and the signal-delivery mechanism. Without it, Context Switching would be impossible. The kernel would have nowhere to save a stopped process's register values. It would also have nowhere to look up what memory that process is allowed to touch. This matters in real systems because the PCB is where abstraction meets mechanism. The Process Abstraction tells us conceptually that a process is "a program in execution." It has its own address space and state. The PCB is the concrete kernel object that makes that abstraction real and enforceable. Understanding the PCB is a prerequisite for understanding several things. It helps explain how Process State and the Lifecycle transitions are implemented. It helps explain how the kernel decides which process runs next. It also helps explain how Process Creation with fork and exec produces a new process. That new process is simultaneously a copy of its parent, yet fully independent. This article gives a precise definition of the PCB. It offers a step-by-step account of how the kernel creates, updates, and destroys it. It provides a worked trace through a context switch. It also gives an honest discussion of the space and time costs of maintaining one PCB per process. It closes with the common bugs that arise from mismanaging PCBs. It also covers the real operating systems — Linux, Windows, and others — that build their process machinery around this structure.
- The PCB is the kernel's per-process record: it stores state, CPU register values, memory-map pointers, and accounting data.
- One PCB exists per process, is created at process creation, and is freed only after the parent reaps the terminated process.
- The kernel indexes PCBs by process ID (PID) and typically links them into per-state queues (ready, blocked, running).
- Saving into and restoring from a PCB is the mechanism underlying Context Switching between processes.
- PCB access is a critical section: interrupts or locks must protect it from concurrent kernel-thread access.
- Real systems name it differently — Linux's `task_struct`, Windows' `EPROCESS`/`KTHREAD` — but the concept is identical.
The PCB is the single kernel object that turns the abstract idea of "a process" into a concrete, resumable, schedulable entity.
The idea
A Process Control Block is a kernel-resident data structure, one per process, that stores all the information the operating system needs to manage that process's execution and to suspend and later resume it without loss of correctness. It is the process's identity card, its saved brain-state, and its administrative file, combined into a single record. Conceptually, the PCB fields fall into four groups. Identification and state: a unique process ID (PID), the parent's PID, and the current position in the Process State and the Lifecycle state machine (new, ready, running, waiting, terminated). CPU context: the saved values of the program counter, stack pointer, general-purpose registers, and processor status flags — exactly what the CPU was doing at the instant the process was last stopped. Memory management information: pointers to page tables or segment tables, the base and limit of the address space, and pointers to the code, data, and stack regions. Accounting and resource information: CPU time consumed, priority and scheduling class, open file descriptors, pending signals, and pointers to the process's I/O status and owned resources such as sockets or locks. The crucial property that makes the PCB useful is this: the PCB is the only place any of this information lives when the process is not running. When a process is executing, some of its state lives in physical CPU registers. The instant the kernel decides to stop that process — because its time slice expired, it blocked on I/O, or a higher-priority process preempted it — every register value that matters is copied into that process's PCB. The reverse copy, from PCB back into registers, is what "resuming" a process means. This save/restore pair, applied to a pair of processes, is precisely the operation known as a context switch, which is introduced properly elsewhere; here we only need the PCB's role as the storage medium that makes it possible.
How it works
The kernel treats the set of all PCBs as a table, conventionally indexed by PID, and additionally threads PCBs onto linked lists corresponding to scheduling queues (ready queue, one queue per blocking device or condition, and so on). Three mechanisms operate on PCBs constantly: creation, context-switch save/restore, and destruction.
Creation. When a new process is made (conceptually via `fork`, detailed in Process Creation with fork and exec), the kernel allocates a fresh PCB, assigns it a new PID, records the parent's PID, initializes memory-map pointers (often by copying and adjusting the parent's), sets the state to new or ready, and inserts it into the ready queue.
Context switch (save and restore). Whenever the kernel stops one process and starts another, it copies the outgoing process's register file into its PCB, updates its state field, moves it to the appropriate queue, then does the reverse for the incoming process: reads registers out of its PCB, restores the memory-map (e.g., loads the page-table base register), sets its state to running, and transfers control via a return-from-interrupt.
Destruction. When a process calls exit (or is killed), its resources are released, its state is set to terminated (a "zombie" in Unix terminology), and it is kept as a husk — just enough of the PCB survives to report the exit status — until the parent calls `wait`, at which point the PCB is finally freed. ``` // Kernel-resident table of all PCBs, indexed by PID. struct PCB { pid_t pid pid_t parent_pid State state // NEW, READY, RUNNING, WAITING, TERMINATED CPUContext context // saved PC, SP, general registers, flags AddressSpace mm // page table base, segment bounds int priority long cpu_time_used FileDescriptorTable fds SignalMask pending_signals PCB next // intrusive link for queue membership } PCB_table[MAX_PID] // sparse array or hash map, PID -> PCB ready_queue, waiting_queue[..], zombie_list // linked lists of PCB* // --- Process creation --- function CREATE_PROCESS(parent): child = allocate(PCB) child.pid = next_free_pid() child.parent_pid = parent.pid child.state = READY child.mm = clone_address_space(parent.mm) // copy-on-write in practice child.context = copy(parent.context) // child resumes as if it were the parent child.priority = parent.priority child.cpu_time_used = 0 child.fds = clone(parent.fds) PCB_table[child.pid] = child enqueue(ready_queue, child) return child.pid // --- Context switch: stop `current`, start `next` --- function CONTEXT_SWITCH(current, next): disable_interrupts() // protect PCB fields (critical section) current.context = save_cpu_registers() // hardware -> PCB if current.state == RUNNING: current.state = READY enqueue(ready_queue, current) load_address_space(next.mm) // switch page-table base register restore_cpu_registers(next.context) // PCB -> hardware next.state = RUNNING enable_interrupts() return_from_interrupt() // resumes `next` at its saved PC // --- Blocking on I/O --- function BLOCK(proc, device): proc.context = save_cpu_registers() proc.state = WAITING enqueue(waiting_queue[device], proc) schedule_next() // pick a READY process, context switch to it // --- Termination --- function EXIT(proc, status): proc.state = TERMINATED proc.exit_status = status release(proc.mm) release(proc.fds) reparent_children(proc, init_process) move_to(zombie_list, proc) // PCB skeleton kept until parent waits schedule_next() function WAIT(parent): if exists z in zombie_list where z.parent_pid == parent.pid: status = z.exit_status remove(zombie_list, z) free(z) // PCB finally released return (z.pid, status) else: parent.state = WAITING schedule_next() ```
Complexity & correctness
Space. Each PCB occupies a fixed-size header (fields for state, PID, registers, pointers) plus variable-size satellite structures it points to, such as the file-descriptor table and page tables. The fixed portion is O(1) per process — typically hundreds of bytes to a few kilobytes depending on the kernel (Linux's `task_struct` is on the order of a few kilobytes; the kernel stack allocated alongside it adds more). Total kernel memory devoted to process bookkeeping is therefore
where is the number of live processes, since each PCB is allocated once per process and its size does not depend on how many other processes exist. This is why operating systems impose a maximum process count (`pid_max`, ulimits): PCB memory is real, unswappable, privileged kernel memory, and an unbounded number of processes would exhaust it.
Time. Three operations dominate:
- Lookup by PID: with a PID-indexed array or hash table, this is average case. (A naive linked-list scan over all processes would be ; real kernels avoid this.)
- Enqueue/dequeue from a scheduling queue: using intrusive linked lists (the `next` pointer lives inside the PCB itself, so no separate allocation is needed to add a PCB to a queue). Some schedulers use priority structures (heaps, red-black trees, as in Linux's CFS) giving insertion/removal in exchange for better scheduling decisions — a classic time/quality tradeoff.
- Context switch: copying the register file and switching the memory-map pointer is in the number of processes — it touches a fixed, small number of registers regardless of — but it is not free in absolute terms: it costs a fixed number of CPU cycles plus, critically, the cost of a TLB flush or address-space-ID reload, which shows up as cache/TLB misses after the switch. This constant-but-nonzero cost is why excessive context switching (thrashing between many runnable processes) degrades throughput even though each individual switch is .
Correctness — the invariant. The essential correctness property the PCB must maintain is: > Resumption invariant: at any instant a process is not the one occupying the CPU, its PCB's `context` field holds exactly the register values it would have had, had it simply been paused in place. This invariant is established by construction: `CONTEXT_SWITCH` always performs `save_cpu_registers()` into the outgoing process's PCB before it overwrites CPU state with the incoming process's values, and interrupts are disabled for the duration so that no nested context switch can occur mid-save. If this ordering were violated — say, if the scheduler picked the next process and loaded its registers before finishing the save of the current one — the outgoing process's register file would be corrupted and it would resume, later, at the wrong point with wrong data, silently and catastrophically. The invariant, combined with disabling interrupts around the save/restore (a critical section, foreshadowing synchronization concerns discussed alongside concurrency), is exactly what guarantees a process can be stopped and resumed an arbitrary number of times with behavior indistinguishable from uninterrupted execution — the defining promise of preemptive multitasking. A second invariant governs lifecycle bookkeeping: every PCB that has reached `TERMINATED` remains in `zombie_list` until exactly one `wait` call by its parent consumes it. This guarantees exit status is never lost and PCB memory is never freed while a parent might still query it — at the cost of tying up a small PCB skeleton indefinitely if a parent never calls `wait` (a real and named bug class, "zombie accumulation," discussed below).
Worked example
Trace a small scenario: two user processes, A (pid 40) and B (pid 41), on a single-core system with a preemptive round-robin scheduler and a 10 ms timer tick. Process A is running; process B is ready and waiting in the queue.
| Time | Event | PCB(A) state | PCB(B) state | CPU registers | Ready queue |
|---|---|---|---|---|---|
| t=0 | A running, executing `add r1, r2` | RUNNING, context stale (A owns live registers) | READY, context = {PC=0x2010, ...} | PC=0x1050 (A's live PC) | [B] |
| t=10ms | Timer interrupt fires | RUNNING → about to be saved | READY | still A's values, about to be copied | [B] |
| t=10ms+ε | `CONTEXT_SWITCH(A, B)`: save A's registers into PCB(A).context | READY, context={PC=0x1054, SP=..., ...} | READY | (in flight) | [A] (enqueued) |
| t=10ms+2ε | Load B's address space (page-table base ← PCB(B).mm) | READY | READY | page-table base = B's | [A] |
| t=10ms+3ε | Restore B's registers from PCB(B).context | READY | RUNNING | PC=0x2010 (B's saved PC) | [A] |
| t=10ms+4ε | Return-from-interrupt resumes B at 0x2010 | READY | RUNNING | B executing | [A] |
| t=15ms | B calls `read()` on disk, blocks | READY | WAITING (moved to waiting_queue[disk]) | B's registers saved into PCB(B) | [A] |
| t=15ms+ε | Scheduler picks A (only ready process) | RUNNING | WAITING | PC restored to 0x1054, A resumes | [ ] |
| t=25ms | Disk I/O completes, interrupt moves B to ready | RUNNING | READY (re-enqueued) | A still running | [B] |
| t=20+10ms tick | Timer fires again, switch A→B | READY | RUNNING | B's context restored | [A] |
Notice the two facts this trace makes concrete. First, process A resumed at `PC=0x1054`, one instruction past where it was interrupted (`0x1050`) — exactly the resumption invariant, because the interrupt handler saved the post-increment PC. Second, PCB(B) is the only place `PC=0x2010` existed between t=10ms and the moment B was restored; if the kernel had crashed or the PCB table had been corrupted in that window, B's execution point would be permanently lost. That fragility, and the discipline required to avoid it, is exactly why the PCB save/restore sequence is written as an atomic, interrupt-disabled operation.
Common pitfalls & traps
| Pitfall | What goes wrong | Fix |
|---|---|---|
| Forgetting to disable interrupts during save/restore | A second interrupt fires mid-save, the scheduler re-enters, and registers get overwritten with a third process's values — silent corruption | Treat the save/restore sequence as a critical section; disable interrupts (or use a lock) around it |
| Freeing a PCB before the parent calls `wait` | Exit status is lost; a subsequent `wait` returns stale or garbage data, or crashes on a dangling pointer | Keep terminated PCBs as zombies until reaped; only then free |
| Never calling `wait` on children | Zombie PCBs accumulate forever, exhausting the PID table / PCB slots — a real resource leak, not just cosmetic | Reap children explicitly, or use `SIGCHLD` handlers that call `wait` automatically |
| Copying the PCB by value instead of by reference across kernel code paths | Updates to "the" process's state silently apply to a stale copy; the real PCB never changes | Always operate on PCBs through pointers/references stored in the PID table |
| Confusing PCB with the process's user-space stack or heap | Assuming the PCB is large and includes all process data, leading to wrong space-complexity reasoning | PCB stores metadata and pointers to memory regions, not the regions themselves |
| Racing PCB field updates across CPUs (multicore) | Two cores simultaneously update `state` or the ready-queue `next` pointer, corrupting the linked list | Protect PCB fields with per-PCB locks or lock-free structures in SMP kernels |
Pioneers & history
The PCB concept crystallized alongside the first true multiprogramming operating systems, when designers first had to answer: if the CPU can run only one instruction stream at a time, but many programs must appear to progress simultaneously, where does each program's "paused self" live? Early batch systems ran one job at a time and needed no such record; the PCB became indispensable only once systems supported timesharing and multiple concurrent jobs contending for one processor.
Origins · 1 minEdsger W. Dijkstra and how this was discovered
Dijkstra's work on the THE multiprogramming system formalized the idea of processes as independent, schedulable sequential threads of control, each requiring its own saved state — a direct conceptual ancestor of the PCB as a kernel object distinct from the running program itself. His insistence on rigorous, layered system structure shaped how later kernels organized per-process bookkeeping.
Where this matters
Every general-purpose operating system kernel implements some form of PCB, though the name and exact layout vary. Linux calls it `task_struct`, a large structure (several kilobytes) containing state, scheduling class and priority, memory descriptor (`mm_struct`), open-file table, signal handlers, and namespace/cgroup pointers for containerization — the same PCB concept extended to support Docker-style isolation. Windows splits the concept across an `EPROCESS` block (address space, handle table, security token) and one `KTHREAD` per schedulable thread within it, reflecting the OS's thread-centric scheduling model. Real-time operating systems used in embedded and automotive systems (e.g., FreeRTOS's task control block, VxWorks) keep PCBs deliberately tiny and fixed-size to make context-switch latency bounded and predictable, since real-time guarantees depend on that cost being both small and known. Virtualization hypervisors maintain an analogous structure per virtual CPU, and container runtimes lean on the kernel's existing PCB machinery (with added namespace fields) rather than inventing a new one — which is why understanding the PCB is foundational before studying Context Switching internals, Process Creation with fork and exec semantics, or multiprocessor scheduling.
Ask the tutor: How exactly does the page-table base register swap inside a PCB-driven context switch interact with the TLB, and why does that interaction make context switches more expensive than the O(1) register-copy cost alone suggests?
Is the PCB the same thing as the process itself?
No. The process, as an abstraction, is "a program in execution" — an active entity with behavior over time. The PCB is a passive kernel data structure: a record the kernel maintains about that process. When the process is running, part of its true state lives in physical CPU registers, not in the PCB; the PCB only holds a faithful snapshot while the process is stopped. Thinking of the PCB as a "profile" or "save file" for the process, rather than the process itself, avoids a common conceptual confusion, especially since in casual speech people often say "the kernel schedules PCBs," which is shorthand for "the kernel schedules processes by manipulating their PCBs."
Does every thread have its own PCB, or do threads share one?
It depends on the kernel's threading model, which is why this article scopes itself to full processes. In kernels with a one-PCB-per-process design extended for threads (like Linux's `task_struct`, reused per-thread with shared memory-descriptor pointers), each thread gets its own minimal control block for registers and stack, but threads within one process share the memory-map pointer, open files, and signal handlers. Windows makes this split explicit with one `EPROCESS` (process-level, shared resources) and multiple `KTHREAD`s (one per thread, holding registers and scheduling info). The shared fields are exactly the resources the Process Abstraction defines as process-wide rather than thread-specific.
Why can't the kernel just keep everything in CPU registers and skip the PCB?
Because there is only one physical register file (per core) and potentially hundreds of processes. The instant the kernel must run a different process — due to a timer interrupt, an I/O block, or preemption — it needs somewhere else to keep the stopped process's state, or that state is gone forever. The PCB is that "somewhere else." It exists precisely because CPU registers are a scarce, shared, physical resource, while the illusion of many processes progressing at once requires each to have durable state that survives being off the CPU.
How big is a real PCB, and does that matter for performance?
Real PCBs are on the order of a few kilobytes; Linux's `task_struct` plus its associated per-thread kernel stack together typically total several kilobytes to a few tens of kilobytes. This size matters for two reasons: total system memory scales linearly with the maximum number of simultaneous processes, and copying or initializing large PCB-adjacent structures (like cloning file tables) adds fixed overhead to every `fork`. This is one reason threads, which share most PCB-adjacent state, are cheaper to create than full processes.
What happens to the PCB during a system call, as opposed to a full context switch?
A system call does not necessarily require switching to a different process's PCB. The CPU traps into kernel mode, but if no scheduling decision is made, the same process keeps running (now in kernel mode), and its PCB is not touched for save/restore purposes — only consulted, for instance to check permissions or update accounting fields like CPU time used. A full context switch, by contrast, always involves saving one PCB and restoring another. System calls that block (like `read` on a slow device) do trigger a context switch as a consequence, but the switch itself is a separate mechanism layered on top of the system-call trap.
Can a process's PCB be swapped out to disk like other memory?
In most modern general-purpose OS designs, no: PCBs live in non-pageable (pinned) kernel memory precisely because the kernel must be able to access them at any time, including during the low-level, latency-critical work of interrupt handling and scheduling, when paging machinery itself may not be usable. Some historical or memory-constrained systems did support swapping entire process images including control information to disk under extreme memory pressure, but this is distinct from ordinary demand-paging of user-space memory and is far rarer in contemporary kernels.
How does the PCB relate to security and isolation between processes?
The PCB's memory-map pointer is the enforcement mechanism for address-space isolation: because the kernel switches the page-table base register using the value stored in the incoming process's PCB, a process can never accidentally (or deliberately, without kernel cooperation) load another process's address space. Similarly, the open-file-descriptor table and security-token fields stored in or pointed to by the PCB define exactly what resources a process may access. Corrupting a PCB's memory-map pointer, hypothetically, would be a severe security bug, which is why PCBs live in protected kernel memory inaccessible to user-mode code.