Copy-on-Write in Linux Kernel
Free Linux Kernel Development Course — Kernel Memory Management, Lecture 12
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 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
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
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:
- 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.
- 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.
- Both processes continue running normally as long as they only read that memory. No physical copying has happened yet.
- 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.
- 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.
- The write instruction is retried and now succeeds against the new, private page. The other process is untouched and keeps using the original page.
Before and After a CoW Write
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 byexec()— 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.
