Skip to main content

Creating Processes: fork, exec, and Process Trees

What This Concept Is

Unix intentionally splits "make a new process" from "replace what a process is running". The split gives you two primitives:

  • fork() -- duplicate the calling process. Both the parent and the freshly cloned child continue executing from the same point. fork returns the child's PID in the parent, and 0 in the child.
  • execve(path, argv, envp) -- replace the caller's current program image. The PID stays the same; the code, data, heap, and stack are torn down and rebuilt from the new binary.

wait / waitpid lets a parent block until a child exits and collect its exit status; otherwise the child stays a zombie.

Because every process (except PID 1) has a parent, the set of all live processes forms the process tree.

Why It Matters Here

The fork / exec split is the reason Unix shells are tiny. Redirection, piping, setting environment variables, changing working directory -- all of those happen in the child between fork and exec, without touching the parent and without baking flags into a single "spawn" call. Once you see this, most system-programming design makes sense.

This concept is also the doorway to process tree reasoning: who reaps whom, orphan adoption by PID 1, and process groups.

Concrete Example

A minimal shell that runs ls -l:

pid_t pid = fork();
if (pid == 0) {
// child
execlp("ls", "ls", "-l", (char *)NULL);
_exit(1); // only reached if exec fails
} else {
// parent
int status;
waitpid(pid, &status, 0);
}

The process tree after fork but before exec:

shell (pid=100)
└── shell-copy (pid=101) <- will soon exec into /bin/ls

After exec in the child, the PID 101 is unchanged but the code is now ls. After ls exits and the parent's waitpid returns, the tree returns to just the shell.

Adding a pipe ls -l | wc -l:

shell (100)
├── ls (101) writes fd=1 to pipe
└── wc (102) reads fd=0 from pipe

The shell forks twice, wires up pipe file descriptors in each child before exec, then waits for both.

Common Confusion / Misconception

"fork is slow because it copies the whole address space." Modern kernels use copy-on-write: the child gets the same physical page-table mappings marked read-only; real copies happen only when a page is written. This is why fork followed immediately by exec is cheap -- nothing was actually copied before the new image replaced it all.

"exec creates a new process." No. exec replaces the running program inside the same process. Same PID, new code. This matters in tracing tools that key on PID.

How To Use It

When reading a shell or init system, ask:

  1. Where is the fork? That marks the point where the tree branches.
  2. Which side, parent or child, is the path with pid == 0?
  3. What happens between fork and exec in the child? Those lines are "configure the child's environment."
  4. Who calls wait? An unwaited-for child becomes a zombie.

Check Yourself

  1. Why does fork return twice?
  2. What would happen if a shell called exec without fork first?
  3. Why does a pipeline of two commands require two forks?

Mini Drill or Application

Predict the process tree after each of:

  1. ./a runs fork(); fork(); with no exec or wait. How many processes exist?
  2. A shell runs sleep 100 & then exits without waiting. What adopts the sleeper?
  3. strace -f -e trace=fork,execve,wait4 bash -c "ls | wc -l" -- list every expected line.

Read This Only If Stuck