fork and exec
ELF Loading covers how a binary’s segments end up mapped into memory, but not how a new process comes to exist in the first place, nor why Unix historically splits that into two separate operations rather than one combined “spawn” call. fork duplicates an already-running process into two independent copies that continue executing from the exact same point; exec replaces a process’s own memory image in place with a different binary’s, without creating a new process at all. Neither alone is what most callers actually want (running a different program as a new process); the common pattern of calling one immediately after the other, fork then exec, is what accomplishes that, and the reason both exist as separate primitives rather than a single “run this program as a new process” call is what the rest of this article covers.
What fork duplicates
Section titled “What fork duplicates”fork gives the calling process a nearly complete copy of itself: the same code, the same data, the same open file descriptors, the same virtual address space layout, differing only in the return value fork itself produces (zero in the new child, the child’s process ID in the original parent) and in the two processes’ own identifiers from that point on. Actually copying an entire address space’s worth of physical memory on every fork call would be prohibitively expensive for what is often an extremely short-lived duplicate, particularly in the common pattern where the child immediately calls exec and discards the copied memory entirely within microseconds.
pid_t pid = fork();if (pid == 0) { // child: pid holds 0} else { // parent: pid holds the child's actual PID}Copy-on-write
Section titled “Copy-on-write”Real implementations avoid that cost through copy-on-write, already covered from the paging side: fork marks every page of the parent’s address space read-only in both the parent’s and the new child’s page tables, rather than allocating and copying physical frames for any of it up front. Both processes continue reading from the same physical memory undisturbed, and only the moment either one attempts to write to a shared page does a fault occur, at which point the kernel allocates a genuinely private physical copy for whichever process faulted and updates just that one mapping, leaving the other process’s page, and the underlying original frame, untouched. This is what makes fork cheap despite superficially appearing to duplicate an entire address space: the cost is deferred to individual pages actually modified afterward, and a child that calls exec immediately, replacing its address space before ever writing to most of what fork gave it, ends up paying for barely any of that apparent duplication at all.
What exec replaces
Section titled “What exec replaces”exec discards a process’s current code and data segments and replaces them with a new binary’s, exactly the mapping process ELF Loading already describes, but reuses the existing process rather than allocating a new one: the process ID stays the same, and so does anything the calling process explicitly arranged to survive the replacement (open file descriptors, by default, unless individually marked close-on-exec). Execution resumes at the new binary’s entry point as if the process had just started, with no return from a successful exec call at all, since the code that called it no longer exists in the process’s memory to return into; exec only returns to the caller on failure, when the replacement never happened.
execve("/bin/ls", argv, envp);// unreachable on success: this process is now running /bin/lsperror("execve failed");Why two calls instead of one
Section titled “Why two calls instead of one”Separating creation from replacement gives the child process a window, between the fork returning and the exec call, to adjust its own environment before the new program ever starts running: closing an inherited file descriptor it doesn’t want the new program to see, duplicating a pipe end onto a specific standard-stream descriptor to redirect input or output, or dropping privileges, are all ordinary operations performed in that window using the process’s already-familiar system call interface, rather than requiring a single combined spawn-style call to accept parameters for every such adjustment a caller might ever want. A shell implementing a pipeline (ls | grep foo) is the textbook example: it forks once per stage, rewires each child’s standard descriptors to the pipe connecting it to its neighbor entirely within that child, and only then calls exec, work that has no natural place to happen if process creation and program replacement were a single inseparable step.
Implementation notes
Section titled “Implementation notes”A kernel implementing this pair has to be careful that copy-on-write’s read-only page-table marking doesn’t leak into what the processes themselves observe: a page genuinely writable before fork, from either process’s own perspective after it, must still behave as writable, with the read-only marking existing purely as an internal mechanism the copy-on-write fault handler resolves transparently, not as a permission either process’s own code should ever be able to detect directly. exec’s address-space replacement similarly has to tear down the old mappings and TLB entries completely before establishing the new binary’s, since a stale mapping surviving the switch would let old and new code coexist in the same address space in a way that violates exec’s whole point.
References
Section titled “References”- ^ W. R. Stevens and S. A. Rago, Advanced Programming in the UNIX Environment, Chapter 8: the standard reference on the fork/exec process model and its variants.
See also
Section titled “See also”- Paging & Virtual Memory: the copy-on-write mechanism that makes fork inexpensive despite appearing to duplicate an entire address space.
- ELF Loading: what actually happens inside exec, once the process it’s replacing already exists.
- Process Termination, Zombies, and wait(): the other half of the process life cycle this article’s creation half is completed by.