3.1
Processes and Threads
What a process owns, what threads share, and what each costs to create.
55 min Reading + animations
Tip: tap or hover any dotted-underlined term to see what it means.
By the end of this lesson you will be able to
- Explain what an operating system and its kernel do, why programs run in user mode, and what a system call costs.
- Say exactly what a process owns (address space, PID, file descriptors, security context, threads) and how the kernel tracks it in a process control block.
- List what threads share and what each thread keeps for itself, and explain why stacks are private but not protected.
- Describe
fork,execandwait, how Linux creates threads withclone, what each costs, and whyforkin a threaded program is dangerous. - Choose an inter-process communication method, from pipes to shared memory to TCP, and name how each one fails.
- Read process states in
ps, and diagnose zombies, orphans, stuck D-state processes and containers that ignore signals. - Write a graceful shutdown with
SIGTERM, and read exit codes such as 137, 139 and 143. - Predict the blast radius of a crash, and compare multi-process and multi-thread servers such as PostgreSQL, MySQL, Nginx and Chrome.
#Where this lesson fits
Lesson 1.2 treated a server as a long-running process, compared process-per-connection, thread-per-connection and event-loop designs in a few lines each, and walked through a rolling deploy. Lesson 1.13 showed how Apache’s MPMs, Nginx’s workers, PHP-FPM and Gunicorn use those designs. Both used the words “process” and “thread” without opening them up. This lesson opens them.
System design depends on this material more than it first appears. Backend servers, databases, web servers, network programs and any high-concurrency application are built from these parts, and most of their limits and failures are limits and failures of the operating system. This chapter covers nine topics from the course notes, across six lessons:
| Topic | Lesson |
|---|---|
| Process vs thread, threads | 3.1, this lesson |
| Concurrency, context switching | 3.2 |
| Thread pools, deadlocks | 3.3 |
| Virtual memory | 3.4 |
| I/O models, epoll | 3.5 |
| Event loop | 3.6 |
We keep to the operating-system view. Scheduling and switching are lesson 3.2, and data races, locks and the memory model in code are Chapter 14.
#What an operating system does
Many programs run on one machine, with one set of disks and network cards. Someone has to share them fairly, stop programs from harming each other, and hide the differences between hardware. That is the operating system. Its core is the kernel: the one program that stays in memory for as long as the machine is on and has full control of the hardware. The rest of the OS (shells, libraries, tools such as ls) are ordinary programs.
| The kernel’s job | What it means for you | Lesson |
|---|---|---|
| Run many programs on few cores | Scheduling, run queues, context switches | 3.2 |
| Give each program its own memory | Virtual addresses, page tables, the OOM killer | 3.4 |
| Present disks as files | File systems, page cache, file descriptors | 3.4, 3.5 |
| Move network data | The TCP/IP stack, sockets, epoll | 1.9, 3.5 |
| Drive hardware | Device drivers, interrupts | |
| Enforce who may do what | Users, permissions, namespaces, cgroups | 3.1, Chapter 4 |
#User mode and kernel mode
The separation is enforced by the CPU, not by convention. Every modern CPU has at least two privilege levels:
- Kernel mode (ring 0 on x86,
EL1on ARM): any instruction, any memory, all hardware. - User mode (ring 3 on x86,
EL0on ARM): ordinary computation only, on the program’s own memory.
If a program in user mode tries to touch a device, change its own memory map or halt the CPU, the hardware refuses and hands control to the kernel, which usually ends the program. So a buggy program can only hurt itself. This is also the line that Chapter 4’s hypervisors extend, and containers share it: every container on a host uses the same kernel.
#System calls
If a program cannot touch the disk or the network, how does it do anything useful? It asks. A system call is a controlled entry into the kernel: the program puts a call number and arguments in registers and runs a special instruction (syscall on x86-64), the CPU switches to kernel mode and jumps to a fixed entry point, the kernel checks everything and does the work, then returns to user mode. Linux has several hundred system calls: open, read, write, socket, accept, fork, mmap and so on. Your language’s File.read or fetch ends up there, usually through the C library.
A system call: crossing into the kernel
Your program runs in user mode and cannot touch hardware. Everything it needs from the outside world, it must ask the kernel for. Step through a fast read, a read that has to wait, and two requests the kernel refuses.
Scenario
The data is already in the kernel’s memory, so the call returns without any disk work.
The file is open as file descriptor 3. The program has no instruction that can talk to a disk, and it would not be allowed to run one anyway.
Why the cost matters. A function call takes around a nanosecond. A cheap system call takes roughly 100 ns to 1 µs, because of the mode switch, the checks and the security mitigations added since Spectre and Meltdown. That is fast, but at millions of calls per second it dominates, which explains a lot of systems design:
- Programs buffer: one
readof 64 KB instead of 64 reads of 1 KB, and logging libraries batch their writes. - Common read-only calls such as
clock_gettimeare served in user mode through thevDSO, without entering the kernel. - High-performance I/O batches calls, or avoids them altogether:
epoll_waitreturning many events at once, andio_uring(lesson 3.5).
#What a process is
A process is a running instance of a program. The program is a file on disk, a recipe. The process is the recipe being cooked: memory in use, files open, a position in the code. When you open Chrome, the OS creates one or more processes for it; start python twice and you have two processes running the same program.
Operating System
|
+---- Chrome process(es)
|
+---- VS Code process
|
+---- MySQL process
|
+---- Python process
Each process owns:
| It has | What it is | Why you care |
|---|---|---|
| Virtual address space | Its private view of memory: code, global data, heap, stacks | Isolation. The same address in two processes is different memory (lesson 3.4) |
| Process ID (PID) | Its number, and its parent’s (PPID) | kill, ps, logs, metrics |
| Memory | The pages actually in use (resident memory) | Capacity planning, the OOM killer |
| Open file descriptors (FDs) | A table of small numbers naming open files, sockets and pipes. 0, 1 and 2 are standard input, output and error | Every connection uses one; the per-process limit (ulimit -n, lesson 1.2) is a real ceiling |
| Security context | UID, GID, capabilities, plus namespaces and cgroups in a container | What a hacked process could do |
| One or more threads | The parts that actually run on a CPU | Parallelism, and the whole next section |
It also has a current directory, environment variables, signal handlers and resource limits. You can see all of this in /proc/<pid>/ on Linux, as you will under “Try it yourself”.
The kernel keeps its own record of each process, traditionally called the process control block (PCB): state, PID, parent, saved registers for when it is not running, its memory map, its file table, accounting. On Linux the structure is task_struct, and there is one per thread; the fields are shared between threads of the same process.
A typical address space, from low addresses to high:
low +--------------------+
| Code (read-only) | the program's machine code, shareable between processes
| Global data | global and static variables
| Heap ↓ grows| malloc, new, every object
| |
| Shared libraries, |
| memory maps |
| |
| Stack ↑ grows| function calls and local variables
high +--------------------+
| Kernel | reserved for the kernel, no access from user mode
+--------------------+
#The wall between processes
One process cannot read or write another’s memory, because it has no address that leads there. Every address goes through that process’s own page table, so the same number in another process means different physical bytes, and an address with no mapping faults at once. That wall gives you three things at the same time: fault isolation (a crash stays inside), security (one user’s process cannot read another’s secrets) and independent resources (limits, restart, upgrade).
The wall has deliberate doors. Two processes can agree to map shared memory, and a debugger with the right permission can use ptrace. The notes’ statement that a process “cannot directly access” another’s memory is true by default, not absolute.
#What a thread is
A thread is a separate flow of execution inside a process. A process always has at least one, and it can have many. Only threads run on CPUs: the scheduler in lesson 3.2 picks threads, not processes.
A program with three threads
|
+---------+---------+
| | |
v v v
Thread 1 Thread 2 Thread 3
UI Network Rendering
The split is the most useful thing to memorise in this chapter:
| Shared by all threads of a process | Private to each thread |
|---|---|
| Heap memory | Stack (function calls, local variables) |
| Global variables | Program counter (the next instruction) |
| Code segment | CPU registers |
| Open file descriptors | Thread ID (TID) |
| Signal handlers, current directory, UID | Signal mask, thread-local storage, errno |
So all threads see the same objects, can use the same sockets and run the same code, but each has its own position in that code and its own local variables. Two threads can run the same function at once without their locals colliding.
Inside a process: what it owns, what threads share
A process is a box of resources. A thread is a line of execution running inside that box. Solid boxes are shared by all threads of the process; each dashed column belongs to one thread.
Scenario
Start the program once and walk through everything the kernel set up for it.
Running a program creates a process: a running instance of it. Start the same program twice and you get two processes with two PIDs. The program file on disk is only the recipe.
Three consequences catch people out:
- Not all memory is shared in practice, and not all of it is safe. Locals on a stack are effectively private, and registers really are. But stacks live in the one address space, so a stray pointer in one thread can overwrite another’s stack. Separate, yes; protected, no.
- File descriptors are shared. If one thread closes a socket, it is closed for all of them. A double close can hit a descriptor that another thread has just received with the same number, which is one of the nastier bugs to find.
- Sharing makes talking easy and getting it right hard. Threads communicate just by writing to memory, with no system call. If two threads write the same data without coordination you get a race; we name it here and teach it in Chapter 14.
#Kernel threads and user-level threads
The threads in this lesson are kernel threads: the kernel knows each one and schedules it, one-to-one. That is what pthread_create and a Java platform thread give you on Linux. Goroutines, Java virtual threads and other green threads are lighter: the language runtime multiplexes many of them onto a few kernel threads. They are covered in lesson 3.6, because they need the event loop’s machinery to work.
#Process vs thread
| Topic | Process | Thread |
|---|---|---|
| What it is | A running program | A flow of execution inside one |
| Memory | Usually a separate address space | Shares the process’s memory, has its own stack |
| Resources | Relatively more | Relatively fewer |
| Creation | Relatively expensive | Relatively light |
| Communication | Needs IPC | Direct, through shared memory |
| Failure isolation | Strong | Weak: threads of one process can affect each other |
| Context switch | Usually costlier (address space changes) | Usually cheaper within one process (lesson 3.2) |
| Security boundary | Yes | No |
| Spreads across machines | Yes, with network IPC | No |
| Limits to watch | Per-process memory, connections to shared stores | Stack memory, pids.max, lock contention |
#Creating processes and threads
#fork, exec and wait
Unix splits “start a program” into two steps, and that split is unusually powerful.
fork: copy me
fork()creates a child that is a near-copy of the parent. It returns twice: 0 in the child and the child’s PID in the parent. Memory is not actually copied at this point: parent and child share the pages through copy-on-write, and a page is copied only when someone writes to it. The mechanism is in lesson 3.4.exec: become that program
execve()replaces the current memory image with a new program. The PID, parent and open file descriptors stay, except those marked close-on-exec. Betweenforkandexecthe child can rearrange things: that is how a shell implements>redirection and pipes, using no special support in the program it runs. On successexecnever returns; if it does return, something failed, and the child must exit.wait: collect the result
wait()orwaitpid()gives the parent the child’s exit status and lets the kernel delete its record. A parent that never does this creates zombies, which we come to shortly.
Windows has no fork; its CreateProcess starts a new program directly, which is one reason process creation on Windows is much slower and why Windows servers lean on threads. posix_spawn offers the same one-step style on Unix.
#Threads on Linux
Linux has no separate thread concept inside the kernel. Both are tasks, created with clone(). Flags choose what is shared: share memory, file table and signal handlers and you get a thread, whose TID is new while its PID (its thread group) is the same. That explains details you will meet: thread stacks appear as memory maps of the process, and limits such as pids.max count threads as well as processes.
#What each one costs
These are orders of magnitude on a typical Linux server. Measure on your own systems before relying on them.
| Action | Rough cost | What it depends on |
|---|---|---|
| Function call | About 1 ns | |
| System call | 100 ns to 1 µs | CPU, mitigations |
| Create a thread | Tens of microseconds | Stack setup |
fork a small process | Tens to hundreds of microseconds | Copying page tables |
fork a process using tens of gigabytes | Milliseconds to much more | Page tables grow with memory |
fork + exec + load a runtime (Python, Java) | Tens of milliseconds to seconds | Program startup, not the kernel |
| Thread’s memory | 8 MB of address space by default on Linux glibc, about 1 MB on a 64-bit JVM; only touched pages use RAM, often tens of kilobytes | ulimit -s, -Xss |
| Process’s memory | Megabytes, sometimes hundreds for a runtime; shared code and copy-on-write pages are counted once | Language |
Three design results follow:
- Do not create per request. Starting a thread or process for each request wastes this cost repeatedly. Reuse them with a pool (lesson 3.3) or pre-fork workers (lesson 1.13).
forkgets slower as the process grows. Redis forks a child to write its snapshot (BGSAVE), and on a large instance that fork alone can freeze command processing for tens of milliseconds or more. Writes during the snapshot then copy pages, so a Redis server can need far more memory than its data alone.- A thread is cheap until you have tens of thousands. A hundred thousand idle WebSocket connections (lesson 2.6) would mean a hundred thousand stacks and a lot of scheduling. That is the problem lessons 3.5 and 3.6 solve.
Creating work: fork, exec, wait, and a thread
On Unix a new process starts as a copy of an existing one, and a separate call loads a new program into it. A thread uses the same kernel machinery but shares everything instead of copying it.
Scenario
What a shell does every time you type a command, here ls.
The shell asks for a copy of itself. It cannot create an empty process: on Unix, processes are always born from another process, all the way back to PID 1.
#Process states
A process or thread is always in one state, and on a server it is rarely “running”.
| State | Meaning | ps stat |
|---|---|---|
| New | Being created | |
| Ready | Could run, waiting for a core | R |
| Running | On a core now | R |
| Waiting (interruptible) | Sleeping until an event: socket data, a timer, a lock | S |
| Waiting (uninterruptible) | Sleeping inside a kernel disk path; signals must wait | D |
| Stopped | Paused by SIGSTOP or a debugger | T |
| Zombie | Exited, status not collected | Z |
Some reading tips:
- A healthy idle server shows almost everything in
S. A thread waiting on a database costs no CPU, which is why “thread count” and “CPU use” can look unrelated. - Many threads in
Rwith few cores means CPU saturation; you will see queueing in the load average and in latency. - Many threads in
Dmeans storage trouble: the classic signature is high load average with low CPU (lesson 1.2 noted that Linux countsDin the load). EvenSIGKILLwaits until the thread wakes.
Process states: where a process spends its life
Most of the time a server’s threads are not running. The kernel moves each one between these states, and the stat column of ps tells you which one it is in.
Scenario
From fork to the record being deleted.
fork has just created the task. The kernel is setting up its record and memory map. It cannot run yet.
#Zombies and orphans
Two names that sound similar and get confused in interviews.
A zombie is a process that has exited but whose parent has not yet called wait. Its memory and files are gone. Only a kernel record with the exit status remains, plus its PID. Every process is a zombie for a moment, which is normal. A bug appears when a parent never reaps: ps fills with <defunct>, and eventually fork fails because the PID space (/proc/sys/kernel/pid_max) or the container’s pids.max is used up.
- You cannot kill a zombie: it is already dead.
kill -9does nothing. - You fix the parent: handle
SIGCHLDand callwaitpidin a loop, or setSIGCHLDto ignored if you do not need the status. In an emergency, kill the parent, so that init adopts and reaps the zombies.
An orphan is a live process whose parent has died. It is not killed. The kernel re-parents it to PID 1 (or a nearer subreaper), and that new parent will reap it when it exits. Orphans are sometimes deliberate (nohup, daemons) and sometimes a surprise: a worker that outlives its master keeps the inherited listening socket, so the restarted service fails with address already in use.
Containers make this your problem. Your application is PID 1 in its PID namespace, so orphans in that container are re-parented to your app, which has no idea it should reap them. Use a tiny init: docker run --init, or tini as the entrypoint.
Zombies and orphans
A zombie is a child that has died but whose parent has not read its exit status. An orphan is a living child whose parent died. Different problems with different fixes.
Scenario
The healthy case: a zombie for a moment.
Every process becomes a zombie when it exits. It is normal and brief: the kernel keeps the exit status because the parent may want it.
#Inter-process communication
Processes need a door in the wall. IPC is the family of those doors.
| Method | How it works | Copies | Reach | Use it for | How it fails |
|---|---|---|---|---|---|
| Pipe | One-way byte stream through a kernel buffer (64 KB by default on Linux) | 2 | Related processes | Shell pipelines, a parent talking to a child | Writer blocks when full; SIGPIPE when the reader has gone |
| Named pipe (FIFO) | A pipe with a file name | 2 | Same machine | Simple unrelated processes | Same, plus blocking when opening with no partner |
| Unix domain socket | Socket API with a file path as the address | 2 | Same machine | Nginx → PHP-FPM, local database connections, the Docker API, sidecars | Stale socket file, wrong file permissions |
| TCP / UDP socket | The full network stack | 2 and up | Anywhere | Services, databases, everything between machines | Timeouts, partial failure |
| Shared memory | Same pages mapped in both | 0 | Same machine | PostgreSQL’s shared buffers, high-rate data | Races; a crash can corrupt it for everyone |
| Message queue (POSIX, System V) | Kernel-held queue of messages | 2 | Same machine | Rare today; apps tend to use sockets | Fills up, size limits |
| Signal | A number delivered asynchronously | None | Same machine | Notifications: stop, reload, a child exited | Carries no data, can merge, limited handler code |
| Files, memory-mapped files | Through the file system | Varies | Same machine, or shared storage | Handing over batches, simple locks | Partial reads, locking |
How to choose. Same machine and streams of requests: a Unix domain socket, as the sockets API is portable to TCP later. Same machine and very high rate, with locks you trust: shared memory. Anything that might ever move to another machine: TCP with a proper protocol (HTTP, gRPC) or a message broker (Chapters 30–32). In practice, a system’s microservices are simply processes doing IPC over the network, and all the extra failure modes come from that last word.
Local IPC also fails more simply. A pipe either works or reports a broken pipe; a network call can simply get no answer, leaving you unsure whether the work happened.
Talking across the wall: inter-process communication
Processes cannot read each other’s memory, so every method asks the kernel to carry the data or to set up a shared area. Compare what each one costs and how each one fails.
Scenario
ls | wc -l: one-way bytes between related processes.
The shell calls pipe, and the kernel creates a buffer with two file descriptors: a read end and a write end. After fork, each side inherits one, so the programs just use ordinary standard output and input.
#Signals and graceful shutdown
A signal is a small asynchronous notification from the kernel, identified only by a number. It can arrive at any instruction. The process then runs its handler, or the default action if it has none.
| Signal | Number | Default | Meaning and typical use |
|---|---|---|---|
SIGHUP | 1 | Terminate | Terminal closed; by convention “reload config” for daemons |
SIGINT | 2 | Terminate | Ctrl+C |
SIGKILL | 9 | Terminate | Kernel ends it now. Cannot be caught, blocked or ignored |
SIGSEGV | 11 | Terminate + core dump | Invalid memory access |
SIGPIPE | 13 | Terminate | Wrote to a closed pipe or socket; servers ignore it and handle the write error |
SIGTERM | 15 | Terminate | “Please stop.” What kill, systemctl stop, docker stop and Kubernetes send first |
SIGCHLD | 17 on x86 Linux | Ignore | A child exited |
SIGSTOP / SIGCONT | 19 / 18 on x86 Linux | Stop / continue | Pause and resume. SIGSTOP cannot be caught |
Exit codes tell you what happened. When a signal kills a process, shells and container platforms report 128 + the signal number:
| Exit code | Meaning | Look at |
|---|---|---|
| 0 | Success | |
| 1, 2, … | The program’s own error | Application logs |
| 126 / 127 | Cannot execute / command not found | The image, the path, the CMD |
| 137 (128 + 9) | SIGKILL: OOM killer, or the grace period ran out | dmesg, the reason in kubectl describe pod |
| 139 (128 + 11) | SIGSEGV: memory bug, usually native code | Core dump, recent native library changes |
| 143 (128 + 15) | SIGTERM with no handler | Add a handler; check for cut requests |
#Writing a graceful shutdown
Lesson 1.2 gave the deploy sequence. Here is the process’s side of it, in Node.js:
const http = require('node:http');
const server = http.createServer((req, res) => res.end('ok\n'));
server.listen(8080);
process.on('SIGTERM', () => {
console.log('SIGTERM: draining');
// Stop accepting; the callback fires when in-flight requests are done.
server.close(() => {
console.log('drained, exiting');
process.exit(0);
});
// Give up shortly before the orchestrator's SIGKILL would.
setTimeout(() => process.exit(1), 25_000).unref();
});
The same order applies in every language:
- Fail readiness so that traffic stops, then wait a moment, because load balancers do not react instantly. In Kubernetes, endpoint removal and
SIGTERMhappen at the same time, not in sequence, so a few seconds of sleep in apreStophook is common. - Stop accepting new work, by closing the listening socket and pausing queue consumption.
- Finish in-flight work, with a deadline shorter than the grace period (30 seconds by default in Kubernetes, 10 in
docker stop). - Release resources: flush logs and metrics, return unfinished messages to the queue, close database connections.
- Exit 0.
In languages where you write the handler in native code, a signal handler may only call a short list of async-signal-safe functions, since it can interrupt anything, including malloc. So handlers just set a flag or write to a pipe, and normal code does the rest. Signals sent to a process go to any one thread that has not blocked them, so multithreaded servers usually block signals everywhere and dedicate one thread to sigwait.
Design for the case where none of this runs. A power failure, kernel panic or SIGKILL gives no warning. Graceful shutdown makes deploys clean, but only idempotent operations, transactions and a durable log make crashes safe.
Signals and graceful shutdown
Every deploy ends with the old process getting SIGTERM. Whether users see errors depends on what the process does next. The grace period here is Kubernetes’ default of 30 seconds.
Scenario
The handler stops new work, finishes old work and exits.
First, traffic is steered away. In Kubernetes this happens at the same time as the signal, not before it, so a short sleep in a preStop hook is a common way to avoid sending requests to a closing server.
#Crash isolation: who dies with whom
The rule is simple: a bug’s blast radius is the unit that shares memory with it.
If one thread dereferences a bad pointer, the kernel sends SIGSEGV and, by default, the whole process dies: all threads and all in-progress requests. It has to be that way. The bug might have overwritten anything in the shared memory, so the other threads cannot be trusted. Separate processes are different: one crashing leaves the others untouched, unless they share memory.
Three things are often mixed up here:
| Event | What dies |
|---|---|
| Segfault in native code (any thread) | The whole process |
| Uncaught exception in Java or Python, in a non-main thread | That thread only; the process goes on, possibly with a half-updated object or a held lock |
| Uncaught exception in Node.js, or an unrecovered panic in any goroutine in Go | The whole process |
| Uncaught exception in C++ | The whole process, through std::terminate |
| Out of memory (Linux OOM killer) | The whole process chosen by the kernel, then possibly others |
| Stack overflow from deep recursion | Usually the whole process in native code; a catchable error in Java and Python |
Security has the same shape. The 2014 Heartbleed bug in OpenSSL let attackers read a server’s process memory, private keys included, because everything lived in one address space. Splitting sensitive work into its own process, as SSH’s privilege separation does, limits what a bug can reach.
#Multi-process vs multi-thread servers
Lesson 1.2 introduced the models. Now you can see why real systems made the choices they did.
| System | Model | Why, and what it costs |
|---|---|---|
| PostgreSQL | One OS process per connection, forked from the postmaster, with shared memory for its buffers | A design from the 1980s and very portable. Each backend takes several megabytes, so max_connections is usually kept in the hundreds (the default is 100). Hence PgBouncer. A crashing backend makes the postmaster reset all the others, because shared memory may be corrupt |
| MySQL | One mysqld process, one thread per connection | Cheaper per connection, and thousands are practical. A crash loses the whole server, and it recovers through the InnoDB redo log after a restart |
| Nginx | Master plus one worker process per core, each an event loop | Workers are isolated; a dead worker is replaced in milliseconds. Sharing state needs an explicit shared memory zone (lesson 1.13) |
| Apache | prefork (processes), worker and event (processes with threads) | Shows the trade-off inside one product: prefork for libraries that are not thread-safe, event for scale |
| PHP-FPM, Gunicorn | Pool of pre-forked workers | Isolates bad requests and memory leaks, works around Python’s GIL for CPU work (lesson 3.2). Workers can be recycled after N requests |
| Tomcat, classic Java and .NET | One process, a thread pool | Shares caches and connection pools in memory and starts fast. One bad native library or heap exhaustion takes down everything |
| Redis | One process; commands on a single thread, optional I/O threads; forks for persistence | No locks in the data path; persistence uses copy-on-write. See lesson 3.6 |
| Chrome | Browser, GPU, network and many renderer processes, with threads inside | A bad page cannot crash or read other sites (Site Isolation). The cost is memory |
Who dies with whom: threads, processes and real servers
The blast radius of a bug is the unit that shares memory with it. Compare one thread’s segfault, an ordinary exception, worker processes, Chrome tabs, PostgreSQL and MySQL.
Scenario
One bad pointer in one thread, 200 requests in the same process.
Three of the threads are shown. They all use the same heap, which is what makes them fast and cheap.
#How to choose
| You need | Lean towards | Because |
|---|---|---|
| Code you do not fully trust (plugins, native libraries, user content) | Processes | The wall contains crashes and exploits |
| Big shared in-memory state (caches, connection pools) | Threads | One heap, no copying |
| CPU work on all cores in a language with a global lock | Processes | Threads cannot run the language code in parallel (CPython’s default build) |
| Many mostly idle connections | Event loop or virtual threads | See lessons 3.5 and 3.6 |
| Security separation between users or privilege levels | Processes (or containers, or VMs) | Threads share credentials and memory |
| Quick start, low memory per unit | Threads | Far less to set up |
| Fast restart of one piece, or leak containment | Processes | Recycling a worker is routine |
| Scaling beyond one machine | Processes that talk over the network | Threads cannot cross a machine boundary |
In practice, modern systems combine them: a few processes per machine or one per container, each with a pool of threads or an event loop, spread across many machines, with state kept outside. A container platform restarts the process when it dies, so process-level isolation is what your whole fleet relies on. The bulkhead pattern extends the same idea by giving separate dependencies separate pools (lesson 3.3).
#Common production problems
| Symptom | Likely cause | Check | Fix |
|---|---|---|---|
| “fork: Resource temporarily unavailable” | Hit pids.max, ulimit -u or pid_max, often through a thread leak | ps -eLf | wc -l, cat /sys/fs/cgroup/pids.current | Bounded pools, fix the leak |
Java: OutOfMemoryError: unable to create native thread | Same task limit, or no address space, while the heap looks fine | Thread count, pids.max | A pool, smaller -Xss |
| “too many open files” | FD limit, or a connection or file leak | ls /proc/<pid>/fd | wc -l | Raise LimitNOFILE, close what you open |
<defunct> entries growing | The parent does not reap | ps -eo pid,ppid,stat,comm | grep Z | Fix the parent; add an init in containers |
| Stop takes exactly 10 or 30 s, then exit 137 | The signal never reached the app, or no handler | Is CMD the shell form? | Exec form, --init, a handler |
| Errors on every deploy | No graceful shutdown, or traffic still arriving after SIGTERM | Exit code 143, 502 spikes | Handler, preStop, drain |
| Random exit 139 | Native memory bug | Core dump, dmesg | Update or isolate the library |
| Load average high, CPU idle | Threads in D: storage hanging | ps -eo stat,comm | grep ^D | Fix the storage or the mount |
Service will not restart: address already in use | An orphan inherited the socket | ss -tlnp | Kill it, use close-on-exec, use systemd cgroup kill |
SIGTERM handler sometimes hangs | Handler does unsafe work and deadlocks on a lock | Stack dump | Handler only sets a flag |
#Processes and threads in system design
- The process is your unit of failure and deployment; the machine is your unit of hardware failure. Assume any process can die at any moment, keep state outside it (lesson 1.3), and run several.
- Threads and processes cost real capacity. A thousand threads with 1 MB stacks touching 100 KB each is about 100 MB of memory plus scheduling overhead. A database with 5,000 processes is an outage waiting to happen. Put a pool in front.
- Connection limits are process and FD limits. A database’s connection cap comes from these costs. In capacity plans, count connections per app instance times the number of instances.
- Stateless processes restart well. Warm in-memory caches go with the process, so a restart can cause a cold-start spike; roll restarts slowly.
- Isolation is a dial: thread → process → container → virtual machine → separate machine. Each step costs more and contains more (Chapter 4 covers the far end).
- Interview angles: “Process vs thread?” Answer with what is shared, the blast radius, and the cost. “Why does PostgreSQL need PgBouncer and MySQL mostly not?” “What is a zombie, and can you kill one?” “Why does a container take 30 s to stop?” “What does exit 137 mean?” “Would you run a plugin in a thread or a process?” “Why can one bad pointer take down 200 requests?”
#Try it yourself
On a Linux machine, or in a container with procps and python3:
# Look at processes: pid, parent, state, thread count, resident KB, name
ps -eo pid,ppid,stat,nlwp,rss,comm --sort=-nlwp | head
# Every thread of every process (LWP is the TID)
ps -eLf | head
# Everything the kernel knows about your shell
grep -E 'Name|State|Tgid|^Pid|PPid|Threads|Uid' /proc/$$/status
ls -l /proc/$$/fd # its file descriptors
id # its security context
# Watch the system calls behind a command, and count them
strace -f -e trace=clone,clone3,execve,wait4 bash -c 'ls > /dev/null'
strace -c ls > /dev/null
# Make a zombie for 30 seconds, then see it
python3 -c "import os, time; pid = os.fork(); (os._exit(0) if pid == 0 else time.sleep(30))" &
sleep 1; ps -o pid,ppid,stat,comm --ppid $!
# Read exit codes: 143 and 137
sleep 100 & P=$!; kill -TERM $P; wait $P; echo "exit code $?"
sleep 100 & P=$!; kill -KILL $P; wait $P; echo "exit code $?"
# One thread segfaults and the whole process goes down: the print never runs, exit code 139
python3 -c 'import ctypes, threading, time
threading.Thread(target=lambda: ctypes.string_at(0)).start()
time.sleep(5); print("never printed")'; echo "exit code $?"
Threads share memory, processes do not. Save this as share.py and run it:
import multiprocessing as mp
import threading
counter = 0
def bump():
global counter
counter = 100
if __name__ == '__main__':
t = threading.Thread(target=bump)
t.start(); t.join()
print('after thread :', counter) # 100: same memory
counter = 0
p = mp.Process(target=bump)
p.start(); p.join()
print('after process:', counter) # 0: the child changed its own copy
A Dockerfile that lets signals reach your app:
# Shell form runs under sh, so the app does not get SIGTERM:
# CMD node server.js
# Exec form makes the app PID 1, and it needs its own handler:
CMD ["node", "server.js"]
Run it with docker run --init to get signal forwarding and zombie reaping, then time docker stop <name>. If that takes exactly 10 seconds, the signal is not getting through.
#Summary
Key takeaways
- The kernel is the privileged core of the OS. Programs run in user mode and ask for everything through system calls, which cost roughly 100 ns to 1 µs, so programs buffer and batch.
- A process is a running program with its own virtual address space, PID, FDs, security context and threads, tracked in the kernel’s PCB. A wall separates processes, with deliberate doors.
- A thread is a flow of execution in a process. Threads share heap, globals, code and FDs; each owns a stack, program counter, registers and TID. Stacks are private but not protected.
forkcopies with copy-on-write,execloads a new program,waitcollects the status. On Linux, threads are tasks fromclonethat share everything. Creation is expensive enough that servers reuse workers. Do notforkand carry on in a threaded program.- States: Ready and Running are both
R,Swaits cheaply,Dcannot be interrupted, and high load with an idle CPU means storage trouble. - A zombie is dead but unreaped and cannot be killed; fix the parent. An orphan is alive and adopted by PID 1. In a container, PID 1 is your app, so use an init.
- IPC: pipes for streams, Unix sockets locally, shared memory for speed at the price of shared failure, TCP for anything that may cross machines.
SIGTERMcan be caught for graceful shutdown;SIGKILLcannot. Exit code = 128 + signal: 137, 139, 143. Shell form in a Dockerfile, or PID 1 with no handler, swallowsSIGTERM.- A bug’s blast radius is whoever shares its memory: a segfault kills all threads of a process. PostgreSQL resets all backends on one crash because of shared buffers, MySQL loses the whole process, and Chrome isolates sites.
- Real systems combine a few processes, pools of threads or event loops, and many machines, with state kept outside.
Answer on your own first, then read the explanation, even when you got it right.
Self-checkQuestion
Which statement best describes the difference between a program and a process?
Self-checkQuestion
Which of these is private to each thread rather than shared across the process?
Self-checkQuestion
Which two statements about user mode and kernel mode are correct?
Choose 2
Self-checkQuestion
A logging library writes each line with its own write system call, and at 200,000 lines per second it uses a lot of CPU. What is the most effective fix?
Self-checkQuestion
Two processes each print the address of a variable and both show 0x7f00a000, but the values there differ. Why?
Self-checkQuestion
One thread of a C server has a bug and writes through a bad pointer that happens to land inside another thread’s stack. What happens?
Self-checkQuestion
A thread calls close(7) on a socket. Another thread in the same process was about to read from FD 7. What is the risk?
Self-checkQuestion
What is fork’s return value in the parent and in the child?
Self-checkQuestion
A parent process using 40 GB of memory calls fork. Which statement is correct?
Self-checkQuestion
A child calls execve on a program that does not exist, and the next line of the code is the parent’s normal server logic. What can go wrong?
Self-checkQuestion
A multithreaded Python service forks a child to do background work, without exec. Occasionally the child hangs for ever at its first memory allocation. What is the most likely reason?
Self-checkQuestion
On Linux, how does the kernel represent threads?
Self-checkQuestion
A container has pids.max set to 512. The app’s heap and CPU are fine, but new requests fail with an error about creating a thread. Which two steps fit best?
Choose 2
Self-checkQuestion
ps shows a web thread with stat S most of the time. What does this tell you?
Self-checkQuestion
A server’s load average is 40 on an 8-core machine, but CPU is 3% busy and kill -9 does not stop some processes. What is most likely?
Self-checkQuestion
ps shows hundreds of processes marked defunct under one parent, and kill -9 on them changes nothing. What is the right fix?
Self-checkQuestion
Which two statements about orphans are correct?
Choose 2
Self-checkQuestion
An app runs in a container with no init. Over weeks, zombie processes build up until new processes cannot start. Why does this happen only in the container?
Self-checkQuestion
Nginx must pass requests to a PHP-FPM pool on the same machine at a high rate. Which option is the most sensible?
Self-checkQuestion
What is the main hazard of using shared memory between processes, compared with a pipe?
Self-checkQuestion
In the pipeline producer | consumer, the consumer is slow and the pipe’s buffer fills. What happens? And if the consumer then crashes?
Self-checkQuestion
A server sometimes dies with no log when a client disconnects during a response. Which fix addresses the most likely cause?
Self-checkQuestion
Which of these signals can a process not catch, block or ignore?
Self-checkQuestion
A Kubernetes pod exits with code 137. Which is the most accurate reading?
Self-checkQuestion
docker stop takes exactly 10 seconds every time, then the container exits with 137. Which two are likely causes?
Choose 2
Self-checkQuestion
Why do signal handlers in native code usually do nothing but set a flag?
Self-checkQuestion
During a Kubernetes rolling deploy, a server handles SIGTERM correctly, yet a few requests still fail right after the signal. What is the most likely cause?
Self-checkQuestion
A Java service has 200 worker threads. One thread segfaults inside a native image library. What happens to the other 199?
Self-checkQuestion
In which of these does an unhandled error in one thread or goroutine end the whole process?
Self-checkQuestion
A PostgreSQL backend segfaults. What do other connected clients see?
Self-checkQuestion
An app with 40 instances keeps 50 connections each to PostgreSQL, and the database struggles. Which fix addresses the actual cost?
Self-checkQuestion
You must run third-party plugins that may crash or contain security bugs, inside your server. Which design is the best fit?
Self-checkQuestion
Which is not a typical reason to prefer threads over processes?