Step 1 of 5 · Reading · ~3 min
Read
Processes & Scheduling
fork() and Copy-on-Write
fork promises a child with a private copy of the parent's whole address space. Copying 4 GiB of memory on every fork would make a shell, which forks for every command and usually replaces the child with a new program a moment later, unbearably slow. Real kernels never copy at fork. They share the frames, mark them read-only, and copy a single page only if somebody tries to write to it. In this lesson your program is the part of the kernel that does this: page tables with flag bits, a reference count per frame, and the page-fault handler.
The idea
- At fork: give the child its own page table whose entries point at the same physical frames as the parent's. Increase each frame's reference count. For every writable page, clear the writable bit in both page tables and set a software flag, COW, meaning "this is read-only only because it is shared; a write should get a private copy".
- On read: nothing special, both processes read the shared frame.
- On write: the CPU raises a page fault, because the PTE is read-only. The handler decides:
| Situation | Action |
|---|---|
| address not mapped at all | genuine error: kill the process (SEGV) |
| page is read-only and not COW (code, constants) | genuine error: SEGV |
| COW page, frame's reference count is above 1 | allocate a new frame, copy the contents, point this PTE at the copy, set writable, clear COW, decrement the old frame's count |
| COW page, reference count is 1 | nobody else shares it any more: just make it writable and clear COW, no copy |
The faulting instruction then re-executes and succeeds; the process never learns anything happened.
Why the reference count matters
Consider parent P and child C sharing frame 5 (count 2). C writes: it gets frame 6 (count 1), and frame 5's count drops to 1. P's PTE still says read-only + COW, but P is now the only owner. When P writes it must not copy again: that would waste a frame and a copy for nothing. The count is how the handler tells the cases apart. When a process exits, every frame it maps loses one reference, and a frame whose count reaches 0 returns to the free pool.
PTE flag bits for this: x86 reserves bits 9-11 of every entry for the OS, which is where Linux and others keep their COW marker.
A worked trace
Four frames. Process 1 has virtual pages 0 (rw) and 1 (ro, code).
| Command | Effect | Output |
|---|---|---|
MAP 1 0 rw | frame 0 | frame 0 |
MAP 1 1 ro | frame 1 | frame 1 |
FORK 1 2 | share both; page 0 becomes W=0, COW=1 in both processes; frame 0 and 1 have 2 references | OK |
WRITE 2 0 | COW, count 2: copy to frame 2 | COPY 0 -> 2 |
WRITE 1 0 | COW, count 1: reuse | REUSE 0 |
WRITE 1 1 | read-only, not COW | SEGV |
Only one extra frame was ever allocated.
Where you meet this
- A shell forks and
execs: nearly nothing is ever copied. - Redis forks to take a snapshot: the child holds a frozen view, and the parent pays one page copy for each page it modifies during the snapshot, so a write-heavy workload can nearly double memory use.
- Zero-page tricks, shared libraries and
mmaped files use the same machinery.
Common mistakes
- Forgetting to clear the writable bit in the parent as well as the child.
- Copying on every write to a COW page instead of checking the reference count first.
- Decrementing the count on the wrong frame (the new one rather than the old one).
- Making read-only code pages COW. A write to them is an error, not a copy.
Your exercise
The starter holds the page tables and prints results. You write fork (sharing, flags, counts) and the write-fault handler.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…