TL;DR — Copy‑on‑Write (CoW) lets the Linux kernel share pages between processes until a write occurs, dramatically reducing memory pressure. Understanding the page‑fault path, the role of the mm_struct, and the interaction with modern subsystems like OverlayFS lets you tune latency and throughput for container‑heavy workloads.

Linux engineers constantly wrestle with the trade‑off between memory efficiency and latency. The classic example is the fork() system call: instead of copying an entire address space, the kernel marks pages read‑only and defers duplication until a process actually writes. This “copy‑on‑write” trick has been in the kernel since the early 1990s, but recent evolutions—such as transparent huge pages (THP), the kcmp syscall, and the rise of container overlays—have given CoW new performance dimensions. In this post we dissect the kernel’s CoW implementation, walk through the relevant data structures, and surface practical knobs you can adjust in production environments.

How Copy‑on‑Write Works at the Kernel Level

The Fork Path and Page Table Duplication

When a process calls fork(), the kernel performs the following high‑level steps:

  1. Allocate a new task_struct for the child.
  2. Clone the mm_struct (memory descriptor) using copy_mm().
  3. Mark every VMA (virtual memory area) as read‑only by clearing the VM_WRITE flag.
  4. Increment the reference count on each struct page that backs the address space.

The crucial line in copy_mm() looks roughly like this (simplified):

static int copy_mm(struct task_struct *dst, struct task_struct *src)
{
    dst->mm = dup_mm(src->mm);
    if (!dst->mm)
        return -ENOMEM;

    /* Increment the reference count on each page */
    for_each_vma(dst->mm, vma) {
        vma->vm_flags &= ~VM_WRITE;
    }
    return 0;
}

Note – The actual kernel code is more defensive, handling special VMAs (e.g., VM_GROWSDOWN) and NUMA policies, but the essence is the same.

Because the child’s page tables point to the same physical pages as the parent, the memory overhead of a fork() is essentially constant, regardless of the parent’s size. The cost is paid later, when either process attempts to write.

Page Fault Handling: The Write Path

A write to a CoW‑shared page triggers a major page fault. The handler (do_page_fault) eventually calls handle_cow_fault(), which:

  1. Checks that the faulting VMA is marked VM_WRITE.
  2. Allocates a new page (alloc_page()).
  3. Copies the contents of the original page (copy_page()).
  4. Updates the child’s page table entry to point to the new page.
  5. Decrements the reference count on the original page.

A condensed version of the core logic:

static int handle_cow_fault(struct vm_area_struct *vma,
                            unsigned long address, unsigned long error_code)
{
    struct page *old_page, *new_page;
    pte_t *pte = get_pte(vma, address);
    old_page = vm_normal_page(vma, address, *pte);
    new_page = alloc_page(GFP_KERNEL);
    if (!new_page)
        return VM_FAULT_OOM;

    copy_page(new_page, old_page);
    set_pte_at(vma->vm_mm, address, pte, mk_pte(new_page, vma->vm_page_prot));
    put_page(old_page);
    return VM_FAULT_MAJOR;
}

The latency of this path depends on several factors:

  • Cache locality – If the original page is hot, copying it may evict useful cache lines.
  • Page size – THP (2 MiB) copies are more expensive than regular 4 KiB pages, but they reduce TLB pressure.
  • NUMA placement – Allocating the new page on the same node as the faulting CPU avoids remote memory latency.

Architecture: CoW in Modern Subsystems

OverlayFS and Container Images

Docker and Kubernetes rely heavily on OverlayFS, a union filesystem that layers a read‑only lower filesystem (the image) with a writable upper layer. OverlayFS itself uses CoW at the file‑system level: when a process modifies a file that exists in the lower layer, the kernel copies the entire file (or a block, depending on the configuration) into the upper layer.

The interaction between fork()‑based CoW and OverlayFS CoW can be subtle:

  • A container that spawns many short‑lived processes may see double CoW—first at the page level, then at the file block level.
  • The overlay2 driver mitigates this by using metadata‑only copy‑up for small writes, but large writes still trigger full page duplication.

Understanding this stack helps you decide when to enable overlayfs’s metacopy=on flag (available since Linux 5.9) to avoid unnecessary block copies.

Transparent Huge Pages (THP) and CoW

THP aggregates 512 contiguous 4 KiB pages into a single 2 MiB entry. While THP reduces TLB misses, it complicates CoW:

  • A write to any byte inside a THP‑backed region forces the kernel to split the huge page into regular pages before copying.
  • The split operation adds ~10–20 µs of latency per fault, which can be noticeable in latency‑sensitive services (e.g., high‑frequency trading).

You can query the THP status of a process via /proc/<pid>/smaps and tune it with /sys/kernel/mm/transparent_hugepage/enabled. In many production workloads, disabling THP for processes that frequently fork() yields a measurable reduction in fork‑induced latency.

Patterns in Production: When CoW Helps, When It Hurts

1. Fork‑Heavy Workloads (e.g., Web Servers)

Nginx and Apache prefork models create a pool of worker processes at startup. Because the workers share the same code and static assets, CoW keeps the resident set low. Tips:

ActionImpact
Set ulimit -n high enough to avoid file‑descriptor exhaustion.Prevents fallback to select() loops that increase CPU.
Pin workers to specific CPUs (taskset) and enable numactl --localalloc.Keeps page copies on the local node, reducing remote latency.
Disable THP (echo never > /sys/kernel/mm/transparent_hugepage/enabled).Avoids split‑on‑write penalties during heavy request spikes.

2. Container‑Orchestrated Jobs (e.g., CI/CD runners)

GitLab Runner spawns a fresh container for each job, often using docker run --rm. The container runtime copies the image layers into a new overlay mount, then the job’s process may fork() (e.g., make -j). In this scenario:

  • Avoid fork() in build scripts where possible; use exec to replace the process instead.
  • Leverage --tmpfs /tmp:size=1G to place temporary files in RAM, sidestepping CoW on the overlay’s lower layer.
  • Tune vm.overcommit_memory=1 to allow the kernel to allocate memory without strict checks, reducing OOM kills during massive parallel builds.

3. In‑Memory Databases (e.g., Redis, Memcached)

These services rarely fork(), but they use fork() for RDB snapshots (Redis) or fork‑based persistence. A snapshot copies the entire dataset via CoW, which can cause copy‑on‑write storms if the dataset is large and the server continues to write.

Mitigation strategies:

  • Use redis.conf’s copy-on-write option (no for fork()‑free snapshots) introduced in Redis 7.0.
  • Enable vm.swappiness=1 to keep pages in RAM longer, reducing swap‑induced copy‑on‑write latency.
  • For Memcached, run memcached -M to disable memory overcommit and force explicit allocation failures instead of silent CoW.

Measuring CoW Impact

Using perf to Trace Page Faults

perf record -e page-faults -p $(pidof myservice) -- sleep 30
perf report --stdio | grep major

The page-faults event captures both minor and major faults. Filtering for “major” isolates the CoW path because a true CoW fault is a major fault (it involves disk I/O or page allocation).

Inspecting /proc/<pid>/smaps

grep -i "private_dirty" /proc/$(pidof myservice)/smaps | awk '{sum+=$2} END {print sum/1024 " KiB"}'

Private_Dirty reflects pages that have been duplicated via CoW. Tracking this metric over time tells you how quickly your workload is “paying back” the memory saved by fork().

Tuning the Kernel for Better CoW Performance

  1. Increase vm.max_map_count if you run many processes with large address spaces (common in Java microservices). This prevents fork() failures due to map limits.
  2. Adjust vm.min_free_kbytes to keep a safety buffer of free memory, reducing the chance that a CoW fault triggers a global OOM.
  3. Enable cgroup v2 memory pressure notifications (memory.pressure_level) to proactively scale down worker pools before CoW spikes become problematic.
  4. Consider kcmp (kernel compare) introduced in Linux 5.8 to quickly compare memory regions without duplicating pages—useful for deduplication services.

Key Takeaways

  • Copy‑on‑Write lets fork() share pages until a write, saving memory but introducing a major page‑fault cost on first write.
  • Modern subsystems (OverlayFS, THP) interact with CoW; disabling THP for fork‑heavy workloads often yields lower latency.
  • Production patterns—prefork web servers, container CI runners, and in‑memory databases—benefit from targeted CoW tuning.
  • Use perf, smaps, and cgroup memory pressure to measure CoW impact; then apply kernel knobs (vm.overcommit_memory, vm.min_free_kbytes) to mitigate stalls.
  • When building containers, prefer exec over fork and leverage overlay options like metacopy=on to avoid double duplication.

Further Reading