Course outline

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, exec and wait, how Linux creates threads with clone, what each costs, and why fork in 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:

TopicLesson
Process vs thread, threads3.1, this lesson
Concurrency, context switching3.2
Thread pools, deadlocks3.3
Virtual memory3.4
I/O models, epoll3.5
Event loop3.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 jobWhat it means for youLesson
Run many programs on few coresScheduling, run queues, context switches3.2
Give each program its own memoryVirtual addresses, page tables, the OOM killer3.4
Present disks as filesFile systems, page cache, file descriptors3.4, 3.5
Move network dataThe TCP/IP stack, sockets, epoll1.9, 3.5
Drive hardwareDevice drivers, interrupts
Enforce who may do whatUsers, permissions, namespaces, cgroups3.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, EL1 on ARM): any instruction, any memory, all hardware.
  • User mode (ring 3 on x86, EL0 on 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.

Your programuser mode
Kernelkernel mode
Diskhardware
wants 4 KB of a file
Step 1 of 5
1

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 read of 64 KB instead of 64 reads of 1 KB, and logging libraries batch their writes.
  • Common read-only calls such as clock_gettime are served in user mode through the vDSO, without entering the kernel.
  • High-performance I/O batches calls, or avoids them altogether: epoll_wait returning many events at once, and io_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 hasWhat it isWhy you care
Virtual address spaceIts private view of memory: code, global data, heap, stacksIsolation. 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
MemoryThe 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 errorEvery connection uses one; the per-process limit (ulimit -n, lesson 1.2) is a real ceiling
Security contextUID, GID, capabilities, plus namespaces and cgroups in a containerWhat a hacked process could do
One or more threadsThe parts that actually run on a CPUParallelism, 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 processPrivate to each thread
Heap memoryStack (function calls, local variables)
Global variablesProgram counter (the next instruction)
Code segmentCPU registers
Open file descriptorsThread ID (TID)
Signal handlers, current directory, UIDSignal 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.

shop-apiPID 4121
Code
shop-api program
Globals
version = 7
Heap
empty
Open FDs
0 1 2 · 7 8
Security
UID 1000
Threads (each has its own)
main
stack: main()
kernel creates PID 4121
Step 1 of 7
1

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:

  1. 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.
  2. 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.
  3. 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

TopicProcessThread
What it isA running programA flow of execution inside one
MemoryUsually a separate address spaceShares the process’s memory, has its own stack
ResourcesRelatively moreRelatively fewer
CreationRelatively expensiveRelatively light
CommunicationNeeds IPCDirect, through shared memory
Failure isolationStrongWeak: threads of one process can affect each other
Context switchUsually costlier (address space changes)Usually cheaper within one process (lesson 3.2)
Security boundaryYesNo
Spreads across machinesYes, with network IPCNo
Limits to watchPer-process memory, connections to shared storesStack 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.

  1. 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.

  2. 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. Between fork and exec the child can rearrange things: that is how a shell implements > redirection and pipes, using no special support in the program it runs. On success exec never returns; if it does return, something failed, and the child must exit.

  3. wait: collect the result

    wait() or waitpid() 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.

ActionRough costWhat it depends on
Function callAbout 1 ns
System call100 ns to 1 µsCPU, mitigations
Create a threadTens of microsecondsStack setup
fork a small processTens to hundreds of microsecondsCopying page tables
fork a process using tens of gigabytesMilliseconds to much morePage tables grow with memory
fork + exec + load a runtime (Python, Java)Tens of milliseconds to secondsProgram startup, not the kernel
Thread’s memory8 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 kilobytesulimit -s, -Xss
Process’s memoryMegabytes, sometimes hundreds for a runtime; shared code and copy-on-write pages are counted onceLanguage

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).
  • fork gets 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.

ParentPID 200
Kernel
New taskchild or thread
fork()
Step 1 of 8
1

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”.

StateMeaningps stat
NewBeing created
ReadyCould run, waiting for a coreR
RunningOn a core nowR
Waiting (interruptible)Sleeping until an event: socket data, a timer, a lockS
Waiting (uninterruptible)Sleeping inside a kernel disk path; signals must waitD
StoppedPaused by SIGSTOP or a debuggerT
ZombieExited, status not collectedZ

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 R with few cores means CPU saturation; you will see queueing in the load average and in latency.
  • Many threads in D means storage trouble: the classic signature is high load average with low CPU (lesson 1.2 noted that Linux counts D in the load). Even SIGKILL waits 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.

New
Readywants a core
Runningon a core
Zombieexited
Waitingsleeping
Gonereaped
Step 1 of 5
1

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 -9 does nothing.
  • You fix the parent: handle SIGCHLD and call waitpid in a loop, or set SIGCHLD to 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.

ParentPID 300
ChildPID 301zombie (Z)
initPID 1
exit(0)
Step 1 of 3
1

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.

MethodHow it worksCopiesReachUse it forHow it fails
PipeOne-way byte stream through a kernel buffer (64 KB by default on Linux)2Related processesShell pipelines, a parent talking to a childWriter blocks when full; SIGPIPE when the reader has gone
Named pipe (FIFO)A pipe with a file name2Same machineSimple unrelated processesSame, plus blocking when opening with no partner
Unix domain socketSocket API with a file path as the address2Same machineNginx → PHP-FPM, local database connections, the Docker API, sidecarsStale socket file, wrong file permissions
TCP / UDP socketThe full network stack2 and upAnywhereServices, databases, everything between machinesTimeouts, partial failure
Shared memorySame pages mapped in both0Same machinePostgreSQL’s shared buffers, high-rate dataRaces; a crash can corrupt it for everyone
Message queue (POSIX, System V)Kernel-held queue of messages2Same machineRare today; apps tend to use socketsFills up, size limits
SignalA number delivered asynchronouslyNoneSame machineNotifications: stop, reload, a child exitedCarries no data, can merge, limited handler code
Files, memory-mapped filesThrough the file systemVariesSame machine, or shared storageHanding over batches, simple locksPartial 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.

Process A
Kernelbuffer: 64 KB
Process B
Step 1 of 3
1

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.

SignalNumberDefaultMeaning and typical use
SIGHUP1TerminateTerminal closed; by convention “reload config” for daemons
SIGINT2TerminateCtrl+C
SIGKILL9TerminateKernel ends it now. Cannot be caught, blocked or ignored
SIGSEGV11Terminate + core dumpInvalid memory access
SIGPIPE13TerminateWrote to a closed pipe or socket; servers ignore it and handle the write error
SIGTERM15Terminate“Please stop.” What kill, systemctl stop, docker stop and Kubernetes send first
SIGCHLD17 on x86 LinuxIgnoreA child exited
SIGSTOP / SIGCONT19 / 18 on x86 LinuxStop / continuePause 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 codeMeaningLook at
0Success
1, 2, …The program’s own errorApplication logs
126 / 127Cannot execute / command not foundThe image, the path, the CMD
137 (128 + 9)SIGKILL: OOM killer, or the grace period ran outdmesg, the reason in kubectl describe pod
139 (128 + 11)SIGSEGV: memory bug, usually native codeCore dump, recent native library changes
143 (128 + 15)SIGTERM with no handlerAdd 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:

  1. Fail readiness so that traffic stops, then wait a moment, because load balancers do not react instantly. In Kubernetes, endpoint removal and SIGTERM happen at the same time, not in sequence, so a few seconds of sleep in a preStop hook is common.
  2. Stop accepting new work, by closing the listening socket and pausing queue consumption.
  3. Finish in-flight work, with a deadline shorter than the grace period (30 seconds by default in Kubernetes, 10 in docker stop).
  4. Release resources: flush logs and metrics, return unfinished messages to the queue, close database connections.
  5. 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.

OrchestratorKubernetes
Load balancer
App process3 requests in flight
remove from targets
Step 1 of 5
1

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:

EventWhat dies
Segfault in native code (any thread)The whole process
Uncaught exception in Java or Python, in a non-main threadThat 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 GoThe 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 recursionUsually 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.

SystemModelWhy, and what it costs
PostgreSQLOne OS process per connection, forked from the postmaster, with shared memory for its buffersA 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
MySQLOne mysqld process, one thread per connectionCheaper per connection, and thousands are practical. A crash loses the whole server, and it recovers through the InnoDB redo log after a restart
NginxMaster plus one worker process per core, each an event loopWorkers are isolated; a dead worker is replaced in milliseconds. Sharing state needs an explicit shared memory zone (lesson 1.13)
Apacheprefork (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, GunicornPool of pre-forked workersIsolates 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 .NETOne process, a thread poolShares caches and connection pools in memory and starts fast. One bad native library or heap exhaustion takes down everything
RedisOne process; commands on a single thread, optional I/O threads; forks for persistenceNo locks in the data path; persistence uses copy-on-write. See lesson 3.6
ChromeBrowser, GPU, network and many renderer processes, with threads insideA 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.

shop-apiPID 4121
Shared heap
all requests’ data
Threads
req A
req B
req C
200 requests in progress
Step 1 of 4
1

Three of the threads are shown. They all use the same heap, which is what makes them fast and cheap.

#How to choose

You needLean towardsBecause
Code you do not fully trust (plugins, native libraries, user content)ProcessesThe wall contains crashes and exploits
Big shared in-memory state (caches, connection pools)ThreadsOne heap, no copying
CPU work on all cores in a language with a global lockProcessesThreads cannot run the language code in parallel (CPython’s default build)
Many mostly idle connectionsEvent loop or virtual threadsSee lessons 3.5 and 3.6
Security separation between users or privilege levelsProcesses (or containers, or VMs)Threads share credentials and memory
Quick start, low memory per unitThreadsFar less to set up
Fast restart of one piece, or leak containmentProcessesRecycling a worker is routine
Scaling beyond one machineProcesses that talk over the networkThreads 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

SymptomLikely causeCheckFix
“fork: Resource temporarily unavailable”Hit pids.max, ulimit -u or pid_max, often through a thread leakps -eLf | wc -l, cat /sys/fs/cgroup/pids.currentBounded pools, fix the leak
Java: OutOfMemoryError: unable to create native threadSame task limit, or no address space, while the heap looks fineThread count, pids.maxA pool, smaller -Xss
“too many open files”FD limit, or a connection or file leakls /proc/<pid>/fd | wc -lRaise LimitNOFILE, close what you open
<defunct> entries growingThe parent does not reapps -eo pid,ppid,stat,comm | grep ZFix the parent; add an init in containers
Stop takes exactly 10 or 30 s, then exit 137The signal never reached the app, or no handlerIs CMD the shell form?Exec form, --init, a handler
Errors on every deployNo graceful shutdown, or traffic still arriving after SIGTERMExit code 143, 502 spikesHandler, preStop, drain
Random exit 139Native memory bugCore dump, dmesgUpdate or isolate the library
Load average high, CPU idleThreads in D: storage hangingps -eo stat,comm | grep ^DFix the storage or the mount
Service will not restart: address already in useAn orphan inherited the socketss -tlnpKill it, use close-on-exec, use systemd cgroup kill
SIGTERM handler sometimes hangsHandler does unsafe work and deadlocks on a lockStack dumpHandler 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.
  • fork copies with copy-on-write, exec loads a new program, wait collects the status. On Linux, threads are tasks from clone that share everything. Creation is expensive enough that servers reuse workers. Do not fork and carry on in a threaded program.
  • States: Ready and Running are both R, S waits cheaply, D cannot 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.
  • SIGTERM can be caught for graceful shutdown; SIGKILL cannot. Exit code = 128 + signal: 137, 139, 143. Shell form in a Dockerfile, or PID 1 with no handler, swallows SIGTERM.
  • 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.