Appearance
2.2 — Processes
2.1 kept saying the kernel "runs programs" and the scheduler "juggles them" — but what, precisely, is a running program? The answer is the process, and it is one of the two or three most important abstractions the operating system provides. Get it exactly right and a dozen everyday realities stop being magic: why closing a terminal kills the command inside it, why Ctrl+C stops a program but sometimes doesn't, what "graceful shutdown" actually means, why your web server can handle a thousand connections, and how a single node app.js becomes a living thing with its own memory that no other program can see. This chapter defines the process from the ground up, shows the strange and beautiful way Unix creates one (fork), and ends with signals — the OS's way of tapping a process on the shoulder — which is exactly the machinery behind graceful shutdown that every backend engineer is expected to know.
1. Program vs process: the noun and the verb
Start with a distinction that sounds pedantic and turns out to be the whole foundation. A program is a passive thing: a file on disk — node, chrome, /usr/bin/ls — a lump of machine-code instructions (1.5) and data, sitting there doing nothing, like a recipe in a closed book. A process is a program in motion: an actual running instance, with a life of its own. The recipe is the program; the act of a cook actually making the dish — with a specific bowl, ingredients half-chopped, at step 4 right now — is the process.
That "specific bowl, at step 4 right now" is the key insight: a process is not just the code, it's the code plus all the live state of executing it. Concretely, the kernel gives every process:
- Its own private memory — an address space no other process can see or touch (the illusion of owning the whole machine's memory, built by virtual memory in Chapter 2.5).
- The CPU state at this instant — the register values, and the program counter (1.5) pointing at the exact next instruction ("step 4 right now").
- A set of open resources — files it has opened, network connections, etc., tracked as file descriptors — the small integer "tickets" the kernel hands out for open things, defined in 2.1.
- Identity and relationships — a unique process ID (PID), the ID of its parent, its owner (which user), and its priority.
Run the same program twice and you get two processes — two independent running instances, each with its own memory and state, that know nothing of each other, exactly as two cooks making the same recipe have their own separate bowls. This is why "the program" and "a process" must be different words: one recipe, many independent cookings.
The shape of a process's memory
That private address space isn't an undifferentiated blob; it has a standard four-part layout, and knowing it explains a surprising number of bugs and behaviours:
- The text segment holds the program's machine-code instructions (read-only, so a bug can't overwrite the code).
- The data segment holds global and static variables — things that exist for the whole program's life.
- The heap is the pool for dynamic memory: whatever you allocate at runtime with
malloc(C),new(C++/Java), or that a language's runtime allocates for your objects. It grows upward as you allocate more. - The stack holds the call chain: every time a function is called, a stack frame with its local variables and return address is pushed; every return pops one. It grows downward. (This is the same stack that made pushdown automata able to match nesting in 1.7, and the reason infinite recursion crashes with a "stack overflow" — the stack grows down until it collides with the heap.)
2. The Process Control Block: how the kernel remembers a process
The kernel manages hundreds of processes at once, so for each one it keeps a record — a data structure called the Process Control Block (PCB; in Linux, task_struct). Think of it as the process's complete file in the kernel's filing cabinet. It holds everything the kernel needs to manage and, crucially, to pause and resume the process: its PID and parent PID, its owner and priority, a pointer to its memory map, its list of open files, and — the part that makes multitasking possible — a saved copy of its CPU register state (including the program counter) for when it isn't currently running.
Every process is always in one of a few process states, and the kernel moves it between them:
- Ready: able to run, just waiting for a free CPU.
- Running: currently executing on a CPU (at most one process per core at any instant).
- Blocked (or waiting): can't proceed until some event completes — usually I/O ("I asked the disk for data; wake me when it arrives"). A blocked process uses no CPU. This state is the entire reason a computer feels fast despite slow disks: while one process waits on I/O, the CPU runs others.
- Terminated: finished, awaiting cleanup.
3. The context switch: the sleight of hand behind multitasking
You have far more processes than CPU cores, yet they all appear to run at once. The trick is speed: the CPU runs process A for a few milliseconds, then rapidly switches to B, then C, then back to A — dozens of times a second, so fast that to human perception everything is simultaneous, the way 24 still frames a second become a movie. That swap is a context switch, and it's a direct payoff of everything above.
To switch from process A to B, the kernel: (1) saves A's current CPU registers and program counter into A's PCB — freezing A at exactly the instruction it was about to run; (2) marks A as Ready (or Blocked); (3) picks B (the scheduler's job, Chapter 2.3); (4) loads B's registers and program counter from B's PCB — restoring B to exactly where it was frozen; (5) returns to user mode, and B resumes as if it had never been paused. The saved-and-restored register set is the "context," and the PCB is where it lives. This is the concrete reason the PCB stores CPU state: without a frozen snapshot to restore, you could never pause and resume a process.
A context switch isn't free — it's overhead where the CPU does no useful application work, just bookkeeping, and worse, the new process's data usually isn't in the CPU cache (1.6), so it runs slowly at first while the caches refill. This cost is why "just spawn 10,000 processes/threads" is a bad plan (all that switching becomes pure overhead — thrashing), and it's a major reason the asynchronous, event-loop model of Node.js (Part 3) exists: handle thousands of connections in one process to avoid paying for thousands of context switches.
4. Creating a process: fork, the beautiful oddity
Here's a puzzle. Every process has a parent — so where does a new process come from? Unix's answer, from 1970, is genuinely strange the first time you meet it, and then you see its elegance. There is no "create a process from this program" call. Instead there are two separate operations, and you combine them.
The first is fork(). When a process calls fork(), the kernel makes a near-exact copy of the calling process — same code, same memory contents, same open files, same everything — producing a second, nearly-identical process. The caller is the parent; the copy is the child. They are now two independent processes with separate memory; changing a variable in one does not affect the other. It's cell division: one process becomes two.
The famously mind-bending part is the return value. fork() is called once but returns twice — once in each process — and the return value is how each figures out who it is: in the parent, fork() returns the child's PID (a positive number); in the child, it returns 0. So the standard idiom:
c
pid_t pid = fork();
if (pid == 0) {
// we are the CHILD — fork returned 0 here
} else if (pid > 0) {
// we are the PARENT — fork returned the child's PID here
} else {
// fork failed (returned -1)
}If you haven't read C before, that snippet is simpler than it looks: pid_t pid = fork(); means "call fork() and store whatever number it returns in a variable named pid" (pid_t is just the name of the type used for process IDs — read it as "a number"). The if / else if / else then checks which number came back: zero means "I am the child," a positive number means "I am the parent, and this is my child's ID," and −1 means the call failed. The lines beginning // are comments — notes for humans, ignored by the machine.
One line of code, executing in two universes that diverge on its result. It feels like science fiction and it's the bedrock of Unix.
But a copy of yourself isn't very useful — you usually want to run a different program. That's the second operation: exec() (the execve family). exec() replaces the current process's entire memory image — code, data, heap, stack — with a new program loaded from disk, and starts executing it from its beginning. Same process (same PID, same parent, same open files), completely new program running inside it. The recipe changes while the cook stays the same.
Put them together and you have the fork-exec pattern, which is how every new program on a Unix system is launched — including every command you type in a shell:
ls, the shell forks a copy of itself, and the child immediately execs /bin/ls, becoming the new program. The parent waits for it to finish. This is how every command runs.When you type ls in your shell: the shell fork()s a child (a clone of the shell), the child exec()s /bin/ls (becoming ls), and the parent shell wait()s for the child to finish before showing you the next prompt. The wait() call is important — it lets the parent collect the child's exit status and tells the kernel to clean up the finished child.
Two loose ends this creates, both real interview fodder. A zombie process is a child that has finished but whose parent hasn't wait()ed for it yet: it's dead but its PCB entry lingers (holding its exit status) until reaped — harmless in small numbers, a resource leak if a buggy parent never reaps. An orphan is a child whose parent died first; the kernel re-parents it to init (PID 1, from 2.1), whose standing duty is to wait() for such waifs and clean them up. This is one reason PID 1 matters, and why in a container (Chapter 2.9) a poorly-chosen PID 1 that doesn't reap can leak zombies.
5. Signals: tapping a process on the shoulder
A running process is busy in its own world — so how does the outside (you, the kernel, another process) get its attention asynchronously, without its cooperation? Through signals: small, predefined, numbered notifications the kernel delivers to a process, interrupting its normal flow to say "something happened." A signal is the OS-level equivalent of a tap on the shoulder — or in some cases, a bullet. This is where the interview-critical knowledge lives.
When a signal arrives, the process can (for most signals) have installed a signal handler — a function the kernel runs immediately, out of band, letting the process react (clean up, save state, refuse) — or let the default action happen (usually: terminate). The signals you must know:
- SIGINT (signal 2) — "interrupt." What Ctrl+C sends. It politely asks the foreground program to stop. A program can catch it (e.g. to ask "really quit?") or ignore it. ⚑What is SIGINT? [EQ-90]
- SIGTERM (signal 15) — "terminate." The polite, catchable request to shut down — the default signal
killsends. This is the one that matters most in production: it means "please finish up and exit cleanly." A well-behaved program catches SIGTERM and does a graceful shutdown (below). ⚑What is SIGTERM? [EQ-91] - SIGKILL (signal 9) — "kill." The un-ignorable, un-catchable sledgehammer. The kernel terminates the process immediately, with no chance to react, clean up, or save.
kill -9is this. Because it can't be caught, a process killed this way can leave corrupt files, orphaned locks, or half-written data — which is exactly why you send SIGTERM first and resort to SIGKILL only if the process ignores it. - SIGSEGV (signal 11) — "segmentation violation." Sent by the kernel when a process touches memory it doesn't own (the segfault from 2.1). Default action: terminate (often with a core dump).
- SIGHUP (signal 1) — "hang up." Originally "the terminal line dropped"; now conventionally repurposed to mean "reload your configuration." (This is why closing a terminal can kill the programs you launched from it — they receive SIGHUP.)
Graceful shutdown — the reason this chapter matters to backend engineers
Here is the whole point, and it's a standard interview question at every level. When you deploy a new version of a server, or a container orchestrator like Kubernetes scales your service down, the system doesn't just yank the process away — that would drop in-flight requests, corrupt data, and leave connections hanging. Instead it sends SIGTERM and waits (typically ~30 seconds). A properly built server catches SIGTERM and performs a graceful shutdown: stop accepting new requests, finish serving the in-flight ones, flush and close database connections and files, then exit cleanly — all before the grace period ends. Only if the process is still alive after the timeout does the orchestrator escalate to the un-catchable SIGKILL. ⚑How do you gracefully shut down an application, and which signals are involved? [EQ-88]⚑What signals can a process listen to? [EQ-89] So "handle SIGTERM for graceful shutdown" is not trivia — it's the difference between a deploy that silently drops user requests and one that's seamless. (The application-code version of this — how you actually wire it in a Node/Express server — lives in Part 9.9; here you own the OS foundation it stands on.)
6. The expert lens
fork is cheap because it lies — copy-on-write. A naïve fork() would copy the parent's entire memory (possibly gigabytes) — ruinously slow, and usually wasteful because the child often calls exec() a microsecond later and throws that copy away. So real kernels use copy-on-write (COW): after fork(), parent and child share the same physical memory pages, marked read-only; the copy of any page is made only if and when one of them tries to write it. If the child immediately execs, almost nothing is ever copied. This is a gorgeous, recurring systems trick — "don't do expensive work until someone actually forces you to" — and you'll meet it again in snapshots, virtual machines, and copy-on-write filesystems (Chapter 2.6).
fork and threads don't mix well — a classic production trap. fork() copies only the calling thread, not the others, but it copies all their memory — including any locks they held, now frozen forever in the locked state with no thread alive to release them. A child that then touches such a lock deadlocks instantly. This is why forking from a multithreaded program is famously dangerous and why the safe pattern is "fork then immediately exec, touching nothing in between." It also underlies why many modern runtimes and servers prefer threads or pre-forking a pool of worker processes before spinning up threads, never mixing the two carelessly.
Signal handlers run in a minefield — async-signal-safety. A signal handler interrupts the program at a completely arbitrary instruction — possibly in the middle of malloc, or while a lock is held. If the handler then calls malloc or printf (which may take that same lock, or use shared state mid-mutation), it can deadlock or corrupt memory. So handlers must only call async-signal-safe functions and, in practice, should do almost nothing — the standard idiom is to just set a flag (a volatile sig_atomic_t) and let the main loop notice it and do the real work safely. This is a subtle correctness trap that separates engineers who've been burned from those who haven't.
The process is the unit of isolation and the unit of failure. Because each process has its own private address space, a crash is contained to one process (2.1) — which is a design tool, not just a safety net. It's why browsers put each tab (or site) in its own process (one hung page doesn't freeze the browser, and a compromised tab can't read another's memory), why databases and web servers isolate work into worker processes, and why "just restart the crashed process" is a viable reliability strategy (Erlang's "let it crash" philosophy, and the supervisor patterns in Part 10). The trade-off is the context-switch and memory cost of many processes — which is exactly the tension that motivates threads (next chapter) and async I/O.
Next chapter: processes are heavyweight — each with its own full memory. But often you want concurrency within one program sharing the same memory, without the cost of separate processes. That's the thread, and deciding which thread runs when is the scheduler's job. Chapter 2.3 takes both on.
Recall
- A program is a passive file on disk; a process is a running instance of it, with its own private memory (text/data/heap/stack), CPU state (registers + program counter), open files, and a unique PID. Same program run twice = two independent processes.
- The kernel tracks each process in a Process Control Block (PCB), including a saved register set so the process can be paused and resumed. Processes cycle through Ready / Running / Blocked / Terminated.
- A context switch saves the running process's registers to its PCB and restores another's — the sleight of hand behind multitasking. It has real overhead (bookkeeping + cold caches), so too many processes/threads = thrashing.
- New processes come from fork() (clone the caller; returns child's PID to the parent and 0 to the child) then exec() (replace the image with a new program). fork-exec launches every command; copy-on-write makes fork cheap. Unreaped finished children are zombies; parentless children are orphans re-parented to init.
- Signals are async notifications: SIGINT (Ctrl+C), SIGTERM (polite, catchable "shut down" — catch it for graceful shutdown), SIGKILL (un-catchable sledgehammer), SIGSEGV (bad memory access). Production shutdown = SIGTERM → finish in-flight work → exit, with SIGKILL only as the timeout fallback.
Self-test: What exactly distinguishes a program from a process? What does a context switch save and restore, and where? Why does fork() return twice, and how does each side tell itself apart? What is graceful shutdown, and which two signals are involved? Why is kill -9 a last resort?
Quiz Bank
FoundationalWhat is the difference between a program and a process?
A program is a passive entity: an executable file on disk (code + data), doing nothing on its own. A process is an active, running instance of a program, with its own private memory address space (text, data, heap, stack), its current CPU register state and program counter, its open files, and a unique PID. Running the same program twice creates two separate, independent processes that share nothing. The program is the recipe; the process is the act of cooking it.
FoundationalWhat are the four regions of a process's memory?
Text (code) — the machine instructions, read-only. Data — global and static variables. Heap — dynamically allocated memory (malloc/new), growing upward as you allocate. Stack — function-call frames with local variables and return addresses, growing downward with each call and shrinking on return. The heap and stack grow toward each other; colliding gives out-of-memory or stack overflow (e.g. from infinite recursion).
AppliedWhat is a context switch, and what does it cost?
A context switch is the kernel swapping the CPU from one process (or thread) to another: it saves the current one's registers and program counter into its PCB, selects the next (scheduler), and loads that one's saved state, so it resumes exactly where it left off. Costs: the switch itself is pure overhead (no application work), and worse, the incoming process's data is usually not in the CPU cache, so it runs slowly until caches refill (cache pollution). Excessive switching (too many runnable processes/threads) is thrashing — which motivates async/event-loop designs that serve many connections in one process.
AppliedExplain fork() and exec(), and how they combine to run a command.
fork() creates a near-identical copy of the calling process (a child with its own memory); it returns twice — the child's PID to the parent, and 0 to the child, so each knows its role. exec() replaces the current process's memory image with a new program from disk, keeping the same PID. Combined (fork-exec): to run ls, the shell forks a child clone of itself, the child execs /bin/ls (becoming that program), and the parent wait()s for it to finish and reaps it. This is how every command and program launch works on Unix.
InterviewWhat is a zombie process and what is an orphan process?
A zombie is a child process that has terminated but whose parent hasn't yet called wait() to collect its exit status; the kernel keeps its PCB entry (just the exit info) until reaped. Many accumulating zombies indicate a buggy parent that never reaps — a resource leak (they consume PID-table slots). An orphan is a process whose parent terminated first; the kernel re-parents it to init (PID 1), which routinely wait()s to clean up such children. Both stem from the parent/child reaping contract of fork-exec.
InterviewHow does graceful shutdown work, and which signals are involved?
When a system wants to stop a process (deploy, scale-down), it sends SIGTERM — a catchable "please shut down" — and waits a grace period (~30s). A well-built process catches SIGTERM and does a graceful shutdown: stop accepting new work, finish in-flight requests, flush/close DB connections and files, then exit. Only if it's still alive after the timeout does the system send SIGKILL (signal 9), which is un-catchable and terminates it immediately with no cleanup. Relying on SIGKILL risks dropped requests and corrupt state, so handling SIGTERM is essential for zero-downtime deploys. (SIGINT / Ctrl+C is the interactive cousin of SIGTERM.)
StaffWhy is calling fork() in a multithreaded program dangerous, and what's the safe pattern?
fork() duplicates only the calling thread but copies all memory, including mutexes/locks that other threads were holding at the fork instant — now frozen in the locked state with no thread in the child to unlock them. If the child then calls any function that tries to acquire such a lock (e.g. malloc, which locks internally), it deadlocks. Similarly, data structures another thread was mid-mutation on are left inconsistent in the child. The safe pattern is fork() then immediately exec(), doing nothing in between that isn't async-signal-safe — exec replaces the whole image, discarding the inherited locks/state. This is why runtimes avoid fork-without-exec in threaded contexts, favor pre-forking worker processes before creating threads, or use posix_spawn. It's a classic source of rare, brutal production hangs.
Flashcards
FlashProgram vs process
Program = passive file on disk. Process = running instance with its own memory, registers, PID, and open files.
FlashFour segments of process memory
Text (code), Data (globals), Heap (dynamic, grows up), Stack (calls/locals, grows down).
FlashWhat a context switch saves/restores
The CPU register set + program counter, saved to/restored from the PCB — so a process resumes exactly where it was paused.
Flashfork() return values
Returns twice: the child's PID in the parent, 0 in the child, -1 on failure.
Flashfork vs exec
fork = clone the calling process. exec = replace the current process's image with a new program (same PID).
FlashSIGTERM vs SIGKILL
SIGTERM (15): catchable "please shut down" — used for graceful shutdown. SIGKILL (9): un-catchable, immediate termination, no cleanup.
FlashCopy-on-write in fork
Parent and child share physical pages read-only after fork; a page is copied only when one writes it — making fork cheap, especially before exec.
Scenario Drill
DrillYour containerized Node service drops a handful of user requests every time you deploy a new version. Nothing in the app logs looks wrong. Using this chapter, explain the likely cause and the fix.
The cause is almost certainly missing graceful-shutdown handling. On deploy, the orchestrator (e.g. Kubernetes) sends the container's main process SIGTERM and waits a grace period before SIGKILL. If the app doesn't catch SIGTERM, the default action terminates it immediately — cutting off any requests currently being served and closing sockets abruptly, so in-flight users get dropped connections (and the logs look clean because the process died before it could log an error). The fix is to install a SIGTERM handler that performs a graceful shutdown: (1) stop the server from accepting new connections; (2) let in-flight requests finish (with a timeout); (3) close database/queue connections and flush buffers; (4) then exit with success. Also ensure the process is actually PID 1 or that signals are correctly forwarded to it (a subtle container gotcha — if the app runs as a child of a shell that is PID 1, SIGTERM may hit the shell, not the app, so use exec-form entrypoints or an init like tini that forwards signals and reaps zombies).
Verify the grace period is long enough for your slowest request. The OS-level truth to state in an interview: deploys don't kill instantly — they ask via SIGTERM first, and your job is to answer that request cleanly before the SIGKILL fallback fires. (Wiring the handler in Express is Part 9.9.)