Operating Systems · Processes and Threads

What fork Copies, and Why Copy-on-Write Defers Almost All of It

How the kernel duplicates an address space by sharing page frames read-only, what a write does to break the sharing, and why fork without exec is expensive.

The fork contract is unusually generous. The child receives a copy of the parent’s entire address space: text, data, heap, stack, and every mapping the parent had established, including anonymous memory, file-backed mappings, and shared segments. Each mapping is inherited with its access attributes, so a MAP_PRIVATE region stays private and a MAP_SHARED region stays shared. On the face of it, a process holding two gigabytes of resident memory should therefore cost two gigabytes to duplicate. It does not, and the gap between the contract and the cost is one of the older tricks in the kernel.

Duplicating Without Copying

Physical pages are not duplicated at fork time. The kernel copies the page tables, not the frames they point to, and marks every private writable mapping in both copies read-only. Both processes now translate the same physical frames through different page tables. The fork cost becomes proportional to the number of populated page-table entries rather than to the number of resident bytes, which for a large sparse address space is a difference of orders of magnitude.

The hardware makes this work through the protection bits. When either process writes to a shared frame, the write faults because the page-table entry lacks write permission. The fault handler recognizes the address as a copy-on-write page, allocates a fresh frame, copies the contents of the original, installs it in the faulting process’s page table with write permission, and resumes the instruction. The other process keeps the original frame, still marked read-only if it is still shared, or writable if it has become the sole owner.

Refcounting is what decides the last step. Each frame carries a count of how many mappings reference it. When the count is one, the faulting process is the sole owner, and the kernel can install a writable mapping of the existing frame rather than allocating and copying. A parent that writes to a region the child never touched hits this path, and so does a child after the parent has exited. A large region written once by one process therefore costs a fault per page but not a copy per page.

The page tables themselves are not free. A process with a large address space has a multi-level page table, and fork must allocate and populate a corresponding tree for the child. On a 64-bit x86 system one lowest-level page-table page maps 512 pages, or two megabytes, so a fully populated two-gigabyte region costs about a thousand such pages. The cost scales with the number of populated entries, so a process that has touched memory sparsely across a wide range pays for the mapping structure even though it pays nothing for the frames. This is why fork on a process with a large but mostly untouched address space is cheaper than one might fear and more expensive than a vfork-style handoff.

What Is Not Shared, and What Is

The address space is shared copy-on-write; the rest of the execution context is copied outright. The file descriptor table is duplicated, so both processes hold descriptors referring to the same open file descriptions, which means they share the file offset. A read in the child advances the offset seen by the parent. This surprises programs that use fork to parallelize reading a single descriptor, and the usual remedy is to open the file separately in each process or to use positional reads that do not depend on the shared offset.

Memory mappings created with MAP_SHARED are not copied at all. Both processes map the same frames and see each other’s writes, because the mapping was never private. Mappings created with MAP_PRIVATE are the copy-on-write case. The distinction is a property of the mapping, established at mmap time, not something fork decides.

Signal dispositions are inherited, but the set of pending signals is empty in the child. Alarm timers and interval timers do not survive. The child gets a new process ID and inherits its group and session membership unchanged, so it stays in the parent’s group and is not a group leader, since its process ID does not equal any existing group identifier. The group rules behind that inheritance are set out in Process Groups, Sessions, and the Controlling Terminal. Resource limits are inherited. The accumulated CPU time counters start at zero. Environment and argument vectors are already in the address space and are therefore copied along with everything else, subject to copy-on-write.

One inherited item deserves separate mention: the child has exactly one thread. If the parent was multithreaded, only the calling thread survives in the child, and any mutex that another thread held at the moment of the call is still held in the child, with no thread that could ever release it. The POSIX contract permits only async-signal-safe calls between fork and exec for this reason, and the practical rule for a multithreaded program is to avoid fork entirely or to follow it immediately with exec.

The Cost of Not Executing

The design assumes fork will be followed by exec. In the shell’s model, the child duplicates the parent and then replaces its image, and the copy-on-write pages are discarded before any of them is written. Almost no copying happens, and the page-table duplication is the only real work.

A program that forks and then keeps running in the child pays the deferred cost as it writes. Each first write to a shared page triggers a fault, an allocation, and a copy of one page, typically four kilobytes on common architectures. The faults are spread over the child’s execution, so the cost appears as a modest increase in minor fault counts rather than a single pause. For a process that immediately overwrites most of its heap, the total work approaches what an eager copy would have cost, but it is paid in small increments and interleaved with useful execution.

vfork was introduced to remove even the page-table duplication for the fork-and-exec case. The child shares the parent’s address space and the parent is suspended until the child calls exec or exits. The contract is severe: the child must not modify memory, must not return from the function that called vfork, and must not call anything but exec or _exit. The last restriction is what makes vfork usable in threaded programs: the child has the parent’s address space but only one thread, so calling into the C library before exec would touch locks that no surviving thread can release. Modern systems provide posix_spawn, which packages the fork-and-exec sequence, including the file descriptor and signal setup, into a single call that the kernel or the C library can implement without duplicating an address space at all.

The alternative design, taken by Windows, is a creation call that builds a new process from an image rather than duplicating an existing one, with handle inheritance specified explicitly. The Unix model makes the child a copy of the parent by default and requires exec to change the image; the Windows model makes the image the starting point and requires explicit inheritance for anything shared. The Unix model gives shell pipelines and redirection for free, because the child inherits the descriptors it needs. The Windows model gives a smaller default attack surface, because a new process does not silently inherit the parent’s entire environment.

Limits and Failure Paths

fork fails with EAGAIN when the kernel cannot allocate the structures the child needs or when one of the per-user, system-wide or cgroup limits on the number of processes and threads is reached, and with ENOMEM when memory for the page tables cannot be obtained. On systems with overcommit disabled or with strict accounting, the allocation of the address space is charged against a commit limit, so a process with a large virtual size can fail to fork even though most of that size is never touched.

The RLIMIT_NPROC limit caps the number of processes and threads a real user ID may own, and it is checked at fork. Threads created with the sharing variant of clone count against the same limit on Linux, which is why a program that creates many threads can hit a process limit despite creating only one process.

A subtle failure mode involves file descriptors marked close-on-exec. fork duplicates them like any other descriptor; the flag takes effect only at exec. A child that forks again before execing will pass the descriptors along, so a long chain of forks can accumulate descriptors that the original author expected to be closed. Setting the flag at the point of creation, rather than just before exec, is the practice that survives re-forking and library code that forks on the caller’s behalf.

The interaction between fork and threads is where most real defects appear, and the mechanism that lets a program create a thread without duplicating an address space is the subject of What Threads Share and What They Own. The two calls are the same kernel operation with different flags, which is why the boundary between them is thinner than the API suggests.

References

  1. fork(2) — create a child process — Linux man-pages project
  2. The UNIX Time-Sharing System — Ritchie and Thompson, Communications of the ACM, 1974
  3. Operating Systems: Three Easy Pieces — Interlude: Process API — Arpaci-Dusseau and Arpaci-Dusseau
  4. POSIX.1-2017, System Interfaces — fork() — IEEE Std 1003.1-2017