fork, exec, and Process Creation in UNIX
Overview
UNIX creates new processes through two system calls:
fork(): creates an exact child copy of the calling process. The child inherits the parent’s memory, file descriptors, and execution context.execve(): replaces the calling process’s address space with a new programme loaded from disk. The PID, file descriptors, and environment survive; everything else is replaced.
Together, fork + exec is the standard pattern: the shell forks, and the child execs the desired command.
fork() in detail
pid_t fork(void);
fork() returns twice — once in the parent (returning the child’s PID) and once in the child (returning 0). This is the fundamental UNIX idiom; every process (except PID 1) was created by fork().
The kernel must duplicate:
| Resource | How it is duplicated |
|---|---|
| Address space | The parent’s page table is copied. Pages are initially shared between parent and child with copy-on-write (COW) — the actual copy is deferred until either process writes to a shared page. |
| Open file descriptors | The child’s file descriptor table is a copy of the parent’s. Both share the same kernel file object, with the same file offset. A lseek() by the child affects the parent’s next read(). |
| Signal handlers | Inherited by the child. |
| Environment and current directory | Inherited. |
| Process group and session | Inherited. |
The child gets a new, unique PID. The parent-child relationship is recorded in the process tree.
Copy-on-write efficiency
Naively copying the entire address space (potentially gigabytes) on every fork() would be ruinous. COW avoids this:
- During
fork(), the kernel marks all pages in both parent and child as read-only. - No physical copy is made — both processes read from the same physical frames.
- When either process attempts a write, the MMU raises a page fault (protection fault — writing a read-only page).
- The page-fault handler sees that the page is a COW page. It allocates a new physical frame, copies the page’s data, updates the faulting process’s page table to point to the new frame (now read-write), and retries the faulting instruction.
- The other process continues to share the original frame (also now marked read-write again if the writer was the only other sharer).
The common fork()-immediately-exec() case is optimised further: exec() will replace the entire address space anyway, so copying is entirely wasted. Modern kernels use vfork() or clone() with flags that avoid even the COW setup.
execve() in detail
int execve(const char *pathname, char *const argv[], char *const envp[]);
If successful, execve() does not return — the calling process is replaced. The old address space is discarded; the kernel:
- Validates that the file at
pathnameexists, is executable, and the caller has execute permission. - Optionally, if the file has a SetUID or SetGID bit, changes the process’s effective UID/GID.
- Loads the executable’s segments into memory (text at the virtual address specified by the ELF headers, data, BSS).
- Sets up a new stack with the
argvandenvpstrings, and auxiliary vectors. - Sets the program counter to the executable’s entry point.
File descriptors survive exec() unless they were opened with the FD_CLOEXEC flag. This allows the shell to set up pipes and redirections before the child execs — the child inherits the file descriptors and uses them transparently.
The fork–exec pattern
pid_t pid = fork();
if (pid == 0) {
// Child
close(pipefd[0]); // close read end
dup2(pipefd[1], STDOUT_FILENO);
execlp("ls", "ls", "-l", NULL);
// execlp never returns on success
perror("exec failed");
_exit(1);
} else {
// Parent
close(pipefd[1]); // close write end
wait(NULL); // reap the child
}
Summary
fork()duplicates the process;exec()replaces the programme.- Copy-on-write makes
fork()cheap — no physical copy of memory until a write occurs. - File descriptors survive
exec(), enabling I/O redirection. - The shell’s job is to fork, set up pipes and redirections, and exec the command.