Heads up: posts on this site are drafted by Claude and fact-checked by Codex. Both can still get things wrong — read with care and verify anything load-bearing before relying on it.
why how

Why fork() is such a weird API

Other systems take a program and arguments. Unix takes your whole process and clones it. The reasons are half historical accident, half deep insight — and the seams still show.

Systems intermediate Apr 29, 2026

Why it exists

The first time you actually look at fork(), it should feel wrong. You call one function, and it returns twice — once in the original process, once in a new copy of the original process that magically picks up at the exact same line of code. The two copies share nothing going forward; they’re separate processes with their own memory, but they started life as identical twins mid-instruction.

Every other operating system in wide use models “make a new process” as “give me a program and some arguments, and I’ll start it for you” — Windows has CreateProcess, classic Mac OS / VMS / older mainframes had similar spawn-style calls. Unix said: don’t pass me a program. Pass me yourself. The new program is a separate step (exec).

Why? The honest answer is that fork existed before there was a sensible alternative. The earliest Unix ran on a PDP-7 with no MMU and tiny memory; “swap the current process out, copy it, swap one of the copies back in” was, in 1969, easier than designing a general “start-this-program” syscall with all its options. Dennis Ritchie’s own retrospective (“The Evolution of the Unix Time-sharing System”) describes fork as a quick implementation choice that turned out to be hard to dislodge once the rest of the system grew up around it.

But once it was there, people noticed it had a strange property: it cleanly separates creating a new process from deciding what that process will run. That separation is the deep insight, and it’s why fork outlived the PDP-7.

Why it matters now

Even if you never write fork() by hand, you live downstream of it:

The short answer

fork = duplicate the current process into two + give each a different return value

The parent gets back the child’s PID; the child gets back zero. Same code, same memory contents, same open files — from that instant on, two independent processes running the same program. What you do after fork (usually exec to replace your program, or just keep running) is the creative part.

How it works

The two-return-values trick

It’s not really two returns. It’s one syscall, two processes. The kernel duplicates the calling process, schedules both, and arranges that when each one resumes from the syscall, the return value register holds something different — 0 in the child, the child’s PID in the parent. So this idiom:

pid_t pid = fork();
if (pid == 0) {
    // child: replace ourselves with a new program
    execvp("ls", argv);
} else if (pid > 0) {
    // parent: wait for child to finish
    waitpid(pid, &status, 0);
} else {
    // fork failed
}

…is one piece of source code that compiles to one binary, but at runtime the if branches differently in each process because the syscall handed them different return values. Once you see it that way, it stops being weird: fork returns once per process, not once per call.

Copy-on-write makes it cheap

The naive read of fork — “duplicate the entire process’s memory” — would be ruinously expensive for a process holding gigabytes of state. It isn’t, because of copy-on-write:

  1. After fork, parent and child share the same physical pages.
  2. The kernel marks every page read-only in both processes’ page tables.
  3. The first time either process writes to a page, the MMU traps, the kernel allocates a fresh physical page, copies the contents, and lets that process continue. Only the touched pages get duplicated.

So fork is roughly O(page-table size), not O(memory size). For a 16 GB PostgreSQL backend forking a worker, only the page table itself and a handful of dirtied pages get copied. This is why fork-heavy designs from the 1970s are still viable on machines a million times bigger than the PDP-11.

The fork+exec separation, and why it’s actually useful

The deep value of fork isn’t speed; it’s the gap between fork and exec. Between those two calls, the child is a fully-constructed process that hasn’t yet committed to a program. You can:

A spawn-style API has to accept a giant struct with all of these as options — and Windows’ CreateProcess has 10 parameters, several of them themselves structs, partly for this reason. Fork-then-exec lets you configure the child by running ordinary code in it. That’s elegant, and it’s why POSIX kept fork even after posix_spawn was added as a faster alternative.

Show the seams

The shape to keep: fork is weird because it’s a clone primitive in a world that mostly builds spawn primitives. The clone separates “make a process” from “decide what it runs,” and that separation — not fork itself — is what proved durable.

Going deeper