All chapters in this track
- 01Chapter 1: Why OS Fundamentals Matter to Application Engineers
- 02Chapter 2: Processes, Threads, and the Process Lifecycle
- 03Chapter 3: The Memory Hierarchy: Registers to Disk
- 04Chapter 4: CPU Scheduling Algorithms
- 05Chapter 5: Memory Management I: Paging and Segmentation
- 06Chapter 6: Memory Management II: Virtual Memory and Page Replacement
- 07Chapter 7: File Systems: How Data Is Actually Stored and Retrieved
- 08Chapter 8: Interprocess Communication: Pipes, Shared Memory, Message Queues, Sockets
- 09Chapter 9: I/O Management and Interrupts
- 10Chapter 10: Deadlocks at the OS Level: Conditions, Prevention, Detection, Recovery
- 11Chapter 11: Virtualization and Containers: A Conceptual Introduction
- 12Chapter 12: When OS Abstractions Leak: Failure Modes Application Engineers Actually Hit
- 13Chapter 13: Walkthrough: What Happens When You Run a Program
- 14Chapter 14: Common OS Interview Questions: Paging, Page Faults, and Deadlock Scenarios
- 15Chapter 15: OS Concepts in System Design and Production Debugging
Chapter 2 of 15
Chapter 2: Processes, Threads, and the Process Lifecycle
Why this matters
"Process" and "thread" are used loosely enough in casual conversation that many engineers can't precisely explain what's actually shared between two threads versus two processes — and that precise answer is exactly what determines whether a bug you're looking at is even possible given the concurrency model in play. (Thread synchronization mechanics — locks, race conditions, and how to avoid them — are covered in full in the companion Concurrency & Multithreading book; this chapter covers the OS-level definitions and lifecycle those mechanics operate on top of.)
The core concept
[BEGINNER] A process is a running instance of a program, with its own isolated memory address space, file descriptors, and OS-level resources. Two processes cannot see each other's memory directly — the OS enforces this isolation.
A thread is a unit of execution within a process. All threads in a process share that process's memory address space, open file descriptors, and most other resources — but each thread has its own stack and its own set of CPU register values (including the program counter), which is what lets each thread execute independently.
Process A (isolated memory space) Process B (separate, isolated memory space)
+----------------------------------+ +----------------------------------+
| Heap, global data (shared by | | Heap, global data (shared by |
| all threads in this process) | | all threads in this process) |
| | | |
| Thread 1: own stack, own regs | | Thread 1: own stack, own regs |
| Thread 2: own stack, own regs | +----------------------------------+
+----------------------------------+
This single distinction — shared memory (threads) versus isolated memory (processes) — is the reason threads are cheaper to create and communicate between, but also why threads can corrupt each other's data if not synchronized, while a bug in one process generally cannot directly corrupt another process's memory at all.
A concrete demonstration in C — creating a process with fork() versus a thread with pthread_create:
#include <stdio.h>
#include <unistd.h>
int global_counter = 0;
int main() {
global_counter = 10;
pid_t pid = fork(); // creates a new PROCESS: a full copy of this process's memory
if (pid == 0) {
// Child process: has its OWN COPY of global_counter, starting at 10.
global_counter = 999;
printf("Child: %d\n", global_counter); // 999
} else {
sleep(1);
printf("Parent: %d\n", global_counter); // still 10 — the child's change is invisible here
}
return 0;
}
#include <stdio.h>
#include <pthread.h>
int global_counter = 0; // SHARED by every thread in this process
void* thread_func(void* arg) {
global_counter = 999; // visible to every other thread in the SAME process immediately
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_func, NULL);
pthread_join(thread, NULL);
printf("Main: %d\n", global_counter); // 999 — the thread's change IS visible here
return 0;
}
The fork() example's child gets its own independent copy of memory (via copy-on-write, covered in Chapter 6) — changes in the child are invisible to the parent. The pthread_create example's new thread shares the exact same memory — changes are immediately visible everywhere in that process. This is the entire practical distinction, made concrete.
The process lifecycle
[BEGINNER] Every process moves through a well-defined set of states, managed by the OS scheduler (Chapter 4):
create
|
v
+--------+ scheduler dispatches +---------+
| NEW |--------------------------> | READY | <-----------------+
+--------+ +---------+ |
| | I/O or event completes
scheduler picks this process |
| |
v |
+-----------+ I/O/event +---------+
| RUNNING |-------------->| WAITING |
+-----------+ requested +---------+
|
process finishes or is killed
|
v
+------------+
| TERMINATED |
+------------+
- New: the process is being created.
- Ready: the process is loaded and waiting for the CPU scheduler to give it a turn.
- Running: the process is actually executing on a CPU core right now.
- Waiting (blocked): the process is waiting for something — disk I/O, a network response, a lock — and cannot proceed until that event occurs, even if given the CPU.
- Terminated: the process has finished (or been killed) and the OS is reclaiming its resources.
[IN PRACTICE] A process bouncing constantly between Running and Waiting (rather than being CPU-bound in Running) is exactly what "I/O-bound" means in practice — and it's directly visible in tools like top, where a process's %CPU staying low despite the process clearly being active tells you it's spending most of its time in the Waiting state, not actually computing.
Context switching: the real cost of "the OS handles it for you"
[SENIOR INSIGHT] Moving a CPU core from running one process/thread to running another — a context switch — isn't free. The OS must save the current thread's complete register state (including the program counter) somewhere, and load the next thread's saved state. For a process switch specifically, there's additional cost: the CPU's memory-address-translation cache (the TLB, covered in Chapter 6) is often invalidated, since the new process has a completely different address space, causing a burst of slow memory lookups until the cache "warms up" again for the new process.
This is why threads are described as "lighter-weight" than processes in practice: switching between threads in the same process doesn't require this address-space change, avoiding the TLB invalidation cost — only the register-state save/restore is needed. This single fact explains a huge amount of real-world architecture advice ("prefer a thread pool over spawning processes for high-frequency parallel work") that otherwise sounds like an arbitrary rule of thumb.
Common mistakes
- Assuming "multi-threaded" and "multi-process" are interchangeable design choices with no meaningful trade-off — they differ fundamentally in memory sharing (which enables both easier communication and easier corruption) and in context-switch cost.
- Forgetting that a process's children don't share memory changes with the parent (the
fork()example) — a common source of confusion when engineers expect a forked worker process's in-memory state changes to be visible elsewhere, when in fact each process needs a separate IPC mechanism (Chapter 8) to communicate. - Reading "the CPU usage is low" as "the program isn't doing meaningful work," when it might simply mean the process is I/O-bound (mostly in the Waiting state) rather than idle — these look identical on a naive CPU-usage graph but have very different implications for diagnosis.
- Not accounting for context-switch overhead when designing highly concurrent systems — spawning far more OS threads/processes than CPU cores can actually run simultaneously causes the system to spend an increasing fraction of its time context-switching rather than doing useful work.
Checklist: reasoning correctly about processes and threads
- Before choosing processes vs. threads for a concurrent design, state explicitly whether shared memory is wanted (threads) or isolation is wanted (processes, for fault containment or security boundaries).
- Remember a forked child process's memory changes are invisible to the parent — plan for explicit IPC (Chapter 8) if communication is needed.
- When a process looks "stuck" with low CPU usage, check whether it's actually in the Waiting state (blocked on I/O) rather than assuming it's idle or hung.
- Account for context-switch cost when deciding how many concurrent threads/processes to run — more than the available CPU cores can execute at once trades throughput for scheduling overhead past a certain point.
- Use
top/ps//proc/<pid>/statusto directly observe a process's actual state rather than inferring it indirectly from application-level symptoms alone.
Discussion / interview questions
-
"What's the fundamental difference between a process and a thread?" Strong answer: A process has its own isolated memory address space; threads within a process share that address space (heap, globals, file descriptors) but each has its own stack and register state — this is what makes threads cheaper to create/switch between but also able to directly corrupt each other's data without synchronization.
-
"Why are threads generally considered 'lighter-weight' than processes for context switching?" Strong answer: Switching between threads in the same process doesn't require an address-space change, avoiding TLB invalidation — only register state needs saving/restoring; switching processes typically does change the address space, incurring that extra cost.
-
"A process shows very low CPU usage in
topbut is clearly still 'doing something.' What does that likely mean?" Strong answer: The process is probably I/O-bound — spending most of its time in the Waiting state (blocked on disk, network, or another event) rather than the Running state, so it's not consuming CPU even though it's actively progressing through work. -
"After calling
fork(), why doesn't a child process's change to a global variable appear in the parent?" Strong answer:fork()gives the child its own independent copy of the parent's memory (implemented efficiently via copy-on-write, Chapter 6) — the two processes have separate address spaces from that point on, so changes in one are invisible to the other without explicit IPC.
Chapter summary / cheat sheet
- Process: isolated memory address space; Thread: a unit of execution within a process, sharing that process's memory but with its own stack and register state.
- Process lifecycle: New → Ready → Running → Waiting (blocked, cycles back to Ready) → Terminated, managed by the OS scheduler.
- Low CPU usage doesn't mean idle — it often means I/O-bound (mostly in the Waiting state).
- Context switching has real cost, higher for processes (address-space/TLB change) than threads (register state only) — this is the concrete reason "prefer threads for lightweight concurrency" is common advice.
fork()'s child gets an independent memory copy; threads created viapthread_createshare memory directly and immediately.