Copy-on-Write in Linux Kernel-Free Linux Device Drivers Tutorial

PREV_LEC  |  NEXT_LEC

Copy-on-Write in Linux Kernel

Free Linux Kernel Development Course — Kernel Memory Management, Lecture 12

Chapter: Kernel Memory Management
Level: Intermediate
Kernel: 6.x mainline

Copy-on-write linux kernel handling, usually shortened to CoW, is one of the cleverest optimizations in the whole memory management subsystem. It is the reason a call to fork(), which in theory duplicates an entire process’s memory, actually completes in a few microseconds instead of copying megabytes of RAM. In this free linux kernel development course lecture, we will walk through exactly how CoW works at the page-table level, and build a small user-space and kernel-space example pair to observe it in action.

free embedded systems course
free linux development course
free linux device drivers course
free linux kernel development course
copy-on-write linux kernel
linux fork memory

What You Will Learn

Why fork() does not copy all memory upfront
How a read-only PTE traps a write
The role of do_wp_page()
VMA marking and page fault interaction
Observing CoW with a simple demo program

Prerequisites

Completed the vmalloc and page faults lecture
Basic understanding of fork() and processes
Familiarity with page table entries (PTE)

The Problem CoW Solves

When a process calls fork(), the kernel creates a near-identical child process. Naively, that would mean copying every single page of the parent’s address space — stack, heap, data segments, everything — into a brand-new set of physical pages for the child. For a process using several hundred megabytes of RAM, that copy would be slow and, in many cases, completely wasted effort, because most programs call exec() immediately after fork() and throw away the copied memory anyway.

Copy-on-write linux kernel design avoids this by sharing physical pages between parent and child at first, and only creating a private copy of a page the moment either process actually writes to it. Until that write happens, parent and child both point at the exact same physical memory, and the “copy” step never runs at all.

How CoW Works Step by Step

Right after fork(), here is what actually happens under the hood:

  1. The child gets its own page table, but the entries are copied from the parent’s page table so that they point at the same physical pages.
  2. Every one of those shared page table entries is marked read-only, even for pages that were originally writable. This is true for both the parent’s and the child’s copy of the entry.
  3. Both processes continue running normally as long as they only read that memory. No physical copying has happened yet.
  4. The first time either process tries to write to one of those shared pages, the CPU raises a page fault, because the PTE is marked read-only.
  5. The kernel’s fault handler recognizes that this is a CoW page (not a real permission error), allocates a brand-new physical page, copies the old content into it, updates the faulting process’s PTE to point at the new page and marks it writable, then flushes the stale TLB entry.
  6. The write instruction is retried and now succeeds against the new, private page. The other process is untouched and keeps using the original page.
Copy-on-Write timeline
fork() called
Parent + child share pages (read-only PTEs)
Child writes to a page
Page fault raised (write to RO page)
New page allocated + copied, PTE updated

Before and After a CoW Write

Physical page sharing

Before the write

Parent PTE → Physical Page A (read-only)
Child PTE → Physical Page A (read-only)

After the child writes

Parent PTE → Physical Page A (unchanged)
Child PTE → Physical Page B (private, writable copy)

The Role of do_wp_page()

The function that actually performs the copy inside the fault handler is commonly referred to by its historical name, do_wp_page() (“write-protect page”). It is invoked whenever the fault handler detects a write attempt against a read-only VMA-backed page that is marked for CoW. Its job is straightforward: allocate a new page, copy the old content byte-for-byte, install the new page in the faulting process’s page table with write permission enabled, and invalidate the corresponding TLB entry so the CPU does not keep using the stale mapping.

Because this whole mechanism piggybacks on the exact same page fault infrastructure covered in the previous lecture, CoW is really nothing more than a deliberate, planned use of a write-protection trap — the kernel intentionally sets up a fault it knows how to resolve cheaply.

Original Example: Observing CoW From User Space

The following original example forks a child process, has both processes read a shared buffer, and only then makes the child write to it — at which point the private copy is created. Compile it as a normal user-space C program.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#define EP_BUF_SIZE (4096)

int main(void)
{
    char *buf = malloc(EP_BUF_SIZE);
    if (!buf) {
        perror("malloc");
        return 1;
    }

    snprintf(buf, EP_BUF_SIZE, "shared before fork");
    printf("Parent: buffer at %p, content = \"%s\"\n", (void *)buf, buf);

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        return 1;
    }

    if (pid == 0) {
        /* Child: still reading only, same physical page as parent */
        printf("Child : buffer at %p, content = \"%s\"\n", (void *)buf, buf);

        /* First write triggers the CoW page fault and private copy */
        snprintf(buf, EP_BUF_SIZE, "modified by child");
        printf("Child : after write, content = \"%s\"\n", buf);
        _exit(0);
    } else {
        waitpid(pid, NULL, 0);
        /* Parent's page was never touched by the child's write */
        printf("Parent: after child exit, content = \"%s\"\n", buf);
    }

    free(buf);
    return 0;
}

Build and Run

$ gcc -o ep_cow_demo ep_cow_demo.c
$ ./ep_cow_demo

Expected Output

Parent: buffer at 0x5578a1b3f2a0, content = "shared before fork"
Child : buffer at 0x5578a1b3f2a0, content = "shared before fork"
Child : after write, content = "modified by child"
Parent: after child exit, content = "shared before fork"

Notice the virtual address printed by both processes is identical — that is expected, since each process has its own address space and the same virtual address can legitimately map to different (or, before the write, the same) physical pages. The important result is the last line: the parent’s copy of the string is untouched even though the child modified “the same” buffer, because the write triggered a private copy for the child only.

Watching the Fault Count

You can confirm the extra page fault caused by the CoW write using /usr/bin/time, which reports minor page faults (faults resolved without disk I/O — exactly the CoW case):

$ /usr/bin/time -v ./ep_cow_demo 2>&1 | grep "Minor"
Minor (reclaiming a frame) page faults: 3

The exact count varies by libc and loader behavior, but you should see minor faults corresponding to the buffer touch, confirming the CoW path was exercised rather than a real copy happening at fork() time.

Real-World Use Cases

  • fork() followed by exec() — the extremely common shell/process-launch pattern relies entirely on CoW to make fork cheap.
  • vfork()-style semantics for quick spawn-and-replace workloads.
  • Checkpoint/restore and container snapshot tools use CoW-style page sharing to keep memory duplication low.
  • Some file-backed private mappings (MAP_PRIVATE) use the same write-protect-then-fault trick for their own copy semantics.

Common Mistakes

Mistake Why It’s a Problem Fix
Assuming fork() is always cheap regardless of what happens after Heavy post-fork writes still trigger real copies, just deferred Measure actual write patterns, not just fork() cost
Confusing CoW with true copy-at-fork semantics Leads to wrong assumptions about memory usage right after fork() Remember pages are shared until first write
Ignoring minor fault overhead in tight loops after fork() Repeated writes to many shared pages cause many CoW faults at once Pre-touch pages deliberately if predictable, or avoid unnecessary post-fork writes

Best Practices

  • Let CoW do its job — avoid manually pre-copying memory after fork() unless you have measured a real need.
  • When forking memory-heavy processes intended to exec() immediately, CoW combined with exec()’s fresh address space is usually the most efficient path.
  • Be aware that a burst of writes right after fork() can generate a burst of page faults; this is a known, expected cost of the CoW model, not a bug.

Performance Considerations

CoW turns an O(memory size) copy at fork() time into an O(1) page-table setup, deferring the real cost to individual page faults spread out over time — and often avoiding the cost entirely, since many forked processes call exec() before writing anything.

Security Considerations

Because CoW pages are shared read-only between processes until a write occurs, the kernel must be strict about only allowing legitimate owners of a VMA to trigger the write-fault path. This is enforced through VMA permission checks that run before do_wp_page() is ever invoked, preventing one process from forcing a write into another’s private copy.

Summary / Key Takeaways

  • fork() shares physical pages between parent and child instead of copying them immediately.
  • Shared pages are marked read-only; a write from either side triggers a page fault.
  • The fault handler (do_wp_page()) allocates a new page, copies the content, and updates only the faulting process’s page table.
  • CoW is a deliberate, efficient reuse of the same page fault mechanism covered in the previous lecture.

Conclusion

Copy-on-write linux kernel design is a great example of turning a hardware trap — the page fault — into a deliberate performance optimization instead of treating it purely as an error condition. Once you understand vmalloc’s lazy allocation and the page fault handler from the previous lecture, CoW simply falls into place as a natural extension of the same idea. Next, we shift focus from RAM-oriented memory management to how the kernel talks to hardware registers through I/O memory.

Frequently Asked Questions

Does fork() copy all of a process’s memory immediately?

No. Physical pages are shared between parent and child at first; a private copy is only made when either side writes to a shared page.

What triggers the actual copy in Copy-on-Write?

A write access to a page table entry that has been deliberately marked read-only for CoW sharing triggers a page fault, which the handler resolves by copying.

What is do_wp_page() responsible for?

It allocates a new physical page, copies the old page’s content into it, and updates the faulting process’s page table entry to point at the new writable page.

Is Copy-on-Write only used by fork()?

fork() is the classic case, but the same write-protect-then-fault technique is used elsewhere, including some private file-backed mappings.

Does the parent process see changes the child makes after a CoW write?

No. Once the child’s write triggers CoW, the child gets its own private physical page and the parent’s original page is left untouched.

Why does CoW make fork() fast?

Because fork() only needs to set up page table entries pointing at existing pages, not copy actual memory contents, deferring any real copying to the first write.

 

Continue the Free Linux Kernel Development Course

Next up: talking to hardware with Port I/O and Memory-Mapped I/O.

PREV_LEC  |  NEXT_LEC

Leave a Reply

Your email address will not be published. Required fields are marked *