arthrop0d

Brief articles on varied topics.

Linux Copy-on-Write Across forked Processes

linuxoperating systemsperformance

Linux implements fork() copy-on-write by letting the parent and child initially point to the same physical memory, then copying an individual page only when one process tries to modify it. The processes appear to have separate memory, but Linux postpones the expensive copying until it is necessary.

The problem fork has to solve

A process sees a private address space: a range of virtual addresses that its program uses for code, stack, heap, and libraries. The operating system maps those addresses to physical pages, fixed-size regions of RAM, commonly 4 KiB.

A straightforward fork() implementation would create a complete copy of the parent’s physical memory before returning. That is costly for a large process, and often wasteful. A common pattern is:

pid_t child = fork();
if (child == 0) {
    execl("/usr/bin/worker", "worker", NULL);
}

The child may immediately call exec, which replaces its address space with a new program. Copying hundreds of megabytes first would provide no lasting benefit. Even when both processes continue running, much of their memory may be read but never changed.

Copy-on-write, usually abbreviated COW, solves this by sharing pages while they are identical and separating them only when a process writes.

What fork changes

Linux represents a process’s virtual-to-physical mappings in page tables. A page-table entry, or PTE, describes where a virtual page is stored and includes permission bits such as whether the page can be written.

During fork(), Linux creates page-table structures for the child that initially refer to the same physical pages as the parent’s structures. It does not copy the contents of every page. The kernel also records how many mappings refer to each physical page, using a reference count so it knows when a page can finally be freed.

The important protection step is that Linux makes the shared private pages read-only in both processes’ PTEs. If a page was writable before fork(), its PTE is changed so that neither the parent nor child may write it directly. The page remains present and readable; only writes are blocked.

This is a page-table operation, not a change to the program’s pointer values. Both processes can still read the same virtual address, and that address can resolve to the same physical page. Their page tables simply describe the sharing and enforce the temporary read-only state.

Linux applies this behavior to private memory, including ordinary anonymous heap and stack pages and private file mappings. A MAP_SHARED mapping has different semantics: writes are intended to be visible through the shared mapping, so it is not treated as an independent COW copy in the same way.

The write and the page fault

Suppose the child executes an instruction that stores a value into a page shared with the parent. The processor consults the child’s PTE, sees that the page is present but not writable, and raises a page fault—a transfer from the program into the kernel because the memory access cannot proceed under the current mapping.

A page fault does not always mean invalid memory. It can also be the expected notification that lets Linux implement features such as COW.

The kernel examines the faulting address and the process’s memory-region metadata to determine whether the process is allowed to write there. If the address belongs to a valid private writable region, Linux handles the COW case approximately as follows:

  1. It allocates a new physical page.
  2. It copies the old page’s contents into the new page.
  3. It changes the faulting process’s PTE to point to the new page and marks that mapping writable.
  4. It reduces the reference count on the old page.
  5. It updates the processor’s cached translation if necessary, then retries the interrupted instruction.

The retry is transparent to the program. The child’s store now modifies its private copy, while the parent continues to see the original contents. If the parent writes later, it takes a similar fault and receives its own copy.

The processor often caches virtual-to-physical translations in a translation lookaside buffer (TLB). Because Linux changed a PTE, it must ensure a stale TLB entry cannot continue to allow or deny access incorrectly. On multiprocessor systems, this can include notifying other CPUs that may have cached the mapping; this operation is called a TLB shootdown.

Not every fault allocates a page

The kernel can optimize some cases. If the shared page is no longer mapped by anyone else, Linux may be able to make the existing page writable for the faulting process instead of allocating and copying another page. The result is still correct because no other process needs the old contents.

Conversely, a write fault can fail. If the address is outside a writable memory region, or the mapping is genuinely read-only, Linux cannot use COW to authorize the write. It terminates the access with the familiar invalid-memory signal, typically SIGSEGV.

Physical memory is also not necessarily allocated for every virtual page at fork() time. For example, untouched regions may have no physical page yet. A later read or write can trigger a different kind of page fault that creates or loads the required page. COW concerns the sharing and separation of pages that already have contents.

The performance trade-off

COW makes fork() fast relative to eagerly duplicating the entire address space. Its immediate costs are the new page-table structures, bookkeeping, and the work of marking shared writable mappings read-only. Its deferred costs appear only for pages that are actually written: a fault, allocation, page copy, and page-table update for each affected page.

That explains why a process can show substantial virtual memory after fork() without immediately consuming an equal amount of additional physical RAM. It also explains why a workload that writes heavily in both parent and child can eventually approach the memory cost of two independent processes—and may experience page-fault and allocation overhead while getting there.

← All articles