Processes and Threads
Process: The Fundamental Unit of Resource Allocation
A process is a running instance of a program. A program is a static file of code residing on disk, whereas a process is the dynamic execution of that code.
To use an analogy: a program is like a recipe (stored on a shelf), while a process is the actual act of cooking the meal according to that recipe (running in the kitchen). The same recipe can be used simultaneously by multiple chefs, just as the same program can instantiate multiple concurrent processes.
Anatomy of a Process
┌─────────────────────────────┐
│ Process │
│ ┌───────────────────────┐ │
│ │ Text Segment (Code) │ │ ← Executable instructions (Read-only, Shared)
│ ├───────────────────────┤ │
│ │ Data Segment (+ BSS) │ │ ← Global & Static variables
│ ├───────────────────────┤ │
│ │ Heap ↓ │ │ ← Dynamically allocated memory (malloc/new)
│ │ │ │
│ │ Free Space │ │
│ │ │ │
│ │ Stack ↑ │ │ ← Call stack (Local variables, Return addresses)
│ ├───────────────────────┤ │
│ │ Kernel Stack │ │ ← Utilized during system calls
│ └───────────────────────┘ │
│ │
│ PCB (Process Control Block)│ ← Process metadata (Managed by OS)
│ - PID, State, Priority │
│ - Program Counter (PC) │
│ - Register Sets │
│ - Memory Map Information │
│ - Open File Descriptors │
│ - Signal Handlers │
└─────────────────────────────┘
The Process Lifecycle
fork()
┌──────┐ ┌────────┐ Scheduled ┌────────┐
│ New │───▶│ Ready │────────────▶│ Running│
└──────┘ └────────┘ └────────┘
▲ │ │
│ │ │
I/O Done Time Slice│ │ Wait for I/O
or Event Preempted │ │ or Event
│ │ │
│ ┌─────┘ │
│ ▼ ▼
┌────────┐ Back to Ready ┌────────┐
│ Blocked│◀──────────────│ Blocked│
└────────┘ └────────┘
│
exit() │
▼
┌────────┐
│ Term │
└────────┘
Linux Process States:
| State | Flag | Description |
|---|---|---|
| Running/Ready | R (Running) |
Actively executing on CPU, or queued in the runqueue. |
| Interruptible Sleep | S (Sleeping) |
Waiting for an event (e.g., I/O). Can be interrupted by signals. |
| Uninterruptible Sleep | D (Disk sleep) |
Waiting for critical I/O (usually disk). Cannot be killed or interrupted. |
| Stopped | T (Stopped) |
Suspended by a SIGSTOP signal or debugger. |
| Zombie | Z (Zombie) |
Terminated, but the parent has not yet reaped its exit status. |
Zombies and Orphans
Zombie Process: The child process has terminated, but the parent process has not invoked wait() to reap its exit status. The child's PCB continues to occupy a slot in the system's process table. A flood of zombies will eventually exhaust the system's PID pool.
Orphan Process: The parent process terminates before the child. The child is subsequently "adopted" by the init process (PID 1). Orphan processes are generally harmless because init is designed to systematically reap them upon termination.
Thread: The Fundamental Unit of CPU Scheduling
A thread is a single sequence of execution embedded within a process. A single process can encapsulate multiple threads. These threads share the process's address space and resources (Text, Data, Heap, File Descriptors), but each thread maintains its own independent Stack and Registers.
┌──────────────── Process ───────────────┐
│ │
│ ┌──────────────────────────────┐ │ ← Shared Area
│ │ Text │ Data │ Heap │ FDs │ │
│ └──────────────────────────────┘ │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │ ← Thread Private
│ │Thread 1│ │Thread 2│ │Thread 3│ │
│ │ ┌────┐ │ │ ┌────┐ │ │ ┌────┐ │ │
│ │ │Stack │ │ │Stack │ │ │Stack │ │ │
│ │ ├────┤ │ │ ├────┤ │ │ ├────┤ │ │
│ │ │ Regs │ │ │ Regs │ │ │ Regs │ │ │
│ │ ├────┤ │ │ ├────┤ │ │ ├────┤ │ │
│ │ │ PC │ │ │ │ PC │ │ │ │ PC │ │ │
│ │ └────┘ │ │ └────┘ │ │ └────┘ │ │
│ └────────┘ └────────┘ └────────┘ │
└──────────────────────────────────────┘
Process vs. Thread
| Dimension | Process | Thread |
|---|---|---|
| Definition | Unit of resource allocation | Unit of CPU scheduling |
| Address Space | Independent virtual address space | Shared process address space |
| Communication | IPC (Pipes, Shared Memory, etc.) | Direct read/write of shared variables |
| Creation Cost | High (Requires address space duplication) | Low (Shares most resources) |
| Switch Cost | High (Page table switch, TLB flush) | Low (No page table switch required) |
| Crash Impact | Isolated; does not affect other processes | A crash in one thread terminates the entire process |
| Concurrency | Inherently isolated | Requires synchronization mechanisms (Locks/Mutexes) |
Thread Implementation in Linux
Architecturally, Linux does not possess a distinct "thread" abstraction within the kernel. To the Linux kernel, a thread is merely a process that shares an address space with other processes. The pthread_create() function is simply a wrapper around the clone() system call, using bitmask flags to explicitly define which resources are shared:
fork() → clone(Share NOTHING) → New Process
pthread_create() → clone(Share Mem, FDs, etc.) → New Thread
Context Switching
When the CPU transitions execution from one process/thread to another, it must persist the current execution state (the "context") and reinstate the target's state.
Process Context Switch
Current Process A Target Process B
│ │
│ 1. Save A's registers to A's PCB │
│ 2. Save A's stack pointer │
│ 3. Switch Page Tables (CR3 Register) │
│ 4. Flush TLB (Address Translation Cache)│
│ 5. Restore B's registers from B's PCB │
│ 6. Restore B's stack pointer │
│ │
Suspend ─────────────────────────────────▶ Resume
Thread Context Switch
Thread switching within the same process does not require switching page tables or flushing the TLB (because the address space is identical). Consequently, thread context switching is orders of magnitude faster than process switching.
| Switch Type | Required Save/Restore | Approximate Overhead |
|---|---|---|
| Process Switch | Registers + Page Tables + TLB Flush + Cache Misses | Thousands of CPU Cycles |
| Thread Switch (Same Process) | Registers + Stack Pointer | Hundreds of CPU Cycles |
Coroutines (Brief Overview)
Coroutines are user-space lightweight threads, scheduled by the application runtime rather than the OS kernel.
| Comparison | Thread | Coroutine |
|---|---|---|
| Scheduler | OS Kernel | User-space Runtime |
| Switch Cost | Requires kernel-mode transition | Minimal; only saves/restores a few registers |
| Concurrency Model | Preemptive (OS can interrupt anytime) | Cooperative (Explicitly yields CPU) |
| Stack Size | Typically 1~8MB | Typically 2~8KB |
| Representatives | POSIX threads (pthreads) | Go goroutines, Kotlin coroutines |
System Design Audit & Observability
When engineering highly concurrent systems, process and thread mechanics dictate architectural boundaries.
1. The Thread Pool Sizing Dilemma
Blindly spawning threads leads to catastrophic context-switch overhead.
- Audit Protocol: For CPU-bound workloads, thread count should ideally equal
N_cores. For I/O-bound workloads, calculate threads using Little's Law or empirical tuning. If a JVM or Node instance is thrashing with 10,000 threads, the architecture must pivot to async I/O or Coroutines (e.g., Kotlin Coroutines, Project Loom).
2. Hunting Zombies and D State Processes
In production, a high load average without matching CPU utilization often points to processes stuck in Uninterruptible Sleep (D state), usually due to failing NFS mounts or dying disks.
- Audit Command:
ps aux | awk '$8=="D"'to detect stuck I/O. Useps aux | awk '$8=="Z"'to hunt Zombies. If a parent process is leaking Zombies, its signal handling (specificallySIGCHLD) is fundamentally flawed and must be rewritten.
3. The Shared Memory Death Trap
Because threads share the heap and data segments, a single OutOfMemoryError (OOM) or a Segmentation Fault (SIGSEGV) in one thread will violently terminate the entire process, taking all other healthy threads down with it.
- Audit Protocol: If an application requires strict fault isolation (e.g., Chrome rendering tabs), multi-processing is mandatory. If the application prioritizes zero-copy data sharing and minimal memory footprint, multi-threading is optimal, provided rigorous bounds-checking and memory safety practices are enforced.