Try to compile this and run it. Don't worry that it's C; it's C, thus it's easy to understand things in it.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int value = 0;
int main(void) {
printf("before fork- &value = %p\n", (void *)&value);
fflush(stdout);
pid_t pid = fork();
int step = (pid == 0) ? 1 : 100;
for (int i = 0; i < 5; i++) {
value += step;
printf("pid %d: &value = %p, value = %d\n",
getpid(), (void *)&value, value);
sleep(1);
}
if (pid > 0) wait(NULL);
return 0;
}
Here's what I got:
before fork: &value = 0x104720000
pid 99257: &value = 0x104720000, value = 100
pid 99314: &value = 0x104720000, value = 1
pid 99314: &value = 0x104720000, value = 2
pid 99257: &value = 0x104720000, value = 200
pid 99314: &value = 0x104720000, value = 3
pid 99257: &value = 0x104720000, value = 300
pid 99314: &value = 0x104720000, value = 4
pid 99257: &value = 0x104720000, value = 400
pid 99314: &value = 0x104720000, value = 5
pid 99257: &value = 0x104720000, value = 500
Did you see it? No? No worries. value is a single global variable, declared at file scope. fork() makes a second process, and both processes proceed to update it , parent adding 100, child adding 1. And they're both reporting the same address: 0x104720000.
So if there is one address, being incremented by two processes, the output should look something like this:
pid 99257: value = 100
pid 99314: value = 101
pid 99257: value = 201
pid 99314: value = 202
pid 99257: value = 302
...
One counter, Incrementing, with the two processes taking turns depending on which one the scheduler runs. But what happened is that 0x104720000 contained a 3 and a 300 at the same moment.
fork() ?
When control hits
fork(), the kernel makes a duplicate of the process that called it, the Original one. And it is called parent, the duplicate is the child. On Linux, thefork()you call from libc is a thin wrapper over theclone()syscall; on macOS and the BSDs, it's its own syscall.
The child gets a fresh PID (process ID), and its PPID (parent process ID) is set to whoever forked it. Its CPU-time and resource counters reset to zero. File record locks held by the parent are not inherited. If either process later changes its working directory, the other doesn't notice.
What is carried over? the register state, the program counter, the file descriptor table, the environment, the umask, the user and group IDs, the working directory as it stood at that instant, and, most importantly for us, the entire address space. Every byte of memory the parent had, the child has too, laid out identically.
(Tiny piece of trivia while we're here: every process on your machine except one is somebody's child. On Linux, the root of the tree is PID 1,
systemdon most distros. On macOS, it'slaunchd. That process is spawned by the kernel at boot and everything else descends from it. Your shell is a child of your terminal, which is a child of the window server, and so on up.)
The file descriptor table is copied, but the open file descriptions it points into are shared. Which means parent and child share a file offset. If both inherit an open file and both write to it, they don't bash each other at byte zero; they advance one shared cursor, taking turns. Two processes but one position in a file. Now this is sharing. But the address space isn't. The address space is logically duplicated, the instant
fork()returns. Two separate copies ofvalueexplains 3 and 300 coexisting just fine. It does not explain them sharing an address.
Fork() copies ?
So what actually happened? Depends what you mean by "copied." Not a single byte of your program's actual data. The kernel did just duplicate a task struct and grow a whole new page table for the child, but that's the map, not the territory , new entries, not new pages. Parent and child are both looking at the same physical memory, the same actual page holding value, and the kernel has marked that memory read-only in both of them. The instant either process runs value += step, the processor tries to write to read-only memory and faults. The kernel catches the fault, and says, "well, that's expected," and it grabs a fresh piece of physical memory, copies the old contents across, points that one process at the copy, restores write permission, and restarts the instruction that just failed. The instruction runs a second time. This time it succeeds. To the process this does not register.
That is Copy-On-Write, universally abbreviated COW, and no, not the animal.
And notice who did the most of the work. fork()? Not really. The page fault handler. fork() itself did almost nothing, flipped some permission bits and returned. Which is why forking is really efficient, fork a four-gigabyte process and it copies nothing up front, defers everything until somebody actually writes, and then copies exactly one chunk at a time, and only the chunks your program actually touched. Fork and then immediately exec a different program, which is what your shell does every single time you run a command, and it often copies zero bytes.
Right. Mystery solved. Go home.
Except, read that back, because I slipped something past you.
I said the kernel "points that one process at the copy." Points what at the copy? And if the two processes walk away holding two physically different pieces of memory, then how on earth are they both still printing 0x104720000? Two different locations in physical RAM but one address? This can only mean one thing, and this is what this series is about. That number a lookup key, not a actual address in your RAM, and each process resolves it differently.