Copy-on-Write fork() and Copying Page Faults
Copy-on-write fork() creates a child process by sharing the parent’s memory pages at first, then copying an individual page only when either process tries to change it. Page-table permissions make that later write produce a page fault, giving the kernel a chance to perform the copy safely.
The problem fork() solves
A process needs an address space: the memory it can access, including its program code, heap, stack, and mapped files. When a process calls fork(), Linux creates a second process whose initial address space looks like the first one.
Without copy-on-write, fork() would need to allocate new physical memory and copy every page immediately. A large process might have gigabytes of memory mapped, even if it is about to replace its entire program with another one using exec(). Copying all that memory would make the common “fork, then exec” pattern slow and wasteful.
Copy-on-write avoids that upfront cost. The parent and child initially use the same physical memory. The cost of copying is paid only for pages that are actually modified.
fork()copies the process’s memory view immediately, not necessarily the memory contents.
The two kinds of address involved
A program uses virtual addresses: the addresses it sees in pointers and machine instructions. The processor translates each virtual address to a physical address, which identifies a location in RAM.
That translation is described by a page table, a data structure maintained by the operating system and consulted by the processor. Memory is divided into fixed-size blocks called pages; the corresponding blocks in physical memory are called page frames. A page-table entry, or PTE, records which physical frame backs a virtual page and what operations are allowed, such as reading, writing, or executing.
When Linux performs a copy-on-write fork, it does roughly this:
- It creates page-table structures for the child that describe the same virtual memory layout as the parent.
- For private, writable pages, both sets of page tables point to the same physical page frames.
- It marks those PTEs as read-only in both the parent and child, even if the original mapping was writable.
- It records that each shared physical frame has multiple users, using a reference count—the number of mappings or owners currently referring to it.
The page contents are not copied during this setup. Code and data that neither process changes can remain shared indefinitely.
The protection change is essential. If both processes retained write permission, the processor would allow one process to modify a shared frame and the other process would see the modification. That would violate the normal fork() rule that each process has its own private memory.
The write that triggers a copy
Suppose the child executes an instruction that stores a new value into one of its writable variables. The processor walks the child’s page table, finds the relevant PTE, and sees that the page is not writable. It raises a page fault, an exception meaning that the requested memory access cannot proceed under the current translation and permissions.
This does not necessarily mean the page is missing from RAM. In this case, the page is present; the fault is being used as a controlled notification to the kernel.
Linux’s fault handler examines the address and the reason for the fault. It checks that the access is a legitimate write to a private mapping. A write to a genuinely read-only page, such as protected code, should instead fail—typically producing SIGSEGV for the process.
For a valid copy-on-write write, the kernel then:
- Allocates a new physical page frame.
- Copies the old frame’s contents into the new frame.
- Changes the faulting process’s PTE to point to the new frame.
- Marks that PTE writable.
- Decrements the old frame’s reference count.
- Updates or invalidates the processor’s cached translation for that virtual address.
The instruction that caused the fault is then restarted. It writes into the new private frame as if nothing unusual happened. The other process still points to the original frame and continues to see the old contents.
Either process can be the first one to write. If the parent writes first, the parent receives the new copy and the child keeps the original. If both eventually write, each gets its own copy. If neither writes, there is only one physical frame for both processes to read.
What is and is not shared
Copy-on-write applies to memory that is private to the process, including typical heap and stack pages. Shared memory mappings are different: their purpose is for processes to observe each other’s writes, so Linux does not turn those writes into private copies in the same way. Read-only mappings can remain shared because no process is allowed to modify them.
The same general idea can apply to private mappings backed by files. A process can initially use data from a shared physical page, but a write to a private mapping causes a private memory version to be created rather than changing the underlying file or other process’s view.
Why this makes process creation practical
The immediate work of fork() is primarily creating and populating page-table structures and bookkeeping, not copying the entire address space. That makes process creation feasible even when the parent has a large memory footprint. The actual memory-copy cost is spread across the pages that the processes really modify.
This is especially valuable for the traditional Unix pattern in which a process calls fork() and the child soon calls exec(). The child may replace its address space before touching most of the inherited pages, so copying those pages would have accomplished nothing.
Copy-on-write is not free: page tables still consume memory, and the first write to each shared page incurs a fault, an allocation, and a copy. Large processes or workloads that immediately rewrite most of their memory can therefore pay substantial costs after the fork. But compared with eagerly duplicating every page, delaying copying until a write is necessary is usually a much better trade-off—and it is the behavior behind many Linux performance discussions involving fork(), “COW faults,” or unexpected memory growth after a process is duplicated.