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() returns 0.
  • On failure, fork() returns -1 to 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():

  1. The kernel locates and validates the target executable binary.
  2. The child's existing address space is discarded and replaced with the new program's segments.
  3. Execution starts at the new program's entry point (such as main() in a C program).
  4. 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.txt in 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.