A program is a passive file sitting on disk. A process is that program in motion: an executing instance with its own memory, registers, and operating-system bookkeeping. Understanding the boundary between a process and a thread, and what it costs the CPU to switch between them, is the foundation for every other operating-systems topic interviewers probe.
What a process actually contains
When the OS loads a program, it carves out an address space and fills a kernel structure called the Process Control Block (PCB). The address space is divided into segments:
- Text (code): the compiled instructions, usually read-only so multiple processes can share one copy.
- Data: initialized global and static variables.
- BSS: uninitialized globals, zero-filled at load time.
- Heap: dynamically allocated memory (
malloc,new) that grows upward. - Stack: function call frames, local variables, and return addresses that grow downward.
The PCB holds the metadata the kernel needs to manage the process: PID, process state, the program counter, CPU register snapshot, memory-management info (page tables), a list of open files, scheduling priority, and accounting data. When the OS suspends a process, the PCB is where its live CPU state is saved.
Process states
A process moves through a small state machine:
- New — being created.
- Ready — waiting for a CPU; sitting in the ready queue.
- Running — currently executing on a core.
- Waiting/Blocked — parked until an event completes (I/O, a lock, a signal).
- Terminated — finished; PCB lingers briefly as a zombie until the parent reaps its exit code with
wait().
A key distinction interviewers love: ready means "willing and able, just needs a CPU," while blocked means "cannot proceed even if a CPU is free." A blocked process never jumps straight to running; it must pass back through ready.
Threads: concurrency inside one address space
A thread is a single sequential flow of control within a process. Every thread has its own stack, program counter, and register set, but all threads in a process share the text, data, heap, and open file descriptors.
| Attribute | Per process | Per thread | |-----------|-------------|------------| | Address space (code, data, heap) | Shared by its threads | Shared | | Stack | One per thread | Private | | Registers / PC | Saved per thread | Private | | Open files, PID | Owned by process | Shared | | Creation cost | High | Low | | Communication | IPC (pipes, shared memory, sockets) | Shared memory directly |
Because threads share the heap, they can communicate by simply reading and writing the same variables — which is exactly why they need synchronization (locks, semaphores) to avoid race conditions. Processes are isolated by default: a bug in one cannot corrupt another's memory, which is safer but slower to coordinate.
User-level vs kernel-level threads
- Kernel-level threads are scheduled directly by the OS; a blocking system call in one thread does not freeze the others, and threads can run truly in parallel on multiple cores.
- User-level threads are managed by a library in user space; they are cheap to create but the kernel sees only one schedulable entity, so one blocking call can stall every thread and they cannot exploit multiple cores by themselves.
- Mapping models: many-to-one (all user threads on one kernel thread), one-to-one (each user thread backed by a kernel thread — the common modern model), and many-to-many (a pool of user threads multiplexed onto a smaller pool of kernel threads).
Context switching: the price of multitasking
A context switch is the act of saving the state of the currently running execution and restoring the state of another so the CPU can run something else. It happens on a timer interrupt (the quantum expired), when a process blocks on I/O, on a system call, or when a higher-priority task becomes ready.
The mechanical steps:
- A trap or interrupt transfers control to the kernel.
- The kernel saves the outgoing context (registers, program counter, stack pointer) into its PCB/TCB.
- The scheduler picks the next runnable entity.
- The kernel restores that entity's saved context.
- Execution resumes in the new context, often in user mode.
// Conceptual switch — real kernels do this in assembly
save_registers(&outgoing->pcb);
switch_address_space(incoming->page_table); // process switch only
restore_registers(&incoming->pcb);
return_to_user_mode();
Why switches are expensive
The direct cost is saving and restoring registers — a few microseconds. The indirect cost usually dominates: switching to a different process reloads the page-table base register and often flushes the TLB (translation lookaside buffer), so subsequent memory accesses miss and must walk page tables. Cache locality is also lost — the new task's data is not in L1/L2, so it suffers cache misses until its working set warms up.
This is why thread switches within one process are cheaper than process switches: threads share the address space, so the TLB and page tables do not need to be flushed. Only the private registers and stack pointer change.
A worked estimate
Suppose a context switch costs about 5 microseconds and your scheduler uses a 10 millisecond time quantum. The overhead ratio is 5 µs / 10 ms = 0.05% — negligible. Shrink the quantum to 100 µs for snappier interactivity and the ratio jumps to 5 / 100 = 5% of CPU time burned purely on switching. This is the core tension in scheduler design: small quanta improve responsiveness but waste CPU on overhead.
Process creation: fork and exec
On Unix, fork() creates a child that is a near-exact copy of the parent; it returns 0 in the child and the child's PID in the parent. Modern kernels use copy-on-write: parent and child initially share physical pages marked read-only, and a page is duplicated only when one side writes to it — so fork is cheap even for large processes. exec() then replaces the child's image with a new program. The classic shell pattern is fork, then exec in the child while the parent wait()s.
Common follow-ups and gotchas
- "Is a context switch the same as a mode switch?" No. A mode switch (user↔kernel, e.g., during a system call) does not necessarily change which process runs. A context switch changes the running task and always involves scheduler work.
- Zombie vs orphan: a zombie has terminated but its parent has not reaped it (PCB retained for the exit status); an orphan's parent died first and it gets re-parented to
init/systemd. - "Threads are always faster." Not for CPU-bound work under a Global Interpreter Lock, and not when false sharing or lock contention serializes them. Threads win for I/O-bound concurrency and cheap communication, not automatically for raw throughput.
- Thread safety: shared mutable state is the danger. Isolation (separate processes) trades performance for robustness — a crash in one process leaves the others standing.