The Process Abstraction
Imagine your laptop right now: a browser rendering a video, a text editor waiting for keystrokes, a music player streaming audio, and a background updater checking for patches. Each program believes it owns the entire machine — its own memory, its own CPU, its own universe. In reality a single CPU (or a handful of cores) is being sliced and shared among dozens of running programs. The mechanism that makes this illusion possible, safe, and efficient is the process. A process is the operating system's abstraction of a running program. It is not the program itself. A program is a passive file of instructions sitting on disk. Instead, it is the live, dynamic instance of that program as it executes. This includes its memory contents, its register values, its open files, and its point of execution. Without this abstraction, running two programs "at once" would require something more. Each program would need to know about every other program on the machine. It would have to coordinate memory usage by hand. It would need to never crash the others by accident. That is untenable at any scale beyond a single trusted application. The process abstraction solves this by giving every running program its own private, virtualized world. This creates an illusion of a dedicated processor and a dedicated, boundless memory. Meanwhile, the OS transparently multiplexes the real, scarce hardware underneath. This is the same intellectual move as The OS Role and Its Core Abstractions: turn a scarce, shared physical resource into an abundant, private virtual one. This happens through a combination of mechanism (context switching, memory protection) and policy (scheduling). This article builds the concept of a process from first principles. It explains what state must be captured to describe "a program in motion." It shows how that state is laid out in memory and in the CPU. It explains why isolating this state per-program is the foundation for multiprogramming and protection. This foundation eventually leads to the finer-grained abstraction of Threads. It sets up the vocabulary — address space, execution context, program counter. The following articles on Process State and the Lifecycle and The Process Control Block (PCB) depend on this vocabulary directly.
- A process is a program in execution: its code, data, and current point of execution, not just the static file on disk.
- Every process gets a virtualized address space (code, data, heap, stack) that looks private even though it shares physical memory with others.
- The execution context — program counter, registers, stack pointer — captures exactly where a paused process should resume.
- The CPU is time-shared: the OS saves one process's context and restores another's, an operation called a context switch.
- This abstraction enables multiprogramming: many processes can be in progress even with only one physical CPU.
- Processes give isolation: a bug or crash in one process cannot corrupt another's memory, thanks to hardware-enforced protection.
A process is the illusion of a private computer — its own CPU and its own memory — built by the OS out of one shared, physical machine.
The idea
A process is formally defined as an instance of a program in execution, together with all the state needed to describe that execution at a given instant. A program, by contrast, is a static entity: a sequence of instructions and initialized data stored as a file on disk (an executable). The distinction matters because the same program file can be launched twice — open two terminal windows running the same shell binary — and produce two entirely distinct processes, each with its own memory, its own variables, and its own execution point, even though they started from identical bytes on disk. To fully describe "a program in motion," the OS must track two broad categories of state. The first is the address space: the range of memory addresses a process can reference, containing its executable code (the text segment), its global and static variables (data), a region for dynamically allocated memory that grows as the program runs (the heap), and a region for function call frames and local variables that grows and shrinks as functions are called and return (the stack). The second is the execution context (also called machine state or hardware context): the values held in CPU registers while the process runs, most critically the program counter (PC), which records the address of the next instruction to execute, and the stack pointer, which records the top of the current call stack. Other general-purpose registers hold whatever intermediate values the running computation needs. Crucially, a process's address space is virtualized. The addresses a program's instructions reference — say, "load the value at address 0x1000" — are not literal physical RAM addresses. They are addresses in a private, per-process virtual address space, translated to physical memory by hardware (the memory management unit) under OS supervision. This virtualization is what permits two processes to both believe their code starts at address 0x1000, without colliding in physical memory, and what makes it impossible for one process's stray pointer to accidentally read or corrupt another process's memory. Address space virtualization is covered in depth in later articles on memory management; here the essential point is that a process is defined relative to its own private view of memory, not the machine's raw physical memory.
How it works
Building the process abstraction requires the OS to answer two operational questions: how is a single process represented, and how does the OS switch the physical CPU among many such representations while preserving the illusion that each process runs continuously?
Representing a process. When a program is launched, the OS creates a fresh address space and loads the program's code and initial data into it, reserves space for a stack and a heap, and initializes the CPU registers — most importantly, setting the program counter to the program's entry point. The OS also allocates a bookkeeping record (which later articles call the Process Control Block (PCB)) that stores metadata: a unique process identifier, the process's current state, and — whenever the process is not actively running — a saved copy of its execution context, so it can be resumed exactly where it left off.
Time-sharing the CPU. With only one (or a few) physical CPUs and potentially dozens of processes wanting to run, the OS achieves the illusion of many simultaneously running processes through context switching: rapidly stopping one process, saving its exact execution state, loading a different process's saved state into the CPU, and resuming it. Done fast enough (milliseconds), this creates the perception of parallelism even on a single core — a technique called time-sharing or multiprogramming. The mechanism relies on a periodic interrupt (a timer interrupt) that forces the CPU to trap into the OS at regular intervals, regardless of what the currently running process is doing. This guarantees the OS periodically regains control — a property essential for fairness and responsiveness, since without it a buggy or malicious process could simply never yield the CPU. ```pseudocode // Executed by the timer-interrupt handler, inside the OS kernel. // 'current' is the process the CPU was executing at the moment // the timer fired. function ON_TIMER_INTERRUPT(current): // 1. Hardware has already saved the interrupted PC and a few // registers onto the kernel stack automatically (trap entry). // The handler now saves the REST of the context explicitly. current.context.pc <- read_saved_pc_from_trap_frame() current.context.registers <- read_all_general_registers() current.context.stack_pointer <- read_stack_pointer_register() current.state <- READY // 2. Decide who runs next (policy: covered by the scheduler). next <- SCHEDULER_PICK_NEXT_READY_PROCESS() // 3. Restore the chosen process's saved context into the CPU. load_registers(next.context.registers) load_stack_pointer(next.context.stack_pointer) next.state <- RUNNING // 4. Return-from-interrupt jumps to next.context.pc automatically, // resuming 'next' exactly where it was previously paused. RETURN_FROM_INTERRUPT(next.context.pc) ``` ```pseudocode // Executed once, when a new process is launched (e.g. on exec()). function CREATE_PROCESS(program_file): pcb <- ALLOCATE_PCB() pcb.pid <- NEXT_UNIQUE_ID() addr_space <- ALLOCATE_VIRTUAL_ADDRESS_SPACE() LOAD_CODE_AND_DATA(addr_space, program_file) // text + data segments ALLOCATE_HEAP(addr_space, initial_size = 0) // grows via malloc-like calls ALLOCATE_STACK(addr_space, size = DEFAULT_STACK) // for calls/locals pcb.address_space <- addr_space pcb.context.pc <- program_file.entry_point pcb.context.sp <- top_of(addr_space.stack) pcb.context.regs <- ZERO_REGISTERS() pcb.state <- READY SCHEDULER_ADD_TO_READY_QUEUE(pcb) return pcb.pid ``` Each context switch has a real, non-zero cost: saving and restoring registers, flushing pipeline state, and — because a new address space usually means new virtual-to-physical mappings — often flushing the CPU's translation lookaside buffer (TLB), a cache that speeds up address translation. The OS's scheduling policy (discussed in dedicated articles) decides which ready process to run next; the mechanism above is agnostic to that policy and only implements the how.
Complexity & correctness
Space complexity. Each process requires fixed metadata for its control block (a handful of registers, a state flag, an identifier — bounded regardless of program size) plus space proportional to its address space contents: code size, static data size, and however much heap and stack it has allocated at a given moment. If processes exist simultaneously, total OS bookkeeping overhead is in the number of processes, independent of how large any individual process's memory footprint is, since the PCB itself does not grow with the process's data.
Time complexity of a context switch. A context switch takes time where is the number of registers the hardware architecture defines — a small constant (dozens, not growing with input size), since saving and restoring registers is a fixed sequence of load/store instructions. In practice the dominant cost is not the register save/restore itself but secondary effects: TLB flushes and cold caches after switching address spaces, which cause subsequent memory accesses to be slower until the new process's working set is "warmed up" again. This is why context switches, though algorithmically -ish, carry real, measurable overhead (typically microseconds) that scheduling policies must amortize by not switching too frequently.
Correctness argument. The correctness property the process abstraction must guarantee is transparent resumability: a paused process, when resumed, must behave exactly as if it had never been paused — same register values, same memory contents, same next instruction. This is an invariant maintained across every context switch: > Invariant. For any process in the `READY` or `BLOCKED` state, the PCB for holds a complete and exact snapshot of 's execution context (PC, SP, all registers) as of the instant stopped running, and 's address space is untouched by any other process. This invariant is established by two guarantees working together: 1. Save-before-switch discipline. The interrupt/trap handler never overwrites a register before it has been copied into the outgoing process's PCB. As long as the save step (lines 1 in `ON_TIMER_INTERRUPT`) completes atomically with respect to interrupts — itself guaranteed because interrupts are disabled while the kernel is mid-handler — no register value is lost. 2. Hardware-enforced address-space isolation. The memory management unit checks every memory access against the currently active process's address-space mappings; an access outside them faults rather than silently succeeding. This guarantees the second half of the invariant — no process can inspect or corrupt another's memory — independent of software bugs in any single process. Given the invariant, resumption correctness follows directly: restoring the saved PC, SP, and registers reconstructs the exact CPU state that existed at the pause point, so the next instruction executed is precisely the one that would have executed had the process never been interrupted. The process cannot observe that it was ever paused (barring timing side channels), which is precisely the illusion of a dedicated, uninterrupted CPU.
Tradeoffs. The abstraction is not free. Isolation and virtualization cost real time (address translation, TLB management, trap overhead) and real space (per-process metadata, page tables). A design that skipped isolation — letting all programs share one flat physical address space directly — would be faster and simpler but catastrophically fragile: any bug in any program could corrupt any other program or the OS itself. The process abstraction trades a modest, well-amortized overhead for strong safety and modularity, a tradeoff virtually every general-purpose OS accepts. Later refinements — Threads sharing one address space, or lightweight containers — exist precisely to reduce this overhead in situations where full isolation is unnecessary.
Worked example
Consider a machine running two processes: `P1` (a text editor, PID 7) and `P2` (a compiler, PID 8), sharing one CPU. Trace what happens over three timer interrupts, assuming a simple round-robin policy with a fixed time slice.
| Time | Event | PC saved / restored | State of P1 | State of P2 |
|---|---|---|---|---|
| t=0 | `P1` created, loaded into memory, PC set to its entry point `0x4000` | P1.pc = 0x4000 | READY → RUNNING | — |
| t=0 to t=10ms | P1 runs: executes instructions, PC advances to `0x4050`, pushes 2 stack frames | (live in CPU) | RUNNING | — |
| t=10ms | Timer interrupt fires | Save P1.context: pc=0x4050, sp=0x7FFE20, R0=13 | RUNNING → READY | — |
| t=10ms | `P2` created (compiler launched), PC set to `0x5000` | P2.pc = 0x5000 | READY | READY → RUNNING |
| t=10ms to t=20ms | P2 runs: allocates heap memory, calls a function, PC advances to `0x5120` | (live in CPU) | READY | RUNNING |
| t=20ms | Timer interrupt fires | Save P2.context: pc=0x5120, sp=0x7FFA10, R0=99 | READY | RUNNING → READY |
| t=20ms | Scheduler picks `P1` again; restore P1.context | Load pc=0x4050, sp=0x7FFE20, R0=13 | READY → RUNNING | READY |
| t=20ms to t=30ms | P1 resumes exactly at `0x4050` — the instruction right after where it paused | (live in CPU) | RUNNING | READY |
Notice the essential fact this trace demonstrates: at , `P1` resumes at PC `0x4050`, not `0x4000`. From `P1`'s own point of view — as a sequence of instructions executing one after another — there is no discontinuity at all; it cannot tell that ten milliseconds of "wall-clock time" and an entire other program's execution happened in between. That is the process abstraction working correctly: the illusion of a dedicated, continuously running processor, built out of one physical CPU rapidly multiplexed between two independent execution contexts and two disjoint address spaces (`P1`'s code lives at `0x4000`–`0x40FF`, `P2`'s at `0x5000`–`0x51FF`, in their respective virtual spaces — both may even be mapped to the same virtual addresses without collision, since each is translated independently to different physical frames).
Common pitfalls & traps
| Pitfall | Why it happens | Consequence |
|---|---|---|
| Confusing "program" with "process" | Treating the static executable file as if it were the running entity | Miscounting: two runs of the same binary are two processes, not one |
| Assuming a process's addresses are physical | Forgetting address-space virtualization | Wrongly believing two processes writing to "address 0x1000" would clash |
| Forgetting to save all context on switch | Partial context save (e.g., forgetting status/flag registers) | Resumed process silently corrupts its own control flow or arithmetic results |
| Ignoring context-switch overhead | Assuming switching is free | Over-frequent switching (very short time slices) can dominate CPU time with overhead instead of useful work |
| Treating PCB size as scaling with program size | Confusing metadata with the address space it describes | Wrong space-complexity reasoning — PCB is , address space is |
Pioneers & history
The process abstraction crystallized during the era of time-sharing systems, when researchers first confronted the problem of running many interactive users' programs on one expensive, scarce mainframe. Earlier batch-processing systems ran one program to completion before loading the next; the leap to multiprogramming — keeping several programs' state alive simultaneously in memory, switching the CPU rapidly among them — required formalizing exactly what "a program in execution" meant as a distinct, manageable object, separate from the static program text.
Origins · 1 minEdsger W. Dijkstra and how this was discovered
Dijkstra's 1968 work on the THE operating system was among the first to treat processes as rigorously defined, independently schedulable entities, and to reason formally about their correct cooperation — laying groundwork for synchronization concepts covered in later articles.
Where this matters
Every general-purpose operating system — Linux, Windows, macOS, Android, iOS — implements the process abstraction as its fundamental unit of execution and protection. When a phone runs a maps app, a messaging app, and a background music player "at once," each is a separate process, isolated so that a crash in one cannot take down the others — a guarantee users depend on dozens of times a day without noticing. Server infrastructure relies on the same abstraction at scale: a web server might spawn one process per incoming connection (or, more commonly today, use threads or event loops within a process, but the isolation guarantees still trace back to process boundaries between tenants). Sandboxing technologies — web browser tabs, containerized microservices, virtual machines — are all, at their core, refinements or compositions of the process abstraction, tightening or loosening its isolation boundary for particular tradeoffs between safety and performance. Kernel-level security models (permissions, access control, crash containment) are enforceable only because processes are isolated; without this abstraction, a single misbehaving program could read another user's passwords directly out of shared memory. Understanding processes precisely is the prerequisite for understanding Process State and the Lifecycle (how a process moves between running, ready, and blocked), the Process Control Block (PCB) (exactly what metadata the OS tracks per process), and eventually Threads (a lighter-weight unit of execution that shares a process's address space).
Ask the tutor: How does the OS actually create the very first process at boot, before any process yet exists to ask for one?
Is a process the same thing as a thread?
No. A process bundles both an address space and at least one thread of execution together with its own protection boundary. A thread is a single, schedulable stream of execution — its own program counter, stack, and registers — but by default it shares the address space of the process it belongs to with any sibling threads. A process can contain multiple threads, all reading and writing the same heap and data segment, whereas two separate processes cannot directly touch each other's memory at all. Threads are covered as a distinct, lighter-weight abstraction built on top of the process concept established here.
Why does the OS need to virtualize memory addresses instead of just giving each process a distinct chunk of physical RAM?
Giving out fixed physical chunks would work in principle but creates serious problems: programs would need to be compiled knowing in advance which physical addresses they'll occupy, memory couldn't be flexibly resized or relocated, and there would be no way to enforce that one process's stray pointer can't reach into another's chunk except by manual, error-prone bounds checking. Virtual addressing lets every process assume it starts at a clean, private address space (say, always starting at 0), with the hardware transparently and safely translating and checking every access, enabling relocation, protection, and even sharing (like shared libraries) without recompilation.
Does having more processes always mean more real parallelism?
No. On a single-core CPU, "many processes running" is really many processes taking turns extremely quickly — concurrency, not true parallelism. Only with multiple physical cores can more than one process literally execute instructions at the same instant, which is true parallelism. The process abstraction supports both: it works identically whether the underlying hardware has one core (time-sharing) or many (time-sharing plus genuine simultaneous execution), because the abstraction only guarantees each process an illusion of a dedicated CPU, not a promise about how many physical CPUs exist underneath.
What exactly gets stored in a process's saved execution context, and where?
The saved execution context includes the program counter, stack pointer, general-purpose registers, and processor status/flags register — essentially every piece of CPU state needed to resume exactly where execution left off. This snapshot is stored in the process's control block, a kernel data structure maintained per process (detailed fully in the article on the Process Control Block (PCB)). It is not stored inside the process's own address space, both because a crashed or misbehaving process must not be able to tamper with its own saved state, and because the OS must be able to read it even while that process is not running.
Can a process exist without ever actually running on the CPU?
Yes — this is common and expected. A process might be `READY` (waiting for its turn on the CPU) or `BLOCKED` (waiting on I/O, like a disk read or network response) for the vast majority of its lifetime, only briefly occupying the CPU when scheduled. The distinct states a process moves through, and the transitions between them, are the subject of the dedicated article on Process State and the Lifecycle; the key point here is that "being a process" does not require "currently executing" — a process is defined by its saved state and address space existing, regardless of whether it holds the CPU at this instant.
Why is a context switch relatively expensive compared to executing a normal instruction?
A single register save or restore is cheap — a handful of CPU cycles. The real cost comes from second-order effects: switching address spaces typically invalidates cached address-translation entries (the TLB), forcing subsequent memory accesses to be slower until translations are recomputed, and it disrupts CPU cache locality, since the newly scheduled process's data likely isn't in the cache. These effects mean a context switch, while conceptually simple, can cost on the order of microseconds — potentially thousands of CPU cycles — which is why schedulers try to avoid switching too frequently, balancing responsiveness against this overhead.
Does every operating system implement processes identically?
The core abstraction — private address space plus execution context, isolated and time-shared — is universal across general-purpose operating systems, but implementation details vary. Some systems make process creation cheap and encourage many small processes (classic Unix philosophy); others favor heavier processes and rely more on threads for concurrency. Mechanisms for creating processes (such as `fork` duplicating a parent, versus creating one from scratch) differ across systems, but the underlying guarantee — that each process believes it has a dedicated, isolated computer — is the shared invariant every mainstream OS implements.