SDE Path

Processes, Threads, and Context Switching

13 min read

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:

  1. New — being created.
  2. Ready — waiting for a CPU; sitting in the ready queue.
  3. Running — currently executing on a core.
  4. Waiting/Blocked — parked until an event completes (I/O, a lock, a signal).
  5. 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:

  1. A trap or interrupt transfers control to the kernel.
  2. The kernel saves the outgoing context (registers, program counter, stack pointer) into its PCB/TCB.
  3. The scheduler picks the next runnable entity.
  4. The kernel restores that entity's saved context.
  5. 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.

Check yourself

5 questions — graded when you submit.

Question 1

Which resources are private to each thread rather than shared across threads of the same process?

Question 2

A process is currently blocked waiting on a disk read. A CPU core becomes free. What happens?

Question 3

Why is a context switch between two threads of the same process typically cheaper than a switch between two different processes?

Question 4

A scheduler uses a 200 µs quantum and each context switch costs 8 µs. Roughly what fraction of CPU time is spent on context-switch overhead?

Question 5

What does fork() return, and how does copy-on-write make it efficient?

Finished reading?

Sign in to save your progress across lessons.