The idea: fork() copies the page table, not the memory

fork() makes a new process that is a copy of the caller: same code, same variables, same heap. Copying all of that memory would be slow, and usually wasted, because the child often calls exec() right away and throws it out. So Linux does not copy memory at fork. It copies the page table, the map from the process's virtual pages to physical frames, and lets both processes share every frame. Each shared page is marked read-only and copy-on-write (COW). A page is copied only when one of them writes to it, and only that one page.

Before fork: the parent's page table maps code, data, heap A, heap B and stack to frames 0 to 4, each with refs 1, data and heap writable. After fork: the parent and the child page tables both point at frames 0 to 4, each frame has refs 2, and data, heap and stack are read-only and marked COW in both tables
After fork() both page tables point at the same frames; writable pages become read-only COW and every frame's reference count goes to 2.

The canvas shows the parent (redis-server, pid 100) and its page table on the left, the 12 physical frames in the middle, and the child (pid 101) on the right. Each frame has its own colour, and a page-table entry's frame cell takes that colour, so two entries with the same colour share memory. Pick an action, a process and a page, then press Do It, or run a demo.

Page tables, the COW mark and the reference count

Every memory access goes through the page table (see Virtual Memory). An entry says which frame holds the page and what is allowed: read, write, execute. At fork() the kernel:

  1. creates the child and copies the parent's page-table entries into it, so both point at the same frames;
  2. clears the write bit of every writable page, in both processes, and marks them COW;
  3. raises each frame's reference count (how many entries point at it) from 1 to 2.

Code is already read-only, so it is simply shared and never copied. Demo 2: reads never copy: both processes read the same frames, with no fault at all.

fork copieswork at fork()memory right after fork()
every page (old Unix)every writable page, 4 KiB eachdoubled
copy-on-write (Linux)the page-table entries onlyunchanged

The write fault, step by step

When a process writes to a COW page, the CPU sees the write bit is off and raises a page fault. The kernel's fault handler sees the COW mark and checks the frame's reference count:

  • refs > 1: the frame is still shared. The handler takes a free frame, copies the 4 KiB into it, points the writer's entry at the new frame with the write bit on, and drops the old frame's count. Then the CPU runs the same write again, and it succeeds (Demo 1).
  • refs = 1: the other process already made its own copy (or is gone). Nothing needs copying; the handler just turns the write bit back on (Demo 3: both write the same page).
Time flows down. The parent writes x on the data page, which is read-only COW. The CPU raises a page fault. The kernel sees frame 1 has refs 2, takes free frame 5, copies 4 KiB from frame 1 to frame 5, points the parent's data entry at frame 5 as writable, and frame 1 drops to refs 1. The write runs again and succeeds; the child still reads the old value in frame 1
Only the page that is written gets copied, at the moment of the first write; the child keeps the old copy.

A write to a page that was never writable, like code, is not a COW fault. The handler finds nothing that allows it and sends SIGSEGV (Demo 7).

Why fork + exec is cheap

A shell runs a command with fork() then exec() in the child. exec() drops every mapping the child had and maps the new program (see Program Loading). With copy-on-write, nothing was copied in between: the cost of fork is just the page table (Demo 4: fork + exec copies nothing). The parent's pages go back to refs 1. They are still marked COW, so the parent's next write to each one faults once and only flips the write bit. Before COW, fork copied every page and exec threw them away (Demo 5: old Unix copies every page).

Even copying the page table costs time for a process with many gigabytes mapped. vfork() skips it: the child borrows the parent's memory and the parent waits until the child calls exec() or _exit(). posix_spawn() wraps that pattern safely, and is what many runtimes use to start programs. Both are system calls like any other (see System Calls).

Redis BGSAVE, memory growth and overcommit

Redis uses COW on purpose. To save a snapshot, BGSAVE forks: the child sees a frozen copy of the whole dataset and writes it to disk, while the parent keeps serving clients. Every page the parent changes during the save gets copied, so memory grows with the write rate (Demo 6: Redis BGSAVE: 3 writes, 5 → 8 frames in use; when the child exits, back to 5).

Frames in use over time during a BGSAVE: 5 before fork, still 5 right after fork, then 6, 7 and 8 as the parent writes heap A, heap B and data while the child reads, and back to 5 when the child exits
Memory does not grow at fork() but at each first write during the snapshot; in the worst case it doubles.

In the worst case every page is written and memory doubles. That is why a fork of a large process can succeed and the system can still run out of memory later, at a write. Linux allows the fork by overcommitting (vm.overcommit_memory; Redis recommends setting it to 1). If no frame is free when a COW fault needs one, the kernel reclaims or swaps pages (see Page Replacement) and, as a last resort, the OOM killer ends a process. Huge pages make each COW copy 2 MiB instead of 4 KiB, which is why Redis also asks for transparent huge pages to be off.

PostgreSQL forks one backend per connection, see PostgreSQL Process Architecture.

See also: What happens to the child after it exits (zombies, wait() and orphans) is on A Linux Process from fork() to Reaping. fork() also copies the fd table, but the open files are shared, offsets included; see What open() Does.

See also Process vs Thread: the same fork() next to pthread_create(), which shares the page table instead of copying it.

What the page leaves out

Multi-level page tables and the TLB flush after the write bits change, shared memory and MAP_SHARED mappings (never COW), private file mappings and the zero page (also COW), lazy loading of the program by exec(), threads (only the calling thread is copied), madvise(MADV_DONTFORK), and how the kernel actually counts references (struct page map counts). Five pages and twelve frames stand in for millions.