Process · Linux userspace / kernel ABI

fork and copy-on-write

Explains how fork duplicates page tables and object references without immediately copying the address space, then separates physical pages on a write fault.

Series
06 / 38
Build
cc -std=c17 -Wall -Wextra -O2 fork_cow.c -o fork_cow
Run
./fork_cow
Kernel
Linux 6.18.37 LTS

Why does memory usage not immediately double after a large process calls fork?

fork creates a new task and mm-related structures, but it does not copy the contents of every anonymous page. It write-protects the parent and child PTEs and makes them refer to the same physical pages; when either side writes, the page fault creates a private copy.

Copy-on-write is not free. It incurs page-table copying, TLB shootdowns, and later write faults and page copies. In a multithreaded process, the functions that the child may call after fork are also restricted.

Structure diagram

Figure 1. Page references immediately after fork and after a write fault

Parent page table

  • VA 0x4000
  • PTE: read-only + COW
  • mapcount reference

Before write

  • Physical page A
  • content=1
  • shared by parent + child

Child page table

  • VA 0x4000
  • PTE: read-only + COW
  • write fault occurs

After child write

  • Parent → page A
  • Child → new page B
  • content B=2

Immediately after fork, parent and child PTEs refer to the same physical page as read-only/COW. A new page appears only on the child's first write.

Call path

Figure 2. From userspace code to observable results
fork() called by the current thread
copy_process duplicate task and resources
dup_mm VMA/page table
write fault private page copy
wait reap child

Distinguish the duplication performed at fork from the duplication performed when the child writes a page. Summing RSS can count shared pages twice and misrepresent actual physical usage.

Figure 3. Major points along the kernel-internal path
kernel_clone interpret clone_args
copy_process task_struct
copy_mm branch on CLONE_VM
copy_page_range COW PTE
handle_mm_fault wp fault

This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.

Linux 6.18.37 LTS source locations

Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.

FileFunction / structureWhat to inspect
kernel/fork.c kernel_clone(), copy_process() Choose the new task and shared/duplicated flags
kernel/fork.c copy_mm(), dup_mm() CLONE_VM selection and mm_struct lifetime
mm/memory.c copy_page_range(), do_wp_page() COW page tables and write-protect faults

Complete runnable example

The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.

Buildcc -std=c17 -Wall -Wextra -O2 fork_cow.c -o fork_cow
01#define _DEFAULT_SOURCE
02#include <stdio.h>
03#include <stdlib.h>
04#include <sys/mman.h>
05#include <sys/wait.h>
06#include <unistd.h>
07
08int main(void)
09{
10    size_t length = 16 * 1024 * 1024;
11    unsigned char *area = mmap(NULL, length, PROT_READ | PROT_WRITE,
12                               MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
13    if (area == MAP_FAILED)
14        return 1;
15    for (size_t i = 0; i < length; i += 4096)
16        area[i] = 1;
17
18    pid_t pid = fork();
19    if (pid < 0)
20        return 1;
21    if (pid == 0) {
22        for (size_t i = 0; i < length; i += 4096)
23            area[i]++;
24        _exit(area[0] == 2 ? 0 : 2);
25    }
26
27    int status;
28    if (waitpid(pid, &status, 0) < 0)
29        return 1;
30    printf("parent=%u child_status=%d\n", area[0], WEXITSTATUS(status));
31    return munmap(area, length) != 0;
32}

Code notes

Source line 12MAP_PRIVATE | MAP_ANONYMOUS

Creates a private mapping unrelated to a file. The pages are initially shared after fork, but a write by one process is not visible to the other.

Source line 15i += 4096

The example writes one byte per typical 4 KiB page to fault in the physical pages beforehand. A real program must obtain the page size with sysconf(_SC_PAGESIZE).

Source line 18pid_t pid = fork

The parent receives the child PID and the child receives 0; both execution flows start at the same following instruction.

Source line 23area[i]++;

Each first write by the child triggers a write-protect fault and creates a private physical page, which is the event being observed.

Source line 24_exit(area[0]

Uses a syscall-level exit in the post-fork child so it does not flush inherited stdio buffers again.

Detailed behavior

01

What gets copied is primarily the mapping policy, not page contents

A VMA describes an address range, protections, and file or anonymous backing. fork duplicates the VMA tree and page tables, and treats the PTEs of writable private mappings as read-only on both sides to induce write faults.

MAP_SHARED mappings and genuinely shared memory are not subject to COW; writes remain visible through the same backing page.

02

Multithreaded fork has a narrow safe interval

Only the thread that calls fork remains in the child. A userspace mutex held by another thread may be copied in the locked state, but no thread remains to unlock it.

Before exec, the child should call only async-signal-safe functions. Complete complex preparation in the parent or consider posix_spawn.

03

Use PSS to measure memory usage

Simply adding parent and child RSS counts shared COW pages twice. Compare Pss, Private_Dirty, and Shared_Dirty in /proc/PID/smaps_rollup immediately after fork and after the child writes.

When transparent huge pages are enabled, the split or copy granularity of one write fault affects the observed result.

Objects and lifetimes

ObjectCreation and releaseValues to inspect
task_structCreated in copy_process and finally released in release_taskpid, state, files/mm pointer
mm_structDuplicated at fork and released when the last mm user departsVMA, page table, mm_users
COW pageShared before fork and separated into private pages on a write faultmapcount, PSS, dirty

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
fork returns ENOMEM/EAGAINmemory commit, pid/cgroup/user process limitulimit -u, pids.current, or overcommit settings
The child deadlocksA userspace lock is held by a thread that disappearedInspect pthread_atfork and the list of calls made by the child
Latency spikes after forkPage-table copying and COW faultsperf stat page-faults, smaps_rollup

Verify it yourself

  1. Insert a sleep before the child's write loop and compare Pss in the parent and child smaps_rollup immediately after fork.
  2. Use perf stat to compare minor-faults with the write loop removed and with it retained.
  3. Change MAP_PRIVATE to MAP_SHARED and observe how area[0] changes in the parent.
Run./fork_cow
Tracestrace -f -e trace=clone,wait4,mmap,munmap ./fork_cow

Primary sources