skip to content

Explain the fork-then-exec model of creating a new process, including what copy-on-write does, and why creating a process is normally more expensive than creating a thread.

level: middleimportance: should knowfreq 50%

answer

  1. fork = duplicate, exec = replace image
  2. the gap between them is where redirection happens
  3. copy page tables, not pages
  4. write fault copies one page
  5. only the calling thread survives a fork

basics

~20 s

Fork clones the calling process, giving the child a duplicate address space and handle table; exec then replaces the child's program image with a new one. Copy-on-write makes the duplicate lazy — pages are shared read-only until written. Processes still cost more than threads: page tables, handle tables and kernel bookkeeping versus one stack.

solid answer

~60 s

The classic model splits creation into two steps. **Fork** duplicates the calling process: the child gets a copy of the address space, the open handles and the execution position, and both sides return from the same call distinguished only by the return value. **Exec** then discards the child's memory image and loads a different program into it. The split exists because it gives you a place to stand between the two: after fork and before exec you are already the child, so you can redirect its standard streams, drop privileges or close handles — which is exactly how shells build pipelines. Naively, fork would copy the whole address space. **Copy-on-write** avoids that: parent and child initially share the same physical pages marked read-only, and only the pages actually written get duplicated, one page at a time, on the faulting write. Since most children exec immediately, most pages are never copied. Even so, a process costs more than a thread: new page tables, a new handle table, kernel accounting, and later a costlier context switch. A thread needs a stack and a control block inside an address space that already exists.

code

text · 9 lines
text
pid = fork()
if pid == 0:            # child
    redirect stdout -> pipe_write
    close(pipe_read)
    drop_privileges()
    exec("program", args)   # never returns on success
else:                   # parent
    close(pipe_write)
    wait(pid)

go deeper

for a junior

Know the sequence: fork duplicates, exec replaces the program, and copy-on-write means the duplication is lazy.

for a middle

Explain why the two-step split exists (redirection, privilege drop between the calls) and what copy-on-write actually costs per written page.

for a senior

Bring in the operational consequences: multi-threaded fork hazards, pre-forked worker pools, never spawning a process per request on a hot path.

for a principal

Compare the fork/exec model with single-call spawn models — interface size, safety in threaded programs, container and sandbox setup — and where process-creation cost forces pooling.

## The two-step model Many operating systems create processes in two moves rather than one. **Fork** produces a near-identical duplicate of the calling process. The child receives a copy of the parent's address space, a copy of the open-handle table, the same working directory and credentials, and resumes at exactly the same instruction. The only difference visible to the code is the return value: the parent learns the child's identifier, the child learns it is the child. **Exec** keeps the process — same identifier, same handle table, same credentials — but throws away its memory image and loads a different executable in its place. Exec does not return on success; the old program is simply gone. Combining them gives "create a process running program X": fork, then in the child, exec X. ## Why split it at all The gap between the two calls is the point. Between fork and exec the child is running your code with the new process's identity, so it can: - redirect its standard input/output to a pipe or file, which is exactly how a shell wires `a | b`; - close handles the new program should not inherit; - drop privileges, change working directory, set resource limits, join a sandbox. A single all-in-one "spawn program with these settings" call has to expose every one of those knobs as parameters. The two-step model expresses them as ordinary code. The cost is that the composed operation is not atomic and inherits some awkward semantics — most notably, what a fork means when the parent has many threads. ## Copy-on-write Duplicating an address space eagerly would be absurd for a parent holding gigabytes. **Copy-on-write** makes the duplication lazy: 1. Fork copies the page *tables*, not the pages. 2. Every writable page in both processes is marked read-only, and the physical page is referenced by both. 3. A write to such a page traps. The kernel allocates a fresh physical page, copies the contents, remaps it writable for the writer, and lets the instruction retry. So the copy cost is proportional to the pages actually modified, not to the address space size. When the child execs immediately — the common case — almost nothing is copied and the whole image is discarded anyway. This also means fork's cost scales with the *number of pages mapped* (page-table work) rather than with the bytes resident, and that a large, write-heavy parent can pay a surprising trickle of copy faults after forking. ## Forking a multi-threaded parent Only the calling thread survives into the child; the others simply do not exist there. Anything they were holding — a lock, a half-updated data structure — is frozen in that state in the child's copy. That is why forking from a multi-threaded program and then doing anything other than exec is a well-known hazard: the child can deadlock on a mutex whose owning thread never existed. The safe discipline is fork, then exec, with minimal work in between. ## Why a process costs more than a thread Creating a thread inside an existing process means: allocate a stack, allocate a small control block, register it with the scheduler. Creating a process means at minimum: allocate and initialize page tables, build a handle table, set up accounting and identity, and — if you then exec — parse an executable, map its segments, resolve dynamic libraries, and start from a cold instruction and data cache. The cost does not stop at creation: - **Switching** between processes changes address spaces, which can invalidate address-translation caches and force them to be re-populated; switching between threads of one process does not. - **Memory footprint** per process includes its own page tables and runtime, not just a stack. - **Communication** across processes copies bytes; across threads it passes a reference. A reasonable mental ordering, without pretending to exact numbers: thread creation is cheap, process creation via fork is meaningfully more expensive, and fork plus exec plus program startup is the expensive one — which is why systems that need many short-lived workers pre-fork a pool at startup and reuse it, and why a request path should never shell out per request if it can avoid it. ## The alternative model Not every system forks. The other common design is a single "create a process from this executable with this configuration" call that never duplicates the parent. It avoids the multi-threaded-fork hazard and the copy-on-write machinery entirely, at the cost of a larger, more parameterized interface. Knowing both models — and that fork's cheapness is entirely due to laziness — is the level of understanding an interviewer is after.

  • Why is calling fork from a multi-threaded process and then doing real work in the child considered dangerous?
    Only the calling thread exists in the child, but the child inherits a snapshot of all shared state — including locks that other threads held at the instant of the fork. Those locks will never be released, so the child can deadlock on library internals such as the allocator or logging. The safe pattern is to exec immediately, or to use a spawn-style call instead.
  • If copy-on-write makes fork cheap, what still scales with the size of the parent?
    The page-table work: fork must duplicate the mapping structures and mark pages read-only, which grows with the number of mapped pages. A very large parent also pays a stream of copy faults afterwards for every page it writes, and each fault is a trap plus a page copy.

saying these in an interview costs you the question

  • Saying fork physically copies the entire address space on modern systems.
  • Believing all threads of the parent appear in the child.
  • Thinking exec creates a new process — it replaces the image of the existing one.
  • Claiming process creation is cheap because copy-on-write exists, ignoring page tables, exec and program startup.
  • Saying the child of a fork starts at the beginning of the program rather than at the fork's return.

context