Frontend Labs
⋯

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

  1. What does fork() do, and what does exec() do — and why are they separate calls?
  2. In the parent process, what does fork() return? In the child?
  3. 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.

Generated from real-life-os-guide.md · 8 Oct 2026 16:36