The demo below simulates a process with several memory pages. Each page starts shared between parent and child after a fork. Click Fork to create the child, then click any page to write to it and watch copy-on-write split it into two private copies.
<!-- COW demo: shows shared/private page state between parent and child processes -->
<p class="hint">A process owns memory pages. After fork(), parent and child share every page — no copy yet. Click a page to write to it: the kernel privately copies just that page (COW). Pages never written are never copied.</p>
<div class="arena" id="arena">
<div class="proc-col" id="col-parent">
<div class="proc-label">Parent process</div>
<div class="pages" id="pages-parent"></div>
</div>
<div class="proc-col hidden" id="col-child">
<div class="proc-label">Child process</div>
<div class="pages" id="pages-child"></div>
</div>
</div>
<div class="status" id="status">Click Fork to create a child process.</div>
<div class="btns">
<button id="btn-fork" type="button">Fork process</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="badge shared">S</span> Shared (no copy)
<span class="badge private-p">P</span> Parent private copy
<span class="badge private-c">C</span> Child private copy
</div>
/* Styles for the COW page-fork simulator */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.arena { display: flex; gap: 1.5rem; margin-bottom: .6rem; }
.proc-col { flex: 1; }
.proc-label { font-size: .8rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #555; margin-bottom: .4rem; }
.pages { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.page { border-radius: 8px; height: 56px; display: flex; flex-direction: column;
align-items: center; justify-content: center; font-size: .78rem; font-weight: 600;
cursor: pointer; border: 2px solid transparent; transition: all .15s;
user-select: none; position: relative; }
.page .pg-name { font-size: .7rem; opacity: .7; }
.page .pg-badge { position: absolute; top: 3px; right: 5px; font-size: .65rem;
font-weight: 800; }
/* Shared pages: blue — both processes point to the same physical frame */
.page.shared { background: #dbeafe; border-color: #93c5fd; color: #1e40af; }
.page.shared:hover { background: #bfdbfe; }
/* Parent private: green — parent has its own copy after a write */
.page.private-p { background: #dcfce7; border-color: #86efac; color: #166534; }
/* Child private: yellow — child has its own copy after a write */
.page.private-c { background: #fef9c3; border-color: #fde047; color: #713f12; }
.page.cow-anim { animation: cowpop .4s ease; }
@keyframes cowpop { 0%{transform:scale(1)} 40%{transform:scale(1.18)} 100%{transform:scale(1)} }
.hidden { visibility: hidden; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.ok { color: #166534; }
.status.info { color: #1e40af; }
.status.warn { color: #92400e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.legend { font-size: .78rem; color: #555; display: flex; gap: .8rem; flex-wrap: wrap;
align-items: center; }
.badge { border-radius: 4px; padding: 1px 5px; font-size: .7rem; font-weight: 800;
display: inline-block; }
.badge.shared { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
.badge.private-p { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.badge.private-c { background: #fef9c3; color: #713f12; border: 1px solid #fde047; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Copy-on-write simulator
// Each process tracks how many times it wrote to each page.
const PAGE_NAMES = ['A','B','C','D','E','F'];
const TOTAL = PAGE_NAMES.length;
// forked = whether fork() has been called
let forked = false;
// write count per page per process (0 = never written = still shared after fork)
const writes = { parent: new Array(TOTAL).fill(0), child: new Array(TOTAL).fill(0) };
// Page state categories
// shared = both point to same frame; private-p = parent has own copy; private-c = child has own copy
function pageState(i) {
// Determine the display state of page i.
if (!forked) return 'unforked';
const pw = writes.parent[i], cw = writes.child[i];
if (pw === 0 && cw === 0) return 'shared';
if (pw > 0 && cw === 0) return 'private-p';
if (cw > 0 && pw === 0) return 'private-c';
return 'both';
}
function render() {
// Re-draw both columns based on current write counts.
const pp = document.getElementById('pages-parent');
const pc = document.getElementById('pages-child');
pp.innerHTML = '';
if (forked) pc.innerHTML = '';
let sharedCount = 0;
PAGE_NAMES.forEach((name, i) => {
const st = pageState(i);
// Build the parent's page tile.
const dp = document.createElement('div');
dp.className = 'page ' + (st === 'unforked' || st === 'private-p' || st === 'both' ? (forked ? (writes.parent[i] > 0 ? 'private-p' : 'shared') : 'unforked') : 'shared');
if (forked && st === 'shared') sharedCount++;
const pBadge = !forked ? '' : (writes.parent[i] > 0 ? 'P' : 'S');
dp.innerHTML = `<span class="pg-name">Page ${name}</span><span class="pg-badge">${pBadge}</span>`;
if (forked) {
dp.title = 'Click to write from the parent';
dp.addEventListener('click', () => writeToPage('parent', i));
}
pp.appendChild(dp);
if (!forked) return;
// Build the child's page tile.
const dc = document.createElement('div');
const childClass = writes.child[i] > 0 ? 'private-c' : 'shared';
dc.className = 'page ' + childClass;
const cBadge = writes.child[i] > 0 ? 'C' : 'S';
dc.innerHTML = `<span class="pg-name">Page ${name}</span><span class="pg-badge">${cBadge}</span>`;
dc.title = 'Click to write from the child';
dc.addEventListener('click', () => writeToPage('child', i));
pc.appendChild(dc);
});
setStatus(forked
? i18nFmt('%shared% of %total% pages still shared — no memory copied yet.', { shared: sharedCount, total: TOTAL })
: 'Click Fork to create a child process.', forked ? 'info' : '');
}
function writeToPage(proc, i) {
// Simulate a write to page i by the given process; first write triggers COW.
writes[proc][i]++;
const colId = proc === 'parent' ? 'pages-parent' : 'pages-child';
const pages = document.getElementById(colId).children;
if (pages[i]) {
pages[i].classList.add('cow-anim');
pages[i].addEventListener('animationend', () => pages[i].classList.remove('cow-anim'), { once: true });
}
const wasCow = writes[proc][i] === 1;
if (wasCow) {
setStatus(i18nFmt('COW: page %page% copied for %proc%.', { page: PAGE_NAMES[i], proc: proc === 'parent' ? 'parent' : 'child' }), 'ok');
console.log(i18nFmt('COW split: page %page% now private for %proc%', { page: PAGE_NAMES[i], proc: proc }));
} else {
setStatus(i18nFmt('Page %page% already private — write proceeds normally.', { page: PAGE_NAMES[i] }), 'info');
}
render();
}
function doFork() {
// Simulate fork(): mark as forked, show child column, disable the fork button.
forked = true;
document.getElementById('col-child').classList.remove('hidden');
document.getElementById('btn-fork').disabled = true;
setStatus(i18nFmt('Forked! All %total% pages shared. Click pages to write.', { total: TOTAL }), 'ok');
console.log('fork() called — all pages marked shared (read-only), no memory copied');
render();
}
function doReset() {
// Reset everything back to the initial single-process state.
forked = false;
writes.parent.fill(0);
writes.child.fill(0);
document.getElementById('col-child').classList.add('hidden');
document.getElementById('btn-fork').disabled = false;
setStatus('Click Fork to create a child process.', '');
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
document.getElementById('btn-fork').addEventListener('click', doFork);
document.getElementById('btn-reset').addEventListener('click', doReset);
render();
<!-- COW demo: shows shared/private page state between parent and child processes -->
<p class="hint">A process owns memory pages. After fork(), parent and child share every page — no copy yet. Click a page to write to it: the kernel privately copies just that page (COW). Pages never written are never copied.</p>
<div class="arena" id="arena">
<div class="proc-col" id="col-parent">
<div class="proc-label">Parent process</div>
<div class="pages" id="pages-parent"></div>
</div>
<div class="proc-col hidden" id="col-child">
<div class="proc-label">Child process</div>
<div class="pages" id="pages-child"></div>
</div>
</div>
<div class="status" id="status">Click Fork to create a child process.</div>
<div class="btns">
<button id="btn-fork" type="button">Fork process</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="badge shared">S</span> Shared (no copy)
<span class="badge private-p">P</span> Parent private copy
<span class="badge private-c">C</span> Child private copy
</div>
/* Styles for the COW page-fork simulator */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.arena { display: flex; gap: 1.5rem; margin-bottom: .6rem; }
.proc-col { flex: 1; }
.proc-label { font-size: .8rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #555; margin-bottom: .4rem; }
.pages { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.page { border-radius: 8px; height: 56px; display: flex; flex-direction: column;
align-items: center; justify-content: center; font-size: .78rem; font-weight: 600;
cursor: pointer; border: 2px solid transparent; transition: all .15s;
user-select: none; position: relative; }
.page .pg-name { font-size: .7rem; opacity: .7; }
.page .pg-badge { position: absolute; top: 3px; right: 5px; font-size: .65rem;
font-weight: 800; }
/* Shared pages: blue — both processes point to the same physical frame */
.page.shared { background: #dbeafe; border-color: #93c5fd; color: #1e40af; }
.page.shared:hover { background: #bfdbfe; }
/* Parent private: green — parent has its own copy after a write */
.page.private-p { background: #dcfce7; border-color: #86efac; color: #166534; }
/* Child private: yellow — child has its own copy after a write */
.page.private-c { background: #fef9c3; border-color: #fde047; color: #713f12; }
.page.cow-anim { animation: cowpop .4s ease; }
@keyframes cowpop { 0%{transform:scale(1)} 40%{transform:scale(1.18)} 100%{transform:scale(1)} }
.hidden { visibility: hidden; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0; min-height: 1.4em; }
.status.ok { color: #166534; }
.status.info { color: #1e40af; }
.status.warn { color: #92400e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.legend { font-size: .78rem; color: #555; display: flex; gap: .8rem; flex-wrap: wrap;
align-items: center; }
.badge { border-radius: 4px; padding: 1px 5px; font-size: .7rem; font-weight: 800;
display: inline-block; }
.badge.shared { background: #dbeafe; color: #1e40af; border: 1px solid #93c5fd; }
.badge.private-p { background: #dcfce7; color: #166534; border: 1px solid #86efac; }
.badge.private-c { background: #fef9c3; color: #713f12; border: 1px solid #fde047; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Copy-on-write simulator
// Each process tracks how many times it wrote to each page.
const PAGE_NAMES = ['A','B','C','D','E','F'];
const TOTAL = PAGE_NAMES.length;
// forked = whether fork() has been called
let forked = false;
// write count per page per process (0 = never written = still shared after fork)
const writes = { parent: new Array(TOTAL).fill(0), child: new Array(TOTAL).fill(0) };
// Page state categories
// shared = both point to same frame; private-p = parent has own copy; private-c = child has own copy
function pageState(i) {
// Determine the display state of page i.
if (!forked) return 'unforked';
const pw = writes.parent[i], cw = writes.child[i];
if (pw === 0 && cw === 0) return 'shared';
if (pw > 0 && cw === 0) return 'private-p';
if (cw > 0 && pw === 0) return 'private-c';
return 'both';
}
function render() {
// Re-draw both columns based on current write counts.
const pp = document.getElementById('pages-parent');
const pc = document.getElementById('pages-child');
pp.innerHTML = '';
if (forked) pc.innerHTML = '';
let sharedCount = 0;
PAGE_NAMES.forEach((name, i) => {
const st = pageState(i);
// Build the parent's page tile.
const dp = document.createElement('div');
dp.className = 'page ' + (st === 'unforked' || st === 'private-p' || st === 'both' ? (forked ? (writes.parent[i] > 0 ? 'private-p' : 'shared') : 'unforked') : 'shared');
if (forked && st === 'shared') sharedCount++;
const pBadge = !forked ? '' : (writes.parent[i] > 0 ? 'P' : 'S');
dp.innerHTML = `<span class="pg-name">Page ${name}</span><span class="pg-badge">${pBadge}</span>`;
if (forked) {
dp.title = 'Click to write from the parent';
dp.addEventListener('click', () => writeToPage('parent', i));
}
pp.appendChild(dp);
if (!forked) return;
// Build the child's page tile.
const dc = document.createElement('div');
const childClass = writes.child[i] > 0 ? 'private-c' : 'shared';
dc.className = 'page ' + childClass;
const cBadge = writes.child[i] > 0 ? 'C' : 'S';
dc.innerHTML = `<span class="pg-name">Page ${name}</span><span class="pg-badge">${cBadge}</span>`;
dc.title = 'Click to write from the child';
dc.addEventListener('click', () => writeToPage('child', i));
pc.appendChild(dc);
});
setStatus(forked
? i18nFmt('%shared% of %total% pages still shared — no memory copied yet.', { shared: sharedCount, total: TOTAL })
: 'Click Fork to create a child process.', forked ? 'info' : '');
}
function writeToPage(proc, i) {
// Simulate a write to page i by the given process; first write triggers COW.
writes[proc][i]++;
const colId = proc === 'parent' ? 'pages-parent' : 'pages-child';
const pages = document.getElementById(colId).children;
if (pages[i]) {
pages[i].classList.add('cow-anim');
pages[i].addEventListener('animationend', () => pages[i].classList.remove('cow-anim'), { once: true });
}
const wasCow = writes[proc][i] === 1;
if (wasCow) {
setStatus(i18nFmt('COW: page %page% copied for %proc%.', { page: PAGE_NAMES[i], proc: proc === 'parent' ? 'parent' : 'child' }), 'ok');
console.log(i18nFmt('COW split: page %page% now private for %proc%', { page: PAGE_NAMES[i], proc: proc }));
} else {
setStatus(i18nFmt('Page %page% already private — write proceeds normally.', { page: PAGE_NAMES[i] }), 'info');
}
render();
}
function doFork() {
// Simulate fork(): mark as forked, show child column, disable the fork button.
forked = true;
document.getElementById('col-child').classList.remove('hidden');
document.getElementById('btn-fork').disabled = true;
setStatus(i18nFmt('Forked! All %total% pages shared. Click pages to write.', { total: TOTAL }), 'ok');
console.log('fork() called — all pages marked shared (read-only), no memory copied');
render();
}
function doReset() {
// Reset everything back to the initial single-process state.
forked = false;
writes.parent.fill(0);
writes.child.fill(0);
document.getElementById('col-child').classList.add('hidden');
document.getElementById('btn-fork').disabled = false;
setStatus('Click Fork to create a child process.', '');
render();
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
document.getElementById('btn-fork').addEventListener('click', doFork);
document.getElementById('btn-reset').addEventListener('click', doReset);
render();
Notice the asymmetry. The fork itself is instant regardless of how many pages exist — no memory is copied. Each write then pays a one-time split cost for that single page. If the child only reads and never writes, the total extra memory used is zero.
Comments
Loading comments...