How Does the Fork and Exec Model Spawn Child Processes?
In Unix-like operating systems, the fork-and-exec model is the standard mechanism for creating and running new programs. Rather than launching a new program in a single step, the operating system splits the task into two distinct system calls: fork, which duplicates the currently running process, and exec, which replaces the duplicated process's memory with a new program. This separation gives shells and other programs a clean window to configure input, output, and environment settings before the new program begins executing.
The Role of fork()
When a process calls fork(), the kernel creates an
almost identical copy of the calling process. The original process is
called the parent, and the newly created copy is called the child.
Both processes continue executing from the exact point where
fork() returned, but they can tell themselves apart by
checking the return value of the system call:
- In the parent process,
fork()returns the process ID (PID) of the newly created child. - In the child process,
fork()returns0. - On failure,
fork()returns-1to the parent, and no child process is created.
Modern Unix-like systems use copy-on-write (COW)
optimization for fork(). Instead of immediately copying all
of the parent's memory pages for the child, the kernel marks the shared
pages as read-only. A physical copy of a memory page is only made if
either the parent or the child attempts to modify it. This makes
fork() fast and memory-efficient, especially when
exec() is called shortly afterward.
The Role of exec()
The exec family of system calls (such as
execve, execl, and execvp) does
not create a new process. Instead, it replaces the current process's
memory space—including its code, data, heap, and stack—with a new
program loaded from an executable file.
When the child process calls exec():
- The kernel locates and validates the target executable binary.
- The child's existing address space is discarded and replaced with the new program's segments.
- Execution starts at the new program's entry point (such as
main()in a C program). - The process ID (PID) remains the same as the child's PID assigned
during
fork().
If exec() succeeds, it never returns to the calling code
because that code no longer exists in memory. If it fails (for example,
if the file does not exist or lacks execute permissions), it returns
-1, and the child process typically exits.
Why Separate fork() and
exec()?
Splitting process creation into two steps provides significant
flexibility. Between calling fork() and calling
exec(), the child process is still running the parent's
code and can modify its own execution environment without affecting the
parent.
Common tasks performed in this window include:
- I/O Redirection: Closing standard input or output
and reopening them to files or pipes (such as
ls > output.txtin a shell). - Working Directory and Permissions: Changing the current working directory or dropping root privileges before running an untrusted binary.
- Environment Variables: Modifying or clearing environment variables passed to the new program.
- Signal Handling: Resetting signal handlers to their default behavior.
Waiting for Completion
After spawning the child, the parent process often calls
wait() or waitpid() to pause until the child
finishes executing and to retrieve its exit status. Reading the exit
status also allows the kernel to clean up the child's remaining process
table entry, preventing it from becoming a zombie process.