Chapter 6 — fork() and exec(): how new processes are born
In one sentence
Linux creates new processes in two separate steps — fork() clones the
current process, exec() replaces a process's code with something else
entirely — and understanding why they're SEPARATE explains a surprising
amount of how Unix actually works.
Why this matters
Shells, servers, and container runtimes all use this exact pattern. Knowing it means "how does bash run a program" and "how does a container start" stop being separate mysteries — they're the same mechanism.
How it actually works
fork() makes an almost-exact COPY of the calling process: same code,
same memory contents (initially), same open file descriptors
-- but a NEW PID. Both the original (parent) and the copy
(child) continue running from the EXACT SAME LINE of code,
right after the fork() call, and can tell themselves apart
by fork()'s RETURN VALUE:
in the parent: fork() returns the child's PID
in the child: fork() returns 0
(if it fails: fork() returns -1)
exec() REPLACES the calling process's code and memory with a
DIFFERENT program entirely, keeping the same PID and open
file descriptors. It doesn't return on success -- the calling
program is simply gone, replaced.
WHY TWO SEPARATE STEPS, rather than one "run this other program" call?
Because fork() ALONE is useful too -- and because the gap BETWEEN
fork() and exec() is exactly where a shell does useful setup work
(redirecting a file descriptor for `>`, changing directory, setting
environment variables) on the CHILD ONLY, before replacing it, without
ever affecting the parent shell itself:
pid_t pid = fork();
if (pid == 0) {
// child: I am a copy of the shell right now
// ... redirect stdout to a file, if the user wrote `> out.txt`
// ... then:
execve("/bin/ls", args, environ); // now I become `ls`
} else {
// parent (the shell): keep running, maybe wait() for the child
}
Real-world example
PostgreSQL's architecture is a clean, real-world illustration of
fork() used WITHOUT a following exec(): PostgreSQL's documentation
describes its "process per connection" model, where the main
`postmaster` process FORKS a new backend process for each client
connection -- and that child does NOT exec() into a different
program. It simply continues running as a full copy of the postmaster,
now dedicated to servicing one connection, benefiting from
copy-on-write (Chapter 10) so the fork itself is cheap even though
postgres is a large program.
This is a DELIBERATE architectural trade-off, openly discussed in
PostgreSQL's own documentation and by its developers: process-per-
connection gives strong ISOLATION (one connection's crash can't
corrupt another's memory, unlike a thread-per-connection model) at
the cost of the per-process overhead this Part keeps returning to --
which is exactly why connection poolers like PgBouncer exist, to
avoid forking a whole new backend for every short-lived client
connection in high-throughput applications.
Try it yourself
# Watch fork+exec happen live, for the simplest possible command
strace -f -e trace=clone,fork,vfork,execve bash -c 'ls' 2>&1
# Write and run a tiny C program that forks (skip if you don't have gcc)
cat > forkdemo.c << 'EOF'
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
printf("child: my PID is %d\n", getpid());
} else {
printf("parent: I created child PID %d\n", pid);
}
return 0;
}
EOF
gcc -o forkdemo forkdemo.c && ./forkdemo
Build it in Go
Go has no fork() in its public API (forking a multi-threaded runtime is
unsafe); os/exec uses a combined fork+exec (clone + execve) under the hood.
Chapter 74 covers exit codes, process groups,
timeouts, and the PID 1 duty of reaping orphans, with a lab that produces
zombies on purpose.
Common mistakes
- Thinking
fork()runs a different program. It doesn't — it duplicates the SAME program.exec()is the step that changes what code is running. - Assuming the parent and child run in a guaranteed order. After
fork(), the OS scheduler (Chapter 8) decides which runs first — your code must not assume one happens before the other unless you explicitly synchronise them. - Forgetting that file descriptors are shared (initially) after fork. Both parent and child have the SAME open files/sockets right after forking — a common source of subtle bugs if both try to write to the same one.
Check yourself
- What does
fork()do, and what doesexec()do — and why are they separate calls? - In the parent process, what does
fork()return? In the child? - Why does PostgreSQL fork a new process per connection instead of using threads, and what does that trade off?
Further reading
- Book (free): OSTEP, "Process API" chapter — walks through real
fork()/exec()code examples in detail. - Reference:
man 2 fork,man 2 execve. - Docs: PostgreSQL's own documentation, "Overview of PostgreSQL Internals" — the process-per-connection model, explained by its own maintainers.