Process Forking: Copy-on-Write and the Unix Model


Every time a Unix process calls fork(), the operating system appears to create a complete copy of the parent process—memory, file descriptors, execution state—and hand it to a new child process. This looks expensive. A typical application process might occupy hundreds of megabytes or even gigabytes of memory. Copying all of that on every fork would be prohibitively slow and wasteful, especially when many forked processes immediately call exec() to replace themselves with a different program.

Yet forking remains fast. The reason is copy-on-write, a memory management technique that delays and often avoids duplication entirely.

How Copy-on-Write Works

When a process forks, the kernel does not immediately duplicate the parent’s memory. Instead, both parent and child processes share the same physical memory pages. The kernel marks these pages as read-only in both address spaces, even if they were previously writable.

When either process attempts to write to a shared page, the hardware triggers a page fault. The kernel intercepts this fault, allocates a new physical page, copies the original page’s contents into it, updates the writing process’s page table to point to the new page, marks both pages as writable, and resumes execution. The write proceeds as if nothing happened, but now the two processes have independent copies of that page.

Pages that are never written remain shared indefinitely. If the child process immediately calls exec(), as is common in shell scripts and many server architectures, almost none of the parent’s memory is duplicated. The child discards its address space and loads a new program, and the kernel reclaims the shared pages.

Why Forking Still Matters

Many modern languages and frameworks avoid forking in favor of threading or async concurrency models. But forking remains the foundation of process isolation on Unix systems. Pre-fork server models, used by Apache httpd’s MPM prefork and Unicorn for Ruby, create a pool of worker processes at startup. Each worker inherits a shared memory footprint—framework code, configuration, shared libraries—but copy-on-write ensures that only modified pages diverge.

This model provides strong isolation. A crash or exploit in one worker does not affect others. Memory corruption is contained. Signal handling and resource limits are straightforward because each worker is an independent process with its own address space.

Container runtimes also rely on forking. Tools like runc fork a process, set up namespaces and cgroups, and exec the container entrypoint. Copy-on-write makes this cheap even when the runtime itself is a large Go binary.

The Cost That Remains

Copy-on-write eliminates most of the memory duplication cost, but forking is not free. The kernel must still copy page tables, which grow with the size of the address space. A process with a large virtual address space incurs a longer fork even if most of it is unused or shared.

File descriptor tables and signal handler state are duplicated. If the parent holds thousands of open file descriptors, the child inherits all of them, and the kernel must replicate internal bookkeeping structures.

Modern alternatives like vfork() and posix_spawn() skip some of this overhead by restricting what the child can do before exec, but they come with their own constraints and are less broadly used.

Beyond Process Creation

Copy-on-write is not limited to forking. File systems like Btrfs and ZFS use it for snapshots, allowing instant backups that share unchanged blocks with the original. Container image layers work the same way: a base image is shared across containers, and only writes create new storage.

The pattern is general: delay expensive operations until they are truly necessary, and share state by default. In memory management, this makes forking a practical primitive rather than a curiosity. Without copy-on-write, the Unix process model would require fundamentally different semantics or fall out of use entirely.

Instead, it remains fast enough that a Ruby web server can afford to fork a new worker on every request during development, and production systems can pre-fork dozens of workers without saturating memory. The illusion of duplication hides a system that shares aggressively and copies only when forced.