The Anatomy of Execution: Processes, Threads, and Memory Isolation
A conceptual deep dive into what it means for code to execute. We explore the fundamental differences between a process (a resource container) and a thread (an execution unit), and how the OS isolates them.
In our Foundations focus group, we explored how a compiler turns C code into assembly, how a linker builds an executable, and how the execve system call loads that executable into memory.
But once that code is loaded, what exactly is it?
When we talk about software running on a computer, we use the terms Process and Thread interchangeably. In casual conversation, this is fine. In systems programming, mixing these terms will lead to fundamentally flawed architecture, race conditions, and catastrophic security vulnerabilities.
This chapter is a strictly conceptual primer on execution. We will not look at a single line of C code, nor will we debate Linux vs. Windows. Before we can write the code, we must build the correct mental model of how an Operating System views our program.
The Process: A Container of Resources
A common misconception is that a process “runs.” A process does not run.
A process is best understood as a Resource Container. When you double-click an icon or run a command in the terminal, the Operating System creates a protective bubble. Everything inside that bubble belongs to the process, and nothing outside the bubble can peek in without explicit permission.
What goes inside this container?
- Virtual Memory Space: A complete, illusionary mapping of RAM. To the process, it appears to own all the memory in the computer (or rather, the user-space portion of it, e.g., the lower 3 GB on 32-bit Linux, and the 47-bit canonical lower half on modern x86-64).
- File Descriptors / Handles: A table of active connections to the outside world—open files, network sockets, or pipes to other programs.
- Execution Context & Limits: The Process ID (PID), current working directory, umask, signal dispositions, and resource limits.
- Security Context: The user who owns the process and the permissions they hold.
- Environment Variables: The configuration state passed to the program at launch.
Think of a process as a fully equipped workshop. It has tools (memory), raw materials (files), and locked doors (security). But a workshop, on its own, cannot build anything. It needs a worker.
The Thread: The Unit of Execution
The Thread is the worker inside the workshop.
A thread is the actual sequence of instructions being executed by the CPU. While a process owns the resources, the thread owns the execution state.
What does a thread own exclusively?
- A Program Counter (Instruction Pointer): A hardware register that points to the exact memory address of the next instruction to execute.
- Hardware Registers: The current state of the CPU’s general-purpose registers (
rax,rbx, etc.). - Stacks: A user-mode stack for local variables and function jumps, plus a kernel stack for system calls.
- Thread-Specific State: Thread-local storage, its own signal mask, and its scheduling priority.
The 1:N Relationship
When a process is created, the OS automatically spawns one thread (the main thread) and places it inside the container.
However, a single workshop can have many workers. A process can spawn hundreds of threads. Because all these threads live inside the same process container, they inherently share everything the process owns.
- The Danger of Sharing: If Thread A opens a file, Thread B can read it. If Thread A allocates a massive chunk of memory on the heap, Thread B has full access to read or overwrite it. This shared memory space is why multi-threaded programming is notoriously difficult; without careful coordination (locks/mutexes), workers will overwrite each other’s work.
Memory Isolation and The MMU
Why is the process container so secure? How does the OS prevent a malicious process from reading the memory of your password manager?
The secret lies in hardware, specifically the Memory Management Unit (MMU).
When a thread attempts to read from a memory address like 0x400500, it is not reading from physical RAM chip at that exact location. It is reading a Virtual Address.
The MMU acts as a bouncer. Every time a thread asks for memory, the MMU first checks a fast hardware cache called the Translation Lookaside Buffer (TLB). If the translation isn’t cached there, it walks the Page Table—a map owned by the process container—to translate the virtual address (0x400500) to a physical RAM address (e.g., 0x8F2000).
Crucially, every process has its own unique Page Table.
Process A’s 0x400500 translates to Physical Address X.
Process B’s 0x400500 translates to Physical Address Y.
Process A simply has no mapping for the physical frames backing Process B’s memory, and only the kernel can install one. Isolation is therefore a policy the kernel enforces through the page tables, not a physical law—processes that want to share memory can ask the kernel for it (e.g., via shared memory), and privileged tools like debuggers can read another process’s memory. The MMU simply will not translate unmapped addresses. This is Memory Isolation.
Context Switching
A typical modern computer might have 8 CPU cores, but it runs thousands of threads concurrently. How?
The OS Kernel employs Time Slicing via a scheduler. The CPU executes a thread for a few milliseconds, pauses it, and switches to another thread. This happens so fast that it creates the illusion of parallel execution.
This pause-and-resume is called a Context Switch.
Thread Context Switch (Lightweight)
If the OS pauses Thread A and resumes Thread B, and both threads belong to the same process, the context switch is relatively fast. The OS only needs to save Thread A’s registers and instruction pointer, and load Thread B’s. The workshop (the virtual memory space) remains exactly the same.
Process Context Switch (Heavyweight)
If the OS pauses a thread in Process A and resumes a thread in Process B, it’s a massive operation. Not only must the registers be swapped, but the entire virtual memory landscape must be changed. The kernel writes the new page-table root into CR3 (x86-64) or TTBR0_EL1 (ARM64). Historically this flushed the whole TLB, which is why process switches were so expensive; modern CPUs tag TLB entries with an address-space ID (PCID on x86, ASID on ARM), so entries survive the switch. The data caches are not flushed—they are physically tagged—but the new process gradually evicts the old process’s cache lines, so the real cost is a cold cache and TLB working set rather than an explicit flush.
Because of this cold cache penalty and scheduler overhead, multi-threading is often chosen over multi-processing for tight performance loops. However, plenty of performance-critical systems (like Chrome, PostgreSQL, or nginx workers) deliberately choose multi-process architectures for strong isolation and fault containment. Both architectures have their critical use cases.
Next Steps
Now that we have established the theoretical boundary between a resource container (Process) and an execution unit (Thread), we are ready to look at how real operating systems implement them.
In the next chapter, we will dive into the Linux kernel to see exactly how it bends these rules using the clone syscall and a brilliant optimization known as Copy-on-Write (COW).